Skip to content

Commit 1919717

Browse files
committed
surface: keep preview-sized targets on a hardware encoder
NVENC's engine has minimum dimensions — 192x128 for AV1, 145x49 for H.264 on this 4090 — and a sidebar preview target is under them: a 256x128 card of a 2318x2235 surface inscribes to 132x128. The preference walk skips every hardware entry on extent alone, so each preview took a scarce Vulkan Video session to encode a thumbnail, on the driver path least exercised by anything else. That is also where the Xid 73 in the previous commit came from, if it came from anything blit chose. Grow the extent to the floor instead: aspect preserved, never past native (the compositor downscales its composite into the target and cannot upscale), cheapest floor by area so H.264's 145x49 wins over AV1's 192x128 rather than whichever is listed first. A 132x74 target becomes 146x82 and lands on NVENC H.264; a surface smaller than every floor keeps its extent and belongs to the tier below. The growth only engages once VA-API has been written off for the host — until then a VA-API candidate outranks the tier and there is nothing to unlock, matching `outranking_encoder_pending`. The in-flight-configure check gets the same growth against the native it is heading for: comparing a grown target with an ungrown projection would read every thumbnail as "the configure will move this" and withhold its frames for as long as a resize was outstanding.
1 parent 9f95425 commit 1919717

2 files changed

Lines changed: 255 additions & 17 deletions

File tree

crates/server/src/lib.rs

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7520,6 +7520,19 @@ async fn tick(state: &AppState) -> TickOutcome {
75207520
},
75217521
surface_encode_cap(&state.config.surface_encoders, client, sid),
75227522
);
7523+
// A target under a hardware encoder's minimum extent would
7524+
// fall through the whole chain to the compositor-resident
7525+
// tier. Grow it to the floor instead: a sidebar preview is
7526+
// then encoded by the same engine as the pane, and the
7527+
// Vulkan Video sessions stay for the extents that need them.
7528+
let (target_w, target_h) = surface_encoder::grown_to_hardware_floor(
7529+
&state.config.surface_encoders,
7530+
surface_codec_support(client, sid),
7531+
target_w,
7532+
target_h,
7533+
native_w,
7534+
native_h,
7535+
);
75237536
let (enc_w, enc_h) = (target_w, target_h);
75247537

75257538
// A Vulkan Video session is per client and per encoded size,
@@ -8071,23 +8084,49 @@ async fn tick(state: &AppState) -> TickOutcome {
80718084
// else: a configure that leaves this client's target where
80728085
// it is (another viewer nudging the mediated size, a
80738086
// one-pixel move) is no reason to withhold a frame.
8074-
if needs_new_encoder
8075-
&& let Some((cw, ch, cs120)) = resize_destination
8076-
&& Session::per_client_encode_target(
8077-
view,
8078-
cw as u32,
8079-
ch as u32,
8080-
// The destination carries the scale it will be
8081-
// configured at, so its logical size is exact —
8082-
// no need to wait for the compositor to report it.
8083-
if scaled.is_some() {
8084-
None
8085-
} else {
8086-
let s = (cs120 as u32).max(120);
8087-
Some(((cw as u32 * 120).div_ceil(s), (ch as u32 * 120).div_ceil(s)))
8088-
},
8089-
surface_encode_cap(&state.config.surface_encoders, client, sid),
8090-
) != (target_w, target_h)
8087+
// Only for a build that is actually pending: this is two
8088+
// target derivations, and the tick loop runs it per client
8089+
// per surface.
8090+
let destination_target =
8091+
resize_destination
8092+
.filter(|_| needs_new_encoder)
8093+
.map(|(cw, ch, cs120)| {
8094+
let (w, h) = Session::per_client_encode_target(
8095+
view,
8096+
cw as u32,
8097+
ch as u32,
8098+
// The destination carries the scale it will be
8099+
// configured at, so its logical size is exact
8100+
// — no need to wait for the compositor to
8101+
// report it.
8102+
if scaled.is_some() {
8103+
None
8104+
} else {
8105+
let s = (cs120 as u32).max(120);
8106+
Some((
8107+
(cw as u32 * 120).div_ceil(s),
8108+
(ch as u32 * 120).div_ceil(s),
8109+
))
8110+
},
8111+
surface_encode_cap(&state.config.surface_encoders, client, sid),
8112+
);
8113+
// Grown against the native the configure is
8114+
// heading for, exactly as the live target above
8115+
// was grown against the current one. Comparing a
8116+
// grown target with an ungrown projection would
8117+
// read every thumbnail as "the configure will move
8118+
// this" and withhold its frames.
8119+
surface_encoder::grown_to_hardware_floor(
8120+
&state.config.surface_encoders,
8121+
surface_codec_support(client, sid),
8122+
w,
8123+
h,
8124+
cw as u32,
8125+
ch as u32,
8126+
)
8127+
});
8128+
if let Some(destination) = destination_target
8129+
&& destination != (target_w, target_h)
80918130
{
80928131
client.skip_last_pixels_mismatch_count =
80938132
client.skip_last_pixels_mismatch_count.saturating_add(1);

crates/server/src/surface_encoder.rs

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)