core: manual buffer sync with sync-rows, reuse unchanged buffers and pipelines across activations

Also lets the screenshot suite point at another reference directory and settle longer.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b2dd-db32-712d-99ce-12ce4a86d979
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-09-18 05:53:44 +00:00
co-authored by heaust
parent 49c80a209e
commit 31d3ae21ff
15 changed files with 185 additions and 48 deletions
+6
View File
@@ -113,6 +113,12 @@ export class YawnCore {
return this.#request("delete-object", { name, id });
}
/** Uploads rows `first .. first + count` of `name` to every active GPU buffer bound to that array. */
async syncRows(name, first, count) {
await this.ready;
return this.#request("sync-rows", { name, first, count });
}
async compileGraph(serialized) {
await this.ready;
return this.#request("compile-graph", { serialized });
+14
View File
@@ -186,6 +186,20 @@ impl Core {
Ok(())
}
/// Uploads rows `first .. first + count` of `name` to every active GPU buffer bound to it.
pub fn sync_rows(&self, name: &str, first: u32, count: u32) -> Result<(), JsError> {
let gpu = self.gpu.borrow();
let gpu = gpu
.as_ref()
.ok_or_else(|| JsError::new("WEBGPU_UNINITIALIZED"))?;
self.store
.borrow()
.sync_rows(name, first, count, gpu, &self.data.borrow())
.map_err(|error| JsError::new(&error))?;
self.data.borrow_mut().mark_dirty();
Ok(())
}
pub fn compile_graph(&self, source: &str) -> Result<String, JsError> {
let graph = compiler::compile(source).map_err(JsError::new)?;
Ok(self.store.borrow_mut().save(graph))
+90 -14
View File
@@ -9,6 +9,7 @@ use serde_json::Value;
use crate::gpu::Wgpu;
use crate::graph::{Binding, Execution, Extent, Pass, RenderGraph, RenderPipeline, Texture};
use crate::render_data::RenderData;
use crate::store::Loadout;
pub struct GpuResources {
pub buffers: HashMap<String, GpuBuffer>,
@@ -38,6 +39,7 @@ pub struct ProfileMap {
pub struct GpuBuffer {
pub buffer: wgpu::Buffer,
pub source: String,
pub usage: wgpu::BufferUsages,
pub sync_each_frame: bool,
}
@@ -65,28 +67,48 @@ pub enum GpuPass {
}
impl GpuResources {
/// Builds the GPU side of `graph`. Buffers whose source array, size, and usage are unchanged
/// and pipelines whose declaration is unchanged are taken over from `previous` instead of being
/// recreated and re-uploaded; bind groups and bundles are always rebuilt.
pub fn activate(
graph: &RenderGraph,
gpu: &Wgpu,
data: &RenderData,
previous: Option<&Self>,
previous: Option<&Loadout>,
) -> 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());
let size = u64::from(rows.bytes.max(4));
let usage = buffer_usage(&source.usage)?;
let reused = previous
.and_then(|previous| previous.resources.buffers.get(&source.id))
.filter(|buffer| {
buffer.source == source.array
&& buffer.usage == usage
&& buffer.buffer.size() == size
})
.map(|buffer| buffer.buffer.clone());
let buffer = match reused {
Some(buffer) => buffer,
None => {
let buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
label: Some(&source.id),
size,
usage,
mapped_at_creation: false,
});
gpu.queue
.write_buffer(&buffer, 0, data.bytes(&source.array).unwrap());
buffer
}
};
buffers.insert(
source.id.clone(),
GpuBuffer {
buffer,
source: source.array.clone(),
usage,
sync_each_frame: source.sync == "frame",
},
);
@@ -102,11 +124,12 @@ impl GpuResources {
let key = source.key()?;
if !source.transient {
if let Some(texture) = previous
.and_then(|resources| {
resources
.and_then(|previous| {
previous
.resources
.texture_slots
.get(&source.id)
.map(|slot| &resources.textures[*slot])
.map(|slot| &previous.resources.textures[*slot])
})
.filter(|texture| texture.key == key)
{
@@ -147,7 +170,23 @@ impl GpuResources {
.render
.iter()
.map(|source| {
create_render_pipeline(source, gpu).map(|pipeline| (source.id.clone(), pipeline))
let reused = previous.and_then(|previous| {
previous
.graph
.pipelines
.render
.iter()
.find(|candidate| candidate.same_as(source))
.and_then(|candidate| {
previous.resources.render_pipelines.get(&candidate.id)
})
.cloned()
});
match reused {
Some(pipeline) => Ok(pipeline),
None => create_render_pipeline(source, gpu),
}
.map(|pipeline| (source.id.clone(), pipeline))
})
.collect::<Result<HashMap<_, _>, _>>()?;
let compute_pipelines = graph
@@ -155,6 +194,21 @@ impl GpuResources {
.compute
.iter()
.map(|source| {
let reused = previous.and_then(|previous| {
previous
.graph
.pipelines
.compute
.iter()
.find(|candidate| candidate.same_as(source))
.and_then(|candidate| {
previous.resources.compute_pipelines.get(&candidate.id)
})
.cloned()
});
if let Some(pipeline) = reused {
return Ok::<_, String>((source.id.clone(), pipeline));
}
let module = gpu
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
@@ -171,7 +225,7 @@ impl GpuResources {
compilation_options: Default::default(),
cache: None,
});
Ok::<_, String>((source.id.clone(), pipeline))
Ok((source.id.clone(), pipeline))
})
.collect::<Result<HashMap<_, _>, _>>()?;
@@ -251,6 +305,28 @@ impl GpuResources {
Ok(resources)
}
/// Copies rows `first .. first + count` of a source array into every GPU buffer bound to it.
pub fn sync_rows(
&self,
name: &str,
first: u32,
count: u32,
gpu: &Wgpu,
data: &RenderData,
) -> Result<(), String> {
let rows = data.rows(name).ok_or("ROWS_UNKNOWN")?;
let end = first
.checked_add(count)
.filter(|end| *end <= rows.rows)
.ok_or("ROW_RANGE")?;
let start = (first * rows.stride) as usize;
let bytes = &data.bytes(name).unwrap()[start..(end * rows.stride) as usize];
for buffer in self.buffers.values().filter(|buffer| buffer.source == name) {
gpu.queue.write_buffer(&buffer.buffer, start as u64, bytes);
}
Ok(())
}
pub fn texture_view(&self, id: &str) -> Option<&wgpu::TextureView> {
self.texture_slots
.get(id)
+26 -8
View File
@@ -85,7 +85,7 @@ pub struct PipelineDeclarations {
pub compute: Vec<ComputePipeline>,
}
#[derive(Clone, Deserialize)]
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RenderPipeline {
pub id: String,
@@ -102,7 +102,7 @@ pub struct RenderPipeline {
pub multisample: Value,
}
#[derive(Clone, Deserialize)]
#[derive(Clone, Deserialize, PartialEq, Eq)]
pub struct ComputePipeline {
pub id: String,
pub code: String,
@@ -110,7 +110,7 @@ pub struct ComputePipeline {
pub entry: String,
}
#[derive(Clone, Deserialize)]
#[derive(Clone, Deserialize, PartialEq, Eq)]
pub struct VertexStage {
#[serde(default = "vertex_entry")]
pub entry: String,
@@ -127,7 +127,7 @@ impl Default for VertexStage {
}
}
#[derive(Clone, Default, Deserialize)]
#[derive(Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VertexBuffer {
pub array_stride: u64,
@@ -137,7 +137,7 @@ pub struct VertexBuffer {
pub attributes: Vec<VertexAttribute>,
}
#[derive(Clone, Deserialize)]
#[derive(Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VertexAttribute {
pub format: String,
@@ -145,7 +145,7 @@ pub struct VertexAttribute {
pub shader_location: u32,
}
#[derive(Clone, Deserialize)]
#[derive(Clone, Deserialize, PartialEq)]
pub struct FragmentStage {
#[serde(default = "fragment_entry")]
pub entry: String,
@@ -162,7 +162,7 @@ impl Default for FragmentStage {
}
}
#[derive(Clone, Deserialize)]
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FragmentTarget {
pub format: String,
@@ -519,7 +519,7 @@ impl RenderGraph {
.resources
.buffers
.iter()
.any(|buffer| !matches!(buffer.sync.as_str(), "frame" | "loadout"))
.any(|buffer| !matches!(buffer.sync.as_str(), "frame" | "manual"))
{
return Err("GRAPH_BUFFER_SYNC");
}
@@ -561,6 +561,24 @@ impl Pass {
}
}
impl RenderPipeline {
/// Same GPU pipeline regardless of declaration ID.
pub(crate) fn same_as(&self, other: &Self) -> bool {
self.code == other.code
&& self.vertex == other.vertex
&& self.fragment == other.fragment
&& self.primitive == other.primitive
&& self.depth_stencil == other.depth_stencil
&& self.multisample == other.multisample
}
}
impl ComputePipeline {
pub(crate) fn same_as(&self, other: &Self) -> bool {
self.code == other.code && self.entry == other.entry
}
}
impl Texture {
pub(crate) fn key(&self) -> Result<String, &'static str> {
let mut usage = self.usage.clone();
+16 -12
View File
@@ -26,12 +26,7 @@ impl Store {
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 mut resources = GpuResources::activate(
&graph,
gpu,
data,
self.active.as_ref().map(|active| &active.resources),
)?;
let mut resources = GpuResources::activate(&graph, gpu, data, self.active.as_ref())?;
for (name, levels) in &self.texture_sources {
for (mip_level, image) in levels {
if resources.needs_upload(name, *mip_level) {
@@ -72,6 +67,20 @@ impl Store {
}
}
pub fn sync_rows(
&self,
name: &str,
first: u32,
count: u32,
gpu: &Wgpu,
data: &RenderData,
) -> Result<(), String> {
match &self.active {
Some(active) => active.resources.sync_rows(name, first, count, gpu, data),
None => Ok(()),
}
}
pub fn active_mut(&mut self) -> Option<&mut Loadout> {
self.active.as_mut()
}
@@ -104,12 +113,7 @@ impl Store {
return Ok(());
};
let graph = active.graph.clone();
let mut resources = GpuResources::activate(
&graph,
gpu,
data,
self.active.as_ref().map(|active| &active.resources),
)?;
let mut resources = GpuResources::activate(&graph, gpu, data, self.active.as_ref())?;
for (name, levels) in &self.texture_sources {
for (mip_level, image) in levels {
if resources.needs_upload(name, *mip_level) {
+3
View File
@@ -34,6 +34,9 @@ addEventListener("message", async ({ data: message }) => {
case "delete-object":
core.delete_object(message.name, message.id);
break;
case "sync-rows":
core.sync_rows(message.name, message.first ?? 0, message.count);
break;
case "compile-graph":
result = core.compile_graph(message.serialized);
break;