From a1c0d7d36d1186a3e5b87455eb0249863ab4a4e6 Mon Sep 17 00:00:00 2001 From: ParantezTech <12572227+par274@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:26:51 +0300 Subject: [PATCH] [Videodec2] Decode guest access units into pitched NV12 frames Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com> --- src/SharpEmu.Libs/Codec/Videodec2Decoder.cs | 312 ++++++++++++++++++- src/SharpEmu.Libs/Codec/Videodec2Exports.cs | 318 +++++++++++++++++--- 2 files changed, 578 insertions(+), 52 deletions(-) diff --git a/src/SharpEmu.Libs/Codec/Videodec2Decoder.cs b/src/SharpEmu.Libs/Codec/Videodec2Decoder.cs index 02fdbc1..7c3c578 100644 --- a/src/SharpEmu.Libs/Codec/Videodec2Decoder.cs +++ b/src/SharpEmu.Libs/Codec/Videodec2Decoder.cs @@ -7,6 +7,21 @@ using SharpEmu.Libs.VideoOut; namespace SharpEmu.Libs.Codec; +internal sealed record Videodec2DecodedFrame( + byte[] Nv12, + uint Width, + uint Height, + uint Pitch, + bool ErrorFrame) +{ + public ulong Pts { get; init; } = ulong.MaxValue; + public ulong Dts { get; init; } = ulong.MaxValue; + public ulong AttachedData { get; init; } + public bool KeyFrame { get; init; } + public uint Profile { get; init; } + public uint Level { get; init; } +} + /// /// Owns one FFmpeg H.264 decode session for a single sceVideodec2 decoder /// handle, feeding it pre-demuxed Annex-B access units from guest memory. @@ -29,6 +44,8 @@ internal sealed unsafe class Videodec2Decoder : IDisposable // Enough lookahead to absorb decode jitter without adding visible latency. private const int FrameQueueCapacity = 4; + private const int WorkQueueCapacity = 64; + // Fallback when the stream doesn't declare a usable framerate. private const double FallbackFps = 30.0; @@ -45,12 +62,12 @@ internal sealed unsafe class Videodec2Decoder : IDisposable 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 + Channel.CreateBounded(new BoundedChannelOptions(WorkQueueCapacity) { + FullMode = BoundedChannelFullMode.Wait, SingleReader = true, - SingleWriter = true, + SingleWriter = false, }); // Bounded and blocking-on-full: the backpressure that keeps decode paced to playback. @@ -74,6 +91,10 @@ internal sealed unsafe class Videodec2Decoder : IDisposable private uint _lastWidth; private uint _lastHeight; + private long _workerInputCount; + private long _workerFrameCount; + private static readonly bool TraceEnabled = IsTraceEnabled(); + private Videodec2Decoder(AVCodecContext* codecContext, AVFrame* frame, AVPacket* packet) { _codecContext = codecContext; @@ -106,18 +127,22 @@ internal sealed unsafe class Videodec2Decoder : IDisposable var codec = ffmpeg.avcodec_find_decoder(AVCodecID.AV_CODEC_ID_H264); if (codec == null) { + Trace("create failed: H.264 decoder not found"); return null; } codecContext = ffmpeg.avcodec_alloc_context3(codec); if (codecContext == null) { + Trace("create failed: avcodec_alloc_context3 returned null"); return null; } + codecContext->flags |= ffmpeg.AV_CODEC_FLAG_COPY_OPAQUE; if (ffmpeg.avcodec_open2(codecContext, codec, null) < 0) { ffmpeg.avcodec_free_context(&codecContext); + Trace("create failed: avcodec_open2 rejected H.264 decoder"); return null; } @@ -136,14 +161,18 @@ internal sealed unsafe class Videodec2Decoder : IDisposable } ffmpeg.avcodec_free_context(&codecContext); + Trace("create failed: frame or packet allocation returned null"); return null; } - return new Videodec2Decoder(codecContext, frame, packet); + var decoder = new Videodec2Decoder(codecContext, frame, packet); + Trace("create succeeded: FFmpeg H.264 worker and scheduler started"); + return decoder; } 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. + Trace($"create failed: {ex.GetType().Name}: {ex.Message}"); if (codecContext != null) { ffmpeg.avcodec_free_context(&codecContext); @@ -174,18 +203,252 @@ internal sealed unsafe class Videodec2Decoder : IDisposable } } - /// Hands one Annex-B access unit to the decode worker and returns immediately. - public void EnqueueAccessUnit(byte[] accessUnit) + public bool EnqueueAccessUnit(byte[] accessUnit) { - _workChannel.Writer.TryWrite(accessUnit); + return _workChannel.Writer.TryWrite(accessUnit); } /// Queues an end-of-stream drain: flush FFmpeg and emit one more buffered picture, if any. - public void RequestDrain() + public bool RequestDrain() { - _workChannel.Writer.TryWrite(null); + return _workChannel.Writer.TryWrite(null); } + public bool DecodeAccessUnit( + byte[] accessUnit, + ulong pts, + ulong dts, + ulong attachedData, + out Videodec2DecodedFrame? decodedFrame) + { + decodedFrame = null; + lock (_gate) + { + if (_disposed || accessUnit.Length == 0) + { + return false; + } + + ffmpeg.av_packet_unref(_packet); + ffmpeg.av_frame_unref(_frame); + var packetResult = ffmpeg.av_new_packet(_packet, accessUnit.Length); + if (packetResult < 0) + { + Trace($"decode failed: av_new_packet={packetResult}"); + return false; + } + + try + { + fixed (byte* source = accessUnit) + { + Buffer.MemoryCopy(source, _packet->data, accessUnit.Length, accessUnit.Length); + } + + _packet->pts = ToAvTimestamp(pts); + _packet->dts = ToAvTimestamp(dts); + + var havePendingFrame = false; + var sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet); + if (sendResult == ffmpeg.AVERROR(ffmpeg.EAGAIN)) + { + var pendingResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame); + if (pendingResult < 0) + { + Trace($"decode failed: receive-before-resend={pendingResult}"); + return false; + } + + havePendingFrame = true; + sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet); + } + + if (sendResult < 0) + { + Trace($"decode failed: avcodec_send_packet={sendResult}"); + return false; + } + + if (!havePendingFrame) + { + var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame); + if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) || + receiveResult == ffmpeg.AVERROR_EOF) + { + return true; + } + + if (receiveResult < 0) + { + Trace($"decode failed: avcodec_receive_frame={receiveResult}"); + return false; + } + } + + decodedFrame = ConvertFrameToNv12Locked(pts, dts, attachedData); + return decodedFrame is not null; + } + finally + { + ffmpeg.av_frame_unref(_frame); + ffmpeg.av_packet_unref(_packet); + } + } + } + + public bool FlushOutput(out Videodec2DecodedFrame? decodedFrame) + { + decodedFrame = null; + lock (_gate) + { + if (_disposed) + { + return false; + } + + ffmpeg.av_frame_unref(_frame); + var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame); + if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) || + receiveResult == ffmpeg.AVERROR_EOF) + { + return true; + } + + if (receiveResult < 0) + { + Trace($"flush failed: avcodec_receive_frame={receiveResult}"); + return false; + } + + try + { + decodedFrame = ConvertFrameToNv12Locked(); + return decodedFrame is not null; + } + finally + { + ffmpeg.av_frame_unref(_frame); + } + } + } + + public void Reset() + { + lock (_gate) + { + if (_disposed) + { + return; + } + + ffmpeg.avcodec_flush_buffers(_codecContext); + ffmpeg.av_packet_unref(_packet); + ffmpeg.av_frame_unref(_frame); + } + } + + private Videodec2DecodedFrame? ConvertFrameToNv12Locked( + ulong fallbackPts = ulong.MaxValue, + ulong fallbackDts = ulong.MaxValue, + ulong attachedData = 0) + { + if (_frame->width <= 0 || _frame->height <= 0) + { + return null; + } + + var width = checked((uint)_frame->width); + var height = checked((uint)_frame->height); + uint pitch; + ulong lumaBytes; + ulong requiredBytes; + try + { + pitch = checked((width + 255u) & ~255u); + lumaBytes = checked((ulong)pitch * height); + var chromaRows = ((ulong)height + 1UL) / 2UL; + requiredBytes = checked(lumaBytes + ((ulong)pitch * chromaRows)); + } + catch (OverflowException) + { + return null; + } + + if (requiredBytes > int.MaxValue) + { + return null; + } + + _swsContext = ffmpeg.sws_getCachedContext( + _swsContext, + _frame->width, + _frame->height, + (AVPixelFormat)_frame->format, + _frame->width, + _frame->height, + AVPixelFormat.AV_PIX_FMT_NV12, + ffmpeg.SWS_FAST_BILINEAR, + null, + null, + null); + if (_swsContext == null) + { + return null; + } + + var nv12 = new byte[(int)requiredBytes]; + fixed (byte* destination = nv12) + { + var destinationPlanes = new byte*[4] + { + destination, + destination + checked((int)lumaBytes), + null, + null, + }; + var destinationStrides = new int[4] + { + checked((int)pitch), + checked((int)pitch), + 0, + 0, + }; + var rows = ffmpeg.sws_scale( + _swsContext, + _frame->data, + _frame->linesize, + 0, + _frame->height, + destinationPlanes, + destinationStrides); + if (rows != _frame->height) + { + return null; + } + } + + return new Videodec2DecodedFrame( + nv12, + width, + height, + pitch, + (_frame->flags & ffmpeg.AV_FRAME_FLAG_CORRUPT) != 0) + { + Pts = FromAvTimestamp(_frame->pts, fallbackPts), + Dts = FromAvTimestamp(_frame->pkt_dts, fallbackDts), + AttachedData = attachedData, + KeyFrame = (_frame->flags & ffmpeg.AV_FRAME_FLAG_KEY) != 0, + Profile = _codecContext->profile > 0 ? (uint)_codecContext->profile : 0, + Level = _codecContext->level > 0 ? (uint)_codecContext->level : 0, + }; + } + + private static long ToAvTimestamp(ulong timestamp) => + timestamp <= long.MaxValue ? (long)timestamp : ffmpeg.AV_NOPTS_VALUE; + + private static ulong FromAvTimestamp(long timestamp, ulong fallback) => + timestamp == ffmpeg.AV_NOPTS_VALUE || timestamp < 0 ? fallback : (ulong)timestamp; + /// Non-blocking: true exactly once per frame the worker has produced, in order. public bool TryConsumeProtocolReadySignal(out uint width, out uint height) { @@ -233,15 +496,28 @@ internal sealed unsafe class Videodec2Decoder : IDisposable return; } + var inputOrdinal = Interlocked.Increment(ref _workerInputCount); 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 (ShouldTrace(inputOrdinal)) + { + Trace( + $"worker input={inputOrdinal} kind={(item is null ? "drain" : "au")} " + + $"bytes={item?.Length ?? 0} ok={decodedOk} picture={hasPicture} size={width}x{height}"); + } if (!decodedOk || !hasPicture || bgraFrame is null) { continue; } + var frameOrdinal = Interlocked.Increment(ref _workerFrameCount); + if (ShouldTrace(frameOrdinal)) + { + Trace($"worker frame={frameOrdinal} produced size={width}x{height} bytes={bgraFrame.Length}"); + } + try { // Blocks if the scheduler hasn't kept up; deliberate backpressure. @@ -564,4 +840,22 @@ internal sealed unsafe class Videodec2Decoder : IDisposable } } } + + private static bool IsTraceEnabled() + { + var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2"); + return string.Equals(value, "1", StringComparison.Ordinal) || + string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); + } + + private static bool ShouldTrace(long ordinal) => + TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0); + + private static void Trace(string message) + { + if (TraceEnabled) + { + Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}"); + } + } } diff --git a/src/SharpEmu.Libs/Codec/Videodec2Exports.cs b/src/SharpEmu.Libs/Codec/Videodec2Exports.cs index c88812f..9f6e03f 100644 --- a/src/SharpEmu.Libs/Codec/Videodec2Exports.cs +++ b/src/SharpEmu.Libs/Codec/Videodec2Exports.cs @@ -1,6 +1,7 @@ // Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +using System.Buffers.Binary; using System.Collections.Concurrent; using SharpEmu.HLE; @@ -15,10 +16,18 @@ public static class Videodec2Exports { private const int Ok = 0; + private const ulong MinimumMemoryBytes = 16UL * 1024 * 1024; + private const uint FrameBufferAlignment = 0x100; + // Null entry = TryCreate() failed; every export falls back to the stub for that handle. private static readonly ConcurrentDictionary Decoders = new(); + private static readonly ConcurrentDictionary PictureInfos = new(); private static long _nextDecoderHandle = unchecked((long)DecoderToken); + private static readonly bool TraceEnabled = IsTraceEnabled(); + private static long _decodeCallCount; + private static long _decodedPictureCount; + [SysAbiExport( Nid = "RnDibcGCPKw", ExportName = "sceVideodec2QueryComputeMemoryInfo", @@ -27,24 +36,25 @@ public static class Videodec2Exports public static int Videodec2QueryComputeMemoryInfo(CpuContext ctx) { var paramAddress = ctx[CpuRegister.Rdi]; - if (paramAddress == 0) + if (paramAddress == 0 || + !ctx.TryWriteUInt64(paramAddress + 0x08, MinimumMemoryBytes) || + !ctx.TryWriteUInt64(paramAddress + 0x10, 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); + private const int VideodecErrorStructSize = unchecked((int)0x811D0101); + private const int VideodecErrorOutputInfo = unchecked((int)0x811D010F); + private const int VideodecErrorComputeQueueId = unchecked((int)0x811D0202); // 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", @@ -52,8 +62,14 @@ public static class Videodec2Exports LibraryName = "libSceVideodec2")] public static int Videodec2AllocateComputeQueue(CpuContext ctx) { - var queueAddress = ctx[CpuRegister.Rdi]; - if (queueAddress == 0 || !ctx.TryWriteUInt64(queueAddress, ComputeQueueToken)) + var configAddress = ctx[CpuRegister.Rdi]; + var memoryInfoAddress = ctx[CpuRegister.Rsi]; + var queueAddress = ctx[CpuRegister.Rdx]; + if (configAddress == 0 || memoryInfoAddress == 0 || queueAddress == 0 || + !ctx.TryReadUInt64(memoryInfoAddress + 0x10, out var computeMemoryAddress) || + computeMemoryAddress == 0 || + !ctx.TryWriteUInt64( + queueAddress, computeMemoryAddress)) { return SetReturn(ctx, VideodecErrorInvalidArg); } @@ -61,7 +77,18 @@ public static class Videodec2Exports return SetReturn(ctx, Ok); } - // A zero size at +0x08/+0x28 makes the game skip its own arena allocation cleanly. + [SysAbiExport( + Nid = "UvtA3FAiF4Y", + ExportName = "sceVideodec2ReleaseComputeQueue", + Target = Generation.Gen5, + LibraryName = "libSceVideodec2")] + public static int Videodec2ReleaseComputeQueue(CpuContext ctx) + { + return SetReturn( + ctx, + ctx[CpuRegister.Rdi] != 0 ? Ok : VideodecErrorComputeQueueId); + } + [SysAbiExport( Nid = "qqMCwlULR+E", ExportName = "sceVideodec2QueryDecoderMemoryInfo", @@ -69,12 +96,18 @@ public static class Videodec2Exports LibraryName = "libSceVideodec2")] public static int Videodec2QueryDecoderMemoryInfo(CpuContext ctx) { + var configAddress = ctx[CpuRegister.Rdi]; 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)) + if (configAddress == 0 || memoryInfoAddress == 0 || + !ctx.TryWriteUInt64(memoryInfoAddress + 0x08, MinimumMemoryBytes) || + !ctx.TryWriteUInt64(memoryInfoAddress + 0x10, 0) || + !ctx.TryWriteUInt64(memoryInfoAddress + 0x18, MinimumMemoryBytes) || + !ctx.TryWriteUInt64(memoryInfoAddress + 0x20, 0) || + !ctx.TryWriteUInt64(memoryInfoAddress + 0x28, MinimumMemoryBytes) || + !ctx.TryWriteUInt64(memoryInfoAddress + 0x30, 0) || + !ctx.TryWriteUInt64(memoryInfoAddress + 0x38, MinimumMemoryBytes) || + !ctx.TryWriteUInt32(memoryInfoAddress + 0x40, FrameBufferAlignment) || + !ctx.TryWriteUInt32(memoryInfoAddress + 0x44, 0)) { return SetReturn(ctx, VideodecErrorInvalidArg); } @@ -99,7 +132,9 @@ public static class Videodec2Exports } var handle = unchecked((ulong)Interlocked.Increment(ref _nextDecoderHandle)); - Decoders[handle] = Videodec2Decoder.TryCreate(); + var decoder = Videodec2Decoder.TryCreate(); + Decoders[handle] = decoder; + Trace($"create handle=0x{handle:X16} real_decoder={decoder is not null}"); if (!ctx.TryWriteUInt64(decoderAddress, handle)) { @@ -111,7 +146,6 @@ public static class Videodec2Exports return SetReturn(ctx, Ok); } - // Clearing the picture-ready byte at [rdx] tells the player "no buffered pictures remain". [SysAbiExport( Nid = "l1hXwscLuCY", ExportName = "sceVideodec2Flush", @@ -120,33 +154,25 @@ public static class Videodec2Exports public static int Videodec2Flush(CpuContext ctx) { var handle = ctx[CpuRegister.Rdi]; + var outputSlotObj = ctx[CpuRegister.Rsi]; var outputInfoAddress = ctx[CpuRegister.Rdx]; - if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture)) + if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress)) { return SetReturn(ctx, VideodecErrorInvalidArg); } - if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null) + if (Decoders.TryGetValue(handle, out var decoder) && + decoder is not null && + decoder.FlushOutput(out var decodedFrame) && + decodedFrame 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(); - } + _ = TryWriteDecodedFrame( + ctx, outputSlotObj, outputInfoAddress, decodedFrame); } return SetReturn(ctx, Ok); } - // No state to reset. [SysAbiExport( Nid = "wJXikG6QFN8", ExportName = "sceVideodec2Reset", @@ -154,6 +180,12 @@ public static class Videodec2Exports LibraryName = "libSceVideodec2")] public static int Videodec2Reset(CpuContext ctx) { + var handle = ctx[CpuRegister.Rdi]; + if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null) + { + decoder.Reset(); + } + return SetReturn(ctx, Ok); } @@ -173,8 +205,6 @@ public static class Videodec2Exports 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", @@ -186,8 +216,9 @@ public static class Videodec2Exports var inputAuStruct = ctx[CpuRegister.Rsi]; var outputSlotObj = ctx[CpuRegister.Rdx]; var outputInfoAddress = ctx[CpuRegister.Rcx]; + var callOrdinal = Interlocked.Increment(ref _decodeCallCount); - if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture)) + if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress)) { return SetReturn(ctx, VideodecErrorInvalidArg); } @@ -195,6 +226,7 @@ public static class Videodec2Exports if (!Decoders.TryGetValue(handle, out var decoder) || decoder is null) { // No real decoder for this handle: stub behavior, "fed the AU, no picture". + TraceDecode(callOrdinal, $"stub handle=0x{handle:X16}"); return SetReturn(ctx, Ok); } @@ -208,35 +240,235 @@ public static class Videodec2Exports slotPtr == 0 || slotSize == 0 || slotSize > MaxPlausibleSlotBytes) { // Nothing sane to feed/fill this call; not an error. + TraceDecode(callOrdinal, "invalid access-unit or output-slot descriptor"); return SetReturn(ctx, Ok); } + _ = ctx.TryReadUInt64(inputAuStruct + 0x18, out var pts); + _ = ctx.TryReadUInt64(inputAuStruct + 0x20, out var dts); + _ = ctx.TryReadUInt64(inputAuStruct + 0x28, out var attachedData); + var auBuffer = new byte[auDataSize]; if (!ctx.Memory.TryRead(auDataPtr, auBuffer)) { + TraceDecode(callOrdinal, $"AU read failed ptr=0x{auDataPtr:X16} bytes=0x{auDataSize:X}"); return SetReturn(ctx, Ok); } - // Queues the AU and returns immediately; decode/present happen on Videodec2Decoder's own threads. - decoder.EnqueueAccessUnit(auBuffer); + var decodeOk = decoder.DecodeAccessUnit( + auBuffer, pts, dts, attachedData, out var decodedFrame); + if (ShouldTrace(callOrdinal)) + { + var prefixBytes = Math.Min(auBuffer.Length, 8); + Trace( + $"decode call={callOrdinal} au=0x{auDataPtr:X16}+0x{auDataSize:X} " + + $"head={Convert.ToHexString(auBuffer.AsSpan(0, prefixBytes))} " + + $"slot=0x{slotPtr:X16}+0x{slotSize:X} ok={decodeOk} " + + $"picture={decodedFrame is not null}"); + } - if (!decoder.TryConsumeProtocolReadySignal(out var width, out var height)) + if (!decodeOk || decodedFrame is null) { return SetReturn(ctx, Ok); } - if (!ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) || - !ctx.TryWriteUInt64(outputInfoAddress + 0x10, height) || - !ctx.Memory.TryWrite(outputInfoAddress, PictureReady)) + var copied = TryWriteDecodedFrame( + ctx, outputSlotObj, outputInfoAddress, decodedFrame); + if (copied) { - return SetReturn(ctx, Ok); + Interlocked.Increment(ref _decodedPictureCount); } + TraceDecode(callOrdinal, $"guest_copy={copied} size={decodedFrame.Width}x{decodedFrame.Height} pitch={decodedFrame.Pitch}"); return SetReturn(ctx, Ok); } - private static readonly byte[] NoPicture = [0]; - private static readonly byte[] PictureReady = [1]; + [SysAbiExport( + Nid = "NtXRa3dRzU0", + ExportName = "sceVideodec2GetPictureInfo", + Target = Generation.Gen5, + LibraryName = "libSceVideodec2")] + public static int Videodec2GetPictureInfo(CpuContext ctx) + { + var outputInfoAddress = ctx[CpuRegister.Rdi]; + var firstPictureInfoAddress = ctx[CpuRegister.Rsi]; + var secondPictureInfoAddress = ctx[CpuRegister.Rdx]; + if (outputInfoAddress == 0 || firstPictureInfoAddress == 0) + { + return SetReturn(ctx, VideodecErrorInvalidArg); + } + + if (!ctx.TryReadUInt64(outputInfoAddress, out var outputInfoSize) || + (outputInfoSize != 0x30 && outputInfoSize != 0x38)) + { + return SetReturn(ctx, VideodecErrorStructSize); + } + + if (!TryReadByte(ctx, outputInfoAddress + 0x08, out var isValid) || + !TryReadByte(ctx, outputInfoAddress + 0x0A, out var pictureCount) || + !ctx.TryReadUInt32(outputInfoAddress + 0x0C, out var codecType) || + !ctx.TryReadUInt64(outputInfoAddress + 0x20, out var pixelsAddress) || + isValid == 0 || pictureCount == 0 || pixelsAddress == 0 || codecType != 1 || + !PictureInfos.TryGetValue(pixelsAddress, out var decodedFrame)) + { + return SetReturn(ctx, VideodecErrorOutputInfo); + } + + if (!ctx.TryReadUInt64(firstPictureInfoAddress, out var requestedSize) || + (requestedSize != 104 && requestedSize != 120)) + { + return SetReturn(ctx, VideodecErrorStructSize); + } + + var picture = new byte[(int)requestedSize]; + BinaryPrimitives.WriteUInt64LittleEndian(picture, requestedSize); + picture[0x08] = 1; + BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x10), decodedFrame.Pts); + BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x18), decodedFrame.Dts); + BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x20), decodedFrame.AttachedData); + picture[0x28] = decodedFrame.KeyFrame ? (byte)1 : (byte)0; + picture[0x29] = (byte)Math.Min(decodedFrame.Profile, byte.MaxValue); + picture[0x2A] = (byte)Math.Min(decodedFrame.Level, byte.MaxValue); + BinaryPrimitives.WriteUInt32LittleEndian( + picture.AsSpan(0x2C), (decodedFrame.Width + 15u) / 16u - 1u); + BinaryPrimitives.WriteUInt32LittleEndian( + picture.AsSpan(0x30), (decodedFrame.Height + 15u) / 16u - 1u); + picture[0x34] = 1; + picture[0x4E] = 1; + picture[0x4F] = 5; + if (!ctx.Memory.TryWrite(firstPictureInfoAddress, picture)) + { + return SetReturn(ctx, VideodecErrorInvalidArg); + } + + if (secondPictureInfoAddress != 0 && + !TryWriteEmptyPictureInfo(ctx, secondPictureInfoAddress)) + { + return SetReturn(ctx, VideodecErrorStructSize); + } + + Trace( + $"picture_info buffer=0x{pixelsAddress:X16} " + + $"pts=0x{decodedFrame.Pts:X16} size={requestedSize}"); + return SetReturn(ctx, Ok); + } + + private static readonly byte[] NotAccepted = [0]; + private static readonly byte[] Accepted = [1]; + private static readonly byte[] NoPicture = [0, 0, 0, 0]; + private static readonly byte[] PictureReady = [1, 0, 1, 0]; + private static readonly byte[] ErrorPictureReady = [1, 1, 1, 0]; + + private static bool TryInitializeNoPicture( + CpuContext ctx, + ulong frameBufferAddress, + ulong outputInfoAddress) + { + return frameBufferAddress != 0 && + outputInfoAddress != 0 && + ctx.Memory.TryWrite(frameBufferAddress + 0x18, NotAccepted) && + ctx.Memory.TryWrite(outputInfoAddress + 0x08, NoPicture); + } + + internal static bool TryWriteDecodedFrame( + CpuContext ctx, + ulong frameBufferAddress, + ulong outputInfoAddress, + Videodec2DecodedFrame decodedFrame) + { + if (!ctx.TryReadUInt64(frameBufferAddress + 0x08, out var pixelsAddress) || + !ctx.TryReadUInt64(frameBufferAddress + 0x10, out var slotBytes) || + !ctx.TryReadUInt64(outputInfoAddress, out var outputInfoBytes) || + pixelsAddress == 0) + { + return false; + } + + if ((ulong)decodedFrame.Nv12.Length > slotBytes || + !ctx.Memory.TryWrite(pixelsAddress, decodedFrame.Nv12)) + { + return false; + } + + if (!ctx.TryWriteUInt32(outputInfoAddress + 0x0C, 1) || + !ctx.TryWriteUInt32(outputInfoAddress + 0x10, decodedFrame.Width) || + !ctx.TryWriteUInt32(outputInfoAddress + 0x14, decodedFrame.Pitch) || + !ctx.TryWriteUInt32(outputInfoAddress + 0x18, decodedFrame.Height) || + !ctx.TryWriteUInt64(outputInfoAddress + 0x20, pixelsAddress) || + !ctx.TryWriteUInt64(outputInfoAddress + 0x28, slotBytes)) + { + return false; + } + + if (outputInfoBytes >= 0x38 && + (!ctx.TryWriteUInt32(outputInfoAddress + 0x30, 0) || + !ctx.TryWriteUInt32(outputInfoAddress + 0x34, decodedFrame.Pitch))) + { + return false; + } + + if (!ctx.Memory.TryWrite(frameBufferAddress + 0x18, Accepted) || + !ctx.Memory.TryWrite( + outputInfoAddress + 0x08, + decodedFrame.ErrorFrame ? ErrorPictureReady : PictureReady)) + { + return false; + } + + PictureInfos[pixelsAddress] = decodedFrame; + return true; + } + + private static bool TryWriteEmptyPictureInfo(CpuContext ctx, ulong address) + { + if (!ctx.TryReadUInt64(address, out var size) || size < 40 || size > 256) + { + return false; + } + + var output = new byte[(int)size]; + BinaryPrimitives.WriteUInt64LittleEndian(output, size); + return ctx.Memory.TryWrite(address, output); + } + + private static bool TryReadByte(CpuContext ctx, ulong address, out byte value) + { + Span buffer = stackalloc byte[1]; + if (!ctx.Memory.TryRead(address, buffer)) + { + value = 0; + return false; + } + + value = buffer[0]; + return true; + } + + private static bool IsTraceEnabled() + { + var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2"); + return string.Equals(value, "1", StringComparison.Ordinal) || + string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); + } + + private static bool ShouldTrace(long ordinal) => + TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0); + + private static void TraceDecode(long ordinal, string message) + { + if (ShouldTrace(ordinal)) + { + Trace($"decode call={ordinal} {message}"); + } + } + + private static void Trace(string message) + { + if (TraceEnabled) + { + Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}"); + } + } private static int SetReturn(CpuContext ctx, int result) {