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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-16 07:52:10 -04:00
1 parent b905f1bc89
commit c6bc4d2ba0
5 files changed
+24 -5

No files matched your search

+4 -1
View File
@@ -133,10 +133,13 @@ jobs:
shared-key: ci shared-key: ci
save-if: ${{ github.ref == 'refs/heads/develop' }} save-if: ${{ github.ref == 'refs/heads/develop' }}
- name: Software GPU (lavapipe), so headless snapshots run the GPU pipeline - 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) - name: cargo xtask ci (the affected crates, or everything)
env: env:
BASE: ${{ needs.plan.outputs.base }} 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: | run: |
if [ -n "$BASE" ]; then cargo xtask ci --affected "$BASE"; else cargo xtask ci; fi if [ -n "$BASE" ]; then cargo xtask ci --affected "$BASE"; else cargo xtask ci; fi
+3 -1
View File
@@ -223,7 +223,7 @@ mod tests {
/// The cantilever drawn headless: a PNG of the size asked for, with the model in it (pixels /// 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 /// 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. /// on the CPU, and both pictures show the same model. A section and a field change it.
#[test] #[test]
fn snapshots_the_model_and_its_mesh_headless() { fn snapshots_the_model_and_its_mesh_headless() {
@@ -243,6 +243,8 @@ mod tests {
assert!(edges > 100, "{drawn:?}: {edges} edge pixels"); assert!(edges > 100, "{drawn:?}: {edges} edge pixels");
if policy == Policy::Cpu { if policy == Policy::Cpu {
assert_eq!(drawn, Renderer::Cpu { why: None }); 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. // A section along the beam draws half of it.
+5 -3
View File
@@ -304,12 +304,14 @@ mod tests {
} }
/// A picture on whatever adapter the machine has: the right size, the triangle's red and /// 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 /// the edge's black on the white background. A machine with no adapter is reported, not
/// a software GPU) is reported, not failed: `auto` then draws on the CPU. /// failed (`auto` then draws on the CPU), unless `FIELDCRAFT_REQUIRE_GPU=1` (CI).
#[test] #[test]
fn draws_offscreen_when_an_adapter_exists() { 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(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"), Ok(None) => return eprintln!("no GPU adapter: skipped"),
Err(e) => return eprintln!("no GPU device ({e}): skipped"), Err(e) => return eprintln!("no GPU device ({e}): skipped"),
}; };
+9
View File
@@ -46,6 +46,15 @@ impl Policy {
pub use crate::Tier; 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). /// An adapter's tier and its rank within it (lower is better).
pub fn rank(t: wgpu::DeviceType) -> (Tier, u8) { pub fn rank(t: wgpu::DeviceType) -> (Tier, u8) {
match t { match t {
+3
View File
@@ -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}"); 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. // 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}"); 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})); let (again, _) = picture(&mut s, json!({"width": 64, "height": 48}));
assert_eq!(again, turned); assert_eq!(again, turned);
// Aimed at a point, the camera looks at it; a section cuts the mesh. // Aimed at a point, the camera looks at it; a section cuts the mesh.