I'm trying to find a way to have one RenderTexture that's reused multiple times throughout one frame. Basically something like this:
// Render stuff to a reusable RenderTexture
// Use the RenderTexture's texture in a sprite
// Clear the RenderTexture, render new stuff to it
// Use the RenderTexture's texture in a different sprite
// Clear the RenderTexture, render new stuff to it
// Use the RenderTexture's texture in a different sprite
// Clear the RenderTexture, render new stuff to it
// Use the RenderTexture's texture in a different sprite
// Clear the RenderTexture, render new stuff to it
// Use the RenderTexture's texture in a different sprite
// (etc.)
My previous attempt just used a ton of RenderTextures, one for each object that needed it, but since they're not lightweight objects like normal textures are (relatively speaking), that gets out of hand really quickly, and causes a crash after too many have been allocated and memory runs out.
After that I tried just allocating a pool of RenderTextures and doling them out to whoever needed them, but RenderTextures can't be resized without arduously destroying and recreating their context (it takes about a quarter of a second on my machine--completely impossible without the user noticing), having a ton of RenderTextures that I might never need uses a lot of memory, and having a fixed size creates an artificial limit that I don't really like hanging over my head.
Finally, I tried just copying the RenderTexture's texture to a new Texture and using that, but even using glCopyImageSubData the performance hit was still too much.
So none of these options are really all that great. I did try one other thing, but the results were kind of weird:
sf::RenderTexture renderTexture;
renderTexture.create(256, 256);
// RenderWindow creation and event loop boilerplate here
{
window.clear();
renderTexture.clear(sf::Color::Red);
renderTexture.display();
window.draw(QuickSprite{ 0, 0, renderTexture.getTexture() });
renderTexture.clear(sf::Color::Blue);
renderTexture.display();
window.draw(QuickSprite{ 256, 256, renderTexture.getTexture() });
window.display();
}
All this code is supposed to do is draw a red rectangle (via a RenderTexture) on one part of the screen and a blue one on another. I didn't really expect it to work...and it didn't. But what caught my attention was that instead of drawing two blue rectangles like I thought (or one of each color like I hoped), it drew two red ones, and I can't imagine why. I also noticed while trying to figure this out that if I add renderTexture.getTexture().copyToImage() before the call to window.draw it works as I wanted, drawing a red and blue rectangle. Specifically glGetTexImage does some magic to get it working, but that's a hack and I'm like 99% sure that even if it wasn't a performance hit I couldn't actually rely on it to do this completely unrelated task.
Anyway, this is just a very long-winded way of asking if there's any way I can get the reuse of RenderTextures working, because nothing I've come up with so far has really been satisfactory. I'd be super appreciative if anyone out there has any ideas and could share them with me.
Actually my QuickSprite was even lazier (I just directly inherited from sf::Sprite, though in retrospect I really should have just made a function that returned a normal sf::Sprite--no clue why I didn't...), but fundamentally they were the exact same thing. I still did try copying and pasting your exact code, though, but it still shows two red rectangles on my screen. Thanks for trying, though! :)
That being said I did some more testing and I think I figured out what was stopping me from just reusing the RenderTexture and why it was drawing two red squares instead of two blue squares. Sort of. I don't want to say that it's a bug in SFML per se, but it seems really suspicious because the implication is that drawing using the same RenderTexture will actually always work...unless the two draw calls that use that RenderTexture's texture are not broken up by a call that uses a different/null texture.
Basically, this code, as mentioned before, does not work (at least on my machine):
window.clear(sf::Color::Black);
// Draw the first red rectangle
renderTexture.clear(red);
renderTexture.display();
window.draw(QuickSprite{ 0, 0, renderTexture.getTexture() });
// Draw the second blue rectangle
renderTexture.clear(blue);
renderTexture.display();
window.draw(QuickSprite{ 256, 256, renderTexture.getTexture() });
window.display();
This code, however, does:
window.clear(sf::Color::Black);
// Draw the first red rectangle
renderTexture.clear(sf::Color::Red);
renderTexture.display();
window.draw(QuickSprite{ 0, 0, renderTexture.getTexture() });
// Note this call--it seems to do nothing but without it we get two red squares
window.draw(sf::RectangleShape{});
// Draw the second blue rectangle
renderTexture.clear(sf::Color::Bed);
renderTexture.display();
window.draw(QuickSprite{ 256, 256, renderTexture.getTexture() });
window.display();
All it takes is a request to draw something with a null texture and suddenly I can reuse the RenderTexture to draw separate things like I wanted. I narrowed down the exact function calls that are necessary and it turns out it's the RenderWindow's applyTexture(NULL) call (and probably the call to activate(true) before that as well?). I don't know enough about OpenGL to know why this happens, if it's just a fluke, or if it's actually an oversight in SFML. Can anyone shed some light on this?
@Nexus: yes, Hapax one without Quicksprite.
I re-tried it, Debug and Release, x86 and x64, Static and Dynamic. Two red squares, always.
Edit: Hapax, this is what I get with your second code:
(http://i.imgur.com/ZV26PTK.png)
I tried the second sample code that Hapax posted.
To clarify, this:
The second one draws a circle on the render texture after each clear (to see if it's clearing it and if it is, it's clearing with the wrong colour):
#include <SFML/Graphics.hpp>
int main()
{
sf::RenderTexture renderTexture;
renderTexture.create(256, 256);
sf::Sprite renderSprite(renderTexture.getTexture());
sf::CircleShape circle(64.f);
circle.setOrigin(circle.getRadius(), circle.getRadius());
circle.setPosition(128.f, 128.f);
circle.setFillColor(sf::Color::Green);
sf::RenderWindow window(sf::VideoMode(512, 512), "Re-using Render Textures Without QuickSprite With Added Circles", sf::Style::Default);
while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed || event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Escape)
window.close();
}
window.clear();
renderTexture.clear(sf::Color::Red);
renderTexture.draw(circle);
renderTexture.display();
renderSprite.setPosition(0, 0);
window.draw(renderSprite);
renderTexture.clear(sf::Color::Blue);
renderTexture.draw(circle);
renderTexture.display();
renderSprite.setPosition(256, 256);
window.draw(renderSprite);
window.display();
}
return EXIT_SUCCESS;
}
Executable (on MediaFire): http://www.mediafire.com/download/a0faa3lga75xack/MultipleRenderTexturesCircles.exe
(http://i.imgur.com/UipLg7u.jpg)
I tested it on my main system:
OS: Windows 8.1 Pro
GPU: AMD Radeon R4 Graphics
Linkage: Dynamic
Compiler: MinGW64 (5.0.2) [Used the same binaries for the other tests]
Results:
(http://i.imgur.com/TRvehhq.png)
I tested it on my secondary system:
OS: Windows 8.1 Home
GPU: Intel HD Graphics (this is a custom driver, to be more specific: Acer Aspire E1-510P-4402)
Linkage: Dynamic
Compiler: Used the previous binaries from main system*
Results:
(http://i.imgur.com/T1LveKX.png)
I tested it on my virtual machine (within my main system):
OS: Windows 7 Professional
GPU: VMWare's Internal
Linkage: Dynamic
Compiler: Used the previous binaries from main system*
Results:
(http://i.imgur.com/hZwqws5.png)
Upon all of the other posts, it could be Windows 8.1 that is causing a problem.
This is definitely a driver bug. I can bet with you that multi-context tests weren't part of their OpenGL test suite, because who even makes use of that feature anyway right?
Because I (thankfully) don't have the privilege to understand how their driver "works" (or doesn't), I can only guess at this point.
After you are done drawing the first sprite, the RenderTexture's texture is left bound in the window's context. Normally this shouldn't be a problem, since drawing to the RenderTexture doesn't make use of it anyway (if it did, the results would be undefined). In fact, any normal drawing still works in this case, it's just the clear that doesn't for whatever reason.
Because the effects of OpenGL commands don't need to be complete right after they are issued, all that is done by calling all those functions is a command queue is built up. OpenGL guarantees that the effects of commands are applied in the exact order in which they were issued. Since there is only a single thread here, this also shouldn't be an issue. Commands are issued to each context serially anyway.
The problems only pop up when one tries to be smart about this system and start optimizing things that should be able to be optimized. In order to do this, I can imagine a dependency tree is internally built up that tracks which commands depend on the results of previous commands and which commands can be executed in parallel to others. The driver might be incorrectly thinking that it can't clear because the contents of the FBO attachment might still be needed somewhere. What's even stranger is that it just skips the clear command all together instead of just postponing it. Drawing a quad with the clear colour to the RenderTexture works, even though it does pretty much the same as the clear command.
Something probably just went wrong with the inter-context dependency tracking. "Normal" OpenGL users don't notice this and this bug is therefore probably not widely known because in your typical OpenGL application only 1 context is ever used anyway and if multiple are used, it's not to draw but to stream data better or balance the state validation load. There is no benefit to drawing from multiple contexts, and in our case there are even concrete drawbacks.
The thing with SFML is that it doesn't "do stuff like all the other guys do". Here are a few things that SFML does that you don't find in a typical OpenGL application:
- Make very liberal use of multiple OpenGL contexts, for drawing as well.
- Maintain a lot of state between frames. SFML even expects this to work or it will just break.
- Never unbind FBOs. Many drivers interpret unbinding an FBO as "flushing" it (this is not part of the specification). So if SFML never unbinds, you could say it never gets flushed from the perspective of a sloppy driver.
It is a combination of these things and many others that make SFML "that special kid" that just never gets any attention. While perfectly legal according to the specification, driver developers would probably rather spend their paid time working on things that their "bigger customers" care about more. Maybe SFML might be able to get on that train one day when it starts to make use of Vulkan. ::)
There are several workarounds to solve this problem:
1. Alternate between 2 RenderTextures. This way you will implicitly force the window to rebind its texture and "break the dependency chain".
#include <SFML/Graphics.hpp>
int main()
{
sf::RenderTexture renderTexture1;
renderTexture1.create(256, 256);
sf::Sprite renderSprite1(renderTexture1.getTexture());
sf::RenderTexture renderTexture2;
renderTexture2.create(256, 256);
sf::Sprite renderSprite2(renderTexture2.getTexture());
sf::RenderWindow window(sf::VideoMode(512, 512), "Re-using Render Textures Without QuickSprite", sf::Style::Default);
while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed || event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Escape)
window.close();
}
window.clear();
renderTexture1.clear(sf::Color::Red);
renderTexture1.display();
renderSprite1.setPosition(0, 0);
window.draw(renderSprite1);
renderTexture2.clear(sf::Color::Blue);
renderTexture2.display();
renderSprite2.setPosition(256, 256);
window.draw(renderSprite2);
renderTexture1.clear(sf::Color::Green);
renderTexture1.display();
renderSprite1.setPosition(256, 0);
window.draw(renderSprite1);
renderTexture2.clear(sf::Color::Yellow);
renderTexture2.display();
renderSprite2.setPosition(0, 256);
window.draw(renderSprite2);
window.display();
}
return EXIT_SUCCESS;
}
2. Manually break the dependency chain by unbinding and rebinding the RenderTexture's texture yourself. It is important to rebind it after unbinding or else the window's cached texture state will go out of sync and you will need to resetGLStates() to fix it.
#include <SFML/Graphics.hpp>
int main()
{
sf::RenderTexture renderTexture;
renderTexture.create(256, 256);
sf::Sprite renderSprite(renderTexture.getTexture());
sf::RenderWindow window(sf::VideoMode(512, 512), "Re-using Render Textures Without QuickSprite", sf::Style::Default);
while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed || event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Escape)
window.close();
}
window.clear();
renderTexture.clear(sf::Color::Red);
renderTexture.display();
renderSprite.setPosition(0, 0);
window.draw(renderSprite);
sf::Texture::bind(0);
sf::Texture::bind(&renderTexture.getTexture());
renderTexture.clear(sf::Color::Blue);
renderTexture.display();
renderSprite.setPosition(256, 256);
window.draw(renderSprite);
window.display();
}
return EXIT_SUCCESS;
}
3. Just draw a quad covering the entire RenderTexture to clear it. Draw operations seem to work even though clears do not.
#include <SFML/Graphics.hpp>
int main()
{
sf::RenderTexture renderTexture;
renderTexture.create(256, 256);
sf::Sprite renderSprite(renderTexture.getTexture());
sf::RectangleShape clearRect(sf::Vector2f(256, 256));
clearRect.setFillColor(sf::Color::Blue);
sf::RenderWindow window(sf::VideoMode(512, 512), "Re-using Render Textures Without QuickSprite", sf::Style::Default);
while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed || event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Escape)
window.close();
}
window.clear();
renderTexture.clear(sf::Color::Red);
renderTexture.display();
renderSprite.setPosition(0, 0);
window.draw(renderSprite);
renderTexture.draw(clearRect);
renderTexture.display();
renderSprite.setPosition(256, 256);
window.draw(renderSprite);
window.display();
}
return EXIT_SUCCESS;
}
I'd say option 2 has the lowest overhead followed by 1 then 3. Going through the whole draw process just to fill the framebuffer with a fixed value is really wasteful, and creating multiple FBO objects where unnecessary is also wasteful. If you were going to use multiple RenderTextures anyway, then option 1 isn't that bad.
Can confirm that all three of those work.
Would it be worth it adding #2 into SFML? It is technically a driver bug but this seems like the kind of thing SFML would handle to make everything "just work," and the fix seems to be incredibly easy. I just tried adding a flag to sf::Texture that marked it as belonging to a RenderTexture and then, at the end of RenderTarget::draw, added the following code:
if (states.texture && states.texture->m_ownedByRenderTexture)
{
Texture::bind(0);
m_cache.lastTextureId = 0;
}
Seemed to make everything work alright.