趣味でゲームエンジン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の構成図はこちらを参考にしました。
まずは最小構成でやってみる(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のマスタ権限をもらうには、drmGetMagic
とdrmAuthMagic
を使用します。
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のファイルデスクリプタをインポートできるようにするには、VkMemoryAllocateInfo
のpNext
に指定するVkImportMemoryFdInfoKHR
のhandleType
にVK_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つの制限があるので、少々回りくどいですが次のような構成でいきます。
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_list
はnull
で大丈夫です。
ディスプレイを得られたあとは、普通にeglInitialize
を使用して初期化し、eglMakeCurrent
でコンテキストを有効化します。今回はEGL/OpenGLではオフスクリーンレンダリングしか行わないので、EGLConfig
やEGLSurface
は不要で全て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
は無視で大丈夫です。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
を使用します。
// 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でテクスチャから参照することができそうです。
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を用いた描画はできるようになります。
......できるようにはなりますが、システムメモリを経由しなければならないというのはなんともオーバーヘッドが大きすぎます。せっかく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
は、例によってdevices
にnull
を指定することで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");
だいぶ複雑な書き方をしているので、少しだけコメントで補足しました。
drmModeGetPlaneResources
(drm::mode::PlaneResourcesPtr::get
)でデバイスが持つPlaneのIDを全て取得し、それぞれに対してdrmModeGetPlane
(drm::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と言われているらしく(おそらくモードチェンジを適用する的な命令だと思われる)、これはこっち側の実装が何か足りていないのか、ユーザグループ的な問題なのか、それとも単純にドライバのバグなのかはちょっとわからないです。
-
実装を予定しているっぽい話 明確に拡張名は出ていませんが https://www.phoronix.com/scan.php?page=news_item&px=NVIDIA-DMA-BUF-Wayland-KDE ↩
-
Mesaのドライバでも
GL_EXT_memory_object
を実装しようという動きはあるようです https://gitlab.freedesktop.org/mesa/mesa/-/issues/1824 ↩