- ➡️ Built-in Emscripten support:
- This fork now works out-of-the-box with Emscripten.
- All existing examples and tests run flawlessly in the browser.
- No explicit
#ifdef SFML_SYSTEM_EMSCRIPTENis required anywhere in user code.
-
➡️ Built-in ImGui support:
- Adds a new
SFML::ImGuimodule that depends onSFML::Graphics. - Can be controlled via the CMake option
SFML_BUILD_IMGUI.
📜 Code example
auto graphicsContext = sf::GraphicsContext::create().value(); // Holds all "global" OpenGL state sf::ImGuiContext imGuiContext; // Holds all "global" ImGui state sf::RenderWindow window({.size{640u, 480u}, .title = "ImGui + SFML = <3"}); sf::Clock deltaClock; while (true) { // `sf::base::Optional` is a drop-in replacement for `std::optional` while (const sf::base::Optional event = window.pollEvent()) { imGuiContext.processEvent(window, *event); if (sf::EventUtils::isClosedOrEscapeKeyPressed(*event)) return 0; } // Updates the ImGui state and sets `window` as the active ImGui window imGuiContext.update(window, deltaClock.restart()); // Native ImGui functions can be used after `imGuiContext.update` ImGui::Begin("Hello, world!"); ImGui::Button("Look at this pretty button"); ImGui::End(); window.clear(); imGuiContext.render(window); window.display(); }
- Adds a new
- ➡️ Complete removal of legacy OpenGL:
- Upstream SFML still uses legacy OpenGL calls such as
glBeginandglEnd. - This fork now internally uses modern OpenGL that is compatible with OpenGL 3.1 ES.
- OpenGL ES 3.1 is now supported on all platforms, including Windows via ANGLE.
- This fork provides built-in shaders that use the following API:
layout(location = 0) uniform mat4 sf_u_mvpMatrix; layout(location = 1) uniform sampler2D sf_u_texture; layout(location = 0) in vec2 sf_a_position; layout(location = 1) in vec4 sf_a_color; layout(location = 2) in vec2 sf_a_texCoord; // non-normalized out vec4 sf_v_color; out vec2 sf_v_texCoord; // normalized
- Upstream SFML still uses legacy OpenGL calls such as
TODO P0: update
-
➡️ Support for simultaneous audio devices:
- Upstream SFML does not support simulataneous different audio devices -- only one playback device and one capture device can be active at any time.
- This fork supports multiple different audio devices at the same time (see
multi_audio_deviceexample): this allows, for example, for game sounds to be played on speakers while multiplayer VOIP is played on headphones.
📜 Code example
// Create sound sources auto music0 = sf::Music::openFromFile("resources/doodle_pop.ogg").value(); auto music1 = sf::Music::openFromFile("resources/ding.flac").value(); auto music2 = sf::Music::openFromFile("resources/ding.mp3").value(); // Store all source sources together for convenience sf::SoundSource* const sources[]{&music0, &music1, &music2}; // Create the audio context auto audioContext = sf::AudioContext::create().value(); // For each hardware playback device, create a SFML playback device std::vector<sf::PlaybackDevice> playbackDevices; for (const sf::PlaybackDeviceHandle& deviceHandle : sf::AudioContext::getAvailablePlaybackDeviceHandles()) playbackDevices.emplace_back(deviceHandle); // Play multiple sources simultaneously on separate playback devices for (base::SizeT i = 0u; i < playbackDevices.size(); ++i) sources[i % 3]->play(playbackDevices[i]);
-
➡️ Restore factory-based creation APIs for SFML resources:
- Factory-based creation APIs considerably increased type-safety and usability of SFML resources as they completely eliminated the presence of an "empty state" and made it obvious to users where errors could occur, forcing them to decide between handling them, ignoring them, or propagating them.
- Despite many months of work and discussion on factory-based APIs, factory-based have been reverted into legacy APIs with SFML/SFML#3152, due to fear that users would find the migration from 2.x to 3.x too difficult.
- This fork does not include SFML/SFML#3152 and moves forward with the vision of a safer and more pedagogically valuable version of SFML.
📜 Code example
// ERROR, does not compile -- SFML resources do not have a default "empty state". /* sf::SoundBuffer soundBuffer; */ // OK, user explicitly chose to throw if the file loading fails const auto soundBuffer = sf::SoundBuffer::loadFromFile(resourcesDir() / "ball.wav").value(); // OK, user explicitly chose return if loading fails const auto optSoundBuffer = sf::SoundBuffer::loadFromFile(resourcesDir() / "ball.wav"); if (!optSoundBuffer.hasValue()) { return EXIT_FAILURE; }
-
➡️ Debug lifetime tracking for all SFML resources:
- Catches common lifetime mistakes between dependee types (e.g.
sf::Font) and dependant types (e.g.sf::Text) at run-time, providing the user with a readable error message. - Enabled for debug builds by default, can be controlled via the CMake option
SFML_ENABLE_LIFETIME_TRACKING. - Rejected for upstream inclusion in SFML/SFML#3097.
- When enabled: zero compilation time impact, negligible runtime performance impact.
📜 Error message example
FATAL ERROR: a texture object was destroyed while existing sprite objects depended on it. Please ensure that every texture object outlives all of the sprite objects associated with it, otherwise those sprites will try to access the memory of the destroyed texture, causing undefined behavior (e.g., crashes, segfaults, or unexpected run-time behavior). One of the ways this issue can occur is when a texture object is created as a local variable in a function and passed to a sprite object. When the function has finished executing, the local texture object will be destroyed, and the sprite object associated with it will now be referring to invalid memory. Example: sf::Sprite createSprite() { sf::Texture texture(/* ... */); sf::Sprite sprite(texture, /* ... */); return sprite; // ^~~~~~ // ERROR: `texture` will be destroyed right after // `sprite` is returned from the function! } Another possible cause of this error is storing both a texture and a sprite together in a data structure (e.g., `class`, `struct`, container, pair, etc...), and then moving that data structure (i.e., returning it from a function, or using `std::move`) -- the internal references between the texture and sprite will not be updated, resulting in the same lifetime issue. In general, make sure that all your texture objects are destroyed *after* all the sprite objects depending on them to avoid these sort of issues. - Catches common lifetime mistakes between dependee types (e.g.
-
➡️ Compile-time--enforced lifetime correctness for
sf::Textureand its dependants (sf::Spriteandsf::Shape):- Rather than
sf::Spriteandsf::Shapestoring asf::Texture*internally, which can easily become invalidated, thesf::Texture*is now passed at the point where it is required: thesf::RenderTarget::drawcall. - This prevents common lifetime issues that SFML users have frequently encountered (i.e. "the white square problem") at compile-time, without any extra overhead.
- This also promotes better code organization and a more linear lifetime hierarchy tree for our users.
- This was proposed for upstream SFML in two different forms (SFML/SFML#3072 and SFML/SFML#3080), but rejected.
📜 Code example
auto graphicsContext = sf::GraphicsContext::create().value(); const auto texture = sf::Texture::loadFromFile("image.png").value(); // ERROR, does not compile -- sprites do not store a texture pointer anymore. /* sf::Sprite sprite(texture); */ // OK, prepare the sprite to eventually display the entire texture sf::Sprite sprite{.textureRect = texture.getRect()}; // ERROR, does not compile -- a texture (or the lack thereof) has to be provided during the draw call. /* window.draw(sprite); */ // OK, user has a valid texture available at the point of the draw call -- no lifetime woes! window.draw(sprite, texture); // Alternatively, just draw the texture directly... window.draw(texture); // ...or with some parameters: window.draw(texture, {.position = {25.f, 25.f}, .rotation = sf::degrees(180.f)});
- Rather than
- ➡️ Removal of polymorphic inheritance trees:
sf::Drawable,sf::Shape, andsf::Transformablehave either been removed or made non-polymorphic.- These inheritance trees promote overuse of OOP and dynamic allocation, and move users away from data-oriented design.
- In practice, it's not useful to have something like
std::vector<std::unique_ptr<sf::Drawable>>, and it actually leads newcomers to poor software engineering practices. - If that sort of polymorphism is required in rare cases, it can always be obtained via
std::functionor other basic type erasure techniques.
- In practice, it's not useful to have something like
- ➡️ Removal of
sf::VertexArrayin lieu ofstd::vector<sf::Vertex>:sf::VertexArraywas just a wrapper overstd::vector<sf::Vertex>that exposes a subset ofstd::vector's API.std::vector<sf::Vertex>should be used instead, and users should be encouraged to do the same.- This was proposed for upstream SFML in SFML/SFML#3118, but rejected.
- ➡️ Removal of global state whenever possible:
- Upstream SFML is full of hidden global state: for example, any graphical resource or audio resource ends up interacting with a global registry (via
std::shared_ptrand other expensive operations) on construction/destruction. - This fork removes any such hidden global state, and requires the user to decide where these registries live via
sf::AudioContextandsf::GraphicsContext.- Generally, these context objects can be created at the beginning of
mainand passed downwards to the rest of the application.
- Generally, these context objects can be created at the beginning of
- Not only this change increases run-time performance and decreases compilation time overhead, but it also simplifies the internal implementation of SFML reducing the risk of subtle global initializiation fiasco issues and promoting users to write simpler software with a clear hierarchical lifetime structure.
- Upstream SFML is full of hidden global state: for example, any graphical resource or audio resource ends up interacting with a global registry (via
-
➡️ New
SFML::Basemodule:- New module containing abstractions and utilities generally useful in any C++ project.
- Significantly decreases reliance on the Standard Library, providing drop-in replacements that are much faster to compile and much more performant at run-time even with optimizations disabled.
- All components of
SFML::Basehave been carefully crafted to maximize compile-time thoughput, debug performance, and ease of use.
📜 Code example
// Drop-in replacement for `std::unique_ptr`: sf::base::UniquePtr<T> uPtr = sf::base::makeUnique<T>(/* ...`T` constructor args... */); // Fast PImpl idiom (zero allocation): sf::base::InPlacePImpl<T, 128 /* buffer size */> pImpl(/* ...`T` constructor args... */); // Fast traits (zero instantiation via compiler built-ins, does not virally include expensive `<type_traits>`) static_assert(SFML_BASE_IS_TRIVIALLY_COPYABLE(T)); // Fast math (uses compiler built-in if available) constexpr auto result = sf::base::cos(3.14f); // Fast index sequences (uses compiler built-in if available) constexpr auto indexSequence = SFML_BASE_MAKE_INDEX_SEQUENCE(32); // ...and much more...
-
➡️
sf::Windowclosed state has been removed:- Windows are now considered always "open".
- If a window needs to be closed/re-opened multiple times, it can be wrapped into an optional.
- As shown by the examples, this makes code simpler and removes another unnecessary "empty state".
📜 Code example
int main() { auto graphicsContext = sf::GraphicsContext::create().value(); sf::RenderWindow window(screen, "Example window"); while (true) // `window.isOpen()` does not exist anymore { while (const sf::base::Optional event = window.pollEvent()) if (sf::EventUtils::isClosedOrEscapeKeyPressed(*event)) return 0; // `window.close()` does not exist anymore, just return } } // Need an empty state? Use `sf::base::Optional<sf::RenderWindow>`.
-
➡️
sf::Socketconstructor now takes aisBlockingparameter:- Following the principle of being more explicit, users now have to explicitly decide whether they want their socket to be blocking or not on construction, rather than relying on the possibly wrong default of blocking mode.
📜 Code example
// ERROR, does not compile -- blocking behavior not specified /* sf::UdpSocket socket; */ // OK, blocking behavior explicitly provided sf::UdpSocket socket(/* isBlocking */ true);
- ➡️ Removal of catch-all module-wide headers like
Audio.hppandWindow.hpp:- These headers go against the principles of header hygiene, they promote poor practices and slow down users' projects compilation times.
- ➡️ Simplified and polished examples:
- All examples have been manually reviewed and polished to be as idiomatic and simple as possible, reducing needless use of inheritance/polymorphism and removing unnecessary layers of abstraction.
- ➡️ Changed C++ Standard to C++23:
- Some features (e.g. designated initializers.
[[likely]],char8_t,constinit, aggregate initialization using parentheses, concepts) are now used throughout the library.
- Some features (e.g. designated initializers.
- ➡️ External dependencies are now downloaded and built:
- This work has been done by @binary1248 and will hopefully be merged into upstream SFML soon: SFML/SFML#3141.
- ➡️ Stack trace generation for errors and assertions:
- Human-readable stack traces are generated on any
sf::priv::err()error message, assertion failure, or lifetime tracking error. - Internally uses a vendored copy of
libbacktrace(Ian Lance Taylor): https://github.com/ianlancetaylor/libbacktrace. - Can be controlled via the CMake option
SFML_ENABLE_STACK_TRACES.
- Human-readable stack traces are generated on any
- ➡️ Changed testing framework from Catch to Doctest:
- Doctest used to be upstream SFML's testing framework as per my proposal.
- Doctest was changed to Catch2 in this PR SFML/SFML#2452 despite my objections.
- Doctest has almost feature-parity with Catch2 but an insanely better compilation time impact: https://github.com/doctest/doctest/.
- ➡️ Massive compilation time speedup:
- Thanks to copious use of PImpl and zero-allocation fast PImpl idioms, header hygiene, use of
SFML::Baseinstead of the Standard Library,extern template, and many other techniques, this fork now compiles blazingly-fast compared to upstream SFML.
- Thanks to copious use of PImpl and zero-allocation fast PImpl idioms, header hygiene, use of
- ➡️
sf::priv::err()enhancements:- Including
Err.hdoes not expose any expensiveiosoriostreamStandard Library header. - The end of a chain of streams is detected automatically, and a flush + newline is added at the end -- no need for
std::endl! - Stack trace support, see above.
- Including
- ➡️ Other various improvements:
- Optimize
sf::Shadersource loading performance by reading into thread-local vector. - Optimize rendering of
sf::Textwith outlines: now takes a single draw call compared to upstream SFML's two draw calls. sf::priv::Erris now thread-safe.- All factory functions have been improved to support RVO or NRVO, checked via GCC's
-Wnrvoflag. - Added
Vec2<T>::movedTowards(T r, Angle phi)function. sf::Vec2,sf::Vec3, andsf::Rect2are now aggregates.- Removed catch-all headers such as
SFML/Audio.hppto promote good header hygiene in user projects.
- Optimize