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>> {
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()
);
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);
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");
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");
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");
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)?;
let cmd_pool = CommandPool::new(&device, queue_family_index)?;
let mut cmd = cmd_pool.allocate_primary()?;
{
let mut rec = cmd.begin()?;
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();
rec.copy_image_to_buffer(
&image,
ImageLayout::TRANSFER_SRC_OPTIMAL,
&staging,
&[BufferImageCopy::full_2d(W, H)],
);
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");
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());
}
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(())
}