From a7a734b5d30aefd2422b3ae31353864bc3142ecc Mon Sep 17 00:00:00 2001 From: nmzik Date: Mon, 12 Oct 2026 21:08:58 +0200 Subject: [PATCH] video: implement PS5 Vdecsw using a shared queued AVC decoder Co-authored-by: Ali Almohaya --- CMakeLists.txt | 10 + src/libs/libVdecsw.cpp | 459 ++++++++++++++++++++++++++++++++++ src/libs/libVideoDec2.cpp | 101 +------- src/libs/libs.cpp | 5 + src/libs/videoDec2Decoder.cpp | 269 +++++++++++++------- src/libs/videoDec2Decoder.h | 81 +++++- tests/VdecswTests.cpp | 376 ++++++++++++++++++++++++++++ 7 files changed, 1105 insertions(+), 196 deletions(-) create mode 100644 src/libs/libVdecsw.cpp create mode 100644 tests/VdecswTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 83466115..ba2d71a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -432,6 +432,14 @@ add_executable(avplayer_file_tests EXCLUDE_FROM_ALL target_link_libraries(avplayer_file_tests common FFmpeg::ffmpeg ZArchive::zarchive) target_include_directories(avplayer_file_tests PRIVATE ${inc_headers}) +add_executable(vdecsw_tests EXCLUDE_FROM_ALL + "${KYTY_TESTS_DIR}/VdecswTests.cpp" + "${KYTY_SOURCE_DIR}/libs/videoDec2Decoder.cpp" + "${KYTY_SOURCE_DIR}/loader/timer.cpp" +) +target_link_libraries(vdecsw_tests common FFmpeg::ffmpeg) +target_include_directories(vdecsw_tests PRIVATE ${inc_headers}) + add_executable(bit_array_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/BitArrayTests.cpp" ) @@ -622,6 +630,7 @@ if(BUILD_TESTING) add_test(NAME archive_file COMMAND $) add_test(NAME trophy_system COMMAND $) add_test(NAME avplayer_file COMMAND $) + add_test(NAME vdecsw COMMAND $) add_test(NAME bit_array COMMAND $) add_test(NAME lru_cache COMMAND $) add_test(NAME shader_vertex_metadata COMMAND $) @@ -698,6 +707,7 @@ if(BUILD_TESTING) archive_file_tests trophy_system_tests avplayer_file_tests + vdecsw_tests bit_array_tests lru_cache_tests shader_vertex_metadata_tests diff --git a/src/libs/libVdecsw.cpp b/src/libs/libVdecsw.cpp new file mode 100644 index 00000000..a11ad3bf --- /dev/null +++ b/src/libs/libVdecsw.cpp @@ -0,0 +1,459 @@ +#include "common/abi.h" +#include "libs/libs.h" +#include "libs/videoDec2Decoder.h" +#include "loader/symbolDatabase.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Libs::Vdecsw { + +LIB_VERSION("Vdecsw", 1, "Vdecsw", 1, 1); + +namespace Decoder = VideoDec2::Decoder; + +enum class Error : uint32_t { + ApiFail = 0x0100, + StructSize, + ArgumentPointer, + DecoderInstance, + MemorySize, + MemoryPointer, + FrameBufferSize, + FrameBufferPointer, + FrameBufferAlignment, + AccessUnitSize = 0x010d, + AccessUnitPointer, + OutputInfo, + ComputeQueue, + InputQueueFull = 0x0113, + OutputBufferFull, + InputQueueEmpty = 0x0116, + DecodePending, + OutputBufferEmpty, + ConfigInfo = 0x0200, + ComputePipeId, + ComputeQueueId, + ResourceType, + CodecType, + InputQueueDepth = 0x0206, + DpbFrameCount = 0x0209, + FrameWidthHeight, + ExtraConfigInfo, + AccessUnit = 0x0301, + OversizeDecode, +}; + +constexpr int32_t Fail(Error error) { + return static_cast(0x81510000u | static_cast(error)); +} + +struct ComputeMemoryInfo { + size_t this_size; + size_t cpu_gpu_memory_size; + void* cpu_gpu_memory; +}; + +struct ComputeConfigInfo { + size_t this_size; + uint16_t pipe_id; + uint16_t queue_id; + bool check_memory_type; + uint8_t reserved0; + uint16_t reserved1; +}; + +struct ComputeQueue { + uint16_t pipe_id; + uint16_t queue_id; + uint32_t decoders = 0; +}; + +struct ConfigInfo { + size_t this_size; + uint32_t resource_type; + uint32_t codec_type; + uint32_t profile; + uint32_t max_level; + int32_t max_width; + int32_t max_height; + int32_t max_dpb_frames; + uint32_t input_queue_depth; + ComputeQueue* compute_queue; + uint64_t cpu_affinity_mask; + int32_t cpu_thread_priority; + bool optimize_progressive; + bool check_memory_type; + uint8_t reserved0; + int8_t extra_dpb_frames; + void* extra_config; + bool disable_sync_input; + uint32_t max_pending_sync; +}; + +struct MemoryInfo { + size_t this_size; + size_t cpu_memory_size; + void* cpu_memory; + size_t gpu_memory_size; + void* gpu_memory; + size_t cpu_gpu_memory_size; + void* cpu_gpu_memory; + size_t max_frame_buffer_size; + uint32_t frame_buffer_alignment; + uint32_t reserved0; +}; + +struct InputData { + size_t this_size; + void* au_data; + size_t au_size; + uint64_t pts; + uint64_t dts; + uint64_t attached_data; +}; + +struct InputResult { + size_t this_size; + void* decoded_au; + uint32_t output_frame_count; + uint32_t reserved0; +}; + +struct FrameBuffer { + size_t this_size; + void* data; + size_t size; +}; + +struct OutputInfo { + size_t this_size; + bool valid; + bool last_frame; + bool error_frame; + uint8_t picture_count; + uint32_t codec_type; + uint32_t width; + uint32_t pitch; + uint32_t height; + bool discarded_frame; + void* frame_buffer; + size_t frame_buffer_size; + uint32_t format; + uint32_t pitch_in_bytes; +}; + +static_assert(sizeof(ComputeMemoryInfo) == 24); +static_assert(sizeof(ComputeConfigInfo) == 16); +static_assert(sizeof(ConfigInfo) == 80); +static_assert(offsetof(ConfigInfo, extra_dpb_frames) == 63); +static_assert(offsetof(ConfigInfo, disable_sync_input) == 72); +static_assert(sizeof(MemoryInfo) == 72); +static_assert(sizeof(InputData) == 48); +static_assert(sizeof(InputResult) == 24); +static_assert(sizeof(FrameBuffer) == 24); +static_assert(sizeof(OutputInfo) == 56); +static_assert(offsetof(OutputInfo, discarded_frame) == 28); +static_assert(offsetof(OutputInfo, frame_buffer) == 32); + +struct State; +static std::shared_mutex g_registry_mutex; +static std::unordered_set g_queues; +static std::unordered_set g_decoders; + +struct State { + ConfigInfo config; + Decoder::Instance* decoder; + std::mutex mutex; + std::deque completed_inputs; + std::optional output_buffer; + + ~State() { + Decoder::Destroy(decoder); + --config.compute_queue->decoders; + } +}; + +// The shared registry lock keeps guest-backed state alive until each API call returns. +// Deletion takes the exclusive lock before releasing that memory to the guest. +struct StateAccess { + std::shared_lock lock {g_registry_mutex}; + State* value; + explicit StateAccess(State* handle): value(g_decoders.contains(handle) ? handle : nullptr) {} + explicit operator bool() const { return value != nullptr; } + State* operator->() const { return value; } +}; + +static int32_t Result(Decoder::Result result) { + switch (result) { + case Decoder::Result::Ok: return 0; + case Decoder::Result::ApiFail: return Fail(Error::ApiFail); + case Decoder::Result::AccessUnit: return Fail(Error::AccessUnit); + case Decoder::Result::FrameBufferSize: return Fail(Error::FrameBufferSize); + case Decoder::Result::OversizeDecode: return Fail(Error::OversizeDecode); + case Decoder::Result::InputFull: return Fail(Error::OutputBufferFull); + case Decoder::Result::NoFrame: return Fail(Error::DecodePending); + } + return Fail(Error::ApiFail); +} + +static int32_t ValidateConfig(const ConfigInfo* config) { + if (config == nullptr) return Fail(Error::ArgumentPointer); + if (config->this_size != sizeof(ConfigInfo)) return Fail(Error::StructSize); + if (config->resource_type != 1) return Fail(Error::ResourceType); + if (config->codec_type != 1) return Fail(Error::CodecType); + if (config->reserved0 != 0) return Fail(Error::ConfigInfo); + if (config->extra_config != nullptr) return Fail(Error::ExtraConfigInfo); + if (config->input_queue_depth < 1 || config->input_queue_depth > 8) + return Fail(Error::InputQueueDepth); + if (config->max_dpb_frames < 1 || config->extra_dpb_frames < 0) + return Fail(Error::DpbFrameCount); + if (config->max_width <= 0 || config->max_height <= 0) return Fail(Error::FrameWidthHeight); + return 0; +} + +static size_t FrameSize(const ConfigInfo& config) { + // AVC output: 256-byte pitch, 16-line maximum height, and 4096 bytes of metadata. + const auto pitch = (static_cast(config.max_width) + 255u) & ~size_t {255}; + const auto height = (static_cast(config.max_height) + 15u) & ~size_t {15}; + return pitch * height * 3 / 2 + 4096; +} + +static int32_t KYTY_SYSV_ABI QueryComputeMemoryInfo(ComputeMemoryInfo* memory) { + PRINT_NAME(); + if (memory == nullptr) return Fail(Error::ArgumentPointer); + if (memory->this_size != sizeof(ComputeMemoryInfo)) return Fail(Error::StructSize); + memory->cpu_gpu_memory_size = sizeof(ComputeQueue); + return 0; +} + +static int32_t KYTY_SYSV_ABI AllocateComputeQueue(const ComputeConfigInfo* config, + const ComputeMemoryInfo* memory, + ComputeQueue** output) { + PRINT_NAME(); + if (config == nullptr || memory == nullptr || output == nullptr) + return Fail(Error::ArgumentPointer); + if (config->this_size != sizeof(ComputeConfigInfo) || + memory->this_size != sizeof(ComputeMemoryInfo)) + return Fail(Error::StructSize); + if (config->reserved0 != 0 || config->reserved1 != 0) return Fail(Error::ConfigInfo); + if (config->pipe_id > 4) return Fail(Error::ComputePipeId); + if (config->queue_id > 7) return Fail(Error::ComputeQueueId); + if (memory->cpu_gpu_memory_size < sizeof(ComputeQueue)) return Fail(Error::MemorySize); + if (memory->cpu_gpu_memory == nullptr || + reinterpret_cast(memory->cpu_gpu_memory) % alignof(ComputeQueue) != 0) + return Fail(Error::MemoryPointer); + std::scoped_lock lock(g_registry_mutex); + for (const auto* queue: g_queues) + if ((queue->pipe_id == config->pipe_id && queue->queue_id == config->queue_id) || + queue == memory->cpu_gpu_memory) + return Fail(Error::ComputeQueue); + auto* queue = new (memory->cpu_gpu_memory) ComputeQueue {config->pipe_id, config->queue_id}; + g_queues.insert(queue); + *output = queue; + return 0; +} + +static int32_t KYTY_SYSV_ABI ReleaseComputeQueue(ComputeQueue* queue) { + PRINT_NAME(); + std::scoped_lock lock(g_registry_mutex); + if (!g_queues.contains(queue) || queue->decoders != 0) return Fail(Error::ComputeQueue); + g_queues.erase(queue); + queue->~ComputeQueue(); + return 0; +} + +static int32_t KYTY_SYSV_ABI QueryDecoderMemoryInfo(const ConfigInfo* config, MemoryInfo* memory) { + PRINT_NAME(); + if (const auto error = ValidateConfig(config); error != 0) return error; + if (memory == nullptr) return Fail(Error::ArgumentPointer); + if (memory->this_size != sizeof(MemoryInfo)) return Fail(Error::StructSize); + memory->cpu_memory_size = sizeof(State); + // The FFmpeg backend needs no guest GPU workspace. + memory->gpu_memory_size = 0; + memory->cpu_gpu_memory_size = 0; + memory->max_frame_buffer_size = FrameSize(*config); + memory->frame_buffer_alignment = 256; + memory->reserved0 = 0; + return 0; +} + +static int32_t KYTY_SYSV_ABI CreateDecoder(const ConfigInfo* config, const MemoryInfo* memory, + State** output) { + PRINT_NAME(); + if (const auto error = ValidateConfig(config); error != 0) return error; + if (memory == nullptr || output == nullptr) return Fail(Error::ArgumentPointer); + if (memory->this_size != sizeof(MemoryInfo)) return Fail(Error::StructSize); + if (memory->cpu_memory_size < sizeof(State)) return Fail(Error::MemorySize); + if (memory->cpu_memory == nullptr || + reinterpret_cast(memory->cpu_memory) % alignof(State) != 0) + return Fail(Error::MemoryPointer); + std::scoped_lock lock(g_registry_mutex); + if (!g_queues.contains(config->compute_queue)) return Fail(Error::ComputeQueue); + if (g_decoders.contains(static_cast(memory->cpu_memory))) + return Fail(Error::MemoryPointer); + auto* decoder = Decoder::Create( + {config->codec_type, config->max_width, config->max_height, + std::max(2u, static_cast(config->max_dpb_frames) + config->extra_dpb_frames)}); + if (decoder == nullptr) return Fail(Error::ApiFail); + auto* state = new (memory->cpu_memory) State {*config, decoder}; + ++config->compute_queue->decoders; + g_decoders.insert(state); + *output = state; + return 0; +} + +static int32_t KYTY_SYSV_ABI DeleteDecoder(State* handle) { + PRINT_NAME(); + std::scoped_lock lock(g_registry_mutex); + if (g_decoders.erase(handle) == 0) return Fail(Error::DecoderInstance); + handle->~State(); + return 0; +} + +static int32_t KYTY_SYSV_ABI SetDecodeInput(State* handle, const InputData* input) { + PRINT_NAME(); + const StateAccess state(handle); + if (!state) return Fail(Error::DecoderInstance); + if (input == nullptr) return Fail(Error::ArgumentPointer); + if (input->this_size != sizeof(InputData)) return Fail(Error::StructSize); + if (input->au_data == nullptr) return Fail(Error::AccessUnitPointer); + if (input->au_size == 0) return Fail(Error::AccessUnitSize); + std::scoped_lock lock(state->mutex); + const auto& config = state->config; + if (!config.disable_sync_input && + state->completed_inputs.size() >= + uint64_t {config.input_queue_depth} + config.max_pending_sync) + return Fail(Error::InputQueueFull); + uint32_t produced = 0; + const auto result = Decoder::Submit( + state->decoder, + {input->au_data, input->au_size, input->pts, input->dts, input->attached_data}, &produced); + if (result != Decoder::Result::Ok) return Result(result); + if (!config.disable_sync_input) + state->completed_inputs.push_back({sizeof(InputResult), input->au_data, produced, 0}); + return 0; +} + +static int32_t KYTY_SYSV_ABI TrySyncDecodeInput(State* handle, InputResult* output) { + PRINT_NAME(); + const StateAccess state(handle); + if (!state) return Fail(Error::DecoderInstance); + if (output == nullptr) return Fail(Error::ArgumentPointer); + if (output->this_size != sizeof(InputResult)) return Fail(Error::StructSize); + std::scoped_lock lock(state->mutex); + if (state->config.disable_sync_input) return Fail(Error::ApiFail); + if (state->completed_inputs.empty()) return Fail(Error::InputQueueEmpty); + *output = state->completed_inputs.front(); + state->completed_inputs.pop_front(); + return 0; +} + +static int32_t KYTY_SYSV_ABI SetDecodeOutput(State* handle, FrameBuffer* buffer) { + PRINT_NAME(); + const StateAccess state(handle); + if (!state) return Fail(Error::DecoderInstance); + if (buffer == nullptr) return Fail(Error::ArgumentPointer); + if (buffer->this_size != sizeof(FrameBuffer)) return Fail(Error::StructSize); + if (buffer->data == nullptr) return Fail(Error::FrameBufferPointer); + if (reinterpret_cast(buffer->data) % 256 != 0) + return Fail(Error::FrameBufferAlignment); + if (buffer->size < FrameSize(state->config)) return Fail(Error::FrameBufferSize); + std::scoped_lock lock(state->mutex); + if (state->output_buffer) return Fail(Error::OutputBufferFull); + state->output_buffer = *buffer; + return 0; +} + +static int32_t KYTY_SYSV_ABI TrySyncDecodeOutput(State* handle, OutputInfo* output) { + PRINT_NAME(); + const StateAccess state(handle); + if (!state) return Fail(Error::DecoderInstance); + if (output == nullptr) return Fail(Error::ArgumentPointer); + if (output->this_size != sizeof(OutputInfo)) return Fail(Error::StructSize); + std::scoped_lock lock(state->mutex); + if (!state->output_buffer) return Fail(Error::OutputBufferEmpty); + const auto& buffer = *state->output_buffer; + Decoder::Output decoded {}; + const auto result = Decoder::Receive(state->decoder, {buffer.data, buffer.size}, &decoded); + if (result != Decoder::Result::Ok) return Result(result); + *output = {sizeof(OutputInfo), + true, + decoded.last_frame, + decoded.error_frame, + 1, + decoded.codec_type, + decoded.width, + decoded.pitch, + decoded.height, + false, + decoded.buffer, + decoded.buffer_size, + 0, + decoded.pitch}; + state->output_buffer.reset(); + return 0; +} + +static int32_t KYTY_SYSV_ABI FinalizeDecodeSequence(State* handle) { + PRINT_NAME(); + const StateAccess state(handle); + if (!state) return Fail(Error::DecoderInstance); + std::scoped_lock lock(state->mutex); + return Result(Decoder::Finalize(state->decoder)); +} + +static int32_t KYTY_SYSV_ABI ResetDecoder(State* handle) { + PRINT_NAME(); + const StateAccess state(handle); + if (!state) return Fail(Error::DecoderInstance); + std::scoped_lock lock(state->mutex); + Decoder::Reset(state->decoder); + state->completed_inputs.clear(); + state->output_buffer.reset(); + return 0; +} + +static int32_t KYTY_SYSV_ABI GetAvcPictureInfo(const OutputInfo* output, + Decoder::AvcPictureInfo* first, + Decoder::AvcPictureInfo* second) { + PRINT_NAME(); + if (output == nullptr || first == nullptr) return Fail(Error::ArgumentPointer); + if (output->this_size != sizeof(OutputInfo) || first->this_size != sizeof(*first) || + (second != nullptr && second->this_size != sizeof(*second))) + return Fail(Error::StructSize); + if (!output->valid || output->codec_type != 1 || output->picture_count == 0) + return Fail(Error::OutputInfo); + Decoder::PictureInfo decoded {}; + if (!Decoder::GetPictureInfo(output->frame_buffer, &decoded) || decoded.codec_type != 1) + return Fail(Error::OutputInfo); + *first = Decoder::MakeAvcPictureInfo(decoded, sizeof(*first)); + if (second != nullptr) *second = {.this_size = sizeof(*second)}; + return 0; +} + +LIB_DEFINE(InitVdecsw_1) { + PRINT_NAME_ENABLE(true); + LIB_FUNC("0moTubWCsTM", QueryComputeMemoryInfo); + LIB_FUNC("hIgrg5h4V6s", AllocateComputeQueue); + LIB_FUNC("A+2M7EivuOU", QueryDecoderMemoryInfo); + LIB_FUNC("+L5ArV1tPGA", CreateDecoder); + LIB_FUNC("ecUtPX+dBYk", DeleteDecoder); + LIB_FUNC("fX-zOOefbbs", ReleaseComputeQueue); + LIB_FUNC("aqMiF0AgUYI", SetDecodeInput); + LIB_FUNC("veb-YBrOqo0", ResetDecoder); + LIB_FUNC("kMBw37oH8nI", TrySyncDecodeOutput); + LIB_FUNC("rgtMCOpyBSc", SetDecodeOutput); + LIB_FUNC("ihNT-uuEAr4", GetAvcPictureInfo); + LIB_FUNC("5Y6nZqIZvBg", FinalizeDecodeSequence); + LIB_FUNC("l4sQYy5wPkc", TrySyncDecodeInput); +} + +} // namespace Libs::Vdecsw diff --git a/src/libs/libVideoDec2.cpp b/src/libs/libVideoDec2.cpp index c97044c4..75d8149f 100644 --- a/src/libs/libVideoDec2.cpp +++ b/src/libs/libVideoDec2.cpp @@ -111,69 +111,7 @@ struct Videodec2FrameBuffer { bool is_accepted; }; -struct Videodec2AvcPictureInfo { - size_t this_size; - bool is_valid; - - uint64_t pts_data; - uint64_t dts_data; - uint64_t attached_data; - - uint8_t idr_picture_flag; - uint8_t profile_idc; - uint8_t level_idc; - uint32_t pic_width_in_mbs_minus1; - uint32_t pic_height_in_map_units_minus1; - uint8_t frame_mbs_only_flag; - - uint8_t frame_cropping_flag; - uint32_t frame_crop_left_offset; - uint32_t frame_crop_right_offset; - uint32_t frame_crop_top_offset; - uint32_t frame_crop_bottom_offset; - - uint8_t aspect_ratio_info_present_flag; - uint8_t aspect_ratio_idc; - uint16_t sar_width; - uint16_t sar_height; - - uint8_t video_signal_type_present_flag; - uint8_t video_format; - uint8_t video_full_range_flag; - uint8_t colour_description_present_flag; - uint8_t colour_primaries; - uint8_t transfer_characteristics; - uint8_t matrix_coefficients; - - uint8_t timing_info_present_flag; - uint32_t num_units_in_tick; - uint32_t time_scale; - uint8_t fixed_frame_rate_flag; - - uint8_t bitstream_restriction_flag; - uint8_t max_dec_frame_buffering; - - uint8_t pic_struct_present_flag; - uint8_t pic_struct; - uint8_t field_pic_flag; - uint8_t bottom_field_flag; - - uint8_t sequence_parameter_set_present_flag; - uint8_t picture_parameter_set_present_flag; - uint8_t au_delimiter_present_flag; - uint8_t end_of_sequence_present_flag; - uint8_t end_of_stream_present_flag; - uint8_t filler_data_present_flag; - uint8_t picture_timing_sei_present_flag; - uint8_t buffering_period_sei_present_flag; - - uint8_t constraint_set0_flag; - uint8_t constraint_set1_flag; - uint8_t constraint_set2_flag; - uint8_t constraint_set3_flag; - uint8_t constraint_set4_flag; - uint8_t constraint_set5_flag; -}; +using Videodec2AvcPictureInfo = Decoder::AvcPictureInfo; struct Videodec2ComputeMemoryInfo { size_t this_size; @@ -235,6 +173,8 @@ static void FillNoPictureOutput(const Videodec2FrameBuffer* frame_buffer, static int32_t MapDecoderResult(Decoder::Result result) { switch (result) { case Decoder::Result::Ok: return OK; + case Decoder::Result::InputFull: + case Decoder::Result::NoFrame: case Decoder::Result::ApiFail: return VIDEODEC2_ERROR_API_FAIL; case Decoder::Result::AccessUnit: return VIDEODEC2_ERROR_ACCESS_UNIT; case Decoder::Result::FrameBufferSize: return VIDEODEC2_ERROR_FRAME_BUFFER_SIZE; @@ -599,40 +539,7 @@ static int32_t KYTY_SYSV_ABI GetPictureInfo(const Videodec2OutputInfo* output_in return VIDEODEC2_ERROR_STRUCT_SIZE; } - Videodec2AvcPictureInfo picture {}; - picture.this_size = requested_size; - picture.is_valid = true; - picture.pts_data = decoded.pts; - picture.dts_data = decoded.dts; - picture.attached_data = decoded.attached_data; - picture.idr_picture_flag = decoded.key_frame ? 1 : 0; - picture.profile_idc = static_cast(decoded.profile); - picture.level_idc = static_cast(decoded.level); - picture.pic_width_in_mbs_minus1 = (decoded.width + 15u) / 16u - 1u; - picture.pic_height_in_map_units_minus1 = (decoded.height + 15u) / 16u - 1u; - picture.frame_mbs_only_flag = 1; - picture.frame_cropping_flag = decoded.crop_left != 0 || decoded.crop_right != 0 || - decoded.crop_top != 0 || decoded.crop_bottom != 0 - ? 1 - : 0; - picture.frame_crop_left_offset = decoded.crop_left; - picture.frame_crop_right_offset = decoded.crop_right; - picture.frame_crop_top_offset = decoded.crop_top; - picture.frame_crop_bottom_offset = decoded.crop_bottom; - picture.aspect_ratio_info_present_flag = - decoded.sar_width != 0 && decoded.sar_height != 0 ? 1 : 0; - picture.aspect_ratio_idc = picture.aspect_ratio_info_present_flag ? 255 : 0; - picture.sar_width = decoded.sar_width; - picture.sar_height = decoded.sar_height; - picture.video_signal_type_present_flag = 1; - picture.video_format = 5; - picture.video_full_range_flag = decoded.color_range == 2 ? 1 : 0; - picture.colour_description_present_flag = - decoded.color_primaries != 0 || decoded.color_trc != 0 || decoded.color_space != 0 ? 1 - : 0; - picture.colour_primaries = decoded.color_primaries; - picture.transfer_characteristics = decoded.color_trc; - picture.matrix_coefficients = decoded.color_space; + const auto picture = Decoder::MakeAvcPictureInfo(decoded, requested_size); std::memcpy(first_picture_info, &picture, requested_size); } else { const auto result = fill_common(first_picture_info, true); diff --git a/src/libs/libs.cpp b/src/libs/libs.cpp index c366dadb..72503244 100644 --- a/src/libs/libs.cpp +++ b/src/libs/libs.cpp @@ -22,6 +22,10 @@ namespace VideoDec2 { LIB_DEFINE(InitVideoDec2_1); } // namespace VideoDec2 +namespace Vdecsw { +LIB_DEFINE(InitVdecsw_1); +} // namespace Vdecsw + namespace LibMouse { LIB_DEFINE(InitMouse_1); } // namespace LibMouse @@ -148,6 +152,7 @@ void InitAll(Loader::SymbolDatabase* s) { LibUlt::InitUlt_1(s); InitUserService_1(s); VideoDec2::InitVideoDec2_1(s); + Vdecsw::InitVdecsw_1(s); LibGen5::InitVideoOut_1(s); LibGen5::VrrStatus::InitVideoOutVrrStatus_1(s); InitWebBrowserDialog_1(s); diff --git a/src/libs/videoDec2Decoder.cpp b/src/libs/videoDec2Decoder.cpp index f7611225..2f057320 100644 --- a/src/libs/videoDec2Decoder.cpp +++ b/src/libs/videoDec2Decoder.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -77,6 +78,7 @@ public: explicit Instance(const Config& config): m_config(config) {} ~Instance() { + ClearFrames(); ClearPictureMetadata(); if (m_sws != nullptr) { sws_freeContext(m_sws); @@ -119,116 +121,138 @@ public: [[nodiscard]] uint32_t CodecType() const { return m_config.codec_type; } + [[nodiscard]] Result Submit(const Input& input, uint32_t* produced) { + std::scoped_lock lock(m_mutex); + return SubmitInput(input, produced); + } + + [[nodiscard]] Result Receive(const FrameBuffer& frame_buffer, Output* output) { + std::scoped_lock lock(m_mutex); + return ReceiveOutput(frame_buffer, output); + } + + [[nodiscard]] Result Finalize() { + std::scoped_lock lock(m_mutex); + if (m_finalized) return Result::Ok; + const auto drained = DrainFrames(); + if (drained != Result::Ok) return drained; + const int result = avcodec_send_packet(m_codec, nullptr); + if (result == AVERROR(EAGAIN)) return Result::InputFull; + if (result < 0 && result != AVERROR_EOF) return Result::ApiFail; + m_finalized = true; + return DrainFrames(); + } + [[nodiscard]] Result DecodeInput(const Input& input, const FrameBuffer& frame_buffer, Output* output) { std::scoped_lock lock(m_mutex); *output = {}; - - AVPacket* packet = av_packet_alloc(); - AVFrame* frame = av_frame_alloc(); - if (packet == nullptr || frame == nullptr || - input.size > static_cast(std::numeric_limits::max())) { - av_packet_free(&packet); - av_frame_free(&frame); - return Result::ApiFail; + uint32_t produced = 0; + auto result = SubmitInput(input, &produced); + if (result == Result::InputFull) { + // The combined API supplies an output buffer together with each AU. + result = ReceiveOutput(frame_buffer, output); + if (result != Result::Ok) return result; + return SubmitInput(input, &produced); } - - int result = av_new_packet(packet, static_cast(input.size)); - if (result < 0) { - LOGF("Videodec2: av_new_packet failed: %s (%d)\n", AvErrorString(result), result); - av_packet_free(&packet); - av_frame_free(&frame); - return Result::ApiFail; - } - std::memcpy(packet->data, input.data, input.size); - packet->pts = ToAvTimestamp(input.pts); - packet->dts = ToAvTimestamp(input.dts); - - packet->opaque_ref = av_buffer_alloc(sizeof(PacketMetadata)); - if (packet->opaque_ref == nullptr) { - av_packet_free(&packet); - av_frame_free(&frame); - return Result::ApiFail; - } - const PacketMetadata metadata {input.pts, input.dts, input.attached_data}; - std::memcpy(packet->opaque_ref->data, &metadata, sizeof(metadata)); - - bool have_pending_frame = false; - result = avcodec_send_packet(m_codec, packet); - if (result == AVERROR(EAGAIN)) { - result = avcodec_receive_frame(m_codec, frame); - if (result < 0) { - LOGF("Videodec2: decoder rejected an AU while no output was available: %s (%d)\n", - AvErrorString(result), result); - av_packet_free(&packet); - av_frame_free(&frame); - return Result::AccessUnit; - } - have_pending_frame = true; - result = avcodec_send_packet(m_codec, packet); - } - if (result < 0) { - LOGF("Videodec2: avcodec_send_packet failed: %s (%d)\n", AvErrorString(result), result); - av_packet_free(&packet); - av_frame_free(&frame); - return Result::AccessUnit; - } - - Result decode_result = Result::Ok; - if (!have_pending_frame) { - result = avcodec_receive_frame(m_codec, frame); - if (result != AVERROR(EAGAIN) && result != AVERROR_EOF) { - if (result < 0) { - LOGF("Videodec2: avcodec_receive_frame failed: %s (%d)\n", - AvErrorString(result), result); - decode_result = Result::AccessUnit; - } else { - decode_result = CopyFrame(frame, frame_buffer, output); - } - } - } else { - decode_result = CopyFrame(frame, frame_buffer, output); - } - - av_packet_free(&packet); - av_frame_free(&frame); - return decode_result; + if (result != Result::Ok) return result; + result = ReceiveOutput(frame_buffer, output); + return result == Result::NoFrame ? Result::Ok : result; } [[nodiscard]] Result FlushOutput(const FrameBuffer& frame_buffer, Output* output) { std::scoped_lock lock(m_mutex); - *output = {}; - - AVFrame* frame = av_frame_alloc(); - if (frame == nullptr) { - return Result::ApiFail; - } - - // Guest Flush collects available pictures between AUs while retaining reference frames. - const int receive_result = avcodec_receive_frame(m_codec, frame); - if (receive_result == AVERROR(EAGAIN) || receive_result == AVERROR_EOF) { - av_frame_free(&frame); - return Result::Ok; - } - if (receive_result < 0) { - LOGF("Videodec2: receiving a flushed frame failed: %s (%d)\n", - AvErrorString(receive_result), receive_result); - av_frame_free(&frame); - return Result::ApiFail; - } - - const auto result = CopyFrame(frame, frame_buffer, output); - av_frame_free(&frame); - return result; + const auto result = ReceiveOutput(frame_buffer, output); + return result == Result::NoFrame ? Result::Ok : result; } void ResetDecoder() { std::scoped_lock lock(m_mutex); avcodec_flush_buffers(m_codec); + ClearFrames(); ClearPictureMetadata(); + m_finalized = m_eof = false; } private: + struct PendingFrame { + AVFrame* frame; + PictureInfo picture; + }; + + void ClearFrames() { + for (auto& pending: m_frames) av_frame_free(&pending.frame); + m_frames.clear(); + } + + [[nodiscard]] Result DrainFrames(uint32_t* produced = nullptr) { + while (!m_eof && m_frames.size() < m_config.max_pending_frames) { + AVFrame* frame = av_frame_alloc(); + if (frame == nullptr) return Result::ApiFail; + const int result = avcodec_receive_frame(m_codec, frame); + if (result < 0) { + av_frame_free(&frame); + if (result == AVERROR_EOF) m_eof = true; + if (result == AVERROR(EAGAIN) || m_eof) return Result::Ok; + LOGF("Video decoder: avcodec_receive_frame failed: %s (%d)\n", + AvErrorString(result), result); + return Result::AccessUnit; + } + m_frames.push_back({frame, MakePictureInfo(frame)}); + if (produced != nullptr) ++*produced; + } + return Result::Ok; + } + + [[nodiscard]] Result SubmitInput(const Input& input, uint32_t* produced) { + *produced = 0; + if (m_finalized) return Result::ApiFail; + const auto drained = DrainFrames(); + if (drained != Result::Ok) return drained; + if (m_frames.size() >= m_config.max_pending_frames) return Result::InputFull; + if (input.size > static_cast(std::numeric_limits::max())) return Result::ApiFail; + AVPacket* packet = av_packet_alloc(); + if (packet == nullptr) return Result::ApiFail; + int result = av_new_packet(packet, static_cast(input.size)); + if (result < 0) { + av_packet_free(&packet); + return Result::ApiFail; + } + std::memcpy(packet->data, input.data, input.size); + packet->pts = ToAvTimestamp(input.pts); + packet->dts = ToAvTimestamp(input.dts); + packet->opaque_ref = av_buffer_alloc(sizeof(PacketMetadata)); + if (packet->opaque_ref == nullptr) { + av_packet_free(&packet); + return Result::ApiFail; + } + const PacketMetadata metadata {input.pts, input.dts, input.attached_data}; + std::memcpy(packet->opaque_ref->data, &metadata, sizeof(metadata)); + result = avcodec_send_packet(m_codec, packet); + av_packet_free(&packet); + if (result == AVERROR(EAGAIN)) return Result::InputFull; // The AU was not accepted. + if (result < 0) { + LOGF("Video decoder: submitting an AU failed: %s (%d)\n", AvErrorString(result), result); + return Result::AccessUnit; + } + // Once accepted, a full output queue cannot turn this AU into a retry. + return DrainFrames(produced); + } + + [[nodiscard]] Result ReceiveOutput(const FrameBuffer& frame_buffer, Output* output) { + *output = {}; + const auto drained = DrainFrames(); + if (drained != Result::Ok) return drained; + if (m_frames.empty()) return Result::NoFrame; + auto& pending = m_frames.front(); + const auto result = CopyFrame(pending.frame, pending.picture, frame_buffer, output); + if (result != Result::Ok) return result; + output->last_frame = m_finalized && m_eof && m_frames.size() == 1u; + av_frame_free(&pending.frame); + m_frames.pop_front(); + return Result::Ok; + } + [[nodiscard]] PictureInfo MakePictureInfo(const AVFrame* frame) const { PictureInfo result {}; if (frame->opaque_ref != nullptr && frame->opaque_ref->size >= sizeof(PacketMetadata)) { @@ -268,8 +292,8 @@ private: return result; } - [[nodiscard]] Result CopyFrame(const AVFrame* frame, const FrameBuffer& frame_buffer, - Output* output) { + [[nodiscard]] Result CopyFrame(const AVFrame* frame, const PictureInfo& picture, + const FrameBuffer& frame_buffer, Output* output) { if (frame->width <= 0 || frame->height <= 0) { return Result::ApiFail; } @@ -329,7 +353,7 @@ private: { std::scoped_lock lock(g_picture_mutex); - g_picture_infos[frame_buffer.data] = {this, MakePictureInfo(frame)}; + g_picture_infos[frame_buffer.data] = {this, picture}; m_picture_buffers.insert(frame_buffer.data); } return Result::Ok; @@ -346,6 +370,9 @@ private: m_picture_buffers.clear(); } + std::deque m_frames; + bool m_finalized = false; + bool m_eof = false; Config m_config; AVCodecContext* m_codec = nullptr; SwsContext* m_sws = nullptr; @@ -358,7 +385,7 @@ bool IsCodecSupported(uint32_t codec_type) { } Instance* Create(const Config& config) { - if (!IsCodecSupported(config.codec_type)) { + if (!IsCodecSupported(config.codec_type) || config.max_pending_frames < 2u) { return nullptr; } auto* instance = new Instance(config); @@ -377,6 +404,18 @@ uint32_t GetCodecType(const Instance* instance) { return instance->CodecType(); } +Result Submit(Instance* instance, const Input& input, uint32_t* produced) { + return instance->Submit(input, produced); +} + +Result Receive(Instance* instance, const FrameBuffer& frame_buffer, Output* output) { + return instance->Receive(frame_buffer, output); +} + +Result Finalize(Instance* instance) { + return instance->Finalize(); +} + Result Decode(Instance* instance, const Input& input, const FrameBuffer& frame_buffer, Output* output) { return instance->DecodeInput(input, frame_buffer, output); @@ -390,6 +429,44 @@ void Reset(Instance* instance) { instance->ResetDecoder(); } +AvcPictureInfo MakeAvcPictureInfo(const PictureInfo& decoded, size_t size) { + AvcPictureInfo picture {}; + picture.this_size = size; + picture.is_valid = true; + picture.pts_data = decoded.pts; + picture.dts_data = decoded.dts; + picture.attached_data = decoded.attached_data; + picture.idr_picture_flag = decoded.key_frame ? 1 : 0; + picture.profile_idc = static_cast(decoded.profile); + picture.level_idc = static_cast(decoded.level); + picture.pic_width_in_mbs_minus1 = (decoded.width + 15u) / 16u - 1u; + picture.pic_height_in_map_units_minus1 = (decoded.height + 15u) / 16u - 1u; + picture.frame_mbs_only_flag = 1; + picture.frame_cropping_flag = decoded.crop_left != 0 || decoded.crop_right != 0 || + decoded.crop_top != 0 || decoded.crop_bottom != 0 + ? 1 + : 0; + picture.frame_crop_left_offset = decoded.crop_left; + picture.frame_crop_right_offset = decoded.crop_right; + picture.frame_crop_top_offset = decoded.crop_top; + picture.frame_crop_bottom_offset = decoded.crop_bottom; + picture.aspect_ratio_info_present_flag = + decoded.sar_width != 0 && decoded.sar_height != 0 ? 1 : 0; + picture.aspect_ratio_idc = picture.aspect_ratio_info_present_flag ? 255 : 0; + picture.sar_width = decoded.sar_width; + picture.sar_height = decoded.sar_height; + picture.video_signal_type_present_flag = 1; + picture.video_format = 5; + picture.video_full_range_flag = decoded.color_range == 2 ? 1 : 0; + picture.colour_description_present_flag = + decoded.color_primaries != 0 || decoded.color_trc != 0 || decoded.color_space != 0 ? 1 + : 0; + picture.colour_primaries = decoded.color_primaries; + picture.transfer_characteristics = decoded.color_trc; + picture.matrix_coefficients = decoded.color_space; + return picture; +} + bool GetPictureInfo(void* frame_buffer, PictureInfo* picture_info) { std::scoped_lock lock(g_picture_mutex); const auto it = g_picture_infos.find(frame_buffer); diff --git a/src/libs/videoDec2Decoder.h b/src/libs/videoDec2Decoder.h index b4c10914..0667a77c 100644 --- a/src/libs/videoDec2Decoder.h +++ b/src/libs/videoDec2Decoder.h @@ -14,12 +14,15 @@ enum class Result { AccessUnit, FrameBufferSize, OversizeDecode, + InputFull, + NoFrame, }; struct Config { - uint32_t codec_type = 0; - int32_t max_width = -1; - int32_t max_height = -1; + uint32_t codec_type = 0; + int32_t max_width = -1; + int32_t max_height = -1; + uint32_t max_pending_frames = 2; }; struct Input { @@ -37,6 +40,7 @@ struct FrameBuffer { struct Output { bool valid = false; + bool last_frame = false; bool error_frame = false; bool buffer_accepted = false; uint32_t codec_type = 0; @@ -69,12 +73,83 @@ struct PictureInfo { bool key_frame = false; }; +struct AvcPictureInfo { + size_t this_size; + bool is_valid; + + uint64_t pts_data; + uint64_t dts_data; + uint64_t attached_data; + + uint8_t idr_picture_flag; + uint8_t profile_idc; + uint8_t level_idc; + uint32_t pic_width_in_mbs_minus1; + uint32_t pic_height_in_map_units_minus1; + uint8_t frame_mbs_only_flag; + + uint8_t frame_cropping_flag; + uint32_t frame_crop_left_offset; + uint32_t frame_crop_right_offset; + uint32_t frame_crop_top_offset; + uint32_t frame_crop_bottom_offset; + + uint8_t aspect_ratio_info_present_flag; + uint8_t aspect_ratio_idc; + uint16_t sar_width; + uint16_t sar_height; + + uint8_t video_signal_type_present_flag; + uint8_t video_format; + uint8_t video_full_range_flag; + uint8_t colour_description_present_flag; + uint8_t colour_primaries; + uint8_t transfer_characteristics; + uint8_t matrix_coefficients; + + uint8_t timing_info_present_flag; + uint32_t num_units_in_tick; + uint32_t time_scale; + uint8_t fixed_frame_rate_flag; + + uint8_t bitstream_restriction_flag; + uint8_t max_dec_frame_buffering; + + uint8_t pic_struct_present_flag; + uint8_t pic_struct; + uint8_t field_pic_flag; + uint8_t bottom_field_flag; + + uint8_t sequence_parameter_set_present_flag; + uint8_t picture_parameter_set_present_flag; + uint8_t au_delimiter_present_flag; + uint8_t end_of_sequence_present_flag; + uint8_t end_of_stream_present_flag; + uint8_t filler_data_present_flag; + uint8_t picture_timing_sei_present_flag; + uint8_t buffering_period_sei_present_flag; + + uint8_t constraint_set0_flag; + uint8_t constraint_set1_flag; + uint8_t constraint_set2_flag; + uint8_t constraint_set3_flag; + uint8_t constraint_set4_flag; + uint8_t constraint_set5_flag; +}; + +static_assert(sizeof(AvcPictureInfo) == 120); + +[[nodiscard]] AvcPictureInfo MakeAvcPictureInfo(const PictureInfo& decoded, size_t size); + class Instance; [[nodiscard]] bool IsCodecSupported(uint32_t codec_type); [[nodiscard]] Instance* Create(const Config& config); void Destroy(Instance* instance); [[nodiscard]] uint32_t GetCodecType(const Instance* instance); +[[nodiscard]] Result Submit(Instance* instance, const Input& input, uint32_t* produced); +[[nodiscard]] Result Receive(Instance* instance, const FrameBuffer& frame_buffer, Output* output); +[[nodiscard]] Result Finalize(Instance* instance); [[nodiscard]] Result Decode(Instance* instance, const Input& input, const FrameBuffer& frame_buffer, Output* output); [[nodiscard]] Result Flush(Instance* instance, const FrameBuffer& frame_buffer, Output* output); diff --git a/tests/VdecswTests.cpp b/tests/VdecswTests.cpp new file mode 100644 index 00000000..1add643e --- /dev/null +++ b/tests/VdecswTests.cpp @@ -0,0 +1,376 @@ +#include "libs/libVdecsw.cpp" + +#include +#include +#include +#include +#include + +namespace { +std::vector registrations; +} +namespace Loader { +void SymbolDatabase::Add(const SymbolResolve& symbol, uint64_t, const std::string&) { + registrations.push_back(symbol); +} +} + +namespace { +using namespace Libs::Vdecsw; + +#define CHECK(condition) do { if (!(condition)) { \ + std::fprintf(stderr, "VdecswTests:%d: %s\n", __LINE__, #condition); std::abort(); \ +} } while (false) + +// Six 16x16 YUV420 frames with Y=40,60,80,100,120,140, encoded by libx264 +// (QP18, bframes=2, b-adapt=0, keyint=12, aud=1; SEI removed). Coding order +// is I0,P3,B1,B2,P5,B4. Both delayed output and packet metadata are observable. +constexpr uint8_t Avc[] = { + 0x00,0x00,0x00,0x01,0x09,0x10,0x00,0x00,0x00,0x01,0x67,0x64,0x00,0x0a,0xac,0xd9, + 0x5e,0x84,0x00,0x00,0x03,0x00,0x04,0x00,0x00,0x03,0x00,0x30,0x3c,0x48,0x96,0x58, + 0x00,0x00,0x00,0x01,0x68,0xeb,0xe0,0x8c,0xb2,0x2c,0x00,0x00,0x01,0x65,0x88,0x84, + 0x00,0xeb,0xfe,0xf5,0x27,0xf8,0x14,0xd0,0x8c,0x2d,0x00,0x00,0x00,0x01,0x09,0x30, + 0x00,0x00,0x01,0x41,0x9a,0x23,0x6c,0x7f,0xa0,0xc3,0x2f,0x81,0xec,0xfb,0x1c,0x00, + 0x00,0x00,0x01,0x09,0x50,0x00,0x00,0x01,0x41,0x9e,0x41,0x78,0xaf,0xd6,0x81,0x00, + 0x00,0x00,0x01,0x09,0x50,0x00,0x00,0x01,0x01,0x9e,0x62,0x6a,0x49,0xff,0xd8,0x80, + 0x00,0x00,0x00,0x01,0x09,0x30,0x00,0x00,0x01,0x41,0x9a,0x65,0x49,0xa8,0x41,0x68, + 0x99,0x4c,0x14,0xff,0xa0,0xc3,0x2f,0x81,0xdd,0x0c,0xc1,0x00,0x00,0x00,0x01,0x09, + 0x50,0x00,0x00,0x01,0x01,0x9e,0x84,0x6a,0x49,0xff,0xd8,0x81, +}; +constexpr std::array PacketOffsets{0, 58, 79, 95, 112, 139, sizeof(Avc)}; +constexpr std::array PresentationOrder{0, 3, 1, 2, 5, 4}; +// One independent, immediately displayable IDR (same pixels, QP18, no B frames). +constexpr uint8_t Intra[] = { + 0x00,0x00,0x00,0x01,0x09,0x10,0x00,0x00,0x00,0x01,0x67,0x64,0x10,0x0a,0xac,0xbb, + 0xd0,0x80,0x00,0x00,0x03,0x00,0x80,0x00,0x00,0x03,0x01,0x02,0x00,0x00,0x00,0x01, + 0x68,0xee,0x02,0x32,0xc8,0xb0,0x00,0x00,0x01,0x65,0x88,0x84,0x3a,0xfe,0xf5,0x27, + 0xf8,0x14,0xd0,0x8c,0x2d, +}; + +InputData Input(uint32_t packet) { + return {sizeof(InputData), const_cast(Avc + PacketOffsets[packet]), + PacketOffsets[packet + 1] - PacketOffsets[packet], + PresentationOrder[packet] * 1000u, packet * 1000u, + 0x12340000u + PresentationOrder[packet]}; +} + +struct GuardedMemory { + uint8_t* allocation; + uint8_t* data; + size_t size; + explicit GuardedMemory(size_t bytes): size(bytes) { + allocation = static_cast(std::aligned_alloc(256, (bytes + 767u) & ~size_t{255})); + CHECK(allocation != nullptr); + data = allocation + 256; + std::memset(allocation, 0xa7, bytes + 512); + } + ~GuardedMemory() { Check(); std::free(allocation); } + void Check() const { + for (size_t i = 0; i < 256; ++i) + CHECK(allocation[i] == 0xa7 && data[size + i] == 0xa7); + } +}; + +ConfigInfo Config() { + ConfigInfo config{}; + config.this_size = sizeof(config); + config.resource_type = config.codec_type = 1; + config.profile = 100; + config.max_level = 10; + config.max_width = config.max_height = 16; + config.max_dpb_frames = 4; + config.extra_dpb_frames = 1; + config.input_queue_depth = 3; + config.cpu_thread_priority = -1; + config.optimize_progressive = true; + return config; +} + +struct Session { + GuardedMemory queue_memory{sizeof(ComputeQueue)}; + GuardedMemory state_memory{sizeof(State)}; + ConfigInfo config = Config(); + MemoryInfo memory{sizeof(MemoryInfo)}; + ComputeQueue* queue = nullptr; + State* decoder = nullptr; + + explicit Session(bool disable_sync = false) { + ComputeMemoryInfo compute{sizeof(compute), 0, queue_memory.data}; + CHECK(QueryComputeMemoryInfo(&compute) == 0); + CHECK(compute.cpu_gpu_memory_size == queue_memory.size); + CHECK(compute.cpu_gpu_memory == queue_memory.data); + ComputeConfigInfo queue_config{sizeof(queue_config), 3, 3, true, 0, 0}; + CHECK(AllocateComputeQueue(&queue_config, &compute, &queue) == 0); + config.compute_queue = queue; + config.disable_sync_input = disable_sync; + CHECK(QueryDecoderMemoryInfo(&config, &memory) == 0); + CHECK(memory.cpu_memory_size == state_memory.size); + CHECK(memory.gpu_memory_size == 0 && memory.cpu_gpu_memory_size == 0); + CHECK(memory.max_frame_buffer_size == 10240 && memory.frame_buffer_alignment == 256); + memory.cpu_memory = state_memory.data; + CHECK(CreateDecoder(&config, &memory, &decoder) == 0); + CHECK(ReleaseComputeQueue(queue) == Fail(Error::ComputeQueue)); + } + ~Session() { + CHECK(DeleteDecoder(decoder) == 0); + CHECK(DeleteDecoder(decoder) == Fail(Error::DecoderInstance)); + CHECK(ReleaseComputeQueue(queue) == 0); + CHECK(ReleaseComputeQueue(queue) == Fail(Error::ComputeQueue)); + } + void Submit(uint32_t packet) { + auto input = Input(packet); + CHECK(SetDecodeInput(decoder, &input) == 0); + InputResult result{sizeof(result)}; + CHECK(TrySyncDecodeInput(decoder, &result) == 0); + CHECK(result.decoded_au == input.au_data && result.reserved0 == 0); + } +}; + +void TestRegistrationAndErrors() { + Loader::SymbolDatabase symbols; + InitVdecsw_1(&symbols); + g_print_name = false; + constexpr std::array nids{"0moTubWCsTM", "hIgrg5h4V6s", "A+2M7EivuOU", "+L5ArV1tPGA", + "ecUtPX+dBYk", "fX-zOOefbbs", "aqMiF0AgUYI", "veb-YBrOqo0", "kMBw37oH8nI", + "rgtMCOpyBSc", "ihNT-uuEAr4", "5Y6nZqIZvBg", "l4sQYy5wPkc"}; + CHECK(registrations.size() == nids.size()); + for (size_t i = 0; i < nids.size(); ++i) { + const auto& symbol = registrations[i]; + CHECK(symbol.name == nids[i] && symbol.library == "Vdecsw" && symbol.module == "Vdecsw"); + CHECK(symbol.library_version == 1 && symbol.module_version_major == 1 && symbol.module_version_minor == 1); + } + CHECK(uint32_t(Fail(Error::StructSize)) == 0x81510101u); + CHECK(QueryComputeMemoryInfo(nullptr) == Fail(Error::ArgumentPointer)); + ComputeMemoryInfo compute{}; + CHECK(QueryComputeMemoryInfo(&compute) == Fail(Error::StructSize)); + auto config = Config(); + MemoryInfo memory{sizeof(memory)}; + config.max_width = 3840; config.max_height = 2160; config.max_level = 52; + CHECK(QueryDecoderMemoryInfo(&config, &memory) == 0 && memory.max_frame_buffer_size == 12445696u); + config.this_size = 72; + CHECK(QueryDecoderMemoryInfo(&config, &memory) == Fail(Error::StructSize)); + Session session; + GuardedMemory other_queue(sizeof(ComputeQueue)); + ComputeMemoryInfo compute_duplicate{sizeof(compute_duplicate), other_queue.size, other_queue.data}; + ComputeConfigInfo same_queue{sizeof(same_queue), 3, 3, false, 0, 0}; + ComputeQueue* duplicate = nullptr; + CHECK(AllocateComputeQueue(&same_queue, &compute_duplicate, &duplicate) == Fail(Error::ComputeQueue)); + CHECK(duplicate == nullptr); + compute_duplicate.cpu_gpu_memory = session.queue_memory.data; + same_queue.queue_id = 2; + CHECK(AllocateComputeQueue(&same_queue, &compute_duplicate, &duplicate) == Fail(Error::ComputeQueue)); + State* duplicate_decoder = nullptr; + CHECK(CreateDecoder(&session.config, &session.memory, &duplicate_decoder) == Fail(Error::MemoryPointer)); + CHECK(duplicate_decoder == nullptr); + CHECK(ResetDecoder(nullptr) == Fail(Error::DecoderInstance)); + InputResult completion{sizeof(completion)}; + CHECK(TrySyncDecodeInput(session.decoder, &completion) == Fail(Error::InputQueueEmpty)); + OutputInfo output{sizeof(output)}; + CHECK(TrySyncDecodeOutput(session.decoder, &output) == Fail(Error::OutputBufferEmpty)); + GuardedMemory pixels(session.memory.max_frame_buffer_size); + FrameBuffer frame{sizeof(frame), pixels.data + 1, pixels.size}; + CHECK(SetDecodeOutput(session.decoder, &frame) == Fail(Error::FrameBufferAlignment)); + frame.data = pixels.data; frame.size--; + CHECK(SetDecodeOutput(session.decoder, &frame) == Fail(Error::FrameBufferSize)); + frame.size++; + CHECK(SetDecodeOutput(session.decoder, &frame) == 0); + CHECK(SetDecodeOutput(session.decoder, &frame) == Fail(Error::OutputBufferFull)); + CHECK(TrySyncDecodeOutput(session.decoder, &output) == Fail(Error::DecodePending)); + CHECK(ResetDecoder(session.decoder) == 0); + CHECK(TrySyncDecodeOutput(session.decoder, &output) == Fail(Error::OutputBufferEmpty)); +} + +void CheckPicture(const OutputInfo& output, uint32_t frame, GuardedMemory& pixels) { + CHECK(output.valid && output.picture_count == 1 && !output.error_frame && !output.discarded_frame); + CHECK(output.width == 16 && output.height == 16 && output.pitch == 256 && output.pitch_in_bytes == 256); + CHECK(output.frame_buffer == pixels.data && output.frame_buffer_size == pixels.size); + for (uint32_t y = 0; y < 16; ++y) + for (uint32_t x = 0; x < 16; ++x) CHECK(pixels.data[y * 256 + x] == 40 + frame * 20); + Decoder::AvcPictureInfo picture{sizeof(picture)}; + CHECK(GetAvcPictureInfo(&output, &picture, nullptr) == 0); + CHECK(picture.is_valid && picture.pts_data == frame * 1000u); + CHECK(picture.attached_data == 0x12340000u + frame); + CHECK(picture.profile_idc == 100 && picture.level_idc == 10); + CHECK(picture.pic_width_in_mbs_minus1 == 0 && picture.pic_height_in_map_units_minus1 == 0); + pixels.Check(); +} + +void TestBFramesAndFinalization() { + Session session; + GuardedMemory pixels(session.memory.max_frame_buffer_size); + FrameBuffer buffer{sizeof(buffer), pixels.data, pixels.size}; + CHECK(SetDecodeOutput(session.decoder, &buffer) == 0); + uint32_t frames = 0; + bool registered = true; + OutputInfo output{sizeof(output)}; + const auto receive = [&] { + if (!registered) { CHECK(SetDecodeOutput(session.decoder, &buffer) == 0); registered = true; } + const auto result = TrySyncDecodeOutput(session.decoder, &output); + if (result == Fail(Error::DecodePending)) return false; + CHECK(result == 0); + CheckPicture(output, frames++, pixels); + registered = false; + return true; + }; + CHECK(!receive()); // Output may be supplied before the first AU. + for (uint32_t packet = 0; packet < 6; ++packet) { + session.Submit(packet); + while (receive()) CHECK(!output.last_frame); + } + CHECK(frames < 6); // B pictures remain in the decoder until end of sequence. + CHECK(FinalizeDecodeSequence(session.decoder) == 0); + while (frames < 6) { CHECK(receive()); CHECK(output.last_frame == (frames == 6)); } + CHECK(FinalizeDecodeSequence(session.decoder) == 0); + auto input = Input(0); + CHECK(SetDecodeInput(session.decoder, &input) == Fail(Error::ApiFail)); + CHECK(ResetDecoder(session.decoder) == 0); + Decoder::AvcPictureInfo picture{sizeof(picture)}; + CHECK(GetAvcPictureInfo(&output, &picture, nullptr) == Fail(Error::OutputInfo)); + CHECK(TrySyncDecodeOutput(session.decoder, &output) == Fail(Error::OutputBufferEmpty)); + InputResult completion{sizeof(completion)}; + CHECK(TrySyncDecodeInput(session.decoder, &completion) == Fail(Error::InputQueueEmpty)); + session.Submit(0); // Reset accepts a new sequence. +} + +void TestImmediateIntra() { + Session session; + InputData input{sizeof(input), const_cast(Intra), sizeof(Intra), 0, 0, 0x12340000}; + CHECK(SetDecodeInput(session.decoder, &input) == 0); + InputResult completion{sizeof(completion)}; + CHECK(TrySyncDecodeInput(session.decoder, &completion) == 0); + CHECK(completion.output_frame_count == 1 && completion.decoded_au == Intra); + GuardedMemory pixels(session.memory.max_frame_buffer_size); + FrameBuffer buffer{sizeof(buffer), pixels.data, pixels.size}; + CHECK(SetDecodeOutput(session.decoder, &buffer) == 0); + OutputInfo output{sizeof(output)}; + CHECK(TrySyncDecodeOutput(session.decoder, &output) == 0); + CheckPicture(output, 0, pixels); + CHECK(!output.last_frame); // Output is available without another AU or finalization. +} + +void TestCompletionOrderAndBackpressure() { + Session session; + for (uint32_t packet = 0; packet < 3; ++packet) { + auto input = Input(packet); + CHECK(SetDecodeInput(session.decoder, &input) == 0); + } + auto retry = Input(3); + CHECK(SetDecodeInput(session.decoder, &retry) == Fail(Error::InputQueueFull)); + for (uint32_t packet = 0; packet < 3; ++packet) { + InputResult output{sizeof(output)}; + CHECK(TrySyncDecodeInput(session.decoder, &output) == 0); + CHECK(output.decoded_au == Input(packet).au_data); + } + session.Submit(3); // A rejected AU was not consumed. + CHECK(ResetDecoder(session.decoder) == 0); +} + +Decoder::Input BackendInput(uint32_t packet) { + const auto input = Input(packet); + return {input.au_data, input.au_size, input.pts, input.dts, input.attached_data}; +} + +void CheckBackendPicture(const Decoder::Output& output, uint32_t frame, GuardedMemory& pixels) { + CHECK(output.valid && output.buffer == pixels.data && output.width == 16 && output.height == 16); + for (uint32_t y = 0; y < 16; ++y) + for (uint32_t x = 0; x < 16; ++x) CHECK(pixels.data[y * 256 + x] == 40 + frame * 20); + Decoder::PictureInfo picture{}; + CHECK(Decoder::GetPictureInfo(pixels.data, &picture)); + CHECK(picture.pts == frame * 1000u && picture.attached_data == 0x12340000u + frame); + pixels.Check(); +} + +void TestFailedCopyRetention() { + auto* decoder = Decoder::Create({1, 16, 16, 8}); + CHECK(decoder != nullptr); + for (uint32_t packet = 0; packet < 6; ++packet) { + uint32_t produced = 0; + CHECK(Decoder::Submit(decoder, BackendInput(packet), &produced) == Decoder::Result::Ok); + } + CHECK(Decoder::Finalize(decoder) == Decoder::Result::Ok); + GuardedMemory pixels(10240); + Decoder::Output output{}; + CHECK(Decoder::Receive(decoder, {pixels.data, 1}, &output) == Decoder::Result::FrameBufferSize); + CHECK(!output.valid); + for (uint32_t frame = 0; frame < 6; ++frame) { + CHECK(Decoder::Receive(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::Ok); + CheckBackendPicture(output, frame, pixels); + CHECK(output.last_frame == (frame == 5)); + } + CHECK(Decoder::Receive(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::NoFrame); + Decoder::PictureInfo picture{}; + Decoder::Destroy(decoder); + CHECK(!Decoder::GetPictureInfo(pixels.data, &picture)); +} + +void TestBoundedQueueFinalization() { + auto* decoder = Decoder::Create({1, 16, 16, 2}); + CHECK(decoder != nullptr); + GuardedMemory pixels(10240); + Decoder::Output output{}; + uint32_t frames = 0; + uint32_t rejected = 0; + for (uint32_t packet = 0; packet < 6;) { + uint32_t produced = 0; + const auto result = Decoder::Submit(decoder, BackendInput(packet), &produced); + if (result == Decoder::Result::InputFull) { + CHECK(produced == 0); + ++rejected; + CHECK(Decoder::Receive(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::Ok); + CheckBackendPicture(output, frames++, pixels); + CHECK(!output.last_frame); + } else { CHECK(result == Decoder::Result::Ok); ++packet; } + } + CHECK(rejected > 0 && frames < 6); + // The retained queue is full, and FFmpeg still holds delayed B pictures. + CHECK(Decoder::Finalize(decoder) == Decoder::Result::Ok); + while (frames < 6) { + CHECK(Decoder::Receive(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::Ok); + CheckBackendPicture(output, frames++, pixels); + CHECK(output.last_frame == (frames == 6)); + } + CHECK(Decoder::Receive(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::NoFrame); + Decoder::Destroy(decoder); +} + +void TestCombinedDecode() { + auto* decoder = Decoder::Create({1, 16, 16}); + CHECK(decoder != nullptr); + GuardedMemory pixels(10240); + Decoder::Output output{}; + uint32_t frames = 0; + for (uint32_t packet = 0; packet < 6; ++packet) { + CHECK(Decoder::Decode(decoder, BackendInput(packet), {pixels.data, pixels.size}, &output) == Decoder::Result::Ok); + if (output.valid) CheckBackendPicture(output, frames++, pixels); + CHECK(Decoder::Flush(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::Ok); + CHECK(!output.valid); // Legacy Flush is receive-only; the next AU remains acceptable. + } + CHECK(frames == 4); + CHECK(Decoder::Finalize(decoder) == Decoder::Result::Ok); + while (frames < 6) { + CHECK(Decoder::Flush(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::Ok); + CheckBackendPicture(output, frames++, pixels); + } + Decoder::Destroy(decoder); +} + +void TestDisabledInputSync() { + Session session(true); + auto input = Input(0); + CHECK(SetDecodeInput(session.decoder, &input) == 0); + InputResult completion{sizeof(completion)}; + CHECK(TrySyncDecodeInput(session.decoder, &completion) == Fail(Error::ApiFail)); +} +} + +int main() { + TestRegistrationAndErrors(); + TestBFramesAndFinalization(); + TestImmediateIntra(); + TestCompletionOrderAndBackpressure(); + TestFailedCopyRetention(); + TestBoundedQueueFinalization(); + TestCombinedDecode(); + TestDisabledInputSync(); + std::puts("Vdecsw tests passed"); +}