atagen

monolith

public
Star0
Watch0

Commit 824bf02b6e55

tarball

atagen <boss@atagen.co>2026-10-16 09:25 UTC

19 files changed+2154-4
render-vk: offscreen Vulkan renderer behind smithay traits

Frames record operations and submit them in one command buffer at finish, returning a fence as the sync point, which keeps the trait contract while leaving room for batching. Push descriptors and dynamic rendering avoid descriptor pools and render passes entirely. The push constant block puts the colour first because lavapipe in Mesa 26.2 returned the wrong fragment value at offset 48.
Modified file: Cargo.lock+63 additions−2 deletions
@@ -18,6 +18,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1818 checksum = "e149dc73cd30538307e7ffa2acd3d2221148eaeed4871f246657b1c3eaa1cbd2"
1919
2020 [[package]]
21+name = "ash"
22+version = "0.38.0+1.3.281"
23+source = "registry+https://github.com/rust-lang/crates.io-index"
24+checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
25+dependencies = [
26+ "libloading",
27+]
28+
29+[[package]]
2130 name = "autocfg"
2231 version = "1.5.1"
2332 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -259,6 +268,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
259268 checksum = "37bfe73dc8ec21f54d181e5a60554327ec3f1c600c455a75ccbfc65d2106a6c4"
260269
261270 [[package]]
271+name = "gpu-allocator"
272+version = "0.27.0"
273+source = "registry+https://github.com/rust-lang/crates.io-index"
274+checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd"
275+dependencies = [
276+ "ash",
277+ "log",
278+ "presser",
279+ "thiserror 1.0.69",
280+]
281+
282+[[package]]
262283 name = "hashbrown"
263284 version = "0.17.1"
264285 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -293,6 +314,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
293314 checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
294315
295316 [[package]]
317+name = "libloading"
318+version = "0.8.9"
319+source = "registry+https://github.com/rust-lang/crates.io-index"
320+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
321+dependencies = [
322+ "cfg-if",
323+ "windows-link",
324+]
325+
326+[[package]]
296327 name = "linux-raw-sys"
297328 version = "0.12.1"
298329 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -394,7 +425,11 @@ dependencies = [
394425 name = "monolith-render-vk"
395426 version = "0.0.0"
396427 dependencies = [
428+ "ash",
429+ "glam",
430+ "gpu-allocator",
397431 "smithay",
432+ "thiserror 2.0.21",
398433 ]
399434
400435 [[package]]
@@ -461,6 +496,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
461496 checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
462497
463498 [[package]]
499+name = "presser"
500+version = "0.3.1"
501+source = "registry+https://github.com/rust-lang/crates.io-index"
502+checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
503+
504+[[package]]
464505 name = "proc-macro-crate"
465506 version = "3.5.0"
466507 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -719,7 +760,7 @@ dependencies = [
719760 "siphasher",
720761 "smallvec",
721762 "tempfile",
722- "thiserror",
763+ "thiserror 2.0.21",
723764 "tracing",
724765 "wayland-protocols",
725766 "wayland-protocols-misc",
@@ -765,11 +806,31 @@ dependencies = [
765806
766807 [[package]]
767808 name = "thiserror"
809+version = "1.0.69"
810+source = "registry+https://github.com/rust-lang/crates.io-index"
811+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
812+dependencies = [
813+ "thiserror-impl 1.0.69",
814+]
815+
816+[[package]]
817+name = "thiserror"
768818 version = "2.0.21"
769819 source = "registry+https://github.com/rust-lang/crates.io-index"
770820 checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
771821 dependencies = [
772- "thiserror-impl",
822+ "thiserror-impl 2.0.21",
823+]
824+
825+[[package]]
826+name = "thiserror-impl"
827+version = "1.0.69"
828+source = "registry+https://github.com/rust-lang/crates.io-index"
829+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
830+dependencies = [
831+ "proc-macro2",
832+ "quote",
833+ "syn 2.0.119",
773834 ]
774835
775836 [[package]]
Modified file: crates/render-vk/Cargo.toml+4 additions
@@ -9,4 +9,8 @@ publish.workspace = true
99 workspace = true
1010
1111 [dependencies]
12+ash = "0.38"
13+glam = { version = "0.33", default-features = false, features = ["std"] }
14+gpu-allocator = { version = "0.27", default-features = false, features = ["vulkan"] }
1215 smithay.workspace = true
16+thiserror = "2"
Added file: crates/render-vk/build.rs+16 additions
@@ -0,0 +1,16 @@
1+use std::path::PathBuf;
2+use std::process::Command;
3+
4+fn main() {
5+ let out = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR"));
6+ println!("cargo:rerun-if-changed=shaders");
7+ for name in ["quad.vert", "solid.frag", "texture.frag"] {
8+ let status = Command::new("glslangValidator")
9+ .args(["-V", "--target-env", "vulkan1.3", "-o"])
10+ .arg(out.join(format!("{name}.spv")))
11+ .arg(format!("shaders/{name}"))
12+ .status()
13+ .expect("glslangValidator not found, build inside the nix devshell");
14+ assert!(status.success(), "failed to compile shaders/{name}");
15+ }
16+}
Added file: crates/render-vk/shaders/push.glsl+8 additions
@@ -0,0 +1,8 @@
1+// `color` must stay at offset 0. lavapipe in Mesa 26.2 returned the wrong
2+// fragment push constants for a member at offset 48 when `dst` held four
3+// equal values.
4+layout(push_constant) uniform Push {
5+ vec4 color;
6+ vec4 dst;
7+ vec2 uv[4];
8+} pc;
Added file: crates/render-vk/shaders/quad.vert+11 additions
@@ -0,0 +1,11 @@
1+#version 450
2+#extension GL_GOOGLE_include_directive : require
3+#include "push.glsl"
4+
5+layout(location = 0) out vec2 v_uv;
6+
7+void main() {
8+ vec2 corner = vec2(gl_VertexIndex & 1, gl_VertexIndex >> 1);
9+ v_uv = pc.uv[gl_VertexIndex];
10+ gl_Position = vec4(pc.dst.xy + corner * pc.dst.zw, 0.0, 1.0);
11+}
Added file: crates/render-vk/shaders/solid.frag+9 additions
@@ -0,0 +1,9 @@
1+#version 450
2+#extension GL_GOOGLE_include_directive : require
3+#include "push.glsl"
4+
5+layout(location = 0) out vec4 out_color;
6+
7+void main() {
8+ out_color = pc.color;
9+}
Added file: crates/render-vk/shaders/texture.frag+11 additions
@@ -0,0 +1,11 @@
1+#version 450
2+#extension GL_GOOGLE_include_directive : require
3+#include "push.glsl"
4+
5+layout(set = 0, binding = 0) uniform sampler2D tex;
6+layout(location = 0) in vec2 v_uv;
7+layout(location = 0) out vec4 out_color;
8+
9+void main() {
10+ out_color = texture(tex, v_uv) * pc.color.a;
11+}
Added file: crates/render-vk/src/device.rs+155 additions
@@ -0,0 +1,155 @@
1+use crate::Error;
2+use ash::vk;
3+use gpu_allocator::vulkan::{Allocator, AllocatorCreateDesc};
4+use std::sync::Mutex;
5+
6+/// Everything that must outlive every Vulkan object the renderer hands out.
7+/// Fields drop in declaration order, so the allocator frees its memory before
8+/// the device is destroyed.
9+pub(crate) struct Shared {
10+ pub allocator: Mutex<Allocator>,
11+ pub device: Device,
12+}
13+
14+pub(crate) struct Device {
15+ pub _entry: ash::Entry,
16+ pub instance: ash::Instance,
17+ #[expect(
18+ dead_code,
19+ reason = "dmabuf import queries format properties through it"
20+ )]
21+ pub physical: vk::PhysicalDevice,
22+ pub device: ash::Device,
23+ pub queue: Mutex<vk::Queue>,
24+ pub queue_family: u32,
25+ pub push_descriptor: ash::khr::push_descriptor::Device,
26+ pub name: String,
27+}
28+
29+impl Drop for Device {
30+ fn drop(&mut self) {
31+ unsafe {
32+ let _ = self.device.device_wait_idle();
33+ self.device.destroy_device(None);
34+ self.instance.destroy_instance(None);
35+ }
36+ }
37+}
38+
39+impl Shared {
40+ /// Opens the first suitable device, or the one whose name contains
41+ /// `MONOLITH_VK_DEVICE` when that is set.
42+ pub fn new() -> Result<Self, Error> {
43+ let entry = unsafe { ash::Entry::load()? };
44+ let app = vk::ApplicationInfo::default()
45+ .application_name(c"monolith")
46+ .engine_name(c"monolith")
47+ .api_version(vk::API_VERSION_1_3);
48+ let instance = unsafe {
49+ entry.create_instance(
50+ &vk::InstanceCreateInfo::default().application_info(&app),
51+ None,
52+ )?
53+ };
54+ let wanted = std::env::var("MONOLITH_VK_DEVICE").ok();
55+ let picked = unsafe { pick_physical(&instance, wanted.as_deref()) };
56+ let Some((physical, queue_family, name)) = picked else {
57+ unsafe { instance.destroy_instance(None) };
58+ return Err(Error::NoSuitableDevice);
59+ };
60+ let priorities = [1.0];
61+ let queues = [vk::DeviceQueueCreateInfo::default()
62+ .queue_family_index(queue_family)
63+ .queue_priorities(&priorities)];
64+ let extensions = [ash::khr::push_descriptor::NAME.as_ptr()];
65+ let mut features13 = vk::PhysicalDeviceVulkan13Features::default()
66+ .dynamic_rendering(true)
67+ .synchronization2(true);
68+ let info = vk::DeviceCreateInfo::default()
69+ .queue_create_infos(&queues)
70+ .enabled_extension_names(&extensions)
71+ .push_next(&mut features13);
72+ let device = match unsafe { instance.create_device(physical, &info, None) } {
73+ Ok(d) => d,
74+ Err(e) => {
75+ unsafe { instance.destroy_instance(None) };
76+ return Err(e.into());
77+ }
78+ };
79+ let queue = unsafe { device.get_device_queue(queue_family, 0) };
80+ let push_descriptor = ash::khr::push_descriptor::Device::new(&instance, &device);
81+ let allocator = Allocator::new(&AllocatorCreateDesc {
82+ instance: instance.clone(),
83+ device: device.clone(),
84+ physical_device: physical,
85+ debug_settings: Default::default(),
86+ buffer_device_address: false,
87+ allocation_sizes: Default::default(),
88+ });
89+ let device = Device {
90+ _entry: entry,
91+ instance,
92+ physical,
93+ device,
94+ queue: Mutex::new(queue),
95+ queue_family,
96+ push_descriptor,
97+ name,
98+ };
99+ Ok(Self {
100+ allocator: Mutex::new(allocator?),
101+ device,
102+ })
103+ }
104+}
105+
106+unsafe fn pick_physical(
107+ instance: &ash::Instance,
108+ wanted: Option<&str>,
109+) -> Option<(vk::PhysicalDevice, u32, String)> {
110+ let devices = unsafe { instance.enumerate_physical_devices() }.ok()?;
111+ let mut candidates = Vec::new();
112+ for physical in devices {
113+ let props = unsafe { instance.get_physical_device_properties(physical) };
114+ if props.api_version < vk::API_VERSION_1_3 {
115+ continue;
116+ }
117+ let name = props
118+ .device_name_as_c_str()
119+ .unwrap_or(c"unknown")
120+ .to_string_lossy()
121+ .into_owned();
122+ if wanted.is_some_and(|w| !name.contains(w)) {
123+ continue;
124+ }
125+ let mut f13 = vk::PhysicalDeviceVulkan13Features::default();
126+ let mut f2 = vk::PhysicalDeviceFeatures2::default().push_next(&mut f13);
127+ unsafe { instance.get_physical_device_features2(physical, &mut f2) };
128+ if f13.dynamic_rendering == vk::FALSE || f13.synchronization2 == vk::FALSE {
129+ continue;
130+ }
131+ let has_push = unsafe { instance.enumerate_device_extension_properties(physical) }
132+ .unwrap_or_default()
133+ .iter()
134+ .any(|e| e.extension_name_as_c_str().ok() == Some(ash::khr::push_descriptor::NAME));
135+ if !has_push {
136+ continue;
137+ }
138+ let families = unsafe { instance.get_physical_device_queue_family_properties(physical) };
139+ let Some(family) = families
140+ .iter()
141+ .position(|f| f.queue_flags.contains(vk::QueueFlags::GRAPHICS))
142+ else {
143+ continue;
144+ };
145+ let rank = match props.device_type {
146+ vk::PhysicalDeviceType::DISCRETE_GPU => 0,
147+ vk::PhysicalDeviceType::INTEGRATED_GPU => 1,
148+ vk::PhysicalDeviceType::VIRTUAL_GPU => 2,
149+ _ => 3,
150+ };
151+ candidates.push((rank, physical, family as u32, name));
152+ }
153+ candidates.sort_by_key(|c| c.0);
154+ candidates.into_iter().next().map(|(_, p, f, n)| (p, f, n))
155+}
Added file: crates/render-vk/src/error.rs+24 additions
@@ -0,0 +1,24 @@
1+use ash::vk;
2+use smithay::backend::allocator::Fourcc;
3+
4+#[derive(Debug, thiserror::Error)]
5+pub enum Error {
6+ #[error("failed to load the Vulkan loader: {0}")]
7+ Loading(#[from] ash::LoadingError),
8+ #[error("Vulkan call failed: {0}")]
9+ Vulkan(#[from] vk::Result),
10+ #[error("GPU memory allocation failed: {0}")]
11+ Allocation(#[from] gpu_allocator::AllocationError),
12+ #[error("no Vulkan 1.3 device with dynamic rendering, synchronization2 and push descriptors")]
13+ NoSuitableDevice,
14+ #[error("unsupported pixel format {0}")]
15+ UnsupportedFormat(Fourcc),
16+ #[error("buffer of {got} bytes is too small, {needed} needed")]
17+ BufferTooSmall { needed: usize, got: usize },
18+ #[error("texture belongs to a different renderer")]
19+ ForeignTexture,
20+ #[error("interrupted while waiting for a sync point")]
21+ Interrupted,
22+ #[error("invalid size {0}x{1}")]
23+ InvalidSize(i32, i32),
24+}
Added file: crates/render-vk/src/format.rs+38 additions
@@ -0,0 +1,38 @@
1+use ash::vk;
2+use smithay::backend::allocator::Fourcc;
3+
4+/// Vulkan format and swizzle for a DRM fourcc. Formats without alpha read
5+/// alpha as one through the image view.
6+pub(crate) struct VkFormat {
7+ pub format: vk::Format,
8+ pub opaque: bool,
9+}
10+
11+pub(crate) const SUPPORTED: [Fourcc; 4] = [
12+ Fourcc::Argb8888,
13+ Fourcc::Xrgb8888,
14+ Fourcc::Abgr8888,
15+ Fourcc::Xbgr8888,
16+];
17+
18+pub(crate) fn vk_format(fourcc: Fourcc) -> Option<VkFormat> {
19+ let (format, opaque) = match fourcc {
20+ Fourcc::Argb8888 => (vk::Format::B8G8R8A8_UNORM, false),
21+ Fourcc::Xrgb8888 => (vk::Format::B8G8R8A8_UNORM, true),
22+ Fourcc::Abgr8888 => (vk::Format::R8G8B8A8_UNORM, false),
23+ Fourcc::Xbgr8888 => (vk::Format::R8G8B8A8_UNORM, true),
24+ _ => return None,
25+ };
26+ Some(VkFormat { format, opaque })
27+}
28+
29+pub(crate) fn components(opaque: bool) -> vk::ComponentMapping {
30+ let a = if opaque {
31+ vk::ComponentSwizzle::ONE
32+ } else {
33+ vk::ComponentSwizzle::IDENTITY
34+ };
35+ vk::ComponentMapping::default().a(a)
36+}
37+
38+pub(crate) const BYTES_PER_PIXEL: usize = 4;
Added file: crates/render-vk/src/frame.rs+201 additions
@@ -0,0 +1,201 @@
1+use crate::Error;
2+use crate::image::{VulkanTarget, VulkanTexture};
3+use crate::pipeline::{Kind, Push};
4+use crate::renderer::VulkanRenderer;
5+use crate::transform::{rect_to_framebuffer, texture_uvs, to_framebuffer};
6+use ash::vk;
7+use smithay::backend::renderer::sync::SyncPoint;
8+use smithay::backend::renderer::{Color32F, ContextId, Frame, Texture};
9+use smithay::utils::{Buffer, Physical, Rectangle, Size, Transform};
10+use std::sync::Arc;
11+
12+pub(crate) enum Op {
13+ Clear {
14+ color: [f32; 4],
15+ rects: Vec<vk::Rect2D>,
16+ },
17+ Draw {
18+ kind: Kind,
19+ texture: Option<VulkanTexture>,
20+ push: Push,
21+ scissors: Vec<vk::Rect2D>,
22+ },
23+}
24+
25+/// A frame in progress. Draws are recorded and submitted together by [`Frame::finish`].
26+pub struct VulkanFrame<'frame, 'buffer> {
27+ renderer: &'frame mut VulkanRenderer,
28+ target: &'frame mut VulkanTarget<'buffer>,
29+ output: Size<i32, Physical>,
30+ transform: Transform,
31+ ops: Vec<Op>,
32+}
33+
34+impl std::fmt::Debug for VulkanFrame<'_, '_> {
35+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36+ f.debug_struct("VulkanFrame")
37+ .field("output", &self.output)
38+ .field("ops", &self.ops.len())
39+ .finish()
40+ }
41+}
42+
43+impl<'frame, 'buffer> VulkanFrame<'frame, 'buffer> {
44+ pub(crate) fn new(
45+ renderer: &'frame mut VulkanRenderer,
46+ target: &'frame mut VulkanTarget<'buffer>,
47+ output: Size<i32, Physical>,
48+ transform: Transform,
49+ ) -> Self {
50+ Self {
51+ renderer,
52+ target,
53+ output,
54+ transform,
55+ ops: Vec::new(),
56+ }
57+ }
58+
59+ fn fb_size(&self) -> Size<i32, Physical> {
60+ (self.target.width() as i32, self.target.height() as i32).into()
61+ }
62+
63+ fn fb_rect(&self, r: Rectangle<i32, Physical>) -> Option<vk::Rect2D> {
64+ let r = rect_to_framebuffer(r, self.output, self.transform)
65+ .intersection(Rectangle::from_size(self.fb_size()))?;
66+ Some(vk::Rect2D {
67+ offset: vk::Offset2D {
68+ x: r.loc.x,
69+ y: r.loc.y,
70+ },
71+ extent: vk::Extent2D {
72+ width: r.size.w as u32,
73+ height: r.size.h as u32,
74+ },
75+ })
76+ }
77+
78+ fn scissors(
79+ &self,
80+ dst: Rectangle<i32, Physical>,
81+ damage: &[Rectangle<i32, Physical>],
82+ ) -> Vec<vk::Rect2D> {
83+ damage
84+ .iter()
85+ .filter_map(|d| d.intersection(Rectangle::from_size(dst.size)))
86+ .filter_map(|mut d| {
87+ d.loc += dst.loc;
88+ self.fb_rect(d)
89+ })
90+ .collect()
91+ }
92+
93+ fn ndc(&self, r: Rectangle<i32, Physical>) -> [f32; 4] {
94+ let fb = self.fb_size();
95+ let (w, h) = (fb.w as f32, fb.h as f32);
96+ [
97+ r.loc.x as f32 / w * 2.0 - 1.0,
98+ r.loc.y as f32 / h * 2.0 - 1.0,
99+ r.size.w as f32 / w * 2.0,
100+ r.size.h as f32 / h * 2.0,
101+ ]
102+ }
103+}
104+
105+impl Frame for VulkanFrame<'_, '_> {
106+ type Error = Error;
107+ type TextureId = VulkanTexture;
108+
109+ fn context_id(&self) -> ContextId<VulkanTexture> {
110+ self.renderer.context_id_ref().clone()
111+ }
112+
113+ fn clear(&mut self, color: Color32F, at: &[Rectangle<i32, Physical>]) -> Result<(), Error> {
114+ let rects: Vec<_> = at.iter().filter_map(|r| self.fb_rect(*r)).collect();
115+ if !rects.is_empty() {
116+ self.ops.push(Op::Clear {
117+ color: color.components(),
118+ rects,
119+ });
120+ }
121+ Ok(())
122+ }
123+
124+ fn draw_solid(
125+ &mut self,
126+ dst: Rectangle<i32, Physical>,
127+ damage: &[Rectangle<i32, Physical>],
128+ color: Color32F,
129+ ) -> Result<(), Error> {
130+ let scissors = self.scissors(dst, damage);
131+ if scissors.is_empty() {
132+ return Ok(());
133+ }
134+ let (fb, _) = to_framebuffer(dst, [[0.0; 2]; 4], self.output, self.transform);
135+ let push = Push {
136+ dst: self.ndc(fb),
137+ uv: [[0.0; 2]; 4],
138+ color: color.components(),
139+ };
140+ self.ops.push(Op::Draw {
141+ kind: Kind::Solid,
142+ texture: None,
143+ push,
144+ scissors,
145+ });
146+ Ok(())
147+ }
148+
149+ fn render_texture_from_to(
150+ &mut self,
151+ texture: &VulkanTexture,
152+ src: Rectangle<f64, Buffer>,
153+ dst: Rectangle<i32, Physical>,
154+ damage: &[Rectangle<i32, Physical>],
155+ _opaque_regions: &[Rectangle<i32, Physical>],
156+ src_transform: Transform,
157+ alpha: f32,
158+ ) -> Result<(), Error> {
159+ if !Arc::ptr_eq(&texture.0.shared, self.renderer.shared()) {
160+ return Err(Error::ForeignTexture);
161+ }
162+ if dst.size.w <= 0 || dst.size.h <= 0 || Arc::ptr_eq(&texture.0, &self.target.0.0) {
163+ return Ok(());
164+ }
165+ let scissors = self.scissors(dst, damage);
166+ if scissors.is_empty() {
167+ return Ok(());
168+ }
169+ let uvs = texture_uvs(src, dst.size, texture.size(), src_transform);
170+ let (fb, uv) = to_framebuffer(dst, uvs, self.output, self.transform);
171+ let push = Push {
172+ dst: self.ndc(fb),
173+ uv,
174+ color: [1.0, 1.0, 1.0, alpha],
175+ };
176+ self.ops.push(Op::Draw {
177+ kind: Kind::Texture,
178+ texture: Some(texture.clone()),
179+ push,
180+ scissors,
181+ });
182+ Ok(())
183+ }
184+
185+ fn transformation(&self) -> Transform {
186+ self.transform
187+ }
188+
189+ fn output_size(&self) -> Size<i32, Physical> {
190+ self.output
191+ }
192+
193+ fn wait(&mut self, sync: &SyncPoint) -> Result<(), Error> {
194+ sync.wait().map_err(|_| Error::Interrupted)
195+ }
196+
197+ fn finish(mut self) -> Result<SyncPoint, Error> {
198+ let ops = std::mem::take(&mut self.ops);
199+ self.renderer.submit(self.target, ops)
200+ }
201+}
Added file: crates/render-vk/src/image.rs+276 additions
@@ -0,0 +1,276 @@
1+use crate::Error;
2+use crate::device::Shared;
3+use crate::format::{components, vk_format};
4+use ash::vk;
5+use gpu_allocator::MemoryLocation;
6+use gpu_allocator::vulkan::{Allocation, AllocationCreateDesc, AllocationScheme};
7+use smithay::backend::allocator::Fourcc;
8+use smithay::backend::renderer::{Texture, TextureMapping};
9+use std::fmt;
10+use std::sync::{Arc, Mutex};
11+
12+pub(crate) struct ImageInner {
13+ pub shared: Arc<Shared>,
14+ pub image: vk::Image,
15+ pub view: vk::ImageView,
16+ pub allocation: Mutex<Option<Allocation>>,
17+ pub fourcc: Fourcc,
18+ pub format: vk::Format,
19+ pub width: u32,
20+ pub height: u32,
21+ pub layout: Mutex<vk::ImageLayout>,
22+}
23+
24+impl fmt::Debug for ImageInner {
25+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26+ f.debug_struct("ImageInner")
27+ .field("image", &self.image)
28+ .field("fourcc", &self.fourcc)
29+ .field("size", &(self.width, self.height))
30+ .finish()
31+ }
32+}
33+
34+impl Drop for ImageInner {
35+ fn drop(&mut self) {
36+ let d = &self.shared.device.device;
37+ unsafe {
38+ d.destroy_image_view(self.view, None);
39+ d.destroy_image(self.image, None);
40+ }
41+ if let Some(a) = self
42+ .allocation
43+ .lock()
44+ .unwrap_or_else(|e| e.into_inner())
45+ .take()
46+ {
47+ let _ = self
48+ .shared
49+ .allocator
50+ .lock()
51+ .unwrap_or_else(|e| e.into_inner())
52+ .free(a);
53+ }
54+ }
55+}
56+
57+impl ImageInner {
58+ pub fn create(
59+ shared: &Arc<Shared>,
60+ fourcc: Fourcc,
61+ width: u32,
62+ height: u32,
63+ usage: vk::ImageUsageFlags,
64+ ) -> Result<Arc<Self>, Error> {
65+ let fmt = vk_format(fourcc).ok_or(Error::UnsupportedFormat(fourcc))?;
66+ if width == 0 || height == 0 {
67+ return Err(Error::InvalidSize(width as i32, height as i32));
68+ }
69+ let d = &shared.device.device;
70+ let info = vk::ImageCreateInfo::default()
71+ .image_type(vk::ImageType::TYPE_2D)
72+ .format(fmt.format)
73+ .extent(vk::Extent3D {
74+ width,
75+ height,
76+ depth: 1,
77+ })
78+ .mip_levels(1)
79+ .array_layers(1)
80+ .samples(vk::SampleCountFlags::TYPE_1)
81+ .tiling(vk::ImageTiling::OPTIMAL)
82+ .usage(usage)
83+ .initial_layout(vk::ImageLayout::UNDEFINED);
84+ let image = unsafe { d.create_image(&info, None)? };
85+ let requirements = unsafe { d.get_image_memory_requirements(image) };
86+ let allocation = shared
87+ .allocator
88+ .lock()
89+ .unwrap_or_else(|e| e.into_inner())
90+ .allocate(&AllocationCreateDesc {
91+ name: "image",
92+ requirements,
93+ location: MemoryLocation::GpuOnly,
94+ linear: false,
95+ allocation_scheme: AllocationScheme::GpuAllocatorManaged,
96+ });
97+ let allocation = match allocation {
98+ Ok(a) => a,
99+ Err(e) => {
100+ unsafe { d.destroy_image(image, None) };
101+ return Err(e.into());
102+ }
103+ };
104+ unsafe { d.bind_image_memory(image, allocation.memory(), allocation.offset())? };
105+ let view_info = vk::ImageViewCreateInfo::default()
106+ .image(image)
107+ .view_type(vk::ImageViewType::TYPE_2D)
108+ .format(fmt.format)
109+ .components(components(fmt.opaque))
110+ .subresource_range(color_range());
111+ let view = unsafe { d.create_image_view(&view_info, None)? };
112+ Ok(Arc::new(Self {
113+ shared: shared.clone(),
114+ image,
115+ view,
116+ allocation: Mutex::new(Some(allocation)),
117+ fourcc,
118+ format: fmt.format,
119+ width,
120+ height,
121+ layout: Mutex::new(vk::ImageLayout::UNDEFINED),
122+ }))
123+ }
124+
125+ pub fn layout(&self) -> vk::ImageLayout {
126+ *self.layout.lock().unwrap_or_else(|e| e.into_inner())
127+ }
128+
129+ pub fn set_layout(&self, layout: vk::ImageLayout) {
130+ *self.layout.lock().unwrap_or_else(|e| e.into_inner()) = layout;
131+ }
132+}
133+
134+pub(crate) fn color_range() -> vk::ImageSubresourceRange {
135+ vk::ImageSubresourceRange::default()
136+ .aspect_mask(vk::ImageAspectFlags::COLOR)
137+ .level_count(1)
138+ .layer_count(1)
139+}
140+
141+/// A host-visible buffer for uploads and readbacks.
142+pub(crate) struct Staging {
143+ pub shared: Arc<Shared>,
144+ pub buffer: vk::Buffer,
145+ pub allocation: Option<Allocation>,
146+}
147+
148+impl Staging {
149+ pub fn new(
150+ shared: &Arc<Shared>,
151+ size: usize,
152+ usage: vk::BufferUsageFlags,
153+ location: MemoryLocation,
154+ ) -> Result<Self, Error> {
155+ let d = &shared.device.device;
156+ let buffer = unsafe {
157+ d.create_buffer(
158+ &vk::BufferCreateInfo::default()
159+ .size(size as u64)
160+ .usage(usage),
161+ None,
162+ )?
163+ };
164+ let requirements = unsafe { d.get_buffer_memory_requirements(buffer) };
165+ let allocation = shared
166+ .allocator
167+ .lock()
168+ .unwrap_or_else(|e| e.into_inner())
169+ .allocate(&AllocationCreateDesc {
170+ name: "staging",
171+ requirements,
172+ location,
173+ linear: true,
174+ allocation_scheme: AllocationScheme::GpuAllocatorManaged,
175+ });
176+ let allocation = match allocation {
177+ Ok(a) => a,
178+ Err(e) => {
179+ unsafe { d.destroy_buffer(buffer, None) };
180+ return Err(e.into());
181+ }
182+ };
183+ unsafe { d.bind_buffer_memory(buffer, allocation.memory(), allocation.offset())? };
184+ Ok(Self {
185+ shared: shared.clone(),
186+ buffer,
187+ allocation: Some(allocation),
188+ })
189+ }
190+
191+ pub fn bytes(&self) -> &[u8] {
192+ self.allocation
193+ .as_ref()
194+ .and_then(|a| a.mapped_slice())
195+ .unwrap_or(&[])
196+ }
197+
198+ pub fn bytes_mut(&mut self) -> &mut [u8] {
199+ self.allocation
200+ .as_mut()
201+ .and_then(|a| a.mapped_slice_mut())
202+ .unwrap_or(&mut [])
203+ }
204+}
205+
206+impl Drop for Staging {
207+ fn drop(&mut self) {
208+ unsafe { self.shared.device.device.destroy_buffer(self.buffer, None) };
209+ if let Some(a) = self.allocation.take() {
210+ let _ = self
211+ .shared
212+ .allocator
213+ .lock()
214+ .unwrap_or_else(|e| e.into_inner())
215+ .free(a);
216+ }
217+ }
218+}
219+
220+/// A sampled texture.
221+#[derive(Clone, Debug)]
222+pub struct VulkanTexture(pub(crate) Arc<ImageInner>);
223+
224+/// An image the renderer can draw into, from [`smithay::backend::renderer::Offscreen`].
225+#[derive(Debug)]
226+pub struct VulkanImage(pub(crate) Arc<ImageInner>);
227+
228+impl VulkanImage {
229+ /// The image's contents as a texture, for drawing it elsewhere.
230+ pub fn texture(&self) -> VulkanTexture {
231+ VulkanTexture(self.0.clone())
232+ }
233+}
234+
235+/// A bound render target.
236+#[derive(Debug)]
237+pub struct VulkanTarget<'a>(pub(crate) &'a mut VulkanImage);
238+
239+/// Pixels read back from the GPU.
240+#[derive(Debug)]
241+pub struct VulkanMapping {
242+ pub(crate) data: Vec<u8>,
243+ pub(crate) width: u32,
244+ pub(crate) height: u32,
245+ pub(crate) fourcc: Fourcc,
246+}
247+
248+macro_rules! texture_impl {
249+ ($t:ty, |$s:ident| $inner:expr) => {
250+ impl Texture for $t {
251+ fn width(&self) -> u32 {
252+ let $s = self;
253+ $inner.width
254+ }
255+ fn height(&self) -> u32 {
256+ let $s = self;
257+ $inner.height
258+ }
259+ fn format(&self) -> Option<Fourcc> {
260+ let $s = self;
261+ Some($inner.fourcc)
262+ }
263+ }
264+ };
265+}
266+
267+texture_impl!(VulkanTexture, |s| s.0);
268+texture_impl!(VulkanImage, |s| s.0);
269+texture_impl!(VulkanTarget<'_>, |s| s.0.0);
270+texture_impl!(VulkanMapping, |s| s);
271+
272+impl TextureMapping for VulkanMapping {
273+ fn flipped(&self) -> bool {
274+ false
275+ }
276+}
Modified file: crates/render-vk/src/lib.rs+18 additions
@@ -1 +1,19 @@
1+//! Vulkan renderer implementing smithay's renderer traits.
2+//!
3+//! A frame records its draws as a list of operations and submits them in one
4+//! command buffer when it finishes, returning a fence as its sync point.
15
6+mod device;
7+mod error;
8+mod format;
9+mod frame;
10+mod image;
11+mod pipeline;
12+mod renderer;
13+mod sync;
14+mod transform;
15+
16+pub use error::Error;
17+pub use frame::VulkanFrame;
18+pub use image::{VulkanImage, VulkanMapping, VulkanTarget, VulkanTexture};
19+pub use renderer::VulkanRenderer;
Added file: crates/render-vk/src/pipeline.rs+169 additions
@@ -0,0 +1,169 @@
1+use crate::Error;
2+use crate::device::Shared;
3+use ash::vk;
4+use std::collections::HashMap;
5+use std::sync::Arc;
6+
7+/// Push constants shared by every shader, see `shaders/push.glsl`.
8+/// `dst` is the quad in normalized device coordinates as (x, y, w, h),
9+/// `uv` the texture coordinate at each corner (TL, TR, BL, BR).
10+#[repr(C)]
11+#[derive(Clone, Copy, Debug, Default)]
12+pub(crate) struct Push {
13+ pub color: [f32; 4],
14+ pub dst: [f32; 4],
15+ pub uv: [[f32; 2]; 4],
16+}
17+
18+impl Push {
19+ pub fn bytes(&self) -> &[u8] {
20+ unsafe { std::slice::from_raw_parts((self as *const Self).cast::<u8>(), size_of::<Self>()) }
21+ }
22+}
23+
24+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25+pub(crate) enum Kind {
26+ Solid,
27+ Texture,
28+}
29+
30+pub(crate) struct Pipelines {
31+ shared: Arc<Shared>,
32+ pub set_layout: vk::DescriptorSetLayout,
33+ pub layout: vk::PipelineLayout,
34+ vert: vk::ShaderModule,
35+ solid: vk::ShaderModule,
36+ texture: vk::ShaderModule,
37+ cache: HashMap<(vk::Format, Kind), vk::Pipeline>,
38+}
39+
40+const QUAD_VERT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/quad.vert.spv"));
41+const SOLID_FRAG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/solid.frag.spv"));
42+const TEXTURE_FRAG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/texture.frag.spv"));
43+
44+impl Pipelines {
45+ pub fn new(shared: &Arc<Shared>) -> Result<Self, Error> {
46+ let d = &shared.device.device;
47+ let bindings = [vk::DescriptorSetLayoutBinding::default()
48+ .binding(0)
49+ .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
50+ .descriptor_count(1)
51+ .stage_flags(vk::ShaderStageFlags::FRAGMENT)];
52+ let set_layout = unsafe {
53+ d.create_descriptor_set_layout(
54+ &vk::DescriptorSetLayoutCreateInfo::default()
55+ .flags(vk::DescriptorSetLayoutCreateFlags::PUSH_DESCRIPTOR_KHR)
56+ .bindings(&bindings),
57+ None,
58+ )?
59+ };
60+ let ranges = [vk::PushConstantRange::default()
61+ .stage_flags(vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT)
62+ .size(size_of::<Push>() as u32)];
63+ let set_layouts = [set_layout];
64+ let layout = unsafe {
65+ d.create_pipeline_layout(
66+ &vk::PipelineLayoutCreateInfo::default()
67+ .set_layouts(&set_layouts)
68+ .push_constant_ranges(&ranges),
69+ None,
70+ )?
71+ };
72+ let module = |code: &[u8]| -> Result<vk::ShaderModule, Error> {
73+ let words = ash::util::read_spv(&mut std::io::Cursor::new(code))
74+ .map_err(|_| vk::Result::ERROR_INVALID_SHADER_NV)?;
75+ Ok(unsafe {
76+ d.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&words), None)?
77+ })
78+ };
79+ Ok(Self {
80+ shared: shared.clone(),
81+ set_layout,
82+ layout,
83+ vert: module(QUAD_VERT)?,
84+ solid: module(SOLID_FRAG)?,
85+ texture: module(TEXTURE_FRAG)?,
86+ cache: HashMap::new(),
87+ })
88+ }
89+
90+ pub fn get(&mut self, format: vk::Format, kind: Kind) -> Result<vk::Pipeline, Error> {
91+ if let Some(&p) = self.cache.get(&(format, kind)) {
92+ return Ok(p);
93+ }
94+ let d = &self.shared.device.device;
95+ let frag = match kind {
96+ Kind::Solid => self.solid,
97+ Kind::Texture => self.texture,
98+ };
99+ let stages = [
100+ vk::PipelineShaderStageCreateInfo::default()
101+ .stage(vk::ShaderStageFlags::VERTEX)
102+ .module(self.vert)
103+ .name(c"main"),
104+ vk::PipelineShaderStageCreateInfo::default()
105+ .stage(vk::ShaderStageFlags::FRAGMENT)
106+ .module(frag)
107+ .name(c"main"),
108+ ];
109+ let vertex = vk::PipelineVertexInputStateCreateInfo::default();
110+ let assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
111+ .topology(vk::PrimitiveTopology::TRIANGLE_STRIP);
112+ let viewport = vk::PipelineViewportStateCreateInfo::default()
113+ .viewport_count(1)
114+ .scissor_count(1);
115+ let raster = vk::PipelineRasterizationStateCreateInfo::default()
116+ .polygon_mode(vk::PolygonMode::FILL)
117+ .cull_mode(vk::CullModeFlags::NONE)
118+ .line_width(1.0);
119+ let multisample = vk::PipelineMultisampleStateCreateInfo::default()
120+ .rasterization_samples(vk::SampleCountFlags::TYPE_1);
121+ let attachments = [vk::PipelineColorBlendAttachmentState::default()
122+ .blend_enable(true)
123+ .src_color_blend_factor(vk::BlendFactor::ONE)
124+ .dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
125+ .color_blend_op(vk::BlendOp::ADD)
126+ .src_alpha_blend_factor(vk::BlendFactor::ONE)
127+ .dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
128+ .alpha_blend_op(vk::BlendOp::ADD)
129+ .color_write_mask(vk::ColorComponentFlags::RGBA)];
130+ let blend = vk::PipelineColorBlendStateCreateInfo::default().attachments(&attachments);
131+ let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
132+ let dynamic = vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
133+ let formats = [format];
134+ let mut rendering =
135+ vk::PipelineRenderingCreateInfo::default().color_attachment_formats(&formats);
136+ let info = vk::GraphicsPipelineCreateInfo::default()
137+ .stages(&stages)
138+ .vertex_input_state(&vertex)
139+ .input_assembly_state(&assembly)
140+ .viewport_state(&viewport)
141+ .rasterization_state(&raster)
142+ .multisample_state(&multisample)
143+ .color_blend_state(&blend)
144+ .dynamic_state(&dynamic)
145+ .layout(self.layout)
146+ .push_next(&mut rendering);
147+ let pipeline =
148+ unsafe { d.create_graphics_pipelines(vk::PipelineCache::null(), &[info], None) }
149+ .map_err(|(_, e)| e)?[0];
150+ self.cache.insert((format, kind), pipeline);
151+ Ok(pipeline)
152+ }
153+}
154+
155+impl Drop for Pipelines {
156+ fn drop(&mut self) {
157+ let d = &self.shared.device.device;
158+ unsafe {
159+ for (_, p) in self.cache.drain() {
160+ d.destroy_pipeline(p, None);
161+ }
162+ d.destroy_shader_module(self.vert, None);
163+ d.destroy_shader_module(self.solid, None);
164+ d.destroy_shader_module(self.texture, None);
165+ d.destroy_pipeline_layout(self.layout, None);
166+ d.destroy_descriptor_set_layout(self.set_layout, None);
167+ }
168+ }
169+}
Added file: crates/render-vk/src/renderer.rs+698 additions
@@ -0,0 +1,698 @@
1+use crate::Error;
2+use crate::device::Shared;
3+use crate::format::{BYTES_PER_PIXEL, SUPPORTED};
4+use crate::frame::{Op, VulkanFrame};
5+use crate::image::{
6+ ImageInner, Staging, VulkanImage, VulkanMapping, VulkanTarget, VulkanTexture, color_range,
7+};
8+use crate::pipeline::Pipelines;
9+use crate::sync::{FenceInner, VkFence};
10+use ash::vk;
11+use gpu_allocator::MemoryLocation;
12+use smithay::backend::allocator::Fourcc;
13+use smithay::backend::renderer::sync::SyncPoint;
14+use smithay::backend::renderer::{
15+ Bind, ContextId, DebugFlags, ExportMem, ImportMem, Offscreen, Renderer, RendererSuper, Texture,
16+ TextureFilter,
17+};
18+use smithay::utils::{Buffer, Physical, Rectangle, Size, Transform};
19+use std::fmt;
20+use std::sync::Arc;
21+
22+struct Pending {
23+ fence: Arc<FenceInner>,
24+ cmd: vk::CommandBuffer,
25+ _keep: Vec<VulkanTexture>,
26+}
27+
28+pub struct VulkanRenderer {
29+ pending: Vec<Pending>,
30+ pipelines: Pipelines,
31+ pool: vk::CommandPool,
32+ samplers: [vk::Sampler; 4],
33+ min_filter: TextureFilter,
34+ mag_filter: TextureFilter,
35+ debug: DebugFlags,
36+ context: ContextId<VulkanTexture>,
37+ shared: Arc<Shared>,
38+}
39+
40+impl fmt::Debug for VulkanRenderer {
41+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42+ f.debug_struct("VulkanRenderer")
43+ .field("device", &self.shared.device.name)
44+ .finish()
45+ }
46+}
47+
48+impl VulkanRenderer {
49+ pub fn new() -> Result<Self, Error> {
50+ let shared = Arc::new(Shared::new()?);
51+ let d = &shared.device.device;
52+ let pool = unsafe {
53+ d.create_command_pool(
54+ &vk::CommandPoolCreateInfo::default()
55+ .queue_family_index(shared.device.queue_family)
56+ .flags(vk::CommandPoolCreateFlags::TRANSIENT),
57+ None,
58+ )?
59+ };
60+ let filter = |f: TextureFilter| match f {
61+ TextureFilter::Linear => vk::Filter::LINEAR,
62+ TextureFilter::Nearest => vk::Filter::NEAREST,
63+ };
64+ let mut samplers = [vk::Sampler::null(); 4];
65+ for (i, s) in samplers.iter_mut().enumerate() {
66+ let (min, mag) = (sampler_filter(i >> 1), sampler_filter(i & 1));
67+ *s = unsafe {
68+ d.create_sampler(
69+ &vk::SamplerCreateInfo::default()
70+ .min_filter(filter(min))
71+ .mag_filter(filter(mag))
72+ .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE)
73+ .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE)
74+ .address_mode_w(vk::SamplerAddressMode::CLAMP_TO_EDGE),
75+ None,
76+ )?
77+ };
78+ }
79+ Ok(Self {
80+ pending: Vec::new(),
81+ pipelines: Pipelines::new(&shared)?,
82+ pool,
83+ samplers,
84+ min_filter: TextureFilter::Linear,
85+ mag_filter: TextureFilter::Linear,
86+ debug: DebugFlags::empty(),
87+ context: ContextId::new(),
88+ shared,
89+ })
90+ }
91+
92+ /// Name of the physical device in use.
93+ pub fn device_name(&self) -> &str {
94+ &self.shared.device.name
95+ }
96+
97+ pub(crate) fn shared(&self) -> &Arc<Shared> {
98+ &self.shared
99+ }
100+
101+ pub(crate) fn context_id_ref(&self) -> &ContextId<VulkanTexture> {
102+ &self.context
103+ }
104+
105+ fn sampler(&self) -> vk::Sampler {
106+ let idx = |f: TextureFilter| usize::from(f == TextureFilter::Nearest);
107+ self.samplers[(idx(self.min_filter) << 1) | idx(self.mag_filter)]
108+ }
109+
110+ fn reap(&mut self) {
111+ let d = &self.shared.device.device;
112+ let pool = self.pool;
113+ self.pending.retain(|p| {
114+ if !p.fence.signaled() {
115+ return true;
116+ }
117+ unsafe { d.free_command_buffers(pool, &[p.cmd]) };
118+ false
119+ });
120+ }
121+
122+ fn begin(&self) -> Result<vk::CommandBuffer, Error> {
123+ let d = &self.shared.device.device;
124+ let cmd = unsafe {
125+ d.allocate_command_buffers(
126+ &vk::CommandBufferAllocateInfo::default()
127+ .command_pool(self.pool)
128+ .level(vk::CommandBufferLevel::PRIMARY)
129+ .command_buffer_count(1),
130+ )?[0]
131+ };
132+ unsafe {
133+ d.begin_command_buffer(
134+ cmd,
135+ &vk::CommandBufferBeginInfo::default()
136+ .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
137+ )?
138+ };
139+ Ok(cmd)
140+ }
141+
142+ fn end_and_submit(&self, cmd: vk::CommandBuffer) -> Result<Arc<FenceInner>, Error> {
143+ let d = &self.shared.device.device;
144+ unsafe { d.end_command_buffer(cmd)? };
145+ let fence = FenceInner::new(&self.shared)?;
146+ let cmds = [vk::CommandBufferSubmitInfo::default().command_buffer(cmd)];
147+ let submit = vk::SubmitInfo2::default().command_buffer_infos(&cmds);
148+ let queue = self
149+ .shared
150+ .device
151+ .queue
152+ .lock()
153+ .unwrap_or_else(|e| e.into_inner());
154+ unsafe { d.queue_submit2(*queue, &[submit], fence.fence)? };
155+ Ok(fence)
156+ }
157+
158+ /// Records, submits and waits for a short command buffer.
159+ fn one_shot(
160+ &mut self,
161+ record: impl FnOnce(&ash::Device, vk::CommandBuffer),
162+ ) -> Result<(), Error> {
163+ let cmd = self.begin()?;
164+ record(&self.shared.device.device, cmd);
165+ let fence = self.end_and_submit(cmd)?;
166+ let res = fence.wait();
167+ unsafe {
168+ self.shared
169+ .device
170+ .device
171+ .free_command_buffers(self.pool, &[cmd])
172+ };
173+ Ok(res?)
174+ }
175+
176+ pub(crate) fn submit(
177+ &mut self,
178+ target: &mut VulkanTarget<'_>,
179+ ops: Vec<Op>,
180+ ) -> Result<SyncPoint, Error> {
181+ self.reap();
182+ let image = target.0.0.clone();
183+ let mut keep = vec![VulkanTexture(image.clone())];
184+ let mut transitions = Vec::new();
185+ for op in &ops {
186+ if let Op::Draw {
187+ texture: Some(t), ..
188+ } = op
189+ {
190+ if t.0.layout() != vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL {
191+ transitions.push(t.clone());
192+ }
193+ keep.push(t.clone());
194+ }
195+ }
196+ let mut pipelines = Vec::with_capacity(ops.len());
197+ for op in &ops {
198+ pipelines.push(match op {
199+ Op::Draw { kind, .. } => Some(self.pipelines.get(image.format, *kind)?),
200+ Op::Clear { .. } => None,
201+ });
202+ }
203+ let cmd = self.begin()?;
204+ let d = &self.shared.device.device;
205+ let push = &self.shared.device.push_descriptor;
206+ let (w, h) = (image.width, image.height);
207+ let sampler = self.sampler();
208+ unsafe {
209+ for t in &transitions {
210+ barrier(
211+ d,
212+ cmd,
213+ t.0.image,
214+ t.0.layout(),
215+ vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
216+ );
217+ t.0.set_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
218+ }
219+ barrier(
220+ d,
221+ cmd,
222+ image.image,
223+ image.layout(),
224+ vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
225+ );
226+ let attachments = [vk::RenderingAttachmentInfo::default()
227+ .image_view(image.view)
228+ .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
229+ .load_op(vk::AttachmentLoadOp::LOAD)
230+ .store_op(vk::AttachmentStoreOp::STORE)];
231+ let area = vk::Rect2D {
232+ offset: vk::Offset2D::default(),
233+ extent: vk::Extent2D {
234+ width: w,
235+ height: h,
236+ },
237+ };
238+ d.cmd_begin_rendering(
239+ cmd,
240+ &vk::RenderingInfo::default()
241+ .render_area(area)
242+ .layer_count(1)
243+ .color_attachments(&attachments),
244+ );
245+ d.cmd_set_viewport(
246+ cmd,
247+ 0,
248+ &[vk::Viewport {
249+ x: 0.0,
250+ y: 0.0,
251+ width: w as f32,
252+ height: h as f32,
253+ min_depth: 0.0,
254+ max_depth: 1.0,
255+ }],
256+ );
257+ let mut bound = vk::Pipeline::null();
258+ for (op, pipeline) in ops.iter().zip(pipelines) {
259+ match op {
260+ Op::Clear { color, rects } => {
261+ let clear = [vk::ClearAttachment {
262+ aspect_mask: vk::ImageAspectFlags::COLOR,
263+ color_attachment: 0,
264+ clear_value: vk::ClearValue {
265+ color: vk::ClearColorValue { float32: *color },
266+ },
267+ }];
268+ let rects: Vec<_> = rects
269+ .iter()
270+ .map(|&rect| vk::ClearRect {
271+ rect,
272+ base_array_layer: 0,
273+ layer_count: 1,
274+ })
275+ .collect();
276+ d.cmd_clear_attachments(cmd, &clear, &rects);
277+ }
278+ Op::Draw {
279+ texture,
280+ push: constants,
281+ scissors,
282+ ..
283+ } => {
284+ let pipeline = pipeline.unwrap_or_default();
285+ if pipeline != bound {
286+ d.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, pipeline);
287+ bound = pipeline;
288+ }
289+ if let Some(t) = texture {
290+ let info = [vk::DescriptorImageInfo::default()
291+ .image_view(t.0.view)
292+ .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
293+ .sampler(sampler)];
294+ let write = vk::WriteDescriptorSet::default()
295+ .dst_binding(0)
296+ .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
297+ .image_info(&info);
298+ push.cmd_push_descriptor_set(
299+ cmd,
300+ vk::PipelineBindPoint::GRAPHICS,
301+ self.pipelines.layout,
302+ 0,
303+ &[write],
304+ );
305+ }
306+ d.cmd_push_constants(
307+ cmd,
308+ self.pipelines.layout,
309+ vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
310+ 0,
311+ constants.bytes(),
312+ );
313+ for s in scissors {
314+ d.cmd_set_scissor(cmd, 0, &[*s]);
315+ d.cmd_draw(cmd, 4, 1, 0, 0);
316+ }
317+ }
318+ }
319+ }
320+ d.cmd_end_rendering(cmd);
321+ }
322+ image.set_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
323+ let fence = self.end_and_submit(cmd)?;
324+ self.pending.push(Pending {
325+ fence: fence.clone(),
326+ cmd,
327+ _keep: keep,
328+ });
329+ Ok(SyncPoint::from(VkFence(fence)))
330+ }
331+
332+ fn upload(
333+ &mut self,
334+ image: &Arc<ImageInner>,
335+ data: &[u8],
336+ region: Rectangle<i32, Buffer>,
337+ flipped: bool,
338+ ) -> Result<(), Error> {
339+ let width = image.width as usize;
340+ let needed = width * image.height as usize * BYTES_PER_PIXEL;
341+ if data.len() < needed {
342+ return Err(Error::BufferTooSmall {
343+ needed,
344+ got: data.len(),
345+ });
346+ }
347+ let mut staging = Staging::new(
348+ &self.shared,
349+ needed,
350+ vk::BufferUsageFlags::TRANSFER_SRC,
351+ MemoryLocation::CpuToGpu,
352+ )?;
353+ let row = width * BYTES_PER_PIXEL;
354+ let rows = image.height as usize;
355+ let dst = staging.bytes_mut();
356+ for y in 0..rows {
357+ let from = if flipped { rows - 1 - y } else { y };
358+ dst[y * row..(y + 1) * row].copy_from_slice(&data[from * row..(from + 1) * row]);
359+ }
360+ let old = image.layout();
361+ let copy = vk::BufferImageCopy::default()
362+ .buffer_offset(
363+ ((region.loc.y as usize * width + region.loc.x as usize) * BYTES_PER_PIXEL) as u64,
364+ )
365+ .buffer_row_length(image.width)
366+ .image_subresource(
367+ vk::ImageSubresourceLayers::default()
368+ .aspect_mask(vk::ImageAspectFlags::COLOR)
369+ .layer_count(1),
370+ )
371+ .image_offset(vk::Offset3D {
372+ x: region.loc.x,
373+ y: region.loc.y,
374+ z: 0,
375+ })
376+ .image_extent(vk::Extent3D {
377+ width: region.size.w as u32,
378+ height: region.size.h as u32,
379+ depth: 1,
380+ });
381+ let (img, buffer) = (image.image, staging.buffer);
382+ self.one_shot(|d, cmd| unsafe {
383+ barrier(d, cmd, img, old, vk::ImageLayout::TRANSFER_DST_OPTIMAL);
384+ d.cmd_copy_buffer_to_image(
385+ cmd,
386+ buffer,
387+ img,
388+ vk::ImageLayout::TRANSFER_DST_OPTIMAL,
389+ &[copy],
390+ );
391+ barrier(
392+ d,
393+ cmd,
394+ img,
395+ vk::ImageLayout::TRANSFER_DST_OPTIMAL,
396+ vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
397+ );
398+ })?;
399+ image.set_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
400+ Ok(())
401+ }
402+
403+ fn read_back(
404+ &mut self,
405+ image: &Arc<ImageInner>,
406+ region: Rectangle<i32, Buffer>,
407+ fourcc: Fourcc,
408+ ) -> Result<VulkanMapping, Error> {
409+ let region = region
410+ .intersection(Rectangle::from_size(
411+ (image.width as i32, image.height as i32).into(),
412+ ))
413+ .unwrap_or_default();
414+ let (w, h) = (region.size.w.max(0) as u32, region.size.h.max(0) as u32);
415+ let swap = needs_swizzle(image.fourcc, fourcc)?;
416+ let len = (w as usize * h as usize * BYTES_PER_PIXEL).max(BYTES_PER_PIXEL);
417+ let staging = Staging::new(
418+ &self.shared,
419+ len,
420+ vk::BufferUsageFlags::TRANSFER_DST,
421+ MemoryLocation::GpuToCpu,
422+ )?;
423+ if w > 0 && h > 0 {
424+ let old = image.layout();
425+ let copy = vk::BufferImageCopy::default()
426+ .image_subresource(
427+ vk::ImageSubresourceLayers::default()
428+ .aspect_mask(vk::ImageAspectFlags::COLOR)
429+ .layer_count(1),
430+ )
431+ .image_offset(vk::Offset3D {
432+ x: region.loc.x,
433+ y: region.loc.y,
434+ z: 0,
435+ })
436+ .image_extent(vk::Extent3D {
437+ width: w,
438+ height: h,
439+ depth: 1,
440+ });
441+ let (img, buffer) = (image.image, staging.buffer);
442+ self.one_shot(|d, cmd| unsafe {
443+ barrier(d, cmd, img, old, vk::ImageLayout::TRANSFER_SRC_OPTIMAL);
444+ d.cmd_copy_image_to_buffer(
445+ cmd,
446+ img,
447+ vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
448+ buffer,
449+ &[copy],
450+ );
451+ })?;
452+ image.set_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL);
453+ }
454+ let mut data = staging.bytes()[..w as usize * h as usize * BYTES_PER_PIXEL].to_vec();
455+ if swap {
456+ for px in data.as_chunks_mut::<BYTES_PER_PIXEL>().0 {
457+ px.swap(0, 2);
458+ }
459+ }
460+ Ok(VulkanMapping {
461+ data,
462+ width: w,
463+ height: h,
464+ fourcc,
465+ })
466+ }
467+
468+ fn own(&self, image: &Arc<ImageInner>) -> Result<(), Error> {
469+ if Arc::ptr_eq(&image.shared, &self.shared) {
470+ Ok(())
471+ } else {
472+ Err(Error::ForeignTexture)
473+ }
474+ }
475+}
476+
477+impl Drop for VulkanRenderer {
478+ fn drop(&mut self) {
479+ let d = &self.shared.device.device;
480+ unsafe {
481+ let _ = d.device_wait_idle();
482+ for p in self.pending.drain(..) {
483+ d.free_command_buffers(self.pool, &[p.cmd]);
484+ }
485+ for s in self.samplers {
486+ d.destroy_sampler(s, None);
487+ }
488+ d.destroy_command_pool(self.pool, None);
489+ }
490+ }
491+}
492+
493+fn sampler_filter(bit: usize) -> TextureFilter {
494+ if bit == 0 {
495+ TextureFilter::Linear
496+ } else {
497+ TextureFilter::Nearest
498+ }
499+}
500+
501+/// A full barrier between all earlier and later work on the image. Coarse,
502+/// but correct for every transition the renderer makes.
503+unsafe fn barrier(
504+ d: &ash::Device,
505+ cmd: vk::CommandBuffer,
506+ image: vk::Image,
507+ old: vk::ImageLayout,
508+ new: vk::ImageLayout,
509+) {
510+ let b = [vk::ImageMemoryBarrier2::default()
511+ .src_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS)
512+ .src_access_mask(vk::AccessFlags2::MEMORY_WRITE)
513+ .dst_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS)
514+ .dst_access_mask(vk::AccessFlags2::MEMORY_READ | vk::AccessFlags2::MEMORY_WRITE)
515+ .old_layout(old)
516+ .new_layout(new)
517+ .image(image)
518+ .subresource_range(color_range())];
519+ unsafe {
520+ d.cmd_pipeline_barrier2(
521+ cmd,
522+ &vk::DependencyInfo::default().image_memory_barriers(&b),
523+ )
524+ };
525+}
526+
527+/// Whether reading `from` as `to` needs red and blue swapped.
528+fn needs_swizzle(from: Fourcc, to: Fourcc) -> Result<bool, Error> {
529+ let bgr = |f: Fourcc| match f {
530+ Fourcc::Argb8888 | Fourcc::Xrgb8888 => Ok(true),
531+ Fourcc::Abgr8888 | Fourcc::Xbgr8888 => Ok(false),
532+ f => Err(Error::UnsupportedFormat(f)),
533+ };
534+ Ok(bgr(from)? != bgr(to)?)
535+}
536+
537+impl RendererSuper for VulkanRenderer {
538+ type Error = Error;
539+ type TextureId = VulkanTexture;
540+ type Framebuffer<'buffer> = VulkanTarget<'buffer>;
541+ type Frame<'frame, 'buffer>
542+ = VulkanFrame<'frame, 'buffer>
543+ where
544+ 'buffer: 'frame,
545+ Self: 'frame;
546+}
547+
548+impl Renderer for VulkanRenderer {
549+ fn context_id(&self) -> ContextId<VulkanTexture> {
550+ self.context.clone()
551+ }
552+
553+ fn downscale_filter(&mut self, filter: TextureFilter) -> Result<(), Error> {
554+ self.min_filter = filter;
555+ Ok(())
556+ }
557+
558+ fn upscale_filter(&mut self, filter: TextureFilter) -> Result<(), Error> {
559+ self.mag_filter = filter;
560+ Ok(())
561+ }
562+
563+ fn set_debug_flags(&mut self, flags: DebugFlags) {
564+ self.debug = flags;
565+ }
566+
567+ fn debug_flags(&self) -> DebugFlags {
568+ self.debug
569+ }
570+
571+ fn render<'frame, 'buffer>(
572+ &'frame mut self,
573+ framebuffer: &'frame mut VulkanTarget<'buffer>,
574+ output_size: Size<i32, Physical>,
575+ dst_transform: Transform,
576+ ) -> Result<VulkanFrame<'frame, 'buffer>, Error>
577+ where
578+ 'buffer: 'frame,
579+ {
580+ self.own(&framebuffer.0.0)?;
581+ self.reap();
582+ Ok(VulkanFrame::new(
583+ self,
584+ framebuffer,
585+ output_size,
586+ dst_transform,
587+ ))
588+ }
589+
590+ fn wait(&mut self, sync: &SyncPoint) -> Result<(), Error> {
591+ sync.wait().map_err(|_| Error::Interrupted)
592+ }
593+
594+ fn cleanup_texture_cache(&mut self) -> Result<(), Error> {
595+ self.reap();
596+ Ok(())
597+ }
598+}
599+
600+impl Bind<VulkanImage> for VulkanRenderer {
601+ fn bind<'a>(&mut self, target: &'a mut VulkanImage) -> Result<VulkanTarget<'a>, Error> {
602+ self.own(&target.0)?;
603+ Ok(VulkanTarget(target))
604+ }
605+}
606+
607+impl Offscreen<VulkanImage> for VulkanRenderer {
608+ fn create_buffer(
609+ &mut self,
610+ format: Fourcc,
611+ size: Size<i32, Buffer>,
612+ ) -> Result<VulkanImage, Error> {
613+ if size.w <= 0 || size.h <= 0 {
614+ return Err(Error::InvalidSize(size.w, size.h));
615+ }
616+ let usage = vk::ImageUsageFlags::COLOR_ATTACHMENT
617+ | vk::ImageUsageFlags::SAMPLED
618+ | vk::ImageUsageFlags::TRANSFER_SRC
619+ | vk::ImageUsageFlags::TRANSFER_DST;
620+ Ok(VulkanImage(ImageInner::create(
621+ &self.shared,
622+ format,
623+ size.w as u32,
624+ size.h as u32,
625+ usage,
626+ )?))
627+ }
628+}
629+
630+impl ImportMem for VulkanRenderer {
631+ fn import_memory(
632+ &mut self,
633+ data: &[u8],
634+ format: Fourcc,
635+ size: Size<i32, Buffer>,
636+ flipped: bool,
637+ ) -> Result<VulkanTexture, Error> {
638+ if size.w <= 0 || size.h <= 0 {
639+ return Err(Error::InvalidSize(size.w, size.h));
640+ }
641+ let usage = vk::ImageUsageFlags::SAMPLED
642+ | vk::ImageUsageFlags::TRANSFER_DST
643+ | vk::ImageUsageFlags::TRANSFER_SRC;
644+ let image = ImageInner::create(&self.shared, format, size.w as u32, size.h as u32, usage)?;
645+ self.upload(&image, data, Rectangle::from_size(size), flipped)?;
646+ Ok(VulkanTexture(image))
647+ }
648+
649+ fn update_memory(
650+ &mut self,
651+ texture: &VulkanTexture,
652+ data: &[u8],
653+ region: Rectangle<i32, Buffer>,
654+ ) -> Result<(), Error> {
655+ self.own(&texture.0)?;
656+ let Some(region) = region.intersection(Rectangle::from_size(texture.size())) else {
657+ return Ok(());
658+ };
659+ self.upload(&texture.0, data, region, false)
660+ }
661+
662+ fn mem_formats(&self) -> Box<dyn Iterator<Item = Fourcc>> {
663+ Box::new(SUPPORTED.into_iter())
664+ }
665+}
666+
667+impl ExportMem for VulkanRenderer {
668+ type TextureMapping = VulkanMapping;
669+
670+ fn copy_framebuffer(
671+ &mut self,
672+ target: &VulkanTarget<'_>,
673+ region: Rectangle<i32, Buffer>,
674+ format: Fourcc,
675+ ) -> Result<VulkanMapping, Error> {
676+ self.own(&target.0.0)?;
677+ let image = target.0.0.clone();
678+ self.read_back(&image, region, format)
679+ }
680+
681+ fn copy_texture(
682+ &mut self,
683+ texture: &VulkanTexture,
684+ region: Rectangle<i32, Buffer>,
685+ format: Fourcc,
686+ ) -> Result<VulkanMapping, Error> {
687+ self.own(&texture.0)?;
688+ self.read_back(&texture.0, region, format)
689+ }
690+
691+ fn can_read_texture(&mut self, _texture: &VulkanTexture) -> Result<bool, Error> {
692+ Ok(true)
693+ }
694+
695+ fn map_texture<'a>(&mut self, mapping: &'a VulkanMapping) -> Result<&'a [u8], Error> {
696+ Ok(&mapping.data)
697+ }
698+}
Added file: crates/render-vk/src/sync.rs+71 additions
@@ -0,0 +1,71 @@
1+use crate::device::Shared;
2+use ash::vk;
3+use smithay::backend::renderer::sync::{Fence, Interrupted};
4+use std::fmt;
5+use std::sync::Arc;
6+
7+pub(crate) struct FenceInner {
8+ pub shared: Arc<Shared>,
9+ pub fence: vk::Fence,
10+}
11+
12+impl FenceInner {
13+ pub fn new(shared: &Arc<Shared>) -> Result<Arc<Self>, vk::Result> {
14+ let fence = unsafe {
15+ shared
16+ .device
17+ .device
18+ .create_fence(&vk::FenceCreateInfo::default(), None)?
19+ };
20+ Ok(Arc::new(Self {
21+ shared: shared.clone(),
22+ fence,
23+ }))
24+ }
25+
26+ pub fn signaled(&self) -> bool {
27+ unsafe { self.shared.device.device.get_fence_status(self.fence) }.unwrap_or(true)
28+ }
29+
30+ pub fn wait(&self) -> Result<(), vk::Result> {
31+ unsafe {
32+ self.shared
33+ .device
34+ .device
35+ .wait_for_fences(&[self.fence], true, u64::MAX)
36+ }
37+ }
38+}
39+
40+impl Drop for FenceInner {
41+ fn drop(&mut self) {
42+ unsafe { self.shared.device.device.destroy_fence(self.fence, None) };
43+ }
44+}
45+
46+impl fmt::Debug for FenceInner {
47+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48+ f.debug_tuple("FenceInner").field(&self.fence).finish()
49+ }
50+}
51+
52+#[derive(Debug)]
53+pub(crate) struct VkFence(pub Arc<FenceInner>);
54+
55+impl Fence for VkFence {
56+ fn is_signaled(&self) -> bool {
57+ self.0.signaled()
58+ }
59+
60+ fn wait(&self) -> Result<(), Interrupted> {
61+ self.0.wait().map_err(|_| Interrupted)
62+ }
63+
64+ fn is_exportable(&self) -> bool {
65+ false
66+ }
67+
68+ fn export(&self) -> Option<std::os::fd::OwnedFd> {
69+ None
70+ }
71+}
Added file: crates/render-vk/src/transform.rs+114 additions
@@ -0,0 +1,114 @@
1+use glam::{Affine2, Vec2};
2+use smithay::utils::{Buffer, Physical, Point, Rectangle, Size, Transform};
3+
4+/// Texture coordinates at the destination's corners (TL, TR, BL, BR), using
5+/// the same mapping as smithay's GLES renderer.
6+pub(crate) fn texture_uvs(
7+ src: Rectangle<f64, Buffer>,
8+ dst: Size<i32, Physical>,
9+ texture: Size<i32, Buffer>,
10+ transform: Transform,
11+) -> [[f32; 2]; 4] {
12+ let dst_src = transform.transform_size(src.size);
13+ let scale = Vec2::new(
14+ (dst_src.w / f64::from(dst.w)) as f32,
15+ (dst_src.h / f64::from(dst.h)) as f32,
16+ );
17+ let (w, h) = (dst_src.w as f32, dst_src.h as f32);
18+ let translation = match transform {
19+ Transform::Normal | Transform::Flipped90 => Vec2::ZERO,
20+ Transform::_90 => Vec2::new(0.0, w),
21+ Transform::_180 => Vec2::new(w, h),
22+ Transform::_270 => Vec2::new(h, 0.0),
23+ Transform::Flipped => Vec2::new(w, 0.0),
24+ Transform::Flipped180 => Vec2::new(0.0, h),
25+ Transform::Flipped270 => Vec2::new(h, w),
26+ };
27+ let mat = Affine2::from_scale(Vec2::new(1.0 / texture.w as f32, 1.0 / texture.h as f32))
28+ * Affine2::from_translation(Vec2::new(src.loc.x as f32, src.loc.y as f32))
29+ * Affine2::from_translation(translation)
30+ * transform.matrix()
31+ * Affine2::from_scale(scale);
32+ let (dw, dh) = (dst.w as f32, dst.h as f32);
33+ [
34+ Vec2::ZERO,
35+ Vec2::new(dw, 0.0),
36+ Vec2::new(0.0, dh),
37+ Vec2::new(dw, dh),
38+ ]
39+ .map(|p| mat.transform_point2(p).to_array())
40+}
41+
42+/// Maps a destination in output space into the framebuffer, permuting the
43+/// per-corner texture coordinates to follow it.
44+pub(crate) fn to_framebuffer(
45+ dst: Rectangle<i32, Physical>,
46+ uvs: [[f32; 2]; 4],
47+ output: Size<i32, Physical>,
48+ transform: Transform,
49+) -> (Rectangle<i32, Physical>, [[f32; 2]; 4]) {
50+ if transform == Transform::Normal {
51+ return (dst, uvs);
52+ }
53+ let fb = transform.transform_rect_in(dst, &output);
54+ let area = output.to_f64();
55+ let (x0, y0) = (f64::from(dst.loc.x), f64::from(dst.loc.y));
56+ let (x1, y1) = (x0 + f64::from(dst.size.w), y0 + f64::from(dst.size.h));
57+ let cx = f64::from(fb.loc.x) + f64::from(fb.size.w) / 2.0;
58+ let cy = f64::from(fb.loc.y) + f64::from(fb.size.h) / 2.0;
59+ let mut out = uvs;
60+ for (corner, uv) in [(x0, y0), (x1, y0), (x0, y1), (x1, y1)]
61+ .into_iter()
62+ .zip(uvs)
63+ {
64+ let p = transform.transform_point_in(Point::<f64, Physical>::from(corner), &area);
65+ let slot = usize::from(p.x > cx) | (usize::from(p.y > cy) << 1);
66+ out[slot] = uv;
67+ }
68+ (fb, out)
69+}
70+
71+/// Maps a rectangle in output space into the framebuffer.
72+pub(crate) fn rect_to_framebuffer(
73+ rect: Rectangle<i32, Physical>,
74+ output: Size<i32, Physical>,
75+ transform: Transform,
76+) -> Rectangle<i32, Physical> {
77+ if transform == Transform::Normal {
78+ rect
79+ } else {
80+ transform.transform_rect_in(rect, &output)
81+ }
82+}
83+
84+#[cfg(test)]
85+mod tests {
86+ use super::*;
87+
88+ #[test]
89+ fn identity_covers_the_whole_texture() {
90+ let uv = texture_uvs(
91+ Rectangle::from_size((4.0, 2.0).into()),
92+ (8, 4).into(),
93+ (4, 2).into(),
94+ Transform::Normal,
95+ );
96+ assert_eq!(uv, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]);
97+ }
98+
99+ #[test]
100+ fn crop_selects_part_of_the_texture() {
101+ let src = Rectangle::new((2.0, 0.0).into(), (2.0, 2.0).into());
102+ let uv = texture_uvs(src, (2, 2).into(), (4, 2).into(), Transform::Normal);
103+ assert_eq!(uv, [[0.5, 0.0], [1.0, 0.0], [0.5, 1.0], [1.0, 1.0]]);
104+ }
105+
106+ #[test]
107+ fn rotation_permutes_corners_without_moving_identity_outputs() {
108+ let dst = Rectangle::new((0, 0).into(), (10, 10).into());
109+ let uvs = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
110+ let (fb, out) = to_framebuffer(dst, uvs, (10, 10).into(), Transform::_180);
111+ assert_eq!(fb, dst);
112+ assert_eq!(out, [[1.0, 1.0], [0.0, 1.0], [1.0, 0.0], [0.0, 0.0]]);
113+ }
114+}
Added file: crates/render-vk/tests/golden.rs+258 additions
@@ -0,0 +1,258 @@
1+//! Renders small scenes offscreen and checks pixels. Runs on whatever Vulkan
2+//! driver is available. `nix flake check` forces lavapipe.
3+
4+use monolith_render_vk::{VulkanImage, VulkanRenderer, VulkanTexture};
5+use smithay::backend::allocator::Fourcc;
6+use smithay::backend::renderer::{
7+ Bind, Color32F, ExportMem, Frame, ImportMem, Offscreen, Renderer, TextureFilter,
8+};
9+use smithay::utils::{Physical, Rectangle, Size, Transform};
10+
11+const W: i32 = 64;
12+const H: i32 = 64;
13+
14+type Pixel = [u8; 4];
15+
16+const RED: Pixel = [255, 0, 0, 255];
17+const GREEN: Pixel = [0, 255, 0, 255];
18+const BLUE: Pixel = [0, 0, 255, 255];
19+const WHITE: Pixel = [255, 255, 255, 255];
20+const BLACK: Pixel = [0, 0, 0, 255];
21+
22+struct Scene {
23+ renderer: VulkanRenderer,
24+ image: VulkanImage,
25+}
26+
27+impl Scene {
28+ fn new() -> Self {
29+ let mut renderer = VulkanRenderer::new().expect("no usable Vulkan device");
30+ let image = renderer
31+ .create_buffer(Fourcc::Argb8888, (W, H).into())
32+ .unwrap();
33+ Self { renderer, image }
34+ }
35+
36+ fn draw(
37+ &mut self,
38+ transform: Transform,
39+ f: impl FnOnce(&mut monolith_render_vk::VulkanFrame<'_, '_>),
40+ ) {
41+ let mut target = self.renderer.bind(&mut self.image).unwrap();
42+ let mut frame = self
43+ .renderer
44+ .render(&mut target, (W, H).into(), transform)
45+ .unwrap();
46+ f(&mut frame);
47+ frame.finish().unwrap().wait().unwrap();
48+ }
49+
50+ /// Pixels as RGBA, whatever the image's byte order.
51+ fn pixels(&mut self) -> Vec<Pixel> {
52+ let target = self.renderer.bind(&mut self.image).unwrap();
53+ let mapping = self
54+ .renderer
55+ .copy_framebuffer(
56+ &target,
57+ Rectangle::from_size((W, H).into()),
58+ Fourcc::Abgr8888,
59+ )
60+ .unwrap();
61+ self.renderer
62+ .map_texture(&mapping)
63+ .unwrap()
64+ .as_chunks::<4>()
65+ .0
66+ .to_vec()
67+ }
68+
69+ fn texture(&mut self, size: (i32, i32), rgba: &[Pixel]) -> VulkanTexture {
70+ let bytes: Vec<u8> = rgba.iter().flatten().copied().collect();
71+ self.renderer
72+ .import_memory(&bytes, Fourcc::Abgr8888, size.into(), false)
73+ .unwrap()
74+ }
75+}
76+
77+fn at(pixels: &[Pixel], x: i32, y: i32) -> Pixel {
78+ pixels[(y * W + x) as usize]
79+}
80+
81+fn full() -> Rectangle<i32, Physical> {
82+ Rectangle::from_size((W, H).into())
83+}
84+
85+fn rgba(p: Pixel) -> Color32F {
86+ p.map(|c| f32::from(c) / 255.0).into()
87+}
88+
89+#[test]
90+fn clears_to_a_colour() {
91+ let mut s = Scene::new();
92+ s.draw(Transform::Normal, |f| {
93+ f.clear(rgba(RED), &[full()]).unwrap()
94+ });
95+ let px = s.pixels();
96+ assert!(px.iter().all(|&p| p == RED));
97+}
98+
99+#[test]
100+fn solid_fill_respects_its_rectangle() {
101+ let mut s = Scene::new();
102+ s.draw(Transform::Normal, |f| {
103+ f.clear(rgba(BLACK), &[full()]).unwrap();
104+ let dst = Rectangle::new((16, 16).into(), (32, 32).into());
105+ f.draw_solid(dst, &[Rectangle::from_size(dst.size)], rgba(GREEN))
106+ .unwrap();
107+ });
108+ let px = s.pixels();
109+ assert_eq!(at(&px, 16, 16), GREEN);
110+ assert_eq!(at(&px, 47, 47), GREEN);
111+ assert_eq!(at(&px, 15, 16), BLACK);
112+ assert_eq!(at(&px, 48, 47), BLACK);
113+}
114+
115+#[test]
116+fn damage_limits_drawing() {
117+ let mut s = Scene::new();
118+ s.draw(Transform::Normal, |f| {
119+ f.clear(rgba(BLACK), &[full()]).unwrap();
120+ let damage = Rectangle::new((0, 0).into(), (8, 8).into());
121+ f.draw_solid(full(), &[damage], rgba(BLUE)).unwrap();
122+ });
123+ let px = s.pixels();
124+ assert_eq!(at(&px, 7, 7), BLUE);
125+ assert_eq!(at(&px, 8, 8), BLACK);
126+}
127+
128+#[test]
129+fn partial_clear_keeps_previous_contents() {
130+ let mut s = Scene::new();
131+ s.draw(Transform::Normal, |f| {
132+ f.clear(rgba(RED), &[full()]).unwrap()
133+ });
134+ s.draw(Transform::Normal, |f| {
135+ f.clear(
136+ rgba(GREEN),
137+ &[Rectangle::new((0, 0).into(), (32, 64).into())],
138+ )
139+ .unwrap()
140+ });
141+ let px = s.pixels();
142+ assert_eq!(at(&px, 0, 0), GREEN);
143+ assert_eq!(at(&px, 63, 0), RED);
144+}
145+
146+#[test]
147+fn textures_sample_with_nearest_filtering() {
148+ let mut s = Scene::new();
149+ s.renderer.upscale_filter(TextureFilter::Nearest).unwrap();
150+ let tex = s.texture((2, 2), &[RED, GREEN, BLUE, WHITE]);
151+ s.draw(Transform::Normal, |f| {
152+ f.render_texture_from_to(
153+ &tex,
154+ Rectangle::from_size((2.0, 2.0).into()),
155+ full(),
156+ &[full()],
157+ &[],
158+ Transform::Normal,
159+ 1.0,
160+ )
161+ .unwrap();
162+ });
163+ let px = s.pixels();
164+ assert_eq!(at(&px, 8, 8), RED);
165+ assert_eq!(at(&px, 56, 8), GREEN);
166+ assert_eq!(at(&px, 8, 56), BLUE);
167+ assert_eq!(at(&px, 56, 56), WHITE);
168+}
169+
170+#[test]
171+fn alpha_blends_premultiplied() {
172+ let mut s = Scene::new();
173+ let tex = s.texture((1, 1), &[WHITE]);
174+ s.draw(Transform::Normal, |f| {
175+ f.clear(rgba(BLACK), &[full()]).unwrap();
176+ f.render_texture_from_to(
177+ &tex,
178+ Rectangle::from_size((1.0, 1.0).into()),
179+ full(),
180+ &[full()],
181+ &[],
182+ Transform::Normal,
183+ 0.5,
184+ )
185+ .unwrap();
186+ });
187+ let p = at(&s.pixels(), 32, 32);
188+ assert!(
189+ (127..=129).contains(&p[0]) && p[0] == p[1] && p[1] == p[2],
190+ "{p:?}"
191+ );
192+}
193+
194+#[test]
195+fn output_transform_moves_content() {
196+ let mut s = Scene::new();
197+ s.draw(Transform::_180, |f| {
198+ f.clear(rgba(BLACK), &[full()]).unwrap();
199+ let dst = Rectangle::new((0, 0).into(), (16, 16).into());
200+ f.draw_solid(dst, &[Rectangle::from_size(dst.size)], rgba(GREEN))
201+ .unwrap();
202+ });
203+ let px = s.pixels();
204+ assert_eq!(at(&px, 0, 0), BLACK);
205+ assert_eq!(at(&px, 63, 63), GREEN);
206+}
207+
208+#[test]
209+fn offscreen_images_can_be_sampled() {
210+ let mut s = Scene::new();
211+ let mut inner = s
212+ .renderer
213+ .create_buffer(Fourcc::Argb8888, Size::from((4, 4)))
214+ .unwrap();
215+ {
216+ let mut t = s.renderer.bind(&mut inner).unwrap();
217+ let mut f = s
218+ .renderer
219+ .render(&mut t, (4, 4).into(), Transform::Normal)
220+ .unwrap();
221+ f.clear(rgba(BLUE), &[Rectangle::from_size((4, 4).into())])
222+ .unwrap();
223+ f.finish().unwrap().wait().unwrap();
224+ }
225+ let tex = inner.texture();
226+ s.draw(Transform::Normal, |f| {
227+ f.render_texture_from_to(
228+ &tex,
229+ Rectangle::from_size((4.0, 4.0).into()),
230+ full(),
231+ &[full()],
232+ &[],
233+ Transform::Normal,
234+ 1.0,
235+ )
236+ .unwrap();
237+ });
238+ assert_eq!(at(&s.pixels(), 32, 32), BLUE);
239+}
240+
241+#[test]
242+fn reports_the_device() {
243+ let r = VulkanRenderer::new().unwrap();
244+ eprintln!("rendering on {}", r.device_name());
245+ assert!(!r.device_name().is_empty());
246+}
247+
248+#[test]
249+fn solid_fill_in_the_corner_keeps_its_colour() {
250+ let mut s = Scene::new();
251+ s.draw(Transform::Normal, |f| {
252+ f.clear(rgba(BLACK), &[full()]).unwrap();
253+ let dst = Rectangle::new((48, 48).into(), (16, 16).into());
254+ f.draw_solid(dst, &[Rectangle::from_size(dst.size)], rgba(RED))
255+ .unwrap();
256+ });
257+ assert_eq!(at(&s.pixels(), 56, 56), RED);
258+}
Modified file: flake.nix+10 additions−2 deletions
@@ -22,14 +22,21 @@
2222 pkgs:
2323 let
2424 craneLib = crane.mkLib pkgs;
25- src = craneLib.cleanCargoSource ./.;
25+ src = lib.cleanSourceWith {
26+ src = ./.;
27+ filter = path: type: (craneLib.filterCargoSources path type) || lib.hasInfix "/shaders" path;
28+ name = "source";
29+ };
2630 lavapipeIcd = "${pkgs.mesa}/share/vulkan/icd.d/lvp_icd.${pkgs.stdenv.hostPlatform.parsed.cpu.name}.json";
2731 commonArgs = {
2832 inherit src;
2933 pname = "monolith";
3034 version = "0.0.0";
3135 strictDeps = true;
32- nativeBuildInputs = [ pkgs.pkg-config ];
36+ nativeBuildInputs = [
37+ pkgs.pkg-config
38+ pkgs.glslang
39+ ];
3340 buildInputs = with pkgs; [
3441 libxkbcommon
3542 vulkan-loader
@@ -62,6 +69,7 @@
6269 // {
6370 inherit cargoArtifacts;
6471 VK_DRIVER_FILES = lavapipeIcd;
72+ LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.vulkan-loader ];
6573 }
6674 );
6775 };