use unlit_ecs::Scope;
use unlit_wgpu::resources::{Rebuild, ResHandle, Resource, ResourceGraph, TextureView};
use unlit_wgpu::scene::{DrawEntry, DrawRange, Scene};
use unlit_wgpu::specialize::{
FragmentStateDesc, PipelineDescriptor, RenderPipelineDesc, SurfaceKey, SurfaceTarget,
VertexStateDesc,
};
use crate::pass::PassInfo;
use crate::source::{FrameSource, FrameSourceOrder, RenderContext};
const VERTEX_MODULE: &str = "fullscreen_vertex";
const VERTEX_ENTRY: &str = "fullscreen_vertex_shader";
const FRAGMENT_MODULE: &str = "blit";
const FRAGMENT_ENTRY: &str = "fs_main";
pub struct BlitSource {
pub view: ResHandle<TextureView>,
gpus: Vec<(SurfaceKey, Gpu)>,
}
struct Gpu {
pipeline: wgpu::RenderPipeline,
layout: wgpu::BindGroupLayout,
sampler: ResHandle<wgpu::Sampler>,
view: ResHandle<TextureView>,
bind_group: ResHandle<wgpu::BindGroup>,
}
impl BlitSource {
pub fn new(view: ResHandle<TextureView>) -> Self {
Self {
view,
gpus: Vec::new(),
}
}
fn build_gpu(&self, scope: &Scope<'_>, ctx: RenderContext, surface: SurfaceKey) -> Gpu {
let device = wgpu::Device::clone(
&scope
.query_one::<&wgpu::Device>(ctx.device)
.expect("the context's device resource exists"),
);
let sampler = ctx.graph(scope).insert(sampler(&device), None);
let layout = bind_group_layout(&device);
let pipeline = pipeline(&device, &layout, surface);
let bind_group = allocate_bind_group(
scope,
ctx,
&device,
&layout,
sampler.clone(),
self.view.clone(),
);
Gpu {
pipeline,
layout,
sampler,
view: self.view.clone(),
bind_group,
}
}
}
fn sampler(device: &wgpu::Device) -> wgpu::Sampler {
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("unlit3d::blit::sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
})
}
fn allocate_bind_group(
scope: &Scope<'_>,
ctx: RenderContext,
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
sampler: ResHandle<wgpu::Sampler>,
view: ResHandle<TextureView>,
) -> ResHandle<wgpu::BindGroup> {
let recipe = bind_group_recipe(
device.clone(),
layout.clone(),
view.clone(),
sampler.clone(),
);
let mut graph = ctx.graph(scope);
let bind_group = recipe(&graph);
let rebuild = Rebuild::new(move |graph| Resource::BindGroup(recipe(graph)));
let bind_group = graph.insert(bind_group, Some(rebuild));
graph.add_dependency(&bind_group, &view);
graph.add_dependency(&bind_group, &sampler);
bind_group
}
impl FrameSource for BlitSource {
fn build_scene(
&mut self,
scope: &Scope<'_>,
ctx: RenderContext,
info: PassInfo,
scene: &mut Scene,
_encoder: &mut wgpu::CommandEncoder,
) {
let maintain = ctx.maintain_scope(*scope);
let index = self.gpus.iter().position(|(key, _)| *key == info.surface);
let index = match index {
Some(index) => index,
None => {
let gpu = self.build_gpu(scope, ctx, info.surface);
self.gpus.push((info.surface, gpu));
self.gpus.len() - 1
}
};
if self.gpus[index].1.view != self.view {
let gpu = &mut self.gpus[index].1;
let device = scope
.query_one::<&wgpu::Device>(ctx.device)
.expect("the context's device resource exists");
gpu.bind_group = allocate_bind_group(
scope,
ctx,
&device,
&gpu.layout,
gpu.sampler.clone(),
self.view.clone(),
);
gpu.view = self.view.clone();
}
drop(maintain);
let graph = ctx.graph(scope);
let gpu = &self.gpus[index].1;
let bind_group = graph
.get(&gpu.bind_group)
.expect("the bind group is registered in the graph");
scene.draws.push(
DrawEntry::new(&gpu.pipeline, DrawRange::vertices(0..3)).with_bind_group(0, bind_group),
);
}
fn order(&self) -> FrameSourceOrder {
FrameSourceOrder::MESH
}
}
fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("unlit3d::blit::bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
})
}
fn bind_group_recipe(
device: wgpu::Device,
layout: wgpu::BindGroupLayout,
view: ResHandle<TextureView>,
sampler: ResHandle<wgpu::Sampler>,
) -> impl Fn(&ResourceGraph) -> wgpu::BindGroup {
move |graph| {
let view = graph
.get(&view)
.expect("the blit view is registered in the graph");
let sampler = graph
.get(&sampler)
.expect("the blit sampler is registered in the graph");
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("unlit3d::blit::bind_group"),
layout: &layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view.view()),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
})
}
}
fn pipeline(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
surface: SurfaceKey,
) -> wgpu::RenderPipeline {
let vertex = shader_module(device, VERTEX_MODULE, "unlit3d::blit::vertex");
let fragment = shader_module(device, FRAGMENT_MODULE, "unlit3d::blit::fragment");
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("unlit3d::blit::pipeline_layout"),
bind_group_layouts: &[Some(layout)],
immediate_size: 0,
});
let mut descriptor = RenderPipelineDesc {
label: Some("unlit3d::blit".to_owned()),
layout: Some(pipeline_layout),
vertex: VertexStateDesc {
module: vertex,
entry_point: Some(VERTEX_ENTRY.to_owned()),
compilation_options: Default::default(),
buffers: Vec::new(),
},
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
fragment: Some(FragmentStateDesc {
module: fragment,
entry_point: Some(FRAGMENT_ENTRY.to_owned()),
compilation_options: Default::default(),
targets: vec![Some(wgpu::ColorTargetState {
format: surface.color_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
};
descriptor.set_surface(surface);
descriptor.create(device)
}
fn shader_module(device: &wgpu::Device, module: &str, label: &str) -> wgpu::ShaderModule {
let wgsl = compose(module)
.unwrap_or_else(|error| panic!("the built-in unlit_wgpu shader compiles: {error}"));
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(label),
source: wgpu::ShaderSource::Wgsl(wgsl.into()),
})
}
fn compose(module: &str) -> Result<String, wesl::Error> {
let main_path = wesl::syntax::ModulePath::new(
wesl::syntax::PathOrigin::Package("unlit_wgpu".to_owned()),
vec![module.to_owned()],
);
let options = wesl::CompileOptions {
keep_main: true,
..Default::default()
};
let mut resolver = wesl::resolver::PackageResolver::new();
resolver.add_package(&unlit_wgpu::shader::PACKAGE);
wesl::Compiler::new_with_resolver(options, resolver)
.compile_module(&main_path)
.map(|result| result.syntax.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_module_keeps_its_entry_point_name() {
let vertex = compose(VERTEX_MODULE).expect("the fullscreen vertex composes");
assert!(vertex.contains("fn fullscreen_vertex_shader"));
let fragment = compose(FRAGMENT_MODULE).expect("the blit fragment composes");
assert!(fragment.contains("fn fs_main"));
}
}