| 1 | use crate::Error; |
| 2 | use crate::device::Shared; |
| 3 | use crate::format::{SUPPORTED, vk_format}; |
| 4 | use ash::vk; |
| 5 | use std::collections::HashMap; |
| 6 | use std::path::PathBuf; |
| 7 | use std::sync::Arc; |
| 8 | use std::sync::atomic::{AtomicU32, Ordering}; |
| 9 | use std::time::Instant; |
| 10 | |
| 11 | /// Push constants shared by every shader, see `shaders/push.glsl`. |
| 12 | /// `dst` is the quad in normalized device coordinates as (x, y, w, h), |
| 13 | /// `uv` the texture coordinate at each corner (TL, TR, BL, BR). |
| 14 | #[repr(C)] |
| 15 | #[derive(Clone, Copy, Debug, Default)] |
| 16 | pub(crate) struct Push { |
| 17 | pub color: [f32; 4], |
| 18 | pub dst: [f32; 4], |
| 19 | pub uv: [[f32; 2]; 4], |
| 20 | /// Transition progress and kind, read only by `shaders/effect.frag`. |
| 21 | pub effect: [f32; 4], |
| 22 | /// See `shaders/clip.glsl`. A zero width turns clipping off. |
| 23 | pub clip: [f32; 4], |
| 24 | pub radii: [f32; 4], |
| 25 | pub ring: [f32; 4], |
| 26 | } |
| 27 | |
| 28 | impl Push { |
| 29 | pub fn bytes(&self) -> &[u8] { |
| 30 | // SAFETY: `Push` is `repr(C)` and all f32, so it has no padding bytes. |
| 31 | unsafe { std::slice::from_raw_parts((self as *const Self).cast::<u8>(), size_of::<Self>()) } |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] |
| 36 | pub(crate) enum Kind { |
| 37 | Solid, |
| 38 | Texture, |
| 39 | Effect, |
| 40 | } |
| 41 | |
| 42 | pub(crate) struct Pipelines { |
| 43 | shared: Arc<Shared>, |
| 44 | pub set_layout: vk::DescriptorSetLayout, |
| 45 | pub layout: vk::PipelineLayout, |
| 46 | vert: vk::ShaderModule, |
| 47 | solid: vk::ShaderModule, |
| 48 | texture: vk::ShaderModule, |
| 49 | effect: vk::ShaderModule, |
| 50 | cache: HashMap<(vk::Format, Kind, bool), vk::Pipeline>, |
| 51 | vk_cache: vk::PipelineCache, |
| 52 | cache_path: Option<PathBuf>, |
| 53 | loaded_len: usize, |
| 54 | } |
| 55 | |
| 56 | const QUAD_VERT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/quad.vert.spv")); |
| 57 | const SOLID_FRAG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/solid.frag.spv")); |
| 58 | const TEXTURE_FRAG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/texture.frag.spv")); |
| 59 | const EFFECT_FRAG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/effect.frag.spv")); |
| 60 | |
| 61 | impl Pipelines { |
| 62 | pub fn new(shared: &Arc<Shared>) -> Result<Self, Error> { |
| 63 | let d = &shared.device.device; |
| 64 | let bindings = [vk::DescriptorSetLayoutBinding::default() |
| 65 | .binding(0) |
| 66 | .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) |
| 67 | .descriptor_count(1) |
| 68 | .stage_flags(vk::ShaderStageFlags::FRAGMENT)]; |
| 69 | let set_layout = unsafe { |
| 70 | d.create_descriptor_set_layout( |
| 71 | &vk::DescriptorSetLayoutCreateInfo::default() |
| 72 | .flags(vk::DescriptorSetLayoutCreateFlags::PUSH_DESCRIPTOR_KHR) |
| 73 | .bindings(&bindings), |
| 74 | None, |
| 75 | )? |
| 76 | }; |
| 77 | let ranges = [vk::PushConstantRange::default() |
| 78 | .stage_flags(vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT) |
| 79 | .size(size_of::<Push>() as u32)]; |
| 80 | let set_layouts = [set_layout]; |
| 81 | let layout = unsafe { |
| 82 | d.create_pipeline_layout( |
| 83 | &vk::PipelineLayoutCreateInfo::default() |
| 84 | .set_layouts(&set_layouts) |
| 85 | .push_constant_ranges(&ranges), |
| 86 | None, |
| 87 | )? |
| 88 | }; |
| 89 | let module = |code: &[u8]| -> Result<vk::ShaderModule, Error> { |
| 90 | let words = ash::util::read_spv(&mut std::io::Cursor::new(code)) |
| 91 | .map_err(|_| vk::Result::ERROR_INVALID_SHADER_NV)?; |
| 92 | Ok(unsafe { |
| 93 | d.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&words), None)? |
| 94 | }) |
| 95 | }; |
| 96 | let cache_path = cache_path(); |
| 97 | let props = unsafe { |
| 98 | shared |
| 99 | .device |
| 100 | .instance |
| 101 | .get_physical_device_properties(shared.device.physical) |
| 102 | }; |
| 103 | let loaded = cache_path |
| 104 | .as_ref() |
| 105 | .and_then(|p| std::fs::read(p).ok()) |
| 106 | .filter(|data| header_matches(data, &props)) |
| 107 | .unwrap_or_default(); |
| 108 | let create = |data: &[u8]| unsafe { |
| 109 | d.create_pipeline_cache( |
| 110 | &vk::PipelineCacheCreateInfo::default().initial_data(data), |
| 111 | None, |
| 112 | ) |
| 113 | }; |
| 114 | let vk_cache = create(&loaded).or_else(|_| create(&[])).unwrap_or_default(); |
| 115 | Ok(Self { |
| 116 | shared: shared.clone(), |
| 117 | set_layout, |
| 118 | layout, |
| 119 | vert: module(QUAD_VERT)?, |
| 120 | solid: module(SOLID_FRAG)?, |
| 121 | texture: module(TEXTURE_FRAG)?, |
| 122 | effect: module(EFFECT_FRAG)?, |
| 123 | cache: HashMap::new(), |
| 124 | vk_cache, |
| 125 | cache_path, |
| 126 | loaded_len: loaded.len(), |
| 127 | }) |
| 128 | } |
| 129 | |
| 130 | /// Compiles the pipelines a frame draws with for every render format, so |
| 131 | /// the first spawn animation does not stall on the driver compiler. |
| 132 | pub fn warm(&mut self) { |
| 133 | let began = Instant::now(); |
| 134 | for fourcc in SUPPORTED { |
| 135 | let Some(format) = vk_format(fourcc) else { |
| 136 | continue; |
| 137 | }; |
| 138 | for (kind, opaque) in [ |
| 139 | (Kind::Solid, false), |
| 140 | (Kind::Solid, true), |
| 141 | (Kind::Texture, false), |
| 142 | (Kind::Texture, true), |
| 143 | (Kind::Effect, false), |
| 144 | ] { |
| 145 | if let Err(e) = self.get(format.format, kind, opaque) { |
| 146 | tracing::warn!(?kind, opaque, "cannot warm a pipeline: {e}"); |
| 147 | return; |
| 148 | } |
| 149 | } |
| 150 | } |
| 151 | tracing::debug!(elapsed = ?began.elapsed(), "warmed pipelines"); |
| 152 | } |
| 153 | |
| 154 | fn save_cache(&self) { |
| 155 | let Some(path) = &self.cache_path else { |
| 156 | return; |
| 157 | }; |
| 158 | if self.vk_cache == vk::PipelineCache::null() { |
| 159 | return; |
| 160 | } |
| 161 | let Ok(data) = (unsafe { |
| 162 | self.shared |
| 163 | .device |
| 164 | .device |
| 165 | .get_pipeline_cache_data(self.vk_cache) |
| 166 | }) else { |
| 167 | return; |
| 168 | }; |
| 169 | if data.len() == self.loaded_len { |
| 170 | return; |
| 171 | } |
| 172 | static WRITERS: AtomicU32 = AtomicU32::new(0); |
| 173 | let tmp = path.with_extension(format!( |
| 174 | "{}.{}.tmp", |
| 175 | std::process::id(), |
| 176 | WRITERS.fetch_add(1, Ordering::Relaxed) |
| 177 | )); |
| 178 | let written = path |
| 179 | .parent() |
| 180 | .map_or(Ok(()), std::fs::create_dir_all) |
| 181 | .and_then(|()| std::fs::write(&tmp, &data)) |
| 182 | .and_then(|()| std::fs::rename(&tmp, path)); |
| 183 | if let Err(e) = written { |
| 184 | tracing::debug!("cannot save the pipeline cache: {e}"); |
| 185 | let _ = std::fs::remove_file(&tmp); |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | /// The pipeline for `kind` into `format`. An `opaque` one replaces the |
| 190 | /// destination instead of blending, for draws known to cover it. |
| 191 | pub fn get( |
| 192 | &mut self, |
| 193 | format: vk::Format, |
| 194 | kind: Kind, |
| 195 | opaque: bool, |
| 196 | ) -> Result<vk::Pipeline, Error> { |
| 197 | if let Some(&p) = self.cache.get(&(format, kind, opaque)) { |
| 198 | return Ok(p); |
| 199 | } |
| 200 | let d = &self.shared.device.device; |
| 201 | let frag = match kind { |
| 202 | Kind::Solid => self.solid, |
| 203 | Kind::Texture => self.texture, |
| 204 | Kind::Effect => self.effect, |
| 205 | }; |
| 206 | let stages = [ |
| 207 | vk::PipelineShaderStageCreateInfo::default() |
| 208 | .stage(vk::ShaderStageFlags::VERTEX) |
| 209 | .module(self.vert) |
| 210 | .name(c"main"), |
| 211 | vk::PipelineShaderStageCreateInfo::default() |
| 212 | .stage(vk::ShaderStageFlags::FRAGMENT) |
| 213 | .module(frag) |
| 214 | .name(c"main"), |
| 215 | ]; |
| 216 | let vertex = vk::PipelineVertexInputStateCreateInfo::default(); |
| 217 | let assembly = vk::PipelineInputAssemblyStateCreateInfo::default() |
| 218 | .topology(vk::PrimitiveTopology::TRIANGLE_STRIP); |
| 219 | let viewport = vk::PipelineViewportStateCreateInfo::default() |
| 220 | .viewport_count(1) |
| 221 | .scissor_count(1); |
| 222 | let raster = vk::PipelineRasterizationStateCreateInfo::default() |
| 223 | .polygon_mode(vk::PolygonMode::FILL) |
| 224 | .cull_mode(vk::CullModeFlags::NONE) |
| 225 | .line_width(1.0); |
| 226 | let multisample = vk::PipelineMultisampleStateCreateInfo::default() |
| 227 | .rasterization_samples(vk::SampleCountFlags::TYPE_1); |
| 228 | let attachments = [vk::PipelineColorBlendAttachmentState::default() |
| 229 | .blend_enable(!opaque) |
| 230 | .src_color_blend_factor(vk::BlendFactor::ONE) |
| 231 | .dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA) |
| 232 | .color_blend_op(vk::BlendOp::ADD) |
| 233 | .src_alpha_blend_factor(vk::BlendFactor::ONE) |
| 234 | .dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA) |
| 235 | .alpha_blend_op(vk::BlendOp::ADD) |
| 236 | .color_write_mask(vk::ColorComponentFlags::RGBA)]; |
| 237 | let blend = vk::PipelineColorBlendStateCreateInfo::default().attachments(&attachments); |
| 238 | let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]; |
| 239 | let dynamic = vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states); |
| 240 | let formats = [format]; |
| 241 | let mut rendering = |
| 242 | vk::PipelineRenderingCreateInfo::default().color_attachment_formats(&formats); |
| 243 | let info = vk::GraphicsPipelineCreateInfo::default() |
| 244 | .stages(&stages) |
| 245 | .vertex_input_state(&vertex) |
| 246 | .input_assembly_state(&assembly) |
| 247 | .viewport_state(&viewport) |
| 248 | .rasterization_state(&raster) |
| 249 | .multisample_state(&multisample) |
| 250 | .color_blend_state(&blend) |
| 251 | .dynamic_state(&dynamic) |
| 252 | .layout(self.layout) |
| 253 | .push_next(&mut rendering); |
| 254 | let pipeline = unsafe { d.create_graphics_pipelines(self.vk_cache, &[info], None) } |
| 255 | .map_err(|(_, e)| e)?[0]; |
| 256 | self.cache.insert((format, kind, opaque), pipeline); |
| 257 | Ok(pipeline) |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | impl Drop for Pipelines { |
| 262 | fn drop(&mut self) { |
| 263 | self.save_cache(); |
| 264 | let d = &self.shared.device.device; |
| 265 | unsafe { |
| 266 | for (_, p) in self.cache.drain() { |
| 267 | d.destroy_pipeline(p, None); |
| 268 | } |
| 269 | d.destroy_pipeline_cache(self.vk_cache, None); |
| 270 | d.destroy_shader_module(self.vert, None); |
| 271 | d.destroy_shader_module(self.solid, None); |
| 272 | d.destroy_shader_module(self.texture, None); |
| 273 | d.destroy_shader_module(self.effect, None); |
| 274 | d.destroy_pipeline_layout(self.layout, None); |
| 275 | d.destroy_descriptor_set_layout(self.set_layout, None); |
| 276 | } |
| 277 | } |
| 278 | } |
| 279 | |
| 280 | fn cache_path() -> Option<PathBuf> { |
| 281 | let base = std::env::var_os("XDG_CACHE_HOME") |
| 282 | .filter(|v| !v.is_empty()) |
| 283 | .map(PathBuf::from) |
| 284 | .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".cache")))?; |
| 285 | Some(base.join("monolith").join("pipelines.bin")) |
| 286 | } |
| 287 | |
| 288 | /// Whether `data` opens with a cache header written by this driver on this |
| 289 | /// device. The spec lets a driver reject a foreign cache but some crash on |
| 290 | /// one instead, so the header is checked first. |
| 291 | fn header_matches(data: &[u8], props: &vk::PhysicalDeviceProperties) -> bool { |
| 292 | let word = |at: usize| { |
| 293 | data.get(at..at + 4) |
| 294 | .and_then(|b| b.try_into().ok()) |
| 295 | .map(u32::from_ne_bytes) |
| 296 | }; |
| 297 | word(0).is_some_and(|size| size >= 32 && size as usize <= data.len()) |
| 298 | && word(4) == Some(vk::PipelineCacheHeaderVersion::ONE.as_raw() as u32) |
| 299 | && word(8) == Some(props.vendor_id) |
| 300 | && word(12) == Some(props.device_id) |
| 301 | && data.get(16..32) == Some(&props.pipeline_cache_uuid[..]) |
| 302 | } |
| 303 | |
| 304 | #[cfg(test)] |
| 305 | mod tests { |
| 306 | use super::*; |
| 307 | |
| 308 | fn props() -> vk::PhysicalDeviceProperties { |
| 309 | vk::PhysicalDeviceProperties { |
| 310 | vendor_id: 0x10de, |
| 311 | device_id: 0x2487, |
| 312 | pipeline_cache_uuid: [7; 16], |
| 313 | ..Default::default() |
| 314 | } |
| 315 | } |
| 316 | |
| 317 | fn header(vendor: u32, uuid: u8) -> Vec<u8> { |
| 318 | let words = [32u32, 1, vendor, 0x2487]; |
| 319 | let mut data: Vec<u8> = words.iter().flat_map(|w| w.to_ne_bytes()).collect(); |
| 320 | data.extend([uuid; 16]); |
| 321 | data |
| 322 | } |
| 323 | |
| 324 | #[test] |
| 325 | fn cache_header_must_match_the_device_and_driver() { |
| 326 | assert!(header_matches(&header(0x10de, 7), &props())); |
| 327 | assert!(!header_matches(&header(0x1002, 7), &props())); |
| 328 | assert!(!header_matches(&header(0x10de, 8), &props())); |
| 329 | assert!(!header_matches(&header(0x10de, 7)[..20], &props())); |
| 330 | assert!(!header_matches(&[], &props())); |
| 331 | } |
| 332 | } |