core: indirect draw passes, device feature list, drop handles-specific bundle heuristics

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:31:11 +00:00
co-authored by heaust
parent dda602c4a2
commit 49c80a209e
11 changed files with 123 additions and 56 deletions
+3
View File
@@ -48,6 +48,8 @@ export class YawnCore {
#pending = new Map();
#profileListeners = new Set();
#next = 1;
/** Optional WebGPU features the device has (WebGPU spelling); populated once `ready` resolves. */
features = new Set();
constructor(canvas, { arenaBytes = 64 * 1024 * 1024, debug = false, workerFactory } = {}) {
if (!canvas) throw new TypeError("canvas is required");
@@ -62,6 +64,7 @@ export class YawnCore {
const offscreen = canvas.transferControlToOffscreen?.() ?? canvas;
this.ready = this.#request("init", { canvas: offscreen, arenaBytes }, [offscreen]).then(async result => {
this.#buffer = result.buffer;
this.features = new Set(result.features);
for (const descriptor of result.rows) this.#arrays.set(
descriptor.name,
new SharedRows(this.#buffer, descriptor),
+6
View File
@@ -87,6 +87,12 @@ impl Core {
serde_json::to_string(&self.data.borrow().descriptors()).unwrap()
}
/// Optional WebGPU features the device was created with (for example `indirect-first-instance`).
pub fn features(&self) -> String {
let gpu = self.gpu.borrow();
serde_json::to_string(&gpu.as_ref().map(|gpu| &gpu.features[..]).unwrap_or(&[])).unwrap()
}
pub fn create_rows(
&self,
name: String,
+54 -47
View File
@@ -212,22 +212,14 @@ impl GpuResources {
for execution in &graph.executions {
let compiled = match execution {
Execution::Render(passes) => {
let (bundle, draws) = resources.render_bundle(graph, passes, gpu)?;
let (bundle, draws) = resources.render_bundle(graph, passes, gpu, data)?;
let first = &graph.passes[passes[0]].id;
let last = &graph.passes[*passes.last().unwrap()].id;
GpuPass::Render {
label: if passes.len() == 1 {
graph.passes[passes[0]].id.clone()
} else if passes
.iter()
.all(|index| graph.passes[*index].id.starts_with("forward-"))
{
format!("Forward ({draws} draws)")
} else if passes
.iter()
.all(|index| graph.passes[*index].id.starts_with("depth-"))
{
format!("Depth ({draws} draws)")
format!("{first} ({draws} draws)")
} else {
format!("Render ({draws} draws)")
format!("{first} … {last} ({draws} draws)")
},
first: passes[0],
last: *passes.last().unwrap(),
@@ -405,6 +397,7 @@ impl GpuResources {
graph: &RenderGraph,
passes: &[usize],
gpu: &Wgpu,
data: &RenderData,
) -> Result<(wgpu::RenderBundle, usize), String> {
let pass = &graph.passes[passes[0]];
let declaration = graph
@@ -441,9 +434,8 @@ impl GpuResources {
let mut previous_pipeline = None;
let mut previous_bindings: Option<&[Binding]> = None;
let mut draws = 0;
let mut at = 0;
while at < passes.len() {
let pass = &graph.passes[passes[at]];
for &at in passes {
let pass = &graph.passes[at];
let pipeline = self
.render_pipelines
.get(&pass.pipeline)
@@ -469,7 +461,7 @@ impl GpuResources {
.buffer;
encoder.set_vertex_buffer(binding.slot, buffer.slice(binding.offset..));
}
if let Some(binding) = &pass.index_buffer {
let indexed = if let Some(binding) = &pass.index_buffer {
let buffer = &self
.buffers
.get(&binding.resource)
@@ -479,26 +471,53 @@ impl GpuResources {
buffer.slice(binding.offset..),
parse(&binding.format, "GRAPH_INDEX_FORMAT")?,
);
let mut instances = pass.draw.instances;
while at + 1 < passes.len()
&& can_instance(pass, &graph.passes[passes[at + 1]], instances)
{
at += 1;
instances += graph.passes[passes[at]].draw.instances;
}
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 + instances,
);
true
} 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,
);
false
};
match (&pass.indirect, indexed) {
(Some(indirect), indexed) => {
let buffer = self
.buffers
.get(&indirect.resource)
.ok_or("GRAPH_RESOURCE_UNKNOWN")?;
let rows = data.rows(&buffer.source).ok_or("GRAPH_ARRAY_UNKNOWN")?;
let end = match indirect.count {
Some(count) => indirect.first.checked_add(count),
None => Some(rows.rows),
}
.filter(|end| indirect.first <= *end && *end <= rows.rows)
.ok_or("GRAPH_INDIRECT")?;
if rows.stride < if indexed { 20 } else { 16 } {
return Err("GRAPH_INDIRECT".into());
}
let count = end - indirect.first;
for row in indirect.first..end {
let offset = u64::from(row) * u64::from(rows.stride);
if indexed {
encoder.draw_indexed_indirect(&buffer.buffer, offset);
} else {
encoder.draw_indirect(&buffer.buffer, offset);
}
}
draws += count as usize;
}
(None, true) => {
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,
);
draws += 1;
}
(None, false) => {
encoder.draw(
pass.draw.first_vertex..pass.draw.first_vertex + pass.draw.vertices,
pass.draw.first_instance..pass.draw.first_instance + pass.draw.instances,
);
draws += 1;
}
}
draws += 1;
at += 1;
}
Ok((
encoder.finish(&wgpu::RenderBundleDescriptor {
@@ -554,18 +573,6 @@ impl GpuResources {
}
}
fn can_instance(first: &Pass, next: &Pass, instances: u32) -> bool {
first.pipeline == next.pipeline
&& first.bindings == next.bindings
&& first.vertex_buffers == next.vertex_buffers
&& first.index_buffer == next.index_buffer
&& first.draw.indices != 0
&& first.draw.indices == next.draw.indices
&& first.draw.first_index == next.draw.first_index
&& first.draw.base_vertex == next.draw.base_vertex
&& first.draw.first_instance + instances == next.draw.first_instance
}
impl GpuPass {
fn label(&self) -> &str {
match self {
+25
View File
@@ -192,10 +192,23 @@ pub struct Pass {
pub index_buffer: Option<IndexBinding>,
#[serde(default)]
pub draw: Draw,
pub indirect: Option<IndirectBinding>,
#[serde(default = "dispatch")]
pub dispatch: [u32; 3],
}
/// Draw commands read from rows of an `indirect` buffer: one command per row starting at
/// `first`, `count` rows (default: every row to the end of the array). The command occupies the
/// first 16 bytes (non-indexed) or 20 bytes (indexed) of each row; the remainder of the row is
/// free for the caller.
#[derive(Clone, Deserialize, PartialEq, Eq)]
pub struct IndirectBinding {
pub resource: String,
#[serde(default)]
pub first: u32,
pub count: Option<u32>,
}
#[derive(Clone, Deserialize, PartialEq, Eq)]
pub struct Binding {
#[serde(default)]
@@ -344,6 +357,17 @@ impl RenderGraph {
"compute" => _ = compute_pipelines.insert(pass.pipeline.clone()),
_ => return Err("GRAPH_PASS"),
}
if let Some(indirect) = &pass.indirect {
let usable = self
.resources
.buffers
.iter()
.find(|buffer| buffer.id == indirect.resource)
.is_some_and(|buffer| buffer.usage.iter().any(|usage| usage == "indirect"));
if !usable || pass.draw != Draw::default() {
return Err("GRAPH_INDIRECT");
}
}
for id in pass.resources() {
used.insert(id.to_owned());
lifetimes
@@ -532,6 +556,7 @@ impl Pass {
.iter()
.map(|value| value.resource.as_str()),
)
.chain(self.indirect.iter().map(|value| value.resource.as_str()))
.collect()
}
}
+21 -1
View File
@@ -8,9 +8,19 @@ pub struct Wgpu {
pub width: u32,
pub height: u32,
pub timestamp_queries: bool,
/// Optional features the device was created with, in WebGPU spelling.
pub features: Vec<&'static str>,
pub adapter: String,
}
const OPTIONAL_FEATURES: [(wgpu::Features, &str); 2] = [
(wgpu::Features::TIMESTAMP_QUERY, "timestamp-query"),
(
wgpu::Features::INDIRECT_FIRST_INSTANCE,
"indirect-first-instance",
),
];
impl Wgpu {
pub async fn new(canvas: web_sys::OffscreenCanvas) -> Result<Self, String> {
let width = canvas.width();
@@ -33,7 +43,12 @@ impl Wgpu {
})
.await
.map_err(|_| "WEBGPU_UNAVAILABLE")?;
let required_features = adapter.features() & wgpu::Features::TIMESTAMP_QUERY;
let required_features = OPTIONAL_FEATURES
.iter()
.fold(wgpu::Features::empty(), |features, (feature, _)| {
features | *feature
})
& adapter.features();
let info = adapter.get_info();
let adapter_name = if info.name.is_empty() {
format!("{:?} · {:?}", info.device_type, info.backend)
@@ -65,6 +80,11 @@ impl Wgpu {
width,
height,
timestamp_queries: required_features.contains(wgpu::Features::TIMESTAMP_QUERY),
features: OPTIONAL_FEATURES
.iter()
.filter(|(feature, _)| required_features.contains(*feature))
.map(|(_, name)| *name)
.collect(),
adapter: adapter_name,
})
}
+1 -1
View File
@@ -15,7 +15,7 @@ addEventListener("message", async ({ data: message }) => {
await core.initialize(message.canvas);
const buffer = wasm.memory.buffer;
if (!(buffer instanceof SharedArrayBuffer)) fail("WASM_MEMORY_NOT_SHARED");
result = { buffer, rows: JSON.parse(core.rows()) };
result = { buffer, rows: JSON.parse(core.rows()), features: JSON.parse(core.features()) };
} else {
if (!core) fail("UNINITIALIZED");
switch (message?.type) {