mod messages;
mod pacing;
pub(crate) use messages::{RenderCommand, RenderFeedback, RenderSnapshot};
use crate::{
audio::sound_data::SoundData,
pipeline::{CurrentLayout, extract_output::ExtractOutput, output::render_output},
runtime::timing::sample_beat_state,
storage::{asset::project::Project, collections::Collections},
};
use gled_gpu::{
effect_state::EffectPipelines,
gpu::OutputContext,
scene_render::{EvalContext, ensure_project_output_pipelines},
};
use kanal::{Receiver, Sender, bounded, unbounded};
use pacing::{FramePacer, FrameStats};
use rusty_link::AblLink;
use std::{collections::VecDeque, sync::Arc, time::Duration};
use tracing::{debug, error, trace, warn};
use wgpu::{Adapter, PollType, SubmissionIndex};
const SNAPSHOT_CHANNEL_CAPACITY: usize = 4;
const MAX_FRAMES_IN_FLIGHT: usize = 2;
const IDLE_FPS: f32 = 60.0;
const GPU_WAIT_TIMEOUT: Duration = Duration::from_secs(1);
#[derive(Debug)]
pub(crate) struct RenderThread {
snapshots: Sender<RenderSnapshot>,
commands: Sender<RenderCommand>,
feedback: Receiver<RenderFeedback>,
}
impl RenderThread {
pub(crate) fn spawn(
adapter: &Adapter,
link: Arc<AblLink>,
extract_output: ExtractOutput,
fps_limit: f32,
) -> anyhow::Result<Self> {
#[cfg(feature = "profiling")]
let features = wgpu_profiler::GpuProfiler::ALL_WGPU_TIMER_FEATURES;
#[cfg(not(feature = "profiling"))]
let features = wgpu::Features::empty();
let context = OutputContext::new(adapter, features)?;
let (snapshot_sender, snapshot_receiver) = bounded(SNAPSHOT_CHANNEL_CAPACITY);
let (command_sender, command_receiver) = unbounded();
let (feedback_sender, feedback_receiver) = unbounded();
std::thread::Builder::new()
.name("gled:render".to_owned())
.spawn(move || {
#[cfg(feature = "profiling")]
{
profiling::register_thread!("render");
crate::runtime::init_wgpu_profiler(&context.gpu.device);
}
RenderLoop {
snapshots: snapshot_receiver,
commands: command_receiver,
feedback: feedback_sender,
context,
link,
collections: Collections::default(),
sound_data: SoundData::default(),
extract_output,
project: None,
pipelines: EffectPipelines::default(),
blackout: true,
fps_limit,
framerate: 0.0,
pacer: FramePacer::new(),
stats: FrameStats::new(),
in_flight: VecDeque::with_capacity(MAX_FRAMES_IN_FLIGHT + 1),
}
.run();
})?;
Ok(Self {
snapshots: snapshot_sender,
commands: command_sender,
feedback: feedback_receiver,
})
}
pub(crate) fn send_snapshot(&self, snapshot: RenderSnapshot) {
match self.snapshots.try_send(snapshot) {
Ok(true) => {}
Ok(false) => trace!("Render thread is busy, dropped project snapshot"),
Err(err) => error!("Render thread is gone, could not send snapshot: {err}"),
}
}
pub(crate) fn send_command(&self, command: RenderCommand) {
if let Err(err) = self.commands.send(command) {
error!("Render thread is gone, could not send command: {err}");
}
}
pub(crate) fn try_recv_feedback(&self) -> Option<RenderFeedback> {
self.feedback.try_recv().ok().flatten()
}
}
struct RenderLoop {
snapshots: Receiver<RenderSnapshot>,
commands: Receiver<RenderCommand>,
feedback: Sender<RenderFeedback>,
context: OutputContext,
link: Arc<AblLink>,
collections: Collections,
sound_data: SoundData,
extract_output: ExtractOutput,
project: Option<Project>,
pipelines: EffectPipelines,
blackout: bool,
fps_limit: f32,
framerate: f32,
pacer: FramePacer,
stats: FrameStats,
in_flight: VecDeque<SubmissionIndex>,
}
impl RenderLoop {
fn run(mut self) {
loop {
let fps_limit = if self.project.is_some() {
self.fps_limit
} else {
IDLE_FPS
};
self.pacer.wait_for_next_frame(fps_limit);
self.collections.update();
self.sound_data.update();
if !self.receive() {
debug!("UI side of the render thread was dropped, stopping");
return;
}
self.frame();
}
}
fn receive(&mut self) -> bool {
let mut changed = false;
loop {
match self.commands.try_recv() {
Ok(Some(command)) => {
self.apply_command(command);
changed = true;
}
Ok(None) => break,
Err(_) => return false,
}
}
let mut latest = None;
loop {
match self.snapshots.try_recv() {
Ok(Some(snapshot)) => latest = Some(snapshot),
Ok(None) => break,
Err(_) => return false,
}
}
if let Some(snapshot) = latest {
self.merge_snapshot(snapshot);
changed = true;
}
if changed && let Some(project) = self.project.as_ref() {
ensure_project_output_pipelines(
project,
&mut self.pipelines,
&self.context,
&self.collections,
);
}
true
}
fn apply_command(&mut self, command: RenderCommand) {
match command {
RenderCommand::ReloadShaderCode(animation) => {
self.collections.update();
self.pipelines.invalidate(animation);
}
RenderCommand::SendPositions => self.pipelines.send_positions(),
}
}
#[cfg_attr(feature = "profiling", profiling::function)]
fn merge_snapshot(&mut self, snapshot: RenderSnapshot) {
let RenderSnapshot {
project,
blackout,
output_config,
fps_limit,
} = snapshot;
self.blackout = blackout;
self.fps_limit = fps_limit;
self.extract_output.config = output_config;
if self.project.is_some() != project.is_some() {
debug!(
"Render thread project changed: {} scene instances",
project
.as_ref()
.map_or(0, |project| project.scenes_instances_grid.len())
);
}
if project.is_none() {
self.pipelines = EffectPipelines::default();
}
self.project = project;
}
#[cfg_attr(feature = "profiling", profiling::function)]
fn frame(&mut self) {
#[cfg(feature = "profiling")]
{
crate::runtime::wgpu_profiler()
.end_frame()
.expect("Could not end WGPU profiler frame");
let latest_profiler_results = crate::runtime::wgpu_profiler()
.process_finished_frame(self.context.gpu.queue.get_timestamp_period());
wgpu_profiler::puffin::output_frame_to_puffin(
&mut crate::runtime::PUFFIN_GPU_PROFILER.lock(),
latest_profiler_results.as_deref().unwrap_or_default(),
);
crate::runtime::PUFFIN_GPU_PROFILER.lock().new_frame();
}
let beat_state = sample_beat_state(&self.link, self.framerate);
if let Some(project) = self.project.as_ref() {
let submission = render_output(
project,
&mut self.pipelines,
&self.context,
&beat_state,
self.blackout,
EvalContext {
layout: &CurrentLayout,
collections: &self.collections,
sound: &self.sound_data,
},
&mut self.extract_output,
);
self.limit_frames_in_flight(submission);
}
if let Some(stats) = self.stats.record_frame() {
self.framerate = stats.fps;
self.send_feedback(RenderFeedback::FrameStats(stats));
}
}
fn limit_frames_in_flight(&mut self, submission: SubmissionIndex) {
self.in_flight.push_back(submission);
while self.in_flight.len() > MAX_FRAMES_IN_FLIGHT {
let Some(oldest) = self.in_flight.pop_front() else {
return;
};
if let Err(err) = self.context.gpu.device.poll(PollType::Wait {
submission_index: Some(oldest),
timeout: Some(GPU_WAIT_TIMEOUT),
}) {
warn!("Waiting for an output frame on the GPU failed: {err}");
}
}
}
fn send_feedback(&self, feedback: RenderFeedback) {
if let Err(err) = self.feedback.send(feedback) {
trace!("UI side of the render thread is gone: {err}");
}
}
}