6516b45b02
The receiver decoded H.264 to YUV420P, converted it to RGBA via a CPU-intensive swscale pass, then uploaded 4 bytes/pixel to an SDL texture — only for the GPU to convert back to RGB during rendering. This eliminated the swscale pass entirely (40-60% of receiver CPU at 1080p) and cut the texture upload by 62%. - DecodedFrame now carries three YUV420P planes with their strides instead of a packed RGBA buffer; the decoder copies the planes directly from the AVFrame (zero conversion for the common software path). Non-YUV420P decoder output (e.g. NV12 from v4l2m2m) is converted once to YUV420P. - The SDL renderer uploads via SDL_UpdateYUVTexture with SDL_PIXELFORMAT_IYUV; the GPU does the YUV→RGB conversion during rendering. - Decoder threading: slice-level with 4 threads (parallelizes within a frame, no added latency), not frame-level (which buffers multiple frames — the initial thread_count=0 broke the loopback test because the H.264 decoder introduced a multi-frame delay before producing output). - The round-trip test converts decoded YUV back to RGBA for pixel comparison via a test-local swscale call (the pipeline itself never converts). meson test 5/5 in both configurations, valgrind clean.
189 lines
7.1 KiB
C++
189 lines
7.1 KiB
C++
#include "screencast/codec/decoder.h"
|
|
#include "screencast/codec/encoder.h"
|
|
|
|
// swscale is a C library; without the extern wrapper its functions get
|
|
// C++ mangled and the linker cannot find them.
|
|
extern "C" {
|
|
#include <libswscale/swscale.h>
|
|
}
|
|
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
template <typename T> void expect_codec_result(const char* what, const sc::CodecResult<T>& result) {
|
|
if (sc::is_codec_error(result)) {
|
|
std::cerr << what << " failed: " << sc::codec_error(result).message << '\n';
|
|
std::abort();
|
|
}
|
|
}
|
|
|
|
constexpr int kWidth = 128;
|
|
constexpr int kHeight = 128;
|
|
constexpr int kFrames = 30;
|
|
constexpr int kFrameRate = 25;
|
|
constexpr uint64_t kNsPerFrame = 1'000'000'000ULL / kFrameRate;
|
|
|
|
sc::CapturedFrame make_frame(uint32_t index) {
|
|
sc::CapturedFrame frame;
|
|
frame.width = kWidth;
|
|
frame.height = kHeight;
|
|
frame.timestamp_ns = static_cast<uint64_t>(index) * kNsPerFrame;
|
|
frame.pixel_format = sc::PixelFormat::Rgba;
|
|
frame.stride = kWidth * 4;
|
|
frame.pixels.resize(static_cast<std::size_t>(kWidth) * kHeight * 4);
|
|
|
|
auto* pixels = reinterpret_cast<std::uint8_t*>(frame.pixels.data());
|
|
for (int y = 0; y < kHeight; ++y) {
|
|
for (int x = 0; x < kWidth; ++x) {
|
|
const std::size_t offset = (static_cast<std::size_t>(y) * kWidth + x) * 4;
|
|
pixels[offset + 0] = static_cast<std::uint8_t>((x + static_cast<int>(index) * 4) & 0xFF);
|
|
pixels[offset + 1] = static_cast<std::uint8_t>((y + static_cast<int>(index) * 2) & 0xFF);
|
|
pixels[offset + 2] = static_cast<std::uint8_t>(((x ^ y) + static_cast<int>(index)) & 0xFF);
|
|
pixels[offset + 3] = 0xFF;
|
|
}
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
// Convert a decoded YUV420P frame back to RGBA for pixel comparison with
|
|
// the original capture. Test-only; the pipeline itself never converts.
|
|
std::vector<std::byte> decoded_to_rgba(const sc::DecodedFrame& decoded) {
|
|
const std::size_t rgba_size = static_cast<std::size_t>(decoded.width) * decoded.height * 4;
|
|
std::vector<std::byte> rgba(rgba_size);
|
|
|
|
const uint8_t* src_planes[4] = {
|
|
reinterpret_cast<const uint8_t*>(decoded.plane_y.data()),
|
|
reinterpret_cast<const uint8_t*>(decoded.plane_u.data()),
|
|
reinterpret_cast<const uint8_t*>(decoded.plane_v.data()),
|
|
nullptr,
|
|
};
|
|
const int src_strides[4] = {decoded.stride_y, decoded.stride_u, decoded.stride_v, 0};
|
|
|
|
uint8_t* dst_planes[4] = {reinterpret_cast<uint8_t*>(rgba.data()), nullptr, nullptr, nullptr};
|
|
const int dst_strides[4] = {decoded.width * 4, 0, 0, 0};
|
|
|
|
SwsContext* scaler = sws_getContext(decoded.width,
|
|
decoded.height,
|
|
AV_PIX_FMT_YUV420P,
|
|
decoded.width,
|
|
decoded.height,
|
|
AV_PIX_FMT_RGBA,
|
|
SWS_BILINEAR,
|
|
nullptr,
|
|
nullptr,
|
|
nullptr);
|
|
assert(scaler != nullptr);
|
|
(void)sws_scale(scaler, src_planes, src_strides, 0, decoded.height, dst_planes, dst_strides);
|
|
sws_freeContext(scaler);
|
|
|
|
return rgba;
|
|
}
|
|
|
|
int max_channel_difference(const sc::DecodedFrame& decoded, const sc::CapturedFrame& expected) {
|
|
const auto decoded_rgba = decoded_to_rgba(decoded);
|
|
const auto* decoded_pixels = reinterpret_cast<const std::uint8_t*>(decoded_rgba.data());
|
|
const auto* expected_pixels = reinterpret_cast<const std::uint8_t*>(expected.pixels.data());
|
|
const std::size_t count = std::min(decoded_rgba.size(), expected.pixels.size());
|
|
|
|
int max_diff = 0;
|
|
for (std::size_t i = 0; i < count; ++i) {
|
|
const int diff = decoded_pixels[i] >= expected_pixels[i] ? decoded_pixels[i] - expected_pixels[i]
|
|
: expected_pixels[i] - decoded_pixels[i];
|
|
max_diff = std::max(max_diff, diff);
|
|
}
|
|
return max_diff;
|
|
}
|
|
|
|
bool starts_with_annex_b_prefix(const sc::EncodedFrame& frame) {
|
|
if (frame.data.size() < 4) {
|
|
return false;
|
|
}
|
|
return frame.data[0] == std::byte{0x00} && frame.data[1] == std::byte{0x00} && frame.data[2] == std::byte{0x00} &&
|
|
frame.data[3] == std::byte{0x01};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
sc::EncoderConfig encoder_config;
|
|
encoder_config.codec_name = "h264";
|
|
encoder_config.width = kWidth;
|
|
encoder_config.height = kHeight;
|
|
encoder_config.frame_rate_num = kFrameRate;
|
|
encoder_config.frame_rate_den = 1;
|
|
encoder_config.bitrate_kbps = 8000;
|
|
encoder_config.hardware_accel = false;
|
|
|
|
auto encoder_result = sc::EncoderFactory::create(encoder_config);
|
|
expect_codec_result("encoder create", encoder_result);
|
|
auto encoder = std::move(sc::codec_value(encoder_result));
|
|
|
|
// Without GLOBAL_HEADER the encoder carries no extradata; SPS/PPS are
|
|
// emitted in-band ahead of every keyframe. This mirrors the streaming
|
|
// path, where a receiver starts decoding from the bitstream alone.
|
|
const auto extradata = encoder->get_extradata();
|
|
assert(extradata.empty());
|
|
|
|
sc::DecoderConfig decoder_config;
|
|
decoder_config.codec_name = "h264";
|
|
decoder_config.width = kWidth;
|
|
decoder_config.height = kHeight;
|
|
|
|
auto decoder_result = sc::DecoderFactory::create(decoder_config);
|
|
expect_codec_result("decoder create", decoder_result);
|
|
auto decoder = std::move(sc::codec_value(decoder_result));
|
|
|
|
std::map<uint64_t, sc::CapturedFrame> expected_by_timestamp;
|
|
bool saw_keyframe = false;
|
|
bool saw_annex_b_prefix = false;
|
|
|
|
auto decode_packets = [&](const std::vector<sc::EncodedFrame>& packets) {
|
|
for (const auto& packet : packets) {
|
|
saw_keyframe = saw_keyframe || packet.is_keyframe;
|
|
saw_annex_b_prefix = saw_annex_b_prefix || starts_with_annex_b_prefix(packet);
|
|
|
|
auto decoded_result = decoder->decode(packet);
|
|
expect_codec_result("decode", decoded_result);
|
|
|
|
for (const auto& decoded : sc::codec_value(decoded_result)) {
|
|
assert(decoded.width == kWidth);
|
|
assert(decoded.height == kHeight);
|
|
|
|
auto it = expected_by_timestamp.find(decoded.capture_timestamp_ns);
|
|
assert(it != expected_by_timestamp.end());
|
|
assert(max_channel_difference(decoded, it->second) <= 64);
|
|
}
|
|
}
|
|
};
|
|
|
|
for (uint32_t i = 0; i < kFrames; ++i) {
|
|
const auto frame = make_frame(i);
|
|
expected_by_timestamp.emplace(frame.timestamp_ns, frame);
|
|
|
|
if (i == kFrames / 2) {
|
|
encoder->request_keyframe();
|
|
}
|
|
|
|
auto encoded_result = encoder->encode(frame);
|
|
expect_codec_result("encode", encoded_result);
|
|
decode_packets(sc::codec_value(encoded_result));
|
|
}
|
|
|
|
auto flushed_result = encoder->flush();
|
|
expect_codec_result("flush", flushed_result);
|
|
decode_packets(sc::codec_value(flushed_result));
|
|
|
|
assert(!expected_by_timestamp.empty());
|
|
assert(saw_keyframe);
|
|
assert(saw_annex_b_prefix);
|
|
|
|
return 0;
|
|
}
|