ttyから自作ゲームエンジンを起動する

趣味でゲームエンジン[^peridot]を開発している者です。

ゲームエンジンというからには、やはり排他的なフルスクリーン表示の機能が欲しくなりますね。
近年のWindowsではDXGIによって簡単にフルスクリーン対応ができます。Vulkanを使用している場合でも、`VK_EXT_full_screen_exclusive`を使用することでDirectXを使った場合と同じようなことができるようです（まだ使ったことはないですが......）。
macOSではそもそもウィンドウシステム自体が全画面表示に対応していますし（これは「排他的」ではない気がしますが）、Android/iOSなどモバイル端末ではそもそもデフォルトで全画面表示になりますから、特に考えることはありません。

最後の砦となるのがLinuxで、このOSだけは一概に「これ！」という選択肢がありません。現状で考えられるものとして、ざっくり以下のような選択肢があります。

* DRM（Direct Rendering Manager）などを直接触ってディスプレイに描画する
* X Window System上であれば、`VK_EXT_acquire_xlib_display`を使ってディスプレイを拝借してそこに描画する

前者の場合、他のコンポジションシステムを完全に迂回することができるためstartxなどでGUIを起動する前の仮想コンソールのタイミングでゲームを起動できるようになります。これめちゃくちゃかっこよくないですか？
というわけで今回はその方式に挑戦してみます。

## 環境

* OS: Arch Linux (Linux 5.9.14-arch1-1)
* GPU: NVIDIA GeForce RTX 2080 Ti
  * ドライババージョン: 455.45.01-7
* ディスプレイ
  * メイン: LG Ultra HD 3840x2160
  * サブ: ASUS VX238 1920x1080

## 今回の概要

今回はLinuxカーネルの機能の一つである**DRM**（Direct Rendering Manager）を直接操作して、X11やWaylandといったGUI基盤に頼らずにVulkan APIを使用したグラフィックスレンダリングを行います。
大まかな全体像としては次のような感じです。

![drm-layers.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94115/f3db4b15-d666-ea34-c127-5fce11c7057d.png)

なお、DRMの構成図は[こちら](https://ttymrz.github.io/libdrm_howto/)を参考にしました。

## まずは最小構成でやってみる(DRM+GBM+Vulkan)

シンプルにいきたかったのですが、途中で結局EGLやOpenGLが出てきててんやわんやします。

このパートで説明する実装: https://github.com/Pctg-x8/peridot/tree/ft-cradle-direct-display-render/cradle/direct/src

### DRMの初期化

まずはDRMの初期化を行います。
DRMを操作する場合、まずは該当のデバイスノードを開く必要があります。システムにどういったDRMのデバイスノードがあるのかは/dev以下を舐める方法でも探せますが、libdrmを使うともっと簡単に見つけることができます。
libdrmでデバイス一覧を取得するには、`drmGetDevices2`を使います。

```rust
    let device_count = unsafe { drm::raw::drmGetDevices2(0, std::ptr::null_mut(), 0) };
    if device_count <= 0 {
        panic!("no drm devices?");
    }
    let mut device_ptrs = vec![std::ptr::null_mut(); device_count as usize];
    unsafe { drm::raw::drmGetDevices2(0, device_ptrs.as_mut_ptr(), device_count) };
    for &dp in &device_ptrs {
        println!("Device: ");
        println!("- Available Nodes: {:08x}", unsafe { (*dp).available_nodes });
        println!("- bustype: {}", unsafe { (*dp).bustype });

        if unsafe { ((*dp).available_nodes & 0x01) != 0 } {
            // primary node
            println!("- primary node str: {}", unsafe { std::ffi::CStr::from_ptr(*(*dp).nodes).to_str().expect("invalid utf-8 sequence") })
        }
    }

    // open first device and get admin
    let device_fd = unsafe { libc::open(*(*device_ptrs[0]).nodes, libc::O_RDWR | libc::O_CLOEXEC) };
    if device_fd < 0 {
        panic!("failed to open primary device");
    }
```

Vulkanの列挙APIと同じく、実体を受け取る引数に`null`を指定することでデバイス数のみを得ることができます。

DRMはシステムで一つだけ「マスタ」をもち、このマスタとなったファイルデスクリプタを通してのみモードセットなどの操作を行うことができるようになっています。
DRMのマスタ権限をもらうには、`drmGetMagic`と`drmAuthMagic`を使用します。

```rust
    let mut magic = 0;
    if unsafe { drm::raw::drmGetMagic(device_fd, &mut magic) } != 0 {
        panic!("failed to get drm magic");
    }
    let r = unsafe { drm::raw::drmAuthMagic(device_fd, magic) };
    if r != 0 {
        panic!("failed to get control of drm: {}", r);
    }
```

それぞれ、失敗した場合には非0が返ります。
これは作っているうちでハマった点ですが、DRMマスタの権限取得は**Vulkanの初期化より前に行う必要があります**。もしかしたら`VK_KHR_display`を拡張で指定しているせいかもしれませんが、Vulkanの初期化よりあとに`drmAuthMagic`を行うと`-13`で失敗します。

### 画面モード選択

DRMの初期化ができたら、次はそのデバイスに紐づいている各種リソースを取得し、Connector, Encoder, CRTCの情報を取得します。
ここのコードはMesaのkmscube[^kmscube]を参考にしました。本当は`drmModeGetResources`で得たポインタはfreeしないといけないのですが、今回は時間がなかったのでサボっています。
コードは長いですが、やっていることはそんな複雑でもなくて「接続状態のConnectorに紐づくEncoder, CRTCを探している」だけになります。`mode`のところはConnectorが推奨する画面解像度か、使える中でもっとも高い解像度を探しています。

```rust
        let res = unsafe {
            drm::raw::drmModeGetResources(drm_fd).as_ref()
                .unwrap_or_else(|| panic!("Failed to drmModeGetResources: {}", std::io::Error::last_os_error()))
        };

        // find connected connector
        let connector = res.connectors().iter()
            .filter_map(|&cid| drm::mode::ConnectorPtr::get(drm_fd, cid))
            .find(|c| c.connection == drm::mode::Connection::Connected)
            .expect("no available connectors");

        // find preferred or highest-resolution mode
        let mode = connector.modes().iter().find(|m| m.is_preferred()).or_else(||
            connector.modes().iter().map(|x| (x, x.hdisplay * x.vdisplay)).max_by_key(|&(_, k)| k).map(|(m, _)| m)
        ).expect("no available modes?");
        println!("selected mode: {}x{}", mode.hdisplay, mode.vdisplay);

        // find encoder
        let encoder = res.encoders().iter()
            .filter_map(|&eid| drm::mode::EncoderPtr::get(drm_fd, eid))
            .find(|e| e.encoder_id == connector.encoder_id);

        // find crtc id
        let crtc_id = match encoder {
            Some(e) => e.crtc_id,
            None => res.encoders().iter()
                .filter_map(|&eid| drm::mode::EncoderPtr::get(drm_fd, eid))
                .filter_map(|e|
                    res.crtcs().iter().enumerate()
                        .find(|&(crx, _)| e.has_possible_crtc_index_bit(crx))
                        .map(|(_, &id)| id)
                )
                .next().expect("no available crtc id")
        };
```

ちなみに、試してはいませんがおそらくEncoderの探索はなくてもよいかもしれません。どの道モードセットでは使われない情報なので......

### フレームバッファを作る

libdrmでフレームバッファを作るには`drmModeAddFB` `drmModeAddFB2` `drmModeAddFB2WithModifiers`を使用します。ただしこれらの関数は実際の描画先のメモリ確保まではしません。DRMだけでもフレームバッファ用のメモリを確保することはできるのですが、ここのメモリ確保は大抵の場合は**GEM**（Graphics Execution Manager）を使うことになります。
ただしこのGEMは、デバイスのクローズ処理こそ共通化されているもののデバイスのオープンやそれ以外の操作はデバイスによってまちまちで、生で触るには少し難しすぎます。
ここで、一般的にバッファAPIと呼ばれるものが登場します。バッファAPIは大きく2つ、**GBM**（Generic Buffer Manager）と**EGLStreams**があります。EGLStreamsの方はEGLの拡張なので正確にはバッファAPIよりもう少し高度です。
今回はあまりライブラリを使わず、できるだけホワイトボックスのまま作ってみたいのでまずはGBMの方を使用してバッファを確保します。EGLStreamsについてはあとで触れます。

GBMを使う場合、まずはデバイスオブジェクトを作成する必要があります。`gbm_device_create`を使用します。

```rust
pub fn gbm_create_device(fd: libc::c_int) -> *mut gbm_device;
```

ここのfdにはDRM側でオープンしたファイルデスクリプタを渡します。
GBMを使用してバッファを確保するには、`gbm_bo_create` `gbm_bo_create_with_modifiers`を利用します。

```rust
pub fn gbm_bo_create(
    gbm: *mut gbm_device, width: u32, height: u32, format: u32, flags: u32
) -> *mut gbm_bo;
pub fn gbm_bo_create_with_modifiers(
    gbm: *mut gbm_device, width: u32, height: u32, format: u32,
    modifiers: *const u64, count: libc::c_uint
) -> *mut gbm_bo;
```

`flags`にはこのバッファオブジェクトがどういった使われ方をするのかを指定します。今回のようにレンダリング対象かつ表示に利用する場合は`GBM_BO_USE_RENDERING | GBM_BO_USE_SCANOUT`を指定します。
`modifiers`には何を指定するのかというと、**DRM Format Modifiers**というものを指定します。ベンダごとの情報になるためここでは詳細を省略します。

`gbm_create_device`でDRMのファイルデスクリプタを指定した場合、このバッファオブジェクトからは`drmModeAddFB`に使用できるハンドルを得ることができます。ハンドルを得るには`gbm_bo_get_handle`を使用します。

```rust
pub fn gbm_bo_get_handle(bo: *mut gbm_bo) -> u64;
```

これで`drmModeAddFB`に渡す引数は揃ったので、フレームバッファを作ることができます。

### モードセット！

DRMでモードセット（画面の切り替え）を行うには、`drmModeSetCrtc`を使用します。

```rust
pub fn drmModeSetCrtc(
    fd: libc::c_int, crtc_id: u32, buffer_id: u32, x: u32, y: u32,
    connectors: *mut u32, count: libc::c_int, mode: *mut drmModeModeInfo
) -> libc::c_int;
```

ここまでで得たCRTCのID、フレームバッファのID、ConnectorのID、モード情報を渡します。
この関数はX11など他のプロセスがディスプレイのコントロールを握っている場合は失敗します。
うまくいくと、X11を起動した時と同じように仮想コンソールが消え黒い画面になります。

### さて描画......あれ？

ここまででDRM側の準備は全て完了しました。
あとはVulkanからどうやってGBMのバッファオブジェクトに描画するかですが、ここで`VK_EXT_external_memory_dma_buf`が利用できそうです。
GBMで確保できるバッファオブジェクトから`gbm_bo_get_fd`を使って得られるファイルデスクリプタはdma-bufを表すものになります。そのため、この外部メモリ拡張を利用してimportすることでVulkanのDeviceMemoryがそのdma-bufを参照するようになり、そのDeviceMemoryをバインドしたImageに描画するとGBMのバッファオブジェクトにも書き込まれ、DRMの機構で表示されるはずです。簡単ですね。

[`VK_EXT_external_memory_dma_buf`](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VK_EXT_external_memory_dma_buf.html)はデバイス拡張となりますので、`vkCreateDevice`のタイミングで指定します。
dma-bufのファイルデスクリプタをインポートできるようにするには、`VkMemoryAllocateInfo`の`pNext`に指定する`VkImportMemoryFdInfoKHR`の`handleType`に`VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT`を指定します。こうすると、同構造体の`fd`はdma-bufを指しているものとして認識されます。

```rust
let memport = VkImportMemoryFdInfoKHR {
    sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_FD_INFO_EXT,
    pNext: std::ptr::null(),
    handleType: VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
    fd: gbm_bo_get_fd(bo)
};
let malloc_info = VkMemoryAllocateInfo {
    sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
    pNext: &memport as *const _ as _,
    ...
};
```

さて、これで`vkAllocateMemory`を発行すれば無事共有されたメモリ領域を得られるはず、なんですが、残念ながら**NVIDIAのドライバでは`VK_EXT_external_memory_dma_buf`が実装されていません**[^nv_extmem_dma]。なんてこった

## EGLを経由して描画する

さて、NVIDIAのプロプライエタリドライバで拡張がないとすると他の手段を考えなければなりません。ということでkmscube[^kmscube]を再び参照すると、どうやらあちらはEGLを利用してOpenGL ESで描画しているみたいです。OpenGL ESはどうかは知りませんが、OpenGLにはOpaque FDを利用したメモリブロックの相互運用拡張が存在します。実際にこれを利用してVulkanと相互運用するサンプルとして[nvpro-samples/gl_vk_simple_interop](https://github.com/nvpro-samples/gl_vk_simple_interop)があります。

この拡張は使えそうです。というわけで、次はこの拡張を使用することを考えてみます。ただし「EGLのイメージを直接Vulkanと共有することはできない」こと、「OpenGLのテクスチャに対して直接Vulkanから描画することはできない」ことの2つの制限があるので、少々回りくどいですが次のような構成でいきます。

![DrmVulkanInterop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94115/33a8e0b2-2b8f-3c64-e3e7-4838b368b5e8.png)

Presentが要求されるたびに、OpenGLのフレームバッファにVulkanのメモリ領域を共有したテクスチャをBlitすることでEGLのイメージに書き写します。これならうまくいくはず。

### EGLの初期化

EGLは普通に初期化します。`EGLDisplay`としてプラットフォーム(GBM)をディスプレイをとして使用するので、`eglGetPlatformDisplayEXT`を使用します。

```rust
// Extension: EGL_EXT_platform_base
fn eglGetPlatformDisplayEXT(
    platform: EGLenum, native_display: *mut libc::c_void, attrib_list: *const EGLAttrib
) -> EGLDisplay;
```

`platform`には`EGL_PLATFORM_GBM_KHR`を、`native_display`にはGBMのデバイスポインタをキャストして渡します。`attrib_list`は`null`で大丈夫です。
ディスプレイを得られたあとは、普通に`eglInitialize`を使用して初期化し、`eglMakeCurrent`でコンテキストを有効化します。今回はEGL/OpenGLではオフスクリーンレンダリングしか行わないので、`EGLConfig`や`EGLSurface`は不要で全て`null`を指定すれば大丈夫です。

### EGLでGBMのメモリ領域を使う

EGLでは[`EGL_EXT_image_dma_buf_import`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_image_dma_buf_import.txt)という拡張を使用することでdma-bufをimportしてくることができます。幸いにも自分の環境ではこれが実装されているようなので、これを使うことでdma-bufのメモリ領域を`EGLImage`として利用できます。
`EGLImage`を作るには`eglCreateImageKHR`を使用します。

```rust
// Extension: EGL_KHR_image_base
pub fn eglCreateImageKHR(
    dpy: EGLDisplay, ctx: EGLContext, target: EGLenum, buffer: EGLClientBuffer, attrib_list: *const EGLint
) -> EGLImageKHR;
```

外部メモリからイメージを作る場合は、`buffer`は無視で大丈夫です。`target`に`EGL_LINUX_DMA_BUF_EXT`を指定し、`attrib_list`でdma-bufに必要な各種パラメータを指定します。
具体的に何を指定するかは https://gitlab.freedesktop.org/mesa/kmscube/-/blob/master/common.c#L239 が参考になると思います。

### EGLのメモリをOpenGLで共有する

イメージができたら、今度はそれをOpenGLのテクスチャとして使用するようにします。これには`glEGLImageTargetTexture2DOES`を使用します。

```rust
// Extension: GL_OES_EGL_image_external
pub fn glEGLImageTargetTexture2DOES(target: libc::c_uint, image: *mut libc::c_void);
```

これはバインドしたオブジェクトに対する操作になります。`target`に`GL_TEXTURE_2D`を、`image`には`EGLImage`を指定します。これによってEGLのイメージオブジェクトをOpenGLで共有して利用できるようになります。
ここまででEGLとOpenGLの共有の準備は一旦完了です。

### VulkanのメモリをOpenGLで使う

今度はVulkanで確保したメモリをOpenGLと共有する方法を考えます。OpenGLで確保したメモリをVulkanから参照する方法は今のところありません。
Vulkanでは、メモリオブジェクトをOpaque FDとして書き出す機能が`VK_KHR_external_memory_fd`で提供されています。これは特定の構造体を`pNext`に指定しなくても利用できます。
また、OpenGL側では拡張機能で**Memory Object**を利用でき、これを使うとOpaque FDをOpenGLにimportしてきて、テクスチャやバッファに紐づけることが可能です。というわけで、これらのオブジェクトを使用することでVulkanのメモリをOpenGLでテクスチャから参照することができそうです。

```rust
let mut mo = 0;
unsafe {
    gl::raw::glCreateMemoryObjectsEXT(1, &mut mo);
    gl::raw::glImportMemoryFdEXT(mo, size, handle_type, fd);
    gl::raw::glBindTexture(gl::raw::GL_TEXTURE_2D, gl_tex);
    gl::raw::glTexStorageMem2DEXT(gl::raw::GL_TEXTURE_2D, 1, gl::raw:GL_RGBA8, mode.hdisplay as _, mode.vdisplay as _, mo, 0);
    gl::raw::glBindTexture(gl::raw::GL_TEXTURE_2D, 0);
}
```

さて、プログラムとしては上でよいのですが、残念ながら自身の環境では動きませんでした。
GBMデバイスでEGLを初期化した場合、EGLのVendorはMesaになります。NVIDIAではありません。
そして、Mesaのドライバはなんと`glCreateMemoryObject`に必要な`GL_EXT_memory_object`を**実装していない**のでした[^egl-memory-ext]。うせやん

メモリ共有系の拡張が軒並み使えなくて、割と八方塞がりな感じになってきました。ということで最終手段です。

## Vulkan Imageを毎フレームreadbackして、システムメモリ経由でGBMバッファにコピーする

割ととりたくなかった手法でした。というのも毎フレームreadbackするというのは一般的にはあまりにも負荷が高すぎるためです。
とはいえ、Vulkan ImageからGBM Buffer ObjectへGPU上でコピーする手法が存在しない以上、ホスト側のシステムメモリを経由してデータをコピーする以外の選択肢はもはやありません。
ということで、最終手段としてこちらを実装していきます。

まずはGBMのバッファオブジェクトをシステムメモリにマップする方法です。これは`gbm_bo_map`を使うことで行えます。逆にunmapは`gbm_bo_unmap`で行えます。

```rust
pub fn gbm_bo_map(
    bo: *mut gbm_bo, x: u32, y: u32, width: u32, height: u32, flags: u32, stride: *mut u32,
    map_data: *mut *mut libc::c_void
) -> *mut libc::c_void;
pub fn gbm_bo_unmap(bo: *mut gbm_bo, map_data: *mut libc::c_void);
```

`flags`にはマップの種類を指定します。これは、読みこむのであれば`GBM_TRANSFER_READ`を、書き込むのであれば`GBM_TRANSFER_WRITE`を指定します。書き込むと指定した場合、なんからの形で参照されるかunmapをしたタイミングでデータが転送されます。
これによって、一旦Vulkan Imageに書いた内容をReadbackして、それからGBMのバッファオブジェクトのマップ領域に転送することであたかもGBMのバッファオブジェクトに書き込んだようになります。あまりきれいな形ではないですが、これで一応GBMのバッファオブジェクトに書き込むという目的は達成できます。

これで、一旦はDRMを用いた描画はできるようになります。

![EpUyZJLUUAEptnf.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94115/63eac90b-2517-17e3-b9e9-892fb142a857.jpeg)

......できるようにはなりますが、システムメモリを経由しなければならないというのはなんともオーバーヘッドが大きすぎます。せっかくDRMでコンポジタによる合成を回避できたのに、これではあまりDirect Renderingをサポートする意味がありません。

今回はNVIDIA GPUでの動作となるため、NVIDIAで推奨されているもう一つのバッファAPIであるEGLStreamsも試してみます。

## EGLStreamsで描画する

このパートで説明する実装: https://github.com/Pctg-x8/peridot/tree/ft-cradle-eglstreams-render/cradle/direct/src
NVIDIAの中の人による参考実装: https://github.com/aritger/eglstreams-kms-example

EGLStreamsはEGLの拡張の一つである`EGL_KHR_stream`を利用した描画となります。

### DRMでEGLを初期化

バッファAPIの位置付けですので、DRMを初期化するあたりはGBMを利用したものと大きくは変わりません。

#### EGLDeviceを取得する

EGLを初期化するには`EGLDisplay`が必要になりますが、`EGLDisplay`はGBMのときと違ってDRMのファイルデスクリプタ単体で取得することができません。他に`EGLDevice`が必要になります。
`EGLDevice`を取得するには`eglQueryDevicesEXT`を使います。ここから、DRMに対応した（`EGL_EXT_device_drm`拡張を持った）デバイスを探し、そのデバイスのデバイスノードへのパスを`eglQueryDeviceStringEXT`で取得して、それを使ってDRMのファイルデスクリプタをopenします。

```rust
// Extension: EGL_EXT_device_enumeration
pub fn eglQueryDevicesEXT(max_devices: EGLint, devices: *mut EGLDeviceEXT, num_devices: *mut EGLint) -> EGLBoolean;
// Extension: EGL_EXT_device_query
pub fn eglQueryDeviceStringEXT(device: EGLDeviceEXT, name: EGLint) -> *const libc::c_char;
```

`eglQueryDevicesEXT`は、例によって`devices`に`null`を指定することで`num_devices`にデバイス数が入ります。

#### DRMのPlaneを取得する

EGLStreamsはDRMのPlaneに対して描画する形になります。そのため、Planeの情報を取得するのと、Client CapabilityとしてUniversal Plane（`DRM_CLIENT_CAP_UNIVERSAL_PLANES`）を有効にしておく必要があります。これを有効にしないとPlaneの情報が取得できません。

```rust
        // DisplayやContextを作る前にDRM側の準備をしておく必要がある
        // EGLStreamsを使う場合、以下のClient Capが必要になる
        let res = unsafe {
            drm::raw::drmSetClientCap(w.device_fd, drm::raw::DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)
        };
        if res != 0 {
            panic!("Unable to set drm client capability");
        }
        // ...
        // find primary plane for crtc
        let plane_id = drm::mode::PlaneResourcesPtr::get(w.device_fd).expect("Failed to get plane resources")
            .planes()
            .iter()
            .copied()
            .find(|&pid| drm::mode::PlanePtr::get(w.device_fd, pid).map_or(false, |p| {
                // このPlaneは該当のCRTCに紐づくか？
                if (p.possible_crtcs & (1 << crtc_index)) == 0 { return false; }

                // "type"と名前のついたプロパティを探して、その値がDRM_PLANE_TYPE_PRIMARYならこのPlaneを採用する
                let props = drm::mode::ObjectProperties::get(w.device_fd, pid, drm::mode::ObjectType::Plane);
                let (prop_ids, prop_values): (&[u32], &[u64]) = props.as_ref().map_or((&[], &[]), |p| (p.props(), p.prop_values()));
                prop_ids.iter().copied().zip(prop_values.iter().copied())
                    .find(|&(propid, _)|
                        drm::mode::Property::get(w.device_fd, propid)
                            .map_or(false, |p| p.name().to_str().map_or(false, |s| s == "type"))
                    )
                    .map_or(false, |(_, v)| v == drm::raw::DRM_PLANE_TYPE_PRIMARY)
            }))
            .expect("no suitable primary plane");
```

だいぶ複雑な書き方をしているので、少しだけコメントで補足しました。
`drmModeGetPlaneResources`（`drm::mode::PlaneResourcesPtr::get`）でデバイスが持つPlaneのIDを全て取得し、それぞれに対して`drmModeGetPlane`（`drm::mode::PlanePtr::get`）でオブジェクトを取得して、それらのプロパティが要件を満たすかを調べています。
冒頭の全体図でも書いた通り、PlaneはCRTCに紐づきます。そのため、CRTCの検索より後に記述します。

ちなみにEGLStreamsはPlaneに直接描画するためフレームバッファが必要ありませんが、モードセットにはフレームバッファが必要です。この場合はDumb Bufferというものを作って、それでダミーのフレームバッファを作っておきます。Dumb Bufferについては[drm-memoryのドキュメント](https://www.systutorials.com/docs/linux/man/7-drm-memory/)に割と細かく書いてあるため、ここでは解説を省略します。

### ストリームを構成する

EGLStreamsでは、ストリームの出力となる`EGLOutputLayerEXT`とそこへのストリーミング出力を行う`EGLStream`、それから`EGLStream`のソースを生成する`EGLSurface`の3つのオブジェクトが必要になります。

`EGLOutputLayerEXT`を取得するには`eglGetOutputLayersEXT`を使用します。

```rust
// Extension: EGL_EXT_output_base
pub fn eglGetOutputLayersEXT(
    dpy: EGLDisplay, attrib_list: *const EGLAttrib,
    layers: *mut EGLOutputLayerEXT, max_layers: EGLint, num_layers: *mut EGLint
) -> EGLBoolean;
```

`attrib_list`でDRMのPlaneのIDを指定することで、そのPlaneに紐づいたOutputLayerを取得できます。`eglInitialize`の時点でモードセットまで完了していないと、ここで虚無が返ってきます。
複数取得できる形になっていますが、そこから特定の条件を満たすものを選ぶといったことはあんまりやらないと思うので`max_layers`を1にして最初に返ってきたものを使用するので大丈夫です。

`EGLStream`の作成には`eglCreateStreamKHR`を使用します。

```rust
// Extension: EGL_KHR_stream
pub fn eglCreateStreamKHR(dpy: EGLDisplay, attrib_list: *const EGLint) -> EGLStreamKHR;
```

`EGLStream`を作成したら、`EGLOutputLayerEXT`を出力として指定します。`eglStreamConsumerOutputEXT`を使用します。

```rust
// Extension: EGL_EXT_stream_consumer_egloutput
pub fn eglStreamConsumerOutputEXT(dpy: EGLDisplay, stream: EGLStreamKHR, layer: EGLOutputLayerEXT) -> EGLBoolean;
```

最後に、ストリームの生成元となる`EGLSurface`を作成します。ストリームの生成元として使える`EGLSurface`を作成するには`eglCreateStreamProducerSurfaceKHR`を使用します。

```rust
// Extension: EGL_KHR_stream_producer_eglsurface
pub fn eglCreateStreamProducerSurfaceKHR(
    dpy: EGLDisplay, config: EGLConfig, stream: EGLStreamKHR, attrib_list: *const EGLint
) -> EGLSurface;
```

`attrib_list`にはサーフェイスのサイズなどの情報を入れておきます。これで`EGLSurface`まで作成できたので、`eglMakeCurrent`でコンテキストを有効化して普通に描画して`eglSwapBuffers`することでDRMのPlaneにも描画できます。

EGLStreamsを利用した描画では「EGLを経由して描画する」で説明した「Vulkanでイメージに描画して、それをOpenGLテクスチャ経由で拾って書き写す」方法を使うことができます。というのも、この方法で初期化した場合はEGL/OpenGLのベンダがNVIDIAになるため、`GL_EXT_memory_object`が使えます。
なので、ここまで来ればそこから先は「VulkanのメモリをOpenGLで使う」のコードでメモリブロックを共有して、Vulkan側のレンダリングを行ったのちOpenGLのテクスチャをBlitするだけです。
画面の見た目は変わらないので省略します。

## おしまい

API相互利用の嵐でだいぶややこしいお話になりましたが、これでVulkanでdirect-to-displayなレンダリングができるようになりました。

......と言いたいところですが、GBMの方ではシステムメモリ経由でピクセルをコピーしていますし、EGLStreamsでも一旦テクスチャに描いてそれをBlitするという形になっているため、Vulkanからは微妙に"Direct"にはなっていません。
`VK_EXT_external_memory_dma_buf`が使えれば真にdirect-to-displayなレンダリングができそうなのに......

以上です。

---

### 番外編: VK_KHR_displayは？

[MesaのVulkanドライバのコード](https://gitlab.freedesktop.org/mesa/mesa/-/tree/master/src/vulkan)をみている感じでは、どうやらこれでも同じようなDirect Renderingができるらしいです。
ですが、NVIDIAのプロプライエタリドライバだとスワップチェーン作成のタイミングでエラーになってしまいます。
straceでシステムコールをのぞいてみた感じだと、どうやらnvidia-modesetへの最後のioctlでOperation not permittedと言われているらしく（おそらくモードチェンジを適用する的な命令だと思われる）、これはこっち側の実装が何か足りていないのか、ユーザグループ的な問題なのか、それとも単純にドライバのバグなのかはちょっとわからないです。

[^peridot]: https://github.com/Pctg-x8/peridot
[^kmscube]: https://gitlab.freedesktop.org/mesa/kmscube
[^nv_extmem_dma]: 実装を予定しているっぽい話 明確に拡張名は出ていませんが https://www.phoronix.com/scan.php?page=news_item&px=NVIDIA-DMA-BUF-Wayland-KDE
[^egl-memory-ext]: Mesaのドライバでも`GL_EXT_memory_object`を実装しようという動きはあるようです https://gitlab.freedesktop.org/mesa/mesa/-/issues/1824