vulkano (rust) 画一个三角形 (vulkan 渲染窗口初始化 (Linux) 下篇)

news2024/12/25 4:12:52

上文说到, vulkan 相比 OpenGL (ES), 更加贴近底层硬件, 许多东西需要应用软件手动管理, 所以 vulkan 的初始化过程比较麻烦, 或者说学习曲线比较陡峭. 但是, 这种麻烦是一次性的, 一旦学会了, 就能开始享受 vulkan 的诸多好处啦 ~

本文以绘制一个三角形为例, 介绍 vulkan 的初始化过程和基础使用.

rust 编程语言对 vulkan API 的封装库有好几个, 各自有不同的特点. 本文选择使用的是 vulkano 库.

本文的知识主要来自 vulkano bookVulkano Tutorial (链接见下方 参考资料), 在此表示感谢 ! 这是两个很好的 rust vulkan 入门学习资料, 强烈推荐 ! 限于篇幅, 本文对一些知识点的解释可能不是很清楚, 这两个资料可以作为很好的补充.


本内容太长, 分为上下两篇文章:

  • 《rust GTK4 窗口创建与 wayland Subsurface (vulkan 渲染窗口初始化 (Linux) 上篇)》
  • (本文) 《vulkano (rust) 画一个三角形 (vulkan 渲染窗口初始化 (Linux) 下篇)》

这里是 穷人小水滴, 专注于 穷人友好型 低成本技术. (本文为 60 号作品. )


相关文章:

  • 《rust GTK4 窗口创建与 wayland Subsurface (vulkan 渲染窗口初始化 (Linux) 上篇)》 https://blog.csdn.net/secext2022/article/details/142300776

参考资料:

  • https://vulkano.rs/
  • https://taidaesal.github.io/vulkano_tutorial/
  • https://crates.io/crates/vulkano

目录

  • 1 测试代码
  • 2 vulkan 初始化过程
    • 2.1 加载 vulkan 库 (VulkanLibrary)
    • 2.2 创建 vulkan 实例 (Instance)
    • 2.3 枚举并选择物理设备 (PhysicalDevice)
    • 2.4 创建 vulkan 设备和队列 (Device, Queue)
    • 2.5 创建内存分配器和窗口表面 (StandardMemoryAllocator, Surface)
  • 3 vulkan 绘制一个三角形
    • 3.1 创建交换链 (Swapchain, Image)
    • 3.2 加载着色器 (PipelineShaderStageCreateInfo)
    • 3.3 创建渲染过程 (RenderPass, Subpass, PipelineLayout)
    • 3.4 创建帧缓冲区 (Framebuffer)
    • 3.5 创建图形管线 (GraphicsPipeline)
    • 3.6 创建命令缓冲区 (StandardCommandBufferAllocator, Subbuffer, PrimaryAutoCommandBuffer)
    • 3.7 执行命令 (绘制)
    • 3.8 运行测试
  • 4 总结与展望

1 测试代码

首先来看一下测试用的调用代码, 对 vulkan 初始化和使用的整体过程有个简单的了解.

文件 pmse/src/main.rs:

//! pmse-bin
#![deny(unsafe_code)]

use std::sync::Arc;

use pmse_gtk::{pmse_gtk_main, Cb, ExitCode, HandleBox};
use pmse_render::{PmseRenderInit, PmseRenderTest};

#[derive(Debug, Clone)]
struct 回调 {
    pri: PmseRenderInit,
}

impl Cb for 回调 {
    fn cb(&self, h: HandleBox) {
        let pr = self.pri.clone().init_w(h.into()).unwrap();
        let t = PmseRenderTest::new(pr, (1280, 720)).unwrap();
        t.draw().unwrap();
        // TODO
    }
}

fn main() -> ExitCode {
    let pri = PmseRenderInit::vulkan().unwrap();
    let 回调: Arc<Box<dyn Cb>> = Arc::new(Box::new(回调 { pri }));

    pmse_gtk_main(
        "io.github.fm_elpac.pmse_bin".into(),
        "测试 (vulkan) 穷人小水滴".into(),
        (1280, 720, 62, 56),
        (44, 8, 8, 8),
        回调,
    )
}

此处的代码在上一篇文章的测试代码中, 增加了一些 vulkan 相关的部分. 重点有:

  • (1) PmseRenderInit::vulkan() 这个函数对 vulkan 进行初步初始化.

  • (2) PmseRenderInit.init_w() 在创建和显示 GTK4 窗口之后 (初始化 wayland Subsurface 之后), 对 vulkan 进行进一步的初始化.

  • (3) PmseRenderTest::new() 对测试渲染部分进行初始化 (画一个三角形).

  • (4) PmseRenderTest.draw() 进行实际的绘制.

注意, 这只是窝选择的封装方式, 仅供参考. 主要目的是把 “vulkan 相关代码” 和 “窗口相关代码” 互相隔离, 各自封装为模块 (从而做到 “高内聚, 低耦合”).

vulkan 其实可以单独运行, 不需要窗口, 但是这样做有一个限制, 就是 vulkan 渲染的内容不能用窗口显示出来. 将 vulkan 和窗口关联起来, 只是为了显示渲染的结果, 并不是 vulkan 必须的.


文件 pmse-render/Cargo.toml:

[package]
name = "pmse-render"
version = "0.1.0-a1"
edition = "2021"
license = "LGPL-3.0-or-later"

[dependencies]
vulkano = "^0.34.1"
vulkano-shaders = "^0.34.0"

# vulkano version
raw-window-handle = "0.5"

这是使用的主要依赖软件包.

2 vulkan 初始化过程

本章节对这一部分代码相关的 vulkan 初始化过程进行介绍, 文件 pmse-render/src/vulkan_init.rs:

//! vulkan 初始化
use std::error::Error;
use std::sync::Arc;

use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
use vulkano::{
    device::{
        physical::PhysicalDevice, Device, DeviceCreateInfo, DeviceExtensions, Queue,
        QueueCreateInfo, QueueFlags,
    },
    instance::{Instance, InstanceCreateInfo},
    memory::allocator::StandardMemoryAllocator,
    swapchain::Surface,
    VulkanLibrary,
};

use crate::E;

/// 正在初始化的 vulkan 渲染器
#[derive(Debug, Clone)]
pub struct PmseRenderInit {: Arc<VulkanLibrary>,
}

impl PmseRenderInit {
    /// 初始化 vulkan (加载 vulkan 库 .so)
    pub fn vulkan() -> Result<Self, Box<dyn Error>> {
        // debug
        println!("init vulkan .. .");

        let= VulkanLibrary::new()?;
        Ok(Self {})
    }

    /// 窗口初始化, 传入窗口
    pub fn init_w(
        self,
        w: Arc<impl HasRawDisplayHandle + HasRawWindowHandle + Send + Sync + 'static>,
    ) -> Result<PmseRenderHost, Box<dyn Error>> {
        let 实例扩展 = Surface::required_extensions(w.as_ref());
        // 创建 vulkan 实例
        let 实例 = Instance::new(
            self..clone(),
            InstanceCreateInfo {
                enabled_extensions: 实例扩展,
                ..Default::default()
            },
        )?;

        // 创建设备队列
        let 设备扩展 = DeviceExtensions {
            khr_swapchain: true,
            ..DeviceExtensions::empty()
        };
        let (物理设备, 队列序号) = 选择设备(&实例, 设备扩展.clone())?;
        let (设备, 队列) = 创建设备队列(&物理设备, 队列序号, 设备扩展)?;

        // 创建内存分配器
        let ma = Arc::new(StandardMemoryAllocator::new_default(设备.clone()));
        // 创建窗口表面
        let 表面 = Surface::from_window(实例.clone(), w)?;

        // 初始化 (这部分) 完成
        Ok(PmseRenderHost::new(物理设备, 设备, 队列, ma, 表面))
    }
}

2.1 加载 vulkan 库 (VulkanLibrary)

        let= VulkanLibrary::new()?;

使用 vulkan 的第一步, 就是加载系统上安装的 vulkan 库. 一般在安装显卡驱动的时候, 这个就已经安装好了.

2.2 创建 vulkan 实例 (Instance)

加载 vulkan 库之后, 就要创建 vulkan 实例了. 如果想要使用窗口显示渲染结果, 就要等到创建窗口之后, 再来创建 vulkan 实例.

    /// 窗口初始化, 传入窗口
    pub fn init_w(
        self,
        w: Arc<impl HasRawDisplayHandle + HasRawWindowHandle + Send + Sync + 'static>,
    ) -> Result<PmseRenderHost, Box<dyn Error>> {
        let 实例扩展 = Surface::required_extensions(w.as_ref());
        // 创建 vulkan 实例
        let 实例 = Instance::new(
            self..clone(),
            InstanceCreateInfo {
                enabled_extensions: 实例扩展,
                ..Default::default()
            },
        )?;

传入参数 w 就是上一篇文章中, 在初始化 wayland Subsurface 之后, 获取的窗口原始指针.

为了让窗口能够显示 vulkan 渲染的结果, 需要启用 vulkan 实例的相应扩展. 我们使用 Surface::required_extensions() 函数, 来获取所需的扩展, 然后创建 vulkan 实例.

2.3 枚举并选择物理设备 (PhysicalDevice)

        let 设备扩展 = DeviceExtensions {
            khr_swapchain: true,
            ..DeviceExtensions::empty()
        };
        let (物理设备, 队列序号) = 选择设备(&实例, 设备扩展.clone())?;

在一个系统上, 可能同时安装有多个支持 vulkan 的硬件设备, 比如多个 GPU (比如, 集成显卡, 独立显卡), 甚至 CPU 也可以进行软件渲染. 在这里需要选择使用哪个 vulkan 设备. 其中 khr_swapchain 是设备需要支持的扩展 (交换链), 这个用于窗口显示渲染结果.

/// 选择 vulkan 设备
fn 选择设备(
    实例: &Arc<Instance>,
    扩展: DeviceExtensions,
) -> Result<(Arc<PhysicalDevice>, u32), Box<dyn Error>> {
    // TODO 优化设备选择功能

    // 列出 (枚举) vulkan 设备
    let 设备列表 = 实例
        .enumerate_physical_devices()?
        .filter(|p| p.supported_extensions().contains(&扩展));
    let mut d1: Option<Arc<PhysicalDevice>> = None;
    for i in 设备列表 {
        // 输出设备列表, 选择第一个 vulkan 设备
        println!("  {}", i.properties().device_name);
        if d1.is_none() {
            d1.replace(i);
        }
    }
    let 设备 = d1.ok_or(E("ERROR vulkan list device".into()))?;

    // 列出 (查找) vulkan 队列
    for f in 设备.queue_family_properties() {
        println!("vulkan device queue {:?}", f.queue_count);
    }
    let queue_family_index = 设备
        .queue_family_properties()
        .iter()
        .enumerate()
        .position(|(_i, q)| q.queue_flags.contains(QueueFlags::GRAPHICS))
        .ok_or(E("ERROR vulkan find queue".into()))? as u32;
    println!("vulkan queue index {}", queue_family_index);

    Ok((设备, queue_family_index))
}

此处给出的是一种非常简单的设备选择方法, 仅供参考, 后续还能继续优化. 也就是选择满足要求的第一个设备.

此处同时需要 (选择) 找出 vulkan 队列的序号, 此处选择支持图形渲染 (QueueFlags::GRAPHICS) 的队列, 并返回队列序号.

vulkan 队列用来提交 vulkan 命令, 也就是让 GPU 干活的一个接口. 同一个 vulkan 设备可能同时具有多种队列, 每种类型的队列也可能有多个, 应用软件需要选择使用合适的队列.

2.4 创建 vulkan 设备和队列 (Device, Queue)

        let (设备, 队列) = 创建设备队列(&物理设备, 队列序号, 设备扩展)?;

上面选择好了使用的 vulkan 物理设备, 以及队列序号, 接下来就要创建 vulkan 设备了.

/// 创建 vulkan 设备, 队列
fn 创建设备队列(
    设备: &Arc<PhysicalDevice>,
    queue_family_index: u32,
    enabled_extensions: DeviceExtensions,
) -> Result<(Arc<Device>, Arc<Queue>), Box<dyn Error>> {
    let (d, mut 队列) = Device::new(
        设备.clone(),
        DeviceCreateInfo {
            queue_create_infos: vec![QueueCreateInfo {
                queue_family_index,
                ..Default::default()
            }],
            enabled_extensions,
            ..Default::default()
        },
    )?;
    let q = 队列.next().unwrap();
    Ok((d, q))
}

简单粗暴, 根据前面的物理设备和队列序号, 直接创建就完事了, 注意启用设备扩展. 在 vulkan 里面, 核心功能是最基本的, 很多功能需要使用之前, 都要启用对应的 扩展.

此处只使用了一个 vulkan 队列, 但是在复杂的应用中, 可能同时使用多个 vulkan 队列.

2.5 创建内存分配器和窗口表面 (StandardMemoryAllocator, Surface)

        // 创建内存分配器
        let ma = Arc::new(StandardMemoryAllocator::new_default(设备.clone()));
        // 创建窗口表面
        let 表面 = Surface::from_window(实例.clone(), w)?;

内存分配器用来分配 (管理) vulkan 设备的内存, 也就是 GPU 显存. 没错, 这就是 vulkan 的强大功能之一: 显存由应用软件进行手动管理. 这样应用软件在内存 (显存) 的分配和使用方面, 就能做更多的优化, 容易达到更高的性能, 而不必依赖太多显卡驱动的优化.

此处使用 vulkano 提供的 StandardMemoryAllocator 分配器, 这是一个通用的内存分配器, 如果没有特别的理由, 默认选这个就好了.

此处创建的 vulkan 表面, 就对应上一篇文章中创建的 wayland 表面 (Subsurface), 也就是 vulkan 渲染结果要显示到的地方.

3 vulkan 绘制一个三角形

本章节对这一部分代码相关的 vulkan 初始化和使用进行介绍, 文件 pmse-render/src/render_test.rs:

//! 渲染测试
use std::error::Error;
use std::sync::Arc;

use vulkano::{
    buffer::{Buffer, BufferContents, BufferCreateInfo, BufferUsage, Subbuffer},
    command_buffer::{
        allocator::StandardCommandBufferAllocator, AutoCommandBufferBuilder, CommandBufferUsage,
        PrimaryAutoCommandBuffer, RenderPassBeginInfo, SubpassBeginInfo, SubpassContents,
        SubpassEndInfo,
    },
    device::{Device, Queue},
    memory::allocator::{AllocationCreateInfo, MemoryTypeFilter, StandardMemoryAllocator},
    pipeline::{
        graphics::{
            color_blend::{ColorBlendAttachmentState, ColorBlendState},
            input_assembly::InputAssemblyState,
            multisample::MultisampleState,
            rasterization::RasterizationState,
            vertex_input::{Vertex, VertexDefinition},
            viewport::{Viewport, ViewportState},
            GraphicsPipelineCreateInfo,
        },
        layout::PipelineDescriptorSetLayoutCreateInfo,
        GraphicsPipeline, PipelineLayout, PipelineShaderStageCreateInfo,
    },
    render_pass::{Framebuffer, RenderPass, Subpass},
    shader::EntryPoint,
    swapchain::Swapchain,
};

use crate::shader;
use crate::util::{交换链执行, 创建命令缓冲分配器};
use crate::E;
use crate::{PmseRenderHost, PmseRenderSc};

// 省略

/// vulkan 渲染测试
pub struct PmseRenderTest {
    h: PmseRenderHost,
    /// 交换链
    sc: PmseRenderSc,
    /// (no Debug) 命令缓冲区
    命令: Vec<Arc<PrimaryAutoCommandBuffer>>,
}

impl PmseRenderTest {
    /// 初始化
    pub fn new(h: PmseRenderHost, size: (u32, u32)) -> Result<Self, Box<dyn Error>> {
        // 创建交换链
        let mut sc = PmseRenderSc::new(&h, size.into())?;

        let (阶段, 顶点入口) = 加载着色器(h.d())?;
        let (渲染过程, 分过程, 布局) = 创建渲染过程(h.d(), &阶段, sc.sc())?;
        // 初始化 帧缓冲区
        sc.init_framebuffer(&渲染过程)?;

        let 视口 = Viewport {
            offset: [0.0, 0.0],
            extent: [size.0 as f32, size.1 as f32],
            depth_range: 0.0..=1.0,
        };
        let 管线 = 创建图形管线(h.d(), 顶点入口, 阶段, 视口, 分过程, 布局)?;

        let ca = 创建命令缓冲分配器(h.d());
        let 顶点数据 = 创建顶点缓冲区(h.ma())?;

        let mut 命令: Vec<Arc<PrimaryAutoCommandBuffer>> = Vec::new();
        for i in sc.framebuffer() {
            命令.push(创建命令缓冲区(&ca, h.q(), &管线, i, &顶点数据)?);
        }

        Ok(Self { h, sc, 命令 })
    }

    /// vulkan 绘制 测试
    pub fn draw(&self) -> Result<(), Box<dyn Error>> {
        // debug
        println!("vulkan_test");
        交换链执行(self.h.d(), self.h.q(), &self.命令, self.sc.sc())?;
        Ok(())
    }
}

3.1 创建交换链 (Swapchain, Image)

impl PmseRenderTest {
    /// 初始化
    pub fn new(h: PmseRenderHost, size: (u32, u32)) -> Result<Self, Box<dyn Error>> {
        // 创建交换链
        let mut sc = PmseRenderSc::new(&h, size.into())?;

为了在窗口中显示内容, 就要创建 交换链 (Swapchain).

交换链可以理解为几张循环重复使用的画布, 其中一张的内容用来屏幕显示, 一张用来 vulkan 绘制, 画好了一个画面之后, 交换画布, 屏幕显示新的内容. 至少需要 2 张画布, 才可以实现这种工作方式, 但是实际可能使用更多张画布, 来取得更好的性能.

文件 pmse-render/src/swapchain.rs:

//! 交换链 (swapchain) 创建/初始化
use std::error::Error;
use std::sync::Arc;

use vulkano::{
    device::{physical::PhysicalDevice, Device},
    image::{view::ImageView, Image, ImageUsage},
    render_pass::{Framebuffer, FramebufferCreateInfo, RenderPass},
    swapchain::{Surface, Swapchain, SwapchainCreateInfo},
};

use crate::PmseRenderHost;

/// 交换链 (swapchain) 管理
#[derive(Debug)]
pub struct PmseRenderSc {
    /// (no Clone) 交换链
    交换链: Arc<Swapchain>,
    /// 图像
    图像: Vec<Arc<Image>>,
    /// 帧缓冲区
    帧缓冲: Vec<Arc<Framebuffer>>,
}

impl PmseRenderSc {
    pub fn new(h: &PmseRenderHost, size: [u32; 2]) -> Result<Self, Box<dyn Error>> {
        let (交换链, 图像) = 创建交换链(h.d(), h.p(), h.s(), size)?;
        // 稍后初始化 帧缓冲
        Ok(Self {
            交换链,
            图像,
            帧缓冲: vec![],
        })
    }

    /// 获取交换链
    pub fn sc(&self) -> &Arc<Swapchain> {
        &self.交换链
    }

    /// 创建帧缓冲区
    pub fn init_framebuffer(&mut self, rp: &Arc<RenderPass>) -> Result<(), Box<dyn Error>> {
        self.帧缓冲 = 创建帧缓冲区(&self.图像, rp)?;
        Ok(())
    }

其中:

/// 创建交换链
fn 创建交换链(
    设备: &Arc<Device>,
    物理设备: &Arc<PhysicalDevice>,
    表面: &Arc<Surface>,
    image_extent: [u32; 2],
) -> Result<(Arc<Swapchain>, Vec<Arc<Image>>), Box<dyn Error>> {
    let 能力 = 物理设备.surface_capabilities(表面, Default::default())?;
    let composite_alpha = 能力.supported_composite_alpha.into_iter().next().unwrap();
    let image_format = 物理设备.surface_formats(表面, Default::default())?[0].0;
    println!("  image format: {:?}", image_format);
    println!("  min_image_count {}", 能力.min_image_count);

    Ok(Swapchain::new(
        设备.clone(),
        表面.clone(),
        SwapchainCreateInfo {
            min_image_count: 能力.min_image_count + 1,
            image_format,
            image_extent,
            image_usage: ImageUsage::COLOR_ATTACHMENT,
            composite_alpha,
            ..Default::default()
        },
    )?)
}

传入参数 表面 就是前面说的窗口表面, image_extent 是显示区域的宽高 (像素).

首先查询表面的参数 (能力), 获取透明度格式 (composite_alpha), 表面的图像格式 (image_format), 然后创建交换链. 注意, 表面支持的透明度以及图像格式, 也可能有多种, 此处使用的是非常简单的选择方法: 选择第一个.

创建交换链的同时, 会返回一组 vulkan 图像 (Image), 也就是已经分配好的内存区域, 之后绘制的目标就是这个.

3.2 加载着色器 (PipelineShaderStageCreateInfo)

着色器 (shader) 是一段在 GPU 上运行的代码, 着色器分为多种类型, 比如: 顶点着色器 (vertex shader), 片段着色器 (fragment shader), 计算着色器 (compute shader), 等等.

在本文画一个三角形的应用场景下, 我们使用两种着色器: 顶点着色器, 用来计算每个顶点的位置. 片段着色器, 用来计算每个像素点的颜色. GPU 是大规模并行计算的结构, 所以顶点着色器会为每个顶点执行一次 (在本文中, 为三角形的每个顶点运行一次, 一共运行 3 次), 片段着色器为每个像素执行一次 (在本文中, 为三角形覆盖的屏幕上的每个像素运行一次, 大约运行几十万次).

vulkan 的着色器被编译成 SPIR-V 的二进制格式, 加载到 vulkan 驱动中. 所以可以使用多种 “高级语言” 来编写着色器, 然后把源代码编译成 SPIR-V 格式, 就能使用了.

其中比较常用的着色器编程语言是 GLSL (OpenGL Shader Language), vulkano 具有 GLSL 着色器的编译功能, 使用起来比较方便.


首先定义一个顶点的数据结构 (rust):

/// 顶点数据结构
#[derive(Debug, Clone, BufferContents, Vertex)]
#[repr(C)]
pub struct 顶点 {
    /// 位置坐标 (x, y, z)
    #[format(R32G32B32_SFLOAT)]
    p: [f32; 3],
    /// 颜色
    #[format(R32G32B32_SFLOAT)]
    color: [f32; 3],
}

impl 顶点 {
    pub fn new(p: [f32; 3], color: [f32; 3]) -> Self {
        Self { p, color }
    }
}

每个顶点具有 2 个数据变量: 位置坐标 (p), 颜色 (color). 都分别使用 3 个单精度浮点数 (f32) 来表示.

文件 pmse-render/shader/test_v.glsl:

#version 460
// 顶点着色器 (vertex shader)

layout(location = 0) in vec3 p;
layout(location = 1) in vec3 color;

layout(location = 0) out vec3 out_color;

void main() {
  gl_Position = vec4(p, 1.0);
  out_color = color;
}

这是顶点着色器的代码 (GLSL), 有 2 输入变量 (p, color), 和上面对应. 有一个输出变量 out_color, 这个会被传递给下面的片段着色器. gl_Position 是一个特殊变量 (GLSL 内置变量), 表示顶点的位置.

此处的顶点着色器代码非常简单, 把位置和颜色原样输出.

文件 pmse-render/shader/test_f.glsl:

#version 460
// 片段着色器 (fragment shader)

layout(location = 0) in vec3 in_color;

layout(location = 0) out vec4 f_color;

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

这是片段着色器的代码 (GLSL), 有一个输入变量 in_color, 来自上面的顶点着色器. 有一个输出变量 f_color, 表示像素的颜色.

此处的代码也很简单, 把颜色值原样输出.

文件 pmse-render/src/shader.rs:

//! vulkan shader (GLSL)

/// 顶点着色器
pub mod test_v {
    vulkano_shaders::shader! {
        ty: "vertex",
        path: "shader/test_v.glsl",
    }
}

/// 片段着色器
pub mod test_f {
    vulkano_shaders::shader! {
        ty: "fragment",
        path: "shader/test_f.glsl",
    }
}

这是使用 vulkano 编译着色器的代码.


        let (阶段, 顶点入口) = 加载着色器(h.d())?;

其中:

/// 初始化 (加载/编译) 着色器
fn 加载着色器(
    设备: &Arc<Device>,
) -> Result<([PipelineShaderStageCreateInfo; 2], EntryPoint), Box<dyn Error>> {
    let 顶点着色器 = shader::test_v::load(设备.clone())?;
    let 片段着色器 = shader::test_f::load(设备.clone())?;

    // 着色器 入口函数
    let 顶点入口 = 顶点着色器
        .entry_point("main")
        .ok_or(E("ERROR vulkan shader vs main".into()))?;
    let 片段入口 = 片段着色器
        .entry_point("main")
        .ok_or(E("ERROR vulkan shader fs main".into()))?;

    let 阶段 = [
        PipelineShaderStageCreateInfo::new(顶点入口.clone()),
        PipelineShaderStageCreateInfo::new(片段入口),
    ];

    Ok((阶段, 顶点入口))
}

使用这样的代码把着色器代码加载到 vulkan 设备, 然后获取着色器执行入口函数, 指定函数名称 (main). 着色器函数名称可以随便, 但是习惯用 main.

其中 阶段 数据的意思是, 在 vulkan 渲染的过程中, 将使用这两个着色器.

3.3 创建渲染过程 (RenderPass, Subpass, PipelineLayout)

在 vulkan 中, 渲染过程 (RenderPass) 描述了如何绘制一帧 (一张图片), 一个渲染过程可能含有多个分过程 (Subpass), 每个分过程都可以使用不同的着色器, 以及输入输出数据.

        let (渲染过程, 分过程, 布局) = 创建渲染过程(h.d(), &阶段, sc.sc())?;

其中:

/// 创建渲染过程
fn 创建渲染过程(
    设备: &Arc<Device>,
    阶段: &[PipelineShaderStageCreateInfo; 2],
    交换链: &Arc<Swapchain>,
) -> Result<(Arc<RenderPass>, Subpass, Arc<PipelineLayout>), Box<dyn Error>> {
    let 布局 = PipelineLayout::new(
        设备.clone(),
        PipelineDescriptorSetLayoutCreateInfo::from_stages(阶段)
            .into_pipeline_layout_create_info(设备.clone())?,
    )?;
    let 渲染过程 = vulkano::single_pass_renderpass!(
        设备.clone(),
        attachments: {
            color: {
                format: 交换链.image_format(),
                samples: 1,
                load_op: Clear,
                store_op: Store,
            }
        },
        pass: {
            color: [color],
            depth_stencil: {},
        }
    )?;
    let 分过程 = Subpass::from(渲染过程.clone(), 0).unwrap();

    Ok((渲染过程, 分过程, 布局))
}

首先, 从上面的着色器 阶段 (PipelineShaderStageCreateInfo) 获取管线的 布局 (PipelineLayout), 然后使用宏 vulkano::single_pass_renderpass!() 来创建一个简单的 渲染过程 (RenderPass), 这个渲染过程只有一个 分过程 (Subpass).

创建时, 需要指定附加数据 (attachments) 以及格式, 此处指定了一个附加数据 (color), 格式为交换链的格式 (也就是窗口表面的格式). 我们要绘制到这里, 然后通过窗口显示到屏幕上.

不使用深度缓冲区 (以及深度检测) (depth_stencil). samples: 1 表示采样数为 1 (也就是不使用 MSAA 等抗锯齿处理). load_op: Clear 表示加载时清除颜色值 (也就是使用设定的背景色). store_op: Store 表示需要保存计算结果.

3.4 创建帧缓冲区 (Framebuffer)

帧缓冲区是配合渲染过程使用的, 帧缓冲区包含了渲染过程中使用的输入输出数据.

        sc.init_framebuffer(&渲染过程)?;

其中 (详见 3.1 创建交换链 章节):

/// 创建帧缓冲区
fn 创建帧缓冲区(
    图像: &Vec<Arc<Image>>,
    渲染过程: &Arc<RenderPass>,
) -> Result<Vec<Arc<Framebuffer>>, Box<dyn Error>> {
    let mut o: Vec<Arc<Framebuffer>> = Vec::new();
    for i in 图像 {
        let 视图 = ImageView::new_default(i.clone())?;
        o.push(Framebuffer::new(
            渲染过程.clone(),
            FramebufferCreateInfo {
                attachments: vec![视图],
                ..Default::default()
            },
        )?)
    }
    Ok(o)
}

此处创建的帧缓冲区, 包含了来自交换链的图像, 用于绘制之后的显示. 此处的每个帧缓冲区, 只包含了一个图像, 其实也可以同时使用多个图像.

3.5 创建图形管线 (GraphicsPipeline)

        let 视口 = Viewport {
            offset: [0.0, 0.0],
            extent: [size.0 as f32, size.1 as f32],
            depth_range: 0.0..=1.0,
        };
        let 管线 = 创建图形管线(h.d(), 顶点入口, 阶段, 视口, 分过程, 布局)?;

其中:

/// 创建图形渲染管线
fn 创建图形管线(
    设备: &Arc<Device>,
    顶点入口: EntryPoint,
    阶段: [PipelineShaderStageCreateInfo; 2],
    视口: Viewport,
    分过程: Subpass,
    布局: Arc<PipelineLayout>,
) -> Result<Arc<GraphicsPipeline>, Box<dyn Error>> {
    let 顶点输入状态 = 顶点::per_vertex().definition(&顶点入口.info().input_interface)?;
    let 管线 = GraphicsPipeline::new(
        设备.clone(),
        None,
        GraphicsPipelineCreateInfo {
            stages: 阶段.into_iter().collect(),
            vertex_input_state: Some(顶点输入状态),
            input_assembly_state: Some(InputAssemblyState::default()),
            viewport_state: Some(ViewportState {
                viewports: [视口].into_iter().collect(),
                ..Default::default()
            }),
            rasterization_state: Some(RasterizationState::default()),
            multisample_state: Some(MultisampleState::default()),
            color_blend_state: Some(ColorBlendState::with_attachment_states(
                分过程.num_color_attachments(),
                ColorBlendAttachmentState::default(),
            )),
            subpass: Some(分过程.into()),
            ..GraphicsPipelineCreateInfo::layout(布局)
        },
    )?;
    Ok(管线)
}

在此处创建图形管线 (GraphicsPipeline), 可以看到, 需要指定一大堆参数. 限于篇幅, 就不一一详细解释了. 其中 视口 (Viewport) 就是屏幕显示的矩形区域 (也就是 vulkan 绘制区域), 需要指定宽高.

这里突出了 vulkan 的一个特点 (优点), 那就是可以配置的参数非常多, 所以应用软件可以做的优化也比较多.

3.6 创建命令缓冲区 (StandardCommandBufferAllocator, Subbuffer, PrimaryAutoCommandBuffer)

        let ca = 创建命令缓冲分配器(h.d());
        let 顶点数据 = 创建顶点缓冲区(h.ma())?;

        let mut 命令: Vec<Arc<PrimaryAutoCommandBuffer>> = Vec::new();
        for i in sc.framebuffer() {
            命令.push(创建命令缓冲区(&ca, h.q(), &管线, i, &顶点数据)?);
        }

上面做了那么多初始化工作, 现在终于可以下达绘制命令了. vulkan 的命令放在 命令缓冲区 中, 然后再批量发送给 GPU 执行, 这样可以提高效率.

文件 pmse-render/src/util.rs:

//! 工具函数
use std::error::Error;
use std::sync::Arc;

use vulkano::{
    command_buffer::{
        allocator::{StandardCommandBufferAllocator, StandardCommandBufferAllocatorCreateInfo},
        PrimaryAutoCommandBuffer,
    },
    device::{Device, Queue},
    swapchain::{self, Swapchain, SwapchainPresentInfo},
    sync::{self, GpuFuture},
    Validated, VulkanError,
};

use crate::E;

/// 创建 命令缓冲区 分配器
pub fn 创建命令缓冲分配器(设备: &Arc<Device>) -> StandardCommandBufferAllocator {
    StandardCommandBufferAllocator::new(
        设备.clone(),
        StandardCommandBufferAllocatorCreateInfo::default(),
    )
}

命令缓冲区分配器类似于前面说的内存分配器, 只不过前面的用于分配数据资源的内存, 此处的用于分配命令缓冲区.


/// 创建 顶点数据缓冲区 (三角形)
fn 创建顶点缓冲区(
    ma: &Arc<StandardMemoryAllocator>,
) -> Result<Subbuffer<[顶点]>, Box<dyn Error>> {
    Ok(Buffer::from_iter(
        ma.clone(),
        BufferCreateInfo {
            usage: BufferUsage::VERTEX_BUFFER,
            ..Default::default()
        },
        AllocationCreateInfo {
            memory_type_filter: MemoryTypeFilter::PREFER_DEVICE
                | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
            ..Default::default()
        },
        vec![
            顶点::new([0.1, 0.8, 0.0], [1.0, 0.0, 0.0]),
            顶点::new([-0.8, -0.6, 0.0], [0.0, 1.0, 0.0]),
            顶点::new([0.9, -0.9, 0.0], [0.0, 0.0, 1.0]),
        ],
    )?)
}

此处创建一个数据缓冲区 (Subbuffer), 存放三角形的顶点数据. 除了顶点数据本身, 还要指定数据的用法, 方便 vulkan 驱动进行优化.

BufferUsage::VERTEX_BUFFER 表示这个缓冲区用来存储顶点数据. MemoryTypeFilter::PREFER_DEVICE 表示优先使用设备内存 (也就是显卡的显存). MemoryTypeFilter::HOST_SEQUENTIAL_WRITE 表示主机 (CPU) 需要顺序写入, 也就是用来加载数据到显卡的显存.


/// 创建 命令缓冲区
fn 创建命令缓冲区(
    ca: &StandardCommandBufferAllocator,
    队列: &Arc<Queue>,
    图形管线: &Arc<GraphicsPipeline>,
    帧缓冲区: &Arc<Framebuffer>,
    顶点缓冲区: &Subbuffer<[顶点]>,
) -> Result<Arc<PrimaryAutoCommandBuffer>, Box<dyn Error>> {
    let mut b = AutoCommandBufferBuilder::primary(
        ca,
        队列.queue_family_index(),
        CommandBufferUsage::MultipleSubmit,
    )?;
    // 渲染命令
    b.begin_render_pass(
        RenderPassBeginInfo {
            clear_values: vec![Some([0.0, 0.0, 0.0, 1.0].into())],
            ..RenderPassBeginInfo::framebuffer(帧缓冲区.clone())
        },
        SubpassBeginInfo {
            contents: SubpassContents::Inline,
            ..Default::default()
        },
    )?
    .bind_pipeline_graphics(图形管线.clone())?
    .bind_vertex_buffers(0, 顶点缓冲区.clone())?
    .draw(顶点缓冲区.len() as u32, 1, 0, 0)?
    .end_render_pass(SubpassEndInfo::default())?;

    Ok(b.build()?)
}

此处使用 AutoCommandBufferBuilder 录制命令. 首先使用 begin_render_pass() 开始渲染过程, 指定清除颜色 (背景颜色), 使用的帧缓冲区. 然后绑定图形管线 (bind_pipeline_graphics), 指定顶点数据 (bind_vertex_buffers), 下达绘制命令 (draw), 最后结束渲染过程 (end_render_pass).

3.7 执行命令 (绘制)

一切准备完毕, 终于可以执行绘制命令了.

    /// vulkan 绘制 测试
    pub fn draw(&self) -> Result<(), Box<dyn Error>> {
        // debug
        println!("vulkan_test");
        交换链执行(self.h.d(), self.h.q(), &self.命令, self.sc.sc())?;
        Ok(())
    }

其中:

/// 从交换链获取一个图像, 绘制
pub fn 交换链执行(
    设备: &Arc<Device>,
    队列: &Arc<Queue>,
    命令: &Vec<Arc<PrimaryAutoCommandBuffer>>,
    交换链: &Arc<Swapchain>,
) -> Result<(), Box<dyn Error>> {
    // 从交换链获取下一个图像
    let (序号, _退化, 获取未来) =
        match swapchain::acquire_next_image(交换链.clone(), None).map_err(Validated::unwrap) {
            Ok(r) => r,
            Err(VulkanError::OutOfDate) => {
                // TODO 重新创建交换链
                println!("ERROR swapchain acquire OutOfDate");
                return Err(Box::new(E("vulkan OutOfDate".into())));
            }
            Err(e) => {
                // TODO unknown error
                println!("ERROR swapchain acquire {}", e);
                return Err(Box::new(E("unknown error".into())));
            }
        };
    // 绘制
    let 执行 = sync::now(设备.clone())
        .join(获取未来)
        .then_execute(队列.clone(), 命令[序号 as usize].clone())?
        .then_swapchain_present(
            队列.clone(),
            SwapchainPresentInfo::swapchain_image_index(交换链.clone(), 序号),
        )
        .then_signal_fence_and_flush();
    // 错误处理
    match 执行.map_err(Validated::unwrap) {
        Ok(f) => {
            f.wait(None)?;
        }
        Err(e) => {
            // TODO unknown error
            println!("ERROR flush {}", e);
        }
    }
    Ok(())
}

首先, 从交换链获取一个空闲的图像 (swapchain::acquire_next_image), 用于绘制. 然后等待 (join), 提交命令缓冲区 (执行命令) (then_execute), 交换链显示绘制的结果 (then_swapchain_present), 同步 GPU (then_signal_fence_and_flush).

这段代码写的很粗糙, 只是勉强能用, 很多地方 (错误处理) 不完善. 有时候, 比如窗口大小改变等, 会导致当前的交换链失效, 需要重新创建交换链, 这段代码没有处理这种情况.

3.8 运行测试

使用 cargo 编译项目:

cargo build -p pmse

运行:

> ./pmse
init vulkan .. .
pmse_gtk_main: (1280, 720, 62, 56) (44, 8, 8, 8) w = 1296, h = 772 (70, 100)
gtk4 backend = Wayland
  WaylandDisplay { inner: TypedObjectRef { inner: 0x55c698face70, type: GdkWaylandDisplay } }
  Connection { backend: Backend { backend: InnerBackend { inner: Inner { state: Mutex { data: ConnectionState { display: 0x55c698fa62c0, owns_display: false, evq: 0x55c699008960, display_id: ObjectId(wl_display@1), last_error: None, known_proxies: {} }, poisoned: false, .. }, dispatch_lock: Mutex { data: Dispatcher, poisoned: false, .. }, debug: false } } } }
  WaylandSurface { inner: TypedObjectRef { inner: 0x55c69901d260, type: GdkWaylandToplevel } }
wayland queue run
wayland gtk4 read
wayland registry global:
  WlCompositor { id: ObjectId(wl_compositor@52), version: 6, data: Some(ObjectData { .. }), backend: WeakBackend { inner: WeakInnerBackend { inner: (Weak) } } }
  WlSubcompositor { id: ObjectId(wl_subcompositor@47), version: 1, data: Some(ObjectData { .. }), backend: WeakBackend { inner: WeakInnerBackend { inner: (Weak) } } }
create subsurface (70, 100)
  Intel(R) HD Graphics 520 (SKL GT2)
vulkan device queue 1
vulkan queue index 0
  image format: R16G16B16A16_SFLOAT
  min_image_count 4
vulkan_test

在这里插入图片描述

然后我们就成功画出了一个三角形, 撒花 ~

咦 ? 上面的顶点数据中, 只是指定了 3 个顶点的颜色, 为什么会画出来一个彩色的三角形呢 ?

嗯, 这是因为, 从顶点着色器传递到片段着色器的数据, 会进行 插值 处理, 所以顶点着色器输出的是单个顶点的颜色, 但是片段着色器接收到的, 就是经过插值之后的渐变颜色啦.

4 总结与展望

本文已经很长了, 所以总结就简单点吧. 本文通过绘制一个三角形, 介绍了 vulkan 的初始化和简单使用. 可以看到, 由于 vulkan 更加接近底层硬件, 初始化过程是比较麻烦的.

本文使用的系统软件环境: ArchLinux (GNOME). 本文相关的完整源代码请见: https://crates.io/crates/pmse-render

vulkan 本身具有很好的跨平台能力, 后续考虑别的平台的 vulkan 初始化, 真正做到 “跨平台”.


本文使用 CC-BY-SA 4.0 许可发布.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2141339.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024最新版,人大赵鑫老师《大语言模型》新书pdf分享

本书主要面向希望系统学习大语言模型技术的读者&#xff0c;将重点突出核心概念与 算法&#xff0c;并且配以示例与代码&#xff08;伪代码&#xff09;帮助读者理解特定算法的实现逻辑。由于大语言模型技术的快速更迭&#xff0c;本书无法覆盖所有相关内容&#xff0c;旨在梳理…

瓶中水位检测系统源码分享

瓶中水位检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…

【数据结构】图的概念和存储结构

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《C游记》《进击的C》《Linux迷航》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 引言一、图的概念二、图的存储结构2.1 邻接矩阵2.1.1 成员变量与默认成员函数2.1.2 GetIndex2.1.3 AddEdge2.1.4 Pr…

使用 Java 初步搭建简单Spring 项目框架:

一、准备工作 安装 Java Development Kit (JDK)&#xff0c;确保环境变量配置正确。 安装一个集成开发环境&#xff08;IDE&#xff09;&#xff0c;如 IntelliJ IDEA 或 Eclipse。 二、创建项目——具体步骤 在 ider 中创建一个新的 Maven 项目 使用 Maven&#xff1a; 在…

Linux bash脚本本地开发环境(Git Bash)配置

参考资料 VSCode: Windows 下配置 VSCode运行shellVSCodeを使用したシェルスクリプトの開発環境作成 目录 一. 必备VSCode插件二. 插件配置说明2.1 Bash IDE2.2 Code Runner2.3 shell-format 一. 必备VSCode插件 Bash IDE 该插件为 Bash 脚本提供了一些实用的开发工具和功能&…

鸿蒙 ArkUI组件二

ArkUI组件&#xff08;续&#xff09; 文本组件 在HarmonyOS中&#xff0c;Text/Span组件是文本控件中的一个关键部分。Text控件可以用来显示文本内容&#xff0c;而Span只能作为Text组件的子组件显示文本内容。 Text/Span组件的用法非常简单和直观。我们可以通过Text组件来显…

重生归来之挖掘stm32底层知识(1)——寄存器

概念理解 要使用stm32首先要知道什么是引脚和寄存器。 如下图所示&#xff0c;芯片通过这些金属丝与电路板连接&#xff0c;这些金属丝叫做引脚。一般做软件开发是不需要了解芯片是怎么焊的&#xff0c;只要会使用就行。我们平常通过编程来控制这些引脚的输入和输出&#xff0c…

应用软件系统开发实操二:任务需求描述

工信部软件界信息技术服务业2020、2021、2022年度数据&#xff08;目前只有这3年的完整数据&#xff09;&#xff0c;以SQL的格式&#xff0c;存放在实操平台上&#xff0c;通过浏览器下载的方式获取。获取数据后&#xff0c;采用自己选择的技术对数据进行处理。阅读下面的要求…

php语言基本语法

HP&#xff08;Hypertext Preprocessor&#xff09;是一种广泛使用的开源服务器端脚本语言&#xff0c;特别适合于Web开发。 它能够嵌入到HTML中&#xff0c;执行动态网页内容。 PHP的一些基本语法元素&#xff1a; 1. 基本结构 PHP代码通常嵌入到HTML中&#xff0c;以<…

C/C++实现植物大战僵尸(PVZ)(打地鼠版)

&#x1f680;欢迎互三&#x1f449;&#xff1a;程序猿方梓燚 &#x1f48e;&#x1f48e; &#x1f680;关注博主&#xff0c;后期持续更新系列文章 &#x1f680;如果有错误感谢请大家批评指出&#xff0c;及时修改 &#x1f680;感谢大家点赞&#x1f44d;收藏⭐评论✍ 游戏…

Django_Vue3_ElementUI_Release_003_前端Vue3项目初始化

1. 概念扫盲 Node.js是基于ChromeV8引擎&#xff0c;让JS在服务端运行的开发平台&#xff0c;就是JS的一种解释器WebPack就是模块打包机&#xff0c;把浏览器不能直接运行的拓展语言找到并打包为合适的格式给浏览器直接使用Vue基于WebPack构件项目的&#xff0c;并带有合理默认…

MoCo对比损失

MoCo&#xff08;Momentum Contrast&#xff0c;动量对比学习&#xff09;是一种自监督学习方法&#xff0c;由Facebook AI Research提出&#xff0c;主要用于无监督学习视觉表示。在MoCo中&#xff0c;对比损失&#xff08;Contrastive Loss&#xff09;扮演着至关重要的角色&…

在麒麟操作系统中查看进程运行时间

在麒麟操作系统中查看进程运行时间 1、使用ps命令查看进程运行时间1.1 基本命令结构1.2 示例&#xff1a;查看sshd进程的运行时间 2、总结 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在Linux操作系统中&#xff0c;包括麒麟&#xff08…

使用Mockito进行单元测试

1、单元测试介绍 Mockito和Junit是用于单元测试的常用框架。单元测试即&#xff1a;从最小的可测试单元&#xff08;如函数、方法或类&#xff09;开始&#xff0c;确保每个单元都能按预期工作。单元测试是白盒测试的核心部分&#xff0c;它有助于发现单元内部的错误。 单元测试…

【Hot100】LeetCode—84. 柱状图中最大的矩形

目录 1- 思路题目识别单调栈 2- 实现⭐84. 柱状图中最大的矩形——题解思路 3- ACM 实现 原题链接&#xff1a;84. 柱状图中最大的矩形 1- 思路 题目识别 识别1 &#xff1a;给定一个数组 heights &#xff0c;求解柱状图的最大面积 单调栈 使用 Stack 来实现&#xff0c;遍…

服务器上PFC配置丢失问题排查与解决方案

现象 基于nccl的多轨通信算力中心出现交换机端口出入方向丢包 分析 机间通信使用RoCE网络&#xff0c;为了避免因丢包导致大量重传报文影响训练性能&#xff0c;我们基于PFC和ECN在交换机和服务器配置搭建了无损网络&#xff0c;理论上是不允许丢包的&#xff0c;现在出现交…

时序差分法

一、时序差分法 时序差分是一种用来估计一个策略的价值函数的方法&#xff0c;它结合了蒙特卡洛和动态规划算法的思想。时序差分方法和蒙特卡洛的相似之处在于可以从样本数据中学习&#xff0c;不需要事先知道环境&#xff1b;和动态 规划的相似之处在于根据贝尔曼方程的思想&…

接口测试(十二)

一、前台、后台、数据库三者关系 fiddler抓包是抓取客户端 --> 服务端 发送的的请求接口 开N个网页&#xff0c;只要有对后端发送请求&#xff0c; fiddler是无差别抓取 F12只抓取当前页面的数据 二、接口概念 接口是什么&#xff1f;— 传递数据的通道 测试系统组件间接口…

CC2530实现按键控制LED

实现按钮控制LED1开启和关闭 1配置环境 2扩展资料 通用io和外设io 设置输入输出 设置输入模式 3实例代码 #include "ioCC2530.h"void delay(int n){int i,j;for(i0;i<n;i){for(j0;j<240;j){asm("NOP");asm("NOP");asm("NOP")…

改编pikachu的打靶经历(题目不全)

前言 题目很少&#xff0c;只做了一些。正常版本的&#xff0c;完整的pikachu可参考下面这个师傅写的 https://www.cnblogs.com/henry666/p/16947270.html xss &#xff08;get&#xff09;反射xss 先尝试 1 这里有长度限制&#xff0c;而且&#xff0c;我改了长度&#xf…