@@ -327,6 +327,120 @@ pub fn outranking_encoder_pending(
327327 false
328328}
329329
330+ /// Grow a coded extent, aspect preserved, to the smallest one an encoder that
331+ /// outranks the Vulkan Video tier will accept. Returns the extent unchanged
332+ /// when nothing would be unlocked by growing it.
333+ ///
334+ /// NVENC's engine has minimum dimensions — 192x128 for AV1 and 145x49 for
335+ /// H.264 on an RTX 4090, queried rather than assumed — and a sidebar preview
336+ /// target (132x128 for a 2318x2235 surface) is under them. The preference
337+ /// walk then skips every hardware entry on extent alone and the surface falls
338+ /// to the compositor-resident tier: one scarce Vulkan Video session per
339+ /// preview, each encoding a thumbnail, on the driver path least exercised by
340+ /// anything else. Two hundred extra rows of picture at 15fps costs less than
341+ /// that, so clear the floor instead and keep previews on the encoder the pane
342+ /// already uses.
343+ ///
344+ /// `native` bounds the growth: the compositor downscales its composite into
345+ /// this target and there is no upscaling past the source. A surface that is
346+ /// itself under the floor therefore keeps its extent — nothing here can help
347+ /// it, and the tier below is where it belongs.
348+ pub fn grown_to_hardware_floor (
349+ preferences : & [ SurfaceEncoderPreference ] ,
350+ codec_support : u8 ,
351+ width : u32 ,
352+ height : u32 ,
353+ native_w : u32 ,
354+ native_h : u32 ,
355+ ) -> ( u32 , u32 ) {
356+ if width == 0 || height == 0 {
357+ return ( width, height) ;
358+ }
359+ let mut best: Option < ( u32 , u32 ) > = None ;
360+ for & pref in preferences {
361+ // Ranked below the tier: past this point nothing outranks Vulkan
362+ // Video, so there is no floor left worth clearing.
363+ if pref. is_vulkan_video ( ) {
364+ break ;
365+ }
366+ if !pref. supported_by_client ( codec_support) || !pref. fits ( width, height) {
367+ continue ;
368+ }
369+ if known_unavailable ( pref, ChromaSubsampling :: Cs420 ) {
370+ continue ;
371+ }
372+ let codec = match pref {
373+ SurfaceEncoderPreference :: NvencAV1 => "av1" ,
374+ SurfaceEncoderPreference :: NvencH264 => "h264" ,
375+ // No queryable minimum, and every one of these takes a thumbnail:
376+ // this candidate already accepts the extent, so the chain never
377+ // reaches the tier and growing would buy nothing.
378+ _ => return ( width, height) ,
379+ } ;
380+ let Ok ( caps) = crate :: nvenc_encode:: caps ( codec, false ) else {
381+ continue ;
382+ } ;
383+ if caps. refuse ( width, height) . is_none ( ) {
384+ // Hardware already takes it as-is.
385+ return ( width, height) ;
386+ }
387+ if width > caps. max_width || height > caps. max_height {
388+ // Refused for being too large; growing makes that worse.
389+ continue ;
390+ }
391+ let Some ( candidate) = grown_to_floor (
392+ width,
393+ height,
394+ caps. min_width ,
395+ caps. min_height ,
396+ native_w,
397+ native_h,
398+ ) else {
399+ continue ;
400+ } ;
401+ // Cheapest by area, so H.264's 145x49 floor wins over AV1's 192x128
402+ // rather than whichever happens to be listed first.
403+ if best. is_none_or ( |( bw, bh) | {
404+ ( candidate. 0 as u64 ) * ( candidate. 1 as u64 ) < ( bw as u64 ) * ( bh as u64 )
405+ } ) {
406+ best = Some ( candidate) ;
407+ }
408+ }
409+ best. unwrap_or ( ( width, height) )
410+ }
411+
412+ /// Smallest extent that has (near enough) the aspect of `width`x`height`, is
413+ /// no smaller than it, clears `(min_w, min_h)` on both axes, and still fits
414+ /// inside the source.
415+ ///
416+ /// Widen until the proportional height reaches `min_h`, then let whichever
417+ /// axis binds set the other. `None` when the result would be larger than
418+ /// the composite it is downscaled from — growing past the source is not a
419+ /// thing the compositor can do, and a surface that small belongs to the tier
420+ /// below anyway.
421+ fn grown_to_floor (
422+ width : u32 ,
423+ height : u32 ,
424+ min_w : u32 ,
425+ min_h : u32 ,
426+ native_w : u32 ,
427+ native_h : u32 ,
428+ ) -> Option < ( u32 , u32 ) > {
429+ if width == 0 || height == 0 {
430+ return None ;
431+ }
432+ let w = width
433+ . max ( min_w)
434+ . max ( ( u64:: from ( min_h) * u64:: from ( width) ) . div_ceil ( u64:: from ( height) ) as u32 ) ;
435+ let h = height
436+ . max ( min_h)
437+ . max ( ( u64:: from ( w) * u64:: from ( height) ) . div_ceil ( u64:: from ( width) ) as u32 ) ;
438+ // Even, like every other coded extent the server hands out: NV12
439+ // sampling grids and the encoder APIs both want it.
440+ let ( w, h) = ( ( w + 1 ) & !1 , ( h + 1 ) & !1 ) ;
441+ ( w <= native_w && h <= native_h) . then_some ( ( w, h) )
442+ }
443+
330444/// Chroma subsampling mode.
331445///
332446/// - **Cs420** (default): 4:2:0 — U/V at half horizontal and half vertical
@@ -2911,6 +3025,91 @@ mod tests {
29113025 ) ;
29123026 }
29133027
3028+ /// The geometry of clearing an engine's minimum extent, which is where
3029+ /// a preview target either keeps its aspect or starts letterboxing.
3030+ #[ test]
3031+ fn growing_to_an_engine_floor_keeps_the_aspect ( ) {
3032+ // A 2318x2235 surface previewed in a 256x128 sidebar card inscribes
3033+ // to 132x128, under AV1's 192x128 floor. Both axes clear it and the
3034+ // aspect survives to within a percent.
3035+ let ( w, h) = grown_to_floor ( 132 , 128 , 192 , 128 , 2318 , 2235 ) . expect ( "fits in native" ) ;
3036+ assert_eq ! ( ( w, h) , ( 192 , 188 ) ) ;
3037+ assert ! ( ( w as f32 / h as f32 - 132.0 / 128.0 ) . abs( ) < 0.02 ) ;
3038+
3039+ // The dock's wide strip is under AV1's floor on height alone, so
3040+ // width has to grow far more than height to keep the shape — 7x the
3041+ // pixels. It already clears H.264's, which is why the caller picks
3042+ // the cheapest floor by area rather than the first one listed.
3043+ assert_eq ! (
3044+ grown_to_floor( 256 , 68 , 192 , 128 , 2318 , 2235 ) ,
3045+ Some ( ( 482 , 130 ) )
3046+ ) ;
3047+ assert_eq ! (
3048+ grown_to_floor( 256 , 68 , 145 , 49 , 2318 , 2235 ) ,
3049+ Some ( ( 256 , 68 ) )
3050+ ) ;
3051+
3052+ // Already clear of the floor: nothing moves but the even rounding.
3053+ assert_eq ! (
3054+ grown_to_floor( 800 , 600 , 192 , 128 , 1600 , 1200 ) ,
3055+ Some ( ( 800 , 600 ) )
3056+ ) ;
3057+
3058+ // No upscaling past the composite: a surface smaller than the floor
3059+ // keeps the extent it had and lands on the tier below.
3060+ assert_eq ! ( grown_to_floor( 100 , 80 , 192 , 128 , 100 , 80 ) , None ) ;
3061+ assert_eq ! ( grown_to_floor( 0 , 128 , 192 , 128 , 2318 , 2235 ) , None ) ;
3062+ }
3063+
3064+ /// The floor walk stops where the tier does, and only NVENC has a floor
3065+ /// worth clearing — every other backend takes a thumbnail as it is.
3066+ #[ test]
3067+ fn nothing_grows_for_a_chain_that_already_takes_the_extent ( ) {
3068+ use SurfaceEncoderPreference as P ;
3069+ let h264 = CODEC_SUPPORT_H264 ;
3070+ // Software sits above the tier here and accepts 132x128, so the
3071+ // chain never reaches Vulkan Video and there is nothing to unlock.
3072+ assert_eq ! (
3073+ grown_to_hardware_floor(
3074+ & [ P :: H264Software , P :: VulkanVideoH264 ] ,
3075+ h264,
3076+ 132 ,
3077+ 128 ,
3078+ 2318 ,
3079+ 2235
3080+ ) ,
3081+ ( 132 , 128 )
3082+ ) ;
3083+ // Below the tier, an encoder's floor is not the tier's problem.
3084+ assert_eq ! (
3085+ grown_to_hardware_floor(
3086+ & [ P :: VulkanVideoH264 , P :: NvencH264 ] ,
3087+ h264,
3088+ 132 ,
3089+ 128 ,
3090+ 2318 ,
3091+ 2235
3092+ ) ,
3093+ ( 132 , 128 )
3094+ ) ;
3095+ assert_eq ! (
3096+ grown_to_hardware_floor( & [ ] , h264, 132 , 128 , 2318 , 2235 ) ,
3097+ ( 132 , 128 )
3098+ ) ;
3099+ // A candidate this client cannot decode is not serving it.
3100+ assert_eq ! (
3101+ grown_to_hardware_floor(
3102+ & [ P :: NvencAV1 , P :: VulkanVideoH264 ] ,
3103+ h264,
3104+ 132 ,
3105+ 128 ,
3106+ 2318 ,
3107+ 2235
3108+ ) ,
3109+ ( 132 , 128 )
3110+ ) ;
3111+ }
3112+
29143113 /// The Vulkan tier ranks below the dedicated encode engines, and this
29153114 /// is what holds it there. Only the structurally-decided backends are
29163115 /// used: the NVENC arm reads host capabilities.
0 commit comments