Texture only shows briefly when closing the SDL window (SDL3 MacOS)

Hello,

I am following LazyFoo’s tutorial on using textures, as far as I can see my setup is correct, but the loaded texture only shows for like one frame when I close the window, and not the whole time the window is open.

I’m using macOS Tahoe 26.3, SDL3, screen resolution 2560x1440 144hz.

This the my main.cpp:

#include <print>

#include <SDL3/SDL.h>

#include "main.h"
#include "texture.h"

constexpr int screenWidth {1024};
constexpr int screenHeight {768};

constexpr int renderWidth {320};
constexpr int renderHeight {180};

auto title = "First Game";

SDL_Window *window {nullptr};
SDL_Renderer *renderer {nullptr};
Texture texture;

void close() {
    SDL_DestroyWindow(window);
    SDL_DestroyRenderer(renderer);
    window = nullptr;
    renderer = nullptr;
    SDL_Quit();
}

int main() {

    if (!SDL_Init(SDL_INIT_VIDEO)) {
        std::println("SDL could not initialize! SDL Error: {}", SDL_GetError());
        return 1;
    }

    if (!SDL_CreateWindowAndRenderer(title, screenWidth, screenHeight, SDL_WINDOW_RESIZABLE, &window, &renderer)) {
        std::println("Failed to create window and renderer! SDL_Error: {}", SDL_GetError());
        return 1;
    }

    // Scale the window resolution to a render resolution of 320x180
    if (!SDL_SetRenderLogicalPresentation(renderer, renderWidth, renderHeight, SDL_LOGICAL_PRESENTATION_LETTERBOX)) {
        std::println("Setting Render logical presentation failed! SDL_Error: {}\n", SDL_GetError());
        return 1;
    }

    if (!SDL_SetRenderVSync(renderer, 1)) { // 0 disabled, 1 enabled
        std::println("Could not set VSYNC! SDL_Error: {}", SDL_GetError());
        return 1;
    }

    if (!texture.loadFromFile(*renderer, "../../images/loaded.png")) {
        std::println("Failed to load media! SDL Error: {}", SDL_GetError());
        return 1;
    }

    bool quit {false};

    SDL_Event event;
    SDL_zero(event);

    while (!quit) {
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_EVENT_QUIT)
                quit = true;
        }
    }

    SDL_SetRenderTarget(renderer, nullptr);
    SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); // white
    SDL_RenderClear(renderer);

    texture.render(*renderer ,0.f, 0.f);

    SDL_RenderPresent(renderer); // only call once per frame

    close();

    return 0;
}

texture.cpp:

#include "texture.h"

#include <print>

#include "SDL3_image/SDL_image.h"

Texture::Texture()
    : mTexture{nullptr}, mWidth{0}, mHeight{0}
{}

Texture::~Texture() {
    destroy();
}

bool Texture::loadFromFile(SDL_Renderer& renderer, const std::string& path) {
    destroy();

    SDL_Surface* loadedSurface = IMG_Load(path.c_str());
    if (loadedSurface == nullptr)
    {
        std::println("Could not load surface '{}'", SDL_GetError());
        return false;
    }

    if (mTexture = SDL_CreateTextureFromSurface(&renderer, loadedSurface); mTexture == nullptr) {
        std::println("Could not create texture from surface '{}'", SDL_GetError());
        return false;
    }

    mWidth = loadedSurface->w;
    mHeight = loadedSurface->h;

    SDL_DestroySurface(loadedSurface);

    return mTexture;
}

void Texture::destroy() {
    SDL_DestroyTexture(mTexture);
    mTexture = nullptr;
    mWidth = 0;
    mHeight = 0;
}

void Texture::render(SDL_Renderer& renderer, float x, float y) const {
    SDL_FRect dstRect{x, y, static_cast<float>(mWidth), static_cast<float>(mHeight)};
    SDL_RenderTexture(&renderer, mTexture, nullptr, &dstRect);
}

int Texture::getWidth() const {
    return mWidth;
}

int Texture::getHeight() const {
    return mHeight;
}

bool Texture::isLoaded() const {
    return mTexture != nullptr;
}

Setting Vsync to 0 or 1 in the Init phase does not seem to make a difference either.

Using SDL_SetRenderTarget(renderer, nullptr); or not also didn’t make a difference.

Does anyone know what could cause this?

Thanks in advance for any insights!

Hi, The program does not work as expected because the refresh loop (the main game loop) is incorrectly positioned in the code. In an application using SDL, you must always have a main loop (often called a game loop) that continuously does three things:

  • Read events (keyboard, mouse, window closure, etc.)
  • Update the program status
  • Draw the image on the screen
  • Display the rendering

In your code, the main loop is present.

while (!quit) {
    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_EVENT_QUIT)
            quit = true;
    }
}

But the problem is that the rendering (the drawing on the screen) is placed after this loop.

The rendering code must be placed inside the main loop, like this:

#include <SDL3/SDL.h>
#include <format>
#include <iostream>
#include <string>
#include <map>
#include <memory>
#include "Texture.hpp"

constexpr int screenWidth{720};
constexpr int screenHeight{680};

constexpr int renderWidth{320};
constexpr int renderHeight{180};

auto title = "First Game";

SDL_Window* window{nullptr};
SDL_Renderer* renderer{nullptr};
std::map<std::string, std::shared_ptr<Texture>> textures;

bool init() {
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        std::cerr << std::format("SDL could not initialize! SDL Error: {}\n", SDL_GetError());
        return false;
    }

    if (!SDL_CreateWindowAndRenderer(title, screenWidth, screenHeight,
                                     SDL_WINDOW_RESIZABLE,
                                     &window, &renderer)) {
        std::cerr << std::format("Failed to create window and renderer! SDL_Error: {}\n", SDL_GetError());
        return false;
    }

    if (!SDL_SetRenderVSync(renderer, 1)) {
        std::cerr << std::format("Could not set VSYNC! SDL_Error: {}\n", SDL_GetError());
        return false;
    }

    return true;
}

bool load() {
    const char* basePathC = SDL_GetBasePath();
    if (!basePathC) {
        std::cerr << std::format("Failed to get base path! SDL_Error: {}\n", SDL_GetError());
        return false;
    }

    std::string basePath{basePathC};

    textures["red_sandstone"] =
        std::make_shared<Texture>(basePath + "assets/red_sandstone.png");

    textures["stone"] =
        std::make_shared<Texture>(basePath + "assets/stone.png");

    for (auto& [name, texture] : textures) {
        if (!texture || !texture->load(*renderer)) {
            std::cerr << std::format("Failed to load texture '{}'. SDL Error: {}\n",
                                     name, SDL_GetError());
            return false;
        }
    }

    return true;
}

void loop() {
    bool quit{false};
    SDL_Event event;

    while (!quit) {
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_EVENT_QUIT)
                quit = true;
        }

        SDL_SetRenderDrawColorFloat(renderer, 0.1f, 0.1f, 0.1f, 1.0f);
        SDL_RenderClear(renderer);

        if (textures.contains("stone"))
            textures["stone"]->render(*renderer, 0.0f, 0.0f, 2.0f);

        if (textures.contains("red_sandstone"))
            textures["red_sandstone"]->render(*renderer, 64.0f, 0.0f, 2.0f);


        SDL_SetRenderScale(renderer, 4.0f, 4.0f);
        SDL_SetRenderDrawColorFloat(renderer, 0.9f, 0.9f, 0.9f, 1.0f);
        SDL_RenderDebugText(renderer, 0, 64, "HELLO!");
        SDL_SetRenderScale(renderer, 1.0f, 1.0f);

        SDL_RenderPresent(renderer);
    }
}

void shutdown() {
    // Resources (SDL_Texture, SDL_Renderer, SDL_window, SDL) are released in the reverse order in which they were created.
    for (auto& [name, texture] : textures) {
        if (texture) {
            texture->destroy();
            texture.reset();
        }
    }
    textures.clear();

    if (renderer) SDL_DestroyRenderer(renderer);
    if (window) SDL_DestroyWindow(window);

    renderer = nullptr;
    window = nullptr;

    SDL_Quit();
}

int main() {
    if (!init())
        return 1;

    if (!load()) {
        shutdown();
        return 1;
    }

    loop();
    shutdown();

    return 0;
}

Texture.hpp

#pragma once

#include <SDL3/SDL.h>
#include <string>

class Texture {
public:
    Texture(const std::string& path);

    ~Texture();

    bool load(SDL_Renderer& renderer);

    void destroy();

    void render(SDL_Renderer& renderer, float x, float y, float scale) const;

    int getWidth() const;

    int getHeight() const;

    bool isLoaded() const;

private:
    std::string mPath;
    SDL_Texture *mTexture;
    int mWidth, mHeight;
};

Texture.cpp

#include "Texture.hpp"
#include <SDL3_image/SDL_image.h>
#include <format>
#include <iostream>
#include <string>

Texture::Texture(const std::string& path)
    : mPath(path), mTexture{nullptr}, mWidth{0}, mHeight{0}
{}

Texture::~Texture() {
    destroy();
}

bool Texture::load(SDL_Renderer& renderer) {
    destroy();

    SDL_Surface* loadedSurface = IMG_Load(mPath.c_str());
    if (loadedSurface == nullptr) {
        std::cerr << std::format("Could not load surface '{}'\n", SDL_GetError());
        return false;
    }

    if (mTexture = SDL_CreateTextureFromSurface(&renderer, loadedSurface); mTexture == nullptr) {
        std::cerr << std::format("Could not create texture from surface '{}'\n", SDL_GetError());
        return false;
    }

    mWidth = loadedSurface->w;
    mHeight = loadedSurface->h;

    SDL_DestroySurface(loadedSurface);

    return mTexture;
}

void Texture::destroy() {
    if (mTexture) SDL_DestroyTexture(mTexture);
    mTexture = nullptr;
    mWidth = 0;
    mHeight = 0;
}

void Texture::render(SDL_Renderer& renderer, float x, float y, float scale) const {
    SDL_FRect dstRect{x, y, static_cast<float>(mWidth)*scale, static_cast<float>(mHeight)*scale};
    SDL_RenderTexture(&renderer, mTexture, nullptr, &dstRect);
}

int Texture::getWidth() const {
    return mWidth;
}

int Texture::getHeight() const {
    return mHeight;
}

bool Texture::isLoaded() const {
    return mTexture != nullptr;
}

Good luck :slight_smile:

Thanks so much for your answer! :slight_smile: After staring at it for a couple hours, I ran out of ideas as to what could cause it :smiley:

And thanks for the slightly different code snippet example, some useful little things in there :slight_smile:

Thanks