FPSLimit (with VSync disabled) can freeze the UI permanently — unsigned underflow in draw_screen() frame limiter
Summary
When VSync=false and FPSLimit is set, the manual frame-limiter in draw_screen() can compute a wildly wrong sleep duration through an unsigned-integer underflow, causing SDL_Delay() to block for up to ~49 days — i.e. a permanent UI freeze. It triggers whenever a single iteration takes longer than the frame budget (1000 / FPSLimit ms), which routinely happens while icon textures are (re)loaded during navigation, page changes, or when returning to the launcher after a launched application exits.
Affected version
- v2.2 (latest release).
- The identical code is present on
master (src/launcher.c), so it is not yet fixed.
- Reproduced on Linux (Ubuntu Server 24.04, SDL 2.30) driving a 1080p60 display, but the logic is platform-independent.
Root cause
src/launcher.c, in draw_screen() (lines ~813–818):
SDL_RenderPresent(renderer);
if (!config.vsync) {
Uint32 sleep_time = refresh_period - (SDL_GetTicks() - ticks.main);
if (sleep_time > 0)
SDL_Delay(sleep_time);
}
ticks.main is captured at the top of each main-loop iteration (ticks.main = SDL_GetTicks();, line ~1357), so SDL_GetTicks() - ticks.main is the full elapsed time of the iteration (event handling + texture loading + rendering).
refresh_period is 1000 / FPSLimit (computed at line ~234).
sleep_time is declared Uint32 (unsigned). When the elapsed time exceeds refresh_period, the expression refresh_period - elapsed is negative, but because the result is unsigned it wraps to a value near UINT32_MAX (~4.29 × 10⁹).
- The guard
if (sleep_time > 0) cannot catch this: an unsigned value is > 0 for everything except exactly 0. So SDL_Delay(~4.29e9 ms) executes and the program hangs for ~49 days. SDL_Delay is uninterruptible, so the launcher must be force-killed.
A single frame easily exceeds the budget because lazy texture loading (icons via SDL_image) during navigation, page changes, or returning from a launched app can take tens to hundreds of ms — far more than e.g. the 66 ms budget at FPSLimit=15. The lower the FPSLimit (the whole point of the setting — reducing idle GPU/CPU), the more likely the freeze.
Steps to reproduce
- In
config.ini under [General]: set VSync=false and FPSLimit=15 (any low cap).
- Launch flex-launcher and navigate between entries/pages, or launch an app and return to the launcher.
- On the first iteration whose load time exceeds
1000 / FPSLimit ms (typically a texture load), the UI freezes permanently.
This is not specific to submenu navigation. It has been reproduced with all entries flattened into a single menu (no :submenu entries at all), confirming it fires on any texture-loading frame — including returning to the launcher after a launched application exits — not just submenu transitions.
Impact
A hard, unrecoverable UI freeze — the process is alive but stuck in SDL_Delay. On an appliance / kiosk / HTPC with no keyboard, recovery requires a power-cycle. This makes FPSLimit effectively unusable for its intended purpose (lowering idle GPU/CPU usage).
Suggested fix
Compute the elapsed time first and only sleep if time remains, avoiding the unsigned subtraction entirely:
if (!config.vsync) {
Uint32 elapsed = SDL_GetTicks() - ticks.main;
if (elapsed < refresh_period)
SDL_Delay(refresh_period - elapsed);
}
(Equivalently: compute sleep_time as a signed int32_t and keep the > 0 check.) Either way, a frame that overruns its budget simply proceeds to the next frame immediately instead of sleeping for ~49 days.
Disclaimer: this report was written by Claude Code on behalf of the reporter. The root-cause analysis was verified directly against src/launcher.c at tag v2.2 and on master, and the freeze was reproduced on the reporter's hardware (HP T630 thin client, Ubuntu Server 24.04, SDL 2.30).
FPSLimit (with VSync disabled) can freeze the UI permanently — unsigned underflow in
draw_screen()frame limiterSummary
When
VSync=falseandFPSLimitis set, the manual frame-limiter indraw_screen()can compute a wildly wrong sleep duration through an unsigned-integer underflow, causingSDL_Delay()to block for up to ~49 days — i.e. a permanent UI freeze. It triggers whenever a single iteration takes longer than the frame budget (1000 / FPSLimitms), which routinely happens while icon textures are (re)loaded during navigation, page changes, or when returning to the launcher after a launched application exits.Affected version
master(src/launcher.c), so it is not yet fixed.Root cause
src/launcher.c, indraw_screen()(lines ~813–818):ticks.mainis captured at the top of each main-loop iteration (ticks.main = SDL_GetTicks();, line ~1357), soSDL_GetTicks() - ticks.mainis the full elapsed time of the iteration (event handling + texture loading + rendering).refresh_periodis1000 / FPSLimit(computed at line ~234).sleep_timeis declaredUint32(unsigned). When the elapsed time exceedsrefresh_period, the expressionrefresh_period - elapsedis negative, but because the result is unsigned it wraps to a value nearUINT32_MAX(~4.29 × 10⁹).if (sleep_time > 0)cannot catch this: an unsigned value is> 0for everything except exactly0. SoSDL_Delay(~4.29e9 ms)executes and the program hangs for ~49 days.SDL_Delayis uninterruptible, so the launcher must be force-killed.A single frame easily exceeds the budget because lazy texture loading (icons via SDL_image) during navigation, page changes, or returning from a launched app can take tens to hundreds of ms — far more than e.g. the 66 ms budget at
FPSLimit=15. The lower the FPSLimit (the whole point of the setting — reducing idle GPU/CPU), the more likely the freeze.Steps to reproduce
config.iniunder[General]: setVSync=falseandFPSLimit=15(any low cap).1000 / FPSLimitms (typically a texture load), the UI freezes permanently.This is not specific to submenu navigation. It has been reproduced with all entries flattened into a single menu (no
:submenuentries at all), confirming it fires on any texture-loading frame — including returning to the launcher after a launched application exits — not just submenu transitions.Impact
A hard, unrecoverable UI freeze — the process is alive but stuck in
SDL_Delay. On an appliance / kiosk / HTPC with no keyboard, recovery requires a power-cycle. This makesFPSLimiteffectively unusable for its intended purpose (lowering idle GPU/CPU usage).Suggested fix
Compute the elapsed time first and only sleep if time remains, avoiding the unsigned subtraction entirely:
(Equivalently: compute
sleep_timeas a signedint32_tand keep the> 0check.) Either way, a frame that overruns its budget simply proceeds to the next frame immediately instead of sleeping for ~49 days.