Render preview: draw within the pixel budget on 4K/5K screens (#1464) (#1477)

The product preview asked the viewport executor for the full physical
viewport, so a maximised 4K window (~8 MP) was refused against the 4 MP
limit on every frame. The preview now draws at the largest size of the
same aspect the executor admits (RenderLimits::fit, scratch included)
and filters it up into the viewport through the existing offscreen
blit. Viewports within the limits still draw at full size.

PNG setups past the image limits now say what the limits are and the
largest size at that aspect, and the image panel offers to use it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-15 15:18:00 -04:00
1 parent b6806b16cf
commit 5fbbc13885
15 files changed
+188 -12

No files matched your search

+48
View File
@@ -116,6 +116,47 @@ impl Default for RenderLimits {
}
}
impl RenderLimits {
/// The largest size with the aspect of `width` × `height`, never larger than it, within
/// these limits (and the scratch budget, when a layout is given). Both axes scale by the
/// same factor. `None` for a zero size or limits that admit no pixel at all.
pub fn fit(self, width: u64, height: u64, scratch: Option<ScratchLayout>) -> Option<(u64, u64)> {
let fits = |w: u64, h: u64| {
w <= self.max_dimension
&& h <= self.max_dimension
&& w.checked_mul(h).is_some_and(|p| p <= self.max_pixels)
&& scratch.is_none_or(|s| s.estimate(w, h).is_ok_and(|b| b <= self.max_scratch_bytes))
};
if width == 0 || height == 0 {
return None;
}
if fits(width, height) {
return Some((width, height));
}
let (w, h) = (width as f64, height as f64);
let mut budget = self.max_pixels as f64;
if let Some(s) = scratch {
let per_pixel = (s.sample_scale as f64).powi(2) * s.sample_bytes_per_pixel as f64
+ s.output_bytes_per_pixel as f64
+ s.encoding_bytes_per_pixel as f64;
if per_pixel > 0.0 {
budget = budget.min(self.max_scratch_bytes as f64 / per_pixel);
}
}
let side = self.max_dimension as f64;
let mut scale = (side / w).min(side / h).min((budget / (w * h)).sqrt()).min(1.0);
// Float rounding can land one pixel over a bound: step down until it fits.
for _ in 0..64 {
let size = (((w * scale).floor() as u64).max(1), ((h * scale).floor() as u64).max(1));
if fits(size.0, size.1) {
return Some(size);
}
scale *= 0.999;
}
None
}
}
/// Estimated pixel-buffer cost, excluding scene/texture/geometry and codec-internal memory.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
@@ -162,6 +203,13 @@ pub struct ExecutorDescriptor {
pub scratch: ScratchLayout,
}
impl ExecutorDescriptor {
/// The largest request size at this aspect this executor admits ([`RenderLimits::fit`]).
pub fn fit(&self, width: u64, height: u64) -> Option<(u64, u64)> {
self.limits.fit(width, height, Some(self.scratch))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BackendDescriptor {
+21
View File
@@ -188,3 +188,24 @@ fn identifiers_and_json_reject_unsafe_or_unsupported_input() {
assert!(serde_json::from_str::<RenderRequest>(text).is_err());
}
}
#[test]
fn fit_scales_both_axes_into_dimension_pixel_and_scratch_limits() {
let limits = RenderLimits::default();
assert_eq!(limits.fit(1920, 1080, None), Some((1920, 1080)));
assert_eq!(limits.fit(4096, 4096, None), Some((2048, 2048)));
assert_eq!(limits.fit(0, 1080, None), None);
// One long side: the dimension limit binds first.
assert_eq!(limits.fit(8192, 16, None), Some((4096, 8)));
for (w, h) in [(3840, 2160), (5120, 2880), (7680, 4320), (2160, 3840)] {
let (fw, fh) = limits.fit(w, h, None).unwrap();
assert!(fw <= 4096 && fh <= 4096 && fw * fh <= limits.max_pixels, "{w}x{h} -> {fw}x{fh}");
// Same aspect (within a pixel) and close to the budget.
assert!((fw as f64 / fh as f64 - w as f64 / h as f64).abs() < 2.0 / fh.min(fw) as f64, "{fw}x{fh}");
assert!(fw * fh > limits.max_pixels * 99 / 100, "{fw}x{fh}");
}
// A scratch layout tighter than the pixel limit binds instead.
let scratch = ScratchLayout { sample_scale: 1, sample_bytes_per_pixel: 128, output_bytes_per_pixel: 8, encoding_bytes_per_pixel: 0 };
let (fw, fh) = limits.fit(3840, 2160, Some(scratch)).unwrap();
assert!(scratch.estimate(fw, fh).unwrap() <= limits.max_scratch_bytes && fw * fh < limits.max_pixels, "{fw}x{fh}");
}