use core::ops::DerefMut;
use unlit_ecs::World;
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::source::{FrameSource, FrameSourceOrder, RenderContext, frame_target};
const VERTEX_MODULE: &str = "fullscreen_vertex";
const VERTEX_ENTRY: &str = "fullscreen_vertex_shader";
const FRAGMENT_MODULE: &str = "blit";
const FRAGMENT_ENTRY: &str = "fs_main";
fn resource_graph<'w>(
world: &'w World,
ctx: RenderContext,
) -> impl DerefMut<Target = ResourceGraph> + 'w {
world
.get_mut::<ResourceGraph>(ctx.graph)
.expect("the context's resource graph exists")
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BlitTexture {
view: ResHandle<TextureView>,
sampler: ResHandle<wgpu::Sampler>,
}
impl BlitTexture {
pub fn new(world: &World, ctx: RenderContext, view: TextureView) -> Self {
let sampler = {
let device = world
.get::<wgpu::Device>(ctx.device)
.expect("the context's device resource exists");
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()
})
};
Self::with_sampler(world, ctx, view, sampler)
}
pub fn with_sampler(
world: &World,
ctx: RenderContext,
view: TextureView,
sampler: wgpu::Sampler,
) -> Self {
let mut graph = resource_graph(world, ctx);
let view = graph.insert(view, None);
let sampler = graph.insert(sampler, None);
Self { view, sampler }
}
}
#[derive(Default)]
pub struct BlitSource {
scene: Scene,
gpu: Option<Gpu>,
surface: Option<SurfaceKey>,
}
struct Gpu {
pipeline: wgpu::RenderPipeline,
layout: wgpu::BindGroupLayout,
texture: BlitTexture,
bind_group: ResHandle<wgpu::BindGroup>,
}
impl BlitSource {
pub fn new() -> Self {
Self::default()
}
fn build_gpu(
&mut self,
world: &World,
ctx: RenderContext,
texture: &BlitTexture,
surface: SurfaceKey,
) {
let device = wgpu::Device::clone(
&world
.get::<wgpu::Device>(ctx.device)
.expect("the context's device resource exists"),
);
let layout = bind_group_layout(&device);
let pipeline = pipeline(&device, &layout, surface);
let bind_group = allocate_bind_group(world, ctx, &device, &layout, texture);
self.gpu = Some(Gpu {
pipeline,
layout,
texture: texture.clone(),
bind_group,
});
self.surface = Some(surface);
}
}
fn allocate_bind_group(
world: &World,
ctx: RenderContext,
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
texture: &BlitTexture,
) -> ResHandle<wgpu::BindGroup> {
let recipe = bind_group_recipe(
device.clone(),
layout.clone(),
texture.view.clone(),
texture.sampler.clone(),
);
let mut graph = resource_graph(world, ctx);
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, &texture.view);
graph.add_dependency(&bind_group, &texture.sampler);
bind_group
}
impl FrameSource for BlitSource {
fn build_scene(
&mut self,
world: &World,
ctx: RenderContext,
_encoder: &mut wgpu::CommandEncoder,
) {
let maintain = ctx.maintain_scope(world);
self.scene.clear();
let Some(target) = frame_target(world) else {
log::warn!(
"BlitSource::build_scene: no FrameTarget in the world, so nothing is blitted"
);
return;
};
let Some((_, texture)) = world.query::<&BlitTexture>().next() else {
log::warn!(
"BlitSource::build_scene: no BlitTexture component, so there is no texture to blit"
);
return;
};
if self.gpu.is_none() || self.surface != Some(target.surface) {
self.build_gpu(world, ctx, &texture, target.surface);
} else if let Some(gpu) = &mut self.gpu
&& gpu.texture != *texture
{
let layout = gpu.layout.clone();
let device = world
.get::<wgpu::Device>(ctx.device)
.expect("the context's device resource exists");
gpu.bind_group = allocate_bind_group(world, ctx, &device, &layout, &texture);
gpu.texture = texture.clone();
}
drop(maintain);
let graph = resource_graph(world, ctx);
let gpu = self.gpu.as_ref().expect("the pipeline was just built");
let bind_group = graph
.get(&gpu.bind_group)
.expect("the bind group is registered in the graph");
self.scene.push(
DrawEntry::new(&gpu.pipeline, DrawRange::vertices(0..3)).with_bind_group(0, bind_group),
);
}
fn scene(&self) -> &Scene {
&self.scene
}
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"));
}
}