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

fix: performance improvements on SpriteBatch APIs #1637

Merged
merged 32 commits into from
Jun 4, 2022

Conversation

nathanaelneveux
Copy link
Contributor

@nathanaelneveux nathanaelneveux commented May 18, 2022

Description

Fix for the performance decrease mentioned in #1614

Some items that need work:

  • I feel like I can be more efficient with object creation here and do the entire operation on the first Uint8List
  • After finding decodeImageFromPixels() I believe I can do the whole operation without adding the Bitmap package
  • I need to do more testing of the caching of the image. What I've written so far seems straight forward but I'm not totally convinced subsequent calls to retrieve the image from the cache are working. As I was stepping through the debugger testing something else I kept seeing the same image being generated, but I haven't done a lot of testing here yet.

Checklist

  • The title of my PR starts with a Conventional Commit prefix (fix:, feat:, docs: etc).
  • I have read the Contributor Guide and followed the process outlined for submitting PRs.
  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation in docs and added dartdoc comments with ///.
  • I have updated/added relevant examples in examples.

Breaking Change

  • Yes, this is a breaking change.
  • No, this is not a breaking change.

Related Issues

Fixes #1614

Copy link
Contributor

@st-pasha st-pasha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there is anything wrong with the current approach that uses PictureRecorder. (If there is, we need to have a clear confirmation of that, then we need to file a bug to Flutter team, and use the suggested workaround until that bug is resolved, but also put it into a separate function so that it can be reused through multiple other places where we use PictureRecorder in Flame).

The more probable cause of #1614 is that SpriteBatch.load() method attempts to generate atlas on every call - even if the sprite batch won't be using the flipped images later on. And that image generation is not cached. So, if the user has code that does SpriteBatch.load() whenever a component is created, and if many such components are created per second, then there will be lots of unnecessary image generation happening. This explanation fits well with the problem described in #1614 as "IOS cannot even see the app launches because it shutting down immediately after launched due to memory issue."

So, I suggest to do the following:

  1. Restore the original image generation through PictureRecorder;
  2. Make invocation of _generateAtlas happen only if the user explicitly requests support for flipped sprites in their SpriteBatch;
  3. Cache that generated image;
  4. (Optionally) Measure the speed difference between generating an image via PictureRecorder vs via the Bitmap library.

packages/flame/pubspec.yaml Outdated Show resolved Hide resolved
Copy link
Contributor

@st-pasha st-pasha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nathanaelneveux can we please start with the following:

  1. Add a boolean flag into SpriteBatch.load() which will tell whether to generate the mirrored image or not. We don't want to create extra images for those users who will not be using them.
  2. For now, have two methods _generateAtlas1 and _generateAtlas2 with the PictureRecorder and the Bitmaps approaches -- this way it would be easier for us to compare and debug the two methods.

@nathanaelneveux
Copy link
Contributor Author

I think we can do better than a flag for the generation of the mirrored images. For one thing SpriteBatch.load is buried pretty deep within RenderableTiledMap with the main user facing function being .fromFile().

How do you feel about doing it as a "lazy load" where once we get a call to addTransform with flip = true we call _generateAtlas?

@spydon
Copy link
Member

spydon commented May 20, 2022

How do you feel about doing it as a "lazy load" where once we get a call to addTransform with flip = true we call _generateAtlas?

Will that not potentially impact performance mid-game instead of in onLoad?

@nathanaelneveux
Copy link
Contributor Author

How do you feel about doing it as a "lazy load" where once we get a call to addTransform with flip = true we call _generateAtlas?

Will that not potentially impact performance mid-game instead of in onLoad?

addTransform is part of the loading process as well and should happen pretty early on in the lifecycle of this component. You have to add all your transforms before you would do your first render. See how its done in Flame Tiled for example.

@spydon
Copy link
Member

spydon commented May 20, 2022

addTransform is part of the loading process as well and should happen pretty early on in the lifecycle of this component. You have to add all your transforms before you would do your first render. See how its done in Flame Tiled for example.

Alright, sounds like it makes sense to have it lazily initialized then!

@spydon
Copy link
Member

spydon commented May 24, 2022

@nathanaelneveux any progress on this one? Anything that you need help with? :)

@nathanaelneveux
Copy link
Contributor Author

@nathanaelneveux any progress on this one? Anything that you need help with? :)

@nathanaelneveux any progress on this one? Anything that you need help with? :)

I'm honestly struggling with what pattern to use to do this with null safety - I'll post what I have so far in a bit with some notes - I could use some advice.

@nathanaelneveux
Copy link
Contributor Author

So obviously you’ll see in the most recent commit that it’s not quite there yet. It’s still generating the flippableAtlas right away.

I was hoping late would cause it to wait until it was used to begin generating which right now would be at render but it isn’t waiting. Once I can get late working then the next step would be to move the late generation up into the addTranform so that it’s still being generated early on in the lifecycle when needed.

I need a reference to Images and a reproducible key - which right now I’m using path with a suffix. I’m trying to avoid adding those to the constructor because I think if your using the constructor it doesn’t make sense to do lazy initialization? Or maybe I’m over thinking that?

@nathanaelneveux
Copy link
Contributor Author

Even though this is passing tests this isn't working on my test map - the flips are wrong. I'll have a test that looks at a larger atlas to get the bottom of this and make sure it doesn't happen again.

@spydon
Copy link
Member

spydon commented Jun 2, 2022

Even though this is passing tests this isn't working on my test map - the flips are wrong. I'll have a test that looks at a larger atlas to get the bottom of this and make sure it doesn't happen again.

It seems like it is failing both of the tests that you added, even the one not using the atlas?
What error are you experiencing with larger maps?

@nathanaelneveux
Copy link
Contributor Author

nathanaelneveux commented Jun 3, 2022

Even though this is passing tests this isn't working on my test map - the flips are wrong. I'll have a test that looks at a larger atlas to get the bottom of this and make sure it doesn't happen again.

It seems like it is failing both of the tests that you added, even the one not using the atlas? What error are you experiencing with larger maps?

There was an error introduced when I stacked the flipped image vertically instead of horizontally in 35e08d8. source.top + source.height, should have been atlas.height/2 + source.top. This was still passing tests because the tile map in this test was only 1 tile high - source.top = atlas.height/2 in this instance. I reverted to the generateAtlas I had before 35e08d8. I believe that isn't passing tests now because atlas.width is no longer reliable - it may still be the width of the atlas before _makeFlippedAtlas() completes.

@nathanaelneveux
Copy link
Contributor Author

Screen Shot 2022-06-02 at 9 17 54 PM

I was wrong about the screenshot I posted yesterday - the above is what is produced by Canvas.drawImageRect. So the batchItem.source is also going to need some adjustments.

@nathanaelneveux
Copy link
Contributor Author

Tests are passing now - I know there must be a better way to do the conditional in the BatchItem construction but its too late here for me to think straight

Copy link
Member

@spydon spydon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your very hard and long work on this PR @nathanaelneveux, very appreciated! 🎉
(Are you on our discord?)

Comment on lines 53 to 55
..translate(source.width / 2, source.height / 2)
..rotateY(flip ? pi : 0)
..translate(-source.width / 2, -source.height / 2),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could these be defined directly in the Matrix4? 🤔
@st-pasha Do you know how to do that, or if it is possible?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could have some extension method to make a Matrix4 out of RSTransform, looks like there isn't one right now.

That _defaultScale looks mighty suspicious to me as well, I think we can just remove it without anyone noticing.

Copy link
Member

@spydon spydon Jun 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it, should this really be 0 and not 1? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the difference between the matrix's with the rotateY code and without:

WITH FLIPS

[0] 1.0,-0.0,-1.2246467991473532e-16,32.0
[1] -0.0,-1.0,0.0,16.0
[2] -0.0,0.0,0.0,0.0
[3] -0.0,0.0,0.0,1.0

[0] -0.0,1.0,0.0,48.0
[1] 1.0,0.0,-1.2246467991473532e-16,0.0
[2] -0.0,0.0,0.0,0.0
[3] -0.0,0.0,0.0,1.0

[0] -0.0,-1.0,0.0,48.0
[1] -1.0,0.0,1.2246467991473532e-16,32.0
[2] -0.0,0.0,0.0,0.0
[3] -0.0,0.0,0.0,1.0

[0] -1.0,-0.0,1.2246467991473532e-16,64.0
[1] -0.0,1.0,0.0,16.0
[2] -0.0,0.0,0.0,0.0
[3] -0.0,0.0,0.0,1.0

NO FLIPS

[0] -1.0,-0.0,0.0,48.0
[1] 0.0,-1.0,0.0,16.0
[2] 0.0,0.0,0.0,0.0
[3] 0.0,0.0,0.0,1.0

[0] 0.0,1.0,0.0,48.0
[1] -1.0,0.0,0.0,16.0
[2] 0.0,0.0,0.0,0.0
[3] 0.0,0.0,0.0,1.0

[0] 0.0,-1.0,0.0,48.0
[1] 1.0,0.0,0.0,16.0
[2] 0.0,0.0,0.0,0.0
[3] 0.0,0.0,0.0,1.0

[0] 1.0,-0.0,0.0,48.0
[1] 0.0,1.0,0.0,16.0
[2] 0.0,0.0,0.0,0.0
[3] 0.0,0.0,0.0,1.0

(.toString doesn't look to be in the same order as the Matrix4 initializer)

@spydon spydon changed the title fix: performance of _generateAtlas WIP fix: performance of _generateAtlas Jun 3, 2022
@wolfenrain wolfenrain changed the title fix: performance of _generateAtlas fix: performance improvements on SpriteBatch APIs Jun 3, 2022
packages/flame/lib/src/sprite_batch.dart Outdated Show resolved Hide resolved
@nathanaelneveux
Copy link
Contributor Author

Thanks for your very hard and long work on this PR @nathanaelneveux, very appreciated! 🎉
(Are you on our discord?)

Phew :) that definitely ending up being more than I expected when first tackling this as a hobby project lol. Thank you all for your patience and help - great community you’ve built here

/// [name] in the cache.
Future<Image> fetchOrGenerate(
String name,
Future<Image> Function() imageGenerator,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it named "Generator" instead of "Builder"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't name it, but it's not a builder (if the builder name is used according to the builder pattern).

///
/// If the [imageGenerator] is used, the resulting [Image] is stored with
/// [name] in the cache.
Future<Image> fetchOrGenerate(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this meant to fetch and if it fails to do so then, it builds an image and also caches it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, is that not clear from the Dartdocs? 😅

@spydon spydon enabled auto-merge (squash) June 4, 2022 15:17
@spydon spydon merged commit 4b19a1b into flame-engine:main Jun 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
blocking release This needs to be fixed to be able to release the next version
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Performance is dramatically decreased After #1610
5 participants