Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an image cache in section 3.4 #90

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
build
.DS_Store
src/.DS_Store
src/.DS_Store
.idea
98 changes: 84 additions & 14 deletions books/en_US/src/c03-04-batch-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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<u8, HashMap<String, Vec<DrawParam>>>` where:
Expand All @@ -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<String, Image>` 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).

Expand Down
7 changes: 5 additions & 2 deletions code/rust-sokoban-c03-04/src/main.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -22,6 +24,7 @@ use crate::systems::*;

struct Game {
world: World,
image_cache: HashMap<String, Image>,
}

impl event::EventHandler for Game {
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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)
}
12 changes: 11 additions & 1 deletion code/rust-sokoban-c03-04/src/systems/rendering_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Image>,
}

impl RenderingSystem<'_> {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down