video: implement PS5 Vdecsw using a shared queued AVC decoder

Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
This commit is contained in:
nmzik 2026-10-12 21:08:58 +02:00
parent e2c8b61de6
commit a7a734b5d3
7 changed files with 1105 additions and 196 deletions

View File

@ -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 $<TARGET_FILE:archive_file_tests>)
add_test(NAME trophy_system COMMAND $<TARGET_FILE:trophy_system_tests>)
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_tests>)
add_test(NAME vdecsw COMMAND $<TARGET_FILE:vdecsw_tests>)
add_test(NAME bit_array COMMAND $<TARGET_FILE:bit_array_tests>)
add_test(NAME lru_cache COMMAND $<TARGET_FILE:lru_cache_tests>)
add_test(NAME shader_vertex_metadata COMMAND $<TARGET_FILE:shader_vertex_metadata_tests>)
@ -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

459
src/libs/libVdecsw.cpp Normal file
View File

@ -0,0 +1,459 @@
#include "common/abi.h"
#include "libs/libs.h"
#include "libs/videoDec2Decoder.h"
#include "loader/symbolDatabase.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <mutex>
#include <new>
#include <optional>
#include <shared_mutex>
#include <unordered_set>
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<int32_t>(0x81510000u | static_cast<uint32_t>(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<ComputeQueue*> g_queues;
static std::unordered_set<State*> g_decoders;
struct State {
ConfigInfo config;
Decoder::Instance* decoder;
std::mutex mutex;
std::deque<InputResult> completed_inputs;
std::optional<FrameBuffer> 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<std::shared_mutex> 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<size_t>(config.max_width) + 255u) & ~size_t {255};
const auto height = (static_cast<size_t>(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<uintptr_t>(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<uintptr_t>(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<State*>(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<uint32_t>(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<uintptr_t>(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

View File

@ -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<uint8_t>(decoded.profile);
picture.level_idc = static_cast<uint8_t>(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);

View File

@ -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);

View File

@ -6,6 +6,7 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <deque>
#include <limits>
#include <mutex>
#include <unordered_map>
@ -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<size_t>(std::numeric_limits<int>::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<int>(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<size_t>(std::numeric_limits<int>::max())) return Result::ApiFail;
AVPacket* packet = av_packet_alloc();
if (packet == nullptr) return Result::ApiFail;
int result = av_new_packet(packet, static_cast<int>(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<PendingFrame> 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<uint8_t>(decoded.profile);
picture.level_idc = static_cast<uint8_t>(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);

View File

@ -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 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);

376
tests/VdecswTests.cpp Normal file
View File

@ -0,0 +1,376 @@
#include "libs/libVdecsw.cpp"
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
namespace {
std::vector<Loader::SymbolResolve> 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<size_t, 7> PacketOffsets{0, 58, 79, 95, 112, 139, sizeof(Avc)};
constexpr std::array<uint32_t, 6> 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<uint8_t*>(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<uint8_t*>(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<uint8_t*>(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");
}