Compare commits
5
Commits
981a4af7e0
...
ec6e888076
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec6e888076 | ||
|
|
4f02f75602 | ||
|
|
16618d2cdc | ||
|
|
15107f54be | ||
|
|
23f915c593 |
+18
-6
@@ -97,7 +97,7 @@ fn boot() !noreturn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A display resolution in pixels.
|
/// A display resolution in pixels.
|
||||||
const Resolution = struct { width: u32, height: u32 };
|
const Resolution = struct { width: u32, height: u32, refresh_hz: u32 };
|
||||||
|
|
||||||
/// Switch the GPU to the monitor's native resolution (when we can determine it)
|
/// Switch the GPU to the monitor's native resolution (when we can determine it)
|
||||||
/// and read the resulting graphics mode into our own framebuffer description.
|
/// and read the resulting graphics mode into our own framebuffer description.
|
||||||
@@ -128,6 +128,10 @@ fn queryFramebuffer(bs: *uefi.tables.BootServices) !boot_handoff.Framebuffer {
|
|||||||
// Each pixel is 32 bits, so the byte pitch is 4 * pixels-per-row.
|
// Each pixel is 32 bits, so the byte pitch is 4 * pixels-per-row.
|
||||||
.pitch = info.pixels_per_scan_line * 4,
|
.pitch = info.pixels_per_scan_line * 4,
|
||||||
.format = try pixelFormat(info.pixel_format),
|
.format = try pixelFormat(info.pixel_format),
|
||||||
|
// The refresh rate rides the EDID preferred timing. If the firmware kept a
|
||||||
|
// non-native mode it may not describe that mode exactly — but it is the panel's
|
||||||
|
// own clock, a far better frame-clock seed than a hardcoded 60 Hz.
|
||||||
|
.refresh_hz = if (native) |n| n.refresh_hz else 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,10 +180,12 @@ fn nativeResolution(bs: *uefi.tables.BootServices, handles: []uefi.Handle) ?Reso
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the native resolution from a raw EDID block. The first Detailed Timing
|
/// Parse the native resolution and refresh rate from a raw EDID block. The first
|
||||||
/// Descriptor (at byte 54) is the preferred — i.e. native — mode by convention;
|
/// Detailed Timing Descriptor (at byte 54) is the preferred — i.e. native — mode by
|
||||||
/// its active pixel counts are split across low bytes and the high nibbles of
|
/// convention; its active pixel counts are split across low bytes and the high nibbles
|
||||||
/// later bytes.
|
/// of later bytes. The refresh rate is derived, not stored: the descriptor carries the
|
||||||
|
/// pixel clock (10 kHz units) and the active+blanking extents, and
|
||||||
|
/// refresh = clock / (horizontal total × vertical total).
|
||||||
fn edidNative(edid: []const u8) ?Resolution {
|
fn edidNative(edid: []const u8) ?Resolution {
|
||||||
if (edid.len < 128) return null;
|
if (edid.len < 128) return null;
|
||||||
// Every EDID begins with this fixed 8-byte header.
|
// Every EDID begins with this fixed 8-byte header.
|
||||||
@@ -193,7 +199,13 @@ fn edidNative(edid: []const u8) ?Resolution {
|
|||||||
const w = @as(u32, dtd[2]) | (@as(u32, dtd[4] & 0xf0) << 4);
|
const w = @as(u32, dtd[2]) | (@as(u32, dtd[4] & 0xf0) << 4);
|
||||||
const h = @as(u32, dtd[5]) | (@as(u32, dtd[7] & 0xf0) << 4);
|
const h = @as(u32, dtd[5]) | (@as(u32, dtd[7] & 0xf0) << 4);
|
||||||
if (w == 0 or h == 0) return null;
|
if (w == 0 or h == 0) return null;
|
||||||
return .{ .width = w, .height = h };
|
|
||||||
|
const clock_hz = (@as(u64, dtd[0]) | (@as(u64, dtd[1]) << 8)) * 10_000;
|
||||||
|
const h_blank = @as(u64, dtd[3]) | (@as(u64, dtd[4] & 0x0f) << 8);
|
||||||
|
const v_blank = @as(u64, dtd[6]) | (@as(u64, dtd[7] & 0x0f) << 8);
|
||||||
|
const total = (@as(u64, w) + h_blank) * (@as(u64, h) + v_blank);
|
||||||
|
const refresh: u32 = if (total == 0) 0 else @intCast((clock_hz + total / 2) / total);
|
||||||
|
return .{ .width = w, .height = h, .refresh_hz = refresh };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open the kernel on the volume we booted from, read it into a pool buffer,
|
/// Open the kernel on the volume we booted from, read it into a pool buffer,
|
||||||
|
|||||||
@@ -850,6 +850,53 @@ pub fn build(b: *std.Build) void {
|
|||||||
const run_efi_step = b.step("run-x86-64", "Boot the x86-64 kernel in QEMU (UEFI/OVMF); serial0 is logged to zig-out/qemu-test/run-x86-64-serial0-<timestamp>.log");
|
const run_efi_step = b.step("run-x86-64", "Boot the x86-64 kernel in QEMU (UEFI/OVMF); serial0 is logged to zig-out/qemu-test/run-x86-64-serial0-<timestamp>.log");
|
||||||
run_efi_step.dependOn(&run_efi.step);
|
run_efi_step.dependOn(&run_efi.step);
|
||||||
|
|
||||||
|
// --- run-x86-64-gpu: the same boot plus a virtio-gpu adapter ---
|
||||||
|
// The VGA device still supplies the boot (GOP) framebuffer the compositor starts
|
||||||
|
// on; the virtio-gpu function is discovered by the device-manager stack, its
|
||||||
|
// driver announces a shared scanout, and the compositor upgrades off the GOP
|
||||||
|
// floor to fenced, tear-free native presents (docs/display-v2.md).
|
||||||
|
// This is the interactive twin of the `display-native` test case, and 512M
|
||||||
|
// matches it (the whole driver stack + the compositor's surfaces at once).
|
||||||
|
// QEMU shows one head per adapter: pick the virtio-gpu head in the View menu
|
||||||
|
// to watch the native output.
|
||||||
|
const run_gpu = b.addSystemCommand(&.{
|
||||||
|
"qemu-system-x86_64",
|
||||||
|
"-device",
|
||||||
|
"qemu-xhci,id=xhci",
|
||||||
|
"-device",
|
||||||
|
"usb-mouse,bus=xhci.0",
|
||||||
|
"-device",
|
||||||
|
"usb-kbd,bus=xhci.0",
|
||||||
|
"-machine",
|
||||||
|
"q35",
|
||||||
|
"-m",
|
||||||
|
"512M",
|
||||||
|
"-drive",
|
||||||
|
b.fmt("if=pflash,format=raw,readonly=on,file={s}", .{ovmf_code}),
|
||||||
|
});
|
||||||
|
run_gpu.addArg("-drive");
|
||||||
|
run_gpu.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
|
||||||
|
run_gpu.addArg("-drive");
|
||||||
|
run_gpu.addPrefixedFileArg("if=none,id=bootusb,format=raw,file=", fat_image_serial);
|
||||||
|
run_gpu.addArgs(&.{
|
||||||
|
"-device",
|
||||||
|
"usb-storage,bus=xhci.0,drive=bootusb,removable=on,bootindex=0",
|
||||||
|
"-net",
|
||||||
|
"none",
|
||||||
|
"-vga",
|
||||||
|
"none",
|
||||||
|
"-device",
|
||||||
|
"VGA,edid=on,xres=1280,yres=720",
|
||||||
|
"-device",
|
||||||
|
"virtio-gpu-pci",
|
||||||
|
});
|
||||||
|
const gpu_serial_log = b.fmt("{s}/run-x86-64-gpu-serial0-{s}.log", .{ log_dir, timestamp(b) });
|
||||||
|
run_gpu.addArgs(&.{ "-serial", b.fmt("file:{s}", .{gpu_serial_log}) });
|
||||||
|
run_gpu.step.dependOn(&make_log_dir.step);
|
||||||
|
|
||||||
|
const run_gpu_step = b.step("run-x86-64-gpu", "Boot in QEMU with a virtio-gpu adapter: the compositor upgrades to fenced (tear-free) native presents; watch the virtio-gpu head in QEMU's View menu");
|
||||||
|
run_gpu_step.dependOn(&run_gpu.step);
|
||||||
|
|
||||||
// const run_cmd = b.addRunArtifact(exe);
|
// const run_cmd = b.addRunArtifact(exe);
|
||||||
// const run_step = b.step("run", "Run the app");
|
// const run_step = b.step("run", "Run the app");
|
||||||
// run_step.dependOn(&run_cmd.step);
|
// run_step.dependOn(&run_cmd.step);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ lands on its own and ends in a **verifiable gate** — shaped for a `/loop` run,
|
|||||||
|
|
||||||
## Locked decisions (do not relitigate)
|
## Locked decisions (do not relitigate)
|
||||||
|
|
||||||
- **First native backend = virtio-gpu** (VM standard: mode-set + present/flush + vsync).
|
- **First native backend = virtio-gpu** (VM standard: mode-set + fenced present/flush).
|
||||||
- **Dynamic hot-attach**: boot on GOP, upgrade to native when the driver **announces**
|
- **Dynamic hot-attach**: boot on GOP, upgrade to native when the driver **announces**
|
||||||
(push, not polling); re-attach across driver restarts; GOP is the floor for "no driver
|
(push, not polling); re-attach across driver restarts; GOP is the floor for "no driver
|
||||||
ever," not a live fall-back after a reprogram.
|
ever," not a live fall-back after a reprogram.
|
||||||
@@ -46,7 +46,7 @@ Extract scanout from the compositor so today's path becomes one backend among fu
|
|||||||
|
|
||||||
- [x] `system/services/display/backend.zig`: a `Backend` tagged union with `info()`,
|
- [x] `system/services/display/backend.zig`: a `Backend` tagged union with `info()`,
|
||||||
`surface()` (the cacheable compose target), `present(damage)`, and capability flags
|
`surface()` (the cacheable compose target), `present(damage)`, and capability flags
|
||||||
(`canModeSet`/`hasVsync`, both false for GOP).
|
(`canModeSet`/`hasFencedPresent`, both false for GOP).
|
||||||
- [x] The v1 GOP path is now `backend.Gop` (claims the `display` node, WC-maps the LFB,
|
- [x] The v1 GOP path is now `backend.Gop` (claims the `display` node, WC-maps the LFB,
|
||||||
keeps the cacheable back buffer, `present` = the damage-rect WC copy). display.zig
|
keeps the cacheable back buffer, `present` = the damage-rect WC copy). display.zig
|
||||||
composes into `backend.surface()` and calls `backend.present(damage)` — no LFB or
|
composes into `backend.surface()` and calls `backend.present(damage)` — no LFB or
|
||||||
@@ -123,7 +123,7 @@ confirm the composited frame landed (`display: native present verified`), while
|
|||||||
ok` still fires — checked order-independently. Without `-device virtio-gpu-pci` nothing is
|
ok` still fires — checked order-independently. Without `-device virtio-gpu-pci` nothing is
|
||||||
announced and it stays on GOP: the v1 `display-service`/`display-demo` gates pass unchanged.
|
announced and it stays on GOP: the v1 `display-service`/`display-demo` gates pass unchanged.
|
||||||
|
|
||||||
## V5 — Mode-setting, EDID, and vsync ✅
|
## V5 — Mode-setting, EDID, and fenced presents ✅
|
||||||
|
|
||||||
- [x] The driver negotiates `VIRTIO_GPU_F_EDID` (when offered) and reads the monitor's EDID,
|
- [x] The driver negotiates `VIRTIO_GPU_F_EDID` (when offered) and reads the monitor's EDID,
|
||||||
logging its preferred mode; it offers a small mode list over `.scanout` `get_modes`. The
|
logging its preferred mode; it offers a small mode list over `.scanout` `get_modes`. The
|
||||||
@@ -131,14 +131,16 @@ announced and it stays on GOP: the v1 `display-service`/`display-demo` gates pas
|
|||||||
scanout rectangle (no resource/surface churn) — a runtime resolution change. `runtime.display`
|
scanout rectangle (no resource/surface churn) — a runtime resolution change. `runtime.display`
|
||||||
gains `modes()` / `setMode()` (display-protocol `get_modes`/`set_mode`, forwarded to the backend).
|
gains `modes()` / `setMode()` (display-protocol `get_modes`/`set_mode`, forwarded to the backend).
|
||||||
- [x] Every `resource_flush` is issued fenced (`VIRTIO_GPU_FLAG_FENCE`); the device signals the
|
- [x] Every `resource_flush` is issued fenced (`VIRTIO_GPU_FLAG_FENCE`); the device signals the
|
||||||
fence when the frame is on screen, which the used-ring ack the synchronous present waits on
|
fence when it has consumed the frame, which the used-ring ack the synchronous present waits
|
||||||
already gates — a tear-free present.
|
on already gates — a tear-free present. (Completion feedback, **not vblank**: base
|
||||||
- [x] `backend.VirtioGpu` reports `canModeSet` / `hasVsync` = true.
|
virtio-gpu 2D has no display-refresh event, so nothing paces presents to the monitor —
|
||||||
|
see the "Fenced is not vsync" note in [display-v2.md](display-v2.md).)
|
||||||
|
- [x] `backend.VirtioGpu` reports `canModeSet` / `hasFencedPresent` = true.
|
||||||
|
|
||||||
**Gate (met):** the `display-modeset` case (reusing the display-native boot) upgrades to
|
**Gate (met):** the `display-modeset` case (reusing the display-native boot) upgrades to
|
||||||
virtio-gpu, queries the driver's modes, `setMode`s to a different resolution, and confirms the
|
virtio-gpu, queries the driver's modes, `setMode`s to a different resolution, and confirms the
|
||||||
change by reading the backend's geometry back (`display: mode set to {w}x{h}, verified`); the
|
change by reading the backend's geometry back (`display: mode set to {w}x{h}, verified`); the
|
||||||
fenced present path is exercised and confirmed (`display: vsync present ok`) — both from serial,
|
fenced present path is exercised and confirmed (`display: fenced present ok`) — both from serial,
|
||||||
passing 3/3. The driver also logs the EDID preferred mode (`virtio-gpu: EDID preferred mode …`).
|
passing 3/3. The driver also logs the EDID preferred mode (`virtio-gpu: EDID preferred mode …`).
|
||||||
|
|
||||||
## V6 — Resilience (restart + re-attach) + tests + docs ✅
|
## V6 — Resilience (restart + re-attach) + tests + docs ✅
|
||||||
|
|||||||
+18
-10
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Status: complete (V1–V6).** The compositor boots on the GOP framebuffer and, when a
|
**Status: complete (V1–V6).** The compositor boots on the GOP framebuffer and, when a
|
||||||
virtio-gpu driver announces itself, hot-attaches a native backend over the shared `shm`
|
virtio-gpu driver announces itself, hot-attaches a native backend over the shared `shm`
|
||||||
scanout surface — with runtime mode-setting, EDID, and fenced (vsync) presents, and it
|
scanout surface — with runtime mode-setting, EDID, and fenced presents, and it
|
||||||
re-attaches across driver restarts. All serial-gated (see [display-v2-plan.md](display-v2-plan.md)).
|
re-attaches across driver restarts. All serial-gated (see [display-v2-plan.md](display-v2-plan.md)).
|
||||||
|
|
||||||
v1 ([display.md](display.md)) is a compositor that owns the **GOP framebuffer** — it
|
v1 ([display.md](display.md)) is a compositor that owns the **GOP framebuffer** — it
|
||||||
@@ -25,10 +25,10 @@ The compositor itself (layers, back buffer, damage) does not change. Only the la
|
|||||||
scanout backend (selected at runtime — GOP by default, native when it appears)
|
scanout backend (selected at runtime — GOP by default, native when it appears)
|
||||||
│
|
│
|
||||||
├─ GopBackend the v1 path: WC copy back→front to the firmware LFB.
|
├─ GopBackend the v1 path: WC copy back→front to the firmware LFB.
|
||||||
│ Always available. No mode-set, no vsync. THE FLOOR.
|
│ Always available. No mode-set, no present fence. THE FLOOR.
|
||||||
│
|
│
|
||||||
└─ VirtioGpuBackend talks to a virtio-gpu driver process over a `scanout`
|
└─ VirtioGpuBackend talks to a virtio-gpu driver process over a `scanout`
|
||||||
service: present via a shared resource + flush (real vsync),
|
service: present via a shared resource + fenced flush,
|
||||||
EDID mode list, runtime mode-set.
|
EDID mode list, runtime mode-set.
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -37,8 +37,8 @@ A **backend** is a small interface the compositor calls:
|
|||||||
- `surface()` → the pixels to compose into and their geometry `{ptr, pitch, format, w, h}`
|
- `surface()` → the pixels to compose into and their geometry `{ptr, pitch, format, w, h}`
|
||||||
(the LFB for GOP; a shared scanout resource for virtio-gpu),
|
(the LFB for GOP; a shared scanout resource for virtio-gpu),
|
||||||
- `present(damage: Rect)` → make the damaged region visible (a no-op-ish WC copy for GOP;
|
- `present(damage: Rect)` → make the damaged region visible (a no-op-ish WC copy for GOP;
|
||||||
a virtio flush, optionally vsync-fenced, for the native path),
|
a fenced virtio flush for the native path),
|
||||||
- capability queries — `canModeSet`, `hasVsync` — and, when supported, `modes()` /
|
- capability queries — `canModeSet`, `hasFencedPresent` — and, when supported, `modes()` /
|
||||||
`setMode(m)`.
|
`setMode(m)`.
|
||||||
|
|
||||||
The compositor composes into `surface()` and calls `present(damage)` exactly as it does
|
The compositor composes into `surface()` and calls `present(damage)` exactly as it does
|
||||||
@@ -98,23 +98,31 @@ compositor when a second backend arrives"). It claims the virtio-gpu PCI functio
|
|||||||
at a chosen mode for **runtime mode-setting**,
|
at a chosen mode for **runtime mode-setting**,
|
||||||
- registers a `scanout` service and announces to the display service.
|
- registers a `scanout` service and announces to the display service.
|
||||||
|
|
||||||
Its `resource_flush` is the real **present** — and gives a genuine **vsync/tear-free**
|
Its `resource_flush` is the real **present** — and gives a **fenced, tear-free** path a
|
||||||
path a dumb GOP framebuffer can't.
|
dumb GOP framebuffer can't.
|
||||||
|
|
||||||
|
**Fenced is not vsync.** The fence completes when the device has *consumed* the frame:
|
||||||
|
real completion feedback, and tear-freedom by snapshot semantics (the host displays
|
||||||
|
discrete transferred frames, never a half-written surface). It is **not** a vblank —
|
||||||
|
base virtio-gpu 2D has no display-refresh event at all (Linux's driver for this device
|
||||||
|
fakes one with a software timer), so nothing paces presents to the monitor's refresh.
|
||||||
|
Refresh-paced presents need either a native driver's vblank interrupt (delivered over
|
||||||
|
the existing IRQ-as-IPC path) or the compositor's own frame clock.
|
||||||
|
|
||||||
## What v2 unlocks — and its honest scope
|
## What v2 unlocks — and its honest scope
|
||||||
|
|
||||||
Behind the abstraction, a native backend gives runtime **mode-setting** (resolution /
|
Behind the abstraction, a native backend gives runtime **mode-setting** (resolution /
|
||||||
refresh / bpp), **EDID** enumeration, and **vsync**. But only on devices we have a driver
|
refresh / bpp), **EDID** enumeration, and **fenced presents**. But only on devices we have a driver
|
||||||
for — realistically **VMs** (virtio-gpu, and later maybe Bochs DISPI). Real discrete GPUs
|
for — realistically **VMs** (virtio-gpu, and later maybe Bochs DISPI). Real discrete GPUs
|
||||||
need per-vendor KMS-class drivers that aren't getting written, so they **stay on GOP** —
|
need per-vendor KMS-class drivers that aren't getting written, so they **stay on GOP** —
|
||||||
which is genuinely fine (v1 on the NVIDIA box is smooth). So v2's real value is twofold:
|
which is genuinely fine (v1 on the NVIDIA box is smooth). So v2's real value is twofold:
|
||||||
the **pluggable architecture** (a driver slots in when one exists) and a **rich, vsync'd
|
the **pluggable architecture** (a driver slots in when one exists) and a **rich, fenced
|
||||||
path in VMs**, where danos development happens. The framebuffer floor never goes away.
|
path in VMs**, where danos development happens. The framebuffer floor never goes away.
|
||||||
|
|
||||||
## Locked decisions
|
## Locked decisions
|
||||||
|
|
||||||
- **First native backend: virtio-gpu** — the VM standard; gives mode-set + a real
|
- **First native backend: virtio-gpu** — the VM standard; gives mode-set + a real
|
||||||
present/flush (and vsync), and exercises the whole pluggable design. Tested with QEMU
|
present/flush (fenced), and exercises the whole pluggable design. Tested with QEMU
|
||||||
`-device virtio-gpu`.
|
`-device virtio-gpu`.
|
||||||
- **Dynamic hot-attach** — boot on GOP, upgrade to native on the driver's announce,
|
- **Dynamic hot-attach** — boot on GOP, upgrade to native on the driver's announce,
|
||||||
re-attach across driver restarts; GOP is the floor for "no driver ever," not a live
|
re-attach across driver restarts; GOP is the floor for "no driver ever," not a live
|
||||||
|
|||||||
+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),
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ pub const Framebuffer = extern struct {
|
|||||||
height: u32, // visible rows (e.g. 1080)
|
height: u32, // visible rows (e.g. 1080)
|
||||||
pitch: u32, // bytes from the start of one row to the start of the next
|
pitch: u32, // bytes from the start of one row to the start of the next
|
||||||
format: PixelFormat,
|
format: PixelFormat,
|
||||||
|
/// The panel's refresh rate in Hz, computed from its EDID preferred timing (pixel
|
||||||
|
/// clock / total pixels per frame) while GOP was still alive — the one moment it is
|
||||||
|
/// readable (docs/gop.md). 0 = unknown (no EDID). The display service paces its
|
||||||
|
/// frame clock by it; without vblank this fixes the *rate*, never the *phase*.
|
||||||
|
refresh_hz: u32 = 0,
|
||||||
|
|
||||||
/// Whether a usable framebuffer was handed over.
|
/// Whether a usable framebuffer was handed over.
|
||||||
pub fn present(self: Framebuffer) bool {
|
pub fn present(self: Framebuffer) bool {
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ pub const DisplayInfo = extern struct {
|
|||||||
height: u32 = 0, // visible rows
|
height: u32 = 0, // visible rows
|
||||||
pitch: u32 = 0, // bytes from one row's start to the next
|
pitch: u32 = 0, // bytes from one row's start to the next
|
||||||
format: u32 = 0, // a DisplayFormat value
|
format: u32 = 0, // a DisplayFormat value
|
||||||
|
refresh_hz: u32 = 0, // panel refresh rate from EDID (0 = unknown); see boot-handoff
|
||||||
};
|
};
|
||||||
|
|
||||||
/// `DeviceDescriptor.parent` for a device with no parent — a root of the device tree.
|
/// `DeviceDescriptor.parent` for a device with no parent — a root of the device tree.
|
||||||
|
|||||||
@@ -53,13 +53,18 @@ const offered_modes = [_]Mode{ .{ .width = 640, .height = 480 }, .{ .width = 800
|
|||||||
var current_width: u32 = offered_modes[0].width;
|
var current_width: u32 = offered_modes[0].width;
|
||||||
var current_height: u32 = offered_modes[0].height;
|
var current_height: u32 = offered_modes[0].height;
|
||||||
|
|
||||||
/// Monotonic fence id for fenced (vsync) flushes; the device signals the fence when the flush
|
/// Monotonic fence id for fenced flushes; the device signals the fence when the flush is
|
||||||
/// is complete, which its used-ring ack already gates our synchronous present on.
|
/// complete, which its used-ring ack already gates our synchronous present on. Completion
|
||||||
|
/// feedback, not vblank — nothing here is paced to the display's refresh.
|
||||||
var fence_next: u64 = 1;
|
var fence_next: u64 = 1;
|
||||||
|
|
||||||
/// Whether the device offered VIRTIO_GPU_F_EDID, so `get_edid` is worth issuing.
|
/// Whether the device offered VIRTIO_GPU_F_EDID, so `get_edid` is worth issuing.
|
||||||
var edid_available = false;
|
var edid_available = false;
|
||||||
|
|
||||||
|
/// The panel refresh rate parsed from the EDID preferred timing (0 = unknown). Carried to
|
||||||
|
/// the compositor in the announce so its frame clock paces to the panel, not a guess.
|
||||||
|
var edid_refresh_hz: u32 = 0;
|
||||||
|
|
||||||
/// The control virtqueue. We drive it synchronously — one command, notify, poll the used
|
/// The control virtqueue. We drive it synchronously — one command, notify, poll the used
|
||||||
/// ring — so a depth of 16 is ample; we ask the device to shrink to it (virtio 1.0 lets the
|
/// ring — so a depth of 16 is ample; we ask the device to shrink to it (virtio 1.0 lets the
|
||||||
/// driver reduce queue_size), keeping the whole ring inside one page.
|
/// driver reduce queue_size), keeping the whole ring inside one page.
|
||||||
@@ -467,10 +472,18 @@ fn readEdid() void {
|
|||||||
}
|
}
|
||||||
// The first detailed timing descriptor (EDID base-block offset 54) is the preferred mode:
|
// The first detailed timing descriptor (EDID base-block offset 54) is the preferred mode:
|
||||||
// active pixels are 12-bit, low byte + high nibble (bytes 2/4 horizontal, 5/7 vertical).
|
// active pixels are 12-bit, low byte + high nibble (bytes 2/4 horizontal, 5/7 vertical).
|
||||||
|
// The refresh rate is derived from the same descriptor: pixel clock (bytes 0-1, 10 kHz
|
||||||
|
// units) over total (active + blanking) pixels per frame — the loader does the identical
|
||||||
|
// computation for the boot framebuffer (boot/efi.zig edidNative).
|
||||||
const e = &response.edid;
|
const e = &response.edid;
|
||||||
const h_active = @as(u32, e[56]) | (@as(u32, e[58] & 0xF0) << 4);
|
const h_active = @as(u32, e[56]) | (@as(u32, e[58] & 0xF0) << 4);
|
||||||
const v_active = @as(u32, e[59]) | (@as(u32, e[61] & 0xF0) << 4);
|
const v_active = @as(u32, e[59]) | (@as(u32, e[61] & 0xF0) << 4);
|
||||||
log("virtio-gpu: EDID preferred mode {d}x{d}\n", .{ h_active, v_active });
|
const clock_hz = (@as(u64, e[54]) | (@as(u64, e[55]) << 8)) * 10_000;
|
||||||
|
const h_blank = @as(u64, e[57]) | (@as(u64, e[58] & 0x0F) << 8);
|
||||||
|
const v_blank = @as(u64, e[60]) | (@as(u64, e[61] & 0x0F) << 8);
|
||||||
|
const total = (@as(u64, h_active) + h_blank) * (@as(u64, v_active) + v_blank);
|
||||||
|
if (total != 0) edid_refresh_hz = @intCast((clock_hz + total / 2) / total);
|
||||||
|
log("virtio-gpu: EDID preferred mode {d}x{d} @ {d} Hz\n", .{ h_active, v_active, edid_refresh_hz });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Present the whole surface: copy the guest backing into the host resource, then flush it to
|
/// Present the whole surface: copy the guest backing into the host resource, then flush it to
|
||||||
@@ -492,8 +505,9 @@ fn presentFull() bool {
|
|||||||
if (command_nodata(@sizeOf(vg.TransferToHost2d)) != ok_nodata) return false;
|
if (command_nodata(@sizeOf(vg.TransferToHost2d)) != ok_nodata) return false;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
// A fenced flush (vsync): the device signals the fence when the frame is actually on
|
// A fenced flush: the device signals the fence once it has consumed the frame — which
|
||||||
// screen — which its used-ring ack, what our synchronous submit waits on, already gates.
|
// its used-ring ack, what our synchronous submit waits on, already gates. Completion
|
||||||
|
// feedback and a tear-free snapshot, not vblank pacing.
|
||||||
const request = requestAt(vg.ResourceFlush);
|
const request = requestAt(vg.ResourceFlush);
|
||||||
request.* = .{
|
request.* = .{
|
||||||
.hdr = .{ .type = @intFromEnum(vg.CmdType.resource_flush), .flags = vg.flag_fence, .fence_id = fence_next },
|
.hdr = .{ .type = @intFromEnum(vg.CmdType.resource_flush), .flags = vg.flag_fence, .fence_id = fence_next },
|
||||||
@@ -548,6 +562,7 @@ fn announce() void {
|
|||||||
var request = dp.Request{
|
var request = dp.Request{
|
||||||
.operation = @intFromEnum(dp.Operation.attach_scanout),
|
.operation = @intFromEnum(dp.Operation.attach_scanout),
|
||||||
.x = max_width, // the shared surface's row stride in pixels (it is sized to the max mode)
|
.x = max_width, // the shared surface's row stride in pixels (it is sized to the max mode)
|
||||||
|
.y = edid_refresh_hz, // the panel refresh from EDID (0 = unknown) — the frame-clock seed
|
||||||
.width = current_width,
|
.width = current_width,
|
||||||
.height = current_height,
|
.height = current_height,
|
||||||
.colour = display_format_bgrx,
|
.colour = display_format_bgrx,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ pub fn init(device_tree: *const platform.DeviceTree) void {
|
|||||||
/// [[boot-handoff]], not the device tree), so it is seeded explicitly, after `init`.
|
/// [[boot-handoff]], not the device tree), so it is seeded explicitly, after `init`.
|
||||||
/// Returns the new device id, or null when there is no framebuffer (headless) or the
|
/// Returns the new device id, or null when there is no framebuffer (headless) or the
|
||||||
/// table is full. Idempotent-ish: only ever call once per boot.
|
/// table is full. Idempotent-ish: only ever call once per boot.
|
||||||
pub fn seedDisplay(base: u64, width: u32, height: u32, pitch: u32, format: u32) ?u64 {
|
pub fn seedDisplay(base: u64, width: u32, height: u32, pitch: u32, format: u32, refresh_hz: u32) ?u64 {
|
||||||
if (base == 0 or width == 0 or height == 0) return null; // headless
|
if (base == 0 or width == 0 or height == 0) return null; // headless
|
||||||
if (count >= maximum_devices) {
|
if (count >= maximum_devices) {
|
||||||
dropped += 1;
|
dropped += 1;
|
||||||
@@ -79,7 +79,7 @@ pub fn seedDisplay(base: u64, width: u32, height: u32, pitch: u32, format: u32)
|
|||||||
.len = @as(u64, height) * pitch,
|
.len = @as(u64, height) * pitch,
|
||||||
.flags = device_abi.resource_flag_write_combining,
|
.flags = device_abi.resource_flag_write_combining,
|
||||||
};
|
};
|
||||||
d.display = .{ .width = width, .height = height, .pitch = pitch, .format = format };
|
d.display = .{ .width = width, .height = height, .pitch = pitch, .format = format, .refresh_hz = refresh_hz };
|
||||||
devices[count] = d;
|
devices[count] = d;
|
||||||
display_device = d.id;
|
display_device = d.id;
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|||||||
@@ -200,8 +200,8 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
// Publish the loader's framebuffer as a claimable `display` device, so a
|
// Publish the loader's framebuffer as a claimable `display` device, so a
|
||||||
// user-space display service can take it over the same claim + mmio_map path as
|
// user-space display service can take it over the same claim + mmio_map path as
|
||||||
// any other hardware (it is not firmware-discovered; it rides the boot handoff).
|
// any other hardware (it is not firmware-discovered; it rides the boot handoff).
|
||||||
if (devices_broker.seedDisplay(fb.base, fb.width, fb.height, fb.pitch, @intFromEnum(fb.format))) |display_id| {
|
if (devices_broker.seedDisplay(fb.base, fb.width, fb.height, fb.pitch, @intFromEnum(fb.format), fb.refresh_hz)) |display_id| {
|
||||||
log.print("/system/kernel: framebuffer device {d} seeded ({d}x{d}, pitch {d}, write-combining)\n", .{ display_id, fb.width, fb.height, fb.pitch });
|
log.print("/system/kernel: framebuffer device {d} seeded ({d}x{d}, pitch {d}, {d} Hz, write-combining)\n", .{ display_id, fb.width, fb.height, fb.pitch, fb.refresh_hz });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install the device-IRQ trampolines, so a driver's irq_bind has vectors to
|
// Install the device-IRQ trampolines, so a driver's irq_bind has vectors to
|
||||||
|
|||||||
@@ -3404,6 +3404,7 @@ fn displayTest(boot_information: *const BootInformation) void {
|
|||||||
|
|
||||||
check("the node is class display", d.class == @intFromEnum(device_abi.DeviceClass.display));
|
check("the node is class display", d.class == @intFromEnum(device_abi.DeviceClass.display));
|
||||||
check("it carries the framebuffer geometry", d.display.width == fb.width and d.display.height == fb.height and d.display.pitch == fb.pitch);
|
check("it carries the framebuffer geometry", d.display.width == fb.width and d.display.height == fb.height and d.display.pitch == fb.pitch);
|
||||||
|
check("it carries the panel refresh rate", d.display.refresh_hz == fb.refresh_hz);
|
||||||
check("it has exactly one resource", d.resource_count == 1);
|
check("it has exactly one resource", d.resource_count == 1);
|
||||||
const r = d.resources[0];
|
const r = d.resources[0];
|
||||||
check("that resource is a memory window", r.kind == @intFromEnum(device_abi.ResourceKind.memory));
|
check("that resource is a memory window", r.kind == @intFromEnum(device_abi.ResourceKind.memory));
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ const scanout_protocol = runtime.scanout_protocol;
|
|||||||
const Rect = compositor.Rect;
|
const Rect = compositor.Rect;
|
||||||
const Surface = compositor.Surface;
|
const Surface = compositor.Surface;
|
||||||
|
|
||||||
/// The current display mode, as a backend reports it.
|
/// The current display mode, as a backend reports it. `refresh_hz` is the panel's
|
||||||
pub const Info = struct { width: u32, height: u32, pitch: u32, format: u32 };
|
/// refresh rate from EDID (0 = unknown) — the frame clock's pacing seed; without vblank
|
||||||
|
/// it fixes the rate, never the phase (docs/display-v2.md, "Fenced is not vsync").
|
||||||
|
pub const Info = struct { width: u32, height: u32, pitch: u32, format: u32, refresh_hz: u32 };
|
||||||
|
|
||||||
/// Enumeration scratch — a `DeviceDescriptor` is large, and only one scan is ever needed.
|
/// Enumeration scratch — a `DeviceDescriptor` is large, and only one scan is ever needed.
|
||||||
var device_table: [64]device.DeviceDescriptor = undefined;
|
var device_table: [64]device.DeviceDescriptor = undefined;
|
||||||
@@ -26,7 +28,7 @@ var device_table: [64]device.DeviceDescriptor = undefined;
|
|||||||
/// framebuffer write-combining as the front buffer, and keeps a cacheable back buffer of
|
/// framebuffer write-combining as the front buffer, and keeps a cacheable back buffer of
|
||||||
/// the same geometry as the compose target. `present` streams the damaged rectangle from
|
/// the same geometry as the compose target. `present` streams the damaged rectangle from
|
||||||
/// the back buffer to the LFB (sequential WC writes; the LFB is never read). No mode-set,
|
/// the back buffer to the LFB (sequential WC writes; the LFB is never read). No mode-set,
|
||||||
/// no vsync — the portable floor (docs/display-v2.md).
|
/// no present fence — the portable floor (docs/display-v2.md).
|
||||||
pub const Gop = struct {
|
pub const Gop = struct {
|
||||||
device_id: u64,
|
device_id: u64,
|
||||||
front: [*]volatile u8, // the LFB (write-combining)
|
front: [*]volatile u8, // the LFB (write-combining)
|
||||||
@@ -35,13 +37,14 @@ pub const Gop = struct {
|
|||||||
height: u32,
|
height: u32,
|
||||||
pitch: u32,
|
pitch: u32,
|
||||||
format: u32,
|
format: u32,
|
||||||
|
refresh_hz: u32, // from the boot EDID via the display0 node (0 = unknown)
|
||||||
|
|
||||||
/// The framebuffer's id and geometry, captured together. `findDisplay` reads these out of
|
/// The framebuffer's id and geometry, captured together. `findDisplay` reads these out of
|
||||||
/// the enumeration table and returns them by value, so the caller never re-reads the table
|
/// the enumeration table and returns them by value, so the caller never re-reads the table
|
||||||
/// across later syscalls (`device_enumerate` writes the whole table straight into this
|
/// across later syscalls (`device_enumerate` writes the whole table straight into this
|
||||||
/// process's memory; reading a descriptor's tail again after other syscalls have run is a
|
/// process's memory; reading a descriptor's tail again after other syscalls have run is a
|
||||||
/// window we simply avoid by copying the few fields we need up front).
|
/// window we simply avoid by copying the few fields we need up front).
|
||||||
const Found = struct { id: u64, width: u32, height: u32, pitch: u32, format: u32 };
|
const Found = struct { id: u64, width: u32, height: u32, pitch: u32, format: u32, refresh_hz: u32 };
|
||||||
|
|
||||||
/// The first `display`-class device with a *valid* (non-zero) geometry, or null. A zero
|
/// The first `display`-class device with a *valid* (non-zero) geometry, or null. A zero
|
||||||
/// geometry is treated as "not ready yet" so the caller retries — a real framebuffer always
|
/// geometry is treated as "not ready yet" so the caller retries — a real framebuffer always
|
||||||
@@ -52,7 +55,7 @@ pub const Gop = struct {
|
|||||||
for (device_table[0..n]) |*d| {
|
for (device_table[0..n]) |*d| {
|
||||||
if (d.class != @intFromEnum(device.DeviceClass.display)) continue;
|
if (d.class != @intFromEnum(device.DeviceClass.display)) continue;
|
||||||
if (d.display.width == 0 or d.display.height == 0 or d.display.pitch == 0) continue;
|
if (d.display.width == 0 or d.display.height == 0 or d.display.pitch == 0) continue;
|
||||||
return .{ .id = d.id, .width = d.display.width, .height = d.display.height, .pitch = d.display.pitch, .format = d.display.format };
|
return .{ .id = d.id, .width = d.display.width, .height = d.display.height, .pitch = d.display.pitch, .format = d.display.format, .refresh_hz = d.display.refresh_hz };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -93,11 +96,12 @@ pub const Gop = struct {
|
|||||||
.height = found.height,
|
.height = found.height,
|
||||||
.pitch = found.pitch,
|
.pitch = found.pitch,
|
||||||
.format = found.format,
|
.format = found.format,
|
||||||
|
.refresh_hz = found.refresh_hz,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn info(self: *const Gop) Info {
|
pub fn info(self: *const Gop) Info {
|
||||||
return .{ .width = self.width, .height = self.height, .pitch = self.pitch, .format = self.format };
|
return .{ .width = self.width, .height = self.height, .pitch = self.pitch, .format = self.format, .refresh_hz = self.refresh_hz };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The cacheable compose target (the back buffer).
|
/// The cacheable compose target (the back buffer).
|
||||||
@@ -110,22 +114,48 @@ pub const Gop = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stream the damaged rectangle from the back buffer to the write-combining LFB, row by
|
/// Stream each damaged rectangle from the back buffer to the write-combining LFB, row
|
||||||
/// row (sequential writes — what WC memory wants; the LFB is never read).
|
/// by row (sequential writes — what WC memory wants; the LFB is never read). The rows
|
||||||
pub fn present(self: *const Gop, damage: Rect) void {
|
/// are copied by `presentSpan` below, which widens the stores by hand: `volatile`
|
||||||
const c = damage.intersect(.{ .x = 0, .y = 0, .w = @intCast(self.width), .h = @intCast(self.height) });
|
/// keeps the compiler from eliding or reordering framebuffer writes, but it also
|
||||||
if (c.isEmpty()) return;
|
/// forbids it from merging them, so a naive per-pixel loop is stuck at one 4-byte
|
||||||
var y: i32 = c.y;
|
/// store per iteration. Keeping each copy small (the damage list) and each store wide
|
||||||
while (y < c.bottom()) : (y += 1) {
|
/// shrinks the window in which scanout can sample a half-written frame.
|
||||||
const off = @as(usize, @intCast(y)) * self.pitch;
|
pub fn present(self: *const Gop, damage: []const Rect) void {
|
||||||
const src: [*]const u32 = @ptrCast(@alignCast(self.back + off));
|
const bounds = Rect{ .x = 0, .y = 0, .w = @intCast(self.width), .h = @intCast(self.height) };
|
||||||
const dst: [*]volatile u32 = @ptrCast(@alignCast(self.front + off));
|
for (damage) |rect| {
|
||||||
var x: i32 = c.x;
|
const c = rect.intersect(bounds);
|
||||||
while (x < c.right()) : (x += 1) dst[@intCast(x)] = src[@intCast(x)];
|
if (c.isEmpty()) continue;
|
||||||
|
const span: usize = @intCast(c.w);
|
||||||
|
var y: i32 = c.y;
|
||||||
|
while (y < c.bottom()) : (y += 1) {
|
||||||
|
const offset = @as(usize, @intCast(y)) * self.pitch + @as(usize, @intCast(c.x)) * 4;
|
||||||
|
const source: [*]const u32 = @ptrCast(@alignCast(self.back + offset));
|
||||||
|
const front_row: [*]volatile u32 = @ptrCast(@alignCast(self.front + offset));
|
||||||
|
presentSpan(front_row, source, span);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Copy `count` pixels into the write-combining front buffer with 8-byte volatile stores
|
||||||
|
/// (plus a 4-byte head/tail where the span isn't 8-aligned — pixel spans are always
|
||||||
|
/// 4-aligned). The loads come from the cacheable back buffer and are assembled into a
|
||||||
|
/// `u64` in registers, so nothing here reads the front buffer.
|
||||||
|
fn presentSpan(destination: [*]volatile u32, source: [*]const u32, count: usize) void {
|
||||||
|
var i: usize = 0;
|
||||||
|
if (i < count and (@intFromPtr(destination) & 7) != 0) {
|
||||||
|
destination[0] = source[0];
|
||||||
|
i = 1;
|
||||||
|
}
|
||||||
|
while (i + 2 <= count) : (i += 2) {
|
||||||
|
const pair = @as(u64, source[i]) | (@as(u64, source[i + 1]) << 32);
|
||||||
|
const wide: *volatile u64 = @ptrCast(@alignCast(destination + i));
|
||||||
|
wide.* = pair;
|
||||||
|
}
|
||||||
|
if (i < count) destination[i] = source[i];
|
||||||
|
}
|
||||||
|
|
||||||
/// A display mode the native backend can switch to.
|
/// A display mode the native backend can switch to.
|
||||||
pub const Mode = scanout_protocol.Mode;
|
pub const Mode = scanout_protocol.Mode;
|
||||||
|
|
||||||
@@ -143,17 +173,19 @@ pub const VirtioGpu = struct {
|
|||||||
width: u32, // the active mode
|
width: u32, // the active mode
|
||||||
height: u32,
|
height: u32,
|
||||||
format: u32,
|
format: u32,
|
||||||
|
refresh_hz: u32, // from the driver's EDID read, carried in the announce (0 = unknown)
|
||||||
scanout: ipc.Handle, // the driver's present + mode channel (looked up on `.scanout`)
|
scanout: ipc.Handle, // the driver's present + mode channel (looked up on `.scanout`)
|
||||||
|
|
||||||
pub fn info(self: *const VirtioGpu) Info {
|
pub fn info(self: *const VirtioGpu) Info {
|
||||||
return .{ .width = self.width, .height = self.height, .pitch = self.stride * 4, .format = self.format };
|
return .{ .width = self.width, .height = self.height, .pitch = self.stride * 4, .format = self.format, .refresh_hz = self.refresh_hz };
|
||||||
}
|
}
|
||||||
pub fn surface(self: *const VirtioGpu) Surface {
|
pub fn surface(self: *const VirtioGpu) Surface {
|
||||||
return .{ .pixels = self.pixels, .stride = self.stride, .width = self.width, .height = self.height };
|
return .{ .pixels = self.pixels, .stride = self.stride, .width = self.width, .height = self.height };
|
||||||
}
|
}
|
||||||
/// Ask the driver to present. The composited pixels are already in the shared surface, so
|
/// Ask the driver to present. The composited pixels are already in the shared surface, so
|
||||||
/// this is a single request over `.scanout`; the driver transfers + fenced-flushes.
|
/// this is a single request over `.scanout` regardless of how many damage rectangles
|
||||||
pub fn present(self: *const VirtioGpu, damage: Rect) void {
|
/// accumulated; the driver transfers + fenced-flushes the whole frame.
|
||||||
|
pub fn present(self: *const VirtioGpu, damage: []const Rect) void {
|
||||||
_ = damage;
|
_ = damage;
|
||||||
var request = scanout_protocol.Request{
|
var request = scanout_protocol.Request{
|
||||||
.operation = @intFromEnum(scanout_protocol.Operation.present),
|
.operation = @intFromEnum(scanout_protocol.Operation.present),
|
||||||
@@ -210,7 +242,7 @@ pub const Backend = union(enum) {
|
|||||||
inline else => |*b| b.surface(),
|
inline else => |*b| b.surface(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub fn present(self: *const Backend, damage: Rect) void {
|
pub fn present(self: *const Backend, damage: []const Rect) void {
|
||||||
switch (self.*) {
|
switch (self.*) {
|
||||||
inline else => |*b| b.present(damage),
|
inline else => |*b| b.present(damage),
|
||||||
}
|
}
|
||||||
@@ -236,9 +268,13 @@ pub const Backend = union(enum) {
|
|||||||
.virtio => true,
|
.virtio => true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/// Whether this backend has a vblank/fence for tear-free present (virtio-gpu: yes, V5 — every
|
/// Whether this backend's present is **fenced** — it completes only once the device has
|
||||||
/// flush is fenced, so the device signals completion when the frame is actually on screen).
|
/// consumed the frame (virtio-gpu: every flush carries a fence the used-ring ack waits on).
|
||||||
pub fn hasVsync(self: *const Backend) bool {
|
/// A fence gives completion feedback and tear-free snapshot presents; it is *not* vblank —
|
||||||
|
/// nothing paces presents to the display's refresh (base virtio-gpu 2D has no vblank event
|
||||||
|
/// at all). True vsync needs a native driver's vblank interrupt. See docs/display-v2.md,
|
||||||
|
/// "Fenced is not vsync".
|
||||||
|
pub fn hasFencedPresent(self: *const Backend) bool {
|
||||||
return switch (self.*) {
|
return switch (self.*) {
|
||||||
.gop => false,
|
.gop => false,
|
||||||
.virtio => true,
|
.virtio => true,
|
||||||
|
|||||||
@@ -57,6 +57,177 @@ pub const Rect = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The dirty screen regions accumulated between presents. Kept as a *list* of rectangles,
|
||||||
|
/// not one bounding box: when two small things move far apart — the cursor on one side of
|
||||||
|
/// the screen, an animating layer on the other — a single bounding box unites them into a
|
||||||
|
/// huge region, and presenting it streams megabytes to the framebuffer for a few thousand
|
||||||
|
/// changed pixels. The long copy widens the window in which scanout (or QEMU's display
|
||||||
|
/// refresh) samples a half-written frame — visible as tearing and cursor trails. Small
|
||||||
|
/// separate rectangles keep each copy, and that window, tight.
|
||||||
|
///
|
||||||
|
/// A new rectangle that overlaps an existing entry is united into it (repainting a modest
|
||||||
|
/// superset is harmless — compositing is idempotent); the grown entry is *not* re-merged
|
||||||
|
/// against the rest, so entries may overlap, which costs only a duplicate repaint. When
|
||||||
|
/// the table is full the newcomer folds into the last entry — degrading toward the old
|
||||||
|
/// bounding-box behaviour instead of dropping damage.
|
||||||
|
pub const DamageList = struct {
|
||||||
|
pub const capacity = 16;
|
||||||
|
|
||||||
|
rects: [capacity]Rect = [_]Rect{Rect.empty} ** capacity,
|
||||||
|
count: usize = 0,
|
||||||
|
|
||||||
|
pub fn add(self: *DamageList, r: Rect) void {
|
||||||
|
if (r.isEmpty()) return;
|
||||||
|
for (self.rects[0..self.count]) |*existing| {
|
||||||
|
if (!existing.intersect(r).isEmpty()) {
|
||||||
|
existing.* = existing.unite(r);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (self.count < capacity) {
|
||||||
|
self.rects[self.count] = r;
|
||||||
|
self.count += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.rects[capacity - 1] = self.rects[capacity - 1].unite(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEmpty(self: *const DamageList) bool {
|
||||||
|
return self.count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn slice(self: *const DamageList) []const Rect {
|
||||||
|
return self.rects[0..self.count];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(self: *DamageList) void {
|
||||||
|
self.count = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The alternative damage tracker: a **fixed tile grid**, the scheme browser compositors
|
||||||
|
/// and tile-based GPUs use. The screen is divided into `tile_size`-pixel tiles up front;
|
||||||
|
/// `add` marks the tiles a rectangle touches (a bit per tile — merging is free and exact,
|
||||||
|
/// no heuristics), and `collect` walks the grid turning runs of adjacent dirty tiles into
|
||||||
|
/// repaint rectangles (horizontal runs, then equal-span rows merged vertically, so
|
||||||
|
/// full-screen damage collapses back to a single rectangle).
|
||||||
|
///
|
||||||
|
/// Trade-off against `DamageList`: tracking is O(1) with a strictly bounded worst case
|
||||||
|
/// (never more than the dirty tiles), but repaints are quantized — a 1-pixel change
|
||||||
|
/// repaints a whole tile. Which wins depends on the workload; the display service has a
|
||||||
|
/// compile-time switch (`damage_mode`) to compare them.
|
||||||
|
pub const TileGrid = struct {
|
||||||
|
pub const tile_size = 64;
|
||||||
|
pub const maximum_columns = 128; // supports screens up to 8192 px wide…
|
||||||
|
pub const maximum_rows = 128; // …and 8192 px tall (beyond that, edge tiles stretch)
|
||||||
|
pub const maximum_tiles = maximum_columns * maximum_rows;
|
||||||
|
/// The most rectangles `collect` produces; extras fold into the last (never dropped).
|
||||||
|
pub const maximum_rects = 64;
|
||||||
|
|
||||||
|
width: u32 = 0,
|
||||||
|
height: u32 = 0,
|
||||||
|
columns: u32 = 0,
|
||||||
|
rows: u32 = 0,
|
||||||
|
dirty_count: u32 = 0,
|
||||||
|
dirty: [maximum_tiles]bool = [_]bool{false} ** maximum_tiles,
|
||||||
|
|
||||||
|
/// Size the grid for a screen. Also clears it — callers reset on a geometry change,
|
||||||
|
/// where the mode-set paths damage the whole new screen anyway.
|
||||||
|
pub fn reset(self: *TileGrid, width: u32, height: u32) void {
|
||||||
|
self.width = width;
|
||||||
|
self.height = height;
|
||||||
|
self.columns = @min((width + tile_size - 1) / tile_size, maximum_columns);
|
||||||
|
self.rows = @min((height + tile_size - 1) / tile_size, maximum_rows);
|
||||||
|
self.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matches(self: *const TileGrid, width: u32, height: u32) bool {
|
||||||
|
return self.width == width and self.height == height;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEmpty(self: *const TileGrid) bool {
|
||||||
|
return self.dirty_count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(self: *TileGrid) void {
|
||||||
|
@memset(&self.dirty, false);
|
||||||
|
self.dirty_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark every tile `r` touches. Clips to the screen first, so out-of-range
|
||||||
|
/// rectangles are harmless.
|
||||||
|
pub fn add(self: *TileGrid, r: Rect) void {
|
||||||
|
const screen = Rect{ .x = 0, .y = 0, .w = @intCast(self.width), .h = @intCast(self.height) };
|
||||||
|
const c = r.intersect(screen);
|
||||||
|
if (c.isEmpty()) return;
|
||||||
|
const column_first: u32 = @intCast(@divTrunc(c.x, tile_size));
|
||||||
|
const row_first: u32 = @intCast(@divTrunc(c.y, tile_size));
|
||||||
|
const column_last: u32 = @min(@as(u32, @intCast(@divTrunc(c.right() - 1, tile_size))), self.columns - 1);
|
||||||
|
const row_last: u32 = @min(@as(u32, @intCast(@divTrunc(c.bottom() - 1, tile_size))), self.rows - 1);
|
||||||
|
var row = row_first;
|
||||||
|
while (row <= row_last) : (row += 1) {
|
||||||
|
var column = column_first;
|
||||||
|
while (column <= column_last) : (column += 1) {
|
||||||
|
const index = row * self.columns + column;
|
||||||
|
if (!self.dirty[index]) {
|
||||||
|
self.dirty[index] = true;
|
||||||
|
self.dirty_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen rectangle covered by tiles [column_first, column_end) of `row`. Edge
|
||||||
|
/// tiles clamp to the true screen size (the last column/row may be partial — or, on a
|
||||||
|
/// screen wider than the grid supports, stretched to cover the remainder).
|
||||||
|
fn tileSpanRect(self: *const TileGrid, column_first: u32, column_end: u32, row: u32) Rect {
|
||||||
|
const x: i32 = @intCast(column_first * tile_size);
|
||||||
|
const y: i32 = @intCast(row * tile_size);
|
||||||
|
const right: i32 = if (column_end >= self.columns) @intCast(self.width) else @intCast(column_end * tile_size);
|
||||||
|
const bottom: i32 = if (row + 1 >= self.rows) @intCast(self.height) else @intCast((row + 1) * tile_size);
|
||||||
|
return .{ .x = x, .y = y, .w = right - x, .h = bottom - y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn the dirty tiles into repaint rectangles in `out`: coalesce each row's runs of
|
||||||
|
/// adjacent dirty tiles, then merge a run into the rectangle directly above it when
|
||||||
|
/// the spans match — so a dirty block of tiles becomes one rectangle. Returns the
|
||||||
|
/// filled prefix of `out`.
|
||||||
|
pub fn collect(self: *const TileGrid, out: []Rect) []Rect {
|
||||||
|
var count: usize = 0;
|
||||||
|
var row: u32 = 0;
|
||||||
|
while (row < self.rows) : (row += 1) {
|
||||||
|
var column: u32 = 0;
|
||||||
|
while (column < self.columns) {
|
||||||
|
if (!self.dirty[row * self.columns + column]) {
|
||||||
|
column += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var run_end = column + 1;
|
||||||
|
while (run_end < self.columns and self.dirty[row * self.columns + run_end]) run_end += 1;
|
||||||
|
const rect = self.tileSpanRect(column, run_end, row);
|
||||||
|
column = run_end;
|
||||||
|
|
||||||
|
var merged = false;
|
||||||
|
for (out[0..count]) |*existing| {
|
||||||
|
if (existing.x == rect.x and existing.w == rect.w and existing.bottom() == rect.y) {
|
||||||
|
existing.h += rect.h;
|
||||||
|
merged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (merged) continue;
|
||||||
|
if (count < out.len) {
|
||||||
|
out[count] = rect;
|
||||||
|
count += 1;
|
||||||
|
} else {
|
||||||
|
out[count - 1] = out[count - 1].unite(rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out[0..count];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/// A block of 32-bit pixels: `pixels` addressed row-major with `stride` pixels between
|
/// A block of 32-bit pixels: `pixels` addressed row-major with `stride` pixels between
|
||||||
/// row starts (≥ width — the framebuffer's stride is pitch/4, a layer's is its width).
|
/// row starts (≥ width — the framebuffer's stride is pitch/4, a layer's is its width).
|
||||||
pub const Surface = struct {
|
pub const Surface = struct {
|
||||||
@@ -74,15 +245,17 @@ pub const Surface = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Fill `rect` of `s` with the native pixel `colour`, clipped to `s`'s bounds.
|
/// Fill `rect` of `s` with the native pixel `colour`, clipped to `s`'s bounds. Each row is
|
||||||
|
/// one `@memset` over the clipped span, so the compiler vectorizes it and the bounds check
|
||||||
|
/// runs once per row, not once per pixel.
|
||||||
pub fn fillRect(s: Surface, rect: Rect, colour: u32) void {
|
pub fn fillRect(s: Surface, rect: Rect, colour: u32) void {
|
||||||
const c = rect.intersect(s.bounds());
|
const c = rect.intersect(s.bounds());
|
||||||
if (c.isEmpty()) return;
|
if (c.isEmpty()) return;
|
||||||
|
const x0: usize = @intCast(c.x);
|
||||||
|
const span: usize = @intCast(c.w);
|
||||||
var y: i32 = c.y;
|
var y: i32 = c.y;
|
||||||
while (y < c.bottom()) : (y += 1) {
|
while (y < c.bottom()) : (y += 1) {
|
||||||
const r = s.row(@intCast(y));
|
@memset((s.row(@intCast(y)) + x0)[0..span], colour);
|
||||||
var x: i32 = c.x;
|
|
||||||
while (x < c.right()) : (x += 1) r[@intCast(x)] = colour;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,35 +267,36 @@ pub fn composite(dst: Surface, dx: i32, dy: i32, layer: Surface, clip: Rect) voi
|
|||||||
const on_screen = Rect{ .x = dx, .y = dy, .w = @intCast(layer.width), .h = @intCast(layer.height) };
|
const on_screen = Rect{ .x = dx, .y = dy, .w = @intCast(layer.width), .h = @intCast(layer.height) };
|
||||||
const region = on_screen.intersect(clip).intersect(dst.bounds());
|
const region = on_screen.intersect(clip).intersect(dst.bounds());
|
||||||
if (region.isEmpty()) return;
|
if (region.isEmpty()) return;
|
||||||
|
const span: usize = @intCast(region.w);
|
||||||
|
const dst_x: usize = @intCast(region.x);
|
||||||
|
const src_x: usize = @intCast(region.x - dx);
|
||||||
var y: i32 = region.y;
|
var y: i32 = region.y;
|
||||||
while (y < region.bottom()) : (y += 1) {
|
while (y < region.bottom()) : (y += 1) {
|
||||||
const src = layer.row(@intCast(y - dy));
|
const source_row = layer.row(@intCast(y - dy)) + src_x;
|
||||||
const d = dst.row(@intCast(y));
|
const destination_row = dst.row(@intCast(y)) + dst_x;
|
||||||
var x: i32 = region.x;
|
@memcpy(destination_row[0..span], source_row[0..span]);
|
||||||
while (x < region.right()) : (x += 1) {
|
|
||||||
d[@intCast(x)] = src[@intCast(x - dx)];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy a `w`×`h` tile of native pixels from `src` (raw little-endian bytes, row-major,
|
/// Copy a `w`×`h` tile of native pixels from `src` (raw little-endian bytes, row-major,
|
||||||
/// tightly packed) into `dst` at (`dx`, `dy`), clipped to `dst`'s bounds. `src` is read
|
/// tightly packed) into `dst` at (`dx`, `dy`), clipped to `dst`'s bounds. `src` comes
|
||||||
/// with `readInt` because it comes straight out of an IPC message buffer and carries no
|
/// straight out of an IPC message buffer and carries no alignment guarantee, so each
|
||||||
/// alignment guarantee. Returns without touching anything if `src` is short.
|
/// clipped row is a byte-wise `@memcpy` — which equals the old per-pixel little-endian
|
||||||
|
/// `readInt` on every danos target (all little-endian) without the alignment concern.
|
||||||
|
/// Returns without touching anything if `src` is short.
|
||||||
pub fn blitTile(dst: Surface, dx: i32, dy: i32, src: []const u8, w: u32, h: u32) void {
|
pub fn blitTile(dst: Surface, dx: i32, dy: i32, src: []const u8, w: u32, h: u32) void {
|
||||||
if (src.len < @as(usize, w) * h * 4) return;
|
if (src.len < @as(usize, w) * h * 4) return;
|
||||||
var ty: u32 = 0;
|
const region = Rect.init(dx, dy, @intCast(w), @intCast(h)).intersect(dst.bounds());
|
||||||
while (ty < h) : (ty += 1) {
|
if (region.isEmpty()) return;
|
||||||
const yy = dy + @as(i32, @intCast(ty));
|
const span: usize = @intCast(region.w);
|
||||||
if (yy < 0 or yy >= dst.height) continue;
|
const tile_x: usize = @intCast(region.x - dx);
|
||||||
const drow = dst.row(@intCast(yy));
|
const dst_x: usize = @intCast(region.x);
|
||||||
var tx: u32 = 0;
|
var y: i32 = region.y;
|
||||||
while (tx < w) : (tx += 1) {
|
while (y < region.bottom()) : (y += 1) {
|
||||||
const xx = dx + @as(i32, @intCast(tx));
|
const tile_y: usize = @intCast(y - dy);
|
||||||
if (xx < 0 or xx >= dst.width) continue;
|
const offset = (tile_y * w + tile_x) * 4;
|
||||||
const off = (@as(usize, ty) * w + tx) * 4;
|
const destination_row = dst.row(@intCast(y)) + dst_x;
|
||||||
drow[@intCast(xx)] = std.mem.readInt(u32, src[off..][0..4], .little);
|
@memcpy(std.mem.sliceAsBytes(destination_row[0..span]), src[offset..][0 .. span * 4]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +353,92 @@ test "composite honours the damage rectangle" {
|
|||||||
try std.testing.expectEqual(@as(u32, 0), back[4 * 8 + 4]); // outside damage
|
try std.testing.expectEqual(@as(u32, 0), back[4 * 8 + 4]); // outside damage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "damage list keeps disjoint rectangles separate and merges overlap" {
|
||||||
|
var list = DamageList{};
|
||||||
|
list.add(Rect.init(0, 0, 10, 10));
|
||||||
|
list.add(Rect.init(100, 100, 10, 10)); // far away: its own entry
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), list.slice().len);
|
||||||
|
list.add(Rect.init(5, 5, 10, 10)); // overlaps the first: united into it
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), list.slice().len);
|
||||||
|
try std.testing.expectEqual(Rect.init(0, 0, 15, 15), list.slice()[0]);
|
||||||
|
try std.testing.expect(!list.isEmpty());
|
||||||
|
list.clear();
|
||||||
|
try std.testing.expect(list.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "damage list folds overflow into the last entry instead of dropping it" {
|
||||||
|
var list = DamageList{};
|
||||||
|
var i: i32 = 0;
|
||||||
|
while (i < DamageList.capacity) : (i += 1) {
|
||||||
|
list.add(Rect.init(i * 100, 0, 10, 10)); // disjoint: fills every slot
|
||||||
|
}
|
||||||
|
try std.testing.expectEqual(@as(usize, DamageList.capacity), list.slice().len);
|
||||||
|
const overflow = Rect.init(0, 5000, 10, 10);
|
||||||
|
list.add(overflow);
|
||||||
|
try std.testing.expectEqual(@as(usize, DamageList.capacity), list.slice().len);
|
||||||
|
const last = list.slice()[DamageList.capacity - 1];
|
||||||
|
try std.testing.expect(!last.intersect(overflow).isEmpty()); // still covered
|
||||||
|
}
|
||||||
|
|
||||||
|
test "damage list ignores empty rectangles" {
|
||||||
|
var list = DamageList{};
|
||||||
|
list.add(Rect.empty);
|
||||||
|
try std.testing.expect(list.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tile grid coalesces a run of adjacent tiles into one rectangle" {
|
||||||
|
var grid = TileGrid{};
|
||||||
|
grid.reset(256, 128); // 4×2 tiles of 64 px
|
||||||
|
grid.add(Rect.init(10, 10, 100, 10)); // spans tiles (0,0) and (1,0)
|
||||||
|
var scratch: [TileGrid.maximum_rects]Rect = undefined;
|
||||||
|
const rects = grid.collect(&scratch);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), rects.len);
|
||||||
|
try std.testing.expectEqual(Rect.init(0, 0, 128, 64), rects[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tile grid: full-screen damage collapses back to a single rectangle" {
|
||||||
|
var grid = TileGrid{};
|
||||||
|
grid.reset(1280, 720); // 20×12 tiles; the bottom row is partial (720 = 11*64 + 16)
|
||||||
|
grid.add(Rect.init(0, 0, 1280, 720));
|
||||||
|
var scratch: [TileGrid.maximum_rects]Rect = undefined;
|
||||||
|
const rects = grid.collect(&scratch);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), rects.len);
|
||||||
|
try std.testing.expectEqual(Rect.init(0, 0, 1280, 720), rects[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tile grid keeps far-apart damage as separate rectangles" {
|
||||||
|
var grid = TileGrid{};
|
||||||
|
grid.reset(1280, 720);
|
||||||
|
grid.add(Rect.init(0, 0, 10, 10)); // top-left tile
|
||||||
|
grid.add(Rect.init(1000, 600, 10, 10)); // a far-away tile
|
||||||
|
var scratch: [TileGrid.maximum_rects]Rect = undefined;
|
||||||
|
const rects = grid.collect(&scratch);
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), rects.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tile grid clamps edge tiles to the true screen size" {
|
||||||
|
var grid = TileGrid{};
|
||||||
|
grid.reset(100, 100); // 2×2 tiles, both partial in each axis
|
||||||
|
grid.add(Rect.init(0, 0, 100, 100));
|
||||||
|
var scratch: [TileGrid.maximum_rects]Rect = undefined;
|
||||||
|
const rects = grid.collect(&scratch);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), rects.len);
|
||||||
|
try std.testing.expectEqual(Rect.init(0, 0, 100, 100), rects[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tile grid clear empties it and reset resizes it" {
|
||||||
|
var grid = TileGrid{};
|
||||||
|
grid.reset(256, 256);
|
||||||
|
grid.add(Rect.init(0, 0, 256, 256));
|
||||||
|
try std.testing.expect(!grid.isEmpty());
|
||||||
|
grid.clear();
|
||||||
|
try std.testing.expect(grid.isEmpty());
|
||||||
|
try std.testing.expect(grid.matches(256, 256));
|
||||||
|
grid.reset(512, 512);
|
||||||
|
try std.testing.expect(!grid.matches(256, 256));
|
||||||
|
try std.testing.expect(grid.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
test "blitTile copies a packed tile, clipping and reading unaligned bytes" {
|
test "blitTile copies a packed tile, clipping and reading unaligned bytes" {
|
||||||
var back = [_]u32{0} ** (4 * 4);
|
var back = [_]u32{0} ** (4 * 4);
|
||||||
const dst = Surface{ .pixels = &back, .stride = 4, .width = 4, .height = 4 };
|
const dst = Surface{ .pixels = &back, .stride = 4, .width = 4, .height = 4 };
|
||||||
|
|||||||
@@ -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");
|
||||||
@@ -50,8 +52,10 @@ var pending_modeset_check: bool = false;
|
|||||||
var background: u32 = 0;
|
var background: u32 = 0;
|
||||||
|
|
||||||
/// The layer stack. A fixed table (a compositor has few top-level surfaces during
|
/// The layer stack. A fixed table (a compositor has few top-level surfaces during
|
||||||
/// bring-up); each used slot owns an mmap'd surface. `damage` accumulates the dirty
|
/// bring-up); each used slot owns an mmap'd surface. `damage_list` accumulates the dirty
|
||||||
/// screen region since the last `present`, so a present touches only what changed.
|
/// screen rectangles since the last `present`, so a present touches only what changed —
|
||||||
|
/// and keeps far-apart changes (the cursor here, an animating layer there) as *separate*
|
||||||
|
/// small copies rather than one huge bounding box (see compositor.DamageList).
|
||||||
const maximum_layers = 16;
|
const maximum_layers = 16;
|
||||||
|
|
||||||
const Layer = struct {
|
const Layer = struct {
|
||||||
@@ -65,7 +69,67 @@ const Layer = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var layers: [maximum_layers]Layer = [_]Layer{.{}} ** maximum_layers;
|
var layers: [maximum_layers]Layer = [_]Layer{.{}} ** maximum_layers;
|
||||||
var damage: Rect = Rect.empty;
|
|
||||||
|
/// Which damage tracker drives `present` — a compile-time A/B switch (both are in
|
||||||
|
/// compositor.zig with the trade-off discussion):
|
||||||
|
/// .list — free-form dirty rectangles (tight bounds, heuristic merging)
|
||||||
|
/// .grid — a fixed 64-px tile grid (exact O(1) merging, tile-quantized repaints)
|
||||||
|
const DamageMode = enum { list, grid };
|
||||||
|
const damage_mode: DamageMode = .grid;
|
||||||
|
|
||||||
|
var damage_list: compositor.DamageList = .{};
|
||||||
|
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.
|
||||||
|
///
|
||||||
|
/// The interval comes from the *active backend's* panel refresh rate (EDID: the loader
|
||||||
|
/// captures it for the GOP floor while firmware still runs; the native driver reads its
|
||||||
|
/// own and carries it in the announce). `updateFrameClock` re-derives it whenever the
|
||||||
|
/// backend changes — the boot framebuffer's clock dies with the GOP floor at upgrade.
|
||||||
|
/// Without a rate the clock defaults to 60 Hz, and it is clamped to [30, 120] Hz so a
|
||||||
|
/// mis-parsed EDID can neither starve nor flood the compositor.
|
||||||
|
var frame_interval_milliseconds: u64 = 16;
|
||||||
|
var frame_timer_armed = false;
|
||||||
|
|
||||||
|
/// Derive the frame-clock interval from the active backend's refresh rate and log what
|
||||||
|
/// the clock is now pacing to. Called at bring-up and again on every backend change.
|
||||||
|
fn updateFrameClock() void {
|
||||||
|
const reported = backend.info().refresh_hz;
|
||||||
|
const rate: u64 = if (reported == 0) 60 else @min(@max(reported, 30), 120);
|
||||||
|
frame_interval_milliseconds = @max(1000 / rate, 1);
|
||||||
|
var line: [96]u8 = undefined;
|
||||||
|
_ = system.write(std.fmt.bufPrint(&line, "display: frame clock {d} Hz ({s})\n", .{
|
||||||
|
1000 / frame_interval_milliseconds,
|
||||||
|
if (reported == 0) "default" else "panel EDID",
|
||||||
|
}) catch return);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 -------------------------------------------------------
|
||||||
|
|
||||||
@@ -78,9 +142,20 @@ fn layerScreenRect(l: *const Layer) Rect {
|
|||||||
return .{ .x = l.x, .y = l.y, .w = @intCast(l.surface.width), .h = @intCast(l.surface.height) };
|
return .{ .x = l.x, .y = l.y, .w = @intCast(l.surface.width), .h = @intCast(l.surface.height) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add `r` (screen coordinates) to the pending damage, clipped to the screen.
|
/// Add `r` (screen coordinates) to the pending damage, clipped to the screen. In grid
|
||||||
|
/// mode the grid re-sizes itself lazily when the screen geometry changes — every
|
||||||
|
/// geometry-changing path (`attach_scanout`, `set_mode`) damages the whole new screen
|
||||||
|
/// right after, so damage pending from the old geometry is safely superseded.
|
||||||
fn addDamage(r: Rect) void {
|
fn addDamage(r: Rect) void {
|
||||||
damage = damage.unite(r.intersect(screenRect()));
|
const clipped = r.intersect(screenRect());
|
||||||
|
switch (damage_mode) {
|
||||||
|
.list => damage_list.add(clipped),
|
||||||
|
.grid => {
|
||||||
|
const mode = backend.info();
|
||||||
|
if (!damage_grid.matches(mode.width, mode.height)) damage_grid.reset(mode.width, mode.height);
|
||||||
|
damage_grid.add(clipped);
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- layer operations (called from onMessage and the self-check) ------------
|
// --- layer operations (called from onMessage and the self-check) ------------
|
||||||
@@ -183,21 +258,29 @@ fn compositeInto(clip: Rect) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Composite the accumulated damage into the backend's surface, hand it to the backend to
|
/// Composite each accumulated damage rectangle into the backend's surface, hand the list
|
||||||
/// put on screen, then clear the damage. A no-op when nothing is dirty. The frame counter
|
/// to the backend to put on screen, then clear the damage. A no-op when nothing is dirty.
|
||||||
/// advances regardless, so callers can name frames.
|
/// The frame counter advances regardless, so callers can name frames.
|
||||||
fn present() void {
|
fn present() void {
|
||||||
const dirty = damage.intersect(screenRect());
|
var scratch: [compositor.TileGrid.maximum_rects]Rect = undefined;
|
||||||
if (!dirty.isEmpty()) {
|
const dirty: []const Rect = switch (damage_mode) {
|
||||||
compositeInto(dirty);
|
.list => damage_list.slice(),
|
||||||
|
.grid => damage_grid.collect(&scratch),
|
||||||
|
};
|
||||||
|
const had_damage = dirty.len != 0;
|
||||||
|
if (had_damage) {
|
||||||
|
for (dirty) |region| compositeInto(region);
|
||||||
backend.present(dirty);
|
backend.present(dirty);
|
||||||
}
|
}
|
||||||
damage = Rect.empty;
|
switch (damage_mode) {
|
||||||
|
.list => damage_list.clear(),
|
||||||
|
.grid => damage_grid.clear(),
|
||||||
|
}
|
||||||
frames += 1;
|
frames += 1;
|
||||||
|
|
||||||
// The first present after a native upgrade confirms the composited frame actually reached
|
// The first present after a native upgrade confirms the composited frame actually reached
|
||||||
// the shared scanout surface (the automated stand-in for "it's on screen").
|
// the shared scanout surface (the automated stand-in for "it's on screen").
|
||||||
if (pending_native_verify and !dirty.isEmpty()) {
|
if (pending_native_verify and had_damage) {
|
||||||
pending_native_verify = false;
|
pending_native_verify = false;
|
||||||
verifyNativePresent();
|
verifyNativePresent();
|
||||||
}
|
}
|
||||||
@@ -221,7 +304,7 @@ fn verifyNativePresent() void {
|
|||||||
/// present channel, switch the backend to virtio-gpu, and queue a full-screen repaint. The
|
/// present channel, switch the backend to virtio-gpu, and queue a full-screen repaint. The
|
||||||
/// present is deferred to a timer (see `service_endpoint`) so it happens after this reply
|
/// present is deferred to a timer (see `service_endpoint`) so it happens after this reply
|
||||||
/// unblocks the driver and it starts serving `.scanout`.
|
/// unblocks the driver and it starts serving `.scanout`.
|
||||||
fn attachScanout(stride: u32, width: u32, height: u32, format: u32, capability: ?ipc.Handle, reply: []u8) usize {
|
fn attachScanout(stride: u32, width: u32, height: u32, format: u32, refresh_hz: u32, capability: ?ipc.Handle, reply: []u8) usize {
|
||||||
const cap = capability orelse return fail(reply);
|
const cap = capability orelse return fail(reply);
|
||||||
if (width == 0 or height == 0 or stride < width) return fail(reply);
|
if (width == 0 or height == 0 or stride < width) return fail(reply);
|
||||||
const mapped = runtime.shm.map(cap) orelse return fail(reply);
|
const mapped = runtime.shm.map(cap) orelse return fail(reply);
|
||||||
@@ -240,9 +323,11 @@ fn attachScanout(stride: u32, width: u32, height: u32, format: u32, capability:
|
|||||||
.width = width,
|
.width = width,
|
||||||
.height = height,
|
.height = height,
|
||||||
.format = format,
|
.format = format,
|
||||||
|
.refresh_hz = refresh_hz,
|
||||||
.scanout = scanout,
|
.scanout = scanout,
|
||||||
} };
|
} };
|
||||||
background = protocol.pack(format, 0x20, 0x30, 0x48); // re-pack the wallpaper for the mode
|
background = protocol.pack(format, 0x20, 0x30, 0x48); // re-pack the wallpaper for the mode
|
||||||
|
updateFrameClock(); // the GOP floor's clock dies here — pace by the GPU's EDID now
|
||||||
addDamage(screenRect()); // the whole new surface must be painted
|
addDamage(screenRect()); // the whole new surface must be painted
|
||||||
pending_native_verify = true;
|
pending_native_verify = true;
|
||||||
if (!reattach) pending_modeset_check = true; // the mode-set self-check runs once, on first upgrade
|
if (!reattach) pending_modeset_check = true; // the mode-set self-check runs once, on first upgrade
|
||||||
@@ -257,7 +342,8 @@ fn attachScanout(stride: u32, width: u32, height: u32, format: u32, capability:
|
|||||||
/// After the native upgrade is verified, prove the runtime-resolution-change and fenced-present
|
/// After the native upgrade is verified, prove the runtime-resolution-change and fenced-present
|
||||||
/// paths: query the driver's modes, switch to one that differs from the current, re-composite
|
/// paths: query the driver's modes, switch to one that differs from the current, re-composite
|
||||||
/// the whole screen at the new size, and confirm the backend now reports that geometry. The
|
/// the whole screen at the new size, and confirm the backend now reports that geometry. The
|
||||||
/// present goes through the driver's fenced flush, so a clean present is a vsync present.
|
/// present goes through the driver's fenced flush, so a clean present is a *fenced* present —
|
||||||
|
/// completion-acknowledged and tear-free, not vblank-paced (docs/display-v2.md).
|
||||||
fn modesetSelfCheck() void {
|
fn modesetSelfCheck() void {
|
||||||
if (!backend.canModeSet()) return;
|
if (!backend.canModeSet()) return;
|
||||||
var mode_list: [4]backend_mod.Mode = undefined;
|
var mode_list: [4]backend_mod.Mode = undefined;
|
||||||
@@ -289,7 +375,7 @@ fn modesetSelfCheck() void {
|
|||||||
if (now.width == wanted.width and now.height == wanted.height) {
|
if (now.width == wanted.width and now.height == wanted.height) {
|
||||||
var line: [80]u8 = undefined;
|
var line: [80]u8 = undefined;
|
||||||
_ = system.write(std.fmt.bufPrint(&line, "display: mode set to {d}x{d}, verified\n", .{ now.width, now.height }) catch "display: mode set, verified\n");
|
_ = system.write(std.fmt.bufPrint(&line, "display: mode set to {d}x{d}, verified\n", .{ now.width, now.height }) catch "display: mode set, verified\n");
|
||||||
if (backend.hasVsync()) _ = system.write("display: vsync present ok\n");
|
if (backend.hasFencedPresent()) _ = system.write("display: fenced present ok\n");
|
||||||
} else {
|
} else {
|
||||||
_ = system.write("display: mode set FAILED (geometry unchanged)\n");
|
_ = system.write("display: mode set FAILED (geometry unchanged)\n");
|
||||||
}
|
}
|
||||||
@@ -443,15 +529,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)
|
||||||
@@ -500,6 +586,7 @@ fn initialise(endpoint: ipc.Handle) bool {
|
|||||||
_ = system.write(std.fmt.bufPrint(&line, "display: online {d}x{d} pitch {d} format {d}\n", .{
|
_ = system.write(std.fmt.bufPrint(&line, "display: online {d}x{d} pitch {d} format {d}\n", .{
|
||||||
mode.width, mode.height, mode.pitch, mode.format,
|
mode.width, mode.height, mode.pitch, mode.format,
|
||||||
}) catch "display: online\n");
|
}) catch "display: online\n");
|
||||||
|
updateFrameClock();
|
||||||
_ = system.write("display: presented frame 0\n");
|
_ = system.write("display: presented frame 0\n");
|
||||||
|
|
||||||
selfCheck();
|
selfCheck();
|
||||||
@@ -561,11 +648,13 @@ 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) => {
|
||||||
return attachScanout(request.x, request.width, request.height, request.colour, capability, reply);
|
return attachScanout(request.x, request.width, request.height, request.colour, request.y, capability, reply);
|
||||||
},
|
},
|
||||||
@intFromEnum(protocol.Operation.set_mode) => {
|
@intFromEnum(protocol.Operation.set_mode) => {
|
||||||
if (!backend.setMode(request.width, request.height)) return fail(reply);
|
if (!backend.setMode(request.width, request.height)) return fail(reply);
|
||||||
@@ -591,21 +680,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 {
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ pub const Operation = enum(u32) {
|
|||||||
damage = 6,
|
damage = 6,
|
||||||
/// present(): composite the dirty layers and flush to the screen.
|
/// present(): composite the dirty layers and flush to the screen.
|
||||||
present = 7,
|
present = 7,
|
||||||
/// attach_scanout(x=stride, width, height, colour=format) + <surface capability>: a native
|
/// attach_scanout(x=stride, y=refresh_hz, width, height, colour=format) + <surface
|
||||||
/// scanout driver announces itself, handing over the shared scanout surface as an `ipc_call`
|
/// capability>: a native scanout driver announces itself, handing over the shared scanout
|
||||||
/// send_cap. The compositor maps it, looks up the driver's `.scanout` present channel, and
|
/// surface as an `ipc_call` send_cap. The compositor maps it, looks up the driver's
|
||||||
/// upgrades off the GOP floor (docs/display-v2.md V4). `x` is the surface's row stride in
|
/// `.scanout` present channel, and upgrades off the GOP floor (docs/display-v2.md V4).
|
||||||
/// pixels, `colour` the DisplayFormat.
|
/// `x` is the surface's row stride in pixels, `y` the panel refresh rate from the
|
||||||
|
/// driver's EDID read (0 = unknown; paces the compositor's frame clock), `colour` the
|
||||||
|
/// DisplayFormat.
|
||||||
attach_scanout = 8,
|
attach_scanout = 8,
|
||||||
/// set_mode(width, height): change the display resolution — only a native backend that
|
/// set_mode(width, height): change the display resolution — only a native backend that
|
||||||
/// reports `canModeSet` honours it; on the GOP floor it fails (docs/display-v2.md V5).
|
/// reports `canModeSet` honours it; on the GOP floor it fails (docs/display-v2.md V5).
|
||||||
|
|||||||
+6
-5
@@ -221,15 +221,16 @@ CASES = [
|
|||||||
# require all three markers to appear somewhere rather than in a fixed order.
|
# require all three markers to appear somewhere rather than in a fixed order.
|
||||||
"expect": r"(?s)(?=.*display: scanout upgraded to virtio-gpu)(?=.*display: native present verified)(?=.*display-demo: ok)",
|
"expect": r"(?s)(?=.*display: scanout upgraded to virtio-gpu)(?=.*display: native present verified)(?=.*display-demo: ok)",
|
||||||
"fail": r"display: native present FAILED|display: could not|display-demo: (no display|create failed)|CPU EXCEPTION|KERNEL PANIC"},
|
"fail": r"display: native present FAILED|display: could not|display-demo: (no display|create failed)|CPU EXCEPTION|KERNEL PANIC"},
|
||||||
# Mode-set + EDID + vsync (v2 V5): same boot as display-native. After upgrading, the
|
# Mode-set + EDID + fenced presents (v2 V5): same boot as display-native. After upgrading,
|
||||||
# compositor queries the driver's modes, switches to a different resolution, and confirms the
|
# the compositor queries the driver's modes, switches to a different resolution, and confirms
|
||||||
# backend now reports it; the fenced present path makes it a vsync present. (The driver also
|
# the backend now reports it; each present is fenced — completion-acknowledged and tear-free,
|
||||||
# logs the EDID preferred mode during bring-up.) Reuses the display-native kernel scenario.
|
# not vblank-paced (docs/display-v2.md, "Fenced is not vsync"). (The driver also logs the
|
||||||
|
# EDID preferred mode during bring-up.) Reuses the display-native kernel scenario.
|
||||||
{"name": "display-modeset",
|
{"name": "display-modeset",
|
||||||
"build_case": "display-native",
|
"build_case": "display-native",
|
||||||
"qemu_extra": ["-device", "virtio-gpu-pci"],
|
"qemu_extra": ["-device", "virtio-gpu-pci"],
|
||||||
"mem": "512M",
|
"mem": "512M",
|
||||||
"expect": r"(?s)(?=.*display: mode set to \d+x\d+, verified)(?=.*display: vsync present ok)",
|
"expect": r"(?s)(?=.*display: mode set to \d+x\d+, verified)(?=.*display: fenced present ok)",
|
||||||
"fail": r"display: mode set FAILED|display: mode-set self-check: |display: native present FAILED|CPU EXCEPTION|KERNEL PANIC"},
|
"fail": r"display: mode set FAILED|display: mode-set self-check: |display: native present FAILED|CPU EXCEPTION|KERNEL PANIC"},
|
||||||
# Resilience: driver restart + re-attach (v2 V6). device-manager (in test-scanout-restart
|
# Resilience: driver restart + re-attach (v2 V6). device-manager (in test-scanout-restart
|
||||||
# mode) kills the virtio-gpu driver once after it hellos; the restart policy respawns it, it
|
# mode) kills the virtio-gpu driver once after it hellos; the restart policy respawns it, it
|
||||||
|
|||||||
Reference in New Issue
Block a user