rust GTK4 窗口创建与 wayland Subsurface (vulkan 渲染窗口初始化 (Linux) 上篇)

rust 有封装好的 GTK4 库 (gtk4-rs), 有封装好的 wayland 库 (wayland-rs), 有封装好的 vulkan 库 (vulkano), 单独使用其中的每一个, 都很简单. 但是, 把这些一起使用, 崩 !! 大坑出现了 !

这个问题的难度超出了事先的预计 (所以原计划一篇文章分成了两篇), 而类似的事情在编程领域经常发生 (不出意外的就要出意外了).

GTK4 (目前) 并不直接支持使用 vulkan 进行绘制, 所以想要同时使用 GTK4 和 vulkan (在同一个窗口中), 就要采取一些曲线救国的方法: wayland Subsurface. 然而巧合的是, GTK4 同时又不支持 wayland Subsurface ! 所以, 此时就要绕过 GTK4 (GDK4), 直接使用底层的 wayland 协议, 来创建和使用 Subsurface. 然而, wayland-rs 库设计是单独使用的, 如果要配合 GTK4 同时使用, 又有了新的麻烦. 总之, 一环套一环, 大坑之中又有深坑, 从这一堆坑中爬出来, 可真不容易啊 ~


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

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

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


相关文章:

参考资料:

目录

  • 1 FAQ
    • 1.1 为什么要使用 wayland ?
    • 1.2 为什么要使用 GTK4 ?
    • 1.3 为什么要使用 vulkan ?
  • 2 GTK4 创建窗口
  • 3 在 GDK4 中初始化 wayland
    • 3.1 获取 wayland 连接
    • 3.2 创建 wayland 事件队列
  • 4 创建 wayland Subsurface
    • 4.1 枚举 wayland 服务
    • 4.2 初始化 Subsurface
    • 4.3 窗口原始指针
    • 4.4 运行测试
  • 5 总结与展望

1 FAQ

问答环节 (FAQ):

1.1 为什么要使用 wayland ?

GNU/Linux 桌面有两种窗口协议: 古老的 (几十年前的) x11, 和新的 wayland. x11 毕竟年龄大了, 很多方面跟不上新时代了. wayland 一般情况下性能更高, 更安全, 历史遗留问题更少. 所以新的软件, 能支持 wayland 尽量优先支持 wayland, x11 只是为了兼容老旧软件.

另外, Linux 桌面用户本来就少 (市场占有率只有 2% ~ 4%), 所以支持一套协议就够了, 没必要 wayland 和 x11 都支持, 同时维护两套东西太麻烦, 负担太重.

什么 ? 有人非要使用 x11 ? 那 … . 也不是不可以. 要知道, wayland 和 x11 的一大特点 (优点), 就是支持疯狂套娃: 在 wayland 里面嵌套运行 wayland (合成器), 在 x11 里面嵌套 (nest) 运行 x11 (server), 这些本来就是支持的. 在 wayland 里面运行 x11, 有 Xwayland. 在 x11 里面运行 wayland, 也可以, 比如使用 weston. 所以, 只支持 wayland 的软件, 非要在 x11 里面运行, 可以, 只不过麻烦一点而已.

1.2 为什么要使用 GTK4 ?

GTK (GIMP Tool Kit) 是一个创建图形用户界面 (GUI 窗口) 的工具包, GTK4 是 GTK 的最新版本. (顺便吐槽, 虽然 GTK 来自 GIMP, 但是 GTK 都到版本 4 了, GIMP 自己却仍然在使用 GTK2, 移植到 GTK3 的工作今年好像刚刚完成 ?)

与 x11 不同, wayland 窗口是没有 “装饰” 的, 也就是窗口的边框, 标题栏, 关闭按钮, 等等, 需要自己画 (绘制), 自己实现 (x11 的窗口可以由 X server 来进行装饰). 所以, 如果直接基于 wayland 协议来做窗口, 是比较麻烦的, 甚至 “关闭窗口” 都需要自己实现, 没有几百行甚至上千行代码, 弄不好.

而使用 GTK4 就可以简单方便的创建好看的窗口, 由 GTK4 实现窗口关闭, 移动, 改变大小, 最大化最小化等基本功能.

1.3 为什么要使用 vulkan ?

vulkan 是一种 GPU 的编程接口 (API) 标准, 可以用于 3D 渲染, GPU 计算等. 也就是说, 通过 vulkan 可以让 GPU (图形处理器) 干活.

vulkan 是 OpenGL (ES) 的升级替代, OpenGL 就很古老了 (也是几十年前的). OpenGL 经过几十年的发展, 有很多历史遗留问题, 但是为了保持兼容旧的软件, 一直保留. vulkan 就是一次新的 “干净的重新开始”, 没有历史包袱.

与 OpenGL 相比, vulkan 更加贴近硬件底层, 使用 vulkan 的软件具有更强的控制能力, 很多东西需要手动管理, 所以灵活度更高, 性能更高. 应用软件 (而不是显卡驱动) 有更多的优化空间, 可以做更多的事情. OpenGL 是单线程运行的 (状态机), 而 vulkan 支持多线程 (提交命令缓冲区, 多个命令队列), 所以 vulkan 更适合现代的多核 CPU. vulkan 对新技术 (比如 光线追踪) 的支持也更好.

vulkan 具有很好的 跨平台 能力, Linux, Android (手机), Windows (PC) 等系统都支持, N 卡, A 卡, I 卡等显卡也都支持. 所以基于 vulkan 的软件可以实现 “一次编写, 到处运行”. (Java: 抄我台词是吧 ?)

2 GTK4 创建窗口

使用 GTK4 创建一个空白窗口是很简单的, 比如:

> cargo new --bin gtk4_test
    Creating binary (application) `gtk4_test` package

文件 gtk4_test/Cargo.toml:

[package]
name = "gtk4_test"
version = "0.1.0"
edition = "2021"

[dependencies]
adw = { version = "^0.7.0", package = "libadwaita", features = ["v1_1"] }
gtk4 = { version = "^0.9.1", features = ["v4_6"] }

文件 gtk4_test/src/main.rs:

use adw::Application;
use gtk4::{glib::ExitCode, prelude::*, ApplicationWindow};

fn main() -> ExitCode {
    let app = Application::builder().application_id("test1").build();

    app.connect_activate(move |app| {
        // 创建窗口
        let w = ApplicationWindow::builder()
            .application(app)
            .default_width(1280)
            .default_height(720)
            .title("测试 GTK4 窗口 (穷人小水滴)")
            .build();
        // 显示窗口
        w.present();
    });

    app.run()
}

编译:

cargo build

运行:

./target/debug/gtk4_test

在这里插入图片描述

3 在 GDK4 中初始化 wayland

GDK4 是 GTK4 对于窗口协议 (wayland 和 x11) 的抽象封装 (注意是 GDK 不是 GTK, 名称容易弄错), 也就是使得 GTK 无需关心底层的实现细节, 可以支持 wayland 和 x11.

所以, 要想在 GTK4 中使用 wayland 协议, 就要从 GDK4 入手.

文件 pmse-gtk/Cargo.toml:

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

[dependencies]
adw = { version = "^0.7.0", package = "libadwaita", features = ["v1_1"] }
gtk4 = { version = "^0.9.1", features = ["v4_6"] }
gdk4 = { version = "^0.9.0", features = ["v4_6"] }
gdk4-wayland = { version = "^0.9.1", features = ["wayland_crate"] }
wayland-backend = { version = "^0.3.7", features = ["client_system", "raw-window-handle"] }

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

此处主要指定一些依赖软件包. libadwaitagtk4 上面已经见过了, 主要用来创建窗口. gdk4, gdk4-wayland, wayland-backend 这几个是使用 wayland 的关键. raw-window-handle 用于获取窗口的原始指针, 在后面初始化 vulkan 要用到.

3.1 获取 wayland 连接

文件 pmse-gtk/src/wayland_conn.rs (节选):

//! wayland connection: 从 gtk4 window 获取连接
#![allow(unsafe_code)]

use std::error::Error;

use gdk4::prelude::DisplayExtManual;
use gdk4_wayland::{
    prelude::WaylandSurfaceExtManual,
    wayland_client::{protocol::wl_surface::WlSurface, Connection},
    WaylandDisplay, WaylandSurface,
};
use gtk4::{
    glib::{object::Cast, translate::ToGlibPtr},
    prelude::{NativeExt, RootExt},
    ApplicationWindow,
};

use crate::{VulkanSurface, E};

/// wayland connection
#[derive(Debug, Clone)]
pub struct WaylandConn {
    // raw
    w: ApplicationWindow,
    // wayland 连接
    c: Connection,
}

impl WaylandConn {
    /// 从 gtk4 window 获取连接
    pub fn new(w: &ApplicationWindow) -> Result<Self, Box<dyn Error>> {
        let wd = 获取wd(w)?;
        let c = 获取连接(&wd);
        // debug
        println!("  {:?}", c);

        Ok(Self { w: w.clone(), c })
    }

    /// 创建 VulkanSurface
    ///
    /// 注意: 必须在窗口显示之后调用
    pub fn surface(&self) -> Result<VulkanSurface, Box<dyn Error>> {
        let ws = 获取窗口表面(&self.w)?;
        Ok(VulkanSurface::new(self.c.clone(), ws))
    }
}

这个模块用于在创建窗口之后, 获取 wayland 连接. wayland 连接就是应用软件 (本程序) 与 wayland 合成器 (窗口管理器) 之间的通信连接 (UNIX socket), 因为窗口是 GTK4 创建的, 所以 GDK4 已经创建好了一个 wayland 连接, 所以我们不应该再自己创建新的连接, 而应该使用 GDK4 的连接.

/// 获取 WaylandDisplay
fn 获取wd(w: &ApplicationWindow) -> Result<WaylandDisplay, Box<dyn Error>> {
    let gdk_d = w.display();
    let 后端 = gdk_d.backend();
    // debug
    println!("gtk4 backend = {:?}", 后端);

    let wd = gdk_d
        .downcast::<WaylandDisplay>()
        .ok()
        .ok_or(E("ERROR wayland cast display".into()))?;
    println!("  {:?}", wd);

    Ok(wd)
}

这个函数从 GTK4 的窗口获取 WaylandDisplay, 这一部分使用了 GDK4 的函数. 其中 downcastglib (GObject) 的函数. GObject 是一套 C 语言的 “面向对象编程” 框架, 因为 GTK 是用 C 语言编写的, 所以有这个东西.

/// 获取 wayland connection
///
/// 注意: 只能调用一次
///
/// https://gtk-rs.org/gtk4-rs/stable/latest/docs/src/gdk4_wayland/wayland_display.rs.html#91
fn 获取连接(wd: &WaylandDisplay) -> Connection {
    use gdk4_wayland::ffi;
    unsafe {
        let display_ptr = ffi::gdk_wayland_display_get_wl_display(wd.to_glib_none().0);
        let backend =
            wayland_backend::sys::client::Backend::from_foreign_display(display_ptr as *mut _);
        Connection::from_backend(backend)
    }
}

在获得了 GDK4 的 WaylandDisplay 之后, 就能获取 wayland 连接了. 此处使用了 unsafe (不安全) rust, 这是因为 GDK 是 C 语言编写的, rust 与 C 语言的底层交互是不安全的. 使用 unsafe 需要特别注意, 因为这部分代码是绕过 rust 编译器 (rustc) 的安全检查的, 可能会有 BUG 导致程序崩溃等 未定义行为 (UB). 未定义行为的意思就是, 程序会做什么, 我们根本不知道.

需要注意, “未定义行为” 并不是 “不确定行为”, 这个概念需要搞清楚. 比如, 如果程序使用随机数 (比如 /dev/urandom), 那么程序的行为是 “不确定” (随机) 的, 但是这个随机行为是 定义 的, 也就是人类明确的告诉程序要随机. 而 UB 的意思是未定义的行为, 这种情况下程序很可能会出现 BUG.

rust 的安全承诺是, 如果不使用 unsafe, 那么不会有 UB. 所以 rust 代码应该尽量不使用 unsafe, 这样代码质量更高, BUG 更少. 但是 C 语言本身是达不到这个安全标准的, 所以 rust 与 C 语言交互时, 不得不使用 unsafe. 使用 unsafe 也就意味着, 编译器不负责了, 代码的安全性由 程序员 (写代码的人) 全部负责, 所以写 unsafe 代码需要特别小心 !

/// 获取窗口的顶层表面 WlSurface
fn 获取窗口表面(w: &ApplicationWindow) -> Result<WlSurface, Box<dyn Error>> {
    let gdk_s = w.surface().ok_or("ERROR wayland no surface")?;
    let ws = gdk_s
        .downcast::<WaylandSurface>()
        .ok()
        .ok_or(E("ERROR wayland cast surface".into()))?;
    println!("  {:?}", ws);

    let s = ws
        .wl_surface()
        .ok_or(E("ERROR wayland wl_surface".into()))?;
    Ok(s)
}

这个函数是获取窗口的 wayland 表面 (WlSurface), 以及一些错误处理代码. wayland 表面就是一块绘制区域, 一张画布, 比如一个窗口就可以是一个 wayland 表面. 这个在后面要用到.

3.2 创建 wayland 事件队列

这一步是难度最大的, 也是决定本次行动 (爬出深坑) 成败的关键.

应用软件 (本程序) 通过 wayland 连接和 wayland 合成器之间互相发送消息, 这是 wayland 协议的工作方式. 从 wayland 合成器接收到的消息, 会被放入一个 事件队列 (EventQueue) 之中, 供程序后续处理.

窗口是 GTK4 创建的, 所以 GDK4 已经创建了事件队列, 供 GDK4 自己使用. 我们想要正常使用 wayland 协议, 就要创建 (初始化) 自己的事件队列.

如果说, GDK4 的 wayland 事件队列是一根已经接好的水管, 那我们就要把这根水管切开一个小口, 接上去一根我们自己的新的水管, 才能喝到水.

文件 pmse-gtk/src/wayland_subsurface.rs (节选):

//! wayland subsurface
//!
//! https://github.com/Smithay/wayland-rs/pull/572
use std::error::Error;
use std::future::poll_fn;
use std::os::unix::io::AsRawFd;
use std::sync::Arc;

use gdk4_wayland::wayland_client::{
    protocol::{wl_compositor, wl_registry, wl_subcompositor, wl_subsurface, wl_surface},
    Connection, Dispatch, EventQueue, QueueHandle,
};
use gtk4::glib::{self, ControlFlow};

use crate::{Cb, HandleBox, E};

/// wayland subsurface (vulkan)
#[derive(Debug, Clone)]
pub struct VulkanSurface {
    c: Connection,
    // toplevel window surface
    ws: wl_surface::WlSurface,
}

impl VulkanSurface {
    pub(crate) fn new(c: Connection, ws: wl_surface::WlSurface) -> Self {
        Self { c, ws }
    }

    /// 运行新的 wayland queue
    pub fn run(self, offset: (i32, i32), cb: Arc<Box<dyn Cb>>) {
        运行(&self.c, self.ws.clone(), offset, cb).unwrap();
    }
}

// 创建 subsurface
struct AppData {
    ws: wl_surface::WlSurface,
    偏移: (i32, i32),
    回调: Arc<Box<dyn Cb>>,
    wc: Option<wl_compositor::WlCompositor>,
    sc: Option<wl_subcompositor::WlSubcompositor>,
    s: Option<wl_surface::WlSurface>,
    ss: Option<wl_subsurface::WlSubsurface>,
}

impl Dispatch<wl_registry::WlRegistry, ()> for AppData {
    fn event(
        state: &mut Self,
        r: &wl_registry::WlRegistry,
        event: wl_registry::Event,
        _: &(),
        c: &Connection,
        h: &QueueHandle<AppData>,
    ) {
// 省略
    }
}

// 省略

/// gtk4 运行 wayland queue
fn 运行队列1(c: &Connection) -> Result<EventQueue<AppData>, Box<dyn Error>> {
    let q = c.new_event_queue();
    let h = q.handle();

    let _r = c.display().get_registry(&h, ());
    // debug
    println!("wayland gtk4 read");
    let 连接 = c.clone();
    let fd = 连接
        .prepare_read()
        .ok_or(E("ERROR wayland prepare_read".into()))?
        .connection_fd()
        .as_raw_fd();
    glib::source::unix_fd_add_local(fd, glib::IOCondition::IN, move |_, _| {
        match 连接.prepare_read() {
            Some(g) => {
                g.read().unwrap();
            }
            None => {
                连接.backend().dispatch_inner_queue().unwrap();
            }
        }
        // TODO
        ControlFlow::Continue
    });

    Ok(q)
}

fn 运行队列2(mut q: EventQueue<AppData>, mut a: AppData) {
    glib::MainContext::default().spawn_local(async move {
        poll_fn(|cx| q.poll_dispatch_pending(cx, &mut a))
            .await
            .unwrap();
    });
}

/// 运行 wayland queue (subcompositor)
fn 运行(
    c: &Connection,
    ws: wl_surface::WlSurface,
    偏移: (i32, i32),
    回调: Arc<Box<dyn Cb>>,
) -> Result<(), Box<dyn Error>> {
    println!("wayland queue run");
    let q = 运行队列1(c)?;

    let a = AppData {
        ws,
        偏移,
        回调,
        wc: None,
        sc: None,
        s: None,
        ss: None,
    };
    println!("wayland registry global:");

    运行队列2(q, a);
    Ok(())
}

抱歉, 这段代码确实有点长, 你忍一下, 很快就好了 ~

首先, 按照 wayland-rs 的用法, 我们需要创建一个数据结构:

struct AppData {
    ws: wl_surface::WlSurface,
    偏移: (i32, i32),
    回调: Arc<Box<dyn Cb>>,
    wc: Option<wl_compositor::WlCompositor>,
    sc: Option<wl_subcompositor::WlSubcompositor>,
    s: Option<wl_surface::WlSurface>,
    ss: Option<wl_subsurface::WlSubsurface>,
}

里面存放运行过程中需要的状态数据, 并实现所需接口:

impl Dispatch<wl_registry::WlRegistry, ()> for AppData {
    fn event(
        state: &mut Self,
        r: &wl_registry::WlRegistry,
        event: wl_registry::Event,
        _: &(),
        c: &Connection,
        h: &QueueHandle<AppData>,
    ) {

事件队列运行过程中, 接收到相应的消息, wayland-rs 就会回调相应的接口, 我们的代码就可以处理对应的消息了.


运行事件队列的两个重要初始化函数 运行队列1, 运行队列2:

/// gtk4 运行 wayland queue
fn 运行队列1(c: &Connection) -> Result<EventQueue<AppData>, Box<dyn Error>> {
    let q = c.new_event_queue();
    let h = q.handle();

    let _r = c.display().get_registry(&h, ());
    // debug
    println!("wayland gtk4 read");
    let 连接 = c.clone();
    let fd = 连接
        .prepare_read()
        .ok_or(E("ERROR wayland prepare_read".into()))?
        .connection_fd()
        .as_raw_fd();
    glib::source::unix_fd_add_local(fd, glib::IOCondition::IN, move |_, _| {
        match 连接.prepare_read() {
            Some(g) => {
                g.read().unwrap();
            }
            None => {
                连接.backend().dispatch_inner_queue().unwrap();
            }
        }
        // TODO
        ControlFlow::Continue
    });

    Ok(q)
}

这段神奇的代码来自: https://github.com/Smithay/wayland-rs/pull/572

嗯, 窝也不知道为什么, 但是能用 ~ (面向 github 编程) 在此感谢写出这段代码的大神 !

这段代码的大致意思是, 使用 glib 对连接的 文件描述符 (Linux fd) 添加一个回调函数, 有数据到达的时候进行读取, 并放入我们自己的事件队列中. (差不多就是从 GDK4 的 “大水管” 里面抢水喝这个意思 ~ )

fn 运行队列2(mut q: EventQueue<AppData>, mut a: AppData) {
    glib::MainContext::default().spawn_local(async move {
        poll_fn(|cx| q.poll_dispatch_pending(cx, &mut a))
            .await
            .unwrap();
    });
}

此处使用了 glib 的异步功能, 在主线程里面塞进去一个函数, 这个函数会不断检查 (poll) 我们自己的事件队列, 并分发消息 (进行回调).

好了, 至此, 我们自己的 wayland 事件队列终于跑起来了, 撒花 ~

4 创建 wayland Subsurface

wayland 窗口是一个 wayland 表面 (surface), GTK4 创建的窗口, 这个表面由 GTK4 负责绘制, 我们无法使用 vulkan 进行绘制. vulkan 也可以实现对 wayland 表面进行绘制. 那么, 如果想要使用 vulkan 进行绘制, 怎么办呢 ? 此时 wayland Subsurface 就出来救场了 !

Subsurface 是一种特殊的 wayland 表面, 可以附加到窗口, 作为窗口的一部分显示, 同时 Subsurface 自己又可以进行绘制.

那么目标就明确了: 创建一个 Subsurface 并添加到窗口, 就可以使用 vulkan 绘制了.

4.1 枚举 wayland 服务

上面我们拿到了 wayland 连接, 也成功跑起来了一个自己的事件队列, 接下来是不是万事大吉了呢 ? 并不 ! 因为 GTK4 不支持 wayland Subsurface, 所以我们无法直接通过 GDK4 获取 wayland Subsurface 服务 (管理器), 也就无法直接用来创建 wayland Subsurface.

首先我们必须枚举 wayland 服务, 也就是 wayland 合成器提供的各种功能 (协议), 这称为 registry.

fn 运行队列1(c: &Connection) -> Result<EventQueue<AppData>, Box<dyn Error>> {
    let q = c.new_event_queue();
    let h = q.handle();

    let _r = c.display().get_registry(&h, ());

在初始化事件队列时, 我们调用 get_registry 函数, 请求 wayland 合成器枚举服务.

impl Dispatch<wl_registry::WlRegistry, ()> for AppData {
    fn event(
        state: &mut Self,
        r: &wl_registry::WlRegistry,
        event: wl_registry::Event,
        _: &(),
        c: &Connection,
        h: &QueueHandle<AppData>,
    ) {
        if let wl_registry::Event::Global {
            name,
            interface,
            version,
        } = event
        {
            //println!("    [{}] {} (v{})", name, interface, version);
            // 绑定感兴趣的接口
            match interface.as_str() {
                "wl_compositor" => {
                    let wc = r.bind::<wl_compositor::WlCompositor, _, _>(name, version, h, ());
                    // debug
                    println!("  {:?}", wc);
                    state.wc.replace(wc);
                }
                "wl_subcompositor" => {
                    let sc =
                        r.bind::<wl_subcompositor::WlSubcompositor, _, _>(name, version, h, ());
                    // debug
                    println!("  {:?}", sc);
                    state.sc.replace(sc);
                }
                _ => {}
            }

            // 检查绑定完成
            state.检查绑定(c, h);
        }
    }
}

这是对应的事件回调处理代码, 在此处保存需要的 wayland 服务. wl_compositor 用来创建 wayland 表面, wl_subcompositor 用来创建 Subsurface.

4.2 初始化 Subsurface

终于, 我们做好了一切准备工作:

impl AppData {
    pub fn 检查绑定(&mut self, c: &Connection, h: &QueueHandle<Self>) {
        // 注意: 只能调用一次, 不能重复创建
        if self.wc.is_some() && self.sc.is_some() && self.ss.is_none() {
            self.创建表面(c, h);
        }
    }

    /// 创建 subsurface
    fn 创建表面(&mut self, c: &Connection, h: &QueueHandle<Self>) {
        // debug
        println!("create subsurface {:?}", self.偏移);
        // 创建新的表面
        let s = self.wc.as_ref().unwrap().create_surface(h, ());
        // 创建下级表面 (设置上级表面)
        let ss = self
            .sc
            .as_ref()
            .unwrap()
            .get_subsurface(&s, &self.ws, h, ());

        // TODO 设置下级表面 偏移
        ss.set_position(self.偏移.0, self.偏移.1);
        // 下级表面显示在上级表面前面 (上方)
        ss.place_above(&self.ws);

        // 分离下级表面 (不再等待上级表面提交)
        ss.set_desync();
        // 同步设置 (提交)
        s.commit();
        self.ws.commit(); // 上级表面也提交, 使设置生效

        // 回调
        let hb = HandleBox::new(&c.backend(), &s);
        self.回调.cb(hb);

        // 初始化完成, 保存结果
        self.s.replace(s);
        self.ss.replace(ss);
    }
}

在获取所需的 wayland 服务之后, 开始创建表面.

首先调用 WlCompositorcreate_surface 函数, 创建 WlSurface. 然后调用 WlSubcompositorget_subsurface 函数, 给刚刚创建的 WlSurface 指定 “角色” (role), 也就是成为 Subsurface. (所以说 Subsurface 是一种特殊的 surface. )

接下来是一些初始化设置. 调用 set_position 设置相对于窗口 (上级表面) 的偏移 (x, y 坐标). 调用 place_above 设置在窗口原来的表面上方 (前方) 显示 (也就是表面的层叠顺序). 调用 set_desync 分离下级表面, 分离之后下级表面可以自己更新 (绘制), 无需等待上级表面更新. 最后两次调用 commit 提交表面设置, 注意一定要提交, 才能使设置生效, 并且需要下级表面和上级表面都提交.

至此, 创建 Subsurface 并初始化完成.

4.3 窗口原始指针

vulkan 初始化的时候需要窗口的原始指针, 此处说明获取方式.

文件 pmse-gtk/src/raw_handle.rs:

//! (wayland) RawWindowHandle, RawDisplayHandle
#![allow(unsafe_code)]

use gdk4_wayland::wayland_client::{protocol::wl_surface::WlSurface, Proxy};
use raw_window_handle::{
    HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, WaylandWindowHandle,
};
use wayland_backend::sys::client::Backend;

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

impl HandleBox {
    pub fn new(b: &Backend, s: &WlSurface) -> Self {
        let rd = b.raw_display_handle();

        // https://docs.rs/winit-gtk/0.29.1/src/winit/platform_impl/linux/window.rs.html
        let mut wh = WaylandWindowHandle::empty();
        wh.surface = s.id().as_ptr() as *mut _;
        let rw = RawWindowHandle::Wayland(wh);

        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 {}

此处 RawDisplayHandle 表示 wayland 连接, RawWindowHandle 表示窗口 (前面创建的 wayland Subsurface). 此处再次用到了 unsafe.

4.4 运行测试

文件 pmse-gtk/src/gtk_main.rs:

use std::sync::Arc;

use adw::Application;
use gtk4::{prelude::*, ApplicationWindow};

use crate::{ExitCode, HandleBox, WaylandConn};

/// 窗口回调
pub trait Cb {
    fn cb(&self, h: HandleBox);
}

/// 创建窗口
///
/// rect 矩形: (x宽, y高, x偏移, y偏移)
/// margin 边距: (上, 右, 下, 左)
pub fn pmse_gtk_main(
    app_id: String,
    title: String,
    rect: (i32, i32, i32, i32),
    margin: (i32, i32, i32, i32),
    cb: Arc<Box<dyn Cb>>,
) -> ExitCode {
    let app = Application::builder().application_id(&app_id).build();
    // 计算窗口长宽
    let x = rect.0 + margin.1 + margin.3;
    let y = rect.1 + margin.0 + margin.2;
    let 偏移 = (margin.3 + rect.2, margin.0 + rect.3);
    // debug
    println!(
        "pmse_gtk_main: {:?} {:?} x = {}, y = {} {:?}",
        rect, margin, x, y, 偏移
    );

    app.connect_activate(move |app| {
        let w = ApplicationWindow::builder()
            .application(app)
            .default_width(x)
            .default_height(y)
            .title(&title)
            // TODO
            .resizable(false)
            .build();
        // 窗口显示前的初始化
        let c = WaylandConn::new(&w).unwrap();
        // 显示窗口
        w.present();
        // 注意: 必须在显示窗口后调用, 否则没有 wayland surface
        let vs = c.surface().unwrap();

        vs.run(偏移, cb.clone());
    });

    app.run()
}

这是执行入口, 调用 GTK4 创建窗口, 并创建和初始化 Subsurface, 然后回调.

文件 pmse/src/main.rs:

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

use std::sync::Arc;

use pmse_gtk::{pmse_gtk_main, Cb, ExitCode, HandleBox};

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

impl Cb for 回调 {
    fn cb(&self, _h: HandleBox) {
    }
}

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

    pmse_gtk_main(
        "io.github.fm_elpac.pmse_bin".into(),
        "测试 (wayland)".into(),
        (1280, 720, 62, 56),
        (44, 8, 8, 8),
        回调,
    )
}

这是测试代码.


使用 cargo 编译项目, 然后运行:

> ./pmse
pmse_gtk_main: (1280, 720, 62, 56) (44, 8, 8, 8) x = 1296, y = 772 (70, 100)
gtk4 backend = Wayland
  WaylandDisplay { inner: TypedObjectRef { inner: 0x60c47222d320, type: GdkWaylandDisplay } }
  Connection { backend: Backend { backend: InnerBackend { inner: Inner { state: Mutex { data: ConnectionState { display: 0x60c472226780, owns_display: false, evq: 0x60c47232d8a0, 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: 0x60c472359520, 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)

代码执行成功, 窗口正常显示.

5 总结与展望

本文介绍了 GTK4 窗口的创建, 获取 GDK4 wayland 连接, 创建 wayland 事件队列, 枚举 wayland 服务, 创建 Subsurface 并初始化, 获取窗口原始指针, 从而为 vulkan 的初始化做好了准备.

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

下篇将使用 vulkan 对 Subsurface 进行绘制.


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值