display: framebuffer handoff primitive + service design (D1)
Kick off the display service track (docs/display.md, docs/display-plan.md): a
user-space compositor that owns the framebuffer. GOP and the PCI display device
are two views of one controller; GOP dies at ExitBootServices, so the portable
base is the boot-handoff linear framebuffer.
D1 makes that framebuffer reachable from user space over the existing device
claim/mmio_map path rather than a bespoke syscall:
- device-abi: a `display` DeviceClass, a DisplayInfo{w,h,pitch,format} on the
descriptor, and a flags field on resources with a write-combining bit.
- devices-broker: seedDisplay() publishes the loader's framebuffer as a
root-level `display` node (one WC-flagged memory resource); kmain seeds it
after discovery. displayDevice()/displayClaimed() track the claim.
- paging/mmio_map: mapUserDeviceInto gains a write_combining bool — a WC-flagged
resource maps through PAT entry 4 instead of strong-uncacheable (an
uncacheable framebuffer blit is glacial).
- console: falls silent while a display service holds the framebuffer, and is
forced back on by the panic/exception paths.
Gate: the `display` kernel test asserts the seeded node's shape and that the
claim + mmio_map leaf is genuinely write-combining (PAT bit set, PCD/PWT clear).
Regression-checked discovery/ioport/claim-release/supervision/device-list/
device-manager with the +1 device in the table.
This commit is contained in:
parent
f157a93c9c
commit
cd812cc00e
|
|
@ -69,7 +69,12 @@ rather than restate it. Roughly in the order things happen at runtime:
|
|||
mouse, joystick): why a synchronous rendezvous can't fan out to many listeners, the
|
||||
asynchronous `ipc_send` primitive built to fix it, and the per-device subscribe/publish
|
||||
service layered on top.
|
||||
19. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
||||
19. **[display.md](display.md) — the display service.** The display half of the GUI
|
||||
track: a user-space compositor that owns the framebuffer, composes a layer stack into
|
||||
a double buffer, and presents it. Why GOP and the PCI display device are two views of
|
||||
one controller, the device-node + write-combining handoff, and what flicker-free buys
|
||||
that tear-free doesn't. Plan: [display-plan.md](display-plan.md).
|
||||
20. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
||||
how `while (true) hlt` parks the CPU safely once there's nothing left to do.
|
||||
|
||||
Start with the north star:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
# Display service — build plan (v1: the dumb-framebuffer compositor)
|
||||
|
||||
The ordered, checkpointable build-out for [display.md](display.md). Each milestone is
|
||||
small, lands on its own, and ends in a **verifiable gate** — shaped for a `/loop` run.
|
||||
Read [display.md](display.md) first for the *why*; this is the *what* and the *order*.
|
||||
|
||||
## Locked decisions (do not relitigate)
|
||||
|
||||
- **Handoff = device node + write-combining `mmio_map`.** The kernel seeds a synthetic
|
||||
`display0` node from `BootInformation.framebuffer`; the service claims + WC-maps it.
|
||||
(Not a bespoke `framebuffer_map` syscall — the device route inherits ownership,
|
||||
release-on-death, and re-claim-on-restart.)
|
||||
- **v1 = the full compositor pipeline on the dumb framebuffer.** One `display` service
|
||||
owns the LFB + a cacheable back buffer + a layer stack; double-buffer + damage-driven
|
||||
present; clients draw via server-side commands. **No** runtime mode-setting, **no**
|
||||
shared-memory surfaces — both deferred (see display.md, "What v1 does not do").
|
||||
|
||||
## Conventions
|
||||
|
||||
Follow [coding-standards.md](coding-standards.md): spell out non-acronym abbreviations in
|
||||
full, kebab-case file names, no `Co-Authored-By` trailers on commits. New user binaries
|
||||
go through `addUserBinary` in [build.zig](../build.zig) and get packed into the
|
||||
initial-ramdisk; protocols are `b.addModule("…-protocol", …)` and imported into the
|
||||
`runtime` module.
|
||||
|
||||
## How to verify along the way
|
||||
|
||||
- `zig build test` — host unit tests (compositor math: layer clipping, damage merge,
|
||||
pitch/format blits are all host-testable with a fake framebuffer).
|
||||
- `python3 test/qemu_test.py <case>` — boots the real kernel in QEMU; assert on the
|
||||
serial log ([tests.zig](../system/kernel/tests.zig) is the registry).
|
||||
- The `run-efi` target renders to QEMU's display (`-device VGA,edid=on,xres=1280,yres=720`)
|
||||
— a screenshot confirms pixels for the milestones whose gate is visual.
|
||||
|
||||
---
|
||||
|
||||
## D1 — The handoff primitive (kernel) ✅
|
||||
|
||||
Make the boot framebuffer reachable and mappable **write-combining** from user space.
|
||||
|
||||
- [x] [device-abi.zig](../system/devices/device-abi.zig): added `DeviceClass.display`; a
|
||||
`DisplayInfo{ width, height, pitch, format }` carried on the descriptor; a
|
||||
`flags` field on `ResourceDescriptor` + `resource_flag_write_combining`.
|
||||
- [x] [devices-broker.zig](../system/kernel/devices-broker.zig): `seedDisplay(base, w, h,
|
||||
pitch, format)` publishes a root-level `display` node with one WC-flagged `memory`
|
||||
resource `[base, height*pitch]` + the `DisplayInfo`; `displayDevice()` /
|
||||
`displayClaimed()`. Seeded from `kmain` after `devices_broker.init`.
|
||||
- [x] [process.zig](../system/kernel/process.zig) `systemMmioMap` + paging
|
||||
(`mapUserDeviceInto` gains a `write_combining` bool): a resource's WC flag maps it
|
||||
through the WC PAT slot (`setupPat`) instead of strong-uncacheable.
|
||||
- [x] [console.zig](../system/kernel/console.zig): `setSuppressed` quiesces `write` while
|
||||
the display device is claimed (driven from `systemDeviceClaim` / release); the
|
||||
terminal panic + exception paths clear it first so a dying machine still draws.
|
||||
|
||||
**Gate (met, automated):** the `display` kernel test (`python3 test/qemu_test.py display`,
|
||||
`displayTest` in [tests.zig](../system/kernel/tests.zig)) asserts the seeded node's shape
|
||||
and geometry, then walks the real claim + `mmio_map` path into a throwaway address space
|
||||
and verifies the leaf is **write-combining** (PAT entry 4: PAT bit set, PCD/PWT clear) —
|
||||
with an uncacheable-still-uncacheable regression guard. Chosen over the original
|
||||
screenshot-of-a-fill gate because it proves the *actual* WC property headlessly; the
|
||||
visible fill folds into D2's gate (the service clears the screen through the back buffer).
|
||||
Regression-checked: `discovery`, `ioport`, `claim-release`, `supervision`, `device-list`,
|
||||
`device-manager` all still pass with the +1 device in the table.
|
||||
|
||||
## D2 — Service skeleton, protocol, runtime module
|
||||
|
||||
Stand up the named service and the double-buffer, no layers yet.
|
||||
|
||||
- [ ] `system/services/display/protocol.zig`: `Operation{ info, create_layer,
|
||||
configure_layer, destroy_layer, fill_rect, blit_tile, damage, present }`; `extern`
|
||||
`Request`/`Reply`; `message_maximum`/`request_size`/`reply_size` consts. (Model:
|
||||
[block/protocol.zig](../system/services/block/protocol.zig).)
|
||||
- [ ] [abi.zig](../system/abi.zig): `ServiceId.display = 9`.
|
||||
- [ ] `system/services/display/display.zig`: `main` → enumerate + claim + WC-map the LFB
|
||||
(front) → `mmap` a cacheable back buffer of `height*pitch` → `runtime.service.run(
|
||||
.{ .service = .display, .init, .on_message })`. Implement `info` and a whole-screen
|
||||
`present` (back → front) first.
|
||||
- [ ] [library/runtime/display.zig](../library/runtime/runtime.zig) (+ export in
|
||||
`runtime.zig`): `info()`, and a stub `present()`. Cached `.display` lookup with
|
||||
retry (model: [block.zig](../library/runtime/block.zig)).
|
||||
- [ ] [init.zig](../system/services/init/init.zig): add `"display"` to `boot_services`.
|
||||
- [ ] [build.zig](../build.zig): `display-protocol` module; `display` exe via
|
||||
`addUserBinary`; import the protocol into `runtime` and into the exe; pack into the
|
||||
initial-ramdisk; install to `/system/services/display`.
|
||||
|
||||
**Gate:** boot; `init` spawns `display`; it logs `display: online {w}x{h}` and clears the
|
||||
screen to a colour **through the back buffer → present path** (double buffering proven —
|
||||
no direct-to-LFB drawing). Screenshot confirms.
|
||||
|
||||
## D3 — Layer stack + compositor + damage present
|
||||
|
||||
The heart: composite an ordered layer stack, present only what changed.
|
||||
|
||||
- [ ] A layer table (fixed capacity, like the input service's subscriber table): each
|
||||
layer = rect, z-order, visible flag, a server-owned surface (`mmap` cacheable).
|
||||
- [ ] Implement `create_layer` / `configure_layer` / `destroy_layer`, `fill_rect`,
|
||||
`blit_tile` (inline tile in the IPC message), `damage`.
|
||||
- [ ] `composite()`: walk layers bottom-to-top, paint dirty regions into the back buffer
|
||||
(clip to layer rect ∩ damage; handle `rgbx`/`bgrx`; step by `pitch`).
|
||||
- [ ] `present()`: flush merged damage rects back → front (sequential WC writes).
|
||||
- [ ] Host tests (`zig build test`): layer clipping, damage-rect merge, and a
|
||||
`blit`/`fill` against a fake in-memory framebuffer for both pixel formats and a
|
||||
`pitch > width*4` case.
|
||||
|
||||
**Gate:** `zig build test` green for the compositor unit tests; an in-service self-check
|
||||
composites two overlapping layers and the overlap shows the top layer's colour.
|
||||
|
||||
## D4 — Client API + the demo client
|
||||
|
||||
Prove the pipeline end-to-end from a separate process.
|
||||
|
||||
- [ ] Finish [runtime/display.zig](../library/runtime/runtime.zig): a `Layer` handle with
|
||||
`fill` / `blitTile` / `damage`, plus `present()`.
|
||||
- [ ] `system/services/display-demo/`: a hardware-free client (the
|
||||
[`input-source`](../system/services/input-source/) analog) — a wallpaper layer, a
|
||||
layer with a rectangle it moves each frame, and a small cursor layer; `present`s in
|
||||
a loop paced by [`runtime.time`](../library/runtime/time.zig). Wire into build +
|
||||
initial-ramdisk.
|
||||
|
||||
**Gate:** run `run-efi`; a screenshot (or two, apart in time) shows the wallpaper, the
|
||||
cursor, and the rectangle in different positions — motion, from a client, through the
|
||||
compositor, on screen.
|
||||
|
||||
## D5 — Test case + docs
|
||||
|
||||
- [ ] `test/qemu_test.py display` + `displayTest` in
|
||||
[tests.zig](../system/kernel/tests.zig): boot, spawn `display` + `display-demo`,
|
||||
pass on `display: presented frame {N}` and `display-demo: ok` heartbeats.
|
||||
- [ ] Flip [display.md](display.md)'s status notes from "planned/built" as appropriate;
|
||||
confirm the [README index](README.md) entry; update the `display-track` memory to
|
||||
DONE with the commit.
|
||||
|
||||
**Gate:** `python3 test/qemu_test.py display` passes in CI-equivalent local run.
|
||||
|
||||
---
|
||||
|
||||
## Deferred (explicitly not in this plan)
|
||||
|
||||
- **Shared-memory surfaces** — generalize M13 capability passing to memory objects
|
||||
(`shm_create`/`shm_map`), so bitmap clients hand the compositor a rendered surface
|
||||
instead of drawing commands. The compositor's layer model already anticipates it.
|
||||
- **Native backend (Bochs DISPI, then virtio-gpu)** — behind the same internal backend
|
||||
interface as the dumb framebuffer: EDID mode list + runtime resolution/bpp change +
|
||||
(eventually) a vblank/flip path for true vsync.
|
||||
- **Driver/compositor process split** — only when a second backend or a second head makes
|
||||
the abstraction pay for itself.
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
# The display service: a framebuffer compositor
|
||||
|
||||
The [framebuffer](framebuffer.md) the loader hands over is a flat block of pixel
|
||||
memory, and the kernel's [bootstrap console](../system/kernel/console.zig) draws text
|
||||
into it directly. That console is a stop-gap. The **display service**
|
||||
(`system/services/display/`) is the real thing: an ordinary ring-3 process that *owns*
|
||||
the framebuffer, composes a stack of **layers** into an off-screen back buffer, and
|
||||
**presents** finished frames to the screen — the display half of the GUI track
|
||||
([vision.md](vision.md)), the sibling of the [input service](input.md).
|
||||
|
||||
This note is the architecture and the reasoning behind it. The concrete build order
|
||||
lives in [display-plan.md](display-plan.md).
|
||||
|
||||
## First, a distinction that shapes everything: GOP vs. the PCI device
|
||||
|
||||
It is tempting to think "the GOP framebuffer" and "the VGA-compatible display
|
||||
controller in the PCIe tree" are two different things. They are not — they are **two
|
||||
interfaces to the same silicon, at different times and different levels**, and knowing
|
||||
which one you're holding decides what you can do.
|
||||
|
||||
- **GOP is firmware's *temporary* driver** for the display controller. It gives you a
|
||||
linear framebuffer pointer and can set video modes — but only until
|
||||
`ExitBootServices`. The loader already leans on this: [`queryFramebuffer`](../boot/efi.zig)
|
||||
reads the monitor's EDID, picks the native mode, and calls `set_mode` **before**
|
||||
exiting ([gop.md](gop.md)). Once the kernel runs, GOP is **gone** — no `set_mode`, no
|
||||
mode list, no EDID. What survives is the frozen snapshot in
|
||||
[`BootInformation.framebuffer`](../system/boot-handoff.zig): `{base, width, height,
|
||||
pitch, format}`, and nothing more.
|
||||
|
||||
- **The PCI class-0x03 device is the raw controller** — BARs, config space, registers,
|
||||
IO ports. It is what you actually *own* after boot. On QEMU's emulated adapter
|
||||
([`-device VGA,edid=on`](../build.zig), the Bochs VBE/DISPI model) the `base` GOP handed
|
||||
you *is* that device's linear-framebuffer BAR — the same physical memory, seen through
|
||||
a different door. On a real discrete GPU, GOP's `base` is an aperture inside the GPU's
|
||||
VRAM BAR. danos already decodes this device
|
||||
([pci-class.zig](../system/devices/pci-class.zig) has the full `display` namespace, and
|
||||
`pci-bus` already reports it to the [device manager](device-manager.md) with its class
|
||||
triple) — but nothing binds it yet.
|
||||
|
||||
What that difference costs you, concretely:
|
||||
|
||||
| You want to… | Dumb GOP framebuffer (boot handoff) | Native device driver (PCI 0x03) |
|
||||
|-------------------------------------------|-------------------------------------|------------------------------------------|
|
||||
| **Report** the current mode | ✅ from the handoff | ✅ |
|
||||
| **Change resolution / bpp at runtime** | ❌ GOP is gone | ✅ program DISPI regs / virtio-gpu queue |
|
||||
| **Re-read EDID, enumerate monitor modes** | ❌ | ✅ the device exposes an EDID block |
|
||||
| **Refresh rate** | ❌ (virtual anyway) | only a real KMS driver — far future |
|
||||
| **vblank / tear-free present** | ❌ no vblank signal | ✅ vblank IRQ + page-flip (real GPUs) |
|
||||
| **Works on the Pi (no PCI VGA)** | ✅ VideoCore hands a simple FB | ✗ per-device |
|
||||
|
||||
The lesson: the **portable base for the whole GUI stack is the GOP / boot-handoff linear
|
||||
framebuffer**. Runtime mode-setting is a *per-device upgrade* layered on top — and on
|
||||
the Raspberry Pis there is no PCI VGA at all, so the neutral framebuffer is the only
|
||||
thing all three target machines share. That is why the display service is built on the
|
||||
dumb framebuffer first, with the native backend as an optional module behind the same
|
||||
interface.
|
||||
|
||||
## Two constraints this service exists to meet
|
||||
|
||||
Like the input service — which existed partly to motivate the asynchronous
|
||||
[`ipc_send`](ipc.md) primitive — the display service runs straight into two limits the
|
||||
rest of the system hasn't had to face:
|
||||
|
||||
1. **The framebuffer is kernel-only today.** It arrives through the boot handoff, is
|
||||
mapped into the kernel's physmap, and is touched only by
|
||||
[`console.zig`](../system/kernel/console.zig). It is *not* a
|
||||
[devices-broker](../system/kernel/devices-broker.zig) node, so
|
||||
`device.claim`/`mmio_map` cannot reach it, and there is no framebuffer
|
||||
[syscall](syscall.md). A user-space display service needs a **new mechanism just to
|
||||
touch the pixels**. (See "The handoff" below — this is built.)
|
||||
|
||||
2. **danos has no cross-process shared memory.** The memory syscalls are `mmap`
|
||||
(private, zeroed), `mmio_map` (a *claimed device's* MMIO), and `dma_alloc` (new
|
||||
pinned physical). The block driver's "pass a buffer by physical address" trick
|
||||
([block/protocol.zig](../system/services/block/protocol.zig)) works *only because its
|
||||
consumer is DMA hardware*. A compositor that CPU-reads and blends client layers can't
|
||||
use it — it would have to *map* another process's memory, which nothing allows. This
|
||||
is deferred (see "What v1 does not do"), because v1 sidesteps it entirely.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
kernel ── owns the boot framebuffer; bootstrap console only
|
||||
│ seeds a "display0" device node from BootInformation.framebuffer
|
||||
│ (ResourceKind.memory = [base, height*pitch], write-combining hint,
|
||||
│ plus DisplayInfo{width, height, pitch, format})
|
||||
▼
|
||||
display service (system/services/display/, ServiceId.display) ← the compositor
|
||||
│ device.claim(display0) → mmio_map(WRITE-COMBINING) = FRONT buffer (the LFB)
|
||||
│ mmap(cacheable) a BACK buffer of the same geometry
|
||||
│ owns: an ordered LAYER STACK + a per-frame DAMAGE list
|
||||
│ loop: composite dirty layers → back buffer → present dirty rects → front
|
||||
│ backend is an INTERNAL interface: {gop-fb} today; {bochs-dispi, virtio-gpu} later
|
||||
▼ reached by name (ipc_lookup); clients drive it over the display protocol
|
||||
┌────────────────────────────────────┬──────────────────────────────────────┐
|
||||
drawing clients (v1) surface clients (deferred)
|
||||
runtime.display commands: runtime.display surfaces:
|
||||
create_layer / configure_layer shm_create → pass as a capability →
|
||||
fill_rect / blit_tile / damage the compositor maps & composites the
|
||||
present client-rendered bitmap directly
|
||||
```
|
||||
|
||||
The bring-up sequence mirrors a hardware driver's — it is the
|
||||
[`usb-xhci-bus` `initialise`](../system/drivers/usb-xhci-bus/usb-xhci-bus.zig) shape
|
||||
(claim → `mmio_map` → run loop) — and the request/reply service shell is the
|
||||
[FAT](../system/services/fat/fat.zig) / [input](../system/services/input/input.zig) shape
|
||||
([`runtime.service.run`](../library/runtime/service.zig) with a `protocol.zig` of
|
||||
`extern struct` messages and an `Operation` tag).
|
||||
|
||||
**One process, for now.** v1 is a *single* service that both owns the framebuffer and
|
||||
composites — it does not split a "framebuffer driver" from a "compositor" the way input
|
||||
splits `ps2-bus` from the input service. The backend (dumb FB vs. a native GPU) is an
|
||||
*internal* interface, not a process boundary. That boundary earns its keep only when a
|
||||
second backend or a second monitor appears; until then it is complexity with no payoff.
|
||||
|
||||
## The handoff: a device node + a write-combining map
|
||||
|
||||
The framebuffer crosses into user space through the machinery that already exists for
|
||||
every other device, rather than a bespoke syscall — so it inherits ownership,
|
||||
release-on-death, and re-claim-on-restart for free (the [resilience](resilience.md)
|
||||
story: a crashed display service returns the LFB to the kernel, and its restart
|
||||
re-claims it).
|
||||
|
||||
- The kernel seeds a synthetic **`display0`** node into the
|
||||
[devices-broker](../system/kernel/devices-broker.zig) at init, from
|
||||
`BootInformation.framebuffer`: one `ResourceKind.memory` resource spanning
|
||||
`[base, height*pitch]`, tagged **write-combining**, plus a small
|
||||
`DisplayInfo{width, height, pitch, format}` (the memory resource says *where* and *how
|
||||
big*; `DisplayInfo` says how to *interpret* the bytes).
|
||||
- The service `device.claim`s it and `mmio_map`s the resource. The map is
|
||||
**write-combining**, not the strong-uncacheable that `mmio_map` uses for register
|
||||
MMIO. The kernel already programs a WC PAT slot for its own console
|
||||
([`setupPat`](../system/kernel/architecture/x86_64/paging.zig)); this reaches it from
|
||||
the user mapping path. **This matters:** an uncacheable framebuffer makes the
|
||||
back→front blit unusably slow.
|
||||
- On `claim`, the kernel's bootstrap console goes quiet, so the two never fight over the
|
||||
LFB. A panic is the one exception — by then the service is likely dead anyway, and a
|
||||
panic on screen wins.
|
||||
|
||||
The display service is a **named boot service**: `init` spawns it by name alongside
|
||||
`vfs`/`input`/`device-manager` ([init.zig](../system/services/init/init.zig)), and it
|
||||
self-discovers `display0` with `device.enumerate`. The [device manager](device-manager.md)
|
||||
matching path (PCI class 0x03 → a driver) is reserved for the future *native* backend, not
|
||||
this singleton synthetic node.
|
||||
|
||||
## Double buffering and the write-combining discipline
|
||||
|
||||
Two buffers, with deliberately different memory types:
|
||||
|
||||
- The **front buffer** is the LFB — **write-combining**: fast to *write*, slow to
|
||||
*read*. The rule is therefore **never read the front buffer**. Only ever stream into
|
||||
it, sequentially.
|
||||
- The **back buffer** is ordinary **cacheable** RAM (`mmap`), the same geometry. All
|
||||
compositing happens here, where reads and read-modify-write blends are cheap.
|
||||
|
||||
So a frame is: compose every dirty layer into the cacheable back buffer, then **present**
|
||||
— copy the changed regions back→front in sequential, WC-friendly writes. Two details the
|
||||
[framebuffer](framebuffer.md) note already establishes carry over: step rows by `pitch`,
|
||||
not `width*4`; and handle both `rgbx` and `bgrx` [pixel formats](gop.md).
|
||||
|
||||
## Flicker vs. tearing — what double buffering does and doesn't buy
|
||||
|
||||
These are two different artifacts, and the dumb framebuffer fixes exactly one of them:
|
||||
|
||||
- **Flicker** is the user seeing intermediate, half-drawn states (a clear-then-redraw
|
||||
flash). Double buffering **eliminates it completely** — the screen only ever receives
|
||||
whole, finished frames.
|
||||
- **Tearing** is a present landing while the display's scanout beam is mid-frame, so the
|
||||
top of the screen shows the new frame and the bottom the old. Avoiding it requires
|
||||
presenting during the vertical blank (**vsync**) — which needs a vblank signal. **A
|
||||
dumb GOP framebuffer has no vblank.**
|
||||
|
||||
So v1 is **flicker-free**, and it *minimizes* the tear window by presenting only damaged
|
||||
rectangles (less to copy → a smaller window in which the beam can catch a half-updated
|
||||
frame), but it is **not tear-free**. Genuine vsync waits for a backend with a vblank IRQ
|
||||
or a flush/flip path — a native-device capability, not something the firmware
|
||||
framebuffer can offer. Stated plainly here so the limitation is understood, not
|
||||
discovered.
|
||||
|
||||
## Layers and the client protocol
|
||||
|
||||
The compositor holds an **ordered stack of layers**. Each layer has a rectangle, a
|
||||
z-order, a visibility flag, and a surface. Presenting walks the stack bottom-to-top,
|
||||
painting each dirty layer into the back buffer, then flushes the damage to the front.
|
||||
|
||||
In v1 the surfaces are **server-owned**, and clients draw into them with a small
|
||||
immediate-mode command protocol — essentially the model early X used, and enough for a
|
||||
shell, a terminal, a cursor, and a wallpaper:
|
||||
|
||||
| Operation | Meaning |
|
||||
|--------------------|---------------------------------------------------------------|
|
||||
| `info` | report `{width, height, pitch, format}` of the display |
|
||||
| `create_layer` | allocate a server-owned surface, return a layer handle |
|
||||
| `configure_layer` | set a layer's rect, z-order, visibility |
|
||||
| `destroy_layer` | release a layer |
|
||||
| `fill_rect` | fill a rectangle of a layer with a colour |
|
||||
| `blit_tile` | copy a small client-supplied pixel tile into a layer (inline) |
|
||||
| `damage` | mark a region of a layer dirty |
|
||||
| `present` | composite dirty layers and flush to the screen |
|
||||
|
||||
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
|
||||
client). Keeping the protocol to rectangles and tiles keeps the compositor small and the
|
||||
policy in the client.
|
||||
|
||||
## `runtime.display`
|
||||
|
||||
Clients speak the protocol through a new [`library/runtime/display.zig`](../library/runtime/runtime.zig),
|
||||
the [`runtime.block`](../library/runtime/block.zig) shape (a cached `.display` lookup
|
||||
with a boot-race retry): `display.info()`, a `Layer` handle with `fill` / `blitTile` /
|
||||
`damage`, and `present()`. Application code never issues the raw syscalls — it calls the
|
||||
runtime, as with every other danos service.
|
||||
|
||||
## What v1 does not do (and why that's fine)
|
||||
|
||||
Two capabilities are deliberately out of the first cut. Neither reshapes anything above;
|
||||
both are clean additions behind the interfaces v1 establishes.
|
||||
|
||||
- **Client-rendered surfaces (shared memory).** The fast path for a bitmap-heavy app is
|
||||
to render into its *own* buffer and hand the compositor a *reference*, not a stream of
|
||||
commands. That needs the missing cross-process shared-memory primitive — best built as
|
||||
the natural generalization of the existing M13 [capability passing](driver-model.md)
|
||||
from *endpoints* to *memory objects* (`shm_create(len) → {cap, vaddr}`, pass `cap` on
|
||||
an `ipc_call`, receiver `shm_map(cap) → vaddr`). v1 avoids it because server-owned
|
||||
surfaces already prove the whole pipeline.
|
||||
|
||||
- **Runtime mode-setting (a native backend).** Detecting the EDID mode list and changing
|
||||
resolution / bpp at runtime needs the raw PCI device. The first native backend is
|
||||
Bochs DISPI — the register interface QEMU's `-device VGA` exposes — behind the same
|
||||
internal backend interface the dumb framebuffer sits behind. Refresh-rate and colour
|
||||
management (a gamma LUT) are real-GPU-KMS territory, far beyond this.
|
||||
|
||||
## Verifying it
|
||||
|
||||
A `display` test case ([tests.zig](../system/kernel/tests.zig), `python3
|
||||
test/qemu_test.py display`) boots the kernel, spawns the service and a synthetic
|
||||
`display-demo` client (the hardware-free analog of
|
||||
[`input-source`](../system/services/input-source/)) that drives layers — a wallpaper, a
|
||||
moving rectangle, a cursor. It passes on a serial heartbeat (`display: presented frame N`
|
||||
/ `display-demo: ok`), proving a frame travelled client → compositor → screen, exactly
|
||||
as the [input test](input.md) proves an event travels source → service → subscriber.
|
||||
|
||||
## See also
|
||||
|
||||
- [framebuffer.md](framebuffer.md) — the linear framebuffer, pitch vs. width, `volatile`.
|
||||
- [gop.md](gop.md) — GOP, and why only linear RGBX/BGRX modes are paintable.
|
||||
- [input.md](input.md) — the sibling service; the async `ipc_send` fan-out.
|
||||
- [driver-model.md](driver-model.md) — claim / `mmio_map`, capability passing, the trust model.
|
||||
- [device-manager.md](device-manager.md) — matching and supervision (the native backend's route).
|
||||
- [display-plan.md](display-plan.md) — the ordered build-out.
|
||||
|
|
@ -38,6 +38,13 @@ pub const DeviceClass = enum(u32) {
|
|||
/// resources; the (class, subclass, protocol) triple that says what it is
|
||||
/// travels in the bus report's identity, not here.
|
||||
usb_device,
|
||||
/// A scanout framebuffer: a linear region of pixel memory the display service
|
||||
/// claims and maps. Unlike the other classes this one is not firmware-discovered
|
||||
/// — the kernel seeds it from the loader's [[boot-handoff]] framebuffer
|
||||
/// (`devices_broker.seedDisplay`). Its one `memory` resource is the framebuffer,
|
||||
/// flagged write-combining; the geometry to interpret it travels in
|
||||
/// `DeviceDescriptor.display`.
|
||||
display,
|
||||
unknown,
|
||||
};
|
||||
|
||||
|
|
@ -59,10 +66,39 @@ pub const ResourceDescriptor = extern struct {
|
|||
kind: u64, // a ResourceKind value
|
||||
start: u64,
|
||||
len: u64,
|
||||
/// A bitmask of `resource_flag_*` hints. Zero for a plain register/RAM window;
|
||||
/// the kernel reads it when it maps the resource. Defaulted so every existing
|
||||
/// literal (which never set flags) keeps compiling and lays out identically.
|
||||
flags: u64 = 0,
|
||||
};
|
||||
|
||||
/// `ResourceDescriptor.flags`: map this `memory` resource **write-combining** rather
|
||||
/// than strong-uncacheable — for a framebuffer, where batched bursts to pixel memory
|
||||
/// are the whole point (an uncacheable framebuffer blit is glacial). See
|
||||
/// `mmio_map` (system/kernel/process.zig) and `setupPat` (…/x86_64/paging.zig).
|
||||
pub const resource_flag_write_combining: u64 = 1 << 0;
|
||||
|
||||
pub const maximum_device_resources = 8;
|
||||
|
||||
/// The byte order of a display's pixels — mirrors the loader's `PixelFormat`
|
||||
/// ([[boot-handoff]]) with the same numeric values, but lives here so user space
|
||||
/// (which must never import the loader↔kernel handoff) can name it. Only the two
|
||||
/// linear 32-bpp layouts a console can paint into exist; see docs/gop.md.
|
||||
pub const DisplayFormat = enum(u32) {
|
||||
rgbx = 0, // byte 0 = Red, 1 = Green, 2 = Blue, 3 = reserved
|
||||
bgrx = 1, // byte 0 = Blue, 1 = Green, 2 = Red, 3 = reserved
|
||||
};
|
||||
|
||||
/// The geometry of a `display` device's framebuffer, carried in its descriptor so a
|
||||
/// claiming driver knows how to interpret the pixel bytes its `memory` resource maps.
|
||||
/// `pitch` is bytes per row (may exceed `width * 4`; see docs/framebuffer.md).
|
||||
pub const DisplayInfo = extern struct {
|
||||
width: u32 = 0, // visible pixels per row
|
||||
height: u32 = 0, // visible rows
|
||||
pitch: u32 = 0, // bytes from one row's start to the next
|
||||
format: u32 = 0, // a DisplayFormat value
|
||||
};
|
||||
|
||||
/// `DeviceDescriptor.parent` for a device with no parent — a root of the device tree.
|
||||
pub const no_parent: u64 = ~@as(u64, 0);
|
||||
|
||||
|
|
@ -92,4 +128,9 @@ pub const DeviceDescriptor = extern struct {
|
|||
resource_count: u64,
|
||||
hid: [8]u8,
|
||||
resources: [maximum_device_resources]ResourceDescriptor,
|
||||
// Framebuffer geometry, meaningful only when `class` is `DeviceClass.display`
|
||||
// (zeroed otherwise). Kept here — a class-specific field on the shared descriptor —
|
||||
// the same way `pci_class` is meaningful only for `pci_device` and `hid` only for
|
||||
// `acpi_device`.
|
||||
display: DisplayInfo = .{},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -168,10 +168,18 @@ pub fn mapUserPageInto(root: u64, virtual: u64, physical: u64, writable: bool, e
|
|||
paging.mapUserInto(root, virtual, physical, writable, executable);
|
||||
}
|
||||
|
||||
/// Map a device MMIO window into address space `root`: strong-uncacheable, RW+NX,
|
||||
/// and marked so teardown won't free the MMIO frames as RAM. For IO passthrough.
|
||||
pub fn mapUserDeviceInto(root: u64, virtual: u64, physical: u64, len: u64) void {
|
||||
paging.mapUserDeviceInto(root, virtual, physical, len);
|
||||
/// Map a device MMIO window into address space `root`: RW+NX, and marked so teardown
|
||||
/// won't free the MMIO frames as RAM. `write_combining` picks the cache type —
|
||||
/// false = strong-uncacheable (registers), true = write-combining (a framebuffer).
|
||||
/// For IO passthrough.
|
||||
pub fn mapUserDeviceInto(root: u64, virtual: u64, physical: u64, len: u64, write_combining: bool) void {
|
||||
paging.mapUserDeviceInto(root, virtual, physical, len, write_combining);
|
||||
}
|
||||
|
||||
/// Is the user leaf mapping `virtual` in address space `root` write-combining? Null if
|
||||
/// unmapped. For tests verifying the framebuffer map's cache type.
|
||||
pub fn userLeafIsWriteCombining(root: u64, virtual: u64) ?bool {
|
||||
return paging.leafIsWriteCombining(root, virtual);
|
||||
}
|
||||
|
||||
/// Map coherent DMA RAM into address space `root`: strong-uncacheable, RW+NX, but
|
||||
|
|
|
|||
|
|
@ -320,8 +320,12 @@ pub fn mapUserInto(pml4: u64, virtual: u64, physical: u64, writable_page: bool,
|
|||
/// RAM allocator (`freeSubtree`). RW + NX; the caller places `virtual` in a
|
||||
/// user-exclusive range (PML4[225]). Both `virtual` and `physical` are page-aligned by
|
||||
/// the caller; a sub-page `physical` offset is the caller's to re-apply.
|
||||
pub fn mapUserDeviceInto(pml4: u64, virtual: u64, physical: u64, len: u64) void {
|
||||
const flags: u64 = present | user | writable | no_execute | pcd | pwt | device_grant;
|
||||
pub fn mapUserDeviceInto(pml4: u64, virtual: u64, physical: u64, len: u64, write_combining: bool) void {
|
||||
// Registers are strong-uncacheable (PCD|PWT). A framebuffer instead wants
|
||||
// write-combining — the PAT bit (bit 7 in a 4 KiB PTE) with PCD=PWT=0 selects PAT
|
||||
// entry 4, which `setupPat` programs to WC — so pixel writes batch into bursts.
|
||||
const cache: u64 = if (write_combining) pte_pat else (pcd | pwt);
|
||||
const flags: u64 = present | user | writable | no_execute | device_grant | cache;
|
||||
const first = physical & ~@as(u64, page_size - 1);
|
||||
const last = (physical + (if (len == 0) 1 else len) - 1) & ~@as(u64, page_size - 1);
|
||||
var off: u64 = 0;
|
||||
|
|
@ -362,6 +366,33 @@ pub fn mapUserDmaInto(pml4: u64, virtual: u64, physical: u64, len: u64) void {
|
|||
}
|
||||
}
|
||||
|
||||
/// The raw leaf entry mapping `virtual` in the address space rooted at `pml4`, or null
|
||||
/// if any level of the walk is absent. **Read-only** — never allocates or descends into
|
||||
/// a missing table (unlike the `map*` paths' `descendUser`). Stops at the first huge
|
||||
/// leaf. For tests and introspection that need a page's actual flag bits.
|
||||
pub fn leafEntryOf(pml4: u64, virtual: u64) ?u64 {
|
||||
const l4 = tableAt(pml4)[(virtual >> 39) & 0x1FF];
|
||||
if (l4 & present == 0) return null;
|
||||
const l3 = tableAt(l4 & address_mask)[(virtual >> 30) & 0x1FF];
|
||||
if (l3 & present == 0) return null;
|
||||
if (l3 & page_size_bit != 0) return l3; // 1 GiB leaf
|
||||
const l2 = tableAt(l3 & address_mask)[(virtual >> 21) & 0x1FF];
|
||||
if (l2 & present == 0) return null;
|
||||
if (l2 & page_size_bit != 0) return l2; // 2 MiB leaf
|
||||
const l1 = tableAt(l2 & address_mask)[(virtual >> 12) & 0x1FF];
|
||||
if (l1 & present == 0) return null;
|
||||
return l1;
|
||||
}
|
||||
|
||||
/// Is the 4 KiB leaf mapping `virtual` write-combining — the PAT bit set with PCD and
|
||||
/// PWT clear, which `setupPat` makes PAT entry 4 (WC)? Null if unmapped. The device
|
||||
/// mapping path (`mapUserDeviceInto`) always uses 4 KiB leaves, so bit 7 (`pte_pat`)
|
||||
/// is the PAT selector in play.
|
||||
pub fn leafIsWriteCombining(pml4: u64, virtual: u64) ?bool {
|
||||
const e = leafEntryOf(pml4, virtual) orelse return null;
|
||||
return (e & pte_pat != 0) and (e & pcd == 0) and (e & pwt == 0);
|
||||
}
|
||||
|
||||
/// Create a new address space: a fresh PML4 with an empty user half and the
|
||||
/// kernel's higher half shared in (copying PML4[256..512), whose entries point
|
||||
/// at the kernel's PDPTs — pre-created at init and never restaled, so growth in
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ const boot_handoff = @import("boot-handoff");
|
|||
var con: Console = undefined;
|
||||
var con_present: bool = false;
|
||||
|
||||
/// Set while a user-space display service owns the framebuffer: `write` falls silent so
|
||||
/// the kernel doesn't paint over the compositor. Driven by the display device's
|
||||
/// claim/release (system/kernel/process.zig). The terminal panic/exception paths clear
|
||||
/// it first (`setSuppressed(false)`) — a dying machine's message wins over any display.
|
||||
var suppressed: bool = false;
|
||||
|
||||
/// Set up the console over `fb`, or mark it absent if there's no usable
|
||||
/// framebuffer. Clears the screen when present.
|
||||
pub fn init(fb: boot_handoff.Framebuffer) void {
|
||||
|
|
@ -41,13 +47,20 @@ pub fn present() bool {
|
|||
return con_present;
|
||||
}
|
||||
|
||||
/// Output sink: draw `bytes` on screen. A no-op when no framebuffer is present,
|
||||
/// so it's always safe to call.
|
||||
/// Output sink: draw `bytes` on screen. A no-op when no framebuffer is present, or
|
||||
/// while a display service owns the screen (`suppressed`), so it's always safe to call.
|
||||
pub fn write(bytes: []const u8) void {
|
||||
if (!con_present) return;
|
||||
if (!con_present or suppressed) return;
|
||||
for (bytes) |c| con.putChar(c);
|
||||
}
|
||||
|
||||
/// Quiesce (or resume) the bootstrap console. Set true when a display service claims the
|
||||
/// framebuffer; set false when that claim is released, or by the panic path to force a
|
||||
/// last message onto a screen a (now-irrelevant) service was holding.
|
||||
pub fn setSuppressed(value: bool) void {
|
||||
suppressed = value;
|
||||
}
|
||||
|
||||
/// The console font, embedded at compile time. cp850-8x16, PSF2 format:
|
||||
/// a 32-byte header, then 256 glyphs of 16 bytes each (one byte per 8-pixel
|
||||
/// row). We index glyphs straight by byte value, so ASCII maps 1:1.
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ var devices: [maximum_devices]device_abi.DeviceDescriptor = undefined;
|
|||
var claimed: [maximum_devices]?u32 = .{null} ** maximum_devices; // owner task id, or null
|
||||
var count: usize = 0;
|
||||
|
||||
/// The id of the seeded framebuffer node (`seedDisplay`), or null when the machine
|
||||
/// handed over no framebuffer. Lets the process layer recognise the display claim
|
||||
/// (to quiesce the bootstrap console) without threading the id through every caller.
|
||||
var display_device: ?u64 = null;
|
||||
|
||||
/// Devices discovery found but the table had no room for. Non-zero means the machine
|
||||
/// is bigger than `maximum_devices` and some hardware is simply invisible to drivers —
|
||||
/// which would otherwise be an entirely silent failure. Logged at boot.
|
||||
|
|
@ -45,10 +50,55 @@ pub var dropped: usize = 0;
|
|||
pub fn init(device_tree: *const platform.DeviceTree) void {
|
||||
count = 0;
|
||||
dropped = 0;
|
||||
display_device = null;
|
||||
for (&claimed) |*c| c.* = null;
|
||||
walk(device_tree.root, device_abi.no_parent);
|
||||
}
|
||||
|
||||
/// Publish the loader's framebuffer as a `display` device — a root-level node with one
|
||||
/// write-combining `memory` resource over the linear framebuffer and its geometry in
|
||||
/// `.display`. The framebuffer is *not* firmware-discovered (it rides the
|
||||
/// [[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
|
||||
/// 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 {
|
||||
if (base == 0 or width == 0 or height == 0) return null; // headless
|
||||
if (count >= maximum_devices) {
|
||||
dropped += 1;
|
||||
return null;
|
||||
}
|
||||
var d = std.mem.zeroes(device_abi.DeviceDescriptor);
|
||||
d.id = count;
|
||||
d.parent = device_abi.no_parent;
|
||||
d.class = @intFromEnum(device_abi.DeviceClass.display);
|
||||
d.pci_class = device_abi.no_pci_class;
|
||||
d.resource_count = 1;
|
||||
d.resources[0] = .{
|
||||
.kind = @intFromEnum(device_abi.ResourceKind.memory),
|
||||
.start = base,
|
||||
.len = @as(u64, height) * pitch,
|
||||
.flags = device_abi.resource_flag_write_combining,
|
||||
};
|
||||
d.display = .{ .width = width, .height = height, .pitch = pitch, .format = format };
|
||||
devices[count] = d;
|
||||
display_device = d.id;
|
||||
count += 1;
|
||||
return d.id;
|
||||
}
|
||||
|
||||
/// The id of the seeded framebuffer device, or null when none was seeded.
|
||||
pub fn displayDevice() ?u64 {
|
||||
return display_device;
|
||||
}
|
||||
|
||||
/// Whether the framebuffer device is currently claimed by some process. The bootstrap
|
||||
/// console uses this (via the process layer) to fall silent while a display service
|
||||
/// owns the screen, and to resume if that service dies and its claim is released.
|
||||
pub fn displayClaimed() bool {
|
||||
const id = display_device orelse return false;
|
||||
return ownerOf(id) != null;
|
||||
}
|
||||
|
||||
/// Record `node` (unless it's the synthetic root) and recurse, threading the id we
|
||||
/// assigned it down to its children as their parent.
|
||||
fn walk(node: *platform.Device, parent_id: u64) void {
|
||||
|
|
|
|||
|
|
@ -195,6 +195,13 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||
log.print("/system/kernel: WARNING {d} device(s) dropped — table full\n", .{devices_broker.dropped});
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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| {
|
||||
log.print("/system/kernel: framebuffer device {d} seeded ({d}x{d}, pitch {d}, write-combining)\n", .{ display_id, fb.width, fb.height, fb.pitch });
|
||||
}
|
||||
|
||||
// Install the device-IRQ trampolines, so a driver's irq_bind has vectors to
|
||||
// land on. Every line stays masked until something binds it (ioapic.init).
|
||||
irq.init();
|
||||
|
|
@ -479,6 +486,9 @@ fn onException(state: *const architecture.CpuState) noreturn {
|
|||
}
|
||||
|
||||
log.checkpoint(cp_exception);
|
||||
// The machine is going down: force the console back on even if a display service
|
||||
// was holding the framebuffer, so the exception actually reaches the screen.
|
||||
console.setSuppressed(false);
|
||||
const core = scheduler.currentCpuIndex();
|
||||
// A fault is user-facing enough to paint on screen too (via statusPrint), on
|
||||
// top of the diagnostic log.
|
||||
|
|
@ -501,6 +511,7 @@ pub const panic = std.debug.FullPanic(struct {
|
|||
_ = first_trace_address;
|
||||
log.checkpoint(cp_panic);
|
||||
log.recordPanic(message);
|
||||
console.setSuppressed(false); // a panic outranks any display service holding the screen
|
||||
status("\nKERNEL PANIC: ");
|
||||
status(message);
|
||||
status("\n");
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const parameters = @import("parameters");
|
|||
const architecture = @import("architecture");
|
||||
const pmm = @import("pmm.zig");
|
||||
const scheduler = @import("scheduler.zig");
|
||||
const console = @import("console.zig");
|
||||
const sync = @import("sync.zig");
|
||||
const ipc = @import("ipc-synchronous.zig");
|
||||
const devices_broker = @import("devices-broker.zig");
|
||||
|
|
@ -301,12 +302,19 @@ fn systemDeviceEnumerate(state: *architecture.CpuState) void {
|
|||
|
||||
/// device_claim(id) -> 0/-1: take exclusive ownership of a device for this process.
|
||||
fn systemDeviceClaim(state: *architecture.CpuState) void {
|
||||
const device_id = architecture.systemCallArg(state, 0);
|
||||
const claim_flags = sync.enter();
|
||||
defer sync.leave(claim_flags);
|
||||
if (devices_broker.claim(architecture.systemCallArg(state, 0), scheduler.current().id))
|
||||
architecture.setSystemCallResult(state, 0)
|
||||
else
|
||||
fail(state);
|
||||
if (devices_broker.claim(device_id, scheduler.current().id)) {
|
||||
// A display service just took the framebuffer — quiesce the bootstrap console
|
||||
// so the kernel and the service don't scribble over each other's pixels. The
|
||||
// claim releases (and the console resumes) automatically if the service dies;
|
||||
// see releaseTaskResourcesLocked.
|
||||
if (devices_broker.displayDevice()) |display_id| {
|
||||
if (device_id == display_id) console.setSuppressed(true);
|
||||
}
|
||||
architecture.setSystemCallResult(state, 0);
|
||||
} else fail(state);
|
||||
}
|
||||
|
||||
/// mmio_map(device_id, resource_index) -> vaddr: map a claimed device's MMIO window into
|
||||
|
|
@ -341,7 +349,10 @@ fn systemMmioMap(state: *architecture.CpuState) void {
|
|||
const base_v = t.device_map_next;
|
||||
if (base_v + pages * page_size > device_arena_end) return fail(state);
|
||||
|
||||
architecture.mapUserDeviceInto(t.aspace, base_v, r.start, r.len);
|
||||
// A framebuffer resource asks (via its flag) to be mapped write-combining rather
|
||||
// than the strong-uncacheable default that register MMIO needs.
|
||||
const write_combining = (r.flags & device_abi.resource_flag_write_combining) != 0;
|
||||
architecture.mapUserDeviceInto(t.aspace, base_v, r.start, r.len, write_combining);
|
||||
t.device_map_next = base_v + pages * page_size;
|
||||
architecture.setSystemCallResult(state, base_v + (r.start & (page_size - 1))); // register base
|
||||
}
|
||||
|
|
@ -601,6 +612,9 @@ fn releaseTaskResourcesLocked(t: *scheduler.Task) void {
|
|||
recordExitLocked(t);
|
||||
irq.releaseOwner(t.id);
|
||||
devices_broker.releaseAllOwnedBy(t.id);
|
||||
// If that dropped the framebuffer claim (this task was the display service), let the
|
||||
// bootstrap console draw again — the screen is nobody's now, so panics/status land.
|
||||
if (!devices_broker.displayClaimed()) console.setSuppressed(false);
|
||||
// The dying task's signal endpoint and one-shot timers go with it.
|
||||
if (t.signal_endpoint) |raw| {
|
||||
ipc.dropRef(@ptrCast(@alignCast(raw)));
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||
iommuTest();
|
||||
} else if (eql(case, "ioport")) {
|
||||
ioPortTest();
|
||||
} else if (eql(case, "display")) {
|
||||
displayTest(boot_information);
|
||||
} else if (eql(case, "clock")) {
|
||||
clockTest();
|
||||
} else if (eql(case, "smp")) {
|
||||
|
|
@ -2627,8 +2629,8 @@ fn ioPassTest() void {
|
|||
result();
|
||||
return;
|
||||
};
|
||||
// Map it the way mmio_map does (device grant), then tear the space down.
|
||||
architecture.mapUserDeviceInto(aspace, process.device_arena_base, frame, abi.page_size);
|
||||
// Map it the way mmio_map does (device grant, strong-uncacheable), then tear the space down.
|
||||
architecture.mapUserDeviceInto(aspace, process.device_arena_base, frame, abi.page_size, false);
|
||||
architecture.destroyAddressSpace(aspace);
|
||||
|
||||
// The page tables were reclaimed; the device-granted frame must not have been.
|
||||
|
|
@ -2638,6 +2640,75 @@ fn ioPassTest() void {
|
|||
result();
|
||||
}
|
||||
|
||||
/// D1 — the framebuffer handoff primitive. The kernel seeds the loader's framebuffer as
|
||||
/// a claimable `display` device with a write-combining `memory` resource; a display
|
||||
/// service reaches it over the ordinary claim + mmio_map path. Prove the whole chain:
|
||||
/// the node is present and correctly shaped, it maps, and — the point of D1 — the
|
||||
/// mapping is genuinely write-combining, not the strong-uncacheable default that would
|
||||
/// make a framebuffer blit glacial.
|
||||
fn displayTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: display\n", .{});
|
||||
const fb = boot_information.framebuffer;
|
||||
if (!fb.present()) {
|
||||
// Headless: nothing to seed. Not a failure of the mechanism, so pass cleanly.
|
||||
log("display: no framebuffer (headless); skipping\n", .{});
|
||||
result();
|
||||
return;
|
||||
}
|
||||
|
||||
// The kernel seeded a display device in kmain, right after devices_broker.init.
|
||||
const display_id = devices_broker.displayDevice() orelse {
|
||||
check("a framebuffer display device was seeded", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
var buffer: [64]device_abi.DeviceDescriptor = undefined;
|
||||
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
|
||||
check("the seeded display id is enumerable", display_id < n);
|
||||
if (display_id >= n) {
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const d = buffer[@intCast(display_id)];
|
||||
|
||||
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 has exactly one resource", d.resource_count == 1);
|
||||
const r = d.resources[0];
|
||||
check("that resource is a memory window", r.kind == @intFromEnum(device_abi.ResourceKind.memory));
|
||||
check("it spans the whole framebuffer", r.start == fb.base and r.len == @as(u64, fb.height) * fb.pitch);
|
||||
check("it is flagged write-combining", (r.flags & device_abi.resource_flag_write_combining) != 0);
|
||||
|
||||
// Walk the real claim + map path a display service would, into a throwaway address
|
||||
// space, and confirm the leaf's cache type. We never run this space (no CR3 load) —
|
||||
// we only read back the page-table entries — so aliasing the same physical page at
|
||||
// two cache types below is inert.
|
||||
const aspace = architecture.createAddressSpace() orelse {
|
||||
check("created a fresh address space", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
defer architecture.destroyAddressSpace(aspace);
|
||||
|
||||
const page_base = fb.base & ~@as(u64, abi.page_size - 1);
|
||||
architecture.mapUserDeviceInto(aspace, process.device_arena_base, page_base, abi.page_size, true);
|
||||
check(
|
||||
"the framebuffer maps write-combining (PAT entry 4: PAT bit set, PCD/PWT clear)",
|
||||
architecture.userLeafIsWriteCombining(aspace, process.device_arena_base) == true,
|
||||
);
|
||||
|
||||
// Regression guard: the strong-uncacheable default is still that, so WC is a real
|
||||
// choice the flag makes, not the only behaviour.
|
||||
architecture.mapUserDeviceInto(aspace, process.device_arena_base + abi.page_size, page_base, abi.page_size, false);
|
||||
check(
|
||||
"a register window still maps strong-uncacheable",
|
||||
architecture.userLeafIsWriteCombining(aspace, process.device_arena_base + abi.page_size) == false,
|
||||
);
|
||||
|
||||
log("display: mapped {d}x{d} pitch {d} (write-combining)\n", .{ fb.width, fb.height, fb.pitch });
|
||||
result();
|
||||
}
|
||||
|
||||
fn faultInvalidOpcode() void {
|
||||
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
|
||||
asm volatile ("ud2");
|
||||
|
|
|
|||
|
|
@ -165,6 +165,12 @@ CASES = [
|
|||
{"name": "ioport",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# Display handoff (D1): the kernel seeds the loader's framebuffer as a claimable
|
||||
# `display` device with a write-combining memory resource; the claim + mmio_map path
|
||||
# maps it, and the leaf is genuinely write-combining (PAT entry 4), not the UC default.
|
||||
{"name": "display",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# Monotonic clock (clock() syscall source): calibrated, advancing, never backwards.
|
||||
{"name": "clock",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
|
|
|
|||
Loading…
Reference in New Issue