gigs/
lib.rs

1//! `gigs`: on-demand graphics jobs for `bevy`
2//!
3//! Gigs is a plugin for the Bevy game engine that aims to provide a simple
4//! abstraction for "graphics jobs", units of rendering work that only need to be
5//! done sporadically, on-demand. For example, a terrain generation compute shader
6//! would only need to be run once for each chunk of terrain. In many cases, this
7//! crate will allow you to skip most or all of the manual extraction and resource
8//! prep boilerplate that comes along with this, and focus on writing shaders.
9//!
10//! Getting started:
11//!
12//! 1. First, add `gigs` to your Cargo dependencies: `cargo add gigs`
13//! 2. Add `GraphicsJobsPlugin` to your `App`
14//! 3. Implement `GraphicsJob` for your job component
15//! 4. Call `init_graphics_job` on `App` to initialize your custom job
16//! 5. To run the job, simply spawn an entity with your job component!
17//!
18//! See the examples in the repo for more in-depth showcases!
19
20#![allow(clippy::type_complexity)]
21
22mod ext;
23pub mod input;
24pub mod meta;
25mod runner;
26use disqualified::ShortName;
27pub use ext::*;
28use input::{JobInput, JobInputItem};
29use meta::{extract_job_meta, JobMarker};
30use runner::{
31    check_job_inputs, erase_jobs, increment_time_out_frames, run_jobs, setup_time_out_frames,
32    sync_completed_jobs, sync_completed_jobs_main_world, time_out_jobs, JobResultMainWorldReceiver,
33    JobResultMainWorldSender, JobResultReceiver, JobResultSender, JobSet,
34};
35
36use core::marker::PhantomData;
37
38use bevy_app::{App, Plugin, Update};
39use bevy_ecs::{
40    component::Component,
41    event::Event,
42    query::Added,
43    schedule::{IntoSystemConfigs, IntoSystemSetConfigs},
44    system::{Commands, Query, Resource},
45    world::World,
46};
47use bevy_render::{
48    extract_resource::{ExtractResource, ExtractResourcePlugin},
49    render_resource::CommandEncoder,
50    renderer::RenderDevice,
51    sync_component::SyncComponentPlugin,
52    ExtractSchedule, Render, RenderApp, RenderSet,
53};
54use bevy_render::{sync_world::RenderEntity, Extract};
55
56/// A trait for components describing a unit of rendering work.
57///
58/// When a [`Component`] implementing this trait is added to the [`World`],
59/// it is extracted to the render world, where it waits for its inputs to be
60/// prepared. When they are ready, it will execute and the commands it encodes
61/// will be submitted before the render graph is executed.
62///
63/// You can also specify a priority for a running job by adding the [`JobPriority`]
64/// component when it is spawned.
65///
66/// Note: you must call [`init_graphics_job`](crate::ext::InitGraphicsJobExt::init_graphics_job)
67/// on [`App`] for the job to execute.
68pub trait GraphicsJob: Component + Clone {
69    type In: JobInput<Self>;
70
71    fn label() -> ShortName<'static> {
72        ShortName::of::<Self>()
73    }
74
75    fn run(
76        &self,
77        world: &World,
78        render_device: &RenderDevice,
79        command_encoder: &mut CommandEncoder,
80        input: JobInputItem<Self, Self::In>,
81    ) -> Result<(), JobError>;
82}
83
84/// The main plugin for `gigs`. This plugin is needed for all functionality.
85#[derive(Default)]
86pub struct GraphicsJobsPlugin {
87    settings: JobExecutionSettings,
88}
89
90impl Plugin for GraphicsJobsPlugin {
91    fn build(&self, app: &mut App) {
92        app.insert_resource(self.settings);
93
94        app.add_plugins((
95            SyncComponentPlugin::<JobMarker>::default(),
96            ExtractResourcePlugin::<JobExecutionSettings>::default(),
97        ));
98
99        let (main_sender, main_receiver) = crossbeam_channel::unbounded();
100
101        app.insert_resource(JobResultMainWorldReceiver(main_receiver))
102            .add_systems(Update, sync_completed_jobs_main_world);
103
104        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
105            let (sender, receiver) = crossbeam_channel::unbounded();
106            render_app
107                .insert_resource(JobResultSender(sender))
108                .insert_resource(JobResultReceiver(receiver))
109                .insert_resource(JobResultMainWorldSender(main_sender));
110
111            render_app.add_systems(ExtractSchedule, extract_job_meta);
112
113            render_app.configure_sets(
114                Render,
115                (
116                    JobSet::Setup,
117                    JobSet::Check,
118                    JobSet::Execute,
119                    JobSet::Cleanup,
120                )
121                    .chain(),
122            );
123
124            render_app.configure_sets(
125                Render,
126                (
127                    JobSet::Check.after(RenderSet::Prepare),
128                    JobSet::Execute.before(RenderSet::Render),
129                    JobSet::Cleanup.in_set(RenderSet::Cleanup),
130                ),
131            );
132
133            render_app.add_systems(
134                Render,
135                (
136                    setup_time_out_frames.in_set(JobSet::Setup),
137                    check_job_inputs.in_set(JobSet::Check),
138                    time_out_jobs.in_set(JobSet::Check),
139                    run_jobs.in_set(JobSet::Execute),
140                    increment_time_out_frames.in_set(JobSet::Cleanup),
141                    sync_completed_jobs.in_set(JobSet::Cleanup),
142                ),
143            );
144        }
145    }
146}
147
148/// Settings for how jobs are scheduled each frame
149#[derive(Copy, Clone, Resource, ExtractResource)]
150pub struct JobExecutionSettings {
151    /// The maximum number of jobs to execute each frame. This number
152    /// may be exceeded in the case that a large number of jobs are
153    /// queued with [`Priority::Critical`].
154    pub max_jobs_per_frame: u32,
155    /// The maximum number of frames a job should wait to execute
156    /// before timing out.
157    pub time_out_frames: u32,
158}
159
160impl Default for JobExecutionSettings {
161    fn default() -> Self {
162        Self {
163            max_jobs_per_frame: 16,
164            time_out_frames: 16,
165        }
166    }
167}
168
169/// A plugin that sets up logic for a specific implementation of [`GraphicsJob`].
170/// It's recommended to call [`init_graphics_job`](crate::ext::InitGraphicsJobExt::init_graphics_job)
171/// on [`App`] rather than add this plugin manually.
172pub struct SpecializedGraphicsJobPlugin<J: GraphicsJob>(PhantomData<J>);
173
174impl<J: GraphicsJob> Default for SpecializedGraphicsJobPlugin<J> {
175    fn default() -> Self {
176        Self(PhantomData)
177    }
178}
179
180impl<J: GraphicsJob> Plugin for SpecializedGraphicsJobPlugin<J> {
181    fn build(&self, app: &mut App) {
182        app.add_plugins(<J as GraphicsJob>::In::plugin());
183
184        app.register_required_components::<J, JobMarker>();
185
186        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
187            render_app
188                .add_systems(ExtractSchedule, extract_jobs::<J>)
189                .add_systems(Render, erase_jobs::<J>.in_set(JobSet::Setup));
190        }
191    }
192}
193
194/// An event signaling a completed (or failed) graphics job.
195#[derive(Event, Copy, Clone, Debug)]
196pub struct JobComplete(pub Result<(), JobError>);
197
198/// Describes how an incomplete job may have failed.
199#[derive(Copy, Clone, Debug)]
200pub enum JobError {
201    /// Signals a job that failed due to timing out, either
202    /// because its needed resources were not ready in time,
203    /// or because too many jobs were scheduled ahead of it.
204    TimedOut,
205    /// Signals a job that failed because its inputs were
206    /// unable to be satisfied, for example if a needed
207    /// extra component was not provided by the user.
208    InputsFailed,
209    /// Signals a job that failed during execution.
210    ExecutionFailed,
211}
212
213fn extract_jobs<J: GraphicsJob>(
214    jobs: Extract<Query<(RenderEntity, &J), Added<JobMarker>>>,
215    mut commands: Commands,
216) {
217    let cloned_jobs = jobs
218        .iter()
219        .map(|(entity, job)| (entity, job.clone()))
220        .collect::<Vec<_>>();
221    commands.insert_batch(cloned_jobs);
222}