From 5583bc166fe04f2f89b1ba8b32cc9d3b9535bc8a Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Sat, 1 Aug 2026 14:13:36 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(compositor):=20rendre=20le=20repli=20CP?= =?UTF-8?q?U=20Linux=20for=C3=A7able,=20test=C3=A9=20et=20garanti?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le repli logiciel existait déjà sur Linux, mais par accident : `create_backend` ignorait son paramètre et wgpu rendait lavapipe de lui-même quand c'était le seul ICD. Ça marche — et c'est précisément le problème, parce que rien ne pouvait l'exercer, rien ne le signalait et rien ne le garantissait. - `create_backend` honore enfin son paramètre. `Backend::Cpu` passe par `force_fallback_adapter` ; `Backend::Hardware` rejette explicitement un adaptateur logiciel, ce qui rend `create` réellement matériel strict (un golden mesuré sur llvmpipe passait jusqu'ici pour une mesure GPU). `create_auto` gagne le repli explicite Hardware -> Cpu, comme côté Windows. - `OPENSCREEN_COMPOSITOR_BACKEND=hardware|cpu` force le choix. `VK_DRIVER_FILES` ferait la même chose au niveau du loader Vulkan, mais s'applique au processus entier : sous Electron il prive aussi Chromium de son GPU, qui rastérise alors toute son UI sur CPU et sature la machine. Le chemin CPU n'était donc pas testable sans casser la session. Même motif que `OPENSCREEN_EXPORT_ENCODER`. - L'adaptateur retenu est journalisé. Windows loggue son repli, Linux ne loggait rien : un hôte tombé sur lavapipe rendait à quelques fps sans que rien ne permette de l'établir à distance. - `diagnose()` sépare « aucun ICD Vulkan installé » du reste et nomme le paquet à installer. C'est la seule panne de cette famille que l'utilisateur peut réparer lui-même, et elle s'affichait en « Aperçu indisponible sur cette machine ». - `classify` s'appuie sur `DeviceType::Cpu` — ce que l'ICD déclare — plutôt que sur une sous-chaîne du nom ; le nom reste en filet. - Le .deb et le pacman déclarent Mesa (`mesa-vulkan-drivers` / `vulkan-swrast`). Aucune dépendance n'était déclarée, donc rien ne garantissait qu'un ICD existe. - Nouveau job CI `Rust test (Linux compositor)`. Le Rust Linux n'était compilé nulle part : la CI couvrait macOS et Windows, et les 2154 lignes du moteur wgpu ne passaient que par le poste des contributeurs. Le job installe lavapipe, donc il exerce pour de vrai le backend CPU sur un runner sans GPU. Deux corrections que ce job a révélées, et sans lesquelles il ne peut pas exister : - `export_timing.rs` et `output_geometry_golden.rs` ne compilaient pas sous Linux (`probe_frame_count` / `readback_resized` n'existent que côté Windows et macOS). Les fichiers de `tests/` étant compilés quelle que soit la plateforme, le crate entier était incompilable en `--tests` sur Linux, en silence. Gardés en `cfg(not(target_os = "linux"))` plutôt qu'en `cfg(windows)`, pour ne pas les retirer du job macOS qui les compile aujourd'hui. - L'export GIF construisait son device avec `Gpu::create` (matériel strict) alors que son propre commentaire dit suivre l'export MP4, qui prend `create_auto`. Un hôte sans GPU exportait donc un MP4 mais pas un GIF — sur le seul chemin où le backend CPU existe précisément pour que l'export aboutisse. Le rendre strict sur Linux sans ce correctif aurait cassé le GIF sur les machines lavapipe qui fonctionnent aujourd'hui. --- .github/workflows/ci.yml | 68 +++++ crates/compositor-view-napi/src/lib.rs | 8 +- crates/compositor/src/d3d_linux.rs | 275 ++++++++++++++++-- crates/compositor/tests/cpu_backend_linux.rs | 92 ++++++ crates/compositor/tests/export_timing.rs | 13 +- .../tests/output_geometry_golden.rs | 6 + electron-builder.json5 | 47 +++ 7 files changed, 484 insertions(+), 25 deletions(-) create mode 100644 crates/compositor/tests/cpu_backend_linux.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d228826d9..a4e75a498 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -190,6 +190,74 @@ jobs: cd crates cargo check -p openscreen-compositor -p compositor-view-napi --all-targets + # Le troisieme cote, et le dernier angle mort : le Rust Linux n'etait compile + # NULLE PART en CI. Les deux jobs ci-dessus couvrent macOS (test) et Windows + # (check) ; `compositor_linux.rs`, `pipeline_linux.rs`, `d3d_linux.rs` et les + # 2154 lignes du moteur wgpu ne passaient que par le poste des contributeurs. + # + # Ce que le trou cachait, trouve en ouvrant ce job : `export_timing.rs` et + # `output_geometry_golden.rs` ne compilaient pas sous Linux — ils appellent + # `probe_frame_count` / `readback_resized`, qui n'existent que cote Windows et + # macOS. Les fichiers de `tests/` etant compiles quelle que soit la plateforme, + # le crate entier etait incompilable en `--tests` sur Linux, en silence. + # + # `cargo test` et pas `check` : `mesa-vulkan-drivers` donne au runner un ICD + # Vulkan logiciel (lavapipe), donc `cpu_backend_linux.rs` exerce POUR DE VRAI le + # backend CPU, qui est la propriete que cette PR ajoute. Un runner GitHub n'ayant + # pas de GPU, c'est meme le seul endroit ou ce chemin est teste sans forcage. + rust-linux-compositor-check: + name: Rust test (Linux compositor) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Pas de ./.github/actions/setup : `fetch-ffmpeg.mjs` n'importe que des + # builtins node, donc `npm ci` serait une minute d'installation pour rien. + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + # libclang-dev, pas libclang1 : bindgen a besoin de libclang pour lire les + # headers ffmpeg, et c'est le paquet -dev qui apporte AUSSI les headers + # built-in de clang. Sans eux bindgen echoue sur `stddef.h file not found`. + # mesa-vulkan-drivers : l'ICD lavapipe. Sans lui le runner n'a aucun + # adaptateur Vulkan et le backend CPU serait intestable. C'est le meme + # paquet que le .deb declare desormais en dependance (electron-builder.json5). + - name: Install libclang and the Mesa Vulkan drivers + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends libclang-dev mesa-vulkan-drivers + - name: Vendor the pinned ffmpeg SDK + run: npm run fetch:ffmpeg:sdk + # `crates/.cargo/config.toml` pose FFMPEG_DIR (arbre win64) et LIBCLANG_PATH + # (chemin Windows) dans un `[env]` GLOBAL — cargo n'a pas de + # `[target..env]`. Les deux valeurs sont donc TOUJOURS renseignees et + # fausses ici ; il faut les surcharger par de vraies variables + # d'environnement, qui gagnent (`force = false` par defaut). + - name: Resolve the toolchain paths + run: | + echo "FFMPEG_DIR=$GITHUB_WORKSPACE/crates/thirdparty/ffmpeg-linux64-lgpl-shared" >> "$GITHUB_ENV" + libclang=$(find /usr/lib/llvm-* -name 'libclang.so' | head -1) + # Echouer ici plutot que de laisser bindgen partir sur le chemin Windows + # et rendre une erreur qui ne designe pas la cause. + test -n "$libclang" || { echo "libclang introuvable apres l'installation"; exit 1; } + echo "LIBCLANG_PATH=$(dirname "$libclang")" >> "$GITHUB_ENV" + - name: cargo test (compositor) + env: + # Les .so ffmpeg vendorises ne sont dans aucun chemin systeme : sans ca + # le binaire de test se lance puis meurt sur `libavformat.so.62`. + LD_LIBRARY_PATH: ${{ github.workspace }}/crates/thirdparty/ffmpeg-linux64-lgpl-shared/lib + # Fait ECHOUER `cpu_backend_linux.rs` s'il n'obtient pas le backend CPU, + # au lieu de le sauter en silence comme sur un poste sans lavapipe. + OPENSCREEN_REQUIRE_CPU_BACKEND: "1" + run: | + cd crates + cargo test -p openscreen-compositor --lib --tests + - name: cargo build (napi addon) + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/crates/thirdparty/ffmpeg-linux64-lgpl-shared/lib + run: | + cd crates + cargo build -p compositor-view-napi --release + semantic-pr: name: Validate PR title (semantic) runs-on: ubuntu-latest diff --git a/crates/compositor-view-napi/src/lib.rs b/crates/compositor-view-napi/src/lib.rs index a68b0fe7a..62c164e0c 100644 --- a/crates/compositor-view-napi/src/lib.rs +++ b/crates/compositor-view-napi/src/lib.rs @@ -554,8 +554,12 @@ impl Task for ExportGifTask { let _previews = PreviewPause::begin(); // Same construction as ExportMultiTask — GIF and MP4 differ only in the - // encoder, so everything up to it is built identically. - let gpu = Gpu::create(false).map_err(|e| Error::from_reason(format!("{e:#}")))?; + // encoder, so everything up to it is built identically. That includes the + // device: `create_auto`, not `create`. `create` is hardware-strict (goldens + // and benches want to fail rather than measure a software rasteriser), so a + // host without a usable GPU could export an MP4 but not a GIF — the one path + // where the CPU backend exists specifically so the export still completes. + let gpu = Gpu::create_auto(false).map_err(|e| Error::from_reason(format!("{e:#}")))?; let mut cfg = config::all().pop().expect("au moins une config"); // C8 cfg.zoom = false; cfg.layout_anim = false; diff --git a/crates/compositor/src/d3d_linux.rs b/crates/compositor/src/d3d_linux.rs index 67a701c18..7c344d831 100644 --- a/crates/compositor/src/d3d_linux.rs +++ b/crates/compositor/src/d3d_linux.rs @@ -13,8 +13,22 @@ //! classe donc en `Backend::Cpu` (le meme repli que WARP cote Windows : notice //! dans la preview, warning a l'export), et un vrai GPU (RADV, dzn, NVK...) en //! `Backend::Hardware`. +//! +//! Ce repli a longtemps ete IMPLICITE : `create_backend` ignorait son parametre et +//! wgpu rendait lavapipe de lui-meme quand c'etait le seul ICD. Ca marche, mais rien +//! ne pouvait l'exercer (pas de forcage), rien ne le signalait (pas de log) et rien +//! ne le garantissait (Mesa n'etait declare dans aucun paquet). Trois consequences, +//! toutes corrigees ici : +//! +//! - `create_backend` honore son parametre -- `Backend::Cpu` passe par +//! `force_fallback_adapter`, `Backend::Hardware` rejette explicitement le +//! logiciel. `create` est donc reellement materiel strict. +//! - l'adaptateur retenu est journalise, comme le repli l'est cote Windows. +//! - `OPENSCREEN_COMPOSITOR_BACKEND=hardware|cpu` force le choix sans passer par +//! `VK_DRIVER_FILES`, qui priverait tout le processus -- Chromium compris -- de +//! son GPU (cf. `FORCE_VAR`). -use anyhow::{Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use std::sync::OnceLock; /// Qui execute le pipeline (symetrie d'API avec `d3d_windows::Backend`). @@ -26,6 +40,17 @@ pub enum Backend { Cpu, } +impl Backend { + /// Le libelle accepte par `OPENSCREEN_COMPOSITOR_BACKEND`, pour que le message + /// d'erreur d'un forcage rate cite la valeur telle qu'on l'ecrit. + fn as_str(self) -> &'static str { + match self { + Backend::Hardware => "hardware", + Backend::Cpu => "cpu", + } + } +} + /// Handle GPU Linux : `wgpu::Device` + `wgpu::Queue` (Arc internes cote wgpu, /// `.clone()` bon marche). Les champs `device`/`context`/`backend`/ /// `feature_level` sont alignes sur `d3d_windows::Gpu` / `d3d_macos::Gpu` pour @@ -48,18 +73,47 @@ pub struct Gpu { static PROBE: OnceLock> = OnceLock::new(); pub fn probe() -> Option { - *PROBE.get_or_init(|| create_backend(Backend::Hardware).ok().map(|g| g.backend)) + // Meme forme que `d3d_windows::Gpu::probe` : on essaie les deux dans l'ordre + // ou la production les prendra. Ne PAS se contenter de `Hardware` -- depuis + // que ce backend est strict (cf. `create_async`), il echoue sur un hote + // lavapipe-seul, et `probe()` y rendrait `None` (= "pas d'addon", qui ne + // declenche aucune notice) au lieu de `Cpu` (= machine degradee, notice). + *PROBE.get_or_init(|| { + // Le forcage vaut aussi ici : sans ca l'UI annoncerait "hardware" pendant que + // `create_auto` rend sur lavapipe, et la notice ne s'afficherait pas. + if let Some(want) = forced_backend() { + return create_backend(want).ok().map(|g| g.backend); + } + for backend in [Backend::Hardware, Backend::Cpu] { + if create_backend(backend).is_ok() { + return Some(backend); + } + } + None + }) } -/// Cree un device wgpu (Vulkan). `_backend` est indicatif : on prend le meilleur -/// adaptateur disponible (HighPerformance) et on reporte son type REEL via -/// `classify` (lavapipe -> `Cpu`, sinon `Hardware`) -- pas de chemin de rendu -/// distinct entre les deux cote Linux, seul le libelle change. -pub fn create_backend(_backend: Backend) -> Result { - pollster::block_on(create_async()) +/// Cree un device wgpu pour le backend DEMANDE. +/// +/// - `Backend::Cpu` -> `force_fallback_adapter`, que le loader Vulkan ne satisfait +/// qu'avec un ICD logiciel. C'est le seul moyen d'atteindre lavapipe sur une +/// machine qui a AUSSI un vrai GPU, donc d'exercer le chemin CPU ailleurs que +/// sur un hote deja casse. +/// - `Backend::Hardware` -> le meilleur adaptateur, PUIS un rejet explicite du +/// logiciel. Sans ce rejet, `create` -- cense etre materiel strict -- rendait un +/// device llvmpipe sans broncher sur un hote sans pilote, et un golden mesure +/// dessus passait pour une mesure GPU. +pub fn create_backend(backend: Backend) -> Result { + pollster::block_on(create_async(backend)).map_err(|err| match backend { + // `Backend::Cpu` ne diagnostique pas : si le rasteriseur logiciel lui-meme + // echoue, il n'y a plus rien derriere a proposer (meme raison que WARP + // cote Windows). + Backend::Cpu => err, + Backend::Hardware => anyhow!("{}", diagnose(&err)), + }) } -async fn create_async() -> Result { +async fn create_async(want: Backend) -> Result { let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { backends: wgpu::Backends::all(), ..Default::default() @@ -67,12 +121,24 @@ async fn create_async() -> Result { let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, + force_fallback_adapter: want == Backend::Cpu, ..Default::default() }) .await - .context("aucun adaptateur graphique compatible")?; + .context(match want { + Backend::Hardware => "aucun adaptateur graphique compatible", + Backend::Cpu => "aucun rasteriseur logiciel Vulkan (lavapipe) sur cet hote", + })?; let info = adapter.get_info(); - let backend = classify(&info); + let got = classify(&info); + // `force_fallback_adapter` garantit le sens `Cpu` ; rien ne garantit l'autre. + if want == Backend::Hardware && got == Backend::Cpu { + bail!( + "backend materiel demande, mais le seul adaptateur Vulkan disponible est le \ + rasteriseur logiciel « {} » -- aucun pilote GPU utilisable sur cet hote", + info.name + ); + } let (device, queue) = adapter .request_device( &wgpu::DeviceDescriptor { @@ -85,34 +151,104 @@ async fn create_async() -> Result { ) .await .context("request_device a echoue")?; + // Windows loggue son repli (`d3d_windows.rs`), Linux ne loggait rien : un hote + // tombe sur lavapipe rendait a quelques fps sans que rien -- ni log, ni rapport + // de bug -- ne permette de l'etablir a distance. + eprintln!( + "[d3d] adaptateur Vulkan : {} ({:?}, {:?}) -> backend {:?}", + info.name, info.device_type, info.backend, got + ); Ok(Gpu { device, context: queue, - backend, + backend: got, feature_level: 0, }) } -/// lavapipe expose "llvmpipe" dans le nom d'adaptateur -- c'est l'equivalent -/// Vulkan de WARP, a ranger sous `Cpu`. +/// `DeviceType::Cpu` d'abord : c'est ce que l'ICD lui-meme declare +/// (`VK_PHYSICAL_DEVICE_TYPE_CPU`), et lavapipe n'est pas le seul rasteriseur +/// logiciel Vulkan -- SwiftShader en est un autre. Le nom ne sert plus que de +/// filet pour un ICD qui mentirait sur son type ; il etait l'unique critere +/// jusqu'ici, on ne le retire pas sans l'avoir vu echouer. fn classify(info: &wgpu::AdapterInfo) -> Backend { + if info.device_type == wgpu::DeviceType::Cpu { + return Backend::Cpu; + } let n = info.name.to_ascii_lowercase(); - if n.contains("llvmpipe") || n.contains("lavapipe") { + if n.contains("llvmpipe") || n.contains("lavapipe") || n.contains("swiftshader") { Backend::Cpu } else { Backend::Hardware } } +/// Forcage explicite du backend : `OPENSCREEN_COMPOSITOR_BACKEND=hardware|cpu`. +/// +/// `VK_DRIVER_FILES` / `VK_ICD_FILENAMES` obtiendraient le meme effet au niveau du +/// loader Vulkan, mais s'appliquent au PROCESSUS ENTIER : sous Electron ils privent +/// aussi Chromium de son GPU, qui rasterise alors toute son UI sur CPU et sature la +/// machine -- le test devient inexploitable et emporte les autres applications. Cette +/// variable-ci ne touche que notre compositeur, ce qui en fait le seul moyen praticable +/// d'exercer le chemin CPU depuis une machine qui a un GPU. +/// +/// Meme motif que `OPENSCREEN_EXPORT_ENCODER` cote pipeline. Linux seulement : Windows +/// a le meme besoin (WARP) mais son chemin n'est pas exerce ici. +pub const FORCE_VAR: &str = "OPENSCREEN_COMPOSITOR_BACKEND"; + +fn forced_backend() -> Option { + let raw = std::env::var(FORCE_VAR).ok()?; + let parsed = parse_forced_backend(&raw); + if parsed.is_none() { + eprintln!("[d3d] {FORCE_VAR}={raw} ignore (attendu : hardware|cpu)"); + } + parsed +} + +/// Separe de `forced_backend` pour etre testable : muter l'environnement depuis un +/// test course avec les autres tests du meme binaire, qui tournent en parallele. +fn parse_forced_backend(raw: &str) -> Option { + match raw.trim().to_ascii_lowercase().as_str() { + "cpu" => Some(Backend::Cpu), + "hardware" => Some(Backend::Hardware), + _ => None, + } +} + impl Gpu { - /// Chemin de production. Symetrie d'API avec `d3d_windows::Gpu::create_auto` ; - /// `_debug` est le pendant de la couche de debug D3D11 (rien a faire ici, - /// wgpu a `WGPU_VALIDATION` en variable d'env). + /// Le device de PRODUCTION : materiel si possible, rasteriseur logiciel sinon. + /// + /// Symetrie d'API avec `d3d_windows::Gpu::create_auto` ; `_debug` est le pendant + /// de la couche de debug D3D11 (rien a faire ici, wgpu a `WGPU_VALIDATION` en + /// variable d'env). + /// + /// Le repli etait implicite jusqu'ici : wgpu rendait lavapipe de lui-meme quand + /// c'etait le seul ICD, ce qui MARCHE mais ne se teste ni ne se loggue. Il est + /// desormais explicite, pour la meme raison que cote Windows. pub fn create_auto(_debug: bool) -> Result { - create_backend(Backend::Hardware) + // Un forcage ne retombe deliberement sur rien : un repli silencieux sur le + // materiel ferait croire au test d'etre passe (meme politique que + // `OPENSCREEN_EXPORT_ENCODER` cote pipeline). + if let Some(want) = forced_backend() { + return create_backend(want).with_context(|| { + format!("{FORCE_VAR}={} inutilisable sur cet hote", want.as_str()) + }); + } + let hw_err = match create_backend(Backend::Hardware) { + Ok(gpu) => return Ok(gpu), + Err(err) => err, + }; + eprintln!("[d3d] backend materiel indisponible ({hw_err:#}) -- repli sur le backend CPU"); + create_backend(Backend::Cpu).map_err(|cpu_err| { + // Le diagnostic MATERIEL en tete : c'est lui qui est actionnable + // ("installez Mesa"), pas "lavapipe indisponible" qui ne dit rien. + anyhow!("{hw_err:#} (le repli logiciel a echoue aussi : {cpu_err:#})") + }) } - /// Creation hardware-strict (tests et goldens). + /// Creation hardware-strict (tests, goldens, bench) : echoue plutot que de rendre + /// un device lavapipe. Mesurer ou comparer le chemin GPU sur un rasteriseur + /// logiciel n'a aucun sens. Le chemin de production, lui, prend `create_auto`. pub fn create(_debug: bool) -> Result { create_backend(Backend::Hardware) } @@ -124,11 +260,39 @@ impl Gpu { } } -/// Message d'echec actionnable (symetrie d'API avec `d3d_windows::diagnose`). +/// Message d'echec ACTIONNABLE (symetrie d'API avec `d3d_windows::diagnose`, qui +/// separe "cet adaptateur n'a pas de decodeur video" de "aucun adaptateur FL 11_1"). +/// +/// La seule panne de cette famille que l'utilisateur peut reparer lui-meme est +/// "aucun ICD Vulkan installe" : ni pilote GPU, ni rasteriseur logiciel, donc meme le +/// repli CPU est hors de portee et la preview s'ouvre sur un echec. On la separe du +/// reste en re-enumerant sans rien exiger -- si meme la aucun adaptateur ne sort, +/// c'est le loader qui est vide, pas notre demande qui etait trop stricte. pub fn diagnose(err: &anyhow::Error) -> String { + if !any_adapter_exists() { + return format!( + "aucun pilote Vulkan sur cet hote ({err:#}). Installez Mesa : \ + `mesa-vulkan-drivers` (Debian/Ubuntu, Fedora) ou `vulkan-swrast` (Arch) \ + donne le rendu logiciel ; le pilote de votre carte (`vulkan-radeon`, \ + `vulkan-intel`, pilote NVIDIA) donne le rendu accelere." + ); + } format!("{err:#}") } +/// Y a-t-il UN adaptateur Vulkan, quel qu'il soit ? Distingue "le loader n'a aucun +/// ICD" de "un adaptateur existe mais la creation a echoue". Volontairement sans +/// cache : `diagnose` n'est appele que sur un chemin d'erreur, jamais en boucle. +fn any_adapter_exists() -> bool { + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::all(), + ..Default::default() + }); + !instance + .enumerate_adapters(wgpu::Backends::all()) + .is_empty() +} + #[cfg(test)] mod tests { use super::*; @@ -160,4 +324,73 @@ mod tests { }; assert_eq!(classify(&info), Backend::Hardware); } + + /// `AdapterInfo` minimal pour les cas ou seuls `name` et `device_type` comptent. + fn info(name: &str, device_type: wgpu::DeviceType) -> wgpu::AdapterInfo { + wgpu::AdapterInfo { + name: name.into(), + vendor: 0, + device: 0, + device_type, + driver: String::new(), + driver_info: String::new(), + backend: wgpu::Backend::Vulkan, + } + } + + /// `DEVICE_TYPE_CPU` prime sur le nom : c'est l'ICD qui se declare, et un + /// rasteriseur logiciel n'est pas tenu de s'appeler llvmpipe. + #[test] + fn classify_suit_le_device_type_quand_le_nom_ne_dit_rien() { + let i = info("Generic Vulkan Device", wgpu::DeviceType::Cpu); + assert_eq!(classify(&i), Backend::Cpu); + } + + /// Le nom reste un filet pour un ICD qui se declarerait mal -- c'etait l'unique + /// critere avant, on ne le retire pas sans l'avoir vu echouer. + #[test] + fn classify_retombe_sur_le_nom_si_le_device_type_ment() { + let i = info("llvmpipe (LLVM 21.1.8, 256 bits)", wgpu::DeviceType::Other); + assert_eq!(classify(&i), Backend::Cpu); + let i = info("SwiftShader Device (Subzero)", wgpu::DeviceType::Other); + assert_eq!(classify(&i), Backend::Cpu); + } + + /// Un GPU virtuel (VM avec passthrough, virtio-gpu) reste du materiel : il a un + /// vrai pilote derriere, ce n'est pas un rasteriseur logiciel. + #[test] + fn classify_hardware_pour_gpu_virtuel() { + let i = info( + "virtio-gpu Venus (Intel Graphics)", + wgpu::DeviceType::VirtualGpu, + ); + assert_eq!(classify(&i), Backend::Hardware); + } + + #[test] + fn parse_forced_backend_accepte_les_deux_libelles() { + assert_eq!(parse_forced_backend("cpu"), Some(Backend::Cpu)); + assert_eq!(parse_forced_backend("hardware"), Some(Backend::Hardware)); + // Tolerant sur la casse et les espaces : la variable est tapee a la main. + assert_eq!(parse_forced_backend(" CPU \n"), Some(Backend::Cpu)); + } + + /// Une valeur inconnue est ignoree, PAS interpretee comme "cpu" : un forcage mal + /// orthographie doit rendre la main au chemin normal et le dire, pas basculer en + /// silence sur un backend qu'on n'a pas demande. + #[test] + fn parse_forced_backend_rejette_le_reste() { + for raw in ["", "warp", "gpu", "vulkan", "true", "1"] { + assert_eq!(parse_forced_backend(raw), None, "valeur : {raw:?}"); + } + } + + /// Les libelles de `as_str` DOIVENT etre ceux que `parse_forced_backend` accepte, + /// sinon le message d'erreur d'un forcage rate propose une valeur invalide. + #[test] + fn as_str_et_parse_forced_backend_sont_reciproques() { + for b in [Backend::Hardware, Backend::Cpu] { + assert_eq!(parse_forced_backend(b.as_str()), Some(b)); + } + } } diff --git a/crates/compositor/tests/cpu_backend_linux.rs b/crates/compositor/tests/cpu_backend_linux.rs new file mode 100644 index 000000000..9731cf2a5 --- /dev/null +++ b/crates/compositor/tests/cpu_backend_linux.rs @@ -0,0 +1,92 @@ +//! Le backend CPU Linux (lavapipe) est ATTEIGNABLE, et se declare comme tel. +//! +//! Pendant Linux de `warp_device_cannot_decode.rs` cote Windows. Ce que ce fichier +//! epingle est la propriete que PR #162 a etablie sur Windows et que Linux n'avait +//! que par accident : sur un hote qui possede un GPU, on doit pouvoir DEMANDER le +//! rasteriseur logiciel et l'obtenir. +//! +//! Pourquoi ca vaut un test plutot qu'une note : sans ce forcage, le seul moyen +//! d'exercer le chemin CPU etait de vider le loader Vulkan du processus +//! (`VK_DRIVER_FILES`) -- ce qui, sous Electron, prive AUSSI Chromium de son GPU. Il +//! rasterise alors toute son UI sur CPU, sature la machine, et la mesure ne dit plus +//! rien sur notre compositeur. Le chemin n'etait donc pas testable du tout. + +// Linux UNIQUEMENT, comme `warp_device_cannot_decode.rs` l'est a Windows : les +// fichiers de `tests/` sont compiles quelle que soit la plateforme, et `d3d` y +// resout vers un autre module. +#![cfg(target_os = "linux")] + +use openscreen_compositor::d3d::{create_backend, Backend, Gpu}; + +/// Pose a 1 par la CI, ou `mesa-vulkan-drivers` est installe. Sur un poste de dev +/// sans ICD logiciel, ces tests se contentent de le dire : echouer y ferait rougir +/// une machine ou rien n'est casse, et le signal deviendrait du bruit. +const REQUIRE: &str = "OPENSCREEN_REQUIRE_CPU_BACKEND"; + +fn required() -> bool { + std::env::var(REQUIRE).is_ok_and(|v| v != "0") +} + +/// La propriete centrale : `Backend::Cpu` demande explicitement rend un adaptateur +/// que `classify` range bien en `Cpu`. Si `force_fallback_adapter` cessait d'etre +/// honore par wgpu, ce test attraperait le retour silencieux au GPU -- exactement le +/// mode de panne qui rendrait le chemin CPU intestable sans qu'on s'en apercoive. +#[test] +fn le_backend_cpu_est_demandable_et_se_declare_cpu() { + match create_backend(Backend::Cpu) { + Ok(gpu) => assert_eq!( + gpu.backend, + Backend::Cpu, + "force_fallback_adapter a rendu un adaptateur que classify() ne range pas en Cpu" + ), + Err(e) if required() => { + panic!("{REQUIRE} est pose mais le backend CPU est inatteignable : {e:#}") + } + Err(e) => { + eprintln!("cpu_backend_linux: pas de rasteriseur logiciel Vulkan ici ({e:#}). Skip.") + } + } +} + +/// `probe()` ne doit JAMAIS rendre `None` tant qu'un adaptateur -- n'importe lequel -- +/// existe. +/// +/// L'enjeu n'est pas cosmetique : `None` remonte a l'UI en `"none"`, que le TS traite +/// comme "pas d'addon natif du tout" (dev pur-web, jsdom) et qui n'affiche donc +/// AUCUNE notice. Un hote lavapipe-seul doit obtenir `Cpu`, sans quoi il rend a +/// quelques fps en silence -- le "l'app rame" que PR #162 avait supprime cote Windows. +#[test] +fn probe_ne_rend_pas_none_quand_un_adaptateur_existe() { + let cpu = create_backend(Backend::Cpu).is_ok(); + let hw = create_backend(Backend::Hardware).is_ok(); + if !cpu && !hw { + assert!( + !required(), + "{REQUIRE} est pose mais aucun adaptateur Vulkan n'existe ici" + ); + eprintln!("cpu_backend_linux: aucun adaptateur Vulkan ici. Skip."); + return; + } + assert!( + Gpu::probe().is_some(), + "un adaptateur existe (cpu={cpu}, hardware={hw}) mais probe() rend None" + ); +} + +/// `create` est documente "materiel strict" -- les goldens et le bench comptent +/// dessus. Sur un hote qui n'a QUE lavapipe, il doit echouer plutot que de rendre un +/// device logiciel : une mesure prise dessus serait presentee comme une mesure GPU. +/// +/// Le test n'est concluant que la ou le materiel manque ; ailleurs il verifie la +/// contrepartie, qui est tout aussi cassable : `create` ne rend jamais du `Cpu`. +#[test] +fn create_est_materiel_strict() { + match create_backend(Backend::Hardware) { + Ok(gpu) => assert_eq!( + gpu.backend, + Backend::Hardware, + "create() a rendu un device logiciel alors qu'il est documente materiel strict" + ), + Err(e) => eprintln!("cpu_backend_linux: pas de GPU ici, create() a bien echoue ({e:#})."), + } +} diff --git a/crates/compositor/tests/export_timing.rs b/crates/compositor/tests/export_timing.rs index ac67d07bb..f0070794b 100644 --- a/crates/compositor/tests/export_timing.rs +++ b/crates/compositor/tests/export_timing.rs @@ -11,8 +11,7 @@ //! //! Needs a D3D11 GPU and the generated media, so it is opt-in: set //! OPENSCREEN_TEST_MEDIA to a directory holding `screen_colors.mp4` and -//! `webcam_gray.mp4`. Without it every test here skips (no CI builds this -//! crate today — see the Rust-CI gap noted in the PR). +//! `webcam_gray.mp4`. Without it every test here skips. //! //! Regenerate the media with the vendored ffmpeg: //! for c in red green blue white; do ffmpeg -f lavfi \ @@ -22,6 +21,16 @@ //! ffmpeg -f lavfi -i "color=c=gray:size=320x240:duration=4:rate=60" \ //! -c:v libopenh264 -g 60 -pix_fmt yuv420p webcam_gray.mp4 +// Pas sur Linux : `pipeline::probe_frame_count` n'existe que dans +// `pipeline_windows` et `pipeline_macos`. Les fichiers de `tests/` sont compiles +// sur TOUTE plateforme, donc sans cette porte ce fichier casse la compilation du +// crate sous Linux — ce que personne ne voyait faute de job Rust Linux en CI (il +// en existe un depuis, d'ou la decouverte). Meme motif que `compose_linux.rs` et +// `warp_device_cannot_decode.rs`, en negatif : ici c'est Linux qu'on exclut, pas +// les autres qu'on cible, pour ne pas retirer ce fichier du job macOS qui le +// compile aujourd'hui. +#![cfg(not(target_os = "linux"))] + use openscreen_compositor::compositor::Compositor; use openscreen_compositor::config::Cfg; use openscreen_compositor::d3d::Gpu; diff --git a/crates/compositor/tests/output_geometry_golden.rs b/crates/compositor/tests/output_geometry_golden.rs index fca407d0d..3e363b27a 100644 --- a/crates/compositor/tests/output_geometry_golden.rs +++ b/crates/compositor/tests/output_geometry_golden.rs @@ -24,6 +24,12 @@ //! portrait : c'est la mesure du détail regagné, aujourd'hui perdu parce //! que le canvas plafonne à 1080 lignes et que `blit_resized` agrandit. +// Pas sur Linux : `Compositor::readback_resized` n'existe que dans +// `compositor_windows` et `compositor_macos`. Meme raison que dans +// `export_timing.rs` — les fichiers de `tests/` sont compiles sur toute +// plateforme, et sans cette porte le crate ne compile pas sous Linux. +#![cfg(not(target_os = "linux"))] + use openscreen_compositor::compositor::Compositor; use openscreen_compositor::d3d::Gpu; use openscreen_compositor::live::Player; diff --git a/electron-builder.json5 b/electron-builder.json5 index b12185877..0a7520545 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -118,6 +118,53 @@ } ] }, + // Le compositeur natif rend via wgpu/Vulkan (crates/compositor/src/d3d_linux.rs). + // Sans AUCUN ICD Vulkan installé, `request_adapter` ne rend rien : pas de pilote + // GPU, et pas non plus le repli logiciel lavapipe — donc `probe()` répond `"none"` + // et l'aperçu s'ouvre sur « Aperçu indisponible sur cette machine ». Le paquet + // Mesa est ce qui garantit qu'au minimum le rastériseur logiciel existe. + // + // `depends` REMPLACE la liste par défaut d'electron-builder au lieu de s'y ajouter + // (app-builder-lib, FpmTarget.getDefaultDepends) : les entrées reprises ci-dessous + // sont donc ce défaut, verbatim, plus la nôtre en dernier. En retirer une casse le + // paquet silencieusement. + // + // L'AppImage n'a pas de mécanisme de dépendances et reste donc exposée : c'est + // pour elle que `d3d_linux::diagnose` nomme le paquet à installer. + "deb": { + "depends": [ + "libgtk-3-0", + "libnotify4", + "libnss3", + "libxss1", + "libxtst6", + "xdg-utils", + "libatspi2.0-0", + "libuuid1", + "libsecret-1-0", + "mesa-vulkan-drivers" + ] + }, + "pacman": { + // `vulkan-swrast` est le lavapipe d'Arch ; il tire `vulkan-icd-loader` avec lui. + "depends": [ + "c-ares", + "ffmpeg", + "gtk3", + "http-parser", + "libevent", + "libvpx", + "libxslt", + "libxss", + "minizip", + "nss", + "re2", + "snappy", + "libnotify", + "libappindicator-gtk3", + "vulkan-swrast" + ] + }, "win": { "target": [ "nsis" From 7ceb892f62b9d05976a51c5a053a7534f860718a Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Sat, 1 Aug 2026 14:26:06 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(ci):=20rendre=20d=C3=A9terministe=20la?= =?UTF-8?q?=20r=C3=A9solution=20de=20libclang=20sur=20le=20runner=20Linux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `find | head -1` prenait le premier résultat dans l'ordre de parcours du système de fichiers, qui n'est pas trié. L'image ubuntu-latest embarque plusieurs LLVM : si l'un d'eux a un paquet -dev préinstallé, on pouvait sélectionner une version autre que celle qu'apt vient d'installer. Les headers built-in de clang étant liés à la version de libclang, le symptôme aurait été `stddef.h file not found` — une erreur qui ne désigne pas sa cause. `sort -V | tail -1` prend la plus récente, de façon déterministe. Remonté par CodeRabbit sur #223. --- .github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4e75a498..0f913785d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -235,9 +235,15 @@ jobs: - name: Resolve the toolchain paths run: | echo "FFMPEG_DIR=$GITHUB_WORKSPACE/crates/thirdparty/ffmpeg-linux64-lgpl-shared" >> "$GITHUB_ENV" - libclang=$(find /usr/lib/llvm-* -name 'libclang.so' | head -1) + # `sort -V | tail -1` et pas `find | head -1` : l'image du runner embarque + # plusieurs LLVM, et l'ordre de parcours du systeme de fichiers n'est pas + # trie -- on pouvait donc tomber sur une version differente de celle + # qu'apt vient d'installer. Les headers built-in de clang etant lies a la + # version de libclang, le symptome aurait ete `stddef.h file not found`, + # qui ne designe pas sa cause. On prend la plus recente, deterministe. + libclang=$(ls -1 /usr/lib/llvm-*/lib/libclang.so 2>/dev/null | sort -V | tail -1) # Echouer ici plutot que de laisser bindgen partir sur le chemin Windows - # et rendre une erreur qui ne designe pas la cause. + # et rendre une erreur qui ne designe pas la cause non plus. test -n "$libclang" || { echo "libclang introuvable apres l'installation"; exit 1; } echo "LIBCLANG_PATH=$(dirname "$libclang")" >> "$GITHUB_ENV" - name: cargo test (compositor)