blob: b5138679bafbbc4602835d53b687acf5277319aa [file] [log] [blame]
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// -----------------------------------------------------------------------------
//
// AOM / AV1 wrappers
//
// Author: Skal (pascal.massimino@gmail.com)
#include "extras/aom_utils.h"
#include <cmath>
#include <cstdint>
#include <cstring>
#include <string>
#include "examples/example_utils.h"
#include "extras/ccsp_imageio.h"
#include "imageio/imageio_util.h"
#include "src/common/av1_common.h"
#include "src/utils/utils.h"
#include "src/wp2/decode.h"
#include "src/wp2/encode.h"
#if defined(WP2_HAVE_AOM)
#include "aom/aom_decoder.h"
#include "aom/aom_encoder.h"
#include "aom/aomcx.h"
#include "aom/aomdx.h"
#include "config/aom_config.h"
#include "config/aom_version.h"
#if defined(WP2_HAVE_AOM_DBG)
#include "stats/aomstats.h"
// to benefit from these, one should build libaom using the config:
// cmake ../ -DCONFIG_ACCOUNTING=1 -DCONFIG_INSPECTION=1
#include "av1/common/common_data.h"
#if defined(CONFIG_ACCOUNTING) && CONFIG_ACCOUNTING
#include "av1/decoder/accounting.h"
#endif
#if defined(CONFIG_INSPECTION) && CONFIG_INSPECTION
#include "av1/decoder/inspection.h"
#endif
#endif // defined(WP2_HAVE_AOM_DBG)
// libgav1 can be used as decoder instead of libaom
#if defined(WP2_HAVE_LIBGAV1)
#include "gav1/decoder.h"
#include "gav1/status_code.h"
#endif // defined(WP2_HAVE_LIBGAV1)
#include "examples/stopwatch.h"
#if !defined(WP2_HAVE_WEBP)
#error "WEBP support is required by WP2_HAVE_AOM"
#else
#include "webp/decode.h"
#include "webp/encode.h"
#endif // !defined(WP2_HAVE_WEBP)
#endif // WP2_HAVE_AOM
#if defined(WP2_HAVE_AVIF)
#include "avif/avif.h"
#endif
namespace WP2 {
namespace {
#if defined(WP2_HAVE_AOM)
//------------------------------------------------------------------------------
bool encode_frame(aom_codec_ctx_t* const codec, aom_image_t* const img, int pts,
aom_enc_frame_flags_t flags, std::string* const out,
bool report_error = true) {
const aom_codec_err_t err =
aom_codec_encode(codec, img, pts, /*duration=*/1, flags);
if (err != AOM_CODEC_OK) {
if (report_error) {
fprintf(stderr, "aom_codec_encode() failed ['%s']\n",
aom_codec_err_to_string(err));
const char* const details = aom_codec_error_detail(codec);
if (details != nullptr) fprintf(stderr, "Details: [%s]\n", details);
}
return false;
}
aom_codec_iter_t iter = NULL;
const aom_codec_cx_pkt_t* pkt = NULL;
bool got_pkts = false;
while ((pkt = aom_codec_get_cx_data(codec, &iter)) != NULL) {
got_pkts = true;
if (pkt->kind == AOM_CODEC_CX_FRAME_PKT) {
out->append((const char*)pkt->data.frame.buf, pkt->data.frame.sz);
}
}
return got_pkts;
}
//------------------------------------------------------------------------------
#if defined(CONFIG_INSPECTION) && CONFIG_INSPECTION
void inspect_cb(void* pbi, void* data) {
insp_frame_data* const frame_data = (insp_frame_data*)data;
ifd_inspect(frame_data, pbi, /*skip_non_transform=*/0);
}
// Given a 'mi_data_name', returns the value of the variable with that name in
// the instance 'mi' of the 'insp_mi_data' struct.
int16_t GetMiData(const insp_mi_data& mi, const std::string& mi_data_name) {
if (mi_data_name == "mode") return mi.mode;
if (mi_data_name == "uv_mode") return mi.uv_mode;
if (mi_data_name == "sb_type") return mi.bsize;
if (mi_data_name == "skip") return mi.skip;
if (mi_data_name == "segment_id") return mi.segment_id;
if (mi_data_name == "dual_filter_type") return mi.dual_filter_type;
if (mi_data_name == "filter[0]") return mi.filter[0];
if (mi_data_name == "filter[1]") return mi.filter[1];
if (mi_data_name == "tx_type") return mi.tx_type;
if (mi_data_name == "tx_size") return mi.tx_size;
if (mi_data_name == "cdef_level") return mi.cdef_level;
if (mi_data_name == "cdef_strength") return mi.cdef_strength;
if (mi_data_name == "cfl_alpha_idx") return mi.cfl_alpha_idx;
if (mi_data_name == "cfl_alpha_sign") return mi.cfl_alpha_sign;
if (mi_data_name == "current_qindex") return mi.current_qindex;
if (mi_data_name == "compound_type") return mi.compound_type;
if (mi_data_name == "motion_mode") return mi.motion_mode;
if (mi_data_name == "intrabc") return mi.intrabc;
if (mi_data_name == "palette") return mi.palette;
if (mi_data_name == "uv_palette") return mi.uv_palette;
assert(false);
return 0;
}
// Draws a 3x3 red square representing the 'value' as 9 bits.
void DrawNumber(uint32_t value, int x, int y, ArgbBuffer* const out) {
for (uint32_t j = 0; j < 3; ++j) {
for (uint32_t i = 0; i < 3; ++i) {
if (x + i < out->width() && y + j < out->height()) {
((uint32_t*)out->GetRow(y + j))[(x + i)] =
(value & 1) ? 0xffff8888u : 0xffff0000u;
}
value >>= 1;
}
}
}
// Fills rectangles with uniform colors representing the values of
// 'mi_data_name' in 'data' blocks.
void DrawMiData(const insp_frame_data& data, const std::string& mi_data_name,
bool draw_mi_number, ArgbBuffer* const out) {
const uint32_t num_cols = data.mi_cols;
const uint32_t num_rows = data.mi_rows;
assert(out != nullptr);
const auto minmax = std::minmax_element(
data.mi_grid, data.mi_grid + num_cols * num_rows,
[&](const insp_mi_data& lhs, const insp_mi_data& rhs) {
return (GetMiData(lhs, mi_data_name) < GetMiData(rhs, mi_data_name));
});
const int16_t min = GetMiData(*minmax.first, mi_data_name);
const int16_t max = GetMiData(*minmax.second, mi_data_name);
// To remember already painted areas.
std::vector<int32_t> values(num_cols * num_rows, max + 1);
for (uint32_t r = 0; r < num_rows && r * 4 < out->height(); ++r) {
for (uint32_t c = 0; c < num_cols && c * 4 < out->width(); ++c) {
const insp_mi_data& mi = data.mi_grid[r * num_cols + c];
const int16_t mi_data = GetMiData(mi, mi_data_name);
const uint8_t intensity = (mi_data - min) * 0xff / std::max(1, max - min);
const uint32_t color = 0xff000000u + 0x010101u * intensity;
if (values[r * num_cols + c] == max + 1) { // Not yet drawn.
const BLOCK_SIZE bsize = (BLOCK_SIZE)mi.bsize;
const uint32_t w = mi_size_wide[bsize], h = mi_size_high[bsize];
const uint32_t min_x = c * 4, min_y = r * 4;
out->Fill({min_x, min_y, w * 4, h * 4},
Argb32b{0xff, intensity, intensity, intensity});
if (draw_mi_number) {
DrawNumber((uint32_t)(mi_data - min), min_x + 1, min_y + 1, out);
}
for (uint32_t sub_r = r; sub_r < std::min(num_rows, r + h); ++sub_r) {
for (uint32_t sub_c = c; sub_c < std::min(num_cols, c + w); ++sub_c) {
values[sub_r * num_cols + sub_c] = mi_data;
}
}
} else if (mi_data != values[r * num_cols + c]) {
// Already drawn but non-uniform values were found within a block.
const uint32_t min_x = c * 4, min_y = r * 4;
const uint32_t max_x = std::min((c + 1) * 4, out->width());
const uint32_t max_y = std::min((r + 1) * 4, out->height());
for (uint32_t y = min_y; y < max_y; ++y) {
for (uint32_t x = min_x; x < max_x; ++x) {
if ((x + y) & 1) continue; // Semi-transparent checkerboard.
(out->GetRow8(y))[x] = color;
}
}
}
}
}
}
// Map from "qindex" to DC quantization value.
constexpr uint32_t kQIndexRange = 256;
constexpr int16_t kDcQuant[] = {
4, 8, 8, 9, 10, 11, 12, 12, 13, 14, 15, 16, 17,
18, 19, 19, 20, 21, 22, 23, 24, 25, 26, 26, 27, 28,
29, 30, 31, 32, 32, 33, 34, 35, 36, 37, 38, 38, 39,
40, 41, 42, 43, 43, 44, 45, 46, 47, 48, 48, 49, 50,
51, 52, 53, 53, 54, 55, 56, 57, 57, 58, 59, 60, 61,
62, 62, 63, 64, 65, 66, 66, 67, 68, 69, 70, 70, 71,
72, 73, 74, 74, 75, 76, 77, 78, 78, 79, 80, 81, 81,
82, 83, 84, 85, 85, 87, 88, 90, 92, 93, 95, 96, 98,
99, 101, 102, 104, 105, 107, 108, 110, 111, 113, 114, 116, 117,
118, 120, 121, 123, 125, 127, 129, 131, 134, 136, 138, 140, 142,
144, 146, 148, 150, 152, 154, 156, 158, 161, 164, 166, 169, 172,
174, 177, 180, 182, 185, 187, 190, 192, 195, 199, 202, 205, 208,
211, 214, 217, 220, 223, 226, 230, 233, 237, 240, 243, 247, 250,
253, 257, 261, 265, 269, 272, 276, 280, 284, 288, 292, 296, 300,
304, 309, 313, 317, 322, 326, 330, 335, 340, 344, 349, 354, 359,
364, 369, 374, 379, 384, 389, 395, 400, 406, 411, 417, 423, 429,
435, 441, 447, 454, 461, 467, 475, 482, 489, 497, 505, 513, 522,
530, 539, 549, 559, 569, 579, 590, 602, 614, 626, 640, 654, 668,
684, 700, 717, 736, 755, 775, 796, 819, 843, 869, 896, 925, 955,
988, 1022, 1058, 1098, 1139, 1184, 1232, 1282, 1336,
};
STATIC_ASSERT_ARRAY_SIZE(kDcQuant, kQIndexRange);
constexpr int16_t kMaxDcQuant = 1336;
// Map from "qindex" to AC quantization value.
constexpr int16_t kAcQuant[] = {
4, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54,
55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66,
67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78,
79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102,
104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126,
128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150,
152, 155, 158, 161, 164, 167, 170, 173, 176, 179, 182, 185,
188, 191, 194, 197, 200, 203, 207, 211, 215, 219, 223, 227,
231, 235, 239, 243, 247, 251, 255, 260, 265, 270, 275, 280,
285, 290, 295, 300, 305, 311, 317, 323, 329, 335, 341, 347,
353, 359, 366, 373, 380, 387, 394, 401, 408, 416, 424, 432,
440, 448, 456, 465, 474, 483, 492, 501, 510, 520, 530, 540,
550, 560, 571, 582, 593, 604, 615, 627, 639, 651, 663, 676,
689, 702, 715, 729, 743, 757, 771, 786, 801, 816, 832, 848,
864, 881, 898, 915, 933, 951, 969, 988, 1007, 1026, 1046, 1066,
1087, 1108, 1129, 1151, 1173, 1196, 1219, 1243, 1267, 1292, 1317, 1343,
1369, 1396, 1423, 1451, 1479, 1508, 1537, 1567, 1597, 1628, 1660, 1692,
1725, 1759, 1793, 1828,
};
STATIC_ASSERT_ARRAY_SIZE(kAcQuant, kQIndexRange);
constexpr int16_t kMaxAcQuant = 1828;
constexpr uint32_t kMaxSegments = 8;
// Returns the qindex corresponding to a given quant value. The quant value
// is assumed to appear in kDcQuant or kAcQuant (depending on is_dc).
uint32_t GetQIndex(int16_t quant, bool is_dc) {
const int16_t* const lookup = (is_dc ? kDcQuant : kAcQuant);
for (uint32_t i = 0; i < kQIndexRange; ++i) {
if (lookup[i] == quant) return i;
}
assert(false);
return 0;
}
void DrawQuant(const insp_frame_data& data, int channel, bool is_dc,
ArgbBuffer* const out) {
const uint32_t num_cols = data.mi_cols;
const uint32_t num_rows = data.mi_rows;
assert(out != nullptr);
// Base quant values per segment. These values do not include the deltaq
// adjustment per superblock.
const int16_t(*dequant)[2] = (channel == 0) ? &data.y_dequant[0]
: (channel == 1) ? &data.u_dequant[0]
: &data.v_dequant[0];
const int16_t max_quant = (is_dc ? kMaxDcQuant : kMaxAcQuant);
// Deltaq (i.e. qindex offset) per segment.
int16_t segment_deltaq[kMaxSegments] = {0};
for (uint32_t i = 0; i < kMaxSegments; ++i) {
const int16_t segment_quant = dequant[i][is_dc ? 0 : 1];
if (segment_quant == 0) continue; // This segment is not used.
segment_deltaq[i] = GetQIndex(segment_quant, is_dc) - data.base_qindex;
}
for (uint32_t r = 0; r < num_rows && r * 4 < out->height(); ++r) {
for (uint32_t c = 0; c < num_cols && c * 4 < out->width(); ++c) {
const insp_mi_data& mi = data.mi_grid[r * num_cols + c];
// Base qindex for the current superblock.
const int16_t sb_qindex = GetMiData(mi, "current_qindex");
const int16_t deltaq = segment_deltaq[GetMiData(mi, "segment_id")];
// Final qindex that includes the delta from the current segment.
const int16_t qindex =
Clamp<int16_t>(sb_qindex + deltaq, 0, kQIndexRange - 1);
const int16_t quant = (is_dc ? kDcQuant[qindex] : kAcQuant[qindex]);
const float normalized_quant = (float)quant / max_quant;
const uint8_t gray = Clamp(normalized_quant * 255.f, 0.f, 255.f);
const Argb32b color{255, gray, gray, gray};
out->Fill({c * 4, r * 4, 4, 4}, color);
}
}
}
std::vector<Rectangle> ExtractBoxes(const insp_frame_data& data, uint32_t width,
uint32_t height,
bool extract_transform_boxes) {
std::vector<Rectangle> boxes;
const uint32_t w = (uint32_t)data.mi_cols;
const uint32_t h = (uint32_t)data.mi_rows;
std::vector<bool> done(w * h, false);
for (uint32_t y = 0; y < h && y * 4u < height; ++y) {
for (uint32_t x = 0; x < w && x * 4u < width; ++x) {
if (done[y * w + x]) continue;
const insp_mi_data& mi = data.mi_grid[y * w + x];
const BLOCK_SIZE bsize = (BLOCK_SIZE)mi.bsize;
const uint32_t bw = mi_size_wide[bsize];
const uint32_t bh = mi_size_high[bsize];
if (extract_transform_boxes) {
// For lossy intra luma only. See av1_loopfilter.c, get_transform_size()
const TX_SIZE tx_size = (TX_SIZE)mi.tx_size;
const uint32_t tw_px = tx_size_wide[tx_size];
const uint32_t th_px = tx_size_high[tx_size];
const uint32_t tw = tw_px / 4, th = th_px / 4;
for (uint32_t ty = 0; ty < bh && (y + ty) * 4u < height; ty += th) {
for (uint32_t tx = 0; tx < bw && (x + tx) * 4u < width; tx += tw) {
const uint32_t tx_px = (x + tx) * 4u, ty_px = (y + ty) * 4u;
boxes.emplace_back(tx_px, ty_px, // Transform position/dimensions
std::min(tw_px, width - tx_px),
std::min(th_px, height - ty_px));
}
}
} else {
const uint32_t x_px = x * 4u, y_px = y * 4u;
const uint32_t bw_px = bw * 4u, bh_px = bh * 4u;
boxes.emplace_back(x_px, y_px, // Block position/dimensions
std::min(bw_px, width - x_px),
std::min(bh_px, height - y_px));
}
for (uint32_t yd = y; yd < std::min(h, y + bh); ++yd) {
for (uint32_t xd = x; xd < std::min(w, x + bw); ++xd) {
done[yd * w + xd] = true;
}
}
}
}
return boxes;
}
void DrawBoxes(const std::vector<Rectangle>& boxes, Argb32b color,
ArgbBuffer* const out) {
assert(out != nullptr);
for (const auto& rect : boxes) out->DrawRect(rect, color);
}
#endif // CONFIG_INSPECTION
// category id, similar to those reported by 'dwebp'
// clang-format off
static const char* const kCtgCoeffs = "coeffs";
static const char* const kCtgPaletteCoeffs = "palette-coeffs";
static const char* const kCtgPredModes = "pred-modes";
static const char* const kCtgSegmentId = "segment-id";
static const char* const kCtgHeader = "header";
static const char* const kCtgPaletteHeader = "palette-header";
static const char* const kCtgBlockSize = "block-size";
static std::map<std::string, const char*> kCategories = {
// coeffs
{"read_coeffs_reverse_2d", kCtgCoeffs},
{"av1_read_coeffs_txb", kCtgCoeffs},
{"read_skip_txfm", kCtgCoeffs},
{"read_coeffs_reverse", kCtgCoeffs},
{"read_golomb", kCtgCoeffs},
// pred-modes
{"read_intra_mode", kCtgPredModes},
{"read_intra_mode_uv", kCtgPredModes},
{"read_angle_delta", kCtgPredModes},
{"read_filter_intra_mode_info", kCtgPredModes},
// segment-id
{"read_segment_id", kCtgSegmentId},
// block-size
{"av1_read_tx_type", kCtgBlockSize},
{"read_selected_tx_size", kCtgBlockSize},
{"read_partition", kCtgHeader},
{"read_skip", kCtgHeader},
// everything about filters
{"read_wiener_filter", kCtgHeader},
{"loop_restoration_read_sb_coeffs", kCtgHeader},
{"read_cdef", kCtgHeader},
{"cfl:signs", kCtgHeader},
{"cfl:alpha_u", kCtgHeader},
{"cfl:alpha_v", kCtgHeader},
{"read_sgrproj_filter", kCtgHeader},
// palette
{"decode_color_map_tokens", kCtgPaletteCoeffs},
{"av1_read_uniform", kCtgPaletteHeader},
{"read_palette_colors_y", kCtgPaletteHeader},
{"read_palette_colors_uv", kCtgPaletteHeader},
{"read_palette_mode_info", kCtgPaletteHeader},
};
// clang-format on
#endif // WP2_HAVE_AOM_DBG
//------------------------------------------------------------------------------
#if defined(WP2_HAVE_AOM)
// Prints the error and returns it, if any.
#define CHECK_AOM_STATUS(status, ctx) \
do { \
const aom_codec_err_t __S__ = (status); \
if (__S__ != AOM_CODEC_OK) { \
fprintf(stderr, "Error: %s\n", aom_codec_err_to_string(__S__)); \
if ((ctx) != nullptr) { \
const char* const __D__ = aom_codec_error_detail(ctx); \
if (__D__ != nullptr) fprintf(stderr, "(%s)\n", __D__); \
} \
return MapAomStatus(__S__); \
} \
} while (0)
// From twopass_encoder.c / aomenc.c
WP2Status wp2_get_frame_stats(aom_codec_ctx_t* ctx, const aom_image_t* img,
aom_codec_pts_t pts, unsigned int duration,
aom_enc_frame_flags_t flags,
aom_fixed_buf_t* stats, bool* const got_stats) {
*got_stats = false;
aom_codec_iter_t iter = NULL;
const aom_codec_cx_pkt_t* pkt = NULL;
CHECK_AOM_STATUS(aom_codec_encode(ctx, img, pts, duration, flags), ctx);
while ((pkt = aom_codec_get_cx_data(ctx, &iter)) != NULL) {
*got_stats = true;
if (pkt->kind == AOM_CODEC_STATS_PKT) {
const uint8_t* const pkt_buf =
(const uint8_t*)pkt->data.twopass_stats.buf;
const size_t pkt_size = pkt->data.twopass_stats.sz;
stats->buf = realloc(stats->buf, stats->sz + pkt_size);
memcpy((uint8_t*)stats->buf + stats->sz, pkt_buf, pkt_size);
stats->sz += pkt_size;
}
}
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
// Converts 'ref' image into AV1 struct 'raw'.
WP2Status CreateAV1Img(const ArgbBuffer& ref, aom_image_t* const raw,
bool use_yuv444) {
YUVPlane yuv;
WP2_CHECK_STATUS(ToYCbCr(ref, /*ycbcr_bit_depth=*/{8, /*is_signed=*/false},
use_yuv444 ? nullptr : &SamplingTaps::kDownSharp,
&yuv));
const bool is_monochrome = yuv.IsMonochrome();
if (is_monochrome) {
use_yuv444 = false;
}
const int w = ref.width();
const int h = ref.height();
if (!aom_img_alloc(raw, use_yuv444 ? AOM_IMG_FMT_I444 : AOM_IMG_FMT_I420, w,
h, /*align=*/16)) {
fprintf(stderr, "aom_img_alloc() failed!");
return WP2_STATUS_OUT_OF_MEMORY;
}
// Match y4m format with luma in [16:235] range.
yuv.Y.To(raw->planes[AOM_PLANE_Y], raw->stride[AOM_PLANE_Y], 16);
if (is_monochrome) {
raw->monochrome = 1;
} else {
yuv.U.To(raw->planes[AOM_PLANE_U], raw->stride[AOM_PLANE_U], 128);
yuv.V.To(raw->planes[AOM_PLANE_V], raw->stride[AOM_PLANE_V], 128);
}
if (ref.HasTransparency()) {
fprintf(stderr, "# WARNING! Alpha channel is discarded with AV1!!\n");
}
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
// Initializes the 'codec'.
WP2Status CreateCodec(int w, int h, bool is_monochrome, const ParamsAV1& params,
aom_enc_pass pass, aom_fixed_buf_t stats,
aom_codec_ctx_t* const codec) {
const aom_codec_iface_t* const itf = aom_codec_av1_cx();
if (itf == NULL) {
fprintf(stderr, "aom_codec_av1_cx() failed!\n");
return WP2_STATUS_INVALID_PARAMETER;
}
aom_codec_enc_cfg_t cfg;
CHECK_AOM_STATUS(aom_codec_enc_config_default(itf, &cfg, 0), nullptr);
cfg.g_w = w;
cfg.g_h = h;
if (pass == AOM_RC_ONE_PASS) {
// Set the maximum number of frames to encode to 1. This instructs libaom to
// set still_picture and reduced_still_picture_header to 1 in AV1 sequence
// headers. In two-pass encoding the number of frames is available in the
// first-pass stats, so this is not necessary.
cfg.g_limit = 1;
}
cfg.g_timebase.num = 1;
cfg.g_timebase.den = 25;
cfg.g_bit_depth = AOM_BITS_8;
cfg.g_usage =
(params.effort == 0) ? AOM_USAGE_REALTIME : AOM_USAGE_GOOD_QUALITY;
cfg.g_profile = ((params.use_yuv444 && !is_monochrome) ? 1 : 0);
cfg.g_threads = params.threads;
cfg.rc_end_usage = AOM_Q;
cfg.g_pass = pass;
cfg.rc_twopass_stats_in = stats;
cfg.monochrome = is_monochrome;
CHECK_AOM_STATUS(aom_codec_enc_init(codec, itf, &cfg, 0), nullptr);
return WP2_STATUS_OK;
}
// Configures the 'codec'.
WP2Status ConfigureCodec(const ParamsAV1& params,
aom_codec_ctx_t* const codec) {
if (params.disable_transforms) {
CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_INTRA_DCT_ONLY, 1),
codec);
}
if (params.disable_palette) {
CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_PALETTE, 0),
codec);
}
if (params.disable_block_copy) {
CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_INTRABC, 0),
codec);
}
if (params.disable_trellis) {
CHECK_AOM_STATUS(
aom_codec_control(codec, AV1E_SET_DISABLE_TRELLIS_QUANT, 1), codec);
}
if (params.disable_predictors) {
// TODO(maryla): replace with AV1E_SET_ENABLE_DIRECTIONAL_INTRA when
// available
CHECK_AOM_STATUS(
aom_codec_control(codec, AV1E_SET_ENABLE_DIAGONAL_INTRA, 0), codec);
CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_FILTER_INTRA, 0),
codec);
CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_SMOOTH_INTRA, 0),
codec);
CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_PAETH_INTRA, 0),
codec);
CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_CFL_INTRA, 0),
codec);
}
if (params.force_block_size > 0) {
WP2_CHECK_OK(
params.force_block_size == 4 || params.force_block_size == 8 ||
params.force_block_size == 16 || params.force_block_size == 32 ||
params.force_block_size == 64 || params.force_block_size == 128,
WP2_STATUS_INVALID_PARAMETER);
CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_MAX_PARTITION_SIZE,
params.force_block_size),
codec);
CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_MIN_PARTITION_SIZE,
params.force_block_size),
codec);
}
if (params.quality > 95) {
CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_LOSSLESS, 1), codec);
} else {
const int level = std::lround(63 - params.quality * 63 / 95);
CHECK_AOM_STATUS(aom_codec_control(codec, AOME_SET_CQ_LEVEL, level), codec);
}
// we avoid including aomcx.h to get aom_tune_metric (=moving target) directly
const int tuning =
(params.tuning == ParamsAV1::Tuning::kPsnr) ? AOM_TUNE_PSNR
: (params.tuning == ParamsAV1::Tuning::kSsim) ? AOM_TUNE_SSIM
: (params.tuning == ParamsAV1::Tuning::kVmaf) ? AOM_TUNE_VMAF_MAX_GAIN
: AOM_TUNE_BUTTERAUGLI;
CHECK_AOM_STATUS(aom_codec_control(codec, AOME_SET_TUNING, tuning), codec);
// AV1's speed param goes opposite to WP2's
const int speed = std::max(8 - (int)params.effort * 8 / 9, 0);
CHECK_AOM_STATUS(aom_codec_control(codec, AOME_SET_CPUUSED, speed), codec);
if (params.filter_strength == 0) {
CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_CDEF, 0), codec);
CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_RESTORATION, 0),
codec);
}
for (const auto& kv : params.extra_options) {
CHECK_AOM_STATUS(aom_codec_control(codec, kv.first, kv.second), codec);
}
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
constexpr int kFlags = AOM_EFLAG_FORCE_KF;
WP2Status FirstPassAV1(const ArgbBuffer& ref, const ParamsAV1& params,
double* const timing, aom_fixed_buf_t* const stats) {
aom_image_t raw;
WP2_CHECK_STATUS(CreateAV1Img(ref, &raw, params.use_yuv444));
Cleaner<aom_image_t> img_cleaner(&raw, aom_img_free);
aom_codec_ctx_t codec;
WP2_CHECK_STATUS(CreateCodec((int)ref.width(), (int)ref.height(),
raw.monochrome, params, AOM_RC_FIRST_PASS,
/*stats=*/{NULL, 0}, &codec));
Cleaner<aom_codec_ctx_t, aom_codec_err_t> codec_cleaner(&codec,
aom_codec_destroy);
WP2_CHECK_STATUS(ConfigureCodec(params, &codec));
const double start_time = GetStopwatchTime();
bool got_stats;
// Calculate frame statistics.
WP2_CHECK_STATUS(wp2_get_frame_stats(&codec, &raw, /*pts=*/0, /*duration=*/1,
kFlags, stats, &got_stats));
// Flush encoder.
do {
WP2_CHECK_STATUS(wp2_get_frame_stats(
&codec, NULL, /*pts=*/0, /*duration=*/1, kFlags, stats, &got_stats));
} while (got_stats);
if (timing != nullptr) *timing = GetStopwatchTime() - start_time;
CHECK_AOM_STATUS(aom_codec_destroy(&codec), nullptr);
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
// Encodes 'image' into 'bitstream'.
WP2Status EncodeAV1WithLibaom(const ArgbBuffer& image, const ParamsAV1& params,
aom_fixed_buf_t stats,
std::string* const bitstream,
double* const timing) {
WP2_CHECK_OK(bitstream != nullptr, WP2_STATUS_NULL_PARAMETER);
bitstream->clear();
aom_image_t raw;
WP2_CHECK_STATUS(CreateAV1Img(image, &raw, params.use_yuv444));
Cleaner<aom_image_t> img_cleaner(&raw, aom_img_free);
aom_codec_ctx_t codec;
// TODO(yguyon): Check possible leaks of itf/cfg/codec
WP2_CHECK_STATUS(CreateCodec(
(int)image.width(), (int)image.height(), raw.monochrome, params,
(stats.buf != NULL) ? AOM_RC_LAST_PASS : AOM_RC_ONE_PASS, stats, &codec));
Cleaner<aom_codec_ctx_t, aom_codec_err_t> codec_cleaner(&codec,
aom_codec_destroy);
WP2_CHECK_STATUS(ConfigureCodec(params, &codec));
const double start_time = GetStopwatchTime();
encode_frame(&codec, &raw, 0, kFlags, bitstream, false);
while (encode_frame(&codec, NULL, -1, 0, bitstream)) { /* flush encoder */
}
if (timing != nullptr) *timing = GetStopwatchTime() - start_time;
CHECK_AOM_STATUS(aom_codec_destroy(&codec), nullptr);
return WP2_STATUS_OK;
}
// Decodes 'bitstream' into 'decoded'. 'ref' is only needed when debug
// information (blocks, transforms, quant) is requested, in order to get the
// width/height.
WP2Status DecodeAV1WithLibaom(const ArgbBuffer& ref, const ParamsAV1& params,
const std::string bitstream,
ArgbBuffer* const decoded, double* const timing,
std::vector<Rectangle>* const blocks,
std::vector<Rectangle>* const transforms,
QuantAV1* const quant) {
(void)blocks, (void)transforms, (void)quant;
WP2_CHECK_OK(decoded != nullptr, WP2_STATUS_NULL_PARAMETER);
const aom_codec_iface_t* const itf = aom_codec_av1_dx();
if (itf == NULL) {
fprintf(stderr, "aom_codec_av1_dx() failed!\n");
return WP2_STATUS_INVALID_PARAMETER;
}
const aom_codec_dec_cfg cfg = {params.threads, ref.width(), ref.height(), 1};
aom_codec_ctx_t codec;
CHECK_AOM_STATUS(aom_codec_dec_init(&codec, itf, &cfg, 0), &codec);
CHECK_AOM_STATUS(aom_codec_control(&codec, AV1_SET_SKIP_LOOP_FILTER), &codec);
#if defined(CONFIG_INSPECTION) && CONFIG_INSPECTION
insp_frame_data frame_data;
ifd_init(&frame_data, ref.width(), ref.height());
WP2_CHECK_OK(frame_data.mi_grid != NULL, WP2_STATUS_OUT_OF_MEMORY);
Cleaner<insp_frame_data> frame_data_cleaner(&frame_data, ifd_clear);
struct aom_inspect_init inspect = {inspect_cb, &frame_data};
const bool use_inspect =
(params.draw_mi_data != nullptr || params.draw_blocks ||
params.draw_transforms || blocks != nullptr || transforms != nullptr ||
quant != nullptr) &&
(aom_codec_control(&codec, AV1_SET_INSPECTION_CALLBACK, &inspect) ==
AOM_CODEC_OK);
#endif // CONFIG_INSPECTION
Av1DecodeReturn user_priv = {nullptr, 0, 0}; // Necessary to enable inspect
const double start_time = GetStopwatchTime();
CHECK_AOM_STATUS(
aom_codec_decode(&codec, (const uint8_t*)bitstream.data(),
bitstream.size(), &user_priv), &codec);
if (timing != nullptr) *timing = GetStopwatchTime() - start_time;
aom_image_t* img = NULL;
aom_codec_iter_t iter = NULL;
bool ok = false;
while ((img = aom_codec_get_frame(&codec, &iter)) != NULL) {
YUVPlane yuv;
WP2_CHECK_STATUS(yuv.Resize(img->d_w, img->d_h, /*pad=*/1,
/*has_alpha=*/false, !params.use_yuv444));
yuv.Y.From(img->planes[AOM_PLANE_Y], img->stride[AOM_PLANE_Y], -16);
yuv.U.From(img->planes[AOM_PLANE_U], img->stride[AOM_PLANE_U], -128);
yuv.V.From(img->planes[AOM_PLANE_V], img->stride[AOM_PLANE_V], -128);
WP2_CHECK_STATUS(ToRGBA(
yuv, params.use_yuv444 ? nullptr : &SamplingTaps::kUpSmooth, decoded));
ok = true;
break;
}
if (!ok) {
fprintf(stderr, "Round-trip error!\n");
return WP2_STATUS_OUT_OF_MEMORY;
}
#if defined(WP2_BITTRACE) && defined(CONFIG_ACCOUNTING) && CONFIG_ACCOUNTING
if (params.bittrace > 0) {
Accounting* acc = NULL;
if (aom_codec_control(&codec, AV1_GET_ACCOUNTING, &acc) == AOM_CODEC_OK) {
std::map<std::string, BitCounts::mapped_type> accum;
const uint32_t num_syms = acc->syms.num_syms;
for (uint32_t i = 0; i < num_syms; ++i) {
const AccountingSymbol& sym = acc->syms.syms[i];
const double bits = sym.bits / 8.;
const char* str = acc->syms.dictionary.strs[sym.id];
const uint32_t num_samples = sym.samples;
if (params.compact_trace) {
if (kCategories[str] == nullptr) {
fprintf(stderr, "Unmapped category! Missing line:\n");
fprintf(stderr, " {\"%s\", kCtgXXX},\n", str);
} else {
str = kCategories[str];
}
}
accum[str].bits += bits;
accum[str].num_occurrences += num_samples;
}
std::vector<TraceType> values;
values.reserve(accum.size());
for (const auto& p : accum) values.push_back(p);
const bool use_bytes = (params.bittrace == 2);
PrintBitTraceCluster(values, true, use_bytes, /*show_histograms=*/false);
} else {
fprintf(stderr, "Error! AV1 accounting (bit traces) not compiled in\n");
}
}
#endif // WP2_BITTRACE && CONFIG_ACCOUNTING
#if defined(CONFIG_INSPECTION) && CONFIG_INSPECTION
if (use_inspect) {
if (params.draw_mi_data != nullptr) {
if (std::strcmp(params.draw_mi_data, "quanty") == 0) {
DrawQuant(frame_data, /*channel=*/0, /*is_dc=*/true, decoded);
} else if (std::strcmp(params.draw_mi_data, "quantu") == 0) {
DrawQuant(frame_data, /*channel=*/1, /*is_dc=*/true, decoded);
} else if (std::strcmp(params.draw_mi_data, "quantv") == 0) {
DrawQuant(frame_data, /*channel=*/2, /*is_dc=*/true, decoded);
} else {
DrawMiData(frame_data, params.draw_mi_data, params.draw_mi_number,
decoded);
}
}
// Print transforms before blocks because transforms are smaller.
if (params.draw_transforms || transforms != nullptr) {
std::vector<Rectangle> t =
ExtractBoxes(frame_data, ref.width(), ref.height(),
/*extract_transform_boxes=*/true);
if (params.draw_transforms) {
DrawBoxes(t, {0xff, 0x08, 0x80, 0xf0}, decoded);
}
if (transforms != nullptr) std::swap(*transforms, t);
}
if (params.draw_blocks || blocks != nullptr) {
std::vector<Rectangle> b =
ExtractBoxes(frame_data, ref.width(), ref.height(),
/*extract_transform_boxes=*/false);
if (params.draw_blocks) DrawBoxes(b, {0xff, 0x08, 0xf0, 0x80}, decoded);
if (blocks != nullptr) std::swap(*blocks, b);
}
if (quant != nullptr) {
for (uint32_t i = 0; i < 8; ++i) {
for (uint32_t j = 0; j < 2; ++j) {
// Raw quant values are multiplied by 4. We shift them back to
// make them comparable with those used by wp2.
quant->y_dequant[i][j] = frame_data.y_dequant[i][j] >> 2;
quant->u_dequant[i][j] = frame_data.u_dequant[i][j] >> 2;
quant->v_dequant[i][j] = frame_data.v_dequant[i][j] >> 2;
}
}
}
}
#endif // CONFIG_INSPECTION
CHECK_AOM_STATUS(aom_codec_destroy(&codec), &codec);
return WP2_STATUS_OK;
}
#if defined(WP2_HAVE_LIBGAV1)
WP2Status ConvertStatus(::libgav1::StatusCode code) {
switch (code) {
case ::libgav1::kStatusOk:
return WP2_STATUS_OK;
case ::libgav1::kStatusUnknownError:
case ::libgav1::kStatusInvalidArgument:
case ::libgav1::kStatusAlready:
case ::libgav1::kStatusNothingToDequeue:
return WP2_STATUS_INVALID_PARAMETER;
case ::libgav1::kStatusOutOfMemory:
return WP2_STATUS_OUT_OF_MEMORY;
case ::libgav1::kStatusResourceExhausted:
return WP2_STATUS_NOT_ENOUGH_DATA;
case ::libgav1::kStatusNotInitialized:
case ::libgav1::kStatusInternalError:
return WP2_STATUS_INVALID_CONFIGURATION;
case ::libgav1::kStatusUnimplemented:
case ::libgav1::kStatusTryAgain:
return WP2_STATUS_UNSUPPORTED_FEATURE;
case ::libgav1::kStatusBitstreamError:
return WP2_STATUS_BITSTREAM_ERROR;
default:
assert(false);
return WP2_STATUS_LAST;
}
}
#endif // defined(WP2_HAVE_LIBGAV1)
// Decodes 'bitstream' into 'decoded'.
WP2Status DecodeAV1WithLibgav1(const ArgbBuffer& ref, const ParamsAV1& params,
const std::string bitstream,
ArgbBuffer* const decoded,
double* const timing) {
#if defined(WP2_HAVE_LIBGAV1)
WP2_CHECK_OK(decoded != nullptr, WP2_STATUS_NULL_PARAMETER);
::libgav1::DecoderSettings settings;
settings.threads = params.threads;
if (params.filter_strength == 0) settings.post_filter_mask = 0x0;
::libgav1::Decoder decoder;
::libgav1::StatusCode status = decoder.Init(&settings);
if (status != ::libgav1::kStatusOk) {
fprintf(stderr, "libgav1::DecoderSettings::Init() failed: %s\n",
::libgav1::GetErrorString(status));
return ConvertStatus(status);
}
const double start_time = GetStopwatchTime();
status = decoder.EnqueueFrame(
reinterpret_cast<const uint8_t*>(bitstream.data()), bitstream.size(),
/*user_private_data=*/0, /*buffer_private_data=*/nullptr);
if (status != ::libgav1::kStatusOk) {
fprintf(stderr, "libgav1::DecoderSettings::EnqueueFrame() failed: %s\n",
::libgav1::GetErrorString(status));
return ConvertStatus(status);
}
const ::libgav1::DecoderBuffer* buffer;
status = decoder.DequeueFrame(&buffer);
if (status != ::libgav1::kStatusOk || buffer == nullptr) {
fprintf(stderr, "libgav1::DecoderSettings::DequeueFrame() failed: %s\n",
::libgav1::GetErrorString(status));
return ConvertStatus(status);
}
if (timing != nullptr) *timing = GetStopwatchTime() - start_time;
WP2_CHECK_OK(buffer->displayed_width[AOM_PLANE_Y] == (int)ref.width() &&
buffer->displayed_height[AOM_PLANE_Y] == (int)ref.height(),
WP2_STATUS_BAD_DIMENSION);
WP2_CHECK_OK(buffer->image_format == kLibgav1ImageFormatYuv420 ||
buffer->image_format == kLibgav1ImageFormatYuv444,
WP2_STATUS_INVALID_COLORSPACE);
const bool is420 = (buffer->image_format == kLibgav1ImageFormatYuv420);
YUVPlane yuv;
WP2_CHECK_STATUS(yuv.Resize(ref.width(), ref.height(), /*pad=*/1,
/*has_alpha=*/false, is420));
yuv.Y.From(buffer->plane[AOM_PLANE_Y], buffer->stride[AOM_PLANE_Y], -16);
yuv.U.From(buffer->plane[AOM_PLANE_U], buffer->stride[AOM_PLANE_U], -128);
yuv.V.From(buffer->plane[AOM_PLANE_V], buffer->stride[AOM_PLANE_V], -128);
WP2_CHECK_STATUS(ToRGBA(
yuv, (is420 && params.use_yuv444) ? nullptr : &SamplingTaps::kUpSmooth,
decoded));
return WP2_STATUS_OK;
#else
(void)ref, (void)params, (void)bitstream, (void)decoded, (void)timing;
return WP2_STATUS_UNSUPPORTED_FEATURE;
#endif // defined(WP2_HAVE_LIBGAV1)
}
// Encodes 'ref' into 'out', decodes it into 'decoded'.
WP2Status CompressAV1(const ArgbBuffer& ref, const ParamsAV1& params,
aom_fixed_buf_t stats, ArgbBuffer* const decoded,
std::string* const out, double timing[],
std::vector<Rectangle>* const blocks,
std::vector<Rectangle>* const transforms,
QuantAV1* const quant) {
if (aom_codec_version() != VERSION_PACKED) {
fprintf(stderr, "Version mismatch! (%x vs %x)\n",
aom_codec_version(), VERSION_PACKED);
fprintf(stderr, "Please check includes and lib locations.\n");
return WP2_STATUS_VERSION_MISMATCH;
}
WP2_CHECK_STATUS(EncodeAV1WithLibaom(
ref, params, stats, out, (timing != nullptr) ? &timing[0] : nullptr));
WP2_CHECK_STATUS(decoded->Resize(ref.width(), ref.height()));
if (params.use_libgav1_decoder) {
WP2_CHECK_STATUS(
DecodeAV1WithLibgav1(ref, params, *out, decoded,
(timing != nullptr) ? &timing[1] : nullptr));
} else {
WP2_CHECK_STATUS(DecodeAV1WithLibaom(
ref, params, *out, decoded, (timing != nullptr) ? &timing[1] : nullptr,
blocks, transforms, quant));
}
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
#endif // defined(WP2_HAVE_AOM)
} // namespace
//------------------------------------------------------------------------------
#if defined(WP2_HAVE_AOM)
WP2Status CompressAV1(const ArgbBuffer& ref, const ParamsAV1& params,
ArgbBuffer* const decoded, std::string* const out,
double timing[2], std::vector<Rectangle>* const blocks,
std::vector<Rectangle>* const transforms,
QuantAV1* const quant) {
if (params.pass < 2) {
return CompressAV1(ref, params, /*stats=*/{NULL, 0}, decoded, out, timing,
blocks, transforms, quant);
} else {
aom_fixed_buf_t stats = {NULL, 0};
double first_pass_timing;
WP2_CHECK_STATUS(FirstPassAV1(ref, params, &first_pass_timing, &stats));
WP2_CHECK_STATUS(CompressAV1(ref, params, stats, decoded, out, timing,
blocks, transforms, quant));
if (timing != nullptr) timing[0] += first_pass_timing;
free(stats.buf);
return WP2_STATUS_OK;
}
}
WP2Status DecodeAV1WithLibaom(const std::string& bitstream,
ArgbBuffer* const decoded) {
return DecodeAV1WithLibaom(ArgbBuffer(), ParamsAV1(), bitstream, decoded,
/*timing=*/nullptr, /*blocks=*/nullptr,
/*transforms=*/nullptr, /*quant=*/nullptr);
}
#else
WP2Status CompressAV1(const ArgbBuffer&, const ParamsAV1&, ArgbBuffer* const,
std::string* const, double[2],
std::vector<Rectangle>* const,
std::vector<Rectangle>* const, QuantAV1* const) {
fprintf(stderr, "CompressAV1: AOM support not enabled at compile time.\n");
return WP2_STATUS_UNSUPPORTED_FEATURE;
}
WP2Status DecodeAV1WithLibaom(const std::string&, ArgbBuffer* const) {
fprintf(stderr,
"DecodeAV1WithLibaom: AOM support not enabled at compile time.\n");
return WP2_STATUS_UNSUPPORTED_FEATURE;
}
#endif // defined(WP2_HAVE_AOM)
//------------------------------------------------------------------------------
// AVIF
#if defined(WP2_HAVE_AVIF)
static float ToAvifQuantizer(float quality) {
// Note AVIF_QUANTIZER_LOSSLESS doesn't actually seem lossless.
if (quality > 95) return AVIF_QUANTIZER_LOSSLESS;
return AVIF_QUANTIZER_WORST_QUALITY +
quality *
(AVIF_QUANTIZER_BEST_QUALITY - AVIF_QUANTIZER_WORST_QUALITY) / 100;
}
WP2Status CompressAVIF(const ArgbBuffer& ref, const ParamsAV1& params,
ArgbBuffer* const decoded, std::string* const out,
double timing[2]) {
WP2_CHECK_OK(decoded != nullptr, WP2_STATUS_NULL_PARAMETER);
WP2_CHECK_OK(out != nullptr, WP2_STATUS_NULL_PARAMETER);
out->clear();
YUVPlane yuv;
WP2_CHECK_STATUS(
ToYCbCr(ref, /*ycbcr_bit_depth=*/{8, /*is_signed=*/false},
params.use_yuv444 ? nullptr : &SamplingTaps::kDownSharp, &yuv));
const bool is_monochrome = yuv.IsMonochrome();
const uint32_t w = ref.width();
const uint32_t h = ref.height();
constexpr int kDepth = 8;
const avifPixelFormat format = is_monochrome ? AVIF_PIXEL_FORMAT_YUV400
: params.use_yuv444 ? AVIF_PIXEL_FORMAT_YUV444
: AVIF_PIXEL_FORMAT_YUV420;
avifImage* const image = avifImageCreate(w, h, kDepth, format);
WP2_CHECK_ALLOC_OK(image != nullptr);
Cleaner<avifImage> image_cleaner(image, avifImageDestroy);
const bool has_alpha = ref.HasTransparency();
if (has_alpha) {
avifImageAllocatePlanes(image, AVIF_PLANES_YUV | AVIF_PLANES_A);
} else {
avifImageAllocatePlanes(image, AVIF_PLANES_YUV);
}
// import into yuv
yuv.Y.To(image->yuvPlanes[AVIF_CHAN_Y], image->yuvRowBytes[AVIF_CHAN_Y]);
if (!is_monochrome) {
yuv.U.To(image->yuvPlanes[AVIF_CHAN_U], image->yuvRowBytes[AVIF_CHAN_U],
128);
yuv.V.To(image->yuvPlanes[AVIF_CHAN_V], image->yuvRowBytes[AVIF_CHAN_V],
128);
}
if (has_alpha) {
WP2_CHECK_STATUS(yuv.A.Resize(w, h));
for (uint32_t y = 0; y < ref.height(); ++y) {
auto* const row_yuv = yuv.A.Row(y);
const uint8_t* const row = ref.GetRow8(y);
for (uint32_t x = 0; x < ref.width(); ++x) {
const uint8_t* argb = &row[x * WP2FormatBpp(ref.format())];
row_yuv[x] = argb[0];
}
}
yuv.A.To(image->alphaPlane, image->alphaRowBytes);
}
// Encode.
avifRWData avif_enc = AVIF_DATA_EMPTY;
Cleaner<avifRWData> avif_enc_cleaner(&avif_enc, avifRWDataFree);
avifEncoder* const encoder = avifEncoderCreate();
WP2_CHECK_ALLOC_OK(encoder != nullptr);
Cleaner<avifEncoder> encoder_cleaner(encoder, avifEncoderDestroy);
encoder->maxThreads = params.threads;
// Scale to go from AVIF_QUANTIZER_BEST_QUALITY to
// AVIF_QUANTIZER_WORST_QUALITY.
encoder->minQuantizer = ToAvifQuantizer(params.quality);
encoder->maxQuantizer = ToAvifQuantizer(params.quality);
encoder->minQuantizerAlpha = ToAvifQuantizer(params.alpha_quality);
encoder->maxQuantizerAlpha = ToAvifQuantizer(params.alpha_quality);
encoder->speed = (int)AVIF_SPEED_FASTEST +
((int)AVIF_SPEED_SLOWEST - (int)AVIF_SPEED_FASTEST) *
(int)params.effort / 9;
double start_time = GetStopwatchTime();
const avifResult encodeResult = avifEncoderWrite(encoder, image, &avif_enc);
if (timing != nullptr) timing[0] = GetStopwatchTime() - start_time;
if (encodeResult == AVIF_RESULT_OK) {
// output contains a valid .avif file's contents
*out = std::string(reinterpret_cast<char*>(avif_enc.data), avif_enc.size);
} else {
fprintf(stderr, "ERROR: Failed to encode: %s\n",
avifResultToString(encodeResult));
}
// Decode back.
avifImage* const avif_dec = avifImageCreateEmpty();
avifDecoder* const decoder = avifDecoderCreate();
start_time = GetStopwatchTime();
const avifResult decodeResult =
avifDecoderReadMemory(decoder, avif_dec, avif_enc.data, avif_enc.size);
if (timing != nullptr) timing[1] = GetStopwatchTime() - start_time;
avifDecoderDestroy(decoder);
if (decodeResult == AVIF_RESULT_OK) {
// re-use 'yuv' plane
yuv.Y.From(avif_dec->yuvPlanes[AVIF_CHAN_Y],
avif_dec->yuvRowBytes[AVIF_CHAN_Y]);
if (avif_dec->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
yuv.U.Fill(0);
yuv.V.Fill(0);
} else {
yuv.U.From(avif_dec->yuvPlanes[AVIF_CHAN_U],
avif_dec->yuvRowBytes[AVIF_CHAN_U], -128);
yuv.V.From(avif_dec->yuvPlanes[AVIF_CHAN_V],
avif_dec->yuvRowBytes[AVIF_CHAN_V], -128);
}
if (has_alpha) {
yuv.A.From(avif_dec->alphaPlane, avif_dec->alphaRowBytes);
}
WP2_CHECK_STATUS(ToRGBA(yuv, &SamplingTaps::kUpSmooth, decoded));
} else {
fprintf(stderr, "ERROR: Failed to decode: %s\n",
avifResultToString(decodeResult));
}
return WP2_STATUS_OK;
}
#else
WP2Status CompressAVIF(const ArgbBuffer& ref, const ParamsAV1& params,
ArgbBuffer* const decoded, std::string* const out,
double timing[2]) {
(void)ref;
(void)params;
(void)decoded;
(void)out;
(void)timing;
fprintf(stderr, "CompressAVIF: AVIF support not enabled at compile time.\n");
return WP2_STATUS_UNSUPPORTED_FEATURE;
}
#endif // WP2_HAVE_AVIF
//------------------------------------------------------------------------------
// Mapping from aom option names to aome_enc_control_id enum values.
static const std::pair<const char*, int> kAomOptions[] = {
{"use-reference", 7},
{"roi-map", 8},
{"activemap", 9},
{"scalemode", 11},
{"spatial-layer-id", 12},
{"cpuused", 13},
{"enableautoaltref", 14},
{"sharpness", 16},
{"static-threshold", 17},
{"get-last-quantizer", 19},
{"get-last-quantizer-64", 20},
{"arnr-maxframes", 21},
{"arnr-strength", 22},
{"tuning", 24},
{"cq-level", 25},
{"max-intra-bitrate-pct", 26},
{"number-spatial-layers", 27},
{"max-inter-bitrate-pct", 28},
{"gf-cbr-boost-pct", 29},
{"lossless", 31},
{"row-mt", 32},
{"tile-columns", 33},
{"tile-rows", 34},
{"enable-tpl-model", 35},
{"enable-keyframe-filtering", 36},
{"frame-parallel-decoding", 37},
{"error-resilient-mode", 38},
{"s-frame-mode", 39},
{"aq-mode", 40},
{"frame-periodic-boost", 41},
{"noise-sensitivity", 42},
{"tune-content", 43},
{"cdf-update-mode", 44},
{"color-primaries", 45},
{"transfer-characteristics", 46},
{"matrix-coefficients", 47},
{"chroma-sample-position", 48},
{"min-gf-interval", 49},
{"max-gf-interval", 50},
{"get-activemap", 51},
{"color-range", 52},
{"render-size", 53},
{"target-seq-level-idx", 54},
{"get-seq-level-idx", 55},
{"superblock-size", 56},
{"enableautobwdref", 57},
{"enable-cdef", 58},
{"enable-restoration", 59},
{"force-video-mode", 60},
{"enable-obmc", 61},
{"disable-trellis-quant", 62},
{"enable-qm", 63},
{"qm-min", 64},
{"qm-max", 65},
{"qm-y", 66},
{"qm-u", 67},
{"qm-v", 68},
{"num-tg", 70},
{"mtu", 71},
{"enable-rect-partitions", 73},
{"enable-ab-partitions", 74},
{"enable-1to4-partitions", 75},
{"min-partition-size", 76},
{"max-partition-size", 77},
{"enable-intra-edge-filter", 78},
{"enable-order-hint", 79},
{"enable-tx64", 80},
{"enable-flip-idtx", 81},
{"enable-rect-tx", 82},
{"enable-dist-wtd-comp", 83},
{"enable-ref-frame-mvs", 84},
{"allow-ref-frame-mvs", 85},
{"enable-dual-filter", 86},
{"enable-chroma-deltaq", 87},
{"enable-masked-comp", 88},
{"enable-onesided-comp", 89},
{"enable-interintra-comp", 90},
{"enable-smooth-interintra", 91},
{"enable-diff-wtd-comp", 92},
{"enable-interinter-wedge", 93},
{"enable-interintra-wedge", 94},
{"enable-global-motion", 95},
{"enable-warped-motion", 96},
{"allow-warped-motion", 97},
{"enable-filter-intra", 98},
{"enable-smooth-intra", 99},
{"enable-paeth-intra", 100},
{"enable-cfl-intra", 101},
{"enable-superres", 102},
{"enable-overlay", 103},
{"enable-palette", 104},
{"enable-intrabc", 105},
{"enable-angle-delta", 106},
{"deltaq-mode", 107},
{"deltalf-mode", 108},
{"single-tile-decoding", 109},
{"enable-motion-vector-unit-test", 110},
{"timing-info-type", 111},
{"film-grain-test-vector", 112},
{"film-grain-table", 113},
{"denoise-noise-level", 114},
{"denoise-block-size", 115},
{"chroma-subsampling-x", 116},
{"chroma-subsampling-y", 117},
{"reduced-tx-type-set", 118},
{"intra-dct-only", 119},
{"inter-dct-only", 120},
{"intra-default-tx-only", 121},
{"quant-b-adapt", 122},
{"gf-max-pyramid-height", 123},
{"max-reference-frames", 124},
{"reduced-reference-set", 125},
{"coeff-cost-upd-freq", 126},
{"mode-cost-upd-freq", 127},
{"mv-cost-upd-freq", 128},
{"tier-mask", 129},
{"min-cr", 130},
{"svc-layer-id", 131},
{"svc-params", 132},
{"svc-ref-frame-config", 133},
{"vmaf-model-path", 134},
{"enable-ext-tile-debug", 135},
{"enable-sb-multipass-unit-test", 136},
{"gf-min-pyramid-height", 137},
{"vbr-corpus-complexity-lap", 138},
{"get-baseline-gf-interval", 139},
{"enable-dnl-denoising", 140},
{"enable-diagonal-intra", 141},
{"dv-cost-upd-freq", 142},
{"partition-info-path", 143},
{"external-partition", 144},
{"enable-directional-intra", 145},
{"enable-tx-size-search", 146},
};
bool ParseLibaomOption(const std::string& str, int* const option_enum,
int* const option_value) {
std::string option_name;
const size_t pos = str.find('=');
if (pos == std::string::npos) { // Assume value is 1.
option_name = str;
*option_value = 1;
} else {
option_name = str.substr(0, pos);
*option_value = std::stoi(str.substr(pos + 1, std::string::npos).c_str());
}
*option_enum = -1;
for (const auto& pair : kAomOptions) {
if (!strcmp(option_name.c_str(), pair.first)) {
*option_enum = pair.second;
break;
}
}
if (*option_enum == -1) {
fprintf(stderr, "Unknown AOM option '%s'. Must be one of:\n",
option_name.c_str());
for (const auto& pair : kAomOptions) {
fprintf(stderr, "%s ", pair.first);
}
fprintf(stderr, "\n");
return false;
}
return true;
}
//------------------------------------------------------------------------------
// IVF
namespace {
static void WriteLittleEndian16(uint16_t v, char* output) {
uint8_t* mem = (uint8_t*)output;
mem[0] = (uint8_t)((v >> 0) & 0xff);
mem[1] = (uint8_t)((v >> 8) & 0xff);
}
static void WriteLittleEndian32(uint32_t v, char* output) {
uint8_t* mem = (uint8_t*)output;
mem[0] = (uint8_t)((v >> 0) & 0xff);
mem[1] = (uint8_t)((v >> 8) & 0xff);
mem[2] = (uint8_t)((v >> 16) & 0xff);
mem[3] = (uint8_t)((v >> 24) & 0xff);
}
// Ivf file header.
// See https://wiki.multimedia.cx/index.php/IVF
std::string IvfFileHeader(uint32_t width, uint32_t height,
uint32_t timebase_denum, uint32_t timebase_num,
uint32_t fourcc, uint32_t num_frames) {
std::string header;
header.resize(32);
header[0] = 'D';
header[1] = 'K';
header[2] = 'I';
header[3] = 'F';
WriteLittleEndian16(0, &header[0] + 4); // version
WriteLittleEndian16(32, &header[0] + 6); // header size
WriteLittleEndian32(fourcc, &header[0] + 8); // codec fourcc
WriteLittleEndian16(width, &header[0] + 12); // width in pixels
WriteLittleEndian16(height, &header[0] + 14); // height in pixels
WriteLittleEndian32(timebase_denum, &header[0] + 16); // timebase denominator
WriteLittleEndian32(timebase_num, &header[0] + 20); // timebase numerator
WriteLittleEndian32(num_frames, &header[0] + 24); // number of frames in file
WriteLittleEndian32(0, &header[0] + 28); // unused
return header;
}
// Ivf frame header.
// See https://wiki.multimedia.cx/index.php/IVF
// 'timestamp' is a 64-bit presentation timestamp in unit timebase, where
// timebase = timebase_num/timebase_denum (in file header).
// 'frame_size' is the frame data size in bytes.
std::string IvfFrameHeader(uint64_t timestamp, uint32_t frame_size) {
std::string header;
header.resize(12);
WriteLittleEndian32(frame_size, &header[0]);
WriteLittleEndian32((uint32_t)(timestamp & 0xFFFFFFFF), &header[0] + 4);
WriteLittleEndian32((uint32_t)(timestamp >> 32), &header[0] + 8);
return header;
}
constexpr int kAv1Fourcc = 0x31305641;
} // namespace
std::string IvfStillImageHeader(uint32_t width, uint32_t height,
uint32_t bitstream_size) {
return IvfFileHeader(width, height, /*timebase_denum=*/1, /*timebase_num=*/0,
kAv1Fourcc, /*num_frames=*/0) +
IvfFrameHeader(/*timestamp=*/0, bitstream_size);
}
//------------------------------------------------------------------------------
std::string WP2::ParamsAV1::ToString() const {
const char* kTuningStr[] = {"PSNR", "SSIM", "VMAF", "BUTTERAUGLI"};
std::string str;
str += WP2SPrint("quality = %f\n", quality);
str += WP2SPrint("alpha_quality = %f\n", alpha_quality);
str += WP2SPrint("size = %d\n", size);
str += WP2SPrint("effort = %d\n", effort);
str += WP2SPrint("threads = %d\n", threads);
str += WP2SPrint("pass = %d\n", pass);
str += WP2SPrint("use_yuv444 = %d\n", use_yuv444);
str += WP2SPrint("filter_strength = %f\n", filter_strength);
str += WP2SPrint("tuning = %d\n", kTuningStr[(int)tuning]);
str += WP2SPrint("bittrace = %d\n", bittrace);
str += WP2SPrint("compact_trace = %d\n", compact_trace);
str += WP2SPrint("draw_blocks = %d\n", draw_blocks);
str += WP2SPrint("draw_transforms = %d\n", draw_transforms);
str += WP2SPrint("draw_mi_data = %s\n", draw_mi_data ? draw_mi_data : "---");
str += WP2SPrint("draw_mi_number = %d\n", draw_mi_number);
str += WP2SPrint("use_libgav1_decoder = %d\n", use_libgav1_decoder);
str += WP2SPrint("disable_transforms = %d\n", disable_transforms);
str += WP2SPrint("extra_options = %d option(s)\n", extra_options.size());
for (const auto& kv : extra_options) {
for (const auto& str_to_enum : kAomOptions) {
if (str_to_enum.second == kv.first) {
str += WP2SPrint(" - %s = %d\n", str_to_enum.first, kv.second);
break;
}
}
}
return str;
}
//------------------------------------------------------------------------------
} // namespace WP2