Skip to content

feat(stlxdm): presented frames through two display buffers - #459

Merged
FlareCoding merged 2 commits into
masterfrom
pr/stlxdm-flip
Oct 25, 2026
Merged

FlareCoding merged 2 commits into
masterfrom
pr/stlxdm-flip

Conversation

@FlareCoding

@FlareCoding FlareCoding commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

stlxdm, the desktop's compositor, drew every frame straight into the framebuffer the screen shows, on a timer of its own, and used none of /dev/gfxfb's display buffers and presents, which show a frame only once it is complete and report when it reaches the screen.

Changes

  • Show only whole frames: flip_presenter in stlxdm's presenter.cpp creates two display buffers at the primary output's mode with GFXFB_CREATE_BUFFER and copies each composed frame's changes into the one off screen. It presents that buffer with GFXFB_PRESENT, naming the regions changed since the buffer's own last accepted present, and the two swap roles only when a present's end reports that it latched, meaning the output shows its buffer from then on.
  • Compose at the display's pace: the presenter interface gains end_fd() and take_end(), so stlxdm's main loop polls /dev/gfxfb while a present waits for its end and composes the next frame once it has ended, still at most once per refresh period. presenter::present() itself waits for that end when it has not come, since the splash presents without polling.
  • Keep every other display working: open_presenter() in main.cpp falls back to the memcpy_presenter that copies into the framebuffer when the display cannot create buffers, as gfxfb's boot display cannot, or any other step of the setup fails, and the boot log names the step. While another program controls the display's buffers (EBUSY) it tries again every 100 ms instead, since frames drawn into the framebuffer would not show.
  • Keep the pointer's look steady: cursor::draw blends the pointer only inside the rect being composed, as the tooltip, star and overlay draw, and decor::draw_outline clips the resize outline the same way so its fill cannot cover the pointer. Each composed rect blended the whole pointer again, thickening it by one blend for each rect a frame composed after the one that repainted it, and two display buffers would hold it at different thicknesses, changing its look whenever they swap.
  • Boots: the desktop booted before and after the change on x86_64 and aarch64, on virtio-gpu and on gfxfb's boot display (the firmware's framebuffer), with no fault. On virtio-gpu stlxdm presented through display buffers, and on the boot display it fell back. Below the top bar every desktop and terminal dump matches the base's pixel for pixel but for one terminal's caret, dumped in the other phase of its blink, and the pointer's pixels match in every one. The splash dumps differ by their animation's timing.
  • Not run: no boot reached stlxdm's wait while another program controls the display's buffers, a present that failed or ended unlatched, a failed or interrupted read of a present's end, a frame whose damage lies wholly off screen, a fallback for any cause but the boot display's, a resize drag, or the power overlay's collapse frames.

Note

Medium Risk
Touches core display presentation and compositor timing on /dev/gfxfb, with framebuffer fallback; drawing changes are localized to cursor/outline clipping.

Overview
stlxdm no longer paints only into the visible framebuffer on a fixed timer. It prefers a flip_presenter that composes into a cached buffer, copies dirty regions into one of two /dev/gfxfb display buffers, and **GFXFB_PRESENT**s whole frames; buffer roles swap when a present-end event reports latch.

The presenter API adds shutdown(), end_fd(), and take_end(). The main loop polls the gfxfb fd while a present is pending and skips compose_tick until that present finishes (still capped at one compose per refresh). open_presenter() retries flip_presenter::init() on EBUSY (another client owns buffers), then falls back to memcpy_presenter.

cursor::draw and decor::draw_outline take a compose clip rect and draw through a sub-surface view so the pointer is not alpha-blended multiple times per frame (which had thickened the cursor across partial compose rects and mismatched flip buffers).

Reviewed by Cursor Bugbot for commit c9a6ecc. Bugbot is set up for automated code reviews on this repo. Configure here.

Every rect composed in a frame blended the whole pointer again, so a
target kept between frames could hold it blended several times over.
The resize outline is clipped too, or its fill would cover the pointer.
stlxdm copies each frame into the display buffer off screen, so a
working display never shows one partly copied, and composes the next
once its present ended. Without such buffers it uses the framebuffer.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c9a6ecc. Configure here.

}

changed.clear();
m_present_pending = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed presents are never retried

Medium Severity · Logic Bug

A refused GFXFB_PRESENT keeps m_unpresented but never marks a present as waiting, and compose_tick still clears m_damage. The main loop then sees no compose work and will not call present() again until some later input or animation dirties the screen, so a frame that is already copied into the back buffer can stay invisible after a transient refuse (ENOMEM, ERR_REFUSED).

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c9a6ecc. Configure here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked: accurate, and kept on purpose. A refused present keeps the back buffer's list of regions changed since its last accepted present, so the refused frame, already copied into that buffer, goes out with the next present, which the next damage brings at most once per refresh period. The memcpy presenter recovers from a failed flush the same way, sending the whole screen with its next flush. The bar's clock repaints every second, so on the desktop that wait is at most a second, and behind the power overlay, whose clock is frozen, it lasts until the next input once the overlay stops animating. Two boots that refused one present with ENOMEM through this path saw the next present 39 ms later at startup and 76 ms later on the idle desktop, each one naming every refused region and latching.

A timed retry would mostly fail again: EIO means a failed device as well as a refused buffer, and EINVAL after a mode change or EAGAIN on a dark output lasts until output events recreate the buffers or relight the output, so retrying every refresh period would keep stlxdm waking on a dead or dark display. Re-presenting after those belongs with the output events. No change made.

@FlareCoding
FlareCoding merged commit c00fad8 into master Oct 25, 2026
11 checks passed
@FlareCoding
FlareCoding deleted the pr/stlxdm-flip branch October 25, 2026 05:00
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