diff --git a/.gitignore b/.gitignore index 26f1cd1..2d616c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build .DS_Store -src/.DS_Store \ No newline at end of file +src/.DS_Store +.idea diff --git a/books/en_US/src/c03-04-batch-rendering.md b/books/en_US/src/c03-04-batch-rendering.md index 614497c..25346b1 100644 --- a/books/en_US/src/c03-04-batch-rendering.md +++ b/books/en_US/src/c03-04-batch-rendering.md @@ -11,13 +11,13 @@ For 1 luckily ggez provides a way to get the fps - see [here](https://docs.rs/gg ```rust // rendering_system.rs -{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:66}} +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:76}} ... -{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:114:118}} +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:124:128}} ... -{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:123}} +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:133}} ``` Run the game and move around with the keys a bit and you will see the FPS drops quite significantly from the expected 60. For me it looks to be in the range of 20-30 but depending on your machine it might be more or less. @@ -27,9 +27,11 @@ Run the game and move around with the keys a bit and you will see the FPS drops ## What is causing the FPS drop? Now you might be asking yourself, what have we done to make this so low? We have a fairly simple game and our logic for input and movement is not actually that complex, we also don't have that many entities or components to warrant such a big FPS drop. Well, to understand this we need to go a bit deeper into how our current rendering system works. -Currently, for every renderable entity, we figure out which image to render and we render it. This means that if we have 20 floor tiles we will load the floor image 20 times and issue 20 separate rendering calls. This is too expensive and it's the cause for our massive FPS drop. +Currently, for every renderable entity, we figure out which image to render, then load it and render it. This means that if we have 20 floor tiles we will load the floor image 20 times and issue 20 separate rendering calls. This is too expensive and it's the cause for our massive FPS drop. -How can we fix this? Well, we can use a technique called batch rendering. With this technique, what we have to do is only load the image once, and tell ggez to render it in all the 20 positions where it needs to be rendered. This way we not only load the image once, but we also only call render once per image, which will speed things up significantly. As a side note, some engines will do this render batching under the hood for you, but ggez doesn't, hence why we need to care. +How can we fix this? Well, we can use a technique called batch rendering. With this technique, what we have to do is only load the image once, and tell ggez to render it in all the 20 positions where it needs to be rendered. This way we not only load the image once, but we also only call render once per image, which will speed things up significantly. As a side note, some engines will do this render batching under the hood for you, but ggez doesn't, hence why we need to care. + +A smaller improvement is to introduce image caching on top of batch rendering. This way our game will only load each image once in the first rendering cycle. In the consecutive rendering cycles, a clone of the cached image will be rendered instead of a freshly loaded one, which gives our game an additional performance boost. But first, let's implement batch rendering. ## Batch rendering Here is what we'll have to do to implement batch rendering: @@ -55,7 +57,7 @@ Now, remember that get_image function we wrote in the Animations chapter to figu ```rust // rendering_system.rs -{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:36:53}} +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:37:54}} ``` Now let's figure out the format we want our batched data to be in. We will use a `HashMap>>` where: @@ -67,31 +69,99 @@ Let's now write the code to populate the rendering_batches hash map. ```rust // rendering_system.rs -{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:66}} +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:76}} ... -{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:72:94}} +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:82:104}} ... -{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:123}} +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:133}} ``` Finally, let's actually render the batches. We will not be able to use the draw(image) function we used before but luckily ggez has a batching API - [SpriteBatch](https://docs.rs/ggez/0.5.1/ggez/graphics/spritebatch/struct.SpriteBatch.html). Also note the `sorted_by` here, that is provided to us to itertools. ```rust // rendering_system.rs -{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:66}} +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:76}} + ... + +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:106:122}} + + ... +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:133}} +``` + +And that's it for batch rendering! Run the game again and you should see a significant FPS improvement. If you are not yet at 60 FPS, don't worry, we are going to do one more performance improvement. + + +## Image caching + +On top of batch rendering, we can boost our game performance by introducing a cache for the images. The idea is to keep the loaded images in memory, so we don't have to access and read the image files in every rendering cycle. Since we create a new rendering system in every cycle, the cache will have to live in the `Game` struct. + +We are going to: +* add a `HashMap` called `image_cache` to the `Game` struct +* pass a mutable reference of the cache to the rendering system - it needs to be mutable since the rendering system will fill the cache on the fly +* add a new function in the rendering system to use the image cache - it will take an image path and check the cache for an existing image under that path. Either we return a clone of the cached image, or we load the image, add it to the cache, and then return it. + +Let's introduce an initially empty image cache to our game and pass it on to the rendering system: +```rust +// main.rs +{{#include ../../../code/rust-sokoban-c03-04/src/main.rs:25:28}} +``` + +```rust +// main.rs +{{#include ../../../code/rust-sokoban-c03-04/src/main.rs:30}} ... -{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:96:112}} +{{#include ../../../code/rust-sokoban-c03-04/src/main.rs:59:67}} + ... +{{#include ../../../code/rust-sokoban-c03-04/src/main.rs:81}} +``` + +```rust +// main.rs +{{#include ../../../code/rust-sokoban-c03-04/src/main.rs:100}} ... -{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:123}} + +{{#include ../../../code/rust-sokoban-c03-04/src/main.rs:115:119}} +``` + +To actually use the cache for rendering we now need to adjust the rendering system: +```rust +// rendering_system.rs +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:15:18}} +``` + +```rust +// rendering_system.rs +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:20}} + ... + +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:56:64}} +``` + +```rust +// rendering_system.rs +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:76}} + ... + +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:106:107}} + ... + +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:111:112}} + ... + +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:121:122}} + ... + +{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:133}} ``` -And that's it! Run the game again and you should see a shiny 60FPS and everything should feel much smoother! +If you run the game again, you should see a shiny 60FPS and everything should feel much smoother! Have fun playing! -![low fps](./images/high_fps.png) +![high fps](./images/high_fps.png) > **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c03-04). diff --git a/code/rust-sokoban-c03-04/src/main.rs b/code/rust-sokoban-c03-04/src/main.rs index e1dfb23..cb18375 100644 --- a/code/rust-sokoban-c03-04/src/main.rs +++ b/code/rust-sokoban-c03-04/src/main.rs @@ -1,8 +1,10 @@ use ggez; use ggez::event::KeyCode; use ggez::event::KeyMods; +use ggez::graphics::Image; use ggez::{conf, event, timer, Context, GameResult}; use specs::{RunNow, World, WorldExt}; +use std::collections::HashMap; use std::path; mod audio; @@ -22,6 +24,7 @@ use crate::systems::*; struct Game { world: World, + image_cache: HashMap, } impl event::EventHandler for Game { @@ -56,7 +59,7 @@ impl event::EventHandler for Game { fn draw(&mut self, context: &mut Context) -> GameResult { // Render game entities { - let mut rs = RenderingSystem { context }; + let mut rs = RenderingSystem { context, image_cache: &mut self.image_cache }; rs.run_now(&self.world); } @@ -110,7 +113,7 @@ pub fn main() -> GameResult { initialize_sounds(&mut world, context); // Create the game state - let game = &mut Game { world }; + let game = &mut Game { world, image_cache: HashMap::new() }; // Run the main event loop event::run(context, event_loop, game) } diff --git a/code/rust-sokoban-c03-04/src/systems/rendering_system.rs b/code/rust-sokoban-c03-04/src/systems/rendering_system.rs index 58f4e57..4d12e97 100644 --- a/code/rust-sokoban-c03-04/src/systems/rendering_system.rs +++ b/code/rust-sokoban-c03-04/src/systems/rendering_system.rs @@ -14,6 +14,7 @@ use std::{collections::HashMap, time::Duration}; pub struct RenderingSystem<'a> { pub context: &'a mut Context, + pub image_cache: &'a mut HashMap, } impl RenderingSystem<'_> { @@ -51,6 +52,15 @@ impl RenderingSystem<'_> { renderable.path(path_index) } + + pub fn load_image(&mut self, image_path: String) -> Image { + if let Some(image) = self.image_cache.get(&image_path) { + return image.clone(); + } + let image: Image = Image::new(self.context, &image_path).expect("expected image"); + self.image_cache.insert(image_path, image.clone()); + return image; + } } // System implementation @@ -99,7 +109,7 @@ impl<'a> System<'a> for RenderingSystem<'a> { .sorted_by(|a, b| Ord::cmp(&a.0, &b.0)) { for (image_path, draw_params) in group { - let image = Image::new(self.context, image_path).expect("expected image"); + let image = self.load_image(image_path.to_string()); let mut sprite_batch = SpriteBatch::new(image); for draw_param in draw_params.iter() {