forked from 1b-agent/opencadstudio-upstream
Older GPUs without usable DirectX 12 Feature Level 12_0 or working Vulkan support (e.g. legacy Intel iGPUs and other old parts), and old GLES drivers whose shader compiler rejects iced's own shaders, crash at startup or on the first drawing viewport: StagingBelt aborts in Buffer::get_mapped_range (panic=abort, silent exit) on the DX12 path, and shader compilation fails inside iced's gradient pipeline on the GLES path. Selecting a working backend via WGPU_BACKEND before iced boots avoids both classes. - Probe each candidate backend (Windows: dx12,vulkan,gl; Linux: vulkan,gl) in an isolated --gpu-probe child: device request, staging-style mapped upload, render pipeline creation (its fragment shader reads a private vec4 array, catching old-Mesa compile failures), offscreen render and readback. First fully-passing backend wins; explicit --backend / WGPU_BACKEND / --safe-mode always win, and Windows keeps preferring DX12/Vulkan (#55). - Software adapters (WARP/llvmpipe/SwiftShader) are the last resort before a best-effort GL attempt: slow but alive. - The crash sentinel names the attempted backend so only implicated backends are skipped next launch. - Winning probes report adapter limits; GPUs without shader storage buffers get the packed compatibility renderer automatically, with a startup command-line notice so the switch is never silent.
197 lines
8.8 KiB
Rust
197 lines
8.8 KiB
Rust
#![allow(non_snake_case)]
|
|
// On Windows release builds, hide the console window the OS would
|
|
// otherwise spawn alongside the GUI. Debug builds keep stdout/stderr
|
|
// attached so eprintln! / panics stay visible while developing.
|
|
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
|
|
|
|
use OpenCADStudio::app;
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use OpenCADStudio::{cli, io, mcp};
|
|
#[cfg(target_arch = "wasm32")]
|
|
use OpenCADStudio::sys;
|
|
|
|
fn main() -> iced::Result {
|
|
// Web (wasm) uses the single-window entry; native uses the multi-window
|
|
// daemon. Trunk calls `main` from its generated JS bootstrap. The web build
|
|
// takes no CLI args, so it skips parsing entirely.
|
|
#[cfg(target_arch = "wasm32")]
|
|
{
|
|
console_error_panic_hook::set_once();
|
|
// After the console hook so the chained panic mirror keeps it; also
|
|
// installs the log-facade listener that surfaces wgpu/naga errors as
|
|
// a copyable on-page banner (#414 — an empty canvas otherwise gives
|
|
// reporters nothing to paste).
|
|
sys::web_diag::init();
|
|
return app::run_web();
|
|
}
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
{
|
|
use clap::Parser;
|
|
let args = cli::Cli::parse();
|
|
|
|
// GPU probe child: exercise one backend offscreen, print one JSON
|
|
// line on success and exit 0/1. This must run before any logging/GUI
|
|
// setup; the parent interprets any non-zero exit (including a driver
|
|
// abort) as "this backend is unusable".
|
|
if let Some(which) = &args.gpu_probe {
|
|
OpenCADStudio::gpu_backend::run_probe_child(which);
|
|
}
|
|
|
|
// Plugin runner mode: the host spawns itself with this hidden flag to
|
|
// load a plugin cdylib in an isolated process. Hand off immediately so
|
|
// the child never touches GUI state.
|
|
if let Some(runner_args) = &args.ocs_plugin_runner {
|
|
if runner_args.len() != 2 {
|
|
eprintln!("--ocs-plugin-runner expects <socket> <cdylib>");
|
|
std::process::exit(1);
|
|
}
|
|
let socket = &runner_args[0];
|
|
let cdylib = std::path::Path::new(&runner_args[1]);
|
|
if let Err(e) = ocs_plugin_api::runner::run(socket, cdylib) {
|
|
eprintln!("[runner] fatal: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
// Thumbnail mode: the OS file-manager thumbnailer invokes us as
|
|
// `--dwg-thumbnail <in> <out> <size>`. Extract the DWG's embedded
|
|
// preview to a PNG and exit — never touch the GUI.
|
|
if let Some(a) = &args.dwg_thumbnail {
|
|
let size = a.get(2).and_then(|s| s.parse().ok()).unwrap_or(256);
|
|
let ok = io::thumbnail::extract_to_png(
|
|
std::path::Path::new(&a[0]),
|
|
std::path::Path::new(&a[1]),
|
|
size,
|
|
);
|
|
std::process::exit(if ok { 0 } else { 1 });
|
|
}
|
|
|
|
// MCP is a client-neutral local entry point. It uses only stdin,
|
|
// stdout and the authenticated GUI bridge, so it must run before any
|
|
// logging or graphics setup can write to the protocol stream.
|
|
if args.mcp {
|
|
mcp::run();
|
|
return Ok(());
|
|
}
|
|
|
|
// Opt-in logging. `--log LEVEL` seeds RUST_LOG; the subscriber then
|
|
// surfaces wgpu / iced / winit diagnostics that are otherwise silent.
|
|
if let Some(level) = &args.log {
|
|
std::env::set_var("RUST_LOG", level);
|
|
}
|
|
if std::env::var_os("RUST_LOG").is_some() {
|
|
let _ = env_logger::try_init();
|
|
}
|
|
|
|
// GPU backend selection. Explicit `--backend` wins; `--safe-mode`
|
|
// forces GL for flaky drivers. On Windows the preference order starts
|
|
// with DX12/Vulkan so the AMD OpenGL ICD (atio6axx.dll) is never
|
|
// touched at startup — it access-violates on some hybrid-GPU laptops
|
|
// before any window appears (#55). An already-set WGPU_BACKEND always
|
|
// wins.
|
|
//
|
|
// Older GPUs without usable DirectX 12 Feature Level 12_0 or working
|
|
// Vulkan (e.g. legacy Intel iGPUs), and old GLES drivers whose shader
|
|
// compiler rejects iced's own shaders, crash at startup or on the
|
|
// first drawing viewport instead. `resolve_gpu` probes each candidate
|
|
// backend in an isolated child process and selects the first one that
|
|
// survives a real offscreen render; a crash sentinel skips backends
|
|
// that died with the previous run.
|
|
let gpu = OpenCADStudio::gpu_backend::resolve_gpu(args.backend.as_deref(), args.safe_mode);
|
|
|
|
// Headless modes exit without ever creating a window.
|
|
if args.serve {
|
|
// `app::serve` reads --port itself from the raw args.
|
|
app::serve();
|
|
return Ok(());
|
|
}
|
|
if let Some(io) = &args.export {
|
|
// clap enforces exactly two values for --export.
|
|
let code = app::export_headless(&io[0], &io[1]);
|
|
std::process::exit(code);
|
|
}
|
|
|
|
// Single instance: a double-clicked drawing belongs as a tab in the
|
|
// editor that is already open, not in a second copy of the app.
|
|
//
|
|
// The gate is POSITIONAL, and that is the point: every headless mode
|
|
// has already returned above — the plugin runner, which is this same
|
|
// binary re-spawning itself, most of all. A flag list here would rot
|
|
// the first time a mode is added; a position cannot.
|
|
if !args.new_instance {
|
|
if let io::single_instance::Claim::Existing(stream) = io::single_instance::claim() {
|
|
// Only bare files forward. `--read-only` / `--script` / `--new`
|
|
// configure the whole editor rather than a tab, so they always
|
|
// get a process of their own.
|
|
let plain_open =
|
|
!args.read_only && args.script.is_none() && !args.new && !args.files.is_empty();
|
|
if plain_open && io::single_instance::handoff(stream, &args.files) {
|
|
return Ok(());
|
|
}
|
|
// Nothing to forward, or the far end never acknowledged: fall
|
|
// through and boot our own window. We hold no listener, so the
|
|
// editor that owns the port keeps serving.
|
|
}
|
|
}
|
|
|
|
// GUI: stash the startup config for `app::boot` to pick up.
|
|
let script_lines = args
|
|
.script
|
|
.as_ref()
|
|
.map(|p| match std::fs::read_to_string(p) {
|
|
Ok(text) => text
|
|
.lines()
|
|
.map(str::trim)
|
|
// Blank script rows are significant: they submit Enter to
|
|
// the active command (for example, accepting a preview).
|
|
.filter(|l| !l.starts_with('#') && !l.starts_with(';'))
|
|
.map(str::to_string)
|
|
.collect(),
|
|
Err(e) => {
|
|
eprintln!("--script: cannot read {}: {e}", p.display());
|
|
Vec::new()
|
|
}
|
|
})
|
|
.unwrap_or_default();
|
|
let gpu_fallback_notice = gpu
|
|
.reason
|
|
.map(|reason| OpenCADStudio::gpu_backend::fallback_notice(&reason));
|
|
// The probe enables the packed renderer automatically on GPUs without
|
|
// shader storage buffers; an explicit `--compat-renderer` does the
|
|
// same without the notice.
|
|
let compat_renderer = args.compat_renderer || gpu.compat_renderer;
|
|
let gpu_compat_auto = !args.compat_renderer && gpu.compat_renderer;
|
|
let _ = cli::GUI_CONFIG.set(cli::GuiConfig {
|
|
files: if args.new { Vec::new() } else { args.files },
|
|
new: args.new,
|
|
read_only: args.read_only,
|
|
compat_renderer,
|
|
script_lines,
|
|
gpu_fallback_notice,
|
|
gpu_compat_auto,
|
|
});
|
|
|
|
// Register (or refresh) the freedesktop DWG thumbnailer so file managers
|
|
// show OCS-embedded previews. Idempotent, best-effort, no consent step —
|
|
// it only points a `.thumbnailer` at this same binary's `--dwg-thumbnail`
|
|
// mode. Silently ignored on failure or non-Linux.
|
|
io::file_association::install_thumbnailer();
|
|
|
|
// Crash sentinel: armed while the GPU is live, naming the attempted
|
|
// backend. A stale file at the next launch means this run aborted, so
|
|
// the resolver skips those backends instead of crashing again.
|
|
// Inactive on macOS/wasm, where the resolver leaves the env alone.
|
|
if OpenCADStudio::gpu_backend::gpu_guard_active() {
|
|
OpenCADStudio::gpu_backend::arm_sentinel(
|
|
gpu.backend_value.as_deref().unwrap_or("auto"),
|
|
);
|
|
let result = app::run();
|
|
OpenCADStudio::gpu_backend::disarm_sentinel();
|
|
result
|
|
} else {
|
|
app::run()
|
|
}
|
|
}
|
|
}
|