SDL blending logic is unclear

Hi there.

I’m pretty new to SDL and to C++ in general so please don’t judge.
Here’s the code I’m confused about. Questions re blending logic are in comments.

Thanks.

#include <SDL3/SDL.h>
#include <cstdint>
#include <cstdlib>
#include <stdlib.h>

static inline constexpr int width{640};
static inline constexpr int height{480};
static inline constexpr uint8_t bytesPerPixel{sizeof(uint32_t)};
static inline constexpr int pitch{width * bytesPerPixel};

int main(int argc, char *args[])
{
    SDL_Window *window{nullptr};
    SDL_Renderer *renderer{nullptr};
    SDL_Texture *texture1{nullptr};
    SDL_Texture *texture2{nullptr};
    uint32_t *pixelData{nullptr};
    int nonConstPitchCopy = pitch;

    SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0");
    SDL_Init(SDL_INIT_VIDEO);
    SDL_CreateWindowAndRenderer("SDL3 Tutorial: Hello SDL3!!!", width, height, 0, &window, &renderer);
    SDL_SetRenderVSync(renderer, 1);
    texture1 = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, width, height);
    texture2 = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, width, height);

    // Commenting out SDL_SetRenderDrawColor and SDL_RenderClear is the same as set transparent black color. Is memory
    // zeroed by default?
    SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
    SDL_RenderClear(renderer);

    SDL_LockTexture(texture1, NULL, (void **)&pixelData, &nonConstPitchCopy);
    for (int i = 0; i < width * height; ++i)
    {
        // If alpha is not opaque, then the texture1 color is blended with the color used for SDL_RenderClear
        // unless SDL_SetTextureBlendMode(texture1, SDL_BLENDMODE_NONE) is called. Why?
        pixelData[i] = 0xFF000080;
    }
    SDL_UnlockTexture(texture1);
    pixelData = nullptr;
    SDL_RenderTexture(renderer, texture1, NULL, NULL);

    SDL_LockTexture(texture2, NULL, (void **)&pixelData, &nonConstPitchCopy);
    for (int i = 0; i < width * height; ++i)
    {
        // If alpha is not opaque, then the texture2 color is blended with the color used for SDL_RenderClear
        // but not with the texture1 color unless SDL_SetTextureAlphaMod is called with non-opaque aplha for the
        // texture2. Why?
        pixelData[i] = 0x00FF0080;
    }
    SDL_UnlockTexture(texture2);
    // This call with non-opaque aplha is needed to actually blend texture1 and texture2 colors.
    // Why is it even when texture2 pixels aplha is non-opaque?
    SDL_SetTextureAlphaMod(texture2, 0x80);
    pixelData = nullptr;
    SDL_RenderTexture(renderer, texture2, NULL, NULL);

    SDL_RenderPresent(renderer);

    bool quit{false};
    SDL_Event e;
    SDL_zero(e);
    while (quit == false)
    {
        SDL_WaitEvent(&e);
        if (e.type == SDL_EVENT_QUIT)
        {
            quit = true;
        }
    }

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    renderer = nullptr;
    window = nullptr;
    texture1 = nullptr;
    texture2 = nullptr;
    SDL_Quit();

    return 0;
}

I’m not really sure what results you’re trying to get, but a couple things I noticed:

  1. You’re using SDL_PIXELFORMAT_RGBA8888 on a little-endian computer, which means the byte order in memory is going to be ABGR. There are various reasons behind this, but basically with SDL pixel format enums think of them as being in logical order, not the physical byte order in memory. If you want the bytes to be RGBA in memory use SDL_PIXELFORMAT_RGBA32 which will do what you expect.

  2. You’re also hand-coding pixel values, which can lead to getting the byte order wrong. Use SDL_MapRGBA(). In your code you’ve specified 0xFF000080, which is stored in memory as 0x80 0x00 0x00 0xFF, and since you’ve told SDL your texture’s bytes are laid out in ABGR order (see #1) the alpha channel’s value is 0x80.

  3. To be clear, alpha blending blends what you’re drawing with whatever was already there. In your specific case that’s the background color, but it isn’t like your texture is specifically being blended with what you passed to SDL_RenderClear().

  4. SDL_SetTextureAlphaMod() changes the alpha value passed to the GPU in the vertex attributes. This way you can make a texture more transparent without having to touch the pixels directly. For instance, if you wanted to make a sprite fade away you could call this function with different values over the course of several frames.

This code draws transparent rectangles on top of each other:

#include <SDL3/SDL.h>
#include <cstdint>
#include <cstdlib>
#include <stdlib.h>

static inline constexpr int width{640};
static inline constexpr int height{480};
static inline constexpr int texWidth{320};
static inline constexpr int texHeight{240};
static inline constexpr uint8_t bytesPerPixel{sizeof(uint32_t)};
static inline constexpr int pitch{width * bytesPerPixel};

int main(int argc, char *args[])
{
    SDL_Window *window{nullptr};
    SDL_Renderer *renderer{nullptr};
	SDL_Texture *texture1{nullptr};
	SDL_Texture *texture2{nullptr};
    uint32_t *pixelData{nullptr};
    int nonConstPitchCopy = pitch;

	const SDL_PixelFormatDetails *pixelFormatDetails = SDL_GetPixelFormatDetails(SDL_PIXELFORMAT_RGBA32);
	const uint32_t DULLRED = SDL_MapRGBA(pixelFormatDetails, nullptr, 0x80, 0x00, 0x00, 0xFF);
	const uint32_t DULLGREEN = SDL_MapRGBA(pixelFormatDetails, nullptr, 0x00, 0x80, 0x00, 0xFF);
	const uint32_t RED_TRANSPARENT = SDL_MapRGBA(pixelFormatDetails, nullptr, 0xFF, 0x00, 0x00, 0x80);
	const uint32_t GREEN_TRANSPARENT = SDL_MapRGBA(pixelFormatDetails, nullptr, 0x00, 0xFF, 0x00, 0x80);

    SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0");
    SDL_Init(SDL_INIT_VIDEO);
    SDL_CreateWindowAndRenderer("SDL3 Tutorial: Hello SDL3!!!", width, height, 0, &window, &renderer);
    SDL_SetRenderVSync(renderer, 1);

	// We're changing the pixel format to RGBA32 to get the in-memory byte order to be [R G B A]
	texture1 = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, texWidth, texHeight);
	texture2 = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, texWidth, texHeight);

    SDL_LockTexture(texture1, nullptr, (void **)&pixelData, &nonConstPitchCopy);
	SDL_memset4(pixelData, RED_TRANSPARENT, texHeight * (nonConstPitchCopy / sizeof(uint32_t)));
    SDL_UnlockTexture(texture1);

    SDL_LockTexture(texture2, nullptr, (void **)&pixelData, &nonConstPitchCopy);
	SDL_memset4(pixelData, GREEN_TRANSPARENT, texHeight * (nonConstPitchCopy / sizeof(uint32_t)));
    SDL_UnlockTexture(texture2);

    bool quit = false;
	uint8_t value = 255;		// an extra bit of fun, to make the transparent colors more apparent
	while(!quit) {
		SDL_Event event;
		while(SDL_PollEvent(&event)) {
			switch(event.type) {
			case SDL_EVENT_QUIT:
				quit = true;
				break;
			}
		}

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

		// We are going to draw the textures side-by-side in the top half of the screen,
		// then overlapping each other in the bottom half
		const SDL_FRect left = { 0.0f, 0.0f, (float)texWidth, (float)texHeight };
		const SDL_FRect right = { (float)texWidth, 0.0f, (float)texWidth, (float)texHeight };
		const SDL_FRect bleft = { (float)texWidth / 3.0f, (float)texHeight + 10.0f, (float)texWidth, (float)texHeight };
		const SDL_FRect bright = { bleft.x + (float)texWidth / 3.0f, (float)texHeight + 10.0f, (float)texWidth, (float)texHeight };

		SDL_RenderTexture(renderer, texture1, nullptr, &left);
		SDL_RenderTexture(renderer, texture2, nullptr, &right);
		SDL_RenderTexture(renderer, texture1, nullptr, &bleft);
		SDL_RenderTexture(renderer, texture2, nullptr, &bright);

		SDL_RenderPresent(renderer);

		value--;		// integer underflow keeps us warm and safe when the wind blows
	}

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}
2 Likes

Thanks @sjr for mentioning endianness. I’ve simply taken the pixel format from numerous tutorials, but now I understand that I have to read more about this and about pixel formats. At least, this gives me a clue why I get non-expected colors when I use 3rd-party libs to write to pixel data.

Nevertheless, some questions are still not clear for me. Let me re-phrase them.

  1. If I don’t care about background color and if I don’t want to blend anything with any background, then calling of SDL_SetRenderDrawColor/SDL_RenderClear is not mandatory at all, right? When I render an opaque texture with no blending, everything that was there before will be just, let’s say, replaced, right?
  2. In the code above, texture1 has non-opaque alpha and is blended with the white background that’s expected and logical, and this happens without calling of SDL_SetTextureAlphaMod for the texture1. But then, if SDL_SetTextureAlphaMod is NOT called for the texture2, then it’s blended with the white background too, not with the texture1, the texture1 is simply not taken into account like it didn’t exist at all. And if SDL_SetTextureAlphaMod is called for the texture2, then it’s blended with the texture1 but in this case it looks like the white background is not taken into account in turn. So, I’d like to understand what’s the right way to blend multiple textures.
  1. Correct. Be aware that on some mobile devices, clearing the screen is a hint to the driver that it doesn’t need to load the framebuffer into the tile cache, so it’ll be faster (clearing the screen is done in hardware by the GPU, not by the CPU). Either way, for a 2D game it probably won’t be a huge speedup.

  2. If you’re drawing a transparent texture over the top of another texture, and the bottom texture isn’t blended into the top texture, something is wrong. Did you run the code I posted? The second texture overlaps the first and is correctly blended with it.

1 Like

@sjr , I’ve tried your code, it works perfectly fine.
My mistake was to operate pixel data directly as I saw in some tutorials and code snippets. After switching to SDL_MapRGBA and SDL_memset4 my code works as expected too. Thanks for both hints and working example.

And I understand now that passing pixel data directly to 3rd-party vg libs (again, hello to certain tutorials authors) without any kind of integration layer is not a good idea either.

I also tried the code and found that DULLRED and DULLGREEN are not used.

Yes, they were just there in case I misinterpreted what the OP was trying to do.

1 Like

In the case of SDL_memset4, do not assume that the pixel data of the image forms a single, contiguous block of memory, as it can vary between graphics drivers and textures. Instead, always use the pitch returned.

1 Like

The sample code I provided uses the pitch.

Oh! I thought a caller specifies a pitch, but it looks like pitch is a kind of out param, right? If it is, it would be nice to specify this somehow in API docs. For ex., if I understand correctly, there are both a return value (return code) and two out parameters that’s not immediately clear, as for me.

If it wasn’t an out parameter there would have been no need to pass it by pointer (especially not a non-const one).