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:
Amp
2026-08-20 05:35:24 +00:00
co-authored by heaust
parent d095ee7849
commit 239b1d25b0
17 changed files with 2697 additions and 667 deletions
+162
View File
@@ -0,0 +1,162 @@
use serde_json::{Map, Value};
use crate::graph::RenderGraph;
pub fn compile(source: &str) -> Result<RenderGraph, &'static str> {
let Expression::List(mut root) = Parser::parse(source)? else {
return Err("GRAPH_WIRE");
};
if root.len() != 3
|| !matches!(&root[0], Expression::Atom(Value::String(tag)) if tag == "yawn-graph")
|| !matches!(&root[1], Expression::Atom(Value::Number(version)) if version.as_u64() == Some(1))
{
return Err("GRAPH_WIRE");
}
let value = decode(root.pop().unwrap())?;
let mut graph: RenderGraph = serde_json::from_value(value).map_err(|_| "GRAPH_SHAPE")?;
graph.prepare()?;
Ok(graph)
}
enum Expression {
Atom(Value),
List(Vec<Expression>),
}
struct Parser<'a> {
source: &'a [u8],
at: usize,
}
impl<'a> Parser<'a> {
fn parse(source: &'a str) -> Result<Expression, &'static str> {
let mut parser = Self {
source: source.as_bytes(),
at: 0,
};
let expression = parser.expression()?;
parser.whitespace();
(parser.at == parser.source.len())
.then_some(expression)
.ok_or("GRAPH_WIRE")
}
fn expression(&mut self) -> Result<Expression, &'static str> {
self.whitespace();
match self.source.get(self.at) {
Some(b'(') => self.list(),
Some(b'"') => self.string(),
Some(b')') | None => Err("GRAPH_WIRE"),
Some(_) => self.atom(),
}
}
fn list(&mut self) -> Result<Expression, &'static str> {
self.at += 1;
let mut values = Vec::new();
loop {
self.whitespace();
match self.source.get(self.at) {
Some(b')') => {
self.at += 1;
return Ok(Expression::List(values));
}
None => return Err("GRAPH_WIRE"),
_ => values.push(self.expression()?),
}
}
}
fn string(&mut self) -> Result<Expression, &'static str> {
let start = self.at;
self.at += 1;
let mut escaped = false;
while let Some(&byte) = self.source.get(self.at) {
self.at += 1;
if escaped {
escaped = false;
} else if byte == b'\\' {
escaped = true;
} else if byte == b'"' {
let value = serde_json::from_slice(&self.source[start..self.at])
.map_err(|_| "GRAPH_WIRE")?;
return Ok(Expression::Atom(Value::String(value)));
}
}
Err("GRAPH_WIRE")
}
fn atom(&mut self) -> Result<Expression, &'static str> {
let start = self.at;
while self
.source
.get(self.at)
.is_some_and(|byte| !byte.is_ascii_whitespace() && !matches!(byte, b'(' | b')'))
{
self.at += 1;
}
let token = std::str::from_utf8(&self.source[start..self.at]).map_err(|_| "GRAPH_WIRE")?;
let value = match token {
"true" => Value::Bool(true),
"false" => Value::Bool(false),
"null" => Value::Null,
_ => serde_json::from_str::<Value>(token)
.ok()
.filter(Value::is_number)
.unwrap_or_else(|| Value::String(token.into())),
};
Ok(Expression::Atom(value))
}
fn whitespace(&mut self) {
while self
.source
.get(self.at)
.is_some_and(u8::is_ascii_whitespace)
{
self.at += 1;
}
}
}
fn decode(expression: Expression) -> Result<Value, &'static str> {
let Expression::List(mut values) = expression else {
return match expression {
Expression::Atom(value) => Ok(value),
Expression::List(_) => unreachable!(),
};
};
if values.is_empty() {
return Err("GRAPH_WIRE");
}
let tag = match values.remove(0) {
Expression::Atom(Value::String(tag)) => tag,
_ => return Err("GRAPH_WIRE"),
};
if tag == "array" {
return values.into_iter().map(decode).collect();
}
if tag != "object" {
return Err("GRAPH_WIRE");
}
let mut object = Map::new();
for field in values {
let Expression::List(mut field) = field else {
return Err("GRAPH_WIRE");
};
if field.len() != 3
|| !matches!(&field[0], Expression::Atom(Value::String(tag)) if tag == "field")
{
return Err("GRAPH_WIRE");
}
let value = decode(field.pop().unwrap())?;
let key = match field.pop().unwrap() {
Expression::Atom(Value::String(key)) => key,
_ => return Err("GRAPH_WIRE"),
};
if object.insert(key, value).is_some() {
return Err("GRAPH_WIRE");
}
}
Ok(Value::Object(object))
}
+545
View File
@@ -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())
}
+500
View File
@@ -0,0 +1,500 @@
use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RenderGraph {
pub id: String,
#[serde(default)]
pub resources: ResourceDeclarations,
#[serde(default)]
pub pipelines: PipelineDeclarations,
pub passes: Vec<Pass>,
}
#[derive(Clone, Default, Deserialize)]
pub struct ResourceDeclarations {
#[serde(default)]
pub buffers: Vec<Buffer>,
#[serde(default)]
pub textures: Vec<Texture>,
#[serde(default)]
pub samplers: Vec<Sampler>,
}
#[derive(Clone, Deserialize)]
pub struct Buffer {
pub id: String,
pub array: String,
#[serde(default)]
pub usage: Vec<String>,
}
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Texture {
pub id: String,
#[serde(default)]
pub size: Vec<Extent>,
pub format: String,
#[serde(default)]
pub usage: Vec<String>,
#[serde(default = "one")]
pub mip_level_count: u32,
#[serde(default = "one")]
pub sample_count: u32,
#[serde(default = "dimension")]
pub dimension: String,
#[serde(default = "yes")]
pub transient: bool,
#[serde(skip)]
pub slot: usize,
}
#[derive(Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum Extent {
Pixels(u32),
Canvas(String),
}
#[derive(Clone, Deserialize)]
pub struct Sampler {
pub id: String,
#[serde(default)]
pub descriptor: Value,
}
#[derive(Clone, Default, Deserialize)]
pub struct PipelineDeclarations {
#[serde(default)]
pub render: Vec<RenderPipeline>,
#[serde(default)]
pub compute: Vec<ComputePipeline>,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RenderPipeline {
pub id: String,
pub code: String,
#[serde(default)]
pub vertex: VertexStage,
#[serde(default)]
pub fragment: FragmentStage,
#[serde(default)]
pub primitive: Value,
#[serde(default)]
pub depth_stencil: Value,
#[serde(default)]
pub multisample: Value,
}
#[derive(Clone, Deserialize)]
pub struct ComputePipeline {
pub id: String,
pub code: String,
#[serde(default = "compute_entry")]
pub entry: String,
}
#[derive(Clone, Deserialize)]
pub struct VertexStage {
#[serde(default = "vertex_entry")]
pub entry: String,
#[serde(default)]
pub buffers: Vec<VertexBuffer>,
}
impl Default for VertexStage {
fn default() -> Self {
Self {
entry: vertex_entry(),
buffers: Vec::new(),
}
}
}
#[derive(Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VertexBuffer {
pub array_stride: u64,
#[serde(default = "vertex_step")]
pub step_mode: String,
#[serde(default)]
pub attributes: Vec<VertexAttribute>,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VertexAttribute {
pub format: String,
pub offset: u64,
pub shader_location: u32,
}
#[derive(Clone, Deserialize)]
pub struct FragmentStage {
#[serde(default = "fragment_entry")]
pub entry: String,
#[serde(default)]
pub targets: Vec<FragmentTarget>,
}
impl Default for FragmentStage {
fn default() -> Self {
Self {
entry: fragment_entry(),
targets: Vec::new(),
}
}
}
#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FragmentTarget {
pub format: String,
#[serde(default)]
pub blend: Value,
pub write_mask: Option<u32>,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Pass {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
pub pipeline: String,
#[serde(default)]
pub after: Vec<String>,
#[serde(skip)]
pub dependencies: Vec<usize>,
#[serde(default)]
pub bindings: Vec<Binding>,
#[serde(default)]
pub color: Vec<ColorAttachment>,
pub depth: Option<DepthAttachment>,
#[serde(default)]
pub vertex_buffers: Vec<VertexBinding>,
pub index_buffer: Option<IndexBinding>,
#[serde(default)]
pub draw: Draw,
#[serde(default = "dispatch")]
pub dispatch: [u32; 3],
}
#[derive(Clone, Deserialize)]
pub struct Binding {
#[serde(default)]
pub group: u32,
pub binding: u32,
pub resource: String,
#[serde(default)]
pub offset: u64,
pub size: Option<u64>,
}
#[derive(Clone, Deserialize)]
pub struct ColorAttachment {
pub resource: String,
#[serde(default)]
pub clear: Vec<f32>,
#[serde(default = "clear_op")]
pub load: String,
#[serde(default = "store_op")]
pub store: String,
}
#[derive(Clone, Deserialize)]
pub struct DepthAttachment {
pub resource: String,
#[serde(default = "one_f32")]
pub clear: f32,
#[serde(default = "clear_op")]
pub load: String,
#[serde(default = "store_op")]
pub store: String,
}
#[derive(Clone, Deserialize)]
pub struct VertexBinding {
#[serde(default)]
pub slot: u32,
pub resource: String,
#[serde(default)]
pub offset: u64,
}
#[derive(Clone, Deserialize)]
pub struct IndexBinding {
pub resource: String,
#[serde(default = "index_format")]
pub format: String,
#[serde(default)]
pub offset: u64,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Draw {
#[serde(default = "three")]
pub vertices: u32,
#[serde(default)]
pub indices: u32,
#[serde(default = "one")]
pub instances: u32,
#[serde(default)]
pub first_vertex: u32,
#[serde(default)]
pub first_index: u32,
#[serde(default)]
pub base_vertex: i32,
#[serde(default)]
pub first_instance: u32,
}
impl Default for Draw {
fn default() -> Self {
Self {
vertices: 3,
indices: 0,
instances: 1,
first_vertex: 0,
first_index: 0,
base_vertex: 0,
first_instance: 0,
}
}
}
impl RenderGraph {
pub fn prepare(&mut self) -> Result<(), &'static str> {
if self.id.is_empty() || self.passes.is_empty() {
return Err("GRAPH_SHAPE");
}
let mut ids = HashMap::new();
for (index, pass) in self.passes.iter().enumerate() {
if pass.id.is_empty() || ids.insert(pass.id.clone(), index).is_some() {
return Err("GRAPH_PASS");
}
}
let dependencies = self
.passes
.iter()
.map(|pass| {
pass.after
.iter()
.map(|id| ids.get(id).copied().ok_or("GRAPH_DEPENDENCY"))
.collect::<Result<HashSet<_>, _>>()
})
.collect::<Result<Vec<_>, _>>()?;
let mut emitted = vec![false; self.passes.len()];
let mut order = Vec::with_capacity(self.passes.len());
while order.len() < self.passes.len() {
let ready = (0..self.passes.len()).find(|&index| {
!emitted[index]
&& dependencies[index]
.iter()
.all(|dependency| emitted[*dependency])
});
let index = ready.ok_or("GRAPH_CYCLE")?;
emitted[index] = true;
order.push(index);
}
let original = self.passes.clone();
self.passes = order
.into_iter()
.map(|index| original[index].clone())
.collect();
let sorted_ids: HashMap<_, _> = self
.passes
.iter()
.enumerate()
.map(|(index, pass)| (pass.id.clone(), index))
.collect();
for pass in &mut self.passes {
pass.dependencies = pass.after.iter().map(|id| sorted_ids[id]).collect();
}
self.plan_resources()
}
fn plan_resources(&mut self) -> Result<(), &'static str> {
let mut used = HashSet::new();
let mut lifetimes = HashMap::new();
let mut render_pipelines = HashSet::new();
let mut compute_pipelines = HashSet::new();
for (frame, pass) in self.passes.iter().enumerate() {
match pass.kind.as_str() {
"render" => _ = render_pipelines.insert(pass.pipeline.clone()),
"compute" => _ = compute_pipelines.insert(pass.pipeline.clone()),
_ => return Err("GRAPH_PASS"),
}
for id in pass.resources() {
used.insert(id.to_owned());
lifetimes
.entry(id.to_owned())
.and_modify(|range: &mut (usize, usize)| range.1 = frame)
.or_insert((frame, frame));
}
}
self.resources
.buffers
.retain(|value| used.contains(&value.id));
self.resources
.samplers
.retain(|value| used.contains(&value.id));
self.pipelines
.render
.retain(|value| render_pipelines.contains(&value.id));
self.pipelines
.compute
.retain(|value| compute_pipelines.contains(&value.id));
let mut textures = self
.resources
.textures
.drain(..)
.filter_map(|texture| {
lifetimes
.get(&texture.id)
.copied()
.map(|range| (texture, range))
})
.collect::<Vec<_>>();
textures.sort_by_key(|value| value.1 .0);
let mut slots: Vec<(String, usize, bool)> = Vec::new();
for (texture, (first, last)) in &mut textures {
let key = texture.key()?;
let reusable = texture
.transient
.then(|| {
slots
.iter()
.position(|slot| slot.2 && slot.0 == key && slot.1 < *first)
})
.flatten();
texture.slot = match reusable {
Some(slot) => {
slots[slot].1 = *last;
slot
}
None => {
slots.push((key, *last, texture.transient));
slots.len() - 1
}
};
}
self.resources.textures = textures.into_iter().map(|value| value.0).collect();
self.validate_ids()
}
fn validate_ids(&self) -> Result<(), &'static str> {
let mut resources = HashSet::new();
for id in self
.resources
.buffers
.iter()
.map(|value| &value.id)
.chain(self.resources.textures.iter().map(|value| &value.id))
.chain(self.resources.samplers.iter().map(|value| &value.id))
{
if id.is_empty() || !resources.insert(id) {
return Err("GRAPH_RESOURCE");
}
}
let mut pipelines = HashSet::new();
for id in self
.pipelines
.render
.iter()
.map(|value| &value.id)
.chain(self.pipelines.compute.iter().map(|value| &value.id))
{
if id.is_empty() || !pipelines.insert(id) {
return Err("GRAPH_PIPELINE");
}
}
Ok(())
}
}
impl Pass {
fn resources(&self) -> Vec<&str> {
self.bindings
.iter()
.map(|value| value.resource.as_str())
.chain(self.color.iter().map(|value| value.resource.as_str()))
.chain(self.depth.iter().map(|value| value.resource.as_str()))
.chain(
self.vertex_buffers
.iter()
.map(|value| value.resource.as_str()),
)
.chain(
self.index_buffer
.iter()
.map(|value| value.resource.as_str()),
)
.collect()
}
}
impl Texture {
fn key(&self) -> Result<String, &'static str> {
let mut usage = self.usage.clone();
usage.sort_unstable();
usage.dedup();
serde_json::to_string(&(
&self.size,
&self.format,
usage,
self.mip_level_count,
self.sample_count,
&self.dimension,
))
.map_err(|_| "GRAPH_RESOURCE")
}
}
fn one() -> u32 {
1
}
fn three() -> u32 {
3
}
fn one_f32() -> f32 {
1.0
}
fn yes() -> bool {
true
}
fn dimension() -> String {
"2d".into()
}
fn vertex_entry() -> String {
"vertex".into()
}
fn fragment_entry() -> String {
"fragment".into()
}
fn compute_entry() -> String {
"main".into()
}
fn vertex_step() -> String {
"vertex".into()
}
fn clear_op() -> String {
"clear".into()
}
fn store_op() -> String {
"store".into()
}
fn index_format() -> String {
"uint32".into()
}
fn dispatch() -> [u32; 3] {
[1, 1, 1]
}
+62
View File
@@ -0,0 +1,62 @@
use std::collections::HashMap;
use crate::gpu::Wgpu;
use crate::gpu_resource::GpuResources;
use crate::graph::RenderGraph;
use crate::render_data::RenderData;
pub struct Loadout {
pub graph: RenderGraph,
pub resources: GpuResources,
}
#[derive(Default)]
pub struct Store {
graphs: HashMap<String, RenderGraph>,
active: Option<Loadout>,
}
impl Store {
pub fn save(&mut self, graph: RenderGraph) -> String {
let id = graph.id.clone();
self.graphs.insert(id.clone(), graph);
id
}
pub fn switch(&mut self, id: &str, gpu: &Wgpu, data: &RenderData) -> Result<(), String> {
let graph = self.graphs.get(id).ok_or("GRAPH_UNKNOWN")?.clone();
let resources = GpuResources::activate(&graph, gpu, data)?;
self.active = Some(Loadout { graph, resources });
Ok(())
}
pub fn active_mut(&mut self) -> Option<&mut Loadout> {
self.active.as_mut()
}
pub fn uses_rows(&self, name: &str) -> bool {
self.active.as_ref().is_some_and(|active| {
active
.graph
.resources
.buffers
.iter()
.any(|buffer| buffer.array == name)
})
}
pub fn refresh_rows(
&mut self,
name: &str,
gpu: &Wgpu,
data: &RenderData,
) -> Result<(), String> {
if !self.uses_rows(name) {
return Ok(());
}
let graph = self.active.as_ref().unwrap().graph.clone();
let resources = GpuResources::activate(&graph, gpu, data)?;
self.active = Some(Loadout { graph, resources });
Ok(())
}
}