cellVdec: Fix decoding of streams without a leading sequence header and of reused AU buffers- #19658

This commit is contained in:
Antonino Di Guardo
2026-10-04 17:45:05 +00:00
committed by GitHub
parent 32b54f1864
commit fcbed0bc6d
+68 -8
View File
@@ -53,6 +53,31 @@ constexpr int averror_eof = AVERROR_EOF; // workaround for old-style-cast error
LOG_CHANNEL(cellVdec);
// Checks whether the AU holds a header that tells the decoder the picture size:
// MPEG-2: sequence header; MPEG-4 Part 2: Video Object Layer header, or a short video header (H.263 picture start code) at the start of the AU
static bool au_has_sequence_header(AVCodecID codec_id, const u8* data, u32 size)
{
if (codec_id == AV_CODEC_ID_MPEG4 && size >= 3 && data[0] == 0 && data[1] == 0 && (data[2] & 0xfc) == 0x80)
{
return true;
}
for (u32 i = 0; i + 3 < size; i++)
{
if (data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1)
{
const u8 code = data[i + 3];
if (codec_id == AV_CODEC_ID_MPEG2VIDEO ? code == 0xb3 : (code & 0xf0) == 0x20)
{
return true;
}
}
}
return false;
}
template<>
void fmt_class_string<CellVdecError>::format(std::string& out, u64 arg)
{
@@ -137,6 +162,10 @@ struct vdec_cmd
: type(_type), seq_id(_seq_id), id(_id), mode(_mode), au(std::move(_au))
{
ensure(_type == vdec_cmd_type::au_decode);
// Copy the AU now: the game may refill its buffer before the decoder thread gets to this AU
const u8* data = vm::_ptr<const u8>(au.startAddr);
au_data.assign(data, data + au.size);
}
explicit vdec_cmd(vdec_cmd_type _type, u64 _seq_id, u64 _id, s32 _framerate)
@@ -151,6 +180,7 @@ struct vdec_cmd
s32 mode{};
s32 framerate{};
CellVdecAuInfo au{};
std::vector<u8> au_data;
};
struct vdec_frame
@@ -224,6 +254,8 @@ struct vdec_context final
AVRational log_time_base{}; // Used to reduce log spam
AVRational log_framerate{}; // Used to reduce log spam
u32 skipped_au_count{}; // Used to reduce log spam
bool sequence_header_found = false;
vdec_context(s32 type, u32 /*profile*/, u32 addr, u32 size, vm::ptr<CellVdecCbMsg> func, u32 arg)
: type(type)
@@ -268,6 +300,9 @@ struct vdec_context final
fmt::throw_exception("avcodec_descriptor_get() failed (type=0x%x)", type);
}
// Only these decoders are checked for a sequence header before an AU is passed to them
sequence_header_found = codec->id != AV_CODEC_ID_MPEG2VIDEO && codec->id != AV_CODEC_ID_MPEG4;
ctx = avcodec_alloc_context3(codec);
if (!ctx)
@@ -413,13 +448,12 @@ struct vdec_context final
const bool drain = cmd->type == vdec_cmd_type::drain;
const u32 au_mode = cmd->mode;
const u32 au_addr = cmd->au.startAddr;
const u32 au_size = cmd->au.size;
const u64 au_pts = u64{cmd->au.pts.upper} << 32 | cmd->au.pts.lower;
const u64 au_dts = u64{cmd->au.dts.upper} << 32 | cmd->au.dts.lower;
au_usrd = cmd->au.userData;
packet.data = vm::_ptr<u8>(au_addr);
packet.data = cmd->au_data.data();
packet.size = au_size;
packet.pts = au_pts != umax ? au_pts : s64{smin};
packet.dts = au_dts != umax ? au_dts : s64{smin};
@@ -447,10 +481,23 @@ struct vdec_context final
{
cellVdec.trace("AU decoding: handle=0x%x, seq_id=%d, cmd_id=%d, size=0x%x, pts=0x%llx, dts=0x%llx, userdata=0x%llx", handle, cmd->seq_id, cmd->id, au_size, au_pts, au_dts, au_usrd);
if (int ret = avcodec_send_packet(ctx, drain ? nullptr : &packet); ret < 0)
if (!drain && !sequence_header_found && !au_has_sequence_header(codec->id, packet.data, au_size))
{
// The decoder cannot use an AU before a sequence header (e.g. a stream continued on a new decoder,
// or one that sends its sequence header after its first pictures): skip it
if (skipped_au_count++ == 0)
{
cellVdec.warning("No sequence header yet, skipping AUs until one arrives (handle=0x%x, seq_id=%d, cmd_id=%d)", handle, cmd->seq_id, cmd->id);
}
}
else if (int ret = avcodec_send_packet(ctx, drain ? nullptr : &packet); ret < 0)
{
fmt::throw_exception("AU queuing error (handle=0x%x, seq_id=%d, cmd_id=%d, error=0x%x): %s", handle, cmd->seq_id, cmd->id, ret, utils::av_error_to_string(ret));
}
else
{
sequence_header_found = true;
}
while (!abort_decode && seq_id == cmd->seq_id)
{
@@ -577,6 +624,12 @@ struct vdec_context final
decoded_frames.push_back(std::move(frame));
}
if (skipped_au_count && !decoded_frames.empty())
{
cellVdec.warning("Decoder output resumed after %d skipped AUs (handle=0x%x, seq_id=%d, cmd_id=%d)", skipped_au_count, handle, cmd->seq_id, cmd->id);
skipped_au_count = 0;
}
}
if (thread_ctrl::state() != thread_state::aborting)
@@ -610,10 +663,10 @@ struct vdec_context final
thread_ctrl::wait_for(10000);
if (elapsed++ >= 500) // 5 seconds
// Report a wait once: a game may keep the pictures of a decoder it does not display for a long time
if (++elapsed == 500) // 5 seconds
{
cellVdec.error("Video au decode has been waiting for a consumer for 5 seconds. (handle=0x%x, seq_id=%d, cmd_id=%d, queue_size=%d)", handle, cmd->seq_id, cmd->id, out_queue.size());
elapsed = 0;
cellVdec.warning("Video au decode has been waiting for a consumer for 5 seconds. (handle=0x%x, seq_id=%d, cmd_id=%d, queue_size=%d)", handle, cmd->seq_id, cmd->id, out_queue.size());
}
}
@@ -622,6 +675,11 @@ struct vdec_context final
break;
}
if (elapsed >= 500)
{
cellVdec.notice("Video au decode resumed after waiting %d seconds for a consumer. (handle=0x%x, seq_id=%d, cmd_id=%d)", elapsed / 100, handle, cmd->seq_id, cmd->id);
}
{
std::lock_guard lock{mutex};
out_queue.push_back(std::move(decoded_frames.front()));
@@ -1685,7 +1743,8 @@ error_code cellVdecDecodeAu(ppu_thread& ppu, u32 handle, CellVdecDecodeMode mode
if (!vdec->au_count.try_inc(4))
{
return CELL_VDEC_ERROR_BUSY;
// The AU queue is full: BUSY asks the caller to retry, it is not an error
return not_an_error(CELL_VDEC_ERROR_BUSY);
}
const u64 seq_id = vdec->seq_id;
@@ -1732,7 +1791,8 @@ error_code cellVdecDecodeAuEx2(ppu_thread& ppu, u32 handle, CellVdecDecodeMode m
if (!vdec->au_count.try_inc(4))
{
return CELL_VDEC_ERROR_BUSY;
// The AU queue is full: BUSY asks the caller to retry, it is not an error
return not_an_error(CELL_VDEC_ERROR_BUSY);
}
CellVdecAuInfo au_info{};