We are currently migrating Bugzilla to GitHub issues.
Any changes made to the bug tracker now will be lost, so please do not post new bugs or make changes to them.
When we're done, all bug URLs will redirect to their equivalent location on the new bug tracker.

Bug 3023

Summary: setting a white and then non-white texture color mod breaks the texture with software renderer
Product: SDL Reporter: Adam M. <adam>
Component: renderAssignee: Sam Lantinga <slouken>
Status: RESOLVED FIXED QA Contact: Sam Lantinga <slouken>
Severity: normal    
Priority: P2    
Version: 2.0.3   
Hardware: x86_64   
OS: Windows 7   
Attachments: proposed patch (v1)

Description Adam M. 2015-06-18 23:52:12 UTC
With the software renderer, if you have a texture with an alpha channel and you set a white texture color mod, draw the texture, and then set a non-white texture color mod, the texture become brokens. It silently fails whenever you try to draw it in the future.

If the texture has blending disabled, it works. If you only uses white or non-white color mods, it works. If you go from a white color mod to a non-white color mod, it breaks.

// create a software renderer
pRenderer = SDL_CreateRenderer(pWindow, -1, SDL_RENDERER_SOFTWARE);

// create a texture with an alpha channel and enable blending
pTexture = SDL_CreateTexture(pRenderer, SDL_PIXELFORMAT_BGRA8888, SDL_TEXTUREACCESS_STATIC, width, height);
SDL_SetTextureBlendMode(pTexture, SDL_BLENDMODE_BLEND);
SDL_UpdateTexture(pTexture, NULL, ...); // fill it

// set a white color mod and draw it
SDL_SetTextureColorMod(pTexture, 255, 255, 255);
SDL_RenderCopy(pRenderer, pTexture, &srect, &drect); // this works

// set a non-white color mod and try to draw it again
SDL_SetTextureColorMod(pTexture, 255, 0, 0); // enable color mod
SDL_RenderCopy(pRenderer, pTexture, &srect, &drect); // fails silently

// now the texture can't be drawn again
Comment 1 Adam M. 2015-06-19 16:58:57 UTC
(You do have to actually do a texture draw between the two color mod sets.)
Comment 2 Adam M. 2015-06-19 17:29:02 UTC
At a first glance, the problem seems to be caused by incorrect RLE decoding. Blitting with a white color mod causes the texture to be converted to RLE by the software renderer. Then, blitting with a non-white color mod causes the texture to be converted back from RLE, but when that happens the alpha channel is not reconstituted. The colors of the pixels are presumably correct, but the alpha channel is zero (e.g. a pixel of 0xFFFFFFFF gets converted into 0x00FFFFFF after the normal -> RLE -> unRLE conversions).

This causes the texture to become invisible.
Comment 3 Adam M. 2015-06-19 18:22:48 UTC
Okay, here is the problem, I think.

During the first blit, RLEAlphaSurface is called to do RLE conversion of the RGBA source into a format allowing it "to be quickly alpha-blittable onto dest". Since the destination is the screen, it has no alpha channel. RLEAlphaSurface calls copy_opaque(dst, src + runstart, len, sf, df) (where copy_opaque is copy_32), which has this code:

SDL_RLEaccel.c:984:
  RGBA_FROM_8888(*src, sfmt, r, g, b, a);
  PIXEL_FROM_RGBA(*d, dfmt, r, g, b, a);

On the first line, it reads the source pixel 0xFFFFFFFF. The second line drops the alpha value (because dfmt for the screen has no alpha channel) and writes 0x00FFFFFF. Later, when the RLE conversion is being undone, uncopy_32 is called, which has the following code:

SDL_RLEaccel.c:1001:
  Uint32 pixel = *s++;
  RGB_FROM_PIXEL(pixel, sfmt, r, g, b);
  a = pixel >> 24;
  PIXEL_FROM_RGBA(*dst, dfmt, r, g, b, a);

However, the the alpha channel has already been dropped by copy_opaque (= copy_32), so pixel = 0x00FFFFFF and 'a' becomes 0. Thus, all opaque pixels lose their alpha channel when being unRLE'd. (I don't know what happens to pixels with alpha from 1-254, but they should be checked too.)

So, that seems to be the problem, but I'm not sure what the solution should be. Since opaque pixels have alpha == 255, I'm thinking to create another uncopy function for opaque pixels that simply uses 255 for alpha.

However, there may be other problems here. For translucent pixels, uncopy_32 assumes the alpha channel is stored in the upper 8 bits, but copy_32 doesn't store it there. Instead, it stores it in whatever location is appropriate for the destination surface. Isn't one of their behaviors incorrect, given the other? I'm not sure which to change, however.
Comment 4 Adam M. 2015-06-19 18:35:27 UTC
For translucent pixels, it seems that the blit function uses do_blend, which is the BLIT_TRANSL_888 macro, which also assumes alpha is in top 8 bits. It has the comment "we have made sure the alpha is stored in the top 8 bits...", but it seems that's not true (copy_32 doesn't make sure the alpha goes there).

Perhaps the correct fix is to make copy_32 put the alpha there, but then that seems to require that RLE conversion be limited to destination surfaces that don't use the upper 8 bits. However, looking further, it seems that has already been done: if (masksum != 0x00ffffff) return -1; /* requires unused high byte */
Comment 5 Adam M. 2015-06-19 19:01:42 UTC
Created attachment 2188 [details]
proposed patch (v1)

I've attached a patch that fixes the problem in my scenario. Please check it and make sure that if there are any other problems related to the original bug that they are fixed also.
Comment 6 Sam Lantinga 2015-06-20 05:26:43 UTC
I'm tempted to undo your last patch and keep the original surface around so that we can use RLE when it's fast, and use non-RLE code paths when needed without the overhead of undoing RLE encoding.

Thoughts?
Comment 7 Sam Lantinga 2015-06-20 05:27:07 UTC
For future reference, the patch that would be affected by this is:
https://hg.libsdl.org/SDL/rev/2f5a57f86e24
Comment 8 Sam Lantinga 2015-06-20 06:13:05 UTC
I must be missing something. copy_32 drops the alpha when copying to a surface that doesn't have an alpha channel, but then BLIT_TRANSL_888() assumes that the alpha is in the top 8 bits.

I just ran a test where I tried RLE transparent blit to a surface without an alpha channel, and sure enough every pixel was untouched because the alpha was 0.

Your patch fixes that, and I think is the correct fix, so I'm going to apply it.
https://hg.libsdl.org/SDL/rev/fd7fe355f173

Thanks!
Comment 9 Sam Lantinga 2015-06-20 06:13:59 UTC
This bug is now fixed.
Comment 10 Adam M. 2015-06-20 08:00:38 UTC
> I'm tempted to undo your last patch and keep the original surface around so
> that we can use RLE when it's fast, and use non-RLE code paths when needed
> without the overhead of undoing RLE encoding.
> 
> Thoughts?
Since it would use almost double the memory, I guess it depends on just how much faster RLE is.

It seems like it primarily benefits surfaces that are almost entirely opaque or transparent, and that have a lot of empty space. Surfaces with little or no transparency would probably be faster with the regular blitters than the more complex RLE blitter, if the destination surface has a compatible format. I'm guessing that the speedup for textures full of translucent pixels, if any, is not that great, but I haven't looked at that code path much.

So I would actually implement it this way: When a static texture is updated, scan the new image and see whether it would actually benefit from RLE encoding. If so, enable RLE. Otherwise, disable it.
Comment 11 Adam M. 2015-06-20 08:04:37 UTC
(That would be in addition to reverting my patch that disables RLE sometimes.)
Comment 12 Adam M. 2015-06-20 08:05:33 UTC
(Arg. And in addition to keeping the original surface around, I meant to say.)