use frust_gpu::lint::PipelineLayoutDesc;
use frust_gpu::{PipelineCache, RenderPipelineDesc, ShaderId, ShaderLibrary, VertexLayout};
use super::GpuStrip;
use super::bindings::{STRIP_BIND_GROUP_COUNT, strip_pipeline_layout};
use super::config::GpuConfig;
use super::shader_src;
use super::strips::PaintClass;
pub const INTERMEDIATE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
pub const DEST_OUT_BLEND: wgpu::BlendState = wgpu::BlendState {
color: DEST_OUT_COMPONENT,
alpha: DEST_OUT_COMPONENT,
};
const DEST_OUT_COMPONENT: wgpu::BlendComponent = wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Zero,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
};
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus;
pub const TOPOLOGY: wgpu::PrimitiveTopology = wgpu::PrimitiveTopology::TriangleStrip;
pub const VS_MAIN: &str = "vs_main";
pub const VS_MAIN_FULLSCREEN: &str = "vs_main_fullscreen";
pub const FS_MAIN: &str = "fs_main";
const STRIP_BIND_GROUPS: usize = STRIP_BIND_GROUP_COUNT;
pub const WARM_CLASSES: [PaintClass; 4] = [
PaintClass::SolidCoverage,
PaintClass::Gradient,
PaintClass::Image,
PaintClass::Glyph,
];
pub const WARM_CLASS_STATES: [EnginePipeline; 2] =
[EnginePipeline::StripOpaque, EnginePipeline::StripDepthAlpha];
pub const VARIANT_COUNT: usize = {
let mut count = 0;
let mut state = 0;
while state < EnginePipeline::ALL.len() {
let pipeline = EnginePipeline::ALL[state];
if pipeline.is_strip() {
let mut class = 0;
while class < PaintClass::ALL.len() {
if pipeline.serves(PaintClass::ALL[class]) {
count += 1;
}
class += 1;
}
} else {
count += 1;
}
state += 1;
}
count
};
const COPY_BIND_GROUPS: usize = 1;
const FILTER_BIND_GROUPS: usize = 2;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EngineShaders {
pub image_sampler: bool,
pub strip: ShaderId,
pub strip_classes: [ShaderId; PaintClass::SPECIALISED.len()],
pub strip_layout: wgpu::PipelineLayout,
pub atlas_strip: ShaderId,
pub atlas_layout: wgpu::PipelineLayout,
pub clear: ShaderId,
pub copy: ShaderId,
pub filter: ShaderId,
}
impl EngineShaders {
#[must_use]
pub fn register(library: &mut ShaderLibrary, device: &wgpu::Device) -> Self {
Self::register_family(library, device, crate::config::image_sampler_enabled())
}
#[must_use]
pub fn register_family(
library: &mut ShaderLibrary,
device: &wgpu::Device,
image_sampler: bool,
) -> Self {
let mut strip_program = |class: PaintClass, image_sampler: bool| {
library.insert_wgsl(
device,
shader_src::strip_program_name(class, image_sampler),
&shader_src::strip_program_source(class, image_sampler),
)
};
let strip = strip_program(PaintClass::Uber, image_sampler);
let strip_classes =
PaintClass::SPECIALISED.map(|class| strip_program(class, image_sampler));
let atlas_strip = strip_program(PaintClass::Uber, false);
let strip_layout = strip_pipeline_layout(device, image_sampler);
let atlas_layout = if image_sampler {
strip_pipeline_layout(device, false)
} else {
strip_layout.clone()
};
Self {
image_sampler,
strip,
strip_classes,
strip_layout,
atlas_strip,
atlas_layout,
clear: library.insert_wgsl(device, shader_src::CLEAR_NAME, shader_src::CLEAR),
copy: library.insert_wgsl(device, shader_src::COPY_NAME, shader_src::COPY),
filter: library.insert_wgsl(device, shader_src::FILTER_NAME, shader_src::FILTER),
}
}
#[must_use]
pub fn strip_class(&self, class: PaintClass) -> ShaderId {
PaintClass::SPECIALISED
.iter()
.position(|specialised| *specialised == class)
.and_then(|index| self.strip_classes.get(index))
.copied()
.unwrap_or(self.strip)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum EngineShaderModule {
Strip,
Clear,
Copy,
Filter,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum EnginePipeline {
StripIntermediate,
StripAlpha,
StripDepthAlpha,
StripOpaque,
StripDestOut,
StripDepthDestOut,
Clear,
Copy,
Filter,
}
impl EnginePipeline {
pub const ALL: [Self; 9] = [
Self::StripIntermediate,
Self::StripAlpha,
Self::StripDepthAlpha,
Self::StripOpaque,
Self::StripDestOut,
Self::StripDepthDestOut,
Self::Clear,
Self::Copy,
Self::Filter,
];
#[must_use]
pub const fn index(self) -> usize {
match self {
Self::StripIntermediate => 0,
Self::StripAlpha => 1,
Self::StripDepthAlpha => 2,
Self::StripOpaque => 3,
Self::StripDestOut => 4,
Self::StripDepthDestOut => 5,
Self::Clear => 6,
Self::Copy => 7,
Self::Filter => 8,
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::StripIntermediate => "strip-intermediate",
Self::StripAlpha => "strip-alpha",
Self::StripDepthAlpha => "strip-depth-alpha",
Self::StripOpaque => "strip-opaque",
Self::StripDestOut => "strip-dest-out",
Self::StripDepthDestOut => "strip-depth-dest-out",
Self::Clear => "clear",
Self::Copy => "copy",
Self::Filter => "filter",
}
}
#[must_use]
pub const fn serves(self, class: PaintClass) -> bool {
match class {
PaintClass::Uber => true,
PaintClass::SolidCoverage => self.is_strip(),
PaintClass::Gradient
| PaintClass::Image
| PaintClass::ImageFiltered
| PaintClass::Glyph => matches!(
self,
Self::StripIntermediate
| Self::StripAlpha
| Self::StripDepthAlpha
| Self::StripOpaque
),
PaintClass::BlurredRect | PaintClass::LayerComposite => matches!(
self,
Self::StripIntermediate | Self::StripAlpha | Self::StripDepthAlpha
),
}
}
#[must_use]
pub const fn fold_class(self, class: PaintClass) -> PaintClass {
if self.serves(class) {
class
} else {
PaintClass::Uber
}
}
#[must_use]
pub const fn is_strip(self) -> bool {
matches!(
self,
Self::StripIntermediate
| Self::StripAlpha
| Self::StripDepthAlpha
| Self::StripOpaque
| Self::StripDestOut
| Self::StripDepthDestOut
)
}
#[must_use]
pub const fn bind_group_count(self) -> usize {
match self {
Self::StripIntermediate
| Self::StripAlpha
| Self::StripDepthAlpha
| Self::StripOpaque
| Self::StripDestOut
| Self::StripDepthDestOut => STRIP_BIND_GROUPS,
Self::Clear => 0,
Self::Copy => COPY_BIND_GROUPS,
Self::Filter => FILTER_BIND_GROUPS,
}
}
#[must_use]
pub const fn format(self, target_format: wgpu::TextureFormat) -> wgpu::TextureFormat {
match self {
Self::StripAlpha
| Self::StripDepthAlpha
| Self::StripOpaque
| Self::StripDestOut
| Self::StripDepthDestOut => target_format,
Self::StripIntermediate | Self::Clear | Self::Copy | Self::Filter => {
INTERMEDIATE_FORMAT
}
}
}
#[must_use]
pub const fn blend(self) -> Option<wgpu::BlendState> {
match self {
Self::StripIntermediate | Self::StripAlpha | Self::StripDepthAlpha => {
Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING)
}
Self::StripDestOut | Self::StripDepthDestOut => Some(DEST_OUT_BLEND),
Self::StripOpaque | Self::Clear | Self::Copy | Self::Filter => None,
}
}
#[must_use]
pub fn depth(self) -> Option<wgpu::DepthStencilState> {
match self {
Self::StripDepthAlpha | Self::StripDepthDestOut => Some(depth_state(false)),
Self::StripOpaque => Some(depth_state(true)),
Self::StripIntermediate
| Self::StripAlpha
| Self::StripDestOut
| Self::Clear
| Self::Copy
| Self::Filter => None,
}
}
#[must_use]
pub fn vertex_layouts(self) -> Vec<VertexLayout> {
match self {
Self::StripIntermediate
| Self::StripAlpha
| Self::StripDepthAlpha
| Self::StripOpaque
| Self::StripDestOut
| Self::StripDepthDestOut => vec![GpuStrip::vertex_layout()],
Self::Clear => vec![clear_vertex_layout()],
Self::Copy => vec![copy_vertex_layout()],
Self::Filter => vec![filter_vertex_layout()],
}
}
#[must_use]
pub const fn module(self) -> EngineShaderModule {
match self {
Self::StripIntermediate
| Self::StripAlpha
| Self::StripDepthAlpha
| Self::StripOpaque
| Self::StripDestOut
| Self::StripDepthDestOut => EngineShaderModule::Strip,
Self::Clear => EngineShaderModule::Clear,
Self::Copy => EngineShaderModule::Copy,
Self::Filter => EngineShaderModule::Filter,
}
}
#[must_use]
pub fn shader(self, shaders: &EngineShaders) -> ShaderId {
self.class_shader(shaders, PaintClass::Uber)
}
#[must_use]
pub fn class_shader(self, shaders: &EngineShaders, class: PaintClass) -> ShaderId {
match self.module() {
EngineShaderModule::Strip => shaders.strip_class(self.fold_class(class)),
EngineShaderModule::Clear => shaders.clear,
EngineShaderModule::Copy => shaders.copy,
EngineShaderModule::Filter => shaders.filter,
}
}
#[must_use]
pub fn desc(
self,
shaders: &EngineShaders,
target_format: wgpu::TextureFormat,
) -> RenderPipelineDesc {
self.class_desc(shaders, target_format, PaintClass::Uber)
}
#[must_use]
pub fn class_desc(
self,
shaders: &EngineShaders,
target_format: wgpu::TextureFormat,
class: PaintClass,
) -> RenderPipelineDesc {
RenderPipelineDesc {
shader: self.class_shader(shaders, class),
vs: VS_MAIN.into(),
fs: FS_MAIN.into(),
vertex_layouts: self.vertex_layouts(),
blend: self.blend(),
format: self.format(target_format),
sample_count: 1,
depth: self.depth(),
topology: TOPOLOGY,
layout: self.is_strip().then(|| shaders.strip_layout.clone()),
}
}
#[must_use]
pub fn layout_desc(self) -> PipelineLayoutDesc {
let layouts = self.vertex_layouts();
PipelineLayoutDesc {
bind_group_count: self.bind_group_count(),
max_vertex_buffers: layouts.len(),
total_vertex_attributes: layouts.iter().map(|l| l.attributes.len()).sum(),
max_vertex_buffer_stride: layouts
.iter()
.map(|l| l.array_stride as usize)
.max()
.unwrap_or(0),
sample_count: 1,
uniform_buffer_sizes: if self.is_strip() {
vec![GpuConfig::SIZE as usize]
} else {
Vec::new()
},
}
}
}
fn depth_state(depth_write_enabled: bool) -> wgpu::DepthStencilState {
wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: Some(depth_write_enabled),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}
}
#[must_use]
pub fn clear_vertex_layout() -> VertexLayout {
VertexLayout {
array_stride: 24,
step_mode: wgpu::VertexStepMode::Instance,
attributes: wgpu::vertex_attr_array![
0 => Uint32x2,
1 => Uint32x2,
2 => Uint32x2,
]
.to_vec(),
}
}
#[must_use]
pub fn copy_vertex_layout() -> VertexLayout {
VertexLayout {
array_stride: 16,
step_mode: wgpu::VertexStepMode::Instance,
attributes: wgpu::vertex_attr_array![
0 => Uint32,
1 => Uint32,
2 => Uint32,
3 => Uint32,
]
.to_vec(),
}
}
#[must_use]
pub fn filter_vertex_layout() -> VertexLayout {
VertexLayout {
array_stride: 32,
step_mode: wgpu::VertexStepMode::Instance,
attributes: wgpu::vertex_attr_array![
0 => Uint32,
1 => Uint32,
2 => Uint32,
3 => Uint32,
4 => Uint32,
5 => Uint32,
6 => Uint32,
7 => Uint32,
]
.to_vec(),
}
}
pub const ATLAS_STRIP_PIPELINE: EnginePipeline = EnginePipeline::StripIntermediate;
#[must_use]
pub fn atlas_strip_desc(shaders: &EngineShaders) -> RenderPipelineDesc {
RenderPipelineDesc {
shader: shaders.atlas_strip,
layout: Some(shaders.atlas_layout.clone()),
..ATLAS_STRIP_PIPELINE.desc(shaders, INTERMEDIATE_FORMAT)
}
}
#[must_use]
pub fn warm_up_descs(
shaders: &EngineShaders,
target_format: wgpu::TextureFormat,
) -> Vec<RenderPipelineDesc> {
warm_up_descs_for(shaders, target_format, crate::config::uber_program_forced())
}
#[must_use]
pub fn warm_up_descs_for(
shaders: &EngineShaders,
target_format: wgpu::TextureFormat,
uber_only: bool,
) -> Vec<RenderPipelineDesc> {
let mut descs: Vec<_> = EnginePipeline::ALL
.iter()
.map(|pipeline| pipeline.desc(shaders, target_format))
.collect();
if shaders.image_sampler {
descs.push(atlas_strip_desc(shaders));
}
if !uber_only {
for state in WARM_CLASS_STATES {
for class in WARM_CLASSES {
descs.push(state.class_desc(shaders, target_format, class));
}
}
}
descs
}
pub fn warm_up(
cache: &mut PipelineCache,
device: &wgpu::Device,
shaders: &EngineShaders,
target_format: wgpu::TextureFormat,
) {
cache.warm_up(device, &warm_up_descs(shaders, target_format));
}
#[cfg(test)]
mod tests {
use super::*;
const TARGET: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8Unorm;
fn variant(
pipeline: EnginePipeline,
) -> (
EngineShaderModule,
wgpu::TextureFormat,
Option<wgpu::BlendState>,
Option<wgpu::DepthStencilState>,
Vec<VertexLayout>,
) {
(
pipeline.module(),
pipeline.format(TARGET),
pipeline.blend(),
pipeline.depth(),
pipeline.vertex_layouts(),
)
}
#[test]
fn the_warm_up_list_covers_every_pipeline_exactly_once() {
assert_eq!(EnginePipeline::ALL.len(), 9);
for (i, a) in EnginePipeline::ALL.iter().enumerate() {
for b in EnginePipeline::ALL.iter().skip(i + 1) {
assert_ne!(a, b, "the warm-up list repeats {}", a.label());
assert_ne!(
variant(*a),
variant(*b),
"{} and {} describe the same pipeline, so one would never be reached",
a.label(),
b.label()
);
}
}
}
#[test]
fn the_class_axis_is_folded_to_thirty_seven_variants() {
assert_eq!(VARIANT_COUNT, 37);
let served = |pipeline: EnginePipeline| {
PaintClass::ALL
.iter()
.filter(|class| pipeline.serves(**class))
.count()
};
assert_eq!(served(EnginePipeline::StripIntermediate), 8);
assert_eq!(served(EnginePipeline::StripAlpha), 8);
assert_eq!(served(EnginePipeline::StripDepthAlpha), 8);
assert_eq!(served(EnginePipeline::StripOpaque), 6);
assert_eq!(served(EnginePipeline::StripDestOut), 2);
assert_eq!(served(EnginePipeline::StripDepthDestOut), 2);
for pipeline in [
EnginePipeline::Clear,
EnginePipeline::Copy,
EnginePipeline::Filter,
] {
assert_eq!(served(pipeline), 1);
assert!(pipeline.serves(PaintClass::Uber));
}
assert_eq!(
EnginePipeline::StripDestOut.fold_class(PaintClass::Gradient),
PaintClass::Uber
);
assert_eq!(
EnginePipeline::StripOpaque.fold_class(PaintClass::LayerComposite),
PaintClass::Uber
);
assert_eq!(
EnginePipeline::StripDestOut.fold_class(PaintClass::SolidCoverage),
PaintClass::SolidCoverage
);
for state in WARM_CLASS_STATES {
assert!(state.is_strip() && state.depth().is_some());
for class in WARM_CLASSES {
assert!(state.serves(class));
}
}
}
#[test]
fn the_state_index_is_the_position_in_all() {
for (index, pipeline) in EnginePipeline::ALL.iter().enumerate() {
assert_eq!(pipeline.index(), index, "{}", pipeline.label());
}
}
#[test]
fn every_pipeline_layout_passes_the_downlevel_lint() {
for pipeline in EnginePipeline::ALL {
let violations = frust_gpu::lint_pipeline_layout(&pipeline.layout_desc());
assert!(
violations.is_empty(),
"{} violates a downlevel design rule: {violations:?}",
pipeline.label()
);
}
}
#[test]
fn the_strip_pipelines_sit_exactly_on_the_bind_group_ceiling() {
let ceiling = wgpu::Limits::downlevel_webgl2_defaults().max_bind_groups as usize;
for pipeline in EnginePipeline::ALL {
assert!(
pipeline.bind_group_count() <= ceiling,
"{} declares {} bind groups, over the WebGL2 ceiling of {ceiling}",
pipeline.label(),
pipeline.bind_group_count()
);
}
assert_eq!(
EnginePipeline::StripAlpha.bind_group_count(),
ceiling,
"the strip programs bind the ceiling exactly, with no headroom left"
);
}
#[test]
fn no_pipeline_multisamples() {
for pipeline in EnginePipeline::ALL {
assert_eq!(
pipeline.layout_desc().sample_count,
1,
"{} multisamples",
pipeline.label()
);
}
}
#[test]
fn the_strip_variants_differ_only_in_target_blend_and_depth() {
let strips = [
EnginePipeline::StripIntermediate,
EnginePipeline::StripAlpha,
EnginePipeline::StripDepthAlpha,
EnginePipeline::StripOpaque,
EnginePipeline::StripDestOut,
EnginePipeline::StripDepthDestOut,
];
for pipeline in strips {
assert_eq!(pipeline.module(), EngineShaderModule::Strip);
assert_eq!(pipeline.vertex_layouts(), vec![GpuStrip::vertex_layout()]);
assert_eq!(pipeline.bind_group_count(), STRIP_BIND_GROUPS);
}
assert_eq!(
EnginePipeline::StripIntermediate.format(TARGET),
INTERMEDIATE_FORMAT
);
assert_eq!(EnginePipeline::StripAlpha.format(TARGET), TARGET);
let premultiplied = Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING);
assert_eq!(EnginePipeline::StripIntermediate.blend(), premultiplied);
assert_eq!(EnginePipeline::StripAlpha.blend(), premultiplied);
assert_eq!(EnginePipeline::StripDepthAlpha.blend(), premultiplied);
assert_eq!(
EnginePipeline::StripOpaque.blend(),
None,
"the opaque variant must not blend"
);
assert_eq!(EnginePipeline::StripIntermediate.depth(), None);
assert_eq!(EnginePipeline::StripAlpha.depth(), None);
assert_eq!(
EnginePipeline::StripDepthAlpha
.depth()
.and_then(|d| d.depth_write_enabled),
Some(false),
"the depth-alpha variant tests depth without writing it"
);
assert_eq!(
EnginePipeline::StripOpaque
.depth()
.and_then(|d| d.depth_write_enabled),
Some(true),
"the opaque variant establishes the depth later passes test against"
);
for pipeline in [
EnginePipeline::StripDepthAlpha,
EnginePipeline::StripOpaque,
EnginePipeline::StripDepthDestOut,
] {
let depth = pipeline.depth().expect("a depth-testing variant");
assert_eq!(depth.format, DEPTH_FORMAT);
assert_eq!(depth.depth_compare, Some(wgpu::CompareFunction::LessEqual));
}
}
#[test]
fn the_dest_out_variants_erase_colour_and_alpha_alike() {
for pipeline in [
EnginePipeline::StripDestOut,
EnginePipeline::StripDepthDestOut,
] {
let blend = pipeline.blend().expect("a destination-out variant blends");
assert_eq!(blend, DEST_OUT_BLEND);
assert_eq!(
blend.color,
blend.alpha,
"{} must erase alpha exactly as it erases colour",
pipeline.label()
);
for component in [blend.color, blend.alpha] {
assert_eq!(component.src_factor, wgpu::BlendFactor::Zero);
assert_eq!(
component.dst_factor,
wgpu::BlendFactor::OneMinusSrcAlpha,
"{} must weight the destination by the source's own coverage",
pipeline.label()
);
assert_eq!(component.operation, wgpu::BlendOperation::Add);
}
assert_eq!(pipeline.format(TARGET), TARGET, "the punch is a frame pass");
assert_eq!(pipeline.bind_group_count(), STRIP_BIND_GROUPS);
}
assert_eq!(
EnginePipeline::StripDestOut.depth(),
None,
"the depth-free punch runs when the frame has no depth attachment"
);
assert_eq!(
EnginePipeline::StripDepthDestOut
.depth()
.and_then(|d| d.depth_write_enabled),
Some(false),
"a punch tests the depth the opaque pass established without writing it"
);
}
#[test]
fn the_atlas_target_reuses_the_intermediate_variant() {
assert_eq!(
crate::gpu::atlas::ATLAS_FORMAT,
INTERMEDIATE_FORMAT,
"an atlas layer is drawn through the intermediate variant, so the two formats are one \
decision"
);
assert_eq!(ATLAS_STRIP_PIPELINE, EnginePipeline::StripIntermediate);
assert_eq!(
ATLAS_STRIP_PIPELINE.format(TARGET),
crate::gpu::atlas::ATLAS_FORMAT,
"the atlas pipeline writes the atlas format whatever the frame's own target is"
);
assert_eq!(
ATLAS_STRIP_PIPELINE.blend(),
Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
"a replayed glyph composites source-over onto whatever the slot already holds"
);
assert_eq!(
ATLAS_STRIP_PIPELINE.depth(),
None,
"an atlas layer carries no depth attachment for a pipeline to test against"
);
assert!(ATLAS_STRIP_PIPELINE.is_strip());
let module = ATLAS_STRIP_PIPELINE.module();
assert_eq!(module, EngineShaderModule::Strip);
assert!(EnginePipeline::ALL.contains(&ATLAS_STRIP_PIPELINE));
assert!(
frust_gpu::lint_pipeline_layout(&ATLAS_STRIP_PIPELINE.layout_desc()).is_empty(),
"the atlas pass must stay inside the downlevel design rules"
);
}
#[test]
fn the_clear_and_copy_pipelines_write_the_intermediate_format_unblended() {
for pipeline in [EnginePipeline::Clear, EnginePipeline::Copy] {
assert_eq!(pipeline.format(TARGET), INTERMEDIATE_FORMAT);
assert_eq!(pipeline.blend(), None);
assert_eq!(pipeline.depth(), None);
assert!(!pipeline.is_strip());
}
assert_eq!(EnginePipeline::Clear.bind_group_count(), 0);
assert_eq!(EnginePipeline::Copy.bind_group_count(), COPY_BIND_GROUPS);
}
#[test]
fn the_filter_pipeline_replaces_an_intermediate_region_unblended_and_undepthed() {
let filter = EnginePipeline::Filter;
assert_eq!(filter.module(), EngineShaderModule::Filter);
assert_eq!(
filter.format(TARGET),
INTERMEDIATE_FORMAT,
"a filter pass writes a pooled page, never the frame's own target"
);
assert_eq!(
filter.blend(),
None,
"a blend would turn the padding border's deliberate erase into a no-op"
);
assert_eq!(filter.depth(), None);
assert!(!filter.is_strip());
assert_eq!(filter.bind_group_count(), FILTER_BIND_GROUPS);
assert_eq!(filter.vertex_layouts(), vec![filter_vertex_layout()]);
assert!(
filter.layout_desc().uniform_buffer_sizes.is_empty(),
"a filter pass takes its viewport from its own instance, not a uniform"
);
assert!(EnginePipeline::ALL.contains(&filter));
assert!(
frust_gpu::lint_pipeline_layout(&filter.layout_desc()).is_empty(),
"the filter pass must stay inside the downlevel design rules"
);
}
#[test]
fn the_instance_layouts_match_their_shader_declarations() {
let strip = GpuStrip::vertex_layout();
assert_eq!(strip.step_mode, wgpu::VertexStepMode::Instance);
assert_eq!(strip.attributes.len(), 6);
let clear = clear_vertex_layout();
assert_eq!(clear.step_mode, wgpu::VertexStepMode::Instance);
assert_eq!(clear.attributes.len(), 3);
assert_eq!(clear.array_stride, 24);
let copy = copy_vertex_layout();
assert_eq!(copy.step_mode, wgpu::VertexStepMode::Instance);
assert_eq!(copy.attributes.len(), 4);
assert_eq!(copy.array_stride, 16);
let filter = filter_vertex_layout();
assert_eq!(filter.step_mode, wgpu::VertexStepMode::Instance);
assert_eq!(filter.attributes.len(), 8);
assert_eq!(
filter.array_stride as usize,
size_of::<crate::filters::blur::FilterInstanceData>()
);
}
fn block_on<F: std::future::Future>(future: F) -> F::Output {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut future = std::pin::pin!(future);
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(value) => return value,
Poll::Pending => std::thread::yield_now(),
}
}
}
fn drain_error_scope(
device: &wgpu::Device,
scope: wgpu::ErrorScopeGuard,
) -> Option<wgpu::Error> {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut future = std::pin::pin!(scope.pop());
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(error) => return error,
Poll::Pending => {
let _ = device.poll(wgpu::PollType::wait_indefinitely());
}
}
}
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine -- --ignored`"]
fn every_pipeline_builds_on_a_real_device() {
let (device, _queue) = block_on(async {
let instance = wgpu::Instance::new(
wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
);
let adapter = wgpu::util::initialize_adapter_from_env_or_default(&instance, None)
.await
.expect("no compatible GPU adapter");
println!(
"frust-engine pipeline test adapter: {:?}",
adapter.get_info()
);
adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("frust-engine pipeline test device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
})
.await
.expect("failed to create the device")
});
for image_sampler in [false, true] {
build_every_pipeline(&device, image_sampler);
}
}
fn build_every_pipeline(device: &wgpu::Device, image_sampler: bool) {
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut library = ShaderLibrary::new();
let shaders = EngineShaders::register_family(&mut library, device, image_sampler);
assert_eq!(shaders.image_sampler, image_sampler);
assert_eq!(
library.len(),
shader_src::MODULES.len() + PaintClass::SPECIALISED.len() + usize::from(image_sampler),
"one module per engine shader source, plus one per strip class — and, in the \
sampler family, the manual Uber program the atlas pass draws with"
);
assert_eq!(
EnginePipeline::ALL
.iter()
.filter(|pipeline| pipeline.is_strip())
.count(),
6,
"six render states over the one strip program"
);
let mut cache = PipelineCache::new(std::sync::Arc::new(library), None);
let warm = warm_up_descs_for(&shaders, TARGET, false);
let atlas_extra = usize::from(image_sampler);
assert_eq!(
warm.len(),
EnginePipeline::ALL.len() + WARM_CLASSES.len() * WARM_CLASS_STATES.len() + atlas_extra
);
assert_eq!(
warm_up_descs_for(&shaders, TARGET, true).len(),
EnginePipeline::ALL.len() + atlas_extra,
"the kill switch warms the Uber states alone"
);
let mut descs = Vec::new();
for pipeline in EnginePipeline::ALL {
for class in PaintClass::ALL {
if pipeline.serves(class) {
descs.push(pipeline.class_desc(&shaders, TARGET, class));
}
}
}
assert_eq!(descs.len(), VARIANT_COUNT);
let atlas = atlas_strip_desc(&shaders);
assert!(
warm.contains(&atlas),
"the atlas description must be one the warm-up list already covers"
);
assert_eq!(descs.contains(&atlas), !image_sampler);
if image_sampler {
descs.push(atlas);
}
for desc in &warm {
assert!(
descs.contains(desc),
"a warmed variant the engine never builds"
);
}
for desc in &descs {
let _pipeline = cache.get_or_create(device, desc);
}
for desc in &descs {
let _pipeline = cache.get_or_create(device, desc);
}
let error = drain_error_scope(device, scope);
assert!(error.is_none(), "pipeline creation raised {error:?}");
assert_eq!(
cache.compiled_variants(),
(VARIANT_COUNT + atlas_extra) as u64,
"each engine pipeline variant must compile exactly once"
);
}
}