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

+26 -3
View File
@@ -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(())