use std::collections::HashMap;
use bevy::log::warn_once;
use noesis_runtime::render_device::types::{
Batch, FORMAT_FOR_VERTEX, RenderState, VERTEX_FOR_SHADER,
};
use crate::render_device::shader_defines::defines_for_shader;
use crate::render_device::shader_preproc::preprocess;
use crate::render_device::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,
}
impl PipelineKey {
#[must_use]
pub fn from_batch(batch: &Batch, has_stencil: bool) -> Self {
let vshader = VERTEX_FOR_SHADER[batch.shader.0 as usize];
let vfmt = FORMAT_FOR_VERTEX[vshader as usize];
Self {
shader: batch.shader.0,
render_state: batch.render_state.0,
vertex_format: vfmt,
has_stencil,
}
}
}
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 PipelineCache {
device: wgpu::Device,
pipeline_layout: wgpu::PipelineLayout,
target_format: wgpu::TextureFormat,
cache: HashMap<PipelineKey, wgpu::RenderPipeline>,
}
impl PipelineCache {
#[must_use]
pub fn new(
device: wgpu::Device,
pipeline_layout: wgpu::PipelineLayout,
target_format: wgpu::TextureFormat,
) -> Self {
Self {
device,
pipeline_layout,
target_format,
cache: HashMap::new(),
}
}
pub fn ensure(&mut self, key: PipelineKey) {
self.cache.entry(key).or_insert_with(|| {
build_pipeline(&self.device, &self.pipeline_layout, self.target_format, key)
});
}
#[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_pipeline(
device: &wgpu::Device,
layout: &wgpu::PipelineLayout,
target_format: wgpu::TextureFormat,
key: PipelineKey,
) -> wgpu::RenderPipeline {
let defines = defines_for_shader(noesis_runtime::render_device::types::Shader(key.shader));
let source = preprocess(NOESIS_WGSL, &defines);
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(&format!("noesis_runtime 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_runtime 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: &[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::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,
})
}