diff --git a/Cargo.lock b/Cargo.lock index d38bd36..7317bf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,15 @@ version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -552,6 +561,16 @@ version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -614,6 +633,7 @@ name = "multibg-sway" version = "0.1.10" dependencies = [ "anyhow", + "ash", "clap", "env_logger", "fast_image_resize", @@ -622,6 +642,7 @@ dependencies = [ "log", "niri-ipc", "rustix", + "scopeguard", "serde", "serde_json", "smithay-client-toolkit", @@ -828,6 +849,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.219" diff --git a/Cargo.toml b/Cargo.toml index 372d2e9..cb8b8ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,25 +15,25 @@ exclude = ["/PKGBUILD", "/scripts/"] [dependencies] anyhow = "1.0.97" +ash = "0.38.0" clap = { version = "4.5.3", features = ["derive"] } env_logger = "0.11.3" fast_image_resize = "5.0.0" libc = "0.2.171" log = "0.4.21" -rustix = {version = "0.38.44", features = ["event", "pipe"] } +niri-ipc = "=25.2.0" +rustix = { version = "0.38.44", features = ["event", "fs", "pipe"] } +scopeguard = "1.2.0" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" +smithay-client-toolkit = { version = "0.19.2", default-features = false } swayipc = "3.0.2" -niri-ipc = "=25.2.0" [dependencies.image] version = "0.25.6" default-features = false features = ["bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp"] -[dependencies.smithay-client-toolkit] -version = "0.19.2" -default-features = false - [features] +default = ["avif"] avif = ["image/avif-native"] diff --git a/PKGBUILD b/PKGBUILD index 9e3f4ec..125299e 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -12,6 +12,8 @@ optdepends=( 'hyprland: supported window manager to set the wallpapers with' 'niri: supported window manager to set the wallpapers with' 'sway: supported window manager to set the wallpapers with' + 'vulkan-driver: upload and serve wallpapers from GPU memory' + 'vulkan-icd-loader: upload and serve wallpapers from GPU memory' ) source=("$pkgname-$pkgver.tar.gz::https://static.crates.io/crates/$pkgname/$pkgname-$pkgver.crate") sha256sums=('2b087124ea07635e53d411e707f7d22f73c69b40f3986a42c841f9cc19fc2d51') diff --git a/src/cli.rs b/src/cli.rs index 778582b..f4fea2b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -90,6 +90,9 @@ pub struct Cli { /// Wayland compositor to connect (autodetect by default) #[arg(long)] pub compositor: Option, + /// upload and serve wallpapers from GPU memory using Vulkan + #[arg(long)] + pub gpu: bool, /// directory with: wallpaper_dir/output/workspace_name.{jpg|png|...} pub wallpaper_dir: String, } diff --git a/src/gpu.rs b/src/gpu.rs new file mode 100644 index 0000000..abc2ecb --- /dev/null +++ b/src/gpu.rs @@ -0,0 +1,302 @@ +#![allow(unsafe_op_in_unsafe_fn)] + +// https://registry.khronos.org/vulkan/specs/latest/man/html/VK_EXT_image_drm_format_modifier.html + +mod device; +mod instance; +mod memory; + +use std::{ + ffi::CStr, + os::fd::OwnedFd, + rc::{Rc, Weak}, + slice, +}; + +use anyhow::Context; +use ash::{ + Device, Entry, Instance, + ext::{ + debug_report, + debug_utils, + image_drm_format_modifier, + }, + khr::external_memory_fd, + vk::{ + Buffer, + CommandBuffer, + CommandPool, + DebugReportCallbackEXT, + DebugUtilsMessengerEXT, + DeviceMemory, + DrmFormatModifierPropertiesEXT, + ExtensionProperties, + Extent2D, + Image, + PhysicalDevice, + PhysicalDeviceMemoryProperties, + Queue, + } +}; +use log::{debug, error}; +use rustix::fs::Dev; + +use device::device; +use instance::instance; +use memory::{upload, uploader}; + +pub struct Gpu { + instance: Rc, + devices: Vec>, +} + +impl Gpu { + pub fn new() -> anyhow::Result { + let instance = Rc::new(unsafe { + instance() + }.context("Failed to create Vulkan instance")?); + let devices = Vec::new(); + Ok(Gpu { instance, devices }) + } + + pub fn uploader( + &mut self, + dmabuf_drm_dev: Option, + width: u32, + height: u32, + drm_format_modifiers: Vec, + ) -> anyhow::Result { + unsafe { + let mut selected = self.select_device(dmabuf_drm_dev); + if selected.is_none() { + let new_device = Rc::new(device(&self.instance, dmabuf_drm_dev) + .context("Failed to create new device")?); + self.devices.push(Rc::downgrade(&new_device)); + selected = Some(new_device); + } + let gpu_device = selected.unwrap(); + uploader(gpu_device, width, height, drm_format_modifiers) + .context("Failed to create GPU uploader") + } + } + + fn select_device( + &mut self, + dmabuf_drm_dev: Option + ) -> Option> { + let mut ret = None; + self.devices.retain(|weak_gpu_device| { + if let Some(gpu_device) = weak_gpu_device.upgrade() { + if ret.is_none() + && gpu_device.dmabuf_drm_dev_eq(dmabuf_drm_dev) + { + ret = Some(gpu_device) + } + true + } else { + false + } + }); + ret + } +} + +struct GpuInstance { + _entry: Entry, + instance: Instance, + debug: Debug, +} + +impl Drop for GpuInstance { + fn drop(&mut self) { + unsafe { + match &self.debug { + Debug::Utils { instance, messenger } => { + instance.destroy_debug_utils_messenger(*messenger, None); + }, + Debug::Report { instance, callback } => { + #[allow(deprecated)] + instance.destroy_debug_report_callback(*callback, None); + } + Debug::None => (), + }; + self.instance.destroy_instance(None); + debug!("Vulkan context has been cleaned up"); + } + } +} + +enum Debug { + Utils { + instance: debug_utils::Instance, + messenger: DebugUtilsMessengerEXT, + }, + Report { + instance: debug_report::Instance, + callback: DebugReportCallbackEXT, + }, + None, +} + +struct GpuDevice { + gpu_instance: Rc, + physdev: PhysicalDevice, + primary_drm_dev: Option, + render_drm_dev: Option, + dmabuf_drm_dev: Option, + memory_props: PhysicalDeviceMemoryProperties, + drm_format_props: Option>, + device: Device, + external_memory_fd_device: external_memory_fd::Device, + image_drm_format_modifier_device: Option, + queue_family_index: u32, + queue: Queue, + command_pool: CommandPool, + command_buffer: CommandBuffer, +} + +impl Drop for GpuDevice { + fn drop(&mut self) { + unsafe { + if let Err(e) = self.device.device_wait_idle() { + error!("Failed to wait device idle: {e}"); + }; + self.device.destroy_command_pool(self.command_pool, None); + self.device.destroy_device(None); + } + } +} + +impl GpuDevice { + fn dmabuf_drm_dev_eq(&self, drm_dev: Option) -> bool { + if drm_dev.is_some() { + assert!(self.dmabuf_drm_dev.is_some()); + drm_dev == self.dmabuf_drm_dev + || drm_dev == self.render_drm_dev + || drm_dev == self.primary_drm_dev + } else { + assert!(self.dmabuf_drm_dev.is_none()); + true + } + } +} + +pub struct GpuUploader { + gpu_device: Rc, + buffer: Buffer, + memory: DeviceMemory, + ptr: *mut u8, + len: usize, + extent: Extent2D, + drm_format_modifiers: Vec, +} + +impl Drop for GpuUploader { + fn drop(&mut self) { + unsafe { + let device = &self.gpu_device.device; + device.unmap_memory(self.memory); + device.free_memory(self.memory, None); + device.destroy_buffer(self.buffer, None); + } + } +} + +impl GpuUploader { + pub fn staging_buffer(&mut self) -> &mut [u8] { + unsafe { slice::from_raw_parts_mut(self.ptr, self.len) } + } + + pub fn upload(&mut self) -> anyhow::Result { + unsafe { upload(self) } + } +} + +pub struct GpuWallpaper { + pub drm_format_modifier: u64, + pub memory_planes_len: usize, + pub memory_planes: [MemoryPlane; 4], + pub gpu_memory: GpuMemory, + pub fd: OwnedFd, +} + +#[derive(Clone, Copy, Default)] +pub struct MemoryPlane { + pub offset: u64, + pub stride: u64, +} + +pub struct GpuMemory { + gpu_device: Rc, + image: Image, + memory: DeviceMemory, + size: usize, + drm_format_modifier: u64, +} + +impl Drop for GpuMemory { + fn drop(&mut self) { + unsafe { + self.gpu_device.device.destroy_image(self.image, None); + self.gpu_device.device.free_memory(self.memory, None); + } + } +} + +impl GpuMemory { + pub fn gpu_uploader_eq(&self, gpu_uploader: &GpuUploader) -> bool { + self.dmabuf_feedback_eq( + gpu_uploader.gpu_device.dmabuf_drm_dev, + gpu_uploader.drm_format_modifiers.as_slice(), + ) + } + + pub fn dmabuf_feedback_eq( + &self, + dmabuf_drm_dev: Option, + drm_format_modifiers: &[u64] + ) -> bool { + self.gpu_device.dmabuf_drm_dev_eq(dmabuf_drm_dev) + && drm_format_modifiers.contains(&self.drm_format_modifier) + } + + pub fn size(&self) -> usize { + self.size + } +} + +// Fourcc codes are based on libdrm drm_fourcc.h +// https://gitlab.freedesktop.org/mesa/drm/-/blob/main/include/drm/drm_fourcc.h +// /usr/include/libdrm/drm_fourcc.h +// under license MIT +pub const fn fourcc_code(a: u8, b: u8, c: u8, d: u8) -> u32 { + (a as u32) | (b as u32) << 8 | (c as u32) << 16 | (d as u32) << 24 +} + +pub const fn fourcc_mod_code(vendor: u64, val: u64) -> u64 { + (vendor << 56) | (val & 0x00ff_ffff_ffff_ffff) +} + +// pub const DRM_FORMAT_INVALID: u32 = 0; +pub const DRM_FORMAT_XRGB8888: u32 = fourcc_code(b'X', b'R', b'2', b'4'); +// pub const DRM_FORMAT_ARGB8888: u32 = fourcc_code(b'A', b'R', b'2', b'4'); + +pub const DRM_FORMAT_MOD_VENDOR_NONE: u64 = 0; +// pub const DRM_FORMAT_RESERVED: u64 = (1 << 56) - 1; + +// pub const DRM_FORMAT_MOD_INVALID: u64 = fourcc_mod_code( +// DRM_FORMAT_MOD_VENDOR_NONE, +// DRM_FORMAT_RESERVED, +// ); +pub const DRM_FORMAT_MOD_LINEAR: u64 = fourcc_mod_code( + DRM_FORMAT_MOD_VENDOR_NONE, + 0, +); + +fn has_extension(extensions: &[ExtensionProperties], name: &CStr) -> bool { + extensions.iter().any(|ext| ext.extension_name_as_c_str() == Ok(name)) +} + +pub fn fmt_modifier(drm_format_modifier: u64) -> String { + format!("{drm_format_modifier:016x}") +} diff --git a/src/gpu/device.rs b/src/gpu/device.rs new file mode 100644 index 0000000..43479e8 --- /dev/null +++ b/src/gpu/device.rs @@ -0,0 +1,420 @@ +use std::{ + ffi::{CStr, c_char}, + rc::Rc, +}; + +use anyhow::{bail, Context}; +use ash::{ + Instance, + ext::{ + external_memory_dma_buf, + image_drm_format_modifier, + physical_device_drm, + queue_family_foreign, + }, + khr::{ + driver_properties, + external_memory_fd, + image_format_list, + }, + vk::{ + self, + api_version_variant, + api_version_major, + api_version_minor, + api_version_patch, + CommandBufferAllocateInfo, + CommandBufferLevel, + CommandPoolCreateFlags, + CommandPoolCreateInfo, + DeviceCreateInfo, + DeviceQueueCreateInfo, + DrmFormatModifierPropertiesEXT, + DrmFormatModifierPropertiesListEXT, + ExtensionProperties, + Format, + FormatProperties2, + PhysicalDevice, + PhysicalDeviceDriverProperties, + PhysicalDeviceDrmPropertiesEXT, + PhysicalDeviceProperties, + PhysicalDeviceProperties2, + PhysicalDeviceType, + QueueFlags, + } +}; +use log::{debug, error, warn}; +use rustix::fs::{Dev, major, makedev, minor}; +use scopeguard::{guard, ScopeGuard}; + +use super::{GpuDevice, GpuInstance, has_extension}; + +struct PhysdevInfo { + physdev: PhysicalDevice, + props: PhysicalDeviceProperties, + extensions: Extensions, + primary: Option, + render: Option, + dmabuf_dev: Option, + score: u32, +} + +const SCORE_MATCHES_DRM_DEV: u32 = 1 << 3; +const SCORE_DISCRETE_GPU: u32 = 1 << 2; +const SCORE_INTEGRATED_GPU: u32 = 1 << 1; +const SCORE_VIRTUAL_GPU: u32 = 1 << 0; + +pub unsafe fn device( + gpu_instance: &Rc, + dmabuf_drm_dev: Option, +) -> anyhow::Result { + let instance = &gpu_instance.instance; + let physdevs = instance.enumerate_physical_devices() + .context("Failed to enumerate physical devices")?; + let count = physdevs.len(); + if count == 0 { + bail!("No physical devices found. Make sure you have a Vulkan driver \ + installed for your GPU and this application has permisson to \ + access graphics devices"); + } + let mut physdev_infos = physdevs.into_iter() + .filter_map(|physdev| physdev_info(instance, dmabuf_drm_dev, physdev)) + .collect::>(); + physdev_infos.sort_by_key(|info| u32::MAX - info.score); + let Some(max_score) = physdev_infos.first().map(|info| info.score) else { + bail!("No physical devices could be probed") + }; + physdev_infos.retain(|info| info.score == max_score); + if physdev_infos.len() == 1 { + debug!("Probed {} physical device(s), max score {}", count, max_score); + return device_with_physdev(gpu_instance, physdev_infos.pop().unwrap()) + } + warn!("Filtered multiple physical devices, {} out of {} with max score {}", + physdev_infos.len(), count, max_score); + let mut gpu_device_ok = None; + let mut errors = Vec::new(); + for physdev_info in physdev_infos { + match device_with_physdev(gpu_instance, physdev_info) { + Ok(gpu_device) => { + gpu_device_ok = Some(gpu_device); + break + }, + Err(e) => errors.push(e), + } + } + if let Some(gpu_device) = gpu_device_ok { + for e in errors { + warn!("{e:#}"); + } + Ok(gpu_device) + } else { + for e in errors { + error!("{e:#}"); + } + bail!("Failed to set up device with all filtered physical devices"); + } +} + +unsafe fn physdev_info( + instance: &Instance, + dmabuf_dev: Option, + physdev: PhysicalDevice, +) -> Option { + let extension_props_vec = match instance + .enumerate_device_extension_properties(physdev) + { + Ok(ext_props) => ext_props, + Err(e) => { + let props = instance.get_physical_device_properties(physdev); + let name = props.device_name_as_c_str().unwrap_or(c"unknown"); + let typ = props.device_type; + error!("Failed to enumerate device extension properties + for physical device {name:?} (type {typ:?}): {e}"); + return None + } + }; + let extensions = Extensions::new(extension_props_vec); + let mut props2_chain = PhysicalDeviceProperties2::default(); + let has_drm_props = extensions.has(physical_device_drm::NAME); + let mut drm_props = PhysicalDeviceDrmPropertiesEXT::default(); + if has_drm_props { + props2_chain = props2_chain.push_next(&mut drm_props); + } + let has_driver_props = extensions.has(driver_properties::NAME); + let mut driver_props = PhysicalDeviceDriverProperties::default(); + if has_driver_props { + props2_chain = props2_chain.push_next(&mut driver_props); + } + instance.get_physical_device_properties2(physdev, &mut props2_chain); + let props = props2_chain.properties; + let name = props.device_name_as_c_str().unwrap_or(c"unknown"); + let typ = props.device_type; + debug!("Probing physical device {name:?} (type {typ:?})"); + let mut score = 0u32; + match typ { + PhysicalDeviceType::DISCRETE_GPU => score |= SCORE_DISCRETE_GPU, + PhysicalDeviceType::INTEGRATED_GPU => score |= SCORE_INTEGRATED_GPU, + PhysicalDeviceType::VIRTUAL_GPU => score |= SCORE_VIRTUAL_GPU, + _ => (), + } + if has_driver_props { + debug!("Physical device driver: {:?}, {:?}", + driver_props.driver_name_as_c_str().unwrap_or(c"unknown"), + driver_props.driver_info_as_c_str().unwrap_or(c"unknown")); + } else { + debug!("VK_KHR_driver_properties unavailable"); + } + let (mut primary, mut render) = (None, None); + if has_drm_props { + if drm_props.has_primary == vk::TRUE { + primary = Some(makedev( + drm_props.primary_major as _, + drm_props.primary_minor as _, + )); + } + if drm_props.has_render == vk::TRUE { + render = Some(makedev( + drm_props.render_major as _, + drm_props.render_minor as _, + )); + } + debug!("Physical device DRM devs: primary {}, render {}", + fmt_dev_option(primary), fmt_dev_option(render)); + // Note [1] + if dmabuf_dev.is_some() + && (dmabuf_dev == primary || dmabuf_dev == render) + { + score |= SCORE_MATCHES_DRM_DEV; + debug!("Physical device matched with the DMA-BUF feedback DRM dev"); + } else { + debug!("Could not match physical device with the DMA-BUF feedback \ + DRM dev"); + } + } else { + debug!("VK_EXT_physical_device_drm unavailable"); + } + Some(PhysdevInfo { + physdev, + props, + extensions, + primary, + render, + dmabuf_dev, + score, + }) +} + +unsafe fn device_with_physdev( + gpu_instance: &Rc, + physdev_info: PhysdevInfo, +) -> anyhow::Result { + let name = physdev_info.props + .device_name_as_c_str().unwrap_or(c"unknown").to_owned(); + let typ = physdev_info.props.device_type; + let score = physdev_info.score; + debug!("Setting up device with physical device {name:?} (type {typ:?})"); + let gpu_device = try_device_with_physdev(gpu_instance, physdev_info) + .with_context(|| format!( + "Failed to set up device with physical device {:?} (type {:?})", + name, typ + ))?; + if score & SCORE_MATCHES_DRM_DEV == 0 { + // We get here if either + // - using Wayland protocol Linux DMA-BUF version < 4 + // - device has no VK_EXT_physical_device_drm + warn!("IMPORTANT: Failed to ensure that we select the same GPU where \ + the compositor is running based on the DRM device numbers. About \ + to use physical device {:?} (type {:?}). If this is incorrect \ + then please restart without the --gpu option and open an issue", + name, typ); + } + Ok(gpu_device) +} + +unsafe fn try_device_with_physdev( + gpu_instance: &Rc, + physdev_info: PhysdevInfo, +) -> anyhow::Result { + let instance = &gpu_instance.instance; + let PhysdevInfo { + physdev, + props, + mut extensions, + primary, + render, + dmabuf_dev, + .. + } = physdev_info; + let variant = api_version_variant(props.api_version); + let major = api_version_major(props.api_version); + let minor = api_version_minor(props.api_version); + let patch = api_version_patch(props.api_version); + if variant != 0 || major != 1 || minor < 1 { + bail!("Need Vulkan device variant 0 version 1.1.0 or compatible, + found variant {variant} version {major}.{minor}.{patch}"); + } + debug!("Vulkan device supports version {major}.{minor}.{patch}"); + let memory_props = instance + .get_physical_device_memory_properties(physdev); + let queue_family_props = instance + .get_physical_device_queue_family_properties(physdev); + let queue_family_index = queue_family_props.iter() + .position(|props| { + props.queue_flags.contains(QueueFlags::GRAPHICS) + && props.queue_count > 0 + }) + .context("Failed to find an appropriate queue family")? as u32; + // Device extension dependency chains with Vulkan 1.1 + // app --> EXT_external_memory_dma_buf -> KHR_external_memory_fd + // \-> EXT_queue_family_foreign + // \-> (optional) EXT_image_drm_format_modifier -> KHR_image_format_list + // EXT_image_drm_format_modifier is notably unsupported by + // - AMD GFX8 and older + // - end-of-life Nvidia GPUs which never got driver version 515 + extensions.try_enable(external_memory_fd::NAME) + .context("KHR_external_memory_fd unavailable")?; + extensions.try_enable(external_memory_dma_buf::NAME) + .context("EXT_external_memory_dma_buf unavailable")?; + extensions.try_enable(queue_family_foreign::NAME) + .context("EXT_queue_family_foreign unavailable")?; + extensions.try_enable(image_format_list::NAME) + .context("KHR_image_format_list unavailable")?; + let has_image_drm_format_modifier = extensions + .try_enable(image_drm_format_modifier::NAME).is_some(); + let device = guard( + instance.create_device( + physdev, + &DeviceCreateInfo::default() + .queue_create_infos(&[DeviceQueueCreateInfo::default() + .queue_family_index(queue_family_index) + .queue_priorities(&[1.0])] + ) + .enabled_extension_names(extensions.enabled()), + None + ).context("Failed to create device")?, + |device| device.destroy_device(None), + ); + let external_memory_fd_device = + external_memory_fd::Device::new(instance, &device); + let image_drm_format_modifier_device = if has_image_drm_format_modifier { + Some(image_drm_format_modifier::Device::new(instance, &device)) + } else { + debug!("EXT_image_drm_format_modifier unavailable"); + None + }; + let queue = device.get_device_queue(queue_family_index, 0); + let command_pool = guard( + device.create_command_pool( + &CommandPoolCreateInfo::default() + .flags(CommandPoolCreateFlags::RESET_COMMAND_BUFFER) + .queue_family_index(queue_family_index), + None + ).context("Failed to create command pool")?, + |command_pool| device.destroy_command_pool(command_pool, None) + ); + let command_buffer = guard( + device.allocate_command_buffers( + &CommandBufferAllocateInfo::default() + .command_buffer_count(1) + .command_pool(*command_pool) + .level(CommandBufferLevel::PRIMARY) + ).context("Failed to allocate command buffer")?[0], + |command_buffer| + device.free_command_buffers(*command_pool, &[command_buffer]), + ); + let drm_format_props = if has_image_drm_format_modifier { + Some(drm_format_props_b8g8r8a8_srgb(instance, physdev)) + } else { + None + }; + Ok(GpuDevice { + command_buffer: ScopeGuard::into_inner(command_buffer), + command_pool: ScopeGuard::into_inner(command_pool), + device: ScopeGuard::into_inner(device), + external_memory_fd_device, + image_drm_format_modifier_device, + physdev, + memory_props, + drm_format_props, + primary_drm_dev: primary, + render_drm_dev: render, + dmabuf_drm_dev: dmabuf_dev, + queue, + queue_family_index, + gpu_instance: Rc::clone(gpu_instance), + }) +} + +struct Extensions { + props: Vec, + enabled: Vec<*const c_char>, +} + +impl Extensions { + fn new(props: Vec) -> Extensions { + Extensions { props, enabled: Vec::new() } + } + + fn has(&self, name: &CStr) -> bool { + has_extension(&self.props, name) + } + + fn try_enable(&mut self, extension: &CStr) -> Option<()> { + if self.props.iter().any(|ext| + ext.extension_name_as_c_str() == Ok(extension) + ) { + self.enabled.push(extension.as_ptr()); + Some(()) + } else { + None + } + } + + fn enabled(&self) -> &[*const c_char] { + &self.enabled + } +} + +fn fmt_dev_option(dev: Option) -> String { + if let Some(dev) = dev { + format!("{}:{}", major(dev), minor(dev)) + } else { + "unavailable".to_string() + } +} + +unsafe fn drm_format_props_b8g8r8a8_srgb( + instance: &Instance, + physdev: PhysicalDevice, +) -> Vec { + let mut drm_format_props_list = + DrmFormatModifierPropertiesListEXT::default(); + instance.get_physical_device_format_properties2( + physdev, + Format::B8G8R8A8_SRGB, + &mut FormatProperties2::default().push_next(&mut drm_format_props_list), + ); + let mut drm_format_props = vec![ + DrmFormatModifierPropertiesEXT::default(); + drm_format_props_list.drm_format_modifier_count as usize + ]; + drm_format_props_list = drm_format_props_list + .drm_format_modifier_properties(&mut drm_format_props); + let mut format_props_chain = FormatProperties2::default() + .push_next(&mut drm_format_props_list); + instance.get_physical_device_format_properties2( + physdev, + Format::B8G8R8A8_SRGB, + &mut format_props_chain, + ); + drm_format_props +} + +// [1] Wayland DMA-BUF says for the feedback main device and for the tranche +// target device one must not compare the dev_t and should use drmDevicesEqual +// from libdrm.so instead to find the same GPU. But neither Mesa Vulkan WSI +// Wayland nor wlroots Vulkan renderer does that, they both use +// PhysicalDeviceDrmPropertiesEXT the same way we do here. So this is probably +// fine (because it provides both the primary and the render DRM node to +// compare against not just one of them (?)). Do we need a fallback using +// libdrm drmDevicesEqual? diff --git a/src/gpu/instance.rs b/src/gpu/instance.rs new file mode 100644 index 0000000..3fa67ae --- /dev/null +++ b/src/gpu/instance.rs @@ -0,0 +1,232 @@ +use std::{ + backtrace::Backtrace, + borrow::Cow, + ffi::{c_void, CStr}, + ptr, +}; + +use anyhow::{bail, Context}; +use ash::{ + Entry, + ext::{ + debug_report, + debug_utils, + }, + vk::{ + self, + api_version_variant, + api_version_major, + api_version_minor, + api_version_patch, + ApplicationInfo, + Bool32, + DebugReportCallbackCreateInfoEXT, + DebugReportFlagsEXT, + DebugReportObjectTypeEXT, + DebugUtilsMessengerCallbackDataEXT, + DebugUtilsMessengerCreateInfoEXT, + DebugUtilsMessageSeverityFlagsEXT, + DebugUtilsMessageTypeFlagsEXT, + InstanceCreateInfo, + LayerProperties, + make_api_version, + } +}; +use log::{debug, error, info, warn}; + +use super::{Debug, GpuInstance, has_extension}; + +const APP_VK_NAME: &CStr = match CStr::from_bytes_with_nul( + concat!(env!("CARGO_PKG_NAME"), '\0').as_bytes() +) { + Ok(val) => val, + Err(_) => panic!(), +}; +const APP_VK_VERSION: u32 = make_api_version( + 0, + parse_decimal(env!("CARGO_PKG_VERSION_MAJOR")), + parse_decimal(env!("CARGO_PKG_VERSION_MINOR")), + parse_decimal(env!("CARGO_PKG_VERSION_PATCH")), +); +const LAYER_VALIDATION: &CStr = c"VK_LAYER_KHRONOS_validation"; +const VULKAN_VERSION_TARGET: u32 = make_api_version(0, 1, 1, 0); + +pub unsafe fn instance() -> anyhow::Result { + let entry = Entry::load() + .context("Failed to load Vulkan shared libraries. Make sure you have \ + the Vulkan loader and a Vulkan driver for your GPU installed")?; + let instance_version = entry.try_enumerate_instance_version() + .context("Failed to enumerate instance version")? + .unwrap_or_else(|| make_api_version(0, 1, 0, 0)); + let variant = api_version_variant(instance_version); + let major = api_version_major(instance_version); + let minor = api_version_minor(instance_version); + let patch = api_version_patch(instance_version); + if variant != 0 || major != 1 || minor < 1 { + bail!("Need Vulkan instance variant 0 version 1.1.0 or compatible, + found variant {variant} version {major}.{minor}.{patch}"); + } + debug!("Vulkan instance supports version {major}.{minor}.{patch}"); + let instance_layer_props = entry.enumerate_instance_layer_properties() + .context("Failed to enumerate instance layers")?; + let instance_extension_props = entry + .enumerate_instance_extension_properties(None) + .context("Failed to enumerate instance extensions")?; + let mut instance_layers = Vec::new(); + let mut extension_layers = Vec::new(); + let mut has_debug_utils = false; + let mut has_debug_report = false; + if log::log_enabled!(log::Level::Debug) { + debug!("Running with log level DEBUG or higher \ + so trying to enable Vulkan validation layers"); + if has_layer(&instance_layer_props, LAYER_VALIDATION) { + instance_layers.push(LAYER_VALIDATION.as_ptr()); + info!("Enabling VK_LAYER_KHRONOS_validation"); + } else { + warn!("VK_LAYER_KHRONOS_validation unavailable"); + } + if has_extension(&instance_extension_props, debug_utils::NAME) { + debug!("Enabling VK_EXT_debug_utils"); + has_debug_utils = true; + extension_layers.push(debug_utils::NAME.as_ptr()); + } else if has_extension(&instance_extension_props, debug_report::NAME) { + debug!("Enabling VK_EXT_debug_report"); + has_debug_report = true; + extension_layers.push(debug_report::NAME.as_ptr()); + } else { + warn!("VK_EXT_debug_utils and VK_EXT_debug_report unavailable"); + } + } + let instance = entry.create_instance( + &InstanceCreateInfo::default() + .application_info(&ApplicationInfo::default() + .application_name(APP_VK_NAME) + .application_version(APP_VK_VERSION) + .engine_name(APP_VK_NAME) + .engine_version(APP_VK_VERSION) + .api_version(VULKAN_VERSION_TARGET) + ) + .enabled_layer_names(&instance_layers) + .enabled_extension_names(&extension_layers), + None, + ).context("Failed to create instance")?; + let mut debug = Debug::None; + if has_debug_utils { + let instance = debug_utils::Instance::new(&entry, &instance); + match instance.create_debug_utils_messenger( + &DebugUtilsMessengerCreateInfoEXT::default() + .message_severity( + DebugUtilsMessageSeverityFlagsEXT::ERROR + | DebugUtilsMessageSeverityFlagsEXT::WARNING + ) + .message_type( + DebugUtilsMessageTypeFlagsEXT::GENERAL + | DebugUtilsMessageTypeFlagsEXT::VALIDATION + | DebugUtilsMessageTypeFlagsEXT::PERFORMANCE, + ) + .pfn_user_callback(Some(debug_utils_callback)), + None + ) { + Ok(messenger) => + debug = Debug::Utils { instance, messenger }, + Err(e) => + error!("Failed to create Vulkan debug utils messenger: {e}"), + }; + } else if has_debug_report { + let instance = debug_report::Instance::new(&entry, &instance); + #[allow(deprecated)] + match instance.create_debug_report_callback( + &DebugReportCallbackCreateInfoEXT::default() + .flags(DebugReportFlagsEXT::WARNING + | DebugReportFlagsEXT::PERFORMANCE_WARNING + | DebugReportFlagsEXT::ERROR) + .pfn_callback(Some(debug_report_callback)) + .user_data(ptr::null_mut()), + None, + ) { + Ok(callback) => + debug = Debug::Report { instance, callback }, + Err(e) => + error!("Failed to create Vulkan debug report callback: {e}"), + }; + } + Ok(GpuInstance { + _entry: entry, + instance, + debug, + }) +} + +const fn parse_decimal(src: &str) -> u32 { + match u32::from_str_radix(src, 10) { + Ok(val) => val, + Err(_) => panic!(), + } +} + +fn has_layer(layers: &[LayerProperties], name: &CStr) -> bool { + layers.iter().any(|layer| layer.layer_name_as_c_str() == Ok(name)) +} + +unsafe extern "system" fn debug_utils_callback( + message_severity: DebugUtilsMessageSeverityFlagsEXT, + message_type: DebugUtilsMessageTypeFlagsEXT, + p_callback_data: *const DebugUtilsMessengerCallbackDataEXT<'_>, + _user_data: *mut c_void, +) -> Bool32 { + if p_callback_data.is_null() { + error!("Vulkan: null data"); + return vk::FALSE + } + let callback_data = *p_callback_data; + let message = if callback_data.p_message.is_null() { + Cow::from("null message") + } else { + CStr::from_ptr(callback_data.p_message).to_string_lossy() + }; + match message_severity { + DebugUtilsMessageSeverityFlagsEXT::ERROR => { + let backtrace = Backtrace::force_capture(); + error!("Vulkan: {message}\nBacktrace:\n{backtrace}"); + }, + DebugUtilsMessageSeverityFlagsEXT::WARNING => { + warn!("Vulkan: {message}"); + }, + severity => { + error!("Unexpected Vulkan message {:?} {:?}: {}", + message_type, severity, message); + }, + }; + vk::FALSE +} + +unsafe extern "system" fn debug_report_callback( + flags: DebugReportFlagsEXT, + _object_type: DebugReportObjectTypeEXT, + _object: u64, + _location: usize, + _message_code: i32, + _p_layer_prefix: *const i8, + p_message: *const i8, + _p_user_data: *mut c_void +) -> u32 { + let message = if p_message.is_null() { + Cow::from("null message") + } else { + CStr::from_ptr(p_message).to_string_lossy() + }; + match flags { + DebugReportFlagsEXT::ERROR => { + let backtrace = Backtrace::force_capture(); + error!("Vulkan: {message}\nBacktrace:\n{backtrace}"); + }, + DebugReportFlagsEXT::WARNING + | DebugReportFlagsEXT::PERFORMANCE_WARNING => { + warn!("Vulkan: {message}"); + }, + flag => { + error!("Unexpected Vulkan message {flag:?}: {message}"); + } + }; + vk::FALSE +} diff --git a/src/gpu/memory.rs b/src/gpu/memory.rs new file mode 100644 index 0000000..6e4cdd8 --- /dev/null +++ b/src/gpu/memory.rs @@ -0,0 +1,441 @@ +#![allow(clippy::too_many_arguments)] + +use std::{ + os::fd::{FromRawFd, OwnedFd}, + rc::Rc, + slice, +}; + +use anyhow::{bail, Context}; +use ash::{ + Instance, + vk::{ + AccessFlags, + BufferCreateFlags, + BufferCreateInfo, + BufferImageCopy, + BufferUsageFlags, + CommandBufferBeginInfo, + CommandBufferResetFlags, + DependencyFlags, + DeviceSize, + DrmFormatModifierPropertiesEXT, + ExportMemoryAllocateInfo, + Extent2D, + ExternalMemoryHandleTypeFlags, + ExternalMemoryImageCreateInfo, + Fence, + Format, + FormatFeatureFlags, + ImageAspectFlags, + ImageCreateFlags, + ImageCreateInfo, + ImageDrmFormatModifierListCreateInfoEXT, + ImageDrmFormatModifierPropertiesEXT, + ImageFormatProperties2, + ImageLayout, + ImageMemoryBarrier, + ImageSubresource, + ImageSubresourceLayers, + ImageSubresourceRange, + ImageTiling, + ImageType, + ImageUsageFlags, + MemoryAllocateInfo, + MemoryGetFdInfoKHR, + MemoryMapFlags, + MemoryPropertyFlags, + MemoryRequirements, + PhysicalDevice, + PhysicalDeviceImageDrmFormatModifierInfoEXT, + PhysicalDeviceImageFormatInfo2, + PhysicalDeviceMemoryProperties, + PipelineStageFlags, + QUEUE_FAMILY_FOREIGN_EXT, + SampleCountFlags, + SharingMode, + SubmitInfo, + } +}; +use log::debug; +use scopeguard::{guard, ScopeGuard}; + +use super::{ + DRM_FORMAT_MOD_LINEAR, fmt_modifier, + GpuDevice, GpuMemory, GpuUploader, GpuWallpaper, + MemoryPlane, +}; + +pub unsafe fn uploader( + gpu_device: Rc, + width: u32, + height: u32, + drm_format_modifiers: Vec, +) -> anyhow::Result { + let GpuDevice { + gpu_instance, + memory_props, + drm_format_props, + device, + .. + } = gpu_device.as_ref(); + let instance = &gpu_instance.instance; + let physdev = gpu_device.physdev; + let queue_family_index = gpu_device.queue_family_index; + let size = width as DeviceSize * height as DeviceSize * 4; + let mut filtered_modifiers = Vec::with_capacity(drm_format_modifiers.len()); + if let Some(drm_format_props) = drm_format_props { + for &drm_format_modifier in drm_format_modifiers.iter() { + match filter_modifier( + instance, physdev, queue_family_index, drm_format_props, + width, height, size, drm_format_modifier, + ) { + Ok(()) => filtered_modifiers.push(drm_format_modifier), + Err(e) => debug!("Cannot use DRM format modifier {}: {:#}", + fmt_modifier(drm_format_modifier), e), + } + if filtered_modifiers.is_empty() { + bail!("None of the DRM format modifiers can be \ + used for image creation"); + } + } + } else if drm_format_modifiers.contains(&DRM_FORMAT_MOD_LINEAR) { + debug!("Image creation can only use DRM_FORMAT_MOD_LINEAR"); + } else { + bail!("VK_EXT_physical_device_drm unavailable and \ + no DRM_FORMAT_MOD_LINEAR was proposed for image creation"); + } + debug!("Image creation can use DRM format modifiers: {}", + filtered_modifiers.iter() + .map(|&modifier| fmt_modifier(modifier)) + .collect::>().join(", ")); + let buffer = guard( + device.create_buffer( + &BufferCreateInfo::default() + .flags(BufferCreateFlags::empty()) + .size(size) + .usage(BufferUsageFlags::TRANSFER_SRC) + .sharing_mode(SharingMode::EXCLUSIVE) + .queue_family_indices(slice::from_ref(&queue_family_index)), + None + ).context("Failed to create staging buffer")?, + |buffer| device.destroy_buffer(buffer, None), + ); + let buffer_memory_req = device.get_buffer_memory_requirements(*buffer); + let buffer_memory_index = find_memorytype_index( + &buffer_memory_req, + memory_props, + MemoryPropertyFlags::HOST_VISIBLE | MemoryPropertyFlags::HOST_COHERENT + | MemoryPropertyFlags::HOST_CACHED, + ).context("Cannot find suitable device memory type for staging buffer")?; + let memory = guard( + device.allocate_memory( + &MemoryAllocateInfo::default() + .allocation_size(buffer_memory_req.size) + .memory_type_index(buffer_memory_index), + None + ).context("Failed to allocate memory for staging buffer")?, + |memory| device.free_memory(memory, None), + ); + device.bind_buffer_memory(*buffer, *memory, 0) + .context("Failed to bind memory to staging buffer")?; + let ptr = device.map_memory( + *memory, + 0, + buffer_memory_req.size, + MemoryMapFlags::empty() + ).context("Failed to map staging buffer memory")?; + Ok(GpuUploader { + memory: ScopeGuard::into_inner(memory), + buffer: ScopeGuard::into_inner(buffer), + ptr: ptr.cast(), + len: buffer_memory_req.size as usize, + extent: Extent2D { width, height }, + drm_format_modifiers: filtered_modifiers, + gpu_device, + }) +} + +unsafe fn filter_modifier( + instance: &Instance, + physdev: PhysicalDevice, + queue_family_index: u32, + drm_format_props: &[DrmFormatModifierPropertiesEXT], + width: u32, + height: u32, + size: DeviceSize, + drm_format_modifier: u64, +) -> anyhow::Result<()> { + let format_props = drm_format_props.iter() + .find(|props| props.drm_format_modifier == drm_format_modifier) + .context("This modifier is unsupported by this Vulkan context")?; + if !format_props.drm_format_modifier_tiling_features + .contains(FormatFeatureFlags::TRANSFER_DST) + { + bail!("FormatFeatureFlag TRANSFER_DST unsupported"); + } + let mut image_format_props2 = ImageFormatProperties2::default(); + let mut image_drm_info = + PhysicalDeviceImageDrmFormatModifierInfoEXT::default() + .drm_format_modifier(drm_format_modifier) + .sharing_mode(SharingMode::EXCLUSIVE) + .queue_family_indices(slice::from_ref(&queue_family_index)); + instance.get_physical_device_image_format_properties2( + physdev, + &PhysicalDeviceImageFormatInfo2::default() + .format(Format::B8G8R8A8_SRGB) + .ty(ImageType::TYPE_2D) + .tiling(ImageTiling::DRM_FORMAT_MODIFIER_EXT) + .usage(ImageUsageFlags::TRANSFER_DST) + .flags(ImageCreateFlags::empty()) + .push_next(&mut image_drm_info), + &mut image_format_props2, + ).context("The needed image format is unsupported for this modifier")?; + let image_format_props = image_format_props2.image_format_properties; + if image_format_props.max_extent.depth < 1 + || image_format_props.max_mip_levels < 1 + || image_format_props.max_array_layers < 1 + || !image_format_props.sample_counts.contains(SampleCountFlags::TYPE_1) + { + bail!("The needed image format is unsupported for this modifier") + } + let max_width = image_format_props.max_extent.width; + let max_height = image_format_props.max_extent.width; + let max_size = image_format_props.max_resource_size; + if width > max_width { + bail!("Needed image width {width} is greter then the max supported \ + image width {max_width}") + } + if height > max_height { + bail!("Needed image height {height} is greter then the max supported \ + image height {max_height}") + } + if size > max_size { + bail!("Needed image size {size} bytes is greter then the max supported \ + image size {max_width} bytes") + } + Ok(()) +} + +// XXX: we could check if dedicated allocation is needed: +// https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_dedicated_allocation.html +pub unsafe fn upload( + uploader: &mut GpuUploader, +) -> anyhow::Result { + let GpuUploader { + gpu_device, + drm_format_modifiers, + .. + } = uploader; + let GpuDevice { + memory_props, + drm_format_props, + device, + external_memory_fd_device, + image_drm_format_modifier_device, + .. + } = gpu_device.as_ref(); + let extent = uploader.extent; + let buffer = uploader.buffer; + let queue_family_index = gpu_device.queue_family_index; + let command_buffer = gpu_device.command_buffer; + let queue = gpu_device.queue; + let mut external_memory_info = ExternalMemoryImageCreateInfo::default() + .handle_types(ExternalMemoryHandleTypeFlags::DMA_BUF_EXT); + let mut modifier_list_info = + ImageDrmFormatModifierListCreateInfoEXT::default() + .drm_format_modifiers(drm_format_modifiers); + let mut image_create_info = ImageCreateInfo::default() + .flags(ImageCreateFlags::empty()) + .image_type(ImageType::TYPE_2D) + .format(Format::B8G8R8A8_SRGB) + .extent(extent.into()) + .mip_levels(1) + .array_layers(1) + .samples(SampleCountFlags::TYPE_1) + .usage(ImageUsageFlags::TRANSFER_DST) + .sharing_mode(SharingMode::EXCLUSIVE) + .queue_family_indices(slice::from_ref(&queue_family_index)) + .initial_layout(ImageLayout::UNDEFINED) + .push_next(&mut external_memory_info); + if image_drm_format_modifier_device.is_some() { + image_create_info = image_create_info + .tiling(ImageTiling::DRM_FORMAT_MODIFIER_EXT) + .push_next(&mut modifier_list_info); + } else { + image_create_info = image_create_info.tiling(ImageTiling::LINEAR); + } + let image = guard( + device.create_image(&image_create_info, None) + .context("Failed to create image")?, + |image| device.destroy_image(image, None), + ); + let image_memory_req = device.get_image_memory_requirements(*image); + let image_memory_index = find_memorytype_index( + &image_memory_req, + memory_props, + MemoryPropertyFlags::DEVICE_LOCAL, + ).context("Failed to find memorytype index for image")?; + let image_memory = guard( + device.allocate_memory( + &MemoryAllocateInfo::default() + .allocation_size(image_memory_req.size) + .memory_type_index(image_memory_index) + .push_next(&mut ExportMemoryAllocateInfo::default() + .handle_types(ExternalMemoryHandleTypeFlags::DMA_BUF_EXT) + ), + None + ).context("Failed to allocate memory for image")?, + |memory| device.free_memory(memory, None), + ); + device.bind_image_memory(*image, *image_memory, 0) + .context("Failed to bind image memory")?; + device.reset_command_buffer( + command_buffer, + CommandBufferResetFlags::empty() + ).context("Failed to reset command buffer")?; + device.begin_command_buffer( + command_buffer, + &CommandBufferBeginInfo::default() + ) .context("Failed to begin command buffer")?; + device.cmd_pipeline_barrier( + command_buffer, + PipelineStageFlags::TOP_OF_PIPE, + PipelineStageFlags::TRANSFER, + DependencyFlags::empty(), + &[], + &[], + &[ImageMemoryBarrier::default() + .src_access_mask(AccessFlags::NONE) + .dst_access_mask(AccessFlags::TRANSFER_WRITE) + .old_layout(ImageLayout::UNDEFINED) + .new_layout(ImageLayout::GENERAL) + .src_queue_family_index(queue_family_index) + .dst_queue_family_index(queue_family_index) + .image(*image) + .subresource_range(ImageSubresourceRange::default() + .aspect_mask(ImageAspectFlags::COLOR) + .level_count(1) + .layer_count(1) + ) + ], + ); + device.cmd_copy_buffer_to_image( + command_buffer, + buffer, + *image, + ImageLayout::GENERAL, + &[BufferImageCopy::default() + .image_subresource(ImageSubresourceLayers::default() + .aspect_mask(ImageAspectFlags::COLOR) + .layer_count(1) + ) + .image_extent(extent.into()) + ] + ); + // https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#resources-external-sharing + device.cmd_pipeline_barrier( + command_buffer, + PipelineStageFlags::TRANSFER, + PipelineStageFlags::BOTTOM_OF_PIPE, + DependencyFlags::empty(), + &[], + &[], + &[ImageMemoryBarrier::default() + .src_access_mask(AccessFlags::TRANSFER_WRITE) + .dst_access_mask(AccessFlags::NONE) + .old_layout(ImageLayout::GENERAL) + .new_layout(ImageLayout::GENERAL) + .src_queue_family_index(queue_family_index) + .dst_queue_family_index(QUEUE_FAMILY_FOREIGN_EXT) + .image(*image) + .subresource_range(ImageSubresourceRange::default() + .aspect_mask(ImageAspectFlags::COLOR) + .level_count(1) + .layer_count(1) + ) + ], + ); + device.end_command_buffer(command_buffer) + .context("Failed to end command buffer")?; + device.queue_submit( + queue, + &[SubmitInfo::default().command_buffers(&[command_buffer])], + Fence::null(), + ).context("Failed to submit queue")?; + device.queue_wait_idle(queue).context("Failed to wait queue idle")?; + let mut drm_format_modifier = DRM_FORMAT_MOD_LINEAR; + let mut memory_plane_count = 1; + let mut aspect_masks = [ImageAspectFlags::COLOR; 4]; + if let Some(modifier_device) = image_drm_format_modifier_device { + let mut props = ImageDrmFormatModifierPropertiesEXT::default(); + modifier_device + .get_image_drm_format_modifier_properties(*image, &mut props) + .context("Failed to get image drm format modifier properties")?; + drm_format_modifier = props.drm_format_modifier; + debug!("Image created with DRM format modifier {}", + fmt_modifier(drm_format_modifier)); + let format_prop = drm_format_props.as_ref().unwrap().iter().find(|f| + f.drm_format_modifier == drm_format_modifier + ).context("Failed to find DRM format modifier properties")?; + memory_plane_count = format_prop + .drm_format_modifier_plane_count as usize; + aspect_masks = [ + ImageAspectFlags::MEMORY_PLANE_0_EXT, + ImageAspectFlags::MEMORY_PLANE_1_EXT, + ImageAspectFlags::MEMORY_PLANE_2_EXT, + ImageAspectFlags::MEMORY_PLANE_3_EXT, + ]; + } + let mut memory_planes = [MemoryPlane::default(); 4]; + for memory_plan_index in 0..memory_plane_count { + let subresource_layout = device.get_image_subresource_layout( + *image, + ImageSubresource::default() + .aspect_mask(aspect_masks[memory_plan_index]) + .mip_level(0) + .array_layer(0) + ); + memory_planes[memory_plan_index] = MemoryPlane { + offset: subresource_layout.offset, + stride: subresource_layout.row_pitch, + }; + } + let raw_fd = external_memory_fd_device.get_memory_fd( + &MemoryGetFdInfoKHR::default() + .memory(*image_memory) + .handle_type(ExternalMemoryHandleTypeFlags::DMA_BUF_EXT) + ).context("Failed to get memory fd")?; + if raw_fd < 0 { + bail!("Got invalid memory fd {raw_fd}") + } + let fd = OwnedFd::from_raw_fd(raw_fd); + Ok(GpuWallpaper { + drm_format_modifier, + memory_planes_len: memory_plane_count, + memory_planes, + gpu_memory: GpuMemory { + memory: ScopeGuard::into_inner(image_memory), + size: uploader.len, + image: ScopeGuard::into_inner(image), + gpu_device: Rc::clone(&uploader.gpu_device), + drm_format_modifier, + }, + fd, + }) +} + +fn find_memorytype_index( + memory_req: &MemoryRequirements, + memory_prop: &PhysicalDeviceMemoryProperties, + flags: MemoryPropertyFlags, +) -> Option { + memory_prop.memory_types[..memory_prop.memory_type_count as _] + .iter() + .enumerate() + .find(|(index, memory_type)| { + (1 << index) & memory_req.memory_type_bits != 0 + && memory_type.property_flags & flags == flags + }) + .map(|(index, _memory_type)| index as _) +} diff --git a/src/image.rs b/src/image.rs index d6dfde8..6cef2d2 100644 --- a/src/image.rs +++ b/src/image.rs @@ -73,7 +73,7 @@ pub fn output_wallpaper_files( pub fn load_wallpaper( path: &Path, - dst: &mut [u8], + buffer: &mut [u8], surface_width: u32, surface_height: u32, surface_stride: usize, @@ -81,6 +81,11 @@ pub fn load_wallpaper( color_transform: ColorTransform, resizer: &mut Resizer, ) -> anyhow::Result<()> { + let surface_size = surface_stride * surface_height as usize; + let Some(dst) = buffer.get_mut(..surface_size) else { + bail!("Provided buffer size {} smaller than wallpaper image size {}", + buffer.len(), surface_size); + }; let reader = ImageReader::open(path) .context("Failed to open image file")? .with_guessed_format() @@ -101,7 +106,6 @@ pub fn load_wallpaper( if image_width == 0 || image_height == 0 || image_size > isize::MAX as u64 { bail!("Image has invalid dimensions {image_width}x{image_height}") }; - let image_size = image_size as usize; debug!("Image {image_width}x{image_height} {image_color_type:?}"); if image_color_type.has_alpha() { warn!("Image has alpha channel which will be ignored"); @@ -120,8 +124,7 @@ pub fn load_wallpaper( && surface_row_len == surface_stride { debug!("Decoding image directly to destination buffer"); - decoder.read_image(&mut dst[..image_size]) - .context("Failed to decode image")?; + decoder.read_image(dst).context("Failed to decode image")?; return Ok(()); } let mut image = DynamicImage::from_decoder(decoder) diff --git a/src/main.rs b/src/main.rs index f398f5c..4987d52 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod compositors; mod cli; +mod gpu; mod image; mod poll; mod signal; @@ -23,6 +24,7 @@ use rustix::{ }; use smithay_client_toolkit::{ compositor::CompositorState, + dmabuf::DmabufState, output::OutputState, registry::RegistryState, shell::wlr_layer::LayerShell, @@ -41,6 +43,7 @@ use crate::{ cli::{Cli, PixelFormat}, compositors::{Compositor, ConnectionTask, WorkspaceVisible}, image::ColorTransform, + gpu::Gpu, poll::{Poll, Waker}, signal::SignalPipe, wayland::BackgroundLayer, @@ -54,37 +57,14 @@ pub struct State { pub layer_shell: LayerShell, pub viewporter: WpViewporter, pub wallpaper_dir: PathBuf, - pub force_xrgb8888: bool, - pub pixel_format: Option, + pub shm_format: wl_shm::Format, pub background_layers: Vec, pub compositor_connection_task: ConnectionTask, pub color_transform: ColorTransform, + pub dmabuf_state: DmabufState, + pub gpu: Option, } -impl State { - fn pixel_format(&mut self) -> wl_shm::Format - { - *self.pixel_format.get_or_insert_with(|| { - - if !self.force_xrgb8888 { - // Consume less gpu memory by using Bgr888 if available, - // fall back to the always supported Xrgb8888 otherwise - for format in self.shm.formats() { - if let wl_shm::Format::Bgr888 = format { - debug!("Using pixel format: {:?}", format); - return *format - } - // XXX: One may add Rgb888 and HDR support here - } - } - - debug!("Using default pixel format: Xrgb8888"); - wl_shm::Format::Xrgb8888 - }) - } -} - - fn main() -> Result<(), ()> { run().map_err(|e| { error!("{e:#}"); }) } @@ -122,12 +102,45 @@ fn run() -> anyhow::Result<()> { let compositor_state = CompositorState::bind(&globals, &qh).unwrap(); let layer_shell = LayerShell::bind(&globals, &qh).unwrap(); let shm = Shm::bind(&globals, &qh).unwrap(); + let mut shm_format = wl_shm::Format::Xrgb8888; + if cli.pixelformat != Some(PixelFormat::Baseline) { + // Consume less gpu memory by using Bgr888 if available, + // fall back to the always supported Xrgb8888 otherwise + if shm.formats().contains(&wl_shm::Format::Bgr888) { + shm_format = wl_shm::Format::Bgr888; + } + } + debug!("Using shm format: {shm_format:?}"); let registry_state = RegistryState::new(&globals); let viewporter: WpViewporter = registry_state .bind_one(&qh, 1..=1, ()).expect("wp_viewporter not available"); + let dmabuf_state = DmabufState::new(&globals, &qh); + let mut gpu = None; + if cli.gpu { + if let Some(version) = dmabuf_state.version() { + if version >= 4 { + debug!("Using Linux DMA-BUF version {version}"); + } else { + warn!("Only legacy Linux DMA-BUF version {version} is \ + available from the compositor where it gives no \ + information about which GPU it uses."); + // TODO handle this better by providing cli options + // to choose DRM device by major:minor or /dev path + } + match Gpu::new() { + Ok(val) => gpu = Some(val), + Err(e) => + error!("Failed to set up GPU, disabling GPU use: {e:#}"), + } + } else { + error!("Wayland protocol Linux DMA-BUF is unavailable \ + from the compositor, disabling GPU use"); + } + } + // Sync tools for sway ipc tasks let (tx, rx) = channel(); let waker = Arc::new(Waker::new().unwrap()); @@ -144,15 +157,15 @@ fn run() -> anyhow::Result<()> { layer_shell, viewporter, wallpaper_dir, - force_xrgb8888: cli.pixelformat - .is_some_and(|p| p == PixelFormat::Baseline), - pixel_format: None, + shm_format, background_layers: Vec::new(), compositor_connection_task: ConnectionTask::new( compositor, tx.clone(), Arc::clone(&waker) ), color_transform, + dmabuf_state, + gpu, }; event_queue.roundtrip(&mut state).unwrap(); diff --git a/src/wayland.rs b/src/wayland.rs index 23ee331..25aedbd 100644 --- a/src/wayland.rs +++ b/src/wayland.rs @@ -1,14 +1,18 @@ use std::{ - cell::Cell, + cell::RefCell, + os::fd::AsFd, path::PathBuf, - rc::Rc, + rc::{Rc, Weak}, }; +use anyhow::{bail, Context}; use log::{debug, error, warn}; +use rustix::fs::{Dev, major, minor}; use smithay_client_toolkit::{ - delegate_compositor, delegate_layer, delegate_output, delegate_registry, - delegate_shm, + delegate_compositor, delegate_dmabuf, delegate_layer, delegate_output, + delegate_registry, delegate_shm, compositor::{CompositorHandler, Region}, + dmabuf::{DmabufFeedback, DmabufHandler, DmabufState}, output::{OutputHandler, OutputState}, registry::{ProvidesRegistryState, RegistryState}, registry_handlers, @@ -33,13 +37,23 @@ use smithay_client_toolkit::reexports::client::{ wl_surface::WlSurface, }, }; -use smithay_client_toolkit::reexports::protocols::wp::viewporter::client::{ - wp_viewport::WpViewport, - wp_viewporter::WpViewporter +use smithay_client_toolkit::reexports::protocols::wp::{ + linux_dmabuf::zv1::client::{ + zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, + zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, + }, + viewporter::client::{ + wp_viewport::WpViewport, + wp_viewporter::WpViewporter + } }; use crate::{ State, + gpu::{ + DRM_FORMAT_XRGB8888, fmt_modifier, + GpuMemory, GpuUploader, GpuWallpaper, + }, image::{load_wallpaper, output_wallpaper_files, WallpaperFile}, }; @@ -91,6 +105,119 @@ impl CompositorHandler for State } } +impl DmabufHandler for State { + fn dmabuf_state(&mut self) -> &mut DmabufState { + &mut self.dmabuf_state + } + + fn dmabuf_feedback( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + proxy: &ZwpLinuxDmabufFeedbackV1, + feedback: DmabufFeedback, + ) { + let Some(bg_layer_pos) = self.background_layers.iter() + .position(|bg_layer| + bg_layer.dmabuf_feedback.as_ref() == Some(proxy) + ) + else { + error!("Received unexpected Linux DMA-BUF feedback"); + return + }; + if let Err(e) = handle_dmabuf_feedback( + self, + qh, + feedback, + bg_layer_pos + ) { + error!("Failed to proceed with DMA-BUF feedback, \ + falling back to shm: {e:#}"); + fallback_shm_load_wallpapers(self, qh, bg_layer_pos); + } + } + + fn created( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + params: &ZwpLinuxBufferParamsV1, + buffer: WlBuffer, + ) { + for bg_layer in self.background_layers.iter_mut() { + for workspace_bg in bg_layer.workspace_backgrounds.iter_mut() { + let wallpaper = &workspace_bg.wallpaper; + let mut wallpaper_borrow = wallpaper.borrow_mut(); + if wallpaper_borrow.memory.dmabuf_params_destroy_eq(params) { + wallpaper_borrow.wl_buffer = Some(buffer); + debug!("Created Linux DMA-BUF buffer for wallpaper \ + file {:?}", wallpaper_borrow.canon_path); + drop(wallpaper_borrow); + if let Some(queued_weak) = &bg_layer.queued_wallpaper { + if let Some(queued) = queued_weak.upgrade() { + if Rc::ptr_eq(&queued, wallpaper) { + let name = workspace_bg.workspace_name.clone(); + bg_layer.draw_workspace_bg(&name); + } + } + } + return + } + } + } + error!("Received unexpected created Linux DMA-BUF buffer"); + } + + fn failed( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + params: &ZwpLinuxBufferParamsV1, + ) { + error!("Failed to create a Linux DMA-BUF buffer"); + let mut failed_bg_layer_indecies = Vec::new(); + for (i, bg_layer) in self.background_layers.iter_mut().enumerate() { + for workspace_bg in bg_layer.workspace_backgrounds.iter_mut() { + let mut wallpaper = workspace_bg.wallpaper.borrow_mut(); + if wallpaper.memory.dmabuf_params_destroy_eq(params) { + error!("Falling back to shm and reloading wallpapers \ + for output {}", bg_layer.output_name); + failed_bg_layer_indecies.push(i); + break + } + } + } + for index in failed_bg_layer_indecies { + fallback_shm_load_wallpapers(self, qh, index); + } + } + + fn released( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + buffer: &WlBuffer + ) { + for bg in self.background_layers.iter_mut() + .flat_map(|bg_layer| &mut bg_layer.workspace_backgrounds) + { + let mut wallpaper = bg.wallpaper.borrow_mut(); + if wallpaper.wl_buffer.as_ref() == Some(buffer) { + if let Some(new_count) = wallpaper.active_count.checked_sub(1) { + debug!("Compositor released the DMA-BUF wl_buffer of {:?}", + wallpaper.canon_path); + wallpaper.active_count = new_count; + } else { + error!("Unexpected release event for the DMA-BUF \ + wl_buffer of {:?}", wallpaper.canon_path); + } + return + } + } + warn!("Release event for already destroyed DMA-BUF wl_buffer"); + } +} + impl LayerShellHandler for State { fn closed( @@ -272,131 +399,63 @@ logical size: {}x{}, transform: {:?}", layer.commit(); - let pixel_format = self.pixel_format(); - let output_dir = self.wallpaper_dir.join(&output_name); - debug!("Looking for wallpapers for new output {} in {:?}", - output_name, output_dir); - let wallpaper_files = match output_wallpaper_files(&output_dir) { - Ok(wallpaper_files) => wallpaper_files, - Err(e) => { - error!("Failed to get wallpapers for new output {output_name} \ - form {output_dir:?}: {e:#}"); - return - } - }; - let mut workspace_backgrounds = Vec::new(); - let mut resizer = fast_image_resize::Resizer::new(); - let mut reused_count = 0usize; - let mut loaded_count = 0usize; - let mut error_count = 0usize; - for wallpaper_file in wallpaper_files { - if log::log_enabled!(log::Level::Debug) { - if wallpaper_file.path == wallpaper_file.canon_path { - debug!("Wallpaper file {:?} for workspace {}", - wallpaper_file.path, wallpaper_file.workspace); - } else { - debug!("Wallpaper file {:?} -> {:?} for workspace {}", - wallpaper_file.path, wallpaper_file.canon_path, - wallpaper_file.workspace); + let mut dmabuf_feedback = None; + let mut gpu_uploader = None; + if let Some(gpu) = self.gpu.as_mut() { + if self.dmabuf_state.version().unwrap() >= 4 { + match self.dmabuf_state.get_surface_feedback(surface, qh) { + Ok(feedback) => { + debug!("Requesting Linux DMA-BUF surface feedback \ + for output {}", output_name); + dmabuf_feedback = Some(feedback); + }, + Err(e) => { + error!("Failed to request Linux DMA-BUF surface \ + feedback for the surface on output {}: {}", + output_name, e); + }, } + } else { + let drm_format_modifiers = self.dmabuf_state.modifiers().iter() + .filter(|dmabuf_format| + dmabuf_format.format == DRM_FORMAT_XRGB8888 + ) + .map(|dmabuf_format| dmabuf_format.modifier) + .collect::>(); + match gpu.uploader( + None, + width as u32, + height as u32, + drm_format_modifiers, + ) { + Ok(uploader) => gpu_uploader = Some(uploader), + Err(e) => error!("Failed to obtain GPU uploader: {e:#}"), + }; } - if let Some(wallpaper) = find_equal_output_wallpaper( - &workspace_backgrounds, - &wallpaper_file - ) { - workspace_backgrounds.push(WorkspaceBackground { - workspace_name: wallpaper_file.workspace, - wallpaper - }); - reused_count += 1; - continue - } - if let Some(wallpaper) = find_equal_wallpaper( - &self.background_layers, - width, - height, - info.transform, - &wallpaper_file - ) { - workspace_backgrounds.push(WorkspaceBackground { - workspace_name: wallpaper_file.workspace, - wallpaper - }); - reused_count += 1; - continue - } - let stride = match pixel_format { - wl_shm::Format::Xrgb8888 => width as usize * 4, - wl_shm::Format::Bgr888 => { - // Align buffer stride to both 4 and pixel format - // block size. Not being aligned to 4 caused - // https://github.com/gergo-salyi/multibg-sway/issues/6 - (width as usize * 3).next_multiple_of(4) - }, - _ => unreachable!() - }; - let shm_size = stride * height as usize; - let mut shm_pool = match RawPool::new(shm_size, &self.shm) { - Ok(shm_pool) => shm_pool, - Err(e) => { - error!("Failed to create shm pool: {e}"); - error_count += 1; - continue - } - }; - if let Err(e) = load_wallpaper( - &wallpaper_file.path, - &mut shm_pool.mmap()[..shm_size], - width as u32, - height as u32, - stride, - pixel_format, - self.color_transform, - &mut resizer - ) { - error!("Failed to load wallpaper: {e:#}"); - error_count += 1; - continue - } - let wl_buffer = shm_pool.create_buffer( - 0, - width, - height, - stride.try_into().unwrap(), - pixel_format, - (), - qh - ); - workspace_backgrounds.push(WorkspaceBackground { - workspace_name: wallpaper_file.workspace, - wallpaper: Rc::new(Wallpaper { - wl_buffer, - active_count: Cell::new(0), - shm_pool, - canon_path: wallpaper_file.canon_path, - canon_modified: wallpaper_file.canon_modified, - }) - }); - loaded_count += 1; } - debug!("Wallpapers for new output: {} reused, {} loaded, {} errors", - reused_count, loaded_count, error_count); - debug!("Wallpapers are available for workspaces: {}", - workspace_backgrounds.iter() - .map(|bg| bg.workspace_name.as_str()) - .collect::>().join(", ")); + let is_dmabuf_feedback = dmabuf_feedback.is_some(); + let bg_layer_index = self.background_layers.len(); self.background_layers.push(BackgroundLayer { output_name, width, height, layer, configured: false, - workspace_backgrounds, - current_workspace: None, + workspace_backgrounds: Vec::new(), + current_wallpaper: None, + queued_wallpaper: None, transform: info.transform, viewport, + dmabuf_feedback, }); - print_memory_stats(&self.background_layers); + if !is_dmabuf_feedback { + load_wallpapers( + self, + qh, + bg_layer_index, + gpu_uploader, + ); + } } fn update_output( @@ -601,6 +660,7 @@ impl ShmHandler for State { } delegate_compositor!(State); +delegate_dmabuf!(State); delegate_layer!(State); delegate_output!(State); delegate_registry!(State); @@ -641,20 +701,20 @@ impl Dispatch for State { _conn: &Connection, _qhandle: &QueueHandle, ) { - for bg_layer in state.background_layers.iter_mut() { - for bg in bg_layer.workspace_backgrounds.iter_mut() { - if bg.wallpaper.wl_buffer == *proxy { - let active_count = bg.wallpaper.active_count.get(); - if let Some(new_count) = active_count.checked_sub(1) { - debug!("Compositor released the wl_shm wl_buffer \ - of {:?}", bg.wallpaper.canon_path); - bg.wallpaper.active_count.set(new_count); - } else { - error!("Unexpected release event for the wl_shm \ - wl_buffer of {:?}", bg.wallpaper.canon_path); - } - return + for bg in state.background_layers.iter_mut() + .flat_map(|bg_layer| &mut bg_layer.workspace_backgrounds) + { + let mut wallpaper = bg.wallpaper.borrow_mut(); + if wallpaper.wl_buffer.as_ref() == Some(proxy) { + if let Some(new_count) = wallpaper.active_count.checked_sub(1) { + debug!("Compositor released the wl_shm wl_buffer of {:?}", + wallpaper.canon_path); + wallpaper.active_count = new_count; + } else { + error!("Unexpected release event for the wl_shm \ + wl_buffer of {:?}", wallpaper.canon_path); } + return } } warn!("Release event for already destroyed wl_shm wl_buffer"); @@ -668,10 +728,13 @@ pub struct BackgroundLayer { pub layer: LayerSurface, pub configured: bool, pub workspace_backgrounds: Vec, - pub current_workspace: Option, + pub current_wallpaper: Option>>, + pub queued_wallpaper: Option>>, pub transform: Transform, pub viewport: Option, + pub dmabuf_feedback: Option, } + impl BackgroundLayer { pub fn draw_workspace_bg(&mut self, workspace_name: &str) @@ -684,12 +747,6 @@ impl BackgroundLayer return; } - if self.current_workspace.as_deref() == Some(workspace_name) { - debug!("Skipping draw on output {} for workspace {} because its \ - wallpaper is already set", self.output_name, workspace_name); - return - } - let Some(workspace_bg) = self.workspace_backgrounds.iter() .find(|workspace_bg| workspace_bg.workspace_name == workspace_name) .or_else(|| self.workspace_backgrounds.iter() @@ -706,19 +763,36 @@ impl BackgroundLayer ); return; }; + let wallpaper = &workspace_bg.wallpaper; + + if let Some(current) = &self.current_wallpaper { + if Rc::ptr_eq(current, wallpaper) { + debug!("Skipping draw on output {} for workspace {} \ + because its wallpaper is already set", + self.output_name, workspace_name); + return + } + } + + let mut wallpaper_borrow = wallpaper.borrow_mut(); + let Some(wl_buffer) = wallpaper_borrow.wl_buffer.as_ref() else { + debug!("Wallpaper for output {} workspace {} is not ready yet", + self.output_name, workspace_name); + self.queued_wallpaper = Some(Rc::downgrade(wallpaper)); + return + }; // Attach and commit to new workspace background - self.layer.attach(Some(&workspace_bg.wallpaper.wl_buffer), 0, 0); - workspace_bg.wallpaper.active_count.set( - workspace_bg.wallpaper.active_count.get() + 1 - ); + self.layer.attach(Some(wl_buffer), 0, 0); + wallpaper_borrow.active_count += 1; // Damage the entire surface self.layer.wl_surface().damage_buffer(0, 0, self.width, self.height); self.layer.commit(); - self.current_workspace = Some(workspace_name.to_string()); + self.current_wallpaper = Some(Rc::clone(wallpaper)); + self.queued_wallpaper = None; debug!( "Setting wallpaper on output '{}' for workspace: {}", @@ -729,24 +803,65 @@ impl BackgroundLayer pub struct WorkspaceBackground { pub workspace_name: String, - pub wallpaper: Rc, + pub wallpaper: Rc>, } pub struct Wallpaper { - pub wl_buffer: WlBuffer, - pub active_count: Cell, - pub shm_pool: RawPool, + pub wl_buffer: Option, + pub active_count: usize, + pub memory: Memory, pub canon_path: PathBuf, pub canon_modified: u128, } impl Drop for Wallpaper { fn drop(&mut self) { - if self.active_count.get() != 0 { - warn!("Destroying a {} times active wl_buffer of wallpaper {:?}", - self.active_count.get(), self.canon_path); + if let Some(wl_buffer) = &self.wl_buffer { + if self.active_count != 0 { + warn!("Destroying a {} times active wl_buffer of \ + wallpaper {:?}", self.active_count, self.canon_path); + } + wl_buffer.destroy(); } - self.wl_buffer.destroy(); + } +} + +pub enum Memory { + WlShm { pool: RawPool }, + Dmabuf { gpu_memory: GpuMemory, params: Option }, +} + +impl Memory { + fn gpu_uploader_eq(&self, gpu_uploader: Option<&GpuUploader>) -> bool { + if let Some(gpu_uploader) = gpu_uploader { + match self { + Memory::WlShm { .. } => false, + Memory::Dmabuf { gpu_memory, .. } => { + gpu_memory.gpu_uploader_eq(gpu_uploader) + }, + } + } else { + match self { + Memory::WlShm { .. } => true, + Memory::Dmabuf { .. } => false, + } + } + } + + fn dmabuf_params_destroy_eq( + &mut self, + other_params: &ZwpLinuxBufferParamsV1 + ) -> bool { + if let Memory::Dmabuf { params: params_option, .. } = self { + if let Some(params) = params_option { + if params == other_params { + params.destroy(); + *params_option = None; + return true + } + } + } + false } } @@ -759,16 +874,19 @@ fn find_equal_wallpaper( width: i32, height: i32, transform: Transform, - wallpaper_file: &WallpaperFile -) -> Option> { + wallpaper_file: &WallpaperFile, + gpu_uploader: Option<&GpuUploader>, +) -> Option>> { for bg_layer in background_layers { if bg_layer.width == width && bg_layer.height == height && bg_layer.transform == transform { for bg in &bg_layer.workspace_backgrounds { - if bg.wallpaper.canon_modified == wallpaper_file.canon_modified - && bg.wallpaper.canon_path == wallpaper_file.canon_path + let wallpaper = bg.wallpaper.borrow(); + if wallpaper.canon_modified == wallpaper_file.canon_modified + && wallpaper.canon_path == wallpaper_file.canon_path + && wallpaper.memory.gpu_uploader_eq(gpu_uploader) { debug!("Reusing the wallpaper of output {} workspace {}", bg_layer.output_name, bg.workspace_name); @@ -782,11 +900,14 @@ fn find_equal_wallpaper( fn find_equal_output_wallpaper( workspace_backgrounds: &[WorkspaceBackground], - wallpaper_file: &WallpaperFile -) -> Option> { + wallpaper_file: &WallpaperFile, + gpu_uploader: Option<&GpuUploader>, +) -> Option>> { for bg in workspace_backgrounds { - if bg.wallpaper.canon_modified == wallpaper_file.canon_modified - && bg.wallpaper.canon_path == wallpaper_file.canon_path + let wallpaper = bg.wallpaper.borrow(); + if wallpaper.canon_modified == wallpaper_file.canon_modified + && wallpaper.canon_path == wallpaper_file.canon_path + && wallpaper.memory.gpu_uploader_eq(gpu_uploader) { debug!("Reusing the wallpaper of workspace {}", bg.workspace_name); @@ -800,15 +921,340 @@ fn print_memory_stats(background_layers: &[BackgroundLayer]) { if log::log_enabled!(log::Level::Debug) { let mut wl_shm_count = 0.0f32; let mut wl_shm_size = 0.0f32; + let mut dmabuf_count = 0.0f32; + let mut dmabuf_size = 0.0f32; for bg_layer in background_layers { for bg in &bg_layer.workspace_backgrounds { let factor = 1.0 / Rc::strong_count(&bg.wallpaper) as f32; - wl_shm_count += factor; - wl_shm_size += factor * bg.wallpaper.shm_pool.len() as f32; + match &bg.wallpaper.borrow().memory { + Memory::WlShm { pool } => { + wl_shm_count += factor; + wl_shm_size += factor * pool.len() as f32; + }, + Memory::Dmabuf { gpu_memory, .. } => { + dmabuf_count += factor; + dmabuf_size += factor * gpu_memory.size() as f32; + }, + } } } - let count = (wl_shm_count + 0.5) as usize; - let size_kb = (wl_shm_size + 0.5) as usize / 1024; - debug!("Memory use: {size_kb} KiB from {count} wl_shm pools"); + let wl_shm_count = (wl_shm_count + 0.5) as usize; + let wl_shm_size_kb = (wl_shm_size + 0.5) as usize / 1024; + let dmabuf_count = (dmabuf_count + 0.5) as usize; + let dmabuf_size_kb = (dmabuf_size + 0.5) as usize / 1024; + debug!("Memory use: {wl_shm_size_kb} KiB from {wl_shm_count} wl_shm \ + pools, {dmabuf_size_kb} KiB from {dmabuf_count} DMA-BUFs"); } } + +fn fallback_shm_load_wallpapers( + state: &mut State, + qh: &QueueHandle, + bg_layer_index: usize, +) { + let bg_layer = &mut state.background_layers[bg_layer_index]; + bg_layer.dmabuf_feedback = None; + bg_layer.workspace_backgrounds.clear(); + load_wallpapers( + state, + qh, + bg_layer_index, + None, + ); +} + +fn load_wallpapers( + state: &mut State, + qh: &QueueHandle, + bg_layer_index: usize, + mut gpu_uploader: Option, +) { + let bg_layer = &state.background_layers[bg_layer_index]; + let output_name = bg_layer.output_name.as_str(); + let width = bg_layer.width; + let height = bg_layer.height; + let transform = bg_layer.transform; + let output_dir = state.wallpaper_dir.join(output_name); + debug!("Looking for wallpapers for new output {} in {:?}", + output_name, output_dir); + let wallpaper_files = match output_wallpaper_files(&output_dir) { + Ok(wallpaper_files) => wallpaper_files, + Err(e) => { + error!("Failed to get wallpapers for new output {output_name} \ + form {output_dir:?}: {e:#}"); + return + } + }; + let shm_stride = match state.shm_format { + wl_shm::Format::Xrgb8888 => width as usize * 4, + wl_shm::Format::Bgr888 => { + // Align buffer stride to both 4 and pixel format + // block size. Not being aligned to 4 caused + // https://github.com/gergo-salyi/multibg-sway/issues/6 + (width as usize * 3).next_multiple_of(4) + }, + _ => unreachable!() + }; + let shm_size = shm_stride * height as usize; + let mut workspace_backgrounds = Vec::new(); + let mut resizer = fast_image_resize::Resizer::new(); + let mut reused_count = 0usize; + let mut loaded_count = 0usize; + let mut error_count = 0usize; + for wallpaper_file in wallpaper_files { + if log::log_enabled!(log::Level::Debug) { + if wallpaper_file.path == wallpaper_file.canon_path { + debug!("Wallpaper file {:?} for workspace {}", + wallpaper_file.path, wallpaper_file.workspace); + } else { + debug!("Wallpaper file {:?} -> {:?} for workspace {}", + wallpaper_file.path, wallpaper_file.canon_path, + wallpaper_file.workspace); + } + } + if let Some(wallpaper) = find_equal_output_wallpaper( + &workspace_backgrounds, + &wallpaper_file, + gpu_uploader.as_ref(), + ) { + workspace_backgrounds.push(WorkspaceBackground { + workspace_name: wallpaper_file.workspace, + wallpaper, + }); + reused_count += 1; + continue + } + if let Some(wallpaper) = find_equal_wallpaper( + &state.background_layers, + width, + height, + transform, + &wallpaper_file, + gpu_uploader.as_ref(), + ) { + workspace_backgrounds.push(WorkspaceBackground { + workspace_name: wallpaper_file.workspace, + wallpaper, + }); + reused_count += 1; + continue + } + if let Some(uploader) = gpu_uploader.as_mut() { + if let Err(e) = load_wallpaper( + &wallpaper_file.path, + uploader.staging_buffer(), + width as u32, + height as u32, + width as usize * 4, + wl_shm::Format::Xrgb8888, + state.color_transform, + &mut resizer + ) { + error!("Failed to load wallpaper: {e:#}"); + error_count += 1; + continue + } + match uploader.upload() { + Ok(gpu_wallpaper) => { + let wallpaper = wallpaper_dmabuf( + &state.dmabuf_state, + qh, + gpu_wallpaper, + width, + height, + wallpaper_file.canon_path, + wallpaper_file.canon_modified, + ); + workspace_backgrounds.push(WorkspaceBackground { + workspace_name: wallpaper_file.workspace, + wallpaper, + }); + loaded_count += 1; + continue + }, + Err(e) => { + error!("Failed to upload wallpaper to GPU: {e:#}"); + gpu_uploader = None; + // fall back to shm + } + } + } + let mut shm_pool = match RawPool::new(shm_size, &state.shm) { + Ok(shm_pool) => shm_pool, + Err(e) => { + error!("Failed to create shm pool: {e}"); + error_count += 1; + continue + } + }; + if let Err(e) = load_wallpaper( + &wallpaper_file.path, + shm_pool.mmap(), + width as u32, + height as u32, + shm_stride, + state.shm_format, + state.color_transform, + &mut resizer + ) { + error!("Failed to load wallpaper: {e:#}"); + error_count += 1; + continue + } + let wl_buffer = shm_pool.create_buffer( + 0, + width, + height, + shm_stride.try_into().unwrap(), + state.shm_format, + (), + qh + ); + workspace_backgrounds.push(WorkspaceBackground { + workspace_name: wallpaper_file.workspace, + wallpaper: Rc::new(RefCell::new(Wallpaper { + wl_buffer: Some(wl_buffer), + active_count: 0, + memory: Memory::WlShm { pool: shm_pool }, + canon_path: wallpaper_file.canon_path, + canon_modified: wallpaper_file.canon_modified, + })), + }); + loaded_count += 1; + } + debug!("Wallpapers for new output: {} reused, {} loaded, {} errors", + reused_count, loaded_count, error_count); + debug!("Wallpapers are available for workspaces: {}", + workspace_backgrounds.iter() + .map(|bg| bg.workspace_name.as_str()) + .collect::>().join(", ")); + state.background_layers[bg_layer_index].workspace_backgrounds = + workspace_backgrounds; + print_memory_stats(&state.background_layers); +} + +fn handle_dmabuf_feedback( + state: &mut State, + qh: &QueueHandle, + feedback: DmabufFeedback, + bg_layer_pos: usize, +) -> anyhow::Result<()> { + let bg_layer = &mut state.background_layers[bg_layer_pos]; + let main_dev = feedback.main_device(); + let format_table = feedback.format_table(); + let tranches = feedback.tranches(); + debug!("Linux DMA-BUF feedback for output {}, main device {}:{}, \ + {} format table entries, {} tranches", &bg_layer.output_name, + major(main_dev), minor(main_dev), + format_table.len(), tranches.len()); + if tranches.is_empty() { + bail!("Linux DMA-BUF feedback has 0 tranches"); + } + let mut selected = None; + for (index, tranche) in tranches.iter().enumerate() { + let target_dev = tranche.device; + debug!("Tranche {index} target device {}:{}", + major(target_dev), minor(target_dev)); + if selected.is_none() && target_dev == main_dev { + selected = Some((index, tranche.formats.as_slice())); + } + } + let Some((index, formats)) = selected else { + bail!("No tranche has the main device as target device"); + }; + debug!("Selected tranche {}, it has {} dmabuf formats", + index, formats.len()); + let mut drm_format_modifiers = Vec::new(); + for index in formats { + let Some(dmabuf_format) = format_table.get(*index as usize) else { + error!("Format index {index} is out of bounds"); + continue + }; + if dmabuf_format.format == DRM_FORMAT_XRGB8888 { + drm_format_modifiers.push(dmabuf_format.modifier); + } + } + if drm_format_modifiers.is_empty() { + bail!("Selected tranche has no modifiers for DRM_FORMAT_XRGB8888"); + } + debug!("Modifiers for DRM_FORMAT_XRGB8888: {}", + drm_format_modifiers.iter() + .map(|&modifier| fmt_modifier(modifier)) + .collect::>().join(", ")); + let dmabuf_drm_dev = Some(main_dev as Dev); + if !bg_layer.workspace_backgrounds.is_empty() + && bg_layer.workspace_backgrounds.iter().all(|bg| { + let memory = &bg.wallpaper.borrow().memory; + if let Memory::Dmabuf { gpu_memory, .. } = memory { + gpu_memory.dmabuf_feedback_eq( + dmabuf_drm_dev, + &drm_format_modifiers + ) + } else { + false + } + }) + { + debug!("Ignoring DMA-BUF feedback with no changes"); + return Ok(()) + } + let gpu_uploader = state.gpu.as_mut().unwrap().uploader( + dmabuf_drm_dev, + bg_layer.width as u32, + bg_layer.height as u32, + drm_format_modifiers + ).context("Failed to create GPU uploader")?; + if !bg_layer.workspace_backgrounds.is_empty() { + debug!("DMA-BUF feedback changed, reloading wallpapers"); + bg_layer.workspace_backgrounds.clear(); + } + load_wallpapers( + state, + qh, + bg_layer_pos, + Some(gpu_uploader), + ); + Ok(()) +} + +fn wallpaper_dmabuf( + dmabuf_state: &DmabufState, + qh: &QueueHandle, + gpu_wallpaper: GpuWallpaper, + width: i32, + height: i32, + canon_path: PathBuf, + canon_modified: u128, +) -> Rc> { + let GpuWallpaper { + drm_format_modifier, + memory_planes_len, + memory_planes, + gpu_memory, + fd, + } = gpu_wallpaper; + let dmabuf_params = dmabuf_state.create_params(qh).unwrap(); + #[allow(clippy::needless_range_loop)] + for memory_plane_index in 0..memory_planes_len { + dmabuf_params.add( + fd.as_fd(), + memory_plane_index as u32, + memory_planes[memory_plane_index].offset as u32, + memory_planes[memory_plane_index].stride as u32, + drm_format_modifier, + ); + } + let params = dmabuf_params.create( + width, + height, + DRM_FORMAT_XRGB8888, + zwp_linux_buffer_params_v1::Flags::empty(), + ); + Rc::new(RefCell::new(Wallpaper { + wl_buffer: None, + active_count: 0, + memory: Memory::Dmabuf { gpu_memory, params: Some(params) }, + canon_path, + canon_modified, + })) +}