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:
+6
-1
@@ -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
|
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
|
asynchronous `ipc_send` primitive built to fix it, and the per-device subscribe/publish
|
||||||
service layered on top.
|
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.
|
how `while (true) hlt` parks the CPU safely once there's nothing left to do.
|
||||||
|
|
||||||
Start with the north star:
|
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.
|
||||||
+250
@@ -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
|
/// resources; the (class, subclass, protocol) triple that says what it is
|
||||||
/// travels in the bus report's identity, not here.
|
/// travels in the bus report's identity, not here.
|
||||||
usb_device,
|
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,
|
unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,10 +66,39 @@ pub const ResourceDescriptor = extern struct {
|
|||||||
kind: u64, // a ResourceKind value
|
kind: u64, // a ResourceKind value
|
||||||
start: u64,
|
start: u64,
|
||||||
len: 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;
|
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.
|
/// `DeviceDescriptor.parent` for a device with no parent — a root of the device tree.
|
||||||
pub const no_parent: u64 = ~@as(u64, 0);
|
pub const no_parent: u64 = ~@as(u64, 0);
|
||||||
|
|
||||||
@@ -92,4 +128,9 @@ pub const DeviceDescriptor = extern struct {
|
|||||||
resource_count: u64,
|
resource_count: u64,
|
||||||
hid: [8]u8,
|
hid: [8]u8,
|
||||||
resources: [maximum_device_resources]ResourceDescriptor,
|
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);
|
paging.mapUserInto(root, virtual, physical, writable, executable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map a device MMIO window into address space `root`: strong-uncacheable, RW+NX,
|
/// Map a device MMIO window into address space `root`: RW+NX, and marked so teardown
|
||||||
/// and marked so teardown won't free the MMIO frames as RAM. For IO passthrough.
|
/// won't free the MMIO frames as RAM. `write_combining` picks the cache type —
|
||||||
pub fn mapUserDeviceInto(root: u64, virtual: u64, physical: u64, len: u64) void {
|
/// false = strong-uncacheable (registers), true = write-combining (a framebuffer).
|
||||||
paging.mapUserDeviceInto(root, virtual, physical, len);
|
/// 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
|
/// 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
|
/// RAM allocator (`freeSubtree`). RW + NX; the caller places `virtual` in a
|
||||||
/// user-exclusive range (PML4[225]). Both `virtual` and `physical` are page-aligned by
|
/// 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.
|
/// 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 {
|
pub fn mapUserDeviceInto(pml4: u64, virtual: u64, physical: u64, len: u64, write_combining: bool) void {
|
||||||
const flags: u64 = present | user | writable | no_execute | pcd | pwt | device_grant;
|
// 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 first = physical & ~@as(u64, page_size - 1);
|
||||||
const last = (physical + (if (len == 0) 1 else len) - 1) & ~@as(u64, page_size - 1);
|
const last = (physical + (if (len == 0) 1 else len) - 1) & ~@as(u64, page_size - 1);
|
||||||
var off: u64 = 0;
|
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
|
/// 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
|
/// 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
|
/// 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: Console = undefined;
|
||||||
var con_present: bool = false;
|
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
|
/// Set up the console over `fb`, or mark it absent if there's no usable
|
||||||
/// framebuffer. Clears the screen when present.
|
/// framebuffer. Clears the screen when present.
|
||||||
pub fn init(fb: boot_handoff.Framebuffer) void {
|
pub fn init(fb: boot_handoff.Framebuffer) void {
|
||||||
@@ -41,13 +47,20 @@ pub fn present() bool {
|
|||||||
return con_present;
|
return con_present;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Output sink: draw `bytes` on screen. A no-op when no framebuffer is present,
|
/// Output sink: draw `bytes` on screen. A no-op when no framebuffer is present, or
|
||||||
/// so it's always safe to call.
|
/// while a display service owns the screen (`suppressed`), so it's always safe to call.
|
||||||
pub fn write(bytes: []const u8) void {
|
pub fn write(bytes: []const u8) void {
|
||||||
if (!con_present) return;
|
if (!con_present or suppressed) return;
|
||||||
for (bytes) |c| con.putChar(c);
|
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:
|
/// 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
|
/// 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.
|
/// 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 claimed: [maximum_devices]?u32 = .{null} ** maximum_devices; // owner task id, or null
|
||||||
var count: usize = 0;
|
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
|
/// 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 —
|
/// is bigger than `maximum_devices` and some hardware is simply invisible to drivers —
|
||||||
/// which would otherwise be an entirely silent failure. Logged at boot.
|
/// 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 {
|
pub fn init(device_tree: *const platform.DeviceTree) void {
|
||||||
count = 0;
|
count = 0;
|
||||||
dropped = 0;
|
dropped = 0;
|
||||||
|
display_device = null;
|
||||||
for (&claimed) |*c| c.* = null;
|
for (&claimed) |*c| c.* = null;
|
||||||
walk(device_tree.root, device_abi.no_parent);
|
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
|
/// Record `node` (unless it's the synthetic root) and recurse, threading the id we
|
||||||
/// assigned it down to its children as their parent.
|
/// assigned it down to its children as their parent.
|
||||||
fn walk(node: *platform.Device, parent_id: u64) void {
|
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});
|
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
|
// 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).
|
// land on. Every line stays masked until something binds it (ioapic.init).
|
||||||
irq.init();
|
irq.init();
|
||||||
@@ -479,6 +486,9 @@ fn onException(state: *const architecture.CpuState) noreturn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.checkpoint(cp_exception);
|
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();
|
const core = scheduler.currentCpuIndex();
|
||||||
// A fault is user-facing enough to paint on screen too (via statusPrint), on
|
// A fault is user-facing enough to paint on screen too (via statusPrint), on
|
||||||
// top of the diagnostic log.
|
// top of the diagnostic log.
|
||||||
@@ -501,6 +511,7 @@ pub const panic = std.debug.FullPanic(struct {
|
|||||||
_ = first_trace_address;
|
_ = first_trace_address;
|
||||||
log.checkpoint(cp_panic);
|
log.checkpoint(cp_panic);
|
||||||
log.recordPanic(message);
|
log.recordPanic(message);
|
||||||
|
console.setSuppressed(false); // a panic outranks any display service holding the screen
|
||||||
status("\nKERNEL PANIC: ");
|
status("\nKERNEL PANIC: ");
|
||||||
status(message);
|
status(message);
|
||||||
status("\n");
|
status("\n");
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const parameters = @import("parameters");
|
|||||||
const architecture = @import("architecture");
|
const architecture = @import("architecture");
|
||||||
const pmm = @import("pmm.zig");
|
const pmm = @import("pmm.zig");
|
||||||
const scheduler = @import("scheduler.zig");
|
const scheduler = @import("scheduler.zig");
|
||||||
|
const console = @import("console.zig");
|
||||||
const sync = @import("sync.zig");
|
const sync = @import("sync.zig");
|
||||||
const ipc = @import("ipc-synchronous.zig");
|
const ipc = @import("ipc-synchronous.zig");
|
||||||
const devices_broker = @import("devices-broker.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.
|
/// device_claim(id) -> 0/-1: take exclusive ownership of a device for this process.
|
||||||
fn systemDeviceClaim(state: *architecture.CpuState) void {
|
fn systemDeviceClaim(state: *architecture.CpuState) void {
|
||||||
|
const device_id = architecture.systemCallArg(state, 0);
|
||||||
const claim_flags = sync.enter();
|
const claim_flags = sync.enter();
|
||||||
defer sync.leave(claim_flags);
|
defer sync.leave(claim_flags);
|
||||||
if (devices_broker.claim(architecture.systemCallArg(state, 0), scheduler.current().id))
|
if (devices_broker.claim(device_id, scheduler.current().id)) {
|
||||||
architecture.setSystemCallResult(state, 0)
|
// A display service just took the framebuffer — quiesce the bootstrap console
|
||||||
else
|
// so the kernel and the service don't scribble over each other's pixels. The
|
||||||
fail(state);
|
// 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
|
/// 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;
|
const base_v = t.device_map_next;
|
||||||
if (base_v + pages * page_size > device_arena_end) return fail(state);
|
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;
|
t.device_map_next = base_v + pages * page_size;
|
||||||
architecture.setSystemCallResult(state, base_v + (r.start & (page_size - 1))); // register base
|
architecture.setSystemCallResult(state, base_v + (r.start & (page_size - 1))); // register base
|
||||||
}
|
}
|
||||||
@@ -601,6 +612,9 @@ fn releaseTaskResourcesLocked(t: *scheduler.Task) void {
|
|||||||
recordExitLocked(t);
|
recordExitLocked(t);
|
||||||
irq.releaseOwner(t.id);
|
irq.releaseOwner(t.id);
|
||||||
devices_broker.releaseAllOwnedBy(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.
|
// The dying task's signal endpoint and one-shot timers go with it.
|
||||||
if (t.signal_endpoint) |raw| {
|
if (t.signal_endpoint) |raw| {
|
||||||
ipc.dropRef(@ptrCast(@alignCast(raw)));
|
ipc.dropRef(@ptrCast(@alignCast(raw)));
|
||||||
|
|||||||
+73
-2
@@ -95,6 +95,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||||||
iommuTest();
|
iommuTest();
|
||||||
} else if (eql(case, "ioport")) {
|
} else if (eql(case, "ioport")) {
|
||||||
ioPortTest();
|
ioPortTest();
|
||||||
|
} else if (eql(case, "display")) {
|
||||||
|
displayTest(boot_information);
|
||||||
} else if (eql(case, "clock")) {
|
} else if (eql(case, "clock")) {
|
||||||
clockTest();
|
clockTest();
|
||||||
} else if (eql(case, "smp")) {
|
} else if (eql(case, "smp")) {
|
||||||
@@ -2627,8 +2629,8 @@ fn ioPassTest() void {
|
|||||||
result();
|
result();
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// Map it the way mmio_map does (device grant), then tear the space down.
|
// 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);
|
architecture.mapUserDeviceInto(aspace, process.device_arena_base, frame, abi.page_size, false);
|
||||||
architecture.destroyAddressSpace(aspace);
|
architecture.destroyAddressSpace(aspace);
|
||||||
|
|
||||||
// The page tables were reclaimed; the device-granted frame must not have been.
|
// The page tables were reclaimed; the device-granted frame must not have been.
|
||||||
@@ -2638,6 +2640,75 @@ fn ioPassTest() void {
|
|||||||
result();
|
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 {
|
fn faultInvalidOpcode() void {
|
||||||
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
|
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
|
||||||
asm volatile ("ud2");
|
asm volatile ("ud2");
|
||||||
|
|||||||
@@ -165,6 +165,12 @@ CASES = [
|
|||||||
{"name": "ioport",
|
{"name": "ioport",
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"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.
|
# Monotonic clock (clock() syscall source): calibrated, advancing, never backwards.
|
||||||
{"name": "clock",
|
{"name": "clock",
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
|
|||||||
Reference in New Issue
Block a user