Optimize render graph and add GPU profiling
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:
+1
-1
@@ -15,5 +15,5 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = { version = "0.3", features = ["OffscreenCanvas"] }
|
||||
web-sys = { version = "0.3", features = ["ImageBitmap", "OffscreenCanvas"] }
|
||||
wgpu = { version = "26", default-features = false, features = ["serde", "webgpu", "wgsl"] }
|
||||
|
||||
+32
-2
@@ -46,9 +46,10 @@ export class YawnCore {
|
||||
#buffer;
|
||||
#arrays = new Map();
|
||||
#pending = new Map();
|
||||
#profileListeners = new Set();
|
||||
#next = 1;
|
||||
|
||||
constructor(canvas, { arenaBytes = 64 * 1024 * 1024, workerFactory } = {}) {
|
||||
constructor(canvas, { arenaBytes = 64 * 1024 * 1024, debug = false, workerFactory } = {}) {
|
||||
if (!canvas) throw new TypeError("canvas is required");
|
||||
this.#worker = workerFactory?.() ?? new Worker(new URL("./worker.js", import.meta.url), {
|
||||
type: "module",
|
||||
@@ -59,12 +60,13 @@ export class YawnCore {
|
||||
this.#worker.addEventListener("messageerror", () => this.#fail("WORKER_ERROR"));
|
||||
this.#worker.start?.();
|
||||
const offscreen = canvas.transferControlToOffscreen?.() ?? canvas;
|
||||
this.ready = this.#request("init", { canvas: offscreen, arenaBytes }, [offscreen]).then(result => {
|
||||
this.ready = this.#request("init", { canvas: offscreen, arenaBytes }, [offscreen]).then(async result => {
|
||||
this.#buffer = result.buffer;
|
||||
for (const descriptor of result.rows) this.#arrays.set(
|
||||
descriptor.name,
|
||||
new SharedRows(this.#buffer, descriptor),
|
||||
);
|
||||
if (debug) await this.#request("set-profiler", { enabled: true });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,6 +119,18 @@ export class YawnCore {
|
||||
return this.#request("switch-loadout", { id });
|
||||
}
|
||||
|
||||
async uploadTexture(name, image) {
|
||||
await this.ready;
|
||||
if (typeof name !== "string" || !(image instanceof ImageBitmap))
|
||||
throw new TypeError("TEXTURE_SOURCE");
|
||||
return this.#request("upload-texture", { name, image }, [image]);
|
||||
}
|
||||
|
||||
async deleteTexture(name) {
|
||||
await this.ready;
|
||||
return this.#request("delete-texture", { name });
|
||||
}
|
||||
|
||||
async play() {
|
||||
await this.ready;
|
||||
return this.#request("play");
|
||||
@@ -132,6 +146,17 @@ export class YawnCore {
|
||||
return this.#request("set-fps", { fps });
|
||||
}
|
||||
|
||||
async setProfiler(enabled) {
|
||||
await this.ready;
|
||||
return this.#request("set-profiler", { enabled: Boolean(enabled) });
|
||||
}
|
||||
|
||||
onProfile(listener) {
|
||||
if (typeof listener !== "function") throw new TypeError("PROFILE_LISTENER");
|
||||
this.#profileListeners.add(listener);
|
||||
return () => this.#profileListeners.delete(listener);
|
||||
}
|
||||
|
||||
array(name) {
|
||||
const array = this.#arrays.get(name);
|
||||
if (!array) throw new Error(`UNKNOWN_ARRAY: ${name}`);
|
||||
@@ -147,6 +172,10 @@ export class YawnCore {
|
||||
}
|
||||
|
||||
#message(message) {
|
||||
if (message?.type === "profile") {
|
||||
for (const listener of this.#profileListeners) listener(message.stats);
|
||||
return;
|
||||
}
|
||||
const pending = this.#pending.get(message?.request);
|
||||
if (!pending) return;
|
||||
this.#pending.delete(message.request);
|
||||
@@ -161,6 +190,7 @@ export class YawnCore {
|
||||
|
||||
dispose() {
|
||||
this.#fail("DISPOSED");
|
||||
this.#profileListeners.clear();
|
||||
this.#worker.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
+29
@@ -174,6 +174,21 @@ impl Core {
|
||||
.map_err(|error| JsError::new(&error))
|
||||
}
|
||||
|
||||
pub fn upload_texture(&self, name: String, image: web_sys::ImageBitmap) -> Result<(), JsError> {
|
||||
let gpu = self.gpu.borrow();
|
||||
let gpu = gpu
|
||||
.as_ref()
|
||||
.ok_or_else(|| JsError::new("WEBGPU_UNINITIALIZED"))?;
|
||||
self.store
|
||||
.borrow_mut()
|
||||
.upload_texture(name, image, gpu)
|
||||
.map_err(|error| JsError::new(&error))
|
||||
}
|
||||
|
||||
pub fn delete_texture(&self, name: &str) {
|
||||
self.store.borrow_mut().delete_texture(name);
|
||||
}
|
||||
|
||||
pub fn play(&self) {
|
||||
self.render.play();
|
||||
}
|
||||
@@ -185,4 +200,18 @@ impl Core {
|
||||
pub fn set_fps(&self, fps: u32) -> Result<(), JsError> {
|
||||
self.render.set_fps(fps).map_err(JsError::new)
|
||||
}
|
||||
|
||||
pub fn set_profiler(&self, enabled: bool) -> bool {
|
||||
let supported = self
|
||||
.gpu
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.is_some_and(|gpu| gpu.timestamp_queries);
|
||||
self.render.set_profiling(enabled && supported);
|
||||
supported
|
||||
}
|
||||
|
||||
pub fn take_profile(&self) -> Option<String> {
|
||||
self.render.take_profile()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::num::NonZeroU64;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::gpu::Wgpu;
|
||||
use crate::graph::{Binding, Extent, Pass, RenderGraph, RenderPipeline, Texture};
|
||||
use crate::graph::{Binding, Execution, Extent, Pass, RenderGraph, RenderPipeline, Texture};
|
||||
use crate::render_data::RenderData;
|
||||
|
||||
pub struct GpuResources {
|
||||
@@ -16,28 +18,59 @@ pub struct GpuResources {
|
||||
pub render_pipelines: HashMap<String, wgpu::RenderPipeline>,
|
||||
pub compute_pipelines: HashMap<String, wgpu::ComputePipeline>,
|
||||
pub passes: Vec<GpuPass>,
|
||||
pub profiler: Option<GpuProfiler>,
|
||||
}
|
||||
|
||||
pub struct GpuProfiler {
|
||||
query_set: wgpu::QuerySet,
|
||||
resolve: wgpu::Buffer,
|
||||
readback: Arc<wgpu::Buffer>,
|
||||
query_count: u32,
|
||||
}
|
||||
|
||||
pub struct ProfileMap {
|
||||
readback: Arc<wgpu::Buffer>,
|
||||
state: Arc<AtomicU8>,
|
||||
labels: Vec<String>,
|
||||
timestamp_period: f32,
|
||||
}
|
||||
|
||||
pub struct GpuBuffer {
|
||||
pub buffer: wgpu::Buffer,
|
||||
pub source: String,
|
||||
pub sync_each_frame: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GpuTexture {
|
||||
pub _texture: wgpu::Texture,
|
||||
pub texture: wgpu::Texture,
|
||||
pub view: wgpu::TextureView,
|
||||
key: String,
|
||||
uploaded: bool,
|
||||
}
|
||||
|
||||
pub enum GpuPass {
|
||||
Render(wgpu::RenderBundle),
|
||||
Render {
|
||||
label: String,
|
||||
first: usize,
|
||||
last: usize,
|
||||
bundle: wgpu::RenderBundle,
|
||||
},
|
||||
Compute {
|
||||
label: String,
|
||||
pass: usize,
|
||||
pipeline: wgpu::ComputePipeline,
|
||||
bind_groups: Vec<(u32, wgpu::BindGroup)>,
|
||||
},
|
||||
}
|
||||
|
||||
impl GpuResources {
|
||||
pub fn activate(graph: &RenderGraph, gpu: &Wgpu, data: &RenderData) -> Result<Self, String> {
|
||||
pub fn activate(
|
||||
graph: &RenderGraph,
|
||||
gpu: &Wgpu,
|
||||
data: &RenderData,
|
||||
previous: Option<&Self>,
|
||||
) -> 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")?;
|
||||
@@ -54,6 +87,7 @@ impl GpuResources {
|
||||
GpuBuffer {
|
||||
buffer,
|
||||
source: source.array.clone(),
|
||||
sync_each_frame: source.sync == "frame",
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -65,12 +99,32 @@ impl GpuResources {
|
||||
let physical = match physical_slots.get(&source.slot) {
|
||||
Some(&physical) => physical,
|
||||
None => {
|
||||
let key = source.key()?;
|
||||
if !source.transient {
|
||||
if let Some(texture) = previous
|
||||
.and_then(|resources| {
|
||||
resources
|
||||
.texture_slots
|
||||
.get(&source.id)
|
||||
.map(|slot| &resources.textures[*slot])
|
||||
})
|
||||
.filter(|texture| texture.key == key)
|
||||
{
|
||||
let physical = textures.len();
|
||||
textures.push(texture.clone());
|
||||
physical_slots.insert(source.slot, physical);
|
||||
texture_slots.insert(source.id.clone(), physical);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
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,
|
||||
texture,
|
||||
key,
|
||||
uploaded: false,
|
||||
});
|
||||
physical_slots.insert(source.slot, physical);
|
||||
physical
|
||||
@@ -121,6 +175,30 @@ impl GpuResources {
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>, _>>()?;
|
||||
|
||||
let profiler = (gpu.timestamp_queries && !graph.executions.is_empty()).then(|| {
|
||||
let query_count = graph.executions.len() as u32 * 2;
|
||||
let bytes = u64::from(query_count) * 8;
|
||||
GpuProfiler {
|
||||
query_set: gpu.device.create_query_set(&wgpu::QuerySetDescriptor {
|
||||
label: Some("frame-profile"),
|
||||
ty: wgpu::QueryType::Timestamp,
|
||||
count: query_count,
|
||||
}),
|
||||
resolve: gpu.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("frame-profile-resolve"),
|
||||
size: bytes,
|
||||
usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
|
||||
mapped_at_creation: false,
|
||||
}),
|
||||
readback: Arc::new(gpu.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("frame-profile-readback"),
|
||||
size: bytes,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
})),
|
||||
query_count,
|
||||
}
|
||||
});
|
||||
let mut resources = Self {
|
||||
buffers,
|
||||
textures,
|
||||
@@ -129,11 +207,27 @@ impl GpuResources {
|
||||
render_pipelines,
|
||||
compute_pipelines,
|
||||
passes: Vec::new(),
|
||||
profiler,
|
||||
};
|
||||
for pass in &graph.passes {
|
||||
let compiled = match pass.kind.as_str() {
|
||||
"render" => GpuPass::Render(resources.render_bundle(graph, pass, gpu)?),
|
||||
"compute" => {
|
||||
for execution in &graph.executions {
|
||||
let compiled = match execution {
|
||||
Execution::Render(passes) => 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)", passes.len())
|
||||
} else {
|
||||
format!("Render ({} draws)", passes.len())
|
||||
},
|
||||
first: passes[0],
|
||||
last: *passes.last().unwrap(),
|
||||
bundle: resources.render_bundle(graph, passes, gpu)?,
|
||||
},
|
||||
Execution::Compute(index) => {
|
||||
let pass = &graph.passes[*index];
|
||||
let pipeline = resources
|
||||
.compute_pipelines
|
||||
.get(&pass.pipeline)
|
||||
@@ -145,11 +239,12 @@ impl GpuResources {
|
||||
gpu,
|
||||
)?;
|
||||
GpuPass::Compute {
|
||||
label: pass.id.clone(),
|
||||
pass: *index,
|
||||
pipeline,
|
||||
bind_groups,
|
||||
}
|
||||
}
|
||||
_ => return Err("GRAPH_PASS".into()),
|
||||
};
|
||||
resources.passes.push(compiled);
|
||||
}
|
||||
@@ -163,16 +258,146 @@ impl GpuResources {
|
||||
.map(|texture| &texture.view)
|
||||
}
|
||||
|
||||
pub fn upload_texture(
|
||||
&mut self,
|
||||
id: &str,
|
||||
image: &web_sys::ImageBitmap,
|
||||
gpu: &Wgpu,
|
||||
) -> Result<(), String> {
|
||||
let texture = self
|
||||
.texture_slots
|
||||
.get(id)
|
||||
.and_then(|slot| self.textures.get_mut(*slot))
|
||||
.ok_or("GRAPH_TEXTURE_UNKNOWN")?;
|
||||
gpu.queue.copy_external_image_to_texture(
|
||||
&wgpu::CopyExternalImageSourceInfo {
|
||||
source: wgpu::ExternalImageSource::ImageBitmap(image.clone()),
|
||||
origin: wgpu::Origin2d::ZERO,
|
||||
flip_y: false,
|
||||
},
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &texture.texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
}
|
||||
.to_tagged(wgpu::PredefinedColorSpace::Srgb, false),
|
||||
wgpu::Extent3d {
|
||||
width: image.width(),
|
||||
height: image.height(),
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
texture.uploaded = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn needs_upload(&self, id: &str) -> bool {
|
||||
self.texture_slots
|
||||
.get(id)
|
||||
.and_then(|slot| self.textures.get(*slot))
|
||||
.is_some_and(|texture| !texture.uploaded)
|
||||
}
|
||||
|
||||
pub fn render_timestamps(&self, pass: usize) -> Option<wgpu::RenderPassTimestampWrites<'_>> {
|
||||
self.profiler
|
||||
.as_ref()
|
||||
.map(|profiler| wgpu::RenderPassTimestampWrites {
|
||||
query_set: &profiler.query_set,
|
||||
beginning_of_pass_write_index: Some(pass as u32 * 2),
|
||||
end_of_pass_write_index: Some(pass as u32 * 2 + 1),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn compute_timestamps(&self, pass: usize) -> Option<wgpu::ComputePassTimestampWrites<'_>> {
|
||||
self.profiler
|
||||
.as_ref()
|
||||
.map(|profiler| wgpu::ComputePassTimestampWrites {
|
||||
query_set: &profiler.query_set,
|
||||
beginning_of_pass_write_index: Some(pass as u32 * 2),
|
||||
end_of_pass_write_index: Some(pass as u32 * 2 + 1),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resolve_profile(&self, encoder: &mut wgpu::CommandEncoder) {
|
||||
let Some(profiler) = &self.profiler else {
|
||||
return;
|
||||
};
|
||||
encoder.resolve_query_set(
|
||||
&profiler.query_set,
|
||||
0..profiler.query_count,
|
||||
&profiler.resolve,
|
||||
0,
|
||||
);
|
||||
encoder.copy_buffer_to_buffer(
|
||||
&profiler.resolve,
|
||||
0,
|
||||
&profiler.readback,
|
||||
0,
|
||||
u64::from(profiler.query_count) * 8,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn map_profile(&self, timestamp_period: f32) -> Option<ProfileMap> {
|
||||
let profiler = self.profiler.as_ref()?;
|
||||
let state = Arc::new(AtomicU8::new(0));
|
||||
let callback = state.clone();
|
||||
profiler
|
||||
.readback
|
||||
.map_async(wgpu::MapMode::Read, .., move |result| {
|
||||
callback.store(if result.is_ok() { 1 } else { 2 }, Ordering::Release);
|
||||
});
|
||||
Some(ProfileMap {
|
||||
readback: profiler.readback.clone(),
|
||||
state,
|
||||
labels: self
|
||||
.passes
|
||||
.iter()
|
||||
.map(|pass| pass.label().to_owned())
|
||||
.collect(),
|
||||
timestamp_period,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfileMap {
|
||||
pub fn state(&self) -> u8 {
|
||||
self.state.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn read(self) -> Vec<(String, f64)> {
|
||||
let bytes = self.readback.get_mapped_range(..);
|
||||
let values = bytes
|
||||
.chunks_exact(8)
|
||||
.map(|bytes| u64::from_le_bytes(bytes.try_into().unwrap()))
|
||||
.collect::<Vec<_>>();
|
||||
let profile = self
|
||||
.labels
|
||||
.into_iter()
|
||||
.zip(values.chunks_exact(2))
|
||||
.map(|(label, timestamps)| {
|
||||
(
|
||||
label,
|
||||
timestamps[1].saturating_sub(timestamps[0]) as f64
|
||||
* f64::from(self.timestamp_period)
|
||||
/ 1_000_000.0,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
drop(bytes);
|
||||
self.readback.unmap();
|
||||
profile
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuResources {
|
||||
fn render_bundle(
|
||||
&self,
|
||||
graph: &RenderGraph,
|
||||
pass: &Pass,
|
||||
passes: &[usize],
|
||||
gpu: &Wgpu,
|
||||
) -> Result<wgpu::RenderBundle, String> {
|
||||
let pipeline = self
|
||||
.render_pipelines
|
||||
.get(&pass.pipeline)
|
||||
.ok_or("GRAPH_PIPELINE")?;
|
||||
let pass = &graph.passes[passes[0]];
|
||||
let declaration = graph
|
||||
.pipelines
|
||||
.render
|
||||
@@ -204,40 +429,56 @@ impl GpuResources {
|
||||
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,
|
||||
);
|
||||
let mut previous_pipeline = None;
|
||||
let mut previous_bindings: Option<&[Binding]> = None;
|
||||
for index in passes {
|
||||
let pass = &graph.passes[*index];
|
||||
let pipeline = self
|
||||
.render_pipelines
|
||||
.get(&pass.pipeline)
|
||||
.ok_or("GRAPH_PIPELINE")?;
|
||||
let pipeline_changed = previous_pipeline != Some(pass.pipeline.as_str());
|
||||
if pipeline_changed {
|
||||
encoder.set_pipeline(pipeline);
|
||||
previous_pipeline = Some(pass.pipeline.as_str());
|
||||
}
|
||||
if pipeline_changed || previous_bindings != Some(pass.bindings.as_slice()) {
|
||||
for (group, bind_group) in
|
||||
self.bind_groups(pass, |group| pipeline.get_bind_group_layout(group), gpu)?
|
||||
{
|
||||
encoder.set_bind_group(group, &bind_group, &[]);
|
||||
}
|
||||
previous_bindings = Some(&pass.bindings);
|
||||
}
|
||||
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),
|
||||
@@ -290,6 +531,14 @@ impl GpuResources {
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuPass {
|
||||
fn label(&self) -> &str {
|
||||
match self {
|
||||
Self::Render { label, .. } | Self::Compute { label, .. } => label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_render_pipeline(
|
||||
source: &RenderPipeline,
|
||||
gpu: &Wgpu,
|
||||
|
||||
@@ -12,6 +12,14 @@ pub struct RenderGraph {
|
||||
#[serde(default)]
|
||||
pub pipelines: PipelineDeclarations,
|
||||
pub passes: Vec<Pass>,
|
||||
#[serde(skip)]
|
||||
pub executions: Vec<Execution>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Execution {
|
||||
Compute(usize),
|
||||
Render(Vec<usize>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
@@ -30,6 +38,8 @@ pub struct Buffer {
|
||||
pub array: String,
|
||||
#[serde(default)]
|
||||
pub usage: Vec<String>,
|
||||
#[serde(default = "frame_sync")]
|
||||
pub sync: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
@@ -186,7 +196,7 @@ pub struct Pass {
|
||||
pub dispatch: [u32; 3],
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[derive(Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct Binding {
|
||||
#[serde(default)]
|
||||
pub group: u32,
|
||||
@@ -318,7 +328,9 @@ impl RenderGraph {
|
||||
for pass in &mut self.passes {
|
||||
pass.dependencies = pass.after.iter().map(|id| sorted_ids[id]).collect();
|
||||
}
|
||||
self.plan_resources()
|
||||
self.plan_resources()?;
|
||||
self.plan_execution();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn plan_resources(&mut self) -> Result<(), &'static str> {
|
||||
@@ -391,6 +403,80 @@ impl RenderGraph {
|
||||
self.validate_ids()
|
||||
}
|
||||
|
||||
fn plan_execution(&mut self) {
|
||||
let mut executions = Vec::new();
|
||||
for index in 0..self.passes.len() {
|
||||
let merge = executions.last().is_some_and(|execution| match execution {
|
||||
Execution::Render(passes) => self.can_merge_render(*passes.last().unwrap(), index),
|
||||
Execution::Compute(_) => false,
|
||||
});
|
||||
if merge {
|
||||
let Some(Execution::Render(passes)) = executions.last_mut() else {
|
||||
unreachable!()
|
||||
};
|
||||
passes.push(index);
|
||||
} else if self.passes[index].kind == "render" {
|
||||
executions.push(Execution::Render(vec![index]));
|
||||
} else {
|
||||
executions.push(Execution::Compute(index));
|
||||
}
|
||||
}
|
||||
self.executions = executions;
|
||||
}
|
||||
|
||||
fn can_merge_render(&self, previous: usize, next: usize) -> bool {
|
||||
let previous = &self.passes[previous];
|
||||
let next = &self.passes[next];
|
||||
if next.kind != "render"
|
||||
|| previous.color.len() != next.color.len()
|
||||
|| self.sample_count(&previous.pipeline) != self.sample_count(&next.pipeline)
|
||||
|| previous
|
||||
.color
|
||||
.iter()
|
||||
.zip(&next.color)
|
||||
.any(|(previous, next)| {
|
||||
previous.resource != next.resource
|
||||
|| previous.store != "store"
|
||||
|| next.load != "load"
|
||||
})
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let same_depth = match (&previous.depth, &next.depth) {
|
||||
(None, None) => true,
|
||||
(Some(previous), Some(next)) => {
|
||||
previous.resource == next.resource
|
||||
&& previous.store == "store"
|
||||
&& next.load == "load"
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
same_depth
|
||||
&& !next.bindings.iter().any(|binding| {
|
||||
next.color
|
||||
.iter()
|
||||
.any(|attachment| attachment.resource == binding.resource)
|
||||
|| next
|
||||
.depth
|
||||
.as_ref()
|
||||
.is_some_and(|attachment| attachment.resource == binding.resource)
|
||||
})
|
||||
}
|
||||
|
||||
fn sample_count(&self, pipeline: &str) -> Option<u64> {
|
||||
self.pipelines
|
||||
.render
|
||||
.iter()
|
||||
.find(|value| value.id == pipeline)
|
||||
.map(|value| {
|
||||
value
|
||||
.multisample
|
||||
.get("count")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(1)
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_ids(&self) -> Result<(), &'static str> {
|
||||
let mut resources = HashSet::new();
|
||||
for id in self
|
||||
@@ -405,6 +491,14 @@ impl RenderGraph {
|
||||
return Err("GRAPH_RESOURCE");
|
||||
}
|
||||
}
|
||||
if self
|
||||
.resources
|
||||
.buffers
|
||||
.iter()
|
||||
.any(|buffer| !matches!(buffer.sync.as_str(), "frame" | "loadout"))
|
||||
{
|
||||
return Err("GRAPH_BUFFER_SYNC");
|
||||
}
|
||||
let mut pipelines = HashSet::new();
|
||||
for id in self
|
||||
.pipelines
|
||||
@@ -443,7 +537,7 @@ impl Pass {
|
||||
}
|
||||
|
||||
impl Texture {
|
||||
fn key(&self) -> Result<String, &'static str> {
|
||||
pub(crate) fn key(&self) -> Result<String, &'static str> {
|
||||
let mut usage = self.usage.clone();
|
||||
usage.sort_unstable();
|
||||
usage.dedup();
|
||||
@@ -498,3 +592,6 @@ fn index_format() -> String {
|
||||
fn dispatch() -> [u32; 3] {
|
||||
[1, 1, 1]
|
||||
}
|
||||
fn frame_sync() -> String {
|
||||
"frame".into()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ pub struct Loadout {
|
||||
#[derive(Default)]
|
||||
pub struct Store {
|
||||
graphs: HashMap<String, RenderGraph>,
|
||||
texture_sources: HashMap<String, web_sys::ImageBitmap>,
|
||||
active: Option<Loadout>,
|
||||
}
|
||||
|
||||
@@ -25,11 +26,44 @@ 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 resources = GpuResources::activate(&graph, gpu, data)?;
|
||||
let mut resources = GpuResources::activate(
|
||||
&graph,
|
||||
gpu,
|
||||
data,
|
||||
self.active.as_ref().map(|active| &active.resources),
|
||||
)?;
|
||||
for (name, image) in &self.texture_sources {
|
||||
if resources.needs_upload(name) {
|
||||
resources.upload_texture(name, image, gpu)?;
|
||||
}
|
||||
}
|
||||
self.active = Some(Loadout { graph, resources });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn upload_texture(
|
||||
&mut self,
|
||||
name: String,
|
||||
image: web_sys::ImageBitmap,
|
||||
gpu: &Wgpu,
|
||||
) -> Result<(), String> {
|
||||
if let Some(active) = &mut self.active {
|
||||
if active.resources.texture_slots.contains_key(&name) {
|
||||
active.resources.upload_texture(&name, &image, gpu)?;
|
||||
}
|
||||
}
|
||||
if let Some(previous) = self.texture_sources.insert(name, image) {
|
||||
previous.close();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_texture(&mut self, name: &str) {
|
||||
if let Some(image) = self.texture_sources.remove(name) {
|
||||
image.close();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_mut(&mut self) -> Option<&mut Loadout> {
|
||||
self.active.as_mut()
|
||||
}
|
||||
@@ -62,7 +96,17 @@ impl Store {
|
||||
return Ok(());
|
||||
};
|
||||
let graph = active.graph.clone();
|
||||
let resources = GpuResources::activate(&graph, gpu, data)?;
|
||||
let mut resources = GpuResources::activate(
|
||||
&graph,
|
||||
gpu,
|
||||
data,
|
||||
self.active.as_ref().map(|active| &active.resources),
|
||||
)?;
|
||||
for (name, image) in &self.texture_sources {
|
||||
if resources.needs_upload(name) {
|
||||
resources.upload_texture(name, image, gpu)?;
|
||||
}
|
||||
}
|
||||
self.active = Some(Loadout { graph, resources });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+126
-18
@@ -1,10 +1,12 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
|
||||
use crate::gpu::Wgpu;
|
||||
use crate::gpu_resource::GpuPass;
|
||||
use crate::gpu_resource::{GpuPass, ProfileMap};
|
||||
use crate::graph::{ColorAttachment, DepthAttachment};
|
||||
use crate::render_data::RenderData;
|
||||
use crate::store::{Loadout, Store};
|
||||
@@ -16,6 +18,13 @@ pub struct RenderLoop {
|
||||
frame: Cell<u32>,
|
||||
elapsed: Cell<f64>,
|
||||
last: Cell<f64>,
|
||||
profiling: Cell<bool>,
|
||||
profile: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
struct Submission {
|
||||
completed: Arc<AtomicBool>,
|
||||
profile: Option<ProfileMap>,
|
||||
}
|
||||
|
||||
impl RenderLoop {
|
||||
@@ -27,6 +36,8 @@ impl RenderLoop {
|
||||
frame: Cell::new(0),
|
||||
elapsed: Cell::new(0.0),
|
||||
last: Cell::new(js_sys::Date::now()),
|
||||
profiling: Cell::new(false),
|
||||
profile: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +58,17 @@ impl RenderLoop {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_profiling(&self, enabled: bool) {
|
||||
self.profiling.set(enabled);
|
||||
if !enabled {
|
||||
self.profile.borrow_mut().take();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_profile(&self) -> Option<String> {
|
||||
self.profile.borrow_mut().take()
|
||||
}
|
||||
|
||||
pub fn start(
|
||||
self: &Rc<Self>,
|
||||
gpu: Rc<RefCell<Option<Wgpu>>>,
|
||||
@@ -71,11 +93,51 @@ impl RenderLoop {
|
||||
data.update_info(delta as f32, frame, elapsed as f32, control.fps.get());
|
||||
data.skip_render()
|
||||
};
|
||||
if !skip {
|
||||
let submission = if !skip {
|
||||
if let (Some(gpu), Some(loadout)) =
|
||||
(gpu.borrow_mut().as_mut(), store.borrow_mut().active_mut())
|
||||
{
|
||||
let _ = gpu.render(loadout, &data.borrow());
|
||||
gpu.render(loadout, &data.borrow(), control.profiling.get())
|
||||
.ok()
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(submission) = submission {
|
||||
while !submission.completed.load(Ordering::Acquire) {
|
||||
TimeoutFuture::new(0).await;
|
||||
}
|
||||
if let Some(profile) = submission.profile {
|
||||
while profile.state() == 0 {
|
||||
TimeoutFuture::new(0).await;
|
||||
}
|
||||
if profile.state() == 1 {
|
||||
let passes = profile
|
||||
.read()
|
||||
.into_iter()
|
||||
.map(|(name, milliseconds)| {
|
||||
serde_json::json!({
|
||||
"name": name,
|
||||
"milliseconds": milliseconds,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let milliseconds = passes
|
||||
.iter()
|
||||
.filter_map(|pass| pass["milliseconds"].as_f64())
|
||||
.sum::<f64>();
|
||||
*control.profile.borrow_mut() = Some(
|
||||
serde_json::json!({
|
||||
"frame": frame,
|
||||
"milliseconds": milliseconds,
|
||||
"passes": passes,
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,8 +150,16 @@ impl RenderLoop {
|
||||
}
|
||||
|
||||
impl Wgpu {
|
||||
fn render(&mut self, loadout: &mut Loadout, data: &RenderData) -> Result<(), String> {
|
||||
fn render(
|
||||
&mut self,
|
||||
loadout: &mut Loadout,
|
||||
data: &RenderData,
|
||||
profile: bool,
|
||||
) -> Result<Option<Submission>, String> {
|
||||
for buffer in loadout.resources.buffers.values() {
|
||||
if !buffer.sync_each_frame {
|
||||
continue;
|
||||
}
|
||||
self.queue.write_buffer(
|
||||
&buffer.buffer,
|
||||
0,
|
||||
@@ -102,7 +172,7 @@ impl Wgpu {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.surface.get_current_texture().map_err(|_| "SURFACE")?
|
||||
}
|
||||
Err(wgpu::SurfaceError::Timeout) => return Ok(()),
|
||||
Err(wgpu::SurfaceError::Timeout) => return Ok(None),
|
||||
Err(_) => return Err("SURFACE".into()),
|
||||
};
|
||||
let surface_view = output
|
||||
@@ -113,15 +183,21 @@ impl Wgpu {
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("frame"),
|
||||
});
|
||||
for (pass, compiled) in loadout.graph.passes.iter().zip(&loadout.resources.passes) {
|
||||
for (pass_index, compiled) in loadout.resources.passes.iter().enumerate() {
|
||||
match compiled {
|
||||
GpuPass::Compute {
|
||||
pass,
|
||||
pipeline,
|
||||
bind_groups,
|
||||
..
|
||||
} => {
|
||||
let pass = &loadout.graph.passes[*pass];
|
||||
let timestamp_writes = profile
|
||||
.then(|| loadout.resources.compute_timestamps(pass_index))
|
||||
.flatten();
|
||||
let mut command = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||
label: Some(&pass.id),
|
||||
timestamp_writes: None,
|
||||
timestamp_writes,
|
||||
});
|
||||
command.set_pipeline(pipeline);
|
||||
for (group, bind_group) in bind_groups {
|
||||
@@ -133,11 +209,19 @@ impl Wgpu {
|
||||
pass.dispatch[2],
|
||||
);
|
||||
}
|
||||
GpuPass::Render(bundle) => {
|
||||
GpuPass::Render {
|
||||
first,
|
||||
last,
|
||||
bundle,
|
||||
..
|
||||
} => {
|
||||
let pass = &loadout.graph.passes[*first];
|
||||
let last = &loadout.graph.passes[*last];
|
||||
let colors = pass
|
||||
.color
|
||||
.iter()
|
||||
.map(|attachment| {
|
||||
.zip(&last.color)
|
||||
.map(|(attachment, final_attachment)| {
|
||||
Ok(Some(wgpu::RenderPassColorAttachment {
|
||||
view: view(
|
||||
&loadout.resources,
|
||||
@@ -146,39 +230,57 @@ impl Wgpu {
|
||||
)?,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: color_ops(attachment)?,
|
||||
ops: color_ops(attachment, final_attachment)?,
|
||||
}))
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
let depth = pass
|
||||
.depth
|
||||
.as_ref()
|
||||
.map(|attachment| {
|
||||
.zip(last.depth.as_ref())
|
||||
.map(|(attachment, final_attachment)| {
|
||||
Ok::<_, String>(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: view(
|
||||
&loadout.resources,
|
||||
&surface_view,
|
||||
&attachment.resource,
|
||||
)?,
|
||||
depth_ops: Some(depth_ops(attachment)?),
|
||||
depth_ops: Some(depth_ops(attachment, final_attachment)?),
|
||||
stencil_ops: None,
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
let timestamp_writes = profile
|
||||
.then(|| loadout.resources.render_timestamps(pass_index))
|
||||
.flatten();
|
||||
let mut command = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some(&pass.id),
|
||||
color_attachments: &colors,
|
||||
depth_stencil_attachment: depth,
|
||||
timestamp_writes: None,
|
||||
timestamp_writes,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
command.execute_bundles([bundle]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if profile {
|
||||
loadout.resources.resolve_profile(&mut encoder);
|
||||
}
|
||||
self.queue.submit([encoder.finish()]);
|
||||
let profile = profile
|
||||
.then(|| {
|
||||
loadout
|
||||
.resources
|
||||
.map_profile(self.queue.get_timestamp_period())
|
||||
})
|
||||
.flatten();
|
||||
output.present();
|
||||
Ok(())
|
||||
let completed = Arc::new(AtomicBool::new(false));
|
||||
let callback = completed.clone();
|
||||
self.queue
|
||||
.on_submitted_work_done(move || callback.store(true, Ordering::Release));
|
||||
Ok(Some(Submission { completed, profile }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +298,10 @@ fn view<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
fn color_ops(attachment: &ColorAttachment) -> Result<wgpu::Operations<wgpu::Color>, String> {
|
||||
fn color_ops(
|
||||
attachment: &ColorAttachment,
|
||||
final_attachment: &ColorAttachment,
|
||||
) -> Result<wgpu::Operations<wgpu::Color>, String> {
|
||||
let clear = attachment.clear.as_slice();
|
||||
Ok(wgpu::Operations {
|
||||
load: match attachment.load.as_str() {
|
||||
@@ -209,18 +314,21 @@ fn color_ops(attachment: &ColorAttachment) -> Result<wgpu::Operations<wgpu::Colo
|
||||
}),
|
||||
_ => return Err("GRAPH_LOAD_OP".into()),
|
||||
},
|
||||
store: store_op(&attachment.store)?,
|
||||
store: store_op(&final_attachment.store)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn depth_ops(attachment: &DepthAttachment) -> Result<wgpu::Operations<f32>, String> {
|
||||
fn depth_ops(
|
||||
attachment: &DepthAttachment,
|
||||
final_attachment: &DepthAttachment,
|
||||
) -> Result<wgpu::Operations<f32>, String> {
|
||||
Ok(wgpu::Operations {
|
||||
load: match attachment.load.as_str() {
|
||||
"load" => wgpu::LoadOp::Load,
|
||||
"clear" => wgpu::LoadOp::Clear(attachment.clear),
|
||||
_ => return Err("GRAPH_LOAD_OP".into()),
|
||||
},
|
||||
store: store_op(&attachment.store)?,
|
||||
store: store_op(&final_attachment.store)?,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ pub struct Wgpu {
|
||||
pub format: wgpu::TextureFormat,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub timestamp_queries: bool,
|
||||
}
|
||||
|
||||
impl Wgpu {
|
||||
@@ -30,8 +31,12 @@ impl Wgpu {
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "WEBGPU_UNAVAILABLE")?;
|
||||
let required_features = adapter.features() & wgpu::Features::TIMESTAMP_QUERY;
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor::default())
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
required_features,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "DEVICE")?;
|
||||
let config = surface
|
||||
@@ -48,6 +53,7 @@ impl Wgpu {
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
timestamp_queries: required_features.contains(wgpu::Features::TIMESTAMP_QUERY),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import initWasm, { Core } from "./pkg/yawn_core.js";
|
||||
|
||||
let core;
|
||||
let profileTimer;
|
||||
|
||||
const fail = code => { throw new Error(code); };
|
||||
|
||||
@@ -39,6 +40,12 @@ addEventListener("message", async ({ data: message }) => {
|
||||
case "switch-loadout":
|
||||
core.switch_loadout(message.id);
|
||||
break;
|
||||
case "upload-texture":
|
||||
core.upload_texture(message.name, message.image);
|
||||
break;
|
||||
case "delete-texture":
|
||||
core.delete_texture(message.name);
|
||||
break;
|
||||
case "play":
|
||||
core.play();
|
||||
break;
|
||||
@@ -48,6 +55,14 @@ addEventListener("message", async ({ data: message }) => {
|
||||
case "set-fps":
|
||||
core.set_fps(message.fps);
|
||||
break;
|
||||
case "set-profiler":
|
||||
result = core.set_profiler(Boolean(message.enabled));
|
||||
clearInterval(profileTimer);
|
||||
profileTimer = result && message.enabled ? setInterval(() => {
|
||||
const stats = core.take_profile();
|
||||
if (stats) postMessage({ type: "profile", stats: JSON.parse(stats) });
|
||||
}, 250) : undefined;
|
||||
break;
|
||||
default:
|
||||
fail("MESSAGE");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user