Open8

Vulkanoで三角形描画

KaraKara

そもそも、vulkanoとは

Vulkano is a Rust wrapper around the Vulkan graphics API.

Vulkan graphics APIのRustラッパーらしい。
Vulkanをまともに触ったことさえないが、Rustが好きなのでvulkanoに入門してみることにする。
このスクラップには、vulkanoで遊ぶ上で気になったことやメモを残すことにしようと思う。

KaraKara

Windowの表示

vulkanoはレンダリングAPIなので、ウィンドウの作成と管理はwinitが行う。

// ロガーの初期化
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

// イベントループを作成
let event_loop = EventLoop::new()?;
let window = Arc::new(WindowBuilder::new().build(&event_loop)?);

event_loop.run(move |event, elwt| {
    elwt.set_control_flow(ControlFlow::Poll);

    match event {
        Event::WindowEvent {
            event: WindowEvent::CloseRequested,
            ..
        } => {
            elwt.exit();
        }
        Event::AboutToWait => window.request_redraw(),
        _ => (),
    }
})?;
KaraKara

インスタンスの作成

描画に必要な拡張機能や、有効化するレイヤー(今回は検証レイヤー)を指定してインスタンスを作成する。

検証レイヤーはVulkan SDKをインストールしないと使用できないので注意。

    // 描画に必要な拡張をSurfaceに問い合わせる
    let required_extensions = Surface::required_extensions(&event_loop)?;
    let available_layers = library.layer_properties()?;
    let requested_layers = VALIDATION_LAYERS
        .iter()
        .map(|s| s.to_string())
        .collect::<HashSet<_>>();
    let instance = Instance::new(
        library,
        InstanceCreateInfo {
            flags: InstanceCreateFlags::ENUMERATE_PORTABILITY,
            enabled_extensions: required_extensions,
            enabled_layers: if VALIDATION_ENABLED {
                available_layers
                    .filter(|p| requested_layers.contains(p.name()))
                    .map(|p| p.name().to_string())
                    .collect::<Vec<_>>()
            } else {
                vec![]
            },
            ..Default::default()
        },
    )?;
KaraKara

物理デバイスの選択と論理デバイスの作成

物理デバイスの選択

複雑そうに見えるがしていることは単純で

  • 物理デバイスのリストから必要な拡張がサポートされたデバイスをフィルター
  • GRAPHICSに使えてsurfaceをサポートしているデバイスをフィルター
  • デバイスのタイプに重みを付けて適しているものを選ぶ

queue_family_indexはよくわからないが、あとから使うのでその時に調べる。

let (physical_device, queue_family_index) = instance
    .enumerate_physical_devices()?
    .filter(|p| p.supported_extensions().contains(&device_extensions))
    .filter_map(|p| {
        p.queue_family_properties()
            .iter()
            .enumerate()
            .position(|(i, q)| {
                q.queue_flags.intersects(QueueFlags::GRAPHICS)
                    && p.surface_support(i as u32, &surface).unwrap_or(false)
            })
            .map(|i| (p, i as u32))
    })
    .min_by_key(|(p, _)| match p.properties().device_type {
        PhysicalDeviceType::DiscreteGpu => 0,
        PhysicalDeviceType::IntegratedGpu => 1,
        PhysicalDeviceType::VirtualGpu => 2,
        PhysicalDeviceType::Cpu => 3,
        PhysicalDeviceType::Other => 4,
        _ => 5,
    })
    .expect("Failed to find a suitable device.");

論理デバイスの作成

Vulkanoでは論理デバイスを作成するときに、一緒にリクエストしたキューがイテレータで帰ってくるらしい。

// キューがイテレーターで帰ってくる
let (device, mut queues) = Device::new(
    physical_device,
    DeviceCreateInfo {
        enabled_extensions: device_extensions,
        queue_create_infos: vec![QueueCreateInfo {
            queue_family_index,
            ..Default::default()
        }],
        ..Default::default()
    },
)?;
// 今回はキューを1つしかリクエストしていないので最初の要素を取り出す
let queue = queues.next().unwrap();
KaraKara

スワップチェーンの作成

Vulkanoではスワップチェーンを作成するときに画面上に表示されるimageも一緒に返却される。
surface_capabilities.min_image_countは一部のドライバでフルスクリーン時に1が返されることがあるらしいので、最小値を2にしておく。

let (mut swapchain, images) = {
  let surface_capabilities = device
      .physical_device()
      .surface_capabilities(&surface, Default::default())?;
  let image_format = device
      .physical_device()
      .surface_formats(&surface, Default::default())?[0]
      .0;

  Swapchain::new(
      device.clone(),
      surface,
      SwapchainCreateInfo {
          min_image_count: surface_capabilities.min_image_count.max(2),
          image_format,
          image_extent: window.inner_size().into(),
          image_usage: ImageUsage::COLOR_ATTACHMENT,
          composite_alpha: surface_capabilities
              .supported_composite_alpha
              .into_iter()
              .next()
              .unwrap(),
          ..Default::default()
      },
  )?
};
KaraKara

頂点バッファの作成

GPU側に頂点情報を渡すために頂点バッファを作成する。
vulkanoではメモリをアロケーターを通して管理しているため、安全に使用することができるらしい。

memory_type_filterについては、よくわかっていないのでまた調べる。

#[derive(BufferContents, vertex_input::Vertex)]
#[repr(C)]
struct Vertex {
    #[format(R32G32_SFLOAT)]
    position: [f32; 2],
}

let memory_allocator = Arc::new(StandardMemoryAllocator::new_default(device.clone()));

let vertices = [
    Vertex {
        position: [-0.5, -0.25],
    },
    Vertex {
        position: [0.0, 0.5],
    },
    Vertex {
        position: [0.25, -0.1],
    },
];
let vertex_buffer = Buffer::from_iter(
    memory_allocator,
    BufferCreateInfo {
        usage: BufferUsage::VERTEX_BUFFER,
        ..Default::default()
    },
    AllocationCreateInfo {
        memory_type_filter: MemoryTypeFilter::PREFER_DEVICE
            | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
        ..Default::default()
    },
    vertices,
)?;
KaraKara

シェーダーモジュールの作成

vulkanoではvulkano_shadershader!マクロを使用して簡単にシェーダーモジュールを記述できる。

mod vs {
    vulkano_shaders::shader! {
        ty: "vertex",
        path: "src/shaders/triangle.vert"
    }
}

mod fs {
    vulkano_shaders::shader! {
        ty: "fragment",
        path: "src/shaders/triangle.frag"
    }
}
src/shaders/triangle.vert
#version 450

layout(location = 0) in vec2 position;

void main() {
  gl_Position = vec4(position, 0.0, 1.0);
}
src/shaders/triangle.frag
#version 450

layout(location = 0) out vec4 f_color;

void main() {
  f_color = vec4(1.0, 0.0, 0.0, 1.0);
}
KaraKara

レンダーパスと描画パイプラインの作成

レンダーパスとは描画の処理順序を記述したもので、vulkanoではマクロを使って簡単に記述できるようになっている。
https://chaosplant.tech/do/vulkan/3-3/

パイプラインとは、頂点シェーダーとかテッセレーションとかの演算順序を記述するオブジェクトのようなもの。
また、subpassというVulkanには存在しない構造体が出てきているが、実際はRenderPasssubpass indexのタプルである。
https://chaosplant.tech/do/vulkan/3-4/

let render_pass = vulkano::single_pass_renderpass!(
    device.clone(),
    attachments: {
        color: {
            format: swapchain.image_format(),
        samples: 1,
        load_op: Clear,
        store_op: Store,
        }
    },
    pass: {
        color: [color],
        depth_stencil: {}
    }
)?;

let pipeline = {
    let vs = vs::load(device.clone())?.entry_point("main").unwrap();
    let fs = fs::load(device.clone())?.entry_point("main").unwrap();

    let vertx_input_state = <Vertex as vertex_input::Vertex>::per_vertex()
        .definition(&vs.info().input_interface)?;
    let stages = [
        PipelineShaderStageCreateInfo::new(vs),
        PipelineShaderStageCreateInfo::new(fs),
    ];

    let layout = PipelineLayout::new(
        device.clone(),
        PipelineDescriptorSetLayoutCreateInfo::from_stages(&stages)
            .into_pipeline_layout_create_info(device.clone())?,
    )?;

    let subpass = Subpass::from(render_pass.clone(), 0).unwrap();

    GraphicsPipeline::new(
        device.clone(),
        None,
        GraphicsPipelineCreateInfo {
            stages: stages.into_iter().collect(),
            // 頂点バッファからどのようにして頂点を読み込むかを指定
            vertex_input_state: Some(vertx_input_state),
            // デフォルトでは三角形
            input_assembly_state: Some(InputAssemblyState::default()),
            viewport_state: Some(ViewportState::default()),
            // 多分かリングを行うとか行わないとか
            rasterization_state: Some(RasterizationState::default()),
            // マルチサンプリングの設定
            multisample_state: Some(MultisampleState::default()),
            // カラーブレンドの設定
            color_blend_state: Some(ColorBlendState::with_attachment_states(
                subpass.num_color_attachments(),
                ColorBlendAttachmentState::default(),
            )),
            dynamic_state: [DynamicState::Viewport].into_iter().collect(),
            subpass: Some(subpass.into()),
            ..GraphicsPipelineCreateInfo::layout(layout)
        },
    )?
};