fix(ui): start with an anti-aliasing the graphics device can't do (#329)
Choosing 8x anti-aliasing in Preferences › Graphics on a device that only allows 1x and 4x for the window's format made every later start panic in wgpu's pipeline validation (`Sample count 8 is not supported by format Bgra8Unorm on this device`), so the app could not be opened again without editing graphics.json by hand. - egui-wgpu (vendored): request TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES when the adapter has it (WebGPU only guarantees 1x and 4x), and clamp the sample count to the largest one the device supports for both the colour target and the depth/stencil format. The supported counts are exposed as `RenderState::msaa_supported`. - At start, a saved count the device can't do goes back to the default (4x, or the largest supported count below it), is written back, and a "Settings were changed" notice says what changed. - The Anti-aliasing list greys out unsupported counts, `ui.prefs` rejects them, and an invalid saved `msaa` loads as the default. - New messages translated in en, de, ja and ko. Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
6dab0fb2c1
commit
e6a95615a0
10 files changed
+277
-7
No files matched your search
@@ -200,6 +200,8 @@ fn main() -> eframe::Result {
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
eprintln!("SolidCraft graphics: {:?}, MSAA {}x (requested {}x)", rs.adapter.get_info().backend, rs.msaa_samples, samples);
|
||||
app.set_wgpu(rs, DEPTH_BITS, STENCIL_BITS, rs.msaa_samples);
|
||||
// A saved anti-aliasing the device can't do: back to the default, with a notice.
|
||||
solvecraft_ui_egui::prefs::check_msaa(&mut app, u32::from(samples));
|
||||
}
|
||||
if let Some(port) = control_port {
|
||||
let rx = control_server::start(port, cc.egui_ctx.clone());
|
||||
|
||||
@@ -239,6 +239,11 @@ pub struct SolveApp {
|
||||
pub recoverable: Vec<solvecraft_engine::recovery::Entry>,
|
||||
/// The bodies of a just-opened Y-up file, offered to be stood up (`stand_up`).
|
||||
pub stand_up: Option<Vec<String>>,
|
||||
/// The anti-aliasing sample counts the graphics device supports (empty without a GPU).
|
||||
pub msaa_supported: Vec<u32>,
|
||||
/// Settings put back at start because they kept SolidCraft from starting, one line each,
|
||||
/// shown until dismissed (`prefs::show_reset_notice`).
|
||||
pub settings_reset: Vec<prefs::SettingReset>,
|
||||
/// The last command was a big operation: autosave after it.
|
||||
pub big_operation: bool,
|
||||
/// Autosave interval (minutes; 0 = only after big operations). A preference.
|
||||
@@ -300,6 +305,8 @@ impl SolveApp {
|
||||
autosave: None,
|
||||
recoverable: Vec::new(),
|
||||
stand_up: None,
|
||||
msaa_supported: Vec::new(),
|
||||
settings_reset: Vec::new(),
|
||||
big_operation: false,
|
||||
autosave_minutes: 5.0,
|
||||
custom_titlebar: false,
|
||||
@@ -335,6 +342,7 @@ impl SolveApp {
|
||||
/// stencil buffer bits and MSAA sample count). Without it the viewport renders on the CPU.
|
||||
pub fn set_wgpu(&mut self, rs: &egui_wgpu::RenderState, depth_bits: u8, stencil_bits: u8, samples: u32) {
|
||||
self.viewport.gpu = Some(gpu::install(rs, depth_bits, stencil_bits, samples));
|
||||
self.msaa_supported = rs.msaa_supported.clone();
|
||||
let info = rs.adapter.get_info();
|
||||
self.help.gpu = Some(format!("{} ({:?}, {:?})", info.name.trim(), info.backend, info.device_type));
|
||||
}
|
||||
@@ -412,7 +420,10 @@ impl SolveApp {
|
||||
if let Some(t) = v.get("toolbar") {
|
||||
self.toolbar_custom.load(t);
|
||||
}
|
||||
if let Some(p) = v.get("preferences").and_then(|p| serde_json::from_value::<prefs::Prefs>(p.clone()).ok()) {
|
||||
if let Some(mut p) = v.get("preferences").and_then(|p| serde_json::from_value::<prefs::Prefs>(p.clone()).ok()) {
|
||||
if !prefs::MSAA.contains(&p.msaa) {
|
||||
p.msaa = prefs::Prefs::default().msaa;
|
||||
}
|
||||
self.preferences = p;
|
||||
}
|
||||
// Saved before the theme was a preference: the Dark/Light flag says which.
|
||||
@@ -1004,6 +1015,7 @@ impl SolveApp {
|
||||
params_dialog::show(self, ui.ctx());
|
||||
recovery_ui::show(self, ui.ctx());
|
||||
stand_up::show(self, ui.ctx());
|
||||
prefs::show_reset_notice(self, ui.ctx());
|
||||
context_menu::show(self, ui.ctx());
|
||||
delete::show(self, ui.ctx());
|
||||
if self.custom_titlebar {
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
"dialog.selected_count": "↖ {count} ausgewählt",
|
||||
"document.save_changes": "Änderungen in {name} speichern?",
|
||||
"document.unsaved_changes": "{name} enthält ungespeicherte Änderungen. Wenn Sie nicht speichern, gehen sie verloren.",
|
||||
"settings.msaa_applies_on_restart": "Die Kantenglättung wird beim nächsten Start von SolidCraft übernommen",
|
||||
"settings.msaa_reset": "{setting}: {requested}× wird von diesem Grafikgerät nicht unterstützt; jetzt {reset}× (unterstützt: {supported}).",
|
||||
"settings.msaa_unsupported": "Von diesem Grafikgerät nicht unterstützt",
|
||||
"settings.reset_intro": "Diese Einstellungen funktionieren auf diesem Computer nicht und würden den Start von SolidCraft verhindern. Sie wurden daher geändert:",
|
||||
"settings.reset_title": "Einstellungen wurden geändert",
|
||||
"sketch.degrees_of_freedom": "{count} Freiheitsgrade",
|
||||
"sketch.draw_curves": "{linetype}-Kurven zeichnen",
|
||||
"sketch.make_selection": "Auswahl als {linetype} markieren",
|
||||
@@ -27,6 +32,7 @@
|
||||
"ui.animate_view_changes": "Ansichtsänderungen animieren",
|
||||
"ui.animation": "Animation",
|
||||
"ui.animation_speed": "Animationsgeschwindigkeit",
|
||||
"ui.anti_aliasing": "Kantenglättung",
|
||||
"ui.apply": "Anwenden",
|
||||
"ui.arc": "Kreisbogen",
|
||||
"ui.assemble": "Zusammenbau",
|
||||
@@ -199,6 +205,7 @@
|
||||
"ui.one_side": "Einseitig",
|
||||
"ui.open": "Öffnen",
|
||||
"ui.open_ellipsis": "Öffnen...",
|
||||
"ui.open_preferences": "Einstellungen öffnen",
|
||||
"ui.open_recent": "Zuletzt verwendet",
|
||||
"ui.operation": "Operation",
|
||||
"ui.options": "Optionen",
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
"dialog.selected_count": "↖ {count} selected",
|
||||
"document.save_changes": "Save changes to {name}?",
|
||||
"document.unsaved_changes": "{name} has unsaved changes. If you don't save, they are lost.",
|
||||
"settings.msaa_applies_on_restart": "Anti-aliasing applies the next time SolidCraft starts",
|
||||
"settings.msaa_reset": "{setting}: {requested}× isn't supported by this graphics device; it is now {reset}× (supported: {supported}).",
|
||||
"settings.msaa_unsupported": "Not supported by this graphics device",
|
||||
"settings.reset_intro": "These settings don't work on this computer and would keep SolidCraft from starting, so they were changed:",
|
||||
"settings.reset_title": "Settings were changed",
|
||||
"sketch.degrees_of_freedom": "{count} degrees of freedom",
|
||||
"sketch.draw_curves": "Draw {linetype} curves",
|
||||
"sketch.make_selection": "Make the selection {linetype}",
|
||||
@@ -27,6 +32,7 @@
|
||||
"ui.animate_view_changes": "Animate view changes",
|
||||
"ui.animation": "Animation",
|
||||
"ui.animation_speed": "Animation speed",
|
||||
"ui.anti_aliasing": "Anti-aliasing",
|
||||
"ui.apply": "Apply",
|
||||
"ui.arc": "Arc",
|
||||
"ui.assemble": "Assemble",
|
||||
@@ -199,6 +205,7 @@
|
||||
"ui.one_side": "One Side",
|
||||
"ui.open": "Open",
|
||||
"ui.open_ellipsis": "Open...",
|
||||
"ui.open_preferences": "Open Preferences",
|
||||
"ui.open_recent": "Open Recent",
|
||||
"ui.operation": "Operation",
|
||||
"ui.options": "Options",
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
"dialog.selected_count": "↖ {count} 個選択",
|
||||
"document.save_changes": "{name} への変更を保存しますか?",
|
||||
"document.unsaved_changes": "{name} には保存されていない変更があります。保存しない場合、変更内容は失われます。",
|
||||
"settings.msaa_applies_on_restart": "アンチエイリアスは次回 SolidCraft を起動したときに適用されます",
|
||||
"settings.msaa_reset": "{setting}: {requested}× はこのグラフィックスデバイスではサポートされていないため、{reset}× に変更しました(サポート: {supported})。",
|
||||
"settings.msaa_unsupported": "このグラフィックスデバイスではサポートされていません",
|
||||
"settings.reset_intro": "次の設定はこのコンピューターでは使用できず、SolidCraft が起動できなくなるため変更されました:",
|
||||
"settings.reset_title": "設定が変更されました",
|
||||
"sketch.degrees_of_freedom": "自由度 {count}",
|
||||
"sketch.draw_curves": "{linetype}の曲線を描く",
|
||||
"sketch.make_selection": "選択を{linetype}にする",
|
||||
@@ -27,6 +32,7 @@
|
||||
"ui.animate_view_changes": "視点変更をアニメーション表示",
|
||||
"ui.animation": "アニメーション",
|
||||
"ui.animation_speed": "アニメーション速度",
|
||||
"ui.anti_aliasing": "アンチエイリアス",
|
||||
"ui.apply": "適用",
|
||||
"ui.arc": "円弧",
|
||||
"ui.assemble": "アセンブリ",
|
||||
@@ -199,6 +205,7 @@
|
||||
"ui.one_side": "片側",
|
||||
"ui.open": "開く",
|
||||
"ui.open_ellipsis": "開く...",
|
||||
"ui.open_preferences": "環境設定を開く",
|
||||
"ui.open_recent": "最近使った項目を開く",
|
||||
"ui.operation": "操作",
|
||||
"ui.options": "オプション",
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
"dialog.selected_count": "↖ {count}개 선택됨",
|
||||
"document.save_changes": "{name}의 변경 사항을 저장하시겠습니까?",
|
||||
"document.unsaved_changes": "{name}에 저장되지 않은 변경 사항이 있습니다. 저장하지 않으면 변경 사항이 사라집니다.",
|
||||
"settings.msaa_applies_on_restart": "안티앨리어싱은 다음에 SolidCraft를 시작할 때 적용됩니다",
|
||||
"settings.msaa_reset": "{setting}: {requested}×는 이 그래픽 장치에서 지원되지 않아 {reset}×로 변경했습니다(지원: {supported}).",
|
||||
"settings.msaa_unsupported": "이 그래픽 장치에서 지원되지 않음",
|
||||
"settings.reset_intro": "다음 설정은 이 컴퓨터에서 작동하지 않아 SolidCraft를 시작할 수 없게 하므로 변경되었습니다:",
|
||||
"settings.reset_title": "설정이 변경되었습니다",
|
||||
"sketch.degrees_of_freedom": "자유도 {count}개",
|
||||
"sketch.draw_curves": "{linetype} 곡선 그리기",
|
||||
"sketch.make_selection": "선택 항목을 {linetype}(으)로 변경",
|
||||
@@ -27,6 +32,7 @@
|
||||
"ui.animate_view_changes": "뷰 변경 애니메이션",
|
||||
"ui.animation": "애니메이션",
|
||||
"ui.animation_speed": "애니메이션 속도",
|
||||
"ui.anti_aliasing": "안티앨리어싱",
|
||||
"ui.apply": "적용",
|
||||
"ui.arc": "호",
|
||||
"ui.assemble": "조립",
|
||||
@@ -199,6 +205,7 @@
|
||||
"ui.one_side": "한 방향",
|
||||
"ui.open": "열기",
|
||||
"ui.open_ellipsis": "열기...",
|
||||
"ui.open_preferences": "환경 설정 열기",
|
||||
"ui.open_recent": "최근 항목 열기",
|
||||
"ui.operation": "작업",
|
||||
"ui.options": "옵션",
|
||||
|
||||
+136
-5
@@ -158,6 +158,7 @@ pub fn nav_preset(id: &str) -> &'static NavPreset {
|
||||
pub const PAGES: [&str; 7] = ["General", "Navigation", "Keyboard Shortcuts", "Design", "Graphics", "Sketch", "Selection"];
|
||||
pub const PAGE_SHORTCUTS: usize = 2;
|
||||
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];
|
||||
|
||||
@@ -332,7 +333,7 @@ fn defaults(app: &mut SolveApp, page: usize) {
|
||||
p.up_axis = d.up_axis;
|
||||
p.offer_stand_up = d.offer_stand_up;
|
||||
}
|
||||
4 => {
|
||||
PAGE_GRAPHICS => {
|
||||
let msaa = p.msaa != d.msaa;
|
||||
p.msaa = d.msaa;
|
||||
app.ui.visual_style = 0;
|
||||
@@ -520,6 +521,77 @@ fn save_graphics(app: &SolveApp) {
|
||||
}
|
||||
}
|
||||
|
||||
/// At start, with the graphics device known: the saved anti-aliasing (`requested` samples, as
|
||||
/// the host read it) is more than the device supports. The window would not have opened with it,
|
||||
/// so the renderer fell back; put the setting back to its default (or the largest supported
|
||||
/// count below it) and tell the user what changed.
|
||||
pub fn check_msaa(app: &mut SolveApp, requested: u32) {
|
||||
let supported = &app.msaa_supported;
|
||||
if requested <= 1 || supported.is_empty() || supported.contains(&requested) {
|
||||
return;
|
||||
}
|
||||
let default = Prefs::default().msaa;
|
||||
let reset = supported.iter().copied().filter(|&n| n <= default).max().unwrap_or(1);
|
||||
let supported = supported.clone();
|
||||
app.preferences.msaa = reset;
|
||||
save_graphics(app);
|
||||
app.settings_reset.push(SettingReset::Msaa { requested, reset, supported });
|
||||
}
|
||||
|
||||
/// A setting put back at start because it would keep SolidCraft from starting.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SettingReset {
|
||||
/// Anti-aliasing samples the graphics device can't render with.
|
||||
Msaa { requested: u32, reset: u32, supported: Vec<u32> },
|
||||
}
|
||||
|
||||
impl SettingReset {
|
||||
/// The line the notice shows, in the current interface language.
|
||||
pub fn message(&self) -> String {
|
||||
match self {
|
||||
SettingReset::Msaa { requested, reset, supported } => {
|
||||
let setting = format!("{} › {}", crate::i18n::tr("ui.graphics"), crate::i18n::tr("ui.anti_aliasing"));
|
||||
let supported = supported.iter().map(|n| format!("{n}")).collect::<Vec<_>>().join("×, ") + "×";
|
||||
crate::i18n::format(
|
||||
"settings.msaa_reset",
|
||||
&[("setting", &setting), ("requested", &requested.to_string()), ("reset", &reset.to_string()), ("supported", &supported)],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings put back at start (`SolveApp::settings_reset`), until dismissed.
|
||||
pub fn show_reset_notice(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
if app.settings_reset.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut ok = false;
|
||||
let mut prefs = false;
|
||||
crate::frame::window(ctx, app.tr("settings.reset_title"), crate::frame::Width::Normal)
|
||||
.id(egui::Id::new("sc_settings_reset"))
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
ui.label(app.tr("settings.reset_intro"));
|
||||
ui.add_space(4.0);
|
||||
for reset in &app.settings_reset {
|
||||
ui.label(format!("• {}", reset.message()));
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ok = ui.button(app.tr("ui.heading_ok")).clicked();
|
||||
prefs = ui.button(app.tr("ui.open_preferences")).clicked();
|
||||
});
|
||||
});
|
||||
if ok || prefs {
|
||||
app.settings_reset.clear();
|
||||
}
|
||||
if prefs {
|
||||
open_at(app, PAGE_GRAPHICS);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
// File › Keyboard Shortcuts and Help › Keyboard Shortcuts open this dialog at that page.
|
||||
if app.keymap.open {
|
||||
@@ -646,7 +718,6 @@ const PREFS_NAV_W: f32 = 128.0;
|
||||
|
||||
/// One page of the dialog.
|
||||
fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
let _language = crate::i18n::scope(&app.preferences.language);
|
||||
let note = |ui: &mut egui::Ui, s: &str| {
|
||||
ui.label("");
|
||||
// Descriptions wrap within the page instead of widening it.
|
||||
@@ -801,14 +872,18 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
ui.label("Perspective view");
|
||||
ui.checkbox(&mut app.ui.perspective, "");
|
||||
ui.end_row();
|
||||
ui.label("Anti-aliasing");
|
||||
ui.label(app.tr("ui.anti_aliasing"));
|
||||
egui::ComboBox::from_id_salt("pref_msaa").selected_text(format!("{}×", app.preferences.msaa)).show_ui(ui, |ui| {
|
||||
for m in MSAA {
|
||||
ui.selectable_value(&mut app.preferences.msaa, m, format!("{m}×"));
|
||||
// Counts the graphics device can't do would keep the window from opening.
|
||||
let ok = app.msaa_supported.is_empty() || app.msaa_supported.contains(&m);
|
||||
ui.add_enabled_ui(ok, |ui| ui.selectable_value(&mut app.preferences.msaa, m, format!("{m}×")))
|
||||
.inner
|
||||
.on_disabled_hover_text(app.tr("settings.msaa_unsupported"));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
note(ui, "Anti-aliasing applies the next time SolidCraft starts");
|
||||
note(ui, app.tr("settings.msaa_applies_on_restart"));
|
||||
ui.label("Renderer");
|
||||
ui.label(match (&app.viewport.gpu, &app.help.gpu) {
|
||||
(Some(_), Some(g)) => format!("GPU: {g}"),
|
||||
@@ -888,6 +963,9 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
|
||||
if !NAV_PRESETS.iter().any(|p| p.id == new.nav) || !MSAA.contains(&new.msaa) {
|
||||
return Err("`nav` must be fusion, solidworks, inventor, alias, tinkercad or openscad and `msaa` 1, 2, 4 or 8".into());
|
||||
}
|
||||
if new.msaa != app.preferences.msaa && !app.msaa_supported.is_empty() && !app.msaa_supported.contains(&new.msaa) {
|
||||
return Err(format!("this graphics device doesn't support `msaa` {}; it supports {:?}", new.msaa, app.msaa_supported));
|
||||
}
|
||||
if !THEMES.iter().any(|(id, _)| *id == new.theme) || !["z", "y"].contains(&new.up_axis.as_str()) {
|
||||
return Err("`theme` must be dark, light or system and `up_axis` z or y".into());
|
||||
}
|
||||
@@ -916,6 +994,59 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unsupported_saved_msaa_is_reset_to_the_default_with_a_notice() {
|
||||
let dir = std::env::temp_dir().join(format!("sc_msaa_reset_{}", std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let path = dir.join("graphics.json");
|
||||
let services = crate::Services { graphics_path: Some(path.to_string_lossy().to_string()), ..Default::default() };
|
||||
let mut app = SolveApp::new(solvecraft_engine::Session::default(), services);
|
||||
// WebGPU's guarantee for the surface format: 8x saved, the device can only do 1x and 4x.
|
||||
app.msaa_supported = vec![1, 4];
|
||||
app.preferences.msaa = 8;
|
||||
check_msaa(&mut app, 8);
|
||||
assert_eq!(app.preferences.msaa, 4);
|
||||
let saved: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap_or_default()).unwrap_or_default();
|
||||
assert_eq!(saved["msaa"], 4);
|
||||
assert_eq!(app.settings_reset, [SettingReset::Msaa { requested: 8, reset: 4, supported: vec![1, 4] }]);
|
||||
assert_eq!(
|
||||
app.settings_reset[0].message(),
|
||||
"Graphics › Anti-aliasing: 8× isn't supported by this graphics device; it is now 4× (supported: 1×, 4×)."
|
||||
);
|
||||
{
|
||||
let _de = crate::i18n::scope("de");
|
||||
assert_eq!(
|
||||
app.settings_reset[0].message(),
|
||||
"Grafik › Kantenglättung: 8× wird von diesem Grafikgerät nicht unterstützt; jetzt 4× (unterstützt: 1×, 4×)."
|
||||
);
|
||||
}
|
||||
// Below the default when the device can't do that either.
|
||||
app.settings_reset.clear();
|
||||
app.msaa_supported = vec![1, 2];
|
||||
check_msaa(&mut app, 8);
|
||||
assert_eq!(app.preferences.msaa, 2);
|
||||
// Supported, off, or no GPU: nothing changes (OpenGL's 1x policy isn't a reset either).
|
||||
for (supported, requested) in [(vec![1, 2, 4, 8], 8), (vec![1, 4], 1), (vec![], 8), (vec![1, 4], 4)] {
|
||||
app.settings_reset.clear();
|
||||
app.preferences.msaa = requested;
|
||||
app.msaa_supported = supported;
|
||||
check_msaa(&mut app, requested);
|
||||
assert!(app.settings_reset.is_empty());
|
||||
assert_eq!(app.preferences.msaa, requested);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_channel_rejects_msaa_the_device_cannot_do_and_bad_saved_msaa_loads_as_default() {
|
||||
let mut app = SolveApp::new(solvecraft_engine::Session::default(), crate::Services::default());
|
||||
app.msaa_supported = vec![1, 4];
|
||||
assert!(set(&mut app, &serde_json::json!({ "msaa": 8 })).is_err());
|
||||
assert!(set(&mut app, &serde_json::json!({ "msaa": 1 })).is_ok());
|
||||
app.load_prefs(&serde_json::json!({ "preferences": { "msaa": 16 } }).to_string());
|
||||
assert_eq!(app.preferences.msaa, Prefs::default().msaa);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn length_fields_read_bare_numbers_in_design_units_and_honour_explicit_units() {
|
||||
let inch = unit_scale("in").0;
|
||||
|
||||
Vendored
+6
@@ -87,3 +87,9 @@ SolidCraft changes:
|
||||
exposing the effective sample count so host paint callbacks can match it.
|
||||
- `src/winit.rs`: use that same effective count for depth and resolve attachments.
|
||||
- Other backends retain requested MSAA, without another adapter or device probe.
|
||||
- `src/lib.rs`: clamp MSAA to the largest sample count the device supports for the surface and
|
||||
depth formats (`supported_msaa_samples`, `RenderState::msaa_supported`): a saved 8x on a
|
||||
device without it failed pipeline validation, a panic at start.
|
||||
- `src/setup.rs`: the default device requests `TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES` when
|
||||
the adapter has it, so 2x and 8x work where the adapter supports them (WebGPU only
|
||||
guarantees 1x and 4x).
|
||||
Vendored
+88
-1
@@ -131,6 +131,10 @@ pub struct RenderState {
|
||||
/// Custom paint callbacks must use this, rather than the requested setting.
|
||||
pub msaa_samples: u32,
|
||||
|
||||
/// SolidCraft: the MSAA sample counts this device can render the target (and depth) format
|
||||
/// with, ascending, always starting with 1.
|
||||
pub msaa_supported: Vec<u32>,
|
||||
|
||||
/// Egui renderer responsible for drawing the UI.
|
||||
pub renderer: Arc<RwLock<Renderer>>,
|
||||
|
||||
@@ -284,8 +288,19 @@ impl RenderState {
|
||||
// SolidCraft: the OpenGL resolve path can present an entirely black frame with MSAA
|
||||
// (notably Mesa/llvmpipe). Decide after selecting the real surface adapter,
|
||||
// before creating either pipelines or attachments; no second GPU probe.
|
||||
// A sample count the device doesn't support for the target or depth format fails
|
||||
// pipeline validation (a panic at start): fall back to the largest supported one.
|
||||
let msaa_supported =
|
||||
supported_msaa_samples(&adapter, &device, target_format, options.depth_stencil_format);
|
||||
let wanted = effective_msaa_samples(adapter.get_info().backend, options.msaa_samples);
|
||||
let msaa_samples = clamp_msaa_samples(&msaa_supported, wanted);
|
||||
if msaa_samples != wanted {
|
||||
log::warn!(
|
||||
"MSAA {wanted}x is not supported for {target_format:?} on this device (supported: {msaa_supported:?}); using {msaa_samples}x"
|
||||
);
|
||||
}
|
||||
let options = RendererOptions {
|
||||
msaa_samples: effective_msaa_samples(adapter.get_info().backend, options.msaa_samples),
|
||||
msaa_samples,
|
||||
..options
|
||||
};
|
||||
let renderer = Renderer::new(&device, target_format, options);
|
||||
@@ -302,6 +317,7 @@ impl RenderState {
|
||||
queue,
|
||||
target_format,
|
||||
msaa_samples: options.msaa_samples,
|
||||
msaa_supported,
|
||||
renderer: Arc::new(RwLock::new(renderer)),
|
||||
surface_config: config.surface,
|
||||
})
|
||||
@@ -571,6 +587,58 @@ pub fn effective_msaa_samples(backend: wgpu::Backend, requested: u32) -> u32 {
|
||||
if backend == wgpu::Backend::Gl { 1 } else { requested.max(1) }
|
||||
}
|
||||
|
||||
/// SolidCraft: the MSAA sample counts `device` can render `target_format` and
|
||||
/// `depth_stencil_format` with (ascending, always starting with 1). Without
|
||||
/// [`wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES`] the device allows only the counts
|
||||
/// WebGPU guarantees for a format (1 and 4), whatever the adapter could do.
|
||||
pub fn supported_msaa_samples(
|
||||
adapter: &wgpu::Adapter,
|
||||
device: &wgpu::Device,
|
||||
target_format: wgpu::TextureFormat,
|
||||
depth_stencil_format: Option<wgpu::TextureFormat>,
|
||||
) -> Vec<u32> {
|
||||
let features = device.features();
|
||||
let flags = |format: wgpu::TextureFormat| {
|
||||
if features.contains(wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES) {
|
||||
adapter.get_texture_format_features(format).flags
|
||||
} else {
|
||||
format.guaranteed_format_features(features).flags
|
||||
}
|
||||
};
|
||||
let target = flags(target_format);
|
||||
let depth = depth_stencil_format.map(flags);
|
||||
msaa_counts(target, depth)
|
||||
}
|
||||
|
||||
/// The sample counts both the colour target and the depth buffer support. A multisampled colour
|
||||
/// target must also resolve into the single-sampled surface.
|
||||
fn msaa_counts(
|
||||
target: wgpu::TextureFormatFeatureFlags,
|
||||
depth: Option<wgpu::TextureFormatFeatureFlags>,
|
||||
) -> Vec<u32> {
|
||||
let resolves = target.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE);
|
||||
[1, 2, 4, 8, 16]
|
||||
.into_iter()
|
||||
.filter(|&n| {
|
||||
n == 1
|
||||
|| (resolves
|
||||
&& target.sample_count_supported(n)
|
||||
&& depth.is_none_or(|d| d.sample_count_supported(n)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// SolidCraft: the largest of the `supported` sample counts that is at most `requested` (1 when
|
||||
/// none is).
|
||||
pub fn clamp_msaa_samples(supported: &[u32], requested: u32) -> u32 {
|
||||
supported
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&n| n <= requested.max(1))
|
||||
.max()
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod msaa_tests {
|
||||
#[test]
|
||||
@@ -582,4 +650,23 @@ mod msaa_tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_sample_counts_fall_back_to_the_largest_supported_one() {
|
||||
use wgpu::TextureFormatFeatureFlags as F;
|
||||
// WebGPU's guarantee for Bgra8Unorm without adapter-specific features: 1 and 4 (a
|
||||
// saved 8x failed pipeline validation at start).
|
||||
let guaranteed = F::MULTISAMPLE_X4 | F::MULTISAMPLE_RESOLVE;
|
||||
let supported = super::msaa_counts(guaranteed, Some(F::MULTISAMPLE_X4));
|
||||
assert_eq!(supported, [1, 4]);
|
||||
assert_eq!(super::clamp_msaa_samples(&supported, 8), 4);
|
||||
assert_eq!(super::clamp_msaa_samples(&supported, 2), 1);
|
||||
assert_eq!(super::clamp_msaa_samples(&supported, 0), 1);
|
||||
// The depth buffer limits it too, and a target that can't resolve can't multisample.
|
||||
let adapter = F::MULTISAMPLE_X2 | F::MULTISAMPLE_X4 | F::MULTISAMPLE_X8 | F::MULTISAMPLE_RESOLVE;
|
||||
assert_eq!(super::msaa_counts(adapter, None), [1, 2, 4, 8]);
|
||||
assert_eq!(super::msaa_counts(adapter, Some(F::MULTISAMPLE_X4)), [1, 4]);
|
||||
assert_eq!(super::msaa_counts(F::MULTISAMPLE_X4, None), [1]);
|
||||
assert_eq!(super::clamp_msaa_samples(&[], 8), 1);
|
||||
}
|
||||
}
|
||||
Vendored
+4
@@ -272,6 +272,10 @@ impl WgpuSetupCreateNew {
|
||||
max_texture_dimension_2d: 8192,
|
||||
..base_limits
|
||||
},
|
||||
// SolidCraft: without it only the sample counts WebGPU guarantees (1x, 4x)
|
||||
// are allowed, even where the adapter supports 2x and 8x.
|
||||
required_features: adapter.features()
|
||||
& wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES,
|
||||
..Default::default()
|
||||
}
|
||||
}),
|
||||
|
||||
Reference in new issue
Block a user