vulkane 0.5.0

Vulkan API bindings generated entirely from vk.xml, with a complete safe RAII wrapper covering compute and graphics: instance/device/queue, buffer, image, sampler, render pass, framebuffer, graphics + compute pipelines, swapchain, a VMA-style sub-allocator with TLSF + linear pools and defragmentation, sync primitives (fences, binary + timeline semaphores, sync2 barriers), query pools, and optional GLSL/WGSL/HLSL→SPIR-V compilation via naga or shaderc. Supports Vulkan 1.2.175 onward — swap vk.xml and rebuild.
//! Headless triangle: render a colored RGB triangle to a 256x256 R8G8B8A8
//! image using a real graphics pipeline, then read the pixels back via a
//! staging buffer and verify the centre pixel is non-black.
//!
//! Demonstrates the safe wrapper's full graphics path without needing a
//! window or swapchain:
//!
//! 1. Loads pre-compiled `triangle.vert.spv` / `triangle.frag.spv`.
//! 2. Creates a 256x256 RGBA8 color attachment image (TRANSFER_SRC so
//!    we can read it back).
//! 3. Builds a single-subpass render pass clearing to black on load
//!    and storing on end.
//! 4. Builds a graphics pipeline with no vertex input (the vertex
//!    shader uses gl_VertexIndex to pick from a hardcoded array).
//! 5. Records a command buffer that begins the render pass, draws 3
//!    vertices, ends the pass, and copies the image to a staging
//!    buffer.
//! 6. Submits, waits, and verifies that the centre pixel of the image
//!    is non-zero (proves the triangle was rasterized).
//!
//! Run with: `cargo run -p vulkane --features fetch-spec --example headless_triangle`

use vulkane::safe::{
    AccessFlags, ApiVersion, AttachmentLoadOp, AttachmentStoreOp, Buffer, BufferCreateInfo,
    BufferImageCopy, BufferUsage, CommandPool, DeviceCreateInfo, DeviceMemory, Fence, Format,
    Framebuffer, GraphicsPipelineBuilder, GraphicsShaderStage, Image, Image2dCreateInfo,
    ImageLayout, ImageUsage, ImageView, Instance, InstanceCreateInfo, MemoryPropertyFlags,
    PipelineLayout, PipelineStage, QueueCreateInfo, QueueFlags, RenderPass, ShaderModule,
};

const W: u32 = 256;
const H: u32 = 256;
const PIXEL_BYTES: u64 = 4;
const BUF_SIZE: u64 = (W as u64) * (H as u64) * PIXEL_BYTES;


fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Load shaders.
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    let vert_path = format!("{manifest_dir}/examples/shaders/triangle.vert.spv");
    let frag_path = format!("{manifest_dir}/examples/shaders/triangle.frag.spv");
    let vert_bytes = std::fs::read(&vert_path).map_err(|e| {
        format!("could not read {vert_path}: {e} (run `cargo run -p vulkane --features naga,fetch-spec --example compile_shader`)")
    })?;
    let frag_bytes =
        std::fs::read(&frag_path).map_err(|e| format!("could not read {frag_path}: {e}"))?;
    println!(
        "[OK] Loaded vertex shader ({} bytes), fragment shader ({} bytes)",
        vert_bytes.len(),
        frag_bytes.len()
    );

    // 2. Instance + physical + device + queue.
    let instance = match Instance::new(InstanceCreateInfo {
        application_name: Some("vulkane headless_triangle"),
        api_version: ApiVersion::V1_0,
        ..InstanceCreateInfo::default()
    }) {
        Ok(i) => i,
        Err(e) => {
            eprintln!("SKIP: could not create Vulkan instance: {e}");
            return Ok(());
        }
    };
    let physical = instance
        .enumerate_physical_devices()?
        .into_iter()
        .find(|pd| pd.find_queue_family(QueueFlags::GRAPHICS).is_some())
        .ok_or("No physical device with a graphics queue family")?;
    println!("[OK] Using GPU: {}", physical.properties().device_name());

    let queue_family_index = physical.find_queue_family(QueueFlags::GRAPHICS).unwrap();
    let device = physical.create_device(DeviceCreateInfo {
        queue_create_infos: &[QueueCreateInfo::single(queue_family_index)],
        ..Default::default()
    })?;
    let queue = device.get_queue(queue_family_index, 0);

    // 3. Color attachment image.
    let image = Image::new_2d(
        &device,
        Image2dCreateInfo {
            format: Format::R8G8B8A8_UNORM,
            width: W,
            height: H,
            usage: ImageUsage::COLOR_ATTACHMENT | ImageUsage::TRANSFER_SRC,
        },
    )?;
    let img_req = image.memory_requirements();
    let img_mt = physical
        .find_memory_type(img_req.memory_type_bits, MemoryPropertyFlags::DEVICE_LOCAL)
        .or_else(|| {
            physical.find_memory_type(img_req.memory_type_bits, MemoryPropertyFlags::HOST_VISIBLE)
        })
        .ok_or("no compatible memory type")?;
    let img_mem = DeviceMemory::allocate(&device, img_req.size, img_mt)?;
    image.bind_memory(&img_mem, 0)?;
    let view = ImageView::new_2d_color(&image)?;
    println!("[OK] Created {W}x{H} R8G8B8A8 color attachment");

    // 4. Render pass: one color attachment, clear on load, store on end,
    //    UNDEFINED -> TRANSFER_SRC_OPTIMAL so we can copy out at the end.
    let render_pass = RenderPass::simple_color(
        &device,
        Format::R8G8B8A8_UNORM,
        AttachmentLoadOp::CLEAR,
        AttachmentStoreOp::STORE,
        ImageLayout::TRANSFER_SRC_OPTIMAL,
    )?;
    let framebuffer = Framebuffer::new(&device, &render_pass, &[&view], W, H)?;
    println!("[OK] Created render pass + framebuffer");

    // 5. Pipeline.
    let vert = ShaderModule::from_spirv_bytes(&device, &vert_bytes)?;
    let frag = ShaderModule::from_spirv_bytes(&device, &frag_bytes)?;
    let pipeline_layout = PipelineLayout::new(&device, &[])?;
    let pipeline = GraphicsPipelineBuilder::new(&pipeline_layout, &render_pass)
        .stage(GraphicsShaderStage::Vertex, &vert, "main")
        .stage(GraphicsShaderStage::Fragment, &frag, "main")
        .viewport_extent(W, H)
        .cull_mode(vulkane::safe::CullMode::NONE)
        .front_face(vulkane::safe::FrontFace::COUNTER_CLOCKWISE)
        .build(&device)?;
    println!("[OK] Built graphics pipeline");

    // 6. Staging buffer for readback.
    let staging = Buffer::new(
        &device,
        BufferCreateInfo {
            size: BUF_SIZE,
            usage: BufferUsage::TRANSFER_DST,
        },
    )?;
    let st_req = staging.memory_requirements();
    let st_mt = physical
        .find_memory_type(
            st_req.memory_type_bits,
            MemoryPropertyFlags::HOST_VISIBLE | MemoryPropertyFlags::HOST_COHERENT,
        )
        .ok_or("no host-visible memory type")?;
    let mut st_mem = DeviceMemory::allocate(&device, st_req.size, st_mt)?;
    staging.bind_memory(&st_mem, 0)?;

    // 7. Record + submit.
    let cmd_pool = CommandPool::new(&device, queue_family_index)?;
    let mut cmd = cmd_pool.allocate_primary()?;
    {
        let mut rec = cmd.begin()?;

        // The render pass already transitions UNDEFINED -> COLOR_ATTACHMENT_OPTIMAL
        // and back to TRANSFER_SRC_OPTIMAL via initial/final layouts, so we
        // don't need an explicit barrier before begin_render_pass.
        rec.begin_render_pass(&render_pass, &framebuffer, &[[0.0, 0.0, 0.0, 1.0]]);
        rec.bind_graphics_pipeline(&pipeline);
        rec.draw(3, 1, 0, 0);
        rec.end_render_pass();

        // Image is now in TRANSFER_SRC_OPTIMAL (per render pass finalLayout).
        // Copy it to the staging buffer.
        rec.copy_image_to_buffer(
            &image,
            ImageLayout::TRANSFER_SRC_OPTIMAL,
            &staging,
            &[BufferImageCopy::full_2d(W, H)],
        );
        // Transfer -> Host
        rec.memory_barrier(PipelineStage::TRANSFER, PipelineStage::HOST, AccessFlags::TRANSFER_WRITE, AccessFlags::HOST_READ);

        rec.end()?;
    }

    let fence = Fence::new(&device)?;
    queue.submit(&[&cmd], Some(&fence))?;
    fence.wait(u64::MAX)?;
    println!("[OK] GPU finished rendering");

    // 8. Verify the centre pixel of the image is non-black (proves the
    //    triangle was actually rasterized).
    let m = st_mem.map()?;
    let bytes = m.as_slice();
    let cx = W / 2;
    let cy = H / 2;
    let i = ((cy * W + cx) * 4) as usize;
    let r = bytes[i];
    let g = bytes[i + 1];
    let b = bytes[i + 2];
    let a = bytes[i + 3];
    println!("[OK] Centre pixel: ({r}, {g}, {b}, {a})");
    if r == 0 && g == 0 && b == 0 {
        return Err("centre pixel is black — triangle was not rasterized".into());
    }

    // Count how many non-black pixels we have. A clean triangle covers
    // about 24% of a 256x256 viewport (the hardcoded triangle in the
    // shader covers vertices at (0,-0.7), (0.7, 0.7), (-0.7, 0.7),
    // which is roughly 0.49 * 0.5 = 24.5%). Allow a wide tolerance.
    let mut painted = 0u32;
    for i in 0..(W * H) as usize {
        let r = bytes[i * 4];
        let g = bytes[i * 4 + 1];
        let b = bytes[i * 4 + 2];
        if r != 0 || g != 0 || b != 0 {
            painted += 1;
        }
    }
    let total = W * H;
    let pct = painted as f32 / total as f32 * 100.0;
    println!("[OK] {painted} / {total} non-black pixels ({pct:.1}%)");
    assert!(
        painted > 5000,
        "fewer than 5000 painted pixels — render didn't cover much of the viewport"
    );

    drop(m);
    device.wait_idle()?;
    println!();
    println!("=== headless_triangle example PASSED ===");
    Ok(())
}