-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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 SpriteBatch #2166
base: master
Are you sure you want to change the base?
Add SpriteBatch #2166
Conversation
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## master #2166 +/- ##
==========================================
- Coverage 23.19% 22.99% -0.20%
==========================================
Files 212 215 +3
Lines 18059 18224 +165
Branches 4411 4457 +46
==========================================
+ Hits 4188 4191 +3
- Misses 13145 13580 +435
+ Partials 726 453 -273
Continue to review full report at Codecov.
|
This is a feature I have seen discussed endlessly, and seen a couple of PRs for in the past. I think of the ones I've seen I currently like this approach the most, but I need to spend a bit more time looking into this and run the examples. But I like the approach, it feels consistent with other areas of SFML (namely how There is however, a lot of other tasks for SFML3 already needing to be done so whether or not this is too big to accept into SFML3 I don't know, but really good work dude. |
const auto& comp = [&](const std::size_t a, const std::size_t b) | ||
{ return m_triangleInfos[a].texture < m_triangleInfos[b].texture; }; | ||
|
||
std::stable_sort(indices.begin(), indices.end(), comp); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason to stable sort instead of a non-stable sort?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my older attempts I used it to enforce that triangles with the same depth are rendered in draw call order, but it seems like the current sort logic also guarantees that. I'll switch it to std::sort
after I test it a bit more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't rely on std::sort
happening to be stable if stable sorting is really what you need. That property is liable to change across implementations and over time since std::sort
makes no stability guarantees.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for this huge effort!
First quick feedback -- I'll need to reserve some time to look if/how this generally fits into SFML's scope.
What is missing in your description is a motivation/rationale for this feature. It may be obvious to you ("performance"), but could you elaborate more precisely where you encountered the biggest limits with current SFML, and how this solution would address those?
I like that you added quite some description to each of the 4 modes. What would the decision process for a user look like (e.g. how to choose between Deferred/Immediate/no batching)?
Could the begin()
+ end()
method calls be replaced with an RAII guard?
{
sf::BatchGuard guard = batch.begin();
...
...
} // automatic end()
while (tempTexture.loadFromFile(resourcesDir() / ("Tex" + std::to_string(currentTex) + ".png"))) | ||
{ | ||
textures.push_back(std::move(tempTexture)); | ||
tempTexture = sf::Texture(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably not going to be necessary once the move constructor for sf::Texture
is implemented, but I'm also fine with leaving it for explicitness.
Could also be that some compilers issue a "access to moved variable" warning otherwise.
@Bromeon I have to admit that I've yet to do my homework on the reasoning part. What I planned to do was to try and use the SpriteBatch inside open source SFML games to see if it would improve performance, particularly in cases where it's shy of being a consistent 60 FPS. It would also help with reducing or eliminating the manual batching inside VertexArrays that people do. For instance, At odds seems to lag a bit on my crappy laptop. If the batcher would nudge the performance above the 60 FPS mark while being easy to use (or swap to), that would be great news. However, I still need to analyze the source code and whatnot. |
@Bromeon Regarding the batch modes, first I need to mention that Immediate is the same as not using batching, since it just calls the RenderTarget directly. In hindsight, I'm going to remove it since it's not really useful. Deferred is similar to regular drawing, since draw order is preserved (i.e. same as draw call order). This makes it easy to change RenderTarget logic to SpriteBatch logic using Deferred, and should provide performance enhancements when drawing many things in a row that use the same texture. So, an use case would be drawing all entities that use a wooden box texture, but which also have physics and can be rotated and moved around (and thus can't be inserted into a VertexArray or something). TextureSort is useful if your application is structured into layers, such as top-down or side-scroller games. Within a layer, it's likely that objects don't overlap each other (or the order isn't very important), so if they use different textures, you can reorder them by texture to create bigger batches to send for rendering (thus you get some nice performance improvements). DepthSort is a combination of the previous two modes, that is you can specify an explicit draw order if needed via the depth parameter, but you can also arrange your objects in multiple layers by using preset depths for each layer. So, to summarize:
|
@Bromeon Regarding the batch guard, I'm not sure if // Set default render state in some parent method
batch.begin(/* default state */);
// Somewhere in a nested method to draw an entity
batch.end();
batch.begin(/* special state */);
/* render cool stuff */
batch.end();
batch.begin(/* previous state */);
// back to parent method
batch.end(); |
Any such insights would be very interesting! I think there's a lot of potential, but since the batching API adds quite some complexity, it would be good to be sure it's beneficial in typical cases 🙂
I think it is useful, just needs to be documented that it's equivalent to using no batching. Why useful? Because it allows to quickly switch implementations and compare performances without rewriting the whole rendering code. Also, your description following that comment is great! Do you think you could add this to the documentation?
I have the feeling these scenarios (having no clear owner of the batch) are precisely the situations which are hard to reason about and cause potential errors. Stuff like early returns or exceptions will immediately cause bugs that can be difficult to debug. If batching should be "stackable", then maybe we should provide a dedicated API, which also helps restoring previous state. But one probably loses most of the advantages when switching batch anyway, no? |
47d1dae
to
5d1c106
Compare
I pushed a modified implementation along with some changes to I've removed the // Set batch strategy
spriteBatch.setBatchMode(batchMode);
spriteBatch.batch(sprite, depth);
spriteBatch.batch(text, depth);
spriteBatch.batch(shape, depth);
// Draw the batch
// RenderStates texture is ignored here, since the batch sets the textures itself
spriteBatch.draw(window, sf::RenderStates(sf::BlendAlpha, sf::Transform::Identity, nullptr, shader));
// Optionally clear the batch for rendering on the next frame
spriteBatch.clear(); The advantage of the new implementation is that you can now keep the batch and reuse it for static scenes, which was not possible in the previous version. It also prepares the batches only once, and reuses the result if multiple calls to Regarding the batch example, I've added texts, shapes and shapes with no texture for checking more drawable types. As expected, the batcher struggles with dynamic texts and shapes, since they're high vertex count drawables and kind of mini-batches by themselves. However, for static stuff, inserting into a batch and reusing said batch works almost in the same way as manually batching using VertexArray, and works pretty well if sorting is applied. |
src/SFML/Graphics/SpriteBatch.cpp
Outdated
|
||
else | ||
{ | ||
const auto comp = [&](const std::size_t a, const std::size_t b) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using this default capture mode ([&]
) is it feasible to list the things you want to capture?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would capturing this
work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No because that's the other default capture mode. Try to explicitly list the objects you're capturing. Sometimes that is not feasible but at least try to make it work as to minimize how much stuff gets put into the scope of the lambda.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think capturing m_triangles
directly won't work, but I can do something like this:
auto& triangles = this->m_triangles;
const auto comp = [triangles](const std::size_t a, const std::size_t b)
{
if (triangles[a].depth != triangles[b].depth)
return triangles[a].depth > triangles[b].depth;
else
return triangles[a].texture < triangles[b].texture;
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, maybe you're right to use [this]
. You can copy m_triangles
into the lambda but that's wasteful. I don't hate the idea of making a local reference and using that instead. In either case, it's more explicit than using [&]
which will also capture everything in the current scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like it's possible to capture m_triangles
with an initializer such as [&triangles = m_triangles]
. Maybe I'll use that instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's just making a non-const copy.
src/SFML/Graphics/SpriteBatch.cpp
Outdated
|
||
if (m_batchMode == BatchMode::TextureSort) | ||
{ | ||
const auto comp = [&](const std::size_t a, const std::size_t b) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using this default capture mode ([&]
) is it feasible to list the things you want to capture?
if (m_triangles[a].depth != m_triangles[b].depth) | ||
return m_triangles[a].depth > m_triangles[b].depth; | ||
else | ||
return m_triangles[a].texture < m_triangles[b].texture; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Omitting the braces in this if/else
helps readability. I'd recommend removing the braces for other simple loops and conditions you've added which only contain a single expression. It halves the total line count and keeps relevant code near each other. Because we have clang-format to check formatting, there is no risk of accidentally writing something like
if (condition)
run1();
run2(); // Uh oh, this always get run no matter the value of condition
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree in the case of simple loops / if
s, although in some cases I feel like the extra space given by adding braces helps readability.
c8a6f7c
to
c440c7d
Compare
c440c7d
to
e09cbbf
Compare
I rebased on |
(previously appeared in forum posts as requests, and also in an issue / PR: Add draw batching #1802)
Description
This is a draft PR for a sprite batching implementation. I have more work to do on the implementation, but I've reached a point where I have something I can show, and I'd like to hear thoughts from other people about this whole batching business.
The PR adds two classes: SpriteBatch, which implements the batching logic, and Batchable, an interface implemented by drawables that support batching. Batchable is similar in functionality with Drawable, and the classes that implement it send data to the batcher using a predefined method that takes in vertices and other state information.
The PR also adds the Batchable interface to Sprite, Text and Shape. In theory, anything that uses triangle primitives should work with the batcher, but I have only tested sprites so far.
The batcher has different modes that can be used for batching:
Deferred
groups sprites that use the same texture and follow each other in the same batch, which makes the end result identical with RenderTarget. However, when rendering many sprites that use the same texture in sequence, the batcher should be more efficient with the draw calls.TextureSort
sorts the sprites by texture before grouping and rendering. This causes draw order to be lost; it's as if a "layer" is created for each group of sprites that have the same texture. This can be useful for drawing things that don't overlap, as in that case draw order is not relevant, and the batcher obtains maximum performance.DepthSort
sorts the sprites by texture and an optional "depth" parameter before grouping and rendering. This is a compromise betweenDeferred
andTextureSort
, as you can create multiple layers that have proper ordering between each other, while drawables within a single layer followTextureSort
logic. This can be useful for drawing scenes that use distinct layers, such as Background, Foreground, GUI.Immediate
, which just calls RenderTarget directly on every draw call.Code that uses SpriteBatch requires calling
begin
andend
for setting shaders and blend modes:Tasks
How to test this PR?
You can test it by running the
examples/sprite_batch
demo I added. It shows the differences between regular rendering and batching, and also works as a basic benchmark for performance. It has controls for raising and lowering the sprite count (W and S), and for switching batching modes (F and G).