display: a ~60 Hz frame clock — presents are scheduled, not immediate
Client 'present' requests and cursor pokes no longer repaint on the spot: they accumulate damage and arm a one-shot 16 ms timer, and the tick composites everything pending as one frame. A fast mouse previously turned every input event into a full present (100+/s); now any number of draws and moves inside one interval coalesce into a single repaint. No backend has a real vblank to pace by (docs/display-v2.md, 'Fenced is not vsync'), so this is the software stand-in — the same strategy Linux uses atop virtio-gpu. Bring-up paths that need pixels synchronously (initialise, self-checks) still present directly. onNotification now handles the message and timer badge bits independently: one coalesced badge can carry both, and the old either/or dispatch would have dropped a tick. All six display QEMU cases pass.
This commit is contained in:
+9
-1
@@ -196,13 +196,21 @@ shell, a terminal, a cursor, and a wallpaper:
|
|||||||
| `fill_rect` | fill a rectangle of a layer with a colour |
|
| `fill_rect` | fill a rectangle of a layer with a colour |
|
||||||
| `blit_tile` | copy a small client-supplied pixel tile into a layer (inline) |
|
| `blit_tile` | copy a small client-supplied pixel tile into a layer (inline) |
|
||||||
| `damage` | mark a region of a layer dirty |
|
| `damage` | mark a region of a layer dirty |
|
||||||
| `present` | composite dirty layers and flush to the screen |
|
| `present` | request a repaint: composited at the next frame-clock tick |
|
||||||
|
|
||||||
Text is intentionally *not* an operation — a client renders glyphs by blitting tiles
|
Text is intentionally *not* an operation — a client renders glyphs by blitting tiles
|
||||||
(the [PSF font](../system/kernel/font.psf) path the console already uses can move into a
|
(the [PSF font](../system/kernel/font.psf) path the console already uses can move into a
|
||||||
client). Keeping the protocol to rectangles and tiles keeps the compositor small and the
|
client). Keeping the protocol to rectangles and tiles keeps the compositor small and the
|
||||||
policy in the client.
|
policy in the client.
|
||||||
|
|
||||||
|
`present` is a *request*, not an immediate flush: the compositor runs a ~60 Hz **frame
|
||||||
|
clock** (a one-shot kernel timer re-armed on demand), and each tick composites all the
|
||||||
|
damage accumulated since the last one. Any number of client presents and cursor moves
|
||||||
|
inside one interval coalesce into a single repaint — the software stand-in for vblank
|
||||||
|
pacing on backends that have none (all of them today; see
|
||||||
|
[display-v2.md](display-v2.md), "Fenced is not vsync"). Bring-up paths that must put
|
||||||
|
pixels on screen synchronously (initialisation, the self-checks) bypass the clock.
|
||||||
|
|
||||||
## `runtime.display`
|
## `runtime.display`
|
||||||
|
|
||||||
Clients speak the protocol through a new [`library/runtime/display.zig`](../library/runtime/runtime.zig),
|
Clients speak the protocol through a new [`library/runtime/display.zig`](../library/runtime/runtime.zig),
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
//! z-order, and visibility. Clients create layers, draw into them by command (`fill_rect`,
|
//! z-order, and visibility. Clients create layers, draw into them by command (`fill_rect`,
|
||||||
//! `blit_tile`), mark `damage`, and ask for a `present`; the compositor repaints only the
|
//! `blit_tile`), mark `damage`, and ask for a `present`; the compositor repaints only the
|
||||||
//! damaged region — clear it, paint the visible layers bottom-to-top into the backend's
|
//! damaged region — clear it, paint the visible layers bottom-to-top into the backend's
|
||||||
//! surface, then `backend.present(damage)`. Shared-memory client surfaces are later
|
//! surface, then `backend.present(damage)`. Presents are paced by a ~60 Hz **frame clock**
|
||||||
|
//! (see `schedulePresent`), so any number of client presents and cursor moves inside one
|
||||||
|
//! interval coalesce into a single frame. Shared-memory client surfaces are later
|
||||||
//! (docs/display-v2.md).
|
//! (docs/display-v2.md).
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
@@ -78,6 +80,37 @@ const damage_mode: DamageMode = .grid;
|
|||||||
var damage_list: compositor.DamageList = .{};
|
var damage_list: compositor.DamageList = .{};
|
||||||
var damage_grid: compositor.TileGrid = .{};
|
var damage_grid: compositor.TileGrid = .{};
|
||||||
|
|
||||||
|
/// The **frame clock**: client `present` requests and cursor motion don't repaint
|
||||||
|
/// immediately — they accumulate damage and arm a one-shot timer, and the tick composites
|
||||||
|
/// everything pending as one frame. That paces presents to ~60 Hz no matter how fast
|
||||||
|
/// clients draw or the mouse moves (previously every mouse event became a full present).
|
||||||
|
/// No backend has a real vblank to pace by (docs/display-v2.md, "Fenced is not vsync");
|
||||||
|
/// this is the software stand-in, the same strategy Linux uses atop virtio-gpu. Bring-up
|
||||||
|
/// paths that need pixels on screen *now* (initialise, the self-checks) still call
|
||||||
|
/// `present()` directly.
|
||||||
|
const frame_interval_milliseconds = 16;
|
||||||
|
var frame_timer_armed = false;
|
||||||
|
|
||||||
|
/// Arm the frame clock unless a tick is already pending: any number of requests inside
|
||||||
|
/// one interval coalesce into that single tick's present.
|
||||||
|
fn schedulePresent() void {
|
||||||
|
if (frame_timer_armed) return;
|
||||||
|
frame_timer_armed = true;
|
||||||
|
_ = system.timerOnce(service_endpoint, frame_interval_milliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A timer landing — the frame clock, or the deferred first native present armed by
|
||||||
|
/// `attach_scanout`: present the accumulated damage, then run the one-shot mode-set
|
||||||
|
/// self-check if the native upgrade queued it.
|
||||||
|
fn frameTick() void {
|
||||||
|
frame_timer_armed = false;
|
||||||
|
present();
|
||||||
|
if (pending_modeset_check) {
|
||||||
|
pending_modeset_check = false;
|
||||||
|
modesetSelfCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- geometry helpers -------------------------------------------------------
|
// --- geometry helpers -------------------------------------------------------
|
||||||
|
|
||||||
fn screenRect() Rect {
|
fn screenRect() Rect {
|
||||||
@@ -474,15 +507,15 @@ fn mouseListener(width: u32, height: u32) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consume the latest cursor position from the channel and repaint the cursor layer at
|
/// Consume the latest cursor position from the channel and move the cursor layer to it.
|
||||||
/// it. Runs on the main loop (the compositor owner) in response to a listener poke.
|
/// Runs on the main loop (the compositor owner) in response to a listener poke.
|
||||||
/// `configureLayer` damages both the old and new footprints, so a plain `present`
|
/// `configureLayer` damages both the old and new footprints; the frame clock presents
|
||||||
/// repaints exactly the two rectangles that changed.
|
/// them at the next tick, so a fast mouse coalesces to at most ~60 repaints a second.
|
||||||
fn renderCursor() void {
|
fn renderCursor() void {
|
||||||
const snapshot = cursor_channel.take() orelse return;
|
const snapshot = cursor_channel.take() orelse return;
|
||||||
const id = cursor_layer orelse return;
|
const id = cursor_layer orelse return;
|
||||||
_ = configureLayer(id, snapshot.x, snapshot.y, cursor_z, true);
|
_ = configureLayer(id, snapshot.x, snapshot.y, cursor_z, true);
|
||||||
present();
|
schedulePresent();
|
||||||
if (!cursor_tracking_reported and
|
if (!cursor_tracking_reported and
|
||||||
@abs(snapshot.x - cursor_origin_x) >= cursor_report_threshold and
|
@abs(snapshot.x - cursor_origin_x) >= cursor_report_threshold and
|
||||||
@abs(snapshot.y - cursor_origin_y) >= cursor_report_threshold)
|
@abs(snapshot.y - cursor_origin_y) >= cursor_report_threshold)
|
||||||
@@ -592,7 +625,9 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Han
|
|||||||
return ok(reply);
|
return ok(reply);
|
||||||
},
|
},
|
||||||
@intFromEnum(protocol.Operation.present) => {
|
@intFromEnum(protocol.Operation.present) => {
|
||||||
present();
|
// Scheduled, not immediate: the frame clock composites the accumulated damage
|
||||||
|
// at the next tick, so back-to-back client presents coalesce into one frame.
|
||||||
|
schedulePresent();
|
||||||
return ok(reply);
|
return ok(reply);
|
||||||
},
|
},
|
||||||
@intFromEnum(protocol.Operation.attach_scanout) => {
|
@intFromEnum(protocol.Operation.attach_scanout) => {
|
||||||
@@ -622,21 +657,15 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Han
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Two notification sources reach the compositor. A **message-notification** is a poke
|
/// Two notification sources reach the compositor, and one coalesced badge can carry
|
||||||
/// from the mouse-listener thread (a buffered self-`ipc.send`, `notify_message_bit`):
|
/// both, so each bit is handled independently. A **message-notification** is a poke from
|
||||||
/// repaint the cursor at its latest channel position. Anything else is the post-attach
|
/// the mouse-listener thread (a buffered self-`ipc.send`, `notify_message_bit`): fold the
|
||||||
/// present **timer**: repaint into the freshly attached native surface, verify the frame
|
/// newest cursor position into the scene. A **timer** (`notify_timer_bit`) is the frame
|
||||||
/// landed, then run the one-shot mode-set self-check (V5).
|
/// clock — or the deferred first native present after `attach_scanout` — either way,
|
||||||
|
/// present the accumulated damage.
|
||||||
fn onNotification(badge: u64) void {
|
fn onNotification(badge: u64) void {
|
||||||
if (badge & ipc.notify_message_bit != 0) {
|
if (badge & ipc.notify_message_bit != 0) renderCursor();
|
||||||
renderCursor();
|
if (badge & ipc.notify_timer_bit != 0) frameTick();
|
||||||
return;
|
|
||||||
}
|
|
||||||
present(); // native present + verify (first timer fire after the upgrade)
|
|
||||||
if (pending_modeset_check) {
|
|
||||||
pending_modeset_check = false;
|
|
||||||
modesetSelfCheck();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() void {
|
pub fn main() void {
|
||||||
|
|||||||
Reference in New Issue
Block a user