From 060bd787b1051e1974b8efd7ca048a9c320f8c63 Mon Sep 17 00:00:00 2001 From: katonar Date: Thu, 5 Jun 2025 15:50:07 +0200 Subject: [PATCH 1/3] Refactor: Replace DRM swap buffer implementation with asynchronous page-flipping and framebuffer caching The original implementation created/destroyed framebuffers (FBs) per-frame, leading to kernel overhead and screen tearing. This commit replaces it with a different approach using: - Asynchronous `drmModePageFlip()` with vblank sync - Framebuffer caching to reduce repeated FB creation/removal operations - Proper resource management through BO callbacks and buffer release synchronization - Added error handling for busy displays, cache overflows, and flip failures - Event-driven cleanup via page_flip_handler to prevent GPU/scanout conflicts Co-authored-by: rob-bits --- src/platforms/rcore_drm.c | 235 ++++++++++++++++++++++++++++++++++---- 1 file changed, 215 insertions(+), 20 deletions(-) diff --git a/src/platforms/rcore_drm.c b/src/platforms/rcore_drm.c index 3ec3135e1..9aebe6cf0 100644 --- a/src/platforms/rcore_drm.c +++ b/src/platforms/rcore_drm.c @@ -71,6 +71,21 @@ #include "EGL/egl.h" // Native platform windowing system interface #include "EGL/eglext.h" // EGL extensions +#include // for drmHandleEvent poll +#include //for EBUSY, EAGAIN + +#define MAX_CACHED_BOS 3 + +typedef struct { + struct gbm_bo *bo; + uint32_t fb_id; // DRM framebuffer ID +} FramebufferCache; + +static FramebufferCache fbCache[MAX_CACHED_BOS]; +static volatile int fbCacheCount = 0; +static volatile bool pendingFlip = false; +static bool crtcSet = false; + #ifndef EGL_OPENGL_ES3_BIT #define EGL_OPENGL_ES3_BIT 0x40 #endif @@ -217,6 +232,7 @@ static const short linuxToRaylibMap[KEYMAP_SIZE] = { //---------------------------------------------------------------------------------- // Module Internal Functions Declaration //---------------------------------------------------------------------------------- +int InitSwapScreenBuffer(void); int InitPlatform(void); // Initialize platform (graphics, inputs and more) void ClosePlatform(void); // Close platform @@ -551,34 +567,207 @@ void DisableCursor(void) CORE.Input.Mouse.cursorHidden = true; } -// Swap back buffer with front buffer (screen drawing) -void SwapScreenBuffer(void) -{ +static void drm_fb_destroy_callback(struct gbm_bo *bo, void *data) { + uint32_t fb_id = (uintptr_t)data; + // Remove from cache + for (int i = 0; i < fbCacheCount; i++) { + if (fbCache[i].bo == bo) { + TRACELOG(LOG_INFO, "DRM: fb removed %u", (uintptr_t)fb_id); + drmModeRmFB(platform.fd, fbCache[i].fb_id); // Release DRM FB + // Shift remaining entries + for (int j = i; j < fbCacheCount - 1; j++) { + fbCache[j] = fbCache[j + 1]; + } + fbCacheCount--; + break; + } + } +} + +// Create or retrieve cached DRM FB for BO +static uint32_t get_or_create_fb_for_bo(struct gbm_bo *bo) { + // Try to find existing cache entry + for (int i = 0; i < fbCacheCount; i++) { + if (fbCache[i].bo == bo) { + return fbCache[i].fb_id; + } + } + + // Create new entry if cache not full + if (fbCacheCount >= MAX_CACHED_BOS) { + //FB cache full! + return 0; + } + + uint32_t handle = gbm_bo_get_handle(bo).u32; + uint32_t stride = gbm_bo_get_stride(bo); + uint32_t width = gbm_bo_get_width(bo); + uint32_t height = gbm_bo_get_height(bo); + + uint32_t fb_id; + if (drmModeAddFB(platform.fd, width, height, 24, 32, stride, handle, &fb_id)) { + //rmModeAddFB failed + return 0; + } + + // Store in cache + fbCache[fbCacheCount] = (FramebufferCache){ .bo = bo, .fb_id = fb_id }; + fbCacheCount++; + + // Set destroy callback to auto-cleanup + gbm_bo_set_user_data(bo, (void*)(uintptr_t)fb_id, drm_fb_destroy_callback); + + TRACELOG(LOG_INFO, "DRM: added new bo %u" , (uintptr_t)fb_id); + return fb_id; +} + +// Renders a blank frame to allocate initial buffers +void RenderBlankFrame() { + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); eglSwapBuffers(platform.device, platform.surface); + + // Ensure the buffer is processed + glFinish(); +} + +// Initialize with first buffer only +int InitSwapScreenBuffer() { + if (!platform.gbmSurface || platform.fd < 0) { + TRACELOG(LOG_ERROR, "DRM not initialized"); + return -1; + } - if (!platform.gbmSurface || (-1 == platform.fd) || !platform.connector || !platform.crtc) TRACELOG(LOG_ERROR, "DISPLAY: DRM initialization failed to swap"); + // Render a blank frame to allocate buffers + RenderBlankFrame(); + // Get first buffer struct gbm_bo *bo = gbm_surface_lock_front_buffer(platform.gbmSurface); - if (!bo) TRACELOG(LOG_ERROR, "DISPLAY: Failed GBM to lock front buffer"); + if (!bo) { + TRACELOG(LOG_ERROR, "Failed to lock initial buffer"); + return -1; + } - uint32_t fb = 0; - int result = drmModeAddFB(platform.fd, platform.connector->modes[platform.modeIndex].hdisplay, platform.connector->modes[platform.modeIndex].vdisplay, 24, 32, gbm_bo_get_stride(bo), gbm_bo_get_handle(bo).u32, &fb); - if (result != 0) TRACELOG(LOG_ERROR, "DISPLAY: drmModeAddFB() failed with result: %d", result); + // Create FB for first buffer + uint32_t fb_id = get_or_create_fb_for_bo(bo); + if (!fb_id) { + gbm_surface_release_buffer(platform.gbmSurface, bo); + return -1; + } - result = drmModeSetCrtc(platform.fd, platform.crtc->crtc_id, fb, 0, 0, &platform.connector->connector_id, 1, &platform.connector->modes[platform.modeIndex]); - if (result != 0) TRACELOG(LOG_ERROR, "DISPLAY: drmModeSetCrtc() failed with result: %d", result); + // Initial CRTC setup + if (drmModeSetCrtc(platform.fd, platform.crtc->crtc_id, fb_id, + 0, 0, &platform.connector->connector_id, 1, + &platform.connector->modes[platform.modeIndex])) { + TRACELOG(LOG_ERROR, "Initial CRTC setup failed: %s", strerror(errno)); + gbm_surface_release_buffer(platform.gbmSurface, bo); + return -1; + } - if (platform.prevFB) - { - result = drmModeRmFB(platform.fd, platform.prevFB); - if (result != 0) TRACELOG(LOG_ERROR, "DISPLAY: drmModeRmFB() failed with result: %d", result); + // Keep first buffer locked until flipped + platform.prevBO = bo; + crtcSet = true; + return 0; +} + +// Static page flip handler +// this will be called once the drmModePageFlip() finished from the drmHandleEvent(platform.fd, &evctx); context +static void page_flip_handler(int fd, unsigned int frame, + unsigned int sec, unsigned int usec, + void *data) { + (void)fd; (void)frame; (void)sec; (void)usec; // Unused + pendingFlip = false; + struct gbm_bo *bo_to_release = (struct gbm_bo *)data; + //Buffers are released after the flip completes (via page_flip_handler), ensuring they're no longer in use. + // Prevents the GPU from writing to a buffer being scanned out + if (bo_to_release) { + gbm_surface_release_buffer(platform.gbmSurface, bo_to_release); } +} - platform.prevFB = fb; +// Swap implementation with proper caching +void SwapScreenBuffer() { + static int loopCnt = 0; + loopCnt++; + static int errCnt[5] = {0}; + if (!crtcSet || !platform.gbmSurface) return; - if (platform.prevBO) gbm_surface_release_buffer(platform.gbmSurface, platform.prevBO); + //call this only, if pendingFlip is not set + eglSwapBuffers(platform.device, platform.surface); - platform.prevBO = bo; + // Process pending events non-blocking + drmEventContext evctx = { + .version = DRM_EVENT_CONTEXT_VERSION, + .page_flip_handler = page_flip_handler + }; + + struct pollfd pfd = { .fd = platform.fd, .events = POLLIN }; + //polling for event for 0ms + while (poll(&pfd, 1, 0) > 0) { + drmHandleEvent(platform.fd, &evctx); + } + + // Skip if previous flip pending + if (pendingFlip) { + //Skip frame: flip pending + errCnt[0]++; + return; + } + + // Get new front buffer + struct gbm_bo *next_bo = gbm_surface_lock_front_buffer(platform.gbmSurface); + if (!next_bo) { + //Failed to lock front buffer + errCnt[1]++; + return; + } + + // Get FB ID (creates new one if needed) + uint32_t fb_id = get_or_create_fb_for_bo(next_bo); + if (!fb_id) { + gbm_surface_release_buffer(platform.gbmSurface, next_bo); + errCnt[2]++; + return; + } + + // Attempt page flip + /* rmModePageFlip() schedules a buffer-flip for the next vblank and then + * notifies us about it. It takes a CRTC-id, fb-id and an arbitrary + * data-pointer and then schedules the page-flip. This is fully asynchronous and + * When the page-flip happens, the DRM-fd will become readable and we can call + * drmHandleEvent(). This will read all vblank/page-flip events and call our + * modeset_page_flip_event() callback with the data-pointer that we passed to + * drmModePageFlip(). We simply call modeset_draw_dev() then so the next frame + * is rendered.. + * returns immediately. + */ + if (drmModePageFlip(platform.fd, platform.crtc->crtc_id, fb_id, + DRM_MODE_PAGE_FLIP_EVENT, platform.prevBO)) { + if (errno == EBUSY) { + //Display busy - skip flip + errCnt[3]++; + } else { + //Page flip failed + errCnt[4]++; + } + gbm_surface_release_buffer(platform.gbmSurface, next_bo); + return; + } + + // Success: update state + pendingFlip = true; + + platform.prevBO = next_bo; + //successful usage, do benchmarking + //in every 10 sec, at 60FPS 60*10 -> 600 + if(loopCnt >= 600) { + TRACELOG(LOG_INFO, "DRM err counters: %d, %d, %d, %d, %d, %d",errCnt[0],errCnt[1],errCnt[2],errCnt[3],errCnt[4], loopCnt); + //reinit the errors + for(int i=0;i<5;i++) { + errCnt[i] = 0; + } + loopCnt = 0; + } } //---------------------------------------------------------------------------------- @@ -910,7 +1099,8 @@ int InitPlatform(void) EGL_BLUE_SIZE, 8, // BLUE color bit depth (alternative: 5) EGL_ALPHA_SIZE, 8, // ALPHA bit depth (required for transparent framebuffer) //EGL_TRANSPARENT_TYPE, EGL_NONE, // Request transparent framebuffer (EGL_TRANSPARENT_RGB does not work on RPI) - EGL_DEPTH_SIZE, 24, // Depth buffer size (Required to use Depth testing!) + //ToDo: verify this. In 5.5 it is 16, in master it was 24 + EGL_DEPTH_SIZE, 16, // Depth buffer size (Required to use Depth testing!) //EGL_STENCIL_SIZE, 8, // Stencil buffer size EGL_SAMPLE_BUFFERS, sampleBuffer, // Activate MSAA EGL_SAMPLES, samples, // 4x Antialiasing if activated (Free on MALI GPUs) @@ -1083,9 +1273,14 @@ int InitPlatform(void) CORE.Storage.basePath = GetWorkingDirectory(); //---------------------------------------------------------------------------- - TRACELOG(LOG_INFO, "PLATFORM: DRM: Initialized successfully"); + if(InitSwapScreenBuffer() == 0) { + TRACELOG(LOG_INFO, "PLATFORM: DRM: Initialized successfully"); + return 0; + } else { + TRACELOG(LOG_INFO, "PLATFORM: DRM: Initialized failed"); + return -1; + } - return 0; } // Close platform From de62be0ec5695e849a0f0c56aa72b7c64b2442ab Mon Sep 17 00:00:00 2001 From: katonar Date: Mon, 7 Jul 2025 17:28:23 +0200 Subject: [PATCH 2/3] - created complier flag SUPPORT_DRM_CACHE, to enable triple buffered DRM caching --- src/config.h | 5 +++++ src/platforms/rcore_drm.c | 42 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/config.h b/src/config.h index dc6cc5893..0fd685e7a 100644 --- a/src/config.h +++ b/src/config.h @@ -299,4 +299,9 @@ //------------------------------------------------------------------------------------ #define MAX_TRACELOG_MSG_LENGTH 256 // Max length of one trace-log message +//DRM configuration +#if defined(PLATFORM_DRM) +//#define SUPPORT_DRM_CACHE 1 //enable triple buffered DRM caching +#endif //PLATFORM_DRM + #endif // CONFIG_H diff --git a/src/platforms/rcore_drm.c b/src/platforms/rcore_drm.c index 9aebe6cf0..33b5e2a05 100644 --- a/src/platforms/rcore_drm.c +++ b/src/platforms/rcore_drm.c @@ -71,6 +71,7 @@ #include "EGL/egl.h" // Native platform windowing system interface #include "EGL/eglext.h" // EGL extensions +#if defined(SUPPORT_DRM_CACHE) #include // for drmHandleEvent poll #include //for EBUSY, EAGAIN @@ -86,6 +87,8 @@ static volatile int fbCacheCount = 0; static volatile bool pendingFlip = false; static bool crtcSet = false; +#endif //SUPPORT_DRM_CACHE + #ifndef EGL_OPENGL_ES3_BIT #define EGL_OPENGL_ES3_BIT 0x40 #endif @@ -232,7 +235,6 @@ static const short linuxToRaylibMap[KEYMAP_SIZE] = { //---------------------------------------------------------------------------------- // Module Internal Functions Declaration //---------------------------------------------------------------------------------- -int InitSwapScreenBuffer(void); int InitPlatform(void); // Initialize platform (graphics, inputs and more) void ClosePlatform(void); // Close platform @@ -567,6 +569,7 @@ void DisableCursor(void) CORE.Input.Mouse.cursorHidden = true; } +#if defined(SUPPORT_DRM_CACHE) static void drm_fb_destroy_callback(struct gbm_bo *bo, void *data) { uint32_t fb_id = (uintptr_t)data; // Remove from cache @@ -769,7 +772,37 @@ void SwapScreenBuffer() { loopCnt = 0; } } +#else //SUPPORT_DRM_CACHE is not defined +// Swap back buffer with front buffer (screen drawing) +void SwapScreenBuffer(void) +{ + eglSwapBuffers(platform.device, platform.surface); + + if (!platform.gbmSurface || (-1 == platform.fd) || !platform.connector || !platform.crtc) TRACELOG(LOG_ERROR, "DISPLAY: DRM initialization failed to swap"); + + struct gbm_bo *bo = gbm_surface_lock_front_buffer(platform.gbmSurface); + if (!bo) TRACELOG(LOG_ERROR, "DISPLAY: Failed GBM to lock front buffer"); + + uint32_t fb = 0; + int result = drmModeAddFB(platform.fd, platform.connector->modes[platform.modeIndex].hdisplay, platform.connector->modes[platform.modeIndex].vdisplay, 24, 32, gbm_bo_get_stride(bo), gbm_bo_get_handle(bo).u32, &fb); + if (result != 0) TRACELOG(LOG_ERROR, "DISPLAY: drmModeAddFB() failed with result: %d", result); + + result = drmModeSetCrtc(platform.fd, platform.crtc->crtc_id, fb, 0, 0, &platform.connector->connector_id, 1, &platform.connector->modes[platform.modeIndex]); + if (result != 0) TRACELOG(LOG_ERROR, "DISPLAY: drmModeSetCrtc() failed with result: %d", result); + if (platform.prevFB) + { + result = drmModeRmFB(platform.fd, platform.prevFB); + if (result != 0) TRACELOG(LOG_ERROR, "DISPLAY: drmModeRmFB() failed with result: %d", result); + } + + platform.prevFB = fb; + + if (platform.prevBO) gbm_surface_release_buffer(platform.gbmSurface, platform.prevBO); + + platform.prevBO = bo; +} +#endif //SUPPORT_DRM_CACHE //---------------------------------------------------------------------------------- // Module Functions Definition: Misc //---------------------------------------------------------------------------------- @@ -1099,8 +1132,7 @@ int InitPlatform(void) EGL_BLUE_SIZE, 8, // BLUE color bit depth (alternative: 5) EGL_ALPHA_SIZE, 8, // ALPHA bit depth (required for transparent framebuffer) //EGL_TRANSPARENT_TYPE, EGL_NONE, // Request transparent framebuffer (EGL_TRANSPARENT_RGB does not work on RPI) - //ToDo: verify this. In 5.5 it is 16, in master it was 24 - EGL_DEPTH_SIZE, 16, // Depth buffer size (Required to use Depth testing!) + EGL_DEPTH_SIZE, 24, // Depth buffer size (Required to use Depth testing!) //EGL_STENCIL_SIZE, 8, // Stencil buffer size EGL_SAMPLE_BUFFERS, sampleBuffer, // Activate MSAA EGL_SAMPLES, samples, // 4x Antialiasing if activated (Free on MALI GPUs) @@ -1273,13 +1305,17 @@ int InitPlatform(void) CORE.Storage.basePath = GetWorkingDirectory(); //---------------------------------------------------------------------------- +#if defined(SUPPORT_DRM_CACHE) if(InitSwapScreenBuffer() == 0) { TRACELOG(LOG_INFO, "PLATFORM: DRM: Initialized successfully"); return 0; } else { +#endif//SUPPORT_DRM_CACHE TRACELOG(LOG_INFO, "PLATFORM: DRM: Initialized failed"); +#if defined(SUPPORT_DRM_CACHE) return -1; } +#endif //SUPPORT_DRM_CACHE } From 5b182139ae3e3223398af825fec8082e5a98f748 Mon Sep 17 00:00:00 2001 From: katonar Date: Mon, 7 Jul 2025 17:55:32 +0200 Subject: [PATCH 3/3] - implementing Raylib coding convention --- src/platforms/rcore_drm.c | 45 ++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/platforms/rcore_drm.c b/src/platforms/rcore_drm.c index 33b5e2a05..09684291c 100644 --- a/src/platforms/rcore_drm.c +++ b/src/platforms/rcore_drm.c @@ -79,10 +79,10 @@ typedef struct { struct gbm_bo *bo; - uint32_t fb_id; // DRM framebuffer ID + uint32_t fbId; // DRM framebuffer ID } FramebufferCache; -static FramebufferCache fbCache[MAX_CACHED_BOS]; +static FramebufferCache fbCache[MAX_CACHED_BOS] = {0}; static volatile int fbCacheCount = 0; static volatile bool pendingFlip = false; static bool crtcSet = false; @@ -570,13 +570,14 @@ void DisableCursor(void) } #if defined(SUPPORT_DRM_CACHE) -static void drm_fb_destroy_callback(struct gbm_bo *bo, void *data) { - uint32_t fb_id = (uintptr_t)data; +//callback to destroy cached framebuffer, set by gbm_bo_set_user_data() +static void DestroyFrameBufferCallback(struct gbm_bo *bo, void *data) { + uint32_t fbId = (uintptr_t)data; // Remove from cache for (int i = 0; i < fbCacheCount; i++) { if (fbCache[i].bo == bo) { - TRACELOG(LOG_INFO, "DRM: fb removed %u", (uintptr_t)fb_id); - drmModeRmFB(platform.fd, fbCache[i].fb_id); // Release DRM FB + TRACELOG(LOG_INFO, "DRM: fb removed %u", (uintptr_t)fbId); + drmModeRmFB(platform.fd, fbCache[i].fbId); // Release DRM FB // Shift remaining entries for (int j = i; j < fbCacheCount - 1; j++) { fbCache[j] = fbCache[j + 1]; @@ -588,11 +589,11 @@ static void drm_fb_destroy_callback(struct gbm_bo *bo, void *data) { } // Create or retrieve cached DRM FB for BO -static uint32_t get_or_create_fb_for_bo(struct gbm_bo *bo) { +static uint32_t GetOrCreateFbForBo(struct gbm_bo *bo) { // Try to find existing cache entry for (int i = 0; i < fbCacheCount; i++) { if (fbCache[i].bo == bo) { - return fbCache[i].fb_id; + return fbCache[i].fbId; } } @@ -607,21 +608,21 @@ static uint32_t get_or_create_fb_for_bo(struct gbm_bo *bo) { uint32_t width = gbm_bo_get_width(bo); uint32_t height = gbm_bo_get_height(bo); - uint32_t fb_id; - if (drmModeAddFB(platform.fd, width, height, 24, 32, stride, handle, &fb_id)) { + uint32_t fbId; + if (drmModeAddFB(platform.fd, width, height, 24, 32, stride, handle, &fbId)) { //rmModeAddFB failed return 0; } // Store in cache - fbCache[fbCacheCount] = (FramebufferCache){ .bo = bo, .fb_id = fb_id }; + fbCache[fbCacheCount] = (FramebufferCache){ .bo = bo, .fbId = fbId }; fbCacheCount++; // Set destroy callback to auto-cleanup - gbm_bo_set_user_data(bo, (void*)(uintptr_t)fb_id, drm_fb_destroy_callback); + gbm_bo_set_user_data(bo, (void*)(uintptr_t)fbId, DestroyFrameBufferCallback); - TRACELOG(LOG_INFO, "DRM: added new bo %u" , (uintptr_t)fb_id); - return fb_id; + TRACELOG(LOG_INFO, "DRM: added new bo %u" , (uintptr_t)fbId); + return fbId; } // Renders a blank frame to allocate initial buffers @@ -652,14 +653,14 @@ int InitSwapScreenBuffer() { } // Create FB for first buffer - uint32_t fb_id = get_or_create_fb_for_bo(bo); - if (!fb_id) { + uint32_t fbId = GetOrCreateFbForBo(bo); + if (!fbId) { gbm_surface_release_buffer(platform.gbmSurface, bo); return -1; } // Initial CRTC setup - if (drmModeSetCrtc(platform.fd, platform.crtc->crtc_id, fb_id, + if (drmModeSetCrtc(platform.fd, platform.crtc->crtc_id, fbId, 0, 0, &platform.connector->connector_id, 1, &platform.connector->modes[platform.modeIndex])) { TRACELOG(LOG_ERROR, "Initial CRTC setup failed: %s", strerror(errno)); @@ -675,7 +676,7 @@ int InitSwapScreenBuffer() { // Static page flip handler // this will be called once the drmModePageFlip() finished from the drmHandleEvent(platform.fd, &evctx); context -static void page_flip_handler(int fd, unsigned int frame, +static void PageFlipHandler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, void *data) { (void)fd; (void)frame; (void)sec; (void)usec; // Unused @@ -701,7 +702,7 @@ void SwapScreenBuffer() { // Process pending events non-blocking drmEventContext evctx = { .version = DRM_EVENT_CONTEXT_VERSION, - .page_flip_handler = page_flip_handler + .page_flip_handler = PageFlipHandler }; struct pollfd pfd = { .fd = platform.fd, .events = POLLIN }; @@ -726,8 +727,8 @@ void SwapScreenBuffer() { } // Get FB ID (creates new one if needed) - uint32_t fb_id = get_or_create_fb_for_bo(next_bo); - if (!fb_id) { + uint32_t fbId = GetOrCreateFbForBo(next_bo); + if (!fbId) { gbm_surface_release_buffer(platform.gbmSurface, next_bo); errCnt[2]++; return; @@ -744,7 +745,7 @@ void SwapScreenBuffer() { * is rendered.. * returns immediately. */ - if (drmModePageFlip(platform.fd, platform.crtc->crtc_id, fb_id, + if (drmModePageFlip(platform.fd, platform.crtc->crtc_id, fbId, DRM_MODE_PAGE_FLIP_EVENT, platform.prevBO)) { if (errno == EBUSY) { //Display busy - skip flip