├── .gitignore ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── Cargo.toml ├── src └── lib.rs ├── README.md └── examples └── states.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | bevy_pipelines_ready is dual-licensed under either 2 | 3 | * MIT License (docs/LICENSE-MIT or ) 4 | * Apache License, Version 2.0 (docs/LICENSE-APACHE or ) 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: dtolnay/rust-toolchain@stable 10 | with: 11 | components: rustfmt, clippy 12 | - name: Install alsa and udev 13 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev 14 | - name: check 15 | run: cargo check 16 | - name: examples 17 | run: cargo check --examples 18 | - name: fmt 19 | run: cargo fmt --all -- --check 20 | - name: clippy 21 | run: cargo clippy --all-targets --all-features -- -Dwarnings 22 | - name: tests 23 | run: cargo test --all-features 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_pipelines_ready" 3 | version = "0.7.0" 4 | edition = "2024" 5 | license = "MIT OR Apache-2.0" 6 | description = "Bevy plugin for tracking render pipeline status." 7 | repository = "https://github.com/rparrett/bevy_pipelines_ready" 8 | homepage = "https://github.com/rparrett/bevy_pipelines_ready" 9 | documentation = "https://docs.rs/bevy_pipelines_ready" 10 | keywords = ["bevy", "gamedev"] 11 | categories = ["game-development"] 12 | readme = "README.md" 13 | exclude = [".github"] 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [features] 18 | webgl2 = ["bevy/webgl2"] 19 | webgpu = ["bevy/webgpu"] 20 | 21 | [dependencies.bevy] 22 | version = "0.17.0" 23 | default-features = false 24 | features = ["bevy_render"] 25 | 26 | [dev-dependencies.bevy] 27 | version = "0.17.0" 28 | default-features = true 29 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | prelude::*, 3 | render::{ 4 | MainWorld, RenderApp, 5 | render_resource::{CachedPipelineState, PipelineCache}, 6 | }, 7 | }; 8 | 9 | pub struct PipelinesReadyPlugin; 10 | 11 | /// A `Resource` in the main world that stores the number of pipelines that are ready. 12 | #[derive(Resource, Default, Debug)] 13 | pub struct PipelinesReady(usize); 14 | impl PipelinesReady { 15 | /// Returns the number of pipelines that are ready. 16 | pub fn get(&self) -> usize { 17 | self.0 18 | } 19 | } 20 | 21 | impl Plugin for PipelinesReadyPlugin { 22 | fn build(&self, app: &mut App) { 23 | app.init_resource::(); 24 | 25 | app.sub_app_mut(RenderApp) 26 | .add_systems(ExtractSchedule, update_pipelines_ready); 27 | } 28 | } 29 | 30 | fn update_pipelines_ready(mut main_world: ResMut, cache: Res) { 31 | if let Some(mut pipelines_ready) = main_world.get_resource_mut::() { 32 | let count = cache 33 | .pipelines() 34 | .filter(|pipeline| matches!(pipeline.state, CachedPipelineState::Ok(_))) 35 | .count(); 36 | 37 | if pipelines_ready.0 == count { 38 | return; 39 | } 40 | 41 | pipelines_ready.0 = count; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_pipelines_ready 2 | 3 | [![crates.io](https://img.shields.io/crates/v/bevy_pipelines_ready.svg)](https://crates.io/crates/bevy_pipelines_ready) 4 | [![docs](https://docs.rs/bevy_pipelines_ready/badge.svg)](https://docs.rs/bevy_pipelines_ready) 5 | [![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking) 6 | 7 | A tiny Bevy plugin that counts the number of render pipelines that are ready and makes that data available as a resource in the main world. 8 | 9 | Bevy's shaders are compiled on-demand. On native platforms, they are compiled asynchronously, which can create pop-in effects. On the web, they are compiled synchronously in the single-threaded environment, which can cause hitches and can be pretty disastrous if music is playing. 10 | 11 | This plugin can help you get your shaders compiled during a loading state where it won't cause as much trouble for your players. 12 | 13 | ## Usage 14 | 15 | See [`examples/states.rs`](examples/states.rs). 16 | 17 | ## WebGL2 and WebGPU 18 | 19 | ### Build 20 | 21 | ```bash 22 | # WebGL 23 | cargo build --target wasm32-unknown-unknown --example "states" --features="webgl2" 24 | 25 | # WebGPU 26 | cargo build --target wasm32-unknown-unknown --example "states" --features="webgpu" 27 | ``` 28 | 29 | ### Bindgen 30 | 31 | ```bash 32 | mkdir -p examples/wasm/target 33 | wget https://raw.githubusercontent.com/bevyengine/bevy/refs/tags/v0.15.0/examples/wasm/index.html -O examples/wasm/index.html 34 | wasm-bindgen --out-dir examples/wasm/target --out-name wasm_example --target web target/wasm32-unknown-unknown/debug/examples/states.wasm 35 | ``` 36 | 37 | ### Serve 38 | 39 | ```bash 40 | basic-http-server examples/wasm/ 41 | ``` 42 | 43 | ## Compatibility 44 | 45 | | `bevy_pipelines_ready` | `bevy` | 46 | | :-- | :-- | 47 | | `0.7` | `0.17` | 48 | | `0.6` | `0.16` | 49 | | `0.5` | `0.15` | 50 | | `0.4` | `0.14` | 51 | | `0.3` | `0.13` | 52 | | `0.2` | `0.12` | 53 | | `0.1` | `0.11` | 54 | 55 | ## Contributing 56 | 57 | Please feel free to open a PR. 58 | -------------------------------------------------------------------------------- /examples/states.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_pipelines_ready::{PipelinesReady, PipelinesReadyPlugin}; 3 | 4 | #[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)] 5 | enum GameState { 6 | #[default] 7 | Loading, 8 | Ready, 9 | } 10 | 11 | // This value should be found experimentally by logging `PipelinesReady` in your app 12 | // during normal use and noting the maximum value. 13 | #[cfg(not(target_arch = "wasm32"))] 14 | const EXPECTED_PIPELINES: usize = 35; 15 | // The value will likely differ on the web due to different implementations of some 16 | // render features. 17 | #[cfg(all(target_arch = "wasm32", feature = "webgpu", not(feature = "webgl2")))] 18 | const EXPECTED_PIPELINES: usize = 20; 19 | // Note: These features can be simplified if your app only builds for one of either 20 | // webgpu or webgl2. Simply use `#[cfg(target_arch = "wasm32")]`. If your app builds 21 | // for both, you must add these features (or similar) to your app. See `Cargo.toml`. 22 | #[cfg(all(target_arch = "wasm32", feature = "webgl2", not(feature = "webgpu")))] 23 | const EXPECTED_PIPELINES: usize = 6; 24 | 25 | // You may want to wait an additional period of time or number of frames. 26 | // 27 | // In my experience, Bevy's framerate seems to take a few seconds to fully recover after 28 | // pipelines are compiled when running in Firefox. 29 | const ADDITIONAL_WAIT_FRAMES: u32 = 10; 30 | 31 | fn main() { 32 | App::new() 33 | .add_plugins(DefaultPlugins) 34 | .add_plugins(PipelinesReadyPlugin) 35 | .init_state::() 36 | .add_systems(Startup, setup_loading_screen) 37 | .add_systems(Update, print.run_if(resource_changed::)) 38 | .add_systems(Update, transition.run_if(in_state(GameState::Loading))) 39 | .run(); 40 | } 41 | 42 | // Your loading screen should include all of the cameras, lights, and other elements that cause 43 | // pipelines to be built in your app. 44 | fn setup_loading_screen( 45 | mut commands: Commands, 46 | mut meshes: ResMut>, 47 | mut materials: ResMut>, 48 | ) { 49 | commands.spawn(( 50 | Text::new("Pipelines loading...".to_string()), 51 | DespawnOnExit(GameState::Loading), 52 | )); 53 | 54 | commands.spawn(( 55 | PointLight { 56 | shadows_enabled: true, 57 | intensity: 1_000_000., 58 | ..default() 59 | }, 60 | Transform::from_xyz(3.0, 6.0, 5.0), 61 | )); 62 | 63 | commands.spawn(( 64 | Mesh3d(meshes.add(Cylinder::default())), 65 | MeshMaterial3d(materials.add(Color::from(bevy::color::palettes::tailwind::PINK_500))), 66 | )); 67 | 68 | commands.spawn(( 69 | Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))), 70 | MeshMaterial3d(materials.add(Color::from(Srgba::gray(0.6)))), 71 | Transform::from_xyz(0., -0.5, 0.), 72 | )); 73 | 74 | commands.spawn(( 75 | Camera3d::default(), 76 | Transform::from_xyz(0.0, 1.5, 3.0).looking_at(Vec3::ZERO, Vec3::Y), 77 | )); 78 | } 79 | 80 | fn print(ready: Res) { 81 | info!("Pipelines Ready: {}/{}", ready.get(), EXPECTED_PIPELINES); 82 | } 83 | 84 | fn transition( 85 | ready: Res, 86 | mut next_state: ResMut>, 87 | mut frames_since_pipelines_ready: Local, 88 | ) { 89 | if ready.get() < EXPECTED_PIPELINES { 90 | return; 91 | } 92 | 93 | *frames_since_pipelines_ready += 1; 94 | 95 | if *frames_since_pipelines_ready < ADDITIONAL_WAIT_FRAMES { 96 | return; 97 | } 98 | 99 | next_state.set(GameState::Ready); 100 | } 101 | --------------------------------------------------------------------------------