Windows (rust) vulkan 画一个三角形: 窗口创建与渲染初始化

news2024/9/27 5:44:25

在每个平台, 每前进一步, 都会出现许多预料之外的困难 (大坑).

本文介绍在 Windows 操作系统之中, 使用 win32 API 创建窗口, 并使用 vulkano (rust) 初始化 vulkan, 绘制一个三角形.

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


相关文章:

  • 《rust GTK4 窗口创建与 wayland Subsurface (vulkan 渲染窗口初始化 (Linux) 上篇)》 https://blog.csdn.net/secext2022/article/details/142300776
  • 《vulkano (rust) 画一个三角形 (vulkan 渲染窗口初始化 (Linux) 下篇)》 https://blog.csdn.net/secext2022/article/details/142300877
  • 《Android (rust) vulkan (JNI) 画一个三角形: VulkanSurfaceView 初始化》 https://blog.csdn.net/secext2022/article/details/142487409

参考资料:

  • https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
  • https://crates.io/crates/windows/
  • https://crates.io/crates/vulkano/
  • https://www.lunarg.com/vulkan-sdk/
  • https://crates.io/crates/raw-window-handle/
  • https://crates.io/crates/pmse-win

目录

  • 1 窗口代码
    • 1.1 win32 创建窗口
    • 1.2 RawWindowHandle
  • 2 vulkan 渲染测试
  • 3 编译运行
  • 4 总结与展望

1 窗口代码

要想在 Windows 系统编写 rust 程序, 我们可以使用 windows 库: https://crates.io/crates/windows/

微软对 rust 编程语言的支持力度还是很大的, 这个库就是微软官方做的, 可以用来调用 Windows 系统的 API.

虽然已经有了现成的系统接口库, 但是我们很快就会遇到一个十分巨大的困难: Windows API 实在太庞大了. 比如 windows (rust) 库 0.58.0 版本就有 691 个 feature flag (功能开关) ! 也就是说, 仅功能模块就有好几百个, 具体的接口函数和数据结构就更多了. 很容易就迷失在这一大堆功能和函数之中 ~ (晕)

1.1 win32 创建窗口

Windows 操作系统, 其英文名称的含义就是 窗口, 可见窗口在这个操作系统的重要地位.

win32 是 Windows 系统的一套经典 API (编程接口). 大家不要被 win32 这个名称迷惑了, 本文中我们实际编写的是一个 64 位 的程序 (x86_64). win32 只是因为历史原因, 曾经叫 win32 (当时是 32 位程序), 后来习惯了, 就不改了 (所以 64 位程序用的还是 win32, 并没有 “win64” 这种叫法).

Windows 系统的 API 保持了很好的向后兼容性 (这样的设计有好处也有坏处), 所以我们今天创建窗口的方式, 和几十年前的程序差不多, 基本上还是老样子.

文件 pmse-win/src/w.rs:

//! Windows 窗口封装
#![allow(unsafe_code)]

use std::ffi::c_void;

use raw_window_handle::{
    HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, Win32WindowHandle,
    WindowsDisplayHandle,
};
use windows::{
    core::{HSTRING, PCWSTR},
    Win32::{
        Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM},
        Graphics::Gdi::{RedrawWindow, ValidateRect, RDW_INVALIDATE},
        System::LibraryLoader::GetModuleHandleW,
        UI::WindowsAndMessaging::{
            CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, GetWindowLongPtrW,
            LoadCursorW, PostQuitMessage, RegisterClassExW, SetWindowLongPtrW, ShowWindow,
            CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, IDC_ARROW, MSG, SW_SHOWNORMAL, WINDOW_EX_STYLE,
            WINDOW_LONG_PTR_INDEX, WM_DESTROY, WM_PAINT, WM_SIZE, WNDCLASSEXW, WS_CAPTION,
            WS_OVERLAPPED, WS_SYSMENU, WS_VISIBLE,
        },
    },
};

struct 窗口数据 {
    pub 绘制回调: Option<Box<dyn FnMut() -> () + 'static>>,
}

struct 窗口封装 {
    实例: HINSTANCE,
    窗口: HWND,

    数据: Box<窗口数据>,
}

impl 窗口封装 {
    /// 创建窗口
    pub unsafe fn new(宽高: (i32, i32), 标题: String) -> Self {
        let 实例: HINSTANCE = GetModuleHandleW(None).unwrap().into();

        let 窗口类名1 = HSTRING::from("pmse_window");
        let 窗口类名 = PCWSTR(窗口类名1.as_ptr());
        let 窗口类 = WNDCLASSEXW {
            cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
            // SetWindowLongPtrW()
            cbWndExtra: std::mem::size_of::<*const c_void>() as i32,

            hInstance: 实例,
            lpszClassName: 窗口类名,
            lpfnWndProc: Some(pmse_win_wndproc),

            style: CS_HREDRAW | CS_VREDRAW,
            hCursor: LoadCursorW(None, IDC_ARROW).unwrap(),

            ..Default::default()
        };
        // 注册窗口类
        let a = RegisterClassExW(&窗口类);
        if 0 == a {
            panic!("RegisterClassExW()");
        }

        let 标题1 = HSTRING::from(标题);
        let 标题 = PCWSTR(标题1.as_ptr());
        // 创建窗口
        let 窗口 = CreateWindowExW(
            WINDOW_EX_STYLE::default(),
            窗口类名,
            标题,
            // 禁止改变窗口大小
            // WS_OVERLAPPEDWINDOW
            WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_VISIBLE,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            宽高.0,
            宽高.1,
            None,
            None,
            实例,
            None,
        )
        .unwrap();

        // 窗口数据
        let 数据 = Box::new(窗口数据 { 绘制回调: None });
        // 设置数据指针
        let 窗口数据指针: *const _ = &*数据;
        SetWindowLongPtrW(窗口, WINDOW_LONG_PTR_INDEX(0), 窗口数据指针 as isize);

        Self {
            实例, 窗口, 数据
        }
    }

    pub fn 设绘制回调(&mut self, 回调: Option<Box<dyn FnMut() -> () + 'static>>) {
        self.数据.绘制回调 = 回调;
    }

    pub fn 获取指针(&self) -> HandleBox {
        HandleBox::new(self.实例.0 as *mut _, self.窗口.0 as *mut _)
    }

    /// 请求重绘窗口
    pub unsafe fn 请求绘制(&mut self) {
        let _ = RedrawWindow(self.窗口, None, None, RDW_INVALIDATE);
    }

    pub unsafe fn 主循环(&mut self) {
        // 显示窗口
        let _ = ShowWindow(self.窗口, SW_SHOWNORMAL);

        let mut 消息 = MSG::default();
        while GetMessageW(&mut 消息, HWND(std::ptr::null_mut()), 0, 0).into() {
            DispatchMessageW(&消息);
        }
    }
}

创建窗口的过程如下:

  • (1) 调用 GetModuleHandleW 函数, 获取本程序的句柄.

  • (2) 创建 WNDCLASSEXW 数据结构, 调用 RegisterClassExW 函数进行 注册窗口类.

  • (3) 调用 CreateWindowExW 函数, 根据上一步注册的窗口类, 创建新的窗口.

  • (4) 调用 ShowWindow 函数, 显示窗口.

在上面创建窗口的过程中可以看到, 窗口有很多参数可以设置, 这些选项决定了窗口的外观以及功能, 具有很大的灵活度. 比如可以设置窗口有没有边框, 有没有标题栏, 有没有最大化/最小化/关闭按钮, 能不能调整窗口的大小, 窗口是否透明, 甚至设置非矩形窗口 (比如圆形窗口).

然后进入处理消息的主循环. Windows 的窗口是基于消息队列的, 也就是操作系统会给窗口发送很多不同种类的消息, 窗口需要处理. 当然程序自己也可以给窗口发送消息.

调用 GetMessageW 函数从消息队列中获取消息, 然后调用 DispatchMessageW 函数分发消息, 进行处理.


unsafe extern "system" fn pmse_win_wndproc(
    窗口: HWND,
    消息: u32,
    w参数: WPARAM,
    l参数: LPARAM,
) -> LRESULT {
    fn 取窗口数据(窗口: HWND) -> *mut 窗口数据 {
        let 指针 = unsafe { GetWindowLongPtrW(窗口, WINDOW_LONG_PTR_INDEX(0)) };
        指针 as *mut _
    }

    match 消息 {
        WM_SIZE => {
            // TODO 窗口大小改变
            let 宽高 = (loword(l参数.0 as u32), hiword(l参数.0 as u32));
            println!("{:?}", 宽高);

            LRESULT(1)
        }

        WM_PAINT => {
            // 绘制回调
            let 数据 = 取窗口数据(窗口);
            match (*数据).绘制回调.as_mut() {
                Some(回调) => {
                    (回调)();
                }
                None => {}
            }

            let _ = ValidateRect(窗口, None);
            LRESULT(0)
        }

        WM_DESTROY => {
            // 关闭窗口
            PostQuitMessage(0);
            LRESULT(0)
        }
        _ => DefWindowProcW(窗口, 消息, w参数, l参数),
    }
}

这个就是大名鼎鼎的 窗口函数 (wndproc), 也就是一个用来处理窗口消息的回调函数.

在上面注册窗口类的时候, 指定对应的窗口函数:

        let 窗口类 = WNDCLASSEXW {
            // 省略

            lpfnWndProc: Some(pmse_win_wndproc),

然后在调用 DispatchMessageW 函数的时候, 系统就会回调相应的窗口函数.

这种奇怪的设计, 窝觉得主要还是历史遗留问题. 在早期, Windows 界面的各种东西都是 “窗口”. 通常用户了解的 “窗口”, 是一种 “顶级窗口”. 然后, Windows 的窗口是可以层层嵌套的, 也就是说和 Android 界面的 View 差不多. Windows 界面上的一个 “按钮”, “图标”, “文字” 等等, 曾经都是一个一个的 “窗口”.

当然, 对于我们 vulkan 渲染这种应用场景, 上述 Windows 提供的各种复杂的窗口功能, 我们都是不需要的. 我们只需要一个顶级窗口, 用来 vulkan 绘制即可.

在上面我们定义的窗口函数中, WM_PAINT 消息用来画窗口内容 (也就是说, 在处理这个消息时, 对窗口进行实际的绘制). WM_DESTROY 消息是关闭窗口, 我们调用 PostQuitMessage 函数来退出主循环, 从而结束程序.

对于别的不感兴趣的窗口消息, 我们调用 DefWindowProcW 函数, 让 Windows 系统进行默认处理.

1.2 RawWindowHandle

上面已经创建并显示了一个窗口, 接下来就要对接 vulkan 初始化了. 还是老一套, 获取窗口原始指针.

/// 提供 RawWindowHandle, RawDisplayHandle (Windows)
#[derive(Debug, Clone)]
pub struct HandleBox {
    rd: RawDisplayHandle,
    rw: RawWindowHandle,
}

impl HandleBox {
    pub fn new(hinstance: *mut c_void, hwnd: *mut c_void) -> Self {
        let mut h = Win32WindowHandle::empty();
        h.hinstance = hinstance;
        h.hwnd = hwnd;

        let rw = RawWindowHandle::Win32(h);
        let rd = RawDisplayHandle::Windows(WindowsDisplayHandle::empty());
        Self { rd, rw }
    }
}

unsafe impl HasRawDisplayHandle for HandleBox {
    fn raw_display_handle(&self) -> RawDisplayHandle {
        self.rd
    }
}

unsafe impl HasRawWindowHandle for HandleBox {
    fn raw_window_handle(&self) -> RawWindowHandle {
        self.rw
    }
}

// TODO
unsafe impl Send for HandleBox {}
unsafe impl Sync for HandleBox {}

代码很简单, 就不多解释了. 然后对上述代码进行封装, 方便使用:

pub trait 回调接口 {
    fn 初始化(&mut self, h: HandleBox);
    fn 绘制(&mut self);
}

/// 封装窗口执行入口
pub fn pmse_win_main<T: 回调接口 + 'static>(标题: String, 宽高: (i32, i32), mut 回调: T) {
    let mut 窗口 = unsafe { 窗口封装::new(宽高, 标题) };
    回调.初始化(窗口.获取指针());

    窗口.设绘制回调(Some(Box::new(move || {
        回调.绘制();
    })));

    unsafe { 窗口.主循环() }
}

2 vulkan 渲染测试

文件 pmse-win/src/main.rs:

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

use env_logger;

use pmse_render::{
    draw_t::{PmseRenderT, 三角形},
    PmseRenderInit,
};

mod w;

use w::{pmse_win_main, HandleBox, 回调接口};

struct 回调 {
    pri: PmseRenderInit,
    t: Option<PmseRenderT>,
}

impl 回调 {
    pub fn new() -> Self {
        let pri = PmseRenderInit::vulkan().unwrap();
        Self { pri, t: None }
    }
}

impl 回调接口 for 回调 {
    fn 初始化(&mut self, h: HandleBox) {
        println!("cb init");

        let pr = self.pri.clone().init_w(h.into()).unwrap();
        let t = PmseRenderT::new(pr, (1280, 720)).unwrap();
        self.t = Some(t);
    }

    fn 绘制(&mut self) {
        println!("cb draw");

        self.t
            .as_mut()
            .unwrap()
            .draw(vec![三角形::default()])
            .unwrap();
    }
}

fn main() {
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
    println!("main");

    pmse_win_main("测试窗口 (vulkan)".into(), (1280, 720), 回调::new());
}

代码和之前发布的文章中的差不多, 调用相同的代码进行 vulkan 初始化和渲染测试.

我们已经把复杂的东西封装好了, 此处调用起来就很简单了.

3 编译运行

需要先安装这些软件:

  • rust: https://www.rust-lang.org/
  • Vulkan SDK: https://www.lunarg.com/vulkan-sdk/

同时推荐安装这些软件:

  • vscode (代码编辑器): https://code.visualstudio.com/
  • git (版本控制系统): https://git-scm.com/

然后编译 (调试版本):

cargo build

如果想编译发布版本 (启用编译优化), 可以使用如下命令:

cargo build --release

编译之后获得:

在这里插入图片描述

文件不大, 只有 4.9MB.

在这里插入图片描述

运行程序, 如图. 大成功 ! 完结撒花 ~

4 总结与展望

至此, vulkan 的 跨平台 能力已经得到了充分验证: GNU/Linux (wayland), Android (手机), Windows (PC) 等操作系统 (平台), 都实现了 vulkan 初始化和绘制三角形. 本文相关的完整源代码请见: https://crates.io/crates/pmse-win

vulkan 部分, 底层基于 vulkano 库, 已经封装好了, 属于 平台无关 代码, 各平台都一样. 窗口系统部分, 每个平台都不一样. 这里我们 没有 选择直接使用封装好的 跨平台窗口库, 而是在每个平台都自己从头开始做, 从而可以对窗口具有更强的控制和定制能力.

GNU/Linux 平台, 采用 wayland 窗口协议, 以及 wayland 次表面 (Subsurface). Android 平台, 通过 JNI (NDK) 和 SurfaceView, 将 rust 代码 “嵌入” 一个普通的 kotlin 应用. Windows 平台, 基于 windows (rust) 库, 使用经典的 win32 API 创建窗口.

我们的 vulkan 程序已经可以支持多个常用平台, 接下来就要回到 vulkan 本身的使用了.


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

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

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

相关文章

企业微信聊天记录怎么查看,会话存档的妙用!

在企业微信中&#xff0c;老板是否可以任意查看任何人的聊天记录取决于是否开通了会话存档功能以及你是否在存档的成员范围内。以下是详细解释&#xff1a; 如果企业开通了会话存档功能&#xff0c;并且在存档的成员范围内&#xff0c;老板可以看到该成员的聊天记录。 存档的…

点餐小程序实战教程11数据源设计

目录 1 设计图2 创建数据源2.1 菜品分类2.2 菜品表 3 创建管理应用4 设置上架下架功能总结 我们用了10篇讲解了一下用户管理及权限设计&#xff0c;有了用户和权限相当于有了骨架&#xff0c;但是我们还需要有良好的设计来确保我们的小程序的开发顺利进行。 在数据源的设计中&a…

光伏开发:一分钟生成光伏项目报告

传统光伏项目报告的编制往往需要收集大量数据、进行复杂计算与分析&#xff0c;耗时长且易受人为因素影响。自动生成光伏项目报告&#xff0c;依托大数据、云计算、人工智能等先进信息技术&#xff0c;实现了对光伏项目关键参数的快速分析、评估与预测。 一、核心功能与流程 1…

Python学习笔记--BeeWare跨平台打包方案2

文章目录 1. 多文件开发支持 首先贴上官方的地址&#xff1a; BeeWare 教程 其中的安装和初始化过程在上一片有描述&#xff0c;这张介绍一些基础使用&#xff1a; 1. 多文件开发支持 通常一个工程下我们通常会分为多个 app进行开发&#xff0c;例如如下目录&#xff1a; 如…

vue3/Element-Plus/路由的使用

我们来实现一个简单的二级路由 1.准备主页和要配置的组件 主页面 <template><!-- 加载配置路由 --><RouterView></RouterView> </template><style scoped></style>组件1 <template><div>考试组件</div> </t…

在 Docker 中部署无头 Chrome:在 Browserless 中运行

什么是 Browserless&#xff1f; Browserless 是一款基于云的浏览器解决方案&#xff0c;旨在实现高效的浏览器自动化、网页抓取和测试。 它利用 Nstbrowser 的指纹库&#xff0c;实现随机指纹切换&#xff0c;确保流畅的数据收集和自动化。得益于其强大的云基础设施&#xf…

ROS理论与实践学习笔记——2 ROS通信机制之话题通信

话题通信是ROS中使用频率最高的一种通信模式&#xff0c;话题通信是基于发布订阅模式的&#xff0c;也即:一个节点发布消息&#xff0c;另一个节点订阅该消息&#xff0c;用于不断更新的、少逻辑处理的数据传输场景。 1.1 话题通信理论模 话题通信实现模型是比较复杂的&#x…

qm 命令:管理PVE虚拟机

一、命令简介 ​qm​ 是 Proxmox Virtual Environment (PVE) 中用于管理虚拟机的命令行工具。它允许用户创建、启动、停止、删除虚拟机&#xff0c;以及管理虚拟机的配置和状态。 ‍ 介绍 PVE Proxmox Virtual Environment (PVE) 是一个开源的虚拟化管理平台&#xff0c;专…

Java 注解详解:从基础到自定义及解析

注解&#xff1a;概述 目标 能够理解注解在程序中的作用 路径 什么是注解注解的作用 注解 什么是注解&#xff1f; 注解(Annotation)也称为元数据&#xff0c;是一种代码级别的说明注解是JDK1.5版本引入的一个特性&#xff0c;和类、接口是在同一个层次注解可以声明在包…

Linux系统--五种IO模型

1、简介 Linux IO 模型根据实现的功能可以划分为为阻塞 IO、 非阻塞 IO、 信号驱动 IO&#xff0c; IO 多路复用和异 步 IO。 根据等待 IO 的执行结果进行划分&#xff0c; 前四个 IO 模型又被称为同步 IO&#xff0c;如下图&#xff1a; 2、详细介绍 2.1 阻塞IO 在阻塞IO模…

Docker容器常用命令详解

Docker容器常用命令&#xff0c;我们经常使用&#xff0c;又经常忘记&#xff0c;今天我们系统分析一下&#xff1a; 1、查看运行的进程 #列出所有运行的容器 sudo docker ps#列出所有容器&#xff0c;包括运行和停止的 docker ps -a #列出所有容器&#xff0c;并过滤 docker…

大数据Hologres(一):Hologres 简单介绍

文章目录 Hologres 简单介绍 一、什么是实时数仓 Hologres 二、产品优势 1、专注实时场景 2、亚秒级交互式分析 3、统一数据服务出口 4、开放生态 5、MaxCompute查询加速 6、计算存储分离架构 三、应用场景 搭建实时数仓 四、产品架构 1、Shared Disk/Storage &am…

深入探索linux的零拷贝(zero-copy):底层技术原理与代码实现

前言 I/O 或输入/输出通常意味着中央处理器(CPU) 与外部设备&#xff08;如磁盘、鼠标、键盘等&#xff09;之间的读写。在深入研究零拷贝之前&#xff0c;有必要指出磁盘 I/O&#xff08;包括磁盘设备和其他块导向设备&#xff09;和网络 I/O之间的区别。 磁盘 I/O 的常用接…

mysql安装教程(新手版)

本教程不需要手动设置配置文件&#xff0c;比较简单&#xff0c;适合新手&#xff0c;过程需联网。 1.找到mysql官网 mysql官网 一.mysql的安装 1.界面如下图&#xff0c;点击箭头所指。 2.选择mysql版本&#xff0c;系统&#xff0c;安装。 3.下载完成后双击打开&#xff0…

JavaWeb--纯小白笔记03:servlet入门---动态网页的创建

笔记&#xff1a;index.html在tomcat中为默认的名字&#xff0c;html里面的语法不严谨。改配置文件要小心&#xff0c;不然容易删掉其他 Servlet&#xff1a;服务器端小程序&#xff0c;写动态网页需要用Servlet&#xff0c;普通的java类通过继承HttpServlet&#xff0c;可以响…

c++9月25日

1.栈的实现 头文件 #ifndef STACKHEAD_H #define STACKHEAD_H #include <iostream> #define MAX 30 using namespace std;class Stack { private:int *ptr;int top; public:Stack();Stack(int *,int);~Stack();bool full();bool empty();int push(int);void show();Sta…

一个超强的Python库!HTTP请求性能分析工具推荐:httpstat!

什么是Python httpstat&#xff1f; httpstat是一个基于命令行的工具&#xff0c;用于在终端中展示HTTP请求的详细统计信息。它以可视化和易读的方式显示了HTTP请求的各个阶段的性能数据&#xff0c;如DNS解析、TCP连接、TLS握手、发送请求、服务器处理、接收响应等。 使用ht…

2024.9.26 Spark学习

资料&#xff1a; Spark基础入门-第一章-1.1-Spark简单介绍_哔哩哔哩_bilibili &#xff08;1&#xff09;基础知识 Apache Spark 是用于大规模数据&#xff08;large-scale data&#xff09;处理的统一分析引擎。 分布式处理数据 PySpark模块 Spark 和 Hadoop 有区别&…

QT编译之后的debug包下运行程序双击运行出现无法定位程序输入点__gxx_personlity_seh0于动态链接库

1.出现这个错误的原因是&#xff1a; 缺少如下文件&#xff1a; 2.解决方法&#xff1a; 在运行程序.exe所在的目录执行&#xff1a;windeployqt untitled.exe&#xff08;指打包的运行程序&#xff09; 3.如果执行提示由于找不到qt5core.dll,无法继续执行代码和无法识别win…

c++进阶学习-----继承

1.继承的概念及定义 1.1继承的概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段&#xff0c;它允许程序员在保持原有类特性的基础上进行扩展&#xff0c;增加功能&#xff0c;这样产生新的类&#xff0c;称派生类。 继承呈现了面向对象 程序设计的…