diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d228826d9..0f913785d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -190,6 +190,80 @@ 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" + # `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 non plus. + 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"