SDL icon indicating copy to clipboard operation
SDL copied to clipboard

Sub-pixel rendering issue in SDL3

Open sbhutch opened this issue 2 years ago • 3 comments

Sub-pixel rendering does not seem to be working as expected in SDL3. The following application sets a logical presentation that is 4x smaller than the window and renders a rectangle moving at 1-pixel per second. In SDL2 the rectangle appears to move smoothly across the screen, but in SDL3 the rectangle visibly jumps 4 pixels at a time. This can be seen in the attached video.

https://github.com/libsdl-org/SDL/assets/102490574/b5ad48a6-1aa0-47ed-b462-46755b4c025b

The issue persists regardless of the logical presentation or scaling modes used on Mac (M1) and Linux.

Code to reproduce:

#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

static SDL_Window* window;
static SDL_Renderer* renderer;

static SDL_FRect rect;
static Uint64 prev_ticks;

int SDL_AppInit(int argc, char** argv)
{
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
        SDL_Log("%s", SDL_GetError());
        return -1;
    }

    if (SDL_CreateWindowAndRenderer(200, 200, 0, &window, &renderer)) {
        SDL_Log("%s", SDL_GetError());
        return -1;
    }

    if (SDL_SetRenderLogicalPresentation(renderer, 50, 50, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE, SDL_SCALEMODE_NEAREST)) {
        SDL_Log("%s", SDL_GetError());
        return -1;
    }

    if (SDL_SetRenderVSync(renderer, 1)) {
        SDL_Log("%s", SDL_GetError());
        return -1;
    }

    rect = (SDL_FRect) { 0.0f, 20.0f, 10.0f, 10.0f };
    prev_ticks = SDL_GetTicks();

    return 0;
}

int SDL_AppIterate(void)
{
    Uint64 curr_ticks = SDL_GetTicks();
    float delta_time = (float) (curr_ticks - prev_ticks) / 1000.0f;
    prev_ticks = curr_ticks;

    rect.x += 1.0f * delta_time;
    SDL_Log("X: %f", rect.x);

    SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
    SDL_RenderClear(renderer);

    SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
    SDL_RenderFillRect(renderer, &rect);

    SDL_RenderPresent(renderer);

    return 0;
}

int SDL_AppEvent(const SDL_Event* event)
{
    if (event->type == SDL_EVENT_QUIT) {
        return 1;
    }

    return 0;
}

void SDL_AppQuit(void)
{
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
}

sbhutch avatar Dec 23 '23 04:12 sbhutch

This is happening because in SDL3, the logical presentation is implemented by scaling a render target of your desired size to the output window. This fixes a number of issues, but has the side effect that you no longer effectively have a high DPI render target.

You can get a similar effect to SDL2's logical presentation by using SDL_SetRenderScale() and SDL_SetRenderClipRect() yourself.

slouken avatar Dec 24 '23 14:12 slouken

You can get a similar effect to SDL2's logical presentation by using SDL_SetRenderScale() and SDL_SetRenderClipRect() yourself.

Awesome, it is good to know that I can still achieve sub-pixel rendering this way, but it does seem unfortunate that the logical presentation API doesn't support sub-pixel precision. It is such a useful API that would be non-trivial to replicate, and I feel that the jerky movement in the current API is not well suited to game development because it is so noticeable. For example, if we scaled a 480x270 resolution to 1920x1080 then we can only move sprites with 4-pixel precision which makes the game look like a slideshow.

sbhutch avatar Dec 25 '23 01:12 sbhutch

So I'm looking at removing the render target for logical rendering, and I'm thinking the best plan here is to hook into the QueueCmd* functions, so everything gets adjusted just as it's going to the platform API.

The pros: don't have to touch any of the backends, don't have to hijack the viewport state. The cons: have to touch every vertex that goes through, which means copying every vertex to a temporary buffer.

Might be a bad idea, I'll report back.

icculus avatar Aug 13 '24 03:08 icculus

It's good that this functionality isn't tied together, but having a smaller resolution render target is required for games that wants to simulate low resolution. This includes many indie games with pixel art and emulators. It'd be nice if there was a function to set this up the way it used to work before removing the render target.

maia-s avatar Sep 25 '24 17:09 maia-s

That is super easy, just create a render target and set it before rendering your game, and then before the present, copy it to the screen. You can even use the logical presentation to set up where it should go and the copy call would just be SDL_RenderTexture(texture, NULL, NULL); and then you can present.

slouken avatar Sep 25 '24 17:09 slouken

Okay, this is fixed in revision control.

Here's an updated version of the test program that compiles against the latest SDL3, to show it working.

#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

static SDL_Window* window;
static SDL_Renderer* renderer;

static SDL_FRect rect;
static Uint64 prev_ticks;

SDL_AppResult SDL_AppInit(void **appstate, int argc, char** argv)
{
    if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
        SDL_Log("%s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    if (!SDL_CreateWindowAndRenderer("Hello SDL", 200, 200, SDL_WINDOW_RESIZABLE, &window, &renderer)) {
        SDL_Log("%s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    if (!SDL_SetRenderLogicalPresentation(renderer, 50, 50, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE)) {
        SDL_Log("%s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    if (!SDL_SetRenderVSync(renderer, 1)) {
        SDL_Log("%s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    rect.x = 0.0f;
    rect.y = 20.0f;
    rect.w = rect.h = 10.0f;
    prev_ticks = SDL_GetTicks();

    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppIterate(void *appstate)
{
    Uint64 curr_ticks = SDL_GetTicks();
    float delta_time = (float) (curr_ticks - prev_ticks) / 1000.0f;
    prev_ticks = curr_ticks;

    rect.x += 1.0f * delta_time;
    SDL_Log("X: %f", rect.x);

    SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
    SDL_RenderClear(renderer);

    SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
    SDL_RenderFillRect(renderer, &rect);

    SDL_RenderPresent(renderer);

    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event* event)
{
    if (event->type == SDL_EVENT_QUIT) {
        return SDL_APP_SUCCESS;
    }

    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate)
{
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
}

icculus avatar Sep 25 '24 20:09 icculus

Your test fails to compile: error C4576: a parenthesized type followed by an initializer list is a non-standard explicit type conversion syntax

slouken avatar Sep 25 '24 21:09 slouken

It worked on GCC. :)

I just updated it with a fix that will probably make it work on Visual Studio.

icculus avatar Sep 25 '24 21:09 icculus

Yep, that works!

slouken avatar Sep 25 '24 21:09 slouken

Thank you for iterating on this ❤️

sbhutch avatar Sep 25 '24 22:09 sbhutch