raspberrypi / bookworm-feedback

13 stars 1 forks source link

SDL2 not able to read the current display mode after a display mode is changed via wlr-randr or arandr (for Wayland SDL_GetDisplayMode error: "index must be in the range of 0 - -1") #319

Open qrp73 opened 1 week ago

qrp73 commented 1 week ago

I'm using latest raspios with labwc on RPI4.

When I change the display mode using wlr-randr or arandr, it causes SDL2 applications to no longer be able to read the current display mode until they are restarted. The issue occurs both with the Wayland backend, where SDL_GetDisplayMode consistently returns the error:

index must be in the range of 0 - -1

and with the X11 backend, where SDL_GetDisplayMode starts returning the old mode, which does not match the actual one.

Steps to reproduce

1) sudo apt install libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev 2) Create test-vblank3.c and copy/paste code (see below) 3) Compile it with gcc -o test-vblank3 test-vblank3.c -Wall -lGL -lSDL2 -lSDL2_ttf -lSDL2_image 4) Run it with ./test-vblank3 --driver wayland 5) Run arandr and change video mode (for example refresh rate), or use wlr-randr (for example wlr-randr --output HDMI-A-1 --custom-mode 1280x1024@50Hz)

Expected result: SDL2 properly returns a new video mode

Actual result: For wayland backend SDL_GetDisplayMode fails with error. For x11 backend it returns incorrect video mode.

Screenshots

screenshot_6

Test code

test-vblank3.c ```c // sudo apt install libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev // sudo pacman -S sdl2 sdl2_ttf sdl2_image // gcc -o test-vblank3 test-vblank3.c -Wall -lGL -lSDL2 -lSDL2_ttf -lSDL2_image #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #define MAX_PATH_LENGTH MAX_PATH #else #include #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); } 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); //TTF_SetFontOutline(font, outlineWidth); SDL_Surface* surface = TTF_RenderText_Blended(font, text, color); if (surface == NULL) { glPopAttrib(); printf("TTF_RenderText_Blended() failed: %s\n", TTF_GetError()); return; } GLuint texture; 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); // shadow imitation //glColor4f(0.0f, 0.0f, 0.0f, 1.0f); //fillTexturedRect(x+1, y+1, surface->w, surface->h); glColor4f(1.0f, 1.0f, 1.0f, 1.0f); fillTexturedRect(x, y, surface->w, surface->h); SDL_FreeSurface(surface); glDeleteTextures(1, &texture); 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 set SDL video driver (equals to SDL_VIDEODRIVER env.variable)\n"); printf(" --size 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 \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); IMG_Quit(); TTF_CloseFont(font); TTF_Quit(); SDL_GL_DeleteContext(glContext); SDL_DestroyWindow(window); SDL_Quit(); return 0; } ```