From 454826eb5886f7b0de52378355bfb4fd56eeaa1f Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 30 Oct 2026 05:15:09 +0000 Subject: [PATCH 1/3] feat(libSceVdecCore): implement the decode exports from shadPS4 ResetDecoder, SetDecodeInput, SyncDecode, GetDecodeOutput, FlushDecodeOutput, SyncDecodeWptr, TrySyncDecode and TrySyncDecodeWptr follow shadPS4's vdeccore.cpp. CreateDecoder starts a decode thread of the decoder's own that takes packet, flush and reset commands in order and decodes with the FFmpeg context it opens, and DeleteDecoder stops and joins it and frees what is still queued. GetDecodeOutput converts a picture to NV12 and copies it, as shadPS4's CopyNV12Data does, to the caller's frame buffer, else the next mapped slot, else a fallback buffer the decoder owns, writes the hardware picture info at the end of the frame buffer and fills the output info as shadPS4 does. The six exports that are stubs in shadPS4 stay unknown. The tests decode the H.264 fixture into slots, a caller buffer and the fallback, compare the visible picture with libSceVideodec2's decode of it, and cover every argument error, the empty queue, an invalid pts, flush, reset, try-sync, the write-pointer forwards and a delete with queued input. --- core/libs/prx/libSceVdecCore/Export.cpp | 359 ++++++++++++++++++- core/libs/prx/libSceVdecCore/include/Api.hpp | 96 ++++- core/libs/tests/GuestVdecCore.cpp | 289 +++++++++++++++ docs/dev/debt/libSceVdecCore.md | 5 +- 4 files changed, 720 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceVdecCore/Export.cpp b/core/libs/prx/libSceVdecCore/Export.cpp index 34b37523cf..211ee4d287 100644 --- a/core/libs/prx/libSceVdecCore/Export.cpp +++ b/core/libs/prx/libSceVdecCore/Export.cpp @@ -1,12 +1,17 @@ #include "prx/libSceVdecCore/include/Api.hpp" #include "prx/libSceVdecCore/include/Decoder.hpp" #include +#include +#include #include #include +#include +#include #include #include #include #include +#include #include #include #include "SceTypes.hpp" @@ -14,12 +19,17 @@ extern "C" { #include +#include } namespace { constexpr int VdecCoreErrorInvalidArgument = static_cast(0x80C00001); constexpr int VdecCoreErrorInvalidCodecType = static_cast(0x80C00003); +constexpr int VdecCoreErrorNoPicture = static_cast(0x80C00010); +constexpr int VdecCoreErrorFatal = static_cast(0x80C00012); +constexpr int VdecCoreErrorDecodeRunning = static_cast(0x80C00018); +constexpr std::int64_t VdecCorePtsInvalid = -1; constexpr std::uint32_t CodecHevc = 4; constexpr std::uint32_t MaxCodecType = 4; constexpr std::uint32_t MaxDecoderType = 8; @@ -28,6 +38,8 @@ constexpr std::uint32_t DefaultFrameWidth = 1920; constexpr std::uint32_t DefaultFrameHeight = 1080; constexpr std::uint32_t DefaultHorizontalAlign = 256; constexpr std::uint32_t FrameHeightAlign = 16; +constexpr std::uint32_t FrameWidthAlign = 16; +constexpr std::uint32_t OutputPitchAlign = 64; constexpr std::uint32_t FrameBufferPadding = 0x1000; constexpr std::uint32_t FrameBufferGranularity = 0x10000; constexpr std::uint32_t FrameBufferAlignment = 256; @@ -43,15 +55,52 @@ struct Geometry { std::uint32_t frameBufferSize; }; +struct Command { + enum class Type { + Packet, + Reset, + Flush, + }; + Type type = Type::Packet; + AVPacket* packet = nullptr; + std::shared_ptr> done; +}; + struct Decoder { std::uint32_t codecType = 0; Geometry geometry{}; AVCodecContext* context = nullptr; + SwsContext* scaler = nullptr; + AVFrame* decodedFrame = nullptr; std::vector mappedBlocks; std::vector frameBufferSlots; + std::uint32_t currentSlot = 0; + std::vector fallbackFrameBuffer; + std::int64_t lastPts = VdecCorePtsInvalid; + std::int64_t lastDts = VdecCorePtsInvalid; + std::uint64_t lastUserData = 0; + std::mutex mutex; + std::condition_variable inputReady; + std::condition_variable outputReady; + std::deque commands; + std::deque frames; + bool busy = false; + std::jthread worker; ~Decoder() { + { + std::lock_guard lock(mutex); + worker.request_stop(); + } + inputReady.notify_all(); + if (worker.joinable()) worker.join(); + for (auto& command : commands) av_packet_free(&command.packet); + commands.clear(); + for (auto* frame : frames) av_frame_free(&frame); + frames.clear(); + av_frame_free(&decodedFrame); avcodec_free_context(&context); + sws_freeContext(scaler); } }; @@ -99,6 +148,123 @@ std::unordered_map>::iterator Find(Decoder return found; } +Decoder& Lookup(const void* decoder, const char* caller) { + auto& state = State(); + std::lock_guard lock(state.mutex); + return *Find(state, decoder, caller)->second; +} + +void DrainFrames(Decoder& decoder) { + while (true) { + AVFrame* frame = av_frame_alloc(); + if (frame == nullptr) break; + if (avcodec_receive_frame(decoder.context, frame) != 0) { + av_frame_free(&frame); + break; + } + std::lock_guard lock(decoder.mutex); + decoder.frames.push_back(frame); + } +} + +void RunWorker(std::stop_token stop, Decoder* decoder) { + while (!stop.stop_requested()) { + Command command; + { + std::unique_lock lock(decoder->mutex); + decoder->inputReady.wait(lock, [&] { return stop.stop_requested() || !decoder->commands.empty(); }); + if (stop.stop_requested()) break; + command = std::move(decoder->commands.front()); + decoder->commands.pop_front(); + decoder->busy = true; + } + switch (command.type) { + case Command::Type::Packet: { + int result = avcodec_send_packet(decoder->context, command.packet); + if (result == AVERROR_EOF) { + avcodec_flush_buffers(decoder->context); + result = avcodec_send_packet(decoder->context, command.packet); + } + if (result == AVERROR(EAGAIN)) { + DrainFrames(*decoder); + avcodec_send_packet(decoder->context, command.packet); + } + av_packet_free(&command.packet); + DrainFrames(*decoder); + break; + } + case Command::Type::Flush: + avcodec_send_packet(decoder->context, nullptr); + DrainFrames(*decoder); + avcodec_flush_buffers(decoder->context); + command.done->store(true, std::memory_order_release); + break; + case Command::Type::Reset: + avcodec_flush_buffers(decoder->context); + { + std::lock_guard lock(decoder->mutex); + for (auto* frame : decoder->frames) av_frame_free(&frame); + decoder->frames.clear(); + decoder->currentSlot = 0; + } + command.done->store(true, std::memory_order_release); + break; + } + std::lock_guard lock(decoder->mutex); + decoder->busy = !decoder->commands.empty(); + decoder->outputReady.notify_all(); + } +} + +void RunCommand(Decoder& decoder, Command::Type type, std::uint32_t* count) { + auto done = std::make_shared>(false); + std::unique_lock lock(decoder.mutex); + decoder.commands.push_back({type, nullptr, done}); + decoder.inputReady.notify_one(); + decoder.outputReady.wait(lock, [&] { return done->load(std::memory_order_acquire); }); + if (count != nullptr) *count = static_cast(decoder.frames.size()); +} + +AVFrame* ConvertNv12(Decoder& decoder, const AVFrame& frame) { + AVFrame* converted = av_frame_alloc(); + if (converted == nullptr) throw std::runtime_error("VdecCore: cannot allocate an NV12 frame"); + converted->format = AV_PIX_FMT_NV12; + converted->width = frame.width; + converted->height = frame.height; + converted->pts = frame.pts; + converted->pkt_dts = frame.pkt_dts; + converted->opaque = frame.opaque; + if (av_frame_get_buffer(converted, 0) < 0) { + av_frame_free(&converted); + throw std::runtime_error("VdecCore: cannot allocate an NV12 frame"); + } + decoder.scaler = sws_getCachedContext(decoder.scaler, frame.width, frame.height, static_cast(frame.format), frame.width, frame.height, AV_PIX_FMT_NV12, SWS_BILINEAR, nullptr, nullptr, nullptr); + if (decoder.scaler == nullptr || sws_scale(decoder.scaler, frame.data, frame.linesize, 0, frame.height, converted->data, converted->linesize) != frame.height) { + av_frame_free(&converted); + throw std::runtime_error("VdecCore: cannot convert a decoded picture to NV12"); + } + return converted; +} + +void CopyNv12(std::uint8_t* destination, const AVFrame& frame) { + const auto width = static_cast(frame.width); + const auto height = static_cast(frame.height); + const std::size_t pitch = AlignUp(width, OutputPitchAlign); + const std::size_t alignedHeight = AlignUp(height, FrameHeightAlign); + std::uint8_t* luma = destination; + std::uint8_t* chroma = destination + pitch * alignedHeight; + const auto lumaRow = [&](std::size_t row) { return frame.data[0] + row * static_cast(frame.linesize[0]); }; + const auto chromaRow = [&](std::size_t row) { return frame.data[1] + row * static_cast(frame.linesize[1]); }; + for (std::size_t row = 0; row < height; ++row) std::memcpy(luma + row * pitch, lumaRow(row), width); + for (std::size_t row = 0; row < height / 2; ++row) std::memcpy(chroma + row * pitch, chromaRow(row), width); + for (std::size_t row = height; row < alignedHeight && height > 0; ++row) std::memcpy(luma + row * pitch, lumaRow(height - 1), width); + for (std::size_t row = height / 2; row < alignedHeight / 2 && height / 2 > 0; ++row) std::memcpy(chroma + row * pitch, chromaRow(height / 2 - 1), width); +} + +std::uint64_t PictureTime(std::int64_t decoded, std::int64_t last) { + return static_cast(decoded != AV_NOPTS_VALUE ? decoded : last); +} + } extern "C" { @@ -106,7 +272,9 @@ extern "C" { std::uint32_t VdecCoreFrameBufferSlots_nid_no_patch(const void* decoder, void** slots, std::uint32_t capacity) { auto& state = State(); std::lock_guard lock(state.mutex); - const auto& found = Find(state, decoder, "VdecCoreFrameBufferSlots")->second->frameBufferSlots; + auto& target = *Find(state, decoder, "VdecCoreFrameBufferSlots")->second; + std::lock_guard decoderLock(target.mutex); + const auto& found = target.frameBufferSlots; const auto copied = std::min(capacity, found.size()); std::copy_n(found.begin(), copied, slots); return static_cast(found.size()); @@ -125,6 +293,8 @@ int APS5_VABI sceVdecCoreCreateDecoder(const VdecCoreConfigInfo* config, const V created->codecType = config->codecType; created->geometry = FrameGeometry(*config); OpenCodec(*created); + created->decodedFrame = av_frame_alloc(); + created->worker = std::jthread(RunWorker, created.get()); void* handle = created.get(); auto& state = State(); std::lock_guard lock(state.mutex); @@ -151,13 +321,125 @@ int APS5_VABI sceVdecCoreFinalizeComputeResource() { return 0; } -int APS5_VABI sceVdecCoreFlushDecodeOutput() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVdecCoreFlushDecodeOutput(void* decoder, std::uint32_t* count, void*) { + if (decoder == nullptr) return VdecCoreErrorInvalidArgument; + RunCommand(Lookup(decoder, __func__), Command::Type::Flush, count); return 0; } -int APS5_VABI sceVdecCoreGetDecodeOutput() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVdecCoreGetDecodeOutput(void* decoder, VdecCoreFrameBuffer* frameBuffer, std::uint32_t, VdecCoreOutputInfo* output, void*) { + if (decoder == nullptr || output == nullptr) return VdecCoreErrorInvalidArgument; + auto& target = Lookup(decoder, __func__); + AVFrame* frame = nullptr; + { + std::lock_guard lock(target.mutex); + if (target.frames.empty()) { + output->frameWidth = target.geometry.width; + output->framePitch = AlignUp(target.geometry.width, OutputPitchAlign); + output->frameHeight = target.geometry.height; + output->pictureCount = 0; + output->frameBuffer = nullptr; + output->frameFormat = 0; + output->reserved0 = 0; + output->pictures[0].frameBuffer = nullptr; + output->pictures[1].frameBuffer = nullptr; + output->lumaBuffer = nullptr; + output->chromaBuffer = nullptr; + return VdecCoreErrorNoPicture; + } + frame = target.frames.front(); + target.frames.pop_front(); + } + struct Release { + AVFrame*& frame; + ~Release() { av_frame_free(&frame); } + } release{frame}; + if (frame->format != AV_PIX_FMT_NV12) { + AVFrame* converted = ConvertNv12(target, *frame); + av_frame_free(&frame); + frame = converted; + } + const auto width = static_cast(frame->width); + const auto height = static_cast(frame->height); + const std::uint32_t alignedWidth = AlignUp(width, FrameWidthAlign); + const std::uint32_t pitch = AlignUp(width, OutputPitchAlign); + const std::uint32_t alignedHeight = AlignUp(height, FrameHeightAlign); + const std::uint32_t pictureSize = pitch * alignedHeight * 3 / 2; + const std::uint32_t sarWidth = frame->sample_aspect_ratio.num > 0 ? static_cast(frame->sample_aspect_ratio.num) : 1; + const std::uint32_t sarHeight = frame->sample_aspect_ratio.den > 0 ? static_cast(frame->sample_aspect_ratio.den) : 1; + const std::uint64_t allocatedSize = frameBuffer != nullptr && frameBuffer->frameBufferSize > 0 ? frameBuffer->frameBufferSize : target.geometry.frameBufferSize; + std::uint8_t* destination = nullptr; + bool fallback = false; + std::int64_t lastPts = 0; + std::int64_t lastDts = 0; + { + std::lock_guard lock(target.mutex); + lastPts = target.lastPts; + lastDts = target.lastDts; + if (frameBuffer != nullptr && frameBuffer->frameBuffer != nullptr) { + destination = static_cast(frameBuffer->frameBuffer); + } else if (!target.frameBufferSlots.empty()) { + destination = static_cast(target.frameBufferSlots[target.currentSlot % target.frameBufferSlots.size()]); + ++target.currentSlot; + } else { + const std::uint64_t fallbackSize = std::max(pictureSize, allocatedSize); + if (target.fallbackFrameBuffer.size() < fallbackSize) target.fallbackFrameBuffer.resize(static_cast(fallbackSize)); + destination = target.fallbackFrameBuffer.data(); + fallback = true; + } + } + if (!fallback && pictureSize > allocatedSize) throw std::runtime_error("VdecCore: a decoded picture is larger than its frame buffer"); + CopyNv12(destination, *frame); + const std::uint64_t pts = PictureTime(frame->pts, lastPts); + const std::uint64_t dts = PictureTime(frame->pkt_dts, lastDts); + const auto userData = static_cast(reinterpret_cast(frame->opaque)); + if (allocatedSize >= sizeof(VdecCoreHwPictureInfo)) { + auto* info = reinterpret_cast(destination + allocatedSize - sizeof(VdecCoreHwPictureInfo)); + std::memset(info, 0, sizeof(*info)); + info->codecType = target.codecType == CodecHevc ? 1 : 0; + info->userData = userData; + info->pts = pts; + info->dts = dts; + info->valid = 1; + info->width = alignedWidth; + info->height = alignedHeight; + info->cropLeft = 0; + info->cropRight = pitch > width ? (pitch - width) / 2 : 0; + info->cropTop = 0; + info->cropBottom = alignedHeight > height ? (alignedHeight - height) / 2 : 0; + info->sarWidth = sarWidth; + info->sarHeight = sarHeight; + info->flags = 0x01; + if (info->cropRight > 0 || info->cropBottom > 0) info->flags |= 0x02; + } + output->frameWidth = alignedWidth; + output->framePitch = pitch; + output->frameHeight = alignedHeight; + output->pictureCount = 1; + output->frameBuffer = destination; + output->frameFormat = 0; + output->reserved0 = 0; + output->pictures[0].flags = target.codecType; + output->pictures[0].reserved0 = 0; + output->pictures[0].pts = pts; + output->pictures[0].dts = dts; + output->pictures[0].userData = userData; + output->pictures[0].pictureInfo = nullptr; + output->pictures[0].frameBuffer = destination; + output->pictures[0].frameBufferSize = pictureSize; + output->pictures[0].framePitch = pitch; + output->pictures[0].reserved1 = 0; + output->pictures[1].frameBuffer = nullptr; + output->aspectRatio = 1; + output->sarWidth = sarWidth; + output->videoFormat = 5; + output->colourPrimaries = 1; + output->transferCharacteristics = 1; + output->framePitchInBytes = pitch; + output->lumaBuffer = destination; + output->chromaBuffer = destination + static_cast(pitch) * alignedHeight; + output->videoFullRangeFlag = frame->color_range == AVCOL_RANGE_JPEG ? 1 : 0; + output->colourDescriptionPresentFlag = 1; return 0; } @@ -172,6 +454,7 @@ int APS5_VABI sceVdecCoreMapMemoryBlock(void* decoder, const VdecCoreMemoryBlock std::lock_guard lock(state.mutex); auto& target = *Find(state, decoder, __func__)->second; if (blocks == nullptr || count == 0) return 0; + std::lock_guard decoderLock(target.mutex); for (std::uint32_t index = 0; index < count; ++index) { const VdecCoreMemoryBlock block = blocks[index]; target.mappedBlocks.push_back(block); @@ -216,13 +499,41 @@ int APS5_VABI sceVdecCoreQueryInstanceSize(const VdecCoreConfigInfo* config, std return 0; } -int APS5_VABI sceVdecCoreResetDecoder() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVdecCoreResetDecoder(void* decoder, void*) { + if (decoder == nullptr) return VdecCoreErrorInvalidArgument; + RunCommand(Lookup(decoder, __func__), Command::Type::Reset, nullptr); return 0; } -int APS5_VABI sceVdecCoreSetDecodeInput() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVdecCoreSetDecodeInput(void* decoder, const VdecCoreInputData* input, void*, std::uint32_t* status) { + if (decoder == nullptr || input == nullptr) return VdecCoreErrorInvalidArgument; + auto& target = Lookup(decoder, __func__); + if (status != nullptr) { + status[0] = 1; + status[1] = 1; + } + if (input->auData == nullptr || input->auSize == 0) return VdecCoreErrorInvalidArgument; + const std::int64_t pts = input->pts == VdecCorePtsInvalid ? AV_NOPTS_VALUE : input->pts; + const std::int64_t dts = input->dts == VdecCorePtsInvalid ? AV_NOPTS_VALUE : input->dts; + AVPacket* packet = av_packet_alloc(); + if (packet == nullptr) return VdecCoreErrorFatal; + if (av_new_packet(packet, static_cast(input->auSize)) < 0) { + av_packet_free(&packet); + return VdecCoreErrorFatal; + } + std::memcpy(packet->data, input->auData, static_cast(input->auSize)); + packet->pts = pts; + packet->dts = dts; + packet->opaque = reinterpret_cast(static_cast(input->userData)); + { + std::lock_guard lock(target.mutex); + target.lastPts = input->pts; + target.lastDts = input->dts; + target.lastUserData = input->userData; + target.commands.push_back({Command::Type::Packet, packet, nullptr}); + target.busy = true; + } + target.inputReady.notify_one(); return 0; } @@ -231,8 +542,12 @@ int APS5_VABI sceVdecCoreSetDecodeOutputSw() { return 0; } -int APS5_VABI sceVdecCoreSyncDecode() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVdecCoreSyncDecode(void* decoder, std::uint32_t* count) { + if (decoder == nullptr) return VdecCoreErrorInvalidArgument; + auto& target = Lookup(decoder, __func__); + std::unique_lock lock(target.mutex); + target.outputReady.wait(lock, [&] { return !target.frames.empty() || (!target.busy && target.commands.empty()); }); + if (count != nullptr) *count = static_cast(target.frames.size()); return 0; } @@ -241,13 +556,20 @@ int APS5_VABI sceVdecCoreSyncDecodeOutputSw() { return 0; } -int APS5_VABI sceVdecCoreSyncDecodeWptr() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVdecCoreSyncDecodeWptr(void* decoder, std::uint32_t* count, void*, void*) { + return sceVdecCoreSyncDecode(decoder, count); } -int APS5_VABI sceVdecCoreTrySyncDecode() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVdecCoreTrySyncDecode(void* decoder, std::uint32_t* count) { + if (decoder == nullptr) return VdecCoreErrorInvalidArgument; + auto& target = Lookup(decoder, __func__); + std::lock_guard lock(target.mutex); + if (!target.frames.empty()) { + if (count != nullptr) *count = static_cast(target.frames.size()); + return 0; + } + if (count != nullptr) *count = 0; + if (target.busy || !target.commands.empty()) return VdecCoreErrorDecodeRunning; return 0; } @@ -256,9 +578,8 @@ int APS5_VABI sceVdecCoreTrySyncDecodeOutputSw() { return 0; } -int APS5_VABI sceVdecCoreTrySyncDecodeWptr() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVdecCoreTrySyncDecodeWptr(void* decoder, std::uint32_t* count, void*, void*) { + return sceVdecCoreTrySyncDecode(decoder, count); } } diff --git a/core/libs/prx/libSceVdecCore/include/Api.hpp b/core/libs/prx/libSceVdecCore/include/Api.hpp index 16a437191d..ab149ad65e 100644 --- a/core/libs/prx/libSceVdecCore/include/Api.hpp +++ b/core/libs/prx/libSceVdecCore/include/Api.hpp @@ -71,27 +71,107 @@ struct VdecCoreMemoryBlock { static_assert(sizeof(VdecCoreMemoryBlock) == 0x18); +struct VdecCoreFrameBuffer { + void* frameBuffer; + std::uint64_t frameBufferSize; +}; + +static_assert(sizeof(VdecCoreFrameBuffer) == 0x10); + +struct VdecCoreInputData { + const void* auData; + std::uint64_t auSize; + std::int64_t pts; + std::int64_t dts; + std::uint64_t userData; + std::uint32_t flags; + std::uint32_t streamId; +}; + +static_assert(sizeof(VdecCoreInputData) == 0x30); + +struct VdecCorePicture { + std::uint32_t flags; + std::uint32_t reserved0; + std::uint64_t pts; + std::uint64_t dts; + std::uint64_t userData; + void* pictureInfo; + void* frameBuffer; + std::uint32_t frameBufferSize; + std::uint32_t framePitch; + std::uint64_t reserved1; +}; + +static_assert(sizeof(VdecCorePicture) == 0x40); + +struct VdecCoreHwPictureInfo { + std::uint32_t codecType; + std::uint32_t reserved0; + std::uint64_t userData; + std::uint64_t pts; + std::uint64_t dts; + std::uint32_t valid; + std::uint32_t reserved1; + std::uint32_t width; + std::uint32_t height; + std::uint32_t cropLeft; + std::uint32_t cropRight; + std::uint32_t cropTop; + std::uint32_t cropBottom; + std::uint32_t sarWidth; + std::uint32_t sarHeight; + std::uint32_t reserved2; + std::uint32_t flags; + std::uint8_t reserved3[0x400 - 0x50]; +}; + +static_assert(sizeof(VdecCoreHwPictureInfo) == 0x400); + +struct VdecCoreOutputInfo { + std::uint32_t frameWidth; + std::uint32_t framePitch; + std::uint32_t frameHeight; + std::uint32_t pictureCount; + void* frameBuffer; + std::uint32_t frameFormat; + std::uint32_t reserved0; + VdecCorePicture pictures[2]; + std::uint32_t aspectRatio; + std::uint32_t sarWidth; + std::uint32_t videoFormat; + std::uint32_t colourPrimaries; + std::uint32_t transferCharacteristics; + std::uint32_t framePitchInBytes; + void* lumaBuffer; + void* chromaBuffer; + std::uint32_t videoFullRangeFlag; + std::uint32_t colourDescriptionPresentFlag; +}; + +static_assert(sizeof(VdecCoreOutputInfo) == 0xd0); + extern "C" { int APS5_VABI sceVdecCoreAnalyzeStream(); int APS5_VABI sceVdecCoreCreateDecoder(const VdecCoreConfigInfo* config, const VdecCoreMemoryInfo* memory, void* computeQueue, void** decoder); int APS5_VABI sceVdecCoreDeleteDecoder(void* decoder); int APS5_VABI sceVdecCoreFinalizeComputeResource(); -int APS5_VABI sceVdecCoreFlushDecodeOutput(); -int APS5_VABI sceVdecCoreGetDecodeOutput(); +int APS5_VABI sceVdecCoreFlushDecodeOutput(void* decoder, std::uint32_t* count, void* reserved); +int APS5_VABI sceVdecCoreGetDecodeOutput(void* decoder, VdecCoreFrameBuffer* frameBuffer, std::uint32_t flags, VdecCoreOutputInfo* output, void* reserved); int APS5_VABI sceVdecCoreInitializeComputeResource(); int APS5_VABI sceVdecCoreMapMemoryBlock(void* decoder, const VdecCoreMemoryBlock* blocks, std::uint32_t count); int APS5_VABI sceVdecCoreQueryComputeResourceInfo(VdecCoreComputeResourceInfo* info); int APS5_VABI sceVdecCoreQueryFrameBufferInfo(const VdecCoreConfigInfo* config, VdecCoreFrameBufferInfo* info); int APS5_VABI sceVdecCoreQueryInstanceSize(const VdecCoreConfigInfo* config, std::uint64_t* instanceSize, std::uint64_t* computeSize, std::uint64_t* reservedSize); -int APS5_VABI sceVdecCoreResetDecoder(); -int APS5_VABI sceVdecCoreSetDecodeInput(); +int APS5_VABI sceVdecCoreResetDecoder(void* decoder, void* resetParam); +int APS5_VABI sceVdecCoreSetDecodeInput(void* decoder, const VdecCoreInputData* input, void* reserved, std::uint32_t* status); int APS5_VABI sceVdecCoreSetDecodeOutputSw(); -int APS5_VABI sceVdecCoreSyncDecode(); +int APS5_VABI sceVdecCoreSyncDecode(void* decoder, std::uint32_t* count); int APS5_VABI sceVdecCoreSyncDecodeOutputSw(); -int APS5_VABI sceVdecCoreSyncDecodeWptr(); -int APS5_VABI sceVdecCoreTrySyncDecode(); +int APS5_VABI sceVdecCoreSyncDecodeWptr(void* decoder, std::uint32_t* count, void* writePointer, void* readPointer); +int APS5_VABI sceVdecCoreTrySyncDecode(void* decoder, std::uint32_t* count); int APS5_VABI sceVdecCoreTrySyncDecodeOutputSw(); -int APS5_VABI sceVdecCoreTrySyncDecodeWptr(); +int APS5_VABI sceVdecCoreTrySyncDecodeWptr(void* decoder, std::uint32_t* count, void* writePointer, void* readPointer); } diff --git a/core/libs/tests/GuestVdecCore.cpp b/core/libs/tests/GuestVdecCore.cpp index e62fbd0248..c4aadbb1e8 100644 --- a/core/libs/tests/GuestVdecCore.cpp +++ b/core/libs/tests/GuestVdecCore.cpp @@ -1,10 +1,12 @@ #include "prx/libSceVdecCore/include/Api.hpp" #include "prx/libSceVdecCore/include/Decoder.hpp" +#include "H264Fixture.hpp" #include #include #include #include +#include #include #ifdef _WIN32 @@ -17,6 +19,15 @@ namespace { constexpr int InvalidArgument = static_cast(0x80C00001); constexpr int InvalidCodecType = static_cast(0x80C00003); +constexpr int NoPicture = static_cast(0x80C00010); +constexpr int DecodeRunning = static_cast(0x80C00018); +constexpr std::int64_t PtsInvalid = -1; +constexpr std::uint64_t SlotSize = 0x10000; +constexpr std::uint32_t OutputPitch = 64; +constexpr std::array VisiblePictureHashes{ + 0x5871fdc85dfa274eull, 0x26ce4afb974cffc4ull, 0xd2e739633733d5baull, + 0x54feb3d290e028fdull, 0xfa1342a4ba1819c8ull, 0x870125dd7f079a4cull, +}; constexpr std::uint32_t CodecAvc = 0; constexpr std::uint32_t CodecHevc = 4; constexpr std::uint32_t DecoderVdec2 = 2; @@ -174,6 +185,277 @@ void CheckMapMemoryBlock() { Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the mapped decoder"); } +void* CreateAvc(std::int32_t width) { + const VdecCoreConfigInfo config = Config(width, static_cast(H264Fixture::Height), 256, 0); + void* decoder = nullptr; + Require(sceVdecCoreCreateDecoder(&config, nullptr, nullptr, &decoder) == 0 && decoder != nullptr, "create an AVC decoder"); + return decoder; +} + +const std::vector>& Units() { + static const std::vector> units = H264Fixture::accessUnits(H264Fixture::Stream.data(), H264Fixture::Stream.size(), false); + Require(units.size() == H264Fixture::PictureHashes.size(), "the fixture's access units"); + return units; +} + +void Feed(void* decoder, std::size_t unit, std::int64_t pts, std::int64_t dts, std::uint64_t userData) { + const auto& data = Units()[unit]; + const VdecCoreInputData input{data.data(), data.size(), pts, dts, userData, 0, 0}; + std::array status{}; + Require(sceVdecCoreSetDecodeInput(decoder, &input, nullptr, status.data()) == 0, "set an access unit as decode input"); + Require(status[0] == 1 && status[1] == 1, "the input status of an accepted access unit"); +} + +void FeedAll(void* decoder, std::int64_t ptsBase, std::int64_t dtsBase, std::uint64_t userBase) { + for (std::size_t unit = 0; unit < Units().size(); ++unit) { + const auto offset = static_cast(unit); + Feed(decoder, unit, ptsBase == PtsInvalid ? PtsInvalid : ptsBase + offset, dtsBase == PtsInvalid ? PtsInvalid : dtsBase + offset, userBase + unit); + } +} + +std::uint32_t Flush(void* decoder) { + std::uint32_t count = 0xffffffffu; + Require(sceVdecCoreFlushDecodeOutput(decoder, &count, nullptr) == 0, "flush the decode output"); + return count; +} + +VdecCoreOutputInfo Output(void* decoder, VdecCoreFrameBuffer* frameBuffer) { + VdecCoreOutputInfo output{}; + Require(sceVdecCoreGetDecodeOutput(decoder, frameBuffer, 0, &output, nullptr) == 0, "get a decoded picture"); + return output; +} + +std::uint64_t VisibleHash(const std::uint8_t* buffer, std::uint32_t pitch, std::uint32_t frameHeight) { + std::uint64_t hash = 0xcbf29ce484222325ull; + const auto mix = [&](const std::uint8_t* row) { + for (std::uint32_t column = 0; column < H264Fixture::Width; ++column) hash = (hash ^ row[column]) * 0x100000001b3ull; + }; + for (std::uint32_t row = 0; row < H264Fixture::Height; ++row) mix(buffer + static_cast(row) * pitch); + const auto* chroma = buffer + static_cast(pitch) * frameHeight; + for (std::uint32_t row = 0; row < H264Fixture::Height / 2; ++row) mix(chroma + static_cast(row) * pitch); + return hash; +} + +bool ExtendsLastRows(const std::uint8_t* buffer, std::uint32_t pitch, std::uint32_t frameHeight) { + const auto* chroma = buffer + static_cast(pitch) * frameHeight; + const auto* lastLuma = buffer + static_cast(pitch) * (H264Fixture::Height - 1); + const auto* lastChroma = chroma + static_cast(pitch) * (H264Fixture::Height / 2 - 1); + for (std::uint32_t row = H264Fixture::Height; row < frameHeight; ++row) { + if (std::memcmp(buffer + static_cast(row) * pitch, lastLuma, H264Fixture::Width) != 0) return false; + } + for (std::uint32_t row = H264Fixture::Height / 2; row < frameHeight / 2; ++row) { + if (std::memcmp(chroma + static_cast(row) * pitch, lastChroma, H264Fixture::Width) != 0) return false; + } + return true; +} + +const VdecCoreHwPictureInfo& HwInfo(const VdecCoreOutputInfo& output, std::uint64_t allocatedSize) { + return *reinterpret_cast(static_cast(output.frameBuffer) + allocatedSize - sizeof(VdecCoreHwPictureInfo)); +} + +void RequirePicture(const VdecCoreOutputInfo& output, std::size_t picture, std::uint64_t allocatedSize, std::uint64_t pts, std::uint64_t userData, const char* message) { + const std::uint32_t height = H264Fixture::CodedHeight; + auto* buffer = static_cast(output.frameBuffer); + Require(output.pictureCount == 1 && buffer != nullptr && output.frameFormat == 0 && output.reserved0 == 0, message); + Require(output.frameWidth == H264Fixture::Width && output.framePitch == OutputPitch && output.frameHeight == height && output.framePitchInBytes == OutputPitch, message); + Require(output.lumaBuffer == buffer && output.chromaBuffer == buffer + static_cast(OutputPitch) * height, message); + const VdecCorePicture& first = output.pictures[0]; + Require(first.flags == 0 && first.reserved0 == 0 && first.pictureInfo == nullptr && first.frameBuffer == buffer && first.frameBufferSize == OutputPitch * height * 3 / 2 && first.framePitch == OutputPitch && first.reserved1 == 0, message); + Require(output.pictures[1].frameBuffer == nullptr, message); + Require(first.pts == pts && first.userData == userData, message); + Require(output.aspectRatio == 1 && output.sarWidth == 1 && output.videoFormat == 5 && output.colourPrimaries == 1 && output.transferCharacteristics == 1 && output.videoFullRangeFlag == 0 && output.colourDescriptionPresentFlag == 1, message); + Require(VisibleHash(buffer, output.framePitch, output.frameHeight) == VisiblePictureHashes[picture], message); + Require(ExtendsLastRows(buffer, output.framePitch, output.frameHeight), message); + const VdecCoreHwPictureInfo& info = HwInfo(output, allocatedSize); + Require(info.codecType == 0 && info.userData == userData && info.pts == pts && info.dts == first.dts && info.valid == 1, message); + Require(info.width == H264Fixture::Width && info.height == height && info.cropLeft == 0 && info.cropRight == 0 && info.cropTop == 0 && info.cropBottom == (height - H264Fixture::Height) / 2, message); + Require(info.sarWidth == 1 && info.sarHeight == 1 && info.flags == 0x03, message); +} + +void CheckDecodeArguments() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + const auto& unit = Units()[0]; + VdecCoreInputData input{unit.data(), unit.size(), 0, 0, 0, 0, 0}; + std::array status{7, 7}; + Require(sceVdecCoreSetDecodeInput(nullptr, &input, nullptr, status.data()) == InvalidArgument, "set input without a decoder"); + Require(sceVdecCoreSetDecodeInput(decoder, nullptr, nullptr, status.data()) == InvalidArgument, "set input without input data"); + Require(status[0] == 7 && status[1] == 7, "an input refused for a missing decoder or input leaves the status alone"); + input.auData = nullptr; + Require(sceVdecCoreSetDecodeInput(decoder, &input, nullptr, status.data()) == InvalidArgument, "set input without access unit data"); + Require(status[0] == 1 && status[1] == 1, "the status is written before the access unit is checked"); + status = {7, 7}; + input.auData = unit.data(); + input.auSize = 0; + Require(sceVdecCoreSetDecodeInput(decoder, &input, nullptr, status.data()) == InvalidArgument, "set input with an empty access unit"); + Require(status[0] == 1 && status[1] == 1, "the status is written before the access unit size is checked"); + input.auSize = unit.size(); + Require(sceVdecCoreSetDecodeInput(decoder, &input, nullptr, nullptr) == 0, "set input without a status"); + + std::uint32_t count = 7; + std::uint64_t pointers[2] = {7, 7}; + VdecCoreOutputInfo output{}; + VdecCoreFrameBuffer frameBuffer{}; + Require(sceVdecCoreResetDecoder(nullptr, nullptr) == InvalidArgument, "reset without a decoder"); + Require(sceVdecCoreSyncDecode(nullptr, &count) == InvalidArgument, "sync without a decoder"); + Require(sceVdecCoreSyncDecodeWptr(nullptr, &count, &pointers[0], &pointers[1]) == InvalidArgument, "write-pointer sync without a decoder"); + Require(sceVdecCoreTrySyncDecode(nullptr, &count) == InvalidArgument, "try-sync without a decoder"); + Require(sceVdecCoreTrySyncDecodeWptr(nullptr, &count, &pointers[0], &pointers[1]) == InvalidArgument, "write-pointer try-sync without a decoder"); + Require(sceVdecCoreGetDecodeOutput(nullptr, &frameBuffer, 0, &output, nullptr) == InvalidArgument, "get output without a decoder"); + Require(sceVdecCoreGetDecodeOutput(decoder, &frameBuffer, 0, nullptr, nullptr) == InvalidArgument, "get output without output info"); + Require(sceVdecCoreFlushDecodeOutput(nullptr, &count, nullptr) == InvalidArgument, "flush without a decoder"); + Require(count == 7 && pointers[0] == 7 && pointers[1] == 7, "refused calls leave their outputs alone"); + + Require(sceVdecCoreSyncDecode(decoder, nullptr) == 0, "sync without a count"); + Require(sceVdecCoreFlushDecodeOutput(decoder, nullptr, nullptr) == 0, "flush without a count"); + std::uint32_t held = 7; + Require(sceVdecCoreTrySyncDecode(decoder, nullptr) == 0 && sceVdecCoreTrySyncDecode(decoder, &held) == 0 && held == 1, "try-sync after a flush counts the held picture"); + Require(sceVdecCoreResetDecoder(decoder, nullptr) == 0, "reset"); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the argument decoder"); +} + +void CheckEmptyQueue() { + void* decoder = CreateAvc(100); + std::uint32_t count = 7; + Require(sceVdecCoreTrySyncDecode(decoder, &count) == 0 && count == 0, "try-sync on an idle decoder"); + count = 7; + Require(sceVdecCoreSyncDecode(decoder, &count) == 0 && count == 0, "sync on an idle decoder"); + VdecCoreOutputInfo output; + std::memset(&output, 0xab, sizeof(output)); + Require(sceVdecCoreGetDecodeOutput(decoder, nullptr, 0, &output, nullptr) == NoPicture, "get output from an empty queue"); + Require(output.frameWidth == 100 && output.framePitch == 128 && output.frameHeight == H264Fixture::Height, "an empty queue reports the maximum width, its 64-byte pitch and the maximum height"); + Require(output.pictureCount == 0 && output.frameBuffer == nullptr && output.frameFormat == 0 && output.reserved0 == 0, "an empty queue reports no picture"); + Require(output.pictures[0].frameBuffer == nullptr && output.pictures[1].frameBuffer == nullptr && output.lumaBuffer == nullptr && output.chromaBuffer == nullptr, "an empty queue reports no buffers"); + Require(output.pictures[0].pts == 0xababababababababull && output.aspectRatio == 0xababababu && output.colourDescriptionPresentFlag == 0xababababu, "an empty queue leaves the other fields alone"); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the idle decoder"); +} + +void CheckDecodeIntoSlots() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + std::vector memory(SlotSize * 4); + const VdecCoreMemoryBlock block{memory.data(), memory.size(), 256, 0}; + Require(sceVdecCoreMapMemoryBlock(decoder, &block, 1) == 0, "map four frame buffer slots"); + const std::vector slots = Slots(decoder); + Require(slots.size() == 4, "four slots"); + + std::size_t pictures = 0; + std::vector dts; + const auto take = [&](std::uint32_t available) { + for (std::uint32_t index = 0; index < available; ++index) { + const VdecCoreOutputInfo output = Output(decoder, nullptr); + Require(pictures < H264Fixture::PictureHashes.size(), "more pictures than access units"); + Require(output.frameBuffer == slots[pictures % slots.size()], "pictures go to the mapped slots in turn"); + const auto unit = H264Fixture::DisplayOrderUnits[pictures]; + RequirePicture(output, pictures, SlotSize, 1000 + unit, 3000 + unit, "a picture decoded into a slot"); + dts.push_back(output.pictures[0].dts); + ++pictures; + } + }; + for (std::size_t unit = 0; unit < Units().size(); ++unit) { + Feed(decoder, unit, 1000 + static_cast(unit), 2000 + static_cast(unit), 3000 + unit); + std::uint32_t tried = 0xffffffffu; + const int result = sceVdecCoreTrySyncDecode(decoder, &tried); + Require(result == 0 || (result == DecodeRunning && tried == 0), "try-sync during a decode"); + std::uint32_t count = 0xffffffffu; + Require(sceVdecCoreSyncDecode(decoder, &count) == 0, "sync after an access unit"); + take(count); + } + take(Flush(decoder)); + Require(pictures == H264Fixture::PictureHashes.size(), "every access unit gives a picture"); + for (const auto value : dts) Require(value >= 2000 && value < 2000 + Units().size(), "a picture's dts is one of the inputs'"); + VdecCoreOutputInfo output{}; + Require(sceVdecCoreGetDecodeOutput(decoder, nullptr, 0, &output, nullptr) == NoPicture, "no picture after the flushed ones"); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the slot decoder"); +} + +void CheckInvalidPts() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + FeedAll(decoder, PtsInvalid, PtsInvalid, 5000); + Require(Flush(decoder) == H264Fixture::PictureHashes.size(), "a flush holds every picture"); + for (std::size_t picture = 0; picture < H264Fixture::PictureHashes.size(); ++picture) { + const VdecCoreOutputInfo output = Output(decoder, nullptr); + RequirePicture(output, picture, SlotSize, ~std::uint64_t{0}, 5000 + H264Fixture::DisplayOrderUnits[picture], "a picture without a pts"); + Require(output.pictures[0].dts == ~std::uint64_t{0}, "a picture without a dts takes the last input's invalid dts"); + } + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the invalid pts decoder"); +} + +void CheckDestinations() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + FeedAll(decoder, 1000, 2000, 3000); + Require(Flush(decoder) == H264Fixture::PictureHashes.size(), "a flush holds every picture for the fallback"); + const VdecCoreOutputInfo fallback = Output(decoder, nullptr); + RequirePicture(fallback, 0, SlotSize, 1000, 3000, "a picture in the fallback buffer"); + const VdecCoreOutputInfo again = Output(decoder, nullptr); + Require(again.frameBuffer == fallback.frameBuffer, "the fallback buffer is reused"); + RequirePicture(again, 1, SlotSize, 1000 + H264Fixture::DisplayOrderUnits[1], 3000 + H264Fixture::DisplayOrderUnits[1], "a second picture in the fallback buffer"); + + std::vector slotMemory(SlotSize); + const VdecCoreMemoryBlock block{slotMemory.data(), slotMemory.size(), 256, 0}; + Require(sceVdecCoreMapMemoryBlock(decoder, &block, 1) == 0, "map one slot"); + std::vector caller(0x2000); + VdecCoreFrameBuffer frameBuffer{caller.data(), caller.size()}; + const VdecCoreOutputInfo supplied = Output(decoder, &frameBuffer); + Require(supplied.frameBuffer == caller.data(), "a caller frame buffer comes before the slots"); + RequirePicture(supplied, 2, caller.size(), 1000 + H264Fixture::DisplayOrderUnits[2], 3000 + H264Fixture::DisplayOrderUnits[2], "a picture in the caller's frame buffer"); + VdecCoreFrameBuffer unaddressed{nullptr, caller.size()}; + const VdecCoreOutputInfo slot = Output(decoder, &unaddressed); + Require(slot.frameBuffer == slotMemory.data(), "a frame buffer without an address leaves the picture to the slots"); + RequirePicture(slot, 3, caller.size(), 1000 + H264Fixture::DisplayOrderUnits[3], 3000 + H264Fixture::DisplayOrderUnits[3], "a picture in a slot with the caller's size"); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the destination decoder"); +} + +void CheckFlushResetAndSync() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + std::vector memory(SlotSize * 3); + const VdecCoreMemoryBlock block{memory.data(), memory.size(), 256, 0}; + Require(sceVdecCoreMapMemoryBlock(decoder, &block, 1) == 0, "map three slots"); + const std::vector slots = Slots(decoder); + + FeedAll(decoder, 1000, 2000, 3000); + const std::uint32_t held = Flush(decoder); + Require(held == H264Fixture::PictureHashes.size(), "a flush returns every held picture"); + std::uint32_t count = 0; + Require(sceVdecCoreSyncDecode(decoder, &count) == 0 && count == held, "sync after a flush counts the held pictures"); + std::uint32_t tried = 0; + Require(sceVdecCoreTrySyncDecode(decoder, &tried) == 0 && tried == held, "try-sync after a sync counts the held pictures"); + std::uint64_t pointers[2] = {7, 7}; + std::uint32_t forwarded = 0; + Require(sceVdecCoreSyncDecodeWptr(decoder, &forwarded, &pointers[0], &pointers[1]) == 0 && forwarded == held, "the write-pointer sync forwards to the sync"); + forwarded = 0; + Require(sceVdecCoreTrySyncDecodeWptr(decoder, &forwarded, &pointers[0], &pointers[1]) == 0 && forwarded == held, "the write-pointer try-sync forwards to the try-sync"); + Require(pointers[0] == 7 && pointers[1] == 7, "the write-pointer variants leave the pointers alone"); + + Require(Output(decoder, nullptr).frameBuffer == slots[0], "the first picture goes to the first slot"); + Require(Output(decoder, nullptr).frameBuffer == slots[1], "the second picture goes to the second slot"); + + FeedAll(decoder, 1000, 2000, 3000); + Require(sceVdecCoreResetDecoder(decoder, nullptr) == 0, "reset with queued input"); + count = 7; + Require(sceVdecCoreSyncDecode(decoder, &count) == 0 && count == 0, "a reset drops the queued input and its pictures"); + tried = 7; + Require(sceVdecCoreTrySyncDecode(decoder, &tried) == 0 && tried == 0, "try-sync after a reset"); + VdecCoreOutputInfo output{}; + Require(sceVdecCoreGetDecodeOutput(decoder, nullptr, 0, &output, nullptr) == NoPicture, "no picture after a reset"); + Require(Flush(decoder) == 0, "a flush after a reset holds nothing"); + + FeedAll(decoder, 1000, 2000, 3000); + Require(Flush(decoder) == H264Fixture::PictureHashes.size(), "the decoder decodes again after a reset"); + const VdecCoreOutputInfo restarted = Output(decoder, nullptr); + Require(restarted.frameBuffer == slots[0], "a reset starts the slots again"); + RequirePicture(restarted, 0, SlotSize, 1000, 3000, "the first picture after a reset"); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the flushed decoder"); +} + +void CheckDeleteWithQueuedInput() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + for (int round = 0; round < 8; ++round) FeedAll(decoder, 1000, 2000, 3000); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete a decoder with queued input"); + void* held = CreateAvc(static_cast(H264Fixture::Width)); + FeedAll(held, 1000, 2000, 3000); + Require(Flush(held) == H264Fixture::PictureHashes.size(), "a flush before the delete holds every picture"); + Require(sceVdecCoreDeleteDecoder(held) == 0, "delete a decoder with held pictures"); +} + } int main() { @@ -183,5 +465,12 @@ int main() { CheckComputeResourceInfo(); CheckCreateAndDelete(); CheckMapMemoryBlock(); + CheckDecodeArguments(); + CheckEmptyQueue(); + CheckDecodeIntoSlots(); + CheckInvalidPts(); + CheckDestinations(); + CheckFlushResetAndSync(); + CheckDeleteWithQueuedInput(); return 0; } diff --git a/docs/dev/debt/libSceVdecCore.md b/docs/dev/debt/libSceVdecCore.md index fbe963cc3f..70f933fca9 100644 --- a/docs/dev/debt/libSceVdecCore.md +++ b/docs/dev/debt/libSceVdecCore.md @@ -2,8 +2,9 @@ ### Unknown function info -- [sceVdecCoreAnalyzeStream, sceVdecCoreFinalizeComputeResource, sceVdecCoreFlushDecodeOutput, sceVdecCoreGetDecodeOutput, sceVdecCoreInitializeComputeResource, sceVdecCoreResetDecoder, sceVdecCoreSetDecodeInput, sceVdecCoreSetDecodeOutputSw, sceVdecCoreSyncDecode, sceVdecCoreSyncDecodeOutputSw, sceVdecCoreSyncDecodeWptr, sceVdecCoreTrySyncDecode, sceVdecCoreTrySyncDecodeOutputSw and sceVdecCoreTrySyncDecodeWptr](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVdecCore.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceVdecCoreAnalyzeStream, sceVdecCoreFinalizeComputeResource, sceVdecCoreInitializeComputeResource, sceVdecCoreSetDecodeOutputSw, sceVdecCoreSyncDecodeOutputSw and sceVdecCoreTrySyncDecodeOutputSw](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVdecCore.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified ### Functional -- [sceVdecCoreQueryInstanceSize, sceVdecCoreQueryFrameBufferInfo, sceVdecCoreQueryComputeResourceInfo, sceVdecCoreCreateDecoder, sceVdecCoreDeleteDecoder and sceVdecCoreMapMemoryBlock](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - console behaviour is unmeasured. The signatures, error codes, 1920x1080 and 16-frame defaults and slot cutting follow [shadPS4's `vdeccore.cpp`](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/videodec/vdeccore.cpp#L275-L706), and the sizes are shadPS4's constants, not console values: instance 0x40000, compute 0x10000, and a frame buffer of NV12 plus 0x1000 rounded to 64 KiB. CreateDecoder opens an FFmpeg H.264 or HEVC context but starts no decode thread; the memory info and compute queue are ignored, and a handle CreateDecoder did not return, or a codec that is unavailable or fails to open, throws +- [sceVdecCoreQueryInstanceSize, sceVdecCoreQueryFrameBufferInfo, sceVdecCoreQueryComputeResourceInfo, sceVdecCoreCreateDecoder, sceVdecCoreDeleteDecoder and sceVdecCoreMapMemoryBlock](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - console behaviour is unmeasured. The signatures, error codes, 1920x1080 and 16-frame defaults and slot cutting follow [shadPS4's `vdeccore.cpp`](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/videodec/vdeccore.cpp#L275-L706), and the sizes are shadPS4's constants, not console values: instance 0x40000, compute 0x10000, and a frame buffer of NV12 plus 0x1000 rounded to 64 KiB. CreateDecoder opens an FFmpeg H.264 or HEVC context and starts the decoder's decode thread, which DeleteDecoder stops and joins; the memory info and compute queue are ignored, and a handle CreateDecoder did not return, or a codec that is unavailable or fails to open, throws +- [sceVdecCoreResetDecoder, sceVdecCoreSetDecodeInput, sceVdecCoreSyncDecode, sceVdecCoreGetDecodeOutput, sceVdecCoreFlushDecodeOutput, sceVdecCoreSyncDecodeWptr, sceVdecCoreTrySyncDecode and sceVdecCoreTrySyncDecodeWptr](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - console behaviour is unmeasured; they follow [shadPS4's `vdeccore.cpp`](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/videodec/vdeccore.cpp#L118-L768). Decoding is FFmpeg software decoding on a host thread. GetDecodeOutput's pitch is the width rounded to 64 while CreateDecoder's frame buffer size uses the horizontal alignment (256 by default), as in shadPS4. Without a caller frame buffer or mapped slot a picture goes to host memory the decoder owns, and the empty-queue fields are shadPS4's. Unlike shadPS4, a picture larger than its frame buffer throws and the NV12 copy goes row by row From 35ea87926158c31f40048179be496ddc9fdeeb5e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:01:27 -0700 Subject: [PATCH 2/3] fix(libSceVdecCore): bound a decoded picture by the frame buffer it goes to sceVdecCoreGetDecodeOutput took the caller's frameBufferSize as the output's size even when the caller gave no address and the picture went to a mapped slot. A size larger than the slot put the picture information past the slot's end. The size now follows the destination: the caller's size for the caller's buffer, the frame buffer size for a mapped slot, and room for the picture and its information in the fallback buffer. A picture larger than its destination is still refused, and the picture information is written only where it fits after the picture. --- core/libs/prx/libSceVdecCore/Export.cpp | 17 +++++++------- core/libs/tests/GuestVdecCore.cpp | 30 ++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceVdecCore/Export.cpp b/core/libs/prx/libSceVdecCore/Export.cpp index 211ee4d287..7862636d7d 100644 --- a/core/libs/prx/libSceVdecCore/Export.cpp +++ b/core/libs/prx/libSceVdecCore/Export.cpp @@ -367,9 +367,8 @@ int APS5_VABI sceVdecCoreGetDecodeOutput(void* decoder, VdecCoreFrameBuffer* fra const std::uint32_t pictureSize = pitch * alignedHeight * 3 / 2; const std::uint32_t sarWidth = frame->sample_aspect_ratio.num > 0 ? static_cast(frame->sample_aspect_ratio.num) : 1; const std::uint32_t sarHeight = frame->sample_aspect_ratio.den > 0 ? static_cast(frame->sample_aspect_ratio.den) : 1; - const std::uint64_t allocatedSize = frameBuffer != nullptr && frameBuffer->frameBufferSize > 0 ? frameBuffer->frameBufferSize : target.geometry.frameBufferSize; std::uint8_t* destination = nullptr; - bool fallback = false; + std::uint64_t capacity = 0; std::int64_t lastPts = 0; std::int64_t lastDts = 0; { @@ -378,23 +377,25 @@ int APS5_VABI sceVdecCoreGetDecodeOutput(void* decoder, VdecCoreFrameBuffer* fra lastDts = target.lastDts; if (frameBuffer != nullptr && frameBuffer->frameBuffer != nullptr) { destination = static_cast(frameBuffer->frameBuffer); + capacity = frameBuffer->frameBufferSize > 0 ? frameBuffer->frameBufferSize : target.geometry.frameBufferSize; } else if (!target.frameBufferSlots.empty()) { destination = static_cast(target.frameBufferSlots[target.currentSlot % target.frameBufferSlots.size()]); ++target.currentSlot; + capacity = target.geometry.frameBufferSize; } else { - const std::uint64_t fallbackSize = std::max(pictureSize, allocatedSize); - if (target.fallbackFrameBuffer.size() < fallbackSize) target.fallbackFrameBuffer.resize(static_cast(fallbackSize)); + const std::uint64_t requested = frameBuffer != nullptr && frameBuffer->frameBufferSize > 0 ? frameBuffer->frameBufferSize : target.geometry.frameBufferSize; + capacity = std::max(std::uint64_t{pictureSize} + sizeof(VdecCoreHwPictureInfo), requested); + if (target.fallbackFrameBuffer.size() < capacity) target.fallbackFrameBuffer.resize(static_cast(capacity)); destination = target.fallbackFrameBuffer.data(); - fallback = true; } } - if (!fallback && pictureSize > allocatedSize) throw std::runtime_error("VdecCore: a decoded picture is larger than its frame buffer"); + if (pictureSize > capacity) throw std::runtime_error("VdecCore: a decoded picture is larger than its frame buffer"); CopyNv12(destination, *frame); const std::uint64_t pts = PictureTime(frame->pts, lastPts); const std::uint64_t dts = PictureTime(frame->pkt_dts, lastDts); const auto userData = static_cast(reinterpret_cast(frame->opaque)); - if (allocatedSize >= sizeof(VdecCoreHwPictureInfo)) { - auto* info = reinterpret_cast(destination + allocatedSize - sizeof(VdecCoreHwPictureInfo)); + if (capacity - pictureSize >= sizeof(VdecCoreHwPictureInfo)) { + auto* info = reinterpret_cast(destination + capacity - sizeof(VdecCoreHwPictureInfo)); std::memset(info, 0, sizeof(*info)); info->codecType = target.codecType == CodecHevc ? 1 : 0; info->userData = userData; diff --git a/core/libs/tests/GuestVdecCore.cpp b/core/libs/tests/GuestVdecCore.cpp index c4aadbb1e8..9da1f4b1b4 100644 --- a/core/libs/tests/GuestVdecCore.cpp +++ b/core/libs/tests/GuestVdecCore.cpp @@ -2,7 +2,9 @@ #include "prx/libSceVdecCore/include/Decoder.hpp" #include "H264Fixture.hpp" +#include #include +#include #include #include #include @@ -400,10 +402,35 @@ void CheckDestinations() { VdecCoreFrameBuffer unaddressed{nullptr, caller.size()}; const VdecCoreOutputInfo slot = Output(decoder, &unaddressed); Require(slot.frameBuffer == slotMemory.data(), "a frame buffer without an address leaves the picture to the slots"); - RequirePicture(slot, 3, caller.size(), 1000 + H264Fixture::DisplayOrderUnits[3], 3000 + H264Fixture::DisplayOrderUnits[3], "a picture in a slot with the caller's size"); + RequirePicture(slot, 3, SlotSize, 1000 + H264Fixture::DisplayOrderUnits[3], 3000 + H264Fixture::DisplayOrderUnits[3], "a picture in a slot keeps the slot's size, not the caller's"); Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the destination decoder"); } +void CheckOutputStaysInItsDestination() { + void* decoder = CreateAvc(static_cast(H264Fixture::Width)); + FeedAll(decoder, 1000, 2000, 3000); + Require(Flush(decoder) == H264Fixture::PictureHashes.size(), "a flush holds every picture for the destination bounds"); + constexpr std::uint8_t Guard = 0x5a; + std::vector memory(SlotSize * 2, Guard); + const VdecCoreMemoryBlock block{memory.data(), SlotSize, 256, 0}; + Require(sceVdecCoreMapMemoryBlock(decoder, &block, 1) == 0, "map one slot followed by guard bytes"); + VdecCoreFrameBuffer larger{nullptr, SlotSize * 2}; + const VdecCoreOutputInfo slot = Output(decoder, &larger); + Require(slot.frameBuffer == memory.data(), "a frame buffer with no address and a larger size goes to the slot"); + RequirePicture(slot, 0, SlotSize, 1000, 3000, "a picture in a slot under a larger caller size"); + Require(std::all_of(memory.begin() + static_cast(SlotSize), memory.end(), [](std::uint8_t byte) { return byte == Guard; }), "a picture in a slot under a larger caller size wrote past the slot"); + const std::uint32_t pictureSize = OutputPitch * H264Fixture::CodedHeight * 3 / 2; + std::vector tight(pictureSize + 0x200, Guard); + VdecCoreFrameBuffer small{tight.data(), tight.size()}; + const VdecCoreOutputInfo caller = Output(decoder, &small); + Require(caller.frameBuffer == tight.data(), "a caller frame buffer with room for the picture only"); + const auto unit = H264Fixture::DisplayOrderUnits[1]; + Require(caller.pictures[0].pts == 1000 + unit && caller.pictures[0].userData == 3000 + unit, "the picture in a caller frame buffer with room for the picture only"); + Require(VisibleHash(tight.data(), caller.framePitch, caller.frameHeight) == VisiblePictureHashes[1] && ExtendsLastRows(tight.data(), caller.framePitch, caller.frameHeight), "picture information too large for the room after a picture overwrote the picture"); + Require(std::all_of(tight.begin() + static_cast(pictureSize), tight.end(), [](std::uint8_t byte) { return byte == Guard; }), "picture information was written into a caller frame buffer with no room for it after the picture"); + Require(sceVdecCoreDeleteDecoder(decoder) == 0, "delete the bounds decoder"); +} + void CheckFlushResetAndSync() { void* decoder = CreateAvc(static_cast(H264Fixture::Width)); std::vector memory(SlotSize * 3); @@ -470,6 +497,7 @@ int main() { CheckDecodeIntoSlots(); CheckInvalidPts(); CheckDestinations(); + CheckOutputStaysInItsDestination(); CheckFlushResetAndSync(); CheckDeleteWithQueuedInput(); return 0; From 935b2b1a26574fe434cc1a38c4983db5b016f753 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:00:38 -0700 Subject: [PATCH 3/3] fix(libSceVdecCore): refuse access units past the FFmpeg packet limit sceVdecCoreSetDecodeInput passed the 64-bit auSize to av_new_packet as an int and to memcpy as a size_t, so a size past 32 bits allocated its low bits and copied the whole length. The size is now checked once against the largest packet av_new_packet accepts, INT_MAX less AV_INPUT_BUFFER_PADDING_SIZE and one, and that one length is used for the allocation and the copy. A larger unit is refused with the invalid-argument error before it is read. --- core/libs/prx/libSceVdecCore/Export.cpp | 18 +++++++++++++--- .../prx/libSceVdecCore/include/Decoder.hpp | 2 ++ core/libs/tests/GuestVdecCore.cpp | 21 +++++++++++++++++++ docs/dev/debt/libSceVdecCore.md | 2 +- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceVdecCore/Export.cpp b/core/libs/prx/libSceVdecCore/Export.cpp index b6605456cc..14d3d725cb 100644 --- a/core/libs/prx/libSceVdecCore/Export.cpp +++ b/core/libs/prx/libSceVdecCore/Export.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,7 @@ constexpr std::uint32_t FrameBufferAlignment = 256; constexpr std::uint64_t InstanceSize = 0x40000; constexpr std::uint64_t ComputeSize = 0x10000; constexpr std::uint32_t ComputeAlignment = 0x10000; +constexpr std::uint64_t MaxAccessUnitSize = static_cast(std::numeric_limits::max() - AV_INPUT_BUFFER_PADDING_SIZE - 1); struct Geometry { std::uint32_t width; @@ -272,6 +274,15 @@ std::uint64_t PictureTime(std::int64_t decoded, std::int64_t last) { extern "C" { +std::uint64_t VdecCoreMaxAccessUnitSize_nid_no_patch() { + return MaxAccessUnitSize; +} + +std::int32_t VdecCoreAccessUnitLength_nid_no_patch(std::uint64_t auSize) { + if (auSize == 0 || auSize > MaxAccessUnitSize) return -1; + return static_cast(auSize); +} + std::uint32_t VdecCoreFrameBufferSlots_nid_no_patch(const void* decoder, void** slots, std::uint32_t capacity) { auto& state = State(); std::lock_guard lock(state.mutex); @@ -516,16 +527,17 @@ int APS5_VABI sceVdecCoreSetDecodeInput(void* decoder, const VdecCoreInputData* status[0] = 1; status[1] = 1; } - if (input->auData == nullptr || input->auSize == 0) return VdecCoreErrorInvalidArgument; + const std::int32_t length = VdecCoreAccessUnitLength_nid_no_patch(input->auSize); + if (input->auData == nullptr || length < 0) return VdecCoreErrorInvalidArgument; const std::int64_t pts = input->pts == VdecCorePtsInvalid ? AV_NOPTS_VALUE : input->pts; const std::int64_t dts = input->dts == VdecCorePtsInvalid ? AV_NOPTS_VALUE : input->dts; AVPacket* packet = av_packet_alloc(); if (packet == nullptr) return VdecCoreErrorFatal; - if (av_new_packet(packet, static_cast(input->auSize)) < 0) { + if (av_new_packet(packet, length) < 0) { av_packet_free(&packet); return VdecCoreErrorFatal; } - std::memcpy(packet->data, input->auData, static_cast(input->auSize)); + std::memcpy(packet->data, input->auData, static_cast(length)); packet->pts = pts; packet->dts = dts; packet->opaque = reinterpret_cast(static_cast(input->userData)); diff --git a/core/libs/prx/libSceVdecCore/include/Decoder.hpp b/core/libs/prx/libSceVdecCore/include/Decoder.hpp index ea4d2d84db..32917a00d2 100644 --- a/core/libs/prx/libSceVdecCore/include/Decoder.hpp +++ b/core/libs/prx/libSceVdecCore/include/Decoder.hpp @@ -5,5 +5,7 @@ extern "C" { std::uint32_t VdecCoreFrameBufferSlots_nid_no_patch(const void* decoder, void** slots, std::uint32_t capacity); +std::uint64_t VdecCoreMaxAccessUnitSize_nid_no_patch(); +std::int32_t VdecCoreAccessUnitLength_nid_no_patch(std::uint64_t auSize); } diff --git a/core/libs/tests/GuestVdecCore.cpp b/core/libs/tests/GuestVdecCore.cpp index 9da1f4b1b4..a7ab8641e5 100644 --- a/core/libs/tests/GuestVdecCore.cpp +++ b/core/libs/tests/GuestVdecCore.cpp @@ -274,6 +274,18 @@ void RequirePicture(const VdecCoreOutputInfo& output, std::size_t picture, std:: Require(info.sarWidth == 1 && info.sarHeight == 1 && info.flags == 0x03, message); } +void CheckAccessUnitLength() { + const std::uint64_t limit = VdecCoreMaxAccessUnitSize_nid_no_patch(); + Require(limit > 0 && limit < 0x7fffffffull, "the largest access unit leaves room for the packet padding in an int"); + Require(VdecCoreAccessUnitLength_nid_no_patch(0) < 0, "an empty access unit has no length"); + Require(VdecCoreAccessUnitLength_nid_no_patch(1) == 1, "a one-byte access unit keeps its length"); + Require(VdecCoreAccessUnitLength_nid_no_patch(limit) == static_cast(limit), "the largest access unit keeps its length"); + Require(VdecCoreAccessUnitLength_nid_no_patch(limit + 1) < 0, "an access unit one past the packet limit has no length"); + Require(VdecCoreAccessUnitLength_nid_no_patch(0x7fffffffull) < 0, "an access unit of the largest int has no length"); + Require(VdecCoreAccessUnitLength_nid_no_patch((1ull << 32) + 16) < 0, "an access unit whose low 32 bits look small has no length"); + Require(VdecCoreAccessUnitLength_nid_no_patch(~0ull) < 0, "the largest 64-bit size has no length"); +} + void CheckDecodeArguments() { void* decoder = CreateAvc(static_cast(H264Fixture::Width)); const auto& unit = Units()[0]; @@ -290,6 +302,14 @@ void CheckDecodeArguments() { input.auSize = 0; Require(sceVdecCoreSetDecodeInput(decoder, &input, nullptr, status.data()) == InvalidArgument, "set input with an empty access unit"); Require(status[0] == 1 && status[1] == 1, "the status is written before the access unit size is checked"); + const std::uint64_t limit = VdecCoreMaxAccessUnitSize_nid_no_patch(); + const std::array oversized{1ull << 31, limit + 1, (1ull << 32) + unit.size(), ~0ull}; + for (const auto size : oversized) { + status = {7, 7}; + input.auSize = size; + Require(sceVdecCoreSetDecodeInput(decoder, &input, nullptr, status.data()) == InvalidArgument, "set input with an access unit past the packet limit"); + Require(status[0] == 1 && status[1] == 1, "the status is written before an oversized access unit is refused"); + } input.auSize = unit.size(); Require(sceVdecCoreSetDecodeInput(decoder, &input, nullptr, nullptr) == 0, "set input without a status"); @@ -492,6 +512,7 @@ int main() { CheckComputeResourceInfo(); CheckCreateAndDelete(); CheckMapMemoryBlock(); + CheckAccessUnitLength(); CheckDecodeArguments(); CheckEmptyQueue(); CheckDecodeIntoSlots(); diff --git a/docs/dev/debt/libSceVdecCore.md b/docs/dev/debt/libSceVdecCore.md index 70f933fca9..c4cea5e37a 100644 --- a/docs/dev/debt/libSceVdecCore.md +++ b/docs/dev/debt/libSceVdecCore.md @@ -7,4 +7,4 @@ ### Functional - [sceVdecCoreQueryInstanceSize, sceVdecCoreQueryFrameBufferInfo, sceVdecCoreQueryComputeResourceInfo, sceVdecCoreCreateDecoder, sceVdecCoreDeleteDecoder and sceVdecCoreMapMemoryBlock](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - console behaviour is unmeasured. The signatures, error codes, 1920x1080 and 16-frame defaults and slot cutting follow [shadPS4's `vdeccore.cpp`](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/videodec/vdeccore.cpp#L275-L706), and the sizes are shadPS4's constants, not console values: instance 0x40000, compute 0x10000, and a frame buffer of NV12 plus 0x1000 rounded to 64 KiB. CreateDecoder opens an FFmpeg H.264 or HEVC context and starts the decoder's decode thread, which DeleteDecoder stops and joins; the memory info and compute queue are ignored, and a handle CreateDecoder did not return, or a codec that is unavailable or fails to open, throws -- [sceVdecCoreResetDecoder, sceVdecCoreSetDecodeInput, sceVdecCoreSyncDecode, sceVdecCoreGetDecodeOutput, sceVdecCoreFlushDecodeOutput, sceVdecCoreSyncDecodeWptr, sceVdecCoreTrySyncDecode and sceVdecCoreTrySyncDecodeWptr](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - console behaviour is unmeasured; they follow [shadPS4's `vdeccore.cpp`](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/videodec/vdeccore.cpp#L118-L768). Decoding is FFmpeg software decoding on a host thread. GetDecodeOutput's pitch is the width rounded to 64 while CreateDecoder's frame buffer size uses the horizontal alignment (256 by default), as in shadPS4. Without a caller frame buffer or mapped slot a picture goes to host memory the decoder owns, and the empty-queue fields are shadPS4's. Unlike shadPS4, a picture larger than its frame buffer throws and the NV12 copy goes row by row +- [sceVdecCoreResetDecoder, sceVdecCoreSetDecodeInput, sceVdecCoreSyncDecode, sceVdecCoreGetDecodeOutput, sceVdecCoreFlushDecodeOutput, sceVdecCoreSyncDecodeWptr, sceVdecCoreTrySyncDecode and sceVdecCoreTrySyncDecodeWptr](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - console behaviour is unmeasured; they follow [shadPS4's `vdeccore.cpp`](https://github.com/shadps4-emu/shadPS4/blob/0614ce324be092791b758d79f93d9f1ca5b10a81/src/core/libraries/videodec/vdeccore.cpp#L118-L768). Decoding is FFmpeg software decoding on a host thread. GetDecodeOutput's pitch is the width rounded to 64 while CreateDecoder's frame buffer size uses the horizontal alignment (256 by default), as in shadPS4. Without a caller frame buffer or mapped slot a picture goes to host memory the decoder owns, and the empty-queue fields are shadPS4's. Unlike shadPS4, a picture larger than its frame buffer throws, an access unit too large for an FFmpeg packet is refused, and the NV12 copy goes row by row