Build Rust render graph core
Keep hot render state in a generic shared SOA arena and use worker messages only for allocation, graph compilation, loadout changes, and render pacing. Compile external S-expression graphs into upfront WebGPU resources, transient aliases, and reusable render bundles. Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -0,0 +1,545 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::gpu::Wgpu;
|
||||
use crate::graph::{Binding, Extent, Pass, RenderGraph, RenderPipeline, Texture};
|
||||
use crate::render_data::RenderData;
|
||||
|
||||
pub struct GpuResources {
|
||||
pub buffers: HashMap<String, GpuBuffer>,
|
||||
pub textures: Vec<GpuTexture>,
|
||||
pub texture_slots: HashMap<String, usize>,
|
||||
pub samplers: HashMap<String, wgpu::Sampler>,
|
||||
pub render_pipelines: HashMap<String, wgpu::RenderPipeline>,
|
||||
pub compute_pipelines: HashMap<String, wgpu::ComputePipeline>,
|
||||
pub passes: Vec<GpuPass>,
|
||||
}
|
||||
|
||||
pub struct GpuBuffer {
|
||||
pub buffer: wgpu::Buffer,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
pub struct GpuTexture {
|
||||
pub _texture: wgpu::Texture,
|
||||
pub view: wgpu::TextureView,
|
||||
}
|
||||
|
||||
pub enum GpuPass {
|
||||
Render(wgpu::RenderBundle),
|
||||
Compute {
|
||||
pipeline: wgpu::ComputePipeline,
|
||||
bind_groups: Vec<(u32, wgpu::BindGroup)>,
|
||||
},
|
||||
}
|
||||
|
||||
impl GpuResources {
|
||||
pub fn activate(graph: &RenderGraph, gpu: &Wgpu, data: &RenderData) -> Result<Self, String> {
|
||||
let mut buffers = HashMap::new();
|
||||
for source in &graph.resources.buffers {
|
||||
let rows = data.rows(&source.array).ok_or("GRAPH_ARRAY_UNKNOWN")?;
|
||||
let buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some(&source.id),
|
||||
size: u64::from(rows.bytes.max(4)),
|
||||
usage: buffer_usage(&source.usage)?,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
gpu.queue
|
||||
.write_buffer(&buffer, 0, data.bytes(&source.array).unwrap());
|
||||
buffers.insert(
|
||||
source.id.clone(),
|
||||
GpuBuffer {
|
||||
buffer,
|
||||
source: source.array.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let mut textures = Vec::new();
|
||||
let mut physical_slots = HashMap::new();
|
||||
let mut texture_slots = HashMap::new();
|
||||
for source in &graph.resources.textures {
|
||||
let physical = match physical_slots.get(&source.slot) {
|
||||
Some(&physical) => physical,
|
||||
None => {
|
||||
let descriptor = texture_descriptor(source, gpu.width, gpu.height)?;
|
||||
let texture = gpu.device.create_texture(&descriptor);
|
||||
let physical = textures.len();
|
||||
textures.push(GpuTexture {
|
||||
view: texture.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||
_texture: texture,
|
||||
});
|
||||
physical_slots.insert(source.slot, physical);
|
||||
physical
|
||||
}
|
||||
};
|
||||
texture_slots.insert(source.id.clone(), physical);
|
||||
}
|
||||
|
||||
let mut samplers = HashMap::new();
|
||||
for source in &graph.resources.samplers {
|
||||
samplers.insert(
|
||||
source.id.clone(),
|
||||
gpu.device
|
||||
.create_sampler(&sampler_descriptor(&source.id, &source.descriptor)?),
|
||||
);
|
||||
}
|
||||
|
||||
let render_pipelines = graph
|
||||
.pipelines
|
||||
.render
|
||||
.iter()
|
||||
.map(|source| {
|
||||
create_render_pipeline(source, gpu).map(|pipeline| (source.id.clone(), pipeline))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>, _>>()?;
|
||||
let compute_pipelines = graph
|
||||
.pipelines
|
||||
.compute
|
||||
.iter()
|
||||
.map(|source| {
|
||||
let module = gpu
|
||||
.device
|
||||
.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some(&source.id),
|
||||
source: wgpu::ShaderSource::Wgsl(source.code.clone().into()),
|
||||
});
|
||||
let pipeline =
|
||||
gpu.device
|
||||
.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: Some(&source.id),
|
||||
layout: None,
|
||||
module: &module,
|
||||
entry_point: Some(&source.entry),
|
||||
compilation_options: Default::default(),
|
||||
cache: None,
|
||||
});
|
||||
Ok::<_, String>((source.id.clone(), pipeline))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>, _>>()?;
|
||||
|
||||
let mut resources = Self {
|
||||
buffers,
|
||||
textures,
|
||||
texture_slots,
|
||||
samplers,
|
||||
render_pipelines,
|
||||
compute_pipelines,
|
||||
passes: Vec::new(),
|
||||
};
|
||||
for pass in &graph.passes {
|
||||
let compiled = match pass.kind.as_str() {
|
||||
"render" => GpuPass::Render(resources.render_bundle(graph, pass, gpu)?),
|
||||
"compute" => {
|
||||
let pipeline = resources
|
||||
.compute_pipelines
|
||||
.get(&pass.pipeline)
|
||||
.ok_or("GRAPH_PIPELINE")?
|
||||
.clone();
|
||||
let bind_groups = resources.bind_groups(
|
||||
pass,
|
||||
|group| pipeline.get_bind_group_layout(group),
|
||||
gpu,
|
||||
)?;
|
||||
GpuPass::Compute {
|
||||
pipeline,
|
||||
bind_groups,
|
||||
}
|
||||
}
|
||||
_ => return Err("GRAPH_PASS".into()),
|
||||
};
|
||||
resources.passes.push(compiled);
|
||||
}
|
||||
Ok(resources)
|
||||
}
|
||||
|
||||
pub fn texture_view(&self, id: &str) -> Option<&wgpu::TextureView> {
|
||||
self.texture_slots
|
||||
.get(id)
|
||||
.and_then(|slot| self.textures.get(*slot))
|
||||
.map(|texture| &texture.view)
|
||||
}
|
||||
|
||||
fn render_bundle(
|
||||
&self,
|
||||
graph: &RenderGraph,
|
||||
pass: &Pass,
|
||||
gpu: &Wgpu,
|
||||
) -> Result<wgpu::RenderBundle, String> {
|
||||
let pipeline = self
|
||||
.render_pipelines
|
||||
.get(&pass.pipeline)
|
||||
.ok_or("GRAPH_PIPELINE")?;
|
||||
let declaration = graph
|
||||
.pipelines
|
||||
.render
|
||||
.iter()
|
||||
.find(|pipeline| pipeline.id == pass.pipeline)
|
||||
.ok_or("GRAPH_PIPELINE")?;
|
||||
let color_formats = pass
|
||||
.color
|
||||
.iter()
|
||||
.map(|attachment| attachment_format(graph, &attachment.resource, gpu.format).map(Some))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let depth_stencil = pass
|
||||
.depth
|
||||
.as_ref()
|
||||
.map(|attachment| {
|
||||
Ok::<_, String>(wgpu::RenderBundleDepthStencil {
|
||||
format: attachment_format(graph, &attachment.resource, gpu.format)?,
|
||||
depth_read_only: false,
|
||||
stencil_read_only: true,
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
let mut encoder =
|
||||
gpu.device
|
||||
.create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
|
||||
label: Some(&pass.id),
|
||||
color_formats: &color_formats,
|
||||
depth_stencil,
|
||||
sample_count: multisample(&declaration.multisample)?.count,
|
||||
multiview: None,
|
||||
});
|
||||
encoder.set_pipeline(pipeline);
|
||||
for (group, bind_group) in
|
||||
self.bind_groups(pass, |group| pipeline.get_bind_group_layout(group), gpu)?
|
||||
{
|
||||
encoder.set_bind_group(group, &bind_group, &[]);
|
||||
}
|
||||
for binding in &pass.vertex_buffers {
|
||||
let buffer = &self
|
||||
.buffers
|
||||
.get(&binding.resource)
|
||||
.ok_or("GRAPH_RESOURCE_UNKNOWN")?
|
||||
.buffer;
|
||||
encoder.set_vertex_buffer(binding.slot, buffer.slice(binding.offset..));
|
||||
}
|
||||
if let Some(binding) = &pass.index_buffer {
|
||||
let buffer = &self
|
||||
.buffers
|
||||
.get(&binding.resource)
|
||||
.ok_or("GRAPH_RESOURCE_UNKNOWN")?
|
||||
.buffer;
|
||||
encoder.set_index_buffer(
|
||||
buffer.slice(binding.offset..),
|
||||
parse(&binding.format, "GRAPH_INDEX_FORMAT")?,
|
||||
);
|
||||
encoder.draw_indexed(
|
||||
pass.draw.first_index..pass.draw.first_index + pass.draw.indices,
|
||||
pass.draw.base_vertex,
|
||||
pass.draw.first_instance..pass.draw.first_instance + pass.draw.instances,
|
||||
);
|
||||
} else {
|
||||
encoder.draw(
|
||||
pass.draw.first_vertex..pass.draw.first_vertex + pass.draw.vertices,
|
||||
pass.draw.first_instance..pass.draw.first_instance + pass.draw.instances,
|
||||
);
|
||||
}
|
||||
Ok(encoder.finish(&wgpu::RenderBundleDescriptor {
|
||||
label: Some(&pass.id),
|
||||
}))
|
||||
}
|
||||
|
||||
fn bind_groups(
|
||||
&self,
|
||||
pass: &Pass,
|
||||
layout: impl Fn(u32) -> wgpu::BindGroupLayout,
|
||||
gpu: &Wgpu,
|
||||
) -> Result<Vec<(u32, wgpu::BindGroup)>, String> {
|
||||
let mut groups: BTreeMap<u32, Vec<&Binding>> = BTreeMap::new();
|
||||
for binding in &pass.bindings {
|
||||
groups.entry(binding.group).or_default().push(binding);
|
||||
}
|
||||
groups
|
||||
.into_iter()
|
||||
.map(|(group, bindings)| {
|
||||
let entries = bindings
|
||||
.into_iter()
|
||||
.map(|binding| {
|
||||
let resource = if let Some(buffer) = self.buffers.get(&binding.resource) {
|
||||
wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||
buffer: &buffer.buffer,
|
||||
offset: binding.offset,
|
||||
size: binding.size.and_then(NonZeroU64::new),
|
||||
})
|
||||
} else if let Some(view) = self.texture_view(&binding.resource) {
|
||||
wgpu::BindingResource::TextureView(view)
|
||||
} else if let Some(sampler) = self.samplers.get(&binding.resource) {
|
||||
wgpu::BindingResource::Sampler(sampler)
|
||||
} else {
|
||||
return Err("GRAPH_RESOURCE_UNKNOWN".into());
|
||||
};
|
||||
Ok(wgpu::BindGroupEntry {
|
||||
binding: binding.binding,
|
||||
resource,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some(&pass.id),
|
||||
layout: &layout(group),
|
||||
entries: &entries,
|
||||
});
|
||||
Ok((group, bind_group))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_render_pipeline(
|
||||
source: &RenderPipeline,
|
||||
gpu: &Wgpu,
|
||||
) -> Result<wgpu::RenderPipeline, String> {
|
||||
let module = gpu
|
||||
.device
|
||||
.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some(&source.id),
|
||||
source: wgpu::ShaderSource::Wgsl(source.code.clone().into()),
|
||||
});
|
||||
let attributes = source
|
||||
.vertex
|
||||
.buffers
|
||||
.iter()
|
||||
.map(|buffer| {
|
||||
buffer
|
||||
.attributes
|
||||
.iter()
|
||||
.map(|attribute| {
|
||||
Ok(wgpu::VertexAttribute {
|
||||
format: parse(&attribute.format, "GRAPH_VERTEX_FORMAT")?,
|
||||
offset: attribute.offset,
|
||||
shader_location: attribute.shader_location,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let layouts = source
|
||||
.vertex
|
||||
.buffers
|
||||
.iter()
|
||||
.zip(&attributes)
|
||||
.map(|(buffer, attributes)| {
|
||||
Ok(wgpu::VertexBufferLayout {
|
||||
array_stride: buffer.array_stride,
|
||||
step_mode: parse(&buffer.step_mode, "GRAPH_VERTEX_STEP")?,
|
||||
attributes,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
let targets = source
|
||||
.fragment
|
||||
.targets
|
||||
.iter()
|
||||
.map(|target| {
|
||||
let format = if target.format == "canvas" {
|
||||
gpu.format
|
||||
} else {
|
||||
parse(&target.format, "GRAPH_TEXTURE_FORMAT")?
|
||||
};
|
||||
let blend = (!target.blend.is_null())
|
||||
.then(|| {
|
||||
serde_json::from_value::<wgpu::BlendState>(target.blend.clone())
|
||||
.map_err(|_| String::from("GRAPH_BLEND"))
|
||||
})
|
||||
.transpose()?;
|
||||
let write_mask = match target.write_mask {
|
||||
Some(bits) => wgpu::ColorWrites::from_bits(bits).ok_or("GRAPH_WRITE_MASK")?,
|
||||
None => wgpu::ColorWrites::ALL,
|
||||
};
|
||||
Ok(Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend,
|
||||
write_mask,
|
||||
}))
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
Ok(gpu
|
||||
.device
|
||||
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(&source.id),
|
||||
layout: None,
|
||||
vertex: wgpu::VertexState {
|
||||
module: &module,
|
||||
entry_point: Some(&source.vertex.entry),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &layouts,
|
||||
},
|
||||
primitive: primitive(&source.primitive)?,
|
||||
depth_stencil: depth_stencil(&source.depth_stencil)?,
|
||||
multisample: multisample(&source.multisample)?,
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some(&source.fragment.entry),
|
||||
compilation_options: Default::default(),
|
||||
targets: &targets,
|
||||
}),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn buffer_usage(names: &[String]) -> Result<wgpu::BufferUsages, String> {
|
||||
names
|
||||
.iter()
|
||||
.try_fold(wgpu::BufferUsages::COPY_DST, |usage, name| {
|
||||
Ok(usage
|
||||
| match name.as_str() {
|
||||
"uniform" => wgpu::BufferUsages::UNIFORM,
|
||||
"storage" => wgpu::BufferUsages::STORAGE,
|
||||
"vertex" => wgpu::BufferUsages::VERTEX,
|
||||
"index" => wgpu::BufferUsages::INDEX,
|
||||
"indirect" => wgpu::BufferUsages::INDIRECT,
|
||||
"copySrc" => wgpu::BufferUsages::COPY_SRC,
|
||||
_ => return Err("GRAPH_BUFFER_USAGE".into()),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn texture_usage(names: &[String]) -> Result<wgpu::TextureUsages, String> {
|
||||
names
|
||||
.iter()
|
||||
.try_fold(wgpu::TextureUsages::empty(), |usage, name| {
|
||||
Ok(usage
|
||||
| match name.as_str() {
|
||||
"render" => wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
"sampled" => wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
"storage" => wgpu::TextureUsages::STORAGE_BINDING,
|
||||
"copySrc" => wgpu::TextureUsages::COPY_SRC,
|
||||
"copyDst" => wgpu::TextureUsages::COPY_DST,
|
||||
_ => return Err("GRAPH_TEXTURE_USAGE".into()),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn texture_descriptor(
|
||||
source: &Texture,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<wgpu::TextureDescriptor<'_>, String> {
|
||||
let value = |index, canvas| -> Result<u32, String> {
|
||||
match source.size.get(index) {
|
||||
Some(Extent::Pixels(value)) => Ok(*value),
|
||||
Some(Extent::Canvas(value)) if value == "canvas" => Ok(canvas),
|
||||
Some(Extent::Canvas(_)) => Err("GRAPH_TEXTURE_SIZE".into()),
|
||||
None => Ok(canvas),
|
||||
}
|
||||
};
|
||||
Ok(wgpu::TextureDescriptor {
|
||||
label: Some(&source.id),
|
||||
size: wgpu::Extent3d {
|
||||
width: value(0, width)?,
|
||||
height: value(1, height)?,
|
||||
depth_or_array_layers: value(2, 1)?,
|
||||
},
|
||||
mip_level_count: source.mip_level_count,
|
||||
sample_count: source.sample_count,
|
||||
dimension: parse(&source.dimension, "GRAPH_TEXTURE_DIMENSION")?,
|
||||
format: parse(&source.format, "GRAPH_TEXTURE_FORMAT")?,
|
||||
usage: texture_usage(&source.usage)?,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
|
||||
fn attachment_format(
|
||||
graph: &RenderGraph,
|
||||
id: &str,
|
||||
surface: wgpu::TextureFormat,
|
||||
) -> Result<wgpu::TextureFormat, String> {
|
||||
if id == "canvas" {
|
||||
return Ok(surface);
|
||||
}
|
||||
graph
|
||||
.resources
|
||||
.textures
|
||||
.iter()
|
||||
.find(|texture| texture.id == id)
|
||||
.ok_or_else(|| "GRAPH_ATTACHMENT".into())
|
||||
.and_then(|texture| parse(&texture.format, "GRAPH_TEXTURE_FORMAT"))
|
||||
}
|
||||
|
||||
fn primitive(value: &Value) -> Result<wgpu::PrimitiveState, String> {
|
||||
if value.is_null() {
|
||||
return Ok(Default::default());
|
||||
}
|
||||
serde_json::from_value(value.clone()).map_err(|_| "GRAPH_PRIMITIVE".into())
|
||||
}
|
||||
|
||||
fn depth_stencil(value: &Value) -> Result<Option<wgpu::DepthStencilState>, String> {
|
||||
if value.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
serde_json::from_value(value.clone())
|
||||
.map(Some)
|
||||
.map_err(|_| "GRAPH_DEPTH_STENCIL".into())
|
||||
}
|
||||
|
||||
fn multisample(value: &Value) -> Result<wgpu::MultisampleState, String> {
|
||||
if value.is_null() {
|
||||
return Ok(Default::default());
|
||||
}
|
||||
let object = value.as_object().ok_or("GRAPH_MULTISAMPLE")?;
|
||||
Ok(wgpu::MultisampleState {
|
||||
count: object.get("count").and_then(Value::as_u64).unwrap_or(1) as u32,
|
||||
mask: object
|
||||
.get("mask")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(u64::MAX),
|
||||
alpha_to_coverage_enabled: object
|
||||
.get("alphaToCoverageEnabled")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
})
|
||||
}
|
||||
|
||||
fn sampler_descriptor<'a>(
|
||||
label: &'a str,
|
||||
value: &Value,
|
||||
) -> Result<wgpu::SamplerDescriptor<'a>, String> {
|
||||
let mut descriptor = wgpu::SamplerDescriptor {
|
||||
label: Some(label),
|
||||
..Default::default()
|
||||
};
|
||||
let Some(object) = value.as_object() else {
|
||||
return Ok(descriptor);
|
||||
};
|
||||
macro_rules! enum_field {
|
||||
($json:literal, $field:ident, $code:literal) => {
|
||||
if let Some(value) = object.get($json).and_then(Value::as_str) {
|
||||
descriptor.$field = parse(value, $code)?;
|
||||
}
|
||||
};
|
||||
}
|
||||
enum_field!("addressModeU", address_mode_u, "GRAPH_SAMPLER");
|
||||
enum_field!("addressModeV", address_mode_v, "GRAPH_SAMPLER");
|
||||
enum_field!("addressModeW", address_mode_w, "GRAPH_SAMPLER");
|
||||
enum_field!("magFilter", mag_filter, "GRAPH_SAMPLER");
|
||||
enum_field!("minFilter", min_filter, "GRAPH_SAMPLER");
|
||||
enum_field!("mipmapFilter", mipmap_filter, "GRAPH_SAMPLER");
|
||||
descriptor.lod_min_clamp = object
|
||||
.get("lodMinClamp")
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or(0.0) as f32;
|
||||
descriptor.lod_max_clamp = object
|
||||
.get("lodMaxClamp")
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or(32.0) as f32;
|
||||
descriptor.compare = object
|
||||
.get("compare")
|
||||
.and_then(Value::as_str)
|
||||
.map(|value| parse(value, "GRAPH_SAMPLER"))
|
||||
.transpose()?;
|
||||
descriptor.anisotropy_clamp = object
|
||||
.get("anisotropyClamp")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(1) as u16;
|
||||
Ok(descriptor)
|
||||
}
|
||||
|
||||
fn parse<T: DeserializeOwned>(value: &str, code: &str) -> Result<T, String> {
|
||||
serde_json::from_value(Value::String(value.into())).map_err(|_| code.into())
|
||||
}
|
||||
Reference in New Issue
Block a user