| Description |
- Allows compatible clients to request PyroWave 8-bit or 10-bit 4:2:0 video streams.
+ Allows compatible clients to request PyroWave 8-bit or 10-bit 4:2:0 or 4:4:4 video streams.
Support is advertised based on encoder capabilities. HDR requires a supported capture backend
and HDR enabled on the host display and client.
@warning{PyroWave consumes a significant amount of bandwidth. Not recommended unless on LAN and utilizing a hardwired connection.}
diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp
index ac13e41bc17..edbe00d2719 100644
--- a/src/nvhttp.cpp
+++ b/src/nvhttp.cpp
@@ -1230,12 +1230,7 @@ namespace nvhttp {
if ((video::active_av1_mode == 4 || video::active_av1_mode == 5) && video::last_encoder_probe_supported_dynamic_range_yuv444_for_codec[2]) {
codec_mode_flags |= SCM_AV1_HIGH10_444;
}
- if (video::active_pyrowave_mode >= 2) {
- codec_mode_flags |= video::pyrowave::scm_sdr;
- }
- if (video::active_pyrowave_mode == 3) {
- codec_mode_flags |= video::pyrowave::scm_hdr;
- }
+ codec_mode_flags |= video::active_pyrowave_profiles & video::pyrowave::scm_mask;
return codec_mode_flags;
}
diff --git a/src/rtsp.cpp b/src/rtsp.cpp
index ea8a8510828..457e5906823 100644
--- a/src/rtsp.cpp
+++ b/src/rtsp.cpp
@@ -910,7 +910,7 @@ namespace rtsp_stream {
ss << "a=rtpmap:98 AV1/90000"sv << std::endl;
}
- if (video::active_pyrowave_mode >= 2) {
+ if (video::active_pyrowave_profiles != 0) {
ss << "a=rtpmap:99 PYROWAVE/90000"sv << std::endl;
ss << "a=x-ss-video[0].pyrowaveFrameFormat:PYRW/1"sv << std::endl;
}
@@ -1231,7 +1231,7 @@ namespace rtsp_stream {
config.monitor.bitrate = (int) configuredBitrateKbps;
}
- if (config.monitor.videoFormat < 0 || config.monitor.videoFormat > video::pyrowave::video_format || (config.monitor.videoFormat == video::pyrowave::video_format && !video::pyrowave::supported(config.monitor, video::active_pyrowave_mode))) {
+ if (config.monitor.videoFormat < 0 || config.monitor.videoFormat > video::pyrowave::video_format || (config.monitor.videoFormat == video::pyrowave::video_format && !video::pyrowave::supported(config.monitor, video::active_pyrowave_profiles))) {
BOOST_LOG(warning) << "Client requested an unavailable video format or PyroWave profile";
respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
return;
diff --git a/src/video.cpp b/src/video.cpp
index be5a5841f52..ac9517ea5fe 100644
--- a/src/video.cpp
+++ b/src/video.cpp
@@ -638,14 +638,55 @@ namespace video {
bool valid_config(const config_t &config) {
return config.videoFormat == video_format &&
- config.width > 0 && config.width <= 16384 && config.width % 2 == 0 &&
- config.height > 0 && config.height <= 16384 && config.height % 2 == 0 &&
+ config.width > 0 && config.width <= 16384 &&
+ config.height > 0 && config.height <= 16384 &&
(config.dynamicRange == 0 || config.dynamicRange == 1) &&
- config.chromaSamplingType == 0 && frame_budget(config) != 0;
+ (config.chromaSamplingType == 1 || (config.chromaSamplingType == 0 && config.width % 2 == 0 && config.height % 2 == 0)) &&
+ frame_budget(config) != 0;
}
- bool supported(const config_t &config, int mode) {
- return valid_config(config) && (mode == 3 || (mode == 2 && config.dynamicRange == 0));
+ /**
+ * @brief Identify the wire capability required by a validated PyroWave configuration.
+ * @param config Validated stream configuration.
+ * @return ServerCodecModeSupport bit for this profile.
+ */
+ uint32_t profile_flag(const config_t &config) {
+ return config.chromaSamplingType ? (config.dynamicRange ? scm_hdr_444 : scm_sdr_444) :
+ (config.dynamicRange ? scm_hdr : scm_sdr);
+ }
+
+ bool supported(const config_t &config, uint32_t profiles) {
+ return valid_config(config) && (profiles & profile_flag(config)) != 0;
+ }
+
+ uint32_t probe_profiles(const std::function &validate) {
+ config_t probe {1920, 1080, 60, 6000, 200000, 1, 0, 3, video_format, 0, 0, 0};
+ uint32_t profiles = 0;
+ for (int chroma : {0, 1}) {
+ for (int hdr : {0, 1}) {
+ probe.chromaSamplingType = chroma;
+ probe.dynamicRange = hdr;
+ if (validate(probe)) {
+ profiles |= profile_flag(probe);
+ BOOST_LOG(info) << "Found PyroWave encoder (" << (hdr ? "HDR" : "SDR") << ", " << (chroma ? "4:4:4" : "4:2:0") << ')';
+ }
+ }
+ }
+ return profiles;
+ }
+
+ AVPixelFormat capture_format(const encoder_platform_formats_t &formats, const config_t &config) {
+ // The shared D3D11 converter supports packed 4:4:4 independently of the hardware video encoder.
+ if (formats.dev_type == platf::mem_type_e::dxgi) {
+ return config.chromaSamplingType ? (config.dynamicRange ? AV_PIX_FMT_XV30 : AV_PIX_FMT_VUYX) :
+ (config.dynamicRange ? AV_PIX_FMT_P010 : AV_PIX_FMT_NV12);
+ }
+ const auto avcodec_formats = dynamic_cast(&formats);
+ if (!avcodec_formats) {
+ return AV_PIX_FMT_NONE;
+ }
+ return config.chromaSamplingType ? (config.dynamicRange ? avcodec_formats->avcodec_pix_fmt_yuv444_10bit : avcodec_formats->avcodec_pix_fmt_yuv444_8bit) :
+ (config.dynamicRange ? avcodec_formats->avcodec_pix_fmt_10bit : avcodec_formats->avcodec_pix_fmt_8bit);
}
static_assert(PYROWAVE_API_VERSION_MAJOR == 1, "Sunshine requires PyroWave C API 1.x");
@@ -683,7 +724,7 @@ namespace video {
}
/**
- * @brief Initialize the codec and planar frame for the negotiated color depth.
+ * @brief Initialize the codec and planar frame for the negotiated color depth and chroma sampling.
*
* @param config Negotiated stream configuration.
* @return True when the Vulkan device and encoder are usable.
@@ -699,7 +740,7 @@ namespace video {
BOOST_LOG(error) << "Could not create a PyroWave Vulkan device";
return false;
}
- const pyrowave_encoder_create_info info {device, config.width, config.height, PYROWAVE_CHROMA_SUBSAMPLING_420};
+ const pyrowave_encoder_create_info info {device, config.width, config.height, config.chromaSamplingType ? PYROWAVE_CHROMA_SUBSAMPLING_444 : PYROWAVE_CHROMA_SUBSAMPLING_420};
if (api.create_encoder(&info, &encoder) != PYROWAVE_SUCCESS) {
BOOST_LOG(error) << "Could not create a PyroWave encoder";
return false;
@@ -710,7 +751,8 @@ namespace video {
if (!planar || !readback) {
return false;
}
- planar->format = config.dynamicRange ? AV_PIX_FMT_YUV420P10 : AV_PIX_FMT_YUV420P;
+ planar->format = config.chromaSamplingType ? (config.dynamicRange ? AV_PIX_FMT_YUV444P10 : AV_PIX_FMT_YUV444P) :
+ (config.dynamicRange ? AV_PIX_FMT_YUV420P10 : AV_PIX_FMT_YUV420P);
planar->width = config.width;
planar->height = config.height;
return av_frame_copy_props(planar.get(), converter->frame) >= 0 && av_frame_get_buffer(planar.get(), 0) >= 0;
@@ -739,13 +781,14 @@ namespace video {
// Only change plane layout/bit storage here. The capture converter has
// already applied the negotiated matrix, range, and HDR transfer function.
- if (source->format != AV_PIX_FMT_NV12 && source->format != AV_PIX_FMT_P010 && source->format != AV_PIX_FMT_YUV420P && source->format != AV_PIX_FMT_YUV420P10) {
+ if (source->format != AV_PIX_FMT_NV12 && source->format != AV_PIX_FMT_P010 && source->format != AV_PIX_FMT_YUV420P && source->format != AV_PIX_FMT_YUV420P10 && source->format != AV_PIX_FMT_YUV444P && source->format != AV_PIX_FMT_YUV444P10 && source->format != AV_PIX_FMT_YUV444P16 && source->format != AV_PIX_FMT_VUYX && source->format != AV_PIX_FMT_XV30) {
BOOST_LOG(error) << "Unsupported PyroWave input pixel format: " << source->format;
return -1;
}
- const bool input_10bit = source->format == AV_PIX_FMT_P010 || source->format == AV_PIX_FMT_YUV420P10;
- if (input_10bit != (planar->format == AV_PIX_FMT_YUV420P10)) {
- BOOST_LOG(error) << "PyroWave input color depth does not match the negotiated profile";
+ const auto input_desc = av_pix_fmt_desc_get(AVPixelFormat(source->format));
+ const auto output_desc = av_pix_fmt_desc_get(AVPixelFormat(planar->format));
+ if ((input_desc->comp[0].depth > 8) != (output_desc->comp[0].depth > 8) || input_desc->log2_chroma_w != output_desc->log2_chroma_w || input_desc->log2_chroma_h != output_desc->log2_chroma_h) {
+ BOOST_LOG(error) << "PyroWave input color depth or chroma sampling does not match the negotiated profile";
return -1;
}
auto context = sws_getCachedContext(scaler.release(), source->width, source->height, AVPixelFormat(source->format), planar->width, planar->height, AVPixelFormat(planar->format), SWS_POINT, nullptr, nullptr, nullptr);
@@ -788,11 +831,13 @@ namespace video {
pyrowave_cpu_buffer input {};
input.width = planar->width;
input.height = planar->height;
- input.format = planar->format == AV_PIX_FMT_YUV420P10 ? PYROWAVE_CPU_BUFFER_FORMAT_YUV420P10 : PYROWAVE_CPU_BUFFER_FORMAT_YUV420P;
+ const auto desc = av_pix_fmt_desc_get(AVPixelFormat(planar->format));
+ input.format = desc->log2_chroma_w == 0 ? (desc->comp[0].depth == 10 ? PYROWAVE_CPU_BUFFER_FORMAT_YUV444P10 : PYROWAVE_CPU_BUFFER_FORMAT_YUV444P) :
+ (desc->comp[0].depth == 10 ? PYROWAVE_CPU_BUFFER_FORMAT_YUV420P10 : PYROWAVE_CPU_BUFFER_FORMAT_YUV420P);
for (int plane = 0; plane < 3; ++plane) {
input.data[plane] = planar->data[plane];
input.row_stride_in_bytes[plane] = planar->linesize[plane];
- input.plane_size_in_bytes[plane] = input.row_stride_in_bytes[plane] * (plane ? input.height / 2 : input.height);
+ input.plane_size_in_bytes[plane] = input.row_stride_in_bytes[plane] * (input.height >> (plane ? desc->log2_chroma_h : 0));
}
size_t count = 0;
if (api.encode_cpu(encoder, &input, &rate_control) != PYROWAVE_SUCCESS || api.num_packets(encoder, packet_boundary, &count) != PYROWAVE_SUCCESS || count == 0 || count > std::numeric_limits::max() || count > max_frame_size / packet_boundary) {
@@ -831,7 +876,7 @@ namespace video {
pyrowave_rate_control rate_control; ///< Per-frame coefficient byte budget.
avcodec_frame_t readback; ///< Downloaded hardware frame, when needed.
avcodec_frame_t planar; ///< Planar 8-bit or 10-bit YCbCr input.
- sws_t scaler; ///< Lossless plane-layout conversion for NV12/P010 inputs.
+ sws_t scaler; ///< Plane-layout and bit-storage conversion preserving negotiated chroma and color depth.
std::vector packets; ///< Packet offsets returned by the C API.
std::vector bitstream; ///< Reusable encoded coefficient storage.
bool ready = false; ///< Whether conversion has produced a usable frame.
@@ -1758,7 +1803,7 @@ namespace video {
static encoder_t *chosen_encoder;
int active_hevc_mode; ///< HEVC mode selected by the most recent encoder probe.
int active_av1_mode; ///< AV1 mode selected by the most recent encoder probe.
- int active_pyrowave_mode; ///< PyroWave profiles validated on the selected capture backend.
+ uint32_t active_pyrowave_profiles; ///< PyroWave profile bits validated on the selected capture backend.
bool last_encoder_probe_supported_ref_frames_invalidation = false; ///< Whether the last probe found reference-frame invalidation support.
std::array last_encoder_probe_supported_yuv444_for_codec = {}; ///< YUV444 support discovered for each probed codec.
std::array last_encoder_probe_supported_dynamic_range_for_codec = {}; ///< 10-bit 4:2:0 support discovered for each probed codec.
@@ -2740,9 +2785,10 @@ namespace video {
* @param width Captured image width.
* @param height Captured image height.
* @param device Platform color-conversion device.
+ * @param api PyroWave C API entry points.
* @return PyroWave session, or null if conversion or encoding is unavailable.
*/
- std::unique_ptr make_pyrowave_encode_session(const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr device) {
+ std::unique_ptr make_pyrowave_encode_session(const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr device, const pyrowave::api_t &api = {}) {
if (!pyrowave::valid_config(config) || !device || !encoder.platform_formats || width <= 0 || height <= 0) {
return nullptr;
}
@@ -2762,8 +2808,8 @@ namespace video {
AV_PIX_FMT_D3D11,
AV_PIX_FMT_NV12,
AV_PIX_FMT_P010,
- AV_PIX_FMT_NONE,
- AV_PIX_FMT_NONE,
+ AV_PIX_FMT_VUYX,
+ AV_PIX_FMT_XV30,
dxgi_init_avcodec_hardware_input_buffer
};
if (encoder.platform_formats->dev_type == platf::mem_type_e::dxgi) {
@@ -2773,7 +2819,10 @@ namespace video {
if (!formats) {
return nullptr;
}
- const auto sw_format = config.dynamicRange ? formats->avcodec_pix_fmt_10bit : formats->avcodec_pix_fmt_8bit;
+ const auto sw_format = pyrowave::capture_format(*encoder.platform_formats, config);
+ if (sw_format == AV_PIX_FMT_NONE) {
+ return nullptr;
+ }
avcodec_frame_t frame {av_frame_alloc()};
if (!frame) {
return nullptr;
@@ -2828,7 +2877,7 @@ namespace video {
return nullptr;
}
device->apply_colorspace();
- return pyrowave::make_session(config, std::move(device), std::move(frames));
+ return pyrowave::make_session(config, std::move(device), api, std::move(frames));
}
/**
@@ -3051,7 +3100,12 @@ namespace video {
auto colorspace = colorspace_from_client_config(config, disp.is_hdr());
platf::pix_fmt_e pix_fmt;
- if (config.chromaSamplingType == 1) {
+ if (config.videoFormat == pyrowave::video_format) {
+ pix_fmt = map_pix_fmt(pyrowave::capture_format(*encoder.platform_formats, config));
+ if (pix_fmt == platf::pix_fmt_e::unknown) {
+ return {};
+ }
+ } else if (config.chromaSamplingType == 1) {
// YUV 4:4:4
if (!(encoder.flags & YUV444_SUPPORT)) {
// Encoder can't support YUV 4:4:4 regardless of hardware capabilities
@@ -3761,29 +3815,23 @@ namespace video {
}
/**
- * @brief Probe PyroWave SDR and HDR using the selected capture and color-conversion backend.
+ * @brief Probe each PyroWave color depth and chroma profile using the selected capture backend.
* @param encoder Encoder descriptor selecting the capture backend.
*/
void probe_pyrowave(const encoder_t &encoder) {
- active_pyrowave_mode = 0;
+ active_pyrowave_profiles = 0;
if (!config::video.pyrowave) {
return;
}
- config_t probe {1920, 1080, 60, 6000, 200000, 1, 0, 3, pyrowave::video_format, 0, 0, 0};
const auto output_name = display_device::map_output_name(config::video.output_name);
std::shared_ptr display;
- reset_display(display, encoder.platform_formats->dev_type, output_name, probe);
- if (!display || validate_config(display, encoder, probe) < 0) {
+ active_pyrowave_profiles = pyrowave::probe_profiles([&](const config_t &probe) {
+ reset_display(display, encoder.platform_formats->dev_type, output_name, probe);
+ return display && validate_config(display, encoder, probe) >= 0;
+ });
+ if (!active_pyrowave_profiles) {
BOOST_LOG(warning) << "PyroWave is unavailable on the selected capture backend";
- return;
- }
- active_pyrowave_mode = 2;
- probe.dynamicRange = 1;
- reset_display(display, encoder.platform_formats->dev_type, output_name, probe);
- if (display && validate_config(display, encoder, probe) >= 0) {
- active_pyrowave_mode = 3;
}
- BOOST_LOG(info) << "Found PyroWave encoder (" << (active_pyrowave_mode == 3 ? "SDR and HDR" : "SDR") << ')';
}
int probe_encoders() {
@@ -3805,7 +3853,7 @@ namespace video {
chosen_encoder = nullptr;
active_hevc_mode = config::video.hevc_mode;
active_av1_mode = config::video.av1_mode;
- active_pyrowave_mode = 0;
+ active_pyrowave_profiles = 0;
reset_encoder_probe_results();
auto adjust_encoder_constraints_hevc = [&](encoder_t *encoder) {
diff --git a/src/video.h b/src/video.h
index a7bdc7a129b..97c95f17b96 100644
--- a/src/video.h
+++ b/src/video.h
@@ -743,7 +743,10 @@ namespace video {
namespace pyrowave {
inline constexpr int video_format = 3; ///< Experimental GameStream bitStreamFormat value.
inline constexpr uint32_t scm_sdr = 0x00800000; ///< ServerCodecModeSupport bit for 8-bit 4:2:0.
+ inline constexpr uint32_t scm_sdr_444 = 0x01000000; ///< ServerCodecModeSupport bit for 8-bit 4:4:4.
inline constexpr uint32_t scm_hdr = 0x02000000; ///< ServerCodecModeSupport bit for 10-bit 4:2:0.
+ inline constexpr uint32_t scm_hdr_444 = 0x04000000; ///< ServerCodecModeSupport bit for 10-bit 4:4:4.
+ inline constexpr uint32_t scm_mask = scm_sdr | scm_sdr_444 | scm_hdr | scm_hdr_444; ///< All PyroWave profile bits.
inline constexpr size_t packet_boundary = 1400; ///< Maximum native codec packet length inside a frame.
inline constexpr size_t max_frame_size = 16 * 1024 * 1024; ///< Bound on codec output allocation.
@@ -751,7 +754,7 @@ namespace video {
* @brief Validate dimensions, color format, framerate, and bitrate for PyroWave.
*
* @param config Negotiated stream configuration.
- * @return True for supported 8-bit or 10-bit 4:2:0 configurations.
+ * @return True for supported 8-bit or 10-bit 4:2:0 or 4:4:4 configurations.
*/
bool valid_config(const config_t &config);
@@ -767,10 +770,27 @@ namespace video {
* @brief Check a requested profile against the last successful encoder probe.
*
* @param config Negotiated stream configuration.
- * @param mode Probed mode: 0 unavailable, 2 SDR, 3 SDR and 10-bit HDR.
+ * @param profiles Successfully probed ServerCodecModeSupport profile bits.
* @return Whether the requested profile may be accepted.
*/
- bool supported(const config_t &config, int mode);
+ bool supported(const config_t &config, uint32_t profiles);
+
+ /**
+ * @brief Probe each color depth and chroma profile independently.
+ *
+ * @param validate Callback that tests capture, conversion, and encoding for a profile.
+ * @return ServerCodecModeSupport bits for successful profiles.
+ */
+ uint32_t probe_profiles(const std::function &validate);
+
+ /**
+ * @brief Select the capture converter's pixel layout for a PyroWave profile.
+ *
+ * @param formats Capture backend's pixel formats.
+ * @param config Negotiated stream configuration.
+ * @return Pixel layout, or AV_PIX_FMT_NONE when unavailable.
+ */
+ AVPixelFormat capture_format(const encoder_platform_formats_t &formats, const config_t &config);
/**
* @brief PyroWave session interface used by the common capture pipeline.
@@ -855,7 +875,7 @@ namespace video {
extern int active_hevc_mode;
extern int active_av1_mode;
- extern int active_pyrowave_mode; ///< Probed PyroWave profiles: 0 unavailable, 2 SDR, 3 SDR and HDR.
+ extern uint32_t active_pyrowave_profiles; ///< Independently probed PyroWave ServerCodecModeSupport profile bits.
extern bool last_encoder_probe_supported_ref_frames_invalidation;
extern std::array last_encoder_probe_supported_yuv444_for_codec; // 0 - H.264, 1 - HEVC, 2 - AV1
extern std::array last_encoder_probe_supported_dynamic_range_for_codec; // 0 - H.264, 1 - HEVC, 2 - AV1
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json
index 35881fba73a..a5250a3e933 100644
--- a/src_assets/common/assets/web/public/assets/locale/en.json
+++ b/src_assets/common/assets/web/public/assets/locale/en.json
@@ -396,7 +396,7 @@
"port_warning": "Exposing the Web UI to the internet is a security risk! Proceed at your own risk!",
"port_web_ui": "Web UI",
"pyrowave": "PyroWave Support",
- "pyrowave_desc": "Allows compatible clients to request PyroWave 8-bit or 10-bit (HDR) video streams. Sunshine advertises support based on encoder capabilities.",
+ "pyrowave_desc": "Allows compatible clients to request PyroWave 8-bit or 10-bit (HDR) video streams with 4:2:0 or 4:4:4 chroma sampling. Sunshine advertises support based on encoder capabilities.",
"qp": "Quantization Parameter",
"qp_desc": "Some devices may not support Constant Bit Rate. For those devices, QP is used instead. Higher value means more compression, but less quality.",
"qsv_coder": "QuickSync Coder (H264)",
diff --git a/tests/unit/test_rtsp.cpp b/tests/unit/test_rtsp.cpp
index dad09e81cd7..745805a1a5b 100644
--- a/tests/unit/test_rtsp.cpp
+++ b/tests/unit/test_rtsp.cpp
@@ -24,6 +24,7 @@ extern "C" {
// local includes
#include
+#include
#include
#include
@@ -140,24 +141,33 @@ namespace {
} // namespace
TEST(RtspPyrowaveTest, AdvertisesOnlyProbedProfiles) {
- const auto previous = video::active_pyrowave_mode;
+ const auto previous = video::active_pyrowave_profiles;
const auto restore = util::fail_guard([previous]() {
- video::active_pyrowave_mode = previous;
+ video::active_pyrowave_profiles = previous;
});
- for (int mode : {0, 2, 3}) {
- video::active_pyrowave_mode = mode;
+ for (uint32_t profiles = 0; profiles < 16; ++profiles) {
+ video::active_pyrowave_profiles = profiles << 23;
auto response = pyrowave_rtsp_response(true);
- EXPECT_EQ(response.find("a=rtpmap:99 PYROWAVE/90000") != std::string::npos, mode >= 2);
- EXPECT_EQ(response.find("a=x-ss-video[0].pyrowaveFrameFormat:PYRW/1") != std::string::npos, mode >= 2);
+ EXPECT_EQ(response.find("a=rtpmap:99 PYROWAVE/90000") != std::string::npos, profiles != 0);
+ EXPECT_EQ(response.find("a=x-ss-video[0].pyrowaveFrameFormat:PYRW/1") != std::string::npos, profiles != 0);
EXPECT_NE(response.find("CSeq: 17"), std::string::npos);
}
}
-TEST(RtspPyrowaveTest, RejectsUnavailableHdrAndUnknownFormatsBeforeStartingCapture) {
- const auto previous = video::active_pyrowave_mode;
- const auto restore = util::fail_guard([previous]() {
- video::active_pyrowave_mode = previous;
+/**
+ * @brief Validate every profile independently, stopping accepted requests at the encryption gate.
+ */
+TEST(RtspPyrowaveTest, NegotiatesProbedProfilesAndRejectsUnknownFormatsBeforeStartingCapture) {
+ const auto previous = video::active_pyrowave_profiles;
+ const auto previous_lan = config::stream.lan_encryption_mode;
+ const auto previous_wan = config::stream.wan_encryption_mode;
+ const auto restore = util::fail_guard([=]() {
+ video::active_pyrowave_profiles = previous;
+ config::stream.lan_encryption_mode = previous_lan;
+ config::stream.wan_encryption_mode = previous_wan;
});
+ config::stream.lan_encryption_mode = config::ENCRYPTION_MODE_MANDATORY;
+ config::stream.wan_encryption_mode = config::ENCRYPTION_MODE_MANDATORY;
const std::string base =
"s=Moonlight\r\n"
"a=x-nv-audio.surround.numChannels:2\r\n"
@@ -170,19 +180,25 @@ TEST(RtspPyrowaveTest, RejectsUnavailableHdrAndUnknownFormatsBeforeStartingCaptu
"a=x-nv-vqos[0].bw.maximumBitrateKbps:200000\r\n"
"a=x-nv-video[0].videoEncoderSlicesPerFrame:1\r\n"
"a=x-nv-video[0].maxNumReferenceFrames:0\r\n";
- for (const auto &[mode, format, hdr, chroma] : std::array {
- std::array {0, 3, 0, 0},
- std::array {2, 3, 1, 0},
- std::array {3, 3, 2, 0},
- std::array {3, 3, 0, 1},
- std::array {3, -1, 0, 0},
- std::array {3, 4, 0, 0}
- }) {
- video::active_pyrowave_mode = mode;
- auto payload = base + "a=x-nv-vqos[0].bitStreamFormat:" + std::to_string(format) + "\r\n" +
- "a=x-nv-video[0].dynamicRangeMode:" + std::to_string(hdr) + "\r\n" +
- "a=x-ss-video[0].chromaSamplingType:" + std::to_string(chroma) + "\r\n";
- EXPECT_TRUE(pyrowave_rtsp_response(false, payload).starts_with("RTSP/1.0 400 BAD REQUEST\r\n"));
+ for (uint32_t profiles = 0; profiles < 16; ++profiles) {
+ video::active_pyrowave_profiles = profiles << 23;
+ for (const auto &[format, hdr, chroma] : std::array {
+ std::array {3, 0, 0},
+ std::array {3, 0, 1},
+ std::array {3, 1, 0},
+ std::array {3, 1, 1},
+ std::array {3, 2, 0},
+ std::array {3, 0, 2},
+ std::array {-1, 0, 0},
+ std::array {4, 0, 0}
+ }) {
+ auto payload = base + "a=x-nv-vqos[0].bitStreamFormat:" + std::to_string(format) + "\r\n" +
+ "a=x-nv-video[0].dynamicRangeMode:" + std::to_string(hdr) + "\r\n" +
+ "a=x-ss-video[0].chromaSamplingType:" + std::to_string(chroma) + "\r\n";
+ const bool supported = format == 3 && hdr < 2 && chroma < 2 && (profiles & (1 << (hdr * 2 + chroma)));
+ EXPECT_TRUE(pyrowave_rtsp_response(false, payload).starts_with(supported ? "RTSP/1.0 403 Forbidden\r\n" : "RTSP/1.0 400 BAD REQUEST\r\n"))
+ << "profiles " << profiles << ", format " << format << ", HDR " << hdr << ", chroma " << chroma;
+ }
}
}
diff --git a/tests/unit/test_video_pyrowave.cpp b/tests/unit/test_video_pyrowave.cpp
index 1e231ddf5c8..8af9fb28395 100644
--- a/tests/unit/test_video_pyrowave.cpp
+++ b/tests/unit/test_video_pyrowave.cpp
@@ -1,6 +1,6 @@
/**
* @file tests/unit/test_video_pyrowave.cpp
- * @brief Regression coverage for PyroWave profiles, HDR precision, framing, and failures.
+ * @brief Regression coverage for PyroWave profiles, chroma and HDR precision, framing, and failures.
*/
// test includes
@@ -19,6 +19,7 @@
extern "C" {
#include
#include
+#include
#ifdef SUNSHINE_BUILD_VULKAN
#include
#endif
@@ -52,6 +53,27 @@ namespace video {
*/
std::unique_ptr make_encode_session(platf::display_t *disp, const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr encode_device);
+ /**
+ * @brief Create a PyroWave capture session with injectable codec entry points.
+ * @param encoder Capture backend descriptor.
+ * @param config Negotiated configuration.
+ * @param width Captured width.
+ * @param height Captured height.
+ * @param device Capture conversion device.
+ * @param api Codec entry points.
+ * @return Initialized session, or null on failure.
+ */
+ std::unique_ptr make_pyrowave_encode_session(const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr device, const pyrowave::api_t &api);
+
+ /**
+ * @brief Request a capture converter for the negotiated format.
+ * @param disp Capture display.
+ * @param encoder Capture backend descriptor.
+ * @param config Negotiated configuration.
+ * @return Capture device, or null when unsupported.
+ */
+ std::unique_ptr make_encode_device(platf::display_t &disp, const encoder_t &encoder, const config_t &config);
+
/**
* @brief Refresh available PyroWave profiles.
* @param encoder Capture backend descriptor.
@@ -67,18 +89,20 @@ namespace nvhttp {
uint32_t get_codec_mode_flags();
} // namespace nvhttp
-TEST(PyrowaveConfigTest, ServerInfoReportsSdrAndHdrIndependently) {
- const auto previous = video::active_pyrowave_mode;
+TEST(PyrowaveConfigTest, ServerInfoReportsEachProfileIndependently) {
+ const auto previous = video::active_pyrowave_profiles;
const auto restore = util::fail_guard([previous]() {
- video::active_pyrowave_mode = previous;
+ video::active_pyrowave_profiles = previous;
});
- video::active_pyrowave_mode = 0;
+ video::active_pyrowave_profiles = 0;
const auto baseline = nvhttp::get_codec_mode_flags();
- EXPECT_EQ(baseline & (video::pyrowave::scm_sdr | video::pyrowave::scm_hdr), 0);
- video::active_pyrowave_mode = 2;
- EXPECT_EQ(nvhttp::get_codec_mode_flags(), baseline | video::pyrowave::scm_sdr);
- video::active_pyrowave_mode = 3;
- EXPECT_EQ(nvhttp::get_codec_mode_flags(), baseline | video::pyrowave::scm_sdr | video::pyrowave::scm_hdr);
+ EXPECT_EQ(baseline & video::pyrowave::scm_mask, 0);
+ for (uint32_t profiles = 0; profiles < 16; ++profiles) {
+ video::active_pyrowave_profiles = profiles << 23;
+ EXPECT_EQ(nvhttp::get_codec_mode_flags(), baseline | (profiles << 23));
+ }
+ video::active_pyrowave_profiles = ~video::pyrowave::scm_mask;
+ EXPECT_EQ(nvhttp::get_codec_mode_flags(), baseline);
}
namespace {
@@ -89,15 +113,64 @@ namespace {
video::config_t pyrowave_config() {
return {64, 48, 60, 6000, 200000, 1, 0, 3, video::pyrowave::video_format, 0, 0, 0};
}
+
+ /**
+ * @brief Record converter requests without requiring a display or video encoder.
+ */
+ class pyrowave_test_display_t: public platf::display_t {
+ public:
+ /**
+ * @copydoc platf::display_t::capture
+ */
+ platf::capture_e capture(const push_captured_image_cb_t &, const pull_free_image_cb_t &, bool *) override {
+ return platf::capture_e::ok;
+ }
+
+ /**
+ * @copydoc platf::display_t::alloc_img
+ */
+ std::shared_ptr alloc_img() override {
+ return {};
+ }
+
+ /**
+ * @copydoc platf::display_t::dummy_img
+ */
+ int dummy_img(platf::img_t *) override {
+ return -1;
+ }
+
+ /**
+ * @copydoc platf::display_t::make_avcodec_encode_device
+ */
+ std::unique_ptr make_avcodec_encode_device(platf::pix_fmt_e format) override {
+ requested = format;
+ return std::make_unique();
+ }
+
+ platf::pix_fmt_e requested = platf::pix_fmt_e::unknown; ///< Last requested conversion format.
+ };
} // namespace
TEST(PyrowaveConfigTest, NegotiatesOnlySuccessfullyProbedProfiles) {
auto config = pyrowave_config();
- for (int mode : {0, 1, 2, 3, 4}) {
- config.dynamicRange = 0;
- EXPECT_EQ(video::pyrowave::supported(config, mode), mode == 2 || mode == 3);
- config.dynamicRange = 1;
- EXPECT_EQ(video::pyrowave::supported(config, mode), mode == 3);
+ const uint32_t flags[] {video::pyrowave::scm_sdr, video::pyrowave::scm_sdr_444, video::pyrowave::scm_hdr, video::pyrowave::scm_hdr_444};
+ for (uint32_t profiles = 0; profiles < 16; ++profiles) {
+ std::array calls {};
+ const auto probed = video::pyrowave::probe_profiles([&](const video::config_t &probe) {
+ EXPECT_TRUE(video::pyrowave::valid_config(probe));
+ const auto profile = probe.dynamicRange * 2 + probe.chromaSamplingType;
+ ++calls.at(profile);
+ return (profiles & (1 << profile)) != 0;
+ });
+ EXPECT_EQ(probed, profiles << 23);
+ EXPECT_EQ(calls, (std::array {1, 1, 1, 1}));
+ for (int profile = 0; profile < 4; ++profile) {
+ config.chromaSamplingType = profile % 2;
+ config.dynamicRange = profile / 2;
+ EXPECT_EQ(video::pyrowave::supported(config, probed), (profiles & (1 << profile)) != 0);
+ EXPECT_TRUE(video::pyrowave::supported(config, flags[profile]));
+ }
}
}
@@ -118,7 +191,7 @@ TEST(PyrowaveConfigTest, RejectsMalformedOrUnsupportedRequests) {
{&video::config_t::dynamicRange, -1},
{&video::config_t::dynamicRange, 2},
{&video::config_t::chromaSamplingType, -1},
- {&video::config_t::chromaSamplingType, 1},
+ {&video::config_t::chromaSamplingType, 2},
{&video::config_t::bitrate, 0},
{&video::config_t::bitrate, -1},
{&video::config_t::bitrate, std::numeric_limits::max()},
@@ -130,8 +203,66 @@ TEST(PyrowaveConfigTest, RejectsMalformedOrUnsupportedRequests) {
auto config = valid;
config.*member = value;
EXPECT_FALSE(video::pyrowave::valid_config(config)) << value;
- EXPECT_FALSE(video::pyrowave::supported(config, 3)) << value;
+ EXPECT_FALSE(video::pyrowave::supported(config, video::pyrowave::scm_mask)) << value;
}
+ auto config = valid;
+ config.chromaSamplingType = 1;
+ config.width = 63;
+ config.height = 47;
+ EXPECT_TRUE(video::pyrowave::valid_config(config));
+}
+
+/**
+ * @brief Select capture layouts independently of the hardware video encoder's codec support.
+ */
+TEST(PyrowaveConfigTest, SelectsCaptureFormatsForEachProfile) {
+ video::encoder_t native {
+ .platform_formats = std::make_unique(
+ platf::mem_type_e::dxgi,
+ platf::pix_fmt_e::nv12,
+ platf::pix_fmt_e::p010,
+ platf::pix_fmt_e::ayuv,
+ platf::pix_fmt_e::yuv444p16
+ )
+ };
+ video::encoder_platform_formats_avcodec cuda {
+ AV_HWDEVICE_TYPE_CUDA,
+ AV_HWDEVICE_TYPE_NONE,
+ AV_PIX_FMT_CUDA,
+ AV_PIX_FMT_NV12,
+ AV_PIX_FMT_P010,
+ AV_PIX_FMT_YUV444P,
+ AV_PIX_FMT_YUV444P16,
+ {}
+ };
+ const AVPixelFormat software_formats[] {AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUV420P10, AV_PIX_FMT_YUV444P10};
+ const AVPixelFormat dxgi_formats[] {AV_PIX_FMT_NV12, AV_PIX_FMT_VUYX, AV_PIX_FMT_P010, AV_PIX_FMT_XV30};
+ const AVPixelFormat cuda_formats[] {AV_PIX_FMT_NV12, AV_PIX_FMT_YUV444P, AV_PIX_FMT_P010, AV_PIX_FMT_YUV444P16};
+ pyrowave_test_display_t display;
+ auto config = pyrowave_config();
+ for (int profile = 0; profile < 4; ++profile) {
+ config.chromaSamplingType = profile % 2;
+ config.dynamicRange = profile / 2;
+ EXPECT_EQ(video::pyrowave::capture_format(*video::software.platform_formats, config), software_formats[profile]);
+ EXPECT_EQ(video::pyrowave::capture_format(*native.platform_formats, config), dxgi_formats[profile]);
+ EXPECT_EQ(video::pyrowave::capture_format(cuda, config), cuda_formats[profile]);
+ ASSERT_NE(video::make_encode_device(display, native, config), nullptr);
+ EXPECT_EQ(display.requested, video::map_pix_fmt(dxgi_formats[profile]));
+ }
+ video::encoder_platform_formats_t unknown {};
+ unknown.dev_type = platf::mem_type_e::system;
+ EXPECT_EQ(video::pyrowave::capture_format(unknown, config), AV_PIX_FMT_NONE);
+ cuda.avcodec_pix_fmt_yuv444_8bit = AV_PIX_FMT_NONE;
+ cuda.avcodec_pix_fmt_yuv444_10bit = AV_PIX_FMT_NONE;
+ EXPECT_EQ(video::pyrowave::capture_format(cuda, config), AV_PIX_FMT_NONE);
+ video::encoder_t unavailable {
+ .platform_formats = std::make_unique(cuda)
+ };
+ display.requested = platf::pix_fmt_e::unknown;
+ EXPECT_EQ(video::make_encode_device(display, unavailable, config), nullptr);
+ EXPECT_EQ(display.requested, platf::pix_fmt_e::unknown);
+ config.dynamicRange = 0;
+ EXPECT_EQ(video::make_encode_session(nullptr, unavailable, config, 64, 48, std::make_unique()), nullptr);
}
/**
@@ -176,20 +307,23 @@ TEST(PyrowaveConfigTest, PreservesHdrWithoutConsultingUnrelatedCodecCapabilities
auto config = pyrowave_config();
config.dynamicRange = 1;
video::encoder_t encoder {};
- EXPECT_EQ(video::resolve_dynamic_range(encoder, config).dynamicRange, 1);
+ for (int chroma : {0, 1}) {
+ config.chromaSamplingType = chroma;
+ EXPECT_EQ(video::resolve_dynamic_range(encoder, config).dynamicRange, 1);
+ }
}
TEST(PyrowaveConfigTest, DisablingTheCodecClearsPreviouslyProbedProfiles) {
- const auto previous_mode = video::active_pyrowave_mode;
+ const auto previous_profiles = video::active_pyrowave_profiles;
const auto previous_enabled = config::video.pyrowave;
const auto restore = util::fail_guard([&]() {
- video::active_pyrowave_mode = previous_mode;
+ video::active_pyrowave_profiles = previous_profiles;
config::video.pyrowave = previous_enabled;
});
- video::active_pyrowave_mode = 3;
+ video::active_pyrowave_profiles = video::pyrowave::scm_mask;
config::video.pyrowave = false;
video::probe_pyrowave({});
- EXPECT_EQ(video::active_pyrowave_mode, 0);
+ EXPECT_EQ(video::active_pyrowave_profiles, 0);
}
TEST(PyrowaveConfigTest, RejectsInvalidCaptureDevicesAndHdrFromRam) {
@@ -207,12 +341,12 @@ TEST(PyrowaveConfigTest, RejectsInvalidCaptureDevicesAndHdrFromRam) {
namespace {
/**
- * @brief Converter supplying padded NV12/P010 frames without display hardware.
+ * @brief Converter supplying padded planar or packed YUV frames without display hardware.
*/
class pyrowave_test_converter_t: public platf::avcodec_encode_device_t {
public:
/**
- * @brief Allocate a frame and fill full-range endpoint values.
+ * @brief Fill alternating luma and checkerboard chroma with full-range endpoint values.
* @param format Pixel storage layout supplied by the capture converter.
*/
explicit pyrowave_test_converter_t(AVPixelFormat format) {
@@ -229,22 +363,23 @@ namespace {
if (av_frame_get_buffer(frame, 128) < 0) {
throw std::bad_alloc();
}
- const bool planar = format == AV_PIX_FMT_YUV420P || format == AV_PIX_FMT_YUV420P10;
- for (int plane = 0; plane < (planar ? 3 : 2); ++plane) {
- for (int y = 0; y < (plane ? 24 : 48); ++y) {
- auto row = frame->data[plane] + y * frame->linesize[plane];
- const auto width = planar && plane ? 32 : 64;
- if (format == AV_PIX_FMT_P010 || format == AV_PIX_FMT_YUV420P10) {
- auto words = reinterpret_cast(row);
- for (int x = 0; x < width; ++x) {
- const auto value = (planar && plane ? plane == 2 : x % 2) ? 1023 : 1;
- words[x] = uint16_t(value << (format == AV_PIX_FMT_P010 ? 6 : 0));
- }
- } else {
- for (int x = 0; x < width; ++x) {
- row[x] = (planar && plane ? plane == 2 : x % 2) ? 255 : 1;
- }
+ for (auto buffer : frame->buf) {
+ if (buffer) {
+ std::memset(buffer->data, 0, buffer->size);
+ }
+ }
+ const auto desc = av_pix_fmt_desc_get(format);
+ const auto maximum = desc->comp[0].depth > 8 ? 1023 : 255;
+ for (int component = 0; component < 3; ++component) {
+ const auto width = 64 >> (component ? desc->log2_chroma_w : 0);
+ const auto height = 48 >> (component ? desc->log2_chroma_h : 0);
+ std::array row {};
+ for (int y = 0; y < height; ++y) {
+ for (int x = 0; x < width; ++x) {
+ const auto value = (x + (component ? y : 0) + (component == 2)) % 2 ? maximum : 1;
+ row[x] = uint16_t(value << (desc->comp[0].depth == 16 ? 6 : 0));
}
+ av_write_image_line(row.data(), frame->data, frame->linesize, desc, 0, y, component, width);
}
}
}
@@ -289,6 +424,8 @@ namespace {
int encoded = 0; ///< Number of codec calls, including repeated frames.
size_t budget = 0; ///< Last requested bitrate budget.
pyrowave_cpu_buffer_format format {}; ///< Last codec input format.
+ pyrowave_chroma_subsampling chroma {}; ///< Chroma sampling used to create the encoder.
+ std::optional> solid_color; ///< Expected uniform YUV values instead of the checkerboard.
std::array samples {}; ///< First two luma samples and first Cb/Cr samples.
std::vector destroyed; ///< Resource destruction order.
};
@@ -318,7 +455,7 @@ namespace {
api.create_encoder = [](const pyrowave_encoder_create_info *info, pyrowave_encoder *encoder) {
EXPECT_EQ(info->width, 64);
EXPECT_EQ(info->height, 48);
- EXPECT_EQ(info->chroma, PYROWAVE_CHROMA_SUBSAMPLING_420);
+ api_state->chroma = info->chroma;
if (api_state->encoder_result == PYROWAVE_SUCCESS) {
*encoder = reinterpret_cast(api_state);
}
@@ -331,11 +468,24 @@ namespace {
++api_state->encoded;
api_state->budget = rate->maximum_bitstream_size;
api_state->format = input->format;
+ const bool hdr = input->format == PYROWAVE_CPU_BUFFER_FORMAT_YUV420P10 || input->format == PYROWAVE_CPU_BUFFER_FORMAT_YUV444P10;
+ const bool subsampled = api_state->chroma == PYROWAVE_CHROMA_SUBSAMPLING_420;
for (int plane = 0; plane < 3; ++plane) {
- EXPECT_GE(input->row_stride_in_bytes[plane], plane ? 32 : 64);
- EXPECT_EQ(input->plane_size_in_bytes[plane], input->row_stride_in_bytes[plane] * (plane ? 24 : 48));
+ const auto width = plane && subsampled ? 32 : 64;
+ const auto height = plane && subsampled ? 24 : 48;
+ EXPECT_GE(input->row_stride_in_bytes[plane], width * (hdr ? 2 : 1));
+ EXPECT_EQ(input->plane_size_in_bytes[plane], input->row_stride_in_bytes[plane] * height);
+ for (int y = 0; y < height; ++y) {
+ const auto row = static_cast(input->data[plane]) + y * input->row_stride_in_bytes[plane];
+ for (int x = 0; x < width; ++x) {
+ const auto value = hdr ? reinterpret_cast(row)[x] : row[x];
+ const auto expected = api_state->solid_color ? (*api_state->solid_color)[plane] :
+ ((x + (plane ? y : 0) + (plane == 2)) % 2 ? (hdr ? 1023 : 255) : 1);
+ EXPECT_EQ(value, expected) << "plane " << plane << ", x " << x << ", y " << y;
+ }
+ }
}
- if (input->format == PYROWAVE_CPU_BUFFER_FORMAT_YUV420P10) {
+ if (hdr) {
api_state->samples = {static_cast(input->data[0])[0], static_cast(input->data[0])[1], static_cast(input->data[1])[0], static_cast(input->data[2])[0]};
} else {
api_state->samples = {static_cast(input->data[0])[0], static_cast(input->data[0])[1], static_cast(input->data[1])[0], static_cast(input->data[2])[0]};
@@ -459,10 +609,15 @@ TEST_F(PyrowaveSessionTest, RetainsTheBorrowedHardwarePoolUntilAfterConverterDes
}
TEST_F(PyrowaveSessionTest, EncodesSdrAndHdrWithExactPlaneValuesAndFraming) {
- for (auto format : {AV_PIX_FMT_NV12, AV_PIX_FMT_P010, AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P10}) {
- const bool hdr = format == AV_PIX_FMT_P010 || format == AV_PIX_FMT_YUV420P10;
+ const AVPixelFormat formats[] {AV_PIX_FMT_NV12, AV_PIX_FMT_P010, AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P10, AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUV444P10, AV_PIX_FMT_YUV444P16, AV_PIX_FMT_VUYX, AV_PIX_FMT_XV30};
+ for (auto format : formats) {
+ SCOPED_TRACE(av_get_pix_fmt_name(format));
+ const auto desc = av_pix_fmt_desc_get(format);
+ const bool hdr = desc->comp[0].depth > 8;
+ const bool yuv444 = desc->log2_chroma_w == 0;
auto config = pyrowave_config();
config.dynamicRange = hdr;
+ config.chromaSamplingType = yuv444;
auto converter = std::make_unique(format);
auto session = video::pyrowave::make_session(config, std::move(converter), fake_pyrowave_api());
ASSERT_NE(session, nullptr);
@@ -479,13 +634,16 @@ TEST_F(PyrowaveSessionTest, EncodesSdrAndHdrWithExactPlaneValuesAndFraming) {
const std::vector expected {'P', 'Y', 'R', 'W', 1, 0, 2, 0, 0, 0, 0, 3, 0x11, 0x22, 0x33, 0, 0, 0, 2, 0x44, 0x55};
EXPECT_EQ(std::vector(packet->data(), packet->data() + packet->data_size()), expected);
EXPECT_EQ(state.budget, 416666);
- EXPECT_EQ(state.format, hdr ? PYROWAVE_CPU_BUFFER_FORMAT_YUV420P10 : PYROWAVE_CPU_BUFFER_FORMAT_YUV420P);
+ EXPECT_EQ(state.chroma, yuv444 ? PYROWAVE_CHROMA_SUBSAMPLING_444 : PYROWAVE_CHROMA_SUBSAMPLING_420);
+ EXPECT_EQ(state.format, yuv444 ? (hdr ? PYROWAVE_CPU_BUFFER_FORMAT_YUV444P10 : PYROWAVE_CPU_BUFFER_FORMAT_YUV444P) : (hdr ? PYROWAVE_CPU_BUFFER_FORMAT_YUV420P10 : PYROWAVE_CPU_BUFFER_FORMAT_YUV420P));
EXPECT_EQ(state.samples, (std::array {1, uint16_t(hdr ? 1023 : 255), 1, uint16_t(hdr ? 1023 : 255)}));
// Repeated capture content must be encoded again so the codec sequence advances.
EXPECT_NE(session->encode(43), nullptr);
+ session.reset();
+ EXPECT_EQ(state.destroyed, (std::vector {"encoder", "device"}));
+ state.destroyed.clear();
}
- EXPECT_EQ(state.encoded, 8);
- EXPECT_EQ(state.destroyed, (std::vector {"encoder", "device", "encoder", "device", "encoder", "device", "encoder", "device"}));
+ EXPECT_EQ(state.encoded, 2 * std::size(formats));
}
/**
@@ -495,19 +653,24 @@ TEST_F(PyrowaveSessionTest, DownloadsHardwareFramesWithoutLosingTenBitPrecision)
#ifdef __APPLE__
constexpr auto device_type = AV_HWDEVICE_TYPE_VIDEOTOOLBOX;
constexpr auto hardware_format = AV_PIX_FMT_VIDEOTOOLBOX;
+ const AVPixelFormat formats[] {AV_PIX_FMT_NV12, AV_PIX_FMT_P010};
#else
constexpr auto device_type = AV_HWDEVICE_TYPE_VULKAN;
constexpr auto hardware_format = AV_PIX_FMT_VULKAN;
+ const AVPixelFormat formats[] {AV_PIX_FMT_NV12, AV_PIX_FMT_P010, AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUV444P10, AV_PIX_FMT_YUV444P16};
#endif
AVBufferRef *raw_device = nullptr;
if (av_hwdevice_ctx_create(&raw_device, device_type, nullptr, nullptr, 0) < 0) {
GTEST_SKIP() << "An FFmpeg-compatible " << av_hwdevice_get_type_name(device_type) << " device is unavailable";
}
video::avcodec_buffer_t device {raw_device};
- for (bool hdr : {false, true}) {
+ for (auto format : formats) {
+ SCOPED_TRACE(av_get_pix_fmt_name(format));
+ const auto desc = av_pix_fmt_desc_get(format);
+ const bool hdr = desc->comp[0].depth > 8;
auto config = pyrowave_config();
config.dynamicRange = hdr;
- const auto format = hdr ? AV_PIX_FMT_P010 : AV_PIX_FMT_NV12;
+ config.chromaSamplingType = desc->log2_chroma_w == 0;
auto converter = std::make_unique(format);
auto host = std::move(converter->owned);
converter->owned.reset(av_frame_alloc());
@@ -564,7 +727,7 @@ TEST_F(PyrowaveSessionTest, DownloadsHardwareFramesWithoutLosingTenBitPrecision)
}
}
-TEST_F(PyrowaveSessionTest, ClearsReadinessAfterConversionFailureAndRejectsWrongDepth) {
+TEST_F(PyrowaveSessionTest, ClearsReadinessAfterConversionFailureAndRejectsWrongDepthOrChroma) {
auto converter = std::make_unique(AV_PIX_FMT_NV12);
auto raw = converter.get();
auto session = video::pyrowave::make_session(pyrowave_config(), std::move(converter), fake_pyrowave_api());
@@ -579,6 +742,8 @@ TEST_F(PyrowaveSessionTest, ClearsReadinessAfterConversionFailureAndRejectsWrong
EXPECT_EQ(session->convert(image), -1);
raw->frame->format = AV_PIX_FMT_P010;
EXPECT_EQ(session->convert(image), -1);
+ raw->frame->format = AV_PIX_FMT_YUV444P;
+ EXPECT_EQ(session->convert(image), -1);
raw->frame->format = AV_PIX_FMT_NV12;
raw->frame->width = 0;
EXPECT_EQ(session->convert(image), -1);
@@ -598,7 +763,7 @@ TEST_F(PyrowaveSessionTest, RejectsInvalidInputsBeforeCallingTheCodec) {
EXPECT_EQ(video::pyrowave::make_session(pyrowave_config(), std::move(converter), fake_pyrowave_api()), nullptr);
}
auto config = pyrowave_config();
- config.chromaSamplingType = 1;
+ config.chromaSamplingType = 2;
EXPECT_EQ(video::pyrowave::make_session(config, std::make_unique(AV_PIX_FMT_NV12), fake_pyrowave_api()), nullptr);
auto converter = std::make_unique(AV_PIX_FMT_NV12);
@@ -713,11 +878,8 @@ class PyrowaveRoundTripTest: public testing::Test {
pyrowave_device device {}; ///< Real Vulkan codec device retained for decoder tests.
};
-TEST_F(PyrowaveRoundTripTest, ConvertsCapturedRgbThroughTheSoftwareBackend) {
- auto converter = std::make_unique();
- converter->colorspace = video::colorspace_from_client_config(pyrowave_config(), false);
- auto session = video::make_encode_session(nullptr, video::software, pyrowave_config(), 128, 96, std::move(converter));
- ASSERT_NE(session, nullptr);
+TEST_F(PyrowaveSessionTest, ConvertsCapturedRgbThroughTheSoftwareBackend) {
+ state.solid_color = std::array {127, 128, 128};
std::vector pixels(128 * 96 * 4, 127);
platf::img_t image {};
image.data = pixels.data();
@@ -725,10 +887,19 @@ TEST_F(PyrowaveRoundTripTest, ConvertsCapturedRgbThroughTheSoftwareBackend) {
image.height = 96;
image.pixel_pitch = 4;
image.row_pitch = 128 * 4;
- ASSERT_EQ(session->convert(image), 0);
- auto pyrowave_session = dynamic_cast(session.get());
- ASSERT_NE(pyrowave_session, nullptr);
- EXPECT_NE(pyrowave_session->encode(1), nullptr);
+ for (int chroma : {0, 1}) {
+ auto config = pyrowave_config();
+ config.chromaSamplingType = chroma;
+ auto converter = std::make_unique();
+ converter->colorspace = video::colorspace_from_client_config(config, false);
+ auto session = video::make_pyrowave_encode_session(video::software, config, 128, 96, std::move(converter), fake_pyrowave_api());
+ ASSERT_NE(session, nullptr);
+ ASSERT_EQ(session->convert(image), 0);
+ auto pyrowave_session = dynamic_cast(session.get());
+ ASSERT_NE(pyrowave_session, nullptr);
+ EXPECT_NE(pyrowave_session->encode(1), nullptr);
+ EXPECT_EQ(state.chroma, chroma ? PYROWAVE_CHROMA_SUBSAMPLING_444 : PYROWAVE_CHROMA_SUBSAMPLING_420);
+ }
}
#ifdef SUNSHINE_BUILD_VULKAN
@@ -740,7 +911,7 @@ namespace {
public:
/**
* @brief Prepare SDR or HDR host pixels for upload.
- * @param format NV12 or P010 input format.
+ * @param format Input pixel layout.
*/
explicit pyrowave_test_upload_t(AVPixelFormat format):
pyrowave_test_converter_t(format) {
@@ -793,7 +964,7 @@ namespace {
};
} // namespace
-TEST_F(PyrowaveRoundTripTest, CreatesHdrSessionsWithoutHardwareVideoEncodeUsage) {
+TEST_F(PyrowaveSessionTest, CreatesSessionsWithoutHardwareVideoEncodeUsage) {
AVBufferRef *raw_device = nullptr;
if (av_hwdevice_ctx_create(&raw_device, AV_HWDEVICE_TYPE_VULKAN, nullptr, nullptr, 0) < 0) {
GTEST_SKIP() << "An FFmpeg-compatible Vulkan device is unavailable";
@@ -806,22 +977,28 @@ TEST_F(PyrowaveRoundTripTest, CreatesHdrSessionsWithoutHardwareVideoEncodeUsage)
AV_PIX_FMT_VULKAN,
AV_PIX_FMT_NV12,
AV_PIX_FMT_P010,
- AV_PIX_FMT_NONE,
- AV_PIX_FMT_NONE,
+ AV_PIX_FMT_YUV444P,
+ AV_PIX_FMT_YUV444P16,
[&](platf::avcodec_encode_device_t *) -> util::Either {
return video::avcodec_buffer_t {av_buffer_ref(device.get())};
}
)
};
- auto config = pyrowave_config();
- config.dynamicRange = 1;
- auto session = video::make_encode_session(nullptr, encoder, config, 64, 48, std::make_unique(AV_PIX_FMT_P010));
- ASSERT_NE(session, nullptr);
- platf::img_t image {};
- ASSERT_EQ(session->convert(image), 0);
- auto pyrowave_session = dynamic_cast(session.get());
- ASSERT_NE(pyrowave_session, nullptr);
- EXPECT_NE(pyrowave_session->encode(1), nullptr);
+ for (auto format : {AV_PIX_FMT_NV12, AV_PIX_FMT_P010, AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUV444P16}) {
+ SCOPED_TRACE(av_get_pix_fmt_name(format));
+ const auto desc = av_pix_fmt_desc_get(format);
+ auto config = pyrowave_config();
+ config.dynamicRange = desc->comp[0].depth > 8;
+ config.chromaSamplingType = desc->log2_chroma_w == 0;
+ auto session = video::make_pyrowave_encode_session(encoder, config, 64, 48, std::make_unique(format), fake_pyrowave_api());
+ ASSERT_NE(session, nullptr);
+ platf::img_t image {};
+ ASSERT_EQ(session->convert(image), 0);
+ auto pyrowave_session = dynamic_cast(session.get());
+ ASSERT_NE(pyrowave_session, nullptr);
+ EXPECT_NE(pyrowave_session->encode(1), nullptr);
+ EXPECT_EQ(state.chroma, config.chromaSamplingType ? PYROWAVE_CHROMA_SUBSAMPLING_444 : PYROWAVE_CHROMA_SUBSAMPLING_420);
+ }
}
#endif
@@ -836,19 +1013,23 @@ TEST_F(PyrowaveRoundTripTest, DecodesSdrAndHdrFrameContainersWithTheUpstreamLibr
VkPhysicalDeviceProperties properties {};
vkGetPhysicalDeviceProperties(physical_device, &properties);
// Lavapipe's decoder shader crash also reproduces in PyroWave's own tests.
- // Still validate both native frame containers on software Vulkan devices.
+ // Still validate native frame containers on software Vulkan devices.
decode_pixels = properties.deviceType != VK_PHYSICAL_DEVICE_TYPE_CPU;
#endif
- for (bool hdr : {false, true}) {
+ for (int profile = 0; profile < 4; ++profile) {
+ const bool hdr = profile / 2;
+ const bool yuv444 = profile % 2;
pyrowave_decoder decoder {};
- const pyrowave_decoder_create_info info {device, 64, 48, PYROWAVE_CHROMA_SUBSAMPLING_420, false};
+ const pyrowave_decoder_create_info info {device, 64, 48, yuv444 ? PYROWAVE_CHROMA_SUBSAMPLING_444 : PYROWAVE_CHROMA_SUBSAMPLING_420, false};
ASSERT_EQ(pyrowave_decoder_create(&info, &decoder), PYROWAVE_SUCCESS);
const auto destroy_decoder = util::fail_guard([&]() {
pyrowave_decoder_destroy(decoder);
});
auto config = pyrowave_config();
config.dynamicRange = hdr;
- auto session = video::pyrowave::make_session(config, std::make_unique(hdr ? AV_PIX_FMT_P010 : AV_PIX_FMT_NV12));
+ config.chromaSamplingType = yuv444;
+ const auto format = yuv444 ? (hdr ? AV_PIX_FMT_YUV444P10 : AV_PIX_FMT_YUV444P) : (hdr ? AV_PIX_FMT_P010 : AV_PIX_FMT_NV12);
+ auto session = video::pyrowave::make_session(config, std::make_unique(format));
ASSERT_NE(session, nullptr);
platf::img_t image {};
ASSERT_EQ(session->convert(image), 0);
@@ -878,11 +1059,11 @@ TEST_F(PyrowaveRoundTripTest, DecodesSdrAndHdrFrameContainersWithTheUpstreamLibr
pyrowave_cpu_buffer decoded {};
decoded.width = 64;
decoded.height = 48;
- decoded.format = PYROWAVE_CPU_BUFFER_FORMAT_YUV420P10;
+ decoded.format = yuv444 ? PYROWAVE_CPU_BUFFER_FORMAT_YUV444P10 : PYROWAVE_CPU_BUFFER_FORMAT_YUV420P10;
for (int plane = 0; plane < 3; ++plane) {
- output[plane].resize(plane ? 32 * 24 : 64 * 48);
+ output[plane].resize(plane && !yuv444 ? 32 * 24 : 64 * 48);
decoded.data[plane] = output[plane].data();
- decoded.row_stride_in_bytes[plane] = (plane ? 32 : 64) * sizeof(uint16_t);
+ decoded.row_stride_in_bytes[plane] = (plane && !yuv444 ? 32 : 64) * sizeof(uint16_t);
decoded.plane_size_in_bytes[plane] = output[plane].size() * sizeof(uint16_t);
}
ASSERT_EQ(pyrowave_decoder_decode_cpu_buffer_synchronous(decoder, &decoded), PYROWAVE_SUCCESS);
@@ -890,6 +1071,8 @@ TEST_F(PyrowaveRoundTripTest, DecodesSdrAndHdrFrameContainersWithTheUpstreamLibr
EXPECT_NEAR(output[0][1], 1023, 8);
EXPECT_NEAR(output[1][0], hdr ? 1 : 4, 8);
EXPECT_NEAR(output[2][0], 1023, 8);
+ EXPECT_NEAR(output[1][1], 1023, 8);
+ EXPECT_NEAR(output[2][1], hdr ? 1 : 4, 8);
}
if (!decode_pixels) {
GTEST_SKIP() << "SDR/HDR packet validation passed; pixel decoding requires a hardware Vulkan device";
From 38a79b63b2cf6098290c221a14a0b97a79c149f0 Mon Sep 17 00:00:00 2001
From: joshrmcdaniel <80354972+joshrmcdaniel@users.noreply.github.com>
Date: Sat, 17 Oct 2026 19:14:09 -0500
Subject: [PATCH 13/17] skip cpu conversion when not needed
---
src/video.cpp | 66 ++++++++----
tests/unit/test_video_pyrowave.cpp | 160 ++++++++++++++++++++++-------
2 files changed, 165 insertions(+), 61 deletions(-)
diff --git a/src/video.cpp b/src/video.cpp
index ac9517ea5fe..85ecef9e758 100644
--- a/src/video.cpp
+++ b/src/video.cpp
@@ -724,7 +724,7 @@ namespace video {
}
/**
- * @brief Initialize the codec and planar frame for the negotiated color depth and chroma sampling.
+ * @brief Initialize the codec and a stable CPU input layout for the negotiated profile.
*
* @param config Negotiated stream configuration.
* @return True when the Vulkan device and encoder are usable.
@@ -746,26 +746,33 @@ namespace video {
return false;
}
- planar.reset(av_frame_alloc());
+ converted.reset(av_frame_alloc());
readback.reset(av_frame_alloc());
- if (!planar || !readback) {
+ input_frame.reset(av_frame_alloc());
+ if (!converted || !readback || !input_frame) {
return false;
}
- planar->format = config.chromaSamplingType ? (config.dynamicRange ? AV_PIX_FMT_YUV444P10 : AV_PIX_FMT_YUV444P) :
- (config.dynamicRange ? AV_PIX_FMT_YUV420P10 : AV_PIX_FMT_YUV420P);
- planar->width = config.width;
- planar->height = config.height;
- return av_frame_copy_props(planar.get(), converter->frame) >= 0 && av_frame_get_buffer(planar.get(), 0) >= 0;
+ auto source_format = converter->frame->format;
+ if (const auto pool = converter->frame->hw_frames_ctx ? converter->frame->hw_frames_ctx : frames.get()) {
+ source_format = reinterpret_cast(pool->data)->sw_format;
+ }
+ // PyroWave caches upload images, so keep the NV12 or planar layout fixed for this session.
+ converted->format = config.chromaSamplingType ? (config.dynamicRange ? AV_PIX_FMT_YUV444P10 : AV_PIX_FMT_YUV444P) :
+ (config.dynamicRange ? AV_PIX_FMT_YUV420P10 : (source_format == AV_PIX_FMT_NV12 ? AV_PIX_FMT_NV12 : AV_PIX_FMT_YUV420P));
+ converted->width = config.width;
+ converted->height = config.height;
+ return av_frame_copy_props(converted.get(), converter->frame) >= 0;
}
/**
- * @brief Convert captured pixels, retaining the platform's HDR color conversion.
+ * @brief Retain compatible frames directly, converting other layouts while preserving HDR precision.
*
* @param img Captured image.
* @return Zero on success, or a negative conversion/transfer error.
*/
int convert(platf::img_t &img) override {
ready = false;
+ av_frame_unref(input_frame.get());
if (converter->convert(img)) {
return -1;
}
@@ -786,14 +793,27 @@ namespace video {
return -1;
}
const auto input_desc = av_pix_fmt_desc_get(AVPixelFormat(source->format));
- const auto output_desc = av_pix_fmt_desc_get(AVPixelFormat(planar->format));
+ const auto output_desc = av_pix_fmt_desc_get(AVPixelFormat(converted->format));
if ((input_desc->comp[0].depth > 8) != (output_desc->comp[0].depth > 8) || input_desc->log2_chroma_w != output_desc->log2_chroma_w || input_desc->log2_chroma_h != output_desc->log2_chroma_h) {
BOOST_LOG(error) << "PyroWave input color depth or chroma sampling does not match the negotiated profile";
return -1;
}
- auto context = sws_getCachedContext(scaler.release(), source->width, source->height, AVPixelFormat(source->format), planar->width, planar->height, AVPixelFormat(planar->format), SWS_POINT, nullptr, nullptr, nullptr);
- scaler.reset(context);
- if (!scaler || av_frame_make_writable(planar.get()) < 0 || sws_scale(scaler.get(), source->data, source->linesize, 0, source->height, planar->data, planar->linesize) != planar->height) {
+ // Matching layouts and dimensions can bypass sws_scale().
+ // PyroWave requires positive row strides, so reversed rows still need conversion.
+ const bool direct = source->format == converted->format && source->width == converted->width && source->height == converted->height &&
+ std::all_of(source->linesize, source->linesize + av_pix_fmt_count_planes(AVPixelFormat(source->format)), [](int stride) {
+ return stride > 0;
+ });
+ if (!direct) {
+ auto context = sws_getCachedContext(scaler.release(), source->width, source->height, AVPixelFormat(source->format), converted->width, converted->height, AVPixelFormat(converted->format), SWS_POINT, nullptr, nullptr, nullptr);
+ scaler.reset(context);
+ if (!scaler || (!converted->buf[0] && av_frame_get_buffer(converted.get(), 0) < 0) || av_frame_make_writable(converted.get()) < 0 || sws_scale(scaler.get(), source->data, source->linesize, 0, source->height, converted->data, converted->linesize) != converted->height) {
+ return -1;
+ }
+ source = converted.get();
+ }
+ // References keep repeated frames alive; av_frame_ref() copies unowned capture buffers.
+ if (av_frame_ref(input_frame.get(), source) < 0) {
return -1;
}
ready = true;
@@ -829,14 +849,15 @@ namespace video {
return nullptr;
}
pyrowave_cpu_buffer input {};
- input.width = planar->width;
- input.height = planar->height;
- const auto desc = av_pix_fmt_desc_get(AVPixelFormat(planar->format));
- input.format = desc->log2_chroma_w == 0 ? (desc->comp[0].depth == 10 ? PYROWAVE_CPU_BUFFER_FORMAT_YUV444P10 : PYROWAVE_CPU_BUFFER_FORMAT_YUV444P) :
- (desc->comp[0].depth == 10 ? PYROWAVE_CPU_BUFFER_FORMAT_YUV420P10 : PYROWAVE_CPU_BUFFER_FORMAT_YUV420P);
- for (int plane = 0; plane < 3; ++plane) {
- input.data[plane] = planar->data[plane];
- input.row_stride_in_bytes[plane] = planar->linesize[plane];
+ input.width = input_frame->width;
+ input.height = input_frame->height;
+ const auto desc = av_pix_fmt_desc_get(AVPixelFormat(input_frame->format));
+ input.format = input_frame->format == AV_PIX_FMT_NV12 ? PYROWAVE_CPU_BUFFER_FORMAT_NV12 :
+ desc->log2_chroma_w == 0 ? (desc->comp[0].depth == 10 ? PYROWAVE_CPU_BUFFER_FORMAT_YUV444P10 : PYROWAVE_CPU_BUFFER_FORMAT_YUV444P) :
+ (desc->comp[0].depth == 10 ? PYROWAVE_CPU_BUFFER_FORMAT_YUV420P10 : PYROWAVE_CPU_BUFFER_FORMAT_YUV420P);
+ for (int plane = 0; plane < av_pix_fmt_count_planes(AVPixelFormat(input_frame->format)); ++plane) {
+ input.data[plane] = input_frame->data[plane];
+ input.row_stride_in_bytes[plane] = input_frame->linesize[plane];
input.plane_size_in_bytes[plane] = input.row_stride_in_bytes[plane] * (input.height >> (plane ? desc->log2_chroma_h : 0));
}
size_t count = 0;
@@ -875,7 +896,8 @@ namespace video {
pyrowave_encoder encoder {}; ///< Codec instance owned by this session.
pyrowave_rate_control rate_control; ///< Per-frame coefficient byte budget.
avcodec_frame_t readback; ///< Downloaded hardware frame, when needed.
- avcodec_frame_t planar; ///< Planar 8-bit or 10-bit YCbCr input.
+ avcodec_frame_t converted; ///< Stable CPU input layout, with buffers allocated only when conversion is needed.
+ avcodec_frame_t input_frame; ///< Retained pixels for encoding, including repeated frames.
sws_t scaler; ///< Plane-layout and bit-storage conversion preserving negotiated chroma and color depth.
std::vector packets; ///< Packet offsets returned by the C API.
std::vector bitstream; ///< Reusable encoded coefficient storage.
diff --git a/tests/unit/test_video_pyrowave.cpp b/tests/unit/test_video_pyrowave.cpp
index 8af9fb28395..b140d926fb3 100644
--- a/tests/unit/test_video_pyrowave.cpp
+++ b/tests/unit/test_video_pyrowave.cpp
@@ -348,8 +348,9 @@ namespace {
/**
* @brief Fill alternating luma and checkerboard chroma with full-range endpoint values.
* @param format Pixel storage layout supplied by the capture converter.
+ * @param negative_stride Store rows in reverse order within each plane.
*/
- explicit pyrowave_test_converter_t(AVPixelFormat format) {
+ explicit pyrowave_test_converter_t(AVPixelFormat format, bool negative_stride = false) {
owned.reset(av_frame_alloc());
frame = owned.get();
frame->format = format;
@@ -369,6 +370,13 @@ namespace {
}
}
const auto desc = av_pix_fmt_desc_get(format);
+ if (negative_stride) {
+ for (int plane = 0; plane < av_pix_fmt_count_planes(format); ++plane) {
+ const auto height = 48 >> (plane ? desc->log2_chroma_h : 0);
+ frame->data[plane] += (height - 1) * frame->linesize[plane];
+ frame->linesize[plane] *= -1;
+ }
+ }
const auto maximum = desc->comp[0].depth > 8 ? 1023 : 255;
for (int component = 0; component < 3; ++component) {
const auto width = 64 >> (component ? desc->log2_chroma_w : 0);
@@ -423,7 +431,7 @@ namespace {
size_t packet_size = 3; ///< First packet length.
int encoded = 0; ///< Number of codec calls, including repeated frames.
size_t budget = 0; ///< Last requested bitrate budget.
- pyrowave_cpu_buffer_format format {}; ///< Last codec input format.
+ pyrowave_cpu_buffer input {}; ///< Last codec input layout and borrowed plane addresses.
pyrowave_chroma_subsampling chroma {}; ///< Chroma sampling used to create the encoder.
std::optional> solid_color; ///< Expected uniform YUV values instead of the checkerboard.
std::array samples {}; ///< First two luma samples and first Cb/Cr samples.
@@ -467,25 +475,34 @@ namespace {
api.encode_cpu = [](pyrowave_encoder, const pyrowave_cpu_buffer *input, const pyrowave_rate_control *rate) {
++api_state->encoded;
api_state->budget = rate->maximum_bitstream_size;
- api_state->format = input->format;
+ api_state->input = *input;
+ EXPECT_EQ(input->width, 64);
+ EXPECT_EQ(input->height, 48);
const bool hdr = input->format == PYROWAVE_CPU_BUFFER_FORMAT_YUV420P10 || input->format == PYROWAVE_CPU_BUFFER_FORMAT_YUV444P10;
+ const bool nv12 = input->format == PYROWAVE_CPU_BUFFER_FORMAT_NV12;
const bool subsampled = api_state->chroma == PYROWAVE_CHROMA_SUBSAMPLING_420;
- for (int plane = 0; plane < 3; ++plane) {
- const auto width = plane && subsampled ? 32 : 64;
- const auto height = plane && subsampled ? 24 : 48;
- EXPECT_GE(input->row_stride_in_bytes[plane], width * (hdr ? 2 : 1));
+ for (int component = 0; component < 3; ++component) {
+ const auto plane = nv12 && component ? 1 : component;
+ const auto width = component && subsampled ? 32 : 64;
+ const auto height = component && subsampled ? 24 : 48;
+ EXPECT_GE(input->row_stride_in_bytes[plane], width * (hdr || (nv12 && component) ? 2 : 1));
EXPECT_EQ(input->plane_size_in_bytes[plane], input->row_stride_in_bytes[plane] * height);
for (int y = 0; y < height; ++y) {
const auto row = static_cast(input->data[plane]) + y * input->row_stride_in_bytes[plane];
for (int x = 0; x < width; ++x) {
- const auto value = hdr ? reinterpret_cast(row)[x] : row[x];
- const auto expected = api_state->solid_color ? (*api_state->solid_color)[plane] :
- ((x + (plane ? y : 0) + (plane == 2)) % 2 ? (hdr ? 1023 : 255) : 1);
+ const auto value = hdr ? reinterpret_cast(row)[x] : row[nv12 && component ? 2 * x + (component == 2) : x];
+ const auto expected = api_state->solid_color ? (*api_state->solid_color)[component] :
+ ((x + (component ? y : 0) + (component == 2)) % 2 ? (hdr ? 1023 : 255) : 1);
EXPECT_EQ(value, expected) << "plane " << plane << ", x " << x << ", y " << y;
}
}
}
- if (hdr) {
+ if (nv12) {
+ EXPECT_EQ(input->data[2], nullptr);
+ EXPECT_EQ(input->row_stride_in_bytes[2], 0);
+ EXPECT_EQ(input->plane_size_in_bytes[2], 0);
+ api_state->samples = {static_cast(input->data[0])[0], static_cast(input->data[0])[1], static_cast(input->data[1])[0], static_cast |