use std::collections::{HashMap, HashSet};
use std::pin::pin;
use std::task::{Context, Poll, Waker};
use noesis_runtime::render_device::types::{
Batch, BlendMode, FORMAT_FOR_VERTEX, RenderState, Shader, StencilMode, VERTEX_FOR_SHADER,
};
use crate::custom_shader::{
MAX_SHADER_CONSTANTS, MAX_SHADER_TEXTURES, PixelShaderDesc, PixelShaderError,
PixelShaderHandle, PixelShaderKind,
};
use crate::shader_defines::{defines_for_custom, defines_for_shader};
use crate::shader_preproc::preprocess;
use crate::vertex_layout::{attributes_for_format, stride_for_format};
const NOESIS_WGSL: &str = include_str!("shaders/noesis.wgsl");
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct PipelineKey {
pub shader: u8,
pub render_state: u8,
pub vertex_format: u8,
pub has_stencil: bool,
pub sample_count: u32,
pub pixel_shader: Option<PixelShaderHandle>,
}
impl PipelineKey {
#[must_use]
pub fn from_batch(
batch: &Batch,
has_stencil: bool,
sample_count: u32,
pixel_shader: Option<PixelShaderHandle>,
) -> Option<Self> {
let vshader = *VERTEX_FOR_SHADER.get(usize::from(batch.shader.0))?;
let vfmt = FORMAT_FOR_VERTEX[vshader as usize];
Some(Self {
shader: batch.shader.0,
render_state: batch.render_state.0,
vertex_format: vfmt,
has_stencil,
sample_count,
pixel_shader,
})
}
}
pub const STENCIL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Stencil8;
fn depth_stencil_for(render_state: RenderState) -> wgpu::DepthStencilState {
use wgpu::{CompareFunction, StencilOperation};
let (compare, pass_op, fail_op) = match render_state.stencil_mode_raw() {
0 | 5 => (
CompareFunction::Always,
StencilOperation::Keep,
StencilOperation::Keep,
),
1 | 6 => (
CompareFunction::Equal,
StencilOperation::Keep,
StencilOperation::Keep,
),
2 => (
CompareFunction::Equal,
StencilOperation::IncrementWrap,
StencilOperation::Keep,
),
3 => (
CompareFunction::Equal,
StencilOperation::DecrementWrap,
StencilOperation::Keep,
),
4 => (
CompareFunction::Always,
StencilOperation::Zero,
StencilOperation::Replace,
),
other => {
warn_once!("unknown StencilMode raw value {other}; disabling stencil test");
(
CompareFunction::Always,
StencilOperation::Keep,
StencilOperation::Keep,
)
}
};
let face = wgpu::StencilFaceState {
compare,
fail_op,
depth_fail_op: StencilOperation::Keep,
pass_op,
};
wgpu::DepthStencilState {
format: STENCIL_FORMAT,
depth_write_enabled: Some(false),
depth_compare: Some(CompareFunction::Always),
stencil: wgpu::StencilState {
front: face,
back: face,
read_mask: 0xff,
write_mask: 0xff,
},
bias: wgpu::DepthBiasState::default(),
}
}
pub struct CustomShader {
pub label: String,
pub kind: PixelShaderKind,
pub source: String,
pub textures: u32,
pub constants: u32,
}
pub struct PipelineCache {
device: wgpu::Device,
pipeline_layout: wgpu::PipelineLayout,
custom_pipeline_layout: wgpu::PipelineLayout,
target_format: wgpu::TextureFormat,
cache: HashMap<PipelineKey, wgpu::RenderPipeline>,
custom_shaders: HashMap<PixelShaderHandle, CustomShader>,
failed: HashSet<PipelineKey>,
}
impl PipelineCache {
#[must_use]
pub fn new(
device: wgpu::Device,
pipeline_layout: wgpu::PipelineLayout,
custom_pipeline_layout: wgpu::PipelineLayout,
target_format: wgpu::TextureFormat,
) -> Self {
Self {
device,
pipeline_layout,
custom_pipeline_layout,
target_format,
cache: HashMap::new(),
custom_shaders: HashMap::new(),
failed: HashSet::new(),
}
}
pub fn ensure(&mut self, key: PipelineKey) -> bool {
if self.cache.contains_key(&key) {
return true;
}
if let Some(handle) = key.pixel_shader {
return self.ensure_custom(key, handle);
}
let Some(defines) = defines_for_shader(Shader(key.shader)) else {
return false;
};
if key.shader == Shader::SDF_LCD_SOLID.0
&& !self
.device
.features()
.contains(wgpu::Features::DUAL_SOURCE_BLENDING)
{
return false;
}
let source = preprocess(NOESIS_WGSL, &defines);
let pipeline = build_pipeline(
&self.device,
&self.pipeline_layout,
self.target_format,
key,
source,
);
self.cache.insert(key, pipeline);
true
}
fn ensure_custom(&mut self, key: PipelineKey, handle: PixelShaderHandle) -> bool {
if self.failed.contains(&key) {
return false;
}
let Some(shader) = self.custom_shaders.get(&handle) else {
return false;
};
match build_custom_pipeline(
&self.device,
&self.custom_pipeline_layout,
self.target_format,
key,
shader,
) {
Ok(pipeline) => {
self.cache.insert(key, pipeline);
true
}
Err(error) => {
log::warn!(
"custom shader '{}' failed as Noesis shader {}: {error}",
shader.label,
key.shader,
);
self.failed.insert(key);
false
}
}
}
pub fn register_custom(
&mut self,
handle: PixelShaderHandle,
desc: &PixelShaderDesc<'_>,
) -> Result<(), PixelShaderError> {
if desc.textures as usize > MAX_SHADER_TEXTURES {
return Err(PixelShaderError(format!(
"'{}' samples {} extra textures; the most is {MAX_SHADER_TEXTURES}",
desc.label, desc.textures,
)));
}
if desc.constants > MAX_SHADER_CONSTANTS {
return Err(PixelShaderError(format!(
"'{}' has {} bytes of constants; the most is {MAX_SHADER_CONSTANTS}",
desc.label, desc.constants,
)));
}
let shader = CustomShader {
label: desc.label.to_owned(),
kind: desc.kind,
source: desc.source.to_owned(),
textures: desc.textures,
constants: desc.constants,
};
let probe = Shader(match desc.kind {
PixelShaderKind::Brush => Shader::PATH_PATTERN.0,
PixelShaderKind::Effect => Shader::CUSTOM_EFFECT.0,
});
let vshader = VERTEX_FOR_SHADER[usize::from(probe.0)];
let key = PipelineKey {
shader: probe.0,
render_state: RenderState::new(true, BlendMode::SrcOver, StencilMode::Disabled, false)
.0,
vertex_format: FORMAT_FOR_VERTEX[vshader as usize],
has_stencil: false,
sample_count: 1,
pixel_shader: Some(handle),
};
let pipeline = build_custom_pipeline(
&self.device,
&self.custom_pipeline_layout,
self.target_format,
key,
&shader,
)
.map_err(|error| PixelShaderError(format!("'{}': {error}", desc.label)))?;
self.cache.insert(key, pipeline);
self.custom_shaders.insert(handle, shader);
Ok(())
}
pub fn remove_custom(&mut self, handle: PixelShaderHandle) {
self.custom_shaders.remove(&handle);
self.cache.retain(|key, _| key.pixel_shader != Some(handle));
self.failed.retain(|key| key.pixel_shader != Some(handle));
}
#[must_use]
pub fn custom(&self, handle: PixelShaderHandle) -> Option<&CustomShader> {
self.custom_shaders.get(&handle)
}
#[must_use]
pub fn len(&self) -> usize {
self.cache.len()
}
#[must_use]
pub fn get(&self, key: PipelineKey) -> &wgpu::RenderPipeline {
self.cache
.get(&key)
.expect("pipeline not built; call ensure() before get()")
}
}
fn blend_state_for(blend_mode_raw: u8) -> Option<wgpu::BlendState> {
let comp = |src, dst| wgpu::BlendComponent {
src_factor: src,
dst_factor: dst,
operation: wgpu::BlendOperation::Add,
};
let src_over_alpha = comp(wgpu::BlendFactor::One, wgpu::BlendFactor::OneMinusSrcAlpha);
match blend_mode_raw {
0 => None, 1 => Some(wgpu::BlendState {
color: src_over_alpha,
alpha: src_over_alpha,
}),
2 => Some(wgpu::BlendState {
color: comp(wgpu::BlendFactor::Dst, wgpu::BlendFactor::OneMinusSrcAlpha),
alpha: src_over_alpha,
}),
3 => Some(wgpu::BlendState {
color: comp(wgpu::BlendFactor::One, wgpu::BlendFactor::OneMinusSrc),
alpha: src_over_alpha,
}),
4 => Some(wgpu::BlendState {
color: comp(wgpu::BlendFactor::One, wgpu::BlendFactor::One),
alpha: src_over_alpha,
}),
5 => Some(wgpu::BlendState {
color: comp(wgpu::BlendFactor::One, wgpu::BlendFactor::OneMinusSrc1),
alpha: comp(wgpu::BlendFactor::One, wgpu::BlendFactor::OneMinusSrc1Alpha),
}),
other => {
warn_once!("unknown BlendMode raw value {other}; using SrcOver");
Some(wgpu::BlendState {
color: src_over_alpha,
alpha: src_over_alpha,
})
}
}
}
fn build_custom_pipeline(
device: &wgpu::Device,
layout: &wgpu::PipelineLayout,
target_format: wgpu::TextureFormat,
key: PipelineKey,
shader: &CustomShader,
) -> Result<wgpu::RenderPipeline, String> {
let Some(defines) = defines_for_custom(Shader(key.shader), shader.kind, shader.constants > 0)
else {
return Err(format!("a {:?} can't draw as this shader", shader.kind));
};
let mut source = preprocess(NOESIS_WGSL, &defines);
source.push_str(&shader.source);
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let pipeline = build_pipeline(device, layout, target_format, key, source);
let mut error = pin!(scope.pop());
match error.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
Poll::Ready(Some(error)) => Err(error.to_string()),
_ => Ok(pipeline),
}
}
fn build_pipeline(
device: &wgpu::Device,
layout: &wgpu::PipelineLayout,
target_format: wgpu::TextureFormat,
key: PipelineKey,
source: String,
) -> wgpu::RenderPipeline {
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(&format!("noesis_wgpu Shader({})", key.shader)),
source: wgpu::ShaderSource::Wgsl(source.into()),
});
let attrs = attributes_for_format(key.vertex_format);
let vertex_layout = wgpu::VertexBufferLayout {
array_stride: stride_for_format(key.vertex_format),
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &attrs,
};
let render_state = RenderState(key.render_state);
let blend = blend_state_for(render_state.blend_mode_raw());
let write_mask = if render_state.color_enable() {
wgpu::ColorWrites::ALL
} else {
wgpu::ColorWrites::empty()
};
let depth_stencil = key.has_stencil.then(|| depth_stencil_for(render_state));
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(&format!(
"noesis_wgpu pipeline shader={} state=0x{:02x} fmt={}",
key.shader, key.render_state, key.vertex_format
)),
layout: Some(layout),
vertex: wgpu::VertexState {
module: &module,
entry_point: Some("vs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[Some(vertex_layout)],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil,
multisample: wgpu::MultisampleState {
count: key.sample_count,
..wgpu::MultisampleState::default()
},
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some("fs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend,
write_mask,
})],
}),
multiview_mask: None,
cache: None,
})
}