374 lines
13 KiB
Rust
374 lines
13 KiB
Rust
use std::cell::{Cell, RefCell};
|
|
use std::rc::Rc;
|
|
|
|
use wasm_bindgen::prelude::wasm_bindgen;
|
|
|
|
use crate::gpu::Wgpu;
|
|
use crate::gpu_resource::{profiler, GpuPass, ProfileMap};
|
|
use crate::graph::{ColorAttachment, DepthAttachment};
|
|
use crate::render_data::RenderData;
|
|
use crate::store::{Loadout, Store};
|
|
|
|
#[wasm_bindgen(inline_js = r#"
|
|
const channel = new MessageChannel();
|
|
const ready = [];
|
|
channel.port1.onmessage = () => ready.shift()?.();
|
|
|
|
export function nextFrame() {
|
|
return new Promise(resolve => requestAnimationFrame(resolve));
|
|
}
|
|
|
|
export function yieldTask() {
|
|
return new Promise(resolve => {
|
|
ready.push(resolve);
|
|
channel.port2.postMessage(0);
|
|
});
|
|
}
|
|
"#)]
|
|
extern "C" {
|
|
#[wasm_bindgen(js_name = nextFrame)]
|
|
async fn next_frame();
|
|
#[wasm_bindgen(js_name = yieldTask)]
|
|
async fn yield_task();
|
|
}
|
|
|
|
pub struct RenderLoop {
|
|
playing: Cell<bool>,
|
|
fps: Cell<u32>,
|
|
started: Cell<bool>,
|
|
frame: Cell<u32>,
|
|
elapsed: Cell<f64>,
|
|
last: Cell<f64>,
|
|
active_loadout: Loadout,
|
|
profile_state: RefCell<profiler::ProfileState>,
|
|
}
|
|
|
|
impl RenderLoop {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
playing: Cell::new(true),
|
|
fps: Cell::new(0),
|
|
started: Cell::new(false),
|
|
frame: Cell::new(0),
|
|
elapsed: Cell::new(0.0),
|
|
last: Cell::new(js_sys::Date::now()),
|
|
active_loadout: todo!(),
|
|
profile_state: RefCell::new(profiler::ProfileState::Detached),
|
|
}
|
|
}
|
|
|
|
pub fn play(&self) {
|
|
self.last.set(js_sys::Date::now());
|
|
self.playing.set(true);
|
|
}
|
|
|
|
pub fn pause(&self) {
|
|
self.playing.set(false);
|
|
}
|
|
|
|
pub fn set_fps(&self, fps: u32) -> Result<(), &'static str> {
|
|
if fps > 1000 {
|
|
return Err("FPS");
|
|
}
|
|
self.fps.set(fps);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_profiling(&mut self) {
|
|
self.profile_state = RefCell::new(profiler::ProfileState::Attached);
|
|
}
|
|
|
|
pub fn take_profile(&self) -> Option<String> {
|
|
if let profiler::ProfileState::Ready(data) = self.profile_state.get_mut().take() {
|
|
Some(data)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn start(self: &Rc<Self>, gpu: &Wgpu, data: Rc<RefCell<RenderData>>) {
|
|
if self.started.replace(true) {
|
|
return;
|
|
}
|
|
let control = self.clone();
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
loop {
|
|
next_frame().await;
|
|
if !control.playing.get() {
|
|
continue;
|
|
}
|
|
|
|
let started = js_sys::Date::now();
|
|
let fps = control.fps.get();
|
|
if fps != 0 && started - control.last.get() < 900.0 / f64::from(fps) {
|
|
continue;
|
|
}
|
|
let delta = (started - control.last.replace(started)) / 1000.0;
|
|
let elapsed = control.elapsed.get() + delta;
|
|
control.elapsed.set(elapsed);
|
|
let frame = control.frame.get().wrapping_add(1);
|
|
control.frame.set(frame);
|
|
{
|
|
let mut data = data.borrow_mut();
|
|
data.update_signals(delta as f32, frame, elapsed as f32, control.fps.get());
|
|
}
|
|
|
|
// returns false when no render is pending
|
|
if !data.borrow_mut().begin_render() {
|
|
continue;
|
|
}
|
|
|
|
let should_profile = matches!(
|
|
*control.profile_state.borrow(),
|
|
profiler::ProfileState::Attached
|
|
);
|
|
|
|
gpu.render(&mut self.active_loadout, &data.borrow(), should_profile)
|
|
.and_then(|profile_metadata| {
|
|
// we are done with render loop but we got a profile data for this
|
|
// frame, so we need to process that
|
|
|
|
control.profile_state
|
|
});
|
|
|
|
if let Some((profile, adapter, width, height)) = submission {
|
|
control.profile_pending.set(true);
|
|
control.profile_after.set(started + 250.0);
|
|
let control = control.clone();
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
while profile.state() == 0 {
|
|
yield_task().await;
|
|
}
|
|
if profile.state() == 1 {
|
|
let passes = profile
|
|
.read()
|
|
.into_iter()
|
|
.map(|(name, milliseconds)| profiler::Pass { name, milliseconds })
|
|
.collect::<Vec<_>>();
|
|
let milliseconds =
|
|
passes.iter().map(|pass| pass.milliseconds).sum::<f64>();
|
|
if control.profiling.get() {
|
|
*control.profile_state.borrow_mut() = Some(
|
|
profiler::FrameData {
|
|
frame,
|
|
milliseconds,
|
|
readback_milliseconds: js_sys::Date::now() - started,
|
|
adapter,
|
|
canvas: profiler::Canvas { width, height },
|
|
passes,
|
|
}
|
|
.to_string(),
|
|
);
|
|
}
|
|
}
|
|
control.profile_pending.set(false);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
impl Wgpu {
|
|
fn render(
|
|
&mut self,
|
|
loadout: &mut Loadout,
|
|
data: &RenderData,
|
|
profile: bool,
|
|
) -> Result<ProfileMap, String> {
|
|
for buffer in loadout.resources.buffers.values() {
|
|
if !buffer.sync_each_frame {
|
|
continue;
|
|
}
|
|
self.queue.write_buffer(
|
|
&buffer.buffer,
|
|
0,
|
|
data.bytes(&buffer.source).ok_or("ROWS_UNKNOWN")?,
|
|
);
|
|
}
|
|
|
|
let output = match self.surface.get_current_texture() {
|
|
Ok(output) => output,
|
|
Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
|
|
self.surface.configure(&self.device, &self.config);
|
|
self.surface.get_current_texture().map_err(|_| "SURFACE")?
|
|
}
|
|
Err(wgpu::SurfaceError::Timeout) => return Err("SURFACE".into()),
|
|
Err(_) => return Err("SURFACE".into()),
|
|
};
|
|
|
|
let surface_view = output
|
|
.texture
|
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
|
|
|
// profiler is used only when enabled
|
|
// the presence of a profiler means:
|
|
// - profiler is available and
|
|
// - we want to profile this frame
|
|
let profiler = if profile {
|
|
loadout.resources.profiler
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let mut encoder = self
|
|
.device
|
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("frame"),
|
|
});
|
|
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 = profiler.and(|p| wgpu::ComputePassTimestampWrites {
|
|
query_set: &p.query_set,
|
|
beginning_of_pass_write_index: Some(pass_index as u32 * 2),
|
|
end_of_pass_write_index: Some(pass_index as u32 + 1),
|
|
});
|
|
|
|
let mut command = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some(&pass.id),
|
|
timestamp_writes,
|
|
});
|
|
command.set_pipeline(pipeline);
|
|
for (group, bind_group) in bind_groups {
|
|
command.set_bind_group(*group, bind_group, &[]);
|
|
}
|
|
command.dispatch_workgroups(
|
|
pass.dispatch[0],
|
|
pass.dispatch[1],
|
|
pass.dispatch[2],
|
|
);
|
|
}
|
|
GpuPass::Render {
|
|
first,
|
|
last,
|
|
bundle,
|
|
..
|
|
} => {
|
|
let pass = &loadout.graph.passes[*first];
|
|
let last = &loadout.graph.passes[*last];
|
|
let colors = pass
|
|
.color
|
|
.iter()
|
|
.zip(&last.color)
|
|
.map(|(attachment, final_attachment)| {
|
|
Ok(Some(wgpu::RenderPassColorAttachment {
|
|
view: view(
|
|
&loadout.resources,
|
|
&surface_view,
|
|
&attachment.resource,
|
|
)?,
|
|
depth_slice: None,
|
|
resolve_target: None,
|
|
ops: color_ops(attachment, final_attachment)?,
|
|
}))
|
|
})
|
|
.collect::<Result<Vec<_>, String>>()?;
|
|
let depth = pass
|
|
.depth
|
|
.as_ref()
|
|
.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, final_attachment)?),
|
|
stencil_ops: None,
|
|
})
|
|
})
|
|
.transpose()?;
|
|
|
|
let timestamp_writes = profiler.and(|p| wgpu::RenderPassTimestampWrites {
|
|
query_set: &p.query_set,
|
|
beginning_of_pass_write_index: Some(pass_index as u32 * 2),
|
|
end_of_pass_write_index: Some(pass_index as u32 * 2 + 1),
|
|
});
|
|
|
|
let mut command = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
label: Some(&pass.id),
|
|
color_attachments: &colors,
|
|
depth_stencil_attachment: depth,
|
|
timestamp_writes,
|
|
occlusion_query_set: None,
|
|
});
|
|
command.execute_bundles([bundle]);
|
|
}
|
|
}
|
|
}
|
|
|
|
profiler.as_ref().map(|p| p.encode_query_set(&mut encoder));
|
|
self.queue.submit([encoder.finish()]);
|
|
|
|
output.present();
|
|
|
|
profiler
|
|
.and_then(|p| p.map_profile(self.queue.get_timestamp_period()))
|
|
.flatten();
|
|
}
|
|
}
|
|
|
|
fn view<'a>(
|
|
resources: &'a crate::gpu_resource::GpuResources,
|
|
surface: &'a wgpu::TextureView,
|
|
id: &str,
|
|
) -> Result<&'a wgpu::TextureView, String> {
|
|
if id == "canvas" {
|
|
Ok(surface)
|
|
} else {
|
|
resources
|
|
.texture_view(id)
|
|
.ok_or_else(|| "GRAPH_ATTACHMENT".into())
|
|
}
|
|
}
|
|
|
|
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() {
|
|
"load" => wgpu::LoadOp::Load,
|
|
"clear" => wgpu::LoadOp::Clear(wgpu::Color {
|
|
r: f64::from(clear.first().copied().unwrap_or(0.0)),
|
|
g: f64::from(clear.get(1).copied().unwrap_or(0.0)),
|
|
b: f64::from(clear.get(2).copied().unwrap_or(0.0)),
|
|
a: f64::from(clear.get(3).copied().unwrap_or(1.0)),
|
|
}),
|
|
_ => return Err("GRAPH_LOAD_OP".into()),
|
|
},
|
|
store: store_op(&final_attachment.store)?,
|
|
})
|
|
}
|
|
|
|
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(&final_attachment.store)?,
|
|
})
|
|
}
|
|
|
|
fn store_op(value: &str) -> Result<wgpu::StoreOp, String> {
|
|
match value {
|
|
"store" => Ok(wgpu::StoreOp::Store),
|
|
"discard" => Ok(wgpu::StoreOp::Discard),
|
|
_ => Err("GRAPH_STORE_OP".into()),
|
|
}
|
|
}
|