Optional supersampling (SSAA) for the viewport, adaptive and capped to the texture limit (#1288) (#1321)

* Optional supersampling (SSAA) for the viewport, adaptive and within the texture limit (#1288)

Preferences ▸ Graphics ▸ Supersampling: Off (default), 1.5× or 2×. The frame is drawn into
textures of the viewport's own kind (format, depth/stencil, MSAA) at that multiple of its pixels,
over transparent black, then filtered down into the viewport with four bilinear taps (the exact
2 × 2 box at 2×) and composited premultiplied over the background. Lines keep their on-screen
width; silhouettes and edges come out smoother.

Adaptive (on by default): the view is drawn at full speed while the camera moves and
supersampled once it has held still for a quarter of a second.

The supersampled size stays within the device's largest texture side (#869) and a 384 MiB
budget for colour and depth (MSAA samples included); under 1.25× is not worth it and the frame
is drawn as usual.

Labels in en/de/ja/ko/zh; `ui.prefs` `supersampling` and `supersampling_adaptive`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Supersampling: factor and adaptive on one row; Off reads Off (1×)

The Graphics page grew past the colour vision menu in the preferences scenario, and a second
Off on the page took its click.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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 03:21:55 -04:00
1 parent b3df2c382b
commit 6f115ad740
9 files changed
+422 -7

No files matched your search

+41
View File
@@ -205,6 +205,8 @@ pub const PAGE_DESIGN: usize = 3;
pub const PAGE_GRAPHICS: usize = 4;
pub const THEMES: [(&str, &str); 3] = [("dark", "Dark"), ("light", "Light"), ("system", "System")];
pub const MSAA: [u32; 4] = [1, 2, 4, 8];
/// Supersampling factors (#1288): off, 1.5× and 2× the viewport's pixels each way.
pub const SUPERSAMPLING: [f32; 3] = [1.0, 1.5, 2.0];
/// Original view navigators, independent of the mouse navigation preset.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
@@ -251,6 +253,11 @@ pub struct Prefs {
/// How finely the viewport tessellates curved faces and edges: `coarse`, `normal`, `fine` or
/// `ultra` (#1287). Only the display plan the viewport draws; never the design or exports.
pub display_quality: String,
/// Supersampling (#1288): the view drawn at this many times its pixels each way and filtered
/// down (1, 1.5 or 2; 1 is off).
pub supersampling: f32,
/// Supersample only while the view holds still (full speed while orbiting).
pub supersampling_adaptive: bool,
/// Decimals shown for lengths and angles.
pub length_decimals: u8,
pub angle_decimals: u8,
@@ -331,6 +338,8 @@ impl Default for Prefs {
space_mouse_speed: 1.0,
msaa: 4,
display_quality: "normal".into(),
supersampling: 1.0,
supersampling_adaptive: true,
length_decimals: 2,
angle_decimals: 1,
theme: "dark".into(),
@@ -472,6 +481,8 @@ fn defaults(app: &mut SolveApp, page: usize) {
let msaa = p.msaa != d.msaa;
p.msaa = d.msaa;
p.display_quality = d.display_quality;
p.supersampling = d.supersampling;
p.supersampling_adaptive = d.supersampling_adaptive;
p.color_vision = d.color_vision;
app.ui.visual_style = 0;
app.ui.show_grid = true;
@@ -1118,6 +1129,19 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
}
ui.end_row();
note(ui, app.tr("settings.display_quality_note"));
ui.label(app.tr("ui.supersampling"));
let ss_label = |f: f32| if f > 1.0 { format!("{f}×") } else { crate::i18n::tr("ui.supersampling_off").to_string() };
let adaptive = app.tr("ui.supersampling_adaptive").to_string();
ui.horizontal(|ui| {
egui::ComboBox::from_id_salt("pref_ssaa").selected_text(ss_label(app.preferences.supersampling)).show_ui(ui, |ui| {
for f in SUPERSAMPLING {
ui.selectable_value(&mut app.preferences.supersampling, f, ss_label(f));
}
});
ui.add_enabled(app.preferences.supersampling > 1.0, egui::Checkbox::new(&mut app.preferences.supersampling_adaptive, adaptive));
});
ui.end_row();
note(ui, app.tr("settings.supersampling_note"));
ui.label(crate::i18n::tr_label("Renderer"));
ui.label(match (&app.viewport.gpu, &app.help.gpu) {
(Some(_), Some(g)) => format!("GPU: {g}"),
@@ -1332,6 +1356,9 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
if solidcraft_engine::doc::display_quality::DisplayQuality::parse(&new.display_quality).is_none() {
return Err("`display_quality` must be coarse, normal, fine or ultra".into());
}
if !SUPERSAMPLING.contains(&new.supersampling) {
return Err("`supersampling` must be 1 (off), 1.5 or 2".into());
}
if MiddleDoubleClick::parse(&new.middle_double_click).is_none() {
return Err("`middle_double_click` must be home, fit or off".into());
}
@@ -1368,6 +1395,20 @@ mod tests {
assert_eq!(app.preferences.display_quality, "normal");
}
/// Supersampling (#1288): off by default, adaptive, set by factor and checked.
#[test]
fn supersampling_is_off_by_default_and_checked() {
use serde_json::json;
let mut app = crate::SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
assert_eq!((app.preferences.supersampling, app.preferences.supersampling_adaptive), (1.0, true));
super::set(&mut app, &json!({"supersampling": 2.0})).unwrap();
assert_eq!(app.preferences.supersampling, 2.0);
assert!(super::set(&mut app, &json!({"supersampling": 3.0})).unwrap_err().contains("1 (off), 1.5 or 2"));
super::set(&mut app, &json!({"supersampling": 1.5, "supersampling_adaptive": false})).unwrap();
super::defaults(&mut app, super::PAGE_GRAPHICS);
assert_eq!((app.preferences.supersampling, app.preferences.supersampling_adaptive), (1.0, true));
}
#[test]
fn a_value_this_version_does_not_understand_keeps_the_other_preferences() {
use serde_json::json;