mod messages;
mod pacing;
pub(crate) use messages::{RenderCommand, RenderFeedback, RenderSnapshot};
use crate::{
app::timing::BeatState,
audio::sound_data::SoundData,
pipeline::{extract_output::ExtractOutput, gpu::OutputContext},
storage::{
asset::{project::Project, scene::effect_state::EffectState},
collections::Collections,
},
};
use kanal::{Receiver, Sender, bounded, unbounded};
use pacing::{FramePacer, FrameStats};
use rusty_link::AblLink;
use std::{
collections::{HashMap, VecDeque},
sync::Arc,
time::Duration,
};
use tracing::{debug, error, trace, warn};
use uuid::Uuid;
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> {
let context = OutputContext::new(adapter)?;
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::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,
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>,
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_mut() {
project.ensure_output_pipelines(&self.context, &self.collections);
}
true
}
fn apply_command(&mut self, command: RenderCommand) {
let Some(project) = self.project.as_mut() else {
return;
};
match command {
RenderCommand::ReloadShaderCode(animation) => {
self.collections.update();
project.invalidate_pipelines(animation);
}
RenderCommand::SendPositions => project.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())
);
}
let mut states = self.take_effect_states();
self.project = project;
let Some(project) = self.project.as_mut() else {
return;
};
for scene_instance in project.scenes_instances_grid.values_mut() {
for (index, effect) in scene_instance.scene.effects.iter_mut().enumerate() {
if let Some(state) = states.remove(&(scene_instance.id, index)) {
effect.state = state;
}
}
}
}
fn take_effect_states(&mut self) -> HashMap<(Uuid, usize), EffectState> {
let Some(project) = self.project.as_mut() else {
return HashMap::new();
};
project
.scenes_instances_grid
.values_mut()
.flat_map(|scene_instance| {
let id = scene_instance.id;
scene_instance
.scene
.effects
.iter_mut()
.enumerate()
.map(move |(index, effect)| ((id, index), std::mem::take(&mut effect.state)))
})
.collect()
}
#[cfg_attr(feature = "profiling", profiling::function)]
fn frame(&mut self) {
#[cfg(feature = "profiling")]
{
crate::wgpu_profiler()
.end_frame()
.expect("Could not end WGPU profiler frame");
let latest_profiler_results = crate::wgpu_profiler()
.process_finished_frame(self.context.gpu.queue.get_timestamp_period());
wgpu_profiler::puffin::output_frame_to_puffin(
&mut crate::PUFFIN_GPU_PROFILER.lock(),
latest_profiler_results.as_deref().unwrap_or_default(),
);
crate::PUFFIN_GPU_PROFILER.lock().new_frame();
}
let beat_state = BeatState::sample(&self.link, self.framerate);
if let Some(project) = self.project.as_mut() {
let submission = project.render_output(
&self.context,
&beat_state,
self.blackout,
&self.collections,
&mut self.extract_output,
&self.sound_data,
);
self.limit_frames_in_flight(submission);
}
if let Some(fps) = self.stats.record_frame() {
self.framerate = fps;
self.send_feedback(RenderFeedback::FrameStats { fps });
}
}
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}");
}
}
}