Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

invalid swapInterval behavior - wrong display buffer update rate #367

Closed
qrp73 opened this issue Feb 12, 2025 · 3 comments
Closed

invalid swapInterval behavior - wrong display buffer update rate #367

qrp73 opened this issue Feb 12, 2025 · 3 comments

Comments

@qrp73
Copy link

qrp73 commented Feb 12, 2025

Steps to reproduce:

  1. create test app:
    1.1) install SDL2 libs with command sudo apt install libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev
    1.2) compile test app from source (see source code in the spoiler at the end of issue details) code with command gcc -o test-vblank4 test-vblank4.c -Wall -lGL -lSDL2 -lSDL2_ttf -lSDL2_image

  2. run test app with the following command ./test-vblank4. It should open window which shows realtime graph which displays time periods measured between SDL_GL_SwapWindow calls, which represent actual window surface update rate.

  3. switch swap interval with keyboard key F1.

When you press the F1 button, it leads to call swapInterval() to change swapInterval value, the values ​​change in a cycle: 1, 2, 3, 0. When you run the test application, the initial value is swapInterval=1.

The last value passed to SDL_GL_SetSwapInterval() function is displayed as "need: 1".
The currently reported swapInterval value reported by SDL_GL_GetSwapInterval() function is displayed as "swap: 1", when value is < 0 it also dispaying the string returned from SDL_GetError().
Current display mode, include display refresh rate is displayed as "mode: 1280x1024@75".

You can take app screenshot with button F12, it will save screenshot to the current app folder.

The expected time interval between SDL_GL_SwapWindow calls for current swapInterval value is highlighted with yellow color.

The correct behavior for swapInterval value is the following:
0 = max possible update rate, with no vblank synchronization
1 = the update rate time interval is equals to the current display refresh rate time interval
2 = the update rate time interval is exactly 2x times longer than for value 1
3 = the update rate time interval is exactly 3x times longer than for value 1

For example, if your display has 60 Hz refresh rate, then it's refresh time period is 1/60 = 16.6 ms. So, it should have the following results for different swapInterval values:
0 = min possible time interval (actual result is about 1-2 ms)
1 = 16.6 ms
2 = 33.3 ms
3 = 50.0 ms

Now check how it actually works on raspberry pi OS bookworm with latest updates for different swapInterval values:

swapInterval=1

Expected result: 16.6 ms for 60 fps display

Actual result: as expected (16.6 ms for 60 fps display)

Image

swapInterval=2

Expected result: 33.3 ms for 60 fps display

Actual result: 22.4 ms for 60 fps display - INVALID

Image

swapInterval=3

Expected result: 50.0 ms for 60 fps display

Actual result: 40.6 ms for 60 fps display - INVALID

Image

swapInterval=0

Expected result: min possible time interval

Actual result: as expected (1.9 ms)

Image

As you can see swapInterval works correctly for values 0 and 1 only. For swapInterval > 1 it gives invalid results. And different drivers (x11, wayland, kmsdrm) produce different time intervals for swapInterval > 1 which is even more confusing.

The behavior of swapInteval value is pretty simple - it just should skip N-1 dispaly frames and then do 1 buffer update. For N=0 it should disable vblank sync and do buffer swap as fast as possible. For N>0 vblank sync should be enabled, for N=1 it should skip 1-1=0 frames - as result it should update buffer on every display frame. For N=2 it should skip 2-1=1 frame, etc.

But for some unknown reason, raspberry pi OS shows some random values for N > 1. Please fix it.

Test code

test-vblank4.c
// sudo apt install libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev
// sudo pacman -S sdl2 sdl2_ttf sdl2_image
// gcc -o test-vblank4 test-vblank4.c -Wall -lGL -lSDL2 -lSDL2_ttf -lSDL2_image
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <limits.h>
#include <GL/gl.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_opengl.h>
#include <SDL2/SDL_ttf.h>
#include <SDL2/SDL_image.h>

#ifdef _WIN32
#include <limits.h>
#define MAX_PATH_LENGTH MAX_PATH
#else
#include <limits.h>
#define MAX_PATH_LENGTH PATH_MAX
#endif


// Simple queue implementation
typedef struct {
    uint64_t* data;
    size_t front;
    size_t rear;
    size_t size;
    size_t capacity;
} Queue64;
void queue_init(Queue64* q, size_t capacity) {
    q->capacity = capacity;
    q->size = 0;
    q->front = 0;
    q->rear = 0;
    q->data = (uint64_t*)malloc(capacity * sizeof(uint64_t));
}
void queue_free(Queue64* q) {
    free(q->data);
}
void queue_resize(Queue64* q, size_t new_capacity) {
    if (new_capacity < q->size) {
        printf("warn: queue_resize(): new capacity %zu is smaller than current size %zu. The last %zu elements will be discarded.\n", new_capacity, q->size, q->size - new_capacity);
        q->size = new_capacity;  // Truncate elements if the new size is smaller than the current size
    }
    uint64_t* new_data = (uint64_t*)malloc(new_capacity * sizeof(uint64_t));
    // copy elements to a new buffer
    for (size_t i = 0; i < q->size; i++) {
        new_data[i] = q->data[(q->front + i) % q->capacity];
    }
    // Free the old buffer and update pointers
    free(q->data);
    q->data = new_data;
    q->front = 0;
    q->rear = q->size;
    q->capacity = new_capacity;
}
void queue_enqueue(Queue64* q, uint64_t value) {
    // If the queue is full, resize it
    if (q->size == q->capacity) {
        queue_resize(q, q->capacity * 2);  // Double the capacity
    }
    // Add the element to the queue
    q->data[q->rear] = value;
    q->rear = (q->rear + 1) % q->capacity;  // Circular move
    q->size++;
}
uint64_t queue_dequeue(Queue64* q) {
    if (q->size == 0) {
        printf("error: queue_dequeue() failed: queue is empty\n");
        return 0;
    }
    uint64_t value = q->data[q->front];
    q->front = (q->front + 1) % q->capacity;
    q->size--;
    return value;
}
// Function to remove N oldest elements from the front of the queue
void queue_trimFront(Queue64* q, size_t n) {
    if (n >= q->size) {
        printf("warn: queue_trimFront(): trying to remove more elements (%zu) than the queue contains (%zu). Clearing the entire queue.\n", n, q->size);
        q->front = 0;  // Reset the front pointer
        q->size = 0;   // Reset the size
    } else {
        // Directly update the front pointer to remove N elements
        q->front = (q->front + n) % q->capacity;
        q->size -= n;
    }
}
size_t queue_getSize(Queue64* q) {
    return q->size;
}
uint64_t queue_getElement(Queue64* queue, int index) {
    if (index < 0 || index >= queue->size) {
        printf("error: queue_getElement() failed: index %d is out of bounds\n", index);
        return 0;  // or some default value for error
    }
    // Calculate the actual index in the circular queue
    size_t actual_index = (queue->front + index) % queue->capacity;
    return queue->data[actual_index];
}

Queue64 _queue;
uint64_t _frameCounter;


void fillRect(float x, float y, float w, float h) {
    GLfloat vertices[] = { x, y + h, x + w, y + h, x + w, y, x, y };
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(2, GL_FLOAT, 0, vertices);
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    //glDisableClientState(GL_VERTEX_ARRAY);    
}

void fillTexturedRect(float x, float y, float w, float h) {
    GLfloat vertices[]  = { x, y + h, x + w, y + h, x + w, y, x, y };
    GLfloat texCoords[] = { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f };
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glVertexPointer(2, GL_FLOAT, 0, vertices);
    glTexCoordPointer(2, GL_FLOAT, 0, texCoords);
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    //glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    //glDisableClientState(GL_VERTEX_ARRAY);
}

void drawLine(float x1, float y1, float x2, float y2) {
    GLfloat vertices[2 * 2] = { x1,y1, x2,y2 };
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(2, GL_FLOAT, 0, vertices);
    glDrawArrays(GL_LINE_STRIP, 0, 2);
    //glDisableClientState(GL_VERTEX_ARRAY);            
}


typedef struct {
    GLuint texture;
    GLuint textureOutline;
    int w;
    int h;
} GlyphEntry;

GlyphEntry* _glyphs[0x100];

void textureARGB2LUMA(SDL_Surface* surface, GLubyte* dst_pixels) {
    // ARGB to LUMINANCE_ALPHA
    uint32_t* src_pixels = (uint32_t*)surface->pixels;
    int pitch32 = surface->pitch / SDL_BYTESPERPIXEL(surface->format->format);
    for (int y = 0; y < surface->h; y++) {
        for (int x = 0; x < surface->w; x++) {
            uint32_t pixel = src_pixels[y * pitch32 + x];
            uint8_t a = (pixel & surface->format->Amask) >> surface->format->Ashift;
            uint8_t r = (pixel & surface->format->Rmask) >> surface->format->Rshift;
            uint8_t g = (pixel & surface->format->Gmask) >> surface->format->Gshift;
            uint8_t b = (pixel & surface->format->Bmask) >> surface->format->Bshift;
            uint8_t l = (r + g + b) / 3;
            *dst_pixels++ = l;      // Luminance
            *dst_pixels++ = a;      // Alpha
        }
    }    
}

void loadGlyph(TTF_Font* font, const uint8_t symbol, GlyphEntry* glyph) {
    glyph->texture = 0;
    glyph->textureOutline = 0;
    glyph->w = 0;
    glyph->h = 0;
    
    SDL_Color color = {255, 255, 255, 255};
    GLuint texture = 0;

    char text[2] = { symbol, 0 };
    TTF_SetFontOutline(font, 0);
    SDL_Surface* surface = TTF_RenderText_Blended(font, text, color);
    if (surface == NULL) {
        glPopAttrib();    
        printf("TTF_RenderText_Blended() failed: %s\n", TTF_GetError());
        return;
    }
    glyph->w = surface->w;
    glyph->h = surface->h;
    GLubyte* texture_pixels = (GLubyte*)malloc(surface->w * surface->h * 2); // LUMINANCE ALPHA
    if (texture_pixels == NULL) {
        printf("malloc() failed!\n");
        SDL_FreeSurface(surface);
        glPopAttrib();
        return;
    }
    textureARGB2LUMA(surface, texture_pixels);
    glEnable(GL_TEXTURE_2D);
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    //glPixelStorei(GL_UNPACK_ROW_LENGTH, surface->pitch / SDL_BYTESPERPIXEL(surface->format->format));
    //glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surface->w, surface->h, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, surface->pixels);
    glPixelStorei(GL_UNPACK_ROW_LENGTH, surface->w);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surface->w, surface->h, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, texture_pixels);    
    glyph->texture = texture;
    free(texture_pixels);
    SDL_FreeSurface(surface);
    surface = NULL;
    texture = 0;
    
    TTF_SetFontOutline(font, 1);
    surface = TTF_RenderText_Blended(font, text, color);
    if (surface == NULL) {
        glPopAttrib();    
        printf("TTF_RenderText_Blended() failed: %s\n", TTF_GetError());
        return;
    }
    texture_pixels = (GLubyte*)malloc(surface->w * surface->h * 2); // LUMINANCE ALPHA
    if (texture_pixels == NULL) {
        printf("malloc() failed!\n");
        SDL_FreeSurface(surface);
        glPopAttrib();
        return;
    }
    textureARGB2LUMA(surface, texture_pixels);    
    glEnable(GL_TEXTURE_2D);
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    //glPixelStorei(GL_UNPACK_ROW_LENGTH, surface->pitch / SDL_BYTESPERPIXEL(surface->format->format));
    //glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surface->w, surface->h, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, surface->pixels);
    glPixelStorei(GL_UNPACK_ROW_LENGTH, surface->w);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surface->w, surface->h, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, texture_pixels);    
    glyph->textureOutline = texture;
    free(texture_pixels);
    SDL_FreeSurface(surface);
    surface = NULL;
    texture = 0;    
}
void freeGlyphs() {
    for (size_t i=0; i < sizeof(_glyphs)/sizeof(GlyphEntry*); i++) {
        GlyphEntry* glyph = _glyphs[i];
        if (glyph == NULL) {
            continue;
        }
        if (glyph->texture != 0) {
            glDeleteTextures(1, &(glyph->texture));
            glyph->texture = 0;
        }
        if (glyph->textureOutline != 0) {
            glDeleteTextures(1, &(glyph->textureOutline));
            glyph->textureOutline = 0;
        }
        free(glyph);
        _glyphs[i] = NULL;
    }
}


void drawText(TTF_Font* font, const char* text, int x, int y, SDL_Color color) {
    glPushAttrib(GL_CURRENT_BIT | GL_DEPTH_BUFFER_BIT | GL_ENABLE_BIT);
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    int cx = x, cy = y;
    for (int i = 0; text[i] != '\0'; i++) {
        uint8_t symbol = text[i] & 0xff;
        GlyphEntry* glyph = _glyphs[symbol];
        if (glyph == NULL) {
            glyph = (GlyphEntry*)malloc(sizeof(GlyphEntry));
            loadGlyph(font, symbol, glyph);
            _glyphs[symbol] = glyph;            
        }
        //if (glyph->textureOutline != 0) {
        //    glBindTexture(GL_TEXTURE_2D, glyph->textureOutline);
        //    glColor4f(0.0f, 0.0f, 0.0f, 1.0f);
        //    fillTexturedRect(cx, cy, glyph->w, glyph->h);            
        //}
        if (glyph->texture != 0) {
            glBindTexture(GL_TEXTURE_2D, glyph->texture);
            glColor4f(color.r/255.0f, color.g/255.0f, color.b/255.0f, 1.0f);            
            fillTexturedRect(cx, cy, glyph->w, glyph->h);            
        }
        cx += glyph->w;
    }
    glBindTexture(GL_TEXTURE_2D, 0);
    glPopAttrib();    
}

#define max(a, b) ((a) > (b) ? (a) : (b))


int _swapInterval = 1;
int _fullscreen = 0;
SDL_DisplayMode _displayMode;


void window_onRender(SDL_Window* window, TTF_Font* font, int width, int height) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    char buf[128];
    SDL_Color textColor = {255, 255, 0}; // Green
    int x = 10, y = 10, step=24;
    if (SDL_GetDisplayMode(0, 0, &_displayMode) != 0) {
        snprintf(buf, sizeof(buf), "SDL_GetDisplayMode() failed: %s\n", SDL_GetError());
        drawText(font, buf, x, y, textColor);
        y+=step;
        //return;
    }
    int swapInterval = SDL_GL_GetSwapInterval();
    
    double fps = 0.0;
    uint64_t tmax = 0;
    uint64_t tmin = -1;
    uint64_t tavg = 0;
    size_t length = queue_getSize(&_queue);    
    if (length > 0) {
        // find tavg for the last second (for fps)
        size_t counter = 0;
        for (size_t i=max(length-_displayMode.refresh_rate, 0); i < length; i++) {
            uint64_t v = queue_getElement(&_queue, i);
            tavg += v;
            counter++;
        }
        if (counter > 0) {
            tavg /= counter;
        }
        fps = (double)1000000000UL / tavg;
        // find statistics
        tmax = 0;
        tmin = -1;
        tavg = 0;
        for (size_t i=0; i < length; i++) {
            uint64_t v = queue_getElement(&_queue, i);
            if (v > tmax) tmax = v;
            if (v < tmin) tmin = v;
            tavg += v;
        }
        tavg /= length;
    }

    float targetRate = 0.0;
    float targetTime = 0.0;
    float viewMax = 0.0;
    if (abs(swapInterval) == 0 || _displayMode.refresh_rate == 0) {
        targetRate = INFINITY;
        targetTime = 0;
        viewMax = 1000000000.0 / max(_displayMode.refresh_rate, 30);
    } else {
        targetRate = (double)_displayMode.refresh_rate / abs(swapInterval);
        targetTime = 1000000000.0 / targetRate;
        viewMax = targetTime;
    }
    viewMax *= 1.2;
    if (tmax > viewMax) viewMax = tmax;
    float scale = viewMax!=0 ? height / viewMax : 0;

    glEnable(GL_BLEND);                
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // Draw grid and limit lines    
    glColor4f(1.0f, 1.0f, 1.0f, 0.2f);  // White    
    // horizontal grid    
    for (uint64_t t=0; t < viewMax; t+=1000000.0) {
        drawLine(0, height - scale * t, width-1, height - scale * t);
    }
    // vertical grid
    for (int x=_frameCounter%_displayMode.refresh_rate; x < width; x+=_displayMode.refresh_rate) {
        drawLine(width-x, 0, width-x, height);
    }
    // target time line
    //glColor4f(1.0f, 0.0f, 1.0f, 0.6f);  // Magenta
    //drawLine(0, height - scale * targetTime, width-1, height - scale * targetTime);    
    
    float limitMax;
    float limitMin;
    if (abs(swapInterval) == 0) {
        limitMin = 0.0;
        limitMax = 0.0;
    } else {
        limitMin = 1000000000.0 / (targetRate+1);
        limitMax = 1000000000.0 / (targetRate-1);
    }
    float limitMinY = height - scale * limitMin;
    float limitMaxY = height - scale * limitMax;
    //glColor4f(1.0f, 0.0f, 0.0f, 0.6f);
    //drawLine(0, limitMinY, width-1, limitMinY);
    //drawLine(0, limitMinY, width-1, limitMaxY);
    
    
    // time graph
    int startIndex = width - length;
    GLfloat vertices[width * 2]; // (x, y)
    for (int i = 0; i < width; ++i) {
        uint64_t v = 0;
        if (i >= startIndex) {
            v = queue_getElement(&_queue, i-startIndex);
        }
        vertices[i * 2] = i;                        // x
        vertices[i * 2 + 1] = height - scale * v;   // y (reversed)
    }
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(2, GL_FLOAT, 0, vertices);
    glColor4f(0.0f, 1.0f, 0.0f, 0.8f);            // Green
    glDrawArrays(GL_LINE_STRIP, 0, sizeof(vertices)/(2*sizeof(GLfloat)));
    glDisableClientState(GL_VERTEX_ARRAY);
    
    // +-1 fps area
    glColor4f(1.0f, 1.0f, 0.0f, 0.3f);      // Yellow
    fillRect(0, limitMinY, width-1, limitMaxY-limitMinY);
    
    glDisable(GL_BLEND);        
    

    snprintf(buf, sizeof(buf), "real: %7.3f fps", fps);
    drawText(font, buf, x, y, textColor);
    y+=step;
    snprintf(buf, sizeof(buf), "need: %7.3f fps", targetRate);
    drawText(font, buf, x, y, textColor);
    y+=step;
    y+=step;    
    snprintf(buf, sizeof(buf), "tmin: %7.3f ms", tmin/1000000.0);
    drawText(font, buf, x, y, textColor);
    y+=step;
    snprintf(buf, sizeof(buf), "tmax: %7.3f ms", tmax/1000000.0);
    drawText(font, buf, x, y, textColor);
    y+=step;
    snprintf(buf, sizeof(buf), "tavg: %7.3f ms", tavg/1000000.0);
    drawText(font, buf, x, y, textColor);
    y+=step;
    y+=step;
    if (swapInterval < 0)
        snprintf(buf, sizeof(buf), "swap: %d, \"%s\"", swapInterval, SDL_GetError());
    else
        snprintf(buf, sizeof(buf), "swap: %d", swapInterval);
    drawText(font, buf, x, y, textColor);
    y+=step;
    snprintf(buf, sizeof(buf), "need: %d", _swapInterval);
    drawText(font, buf, x, y, textColor);    
    y+=step;
    y+=step;            
    snprintf(buf, sizeof(buf), "drv:  %s", (const char*)SDL_GetCurrentVideoDriver());
    drawText(font, buf, x, y, textColor);    
    y+=step;    
    snprintf(buf, sizeof(buf), "mode: %dx%d@%d", _displayMode.w, _displayMode.h, _displayMode.refresh_rate);
    drawText(font, buf, x, y, textColor);
    y+=step;
    snprintf(buf, sizeof(buf), "size: %dx%d", width, height);
    drawText(font, buf, x, y, textColor);
}

void window_onResize(int width, int height) {
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0, width, height, 0.0, -1.0, 1.0); // coordinate setup: (0,0)=(left,top), (width,height)=(bottom,right)
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

int findFont(const char* font_name, char *buffer, size_t buffer_size) {
    char command[MAX_PATH_LENGTH];
    snprintf(command, sizeof(command), "fc-match -f '%%{file}' %s", font_name);
    FILE *fp = popen(command, "r");
    if (fp == NULL) {
        return 0;
    }
    if (fgets(buffer, buffer_size, fp) == NULL) {
        pclose(fp);
        return 0;
    }
    buffer[strcspn(buffer, "\n")] = '\0';
    pclose(fp);
    return 1;
}

void saveScreenshot(SDL_Window* window) {
    int width, height;
    SDL_GetWindowSize(window, &width, &height);
    SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_RGBA32);
    if (!surface) {
        printf("SDL_CreateRGBSurfaceWithFormat failed: %s\n", SDL_GetError());
        return;
    }
    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, surface->pixels);
    if (glGetError() != GL_NO_ERROR) {
        printf("glReadPixels failed\n");
        SDL_FreeSurface(surface);
        return;
    }
    Uint32* pixels = (Uint32*)surface->pixels;
    for (int y = 0; y < height / 2; ++y) {
        for (int x = 0; x < width; ++x) {
            Uint32 temp = pixels[y * width + x];
            pixels[y * width + x] = pixels[(height - y - 1) * width + x];
            pixels[(height - y - 1) * width + x] = temp;
        }
    }
    // Create unique filename
    char filename[128];
    int counter = 1;
    do {
        snprintf(filename, sizeof(filename), "screenshot_%d.png", counter++);
    } while (SDL_RWFromFile(filename, "rb") != NULL);
    // Save
    if (IMG_SavePNG(surface, filename) != 0) {
        printf("IMG_SavePNG() failed: %s\n", IMG_GetError());
    } else {
        printf("Screenshot saved to %s\n", filename);
    }
    SDL_FreeSurface(surface);
}


int main(int argc, char *argv[]) {
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        printf("Failed to initialize SDL: %s\n", SDL_GetError());
        return -1;
    }
    //printf("SDL_GetCurrentVideoDriver(): %s\n", (const char*)SDL_GetCurrentVideoDriver());
    
    int width  = 800;
    int height = 600;
    int doublebuffer = 1;
    const char *drivername = NULL;

    // parse command line
    for (int i = 1; i < argc; ++i) {
        if (strcmp(argv[i], "--help") == 0) {
            printf("USAGE: %s [options]\n", argv[0]);
            printf("  F1  - toggle swapInterval\n");
            printf("  F11 - toggle fullscreen mode\n");
            printf("  F12 - save screenshot\n");
            printf("  ESC - exit\n");
            printf("options:\n");
            printf("  --list                  list available SDL video drivers\n");
            printf("  --driver <drivername>   set SDL video driver (equals to SDL_VIDEODRIVER env.variable)\n");
            printf("  --size <width,height>   set initial window size\n");
            printf("  --doublebuffer <1|0>    set SDL_GL_DOUBLEBUFFER attribute value\n");
            SDL_Quit();
            return 0;
        } else if (strcmp(argv[i], "--list") == 0) {
            int num_drivers = SDL_GetNumVideoDrivers();
            printf("\nAvailable --driver values:\n");
            for (int j = 0; j < num_drivers; ++j) {
                printf("  \"%s\"\n", SDL_GetVideoDriver(j));
            }
            SDL_Quit();
            return 0;
        } else if (strcmp(argv[i], "--driver") == 0 && i + 1 < argc) {
            drivername = argv[i + 1];
            printf("--driver %s\n", drivername);
            i++;
        } else if (strcmp(argv[i], "--size") == 0 && i + 1 < argc) {
            if (sscanf(argv[i + 1], "%d,%d", &width, &height) != 2) {
                printf("Invalid --size format. Expected --size <width,height>\n");
                SDL_Quit();
                return -1;
            }
            printf("--size %d,%d\n", width, height);
            i++;
        } else if (strcmp(argv[i], "--doublebuffer") == 0 && i + 1 < argc) {
            if (sscanf(argv[i + 1], "%d", &doublebuffer) != 1) {
                printf("Invalid --doublebuffer format. Expected --doublebuffer <0|1>\n");
                SDL_Quit();
                return -1;
            }
            doublebuffer = doublebuffer ? 1 : 0;
            printf("--doublebuffer %d\n", doublebuffer);
            i++;
        } else {
            printf("error: unknown command line option \"%s\"\n", argv[i]);
            SDL_Quit();
            return -1;            
        } 
    }
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, doublebuffer);    
    if (SDL_VideoInit(drivername) < 0) {
        printf("SDL_VideoInit(\"%s\") failed: %s\n", drivername, SDL_GetError());
        SDL_Quit();
        return -1;
    }
    printf("SDL_GetCurrentVideoDriver(): %s\n", (const char*)SDL_GetCurrentVideoDriver());
    
    if (SDL_GetDisplayMode(0, 0, &_displayMode) != 0) {
        printf("SDL_GetDisplayMode() failed: %s\n", SDL_GetError());
        SDL_Quit();
        return -1;
    }    
    printf("SDL_GetDisplayMode(): %d x %d @ %d, format=0x%08x\n", _displayMode.w, _displayMode.h, _displayMode.refresh_rate, _displayMode.format);

    if (TTF_Init() < 0) {
        printf("TTF_Init() failed: %s\n", TTF_GetError());
        SDL_Quit();
        return -1;
    }
    // find path: $ fc-match -f '%{file}\n' Monospace
    char fontPath[MAX_PATH_LENGTH];
    if (!findFont("Monospace", fontPath, sizeof(fontPath))) {
        printf("findFont(\"Monospace\") failed\n");
        return -1;
    }
    TTF_Font* font = TTF_OpenFont(fontPath, 20);
    if (font == NULL) {
        printf("TTF_OpenFont() failed: %s\n", TTF_GetError());
        TTF_Quit();
        SDL_Quit();
        return -1;
    }
    if (IMG_Init(IMG_INIT_PNG) == 0) {
        printf("IMG_Init failed: %s\n", IMG_GetError());
        SDL_Quit();
        return -1;
    }

    SDL_Window* window = SDL_CreateWindow(
        "test-vblank3", 
        SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
        width, height,
        SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    if (window == NULL) {
        printf("SDL_CreateWindow() failed: %s\n", SDL_GetError());
        TTF_CloseFont(font);        
        TTF_Quit();
        SDL_Quit();
        return -1;
    }
    if (_fullscreen)
    {
        SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP);
    }
    SDL_GLContext glContext = SDL_GL_CreateContext(window);
    SDL_GL_SetSwapInterval(_swapInterval);
    printf("SDL_GL_SetSwapInterval(%d) => SDL_GL_GetSwapInterval() = %d\n", _swapInterval, SDL_GL_GetSwapInterval());
        

    const int   printAttrs[] = { SDL_GL_DOUBLEBUFFER, SDL_GL_MULTISAMPLEBUFFERS, SDL_GL_MULTISAMPLESAMPLES, 0 };
    const char* printNames[] = { "SDL_GL_DOUBLEBUFFER", "SDL_GL_MULTISAMPLEBUFFERS", "SDL_GL_MULTISAMPLESAMPLES", 0 };
    for (int i=0; printAttrs[i] != 0; i++) {
        int value;
        if (SDL_GL_GetAttribute(printAttrs[i], &value)) {
            printf("SDL_GL_GetAttribute(%s) failed: %s\n", printNames[i], SDL_GetError());
            continue;
        }
        printf("%s = %d\n", printNames[i], value);
    }

    SDL_GetWindowSize(window, &width, &height);
    printf("SDL_GetWindowSize(): %d, %d\n", width, height);
    window_onResize(width, height);

    int running = 1;
    struct timespec ts1, ts2;
    clock_gettime(CLOCK_MONOTONIC_RAW, &ts1);
    
    queue_init(&_queue, 16384);

    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    while (running) {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) { 
                running = 0;
            }
            if (event.type == SDL_KEYDOWN) {
                if (event.key.keysym.sym == SDLK_ESCAPE) { 
                    running = 0;
                }
                if (event.key.keysym.sym == SDLK_F11) {
                    _fullscreen = _fullscreen ? 0:1;
                    printf("SDL_SetWindowFullscreen(%d)\n", _fullscreen);
                    SDL_SetWindowFullscreen(window, _fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
                }
                if (event.key.keysym.sym == SDLK_F1) {
                    _swapInterval = (_swapInterval+1) & 3;
                    SDL_GL_SetSwapInterval(_swapInterval);
                    printf("SDL_GL_SetSwapInterval(%d) => SDL_GL_GetSwapInterval() = %d\n", _swapInterval, SDL_GL_GetSwapInterval());
                }
                if (event.key.keysym.sym == SDLK_F12) {
                    saveScreenshot(window);
                }
            }
            if (event.type == SDL_WINDOWEVENT) {
                if (event.window.event == SDL_WINDOWEVENT_RESIZED || 
                    event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
                    width = event.window.data1;
                    height = event.window.data2;
                    window_onResize(width, height);
                }
            }
        }

        window_onRender(window, font, width, height);

        SDL_GL_SwapWindow(window);
        clock_gettime(CLOCK_MONOTONIC_RAW, &ts2);
        uint64_t dt_ns = ((uint64_t)ts2.tv_sec - (uint64_t)ts1.tv_sec) * 1000000000UL + (ts2.tv_nsec - ts1.tv_nsec);
        ts1 = ts2;
        _frameCounter++;

        queue_enqueue(&_queue, dt_ns);
        SDL_GetWindowSize(window, &width, &height);
        ssize_t excess_elements = (ssize_t)queue_getSize(&_queue) - width;
        if (excess_elements > 0) {
            queue_trimFront(&_queue, excess_elements);
        }
    }

    queue_free(&_queue);
    freeGlyphs();

    IMG_Quit();
    TTF_CloseFont(font);
    TTF_Quit();
    SDL_GL_DeleteContext(glContext);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}
@qrp73 qrp73 changed the title invalid swapInterval behavior - wrong display update rate invalid swapInterval behavior - wrong display buffer update rate Feb 12, 2025
@popcornmix
Copy link

The docs say 0 and 1 are the valid options, with -1 an optionally supported option. No mentions of 2 or 3.

The code confirms only 0 and 1 are valid options.

@lurch
Copy link
Collaborator

lurch commented Feb 12, 2025

The code confirms only 0 and 1 are valid options.

Strange, I wonder why the OP is seeing different behaviour between 1 and 2 then? 🤔

But as this is exploiting functionality which isn't mentioned in the SDL docs, I'll close this issue as undefined behaviour.

@lurch lurch closed this as not planned Won't fix, can't repro, duplicate, stale Feb 12, 2025
@qrp73
Copy link
Author

qrp73 commented Feb 13, 2025

The docs say 0 and 1 are the valid options, with -1 an optionally supported option. No mentions of 2 or 3.

This is doc mistake, because basically this is just a wrapper for OpenGL SwapInterval extenstion function. Which supports value > 1.

Actually I catch this issue with direct call to OpenGL SwapInterval extension, with no using SDL2 at all.

This is OpenGL bug of Raspberry Pi driver, don't confuse it with SDL2. I just using SDL2 to demonstrate the issue. But you can call OpenGL extension directly and reproduce this bug with no SDL2 library at all. It shows exactly the same behavior.

I tested it on NVidia/AMD drivers and it works with any swapInterval value as expected.
The issue with incorrect swap interval behavior happens with Raspberry Pi drivers only.

See official OpenGL specification for swapInterval extension:
https://registry.khronos.org/OpenGL/extensions/EXT/EXT_swap_control.txt

glXSwapIntervalEXT specifies the minimum number of video frame
    periods per buffer swap for a particular GLX drawable (e.g. a value
    of two means that the color buffers will be swapped at most every
    other video frame). The interval takes effect when glXSwapBuffers
    is first called on the drawable subsequent to the glXSwapIntervalEXT
    call.

    A video frame period is the time required by the monitor to display
    a full frame of video data.  In the case of an interlaced monitor,
    this is typically the time required to display both the even and odd
    fields of a frame of video data.

As you can see it mention swapInterval=2 (see "e.g. a value of two means") as example to explain how it should work.

But Raspberry Pi OpenGL driver works incorrectly for value 2 and above and breaks OpenGL specification, see steps of this issue to reproduce it for test.

I suspect this issue also may lead to graphics issues in chromium browser.

Do I need to open a separate issue about the incorrect OpenGL implementation of the swapInterval extension, or will you reopen this issue?

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

No branches or pull requests

3 participants