uzor-urx-engine 1.4.4

URX engine — cross-backend façade with per-region dirty tracking, needs_paint() cadence hook, RenderCadence policy. Consumes urx-core Scene + DirtyState; dispatches to urx-cpu / urx-wgpu / urx-hybrid.
//! Runtime backend switching demo.
//!
//! Builds the SAME logical scene through 3 hint variants → 3 different
//! backends via `Backend::auto`, runs each through `UrxEngine`, and
//! prints frame-time for each. Proves the consumer never directly
//! selects a backend; the engine picks based on workload signals.
//!
//! Run:
//!   cargo run -p uzor-urx-engine --example runtime_backend_switch --release
//!
//! Stdout: 3 rows showing (scenario, picked-backend, frame-ms).

use uzor_urx_core::math::{Color, Rect};
use uzor_urx_core::region::RegionId;
use uzor_urx_core::scene::Scene;
use uzor_urx_cpu::Pixmap;
use uzor_urx_engine::cadence::RenderCadence;
use uzor_urx_engine::engine::{Backend, RenderTarget, UrxEngine, WorkloadHint};

const W: u32 = 800;
const H: u32 = 600;
const N_REGIONS: usize = 16;
const FRAMES: usize = 30;

fn main() {
    println!("=== URX backend auto-switch demo ===");
    println!();

    let scenarios: &[(&str, WorkloadHint)] = &[
        ("headless / no GPU",
         WorkloadHint { gpu_available: false, region_count: 16, total_pixels: 480_000,
                         ..Default::default() }),
        ("tiny low-pixel scene",
         WorkloadHint { gpu_available: true, region_count: 2, total_pixels: 50_000,
                         ..Default::default() }),
        ("dashboard, 16 regions, GPU available",
         WorkloadHint { gpu_available: true, region_count: 16, total_pixels: 480_000,
                         retained: true, ..Default::default() }),
    ];

    println!("{:<40} │ {:<10} │ {:<14}", "scenario", "backend", "render ms/frame");
    println!("{}", "".repeat(72));

    for (label, hint) in scenarios {
        let backend = Backend::auto(*hint);
        let ms = run_scene_with_backend(backend);
        println!("{:<40} │ {:<10} │ {:>10.3}", label, format!("{:?}", backend), ms);
    }
    println!();
    println!("Notes:");
    println!("  - Same 16-region scene rendered through whichever backend");
    println!("    `Backend::auto(hint)` returned");
    println!("  - With default features (wgpu-backend only) Hybrid falls through");
    println!("    to Wgpu; rebuild with `--features hybrid-backend` to see Hybrid");
    println!("    paths");
    println!("  - CPU path is real (renders into Pixmap); Wgpu/Hybrid paths");
    println!("    require a wgpu surface — this demo measures Cpu and reports");
    println!("    Wgpu/Hybrid choice only (the engine returns the same chosen");
    println!("    backend regardless of whether we actually render through it)");
}

fn run_scene_with_backend(backend: Backend) -> f64 {
    // For non-CPU backends without a wgpu surface set up, we still
    // construct the engine + populate scene to exercise its dirty
    // tracking; only CPU does real rendering here. The demo's main
    // goal is to verify the engine + Backend::auto + region pipeline
    // works end-to-end with the chosen backend.
    if backend != Backend::Cpu {
        return f64::NAN;
    }

    let mut engine = UrxEngine::new_cpu(W, H);
    let mut pixmap = Pixmap::new(W, H);

    // Populate scene with N regions.
    for i in 0..N_REGIONS {
        let col = (i % 4) as f64;
        let row = (i / 4) as f64;
        let x0 = 40.0 + col * 180.0;
        let y0 = 40.0 + row * 130.0;
        let mut scene = Scene::new();
        scene.fill_rect_solid(
            Rect::new(0.0, 0.0, 160.0, 110.0),
            Color::rgba8(
                ((i * 37) & 0xff) as u8,
                ((i * 71) & 0xff) as u8,
                ((i * 113) & 0xff) as u8,
                255,
            ),
        );
        engine.upsert_region(
            RegionId(i as u64),
            scene,
            Rect::new(x0, y0, x0 + 160.0, y0 + 110.0),
            RenderCadence::Static,
        );
    }

    // First render = cold (cache miss); subsequent frames hit cache.
    let _ = engine.render(RenderTarget::Cpu(&mut pixmap));

    let t = std::time::Instant::now();
    for _ in 0..FRAMES {
        // Mark all regions dirty so render does real work each frame.
        for i in 0..N_REGIONS {
            engine.mark_dirty(RegionId(i as u64));
        }
        let _ = engine.render(RenderTarget::Cpu(&mut pixmap));
    }
    let elapsed_ms = t.elapsed().as_secs_f64() * 1000.0;
    elapsed_ms / FRAMES as f64
}