atagen

monolith

public
Star0
Watch0
9d1128e3c22797988dafcb931e07cd2a3ea9ad2f
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/render-vk/src/pipeline.rsRust338 lines12.4 KB
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
        self.save_cache();
153
    }
154
155
    fn save_cache(&mut self) {
156
        if crate::error::device_lost() {
157
            return;
158
        }
159
        let Some(path) = &self.cache_path else {
160
            return;
161
        };
162
        if self.vk_cache == vk::PipelineCache::null() {
163
            return;
164
        }
165
        let Ok(data) = (unsafe {
166
            self.shared
167
                .device
168
                .device
169
                .get_pipeline_cache_data(self.vk_cache)
170
        }) else {
171
            return;
172
        };
173
        if data.len() == self.loaded_len {
174
            return;
175
        }
176
        static WRITERS: AtomicU32 = AtomicU32::new(0);
177
        let tmp = path.with_extension(format!(
178
            "{}.{}.tmp",
179
            std::process::id(),
180
            WRITERS.fetch_add(1, Ordering::Relaxed)
181
        ));
182
        let written = path
183
            .parent()
184
            .map_or(Ok(()), std::fs::create_dir_all)
185
            .and_then(|()| std::fs::write(&tmp, &data))
186
            .and_then(|()| std::fs::rename(&tmp, path));
187
        if let Err(e) = written {
188
            tracing::debug!("cannot save the pipeline cache: {e}");
189
            let _ = std::fs::remove_file(&tmp);
190
        } else {
191
            self.loaded_len = data.len();
192
        }
193
    }
194
195
    /// The pipeline for `kind` into `format`. An `opaque` one replaces the
196
    /// destination instead of blending, for draws known to cover it.
197
    pub fn get(
198
        &mut self,
199
        format: vk::Format,
200
        kind: Kind,
201
        opaque: bool,
202
    ) -> Result<vk::Pipeline, Error> {
203
        if let Some(&p) = self.cache.get(&(format, kind, opaque)) {
204
            return Ok(p);
205
        }
206
        let d = &self.shared.device.device;
207
        let frag = match kind {
208
            Kind::Solid => self.solid,
209
            Kind::Texture => self.texture,
210
            Kind::Effect => self.effect,
211
        };
212
        let stages = [
213
            vk::PipelineShaderStageCreateInfo::default()
214
                .stage(vk::ShaderStageFlags::VERTEX)
215
                .module(self.vert)
216
                .name(c"main"),
217
            vk::PipelineShaderStageCreateInfo::default()
218
                .stage(vk::ShaderStageFlags::FRAGMENT)
219
                .module(frag)
220
                .name(c"main"),
221
        ];
222
        let vertex = vk::PipelineVertexInputStateCreateInfo::default();
223
        let assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
224
            .topology(vk::PrimitiveTopology::TRIANGLE_STRIP);
225
        let viewport = vk::PipelineViewportStateCreateInfo::default()
226
            .viewport_count(1)
227
            .scissor_count(1);
228
        let raster = vk::PipelineRasterizationStateCreateInfo::default()
229
            .polygon_mode(vk::PolygonMode::FILL)
230
            .cull_mode(vk::CullModeFlags::NONE)
231
            .line_width(1.0);
232
        let multisample = vk::PipelineMultisampleStateCreateInfo::default()
233
            .rasterization_samples(vk::SampleCountFlags::TYPE_1);
234
        let attachments = [vk::PipelineColorBlendAttachmentState::default()
235
            .blend_enable(!opaque)
236
            .src_color_blend_factor(vk::BlendFactor::ONE)
237
            .dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
238
            .color_blend_op(vk::BlendOp::ADD)
239
            .src_alpha_blend_factor(vk::BlendFactor::ONE)
240
            .dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
241
            .alpha_blend_op(vk::BlendOp::ADD)
242
            .color_write_mask(vk::ColorComponentFlags::RGBA)];
243
        let blend = vk::PipelineColorBlendStateCreateInfo::default().attachments(&attachments);
244
        let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
245
        let dynamic = vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
246
        let formats = [format];
247
        let mut rendering =
248
            vk::PipelineRenderingCreateInfo::default().color_attachment_formats(&formats);
249
        let info = vk::GraphicsPipelineCreateInfo::default()
250
            .stages(&stages)
251
            .vertex_input_state(&vertex)
252
            .input_assembly_state(&assembly)
253
            .viewport_state(&viewport)
254
            .rasterization_state(&raster)
255
            .multisample_state(&multisample)
256
            .color_blend_state(&blend)
257
            .dynamic_state(&dynamic)
258
            .layout(self.layout)
259
            .push_next(&mut rendering);
260
        let pipeline = unsafe { d.create_graphics_pipelines(self.vk_cache, &[info], None) }
261
            .map_err(|(_, e)| e)?[0];
262
        self.cache.insert((format, kind, opaque), pipeline);
263
        Ok(pipeline)
264
    }
265
}
266
267
impl Drop for Pipelines {
268
    fn drop(&mut self) {
269
        self.save_cache();
270
        let d = &self.shared.device.device;
271
        unsafe {
272
            for (_, p) in self.cache.drain() {
273
                d.destroy_pipeline(p, None);
274
            }
275
            d.destroy_pipeline_cache(self.vk_cache, None);
276
            d.destroy_shader_module(self.vert, None);
277
            d.destroy_shader_module(self.solid, None);
278
            d.destroy_shader_module(self.texture, None);
279
            d.destroy_shader_module(self.effect, None);
280
            d.destroy_pipeline_layout(self.layout, None);
281
            d.destroy_descriptor_set_layout(self.set_layout, None);
282
        }
283
    }
284
}
285
286
fn cache_path() -> Option<PathBuf> {
287
    let base = std::env::var_os("XDG_CACHE_HOME")
288
        .filter(|v| !v.is_empty())
289
        .map(PathBuf::from)
290
        .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".cache")))?;
291
    Some(base.join("monolith").join("pipelines.bin"))
292
}
293
294
/// Whether `data` opens with a cache header written by this driver on this
295
/// device. The spec lets a driver reject a foreign cache but some crash on
296
/// one instead, so the header is checked first.
297
fn header_matches(data: &[u8], props: &vk::PhysicalDeviceProperties) -> bool {
298
    let word = |at: usize| {
299
        data.get(at..at + 4)
300
            .and_then(|b| b.try_into().ok())
301
            .map(u32::from_ne_bytes)
302
    };
303
    word(0).is_some_and(|size| size >= 32 && size as usize <= data.len())
304
        && word(4) == Some(vk::PipelineCacheHeaderVersion::ONE.as_raw() as u32)
305
        && word(8) == Some(props.vendor_id)
306
        && word(12) == Some(props.device_id)
307
        && data.get(16..32) == Some(&props.pipeline_cache_uuid[..])
308
}
309
310
#[cfg(test)]
311
mod tests {
312
    use super::*;
313
314
    fn props() -> vk::PhysicalDeviceProperties {
315
        vk::PhysicalDeviceProperties {
316
            vendor_id: 0x10de,
317
            device_id: 0x2487,
318
            pipeline_cache_uuid: [7; 16],
319
            ..Default::default()
320
        }
321
    }
322
323
    fn header(vendor: u32, uuid: u8) -> Vec<u8> {
324
        let words = [32u32, 1, vendor, 0x2487];
325
        let mut data: Vec<u8> = words.iter().flat_map(|w| w.to_ne_bytes()).collect();
326
        data.extend([uuid; 16]);
327
        data
328
    }
329
330
    #[test]
331
    fn cache_header_must_match_the_device_and_driver() {
332
        assert!(header_matches(&header(0x10de, 7), &props()));
333
        assert!(!header_matches(&header(0x1002, 7), &props()));
334
        assert!(!header_matches(&header(0x10de, 8), &props()));
335
        assert!(!header_matches(&header(0x10de, 7)[..20], &props()));
336
        assert!(!header_matches(&[], &props()));
337
    }
338
}