Sub-pixel rendering issue in SDL3
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();
}
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.
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.
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.
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.
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.
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);
}
Your test fails to compile:
error C4576: a parenthesized type followed by an initializer list is a non-standard explicit type conversion syntax
It worked on GCC. :)
I just updated it with a fix that will probably make it work on Visual Studio.
Yep, that works!
Thank you for iterating on this ❤️