use ash::vk;
use crate::gfx::render_graph::{GraphResourceClass, ReadStages, ResourceState};
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(super) enum VkResting {
Discarded,
Sampled,
}
impl VkResting {
#[cfg(debug_assertions)]
pub(super) fn layout(self) -> vk::ImageLayout {
self.triple().0
}
fn triple(self) -> (vk::ImageLayout, vk::AccessFlags, vk::PipelineStageFlags) {
match self {
VkResting::Discarded => (
vk::ImageLayout::UNDEFINED,
vk::AccessFlags::empty(),
vk::PipelineStageFlags::TOP_OF_PIPE,
),
VkResting::Sampled => (
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
vk::AccessFlags::SHADER_READ,
vk::PipelineStageFlags::FRAGMENT_SHADER | vk::PipelineStageFlags::COMPUTE_SHADER,
),
}
}
}
fn read_stage_mask(stages: ReadStages) -> vk::PipelineStageFlags {
let mut mask = vk::PipelineStageFlags::empty();
if stages.contains(ReadStages::FRAGMENT) {
mask |= vk::PipelineStageFlags::FRAGMENT_SHADER;
}
if stages.contains(ReadStages::COMPUTE) {
mask |= vk::PipelineStageFlags::COMPUTE_SHADER;
}
if mask.is_empty() {
mask = vk::PipelineStageFlags::FRAGMENT_SHADER;
}
mask
}
pub(super) fn vk_state(
class: GraphResourceClass,
state: ResourceState,
read_stages: ReadStages,
) -> (vk::ImageLayout, vk::AccessFlags, vk::PipelineStageFlags) {
let depth_attachment = (
vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS | vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
);
match (class, state) {
(_, ResourceState::Undefined) => VkResting::Discarded.triple(),
(GraphResourceClass::ColorTarget, ResourceState::Write) => (
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
),
(GraphResourceClass::DepthTarget, ResourceState::Write) => depth_attachment,
(GraphResourceClass::StorageImage, ResourceState::Write) => (
vk::ImageLayout::GENERAL,
vk::AccessFlags::SHADER_WRITE,
vk::PipelineStageFlags::COMPUTE_SHADER,
),
(GraphResourceClass::IndirectBuffer, ResourceState::Read) => (
vk::ImageLayout::UNDEFINED,
vk::AccessFlags::INDIRECT_COMMAND_READ,
vk::PipelineStageFlags::DRAW_INDIRECT,
),
(
GraphResourceClass::StorageBuffer | GraphResourceClass::UnorderedBuffer,
ResourceState::Read,
) => (
vk::ImageLayout::UNDEFINED,
vk::AccessFlags::SHADER_READ,
read_stage_mask(read_stages),
),
(
GraphResourceClass::IndirectBuffer
| GraphResourceClass::StorageBuffer
| GraphResourceClass::UnorderedBuffer,
ResourceState::Write,
) => (
vk::ImageLayout::UNDEFINED,
vk::AccessFlags::SHADER_WRITE,
vk::PipelineStageFlags::COMPUTE_SHADER,
),
(_, ResourceState::Read) => (
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
vk::AccessFlags::SHADER_READ,
read_stage_mask(read_stages),
),
}
}
type VkTransition = (
vk::ImageLayout,
vk::ImageLayout,
vk::AccessFlags,
vk::AccessFlags,
vk::PipelineStageFlags,
vk::PipelineStageFlags,
);
pub(super) fn vk_transition(
class: GraphResourceClass,
resting: VkResting,
from: ResourceState,
to: ResourceState,
read_stages: ReadStages,
) -> Option<VkTransition> {
let (old, src_access, src_stage) = if from == ResourceState::Undefined {
resting.triple()
} else {
vk_state(class, from, read_stages)
};
let (new, dst_access, dst_stage) = vk_state(class, to, read_stages);
let write_after_write = from == ResourceState::Write && to == ResourceState::Write;
(old != new || write_after_write || class.is_buffer())
.then_some((old, new, src_access, dst_access, src_stage, dst_stage))
}
pub(super) fn vk_restore(
class: GraphResourceClass,
resting: VkResting,
state: ResourceState,
read_stages: ReadStages,
) -> Option<VkTransition> {
if resting == VkResting::Discarded || state == ResourceState::Undefined || class.is_buffer() {
return None;
}
let (old, src_access, src_stage) = vk_state(class, state, read_stages);
let (new, dst_access, dst_stage) = resting.triple();
(old != new).then_some((old, new, src_access, dst_access, src_stage, dst_stage))
}
#[cfg(test)]
mod tests {
use super::*;
const FRAG: ReadStages = ReadStages::FRAGMENT;
#[test]
fn class_state_mapping_is_pinned() {
let (layout, access, stage) =
vk_state(GraphResourceClass::ColorTarget, ResourceState::Write, FRAG);
assert_eq!(layout, vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
assert_eq!(access, vk::AccessFlags::COLOR_ATTACHMENT_WRITE);
assert_eq!(stage, vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT);
let read = vk_state(GraphResourceClass::DepthTarget, ResourceState::Read, FRAG);
let write = vk_state(GraphResourceClass::DepthTarget, ResourceState::Write, FRAG);
assert_eq!(write.0, vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
assert_eq!(read.0, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
assert_eq!(
vk_state(GraphResourceClass::ColorTarget, ResourceState::Read, FRAG),
read
);
let (sl, sa, ss) = vk_state(GraphResourceClass::StorageImage, ResourceState::Write, FRAG);
assert_eq!(sl, vk::ImageLayout::GENERAL);
assert_eq!(sa, vk::AccessFlags::SHADER_WRITE);
assert_eq!(ss, vk::PipelineStageFlags::COMPUTE_SHADER);
assert_eq!(
vk_state(GraphResourceClass::StorageImage, ResourceState::Read, FRAG).0,
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL
);
}
#[test]
fn read_stage_follows_consuming_union() {
let frag = vk_state(
GraphResourceClass::ColorTarget,
ResourceState::Read,
ReadStages::FRAGMENT,
);
assert_eq!(frag.0, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
assert_eq!(frag.2, vk::PipelineStageFlags::FRAGMENT_SHADER);
let comp = vk_state(
GraphResourceClass::ColorTarget,
ResourceState::Read,
ReadStages::COMPUTE,
);
assert_eq!(comp.0, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
assert_eq!(comp.2, vk::PipelineStageFlags::COMPUTE_SHADER);
let both = vk_state(
GraphResourceClass::ColorTarget,
ResourceState::Read,
ReadStages::FRAGMENT | ReadStages::COMPUTE,
);
assert_eq!(
both.2,
vk::PipelineStageFlags::FRAGMENT_SHADER | vk::PipelineStageFlags::COMPUTE_SHADER
);
let empty = vk_state(
GraphResourceClass::ColorTarget,
ResourceState::Read,
ReadStages::empty(),
);
assert_eq!(empty.2, vk::PipelineStageFlags::FRAGMENT_SHADER);
}
#[test]
fn a_first_use_opens_from_the_resource_resting_layout() {
let (po, pn, ..) = vk_transition(
GraphResourceClass::DepthTarget,
VkResting::Sampled,
ResourceState::Undefined,
ResourceState::Write,
FRAG,
)
.expect("a real cross-frame reset");
assert_eq!(po, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
assert_eq!(pn, vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
let (po, pn, ..) = vk_transition(
GraphResourceClass::DepthTarget,
VkResting::Discarded,
ResourceState::Undefined,
ResourceState::Write,
FRAG,
)
.expect("a discarding open");
assert_eq!(po, vk::ImageLayout::UNDEFINED);
assert_eq!(pn, vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
}
#[test]
fn transition_resolves_migrated_producers_and_consumers() {
assert!(
vk_transition(
GraphResourceClass::ColorTarget,
VkResting::Discarded,
ResourceState::Undefined,
ResourceState::Write,
FRAG,
)
.is_some()
);
assert!(
vk_transition(
GraphResourceClass::ColorTarget,
VkResting::Discarded,
ResourceState::Write,
ResourceState::Read,
FRAG,
)
.is_some()
);
let (old, new, ..) = vk_transition(
GraphResourceClass::DepthTarget,
VkResting::Sampled,
ResourceState::Write,
ResourceState::Read,
FRAG,
)
.expect("a real close");
assert_eq!(old, vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
assert_eq!(new, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
assert!(
vk_transition(
GraphResourceClass::DepthTarget,
VkResting::Sampled,
ResourceState::Undefined,
ResourceState::Read,
FRAG,
)
.is_none()
);
let (po, pn, ..) = vk_transition(
GraphResourceClass::StorageImage,
VkResting::Sampled,
ResourceState::Undefined,
ResourceState::Write,
FRAG,
)
.expect("a real open");
assert_eq!(po, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
assert_eq!(pn, vk::ImageLayout::GENERAL);
let (co, cn, ..) = vk_transition(
GraphResourceClass::StorageImage,
VkResting::Sampled,
ResourceState::Write,
ResourceState::Read,
FRAG,
)
.expect("a real close");
assert_eq!(co, vk::ImageLayout::GENERAL);
assert_eq!(cn, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
}
#[test]
fn transition_threads_compute_read_stage() {
let (.., src_stage, dst_stage) = vk_transition(
GraphResourceClass::ColorTarget,
VkResting::Discarded,
ResourceState::Write,
ResourceState::Read,
ReadStages::COMPUTE,
)
.expect("a real close");
assert_eq!(src_stage, vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT);
assert_eq!(dst_stage, vk::PipelineStageFlags::COMPUTE_SHADER);
let (.., dst_stage) = vk_transition(
GraphResourceClass::ColorTarget,
VkResting::Discarded,
ResourceState::Write,
ResourceState::Read,
ReadStages::FRAGMENT | ReadStages::COMPUTE,
)
.expect("a real close");
assert_eq!(
dst_stage,
vk::PipelineStageFlags::FRAGMENT_SHADER | vk::PipelineStageFlags::COMPUTE_SHADER
);
let (old, new, .., src_stage, _dst_stage) = vk_transition(
GraphResourceClass::ColorTarget,
VkResting::Discarded,
ResourceState::Read,
ResourceState::Write,
ReadStages::COMPUTE,
)
.expect("a real WAR");
assert_eq!(old, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
assert_eq!(new, vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
assert_eq!(src_stage, vk::PipelineStageFlags::COMPUTE_SHADER);
}
#[test]
fn a_frame_that_ends_off_resting_takes_one_restore() {
let (old, new, ..) = vk_restore(
GraphResourceClass::StorageImage,
VkResting::Sampled,
ResourceState::Write,
ReadStages::empty(),
)
.expect("a real restore");
assert_eq!(old, vk::ImageLayout::GENERAL);
assert_eq!(new, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
assert!(
vk_restore(
GraphResourceClass::StorageImage,
VkResting::Sampled,
ResourceState::Read,
FRAG,
)
.is_none()
);
assert!(
vk_restore(
GraphResourceClass::StorageImage,
VkResting::Sampled,
ResourceState::Undefined,
ReadStages::empty(),
)
.is_none()
);
assert!(
vk_restore(
GraphResourceClass::DepthTarget,
VkResting::Discarded,
ResourceState::Read,
FRAG,
)
.is_none()
);
assert!(
vk_restore(
GraphResourceClass::StorageBuffer,
VkResting::Sampled,
ResourceState::Write,
ReadStages::empty(),
)
.is_none()
);
}
}