Skip to content

Commit f6ebe21

Browse files
committed
feat(macos): wire 4:4:4 BiPlanar capture for ProRes end-to-end
ProRes profiles are intrinsically 10-bit (proxy / lt / standard / hq) or 12-bit (4444 / 4444 XQ); FFmpeg's prores_videotoolbox supported input pix_fmt list does not include the 4:2:0 BiPlanar formats (NV12 / P010) Sunshine has historically delivered for H.264 / HEVC / AV1. The 422 family wants 4:2:2 or higher chroma and the 4444 family wants 4:4:4 natively. Until now, the macOS capture path could only produce 4:2:0 buffers, so the ProRes encoder probe failed at avcodec_open2 with "Couldn't open" regardless of profile selection or colorspace configuration. Wire the 4:4:4 BiPlanar capture path end-to-end: * Add nv24 / p410 to platf::pix_fmt_e and the from_pix_fmt switch. * Map AV_PIX_FMT_NV24 / AV_PIX_FMT_P410 in video::map_pix_fmt. * Populate the previously-NONE 4:4:4 pix_fmt slots on the videotoolbox encoder declaration and add YUV444_SUPPORT to its flags. H.264 and HEVC VideoToolbox don't gain anything new functionally — they remain 4:2:0 only on Apple Silicon hardware — but the 4:4:4 probe runs against them harmlessly and falls through with their YUV444 capability bit set false, which is correct. * Route nv24 / p410 through nv12_zero_device in display.mm::make_avcodec_encode_device, alongside the existing nv12 / p010 paths. * Set the matching CVPixelBufferType (kCVPixelFormatType_444YpCbCr*BiPlanarVideoRange) per pix_fmt in nv12_zero_device::init. Update the ProRes encoder probe in test_prores to use a ProRes-shaped config: * dynamicRange = 1 (10-bit pix_fmt slot instead of 8-bit). * encoderCscMode = 3 (full range, BT.709 instead of BT.601). * chromaSamplingType = 1 (4:4:4 P410 instead of 4:2:0 P010). Any ProRes probe that succeeds inherently uses 10-bit input, so DYNAMIC_RANGE is promoted eagerly here rather than relying on test_hdr_and_yuv444 below (which gates on PASSED and only sets DYNAMIC_RANGE itself). Verified end-to-end on M4 Max: the startup probe now produces "Found ProRes encoder: prores_videotoolbox [videotoolbox]", where previously the encoder failed to open at every config permutation attempted. This is the runtime-side companion to the build-deps change that adds prores_videotoolbox to the FFmpeg configure's --enable-encoder flag (LizardByte/build-deps#693). Without that, the encoder symbol is missing from libavcodec.a's static codec list and avcodec_find_encoder_by_name returns null regardless of the wiring in this commit.
1 parent ab3f788 commit f6ebe21

4 files changed

Lines changed: 101 additions & 11 deletions

File tree

src/platform/common.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ namespace platf {
241241
yuv420p10, ///< YUV 4:2:0 10-bit
242242
nv12, ///< NV12
243243
p010, ///< P010
244+
nv24, ///< NV24 (YUV 4:4:4 8-bit BiPlanar)
245+
p410, ///< P410 (YUV 4:4:4 10-bit BiPlanar)
244246
ayuv, ///< AYUV
245247
yuv444p16, ///< Planar 10-bit (shifted to 16-bit) YUV 4:4:4
246248
yuv444p, ///< Planar 8-bit YUV 4:4:4
@@ -258,6 +260,8 @@ namespace platf {
258260
_CONVERT(yuv420p10);
259261
_CONVERT(nv12);
260262
_CONVERT(p010);
263+
_CONVERT(nv24);
264+
_CONVERT(p410);
261265
_CONVERT(ayuv);
262266
_CONVERT(yuv444p16);
263267
_CONVERT(yuv444p);

src/platform/macos/display.mm

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,15 @@ capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const
301301
av_capture.pixelFormat = kCVPixelFormatType_32BGRA;
302302

303303
return std::make_unique<avcodec_encode_device_t>();
304-
} else if (pix_fmt == pix_fmt_e::nv12 || pix_fmt == pix_fmt_e::p010) {
304+
} else if (pix_fmt == pix_fmt_e::nv12 || pix_fmt == pix_fmt_e::p010 ||
305+
pix_fmt == pix_fmt_e::nv24 || pix_fmt == pix_fmt_e::p410) {
306+
// nv12 / p010 are 4:2:0 BiPlanar (8 / 10 bit); nv24 / p410 are the
307+
// 4:4:4 BiPlanar equivalents required by prores_videotoolbox for
308+
// ProRes 422 profiles (encoder downsamples internally) and ProRes
309+
// 4444 (native). nv12_zero_device is format-agnostic at the wrap
310+
// layer — it sets the capture-side CVPixelBufferType and then wraps
311+
// frames for AV_PIX_FMT_VIDEOTOOLBOX, so the same device handles all
312+
// four.
305313
auto device = std::make_unique<nv12_zero_device>();
306314

307315
device->init((void *) av_capture, pix_fmt, setResolution, setPixelFormat);

src/platform/macos/nv12_zero_device.cpp

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,30 @@ namespace platf {
5656
}
5757

5858
int nv12_zero_device::init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn) {
59-
pixel_format_fn(display, pix_fmt == pix_fmt_e::nv12 ? kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange : kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange);
59+
// Map the abstract pix_fmt_e to the matching CVPixelBufferType. The
60+
// 4:2:0 BiPlanar formats (NV12 / P010) cover H.264 / HEVC / AV1; the
61+
// 4:4:4 BiPlanar formats (NV24 / P410) cover ProRes (422 profiles via
62+
// encoder-internal downsample, 4444 profiles natively).
63+
OSType cv_format;
64+
switch (pix_fmt) {
65+
case pix_fmt_e::nv12:
66+
cv_format = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
67+
break;
68+
case pix_fmt_e::nv24:
69+
cv_format = kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange;
70+
break;
71+
case pix_fmt_e::p410:
72+
cv_format = kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange;
73+
break;
74+
case pix_fmt_e::p010:
75+
default:
76+
// p010 is the historical 10-bit 4:2:0 path; the default fall-through
77+
// matches it because display.mm::make_avcodec_encode_device is the
78+
// source of truth for which pix_fmt values reach this method.
79+
cv_format = kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange;
80+
break;
81+
}
82+
pixel_format_fn(display, cv_format);
6083

6184
this->display = display;
6285
this->resolution_fn = std::move(resolution_fn);

src/video.cpp

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,8 +1124,15 @@ namespace video {
11241124
AV_PIX_FMT_VIDEOTOOLBOX,
11251125
AV_PIX_FMT_NV12,
11261126
AV_PIX_FMT_P010,
1127-
AV_PIX_FMT_NONE,
1128-
AV_PIX_FMT_NONE,
1127+
// YUV 4:4:4 BiPlanar formats are required by prores_videotoolbox: the
1128+
// 422 family (proxy / lt / standard / hq) wants 4:2:2 or higher chroma
1129+
// and the 4444 family wants 4:4:4 natively. Feeding 4:4:4 P410 lets
1130+
// the encoder downsample to the chosen 422 profile internally. H.264
1131+
// and HEVC VideoToolbox will simply fail the 4:4:4 probe on Apple
1132+
// Silicon (hardware encoder is 4:2:0 only for those codecs) and the
1133+
// capability bit stays false for them, which is correct.
1134+
AV_PIX_FMT_NV24,
1135+
AV_PIX_FMT_P410,
11291136
vt_init_avcodec_hardware_input_buffer
11301137
),
11311138
{
@@ -1197,7 +1204,13 @@ namespace video {
11971204
{}, // Fallback options
11981205
"prores_videotoolbox"s,
11991206
},
1200-
PARALLEL_ENCODING
1207+
// YUV444_SUPPORT enables the 4:4:4 probe path; only ProRes 4444 /
1208+
// 4444 XQ profiles consume it natively on macOS but the flag is
1209+
// per-encoder family rather than per-codec, so H.264 / HEVC will
1210+
// probe at 4:4:4 too and fall through with their YUV444 capability
1211+
// bit set false (Apple Silicon's hardware H.264/HEVC encoder is
1212+
// 4:2:0 only).
1213+
PARALLEL_ENCODING | YUV444_SUPPORT
12011214
};
12021215
#endif
12031216

@@ -2783,17 +2796,55 @@ namespace video {
27832796
}
27842797

27852798
if (test_prores) {
2786-
config_max_ref_frames.videoFormat = SUNSHINE_FORMAT_PRORES;
2787-
config_autoselect.videoFormat = SUNSHINE_FORMAT_PRORES;
2788-
2789-
if (disp->is_codec_supported(encoder.prores.name, config_autoselect)) {
2790-
auto max_ref_frames_prores = validate_config(disp, encoder, config_max_ref_frames);
2799+
// ProRes profiles are intrinsically 10-bit (proxy / lt / standard / hq)
2800+
// or 12-bit (4444 / 4444 XQ) — there is no 8-bit ProRes input path in
2801+
// the FFmpeg encoder. Probe with dynamicRange = 1 so validate_config
2802+
// feeds the 10-bit pix_fmt (P010) to prores_videotoolbox rather than
2803+
// the 8-bit NV12 the H.264/HEVC probes use; otherwise the encoder
2804+
// legitimately refuses to open and PASSED stays false even though the
2805+
// downstream HDR probe would have succeeded.
2806+
config_t prores_max_ref_frames = config_max_ref_frames;
2807+
config_t prores_autoselect = config_autoselect;
2808+
prores_max_ref_frames.videoFormat = SUNSHINE_FORMAT_PRORES;
2809+
prores_autoselect.videoFormat = SUNSHINE_FORMAT_PRORES;
2810+
prores_max_ref_frames.dynamicRange = 1;
2811+
prores_autoselect.dynamicRange = 1;
2812+
// encoderCscMode = 3 (full range, BT.709) — prores_videotoolbox rejects
2813+
// the BT.601 colorspace the default SDR config carries (encoderCscMode
2814+
// = 1) even at 10-bit, because ProRes was never intended for SD content.
2815+
// BT.709 matches what test_hdr_and_yuv444 already uses for HDR probes
2816+
// and what the encoder actually expects. dynamicRange = 1 above promotes
2817+
// the color_trc to PQ where supported by the VT compression session;
2818+
// otherwise the encoder keeps BT.709 SDR tags, which it also accepts.
2819+
prores_max_ref_frames.encoderCscMode = 3;
2820+
prores_autoselect.encoderCscMode = 3;
2821+
// chromaSamplingType = 1 (4:4:4) selects the P410 pix_fmt slot.
2822+
// prores_videotoolbox's supported input pix_fmt list does not include
2823+
// 4:2:0 BiPlanar formats (P010) for any profile — the 422 family
2824+
// (proxy / lt / standard / hq) wants 4:2:2 or higher chroma, and the
2825+
// 4444 family wants 4:4:4 natively. Feeding 4:4:4 P410 lets the
2826+
// encoder downsample to the selected profile internally; feeding
2827+
// P010 makes it refuse to open with "Couldn't open".
2828+
prores_max_ref_frames.chromaSamplingType = 1;
2829+
prores_autoselect.chromaSamplingType = 1;
2830+
2831+
if (disp->is_codec_supported(encoder.prores.name, prores_autoselect)) {
2832+
auto max_ref_frames_prores = validate_config(disp, encoder, prores_max_ref_frames);
27912833
auto autoselect_prores = max_ref_frames_prores >= 0 ?
27922834
max_ref_frames_prores :
2793-
validate_config(disp, encoder, config_autoselect);
2835+
validate_config(disp, encoder, prores_autoselect);
27942836

27952837
encoder.prores[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_prores >= 0;
27962838
encoder.prores[encoder_t::PASSED] = max_ref_frames_prores >= 0 || autoselect_prores >= 0;
2839+
2840+
// Any ProRes probe that succeeds inherently uses 10-bit input, so
2841+
// promote DYNAMIC_RANGE here. test_hdr_and_yuv444 below gates on
2842+
// PASSED and only sets DYNAMIC_RANGE itself; setting it eagerly here
2843+
// makes the encoder's capability advertisement consistent for clients
2844+
// that opt into ProRes via prores_mode > 0.
2845+
if (encoder.prores[encoder_t::PASSED]) {
2846+
encoder.prores[encoder_t::DYNAMIC_RANGE] = true;
2847+
}
27972848
} else {
27982849
BOOST_LOG(info) << "Encoder ["sv << encoder.prores.name << "] is not supported on this GPU"sv;
27992850
encoder.prores.capabilities.reset();
@@ -3365,6 +3416,10 @@ namespace video {
33653416
return platf::pix_fmt_e::yuv444p;
33663417
case AV_PIX_FMT_YUV444P16:
33673418
return platf::pix_fmt_e::yuv444p16;
3419+
case AV_PIX_FMT_NV24:
3420+
return platf::pix_fmt_e::nv24;
3421+
case AV_PIX_FMT_P410:
3422+
return platf::pix_fmt_e::p410;
33683423
default:
33693424
return platf::pix_fmt_e::unknown;
33703425
}

0 commit comments

Comments
 (0)