From a6c2e44cc429bb7c2d9630f91967aee829ad1b95 Mon Sep 17 00:00:00 2001 From: Matej Hrica Date: Wed, 5 Nov 2025 12:38:05 +0100 Subject: [PATCH 1/6] gui_vm: Configure gpu with more reasonable blob parameters The memory unit is bytes, so this was nowhere near enough, now it's set to 4GiB. VIRGLRENDERER_USE_EXTERNAL_BLOB is required fo blob mapping to work properly. Signed-off-by: Matej Hrica --- examples/gui_vm/src/main.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/gui_vm/src/main.rs b/examples/gui_vm/src/main.rs index 660a2e21a..c1bac1ce6 100644 --- a/examples/gui_vm/src/main.rs +++ b/examples/gui_vm/src/main.rs @@ -8,8 +8,8 @@ use gtk_display::{ use krun_sys::{ KRUN_LOG_LEVEL_TRACE, KRUN_LOG_LEVEL_WARN, KRUN_LOG_STYLE_ALWAYS, KRUN_LOG_TARGET_DEFAULT, VIRGLRENDERER_RENDER_SERVER, VIRGLRENDERER_THREAD_SYNC, VIRGLRENDERER_USE_ASYNC_FENCE_CB, - VIRGLRENDERER_USE_EGL, VIRGLRENDERER_VENUS, krun_add_display, krun_add_input_device, - krun_add_input_device_fd, krun_create_ctx, krun_display_set_dpi, + VIRGLRENDERER_USE_EGL, VIRGLRENDERER_USE_EXTERNAL_BLOB, VIRGLRENDERER_VENUS, krun_add_display, + krun_add_input_device, krun_add_input_device_fd, krun_create_ctx, krun_display_set_dpi, krun_display_set_physical_size, krun_display_set_refresh_rate, krun_init_log, krun_set_display_backend, krun_set_exec, krun_set_gpu_options2, krun_set_root, krun_set_vm_config, krun_start_enter, @@ -156,8 +156,9 @@ fn krun_thread( | VIRGLRENDERER_VENUS | VIRGLRENDERER_RENDER_SERVER | VIRGLRENDERER_THREAD_SYNC - | VIRGLRENDERER_USE_ASYNC_FENCE_CB, - 4096 + | VIRGLRENDERER_USE_ASYNC_FENCE_CB + | VIRGLRENDERER_USE_EXTERNAL_BLOB, + 1 << 32 ))?; krun_call!(krun_set_root(ctx, args.root_dir.as_ptr()))?; From 507ec30dbe04f4809f13b6ba2afcb0e597cc5d8e Mon Sep 17 00:00:00 2001 From: Matej Hrica Date: Fri, 7 Nov 2025 15:31:45 +0100 Subject: [PATCH 2/6] rutabaga_gfx/virgl_renderer: add fallback for DMABUF handle and info retrieval This patch enhances the `create_3d` method for VirglRenderer to fall back to virgl_renderer_resource_get_info_ext and virgl_renderer_get_fd_for_texture when export_blob and query both fail to return a DMABUF handle or 3D info. This ensures that resources created via create_3d still obtain valid handle and metadata when the primary export path is unavailable. This enables the downstream users(e.g vhost-device-gpu) to access the 3D info after `resource_create_3d()` and forward it correctly to the VHOST scanout handlers(e.g. via VHOST_USER_GPU_DMABUF_SCANOUT2). Cherry-picked from magma-gpu/rutabaga_gfx repository: https://github.com/magma-gpu/rutabaga_gfx/commit/a84179a072c723ca22c6841b225092e4086d800f Original author: Dorinda Bassey Co-Authored-By: Dorinda Bassey Signed-off-by: Matej Hrica --- .../src/generated/virgl_renderer_bindings.rs | 18 +++++++ src/rutabaga_gfx/src/virgl_renderer.rs | 49 ++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/rutabaga_gfx/src/generated/virgl_renderer_bindings.rs b/src/rutabaga_gfx/src/generated/virgl_renderer_bindings.rs index 970e500d1..6ce8cdded 100644 --- a/src/rutabaga_gfx/src/generated/virgl_renderer_bindings.rs +++ b/src/rutabaga_gfx/src/generated/virgl_renderer_bindings.rs @@ -316,12 +316,30 @@ pub struct virgl_renderer_resource_info { pub stride: u32, pub drm_fourcc: ::std::os::raw::c_int, } +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct virgl_renderer_resource_info_ext { + pub version: ::std::os::raw::c_int, + pub base: virgl_renderer_resource_info, + pub has_dmabuf_export: bool, + _padding1: [u8; 3], + pub planes: ::std::os::raw::c_int, + _padding2: [u8; 4], + pub modifiers: u64, + pub d3d_tex2d: *mut ::std::os::raw::c_void, +} extern "C" { pub fn virgl_renderer_resource_get_info( res_handle: ::std::os::raw::c_int, info: *mut virgl_renderer_resource_info, ) -> ::std::os::raw::c_int; } +extern "C" { + pub fn virgl_renderer_resource_get_info_ext( + res_handle: ::std::os::raw::c_int, + info: *mut virgl_renderer_resource_info_ext, + ) -> ::std::os::raw::c_int; +} extern "C" { pub fn virgl_renderer_cleanup(cookie: *mut ::std::os::raw::c_void); } diff --git a/src/rutabaga_gfx/src/virgl_renderer.rs b/src/rutabaga_gfx/src/virgl_renderer.rs index bbb312887..3d4dd8a16 100644 --- a/src/rutabaga_gfx/src/virgl_renderer.rs +++ b/src/rutabaga_gfx/src/virgl_renderer.rs @@ -497,9 +497,54 @@ impl RutabagaComponent for VirglRenderer { let ret = unsafe { virgl_renderer_resource_create(&mut args, null_mut(), 0) }; ret_to_res(ret)?; + let mut resource_handle: Option> = self.export_blob(resource_id).ok(); + let mut resource_info_3d: Option = self.query(resource_id).ok(); + + // Fallback if export_blob and query both fail to return a DMABUF handle or 3D info. + if resource_handle.is_none() && resource_info_3d.is_none() { + let mut info_ext = Default::default(); + + // SAFETY: virglrenderer is initialized; info_ext is a valid pointer. + // Function writes into info_ext but does not retain the pointer after returning. + let ret_info = + unsafe { virgl_renderer_resource_get_info_ext(resource_id as i32, &mut info_ext) }; + + if ret_info == 0 { + // Successfully got info_ext, now try to get the FD. + let mut fd = -1; + + // SAFETY: virglrenderer is initialized; tex_id is from valid resource. + // fd is written by the call and remains owned by the caller. + let ret_fd = + unsafe { virgl_renderer_get_fd_for_texture(info_ext.base.tex_id, &mut fd) }; + + if ret_fd == 0 && fd >= 0 { + // Successfully got DMABUF FD. + let fourcc: u32 = info_ext.base.drm_fourcc as u32; + + // SAFETY: `fd` is validated to be >= 0 and uniquely owned. + let owned_fd = unsafe { SafeDescriptor::from_raw_descriptor(fd) }; + + resource_handle = Some(Arc::new(RutabagaHandle { + os_handle: owned_fd, + handle_type: RUTABAGA_MEM_HANDLE_TYPE_DMABUF, + })); + + resource_info_3d = Some(Resource3DInfo { + width: info_ext.base.width, + height: info_ext.base.height, + drm_fourcc: fourcc, + strides: [info_ext.base.stride, 0, 0, 0], + offsets: [0, 0, 0, 0], + modifier: info_ext.modifiers, + }); + } + } + } + Ok(RutabagaResource { resource_id, - handle: self.export_blob(resource_id).ok(), + handle: resource_handle, blob: false, blob_mem: 0, blob_flags: 0, @@ -507,7 +552,7 @@ impl RutabagaComponent for VirglRenderer { #[cfg(target_os = "macos")] map_ptr: None, info_2d: None, - info_3d: self.query(resource_id).ok(), + info_3d: resource_info_3d, vulkan_info: None, backing_iovecs: None, component_mask: 1 << (RutabagaComponentType::VirglRenderer as u8), From 7c00a11250890a0022532ec4e7169a3f95217d2d Mon Sep 17 00:00:00 2001 From: Matej Hrica Date: Wed, 5 Nov 2025 12:42:53 +0100 Subject: [PATCH 3/6] examples/gui_vm: Use a GUI_VM_LOG env variable for controlling log Instead of hardcoding the log level specify it via an env variable Signed-off-by: Matej Hrica --- examples/gui_vm/src/main.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/gui_vm/src/main.rs b/examples/gui_vm/src/main.rs index c1bac1ce6..f10c07b58 100644 --- a/examples/gui_vm/src/main.rs +++ b/examples/gui_vm/src/main.rs @@ -14,7 +14,6 @@ use krun_sys::{ krun_set_display_backend, krun_set_exec, krun_set_gpu_options2, krun_set_root, krun_set_vm_config, krun_start_enter, }; -use log::LevelFilter; use regex::{Captures, Regex}; use std::ffi::{CString, c_void}; use std::fmt::Display; @@ -225,9 +224,7 @@ fn krun_thread( } fn main() -> anyhow::Result<()> { - env_logger::builder() - .filter_level(LevelFilter::Debug) - .init(); + env_logger::builder().parse_env("GUI_VM_LOG").init(); let args = Args::parse(); let mut per_display_inputs = vec![vec![]; args.display.len()]; From 4529e5667eed4022e81dee20aab6cfb81475e8dc Mon Sep 17 00:00:00 2001 From: Matej Hrica Date: Wed, 5 Nov 2025 13:02:35 +0100 Subject: [PATCH 4/6] krun_display: Introduce KRUN_DISPLAY_FEATURE_DMABUF_CONSUMER feature Introduce a dmabuf display consumer feature into the header, and handle the new extended vtable internally. Signed-off-by: Matej Hrica --- src/krun_display/libkrun_display.h | 91 ++++++++ src/krun_display/src/c_to_rust.rs | 243 ++++++++++++++----- src/krun_display/src/lib.rs | 34 ++- src/krun_display/src/rust_to_c.rs | 362 ++++++++++++++++++++++------- src/libkrun/src/api_tests.rs | 110 +++++++++ src/libkrun/src/lib.rs | 33 ++- 6 files changed, 726 insertions(+), 147 deletions(-) create mode 100644 src/libkrun/src/api_tests.rs diff --git a/src/krun_display/libkrun_display.h b/src/krun_display/libkrun_display.h index 975c080ba..4a2b300c7 100644 --- a/src/krun_display/libkrun_display.h +++ b/src/krun_display/libkrun_display.h @@ -39,6 +39,14 @@ extern "C" { */ #define KRUN_DISPLAY_FEATURE_BASIC_FRAMEBUFFER 1 +/** + * Indicates support for DMABUF-based display operations where the display backend consumes + * dmabufs allocated by libkrun/rutabaga. + * If supported, the implementation must provide `disable_scanout`, `configure_scanout_dmabuf`, + * and `present_dmabuf`. + */ +#define KRUN_DISPLAY_FEATURE_DMABUF_CONSUMER 2 + /** * Called to create a display instance. * @@ -145,6 +153,79 @@ struct krun_rect { */ typedef int32_t (*krun_display_present_frame_fn)(void *instance, uint32_t scanout_id, uint32_t frame_id, const struct krun_rect* damage_area); +struct krun_display_dmabuf_export { + int dmabuf_fds[4]; + uint32_t n_planes; + uint32_t width; + uint32_t height; + uint32_t fourcc; + uint32_t strides[4]; + uint32_t offsets[4]; + uint64_t modifier; +}; + +/** + * Imports a DMABUF into the display backend for later use. + * The imported dmabuf can be shared across multiple scanouts. + * + * Arguments: + * "instance" - userdata set by `krun_display_create`, represents this/self argument + * "dmabuf_export" - Pointer to dmabuf metadata including fds, dimensions, format, strides, offsets and modifier. + * + * Returns: + * A positive dmabuf_id on success or a negative error code (KRUN_DISPLAY_ERR_*) otherwise. + */ +typedef int32_t (*krun_display_import_dmabuf_fn)(void *instance, + const struct krun_display_dmabuf_export *dmabuf_export); + +/** + * Unreferences/frees a previously imported DMABUF. + * + * Arguments: + * "instance" - userdata set by `krun_display_create`, represents this/self argument + * "dmabuf_id" - The ID of the dmabuf to free, as returned by import_dmabuf. + * + * Returns: + * Zero on success or a negative error code (KRUN_DISPLAY_ERR_*) otherwise. + */ +typedef int32_t (*krun_display_unref_dmabuf_fn)(void *instance, uint32_t dmabuf_id); + +/** + * Configures a display scanout to use a previously imported DMABUF. + * + * Arguments: + * "instance" - userdata set by `krun_display_create`, represents this/self argument + * "scanout_id" - The identifier of the scanout to configure. + * "display_width" - The original width of the display in pixels. + * "display_height" - The original height of the display in pixels. + * "dmabuf_id" - The ID of the imported dmabuf to use. + * "src_rect" - (Optional) Source rectangle defining the sub-area of the dmabuf to display. + * If NULL, the entire dmabuf is used. + * + * Returns: + * Zero on success or a negative error code (KRUN_DISPLAY_ERR_*) otherwise. + */ +typedef int32_t (*krun_display_configure_scanout_dmabuf_fn)(void *instance, + uint32_t scanout_id, + uint32_t display_width, + uint32_t display_height, + uint32_t dmabuf_id, + const struct krun_rect *src_rect); + +/** + * Presents a DMABUF-backed frame to the display. + * + * Arguments: + * "instance" - userdata set by `krun_display_create`, represents this/self argument + * "scanout_id" - The identifier of the scanout on which to present the frame. + * "damage_area" - (Optional) Optimization hint describing the area that has changed since the last call to + * present_dmabuf. If NULL, the entire frame is assumed to be damaged. + * + * Returns: + * Zero on success or a negative error or a negative error code (KRUN_DISPLAY_ERR_*) otherwise. + */ +typedef int32_t (*krun_display_present_dmabuf_fn)(void *instance, uint32_t scanout_id, const struct krun_rect* damage_area); + /** * Defines the set of callbacks for a display implementation. * This structure holds function pointers that a display backend implements to integrate with the libkrun. @@ -170,8 +251,18 @@ struct krun_display_basic_framebuffer_vtable { krun_display_present_frame_fn present_frame; // Required by KRUN_DISPLAY_FEATURE_BASIC_FRAMEBUFFER }; +struct krun_display_dmabuf_vtable { + struct krun_display_basic_framebuffer_vtable basic_framebuffer; + // DMABUF-specific methods + krun_display_import_dmabuf_fn import_dmabuf; // Required by KRUN_DISPLAY_FEATURE_DMABUF_CONSUMER + krun_display_unref_dmabuf_fn unref_dmabuf; // Required by KRUN_DISPLAY_FEATURE_DMABUF_CONSUMER + krun_display_configure_scanout_dmabuf_fn configure_scanout_dmabuf; // Required by KRUN_DISPLAY_FEATURE_DMABUF_CONSUMER + krun_display_present_dmabuf_fn present_dmabuf; // Required by KRUN_DISPLAY_FEATURE_DMABUF_CONSUMER +}; + union krun_display_vtable { struct krun_display_basic_framebuffer_vtable basic_framebuffer; + struct krun_display_dmabuf_vtable dmabuf; }; struct krun_display_backend { diff --git a/src/krun_display/src/c_to_rust.rs b/src/krun_display/src/c_to_rust.rs index a15a5bb2a..749fd602e 100644 --- a/src/krun_display/src/c_to_rust.rs +++ b/src/krun_display/src/c_to_rust.rs @@ -1,6 +1,6 @@ use crate::{ - DisplayBackendBasicFramebuffer, DisplayBackendError, DisplayBasicFramebufferVtable, - DisplayFeatures, DisplayVtable, Rect, ResourceFormat, header, + DisplayBackendBasicFramebuffer, DisplayBackendError, DisplayFeatures, DisplayVtable, Rect, + ResourceFormat, header, }; use log::{error, warn}; use static_assertions::assert_not_impl_any; @@ -33,18 +33,10 @@ macro_rules! into_rust_result { }; } -macro_rules! method_call { - ($self:ident.$method:ident($($args:expr),*) ) => { - unsafe { - $self.vtable.$method - .ok_or(DisplayBackendError::MethodNotSupported)?( $self.instance, $($args),* ) - } - }; -} - pub struct DisplayBackendInstance { instance: *mut c_void, - vtable: DisplayBasicFramebufferVtable, + vtable: DisplayVtable, + features: DisplayFeatures, } // By design the struct is !Send and !Sync to allow for the implementation to safely assume that @@ -53,7 +45,10 @@ assert_not_impl_any!(DisplayBackendInstance: Sync, Send); impl Drop for DisplayBackendInstance { fn drop(&mut self) { - let Some(destroy_fn) = self.vtable.destroy else { + // SAFETY: destroy is at the same offset in both vtable variants + let destroy_fn = unsafe { self.vtable.basic_framebuffer.destroy }; + + let Some(destroy_fn) = destroy_fn else { return; }; @@ -63,6 +58,12 @@ impl Drop for DisplayBackendInstance { } } +impl DisplayBackendInstance { + pub fn supports_dmabuf(&self) -> bool { + self.features.contains(DisplayFeatures::DMABUF_CONSUMER) + } +} + impl DisplayBackendBasicFramebuffer for DisplayBackendInstance { fn configure_scanout( &mut self, @@ -73,43 +74,58 @@ impl DisplayBackendBasicFramebuffer for DisplayBackendInstance { height: u32, format: ResourceFormat, ) -> Result<(), DisplayBackendError> { - into_rust_result!(method_call! { - self.configure_scanout( - scanout_id, - display_width, - display_height, - width, - height, - format as u32 - ) + // SAFETY: configure_scanout is at the same offset in both vtable variants + let configure_scanout = unsafe { self.vtable.basic_framebuffer.configure_scanout }; + + into_rust_result!(unsafe { + configure_scanout.ok_or(DisplayBackendError::MethodNotSupported)?( + self.instance, + scanout_id, + display_width, + display_height, + width, + height, + format as u32, + ) }) } fn disable_scanout(&mut self, scanout_id: u32) -> Result<(), DisplayBackendError> { - into_rust_result! { - method_call! { - self.disable_scanout(scanout_id) - } - } + // SAFETY: disable_scanout is at the same offset in both vtable variants + let disable_scanout = unsafe { self.vtable.basic_framebuffer.disable_scanout }; + + into_rust_result!(unsafe { + disable_scanout.ok_or(DisplayBackendError::MethodNotSupported)?( + self.instance, + scanout_id, + ) + }) } // Soundness note: this method has to take &mut self in order for the lifetime of the returned // slice to be tied to self. This way the returned slice cannot stay borrowed once another method // on self is called. fn alloc_frame(&mut self, scanout_id: u32) -> Result<(u32, &mut [u8]), DisplayBackendError> { + // SAFETY: alloc_frame is at the same offset in both vtable variants + let alloc_frame = unsafe { self.vtable.basic_framebuffer.alloc_frame }; + let mut buffer: *mut u8 = null_mut(); let mut buffer_len: usize = 0; let frame_id = into_rust_result! { - method_call! { - self.alloc_frame(scanout_id, &raw mut buffer, &raw mut buffer_len) + unsafe { + alloc_frame + .ok_or(DisplayBackendError::MethodNotSupported)?( + self.instance, + scanout_id, + &raw mut buffer, + &raw mut buffer_len + ) }, result @ 0.. => Ok(result as u32) }?; assert_ne!(buffer, null_mut()); assert_ne!(buffer_len, 0); - // SAFETY: We have obtained the buffer and buffer_len from the krun_gtk_display impl. Because - // the alloc_frame_fn return an error we assume they should be valid. let buffer = unsafe { slice_from_raw_parts_mut(buffer, buffer_len) .as_mut() @@ -124,12 +140,108 @@ impl DisplayBackendBasicFramebuffer for DisplayBackendInstance { frame_id: u32, rect: Option<&Rect>, ) -> Result<(), DisplayBackendError> { + // SAFETY: present_frame is at the same offset in both vtable variants + let present_frame = unsafe { self.vtable.basic_framebuffer.present_frame }; + + let rect_ptr = rect.map_or(null(), std::ptr::from_ref); + into_rust_result!(unsafe { + present_frame.ok_or(DisplayBackendError::MethodNotSupported)?( + self.instance, + scanout_id, + frame_id, + rect_ptr, + ) + }) + } +} + +impl DisplayBackendInstance { + pub fn import_dmabuf( + &mut self, + dmabuf_export: &crate::DmabufExport, + ) -> Result { + if !self.features.contains(DisplayFeatures::DMABUF_CONSUMER) { + return Err(DisplayBackendError::MethodNotSupported); + } + into_rust_result! { - method_call!{ - self.present_frame(scanout_id, frame_id, rect.map(|r| r as *const _).unwrap_or(null())) - } + unsafe { + self.vtable + .dmabuf + .import_dmabuf + .ok_or(DisplayBackendError::MethodNotSupported)?( + self.instance, + dmabuf_export as *const _, + ) + }, + result @ 0.. => Ok(result as u32) } } + + pub fn unref_dmabuf(&mut self, dmabuf_id: u32) -> Result<(), DisplayBackendError> { + if !self.features.contains(DisplayFeatures::DMABUF_CONSUMER) { + return Err(DisplayBackendError::MethodNotSupported); + } + + into_rust_result!(unsafe { + self.vtable + .dmabuf + .unref_dmabuf + .ok_or(DisplayBackendError::MethodNotSupported)?( + self.instance, dmabuf_id + ) + }) + } + + pub fn configure_scanout_dmabuf( + &mut self, + scanout_id: u32, + display_width: u32, + display_height: u32, + dmabuf_id: u32, + src_rect: Option<&Rect>, + ) -> Result<(), DisplayBackendError> { + if !self.features.contains(DisplayFeatures::DMABUF_CONSUMER) { + return Err(DisplayBackendError::MethodNotSupported); + } + + let rect_ptr = src_rect.map_or(null(), std::ptr::from_ref); + into_rust_result!(unsafe { + self.vtable + .dmabuf + .configure_scanout_dmabuf + .ok_or(DisplayBackendError::MethodNotSupported)?( + self.instance, + scanout_id, + display_width, + display_height, + dmabuf_id, + rect_ptr, + ) + }) + } + + pub fn present_dmabuf( + &mut self, + scanout_id: u32, + rect: Option<&Rect>, + ) -> Result<(), DisplayBackendError> { + if !self.features.contains(DisplayFeatures::DMABUF_CONSUMER) { + return Err(DisplayBackendError::MethodNotSupported); + } + + let rect_ptr = rect.map_or(null(), std::ptr::from_ref); + into_rust_result!(unsafe { + self.vtable + .dmabuf + .present_dmabuf + .ok_or(DisplayBackendError::MethodNotSupported)?( + self.instance, + scanout_id, + rect_ptr, + ) + }) + } } #[derive(Copy, Clone)] @@ -154,37 +266,64 @@ impl DisplayBackend<'_> { } assert!(self.verify()); + let features = DisplayFeatures::from_bits_retain(self.features); + Ok(DisplayBackendInstance { instance, - // SAFETY: we have checked the feature flags, so basic_framebuffer should be populated - vtable: unsafe { self.vtable.basic_framebuffer }, + vtable: self.vtable, + features, }) } pub fn verify(&self) -> bool { let features = DisplayFeatures::from_bits_retain(self.features); - // This requirement might change in the future when we add support for alternatives to this - if !features.contains(DisplayFeatures::BASIC_FRAMEBUFFER) { - error!("This version of libkrun requires BASIC_FRAMEBUFFER feature"); + // Require at least one display feature + if !features.contains(DisplayFeatures::BASIC_FRAMEBUFFER) + && !features.contains(DisplayFeatures::DMABUF_CONSUMER) + { + error!( + "This version of libkrun requires BASIC_FRAMEBUFFER or DMABUF_CONSUMER display feature" + ); return false; } for feature in features { - if feature.contains(DisplayFeatures::BASIC_FRAMEBUFFER) { - // SAFETY: We have checked the feature flag is enabled, so we should be able to - // access the union field. - if unsafe { - self.vtable.basic_framebuffer.disable_scanout.is_none() - || self.vtable.basic_framebuffer.configure_scanout.is_none() - || self.vtable.basic_framebuffer.alloc_frame.is_none() - || self.vtable.basic_framebuffer.present_frame.is_none() - } { - error!("Missing required methods for BASIC_FRAMEBUFFER"); - return false; + match feature { + DisplayFeatures::BASIC_FRAMEBUFFER => { + // SAFETY: We have checked the feature flag is enabled, so we should be able to + // access the these union fields. + if unsafe { + self.vtable.basic_framebuffer.disable_scanout.is_none() + || self.vtable.basic_framebuffer.configure_scanout.is_none() + || self.vtable.basic_framebuffer.alloc_frame.is_none() + || self.vtable.basic_framebuffer.present_frame.is_none() + } { + error!("Missing required methods for BASIC_FRAMEBUFFER"); + return false; + } + } + DisplayFeatures::DMABUF_CONSUMER => { + // SAFETY: We have checked the feature flag is enabled, so we should be able to + // access the these union fields. + if unsafe { + self.vtable + .dmabuf + .basic_framebuffer + .disable_scanout + .is_none() + || self.vtable.dmabuf.import_dmabuf.is_none() + || self.vtable.dmabuf.unref_dmabuf.is_none() + || self.vtable.dmabuf.configure_scanout_dmabuf.is_none() + || self.vtable.dmabuf.present_dmabuf.is_none() + } { + error!("Missing required methods for DMABUF_CONSUMER"); + return false; + } + } + features => { + warn!("Unknown display features ({features:x}) will be ignored") } - } else { - warn!("Unknown display features ({feature:x}) will be ignored") } } true diff --git a/src/krun_display/src/lib.rs b/src/krun_display/src/lib.rs index 2297951d5..07d711246 100644 --- a/src/krun_display/src/lib.rs +++ b/src/krun_display/src/lib.rs @@ -13,13 +13,15 @@ use thiserror::Error; dead_code, unused_variables )] -mod header { +pub mod header { include!(concat!(env!("OUT_DIR"), "/display_header.rs")); } bitflags! { + #[derive(PartialEq, Eq)] pub struct DisplayFeatures: u64 { const BASIC_FRAMEBUFFER = header::KRUN_DISPLAY_FEATURE_BASIC_FRAMEBUFFER as u64; + const DMABUF_CONSUMER = header::KRUN_DISPLAY_FEATURE_DMABUF_CONSUMER as u64; } } @@ -75,6 +77,34 @@ impl TryFrom for ResourceFormat { } } -pub type DisplayVtable = header::krun_display_vtable; +// V1 ABI: Before dmabuf support was added (backward compatibility) +#[repr(C)] +pub struct DisplayBackendV1 { + pub features: u64, + pub create_userdata: *const std::ffi::c_void, + pub create: header::krun_display_create_fn, + pub vtable: DisplayVtableV1, +} + +#[repr(C)] +pub union DisplayVtableV1 { + pub basic_framebuffer: DisplayBasicFramebufferVtable, +} + +// V2 ABI: Current version with dmabuf support +pub type DisplayBackendV2 = header::krun_display_backend; +pub type DisplayVtableV2 = header::krun_display_vtable; + +// Known vtables pub type DisplayBasicFramebufferVtable = header::krun_display_basic_framebuffer_vtable; +pub type DisplayDmabufVtable = header::krun_display_dmabuf_vtable; + +// Default to V2 +pub type DisplayVtable = DisplayVtableV2; + +const _: () = { + assert!(std::mem::size_of::() < std::mem::size_of::(),); +}; + pub type Rect = header::krun_rect; +pub type DmabufExport = header::krun_display_dmabuf_export; diff --git a/src/krun_display/src/rust_to_c.rs b/src/krun_display/src/rust_to_c.rs index 7bee37a1a..a10f2b650 100644 --- a/src/krun_display/src/rust_to_c.rs +++ b/src/krun_display/src/rust_to_c.rs @@ -1,6 +1,6 @@ use crate::{ - DisplayBackend, DisplayBackendError, DisplayBasicFramebufferVtable, DisplayFeatures, - DisplayVtable, Rect, ResourceFormat, + DisplayBackend, DisplayBackendError, DisplayBasicFramebufferVtable, DisplayDmabufVtable, + DisplayFeatures, DisplayVtable, DmabufExport, Rect, ResourceFormat, }; use log::error; use std::ffi::c_void; @@ -35,118 +35,304 @@ pub trait DisplayBackendBasicFramebuffer { ) -> Result<(), DisplayBackendError>; } +pub trait DisplayBackendDmabuf: DisplayBackendBasicFramebuffer { + fn import_dmabuf(&mut self, dmabuf_export: &DmabufExport) -> Result; + + fn unref_dmabuf(&mut self, dmabuf_id: u32) -> Result<(), DisplayBackendError>; + + fn configure_scanout_dmabuf( + &mut self, + scanout_id: u32, + display_width: u32, + display_height: u32, + dmabuf_id: u32, + src_rect: Option<&Rect>, + ) -> Result<(), DisplayBackendError>; + + fn present_dmabuf( + &mut self, + scanout_id: u32, + rect: Option<&Rect>, + ) -> Result<(), DisplayBackendError>; +} + pub trait IntoDisplayBackend { fn into_display_backend(userdata: Option<&T>) -> DisplayBackend<'_>; } +pub fn into_display_backend_basic_framebuffer< + T: Sync, + I: DisplayBackendBasicFramebuffer + DisplayBackendNew, +>( + userdata: Option<&T>, +) -> DisplayBackend<'_> { + extern "C" fn create_fn>( + instance: *mut *mut c_void, + userdata: *const c_void, + _reserved: *const c_void, + ) -> i32 { + unsafe { + assert_ne!( + instance, + null_mut(), + "Pointer to location where to create instance cannot be null" + ); + let userdata_ref = (userdata as *const T).as_ref(); + *(instance as *mut *mut I) = Box::into_raw(Box::new(I::new(userdata_ref))); + } + 0 + } + + extern "C" fn destroy_fn(instance: *mut c_void) -> i32 { + drop(unsafe { Box::from_raw(instance as *mut I) }); + 0 + } + + fn cast_instance<'a, I: DisplayBackendBasicFramebuffer>(instance: *mut c_void) -> &'a mut I { + assert_ne!(instance, null_mut()); + unsafe { &mut *(instance as *mut I) } + } + + extern "C" fn configure_scanout_fn( + instance: *mut c_void, + scanout_id: u32, + display_width: u32, + display_height: u32, + width: u32, + height: u32, + format: u32, + ) -> i32 { + let Ok(format) = ResourceFormat::try_from(format) else { + error!("Unknown display format: {format}"); + return DisplayBackendError::InvalidParam as i32; + }; + + from_rust_result(cast_instance::(instance).configure_scanout( + scanout_id, + display_width, + display_height, + width, + height, + format, + )) + } + + extern "C" fn disable_scanout_fb( + instance: *mut c_void, + scanout_id: u32, + ) -> i32 { + from_rust_result(cast_instance::(instance).disable_scanout(scanout_id)) + } + + extern "C" fn alloc_frame( + instance: *mut c_void, + scanout_id: u32, + buffer: *mut *mut u8, + buffer_size: *mut usize, + ) -> i32 { + match cast_instance::(instance).alloc_frame(scanout_id) { + Ok((frame_id, allocated_buffer)) => { + unsafe { + *buffer_size = allocated_buffer.len(); + *buffer = allocated_buffer.as_mut_ptr(); + } + frame_id as i32 + } + Err(e) => e as i32, + } + } + + extern "C" fn present_frame( + instance: *mut c_void, + scanout_id: u32, + frame_id: u32, + rect: *const Rect, + ) -> i32 { + // SAFETY: The pointer obtained from the bindings should be safe + let rect: Option<&Rect> = unsafe { ptr_to_option_ref(rect) }; + from_rust_result(cast_instance::(instance).present_frame(scanout_id, frame_id, rect)) + } + + DisplayBackend { + create_userdata: userdata.map_or(null(), |t| ptr::from_ref(t) as *const c_void), + create_userdata_lifetime: PhantomData, + features: DisplayFeatures::BASIC_FRAMEBUFFER.bits(), + create_fn: Some(create_fn::), + vtable: DisplayVtable { + basic_framebuffer: DisplayBasicFramebufferVtable { + destroy: Some(destroy_fn::), + configure_scanout: Some(configure_scanout_fn::), + present_frame: Some(present_frame::), + alloc_frame: Some(alloc_frame::), + disable_scanout: Some(disable_scanout_fb::), + }, + }, + } +} + impl> IntoDisplayBackend for I { fn into_display_backend(userdata: Option<&T>) -> DisplayBackend<'_> { - extern "C" fn create_fn>( - instance: *mut *mut c_void, - userdata: *const c_void, - _reserved: *const c_void, - ) -> i32 { - unsafe { - assert_ne!( - instance, - null_mut(), - "Pointer to location where to create instance cannot be null" - ); - let userdata_ref = (userdata as *const T).as_ref(); - *(instance as *mut *mut I) = Box::into_raw(Box::new(I::new(userdata_ref))); - } - 0 - } + into_display_backend_basic_framebuffer::(userdata) + } +} - extern "C" fn destroy_fn(instance: *mut c_void) -> i32 { - drop(unsafe { Box::from_raw(instance as *mut I) }); - 0 +pub fn into_display_backend_dmabuf>( + userdata: Option<&T>, +) -> DisplayBackend<'_> { + extern "C" fn create_fn>( + instance: *mut *mut c_void, + userdata: *const c_void, + _reserved: *const c_void, + ) -> i32 { + unsafe { + assert_ne!( + instance, + null_mut(), + "Pointer to location where to create instance cannot be null" + ); + let userdata_ref = (userdata as *const T).as_ref(); + *(instance as *mut *mut I) = Box::into_raw(Box::new(I::new(userdata_ref))); } + 0 + } - fn cast_instance<'a, I: DisplayBackendBasicFramebuffer>( - instance: *mut c_void, - ) -> &'a mut I { - assert_ne!(instance, null_mut()); - unsafe { &mut *(instance as *mut I) } - } + extern "C" fn destroy_fn(instance: *mut c_void) -> i32 { + drop(unsafe { Box::from_raw(instance as *mut I) }); + 0 + } - extern "C" fn configure_scanout_fn( - instance: *mut c_void, - scanout_id: u32, - display_width: u32, - display_height: u32, - width: u32, - height: u32, - format: u32, - ) -> i32 { - let Ok(format) = ResourceFormat::try_from(format) else { - error!("Unknown display format: {format}"); - return DisplayBackendError::InvalidParam as i32; - }; - - from_rust_result(cast_instance::(instance).configure_scanout( - scanout_id, - display_width, - display_height, - width, - height, - format, - )) - } + fn cast_instance<'a, I: DisplayBackendDmabuf>(instance: *mut c_void) -> &'a mut I { + assert_ne!(instance, null_mut()); + unsafe { &mut *(instance as *mut I) } + } - extern "C" fn disable_scanout_fb( - instance: *mut c_void, - scanout_id: u32, - ) -> i32 { - from_rust_result(cast_instance::(instance).disable_scanout(scanout_id)) - } + extern "C" fn configure_scanout_fn( + instance: *mut c_void, + scanout_id: u32, + display_width: u32, + display_height: u32, + width: u32, + height: u32, + format: u32, + ) -> i32 { + let Ok(format) = ResourceFormat::try_from(format) else { + error!("Unknown display format: {format}"); + return DisplayBackendError::InvalidParam as i32; + }; + + from_rust_result(cast_instance::(instance).configure_scanout( + scanout_id, + display_width, + display_height, + width, + height, + format, + )) + } - extern "C" fn alloc_frame( - instance: *mut c_void, - scanout_id: u32, - buffer: *mut *mut u8, - buffer_size: *mut usize, - ) -> i32 { - match cast_instance::(instance).alloc_frame(scanout_id) { - Ok((frame_id, allocated_buffer)) => { - unsafe { - *buffer_size = allocated_buffer.len(); - *buffer = allocated_buffer.as_mut_ptr(); - } - frame_id as i32 + extern "C" fn disable_scanout_dmabuf( + instance: *mut c_void, + scanout_id: u32, + ) -> i32 { + from_rust_result(cast_instance::(instance).disable_scanout(scanout_id)) + } + + extern "C" fn alloc_frame( + instance: *mut c_void, + scanout_id: u32, + buffer: *mut *mut u8, + buffer_size: *mut usize, + ) -> i32 { + match cast_instance::(instance).alloc_frame(scanout_id) { + Ok((frame_id, allocated_buffer)) => { + unsafe { + *buffer_size = allocated_buffer.len(); + *buffer = allocated_buffer.as_mut_ptr(); } - Err(e) => e as i32, + frame_id as i32 } + Err(e) => e as i32, } + } - extern "C" fn present_frame( - instance: *mut c_void, - scanout_id: u32, - frame_id: u32, - rect: *const Rect, - ) -> i32 { - // SAFETY: The pointer obtained from the bindings should be safe - let rect: Option<&Rect> = unsafe { ptr_to_option_ref(rect) }; - from_rust_result(cast_instance::(instance).present_frame(scanout_id, frame_id, rect)) + extern "C" fn present_frame( + instance: *mut c_void, + scanout_id: u32, + frame_id: u32, + rect: *const Rect, + ) -> i32 { + let rect: Option<&Rect> = unsafe { ptr_to_option_ref(rect) }; + from_rust_result(cast_instance::(instance).present_frame(scanout_id, frame_id, rect)) + } + + extern "C" fn import_dmabuf_fn( + instance: *mut c_void, + dmabuf_export: *const DmabufExport, + ) -> i32 { + let dmabuf_export = + unsafe { ptr_to_option_ref(dmabuf_export) }.expect("dmabuf_export must not be null"); + match cast_instance::(instance).import_dmabuf(dmabuf_export) { + Ok(dmabuf_id) => dmabuf_id as i32, + Err(e) => e as i32, } + } - DisplayBackend { - create_userdata: userdata.map_or(null(), |t| ptr::from_ref(t) as *const c_void), - create_userdata_lifetime: PhantomData, - features: DisplayFeatures::BASIC_FRAMEBUFFER.bits(), - create_fn: Some(create_fn::), - vtable: DisplayVtable { + extern "C" fn unref_dmabuf_fn( + instance: *mut c_void, + dmabuf_id: u32, + ) -> i32 { + from_rust_result(cast_instance::(instance).unref_dmabuf(dmabuf_id)) + } + + extern "C" fn configure_scanout_dmabuf_fn( + instance: *mut c_void, + scanout_id: u32, + display_width: u32, + display_height: u32, + dmabuf_id: u32, + src_rect: *const Rect, + ) -> i32 { + let src_rect: Option<&Rect> = unsafe { ptr_to_option_ref(src_rect) }; + from_rust_result(cast_instance::(instance).configure_scanout_dmabuf( + scanout_id, + display_width, + display_height, + dmabuf_id, + src_rect, + )) + } + + extern "C" fn present_dmabuf_fn( + instance: *mut c_void, + scanout_id: u32, + rect: *const Rect, + ) -> i32 { + let rect: Option<&Rect> = unsafe { ptr_to_option_ref(rect) }; + from_rust_result(cast_instance::(instance).present_dmabuf(scanout_id, rect)) + } + + DisplayBackend { + create_userdata: userdata.map_or(null(), |t| ptr::from_ref(t) as *const c_void), + create_userdata_lifetime: PhantomData, + features: (DisplayFeatures::DMABUF_CONSUMER | DisplayFeatures::BASIC_FRAMEBUFFER).bits(), + create_fn: Some(create_fn::), + vtable: DisplayVtable { + dmabuf: DisplayDmabufVtable { basic_framebuffer: DisplayBasicFramebufferVtable { destroy: Some(destroy_fn::), + disable_scanout: Some(disable_scanout_dmabuf::), configure_scanout: Some(configure_scanout_fn::), - present_frame: Some(present_frame::), alloc_frame: Some(alloc_frame::), - disable_scanout: Some(disable_scanout_fb::), + present_frame: Some(present_frame::), }, + import_dmabuf: Some(import_dmabuf_fn::), + unref_dmabuf: Some(unref_dmabuf_fn::), + configure_scanout_dmabuf: Some(configure_scanout_dmabuf_fn::), + present_dmabuf: Some(present_dmabuf_fn::), }, - } + }, } } diff --git a/src/libkrun/src/api_tests.rs b/src/libkrun/src/api_tests.rs new file mode 100644 index 000000000..e9e8d3728 --- /dev/null +++ b/src/libkrun/src/api_tests.rs @@ -0,0 +1,110 @@ +//! Unit tests for the public libkrun C API. +//! +//! Verifies some basic API behavior (ABI, error codes, etc.) without starting actual VMs. + +use crate::*; + +#[test] +#[cfg(feature = "gpu")] +fn test_display_backend_abi_compatibility() { + use krun_display::*; + + // This test verifies that old applications compiled against the basic_framebuffer-only + // version (V1 ABI) can still pass their smaller DisplayBackend struct to the new libkrun + // that supports dmabuf (V2 ABI). + + unsafe extern "C" fn dummy_disable_scanout(_instance: *mut c_void, _scanout_id: u32) -> i32 { + DisplayBackendError::InternalError as _ + } + + unsafe extern "C" fn dummy_configure_scanout( + _instance: *mut c_void, + _scanout_id: u32, + _display_width: u32, + _display_height: u32, + _width: u32, + _height: u32, + _format: u32, + ) -> i32 { + DisplayBackendError::InternalError as _ + } + + unsafe extern "C" fn dummy_alloc_frame( + _instance: *mut c_void, + _scanout_id: u32, + _buffer: *mut *mut u8, + _buffer_size: *mut usize, + ) -> i32 { + DisplayBackendError::InternalError as _ + } + + unsafe extern "C" fn dummy_present_frame( + _instance: *mut c_void, + _scanout_id: u32, + _frame_id: u32, + _rect: *const krun_display::header::krun_rect, + ) -> i32 { + DisplayBackendError::InternalError as _ + } + + unsafe { + let v1_size = size_of::(); + let v2_size = size_of::(); + + // Test 1: V1 ABI with basic_framebuffer only (smaller size) + // Create a properly structured V1 backend + let v1_backend = DisplayBackendV1 { + features: DisplayFeatures::BASIC_FRAMEBUFFER.bits(), + create_userdata: std::ptr::null(), + create: None, + vtable: DisplayVtableV1 { + basic_framebuffer: DisplayBasicFramebufferVtable { + destroy: None, + disable_scanout: Some(dummy_disable_scanout), + configure_scanout: Some(dummy_configure_scanout), + alloc_frame: Some(dummy_alloc_frame), + present_frame: Some(dummy_present_frame), + }, + }, + }; + + // Create a context for testing + krun_set_log_level(0); + let ctx: u32 = krun_create_ctx(); + assert!(ctx > 0, "Failed to create context"); + + // This should succeed with the V1 (old, smaller) size + let result = + krun_set_display_backend(ctx, &v1_backend as *const _ as *const c_void, v1_size); + + assert_eq!( + result, 0, + "V1 ABI test failed: krun_set_display_backend returned {result}", + ); + + // Test 2: V2 ABI with full DisplayBackend structure (current size) + let ctx2: u32 = krun_create_ctx(); + assert!(ctx2 > 0, "Failed to create second context"); + + let mut v2_backend: krun_display::header::krun_display_backend = std::mem::zeroed(); + v2_backend.features = DisplayFeatures::BASIC_FRAMEBUFFER.bits(); + v2_backend.create_userdata = std::ptr::null_mut(); + v2_backend.create = None; + + // Set the basic_framebuffer vtable + v2_backend.vtable.basic_framebuffer.destroy = None; + v2_backend.vtable.basic_framebuffer.disable_scanout = Some(dummy_disable_scanout); + v2_backend.vtable.basic_framebuffer.configure_scanout = Some(dummy_configure_scanout); + v2_backend.vtable.basic_framebuffer.alloc_frame = Some(dummy_alloc_frame); + v2_backend.vtable.basic_framebuffer.present_frame = Some(dummy_present_frame); + + // This should also succeed with the V2 (new, larger) size + let result = + krun_set_display_backend(ctx2, &v2_backend as *const _ as *const c_void, v2_size); + + assert_eq!( + result, 0, + "V2 ABI test failed: krun_set_display_backend returned {result}", + ); + } +} diff --git a/src/libkrun/src/lib.rs b/src/libkrun/src/lib.rs index 3a5c577a4..616d291c5 100644 --- a/src/libkrun/src/lib.rs +++ b/src/libkrun/src/lib.rs @@ -1382,14 +1382,34 @@ pub extern "C" fn krun_set_display_backend( vtable: *const c_void, vtable_size: usize, ) -> i32 { - if vtable_size < size_of::() { + use krun_display::DisplayBackendV1; + use std::cmp::min; + + // The absolute minimum size is V1 (basic_framebuffer only) + let min_size = size_of::(); + if vtable_size < min_size { + error!( + "Provided display backend size ({vtable_size}) is smaller than the minimum V1 ABI size ({min_size})" + ); return -libc::EINVAL; } - // SAFETY: We have checked the vtable size is fine, otherwise we have to trust the user. Just - // to be extra careful, this uses read_unaligned, but we could probably get away with ptr::read. - let display_backend: DisplayBackend = - unsafe { std::ptr::read_unaligned(vtable as *const DisplayBackend) }; + // SAFETY: We have verified minimum size above. + // Zero-initialize the backend, then copy only the actual size provided (clamped to our size). + // This handles both older (smaller) and newer (larger) ABI versions safely: + // - Older versions: copy their smaller size, rest stays zeroed + // - Newer versions: copy only what fits in our current DisplayBackend, ignore unknown fields + // The subsequent verify() call will check that required function pointers for the + // declared features are non-null, which also catches the cases where the vtable is too small. + let mut display_backend: DisplayBackend = unsafe { std::mem::zeroed() }; + let copy_size = min(vtable_size, size_of::()); + unsafe { + std::ptr::copy_nonoverlapping( + vtable as *const u8, + &mut display_backend as *mut DisplayBackend as *mut u8, + copy_size, + ); + } if !display_backend.verify() { return -libc::EINVAL; @@ -2601,3 +2621,6 @@ fn krun_start_enter_nitro(ctx_id: u32) -> i32 { } } } + +#[cfg(test)] +mod api_tests; From 38091494cc787d033930b4cc4b6d1fc48a1e23de Mon Sep 17 00:00:00 2001 From: Matej Hrica Date: Wed, 5 Nov 2025 14:23:41 +0100 Subject: [PATCH 5/6] gpu: Add support for dmabuf scanout Add support for a zero-copy dmabuf scanout of resources. If the dmabuf scanout is not supported we still fallback to the basic_framebuffer feature (CPU copy). Signed-off-by: Matej Hrica --- src/devices/src/virtio/gpu/virtio_gpu.rs | 242 +++++++++++++++++++---- 1 file changed, 207 insertions(+), 35 deletions(-) diff --git a/src/devices/src/virtio/gpu/virtio_gpu.rs b/src/devices/src/virtio/gpu/virtio_gpu.rs index c383b6ed7..fb0bfab13 100644 --- a/src/devices/src/virtio/gpu/virtio_gpu.rs +++ b/src/devices/src/virtio/gpu/virtio_gpu.rs @@ -15,24 +15,30 @@ use super::protocol::{ #[cfg(target_os = "macos")] use crossbeam_channel::{unbounded, Sender}; use krun_display::{ - DisplayBackend, DisplayBackendBasicFramebuffer, DisplayBackendInstance, Rect, ResourceFormat, + DisplayBackend, DisplayBackendBasicFramebuffer, DisplayBackendInstance, Rect, + ResourceFormat, }; +#[cfg(target_os = "linux")] +use krun_display::DmabufExport; use libc::c_void; #[cfg(target_os = "macos")] use rutabaga_gfx::RUTABAGA_MEM_HANDLE_TYPE_APPLE; +#[cfg(target_os = "linux")] +use rutabaga_gfx::RUTABAGA_MEM_HANDLE_TYPE_DMABUF; #[cfg(all(not(feature = "virgl_resource_map2"), target_os = "linux"))] use rutabaga_gfx::RUTABAGA_MEM_HANDLE_TYPE_OPAQUE_FD; #[cfg(all(feature = "virgl_resource_map2", target_os = "linux"))] use rutabaga_gfx::RUTABAGA_MEM_HANDLE_TYPE_SHM; use rutabaga_gfx::{ ResourceCreate3D, ResourceCreateBlob, Rutabaga, RutabagaBuilder, RutabagaChannel, - RutabagaFence, RutabagaFenceHandler, RutabagaIovec, Transfer3D, RUTABAGA_CHANNEL_TYPE_WAYLAND, - RUTABAGA_MAP_CACHE_MASK, + RutabagaFence, RutabagaFenceHandler, RutabagaIovec, Transfer3D, + RUTABAGA_CHANNEL_TYPE_WAYLAND, RUTABAGA_MAP_CACHE_MASK, }; #[cfg(target_os = "linux")] use rutabaga_gfx::{ - RUTABAGA_CHANNEL_TYPE_PW, RUTABAGA_CHANNEL_TYPE_X11, RUTABAGA_MAP_ACCESS_MASK, - RUTABAGA_MAP_ACCESS_READ, RUTABAGA_MAP_ACCESS_RW, RUTABAGA_MAP_ACCESS_WRITE, + RutabagaIntoRawDescriptor, RUTABAGA_CHANNEL_TYPE_PW, RUTABAGA_CHANNEL_TYPE_X11, + RUTABAGA_MAP_ACCESS_MASK, RUTABAGA_MAP_ACCESS_READ, RUTABAGA_MAP_ACCESS_RW, + RUTABAGA_MAP_ACCESS_WRITE, }; #[cfg(target_os = "macos")] use utils::worker_message::WorkerMessage; @@ -116,6 +122,9 @@ struct VirtioGpuResource { size: u64, // only for blob resources shmem_offset: Option, rutabaga_external_mapping: bool, + #[cfg(target_os = "linux")] + // The id under which this resource is imported to the display + display_dmabuf_id: Option, } impl VirtioGpuResource { @@ -137,12 +146,15 @@ impl VirtioGpuResource { format, shmem_offset: None, rutabaga_external_mapping: false, + #[cfg(target_os = "linux")] + display_dmabuf_id: None, } } } pub struct VirtioGpuScanout { resource_id: u32, + uses_dmabuf: bool, } pub struct VirtioGpu { @@ -367,6 +379,15 @@ impl VirtioGpu { return Err(ErrUnspec); } + #[cfg(target_os = "linux")] + if self.display_backend.supports_dmabuf() { + if let Some(display_dmabuf_id) = resource.display_dmabuf_id { + if let Err(e) = self.display_backend.unref_dmabuf(display_dmabuf_id) { + warn!("Failed to unref display DMABUF resource, resource_id: {resource_id}:, display_dmabuf_id: {display_dmabuf_id}, error: {e:?}"); + } + } + } + if resource.rutabaga_external_mapping { self.rutabaga.unmap(resource_id)?; } @@ -382,26 +403,21 @@ impl VirtioGpu { width: u32, height: u32, ) -> VirtioGpuResult { - let scanout = self - .scanouts - .get_mut(scanout_id as usize) - .ok_or(ErrInvalidScanoutId)?; - - // If a resource is already associated with this scanout, make sure to disable - // this scanout for that resource - if let Some(resource_id) = scanout.as_ref().map(|scanout| scanout.resource_id) { - let resource = self - .resources - .get_mut(&resource_id) - .ok_or(ErrInvalidResourceId)?; + if scanout_id as usize >= self.scanouts.len() { + return Err(ErrInvalidScanoutId); + } - resource.scanouts.disable(scanout_id); + // If a resource is already associated with this scanout, disable it for that resource + if let Some(old_scanout) = &self.scanouts[scanout_id as usize] { + if let Some(resource) = self.resources.get_mut(&old_scanout.resource_id) { + resource.scanouts.disable(scanout_id); + } } // Virtio spec: "The driver can use resource_id = 0 to disable a scanout." if resource_id == 0 { debug!("Disabling scanout {scanout_id:?}"); - *scanout = None; + self.scanouts[scanout_id as usize] = None; self.display_backend.disable_scanout(scanout_id)?; return Ok(OkNoData); } @@ -412,28 +428,163 @@ impl VirtioGpu { .get_mut(&resource_id) .ok_or(ErrInvalidResourceId)?; resource.scanouts.enable(scanout_id); + let resource_format = resource.format; + + let (display_width, display_height) = { + let display_info = self + .displays + .get(scanout_id as usize) + .ok_or(ErrInvalidScanoutId)?; + (display_info.width, display_info.height) + }; - let Some(format) = resource.format else { - warn!("Cannot use resource {resource_id} with unknown format for scanout"); + // Try dmabuf path first if supported + #[cfg(target_os = "linux")] + if self.display_backend.supports_dmabuf() { + if let Ok(dmabuf_scanout) = self.try_configure_dmabuf_scanout( + scanout_id, + resource_id, + display_width, + display_height, + width, + height, + ) { + self.scanouts[scanout_id as usize] = Some(dmabuf_scanout); + return Ok(OkNoData); + } + } + + let Some(format) = resource_format else { + warn!("Cannot use resource {resource_id} with unknown format for basic framebuffer scanout"); return Err(ErrUnspec); }; - let display_info = self - .displays - .get(scanout_id as usize) - .ok_or(ErrInvalidScanoutId)?; + // Fallback to basic framebuffer + let basic_scanout = self.configure_basic_framebuffer_scanout( + scanout_id, + resource_id, + display_width, + display_height, + width, + height, + format, + )?; + self.scanouts[scanout_id as usize] = Some(basic_scanout); + Ok(OkNoData) + } + #[cfg(target_os = "linux")] + fn try_configure_dmabuf_scanout( + &mut self, + scanout_id: u32, + resource_id: u32, + display_width: u32, + display_height: u32, + _width: u32, + _height: u32, + ) -> std::result::Result { + // Check if this resource has already been exported to the display + let resource = self.resources.get_mut(&resource_id).ok_or(())?; + let dmabuf_id = match resource.display_dmabuf_id { + Some(old_dmabuf_id) => { + trace!("Resource {resource_id} already imported as dmabuf with ID {old_dmabuf_id}"); + old_dmabuf_id + } + None => { + let export = self.rutabaga.export_blob(resource_id).map_err(|e| { + debug!("Failed to export resource {resource_id} as dmabuf: {e}"); + })?; + + // Verify that the exported handle is actually a dmabuf + if export.handle_type != RUTABAGA_MEM_HANDLE_TYPE_DMABUF { + debug!( + "Scanout resource {resource_id} was exported with handle type 0x{:x}, not DMABUF (0x{:x})", + export.handle_type, RUTABAGA_MEM_HANDLE_TYPE_DMABUF + ); + return Err(()); + } + + let info_3d = self.rutabaga.query(resource_id).map_err(|e| { + debug!("Failed to query resource {resource_id} for dmabuf info: {e}"); + })?; + + debug!("Resource {resource_id} dmabuf info: fourcc=0x{:08x}, modifier=0x{:016x}, strides={:?}, offsets={:?}", + info_3d.drm_fourcc, info_3d.modifier, info_3d.strides, info_3d.offsets + ); + + // Currently we only ever have 1 plane, but the display frontend API is generic and + // supports multiple + const NUM_PLANES: u32 = 1; + let dmabuf_fd = export.os_handle.into_raw_descriptor(); + let dmabuf_fds = [dmabuf_fd, -1, -1, -1]; + + let dmabuf_export = DmabufExport { + dmabuf_fds, + n_planes: NUM_PLANES, + width: info_3d.width, + height: info_3d.height, + fourcc: info_3d.drm_fourcc, + strides: info_3d.strides, + offsets: info_3d.offsets, + modifier: info_3d.modifier, + }; + + // Import the dmabuf into the display backend + let dmabuf_id = + self.display_backend + .import_dmabuf(&dmabuf_export) + .map_err(|e| { + warn!("Failed to import dmabuf for resource {resource_id}: {e}"); + })?; + + debug!("Imported resource {resource_id} as dmabuf with ID {dmabuf_id}"); + + // Store the dmabuf ID in the resource + resource.display_dmabuf_id = Some(dmabuf_id); + dmabuf_id + } + }; + + // Configure scanout to use the imported dmabuf (no src_rect for now, use entire dmabuf) + self.display_backend + .configure_scanout_dmabuf(scanout_id, display_width, display_height, dmabuf_id, None) + .map_err(|e| { + debug!("Failed to configure dmabuf scanout for resource {resource_id}: {e}"); + })?; + + debug!( + "Successfully configured scanout {scanout_id} with dmabuf for resource {resource_id}" + ); + Ok(VirtioGpuScanout { + resource_id, + uses_dmabuf: true, + }) + } + + #[allow(clippy::too_many_arguments)] + fn configure_basic_framebuffer_scanout( + &mut self, + scanout_id: u32, + resource_id: u32, + display_width: u32, + display_height: u32, + width: u32, + height: u32, + format: ResourceFormat, + ) -> std::result::Result { self.display_backend.configure_scanout( scanout_id, - display_info.width, - display_info.height, + display_width, + display_height, width, height, format, )?; - *scanout = Some(VirtioGpuScanout { resource_id }); - Ok(OkNoData) + Ok(VirtioGpuScanout { + resource_id, + uses_dmabuf: false, + }) } fn read_2d_resource( @@ -473,14 +624,35 @@ impl VirtioGpu { .get(&resource_id) .ok_or(ErrInvalidResourceId)?; + #[cfg(target_os = "linux")] + unsafe { + #[link(name = "GL")] + extern "C" { + fn glFlush(); + } + + glFlush() + }; + for scanout_id in resource.scanouts.iter_enabled() { - let (frame_id, buffer) = self.display_backend.alloc_frame(scanout_id)?; - if let Err(e) = Self::read_2d_resource(&mut self.rutabaga, resource, buffer) { - log::error!("Failed to read resource {resource_id} for scanout {scanout_id}: {e}"); - return Err(ErrUnspec); + if self.scanouts[scanout_id as usize] + .as_ref() + .unwrap() + .uses_dmabuf + { + self.display_backend + .present_dmabuf(scanout_id, Some(&rect))?; + } else { + let (frame_id, buffer) = self.display_backend.alloc_frame(scanout_id)?; + if let Err(e) = Self::read_2d_resource(&mut self.rutabaga, resource, buffer) { + log::error!( + "Failed to read resource {resource_id} for scanout {scanout_id}: {e}" + ); + return Err(ErrUnspec); + } + self.display_backend + .present_frame(scanout_id, frame_id, Some(&rect))? } - self.display_backend - .present_frame(scanout_id, frame_id, Some(&rect))? } #[cfg(windows)] From 864c34e8285666895bdf9702bb01342e9d8404a5 Mon Sep 17 00:00:00 2001 From: Matej Hrica Date: Wed, 5 Nov 2025 14:43:57 +0100 Subject: [PATCH 6/6] krun_gtk_display: Support importing and rendering dmabufs Signed-off-by: Matej Hrica --- .../krun_gtk_display/src/display_backend.rs | 111 ++++++++++- .../krun_gtk_display/src/display_worker.rs | 188 +++++++++++++++++- examples/krun_gtk_display/src/lib.rs | 15 +- .../src/scanout_paintable/imp.rs | 151 +++++++++++--- .../src/scanout_paintable/mod.rs | 51 ++++- src/devices/src/virtio/gpu/virtio_gpu.rs | 15 +- 6 files changed, 476 insertions(+), 55 deletions(-) diff --git a/examples/krun_gtk_display/src/display_backend.rs b/examples/krun_gtk_display/src/display_backend.rs index d30ffed33..59a450b58 100644 --- a/examples/krun_gtk_display/src/display_backend.rs +++ b/examples/krun_gtk_display/src/display_backend.rs @@ -4,6 +4,8 @@ use krun_display::{ DisplayBackendBasicFramebuffer, DisplayBackendError, DisplayBackendNew, MAX_DISPLAYS, Rect, ResourceFormat, }; +#[cfg(target_os = "linux")] +use krun_display::{DisplayBackendDmabuf, DmabufExport}; use log::error; use std::mem; use utils::pollable_channel::PollableChannelSender; @@ -21,6 +23,15 @@ const _: () = { #[derive(Debug, Clone)] pub enum DisplayEvent { + #[cfg(target_os = "linux")] + ImportDmabuf { + dmabuf_id: u32, + dmabuf_export: DmabufExport, + }, + #[cfg(target_os = "linux")] + UnrefDmabuf { + dmabuf_id: u32, + }, ConfigureScanout { scanout_id: u32, display_width: u32, @@ -29,6 +40,14 @@ pub enum DisplayEvent { height: u32, format: MemoryFormat, }, + #[cfg(target_os = "linux")] + ConfigureScanoutDmabuf { + scanout_id: u32, + display_width: u32, + display_height: u32, + dmabuf_id: u32, + src_rect: Option, + }, DisableScanout { scanout_id: u32, }, @@ -37,6 +56,11 @@ pub enum DisplayEvent { buffer: Bytes, rect: Option, }, + #[cfg(target_os = "linux")] + UpdateScanoutDmabuf { + scanout_id: u32, + rect: Option, + }, } // Implements libkrun traits (callbacks) to provide a display implementation, by forwarding the @@ -44,17 +68,20 @@ pub enum DisplayEvent { pub struct GtkDisplayBackend { channel: PollableChannelSender, scanouts: [Option; MAX_DISPLAYS], + #[cfg(target_os = "linux")] + next_dmabuf_id: u32, } impl DisplayBackendNew> for GtkDisplayBackend { - fn new(channel: Option<&PollableChannelSender>) -> Self { - let channel = channel - .expect("The channel should have been set by GtkDisplayBackend::into_display_backend") - .clone(); + fn new(userdata: Option<&PollableChannelSender>) -> Self { + let channel = userdata + .expect("The userdata should have been set by GtkDisplayBackend::into_display_backend"); Self { - channel, + channel: channel.clone(), scanouts: Default::default(), + #[cfg(target_os = "linux")] + next_dmabuf_id: 1, } } } @@ -89,6 +116,8 @@ impl DisplayBackendBasicFramebuffer for GtkDisplayBackend { buffer_rx, buffer_tx, current_buffer: Vec::new(), + #[cfg(target_os = "linux")] + has_dmabuf: false, }); } @@ -157,6 +186,76 @@ impl DisplayBackendBasicFramebuffer for GtkDisplayBackend { } } +#[cfg(target_os = "linux")] +impl DisplayBackendDmabuf for GtkDisplayBackend { + fn import_dmabuf(&mut self, dmabuf_export: &DmabufExport) -> Result { + let dmabuf_id = self.next_dmabuf_id; + self.next_dmabuf_id += 1; + + self.channel + .send(DisplayEvent::ImportDmabuf { + dmabuf_id, + dmabuf_export: *dmabuf_export, + }) + .unwrap(); + + Ok(dmabuf_id) + } + + fn unref_dmabuf(&mut self, dmabuf_id: u32) -> Result<(), DisplayBackendError> { + self.channel + .send(DisplayEvent::UnrefDmabuf { dmabuf_id }) + .unwrap(); + + Ok(()) + } + + fn configure_scanout_dmabuf( + &mut self, + scanout_id: u32, + display_width: u32, + display_height: u32, + dmabuf_id: u32, + src_rect: Option<&Rect>, + ) -> Result<(), DisplayBackendError> { + let Some(scanout) = &mut self.scanouts[scanout_id as usize] else { + return Err(DisplayBackendError::InvalidScanoutId); + }; + + scanout.has_dmabuf = true; + + self.channel + .send(DisplayEvent::ConfigureScanoutDmabuf { + scanout_id, + display_width, + display_height, + dmabuf_id, + src_rect: src_rect.copied(), + }) + .unwrap(); + Ok(()) + } + + fn present_dmabuf( + &mut self, + scanout_id: u32, + rect: Option<&Rect>, + ) -> Result<(), DisplayBackendError> { + if self.scanouts[scanout_id as usize] + .as_ref() + .is_none_or(|scanout| !scanout.has_dmabuf) + { + return Err(DisplayBackendError::InvalidScanoutId); + }; + + let rect = rect.copied(); + self.channel + .send(DisplayEvent::UpdateScanoutDmabuf { scanout_id, rect }) + .unwrap(); + Ok(()) + } +} + fn resource_format_into_gdk(format: ResourceFormat) -> MemoryFormat { match format { ResourceFormat::BGRA => MemoryFormat::B8g8r8a8, @@ -175,6 +274,8 @@ struct Scanout { buffer_rx: Receiver>, required_buffer_size: usize, current_buffer: Vec, + #[cfg(target_os = "linux")] + has_dmabuf: bool, } impl Scanout { diff --git a/examples/krun_gtk_display/src/display_worker.rs b/examples/krun_gtk_display/src/display_worker.rs index 093729033..a7ac2bcec 100644 --- a/examples/krun_gtk_display/src/display_worker.rs +++ b/examples/krun_gtk_display/src/display_worker.rs @@ -1,13 +1,21 @@ use super::scanout_paintable::ScanoutPaintable; use crate::{Axis, DisplayEvent, DisplayInputOptions, TouchArea, TouchScreenOptions}; +#[cfg(target_os = "linux")] +use krun_display::DmabufExport; use krun_display::Rect; use krun_input::{InputEvent, InputEventType}; use log::{debug, trace, warn}; use std::cell::RefCell; +#[cfg(target_os = "linux")] +use std::collections::HashMap; use std::collections::HashSet; use std::iter; +#[cfg(target_os = "linux")] +use std::ops::Deref; use std::os::fd::AsRawFd; use std::rc::Rc; +#[cfg(target_os = "linux")] +use std::sync::Arc; use std::time::Duration; use utils::pollable_channel::{PollableChannelReciever, PollableChannelSender}; @@ -32,6 +40,7 @@ use gtk::{ prelude::*, }; use krun_display::MAX_DISPLAYS; +use libc::close; type EventSender = PollableChannelSender; @@ -311,8 +320,10 @@ struct ScanoutWindow { window: ApplicationWindow, width: i32, height: i32, - format: MemoryFormat, + format: Option, scanout_paintable: ScanoutPaintable, + #[cfg(target_os = "linux")] + current_dmabuf_id: Option, } impl ScanoutWindow { @@ -324,7 +335,7 @@ impl ScanoutWindow { display_height: i32, width: i32, height: i32, - format: MemoryFormat, + format: Option, keyboard_event_tx: Option, per_display_inputs: Vec<(EventSender, DisplayInputOptions)>, ) -> Self { @@ -387,6 +398,10 @@ impl ScanoutWindow { header_bar.pack_end(&fullscreen_btn); let overlay = build_overlay(window.as_ref()); + /*let offload = GraphicsOffload::builder() + .child(&picture) + .black_background(true) + .build();*/ overlay.set_child(Some(&picture)); window.set_child(Some(&overlay)); window.set_visible(true); @@ -404,18 +419,36 @@ impl ScanoutWindow { height, format, scanout_paintable, + #[cfg(target_os = "linux")] + current_dmabuf_id: None, } } pub fn reconfigure(&mut self, width: i32, height: i32, format: gdk::MemoryFormat) { self.width = width; self.height = height; - self.format = format; + self.format = Some(format); } pub fn update(&self, buffer: Bytes, rect: Option) { self.scanout_paintable - .update(buffer, self.width, self.height, self.format, rect); + .update(buffer, self.width, self.height, self.format.unwrap(), rect); + } + + #[cfg(target_os = "linux")] + pub fn set_dmabuf(&mut self, dmabuf: SharedDmabuf, damage_area: Option) { + self.scanout_paintable + .configure_dmabuf(dmabuf, None, damage_area); + } + + #[cfg(target_os = "linux")] + pub fn set_current_dmabuf_id(&mut self, dmabuf_id: u32) { + self.current_dmabuf_id = Some(dmabuf_id); + } + + #[cfg(target_os = "linux")] + pub fn get_current_dmabuf_id(&self) -> Option { + self.current_dmabuf_id } } @@ -466,7 +499,7 @@ fn attach_keyboard(keyboard_tx: EventSender, widget: &impl IsA) { let input_event = InputEvent { type_: InputEventType::Key as u16, code: linux_keycode, - value: 0, // Key release + value: 0, }; debug!( "Forwarding key release: GTK key={}, code={}, Linux code={}", @@ -654,6 +687,53 @@ fn build_overlay(window: &Window) -> Overlay { overlay } +#[cfg(target_os = "linux")] +struct DmabufInner(DmabufExport); + +#[cfg(target_os = "linux")] +impl Drop for DmabufInner { + fn drop(&mut self) { + for &fd in self.0.dmabuf_fds.iter().take(self.0.n_planes as usize) { + log::debug!( + "Closing dmabuf fd {} (fourcc=0x{:08x}, modifier=0x{:016x})", + fd, + self.0.fourcc, + self.0.modifier + ); + unsafe { + close(fd); + } + } + } +} + +#[cfg(target_os = "linux")] +#[derive(Clone)] +pub struct SharedDmabuf(Arc); + +#[cfg(target_os = "linux")] +impl SharedDmabuf { + fn new(dmabuf_export: DmabufExport) -> Self { + // Assert that all plane fds are valid + for &fd in dmabuf_export + .dmabuf_fds + .iter() + .take(dmabuf_export.n_planes as usize) + { + assert!(fd >= 0, "Invalid dmabuf fd {}", fd); + } + Self(Arc::new(DmabufInner(dmabuf_export))) + } +} + +#[cfg(target_os = "linux")] +impl Deref for SharedDmabuf { + type Target = DmabufExport; + fn deref(&self) -> &Self::Target { + &self.0.0 + } +} + pub struct DisplayWorker { app: Application, app_name: String, @@ -661,6 +741,8 @@ pub struct DisplayWorker { keyboard_event_tx: Option, per_display_inputs: Vec, DisplayInputOptions)>>, scanouts: RefCell<[Option; MAX_DISPLAYS]>, + #[cfg(target_os = "linux")] + imported_dmabufs: RefCell>, } impl DisplayWorker { @@ -678,6 +760,8 @@ impl DisplayWorker { keyboard_event_tx, per_display_inputs, scanouts: Default::default(), + #[cfg(target_os = "linux")] + imported_dmabufs: RefCell::new(HashMap::new()), } } @@ -685,6 +769,21 @@ impl DisplayWorker { let mut scanouts = self.scanouts.borrow_mut(); while let Some(msg) = self.rx.try_recv().unwrap() { match msg { + #[cfg(target_os = "linux")] + DisplayEvent::ImportDmabuf { + dmabuf_id, + dmabuf_export, + } => { + debug!("Importing dmabuf ID {dmabuf_id}"); + self.imported_dmabufs + .borrow_mut() + .insert(dmabuf_id, SharedDmabuf::new(dmabuf_export)); + } + #[cfg(target_os = "linux")] + DisplayEvent::UnrefDmabuf { dmabuf_id } => { + debug!("Unreferencing dmabuf ID {dmabuf_id}"); + self.imported_dmabufs.borrow_mut().remove(&dmabuf_id); + } DisplayEvent::ConfigureScanout { scanout_id, display_width, @@ -712,7 +811,7 @@ impl DisplayWorker { display_height as i32, width as i32, height as i32, - format, + Some(format), self.keyboard_event_tx.clone(), self.per_display_inputs .get(scanout_id as usize) @@ -721,6 +820,57 @@ impl DisplayWorker { )); } } + #[cfg(target_os = "linux")] + DisplayEvent::ConfigureScanoutDmabuf { + scanout_id, + display_width, //FIXME! we need to create a scanout window! + display_height, + dmabuf_id, + src_rect, + } => { + // Get dmabuf from storage + let dmabuf_export = self + .imported_dmabufs + .borrow() + .get(&dmabuf_id) + .cloned() + .expect("Dmabuf ID should be valid"); + + if let Some(ref mut scanout) = scanouts[scanout_id as usize] { + debug!( + "Configure scanout {scanout_id} with dmabuf ID {dmabuf_id}: width={} height={}, n_planes={}, fds={:?}, src_rect={:?}", + dmabuf_export.width, + dmabuf_export.height, + dmabuf_export.n_planes, + &dmabuf_export.dmabuf_fds[..dmabuf_export.n_planes as usize], + src_rect + ); + scanout.set_current_dmabuf_id(dmabuf_id); + } else { + let mut scanout = ScanoutWindow::new( + &self.app, + &format!( + "{name} - display {scanout_id} ({width}x{height})", + name = self.app_name, + width = dmabuf_export.width, + height = dmabuf_export.height + ), + display_width as i32, + display_height as i32, + dmabuf_export.width as i32, + dmabuf_export.height as i32, + None, + self.keyboard_event_tx.clone(), + self.per_display_inputs + .get(scanout_id as usize) + .cloned() + .unwrap_or_default(), + ); + + scanout.set_current_dmabuf_id(dmabuf_id); + scanouts[scanout_id as usize] = Some(scanout); + } + } DisplayEvent::DisableScanout { scanout_id } => { debug!("Disable scanout {scanout_id}"); scanouts[scanout_id as usize] = None; @@ -737,6 +887,32 @@ impl DisplayWorker { warn!("Attempted to update non-existent scanout: {scanout_id}"); } } + #[cfg(target_os = "linux")] + DisplayEvent::UpdateScanoutDmabuf { scanout_id, rect } => { + if let Some(scanout) = &mut scanouts[scanout_id as usize] { + trace!("Update scanout {scanout_id} dmabuf"); + + if let Some(dmabuf_id) = scanout.get_current_dmabuf_id() { + if let Some(dmabuf_export) = + self.imported_dmabufs.borrow().get(&dmabuf_id).cloned() + { + log::trace!( + "Updating dmabuf scanout for dmabuf_id: {}, damage: {:?}", + dmabuf_id, + rect + ); + + scanout.set_dmabuf(dmabuf_export, rect); + } else { + warn!("No dmabuf export found for ID {}", dmabuf_id); + } + } else { + warn!("No current dmabuf_id for scanout {}", scanout_id); + } + } else { + warn!("Attempted to update dmabuf for non-existent scanout: {scanout_id}"); + } + } } } } diff --git a/examples/krun_gtk_display/src/lib.rs b/examples/krun_gtk_display/src/lib.rs index d8c79b214..c18155816 100644 --- a/examples/krun_gtk_display/src/lib.rs +++ b/examples/krun_gtk_display/src/lib.rs @@ -9,7 +9,10 @@ use crate::input_backend::{GtkInputEventProvider, GtkKeyboardConfig, GtkTouchscr use anyhow::Context; pub use display_backend::DisplayEvent; pub use display_backend::GtkDisplayBackend; -use krun_display::{DisplayBackend, IntoDisplayBackend}; +#[cfg(not(target_os = "linux"))] +use krun_display::{DisplayBackend, into_display_backend_basic_framebuffer}; +#[cfg(target_os = "linux")] +use krun_display::{DisplayBackend, into_display_backend_dmabuf}; use krun_input::{InputAbsInfo, InputConfigBackend, InputEventProviderBackend}; use krun_input::{InputEvent, IntoInputConfig, IntoInputEvents}; use utils::pollable_channel::{PollableChannelReciever, PollableChannelSender, pollable_channel}; @@ -20,7 +23,14 @@ pub struct DisplayBackendHandle { impl DisplayBackendHandle { pub fn get(&self) -> DisplayBackend<'_> { - GtkDisplayBackend::into_display_backend(Some(&self.tx)) + #[cfg(target_os = "linux")] + { + into_display_backend_dmabuf::<_, GtkDisplayBackend>(Some(&self.tx)) + } + #[cfg(not(target_os = "linux"))] + { + into_display_backend_basic_framebuffer::<_, GtkDisplayBackend>(Some(&self.tx)) + } } } @@ -167,6 +177,7 @@ pub fn init( let (display_tx, display_rx) = pollable_channel().context("Failed to create display events channel")?; + let display_backend = DisplayBackendHandle { tx: display_tx }; let worker = DisplayBackendWorker { diff --git a/examples/krun_gtk_display/src/scanout_paintable/imp.rs b/examples/krun_gtk_display/src/scanout_paintable/imp.rs index a26d0cb32..34f647209 100644 --- a/examples/krun_gtk_display/src/scanout_paintable/imp.rs +++ b/examples/krun_gtk_display/src/scanout_paintable/imp.rs @@ -1,24 +1,39 @@ +#[cfg(target_os = "linux")] +use crate::display_worker::SharedDmabuf; +use gtk::cairo::{RectangleInt as CairoRect, Region}; +#[cfg(target_os = "linux")] +use gtk::gdk::DmabufTextureBuilder; use gtk::{ + gdk, gdk::{Paintable, PaintableFlags, RGBA, Snapshot, Texture}, glib, - graphene::Rect, + graphene::Rect as GrapheneRect, prelude::*, subclass::prelude::*, }; - +use krun_display::{Rect as KrunRect, Rect}; use log::debug; use std::cell::{Cell, RefCell}; -#[derive(Default, glib::Properties)] +#[cfg(target_os = "linux")] +pub struct DmabufUpdate { + pub dmabuf: SharedDmabuf, + pub damage_area: Option, +} + +#[derive(glib::Properties)] #[properties(wrapper_type = super::ScanoutPaintable)] +#[derive(Default)] pub struct ScanoutPaintable { - // Store the texture that this paintable will draw. - pub texture: RefCell>, #[property(get, set)] - pub default_width: Cell, + pub width: Cell, #[property(get, set)] - pub default_height: Cell, - pub rect: Rect, + pub height: Cell, + // Store the texture that this paintable will draw. + pub texture: RefCell>, + pub src_rect: RefCell>, + #[cfg(target_os = "linux")] + pub dmabuf_update: RefCell>, } #[glib::object_subclass] @@ -35,14 +50,106 @@ impl ObjectImpl for ScanoutPaintable { } } +#[cfg(target_os = "linux")] +fn build_dmabuf_texture(update: DmabufUpdate, old_texture: Option<&Texture>) -> Option { + let dmabuf = update.dmabuf; + let damage_area = update.damage_area; + + let n_planes = dmabuf.n_planes; + assert_eq!(n_planes, 1); + let mut builder = DmabufTextureBuilder::new() + .set_display(gdk::Display::default().as_ref().unwrap()) + .set_width(dmabuf.width) + .set_height(dmabuf.height) + .set_fourcc(dmabuf.fourcc) + .set_modifier(dmabuf.modifier) + .set_n_planes(n_planes); + + // Configure damage area on the texture if provided + if let Some(damage) = damage_area { + log::trace!( + "Building texture with damage area: x={}, y={}, width={}, height={}", + damage.x, + damage.y, + damage.width, + damage.height + ); + + // Configure damage area on the texture builder + if let Some(old_tex) = old_texture { + // Create a cairo region from the damage rectangle + let rect = CairoRect::new( + damage.x as i32, + damage.y as i32, + damage.width as i32, + damage.height as i32, + ); + let region = Region::create_rectangle(&rect); + + builder = builder + .set_update_texture(Some(old_tex)) + .set_update_region(Some(®ion)); + } + } + + for i in 0..n_planes as usize { + let stride = dmabuf.strides[i]; + let offset = dmabuf.offsets[i]; + let fd = dmabuf.dmabuf_fds[i]; + + builder = builder + .set_stride(i as u32, stride) + .set_offset(i as u32, offset); + // SAFETY: Safe, the lifetime of the fd (on the display side) is managed by reference + // counting. + unsafe { + builder = builder.set_fd(i as u32, fd); + } + } + + match unsafe { + let dmabuf = dmabuf.clone(); + builder.build_with_release_func(move || { + // This only decrements ref-count, the dmabuf may be shared + drop(dmabuf); + }) + } { + Ok(texture) => { + let texture_upcast: Texture = texture.upcast(); + Some(texture_upcast) + } + Err(e) => { + log::error!( + "Failed to build dmabuf texture: {e} (n_planes={}, fds={:?}, fourcc=0x{:08x}, modifier=0x{:016x})", + dmabuf.n_planes, + &dmabuf.dmabuf_fds[..dmabuf.n_planes as usize], + dmabuf.fourcc, + dmabuf.modifier + ); + None + } + } +} + impl PaintableImpl for ScanoutPaintable { fn snapshot(&self, snapshot: &Snapshot, width: f64, height: f64) { + snapshot.append_color( + &RGBA::BLACK, + &GrapheneRect::new(0.0, 0.0, width as f32, height as f32), + ); + + #[cfg(target_os = "linux")] + if let Some(update) = self.dmabuf_update.borrow_mut().take() { + let new_texture = build_dmabuf_texture(update, self.texture.borrow().as_ref()); + if let Some(new_texture) = new_texture { + self.texture.replace(Some(new_texture)); + } + } + if let Some(texture) = self.texture.borrow().as_ref() { - snapshot.append_texture(texture, &Rect::new(0.0, 0.0, width as f32, height as f32)); - } else { - snapshot.append_color( - &RGBA::new(0.0, 0.0, 0.0, 1.0), - &Rect::new(0.0, 0.0, width as f32, height as f32), + snapshot.append_texture( + texture, + &GrapheneRect::new(0.0, 0.0, width as f32, height as f32), ); } } @@ -52,26 +159,14 @@ impl PaintableImpl for ScanoutPaintable { } fn intrinsic_aspect_ratio(&self) -> f64 { - if let Some(texture) = self.texture.borrow().as_ref() { - texture.width() as f64 / texture.height() as f64 - } else { - self.default_width.get() as f64 / self.default_height.get() as f64 - } + self.width.get() as f64 / self.height.get() as f64 } fn intrinsic_width(&self) -> i32 { - self.texture - .borrow() - .as_ref() - .map(|t| t.width()) - .unwrap_or(self.default_width.get()) + self.width.get() } fn intrinsic_height(&self) -> i32 { - self.texture - .borrow() - .as_ref() - .map(|t| t.height()) - .unwrap_or(self.default_height.get()) + self.height.get() } } diff --git a/examples/krun_gtk_display/src/scanout_paintable/mod.rs b/examples/krun_gtk_display/src/scanout_paintable/mod.rs index d2aab553f..81eb5ef95 100644 --- a/examples/krun_gtk_display/src/scanout_paintable/mod.rs +++ b/examples/krun_gtk_display/src/scanout_paintable/mod.rs @@ -1,5 +1,9 @@ mod imp; +#[cfg(target_os = "linux")] +use crate::display_worker::SharedDmabuf; +#[cfg(target_os = "linux")] +use crate::scanout_paintable::imp::DmabufUpdate; use gtk::{ cairo::{RectangleInt, Region}, gdk::{self, MemoryFormat, MemoryTextureBuilder}, @@ -17,8 +21,8 @@ glib::wrapper! { impl ScanoutPaintable { pub fn new(default_width: i32, default_height: i32) -> Self { glib::Object::builder() - .property("default-width", default_width) - .property("default-height", default_height) + .property("width", default_width) + .property("height", default_height) .build() } @@ -52,13 +56,46 @@ impl ScanoutPaintable { builder }; - let old_texture = imp.texture.replace(Some(builder.build())); + imp.texture.replace(Some(builder.build())); self.invalidate_contents(); - if let Some(old_texture) = old_texture - && old_texture.width() != width - && old_texture.height() != height - { + if self.height() != height || self.width() != width { + self.set_width(width); + self.set_height(height); + self.invalidate_size(); + } + } + + #[cfg(target_os = "linux")] + pub fn configure_dmabuf( + &self, + dmabuf: SharedDmabuf, + src_rect: Option, + damage_rect: Option, + ) { + let imp = self.imp(); + + let damage_area = if imp.dmabuf_update.borrow().is_some() { + // We don't currently handle multiple damage area changes + None + } else { + damage_rect + }; + + imp.dmabuf_update.replace(Some(DmabufUpdate { + dmabuf: dmabuf.clone(), + damage_area, + })); + + self.invalidate_contents(); + let (width, height) = if let Some(src_rect) = src_rect { + (src_rect.width, src_rect.height) + } else { + (dmabuf.width, dmabuf.height) + }; + if self.width() != width as i32 || self.height() != height as i32 { + self.set_width(width as i32); + self.set_height(height as i32); self.invalidate_size(); } } diff --git a/src/devices/src/virtio/gpu/virtio_gpu.rs b/src/devices/src/virtio/gpu/virtio_gpu.rs index fb0bfab13..4522d3b10 100644 --- a/src/devices/src/virtio/gpu/virtio_gpu.rs +++ b/src/devices/src/virtio/gpu/virtio_gpu.rs @@ -14,12 +14,11 @@ use super::protocol::{ }; #[cfg(target_os = "macos")] use crossbeam_channel::{unbounded, Sender}; -use krun_display::{ - DisplayBackend, DisplayBackendBasicFramebuffer, DisplayBackendInstance, Rect, - ResourceFormat, -}; #[cfg(target_os = "linux")] use krun_display::DmabufExport; +use krun_display::{ + DisplayBackend, DisplayBackendBasicFramebuffer, DisplayBackendInstance, Rect, ResourceFormat, +}; use libc::c_void; #[cfg(target_os = "macos")] use rutabaga_gfx::RUTABAGA_MEM_HANDLE_TYPE_APPLE; @@ -31,8 +30,8 @@ use rutabaga_gfx::RUTABAGA_MEM_HANDLE_TYPE_OPAQUE_FD; use rutabaga_gfx::RUTABAGA_MEM_HANDLE_TYPE_SHM; use rutabaga_gfx::{ ResourceCreate3D, ResourceCreateBlob, Rutabaga, RutabagaBuilder, RutabagaChannel, - RutabagaFence, RutabagaFenceHandler, RutabagaIovec, Transfer3D, - RUTABAGA_CHANNEL_TYPE_WAYLAND, RUTABAGA_MAP_CACHE_MASK, + RutabagaFence, RutabagaFenceHandler, RutabagaIovec, Transfer3D, RUTABAGA_CHANNEL_TYPE_WAYLAND, + RUTABAGA_MAP_CACHE_MASK, }; #[cfg(target_os = "linux")] use rutabaga_gfx::{ @@ -629,9 +628,11 @@ impl VirtioGpu { #[link(name = "GL")] extern "C" { fn glFlush(); + fn glFinish(); } - glFlush() + glFlush(); + glFinish(); }; for scanout_id in resource.scanouts.iter_enabled() {