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:
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user