Files
utun2/tools/chatgui/tests/test_call_video_gpu.cpp
T

173 lines
9.7 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Настоящие software bitstream/decoder под GPU-границей FFmpeg: задержка, пачки,
* PTS QSV 90kHz, AMF с периодическим IDR, отказ замены и запрет CPU fallback.
* Драйвер не эмулируется: тест проверяет конвейер приложения, аппаратные прогоны нужны отдельно. */
#include "../src/call_video_encoder.h"
#include "../src/call_video_decoder.h"
#include <QtTest>
#include <QScopeGuard>
#include <deque>
#include <map>
#include <mutex>
extern "C" {
#include "debug_config.h"
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
}
struct Pending {
std::deque<AVPacket*> packets;
unsigned inputs = 0, serial = 0;
bool draining = false;
};
static std::mutex lock;
static std::map<AVCodecContext*, Pending> contexts;
static QByteArray selected;
static unsigned opened = 0, software = 0;
static int depth = 1;
static bool burst = false, badReplacement = false, badProbe = false;
extern "C" const AVCodec* __real_avcodec_find_encoder_by_name(const char*);
extern "C" const AVCodec* __wrap_avcodec_find_encoder_by_name(const char* name) {
QByteArray requested(name);
bool hardware = requested.endsWith("_nvenc") || requested.endsWith("_amf")
|| requested.endsWith("_qsv") || requested.endsWith("_vaapi");
if (hardware) return requested == selected ? __real_avcodec_find_encoder_by_name(
requested.startsWith("h264") ? "libx264" : "libx265") : nullptr;
++software; return __real_avcodec_find_encoder_by_name(name);
}
extern "C" int __real_avcodec_open2(AVCodecContext*, const AVCodec*, AVDictionary**);
extern "C" int __wrap_avcodec_open2(AVCodecContext* context, const AVCodec* codec, AVDictionary** options) {
if (!av_codec_is_encoder(codec)) return __real_avcodec_open2(context, codec, options);
std::lock_guard<std::mutex> guard(lock);
if (selected.endsWith("_nvenc")) {
auto* delay = av_dict_get(*options, "delay", nullptr, 0);
if (!delay || QByteArray(delay->value) != "0") return AVERROR(EINVAL);
}
if (selected.endsWith("_qsv") && context->time_base.den != 90000) return AVERROR(EINVAL);
if (selected.endsWith("_amf") && context->pix_fmt != AV_PIX_FMT_NV12 && context->pix_fmt != AV_PIX_FMT_YUV420P)
return AVERROR(EINVAL);
av_dict_free(options);
av_dict_set(options, "preset", "ultrafast", 0); av_dict_set(options, "tune", "zerolatency", 0);
av_dict_set(options, "forced-idr", "1", 0);
bool h264 = context->codec_id == AV_CODEC_ID_H264;
av_dict_set(options, "profile", h264 ? "baseline" : "main", 0);
QByteArray parameters = h264 ? "scenecut=0:open-gop=0:ref=1:level=" :
"pools=1:frame-threads=1:bframes=0:rc-lookahead=0:open-gop=0:ref=1:info=0:log-level=error:high-tier=0:level-idc=";
parameters += QByteArray::number(h264 || selected.endsWith("_qsv") ? context->level : context->level / 3);
av_dict_set(options, h264 ? "x264-params" : "x265-params", parameters.constData(), 0);
int result = __real_avcodec_open2(context, codec, options);
if (result >= 0 && !h264 && selected.endsWith("_amf")) {
// AMF разрешает dependent segments в PPS. Первый слайс остаётся независимым, CABAC не меняется.
auto* data = context->extradata;
for (int i = 0; i + 5 < context->extradata_size; ++i) {
if (!data[i] && !data[i + 1] && data[i + 2] == 1 && data[i + 3] == 68) {
if (data[i + 5] != 0xc0) return AVERROR_INVALIDDATA;
data[i + 5] |= 0x20;
if (badProbe) data[i + 4] = 2; // Неподдерживаемый temporal_id: отбрасывается общим call verifier.
}
}
}
if (result >= 0) contexts[context].serial = ++opened;
return result;
}
extern "C" int __real_avcodec_send_frame(AVCodecContext*, const AVFrame*);
extern "C" int __wrap_avcodec_send_frame(AVCodecContext* context, const AVFrame* frame) {
int result = __real_avcodec_send_frame(context, frame);
if (result >= 0) {
std::lock_guard<std::mutex> guard(lock);
if (frame) ++contexts[context].inputs; else contexts[context].draining = true;
}
return result;
}
extern "C" int __real_avcodec_receive_packet(AVCodecContext*, AVPacket*);
extern "C" int __wrap_avcodec_receive_packet(AVCodecContext* context, AVPacket* output) {
std::lock_guard<std::mutex> guard(lock);
auto& state = contexts[context];
AVPacket* packet = av_packet_alloc(); if (!packet) return AVERROR(ENOMEM);
int result = __real_avcodec_receive_packet(context, packet);
if (result >= 0) {
if (badReplacement && state.serial >= 4) packet->flags &= ~AV_PKT_FLAG_KEY;
state.packets.push_back(packet);
} else av_packet_free(&packet);
if (result < 0 && result != AVERROR(EAGAIN) && result != AVERROR_EOF) return result;
if (result == AVERROR_EOF && state.packets.empty()) return result;
bool ready = state.draining || (burst ? state.inputs % 3 == 0 : state.packets.size() > size_t(depth));
if (!ready || state.packets.empty()) return AVERROR(EAGAIN);
av_packet_move_ref(output, state.packets.front()); av_packet_free(&state.packets.front()); state.packets.pop_front(); return 0;
}
extern "C" void __real_avcodec_free_context(AVCodecContext**);
extern "C" void __wrap_avcodec_free_context(AVCodecContext** context) {
{ std::lock_guard<std::mutex> guard(lock);
auto it = contexts.find(*context);
if (it != contexts.end()) { for (auto* packet : it->second.packets) av_packet_free(&packet); contexts.erase(it); } }
__real_avcodec_free_context(context);
}
class GpuTest : public QObject {
Q_OBJECT
private slots:
void initTestCase() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); av_log_set_level(AV_LOG_ERROR); }
void init() { selected.clear(); opened = software = 0; depth = 1; burst = badReplacement = badProbe = false; QVERIFY(contexts.empty()); }
void cleanup() { QVERIFY(contexts.empty()); }
void gpuOnly() {
CallVideoEncoder encoder; call_video_format format{1, 640, 360, 30, 0, 1, 31, 4000000, 2};
QVERIFY(!encoder.open(format, 1000000)); QCOMPARE(software, 0u);
}
void incompatibleProbe() {
selected = "hevc_amf"; badProbe = true;
CallVideoEncoder encoder; call_video_format format{1, 640, 360, 30, 0, 2, 93, 4000000, 1};
QVERIFY(!encoder.open(format, 1000000));
QVERIFY2(encoder.error().contains("call bitstream contract"), qPrintable(encoder.error()));
QCOMPARE(software, 0u);
}
void pipeline_data() {
QTest::addColumn<QByteArray>("backend"); QTest::addColumn<bool>("batch"); QTest::addColumn<bool>("reject");
for (auto name : {"h264_nvenc", "hevc_nvenc", "h264_amf", "hevc_amf", "h264_qsv", "hevc_qsv"})
QTest::newRow(name) << QByteArray(name) << false << false;
QTest::newRow("batch") << QByteArray("h264_nvenc") << true << false;
QTest::newRow("rollback") << QByteArray("h264_nvenc") << false << true;
}
void pipeline() {
QFETCH(QByteArray, backend); QFETCH(bool, batch); QFETCH(bool, reject);
selected = backend; burst = batch; badReplacement = reject;
const bool h264 = selected.startsWith("h264");
call_video_format format{1, 640, 360, 30, 0, uint8_t(h264 ? 1 : 2), uint8_t(h264 ? 31 : 93), 4000000, uint8_t(h264 ? 2 : 1)};
CallVideoEncoder encoder; QVERIFY2(encoder.open(format, 1000000), qPrintable(encoder.error()));
QCOMPARE(software, 0u);
CallVideoDecoder decoder; QImage image(640, 360, QImage::Format_RGB32); image.fill(Qt::blue);
uint64_t last = 0, request = 0; unsigned shown = 0, maximumBatch = 0;
bool completed = false, failed = false;
for (unsigned i = 0; i < 120; ++i) {
if (i == 7) { request = 1; QVERIFY(encoder.requestBitrate(1, request, 300000)); }
if (request && !completed && !failed) QTest::qWait(5);
const uint64_t pts = 1234567 + i * 33333ull; QList<LiveEncodedFrame> output; CallVideoRateCompletion completion;
QVERIFY2(encoder.encode(image, pts, false, output, completion), qPrintable(encoder.error()));
if (completion.request) {
QCOMPARE(completion.request, request); QCOMPARE(completion.bitrate, reject ? 0u : 300000u);
completed = completion.bitrate != 0; failed = !completed;
}
maximumBatch = std::max(maximumBatch, unsigned(output.size()));
for (const auto& frame : output) {
QCOMPARE(frame.pts, 1234567 + shown * 33333ull); // Ни одного потерянного PTS при замене encoder.
QVERIFY(frame.pts > last); QVERIFY(frame.pts <= pts);
QCOMPARE((frame.pts - 1234567) % 33333, uint64_t(0));
QVERIFY(pts - frame.pts <= (batch ? 2u : 1u) * 33333);
call_video_frame wire{}; wire.call_id = 1; wire.frame_id = ++shown; wire.pts_us = frame.pts;
wire.format = format; wire.size = wire.length = frame.data.size(); wire.flags = frame.key ? CALL_VIDEO_KEY : 0;
wire.data = reinterpret_cast<const uint8_t*>(frame.data.constData());
QImage decoded; QVERIFY(decoder.decode(wire, decoded));
last = frame.pts;
}
}
QVERIFY(shown >= 115); QCOMPARE(completed, !reject); QCOMPARE(failed, reject);
QCOMPARE(shown + encoder.pendingFrames(), 120u);
QCOMPARE(encoder.bitrate(), reject ? 1000000u : 300000u);
if (batch) QVERIFY(maximumBatch >= 2);
qInfo("GPU pipeline %s shown=%u max_batch=%u rollback=%d", selected.constData(), shown, maximumBatch, failed);
}
};
QTEST_GUILESS_MAIN(GpuTest)
#include "test_call_video_gpu.moc"