From c6bc4d2ba09902ebbd4ce473cd56fb676eb7d4b3 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:26:16 -0400 Subject: [PATCH] ci: prove the lavapipe GPU path draws (FIELDCRAFT_REQUIRE_GPU=1) The drawing tests skipped silently without an adapter. Under FIELDCRAFT_REQUIRE_GPU=1 they fail instead, and the snapshot and MCP screenshot tests require a GPU renderer. CI runs them with FIELDCRAFT_GPU=software on lavapipe. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo --- .github/workflows/ci.yml | 5 ++++- apps/fieldcraft-cli/src/snapshot.rs | 4 +++- crates/gpu/src/capture.rs | 8 +++++--- crates/gpu/src/select.rs | 9 +++++++++ crates/mcp/src/tests.rs | 3 +++ 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53a4d53..4c319b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,10 +133,13 @@ jobs: shared-key: ci save-if: ${{ github.ref == 'refs/heads/develop' }} - name: Software GPU (lavapipe), so headless snapshots run the GPU pipeline - run: sudo apt-get update -q && sudo apt-get install -y -q --no-install-recommends mesa-vulkan-drivers + run: sudo apt-get update -q && sudo apt-get install -y -q --no-install-recommends mesa-vulkan-drivers libvulkan1 - name: cargo xtask ci (the affected crates, or everything) env: BASE: ${{ needs.plan.outputs.base }} + # The drawing tests run on lavapipe and fail, instead of skipping, without it. + FIELDCRAFT_GPU: software + FIELDCRAFT_REQUIRE_GPU: "1" run: | if [ -n "$BASE" ]; then cargo xtask ci --affected "$BASE"; else cargo xtask ci; fi diff --git a/apps/fieldcraft-cli/src/snapshot.rs b/apps/fieldcraft-cli/src/snapshot.rs index 2643fc4..b1c97a6 100644 --- a/apps/fieldcraft-cli/src/snapshot.rs +++ b/apps/fieldcraft-cli/src/snapshot.rs @@ -223,7 +223,7 @@ mod tests { /// The cantilever drawn headless: a PNG of the size asked for, with the model in it (pixels /// of the hex colour) and its element edges (dark pixels). `auto` draws on the GPU when the - /// machine has one and on the CPU otherwise (a CI box without a software GPU); `cpu` always + /// machine has one and on the CPU otherwise (it must be the GPU under `FIELDCRAFT_REQUIRE_GPU=1`); `cpu` always /// on the CPU, and both pictures show the same model. A section and a field change it. #[test] fn snapshots_the_model_and_its_mesh_headless() { @@ -243,6 +243,8 @@ mod tests { assert!(edges > 100, "{drawn:?}: {edges} edge pixels"); if policy == Policy::Cpu { assert_eq!(drawn, Renderer::Cpu { why: None }); + } else if fieldcraft_gpu::select::gpu_required() { + assert!(matches!(drawn, Renderer::Gpu { .. }), "FIELDCRAFT_REQUIRE_GPU=1 but drawn by {drawn:?}"); } } // A section along the beam draws half of it. diff --git a/crates/gpu/src/capture.rs b/crates/gpu/src/capture.rs index ae385f2..51f88d3 100644 --- a/crates/gpu/src/capture.rs +++ b/crates/gpu/src/capture.rs @@ -304,12 +304,14 @@ mod tests { } /// A picture on whatever adapter the machine has: the right size, the triangle's red and - /// the edge's black on the white background. A machine with no adapter (a CI box without - /// a software GPU) is reported, not failed: `auto` then draws on the CPU. + /// the edge's black on the white background. A machine with no adapter is reported, not + /// failed (`auto` then draws on the CPU), unless `FIELDCRAFT_REQUIRE_GPU=1` (CI). #[test] fn draws_offscreen_when_an_adapter_exists() { - let cap = match Capturer::open(Policy::Auto) { + let cap = match Capturer::open(Policy::from_env().unwrap()) { Ok(Some(c)) => c, + Ok(None) if crate::select::gpu_required() => panic!("FIELDCRAFT_REQUIRE_GPU=1 but no GPU adapter"), + Err(e) if crate::select::gpu_required() => panic!("FIELDCRAFT_REQUIRE_GPU=1 but no GPU device: {e}"), Ok(None) => return eprintln!("no GPU adapter: skipped"), Err(e) => return eprintln!("no GPU device ({e}): skipped"), }; diff --git a/crates/gpu/src/select.rs b/crates/gpu/src/select.rs index 8fc9ab7..2975bf3 100644 --- a/crates/gpu/src/select.rs +++ b/crates/gpu/src/select.rs @@ -46,6 +46,15 @@ impl Policy { pub use crate::Tier; +/// Set to `1` where a GPU adapter must exist (CI with lavapipe): tests that draw then fail +/// instead of skipping when none is found, so the GPU path is proven to draw. +pub const REQUIRE_ENV: &str = "FIELDCRAFT_REQUIRE_GPU"; + +/// Whether [`REQUIRE_ENV`] is set to `1`. +pub fn gpu_required() -> bool { + std::env::var(REQUIRE_ENV).is_ok_and(|v| v.trim() == "1") +} + /// An adapter's tier and its rank within it (lower is better). pub fn rank(t: wgpu::DeviceType) -> (Tier, u8) { match t { diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index 8e27f62..493ec46 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -222,6 +222,9 @@ fn screenshot_takes_a_camera_orientation() { assert_eq!(info["view"], json!({"yaw": 30.0, "pitch": 90.0, "distance": 1e9, "target": {"center_of": "mass"}}), "{info}"); // The reply names what drew it: the viewport's pipeline on a GPU, or the CPU rasterizer. assert!(matches!(info["renderer"]["kind"].as_str(), Some("hardware" | "software" | "cpu")), "{info}"); + if fieldcraft_gpu::select::gpu_required() { + assert!(matches!(info["renderer"]["kind"].as_str(), Some("hardware" | "software")), "FIELDCRAFT_REQUIRE_GPU=1: {info}"); + } let (again, _) = picture(&mut s, json!({"width": 64, "height": 48})); assert_eq!(again, turned); // Aimed at a point, the camera looks at it; a section cuts the mesh.