[Videodec2] Decode guest access units into pitched NV12 frames

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
This commit is contained in:
ParantezTech 2026-10-08 17:26:51 +03:00
parent 6cc753e0fc
commit a1c0d7d36d
2 changed files with 578 additions and 52 deletions

View File

@ -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; }
}
/// <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.
@ -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<byte[]?> _workChannel =
Channel.CreateUnbounded<byte[]?>(new UnboundedChannelOptions
Channel.CreateBounded<byte[]?>(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
}
}
/// <summary>Hands one Annex-B access unit to the decode worker and returns immediately.</summary>
public void EnqueueAccessUnit(byte[] accessUnit)
public bool EnqueueAccessUnit(byte[] accessUnit)
{
_workChannel.Writer.TryWrite(accessUnit);
return _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()
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;
/// <summary>Non-blocking: true exactly once per frame the worker has produced, in order.</summary>
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}");
}
}
}

View File

@ -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<ulong, Videodec2Decoder?> Decoders = new();
private static readonly ConcurrentDictionary<ulong, Videodec2DecodedFrame> 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<byte> 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)
{