Add implementation to upload and serve wallpapers from GPU memory using Vulkan

Add handling for the Linux DMA-BUF Wayland protocol
Refactor Wayland code for image loading and image equivalence
Add Vulkan code using the ash crate dependency targeting Vulkan 1.1
For image DMA-BUF exporting use EXT_image_drm_format_modifier if available,
fall back to linear images otherwise
Add --gpu command line option
Add Vulkan packages as optional dependencies to Arch Linux PKGBUILD
This commit is contained in:
Gergő Sályi 2025-04-21 17:46:48 +02:00
parent 3ded435028
commit 5fd6cbe114
11 changed files with 2094 additions and 205 deletions

27
Cargo.lock generated
View file

@ -85,6 +85,15 @@ version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.4.0" version = "1.4.0"
@ -552,6 +561,16 @@ version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 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]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.15" version = "0.4.15"
@ -614,6 +633,7 @@ name = "multibg-sway"
version = "0.1.10" version = "0.1.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ash",
"clap", "clap",
"env_logger", "env_logger",
"fast_image_resize", "fast_image_resize",
@ -622,6 +642,7 @@ dependencies = [
"log", "log",
"niri-ipc", "niri-ipc",
"rustix", "rustix",
"scopeguard",
"serde", "serde",
"serde_json", "serde_json",
"smithay-client-toolkit", "smithay-client-toolkit",
@ -828,6 +849,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.219" version = "1.0.219"

View file

@ -15,25 +15,25 @@ exclude = ["/PKGBUILD", "/scripts/"]
[dependencies] [dependencies]
anyhow = "1.0.97" anyhow = "1.0.97"
ash = "0.38.0"
clap = { version = "4.5.3", features = ["derive"] } clap = { version = "4.5.3", features = ["derive"] }
env_logger = "0.11.3" env_logger = "0.11.3"
fast_image_resize = "5.0.0" fast_image_resize = "5.0.0"
libc = "0.2.171" libc = "0.2.171"
log = "0.4.21" 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 = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.140"
smithay-client-toolkit = { version = "0.19.2", default-features = false }
swayipc = "3.0.2" swayipc = "3.0.2"
niri-ipc = "=25.2.0"
[dependencies.image] [dependencies.image]
version = "0.25.6" version = "0.25.6"
default-features = false default-features = false
features = ["bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp"] 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] [features]
default = ["avif"]
avif = ["image/avif-native"] avif = ["image/avif-native"]

View file

@ -12,6 +12,8 @@ optdepends=(
'hyprland: supported window manager to set the wallpapers with' 'hyprland: supported window manager to set the wallpapers with'
'niri: 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' '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") source=("$pkgname-$pkgver.tar.gz::https://static.crates.io/crates/$pkgname/$pkgname-$pkgver.crate")
sha256sums=('2b087124ea07635e53d411e707f7d22f73c69b40f3986a42c841f9cc19fc2d51') sha256sums=('2b087124ea07635e53d411e707f7d22f73c69b40f3986a42c841f9cc19fc2d51')

View file

@ -90,6 +90,9 @@ pub struct Cli {
/// Wayland compositor to connect (autodetect by default) /// Wayland compositor to connect (autodetect by default)
#[arg(long)] #[arg(long)]
pub compositor: Option<crate::compositors::Compositor>, pub compositor: Option<crate::compositors::Compositor>,
/// upload and serve wallpapers from GPU memory using Vulkan
#[arg(long)]
pub gpu: bool,
/// directory with: wallpaper_dir/output/workspace_name.{jpg|png|...} /// directory with: wallpaper_dir/output/workspace_name.{jpg|png|...}
pub wallpaper_dir: String, pub wallpaper_dir: String,
} }

302
src/gpu.rs Normal file
View file

@ -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<GpuInstance>,
devices: Vec<Weak<GpuDevice>>,
}
impl Gpu {
pub fn new() -> anyhow::Result<Gpu> {
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<Dev>,
width: u32,
height: u32,
drm_format_modifiers: Vec<u64>,
) -> anyhow::Result<GpuUploader> {
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<Dev>
) -> Option<Rc<GpuDevice>> {
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<GpuInstance>,
physdev: PhysicalDevice,
primary_drm_dev: Option<Dev>,
render_drm_dev: Option<Dev>,
dmabuf_drm_dev: Option<Dev>,
memory_props: PhysicalDeviceMemoryProperties,
drm_format_props: Option<Vec<DrmFormatModifierPropertiesEXT>>,
device: Device,
external_memory_fd_device: external_memory_fd::Device,
image_drm_format_modifier_device: Option<image_drm_format_modifier::Device>,
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<Dev>) -> 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<GpuDevice>,
buffer: Buffer,
memory: DeviceMemory,
ptr: *mut u8,
len: usize,
extent: Extent2D,
drm_format_modifiers: Vec<u64>,
}
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<GpuWallpaper> {
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<GpuDevice>,
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<Dev>,
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}")
}

420
src/gpu/device.rs Normal file
View file

@ -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<Dev>,
render: Option<Dev>,
dmabuf_dev: Option<Dev>,
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<GpuInstance>,
dmabuf_drm_dev: Option<Dev>,
) -> anyhow::Result<GpuDevice> {
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::<Vec<_>>();
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<Dev>,
physdev: PhysicalDevice,
) -> Option<PhysdevInfo> {
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<GpuInstance>,
physdev_info: PhysdevInfo,
) -> anyhow::Result<GpuDevice> {
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<GpuInstance>,
physdev_info: PhysdevInfo,
) -> anyhow::Result<GpuDevice> {
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<ExtensionProperties>,
enabled: Vec<*const c_char>,
}
impl Extensions {
fn new(props: Vec<ExtensionProperties>) -> 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<Dev>) -> 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<DrmFormatModifierPropertiesEXT> {
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?

232
src/gpu/instance.rs Normal file
View file

@ -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<GpuInstance> {
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
}

441
src/gpu/memory.rs Normal file
View file

@ -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<GpuDevice>,
width: u32,
height: u32,
drm_format_modifiers: Vec<u64>,
) -> anyhow::Result<GpuUploader> {
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::<Vec<_>>().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<GpuWallpaper> {
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<u32> {
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 _)
}

View file

@ -73,7 +73,7 @@ pub fn output_wallpaper_files(
pub fn load_wallpaper( pub fn load_wallpaper(
path: &Path, path: &Path,
dst: &mut [u8], buffer: &mut [u8],
surface_width: u32, surface_width: u32,
surface_height: u32, surface_height: u32,
surface_stride: usize, surface_stride: usize,
@ -81,6 +81,11 @@ pub fn load_wallpaper(
color_transform: ColorTransform, color_transform: ColorTransform,
resizer: &mut Resizer, resizer: &mut Resizer,
) -> anyhow::Result<()> { ) -> 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) let reader = ImageReader::open(path)
.context("Failed to open image file")? .context("Failed to open image file")?
.with_guessed_format() .with_guessed_format()
@ -101,7 +106,6 @@ pub fn load_wallpaper(
if image_width == 0 || image_height == 0 || image_size > isize::MAX as u64 { if image_width == 0 || image_height == 0 || image_size > isize::MAX as u64 {
bail!("Image has invalid dimensions {image_width}x{image_height}") 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:?}"); debug!("Image {image_width}x{image_height} {image_color_type:?}");
if image_color_type.has_alpha() { if image_color_type.has_alpha() {
warn!("Image has alpha channel which will be ignored"); warn!("Image has alpha channel which will be ignored");
@ -120,8 +124,7 @@ pub fn load_wallpaper(
&& surface_row_len == surface_stride && surface_row_len == surface_stride
{ {
debug!("Decoding image directly to destination buffer"); debug!("Decoding image directly to destination buffer");
decoder.read_image(&mut dst[..image_size]) decoder.read_image(dst).context("Failed to decode image")?;
.context("Failed to decode image")?;
return Ok(()); return Ok(());
} }
let mut image = DynamicImage::from_decoder(decoder) let mut image = DynamicImage::from_decoder(decoder)

View file

@ -1,5 +1,6 @@
mod compositors; mod compositors;
mod cli; mod cli;
mod gpu;
mod image; mod image;
mod poll; mod poll;
mod signal; mod signal;
@ -23,6 +24,7 @@ use rustix::{
}; };
use smithay_client_toolkit::{ use smithay_client_toolkit::{
compositor::CompositorState, compositor::CompositorState,
dmabuf::DmabufState,
output::OutputState, output::OutputState,
registry::RegistryState, registry::RegistryState,
shell::wlr_layer::LayerShell, shell::wlr_layer::LayerShell,
@ -41,6 +43,7 @@ use crate::{
cli::{Cli, PixelFormat}, cli::{Cli, PixelFormat},
compositors::{Compositor, ConnectionTask, WorkspaceVisible}, compositors::{Compositor, ConnectionTask, WorkspaceVisible},
image::ColorTransform, image::ColorTransform,
gpu::Gpu,
poll::{Poll, Waker}, poll::{Poll, Waker},
signal::SignalPipe, signal::SignalPipe,
wayland::BackgroundLayer, wayland::BackgroundLayer,
@ -54,37 +57,14 @@ pub struct State {
pub layer_shell: LayerShell, pub layer_shell: LayerShell,
pub viewporter: WpViewporter, pub viewporter: WpViewporter,
pub wallpaper_dir: PathBuf, pub wallpaper_dir: PathBuf,
pub force_xrgb8888: bool, pub shm_format: wl_shm::Format,
pub pixel_format: Option<wl_shm::Format>,
pub background_layers: Vec<BackgroundLayer>, pub background_layers: Vec<BackgroundLayer>,
pub compositor_connection_task: ConnectionTask, pub compositor_connection_task: ConnectionTask,
pub color_transform: ColorTransform, pub color_transform: ColorTransform,
pub dmabuf_state: DmabufState,
pub gpu: Option<Gpu>,
} }
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<(), ()> { fn main() -> Result<(), ()> {
run().map_err(|e| { error!("{e:#}"); }) run().map_err(|e| { error!("{e:#}"); })
} }
@ -122,12 +102,45 @@ fn run() -> anyhow::Result<()> {
let compositor_state = CompositorState::bind(&globals, &qh).unwrap(); let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
let layer_shell = LayerShell::bind(&globals, &qh).unwrap(); let layer_shell = LayerShell::bind(&globals, &qh).unwrap();
let shm = Shm::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 registry_state = RegistryState::new(&globals);
let viewporter: WpViewporter = registry_state let viewporter: WpViewporter = registry_state
.bind_one(&qh, 1..=1, ()).expect("wp_viewporter not available"); .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 // Sync tools for sway ipc tasks
let (tx, rx) = channel(); let (tx, rx) = channel();
let waker = Arc::new(Waker::new().unwrap()); let waker = Arc::new(Waker::new().unwrap());
@ -144,15 +157,15 @@ fn run() -> anyhow::Result<()> {
layer_shell, layer_shell,
viewporter, viewporter,
wallpaper_dir, wallpaper_dir,
force_xrgb8888: cli.pixelformat shm_format,
.is_some_and(|p| p == PixelFormat::Baseline),
pixel_format: None,
background_layers: Vec::new(), background_layers: Vec::new(),
compositor_connection_task: ConnectionTask::new( compositor_connection_task: ConnectionTask::new(
compositor, compositor,
tx.clone(), Arc::clone(&waker) tx.clone(), Arc::clone(&waker)
), ),
color_transform, color_transform,
dmabuf_state,
gpu,
}; };
event_queue.roundtrip(&mut state).unwrap(); event_queue.roundtrip(&mut state).unwrap();

View file

@ -1,14 +1,18 @@
use std::{ use std::{
cell::Cell, cell::RefCell,
os::fd::AsFd,
path::PathBuf, path::PathBuf,
rc::Rc, rc::{Rc, Weak},
}; };
use anyhow::{bail, Context};
use log::{debug, error, warn}; use log::{debug, error, warn};
use rustix::fs::{Dev, major, minor};
use smithay_client_toolkit::{ use smithay_client_toolkit::{
delegate_compositor, delegate_layer, delegate_output, delegate_registry, delegate_compositor, delegate_dmabuf, delegate_layer, delegate_output,
delegate_shm, delegate_registry, delegate_shm,
compositor::{CompositorHandler, Region}, compositor::{CompositorHandler, Region},
dmabuf::{DmabufFeedback, DmabufHandler, DmabufState},
output::{OutputHandler, OutputState}, output::{OutputHandler, OutputState},
registry::{ProvidesRegistryState, RegistryState}, registry::{ProvidesRegistryState, RegistryState},
registry_handlers, registry_handlers,
@ -33,13 +37,23 @@ use smithay_client_toolkit::reexports::client::{
wl_surface::WlSurface, wl_surface::WlSurface,
}, },
}; };
use smithay_client_toolkit::reexports::protocols::wp::viewporter::client::{ use smithay_client_toolkit::reexports::protocols::wp::{
wp_viewport::WpViewport, linux_dmabuf::zv1::client::{
wp_viewporter::WpViewporter zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1,
zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1},
},
viewporter::client::{
wp_viewport::WpViewport,
wp_viewporter::WpViewporter
}
}; };
use crate::{ use crate::{
State, State,
gpu::{
DRM_FORMAT_XRGB8888, fmt_modifier,
GpuMemory, GpuUploader, GpuWallpaper,
},
image::{load_wallpaper, output_wallpaper_files, WallpaperFile}, 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<Self>,
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<Self>,
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<Self>,
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<Self>,
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 impl LayerShellHandler for State
{ {
fn closed( fn closed(
@ -272,131 +399,63 @@ logical size: {}x{}, transform: {:?}",
layer.commit(); layer.commit();
let pixel_format = self.pixel_format(); let mut dmabuf_feedback = None;
let output_dir = self.wallpaper_dir.join(&output_name); let mut gpu_uploader = None;
debug!("Looking for wallpapers for new output {} in {:?}", if let Some(gpu) = self.gpu.as_mut() {
output_name, output_dir); if self.dmabuf_state.version().unwrap() >= 4 {
let wallpaper_files = match output_wallpaper_files(&output_dir) { match self.dmabuf_state.get_surface_feedback(surface, qh) {
Ok(wallpaper_files) => wallpaper_files, Ok(feedback) => {
Err(e) => { debug!("Requesting Linux DMA-BUF surface feedback \
error!("Failed to get wallpapers for new output {output_name} \ for output {}", output_name);
form {output_dir:?}: {e:#}"); dmabuf_feedback = Some(feedback);
return },
} Err(e) => {
}; error!("Failed to request Linux DMA-BUF surface \
let mut workspace_backgrounds = Vec::new(); feedback for the surface on output {}: {}",
let mut resizer = fast_image_resize::Resizer::new(); output_name, e);
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);
} }
} 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::<Vec<_>>();
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", let is_dmabuf_feedback = dmabuf_feedback.is_some();
reused_count, loaded_count, error_count); let bg_layer_index = self.background_layers.len();
debug!("Wallpapers are available for workspaces: {}",
workspace_backgrounds.iter()
.map(|bg| bg.workspace_name.as_str())
.collect::<Vec<_>>().join(", "));
self.background_layers.push(BackgroundLayer { self.background_layers.push(BackgroundLayer {
output_name, output_name,
width, width,
height, height,
layer, layer,
configured: false, configured: false,
workspace_backgrounds, workspace_backgrounds: Vec::new(),
current_workspace: None, current_wallpaper: None,
queued_wallpaper: None,
transform: info.transform, transform: info.transform,
viewport, 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( fn update_output(
@ -601,6 +660,7 @@ impl ShmHandler for State {
} }
delegate_compositor!(State); delegate_compositor!(State);
delegate_dmabuf!(State);
delegate_layer!(State); delegate_layer!(State);
delegate_output!(State); delegate_output!(State);
delegate_registry!(State); delegate_registry!(State);
@ -641,20 +701,20 @@ impl Dispatch<WlBuffer, ()> for State {
_conn: &Connection, _conn: &Connection,
_qhandle: &QueueHandle<Self>, _qhandle: &QueueHandle<Self>,
) { ) {
for bg_layer in state.background_layers.iter_mut() { for bg in state.background_layers.iter_mut()
for bg in bg_layer.workspace_backgrounds.iter_mut() { .flat_map(|bg_layer| &mut bg_layer.workspace_backgrounds)
if bg.wallpaper.wl_buffer == *proxy { {
let active_count = bg.wallpaper.active_count.get(); let mut wallpaper = bg.wallpaper.borrow_mut();
if let Some(new_count) = active_count.checked_sub(1) { if wallpaper.wl_buffer.as_ref() == Some(proxy) {
debug!("Compositor released the wl_shm wl_buffer \ if let Some(new_count) = wallpaper.active_count.checked_sub(1) {
of {:?}", bg.wallpaper.canon_path); debug!("Compositor released the wl_shm wl_buffer of {:?}",
bg.wallpaper.active_count.set(new_count); wallpaper.canon_path);
} else { wallpaper.active_count = new_count;
error!("Unexpected release event for the wl_shm \ } else {
wl_buffer of {:?}", bg.wallpaper.canon_path); error!("Unexpected release event for the wl_shm \
} wl_buffer of {:?}", wallpaper.canon_path);
return
} }
return
} }
} }
warn!("Release event for already destroyed wl_shm wl_buffer"); warn!("Release event for already destroyed wl_shm wl_buffer");
@ -668,10 +728,13 @@ pub struct BackgroundLayer {
pub layer: LayerSurface, pub layer: LayerSurface,
pub configured: bool, pub configured: bool,
pub workspace_backgrounds: Vec<WorkspaceBackground>, pub workspace_backgrounds: Vec<WorkspaceBackground>,
pub current_workspace: Option<String>, pub current_wallpaper: Option<Rc<RefCell<Wallpaper>>>,
pub queued_wallpaper: Option<Weak<RefCell<Wallpaper>>>,
pub transform: Transform, pub transform: Transform,
pub viewport: Option<WpViewport>, pub viewport: Option<WpViewport>,
pub dmabuf_feedback: Option<ZwpLinuxDmabufFeedbackV1>,
} }
impl BackgroundLayer impl BackgroundLayer
{ {
pub fn draw_workspace_bg(&mut self, workspace_name: &str) pub fn draw_workspace_bg(&mut self, workspace_name: &str)
@ -684,12 +747,6 @@ impl BackgroundLayer
return; 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() let Some(workspace_bg) = self.workspace_backgrounds.iter()
.find(|workspace_bg| workspace_bg.workspace_name == workspace_name) .find(|workspace_bg| workspace_bg.workspace_name == workspace_name)
.or_else(|| self.workspace_backgrounds.iter() .or_else(|| self.workspace_backgrounds.iter()
@ -706,19 +763,36 @@ impl BackgroundLayer
); );
return; 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 // Attach and commit to new workspace background
self.layer.attach(Some(&workspace_bg.wallpaper.wl_buffer), 0, 0); self.layer.attach(Some(wl_buffer), 0, 0);
workspace_bg.wallpaper.active_count.set( wallpaper_borrow.active_count += 1;
workspace_bg.wallpaper.active_count.get() + 1
);
// Damage the entire surface // Damage the entire surface
self.layer.wl_surface().damage_buffer(0, 0, self.width, self.height); self.layer.wl_surface().damage_buffer(0, 0, self.width, self.height);
self.layer.commit(); self.layer.commit();
self.current_workspace = Some(workspace_name.to_string()); self.current_wallpaper = Some(Rc::clone(wallpaper));
self.queued_wallpaper = None;
debug!( debug!(
"Setting wallpaper on output '{}' for workspace: {}", "Setting wallpaper on output '{}' for workspace: {}",
@ -729,24 +803,65 @@ impl BackgroundLayer
pub struct WorkspaceBackground { pub struct WorkspaceBackground {
pub workspace_name: String, pub workspace_name: String,
pub wallpaper: Rc<Wallpaper>, pub wallpaper: Rc<RefCell<Wallpaper>>,
} }
pub struct Wallpaper { pub struct Wallpaper {
pub wl_buffer: WlBuffer, pub wl_buffer: Option<WlBuffer>,
pub active_count: Cell<usize>, pub active_count: usize,
pub shm_pool: RawPool, pub memory: Memory,
pub canon_path: PathBuf, pub canon_path: PathBuf,
pub canon_modified: u128, pub canon_modified: u128,
} }
impl Drop for Wallpaper { impl Drop for Wallpaper {
fn drop(&mut self) { fn drop(&mut self) {
if self.active_count.get() != 0 { if let Some(wl_buffer) = &self.wl_buffer {
warn!("Destroying a {} times active wl_buffer of wallpaper {:?}", if self.active_count != 0 {
self.active_count.get(), self.canon_path); 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<ZwpLinuxBufferParamsV1> },
}
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, width: i32,
height: i32, height: i32,
transform: Transform, transform: Transform,
wallpaper_file: &WallpaperFile wallpaper_file: &WallpaperFile,
) -> Option<Rc<Wallpaper>> { gpu_uploader: Option<&GpuUploader>,
) -> Option<Rc<RefCell<Wallpaper>>> {
for bg_layer in background_layers { for bg_layer in background_layers {
if bg_layer.width == width if bg_layer.width == width
&& bg_layer.height == height && bg_layer.height == height
&& bg_layer.transform == transform && bg_layer.transform == transform
{ {
for bg in &bg_layer.workspace_backgrounds { for bg in &bg_layer.workspace_backgrounds {
if bg.wallpaper.canon_modified == wallpaper_file.canon_modified let wallpaper = bg.wallpaper.borrow();
&& bg.wallpaper.canon_path == wallpaper_file.canon_path 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 {}", debug!("Reusing the wallpaper of output {} workspace {}",
bg_layer.output_name, bg.workspace_name); bg_layer.output_name, bg.workspace_name);
@ -782,11 +900,14 @@ fn find_equal_wallpaper(
fn find_equal_output_wallpaper( fn find_equal_output_wallpaper(
workspace_backgrounds: &[WorkspaceBackground], workspace_backgrounds: &[WorkspaceBackground],
wallpaper_file: &WallpaperFile wallpaper_file: &WallpaperFile,
) -> Option<Rc<Wallpaper>> { gpu_uploader: Option<&GpuUploader>,
) -> Option<Rc<RefCell<Wallpaper>>> {
for bg in workspace_backgrounds { for bg in workspace_backgrounds {
if bg.wallpaper.canon_modified == wallpaper_file.canon_modified let wallpaper = bg.wallpaper.borrow();
&& bg.wallpaper.canon_path == wallpaper_file.canon_path 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 {}", debug!("Reusing the wallpaper of workspace {}",
bg.workspace_name); bg.workspace_name);
@ -800,15 +921,340 @@ fn print_memory_stats(background_layers: &[BackgroundLayer]) {
if log::log_enabled!(log::Level::Debug) { if log::log_enabled!(log::Level::Debug) {
let mut wl_shm_count = 0.0f32; let mut wl_shm_count = 0.0f32;
let mut wl_shm_size = 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_layer in background_layers {
for bg in &bg_layer.workspace_backgrounds { for bg in &bg_layer.workspace_backgrounds {
let factor = 1.0 / Rc::strong_count(&bg.wallpaper) as f32; let factor = 1.0 / Rc::strong_count(&bg.wallpaper) as f32;
wl_shm_count += factor; match &bg.wallpaper.borrow().memory {
wl_shm_size += factor * bg.wallpaper.shm_pool.len() as f32; 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 wl_shm_count = (wl_shm_count + 0.5) as usize;
let size_kb = (wl_shm_size + 0.5) as usize / 1024; let wl_shm_size_kb = (wl_shm_size + 0.5) as usize / 1024;
debug!("Memory use: {size_kb} KiB from {count} wl_shm pools"); 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<State>,
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<State>,
bg_layer_index: usize,
mut gpu_uploader: Option<GpuUploader>,
) {
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::<Vec<_>>().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<State>,
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::<Vec<_>>().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<State>,
gpu_wallpaper: GpuWallpaper,
width: i32,
height: i32,
canon_path: PathBuf,
canon_modified: u128,
) -> Rc<RefCell<Wallpaper>> {
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,
}))
}