use ash::vk;
use concinnity_core::render::backend_init;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::shadow_bias;
use crate::vulkan::builtin_shaders::CompileProgram;
use crate::vulkan::owned::{OwnedPipeline, VkDevice};
use crate::vulkan::pipeline_desc::{Blend, Depth, DepthBias, GraphicsPipelineDesc, Raster};
#[cfg(test)]
pub(super) fn is_spirv(bytes: &[u8]) -> bool {
bytes.len() >= 4 && u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) == 0x07230203
}
pub(super) struct BindlessSpv {
pub vert: Vec<u8>,
pub frag: Vec<u8>,
pub prepass_vert: Vec<u8>,
pub prepass_frag: Vec<u8>,
}
pub(super) fn compile_bindless_shaders(hot_reload: bool) -> RenderResult<BindlessSpv> {
use super::builtin_shaders::{
MAIN_BINDLESS_FRAG, MAIN_BINDLESS_VERT, MAIN_PREPASS_FRAG, MAIN_PREPASS_VERT,
};
Ok(BindlessSpv {
vert: MAIN_BINDLESS_VERT.compile(hot_reload)?,
frag: MAIN_BINDLESS_FRAG.compile(hot_reload)?,
prepass_vert: MAIN_PREPASS_VERT.compile(hot_reload)?,
prepass_frag: MAIN_PREPASS_FRAG.compile(hot_reload)?,
})
}
pub(super) const CULL_PUSH_CONSTANT_BYTES: u32 = 120;
pub(super) fn compile_cull_shader(hot_reload: bool) -> RenderResult<Vec<u8>> {
super::builtin_shaders::CULL_PHASE1.compile(hot_reload)
}
pub(super) fn compile_cull_shader_phase2(hot_reload: bool) -> RenderResult<Vec<u8>> {
super::builtin_shaders::CULL_PHASE2.compile(hot_reload)
}
pub(super) fn compile_shadow_cull_shader(hot_reload: bool) -> RenderResult<Vec<u8>> {
super::builtin_shaders::CULL_SHADOW.compile(hot_reload)
}
pub(super) fn compile_shadow_bindless_vs(hot_reload: bool) -> RenderResult<Vec<u8>> {
super::builtin_shaders::SHADOW_VERT_BINDLESS.compile(hot_reload)
}
pub(in crate::vulkan) struct SpvModule<'d> {
device: &'d VkDevice,
module: vk::ShaderModule,
}
impl SpvModule<'_> {
pub(in crate::vulkan) fn handle(&self) -> vk::ShaderModule {
self.module
}
}
impl Drop for SpvModule<'_> {
fn drop(&mut self) {
unsafe { self.device.destroy_shader_module(self.module, None) };
}
}
pub(super) fn spirv_words(spv: &[u8]) -> RenderResult<Vec<u32>> {
if !spv.len().is_multiple_of(4) {
return Err(RenderError::Other(format!(
"SPIR-V length {} is not a whole number of words",
spv.len()
)));
}
Ok(spv
.chunks_exact(4)
.map(|w| u32::from_ne_bytes([w[0], w[1], w[2], w[3]]))
.collect())
}
pub(in crate::vulkan) fn spv_module<'d>(
device: &'d VkDevice,
spv: &[u8],
) -> RenderResult<SpvModule<'d>> {
let code = spirv_words(spv).map_err(|e| e.context("shader module"))?;
let info = vk::ShaderModuleCreateInfo::default().code(&code);
let module = unsafe { device.create_shader_module(&info, None) }
.map_err(|e| super::error::map_vk_result(e, "shader module"))?;
Ok(SpvModule { device, module })
}
pub(in crate::vulkan) const SHADER_ENTRY: &std::ffi::CStr = c"main";
pub(in crate::vulkan) struct GraphicsStages<'d> {
vert: SpvModule<'d>,
frag: SpvModule<'d>,
}
impl<'d> GraphicsStages<'d> {
pub(in crate::vulkan) fn new(
device: &'d VkDevice,
vert_spv: &[u8],
frag_spv: &[u8],
) -> RenderResult<Self> {
Ok(Self {
vert: spv_module(device, vert_spv)?,
frag: spv_module(device, frag_spv)?,
})
}
pub(in crate::vulkan) fn infos(&self) -> [vk::PipelineShaderStageCreateInfo<'_>; 2] {
[
vk::PipelineShaderStageCreateInfo::default()
.stage(vk::ShaderStageFlags::VERTEX)
.module(self.vert.handle())
.name(SHADER_ENTRY),
vk::PipelineShaderStageCreateInfo::default()
.stage(vk::ShaderStageFlags::FRAGMENT)
.module(self.frag.handle())
.name(SHADER_ENTRY),
]
}
}
pub(super) fn world_entry(
world: &concinnity_core::components::ShaderPrograms,
entry: &str,
hot_reload: bool,
) -> RenderResult<Vec<u8>> {
let req = crate::shader::surface_source::Request {
platform: concinnity_core::platform::Platform::Vulkan,
hot_reload,
};
crate::shader::surface_source::artifact(world, entry, &req, crate::shader::compile::cooked)
.map(|c| c.into_owned())
}
pub(super) fn compile_text_shaders(hot_reload: bool) -> RenderResult<(Vec<u8>, Vec<u8>)> {
let vert = super::builtin_shaders::TEXT_VERT.compile(hot_reload)?;
let frag = super::builtin_shaders::TEXT_FRAG.compile(hot_reload)?;
Ok((vert, frag))
}
pub(super) fn compile_composite_shaders(hot_reload: bool) -> RenderResult<(Vec<u8>, Vec<u8>)> {
let vert = super::builtin_shaders::FULLSCREEN_VERT.compile(hot_reload)?;
let frag = super::builtin_shaders::COMPOSITE_FRAG.compile(hot_reload)?;
Ok((vert, frag))
}
const fn vertex_binding(stride: u32) -> [vk::VertexInputBindingDescription; 1] {
[vk::VertexInputBindingDescription {
binding: 0,
stride,
input_rate: vk::VertexInputRate::VERTEX,
}]
}
const fn attr(
location: u32,
format: vk::Format,
offset: u32,
) -> vk::VertexInputAttributeDescription {
vk::VertexInputAttributeDescription {
location,
binding: 0,
format,
offset,
}
}
const MAIN_VERTEX_BINDING: [vk::VertexInputBindingDescription; 1] = vertex_binding(56);
pub(super) const MAIN_VERTEX_ATTRS: [vk::VertexInputAttributeDescription; 5] = [
attr(0, vk::Format::R32G32B32_SFLOAT, 0),
attr(1, vk::Format::R32G32B32_SFLOAT, 12),
attr(2, vk::Format::R32G32B32_SFLOAT, 24),
attr(3, vk::Format::R32G32B32_SFLOAT, 36),
attr(4, vk::Format::R32G32_SFLOAT, 48),
];
const SHADOW_VERTEX_ATTRS: [vk::VertexInputAttributeDescription; 1] = [MAIN_VERTEX_ATTRS[0]];
const TEXT_VERTEX_BINDING: [vk::VertexInputBindingDescription; 1] = vertex_binding(32);
const TEXT_VERTEX_ATTRS: [vk::VertexInputAttributeDescription; 4] = [
attr(0, vk::Format::R32G32_SFLOAT, 0),
attr(1, vk::Format::R32G32_SFLOAT, 8),
attr(2, vk::Format::R32G32B32_SFLOAT, 16),
attr(3, vk::Format::R32_SFLOAT, 28),
];
pub(super) struct MeshPipelineTargets<'a> {
pub render_pass: vk::RenderPass,
pub layout: vk::PipelineLayout,
pub vert_spv: &'a [u8],
pub frag_spv: &'a [u8],
}
pub(super) struct BucketPipelines {
pub main: OwnedPipeline,
pub prepass: Option<OwnedPipeline>,
pub programs: Option<concinnity_core::components::ShaderPrograms>,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub(super) struct PrepassTargets {
pub render_pass: vk::RenderPass,
pub layout: vk::PipelineLayout,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub(super) struct BucketPipelineTargets {
pub render_pass: vk::RenderPass,
pub layout: vk::PipelineLayout,
pub prepass: Option<PrepassTargets>,
pub msaa_samples: vk::SampleCountFlags,
pub swapchain_format: vk::Format,
pub hot_reload: bool,
pub template_generation: u64,
}
pub(super) fn build_bucket_pipeline(
device: &VkDevice,
targets: BucketPipelineTargets,
bucket: usize,
shader: backend_init::WorldShader<'_>,
engine_default: &BindlessSpv,
) -> RenderResult<BucketPipelines> {
match shader.programs {
Some(programs) => build_world_shader_pipeline(device, targets, bucket, programs),
None => {
let main = create_bucket_main(device, targets, bucket, engine_default)?;
let prepass = targets.prepass.and_then(|prepass| {
let stages = (
&engine_default.prepass_vert[..],
&engine_default.prepass_frag[..],
);
build_bucket_prepass(device, prepass, bucket, stages)
});
Ok(BucketPipelines {
main,
prepass,
programs: None,
})
}
}
}
pub(super) fn build_world_shader_pipeline(
device: &VkDevice,
targets: BucketPipelineTargets,
bucket: usize,
programs: &concinnity_core::components::ShaderPrograms,
) -> RenderResult<BucketPipelines> {
use concinnity_core::render::shader_programs::surface;
let entry =
|program: surface::Program| world_entry(programs, program.entry, targets.hot_reload);
let spv = BindlessSpv {
vert: entry(surface::MAIN_VERTEX)?,
frag: entry(surface::MAIN_FRAGMENT)?,
prepass_vert: Vec::new(),
prepass_frag: Vec::new(),
};
let main = create_bucket_main(device, targets, bucket, &spv)?;
let prepass = targets.prepass.and_then(|prepass| {
build_world_prepass(device, prepass, bucket, programs, targets.hot_reload)
});
Ok(BucketPipelines {
main,
prepass,
programs: Some(programs.clone()),
})
}
pub(super) fn build_world_prepass(
device: &VkDevice,
prepass: PrepassTargets,
bucket: usize,
programs: &concinnity_core::components::ShaderPrograms,
hot_reload: bool,
) -> Option<OwnedPipeline> {
use concinnity_core::render::shader_programs::surface;
let stages = world_entry(programs, surface::PREPASS_VERTEX.entry, hot_reload).and_then(|vs| {
world_entry(programs, surface::PREPASS_FRAGMENT.entry, hot_reload).map(|fs| (vs, fs))
});
match stages {
Ok((vs, fs)) => build_bucket_prepass(device, prepass, bucket, (&vs, &fs)),
Err(e) => {
tracing::warn!("shader bucket {bucket}'s G-buffer pre-pass did not build: {e}");
None
}
}
}
pub(super) fn build_bucket_prepass(
device: &VkDevice,
prepass: PrepassTargets,
bucket: usize,
stages: (&[u8], &[u8]),
) -> Option<OwnedPipeline> {
let (vert_spv, frag_spv) = stages;
let built = super::post::gbuffer::create_prepass_pipeline(
device,
MeshPipelineTargets {
render_pass: prepass.render_pass,
layout: prepass.layout,
vert_spv,
frag_spv,
},
);
built
.map_err(|e| {
tracing::warn!("shader bucket {bucket}'s G-buffer pre-pass did not build: {e}")
})
.ok()
}
fn create_bucket_main(
device: &VkDevice,
targets: BucketPipelineTargets,
bucket: usize,
spv: &BindlessSpv,
) -> RenderResult<OwnedPipeline> {
if spv.vert.is_empty() || spv.frag.is_empty() {
return Err(RenderError::Other(format!(
"shader bucket {bucket} carries no SPIR-V stages"
)));
}
create_main_pipeline(
device,
MeshPipelineTargets {
render_pass: targets.render_pass,
layout: targets.layout,
vert_spv: &spv.vert,
frag_spv: &spv.frag,
},
targets.msaa_samples,
targets.swapchain_format,
)
.map_err(|e| e.context(format_args!("shader bucket {bucket}")))
}
pub(super) fn build_world_pipeline_table(
device: &VkDevice,
targets: BucketPipelineTargets,
bucket_shaders: &[backend_init::WorldShader<'_>],
engine_default: &BindlessSpv,
) -> RenderResult<Vec<Option<BucketPipelines>>> {
let mut table = Vec::with_capacity(bucket_shaders.len());
for (i, shader) in bucket_shaders.iter().enumerate() {
if shader.deferred {
table.push(None);
continue;
}
table.push(Some(build_bucket_pipeline(
device,
targets,
i + 1,
*shader,
engine_default,
)?));
}
Ok(table)
}
pub(super) fn create_main_pipeline(
device: &VkDevice,
targets: MeshPipelineTargets<'_>,
msaa: vk::SampleCountFlags,
surface_format: vk::Format,
) -> RenderResult<OwnedPipeline> {
create_main_pipeline_filled(device, targets, msaa, surface_format, vk::PolygonMode::FILL)
}
pub(super) fn create_main_pipeline_wireframe(
device: &VkDevice,
targets: MeshPipelineTargets<'_>,
msaa: vk::SampleCountFlags,
surface_format: vk::Format,
) -> RenderResult<OwnedPipeline> {
create_main_pipeline_filled(device, targets, msaa, surface_format, vk::PolygonMode::LINE)
}
fn create_main_pipeline_filled(
device: &VkDevice,
targets: MeshPipelineTargets<'_>,
msaa: vk::SampleCountFlags,
_surface_format: vk::Format,
polygon_mode: vk::PolygonMode,
) -> RenderResult<OwnedPipeline> {
GraphicsPipelineDesc {
depth: Depth::write(),
raster: Raster {
polygon_mode,
..Raster::default()
},
samples: msaa,
vertex_bindings: &MAIN_VERTEX_BINDING,
vertex_attributes: &MAIN_VERTEX_ATTRS,
..GraphicsPipelineDesc::fullscreen(
targets.vert_spv,
targets.frag_spv,
targets.layout,
targets.render_pass,
&[Blend::Opaque],
)
}
.build(device, "main")
}
pub(super) fn create_shadow_pipeline(
device: &VkDevice,
render_pass: vk::RenderPass,
layout: vk::PipelineLayout,
vert_spv: &[u8],
) -> RenderResult<OwnedPipeline> {
GraphicsPipelineDesc {
frag: None,
color_targets: &[],
depth: Depth::write(),
raster: Raster {
bias: Some(DepthBias {
constant: shadow_bias::RASTER_CONSTANT,
clamp: device.depth_bias_clamp(),
slope: shadow_bias::RASTER_SLOPE,
}),
..Raster::default()
},
vertex_bindings: &MAIN_VERTEX_BINDING,
vertex_attributes: &SHADOW_VERTEX_ATTRS,
..GraphicsPipelineDesc::fullscreen(vert_spv, &[], layout, render_pass, &[])
}
.build(device, "shadow")
}
pub(super) fn create_text_pipeline(
device: &VkDevice,
render_pass: vk::RenderPass,
layout: vk::PipelineLayout,
vert_spv: &[u8],
frag_spv: &[u8],
msaa: vk::SampleCountFlags,
) -> RenderResult<OwnedPipeline> {
GraphicsPipelineDesc {
samples: msaa,
vertex_bindings: &TEXT_VERTEX_BINDING,
vertex_attributes: &TEXT_VERTEX_ATTRS,
..GraphicsPipelineDesc::fullscreen(
vert_spv,
frag_spv,
layout,
render_pass,
&[Blend::AlphaOver],
)
}
.build(device, "text")
}
pub(super) fn create_composite_pipeline(
device: &VkDevice,
render_pass: vk::RenderPass,
layout: vk::PipelineLayout,
vert_spv: &[u8],
frag_spv: &[u8],
) -> RenderResult<OwnedPipeline> {
GraphicsPipelineDesc::fullscreen(vert_spv, frag_spv, layout, render_pass, &[Blend::Opaque])
.build(device, "composite")
}
#[cfg(test)]
mod tests {
use super::{
CompileProgram, SHADOW_VERTEX_ATTRS, compile_bindless_shaders, compile_cull_shader,
compile_cull_shader_phase2, compile_shadow_bindless_vs, compile_shadow_cull_shader,
is_spirv, spirv_words, world_entry,
};
#[test]
fn spirv_words_reads_whole_words() {
let bytes = [0x03, 0x02, 0x23, 0x07, 0x00, 0x01, 0x00, 0x00];
let words = spirv_words(&bytes).expect("a two-word blob converts");
assert_eq!(
words,
vec![
u32::from_ne_bytes([0x03, 0x02, 0x23, 0x07]),
u32::from_ne_bytes([0x00, 0x01, 0x00, 0x00]),
]
);
assert_eq!(spirv_words(&[]).expect("empty converts"), Vec::<u32>::new());
}
#[test]
fn spirv_words_rejects_a_partial_word() {
for len in [1usize, 2, 3, 5, 7] {
let bytes = vec![0xFFu8; len];
assert!(
spirv_words(&bytes).is_err(),
"length {len} is not a whole number of words"
);
}
}
#[test]
fn cull_shaders_compile_both_phases() {
let phase1 = compile_cull_shader(false).expect("phase-1 cull compiles");
let phase2 = compile_cull_shader_phase2(false).expect("phase-2 cull compiles");
let shadow = compile_shadow_cull_shader(false).expect("shadow cull compiles");
assert!(is_spirv(&phase1), "phase-1 cull is valid SPIR-V");
assert!(is_spirv(&phase2), "phase-2 cull is valid SPIR-V");
assert!(is_spirv(&shadow), "shadow cull is valid SPIR-V");
assert_ne!(phase1, phase2);
assert_ne!(phase1, shadow);
}
#[test]
fn shadow_bindless_vs_compiles() {
concinnity_shader::require_dxc!();
let vs = compile_shadow_bindless_vs(false).expect("shadow bindless VS compiles");
assert!(is_spirv(&vs), "shadow bindless VS is valid SPIR-V");
}
#[test]
fn the_shadow_pipeline_binds_what_its_shader_reads() {
concinnity_shader::require_dxc!();
let vs = compile_shadow_bindless_vs(false).expect("shadow bindless VS compiles");
let words = spirv_words(&vs).expect("whole words");
let mut bound: Vec<u32> = SHADOW_VERTEX_ATTRS.iter().map(|a| a.location).collect();
bound.sort_unstable();
assert_eq!(super::super::spirv_inputs::input_locations(&words), bound);
}
#[test]
fn bindless_shaders_compile() {
concinnity_shader::require_dxc!();
let spv = compile_bindless_shaders(false).expect("bindless shaders compile");
for (stage, bytes) in [
("vertex", &spv.vert),
("fragment", &spv.frag),
("pre-pass vertex", &spv.prepass_vert),
("pre-pass fragment", &spv.prepass_frag),
] {
assert!(is_spirv(bytes), "bindless {stage} is valid SPIR-V");
}
let frag_src = crate::vulkan::builtin_shaders::MAIN_BINDLESS_FRAG.source(false);
let injected: Vec<&str> = frag_src
.lines()
.take_while(|l| l.starts_with("#define "))
.collect();
assert_eq!(injected, ["#define CN_BACKEND_VULKAN 1"]);
}
#[test]
fn a_world_shader_compiles_its_own_bindless_pair() {
concinnity_shader::require_dxc!();
let programs = concinnity_core::components::ShaderPrograms {
name: "wall".to_string(),
vertex: None,
fragment: concinnity_core::components::ShaderSource {
path: "shaders/wall.hlsl".to_string(),
text: "float4 shade(VertexOut v, GpuObjectData od) { return (float4)(1.0); }"
.to_string(),
},
programs: Vec::new(),
};
let compiled: Vec<Vec<u8>> = concinnity_core::render::shader_programs::surface::ALL
.iter()
.map(|p| world_entry(&programs, p.entry, false).unwrap())
.collect();
assert!(
compiled.iter().all(|s| is_spirv(s)),
"the world's entries compile"
);
let engine = compile_bindless_shaders(false).unwrap();
assert_ne!(
compiled[1], engine.frag,
"the world's fragment is its own program"
);
}
}