Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b01b9689c8 | ||
|
|
4426da6f5d | ||
|
|
5ead82e627 | ||
|
|
c29b0d2f13 | ||
|
|
480fd8d517 | ||
|
|
4c09fa5355 |
No files matched your search
@@ -306,11 +306,38 @@ jobs:
|
|||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
with:
|
with:
|
||||||
key: windows-fxc
|
key: windows-fxc
|
||||||
- run: cargo test -p solidcraft-gpu --profile ci --locked
|
# One test at a time: WARP's shader JIT crashes now and then (a null read in
|
||||||
|
# d3d10warp.dll's JIT optimizer, about 1 run in 60) when several devices compile compute
|
||||||
|
# pipelines at once, which the parallel tests do. Minidumps showed it in WARP itself, not
|
||||||
|
# in SolidCraft or FXC; serial runs never hit it.
|
||||||
|
- run: cargo test -p solidcraft-gpu --profile ci --locked -- --test-threads=1
|
||||||
|
|
||||||
|
# The Windows app starts D3D12 first and only the backend it uses (a Vulkan driver crashed the
|
||||||
|
# app at startup on Intel). Start the real app on the runner (its D3D12 adapter is WARP) with
|
||||||
|
# several saved settings and safe mode, and check each draws a frame and quits cleanly.
|
||||||
|
windows-startup:
|
||||||
|
name: Windows app startup
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.native == 'true'
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
|
with:
|
||||||
|
key: windows-startup
|
||||||
|
- name: Backend order and probing on Windows
|
||||||
|
run: cargo test -p solidcraft --profile ci --locked -- startup::tests
|
||||||
|
- name: Build the app
|
||||||
|
run: cargo build -p solidcraft --profile ci --locked
|
||||||
|
- name: Start the app under different settings
|
||||||
|
shell: pwsh
|
||||||
|
run: tools/windows-startup-smoke.ps1 -Exe target/ci/solidcraft.exe
|
||||||
|
|
||||||
# Preserve the existing check name for branch protection, including docs-only PRs.
|
# Preserve the existing check name for branch protection, including docs-only PRs.
|
||||||
ci:
|
ci:
|
||||||
needs: [changes, checks, clippy, build-tests, tests, doc-tests, oracle, windows-fxc]
|
needs: [changes, checks, clippy, build-tests, tests, doc-tests, oracle, windows-fxc, windows-startup]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
@@ -325,6 +352,7 @@ jobs:
|
|||||||
DOCS: ${{ needs.doc-tests.result }}
|
DOCS: ${{ needs.doc-tests.result }}
|
||||||
ORACLE: ${{ needs.oracle.result }}
|
ORACLE: ${{ needs.oracle.result }}
|
||||||
WINDOWS_FXC: ${{ needs.windows-fxc.result }}
|
WINDOWS_FXC: ${{ needs.windows-fxc.result }}
|
||||||
|
WINDOWS_STARTUP: ${{ needs.windows-startup.result }}
|
||||||
run: |
|
run: |
|
||||||
test "$CHANGES" = success
|
test "$CHANGES" = success
|
||||||
test "$CHECKS" = success
|
test "$CHECKS" = success
|
||||||
@@ -335,6 +363,7 @@ jobs:
|
|||||||
test "$DOCS" = success
|
test "$DOCS" = success
|
||||||
test "$ORACLE" = success
|
test "$ORACLE" = success
|
||||||
test "$WINDOWS_FXC" = success
|
test "$WINDOWS_FXC" = success
|
||||||
|
test "$WINDOWS_STARTUP" = success
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wasm:
|
wasm:
|
||||||
|
|||||||
Generated
+14
-14
@@ -3445,7 +3445,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solidcraft"
|
name = "solidcraft"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block2 0.6.2",
|
"block2 0.6.2",
|
||||||
"eframe",
|
"eframe",
|
||||||
@@ -3471,7 +3471,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solidcraft-cli"
|
name = "solidcraft-cli"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"solidcraft-engine",
|
"solidcraft-engine",
|
||||||
@@ -3480,7 +3480,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solidcraft-doc"
|
name = "solidcraft-doc"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3495,7 +3495,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solidcraft-engine"
|
name = "solidcraft-engine"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3511,7 +3511,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solidcraft-geom"
|
name = "solidcraft-geom"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3520,7 +3520,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solidcraft-gpu"
|
name = "solidcraft-gpu"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"naga",
|
"naga",
|
||||||
@@ -3533,7 +3533,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solidcraft-io"
|
name = "solidcraft-io"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3548,7 +3548,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solidcraft-kernel"
|
name = "solidcraft-kernel"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"rayon",
|
"rayon",
|
||||||
@@ -3566,7 +3566,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solidcraft-mcp"
|
name = "solidcraft-mcp"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"solidcraft-engine",
|
"solidcraft-engine",
|
||||||
@@ -3574,7 +3574,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solidcraft-render"
|
name = "solidcraft-render"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"image",
|
"image",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3584,7 +3584,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solidcraft-sketch"
|
name = "solidcraft-sketch"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3595,7 +3595,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solidcraft-ui-egui"
|
name = "solidcraft-ui-egui"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"egui",
|
"egui",
|
||||||
"egui-wgpu",
|
"egui-wgpu",
|
||||||
@@ -3616,7 +3616,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solidcraft-web"
|
name = "solidcraft-web"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
@@ -5094,7 +5094,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xtask"
|
name = "xtask"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
|||||||
exclude = ["vendor"]
|
exclude = ["vendor"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ log = { workspace = true }
|
|||||||
rfd = { workspace = true }
|
rfd = { workspace = true }
|
||||||
# Checks eframe's settings file (app.ron) before eframe reads it (src/startup.rs).
|
# Checks eframe's settings file (app.ron) before eframe reads it (src/startup.rs).
|
||||||
ron = "0.12"
|
ron = "0.12"
|
||||||
|
# Waits for wgpu's adapter list when choosing the graphics backend before the window opens.
|
||||||
|
pollster = { workspace = true }
|
||||||
getrandom = "0.3"
|
getrandom = "0.3"
|
||||||
# The update check (docs/updates.md): HTTPS with rustls, certificates from the operating system.
|
# The update check (docs/updates.md): HTTPS with rustls, certificates from the operating system.
|
||||||
ureq = { version = "3.4", default-features = false, features = ["rustls-no-provider", "platform-verifier"] }
|
ureq = { version = "3.4", default-features = false, features = ["rustls-no-provider", "platform-verifier"] }
|
||||||
@@ -50,8 +52,5 @@ block2 = "0.6"
|
|||||||
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
|
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
|
||||||
winit = { version = "0.30", default-features = false, features = ["x11"] }
|
winit = { version = "0.30", default-features = false, features = ["x11"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
pollster = { workspace = true }
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
embed-resource = "3.0.11"
|
embed-resource = "3.0.11"
|
||||||
@@ -276,11 +276,19 @@ fn adopt_settings(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `graphics.json`, read before the window opens.
|
||||||
|
fn graphics() -> Option<serde_json::Value> {
|
||||||
|
graphics_path().and_then(|p| std::fs::read_to_string(p).ok()).and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The graphics backend preference (`graphics.json`'s `backend`: `auto`, `dx12`, `vulkan`, `gl`).
|
||||||
|
fn backend_preference() -> Option<String> {
|
||||||
|
graphics().and_then(|v| v.get("backend").and_then(serde_json::Value::as_str).map(str::to_string))
|
||||||
|
}
|
||||||
|
|
||||||
/// Anti-aliasing samples from Preferences (4 when unset).
|
/// Anti-aliasing samples from Preferences (4 when unset).
|
||||||
fn msaa() -> u16 {
|
fn msaa() -> u16 {
|
||||||
graphics_path()
|
graphics()
|
||||||
.and_then(|p| std::fs::read_to_string(p).ok())
|
|
||||||
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
|
||||||
.and_then(|v| v.get("msaa").and_then(serde_json::Value::as_u64))
|
.and_then(|v| v.get("msaa").and_then(serde_json::Value::as_u64))
|
||||||
.and_then(|m| u16::try_from(m).ok())
|
.and_then(|m| u16::try_from(m).ok())
|
||||||
.filter(|m| [1, 2, 4, 8].contains(m))
|
.filter(|m| [1, 2, 4, 8].contains(m))
|
||||||
@@ -399,7 +407,7 @@ fn main() -> eframe::Result {
|
|||||||
settings.as_ref().map(|d| d.join("app.ron"))
|
settings.as_ref().map(|d| d.join("app.ron"))
|
||||||
},
|
},
|
||||||
persist_window: !safe_mode,
|
persist_window: !safe_mode,
|
||||||
wgpu_options: wgpu_options(cpu_renderer),
|
wgpu_options: wgpu_options(cpu_renderer, if safe_mode { None } else { backend_preference() }.as_deref()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
#[cfg(all(unix, not(target_os = "macos")))]
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
@@ -504,11 +512,20 @@ fn main() -> eframe::Result {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The renderer settings: with `cpu`, the software adapter (WARP, lavapipe, llvmpipe).
|
/// The renderer settings: with `cpu`, the software adapter (WARP, lavapipe, llvmpipe). The
|
||||||
fn wgpu_options(cpu: bool) -> eframe::egui_wgpu::WgpuConfiguration {
|
/// backends come from `startup::choose_backends` (on Windows D3D12 first, and only the backend
|
||||||
|
/// in use is started); `preference` is `graphics.json`'s `backend`.
|
||||||
|
fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuConfiguration {
|
||||||
let mut options = eframe::egui_wgpu::WgpuConfiguration::default();
|
let mut options = eframe::egui_wgpu::WgpuConfiguration::default();
|
||||||
if cpu && let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut options.wgpu_setup {
|
if let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut options.wgpu_setup {
|
||||||
setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter));
|
let env_set = std::env::var(startup::WGPU_BACKEND_ENV).is_ok_and(|v| !v.trim().is_empty());
|
||||||
|
if let Some(backends) = startup::choose_backends(cfg!(target_os = "windows"), env_set, cpu, preference, startup::has_hardware_adapter) {
|
||||||
|
eprintln!("SolidCraft graphics: starting {backends:?}");
|
||||||
|
setup.instance_descriptor.backends = backends;
|
||||||
|
}
|
||||||
|
if cpu {
|
||||||
|
setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
options
|
options
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,68 @@ pub fn pick_cpu_adapter(adapters: &[eframe::wgpu::Adapter], surface: Option<&efr
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `WGPU_BACKEND`: when set, wgpu picks the backends from it and the order below is not used.
|
||||||
|
pub const WGPU_BACKEND_ENV: &str = "WGPU_BACKEND";
|
||||||
|
|
||||||
|
/// The graphics backends to start with, or `None` to leave it to wgpu (`WGPU_BACKEND`, or all
|
||||||
|
/// the backends it was built with).
|
||||||
|
///
|
||||||
|
/// On Windows, D3D12 comes first, then Vulkan, then OpenGL, and only the first one with a
|
||||||
|
/// hardware adapter is started at all. wgpu would otherwise start Vulkan, D3D12 and OpenGL
|
||||||
|
/// together and prefer Vulkan, and starting Vulkan loads every installed Vulkan driver: Intel's
|
||||||
|
/// (`igvk64.dll`) crashed SolidCraft at startup that way, inside the driver, before anything
|
||||||
|
/// could be reported. D3D12 is Windows' own graphics API, the one its drivers are tested with.
|
||||||
|
/// `has_hardware(backends)` says whether those backends have an adapter that isn't software
|
||||||
|
/// (it is only asked about the backends being tried, one at a time).
|
||||||
|
///
|
||||||
|
/// - `WGPU_BACKEND` set (`env_set`): `None`, whatever it says wins.
|
||||||
|
/// - The software renderer (`cpu`, safe mode): D3D12 alone on Windows (WARP); `None` elsewhere.
|
||||||
|
/// - `preference` (`graphics.json`'s `backend`: `dx12`, `vulkan`, `gl` or a comma list; `auto`
|
||||||
|
/// or empty for the order above): those backends, on any system.
|
||||||
|
/// - Elsewhere than Windows: `None`.
|
||||||
|
/// - No hardware adapter at all: D3D12 (WARP, Windows' software renderer).
|
||||||
|
pub fn choose_backends(
|
||||||
|
windows: bool,
|
||||||
|
env_set: bool,
|
||||||
|
cpu: bool,
|
||||||
|
preference: Option<&str>,
|
||||||
|
mut has_hardware: impl FnMut(eframe::wgpu::Backends) -> bool,
|
||||||
|
) -> Option<eframe::wgpu::Backends> {
|
||||||
|
use eframe::wgpu::Backends;
|
||||||
|
if env_set {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if cpu {
|
||||||
|
return windows.then_some(Backends::DX12);
|
||||||
|
}
|
||||||
|
if let Some(p) = preference.map(str::trim).filter(|p| !p.is_empty() && !p.eq_ignore_ascii_case("auto")) {
|
||||||
|
let chosen = Backends::from_comma_list(p);
|
||||||
|
if !chosen.is_empty() {
|
||||||
|
return Some(chosen);
|
||||||
|
}
|
||||||
|
log::warn!("graphics: unknown backend preference `{p}`; using the default order");
|
||||||
|
}
|
||||||
|
if !windows {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(WINDOWS_ORDER.into_iter().find(|b| has_hardware(*b)).unwrap_or(Backends::DX12))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The order backends are tried in on Windows.
|
||||||
|
pub const WINDOWS_ORDER: [eframe::wgpu::Backends; 3] = [eframe::wgpu::Backends::DX12, eframe::wgpu::Backends::VULKAN, eframe::wgpu::Backends::GL];
|
||||||
|
|
||||||
|
/// Whether `backends` (started on their own, nothing else) have a hardware adapter.
|
||||||
|
pub fn has_hardware_adapter(backends: eframe::wgpu::Backends) -> bool {
|
||||||
|
let mut desc = eframe::wgpu::InstanceDescriptor::new_without_display_handle();
|
||||||
|
desc.backends = backends;
|
||||||
|
desc.flags = eframe::wgpu::InstanceFlags::from_build_config().with_env();
|
||||||
|
let instance = eframe::wgpu::Instance::new(desc);
|
||||||
|
let adapters = pollster::block_on(instance.enumerate_adapters(backends));
|
||||||
|
let found: Vec<String> = adapters.iter().map(|a| format!("{} ({:?})", a.get_info().name, a.get_info().device_type)).collect();
|
||||||
|
log::info!("graphics: {backends:?} adapters: {found:?}");
|
||||||
|
adapters.iter().any(|a| a.get_info().device_type != eframe::wgpu::DeviceType::Cpu)
|
||||||
|
}
|
||||||
|
|
||||||
/// The message shown when no window could be opened at all.
|
/// The message shown when no window could be opened at all.
|
||||||
pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
|
pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
|
||||||
let folder = settings.map(|d| d.display().to_string()).unwrap_or_else(|| "the SolidCraft settings folder".into());
|
let folder = settings.map(|d| d.display().to_string()).unwrap_or_else(|| "the SolidCraft settings folder".into());
|
||||||
@@ -182,6 +244,65 @@ mod tests {
|
|||||||
let _ = std::fs::remove_dir_all(&d);
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn windows_starts_d3d12_first_and_vulkan_only_when_d3d12_has_no_gpu() {
|
||||||
|
use eframe::wgpu::Backends;
|
||||||
|
let probe = |with: &'static [Backends]| {
|
||||||
|
move |b: Backends| {
|
||||||
|
assert!(b == Backends::DX12 || b == Backends::VULKAN || b == Backends::GL, "one backend at a time: {b:?}");
|
||||||
|
with.contains(&b)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// D3D12 works: Vulkan and OpenGL are never even asked about (starting Vulkan loads
|
||||||
|
// its drivers, which is what crashed).
|
||||||
|
let mut asked = Vec::new();
|
||||||
|
let chosen = choose_backends(true, false, false, None, |b| {
|
||||||
|
asked.push(b);
|
||||||
|
b == Backends::DX12
|
||||||
|
});
|
||||||
|
assert_eq!(chosen, Some(Backends::DX12));
|
||||||
|
assert_eq!(asked, [Backends::DX12]);
|
||||||
|
// Everything works: still D3D12 alone.
|
||||||
|
assert_eq!(choose_backends(true, false, false, None, probe(&[Backends::DX12, Backends::VULKAN, Backends::GL])), Some(Backends::DX12));
|
||||||
|
// No D3D12 hardware adapter: Vulkan, else OpenGL, else D3D12's software adapter (WARP).
|
||||||
|
assert_eq!(choose_backends(true, false, false, None, probe(&[Backends::VULKAN, Backends::GL])), Some(Backends::VULKAN));
|
||||||
|
assert_eq!(choose_backends(true, false, false, None, probe(&[Backends::GL])), Some(Backends::GL));
|
||||||
|
assert_eq!(choose_backends(true, false, false, None, probe(&[])), Some(Backends::DX12));
|
||||||
|
// `auto` and blanks are the default order.
|
||||||
|
assert_eq!(choose_backends(true, false, false, Some(" auto "), probe(&[Backends::DX12])), Some(Backends::DX12));
|
||||||
|
assert_eq!(choose_backends(true, false, false, Some(""), probe(&[Backends::GL])), Some(Backends::GL));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wgpu_backend_the_preference_and_the_software_renderer_override_the_order() {
|
||||||
|
use eframe::wgpu::Backends;
|
||||||
|
let never = |b: Backends| -> bool { panic!("no probing expected, asked about {b:?}") };
|
||||||
|
// WGPU_BACKEND: wgpu reads it itself, nothing is probed.
|
||||||
|
assert_eq!(choose_backends(true, true, false, Some("gl"), never), None);
|
||||||
|
assert_eq!(choose_backends(true, true, true, None, never), None);
|
||||||
|
// The software renderer (safe mode, the retry): WARP through D3D12 alone, never Vulkan.
|
||||||
|
assert_eq!(choose_backends(true, false, true, Some("vulkan"), never), Some(Backends::DX12));
|
||||||
|
assert_eq!(choose_backends(false, false, true, None, never), None);
|
||||||
|
// The preference, on any system, without probing.
|
||||||
|
assert_eq!(choose_backends(true, false, false, Some("vulkan"), never), Some(Backends::VULKAN));
|
||||||
|
assert_eq!(choose_backends(true, false, false, Some("GL"), never), Some(Backends::GL));
|
||||||
|
assert_eq!(choose_backends(true, false, false, Some("dx12,vulkan"), never), Some(Backends::DX12 | Backends::VULKAN));
|
||||||
|
assert_eq!(choose_backends(false, false, false, Some("vulkan"), never), Some(Backends::VULKAN));
|
||||||
|
// An unknown preference is the default order.
|
||||||
|
assert_eq!(choose_backends(true, false, false, Some("glide"), |b| b == Backends::DX12), Some(Backends::DX12));
|
||||||
|
// Elsewhere: wgpu's own choice.
|
||||||
|
assert_eq!(choose_backends(false, false, false, None, never), None);
|
||||||
|
assert_eq!(choose_backends(false, false, false, Some("auto"), never), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn probing_a_backend_alone_never_panics() {
|
||||||
|
// Whatever this machine has (often nothing): an answer, not a crash.
|
||||||
|
for b in WINDOWS_ORDER {
|
||||||
|
let _ = has_hardware_adapter(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_message_names_the_settings_folder_and_safe_mode() {
|
fn the_message_names_the_settings_folder_and_safe_mode() {
|
||||||
let m = no_window_message("no adapter", Some(Path::new("/x/SolidCraft")));
|
let m = no_window_message("no adapter", Some(Path::new("/x/SolidCraft")));
|
||||||
|
|||||||
@@ -629,7 +629,14 @@ pub fn show_mm_bare(app: &SolveApp, v: f64) -> String {
|
|||||||
/// change while it is open).
|
/// change while it is open).
|
||||||
fn save_graphics(app: &SolveApp) {
|
fn save_graphics(app: &SolveApp) {
|
||||||
if let Some(path) = &app.services.graphics_path {
|
if let Some(path) = &app.services.graphics_path {
|
||||||
let _ = std::fs::write(path, json!({ "msaa": app.preferences.msaa }).to_string());
|
// Other settings in the file (the graphics `backend`) stay as they are.
|
||||||
|
let mut graphics = std::fs::read_to_string(path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||||
|
.filter(serde_json::Value::is_object)
|
||||||
|
.unwrap_or_else(|| json!({}));
|
||||||
|
graphics["msaa"] = json!(app.preferences.msaa);
|
||||||
|
let _ = std::fs::write(path, graphics.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1215,10 +1222,12 @@ mod tests {
|
|||||||
// WebGPU's guarantee for the surface format: 8x saved, the device can only do 1x and 4x.
|
// WebGPU's guarantee for the surface format: 8x saved, the device can only do 1x and 4x.
|
||||||
app.msaa_supported = vec![1, 4];
|
app.msaa_supported = vec![1, 4];
|
||||||
app.preferences.msaa = 8;
|
app.preferences.msaa = 8;
|
||||||
|
std::fs::write(&path, r#"{"msaa":8,"backend":"vulkan"}"#).unwrap();
|
||||||
check_msaa(&mut app, 8);
|
check_msaa(&mut app, 8);
|
||||||
assert_eq!(app.preferences.msaa, 4);
|
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();
|
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!(saved["msaa"], 4);
|
||||||
|
assert_eq!(saved["backend"], "vulkan", "the backend preference is kept");
|
||||||
assert_eq!(app.settings_reset, [SettingReset::Msaa { requested: 8, reset: 4, supported: vec![1, 4] }]);
|
assert_eq!(app.settings_reset, [SettingReset::Msaa { requested: 8, reset: 4, supported: vec![1, 4] }]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.settings_reset[0].message(),
|
app.settings_reset[0].message(),
|
||||||
|
|||||||
@@ -62,6 +62,28 @@ saying what failed (earlier versions closed without a word). Things to try:
|
|||||||
- Run `solidcraft --crash-log` to list recent crash reports, and attach the newest one to an
|
- Run `solidcraft --crash-log` to list recent crash reports, and attach the newest one to an
|
||||||
issue.
|
issue.
|
||||||
|
|
||||||
|
## SolidCraft closes right away on Windows
|
||||||
|
|
||||||
|
A graphics driver can crash the app while it starts, before SolidCraft can show anything (an
|
||||||
|
old Intel Vulkan driver, `igvk64.dll`, did). Windows' Event Viewer names the driver under
|
||||||
|
*Windows Logs › Application* ("Faulting module name").
|
||||||
|
|
||||||
|
From 0.11.1, SolidCraft on Windows starts Direct3D 12 first and loads no other graphics API
|
||||||
|
when Direct3D 12 has a graphics card; Vulkan and then OpenGL are tried only on a computer
|
||||||
|
without one. Earlier versions started Vulkan, Direct3D 12 and OpenGL together and preferred
|
||||||
|
Vulkan. To choose the graphics API yourself, in any version, open a Command Prompt and start
|
||||||
|
SolidCraft from it:
|
||||||
|
|
||||||
|
```
|
||||||
|
set WGPU_BACKEND=dx12
|
||||||
|
"C:\Program Files\SolidCraft\solidcraft.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
`vulkan` and `gl` are the other choices. `setx WGPU_BACKEND dx12` keeps it for apps started
|
||||||
|
from the Start menu after you sign in again. From 0.11.1 the setting can also live in
|
||||||
|
`graphics.json` in the settings folder, as `{"msaa": 4, "backend": "dx12"}` (`auto` is the
|
||||||
|
order above). Safe mode ignores it and always uses the software renderer.
|
||||||
|
|
||||||
## A design saved by 0.10 won't open in an older version
|
## A design saved by 0.10 won't open in an older version
|
||||||
|
|
||||||
Designs saved by a newer SolidCraft use a newer file format. Older versions refuse them with
|
Designs saved by a newer SolidCraft use a newer file format. Older versions refuse them with
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Start the Windows app under different graphics settings and check each one gets its window up,
|
||||||
|
# draws a frame and quits cleanly, with no crash report. The CI runner has no GPU: its D3D12 adapter
|
||||||
|
# is WARP, so this exercises the backend order's fallback and the D3D12 path end to end.
|
||||||
|
#
|
||||||
|
# Usage: tools/windows-startup-smoke.ps1 -Exe target\ci\solidcraft.exe
|
||||||
|
param([Parameter(Mandatory = $true)][string]$Exe)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$cases = @(
|
||||||
|
@{ Name = 'default order'; Graphics = $null; Env = @{}; Args = @(); Expect = 'graphics: Dx12' },
|
||||||
|
@{ Name = 'saved 8x anti-aliasing'; Graphics = '{"msaa":8}'; Env = @{}; Args = @(); Expect = '(requested 8x)' },
|
||||||
|
@{ Name = 'saved 2x, backend preference dx12'; Graphics = '{"msaa":2,"backend":"dx12"}'; Env = @{}; Args = @(); Expect = '(requested 2x)' },
|
||||||
|
@{ Name = 'damaged graphics.json'; Graphics = '{"msaa":'; Env = @{}; Args = @(); Expect = 'graphics: Dx12' },
|
||||||
|
@{ Name = 'WGPU_BACKEND=dx12 with FXC'; Graphics = $null; Env = @{ WGPU_BACKEND = 'dx12'; WGPU_DX12_COMPILER = 'fxc' }; Args = @(); Expect = 'graphics: Dx12' },
|
||||||
|
@{ Name = 'safe mode'; Graphics = '{"msaa":8,"backend":"vulkan"}'; Env = @{}; Args = @('--safe-mode'); Expect = 'graphics: Dx12' }
|
||||||
|
)
|
||||||
|
|
||||||
|
$failed = @()
|
||||||
|
$port = 47811
|
||||||
|
foreach ($case in $cases) {
|
||||||
|
$port += 1
|
||||||
|
$root = Join-Path $env:RUNNER_TEMP ("smoke-" + $port)
|
||||||
|
Remove-Item -Recurse -Force $root -ErrorAction SilentlyContinue
|
||||||
|
# eframe asks Windows for the roaming folder (not %APPDATA%), so the settings go in the
|
||||||
|
# runner's real one, emptied for each case.
|
||||||
|
$settings = Join-Path ([Environment]::GetFolderPath('ApplicationData')) 'SolidCraft\data'
|
||||||
|
Remove-Item -Recurse -Force $settings -ErrorAction SilentlyContinue
|
||||||
|
$local = Join-Path $root 'local'
|
||||||
|
$crashes = Join-Path $root 'crashes'
|
||||||
|
New-Item -ItemType Directory -Force $settings, $local, $crashes | Out-Null
|
||||||
|
if ($case.Graphics) { Set-Content -NoNewline -Path (Join-Path $settings 'graphics.json') -Value $case.Graphics }
|
||||||
|
$token = -join ((1..64) | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 16) })
|
||||||
|
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = (Resolve-Path $Exe).Path
|
||||||
|
foreach ($a in (@('--sample', '--control', "$port") + $case.Args)) { [void]$psi.ArgumentList.Add($a) }
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.Environment['LOCALAPPDATA'] = $local
|
||||||
|
$psi.Environment['SOLIDCRAFT_CRASH_DIR'] = $crashes
|
||||||
|
$psi.Environment['SOLIDCRAFT_CONTROL_TOKEN'] = $token
|
||||||
|
$psi.Environment.Remove('WGPU_BACKEND') | Out-Null
|
||||||
|
$psi.Environment.Remove('WGPU_DX12_COMPILER') | Out-Null
|
||||||
|
foreach ($k in $case.Env.Keys) { $psi.Environment[$k] = $case.Env[$k] }
|
||||||
|
Write-Host "=== $($case.Name)"
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$stderr = $p.StandardError.ReadToEndAsync()
|
||||||
|
$stdout = $p.StandardOutput.ReadToEndAsync()
|
||||||
|
|
||||||
|
$ok = $false
|
||||||
|
$why = ''
|
||||||
|
try {
|
||||||
|
$client = $null
|
||||||
|
$deadline = (Get-Date).AddSeconds(180)
|
||||||
|
while (-not $client -and (Get-Date) -lt $deadline -and -not $p.HasExited) {
|
||||||
|
try { $client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $port) } catch { Start-Sleep -Milliseconds 500 }
|
||||||
|
}
|
||||||
|
if (-not $client) { throw "no control channel (exited: $($p.HasExited))" }
|
||||||
|
$client.ReceiveTimeout = 120000
|
||||||
|
$stream = $client.GetStream()
|
||||||
|
$writer = New-Object System.IO.StreamWriter($stream)
|
||||||
|
$writer.AutoFlush = $true
|
||||||
|
$reader = New-Object System.IO.StreamReader($stream)
|
||||||
|
$shot = Join-Path $root 'shot.png'
|
||||||
|
$writer.WriteLine((@{ id = 1; token = $token; method = 'ui.screenshot'; params = @{ path = $shot } } | ConvertTo-Json -Compress))
|
||||||
|
$reply = $reader.ReadLine()
|
||||||
|
Write-Host "screenshot: $reply"
|
||||||
|
if (-not ($reply -like '*"ok":true*') -or -not (Test-Path $shot)) { throw "no frame drawn: $reply" }
|
||||||
|
$writer.WriteLine((@{ id = 2; method = 'app.quit'; params = @{ force = $true } } | ConvertTo-Json -Compress))
|
||||||
|
Write-Host "quit: $($reader.ReadLine())"
|
||||||
|
$client.Close()
|
||||||
|
if (-not $p.WaitForExit(60000)) { throw 'did not exit after app.quit' }
|
||||||
|
if ($p.ExitCode -ne 0) { throw ('exit code 0x{0:x}' -f $p.ExitCode) }
|
||||||
|
$ok = $true
|
||||||
|
} catch {
|
||||||
|
$why = $_.Exception.Message
|
||||||
|
} finally {
|
||||||
|
if (-not $p.HasExited) { $p.Kill(); $p.WaitForExit() }
|
||||||
|
}
|
||||||
|
$err = $stderr.Result
|
||||||
|
Write-Host $err
|
||||||
|
if ($ok -and -not $err.Contains($case.Expect)) { $ok = $false; $why = "stderr does not say '$($case.Expect)'" }
|
||||||
|
$reports = @(Get-ChildItem -Recurse -Filter report.json $crashes -ErrorAction SilentlyContinue)
|
||||||
|
if ($reports.Count -gt 0) {
|
||||||
|
foreach ($r in $reports) { Get-Content $r.FullName | Write-Host }
|
||||||
|
if ($ok) { $ok = $false; $why = 'a crash report was written' }
|
||||||
|
}
|
||||||
|
if ($ok) { Write-Host "PASS: $($case.Name)" } else { Write-Host "FAIL: $($case.Name): $why"; $failed += $case.Name }
|
||||||
|
}
|
||||||
|
if ($failed.Count -gt 0) { Write-Error ("failed: " + ($failed -join ', ')) }
|
||||||
Reference in new issue
Block a user