lgpu 0.1.2

Lightweight, bindless-first Vulkan 1.3 wrapper for Rust
Documentation
//! Integration tests verifying command pool and synchronization features:
//! - Timeline Semaphores (CPU-GPU & CPU-CPU signaling and waiting)
//! - Asynchronous queue submissions with timeline semaphore dependencies
//! - Zero-allocation command recorder harvesting across async frames
//! - Multi-threaded command recording and atomic batch submissions
//! - GPU execution, memory, and buffer pipeline barriers

use lgpu::{
    BufferDesc, BufferUsage, Instance, MemoryLocation, TextureDesc, TextureFormat, TextureUsage,
};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

#[test]
fn test_timeline_semaphore_cpu_signaling_and_waiting() {
    let instance = Instance::new().expect("Failed to create instance");
    let adapters = instance
        .enumerate_adapters()
        .expect("Failed to enumerate adapters");
    let (device, _queue) = adapters[0]
        .request_device(&instance)
        .expect("Failed to request device");

    let semaphore = Arc::new(
        device
            .create_timeline_semaphore(0)
            .expect("Failed to create timeline semaphore"),
    );

    let initial_val = semaphore.value().expect("Failed to get semaphore value");
    assert_eq!(initial_val, 0, "Initial timeline semaphore value must be 0");

    // Signal from host
    semaphore
        .signal(5)
        .expect("Failed to signal timeline semaphore");
    let val_after_signal = semaphore.value().expect("Failed to get semaphore value");
    assert_eq!(
        val_after_signal, 5,
        "Timeline semaphore value must be 5 after signaling"
    );

    // Test cross-thread signaling and waiting
    let sem_clone = Arc::clone(&semaphore);
    let handle = thread::spawn(move || {
        thread::sleep(Duration::from_millis(50));
        sem_clone
            .signal(10)
            .expect("Thread failed to signal semaphore");
    });

    // Wait on main thread with a 5 second timeout
    semaphore
        .wait(10, 5_000_000_000)
        .expect("Wait on timeline semaphore failed");
    let val_final = semaphore.value().expect("Failed to get semaphore value");
    assert!(val_final >= 10, "Timeline semaphore value must be >= 10");

    handle.join().expect("Worker thread panicked");
    println!("SUCCESS: Timeline semaphore CPU signaling and cross-thread waiting verified.");
}

#[test]
fn test_timeline_semaphore_gpu_async_pipelining() {
    let instance = Instance::new().expect("Failed to create instance");
    let adapters = instance
        .enumerate_adapters()
        .expect("Failed to enumerate adapters");
    let (device, queue) = adapters[0]
        .request_device(&instance)
        .expect("Failed to request device");

    let timeline = device
        .create_timeline_semaphore(0)
        .expect("Failed to create timeline semaphore");

    let tex_desc = TextureDesc {
        label: Some("AsyncTex"),
        width: 128,
        height: 128,
        depth: 1,
        mip_levels: 1,
        sample_count: 1,
        format: TextureFormat::Rgba8Unorm,
        usage: TextureUsage::RENDER_ATTACHMENT | TextureUsage::COPY_SRC,
    };
    let texture = device
        .create_texture(&tex_desc)
        .expect("Failed to create texture");

    // Step 1: Submit async command buffer that signals timeline value 1 on GPU completion
    let mut encoder1 = device
        .create_command_encoder()
        .expect("Failed to create encoder");
    encoder1
        .clear_and_render_texture(&texture, [0.1, 0.2, 0.3, 1.0])
        .expect("clear failed");
    encoder1.memory_barrier();
    let cmd1 = encoder1.finish();

    queue
        .submit_async(cmd1, &[], &[(&timeline, 1)])
        .expect("Failed to submit async batch 1");

    // Wait on CPU for GPU to reach timeline value 1
    timeline
        .wait(1, 5_000_000_000)
        .expect("Timeout waiting for GPU timeline semaphore at value 1");
    let val1 = timeline.value().expect("Failed to read timeline value");
    assert!(
        val1 >= 1,
        "Timeline value must be >= 1 after GPU completion"
    );

    // Step 2: Submit second async command buffer that waits for timeline value 1 and signals value 2
    let mut encoder2 = device
        .create_command_encoder()
        .expect("Failed to create encoder");
    encoder2
        .clear_and_render_texture(&texture, [0.4, 0.5, 0.6, 1.0])
        .expect("clear failed");
    let cmd2 = encoder2.finish();

    queue
        .submit_async(cmd2, &[(&timeline, 1)], &[(&timeline, 2)])
        .expect("Failed to submit async batch 2");

    // Wait on CPU for GPU to reach timeline value 2
    timeline
        .wait(2, 5_000_000_000)
        .expect("Timeout waiting for GPU timeline semaphore at value 2");
    let val2 = timeline.value().expect("Failed to read timeline value");
    assert!(
        val2 >= 2,
        "Timeline value must be >= 2 after GPU completion"
    );

    println!("SUCCESS: GPU async pipelining with timeline semaphores verified.");
}

#[test]
fn test_multithreaded_batch_submission() {
    let instance = Instance::new().expect("Failed to create instance");
    let adapters = instance
        .enumerate_adapters()
        .expect("Failed to enumerate adapters");
    let (device, queue) = adapters[0]
        .request_device(&instance)
        .expect("Failed to request device");

    let device_arc = Arc::new(device);
    let mut thread_handles = Vec::new();

    // Spawn 4 worker threads, each independently creating and recording a command buffer
    for thread_idx in 0..4 {
        let dev = Arc::clone(&device_arc);
        let handle = thread::spawn(move || {
            let mut encoder = dev
                .create_command_encoder()
                .expect("Failed to create encoder");
            // Add a memory barrier in each recorded buffer
            encoder.memory_barrier();
            // Create a small texture and clear it
            let tex_desc = TextureDesc {
                label: Some("ThreadTex"),
                width: 64,
                height: 64,
                depth: 1,
                mip_levels: 1,
                sample_count: 1,
                format: TextureFormat::Rgba8Unorm,
                usage: TextureUsage::RENDER_ATTACHMENT,
            };
            let tex = dev
                .create_texture(&tex_desc)
                .expect("Failed to create texture");
            let c = (thread_idx as f32 + 1.0) * 0.2;
            encoder
                .clear_and_render_texture(&tex, [c, c, c, 1.0])
                .expect("clear failed");
            (encoder.finish(), tex)
        });
        thread_handles.push(handle);
    }

    // Collect command buffers recorded in parallel across threads
    let mut command_buffers = Vec::new();
    let mut kept_textures = Vec::new();
    for h in thread_handles {
        let (cmd, tex) = h.join().expect("Worker thread recording failed");
        command_buffers.push(cmd);
        kept_textures.push(tex);
    }

    assert_eq!(command_buffers.len(), 4);

    // Submit all 4 command buffers atomically in a single multi-threaded batch submission!
    queue
        .submit_batches(command_buffers)
        .expect("Failed to submit command buffer batch");

    drop(kept_textures);

    println!("SUCCESS: Multi-threaded recording and atomic batch submission verified.");
}

#[test]
fn test_gpu_barriers_and_buffer_synchronization() {
    let instance = Instance::new().expect("Failed to create instance");
    let adapters = instance
        .enumerate_adapters()
        .expect("Failed to enumerate adapters");
    let (device, queue) = adapters[0]
        .request_device(&instance)
        .expect("Failed to request device");

    let buf_desc = BufferDesc {
        label: Some("BarrierTestBuffer"),
        size: 512,
        usage: BufferUsage::STORAGE | BufferUsage::TRANSFER_SRC | BufferUsage::TRANSFER_DST,
        memory_location: MemoryLocation::CpuToGpu,
    };
    let buffer = device
        .create_buffer(&buf_desc)
        .expect("Failed to create buffer");

    let mut encoder = device
        .create_command_encoder()
        .expect("Failed to create encoder");
    // Test general GPU memory barrier
    encoder.memory_barrier();
    // Test buffer-specific barrier
    encoder.buffer_barrier(&buffer);
    let cmd = encoder.finish();

    queue
        .submit(cmd)
        .expect("Failed to submit barrier command buffer");

    println!("SUCCESS: GPU memory barrier and buffer-specific barrier verified.");
}

#[test]
fn test_async_recorder_harvesting_leak_free() {
    let instance = Instance::new().expect("Failed to create instance");
    let adapters = instance
        .enumerate_adapters()
        .expect("Failed to enumerate adapters");
    let (device, queue) = adapters[0]
        .request_device(&instance)
        .expect("Failed to request device");

    let timeline = device
        .create_timeline_semaphore(0)
        .expect("Failed to create timeline semaphore");

    // Run 50 iterations of async submissions without explicit CPU fences
    // Verifies that the internal timeline frame semaphore and pending_async_recorders
    // harvest finished command pools smoothly without resource exhaustion or leaks.
    for frame in 1..=50u64 {
        let mut encoder = device
            .create_command_encoder()
            .expect("Failed to create encoder");
        encoder.memory_barrier();
        let cmd = encoder.finish();

        let wait_val = frame - 1;
        let signal_val = frame;
        let wait_sems: &[(&lgpu::TimelineSemaphore, u64)] = if wait_val > 0 {
            &[(&timeline, wait_val)]
        } else {
            &[]
        };

        queue
            .submit_async(cmd, wait_sems, &[(&timeline, signal_val)])
            .expect("Async submit failed");
    }

    // Wait for the final frame to complete
    timeline
        .wait(50, 5_000_000_000)
        .expect("Timeout waiting for final async frame");

    let final_val = timeline.value().expect("Failed to get timeline value");
    assert_eq!(final_val, 50, "Final timeline value must reach 50");

    println!("SUCCESS: 50 iterations of async submissions with zero leaks and automatic recycling verified.");
}