Optimize forward rendering and add benchmark
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:
+12
-3
@@ -119,11 +119,20 @@ export class YawnCore {
|
||||
return this.#request("switch-loadout", { id });
|
||||
}
|
||||
|
||||
async uploadTexture(name, image) {
|
||||
async uploadTexture(name, image, mipLevel = 0) {
|
||||
await this.ready;
|
||||
if (typeof name !== "string" || !(image instanceof ImageBitmap))
|
||||
if (
|
||||
typeof name !== "string" ||
|
||||
!(image instanceof ImageBitmap) ||
|
||||
!Number.isInteger(mipLevel) ||
|
||||
mipLevel < 0
|
||||
)
|
||||
throw new TypeError("TEXTURE_SOURCE");
|
||||
return this.#request("upload-texture", { name, image }, [image]);
|
||||
return this.#request(
|
||||
"upload-texture",
|
||||
{ name, image, mipLevel },
|
||||
[image],
|
||||
);
|
||||
}
|
||||
|
||||
async deleteTexture(name) {
|
||||
|
||||
+7
-2
@@ -174,14 +174,19 @@ impl Core {
|
||||
.map_err(|error| JsError::new(&error))
|
||||
}
|
||||
|
||||
pub fn upload_texture(&self, name: String, image: web_sys::ImageBitmap) -> Result<(), JsError> {
|
||||
pub fn upload_texture(
|
||||
&self,
|
||||
name: String,
|
||||
mip_level: u32,
|
||||
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)
|
||||
.upload_texture(name, mip_level, image, gpu)
|
||||
.map_err(|error| JsError::new(&error))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::num::NonZeroU64;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -46,7 +46,7 @@ pub struct GpuTexture {
|
||||
pub texture: wgpu::Texture,
|
||||
pub view: wgpu::TextureView,
|
||||
key: String,
|
||||
uploaded: bool,
|
||||
uploaded_mips: HashSet<u32>,
|
||||
}
|
||||
|
||||
pub enum GpuPass {
|
||||
@@ -124,7 +124,7 @@ impl GpuResources {
|
||||
view: texture.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||
texture,
|
||||
key,
|
||||
uploaded: false,
|
||||
uploaded_mips: HashSet::new(),
|
||||
});
|
||||
physical_slots.insert(source.slot, physical);
|
||||
physical
|
||||
@@ -211,21 +211,29 @@ impl GpuResources {
|
||||
};
|
||||
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::Render(passes) => {
|
||||
let (bundle, draws) = resources.render_bundle(graph, passes, gpu)?;
|
||||
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)")
|
||||
} else {
|
||||
format!("Render ({draws} draws)")
|
||||
},
|
||||
first: passes[0],
|
||||
last: *passes.last().unwrap(),
|
||||
bundle,
|
||||
}
|
||||
}
|
||||
Execution::Compute(index) => {
|
||||
let pass = &graph.passes[*index];
|
||||
let pipeline = resources
|
||||
@@ -261,6 +269,7 @@ impl GpuResources {
|
||||
pub fn upload_texture(
|
||||
&mut self,
|
||||
id: &str,
|
||||
mip_level: u32,
|
||||
image: &web_sys::ImageBitmap,
|
||||
gpu: &Wgpu,
|
||||
) -> Result<(), String> {
|
||||
@@ -277,7 +286,7 @@ impl GpuResources {
|
||||
},
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &texture.texture,
|
||||
mip_level: 0,
|
||||
mip_level,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
}
|
||||
@@ -288,15 +297,15 @@ impl GpuResources {
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
texture.uploaded = true;
|
||||
texture.uploaded_mips.insert(mip_level);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn needs_upload(&self, id: &str) -> bool {
|
||||
pub fn needs_upload(&self, id: &str, mip_level: u32) -> bool {
|
||||
self.texture_slots
|
||||
.get(id)
|
||||
.and_then(|slot| self.textures.get(*slot))
|
||||
.is_some_and(|texture| !texture.uploaded)
|
||||
.is_some_and(|texture| !texture.uploaded_mips.contains(&mip_level))
|
||||
}
|
||||
|
||||
pub fn render_timestamps(&self, pass: usize) -> Option<wgpu::RenderPassTimestampWrites<'_>> {
|
||||
@@ -396,7 +405,7 @@ impl GpuResources {
|
||||
graph: &RenderGraph,
|
||||
passes: &[usize],
|
||||
gpu: &Wgpu,
|
||||
) -> Result<wgpu::RenderBundle, String> {
|
||||
) -> Result<(wgpu::RenderBundle, usize), String> {
|
||||
let pass = &graph.passes[passes[0]];
|
||||
let declaration = graph
|
||||
.pipelines
|
||||
@@ -431,8 +440,10 @@ impl GpuResources {
|
||||
});
|
||||
let mut previous_pipeline = None;
|
||||
let mut previous_bindings: Option<&[Binding]> = None;
|
||||
for index in passes {
|
||||
let pass = &graph.passes[*index];
|
||||
let mut draws = 0;
|
||||
let mut at = 0;
|
||||
while at < passes.len() {
|
||||
let pass = &graph.passes[passes[at]];
|
||||
let pipeline = self
|
||||
.render_pipelines
|
||||
.get(&pass.pipeline)
|
||||
@@ -468,10 +479,17 @@ 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 + pass.draw.instances,
|
||||
pass.draw.first_instance..pass.draw.first_instance + instances,
|
||||
);
|
||||
} else {
|
||||
encoder.draw(
|
||||
@@ -479,10 +497,15 @@ impl GpuResources {
|
||||
pass.draw.first_instance..pass.draw.first_instance + pass.draw.instances,
|
||||
);
|
||||
}
|
||||
draws += 1;
|
||||
at += 1;
|
||||
}
|
||||
Ok(encoder.finish(&wgpu::RenderBundleDescriptor {
|
||||
label: Some(&pass.id),
|
||||
}))
|
||||
Ok((
|
||||
encoder.finish(&wgpu::RenderBundleDescriptor {
|
||||
label: Some(&pass.id),
|
||||
}),
|
||||
draws,
|
||||
))
|
||||
}
|
||||
|
||||
fn bind_groups(
|
||||
@@ -531,6 +554,18 @@ 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 {
|
||||
@@ -621,7 +656,7 @@ fn create_render_pipeline(
|
||||
primitive: primitive(&source.primitive)?,
|
||||
depth_stencil: depth_stencil(&source.depth_stencil)?,
|
||||
multisample: multisample(&source.multisample)?,
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
fragment: (!targets.is_empty()).then_some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some(&source.fragment.entry),
|
||||
compilation_options: Default::default(),
|
||||
|
||||
@@ -229,7 +229,7 @@ pub struct DepthAttachment {
|
||||
pub store: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[derive(Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct VertexBinding {
|
||||
#[serde(default)]
|
||||
pub slot: u32,
|
||||
@@ -238,7 +238,7 @@ pub struct VertexBinding {
|
||||
pub offset: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[derive(Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct IndexBinding {
|
||||
pub resource: String,
|
||||
#[serde(default = "index_format")]
|
||||
@@ -247,7 +247,7 @@ pub struct IndexBinding {
|
||||
pub offset: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[derive(Clone, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Draw {
|
||||
#[serde(default = "three")]
|
||||
|
||||
+21
-11
@@ -13,7 +13,7 @@ pub struct Loadout {
|
||||
#[derive(Default)]
|
||||
pub struct Store {
|
||||
graphs: HashMap<String, RenderGraph>,
|
||||
texture_sources: HashMap<String, web_sys::ImageBitmap>,
|
||||
texture_sources: HashMap<String, HashMap<u32, web_sys::ImageBitmap>>,
|
||||
active: Option<Loadout>,
|
||||
}
|
||||
|
||||
@@ -32,9 +32,11 @@ impl Store {
|
||||
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)?;
|
||||
for (name, levels) in &self.texture_sources {
|
||||
for (mip_level, image) in levels {
|
||||
if resources.needs_upload(name, *mip_level) {
|
||||
resources.upload_texture(name, *mip_level, image, gpu)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.active = Some(Loadout { graph, resources });
|
||||
@@ -44,23 +46,29 @@ impl Store {
|
||||
pub fn upload_texture(
|
||||
&mut self,
|
||||
name: String,
|
||||
mip_level: u32,
|
||||
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)?;
|
||||
active
|
||||
.resources
|
||||
.upload_texture(&name, mip_level, &image, gpu)?;
|
||||
}
|
||||
}
|
||||
if let Some(previous) = self.texture_sources.insert(name, image) {
|
||||
let levels = self.texture_sources.entry(name).or_default();
|
||||
if let Some(previous) = levels.insert(mip_level, image) {
|
||||
previous.close();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_texture(&mut self, name: &str) {
|
||||
if let Some(image) = self.texture_sources.remove(name) {
|
||||
image.close();
|
||||
if let Some(levels) = self.texture_sources.remove(name) {
|
||||
for image in levels.into_values() {
|
||||
image.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +110,11 @@ impl Store {
|
||||
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)?;
|
||||
for (name, levels) in &self.texture_sources {
|
||||
for (mip_level, image) in levels {
|
||||
if resources.needs_upload(name, *mip_level) {
|
||||
resources.upload_texture(name, *mip_level, image, gpu)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.active = Some(Loadout { graph, resources });
|
||||
|
||||
@@ -110,6 +110,7 @@ impl RenderLoop {
|
||||
while !submission.completed.load(Ordering::Acquire) {
|
||||
TimeoutFuture::new(0).await;
|
||||
}
|
||||
let wall_milliseconds = js_sys::Date::now() - started;
|
||||
if let Some(profile) = submission.profile {
|
||||
while profile.state() == 0 {
|
||||
TimeoutFuture::new(0).await;
|
||||
@@ -129,10 +130,15 @@ impl RenderLoop {
|
||||
.iter()
|
||||
.filter_map(|pass| pass["milliseconds"].as_f64())
|
||||
.sum::<f64>();
|
||||
let gpu = gpu.borrow();
|
||||
let gpu = gpu.as_ref().unwrap();
|
||||
*control.profile.borrow_mut() = Some(
|
||||
serde_json::json!({
|
||||
"frame": frame,
|
||||
"milliseconds": milliseconds,
|
||||
"wallMilliseconds": wall_milliseconds,
|
||||
"adapter": gpu.adapter,
|
||||
"canvas": { "width": gpu.width, "height": gpu.height },
|
||||
"passes": passes,
|
||||
})
|
||||
.to_string(),
|
||||
|
||||
+13
-1
@@ -8,6 +8,7 @@ pub struct Wgpu {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub timestamp_queries: bool,
|
||||
pub adapter: String,
|
||||
}
|
||||
|
||||
impl Wgpu {
|
||||
@@ -27,11 +28,21 @@ impl Wgpu {
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
compatible_surface: Some(&surface),
|
||||
..Default::default()
|
||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "WEBGPU_UNAVAILABLE")?;
|
||||
let required_features = adapter.features() & wgpu::Features::TIMESTAMP_QUERY;
|
||||
let info = adapter.get_info();
|
||||
let adapter_name = if info.name.is_empty() {
|
||||
format!("{:?} · {:?}", info.device_type, info.backend)
|
||||
} else {
|
||||
format!(
|
||||
"{} · {:?} · {:?}",
|
||||
info.name, info.device_type, info.backend
|
||||
)
|
||||
};
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
required_features,
|
||||
@@ -54,6 +65,7 @@ impl Wgpu {
|
||||
width,
|
||||
height,
|
||||
timestamp_queries: required_features.contains(wgpu::Features::TIMESTAMP_QUERY),
|
||||
adapter: adapter_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ addEventListener("message", async ({ data: message }) => {
|
||||
core.switch_loadout(message.id);
|
||||
break;
|
||||
case "upload-texture":
|
||||
core.upload_texture(message.name, message.image);
|
||||
core.upload_texture(message.name, message.mipLevel, message.image);
|
||||
break;
|
||||
case "delete-texture":
|
||||
core.delete_texture(message.name);
|
||||
|
||||
Reference in New Issue
Block a user