Skip to content

display: bulk-copy the frame path, keep a back buffer, cache the scaling - #1

Open
ya-luotao wants to merge 2 commits into
gpu-m0from
display-frame-path
Open

display: bulk-copy the frame path, keep a back buffer, cache the scaling#1
ya-luotao wants to merge 2 commits into
gpu-m0from
display-frame-path

Conversation

@ya-luotao

@ya-luotao ya-luotao commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Viewer-side frame path for msb display (stacks on gpu-m0, part of the superradcompany#1482 series).

App::redraw() re-converted every pixel of the scanout on every frame with u32::from_le_bytes, and rescaled per pixel whenever the window size differed from the scanout. The guest emits a frame for every pointer move, so at 1920x1080 that was 2M conversions per mouse event.

Now:

  • a persistent 0RGB back buffer of scanout size; a Frame's damage rectangle is clipped and only that rectangle is copied out of the mmap slot, one memcpy per row (softbuffer's 0RGB on macOS is [b,g,r,x] in memory — the guest's BGRX layout, so no conversion at all);
  • damage from frames that arrive between two redraws is coalesced (bounding box; a whole-frame update wins);
  • the present blit is one copy_from_slice (softbuffer 0.4.8's macOS buffer_mut() hands out a fresh Vec every time and present_with_damage ignores damage, so the whole back buffer must be blitted each redraw);
  • scaling (window resized, fullscreen, 1x external display) keeps nearest-neighbour but drops the per-pixel division: integer factors duplicate pixels/rows with slice copies, other sizes use a cached x-index table;
  • MSB_DISPLAY_STATS=1 prints frames/s, redraws/s, MB/s copied and ms per redraw.

Viewer CPU (top -l 6 -s 1, samples 2-6 averaged; sandbox omarchy, 1920x1080 BGRX, window 1:1 to device pixels on a 2x display):

scenario before after
idle desktop 0.0 % 0.0 %
pointer motion 89.4 % 24.6 %
full-screen churn 40.9 % 10.6 %

Six unit tests cover rect clipping, damage coalescing, copy_rect, integer and non-integer scaling, and the too-small-buffer guard.

Follow-up (not in this PR): Hyprland's FLUSH rectangle currently covers the whole scanout on every frame, so the rectangle path is not yet exercised in practice, and read_2d_resource in third_party/msb_krun_devices still copies the whole scanout per flush on the gpu worker thread. Both are being looked at separately.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PN7mepn7ryjXupoHbQjFmR


Update: second commit 323595d drops the back buffer and the damage machinery again — Linux v6.12 drivers/gpu/drm/virtio/virtgpu_plane.c:91-97 sets ignore_damage_clips whenever the plane's fb changes (page flip), so a double-buffered compositor always flushes the whole scanout and the rect path could never fire. The viewer now blits the slot straight into the surface (one memcpy). Back-to-back numbers against b3db67c on the same sandbox are in that commit message (5.35 → 5.17 ms/redraw; the table above is from the first round and not directly comparable across sandbox instances).

`App::redraw()` re-converted every pixel of the scanout on every frame with
`u32::from_le_bytes`, and rescaled per pixel (a division per axis) whenever the
window size differed from the scanout. At 1920x1080 that is 2M conversions per
frame, and the guest emits a frame for every pointer move.

The viewer now keeps a persistent `0RGB` back buffer of scanout size. A
`Frame`'s damage rectangle is clipped to the scanout and only that rectangle is
copied out of the mmap slot, one `memcpy` per row: softbuffer's `0RGB` `u32` is
`[b, g, r, x]` in memory on macOS (`NoneSkipFirst` + `Order32Little`,
softbuffer-0.4.8 `backends/cg.rs:326`), which is exactly the guest's BGRX
layout, so no per-pixel conversion is needed at all. Malformed or absent
rectangles fall back to the whole frame, and frames that arrive between two
redraws have their rectangles coalesced into a bounding box.

The present path still blits the whole back buffer every redraw: softbuffer's
macOS `buffer_mut()` hands out a fresh zeroed `Vec` and `present()` moves it
into a `CGDataProvider` (`cg.rs:261`, `cg.rs:298`), so the surface buffer does
not persist between frames, and `present_with_damage` ignores its damage
(`cg.rs:364`). That blit is one `copy_from_slice`.

Scaling, used when the window size differs from the scanout (resize, fullscreen,
a 1x external display), keeps nearest-neighbour but drops the per-pixel
division: integer factors duplicate pixels and rows with slice fills and copies,
other sizes use an x-index table built once per (source, destination) width, and
destination rows sampling the same source row are copied rather than recomputed.
The window's size policy is unchanged; the scale factor is now logged once at
window creation.

`MSB_DISPLAY_STATS=1` prints a one-line summary per second: frames, redraws,
bytes copied, ms per redraw.

Viewer CPU (`top -l 6 -s 1`, samples 2-6 averaged; sandbox `omarchy`,
1920x1080 BGRX scanout, window 1:1 to device pixels on a 2x display):

  scenario                before    after
  idle desktop             0.0 %    0.0 %
  pointer motion          89.4 %   24.6 %
  full-screen churn       40.9 %   10.6 %

Pointer motion is Hyprland's cursor driven in a loop from the host; churn is
`foot` printing 200 MB at a time. After: ~46 frames/s during motion, ~20/s
during churn, 5.4 ms per redraw.

Note for follow-up work: Hyprland's FLUSH rectangle covers the whole scanout in
both scenarios (8.3 MB per frame at 1920x1080, per the stats line), so the win
above comes from the bulk copies, not yet from the damage rectangle. The
rectangle path is exercised by unit tests and pays off as soon as the guest
sends real damage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012LJqoJBYbk5RiNCmiR4zFC

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b3db67cbe9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/cli/lib/commands/display.rs Outdated
Comment on lines +71 to +73
/// A rectangle of the scanout, in scanout pixels.
#[derive(Clone, Copy)]
struct Rect {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Put the new Rust items under the required sections

This insertion begins a large block of new types, inherent implementations, and free functions without the mandated Types, Methods, and Functions section delimiters; only the later test module is sectioned. Reorganize these additions under the repository's exact Rust section headers.

AGENTS.md reference: AGENTS.md:L159-L161

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-30T16:23:53.427897Z 323595d New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

The damage rectangle can never be smaller than the whole scanout with a
Wayland compositor: Linux's virtio-gpu driver sets `ignore_damage_clips`
whenever the plane's framebuffer object changes, because uploads are done per
buffer (v6.12 `drivers/gpu/drm/virtio/virtgpu_plane.c:91-97`), and a compositor
page-flips between buffers on every frame. Measurements on the previous commit
confirmed it: 8.3 MB copied per frame at 1920x1080, in every scenario.

With the rectangle always covering everything, the viewer-side back buffer only
added work. The runtime already writes a complete frame into the slot
(`read_2d_resource`), and softbuffer's macOS surface buffer never persists —
`buffer_mut` returns a fresh zeroed `Vec` and `present` moves it into a
`CGDataProvider` (softbuffer-0.4.8 `backends/cg.rs:261`, `:298`), while
`present_with_damage` ignores its damage (`:364`). So a redraw was doing
slot -> back (8.3 MB) + back -> surface (8.3 MB) where one copy will do.

The viewer now blits the slot's bytes straight into the surface buffer, which
is a single `memcpy` because softbuffer's `0RGB` `u32` is `[b, g, r, x]` in
memory on macOS — the guest's BGRX layout, top byte ignored. The scaler used
when the window is not the scanout size now reads BGRX directly, keeping its
integer-factor and index-table fast paths and expanding each source row once.
`Rect`, `Damage`, the rectangle clipping/union and the back buffer are gone;
`rect` stays in the protocol (it costs nothing and a guest that does send real
damage would be worth supporting) but the viewer ignores it, with the kernel
reference in a comment.

Viewer CPU and cost per redraw, same method as the previous commit
(`top -l 6 -s 1`, samples 2-6 averaged), the two builds measured back to back
against the same sandbox and the same driver load:

  scenario           b3db67c          this commit
  pointer motion     33.5 %  5.35 ms   31.9 %  5.17 ms   (66 frames/s)
  full-screen churn  11.9 %  5.46 ms   11.1 %  5.20 ms   (~22 frames/s)

The frame rate is set by the guest, so the honest signal is the time per
redraw: 3-5 % less, consistently. It is a small win because a redraw is
dominated by `present()` (CGImage plus the CATransaction), not by the copy —
removing 8.3 MB of memcpy saves about 0.2 ms of a 5.3 ms redraw. The real
gain is 157 fewer lines of machinery that could never pay off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012LJqoJBYbk5RiNCmiR4zFC
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