Added assert that a texture is valid when attempting to bind it.#3122
Added assert that a texture is valid when attempting to bind it.#3122ChrisThrasher merged 1 commit intomasterfrom
Conversation
Pull Request Test Coverage Report for Build 9633512138Details
💛 - Coveralls |
|
I still appreciate the attempt to solve the lifetime issue, but I am not a fan of this solution either. Problems with this solution:
If we are moving forward with the direction of "debug-only checks", then I don't see why we shouldn't accept #3097. #3097 doesn't suffer from any of the drawbacks mentioned above -- it's a general, testable, minimal-overhead solution that does not rely on undefined behavior and doesn't require modifying the internals of any SFML type. @binary1248: do you have anything in particular against #3097? Here's an example of the output we would get with #3097 for both the original example used in this PR and the one I created above inspired by #3062:
|
If this is true then why do you think UBSan doesn't complain? I know sanitizers may not be perfect. Is this just a case of UBSan not being sophisticated enough to detect this? |
The Standard is quite clear on this matter (surprisingly...): https://stackoverflow.com/a/68736835/598696 I don't know why UBSan doesn't complain, but it seems like a check for this issue could be implemented in the sanitizer by storing a flag alongside every object, or in a heap allocated registry. Perhaps it slowed down execution too much, or it was just never considered worthwhile implementing, or there's some other weird technical issue that I'm not seeing. It's a good question. |
|
@ChrisThrasher: I found something interesting: google/sanitizers#73 |
4627966 to
c00abe8
Compare
|
If these assertions rely on reading data from an object whose lifetime has already ended then I'm afraid that alone makes it a non-starter for me. |
|
The very obvious difference between this solution and #3097 is that this solution leverages debug machinery that already comes built into most compilers/debuggers. I am not trying to invent new kinds of wheels here, just making it easier for the debugger and any other kind of existing tooling to do its job when running a debug build. Does this catch every single possible case of undefined behaviour? Definitely not, and that was never the claim. If you claim that something is not worth doing if it doesn't have a 100% success rate of achieving its goal, then you can also question the effort compiler vendors have put into flagging memory after it has been freed. They also never guaranteed that it would make detecting errors a 100% probability because you know... we are talking about undefined behaviour. And yet, they still go through that effort because the general consensus among the community is that it is worth it even though it doesn't always work.
How is this even a valid argument? By this logic, all Also, don't forget that undefined behaviour is undefined in the context of the standard. Undefined behaviour can still be defined to a certain degree when running e.g. under a debugger with debug allocators and other instrumentation enabled. The standard makes no mention about what the differences between release and debug builds should be because it leaves it up to vendors to make their own informed decisions on how to go about with their concrete implementations. This change targets vendor-implementation-defined behaviour, specifically when debugging. I already listed 2 likely scenarios in the description of this PR, and the probability of memory blocks being recycled by the allocator instead of being flagged with the freed bit pattern is minuscule unless the allocator has absolutely no other choice (e.g. high memory requirement applications). So in this case, when running under the debugger, "undefined behaviour" isn't as random as it would be when running in release and since this change literally only targets debug builds I don't see why we have to treat "undefined behaviour" in this context as the same scary ghost we should instantly run away from in release.
I don't see how you can assume this is arbitrary... Every OpenGL implementation I have seen in my life generates texture names in ascending order and recycles names that have been explicitly freed through As I mentioned in the description, even 0xDDDDDDDD wouldn't be problematic because that many texture objects would also never be created in any application that was not deliberately trying to be malicious.
I adjusted the change to flag both For anything who wants to verify this: #include <SFML/Graphics.hpp>
struct S
{
sf::Texture texture{sf::Texture::loadFromImage(sf::Image({100, 100}, sf::Color::Red)).value()};
sf::Sprite sprite{texture};
};
std::optional<S> makeS()
{
return std::make_optional<S>();
}
int main()
{
sf::RenderWindow window(sf::VideoMode({200, 200}), "Test");
auto s = makeS().value();
while (window.isOpen())
{
while (const auto event = window.pollEvent())
{
if (event.is<sf::Event::Closed>())
return 0;
}
window.clear();
window.draw(s.sprite);
window.display();
}
}
This change is only meant to target
Why is this all of a sudden an argument? We have plenty of other asserts that we also don't currently "test" due to Catch's lack of support for requiring that the program abnormally terminates akin to googletest's
The same argument can be made about all our other asserts if you claim that the provided text string is not helpful enough. As I said above... This change is not meant to be a shiny new wheel that is eager to be put to use all over the place. The problem definition is clear, the debugging machinery we expect users to be comfortable with already exists, this change just makes this one specific problem easier to debug than it would be without, with minimal code impact. I just don't see how anybody in good faith could be against the general idea of this kind of change regardless of the concrete implementation. |
Pull Request Test Coverage Report for Build 9634618962Details
💛 - Coveralls |
#3097 is not doing anything particularly fancy. It's just an atomic counter with some sugar on top. Not using
I am not claiming that. I am claiming that #3097 is a superior and more general solution to the problem you're trying to solve here. It also conforms to the C++ Standard.
Yes, that is what I am claiming. I don't think it's common practice to rely on undefined behavior in assertions, but you seem to imply it is.
Exactly, this is why #3097 avoid triggering any form of undefined behavior in the first place. The detection mechanism happens and reports a diagnostic prior to UB happening.
Yes, you can carefully check that "UB" does what you expect on your platform, with your compiler, and so on. But you can never predict if a future compiler version, or some weird flag, or some niche architecture behaves as you expect. The easiest solution is to just... not rely on UB in the first place.
Or we could consider using #3097 which already solves the problem for all SFML types and does not rely on undefined behavior.
It's an argument because we already have a solution to the problem that is testable, which is a plus.
Ditto, I'm not saying that your assertion is not valuable, I'm saying that we already have a solution with a more informative and useful message. If I have to weigh this PR against #3097, I have a really hard time seeing why I would pick this one over the #3097.
And I don't see how anybody in good faith could be against an existing solution which does not invoke UB, is testable, has already been implemented for all SFML types, can be toggled at will by users, provides a more informative/actionable diagnostic, has minimal impact on the actual definition of SFML types, does not require us hardcoding magic pointer constants, etc... I also don't see why you think I'm against the general idea, given that I implemented my own solution on that same idea. |
|
@vittorioromeo It's not helpful to create red herring arguments that really just are advocating for your implementation and moving goal posts for the proposed change (i.e. all the "my PR does this, yours doesn't" when that was never the goal, nor a requirement). We're all smart people here and are capable of understanding the different proposals. Next time you can just state that you're in favor of your change, you can of course still list the reasons why. Regarding UB, I don't exactly see the problem. We're already in UB-land before hitting the assert and without the assert, we'll end up in UB-land again. The "white square" problem has been very consistent, one could even say "stable" 😄 , which if I understood correctly relied on OpenGL trying to use the already destroyed texture. |
|
@eXpl0it3r: all the arguments I've made are factual and publicly verifiable. If there are two competing proposals trying to solve the same problem in different ways, I'm going to argue in favour of the one that I believe is superior, regardless of its author. I value genericity, testability, and adherence to the C++ standard. Frankly, accusing me of intentionally creating red herrings is a bit too much. Working on SFML is not giving me any joy nowadays, so I will step down. |
ChrisThrasher
left a comment
There was a problem hiding this comment.
Regarding UB, I don't exactly see the problem. We're already in UB-land before hitting the assert and without the assert, we'll end up in UB-land again.
I'm still skeptical to add more UB to the pile but I suppose we're already reading texture->m_texture prior to this assertion so I'm not sure how the addition of the assertion could make the situation worse.
I'm a bit reluctant to approve this PR, but I suppose I'll give this a shot. 🤞🏻

Title.
Perhaps the simplest solution to make the "white square problem" at least detectable when running a debug build.
Normally, debug memory allocators fill freed memory with patterns such as
0xDDDDDDDDin order to make detecting use-after-frees easier. In our case however, because textures can be (and more recently are probably) stored instd::optionals, we can't assume that the memory will be freed by the allocator even after the texture is destroyed. The OpenGL texture name will be deleted, but the actual memory contents of the still allocatedsf::Texturememory block will remain identical after its destructor has completed.In order to detect bind-after-frees even in such cases, (in debug builds) we assign
m_texturewith a value that is very unlikely to be generated as an OpenGL texture name. After the destructor runs but before the memory block is deallocated, this value will sit inm_textureand any attempt to bind the texture will read this value and fail the assert becauseglIsTexturewill returnGL_FALSE.In the case the memory was actually deallocated, either the operating system throws a SEGFAULT/Access Violation or
0xDDDDDDDD(or whatever pattern your allocator uses) is read fromm_texturewhen attempting to bind it.0xDDDDDDDDis also very unlikely to be a texture name OpenGL generates, so the assertion should fail in this case as well.Test with any code that causes the "white square problem" to manifest itself, e.g.: