Skip to content

Improve framebuffer transitions and screen initialization - #1940

Open
robcodedev wants to merge 3 commits into
OnionUI:v4.5-devfrom
robcodedev:framebuffer-transitions
Open

Improve framebuffer transitions and screen initialization#1940
robcodedev wants to merge 3 commits into
OnionUI:v4.5-devfrom
robcodedev:framebuffer-transitions

Conversation

@robcodedev

Copy link
Copy Markdown

This PR improves framebuffer handling on 752x560-capable devices, adds an optional 640x480 mode, and shortens screen detection without reintroducing the display-initialization race that can produce sheared frames.

The changes are split into three commits so each part can still be reviewed independently:

  1. use fbmode for verified framebuffer transitions
  2. add the optional .force640Res override
  3. shorten resolution detection while preserving framebuffer-driver readiness

1. Verified framebuffer transitions

Runtime currently uses fbset for resolution changes. It requests a geometry but does not verify when the driver has actually latched it, always requests two virtual pages, and does not clear framebuffer memory whose page boundaries have just moved.

That last point is what produces the sheared frames seen around 752x560 handoffs. The 640x480 and 752x560 layouts do not divide framebuffer memory at the same offsets, so bytes written using the 752x560 stride can briefly be scanned out using the 640x480 stride.

This adds fbmode, a small framebuffer helper that:

  • waits for the requested visible and virtual geometry to be reported
  • uses the driver's actual post-switch line_length
  • supports pre-clearing before a pitch change
  • leaves scanout parked on page 0
  • exposes --probe so redundant mode changes can be skipped
  • reports failure back to runtime so existing fbset fallbacks can be used

GameSwitcher handoff

launch_switcher becomes the single owner of GameSwitcher's 752x560 requirement.

A game exiting toward GameSwitcher leaves the framebuffer alone instead of dropping to 640x480 only for the next stage to switch it straight back.

Before launching GameSwitcher, runtime probes the current framebuffer and leaves an already-correct 752x560 two-page layout untouched, avoiding a forced same-mode relatch when arriving from a 752x560 game.

Shutdown

keymon draws the End_Save screen from deepsleep() and backgrounds it, so it is usually still rendering when the game exits and runtime reaches the post-game resolution change. bootScreen reads the framebuffer geometry once at startup and writes rows at that pitch, so changing the mode underneath it shears the shutdown screen.

Runtime now preserves the framebuffer when /tmp/.offOrder is set. Nothing after that point renders except the shutdown screen itself, which reads the live geometry, so the transition was only ever cost. .offOrder is already used this way in launch_game - the "SAVING" panel is skipped on the same condition.

MainUI handoff

On 752x560-capable devices, runtime establishes MainUI's 640x480 three-page layout in init_system, before the boot screen is drawn.

launch_main_ui then probes the framebuffer. If it is still 640x480/3, the layout is preserved and the boot screen remains visible until MainUI paints over it.

If the layout differs, runtime preclears before changing it so a pitch change cannot expose stale pixels written using the previous stride.

640-only devices do not use these extra preparation paths.


2. Optional forced 640x480 mode

Adds an opt-in flag:

$sysdir/config/.force640Res

When present, runtime pins screen_resolution to 640x480, removes /tmp/new_res_available, and establishes the 640x480 three-page framebuffer layout.

Removing /tmp/new_res_available is intentional: downstream code then takes the same paths it would on hardware that never exposed 560p, rather than every resolution-related call site needing a separate override case.

/tmp/screen_resolution records 640x480, so components that read it use the same geometry runtime is using - DraStic's launch.sh uses this file to decide whether to select its 752x560 mode, and display_getResolution() feeds DISPLAY_WIDTH/DISPLAY_HEIGHT for screenshot capture and GameSwitcher thumbnail scaling.

No flag file means no behavior change.


3. Faster screen detection with explicit driver readiness

get_screen_resolution previously polled:

/proc/mi_modules/fb/mi_fb0

every 500ms until the display driver published its timing.

On some boots that takes several seconds, and the coarse interval can add up to another 500ms after the driver is actually ready.

Resolution is now resolved from three sources, in order:

  1. read mi_fb0 once
  2. if unavailable, read FB_TIMING_WIDTH / FB_TIMING_HEIGHT from dmesg
  3. if neither answers, poll mi_fb0 every 100ms with the same 5 second ceiling

mi_fb0 remains authoritative. The dmesg timing describes the configured boot framebuffer and is not guaranteed to equal the panel, so it is only used when mi_fb0 has no answer and can never override a real driver reading.

The parser also accepts the FB_TIMMING_* spelling seen in some logs.

Preserve driver readiness

The old polling loop did two jobs implicitly:

  • determine the screen resolution
  • wait until the framebuffer driver was ready

The dmesg path can answer the first question before mi_fb0 exists, so that implicit readiness barrier disappears.

That matters because both the normal 752-capable boot path and .force640Res perform a boot-time framebuffer mode change. Calling fbmode before the display driver has finished its own initialization can let the driver's later setup overwrite runtime's mode, leaving scanout and framebuffer contents using different layouts.

This was observed as intermittent sheared output when booting a 752x560 device with .force640Res.

wait_for_fb_driver() makes the old guarantee explicit. It waits for mi_fb0 to publish Current TimingWidth before either boot-time fbmode operation.

A fast dmesg hit can therefore resolve the screen type immediately without allowing the framebuffer transition itself to race late driver initialization.

If the readiness wait times out, runtime logs it and still attempts the mode change as a best effort.

Fallback

Existing resolution-change paths retain their fbset fallback.

The new framebuffer paths are gated on the presence of fbmode, and failed fbmode transitions fall back to fbset where a mode change is required.

Testing

Tested on Miyoo Mini V4 / Miyoo Flip 752x560 hardware:

  • repeated cold boots
  • boot with and without .force640Res
  • MainUI startup and return from games
  • 640x480 and 752x560 game launches
  • GameSwitcher handoffs and quick switching
  • shutdown by power button from a 752x560 game
  • repeated forced-640 boots to check for intermittent sheared output
  • verified to still work as before on Miyoo Mini Plus (640x480 device)
  • checked which resolution-detection path fired in the runtime log

fbset requests a geometry and returns without verifying that the driver
has latched it. Runtime also always asked for two virtual pages, even
though MainUI uses three, and stale framebuffer contents were left in
memory when page boundaries moved.

That last part causes the sheared frames seen around 752x560 handoffs.
A 640 page can start inside a 752 page, so pixels written at a
3008-byte stride can briefly be scanned out at 2560 bytes per row.

Add fbmode, which verifies both visible and virtual geometry, clears
using the driver's real post-switch line_length, can preclear before a
pitch change, and leaves scanout parked on page 0.

Make launch_switcher the single owner of GameSwitcher's 752x560
requirement. A game exiting toward GameSwitcher leaves the framebuffer
alone, and launch_switcher probes first so an already-correct 752x560
two-page layout is left untouched.

Leave the framebuffer alone when a shutdown is pending too. keymon draws
the End_Save screen from deepsleep() and backgrounds it, so it is
usually still rendering when the game exits and runtime reaches the
post-game resolution change. bootScreen reads the geometry once at
startup and writes rows at that pitch, so changing the mode underneath
it shears the shutdown screen. Nothing after that point needs 640x480.

On 752x560-capable devices, establish MainUI's 640x480 three-page
layout in init_system before the boot screen is drawn. launch_main_ui
probes that layout and preserves it when already correct; if a real
change is needed it preclears before committing the new geometry.

Existing resolution-change paths retain fbset fallback. The new
preparation paths only run when fbmode is available, and failed mode
changes fall back to fbset where a transition is required.
Not everyone wants 560p on a 752x560 panel. Drop a .force640Res file in
$sysdir/config and runtime keeps the framebuffer at 640x480.

The override is applied after resolution detection. When set, it pins
screen_resolution to 640x480, removes /tmp/new_res_available, and
establishes the 640x480 three-page layout through fbmode.

Removing /tmp/new_res_available means the rest of runtime takes the same
paths it would on a device that never exposed 560p, rather than every
call site needing to know about the override.

The actual mode used by Onion is written to /tmp/screen_resolution.

No flag, no behaviour change.
get_screen_resolution polled /proc/mi_modules/fb/mi_fb0 every 500ms until
the driver came up. On some boots that costs 3 seconds or more, and the
500ms interval can add almost another half-second after the driver is
actually ready.

Try three sources in order instead:

1. Read mi_fb0 once. If the driver is already up, use its timing.
2. If it is not, read FB_TIMING_WIDTH/HEIGHT from dmesg.
3. If neither answers, poll mi_fb0 at 100ms instead of 500ms, keeping
   the same 5 second ceiling.

FB_TIMING_* is the configured boot framebuffer and is not guaranteed to
equal the panel, so dmesg is only consulted when mi_fb0 cannot answer.
It can never override a real driver reading. The parser accepts both
FB_TIMING_* and FB_TIMMING_* spellings.

The dmesg path can answer before mi_fb0 exists, which removes the
implicit driver-readiness barrier the old polling loop provided.

Add wait_for_fb_driver and call it before both boot-time fbmode changes
so an early dmesg result cannot let runtime race the display driver's
late initialization. This fixes intermittent sheared output seen with
.force640Res on 752x560 hardware.
Copilot AI added a commit to Amiga500/Onion that referenced this pull request Sep 9, 2026
…and previews, GameSwitcher favorites + crash fixes, fbmode framebuffer transitions

Co-authored-by: Amiga500 <16525337+Amiga500@users.noreply.github.com>
@robcodedev

Copy link
Copy Markdown
Author

Thanks @Amiga500 for testing and coming with feedback. I will dropping the detection commit rather than patching it. Force-pushing later so this is just the first 2 safe commits.

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

Successfully merging this pull request may close these issues.

1 participant