atagen

monolith

public
Star0
Watch0
a1f375a8b4bed2954bd4bb4deb4dc6a6efeeb217
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/render-vk/src/pipeline.rsRust332 lines12.2 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
    }
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
}