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,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))
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user