ref:f0ad7cdedb25c4f112bef971ae53f85ed9c363f8

feat(amf): add amd_max_au_size option (#4926)

Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
SHA: f0ad7cdedb25c4f112bef971ae53f85ed9c363f8
Author: pollopopo <61017252+pollopopo@users.noreply.github.com>
Date: 2026-08-21 19:32
Parents: 3dfbfe0
8 files changed +159 -0
Type
docs/configuration.md +34 −0
@@ -2780,6 +2780,40 @@
</tr>
</table>
### amd_max_au_size
<table>
<tr>
<td>Description</td>
<td colspan="2">
Maximum Access Unit (frame) size for rate control, in bits. This is a last-resort
control for capping oversized encoded frames that would otherwise exceed Sunshine's
four-block FEC capacity and be sent without FEC protection.
@note{This option only applies to H.264 and HEVC when using amdvce [encoder](#encoder).
AV1 does not support this option.}
@note{The FEC ceiling depends on the negotiated packet size and [fec_percentage](#fec_percentage),
not the average bitrate or frame rate. The approximate encoded-payload ceiling is
`4 * floor(25500 / (100 + fec_percentage)) * (packet_size - 16) * 8` bits, before
allowing for the short frame header or codec-header replacements. With 20% FEC and
1024-byte packets, 6400000 bits provides conservative headroom.}
@warning{Setting this unnecessarily low can reduce image quality, especially for
keyframes. Leave it empty unless oversized frames are actually causing FEC to be skipped.}
</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">@code{}
@endcode</td>
</tr>
<tr>
<td>Example</td>
<td colspan="2">@code{}
amd_max_au_size = 6400000
@endcode</td>
</tr>
</table>
### amd_quality
<table>
src/config.cpp +19 −0
@@ -748,5 +748,6 @@
(int) amd::quality_av1_e::balanced, // quality (av1)
0, // preanalysis
1, // vbaq
{}, // max_au_size (disabled by default)
(int) amd::coder_e::_auto, // coder
}, // amd
@@ -1630,6 +1631,13 @@
bool_f(vars, "amd_preanalysis", (bool &) video.amd.amd_preanalysis);
bool_f(vars, "amd_vbaq", (bool &) video.amd.amd_vbaq);
bool_f(vars, "amd_enforce_hrd", (bool &) video.amd.amd_enforce_hrd);
{
auto max_au_size = video.amd.amd_max_au_size;
int_f(vars, "amd_max_au_size", max_au_size);
if (!max_au_size || *max_au_size >= -1) {
video.amd.amd_max_au_size = max_au_size;
}
}
int_f(vars, "vt_coder", video.vt.vt_coder, vt::coder_from_view);
int_f(vars, "vt_software", video.vt.vt_allow_sw, vt::allow_software_from_view);
@@ -1885,6 +1893,17 @@
}
}
}
#ifdef SUNSHINE_TESTS
/**
* @brief Parse and apply serialized configuration text for unit tests.
*
* @param file_content Raw configuration text to parse and apply.
*/
void apply_config_for_test(const std::string_view file_content) {
apply_config(parse_config(file_content));
}
#endif
/**
* @brief Parse serialized text into the corresponding runtime representation.
src/config.h +10 −0
@@ -94,5 +94,6 @@
std::optional<int> amd_quality_av1;
std::optional<int> amd_preanalysis;
std::optional<int> amd_vbaq;
std::optional<int> amd_max_au_size; ///< Maximum AMF H.264/HEVC access unit size in bits; unset uses the encoder default.
int amd_coder;
} amd; ///< AMD AMF encoder options.
@@ -382,6 +383,15 @@
extern nvhttp_t nvhttp;
extern input_t input;
extern sunshine_t sunshine;
#ifdef SUNSHINE_TESTS
/**
* @brief Parse and apply serialized configuration text for unit tests.
*
* @param file_content Raw configuration text to parse and apply.
*/
void apply_config_for_test(std::string_view file_content);
#endif
/**
* @brief Parse serialized text into the corresponding runtime representation.
src/video.cpp +2 −0
@@ -994,6 +994,7 @@
{"usage"s, &config::video.amd.amd_usage_hevc},
{"vbaq"s, &config::video.amd.amd_vbaq},
{"enforce_hrd"s, &config::video.amd.amd_enforce_hrd},
{"max_au_size"s, &config::video.amd.amd_max_au_size},
{"level"s, [](const config_t &cfg) {
auto size = cfg.width * cfg.height;
// For 4K and below, try to use level 5.1 or 5.2 if possible
@@ -1032,6 +1033,7 @@
{"vbaq"s, &config::video.amd.amd_vbaq},
{"coder"s, &config::video.amd.amd_coder},
{"enforce_hrd"s, &config::video.amd.amd_enforce_hrd},
{"max_au_size"s, &config::video.amd.amd_max_au_size},
},
{}, // SDR-specific options
{}, // HDR-specific options
src_assets/common/assets/web/config.html +1 −0
@@ -337,6 +337,7 @@
"amd_usage": "ultralowlatency",
"amd_rc": "vbr_latency",
"amd_enforce_hrd": "disabled",
"amd_max_au_size": "",
"amd_quality": "balanced",
"amd_preanalysis": "disabled",
"amd_vbaq": "enabled",
src_assets/common/assets/web/configs/tabs/encoders/AmdAmfEncoder.vue +8 −0
@@ -56,6 +56,14 @@
v-model="config.amd_enforce_hrd"
default="false"
></Checkbox>
<!-- AMF Max AU Size -->
<div class="mb-3">
<label for="amd_max_au_size" class="form-label">{{ $t('config.amd_max_au_size') }}</label>
<input type="number" class="form-control" id="amd_max_au_size"
placeholder="-1" min="-1" max="2147483647" v-model="config.amd_max_au_size" />
<div class="form-text">{{ $t('config.amd_max_au_size_desc') }}</div>
</div>
</div>
</div>
</div>
src_assets/common/assets/web/public/assets/locale/en.json +2 −0
@@ -123,6 +123,8 @@
"amd_coder_desc": "Allows you to select the entropy encoding to prioritize quality or encoding speed. H.264 only.",
"amd_enforce_hrd": "AMF Hypothetical Reference Decoder (HRD) Enforcement",
"amd_enforce_hrd_desc": "Increases the constraints on rate control to meet HRD model requirements. This greatly reduces bitrate overflows, but may cause encoding artifacts or reduced quality on certain cards.",
"amd_max_au_size": "AMF Max AU Size (bits)",
"amd_max_au_size_desc": "Maximum encoded H.264 or HEVC Access Unit size in bits. Empty or -1 leaves the AMF default. Use only to cap oversized frames that exceed Sunshine's FEC capacity. The limit depends on packet size and FEC percentage, not bitrate or frame rate. See the documentation for sizing.",
"amd_preanalysis": "AMF Preanalysis",
"amd_preanalysis_desc": "This enables rate-control preanalysis, which may increase quality at the expense of increased encoding latency.",
"amd_quality": "AMF Quality",
tests/unit/test_video.cpp +83 −0
@@ -7,6 +7,8 @@
// standard includes
#include <algorithm>
#include <limits>
#include <optional>
#include <tuple>
#include <utility>
#include <vector>
@@ -99,7 +101,88 @@
ASSERT_NE(video::amdvce.h264.common_options.end(), coder_option);
ASSERT_TRUE(std::holds_alternative<int *>(coder_option->value));
EXPECT_EQ(&config::video.amd.amd_coder, std::get<int *>(coder_option->value));
}
/**
* @brief Parameterized coverage for the AMF maximum access-unit-size option mappings.
*/
struct AmfMaxAuSizeOptionsTest: testing::TestWithParam<std::tuple<const video::encoder_t::codec_t *, bool>> {};
TEST_P(AmfMaxAuSizeOptionsTest, UsesConfiguredValueForSupportedCodecsOnly) {
const auto &[codec, supported] = GetParam();
const auto option = std::ranges::find(codec->common_options, "max_au_size"sv, &video::encoder_t::option_t::name);
if (!supported) {
EXPECT_EQ(codec->common_options.end(), option);
return;
}
ASSERT_NE(codec->common_options.end(), option);
ASSERT_TRUE(std::holds_alternative<std::optional<int> *>(option->value));
EXPECT_EQ(&config::video.amd.amd_max_au_size, std::get<std::optional<int> *>(option->value));
}
INSTANTIATE_TEST_SUITE_P(
AmfCodecOptions,
AmfMaxAuSizeOptionsTest,
testing::Values(
std::make_tuple(&video::amdvce.h264, true),
std::make_tuple(&video::amdvce.hevc, true),
std::make_tuple(&video::amdvce.av1, false)
)
);
#endif
using AmfMaxAuSizeConfigParam = std::tuple<std::string_view, std::optional<int>>;
/**
* @brief Parameterized coverage for parsing and validating the AMF maximum access-unit size.
*/
struct AmfMaxAuSizeConfigTest: BaseTest, testing::WithParamInterface<AmfMaxAuSizeConfigParam> {
void SetUp() override {
BaseTest::SetUp();
config::video.amd.amd_max_au_size.reset();
config::stream.file_apps = SUNSHINE_SOURCE_DIR "/tests/unit/test_video.cpp";
}
void TearDown() override {
config::video = original_video;
config::audio = original_audio;
config::stream = original_stream;
config::nvhttp = original_nvhttp;
config::input = original_input;
config::sunshine = original_sunshine;
config::modified_config_settings = original_modified_config_settings;
BaseTest::TearDown();
}
config::video_t original_video {config::video}; ///< Video configuration restored after each parameterized test.
config::audio_t original_audio {config::audio}; ///< Audio configuration restored after each parameterized test.
config::stream_t original_stream {config::stream}; ///< Stream configuration restored after each parameterized test.
config::nvhttp_t original_nvhttp {config::nvhttp}; ///< HTTP configuration restored after each parameterized test.
config::input_t original_input {config::input}; ///< Input configuration restored after each parameterized test.
config::sunshine_t original_sunshine {config::sunshine}; ///< Core configuration restored after each parameterized test.
decltype(config::modified_config_settings) original_modified_config_settings {config::modified_config_settings}; ///< Modified settings restored after each parameterized test.
};
TEST_P(AmfMaxAuSizeConfigTest, AcceptsOnlyFfmpegSupportedRange) {
const auto &[setting, expected] = GetParam();
config::apply_config_for_test(setting);
EXPECT_EQ(expected, config::video.amd.amd_max_au_size);
}
INSTANTIATE_TEST_SUITE_P(
AmfMaxAuSizeValues,
AmfMaxAuSizeConfigTest,
testing::Values(
AmfMaxAuSizeConfigParam {""sv, std::nullopt},
AmfMaxAuSizeConfigParam {"amd_max_au_size = -2\n"sv, std::nullopt},
AmfMaxAuSizeConfigParam {"amd_max_au_size = -1\n"sv, -1},
AmfMaxAuSizeConfigParam {"amd_max_au_size = 0\n"sv, 0},
AmfMaxAuSizeConfigParam {"amd_max_au_size = 800000\n"sv, 800000},
AmfMaxAuSizeConfigParam {"amd_max_au_size = 2147483647\n"sv, std::numeric_limits<int>::max()}
)
);
struct FramerateX100Test: BaseTest, testing::WithParamInterface<std::tuple<std::int32_t, AVRational>> {};