diff --git a/lib/include/ultrahdr/avifultrahdr.h b/lib/include/ultrahdr/avifultrahdr.h index f5b6f1bd..c8bfebc9 100644 --- a/lib/include/ultrahdr/avifultrahdr.h +++ b/lib/include/ultrahdr/avifultrahdr.h @@ -171,6 +171,8 @@ class AvifUltraHdr : public UltraHdr { DataStruct* alternateIcc); uhdr_codec_t mCodec; + // Preserve the caller's preset; API-0 changes mEncPreset for gain-map generation. + bool mUseRealtimeAomSpeed; }; } // namespace ultrahdr diff --git a/lib/src/avifencoder.h b/lib/src/avifencoder.h new file mode 100644 index 00000000..3c3228be --- /dev/null +++ b/lib/src/avifencoder.h @@ -0,0 +1,45 @@ +#ifndef ULTRAHDR_AVIFENCODER_H +#define ULTRAHDR_AVIFENCODER_H + +#include + +#include "libheif/heif.h" + +namespace ultrahdr { +namespace internal { + +static inline heif_error get_encoder_for_format(heif_context* ctx, + heif_compression_format format, + bool use_realtime_aom_speed, + heif_encoder** encoder) { + if (!use_realtime_aom_speed || format != heif_compression_AV1) { + return heif_context_get_encoder_for_format(ctx, format, encoder); + } + + // The first descriptor is libheif's normal priority-selected encoder for this format. + const heif_encoder_descriptor* descriptor = nullptr; + if (heif_get_encoder_descriptors(format, nullptr, &descriptor, 1) <= 0 || + descriptor == nullptr) { + return heif_context_get_encoder_for_format(ctx, format, encoder); + } + + heif_error error = heif_context_get_encoder(ctx, descriptor, encoder); + if (error.code != heif_error_Ok) return error; + + const char* encoder_id = heif_encoder_descriptor_get_id_name(descriptor); + if (encoder_id == nullptr || std::strcmp(encoder_id, "aom") != 0) return error; + + error = heif_encoder_set_parameter_integer(*encoder, "speed", 8); + if (error.code != heif_error_Ok && + !(error.code == heif_error_Usage_error && + error.subcode == heif_suberror_Unsupported_parameter)) { + return error; + } + + return {heif_error_Ok, heif_suberror_Unspecified, nullptr}; +} + +} // namespace internal +} // namespace ultrahdr + +#endif // ULTRAHDR_AVIFENCODER_H diff --git a/lib/src/avifultrahdr.cpp b/lib/src/avifultrahdr.cpp index 779e95c1..e2df9635 100644 --- a/lib/src/avifultrahdr.cpp +++ b/lib/src/avifultrahdr.cpp @@ -23,6 +23,7 @@ #include #include "ultrahdr/avifultrahdr.h" +#include "avifencoder.h" #include "ultrahdr/gainmapmath.h" #include "ultrahdr/gainmapmetadata.h" @@ -338,7 +339,8 @@ AvifUltraHdr::AvifUltraHdr(void* uhdrGLESCtxt, int mapDimensionScaleFactor, int uhdr_codec_t codec) : UltraHdr(uhdrGLESCtxt, mapDimensionScaleFactor, mapCompressQuality, useMultiChannelGainMap, gamma, preset, minContentBoost, maxContentBoost, targetDispPeakBrightness), - mCodec(codec) {} + mCodec(codec), + mUseRealtimeAomSpeed(preset == UHDR_USAGE_REALTIME) {} /* Encode API-0 */ uhdr_error_info_t AvifUltraHdr::encodeAvifUltraHdr(uhdr_raw_image_t* hdr_intent, uhdr_compressed_image_t* dest, @@ -464,8 +466,9 @@ uhdr_error_info_t AvifUltraHdr::encodeAvifUltraHdr(uhdr_raw_image_t* sdr_intent, return status; } - HEIF_ERR_CHECK(heif_context_get_encoder_for_format( - ctx, mCodec == UHDR_CODEC_AVIF ? heif_compression_AV1 : heif_compression_HEVC, &encoder)); + HEIF_ERR_CHECK(internal::get_encoder_for_format( + ctx, mCodec == UHDR_CODEC_AVIF ? heif_compression_AV1 : heif_compression_HEVC, + mCodec == UHDR_CODEC_AVIF && mUseRealtimeAomSpeed, &encoder)); // set the encoder parameters HEIF_ERR_CHECK(heif_encoder_set_lossy_quality(encoder, quality)); diff --git a/tests/ultrahdr_api_test.cpp b/tests/ultrahdr_api_test.cpp index f8b9d213..6a4f32c7 100644 --- a/tests/ultrahdr_api_test.cpp +++ b/tests/ultrahdr_api_test.cpp @@ -24,6 +24,7 @@ #include "ultrahdr/avifultrahdr.h" #if defined(UHDR_ENABLE_HEIF) #include "libheif/heif.h" +#include "../lib/src/avifencoder.h" #endif namespace ultrahdr { @@ -1210,6 +1211,116 @@ TEST_F(UltraHdrApiTest, HeicCompressedIntentsUnsupported) { // AVIF Tests (API-0, API-1, and Unsupported APIs) // ============================================================================ +TEST(AvifEncoderPolicyTest, RealtimeSetsSpeedOnlyForTheSelectedAomEncoder) { + std::unique_ptr context(heif_context_alloc(), + heif_context_free); + ASSERT_NE(context, nullptr); + + const heif_encoder_descriptor* default_descriptor = nullptr; + if (heif_get_encoder_descriptors(heif_compression_AV1, nullptr, &default_descriptor, 1) <= 0 || + default_descriptor == nullptr) { + GTEST_SKIP() << "AV1 encoder plugin not available"; + } + + heif_encoder* default_encoder_raw = nullptr; + ASSERT_EQ(heif_context_get_encoder_for_format(context.get(), heif_compression_AV1, + &default_encoder_raw) + .code, + heif_error_Ok); + std::unique_ptr default_encoder( + default_encoder_raw, heif_encoder_release); + ASSERT_NE(default_encoder, nullptr); + EXPECT_STREQ(heif_encoder_get_name(default_encoder.get()), + heif_encoder_descriptor_get_name(default_descriptor)); + + const char* encoder_id = heif_encoder_descriptor_get_id_name(default_descriptor); + const bool default_is_aom = encoder_id != nullptr && std::strcmp(encoder_id, "aom") == 0; + int default_speed = -1; + if (default_is_aom) { + ASSERT_EQ(heif_encoder_get_parameter_integer(default_encoder.get(), "speed", &default_speed) + .code, + heif_error_Ok); + } + + for (const bool realtime : {true, false}) { + SCOPED_TRACE(realtime ? "realtime" : "best quality"); + heif_encoder* mode_encoder_raw = nullptr; + ASSERT_EQ(internal::get_encoder_for_format(context.get(), heif_compression_AV1, realtime, + &mode_encoder_raw) + .code, + heif_error_Ok); + std::unique_ptr mode_encoder( + mode_encoder_raw, heif_encoder_release); + ASSERT_NE(mode_encoder, nullptr); + EXPECT_STREQ(heif_encoder_get_name(mode_encoder.get()), + heif_encoder_descriptor_get_name(default_descriptor)); + if (default_is_aom) { + int mode_speed = -1; + ASSERT_EQ(heif_encoder_get_parameter_integer(mode_encoder.get(), "speed", &mode_speed).code, + heif_error_Ok); + EXPECT_EQ(mode_speed, realtime ? 8 : default_speed); + } + } +} + +TEST_F(UltraHdrApiTest, AvifApi0CallerPresetControlsRealtimeAomSpeed) { + // Context allocation initializes libheif plugins before the descriptor query below. + std::unique_ptr context(heif_context_alloc(), + heif_context_free); + ASSERT_NE(context, nullptr); + + const heif_encoder_descriptor* default_descriptor = nullptr; + if (heif_get_encoder_descriptors(heif_compression_AV1, nullptr, &default_descriptor, 1) <= 0 || + default_descriptor == nullptr) { + GTEST_SKIP() << "AV1 encoder plugin not available"; + } + const char* encoder_id = heif_encoder_descriptor_get_id_name(default_descriptor); + if (encoder_id == nullptr || std::strcmp(encoder_id, "aom") != 0) { + GTEST_SKIP() << "libheif's selected AV1 encoder is not AOM"; + } + + auto encode = [this](uhdr_enc_preset_t preset, std::vector& bytes) { + SCOPED_TRACE(preset == UHDR_USAGE_REALTIME ? "realtime" : "best quality"); + std::unique_ptr encoder( + uhdr_create_encoder(), uhdr_release_encoder); + ASSERT_NE(encoder, nullptr); + + uhdr_error_info_t status = uhdr_enc_set_raw_image(encoder.get(), &mHdrRaw, UHDR_HDR_IMG); + ASSERT_EQ(status.error_code, UHDR_CODEC_OK) + << (status.has_detail ? status.detail : "failed to set HDR image"); + status = uhdr_enc_set_output_format(encoder.get(), UHDR_CODEC_AVIF); + ASSERT_EQ(status.error_code, UHDR_CODEC_OK) + << (status.has_detail ? status.detail : "failed to set AVIF output format"); + status = uhdr_enc_set_preset(encoder.get(), preset); + ASSERT_EQ(status.error_code, UHDR_CODEC_OK) + << (status.has_detail ? status.detail : "failed to set encoder preset"); + status = uhdr_enc_set_quality(encoder.get(), 85, UHDR_BASE_IMG); + ASSERT_EQ(status.error_code, UHDR_CODEC_OK) + << (status.has_detail ? status.detail : "failed to set base image quality"); + status = uhdr_enc_set_quality(encoder.get(), 85, UHDR_GAIN_MAP_IMG); + ASSERT_EQ(status.error_code, UHDR_CODEC_OK) + << (status.has_detail ? status.detail : "failed to set gain map quality"); + + status = uhdr_encode(encoder.get()); + ASSERT_EQ(status.error_code, UHDR_CODEC_OK) + << (status.has_detail ? status.detail : "failed to encode AVIF"); + uhdr_compressed_image_t* output = uhdr_get_encoded_stream(encoder.get()); + ASSERT_NE(output, nullptr); + ASSERT_GT(output->data_sz, 0u); + const auto* output_data = static_cast(output->data); + bytes.assign(output_data, output_data + output->data_sz); + }; + + std::vector best_stream; + std::vector realtime_stream; + ASSERT_NO_FATAL_FAILURE(encode(UHDR_USAGE_BEST_QUALITY, best_stream)); + ASSERT_NO_FATAL_FAILURE(encode(UHDR_USAGE_REALTIME, realtime_stream)); + ASSERT_FALSE(best_stream.empty()); + ASSERT_FALSE(realtime_stream.empty()); + EXPECT_FALSE(best_stream == realtime_stream) + << "API-0's internal realtime gain-map preset must not hide the caller's AVIF preset"; +} + TEST_F(UltraHdrApiTest, AvifEncodeApi0AndDecode) { uhdr_codec_private_t* enc = uhdr_create_encoder(); ASSERT_NE(enc, nullptr); diff --git a/ultrahdr_api.h b/ultrahdr_api.h index 4a112359..ea5c8664 100644 --- a/ultrahdr_api.h +++ b/ultrahdr_api.h @@ -481,6 +481,9 @@ UHDR_EXTERN uhdr_error_info_t uhdr_enc_set_target_display_peak_brightness(uhdr_c * \param[in] enc encoder instance. * \param[in] preset encoding preset. #UHDR_USAGE_REALTIME - Tune settings for best performance * #UHDR_USAGE_BEST_QUALITY - Tune settings for best quality + * \note For AVIF, the realtime preset sets AOM speed to 8 when libheif selects its AOM encoder. + * Other selected encoders keep their default speed. If AOM does not support this parameter, + * its default speed is used. * * \return uhdr_error_info_t #UHDR_CODEC_OK if operation succeeds, * #UHDR_CODEC_INVALID_PARAM otherwise.