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 <[email protected]>
This commit is contained in:
1 parent
b6806b16cf
commit
5fbbc13885
15 files changed
+188
-12
No files matched your search
@@ -244,7 +244,8 @@ pub struct ViewportCallback {
|
||||
/// and the depth bias.
|
||||
pub eye: [f32; 4],
|
||||
/// Supersampling (#1288): draw the frame this many times the viewport's pixels in each
|
||||
/// direction and filter it down (1: off).
|
||||
/// direction and filter it down (1: off). Under 1 the frame is drawn smaller and filtered up
|
||||
/// (the product preview past its pixel budget, #1464).
|
||||
pub ssaa: f32,
|
||||
}
|
||||
|
||||
@@ -522,9 +523,12 @@ const SS_BUDGET: u64 = 384 << 20;
|
||||
/// The size (px) to draw a `size` viewport supersampled `factor` times in each direction:
|
||||
/// within the device's largest texture side (#869) and the memory budget (at `samples` per pixel,
|
||||
/// four bytes of colour and four of depth each). `None` when that leaves under 1.25×: the frame
|
||||
/// is drawn as usual.
|
||||
/// is drawn as usual. A `factor` under 1 draws it that much smaller, to be filtered up.
|
||||
pub(crate) fn supersampled_size(size: [f32; 2], factor: f32, max_side: u32, samples: u32) -> Option<[u32; 2]> {
|
||||
let [w, h] = size;
|
||||
if w >= 1.0 && h >= 1.0 && factor > 0.0 && factor < 1.0 {
|
||||
return Some([((w * factor).floor() as u32).clamp(1, max_side.max(1)), ((h * factor).floor() as u32).clamp(1, max_side.max(1))]);
|
||||
}
|
||||
if !(w >= 1.0 && h >= 1.0 && factor.is_finite() && factor > 1.0) {
|
||||
return None;
|
||||
}
|
||||
@@ -1139,7 +1143,7 @@ impl egui_wgpu::CallbackTrait for ViewportCallback {
|
||||
res.quads.push((img.id, b));
|
||||
}
|
||||
}
|
||||
res.ss_ready = self.ssaa > 1.0 && res.supersample(device, encoder, self);
|
||||
res.ss_ready = self.ssaa != 1.0 && res.supersample(device, encoder, self);
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
|
||||
@@ -266,6 +266,18 @@ fn image_controls(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
app.render_workspace.view.dirty = true;
|
||||
}
|
||||
});
|
||||
let draft = &app.render_workspace.view.draft;
|
||||
let size = (u64::from(draft.width), u64::from(draft.height));
|
||||
if let Some((w, h)) = RenderLimits::default().fit(size.0, size.1, None)
|
||||
&& (w, h) != size
|
||||
{
|
||||
ui.small(app.tr("render.image_too_large"));
|
||||
if ui.button(format!("{} {w} × {h}", app.tr("render.image_use_largest"))).clicked() {
|
||||
app.render_workspace.view.draft.width = u32::try_from(w).unwrap_or(1);
|
||||
app.render_workspace.view.draft.height = u32::try_from(h).unwrap_or(1);
|
||||
app.render_workspace.view.dirty = true;
|
||||
}
|
||||
}
|
||||
let transparent_label = app.tr("render.image_transparent").to_string();
|
||||
ui.checkbox(&mut app.render_workspace.view.transparent, transparent_label);
|
||||
if ui.button(app.tr("render.image_check")).clicked() {
|
||||
@@ -435,6 +447,13 @@ fn hash_surface(surface: Option<solidcraft_engine::render::surface::OpaqueSurfac
|
||||
}
|
||||
}
|
||||
|
||||
/// The size the product preview draws at for a viewport of `full` physical pixels: the full
|
||||
/// size when the viewport executor admits it, else the largest size at the same aspect it does.
|
||||
/// Without an executor the request is left for resolution to report.
|
||||
pub fn preview_size(executor: Option<&ExecutorDescriptor>, full: (u64, u64)) -> (u64, u64) {
|
||||
executor.and_then(|e| e.fit(full.0, full.1)).unwrap_or(full)
|
||||
}
|
||||
|
||||
pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let rect = ui.max_rect();
|
||||
app.viewport.rect = Some(rect);
|
||||
@@ -454,12 +473,16 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
if setup.exposure_ev != 0.0 || setup.environment_rotation != 0.0 || setup.environment_intensity != 1.0 {
|
||||
return Err("fixed studio preview requires exposure 0, environment rotation 0 and intensity 1".into());
|
||||
}
|
||||
// A 4K or 5K viewport is past the preview's pixel budget: draw it smaller and filter it
|
||||
// up into the viewport rather than refusing the frame.
|
||||
let full = ((rect.width() * ppp).max(1.0) as u64, (rect.height() * ppp).max(1.0) as u64);
|
||||
let (width, height) = preview_size(app.session.render_viewport(), full);
|
||||
let mut request = RenderRequest {
|
||||
backend: Some(setup.backend.clone()),
|
||||
mode: setup.mode,
|
||||
operation: RenderOperation::Viewport,
|
||||
width: (rect.width() * ppp).max(1.0) as u64,
|
||||
height: (rect.height() * ppp).max(1.0) as u64,
|
||||
width,
|
||||
height,
|
||||
format: None,
|
||||
features: Vec::new(),
|
||||
policy: Default::default(),
|
||||
@@ -516,7 +539,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
access: None,
|
||||
outline: false,
|
||||
eye: [eye.x as f32, eye.y as f32, eye.z as f32, if camera.fov > 0.0 { 1.0 } else { 0.0 }],
|
||||
ssaa: 1.0,
|
||||
ssaa: if width < full.0 { width as f32 / full.0 as f32 } else { 1.0 },
|
||||
};
|
||||
ui.painter().add(egui_wgpu::Callback::new_paint_callback(rect, callback));
|
||||
Ok(())
|
||||
|
||||
@@ -636,3 +636,40 @@ fn render_workspace_view_keys_double_middle_click_and_navigators_work() {
|
||||
assert!(h.app.session.selection.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
/// #1464: a maximised 4K or 5K window is past the preview's pixel budget. The preview draws
|
||||
/// smaller at the same aspect instead of refusing every frame.
|
||||
#[test]
|
||||
fn four_and_five_k_viewports_preview_downscaled_within_the_limits() {
|
||||
use solidcraft_engine::render::*;
|
||||
for samples in [1, 4, 16] {
|
||||
let host = host_executor(16384, true, samples).unwrap();
|
||||
let mut s = Session::default();
|
||||
s.set_render_viewport(Some(host.clone())).unwrap();
|
||||
let request = |width, height| RenderRequest {
|
||||
backend: None,
|
||||
mode: RenderMode::Preview,
|
||||
operation: RenderOperation::Viewport,
|
||||
width,
|
||||
height,
|
||||
format: None,
|
||||
features: vec![RenderFeature::Pbr],
|
||||
policy: Default::default(),
|
||||
};
|
||||
if samples <= 4 {
|
||||
assert_eq!(preview_size(s.render_viewport(), (1920, 1080)), (1920, 1080), "fits: drawn at full size");
|
||||
}
|
||||
// 16× MSAA's scratch cost brings even 1080p under the budget.
|
||||
let sizes: &[(u64, u64)] = if samples == 16 { &[(1920, 1080), (3840, 2160), (5120, 2880)] } else { &[(3840, 2160), (5120, 2880)] };
|
||||
for &full in sizes {
|
||||
assert!(resolve_request(&[s.render_backend()], &request(full.0, full.1)).is_err(), "{full:?} at full size is over the limit");
|
||||
let (w, h) = preview_size(s.render_viewport(), full);
|
||||
assert!(w < full.0 && h < full.1 && w * h <= host.limits.max_pixels, "{full:?} -> {w}x{h}");
|
||||
assert!((w as f64 / h as f64 - 16.0 / 9.0).abs() < 0.01, "{w}x{h} keeps the aspect");
|
||||
let resolved = resolve_request(&[s.render_backend()], &request(w, h)).unwrap();
|
||||
assert_eq!(resolved.executor.as_str(), "gpu-viewport");
|
||||
assert!(resolved.estimated_scratch_bytes <= host.limits.max_scratch_bytes);
|
||||
}
|
||||
}
|
||||
assert_eq!(preview_size(None, (5120, 2880)), (5120, 2880), "no executor: resolution reports it");
|
||||
}
|
||||
@@ -3459,6 +3459,8 @@ mod tests {
|
||||
assert_eq!(supersampled_size([3840.0, 2160.0], 2.0, 4096, 1), None);
|
||||
assert_eq!(supersampled_size([1920.0, 1080.0], 1.0, 16384, 1), None);
|
||||
assert_eq!(supersampled_size([0.0, 1080.0], 2.0, 16384, 1), None);
|
||||
// Under 1: drawn smaller and filtered up (the product preview past its budget, #1464).
|
||||
assert_eq!(supersampled_size([3840.0, 2160.0], 0.5, 4096, 4), Some([1920, 1080]));
|
||||
// 4× MSAA at 1440p: the memory budget caps it below 2×.
|
||||
let [w, h] = supersampled_size([2560.0, 1440.0], 2.0, 16384, 4).unwrap();
|
||||
assert!(w < 5120 && w as f32 >= 2560.0 * 1.25 && u64::from(w) * u64::from(h) * 32 <= 384 << 20, "{w} x {h}");
|
||||
|
||||
Reference in new issue
Block a user