LoginSignup
25
14

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-20

趣味でゲームエンジン1を開発している者です。

ゲームエンジンというからには、やはり排他的なフルスクリーン表示の機能が欲しくなりますね。
近年の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

なお、DRMの構成図はこちらを参考にしました。

まずは最小構成でやってみる(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を使います。

    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のマスタ権限をもらうには、drmGetMagicdrmAuthMagicを使用します。

    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のkmscube2を参考にしました。本当はdrmModeGetResourcesで得たポインタはfreeしないといけないのですが、今回は時間がなかったのでサボっています。
コードは長いですが、やっていることはそんな複雑でもなくて「接続状態のConnectorに紐づくEncoder, CRTCを探している」だけになります。modeのところはConnectorが推奨する画面解像度か、使える中でもっとも高い解像度を探しています。

        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を使用します。

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

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

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を使用します。

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

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

モードセット!

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

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はデバイス拡張となりますので、vkCreateDeviceのタイミングで指定します。
dma-bufのファイルデスクリプタをインポートできるようにするには、VkMemoryAllocateInfopNextに指定するVkImportMemoryFdInfoKHRhandleTypeVK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXTを指定します。こうすると、同構造体のfdはdma-bufを指しているものとして認識されます。

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が実装されていません3。なんてこった

EGLを経由して描画する

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

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

DrmVulkanInterop.png

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

EGLの初期化

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

// 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_listnullで大丈夫です。
ディスプレイを得られたあとは、普通にeglInitializeを使用して初期化し、eglMakeCurrentでコンテキストを有効化します。今回はEGL/OpenGLではオフスクリーンレンダリングしか行わないので、EGLConfigEGLSurfaceは不要で全てnullを指定すれば大丈夫です。

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

EGLではEGL_EXT_image_dma_buf_importという拡張を使用することでdma-bufをimportしてくることができます。幸いにも自分の環境ではこれが実装されているようなので、これを使うことでdma-bufのメモリ領域をEGLImageとして利用できます。
EGLImageを作るにはeglCreateImageKHRを使用します。

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

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

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

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

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

これはバインドしたオブジェクトに対する操作になります。targetGL_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でテクスチャから参照することができそうです。

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実装していないのでした4。うせやん

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

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

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

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

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

......できるようにはなりますが、システムメモリを経由しなければならないというのはなんともオーバーヘッドが大きすぎます。せっかく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します。

// 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は、例によってdevicesnullを指定することでnum_devicesにデバイス数が入ります。

DRMのPlaneを取得する

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

        // 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");

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

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

ストリームを構成する

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

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

// 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を使用します。

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

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

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

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

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


  1. https://github.com/Pctg-x8/peridot 

  2. https://gitlab.freedesktop.org/mesa/kmscube 

  3. 実装を予定しているっぽい話 明確に拡張名は出ていませんが https://www.phoronix.com/scan.php?page=news_item&px=NVIDIA-DMA-BUF-Wayland-KDE 

  4. MesaのドライバでもGL_EXT_memory_objectを実装しようという動きはあるようです https://gitlab.freedesktop.org/mesa/mesa/-/issues/1824 

25
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
14