cgpu 0.1.0

A tunable GPU compute executor with automatic CPU fallback, byte-based batching, and inline shader generation.
Documentation
use cgpu::*;

#[derive(Clone)]
struct Item {
    input: usize,
    output: usize,
}

impl WorkItem for Item {
    fn bytes_in(&self) -> usize {
        self.input
    }

    fn bytes_out(&self) -> usize {
        self.output
    }
}

#[test]
fn packer_batches_by_bytes() {
    let items = vec![
        Item {
            input: 30,
            output: 5,
        },
        Item {
            input: 30,
            output: 5,
        },
        Item {
            input: 30,
            output: 5,
        },
    ];
    let budget = GpuMemoryBudget::new(100, 0.90).unwrap();
    let mut telemetry = GpuTelemetry::default();

    let plan = pack_by_bytes(&items, budget, &mut telemetry).unwrap();

    assert_eq!(plan.spans().len(), 2);
    assert_eq!(plan.spans()[0].range(), 0..2);
    assert_eq!(plan.spans()[1].range(), 2..3);
    assert_eq!(telemetry.bytes_in, 90);
    assert_eq!(telemetry.bytes_out, 15);
    assert_eq!(telemetry.batch_count, 2);
}

#[test]
fn packer_rejects_items_above_safe_budget() {
    let items = vec![Item {
        input: 100,
        output: 1,
    }];
    let budget = GpuMemoryBudget::new(100, 0.90).unwrap();
    let mut telemetry = GpuTelemetry::default();

    let error = pack_by_bytes(&items, budget, &mut telemetry).unwrap_err();

    assert_eq!(
        error,
        GpuError::ItemExceedsSafeBudget {
            item_index: 0,
            item_bytes: 101,
            safe_vram_bytes: 100,
        }
    );
}

#[test]
fn cpu_fallback_uses_existing_batch_contract() {
    let items = vec![
        Item {
            input: 30,
            output: 1,
        },
        Item {
            input: 30,
            output: 1,
        },
        Item {
            input: 30,
            output: 1,
        },
    ];
    let budget = GpuMemoryBudget::new(100, 0.90).unwrap();
    let mut telemetry = GpuTelemetry::default();
    let plan = pack_by_bytes(&items, budget, &mut telemetry).unwrap();

    let output = run_cpu_fallback(
        &GpuError::GpuFailure(GpuFailure::DeviceLost),
        &items,
        &plan,
        &mut telemetry,
        |batch| {
            Ok(batch
                .iter()
                .map(|item| item.input.saturating_mul(2))
                .collect())
        },
    )
    .unwrap();

    assert_eq!(output, vec![60, 60, 60]);
}

#[test]
fn buffer_pool_reuses_compatible_slots() {
    let mut pool = BufferPool::default();
    let first = pool.acquire(64, BufferUse::Input);
    assert!(pool.release(first));

    let second = pool.acquire(32, BufferUse::Input);

    assert_eq!(first.id, second.id);
    assert_eq!(pool.slot_count(), 1);
}

#[test]
fn resource_cache_keeps_long_lived_objects() {
    let mut cache = ResourceCache::default();

    assert_eq!(*cache.get_or_insert_with("pipeline:a", || 10), 10);
    assert_eq!(*cache.get_or_insert_with("pipeline:a", || 20), 10);

    assert_eq!(cache.len(), 1);
}

#[test]
fn telemetry_log_line_includes_required_buckets() {
    let telemetry = GpuTelemetry {
        pack_ms: 1.0,
        gpu_ms: 2.0,
        readback_ms: 3.0,
        post_ms: 4.0,
        bytes_in: 5,
        bytes_out: 6,
        batch_count: 7,
    };

    let line = telemetry.log_line("layer0");

    assert!(line.contains("pack_ms=1.000"));
    assert!(line.contains("gpu_ms=2.000"));
    assert!(line.contains("readback_ms=3.000"));
    assert!(line.contains("post_ms=4.000"));
    assert!(line.contains("bytes_in=5"));
    assert!(line.contains("bytes_out=6"));
    assert!(line.contains("batch_count=7"));
}

#[test]
fn u32_helpers_round_trip_bytes() {
    let bytes = u32_slice_bytes(&[7, 42]);

    assert_eq!(read_u32(&bytes, 0).unwrap(), 7);
    assert_eq!(read_u32(&bytes, 4).unwrap(), 42);
    assert!(read_u32(&bytes, 8).is_err());
}

#[cfg(feature = "gpu")]
#[test]
fn memory_estimates_from_wgpu_limits_without_static_ceiling() {
    let limits = wgpu::Limits {
        max_storage_buffers_per_shader_stage: 8,
        max_storage_buffer_binding_size: 256 * 1024 * 1024,
        max_buffer_size: 512 * 1024 * 1024,
        ..wgpu::Limits::downlevel_defaults()
    };

    let availability = estimate_gpu_memory_availability_from_limits(&limits, "no platform probe");

    assert_eq!(availability.available_bytes, 1_879_048_192);
    assert!(availability.is_estimate);
    assert_eq!(availability.source, "wgpu.limits.estimate");
}