c++sdlsdl-2

Created textures break in SDL2 when resizing a window


I'm trying to create and render a large number of textures in a window with a SDL_WINDOW_RESIZABLE flag, but when I'm resizing it, some textures sometimes break. I haven't noticed this issue if I'm creating a texture in some other way, for example, by rendering a glyph and converting it to a texture. Here's full code:

#include <iostream>
#include <string>
#include <SDL.h>
#include <vector>

SDL_Texture* generateColoredTexture(SDL_Renderer *renderer_, int w_, int h_, const SDL_Color &col_)
{
    auto *tex = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, w_, h_);
    SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);

    auto oldTar = SDL_GetRenderTarget(renderer_);
    SDL_SetRenderTarget(renderer_, tex);

    SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0);
    SDL_RenderClear(renderer_);

    SDL_SetRenderDrawColor(renderer_, col_.r, col_.g, col_.b, col_.a);
    SDL_Rect dst = {4, 4, w_ - 8, h_ - 8};
    SDL_RenderFillRect(renderer_, &dst);

    SDL_SetRenderTarget(renderer_, oldTar);
    return tex;
}

int main(int argc, char* args[])
{    
    if (SDL_Init(SDL_INIT_EVERYTHING) < 0)
    {
        std::cout << "SDL initialization error: " << SDL_GetError() << std::endl;
        return -1;
    }
    
    auto window = SDL_CreateWindow("TestWin", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1280, 720, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
    if (window == nullptr)
    {
        std::cout << "Window creation error: " << SDL_GetError() << std::endl;
        return -3;
    }

    auto renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE);
    if (renderer == nullptr) {
        std::cout << "Renderer creation error: " << SDL_GetError() << std::endl;
        return -4;
    }

    std::vector<SDL_Texture*> texs;
    for (int i = 0; i < 53; ++i)
    {
        texs.push_back(generateColoredTexture(renderer, 50, 50, {uint8_t(rand() % 255), uint8_t(rand() % 255), uint8_t(rand() % 255), 255}));
    }
    
    std::vector<std::vector<int>> cells;
    for (int y = 0; y < 10; ++y)
    {
        cells.push_back({});
        for (int x = 0; x < 20; ++x)
        {
            cells.back().push_back(rand() % texs.size());
        }
    }

    bool running = true;
    SDL_Event e;

    while (running)
    {
        while( SDL_PollEvent( &e ) != 0 )
        {
            if( e.type == SDL_QUIT )
            {
                running = false;
            }
        }

        SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
        SDL_RenderClear(renderer);

        for (int y = 0; y < cells.size(); ++y)
        {
            for (int x = 0; x < cells[y].size(); ++x)
            {
                SDL_Rect dstrect{x * 50, y * 50, 50, 50};
                SDL_RenderCopy(renderer, texs[cells[y][x]], NULL, &dstrect);
            }
        }

        SDL_RenderPresent(renderer);
    }

    for (auto *tex : texs)
    {
        SDL_DestroyTexture(tex);
    }
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

Video of this issue. I haven't found any notes about created textures or window resizing on sdl wiki. I also tried to catch SDL_WINDOWEVENT_EXPOSED event, but there was none. What causes this issue and is there any solution?

EDIT. I tried to build this code on linux (EndeavourOS, 6.9.3-arch1-1, plasma 6.0.5) and haven't noticed any issues there so I guess its windows-specific


Solution

  • This problem is commonly refered to as "device loss". It was quite common when switching to fullscreen mode (in past times where fullscreen didn't mean "fullscreen window without borders" but actual exclusive fullscreen with video mode change) with Direct3D. D3D provides different flags for texture usage, some of which are automatically restored on device loss, and some are not.

    SDL does something similar,automatically recreating all non-rendertarget textures on device loss. See https://github.com/libsdl-org/SDL/blob/5d1ca1c8c744c51c1d4046b8a10d37f2aea79d0b/src/render/direct3d/SDL_render_d3d.c#L1481 . But for render targets it can't do that effectively (as content of render target could change very frequently, it is impractical to read contents of render target back to system memory just to be able to restore it if device is ever lost), so it gives you special event SDL_RENDER_TARGETS_RESET. Upon that event, your application could rebuild content of render target, or recreate new render target of different size if that is requried to function correctly.

    This is not a problem in OpenGL as GL driver itself handles device loss and for your application it seems device is never actually lost (but with GL ES on mobile devices it could be lost; it is only about desktop systems).

    So, as I see it, you have 3 options:

    1. use SDL's OpenGL rendering backend, e.g. issue
    SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl");
    

    before creating renderer. This is probably not a great solution but it should work.

    1. refresh contents of your render textures when you get SDL_RENDER_TARGETS_RESET event. If your texture creation code contains some randomness, you'll have to save random seed to be able to recreate exactly the same content.

    2. if contents of texture doesn't change, after preparing contents of your render texture, convert it to static texture. E.g. something like:

    // ... render to RT texture
    Uint32 format;
    SDL_QueryTexture(render_texture, &format, NULL, NULL, NULL);
    
    SDL_Texture *static_texture = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_STATIC, w, h);
    SDL_SetTextureBlendMode(static_texture, SDL_BLENDMODE_BLEND);
    // in case render_texture and static_texture formats are actually different
    SDL_QueryTexture(static_texture, &format, NULL, NULL, NULL);
    
    const int pitch = w * 4;  // presume 4 as we requested RGBA8888
    void *pixels = malloc(h * pitch);
    // read from current RT (render_texture)
    SDL_RenderReadPixels(renderer, NULL, format, pixels, pitch);
    SDL_UpdateTexture(static_texture, NULL, pixels, pitch);
    free(pixels);