Skip to content

Conversation

@ccawley2011
Copy link
Contributor

@ccawley2011 ccawley2011 commented Sep 6, 2022

This extends the limited support for paletted textures in the DirectFB render driver to allow support to be added to other platforms, and to allow the palette to be changed after the texture has been created, which is useful in older games.

This is currently implemented in the Direct3D 9, OpenGL and OpenGL ES 2 drivers using shaders, but these currently don't work correctly when bilinear filtering is enabled. Fixing it would require using texture targets, but I'm not sure on the best way to implement this. A software-based fallback is also available, which doesn't have this problem.

testoverlay2 has been modified to make use of this feature, and another test program has been added to test palette rotation effects that may occur without updating the image data. I've also opened sergiou87/open-supaplex#41 to demonstrate this in a real-world application.

@icculus
Copy link
Collaborator

icculus commented Nov 23, 2022

I really don't want to get into paletted textures in the 2D API, fwiw; if there isn't a trivial way to upload them to a non-paletted format, we should add that as a generic thing, and let people that need this use a shader directly in the upcoming GPU API.

@ihhub
Copy link

ihhub commented Dec 26, 2022

I really don't want to get into paletted textures in the 2D API, fwiw; if there isn't a trivial way to upload them to a non-paletted format, we should add that as a generic thing, and let people that need this use a shader directly in the upcoming GPU API.

Hi @icculus , SDL 1 supports paletted textures and for some projects like fheroes2 it is important to have such ability. Right now we do a hack to convert from 8-bit images to 32-bit images which leads to 20% performance drop. This is a very significant reduction which affects not only the gameplay but eats more resources and battery on portable devices.

If you think about an easier way to add support for such feature then please explain it.

Copy link

@ihhub ihhub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ccawley2011, I roughly took a look at your changes and put several general comments here. Could you please take a look when you have time?

* \param ncolors the number of entries to modify
* \return 0 on success, or -1 if the texture is not valid.
*
* \since This function is available since SDL 2.0.22.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should update this comment due to new releases being online.

Comment on lines +34 to +37
/*
case SDL_PIXELFORMAT_INDEX1LSB:
case SDL_PIXELFORMAT_INDEX1MSB:
case SDL_PIXELFORMAT_INDEX4LSB:
case SDL_PIXELFORMAT_INDEX4MSB:
*/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this commented code to be present? If not then we can do:

if (format != SDL_PIXELFORMAT_INDEX8) {
    SDL_SetError("Unsupported palette format");
    return NULL;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to support 1-bit and 4-bit formats as well, I'm just not sure how locking should behave in these cases.

Copy link

@ihhub ihhub Dec 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this pull request is stuck for multiple months a good approach is to implement only for 1 format and add TODO comments in relevant places for other format. On top of this after this pull request is merged it would be wise to create an issue asking to add these missing formats.

return NULL;
}

swdata = (SDL_SW_PaletteTexture *) SDL_calloc(1, sizeof(*swdata));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this project's way of coding but a good practice is to use variable declaration with its initialization:

SDL_SW_PaletteTexture *swdata = (SDL_SW_PaletteTexture *) SDL_calloc(1, sizeof(*swdata));

This way we reduce the chance of uninitialized pointers being used in the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixed declarations and code is only possible in C99, while SDL uses C89.

src = pixels;
dst = (Uint8*)swdata->surface->pixels + rect->y * swdata->surface->pitch + rect->x;
length = rect->w;
for (row = 0; row < rect->h; ++row) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should initialize row within the loop as it would be easier to read and at the same time would be the same performance since this variable is still on stack.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only possible in C99.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Is there a restriction not to use C99?

Comment on lines 103 to 105
SDL_SW_LockPaletteTexture(SDL_SW_PaletteTexture * swdata, const SDL_Rect * rect,
void **pixels, int *pitch)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This place and other places in the code: please align pointers to have the same code style. For example, here we align them in the middle while in the include/SDL_render.h we align to left.


typedef struct SDL_SW_PaletteTexture SDL_SW_PaletteTexture;

SDL_SW_PaletteTexture *SDL_SW_CreatePaletteTexture(Uint32 format, int w, int h);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these functions miss Doxygen comments and also please add empty lines between them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doxygen comments don't generally seem to be used in internal headers such as this one.

}
}

/* vi: set ts=4 sw=4 expandtab: */
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Despite of fact this line is present in other files this is an editor specific thing which shouldn't exist in the generic source code. This is a developer responsibility to adjust editors per their needs.

@ihhub
Copy link

ihhub commented Dec 26, 2022

Also it would be important to know the target version of this change, whether this PR going to be a part of the source code or there are concerns about integrating it. If there are concerns what are they besides the fact of possible complexity of the code?

@ccawley2011
Copy link
Contributor Author

The main blocker for this PR is the fact that bilinear filtering still doesn't work with backends that implement this using shaders. Once that's done it should be ready, but I'm not sure that I'm capable of doing this myself. If anyone else is interested, then feel free to take over this PR.

If this is ever finished, I would quite like to get this into SDL2 as well as SDL3, but I don't know if new API calls are still being accepted there.

@ihhub
Copy link

ihhub commented Dec 26, 2022

Hi @ccawley2011 , bilinear interpolation cannot be done on indexed images. The only way to do it is to get values in RGB space and then find the nearest value in the palette to the result. If we think that RGB is a 3D space then it's a task of finding the smallest distance between 2 dots: the result value and existing palette value. The complexity of this algorithm is huge and bigger than to O(M*N). It would definitely be very slow.

@ccawley2011 ccawley2011 changed the base branch from main to SDL2 June 13, 2023 15:19
@glebm
Copy link
Contributor

glebm commented Dec 1, 2023

I've implemented bilinear downscaling for indexed images in the past here. That file has 2 functions, one does 32-bit scaling to arbitrary size, the other one downscales 8-bit indexed images by 50%.

Downscaling of 8-bit indexed images is achieved by passing a Uint8 paletteBlendingTable[256][256], created on palette changes, which is very slow (but we do it infrequently in DevilutionX). The table is created here.

@nlapinski
Copy link

You can handle the filtering in the shader method with some number of weighted lookups on the texture then blend. (I'm not sure this works for scaling down?).

#define SHADER_FRAGMENT_CUBIC                                   \
"varying vec4 v_color;\n"                                       \
"varying vec2 v_texCoord;\n"                                    \
"uniform sampler2D tex0;\n"                                     \
"\n"                                                            \
"const float pal[768] = float[768](\n"                          \
PAL_0                                                           \
");\n"                                                          \
"float cubic(float x) {\n"                                                                  \
"    x = abs(x);\n"                                                                         \
"    float x2 = x * x;\n"                                                                   \
"    float x3 = x2 * x;\n"                                                                  \
"    if (x <= 1.0) {\n"                                                                     \
"        return 1.0 - 2.0 * x2 + x3;\n"                                                     \
"    } else if (x < 2.0) {\n"                                                               \
"        return 4.0 - 8.0 * x + 5.0 * x2 - x3;\n"                                           \
"    } else {\n"                                                                            \
"        return 0.0;\n"                                                                     \
"    }\n"                                                                                   \
"}\n"                                                                                       \
"vec4 textureBicubic(sampler2D sampler, vec2 uv) {\n"                                       \
"    vec2 texSize = textureSize(sampler, 0);\n"                                             \
"    vec2 invTexSize = 1.0 / texSize;\n"                                                    \
"    vec2 f = fract(uv * texSize - 0.5);\n"                                                 \
"    vec2 p = floor(uv * texSize - 0.5);\n"                                                 \
"    vec4 sum = vec4(0.0);\n"                                                               \
"    for (int i = -1; i <= 2; ++i) {\n"                                                     \
"        for (int j = -1; j <= 2; ++j) {\n"                                                 \
"            vec2 index = p + vec2(i, j);\n"                                                \
"            float weight = cubic(f.x - float(i)) * cubic(f.y - float(j));\n"               \
"            float idx = texture2D(sampler, (index + 0.5) * invTexSize).r * 255.0;\n"       \
"            int colorIdx = int(idx) * 3;\n"                                                \
"            float alpha = colorIdx <= 2.0 ? 0.0 : 1.0;\n"                                  \
"            vec4 color = vec4(pal[colorIdx], pal[colorIdx+1], pal[colorIdx+2], alpha);\n"  \
"            sum += color * weight;\n"                                                      \
"        }\n"                                                                               \
"    }\n"                                                                                   \
"    return sum;\n"                                                                         \
"}\n"                                                                                       \
"void main() {\n"                                                                           \
"    vec4 color = textureBicubic(tex0, v_texCoord);\n"                                      \
"    gl_FragColor = color * v_color;\n"                                                     \
"}\n"

For sdl2 I think this is maybe too much to add since it adds a lot to each rendering backend implementation, new indexed texture formats and specific shaders. Any use of this would likely need some shader customization anyway ontop - which SDL2 is not built for.

I would rather see a generic data/byte type of image that can bind a generic/custom/utility shader to , something that could also be useful for tonemapping, luts etc. (thinking sdl3)

@slouken
Copy link
Collaborator

slouken commented Aug 6, 2024

This hasn't been updated in a while and we're cleaning house for SDL 3.0. Please feel free to reopen this if you'd like to clean it up and get it in!

@slouken slouken closed this Aug 6, 2024
@glebm
Copy link
Contributor

glebm commented Aug 31, 2025

The complexity of this algorithm is huge and bigger than to O(M*N). It would definitely be very slow.

Actually, you can map RGB to palette a lot faster using a kd-tree. Still slow of course and nobody should be doing that on every frame.

Still, if someone wants it, I have a heavily optimized RGB -> palette index kd-tree implementation here https://github.com/diasurgical/DevilutionX/blob/master/Source/utils/palette_kd_tree.hpp (C++). We use it to precompute a 256*256 lookup table that is used for blending two palette colors to another palette index. I don't think it should be in SDL though.

SDL1 did not allow for that, so I'm not sure why the current would need to do that either.
SDL's support for palettized textures limited only to what SDL1.2 supported seems totally fine.

@ccawley2011
Copy link
Contributor Author

IMHO, the best way to support filtering is to render the paletted texture to a render target without filtering, then render the render target with filtering. This is what ScummVM does with its OpenGL renderer, but I'm not sure how to accommodate that in SDL2/3.

@glebm What platforms do you need? It should be possible to implement this with just e.g. GU_PSM_T4/GU_PSM_T8 on PSP plus the software fallback and leave dealing with shader implementations for a separate PR, but I'd need assistance with testing it.

@slouken slouken reopened this Sep 26, 2025
@slouken slouken added this to the 3.4.0 milestone Sep 26, 2025
@slouken
Copy link
Collaborator

slouken commented Sep 26, 2025

I'm reopening this PR for 3.4, where it makes a lot of sense with the new pixelart scale mode. To resolve the linear filtering issue, I think we either only allow nearest / pixelart scale modes, or we automatically use pixelart when linear is requested for palettized textures.

@nlapinski
Copy link

I need to have another look at this, but I think just nearest filtering is a good general purpose solution? Any other filtering starts to make it a more complex issue.

slouken added a commit to slouken/SDL that referenced this pull request Sep 26, 2025
slouken added a commit to slouken/SDL that referenced this pull request Sep 26, 2025
slouken added a commit to slouken/SDL that referenced this pull request Sep 26, 2025
slouken added a commit to slouken/SDL that referenced this pull request Sep 26, 2025
slouken added a commit to slouken/SDL that referenced this pull request Sep 26, 2025
slouken added a commit to slouken/SDL that referenced this pull request Sep 26, 2025
slouken added a commit to slouken/SDL that referenced this pull request Sep 27, 2025
slouken added a commit to slouken/SDL that referenced this pull request Sep 27, 2025
slouken added a commit to slouken/SDL that referenced this pull request Sep 27, 2025
@slouken
Copy link
Collaborator

slouken commented Sep 29, 2025

I spent the weekend working on textures with palettes and now it's in! The API is very clean, one application palette maps to one GPU palette, which can be shared with any number of textures, and when you change the palette it automatically updates rendering for all textures that use it.

Enjoy!

slouken added a commit to slouken/SDL that referenced this pull request Sep 29, 2025
sulix added a commit to sulix/omnispeak that referenced this pull request Oct 9, 2025
SDL 3.4.0+ is introducing support for palettized textures (at last!):
libsdl-org/SDL@0b4b254
libsdl-org/SDL#6192

This allows us to do the palette resolution on the GPU (or at least, in the
SDL library), instead of manually doing it ourselves, putting the SDL3 render
backend on par with the OpenGL/Vulkan/SDL3_GPU backends.

Further work will explore using the 'pixel art' scaling mode.

Note that building with SDL 3.4.0+ will require SDL 3.4.0+ at runtime.

In the process, fix a few small leaks/errors, too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants