Compare commits
113
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fc2d5b083 | ||
|
|
105203b447 | ||
|
|
f9cf0007c5 | ||
|
|
69b018cc32 | ||
|
|
cd812cc00e | ||
|
|
f157a93c9c | ||
|
|
88644e57d6 | ||
|
|
10c11d1806 | ||
|
|
c4595700ba | ||
|
|
28b4dabbaa | ||
|
|
4cb4f2a80f | ||
|
|
67702fa250 | ||
|
|
8a38540312 | ||
|
|
54635eecf5 | ||
|
|
184d90c2c6 | ||
|
|
a32eed877d | ||
|
|
347a041d85 | ||
|
|
53e42837e0 | ||
|
|
f52c591f5e | ||
|
|
77d2e22ed1 | ||
|
|
a64a01a6a9 | ||
|
|
35e8921de8 | ||
|
|
3fb9d5936a | ||
|
|
452080e997 | ||
|
|
5b63a841ba | ||
|
|
4b9507bd59 | ||
|
|
7dec1b0767 | ||
|
|
45b8fd8614 | ||
|
|
dd22bfbc48 | ||
|
|
6e60daed6a | ||
|
|
446f655c69 | ||
|
|
a785efa4a3 | ||
|
|
767a2a9a7c | ||
|
|
dd044fb115 | ||
|
|
3ec14509a0 | ||
|
|
1f2c60b3ec | ||
|
|
d71a5f25d3 | ||
|
|
2a0f17ae86 | ||
|
|
9ef61a0844 | ||
|
|
688b9101e8 | ||
|
|
1d7ba814dc | ||
|
|
8aba86b4ce | ||
|
|
a0c83f4b3f | ||
|
|
1ea48ed5d6 | ||
|
|
dfc7d6a609 | ||
|
|
77a3ccd33d | ||
|
|
07da27dc39 | ||
|
|
d89657d0a4 | ||
|
|
849b4b62d4 | ||
|
|
8589bf713b | ||
|
|
738f6aa697 | ||
|
|
01e56e3f36 | ||
|
|
d5d15cefcb | ||
|
|
fd96a35eb9 | ||
|
|
e3fe3f3f45 | ||
|
|
60da667b42 | ||
|
|
36145e623b | ||
|
|
565415327d | ||
|
|
bf6bdb389d | ||
|
|
0628944b15 | ||
|
|
e6d0bb7ef0 | ||
|
|
5ca804d827 | ||
|
|
a299363b59 | ||
|
|
d8dd62c639 | ||
|
|
d106b6e8dc | ||
|
|
af2c766f42 | ||
|
|
d26262bf56 | ||
|
|
10b89c06ff | ||
|
|
a2a05d0b3d | ||
|
|
75d62660b0 | ||
|
|
a53c2b0193 | ||
|
|
bf481c080c | ||
|
|
3a78dcab3f | ||
|
|
470f93a83d | ||
|
|
7798706b41 | ||
|
|
ad40de03c2 | ||
|
|
d8778b4b70 | ||
|
|
79d859a111 | ||
|
|
37fb09f75e | ||
|
|
34ebeb968d | ||
|
|
3cc1d38dd0 | ||
|
|
36e804b848 | ||
|
|
be83a42d42 | ||
|
|
650a1b1595 | ||
|
|
d8c55c6f2f | ||
|
|
2ebfb0c3b0 | ||
|
|
888eaa74e1 | ||
|
|
ed76cbbc79 | ||
|
|
140229b88d | ||
|
|
cb2379fd06 | ||
|
|
1665b239b0 | ||
|
|
70ed0337f8 | ||
|
|
116b8f6c41 | ||
|
|
77901bbba6 | ||
|
|
78582d24d2 | ||
|
|
1cdffe21b1 | ||
|
|
4df90bc212 | ||
|
|
713e77354b | ||
|
|
abb7b1b634 | ||
|
|
f5f0e15769 | ||
|
|
8652b4a724 | ||
|
|
e8233127c7 | ||
|
|
88e92254e9 | ||
|
|
5725d35e5b | ||
|
|
8c95525793 | ||
|
|
5bba5d3363 | ||
|
|
80b72db676 | ||
|
|
aa0c97353a | ||
|
|
dd93204b44 | ||
|
|
c7b17aaa0e | ||
|
|
d7a154a596 | ||
|
|
1bf91115dd | ||
|
|
65244e3103 |
@@ -0,0 +1,16 @@
|
|||||||
|
# EditorConfig: https://editorconfig.org/
|
||||||
|
# Follows the Zig style guide: https://ziglang.org/documentation/0.16.0/#Style-Guide
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.zig]
|
||||||
|
# "Line length: aim for 100; use common sense."
|
||||||
|
max_line_length = 100
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
*.zig text eol=lf
|
||||||
@@ -2,12 +2,31 @@
|
|||||||
Codename: Shodan
|
Codename: Shodan
|
||||||
Version: 1
|
Version: 1
|
||||||
|
|
||||||
A small operating system, written from scratch in Zig — a bootloader (`boot/`)
|
A small resilient operating system, written from scratch in Zig.
|
||||||
and a microkernel (`system/kernel/`), sharing a neutral handoff contract (`system/boot-handoff.zig`).
|
|
||||||
It boots x86-64 via UEFI, and so far has a framebuffer console, a physical frame
|
## Zen of DanOS:
|
||||||
allocator, its own paging with W^X permissions, interrupt/exception handling, a
|
|
||||||
LAPIC timer, a kernel heap, a fixed-priority preemptive scheduler, and in-kernel IPC
|
- Resilient Micro-Kernel Architecture.
|
||||||
channels. See [`docs/`](docs/README.md) for how each piece works.
|
- Every process run in an isolated user space not kernel space.
|
||||||
|
- Processes cannot take down the entire OS with it when they die or is killed
|
||||||
|
- Stable public runtime library, private OS ABI.
|
||||||
|
- Keeps a stable runtime for user space processes between OS versions (great for backwards compatibility)
|
||||||
|
- Allows the underlying OS to be changed without effecting applications
|
||||||
|
- Provides a boundary to enable compatibility between OS's e.g. POSIX, MUSL etc
|
||||||
|
- Drivers are just isolated processes in user space.
|
||||||
|
- Thin binaries that can be restarted like applications.
|
||||||
|
- Useful during driver development.
|
||||||
|
- Drivers can claim MMIO / ports
|
||||||
|
- Driver resources (e.g. IRQ/Port/MMIO) claims are automatically cleaned up if the driver dies or is killed
|
||||||
|
- Drivers can also hook into the process lifecyle to clean up or reset hardware
|
||||||
|
- No legacy to deal with
|
||||||
|
- Zig code uses a clean coding style (Zen of Zig)
|
||||||
|
- Favor reading code over writing code.
|
||||||
|
- No magic numbers.
|
||||||
|
- No shortend names unless its for ABI compatibility or acronyms
|
||||||
|
- Inter-Process Communication (IPC)
|
||||||
|
- Publish and subscribe to Asynchronous Messages
|
||||||
|
- Talk to services and processes synchronously
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -60,9 +79,13 @@ straight into CI.
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Design notes explaining the *why* behind the code live in
|
Design notes explaining *why* behind the code live in
|
||||||
[`docs/`](docs/README.md) — start with [`docs/README.md`](docs/README.md).
|
[`docs/`](docs/README.md) — start with [`docs/README.md`](docs/README.md).
|
||||||
|
|
||||||
|
For the hardware needed to run DanOS — minimum specs plus a plain-language guide
|
||||||
|
matching Intel/AMD CPU generations by name — see
|
||||||
|
[`docs/system-requirements.md`](docs/system-requirements.md).
|
||||||
|
|
||||||
## Logo
|
## Logo
|
||||||
|
|
||||||
San Serif Text "Dan OS" with a black karate belt around it.
|
San Serif Text "Dan OS" with a black karate belt around it.
|
||||||
|
|||||||
+15
-6
@@ -2,6 +2,7 @@ const std = @import("std");
|
|||||||
const uefi = std.os.uefi;
|
const uefi = std.os.uefi;
|
||||||
const elf = std.elf;
|
const elf = std.elf;
|
||||||
const boot_handoff = @import("boot-handoff");
|
const boot_handoff = @import("boot-handoff");
|
||||||
|
const build_options = @import("build_options");
|
||||||
const BootInformation = boot_handoff.BootInformation;
|
const BootInformation = boot_handoff.BootInformation;
|
||||||
const GraphicsOutput = uefi.protocol.GraphicsOutput;
|
const GraphicsOutput = uefi.protocol.GraphicsOutput;
|
||||||
const EdidActive = uefi.protocol.edid.Active;
|
const EdidActive = uefi.protocol.edid.Active;
|
||||||
@@ -29,7 +30,7 @@ pub fn main() uefi.Status {
|
|||||||
// report the reason (boot services are still up) and park the machine so the
|
// report the reason (boot services are still up) and park the machine so the
|
||||||
// message stays on screen.
|
// message stays on screen.
|
||||||
boot() catch |err| {
|
boot() catch |err| {
|
||||||
log("\r\ndanos: boot failed: ");
|
log("\r\nEFI: boot failed: ");
|
||||||
logBytes(@errorName(err));
|
logBytes(@errorName(err));
|
||||||
log("\r\n");
|
log("\r\n");
|
||||||
while (true) asm volatile ("hlt");
|
while (true) asm volatile ("hlt");
|
||||||
@@ -65,14 +66,14 @@ fn boot() !noreturn {
|
|||||||
|
|
||||||
// Best effort: a volume without /system/services/init still boots (kernel-only).
|
// Best effort: a volume without /system/services/init still boots (kernel-only).
|
||||||
loadInit(bs, &boot_information) catch |err| {
|
loadInit(bs, &boot_information) catch |err| {
|
||||||
log("danos: no /system/services/init (");
|
log("EFI: no /system/services/init (");
|
||||||
logBytes(@errorName(err));
|
logBytes(@errorName(err));
|
||||||
log(") - booting without user space\r\n");
|
log(") - booting without user space\r\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Best effort: the initial_ramdisk (VFS server + drivers) is optional too.
|
// Best effort: the initial_ramdisk (VFS server + drivers) is optional too.
|
||||||
loadInitialRamdisk(bs, &boot_information) catch |err| {
|
loadInitialRamdisk(bs, &boot_information) catch |err| {
|
||||||
log("danos: no initial_ramdisk (");
|
log("EFI: no initial_ramdisk (");
|
||||||
logBytes(@errorName(err));
|
logBytes(@errorName(err));
|
||||||
log(")\r\n");
|
log(")\r\n");
|
||||||
};
|
};
|
||||||
@@ -84,7 +85,7 @@ fn boot() !noreturn {
|
|||||||
// the map and exiting would invalidate the map key.
|
// the map and exiting would invalidate the map key.
|
||||||
const cr3 = try buildBootstrapTables(bs, &boot_information);
|
const cr3 = try buildBootstrapTables(bs, &boot_information);
|
||||||
|
|
||||||
log("danos: kernel loaded, exiting boot services\r\n");
|
progress("EFI: kernel loaded, exiting boot services\r\n");
|
||||||
boot_information.memory_map = try exitBootServices(bs);
|
boot_information.memory_map = try exitBootServices(bs);
|
||||||
|
|
||||||
// Switch onto our tables and jump to the kernel in one uninterruptible step.
|
// Switch onto our tables and jump to the kernel in one uninterruptible step.
|
||||||
@@ -395,7 +396,7 @@ fn loadInit(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !
|
|||||||
const image = try loadFile(bs, init_file_name);
|
const image = try loadFile(bs, init_file_name);
|
||||||
boot_information.init_base = @intFromPtr(image.ptr);
|
boot_information.init_base = @intFromPtr(image.ptr);
|
||||||
boot_information.init_len = image.len;
|
boot_information.init_len = image.len;
|
||||||
log("danos: /system/services/init loaded\r\n");
|
progress("EFI: /system/services/init loaded\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ferry the initial_ramdisk (the VFS server + drivers) to the kernel, same as init.
|
/// Ferry the initial_ramdisk (the VFS server + drivers) to the kernel, same as init.
|
||||||
@@ -403,7 +404,7 @@ fn loadInitialRamdisk(bs: *uefi.tables.BootServices, boot_information: *BootInfo
|
|||||||
const image = try loadFile(bs, initial_ramdisk_file_name);
|
const image = try loadFile(bs, initial_ramdisk_file_name);
|
||||||
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
|
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
|
||||||
boot_information.initial_ramdisk_len = image.len;
|
boot_information.initial_ramdisk_len = image.len;
|
||||||
log("danos: initial_ramdisk loaded\r\n");
|
progress("EFI: initial_ramdisk loaded\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate the ELF, copy every PT_LOAD segment to its physical address, and
|
/// Validate the ELF, copy every PT_LOAD segment to its physical address, and
|
||||||
@@ -561,6 +562,14 @@ fn log(comptime message: []const u8) void {
|
|||||||
_ = out.outputString(std.unicode.utf8ToUtf16LeStringLiteral(message)) catch {};
|
_ = out.outputString(std.unicode.utf8ToUtf16LeStringLiteral(message)) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A boot-progress breadcrumb: like `log`, but compiled out unless `-Dserial`
|
||||||
|
/// (off by default), so a real-hardware boot stays silent. Fatal errors use
|
||||||
|
/// `log` directly and always show, so a failed boot still explains itself.
|
||||||
|
fn progress(comptime message: []const u8) void {
|
||||||
|
if (!build_options.serial) return;
|
||||||
|
log(message);
|
||||||
|
}
|
||||||
|
|
||||||
/// Write a runtime ASCII byte string (e.g. an @errorName) by widening to UTF-16.
|
/// Write a runtime ASCII byte string (e.g. an @errorName) by widening to UTF-16.
|
||||||
fn logBytes(bytes: []const u8) void {
|
fn logBytes(bytes: []const u8) void {
|
||||||
const out = uefi.system_table.con_out orelse return;
|
const out = uefi.system_table.con_out orelse return;
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ fn addUserBinary(
|
|||||||
b: *std.Build,
|
b: *std.Build,
|
||||||
target: std.Build.ResolvedTarget,
|
target: std.Build.ResolvedTarget,
|
||||||
runtime_module: *std.Build.Module,
|
runtime_module: *std.Build.Module,
|
||||||
posix_module: *std.Build.Module,
|
|
||||||
mmio_module: *std.Build.Module,
|
mmio_module: *std.Build.Module,
|
||||||
|
xkeyboard_config_module: *std.Build.Module,
|
||||||
|
acpi_ids_module: *std.Build.Module,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
root: []const u8,
|
root: []const u8,
|
||||||
) *std.Build.Step.Compile {
|
) *std.Build.Step.Compile {
|
||||||
@@ -76,11 +77,14 @@ fn addUserBinary(
|
|||||||
.stack_protector = false,
|
.stack_protector = false,
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "runtime", .module = runtime_module },
|
.{ .name = "runtime", .module = runtime_module },
|
||||||
// POSIX/C compatibility layer, available to any program that wants it
|
|
||||||
// (danos-native code uses `runtime` directly). See library/posix/.
|
|
||||||
.{ .name = "posix", .module = posix_module },
|
|
||||||
// Typed volatile MMIO + memory barriers, for drivers. See library/mmio/.
|
// Typed volatile MMIO + memory barriers, for drivers. See library/mmio/.
|
||||||
.{ .name = "mmio", .module = mmio_module },
|
.{ .name = "mmio", .module = mmio_module },
|
||||||
|
// Keyboard layouts (keycode + modifiers -> keysym/character), available
|
||||||
|
// to any program that wants it. See library/xkeyboard-config/.
|
||||||
|
.{ .name = "xkeyboard-config", .module = xkeyboard_config_module },
|
||||||
|
// ACPI/PnP hardware-ID registry, so drivers name devices
|
||||||
|
// (HardwareId.ps2_keyboard) instead of magic "_HID" strings.
|
||||||
|
.{ .name = "acpi-ids", .module = acpi_ids_module },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -92,6 +96,105 @@ fn addUserBinary(
|
|||||||
return exe;
|
return exe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The modules the kernel imports, gathered once so both kernel variants (the
|
||||||
|
/// installed one and the serial-enabled one `run-x86-64` boots) are built from
|
||||||
|
/// the same set. `build_options` is *not* here — it carries `serial`/`test_case`,
|
||||||
|
/// which differ per variant, so `addKernel` builds it fresh each time.
|
||||||
|
const KernelModules = struct {
|
||||||
|
boot_handoff: *std.Build.Module,
|
||||||
|
abi: *std.Build.Module,
|
||||||
|
device_abi: *std.Build.Module,
|
||||||
|
architecture: *std.Build.Module,
|
||||||
|
platform: *std.Build.Module,
|
||||||
|
parameters: *std.Build.Module,
|
||||||
|
initial_ramdisk: *std.Build.Module,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Build the freestanding x86_64 kernel ELF. Factored so we can build it twice
|
||||||
|
/// from one recipe: the installed/flashable image (serial off by default) and the
|
||||||
|
/// serial-enabled variant `run-x86-64` boots — they differ only in the `serial`
|
||||||
|
/// build option baked into `build_options`.
|
||||||
|
fn addKernel(
|
||||||
|
b: *std.Build,
|
||||||
|
kernel_target: std.Build.ResolvedTarget,
|
||||||
|
optimize: std.builtin.OptimizeMode,
|
||||||
|
modules: KernelModules,
|
||||||
|
test_case: ?[]const u8,
|
||||||
|
serial: bool,
|
||||||
|
) *std.Build.Step.Compile {
|
||||||
|
// Compile-time configuration the kernel reads as `@import("build_options")`:
|
||||||
|
// the QEMU harness's -Dtest-case, and whether the serial log sink is compiled
|
||||||
|
// in (see the -Dserial option). Built per variant since `serial` differs.
|
||||||
|
const build_options = b.addOptions();
|
||||||
|
build_options.addOption(?[]const u8, "test_case", test_case);
|
||||||
|
build_options.addOption(bool, "serial", serial);
|
||||||
|
const build_options_module = build_options.createModule();
|
||||||
|
|
||||||
|
const exe = b.addExecutable(.{
|
||||||
|
.name = "kernel",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("system/kernel/kernel.zig"),
|
||||||
|
.target = kernel_target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.code_model = .kernel, // kernel runs in the top 2 GiB (higher half)
|
||||||
|
.red_zone = false, // interrupts would corrupt the SystemV red zone
|
||||||
|
.single_threaded = false, // SMP: the big kernel lock's atomics must be real across cores
|
||||||
|
.sanitize_c = .off, // the UBSan runtime needs f128/SSE support we don't provide
|
||||||
|
.stack_check = false, // stack-probe calls have no runtime to land in
|
||||||
|
.stack_protector = false,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "boot-handoff", .module = modules.boot_handoff },
|
||||||
|
.{ .name = "abi", .module = modules.abi },
|
||||||
|
.{ .name = "device-abi", .module = modules.device_abi },
|
||||||
|
.{ .name = "architecture", .module = modules.architecture },
|
||||||
|
.{ .name = "platform", .module = modules.platform },
|
||||||
|
.{ .name = "parameters", .module = modules.parameters },
|
||||||
|
.{ .name = "build_options", .module = build_options_module },
|
||||||
|
.{ .name = "initial-ramdisk", .module = modules.initial_ramdisk },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
exe.setLinkerScript(b.path("system/kernel/architecture/x86_64/linker.ld"));
|
||||||
|
exe.entry = .{ .symbol_name = "_start" };
|
||||||
|
// The self-hosted linker ignores parts of the linker script (PHDRS,
|
||||||
|
// /DISCARD/, AT(), section order); the higher-half layout depends on the
|
||||||
|
// script being authoritative, so pin the kernel to LLVM + LLD.
|
||||||
|
exe.use_llvm = true;
|
||||||
|
exe.use_lld = true;
|
||||||
|
// Higher-half virtual base (matches KERNEL_VIRT_BASE in linker.ld); the
|
||||||
|
// linker's AT() clauses give each segment a low physical load address
|
||||||
|
// (.text at 1 MiB), which the loader allocates and copies into.
|
||||||
|
exe.image_base = 0xFFFFFFFF80100000;
|
||||||
|
return exe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assemble the bootable FAT32 image (the in-repo Python builder) holding what
|
||||||
|
/// the firmware and loader need off the ESP: the EFI stub, `kernel`, `init`, and
|
||||||
|
/// the initial-ramdisk. Factored so the serial-enabled `run-x86-64` variant can
|
||||||
|
/// bundle its own kernel while sharing the (serial-independent) loader, init, and
|
||||||
|
/// ramdisk. Returns the image's LazyPath.
|
||||||
|
fn addBootImage(
|
||||||
|
b: *std.Build,
|
||||||
|
kernel_bin: std.Build.LazyPath,
|
||||||
|
efi_bin: std.Build.LazyPath,
|
||||||
|
init_bin: std.Build.LazyPath,
|
||||||
|
initial_ramdisk_img: std.Build.LazyPath,
|
||||||
|
) std.Build.LazyPath {
|
||||||
|
const mk_fat = b.addSystemCommand(&.{"python3"});
|
||||||
|
mk_fat.addFileArg(b.path("tools/make-fat-image.py"));
|
||||||
|
const fat_image = mk_fat.addOutputFileArg("danos-usb.img");
|
||||||
|
mk_fat.addArg("64"); // MiB
|
||||||
|
mk_fat.addArg("EFI/BOOT/BOOTX64.efi");
|
||||||
|
mk_fat.addFileArg(efi_bin);
|
||||||
|
mk_fat.addArg("system/kernel");
|
||||||
|
mk_fat.addFileArg(kernel_bin);
|
||||||
|
mk_fat.addArg("system/services/init");
|
||||||
|
mk_fat.addFileArg(init_bin);
|
||||||
|
mk_fat.addArg("boot/initial-ramdisk.img");
|
||||||
|
mk_fat.addFileArg(initial_ramdisk_img);
|
||||||
|
return fat_image;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
pub fn build(b: *std.Build) void {
|
||||||
ensureZigVersion();
|
ensureZigVersion();
|
||||||
|
|
||||||
@@ -123,10 +226,39 @@ pub fn build(b: *std.Build) void {
|
|||||||
});
|
});
|
||||||
// ACPI/PnP hardware-ID (_HID) names — the flat analog of pci-class for acpi_device
|
// ACPI/PnP hardware-ID (_HID) names — the flat analog of pci-class for acpi_device
|
||||||
// nodes. Also shared reference data.
|
// nodes. Also shared reference data.
|
||||||
|
// The AML interpreter, a build module so the ring-3 acpi service can run the
|
||||||
|
// same parser the kernel does (docs/discovery.md — the shared AML module).
|
||||||
|
// Pure Zig, no kernel imports — one source, two builds.
|
||||||
|
const aml_module = b.addModule("aml", .{
|
||||||
|
.root_source_file = b.path("system/devices/aml/aml.zig"),
|
||||||
|
});
|
||||||
|
|
||||||
const acpi_ids_module = b.addModule("acpi-ids", .{
|
const acpi_ids_module = b.addModule("acpi-ids", .{
|
||||||
.root_source_file = b.path("system/devices/acpi-ids.zig"),
|
.root_source_file = b.path("system/devices/acpi-ids.zig"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The USB device-framework wire ABI (chapter-9 set-up packets, standard +
|
||||||
|
// class requests, descriptors) and the USB class-code taxonomy — the flat
|
||||||
|
// reference the xHCI bus driver, the USB class drivers, and the device
|
||||||
|
// manager's identity matcher all share. Pure data, like pci-class/acpi-ids.
|
||||||
|
const usb_abi_module = b.addModule("usb-abi", .{
|
||||||
|
.root_source_file = b.path("system/devices/usb-abi.zig"),
|
||||||
|
});
|
||||||
|
const usb_ids_module = b.addModule("usb-ids", .{
|
||||||
|
.root_source_file = b.path("system/devices/usb-ids.zig"),
|
||||||
|
});
|
||||||
|
// The USB transfer protocol: what a USB class driver says to the xHCI bus
|
||||||
|
// driver to drive its device (open / control / interrupt / bulk). A protocol
|
||||||
|
// module like vfs-protocol, shared by the bus driver and every class driver.
|
||||||
|
const usb_transfer_protocol_module = b.addModule("usb-transfer-protocol", .{
|
||||||
|
.root_source_file = b.path("system/drivers/usb-xhci-bus/usb-transfer-protocol.zig"),
|
||||||
|
});
|
||||||
|
// The block-device protocol: read/write of fixed-size blocks, spoken between a
|
||||||
|
// filesystem and a block driver (usb-storage). A protocol module like the rest.
|
||||||
|
const block_protocol_module = b.addModule("block-protocol", .{
|
||||||
|
.root_source_file = b.path("system/services/block/protocol.zig"),
|
||||||
|
});
|
||||||
|
|
||||||
// Kernel tunables (maximum_cpus, stack sizes, tick rate). A dependency-free module of
|
// Kernel tunables (maximum_cpus, stack sizes, tick rate). A dependency-free module of
|
||||||
// compile-time constants, imported wherever a knob is read; keeps the trade-offs
|
// compile-time constants, imported wherever a knob is read; keeps the trade-offs
|
||||||
// in one place instead of scattered across the tree. See system/parameters.zig.
|
// in one place instead of scattered across the tree. See system/parameters.zig.
|
||||||
@@ -178,6 +310,13 @@ pub fn build(b: *std.Build) void {
|
|||||||
.root_source_file = b.path("system/services/vfs/protocol.zig"),
|
.root_source_file = b.path("system/services/vfs/protocol.zig"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The input wire protocol: the input service's public interface, exposed as its own
|
||||||
|
// module the same way vfs-protocol is. Shared by the input service, the runtime's
|
||||||
|
// `input` helper (subscribe/publish), and every source and subscriber.
|
||||||
|
const input_protocol_module = b.addModule("input-protocol", .{
|
||||||
|
.root_source_file = b.path("system/services/input/protocol.zig"),
|
||||||
|
});
|
||||||
|
|
||||||
// The danos-native user-space runtime: system_call wrappers, the C-convention
|
// The danos-native user-space runtime: system_call wrappers, the C-convention
|
||||||
// heap, IPC helpers, the process start shim, device access. This is the stable
|
// heap, IPC helpers, the process start shim, device access. This is the stable
|
||||||
// application ABI; POSIX compatibility is a separate library on top (see below).
|
// application ABI; POSIX compatibility is a separate library on top (see below).
|
||||||
@@ -193,9 +332,35 @@ pub fn build(b: *std.Build) void {
|
|||||||
.{ .name = "abi", .module = abi_module },
|
.{ .name = "abi", .module = abi_module },
|
||||||
.{ .name = "device-abi", .module = device_abi_module },
|
.{ .name = "device-abi", .module = device_abi_module },
|
||||||
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
|
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
|
||||||
|
.{ .name = "input-protocol", .module = input_protocol_module },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The device-manager protocol: hello + (M18.2) tree reports, exposed as its
|
||||||
|
// own module like the other protocol modules. Imported through the runtime.
|
||||||
|
const device_manager_protocol_module = b.addModule("device-manager-protocol", .{
|
||||||
|
.root_source_file = b.path("system/services/device-manager/device-manager-protocol.zig"),
|
||||||
|
});
|
||||||
|
runtime_module.addImport("device-manager-protocol", device_manager_protocol_module);
|
||||||
|
// The USB transfer protocol, so runtime.usb (the class-driver client) can speak
|
||||||
|
// it, the way runtime.input speaks the input protocol.
|
||||||
|
runtime_module.addImport("usb-transfer-protocol", usb_transfer_protocol_module);
|
||||||
|
// The block protocol, so runtime.block (the block-device client) can speak it.
|
||||||
|
runtime_module.addImport("block-protocol", block_protocol_module);
|
||||||
|
|
||||||
|
// The display protocol, so runtime.display (the compositor client) and the display
|
||||||
|
// service both speak it through the runtime, like the other protocol modules.
|
||||||
|
const display_protocol_module = b.addModule("display-protocol", .{
|
||||||
|
.root_source_file = b.path("system/services/display/protocol.zig"),
|
||||||
|
});
|
||||||
|
runtime_module.addImport("display-protocol", display_protocol_module);
|
||||||
|
|
||||||
|
// The power protocol: system power's domain-named surface (docs/power.md).
|
||||||
|
const power_protocol_module = b.addModule("power-protocol", .{
|
||||||
|
.root_source_file = b.path("system/services/power/protocol.zig"),
|
||||||
|
});
|
||||||
|
runtime_module.addImport("power-protocol", power_protocol_module);
|
||||||
|
|
||||||
// Typed volatile MMIO register access + memory-ordering barriers, for drivers on
|
// Typed volatile MMIO register access + memory-ordering barriers, for drivers on
|
||||||
// top of an mmio_map grant. Depends only on `builtin` (arch-conditional barriers);
|
// top of an mmio_map grant. Depends only on `builtin` (arch-conditional barriers);
|
||||||
// no target set, so it inherits each driver's. See library/mmio/mmio.zig.
|
// no target set, so it inherits each driver's. See library/mmio/mmio.zig.
|
||||||
@@ -203,15 +368,17 @@ pub fn build(b: *std.Build) void {
|
|||||||
.root_source_file = b.path("library/mmio/mmio.zig"),
|
.root_source_file = b.path("library/mmio/mmio.zig"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// The POSIX / C compatibility layer, a separate library layered strictly over the
|
// Keyboard layouts compiled from the X11 xkeyboard-config database into native Zig
|
||||||
// runtime (it calls the runtime's IPC/heap, never system calls directly). This is
|
// (keycode + modifiers -> keysym/character). The `layouts` tables are generated by
|
||||||
// the one place POSIX/C spellings are allowed verbatim — see docs/coding-standards.md
|
// tools/make-xkeyboard-config.py; `xkeyboard-config` is the hand-written API over them.
|
||||||
// and library/posix/posix.zig.
|
// No target set, so each inherits its importer's. See library/xkeyboard-config/.
|
||||||
const posix_module = b.addModule("posix", .{
|
const xkb_layouts_module = b.addModule("layouts", .{
|
||||||
.root_source_file = b.path("library/posix/posix.zig"),
|
.root_source_file = b.path("library/xkeyboard-config/generated/layouts.zig"),
|
||||||
|
});
|
||||||
|
const xkeyboard_config_module = b.addModule("xkeyboard-config", .{
|
||||||
|
.root_source_file = b.path("library/xkeyboard-config/xkeyboard-config.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "runtime", .module = runtime_module },
|
.{ .name = "layouts", .module = xkb_layouts_module },
|
||||||
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -224,9 +391,12 @@ pub fn build(b: *std.Build) void {
|
|||||||
// Compile-time configuration the kernel reads as `@import("build_options")`. The
|
// Compile-time configuration the kernel reads as `@import("build_options")`. The
|
||||||
// QEMU test harness sets -Dtest-case=<name> to run one self-test at boot.
|
// QEMU test harness sets -Dtest-case=<name> to run one self-test at boot.
|
||||||
const test_case = b.option([]const u8, "test-case", "Kernel self-test case to run at boot (see system/kernel/tests.zig)");
|
const test_case = b.option([]const u8, "test-case", "Kernel self-test case to run at boot (see system/kernel/tests.zig)");
|
||||||
const build_options = b.addOptions();
|
// The serial-console log sink. Off by default: a real machine often has no
|
||||||
build_options.addOption(?[]const u8, "test_case", test_case);
|
// working legacy COM1, and the boot log is kept in RAM (klog) and flushed to
|
||||||
const build_options_module = build_options.createModule();
|
// disk instead — serial is now only a QEMU convenience. `run-x86-64` and the
|
||||||
|
// QEMU test harness (test/qemu_test.py, which asserts on serial markers) turn
|
||||||
|
// it on; a flashable `zig build` image leaves it out. See serial.zig.
|
||||||
|
const serial = b.option(bool, "serial", "Compile the serial-console log sink into the kernel (default: off; run-x86-64 and the test harness enable it)") orelse false;
|
||||||
|
|
||||||
// --- Kernel: freestanding x86_64 ELF, jumped to by the bootloader ---
|
// --- Kernel: freestanding x86_64 ELF, jumped to by the bootloader ---
|
||||||
// SSE2 is part of the x86_64 baseline and UEFI leaves it enabled at handoff,
|
// SSE2 is part of the x86_64 baseline and UEFI leaves it enabled at handoff,
|
||||||
@@ -238,41 +408,17 @@ pub fn build(b: *std.Build) void {
|
|||||||
.abi = .none,
|
.abi = .none,
|
||||||
});
|
});
|
||||||
|
|
||||||
const exe = b.addExecutable(.{
|
const kernel_modules = KernelModules{
|
||||||
.name = "kernel",
|
.boot_handoff = boot_handoff_module,
|
||||||
.root_module = b.createModule(.{
|
.abi = abi_module,
|
||||||
.root_source_file = b.path("system/kernel/kernel.zig"),
|
.device_abi = device_abi_module,
|
||||||
.target = kernel_target,
|
.architecture = architecture_module,
|
||||||
.optimize = optimize,
|
.platform = platform_module,
|
||||||
.code_model = .kernel, // kernel runs in the top 2 GiB (higher half)
|
.parameters = parameters_module,
|
||||||
.red_zone = false, // interrupts would corrupt the SystemV red zone
|
.initial_ramdisk = initial_ramdisk_module,
|
||||||
.single_threaded = false, // SMP: the big kernel lock's atomics must be real across cores
|
};
|
||||||
.sanitize_c = .off, // the UBSan runtime needs f128/SSE support we don't provide
|
// The installed/flashable kernel: serial follows -Dserial (off by default).
|
||||||
.stack_check = false, // stack-probe calls have no runtime to land in
|
const exe = addKernel(b, kernel_target, optimize, kernel_modules, test_case, serial);
|
||||||
.stack_protector = false,
|
|
||||||
.imports = &.{
|
|
||||||
.{ .name = "boot-handoff", .module = boot_handoff_module },
|
|
||||||
.{ .name = "abi", .module = abi_module },
|
|
||||||
.{ .name = "device-abi", .module = device_abi_module },
|
|
||||||
.{ .name = "architecture", .module = architecture_module },
|
|
||||||
.{ .name = "platform", .module = platform_module },
|
|
||||||
.{ .name = "parameters", .module = parameters_module },
|
|
||||||
.{ .name = "build_options", .module = build_options_module },
|
|
||||||
.{ .name = "initial-ramdisk", .module = initial_ramdisk_module },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
exe.setLinkerScript(b.path("system/kernel/architecture/x86_64/linker.ld"));
|
|
||||||
exe.entry = .{ .symbol_name = "_start" };
|
|
||||||
// The self-hosted linker ignores parts of the linker script (PHDRS,
|
|
||||||
// /DISCARD/, AT(), section order); the higher-half layout depends on the
|
|
||||||
// script being authoritative, so pin the kernel to LLVM + LLD.
|
|
||||||
exe.use_llvm = true;
|
|
||||||
exe.use_lld = true;
|
|
||||||
// Higher-half virtual base (matches KERNEL_VIRT_BASE in linker.ld); the
|
|
||||||
// linker's AT() clauses give each segment a low physical load address
|
|
||||||
// (.text at 1 MiB), which the loader allocates and copies into.
|
|
||||||
exe.image_base = 0xFFFFFFFF80100000;
|
|
||||||
|
|
||||||
// Everything installs into a FHS-shaped zig-out: it IS the danos filesystem *and*
|
// Everything installs into a FHS-shaped zig-out: it IS the danos filesystem *and*
|
||||||
// the boot volume. Each binary lands at its addressed, leaf-collapsed path — the
|
// the boot volume. Each binary lands at its addressed, leaf-collapsed path — the
|
||||||
@@ -286,7 +432,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
// Built by the shared user-binary recipe (see addUserBinary): freestanding,
|
// Built by the shared user-binary recipe (see addUserBinary): freestanding,
|
||||||
// linked into the kernel's user region against the `runtime` runtime library, and
|
// linked into the kernel's user region against the `runtime` runtime library, and
|
||||||
// started in ring 3 by the kernel's user-ELF loader.
|
// started in ring 3 by the kernel's user-ELF loader.
|
||||||
const init_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "init", "system/services/init/init.zig");
|
const init_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "init", "system/services/init/init.zig");
|
||||||
const init_install = b.addInstallArtifact(init_exe, .{ .dest_dir = .{ .override = .{ .custom = "system/services" } } });
|
const init_install = b.addInstallArtifact(init_exe, .{ .dest_dir = .{ .override = .{ .custom = "system/services" } } });
|
||||||
b.getInstallStep().dependOn(&init_install.step);
|
b.getInstallStep().dependOn(&init_install.step);
|
||||||
|
|
||||||
@@ -294,16 +440,72 @@ pub fn build(b: *std.Build) void {
|
|||||||
// Each is built by the same user-binary recipe, then packed into one image by
|
// Each is built by the same user-binary recipe, then packed into one image by
|
||||||
// the host-side make-initial-ramdisk tool. The bootloader ferries the image to the kernel,
|
// the host-side make-initial-ramdisk tool. The bootloader ferries the image to the kernel,
|
||||||
// which unpacks it and spawns each program (system/initial-ramdisk.zig).
|
// which unpacks it and spawns each program (system/initial-ramdisk.zig).
|
||||||
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "vfs", "system/services/vfs/vfs.zig");
|
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs", "system/services/vfs/vfs.zig");
|
||||||
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "vfs-test", "system/services/vfs/vfs-test.zig");
|
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs/vfs-test.zig");
|
||||||
const hpet_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "hpet", "system/drivers/hpet/hpet.zig");
|
const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig");
|
||||||
const bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "bus", "system/drivers/bus/bus.zig");
|
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
|
||||||
const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "bus", "system/drivers/ps2-bus/ps2-bus.zig");
|
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
|
||||||
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
|
const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig");
|
||||||
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
|
// The xHCI bus driver builds chapter-9 requests and decodes descriptors from
|
||||||
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "device-manager", "system/services/device-manager/device-manager.zig");
|
// usb-abi, and reports each interface's (class,subclass,protocol) identity via
|
||||||
const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "args-echo", "system/services/args-echo/args-echo.zig");
|
// usb-ids.packTriple.
|
||||||
const process_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "process-test", "system/services/process-test/process-test.zig");
|
usb_xhci_bus_exe.root_module.addImport("usb-abi", usb_abi_module);
|
||||||
|
usb_xhci_bus_exe.root_module.addImport("usb-ids", usb_ids_module);
|
||||||
|
usb_xhci_bus_exe.root_module.addImport("usb-transfer-protocol", usb_transfer_protocol_module);
|
||||||
|
// The USB HID class drivers: keyboard and mouse. They own no hardware — each
|
||||||
|
// opens its device through runtime.usb (the transfer protocol) and publishes to
|
||||||
|
// the input service. They build chapter-9 class requests from usb-abi.
|
||||||
|
const usb_hid_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-keyboard", "system/drivers/usb-hid/keyboard.zig");
|
||||||
|
usb_hid_keyboard_exe.root_module.addImport("usb-abi", usb_abi_module);
|
||||||
|
const usb_hid_mouse_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-mouse", "system/drivers/usb-hid/mouse.zig");
|
||||||
|
usb_hid_mouse_exe.root_module.addImport("usb-abi", usb_abi_module);
|
||||||
|
// The USB mass-storage class driver: opens its device via runtime.usb, drives it
|
||||||
|
// with Bulk-Only Transport + SCSI, and serves the block protocol under `.block`.
|
||||||
|
const usb_storage_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-storage", "system/drivers/usb-storage/usb-storage.zig");
|
||||||
|
usb_storage_exe.root_module.addImport("block-protocol", block_protocol_module);
|
||||||
|
// The FAT filesystem server: mounts the block device and serves it into the VFS
|
||||||
|
// at /mnt/usb. Its engine (engine.zig / on-disk.zig) is imported relatively.
|
||||||
|
const fat_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat", "system/services/fat/fat.zig");
|
||||||
|
const display_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display", "system/services/display/display.zig");
|
||||||
|
const display_demo_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display-demo", "system/services/display-demo/display-demo.zig");
|
||||||
|
const fat_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat-test", "system/services/fat/fat-test.zig");
|
||||||
|
const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig");
|
||||||
|
// The PCI bus driver decodes each function's class triple to human names in its
|
||||||
|
// boot log (class/subclass/prog-IF), so pull in the shared pci-class reference.
|
||||||
|
pci_bus_exe.root_module.addImport("pci-class", pci_class_module);
|
||||||
|
// A test fixture, not a real driver: hellos to the device manager, then faults —
|
||||||
|
// what the driver-restart scenario drives the crash-loop cap with.
|
||||||
|
const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig");
|
||||||
|
const device_list_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-list", "system/services/device-list/device-list.zig");
|
||||||
|
// The discovery service: one swappable process per firmware
|
||||||
|
// (docs/discovery.md), bundled under the neutral ramdisk name
|
||||||
|
// "discovery" so the device manager never learns which firmware it is on.
|
||||||
|
// x86 boots describe hardware with ACPI; the Raspberry Pis hand over a
|
||||||
|
// flattened device tree — the aarch64 target flips the default when it
|
||||||
|
// lands (docs/arm.md). Both are placeholders until M20.1 (acpi) and the
|
||||||
|
// ARM bring-up (fdt).
|
||||||
|
const Discovery = enum { acpi, fdt };
|
||||||
|
const discovery = b.option(Discovery, "discovery", "Which discovery service fills the ramdisk's 'discovery' slot (default: acpi)") orelse Discovery.acpi;
|
||||||
|
const discovery_source: []const u8 = switch (discovery) {
|
||||||
|
.acpi => "system/services/acpi/acpi.zig",
|
||||||
|
.fdt => "system/services/fdt/fdt.zig",
|
||||||
|
};
|
||||||
|
const discovery_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "discovery", discovery_source);
|
||||||
|
if (discovery == .acpi) discovery_exe.root_module.addImport("aml", aml_module);
|
||||||
|
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig");
|
||||||
|
// Names the xHCI PCI class triple from the shared taxonomy instead of a bare 0x0C0330.
|
||||||
|
device_manager_exe.root_module.addImport("pci-class", pci_class_module);
|
||||||
|
// The manager matches reported USB interfaces by their (class,subclass,protocol)
|
||||||
|
// triple (usbDriverForIdentity), built from the named usb-ids codes.
|
||||||
|
device_manager_exe.root_module.addImport("usb-ids", usb_ids_module);
|
||||||
|
// The input service and its exercisers: the fan-out server, a hardware-free synthetic
|
||||||
|
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
|
||||||
|
const input_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input", "system/services/input/input.zig");
|
||||||
|
const input_source_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-source", "system/services/input-source/input-source.zig");
|
||||||
|
const input_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-test", "system/services/input-test/input-test.zig");
|
||||||
|
const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "args-echo", "system/services/args-echo/args-echo.zig");
|
||||||
|
const process_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "process-test", "system/services/process-test/process-test.zig");
|
||||||
|
const log_flush_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "log-flush", "system/services/log-flush/log-flush.zig");
|
||||||
|
|
||||||
// Pack the user binaries into the initial_ramdisk image with the host-side Python tool
|
// Pack the user binaries into the initial_ramdisk image with the host-side Python tool
|
||||||
// (the container format is trivial, and Python sidesteps std API churn). Args:
|
// (the container format is trivial, and Python sidesteps std API churn). Args:
|
||||||
@@ -315,33 +517,67 @@ pub fn build(b: *std.Build) void {
|
|||||||
mk_run.addFileArg(vfs_exe.getEmittedBin());
|
mk_run.addFileArg(vfs_exe.getEmittedBin());
|
||||||
mk_run.addArg("vfs-test");
|
mk_run.addArg("vfs-test");
|
||||||
mk_run.addFileArg(vfstest_exe.getEmittedBin());
|
mk_run.addFileArg(vfstest_exe.getEmittedBin());
|
||||||
mk_run.addArg("hpet");
|
|
||||||
mk_run.addFileArg(hpet_exe.getEmittedBin());
|
|
||||||
mk_run.addArg("bus");
|
|
||||||
mk_run.addFileArg(bus_exe.getEmittedBin());
|
|
||||||
mk_run.addArg("ps2-bus");
|
mk_run.addArg("ps2-bus");
|
||||||
mk_run.addFileArg(ps2_bus_exe.getEmittedBin());
|
mk_run.addFileArg(ps2_bus_exe.getEmittedBin());
|
||||||
mk_run.addArg("ps2-keyboard");
|
mk_run.addArg("ps2-keyboard");
|
||||||
mk_run.addFileArg(ps2_keyboard_exe.getEmittedBin());
|
mk_run.addFileArg(ps2_keyboard_exe.getEmittedBin());
|
||||||
mk_run.addArg("ps2-mouse");
|
mk_run.addArg("ps2-mouse");
|
||||||
mk_run.addFileArg(ps2_mouse_exe.getEmittedBin());
|
mk_run.addFileArg(ps2_mouse_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("usb-xhci-bus");
|
||||||
|
mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("usb-hid-keyboard");
|
||||||
|
mk_run.addFileArg(usb_hid_keyboard_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("usb-hid-mouse");
|
||||||
|
mk_run.addFileArg(usb_hid_mouse_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("usb-storage");
|
||||||
|
mk_run.addFileArg(usb_storage_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("fat");
|
||||||
|
mk_run.addFileArg(fat_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("fat-test");
|
||||||
|
mk_run.addFileArg(fat_test_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("display");
|
||||||
|
mk_run.addFileArg(display_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("display-demo");
|
||||||
|
mk_run.addFileArg(display_demo_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("pci-bus");
|
||||||
|
mk_run.addFileArg(pci_bus_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("crash-test");
|
||||||
|
mk_run.addFileArg(crash_test_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("device-list");
|
||||||
|
mk_run.addFileArg(device_list_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("discovery");
|
||||||
|
mk_run.addFileArg(discovery_exe.getEmittedBin());
|
||||||
mk_run.addArg("device-manager");
|
mk_run.addArg("device-manager");
|
||||||
mk_run.addFileArg(device_manager_exe.getEmittedBin());
|
mk_run.addFileArg(device_manager_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("input");
|
||||||
|
mk_run.addFileArg(input_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("input-source");
|
||||||
|
mk_run.addFileArg(input_source_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("input-test");
|
||||||
|
mk_run.addFileArg(input_test_exe.getEmittedBin());
|
||||||
mk_run.addArg("args-echo");
|
mk_run.addArg("args-echo");
|
||||||
mk_run.addFileArg(args_echo_exe.getEmittedBin());
|
mk_run.addFileArg(args_echo_exe.getEmittedBin());
|
||||||
mk_run.addArg("process-test");
|
mk_run.addArg("process-test");
|
||||||
mk_run.addFileArg(process_test_exe.getEmittedBin());
|
mk_run.addFileArg(process_test_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("log-flush");
|
||||||
|
mk_run.addFileArg(log_flush_exe.getEmittedBin());
|
||||||
|
|
||||||
// Also install the packed binaries to their FHS homes, so zig-out is a true image
|
// Also install the packed binaries to their FHS homes, so zig-out is a true image
|
||||||
// of the filesystem — even though at boot they arrive inside the initial-ramdisk.
|
// of the filesystem — even though at boot they arrive inside the initial-ramdisk.
|
||||||
for ([_]struct { *std.Build.Step.Compile, []const u8 }{
|
for ([_]struct { *std.Build.Step.Compile, []const u8 }{
|
||||||
.{ vfs_exe, "system/services" },
|
.{ vfs_exe, "system/services" },
|
||||||
.{ device_manager_exe, "system/services" },
|
.{ device_manager_exe, "system/services" },
|
||||||
.{ hpet_exe, "system/drivers" },
|
.{ input_exe, "system/services" },
|
||||||
.{ bus_exe, "system/drivers" },
|
|
||||||
.{ ps2_bus_exe, "system/drivers" },
|
.{ ps2_bus_exe, "system/drivers" },
|
||||||
.{ ps2_keyboard_exe, "system/drivers" },
|
.{ ps2_keyboard_exe, "system/drivers" },
|
||||||
.{ ps2_mouse_exe, "system/drivers" },
|
.{ ps2_mouse_exe, "system/drivers" },
|
||||||
|
.{ usb_xhci_bus_exe, "system/drivers" },
|
||||||
|
.{ usb_hid_keyboard_exe, "system/drivers" },
|
||||||
|
.{ usb_hid_mouse_exe, "system/drivers" },
|
||||||
|
.{ usb_storage_exe, "system/drivers" },
|
||||||
|
.{ fat_exe, "system/services" },
|
||||||
|
.{ display_exe, "system/services" },
|
||||||
|
.{ log_flush_exe, "system/services" },
|
||||||
}) |entry| {
|
}) |entry| {
|
||||||
const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } });
|
const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } });
|
||||||
b.getInstallStep().dependOn(&step.step);
|
b.getInstallStep().dependOn(&step.step);
|
||||||
@@ -354,6 +590,13 @@ pub fn build(b: *std.Build) void {
|
|||||||
// Boot methods live in boot/, one per way of getting the kernel running.
|
// Boot methods live in boot/, one per way of getting the kernel running.
|
||||||
// Each is its own binary/entry (a loader is built for its own target); today
|
// Each is its own binary/entry (a loader is built for its own target); today
|
||||||
// that's UEFI for x86-64, with room for e.g. a device-tree path for the Pis.
|
// that's UEFI for x86-64, with room for e.g. a device-tree path for the Pis.
|
||||||
|
// The loader reads -Dserial too, so its boot-progress breadcrumbs (con_out,
|
||||||
|
// which firmware may mirror to a serial console) are silenced by default — a
|
||||||
|
// real-hardware boot stays quiet. Fatal-error messages ignore this and always
|
||||||
|
// show, so a failed boot still explains itself on screen. See boot/efi.zig.
|
||||||
|
const loader_options = b.addOptions();
|
||||||
|
loader_options.addOption(bool, "serial", serial);
|
||||||
|
const loader_options_module = loader_options.createModule();
|
||||||
const efiexe = b.addExecutable(.{
|
const efiexe = b.addExecutable(.{
|
||||||
.name = "BOOTX64",
|
.name = "BOOTX64",
|
||||||
.root_module = b.createModule(.{
|
.root_module = b.createModule(.{
|
||||||
@@ -366,6 +609,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
.imports = &.{
|
.imports = &.{
|
||||||
// The bootloader speaks only the handoff contract — never the user ABI.
|
// The bootloader speaks only the handoff contract — never the user ABI.
|
||||||
.{ .name = "boot-handoff", .module = boot_handoff_module },
|
.{ .name = "boot-handoff", .module = boot_handoff_module },
|
||||||
|
.{ .name = "build_options", .module = loader_options_module },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -375,6 +619,32 @@ pub fn build(b: *std.Build) void {
|
|||||||
const efi_install = b.addInstallArtifact(efiexe, .{ .dest_dir = .{ .override = .{ .custom = "EFI/BOOT" } } });
|
const efi_install = b.addInstallArtifact(efiexe, .{ .dest_dir = .{ .override = .{ .custom = "EFI/BOOT" } } });
|
||||||
b.getInstallStep().dependOn(&efi_install.step);
|
b.getInstallStep().dependOn(&efi_install.step);
|
||||||
|
|
||||||
|
// --- danos-usb.img: the bootable FAT32 USB image ---
|
||||||
|
// Format a real FAT32 image (the in-repo Python builder, no external tools)
|
||||||
|
// holding exactly what the firmware and bootloader need off the ESP: the EFI
|
||||||
|
// stub, the kernel, init, and the initial-ramdisk. QEMU presents this image as
|
||||||
|
// a USB mass-storage device the guest boots from (see run-x86-64 and the test
|
||||||
|
// harness), and the danos fat driver mounts the same image at /mnt/usb.
|
||||||
|
const fat_image = addBootImage(b, exe.getEmittedBin(), efiexe.getEmittedBin(), init_exe.getEmittedBin(), initial_ramdisk_img);
|
||||||
|
const fat_image_install = b.addInstallFile(fat_image, "danos-usb.img");
|
||||||
|
b.getInstallStep().dependOn(&fat_image_install.step);
|
||||||
|
|
||||||
|
// The image `run-x86-64` boots: identical to the flashable one but with the
|
||||||
|
// serial log sink compiled in, so a developer always gets the machine-readable
|
||||||
|
// log captured to serial0 — without baking serial into the image users flash.
|
||||||
|
// Built lazily (only when `run-x86-64` is requested), and never installed.
|
||||||
|
const exe_serial = addKernel(b, kernel_target, optimize, kernel_modules, test_case, true);
|
||||||
|
const fat_image_serial = addBootImage(b, exe_serial.getEmittedBin(), efiexe.getEmittedBin(), init_exe.getEmittedBin(), initial_ramdisk_img);
|
||||||
|
|
||||||
|
// `zig build check-fat-image` — validate the produced image is a real FAT32
|
||||||
|
// with the EFI stub present (the builder's own --verify, no external tools).
|
||||||
|
const check_fat = b.addSystemCommand(&.{"python3"});
|
||||||
|
check_fat.addFileArg(b.path("tools/make-fat-image.py"));
|
||||||
|
check_fat.addArg("--verify");
|
||||||
|
check_fat.addFileArg(fat_image);
|
||||||
|
const check_fat_step = b.step("check-fat-image", "Verify the FAT32 USB image is valid and bootable");
|
||||||
|
check_fat_step.dependOn(&check_fat.step);
|
||||||
|
|
||||||
// --- run-x86-64: boot the x86-64 kernel in QEMU via UEFI/OVMF ---
|
// --- run-x86-64: boot the x86-64 kernel in QEMU via UEFI/OVMF ---
|
||||||
// Firmware lives in different places per OS/distro, so probe the known
|
// Firmware lives in different places per OS/distro, so probe the known
|
||||||
// layouts (Architecture, Debian/Ubuntu, Fedora, macOS Homebrew) and use the first
|
// layouts (Architecture, Debian/Ubuntu, Fedora, macOS Homebrew) and use the first
|
||||||
@@ -413,6 +683,19 @@ pub fn build(b: *std.Build) void {
|
|||||||
|
|
||||||
const run_efi = b.addSystemCommand(&.{
|
const run_efi = b.addSystemCommand(&.{
|
||||||
"qemu-system-x86_64",
|
"qemu-system-x86_64",
|
||||||
|
"-device",
|
||||||
|
"qemu-xhci,id=xhci",
|
||||||
|
"-device",
|
||||||
|
"usb-mouse,bus=xhci.0",
|
||||||
|
"-device",
|
||||||
|
"usb-kbd,bus=xhci.0",
|
||||||
|
// "-usb",
|
||||||
|
// "-device",
|
||||||
|
// "usb-ehci,id=ehci",
|
||||||
|
// "-device",
|
||||||
|
// "usb-tablet,bus=usb-bus.0",
|
||||||
|
// "-device",
|
||||||
|
// "usb-mouse,bus=ehci.0",
|
||||||
"-machine",
|
"-machine",
|
||||||
"q35",
|
"q35",
|
||||||
"-m",
|
"-m",
|
||||||
@@ -422,10 +705,14 @@ pub fn build(b: *std.Build) void {
|
|||||||
});
|
});
|
||||||
run_efi.addArg("-drive");
|
run_efi.addArg("-drive");
|
||||||
run_efi.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
|
run_efi.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
|
||||||
// Present the FHS zig-out to the guest as a FAT drive — it is the boot volume.
|
// Boot off the FAT32 USB image: a mass-storage device on the same xHCI bus as
|
||||||
|
// the keyboard and mouse. OVMF finds \EFI\BOOT\BOOTX64.efi on it and boots.
|
||||||
|
// The serial-enabled variant, so serial0 carries the log for this dev boot.
|
||||||
|
run_efi.addArg("-drive");
|
||||||
|
run_efi.addPrefixedFileArg("if=none,id=bootusb,format=raw,file=", fat_image_serial);
|
||||||
run_efi.addArgs(&.{
|
run_efi.addArgs(&.{
|
||||||
"-drive",
|
"-device",
|
||||||
b.fmt("format=raw,file=fat:rw:{s}", .{b.install_path}),
|
"usb-storage,bus=xhci.0,drive=bootusb,removable=on,bootindex=0",
|
||||||
"-net",
|
"-net",
|
||||||
"none",
|
"none",
|
||||||
// Emulated display advertising 1280x720 as its native (EDID preferred)
|
// Emulated display advertising 1280x720 as its native (EDID preferred)
|
||||||
@@ -444,8 +731,10 @@ pub fn build(b: *std.Build) void {
|
|||||||
const make_log_dir = b.addSystemCommand(&.{ "mkdir", "-p", log_dir });
|
const make_log_dir = b.addSystemCommand(&.{ "mkdir", "-p", log_dir });
|
||||||
const serial_log = b.fmt("{s}/run-x86-64-serial0-{s}.log", .{ log_dir, timestamp(b) });
|
const serial_log = b.fmt("{s}/run-x86-64-serial0-{s}.log", .{ log_dir, timestamp(b) });
|
||||||
run_efi.addArgs(&.{ "-serial", b.fmt("file:{s}", .{serial_log}) });
|
run_efi.addArgs(&.{ "-serial", b.fmt("file:{s}", .{serial_log}) });
|
||||||
// The whole FHS zig-out must be installed (and the scratch dir created) before we mount it.
|
// We boot the self-contained `fat_image_serial` (added as a file arg above, so
|
||||||
run_efi.step.dependOn(b.getInstallStep());
|
// it's already a dependency) — not the installed FHS zig-out — so `run-x86-64`
|
||||||
|
// builds only the serial kernel, never the flashable one. Just make the serial
|
||||||
|
// scratch dir first.
|
||||||
run_efi.step.dependOn(&make_log_dir.step);
|
run_efi.step.dependOn(&make_log_dir.step);
|
||||||
|
|
||||||
const run_efi_step = b.step("run-x86-64", "Boot the x86-64 kernel in QEMU (UEFI/OVMF); serial0 is logged to zig-out/qemu-test/run-x86-64-serial0-<timestamp>.log");
|
const run_efi_step = b.step("run-x86-64", "Boot the x86-64 kernel in QEMU (UEFI/OVMF); serial0 is logged to zig-out/qemu-test/run-x86-64-serial0-<timestamp>.log");
|
||||||
@@ -471,7 +760,21 @@ pub fn build(b: *std.Build) void {
|
|||||||
"system/devices/device-abi.zig",
|
"system/devices/device-abi.zig",
|
||||||
"system/devices/pci-class.zig", // class/subclass/prog-IF name decoding
|
"system/devices/pci-class.zig", // class/subclass/prog-IF name decoding
|
||||||
"system/devices/acpi-ids.zig", // _HID name decoding
|
"system/devices/acpi-ids.zig", // _HID name decoding
|
||||||
|
"system/devices/aml/aml.zig", // AML parse + interpret, incl. Notify dispatch (M21)
|
||||||
|
"system/devices/usb-abi.zig", // wire sizes + bit packings + set-up packet encodings
|
||||||
|
"system/devices/usb-ids.zig", // class/subclass/protocol code assignments
|
||||||
"library/mmio/mmio.zig", // barriers assemble + registers round-trip
|
"library/mmio/mmio.zig", // barriers assemble + registers round-trip
|
||||||
|
"system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine
|
||||||
|
"system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly
|
||||||
|
"system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode
|
||||||
|
"system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes
|
||||||
|
"system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian)
|
||||||
|
"system/services/vfs/path.zig", // mount-prefix path matching
|
||||||
|
"system/services/vfs/protocol.zig", // NodeKind / DirectoryEntry sizes + op values
|
||||||
|
"system/services/fat/on-disk.zig", // FAT on-disk struct sizes + type detection
|
||||||
|
"system/services/fat/engine.zig", // FAT read/write over a RAM-backed image
|
||||||
|
"system/services/display/compositor.zig", // Rect math + fill/composite/blit-tile
|
||||||
|
"system/services/display/protocol.zig", // pack(): native pixel encoding per format
|
||||||
}) |root| {
|
}) |root| {
|
||||||
const mod_tests = b.addTest(.{
|
const mod_tests = b.addTest(.{
|
||||||
.root_module = b.createModule(.{
|
.root_module = b.createModule(.{
|
||||||
@@ -482,4 +785,40 @@ pub fn build(b: *std.Build) void {
|
|||||||
});
|
});
|
||||||
test_step.dependOn(&b.addRunArtifact(mod_tests).step);
|
test_step.dependOn(&b.addRunArtifact(mod_tests).step);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The xkeyboard-config keymap tests need its generated `layouts` import wired, so they
|
||||||
|
// don't fit the plain loop above. Its keycode->character assertions are the end-to-end
|
||||||
|
// proof that the xkb-data -> generator -> Zig-lookup pipeline is correct.
|
||||||
|
const xkb_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("library/xkeyboard-config/xkeyboard-config.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "layouts", .module = xkb_layouts_module },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
test_step.dependOn(&b.addRunArtifact(xkb_tests).step);
|
||||||
|
|
||||||
|
// runtime.time's Instant/Duration arithmetic. time.zig pulls in system.zig (the
|
||||||
|
// syscall wrappers), which needs the `abi` module, so it doesn't fit the plain
|
||||||
|
// loop above.
|
||||||
|
const time_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("library/runtime/time.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "abi", .module = abi_module },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
test_step.dependOn(&b.addRunArtifact(time_tests).step);
|
||||||
|
|
||||||
|
// Convenience: `zig build gen-xkeyboard-config` regenerates the layout tables from the
|
||||||
|
// vendored data (offline). `fetch` (the network step) stays a manual script run.
|
||||||
|
const gen_xkb = b.addSystemCommand(&.{ "python3", "tools/make-xkeyboard-config.py", "generate" });
|
||||||
|
const gen_xkb_step = b.step("gen-xkeyboard-config", "Regenerate library/xkeyboard-config/generated from the vendored data");
|
||||||
|
gen_xkb_step.dependOn(&gen_xkb.step);
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-18
@@ -45,14 +45,36 @@ rather than restate it. Roughly in the order things happen at runtime:
|
|||||||
until its hardware interrupts it**. The claim is the capability; `irq_ack` is the
|
until its hardware interrupts it**. The claim is the capability; `irq_ack` is the
|
||||||
unmask.
|
unmask.
|
||||||
14. **[driver-model.md](driver-model.md) — buses, classes and host controllers.** How
|
14. **[driver-model.md](driver-model.md) — buses, classes and host controllers.** How
|
||||||
real driver stacks factor into three shapes, how families share code, and the
|
real driver stacks factor into three shapes and how families share code. The
|
||||||
proposed ABI for the three primitives still missing (capability passing, DMA +
|
three primitives it proposed are long since built (M13 capability passing,
|
||||||
memory barriers, MSI).
|
M14 DMA + barriers, M15 MSI), and the driver *contract* on top of them —
|
||||||
|
hello, supervision, restart — is built too (device-manager.md, M18).
|
||||||
15. **[process-management.md](process-management.md) — process management.** The
|
15. **[process-management.md](process-management.md) — process management.** The
|
||||||
microkernel's `ps`/`kill`/SIGCHLD: enumerate as a table snapshot, the
|
microkernel's `ps`/`kill`/SIGCHLD: enumerate as a table snapshot, the
|
||||||
supervision link as the kill authority, and child-exit notifications over the
|
supervision link as the kill authority, and child-exit notifications over the
|
||||||
same endpoints IRQs arrive on.
|
same endpoints IRQs arrive on.
|
||||||
16. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
16. **[process-lifecycle.md](process-lifecycle.md) — the process lifecycle.** Built
|
||||||
|
(M17): signals over IPC as the one lifecycle vocabulary every process speaks — the
|
||||||
|
POSIX.1-1990 words with message delivery instead of stack hijack, the stable
|
||||||
|
`runtime.process` interface, exit reasons, published exit events any stateful
|
||||||
|
service can subscribe to (the VFS releasing dead clients' handles), and the two
|
||||||
|
iron rules (cleanup is the kernel's job; kill is not a signal).
|
||||||
|
17. **[device-manager.md](device-manager.md) — the device manager.** Built (M18,
|
||||||
|
through the app surface): the
|
||||||
|
tree, the matcher, and the supervisor. Tree structure lives in the manager,
|
||||||
|
authority stays in the kernel; bus drivers report what they see; drivers are
|
||||||
|
restarted through the lifecycle vocabulary — the plan that turns
|
||||||
|
[resilience.md](resilience.md)'s restart goal into increments.
|
||||||
|
18. **[input.md](input.md) — the input module.** Broadcasting input events (keyboard,
|
||||||
|
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. **[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:
|
||||||
@@ -65,9 +87,19 @@ Start with the north star:
|
|||||||
- **[resilience.md](resilience.md) — resilience.** A design note (not built yet) on
|
- **[resilience.md](resilience.md) — resilience.** A design note (not built yet) on
|
||||||
fault isolation + live restart — the reincarnation-server + capability model that
|
fault isolation + live restart — the reincarnation-server + capability model that
|
||||||
makes "if I break it, I can restart it" real. danos's core motivation.
|
makes "if I break it, I can restart it" real. danos's core motivation.
|
||||||
|
- **[zig-self-hosting.md](zig-self-hosting.md) — running Zig on danos.** A design note
|
||||||
|
(not built yet) on making danos a real Zig target (`-target x86_64-danos`) and
|
||||||
|
eventually running the compiler on it. The key realisation: Zig 0.16 reduces an OS
|
||||||
|
port to **one seam** (`std.os.danos`), so we build `runtime.os` (→ that seam) plus a
|
||||||
|
thin `runtime.fs`, retire the `posix` shim, and follow a phased path to
|
||||||
|
`zig build-exe hello.zig` running on danos — **not** Linux-ABI emulation.
|
||||||
|
|
||||||
Cutting across all of these:
|
Cutting across all of these:
|
||||||
|
|
||||||
|
- **[system-requirements.md](system-requirements.md) — system requirements.** The
|
||||||
|
hardware needed to run danos: minimum specs (UEFI x86-64, ACPI, PCIe ECAM,
|
||||||
|
xHCI, ~128 MiB RAM) grounded in what the boot path actually assumes, plus a
|
||||||
|
plain-language guide matching Intel/AMD CPU generations by name.
|
||||||
- **[arch.md](arch.md) — the architecture split.** How CPU-specific code is kept
|
- **[arch.md](arch.md) — the architecture split.** How CPU-specific code is kept
|
||||||
behind a build-time `arch` module so the generic kernel never names x86_64,
|
behind a build-time `arch` module so the generic kernel never names x86_64,
|
||||||
leaving room for other systems (e.g. an AArch64 Raspberry Pi) later.
|
leaving room for other systems (e.g. an AArch64 Raspberry Pi) later.
|
||||||
@@ -79,7 +111,16 @@ Cutting across all of these:
|
|||||||
when to build it, and how to keep it architecture-agnostic.
|
when to build it, and how to keep it architecture-agnostic.
|
||||||
- **[acpi.md](acpi.md) — finding the ACPI tables.** The concrete x86 locator chain:
|
- **[acpi.md](acpi.md) — finding the ACPI tables.** The concrete x86 locator chain:
|
||||||
how the loader captures the **RSDP**, hands its physical address across in `BootInfo`,
|
how the loader captures the **RSDP**, hands its physical address across in `BootInfo`,
|
||||||
and how the platform derives the **RSDT/XSDT** from it and walks the SDTs.
|
and how the platform derives the **RSDT/XSDT** from it and walks the SDTs — plus the
|
||||||
|
live event side (the SCI, the power button, GPE/Notify) the ring-3 acpi service runs.
|
||||||
|
- **[power.md](power.md) — the power service.** System power as a domain-named
|
||||||
|
service: button/lid/battery events published to subscribers, and init's orderly
|
||||||
|
shutdown composing the [lifecycle](process-lifecycle.md) stop sequence with an ACPI
|
||||||
|
S5 write. Firmware-neutral — a PSCI backend drops in on ARM.
|
||||||
|
- **[timers.md](timers.md) — timers and time.** The ring-3 surface for reading the
|
||||||
|
clock and waiting: why `now()` is a syscall rather than a service, and the one-shot
|
||||||
|
timer notification (`timer_bind`) that gives supervisors a timed wait — built on the
|
||||||
|
LAPIC heartbeat and calibrated TSC of [device-interrupts.md](device-interrupts.md).
|
||||||
- **[smp.md](smp.md) — multiple cores.** A design/research note on how microkernels
|
- **[smp.md](smp.md) — multiple cores.** A design/research note on how microkernels
|
||||||
(L4, seL4) handle SMP — big kernel lock vs per-CPU vs multikernel — and how the
|
(L4, seL4) handle SMP — big kernel lock vs per-CPU vs multikernel — and how the
|
||||||
right choice depends on whether danos is chasing real-time or resilience.
|
right choice depends on whether danos is chasing real-time or resilience.
|
||||||
@@ -136,7 +177,7 @@ addressed as **`system/services/init`** — the repeated leaf resolves away:
|
|||||||
| Source (root file) | Addressed as (module / binary / FHS path) |
|
| Source (root file) | Addressed as (module / binary / FHS path) |
|
||||||
|----------------------------------------|--------------------------------------------|
|
|----------------------------------------|--------------------------------------------|
|
||||||
| `system/services/init/init.zig` | `system/services/init` → `/system/services/init` |
|
| `system/services/init/init.zig` | `system/services/init` → `/system/services/init` |
|
||||||
| `system/drivers/hpet/hpet.zig` | `system/drivers/hpet` → `/system/drivers/hpet` |
|
| `system/drivers/ps2-bus/ps2-bus.zig` | `system/drivers/ps2-bus` → `/system/drivers/ps2-bus` |
|
||||||
| `library/runtime/runtime.zig` | `library/runtime` (the `runtime` module) |
|
| `library/runtime/runtime.zig` | `library/runtime` (the `runtime` module) |
|
||||||
|
|
||||||
In **source**, a sub-project is a directory so it can hold many files — the entry is
|
In **source**, a sub-project is a directory so it can hold many files — the entry is
|
||||||
@@ -161,21 +202,22 @@ system/ → /system danos's own internals (the self-representation)
|
|||||||
services/ init/ vfs/ device-manager/ system servers → /system/services (vfs/ holds
|
services/ init/ vfs/ device-manager/ system servers → /system/services (vfs/ holds
|
||||||
vfs.zig, vfs-test.zig, protocol.zig)
|
vfs.zig, vfs-test.zig, protocol.zig)
|
||||||
library/ → /lib libraries, one sub-directory each
|
library/ → /lib libraries, one sub-directory each
|
||||||
runtime/ the danos-native runtime — the stable application ABI
|
runtime/ the danos-native runtime + file API (fs) — the stable application ABI
|
||||||
posix/ POSIX/C compatibility, layered over runtime
|
|
||||||
boot/ → /boot the loaders
|
boot/ → /boot the loaders
|
||||||
tools/ test/ host-side build + QEMU test harness
|
tools/ test/ host-side build + QEMU test harness
|
||||||
```
|
```
|
||||||
|
|
||||||
A sub-project exposes its **public interface as a module**: `system/services/vfs/` owns
|
A sub-project exposes its **public interface as a module**: `system/services/vfs/` owns
|
||||||
the VFS wire protocol (`protocol.zig`, the `vfs-protocol` module), which the POSIX
|
the VFS wire protocol (`protocol.zig`, the `vfs-protocol` module), which the runtime's
|
||||||
layer imports by name. `usb`/`block` drivers will expose their protocols the same way.
|
file API (`runtime.fs`) imports by name. `usb`/`block` drivers expose their protocols the
|
||||||
|
same way.
|
||||||
|
|
||||||
`library/posix/` is special: it is the **one place** POSIX/C spellings are allowed
|
There is **no POSIX/C compatibility layer today**: danos programs do file I/O through the
|
||||||
verbatim (`stat`, `O_CREAT`, `fopen`, `errno`). Everywhere else follows the danos
|
danos-native `runtime.fs` (open/read/write/list over the VFS). A hand-rolled POSIX shim
|
||||||
naming rule with no exception — see [coding-standards.md](coding-standards.md). The
|
(`library/posix/`) was retired as premature — the real POSIX/C surface will come later
|
||||||
POSIX layer calls the runtime, never the kernel's system calls directly, so it never
|
from the `std.os.danos` seam (and, eventually, musl) when danos becomes a Zig target (see
|
||||||
appears in the private-ABI path.
|
[zig-self-hosting.md](zig-self-hosting.md)). When it does, the foreign-ABI naming
|
||||||
|
exception in [coding-standards.md](coding-standards.md) applies to that seam.
|
||||||
|
|
||||||
## Source map
|
## Source map
|
||||||
|
|
||||||
@@ -199,9 +241,8 @@ appears in the private-ABI path.
|
|||||||
| Framebuffer text console (mirrors to serial) | `system/kernel/console.zig` |
|
| Framebuffer text console (mirrors to serial) | `system/kernel/console.zig` |
|
||||||
| In-kernel test cases | `system/kernel/tests.zig` |
|
| In-kernel test cases | `system/kernel/tests.zig` |
|
||||||
| Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/IO-APIC/timer, serial, linker script) | `system/kernel/architecture/x86_64/` |
|
| Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/IO-APIC/timer, serial, linker script) | `system/kernel/architecture/x86_64/` |
|
||||||
| danos-native runtime (`runtime`): syscall wrappers, heap, IPC, device access — the stable application ABI | `library/runtime/` |
|
| danos-native runtime (`runtime`): syscall wrappers, heap, IPC, device access, the file API (`fs`) — the stable application ABI | `library/runtime/` |
|
||||||
| POSIX/C compatibility (`posix`): unistd, stdio — the one place POSIX names are allowed | `library/posix/` |
|
|
||||||
| System services (init, the VFS server + `protocol`, the device-manager) | `system/services/` |
|
| System services (init, the VFS server + `protocol`, the device-manager) | `system/services/` |
|
||||||
| Device drivers, one sub-project each (`hpet` leaf driver, `bus` bus driver) | `system/drivers/` |
|
| Device drivers, one sub-project each (`pci-bus`, `ps2-bus`, `usb-xhci-bus` bus drivers) | `system/drivers/` |
|
||||||
| Build + `run-x86-64` (QEMU/OVMF) | `build.zig` |
|
| Build + `run-x86-64` (QEMU/OVMF) | `build.zig` |
|
||||||
| QEMU integration test harness | `test/qemu_test.py` |
|
| QEMU integration test harness | `test/qemu_test.py` |
|
||||||
|
|||||||
+55
-1
@@ -107,12 +107,66 @@ firmware-agnostic [device model](discovery.md) gets populated; this note stops a
|
|||||||
part that answers "where are the tables?" — everything past the RSDP is just following
|
part that answers "where are the tables?" — everything past the RSDP is just following
|
||||||
more pointers the tables themselves provide.
|
more pointers the tables themselves provide.
|
||||||
|
|
||||||
|
## ACPI events: the SCI, the power button, and GPEs (M21)
|
||||||
|
|
||||||
|
The tables above are static description; ACPI is also a *live* channel. Hardware
|
||||||
|
raises the **SCI** (System Control Interrupt) — one shared, level-triggered line
|
||||||
|
whose vector the FADT names — and the OS reads status registers to learn what
|
||||||
|
happened: a fixed event like the power button, or a **General-Purpose Event**
|
||||||
|
(GPE) whose handler is an AML method. Since [discovery](discovery.md) moved AML
|
||||||
|
to ring 3, the event side lives there too, in the same **acpi service** — the
|
||||||
|
device discoverer and the event source are one process, because both need the
|
||||||
|
namespace and the port grant.
|
||||||
|
|
||||||
|
**The kernel hands the service what it needs and no more.** Reading PM1 event
|
||||||
|
blocks and GPE blocks requires the FADT, which the kernel already parses for its
|
||||||
|
own `\_S5` poweroff. Rather than re-parse, the kernel appends the **FADT as one
|
||||||
|
more memory resource** on the `acpi-tables` node; the service tells it apart
|
||||||
|
from the AML blob resources by signature — the FADT keeps its intact `"FACP"`
|
||||||
|
header, while the blob resources are header-stripped bytecode that starts with
|
||||||
|
no signature. The kernel's own FADT parse is untouched; the service reads the
|
||||||
|
PM1 *event* blocks (which the kernel never parsed — it only needs PM1 *control*
|
||||||
|
for `\_S5`) and the GPE0/GPE1 blocks straight from its copy. The **SCI itself**
|
||||||
|
arrives as the node's one `len == 1` irq resource (distinct from the broad
|
||||||
|
`[0, 256)` window that covers children's legacy lines), which is how the service
|
||||||
|
finds the line to `irq_bind`.
|
||||||
|
|
||||||
|
With those in hand the service enables ACPI mode (only if `SCI_EN` is clear —
|
||||||
|
some firmwares boot with it already set), sets `PWRBTN_EN`, and on each SCI:
|
||||||
|
|
||||||
|
- **The power button** is a *fixed* event: a set `PWRBTN_STS` bit in PM1 status.
|
||||||
|
The handler clears it (write-1-to-clear), logs the press, and publishes a
|
||||||
|
[`power`](power.md) `power_button` event to subscribers.
|
||||||
|
- **GPEs** are the general path: for each set-and-enabled GPE bit `n`, the
|
||||||
|
service evaluates its `\_GPE._L%02X` (level) or `_E%02X` (edge) handler
|
||||||
|
method, drains the **Notify** queue that method produced, maps each notified
|
||||||
|
device to an event (battery, AC, lid, or a generic `notify` with its code),
|
||||||
|
and clears the status bit. A missing handler method is clear-and-log, not an
|
||||||
|
error. Making GPEs work required teaching the interpreter one opcode it never
|
||||||
|
handled — `Notify` (`0x86`) — which it now folds into a bounded queue drained
|
||||||
|
per evaluation; everything else a handler needs (field access, control flow,
|
||||||
|
method calls) was already proven by the ring-3 `_STA`/`_CRS` work.
|
||||||
|
|
||||||
|
**How this is tested.** QEMU cannot raise GPEs deterministically on this config,
|
||||||
|
so GPE/Notify correctness is proven by **host unit tests** — hand-encoded AML
|
||||||
|
with a `Notify` inside a method body, run under `zig build test`. The QEMU
|
||||||
|
`power-button` scenario proves the fixed-event path end to end: a QMP
|
||||||
|
`system_powerdown` injects a real ACPI power-button press, and the service's SCI
|
||||||
|
handler must log it. Battery/AC/lid and the embedded controller's `_Qxx` queries
|
||||||
|
are interface-complete but validated on real hardware later.
|
||||||
|
|
||||||
|
The service surface these events are *published on* — subscription, the event
|
||||||
|
vocabulary, and orderly shutdown — is the power service, [power.md](power.md).
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [efi.md](efi.md) — the loader that captures the RSDP before `ExitBootServices`.
|
- [efi.md](efi.md) — the loader that captures the RSDP before `ExitBootServices`.
|
||||||
- [memory-map.md](memory-map.md) — the same loader-captures / kernel-consumes seam, and
|
- [memory-map.md](memory-map.md) — the same loader-captures / kernel-consumes seam, and
|
||||||
the ACPI-reclaim memory the RSDP lives in.
|
the ACPI-reclaim memory the RSDP lives in.
|
||||||
- [discovery.md](discovery.md) — the broader (still-evolving) plan for turning these
|
- [discovery.md](discovery.md) — the broader (still-evolving) plan for turning these
|
||||||
tables into one neutral device model shared with the ARM device-tree path.
|
tables into one neutral device model shared with the ARM device-tree path, and how
|
||||||
|
ACPI enumeration and events moved to the ring-3 acpi service.
|
||||||
|
- [power.md](power.md) — the domain-named power service the ACPI event side publishes
|
||||||
|
to (button, lid, battery) and its orderly-shutdown path into S5.
|
||||||
- [arch.md](arch.md) — why the kernel reaches the device code through a `platform`
|
- [arch.md](arch.md) — why the kernel reaches the device code through a `platform`
|
||||||
module and never names ACPI directly.
|
module and never names ACPI directly.
|
||||||
|
|||||||
+56
-11
@@ -65,15 +65,18 @@ Three, and only three.
|
|||||||
`errno`, `O_CREAT`. We don't get to rename `fwrite` to `fileWrite` — it wouldn't be
|
`errno`, `O_CREAT`. We don't get to rename `fwrite` to `fileWrite` — it wouldn't be
|
||||||
`fwrite` any more.
|
`fwrite` any more.
|
||||||
|
|
||||||
**This exception is scoped to one place: `library/posix/`.** A file under
|
**This exception is scoped to a file that *is* a foreign ABI, and nothing else.**
|
||||||
`library/posix/` *is* the foreign ABI, so it keeps the ABI's spellings — that is the
|
danos has no such file today: the old `library/posix/` compatibility shim was retired
|
||||||
whole rule for that directory. **Everywhere else, Zig/danos naming applies with no
|
once its callers moved to the danos-native `runtime.fs`, since a hand-rolled POSIX
|
||||||
POSIX exception**, so there is nothing to get wrong: if you're not in
|
layer is premature until danos actually needs it (see
|
||||||
`library/posix/`, expand it. A concept POSIX also has gets a danos name outside that
|
[zig-self-hosting.md](zig-self-hosting.md)). The exception will apply again to the
|
||||||
layer — the VFS wire protocol carries a `FileStatus`, not a `Stat`, and a `create`
|
`std.os.danos` seam when danos becomes a real Zig target — that module *is* the C-ABI
|
||||||
flag, not `O_CREAT`; `library/posix/` is what maps `stat`→`status` and
|
`system` interface, so it keeps `open`/`read`/`errno`/`O_CREAT`. **Everywhere else,
|
||||||
`O_CREAT`→`create` at the boundary. (The `syscall` *wrappers* elsewhere are not an
|
Zig/danos naming applies with no exception**: a concept POSIX also has gets a danos
|
||||||
exception to this — they wrap the private danos ABI, so they use danos names.)
|
name — the VFS wire protocol carries a `FileStatus`, not a `Stat`, and a `create`
|
||||||
|
flag, not `O_CREAT`; the boundary is where `stat`→`status` and `O_CREAT`→`create` get
|
||||||
|
mapped. (The `syscall` *wrappers* elsewhere are not an exception — they wrap the
|
||||||
|
private danos ABI, so they use danos names.)
|
||||||
|
|
||||||
2. **Zig idioms are spelled the way Zig spells them.** Three names are the language's,
|
2. **Zig idioms are spelled the way Zig spells them.** Three names are the language's,
|
||||||
not ours, and are left alone:
|
not ours, and are left alone:
|
||||||
@@ -96,7 +99,7 @@ That's all — no Unix-abbreviation exception. The source directories are full w
|
|||||||
(`system`, `library`, not `src`/`lib`), and there is no daemon `d` suffix: a driver
|
(`system`, `library`, not `src`/`lib`), and there is no daemon `d` suffix: a driver
|
||||||
lives in `system/drivers/` and a service in `system/services/`, so the *location*
|
lives in `system/drivers/` and a service in `system/services/`, so the *location*
|
||||||
already says what it is. Encoding the role in the name too (`busd`, `vfsd`) is
|
already says what it is. Encoding the role in the name too (`busd`, `vfsd`) is
|
||||||
redundant — the program is just `bus`, `vfs`. Don't put in a name what its directory
|
redundant — the program is just `ps2-bus`, `vfs`. Don't put in a name what its directory
|
||||||
already tells you.
|
already tells you.
|
||||||
|
|
||||||
## A note on collisions
|
## A note on collisions
|
||||||
@@ -138,10 +141,36 @@ single word or acronym needs no hyphen: `scheduler.zig`, `paging.zig`, `apic.zig
|
|||||||
conventions above — `snake_case` — because it's an identifier, not a filename.)
|
conventions above — `snake_case` — because it's an identifier, not a filename.)
|
||||||
|
|
||||||
**A sub-project's entry point repeats its directory's name** — `init/init.zig`,
|
**A sub-project's entry point repeats its directory's name** — `init/init.zig`,
|
||||||
`runtime/runtime.zig`, `hpet/hpet.zig` — and the sub-project is addressed by the
|
`runtime/runtime.zig`, `ps2-bus/ps2-bus.zig` — and the sub-project is addressed by the
|
||||||
*directory* (`system/services/init`, `library/runtime`), with the repeated leaf
|
*directory* (`system/services/init`, `library/runtime`), with the repeated leaf
|
||||||
resolving away. See the repository-layout section of [README.md](README.md).
|
resolving away. See the repository-layout section of [README.md](README.md).
|
||||||
|
|
||||||
|
## Named values, not magic numbers
|
||||||
|
|
||||||
|
The naming rule has a twin: **a value with meaning gets a name, too.** The same
|
||||||
|
principle drives both — a reader should never have to leave the code to understand it.
|
||||||
|
An abbreviated *name* forces a reader to guess; a bare *number* forces them worse, out
|
||||||
|
to a spec or a header or a comment three files away, to learn what the value even *is*.
|
||||||
|
If `0x0C` is the PCI serial-bus class, the code says `BaseClass.serial_bus`, not `0x0C`;
|
||||||
|
if `0x04` is the ACPI IRQ resource descriptor, it says `SmallResourceType.irq`, not
|
||||||
|
`0x04`. The number is an implementation detail of the name — recorded once, where the
|
||||||
|
name is defined, and never spelled again at a use site.
|
||||||
|
|
||||||
|
**Prefer an `enum`** when the values form a set (device classes, AML opcodes, resource
|
||||||
|
descriptor types, states): the type then also says *which* set a value belongs to, and
|
||||||
|
the compiler rejects a value from the wrong one. A lone `pub const` with a descriptive
|
||||||
|
name suffices for a one-off (`const large_descriptor_bit = 0x80`). Reach for the enum
|
||||||
|
the moment code elsewhere compares against, packs, or produces the value — a packed PCI
|
||||||
|
class triple is written from named parts (`.serial_bus`, `.usb`, `.xhci`), never as
|
||||||
|
`0x0C_03_30` under a comment that decodes the bytes.
|
||||||
|
|
||||||
|
The exceptions are the numbers that carry no hidden meaning: `0` and `1` as plain zero
|
||||||
|
and one, an index step, a field width, a bit shift. `x + 1`, `buffer[0]`, and `<< 8`
|
||||||
|
need no christening — there is nothing to look up. The test is exactly the naming test:
|
||||||
|
*would a reader have to look this up to know what it means?* If yes, name it. This is
|
||||||
|
what `opcodes.zig`'s `*_opcode` constants, `acpi-ids`'s `HardwareId`, and `pci-class`'s
|
||||||
|
class enums already are — reference data defined once and named everywhere it is used.
|
||||||
|
|
||||||
## Why acronyms are the line
|
## Why acronyms are the line
|
||||||
|
|
||||||
Because an acronym has no letters to restore. `MMIO` doesn't become "memory mapped
|
Because an acronym has no letters to restore. `MMIO` doesn't become "memory mapped
|
||||||
@@ -150,3 +179,19 @@ input output" in code — that expansion is what the acronym *is for*. But `msg`
|
|||||||
test for "is this an abbreviation I must expand" is simply: *is there a longer word this
|
test for "is this an abbreviation I must expand" is simply: *is there a longer word this
|
||||||
is a clipped form of?* If yes, write the word. If it's an initialism standing in for a
|
is a clipped form of?* If yes, write the word. If it's an initialism standing in for a
|
||||||
phrase, leave it.
|
phrase, leave it.
|
||||||
|
|
||||||
|
## Zen of Zig
|
||||||
|
|
||||||
|
* Communicate intent precisely.
|
||||||
|
* Edge cases matter.
|
||||||
|
* Favor reading code over writing code.
|
||||||
|
* Only one obvious way to do things.
|
||||||
|
* Runtime crashes are better than bugs.
|
||||||
|
* Compile errors are better than runtime crashes.
|
||||||
|
* Incremental improvements.
|
||||||
|
* Avoid local maximums.
|
||||||
|
* Reduce the amount one must remember.
|
||||||
|
* Focus on code rather than style.
|
||||||
|
* Resource allocation may fail; resource deallocation must succeed.
|
||||||
|
* Memory is a resource.
|
||||||
|
* Together we serve the users.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Most modern Unix and Unix-like operating systems follow the FHS. DanOS has its o
|
|||||||
| /srv | Site-specific data served by this system, such as data and scripts for web servers, data offered by FTP servers, and repositories for version control systems |
|
| /srv | Site-specific data served by this system, such as data and scripts for web servers, data offered by FTP servers, and repositories for version control systems |
|
||||||
| /system | DanOS operating system files (similar idea to C:\Windows). A true representation of danos — its layout mirrors the source tree, so `/system` is what danos *is*. |
|
| /system | DanOS operating system files (similar idea to C:\Windows). A true representation of danos — its layout mirrors the source tree, so `/system` is what danos *is*. |
|
||||||
| /system/devices | danos virtual device tree e.g. similar to /sys on linux but with danos device tree conventions (the structures in the devices module) |
|
| /system/devices | danos virtual device tree e.g. similar to /sys on linux but with danos device tree conventions (the structures in the devices module) |
|
||||||
| /system/drivers | driver binaries, one sub-project each (e.g. /system/drivers/hpet) |
|
| /system/drivers | driver binaries, one sub-project each (e.g. /system/drivers/pci-bus, /system/drivers/ps2-bus) |
|
||||||
| /system/services | system-service binaries — the VFS server, init, and other user-mode servers (e.g. /system/services/vfs, /system/services/init) |
|
| /system/services | system-service binaries — the VFS server, init, and other user-mode servers (e.g. /system/services/vfs, /system/services/init) |
|
||||||
| /system/kernel | the kernel image |
|
| /system/kernel | the kernel image |
|
||||||
| /tmp | Directory for temporary files (see also /var/tmp). Often not preserved between system reboots and may be severely size-restricted. |
|
| /tmp | Directory for temporary files (see also /var/tmp). Often not preserved between system reboots and may be severely size-restricted. |
|
||||||
@@ -61,8 +61,8 @@ to the driver in the order written, and a read consumes what is there. Terminals
|
|||||||
serial lines, keyboards and mice are all of this shape. These are the natural first
|
serial lines, keyboards and mice are all of this shape. These are the natural first
|
||||||
device nodes in danos, because a character driver needs nothing the kernel doesn't
|
device nodes in danos, because a character driver needs nothing the kernel doesn't
|
||||||
already provide — it claims its device, maps its registers with `mmio_map`, and blocks
|
already provide — it claims its device, maps its registers with `mmio_map`, and blocks
|
||||||
on `replyWait` for either an interrupt or a client request. `system/drivers/hpet/hpet.zig` is already
|
on `replyWait` for either an interrupt or a client request. `system/drivers/ps2-bus/ps2-bus.zig`
|
||||||
that program, minus the client half.
|
is already that program, minus the file-node client half.
|
||||||
|
|
||||||
The obstacle was never the file type; it is which hardware a ring-3 driver can reach.
|
The obstacle was never the file type; it is which hardware a ring-3 driver can reach.
|
||||||
Direct `in`/`out` from user space is still a #GP (no TSS I/O bitmap, IOPL never raised),
|
Direct `in`/`out` from user space is still a #GP (no TSS I/O bitmap, IOPL never raised),
|
||||||
|
|||||||
@@ -78,6 +78,40 @@ preemption and wakeups (1 ms granularity); the **TSC** is the resolution you rea
|
|||||||
time at. Making `sleep` itself sub-millisecond would take a tickless one-shot
|
time at. Making `sleep` itself sub-millisecond would take a tickless one-shot
|
||||||
timer — a later step.
|
timer — a later step.
|
||||||
|
|
||||||
|
### Is the TSC trustworthy? Invariant, and synchronized
|
||||||
|
|
||||||
|
A cycle counter is only a valid *clock* if two things hold, and danos checks both,
|
||||||
|
because they decide whether we read time with a cheap `rdtsc` or fall back to the HPET.
|
||||||
|
|
||||||
|
**Invariant.** An old TSC counted core clock cycles, so it sped up and slowed down with
|
||||||
|
frequency scaling — useless as wall time. Modern CPUs (all of danos's targets) provide an
|
||||||
|
**invariant TSC**: a constant rate across P/C-states that never stops. The guarantee is a
|
||||||
|
CPUID bit — leaf `0x80000007`, EDX bit 8 — on both Intel *and* AMD. danos reads it in
|
||||||
|
`calibrate`, and a TSC that doesn't advertise it is not used as the clocksource. AMD is
|
||||||
|
why this matters in practice: it doesn't populate the Intel leaf `0x15` that enumerates
|
||||||
|
the TSC *frequency*, so danos already measures AMD's rate against the HPET — but a
|
||||||
|
measured frequency without the invariance guarantee is not enough.
|
||||||
|
|
||||||
|
**Synchronized.** Each core has its own TSC. Even invariant ones can start at different
|
||||||
|
values (a second socket, some firmware), so a thread migrating from a core reading
|
||||||
|
`1_000_000` to one reading `999_000` would see time jump *backward*. danos runs a **warp
|
||||||
|
check** as each application processor comes online (`checkWarpSource`, adapted from
|
||||||
|
Linux's): the waking core and the BSP hammer a shared "highest seen" TSC under a lock,
|
||||||
|
and if either ever reads below it, the cores' TSCs are skewed. It's pairwise because APs
|
||||||
|
come up one at a time ([smp.md](smp.md)).
|
||||||
|
|
||||||
|
**The fallback.** When the TSC fails either test — non-invariant (a bare VM such as the
|
||||||
|
default qemu64), or warped between cores — danos moves the monotonic clock onto the
|
||||||
|
**HPET** main counter: one fixed-rate counter, so it can neither skew between cores nor
|
||||||
|
drift with frequency. It costs a memory-mapped read instead of a register read, but it
|
||||||
|
keeps time *accurate*, which is the whole point. The switch preserves the current value,
|
||||||
|
so the clock never jumps. The boot log names the outcome:
|
||||||
|
|
||||||
|
```
|
||||||
|
/system/kernel: clocksource tsc (TSC invariant: yes, synchronized: yes) # real Intel/AMD
|
||||||
|
/system/kernel: clocksource hpet (TSC invariant: no, synchronized: yes) # a bare VM (TCG)
|
||||||
|
```
|
||||||
|
|
||||||
## Two kinds of vector, one dispatch
|
## Two kinds of vector, one dispatch
|
||||||
|
|
||||||
The IDT now installs gates `0-47`: the 32 exceptions plus the device range. Every
|
The IDT now installs gates `0-47`: the 32 exceptions plus the device range. Every
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# The device manager
|
||||||
|
|
||||||
|
**Status: the protocol and supervision are built** (M18.1, 2026-07-13): `hello`
|
||||||
|
with its deadline, supervised spawn, restart with backoff, and the crash-loop
|
||||||
|
cap are in — usb-xhci-bus is the first conforming driver, and the
|
||||||
|
`driver-restart` scenario proves fault → backoff → re-claim → cap end to end.
|
||||||
|
Tree reports are built too (M18.2, 2026-07-13): the xHCI driver scans its
|
||||||
|
root-hub ports and reports each connected device (`child_added`); the manager
|
||||||
|
mirrors them and prunes a dead reporter's children, and the `usb-report`
|
||||||
|
scenario proves report → prune → respawn → re-report. The application surface is built (M18.3, 2026-07-13):
|
||||||
|
`enumerate` and `subscribe` over IPC, with `device-list` as the first client —
|
||||||
|
the manager is now the one answer to "what devices exist" for applications.
|
||||||
|
The primitives underneath are real ([process-management.md](process-management.md):
|
||||||
|
spawn/supervise/kill/exit-notification; [driver-model.md](driver-model.md): the device
|
||||||
|
table as a capability system; [drivers.md](drivers.md): claim/map/IRQ), and the first
|
||||||
|
per-device driver spawn works (the device manager matches the xHCI controller by PCI
|
||||||
|
class and spawns `usb-xhci-bus` with the device id as argv[1]). This document designs
|
||||||
|
the rest: the device manager as **the tree, the matcher, and the supervisor** — the
|
||||||
|
policy process that turns [resilience.md](resilience.md)'s restart goal into practice
|
||||||
|
for drivers.
|
||||||
|
|
||||||
|
How processes stop, reload, and report their deaths is deliberately **not** in this
|
||||||
|
document: that is the universal lifecycle every danos process speaks —
|
||||||
|
[process-lifecycle.md](process-lifecycle.md), signals over IPC and the stable
|
||||||
|
`runtime.process` interface. The device manager is that design's first serious
|
||||||
|
customer, not its owner. Its own protocol contains nothing lifecycle-shaped; a
|
||||||
|
driver is stopped, health-checked, and buried exactly like any other process.
|
||||||
|
|
||||||
|
## The tree: structure in the manager, authority in the kernel
|
||||||
|
|
||||||
|
The device tree is two things fused: *information* (what exists, how it nests) and
|
||||||
|
*authority* (a descriptor is a licence to map physical memory). They separate:
|
||||||
|
|
||||||
|
- The **kernel keeps the capability system** — device, I/O-port, and interrupt
|
||||||
|
claims, resource containment on `device_register`, the
|
||||||
|
`mmio_map`/`irq_bind`/`msi_bind` gates — and **cleans all of it up when a process
|
||||||
|
dies** (settled; it is increment 1 of
|
||||||
|
[process-lifecycle.md](process-lifecycle.md)). The three invariants in
|
||||||
|
[driver-model.md](driver-model.md) stay exactly where they are. A device manager
|
||||||
|
that could mint MMIO mappings by its own say-so would be a second kernel, and a
|
||||||
|
buggy one would un-earn everything the microkernel bought.
|
||||||
|
- The **device manager owns the tree as data** — identity, topology, naming, driver
|
||||||
|
matching, hotplug events, and being the one process everything else asks about
|
||||||
|
devices. Firmware discovery seeds it (today via the kernel's snapshot); **bus
|
||||||
|
drivers grow it** by reporting what they see; applications query and watch it.
|
||||||
|
`device_enumerate` fades to a manager-internal (then deleted) seam.
|
||||||
|
|
||||||
|
Long-term, discovery itself leaves the kernel — but not *into* the manager. PCI
|
||||||
|
enumeration is a **pci-bus driver**: the manager spawns it against the host bridge
|
||||||
|
(already a device with the ECAM window as a resource), it scans, it reports functions
|
||||||
|
like any bus reports children. ACPI becomes an **acpi service** that interprets the
|
||||||
|
tables and reports the namespace. The manager only orchestrates and merges. Moving
|
||||||
|
AML interpretation out of ring 0 is its own project on its own track; nothing here
|
||||||
|
depends on when it lands. (It landed: [discovery.md](discovery.md), M19–M20.)
|
||||||
|
|
||||||
|
`device_register` is **idempotent on exact match**: a re-registration with an
|
||||||
|
identical (parent, class, identity, resources) tuple returns the existing id
|
||||||
|
instead of appending a duplicate. The kernel table has no unregister, so without
|
||||||
|
this a restarted registering bus would re-report its children as fresh nodes on
|
||||||
|
every respawn. Idempotence is what makes restart-and-re-report sound for *every*
|
||||||
|
reporting bus — pci-bus, the acpi service, a future fdt service — not just one,
|
||||||
|
and it is why supervision (below) can prune a dead bus's subtree and trust the
|
||||||
|
restarted instance to rebuild exactly the same ids.
|
||||||
|
|
||||||
|
## The protocol
|
||||||
|
|
||||||
|
A `device-manager-protocol` module (the vfs-protocol pattern): extern-struct
|
||||||
|
messages, a version in the handshake, reserved fields everywhere. The manager is a
|
||||||
|
well-known endpoint (`ipc.register(.device_manager)`); the badge tells it who is
|
||||||
|
talking; the same endpoint receives its children's exit notifications — one loop,
|
||||||
|
one world.
|
||||||
|
|
||||||
|
| Direction | Message | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| driver → manager | `hello { version, role, device_id }` | confirms the argv assignment, starts the deadline clock |
|
||||||
|
| bus → manager | `child_added { parent, identity, resources }` | one node the bus discovered |
|
||||||
|
| bus → manager | `child_removed { id }` | unplug, or the bus lost it |
|
||||||
|
| app → manager | `enumerate` | snapshot of the tree (read-only) |
|
||||||
|
| app → manager | `subscribe` | receive published add/remove events |
|
||||||
|
|
||||||
|
`hello` is the one deadline the manager enforces itself: spawned and silent past the
|
||||||
|
deadline means wrong binary, wrong protocol version, or wedged before main — apply
|
||||||
|
the stop sequence and the restart policy. Everything else lifecycle-shaped
|
||||||
|
(terminate, the common `ping` liveness call, exit reasons) arrives through
|
||||||
|
[process-lifecycle.md](process-lifecycle.md)'s vocabulary, not this protocol.
|
||||||
|
|
||||||
|
Assignment stays argv (`usb-xhci-bus <device id>`) for now — simple, and it works.
|
||||||
|
The step after `hello` exists is delegation: the manager claims (or is granted) the
|
||||||
|
devices and passes the claim to the driver over IPC (the M13 capability-transfer
|
||||||
|
mechanism), replacing first-come-first-served `device_claim` with policy. Identity in
|
||||||
|
`child_added` is per-bus: PCI children carry the class triple (`pci_class`, as the
|
||||||
|
xHCI match already uses); USB children carry the (class, subclass, protocol) triple
|
||||||
|
from usb-ids.zig — each bus's native language, decoded by the shared ids modules.
|
||||||
|
|
||||||
|
## Supervision and restart
|
||||||
|
|
||||||
|
Every driver is spawned with the manager's exit endpoint (`spawnSupervised` — built).
|
||||||
|
On a death notification:
|
||||||
|
|
||||||
|
1. **Read the reason** ([process-lifecycle.md](process-lifecycle.md) increment 2).
|
||||||
|
Clean exit → it meant to; don't restart. Fault or missed `hello` deadline →
|
||||||
|
restart with **backoff**, and a crash-loop cap (three fast deaths → mark failed,
|
||||||
|
stop respawning, log loudly; a later `reload` to the manager can retry).
|
||||||
|
2. **Prune the subtree** the dead bus driver reported. Its children describe
|
||||||
|
protocol state (xHCI slot ids, transfer rings) that died with the process;
|
||||||
|
keeping the nodes would be keeping a lie. Watchers receive `child_removed` — the
|
||||||
|
input service losing, then regaining, a keyboard is the *honest* description of
|
||||||
|
what happened. The restarted instance rediscovers and re-reports.
|
||||||
|
3. **The claim is already free** because the kernel released it at death — the
|
||||||
|
restarted instance claims the same controller and comes up.
|
||||||
|
|
||||||
|
Who supervises the supervisor: **init** (PID 1), which already supervises the
|
||||||
|
services it starts. If the manager dies, drivers keep running (they hold their
|
||||||
|
claims; the kernel doesn't care who their supervisor was — though their exit
|
||||||
|
notifications now dangle harmlessly). The restarted manager re-learns the world:
|
||||||
|
kernel snapshot, then a re-`hello` round — drivers answer a broadcast or are stopped
|
||||||
|
and respawned. Full state handoff is deliberately not attempted.
|
||||||
|
|
||||||
|
## Thin drivers, class protocols
|
||||||
|
|
||||||
|
The [driver-model.md](driver-model.md) three-shape split, restated as processes:
|
||||||
|
|
||||||
|
- A **bus driver** (usb-xhci-bus) owns its controller — claim, MMIO, IRQ/MSI, DMA
|
||||||
|
rings — and offers a *transfer* protocol ("submit a control transfer to device N",
|
||||||
|
built from the usb-abi request constructors) plus tree reports to the manager.
|
||||||
|
- A **class driver** (usb-hid, usb-storage) owns nothing: it is matched to a reported
|
||||||
|
child by its identity triple, speaks the bus's transfer protocol downward and its
|
||||||
|
service's protocol upward — HID reports to the input service, blocks to the block
|
||||||
|
service. It works unchanged over any controller.
|
||||||
|
- **Services** (input, display, block) aggregate class drivers and face applications.
|
||||||
|
|
||||||
|
Each arrow is a protocol module. The manager routes none of the data plane — it
|
||||||
|
introduces the parties (matching), supervises them (lifecycle), and gets out of the
|
||||||
|
way.
|
||||||
|
|
||||||
|
## Increments
|
||||||
|
|
||||||
|
Increments 1–4 are the lifecycle prerequisites and live in
|
||||||
|
[process-lifecycle.md](process-lifecycle.md) (claim cleanup on death, exit reasons,
|
||||||
|
published exit events, signals + `runtime.process`). On top of those:
|
||||||
|
|
||||||
|
5. **device-manager-protocol**: `hello`, supervised spawn with restart policy;
|
||||||
|
usb-xhci-bus becomes the first conforming driver.
|
||||||
|
6. **Tree reports**: `child_added`/`child_removed`; the manager mirrors; xHCI reports
|
||||||
|
the mouse and keyboard QEMU already hangs off it.
|
||||||
|
7. **App surface**: `enumerate`/`subscribe` over IPC; `device_enumerate` retreats
|
||||||
|
to a manager-internal seam.
|
||||||
|
8. **Discovery migration** — DONE (M19–M20, 2026-07-13): enumeration moved to
|
||||||
|
ring 3 as swappable per-firmware discoverers — the pci-bus driver (M19) then
|
||||||
|
the acpi service (M20), see [discovery.md](discovery.md); the kernel seeds
|
||||||
|
only the host bridge and the acpi-tables node. Matching moved with it:
|
||||||
|
`child_added` grew a `device_id` (the kernel-registered id, `no_device` for
|
||||||
|
unregistered leaves like USB ports) and a firmware `hid`, and the manager now
|
||||||
|
matches drivers from those **reports** rather than its boot-time snapshot. The
|
||||||
|
PCI arm flipped in M19.3, the ACPI arm (ps2-bus matched from `_HID`) in M20.3
|
||||||
|
— each in a single phase so no device is ever matched from both sources at
|
||||||
|
once. The acpi service reports only the non-PCI `_HID` devices, since pci-bus
|
||||||
|
already reports PCI functions (M20.2).
|
||||||
|
|
||||||
|
## Settled questions (2026-07-12)
|
||||||
|
|
||||||
|
- **Stateful buses**: pruning the subtree on bus-driver death is right for USB. A
|
||||||
|
future storage bus with in-flight writes wants drain-before-terminate — which is
|
||||||
|
exactly the `deadline_ms` parameter `stop()` already has; a per-driver deadline
|
||||||
|
is one value in the manager's policy table when such a bus arrives. No design
|
||||||
|
change.
|
||||||
|
- **Manager death**: drivers survive the manager; the restarted manager re-learns
|
||||||
|
the world (above). Checkpointing driver state with the manager is deferred until
|
||||||
|
something demonstrates the need.
|
||||||
|
- **Matching stays code until the third bus.** `driverFor`/`pciDriverFor` are
|
||||||
|
honest at two bus types; the third triggers the manifest (a driver declares what
|
||||||
|
it binds: a PCI class triple, a USB class triple, an ACPI `_HID`).
|
||||||
@@ -167,3 +167,81 @@ free; discovery on x86 is partly about *finding* what ARM just tells you.
|
|||||||
- [ipc.md](ipc.md) — the channels that interrupts-as-messages and the device manager
|
- [ipc.md](ipc.md) — the channels that interrupts-as-messages and the device manager
|
||||||
will ride on.
|
will ride on.
|
||||||
- [vision.md](vision.md) — why drivers belong in isolated user space at all.
|
- [vision.md](vision.md) — why drivers belong in isolated user space at all.
|
||||||
|
|
||||||
|
## Update (M19.3, 2026-07-13): PCI enumeration left the kernel
|
||||||
|
|
||||||
|
The kernel now seeds only the `pci_host_bridge` node (ECAM window, MMIO
|
||||||
|
apertures derived from the memory map's holes, bus range, and the 16-bit I/O
|
||||||
|
window). The per-function walk moved to the ring-3 `pci-bus` driver
|
||||||
|
([device-manager.md](device-manager.md)): it claims the bridge, repeats the
|
||||||
|
ECAM scan through its mmio grant, and `device_register`s what it finds, which
|
||||||
|
the device manager mirrors and matches. The ACPI namespace walk follows in M20;
|
||||||
|
the static tables (MADT, HPET, MCFG, FADT + `\\_S5`) stay kernel-side.
|
||||||
|
|
||||||
|
## Update (M20.3, 2026-07-13): ACPI enumeration left the kernel too
|
||||||
|
|
||||||
|
The kernel no longer folds the AML namespace's Device objects into the device
|
||||||
|
tree. It still parses the *static* tables (MADT for SMP, HPET for the tick, MCFG
|
||||||
|
for the host bridge, FADT) and still builds the AML namespace — but only to read
|
||||||
|
the `\\_S5` sleep type for poweroff. Device discovery is the ring-3 **acpi
|
||||||
|
service** ([device-manager.md](device-manager.md)): it claims the `acpi-tables`
|
||||||
|
node the kernel publishes (the AML blobs, a broad io_port grant, the SCI),
|
||||||
|
re-parses the same blobs with the shared AML module, evaluates `_STA`/`_CRS`,
|
||||||
|
and registers + reports each `_HID` device — the device manager matches drivers
|
||||||
|
(ps2-bus) from those reports. With M19's pci-bus driver, discovery now runs
|
||||||
|
entirely in user space; the kernel seeds only the host bridge and the
|
||||||
|
acpi-tables node.
|
||||||
|
|
||||||
|
## Discovery is a swappable process per firmware (M19–M20)
|
||||||
|
|
||||||
|
Moving PCI and ACPI enumeration out of ring 0 was not just a relocation — it
|
||||||
|
made discovery **firmware-neutral by construction**, which is the whole reason
|
||||||
|
to do it before the second architecture rather than after. Everything at and
|
||||||
|
above the [device-manager](device-manager.md) protocol — descriptors,
|
||||||
|
containment, reports, matching, supervision — is generic and may never become
|
||||||
|
x86-specific. Discovery is the single firmware-specific piece, and it is
|
||||||
|
isolated as **one swappable process per firmware**:
|
||||||
|
|
||||||
|
- **x86** boots describe hardware with ACPI, so the discoverer is the **acpi
|
||||||
|
service** ([acpi.md](acpi.md)): it claims the `acpi-tables` node and runs AML.
|
||||||
|
- **The Raspberry Pis** hand over a flattened device tree, so the discoverer is
|
||||||
|
an **fdt service**: it claims a `devicetree-blob` node and walks the tree —
|
||||||
|
pure data, no bytecode, so it needs neither a port grant nor an interpreter,
|
||||||
|
strictly simpler than ACPI. (A placeholder until the [aarch64](arm.md)
|
||||||
|
bring-up fills it in.)
|
||||||
|
|
||||||
|
The device manager spawns the discoverer under the **neutral ramdisk name
|
||||||
|
`discovery`** and never learns which firmware it is on; the build's
|
||||||
|
`-Ddiscovery=acpi|fdt` option fills that slot (x86 defaults to `acpi`, the
|
||||||
|
aarch64 target flips the default when it lands). The manager owns the device
|
||||||
|
tree as *data* and touches no hardware, ever — firmware bytecode runs only
|
||||||
|
inside the crashable, supervised discoverer, so an AML fault can never take
|
||||||
|
down the supervisor.
|
||||||
|
|
||||||
|
Two consequences of neutrality bind on later work:
|
||||||
|
|
||||||
|
- **Cross-firmware surfaces are named by domain, not firmware.** System power is
|
||||||
|
a [`power`](power.md) protocol, not an "ACPI events" protocol: on x86 the acpi
|
||||||
|
service registers it, on ARM a PSCI/mailbox service registers the same
|
||||||
|
`ServiceId.power`, and subscribers never learn the difference.
|
||||||
|
- **Identity must widen before the fdt service exists.** `DeviceDescriptor`'s
|
||||||
|
8-byte `hid` holds an EISA id but cannot hold an FDT `compatible` string
|
||||||
|
(`"brcm,bcm2835-aux-uart"`); the identity field grows before the ARM path can
|
||||||
|
report a real node.
|
||||||
|
|
||||||
|
Two supporting decisions keep the kernel's remaining slice honest:
|
||||||
|
|
||||||
|
- **The AML interpreter is a shared build module**, compiled into both the
|
||||||
|
kernel and the acpi service — one source, two builds, no fork. The kernel
|
||||||
|
links it for the `\_S5` poweroff evaluation, the service links it for
|
||||||
|
everything else, and the `acpi-parse` test asserts the two produce the same
|
||||||
|
device count across the ring-3 move.
|
||||||
|
- **Bridge apertures come from the firmware memory map, not AML.** Registered
|
||||||
|
PCI functions carry BAR resources, and `device_register` containment demands
|
||||||
|
the bridge own windows that cover them. Those apertures are derived
|
||||||
|
kernel-side from the boot memory map's MMIO holes (regions that are neither
|
||||||
|
RAM nor tables) — mechanical, AML-free, and available at boot regardless of
|
||||||
|
what later moved to user space. The acpi service's authority is likewise
|
||||||
|
exactly one node: the `acpi-tables` node, whose broad io_port grant is the
|
||||||
|
documented trust boundary for the one process allowed to run firmware
|
||||||
|
bytecode.
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
- [x] `system/services/display/protocol.zig`: `Operation{ info, create_layer,
|
||||||
|
configure_layer, destroy_layer, fill_rect, blit_tile, damage, present }`; `extern`
|
||||||
|
`Request`/`Reply`; size + `maximum_payload` consts. (Model: block/protocol.zig.)
|
||||||
|
- [x] [abi.zig](../system/abi.zig): `ServiceId.display = 9`.
|
||||||
|
- [x] `system/services/display/display.zig`: `main` → enumerate + claim + WC-map the LFB
|
||||||
|
(front) → `mmap` a cacheable back buffer of `height*pitch` → `runtime.service.run`.
|
||||||
|
`info` and a whole-screen `present` (back → front) are live; layer ops fail-stub
|
||||||
|
until D3. Init clears the back buffer and presents it — the double-buffer path.
|
||||||
|
- [x] [library/runtime/display.zig](../library/runtime/runtime.zig) (+ barrel export of
|
||||||
|
`display` and `display_protocol`): `info()` and `present()`, cached `.display`
|
||||||
|
lookup with retry (model: block.zig).
|
||||||
|
- [x] [init.zig](../system/services/init/init.zig): `"display"` added to `boot_services`.
|
||||||
|
- [x] [build.zig](../build.zig): `display-protocol` module on the runtime; `display` exe
|
||||||
|
via `addUserBinary`; packed into the initial-ramdisk; installed to
|
||||||
|
`/system/services/display`.
|
||||||
|
- [x] **Kernel fix the back buffer surfaced:** `mmap` was capped at 256 pages (1 MiB) by
|
||||||
|
a fixed kernel-stack `frames` array. Rewrote `systemMmap` to map page-by-page with
|
||||||
|
rollback (no scratch array) and raised the cap to 8192 pages (32 MiB) — enough for a
|
||||||
|
4K back buffer. A real limitation met, exactly the kind this project chases.
|
||||||
|
|
||||||
|
**Gate (met, automated):** `python3 test/qemu_test.py display-service` spawns the
|
||||||
|
compositor and matches its own serial heartbeats — `display: online {w}x{h} pitch …`
|
||||||
|
followed by `display: presented frame 0` — which it prints only after the whole
|
||||||
|
claim → WC-map → back-buffer → clear → present chain succeeds (matched on serial like the
|
||||||
|
fault cases, since a lone blocking service can't reschedule the in-kernel test context to
|
||||||
|
poll). Regression-checked: `usermem`, `heap` (the `mmap` rewrite), `init` (the boot-list
|
||||||
|
addition), and D1's `display` all still pass.
|
||||||
|
|
||||||
|
## D3 — Layer stack + compositor + damage present ✅
|
||||||
|
|
||||||
|
The heart: composite an ordered layer stack, present only what changed.
|
||||||
|
|
||||||
|
- [x] A layer table (16 slots): each `Layer` = position, z, visible, a server-owned
|
||||||
|
`mmap`'d surface (freed on `destroy_layer`). `damage` accumulates the dirty screen
|
||||||
|
region since the last present.
|
||||||
|
- [x] `create_layer` / `configure_layer` (damages old + new footprints) / `destroy_layer`,
|
||||||
|
`fill_rect`, `blit_tile` (reads the inline tile from the IPC payload, unaligned-safe),
|
||||||
|
`damage`, `present`.
|
||||||
|
- [x] Pure, host-tested [compositor.zig](../system/services/display/compositor.zig): `Rect`
|
||||||
|
(intersect/unite), `Surface`, `fillRect`, `composite` (opaque, clipped to a damage
|
||||||
|
rect), `blitTile`. `present` clears the damaged region to the wallpaper, paints the
|
||||||
|
visible layers bottom-to-top (z-sorted), and flushes just that rect back → front (WC).
|
||||||
|
Colour packing (rgbx/bgrx) is `protocol.pack`, also host-tested.
|
||||||
|
- [x] Host tests (`zig build test`, green): rect intersect/unite, `fillRect` clipping +
|
||||||
|
`stride > width` padding, `composite` overlap-shows-top + damage clipping, `blitTile`
|
||||||
|
unaligned read + clipping, and `pack` for both pixel formats.
|
||||||
|
|
||||||
|
**Gate (met):** `zig build test` green for the compositor + pack unit tests, **and** the
|
||||||
|
`display-service` case's startup self-check composites two overlapping layers on the real
|
||||||
|
framebuffer and reads back the composited pixels — overlap = top layer, outside = bottom
|
||||||
|
layer — logging `display: compositor self-check ok` (matched by the harness).
|
||||||
|
|
||||||
|
## D4 — Client API + the demo client ✅
|
||||||
|
|
||||||
|
Prove the pipeline end-to-end from a separate process.
|
||||||
|
|
||||||
|
- [x] Finished [runtime/display.zig](../library/runtime/runtime.zig): a `Layer` handle with
|
||||||
|
`fill` / `blitTile` (inline tile) / `configure` (move/restack/show) / `damage` /
|
||||||
|
`destroy`, `createLayer`, and a `color(r,g,b)` helper (caches the mode, packs via
|
||||||
|
`protocol.pack`). Coordinates are signed over the wire (`@bitCast` both ways).
|
||||||
|
- [x] `system/services/display-demo/`: a hardware-free client (the `input-source` analog)
|
||||||
|
— a full-screen wallpaper layer, a rectangle that slides back and forth (moved by
|
||||||
|
`configure` each frame, so the compositor repaints old + new), and a cursor layer;
|
||||||
|
presents in a loop paced by `runtime.time`. Wired into build + initial-ramdisk.
|
||||||
|
- [x] **Bug this surfaced:** `protocol.message_maximum` was 4096, but the kernel caps
|
||||||
|
every IPC message at `MESSAGE_MAXIMUM` = 256 — so `replyWait` rejected the oversized
|
||||||
|
receive buffer with `-E2BIG` and the serve loop had been *spinning* since D2 (unseen,
|
||||||
|
as D2/D3 matched init-time heartbeats). Set it to 256; `blit_tile` is now explicitly
|
||||||
|
a small-tile path (≤ 54 px inline), larger bitmaps being the deferred shm surface.
|
||||||
|
|
||||||
|
**Gate (met):** `python3 test/qemu_test.py display-demo` spawns the service + `display-demo`;
|
||||||
|
the demo drives a run of frames of motion through the layer client API and logs
|
||||||
|
`display-demo: ok` (the visible motion is a screenshot via `zig build run-x86-64`).
|
||||||
|
Regression-checked: `zig build test`, `display` (D1), and `display-service` (D2/D3) all
|
||||||
|
still pass, and the default `zig build` is clean.
|
||||||
|
|
||||||
|
## D5 — Test cases + docs ✅
|
||||||
|
|
||||||
|
- [x] The three integration cases exist and pass: `display` (D1 handoff, kernel),
|
||||||
|
`display-service` (D2/D3 compositor + self-check), and `display-demo` (D4 full
|
||||||
|
pipeline: spawn `display` + `display-demo`, match `display-demo: ok`) —
|
||||||
|
[tests.zig](../system/kernel/tests.zig) + [qemu_test.py](../test/qemu_test.py). Plus
|
||||||
|
the pure host tests (`zig build test`).
|
||||||
|
- [x] [display.md](display.md) updated to the built state (the "Verifying it" section names
|
||||||
|
the real cases); [README index](README.md) entry present (#19); the `display-track`
|
||||||
|
memory marked DONE with the commits.
|
||||||
|
|
||||||
|
**Gate (met):** `python3 test/qemu_test.py display display-service display-demo` all pass,
|
||||||
|
`zig build test` is green, and the default `zig build` is clean.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1 status: complete
|
||||||
|
|
||||||
|
D1–D5 done. The display service is a working framebuffer compositor: it owns the
|
||||||
|
framebuffer (write-combining), composites a z-ordered layer stack into a cacheable back
|
||||||
|
buffer, presents only the damaged region, and is driven over IPC by the `runtime.display`
|
||||||
|
client — proven end-to-end by a separate demo process. Two limitations are deliberate and
|
||||||
|
documented (docs/display.md): no runtime mode-setting (native backend) and no true vsync
|
||||||
|
(no vblank on a dumb framebuffer). Next steps are the Deferred items below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
+264
@@ -0,0 +1,264 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
Three QEMU test cases ([tests.zig](../system/kernel/tests.zig), `python3
|
||||||
|
test/qemu_test.py <case>`), each layering on the last:
|
||||||
|
|
||||||
|
- **`display`** — the kernel handoff: the seeded `display` device is shaped correctly and
|
||||||
|
the claim → `mmio_map` leaf is genuinely **write-combining** (PAT entry 4), asserted at
|
||||||
|
the page-table level.
|
||||||
|
- **`display-service`** — the compositor comes up: it claims the framebuffer, allocates
|
||||||
|
the cacheable back buffer, presents a cleared frame through the double-buffer path
|
||||||
|
(`display: online … / presented frame 0`), and a startup **self-check** composites two
|
||||||
|
overlapping layers on the real framebuffer and reads them back — overlap = the top
|
||||||
|
layer — logging `display: compositor self-check ok`.
|
||||||
|
- **`display-demo`** — the full pipeline from a separate process: the hardware-free
|
||||||
|
[`display-demo`](../system/services/display-demo/) client (the
|
||||||
|
[`input-source`](../system/services/input-source/) analog) drives layers — a wallpaper, a
|
||||||
|
sliding rectangle, a cursor — through the layer client API and heartbeats
|
||||||
|
`display-demo: ok`, proving a frame travelled client → compositor → screen, exactly as
|
||||||
|
the [input test](input.md) proves an event travels source → service → subscriber. The
|
||||||
|
visible motion itself is a screenshot away via `zig build run-x86-64`.
|
||||||
|
|
||||||
|
The compositor's pixel math (rectangle clipping, fill, composite, tile blit) and colour
|
||||||
|
packing are additionally covered by pure host unit tests under `zig build test`.
|
||||||
|
|
||||||
|
## 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.
|
||||||
+10
-9
@@ -57,8 +57,9 @@ is not an address window. Discovery is trusted; user space is not.
|
|||||||
|
|
||||||
### What a bus driver looks like
|
### What a bus driver looks like
|
||||||
|
|
||||||
`system/drivers/bus/bus.zig` is the smallest honest one. Its "bus" is the HPET's register block and
|
danos ships no demo bus driver — the real ones are `pci-bus`, `ps2-bus`, and
|
||||||
its "devices" are the block's comparators:
|
`usb-xhci-bus`. The smallest *honest* shape, illustrated here with an HPET register block
|
||||||
|
as the "bus" and its comparators as the "devices", is:
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
_ = dev.claim(bus.id); // 1. own the bus
|
_ = dev.claim(bus.id); // 1. own the bus
|
||||||
@@ -78,8 +79,8 @@ for (0..n) |i| { // 3. publish each child
|
|||||||
|
|
||||||
Each child is left **unclaimed**, which is the handoff: a comparator driver can now
|
Each child is left **unclaimed**, which is the handoff: a comparator driver can now
|
||||||
`device_claim` one and `mmio_map` it, and will see only its own 0x20-byte window. A child
|
`device_claim` one and `mmio_map` it, and will see only its own 0x20-byte window. A child
|
||||||
whose window escapes the bus is refused — `bus` asserts that, and the `bus` test
|
whose window escapes the bus is refused; the in-kernel `containment` test asserts the
|
||||||
asserts the kernel's table upholds it.
|
kernel's table upholds that ([drivers.md](drivers.md)).
|
||||||
|
|
||||||
A USB device has *no* resources at all: `resource_count = 0`, because it's addressed
|
A USB device has *no* resources at all: `resource_count = 0`, because it's addressed
|
||||||
through its controller, not by MMIO. That case is allowed and is the common one.
|
through its controller, not by MMIO. That case is allowed and is the common one.
|
||||||
@@ -143,7 +144,7 @@ If a class driver needs `mmio`, it has become an HCD and should be one.
|
|||||||
physically-contiguous, pinned, uncacheable, reclaim-on-teardown buffers with the
|
physically-contiguous, pinned, uncacheable, reclaim-on-teardown buffers with the
|
||||||
physical address exposed (`pmm.allocContiguous`, a DMA arena, `mapUserDmaInto`).
|
physical address exposed (`pmm.allocContiguous`, a DMA arena, `mapUserDmaInto`).
|
||||||
`dma_below_4g` caps the address for legacy engines; `dma_write_combining` is accepted
|
`dma_below_4g` caps the address for legacy engines; `dma_write_combining` is accepted
|
||||||
but falls back to coherent until PAT is programmed. hpet is refactored onto `/lib/mmio`;
|
but falls back to coherent until PAT is programmed. The bus drivers use `/lib/mmio`;
|
||||||
no DMA driver consumes `dma_alloc` yet.
|
no DMA driver consumes `dma_alloc` yet.
|
||||||
- **M15** — interrupts for PCI devices, the MSI half. Discovery now gives every PCI
|
- **M15** — interrupts for PCI devices, the MSI half. Discovery now gives every PCI
|
||||||
function its 4 KiB ECAM config space as resource 0 (unblocking the capability walk
|
function its 4 KiB ECAM config space as resource 0 (unblocking the capability walk
|
||||||
@@ -302,8 +303,8 @@ rather than an out-struct. The rest of this section is the original design note.
|
|||||||
|
|
||||||
**The blocker, and it's a hard one.** No PCI device can take an interrupt today.
|
**The blocker, and it's a hard one.** No PCI device can take an interrupt today.
|
||||||
[`addBars`](system/devices/acpi.zig) records `.memory` and `.io_port` BARs and never an
|
[`addBars`](system/devices/acpi.zig) records `.memory` and `.io_port` BARs and never an
|
||||||
`.irq`; there is no `_PRT` parsing anywhere in the tree. `hpet` only works because the
|
`.irq`; there is no `_PRT` parsing anywhere in the tree. The HPET is the one exception —
|
||||||
HPET advertises its own routing options in its own registers — a privilege no ordinary
|
it advertises its own interrupt routing in its own registers, a privilege no ordinary
|
||||||
device has.
|
device has.
|
||||||
|
|
||||||
**The fix, in two halves.**
|
**The fix, in two halves.**
|
||||||
@@ -326,7 +327,7 @@ which means **discovery should give each `pci_device` a `.memory` resource for i
|
|||||||
4 KiB ECAM slot**. That's a small change to `parseMcfg` and it unblocks the whole
|
4 KiB ECAM slot**. That's a small change to `parseMcfg` and it unblocks the whole
|
||||||
capability walk (MSI, MSI-X, PCIe extended caps) without any new syscall.
|
capability walk (MSI, MSI-X, PCIe extended caps) without any new syscall.
|
||||||
|
|
||||||
Note QEMU's HPET reports `Tn_FSB_INT_DEL_CAP = 0` — no MSI — so `hpet` can never
|
Note QEMU's HPET reports `Tn_FSB_INT_DEL_CAP = 0` — no MSI — so an HPET timer could never
|
||||||
exercise this path. The first MSI driver will be the first PCI driver.
|
exercise this path. The first MSI driver will be the first PCI driver.
|
||||||
|
|
||||||
## M16 — the IOMMU, and the honest caveat ◑ detection done, enforcement pending
|
## M16 — the IOMMU, and the honest caveat ◑ detection done, enforcement pending
|
||||||
@@ -353,7 +354,7 @@ gap should be named rather than implied.
|
|||||||
|
|
||||||
`M13` (capability passing) is independent of `M14`/`M15` and is the cheapest. It
|
`M13` (capability passing) is independent of `M14`/`M15` and is the cheapest. It
|
||||||
unlocks class drivers, which are the shape with no hardware requirements at all — you
|
unlocks class drivers, which are the shape with no hardware requirements at all — you
|
||||||
could write a real one against `bus`'s comparators tomorrow.
|
could write a real one against any device a bus driver publishes tomorrow.
|
||||||
|
|
||||||
`M14` and `M15` together unlock the first HCD. `M14`'s barrier layer is worth landing
|
`M14` and `M15` together unlock the first HCD. `M14`'s barrier layer is worth landing
|
||||||
on its own regardless: it's small, obviously correct, and stops every future driver
|
on its own regardless: it's small, obviously correct, and stops every future driver
|
||||||
|
|||||||
+60
-29
@@ -22,12 +22,12 @@ say.*
|
|||||||
|
|
||||||
## How a driver gets started: discover, match, spawn
|
## How a driver gets started: discover, match, spawn
|
||||||
|
|
||||||
Nothing in the kernel decides that the HPET needs the `hpet` driver — that is policy,
|
Nothing in the kernel decides that the PCI host bridge needs the `pci-bus` driver — that
|
||||||
and policy lives in user space. Boot brings user space up as a three-level supervision
|
is policy, and policy lives in user space. Boot brings user space up as a three-level
|
||||||
hierarchy, each level owning one job:
|
supervision hierarchy, each level owning one job:
|
||||||
|
|
||||||
```
|
```
|
||||||
kernel ──spawns──► init (PID 1) ──spawns──► device-manager ──spawns──► hpet
|
kernel ──spawns──► init (PID 1) ──spawns──► device-manager ──spawns──► pci-bus
|
||||||
| | |
|
| | |
|
||||||
spawns only init, the service supervisor: the driver supervisor: enumerates
|
spawns only init, the service supervisor: the driver supervisor: enumerates
|
||||||
publishes the starts the system /system/devices, matches each device
|
publishes the starts the system /system/devices, matches each device
|
||||||
@@ -184,7 +184,11 @@ Two properties worth knowing:
|
|||||||
|
|
||||||
## A whole driver
|
## A whole driver
|
||||||
|
|
||||||
`system/drivers/hpet/hpet.zig` is ~150 lines and does all of it. The shape:
|
A minimal leaf driver is only ~150 lines and does all of it. danos ships **no such
|
||||||
|
example binary** — the driver model is proven by the real drivers (`pci-bus`, `ps2-bus`,
|
||||||
|
`usb-xhci-bus`), and a teaching example belongs here, in the docs, rather than as a
|
||||||
|
compiled program nobody runs. Illustrated with a hypothetical HPET timer driver, the
|
||||||
|
shape is:
|
||||||
|
|
||||||
```zig
|
```zig
|
||||||
const hpet = findHpet(buf) orelse return; // device_enumerate, look for
|
const hpet = findHpet(buf) orelse return; // device_enumerate, look for
|
||||||
@@ -209,8 +213,8 @@ while (...) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The HPET is a good first driver for a reason that isn't obvious. Its *counter* is a
|
The HPET makes a good illustration for a reason that isn't obvious. Its *counter* is a
|
||||||
clocksource — the only way to use it is to read it, so it proved `mmio_map` without
|
clocksource — the only way to use it is to read it, so it exercises `mmio_map` without
|
||||||
needing interrupts at all. Its *comparators* are a clockevent, and can be configured
|
needing interrupts at all. Its *comparators* are a clockevent, and can be configured
|
||||||
**level-triggered** (`Tn_INT_TYPE_CNF`), which asserts a bit in `GENERAL_INT_STATUS`
|
**level-triggered** (`Tn_INT_TYPE_CNF`), which asserts a bit in `GENERAL_INT_STATUS`
|
||||||
that the driver must write-1-to-clear. That's a genuine deassert step, so the full
|
that the driver must write-1-to-clear. That's a genuine deassert step, so the full
|
||||||
@@ -252,9 +256,10 @@ bus driver may only ever subdivide what it already owns.
|
|||||||
A device with **no resources** is legal and common. A USB device is reached through its
|
A device with **no resources** is legal and common. A USB device is reached through its
|
||||||
controller, not by MMIO, so it gets `resource_count = 0`.
|
controller, not by MMIO, so it gets `resource_count = 0`.
|
||||||
|
|
||||||
See [`system/drivers/bus/bus.zig`](../system/drivers/bus/bus.zig) for a complete one, and
|
See [`system/drivers/pci-bus/pci-bus.zig`](../system/drivers/pci-bus/pci-bus.zig) for a
|
||||||
[driver-model.md](driver-model.md) for how bus drivers, class drivers and host
|
real one — it claims a PCI host bridge, maps its ECAM window, and publishes each function
|
||||||
controller drivers fit together.
|
it finds as a child — and [driver-model.md](driver-model.md) for how bus drivers, class
|
||||||
|
drivers and host controller drivers fit together.
|
||||||
|
|
||||||
## What the kernel does not do for you
|
## What the kernel does not do for you
|
||||||
|
|
||||||
@@ -313,32 +318,38 @@ uncacheable, physical address exposed), and **memory barriers** (`/lib/mmio`'s
|
|||||||
|
|
||||||
## Verifying it
|
## Verifying it
|
||||||
|
|
||||||
The `hpet` test spawns `hpet` from the initial ramdisk and watches the serial log. The driver
|
No demo driver ships to prove this end to end; the *real* drivers do, so the tests
|
||||||
prints `hpet: ok` only after being woken five times, and its loop's only exit is
|
target them and the kernel primitives directly:
|
||||||
through `replyWait` returning a notification — it cannot reach that line by polling.
|
|
||||||
|
|
||||||
The last check doesn't trust the driver's self-report at all: the kernel reads the I/O
|
- **`device-manager`** — boots only the device manager, which discovers the PCI host
|
||||||
APIC redirection entry back and asserts the line really is routed to a device vector,
|
bridge, matches `pci-bus`, and `system_spawn`s it. The test reads kernel state — the
|
||||||
really is level-triggered, and really was left unmasked by the driver's final
|
process table and the device tree — to confirm pci-bus came up and registered the
|
||||||
`irq_ack`.
|
functions it enumerated: the whole discover → match → spawn → driver-up chain.
|
||||||
|
- **`acpi-ps2`** — a user-space driver (`ps2-bus`) is woken by its device's IRQ,
|
||||||
|
delivered as an IPC notification, and attaches the keyboard: IRQ-as-IPC, end to end.
|
||||||
|
- **`pci-scan`** — a user-space driver (`pci-bus`) maps its device's MMIO (the ECAM
|
||||||
|
window) and walks it: `mmio_map`, end to end.
|
||||||
|
- **`containment`** — the kernel refuses a `device_register` whose child window escapes
|
||||||
|
the parent's grant (else it would be a syscall for mapping arbitrary memory), while an
|
||||||
|
identical re-register stays idempotent. Asserted in-kernel, straight against the broker.
|
||||||
|
- **`irqfree`** — the teardown path. Binds two owners to one shared endpoint, releases
|
||||||
|
one, and reads the I/O APIC back: the departing owner's line is masked, the sibling's
|
||||||
|
is not. That second half is why bindings are keyed on the owning *task* and not on the
|
||||||
|
endpoint pointer — endpoints are shared, so releasing "everything pointing at this
|
||||||
|
endpoint" would silently mask a live driver's device.
|
||||||
|
- **`iopass`** — the `device_grant` teardown rule, so destroying a driver's address
|
||||||
|
space never returns MMIO frames to the RAM pool.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ python3 test/qemu_test.py hpet irqfree iopass
|
$ python3 test/qemu_test.py device-manager acpi-ps2 pci-scan containment irqfree iopass
|
||||||
hpet ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
device-manager ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
||||||
|
acpi-ps2 ... PASS
|
||||||
|
pci-scan ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
||||||
|
containment ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
||||||
irqfree ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
irqfree ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
||||||
iopass ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
iopass ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
||||||
```
|
```
|
||||||
|
|
||||||
Two companions cover what `hpet` can't, because it never exits:
|
|
||||||
|
|
||||||
- **`irqfree`** — the teardown path. Binds two owners to one shared endpoint, releases
|
|
||||||
one, and reads the I/O APIC back: the departing owner's line is masked, the sibling's
|
|
||||||
is not. That second half is why bindings are keyed on the owning *task* and not on
|
|
||||||
the endpoint pointer — endpoints are shared, so releasing "everything pointing at
|
|
||||||
this endpoint" would silently mask a live driver's device.
|
|
||||||
- **`iopass`** — the `device_grant` teardown rule, so destroying a driver's address
|
|
||||||
space never returns MMIO frames to the RAM pool.
|
|
||||||
|
|
||||||
## What's next (not done here)
|
## What's next (not done here)
|
||||||
|
|
||||||
The big driver-model pieces — capability passing (class drivers), DMA + barriers, MSI,
|
The big driver-model pieces — capability passing (class drivers), DMA + barriers, MSI,
|
||||||
@@ -362,3 +373,23 @@ the first DMA driver to protect and test against) and these smaller items:
|
|||||||
- **Interrupt priority / threaded IRQ latency.** `notifyFromIsr` enqueues the woken
|
- **Interrupt priority / threaded IRQ latency.** `notifyFromIsr` enqueues the woken
|
||||||
driver but doesn't preempt (`wakeLocked` deliberately leaves that to the caller), so
|
driver but doesn't preempt (`wakeLocked` deliberately leaves that to the caller), so
|
||||||
a woken driver waits for the next scheduling point.
|
a woken driver waits for the next scheduling point.
|
||||||
|
|
||||||
|
## The driver contract (M17–M18)
|
||||||
|
|
||||||
|
Claiming and mapping is half of being a danos driver; the other half is the
|
||||||
|
**lifecycle and protocol contract**, and the runtime makes it nearly free:
|
||||||
|
|
||||||
|
- Build on `runtime.service.run` — one replyWait loop folding protocol
|
||||||
|
requests, signals, and notifications into callbacks. The harness answers the
|
||||||
|
universal zero-length ping and turns `terminate` into a clean exit for you
|
||||||
|
([process-lifecycle.md](process-lifecycle.md)).
|
||||||
|
- A driver spawned with an assignment (its device id as argv[1]) sends the
|
||||||
|
versioned `hello` to the device manager inside the deadline, and a **bus**
|
||||||
|
driver reports what it discovers with `child_added`
|
||||||
|
([device-manager.md](device-manager.md); usb-xhci-bus is the reference
|
||||||
|
implementation).
|
||||||
|
- Crash freely — that is the design. The kernel releases your claims, IRQ
|
||||||
|
bindings, and MSI vectors at death; the manager reads your exit reason,
|
||||||
|
prunes what you reported, restarts you with backoff, and your fresh instance
|
||||||
|
re-claims and re-reports. Never depend on your own cleanup running
|
||||||
|
(iron rule 1).
|
||||||
|
|||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
# The input module: broadcasting input events
|
||||||
|
|
||||||
|
A keyboard driver has one keystroke and *many* programs that might want it — a shell, a
|
||||||
|
window server, a logger. None of them owns the hardware, and the driver should not know
|
||||||
|
who is listening. So between the drivers and the listeners sits the **input service**
|
||||||
|
(`system/services/input/`): drivers **publish** events to it, programs **subscribe**, and
|
||||||
|
it fans each event out to every interested subscriber. It is an ordinary ring-3 process
|
||||||
|
reached over IPC, like the [VFS server](../system/services/vfs/vfs.zig) — no kernel knows
|
||||||
|
what a key is.
|
||||||
|
|
||||||
|
## One service, several device classes
|
||||||
|
|
||||||
|
The service carries three device classes today — **keyboard**, **mouse**, and
|
||||||
|
**joystick/gamepad** — and is built to take more
|
||||||
|
([protocol.zig](../system/services/input/protocol.zig)). Each class has its own typed
|
||||||
|
event:
|
||||||
|
|
||||||
|
- `KeyEvent` — `key_down`/`key_up` (physical make/break) and `key_press` (a character was
|
||||||
|
produced, carrying the Unicode scalar); plus a layout-independent `keycode` and a
|
||||||
|
`modifiers` bitmask.
|
||||||
|
- `MouseEvent` — relative `motion` (`dx`/`dy`), `button_down`/`button_up`, and `scroll`.
|
||||||
|
- `JoystickEvent` — `axis` moves (a signed value on a `control` index) and
|
||||||
|
`button_down`/`button_up`.
|
||||||
|
|
||||||
|
All three travel in one **`InputEvent` envelope** tagged with a `DeviceKind`, so the
|
||||||
|
fan-out is a single code path and a subscriber can take a mix of classes on one stream.
|
||||||
|
Decode an envelope with `asKeyboard()` / `asMouse()` / `asJoystick()` (each returns null
|
||||||
|
unless the tag matches). A subscriber names the classes it wants with a **`device_mask`**,
|
||||||
|
and the service routes each event only to subscribers whose mask includes its class — so a
|
||||||
|
mouse-only listener never wakes for keystrokes.
|
||||||
|
|
||||||
|
## Why this needed a new kernel primitive
|
||||||
|
|
||||||
|
The interesting part is delivery, and it runs straight into the shape of danos IPC.
|
||||||
|
[ipc.md](ipc.md) describes a **synchronous rendezvous**: a server holds exactly one
|
||||||
|
pending reply (`Task.ipc_client`) and *must* answer it on its next `replyWait`. Two
|
||||||
|
consequences decide the whole design:
|
||||||
|
|
||||||
|
1. **You cannot block N subscribers waiting for "the next event".** A server can hold only
|
||||||
|
one caller at a time, so the natural "subscriber calls `next_event()` and blocks" API
|
||||||
|
is impossible for more than one subscriber. Delivery therefore has to be **push** — the
|
||||||
|
service reaching out to subscribers — not pull.
|
||||||
|
|
||||||
|
2. **A synchronous push can hang the whole service.** If the service delivered with
|
||||||
|
`ipc_call`, it would block until each subscriber replied. `ipc_call` has no timeout, and
|
||||||
|
the kernel does **not** wake a caller parked on a *dead* peer's endpoint (it only fails a
|
||||||
|
peer that was mid-reply — see [process.zig](../system/kernel/process.zig)
|
||||||
|
`releaseTaskResourcesLocked`). One subscriber that exits mid-delivery would wedge input
|
||||||
|
for everyone. That is the opposite of the resilience the microkernel is for.
|
||||||
|
|
||||||
|
The fix is the asynchronous send that [ipc.md](ipc.md) had already earmarked as future
|
||||||
|
work ("asynchronous / buffered send … for notifications between servers"):
|
||||||
|
|
||||||
|
```
|
||||||
|
ipc_send(handle, message_ptr, message_len) -> 0 / -errno
|
||||||
|
```
|
||||||
|
|
||||||
|
`ipc_send` copies a small payload into the endpoint's **bounded queue** and wakes a
|
||||||
|
receiver, then returns immediately — it never blocks and so can never hang on a dead or
|
||||||
|
slow subscriber. The receiver picks it up through the same `replyWait` it already runs:
|
||||||
|
the wake arrives as a **buffered message** — `notify_badge_bit | notify_message_bit` set in
|
||||||
|
the badge (distinguishing it from a bare IRQ/child-exit notification), the sender's task id
|
||||||
|
in the low bits, and the payload in the receive buffer, with no reply owed. The queue holds
|
||||||
|
16 messages per endpoint; a full queue **drops the oldest**, because a buffered message is
|
||||||
|
discrete data, not a coalescing "level" like an interrupt. See
|
||||||
|
[ipc-synchronous.zig](../system/kernel/ipc-synchronous.zig) (`sendLocked`, `popPost`, and
|
||||||
|
the `replyWait` receive loop).
|
||||||
|
|
||||||
|
This is the async counterpart of `ipc_call`, and the input service is its first consumer.
|
||||||
|
|
||||||
|
## How the pieces fit
|
||||||
|
|
||||||
|
```
|
||||||
|
keyboard/mouse driver, input-source input service subscriber(s)
|
||||||
|
----------------------------------- ------------- -------------
|
||||||
|
connectSource(); loop: replyWait: subscribeKeyboard()/…All:
|
||||||
|
publishKeyboardEvent(k) ─ ipc_call ─▶ publish → broadcast: createIpcEndpoint()
|
||||||
|
publishMouseEvent(m) for each sub whose callCap(subscribe,
|
||||||
|
publishJoystickEvent(j) mask matches event.device: send_cap = ep,
|
||||||
|
ipc_send(sub_ep) ──────▶ device_mask)
|
||||||
|
reply ok loop: next()
|
||||||
|
subscribe → store {ep cap, └─ replyWait(ep)
|
||||||
|
task id, device_mask} → InputEvent
|
||||||
|
```
|
||||||
|
|
||||||
|
- A **subscriber** calls `input.subscribe(mask)` — or a typed helper: `subscribeKeyboard()`,
|
||||||
|
`subscribeMouse()`, `subscribeJoystick()` (one class, `next()` returns the decoded event),
|
||||||
|
or `subscribeAll()` (every class, `next()` returns a tagged `InputEvent`)
|
||||||
|
([library/runtime/input.zig](../library/runtime/input.zig)). It creates its own endpoint
|
||||||
|
and hands it to the service as a **capability** (M13 capability passing — the input
|
||||||
|
service is that feature's first real user), along with its `device_mask`. Then it loops on
|
||||||
|
`next()`, a `replyWait` on that endpoint returning each pushed event.
|
||||||
|
- A **source** (a keyboard, mouse, or joystick driver) calls `input.connectSource()` and the
|
||||||
|
method for its class: `publishKeyboardEvent`, `publishMouseEvent`, or
|
||||||
|
`publishJoystickEvent`. Publishing is a short synchronous `ipc_call` the service answers at
|
||||||
|
once; the service's own fan-out is asynchronous, so publishing never blocks on a slow
|
||||||
|
subscriber.
|
||||||
|
- The **service** ([input.zig](../system/services/input/input.zig)) keeps a small subscriber
|
||||||
|
table (endpoint handle + owning task id + `device_mask`). On `publish` it `ipc_send`s the
|
||||||
|
event to every subscriber whose mask includes the event's device class. On `subscribe` it
|
||||||
|
stores the passed capability and mask and, as housekeeping, prunes any slot whose owning
|
||||||
|
process has exited (checked against `process_enumerate`) — not for correctness (an async
|
||||||
|
send to an orphaned endpoint is harmless) but to reclaim the slot.
|
||||||
|
|
||||||
|
Publisher and subscriber must be **separate processes**: a single thread that both
|
||||||
|
published and serviced its own subscription would deadlock (its `publish` call blocks until
|
||||||
|
the service delivers to its endpoint, which only the same thread could receive).
|
||||||
|
|
||||||
|
## Status and follow-ups
|
||||||
|
|
||||||
|
- **The keyboard is real.** The `ps2-bus` driver owns PNP0303, which carries *both* the
|
||||||
|
0x60/0x64 ports and IRQ1, so reading the hardware lives in the bus, not in
|
||||||
|
[keyboard.zig](../system/drivers/ps2-bus/keyboard.zig): the bus binds IRQ1 and, on each
|
||||||
|
interrupt, drains port 0x60, routing every byte by the status register's
|
||||||
|
auxiliary-output bit to whichever child driver **attached** for that device (an
|
||||||
|
`AttachRequest` to the well-known `ps2_bus` service, carrying the child's endpoint as a
|
||||||
|
capability; the bytes then arrive as asynchronous `ForwardedByte` messages, so the IRQ
|
||||||
|
path never blocks on a child). The keyboard driver decodes the stream — scancode **set 2**,
|
||||||
|
what the keyboard sends with the 8042's legacy translation off, decoded by
|
||||||
|
[scancode.zig](../system/drivers/ps2-bus/scancode.zig) into USB HID usage keycodes with
|
||||||
|
make/break, typematic-repeat, and modifier tracking (host-tested under `zig build test`) —
|
||||||
|
and publishes real `key_down`/`key_press`/`key_up` events.
|
||||||
|
- **Keycode → character** is wired in: the keyboard driver fills a `key_press` event's
|
||||||
|
`character` through [`library/xkeyboard-config`](../library/xkeyboard-config/README.md)
|
||||||
|
(`xkb.map(layout, keycode, mods)` → keysym + Unicode character), synthesizing the ASCII
|
||||||
|
control characters for Enter/Tab/Backspace/Escape, whose keysyms map to no Unicode. The
|
||||||
|
layout defaults to `us`; the bus can pass another as the driver's argv[2] — the seam for
|
||||||
|
a future settings source.
|
||||||
|
- **The mouse is real too.** IRQ12 is enumerated on the auxiliary device's own ACPI node
|
||||||
|
(PNP0F13), so the bus claims that node alongside the controller and routes both IRQs to
|
||||||
|
its one endpoint, acking whichever line the notification's badge names.
|
||||||
|
[mouse.zig](../system/drivers/ps2-bus/mouse.zig) attaches the way the keyboard does and
|
||||||
|
assembles the forwarded bytes with
|
||||||
|
[mouse-packet.zig](../system/drivers/ps2-bus/mouse-packet.zig) (three-byte stream-mode
|
||||||
|
packets: sync/overflow handling, nine-bit movement, screen-convention `dy` — host-tested
|
||||||
|
under `zig build test`) into `button_down`/`button_up` transitions and `motion` events.
|
||||||
|
**Follow-up:** the IntelliMouse magic-knock for a scroll wheel (four-byte packets) and
|
||||||
|
`scroll` events. The hardware-free `input-source` still rotates through all three classes
|
||||||
|
synthetically (including a joystick, which has no driver yet) via the
|
||||||
|
`input.synthetic*Event` helpers.
|
||||||
|
- **Drop-oldest under overflow** is a defined loss; the 16-slot ring absorbs normal bursts.
|
||||||
|
Real backpressure/flow-control is future work.
|
||||||
|
- **`publish` is unauthenticated** — any process may publish, consistent with the current
|
||||||
|
bring-up trust model (see [driver-model.md](driver-model.md)). A source capability is
|
||||||
|
future work.
|
||||||
|
|
||||||
|
## Verifying it
|
||||||
|
|
||||||
|
The `input` case (`python3 test/qemu_test.py input`, in
|
||||||
|
[tests.zig](../system/kernel/tests.zig) `inputTest`) boots the real kernel and spawns the
|
||||||
|
service, the synthetic source (which cycles keyboard, mouse, and joystick events), and a
|
||||||
|
subscriber that took all three classes. It passes only when the subscriber heartbeats
|
||||||
|
`input-test: ok` — proof that an event travelled source → service → subscriber over IPC,
|
||||||
|
exercising `ipc_send`, capability-passing subscription, and per-device routing. Each
|
||||||
|
serial line names the class received, so the log shows all three arriving on one stream.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [ipc.md](ipc.md) — the synchronous rendezvous and the notification path `ipc_send` extends.
|
||||||
|
- [syscall.md](syscall.md) — the system-call surface, including `ipc_send`.
|
||||||
|
- [driver-model.md](driver-model.md) — class drivers, capability passing (M13), the trust model.
|
||||||
+25
-1
@@ -95,6 +95,30 @@ This is what makes a user-space driver possible at all, and it's the subject of
|
|||||||
every capability is either well-known (the registry) or inherited — there's no way
|
every capability is either well-known (the registry) or inherited — there's no way
|
||||||
to delegate one.
|
to delegate one.
|
||||||
- **Asynchronous / buffered send** for the cases where a rendezvous is the wrong
|
- **Asynchronous / buffered send** for the cases where a rendezvous is the wrong
|
||||||
shape (logging, notifications between servers).
|
shape (logging, notifications between servers). *Landed as `ipc_send`* — a
|
||||||
|
non-blocking post to an endpoint's bounded payload queue, delivered through
|
||||||
|
`reply_wait` as a buffered message (badge bit `notify_message_bit`). Built for, and
|
||||||
|
first used by, the [input service](input.md)'s keyboard-event broadcast, where a
|
||||||
|
synchronous push would let one dead subscriber hang the fan-out. A full queue drops
|
||||||
|
the oldest (discrete messages, not a coalescing level like the notification ring).
|
||||||
- **A bounded reply.** `MSG_MAX` is 256 bytes and the copy runs under the big kernel
|
- **A bounded reply.** `MSG_MAX` is 256 bytes and the copy runs under the big kernel
|
||||||
lock; a bulk transfer wants shared pages, not a copy.
|
lock; a bulk transfer wants shared pages, not a copy.
|
||||||
|
|
||||||
|
## Lifecycle conventions over IPC (M17)
|
||||||
|
|
||||||
|
Three conventions from [process-lifecycle.md](process-lifecycle.md) ride the
|
||||||
|
notification mechanism:
|
||||||
|
|
||||||
|
- **Signals** arrive as notifications on the endpoint a process nominated with
|
||||||
|
`signal_bind` (`runtime.process.bindSignals`): badge = the signal bit plus the
|
||||||
|
coalesced pending mask (`runtime.process.signalsFrom` decodes). Statements,
|
||||||
|
never questions; no payload, no reply.
|
||||||
|
- **One-shot timers** (`timer_bind`, `runtime.system.timerOnce`) land as a
|
||||||
|
timer-bit notification — the timed wait: a service arms a deadline and keeps
|
||||||
|
serving, instead of blocking in sleep.
|
||||||
|
- **The universal ping**: a **zero-length request is the liveness probe**,
|
||||||
|
answered with a zero-length reply by the service harness itself
|
||||||
|
(`runtime.service.run`). No protocol's requests start at length zero, so the
|
||||||
|
encoding cannot collide, and a wedged service simply fails to answer — which
|
||||||
|
is the diagnosis. Deep health ("can I reach my hardware?") stays a per-service
|
||||||
|
protocol message.
|
||||||
|
|||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
# The power service: events and shutdown
|
||||||
|
|
||||||
|
A laptop lid closes, a battery drains, someone presses the power button — and
|
||||||
|
several parts of the system might care: a session manager dims the screen, a
|
||||||
|
logger notes it, and ultimately *something* has to turn the machine off. None of
|
||||||
|
them owns the hardware that reported the event, and the reporter should not know
|
||||||
|
who is listening. So system power is a **service**: an event source **publishes**
|
||||||
|
button/lid/battery/AC events, interested processes **subscribe**, and one
|
||||||
|
privileged caller — init — can ask it to power the machine off. It is the same
|
||||||
|
publish/subscribe shape as the [input service](input.md), applied to power.
|
||||||
|
|
||||||
|
## Why a service, and why it is named for the domain, not the firmware
|
||||||
|
|
||||||
|
Where the events come from is firmware-specific — on x86 they ride the ACPI SCI
|
||||||
|
([acpi.md](acpi.md)); on a Raspberry Pi they would come from PSCI or a mailbox.
|
||||||
|
What subscribers want is not: *the lid closed* means the same thing regardless of
|
||||||
|
who noticed. So the surface is **domain-named**. There is a `power-protocol`
|
||||||
|
module and a well-known `ServiceId.power = 5`; on x86 the **acpi service**
|
||||||
|
registers it, and on ARM a PSCI/mailbox service will register the *same* id.
|
||||||
|
Subscribers call `runtime.ipc.lookup(.power)` and never learn which firmware they
|
||||||
|
are on — the neutrality the whole [discovery](discovery.md) migration exists to
|
||||||
|
preserve, carried one layer up into a running-system surface.
|
||||||
|
|
||||||
|
This is why the protocol is `power`, not "ACPI events": naming a cross-firmware
|
||||||
|
surface after one firmware would leak x86 into code the ARM port must reuse
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
## The protocol
|
||||||
|
|
||||||
|
The `power-protocol` module ([system/services/power/protocol.zig](../system/services/power/protocol.zig))
|
||||||
|
follows the vfs-protocol pattern — extern-struct messages, a version, reserved
|
||||||
|
fields. Three operations:
|
||||||
|
|
||||||
|
| Direction | Operation | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| subscriber → service | `subscribe` | receive published events; the subscriber's endpoint rides as the call's **capability** (the input/device-manager pattern) |
|
||||||
|
| init → service | `shutdown` | orderly shutdown's last step: enter S5 (soft off) |
|
||||||
|
| service → subscriber | `event` | a published `EventMessage`, delivered as a buffered message (never sent *to* the service) |
|
||||||
|
|
||||||
|
Events are published, not polled: like the input service, the service holds
|
||||||
|
subscriber endpoints as capabilities and `ipc_send`s each event as a buffered
|
||||||
|
message, so a slow or dead subscriber can never wedge the source. The event
|
||||||
|
vocabulary is hardware-neutral:
|
||||||
|
|
||||||
|
- `power_button` — the button was pressed (a fixed ACPI event on x86).
|
||||||
|
- `lid`, `ac`, `battery` — the named GPE-driven events.
|
||||||
|
- `notify` — a device notification that maps to none of the above; its `code`
|
||||||
|
(the ACPI `Notify` argument) and the notifying device's `hid` say which device
|
||||||
|
and what happened.
|
||||||
|
|
||||||
|
An `EventMessage` carries the `event` tag plus `code` and an 8-byte `hid`, so a
|
||||||
|
generic `notify` is fully described without a second round trip.
|
||||||
|
|
||||||
|
**`shutdown` is authority, not information.** It is the only operation that
|
||||||
|
*does* something irreversible, so it is gated: the contract is that only init
|
||||||
|
(PID 1) may request it, because init is the process that has already run the stop
|
||||||
|
sequence over everything else. The acpi service implements this as a **soft
|
||||||
|
gate** — it honors `shutdown` only from a process that is a *subscriber*, and
|
||||||
|
init is the one subscriber. That stands in for "only the system supervisor may
|
||||||
|
power off" without hard-coding a pid, so it still holds under tests where PID 1
|
||||||
|
is not init.
|
||||||
|
|
||||||
|
## Orderly shutdown
|
||||||
|
|
||||||
|
Powering off cleanly is where the power service, the [process
|
||||||
|
lifecycle](process-lifecycle.md), and [ACPI events](acpi.md) compose. init
|
||||||
|
already supervises the services it starts; for shutdown it runs **one event loop
|
||||||
|
over one endpoint** that carries three things at once: its children's exit
|
||||||
|
notifications, the lifecycle **signals** it can receive (`terminate`), and the
|
||||||
|
**power events** it subscribes to — plus a re-arming heartbeat timer proving PID
|
||||||
|
1 is alive. (init subscribes with retries, because the power service registers
|
||||||
|
`.power` well after init starts; a missing power service is not fatal — a
|
||||||
|
`terminate` signal drives the same path.)
|
||||||
|
|
||||||
|
On a `power_button` event or a `terminate` signal, init:
|
||||||
|
|
||||||
|
1. logs that it is shutting down,
|
||||||
|
2. runs the standard stop sequence — `runtime.process.stop(child, deadline,
|
||||||
|
endpoint)` — over its children **in reverse spawn order**, so the VFS stops
|
||||||
|
last (other services may flush through it), each child getting the
|
||||||
|
*terminate → deadline → kill* escalation from
|
||||||
|
[process-lifecycle.md](process-lifecycle.md), and
|
||||||
|
3. requests `.power` `shutdown`.
|
||||||
|
|
||||||
|
The service then enters **S5** (soft off) by writing `SLP_TYP | SLP_EN` to the
|
||||||
|
PM1 control register(s) from ring 3, mirroring the kernel's own
|
||||||
|
`system/devices/power.zig` `sleepValue`. If the write returns instead of powering
|
||||||
|
the machine off, it logs loudly so a test fails rather than hangs.
|
||||||
|
|
||||||
|
**No new system call was needed for S5.** The broad io_port grant on the
|
||||||
|
`acpi-tables` node ([discovery.md](discovery.md)) already put the PM1 control
|
||||||
|
ports in the acpi service's hands, so writing S5 from ring 3 is something it
|
||||||
|
could physically already do; formalizing it as a protocol operation added a
|
||||||
|
contract, not authority. The kernel keeps `power.zig` for its own test paths and
|
||||||
|
panic-time poweroff, where no user space is available to ask.
|
||||||
|
|
||||||
|
## Verifying it
|
||||||
|
|
||||||
|
Two QEMU scenarios exercise the path, both injecting a real ACPI power-button
|
||||||
|
press via QMP `system_powerdown` (there is no other deterministic power event on
|
||||||
|
this config):
|
||||||
|
|
||||||
|
- `power-button` proves the source: the acpi service's SCI handler logs the
|
||||||
|
press and publishes `power_button` (the ACPI half is in [acpi.md](acpi.md)).
|
||||||
|
- `orderly-shutdown` proves the whole composition: button → init logs shutting
|
||||||
|
down → children stopped → the service enters S5 → QEMU exits. The ordered
|
||||||
|
regex is the proof, and QEMU's self-exit through S5 is the pass.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Interface-complete but validated on real hardware (the author's laptop) later,
|
||||||
|
because QEMU does not emulate them: battery `_BST`/`_BIF` evaluation beyond the
|
||||||
|
interface stubs, lid and AC events, and the embedded controller's `_Qxx`
|
||||||
|
queries. Deliberately out of scope for now: reboot over the power protocol, S3
|
||||||
|
sleep, per-device D-states (a future lifecycle-vocabulary extension, since
|
||||||
|
"suspend" has the shape of a signal every driver must answer and has no consumer
|
||||||
|
until laptop sleep), and thermal zones.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [acpi.md](acpi.md) — where the events come from on x86: the SCI, the power
|
||||||
|
button fixed event, and GPE/Notify dispatch in the acpi service.
|
||||||
|
- [discovery.md](discovery.md) — why the surface is domain-named, and the
|
||||||
|
firmware neutrality that makes a PSCI backend drop-in on ARM.
|
||||||
|
- [process-lifecycle.md](process-lifecycle.md) — the stop sequence
|
||||||
|
(`terminate → deadline → kill`) and signals init composes into shutdown.
|
||||||
|
- [device-manager.md](device-manager.md) — the supervision model init mirrors for
|
||||||
|
its own children.
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
# Process lifecycle: signals over IPC
|
||||||
|
|
||||||
|
**Status: increments 1–4 built** (2026-07-12): claim release on death, exit
|
||||||
|
reasons, published exit events, and signals + one-shot timers + the service
|
||||||
|
harness are all in — the interface below is as-built. The primitives underneath
|
||||||
|
predate this design ([process-management.md](process-management.md):
|
||||||
|
spawn, the supervision link, kill, child-exit notifications); this document designs
|
||||||
|
the layer above them — the standard vocabulary a danos process speaks about its own
|
||||||
|
life, and the stable `runtime.process` interface that carries it. Nothing here is
|
||||||
|
device- or driver-specific: a driver, the VFS, and a user application all stop,
|
||||||
|
reload, and die the same way. The device manager is simply this design's first
|
||||||
|
serious customer ([device-manager.md](device-manager.md)).
|
||||||
|
|
||||||
|
**"POSIX" in this document means the concepts, never the letter of the standard.**
|
||||||
|
danos borrows the ideas and the hard-won lessons (what SIGTERM *means*, why SIGPIPE
|
||||||
|
was a mistake) without inheriting the mechanism, the API, or the names. The naming
|
||||||
|
rule is danos's own and it is strict: plain words that communicate intent
|
||||||
|
(`terminate`, `reload`, `exited`) and the IPC vocabulary the system already speaks
|
||||||
|
(`bind`, `subscribe`, `publish`, `endpoint`) — never `SIG*`, never a second word for
|
||||||
|
a concept that already has one. Literal POSIX arrives later and lives elsewhere: the
|
||||||
|
`std.os.danos` seam that makes danos a Zig target, and eventually a **musl-based C
|
||||||
|
layer** on the same native surface (see [zig-self-hosting.md](zig-self-hosting.md)) —
|
||||||
|
musl's syscall surface retargeted at danos system calls and IPC protocols (files onto
|
||||||
|
the VFS protocol, `sigaction`/`wait` onto this lifecycle, sockets onto whatever
|
||||||
|
networking becomes). Ported programs see POSIX; the system underneath never does.
|
||||||
|
|
||||||
|
## Why a standard vocabulary
|
||||||
|
|
||||||
|
A supervisor can only manage processes it has never heard of if "please exit" means
|
||||||
|
the same thing to all of them. That is the one thing POSIX signals got deeply right:
|
||||||
|
`SIGTERM` means the same thing to nginx and to a five-line script, which is why
|
||||||
|
process supervision on Unix (init systems, container runtimes) is possible at all.
|
||||||
|
danos wants that property from day one, because supervision-and-restart is the
|
||||||
|
system's core motivation ([resilience.md](resilience.md)).
|
||||||
|
|
||||||
|
What POSIX got wrong — for a system like this — is the **delivery mechanism**:
|
||||||
|
asynchronous control-flow hijack. A Unix handler runs on a stolen stack at an
|
||||||
|
arbitrary instruction boundary, which is why the async-signal-safe function list
|
||||||
|
exists, why `errno` must be saved, and why the canonical signal bug is a SIGTERM
|
||||||
|
handler innocently calling `printf` mid-`malloc`. That entire bug class comes from
|
||||||
|
the mechanism, not the vocabulary, and none of it is worth importing.
|
||||||
|
|
||||||
|
A microkernel already has the right channel: **a signal is a message.** QNX delivers
|
||||||
|
POSIX signals over its message passing; seL4 has notification objects; Erlang turned
|
||||||
|
"death is a message to whoever linked" into a reliability philosophy. danos has
|
||||||
|
already done it once without naming it: a child's death arrives as a notification
|
||||||
|
badge on the supervisor's endpoint — the microkernel's SIGCHLD, the IRQ-as-IPC
|
||||||
|
pattern reused. Signals are the same pattern reused a third time.
|
||||||
|
|
||||||
|
## The mechanism
|
||||||
|
|
||||||
|
- **`signal_bind(endpoint)`** — a process nominates the endpoint its signals arrive
|
||||||
|
on, exactly as `irq_bind` nominates where a device's interrupts land. The runtime
|
||||||
|
does this at startup for any program that opts in.
|
||||||
|
- **`process_signal(id, signal)`** — posts the signal as an asynchronous
|
||||||
|
notification to the target's bound endpoint: badge = `notify_badge_bit |
|
||||||
|
notify_signal_bit | pending signals`. Non-blocking for the sender, always.
|
||||||
|
- **Pending signals coalesce** in a per-process bitmask until the target next waits
|
||||||
|
— exactly like interrupt notifications, and exactly POSIX's own semantics for
|
||||||
|
non-realtime signals (two pending SIGTERMs are one SIGTERM). The bitmask *is* the
|
||||||
|
design: signals carry no payload. Anything with a payload is a protocol message.
|
||||||
|
- **Authority**: the supervisor may signal its children — the same link that is
|
||||||
|
already the kill authority. A process may signal itself. Anything broader waits
|
||||||
|
for transferable process handles.
|
||||||
|
- **No binding, no problem**: a process that never calls `signal_bind` is not
|
||||||
|
broken — its signals pend unread and only `process_kill` works on it. Simple
|
||||||
|
programs stay simple; the vocabulary is opt-in, the kill authority is not.
|
||||||
|
|
||||||
|
Because delivery is a message into the process's own event loop, there is no
|
||||||
|
async-signal-safe list in danos: a handler is ordinary code running at a point the
|
||||||
|
process chose. The bug class is gone by construction, not by discipline.
|
||||||
|
|
||||||
|
## The vocabulary: POSIX.1-1990, sorted honestly
|
||||||
|
|
||||||
|
The full 1990 set, and what each becomes. Two intrinsically problematic cases get a
|
||||||
|
defense below the table.
|
||||||
|
|
||||||
|
| POSIX.1-1990 | danos disposition | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| SIGTERM | signal `terminate` | finish up and exit; the supervisor's polite half |
|
||||||
|
| SIGHUP | signal `reload` | re-read configuration / re-scan |
|
||||||
|
| SIGINT | signal `interrupt` | interactive interrupt; meaningful once a console can send it, in the vocabulary now so numbering is stable |
|
||||||
|
| SIGQUIT | signal `quit` | as SIGINT, without the core-dump baggage |
|
||||||
|
| SIGALRM | signal `alarm` | timer expiry as a message; the Unix SIGALRM+`longjmp` timeout hacks are impossible here. In the vocabulary, unbuilt: no consumer yet, and when one appears it is runtime sugar over the existing timer — zero kernel work |
|
||||||
|
| SIGUSR1, SIGUSR2 | signals `user_1`, `user_2` | service-defined |
|
||||||
|
| SIGCHLD | **already exists** — the exit notification | the badge carries the child id, dodging the classic coalescing bug (Unix code must loop `waitpid`) |
|
||||||
|
| SIGKILL | `process_kill` — kernel mechanism | its definition is "cannot be handled"; it was never really a signal |
|
||||||
|
| SIGABRT | exit reason `abort` | `abort()` is synchronous self-termination, not an event |
|
||||||
|
| SIGSEGV, SIGILL, SIGFPE | exit reasons, **never delivered** | see below |
|
||||||
|
| SIGPIPE | **an error return**, not a signal | see below |
|
||||||
|
| SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU, SIGCONT | deferred | job control needs terminals, sessions, and process groups; stop/continue is scheduler territory |
|
||||||
|
|
||||||
|
**The fault signals (SIGSEGV, SIGILL, SIGFPE) are intrinsically wrong for messages.**
|
||||||
|
They are *synchronous* — raised at a specific faulting instruction, not "sometime
|
||||||
|
soon". A message cannot be delivered to a process whose next instruction re-faults;
|
||||||
|
it never reaches its event loop to read it. POSIX only makes fault handlers "work"
|
||||||
|
via the async hijack (run the handler *instead of* the instruction), and even there,
|
||||||
|
returning from a SIGSEGV handler without curing the cause is undefined behavior.
|
||||||
|
danos's architecture already has the better answer: fault → the kernel kills the
|
||||||
|
process ([resilience.md](resilience.md) step 2, built) → the supervisor reads the
|
||||||
|
reason → restart. Recovery is restart, not a handler. This is also truer to the 1990
|
||||||
|
standard than handling is: the standard's default action for all three was
|
||||||
|
"terminate the process".
|
||||||
|
|
||||||
|
**SIGPIPE deserves special contempt.** Its default kills a process that writes to a
|
||||||
|
closed pipe — which is why "the whole server died because one client disconnected"
|
||||||
|
is roughly every network daemon's first production bug, and why every mature codebase
|
||||||
|
contains the same fix: ignore SIGPIPE, handle the `EPIPE` error return. danos made
|
||||||
|
the right choice natively already — a reply owed to a dead peer fails with `-EPEER`.
|
||||||
|
Errors from operations are error returns from those operations. The posix layer can
|
||||||
|
synthesize SIGPIPE for ported code that expects it.
|
||||||
|
|
||||||
|
### Statements, not questions
|
||||||
|
|
||||||
|
A signal and a protocol message both travel over IPC — the difference is the
|
||||||
|
**contract**, not the transport. danos IPC has two primitives, both already in
|
||||||
|
daily use: the **asynchronous notification** (a badge — bits that coalesce into a
|
||||||
|
pending mask; the sender never blocks; no payload, *no reply path*; how IRQs and
|
||||||
|
exit events arrive) and the **synchronous call** (a rendezvous — payload both
|
||||||
|
ways, the caller waits for the reply; how VFS requests work). A signal is the
|
||||||
|
first kind: a *statement*. `terminate` wants no reply — the exit notification is
|
||||||
|
its acknowledgement.
|
||||||
|
|
||||||
|
A health probe is the second kind: a *question*, worthless without its answer —
|
||||||
|
and the answer's absence within a deadline is the very thing being measured.
|
||||||
|
Asked as a signal it has no reply channel (a coalescing bit can't carry an answer,
|
||||||
|
and the authority rule forbids a child signalling its supervisor back); asked as a
|
||||||
|
call, the timeout-is-the-diagnosis semantics come free. So there is no `health`
|
||||||
|
signal. Liveness is the common **`ping`**: a reserved request every harness-run
|
||||||
|
service answers automatically on its main endpoint — still free for the service
|
||||||
|
author, still one obvious way — and a supervisor's probe is a `ping` call with a
|
||||||
|
deadline.
|
||||||
|
|
||||||
|
## The two iron rules
|
||||||
|
|
||||||
|
1. **Cleanup is the kernel's job.** A process can die with no warning — fault,
|
||||||
|
kill, power. Correctness must never depend on a `terminate` handler running. On
|
||||||
|
any death the kernel releases the address space, IPC handles, IRQ bindings, and
|
||||||
|
owed replies (built), and must also release **device, I/O-port, and interrupt
|
||||||
|
claims and MSI vectors** (the known gap in
|
||||||
|
[process-management.md](process-management.md); increment 1). A signal handler is
|
||||||
|
for *graceful* work — flushing, deregistering, saving — never for *necessary*
|
||||||
|
work.
|
||||||
|
2. **Kill is not a signal, and exit reasons are load-bearing.** The standard stop
|
||||||
|
sequence is *terminate → deadline → `process_kill`*; the unhandleable kill stays
|
||||||
|
a kernel mechanism. And a supervisor deciding whether to restart must know *how*
|
||||||
|
the child died: clean exit (meant to — don't restart), fault (restart with
|
||||||
|
backoff), killed (the supervisor did it). The exit notification today carries
|
||||||
|
only the id; it grows a reason. Restart policy cannot be written without it.
|
||||||
|
|
||||||
|
## Who learns of a death
|
||||||
|
|
||||||
|
A death has three audiences, and conflating them is how systems end up with either
|
||||||
|
zombie state or privileged snooping:
|
||||||
|
|
||||||
|
1. **The supervisor** — gets the exit notification on the endpoint it gave at spawn
|
||||||
|
(built), which grows the `ExitReason` (increment 2). The supervisor is the only
|
||||||
|
audience that needs the *reason*, because it is the only one deciding whether to
|
||||||
|
restart.
|
||||||
|
2. **The peer owed a reply** — already built: a client that dies mid-request fails
|
||||||
|
the server's reply with `-EPEER`; a server that dies fails its waiting clients
|
||||||
|
the same way. This covers the *synchronous* case only.
|
||||||
|
3. **The subscribers** — the new piece, and it is the input service's
|
||||||
|
publish/subscribe shape ([input.md](input.md)) applied to exits. A stateful
|
||||||
|
service accumulates per-client state across many requests: the VFS holds a dead
|
||||||
|
client's open file handles, the input service holds its subscriptions, a future
|
||||||
|
network stack holds its sockets. None of these are the client's supervisor, and
|
||||||
|
none learn anything from a failed reply if the client simply never calls again.
|
||||||
|
So the kernel **publishes every exit** to whoever subscribed:
|
||||||
|
`process_subscribe(endpoint)` adds a subscriber, and each death posts a
|
||||||
|
notification to every subscriber (badge = `notify_exit_bit | process id` — the
|
||||||
|
same encoding supervisors already decode, the IRQ-as-IPC pattern once more). The
|
||||||
|
subscriber filters for ids it holds state for and releases what the dead client
|
||||||
|
held. Correlating is free of bookkeeping: an IPC sender's badge already *is* its
|
||||||
|
task id (`runtime.ipc.Received`), so the id a service has been keying client
|
||||||
|
state by all along is the id the exit event carries.
|
||||||
|
|
||||||
|
Subscription, not broadcast-to-everyone: only processes that asked receive
|
||||||
|
events, the kernel keeps a bounded subscriber table, and delivery is the same
|
||||||
|
non-blocking coalescing notification as everything else — a dying process never
|
||||||
|
waits on its mourners. Subscribing is ungated, like `process_enumerate`: what is
|
||||||
|
running (and dying) is not a secret between cooperating processes. Subscribers
|
||||||
|
do not receive the exit reason — the VFS does not care *why* the client died.
|
||||||
|
|
||||||
|
This is the service-side mirror of iron rule 1: **a service must never depend on
|
||||||
|
its clients cleaning up after themselves.** Handle release on client death is the
|
||||||
|
service's job, triggered by the published exit event — never by a courtesy
|
||||||
|
"closing now" message that a crashed client will never send.
|
||||||
|
|
||||||
|
## The stable interface: `runtime.process`
|
||||||
|
|
||||||
|
`runtime.process` already owns what a process receives at birth (`Init`, the
|
||||||
|
argv contract). It grows to own the other end of life.
|
||||||
|
|
||||||
|
**The runtime is the stable interface; the numbers are not.** danos applications do
|
||||||
|
not make system calls — they call the runtime library, and the system-call numbers,
|
||||||
|
notification bits, and signal bit positions beneath it are a **private kernel ↔
|
||||||
|
runtime contract** that may change at any time (settled 2026-07-12). This is why
|
||||||
|
the runtime exists. Today kernel and runtime ship from one tree in one image, so
|
||||||
|
"stability" is simply building them together. When driver binaries start shipping
|
||||||
|
as separately-versioned applications — the whole point of the restart design — the
|
||||||
|
binary's embedded runtime version becomes compatibility metadata (the same idea as
|
||||||
|
the protocol version in the device manager's `hello`), and the kernel refuses what
|
||||||
|
it cannot serve. Signals therefore need no reserved numbering scheme: the enum
|
||||||
|
below is vocabulary, not ABI.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
/// The signal vocabulary. The value is the bit position in the pending mask — a
|
||||||
|
/// private kernel/runtime detail, free to change while they ship together.
|
||||||
|
pub const Signal = enum(u5) {
|
||||||
|
terminate = 0, // SIGTERM: finish up and exit
|
||||||
|
reload = 1, // SIGHUP: re-read configuration
|
||||||
|
interrupt = 2, // SIGINT
|
||||||
|
quit = 3, // SIGQUIT
|
||||||
|
alarm = 4, // SIGALRM
|
||||||
|
user_1 = 5, // SIGUSR1
|
||||||
|
user_2 = 6, // SIGUSR2
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A decoded pending mask: the coalesced set of signals a notification delivered.
|
||||||
|
pub const SignalSet = struct {
|
||||||
|
pending: u32,
|
||||||
|
pub fn has(set: SignalSet, signal: Signal) bool { ... }
|
||||||
|
pub fn iterate(set: SignalSet) Iterator { ... }
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Nominate `endpoint` as this process's signal endpoint (signal_bind). The
|
||||||
|
/// runtime's service harness calls this; a bare program may call it directly and
|
||||||
|
/// fold signals into its own replyWait loop.
|
||||||
|
pub fn bindSignals(endpoint: usize) bool { ... }
|
||||||
|
|
||||||
|
/// Decode a received badge into signals, or null if the badge is not a signal
|
||||||
|
/// notification (mirrors ipc.Received.isChildExit).
|
||||||
|
pub fn signalsFrom(badge: usize) ?SignalSet { ... }
|
||||||
|
|
||||||
|
/// Send `signal` to process `id`. Supervisor-gated, like kill; non-blocking.
|
||||||
|
pub fn sendSignal(id: u32, signal: Signal) bool { ... }
|
||||||
|
|
||||||
|
/// The standard stop sequence: terminate, wait up to `deadline_ms` for the exit
|
||||||
|
/// notification, then process_kill. The one call a supervisor needs.
|
||||||
|
pub fn stop(id: u32, deadline_ms: u64) void { ... }
|
||||||
|
|
||||||
|
/// Subscribe `endpoint` to published exit events (process_subscribe). Every
|
||||||
|
/// process death posts an asynchronous notification: badge = notify_exit_bit |
|
||||||
|
/// process id — the same encoding a supervisor's exit notification uses, decoded
|
||||||
|
/// by the same ipc.Received helpers. For stateful services: release what the dead
|
||||||
|
/// client held (file handles, subscriptions, sockets). Ungated, like
|
||||||
|
/// process_enumerate.
|
||||||
|
pub fn subscribeExits(endpoint: usize) bool { ... }
|
||||||
|
|
||||||
|
/// How a process ended — queried after the exit notification (the kernel records
|
||||||
|
/// it first, so the two never race). What restart policy reads. (Built in M17.2.)
|
||||||
|
pub const ExitReason = enum(u8) {
|
||||||
|
exited, // returned from main / clean exit
|
||||||
|
aborted, // abort() — deliberate self-termination (SIGABRT's ghost; reserved)
|
||||||
|
segmentation_fault, // SIGSEGV's ghost
|
||||||
|
illegal_instruction, // SIGILL's ghost
|
||||||
|
arithmetic_fault, // SIGFPE's ghost
|
||||||
|
protection_fault, // general protection fault
|
||||||
|
fault, // any other CPU exception
|
||||||
|
killed, // process_kill
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Two deliberate absences. There is no `mask`/`block` API — a process that is not
|
||||||
|
ready for a signal simply has not waited on its endpoint yet; the pending mask *is*
|
||||||
|
the blocked set. And there is no per-signal handler registration at this layer —
|
||||||
|
dispatch is the process's own `switch` over `SignalSet`, or the service harness's
|
||||||
|
callbacks (`on_terminate`, `on_reload`) for programs that want defaults.
|
||||||
|
|
||||||
|
### The service harness
|
||||||
|
|
||||||
|
`runtime.service` owns the `replyWait` loop and folds every event source — signals,
|
||||||
|
child exits, protocol messages — into callbacks, with the vocabulary's defaults:
|
||||||
|
`terminate` returns from the loop (clean exit), the common `ping` is answered automatically,
|
||||||
|
`reload` is ignored unless overridden. One loop, no locking, nothing reentrant. A
|
||||||
|
service author writes domain logic; the lifecycle contract is satisfied by the
|
||||||
|
harness. A process that bypasses the harness and ignores its signals meets the
|
||||||
|
deadline-then-kill escalation — you cannot force a process to implement an
|
||||||
|
interface, but you can make compliance free and non-compliance fatal.
|
||||||
|
|
||||||
|
### The musl layer later
|
||||||
|
|
||||||
|
The POSIX C layer is a **musl port**: musl's arch/syscall layer retargeted so that
|
||||||
|
what musl believes are kernel syscalls become danos runtime calls and IPC — `open`
|
||||||
|
and `read` onto the VFS protocol, `kill`/`sigaction`/`waitpid` onto this document's
|
||||||
|
vocabulary, `exit` onto the runtime's exit path. `sigaction` handlers registered
|
||||||
|
through it are invoked by the runtime's loop when the signal message arrives —
|
||||||
|
synchronous underneath, async-looking to ported code, delivered at wait boundaries
|
||||||
|
the way most Unix programs already experience signals (at syscalls). No stack hijack
|
||||||
|
ever happens, `SA_RESTART` semantics come free because nothing was interrupted, and
|
||||||
|
SIGPIPE can be synthesized from `-EPEER` for the programs that expect it. C programs
|
||||||
|
get POSIX; danos-native programs never pay for it.
|
||||||
|
|
||||||
|
## Increments
|
||||||
|
|
||||||
|
1. **Kernel: release device/port/IRQ claims and MSI vectors on death** — the
|
||||||
|
cleanup half of iron rule 1, and the prerequisite for any restart story. Test:
|
||||||
|
kill a claiming driver, spawn it again, the claim succeeds.
|
||||||
|
2. **Exit reason in the death notification** (`ExitReason` above).
|
||||||
|
3. **Exit events**: `process_subscribe` in the kernel (bounded subscriber table,
|
||||||
|
publishes on every death), `runtime.process.subscribeExits`; the VFS becomes the
|
||||||
|
first subscriber — releasing a dead client's handles is its proof test.
|
||||||
|
4. **Signals**: `signal_bind` + `process_signal` + the pending mask in the kernel;
|
||||||
|
`runtime.process` grows the interface above; the service harness handles
|
||||||
|
`terminate` and answers the common `ping`; `stop()` for supervisors.
|
||||||
|
|
||||||
|
[device-manager.md](device-manager.md) builds directly on all four.
|
||||||
|
|
||||||
|
## Settled questions (2026-07-12)
|
||||||
|
|
||||||
|
- **Signal numbering is not ABI**: the runtime is the stable interface; the numbers
|
||||||
|
beneath it are a private kernel ↔ runtime contract (see "The stable interface").
|
||||||
|
- **Liveness is a `ping` call, not a signal**: signals are statements, questions
|
||||||
|
are synchronous calls (see "Statements, not questions"). A service wanting *deep*
|
||||||
|
health ("can I reach my hardware?") defines its own protocol message on top.
|
||||||
|
- **Process handles: deferred.** Pids + the supervisor gate cover everything
|
||||||
|
planned; transferable handles (Fuchsia-style, delegating signalling without
|
||||||
|
delegating kill) wait for the capability table to grow types beyond endpoints.
|
||||||
|
- **`alarm`: in the vocabulary, unbuilt.** No consumer yet; when one appears it is
|
||||||
|
runtime sugar over the existing timer (arm a timer that posts your own signal) —
|
||||||
|
zero kernel work, so deferring costs nothing.
|
||||||
|
- **Subscription granularity: all exits**, subscriber-side filtering — one
|
||||||
|
subscription per service, a bounded kernel table. Per-id subscriptions only if
|
||||||
|
event volume ever matters (hundreds of processes, not before).
|
||||||
|
- **Client identity across the exit boundary: no convention needed** — an IPC
|
||||||
|
sender's badge already is its task id (see "Who learns of a death").
|
||||||
@@ -95,11 +95,18 @@ the architecture layer calls up into `tick`.
|
|||||||
|
|
||||||
## Known gaps (bring-up honesty)
|
## Known gaps (bring-up honesty)
|
||||||
|
|
||||||
- Device **claims** are not released on death (pre-existing: the fault path has
|
- ~~Device claims are not released on death~~ Closed (M17.1): every path out of a
|
||||||
the same gap) — a killed driver's device stays claimed until reboot.
|
process releases its device claims alongside its IRQ and MSI bindings
|
||||||
|
(`releaseTaskResourcesLocked`), so a restarted driver can claim its hardware
|
||||||
|
again — the cleanup half of [process-lifecycle.md](process-lifecycle.md)'s iron
|
||||||
|
rule 1. The `claim-release` test proves the kill → release → re-claim cycle.
|
||||||
- Kernel stacks of dead tasks are leaked, as on every exit path (no reaper yet).
|
- Kernel stacks of dead tasks are leaked, as on every exit path (no reaper yet).
|
||||||
- There is no exit *status* in the notification, only the id; a supervisor that
|
- ~~There is no exit status in the notification~~ Closed (M17.2): the kernel
|
||||||
needs the code can grow a wait-style call later.
|
records how every process ends — exited, a fault class, or killed — before it
|
||||||
|
posts the exit notification, and the supervisor reads it with
|
||||||
|
`process_exit_reason` (`runtime.process.exitReason`). This is the input to
|
||||||
|
restart policy ([process-lifecycle.md](process-lifecycle.md)); an exit *code*
|
||||||
|
for the clean case can still ride alongside later.
|
||||||
- Enumerate writes through the caller's raw pointer under the bring-up trust
|
- Enumerate writes through the caller's raw pointer under the bring-up trust
|
||||||
model, like `device_enumerate` (an unmapped page is a self-DoS, not an
|
model, like `device_enumerate` (an unmapped page is a self-DoS, not an
|
||||||
isolation break).
|
isolation break).
|
||||||
@@ -109,4 +116,5 @@ the architecture layer calls up into `tick`.
|
|||||||
`process-list` (enumerate), `process-kill` (kernel-level kill paths, refusals,
|
`process-list` (enumerate), `process-kill` (kernel-level kill paths, refusals,
|
||||||
notifications), `supervision` (the whole user-side surface via the process-test
|
notifications), `supervision` (the whole user-side surface via the process-test
|
||||||
service: spawn supervised → enumerate → kill blocked and spinning children →
|
service: spawn supervised → enumerate → kill blocked and spinning children →
|
||||||
notifications → gone). See test/qemu_test.py.
|
notifications → gone), `claim-release` (a killed claim-holder's device is
|
||||||
|
claimable again). See test/qemu_test.py.
|
||||||
|
|||||||
+12
-5
@@ -1,10 +1,17 @@
|
|||||||
# Resilience: fault isolation and live restart
|
# Resilience: fault isolation and live restart
|
||||||
|
|
||||||
Steps 1–2 of the ordering below are **built**: user-mode isolation, and fault →
|
Steps 1–4 of the ordering below are **built** (M17–M18, 2026-07-13): user-mode
|
||||||
kill the process → keep the core (`onException` in `system/kernel/kernel.zig`; the
|
isolation; fault → kill the process → keep the core (`onException`; the
|
||||||
`fault-recovery` test proves a crashing ring-3 process dies alone while the system
|
`fault-recovery` test); the supervisor notification **with exit reasons**
|
||||||
keeps running). The supervisor notification and restart policy (steps 3+) are
|
([process-lifecycle.md](process-lifecycle.md) — clean exit, fault class, or
|
||||||
still design. This is the property danos is really chasing:
|
killed, recorded before the notice posts); and the **restart policy itself**
|
||||||
|
([device-manager.md](device-manager.md)): the device manager supervises every
|
||||||
|
driver, restarts crashes with backoff, caps crash loops, and re-claims work
|
||||||
|
because the kernel releases a dead process's claims. The `driver-restart` and
|
||||||
|
`usb-report` scenarios prove kill → release → respawn → re-claim → re-report
|
||||||
|
end to end. What remains of this document's ladder is scope, not mechanism:
|
||||||
|
more of the system moved into restartable processes (the discovery migration,
|
||||||
|
[discovery.md](discovery.md), is the next rung). This is the property danos is really chasing:
|
||||||
**if a part of the OS breaks, isolate it, and re-initialise it — without rebooting.**
|
**if a part of the OS breaks, isolate it, and re-initialise it — without rebooting.**
|
||||||
A crashed driver gets restarted; a wedged service gets killed and brought back. It's
|
A crashed driver gets restarted; a wedged service gets killed and brought back. It's
|
||||||
the reason the [microkernel](vision.md) shape was chosen, and it's a *separate* goal
|
the reason the [microkernel](vision.md) shape was chosen, and it's a *separate* goal
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ Everything else---including`read()`,`write()`,`malloc()`, and`fork()`---will run
|
|||||||
- **What it does:**Used strictly by your background user-space servers (like your disk driver or filesystem). It sends a reply to the last client that called it, and immediately puts the server to sleep until the next request arrives.[[1](https://news.ycombinator.com/item?id=33078441)]
|
- **What it does:**Used strictly by your background user-space servers (like your disk driver or filesystem). It sends a reply to the last client that called it, and immediately puts the server to sleep until the next request arrives.[[1](https://news.ycombinator.com/item?id=33078441)]
|
||||||
3. **`Yield()`/`Thread_Ctrl()`**
|
3. **`Yield()`/`Thread_Ctrl()`**
|
||||||
- **What it does:**Allows a thread to voluntarily give up its CPU time slice, or allows a root task to spawn/kill threads.
|
- **What it does:**Allows a thread to voluntarily give up its CPU time slice, or allows a root task to spawn/kill threads.
|
||||||
|
4. **`ipc_send(endpoint, message_buffer)`(Asynchronous Send)**
|
||||||
|
- **What it does:**Posts a small payload to an endpoint's bounded queue and returns *without* blocking — no rendezvous, no reply. The receiver picks it up through the same `IPC_ReplyWait`, as a buffered message. It is the async counterpart of `IPC_Call`, for one-to-many broadcasts where a synchronous rendezvous would let one dead or slow receiver hang the sender. The [input service](input.md) — keyboard-event fan-out — is its first user. A full queue drops the oldest message (a buffered message is discrete data, unlike a coalescing interrupt notification).
|
||||||
|
|
||||||
* * * * *
|
* * * * *
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
# System Requirements
|
||||||
|
|
||||||
|
Minimum and recommended hardware for running danos. Every requirement below is
|
||||||
|
grounded in what the current code actually assumes at boot — this is a
|
||||||
|
description of the real target, not an aspirational one.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
danos targets a **modern UEFI x86-64 PC with ACPI and PCIe**. The practical
|
||||||
|
minimum is:
|
||||||
|
|
||||||
|
- 64-bit x86-64 CPU with SSE2, APIC, and `syscall`/`sysret`
|
||||||
|
- UEFI firmware (no BIOS / legacy boot)
|
||||||
|
- ACPI tables: MADT, MCFG, FADT
|
||||||
|
- PCIe with an ECAM (MMConfig) window
|
||||||
|
- **128 MiB RAM** (target); see [Memory](#memory) for the breakdown
|
||||||
|
- USB via **xHCI only**
|
||||||
|
|
||||||
|
There is no support for legacy BIOS boot, x2APIC, port-IO PCI configuration, or
|
||||||
|
any USB host controller other than xHCI.
|
||||||
|
|
||||||
|
## Plain-language hardware guide
|
||||||
|
|
||||||
|
If you don't want to cross-reference chipset datasheets, here's roughly what era
|
||||||
|
of PC works. These are **guidance based on when the required features became
|
||||||
|
standard**, not a list of tested machines — the authoritative rules are in the
|
||||||
|
technical sections below.
|
||||||
|
|
||||||
|
The feature that sets the floor is **built-in xHCI USB** (danos supports no other
|
||||||
|
USB controller) combined with **UEFI firmware**. Both became standard on
|
||||||
|
mainstream desktops and laptops around **2012**.
|
||||||
|
|
||||||
|
| | Known-good baseline | Comfortable recommendation |
|
||||||
|
|---|---|---|
|
||||||
|
| **Intel** | 3rd-gen Core "Ivy Bridge" (2012) with a 7-series "Panther Point" chipset — Intel's first chipset with xHCI built in | 6th-gen Core "Skylake" (2015) or newer |
|
||||||
|
| **AMD** | A-series "Llano" APU with an A75 FCH (2011) — the industry's first chipset with built-in xHCI | Any AM4 platform, i.e. Ryzen (2017) or newer |
|
||||||
|
|
||||||
|
**AMD is not behind Intel here — it was first.** AMD's A75 FCH shipped with
|
||||||
|
native xHCI in April 2011, about a year *ahead* of Intel's 7-series (2012); AMD
|
||||||
|
was the first vendor to earn USB-IF certification for chipset-level USB 3.0. The
|
||||||
|
two "comfortable recommendation" dates differ only because they name convenient,
|
||||||
|
long-supported product lines (Skylake, Ryzen) — not because of any USB
|
||||||
|
capability gap. Every AMD desktop platform from the A75 FCH (2011) and FM2/AM3+
|
||||||
|
era onward has built-in xHCI, and any of them qualifies as a baseline.
|
||||||
|
|
||||||
|
Older 64-bit machines (e.g. Intel Core 2, Nehalem, Sandy Bridge) meet the CPU
|
||||||
|
requirements but typically **lack built-in xHCI and/or ship with BIOS instead of
|
||||||
|
UEFI**, so they are not supported.
|
||||||
|
|
||||||
|
### Matching your CPU by name
|
||||||
|
|
||||||
|
If you know your chip's marketing name or codename, find it here. Everything from
|
||||||
|
the **Supported** rows down works; the **Too old** row does not.
|
||||||
|
|
||||||
|
**Intel Core** (the "-lake"/"-bridge"/"-well" codenames):
|
||||||
|
|
||||||
|
| Status | Generation | Codename(s) | Year |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Too old | 2nd gen | Sandy Bridge | 2011 |
|
||||||
|
| Supported (baseline) | 3rd gen | Ivy Bridge | 2012 |
|
||||||
|
| Supported | 4th–5th gen | Haswell, Broadwell | 2013–2014 |
|
||||||
|
| **Recommended** | 6th–9th gen | **Skylake**, Kaby Lake, Coffee Lake | 2015–2018 |
|
||||||
|
| Recommended | 10th–11th gen | Comet Lake, Ice Lake, Tiger Lake, Rocket Lake | 2019–2021 |
|
||||||
|
| Recommended | 12th gen+ | Alder Lake, Raptor Lake | 2021–2023 |
|
||||||
|
| Recommended | Core Ultra | Meteor Lake, Arrow Lake, Lunar Lake | 2023+ |
|
||||||
|
|
||||||
|
**AMD:**
|
||||||
|
|
||||||
|
| Status | Family | Codename(s) | Year |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Supported (baseline) | A-series APU (A75/A85 FCH) | Llano, Trinity, Richland, Kaveri | 2011–2014 |
|
||||||
|
| Supported | FX (AM3+) | Bulldozer, Piledriver | 2011–2012 |
|
||||||
|
| **Recommended** | **Ryzen** 1000–5000 (AM4) | Summit/Pinnacle Ridge, Matisse, Vermeer (Zen–Zen 3) | 2017–2020 |
|
||||||
|
| Recommended | Ryzen 7000+ (AM5) | Raphael, Granite Ridge (Zen 4 / Zen 5) | 2022+ |
|
||||||
|
| Recommended | Threadripper / EPYC | Zen and later | 2017+ |
|
||||||
|
|
||||||
|
(These map generations to the era their platforms shipped built-in xHCI + UEFI;
|
||||||
|
they are guidance, not a tested-hardware list.)
|
||||||
|
|
||||||
|
**Two caveats that matter regardless of CPU:**
|
||||||
|
|
||||||
|
- **Firmware must be UEFI.** Many 2011-era machines could do either UEFI or
|
||||||
|
legacy BIOS — danos needs it set to UEFI. There is no BIOS boot path.
|
||||||
|
- **Input is PS/2 only, for now.** danos does not yet support USB
|
||||||
|
keyboards/mice. This is fine on most **laptops** (their built-in keyboards are
|
||||||
|
wired to a PS/2-style i8042 controller) but means a **desktop with only USB
|
||||||
|
ports** currently has no usable keyboard. USB HID input is planned.
|
||||||
|
|
||||||
|
Virtual machines are the easiest way to meet every requirement: QEMU (with OVMF/
|
||||||
|
UEFI, a `qemu-xhci` controller, and the default Q35 machine type), or any
|
||||||
|
hypervisor configured for UEFI firmware and an xHCI USB controller.
|
||||||
|
|
||||||
|
## CPU / architecture
|
||||||
|
|
||||||
|
| Requirement | Detail | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| **x86-64, 64-bit only** | Kernel and loader are built exclusively for `x86_64`; the loader rejects any non-x86-64 kernel ELF (`error.WrongArchitecture`). | `build.zig:285`, `boot/efi.zig:418` |
|
||||||
|
| **Long mode + PAE + NX** | AP trampoline sets `CR4.PAE`, `EFER.LME`, `EFER.NXE`; NX is used in kernel page-table entries. | `system/kernel/architecture/x86_64/trampoline.s:62` |
|
||||||
|
| **SSE / SSE2** | Baseline: the compiler emits SSE for ordinary struct copies. Trampoline enables `CR4.OSFXSR` + `OSXMMEXCPT` and clears `CR0.EM`. | `build.zig:282`, `trampoline.s:62` |
|
||||||
|
| **`syscall` / `sysret`** | Primary user↔kernel entry path. `EFER.SCE` enabled; `STAR`/`LSTAR`/`SFMASK` programmed per core. (`int 0x80` exists as a parallel gate.) | `architecture/x86_64/per-cpu.zig:59`, `isr.s:169` |
|
||||||
|
| **Local APIC (xAPIC)** | LAPIC accessed via MMIO at `0xFEE00000`. LAPIC ID read as a `u8` — classic xAPIC. **x2APIC is not supported** (no MSR path). | `apic.zig:62`, `apic.zig:414` |
|
||||||
|
| **CPUID + RDTSC** | CPUID leaf `0x15` for TSC frequency; RDTSC is the monotonic clock. | `apic.zig:279`, `apic.zig:84` |
|
||||||
|
| **SMP (optional)** | Multi-core supported via INIT–SIPI–SIPI; ceiling `maximum_cpus = 128`. Single core is fine. Cores beyond the ceiling are parked. | `system/parameters.zig:16`, `apic.zig:144` |
|
||||||
|
|
||||||
|
## Firmware / boot
|
||||||
|
|
||||||
|
- **UEFI only.** A custom UEFI application loader is installed to
|
||||||
|
`\EFI\BOOT\BOOTX64.efi`. There is **no BIOS, multiboot, or limine** path. The
|
||||||
|
loader tolerates UEFI Class-3 machines with no legacy PIC/PIT.
|
||||||
|
(`build.zig:464`, `boot/efi.zig`)
|
||||||
|
- **ACPI is the hardware-discovery mechanism.** The RSDP is taken from the UEFI
|
||||||
|
configuration table (ACPI 2.0 GUID preferred, 1.0 fallback). Without a valid
|
||||||
|
RSDP there is **no device discovery** — no SMP, no IOAPIC routing, no PCI/USB.
|
||||||
|
(`efi.zig:578`, `boot-handoff.zig:144`)
|
||||||
|
- **Required ACPI tables:** MADT (interrupt topology), MCFG (PCIe ECAM base),
|
||||||
|
FADT (power / PM timer). Optionally consumed: HPET, DMAR, SPCR.
|
||||||
|
(`system/devices/acpi.zig:3`)
|
||||||
|
- The loader reads `/system/kernel`, `/system/services/init`, and
|
||||||
|
`/boot/initial-ramdisk.img` off the FAT boot volume. The kernel can boot
|
||||||
|
"kernel-only" without init or the ramdisk. (`efi.zig:14`, `efi.zig:66`)
|
||||||
|
|
||||||
|
## Interrupt controller
|
||||||
|
|
||||||
|
- **Local APIC + I/O APIC required.** I/O APIC base, GSI base, and MADT
|
||||||
|
interrupt-source overrides come from ACPI. (`cpu.zig:365`, `apic.zig:119`)
|
||||||
|
- **MSI supported** — edge-triggered, keyed by vector, no I/O APIC mask cycle.
|
||||||
|
Vector window 33–46, timer on 32, spurious on 47. (`system/kernel/irq.zig:70`,
|
||||||
|
`cpu.zig:397`)
|
||||||
|
- The legacy 8259 PIC is remapped and masked **only if present** (MADT
|
||||||
|
`PCAT_COMPAT`); it is not required. (`apic.zig:103`)
|
||||||
|
|
||||||
|
## PCI / PCIe
|
||||||
|
|
||||||
|
- **PCIe with ECAM (MMConfig) required.** The PCI bus driver maps the host
|
||||||
|
bridge's ECAM window (1 MiB config space per bus) and computes config
|
||||||
|
addresses directly. **There is no legacy CF8/CFC port-IO config path** — the
|
||||||
|
driver bails if the bridge exposes no ECAM window. The ECAM base comes from
|
||||||
|
the ACPI MCFG table. (`system/drivers/pci-bus/pci-bus.zig:41`, `acpi.zig:6`)
|
||||||
|
|
||||||
|
## USB
|
||||||
|
|
||||||
|
- **xHCI only.** The sole USB driver is `usb-xhci-bus`, and the device manager
|
||||||
|
binds it strictly to PCI prog-IF `0x30` (xHCI). UHCI / OHCI / EHCI exist only
|
||||||
|
as report strings with no driver behind them — **USB 1.x/2.0-only controllers
|
||||||
|
are not supported.** (`system/drivers/usb-xhci-bus/`,
|
||||||
|
`system/services/device-manager/device-manager.zig:34`)
|
||||||
|
- USB input (keyboard/mouse over HID) is future work; the current input stack is
|
||||||
|
PS/2. See [Buses & devices](#buses--devices).
|
||||||
|
|
||||||
|
## Timers
|
||||||
|
|
||||||
|
Calibration prefers, in order: (1) CPUID leaf `0x15` TSC frequency, (2) HPET,
|
||||||
|
(3) ACPI PM timer (3.579545 MHz, from FADT), (4) legacy PIT. Any one suffices —
|
||||||
|
HPET/PM-timer/PIT are optional fallbacks when CPUID `0x15` is absent.
|
||||||
|
(`apic.zig:180`)
|
||||||
|
|
||||||
|
- **TSC** — monotonic high-resolution clock.
|
||||||
|
- **LAPIC timer** — scheduler heartbeat, periodic at `timer_hz = 1000 Hz`.
|
||||||
|
(`parameters.zig:39`)
|
||||||
|
|
||||||
|
## Memory
|
||||||
|
|
||||||
|
**Target: 128 MiB RAM.** The system uses 4 KiB pages and a bitmap physical-frame
|
||||||
|
allocator built from the firmware memory map. There is no hardcoded minimum-RAM
|
||||||
|
constant — the allocator only panics if there is no usable region, or none large
|
||||||
|
enough to hold its own bitmap. (`system/kernel/pmm.zig:13`, `pmm.zig:77`)
|
||||||
|
|
||||||
|
Where the budget goes:
|
||||||
|
|
||||||
|
| Consumer | Size | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Kernel heap (cap, grown one page at a time) | up to **64 MiB** | `system/kernel/heap.zig:26` |
|
||||||
|
| Kernel stack, per CPU | 16 KiB | `parameters.zig:26` |
|
||||||
|
| IST stack, per CPU | 16 KiB | `parameters.zig:36` |
|
||||||
|
| User stack, per task | 8 pages / 32 KiB | `parameters.zig:32` |
|
||||||
|
| Max concurrent tasks | 32 | `parameters.zig:23` |
|
||||||
|
| Boot page-table pool | 64 frames / 256 KiB | `efi.zig:299` |
|
||||||
|
|
||||||
|
The 64 MiB heap cap plus kernel image, per-CPU stacks, task stacks, the frame
|
||||||
|
bitmap, and DMA-contiguous allocations fit comfortably within 128 MiB on a
|
||||||
|
single- or low-core-count machine. Very high core counts (toward the 128-CPU
|
||||||
|
ceiling) add per-CPU stack overhead and push toward more RAM.
|
||||||
|
|
||||||
|
**Note on the 4 GiB physmap:** the loader identity-maps and physmaps the low
|
||||||
|
4 GiB of address space with 2 MiB leaves. This is *virtual address* reach, not a
|
||||||
|
RAM requirement — RAM above 4 GiB simply needs an extra mapping window and is not
|
||||||
|
needed to boot. (`efi.zig:305`)
|
||||||
|
|
||||||
|
Virtual-memory layout (`boot-handoff.zig:47`):
|
||||||
|
|
||||||
|
| Region | Base |
|
||||||
|
|---|---|
|
||||||
|
| User space | `0x0000_7000_0000_0000` |
|
||||||
|
| Kernel heap | `0xFFFF_8000_0000_0000` |
|
||||||
|
| Physmap | `0xFFFF_8800_0000_0000` |
|
||||||
|
| Kernel image | `0xFFFF_FFFF_8000_0000` |
|
||||||
|
|
||||||
|
## Buses & devices
|
||||||
|
|
||||||
|
Buses with real drivers today:
|
||||||
|
|
||||||
|
- **PCIe** via ECAM (`pci-bus`)
|
||||||
|
- **xHCI USB** (`usb-xhci-bus`)
|
||||||
|
- **PS/2** keyboard + mouse (`ps2-bus`) — the current input stack
|
||||||
|
- **Serial UART** (16550/16450), configured from the ACPI SPCR table
|
||||||
|
|
||||||
|
**No storage driver exists yet.** AHCI / NVMe / IDE are named for reporting only;
|
||||||
|
there is no block-device driver. Persistent storage is future work.
|
||||||
|
|
||||||
|
## IOMMU
|
||||||
|
|
||||||
|
**Detection only; enforcement deferred.** The ACPI DMAR table is parsed for the
|
||||||
|
first VT-d DRHD unit and its capabilities are exposed via `PlatformInfo`
|
||||||
|
(`iommu_present`, `iommu_base`, `iommu_version`). No DMA-remapping tables are
|
||||||
|
programmed and no translation is enforced. An IOMMU is therefore **not required**
|
||||||
|
and does not currently constrain devices. (`system/devices/acpi.zig:96`)
|
||||||
|
|
||||||
|
## What is explicitly NOT supported
|
||||||
|
|
||||||
|
- Legacy BIOS / multiboot / limine boot
|
||||||
|
- 32-bit x86
|
||||||
|
- x2APIC
|
||||||
|
- Legacy port-IO (CF8/CFC) PCI configuration
|
||||||
|
- Non-xHCI USB (UHCI / OHCI / EHCI)
|
||||||
|
- Machines without ACPI (no device discovery)
|
||||||
|
- Persistent storage (no AHCI / NVMe / IDE driver yet)
|
||||||
|
- USB HID input (PS/2 only for now)
|
||||||
@@ -27,6 +27,15 @@ transcript. Serial is per-architecture (x86 uses port I/O; an ARM board uses a
|
|||||||
memory-mapped UART), so it lives behind the [arch](arch.md) boundary — and adding
|
memory-mapped UART), so it lives behind the [arch](arch.md) boundary — and adding
|
||||||
a new architecture's UART is what makes the same tests run there.
|
a new architecture's UART is what makes the same tests run there.
|
||||||
|
|
||||||
|
The serial log sink is **compiled in only under `-Dserial`** (off by default).
|
||||||
|
A real machine often has no live legacy COM1 — writing to a dead one is slow —
|
||||||
|
and the boot log is kept in a RAM buffer (`klog`) and flushed to disk instead,
|
||||||
|
so serial is now purely a QEMU/dev aid. The harness (`test/qemu_test.py`) builds
|
||||||
|
every case with `-Dserial=true`, and `zig build run-x86-64` boots a serial-enabled
|
||||||
|
image variant, so both get the transcript; a flashable `zig build` image leaves
|
||||||
|
serial out. (Even with `-Dserial`, a loopback probe disables a dead port at boot,
|
||||||
|
so a serial-enabled image is still safe on real hardware.)
|
||||||
|
|
||||||
## In-kernel test cases
|
## In-kernel test cases
|
||||||
|
|
||||||
Building with `-Dtest-case=<name>` makes the kernel, after normal bring-up, run one
|
Building with `-Dtest-case=<name>` makes the kernel, after normal bring-up, run one
|
||||||
|
|||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
# Timers and time
|
||||||
|
|
||||||
|
Two different needs hide under the word "timer", and danos keeps them apart:
|
||||||
|
|
||||||
|
- **Reading the clock** — *what time is it?* A read of a free-running counter.
|
||||||
|
- **Waiting** — *wake me in N milliseconds*, or *notify me when a deadline passes.*
|
||||||
|
|
||||||
|
Both are answered by the **kernel**, because the kernel already owns a timer: it has
|
||||||
|
to, to preempt tasks. The LAPIC heartbeat and the calibrated TSC that back all of this
|
||||||
|
are built in [device-interrupts.md](device-interrupts.md); the scheduler's blocking and
|
||||||
|
wait queues are in [scheduling.md](scheduling.md). This page is about the surface a
|
||||||
|
ring-3 program actually uses, and one deliberate absence: **there is no user-space time
|
||||||
|
service.**
|
||||||
|
|
||||||
|
## Why time is a syscall, not a service
|
||||||
|
|
||||||
|
The tempting microkernel move is to put a timer *driver* in user space and have
|
||||||
|
applications ask it for the time over IPC. For a **monotonic clock that is wrong** —
|
||||||
|
reading `now()` should never cost an IPC round trip. The kernel is already holding the
|
||||||
|
answer: it computes the current time every time it schedules, from the TSC, in a couple
|
||||||
|
of instructions. Surfacing that as a system call is pure mechanism; routing it through a
|
||||||
|
message to another process would be slower *and* redundant, and a device like the HPET
|
||||||
|
(uncacheable MMIO reads) is a particularly bad thing to read on every `now()`.
|
||||||
|
|
||||||
|
This is the same conclusion every serious system reaches: Linux and Zircon read the
|
||||||
|
counter in the vDSO, L4 exposes a clock field in a shared kernel page, seL4 reads the
|
||||||
|
cycle counter directly. None of them make a clock read an IPC. danos makes it a syscall.
|
||||||
|
|
||||||
|
That "from the TSC" hides a portability question, because the TSC is only a valid clock
|
||||||
|
when the CPU guarantees it is *invariant* and when every core's TSC is *synchronized*.
|
||||||
|
danos checks both — the invariant-TSC CPUID bit (`0x80000007` EDX[8], set on Intel and
|
||||||
|
AMD), and a cross-core "warp" check as the cores come up — and falls back to the HPET
|
||||||
|
counter when either fails. So `now()` stays accurate on a real Intel box, a real AMD box,
|
||||||
|
and inside a VM alike; only the source behind it differs. The mechanism is in
|
||||||
|
[device-interrupts.md](device-interrupts.md).
|
||||||
|
|
||||||
|
So the timer hardware lives in the kernel, and there is **no `hpet` driver and no time
|
||||||
|
server** to consume. (An earlier HPET driver existed only to *demonstrate* the driver
|
||||||
|
model; that role now lives in [drivers.md](drivers.md), as documentation.) The one place
|
||||||
|
a user-space time service *is* justified — **wall-clock / calendar time** — is discussed
|
||||||
|
at the end; it is deliberately not built yet.
|
||||||
|
|
||||||
|
## The three system calls
|
||||||
|
|
||||||
|
Time and waiting are three entries in the small syscall table ([syscall.md](syscall.md)):
|
||||||
|
|
||||||
|
- **`clock` (#23)** → monotonic nanoseconds since boot. It only moves forward. Not
|
||||||
|
wall-clock: no date, no timezone. Backed by `architecture.nanos()` (TSC, scaled with a
|
||||||
|
128-bit intermediate so a long uptime can't overflow) — a few nanoseconds of
|
||||||
|
resolution, and just an `rdtsc` plus a multiply.
|
||||||
|
- **`sleep` (#3)** → block the caller for N milliseconds. The scheduler records a wake
|
||||||
|
deadline and the tick sweep wakes it (`scheduler.sleep`).
|
||||||
|
- **`timer_bind` (#31)** → arm a one-shot timer that, after N milliseconds, posts a
|
||||||
|
**timer notification** to an IPC endpoint. Unlike `sleep` it does **not** block: a
|
||||||
|
service can keep answering messages on the same endpoint while a deadline is pending.
|
||||||
|
This is the timed wait that stop-sequence escalation, hello deadlines, and restart
|
||||||
|
backoff are built from ([process-lifecycle.md](process-lifecycle.md),
|
||||||
|
[device-manager.md](device-manager.md)).
|
||||||
|
|
||||||
|
The kernel's own scheduling timer (the LAPIC, vector 32) is never exposed to user space;
|
||||||
|
programs read the TSC through `clock` and get timed wakeups through `sleep`/`timer_bind`,
|
||||||
|
both riding the scheduler tick.
|
||||||
|
|
||||||
|
## `runtime.time` — the generic interface
|
||||||
|
|
||||||
|
Applications don't call the syscalls directly; they use `runtime.time`
|
||||||
|
(`library/runtime/time.zig`), a thin `Instant`/`Duration` layer over them — an ergonomic
|
||||||
|
front door, not new mechanism.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const time = @import("runtime").time;
|
||||||
|
|
||||||
|
const start = time.now(); // Instant — monotonic
|
||||||
|
doWork();
|
||||||
|
const took = start.elapsed(); // Duration
|
||||||
|
time.sleep(time.Duration.fromMillis(5)); // block ~5 ms
|
||||||
|
|
||||||
|
// A deadline delivered as a notification, so a service keeps serving meanwhile:
|
||||||
|
_ = time.after(endpoint, time.Duration.fromMillis(200));
|
||||||
|
```
|
||||||
|
|
||||||
|
- `Duration` is nanoseconds under the hood, with `fromNanos/fromMicros/fromMillis/
|
||||||
|
fromSeconds` and `asNanos/asMillis`. `ceilMillis` rounds *up* to the kernel's
|
||||||
|
millisecond granularity, so a sub-millisecond `sleep` never rounds down to zero and
|
||||||
|
returns early. All arithmetic saturates rather than wraps.
|
||||||
|
- `Instant` is a point on the monotonic clock: `since`, `elapsed`, `plus`, `reached` —
|
||||||
|
built for deadline loops (`while (!deadline.reached()) …`).
|
||||||
|
- `now()` / `monotonicNanos()` wrap `clock`. `available()` reports whether the clock is
|
||||||
|
calibrated at all (the kernel returns 0 until the TSC frequency is known, so a caller
|
||||||
|
that needs real time can treat 0 as "unavailable" rather than assume it advances).
|
||||||
|
- `sleep(d)` wraps `sleep`; `spin(d)` busy-polls `now()` for the sub-millisecond delays
|
||||||
|
the millisecond tick can't express; `after(endpoint, d)` wraps `timer_bind`.
|
||||||
|
|
||||||
|
The raw wrappers (`system.clock`, `system.sleep`, `system.timerOnce`) stay in
|
||||||
|
`library/runtime/system.zig`; `runtime.time` is the layer meant for everyday use.
|
||||||
|
|
||||||
|
## Wall-clock time (not built)
|
||||||
|
|
||||||
|
Everything above is **monotonic**: elapsed time since boot, perfect for timeouts and
|
||||||
|
measurement, useless for "what is the date?" Calendar time — a real-time clock, time
|
||||||
|
zones, leap seconds — is genuinely a **user-space** concern, and it *is* the case a time
|
||||||
|
service is for. It would be backed by an **RTC** driver (the CMOS real-time clock), not
|
||||||
|
the HPET, and exposed as a `CLOCK_REALTIME`-style service alongside the monotonic
|
||||||
|
syscall. It is deferred until something needs it; the monotonic clock the kernel already
|
||||||
|
owns covers every current use.
|
||||||
|
|
||||||
|
## Verifying it
|
||||||
|
|
||||||
|
`runtime.time`'s `Instant`/`Duration` arithmetic has unit tests that run on the host:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ zig build test # includes library/runtime/time.zig
|
||||||
|
```
|
||||||
|
|
||||||
|
End to end, the proof the clock is real is that it *advances*: read `now()`, `sleep` a
|
||||||
|
`Duration`, read `now()` again, and the second reading is later — the kernel's timer
|
||||||
|
driving a ring-3 program with no service in between.
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
# Running Zig on danos: the self-hosting roadmap
|
||||||
|
|
||||||
|
A design note (not built yet) on the path to making danos a **real Zig target** — a
|
||||||
|
target you can name (`-target x86_64-danos`) and, eventually, run the Zig compiler
|
||||||
|
itself on. It is forward-looking, like [vision.md](vision.md): it sets a direction
|
||||||
|
and the decisions that follow from it, so the code we write now bends toward it
|
||||||
|
instead of away.
|
||||||
|
|
||||||
|
This note deliberately does **not** cover a text editor or terminal. Those are
|
||||||
|
easier (single-process, I/O-bound) and fall out of the early phases here almost for
|
||||||
|
free; the hard, shaping problem is the standard-library surface, so that is what
|
||||||
|
this roadmap is about.
|
||||||
|
|
||||||
|
The analysis behind it was done against **Zig 0.16** (the pinned toolchain). Zig's
|
||||||
|
standard library moves between releases — especially the parts described here — so
|
||||||
|
treat upstream references as "the shape in 0.16.x," and expect to re-check them on a
|
||||||
|
toolchain bump.
|
||||||
|
|
||||||
|
## The win condition
|
||||||
|
|
||||||
|
danos runs the Zig compiler when a bare
|
||||||
|
|
||||||
|
```
|
||||||
|
zig build-exe hello.zig
|
||||||
|
```
|
||||||
|
|
||||||
|
completes **on danos** and produces a runnable danos binary. Note the milestone is
|
||||||
|
`build-exe`, not `zig build`: the `zig build` runner spawns child processes (the
|
||||||
|
build steps), which needs a whole process-control surface danos does not have yet.
|
||||||
|
A single `build-exe` needs none of that (see Phase 3). Reaching `build-exe` is
|
||||||
|
"self-hosting"; reaching `zig build` is a later, separate lift.
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- **No Linux syscall/ABI emulation.** danos will not implement the Linux `syscall`
|
||||||
|
interface so that stock `x86_64-linux` binaries run. That is a permanent
|
||||||
|
compatibility treadmill and it inverts the microkernel design — explicitly out.
|
||||||
|
- **No musl port yet.** A musl libc port is a reasonable *later* effort (it unlocks
|
||||||
|
the C ecosystem), but it is not on the critical path to Zig-on-danos, and it is
|
||||||
|
deferred. The roadmap below is arranged so the work still pays off if musl ever
|
||||||
|
happens (see "The same surface, twice").
|
||||||
|
- **Editor/terminal are out of scope for this note** (they are downstream of Phase 1).
|
||||||
|
|
||||||
|
**On FFI.** Foreign-function interop splits the same way as the doors below. Zig-level
|
||||||
|
and C-ABI-*exposing* FFI (`extern`, `callconv(.c)`, C-ABI structs) work on a real target
|
||||||
|
immediately — and the `std.os.danos` seam is C-ABI-shaped by construction, so it is
|
||||||
|
FFI-friendly from the start. *Consuming* C libraries (`@cImport`, linking archives) is
|
||||||
|
the part that needs a libc + headers, i.e. the deferred musl door. So an eventual FFI
|
||||||
|
need reinforces keeping that door open; it does not change the plan.
|
||||||
|
|
||||||
|
## The realization that shapes everything: 0.16 gives us *one* seam
|
||||||
|
|
||||||
|
The instinct "to target Zig we'd have to reimplement all the `std` namespaces" was
|
||||||
|
how older Zig worked. Zig 0.16 (post-"writergate") is far kinder:
|
||||||
|
|
||||||
|
- **`std.fs` is essentially gone.** It is now path helpers plus deprecated aliases;
|
||||||
|
there is no `std.fs.File`, `std.fs.Dir`, or `std.fs.cwd()`. File and directory
|
||||||
|
work goes through **`std.Io`** — a single runtime **vtable** (`Io.zig`) of
|
||||||
|
function pointers handed to `main` as `std.process.Init.io`. `std.Io.File` and
|
||||||
|
`std.Io.Dir` are thin forwarders to that vtable. `Io.zig` and the `fs` shim carry
|
||||||
|
**zero** per-OS branches.
|
||||||
|
- **`std.posix` is one generic body** parameterised over a single `system` module.
|
||||||
|
With no libc, `system` resolves **per target OS**: `.linux => std.os.linux`,
|
||||||
|
`.plan9 => std.os.plan9`, and so on. The generic `std.posix.read`/`write`/`open`
|
||||||
|
bodies are just `system.read(...)` plus an errno switch — *identical for every
|
||||||
|
OS*. The only variable is what `system` binds to.
|
||||||
|
- **`std.os.<tag>`** (e.g. `std/os/linux.zig`) is therefore the real porting seam: a
|
||||||
|
low-level, C-ABI-shaped module of `read/write/open/close/lseek/mmap/clock/exit/…`
|
||||||
|
plus an `errno` enum and the constant tables (`O_*`, `CLOCK_*`, `S_*`).
|
||||||
|
|
||||||
|
Put together: **to port danos we write `std.os.danos` once** — the ~30-operation
|
||||||
|
seam — and the whole `std.posix` / `std.fs` / `std.Io` tower above it lights up
|
||||||
|
generically, because none of it branches on the OS. That is a dramatically smaller
|
||||||
|
and more contained target than "reimplement the namespaces."
|
||||||
|
|
||||||
|
## Three doors, and why we take the first
|
||||||
|
|
||||||
|
| Door | What it is | Verdict |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| **1. Implement the std seam** (`std.os.danos`) | Write the ~30-op `system` module over danos's native ABI + VFS; the generic std tower lights up. | **Take this.** The only door that touches neither C nor the Linux ABI. |
|
||||||
|
| **2. Port musl** | Port musl libc to danos, link Zig against it. | Defer. Good later for the *C* ecosystem; barely helps *Zig* (std only uses libc on the libc-linked path). |
|
||||||
|
| **3. Emulate the Linux ABI** | Implement Linux syscalls so stock linux binaries run. | Reject. Bottomless compatibility treadmill; against the design. |
|
||||||
|
|
||||||
|
### The same surface, twice
|
||||||
|
|
||||||
|
Doors 1 and 2 are the **same native surface at different layers**. `std.posix.read`
|
||||||
|
is `system.read(...)` + an errno switch *regardless of OS* — the only question is
|
||||||
|
whether `system` is **`std.os.danos` (Zig)** or **musl (C)**. Either way, the set of
|
||||||
|
danos-facing operations you must implement is the *same* ~30 ops, all bottoming out
|
||||||
|
in danos's native syscalls + the VFS/FAT server.
|
||||||
|
|
||||||
|
So the runtime work below is **not throwaway** if musl ever happens: you are building
|
||||||
|
the danos-native implementations of that surface either way. Door 1 just packages
|
||||||
|
them as Zig; a future musl re-uses the identical kernel/VFS operations underneath. The
|
||||||
|
two symmetries worth keeping in mind: doors 1 and 2 converge at the **top** (identical
|
||||||
|
POSIX surface); doors 2 and 3 converge at the **bottom** (unmodified musl needs the
|
||||||
|
Linux syscall ABI). Door 1 is the only one that avoids both C and Linux.
|
||||||
|
|
||||||
|
### A fork is table stakes — for any door
|
||||||
|
|
||||||
|
`std.Target.Os.Tag` is a **closed enum** baked into the compiler binary *and* into
|
||||||
|
the `std` linked with every program; `-target x86_64-danos` resolves through it. So
|
||||||
|
adding `danos` as a name requires patching and rebuilding the compiler — even the
|
||||||
|
musl door needs this. "Fork Zig" is therefore not an extra cost unique to door 1; it
|
||||||
|
is the price of admission for *any* real target. What door 1 adds on top is small and
|
||||||
|
localised (below).
|
||||||
|
|
||||||
|
## The architecture decision: `runtime.os` + `runtime.fs`, and retire `posix`
|
||||||
|
|
||||||
|
danos already has the right split ([the private-ABI boundary](../README.md)): the
|
||||||
|
kernel exposes a minimal syscall ABI ([syscall.md](syscall.md)); the **`runtime`**
|
||||||
|
library is the stable, danos-native application ABI. What this roadmap adds:
|
||||||
|
|
||||||
|
- **`runtime.os` — the seam.** A C-ABI-shaped module of the ~30 operations
|
||||||
|
(`read/write/open/close/lseek/mmap/munmap/clock/exit/…`) + an errno enum + the
|
||||||
|
constant tables, each backed by danos's native syscalls and the VFS. **Structure it
|
||||||
|
to mirror `std/os/linux.zig`.** This is the load-bearing, *non-throwaway* artifact:
|
||||||
|
when we fork Zig, `runtime.os` is copy-pasted (near-verbatim) into `std.os.danos`.
|
||||||
|
- **`runtime.fs` — the thin native file API** danos programs use *today*, layered
|
||||||
|
over `runtime.os`. It is also the concrete backing for the `std.Io` vtable's
|
||||||
|
file-write entry once we're a real target, which is why program stdout, diagnostics,
|
||||||
|
and file writes should all be *decided once at that seam* rather than as bespoke
|
||||||
|
per-call helpers (see "How this informs decisions now").
|
||||||
|
|
||||||
|
**Do not hand-mirror the high-level std namespaces.** `std.fs`/`std.Io`/`std.process`
|
||||||
|
are generic and OS-agnostic; once `std.os.danos` exists and we fork, upstream *gives*
|
||||||
|
them to danos for free. Hand-writing `runtime.std.fs` to imitate them would be
|
||||||
|
redundant the day the fork works, and it would chase a moving target (0.16's `std.Io`
|
||||||
|
is large and still shifting). Build the seam well; take the tower for free.
|
||||||
|
|
||||||
|
**Why not a library called `std`?** Because `@import("std")` resolves to the
|
||||||
|
compiler-provided standard library; a user module named `std` would *shadow* it for
|
||||||
|
anything that imports it that way. That is the real reason the seam lives *inside* a
|
||||||
|
forked std as `std/os/danos.zig`, not as a `runtime.std` library — and why danos's end
|
||||||
|
state (`@import("std")` just working, and knowing danos) is the most natively Zig it can
|
||||||
|
be. `runtime.os` is only the interim staging ground: developed against the stock
|
||||||
|
toolchain so Phase 1 need not wait on the fork, then promoted near-verbatim into the
|
||||||
|
fork's `std/os/danos.zig`.
|
||||||
|
|
||||||
|
### Retire `library/posix`
|
||||||
|
|
||||||
|
The `posix` compatibility layer (`unistd`, `stdio`) was the right instinct too early.
|
||||||
|
Its whole value is POSIX *spellings* for POSIX software — and danos has no POSIX
|
||||||
|
software; every current caller is danos-native code that could use `runtime.fs`
|
||||||
|
directly. The real POSIX story arrives later and from elsewhere (musl, or upstream
|
||||||
|
`std`'s own posix over `std.os.danos`), which supersedes a hand-rolled shim. So it is
|
||||||
|
premature abstraction that adds a "which layer do I use?" fork with no payoff yet.
|
||||||
|
|
||||||
|
Its footprint is tiny: **five** call sites, all `unistd` file operations —
|
||||||
|
`system/services/fat/fat.zig` (`mount`), the `vfs-test` and `fat-test` clients, and
|
||||||
|
(from the boot-log work) `init.zig` and `log-flush.zig`. `stdio.zig` is dead — nothing
|
||||||
|
imports it. The plan: build `runtime.fs`, migrate those five to it, delete
|
||||||
|
`library/posix/`, and drop the `posix` module from `build.zig`'s `addUserBinary`.
|
||||||
|
|
||||||
|
## Where danos stands: coverage vs. the gaps
|
||||||
|
|
||||||
|
What the seam needs, and what danos already provides:
|
||||||
|
|
||||||
|
| std need | danos today | Gap |
|
||||||
|
|----------|-------------|-----|
|
||||||
|
| open / read / write / close / lseek | VFS (via the current `unistd`, → `runtime.fs`) | none — repackage |
|
||||||
|
| directory read (`getdents`) | VFS `readdir` | none — repackage |
|
||||||
|
| mmap / munmap | native syscalls ([abi.zig](../system/abi.zig)) | none |
|
||||||
|
| page allocator | over `mmap`, via `root.os.heap.page_allocator` override | ~30-line hook |
|
||||||
|
| monotonic clock | `clock` syscall | none |
|
||||||
|
| args / argv | SysV entry stack ([sysv.md](sysv.md)), `runtime.process.Init` | none |
|
||||||
|
| stdout / stderr | `debug_write` today | wire fd 1/2 to a console **byte** stream |
|
||||||
|
| mkdir / unlink / rename / truncate | done — engine + VFS + `runtime.fs` (Phase 2) | — |
|
||||||
|
| stat fields | `{size, kind, mtime}` | **mode / inode** still missing (cache validity) |
|
||||||
|
| wall-clock / realtime | done — `wall_clock` syscall (CMOS RTC, Phase 2d) | — |
|
||||||
|
| **environment variables** | `Init` has no env field | missing (can start empty) |
|
||||||
|
| **cwd / chdir** | paths are absolute or bare | missing (no cwd anchor) |
|
||||||
|
| **entropy / random** | — | missing (needed behind `vtable.random`) |
|
||||||
|
| process spawn + exit status | `system_spawn` starts a *named ramdisk binary*; `ExitReason` is a *category* | no exec-of-path, no numeric `WEXITSTATUS` |
|
||||||
|
| threads | one thread per process | avoided via `-fsingle-threaded` (below) |
|
||||||
|
| symlinks | `NodeKind` has the tag; unimplemented | low priority |
|
||||||
|
|
||||||
|
The clustering is clear: reads and memory are basically done; the real work is
|
||||||
|
**filesystem mutation + richer stat + wall-clock**, and a few small seam pieces
|
||||||
|
(page-allocator hook, stdio bytes, entropy). Process spawning and threads are
|
||||||
|
side-stepped entirely for a single `build-exe`.
|
||||||
|
|
||||||
|
## The roadmap
|
||||||
|
|
||||||
|
### Phase 0 — Make `danos` a real target
|
||||||
|
|
||||||
|
**Host, target, self-host — keep the three roles straight.** The *host* is where the
|
||||||
|
compiler runs (your mac + linux dev machines); the *target* is what it emits (`danos`);
|
||||||
|
and eventually danos becomes a host too (self-hosting — the win condition). So the move
|
||||||
|
is: fork the compiler, build it **for** your dev hosts, and teach it to **cross-compile
|
||||||
|
to** danos. You already do this — danos is cross-compiled `freestanding` from your dev
|
||||||
|
host today; Phase 0 swaps that `freestanding` target for a real `x86_64-danos` one, which
|
||||||
|
is what unlocks the native `std`.
|
||||||
|
|
||||||
|
**Why a compiler fork, not just a `--zig-lib-dir` override.** `std.Target.Os.Tag` is a
|
||||||
|
*closed enum compiled into the compiler binary*, so `-target x86_64-danos` will not even
|
||||||
|
parse unless the compiler itself knows the tag. Overriding the std lib directory alone
|
||||||
|
cannot add a target — and there is no libc-only shortcut (a future musl needs the same
|
||||||
|
patch). The only alternative, staying on `freestanding` + hand-shims, is exactly the
|
||||||
|
non-native feel we are leaving: `@import("std")` there is stubbed, not real.
|
||||||
|
|
||||||
|
**The fork.** Clone `ziglang/zig` at the pinned 0.16 tag; build it with a stock
|
||||||
|
same-version `zig` (`zig build` in the tree — a standard, LLVM-pulling, roughly one-time
|
||||||
|
build); point danos's `build.zig`/CI at the resulting binary. Four localised patches:
|
||||||
|
|
||||||
|
- add `danos` to `std.Target.Os.Tag`, in the "no version range" group alongside
|
||||||
|
plan9/serenity;
|
||||||
|
- add `danos` to the freestanding/other **no-op `_start` list** in `std`'s `start.zig`,
|
||||||
|
so std does *not* emit its own System-V `_start` — danos keeps owning the entry shim
|
||||||
|
and `Init`/argv construction it already builds ([sysv.md](sysv.md));
|
||||||
|
- wire the `system` selector `.danos => std.os.danos` in `std.posix`;
|
||||||
|
- add `std/os/danos.zig` — **the seam itself**, promoted near-verbatim from the
|
||||||
|
`runtime.os` developed first in Phase 1 (against the stock toolchain, so the fork is
|
||||||
|
not a prerequisite for starting).
|
||||||
|
|
||||||
|
This is the fork treadmill we accept once. Keep the patch set tiny and `else`-friendly,
|
||||||
|
pin to one 0.16.x, and rebase on point releases.
|
||||||
|
|
||||||
|
### Phase 1 — `runtime.os` read-side + allocator + stdio + cwd; retire `posix`
|
||||||
|
|
||||||
|
Author `runtime.os` (→ `std.os.danos`): the `errno` enum, the constant tables, and
|
||||||
|
the C-convention `read / write / open / openat / close / lseek / mmap / munmap /
|
||||||
|
exit`, each returning result-or-`-errno`. Most backing already exists (VFS + native
|
||||||
|
mmap + clock).
|
||||||
|
|
||||||
|
- Provide `page_allocator` via `root.os.heap.page_allocator` (a thin override over
|
||||||
|
danos `mmap`). This sits **outside** the `std.Io` vtable, so it is wired separately.
|
||||||
|
- Wire fd 0/1/2 to a console **byte** stream (today output only reaches `debug_write`;
|
||||||
|
input is structured `InputEvent` IPC — a byte tty is a new, small thing in both
|
||||||
|
directions).
|
||||||
|
- Add a `getcwd`/`chdir` anchor so `std.fs.cwd()`-style resolution has something to
|
||||||
|
resolve against.
|
||||||
|
- Build `runtime.fs` over `runtime.os`; migrate the five `posix` callers to it; delete
|
||||||
|
`library/posix/` and drop its build module.
|
||||||
|
|
||||||
|
After Phase 1, the surface an editor or terminal needs (open/read/write/close/lseek/
|
||||||
|
readdir/isatty/args/exit) exists. Those are downstream and out of scope here.
|
||||||
|
|
||||||
|
### Phase 2 — Filesystem mutation + real stat (the compiler's cache tower)
|
||||||
|
|
||||||
|
danos's biggest genuine gap, and the correctness-critical one:
|
||||||
|
|
||||||
|
- Add **mkdir / unlink / rename / truncate** to *both* the VFS wire protocol
|
||||||
|
([protocol.zig](../system/services/vfs/protocol.zig)) and the FAT engine
|
||||||
|
([engine.zig](../system/services/fat/engine.zig)), then expose them via `runtime.os`.
|
||||||
|
- Extend `stat` beyond `{size, kind}` to carry **mtime + inode + mode** — `std`'s file
|
||||||
|
stat needs them for build-cache validity — which in turn needs **wall-clock** time
|
||||||
|
(danos is monotonic-only today; an RTC/time service is the dependency).
|
||||||
|
|
||||||
|
Because `std.fs`/`std.Io` have no per-OS branches, finishing this in `runtime.os`
|
||||||
|
lights up the whole file tower for the compiler at once. Environment can stay an empty
|
||||||
|
map until the kernel populates a non-empty `envp`.
|
||||||
|
|
||||||
|
**Status — Phase 2 complete.** `truncate` (O_TRUNC, closing the boot-log stale-tail
|
||||||
|
bug), `mkdir`, `unlink`, and `rename` are all wired through the FAT engine, the VFS
|
||||||
|
protocol + router, and `runtime.fs` (`makeDirectory` / `remove` / `rename`) —
|
||||||
|
host-tested and QEMU-tested (`fat-mutations` + `fat-rename` make a directory, write+read
|
||||||
|
a file in it, rename it, then remove it through the mount). `removeFile` and `rename`
|
||||||
|
are LFN-aware; `rename` is same-directory + 8.3 (cross-directory and long-name-
|
||||||
|
preserving rename are noted limitations). Wall-clock is now a kernel syscall
|
||||||
|
(`wall_clock`, a CMOS-RTC read anchored to the monotonic clock), and the FAT engine
|
||||||
|
stamps and reports **mtime** — `stat` / `runtime.fs.Attributes` carry a real
|
||||||
|
modification time (the `fat-mtime` case reads it back within seconds of the host clock).
|
||||||
|
The remaining `stat` fields, `mode`/`inode`, are deferred (not needed until the
|
||||||
|
compiler's cache layer wants them). **Everything past here is gated on Phase 0 (the
|
||||||
|
fork):** the `runtime.os` seam, `cwd`, stdio-as-fds, and the compiler bring-up.
|
||||||
|
|
||||||
|
### Phase 3 — Single-threaded, self-linked compiler bring-up
|
||||||
|
|
||||||
|
Build the compiler with **two load-bearing flags**:
|
||||||
|
|
||||||
|
- **`-fsingle-threaded`** removes `std.Thread` entirely — `Thread.spawn` is a hard
|
||||||
|
compile error under it, and `std.Io`'s threaded backend runs inline. danos being
|
||||||
|
one-thread-per-process is therefore **not** a blocker. Parallel codegen is a
|
||||||
|
throughput optimisation, not a correctness requirement.
|
||||||
|
- **`-fno-llvm -fno-lld`** keeps codegen and linking **in-process** (the self-hosted
|
||||||
|
x86-64 backend + self-linker), so a single `build-exe` **never forks a child**. That
|
||||||
|
is what lets us defer the entire spawn/exec/wait surface.
|
||||||
|
|
||||||
|
Then supply the few remaining seam pieces: `now` (wrap the danos clock), an entropy
|
||||||
|
source behind `vtable.random` (`randomSecure` can alias it initially — low volume, for
|
||||||
|
temp-file names and hashmap seeds), and the Phase-2 mkdir/rename/unlink for cache dir
|
||||||
|
trees and atomic temp-then-rename output.
|
||||||
|
|
||||||
|
**Explicitly deferred** (not on the `build-exe` path): child-process spawn/exec (only
|
||||||
|
`zig build` and external tools need it), `std.Thread`, `fsync` (FAT is write-through
|
||||||
|
today), symlinks, and musl.
|
||||||
|
|
||||||
|
## Risks and gotchas
|
||||||
|
|
||||||
|
- **The std-fork rebase treadmill is the main ongoing cost.** A new OS tag touches the
|
||||||
|
same broad file set plan9/serenity touch (hundreds of `native_os` sites, plus
|
||||||
|
"unsupported OS" `@compileError` dead-ends a new tag must be routed around), and the
|
||||||
|
entire `std.Io` layer is new in 0.16 and still moving. Stay pinned to one 0.16.x,
|
||||||
|
keep additions localised and `else`-friendly. Watch the closed-enum gotcha: adding
|
||||||
|
`danos` to `Os.Tag` can break existing *exhaustive* switches that lack an `else`, so
|
||||||
|
expect to touch switch sites beyond the ones you implement.
|
||||||
|
- **Single-threaded is load-bearing.** The "no `std.Thread`" simplification rests
|
||||||
|
entirely on `-fsingle-threaded`. If a dependency or flag flips threading back on, you
|
||||||
|
inherit an unescapable compile error (no root-hook exists) — the only outs are a full
|
||||||
|
thread-impl fork or linking libc for pthreads. Keep `single_threaded` asserted end to
|
||||||
|
end.
|
||||||
|
- **In-process linking is load-bearing.** Reaching the compiler without fork/exec
|
||||||
|
depends on `-fno-llvm -fno-lld`. The moment you shell out to LLD/`ld`, you need the
|
||||||
|
full `spawn`/`wait` surface — the hardest microkernel piece — and danos's
|
||||||
|
`system_spawn` only starts a *named ramdisk binary*, not exec of an arbitrary path.
|
||||||
|
Verify the self-hosted backend covers the target output before assuming child
|
||||||
|
processes are optional.
|
||||||
|
- **The shim cannot host the compiler.** danos's current `runtime`/`posix` is fine for
|
||||||
|
danos's *own* native programs, but the compiler `import`s *upstream* `std`, which on
|
||||||
|
a non-target hits the void `system` stub. So the compiler forces the real target
|
||||||
|
(Phase 0's fork). Do not over-invest in extending the hand-shim for compiler
|
||||||
|
purposes; put that effort into `runtime.os` + the VFS/FAT operations, which both the
|
||||||
|
fork *and* a future musl consume.
|
||||||
|
- **`"w"`/`O_CREAT` does not truncate — a silent-corruption bug on this road.** The FAT
|
||||||
|
engine's `writeFile` only *grows* `node.size`, so overwriting a shorter file leaves
|
||||||
|
trailing garbage. Harmless for the boot log today, but for a compiler it means
|
||||||
|
**corrupt `.o`/cache files that look like nondeterministic compiler bugs.** Land
|
||||||
|
`truncate` (Phase 2) before the compiler ever writes cache.
|
||||||
|
- **Exit status is categorical, not numeric.** `process_exit_reason` returns an
|
||||||
|
`ExitReason` *category*, not a numeric code (`WEXITSTATUS`). Fine while spawn is
|
||||||
|
stubbed; the day `zig build` or external tools arrive, plan a kernel exit-record
|
||||||
|
extension — do not let it surprise you.
|
||||||
|
|
||||||
|
## How this informs decisions now
|
||||||
|
|
||||||
|
Two current decisions fall out of this roadmap:
|
||||||
|
|
||||||
|
1. **The `runtime.fs` / `std.Io` question resolves at the vtable seam.** Because 0.16
|
||||||
|
routes *all* output through the `std.Io` vtable's file-write entry, and stdout/stderr
|
||||||
|
are just `File`s with well-known handles, build `runtime.fs` (and the console stdout)
|
||||||
|
as the concrete backing for that entry — not as a bespoke `std.Io.Writer`-only shim.
|
||||||
|
Decide it once, at the seam, and program stdout, diagnostics, and file writes all
|
||||||
|
flow through the same danos VFS/console path.
|
||||||
|
2. **The boot-log `truncate` caveat is now fixed** (Phase 2a). It was the same
|
||||||
|
`writeFile`-only-grows gap that on the self-hosting road would corrupt build output;
|
||||||
|
`engine.truncate` + an O_TRUNC open flag now free the old chain so a shorter rewrite
|
||||||
|
leaves no stale tail, and the boot-log flush opens with it.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [vision.md](vision.md) — the north star this serves.
|
||||||
|
- [syscall.md](syscall.md) — the kernel↔runtime ABI `runtime.os` is built on.
|
||||||
|
- [sysv.md](sysv.md) — the entry stack (`argc/argv/envp/auxv`) danos already constructs.
|
||||||
|
- [ipc.md](ipc.md) — the IPC the VFS/FAT operations travel over.
|
||||||
|
- [danos-file-system-hierarchy-FSH.md](danos-file-system-hierarchy-FSH.md) — the
|
||||||
|
filesystem layout the file surface serves.
|
||||||
|
- [coding-standards.md](coding-standards.md) — danos naming (why the compat spellings
|
||||||
|
are confined, and now retired).
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//! DanOS's POSIX / C compatibility layer — `unistd`, `stdio`, and (later) the C
|
|
||||||
//! `errno` / `struct stat` / `extern "C"` surface. This is the *one* place POSIX and
|
|
||||||
//! C spellings are allowed to appear verbatim (see docs/coding-standards.md): a file
|
|
||||||
//! under library/posix/ *is* the foreign ABI, so it keeps the ABI's names. Everything
|
|
||||||
//! it touches on the danos side (the VFS protocol, the runtime) uses danos names,
|
|
||||||
//! which this layer translates to at the boundary.
|
|
||||||
//!
|
|
||||||
//! It is layered strictly *over* the runtime: it calls the runtime's IPC and heap,
|
|
||||||
//! never the kernel's system calls directly. danos-native applications use the
|
|
||||||
//! runtime; this exists so *POSIX* software can too.
|
|
||||||
|
|
||||||
pub const unistd = @import("unistd.zig");
|
|
||||||
pub const stdio = @import("stdio.zig");
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
//! A small C stdio layer over the POSIX-style file API (unistd.zig). Unbuffered
|
|
||||||
//! for now — each fread/fwrite is one VFS round trip; an internal buffer (fewer
|
|
||||||
//! IPC calls) is a later optimisation. Both a Zig-callable API and `extern "C"`
|
|
||||||
//! symbols are provided, so Zig and future C programs share it.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const unistd = @import("unistd.zig");
|
|
||||||
const heap = @import("runtime").heap;
|
|
||||||
|
|
||||||
pub const SEEK_SET = unistd.SEEK_SET;
|
|
||||||
pub const SEEK_CURRENT = unistd.SEEK_CURRENT;
|
|
||||||
pub const SEEK_END = unistd.SEEK_END;
|
|
||||||
|
|
||||||
/// A C `FILE`: an fd plus sticky end-of-file / error flags. Allocated on the
|
|
||||||
/// heap; `fclose` frees it.
|
|
||||||
pub const FILE = extern struct {
|
|
||||||
fd: i32,
|
|
||||||
eof: c_int = 0,
|
|
||||||
err: c_int = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn flagsFor(mode: []const u8) u32 {
|
|
||||||
if (mode.len == 0) return 0;
|
|
||||||
return switch (mode[0]) {
|
|
||||||
'w', 'a' => unistd.O_CREAT,
|
|
||||||
else => 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open `path` in `mode` ("r"/"w"/"a", '+' ignored for now). Returns null on error.
|
|
||||||
pub fn fopen(path: []const u8, mode: []const u8) ?*FILE {
|
|
||||||
const fd = unistd.open(path, flagsFor(mode));
|
|
||||||
if (fd < 0) return null;
|
|
||||||
const f = heap.allocator().create(FILE) catch {
|
|
||||||
unistd.close(fd);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
f.* = .{ .fd = fd };
|
|
||||||
if (mode.len > 0 and mode[0] == 'a') _ = unistd.lseek(fd, 0, unistd.SEEK_END);
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fclose(f: *FILE) c_int {
|
|
||||||
unistd.close(f.fd);
|
|
||||||
heap.allocator().destroy(f);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read `size*nmemb` bytes; returns the number of whole items read.
|
|
||||||
pub fn fread(buffer: []u8, size: usize, nmemb: usize, f: *FILE) usize {
|
|
||||||
const total = size * nmemb;
|
|
||||||
if (total == 0) return 0;
|
|
||||||
const n = unistd.read(f.fd, buffer[0..@min(buffer.len, total)]);
|
|
||||||
if (n <= 0) {
|
|
||||||
f.eof = 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return @as(usize, @intCast(n)) / size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write `size*nmemb` bytes; returns the number of whole items written.
|
|
||||||
pub fn fwrite(data: []const u8, size: usize, nmemb: usize, f: *FILE) usize {
|
|
||||||
const total = @min(data.len, size * nmemb);
|
|
||||||
if (total == 0) return 0;
|
|
||||||
const n = unistd.write(f.fd, data[0..total]);
|
|
||||||
if (n <= 0) {
|
|
||||||
f.err = 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return @as(usize, @intCast(n)) / size;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fseek(f: *FILE, off: i64, whence: u32) c_int {
|
|
||||||
f.eof = 0;
|
|
||||||
return if (unistd.lseek(f.fd, off, whence) < 0) -1 else 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ftell(f: *FILE) i64 {
|
|
||||||
return unistd.lseek(f.fd, 0, unistd.SEEK_CURRENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rewind(f: *FILE) void {
|
|
||||||
_ = fseek(f, 0, SEEK_SET);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn feof(f: *FILE) c_int {
|
|
||||||
return f.eof;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ferror(f: *FILE) c_int {
|
|
||||||
return f.err;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fputs(s: []const u8, f: *FILE) c_int {
|
|
||||||
return if (unistd.write(f.fd, s) < 0) -1 else 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fputc(c: u8, f: *FILE) c_int {
|
|
||||||
const b = [_]u8{c};
|
|
||||||
return if (unistd.write(f.fd, &b) == 1) c else -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fgetc(f: *FILE) c_int {
|
|
||||||
var b: [1]u8 = undefined;
|
|
||||||
const n = unistd.read(f.fd, &b);
|
|
||||||
if (n <= 0) {
|
|
||||||
f.eof = 1;
|
|
||||||
return -1; // EOF
|
|
||||||
}
|
|
||||||
return b[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Real `extern "C"` symbols (fopen/fread/fseek/...) — with a C-string signature
|
|
||||||
// distinct from the Zig slice API above — land with the first C program, wired
|
|
||||||
// via @export so they don't collide with these Zig names.
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
//! POSIX-style file API for user programs — the low level under C stdio. Files
|
|
||||||
//! are named objects served by the user-space VFS server (system/services/vfs/vfs.zig); each
|
|
||||||
//! call marshals a request, IPC_Calls the VFS, and unmarshals the reply. The
|
|
||||||
//! kernel knows nothing of files or fds — the fd table lives here, per process.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const protocol = @import("vfs-protocol");
|
|
||||||
const ipc = @import("runtime").ipc;
|
|
||||||
|
|
||||||
pub const O_CREAT = protocol.create;
|
|
||||||
pub const SEEK_SET: u32 = 0;
|
|
||||||
pub const SEEK_CURRENT: u32 = 1;
|
|
||||||
pub const SEEK_END: u32 = 2;
|
|
||||||
|
|
||||||
// Resolve (and cache) the VFS server endpoint, looked up by well-known id.
|
|
||||||
var vfs_handle: usize = 0;
|
|
||||||
var vfs_resolved = false;
|
|
||||||
fn vfs() ?usize {
|
|
||||||
if (!vfs_resolved) {
|
|
||||||
vfs_handle = ipc.lookup(.vfs) orelse return null;
|
|
||||||
vfs_resolved = true;
|
|
||||||
}
|
|
||||||
return vfs_handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maximum_fds = 32;
|
|
||||||
const Fd = struct { used: bool = false, node: u64 = 0, offset: u64 = 0 };
|
|
||||||
var fds = [_]Fd{.{}} ** maximum_fds;
|
|
||||||
|
|
||||||
fn allocFd() ?usize {
|
|
||||||
for (&fds, 0..) |*f, i| {
|
|
||||||
if (!f.used) {
|
|
||||||
f.* = .{ .used = true };
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Result = struct { reply: protocol.Reply, payload: []u8 };
|
|
||||||
|
|
||||||
/// One request/reply round trip: [Request header][send payload] -> VFS ->
|
|
||||||
/// [Reply header][receive payload]. The receive payload is written into `out`.
|
|
||||||
fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result {
|
|
||||||
const h = vfs() orelse return null;
|
|
||||||
var message: [protocol.message_maximum]u8 = undefined;
|
|
||||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
|
||||||
const slen = @min(send.len, protocol.maximum_payload);
|
|
||||||
@memcpy(message[protocol.request_size..][0..slen], send[0..slen]);
|
|
||||||
|
|
||||||
var rbuf: [protocol.message_maximum]u8 = undefined;
|
|
||||||
const n = ipc.call(h, message[0 .. protocol.request_size + slen], &rbuf) catch return null;
|
|
||||||
if (n < protocol.reply_size) return null;
|
|
||||||
const reply = std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]);
|
|
||||||
const rpl = @min(n - protocol.reply_size, out.len);
|
|
||||||
@memcpy(out[0..rpl], rbuf[protocol.reply_size..][0..rpl]);
|
|
||||||
return .{ .reply = reply, .payload = out[0..rpl] };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open (or create, with O_CREAT) `path`; returns an fd or -1.
|
|
||||||
pub fn open(path: []const u8, flags: u32) i32 {
|
|
||||||
const fd = allocFd() orelse return -1;
|
|
||||||
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = flags };
|
|
||||||
const r = transact(request, path, &.{}) orelse {
|
|
||||||
fds[fd].used = false;
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
if (r.reply.status != 0) {
|
|
||||||
fds[fd].used = false;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
fds[fd] = .{ .used = true, .node = r.reply.node, .offset = 0 };
|
|
||||||
return @intCast(fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fdPtr(fd: i32) ?*Fd {
|
|
||||||
if (fd < 0 or fd >= maximum_fds) return null;
|
|
||||||
const f = &fds[@intCast(fd)];
|
|
||||||
return if (f.used) f else null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read up to `buffer.len` bytes at the current offset; returns the count or -1.
|
|
||||||
pub fn read(fd: i32, buffer: []u8) isize {
|
|
||||||
const f = fdPtr(fd) orelse return -1;
|
|
||||||
const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload));
|
|
||||||
const request = protocol.Request{ .operation = .read, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
|
|
||||||
const r = transact(request, &.{}, buffer) orelse return -1;
|
|
||||||
if (r.reply.status != 0) return -1;
|
|
||||||
f.offset += r.reply.len;
|
|
||||||
return @intCast(r.reply.len);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write `data` at the current offset; returns the count or -1.
|
|
||||||
pub fn write(fd: i32, data: []const u8) isize {
|
|
||||||
const f = fdPtr(fd) orelse return -1;
|
|
||||||
const want: u32 = @intCast(@min(data.len, protocol.maximum_payload));
|
|
||||||
const request = protocol.Request{ .operation = .write, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
|
|
||||||
const r = transact(request, data[0..want], &.{}) orelse return -1;
|
|
||||||
if (r.reply.status != 0) return -1;
|
|
||||||
f.offset += r.reply.len;
|
|
||||||
return @intCast(r.reply.len);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reposition the fd's offset. Returns the new offset or -1. (SEEK_END needs the
|
|
||||||
/// file size, which `stat` provides; handled by fetching it here.)
|
|
||||||
pub fn lseek(fd: i32, off: i64, whence: u32) i64 {
|
|
||||||
const f = fdPtr(fd) orelse return -1;
|
|
||||||
const base: i64 = switch (whence) {
|
|
||||||
SEEK_SET => 0,
|
|
||||||
SEEK_CURRENT => @intCast(f.offset),
|
|
||||||
SEEK_END => blk: {
|
|
||||||
const request = protocol.Request{ .operation = .status, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
|
||||||
var sbuf: [@sizeOf(protocol.FileStatus)]u8 = undefined;
|
|
||||||
const r = transact(request, &.{}, &sbuf) orelse return -1;
|
|
||||||
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return -1;
|
|
||||||
const st = std.mem.bytesToValue(protocol.FileStatus, sbuf[0..@sizeOf(protocol.FileStatus)]);
|
|
||||||
break :blk @intCast(st.size);
|
|
||||||
},
|
|
||||||
else => return -1,
|
|
||||||
};
|
|
||||||
const pos = base + off;
|
|
||||||
if (pos < 0) return -1;
|
|
||||||
f.offset = @intCast(pos);
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stat `path`. Returns 0 or -1.
|
|
||||||
pub fn stat(path: []const u8, out: *protocol.FileStatus) i32 {
|
|
||||||
// Open, stat by node, close — simple and enough for now.
|
|
||||||
const fd = open(path, 0);
|
|
||||||
if (fd < 0) return -1;
|
|
||||||
defer close(fd);
|
|
||||||
const f = fdPtr(fd).?;
|
|
||||||
const request = protocol.Request{ .operation = .status, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
|
||||||
var sbuf: [@sizeOf(protocol.FileStatus)]u8 = undefined;
|
|
||||||
const r = transact(request, &.{}, &sbuf) orelse return -1;
|
|
||||||
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return -1;
|
|
||||||
out.* = std.mem.bytesToValue(protocol.FileStatus, sbuf[0..@sizeOf(protocol.FileStatus)]);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Close an fd (best effort — tells the VFS to release the open file).
|
|
||||||
pub fn close(fd: i32) void {
|
|
||||||
const f = fdPtr(fd) orelse return;
|
|
||||||
const request = protocol.Request{ .operation = .close, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
|
||||||
_ = transact(request, &.{}, &.{});
|
|
||||||
f.used = false;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
//! Block-device client: the helper a filesystem uses to read and write a block
|
||||||
|
//! device (a USB stick, via usb-storage) without hand-rolling the block-protocol
|
||||||
|
//! IPC. Layered over `ipc` and the shared `block-protocol` wire format, like
|
||||||
|
//! `runtime.usb` over the transfer protocol.
|
||||||
|
//!
|
||||||
|
//! Transfers name a caller-owned DMA buffer by physical address (from
|
||||||
|
//! `runtime.dma.alloc`), so whole sectors move without crossing the IPC size
|
||||||
|
//! limit — the same handoff usb-storage uses toward the controller.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const ipc = @import("ipc.zig");
|
||||||
|
const system = @import("system.zig");
|
||||||
|
const protocol = @import("block-protocol");
|
||||||
|
|
||||||
|
pub const Geometry = struct { block_size: u32, block_count: u64 };
|
||||||
|
|
||||||
|
pub const Device = struct {
|
||||||
|
endpoint: ipc.Handle,
|
||||||
|
|
||||||
|
/// The device's block size and total block count.
|
||||||
|
pub fn geometry(self: Device) ?Geometry {
|
||||||
|
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.geometry), .lba = 0, .count = 0, .physical = 0 };
|
||||||
|
var reply: [protocol.reply_size]u8 = undefined;
|
||||||
|
const n = ipc.call(self.endpoint, std.mem.asBytes(&request), &reply) catch return null;
|
||||||
|
if (n < protocol.reply_size) return null;
|
||||||
|
const result = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]);
|
||||||
|
if (result.status != 0) return null;
|
||||||
|
return .{ .block_size = result.block_size, .block_count = result.block_count };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read `count` blocks starting at `lba` into the DMA buffer at `physical`.
|
||||||
|
pub fn read(self: Device, lba: u64, count: u32, physical: u64) bool {
|
||||||
|
return self.transfer(.read, lba, count, physical);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write `count` blocks starting at `lba` from the DMA buffer at `physical`.
|
||||||
|
pub fn write(self: Device, lba: u64, count: u32, physical: u64) bool {
|
||||||
|
return self.transfer(.write, lba, count, physical);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commit any device write cache to stable media (SCSI SYNCHRONIZE CACHE), so
|
||||||
|
/// prior writes survive a power-off. A filesystem calls this before the machine
|
||||||
|
/// goes down; no data transfer, so the buffer arguments are unused.
|
||||||
|
pub fn flush(self: Device) bool {
|
||||||
|
return self.transfer(.flush, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transfer(self: Device, operation: protocol.Operation, lba: u64, count: u32, physical: u64) bool {
|
||||||
|
var request = protocol.Request{ .operation = @intFromEnum(operation), .lba = lba, .count = count, .physical = physical };
|
||||||
|
var reply: [protocol.reply_size]u8 = undefined;
|
||||||
|
const n = ipc.call(self.endpoint, std.mem.asBytes(&request), &reply) catch return false;
|
||||||
|
if (n < protocol.reply_size) return false;
|
||||||
|
return std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status == 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Look up the block device, retrying generously while the USB storage chain
|
||||||
|
/// (controller reset, enumeration, mass-storage bring-up) comes up.
|
||||||
|
pub fn open() ?Device {
|
||||||
|
// Patient: the whole USB storage chain (firmware discovery, xHCI reset and
|
||||||
|
// enumeration, mass-storage bring-up) must complete first, which can take
|
||||||
|
// tens of seconds under emulation.
|
||||||
|
var attempts: usize = 0;
|
||||||
|
while (attempts < 1200) : (attempts += 1) {
|
||||||
|
if (ipc.lookup(.block)) |handle| return .{ .endpoint = handle };
|
||||||
|
system.sleep(50);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -37,6 +37,10 @@ pub fn mmioMap(device_id: u64, resource_index: u64) ?usize {
|
|||||||
/// `DeviceDescriptor.parent` for a device with no parent.
|
/// `DeviceDescriptor.parent` for a device with no parent.
|
||||||
pub const no_parent = device_abi.no_parent;
|
pub const no_parent = device_abi.no_parent;
|
||||||
|
|
||||||
|
/// `DeviceDescriptor.pci_class` for a device that is not a PCI function. Set this on
|
||||||
|
/// descriptors passed to `register` unless the child really is one.
|
||||||
|
pub const no_pci_class = device_abi.no_pci_class;
|
||||||
|
|
||||||
/// Publish `descriptor` as a child of `parent_id`, which this process must have claimed.
|
/// Publish `descriptor` as a child of `parent_id`, which this process must have claimed.
|
||||||
/// Returns the new device id. The child is left unclaimed, so whichever driver owns
|
/// Returns the new device id. The child is left unclaimed, so whichever driver owns
|
||||||
/// that class of device can `claim` it — that is how a bus hands off a device.
|
/// that class of device can `claim` it — that is how a bus hands off a device.
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
//! User-space display client: talk to the display service (query the mode, and — from D3
|
||||||
|
//! — create layers, draw, and present) without hand-rolling the IPC. The `runtime.block`
|
||||||
|
//! shape: a cached `.display` lookup with a boot-race retry, then extern-struct request/
|
||||||
|
//! reply marshalling. See system/services/display/ and docs/display.md.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const ipc = @import("ipc.zig");
|
||||||
|
const system = @import("system.zig");
|
||||||
|
const protocol = @import("display-protocol");
|
||||||
|
|
||||||
|
/// The display's current mode, as `info()` reports it.
|
||||||
|
pub const Info = struct {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
pitch: u32, // bytes per row (may exceed width*4; see docs/framebuffer.md)
|
||||||
|
format: u32, // a device-abi DisplayFormat value (0 = rgbx, 1 = bgrx)
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The service endpoint, looked up once and cached.
|
||||||
|
var handle: ?ipc.Handle = null;
|
||||||
|
|
||||||
|
/// Look up the display service, retrying while it comes up (a client races its
|
||||||
|
/// registration at boot). Returns the endpoint, or null if it never appears.
|
||||||
|
fn service() ?ipc.Handle {
|
||||||
|
if (handle) |h| return h;
|
||||||
|
var attempts: usize = 0;
|
||||||
|
while (attempts < 100) : (attempts += 1) {
|
||||||
|
if (ipc.lookup(.display)) |h| {
|
||||||
|
handle = h;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
system.sleep(50);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send one request, receive its reply; true on a zero status. `out` receives the reply
|
||||||
|
/// so callers can read `info`/`layer` fields on success.
|
||||||
|
fn transact(request: protocol.Request, out: *protocol.Reply) bool {
|
||||||
|
const h = service() orelse return false;
|
||||||
|
var req = request;
|
||||||
|
var reply: [protocol.reply_size]u8 = undefined;
|
||||||
|
const len = ipc.call(h, std.mem.asBytes(&req), &reply) catch return false;
|
||||||
|
if (len < protocol.reply_size) return false;
|
||||||
|
out.* = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]);
|
||||||
|
return out.status == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The display's current mode, or null if the service never came up.
|
||||||
|
pub fn info() ?Info {
|
||||||
|
var reply: protocol.Reply = undefined;
|
||||||
|
if (!transact(.{ .operation = @intFromEnum(protocol.Operation.info) }, &reply)) return null;
|
||||||
|
return .{ .width = reply.width, .height = reply.height, .pitch = reply.pitch, .format = reply.format };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Composite the dirty layers and flush the frame to the screen.
|
||||||
|
pub fn present() bool {
|
||||||
|
var reply: protocol.Reply = undefined;
|
||||||
|
return transact(.{ .operation = @intFromEnum(protocol.Operation.present) }, &reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The mode, cached after the first `info()` so `color()` doesn't round-trip per pixel.
|
||||||
|
var mode: ?Info = null;
|
||||||
|
|
||||||
|
fn cachedInfo() ?Info {
|
||||||
|
if (mode) |m| return m;
|
||||||
|
const i = info() orelse return null;
|
||||||
|
mode = i;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The native pixel value for an 8-bit-per-channel colour, in the display's format. A
|
||||||
|
/// client packs colours through this so it never has to know the byte order itself.
|
||||||
|
pub fn color(r: u8, g: u8, b: u8) u32 {
|
||||||
|
const format = if (cachedInfo()) |i| i.format else 0;
|
||||||
|
return protocol.pack(format, r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handle to a server-owned layer: a positioned, z-ordered surface the client draws
|
||||||
|
/// into by command. Create with `createLayer`; drawing and moves take effect on the next
|
||||||
|
/// `present`. Coordinates are signed (a layer may sit partly off-screen).
|
||||||
|
pub const Layer = struct {
|
||||||
|
id: u32,
|
||||||
|
|
||||||
|
/// Fill a rectangle of this layer (layer-local coordinates) with a native `colour`.
|
||||||
|
pub fn fill(self: Layer, x: i32, y: i32, w: u32, h: u32, colour: u32) bool {
|
||||||
|
var reply: protocol.Reply = undefined;
|
||||||
|
return transact(.{
|
||||||
|
.operation = @intFromEnum(protocol.Operation.fill_rect),
|
||||||
|
.layer = self.id,
|
||||||
|
.x = @bitCast(x),
|
||||||
|
.y = @bitCast(y),
|
||||||
|
.width = w,
|
||||||
|
.height = h,
|
||||||
|
.colour = colour,
|
||||||
|
}, &reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy a `w`×`h` tile of native pixels (row-major, little-endian bytes) into this
|
||||||
|
/// layer at (`x`, `y`). The tile rides inline in the request, so `w*h*4` must fit
|
||||||
|
/// `protocol.maximum_payload`.
|
||||||
|
pub fn blitTile(self: Layer, x: i32, y: i32, w: u32, h: u32, pixels: []const u8) bool {
|
||||||
|
var request = protocol.Request{
|
||||||
|
.operation = @intFromEnum(protocol.Operation.blit_tile),
|
||||||
|
.layer = self.id,
|
||||||
|
.x = @bitCast(x),
|
||||||
|
.y = @bitCast(y),
|
||||||
|
.width = w,
|
||||||
|
.height = h,
|
||||||
|
};
|
||||||
|
const header = std.mem.asBytes(&request);
|
||||||
|
if (header.len + pixels.len > protocol.message_maximum) return false;
|
||||||
|
var buffer: [protocol.message_maximum]u8 = undefined;
|
||||||
|
@memcpy(buffer[0..header.len], header);
|
||||||
|
@memcpy(buffer[header.len..][0..pixels.len], pixels);
|
||||||
|
const h_svc = service() orelse return false;
|
||||||
|
var reply: [protocol.reply_size]u8 = undefined;
|
||||||
|
const len = ipc.call(h_svc, buffer[0 .. header.len + pixels.len], &reply) catch return false;
|
||||||
|
if (len < protocol.reply_size) return false;
|
||||||
|
return std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move / restack / show or hide the layer.
|
||||||
|
pub fn configure(self: Layer, x: i32, y: i32, z: u32, visible: bool) bool {
|
||||||
|
var reply: protocol.Reply = undefined;
|
||||||
|
return transact(.{
|
||||||
|
.operation = @intFromEnum(protocol.Operation.configure_layer),
|
||||||
|
.layer = self.id,
|
||||||
|
.x = @bitCast(x),
|
||||||
|
.y = @bitCast(y),
|
||||||
|
.z = z,
|
||||||
|
.visible = if (visible) 1 else 0,
|
||||||
|
}, &reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a rectangle of this layer (layer-local) dirty for the next present — for when
|
||||||
|
/// the layer's pixels changed without a drawing call the compositor already tracked.
|
||||||
|
pub fn damage(self: Layer, x: i32, y: i32, w: u32, h: u32) bool {
|
||||||
|
var reply: protocol.Reply = undefined;
|
||||||
|
return transact(.{
|
||||||
|
.operation = @intFromEnum(protocol.Operation.damage),
|
||||||
|
.layer = self.id,
|
||||||
|
.x = @bitCast(x),
|
||||||
|
.y = @bitCast(y),
|
||||||
|
.width = w,
|
||||||
|
.height = h,
|
||||||
|
}, &reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release the layer and its surface.
|
||||||
|
pub fn destroy(self: Layer) bool {
|
||||||
|
var reply: protocol.Reply = undefined;
|
||||||
|
return transact(.{ .operation = @intFromEnum(protocol.Operation.destroy_layer), .layer = self.id }, &reply);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Create a server-owned layer of `w`×`h` pixels at screen (`x`, `y`) with stacking order
|
||||||
|
/// `z` (higher is nearer the front), initially visible. Returns a handle, or null.
|
||||||
|
pub fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32) ?Layer {
|
||||||
|
var reply: protocol.Reply = undefined;
|
||||||
|
if (!transact(.{
|
||||||
|
.operation = @intFromEnum(protocol.Operation.create_layer),
|
||||||
|
.x = @bitCast(x),
|
||||||
|
.y = @bitCast(y),
|
||||||
|
.width = w,
|
||||||
|
.height = h,
|
||||||
|
.z = z,
|
||||||
|
.visible = 1,
|
||||||
|
}, &reply)) return null;
|
||||||
|
return .{ .id = reply.layer };
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
//! runtime.fs — the danos-native file API. A program opens, reads, writes, and
|
||||||
|
//! lists files served by the user-space VFS (system/services/vfs), each call
|
||||||
|
//! marshalling a vfs-protocol request over IPC. This is the danos-native layer
|
||||||
|
//! danos programs use directly; it is also where the file operations that later
|
||||||
|
//! become `std.os.danos` are staged (see docs/zig-self-hosting.md). It replaces
|
||||||
|
//! the old POSIX `unistd` shim — a compatibility spelling danos does not need yet.
|
||||||
|
//!
|
||||||
|
//! Handles are *values*, not entries in a global descriptor table: a `File` /
|
||||||
|
//! `Directory` owns its VFS node id and (for files) a byte offset. So there is no
|
||||||
|
//! per-process fd limit and no shared table to synchronise — the danos-native
|
||||||
|
//! shape, unlike the POSIX fd model the old shim emulated.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const ipc = @import("ipc.zig");
|
||||||
|
const protocol = @import("vfs-protocol");
|
||||||
|
|
||||||
|
/// The kind of a filesystem node — re-exported so a caller need not import the
|
||||||
|
/// wire protocol.
|
||||||
|
pub const Kind = protocol.NodeKind;
|
||||||
|
|
||||||
|
/// A node's metadata (the answer to a status request).
|
||||||
|
pub const Attributes = struct {
|
||||||
|
size: u64,
|
||||||
|
kind: Kind,
|
||||||
|
/// Modification time — Unix epoch seconds, UTC. 0 if the filesystem has none.
|
||||||
|
mtime: u64 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map a wire `NodeKind` value to the enum, defaulting anything unrecognised to
|
||||||
|
// `.regular` (the server is trusted, but a value outside the enum would be
|
||||||
|
// illegal to `@enumFromInt` directly).
|
||||||
|
fn kindFromWire(value: u32) Kind {
|
||||||
|
return switch (value) {
|
||||||
|
@intFromEnum(Kind.directory) => .directory,
|
||||||
|
@intFromEnum(Kind.character_device) => .character_device,
|
||||||
|
@intFromEnum(Kind.block_device) => .block_device,
|
||||||
|
@intFromEnum(Kind.symbolic_link) => .symbolic_link,
|
||||||
|
@intFromEnum(Kind.fifo) => .fifo,
|
||||||
|
@intFromEnum(Kind.socket) => .socket,
|
||||||
|
else => .regular,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How to open a path.
|
||||||
|
pub const OpenOptions = struct {
|
||||||
|
/// Create the file if it does not exist.
|
||||||
|
create: bool = false,
|
||||||
|
/// Open a directory node (for listing) rather than a file.
|
||||||
|
directory: bool = false,
|
||||||
|
/// Truncate an existing file to zero length on open (O_TRUNC) — replace its
|
||||||
|
/// contents rather than overwriting in place.
|
||||||
|
truncate: bool = false,
|
||||||
|
|
||||||
|
fn wireFlags(self: OpenOptions) u32 {
|
||||||
|
var f: u32 = 0;
|
||||||
|
if (self.create) f |= protocol.create;
|
||||||
|
if (self.directory) f |= protocol.directory;
|
||||||
|
if (self.truncate) f |= protocol.truncate;
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The VFS server endpoint, looked up once by well-known id and cached.
|
||||||
|
var vfs_handle: ipc.Handle = 0;
|
||||||
|
var vfs_resolved = false;
|
||||||
|
fn vfs() ?ipc.Handle {
|
||||||
|
if (!vfs_resolved) {
|
||||||
|
vfs_handle = ipc.lookup(.vfs) orelse return null;
|
||||||
|
vfs_resolved = true;
|
||||||
|
}
|
||||||
|
return vfs_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Result = struct { reply: protocol.Reply, payload: []u8 };
|
||||||
|
|
||||||
|
// One request/reply round trip: [Request header][send payload] -> VFS ->
|
||||||
|
// [Reply header][receive payload]. The receive payload lands in `out`.
|
||||||
|
fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result {
|
||||||
|
const h = vfs() orelse return null;
|
||||||
|
var message: [protocol.message_maximum]u8 = undefined;
|
||||||
|
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||||
|
const slen = @min(send.len, protocol.maximum_payload);
|
||||||
|
@memcpy(message[protocol.request_size..][0..slen], send[0..slen]);
|
||||||
|
|
||||||
|
var rbuf: [protocol.message_maximum]u8 = undefined;
|
||||||
|
const n = ipc.call(h, message[0 .. protocol.request_size + slen], &rbuf) catch return null;
|
||||||
|
if (n < protocol.reply_size) return null;
|
||||||
|
const reply = std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]);
|
||||||
|
const rpl = @min(n - protocol.reply_size, out.len);
|
||||||
|
@memcpy(out[0..rpl], rbuf[protocol.reply_size..][0..rpl]);
|
||||||
|
return .{ .reply = reply, .payload = out[0..rpl] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An open file: a VFS node plus a byte cursor. Read and write advance the cursor.
|
||||||
|
pub const File = struct {
|
||||||
|
node: u64,
|
||||||
|
offset: u64 = 0,
|
||||||
|
|
||||||
|
/// Read up to `buffer.len` bytes at the current offset; returns the count, or
|
||||||
|
/// null on error.
|
||||||
|
pub fn read(self: *File, buffer: []u8) ?usize {
|
||||||
|
const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload));
|
||||||
|
const request = protocol.Request{ .operation = .read, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
|
||||||
|
const r = transact(request, &.{}, buffer) orelse return null;
|
||||||
|
if (r.reply.status != 0) return null;
|
||||||
|
self.offset += r.reply.len;
|
||||||
|
return r.reply.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write `data` at the current offset; returns the count written. A single
|
||||||
|
/// call is capped at the VFS payload size, so the return may be short — use
|
||||||
|
/// `writeAll` to write the whole slice. Null on error.
|
||||||
|
pub fn write(self: *File, data: []const u8) ?usize {
|
||||||
|
const want: u32 = @intCast(@min(data.len, protocol.maximum_payload));
|
||||||
|
const request = protocol.Request{ .operation = .write, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
|
||||||
|
const r = transact(request, data[0..want], &.{}) orelse return null;
|
||||||
|
if (r.reply.status != 0) return null;
|
||||||
|
self.offset += r.reply.len;
|
||||||
|
return r.reply.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write all of `data`, looping past the per-call payload cap. Returns the
|
||||||
|
/// total written, or null if a write failed before any progress.
|
||||||
|
pub fn writeAll(self: *File, data: []const u8) ?usize {
|
||||||
|
var written: usize = 0;
|
||||||
|
while (written < data.len) {
|
||||||
|
const n = self.write(data[written..]) orelse return if (written == 0) null else written;
|
||||||
|
if (n == 0) return written; // no forward progress; stop rather than spin
|
||||||
|
written += n;
|
||||||
|
}
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the read/write cursor to an absolute byte position.
|
||||||
|
pub fn seekTo(self: *File, position: u64) void {
|
||||||
|
self.offset = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This file's metadata.
|
||||||
|
pub fn attributes(self: *File) ?Attributes {
|
||||||
|
const request = protocol.Request{ .operation = .status, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
|
||||||
|
var buffer: [@sizeOf(protocol.FileStatus)]u8 = undefined;
|
||||||
|
const r = transact(request, &.{}, &buffer) orelse return null;
|
||||||
|
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return null;
|
||||||
|
const status = std.mem.bytesToValue(protocol.FileStatus, buffer[0..@sizeOf(protocol.FileStatus)]);
|
||||||
|
return .{ .size = status.size, .kind = kindFromWire(status.kind), .mtime = status.mtime };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release the VFS's open handle for this file.
|
||||||
|
pub fn close(self: *File) void {
|
||||||
|
const request = protocol.Request{ .operation = .close, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
|
||||||
|
_ = transact(request, &.{}, &.{});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Open (or create, with `.create`) `path`. Returns the open file, or null.
|
||||||
|
pub fn open(path: []const u8, options: OpenOptions) ?File {
|
||||||
|
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = options.wireFlags() };
|
||||||
|
const r = transact(request, path, &.{}) orelse return null;
|
||||||
|
if (r.reply.status != 0) return null;
|
||||||
|
return .{ .node = r.reply.node };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A path's metadata without keeping it open (open -> status -> close).
|
||||||
|
pub fn attributes(path: []const u8) ?Attributes {
|
||||||
|
var file = open(path, .{}) orelse return null;
|
||||||
|
defer file.close();
|
||||||
|
return file.attributes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `path` resolves — handy as a readiness check (e.g. waiting for a mount
|
||||||
|
/// to come up before writing to it).
|
||||||
|
pub fn exists(path: []const u8) bool {
|
||||||
|
return attributes(path) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One entry returned by `Directory.next`.
|
||||||
|
pub const Entry = struct {
|
||||||
|
kind: Kind = .regular,
|
||||||
|
size: u64 = 0,
|
||||||
|
name_buffer: [64]u8 = undefined,
|
||||||
|
name_len: usize = 0,
|
||||||
|
|
||||||
|
pub fn name(self: *const Entry) []const u8 {
|
||||||
|
return self.name_buffer[0..self.name_len];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An open directory being listed, cursor-advanced by `next`.
|
||||||
|
pub const Directory = struct {
|
||||||
|
node: u64,
|
||||||
|
cursor: u64 = 0,
|
||||||
|
|
||||||
|
/// Fill `entry` with the next directory entry; false at end of directory or
|
||||||
|
/// on error.
|
||||||
|
pub fn next(self: *Directory, entry: *Entry) bool {
|
||||||
|
const request = protocol.Request{ .operation = .readdir, .node = self.node, .offset = self.cursor, .len = 0, .flags = 0 };
|
||||||
|
var buffer: [protocol.message_maximum]u8 = undefined;
|
||||||
|
const r = transact(request, &.{}, &buffer) orelse return false;
|
||||||
|
if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF
|
||||||
|
if (r.payload.len < protocol.directory_entry_size) return false;
|
||||||
|
const header = std.mem.bytesToValue(protocol.DirectoryEntry, r.payload[0..protocol.directory_entry_size]);
|
||||||
|
entry.kind = kindFromWire(header.kind);
|
||||||
|
entry.size = header.size;
|
||||||
|
const source = r.payload[protocol.directory_entry_size..];
|
||||||
|
const nlen = @min(@min(@as(usize, header.name_len), source.len), entry.name_buffer.len);
|
||||||
|
@memcpy(entry.name_buffer[0..nlen], source[0..nlen]);
|
||||||
|
entry.name_len = nlen;
|
||||||
|
self.cursor += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release the VFS's open handle for this directory.
|
||||||
|
pub fn close(self: *Directory) void {
|
||||||
|
var f = File{ .node = self.node };
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Open `path` as a directory for listing. Returns null if it isn't one / on error.
|
||||||
|
pub fn openDirectory(path: []const u8) ?Directory {
|
||||||
|
const file = open(path, .{ .directory = true }) orelse return null;
|
||||||
|
return .{ .node = file.node };
|
||||||
|
}
|
||||||
|
|
||||||
|
// A path-based request that returns only a status (mkdir, unlink).
|
||||||
|
fn pathOperation(operation: protocol.Operation, path: []const u8) bool {
|
||||||
|
const request = protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = 0 };
|
||||||
|
const r = transact(request, path, &.{}) orelse return false;
|
||||||
|
return r.reply.status == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a directory at `path` (its parent must already exist). Returns true on
|
||||||
|
/// success. Only works under a mounted filesystem that supports directories.
|
||||||
|
pub fn makeDirectory(path: []const u8) bool {
|
||||||
|
return pathOperation(.mkdir, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the file at `path`. Returns true on success. Directories are refused
|
||||||
|
/// (a separate directory-removal would have to check emptiness).
|
||||||
|
pub fn remove(path: []const u8) bool {
|
||||||
|
return pathOperation(.unlink, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rename `old_path` to `new_path`. Both must be in the same directory (same-
|
||||||
|
/// directory, 8.3-name rename only for now). Returns true on success.
|
||||||
|
pub fn rename(old_path: []const u8, new_path: []const u8) bool {
|
||||||
|
const total = old_path.len + 1 + new_path.len;
|
||||||
|
if (total > protocol.maximum_payload) return false;
|
||||||
|
var payload: [protocol.maximum_payload]u8 = undefined;
|
||||||
|
@memcpy(payload[0..old_path.len], old_path);
|
||||||
|
payload[old_path.len] = 0;
|
||||||
|
@memcpy(payload[old_path.len + 1 ..][0..new_path.len], new_path);
|
||||||
|
const request = protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 };
|
||||||
|
const r = transact(request, payload[0..total], &.{}) orelse return false;
|
||||||
|
return r.reply.status == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
|
||||||
|
/// the VFS then routes everything under `target` to that backend. This is the one
|
||||||
|
/// call that hands the VFS a capability (the backend endpoint). Returns true on
|
||||||
|
/// success.
|
||||||
|
pub fn mount(target: []const u8, backend: ipc.Handle) bool {
|
||||||
|
const h = vfs() orelse return false;
|
||||||
|
const request = protocol.Request{ .operation = .mount, .node = 0, .offset = 0, .len = @intCast(target.len), .flags = 0 };
|
||||||
|
var message: [protocol.message_maximum]u8 = undefined;
|
||||||
|
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||||
|
const tlen = @min(target.len, protocol.maximum_payload);
|
||||||
|
@memcpy(message[protocol.request_size..][0..tlen], target[0..tlen]);
|
||||||
|
var rbuf: [protocol.message_maximum]u8 = undefined;
|
||||||
|
const result = ipc.callCap(h, message[0 .. protocol.request_size + tlen], &rbuf, backend) catch return false;
|
||||||
|
if (result.len < protocol.reply_size) return false;
|
||||||
|
return std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]).status == 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
//! User-space input helpers: the client and publisher sides of the input service, so a
|
||||||
|
//! program listening for input events — or a driver broadcasting them — doesn't hand-roll
|
||||||
|
//! the IPC. Layered over `ipc` (endpoints, capability passing, `send`) and the shared
|
||||||
|
//! `input-protocol` wire format, the way `device.zig` layers over the raw `device_*` calls.
|
||||||
|
//! See system/services/input/input.zig.
|
||||||
|
//!
|
||||||
|
//! The service carries several device classes (keyboard, mouse, joystick/gamepad). A
|
||||||
|
//! **source** publishes its class with the matching method:
|
||||||
|
//! var source = input.connectSource() orelse return;
|
||||||
|
//! _ = source.publishKeyboardEvent(.{ .kind = ..., .keycode = ..., ... });
|
||||||
|
//! _ = source.publishMouseEvent(.{ ... });
|
||||||
|
//! _ = source.publishJoystickEvent(.{ ... });
|
||||||
|
//!
|
||||||
|
//! A **subscriber** either takes one class with a typed helper —
|
||||||
|
//! var keys = input.subscribeKeyboard() orelse return;
|
||||||
|
//! while (true) { const key = keys.next() orelse continue; ... }
|
||||||
|
//! — or takes several at once and inspects the tagged envelope:
|
||||||
|
//! var listener = input.subscribeAll() orelse return;
|
||||||
|
//! while (true) {
|
||||||
|
//! const event = listener.next() orelse continue;
|
||||||
|
//! if (event.asKeyboard()) |k| { ... } else if (event.asMouse()) |m| { ... }
|
||||||
|
//! }
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const abi = @import("abi");
|
||||||
|
const ipc = @import("ipc.zig");
|
||||||
|
const system = @import("system.zig");
|
||||||
|
const protocol = @import("input-protocol");
|
||||||
|
|
||||||
|
pub const DeviceKind = protocol.DeviceKind;
|
||||||
|
pub const InputEvent = protocol.InputEvent;
|
||||||
|
pub const KeyEvent = protocol.KeyEvent;
|
||||||
|
pub const MouseEvent = protocol.MouseEvent;
|
||||||
|
pub const JoystickEvent = protocol.JoystickEvent;
|
||||||
|
pub const EventKind = protocol.EventKind;
|
||||||
|
pub const MouseEventKind = protocol.MouseEventKind;
|
||||||
|
pub const JoystickEventKind = protocol.JoystickEventKind;
|
||||||
|
pub const Keycode = protocol.Keycode;
|
||||||
|
|
||||||
|
/// Interest masks re-exported so a caller can `subscribe(input.device_keyboard |
|
||||||
|
/// input.device_mouse)`.
|
||||||
|
pub const device_keyboard = protocol.device_keyboard;
|
||||||
|
pub const device_mouse = protocol.device_mouse;
|
||||||
|
pub const device_joystick = protocol.device_joystick;
|
||||||
|
pub const device_all = protocol.device_all;
|
||||||
|
|
||||||
|
/// Look up the input service, retrying while it is still coming up. Both a subscriber and
|
||||||
|
/// a source race the service's registration at boot, so both wait for it here rather than
|
||||||
|
/// failing. Returns the service endpoint handle, or null if it never appears.
|
||||||
|
fn lookupService() ?ipc.Handle {
|
||||||
|
var attempts: usize = 0;
|
||||||
|
while (attempts < 100) : (attempts += 1) {
|
||||||
|
if (ipc.lookup(.input)) |handle| return handle;
|
||||||
|
system.sleep(50);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- subscribing ------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A subscription to the input service: our own endpoint, which the service pushes events
|
||||||
|
/// to. `next` returns each event as a tagged `InputEvent`; use `asKeyboard`/`asMouse`/
|
||||||
|
/// `asJoystick` to decode. Created with `subscribe`/`subscribeAll`; for a single device
|
||||||
|
/// class prefer the typed helpers (`subscribeKeyboard`, ...), which return decoded events.
|
||||||
|
pub const Subscriber = struct {
|
||||||
|
/// The endpoint the service delivers events to (created and owned by us; its handle
|
||||||
|
/// was handed to the service as a capability at subscribe time).
|
||||||
|
endpoint: ipc.Handle,
|
||||||
|
receive: [protocol.event_size]u8 = undefined,
|
||||||
|
|
||||||
|
/// Block until the next event is pushed, and return it. Events arrive as asynchronous
|
||||||
|
/// buffered messages (`ipc_send` from the service), so nothing is owed in reply — the
|
||||||
|
/// empty reply this issues is a harmless no-op. Returns null for any non-event wake-up
|
||||||
|
/// (there should be none), so callers can loop.
|
||||||
|
pub fn next(self: *Subscriber) ?InputEvent {
|
||||||
|
const got = ipc.replyWait(self.endpoint, &.{}, &self.receive, null);
|
||||||
|
if (!got.isMessage() or got.len < protocol.event_size) return null;
|
||||||
|
return std.mem.bytesToValue(InputEvent, self.receive[0..protocol.event_size]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Subscribe to the input classes named in `device_mask` (an OR of `device_*`, or
|
||||||
|
/// `device_all`). Creates an endpoint for the service to push to and hands it over as a
|
||||||
|
/// capability. Returns a `Subscriber` to loop `next` on, or null on failure.
|
||||||
|
pub fn subscribe(device_mask: u32) ?Subscriber {
|
||||||
|
const service = lookupService() orelse return null;
|
||||||
|
const endpoint = ipc.createIpcEndpoint() orelse return null;
|
||||||
|
|
||||||
|
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.subscribe), .device_mask = device_mask };
|
||||||
|
var reply: [protocol.reply_size]u8 = undefined;
|
||||||
|
const result = ipc.callCap(service, std.mem.asBytes(&request), &reply, endpoint) catch return null;
|
||||||
|
if (result.len < protocol.reply_size) return null;
|
||||||
|
if (std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status != 0) return null;
|
||||||
|
return .{ .endpoint = endpoint };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to every input class (keyboard, mouse, joystick) on one stream.
|
||||||
|
pub fn subscribeAll() ?Subscriber {
|
||||||
|
return subscribe(device_all);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A subscriber filtered to keyboard events, whose `next` returns a decoded `KeyEvent`.
|
||||||
|
pub const KeyboardSubscriber = struct {
|
||||||
|
inner: Subscriber,
|
||||||
|
pub fn next(self: *KeyboardSubscriber) ?KeyEvent {
|
||||||
|
return (self.inner.next() orelse return null).asKeyboard();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A subscriber filtered to mouse events, whose `next` returns a decoded `MouseEvent`.
|
||||||
|
pub const MouseSubscriber = struct {
|
||||||
|
inner: Subscriber,
|
||||||
|
pub fn next(self: *MouseSubscriber) ?MouseEvent {
|
||||||
|
return (self.inner.next() orelse return null).asMouse();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A subscriber filtered to joystick/gamepad events, whose `next` returns a decoded
|
||||||
|
/// `JoystickEvent`.
|
||||||
|
pub const JoystickSubscriber = struct {
|
||||||
|
inner: Subscriber,
|
||||||
|
pub fn next(self: *JoystickSubscriber) ?JoystickEvent {
|
||||||
|
return (self.inner.next() orelse return null).asJoystick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Subscribe to keyboard events only; `next` returns decoded `KeyEvent`s.
|
||||||
|
pub fn subscribeKeyboard() ?KeyboardSubscriber {
|
||||||
|
return .{ .inner = subscribe(device_keyboard) orelse return null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to mouse events only; `next` returns decoded `MouseEvent`s.
|
||||||
|
pub fn subscribeMouse() ?MouseSubscriber {
|
||||||
|
return .{ .inner = subscribe(device_mouse) orelse return null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to joystick/gamepad events only; `next` returns decoded `JoystickEvent`s.
|
||||||
|
pub fn subscribeJoystick() ?JoystickSubscriber {
|
||||||
|
return .{ .inner = subscribe(device_joystick) orelse return null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- publishing -------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A connection to the input service for a source (a keyboard/mouse/joystick driver) that
|
||||||
|
/// publishes events. Each `publish*Event` is a short synchronous call the service answers
|
||||||
|
/// at once; its own fan-out to subscribers is asynchronous, so publishing never blocks on
|
||||||
|
/// a slow subscriber.
|
||||||
|
pub const Publisher = struct {
|
||||||
|
service: ipc.Handle,
|
||||||
|
|
||||||
|
fn publish(self: Publisher, event: InputEvent) bool {
|
||||||
|
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.publish), .event = event };
|
||||||
|
var reply: [protocol.reply_size]u8 = undefined;
|
||||||
|
const len = ipc.call(self.service, std.mem.asBytes(&request), &reply) catch return false;
|
||||||
|
if (len < protocol.reply_size) return false;
|
||||||
|
return std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Broadcast a keyboard event to every subscriber that took keyboard events.
|
||||||
|
pub fn publishKeyboardEvent(self: Publisher, event: KeyEvent) bool {
|
||||||
|
return self.publish(InputEvent.fromKeyboard(event));
|
||||||
|
}
|
||||||
|
/// Broadcast a mouse event to every subscriber that took mouse events.
|
||||||
|
pub fn publishMouseEvent(self: Publisher, event: MouseEvent) bool {
|
||||||
|
return self.publish(InputEvent.fromMouse(event));
|
||||||
|
}
|
||||||
|
/// Broadcast a joystick/gamepad event to every subscriber that took joystick events.
|
||||||
|
pub fn publishJoystickEvent(self: Publisher, event: JoystickEvent) bool {
|
||||||
|
return self.publish(InputEvent.fromJoystick(event));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Connect to the input service as an event source, waiting for it to come up. Returns a
|
||||||
|
/// `Publisher`, or null if the service never registered.
|
||||||
|
pub fn connectSource() ?Publisher {
|
||||||
|
return .{ .service = lookupService() orelse return null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- synthetic scaffolding --------------------------------------------------
|
||||||
|
|
||||||
|
/// Synthetic key events, shared by the demo source and the keyboard driver's placeholder
|
||||||
|
/// stream while real scancode decoding is still a follow-up. `step` rolls through A..E,
|
||||||
|
/// emitting for each key a `key_down`, then a `key_press` carrying the character, then a
|
||||||
|
/// `key_up`. Scaffolding, not wire protocol — hence it lives with the helpers.
|
||||||
|
pub fn syntheticKeyEvent(step: usize) KeyEvent {
|
||||||
|
const Key = struct { code: Keycode, character: u32 };
|
||||||
|
const keys = [_]Key{
|
||||||
|
.{ .code = .a, .character = 'A' },
|
||||||
|
.{ .code = .b, .character = 'B' },
|
||||||
|
.{ .code = .c, .character = 'C' },
|
||||||
|
.{ .code = .d, .character = 'D' },
|
||||||
|
.{ .code = .e, .character = 'E' },
|
||||||
|
};
|
||||||
|
const key = keys[(step / 3) % keys.len];
|
||||||
|
return switch (step % 3) {
|
||||||
|
0 => .{ .kind = @intFromEnum(EventKind.key_down), .keycode = @intFromEnum(key.code), .character = 0, .modifiers = 0 },
|
||||||
|
1 => .{ .kind = @intFromEnum(EventKind.key_press), .keycode = @intFromEnum(key.code), .character = key.character, .modifiers = 0 },
|
||||||
|
else => .{ .kind = @intFromEnum(EventKind.key_up), .keycode = @intFromEnum(key.code), .character = 0, .modifiers = 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synthetic mouse events (placeholder until real PS/2 packet decoding). `step` alternates
|
||||||
|
/// a small diagonal motion with a left-button click.
|
||||||
|
pub fn syntheticMouseEvent(step: usize) MouseEvent {
|
||||||
|
return switch (step % 3) {
|
||||||
|
0 => .{ .kind = @intFromEnum(MouseEventKind.motion), .button = 0, .dx = 1, .dy = 1, .scroll_x = 0, .scroll_y = 0, .buttons = 0 },
|
||||||
|
1 => .{ .kind = @intFromEnum(MouseEventKind.button_down), .button = protocol.mouse_button_left, .dx = 0, .dy = 0, .scroll_x = 0, .scroll_y = 0, .buttons = protocol.mouse_button_left },
|
||||||
|
else => .{ .kind = @intFromEnum(MouseEventKind.button_up), .button = protocol.mouse_button_left, .dx = 0, .dy = 0, .scroll_x = 0, .scroll_y = 0, .buttons = 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synthetic joystick/gamepad events (placeholder until a real controller driver). `step`
|
||||||
|
/// sweeps axis 0 and toggles button 0.
|
||||||
|
pub fn syntheticJoystickEvent(step: usize) JoystickEvent {
|
||||||
|
return switch (step % 3) {
|
||||||
|
0 => .{ .kind = @intFromEnum(JoystickEventKind.axis), .control = 0, .value = 16384, .buttons = 0 },
|
||||||
|
1 => .{ .kind = @intFromEnum(JoystickEventKind.button_down), .control = 0, .value = 0, .buttons = 1 },
|
||||||
|
else => .{ .kind = @intFromEnum(JoystickEventKind.button_up), .control = 0, .value = 0, .buttons = 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -79,16 +79,41 @@ pub fn call(h: Handle, message: []const u8, reply: []u8) CallError!usize {
|
|||||||
return (try callCap(h, message, reply, null)).len;
|
return (try callCap(h, message, reply, null)).len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Post `message` to endpoint `h`'s asynchronous queue and return immediately — no
|
||||||
|
/// rendezvous, no reply, no blocking. The receiver picks it up through `replyWait` as a
|
||||||
|
/// buffered message (`Received.isMessage`). Unlike `call`, this **cannot hang on a dead
|
||||||
|
/// or slow peer**, which is why a broadcaster (the input service) delivers events this
|
||||||
|
/// way. The payload must fit an endpoint slot (64 bytes); a full queue drops the oldest
|
||||||
|
/// message. Returns false on failure (bad handle, oversized payload, bad buffer).
|
||||||
|
pub fn send(h: Handle, message: []const u8) bool {
|
||||||
|
return !failed(sc.systemCall3(.ipc_send, h, @intFromPtr(message.ptr), message.len));
|
||||||
|
}
|
||||||
|
|
||||||
/// Set in `Received.badge` when what arrived is an asynchronous notification — a
|
/// Set in `Received.badge` when what arrived is an asynchronous notification — a
|
||||||
/// bound device interrupt — rather than a client's message. The low bits carry the
|
/// bound device interrupt — rather than a client's message. The low bits carry the
|
||||||
/// GSI. See `isNotification`.
|
/// GSI. See `isNotification`.
|
||||||
pub const notify_badge_bit: u64 = abi.notify_badge_bit;
|
pub const notify_badge_bit: u64 = abi.notify_badge_bit;
|
||||||
|
|
||||||
|
/// Set alongside `notify_badge_bit` when the notification is a **signal** — the
|
||||||
|
/// lifecycle vocabulary of docs/process-lifecycle.md, delivered to the endpoint
|
||||||
|
/// nominated with `process.bindSignals`. Decode with `process.signalsFrom`.
|
||||||
|
pub const notify_signal_bit: u64 = abi.notify_signal_bit;
|
||||||
|
|
||||||
|
/// Set alongside `notify_badge_bit` when the notification is a **one-shot timer**
|
||||||
|
/// landing (`system.timerOnce`).
|
||||||
|
pub const notify_timer_bit: u64 = abi.notify_timer_bit;
|
||||||
|
|
||||||
/// Set alongside `notify_badge_bit` when the notification is a **child-exit
|
/// Set alongside `notify_badge_bit` when the notification is a **child-exit
|
||||||
/// notice** — a process this one spawned (with an exit endpoint) has ended —
|
/// notice** — a process this one spawned (with an exit endpoint) has ended —
|
||||||
/// rather than a device interrupt. The low bits carry the child's process id.
|
/// rather than a device interrupt. The low bits carry the child's process id.
|
||||||
pub const notify_exit_bit: u64 = abi.notify_exit_bit;
|
pub const notify_exit_bit: u64 = abi.notify_exit_bit;
|
||||||
|
|
||||||
|
/// Set alongside `notify_badge_bit` when the wake-up is a **buffered message** — a payload
|
||||||
|
/// posted with `send` (`ipc_send`) — rather than a bare device interrupt or child-exit
|
||||||
|
/// notice. The payload is in the `replyWait` receive buffer (`Received.len` bytes); the
|
||||||
|
/// low bits of the badge carry the sender's task id. See `Received.isMessage`.
|
||||||
|
pub const notify_message_bit: u64 = abi.notify_message_bit;
|
||||||
|
|
||||||
/// The result of a `replyWait`: the request length, the sender's badge (a task id, or
|
/// The result of a `replyWait`: the request length, the sender's badge (a task id, or
|
||||||
/// an IRQ notification if the high bit is set), and any capability the request carried.
|
/// an IRQ notification if the high bit is set), and any capability the request carried.
|
||||||
pub const Received = struct {
|
pub const Received = struct {
|
||||||
@@ -109,6 +134,30 @@ pub const Received = struct {
|
|||||||
return self.isNotification() and self.badge & notify_exit_bit != 0;
|
return self.isNotification() and self.badge & notify_exit_bit != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if this wake-up is a **buffered message** posted with `send` (`ipc_send`):
|
||||||
|
/// there is a payload in the receive buffer (`self.len` bytes) and no reply is owed.
|
||||||
|
/// The subscriber side of a broadcast branches on this.
|
||||||
|
pub fn isMessage(self: Received) bool {
|
||||||
|
return self.isNotification() and self.badge & notify_message_bit != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The task id of whoever posted a buffered message, meaningful only when
|
||||||
|
/// Whether this arrival is a signal notification — decode the set with
|
||||||
|
/// `process.signalsFrom(badge)`.
|
||||||
|
pub fn isSignal(self: Received) bool {
|
||||||
|
return self.isNotification() and self.badge & notify_signal_bit != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this arrival is a one-shot timer landing (`system.timerOnce`).
|
||||||
|
pub fn isTimer(self: Received) bool {
|
||||||
|
return self.isNotification() and self.badge & notify_timer_bit != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `isMessage`. (The badge's low bits, with the three high marker bits masked off.)
|
||||||
|
pub fn senderTaskId(self: Received) u32 {
|
||||||
|
return @intCast(self.badge & ~(notify_badge_bit | notify_exit_bit | notify_message_bit));
|
||||||
|
}
|
||||||
|
|
||||||
/// The interrupt source (a GSI), meaningful only when `isNotification` and
|
/// The interrupt source (a GSI), meaningful only when `isNotification` and
|
||||||
/// not `isChildExit`.
|
/// not `isChildExit`.
|
||||||
pub fn source(self: Received) u64 {
|
pub fn source(self: Received) u64 {
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
//! Process-level runtime types: what a user program receives at entry. Mirrors
|
//! Process-level runtime types: what a user program receives at entry (`Init`,
|
||||||
|
//! the argv contract) and the process end of the lifecycle
|
||||||
|
//! (docs/process-lifecycle.md) — today the exit reason a supervisor reads to
|
||||||
|
//! decide restart; signals and the stop sequence land here with M17.4. Mirrors
|
||||||
//! the spirit of `std.process.Init.Minimal` in danos terms — std's `Args` holds
|
//! the spirit of `std.process.Init.Minimal` in danos terms — std's `Args` holds
|
||||||
//! no data on freestanding targets, so the type is danos's own.
|
//! no data on freestanding targets, so the type is danos's own.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const abi = @import("abi");
|
||||||
|
const sc = @import("system-call.zig");
|
||||||
|
const ipc = @import("ipc.zig");
|
||||||
|
const system = @import("system.zig");
|
||||||
|
|
||||||
/// Everything a program receives at entry. Passed to
|
/// Everything a program receives at entry. Passed to
|
||||||
/// `pub fn main(init: runtime.process.Init)`; programs that need nothing keep
|
/// `pub fn main(init: runtime.process.Init)`; programs that need nothing keep
|
||||||
@@ -45,3 +52,86 @@ pub const Arguments = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// How a process ended — what a supervisor's restart policy reads: a clean exit
|
||||||
|
/// meant to stop, a fault wants a restart with backoff, killed means the
|
||||||
|
/// supervisor did it itself (docs/process-lifecycle.md).
|
||||||
|
pub const ExitReason = abi.ExitReason;
|
||||||
|
|
||||||
|
/// How dead child `id` ended. Ask after the exit notification arrives — the
|
||||||
|
/// kernel records the reason before it posts the notification, so this never
|
||||||
|
/// races it. Returns null for an id that never lived, is still alive, was
|
||||||
|
/// evicted from the kernel's bounded record, or is not this process's child
|
||||||
|
/// (the same authority gate as `kill`).
|
||||||
|
pub fn exitReason(id: u32) ?ExitReason {
|
||||||
|
const r = sc.systemCall1(.process_exit_reason, id);
|
||||||
|
if (r > ~@as(usize, 0) - 4095) return null; // a wrapped -errno
|
||||||
|
return @enumFromInt(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The signal vocabulary (docs/process-lifecycle.md): POSIX's concepts, danos's
|
||||||
|
/// names, message delivery. A signal is a one-way coalescing statement — never a
|
||||||
|
/// question (liveness is the zero-length ping call) and never kill (that is
|
||||||
|
/// `system.kill`, unhandleable by definition).
|
||||||
|
pub const Signal = abi.Signal;
|
||||||
|
|
||||||
|
/// The coalesced set of signals one notification delivered: two pending
|
||||||
|
/// terminates arrive as one. Decode a received badge with `signalsFrom`.
|
||||||
|
pub const SignalSet = struct {
|
||||||
|
pending: u32,
|
||||||
|
|
||||||
|
pub fn has(set: SignalSet, signal: Signal) bool {
|
||||||
|
return set.pending & (@as(u32, 1) << @intFromEnum(signal)) != 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Nominate `endpoint` as this process's signal endpoint. Signals posted while
|
||||||
|
/// unbound have pended; they are delivered immediately on bind, coalesced.
|
||||||
|
pub fn bindSignals(endpoint: usize) bool {
|
||||||
|
return sc.systemCall1(.signal_bind, endpoint) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a received badge into the signals it delivered, or null if it is not
|
||||||
|
/// a signal notification.
|
||||||
|
pub fn signalsFrom(badge: u64) ?SignalSet {
|
||||||
|
if (badge & abi.notify_badge_bit == 0 or badge & abi.notify_signal_bit == 0) return null;
|
||||||
|
return .{ .pending = @truncate(badge & ~(abi.notify_badge_bit | abi.notify_signal_bit)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Post `signal` to child `id` (or to yourself). Supervisor-gated, like kill;
|
||||||
|
/// non-blocking, always — a statement, not a conversation.
|
||||||
|
pub fn sendSignal(id: u32, signal: Signal) bool {
|
||||||
|
return sc.systemCall2(.process_signal, id, @intFromEnum(signal)) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The standard stop sequence (docs/process-lifecycle.md): terminate, wait up to
|
||||||
|
/// `deadline_ms` for the exit notification on `exit_endpoint` (the endpoint the
|
||||||
|
/// child was spawned with), then kill. Any *other* notifications arriving on
|
||||||
|
/// that endpoint while stopping are consumed and dropped — a supervisor with
|
||||||
|
/// concurrent traffic implements the same sequence inside its own event loop
|
||||||
|
/// (arm `system.timerOnce`, keep serving) instead of calling this.
|
||||||
|
pub fn stop(id: u32, deadline_ms: u64, exit_endpoint: usize) void {
|
||||||
|
_ = sendSignal(id, .terminate);
|
||||||
|
_ = system.timerOnce(exit_endpoint, deadline_ms);
|
||||||
|
var receive: [8]u8 = undefined;
|
||||||
|
while (true) {
|
||||||
|
const got = ipc.replyWait(exit_endpoint, &.{}, &receive, null);
|
||||||
|
if (got.isChildExit() and got.childProcessId() == id) return;
|
||||||
|
if (got.isTimer()) break; // the deadline passed first — escalate
|
||||||
|
}
|
||||||
|
_ = system.kill(id);
|
||||||
|
while (true) {
|
||||||
|
const got = ipc.replyWait(exit_endpoint, &.{}, &receive, null);
|
||||||
|
if (got.isChildExit() and got.childProcessId() == id) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe `endpoint` to published exit events: every process death posts an
|
||||||
|
/// asynchronous notification with the same badge encoding as a supervisor's exit
|
||||||
|
/// notice (decode with `ipc.Received.isChildExit`/`childProcessId`). For stateful
|
||||||
|
/// services: release what the dead client held — file handles, subscriptions —
|
||||||
|
/// because a service must never depend on clients cleaning up after themselves
|
||||||
|
/// (docs/process-lifecycle.md). Ungated, like `system.processes`.
|
||||||
|
pub fn subscribeExits(endpoint: usize) bool {
|
||||||
|
return sc.systemCall1(.process_subscribe, endpoint) == 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,11 +12,25 @@
|
|||||||
//! (arguments arrive via `init`).
|
//! (arguments arrive via `init`).
|
||||||
|
|
||||||
pub const system = @import("system.zig");
|
pub const system = @import("system.zig");
|
||||||
|
/// Monotonic time, delays, and deadlines over the kernel clock/sleep/timer syscalls
|
||||||
|
/// — an `Instant`/`Duration` front door, no time service (docs/timers.md).
|
||||||
|
pub const time = @import("time.zig");
|
||||||
pub const heap = @import("heap.zig");
|
pub const heap = @import("heap.zig");
|
||||||
pub const ipc = @import("ipc.zig");
|
pub const ipc = @import("ipc.zig");
|
||||||
pub const start = @import("start.zig");
|
pub const start = @import("start.zig");
|
||||||
/// The VFS wire protocol (shared with the VFS server).
|
/// The VFS wire protocol (shared with the VFS server).
|
||||||
pub const vfs_protocol = @import("vfs-protocol");
|
pub const vfs_protocol = @import("vfs-protocol");
|
||||||
|
|
||||||
|
/// The device-manager protocol: hello + tree reports (docs/device-manager.md).
|
||||||
|
pub const device_manager_protocol = @import("device-manager-protocol");
|
||||||
|
|
||||||
|
/// The power protocol: events (button, lid, battery) + shutdown (docs/power.md).
|
||||||
|
pub const power_protocol = @import("power-protocol");
|
||||||
|
/// Keyboard-event listening (subscribe/next) and broadcasting (publish), over the input
|
||||||
|
/// service. See library/runtime/input.zig and system/services/input/.
|
||||||
|
pub const input = @import("input.zig");
|
||||||
|
/// The input wire protocol (shared with the input service and its clients).
|
||||||
|
pub const input_protocol = @import("input-protocol");
|
||||||
/// POSIX-style file API: open/read/write/lseek/stat/close.
|
/// POSIX-style file API: open/read/write/lseek/stat/close.
|
||||||
/// C stdio: fopen/fread/fwrite/fseek/ftell/fclose over unistd.
|
/// C stdio: fopen/fread/fwrite/fseek/ftell/fclose over unistd.
|
||||||
/// Device access for drivers: enumerate/claim/mmioMap.
|
/// Device access for drivers: enumerate/claim/mmioMap.
|
||||||
@@ -24,11 +38,34 @@ pub const device = @import("device.zig");
|
|||||||
/// DMA-capable memory for drivers: contiguous, pinned, uncacheable buffers.
|
/// DMA-capable memory for drivers: contiguous, pinned, uncacheable buffers.
|
||||||
pub const dma = @import("dma.zig");
|
pub const dma = @import("dma.zig");
|
||||||
|
|
||||||
|
/// USB class-driver client: open a device on the xHCI bus and drive it
|
||||||
|
/// (control / interrupt / bulk transfers). See library/runtime/usb.zig.
|
||||||
|
pub const usb = @import("usb.zig");
|
||||||
|
|
||||||
|
/// Block-device client: read/write a block device (a USB stick, via
|
||||||
|
/// usb-storage). See library/runtime/block.zig.
|
||||||
|
pub const block = @import("block.zig");
|
||||||
|
|
||||||
|
/// Display-service client: query the mode, and (from D3) create layers, draw, and
|
||||||
|
/// present frames. See library/runtime/display.zig and system/services/display/.
|
||||||
|
pub const display = @import("display.zig");
|
||||||
|
/// The display wire protocol (shared with the display service and its clients).
|
||||||
|
pub const display_protocol = @import("display-protocol");
|
||||||
|
|
||||||
|
/// The danos-native file API (open/read/write/list over the user-space VFS) — the
|
||||||
|
/// layer danos programs use directly, and where the operations that later become
|
||||||
|
/// `std.os.danos` are staged. See docs/zig-self-hosting.md.
|
||||||
|
pub const fs = @import("fs.zig");
|
||||||
|
|
||||||
/// Re-exported so a user binary can `pub const panic = runtime.panic;`.
|
/// Re-exported so a user binary can `pub const panic = runtime.panic;`.
|
||||||
pub const panic = start.panic;
|
pub const panic = start.panic;
|
||||||
|
|
||||||
/// Process entry types: the `Init` handed to `main`, and its `Arguments`.
|
/// Process entry types: the `Init` handed to `main`, and its `Arguments`.
|
||||||
pub const process = @import("process.zig");
|
pub const process = @import("process.zig");
|
||||||
|
|
||||||
|
/// The service harness: one replyWait loop folding requests, signals, and
|
||||||
|
/// notifications into callbacks (docs/process-lifecycle.md).
|
||||||
|
pub const service = @import("service.zig");
|
||||||
|
|
||||||
/// The heap as a `std.mem.Allocator`, for Zig `std` containers in user code.
|
/// The heap as a `std.mem.Allocator`, for Zig `std` containers in user code.
|
||||||
pub const allocator = heap.allocator;
|
pub const allocator = heap.allocator;
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
//! The service harness (docs/process-lifecycle.md): one replyWait loop that
|
||||||
|
//! folds protocol requests, signals, and subscribed notifications into
|
||||||
|
//! callbacks — so the lifecycle contract ("answers ping, exits on terminate")
|
||||||
|
//! is satisfied by construction and a service author writes domain logic only.
|
||||||
|
//! Nothing is asynchronous inside the process: a callback runs at a point the
|
||||||
|
//! loop chose, never on a hijacked stack — the whole reason signals are
|
||||||
|
//! messages.
|
||||||
|
//!
|
||||||
|
//! The liveness probe: a **zero-length request is the universal ping**, answered
|
||||||
|
//! with a zero-length reply by the harness itself. No protocol's requests start
|
||||||
|
//! at length zero, so the encoding cannot collide, and there is nothing for a
|
||||||
|
//! service author to implement — a wedged service simply fails to answer, which
|
||||||
|
//! is the diagnosis (see docs/ipc.md).
|
||||||
|
|
||||||
|
const abi = @import("abi");
|
||||||
|
const ipc = @import("ipc.zig");
|
||||||
|
const process = @import("process.zig");
|
||||||
|
|
||||||
|
pub const Callbacks = struct {
|
||||||
|
/// Called once with the service's endpoint before the loop starts — the
|
||||||
|
/// place to subscribe to exit events, bind IRQs, or announce readiness.
|
||||||
|
/// Return false to abort startup (the process exits).
|
||||||
|
init: ?*const fn (endpoint: ipc.Handle) bool = null,
|
||||||
|
/// One protocol request from `sender` (a task id): write the reply into
|
||||||
|
/// `reply`, return its length. `capability` is the handle the request
|
||||||
|
/// carried, if any (M13 cap passing — how a subscriber hands over its
|
||||||
|
/// endpoint). The zero-length ping never reaches this.
|
||||||
|
on_message: *const fn (message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize,
|
||||||
|
/// A notification that is not a signal — a subscribed exit event, a bound
|
||||||
|
/// IRQ, a timer landing. The raw badge; decode with the ipc helpers.
|
||||||
|
on_notification: ?*const fn (badge: u64) void = null,
|
||||||
|
/// The reload signal. Default: ignored.
|
||||||
|
on_reload: ?*const fn () void = null,
|
||||||
|
/// The terminate signal, called before the loop returns. The clean exit is
|
||||||
|
/// the return itself — never put *necessary* work here (iron rule 1: a kill
|
||||||
|
/// arrives with no warning; this is for graceful extras only).
|
||||||
|
on_terminate: ?*const fn () void = null,
|
||||||
|
/// Publish the endpoint under a well-known service id at startup.
|
||||||
|
service: ?abi.ServiceId = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Run the service: create and (optionally) register the endpoint, bind signals
|
||||||
|
/// to it, call `init`, then serve until `terminate` arrives — at which point the
|
||||||
|
/// loop returns and main's return is the clean exit the supervisor reads as
|
||||||
|
/// `ExitReason.exited`. `maximum_message` sizes the receive and reply buffers
|
||||||
|
/// (a service passes its protocol's message maximum).
|
||||||
|
pub fn run(comptime maximum_message: usize, callbacks: Callbacks) void {
|
||||||
|
const endpoint = ipc.createIpcEndpoint() orelse return;
|
||||||
|
if (callbacks.service) |id| {
|
||||||
|
if (!ipc.register(id, endpoint)) return;
|
||||||
|
}
|
||||||
|
_ = process.bindSignals(endpoint);
|
||||||
|
if (callbacks.init) |initialise| {
|
||||||
|
if (!initialise(endpoint)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reply_buffer: [maximum_message]u8 = undefined;
|
||||||
|
var reply_len: usize = 0;
|
||||||
|
var receive: [maximum_message]u8 = undefined;
|
||||||
|
while (true) {
|
||||||
|
const got = ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
|
||||||
|
if (got.isNotification()) {
|
||||||
|
reply_len = 0; // nothing owed for a notification
|
||||||
|
if (process.signalsFrom(got.badge)) |signals| {
|
||||||
|
if (signals.has(.reload)) {
|
||||||
|
if (callbacks.on_reload) |onReload| onReload();
|
||||||
|
}
|
||||||
|
if (signals.has(.terminate)) {
|
||||||
|
if (callbacks.on_terminate) |onTerminate| onTerminate();
|
||||||
|
return; // the loop's return IS the clean exit
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (callbacks.on_notification) |onNotification| onNotification(got.badge);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (got.len == 0) {
|
||||||
|
reply_len = 0; // the universal ping: a zero-length reply, from the harness
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
reply_len = callbacks.on_message(receive[0..got.len], &reply_buffer, got.senderTaskId(), got.cap);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,34 +19,49 @@ pub inline fn systemCall0(n: SystemCall) usize {
|
|||||||
pub inline fn systemCall1(n: SystemCall, a0: usize) usize {
|
pub inline fn systemCall1(n: SystemCall, a0: usize) usize {
|
||||||
return asm volatile ("syscall"
|
return asm volatile ("syscall"
|
||||||
: [ret] "={rax}" (-> usize),
|
: [ret] "={rax}" (-> usize),
|
||||||
: [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0),
|
: [n] "{rax}" (@intFromEnum(n)),
|
||||||
|
[a0] "{rdi}" (a0),
|
||||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub inline fn systemCall2(n: SystemCall, a0: usize, a1: usize) usize {
|
pub inline fn systemCall2(n: SystemCall, a0: usize, a1: usize) usize {
|
||||||
return asm volatile ("syscall"
|
return asm volatile ("syscall"
|
||||||
: [ret] "={rax}" (-> usize),
|
: [ret] "={rax}" (-> usize),
|
||||||
: [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0), [a1] "{rsi}" (a1),
|
: [n] "{rax}" (@intFromEnum(n)),
|
||||||
|
[a0] "{rdi}" (a0),
|
||||||
|
[a1] "{rsi}" (a1),
|
||||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub inline fn systemCall3(n: SystemCall, a0: usize, a1: usize, a2: usize) usize {
|
pub inline fn systemCall3(n: SystemCall, a0: usize, a1: usize, a2: usize) usize {
|
||||||
return asm volatile ("syscall"
|
return asm volatile ("syscall"
|
||||||
: [ret] "={rax}" (-> usize),
|
: [ret] "={rax}" (-> usize),
|
||||||
: [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0), [a1] "{rsi}" (a1), [a2] "{rdx}" (a2),
|
: [n] "{rax}" (@intFromEnum(n)),
|
||||||
|
[a0] "{rdi}" (a0),
|
||||||
|
[a1] "{rsi}" (a1),
|
||||||
|
[a2] "{rdx}" (a2),
|
||||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub inline fn systemCall4(n: SystemCall, a0: usize, a1: usize, a2: usize, a3: usize) usize {
|
pub inline fn systemCall4(n: SystemCall, a0: usize, a1: usize, a2: usize, a3: usize) usize {
|
||||||
return asm volatile ("syscall"
|
return asm volatile ("syscall"
|
||||||
: [ret] "={rax}" (-> usize),
|
: [ret] "={rax}" (-> usize),
|
||||||
: [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0), [a1] "{rsi}" (a1), [a2] "{rdx}" (a2), [a3] "{r10}" (a3),
|
: [n] "{rax}" (@intFromEnum(n)),
|
||||||
|
[a0] "{rdi}" (a0),
|
||||||
|
[a1] "{rsi}" (a1),
|
||||||
|
[a2] "{rdx}" (a2),
|
||||||
|
[a3] "{r10}" (a3),
|
||||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub inline fn systemCall5(n: SystemCall, a0: usize, a1: usize, a2: usize, a3: usize, a4: usize) usize {
|
pub inline fn systemCall5(n: SystemCall, a0: usize, a1: usize, a2: usize, a3: usize, a4: usize) usize {
|
||||||
return asm volatile ("syscall"
|
return asm volatile ("syscall"
|
||||||
: [ret] "={rax}" (-> usize),
|
: [ret] "={rax}" (-> usize),
|
||||||
: [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0), [a1] "{rsi}" (a1), [a2] "{rdx}" (a2), [a3] "{r10}" (a3), [a4] "{r8}" (a4),
|
: [n] "{rax}" (@intFromEnum(n)),
|
||||||
|
[a0] "{rdi}" (a0),
|
||||||
|
[a1] "{rsi}" (a1),
|
||||||
|
[a2] "{rdx}" (a2),
|
||||||
|
[a3] "{r10}" (a3),
|
||||||
|
[a4] "{r8}" (a4),
|
||||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ pub fn sleep(ms: usize) void {
|
|||||||
_ = sc.systemCall1(.sleep, ms);
|
_ = sc.systemCall1(.sleep, ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Arm a one-shot timer: after `ms` milliseconds the kernel posts a timer
|
||||||
|
/// notification (`ipc.Received.isTimer`) to `endpoint`. The timed wait of
|
||||||
|
/// docs/process-lifecycle.md — a service arms a deadline and keeps serving,
|
||||||
|
/// instead of blocking in sleep; what stop-sequence escalation, hello deadlines,
|
||||||
|
/// and restart backoff are built from.
|
||||||
|
pub fn timerOnce(endpoint: usize, ms: u64) bool {
|
||||||
|
return sc.systemCall2(.timer_bind, endpoint, ms) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// Monotonic nanoseconds since boot — a time source for timeouts and short delays. It
|
/// Monotonic nanoseconds since boot — a time source for timeouts and short delays. It
|
||||||
/// only ever moves forward. This is *not* wall-clock time (no date, no timezone — that
|
/// only ever moves forward. This is *not* wall-clock time (no date, no timezone — that
|
||||||
/// is a user-space service layered on top). Deadline pattern for a bounded poll loop:
|
/// is a user-space service layered on top). Deadline pattern for a bounded poll loop:
|
||||||
@@ -42,6 +51,24 @@ pub fn clock() u64 {
|
|||||||
return @intCast(sc.systemCall0(.clock));
|
return @intCast(sc.systemCall0(.clock));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wall-clock time in Unix epoch seconds (UTC) — the real date/time, from the RTC.
|
||||||
|
/// Unlike `clock` (monotonic since boot), this tracks calendar time, so it is what a
|
||||||
|
/// filesystem stamps as a file's modification time. Formatting it into a calendar
|
||||||
|
/// date/timezone is user-space policy layered on top.
|
||||||
|
pub fn wallClock() u64 {
|
||||||
|
return @intCast(sc.systemCall0(.wall_clock));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy bytes out of the kernel's in-memory diagnostic log — the accumulated
|
||||||
|
/// stream of everything `write` (and the kernel itself) has emitted — starting at
|
||||||
|
/// `offset`, into `out`. Returns the number of bytes copied (0 at end of buffer).
|
||||||
|
/// A program reads the whole log by looping from offset 0, advancing by the return
|
||||||
|
/// value, until it gets 0. This is how the boot log is persisted to disk on a
|
||||||
|
/// headless/real machine where serial output is otherwise lost.
|
||||||
|
pub fn klogRead(offset: usize, out: []u8) usize {
|
||||||
|
return sc.systemCall3(.klog_read, offset, @intFromPtr(out.ptr), out.len);
|
||||||
|
}
|
||||||
|
|
||||||
/// End the process. Never returns.
|
/// End the process. Never returns.
|
||||||
pub fn exit(code: usize) noreturn {
|
pub fn exit(code: usize) noreturn {
|
||||||
_ = sc.systemCall1(.exit, code);
|
_ = sc.systemCall1(.exit, code);
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
//! The danos time interface — monotonic time, delays, and deadlines for user space.
|
||||||
|
//!
|
||||||
|
//! There is no time *service*: the kernel already owns the scheduling timer and
|
||||||
|
//! surfaces it directly, so reading the clock is one system call (an `rdtsc` and a
|
||||||
|
//! scale), never an IPC round trip (docs/timers.md explains why). This module is a
|
||||||
|
//! thin, generic layer over the `clock`/`sleep`/`timer_bind` wrappers in `system.zig`
|
||||||
|
//! — an ergonomic `Instant`/`Duration` front door, not new mechanism.
|
||||||
|
//!
|
||||||
|
//! It is **monotonic** time only: nanoseconds since boot, moving forward, no date or
|
||||||
|
//! timezone. Wall-clock/calendar time is a separate user-space service (an RTC-backed
|
||||||
|
//! CLOCK_REALTIME) layered on top later.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const system = @import("system.zig");
|
||||||
|
|
||||||
|
const nanos_per_micro: u64 = 1_000;
|
||||||
|
const nanos_per_milli: u64 = 1_000_000;
|
||||||
|
const nanos_per_second: u64 = 1_000_000_000;
|
||||||
|
|
||||||
|
/// A span of time, held as nanoseconds. Constructors name their unit; accessors
|
||||||
|
/// truncate toward zero. `ceilMillis` rounds *up*, since `sleep`/`after` land on the
|
||||||
|
/// kernel's millisecond granularity and rounding down could return early.
|
||||||
|
pub const Duration = struct {
|
||||||
|
ns: u64,
|
||||||
|
|
||||||
|
pub fn fromNanos(n: u64) Duration {
|
||||||
|
return .{ .ns = n };
|
||||||
|
}
|
||||||
|
pub fn fromMicros(n: u64) Duration {
|
||||||
|
return .{ .ns = n *| nanos_per_micro };
|
||||||
|
}
|
||||||
|
pub fn fromMillis(n: u64) Duration {
|
||||||
|
return .{ .ns = n *| nanos_per_milli };
|
||||||
|
}
|
||||||
|
pub fn fromSeconds(n: u64) Duration {
|
||||||
|
return .{ .ns = n *| nanos_per_second };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asNanos(d: Duration) u64 {
|
||||||
|
return d.ns;
|
||||||
|
}
|
||||||
|
pub fn asMicros(d: Duration) u64 {
|
||||||
|
return d.ns / nanos_per_micro;
|
||||||
|
}
|
||||||
|
pub fn asMillis(d: Duration) u64 {
|
||||||
|
return d.ns / nanos_per_milli;
|
||||||
|
}
|
||||||
|
pub fn asSeconds(d: Duration) u64 {
|
||||||
|
return d.ns / nanos_per_second;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whole milliseconds, rounded up — the argument `sleep`/`after` pass the kernel.
|
||||||
|
/// A non-zero sub-millisecond duration becomes 1 ms rather than 0.
|
||||||
|
pub fn ceilMillis(d: Duration) u64 {
|
||||||
|
return (d.ns +| (nanos_per_milli - 1)) / nanos_per_milli;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn plus(a: Duration, b: Duration) Duration {
|
||||||
|
return .{ .ns = a.ns +| b.ns };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A point on the monotonic clock — nanoseconds since boot. Compare and subtract
|
||||||
|
/// instants to measure elapsed time; it never runs backward, so `since` is safe to
|
||||||
|
/// saturate at zero rather than wrap.
|
||||||
|
pub const Instant = struct {
|
||||||
|
ns: u64,
|
||||||
|
|
||||||
|
/// The span from `earlier` to `self`, saturating at zero if `earlier` is later
|
||||||
|
/// (which the monotonic clock should never produce, but callers may pass any pair).
|
||||||
|
pub fn since(self: Instant, earlier: Instant) Duration {
|
||||||
|
return .{ .ns = self.ns -| earlier.ns };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How long since this instant, sampled now.
|
||||||
|
pub fn elapsed(self: Instant) Duration {
|
||||||
|
return now().since(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This instant advanced by `d` (a deadline, `d` from here).
|
||||||
|
pub fn plus(self: Instant, d: Duration) Instant {
|
||||||
|
return .{ .ns = self.ns +| d.ns };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the monotonic clock has reached this instant (used as a deadline).
|
||||||
|
pub fn reached(deadline: Instant) bool {
|
||||||
|
return now().ns >= deadline.ns;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The current monotonic time.
|
||||||
|
pub fn now() Instant {
|
||||||
|
return .{ .ns = system.clock() };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Monotonic nanoseconds since boot — the raw `clock()` reading, for callers that
|
||||||
|
/// want a plain integer instead of an `Instant`.
|
||||||
|
pub fn monotonicNanos() u64 {
|
||||||
|
return system.clock();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the monotonic clock is usable. The kernel returns 0 until the TSC is
|
||||||
|
/// calibrated (`tsc_hz == 0`); a caller that needs real time can treat that as
|
||||||
|
/// "unavailable" instead of assuming the clock advances.
|
||||||
|
pub fn available() bool {
|
||||||
|
return system.clock() != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Block the caller for at least `d`, rounded up to the kernel's millisecond
|
||||||
|
/// granularity. For sub-millisecond precision the scheduler cannot express, use
|
||||||
|
/// `spin`.
|
||||||
|
pub fn sleep(d: Duration) void {
|
||||||
|
system.sleep(d.ceilMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Block the caller for `ms` milliseconds — the coarse, allocation-free form.
|
||||||
|
pub fn sleepMillis(ms: u64) void {
|
||||||
|
system.sleep(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Busy-wait until `d` has elapsed, polling the monotonic clock. This burns the CPU
|
||||||
|
/// on purpose, to hit sub-millisecond delays the scheduler's millisecond tick cannot.
|
||||||
|
/// Prefer `sleep` for anything at or above a millisecond.
|
||||||
|
pub fn spin(d: Duration) void {
|
||||||
|
const deadline = now().plus(d);
|
||||||
|
while (!deadline.reached()) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arm a one-shot timer against `endpoint` (a handle from `ipc.createIpcEndpoint`):
|
||||||
|
/// after `d` the kernel posts a timer notification (`ipc.Received.isTimer`) there.
|
||||||
|
/// Unlike `sleep`, this does not block — a service can keep serving IPC on the same
|
||||||
|
/// endpoint while the deadline is pending. Rounds `d` up to milliseconds; returns
|
||||||
|
/// false if the timer could not be armed. See `system.timerOnce`.
|
||||||
|
pub fn after(endpoint: usize, d: Duration) bool {
|
||||||
|
return system.timerOnce(endpoint, d.ceilMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Duration unit conversions round toward zero" {
|
||||||
|
try std.testing.expectEqual(@as(u64, 1_000_000_000), Duration.fromSeconds(1).asNanos());
|
||||||
|
try std.testing.expectEqual(@as(u64, 1_500), Duration.fromNanos(1_500).asNanos());
|
||||||
|
try std.testing.expectEqual(@as(u64, 2), Duration.fromMillis(2).asMillis());
|
||||||
|
try std.testing.expectEqual(@as(u64, 1), Duration.fromNanos(1_999_999).asMillis());
|
||||||
|
try std.testing.expectEqual(@as(u64, 250), Duration.fromMicros(250).asMicros());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "ceilMillis rounds up, and never turns a nonzero span into zero" {
|
||||||
|
try std.testing.expectEqual(@as(u64, 0), Duration.fromNanos(0).ceilMillis());
|
||||||
|
try std.testing.expectEqual(@as(u64, 1), Duration.fromNanos(1).ceilMillis());
|
||||||
|
try std.testing.expectEqual(@as(u64, 1), Duration.fromMillis(1).ceilMillis());
|
||||||
|
try std.testing.expectEqual(@as(u64, 2), Duration.fromNanos(nanos_per_milli + 1).ceilMillis());
|
||||||
|
try std.testing.expectEqual(@as(u64, 5), Duration.fromMillis(5).ceilMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Instant arithmetic: since saturates, plus/reached form deadlines" {
|
||||||
|
const t0 = Instant{ .ns = 1_000 };
|
||||||
|
const t1 = Instant{ .ns = 4_000 };
|
||||||
|
try std.testing.expectEqual(@as(u64, 3_000), t1.since(t0).asNanos());
|
||||||
|
// earlier-than-self can't happen on a monotonic clock; saturate rather than wrap.
|
||||||
|
try std.testing.expectEqual(@as(u64, 0), t0.since(t1).asNanos());
|
||||||
|
const deadline = t0.plus(Duration.fromNanos(2_500));
|
||||||
|
try std.testing.expectEqual(@as(u64, 3_500), deadline.ns);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "saturating arithmetic does not overflow at the u64 ceiling" {
|
||||||
|
const big = Duration.fromSeconds(std.math.maxInt(u64));
|
||||||
|
try std.testing.expectEqual(@as(u64, std.math.maxInt(u64)), big.asNanos());
|
||||||
|
const late = Instant{ .ns = std.math.maxInt(u64) };
|
||||||
|
try std.testing.expectEqual(@as(u64, std.math.maxInt(u64)), late.plus(Duration.fromSeconds(10)).ns);
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
//! USB class-driver client: the helper a keyboard, mouse, or mass-storage driver
|
||||||
|
//! uses to reach its device through the xHCI bus driver, so it never hand-rolls
|
||||||
|
//! the transfer-protocol IPC. Layered over `ipc` and the shared
|
||||||
|
//! `usb-transfer-protocol` wire format, the way `input.zig` layers over the input
|
||||||
|
//! service and `device.zig` over the raw device calls.
|
||||||
|
//!
|
||||||
|
//! A class driver, spawned with its interface's assigned device id as argv[1]:
|
||||||
|
//! if (!usb.helloManager(id)) return; // meet the spawn deadline
|
||||||
|
//! var device = usb.open(id) orelse return; // open + get its endpoints
|
||||||
|
//! _ = device.controlOut(usb_abi.setProtocol(...));// class requests, descriptors
|
||||||
|
//! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously
|
||||||
|
//! while (true) { ... ipc.replyWait(device.endpoint, ...) ... } // its own loop
|
||||||
|
//!
|
||||||
|
//! Reports are delivered to `device.endpoint` as asynchronous `InterruptReport`
|
||||||
|
//! messages (the class driver runs a bare `replyWait` loop to read them, because
|
||||||
|
//! the service harness drops buffered-message payloads — see service.zig).
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const ipc = @import("ipc.zig");
|
||||||
|
const system = @import("system.zig");
|
||||||
|
const protocol = @import("usb-transfer-protocol");
|
||||||
|
const device_manager = @import("device-manager-protocol");
|
||||||
|
|
||||||
|
pub const Endpoint = protocol.Endpoint;
|
||||||
|
pub const InterruptReport = protocol.InterruptReport;
|
||||||
|
pub const max_report_data = protocol.max_report_data;
|
||||||
|
|
||||||
|
// Endpoint transfer types (EndpointDescriptor attributes), for `findEndpoint`.
|
||||||
|
pub const transfer_type_bulk: u8 = 2;
|
||||||
|
pub const transfer_type_interrupt: u8 = 3;
|
||||||
|
|
||||||
|
/// An opened USB device: the bus endpoint to send requests to, this driver's own
|
||||||
|
/// endpoint that reports arrive on, the device token, and the interface's
|
||||||
|
/// endpoints (so a driver need not re-read the configuration descriptor).
|
||||||
|
pub const Device = struct {
|
||||||
|
bus: ipc.Handle,
|
||||||
|
endpoint: ipc.Handle,
|
||||||
|
token: u64,
|
||||||
|
class: u8,
|
||||||
|
subclass: u8,
|
||||||
|
protocol_code: u8,
|
||||||
|
interface_number: u8,
|
||||||
|
endpoint_count: usize = 0,
|
||||||
|
endpoints: [protocol.max_reported_endpoints]Endpoint = undefined,
|
||||||
|
|
||||||
|
/// The interface's first endpoint of the given transfer type and direction
|
||||||
|
/// (`transfer_type_bulk` / `transfer_type_interrupt`), or null.
|
||||||
|
pub fn findEndpoint(self: *const Device, transfer_type: u8, direction_in: bool) ?Endpoint {
|
||||||
|
for (self.endpoints[0..self.endpoint_count]) |endpoint| {
|
||||||
|
if (endpoint.transfer_type == transfer_type and (endpoint.address & 0x80 != 0) == direction_in) return endpoint;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn controlTransfer(self: *Device, setup: [8]u8, direction_in: bool, data: []u8) ?usize {
|
||||||
|
var request = protocol.ControlRequest{
|
||||||
|
.device_token = self.token,
|
||||||
|
.setup = setup,
|
||||||
|
.direction_in = @intFromBool(direction_in),
|
||||||
|
.data_length = @intCast(data.len),
|
||||||
|
};
|
||||||
|
if (!direction_in and data.len > 0) @memcpy(request.data[0..data.len], data);
|
||||||
|
var reply: [@sizeOf(protocol.ControlReply)]u8 = undefined;
|
||||||
|
const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null;
|
||||||
|
if (length < @sizeOf(protocol.ControlReply)) return null;
|
||||||
|
const control_reply = std.mem.bytesToValue(protocol.ControlReply, reply[0..@sizeOf(protocol.ControlReply)]);
|
||||||
|
if (control_reply.status != 0) return null;
|
||||||
|
const actual = @min(control_reply.actual_length, data.len);
|
||||||
|
if (direction_in and actual > 0) @memcpy(data[0..actual], control_reply.data[0..actual]);
|
||||||
|
return actual;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A control transfer with no data stage (SET_PROTOCOL, SET_IDLE, ...). The
|
||||||
|
/// `setup` is a bit-cast `usb_abi.Request`.
|
||||||
|
pub fn controlOut(self: *Device, setup: [8]u8) bool {
|
||||||
|
return self.controlTransfer(setup, false, &.{}) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A device-to-host control transfer, returning the bytes read into `out`.
|
||||||
|
pub fn controlIn(self: *Device, setup: [8]u8, out: []u8) ?usize {
|
||||||
|
return self.controlTransfer(setup, true, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Begin periodic IN polling of an interrupt endpoint; reports flow back to
|
||||||
|
/// `self.endpoint` as asynchronous `InterruptReport` messages.
|
||||||
|
pub fn subscribeInterrupt(self: *Device, endpoint_address: u8, max_length: u16) bool {
|
||||||
|
var request = protocol.InterruptSubscribeRequest{
|
||||||
|
.device_token = self.token,
|
||||||
|
.endpoint_address = endpoint_address,
|
||||||
|
.max_length = max_length,
|
||||||
|
};
|
||||||
|
var reply: [@sizeOf(protocol.InterruptSubscribeReply)]u8 = undefined;
|
||||||
|
const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return false;
|
||||||
|
if (length < @sizeOf(protocol.InterruptSubscribeReply)) return false;
|
||||||
|
return std.mem.bytesToValue(protocol.InterruptSubscribeReply, reply[0..@sizeOf(protocol.InterruptSubscribeReply)]).status == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One bulk transfer (IN or OUT per `endpoint_address`'s direction bit) to or
|
||||||
|
/// from the caller's own DMA buffer at `physical`. Returns the bytes moved.
|
||||||
|
pub fn bulk(self: *Device, endpoint_address: u8, physical: u64, length: u32) ?u32 {
|
||||||
|
var request = protocol.BulkRequest{
|
||||||
|
.device_token = self.token,
|
||||||
|
.physical_address = physical,
|
||||||
|
.length = length,
|
||||||
|
.endpoint_address = endpoint_address,
|
||||||
|
};
|
||||||
|
var reply: [@sizeOf(protocol.BulkReply)]u8 = undefined;
|
||||||
|
const replied = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null;
|
||||||
|
if (replied < @sizeOf(protocol.BulkReply)) return null;
|
||||||
|
const bulk_reply = std.mem.bytesToValue(protocol.BulkReply, reply[0..@sizeOf(protocol.BulkReply)]);
|
||||||
|
if (bulk_reply.status != 0) return null;
|
||||||
|
return bulk_reply.actual_length;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Look up the USB bus and open the device with the assigned id, handing over a
|
||||||
|
/// freshly created endpoint for asynchronous interrupt reports. Retries while the
|
||||||
|
/// bus is still coming up (a class driver races the bus driver at boot).
|
||||||
|
pub fn open(device_id: u64) ?Device {
|
||||||
|
var attempts: usize = 0;
|
||||||
|
const bus = while (attempts < 100) : (attempts += 1) {
|
||||||
|
if (ipc.lookup(.usb_bus)) |handle| break handle;
|
||||||
|
system.sleep(20);
|
||||||
|
} else return null;
|
||||||
|
|
||||||
|
const endpoint = ipc.createIpcEndpoint() orelse return null;
|
||||||
|
var request = protocol.OpenRequest{ .device_id = device_id };
|
||||||
|
var reply: [@sizeOf(protocol.OpenReply)]u8 = undefined;
|
||||||
|
const result = ipc.callCap(bus, std.mem.asBytes(&request), &reply, endpoint) catch return null;
|
||||||
|
if (result.len < @sizeOf(protocol.OpenReply)) return null;
|
||||||
|
const open_reply = std.mem.bytesToValue(protocol.OpenReply, reply[0..@sizeOf(protocol.OpenReply)]);
|
||||||
|
if (open_reply.status != 0) return null;
|
||||||
|
|
||||||
|
var device = Device{
|
||||||
|
.bus = bus,
|
||||||
|
.endpoint = endpoint,
|
||||||
|
.token = open_reply.device_token,
|
||||||
|
.class = open_reply.interface_class,
|
||||||
|
.subclass = open_reply.interface_subclass,
|
||||||
|
.protocol_code = open_reply.interface_protocol,
|
||||||
|
.interface_number = open_reply.interface_number,
|
||||||
|
.endpoint_count = @min(open_reply.endpoint_count, protocol.max_reported_endpoints),
|
||||||
|
};
|
||||||
|
for (0..device.endpoint_count) |index| device.endpoints[index] = open_reply.endpoints[index];
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hello the device manager as a class driver (Role.device) so a supervised
|
||||||
|
/// spawn meets its hello deadline. Retries while the manager comes up.
|
||||||
|
pub fn helloManager(device_id: u64) bool {
|
||||||
|
var attempts: usize = 0;
|
||||||
|
const manager = while (attempts < 100) : (attempts += 1) {
|
||||||
|
if (ipc.lookup(.device_manager)) |handle| break handle;
|
||||||
|
system.sleep(20);
|
||||||
|
} else return false;
|
||||||
|
|
||||||
|
const hello = device_manager.Hello{ .role = @intFromEnum(device_manager.Role.device), .device_id = device_id };
|
||||||
|
var reply: [device_manager.message_maximum]u8 = undefined;
|
||||||
|
const length = ipc.call(manager, std.mem.asBytes(&hello), &reply) catch return false;
|
||||||
|
if (length < device_manager.reply_size) return false;
|
||||||
|
return std.mem.bytesToValue(device_manager.HelloReply, reply[0..device_manager.reply_size]).status == 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# xkeyboard-config — X11 keyboard layouts, compiled to Zig
|
||||||
|
|
||||||
|
This module turns a physical key (a **USB HID usage**, as the [input module](../../docs/input.md)
|
||||||
|
delivers in `KeyEvent.keycode`) plus a modifier state into a **keysym** and, when the key
|
||||||
|
produces one, a **character** (a Unicode scalar). It is what lets a `keycode` become a
|
||||||
|
`character` — a keymap — without danos shipping an X11 runtime.
|
||||||
|
|
||||||
|
The layout data comes from the X11 [xkeyboard-config](https://gitlab.freedesktop.org/xkeyboard-config/xkeyboard-config)
|
||||||
|
database, but it is **compiled to native Zig at build time** rather than parsed at runtime.
|
||||||
|
`tools/make-xkeyboard-config.py` reads the vendored xkb data and emits pure-data tables into
|
||||||
|
`generated/layouts.zig`; `xkeyboard-config.zig` is the hand-written API over them. This is
|
||||||
|
the same build-time-codegen pattern as `tools/make-initial-ramdisk.py`.
|
||||||
|
|
||||||
|
## Using it
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const xkb = @import("xkeyboard-config");
|
||||||
|
|
||||||
|
const m = xkb.map(xkb.us, key_event.keycode, .{ .shift = shift_held, .caps_lock = caps });
|
||||||
|
if (m.character) |ch| { /* a printable Unicode scalar */ }
|
||||||
|
// m.keysym is always set (e.g. an X11 keysym for Return / F1 / a dead key).
|
||||||
|
|
||||||
|
const layout = xkb.byName("gb") orelse xkb.us; // choose a layout by name
|
||||||
|
for (xkb.all) |l| { /* enumerate available layouts */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
`Modifiers` carries `shift`, `caps_lock`, `level3` (AltGr), and `control`. `map` selects the
|
||||||
|
level from the key's XKB *type* (the generated data) and those modifiers (the policy, in
|
||||||
|
`xkeyboard-config.zig`), so data and semantics stay separable.
|
||||||
|
|
||||||
|
Layouts: **us, gb, de, fr, es, dvorak**.
|
||||||
|
|
||||||
|
## Regenerating
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 tools/make-xkeyboard-config.py fetch # network: download + vendor the data subset
|
||||||
|
python3 tools/make-xkeyboard-config.py generate # offline: emit generated/layouts.zig
|
||||||
|
# or, from the build:
|
||||||
|
zig build gen-xkeyboard-config
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`fetch`** downloads the pinned xkeyboard-config release (version + sha256 in the script),
|
||||||
|
resolves the `include` graph for the configured layouts, and vendors *only* the symbols
|
||||||
|
files actually reached (plus `keysymdef.h`, `COPYING`, and `PROVENANCE.md`) into `vendor/`.
|
||||||
|
Run it when bumping the version or adding a layout.
|
||||||
|
- **`generate`** is deterministic and offline — same vendored input produces byte-identical
|
||||||
|
output. To add a layout, extend `TARGETS` (and `HID_TO_NAME` if a new physical key is
|
||||||
|
involved), then re-run `fetch` (to vendor any new includes) and `generate`.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
A pragmatic subset, enough for real Latin-script typing:
|
||||||
|
|
||||||
|
- **Group 1 only** — no multi-layout group switching.
|
||||||
|
- **No dead-key / compose composition** — a dead key returns its keysym with no `character`
|
||||||
|
(composing `´` + `e` → `é` is a higher layer's job).
|
||||||
|
- **Curated key types** — the common XKB types (one/two-level, alphabetic, four-level, …);
|
||||||
|
unmapped keys and unknown types fall back to level-by-shift.
|
||||||
|
- **6 layouts** — extend via `TARGETS` as above.
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
xkeyboard-config and `keysymdef.h` (xorgproto) are MIT/X11 licensed. The vendored data
|
||||||
|
subset carries the upstream `vendor/COPYING`, and `vendor/PROVENANCE.md` records the exact
|
||||||
|
version, source URL, and sha256. The generated tables are a derived work under the same terms.
|
||||||
File diff suppressed because it is too large
Load Diff
+190
@@ -0,0 +1,190 @@
|
|||||||
|
Copyright 1996 by Joseph Moss
|
||||||
|
Copyright (C) 2002-2007 Free Software Foundation, Inc.
|
||||||
|
Copyright (C) Dmitry Golubev <lastguru@mail.ru>, 2003-2004
|
||||||
|
Copyright (C) 2004, Gregory Mokhin <mokhin@bog.msu.ru>
|
||||||
|
Copyright (C) 2006 Erdal Ronahî
|
||||||
|
|
||||||
|
Permission to use, copy, modify, distribute, and sell this software and its
|
||||||
|
documentation for any purpose is hereby granted without fee, provided that
|
||||||
|
the above copyright notice appear in all copies and that both that
|
||||||
|
copyright notice and this permission notice appear in supporting
|
||||||
|
documentation, and that the name of the copyright holder(s) not be used in
|
||||||
|
advertising or publicity pertaining to distribution of the software without
|
||||||
|
specific, written prior permission. The copyright holder(s) makes no
|
||||||
|
representations about the suitability of this software for any purpose. It
|
||||||
|
is provided "as is" without express or implied warranty.
|
||||||
|
|
||||||
|
THE COPYRIGHT HOLDER(S) DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
|
||||||
|
INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
|
||||||
|
EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR
|
||||||
|
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
|
||||||
|
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
|
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
Copyright (c) 1996 Digital Equipment Corporation
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL DIGITAL EQUIPMENT CORPORATION BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
|
||||||
|
THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
Except as contained in this notice, the name of the Digital Equipment
|
||||||
|
Corporation shall not be used in advertising or otherwise to promote
|
||||||
|
the sale, use or other dealings in this Software without prior written
|
||||||
|
authorization from Digital Equipment Corporation.
|
||||||
|
|
||||||
|
|
||||||
|
Copyright 1996, 1998 The Open Group
|
||||||
|
|
||||||
|
Permission to use, copy, modify, distribute, and sell this software and its
|
||||||
|
documentation for any purpose is hereby granted without fee, provided that
|
||||||
|
the above copyright notice appear in all copies and that both that
|
||||||
|
copyright notice and this permission notice appear in supporting
|
||||||
|
documentation.
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
Except as contained in this notice, the name of The Open Group shall
|
||||||
|
not be used in advertising or otherwise to promote the sale, use or
|
||||||
|
other dealings in this Software without prior written authorization
|
||||||
|
from The Open Group.
|
||||||
|
|
||||||
|
|
||||||
|
Copyright 2004-2005 Sun Microsystems, Inc. All rights reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice (including the next
|
||||||
|
paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
Copyright (c) 1996 by Silicon Graphics Computer Systems, Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and distribute this
|
||||||
|
software and its documentation for any purpose and without
|
||||||
|
fee is hereby granted, provided that the above copyright
|
||||||
|
notice appear in all copies and that both that copyright
|
||||||
|
notice and this permission notice appear in supporting
|
||||||
|
documentation, and that the name of Silicon Graphics not be
|
||||||
|
used in advertising or publicity pertaining to distribution
|
||||||
|
of the software without specific prior written permission.
|
||||||
|
Silicon Graphics makes no representation about the suitability
|
||||||
|
of this software for any purpose. It is provided "as is"
|
||||||
|
without any express or implied warranty.
|
||||||
|
|
||||||
|
SILICON GRAPHICS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
|
||||||
|
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON
|
||||||
|
GRAPHICS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
|
||||||
|
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
|
||||||
|
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||||
|
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH
|
||||||
|
THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
Copyright (c) 1996 X Consortium
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
Except as contained in this notice, the name of the X Consortium shall
|
||||||
|
not be used in advertising or otherwise to promote the sale, use or
|
||||||
|
other dealings in this Software without prior written authorization
|
||||||
|
from the X Consortium.
|
||||||
|
|
||||||
|
|
||||||
|
Copyright (C) 2004, 2006 Ævar Arnfjörð Bjarmason <avarab@gmail.com>
|
||||||
|
|
||||||
|
Permission to use, copy, modify, distribute, and sell this software and its
|
||||||
|
documentation for any purpose is hereby granted without fee, provided that
|
||||||
|
the above copyright notice appear in all copies and that both that
|
||||||
|
copyright notice and this permission notice appear in supporting
|
||||||
|
documentation.
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
Except as contained in this notice, the name of a copyright holder shall
|
||||||
|
not be used in advertising or otherwise to promote the sale, use or
|
||||||
|
other dealings in this Software without prior written authorization of
|
||||||
|
the copyright holder.
|
||||||
|
|
||||||
|
|
||||||
|
Copyright (C) 1999, 2000 by Anton Zinoviev <anton@lml.bas.bg>
|
||||||
|
|
||||||
|
This software may be used, modified, copied, distributed, and sold,
|
||||||
|
in both source and binary form provided that the above copyright
|
||||||
|
and these terms are retained. Under no circumstances is the author
|
||||||
|
responsible for the proper functioning of this software, nor does
|
||||||
|
the author assume any responsibility for damages incurred with its
|
||||||
|
use.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use, distribute and modify
|
||||||
|
this file in any way, provided that the above copyright notice
|
||||||
|
is left intact and the author of the modification summarizes
|
||||||
|
the changes in this header.
|
||||||
|
|
||||||
|
This file is distributed without any expressed or implied warranty.
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Vendored xkeyboard-config subset
|
||||||
|
|
||||||
|
- **Package**: xkeyboard-config 2.44
|
||||||
|
- **Source**: https://gitlab.freedesktop.org/xkeyboard-config/xkeyboard-config/-/archive/xkeyboard-config-2.44/xkeyboard-config-2.44.tar.gz
|
||||||
|
- **sha256**: `35e34edeaf4e8da8d0696ff6b241ee11ddb1b8c6730bac7252d4d0a88ea5f05b`
|
||||||
|
- **keysymdef.h**: xorgproto, copied from `/opt/homebrew/include/X11/keysymdef.h`
|
||||||
|
- **License**: MIT/X11 (see COPYING)
|
||||||
|
|
||||||
|
Only the symbols files reachable from the generated layouts (tools/make-xkeyboard-config.py `TARGETS`) are vendored; regenerate with
|
||||||
|
`python3 tools/make-xkeyboard-config.py fetch` then `... generate`.
|
||||||
|
|
||||||
|
Vendored symbols files:
|
||||||
|
|
||||||
|
- `symbols/de`
|
||||||
|
- `symbols/es`
|
||||||
|
- `symbols/fr`
|
||||||
|
- `symbols/gb`
|
||||||
|
- `symbols/kpdl`
|
||||||
|
- `symbols/latin`
|
||||||
|
- `symbols/level3`
|
||||||
|
- `symbols/us`
|
||||||
+2584
File diff suppressed because it is too large
Load Diff
+1232
File diff suppressed because it is too large
Load Diff
+250
@@ -0,0 +1,250 @@
|
|||||||
|
// Keyboard layouts for Spain.
|
||||||
|
|
||||||
|
// Modified for a real Spanish keyboard by Jon Tombs.
|
||||||
|
default partial alphanumeric_keys
|
||||||
|
xkb_symbols "basic" {
|
||||||
|
|
||||||
|
include "latin(type4)"
|
||||||
|
|
||||||
|
name[Group1]="Spanish";
|
||||||
|
|
||||||
|
key <TLDE> { [ masculine, ordfeminine, backslash, backslash ] };
|
||||||
|
key <AE01> { [ 1, exclam, bar, exclamdown ] };
|
||||||
|
key <AE03> { [ 3, periodcentered, numbersign, sterling ] };
|
||||||
|
key <AE04> { [ 4, dollar, asciitilde, dollar ] };
|
||||||
|
key <AE11> { [apostrophe, question, backslash, questiondown ] };
|
||||||
|
key <AE12> { [exclamdown, questiondown, dead_cedilla, dead_ogonek] };
|
||||||
|
|
||||||
|
key <AD11> { [dead_grave, dead_circumflex, bracketleft, dead_abovering ] };
|
||||||
|
key <AD12> { [ plus, asterisk, bracketright, dead_macron ] };
|
||||||
|
|
||||||
|
key <AC10> { [ ntilde, Ntilde, dead_tilde, dead_doubleacute ] };
|
||||||
|
key <AC11> { [dead_acute, dead_diaeresis, braceleft, dead_caron ] };
|
||||||
|
key <BKSL> { [ ccedilla, Ccedilla, braceright, dead_breve ] };
|
||||||
|
|
||||||
|
include "level3(ralt_switch)"
|
||||||
|
};
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "winkeys" {
|
||||||
|
|
||||||
|
include "es(basic)"
|
||||||
|
name[Group1]="Spanish (Windows)";
|
||||||
|
include "eurosign(5)"
|
||||||
|
};
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "nodeadkeys" {
|
||||||
|
|
||||||
|
include "es(basic)"
|
||||||
|
|
||||||
|
name[Group1]="Spanish (no dead keys)";
|
||||||
|
|
||||||
|
key <AE12> { [exclamdown, questiondown, cedilla, ogonek ] };
|
||||||
|
key <AD11> { [ grave, asciicircum, bracketleft, degree ] };
|
||||||
|
key <AD12> { [ plus, asterisk, bracketright, macron ] };
|
||||||
|
key <AC07> { [ j, J, ezh, EZH ] };
|
||||||
|
key <AC10> { [ ntilde, Ntilde, asciitilde, doubleacute ] };
|
||||||
|
key <AC11> { [ acute, diaeresis, braceleft, caron ] };
|
||||||
|
key <BKSL> { [ ccedilla, Ccedilla, braceright, breve ] };
|
||||||
|
key <AB10> { [ minus, underscore, ellipsis, abovedot ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spanish Dvorak mapping (note R-H exchange)
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "dvorak" {
|
||||||
|
|
||||||
|
name[Group1]="Spanish (Dvorak)";
|
||||||
|
|
||||||
|
key <TLDE> {[ masculine, ordfeminine, backslash, degree ]};
|
||||||
|
key <AE01> {[ 1, exclam, bar, onesuperior ]};
|
||||||
|
key <AE02> {[ 2, quotedbl, at, twosuperior ]};
|
||||||
|
key <AE03> {[ 3, periodcentered, numbersign, threesuperior ]};
|
||||||
|
key <AE04> {[ 4, dollar, asciitilde, onequarter ]};
|
||||||
|
key <AE05> {[ 5, percent, brokenbar, fiveeighths ]};
|
||||||
|
key <AE06> {[ 6, ampersand, notsign, threequarters ]};
|
||||||
|
key <AE07> {[ 7, slash, onehalf, seveneighths ]};
|
||||||
|
key <AE08> {[ 8, parenleft, oneeighth, threeeighths ]};
|
||||||
|
key <AE09> {[ 9, parenright, asciicircum ]};
|
||||||
|
key <AE10> {[ 0, equal, grave, dead_doubleacute ]};
|
||||||
|
key <AE11> {[ apostrophe, question, dead_macron, dead_ogonek ]};
|
||||||
|
key <AE12> {[ exclamdown, questiondown, dead_breve, dead_abovedot ]};
|
||||||
|
|
||||||
|
key <AD01> {[ period, colon, less, guillemotleft ]};
|
||||||
|
key <AD02> {[ comma, semicolon, greater, guillemotright ]};
|
||||||
|
key <AD03> {[ ntilde, Ntilde, lstroke, Lstroke ]};
|
||||||
|
key <AD04> {[ p, P, paragraph ]};
|
||||||
|
key <AD05> {[ y, Y, yen ]};
|
||||||
|
key <AD06> {[ f, F, tslash, Tslash ]};
|
||||||
|
key <AD07> {[ g, G, dstroke, Dstroke ]};
|
||||||
|
key <AD08> {[ c, C, cent, copyright ]};
|
||||||
|
key <AD09> {[ h, H, hstroke, Hstroke ]};
|
||||||
|
key <AD10> {[ l, L, sterling ]};
|
||||||
|
key <AD11> {[ dead_grave, dead_circumflex, bracketleft, dead_caron ]};
|
||||||
|
key <AD12> {[ plus, asterisk, bracketright, plusminus ]};
|
||||||
|
|
||||||
|
key <AC01> {[ a, A, ae, AE ]};
|
||||||
|
key <AC02> {[ o, O, oslash, Oslash ]};
|
||||||
|
key <AC03> {[ e, E, EuroSign ]};
|
||||||
|
key <AC04> {[ u, U, aring, Aring ]};
|
||||||
|
key <AC05> {[ i, I, oe, OE ]};
|
||||||
|
key <AC06> {[ d, D, eth, ETH ]};
|
||||||
|
key <AC07> {[ r, R, registered, trademark ]};
|
||||||
|
key <AC08> {[ t, T, thorn, THORN ]};
|
||||||
|
key <AC09> {[ n, N, eng, ENG ]};
|
||||||
|
key <AC10> {[ s, S, ssharp, section ]};
|
||||||
|
key <AC11> {[ dead_acute, dead_diaeresis, braceleft, dead_tilde ]};
|
||||||
|
key <BKSL> {[ ccedilla, Ccedilla, braceright, dead_cedilla ]};
|
||||||
|
|
||||||
|
key <LSGT> {[ less, greater, guillemotleft, guillemotright ]};
|
||||||
|
key <AB01> {[ minus, underscore, hyphen, macron ]};
|
||||||
|
key <AB02> {[ q, Q, currency ]};
|
||||||
|
key <AB03> {[ j, J ]};
|
||||||
|
key <AB04> {[ k, K, kra ]};
|
||||||
|
key <AB05> {[ x, X, multiply, division ]};
|
||||||
|
key <AB06> {[ b, B ]};
|
||||||
|
key <AB07> {[ m, M, mu ]};
|
||||||
|
key <AB08> {[ w, W ]};
|
||||||
|
key <AB09> {[ v, V ]};
|
||||||
|
key <AB10> {[ z, Z ]};
|
||||||
|
|
||||||
|
include "level3(ralt_switch)"
|
||||||
|
};
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "cat" {
|
||||||
|
|
||||||
|
include "es(basic)"
|
||||||
|
|
||||||
|
name[Group1]="Catalan (Spain, with middle-dot L)";
|
||||||
|
|
||||||
|
key <AC09> { [ l, L, 0x1000140, 0x100013F ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "ast" {
|
||||||
|
|
||||||
|
include "es(basic)"
|
||||||
|
|
||||||
|
name[Group1]="Asturian (Spain, with bottom-dot H and L)";
|
||||||
|
|
||||||
|
key <AC06> { [ h, H, 0x1001E25, 0x1001E24 ] };
|
||||||
|
key <AC09> { [ l, L, 0x1001E37, 0x1001E36 ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "olpc" {
|
||||||
|
|
||||||
|
// #HW-SPECIFIC
|
||||||
|
|
||||||
|
// http://wiki.laptop.org/go/OLPC_Spanish_Keyboard
|
||||||
|
|
||||||
|
include "us(basic)"
|
||||||
|
name[Group1]="Spanish";
|
||||||
|
|
||||||
|
key <AE00> { [ masculine, ordfeminine ] };
|
||||||
|
key <AE01> { [ 1, exclam, bar ] };
|
||||||
|
key <AE02> { [ 2, quotedbl, at ] };
|
||||||
|
key <AE03> { [ 3, dead_grave, numbersign, grave ] };
|
||||||
|
key <AE05> { [ 5, percent, asciicircum, dead_circumflex ] };
|
||||||
|
key <AE06> { [ 6, ampersand, notsign ] };
|
||||||
|
key <AE07> { [ 7, slash, backslash ] };
|
||||||
|
key <AE08> { [ 8, parenleft ] };
|
||||||
|
key <AE09> { [ 9, parenright ] };
|
||||||
|
key <AE10> { [ 0, equal ] };
|
||||||
|
key <AE11> { [ apostrophe, question ] };
|
||||||
|
key <AE12> { [ exclamdown, questiondown ] };
|
||||||
|
|
||||||
|
key <AD03> { [ e, E, EuroSign ] };
|
||||||
|
key <AD11> { [ dead_acute, dead_diaeresis, acute, dead_abovering ] };
|
||||||
|
key <AD12> { [ bracketleft, braceleft ] };
|
||||||
|
|
||||||
|
key <AC10> { [ ntilde, Ntilde ] };
|
||||||
|
key <AC11> { [ plus, asterisk, dead_tilde ] };
|
||||||
|
key <AC12> { [ bracketright, braceright, section ] };
|
||||||
|
|
||||||
|
key <AB08> { [ comma, semicolon ] };
|
||||||
|
key <AB09> { [ period, colon ] };
|
||||||
|
key <AB10> { [ minus, underscore ] };
|
||||||
|
|
||||||
|
key <I219> { [ less, greater, ISO_Next_Group ] };
|
||||||
|
|
||||||
|
include "level3(ralt_switch)"
|
||||||
|
};
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "olpcm" {
|
||||||
|
|
||||||
|
// #HW-SPECIFIC
|
||||||
|
|
||||||
|
// Mechanical (non-membrane) OLPC Spanish keyboard layout.
|
||||||
|
// See: http://wiki.laptop.org/go/OLPC_Spanish_Non-membrane_Keyboard
|
||||||
|
|
||||||
|
include "us(basic)"
|
||||||
|
name[Group1]="Spanish";
|
||||||
|
|
||||||
|
key <AE00> { [ questiondown, exclamdown, backslash ] };
|
||||||
|
key <AE01> { [ 1, exclam, bar ] };
|
||||||
|
key <AE02> { [ 2, quotedbl, at ] };
|
||||||
|
key <AE03> { [ 3, dead_grave, numbersign, grave ] };
|
||||||
|
key <AE04> { [ 4, dollar, asciitilde, dead_tilde ] };
|
||||||
|
key <AE05> { [ 5, percent, asciicircum, dead_circumflex ] };
|
||||||
|
key <AE06> { [ 6, ampersand, notsign ] };
|
||||||
|
key <AE07> { [ 7, slash, backslash ] }; // no '\' label on olpcm, leave for compatibility
|
||||||
|
key <AE08> { [ 8, parenleft, masculine ] };
|
||||||
|
key <AE09> { [ 9, parenright, ordfeminine ] };
|
||||||
|
key <AE10> { [ 0, equal ] };
|
||||||
|
key <AE11> { [ apostrophe, question ] };
|
||||||
|
|
||||||
|
key <AD03> { [ e, E, EuroSign ] };
|
||||||
|
key <AD11> { [ dead_acute, dead_diaeresis, dead_abovering, acute ] };
|
||||||
|
key <AD12> { [ plus, asterisk ] };
|
||||||
|
|
||||||
|
key <AC10> { [ ntilde, Ntilde ] };
|
||||||
|
// no AC11 or AC12 on olpcm
|
||||||
|
|
||||||
|
key <AB08> { [ comma, semicolon ] };
|
||||||
|
key <AB09> { [ period, colon ] };
|
||||||
|
key <AB10> { [ minus, underscore ] };
|
||||||
|
|
||||||
|
key <AA02> { [ less, greater ] };
|
||||||
|
key <AA06> { [ bracketleft, braceleft, ccedilla, Ccedilla ] };
|
||||||
|
key <AA07> { [ bracketright, braceright ] };
|
||||||
|
|
||||||
|
include "level3(ralt_switch)"
|
||||||
|
};
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "deadtilde" {
|
||||||
|
|
||||||
|
include "es(basic)"
|
||||||
|
|
||||||
|
name[Group1]="Spanish (dead tilde)";
|
||||||
|
|
||||||
|
key <AE04> { [ 4, dollar, dead_tilde, dollar ] };
|
||||||
|
key <AC10> { [ ntilde, Ntilde, asciitilde, dead_doubleacute ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "olpc2" {
|
||||||
|
// #HW-SPECIFIC
|
||||||
|
|
||||||
|
// Modified variant of US International layout, specifically for Peru
|
||||||
|
// Contact: Sayamindu Dasgupta <sayamindu@laptop.org>
|
||||||
|
|
||||||
|
include "us(olpc)"
|
||||||
|
name[Group1]="Spanish";
|
||||||
|
|
||||||
|
key <AE03> { [ 3, numbersign, dead_grave, dead_grave] }; // combining grave
|
||||||
|
key <I236> { [ XF86Start ] };
|
||||||
|
|
||||||
|
include "level3(ralt_switch)"
|
||||||
|
};
|
||||||
|
|
||||||
|
// EXTRAS:
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "sun_type6" {
|
||||||
|
include "sun_vndr/es(sun_type6)"
|
||||||
|
};
|
||||||
+1404
File diff suppressed because it is too large
Load Diff
+249
@@ -0,0 +1,249 @@
|
|||||||
|
// Keyboard layouts for Great Britain.
|
||||||
|
|
||||||
|
default partial alphanumeric_keys
|
||||||
|
xkb_symbols "basic" {
|
||||||
|
|
||||||
|
// The basic UK layout, also known as the IBM 166 layout,
|
||||||
|
// but with the useless brokenbar pushed two levels up.
|
||||||
|
|
||||||
|
include "latin"
|
||||||
|
|
||||||
|
name[Group1]="English (UK)";
|
||||||
|
|
||||||
|
key <TLDE> { [ grave, notsign, bar, bar ] };
|
||||||
|
key <AE02> { [ 2, quotedbl, twosuperior, oneeighth ] };
|
||||||
|
key <AE03> { [ 3, sterling, threesuperior, sterling ] };
|
||||||
|
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||||
|
|
||||||
|
key <AC11> { [apostrophe, at, dead_circumflex, dead_caron] };
|
||||||
|
key <BKSL> { [numbersign, asciitilde, dead_grave, dead_breve ] };
|
||||||
|
|
||||||
|
key <LSGT> { [ backslash, bar, bar, brokenbar ] };
|
||||||
|
|
||||||
|
include "level3(ralt_switch)"
|
||||||
|
};
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "intl" {
|
||||||
|
|
||||||
|
// A UK layout but with five accents made into dead keys:
|
||||||
|
// grave, diaeresis, circumflex, acute, and tilde.
|
||||||
|
// By Phil Jones <philjones1 at blueyonder.co.uk>.
|
||||||
|
|
||||||
|
include "latin"
|
||||||
|
|
||||||
|
name[Group1]="English (UK, intl., with dead keys)";
|
||||||
|
|
||||||
|
key <TLDE> { [ dead_grave, notsign, bar, bar ] };
|
||||||
|
key <AE02> { [ 2, dead_diaeresis, twosuperior, onehalf ] };
|
||||||
|
key <AE03> { [ 3, sterling, threesuperior, onethird ] };
|
||||||
|
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||||
|
key <AE06> { [ 6, dead_circumflex, threequarters, onesixth ] };
|
||||||
|
|
||||||
|
key <AC11> { [ dead_acute, at, apostrophe, bar ] };
|
||||||
|
key <BKSL> { [ numbersign, dead_tilde, bar, bar ] };
|
||||||
|
|
||||||
|
key <LSGT> { [ backslash, bar, bar, bar ] };
|
||||||
|
key <AB08> { [ comma, less, ccedilla, Ccedilla ] };
|
||||||
|
|
||||||
|
include "level3(ralt_switch)"
|
||||||
|
};
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "extd" {
|
||||||
|
// Clone of the Microsoft "United Kingdom Extended" layout, which
|
||||||
|
// includes dead keys for: grave; diaeresis; circumflex; tilde; and
|
||||||
|
// accute. It also enables direct access to accute characters using
|
||||||
|
// the Multi_key (Alt Gr).
|
||||||
|
//
|
||||||
|
// Taken from...
|
||||||
|
// "Windows Keyboard Layouts"
|
||||||
|
// https://docs.microsoft.com/en-gb/globalization/windows-keyboard-layouts#U
|
||||||
|
//
|
||||||
|
// -- Jonathan Miles <jon@cybah.co.uk>
|
||||||
|
|
||||||
|
include "latin"
|
||||||
|
|
||||||
|
name[Group1]="English (UK, extended, Windows)";
|
||||||
|
|
||||||
|
key <TLDE> { [ dead_grave, notsign, brokenbar, NoSymbol ] };
|
||||||
|
key <AE02> { [ 2, quotedbl, dead_diaeresis, onehalf ] };
|
||||||
|
key <AE03> { [ 3, sterling, threesuperior, onethird ] };
|
||||||
|
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||||
|
key <AE06> { [ 6, asciicircum, dead_circumflex, NoSymbol ] };
|
||||||
|
|
||||||
|
key <AD02> { [ w, W, wacute, Wacute ] };
|
||||||
|
key <AD03> { [ e, E, eacute, Eacute ] };
|
||||||
|
key <AD06> { [ y, Y, yacute, Yacute ] };
|
||||||
|
key <AD07> { [ u, U, uacute, Uacute ] };
|
||||||
|
key <AD08> { [ i, I, iacute, Iacute ] };
|
||||||
|
key <AD09> { [ o, O, oacute, Oacute ] };
|
||||||
|
key <AD12> { [ bracketright, braceright, NoSymbol, bar ] };
|
||||||
|
|
||||||
|
key <AC01> { [ a, A, aacute, Aacute ] };
|
||||||
|
key <AC11> { [ apostrophe, at, dead_acute, grave ] };
|
||||||
|
key <BKSL> { [ numbersign, asciitilde, dead_tilde, backslash ] };
|
||||||
|
|
||||||
|
key <LSGT> { [ backslash, bar, NoSymbol, NoSymbol ] };
|
||||||
|
key <AB03> { [ c, C, ccedilla, Ccedilla ] };
|
||||||
|
|
||||||
|
include "level3(ralt_switch)"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Describe the differences between the US Colemak layout
|
||||||
|
// and a UK variant. By Andy Buckley (andy@insectnation.org)
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "colemak" {
|
||||||
|
include "us(colemak)"
|
||||||
|
|
||||||
|
name[Group1]="English (UK, Colemak)";
|
||||||
|
|
||||||
|
key <TLDE> { [ grave, notsign, bar, asciitilde ] };
|
||||||
|
key <AE02> { [ 2, quotedbl, twosuperior, oneeighth ] };
|
||||||
|
key <AE03> { [ 3, sterling, threesuperior, sterling ] };
|
||||||
|
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||||
|
|
||||||
|
key <AC11> { [apostrophe, at, dead_circumflex, dead_caron] };
|
||||||
|
key <BKSL> { [numbersign, asciitilde, dead_grave, dead_breve ] };
|
||||||
|
|
||||||
|
key <LSGT> { [ backslash, bar, asciitilde, brokenbar ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Colemak-DH (ISO) layout, UK Variant, https://colemakmods.github.io/mod-dh/
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "colemak_dh" {
|
||||||
|
include "us(colemak_dh)"
|
||||||
|
|
||||||
|
name[Group1]="English (UK, Colemak-DH)";
|
||||||
|
|
||||||
|
key <TLDE> { [ grave, notsign, bar, asciitilde ] };
|
||||||
|
key <AE02> { [ 2, quotedbl, twosuperior, oneeighth ] };
|
||||||
|
key <AE03> { [ 3, sterling, threesuperior, sterling ] };
|
||||||
|
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||||
|
|
||||||
|
key <AC11> { [apostrophe, at, dead_circumflex, dead_caron] };
|
||||||
|
key <BKSL> { [numbersign, asciitilde, dead_grave, dead_breve ] };
|
||||||
|
|
||||||
|
key <AB05> { [ backslash, bar, asciitilde, brokenbar ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Dvorak (UK) keymap (by odaen) allowing the usage of
|
||||||
|
// the £ and ? key and swapping the @ and " keys.
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "dvorak" {
|
||||||
|
include "us(dvorak-alt-intl)"
|
||||||
|
|
||||||
|
name[Group1]="English (UK, Dvorak)";
|
||||||
|
|
||||||
|
key <TLDE> { [ grave, notsign, bar, bar ] };
|
||||||
|
key <AE02> { [ 2, quotedbl, twosuperior, NoSymbol ] };
|
||||||
|
key <AE03> { [ 3, sterling, threesuperior, NoSymbol ] };
|
||||||
|
key <AD01> { [ apostrophe, at ] };
|
||||||
|
key <BKSL> { [ numbersign, asciitilde ] };
|
||||||
|
key <LSGT> { [ backslash, bar ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dvorak letter positions, but punctuation all in the normal UK positions.
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "dvorakukp" {
|
||||||
|
include "gb(dvorak)"
|
||||||
|
|
||||||
|
name[Group1]="English (UK, Dvorak, with UK punctuation)";
|
||||||
|
|
||||||
|
key <AE11> { [ minus, underscore ] };
|
||||||
|
key <AE12> { [ equal, plus ] };
|
||||||
|
key <AD11> { [ bracketleft, braceleft ] };
|
||||||
|
key <AD12> { [ bracketright, braceright ] };
|
||||||
|
key <AD01> { [ slash, question ] };
|
||||||
|
key <AC11> { [apostrophe, at, dead_circumflex, dead_caron] };
|
||||||
|
};
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "mac" {
|
||||||
|
|
||||||
|
include "latin"
|
||||||
|
|
||||||
|
name[Group1]= "English (UK, Macintosh)";
|
||||||
|
|
||||||
|
key <TLDE> { [ section, plusminus ] };
|
||||||
|
key <AE02> { [ 2, at, EuroSign ] };
|
||||||
|
key <AE03> { [ 3, sterling, numbersign ] };
|
||||||
|
key <LSGT> { [ grave, asciitilde ] };
|
||||||
|
|
||||||
|
include "level3(ralt_switch)"
|
||||||
|
include "level3(enter_switch)"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "mac_intl" {
|
||||||
|
|
||||||
|
include "latin"
|
||||||
|
|
||||||
|
name[Group1]="English (UK, Macintosh, intl.)";
|
||||||
|
|
||||||
|
key <TLDE> { [ section, plusminus, notsign, notsign ] }; //dead_grave
|
||||||
|
key <AE02> { [ 2, at, EuroSign, onehalf ] };
|
||||||
|
key <AE03> { [ 3, sterling, twosuperior, onethird ] };
|
||||||
|
key <AE04> { [ 4, dollar, threesuperior, onequarter ] };
|
||||||
|
key <AE06> { [ 6, dead_circumflex, NoSymbol, onesixth ] };
|
||||||
|
key <AD09> { [ o, O, oe, OE ] };
|
||||||
|
|
||||||
|
key <AC11> { [ dead_acute, dead_diaeresis, dead_diaeresis, bar ] }; //dead_doubleacute
|
||||||
|
key <BKSL> { [ backslash, bar, numbersign, bar ] };
|
||||||
|
|
||||||
|
key <LSGT> { [ dead_grave, dead_tilde, brokenbar, bar ] };
|
||||||
|
|
||||||
|
include "level3(ralt_switch)"
|
||||||
|
};
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "pl" {
|
||||||
|
|
||||||
|
// Polish accented letters on upper levels of corresponding base letters.
|
||||||
|
// Idea from Wawrzyniec Niewodniczański, adapted by Aleksander Kowalski.
|
||||||
|
|
||||||
|
include "gb(basic)"
|
||||||
|
|
||||||
|
name[Group1]="Polish (British keyboard)";
|
||||||
|
|
||||||
|
key <AD03> { [ e, E, eogonek, Eogonek ] };
|
||||||
|
key <AD09> { [ o, O, oacute, Oacute ] };
|
||||||
|
|
||||||
|
key <AC01> { [ a, A, aogonek, Aogonek ] };
|
||||||
|
key <AC02> { [ s, S, sacute, Sacute ] };
|
||||||
|
|
||||||
|
key <AB01> { [ z, Z, zabovedot, Zabovedot ] };
|
||||||
|
key <AB02> { [ x, X, zacute, Zacute ] };
|
||||||
|
key <AB03> { [ c, C, cacute, Cacute ] };
|
||||||
|
key <AB06> { [ n, N, nacute, Nacute ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "gla" {
|
||||||
|
|
||||||
|
// Grave-accented letters on the upper levels of the relevant vowels.
|
||||||
|
|
||||||
|
include "gb(basic)"
|
||||||
|
|
||||||
|
name[Group1]="Scottish Gaelic";
|
||||||
|
|
||||||
|
key <AD03> { [ e, E, egrave, Egrave ] };
|
||||||
|
key <AD07> { [ u, U, ugrave, Ugrave ] };
|
||||||
|
key <AD08> { [ i, I, igrave, Igrave ] };
|
||||||
|
key <AD09> { [ o, O, ograve, Ograve ] };
|
||||||
|
|
||||||
|
key <AC01> { [ a, A, agrave, Agrave ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
// EXTRAS:
|
||||||
|
|
||||||
|
partial alphanumeric_keys
|
||||||
|
xkb_symbols "sun_type6" {
|
||||||
|
include "sun_vndr/gb(sun_type6)"
|
||||||
|
};
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
// The <KPDL> key is a mess.
|
||||||
|
// It was probably originally meant to be a decimal separator.
|
||||||
|
// Except since it was declared by USA people it didn't use the original
|
||||||
|
// SI separator "," but a "." (since then the USA managed to f-up the SI
|
||||||
|
// by making "." an accepted alternative, but standards still use "," as
|
||||||
|
// default)
|
||||||
|
// As a result users of SI-abiding countries expect either a "." or a ","
|
||||||
|
// or a "decimal_separator" which may or may not be translated in one of the
|
||||||
|
// above depending on applications.
|
||||||
|
// It's not possible to define a default per-country since user expectations
|
||||||
|
// depend on the conflicting choices of their most-used applications,
|
||||||
|
// operating system, etc. Therefore it needs to be a configuration setting
|
||||||
|
// Copyright © 2007 Nicolas Mailhot <nicolas.mailhot @ laposte.net>
|
||||||
|
|
||||||
|
|
||||||
|
// Legacy <KPDL> #1
|
||||||
|
// This assumes KP_Decimal will be translated in a dot
|
||||||
|
partial keypad_keys
|
||||||
|
xkb_symbols "dot" {
|
||||||
|
|
||||||
|
key.type[Group1]="KEYPAD" ;
|
||||||
|
|
||||||
|
key <KPDL> { [ KP_Delete, KP_Decimal ] }; // <delete> <separator>
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Legacy <KPDL> #2
|
||||||
|
// This assumes KP_Separator will be translated in a comma
|
||||||
|
partial keypad_keys
|
||||||
|
xkb_symbols "comma" {
|
||||||
|
|
||||||
|
key.type[Group1]="KEYPAD" ;
|
||||||
|
|
||||||
|
key <KPDL> { [ KP_Delete, KP_Separator ] }; // <delete> <separator>
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Period <KPDL>, usual keyboard serigraphy in most countries
|
||||||
|
partial keypad_keys
|
||||||
|
xkb_symbols "dotoss" {
|
||||||
|
|
||||||
|
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||||
|
|
||||||
|
key <KPDL> { [ KP_Delete, period, comma, 0x100202F ] }; // <delete> . , ⍽ (narrow no-break space)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Period <KPDL>, usual keyboard serigraphy in most countries, latin-9 restriction
|
||||||
|
partial keypad_keys
|
||||||
|
xkb_symbols "dotoss_latin9" {
|
||||||
|
|
||||||
|
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||||
|
|
||||||
|
key <KPDL> { [ KP_Delete, period, comma, nobreakspace ] }; // <delete> . , ⍽ (no-break space)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Comma <KPDL>, what most non anglo-saxon people consider the real separator
|
||||||
|
partial keypad_keys
|
||||||
|
xkb_symbols "commaoss" {
|
||||||
|
|
||||||
|
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||||
|
|
||||||
|
key <KPDL> { [ KP_Delete, comma, period, 0x100202F ] }; // <delete> , . ⍽ (narrow no-break space)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Momayyez <KPDL>: Bahrain, Iran, Iraq, Kuwait, Oman, Qatar, Saudi Arabia, Syria, UAE
|
||||||
|
partial keypad_keys
|
||||||
|
xkb_symbols "momayyezoss" {
|
||||||
|
|
||||||
|
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||||
|
|
||||||
|
key <KPDL> { [ KP_Delete, 0x100066B, comma, 0x100202F ] }; // <delete> ? , ⍽ (narrow no-break space)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Abstracted <KPDL>, pray everything will work out (it usually does not)
|
||||||
|
partial keypad_keys
|
||||||
|
xkb_symbols "kposs" {
|
||||||
|
|
||||||
|
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||||
|
|
||||||
|
key <KPDL> { [ KP_Delete, KP_Decimal, KP_Separator, 0x100202F ] }; // <delete> ? ? ⍽ (narrow no-break space)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spreadsheets may be configured to use the dot as decimal
|
||||||
|
// punctuation, comma as a thousands separator and then semi-colon as
|
||||||
|
// the list separator. Of these, dot and semi-colon is most important
|
||||||
|
// when entering data by the keyboard; the comma can then be inferred
|
||||||
|
// and added to the presentation afterwards. Using semi-colon as a
|
||||||
|
// general separator may in fact be preferred to avoid ambiguities
|
||||||
|
// in data files. Most times a decimal separator is hard-coded, it
|
||||||
|
// seems to be period, probably since this is the syntax used in
|
||||||
|
// (most) programming languages.
|
||||||
|
partial keypad_keys
|
||||||
|
xkb_symbols "semi" {
|
||||||
|
|
||||||
|
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||||
|
|
||||||
|
key <KPDL> { [ NoSymbol, NoSymbol, semicolon ] };
|
||||||
|
};
|
||||||
+255
@@ -0,0 +1,255 @@
|
|||||||
|
// Common Latin alphabet layout
|
||||||
|
|
||||||
|
default partial
|
||||||
|
xkb_symbols "basic" {
|
||||||
|
|
||||||
|
key <AE01> { [ 1, exclam, onesuperior, exclamdown ] };
|
||||||
|
key <AE02> { [ 2, at, twosuperior, oneeighth ] };
|
||||||
|
key <AE03> { [ 3, numbersign, threesuperior, sterling ] };
|
||||||
|
key <AE04> { [ 4, dollar, onequarter, dollar ] };
|
||||||
|
key <AE05> { [ 5, percent, onehalf, threeeighths ] };
|
||||||
|
key <AE06> { [ 6, asciicircum, threequarters, fiveeighths ] };
|
||||||
|
key <AE07> { [ 7, ampersand, braceleft, seveneighths ] };
|
||||||
|
key <AE08> { [ 8, asterisk, bracketleft, trademark ] };
|
||||||
|
key <AE09> { [ 9, parenleft, bracketright, plusminus ] };
|
||||||
|
key <AE10> { [ 0, parenright, braceright, degree ] };
|
||||||
|
key <AE11> { [ minus, underscore, backslash, questiondown ] };
|
||||||
|
key <AE12> { [ equal, plus, dead_cedilla, dead_ogonek ] };
|
||||||
|
|
||||||
|
key <AD01> { [ q, Q, at, Greek_OMEGA ] };
|
||||||
|
key <AD02> { [ w, W, U017F, section ] };
|
||||||
|
key <AD03> { [ e, E, e, E ] };
|
||||||
|
key <AD04> { [ r, R, paragraph, registered ] };
|
||||||
|
key <AD05> { [ t, T, tslash, Tslash ] };
|
||||||
|
key <AD06> { [ y, Y, leftarrow, yen ] };
|
||||||
|
key <AD07> { [ u, U, downarrow, uparrow ] };
|
||||||
|
key <AD08> { [ i, I, rightarrow, idotless ] };
|
||||||
|
key <AD09> { [ o, O, oslash, Oslash ] };
|
||||||
|
key <AD10> { [ p, P, thorn, THORN ] };
|
||||||
|
key <AD11> { [bracketleft, braceleft, dead_diaeresis, dead_abovering ] };
|
||||||
|
key <AD12> { [bracketright, braceright, dead_tilde, dead_macron ] };
|
||||||
|
|
||||||
|
key <AC01> { [ a, A, ae, AE ] };
|
||||||
|
key <AC02> { [ s, S, ssharp, U1E9E ] };
|
||||||
|
key <AC03> { [ d, D, eth, ETH ] };
|
||||||
|
key <AC04> { [ f, F, dstroke, ordfeminine ] };
|
||||||
|
key <AC05> { [ g, G, eng, ENG ] };
|
||||||
|
key <AC06> { [ h, H, hstroke, Hstroke ] };
|
||||||
|
key <AC07> { [ j, J, dead_hook, dead_horn ] };
|
||||||
|
key <AC08> { [ k, K, kra, ampersand ] };
|
||||||
|
key <AC09> { [ l, L, lstroke, Lstroke ] };
|
||||||
|
key <AC10> { [ semicolon, colon, dead_acute, dead_doubleacute ] };
|
||||||
|
key <AC11> { [apostrophe, quotedbl, dead_circumflex, dead_caron ] };
|
||||||
|
key <TLDE> { [ grave, asciitilde, notsign, notsign ] };
|
||||||
|
|
||||||
|
key <BKSL> { [ backslash, bar, dead_grave, dead_breve ] };
|
||||||
|
key <AB01> { [ z, Z, guillemotleft, less ] };
|
||||||
|
key <AB02> { [ x, X, guillemotright, greater ] };
|
||||||
|
key <AB03> { [ c, C, cent, copyright ] };
|
||||||
|
key <AB04> { [ v, V, doublelowquotemark, singlelowquotemark ] };
|
||||||
|
key <AB05> { [ b, B, leftdoublequotemark, leftsinglequotemark ] };
|
||||||
|
key <AB06> { [ n, N, rightdoublequotemark, rightsinglequotemark ] };
|
||||||
|
key <AB07> { [ m, M, mu, masculine ] };
|
||||||
|
key <AB08> { [ comma, less, U2022, multiply ] }; // bullet
|
||||||
|
key <AB09> { [ period, greater, periodcentered, division ] };
|
||||||
|
key <AB10> { [ slash, question, dead_belowdot, dead_abovedot ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Northern Europe ( Danish, Finnish, Norwegian, Swedish) common layout
|
||||||
|
|
||||||
|
partial
|
||||||
|
xkb_symbols "type2" {
|
||||||
|
|
||||||
|
include "latin"
|
||||||
|
|
||||||
|
key <AE01> { [ 1, exclam, exclamdown, onesuperior ] };
|
||||||
|
key <AE02> { [ 2, quotedbl, at, twosuperior ] };
|
||||||
|
key <AE03> { [ 3, numbersign, sterling, threesuperior] };
|
||||||
|
key <AE04> { [ 4, currency, dollar, onequarter ] };
|
||||||
|
key <AE05> { [ 5, percent, onehalf, cent ] };
|
||||||
|
key <AE06> { [ 6, ampersand, yen, fiveeighths ] };
|
||||||
|
key <AE07> { [ 7, slash, braceleft, division ] };
|
||||||
|
key <AE08> { [ 8, parenleft, bracketleft, guillemotleft] };
|
||||||
|
key <AE09> { [ 9, parenright, bracketright, guillemotright] };
|
||||||
|
key <AE10> { [ 0, equal, braceright, degree ] };
|
||||||
|
|
||||||
|
key <AD03> { [ e, E, EuroSign, cent ] };
|
||||||
|
key <AD04> { [ r, R, registered, registered ] };
|
||||||
|
key <AD05> { [ t, T, thorn, THORN ] };
|
||||||
|
key <AD09> { [ o, O, oe, OE ] };
|
||||||
|
key <AD11> { [ aring, Aring, dead_diaeresis, dead_abovering ] };
|
||||||
|
key <AD12> { [dead_diaeresis, dead_circumflex, dead_tilde, dead_caron ] };
|
||||||
|
|
||||||
|
key <AC01> { [ a, A, ordfeminine, masculine ] };
|
||||||
|
|
||||||
|
key <AB03> { [ c, C, copyright, copyright ] };
|
||||||
|
key <AB08> { [ comma, semicolon, dead_cedilla, dead_ogonek ] };
|
||||||
|
key <AB09> { [ period, colon, periodcentered, dead_abovedot ] };
|
||||||
|
key <AB10> { [ minus, underscore, dead_belowdot, dead_abovedot ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Slavic Latin ( Albanian, Croatian, Polish, Slovene, Yugoslav)
|
||||||
|
// common layout
|
||||||
|
|
||||||
|
partial
|
||||||
|
xkb_symbols "type3" {
|
||||||
|
|
||||||
|
include "latin"
|
||||||
|
|
||||||
|
key <AD01> { [ q, Q, backslash, Greek_OMEGA ] };
|
||||||
|
key <AD02> { [ w, W, bar, section ] };
|
||||||
|
key <AD06> { [ z, Z, leftarrow, yen ] };
|
||||||
|
|
||||||
|
key <AC04> { [ f, F, bracketleft, ordfeminine ] };
|
||||||
|
key <AC05> { [ g, G, bracketright, ENG ] };
|
||||||
|
key <AC08> { [ k, K, lstroke, ampersand ] };
|
||||||
|
|
||||||
|
key <AB01> { [ y, Y, guillemotleft, less ] };
|
||||||
|
key <AB04> { [ v, V, at, grave ] };
|
||||||
|
key <AB05> { [ b, B, braceleft, apostrophe ] };
|
||||||
|
key <AB06> { [ n, N, braceright, acute ] };
|
||||||
|
key <AB07> { [ m, M, section, masculine ] };
|
||||||
|
key <AB08> { [ comma, semicolon, less, multiply ] };
|
||||||
|
key <AB09> { [ period, colon, greater, division ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Another common Latin layout
|
||||||
|
// (German, Estonian, Spanish, Icelandic, Italian, Latin American, Portuguese)
|
||||||
|
|
||||||
|
partial
|
||||||
|
xkb_symbols "type4" {
|
||||||
|
|
||||||
|
include "latin"
|
||||||
|
|
||||||
|
key <AE02> { [ 2, quotedbl, at, oneeighth ] };
|
||||||
|
key <AE06> { [ 6, ampersand, notsign, fiveeighths ] };
|
||||||
|
key <AE07> { [ 7, slash, braceleft, seveneighths ] };
|
||||||
|
key <AE08> { [ 8, parenleft, bracketleft, trademark ] };
|
||||||
|
key <AE09> { [ 9, parenright, bracketright, plusminus ] };
|
||||||
|
key <AE10> { [ 0, equal, braceright, degree ] };
|
||||||
|
|
||||||
|
key <AD03> { [ e, E, EuroSign, cent ] };
|
||||||
|
|
||||||
|
key <AB08> { [ comma, semicolon, U2022, multiply ] }; // bullet
|
||||||
|
key <AB09> { [ period, colon, periodcentered, division ] };
|
||||||
|
key <AB10> { [ minus, underscore, dead_belowdot, dead_abovedot ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
partial
|
||||||
|
xkb_symbols "nodeadkeys" {
|
||||||
|
|
||||||
|
key <AE12> { [ equal, plus, cedilla, ogonek ] };
|
||||||
|
key <AD11> { [bracketleft, braceleft, diaeresis, degree ] };
|
||||||
|
key <AD12> { [bracketright, braceright, asciitilde, macron ] };
|
||||||
|
key <AC07> { [ j, J, ezh, EZH ] };
|
||||||
|
key <AC10> { [ semicolon, colon, acute, doubleacute ] };
|
||||||
|
key <AC11> { [apostrophe, quotedbl, asciicircum, caron ] };
|
||||||
|
key <BKSL> { [ backslash, bar, grave, breve ] };
|
||||||
|
key <AB10> { [ slash, question, ellipsis, abovedot ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
partial
|
||||||
|
xkb_symbols "type2_nodeadkeys" {
|
||||||
|
|
||||||
|
include "latin(nodeadkeys)"
|
||||||
|
|
||||||
|
key <AD11> { [ aring, Aring, diaeresis, degree ] };
|
||||||
|
key <AD12> { [ diaeresis, asciicircum, asciitilde, caron ] };
|
||||||
|
key <AB08> { [ comma, semicolon, cedilla, ogonek ] };
|
||||||
|
key <AB09> { [ period, colon, periodcentered, abovedot ] };
|
||||||
|
key <AB10> { [ minus, underscore, ellipsis, abovedot ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
partial
|
||||||
|
xkb_symbols "type3_nodeadkeys" {
|
||||||
|
|
||||||
|
include "latin(nodeadkeys)"
|
||||||
|
};
|
||||||
|
|
||||||
|
partial
|
||||||
|
xkb_symbols "type4_nodeadkeys" {
|
||||||
|
|
||||||
|
include "latin(nodeadkeys)"
|
||||||
|
|
||||||
|
key <AB10> { [ minus, underscore, ellipsis, abovedot ] };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Added 2008.03.05 by Marcin Woliński
|
||||||
|
// See http://marcinwolinski.pl/keyboard/ for a description.
|
||||||
|
// Used by pl(intl)
|
||||||
|
//
|
||||||
|
// ┌─────┐
|
||||||
|
// │ 2 4 │ 2 = Shift, 4 = Level3 + Shift
|
||||||
|
// │ 1 3 │ 1 = Normal, 3 = Level3
|
||||||
|
// └─────┘
|
||||||
|
// ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━━━━━┓
|
||||||
|
// │ ~ ~ │ ! ' │ @ " │ # ˝ │ $ ¸ │ % ˇ │ ^ ^ │ & ˘ │ * ̇ │ ( ̣ │ ) ° │ _ ¯ │ + ˛ ┃ ⌫ Back- ┃
|
||||||
|
// │ ` ` │ 1 ¡ │ 2 © │ 3 • │ 4 § │ 5 € │ 6 ¢ │ 7 − │ 8 × │ 9 ÷ │ 0 ° │ - – │ = — ┃ space ┃
|
||||||
|
// ┢━━━━━┷━┱───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┺━┳━━━━━━━┫
|
||||||
|
// ┃ ┃ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │ { « │ } » ┃ Enter ┃
|
||||||
|
// ┃Tab ↹ ┃ q │ w │ e │ r │ t │ y │ u │ i │ o │ p │ [ ‹ │ ] › ┃ ⏎ ┃
|
||||||
|
// ┣━━━━━━━┻┱────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┺┓ ┃
|
||||||
|
// ┃ ┃ A │ S │ D │ F │ G │ H │ J │ K │ L │ : “ │ " ” │ | ¶ ┃ ┃
|
||||||
|
// ┃Caps ⇬ ┃ a │ s │ d │ f │ g │ h │ j │ k │ l │ ; ‘ │ ' ’ │ \ ┃ ┃
|
||||||
|
// ┣━━━━━━━━┹────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┲┷━━━━━┻━━━━━━┫
|
||||||
|
// ┃ │ Z │ X │ C │ V │ B │ N │ M │ < „ │ > · │ ? ¿ ┃ ┃
|
||||||
|
// ┃Shift ⇧ │ z │ x │ c │ v │ b │ n │ m │ , ‚ │ . … │ / ⁄ ┃Shift ⇧ ┃
|
||||||
|
// ┣━━━━━━━┳━━━━━┷━┳━━━┷━━━┱─┴─────┴─────┴─────┴─────┴─────┴───┲━┷━━━━━╈━━━━━┻━┳━━━━━━━┳━━━┛
|
||||||
|
// ┃ ┃ ┃ ┃ ␣ ⍽ ┃ ┃ ┃ ┃
|
||||||
|
// ┃Ctrl ┃Meta ┃Alt ┃ ␣ Space ⍽ ┃AltGr ⇮┃Menu ┃Ctrl ┃
|
||||||
|
// ┗━━━━━━━┻━━━━━━━┻━━━━━━━┹───────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┛
|
||||||
|
|
||||||
|
partial
|
||||||
|
xkb_symbols "intl" {
|
||||||
|
|
||||||
|
key <TLDE> { [ grave, asciitilde, dead_grave, dead_tilde ] };
|
||||||
|
key <AE01> { [ 1, exclam, exclamdown, dead_acute ] };
|
||||||
|
key <AE02> { [ 2, at, copyright, dead_diaeresis ] };
|
||||||
|
key <AE03> { [ 3, numbersign, U2022, dead_doubleacute ] }; // U+2022 is bullet (the name bullet does not work)
|
||||||
|
key <AE04> { [ 4, dollar, section, dead_cedilla ] };
|
||||||
|
key <AE05> { [ 5, percent, EuroSign, dead_caron ] };
|
||||||
|
key <AE06> { [ 6, asciicircum, cent, dead_circumflex ] };
|
||||||
|
key <AE07> { [ 7, ampersand, U2212, dead_breve ] }; // U+2212 is MINUS SIGN
|
||||||
|
key <AE08> { [ 8, asterisk, multiply, dead_abovedot ] };
|
||||||
|
key <AE09> { [ 9, parenleft, division, dead_belowdot ] };
|
||||||
|
key <AE10> { [ 0, parenright, degree, dead_abovering ] };
|
||||||
|
key <AE11> { [ minus, underscore, endash, dead_macron ] };
|
||||||
|
key <AE12> { [ equal, plus, emdash, dead_ogonek ] };
|
||||||
|
|
||||||
|
key <AD01> { [ q, Q ] };
|
||||||
|
key <AD02> { [ w, W ] };
|
||||||
|
key <AD03> { [ e, E ] };
|
||||||
|
key <AD04> { [ r, R ] };
|
||||||
|
key <AD05> { [ t, T ] };
|
||||||
|
key <AD06> { [ y, Y ] };
|
||||||
|
key <AD07> { [ u, U ] };
|
||||||
|
key <AD08> { [ i, I ] };
|
||||||
|
key <AD09> { [ o, O ] };
|
||||||
|
key <AD10> { [ p, P ] };
|
||||||
|
key <AD11> { [bracketleft, braceleft, U2039, guillemotleft ] };
|
||||||
|
key <AD12> { [bracketright, braceright, U203A, guillemotright ] };
|
||||||
|
|
||||||
|
key <AC01> { [ a, A ] };
|
||||||
|
key <AC02> { [ s, S ] };
|
||||||
|
key <AC03> { [ d, D ] };
|
||||||
|
key <AC04> { [ f, F ] };
|
||||||
|
key <AC05> { [ g, G ] };
|
||||||
|
key <AC06> { [ h, H ] };
|
||||||
|
key <AC07> { [ j, J ] };
|
||||||
|
key <AC08> { [ k, K ] };
|
||||||
|
key <AC09> { [ l, L ] };
|
||||||
|
key <AC10> { [ semicolon, colon, leftsinglequotemark, leftdoublequotemark ] };
|
||||||
|
key <AC11> { [apostrophe, quotedbl, rightsinglequotemark, rightdoublequotemark ] };
|
||||||
|
|
||||||
|
key <BKSL> { [ backslash, bar, NoSymbol, paragraph ] };
|
||||||
|
key <AB01> { [ z, Z ] };
|
||||||
|
key <AB02> { [ x, X ] };
|
||||||
|
key <AB03> { [ c, C ] };
|
||||||
|
key <AB04> { [ v, V ] };
|
||||||
|
key <AB05> { [ b, B ] };
|
||||||
|
key <AB06> { [ n, N ] };
|
||||||
|
key <AB07> { [ m, M ] };
|
||||||
|
key <AB08> { [ comma, less, singlelowquotemark, doublelowquotemark ] };
|
||||||
|
key <AB09> { [ period, greater, ellipsis, periodcentered ] };
|
||||||
|
key <AB10> { [ slash, question, U2044, questiondown ] }; // U+2044 is FRACTION SLASH
|
||||||
|
};
|
||||||
+156
@@ -0,0 +1,156 @@
|
|||||||
|
// These variants assign ISO_Level3_Shift to various keys
|
||||||
|
// so that levels 3 and 4 can be reached.
|
||||||
|
|
||||||
|
// The default behaviour:
|
||||||
|
// the right Alt key (AltGr) chooses the third symbol engraved on a key.
|
||||||
|
default partial modifier_keys
|
||||||
|
xkb_symbols "ralt_switch" {
|
||||||
|
key <RALT> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The right Alt key never chooses the third level.
|
||||||
|
// This option attempts to undo the effect of a layout's inclusion of
|
||||||
|
// 'ralt_switch'. You may want to also select another level3 option
|
||||||
|
// to map the level3 shift to some other key.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "ralt_alt" {
|
||||||
|
key <RALT> {[ Alt_R, Meta_R ], type[group1]="TWO_LEVEL" };
|
||||||
|
modifier_map Mod1 { <RALT> };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The right Alt key (while pressed) chooses the third shift level,
|
||||||
|
// and Compose is mapped to its second level.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "ralt_switch_multikey" {
|
||||||
|
key <RALT> {[ ISO_Level3_Shift, Multi_key ], type[group1]="TWO_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Either Alt key (while pressed) chooses the third shift level.
|
||||||
|
// (To be used mostly to imitate Mac OS functionality.)
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "alt_switch" {
|
||||||
|
include "level3(lalt_switch)"
|
||||||
|
include "level3(ralt_switch)"
|
||||||
|
};
|
||||||
|
|
||||||
|
// The left Alt key (while pressed) chooses the third shift level.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "lalt_switch" {
|
||||||
|
key <LALT> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The right Ctrl key (while pressed) chooses the third shift level.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "switch" {
|
||||||
|
key <RCTL> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Menu key (while pressed) chooses the third shift level.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "menu_switch" {
|
||||||
|
key <MENU> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Either Win key (while pressed) chooses the third shift level.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "win_switch" {
|
||||||
|
include "level3(lwin_switch)"
|
||||||
|
include "level3(rwin_switch)"
|
||||||
|
};
|
||||||
|
|
||||||
|
// The left Win key (while pressed) chooses the third shift level.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "lwin_switch" {
|
||||||
|
key <LWIN> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The right Win key (while pressed) chooses the third shift level.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "rwin_switch" {
|
||||||
|
key <RWIN> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Enter key on the kepypad (while pressed) chooses the third shift level.
|
||||||
|
// (This is especially useful for Mac laptops which miss the right Alt key.)
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "enter_switch" {
|
||||||
|
key <KPEN> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The CapsLock key (while pressed) chooses the third shift level.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "caps_switch" {
|
||||||
|
key <CAPS> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The CapsLock key (while pressed) chooses the third shift level and
|
||||||
|
// Ctrl + CapsLock has the original CapsLock function.
|
||||||
|
// The 2023 DIN standard for German keyboards recommends it as an option:
|
||||||
|
// - https://de.wikipedia.org/wiki/E1_(Tastaturbelegung)#Feststelltaste/Umschaltsperre
|
||||||
|
// - https://en.wikipedia.org/wiki/Caps_Lock#Abolition
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "caps_switch_capslock_with_ctrl" {
|
||||||
|
virtual_modifiers LevelThree;
|
||||||
|
|
||||||
|
key <CAPS> {
|
||||||
|
type[Group1] = "PC_CONTROL_LEVEL2",
|
||||||
|
symbols[Group1] = [ ISO_Level3_Shift, Caps_Lock ],
|
||||||
|
// Explicit actions are preferred over modMap None/Mod5 { Caps_Lock }
|
||||||
|
// because they have no side effect
|
||||||
|
actions[Group1] = [ SetMods(modifiers = LevelThree), LockMods(modifiers = Lock) ]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Backslash key (while pressed) chooses the third shift level.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "bksl_switch" {
|
||||||
|
key <BKSL> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The AC11 key (while pressed) chooses the third shift level.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "ac11_switch" {
|
||||||
|
key <AC11> {[ ISO_Level3_Shift ], type[Group1]="ONE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Less/Greater key (while pressed) chooses the third shift level.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "lsgt_switch" {
|
||||||
|
key <LSGT> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The CapsLock key (while pressed) chooses the third shift level,
|
||||||
|
// and latches when pressed together with another third-level chooser.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "caps_switch_latch" {
|
||||||
|
key <CAPS> {[ ISO_Level3_Shift, ISO_Level3_Shift, ISO_Level3_Latch ],
|
||||||
|
type[group1]="THREE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Backslash key (while pressed) chooses the third shift level,
|
||||||
|
// and latches when pressed together with another third-level chooser.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "bksl_switch_latch" {
|
||||||
|
key <BKSL> {[ ISO_Level3_Shift, ISO_Level3_Shift, ISO_Level3_Latch ],
|
||||||
|
type[group1]="THREE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Less/Greater key (while pressed) chooses the third shift level,
|
||||||
|
// and latches when pressed together with another third-level chooser.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "lsgt_switch_latch" {
|
||||||
|
key <LSGT> {[ ISO_Level3_Shift, ISO_Level3_Shift, ISO_Level3_Latch ],
|
||||||
|
type[group1]="THREE_LEVEL" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Top-row digit key 4 chooses third shift level when pressed alone.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "4_switch_isolated" {
|
||||||
|
override key <AE04> {[ ISO_Level3_Shift ]};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Top-row digit key 9 chooses third shift level when pressed alone.
|
||||||
|
partial modifier_keys
|
||||||
|
xkb_symbols "9_switch_isolated" {
|
||||||
|
override key <AE09> {[ ISO_Level3_Shift ]};
|
||||||
|
};
|
||||||
+2238
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,153 @@
|
|||||||
|
//! xkeyboard-config — keyboard layouts, compiled from the X11 xkeyboard-config database
|
||||||
|
//! into native Zig. It turns a physical key (a USB HID usage, as the input module delivers)
|
||||||
|
//! plus a modifier state into a **keysym** and, when the key produces one, a **character**
|
||||||
|
//! (a Unicode scalar). This is the piece that lets a `KeyEvent.keycode` become a
|
||||||
|
//! `KeyEvent.character`, without shipping an X11 runtime.
|
||||||
|
//!
|
||||||
|
//! The layout tables in `generated/layouts.zig` are produced by
|
||||||
|
//! `tools/make-xkeyboard-config.py` (see ./README.md to regenerate). Those tables are
|
||||||
|
//! deliberately pure data — each key carries its up-to-four levels and an XKB *type*. The
|
||||||
|
//! type -> level selection semantics (which modifier picks which level) live here, so the
|
||||||
|
//! data and the policy are separable.
|
||||||
|
//!
|
||||||
|
//! Scope (documented in README.md): group 1 only, no dead-key/compose composition (a dead
|
||||||
|
//! key returns its keysym with no character), and a curated set of key types. Layouts:
|
||||||
|
//! us, gb, de, fr, es, dvorak.
|
||||||
|
//!
|
||||||
|
//! Upstream xkeyboard-config and keysymdef.h are MIT/X11 licensed; see vendor/COPYING and
|
||||||
|
//! vendor/PROVENANCE.md.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const generated = @import("layouts");
|
||||||
|
|
||||||
|
pub const Level = generated.Level;
|
||||||
|
pub const KeyType = generated.KeyType;
|
||||||
|
pub const Key = generated.Key;
|
||||||
|
pub const Layout = generated.Layout;
|
||||||
|
|
||||||
|
/// The generated layouts, by name — as pointers, so they share identity with `all` and
|
||||||
|
/// `byName` (and match the `*const Layout` that `map` takes).
|
||||||
|
pub const us: *const Layout = &generated.us;
|
||||||
|
pub const gb: *const Layout = &generated.gb;
|
||||||
|
pub const de: *const Layout = &generated.de;
|
||||||
|
pub const fr: *const Layout = &generated.fr;
|
||||||
|
pub const es: *const Layout = &generated.es;
|
||||||
|
pub const dvorak: *const Layout = &generated.dvorak;
|
||||||
|
|
||||||
|
/// Every generated layout, for enumeration (e.g. a settings UI).
|
||||||
|
pub const all = generated.all;
|
||||||
|
|
||||||
|
/// The modifier state that selects a key's level. `level3` is AltGr (ISO Level3 Shift);
|
||||||
|
/// `control` is accepted for completeness but does not affect level selection here.
|
||||||
|
pub const Modifiers = struct {
|
||||||
|
shift: bool = false,
|
||||||
|
caps_lock: bool = false,
|
||||||
|
level3: bool = false,
|
||||||
|
control: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The result of a lookup: the X11 `keysym`, and the `character` it produces (a Unicode
|
||||||
|
/// scalar) when it is a printable key — null for keys that produce none (Return, F1, a
|
||||||
|
/// bare dead key, an unmapped key).
|
||||||
|
pub const Mapping = struct {
|
||||||
|
keysym: u32,
|
||||||
|
character: ?u21,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Which level (0..3) a key of `kind` selects under `mods`. XKB's canonical semantics:
|
||||||
|
/// Shift picks the odd level, AltGr (level3) adds 2, and Caps acts like Shift for the
|
||||||
|
/// alphabetic types. See the XKB "key types" — this covers the ones the vendored layouts
|
||||||
|
/// use; anything else falls back to shift-or-not.
|
||||||
|
fn selectLevel(kind: KeyType, mods: Modifiers) usize {
|
||||||
|
const shift_or_caps = mods.shift != mods.caps_lock; // XOR: Caps behaves like Shift
|
||||||
|
const low: usize = if (mods.shift) 1 else 0;
|
||||||
|
const high: usize = if (mods.level3) 2 else 0;
|
||||||
|
return switch (kind) {
|
||||||
|
.one_level => 0,
|
||||||
|
.two_level, .keypad, .other => low,
|
||||||
|
.alphabetic => if (shift_or_caps) 1 else 0,
|
||||||
|
.four_level => low + high,
|
||||||
|
.four_level_alphabetic => (if (shift_or_caps) @as(usize, 1) else 0) + high,
|
||||||
|
// Caps affects only the base pair, not the AltGr pair.
|
||||||
|
.four_level_semialphabetic => if (mods.level3) 2 + low else (if (shift_or_caps) @as(usize, 1) else 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a physical key (`hid_usage`, a USB HID keyboard-page usage) under `mods` on
|
||||||
|
/// `layout` to its keysym and character. Falls back gracefully when the selected level is
|
||||||
|
/// undefined for the key: it drops the AltGr component, then the shift component, so a key
|
||||||
|
/// with only a base/shift pair still yields something sensible under AltGr.
|
||||||
|
pub fn map(layout: *const Layout, hid_usage: u8, mods: Modifiers) Mapping {
|
||||||
|
const key = &layout.keys[hid_usage];
|
||||||
|
var level = selectLevel(key.kind, mods);
|
||||||
|
// Fall back to a defined level: full -> without AltGr -> base.
|
||||||
|
if (key.levels[level].keysym == 0 and key.levels[level].unicode == 0) {
|
||||||
|
const candidates = [_]usize{ level & 1, 0 };
|
||||||
|
for (candidates) |candidate| {
|
||||||
|
if (key.levels[candidate].keysym != 0 or key.levels[candidate].unicode != 0) {
|
||||||
|
level = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const chosen = key.levels[level];
|
||||||
|
return .{
|
||||||
|
.keysym = chosen.keysym,
|
||||||
|
.character = if (chosen.unicode != 0) @intCast(chosen.unicode) else null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up a layout by its name (`"us"`, `"gb"`, ...), or null if unknown.
|
||||||
|
pub fn byName(name: []const u8) ?*const Layout {
|
||||||
|
for (all) |layout| {
|
||||||
|
if (std.mem.eql(u8, layout.name, name)) return layout;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- tests (host-run via `zig build test`) ---------------------------------
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
// USB HID usages used in the tests (keyboard page 0x07).
|
||||||
|
const hid_a: u8 = 0x04;
|
||||||
|
const hid_1: u8 = 0x1e;
|
||||||
|
const hid_3: u8 = 0x20;
|
||||||
|
|
||||||
|
test "us: letters obey shift and caps" {
|
||||||
|
try testing.expectEqual(@as(?u21, 'a'), map(us, hid_a, .{}).character);
|
||||||
|
try testing.expectEqual(@as(?u21, 'A'), map(us, hid_a, .{ .shift = true }).character);
|
||||||
|
try testing.expectEqual(@as(?u21, 'A'), map(us, hid_a, .{ .caps_lock = true }).character);
|
||||||
|
// Shift + Caps cancels for an alphabetic key.
|
||||||
|
try testing.expectEqual(@as(?u21, 'a'), map(us, hid_a, .{ .shift = true, .caps_lock = true }).character);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "us: digits and their shifted symbols" {
|
||||||
|
try testing.expectEqual(@as(?u21, '1'), map(us, hid_1, .{}).character);
|
||||||
|
try testing.expectEqual(@as(?u21, '!'), map(us, hid_1, .{ .shift = true }).character);
|
||||||
|
try testing.expectEqual(@as(?u21, '3'), map(us, hid_3, .{}).character);
|
||||||
|
try testing.expectEqual(@as(?u21, '#'), map(us, hid_3, .{ .shift = true }).character);
|
||||||
|
// A digit is not alphabetic: Caps alone must not shift it.
|
||||||
|
try testing.expectEqual(@as(?u21, '3'), map(us, hid_3, .{ .caps_lock = true }).character);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "layouts differ: GB pound vs US hash on shift+3" {
|
||||||
|
try testing.expectEqual(@as(?u21, '#'), map(us, hid_3, .{ .shift = true }).character);
|
||||||
|
try testing.expectEqual(@as(?u21, '£'), map(gb, hid_3, .{ .shift = true }).character);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "french azerty places q where us has a" {
|
||||||
|
try testing.expectEqual(@as(?u21, 'q'), map(fr, hid_a, .{}).character);
|
||||||
|
try testing.expectEqual(@as(?u21, 'Q'), map(fr, hid_a, .{ .shift = true }).character);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "byName resolves and rejects" {
|
||||||
|
try testing.expect(byName("us") == us);
|
||||||
|
try testing.expect(byName("gb") == gb);
|
||||||
|
try testing.expect(byName("nonsense") == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "unmapped key yields no character" {
|
||||||
|
// HID 0x00 is not a key; every level is empty.
|
||||||
|
try testing.expectEqual(@as(?u21, null), map(us, 0x00, .{}).character);
|
||||||
|
}
|
||||||
@@ -52,9 +52,34 @@ pub const SystemCall = enum(u64) {
|
|||||||
clock = 23, // clock() -> nanoseconds since boot: a monotonic time source (for timeouts/delays)
|
clock = 23, // clock() -> nanoseconds since boot: a monotonic time source (for timeouts/delays)
|
||||||
process_enumerate = 24, // process_enumerate(buffer, maximum) -> total: snapshot the task table
|
process_enumerate = 24, // process_enumerate(buffer, maximum) -> total: snapshot the task table
|
||||||
process_kill = 25, // process_kill(id) -> 0/-errno: end a process this process spawned
|
process_kill = 25, // process_kill(id) -> 0/-errno: end a process this process spawned
|
||||||
|
ipc_send = 26, // ipc_send(handle, message_ptr, message_len) -> 0/-errno: post a payload to an endpoint's async queue without blocking
|
||||||
|
process_exit_reason = 27, // process_exit_reason(id) -> ExitReason/-errno: how a dead child ended (its supervisor only)
|
||||||
|
process_subscribe = 28, // process_subscribe(endpoint) -> 0/-errno: subscribe to published exit events — every death posts a notification
|
||||||
|
signal_bind = 29, // signal_bind(endpoint) -> 0/-errno: nominate the endpoint this process's signals arrive on
|
||||||
|
process_signal = 30, // process_signal(id, signal) -> 0/-errno: post a signal to a child (or to yourself)
|
||||||
|
timer_bind = 31, // timer_bind(endpoint, ms) -> 0/-errno: one-shot timer — posts a notification when ms elapse
|
||||||
|
klog_read = 32, // klog_read(offset, ptr, len) -> bytes copied: copy the kernel RAM log buffer out to a user buffer (for persisting the boot log to disk)
|
||||||
|
wall_clock = 33, // wall_clock() -> Unix epoch seconds (UTC): the RTC wall-clock time, for filesystem timestamps (mtime). Monotonic time is `clock`.
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// How a process ended — recorded by the kernel at death, queried by the
|
||||||
|
/// supervisor with `process_exit_reason`, and the input to its restart decision
|
||||||
|
/// (docs/process-lifecycle.md): a clean exit meant to stop, a fault wants a
|
||||||
|
/// restart with backoff, killed means the supervisor did it itself. The faults
|
||||||
|
/// mirror the CPU exceptions a ring-3 process can die of; they are exit reasons,
|
||||||
|
/// never delivered to the faulting process (recovery is restart, not a handler).
|
||||||
|
pub const ExitReason = enum(u8) {
|
||||||
|
exited = 0, // returned from main / called exit
|
||||||
|
aborted = 1, // deliberate self-termination (reserved: no abort path yet)
|
||||||
|
segmentation_fault = 2, // page fault
|
||||||
|
illegal_instruction = 3, // invalid opcode
|
||||||
|
arithmetic_fault = 4, // divide error, x87 or SIMD fault
|
||||||
|
protection_fault = 5, // general protection fault
|
||||||
|
fault = 6, // any other CPU exception
|
||||||
|
killed = 7, // process_kill
|
||||||
|
};
|
||||||
|
|
||||||
/// The x86 MSI message address base (`0xFEE0_0000`): a device raises an MSI by writing
|
/// The x86 MSI message address base (`0xFEE0_0000`): a device raises an MSI by writing
|
||||||
/// `data` to this address, which the Local APIC turns into an interrupt at the vector
|
/// `data` to this address, which the Local APIC turns into an interrupt at the vector
|
||||||
/// in `data`. The kernel returns the concrete (address, data) from `msi_bind`; this is
|
/// in `data`. The kernel returns the concrete (address, data) from `msi_bind`; this is
|
||||||
@@ -83,6 +108,44 @@ pub const notify_badge_bit: u64 = 1 << 63;
|
|||||||
/// notifications, which never set this bit). The microkernel's SIGCHLD.
|
/// notifications, which never set this bit). The microkernel's SIGCHLD.
|
||||||
pub const notify_exit_bit: u64 = 1 << 62;
|
pub const notify_exit_bit: u64 = 1 << 62;
|
||||||
|
|
||||||
|
/// Set (alongside `notify_badge_bit`) in the badge of a **buffered message** — a payload
|
||||||
|
/// posted to an endpoint's async queue by `ipc_send`, delivered through `ipc_reply_wait`
|
||||||
|
/// like a notification (no reply owed) but carrying bytes in the receive buffer, not just
|
||||||
|
/// a badge. This is what distinguishes a payload-bearing async message from a bare IRQ /
|
||||||
|
/// child-exit notification (which sets neither this nor `notify_exit_bit`). The low bits
|
||||||
|
/// carry the sender's task id. The async counterpart of the synchronous `ipc_call`, for
|
||||||
|
/// broadcasts where a rendezvous is the wrong shape (the input service is the first user).
|
||||||
|
pub const notify_message_bit: u64 = 1 << 61;
|
||||||
|
|
||||||
|
/// Set (alongside `notify_badge_bit`) in the badge of a **signal notification** —
|
||||||
|
/// the process-lifecycle vocabulary of docs/process-lifecycle.md, delivered to the
|
||||||
|
/// endpoint the process nominated with `signal_bind`. The low bits carry the
|
||||||
|
/// coalesced pending mask (bit positions = `Signal` values): signals are
|
||||||
|
/// statements, not questions, and two pending terminates are one terminate.
|
||||||
|
pub const notify_signal_bit: u64 = 1 << 60;
|
||||||
|
|
||||||
|
/// Set (alongside `notify_badge_bit`) in the badge of a **timer notification** —
|
||||||
|
/// a one-shot `timer_bind` deadline landing. No payload bits: what to do when the
|
||||||
|
/// deadline fires is whatever the receiver armed it for (a stop-sequence
|
||||||
|
/// escalation, a restart backoff, an alarm).
|
||||||
|
pub const notify_timer_bit: u64 = 1 << 59;
|
||||||
|
|
||||||
|
/// The signal vocabulary (docs/process-lifecycle.md): POSIX's concepts, danos's
|
||||||
|
/// names, message delivery. The value is the bit position in the pending mask — a
|
||||||
|
/// private kernel/runtime detail, free to change while they ship together. Kill
|
||||||
|
/// is not here (it is `process_kill`, unhandleable by definition); faults are not
|
||||||
|
/// here (they are `ExitReason`s — recovery is restart, not a handler); liveness is
|
||||||
|
/// not here (a question, asked as the zero-length ping call, not a statement).
|
||||||
|
pub const Signal = enum(u5) {
|
||||||
|
terminate = 0, // finish up and exit (the polite half of the stop sequence)
|
||||||
|
reload = 1, // re-read configuration / re-scan
|
||||||
|
interrupt = 2, // interactive interrupt (no sender until a console exists)
|
||||||
|
quit = 3, // as interrupt, by convention more final
|
||||||
|
alarm = 4, // a timer the process armed for itself (unbuilt: no consumer yet)
|
||||||
|
user_1 = 5, // service-defined
|
||||||
|
user_2 = 6, // service-defined
|
||||||
|
};
|
||||||
|
|
||||||
/// Capacity of `ProcessDescriptor.name` — matches the longest name `system_spawn`
|
/// Capacity of `ProcessDescriptor.name` — matches the longest name `system_spawn`
|
||||||
/// accepts, so a process's recorded name (its argv[0]) is never truncated.
|
/// accepts, so a process's recorded name (its argv[0]) is never truncated.
|
||||||
pub const maximum_process_name = 64;
|
pub const maximum_process_name = 64;
|
||||||
@@ -113,6 +176,14 @@ pub const ProcessDescriptor = extern struct {
|
|||||||
/// during bring-up. The VFS server registers under `vfs`; clients look it up.
|
/// during bring-up. The VFS server registers under `vfs`; clients look it up.
|
||||||
pub const ServiceId = enum(u32) {
|
pub const ServiceId = enum(u32) {
|
||||||
vfs = 1,
|
vfs = 1,
|
||||||
|
input = 2,
|
||||||
|
ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes
|
||||||
|
device_manager = 4, // the tree, the matcher, the supervisor (docs/device-manager.md)
|
||||||
|
power = 5, // system power: events (button, lid, battery) + shutdown (docs/power.md; domain-named per docs/discovery.md — the acpi service registers it on x86, a PSCI service will on ARM)
|
||||||
|
usb_bus = 6, // the xHCI host-controller driver's transfer endpoint; USB class drivers look it up and `callCap`-open their device to get a private per-device transfer channel (docs/driver-model.md)
|
||||||
|
block = 7, // a block-device driver (USB mass storage today): read/write of fixed-size blocks, the storage a filesystem sits on
|
||||||
|
fat = 8, // the FAT filesystem server; the VFS mounts it and forwards paths under its mount point (/mnt/usb) to it
|
||||||
|
display = 9, // the display service: owns the framebuffer, composites a layer stack, presents frames (docs/display.md)
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+115
-45
@@ -1,60 +1,120 @@
|
|||||||
//! ACPI / PnP hardware-ID (`_HID`) names: the flat analog of pci-class.zig for
|
//! ACPI / PnP hardware-ID (`_HID`) names: the flat analog of pci-class.zig for
|
||||||
//! `acpi_device` nodes. Unlike PCI, ACPI has no class/subclass/prog-IF taxonomy — a
|
//! `acpi_device` nodes. Unlike PCI, ACPI has no class/subclass/prog-IF taxonomy — a
|
||||||
//! device's identity *is* its `_HID` string (`PNP0303` simply means "PS/2 keyboard"),
|
//! device's identity *is* its `_HID` string (`PNP0303` simply means "PS/2 keyboard"),
|
||||||
//! so this is a plain id -> description registry rather than a hierarchical decoder.
|
//! so this is a plain id <-> name registry rather than a hierarchical decoder.
|
||||||
//! The well-known PnP/ACPI IDs; vendor-specific ids (e.g. `QEMU0002`, `INTC1234`) have
|
//! The well-known PnP/ACPI IDs; vendor-specific ids (e.g. `QEMU0002`, `INTC1234`) have
|
||||||
//! no standard name and return "". Pure reference data, so it is shared by kernel
|
//! no standard name and decode to nothing. Pure reference data, so it is shared by
|
||||||
//! discovery (the device-tree dump) and any user-space tool.
|
//! kernel discovery (the device-tree dump) and any user-space driver or tool.
|
||||||
|
//!
|
||||||
|
//! Code that means a specific device names the `HardwareId` variant instead of its
|
||||||
|
//! `_HID` string — `HardwareId.ps2_keyboard.hid()` reads without a registry lookup,
|
||||||
|
//! where a bare `"PNP0303"` does not.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// The common standard PnP/ACPI hardware IDs, as named values. Prefix ranges hint at
|
||||||
|
/// the grouping (PNP03xx keyboards, PNP0Fxx pointing devices, PNP0Cxx ACPI
|
||||||
|
/// power/thermal, PNP0Axx buses), but there is no formal hierarchy — hence a flat
|
||||||
|
/// enum over a flat registry.
|
||||||
|
pub const HardwareId = enum {
|
||||||
|
programmable_interrupt_controller,
|
||||||
|
system_timer,
|
||||||
|
high_precision_event_timer,
|
||||||
|
dma_controller,
|
||||||
|
ps2_keyboard,
|
||||||
|
parallel_port,
|
||||||
|
ecp_parallel_port,
|
||||||
|
serial_port,
|
||||||
|
floppy_disk_controller,
|
||||||
|
system_speaker,
|
||||||
|
pci_bus,
|
||||||
|
generic_container,
|
||||||
|
/// The second id the ACPI spec assigns the same "Generic Container Device" name.
|
||||||
|
generic_container_extended,
|
||||||
|
pci_express_root_bridge,
|
||||||
|
real_time_clock,
|
||||||
|
system_board,
|
||||||
|
motherboard_reserved_resources,
|
||||||
|
math_coprocessor,
|
||||||
|
acpi_system_board,
|
||||||
|
embedded_controller,
|
||||||
|
control_method_battery,
|
||||||
|
fan,
|
||||||
|
power_button,
|
||||||
|
lid,
|
||||||
|
sleep_button,
|
||||||
|
pci_interrupt_link,
|
||||||
|
microsoft_ps2_mouse,
|
||||||
|
ps2_mouse,
|
||||||
|
ac_adapter,
|
||||||
|
processor_device,
|
||||||
|
processor_aggregator,
|
||||||
|
processor_container,
|
||||||
|
|
||||||
const Entry = struct { hid: []const u8, name: []const u8 };
|
const Entry = struct { hid: []const u8, name: []const u8 };
|
||||||
|
|
||||||
/// The common standard PnP/ACPI hardware IDs. Prefix ranges hint at the grouping
|
/// The registry row for this id: its `_HID` string and human-readable name.
|
||||||
/// (PNP03xx keyboards, PNP0Fxx pointing devices, PNP0Cxx ACPI power/thermal,
|
fn entry(self: HardwareId) Entry {
|
||||||
/// PNP0Axx buses), but there is no formal hierarchy — hence a flat table.
|
return switch (self) {
|
||||||
const table = [_]Entry{
|
.programmable_interrupt_controller => .{ .hid = "PNP0000", .name = "Programmable Interrupt Controller (PIC)" },
|
||||||
.{ .hid = "PNP0000", .name = "Programmable Interrupt Controller (PIC)" },
|
.system_timer => .{ .hid = "PNP0100", .name = "System Timer (PIT)" },
|
||||||
.{ .hid = "PNP0100", .name = "System Timer (PIT)" },
|
.high_precision_event_timer => .{ .hid = "PNP0103", .name = "High Precision Event Timer (HPET)" },
|
||||||
.{ .hid = "PNP0103", .name = "High Precision Event Timer (HPET)" },
|
.dma_controller => .{ .hid = "PNP0200", .name = "DMA Controller" },
|
||||||
.{ .hid = "PNP0200", .name = "DMA Controller" },
|
.ps2_keyboard => .{ .hid = "PNP0303", .name = "PS/2 Keyboard" },
|
||||||
.{ .hid = "PNP0303", .name = "PS/2 Keyboard" },
|
.parallel_port => .{ .hid = "PNP0400", .name = "Standard LPT Parallel Port" },
|
||||||
.{ .hid = "PNP0400", .name = "Standard LPT Parallel Port" },
|
.ecp_parallel_port => .{ .hid = "PNP0401", .name = "ECP Parallel Port" },
|
||||||
.{ .hid = "PNP0401", .name = "ECP Parallel Port" },
|
.serial_port => .{ .hid = "PNP0501", .name = "16550A-compatible Serial Port" },
|
||||||
.{ .hid = "PNP0501", .name = "16550A-compatible Serial Port" },
|
.floppy_disk_controller => .{ .hid = "PNP0700", .name = "PC Floppy Disk Controller" },
|
||||||
.{ .hid = "PNP0700", .name = "PC Floppy Disk Controller" },
|
.system_speaker => .{ .hid = "PNP0800", .name = "System Speaker" },
|
||||||
.{ .hid = "PNP0800", .name = "System Speaker" },
|
.pci_bus => .{ .hid = "PNP0A03", .name = "PCI Bus" },
|
||||||
.{ .hid = "PNP0A03", .name = "PCI Bus" },
|
.generic_container => .{ .hid = "PNP0A05", .name = "Generic Container Device" },
|
||||||
.{ .hid = "PNP0A05", .name = "Generic Container Device" },
|
.generic_container_extended => .{ .hid = "PNP0A06", .name = "Generic Container Device" },
|
||||||
.{ .hid = "PNP0A06", .name = "Generic Container Device" },
|
.pci_express_root_bridge => .{ .hid = "PNP0A08", .name = "PCI Express Root Bridge" },
|
||||||
.{ .hid = "PNP0A08", .name = "PCI Express Root Bridge" },
|
.real_time_clock => .{ .hid = "PNP0B00", .name = "Real-Time Clock (RTC)" },
|
||||||
.{ .hid = "PNP0B00", .name = "Real-Time Clock (RTC)" },
|
.system_board => .{ .hid = "PNP0C01", .name = "System Board" },
|
||||||
.{ .hid = "PNP0C01", .name = "System Board" },
|
.motherboard_reserved_resources => .{ .hid = "PNP0C02", .name = "Motherboard Reserved Resources" },
|
||||||
.{ .hid = "PNP0C02", .name = "Motherboard Reserved Resources" },
|
.math_coprocessor => .{ .hid = "PNP0C04", .name = "Math Coprocessor" },
|
||||||
.{ .hid = "PNP0C04", .name = "Math Coprocessor" },
|
.acpi_system_board => .{ .hid = "PNP0C08", .name = "ACPI System Board" },
|
||||||
.{ .hid = "PNP0C08", .name = "ACPI System Board" },
|
.embedded_controller => .{ .hid = "PNP0C09", .name = "ACPI Embedded Controller" },
|
||||||
.{ .hid = "PNP0C09", .name = "ACPI Embedded Controller" },
|
.control_method_battery => .{ .hid = "PNP0C0A", .name = "ACPI Control Method Battery" },
|
||||||
.{ .hid = "PNP0C0A", .name = "ACPI Control Method Battery" },
|
.fan => .{ .hid = "PNP0C0B", .name = "ACPI Fan" },
|
||||||
.{ .hid = "PNP0C0B", .name = "ACPI Fan" },
|
.power_button => .{ .hid = "PNP0C0C", .name = "ACPI Power Button" },
|
||||||
.{ .hid = "PNP0C0C", .name = "ACPI Power Button" },
|
.lid => .{ .hid = "PNP0C0D", .name = "ACPI Lid" },
|
||||||
.{ .hid = "PNP0C0D", .name = "ACPI Lid" },
|
.sleep_button => .{ .hid = "PNP0C0E", .name = "ACPI Sleep Button" },
|
||||||
.{ .hid = "PNP0C0E", .name = "ACPI Sleep Button" },
|
.pci_interrupt_link => .{ .hid = "PNP0C0F", .name = "PCI Interrupt Link Device" },
|
||||||
.{ .hid = "PNP0C0F", .name = "PCI Interrupt Link Device" },
|
.microsoft_ps2_mouse => .{ .hid = "PNP0F03", .name = "Microsoft PS/2 Mouse" },
|
||||||
.{ .hid = "PNP0F03", .name = "Microsoft PS/2 Mouse" },
|
.ps2_mouse => .{ .hid = "PNP0F13", .name = "PS/2 Mouse" },
|
||||||
.{ .hid = "PNP0F13", .name = "PS/2 Mouse" },
|
.ac_adapter => .{ .hid = "ACPI0003", .name = "AC Adapter" },
|
||||||
.{ .hid = "ACPI0003", .name = "AC Adapter" },
|
.processor_device => .{ .hid = "ACPI0007", .name = "Processor Device" },
|
||||||
.{ .hid = "ACPI0007", .name = "Processor Device" },
|
.processor_aggregator => .{ .hid = "ACPI000C", .name = "Processor Aggregator" },
|
||||||
.{ .hid = "ACPI000C", .name = "Processor Aggregator" },
|
.processor_container => .{ .hid = "ACPI0010", .name = "Processor Container" },
|
||||||
.{ .hid = "ACPI0010", .name = "Processor Container" },
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This id's `_HID` string (e.g. `.ps2_keyboard` -> "PNP0303").
|
||||||
|
pub fn hid(self: HardwareId) []const u8 {
|
||||||
|
return self.entry().hid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This id's human-readable name (e.g. `.ps2_keyboard` -> "PS/2 Keyboard").
|
||||||
|
pub fn description(self: HardwareId) []const u8 {
|
||||||
|
return self.entry().name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The named value for a `_HID` string, or null if it is not a known standard
|
||||||
|
/// id (vendor-specific ids are not in the registry).
|
||||||
|
pub fn fromHid(hid_string: []const u8) ?HardwareId {
|
||||||
|
for (std.enums.values(HardwareId)) |id| {
|
||||||
|
if (std.mem.eql(u8, id.hid(), hid_string)) return id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The human-readable name for a `_HID`, or "" if it is not a known standard id
|
/// The human-readable name for a `_HID` string, or "" if it is not a known standard
|
||||||
/// (vendor-specific ids have no registry name — callers just print the raw HID).
|
/// id (vendor-specific ids have no registry name — callers just print the raw HID).
|
||||||
pub fn description(hid: []const u8) []const u8 {
|
pub fn description(hid: []const u8) []const u8 {
|
||||||
for (table) |entry| {
|
return (HardwareId.fromHid(hid) orelse return "").description();
|
||||||
if (std.mem.eql(u8, entry.hid, hid)) return entry.name;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "decodes standard PnP/ACPI ids and leaves the rest alone" {
|
test "decodes standard PnP/ACPI ids and leaves the rest alone" {
|
||||||
@@ -66,3 +126,13 @@ test "decodes standard PnP/ACPI ids and leaves the rest alone" {
|
|||||||
try eq("", description("QEMU0002")); // vendor-specific: no standard name
|
try eq("", description("QEMU0002")); // vendor-specific: no standard name
|
||||||
try eq("", description("")); // no HID at all
|
try eq("", description("")); // no HID at all
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "named values round-trip through their _HID strings" {
|
||||||
|
const testing = std.testing;
|
||||||
|
try testing.expectEqualStrings("PNP0303", HardwareId.ps2_keyboard.hid());
|
||||||
|
try testing.expectEqual(@as(?HardwareId, .ps2_mouse), HardwareId.fromHid("PNP0F13"));
|
||||||
|
try testing.expectEqual(@as(?HardwareId, null), HardwareId.fromHid("QEMU0002"));
|
||||||
|
for (std.enums.values(HardwareId)) |id| {
|
||||||
|
try testing.expectEqual(@as(?HardwareId, id), HardwareId.fromHid(id.hid()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+153
-545
@@ -3,11 +3,13 @@
|
|||||||
//! Walks the ACPI tables the firmware left in memory (starting from the RSDP the
|
//! Walks the ACPI tables the firmware left in memory (starting from the RSDP the
|
||||||
//! bootloader handed us) and translates the static tables into the generic
|
//! bootloader handed us) and translates the static tables into the generic
|
||||||
//! `device` model, so the kernel enumerates hardware without knowing ACPI is the
|
//! `device` model, so the kernel enumerates hardware without knowing ACPI is the
|
||||||
//! source. This is deliberately the *static-table* path: MADT (CPUs / interrupt
|
//! source. This is deliberately the *static-table* path, and **only** that: MADT
|
||||||
//! controllers), MCFG (PCIe ECAM -> PCI enumeration), HPET (timer), and FADT
|
//! (CPUs / interrupt controllers), MCFG (PCIe ECAM -> PCI enumeration), HPET
|
||||||
//! (power register map). The DSDT/SSDT bytecode is handed to the `aml` submodule
|
//! (timer), and FADT (power register map). The DSDT/SSDT bytecode is *not*
|
||||||
//! only to extract the sleep-state (`_Sx`) values for power management; full AML namespace
|
//! interpreted here — the kernel collects the blobs and publishes them on the
|
||||||
//! interpretation is a separate, larger subproject.
|
//! acpi-tables node for the ring-3 acpi service to parse (device enumeration and
|
||||||
|
//! soft-off). Keeping the ~0.5 MB AML interpretation out of kernel init keeps it
|
||||||
|
//! off the single-core critical path (nothing else runs alongside it there).
|
||||||
//!
|
//!
|
||||||
//! ACPI tables live in `.acpi_tables` / `.acpi_nvs` memory, which the kernel
|
//! ACPI tables live in `.acpi_tables` / `.acpi_nvs` memory, which the kernel
|
||||||
//! identity-maps, so table addresses are dereferenced directly. PCIe ECAM is MMIO
|
//! identity-maps, so table addresses are dereferenced directly. PCIe ECAM is MMIO
|
||||||
@@ -19,7 +21,6 @@ const boot_handoff = @import("boot-handoff");
|
|||||||
const abi = @import("abi");
|
const abi = @import("abi");
|
||||||
const parameters = @import("parameters");
|
const parameters = @import("parameters");
|
||||||
const device_model = @import("device-model.zig");
|
const device_model = @import("device-model.zig");
|
||||||
const aml = @import("aml/aml.zig");
|
|
||||||
const DeviceTree = device_model.DeviceTree;
|
const DeviceTree = device_model.DeviceTree;
|
||||||
const Hal = device_model.Hal;
|
const Hal = device_model.Hal;
|
||||||
|
|
||||||
@@ -37,9 +38,15 @@ pub const RegisterAccess = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Everything the power subsystem needs, extracted from the FADT and the AML
|
/// The power register map, extracted from the FADT during discovery. Populated by
|
||||||
/// sleep packages during discovery. Populated by `discover`, read by `power`.
|
/// `discover`, read by `power` (kernel reboot). The **sleep-state (`_Sx`) values
|
||||||
|
/// live in AML**, which the kernel no longer parses — soft-off (S5) is owned by the
|
||||||
|
/// ring-3 acpi service (it re-parses the blobs on the published acpi-tables node and
|
||||||
|
/// writes the PM1 control register itself). So this holds only the FADT scalars.
|
||||||
pub const PowerInformation = struct {
|
pub const PowerInformation = struct {
|
||||||
|
/// The System Control Interrupt's GSI (FADT SCI_INT) — the line ACPI events
|
||||||
|
/// (power button, GPEs) arrive on. Published to the acpi service for M21.
|
||||||
|
sci_interrupt: u16 = 0,
|
||||||
/// The SMM command port and the value that switches the platform into ACPI mode.
|
/// The SMM command port and the value that switches the platform into ACPI mode.
|
||||||
smi_cmd: u16 = 0,
|
smi_cmd: u16 = 0,
|
||||||
acpi_enable: u8 = 0,
|
acpi_enable: u8 = 0,
|
||||||
@@ -51,9 +58,6 @@ pub const PowerInformation = struct {
|
|||||||
reset: RegisterAccess = .{},
|
reset: RegisterAccess = .{},
|
||||||
reset_value: u8 = 0,
|
reset_value: u8 = 0,
|
||||||
reset_supported: bool = false,
|
reset_supported: bool = false,
|
||||||
/// SLP_TYP values for S5 (soft off) and S3 (suspend), from the AML sleep-state (`_Sx`) packages.
|
|
||||||
s5: ?aml.SleepType = null,
|
|
||||||
s3: ?aml.SleepType = null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Filled in by `discover`; the power service reads it to reboot/shutdown.
|
/// Filled in by `discover`; the power service reads it to reboot/shutdown.
|
||||||
@@ -137,25 +141,20 @@ const maximum_cpus = parameters.maximum_cpus;
|
|||||||
/// Filled in by `discover` (from the MADT); SMP bring-up reads it to wake the APs.
|
/// Filled in by `discover` (from the MADT); SMP bring-up reads it to wake the APs.
|
||||||
pub var cpu_information: CpuInformation = .{};
|
pub var cpu_information: CpuInformation = .{};
|
||||||
|
|
||||||
/// Integrity/diagnostics for the AML parse. `consumed == total` means the parser
|
|
||||||
/// walked every byte of the DSDT/SSDTs without desyncing.
|
|
||||||
pub const AmlStats = struct {
|
|
||||||
nodes: usize = 0,
|
|
||||||
consumed: usize = 0,
|
|
||||||
total: usize = 0,
|
|
||||||
};
|
|
||||||
pub var aml_stats: AmlStats = .{};
|
|
||||||
|
|
||||||
/// The ACPI namespace built from the DSDT/SSDTs, kept for sleep-state (`_Sx`) lookup now and
|
|
||||||
/// device enumeration later. Null until `discover` runs successfully.
|
|
||||||
pub var namespace: ?aml.Namespace = null;
|
|
||||||
|
|
||||||
/// Physical address of the DSDT the FADT points at, or 0.
|
/// Physical address of the DSDT the FADT points at, or 0.
|
||||||
pub var dsdt_physical: u64 = 0;
|
pub var dsdt_physical: u64 = 0;
|
||||||
|
|
||||||
|
/// The FADT itself (physical + length), published on the acpi-tables node so
|
||||||
|
/// the ring-3 acpi service can read the PM1 event and GPE blocks it needs for
|
||||||
|
/// the event side (docs/acpi.md — ACPI events). Distinguished from the AML
|
||||||
|
/// blob resources by its intact "FACP" header — the blobs are header-stripped.
|
||||||
|
var fadt_physical: u64 = 0;
|
||||||
|
var fadt_length: u64 = 0;
|
||||||
|
|
||||||
// AML blocks (DSDT + any SSDTs) collected during the table walk, as physical
|
// AML blocks (DSDT + any SSDTs) collected during the table walk, as physical
|
||||||
// address + length of each table's post-header bytecode. Scanned after the walk
|
// address + length of each table's post-header bytecode. The kernel does not
|
||||||
// for the sleep-state (`_Sx`) packages.
|
// interpret them — it publishes them on the acpi-tables node for the ring-3 acpi
|
||||||
|
// service to parse (device enumeration + soft-off). See publishAcpiTablesNode.
|
||||||
var aml_block_physical: [32]u64 = undefined;
|
var aml_block_physical: [32]u64 = undefined;
|
||||||
var aml_block_len: [32]usize = undefined;
|
var aml_block_len: [32]usize = undefined;
|
||||||
var aml_block_count: usize = 0;
|
var aml_block_count: usize = 0;
|
||||||
@@ -197,7 +196,8 @@ const ExtendedSystemDescriptorPointer = extern struct {
|
|||||||
root_system_description_table_address: u32 align(1),
|
root_system_description_table_address: u32 align(1),
|
||||||
/// The size of the RSDP.
|
/// The size of the RSDP.
|
||||||
length: u32 align(1),
|
length: u32 align(1),
|
||||||
/// A 64-bit physical address pointing to the XSDT. If the revision is at least 2, the XSDT should be used regardless of architecture, as the RSDT was deprecated.
|
/// A 64-bit physical address pointing to the XSDT. If the revision is at least 2, the XSDT
|
||||||
|
/// should be used regardless of architecture, as the RSDT was deprecated.
|
||||||
extended_system_descriptor_table_address: u64 align(1),
|
extended_system_descriptor_table_address: u64 align(1),
|
||||||
/// A checksum used for the entire table.
|
/// A checksum used for the entire table.
|
||||||
extended_checksum: u8,
|
extended_checksum: u8,
|
||||||
@@ -357,38 +357,20 @@ const Hpet = extern struct {
|
|||||||
page_protection: u8,
|
page_protection: u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- PCI configuration-space header (first 64 bytes, common fields) ---------
|
|
||||||
|
|
||||||
const PciHeader = extern struct {
|
|
||||||
vendor_id: u16 align(1),
|
|
||||||
device_id: u16 align(1),
|
|
||||||
command: u16 align(1),
|
|
||||||
status: u16 align(1),
|
|
||||||
revision_id: u8,
|
|
||||||
prog_if: u8,
|
|
||||||
subclass: u8,
|
|
||||||
class_code: u8,
|
|
||||||
cache_line_size: u8,
|
|
||||||
latency_timer: u8,
|
|
||||||
/// bit 7 set => multi-function device.
|
|
||||||
header_type: u8,
|
|
||||||
bist: u8,
|
|
||||||
// 0x10 onward (BARs, etc.) depends on header_type; read separately.
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Entry point ------------------------------------------------------------
|
// --- Entry point ------------------------------------------------------------
|
||||||
|
|
||||||
/// Discover hardware from the ACPI tables rooted at `rsdp_physical` and populate
|
/// Discover hardware from the ACPI tables rooted at `rsdp_physical` and populate
|
||||||
/// `device_tree`. `hal` provides MMIO mapping (for PCIe ECAM) and port I/O. Also parses the
|
/// `device_tree`. `hal` provides MMIO mapping (for PCIe ECAM) and port I/O. Also parses the
|
||||||
/// FADT and the AML sleep-state (`_Sx`) packages into `power_information` for the power service.
|
/// FADT into `power_information`, and publishes the AML blobs for the ring-3 acpi service.
|
||||||
pub fn discover(rsdp_physical: u64, device_tree: *DeviceTree, hal: Hal) !void {
|
pub fn discover(rsdp_physical: u64, memory_regions: []const boot_handoff.MemoryRegion, device_tree: *DeviceTree, hal: Hal) !void {
|
||||||
if (rsdp_physical == 0) return error.NoRsdp;
|
if (rsdp_physical == 0) return error.NoRsdp;
|
||||||
|
boot_memory_regions = memory_regions;
|
||||||
|
|
||||||
// Start clean so a re-run doesn't accumulate stale state.
|
// Start clean so a re-run doesn't accumulate stale state.
|
||||||
power_information = .{};
|
power_information = .{};
|
||||||
|
fadt_physical = 0;
|
||||||
|
fadt_length = 0;
|
||||||
platform_information = .{};
|
platform_information = .{};
|
||||||
aml_stats = .{};
|
|
||||||
namespace = null;
|
|
||||||
dsdt_physical = 0;
|
dsdt_physical = 0;
|
||||||
aml_block_count = 0;
|
aml_block_count = 0;
|
||||||
|
|
||||||
@@ -405,24 +387,50 @@ pub fn discover(rsdp_physical: u64, device_tree: *DeviceTree, hal: Hal) !void {
|
|||||||
try walkRoot(u32, rsdp.root_system_description_table_address, device_tree, hal);
|
try walkRoot(u32, rsdp.root_system_description_table_address, device_tree, hal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that the DSDT and any SSDTs are collected, build the AML namespace and
|
// The kernel does **not** interpret the DSDT/SSDTs. Static-table discovery
|
||||||
// read the sleep types from it.
|
// above (MADT/HPET/FADT/MCFG) is all the kernel needs — CPUs, timers, PCIe,
|
||||||
var blocks: [aml_block_physical.len][]const u8 = undefined;
|
// and the power register map. The AML bytecode (device enumeration and the
|
||||||
for (0..aml_block_count) |i| {
|
// sleep-state `_Sx` values for soft-off) is entirely the ring-3 acpi service's
|
||||||
blocks[i] = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(aml_block_physical[i])))[0..aml_block_len[i]];
|
// job: it claims the acpi-tables node published below, parses the same blobs,
|
||||||
|
// and both registers the `_HID` devices and owns S5. Not parsing ~0.5 MB of
|
||||||
|
// AML in the kernel keeps boot latency off the critical, single-core path.
|
||||||
|
|
||||||
|
// Publish the acpi-tables node (docs/discovery.md): the AML blobs as
|
||||||
|
// memory resources for the acpi service to map and parse in ring 3, a broad
|
||||||
|
// io_port grant for the OperationRegion access its interpreter needs, and
|
||||||
|
// the SCI for the events track (M21). Exactly one node, one trusted
|
||||||
|
// claimant — the sole path by which AML (devices + soft-off) reaches ring 3,
|
||||||
|
// now that the kernel keeps only the *static* tables for itself.
|
||||||
|
publishAcpiTablesNode(device_tree) catch {};
|
||||||
}
|
}
|
||||||
const active = blocks[0..aml_block_count];
|
|
||||||
if (aml.parse(device_tree.allocator, active)) |pr| {
|
/// Build the acpi-tables node (see the call site in discover). Best-effort: a
|
||||||
namespace = pr.namespace;
|
/// failure here leaves the kernel-seeded tree working, only the ring-3 service
|
||||||
aml_stats = .{ .nodes = namespace.?.nodeCount(), .consumed = pr.consumed, .total = pr.total };
|
/// finds nothing to claim.
|
||||||
power_information.s5 = aml.sleepState(&namespace.?, 5);
|
fn publishAcpiTablesNode(device_tree: *DeviceTree) !void {
|
||||||
power_information.s3 = aml.sleepState(&namespace.?, 3);
|
const node = try device_tree.addChild(device_tree.root, .acpi_tables, "acpi-tables");
|
||||||
// Fold the namespace's Device objects into the generic tree.
|
// One memory resource per AML block — page-aligned base down, length padded
|
||||||
wireAcpiDevices(device_tree, &namespace.?, hal) catch {};
|
// up to cover the bytecode, so mmio_map hands the service a pointer into it.
|
||||||
} else |_| {
|
var i: usize = 0;
|
||||||
// AML parse failed (e.g. out of memory); power stays best-effort with
|
while (i < aml_block_count and i < device_model.maximum_resources - 2) : (i += 1) {
|
||||||
// whatever the FADT alone provided.
|
// mmio_map preserves the sub-page offset, so the service maps this and
|
||||||
|
// gets a pointer straight to the bytecode.
|
||||||
|
_ = node.addResource(.memory, aml_block_physical[i], aml_block_len[i]);
|
||||||
}
|
}
|
||||||
|
// The broad I/O grant: OperationRegions name whatever ports the firmware
|
||||||
|
// chose (EC, PM1, GPE, SMBus); which ports cannot be known before the AML
|
||||||
|
// that names them is parsed, so the grant is the whole space — the honest
|
||||||
|
// trust boundary of docs/discovery.md (the acpi service's one trusted node).
|
||||||
|
_ = node.addResource(.io_port, 0, 1 << 16);
|
||||||
|
// A broad interrupt window: ACPI _CRS names legacy ISA IRQs (the PS/2 lines
|
||||||
|
// 1 and 12, the RTC, …), and the service registers those devices under this
|
||||||
|
// node, so it must own a superset. The range [0, 256) covers every GSI; the
|
||||||
|
// SCI (recorded first, len 1) stays distinct so M21 can pick it out.
|
||||||
|
if (power_information.sci_interrupt != 0) _ = node.addResource(.irq, power_information.sci_interrupt, 1);
|
||||||
|
_ = node.addResource(.irq, 0, 256);
|
||||||
|
// The FADT rides along (M21): the service reads the PM1 event / GPE blocks
|
||||||
|
// from its own copy, telling it apart from the AML blobs by signature.
|
||||||
|
if (fadt_physical != 0) _ = node.addResource(.memory, fadt_physical, fadt_length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Walk the RSDT (Entry = u32) or XSDT (Entry = u64): validate it, then dispatch
|
/// Walk the RSDT (Entry = u32) or XSDT (Entry = u64): validate it, then dispatch
|
||||||
@@ -448,17 +456,19 @@ fn handleTable(device_tree: *DeviceTree, hal: Hal, sdt_physical: u64) !void {
|
|||||||
if (std.mem.eql(u8, &sig, &APIC)) {
|
if (std.mem.eql(u8, &sig, &APIC)) {
|
||||||
try parseMadt(device_tree, header);
|
try parseMadt(device_tree, header);
|
||||||
} else if (std.mem.eql(u8, &sig, &MCFG)) {
|
} else if (std.mem.eql(u8, &sig, &MCFG)) {
|
||||||
try parseMcfg(device_tree, hal, header);
|
try parseMcfg(device_tree, header);
|
||||||
} else if (std.mem.eql(u8, &sig, &HPET)) {
|
} else if (std.mem.eql(u8, &sig, &HPET)) {
|
||||||
try parseHpet(device_tree, hal, header);
|
try parseHpet(device_tree, hal, header);
|
||||||
} else if (std.mem.eql(u8, &sig, &FACP)) {
|
} else if (std.mem.eql(u8, &sig, &FACP)) {
|
||||||
|
fadt_physical = sdt_physical;
|
||||||
|
fadt_length = header.length;
|
||||||
parseFadt(header);
|
parseFadt(header);
|
||||||
} else if (std.mem.eql(u8, &sig, &SPCR)) {
|
} else if (std.mem.eql(u8, &sig, &SPCR)) {
|
||||||
parseSpcr(header);
|
parseSpcr(header);
|
||||||
} else if (std.mem.eql(u8, &sig, &DMAR)) {
|
} else if (std.mem.eql(u8, &sig, &DMAR)) {
|
||||||
parseDmar(hal, header);
|
parseDmar(hal, header);
|
||||||
} else if (std.mem.eql(u8, &sig, &SSDT)) {
|
} else if (std.mem.eql(u8, &sig, &SSDT)) {
|
||||||
// Secondary namespace bytecode — collect for the sleep-state (`_Sx`) scan.
|
// Secondary namespace bytecode — collect it to publish for the ring-3 parse.
|
||||||
addAmlBlock(sdt_physical);
|
addAmlBlock(sdt_physical);
|
||||||
}
|
}
|
||||||
// Any other signature is recognised but left opaque for now.
|
// Any other signature is recognised but left opaque for now.
|
||||||
@@ -533,7 +543,7 @@ fn parseMadt(device_tree: *DeviceTree, header: *const SystemDescriptorTableHeade
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// MCFG -> a pci_host_bridge per ECAM segment, then a PCI enumeration underneath.
|
/// MCFG -> a pci_host_bridge per ECAM segment, then a PCI enumeration underneath.
|
||||||
fn parseMcfg(device_tree: *DeviceTree, hal: Hal, header: *const SystemDescriptorTableHeader) !void {
|
fn parseMcfg(device_tree: *DeviceTree, header: *const SystemDescriptorTableHeader) !void {
|
||||||
const total: usize = header.length;
|
const total: usize = header.length;
|
||||||
const base: [*]const u8 = @ptrCast(header);
|
const base: [*]const u8 = @ptrCast(header);
|
||||||
|
|
||||||
@@ -548,109 +558,85 @@ fn parseMcfg(device_tree: *DeviceTree, hal: Hal, header: *const SystemDescriptor
|
|||||||
// ECAM window: 1 MiB of configuration space per bus.
|
// ECAM window: 1 MiB of configuration space per bus.
|
||||||
_ = bridge.addResource(.memory, alloc.base_address, bus_count << 20);
|
_ = bridge.addResource(.memory, alloc.base_address, bus_count << 20);
|
||||||
_ = bridge.addResource(.bus_range, alloc.start_bus, bus_count);
|
_ = bridge.addResource(.bus_range, alloc.start_bus, bus_count);
|
||||||
|
addBridgeApertures(bridge);
|
||||||
|
// The bridge decodes the whole 16-bit I/O space toward its bus — the
|
||||||
|
// window functions' I/O BARs must register-contain within (M19.2).
|
||||||
|
_ = bridge.addResource(.io_port, 0, 1 << 16);
|
||||||
|
|
||||||
try enumeratePci(device_tree, bridge, hal, alloc.*);
|
// The function walk itself retired to ring 3 (M19.3): the pci-bus
|
||||||
|
// driver claims this bridge, repeats the scan through its ECAM grant,
|
||||||
|
// and device_registers what it finds — the kernel seeds only the
|
||||||
|
// bridge. The scan's equivalence was proven before the hand-off
|
||||||
|
// (pci-scan), and the walk's history is in git if archaeology calls.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Brute-force scan the ECAM window's bus range for present PCI functions. No
|
/// The boot memory map, stored at discover() entry for the aperture derivation
|
||||||
/// bridge recursion yet: on the ECAM path the host bridge decodes every bus in
|
/// below (and, in M20, for the acpi-tables node's containment windows).
|
||||||
/// the window, so scanning the declared range finds everything QEMU exposes.
|
var boot_memory_regions: []const boot_handoff.MemoryRegion = &.{};
|
||||||
fn enumeratePci(
|
|
||||||
device_tree: *DeviceTree,
|
|
||||||
bridge: *device_model.Device,
|
|
||||||
hal: Hal,
|
|
||||||
alloc: McfgAllocation,
|
|
||||||
) !void {
|
|
||||||
var bus: u16 = alloc.start_bus;
|
|
||||||
while (bus <= alloc.end_bus) : (bus += 1) {
|
|
||||||
var device: u8 = 0;
|
|
||||||
while (device < 32) : (device += 1) {
|
|
||||||
const h0: *align(1) const PciHeader = @ptrCast(pciConfigurationPtr(alloc, hal, @intCast(bus), device, 0));
|
|
||||||
if (h0.vendor_id == 0xFFFF) continue; // no function 0 => slot empty
|
|
||||||
|
|
||||||
const funcs: u8 = if (h0.header_type & 0x80 != 0) 8 else 1;
|
/// The bridge's MMIO apertures, derived from the boot memory map's holes
|
||||||
var function: u8 = 0;
|
/// (docs/discovery.md — apertures from the memory map): registered PCI functions carry BAR
|
||||||
while (function < funcs) : (function += 1) {
|
/// resources, and `device_register` containment demands the bridge own windows
|
||||||
const configuration = pciConfigurationPtr(alloc, hal, @intCast(bus), device, function);
|
/// that cover them. Everything the firmware described is "not hole"; the low
|
||||||
const h: *align(1) const PciHeader = @ptrCast(configuration);
|
/// aperture runs from the end of the described space below 4 GiB up to the
|
||||||
if (h.vendor_id == 0xFFFF) continue;
|
/// I/O-APIC region, the high one from 4 GiB (or the end of RAM above it) to
|
||||||
|
/// the 46-bit line. Coarse, mechanical, and AML-free — available at boot no
|
||||||
var nb: [24]u8 = undefined;
|
/// matter what later moved to user space.
|
||||||
const nm = std.fmt.bufPrint(&nb, "{s}:{x:0>2}:{x:0>2}.{d}", .{
|
fn addBridgeApertures(bridge: *device_model.Device) void {
|
||||||
bridge.name(), bus, device, function,
|
// Below 4 GiB the described regions are sparse (RAM low, firmware flash
|
||||||
}) catch "pcidev";
|
// and tables high), so the holes are the *gaps between* them — a single
|
||||||
const node = try device_tree.addChild(bridge, .pci_device, nm);
|
// "after the last region" rule dies on OVMF's flash at the very top.
|
||||||
// Resource 0 is the function's own 4 KiB ECAM configuration space. A
|
// Sort-merge the described ranges, then keep the three largest gaps
|
||||||
// claimed PCI driver mmio_maps this to reach its command register,
|
// (resource slots are bounded at 8 per device; ECAM + bus range + 3 + the
|
||||||
// BARs, and — the point — its capability list (MSI/MSI-X, PCIe
|
// high aperture fits). Above 4 GiB one aperture runs from the end of the
|
||||||
// extended caps), without any new syscall. Physical address per the
|
// described space to the 46-bit line.
|
||||||
// ECAM formula (same as pciConfigurationPtr).
|
const Range = struct { base: u64, end: u64 };
|
||||||
const config_physical = alloc.base_address +
|
var below: [64]Range = undefined;
|
||||||
(@as(u64, @as(u8, @intCast(bus)) - alloc.start_bus) << 20) +
|
var below_count: usize = 0;
|
||||||
(@as(u64, device) << 15) + (@as(u64, function) << 12);
|
var high_end: u64 = 1 << 32;
|
||||||
_ = node.addResource(.memory, config_physical, abi.page_size);
|
for (boot_memory_regions) |region| {
|
||||||
node.ids.pci_vendor = h.vendor_id;
|
const end = region.base + region.pages * 4096;
|
||||||
node.ids.pci_device = h.device_id;
|
// Above 4 GiB only *usable RAM* blocks the aperture: OVMF describes
|
||||||
node.ids.pci_class = (@as(u24, h.class_code) << 16) |
|
// its own 64-bit PCI window as a reserved region and then programs
|
||||||
(@as(u24, h.subclass) << 8) | h.prog_if;
|
// BARs inside it — honoring reserved there would exclude the very
|
||||||
node.ids.pci_bdf = (@as(u16, @intCast(bus)) << 8) | (@as(u16, device) << 3) | function;
|
// space BARs live in. Below 4 GiB every described region blocks (the
|
||||||
|
// kernel image, the tables, the ramdisk all live there). Bring-up
|
||||||
// BARs only exist in header type 0 (normal devices), not bridges.
|
// trust: only the bridge's claimant can register into the aperture.
|
||||||
if (h.header_type & 0x7F == 0) addBars(node, configuration);
|
if (region.kind == .usable and end > high_end) high_end = end;
|
||||||
|
if (region.base >= (1 << 32) or below_count == below.len) continue;
|
||||||
|
below[below_count] = .{ .base = region.base, .end = @min(end, 1 << 32) };
|
||||||
|
below_count += 1;
|
||||||
|
}
|
||||||
|
// Insertion sort by base (the map is small and this runs once at boot).
|
||||||
|
for (1..below_count) |i| {
|
||||||
|
const key = below[i];
|
||||||
|
var j = i;
|
||||||
|
while (j > 0 and below[j - 1].base > key.base) : (j -= 1) below[j] = below[j - 1];
|
||||||
|
below[j] = key;
|
||||||
|
}
|
||||||
|
// Walk the sorted ranges, collecting inter-region gaps of at least 1 MiB.
|
||||||
|
var gaps: [3]Range = .{Range{ .base = 0, .end = 0 }} ** 3;
|
||||||
|
var cursor: u64 = 0;
|
||||||
|
var index: usize = 0;
|
||||||
|
while (index <= below_count) : (index += 1) {
|
||||||
|
const gap_end = if (index == below_count) (1 << 32) else below[index].base;
|
||||||
|
if (gap_end > cursor and gap_end - cursor >= (1 << 20)) {
|
||||||
|
// Keep the three largest, replacing the smallest kept so far.
|
||||||
|
var smallest: usize = 0;
|
||||||
|
for (gaps, 0..) |gap, gi| {
|
||||||
|
if (gap.end - gap.base < gaps[smallest].end - gaps[smallest].base) smallest = gi;
|
||||||
|
}
|
||||||
|
if (gap_end - cursor > gaps[smallest].end - gaps[smallest].base) {
|
||||||
|
gaps[smallest] = .{ .base = cursor, .end = gap_end };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (index < below_count and below[index].end > cursor) cursor = below[index].end;
|
||||||
}
|
}
|
||||||
|
for (gaps) |gap| {
|
||||||
|
if (gap.end > gap.base) _ = bridge.addResource(.memory, gap.base, gap.end - gap.base);
|
||||||
}
|
}
|
||||||
|
_ = bridge.addResource(.memory, high_end, (@as(u64, 1) << 46) - high_end);
|
||||||
/// Record and size the memory/IO windows named by a device's Base Address
|
|
||||||
/// Registers. Sizing is the standard probe: disable decode, write all-ones, read
|
|
||||||
/// back the writable (address) bits, restore. `size = ~mask + 1`.
|
|
||||||
fn addBars(node: *device_model.Device, configuration: [*]align(1) u8) void {
|
|
||||||
// Stop the device decoding its BARs while we transiently write all-ones.
|
|
||||||
const command = rd(u16, configuration, 0x04);
|
|
||||||
wr(u16, configuration, 0x04, command & ~@as(u16, 0b11));
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < 6) : (i += 1) {
|
|
||||||
const off = 0x10 + i * 4;
|
|
||||||
const orig = rd(u32, configuration, off);
|
|
||||||
if (orig == 0) continue;
|
|
||||||
|
|
||||||
if (orig & 1 != 0) {
|
|
||||||
// I/O-space BAR (16-bit address space on x86).
|
|
||||||
wr(u32, configuration, off, 0xFFFF_FFFF);
|
|
||||||
const readback = rd(u32, configuration, off);
|
|
||||||
wr(u32, configuration, off, orig);
|
|
||||||
const mask = readback & 0xFFFF_FFFC;
|
|
||||||
const size: u32 = if (mask == 0) 0 else (~mask +% 1) & 0xFFFF;
|
|
||||||
_ = node.addResource(.io_port, orig & 0xFFFF_FFFC, size);
|
|
||||||
} else if ((orig >> 1) & 0x3 == 2) {
|
|
||||||
// 64-bit memory BAR: this BAR pair spans two configuration slots.
|
|
||||||
const orig_hi = rd(u32, configuration, off + 4);
|
|
||||||
wr(u32, configuration, off, 0xFFFF_FFFF);
|
|
||||||
wr(u32, configuration, off + 4, 0xFFFF_FFFF);
|
|
||||||
const lo = rd(u32, configuration, off);
|
|
||||||
const hi = rd(u32, configuration, off + 4);
|
|
||||||
wr(u32, configuration, off, orig);
|
|
||||||
wr(u32, configuration, off + 4, orig_hi);
|
|
||||||
const readback = (@as(u64, hi) << 32) | (lo & 0xFFFF_FFF0);
|
|
||||||
const size: u64 = if (readback == 0) 0 else ~readback +% 1;
|
|
||||||
const address = (@as(u64, orig_hi) << 32) | (orig & 0xFFFF_FFF0);
|
|
||||||
_ = node.addResource(.memory, address, size);
|
|
||||||
i += 1; // consumed the high half
|
|
||||||
} else {
|
|
||||||
// 32-bit memory BAR.
|
|
||||||
wr(u32, configuration, off, 0xFFFF_FFFF);
|
|
||||||
const readback = rd(u32, configuration, off);
|
|
||||||
wr(u32, configuration, off, orig);
|
|
||||||
const mask = readback & 0xFFFF_FFF0;
|
|
||||||
const size: u32 = if (mask == 0) 0 else ~mask +% 1;
|
|
||||||
_ = node.addResource(.memory, orig & 0xFFFF_FFF0, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wr(u16, configuration, 0x04, command); // restore decode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// HPET -> a timer node with its register block as an MMIO resource, plus the GSI
|
/// HPET -> a timer node with its register block as an MMIO resource, plus the GSI
|
||||||
@@ -709,6 +695,7 @@ const fadt_pm1a_cnt_blk = 64; // u32 (I/O port)
|
|||||||
const fadt_pm1b_cnt_blk = 68; // u32 (I/O port)
|
const fadt_pm1b_cnt_blk = 68; // u32 (I/O port)
|
||||||
const fadt_pm_tmr_blk = 76; // u32 (I/O port) — the PM timer counter
|
const fadt_pm_tmr_blk = 76; // u32 (I/O port) — the PM timer counter
|
||||||
const fadt_pm1_cnt_len = 89; // u8 (bytes)
|
const fadt_pm1_cnt_len = 89; // u8 (bytes)
|
||||||
|
const fadt_sci_int = 46; // u16 (the SCI's GSI)
|
||||||
const fadt_flags = 112; // u32
|
const fadt_flags = 112; // u32
|
||||||
const fadt_reset_register = 116; // GAS (12 bytes)
|
const fadt_reset_register = 116; // GAS (12 bytes)
|
||||||
const fadt_reset_value = 128; // u8
|
const fadt_reset_value = 128; // u8
|
||||||
@@ -719,13 +706,14 @@ const fadt_x_pm_tmr_blk = 208; // GAS
|
|||||||
const flag_reset_register_supported = 1 << 10;
|
const flag_reset_register_supported = 1 << 10;
|
||||||
const flag_tmr_value_ext = 1 << 8; // PM timer counter is 32-bit (else 24-bit)
|
const flag_tmr_value_ext = 1 << 8; // PM timer counter is 32-bit (else 24-bit)
|
||||||
|
|
||||||
/// FADT -> the power register map (into `power_information`) and the DSDT address, which
|
/// FADT -> the power register map (into `power_information`) and the DSDT address,
|
||||||
/// is queued for the AML sleep-state (`_Sx`) scan. No AML interpretation happens here.
|
/// whose bytecode is collected for the ring-3 parse. No AML interpretation here.
|
||||||
fn parseFadt(header: *const SystemDescriptorTableHeader) void {
|
fn parseFadt(header: *const SystemDescriptorTableHeader) void {
|
||||||
const base: [*]align(1) const u8 = @ptrCast(header);
|
const base: [*]align(1) const u8 = @ptrCast(header);
|
||||||
const len: usize = header.length;
|
const len: usize = header.length;
|
||||||
const pi = &power_information;
|
const pi = &power_information;
|
||||||
|
|
||||||
|
pi.sci_interrupt = @truncate(fadt(u16, base, len, fadt_sci_int) orelse 0);
|
||||||
pi.smi_cmd = @truncate(fadt(u32, base, len, fadt_smi_cmd) orelse 0);
|
pi.smi_cmd = @truncate(fadt(u32, base, len, fadt_smi_cmd) orelse 0);
|
||||||
pi.acpi_enable = fadt(u8, base, len, fadt_acpi_enable) orelse 0;
|
pi.acpi_enable = fadt(u8, base, len, fadt_acpi_enable) orelse 0;
|
||||||
pi.acpi_disable = fadt(u8, base, len, fadt_acpi_disable) orelse 0;
|
pi.acpi_disable = fadt(u8, base, len, fadt_acpi_disable) orelse 0;
|
||||||
@@ -805,337 +793,6 @@ fn parseDmar(hal: Hal, header: *const SystemDescriptorTableHeader) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- AML namespace -> generic device tree -----------------------------------
|
|
||||||
|
|
||||||
/// The PCI bus context while descending the ACPI namespace: the generic host
|
|
||||||
/// bridge whose children ACPI address (`_ADR`) devices resolve against, and the bus number.
|
|
||||||
const PciContext = struct { bridge: *device_model.Device, bus: u8 };
|
|
||||||
|
|
||||||
/// Mirror the ACPI namespace's Device objects into the generic tree, *merging*
|
|
||||||
/// them with the PCI-enumerated nodes: a PCI root bridge (`PNP0A03`/`PNP0A08`)
|
|
||||||
/// folds onto the existing `pci_host_bridge`, and each addressed (`_ADR`) device folds onto
|
|
||||||
/// the matching PCI function (annotating it with the ACPI hardware ID (`_HID`) and nesting the
|
|
||||||
/// ACPI-only children — keyboard, RTC, … — beneath it). Namespace devices with no
|
|
||||||
/// PCI match land under a synthetic `acpi` node.
|
|
||||||
fn wireAcpiDevices(device_tree: *DeviceTree, aml_namespace: *aml.Namespace, hal: Hal) !void {
|
|
||||||
var arena = std.heap.ArenaAllocator.init(device_tree.allocator);
|
|
||||||
defer arena.deinit();
|
|
||||||
var interpreter = aml.Interpreter.init(aml_namespace, .{
|
|
||||||
.mapMmio = hal.mapMmio,
|
|
||||||
.pioRead = hal.pioRead,
|
|
||||||
.pioWrite = hal.pioWrite,
|
|
||||||
}, arena.allocator());
|
|
||||||
|
|
||||||
const acpi_root = try device_tree.addChild(device_tree.root, .unknown, "acpi");
|
|
||||||
try mirrorDevices(device_tree, aml_namespace.root, acpi_root, null, &interpreter);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mirrorDevices(device_tree: *DeviceTree, node: *aml.Node, parent_device: *device_model.Device, context: ?PciContext, interpreter: *aml.Interpreter) (error{OutOfMemory})!void {
|
|
||||||
var child = node.first_child;
|
|
||||||
while (child) |c| : (child = c.next_sibling) {
|
|
||||||
if (c.kind != .device) {
|
|
||||||
// A scope — the System Bus (\_SB), General Purpose Events (\_GPE), … —
|
|
||||||
// descend without adding a node.
|
|
||||||
try mirrorDevices(device_tree, c, parent_device, context, interpreter);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip devices the firmware reports as not present (via a device-status (`_STA`) method),
|
|
||||||
// along with their whole subtree — per the ACPI rules.
|
|
||||||
if (!devicePresent(interpreter, c)) continue;
|
|
||||||
|
|
||||||
var mirrored_device: *device_model.Device = undefined;
|
|
||||||
var child_context = context;
|
|
||||||
|
|
||||||
if (isPciRootNode(c)) {
|
|
||||||
// The PCI root bridge folds onto the generic host bridge.
|
|
||||||
mirrored_device = matchHostBridge(device_tree) orelse
|
|
||||||
try device_tree.addChild(parent_device, .acpi_device, &c.segment);
|
|
||||||
child_context = .{ .bridge = mirrored_device, .bus = 0 };
|
|
||||||
} else {
|
|
||||||
// An addressed device folds onto its matching PCI function; anything
|
|
||||||
// else becomes a fresh node under the current parent.
|
|
||||||
mirrored_device = pick: {
|
|
||||||
if (context) |pc| {
|
|
||||||
if (readAdr(c)) |adr| {
|
|
||||||
if (findPciNode(pc.bridge, pc.bus, adr)) |pnode| break :pick pnode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break :pick try device_tree.addChild(parent_device, .acpi_device, &c.segment);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
applyHid(mirrored_device, c, interpreter);
|
|
||||||
applyCrs(mirrored_device, c, interpreter);
|
|
||||||
try mirrorDevices(device_tree, c, mirrored_device, child_context, interpreter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Evaluate a device's status (`_STA`) to decide if it is present. An absent status
|
|
||||||
/// (`_STA`) means present by default; an evaluation failure is treated as present too (we'd
|
|
||||||
/// rather over-report than hide a device we couldn't introspect).
|
|
||||||
fn devicePresent(interpreter: *aml.Interpreter, node: *aml.Node) bool {
|
|
||||||
const sta = aml.Namespace.childOf(node, seg4("_STA")) orelse return true;
|
|
||||||
const obj = interpreter.evaluate(sta, &.{}) catch return true;
|
|
||||||
const status = obj.asInteger() catch return true;
|
|
||||||
return (status & 0x01) != 0; // bit 0 = present
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The first PCI host bridge in the generic tree (segment 0).
|
|
||||||
fn matchHostBridge(device_tree: *DeviceTree) ?*device_model.Device {
|
|
||||||
var c = device_tree.root.first_child;
|
|
||||||
while (c) |ch| : (c = ch.next_sibling) {
|
|
||||||
if (ch.class == .pci_host_bridge) return ch;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The PCI function node under `bridge` at the address the device's address object
|
|
||||||
/// (`_ADR`) names (device/function on
|
|
||||||
/// `bus`), or null.
|
|
||||||
fn findPciNode(bridge: *device_model.Device, bus: u8, adr: u32) ?*device_model.Device {
|
|
||||||
const device: u16 = @truncate((adr >> 16) & 0x1F);
|
|
||||||
const function: u16 = @truncate(adr & 0x7);
|
|
||||||
const target: u16 = (@as(u16, bus) << 8) | (device << 3) | function;
|
|
||||||
var c = bridge.first_child;
|
|
||||||
while (c) |ch| : (c = ch.next_sibling) {
|
|
||||||
if (ch.ids.pci_bdf) |bdf| {
|
|
||||||
if (bdf == target) return ch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A device's address (`_ADR`) — a static integer Name — or null.
|
|
||||||
fn readAdr(node: *aml.Node) ?u32 {
|
|
||||||
const n = aml.Namespace.childOf(node, seg4("_ADR")) orelse return null;
|
|
||||||
if (n.kind != .name) return null;
|
|
||||||
var p: usize = 0;
|
|
||||||
return @truncate(readIntObj(n.value, &p) orelse return null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether a namespace device is a PCI(e) host bridge (`PNP0A03` / `PNP0A08`).
|
|
||||||
fn isPciRootNode(node: *aml.Node) bool {
|
|
||||||
const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return false;
|
|
||||||
if (hid.kind != .name or hid.value.len == 0) return false;
|
|
||||||
switch (hid.value[0]) {
|
|
||||||
0x00, 0x01, 0xFF, 0x0A, 0x0B, 0x0C, 0x0E => {
|
|
||||||
var p: usize = 0;
|
|
||||||
const n = readIntObj(hid.value, &p) orelse return false;
|
|
||||||
return n == 0x030AD041 or n == 0x080AD041; // PNP0A03 / PNP0A08
|
|
||||||
},
|
|
||||||
0x0D => {
|
|
||||||
const s = cstr(hid.value[1..]);
|
|
||||||
return std.mem.eql(u8, s, "PNP0A03") or std.mem.eql(u8, s, "PNP0A08");
|
|
||||||
},
|
|
||||||
else => return false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a device's hardware ID (`_HID`) into the generic device: an integer decodes as an EISA
|
|
||||||
/// id ("PNP0A03"), a string is taken verbatim. Handles both the common static
|
|
||||||
/// Name form and a Method form (evaluated).
|
|
||||||
fn applyHid(device: *device_model.Device, node: *aml.Node, interpreter: *aml.Interpreter) void {
|
|
||||||
const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return;
|
|
||||||
if (hid.kind == .method) {
|
|
||||||
const obj = interpreter.evaluate(hid, &.{}) catch return;
|
|
||||||
switch (obj) {
|
|
||||||
.integer => |n| setEisaHid(device, @truncate(n)),
|
|
||||||
.string => |s| device.setHid(s),
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (hid.kind != .name or hid.value.len == 0) return;
|
|
||||||
const v = hid.value;
|
|
||||||
switch (v[0]) {
|
|
||||||
0x00, 0x01, 0xFF, 0x0A, 0x0B, 0x0C, 0x0E => {
|
|
||||||
var p: usize = 0;
|
|
||||||
const n = readIntObj(v, &p) orelse return;
|
|
||||||
setEisaHid(device, @truncate(n));
|
|
||||||
},
|
|
||||||
0x0D => device.setHid(cstr(v[1..])), // StringPrefix
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setEisaHid(device: *device_model.Device, id: u32) void {
|
|
||||||
device.ids.acpi_hid = id;
|
|
||||||
var buffer: [8]u8 = undefined;
|
|
||||||
device.setHid(eisaIdToStr(id, &buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a device's current resource settings (`_CRS`). The evaluator handles both the static
|
|
||||||
/// `Buffer` form (a `Name`) and the method form uniformly, yielding the
|
|
||||||
/// ResourceTemplate bytes we then decode.
|
|
||||||
fn applyCrs(device: *device_model.Device, node: *aml.Node, interpreter: *aml.Interpreter) void {
|
|
||||||
const crs = aml.Namespace.childOf(node, seg4("_CRS")) orelse return;
|
|
||||||
const obj = interpreter.evaluate(crs, &.{}) catch return;
|
|
||||||
const buffer = switch (obj) {
|
|
||||||
.buffer => |b| b,
|
|
||||||
else => return,
|
|
||||||
};
|
|
||||||
parseResourceTemplate(device, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Walk a ResourceTemplate byte list, adding recognised descriptors as resources.
|
|
||||||
fn parseResourceTemplate(device: *device_model.Device, bytes: []const u8) void {
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < bytes.len) {
|
|
||||||
const tag = bytes[i];
|
|
||||||
if (tag & 0x80 == 0) {
|
|
||||||
// Small descriptor: length in low 3 bits, type in bits [6:3].
|
|
||||||
const len: usize = tag & 0x07;
|
|
||||||
const body = i + 1;
|
|
||||||
if (body + len > bytes.len) break;
|
|
||||||
switch ((tag >> 3) & 0x0F) {
|
|
||||||
0x04 => if (len >= 2) { // IRQ: a 16-bit mask, one resource per set bit
|
|
||||||
const mask = @as(u16, bytes[body]) | (@as(u16, bytes[body + 1]) << 8);
|
|
||||||
var b: usize = 0;
|
|
||||||
while (b < 16) : (b += 1) {
|
|
||||||
if (mask & (@as(u16, 1) << @intCast(b)) != 0) _ = device.addResource(.irq, b, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
0x08 => if (len >= 7) { // IO port: minimum at +1, length at +6
|
|
||||||
_ = device.addResource(.io_port, rd16(bytes, body + 1), bytes[body + 6]);
|
|
||||||
},
|
|
||||||
0x09 => if (len >= 3) { // Fixed IO: base at +0, length at +2
|
|
||||||
_ = device.addResource(.io_port, rd16(bytes, body), bytes[body + 2]);
|
|
||||||
},
|
|
||||||
0x0F => break, // EndTag
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
i = body + len;
|
|
||||||
} else {
|
|
||||||
// Large descriptor: 16-bit length follows the tag.
|
|
||||||
if (i + 3 > bytes.len) break;
|
|
||||||
const len: usize = @intCast(rd16(bytes, i + 1));
|
|
||||||
const body = i + 3;
|
|
||||||
if (body + len > bytes.len) break;
|
|
||||||
switch (tag) {
|
|
||||||
0x85 => if (len >= 17) { // Memory32: minimum at +1, length at +13
|
|
||||||
_ = device.addResource(.memory, rd32(bytes, body + 1), rd32(bytes, body + 13));
|
|
||||||
},
|
|
||||||
0x86 => if (len >= 9) { // Memory32Fixed: base at +1, length at +5
|
|
||||||
_ = device.addResource(.memory, rd32(bytes, body + 1), rd32(bytes, body + 5));
|
|
||||||
},
|
|
||||||
0x89 => if (len >= 2) { // Extended IRQ: count at +1, then count u32s
|
|
||||||
const count = bytes[body + 1];
|
|
||||||
var k: usize = 0;
|
|
||||||
while (k < count and body + 2 + k * 4 + 4 <= body + len) : (k += 1) {
|
|
||||||
_ = device.addResource(.irq, rd32(bytes, body + 2 + k * 4), 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
0x87, 0x88, 0x8A => parseAddressSpace(device, tag, bytes[body .. body + len]),
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
i = body + len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Word/DWord/QWord address-space descriptors: resource type at [0], then
|
|
||||||
/// granularity/minimum/maximum/translation/length, each of width `w`.
|
|
||||||
fn parseAddressSpace(device: *device_model.Device, tag: u8, body: []const u8) void {
|
|
||||||
const w: usize = switch (tag) {
|
|
||||||
0x88 => 2, // Word
|
|
||||||
0x87 => 4, // DWord
|
|
||||||
else => 8, // QWord (0x8A)
|
|
||||||
};
|
|
||||||
if (body.len < 3 + 5 * w) return;
|
|
||||||
const minimum = readN(body, 3 + w, w);
|
|
||||||
const length = readN(body, 3 + 4 * w, w);
|
|
||||||
const kind: device_model.ResourceKind = switch (body[0]) {
|
|
||||||
0 => .memory,
|
|
||||||
1 => .io_port,
|
|
||||||
else => .bus_range,
|
|
||||||
};
|
|
||||||
_ = device.addResource(kind, minimum, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decode a packed EISA id into its 7-char string (e.g. 0x030AD041 -> "PNP0A03").
|
|
||||||
fn eisaIdToStr(id: u32, buffer: *[8]u8) []const u8 {
|
|
||||||
const b0: u16 = @intCast(id & 0xFF);
|
|
||||||
const b1: u16 = @intCast((id >> 8) & 0xFF);
|
|
||||||
const b2: u8 = @truncate(id >> 16);
|
|
||||||
const b3: u8 = @truncate(id >> 24);
|
|
||||||
const mfg = (b0 << 8) | b1;
|
|
||||||
buffer[0] = '@' + @as(u8, @intCast((mfg >> 10) & 0x1F));
|
|
||||||
buffer[1] = '@' + @as(u8, @intCast((mfg >> 5) & 0x1F));
|
|
||||||
buffer[2] = '@' + @as(u8, @intCast(mfg & 0x1F));
|
|
||||||
buffer[3] = hexDigit((b2 >> 4) & 0xF);
|
|
||||||
buffer[4] = hexDigit(b2 & 0xF);
|
|
||||||
buffer[5] = hexDigit((b3 >> 4) & 0xF);
|
|
||||||
buffer[6] = hexDigit(b3 & 0xF);
|
|
||||||
return buffer[0..7];
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hexDigit(n: u8) u8 {
|
|
||||||
return if (n < 10) '0' + n else 'A' + (n - 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn seg4(comptime s: *const [4:0]u8) [4]u8 {
|
|
||||||
return s[0..4].*;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cstr(bytes: []const u8) []const u8 {
|
|
||||||
const index = std.mem.indexOfScalar(u8, bytes, 0) orelse bytes.len;
|
|
||||||
return bytes[0..index];
|
|
||||||
}
|
|
||||||
|
|
||||||
const PkgLen = struct { value: usize, size: usize };
|
|
||||||
|
|
||||||
fn packageLength(bytes: []const u8, p: usize) ?PkgLen {
|
|
||||||
if (p >= bytes.len) return null;
|
|
||||||
const lead = bytes[p];
|
|
||||||
const follow: usize = lead >> 6;
|
|
||||||
if (p + 1 + follow > bytes.len) return null;
|
|
||||||
if (follow == 0) return .{ .value = lead & 0x3F, .size = 1 };
|
|
||||||
var value: usize = lead & 0x0F;
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < follow) : (i += 1) value |= @as(usize, bytes[p + 1 + i]) << @intCast(4 + i * 8);
|
|
||||||
return .{ .value = value, .size = 1 + follow };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read an AML integer object at `p`, advancing `p` past it.
|
|
||||||
fn readIntObj(bytes: []const u8, p: *usize) ?u64 {
|
|
||||||
if (p.* >= bytes.len) return null;
|
|
||||||
const opcode = bytes[p.*];
|
|
||||||
p.* += 1;
|
|
||||||
return switch (opcode) {
|
|
||||||
0x00 => 0,
|
|
||||||
0x01 => 1,
|
|
||||||
0xFF => 0xFF,
|
|
||||||
0x0A => readLE(bytes, p, 1),
|
|
||||||
0x0B => readLE(bytes, p, 2),
|
|
||||||
0x0C => readLE(bytes, p, 4),
|
|
||||||
0x0E => readLE(bytes, p, 8),
|
|
||||||
else => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn readLE(bytes: []const u8, p: *usize, n: usize) ?u64 {
|
|
||||||
if (p.* + n > bytes.len) return null;
|
|
||||||
const v = readN(bytes, p.*, n);
|
|
||||||
p.* += n;
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn readN(bytes: []const u8, off: usize, n: usize) u64 {
|
|
||||||
var v: u64 = 0;
|
|
||||||
var k: usize = 0;
|
|
||||||
while (k < n and off + k < bytes.len) : (k += 1) v |= @as(u64, bytes[off + k]) << @intCast(k * 8);
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rd16(bytes: []const u8, off: usize) u64 {
|
|
||||||
return readN(bytes, off, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rd32(bytes: []const u8, off: usize) u64 {
|
|
||||||
return readN(bytes, off, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- helpers ----------------------------------------------------------------
|
// --- helpers ----------------------------------------------------------------
|
||||||
|
|
||||||
/// Sum `len` bytes; an ACPI table/pointer is valid when the low 8 bits are zero.
|
/// Sum `len` bytes; an ACPI table/pointer is valid when the low 8 bits are zero.
|
||||||
@@ -1176,58 +833,9 @@ fn readCntRegister(base: [*]align(1) const u8, len: usize, xoff: usize, legacy_o
|
|||||||
return .{ .mmio = false, .address = port, .width = width };
|
return .{ .mmio = false, .address = port, .width = width };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The mapped configuration space of one PCI function (its 4 KiB ECAM page). Mapped
|
|
||||||
/// writable so BAR sizing can probe it; reads and writes both go through here.
|
|
||||||
fn pciConfigurationPtr(alloc: McfgAllocation, hal: Hal, bus: u8, device: u8, function: u8) [*]align(1) u8 {
|
|
||||||
const physical = alloc.base_address +
|
|
||||||
(@as(u64, bus - alloc.start_bus) << 20) +
|
|
||||||
(@as(u64, device) << 15) +
|
|
||||||
(@as(u64, function) << 12);
|
|
||||||
// Map the configuration page (writable, for BAR sizing) and use the virtual
|
|
||||||
// address the HAL hands back.
|
|
||||||
return @ptrFromInt(hal.mapMmio(physical, abi.page_size, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a little-endian integer at `off` from a (possibly unaligned) byte pointer.
|
/// Read a little-endian integer at `off` from a (possibly unaligned) byte pointer.
|
||||||
/// x86 is little-endian and native, so an unaligned load suffices.
|
/// x86 is little-endian and native, so an unaligned load suffices.
|
||||||
fn rd(comptime T: type, bytes: [*]align(1) const u8, off: usize) T {
|
fn rd(comptime T: type, bytes: [*]align(1) const u8, off: usize) T {
|
||||||
const p: *align(1) const T = @ptrCast(bytes + off);
|
const p: *align(1) const T = @ptrCast(bytes + off);
|
||||||
return p.*;
|
return p.*;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a little-endian integer at `off` through a (possibly unaligned) pointer.
|
|
||||||
fn wr(comptime T: type, bytes: [*]align(1) u8, off: usize, value: T) void {
|
|
||||||
const p: *align(1) T = @ptrCast(bytes + off);
|
|
||||||
p.* = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- tests ------------------------------------------------------------------
|
|
||||||
|
|
||||||
test "eisaIdToStr decodes a packed EISA id" {
|
|
||||||
var buffer: [8]u8 = undefined;
|
|
||||||
// 0x030AD041 is the well-known encoding of "PNP0A03" (PCI root bridge).
|
|
||||||
try std.testing.expectEqualStrings("PNP0A03", eisaIdToStr(0x030AD041, &buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parseResourceTemplate extracts IO, IRQ, and fixed memory" {
|
|
||||||
// ResourceTemplate { IO(minimum 0x60, len 8), IRQ(4), Memory32Fixed(0xFED00000, 0x1000) }
|
|
||||||
const runtime = [_]u8{
|
|
||||||
0x47, 0x01, 0x60, 0x00, 0x60, 0x00, 0x01, 0x08, // small IO descriptor
|
|
||||||
0x22, 0x10, 0x00, // small IRQ descriptor (mask bit 4 -> IRQ 4)
|
|
||||||
0x86, 0x09, 0x00, 0x01, 0x00, 0x00, 0xD0, 0xFE, 0x00, 0x10, 0x00, 0x00, // Memory32Fixed
|
|
||||||
0x79, 0x00, // EndTag
|
|
||||||
};
|
|
||||||
var device = device_model.Device{};
|
|
||||||
parseResourceTemplate(&device, &runtime);
|
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(u8, 3), device.resource_count);
|
|
||||||
const rs = device.resources[0..device.resource_count];
|
|
||||||
try std.testing.expectEqual(device_model.ResourceKind.io_port, rs[0].kind);
|
|
||||||
try std.testing.expectEqual(@as(u64, 0x60), rs[0].start);
|
|
||||||
try std.testing.expectEqual(@as(u64, 8), rs[0].len);
|
|
||||||
try std.testing.expectEqual(device_model.ResourceKind.irq, rs[1].kind);
|
|
||||||
try std.testing.expectEqual(@as(u64, 4), rs[1].start);
|
|
||||||
try std.testing.expectEqual(device_model.ResourceKind.memory, rs[2].kind);
|
|
||||||
try std.testing.expectEqual(@as(u64, 0xFED00000), rs[2].start);
|
|
||||||
try std.testing.expectEqual(@as(u64, 0x1000), rs[2].len);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ const std = @import("std");
|
|||||||
const opcode = @import("opcodes.zig");
|
const opcode = @import("opcodes.zig");
|
||||||
const parser = @import("parser.zig");
|
const parser = @import("parser.zig");
|
||||||
|
|
||||||
|
/// The named AML opcode/prefix bytes (`zero_opcode`, `byte_prefix`, …). Re-exported so
|
||||||
|
/// callers that decode raw AML bytes — e.g. the acpi service reading a `_HID` integer —
|
||||||
|
/// name the opcodes instead of writing bare 0x0A/0x0B/… literals (docs/coding-standards.md).
|
||||||
|
pub const opcodes = @import("opcodes.zig");
|
||||||
|
|
||||||
pub const Namespace = @import("namespace.zig").Namespace;
|
pub const Namespace = @import("namespace.zig").Namespace;
|
||||||
pub const Node = @import("namespace.zig").Node;
|
pub const Node = @import("namespace.zig").Node;
|
||||||
pub const NodeKind = @import("namespace.zig").NodeKind;
|
pub const NodeKind = @import("namespace.zig").NodeKind;
|
||||||
@@ -50,6 +55,20 @@ pub fn parse(allocator: std.mem.Allocator, blocks: []const []const u8) !ParseRes
|
|||||||
return .{ .namespace = namespace, .consumed = consumed, .total = total };
|
return .{ .namespace = namespace, .consumed = consumed, .total = total };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Count the Device objects in a parsed namespace — what the acpi service
|
||||||
|
/// (docs/discovery.md) reports, and what the kernel's own parse counts
|
||||||
|
/// so the two can be checked equal across the ring-3 move.
|
||||||
|
pub fn deviceCount(namespace: *const Namespace) usize {
|
||||||
|
return countKind(namespace.root, .device);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn countKind(node: *const Node, kind: NodeKind) usize {
|
||||||
|
var n: usize = if (node.kind == kind) 1 else 0;
|
||||||
|
var c = node.first_child;
|
||||||
|
while (c) |child| : (c = child.next_sibling) n += countKind(child, kind);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
/// Look up the `\_S{state}` sleep package in a parsed namespace and return its
|
/// Look up the `\_S{state}` sleep package in a parsed namespace and return its
|
||||||
/// first two integer elements (SLP_TYP for PM1a / PM1b), or null if absent.
|
/// first two integer elements (SLP_TYP for PM1a / PM1b), or null if absent.
|
||||||
pub fn sleepState(namespace: *Namespace, state: u8) ?SleepType {
|
pub fn sleepState(namespace: *Namespace, state: u8) ?SleepType {
|
||||||
@@ -126,17 +145,22 @@ test "parses a nested namespace and finds the sleep package" {
|
|||||||
// Scope(\_SB) packagelen=0x27
|
// Scope(\_SB) packagelen=0x27
|
||||||
0x10, 0x27, 0x5C, 0x5F, 0x53, 0x42, 0x5F,
|
0x10, 0x27, 0x5C, 0x5F, 0x53, 0x42, 0x5F,
|
||||||
// Device(PCI0) packagelen=0x1F
|
// Device(PCI0) packagelen=0x1F
|
||||||
0x5B, 0x82, 0x1F, 0x50, 0x43, 0x49, 0x30,
|
0x5B, 0x82, 0x1F, 0x50, 0x43,
|
||||||
|
0x49, 0x30,
|
||||||
// Name(_HID, 0x11)
|
// Name(_HID, 0x11)
|
||||||
0x08, 0x5F, 0x48, 0x49, 0x44, 0x0A, 0x11,
|
0x08, 0x5F, 0x48, 0x49, 0x44, 0x0A, 0x11,
|
||||||
// Method(MTHD, flags=1) empty, packagelen=0x06
|
// Method(MTHD, flags=1) empty, packagelen=0x06
|
||||||
0x14, 0x06, 0x4D, 0x54, 0x48, 0x44, 0x01,
|
0x14, 0x06, 0x4D,
|
||||||
|
0x54, 0x48, 0x44, 0x01,
|
||||||
// Method(CALL, flags=0) { MTHD(Zero) }, packagelen=0x0B
|
// Method(CALL, flags=0) { MTHD(Zero) }, packagelen=0x0B
|
||||||
0x14, 0x0B, 0x43, 0x41, 0x4C, 0x4C, 0x00, 0x4D, 0x54, 0x48, 0x44, 0x00,
|
0x14, 0x0B, 0x43, 0x41, 0x4C, 0x4C, 0x00, 0x4D,
|
||||||
|
0x54, 0x48, 0x44, 0x00,
|
||||||
// OperationRegion(DBG0, SystemIO, Word 0x0402, Byte 1)
|
// OperationRegion(DBG0, SystemIO, Word 0x0402, Byte 1)
|
||||||
0x5B, 0x80, 0x44, 0x42, 0x47, 0x30, 0x01, 0x0B, 0x02, 0x04, 0x0A, 0x01,
|
0x5B, 0x80, 0x44, 0x42, 0x47, 0x30, 0x01, 0x0B,
|
||||||
|
0x02, 0x04, 0x0A, 0x01,
|
||||||
// Field(DBG0, flags=1) { DBGB, 8 }, packagelen=0x0B
|
// Field(DBG0, flags=1) { DBGB, 8 }, packagelen=0x0B
|
||||||
0x5B, 0x81, 0x0B, 0x44, 0x42, 0x47, 0x30, 0x01, 0x44, 0x42, 0x47, 0x42, 0x08,
|
0x5B, 0x81, 0x0B, 0x44, 0x42, 0x47, 0x30, 0x01,
|
||||||
|
0x44, 0x42, 0x47, 0x42, 0x08,
|
||||||
};
|
};
|
||||||
|
|
||||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
@@ -206,3 +230,31 @@ test "interpreter runs a method with args, arithmetic, and control flow" {
|
|||||||
const lo = try interpreter.evaluate(tst, &.{.{ .integer = 2 }}); // 2+5=7 !> 10 -> 0
|
const lo = try interpreter.evaluate(tst, &.{.{ .integer = 2 }}); // 2+5=7 !> 10 -> 0
|
||||||
try std.testing.expectEqual(@as(u64, 0), try lo.asInteger());
|
try std.testing.expectEqual(@as(u64, 0), try lo.asInteger());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "interpreter records Notify(device, code)" {
|
||||||
|
// Device(DEV_) { Name(_HID, 0x030AD041) } // PNP0A03-ish placeholder
|
||||||
|
// Method(TST_, 0) { Notify(DEV_, 0x80); Return(Zero) }
|
||||||
|
// Encoded: a Device holding a Name, then a Method issuing Notify on it.
|
||||||
|
const blob = [_]u8{
|
||||||
|
0x5B, 0x82, 0x0F, 0x44, 0x45, 0x56, 0x5F, // Device(DEV_) len=0x0F (pkglen + DEV_ + Name)
|
||||||
|
0x08, 0x5F, 0x48, 0x49, 0x44, 0x0C, 0x41, 0xD0, 0x0A, 0x03, // Name(_HID, DWord 0x030AD041)
|
||||||
|
0x14, 0x0F, 0x54, 0x53, 0x54, 0x5F, 0x00, // Method(TST_, 0) len=0x0F (pkglen + TST_ + flags + body)
|
||||||
|
0x86, 0x44, 0x45, 0x56, 0x5F, 0x0A, 0x80, // Notify(DEV_, 0x80)
|
||||||
|
0xA4, 0x00, // Return(Zero)
|
||||||
|
};
|
||||||
|
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
var result = try parse(arena.allocator(), &.{&blob});
|
||||||
|
const namespace = &result.namespace;
|
||||||
|
const tst = namespace.resolve(namespace.root, false, 0, &.{.{ 'T', 'S', 'T', '_' }}) orelse return error.NoMethod;
|
||||||
|
const dev = namespace.resolve(namespace.root, false, 0, &.{.{ 'D', 'E', 'V', '_' }}) orelse return error.NoDevice;
|
||||||
|
|
||||||
|
var interpreter = Interpreter.init(namespace, .{ .mapMmio = noMap, .pioRead = noRead, .pioWrite = noWrite }, arena.allocator());
|
||||||
|
_ = try interpreter.evaluate(tst, &.{});
|
||||||
|
|
||||||
|
const events = interpreter.takeNotifications();
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), events.len);
|
||||||
|
try std.testing.expectEqual(dev, events[0].node);
|
||||||
|
try std.testing.expectEqual(@as(u64, 0x80), events[0].code);
|
||||||
|
}
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ const Frame = struct {
|
|||||||
/// A CreateField binding: a name that indexes into a buffer object.
|
/// A CreateField binding: a name that indexes into a buffer object.
|
||||||
const BufferField = struct { buffer: *Node, byte_off: usize, bit_width: u32 };
|
const BufferField = struct { buffer: *Node, byte_off: usize, bit_width: u32 };
|
||||||
|
|
||||||
|
/// One Notify(device, code) the interpreter executed.
|
||||||
|
pub const NotifyEvent = struct { node: *Node, code: u64 };
|
||||||
|
|
||||||
pub const Interpreter = struct {
|
pub const Interpreter = struct {
|
||||||
namespace: *Namespace,
|
namespace: *Namespace,
|
||||||
hal: Hal,
|
hal: Hal,
|
||||||
@@ -149,6 +152,11 @@ pub const Interpreter = struct {
|
|||||||
dynamic_overrides: std.AutoHashMapUnmanaged(*Node, Object) = .{},
|
dynamic_overrides: std.AutoHashMapUnmanaged(*Node, Object) = .{},
|
||||||
/// CreateField bindings active for the current evaluation.
|
/// CreateField bindings active for the current evaluation.
|
||||||
fields: std.AutoHashMapUnmanaged(*Node, BufferField) = .{},
|
fields: std.AutoHashMapUnmanaged(*Node, BufferField) = .{},
|
||||||
|
/// Notify(device, code) operations the last evaluation executed — a GPE or
|
||||||
|
/// EC handler tells the OS "look at this device" this way. Bounded; the
|
||||||
|
/// caller drains it with `takeNotifications` after `evaluate` (M21).
|
||||||
|
notify_queue: [16]NotifyEvent = undefined,
|
||||||
|
notify_count: usize = 0,
|
||||||
|
|
||||||
pub fn init(namespace: *Namespace, hal: Hal, arena: std.mem.Allocator) Interpreter {
|
pub fn init(namespace: *Namespace, hal: Hal, arena: std.mem.Allocator) Interpreter {
|
||||||
return .{ .namespace = namespace, .hal = hal, .arena = arena };
|
return .{ .namespace = namespace, .hal = hal, .arena = arena };
|
||||||
@@ -157,6 +165,7 @@ pub const Interpreter = struct {
|
|||||||
/// Evaluate a namespace object: invoke a Method, read a Name's value, or read a
|
/// Evaluate a namespace object: invoke a Method, read a Name's value, or read a
|
||||||
/// Field. Resets per-evaluation runtime state first.
|
/// Field. Resets per-evaluation runtime state first.
|
||||||
pub fn evaluate(self: *Interpreter, node: *Node, args: []const Object) Error!Object {
|
pub fn evaluate(self: *Interpreter, node: *Node, args: []const Object) Error!Object {
|
||||||
|
self.notify_count = 0;
|
||||||
self.dynamic_overrides.clearRetainingCapacity();
|
self.dynamic_overrides.clearRetainingCapacity();
|
||||||
self.fields.clearRetainingCapacity();
|
self.fields.clearRetainingCapacity();
|
||||||
return self.invoke(node, args);
|
return self.invoke(node, args);
|
||||||
@@ -267,6 +276,8 @@ pub const Interpreter = struct {
|
|||||||
},
|
},
|
||||||
opcode.to_buffer_opcode => try self.passThroughUnary(current, frame),
|
opcode.to_buffer_opcode => try self.passThroughUnary(current, frame),
|
||||||
|
|
||||||
|
opcode.notify_opcode => try self.notify(current, frame),
|
||||||
|
|
||||||
opcode.extended_opcode_prefix => try self.ext(current, frame),
|
opcode.extended_opcode_prefix => try self.ext(current, frame),
|
||||||
|
|
||||||
// CreateXField: source, index, name (bit widths differ by op)
|
// CreateXField: source, index, name (bit widths differ by op)
|
||||||
@@ -542,6 +553,36 @@ pub const Interpreter = struct {
|
|||||||
try self.storeInto(current, frame, value);
|
try self.storeInto(current, frame, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Notify(SuperName, NotifyValue): resolve the named device, evaluate the
|
||||||
|
/// code, and record the pair for the caller to dispatch. AML control flow
|
||||||
|
/// continues (Notify returns nothing).
|
||||||
|
fn notify(self: *Interpreter, current: *Cursor, frame: *Frame) Error!Object {
|
||||||
|
const lead = current.peek() orelse return error.Truncated;
|
||||||
|
var target: ?*Node = null;
|
||||||
|
if (isNameStart(lead)) {
|
||||||
|
const name_path = try current.nameString();
|
||||||
|
target = self.namespace.resolve(frame.scope, name_path.rooted, name_path.parents, name_path.slice());
|
||||||
|
} else {
|
||||||
|
// A non-name SuperName (Local/Arg holding a reference).
|
||||||
|
const obj = try self.term(current, frame);
|
||||||
|
if (obj == .reference) target = obj.reference;
|
||||||
|
}
|
||||||
|
const code = try self.evaluateInteger(current, frame);
|
||||||
|
if (target) |node| {
|
||||||
|
if (self.notify_count < self.notify_queue.len) {
|
||||||
|
self.notify_queue[self.notify_count] = .{ .node = node, .code = code };
|
||||||
|
self.notify_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .uninitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Notify events the last `evaluate` produced. Valid until the next
|
||||||
|
/// `evaluate` clears the queue.
|
||||||
|
pub fn takeNotifications(self: *Interpreter) []const NotifyEvent {
|
||||||
|
return self.notify_queue[0..self.notify_count];
|
||||||
|
}
|
||||||
|
|
||||||
fn storeInto(self: *Interpreter, current: *Cursor, frame: *Frame, value: Object) Error!void {
|
fn storeInto(self: *Interpreter, current: *Cursor, frame: *Frame, value: Object) Error!void {
|
||||||
const lead = current.peek() orelse return error.Truncated;
|
const lead = current.peek() orelse return error.Truncated;
|
||||||
if (isNameStart(lead)) {
|
if (isNameStart(lead)) {
|
||||||
|
|||||||
@@ -28,6 +28,23 @@ pub const DeviceClass = enum(u32) {
|
|||||||
/// A device named in the ACPI namespace (from the DSDT/SSDT), carrying a
|
/// A device named in the ACPI namespace (from the DSDT/SSDT), carrying a
|
||||||
/// hardware ID (`_HID`) and, where static, current resource settings (`_CRS`).
|
/// hardware ID (`_HID`) and, where static, current resource settings (`_CRS`).
|
||||||
acpi_device,
|
acpi_device,
|
||||||
|
/// The ACPI tables themselves, published as one node for the user-space acpi
|
||||||
|
/// service (docs/discovery.md): memory resources over the AML blobs,
|
||||||
|
/// a broad io_port grant for OperationRegion access, and the SCI interrupt.
|
||||||
|
/// The one node whose claimant is trusted to run firmware bytecode.
|
||||||
|
acpi_tables,
|
||||||
|
/// One interface of a USB device, registered by the xHCI bus driver. It owns
|
||||||
|
/// no MMIO — it is reached through its controller — so it carries no
|
||||||
|
/// 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,
|
unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,13 +66,46 @@ 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);
|
||||||
|
|
||||||
|
/// `DeviceDescriptor.pci_class` for a device that is not a PCI function. (Zero would be
|
||||||
|
/// ambiguous: 0x000000 is a real class code, "unclassified device".)
|
||||||
|
pub const no_pci_class: u64 = ~@as(u64, 0);
|
||||||
|
|
||||||
/// A device, as snapshotted for user space by `device_enumerate`. A driver scans
|
/// A device, as snapshotted for user space by `device_enumerate`. A driver scans
|
||||||
/// these to find the hardware it owns, claims it, and maps its MMIO.
|
/// these to find the hardware it owns, claims it, and maps its MMIO.
|
||||||
///
|
///
|
||||||
@@ -69,8 +119,18 @@ pub const DeviceDescriptor = extern struct {
|
|||||||
id: u64,
|
id: u64,
|
||||||
parent: u64, // a device id, or `no_parent`
|
parent: u64, // a device id, or `no_parent`
|
||||||
class: u64, // a DeviceClass value
|
class: u64, // a DeviceClass value
|
||||||
|
// The PCI class/subclass/prog-IF triple packed as 0xCCSSPP when this device is a PCI
|
||||||
|
// function, or `no_pci_class` otherwise. This is how a manager tells *what* a
|
||||||
|
// `pci_device` is (an xHCI controller, an AHCI controller) — decode the triple into
|
||||||
|
// names with the pci-class module.
|
||||||
|
pci_class: u64,
|
||||||
hid_len: u64,
|
hid_len: u64,
|
||||||
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 = .{},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -198,8 +198,7 @@ fn dumpNode(device: *const Device, depth: usize, emit: *const fn ([]const u8) vo
|
|||||||
std.fmt.bufPrint(buffer[indent..], "{s} [{s}] hid={s} ({s})\n", .{ device.name(), @tagName(device.class), device.hid(), desc }) catch return
|
std.fmt.bufPrint(buffer[indent..], "{s} [{s}] hid={s} ({s})\n", .{ device.name(), @tagName(device.class), device.hid(), desc }) catch return
|
||||||
else
|
else
|
||||||
std.fmt.bufPrint(buffer[indent..], "{s} [{s}] hid={s}\n", .{ device.name(), @tagName(device.class), device.hid() }) catch return;
|
std.fmt.bufPrint(buffer[indent..], "{s} [{s}] hid={s}\n", .{ device.name(), @tagName(device.class), device.hid() }) catch return;
|
||||||
} else
|
} else std.fmt.bufPrint(buffer[indent..], "{s} [{s}]\n", .{ device.name(), @tagName(device.class) }) catch return;
|
||||||
std.fmt.bufPrint(buffer[indent..], "{s} [{s}]\n", .{ device.name(), @tagName(device.class) }) catch return;
|
|
||||||
emit(buffer[0 .. indent + body.len]);
|
emit(buffer[0 .. indent + body.len]);
|
||||||
|
|
||||||
// For a PCI function, decode its class code — the (class / subclass / prog-IF)
|
// For a PCI function, decode its class code — the (class / subclass / prog-IF)
|
||||||
|
|||||||
+496
-189
@@ -7,6 +7,16 @@
|
|||||||
//! apart. Pure reference data (from the PCI spec; see https://wiki.osdev.org/PCI) — no
|
//! apart. Pure reference data (from the PCI spec; see https://wiki.osdev.org/PCI) — no
|
||||||
//! hardware access — so it is shared by kernel discovery (the device-tree dump) and any
|
//! hardware access — so it is shared by kernel discovery (the device-tree dump) and any
|
||||||
//! user-space tool (a future lspci, driver matching).
|
//! user-space tool (a future lspci, driver matching).
|
||||||
|
//!
|
||||||
|
//! The taxonomy is named, not numbered (docs/coding-standards.md, "Named values"): the
|
||||||
|
//! base class is a `BaseClass` enum, and each class with defined subclasses gets a
|
||||||
|
//! namespace holding its `SubClass` enum (and, where the spec defines them, per-subclass
|
||||||
|
//! `ProgIf` enums) — the same shape as `usb-ids.zig`. Code that *means* a specific class
|
||||||
|
//! names it (`BaseClass.serial_bus`, `serial_bus.usb.ProgIf.xhci`) rather than writing a
|
||||||
|
//! bare 0x0C/0x03/0x30. The `className`/`subclassName`/`progIfName` functions still take
|
||||||
|
//! the raw bytes a function reports in its header, because that is what hardware hands us.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
/// The three bytes of a PCI class code, unpacked from the `0xCCSSPP` value discovery
|
/// The three bytes of a PCI class code, unpacked from the `0xCCSSPP` value discovery
|
||||||
/// records in `Device.ids.pci_class` (CC = base class, SS = subclass, PP = prog-IF).
|
/// records in `Device.ids.pci_class` (CC = base class, SS = subclass, PP = prog-IF).
|
||||||
@@ -22,148 +32,465 @@ pub const ClassCode = struct {
|
|||||||
.prog_if = @intCast(packed_code & 0xFF),
|
.prog_if = @intCast(packed_code & 0xFF),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Re-pack the triple into the `0xCCSSPP` form. Lets code name a whole class code
|
||||||
|
/// from its parts — `pack(.{ .base = @intFromEnum(BaseClass.serial_bus), … })` —
|
||||||
|
/// instead of writing the literal 0x0C0330.
|
||||||
|
pub fn pack(self: ClassCode) u24 {
|
||||||
|
return (@as(u24, self.base) << 16) | (@as(u24, self.subclass) << 8) | self.prog_if;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Base class (config byte 0x0B). Non-exhaustive: an unlisted code is a real but
|
||||||
|
/// unnamed class, decoded as "Unknown" rather than rejected.
|
||||||
|
pub const BaseClass = enum(u8) {
|
||||||
|
unclassified = 0x00,
|
||||||
|
mass_storage = 0x01,
|
||||||
|
network = 0x02,
|
||||||
|
display = 0x03,
|
||||||
|
multimedia = 0x04,
|
||||||
|
memory = 0x05,
|
||||||
|
bridge = 0x06,
|
||||||
|
simple_communication = 0x07,
|
||||||
|
base_system_peripheral = 0x08,
|
||||||
|
input_device = 0x09,
|
||||||
|
docking_station = 0x0A,
|
||||||
|
processor = 0x0B,
|
||||||
|
serial_bus = 0x0C,
|
||||||
|
wireless = 0x0D,
|
||||||
|
intelligent = 0x0E,
|
||||||
|
satellite_communication = 0x0F,
|
||||||
|
encryption = 0x10,
|
||||||
|
signal_processing = 0x11,
|
||||||
|
processing_accelerator = 0x12,
|
||||||
|
non_essential_instrumentation = 0x13,
|
||||||
|
co_processor = 0x40,
|
||||||
|
unassigned = 0xFF,
|
||||||
|
_,
|
||||||
|
|
||||||
|
pub fn name(self: BaseClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.unclassified => "Unclassified",
|
||||||
|
.mass_storage => "Mass Storage Controller",
|
||||||
|
.network => "Network Controller",
|
||||||
|
.display => "Display Controller",
|
||||||
|
.multimedia => "Multimedia Controller",
|
||||||
|
.memory => "Memory Controller",
|
||||||
|
.bridge => "Bridge",
|
||||||
|
.simple_communication => "Simple Communication Controller",
|
||||||
|
.base_system_peripheral => "Base System Peripheral",
|
||||||
|
.input_device => "Input Device Controller",
|
||||||
|
.docking_station => "Docking Station",
|
||||||
|
.processor => "Processor",
|
||||||
|
.serial_bus => "Serial Bus Controller",
|
||||||
|
.wireless => "Wireless Controller",
|
||||||
|
.intelligent => "Intelligent Controller",
|
||||||
|
.satellite_communication => "Satellite Communication Controller",
|
||||||
|
.encryption => "Encryption Controller",
|
||||||
|
.signal_processing => "Signal Processing Controller",
|
||||||
|
.processing_accelerator => "Processing Accelerator",
|
||||||
|
.non_essential_instrumentation => "Non-Essential Instrumentation",
|
||||||
|
.co_processor => "Co-Processor",
|
||||||
|
.unassigned => "Unassigned Class (Vendor specific)",
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Per-class subclass (and prog-IF) taxonomies --------------------------------------
|
||||||
|
// One namespace per base class that has defined subclasses, named after the class. Each
|
||||||
|
// holds an exhaustive `SubClass` enum (so an unlisted code decodes to the class default,
|
||||||
|
// not a wrong name), and, where the spec assigns them, per-subclass `ProgIf` enums.
|
||||||
|
|
||||||
|
pub const mass_storage = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
scsi_bus = 0x00,
|
||||||
|
ide = 0x01,
|
||||||
|
floppy = 0x02,
|
||||||
|
ipi_bus = 0x03,
|
||||||
|
raid = 0x04,
|
||||||
|
ata = 0x05,
|
||||||
|
serial_ata = 0x06,
|
||||||
|
serial_attached_scsi = 0x07,
|
||||||
|
non_volatile_memory = 0x08,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.scsi_bus => "SCSI Bus Controller",
|
||||||
|
.ide => "IDE Controller",
|
||||||
|
.floppy => "Floppy Disk Controller",
|
||||||
|
.ipi_bus => "IPI Bus Controller",
|
||||||
|
.raid => "RAID Controller",
|
||||||
|
.ata => "ATA Controller",
|
||||||
|
.serial_ata => "Serial ATA Controller",
|
||||||
|
.serial_attached_scsi => "Serial Attached SCSI Controller",
|
||||||
|
.non_volatile_memory => "Non-Volatile Memory Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const serial_ata = struct {
|
||||||
|
pub const ProgIf = enum(u8) {
|
||||||
|
vendor_specific = 0x00,
|
||||||
|
ahci = 0x01,
|
||||||
|
serial_storage_bus = 0x02,
|
||||||
|
|
||||||
|
pub fn name(self: ProgIf) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.vendor_specific => "Vendor Specific Interface",
|
||||||
|
.ahci => "AHCI 1.0",
|
||||||
|
.serial_storage_bus => "Serial Storage Bus",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const non_volatile_memory = struct {
|
||||||
|
pub const ProgIf = enum(u8) {
|
||||||
|
nvmhci = 0x01,
|
||||||
|
nvm_express = 0x02,
|
||||||
|
|
||||||
|
pub fn name(self: ProgIf) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.nvmhci => "NVMHCI",
|
||||||
|
.nvm_express => "NVM Express",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const network = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
ethernet = 0x00,
|
||||||
|
token_ring = 0x01,
|
||||||
|
fddi = 0x02,
|
||||||
|
atm = 0x03,
|
||||||
|
isdn = 0x04,
|
||||||
|
picmg_multi_computing = 0x06,
|
||||||
|
infiniband = 0x07,
|
||||||
|
fabric = 0x08,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.ethernet => "Ethernet Controller",
|
||||||
|
.token_ring => "Token Ring Controller",
|
||||||
|
.fddi => "FDDI Controller",
|
||||||
|
.atm => "ATM Controller",
|
||||||
|
.isdn => "ISDN Controller",
|
||||||
|
.picmg_multi_computing => "PICMG 2.14 Multi Computing Controller",
|
||||||
|
.infiniband => "Infiniband Controller",
|
||||||
|
.fabric => "Fabric Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const display = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
vga_compatible = 0x00,
|
||||||
|
xga = 0x01,
|
||||||
|
three_dimensional = 0x02,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.vga_compatible => "VGA Compatible Controller",
|
||||||
|
.xga => "XGA Controller",
|
||||||
|
.three_dimensional => "3D Controller (Not VGA-Compatible)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const vga_compatible = struct {
|
||||||
|
pub const ProgIf = enum(u8) {
|
||||||
|
vga = 0x00,
|
||||||
|
compatible_8514 = 0x01,
|
||||||
|
|
||||||
|
pub fn name(self: ProgIf) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.vga => "VGA Controller",
|
||||||
|
.compatible_8514 => "8514-Compatible Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const multimedia = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
video = 0x00,
|
||||||
|
audio = 0x01,
|
||||||
|
telephony = 0x02,
|
||||||
|
audio_device = 0x03,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.video => "Multimedia Video Controller",
|
||||||
|
.audio => "Multimedia Audio Controller",
|
||||||
|
.telephony => "Computer Telephony Device",
|
||||||
|
.audio_device => "Audio Device",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const memory = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
ram = 0x00,
|
||||||
|
flash = 0x01,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.ram => "RAM Controller",
|
||||||
|
.flash => "Flash Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const bridge = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
host = 0x00,
|
||||||
|
isa = 0x01,
|
||||||
|
eisa = 0x02,
|
||||||
|
mca = 0x03,
|
||||||
|
pci_to_pci = 0x04,
|
||||||
|
pcmcia = 0x05,
|
||||||
|
nubus = 0x06,
|
||||||
|
cardbus = 0x07,
|
||||||
|
raceway = 0x08,
|
||||||
|
pci_to_pci_semi_transparent = 0x09,
|
||||||
|
infiniband_to_pci = 0x0A,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.host => "Host Bridge",
|
||||||
|
.isa => "ISA Bridge",
|
||||||
|
.eisa => "EISA Bridge",
|
||||||
|
.mca => "MCA Bridge",
|
||||||
|
.pci_to_pci => "PCI-to-PCI Bridge",
|
||||||
|
.pcmcia => "PCMCIA Bridge",
|
||||||
|
.nubus => "NuBus Bridge",
|
||||||
|
.cardbus => "CardBus Bridge",
|
||||||
|
.raceway => "RACEway Bridge",
|
||||||
|
.pci_to_pci_semi_transparent => "PCI-to-PCI Bridge (Semi-Transparent)",
|
||||||
|
.infiniband_to_pci => "InfiniBand-to-PCI Host Bridge",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const pci_to_pci = struct {
|
||||||
|
pub const ProgIf = enum(u8) {
|
||||||
|
normal_decode = 0x00,
|
||||||
|
subtractive_decode = 0x01,
|
||||||
|
|
||||||
|
pub fn name(self: ProgIf) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.normal_decode => "Normal Decode",
|
||||||
|
.subtractive_decode => "Subtractive Decode",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const simple_communication = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
serial = 0x00,
|
||||||
|
parallel = 0x01,
|
||||||
|
multiport_serial = 0x02,
|
||||||
|
modem = 0x03,
|
||||||
|
gpib = 0x04,
|
||||||
|
smart_card = 0x05,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.serial => "Serial Controller",
|
||||||
|
.parallel => "Parallel Controller",
|
||||||
|
.multiport_serial => "Multiport Serial Controller",
|
||||||
|
.modem => "Modem",
|
||||||
|
.gpib => "IEEE 488.1/2 (GPIB) Controller",
|
||||||
|
.smart_card => "Smart Card Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const serial = struct {
|
||||||
|
pub const ProgIf = enum(u8) {
|
||||||
|
compatible_8250 = 0x00,
|
||||||
|
compatible_16450 = 0x01,
|
||||||
|
compatible_16550 = 0x02,
|
||||||
|
compatible_16650 = 0x03,
|
||||||
|
compatible_16750 = 0x04,
|
||||||
|
compatible_16850 = 0x05,
|
||||||
|
compatible_16950 = 0x06,
|
||||||
|
|
||||||
|
pub fn name(self: ProgIf) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.compatible_8250 => "8250-Compatible (Generic XT)",
|
||||||
|
.compatible_16450 => "16450-Compatible",
|
||||||
|
.compatible_16550 => "16550-Compatible",
|
||||||
|
.compatible_16650 => "16650-Compatible",
|
||||||
|
.compatible_16750 => "16750-Compatible",
|
||||||
|
.compatible_16850 => "16850-Compatible",
|
||||||
|
.compatible_16950 => "16950-Compatible",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const base_system_peripheral = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
pic = 0x00,
|
||||||
|
dma = 0x01,
|
||||||
|
timer = 0x02,
|
||||||
|
rtc = 0x03,
|
||||||
|
pci_hot_plug = 0x04,
|
||||||
|
sd_host = 0x05,
|
||||||
|
iommu = 0x06,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.pic => "PIC",
|
||||||
|
.dma => "DMA Controller",
|
||||||
|
.timer => "Timer",
|
||||||
|
.rtc => "RTC Controller",
|
||||||
|
.pci_hot_plug => "PCI Hot-Plug Controller",
|
||||||
|
.sd_host => "SD Host Controller",
|
||||||
|
.iommu => "IOMMU",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const input_device = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
keyboard = 0x00,
|
||||||
|
digitizer_pen = 0x01,
|
||||||
|
mouse = 0x02,
|
||||||
|
scanner = 0x03,
|
||||||
|
gameport = 0x04,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.keyboard => "Keyboard Controller",
|
||||||
|
.digitizer_pen => "Digitizer Pen",
|
||||||
|
.mouse => "Mouse Controller",
|
||||||
|
.scanner => "Scanner Controller",
|
||||||
|
.gameport => "Gameport Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const serial_bus = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
firewire = 0x00,
|
||||||
|
access_bus = 0x01,
|
||||||
|
ssa = 0x02,
|
||||||
|
usb = 0x03,
|
||||||
|
fibre_channel = 0x04,
|
||||||
|
smbus = 0x05,
|
||||||
|
infiniband = 0x06,
|
||||||
|
ipmi = 0x07,
|
||||||
|
sercos = 0x08,
|
||||||
|
canbus = 0x09,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.firewire => "FireWire (IEEE 1394) Controller",
|
||||||
|
.access_bus => "ACCESS Bus Controller",
|
||||||
|
.ssa => "SSA",
|
||||||
|
.usb => "USB Controller",
|
||||||
|
.fibre_channel => "Fibre Channel",
|
||||||
|
.smbus => "SMBus Controller",
|
||||||
|
.infiniband => "InfiniBand Controller",
|
||||||
|
.ipmi => "IPMI Interface",
|
||||||
|
.sercos => "SERCOS Interface (IEC 61491)",
|
||||||
|
.canbus => "CANbus Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const usb = struct {
|
||||||
|
pub const ProgIf = enum(u8) {
|
||||||
|
uhci = 0x00,
|
||||||
|
ohci = 0x10,
|
||||||
|
ehci = 0x20,
|
||||||
|
xhci = 0x30,
|
||||||
|
unspecified = 0x80,
|
||||||
|
device = 0xFE,
|
||||||
|
|
||||||
|
pub fn name(self: ProgIf) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.uhci => "UHCI Controller",
|
||||||
|
.ohci => "OHCI Controller",
|
||||||
|
.ehci => "EHCI (USB2) Controller",
|
||||||
|
.xhci => "XHCI (USB3) Controller",
|
||||||
|
.unspecified => "Unspecified",
|
||||||
|
.device => "USB Device (not a host controller)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const wireless = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
irda = 0x00,
|
||||||
|
consumer_ir = 0x01,
|
||||||
|
rf = 0x10,
|
||||||
|
bluetooth = 0x11,
|
||||||
|
broadband = 0x12,
|
||||||
|
ethernet_802_1a = 0x20,
|
||||||
|
ethernet_802_1b = 0x21,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.irda => "iRDA Compatible Controller",
|
||||||
|
.consumer_ir => "Consumer IR Controller",
|
||||||
|
.rf => "RF Controller",
|
||||||
|
.bluetooth => "Bluetooth Controller",
|
||||||
|
.broadband => "Broadband Controller",
|
||||||
|
.ethernet_802_1a => "Ethernet Controller (802.1a)",
|
||||||
|
.ethernet_802_1b => "Ethernet Controller (802.1b)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Raw-byte decoding (what a function reports in its header) -------------------------
|
||||||
|
|
||||||
|
/// The name of an exhaustive class-code enum member, or null if `value` is not one — the
|
||||||
|
/// bridge from a raw config byte to a named taxonomy above.
|
||||||
|
fn enumName(comptime Enum: type, value: u8) ?[]const u8 {
|
||||||
|
return (std.enums.fromInt(Enum, value) orelse return null).name();
|
||||||
|
}
|
||||||
|
|
||||||
/// Name of the base class (byte 0x0B), e.g. `0x06` -> "Bridge".
|
/// Name of the base class (byte 0x0B), e.g. `0x06` -> "Bridge".
|
||||||
pub fn className(base: u8) []const u8 {
|
pub fn className(base: u8) []const u8 {
|
||||||
return switch (base) {
|
return @as(BaseClass, @enumFromInt(base)).name();
|
||||||
0x00 => "Unclassified",
|
|
||||||
0x01 => "Mass Storage Controller",
|
|
||||||
0x02 => "Network Controller",
|
|
||||||
0x03 => "Display Controller",
|
|
||||||
0x04 => "Multimedia Controller",
|
|
||||||
0x05 => "Memory Controller",
|
|
||||||
0x06 => "Bridge",
|
|
||||||
0x07 => "Simple Communication Controller",
|
|
||||||
0x08 => "Base System Peripheral",
|
|
||||||
0x09 => "Input Device Controller",
|
|
||||||
0x0A => "Docking Station",
|
|
||||||
0x0B => "Processor",
|
|
||||||
0x0C => "Serial Bus Controller",
|
|
||||||
0x0D => "Wireless Controller",
|
|
||||||
0x0E => "Intelligent Controller",
|
|
||||||
0x0F => "Satellite Communication Controller",
|
|
||||||
0x10 => "Encryption Controller",
|
|
||||||
0x11 => "Signal Processing Controller",
|
|
||||||
0x12 => "Processing Accelerator",
|
|
||||||
0x13 => "Non-Essential Instrumentation",
|
|
||||||
0x40 => "Co-Processor",
|
|
||||||
0xFF => "Unassigned Class (Vendor specific)",
|
|
||||||
else => "Unknown",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Name of the subclass within its base class, e.g. `(0x06, 0x01)` -> "ISA Bridge".
|
/// Name of the subclass within its base class, e.g. `(0x06, 0x01)` -> "ISA Bridge".
|
||||||
/// Subclass `0x80` is "Other" by PCI convention; anything unlisted is "Unknown".
|
/// Subclass `0x80` is "Other" by PCI convention; anything unlisted is "Unknown".
|
||||||
pub fn subclassName(base: u8, subclass: u8) []const u8 {
|
pub fn subclassName(base: u8, subclass: u8) []const u8 {
|
||||||
return switch (base) {
|
const named: ?[]const u8 = switch (@as(BaseClass, @enumFromInt(base))) {
|
||||||
0x01 => switch (subclass) {
|
.mass_storage => enumName(mass_storage.SubClass, subclass),
|
||||||
0x00 => "SCSI Bus Controller",
|
.network => enumName(network.SubClass, subclass),
|
||||||
0x01 => "IDE Controller",
|
.display => enumName(display.SubClass, subclass),
|
||||||
0x02 => "Floppy Disk Controller",
|
.multimedia => enumName(multimedia.SubClass, subclass),
|
||||||
0x03 => "IPI Bus Controller",
|
.memory => enumName(memory.SubClass, subclass),
|
||||||
0x04 => "RAID Controller",
|
.bridge => enumName(bridge.SubClass, subclass),
|
||||||
0x05 => "ATA Controller",
|
.simple_communication => enumName(simple_communication.SubClass, subclass),
|
||||||
0x06 => "Serial ATA Controller",
|
.base_system_peripheral => enumName(base_system_peripheral.SubClass, subclass),
|
||||||
0x07 => "Serial Attached SCSI Controller",
|
.input_device => enumName(input_device.SubClass, subclass),
|
||||||
0x08 => "Non-Volatile Memory Controller",
|
.serial_bus => enumName(serial_bus.SubClass, subclass),
|
||||||
else => defaultSubclass(subclass),
|
.wireless => enumName(wireless.SubClass, subclass),
|
||||||
},
|
else => null,
|
||||||
0x02 => switch (subclass) {
|
|
||||||
0x00 => "Ethernet Controller",
|
|
||||||
0x01 => "Token Ring Controller",
|
|
||||||
0x02 => "FDDI Controller",
|
|
||||||
0x03 => "ATM Controller",
|
|
||||||
0x04 => "ISDN Controller",
|
|
||||||
0x06 => "PICMG 2.14 Multi Computing Controller",
|
|
||||||
0x07 => "Infiniband Controller",
|
|
||||||
0x08 => "Fabric Controller",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x03 => switch (subclass) {
|
|
||||||
0x00 => "VGA Compatible Controller",
|
|
||||||
0x01 => "XGA Controller",
|
|
||||||
0x02 => "3D Controller (Not VGA-Compatible)",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x04 => switch (subclass) {
|
|
||||||
0x00 => "Multimedia Video Controller",
|
|
||||||
0x01 => "Multimedia Audio Controller",
|
|
||||||
0x02 => "Computer Telephony Device",
|
|
||||||
0x03 => "Audio Device",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x05 => switch (subclass) {
|
|
||||||
0x00 => "RAM Controller",
|
|
||||||
0x01 => "Flash Controller",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x06 => switch (subclass) {
|
|
||||||
0x00 => "Host Bridge",
|
|
||||||
0x01 => "ISA Bridge",
|
|
||||||
0x02 => "EISA Bridge",
|
|
||||||
0x03 => "MCA Bridge",
|
|
||||||
0x04 => "PCI-to-PCI Bridge",
|
|
||||||
0x05 => "PCMCIA Bridge",
|
|
||||||
0x06 => "NuBus Bridge",
|
|
||||||
0x07 => "CardBus Bridge",
|
|
||||||
0x08 => "RACEway Bridge",
|
|
||||||
0x09 => "PCI-to-PCI Bridge (Semi-Transparent)",
|
|
||||||
0x0A => "InfiniBand-to-PCI Host Bridge",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x07 => switch (subclass) {
|
|
||||||
0x00 => "Serial Controller",
|
|
||||||
0x01 => "Parallel Controller",
|
|
||||||
0x02 => "Multiport Serial Controller",
|
|
||||||
0x03 => "Modem",
|
|
||||||
0x04 => "IEEE 488.1/2 (GPIB) Controller",
|
|
||||||
0x05 => "Smart Card Controller",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x08 => switch (subclass) {
|
|
||||||
0x00 => "PIC",
|
|
||||||
0x01 => "DMA Controller",
|
|
||||||
0x02 => "Timer",
|
|
||||||
0x03 => "RTC Controller",
|
|
||||||
0x04 => "PCI Hot-Plug Controller",
|
|
||||||
0x05 => "SD Host Controller",
|
|
||||||
0x06 => "IOMMU",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x09 => switch (subclass) {
|
|
||||||
0x00 => "Keyboard Controller",
|
|
||||||
0x01 => "Digitizer Pen",
|
|
||||||
0x02 => "Mouse Controller",
|
|
||||||
0x03 => "Scanner Controller",
|
|
||||||
0x04 => "Gameport Controller",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x0C => switch (subclass) {
|
|
||||||
0x00 => "FireWire (IEEE 1394) Controller",
|
|
||||||
0x01 => "ACCESS Bus Controller",
|
|
||||||
0x02 => "SSA",
|
|
||||||
0x03 => "USB Controller",
|
|
||||||
0x04 => "Fibre Channel",
|
|
||||||
0x05 => "SMBus Controller",
|
|
||||||
0x06 => "InfiniBand Controller",
|
|
||||||
0x07 => "IPMI Interface",
|
|
||||||
0x08 => "SERCOS Interface (IEC 61491)",
|
|
||||||
0x09 => "CANbus Controller",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x0D => switch (subclass) {
|
|
||||||
0x00 => "iRDA Compatible Controller",
|
|
||||||
0x01 => "Consumer IR Controller",
|
|
||||||
0x10 => "RF Controller",
|
|
||||||
0x11 => "Bluetooth Controller",
|
|
||||||
0x12 => "Broadband Controller",
|
|
||||||
0x20 => "Ethernet Controller (802.1a)",
|
|
||||||
0x21 => "Ethernet Controller (802.1b)",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
};
|
};
|
||||||
|
return named orelse defaultSubclass(subclass);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn defaultSubclass(subclass: u8) []const u8 {
|
fn defaultSubclass(subclass: u8) []const u8 {
|
||||||
@@ -175,68 +502,34 @@ fn defaultSubclass(subclass: u8) []const u8 {
|
|||||||
/// Returns "" when the prog-IF carries no standard meaning for this class/subclass —
|
/// Returns "" when the prog-IF carries no standard meaning for this class/subclass —
|
||||||
/// callers just print the hex byte in that case.
|
/// callers just print the hex byte in that case.
|
||||||
pub fn progIfName(base: u8, subclass: u8, prog_if: u8) []const u8 {
|
pub fn progIfName(base: u8, subclass: u8, prog_if: u8) []const u8 {
|
||||||
return switch (base) {
|
const named: ?[]const u8 = switch (@as(BaseClass, @enumFromInt(base))) {
|
||||||
0x01 => switch (subclass) {
|
.mass_storage => switch (std.enums.fromInt(mass_storage.SubClass, subclass) orelse return "") {
|
||||||
0x06 => switch (prog_if) { // Serial ATA
|
.serial_ata => enumName(mass_storage.serial_ata.ProgIf, prog_if),
|
||||||
0x00 => "Vendor Specific Interface",
|
.non_volatile_memory => enumName(mass_storage.non_volatile_memory.ProgIf, prog_if),
|
||||||
0x01 => "AHCI 1.0",
|
else => null,
|
||||||
0x02 => "Serial Storage Bus",
|
|
||||||
else => "",
|
|
||||||
},
|
},
|
||||||
0x08 => switch (prog_if) { // Non-Volatile Memory
|
.display => switch (std.enums.fromInt(display.SubClass, subclass) orelse return "") {
|
||||||
0x01 => "NVMHCI",
|
.vga_compatible => enumName(display.vga_compatible.ProgIf, prog_if),
|
||||||
0x02 => "NVM Express",
|
else => null,
|
||||||
else => "",
|
|
||||||
},
|
},
|
||||||
else => "",
|
.bridge => switch (std.enums.fromInt(bridge.SubClass, subclass) orelse return "") {
|
||||||
|
.pci_to_pci => enumName(bridge.pci_to_pci.ProgIf, prog_if),
|
||||||
|
else => null,
|
||||||
},
|
},
|
||||||
0x03 => switch (subclass) {
|
.simple_communication => switch (std.enums.fromInt(simple_communication.SubClass, subclass) orelse return "") {
|
||||||
0x00 => switch (prog_if) { // VGA Compatible
|
.serial => enumName(simple_communication.serial.ProgIf, prog_if),
|
||||||
0x00 => "VGA Controller",
|
else => null,
|
||||||
0x01 => "8514-Compatible Controller",
|
|
||||||
else => "",
|
|
||||||
},
|
},
|
||||||
else => "",
|
.serial_bus => switch (std.enums.fromInt(serial_bus.SubClass, subclass) orelse return "") {
|
||||||
|
.usb => enumName(serial_bus.usb.ProgIf, prog_if),
|
||||||
|
else => null,
|
||||||
},
|
},
|
||||||
0x06 => switch (subclass) {
|
else => null,
|
||||||
0x04 => switch (prog_if) { // PCI-to-PCI Bridge
|
|
||||||
0x00 => "Normal Decode",
|
|
||||||
0x01 => "Subtractive Decode",
|
|
||||||
else => "",
|
|
||||||
},
|
|
||||||
else => "",
|
|
||||||
},
|
|
||||||
0x07 => switch (subclass) {
|
|
||||||
0x00 => switch (prog_if) { // Serial Controller
|
|
||||||
0x00 => "8250-Compatible (Generic XT)",
|
|
||||||
0x01 => "16450-Compatible",
|
|
||||||
0x02 => "16550-Compatible",
|
|
||||||
0x03 => "16650-Compatible",
|
|
||||||
0x04 => "16750-Compatible",
|
|
||||||
0x05 => "16850-Compatible",
|
|
||||||
0x06 => "16950-Compatible",
|
|
||||||
else => "",
|
|
||||||
},
|
|
||||||
else => "",
|
|
||||||
},
|
|
||||||
0x0C => switch (subclass) {
|
|
||||||
0x03 => switch (prog_if) { // USB Controller
|
|
||||||
0x00 => "UHCI Controller",
|
|
||||||
0x10 => "OHCI Controller",
|
|
||||||
0x20 => "EHCI (USB2) Controller",
|
|
||||||
0x30 => "XHCI (USB3) Controller",
|
|
||||||
0x80 => "Unspecified",
|
|
||||||
0xFE => "USB Device (not a host controller)",
|
|
||||||
else => "",
|
|
||||||
},
|
|
||||||
else => "",
|
|
||||||
},
|
|
||||||
else => "",
|
|
||||||
};
|
};
|
||||||
|
return named orelse "";
|
||||||
}
|
}
|
||||||
|
|
||||||
test "decodes the common class codes" {
|
test "decodes the common class codes" {
|
||||||
const std = @import("std");
|
|
||||||
const eq = std.testing.expectEqualStrings;
|
const eq = std.testing.expectEqualStrings;
|
||||||
|
|
||||||
const isa = ClassCode.unpack(0x06_01_00);
|
const isa = ClassCode.unpack(0x06_01_00);
|
||||||
@@ -251,11 +544,25 @@ test "decodes the common class codes" {
|
|||||||
try eq("AHCI 1.0", progIfName(ahci.base, ahci.subclass, ahci.prog_if));
|
try eq("AHCI 1.0", progIfName(ahci.base, ahci.subclass, ahci.prog_if));
|
||||||
|
|
||||||
const xhci = ClassCode.unpack(0x0C_03_30);
|
const xhci = ClassCode.unpack(0x0C_03_30);
|
||||||
|
try eq("Serial Bus Controller", className(xhci.base));
|
||||||
try eq("USB Controller", subclassName(xhci.base, xhci.subclass));
|
try eq("USB Controller", subclassName(xhci.base, xhci.subclass));
|
||||||
try eq("XHCI (USB3) Controller", progIfName(xhci.base, xhci.subclass, xhci.prog_if));
|
try eq("XHCI (USB3) Controller", progIfName(xhci.base, xhci.subclass, xhci.prog_if));
|
||||||
|
}
|
||||||
|
|
||||||
// Unknowns and the "Other" convention.
|
test "unlisted codes fall back without a wrong name" {
|
||||||
try eq("Other", subclassName(0x02, 0x80));
|
const eq = std.testing.expectEqualStrings;
|
||||||
try eq("Unknown", subclassName(0x06, 0x7E));
|
try eq("Unknown", className(0x77)); // no such base class
|
||||||
try eq("", progIfName(0x06, 0x00, 0x00)); // host bridge: prog-IF has no standard name
|
try eq("Other", subclassName(0x01, 0x80)); // 0x80 is the PCI "Other" convention
|
||||||
|
try eq("Unknown", subclassName(0x01, 0x7A)); // unlisted mass-storage subclass
|
||||||
|
try eq("", progIfName(0x01, 0x06, 0x7F)); // no standard SATA prog-IF for 0x7F
|
||||||
|
try eq("", progIfName(0x02, 0x00, 0x00)); // class with no prog-IF taxonomy at all
|
||||||
|
}
|
||||||
|
|
||||||
|
test "named parts pack to the raw triple" {
|
||||||
|
const xhci = ClassCode{
|
||||||
|
.base = @intFromEnum(BaseClass.serial_bus),
|
||||||
|
.subclass = @intFromEnum(serial_bus.SubClass.usb),
|
||||||
|
.prog_if = @intFromEnum(serial_bus.usb.ProgIf.xhci),
|
||||||
|
};
|
||||||
|
try std.testing.expectEqual(@as(u24, 0x0C_03_30), xhci.pack());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,14 @@ pub const Resource = device_model.Resource;
|
|||||||
pub const ResourceKind = device_model.ResourceKind;
|
pub const ResourceKind = device_model.ResourceKind;
|
||||||
pub const Hal = device_model.Hal;
|
pub const Hal = device_model.Hal;
|
||||||
pub const PowerInformation = acpi.PowerInformation;
|
pub const PowerInformation = acpi.PowerInformation;
|
||||||
pub const AmlStats = acpi.AmlStats;
|
|
||||||
pub const PlatformInformation = acpi.PlatformInformation;
|
pub const PlatformInformation = acpi.PlatformInformation;
|
||||||
pub const RegisterAccess = acpi.RegisterAccess;
|
pub const RegisterAccess = acpi.RegisterAccess;
|
||||||
pub const IsoEntry = acpi.IsoEntry;
|
pub const IsoEntry = acpi.IsoEntry;
|
||||||
pub const Cpu = acpi.Cpu;
|
pub const Cpu = acpi.Cpu;
|
||||||
|
|
||||||
/// The register map + sleep types discovery extracted, for logging/diagnostics.
|
/// The FADT power register map discovery extracted (PM1 control, reset register),
|
||||||
|
/// for kernel reboot and diagnostics. Sleep-state values are userspace's (S5 is
|
||||||
|
/// owned by the ring-3 acpi service), so they are not here.
|
||||||
pub fn powerInformation() PowerInformation {
|
pub fn powerInformation() PowerInformation {
|
||||||
return acpi.power_information;
|
return acpi.power_information;
|
||||||
}
|
}
|
||||||
@@ -39,11 +40,6 @@ pub fn platformInformation() PlatformInformation {
|
|||||||
return acpi.platform_information;
|
return acpi.platform_information;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AML parse integrity/diagnostics (namespace node count, bytes consumed).
|
|
||||||
pub fn amlStats() AmlStats {
|
|
||||||
return acpi.aml_stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The usable logical processors discovered during enumeration — one entry per
|
/// The usable logical processors discovered during enumeration — one entry per
|
||||||
/// core danos may schedule on, each carrying the Local APIC ID an SMP wake targets.
|
/// core danos may schedule on, each carrying the Local APIC ID an SMP wake targets.
|
||||||
/// `len` is the hardware's degree of parallelism: how many tasks *could* run at the
|
/// `len` is the hardware's degree of parallelism: how many tasks *could* run at the
|
||||||
@@ -72,7 +68,8 @@ pub fn discover(
|
|||||||
var device_tree = try DeviceTree.init(allocator);
|
var device_tree = try DeviceTree.init(allocator);
|
||||||
|
|
||||||
if (boot_information.acpi_rsdp != 0) {
|
if (boot_information.acpi_rsdp != 0) {
|
||||||
try acpi.discover(boot_information.acpi_rsdp, &device_tree, hal);
|
const memory_regions = @as([*]const boot_handoff.MemoryRegion, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.memory_map.regions)))[0..boot_information.memory_map.len];
|
||||||
|
try acpi.discover(boot_information.acpi_rsdp, memory_regions, &device_tree, hal);
|
||||||
} else {
|
} else {
|
||||||
// No ACPI RSDP. A device-tree boot would parse its blob here; today that
|
// No ACPI RSDP. A device-tree boot would parse its blob here; today that
|
||||||
// path is a stub, so this reports the machine described itself no way we
|
// path is a stub, so this reports the machine described itself no way we
|
||||||
@@ -84,12 +81,8 @@ pub fn discover(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Restart the machine. Never returns on success; returns only if no reset method
|
/// Restart the machine. Never returns on success; returns only if no reset method
|
||||||
/// worked (extremely unlikely). Backend-agnostic entry the kernel calls.
|
/// worked (extremely unlikely). Backend-agnostic entry the kernel calls. Soft-off
|
||||||
|
/// (S5) is not a kernel operation — the ring-3 acpi service owns it (docs/power.md).
|
||||||
pub fn reboot(hal: Hal) void {
|
pub fn reboot(hal: Hal) void {
|
||||||
power.reboot(hal);
|
power.reboot(hal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Power the machine off (ACPI S5). Never returns on success.
|
|
||||||
pub fn shutdown(hal: Hal) void {
|
|
||||||
power.shutdown(hal);
|
|
||||||
}
|
|
||||||
|
|||||||
+10
-63
@@ -1,34 +1,17 @@
|
|||||||
//! Machine power control: enter ACPI mode, reboot, and power off (ACPI S5).
|
//! Machine reboot: restart via the FADT reset register, with legacy fallbacks.
|
||||||
//!
|
//!
|
||||||
//! Built entirely on the register map `acpi` extracted from the FADT plus the
|
//! Built on the register map `acpi` extracted from the FADT, driven through the
|
||||||
//! sleep-state (`_Sx`) types the AML submodule pulled from the DSDT, driven through the
|
//! injected `Hal` (port I/O and MMIO). Soft-off (ACPI S5) and suspend (S3) are
|
||||||
//! injected `Hal` (port I/O and MMIO). Nothing here is x86-specific beyond the
|
//! **not** here: they need the AML sleep-state (`_Sx`) values, which the kernel no
|
||||||
//! well-known legacy reset fallbacks, which are guarded behind the ACPI methods.
|
//! longer parses — the ring-3 acpi service owns power management (it re-parses the
|
||||||
//!
|
//! blobs and writes the PM1 control register itself). See docs/power.md. Reboot
|
||||||
//! S3 (suspend-to-RAM) is stubbed: it needs a wake trampoline and device
|
//! stays in the kernel because it needs no AML — only the FADT reset register and
|
||||||
//! re-initialisation, a milestone of its own.
|
//! the well-known legacy fallbacks — so it survives as a last-resort restart.
|
||||||
|
|
||||||
const acpi = @import("acpi.zig");
|
const acpi = @import("acpi.zig");
|
||||||
const device_model = @import("device-model.zig");
|
const device_model = @import("device-model.zig");
|
||||||
const Hal = device_model.Hal;
|
const Hal = device_model.Hal;
|
||||||
|
|
||||||
const slp_en: u32 = 1 << 13; // SLP_EN: writing 1 triggers the sleep transition
|
|
||||||
const sci_en: u32 = 1 << 0; // SCI_EN in PM1 control: set once ACPI mode is active
|
|
||||||
|
|
||||||
/// Switch the platform into ACPI mode if it isn't already, so the PM1 control
|
|
||||||
/// register is live. A no-op when the firmware exposes no SMI command port (ACPI
|
|
||||||
/// already enabled, as under QEMU/OVMF) — we still verify SCI_EN first.
|
|
||||||
pub fn enable(hal: Hal) void {
|
|
||||||
const pi = acpi.power_information;
|
|
||||||
if (!pi.pm1a_cnt.present()) return;
|
|
||||||
if (readRegister(hal, pi.pm1a_cnt) & sci_en != 0) return; // already in ACPI mode
|
|
||||||
if (pi.smi_cmd == 0 or pi.acpi_enable == 0) return; // no way to switch; assume fine
|
|
||||||
|
|
||||||
hal.pioWrite(1, pi.smi_cmd, pi.acpi_enable);
|
|
||||||
var spins: usize = 0;
|
|
||||||
while (readRegister(hal, pi.pm1a_cnt) & sci_en == 0 and spins < 1_000_000) : (spins += 1) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Restart the machine. Tries the ACPI reset register first, then the two legacy
|
/// Restart the machine. Tries the ACPI reset register first, then the two legacy
|
||||||
/// fallbacks. Returns only if every method failed (very unlikely).
|
/// fallbacks. Returns only if every method failed (very unlikely).
|
||||||
pub fn reboot(hal: Hal) void {
|
pub fn reboot(hal: Hal) void {
|
||||||
@@ -48,42 +31,6 @@ pub fn reboot(hal: Hal) void {
|
|||||||
delay();
|
delay();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Power the machine off via ACPI S5. Requires the soft-off (`_S5`) sleep type; if
|
|
||||||
/// it wasn't found in the AML, there is nothing safe to do and this returns.
|
|
||||||
pub fn shutdown(hal: Hal) void {
|
|
||||||
enable(hal);
|
|
||||||
const pi = acpi.power_information;
|
|
||||||
const s5 = pi.s5 orelse return;
|
|
||||||
|
|
||||||
if (pi.pm1a_cnt.present()) {
|
|
||||||
writeRegister(hal, pi.pm1a_cnt, sleepValue(s5.slp_typ_a));
|
|
||||||
}
|
|
||||||
if (pi.pm1b_cnt.present()) {
|
|
||||||
writeRegister(hal, pi.pm1b_cnt, sleepValue(s5.slp_typ_b));
|
|
||||||
}
|
|
||||||
delay();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// S3 suspend-to-RAM — not implemented (needs a wake path + device re-init).
|
|
||||||
pub fn sleepS3(hal: Hal) error{Unsupported}!void {
|
|
||||||
_ = hal;
|
|
||||||
return error.Unsupported;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The PM1 control write that requests sleep type `slp_typ`: SLP_TYP in bits
|
|
||||||
/// [12:10], SLP_EN in bit 13.
|
|
||||||
fn sleepValue(slp_typ: u8) u32 {
|
|
||||||
return (@as(u32, slp_typ & 0x7) << 10) | slp_en;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn readRegister(hal: Hal, register: acpi.RegisterAccess) u32 {
|
|
||||||
if (register.mmio) {
|
|
||||||
const p: *align(1) volatile u32 = @ptrFromInt(hal.mapMmio(register.address, 4, true));
|
|
||||||
return p.*;
|
|
||||||
}
|
|
||||||
return hal.pioRead(register.width, @intCast(register.address));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeRegister(hal: Hal, register: acpi.RegisterAccess, value: u32) void {
|
fn writeRegister(hal: Hal, register: acpi.RegisterAccess, value: u32) void {
|
||||||
if (register.mmio) {
|
if (register.mmio) {
|
||||||
const p: *align(1) volatile u32 = @ptrFromInt(hal.mapMmio(register.address, 4, true));
|
const p: *align(1) volatile u32 = @ptrFromInt(hal.mapMmio(register.address, 4, true));
|
||||||
@@ -93,8 +40,8 @@ fn writeRegister(hal: Hal, register: acpi.RegisterAccess, value: u32) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A short busy-wait so a reset/power-off takes effect before we fall through to
|
/// A short busy-wait so a reset takes effect before we fall through to the next
|
||||||
/// the next method. The empty asm is an architecture-neutral barrier that keeps the loop
|
/// method. The empty asm is an architecture-neutral barrier that keeps the loop
|
||||||
/// from being optimised away.
|
/// from being optimised away.
|
||||||
fn delay() void {
|
fn delay() void {
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
|
|||||||
@@ -0,0 +1,945 @@
|
|||||||
|
//! USB device-framework wire ABI: the set-up packets, standard requests, and standard
|
||||||
|
//! descriptors every USB device speaks over its default control pipe, as defined by chapter 9
|
||||||
|
//! of the USB 2.0 specification (see https://wiki.osdev.org/Universal_Serial_Bus). Pure data
|
||||||
|
//! definitions — no hardware access — shared by the host-controller bus drivers (which build
|
||||||
|
//! the requests) and anything that parses what devices return (device naming, driver
|
||||||
|
//! matching, configuration). The structs mirror the wire byte-for-byte: multi-byte fields are
|
||||||
|
//! little-endian and align(1), so a descriptor can be bit-cast straight out of a transfer
|
||||||
|
//! buffer at any offset, and bitmap bytes are packed structs so no caller ever needs a magic
|
||||||
|
//! mask. Class, subclass, and protocol code tables live in usb-ids.zig.
|
||||||
|
|
||||||
|
pub const DeviceState = enum(u8) {
|
||||||
|
// Immediately after the USB device is attached to the USB system, it is in this state.
|
||||||
|
// The USB specifications do not define the state of a USB device that is detached from
|
||||||
|
// a USB system.
|
||||||
|
attached,
|
||||||
|
// A device is in this state after it has both been attached to the bus, and the VBUS line is
|
||||||
|
// applied to the device (the host controller drives the VBUS at +5V, however this is only
|
||||||
|
// particularly important for hardware developers). In this state, the device must not respond
|
||||||
|
// to any bus transactions. The USB specification recognizes three potential scenarios with
|
||||||
|
// respect to how a device draws power:
|
||||||
|
// - Self-Powered Devices draw power from an external power source (e.g, a USB printer plugs
|
||||||
|
// into the wall as well as a USB port). Although the device may be considered
|
||||||
|
// technically "powered" even before attachment to the USB, it is still only considered
|
||||||
|
// powered after the VBUS line is applied to the device.
|
||||||
|
// - Bus-Powered Devices draw power solely from the USB up to 100mA.
|
||||||
|
// - Self- or Bus-Powered Devices may draw power from either the bus or an external power
|
||||||
|
// source, depending on the configuration. These devices may change power source at any
|
||||||
|
// time. If a device is currently self-powered and requires more than 100mA of power, but
|
||||||
|
// switches to being bus-powered, then the device must return to the Address state.
|
||||||
|
powered,
|
||||||
|
// A device in the powered state enters the default state after receiving a bus reset. In this
|
||||||
|
// state, the device is addressable at the default, reserved address of 0. At this point, the
|
||||||
|
// device is operating at the correct speed. The host is expected to allow 10 milliseconds
|
||||||
|
// before expecting the device to respond to data transfers after reset.
|
||||||
|
default,
|
||||||
|
// A device enters this state after the host assigns it an address via the default control pipe,
|
||||||
|
// which is always accessible whether the device's address has been set or not.
|
||||||
|
address,
|
||||||
|
// A device is in this state after the host examines its possible configurations and selects
|
||||||
|
// one. All endpoint's data toggle bits are initialized to zero when a device enters this state.
|
||||||
|
configured,
|
||||||
|
// When no traffic is observed on the bus for a period of 1 millisecond, a USB device enters
|
||||||
|
// this state, characterized by its low power consumption. The device's address and
|
||||||
|
// configuration settings are maintained while suspended. A device exits the suspended state as
|
||||||
|
// soon as it begins seeing bus activity again. The host is expected to allow 10 milliseconds
|
||||||
|
// before expecting the device to respond to data transfers after resume.
|
||||||
|
suspended,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestCode = enum(u8) {
|
||||||
|
get_status = 0,
|
||||||
|
clear_feature = 1,
|
||||||
|
set_feature = 3,
|
||||||
|
set_address = 5,
|
||||||
|
get_descriptor = 6,
|
||||||
|
set_descriptor = 7,
|
||||||
|
get_configuration = 8,
|
||||||
|
set_configuration = 9,
|
||||||
|
get_interface = 10,
|
||||||
|
set_interface = 11,
|
||||||
|
sync_frame = 12,
|
||||||
|
// Non-exhaustive: class-specific requests (HID, mass storage) reuse this byte
|
||||||
|
// field with codes from their own class's namespace — see the class-request
|
||||||
|
// constructors below. Some class codes numerically coincide with a standard
|
||||||
|
// one; the wire byte is what matters, and the constructors set it explicitly.
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Direction of an endpoint, from the host's point of view
|
||||||
|
pub const EndpointDirection = enum(u1) {
|
||||||
|
out = 0,
|
||||||
|
in = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Identifier newtypes: distinct wire-sized types for values that identify something on the
|
||||||
|
// device rather than count something. Each is a non-exhaustive enum whose values originate
|
||||||
|
// in the descriptors below and flow, still typed, into the standard request constructors —
|
||||||
|
// so an interface number can never be passed where a configuration value is expected.
|
||||||
|
|
||||||
|
// The bus address of a device, assigned by the host with SET_ADDRESS. Addresses are 7 bits
|
||||||
|
// wide.
|
||||||
|
pub const DeviceAddress = enum(u7) {
|
||||||
|
// The default address every device answers at after a reset, until SET_ADDRESS
|
||||||
|
// completes
|
||||||
|
default = 0,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Identifies a configuration; from ConfigurationDescriptor.configuration_value.
|
||||||
|
pub const ConfigurationValue = enum(u8) {
|
||||||
|
// Not configured: returned by GET_CONFIGURATION while the device is in the address
|
||||||
|
// state, and passed to SET_CONFIGURATION to return a configured device to the address
|
||||||
|
// state
|
||||||
|
none = 0,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Identifies an interface within a configuration; from
|
||||||
|
// InterfaceDescriptor.interface_number.
|
||||||
|
pub const InterfaceNumber = enum(u8) { _ };
|
||||||
|
|
||||||
|
// Selects between the alternate settings of one interface; from
|
||||||
|
// InterfaceDescriptor.alternate_setting.
|
||||||
|
pub const AlternateSetting = enum(u8) {
|
||||||
|
// The default setting of an interface
|
||||||
|
default = 0,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The number of an endpoint within a device, 4 bits wide. The direction bit carried
|
||||||
|
// alongside it tells the two endpoints sharing a number apart.
|
||||||
|
pub const EndpointNumber = enum(u4) {
|
||||||
|
// Endpoint zero: the default control pipe every device provides
|
||||||
|
default_control = 0,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Index of a STRING descriptor, stored in descriptors that reference a string and passed to
|
||||||
|
// GET_DESCRIPTOR to read it.
|
||||||
|
pub const StringIndex = enum(u8) {
|
||||||
|
// The device has no string descriptor for this field
|
||||||
|
none = 0,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Characteristics of a device request (the bmRequestType field of a set-up packet). Fields are
|
||||||
|
// declared least-significant first: recipient occupies bits 4...0, kind bits 6...5, and
|
||||||
|
// direction bit 7.
|
||||||
|
pub const RequestType = packed struct(u8) {
|
||||||
|
// The recipient of the request (values 4...31 are reserved)
|
||||||
|
recipient: Recipient,
|
||||||
|
// The type of the request
|
||||||
|
kind: Kind,
|
||||||
|
// Data transfer direction. The value of this bit is ignored when length is zero.
|
||||||
|
direction: Direction,
|
||||||
|
|
||||||
|
pub const Recipient = enum(u5) {
|
||||||
|
device = 0,
|
||||||
|
interface = 1,
|
||||||
|
endpoint = 2,
|
||||||
|
other = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Kind = enum(u2) {
|
||||||
|
standard = 0,
|
||||||
|
class = 1,
|
||||||
|
vendor = 2,
|
||||||
|
reserved = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Direction = enum(u1) {
|
||||||
|
host_to_device = 0,
|
||||||
|
device_to_host = 1,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Request = extern struct {
|
||||||
|
// Characteristics of the request
|
||||||
|
request_type: RequestType,
|
||||||
|
// Specific request
|
||||||
|
request_code: RequestCode,
|
||||||
|
// Word-sized field that may (or may not) serve as a parameter to the request, depending
|
||||||
|
// on the specific request. For GET_DESCRIPTOR and SET_DESCRIPTOR, bit-cast a
|
||||||
|
// DescriptorValue into this field.
|
||||||
|
value: u16 align(1),
|
||||||
|
// Word-sized field that may (or may not) serve as a parameter to the request, depending
|
||||||
|
// on the specific request. Typically this field holds an index or an offset value. When
|
||||||
|
// request_type specifies an endpoint or an interface as the recipient, bit-cast an
|
||||||
|
// EndpointIndex or an InterfaceIndex into this field.
|
||||||
|
index: u16 align(1),
|
||||||
|
// Number of bytes to transfer if there is a DATA stage.
|
||||||
|
// - If this field is non-zero, and request_type indicates a transfer from
|
||||||
|
// device-to-host, then the device must never return more than length bytes of data.
|
||||||
|
// However, a device may return less.
|
||||||
|
// - If this field is non-zero, and request_type indicates a transfer from
|
||||||
|
// host-to-device, then the host must send exactly length bytes of data. If the host
|
||||||
|
// sends more than length bytes, the behavior of the device is undefined.
|
||||||
|
length: u16 align(1),
|
||||||
|
|
||||||
|
// The format of the index field when request_type specifies an endpoint as the
|
||||||
|
// recipient. The host should always set the direction bit to zero (but the device
|
||||||
|
// should accept either value) when the endpoint is part of a control pipe.
|
||||||
|
pub const EndpointIndex = packed struct(u16) {
|
||||||
|
// Endpoint number
|
||||||
|
number: EndpointNumber,
|
||||||
|
// Reserved (reset to zero)
|
||||||
|
reserved: u3 = 0,
|
||||||
|
// Selects the OUT or the IN endpoint with the specified endpoint number
|
||||||
|
direction: EndpointDirection,
|
||||||
|
// Reserved (reset to zero)
|
||||||
|
reserved_high: u8 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The format of the index field when request_type specifies an interface as the
|
||||||
|
// recipient.
|
||||||
|
pub const InterfaceIndex = packed struct(u16) {
|
||||||
|
// Interface number
|
||||||
|
number: u8,
|
||||||
|
// Reserved (reset to zero)
|
||||||
|
reserved: u8 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The format of the value field of GET_DESCRIPTOR and SET_DESCRIPTOR requests: the
|
||||||
|
// descriptor type in the high byte, and the descriptor index in the low byte. The index
|
||||||
|
// is used to select a specific descriptor (only for CONFIGURATION and STRING
|
||||||
|
// descriptors) when several descriptors of that type are implemented by a device.
|
||||||
|
pub const DescriptorValue = packed struct(u16) {
|
||||||
|
// Descriptor index
|
||||||
|
index: u8 = 0,
|
||||||
|
// Descriptor type
|
||||||
|
kind: DescriptorType,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Feature selectors, used as the value field of CLEAR_FEATURE and SET_FEATURE requests. The
|
||||||
|
// comment on each value notes the recipient the selector applies to.
|
||||||
|
pub const FeatureSelector = enum(u16) {
|
||||||
|
// Halts an endpoint (recipient: endpoint)
|
||||||
|
endpoint_halt = 0,
|
||||||
|
// Enables or disables the device's remote wakeup capability (recipient: device)
|
||||||
|
device_remote_wakeup = 1,
|
||||||
|
// Puts a hi-speed device into a test mode, selected by a TestMode value in the high
|
||||||
|
// byte of the index field (recipient: device)
|
||||||
|
test_mode = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test mode selectors, passed in the high byte of the index field of a SET_FEATURE request
|
||||||
|
// with the test_mode feature selector. Values 06h...3Fh are reserved for standard test
|
||||||
|
// selectors and C0h...FFh for vendor-specific test modes; all other unlisted values are
|
||||||
|
// reserved.
|
||||||
|
pub const TestMode = enum(u8) {
|
||||||
|
test_j = 0x01,
|
||||||
|
test_k = 0x02,
|
||||||
|
test_se0_nak = 0x03,
|
||||||
|
test_packet = 0x04,
|
||||||
|
test_force_enable = 0x05,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The two bytes returned by a GET_STATUS request directed at a device. Fields are declared
|
||||||
|
// least-significant first.
|
||||||
|
pub const DeviceStatus = packed struct(u16) {
|
||||||
|
// Whether the device is currently self-powered (as opposed to bus-powered). This bit
|
||||||
|
// cannot be changed with the SET_FEATURE or CLEAR_FEATURE requests.
|
||||||
|
self_powered: bool,
|
||||||
|
// Whether the device is currently enabled to request remote wakeup. Changed with the
|
||||||
|
// SET_FEATURE and CLEAR_FEATURE requests using the device_remote_wakeup feature
|
||||||
|
// selector.
|
||||||
|
remote_wakeup: bool,
|
||||||
|
// Reserved (reset to zero)
|
||||||
|
reserved: u14,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The two bytes returned by a GET_STATUS request directed at an endpoint. (A GET_STATUS
|
||||||
|
// request directed at an interface returns two bytes that are entirely reserved.)
|
||||||
|
pub const EndpointStatus = packed struct(u16) {
|
||||||
|
// Whether the endpoint is currently halted. Set with the SET_FEATURE request using the
|
||||||
|
// endpoint_halt feature selector, and cleared with CLEAR_FEATURE.
|
||||||
|
halted: bool,
|
||||||
|
// Reserved (reset to zero)
|
||||||
|
reserved: u15,
|
||||||
|
};
|
||||||
|
|
||||||
|
// A target for the standard requests that may be directed at the device, an interface, or
|
||||||
|
// an endpoint.
|
||||||
|
pub const Target = union(enum) {
|
||||||
|
device,
|
||||||
|
interface: InterfaceNumber,
|
||||||
|
endpoint: Request.EndpointIndex,
|
||||||
|
|
||||||
|
fn recipient(target: Target) RequestType.Recipient {
|
||||||
|
return switch (target) {
|
||||||
|
.device => .device,
|
||||||
|
.interface => .interface,
|
||||||
|
.endpoint => .endpoint,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index(target: Target) u16 {
|
||||||
|
return switch (target) {
|
||||||
|
.device => 0,
|
||||||
|
.interface => |number| @intFromEnum(number),
|
||||||
|
.endpoint => |endpoint| @bitCast(endpoint),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Constructors for the standard device requests, one per RequestCode. Each returns a
|
||||||
|
// ready-to-send set-up packet with the request_type, value, index, and length fields the
|
||||||
|
// specification prescribes for that request.
|
||||||
|
|
||||||
|
// Reads the status of the given target: bit-cast the two bytes the device returns into a
|
||||||
|
// DeviceStatus or an EndpointStatus. (The two bytes returned for an interface are entirely
|
||||||
|
// reserved.)
|
||||||
|
pub fn getStatus(target: Target) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{
|
||||||
|
.recipient = target.recipient(),
|
||||||
|
.kind = .standard,
|
||||||
|
.direction = .device_to_host,
|
||||||
|
},
|
||||||
|
.request_code = .get_status,
|
||||||
|
.value = 0,
|
||||||
|
.index = target.index(),
|
||||||
|
.length = 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clears or disables the given feature. A device cannot be taken out of a test mode with
|
||||||
|
// this request; test_mode is only cleared by cycling power.
|
||||||
|
pub fn clearFeature(feature: FeatureSelector, target: Target) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{
|
||||||
|
.recipient = target.recipient(),
|
||||||
|
.kind = .standard,
|
||||||
|
.direction = .host_to_device,
|
||||||
|
},
|
||||||
|
.request_code = .clear_feature,
|
||||||
|
.value = @intFromEnum(feature),
|
||||||
|
.index = target.index(),
|
||||||
|
.length = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets or enables the given feature. For the test_mode feature selector, use setTestMode
|
||||||
|
// instead: the test selector rides in the high byte of the index field.
|
||||||
|
pub fn setFeature(feature: FeatureSelector, target: Target) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{
|
||||||
|
.recipient = target.recipient(),
|
||||||
|
.kind = .standard,
|
||||||
|
.direction = .host_to_device,
|
||||||
|
},
|
||||||
|
.request_code = .set_feature,
|
||||||
|
.value = @intFromEnum(feature),
|
||||||
|
.index = target.index(),
|
||||||
|
.length = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Puts a hi-speed device into the given test mode: a SET_FEATURE request with the test_mode
|
||||||
|
// feature selector and the test selector in the high byte of the index field.
|
||||||
|
pub fn setTestMode(mode: TestMode) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{
|
||||||
|
.recipient = .device,
|
||||||
|
.kind = .standard,
|
||||||
|
.direction = .host_to_device,
|
||||||
|
},
|
||||||
|
.request_code = .set_feature,
|
||||||
|
.value = @intFromEnum(FeatureSelector.test_mode),
|
||||||
|
.index = @as(u16, @intFromEnum(mode)) << 8,
|
||||||
|
.length = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assigns the device its bus address, moving it from the default state to the address
|
||||||
|
// state. The device does not answer at the new address until the status stage of this
|
||||||
|
// request completes.
|
||||||
|
pub fn setAddress(address: DeviceAddress) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{
|
||||||
|
.recipient = .device,
|
||||||
|
.kind = .standard,
|
||||||
|
.direction = .host_to_device,
|
||||||
|
},
|
||||||
|
.request_code = .set_address,
|
||||||
|
.value = @intFromEnum(address),
|
||||||
|
.index = 0,
|
||||||
|
.length = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads a descriptor from the device.
|
||||||
|
// - descriptor_index selects among descriptors of the same type, and is only used for
|
||||||
|
// configuration and string descriptors.
|
||||||
|
// - language_id selects the language of a string descriptor, and is zero otherwise.
|
||||||
|
// - length is the number of bytes to read; a device never returns more than length bytes,
|
||||||
|
// but may return less if the descriptor is shorter.
|
||||||
|
pub fn getDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{
|
||||||
|
.recipient = .device,
|
||||||
|
.kind = .standard,
|
||||||
|
.direction = .device_to_host,
|
||||||
|
},
|
||||||
|
.request_code = .get_descriptor,
|
||||||
|
.value = @bitCast(Request.DescriptorValue{ .index = descriptor_index, .kind = kind }),
|
||||||
|
.index = language_id,
|
||||||
|
.length = length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates an existing descriptor or adds a new one (optional; many devices do not support
|
||||||
|
// this request). The parameters mirror getDescriptor; the descriptor itself is sent in the
|
||||||
|
// DATA stage.
|
||||||
|
pub fn setDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{
|
||||||
|
.recipient = .device,
|
||||||
|
.kind = .standard,
|
||||||
|
.direction = .host_to_device,
|
||||||
|
},
|
||||||
|
.request_code = .set_descriptor,
|
||||||
|
.value = @bitCast(Request.DescriptorValue{ .index = descriptor_index, .kind = kind }),
|
||||||
|
.index = language_id,
|
||||||
|
.length = length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads the currently active configuration: @enumFromInt the byte the device returns into a
|
||||||
|
// ConfigurationValue, which is none while the device is not configured.
|
||||||
|
pub fn getConfiguration() Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{
|
||||||
|
.recipient = .device,
|
||||||
|
.kind = .standard,
|
||||||
|
.direction = .device_to_host,
|
||||||
|
},
|
||||||
|
.request_code = .get_configuration,
|
||||||
|
.value = 0,
|
||||||
|
.index = 0,
|
||||||
|
.length = 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selects the configuration with the given configuration_value (from
|
||||||
|
// ConfigurationDescriptor.configuration_value), moving the device from the address state to
|
||||||
|
// the configured state. Selecting none returns the device to the address state.
|
||||||
|
pub fn setConfiguration(configuration_value: ConfigurationValue) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{
|
||||||
|
.recipient = .device,
|
||||||
|
.kind = .standard,
|
||||||
|
.direction = .host_to_device,
|
||||||
|
},
|
||||||
|
.request_code = .set_configuration,
|
||||||
|
.value = @intFromEnum(configuration_value),
|
||||||
|
.index = 0,
|
||||||
|
.length = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads the alternate setting currently selected for the given interface: @enumFromInt the
|
||||||
|
// byte the device returns into an AlternateSetting.
|
||||||
|
pub fn getInterface(interface: InterfaceNumber) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{
|
||||||
|
.recipient = .interface,
|
||||||
|
.kind = .standard,
|
||||||
|
.direction = .device_to_host,
|
||||||
|
},
|
||||||
|
.request_code = .get_interface,
|
||||||
|
.value = 0,
|
||||||
|
.index = @intFromEnum(interface),
|
||||||
|
.length = 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selects an alternate setting (from InterfaceDescriptor.alternate_setting) for the given
|
||||||
|
// interface.
|
||||||
|
pub fn setInterface(interface: InterfaceNumber, alternate_setting: AlternateSetting) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{
|
||||||
|
.recipient = .interface,
|
||||||
|
.kind = .standard,
|
||||||
|
.direction = .host_to_device,
|
||||||
|
},
|
||||||
|
.request_code = .set_interface,
|
||||||
|
.value = @intFromEnum(alternate_setting),
|
||||||
|
.index = @intFromEnum(interface),
|
||||||
|
.length = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads the two-byte number of the frame in which the given isochronous endpoint's
|
||||||
|
// repeating pattern of transfers begins.
|
||||||
|
pub fn syncFrame(endpoint: Request.EndpointIndex) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{
|
||||||
|
.recipient = .endpoint,
|
||||||
|
.kind = .standard,
|
||||||
|
.direction = .device_to_host,
|
||||||
|
},
|
||||||
|
.request_code = .sync_frame,
|
||||||
|
.value = 0,
|
||||||
|
.index = @bitCast(endpoint),
|
||||||
|
.length = 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Class-specific requests. These carry a `kind = .class` request_type and a
|
||||||
|
// request_code from the interface's class namespace (not the standard
|
||||||
|
// RequestCode set above); the code is written into the same byte field, which
|
||||||
|
// is why RequestCode is non-exhaustive. Each is directed at an interface, whose
|
||||||
|
// number rides in the index field.
|
||||||
|
|
||||||
|
// The HID class request codes (USB HID 1.11 §7.2). Only the ones danos issues
|
||||||
|
// are named; the field on the wire is the raw byte.
|
||||||
|
pub const HidRequestCode = enum(u8) {
|
||||||
|
get_report = 0x01,
|
||||||
|
get_idle = 0x02,
|
||||||
|
get_protocol = 0x03,
|
||||||
|
set_report = 0x09,
|
||||||
|
set_idle = 0x0A,
|
||||||
|
set_protocol = 0x0B,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The two protocols a boot-capable HID device can run (USB HID 1.11 §7.2.5).
|
||||||
|
// A driver selects `boot` for the simplified fixed-format boot report, usable
|
||||||
|
// before a full report-descriptor parser exists.
|
||||||
|
pub const HidProtocol = enum(u8) {
|
||||||
|
boot = 0,
|
||||||
|
report = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// SET_PROTOCOL: choose the boot or report protocol on a HID interface.
|
||||||
|
pub fn setProtocol(interface: InterfaceNumber, protocol: HidProtocol) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{ .recipient = .interface, .kind = .class, .direction = .host_to_device },
|
||||||
|
.request_code = @enumFromInt(@intFromEnum(HidRequestCode.set_protocol)),
|
||||||
|
.value = @intFromEnum(protocol),
|
||||||
|
.index = @intFromEnum(interface),
|
||||||
|
.length = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// SET_IDLE: bound a HID interface's report rate. `duration` is in 4 ms units
|
||||||
|
// (0 means report only on change); `report_id` selects a report (0 = all).
|
||||||
|
pub fn setIdle(interface: InterfaceNumber, duration: u8, report_id: u8) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{ .recipient = .interface, .kind = .class, .direction = .host_to_device },
|
||||||
|
.request_code = @enumFromInt(@intFromEnum(HidRequestCode.set_idle)),
|
||||||
|
.value = (@as(u16, duration) << 8) | report_id,
|
||||||
|
.index = @intFromEnum(interface),
|
||||||
|
.length = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk-Only Mass Storage Reset (USB MSC BOT §3.1): ready a mass-storage
|
||||||
|
// interface for the next Command Block Wrapper after a protocol error.
|
||||||
|
pub fn bulkOnlyMassStorageReset(interface: InterfaceNumber) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{ .recipient = .interface, .kind = .class, .direction = .host_to_device },
|
||||||
|
.request_code = @enumFromInt(0xFF),
|
||||||
|
.value = 0,
|
||||||
|
.index = @intFromEnum(interface),
|
||||||
|
.length = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Max LUN (USB MSC BOT §3.2): read the highest logical unit number the
|
||||||
|
// device supports (0 for a single-LUN flash drive). One byte is returned.
|
||||||
|
pub fn getMaxLun(interface: InterfaceNumber) Request {
|
||||||
|
return .{
|
||||||
|
.request_type = .{ .recipient = .interface, .kind = .class, .direction = .device_to_host },
|
||||||
|
.request_code = @enumFromInt(0xFE),
|
||||||
|
.value = 0,
|
||||||
|
.index = @intFromEnum(interface),
|
||||||
|
.length = 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const DescriptorType = enum(u8) {
|
||||||
|
device = 1,
|
||||||
|
configuration = 2,
|
||||||
|
string = 3,
|
||||||
|
interface = 4,
|
||||||
|
endpoint = 5,
|
||||||
|
device_qualifier = 6,
|
||||||
|
other_speed_configuration = 7,
|
||||||
|
interface_power = 8,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DeviceDescriptor = extern struct {
|
||||||
|
// Size of this descriptor in bytes
|
||||||
|
length: u8,
|
||||||
|
// DEVICE Descriptor Type
|
||||||
|
descriptor_type: DescriptorType,
|
||||||
|
// USB Specification Release Number in Binary-Coded Decimal (i.e, 2.10 is expressed as 210h).
|
||||||
|
// Identifies the release of the USB Specification with with the device and its
|
||||||
|
// descriptors are compliant.
|
||||||
|
bcd_usb: u16 align(1),
|
||||||
|
// Class code (assigned by the USB-IF)
|
||||||
|
// - This field is reset to zero if each interface within a configuration specifies its own
|
||||||
|
// class information and the various interfaces operate independently.
|
||||||
|
// - A value of FFh in this field indicates the device class is vendor-specific.
|
||||||
|
device_class: u8,
|
||||||
|
// Subclass Code (assigned by the USB-IF)
|
||||||
|
// - The subclass code of a device is qualified by the class code of that device.
|
||||||
|
// - If device_class is reset to zero, then this field must also be reset to zero.
|
||||||
|
// - When device_class is not set to FFh, then all values for this field are reserved for
|
||||||
|
// assignment by the USB-IF.
|
||||||
|
device_subclass: u8,
|
||||||
|
// Protocol code (assigned by the USB-IF)
|
||||||
|
// - The protocol code of a device is qualified by both the class and subclass codes of
|
||||||
|
// that device.
|
||||||
|
// - A value of 00h in this field means that the device may specify class-specific
|
||||||
|
// protocols on an interface basis, though this is not a requirement.
|
||||||
|
// - If this field is set to FFh, then the device uses a vendor-specific protocol.
|
||||||
|
device_protocol: u8,
|
||||||
|
// Maximum packet size for endpoint zero (8, 16, 32, or 64 are the only valid options)
|
||||||
|
max_packet_size_0: u8,
|
||||||
|
// Vendor ID (assigned by the USB-IF)
|
||||||
|
vendor_id: u16 align(1),
|
||||||
|
// Product ID (assigned by the USB-IF)
|
||||||
|
product_id: u16 align(1),
|
||||||
|
// Device release number in binary-coded decimal
|
||||||
|
bcd_device: u16 align(1),
|
||||||
|
// Index of STRING descriptor describing manufacturer
|
||||||
|
manufacturer_index: StringIndex,
|
||||||
|
// Index of STRING descriptor describing product
|
||||||
|
product_index: StringIndex,
|
||||||
|
// Index of STRING descriptor describing the device's serial number
|
||||||
|
serial_number_index: StringIndex,
|
||||||
|
// Number of possible configurations
|
||||||
|
configuration_count: u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DeviceQualifierDescriptor = extern struct {
|
||||||
|
// Size of this descriptor in bytes
|
||||||
|
length: u8,
|
||||||
|
// DEVICE_QUALIFIER Descriptor Type
|
||||||
|
descriptor_type: DescriptorType,
|
||||||
|
// USB Specification Release Number in Binary-Coded Decimal (i.e, 2.00 is expressed as 200h).
|
||||||
|
// Identifies the release of the USB Specification with with the device and its
|
||||||
|
// descriptors are compliant. This field must be at least 0200h.
|
||||||
|
bcd_usb: u16 align(1),
|
||||||
|
// Class code (assigned by the USB-IF)
|
||||||
|
device_class: u8,
|
||||||
|
// Subclass Code (assigned by the USB-IF)
|
||||||
|
device_subclass: u8,
|
||||||
|
// Protocol code (assigned by the USB-IF)
|
||||||
|
device_protocol: u8,
|
||||||
|
// Maximum packet size for endpoint zero (8, 16, 32, or 64 are the only valid options)
|
||||||
|
max_packet_size_0: u8,
|
||||||
|
// Number of possible configurations
|
||||||
|
configuration_count: u8,
|
||||||
|
// Reserved for future uses, must be zero.
|
||||||
|
reserved: u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ConfigurationDescriptor = extern struct {
|
||||||
|
// Size of this descriptor in bytes
|
||||||
|
length: u8,
|
||||||
|
// CONFIGURATION Descriptor Type
|
||||||
|
descriptor_type: DescriptorType,
|
||||||
|
// The total combined length in bytes of all the descriptors returned with the request for
|
||||||
|
// this CONFIGURATION descriptor (including CONFIGURATION, INTERFACE, ENDPOINT, class- and
|
||||||
|
// vendor-specific descriptors).
|
||||||
|
total_length: u16 align(1),
|
||||||
|
// Number of interfaces supported by this configuration
|
||||||
|
interface_count: u8,
|
||||||
|
// Value which when used as an argument in the SET_CONFIGURATION request, causes the device
|
||||||
|
// to assume the configuration described by this descriptor.
|
||||||
|
configuration_value: ConfigurationValue,
|
||||||
|
// Index of STRING descriptor describing this configuration.
|
||||||
|
configuration_index: StringIndex,
|
||||||
|
// Configuration Characteristics
|
||||||
|
attributes: Attributes,
|
||||||
|
// Maximum power consumption of this device from the bus when fully operational and using
|
||||||
|
// this configuration. Expressed in units of 2mA (i.e., a value of 50 in this field
|
||||||
|
// indicates 100mA).
|
||||||
|
// - A device reports with the attributes field whether the configuration is bus- or
|
||||||
|
// self-powered, but the device status (retrieved with a GET_STATUS request) reports
|
||||||
|
// whether the device is currently self-powered.
|
||||||
|
// - If a device is disconnected from an external power source, it may not draw more
|
||||||
|
// power from the bus than specified in this field.
|
||||||
|
max_power: u8,
|
||||||
|
|
||||||
|
// Configuration characteristics. Fields are declared least-significant first.
|
||||||
|
pub const Attributes = packed struct(u8) {
|
||||||
|
// Reserved, reset to zero (D4...0)
|
||||||
|
reserved: u5,
|
||||||
|
// Whether Remote Wakeup is supported by this configuration (D5)
|
||||||
|
remote_wakeup: bool,
|
||||||
|
// Self-Powered (D6)
|
||||||
|
// - false: Device runs on power supplied by the bus
|
||||||
|
// - true: Device provides a local power source; if max_power is non-zero, the
|
||||||
|
// device also may use bus power.
|
||||||
|
self_powered: bool,
|
||||||
|
// Reserved, must be set to one for historical reasons (D7)
|
||||||
|
reserved_one: u1,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// This descriptor describes the configuration of a high-speed device if it were operating at
|
||||||
|
// its alternative speed. The structure of the OTHER_SPEED_CONFIGURATION is identical to that
|
||||||
|
// of the CONFIGURATION descriptor; the only difference is that the descriptor_type field
|
||||||
|
// reflects that the descriptor is an OTHER_SPEED_CONFIGURATION descriptor.
|
||||||
|
pub const OtherSpeedConfigurationDescriptor = ConfigurationDescriptor;
|
||||||
|
|
||||||
|
pub const InterfaceDescriptor = extern struct {
|
||||||
|
// Size of this descriptor in bytes
|
||||||
|
length: u8,
|
||||||
|
// INTERFACE Descriptor Type
|
||||||
|
descriptor_type: DescriptorType,
|
||||||
|
// Number of this interface. Zero-based value which identifies the index of this interface
|
||||||
|
// in the array of interfaces supported within a configuration.
|
||||||
|
interface_number: InterfaceNumber,
|
||||||
|
// Value used to select the alternate settings described by this INTERFACE descriptor for
|
||||||
|
// the interface with the interface_number in the previous field. This value is zero if
|
||||||
|
// this descriptor describes the default settings for a particular interface.
|
||||||
|
alternate_setting: AlternateSetting,
|
||||||
|
// Number of endpoints used by this interface, not including endpoint zero.
|
||||||
|
endpoint_count: u8,
|
||||||
|
// Class code (assigned by the USB-IF)
|
||||||
|
// - A value of zero here is reserved for future standardization.
|
||||||
|
// - If this value is FFh, the interface class is vendor-specific.
|
||||||
|
// - All other values are reserved for assignment by the USB-IF.
|
||||||
|
interface_class: u8,
|
||||||
|
// Subclass code (assigned by the USB-IF)
|
||||||
|
// - The subclass code in this field is qualified by the value of the interface_class
|
||||||
|
// field.
|
||||||
|
// - If interface_class is reset to zero, then this field must also be reset to zero.
|
||||||
|
// - If interface_class is not set to the value of FFh, then all values of this field are
|
||||||
|
// reserved for assignment by the USB-IF.
|
||||||
|
interface_subclass: u8,
|
||||||
|
// Protocol code (assigned by the USB-IF)
|
||||||
|
// - The protocol code in this field is qualified by the values of the interface_class
|
||||||
|
// and interface_subclass fields.
|
||||||
|
// - If an interface supports class-specific requests, then this field identifies the
|
||||||
|
// protocols that the device uses as defined by the specifications of the device class.
|
||||||
|
// - If this field is reset to zero, then the device does not use a class-specific
|
||||||
|
// protocol on this interface.
|
||||||
|
// - If this field is set to FFh, then the device uses a vendor-specific protocol on
|
||||||
|
// this interface.
|
||||||
|
interface_protocol: u8,
|
||||||
|
// Index of STRING descriptor describing this interface
|
||||||
|
interface_index: StringIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const EndpointDescriptor = extern struct {
|
||||||
|
// Size of this descriptor in bytes
|
||||||
|
length: u8,
|
||||||
|
// ENDPOINT Descriptor Type
|
||||||
|
descriptor_type: DescriptorType,
|
||||||
|
// The address of the endpoint on the USB device described by this descriptor
|
||||||
|
endpoint_address: Address,
|
||||||
|
// The endpoint's attributes
|
||||||
|
attributes: Attributes,
|
||||||
|
// Maximum packet size that this endpoint is capable of sending or receiving. For
|
||||||
|
// isochronous endpoints, this value is used to reserve bus time; the pipe, however, may
|
||||||
|
// not always use all of the reserved bus time.
|
||||||
|
max_packet_size: MaxPacketSize align(1),
|
||||||
|
// Interval for polling a device during a data transfer, expressed in units of microframes
|
||||||
|
// for high-speed devices, and frames for low- and full-speed devices. The exact meaning of
|
||||||
|
// the value in this field depends on the endpoint type and the operating speed of the
|
||||||
|
// device:
|
||||||
|
// - Full- and High-speed isochronous endpoints, and high-speed interrupt endpoints:
|
||||||
|
// This field must be in the range from 1 to 16, and is used to calculate the period
|
||||||
|
// as 2^(interval - 1). That is, a value of 4 calculates to 2^(4 - 1) = 2^3 = 8.
|
||||||
|
// - Full- and Low-speed interrupt endpoints: This field must be in the range from
|
||||||
|
// 1 to 255.
|
||||||
|
// - High-speed bulk and control OUT endpoints: This field must be in the range from
|
||||||
|
// 0 to 255, and specifies the maximum NAK rate of the endpoint. A value of zero
|
||||||
|
// indicates that the endpoint never NAKs; other values indicate at most 1 NAK each
|
||||||
|
// interval number of microframes.
|
||||||
|
interval: u8,
|
||||||
|
|
||||||
|
// The address of an endpoint. Fields are declared least-significant first.
|
||||||
|
pub const Address = packed struct(u8) {
|
||||||
|
// Endpoint Number (D3...0)
|
||||||
|
number: EndpointNumber,
|
||||||
|
// Reserved, reset to zero (D6...4)
|
||||||
|
reserved: u3,
|
||||||
|
// Direction, ignored for control endpoints (D7)
|
||||||
|
direction: EndpointDirection,
|
||||||
|
};
|
||||||
|
|
||||||
|
// An endpoint's attributes. Fields are declared least-significant first.
|
||||||
|
pub const Attributes = packed struct(u8) {
|
||||||
|
// Transfer Type (D1...0)
|
||||||
|
transfer_type: TransferType,
|
||||||
|
// Synchronization Type; isochronous endpoints only, reserved and reset to zero for
|
||||||
|
// other endpoint types (D3...2)
|
||||||
|
synchronization: Synchronization,
|
||||||
|
// Usage Type; isochronous endpoints only, reserved and reset to zero for other
|
||||||
|
// endpoints (D5...4)
|
||||||
|
usage: Usage,
|
||||||
|
// Reserved, reset to zero (D7...6)
|
||||||
|
reserved: u2,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TransferType = enum(u2) {
|
||||||
|
control = 0,
|
||||||
|
isochronous = 1,
|
||||||
|
bulk = 2,
|
||||||
|
interrupt = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Synchronization = enum(u2) {
|
||||||
|
none = 0,
|
||||||
|
asynchronous = 1,
|
||||||
|
adaptive = 2,
|
||||||
|
synchronous = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Usage = enum(u2) {
|
||||||
|
data = 0,
|
||||||
|
feedback = 1,
|
||||||
|
implicit_feedback_data = 2,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The maximum packet size of an endpoint. Fields are declared least-significant first.
|
||||||
|
pub const MaxPacketSize = packed struct(u16) {
|
||||||
|
// Maximum packet size in bytes (bits 10...0)
|
||||||
|
size: u11,
|
||||||
|
// Number of additional transaction opportunities per microframe, for high-speed
|
||||||
|
// isochronous and interrupt endpoints; reserved and reset to zero for other
|
||||||
|
// endpoints (bits 12...11)
|
||||||
|
additional_transactions: AdditionalTransactions,
|
||||||
|
// Reserved, must be reset to zero (bits 15...13)
|
||||||
|
reserved: u3,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const AdditionalTransactions = enum(u2) {
|
||||||
|
// None (1 transaction per microframe)
|
||||||
|
none = 0,
|
||||||
|
// 1 additional (2 transactions per microframe)
|
||||||
|
one = 1,
|
||||||
|
// 2 additional (3 transactions per microframe)
|
||||||
|
two = 2,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// A STRING descriptor at index zero returns the list of LANGID codes supported by the
|
||||||
|
// device; all other indices return a Unicode string. Both forms start with this two-byte
|
||||||
|
// header, followed by the variable-length payload:
|
||||||
|
// - index 0: an array of two-byte LANGID codes (wLangID[0] through wLangID[x])
|
||||||
|
// - other indices: a Unicode string of N bytes
|
||||||
|
pub const StringDescriptor = extern struct {
|
||||||
|
// Size of this descriptor in bytes
|
||||||
|
length: u8,
|
||||||
|
// STRING Descriptor Type
|
||||||
|
descriptor_type: DescriptorType,
|
||||||
|
};
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
test "wire sizes and offsets match the specification" {
|
||||||
|
const expectEqual = std.testing.expectEqual;
|
||||||
|
|
||||||
|
try expectEqual(8, @sizeOf(Request));
|
||||||
|
try expectEqual(18, @sizeOf(DeviceDescriptor));
|
||||||
|
try expectEqual(10, @sizeOf(DeviceQualifierDescriptor));
|
||||||
|
try expectEqual(9, @sizeOf(ConfigurationDescriptor));
|
||||||
|
try expectEqual(9, @sizeOf(InterfaceDescriptor));
|
||||||
|
try expectEqual(7, @sizeOf(EndpointDescriptor));
|
||||||
|
try expectEqual(2, @sizeOf(StringDescriptor));
|
||||||
|
|
||||||
|
try expectEqual(2, @offsetOf(DeviceDescriptor, "bcd_usb"));
|
||||||
|
try expectEqual(8, @offsetOf(DeviceDescriptor, "vendor_id"));
|
||||||
|
try expectEqual(17, @offsetOf(DeviceDescriptor, "configuration_count"));
|
||||||
|
try expectEqual(2, @offsetOf(ConfigurationDescriptor, "total_length"));
|
||||||
|
try expectEqual(4, @offsetOf(EndpointDescriptor, "max_packet_size"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "bitmap packings match the specification" {
|
||||||
|
const expectEqual = std.testing.expectEqual;
|
||||||
|
const expect = std.testing.expect;
|
||||||
|
|
||||||
|
// bmRequestType for GET_DESCRIPTOR: device-to-host | standard | device = 80h
|
||||||
|
const request_type = RequestType{
|
||||||
|
.recipient = .device,
|
||||||
|
.kind = .standard,
|
||||||
|
.direction = .device_to_host,
|
||||||
|
};
|
||||||
|
try expectEqual(0x80, @as(u8, @bitCast(request_type)));
|
||||||
|
|
||||||
|
// wValue for GET_DESCRIPTOR(CONFIGURATION, index 0) = 0200h
|
||||||
|
const descriptor_value = Request.DescriptorValue{ .kind = .configuration };
|
||||||
|
try expectEqual(0x0200, @as(u16, @bitCast(descriptor_value)));
|
||||||
|
|
||||||
|
// wIndex for the IN endpoint 1 = 0081h
|
||||||
|
const endpoint_index = Request.EndpointIndex{ .number = @enumFromInt(1), .direction = .in };
|
||||||
|
try expectEqual(0x0081, @as(u16, @bitCast(endpoint_index)));
|
||||||
|
|
||||||
|
// Endpoint address 81h = IN endpoint 1
|
||||||
|
const address: EndpointDescriptor.Address = @bitCast(@as(u8, 0x81));
|
||||||
|
try expectEqual(1, @intFromEnum(address.number));
|
||||||
|
try expectEqual(.in, address.direction);
|
||||||
|
|
||||||
|
// Endpoint attributes 03h = interrupt transfer
|
||||||
|
const attributes: EndpointDescriptor.Attributes = @bitCast(@as(u8, 0x03));
|
||||||
|
try expectEqual(.interrupt, attributes.transfer_type);
|
||||||
|
|
||||||
|
// wMaxPacketSize 0008h = 8 bytes, no additional transactions
|
||||||
|
const max_packet_size: EndpointDescriptor.MaxPacketSize = @bitCast(@as(u16, 0x0008));
|
||||||
|
try expectEqual(8, max_packet_size.size);
|
||||||
|
try expectEqual(.none, max_packet_size.additional_transactions);
|
||||||
|
|
||||||
|
// Configuration attributes C0h = self-powered, with the historical D7 bit set
|
||||||
|
const configuration_attributes: ConfigurationDescriptor.Attributes = @bitCast(@as(u8, 0xC0));
|
||||||
|
try expect(configuration_attributes.self_powered);
|
||||||
|
try expect(!configuration_attributes.remote_wakeup);
|
||||||
|
try expectEqual(1, configuration_attributes.reserved_one);
|
||||||
|
|
||||||
|
// GET_STATUS words: device 0001h = self-powered; endpoint 0001h = halted
|
||||||
|
const device_status: DeviceStatus = @bitCast(@as(u16, 0x0001));
|
||||||
|
try expect(device_status.self_powered and !device_status.remote_wakeup);
|
||||||
|
const endpoint_status: EndpointStatus = @bitCast(@as(u16, 0x0001));
|
||||||
|
try expect(endpoint_status.halted);
|
||||||
|
|
||||||
|
// DescriptorType is non-exhaustive: class-specific values (HID = 21h) pass through
|
||||||
|
const hid_type: DescriptorType = @enumFromInt(0x21);
|
||||||
|
try expectEqual(0x21, @intFromEnum(hid_type));
|
||||||
|
try expect(hid_type != .device);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expectRequestBytes(request: Request, expected: [8]u8) !void {
|
||||||
|
try std.testing.expectEqualSlices(u8, &expected, std.mem.asBytes(&request));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "standard request constructors encode the specification's set-up packets" {
|
||||||
|
try expectRequestBytes(getStatus(.device), .{ 0x80, 0, 0, 0, 0, 0, 2, 0 });
|
||||||
|
try expectRequestBytes(getStatus(.{ .interface = @enumFromInt(3) }), .{ 0x81, 0, 0, 0, 3, 0, 2, 0 });
|
||||||
|
try expectRequestBytes(getStatus(.{ .endpoint = .{ .number = @enumFromInt(2), .direction = .in } }), .{ 0x82, 0, 0, 0, 0x82, 0, 2, 0 });
|
||||||
|
try expectRequestBytes(clearFeature(.endpoint_halt, .{ .endpoint = .{ .number = @enumFromInt(1), .direction = .out } }), .{ 0x02, 1, 0, 0, 0x01, 0, 0, 0 });
|
||||||
|
try expectRequestBytes(setFeature(.device_remote_wakeup, .device), .{ 0x00, 3, 1, 0, 0, 0, 0, 0 });
|
||||||
|
try expectRequestBytes(setTestMode(.test_packet), .{ 0x00, 3, 2, 0, 0, 0x04, 0, 0 });
|
||||||
|
try expectRequestBytes(setAddress(@enumFromInt(5)), .{ 0x00, 5, 5, 0, 0, 0, 0, 0 });
|
||||||
|
try expectRequestBytes(getDescriptor(.device, 0, 0, 18), .{ 0x80, 6, 0, 1, 0, 0, 18, 0 });
|
||||||
|
try expectRequestBytes(getDescriptor(.string, 2, 0x0409, 255), .{ 0x80, 6, 2, 3, 0x09, 0x04, 255, 0 });
|
||||||
|
try expectRequestBytes(setDescriptor(.string, 2, 0x0409, 16), .{ 0x00, 7, 2, 3, 0x09, 0x04, 16, 0 });
|
||||||
|
try expectRequestBytes(getConfiguration(), .{ 0x80, 8, 0, 0, 0, 0, 1, 0 });
|
||||||
|
try expectRequestBytes(setConfiguration(@enumFromInt(1)), .{ 0x00, 9, 1, 0, 0, 0, 0, 0 });
|
||||||
|
try expectRequestBytes(getInterface(@enumFromInt(2)), .{ 0x81, 10, 0, 0, 2, 0, 1, 0 });
|
||||||
|
try expectRequestBytes(setInterface(@enumFromInt(2), @enumFromInt(1)), .{ 0x01, 11, 1, 0, 2, 0, 0, 0 });
|
||||||
|
try expectRequestBytes(syncFrame(.{ .number = @enumFromInt(3), .direction = .in }), .{ 0x82, 12, 0, 0, 0x83, 0, 2, 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
test "class request constructors encode the specification's set-up packets" {
|
||||||
|
// bmRequestType for a host-to-device class request to an interface = 0x21;
|
||||||
|
// device-to-host = 0xA1. The request_code byte is the class code, not a
|
||||||
|
// standard one — SET_PROTOCOL 0x0B, SET_IDLE 0x0A, BOT reset 0xFF, Max LUN 0xFE.
|
||||||
|
try expectRequestBytes(setProtocol(@enumFromInt(0), .boot), .{ 0x21, 0x0B, 0, 0, 0, 0, 0, 0 });
|
||||||
|
try expectRequestBytes(setProtocol(@enumFromInt(1), .report), .{ 0x21, 0x0B, 1, 0, 1, 0, 0, 0 });
|
||||||
|
try expectRequestBytes(setIdle(@enumFromInt(1), 0, 0), .{ 0x21, 0x0A, 0, 0, 1, 0, 0, 0 });
|
||||||
|
try expectRequestBytes(bulkOnlyMassStorageReset(@enumFromInt(0)), .{ 0x21, 0xFF, 0, 0, 0, 0, 0, 0 });
|
||||||
|
try expectRequestBytes(getMaxLun(@enumFromInt(0)), .{ 0xA1, 0xFE, 0, 0, 0, 0, 1, 0 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
//! USB class-code decoding: turn the (class, subclass, protocol) triple a USB device or
|
||||||
|
//! interface reports in its descriptors into typed values. The device descriptor carries one
|
||||||
|
//! triple for the whole device, and each interface descriptor carries its own; a class code
|
||||||
|
//! of zero at the device level defers entirely to the interfaces. Subclass and protocol
|
||||||
|
//! codes are qualified by the class code — the same value means different things under
|
||||||
|
//! different classes — so there is no single SubClass or Protocol enum: each class with
|
||||||
|
//! spec-defined codes gets its own namespace below. Pure reference data (from the USB-IF
|
||||||
|
//! defined class codes; see https://www.usb.org/defined-class-codes) — no hardware access —
|
||||||
|
//! so it is shared by kernel discovery and any user-space tool (device naming, driver
|
||||||
|
//! matching).
|
||||||
|
|
||||||
|
// Base class codes (assigned by the USB-IF). The comment on each value notes where the code
|
||||||
|
// may legally appear: in the device descriptor, in interface descriptors, or both.
|
||||||
|
pub const Class = enum(u8) {
|
||||||
|
// Use class information in the interface descriptors (device descriptor only). Each
|
||||||
|
// interface within a configuration specifies its own class information and the various
|
||||||
|
// interfaces operate independently.
|
||||||
|
per_interface = 0x00,
|
||||||
|
// Audio: speakers, microphones, sound cards (interface)
|
||||||
|
audio = 0x01,
|
||||||
|
// Communications and CDC control: modems, network adapters (both)
|
||||||
|
communications = 0x02,
|
||||||
|
// Human Interface Device: keyboards, mice, game controllers (interface)
|
||||||
|
hid = 0x03,
|
||||||
|
// Physical: force-feedback devices (interface)
|
||||||
|
physical = 0x05,
|
||||||
|
// Image: still-imaging cameras, scanners (interface)
|
||||||
|
image = 0x06,
|
||||||
|
// Printer (interface)
|
||||||
|
printer = 0x07,
|
||||||
|
// Mass storage: flash drives, external disks, card readers (interface)
|
||||||
|
mass_storage = 0x08,
|
||||||
|
// Hub (device descriptor only)
|
||||||
|
hub = 0x09,
|
||||||
|
// CDC-Data: the data interfaces paired with a communications control interface
|
||||||
|
// (interface)
|
||||||
|
cdc_data = 0x0A,
|
||||||
|
// Smart card readers (interface)
|
||||||
|
smart_card = 0x0B,
|
||||||
|
// Content security (interface)
|
||||||
|
content_security = 0x0D,
|
||||||
|
// Video: webcams (interface)
|
||||||
|
video = 0x0E,
|
||||||
|
// Personal healthcare devices (interface)
|
||||||
|
personal_healthcare = 0x0F,
|
||||||
|
// Audio/Video devices (interface)
|
||||||
|
audio_video = 0x10,
|
||||||
|
// Billboard: describes alternate modes a USB Type-C device supports (device descriptor
|
||||||
|
// only)
|
||||||
|
billboard = 0x11,
|
||||||
|
// USB Type-C bridge (interface)
|
||||||
|
type_c_bridge = 0x12,
|
||||||
|
// USB Bulk Display Protocol devices (interface)
|
||||||
|
bulk_display = 0x13,
|
||||||
|
// MCTP over USB protocol endpoint devices (interface)
|
||||||
|
mctp = 0x14,
|
||||||
|
// I3C devices (interface)
|
||||||
|
i3c = 0x3C,
|
||||||
|
// Diagnostic devices (both)
|
||||||
|
diagnostic = 0xDC,
|
||||||
|
// Wireless controllers: Bluetooth adapters (interface)
|
||||||
|
wireless_controller = 0xE0,
|
||||||
|
// Miscellaneous (both)
|
||||||
|
miscellaneous = 0xEF,
|
||||||
|
// Application-specific: firmware upgrade, IrDA bridges, test and measurement
|
||||||
|
// (interface)
|
||||||
|
application_specific = 0xFE,
|
||||||
|
// Vendor-specific (both)
|
||||||
|
vendor_specific = 0xFF,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subclass and protocol codes qualified by Class.hub. Hubs have no subclass codes; the
|
||||||
|
// protocol distinguishes the hub's transaction-translator arrangement.
|
||||||
|
pub const hub = struct {
|
||||||
|
pub const Protocol = enum(u8) {
|
||||||
|
// Full-speed hub
|
||||||
|
full_speed = 0x00,
|
||||||
|
// Hi-speed hub with a single transaction translator
|
||||||
|
hi_speed_single_tt = 0x01,
|
||||||
|
// Hi-speed hub with multiple transaction translators
|
||||||
|
hi_speed_multi_tt = 0x02,
|
||||||
|
// SuperSpeed hub (USB 3)
|
||||||
|
super_speed = 0x03,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subclass and protocol codes qualified by Class.hid.
|
||||||
|
pub const hid = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
// No subclass
|
||||||
|
none = 0x00,
|
||||||
|
// Boot interface: the device also supports the simplified boot protocol, usable by
|
||||||
|
// firmware before a full HID report-descriptor parser is available
|
||||||
|
boot = 0x01,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only meaningful when the subclass is boot
|
||||||
|
pub const Protocol = enum(u8) {
|
||||||
|
none = 0x00,
|
||||||
|
keyboard = 0x01,
|
||||||
|
mouse = 0x02,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subclass and protocol codes qualified by Class.mass_storage. The subclass identifies the
|
||||||
|
// command set the device understands; the protocol identifies the transport used to carry
|
||||||
|
// commands, data, and status over the bus.
|
||||||
|
pub const mass_storage = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
// SCSI command set not reported; de facto, treat as scsi
|
||||||
|
not_reported = 0x00,
|
||||||
|
// Reduced Block Commands: typically flash devices
|
||||||
|
rbc = 0x01,
|
||||||
|
// MMC-5 (ATAPI): CD and DVD drives
|
||||||
|
atapi = 0x02,
|
||||||
|
// QIC-157 tape drives (obsolete)
|
||||||
|
qic_157 = 0x03,
|
||||||
|
// UFI: floppy disk drives
|
||||||
|
ufi = 0x04,
|
||||||
|
// SFF-8070i (obsolete)
|
||||||
|
sff_8070i = 0x05,
|
||||||
|
// Transparent SCSI command set: the common case for flash drives and disks
|
||||||
|
scsi = 0x06,
|
||||||
|
// LSD FS: negotiated access to large storage devices
|
||||||
|
lsd_fs = 0x07,
|
||||||
|
// IEEE 1667
|
||||||
|
ieee_1667 = 0x08,
|
||||||
|
// Vendor-specific
|
||||||
|
vendor_specific = 0xFF,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Protocol = enum(u8) {
|
||||||
|
// Control/Bulk/Interrupt with command completion interrupt
|
||||||
|
cbi_completion_interrupt = 0x00,
|
||||||
|
// Control/Bulk/Interrupt without command completion interrupt
|
||||||
|
cbi = 0x01,
|
||||||
|
// Bulk-only transport: the common case for flash drives and disks
|
||||||
|
bulk_only = 0x50,
|
||||||
|
// USB attached SCSI
|
||||||
|
uas = 0x62,
|
||||||
|
// Vendor-specific
|
||||||
|
vendor_specific = 0xFF,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subclass and protocol codes qualified by Class.communications (CDC). The protocol codes
|
||||||
|
// are model-specific; the useful invariant is the subclass, which selects the control model
|
||||||
|
// the interface implements.
|
||||||
|
pub const communications = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
// Direct line control model
|
||||||
|
direct_line = 0x01,
|
||||||
|
// Abstract control model: USB modems and serial adapters
|
||||||
|
abstract_control = 0x02,
|
||||||
|
// Telephone control model
|
||||||
|
telephone = 0x03,
|
||||||
|
// Multi-channel control model
|
||||||
|
multi_channel = 0x04,
|
||||||
|
// CAPI control model
|
||||||
|
capi = 0x05,
|
||||||
|
// Ethernet networking control model
|
||||||
|
ethernet = 0x06,
|
||||||
|
// ATM networking control model
|
||||||
|
atm = 0x07,
|
||||||
|
// Wireless handset control model
|
||||||
|
wireless_handset = 0x08,
|
||||||
|
// Device management
|
||||||
|
device_management = 0x09,
|
||||||
|
// Mobile direct line model
|
||||||
|
mobile_direct_line = 0x0A,
|
||||||
|
// OBEX
|
||||||
|
obex = 0x0B,
|
||||||
|
// Ethernet emulation model
|
||||||
|
ethernet_emulation = 0x0C,
|
||||||
|
// Network control model
|
||||||
|
network_control = 0x0D,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subclass and protocol codes qualified by Class.wireless_controller.
|
||||||
|
pub const wireless_controller = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
// Radio frequency controllers
|
||||||
|
radio_frequency = 0x01,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only meaningful when the subclass is radio_frequency
|
||||||
|
pub const Protocol = enum(u8) {
|
||||||
|
// Bluetooth programming interface
|
||||||
|
bluetooth = 0x01,
|
||||||
|
// Ultra-wideband radio control
|
||||||
|
ultra_wideband = 0x02,
|
||||||
|
// Remote NDIS
|
||||||
|
remote_ndis = 0x03,
|
||||||
|
// Bluetooth AMP controller
|
||||||
|
bluetooth_amp = 0x04,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subclass and protocol codes qualified by Class.miscellaneous.
|
||||||
|
pub const miscellaneous = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
// Common class
|
||||||
|
common = 0x02,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only meaningful when the subclass is common
|
||||||
|
pub const Protocol = enum(u8) {
|
||||||
|
// Interface association descriptor: at the device level, announces that the
|
||||||
|
// configuration groups interfaces into functions with IADs
|
||||||
|
interface_association = 0x01,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subclass and protocol codes qualified by Class.application_specific.
|
||||||
|
pub const application_specific = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
// Device firmware upgrade
|
||||||
|
firmware_upgrade = 0x01,
|
||||||
|
// IrDA bridge
|
||||||
|
irda_bridge = 0x02,
|
||||||
|
// Test and measurement
|
||||||
|
test_and_measurement = 0x03,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Pack a (class, subclass, protocol) triple into one 0xCCSSPP value — the
|
||||||
|
/// bus-native identity a USB bus driver reports in `ChildAdded.identity` and the
|
||||||
|
/// device manager matches on (the USB analog of a packed PCI class code). Mirrors
|
||||||
|
/// `pci_class.ClassCode.pack`, so both sides build/decode the identical u64.
|
||||||
|
pub fn packTriple(class: u8, subclass: u8, protocol: u8) u64 {
|
||||||
|
return (@as(u64, class) << 16) | (@as(u64, subclass) << 8) | protocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The inverse of `packTriple`.
|
||||||
|
pub fn unpackTriple(triple: u64) struct { class: u8, subclass: u8, protocol: u8 } {
|
||||||
|
return .{
|
||||||
|
.class = @truncate(triple >> 16),
|
||||||
|
.subclass = @truncate(triple >> 8),
|
||||||
|
.protocol = @truncate(triple),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test "class codes match the USB-IF assignments" {
|
||||||
|
const std = @import("std");
|
||||||
|
const expectEqual = std.testing.expectEqual;
|
||||||
|
|
||||||
|
try expectEqual(0x03, @intFromEnum(Class.hid));
|
||||||
|
try expectEqual(0x09, @intFromEnum(Class.hub));
|
||||||
|
try expectEqual(0xFF, @intFromEnum(Class.vendor_specific));
|
||||||
|
|
||||||
|
// A typical flash drive: mass storage, transparent SCSI, bulk-only transport.
|
||||||
|
try expectEqual(0x06, @intFromEnum(mass_storage.SubClass.scsi));
|
||||||
|
try expectEqual(0x50, @intFromEnum(mass_storage.Protocol.bulk_only));
|
||||||
|
|
||||||
|
// A boot keyboard: HID, boot subclass, keyboard protocol.
|
||||||
|
try expectEqual(0x01, @intFromEnum(hid.SubClass.boot));
|
||||||
|
try expectEqual(0x01, @intFromEnum(hid.Protocol.keyboard));
|
||||||
|
|
||||||
|
// Class codes are non-exhaustive: unlisted values pass through undamaged.
|
||||||
|
const unknown: Class = @enumFromInt(0x42);
|
||||||
|
try expectEqual(0x42, @intFromEnum(unknown));
|
||||||
|
|
||||||
|
_ = hub.Protocol.hi_speed_multi_tt;
|
||||||
|
_ = communications.SubClass.abstract_control;
|
||||||
|
_ = wireless_controller.Protocol.bluetooth;
|
||||||
|
_ = miscellaneous.Protocol.interface_association;
|
||||||
|
_ = application_specific.SubClass.firmware_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "packTriple / unpackTriple round-trip the identity a bus driver reports" {
|
||||||
|
const std = @import("std");
|
||||||
|
const expectEqual = std.testing.expectEqual;
|
||||||
|
|
||||||
|
// A boot keyboard interface: HID / boot / keyboard.
|
||||||
|
const keyboard = packTriple(
|
||||||
|
@intFromEnum(Class.hid),
|
||||||
|
@intFromEnum(hid.SubClass.boot),
|
||||||
|
@intFromEnum(hid.Protocol.keyboard),
|
||||||
|
);
|
||||||
|
try expectEqual(@as(u64, 0x03_01_01), keyboard);
|
||||||
|
|
||||||
|
// A flash drive interface: mass storage / SCSI / bulk-only.
|
||||||
|
const storage = packTriple(
|
||||||
|
@intFromEnum(Class.mass_storage),
|
||||||
|
@intFromEnum(mass_storage.SubClass.scsi),
|
||||||
|
@intFromEnum(mass_storage.Protocol.bulk_only),
|
||||||
|
);
|
||||||
|
try expectEqual(@as(u64, 0x08_06_50), storage);
|
||||||
|
|
||||||
|
const parts = unpackTriple(storage);
|
||||||
|
try expectEqual(@as(u8, 0x08), parts.class);
|
||||||
|
try expectEqual(@as(u8, 0x06), parts.subclass);
|
||||||
|
try expectEqual(@as(u8, 0x50), parts.protocol);
|
||||||
|
}
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
//! /system/drivers/bus — a user-space **bus driver**, and the smallest honest example of one.
|
|
||||||
//!
|
|
||||||
//! A bus driver owns a device that *contains other devices*, enumerates them by some
|
|
||||||
//! bus-specific protocol, and publishes each one into the kernel's device table so a
|
|
||||||
//! class driver can claim it. PCI walks configuration space; USB walks hub descriptors. Here
|
|
||||||
//! the "bus" is the HPET's register block and the "devices" are its comparators, each
|
|
||||||
//! a 0x20-byte window at 0x100 + 0x20*n that can be driven independently.
|
|
||||||
//!
|
|
||||||
//! It's a toy bus, but nothing about the mechanism is: `bus` reads how many children
|
|
||||||
//! exist from the hardware (GENERAL_CAP bits [12:8]), publishes one `DeviceDescriptor` per
|
|
||||||
//! child with a sub-window of its own MMIO plus the shared IRQ, and the kernel checks
|
|
||||||
//! every one of those resources is contained in what `bus` was granted. A comparator
|
|
||||||
//! driver then claims a child and maps only *its* registers — not the whole block.
|
|
||||||
//!
|
|
||||||
//! It also proves the negative: registering a child whose window escapes the parent's
|
|
||||||
//! is refused. Without that check, `device_register` would be a system_call for mapping
|
|
||||||
//! arbitrary physical memory.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const runtime = @import("runtime");
|
|
||||||
const device = runtime.device;
|
|
||||||
|
|
||||||
const register_general_cap = 0x000;
|
|
||||||
|
|
||||||
/// Comparator n's registers: configuration+comparator+FSB route, 0x20 bytes.
|
|
||||||
fn timerWindow(hpet_base: u64, n: u64) device.ResourceDescriptor {
|
|
||||||
return .{
|
|
||||||
.kind = @intFromEnum(device.ResourceKind.memory),
|
|
||||||
.start = hpet_base + 0x100 + 0x20 * n,
|
|
||||||
.len = 0x20,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn findHpet(buffer: []device.DeviceDescriptor) ?device.DeviceDescriptor {
|
|
||||||
const total = device.enumerate(buffer);
|
|
||||||
const n = @min(total, buffer.len);
|
|
||||||
for (buffer[0..n]) |d| {
|
|
||||||
if (d.class != @intFromEnum(device.DeviceClass.timer)) continue;
|
|
||||||
if (d.parent != device.no_parent) continue; // the block, not a comparator child
|
|
||||||
for (0..d.resource_count) |j| {
|
|
||||||
if (d.resources[j].kind == @intFromEnum(device.ResourceKind.memory)) return d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The parent's MMIO resource, and its IRQ if it has one.
|
|
||||||
fn resourcesOf(d: device.DeviceDescriptor) struct { mmio: device.ResourceDescriptor, irq: ?device.ResourceDescriptor } {
|
|
||||||
var mmio: device.ResourceDescriptor = undefined;
|
|
||||||
var irq: ?device.ResourceDescriptor = null;
|
|
||||||
for (0..d.resource_count) |j| {
|
|
||||||
const r = d.resources[j];
|
|
||||||
if (r.kind == @intFromEnum(device.ResourceKind.memory)) mmio = r;
|
|
||||||
if (r.kind == @intFromEnum(device.ResourceKind.irq)) irq = r;
|
|
||||||
}
|
|
||||||
return .{ .mmio = mmio, .irq = irq };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn firstChildOf(buffer: []device.DeviceDescriptor, total: usize, parent_id: u64) ?u64 {
|
|
||||||
for (buffer[0..@min(total, buffer.len)]) |d| {
|
|
||||||
if (d.parent == parent_id) return d.id;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main() void {
|
|
||||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
|
||||||
_ = runtime.system.write("bus: out of memory\n");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parent = findHpet(buffer) orelse {
|
|
||||||
_ = runtime.system.write("bus: no HPET\n");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
const resource = resourcesOf(parent);
|
|
||||||
|
|
||||||
// Claim the bus. Everything below is subdivision of what this claim granted.
|
|
||||||
//
|
|
||||||
// Claims are exclusive, and at a normal boot the kernel spawns every initial_ramdisk
|
|
||||||
// binary — so hpet may own the HPET already. That's not an error, it's the
|
|
||||||
// capability model working: exit quietly and leave the device to its owner. The
|
|
||||||
// `bus` test spawns bus alone, so there it wins the claim.
|
|
||||||
if (!device.claim(parent.id)) {
|
|
||||||
_ = runtime.system.write("bus: HPET already claimed by another driver, nothing to do\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enumerate the bus: ask the hardware how many children it has.
|
|
||||||
const base = device.mmioMap(parent.id, 0) orelse {
|
|
||||||
_ = runtime.system.write("bus: mmio_map failed\n");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
const cap: *volatile u64 = @ptrFromInt(base + register_general_cap);
|
|
||||||
const n_children = ((cap.* >> 8) & 0x1F) + 1;
|
|
||||||
|
|
||||||
// Publish one child per comparator, each owning only its own window.
|
|
||||||
var published: u64 = 0;
|
|
||||||
var n: u64 = 0;
|
|
||||||
while (n < n_children) : (n += 1) {
|
|
||||||
var child = std.mem.zeroes(device.DeviceDescriptor);
|
|
||||||
child.class = @intFromEnum(device.DeviceClass.timer);
|
|
||||||
child.hid_len = 6;
|
|
||||||
child.hid[0..6].* = "hpet-t".*;
|
|
||||||
child.resource_count = 1;
|
|
||||||
child.resources[0] = timerWindow(resource.mmio.start, n);
|
|
||||||
// Comparators share the block's interrupt line; only one child can bind it,
|
|
||||||
// but all of them may legitimately name it.
|
|
||||||
if (resource.irq) |i| {
|
|
||||||
child.resources[child.resource_count] = i;
|
|
||||||
child.resource_count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device.register(parent.id, &child) == null) {
|
|
||||||
_ = runtime.system.write("bus: register failed\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
published += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The negative case. A window one byte past the end of the parent's must be
|
|
||||||
// refused — otherwise device_register would be "map any physical page you like".
|
|
||||||
// Confirm the table did not grow, not merely that the call returned null: null
|
|
||||||
// also means NoSpace/BadParent, so a size check is what actually proves the
|
|
||||||
// *containment* rule fired.
|
|
||||||
const before = device.enumerate(buffer);
|
|
||||||
var rogue = std.mem.zeroes(device.DeviceDescriptor);
|
|
||||||
rogue.class = @intFromEnum(device.DeviceClass.unknown);
|
|
||||||
rogue.resource_count = 1;
|
|
||||||
rogue.resources[0] = .{
|
|
||||||
.kind = @intFromEnum(device.ResourceKind.memory),
|
|
||||||
.start = resource.mmio.start + resource.mmio.len,
|
|
||||||
.len = 0x1000,
|
|
||||||
};
|
|
||||||
if (device.register(parent.id, &rogue) != null) {
|
|
||||||
_ = runtime.system.write("bus: FAIL out-of-window child was accepted\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (device.enumerate(buffer) != before) {
|
|
||||||
_ = runtime.system.write("bus: FAIL rogue child leaked into the table\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// And confirm the children came back with the right parent and a *narrower*
|
|
||||||
// window than the bus — read from the table, not from our own memory.
|
|
||||||
const total = device.enumerate(buffer);
|
|
||||||
var seen: u64 = 0;
|
|
||||||
for (buffer[0..@min(total, buffer.len)]) |d| {
|
|
||||||
if (d.parent != parent.id) continue;
|
|
||||||
const w = d.resources[0];
|
|
||||||
if (w.start < resource.mmio.start or w.len >= resource.mmio.len) {
|
|
||||||
_ = runtime.system.write("bus: FAIL child window is not inside the bus\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
seen += 1;
|
|
||||||
}
|
|
||||||
if (seen != published) {
|
|
||||||
_ = runtime.system.write("bus: FAIL child count mismatch\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delegation, end to end: claim a child and map *it*. A real class driver would be
|
|
||||||
// a different process; here bus plays both parts, which exercises the same path.
|
|
||||||
// The child's window is 0x20 bytes at parent+0x100, so the register it sees at
|
|
||||||
// offset 0 must be the same timer-0 configuration register the bus sees at 0x100.
|
|
||||||
//
|
|
||||||
// (mmio_map rounds to a page, so the child's mapping physically covers the whole
|
|
||||||
// 4 KiB the HPET lives in — the granularity limit documented in docs/drivers.md.
|
|
||||||
// The *resource* is narrow even though the page isn't.)
|
|
||||||
const child_id = firstChildOf(buffer, device.enumerate(buffer), parent.id) orelse {
|
|
||||||
_ = runtime.system.write("bus: FAIL no child to claim\n");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if (!device.claim(child_id)) {
|
|
||||||
_ = runtime.system.write("bus: FAIL could not claim own child\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const child_base = device.mmioMap(child_id, 0) orelse {
|
|
||||||
_ = runtime.system.write("bus: FAIL child mmio_map refused\n");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
const via_child: *volatile u64 = @ptrFromInt(child_base);
|
|
||||||
const via_bus: *volatile u64 = @ptrFromInt(base + 0x100);
|
|
||||||
if (via_child.* != via_bus.*) {
|
|
||||||
_ = runtime.system.write("bus: FAIL child window does not alias the bus register\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A descriptor pointer into an unmapped page must fail the call, not fault the
|
|
||||||
// kernel. Grab a page, free it, and register through the stale address: if the
|
|
||||||
// kernel dereferenced it raw (rather than copying in through the page tables) this
|
|
||||||
// would triple-fault QEMU and the test would time out instead of printing ok.
|
|
||||||
const scratch = runtime.system.mmap(0x1000, runtime.system.PROT_READ | runtime.system.PROT_WRITE);
|
|
||||||
if (!runtime.system.mmapFailed(scratch)) {
|
|
||||||
_ = runtime.system.munmap(scratch, 0x1000);
|
|
||||||
const descriptor: *const device.DeviceDescriptor = @ptrFromInt(scratch);
|
|
||||||
if (device.register(parent.id, descriptor) != null) {
|
|
||||||
_ = runtime.system.write("bus: FAIL register accepted an unmapped descriptor\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = runtime.system.write("bus: ok\n");
|
|
||||||
while (true) runtime.system.sleep(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const panic = runtime.panic;
|
|
||||||
comptime {
|
|
||||||
_ = &runtime.start._start;
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
//! /system/drivers/hpet — a user-space HPET driver. It proves the whole driver model end to
|
|
||||||
//! end: enumerate the device table, find the HPET, claim it, map its registers into
|
|
||||||
//! this ring-3 address space (strong-uncacheable), **bind its interrupt to an IPC
|
|
||||||
//! endpoint**, then sit blocked in `replyWait` until the hardware wakes it.
|
|
||||||
//!
|
|
||||||
//! Nothing here polls. Between interrupts the process is `.blocked` and off every
|
|
||||||
//! scheduler queue; the core runs other work or idles. That is the point of the
|
|
||||||
//! exercise — a driver is a process that sleeps until its device has something to
|
|
||||||
//! say (see docs/drivers.md).
|
|
||||||
//!
|
|
||||||
//! The comparator is configured **level-triggered** on purpose. Edge would be
|
|
||||||
//! simpler, but level is the discipline every real device line needs, and it forces
|
|
||||||
//! the full cycle to be correct:
|
|
||||||
//!
|
|
||||||
//! kernel ISR mask the GSI -> EOI -> notify this endpoint
|
|
||||||
//! hpet wake, clear GENERAL_INT_STATUS (deasserts the line), re-arm
|
|
||||||
//! hpet irq_ack -> kernel unmasks the GSI
|
|
||||||
//!
|
|
||||||
//! Clear the status bit *before* acking, or the line is still asserted when the
|
|
||||||
//! kernel unmasks and the I/O APIC redelivers forever.
|
|
||||||
//!
|
|
||||||
//! Register map (HPET spec 1.0a):
|
|
||||||
//! 0x000 GENERAL_CAP [63:32] fs per tick, [12:8] number timers - 1
|
|
||||||
//! 0x010 GENERAL_CONFIGURATION bit0 ENABLE_CNF, bit1 LEG_RT_CNF
|
|
||||||
//! 0x020 GENERAL_INT_STATUS bit n = timer n asserted (write 1 to clear)
|
|
||||||
//! 0x0F0 MAIN_COUNTER
|
|
||||||
//! 0x100 TIMER0_CONFIGURATION bit1 INT_TYPE(1=level) bit2 INT_ENB bit3 TYPE(periodic)
|
|
||||||
//! bits[13:9] INT_ROUTE, [63:32] INT_ROUTE_CAP
|
|
||||||
//! 0x108 TIMER0_COMPARATOR
|
|
||||||
|
|
||||||
const runtime = @import("runtime");
|
|
||||||
const mmio = @import("mmio");
|
|
||||||
const device = runtime.device;
|
|
||||||
const ipc = runtime.ipc;
|
|
||||||
|
|
||||||
const register_general_cap = 0x000;
|
|
||||||
const register_general_configuration = 0x010;
|
|
||||||
const register_int_status = 0x020;
|
|
||||||
const register_main_counter = 0x0F0;
|
|
||||||
const register_timer0_configuration = 0x100;
|
|
||||||
const register_timer0_comparator = 0x108;
|
|
||||||
|
|
||||||
const configuration_enable: u64 = 1 << 0; // GENERAL_CONFIGURATION.ENABLE_CNF
|
|
||||||
const configuration_leg_rt: u64 = 1 << 1; // GENERAL_CONFIGURATION.LEG_RT_CNF
|
|
||||||
const tn_int_type_level: u64 = 1 << 1;
|
|
||||||
const tn_int_enb: u64 = 1 << 2;
|
|
||||||
const tn_type_periodic: u64 = 1 << 3;
|
|
||||||
const tn_route_shift = 9;
|
|
||||||
const tn_route_mask: u64 = 0x1F << tn_route_shift;
|
|
||||||
|
|
||||||
/// Interrupts to observe before declaring victory.
|
|
||||||
const target_ticks = 5;
|
|
||||||
|
|
||||||
/// Read/write a 64-bit HPET register through the typed volatile MMIO layer (/lib/mmio).
|
|
||||||
/// The HPET is pure MMIO with no DMA, and on x86 its grant is strong-uncacheable (so
|
|
||||||
/// UC writes are already ordered) — no barriers are needed here; the point is the
|
|
||||||
/// typed, arch-portable access every driver should use.
|
|
||||||
inline fn rd(base: usize, off: usize) u64 {
|
|
||||||
return mmio.read(u64, base + off);
|
|
||||||
}
|
|
||||||
inline fn wr(base: usize, off: usize, value: u64) void {
|
|
||||||
mmio.write(u64, base + off, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A timer-class device exposing both an MMIO window and an IRQ: its id, the two
|
|
||||||
/// resource indices, and the GSI discovery chose out of `Tn_INT_ROUTE_CAP`.
|
|
||||||
const Found = struct { device_id: u64, mmio: u64, irq: u64, gsi: u64 };
|
|
||||||
|
|
||||||
fn findHpet(buffer: []device.DeviceDescriptor) ?Found {
|
|
||||||
const total = device.enumerate(buffer);
|
|
||||||
const n = @min(total, buffer.len);
|
|
||||||
for (buffer[0..n]) |d| {
|
|
||||||
if (d.class != @intFromEnum(device.DeviceClass.timer)) continue;
|
|
||||||
// Skip comparator children a bus driver may have published below the block
|
|
||||||
// (see system/drivers/bus/bus.zig) — we want the register block itself.
|
|
||||||
if (d.parent != device.no_parent) continue;
|
|
||||||
var mmio_index: ?u64 = null;
|
|
||||||
var irq: ?u64 = null;
|
|
||||||
for (0..d.resource_count) |j| {
|
|
||||||
switch (d.resources[j].kind) {
|
|
||||||
@intFromEnum(device.ResourceKind.memory) => mmio_index = mmio_index orelse j,
|
|
||||||
@intFromEnum(device.ResourceKind.irq) => irq = irq orelse j,
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (mmio_index) |m| if (irq) |i| {
|
|
||||||
return .{ .device_id = d.id, .mmio = m, .irq = i, .gsi = d.resources[i].start };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main() void {
|
|
||||||
// Enumerate into a heap buffer (too big for the one-page user stack).
|
|
||||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 32) catch {
|
|
||||||
_ = runtime.system.write("hpet: out of memory\n");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hpet = findHpet(buffer) orelse {
|
|
||||||
_ = runtime.system.write("hpet: no HPET with an IRQ\n");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!device.claim(hpet.device_id)) {
|
|
||||||
_ = runtime.system.write("hpet: claim failed\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const base = device.mmioMap(hpet.device_id, hpet.mmio) orelse {
|
|
||||||
_ = runtime.system.write("hpet: mmio_map failed\n");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The GSI discovery picked for us out of Tn_INT_ROUTE_CAP. Program the comparator
|
|
||||||
// to raise exactly this line — the kernel will only bind the one it recorded.
|
|
||||||
const gsi = hpet.gsi;
|
|
||||||
|
|
||||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
|
||||||
_ = runtime.system.write("hpet: create_ipc_endpoint failed\n");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- program the hardware ------------------------------------------------
|
|
||||||
// Counter period, so we can arm the comparator a fixed wall-clock distance out.
|
|
||||||
const femtos_per_tick = rd(base, register_general_cap) >> 32;
|
|
||||||
if (femtos_per_tick == 0) {
|
|
||||||
_ = runtime.system.write("hpet: bad HPET period\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ticks_per_ms = 1_000_000_000_000 / femtos_per_tick;
|
|
||||||
|
|
||||||
// Stop the counter and take the legacy route off while we reconfigure.
|
|
||||||
wr(base, register_general_configuration, rd(base, register_general_configuration) & ~(configuration_enable | configuration_leg_rt));
|
|
||||||
|
|
||||||
// Timer 0: one-shot, level-triggered, routed to our GSI, interrupt enabled.
|
|
||||||
// One-shot (not periodic) sidesteps the HPET's Tn_value_SET accumulator quirk —
|
|
||||||
// we simply re-arm from the driver on each interrupt, which is what a tickless
|
|
||||||
// timer driver does anyway.
|
|
||||||
var t0 = rd(base, register_timer0_configuration);
|
|
||||||
t0 &= ~(tn_route_mask | tn_type_periodic);
|
|
||||||
t0 |= tn_int_type_level | tn_int_enb | (gsi << tn_route_shift);
|
|
||||||
wr(base, register_timer0_configuration, t0);
|
|
||||||
|
|
||||||
// Clear any stale assertion, then arm ~100 ms out and start the counter.
|
|
||||||
wr(base, register_int_status, 1);
|
|
||||||
wr(base, register_timer0_comparator, rd(base, register_main_counter) + ticks_per_ms * 100);
|
|
||||||
wr(base, register_general_configuration, rd(base, register_general_configuration) | configuration_enable);
|
|
||||||
|
|
||||||
if (!device.irqBind(hpet.device_id, hpet.irq, endpoint)) {
|
|
||||||
_ = runtime.system.write("hpet: irq_bind failed\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_ = runtime.system.write("hpet: bound, sleeping until the hardware speaks\n");
|
|
||||||
|
|
||||||
// --- the driver loop -----------------------------------------------------
|
|
||||||
// Blocked in replyWait. No polling, no spinning: the next line of this function
|
|
||||||
// runs only because an interrupt fired.
|
|
||||||
var receive: [64]u8 = undefined;
|
|
||||||
var count: usize = 0;
|
|
||||||
while (count < target_ticks) {
|
|
||||||
// Blocked here. The task is `.blocked` and off every scheduler queue; the
|
|
||||||
// next line runs only because the HPET raised its line.
|
|
||||||
const r = ipc.replyWait(endpoint, &.{}, &receive, null);
|
|
||||||
if (!r.isNotification()) continue; // a client request, not our IRQ
|
|
||||||
|
|
||||||
// Quiet the device: write 1 to timer 0's status bit. Until this lands, the
|
|
||||||
// line is still asserted and unmasking would refire immediately.
|
|
||||||
wr(base, register_int_status, 1);
|
|
||||||
count += 1;
|
|
||||||
|
|
||||||
if (count < target_ticks) {
|
|
||||||
wr(base, register_timer0_comparator, rd(base, register_main_counter) + ticks_per_ms * 100);
|
|
||||||
} else {
|
|
||||||
// Last one: stop the source rather than re-arming, so the line is left
|
|
||||||
// both quiet *and* unmasked by the ack below. Re-arming here would leave
|
|
||||||
// a pending interrupt that nobody is waiting for, and the ISR would mask
|
|
||||||
// the line again a moment later.
|
|
||||||
wr(base, register_timer0_configuration, rd(base, register_timer0_configuration) & ~tn_int_enb);
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = runtime.system.write("hpet: irq\n");
|
|
||||||
if (!device.irqAck(hpet.device_id, hpet.irq)) {
|
|
||||||
_ = runtime.system.write("hpet: irq_ack failed\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = runtime.system.write("hpet: ok\n");
|
|
||||||
while (true) runtime.system.sleep(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const panic = runtime.panic;
|
|
||||||
comptime {
|
|
||||||
_ = &runtime.start._start;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
//! /system/drivers/pci-bus — the PCI bus driver: enumeration moved out of ring 0
|
||||||
|
//! (docs/discovery.md). The device manager matches the `pci_host_bridge`
|
||||||
|
//! node and spawns one instance per bridge, the bridge's device id as argv[1] —
|
||||||
|
//! the same per-device contract as usb-xhci-bus.
|
||||||
|
//!
|
||||||
|
//! M19.1 (this increment): claim the bridge, map its ECAM window (resource 0;
|
||||||
|
//! the bus range and the MMIO apertures follow it), walk every
|
||||||
|
//! bus/device/function config header, and log what the walk finds — ending
|
||||||
|
//! with "/system/drivers/pci-bus: N functions found", which the `pci-scan` scenario compares
|
||||||
|
//! against the kernel's own enumeration. Registration and reports (M19.2), and
|
||||||
|
//! the kernel walk's retirement (M19.3), build on this proven-equivalent scan.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const runtime = @import("runtime");
|
||||||
|
const protocol = runtime.device_manager_protocol;
|
||||||
|
const device = runtime.device;
|
||||||
|
const pci_class = @import("pci-class");
|
||||||
|
|
||||||
|
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||||
|
var line: [128]u8 = undefined;
|
||||||
|
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log a discovered function with its (class / subclass / prog-IF) triple decoded
|
||||||
|
/// to human names — the boot-log breadcrumb that says *what* the hardware is, so
|
||||||
|
/// "class 0x01 (Mass Storage Controller) subclass 0x06 (Serial ATA Controller)
|
||||||
|
/// progif 0x01 (AHCI 1.0)" reads straight off the log when writing a new driver.
|
||||||
|
/// A dedicated wider buffer than `writeLine`'s, since the decoded names are long.
|
||||||
|
fn logFunction(bus: u64, dev: u64, function: u64, class_triple: u32) void {
|
||||||
|
const cc = pci_class.ClassCode.unpack(@truncate(class_triple));
|
||||||
|
const pif = pci_class.progIfName(cc.base, cc.subclass, cc.prog_if);
|
||||||
|
var line: [200]u8 = undefined;
|
||||||
|
const text = if (pif.len != 0)
|
||||||
|
std.fmt.bufPrint(&line, "/system/drivers/pci-bus: {d}:{d}.{d} class 0x{x:0>2} ({s}) subclass 0x{x:0>2} ({s}) progif 0x{x:0>2} ({s})\n", .{ bus, dev, function, cc.base, pci_class.className(cc.base), cc.subclass, pci_class.subclassName(cc.base, cc.subclass), cc.prog_if, pif }) catch return
|
||||||
|
else
|
||||||
|
std.fmt.bufPrint(&line, "/system/drivers/pci-bus: {d}:{d}.{d} class 0x{x:0>2} ({s}) subclass 0x{x:0>2} ({s}) progif 0x{x:0>2}\n", .{ bus, dev, function, cc.base, pci_class.className(cc.base), cc.subclass, pci_class.subclassName(cc.base, cc.subclass), cc.prog_if }) catch return;
|
||||||
|
_ = runtime.system.write(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bridge_id: u64 = protocol.no_device;
|
||||||
|
var ecam_base: usize = 0;
|
||||||
|
var ecam_physical: u64 = 0;
|
||||||
|
var start_bus: u64 = 0;
|
||||||
|
var bus_count: u64 = 0;
|
||||||
|
var manager_handle: runtime.ipc.Handle = 0;
|
||||||
|
|
||||||
|
/// One aligned 32-bit read from a function's configuration space.
|
||||||
|
fn configRead(bus: u64, dev: u64, function: u64, offset: u64) u32 {
|
||||||
|
const address = ecam_base + (((bus - start_bus) << 20) | (dev << 15) | (function << 12) | offset);
|
||||||
|
const register: *volatile u32 = @ptrFromInt(address);
|
||||||
|
return register.*;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configWrite(bus: u64, dev: u64, function: u64, offset: u64, value: u32) void {
|
||||||
|
const address = ecam_base + (((bus - start_bus) << 20) | (dev << 15) | (function << 12) | offset);
|
||||||
|
const register: *volatile u32 = @ptrFromInt(address);
|
||||||
|
register.* = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configRead16(bus: u64, dev: u64, function: u64, offset: u64) u16 {
|
||||||
|
const word = configRead(bus, dev, function, offset & ~@as(u64, 3));
|
||||||
|
return @truncate(word >> @intCast((offset & 3) * 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configWrite16(bus: u64, dev: u64, function: u64, offset: u64, value: u16) void {
|
||||||
|
const aligned = offset & ~@as(u64, 3);
|
||||||
|
const shift: u5 = @intCast((offset & 3) * 8);
|
||||||
|
const word = configRead(bus, dev, function, aligned);
|
||||||
|
const mask = @as(u32, 0xFFFF) << shift;
|
||||||
|
configWrite(bus, dev, function, aligned, (word & ~mask) | (@as(u32, value) << shift));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Claim the bridge, map the ECAM, hello the manager, then scan.
|
||||||
|
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||||
|
_ = endpoint;
|
||||||
|
if (!device.claim(bridge_id)) {
|
||||||
|
writeLine("/system/drivers/pci-bus: unable to claim bridge device {d}\n", .{bridge_id});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||||
|
_ = runtime.system.write("/system/drivers/pci-bus: out of memory\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const total = device.enumerate(buffer);
|
||||||
|
const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| {
|
||||||
|
if (d.id == bridge_id) break d;
|
||||||
|
} else {
|
||||||
|
writeLine("/system/drivers/pci-bus: device {d} not in the device tree\n", .{bridge_id});
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
// Resource 0 is the ECAM window (1 MiB of config space per bus); the bus
|
||||||
|
// range rides beside it. The MMIO apertures (M19.0) come after both.
|
||||||
|
if (descriptor.resource_count < 2 or descriptor.resources[0].kind != @intFromEnum(device.ResourceKind.memory)) {
|
||||||
|
_ = runtime.system.write("/system/drivers/pci-bus: bridge has no ECAM window\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bus_range = for (descriptor.resources[0..@intCast(descriptor.resource_count)]) |resource| {
|
||||||
|
if (resource.kind == @intFromEnum(device.ResourceKind.bus_range)) break resource;
|
||||||
|
} else {
|
||||||
|
_ = runtime.system.write("/system/drivers/pci-bus: bridge has no bus range\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
start_bus = bus_range.start;
|
||||||
|
bus_count = bus_range.len;
|
||||||
|
ecam_physical = descriptor.resources[0].start;
|
||||||
|
ecam_base = device.mmioMap(bridge_id, 0) orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/pci-bus: ECAM mmio_map failed\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The handshake, then the scan (reports join in M19.2).
|
||||||
|
var manager: ?runtime.ipc.Handle = null;
|
||||||
|
var tries: u32 = 0;
|
||||||
|
while (manager == null and tries < 100) : (tries += 1) {
|
||||||
|
manager = runtime.ipc.lookup(.device_manager);
|
||||||
|
if (manager == null) runtime.system.sleep(20);
|
||||||
|
}
|
||||||
|
const h = manager orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/pci-bus: no device manager to hello\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const hello = protocol.Hello{ .role = @intFromEnum(protocol.Role.bus), .device_id = bridge_id };
|
||||||
|
var reply: [protocol.message_maximum]u8 = undefined;
|
||||||
|
const n = runtime.ipc.call(h, std.mem.asBytes(&hello), &reply) catch {
|
||||||
|
_ = runtime.system.write("/system/drivers/pci-bus: hello call failed\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (n < protocol.reply_size or std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0) {
|
||||||
|
_ = runtime.system.write("/system/drivers/pci-bus: hello refused\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
manager_handle = h;
|
||||||
|
|
||||||
|
scan();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The brute-force walk the kernel does today, from ring 3: every bus in the
|
||||||
|
/// range, 32 devices, 8 functions; vendor id FFFFh means nothing decodes there,
|
||||||
|
/// and only multifunction devices get their functions 1..7 probed.
|
||||||
|
fn scan() void {
|
||||||
|
var found: u32 = 0;
|
||||||
|
var bus: u64 = start_bus;
|
||||||
|
while (bus < start_bus + bus_count) : (bus += 1) {
|
||||||
|
var dev: u64 = 0;
|
||||||
|
while (dev < 32) : (dev += 1) {
|
||||||
|
const first = configRead(bus, dev, 0, 0);
|
||||||
|
if (first & 0xFFFF == 0xFFFF) continue;
|
||||||
|
const multifunction = (configRead(bus, dev, 0, 0x0C) >> 16) & 0x80 != 0;
|
||||||
|
var function: u64 = 0;
|
||||||
|
while (function < 8) : (function += 1) {
|
||||||
|
if (function != 0 and !multifunction) break;
|
||||||
|
const vendor_device = configRead(bus, dev, function, 0);
|
||||||
|
if (vendor_device & 0xFFFF == 0xFFFF) continue;
|
||||||
|
const class_revision = configRead(bus, dev, function, 0x08);
|
||||||
|
found += 1;
|
||||||
|
logFunction(bus, dev, function, class_revision >> 8);
|
||||||
|
registerAndReport(bus, dev, function, class_revision >> 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeLine("/system/drivers/pci-bus: {d} functions found\n", .{found});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register one function under the bridge and report it to the manager. The
|
||||||
|
/// descriptor mirrors the kernel's own recording byte for byte — config slice
|
||||||
|
/// as resource 0, then the sized BARs — so during coexistence the idempotent
|
||||||
|
/// device_register (M19.0) returns the kernel's existing node id rather than
|
||||||
|
/// growing a duplicate, and the report carries the id drivers already use.
|
||||||
|
fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void {
|
||||||
|
var descriptor = std.mem.zeroes(device.DeviceDescriptor);
|
||||||
|
descriptor.class = @intFromEnum(device.DeviceClass.pci_device);
|
||||||
|
descriptor.pci_class = class_triple;
|
||||||
|
descriptor.resources[0] = .{
|
||||||
|
.kind = @intFromEnum(device.ResourceKind.memory),
|
||||||
|
.start = ecam_physical + (((bus - start_bus) << 20) | (dev << 15) | (function << 12)),
|
||||||
|
.len = 4096,
|
||||||
|
};
|
||||||
|
descriptor.resource_count = 1;
|
||||||
|
|
||||||
|
// The standard BAR-sizing probe, exactly as the kernel does it: decode off,
|
||||||
|
// write all-ones, read the writable mask back, restore. Header type 0 only.
|
||||||
|
const header_type = (configRead(bus, dev, function, 0x0C) >> 16) & 0x7F;
|
||||||
|
if (header_type == 0) {
|
||||||
|
const command = configRead16(bus, dev, function, 0x04);
|
||||||
|
configWrite16(bus, dev, function, 0x04, command & ~@as(u16, 0b11));
|
||||||
|
var i: u64 = 0;
|
||||||
|
while (i < 6) : (i += 1) {
|
||||||
|
if (descriptor.resource_count >= 8) break;
|
||||||
|
const off = 0x10 + i * 4;
|
||||||
|
const original = configRead(bus, dev, function, off);
|
||||||
|
if (original == 0) continue;
|
||||||
|
const slot: usize = @intCast(descriptor.resource_count);
|
||||||
|
if (original & 1 != 0) {
|
||||||
|
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
|
||||||
|
const readback = configRead(bus, dev, function, off);
|
||||||
|
configWrite(bus, dev, function, off, original);
|
||||||
|
const mask = readback & 0xFFFF_FFFC;
|
||||||
|
const size: u32 = if (mask == 0) 0 else (~mask +% 1) & 0xFFFF;
|
||||||
|
if (size == 0) continue; // unimplemented BAR — nothing to register
|
||||||
|
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.io_port), .start = original & 0xFFFF_FFFC, .len = size };
|
||||||
|
descriptor.resource_count += 1;
|
||||||
|
} else if ((original >> 1) & 0x3 == 2) {
|
||||||
|
const original_high = configRead(bus, dev, function, off + 4);
|
||||||
|
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
|
||||||
|
configWrite(bus, dev, function, off + 4, 0xFFFF_FFFF);
|
||||||
|
const lo = configRead(bus, dev, function, off);
|
||||||
|
const hi = configRead(bus, dev, function, off + 4);
|
||||||
|
configWrite(bus, dev, function, off, original);
|
||||||
|
configWrite(bus, dev, function, off + 4, original_high);
|
||||||
|
const readback = (@as(u64, hi) << 32) | (lo & 0xFFFF_FFF0);
|
||||||
|
const size: u64 = if (readback == 0) 0 else ~readback +% 1;
|
||||||
|
i += 1; // consumed the high half regardless
|
||||||
|
if (size == 0) continue;
|
||||||
|
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = (@as(u64, original_high) << 32) | (original & 0xFFFF_FFF0), .len = size };
|
||||||
|
descriptor.resource_count += 1;
|
||||||
|
} else {
|
||||||
|
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
|
||||||
|
const readback = configRead(bus, dev, function, off);
|
||||||
|
configWrite(bus, dev, function, off, original);
|
||||||
|
const mask = readback & 0xFFFF_FFF0;
|
||||||
|
const size: u32 = if (mask == 0) 0 else ~mask +% 1;
|
||||||
|
if (size == 0) continue;
|
||||||
|
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = original & 0xFFFF_FFF0, .len = size };
|
||||||
|
descriptor.resource_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configWrite16(bus, dev, function, 0x04, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
const registered = device.register(bridge_id, &descriptor) orelse {
|
||||||
|
writeLine("/system/drivers/pci-bus: register refused for {d}:{d}.{d}\n", .{ bus, dev, function });
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const report = protocol.ChildAdded{
|
||||||
|
.parent = bridge_id,
|
||||||
|
.bus_address = (bus << 8) | (dev << 3) | function,
|
||||||
|
.identity = class_triple,
|
||||||
|
.device_id = registered,
|
||||||
|
};
|
||||||
|
var reply: [protocol.message_maximum]u8 = undefined;
|
||||||
|
_ = runtime.ipc.call(manager_handle, std.mem.asBytes(&report), &reply) catch {
|
||||||
|
writeLine("/system/drivers/pci-bus: child report for {d}:{d}.{d} failed\n", .{ bus, dev, function });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||||
|
_ = message;
|
||||||
|
_ = reply;
|
||||||
|
_ = sender;
|
||||||
|
_ = capability;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(init: runtime.process.Init) void {
|
||||||
|
const argument = init.arguments.get(1) orelse return; // bare (ramdisk sweep): stay silent
|
||||||
|
bridge_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||||
|
writeLine("/system/drivers/pci-bus: malformed bridge device id '{s}'\n", .{argument});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
runtime.service.run(protocol.message_maximum, .{
|
||||||
|
.init = initialise,
|
||||||
|
.on_message = onMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = runtime.panic;
|
||||||
|
comptime {
|
||||||
|
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||||
|
}
|
||||||
@@ -1,43 +1,181 @@
|
|||||||
//! PS/2 Keyboard Driver
|
//! PS/2 Keyboard Driver
|
||||||
//!
|
//!
|
||||||
//! Spawned by the ps2-bus driver once the controller is initialized and port 1
|
//! Spawned by the ps2-bus driver once the controller is initialized and the port
|
||||||
//! has passed its interface test and device reset. The bus driver hands us our
|
//! has passed its interface test and device reset. The bus driver hands us our
|
||||||
//! device HID as argv[1]; we use it to locate our own device descriptor.
|
//! device HID as argv[1] and, optionally, a layout name (`"us"`, `"gb"`, ...) as
|
||||||
|
//! argv[2].
|
||||||
|
//!
|
||||||
|
//! The 8042's ports (0x60/0x64) and IRQ1 live on the PNP0303 node, which the
|
||||||
|
//! ps2-bus driver exclusively owns — so this driver never touches the hardware.
|
||||||
|
//! Instead it **attaches** to the bus (handing over its endpoint as a capability)
|
||||||
|
//! and receives every scancode byte as a forwarded asynchronous message. Each byte
|
||||||
|
//! feeds the set-2 decoder; a decoded key becomes input-protocol events:
|
||||||
|
//!
|
||||||
|
//! scancode byte -> HID usage keycode -> key_down / key_up
|
||||||
|
//! -> xkeyboard-config -> character -> key_press
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const runtime = @import("runtime");
|
const runtime = @import("runtime");
|
||||||
|
const xkb = @import("xkeyboard-config");
|
||||||
const ps2 = @import("ps2-library.zig");
|
const ps2 = @import("ps2-library.zig");
|
||||||
|
const scancode = @import("scancode.zig");
|
||||||
const device = runtime.device;
|
const device = runtime.device;
|
||||||
|
const ipc = runtime.ipc;
|
||||||
|
const protocol = runtime.input_protocol;
|
||||||
|
|
||||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||||
var line: [128]u8 = undefined;
|
var line: [128]u8 = undefined;
|
||||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() void {
|
/// Look up the ps2-bus service, retrying while the bus (which spawned us before
|
||||||
const hid = runtime.argument(1);
|
/// registering) is still coming up.
|
||||||
|
fn lookupBus() ?ipc.Handle {
|
||||||
|
var attempts: usize = 0;
|
||||||
|
while (attempts < 100) : (attempts += 1) {
|
||||||
|
if (ipc.lookup(.ps2_bus)) |handle| return handle;
|
||||||
|
runtime.system.sleep(50);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The character a pressed key produces under `modifiers`, or 0 for none. The
|
||||||
|
/// layout lookup answers for printable keys; the keys whose keysym has no Unicode
|
||||||
|
/// mapping but that every consumer still expects as a character (Enter, Tab,
|
||||||
|
/// Backspace, Escape) are given their ASCII control characters here.
|
||||||
|
fn characterFor(layout: *const xkb.Layout, usage: u8, modifiers: scancode.ModifierSnapshot) u32 {
|
||||||
|
const mapping = xkb.map(layout, usage, .{
|
||||||
|
.shift = modifiers.shift,
|
||||||
|
.caps_lock = modifiers.caps_lock,
|
||||||
|
.level3 = modifiers.right_alt,
|
||||||
|
.control = modifiers.control,
|
||||||
|
});
|
||||||
|
if (mapping.character) |character| return character;
|
||||||
|
return switch (@as(protocol.Keycode, @enumFromInt(usage))) {
|
||||||
|
.enter, .keypad_enter => '\n',
|
||||||
|
.tab => '\t',
|
||||||
|
.backspace => 0x08,
|
||||||
|
.escape => 0x1B,
|
||||||
|
else => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The input protocol's modifier word for a snapshot.
|
||||||
|
fn modifierWord(modifiers: scancode.ModifierSnapshot) u32 {
|
||||||
|
var word: u32 = 0;
|
||||||
|
if (modifiers.shift) word |= protocol.modifier_shift;
|
||||||
|
if (modifiers.control) word |= protocol.modifier_control;
|
||||||
|
if (modifiers.alt) word |= protocol.modifier_alt;
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(init: runtime.process.Init) void {
|
||||||
|
const hid = init.arguments.get(1).?;
|
||||||
if (hid.len == 0) {
|
if (hid.len == 0) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: no HID argument\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: no HID argument\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
writeLine("system/drivers/ps2-bus/keyboard: starting for hid {s}\n", .{hid});
|
writeLine("/system/drivers/ps2-bus/keyboard: starting for hid {s}\n", .{hid});
|
||||||
|
|
||||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: out of memory\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: out of memory\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if (device.findDeviceDescriptorByHid(buffer, hid) == null) {
|
if (device.findDeviceDescriptorByHid(buffer, hid) == null) {
|
||||||
writeLine("system/drivers/ps2-bus/keyboard: no device for hid {s}\n", .{hid});
|
writeLine("/system/drivers/ps2-bus/keyboard: no device for hid {s}\n", .{hid});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The 8042 ports (0x60/0x64) and this keyboard's IRQ1 both live on the same
|
// The layout is a spawn argument so a later settings source can choose it;
|
||||||
// PNP0303 node, which the ps2-bus driver exclusively owns — so the keyboard is
|
// absent (as today) it defaults to us.
|
||||||
// served through the bus and does not claim the controller itself.
|
const layout_name = init.arguments.get(2) orelse "us";
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: served by ps2-bus (controller owned by bus)\n");
|
const layout = xkb.byName(layout_name) orelse xkb.us;
|
||||||
|
writeLine("/system/drivers/ps2-bus/keyboard: layout {s}\n", .{layout.name});
|
||||||
|
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: ok\n");
|
// Attach to the bus: hand it our endpoint, and it forwards every byte the
|
||||||
while (true) runtime.system.sleep(1000);
|
// keyboard sends (it owns the controller; we own the decoding).
|
||||||
|
const bus = lookupBus() orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: ps2-bus service unavailable\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: no endpoint\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
var attach = ps2.AttachRequest{ .device_type = @intFromEnum(ps2.DeviceType.keyboard) };
|
||||||
|
var attach_reply: [@sizeOf(ps2.AttachReply)]u8 = undefined;
|
||||||
|
const attached = ipc.callCap(bus, std.mem.asBytes(&attach), &attach_reply, endpoint) catch {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: attach call failed\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (attached.len < @sizeOf(ps2.AttachReply) or
|
||||||
|
std.mem.bytesToValue(ps2.AttachReply, attach_reply[0..@sizeOf(ps2.AttachReply)]).status != @intFromEnum(ps2.AttachStatus.ok))
|
||||||
|
{
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: attach refused\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast keyboard events through the input service so programs can listen
|
||||||
|
// for them (docs/input.md).
|
||||||
|
var source = runtime.input.connectSource() orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: input service unavailable\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: ok\n");
|
||||||
|
|
||||||
|
var decoder = scancode.Decoder{};
|
||||||
|
var state = scancode.KeyboardState{};
|
||||||
|
var receive: [@sizeOf(ps2.ForwardedByte)]u8 = undefined;
|
||||||
|
while (true) {
|
||||||
|
const got = ipc.replyWait(endpoint, &.{}, &receive, null);
|
||||||
|
if (!got.isMessage() or got.len < @sizeOf(ps2.ForwardedByte)) continue;
|
||||||
|
const forwarded = std.mem.bytesToValue(ps2.ForwardedByte, receive[0..@sizeOf(ps2.ForwardedByte)]);
|
||||||
|
|
||||||
|
const key = decoder.feed(@intCast(forwarded.byte & 0xFF)) orelse continue;
|
||||||
|
const transition = state.apply(key);
|
||||||
|
const modifiers = modifierWord(transition.modifiers);
|
||||||
|
|
||||||
|
switch (transition.action) {
|
||||||
|
.pressed => {
|
||||||
|
_ = source.publishKeyboardEvent(.{
|
||||||
|
.kind = @intFromEnum(protocol.EventKind.key_down),
|
||||||
|
.keycode = key.usage,
|
||||||
|
.character = 0,
|
||||||
|
.modifiers = modifiers,
|
||||||
|
});
|
||||||
|
const character = characterFor(layout, key.usage, transition.modifiers);
|
||||||
|
if (character != 0) {
|
||||||
|
_ = source.publishKeyboardEvent(.{
|
||||||
|
.kind = @intFromEnum(protocol.EventKind.key_press),
|
||||||
|
.keycode = key.usage,
|
||||||
|
.character = character,
|
||||||
|
.modifiers = modifiers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Typematic repeat: the key did not physically go down again, so no
|
||||||
|
// key_down — but it keeps producing its character.
|
||||||
|
.repeated => {
|
||||||
|
const character = characterFor(layout, key.usage, transition.modifiers);
|
||||||
|
if (character != 0) {
|
||||||
|
_ = source.publishKeyboardEvent(.{
|
||||||
|
.kind = @intFromEnum(protocol.EventKind.key_press),
|
||||||
|
.keycode = key.usage,
|
||||||
|
.character = character,
|
||||||
|
.modifiers = modifiers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.released => {
|
||||||
|
_ = source.publishKeyboardEvent(.{
|
||||||
|
.kind = @intFromEnum(protocol.EventKind.key_up),
|
||||||
|
.keycode = key.usage,
|
||||||
|
.character = 0,
|
||||||
|
.modifiers = modifiers,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const panic = runtime.panic;
|
pub const panic = runtime.panic;
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
//! PS/2 mouse packet assembly — the byte stream a streaming mouse sends, turned
|
||||||
|
//! into decoded movement/button reports.
|
||||||
|
//!
|
||||||
|
//! A standard PS/2 mouse in stream mode sends three-byte packets:
|
||||||
|
//!
|
||||||
|
//! byte 0: | Y ovf | X ovf | Y sign | X sign | 1 | middle | right | left |
|
||||||
|
//! byte 1: X movement (low eight bits; the sign bit lives in byte 0)
|
||||||
|
//! byte 2: Y movement (likewise)
|
||||||
|
//!
|
||||||
|
//! Movement is nine-bit two's complement, PS/2 convention: positive X right,
|
||||||
|
//! positive Y **up**. The decoded packet converts Y to the screen convention
|
||||||
|
//! (positive down), matching what every consumer of relative motion expects.
|
||||||
|
//! Bit 3 of byte 0 is always set — the resynchronization anchor: a byte at
|
||||||
|
//! packet start with bit 3 clear cannot be a packet header and is dropped.
|
||||||
|
//!
|
||||||
|
//! Everything here is pure (no imports beyond `std`, no IO), so it is
|
||||||
|
//! host-testable: the tests at the bottom run under `zig build test`.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// One decoded movement/button report, in screen convention (positive dy down).
|
||||||
|
pub const Packet = struct {
|
||||||
|
left: bool,
|
||||||
|
right: bool,
|
||||||
|
middle: bool,
|
||||||
|
dx: i16,
|
||||||
|
dy: i16,
|
||||||
|
};
|
||||||
|
|
||||||
|
const header_always_set: u8 = 1 << 3;
|
||||||
|
const header_left: u8 = 1 << 0;
|
||||||
|
const header_right: u8 = 1 << 1;
|
||||||
|
const header_middle: u8 = 1 << 2;
|
||||||
|
const header_x_sign: u8 = 1 << 4;
|
||||||
|
const header_y_sign: u8 = 1 << 5;
|
||||||
|
const header_x_overflow: u8 = 1 << 6;
|
||||||
|
const header_y_overflow: u8 = 1 << 7;
|
||||||
|
|
||||||
|
/// Device protocol bytes that can reach the packet stream around bring-up (the
|
||||||
|
/// acknowledge to enable-reporting, a reset's self-test result). Both have bit 3
|
||||||
|
/// set, so the header check alone cannot reject them; they are recognized only
|
||||||
|
/// at packet start, where a real header cannot be one of them in practice.
|
||||||
|
const response_acknowledge: u8 = 0xFA;
|
||||||
|
const response_self_test_passed: u8 = 0xAA;
|
||||||
|
|
||||||
|
/// Accumulates the byte stream into `Packet`s. Feed it every byte the mouse
|
||||||
|
/// sends; the third byte of each well-formed packet returns one.
|
||||||
|
pub const Assembler = struct {
|
||||||
|
bytes: [3]u8 = undefined,
|
||||||
|
count: u8 = 0,
|
||||||
|
|
||||||
|
pub fn feed(self: *Assembler, byte: u8) ?Packet {
|
||||||
|
if (self.count == 0) {
|
||||||
|
// Resynchronize: a packet must start with a plausible header.
|
||||||
|
if (byte & header_always_set == 0) return null;
|
||||||
|
if (byte == response_acknowledge or byte == response_self_test_passed) return null;
|
||||||
|
}
|
||||||
|
self.bytes[self.count] = byte;
|
||||||
|
self.count += 1;
|
||||||
|
if (self.count < 3) return null;
|
||||||
|
self.count = 0;
|
||||||
|
|
||||||
|
const header = self.bytes[0];
|
||||||
|
// An overflowed count is garbage by definition; discard the packet.
|
||||||
|
if (header & (header_x_overflow | header_y_overflow) != 0) return null;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.left = header & header_left != 0,
|
||||||
|
.right = header & header_right != 0,
|
||||||
|
.middle = header & header_middle != 0,
|
||||||
|
.dx = movement(self.bytes[1], header & header_x_sign != 0),
|
||||||
|
// PS/2 positive Y is up; screen positive Y is down.
|
||||||
|
.dy = -movement(self.bytes[2], header & header_y_sign != 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nine-bit two's complement: the eight movement bits plus the header's sign.
|
||||||
|
fn movement(low: u8, negative: bool) i16 {
|
||||||
|
const value: i16 = low;
|
||||||
|
return if (negative) value - 256 else value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- tests (host-run via `zig build test`) ------------------------------------
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
fn feedAll(assembler: *Assembler, bytes: []const u8) ?Packet {
|
||||||
|
var result: ?Packet = null;
|
||||||
|
for (bytes) |byte| {
|
||||||
|
if (assembler.feed(byte)) |packet| result = packet;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "plain motion decodes with screen-convention y" {
|
||||||
|
var assembler = Assembler{};
|
||||||
|
const packet = feedAll(&assembler, &.{ 0x08, 5, 3 }).?;
|
||||||
|
try testing.expectEqual(@as(i16, 5), packet.dx);
|
||||||
|
try testing.expectEqual(@as(i16, -3), packet.dy); // PS/2 up 3 -> screen -3
|
||||||
|
try testing.expect(!packet.left and !packet.right and !packet.middle);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "negative movement sign-extends through the header bits" {
|
||||||
|
var assembler = Assembler{};
|
||||||
|
// X sign and Y sign set: dx = 0xFB - 256 = -5, dy raw = 0xFE - 256 = -2 -> screen +2.
|
||||||
|
const packet = feedAll(&assembler, &.{ 0x08 | 0x10 | 0x20, 0xFB, 0xFE }).?;
|
||||||
|
try testing.expectEqual(@as(i16, -5), packet.dx);
|
||||||
|
try testing.expectEqual(@as(i16, 2), packet.dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "buttons decode from the header" {
|
||||||
|
var assembler = Assembler{};
|
||||||
|
const packet = feedAll(&assembler, &.{ 0x08 | 0x01 | 0x02, 0, 0 }).?;
|
||||||
|
try testing.expect(packet.left);
|
||||||
|
try testing.expect(packet.right);
|
||||||
|
try testing.expect(!packet.middle);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a byte with bit 3 clear at packet start is dropped" {
|
||||||
|
var assembler = Assembler{};
|
||||||
|
// The stray 0x02 cannot be a header; the following packet still decodes.
|
||||||
|
try testing.expectEqual(@as(?Packet, null), assembler.feed(0x02));
|
||||||
|
const packet = feedAll(&assembler, &.{ 0x09, 1, 0 }).?;
|
||||||
|
try testing.expect(packet.left);
|
||||||
|
try testing.expectEqual(@as(i16, 1), packet.dx);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "protocol bytes at packet start are dropped" {
|
||||||
|
var assembler = Assembler{};
|
||||||
|
try testing.expectEqual(@as(?Packet, null), assembler.feed(0xFA)); // enable-reporting ACK
|
||||||
|
try testing.expectEqual(@as(?Packet, null), assembler.feed(0xAA)); // self-test passed
|
||||||
|
const packet = feedAll(&assembler, &.{ 0x08, 7, 0 }).?;
|
||||||
|
try testing.expectEqual(@as(i16, 7), packet.dx);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "an overflowed packet is discarded whole" {
|
||||||
|
var assembler = Assembler{};
|
||||||
|
try testing.expectEqual(@as(?Packet, null), feedAll(&assembler, &.{ 0x08 | 0x40, 0xFF, 0xFF }));
|
||||||
|
// The assembler is back at packet start.
|
||||||
|
const packet = feedAll(&assembler, &.{ 0x08, 1, 1 }).?;
|
||||||
|
try testing.expectEqual(@as(i16, 1), packet.dx);
|
||||||
|
}
|
||||||
@@ -1,47 +1,141 @@
|
|||||||
//! PS/2 Mouse Driver
|
//! PS/2 Mouse Driver
|
||||||
//!
|
//!
|
||||||
//! Spawned by the ps2-bus driver once the controller is initialized and port 2
|
//! Spawned by the ps2-bus driver once the controller is initialized and the port
|
||||||
//! has passed its interface test and device reset. The bus driver hands us our
|
//! has passed its interface test and device reset. The bus driver hands us our
|
||||||
//! device HID as argv[1]; we use it to locate our own device descriptor.
|
//! device HID as argv[1].
|
||||||
|
//!
|
||||||
|
//! Like the keyboard, this driver never touches the hardware: the 8042's ports
|
||||||
|
//! and both port IRQs are owned by the ps2-bus driver (the auxiliary port's
|
||||||
|
//! IRQ12 lives on the PNP0F13 node, which the bus claims alongside the
|
||||||
|
//! controller). The driver **attaches** to the bus and receives every byte the
|
||||||
|
//! mouse sends as a forwarded asynchronous message. The bytes assemble into
|
||||||
|
//! three-byte packets, and each packet becomes input-protocol events:
|
||||||
|
//!
|
||||||
|
//! packet -> button transitions -> button_down / button_up
|
||||||
|
//! -> movement -> motion (dx/dy, screen convention)
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const runtime = @import("runtime");
|
const runtime = @import("runtime");
|
||||||
const ps2 = @import("ps2-library.zig");
|
const ps2 = @import("ps2-library.zig");
|
||||||
|
const mouse_packet = @import("mouse-packet.zig");
|
||||||
const device = runtime.device;
|
const device = runtime.device;
|
||||||
|
const ipc = runtime.ipc;
|
||||||
|
const protocol = runtime.input_protocol;
|
||||||
|
|
||||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||||
var line: [128]u8 = undefined;
|
var line: [128]u8 = undefined;
|
||||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() void {
|
/// Look up the ps2-bus service, retrying while the bus (which spawned us before
|
||||||
const hid = runtime.argument(1);
|
/// registering) is still coming up.
|
||||||
|
fn lookupBus() ?ipc.Handle {
|
||||||
|
var attempts: usize = 0;
|
||||||
|
while (attempts < 100) : (attempts += 1) {
|
||||||
|
if (ipc.lookup(.ps2_bus)) |handle| return handle;
|
||||||
|
runtime.system.sleep(50);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The protocol's pressed-button bitmask for a packet.
|
||||||
|
fn buttonMask(packet: mouse_packet.Packet) u32 {
|
||||||
|
var mask: u32 = 0;
|
||||||
|
if (packet.left) mask |= protocol.mouse_button_left;
|
||||||
|
if (packet.right) mask |= protocol.mouse_button_right;
|
||||||
|
if (packet.middle) mask |= protocol.mouse_button_middle;
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(init: runtime.process.Init) void {
|
||||||
|
const hid = init.arguments.get(1).?;
|
||||||
|
|
||||||
if (hid.len == 0) {
|
if (hid.len == 0) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: no HID argument\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: no HID argument\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
writeLine("system/drivers/ps2-bus/mouse: starting for hid {s}\n", .{hid});
|
writeLine("/system/drivers/ps2-bus/mouse: starting for hid {s}\n", .{hid});
|
||||||
|
|
||||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: out of memory\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: out of memory\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
const mouse_device_descriptor = device.findDeviceDescriptorByHid(buffer, hid) orelse {
|
if (device.findDeviceDescriptorByHid(buffer, hid) == null) {
|
||||||
writeLine("system/drivers/ps2-bus/mouse: no device for hid {s}\n", .{hid});
|
writeLine("/system/drivers/ps2-bus/mouse: no device for hid {s}\n", .{hid});
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The mouse's own node (PNP0F13) carries IRQ12 and is not claimed by the bus,
|
|
||||||
// so this driver takes exclusive ownership of it. Port IO still goes through
|
|
||||||
// the bus, which owns the shared 8042 ports.
|
|
||||||
if (!device.claim(mouse_device_descriptor.id)) {
|
|
||||||
writeLine("system/drivers/ps2-bus/mouse: unable to claim device for hid {s}\n", .{hid});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
writeLine("system/drivers/ps2-bus/mouse: claimed device for hid {s}\n", .{hid});
|
|
||||||
|
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: ok\n");
|
// Attach to the bus: hand it our endpoint, and it forwards every byte the
|
||||||
while (true) runtime.system.sleep(1000);
|
// mouse sends (it owns the controller; we own the decoding).
|
||||||
|
const bus = lookupBus() orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: ps2-bus service unavailable\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: no endpoint\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
var attach = ps2.AttachRequest{ .device_type = @intFromEnum(ps2.DeviceType.mouse) };
|
||||||
|
var attach_reply: [@sizeOf(ps2.AttachReply)]u8 = undefined;
|
||||||
|
const attached = ipc.callCap(bus, std.mem.asBytes(&attach), &attach_reply, endpoint) catch {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: attach call failed\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (attached.len < @sizeOf(ps2.AttachReply) or
|
||||||
|
std.mem.bytesToValue(ps2.AttachReply, attach_reply[0..@sizeOf(ps2.AttachReply)]).status != @intFromEnum(ps2.AttachStatus.ok))
|
||||||
|
{
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: attach refused\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast mouse events through the input service so programs can listen
|
||||||
|
// for them (docs/input.md).
|
||||||
|
var source = runtime.input.connectSource() orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: input service unavailable\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: ok\n");
|
||||||
|
|
||||||
|
var assembler = mouse_packet.Assembler{};
|
||||||
|
var buttons: u32 = 0;
|
||||||
|
var receive: [@sizeOf(ps2.ForwardedByte)]u8 = undefined;
|
||||||
|
while (true) {
|
||||||
|
const got = ipc.replyWait(endpoint, &.{}, &receive, null);
|
||||||
|
if (!got.isMessage() or got.len < @sizeOf(ps2.ForwardedByte)) continue;
|
||||||
|
const forwarded = std.mem.bytesToValue(ps2.ForwardedByte, receive[0..@sizeOf(ps2.ForwardedByte)]);
|
||||||
|
|
||||||
|
const packet = assembler.feed(@intCast(forwarded.byte & 0xFF)) orelse continue;
|
||||||
|
const new_buttons = buttonMask(packet);
|
||||||
|
|
||||||
|
// A button transition per changed button, carrying the new whole mask.
|
||||||
|
const changed = buttons ^ new_buttons;
|
||||||
|
for ([_]u32{ protocol.mouse_button_left, protocol.mouse_button_right, protocol.mouse_button_middle }) |button| {
|
||||||
|
if (changed & button == 0) continue;
|
||||||
|
const kind: protocol.MouseEventKind = if (new_buttons & button != 0) .button_down else .button_up;
|
||||||
|
_ = source.publishMouseEvent(.{
|
||||||
|
.kind = @intFromEnum(kind),
|
||||||
|
.button = button,
|
||||||
|
.dx = 0,
|
||||||
|
.dy = 0,
|
||||||
|
.scroll_x = 0,
|
||||||
|
.scroll_y = 0,
|
||||||
|
.buttons = new_buttons,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
buttons = new_buttons;
|
||||||
|
|
||||||
|
if (packet.dx != 0 or packet.dy != 0) {
|
||||||
|
_ = source.publishMouseEvent(.{
|
||||||
|
.kind = @intFromEnum(protocol.MouseEventKind.motion),
|
||||||
|
.button = 0,
|
||||||
|
.dx = packet.dx,
|
||||||
|
.dy = packet.dy,
|
||||||
|
.scroll_x = 0,
|
||||||
|
.scroll_y = 0,
|
||||||
|
.buttons = new_buttons,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const panic = runtime.panic;
|
pub const panic = runtime.panic;
|
||||||
|
|||||||
@@ -11,8 +11,10 @@
|
|||||||
//! - irq 0xc len 0x1
|
//! - irq 0xc len 0x1
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const runtime = @import("runtime");
|
const runtime = @import("runtime");
|
||||||
|
const acpi_ids = @import("acpi-ids");
|
||||||
const ps2 = @import("ps2-library.zig");
|
const ps2 = @import("ps2-library.zig");
|
||||||
const device = runtime.device;
|
const device = runtime.device;
|
||||||
|
const ipc = runtime.ipc;
|
||||||
|
|
||||||
/// Format one whole log line and emit it in a single `debug_write`, so output
|
/// Format one whole log line and emit it in a single `debug_write`, so output
|
||||||
/// from the child drivers (which run concurrently) can never interleave with it.
|
/// from the child drivers (which run concurrently) can never interleave with it.
|
||||||
@@ -23,55 +25,105 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
|||||||
|
|
||||||
/// Ask the device on `port` what it is, then spawn the matching driver from the
|
/// Ask the device on `port` what it is, then spawn the matching driver from the
|
||||||
/// initial-ramdisk, handing it the device's HID as argv[1]. The driver is chosen
|
/// initial-ramdisk, handing it the device's HID as argv[1]. The driver is chosen
|
||||||
/// from what the device reports, not from the port number.
|
/// from what the device reports, not from the port number. Returns the identified
|
||||||
fn spawnIdentifiedDriver(controller: ps2.Controller, port: ps2.Port) void {
|
/// type so the forwarding loop can route that port's bytes to the driver once it
|
||||||
|
/// attaches, or null if nothing was spawned.
|
||||||
|
fn spawnIdentifiedDriver(controller: ps2.Controller, port: ps2.Port) ?ps2.DeviceType {
|
||||||
const device_type = controller.identifyDevice(port) orelse {
|
const device_type = controller.identifyDevice(port) orelse {
|
||||||
writeLine("system/drivers/ps2-bus: identify timed out on port {s}\n", .{@tagName(port)});
|
writeLine("/system/drivers/ps2-bus: identify timed out on port {s}\n", .{@tagName(port)});
|
||||||
return;
|
return null;
|
||||||
};
|
};
|
||||||
const driver_name = device_type.driverName() orelse {
|
const driver_name = device_type.driverName() orelse {
|
||||||
writeLine("system/drivers/ps2-bus: unrecognized device on port {s}\n", .{@tagName(port)});
|
writeLine("/system/drivers/ps2-bus: unrecognized device on port {s}\n", .{@tagName(port)});
|
||||||
return;
|
return null;
|
||||||
};
|
};
|
||||||
const hid = device_type.hid() orelse "";
|
const hid = device_type.hid() orelse "";
|
||||||
if (runtime.system.spawnWithArguments(driver_name, &.{hid}) != null) {
|
if (runtime.system.spawnWithArguments(driver_name, &.{hid}) != null) {
|
||||||
writeLine("system/drivers/ps2-bus: port {s} is a {s}, spawned {s}\n", .{ @tagName(port), hid, driver_name });
|
writeLine("/system/drivers/ps2-bus: port {s} is a {s}, spawned {s}\n", .{ @tagName(port), hid, driver_name });
|
||||||
} else {
|
return device_type;
|
||||||
writeLine("system/drivers/ps2-bus: failed to spawn {s}\n", .{driver_name});
|
|
||||||
}
|
}
|
||||||
|
writeLine("/system/drivers/ps2-bus: failed to spawn {s}\n", .{driver_name});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resource index of the controller's IRQ (IRQ1) on the PNP0303 descriptor, found
|
||||||
|
/// the way the ports are found in `Controller.init`.
|
||||||
|
fn findInterruptResourceIndex(descriptor: device.DeviceDescriptor) ?u64 {
|
||||||
|
for (0..descriptor.resource_count) |index| {
|
||||||
|
if (descriptor.resources[index].kind == @intFromEnum(device.ResourceKind.irq)) return index;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forwarding endpoints of the attached child drivers, indexed by `ps2.Port`.
|
||||||
|
/// Written when a child's `AttachRequest` arrives, read on every forwarded byte.
|
||||||
|
var port_endpoints = [_]?ipc.Handle{ null, null };
|
||||||
|
|
||||||
|
/// Which device type each port identified as, so an attaching child (which knows
|
||||||
|
/// its type, not its port) can be matched to the right port's byte stream.
|
||||||
|
var port_device_types = [_]?ps2.DeviceType{ null, null };
|
||||||
|
|
||||||
|
/// Handle a child driver's `AttachRequest`: record the endpoint capability it
|
||||||
|
/// passed as the forwarding target for the port whose device matches its type.
|
||||||
|
/// Writes an `AttachReply` into `out` and returns its length.
|
||||||
|
fn handleAttach(message: []const u8, got: ipc.Received, out: []u8) usize {
|
||||||
|
const reply = struct {
|
||||||
|
fn write(buffer: []u8, status: ps2.AttachStatus) usize {
|
||||||
|
const header = ps2.AttachReply{ .status = @intFromEnum(status) };
|
||||||
|
@memcpy(buffer[0..@sizeOf(ps2.AttachReply)], std.mem.asBytes(&header));
|
||||||
|
return @sizeOf(ps2.AttachReply);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (message.len < @sizeOf(ps2.AttachRequest)) return reply.write(out, .invalid_request);
|
||||||
|
const request = std.mem.bytesToValue(ps2.AttachRequest, message[0..@sizeOf(ps2.AttachRequest)]);
|
||||||
|
const endpoint = got.cap orelse return reply.write(out, .missing_endpoint);
|
||||||
|
|
||||||
|
for (&port_device_types, 0..) |maybe_type, port_index| {
|
||||||
|
const device_type = maybe_type orelse continue;
|
||||||
|
if (@intFromEnum(device_type) != request.device_type) continue;
|
||||||
|
port_endpoints[port_index] = endpoint;
|
||||||
|
writeLine("/system/drivers/ps2-bus: {s} driver attached\n", .{@tagName(device_type)});
|
||||||
|
return reply.write(out, .ok);
|
||||||
|
}
|
||||||
|
return reply.write(out, .no_such_device);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() void {
|
pub fn main() void {
|
||||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: out of memory\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: out of memory\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
var has_two_channels = false;
|
var has_two_channels = false;
|
||||||
|
var maybe_controller: ?ps2.Controller = null;
|
||||||
|
var maybe_interrupt_index: ?u64 = null;
|
||||||
// The 8042's IO ports (0x60/0x64) are enumerated under the keyboard ACPI node
|
// The 8042's IO ports (0x60/0x64) are enumerated under the keyboard ACPI node
|
||||||
// (PNP0303), so we init the controller from that descriptor — but which device
|
// (PNP0303), so we init the controller from that descriptor — but which device
|
||||||
// is on which port is decided later by identify, not by this HID.
|
// is on which port is decided later by identify, not by this HID.
|
||||||
const maybe_controller_device_descriptor = device.findDeviceDescriptorByHid(buffer, "PNP0303");
|
const maybe_controller_device_descriptor = device.findDeviceDescriptorByHid(buffer, acpi_ids.HardwareId.ps2_keyboard.hid());
|
||||||
if (maybe_controller_device_descriptor) |controller_device_descriptor| {
|
if (maybe_controller_device_descriptor) |controller_device_descriptor| {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: found PS/2 controller\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: found PS/2 controller\n");
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: initializing controller\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: initializing controller\n");
|
||||||
|
|
||||||
if (!device.claim(controller_device_descriptor.id)) {
|
if (!device.claim(controller_device_descriptor.id)) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: unable to claim controller \n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: unable to claim controller \n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = ps2.Controller.init(controller_device_descriptor) orelse {
|
const controller = ps2.Controller.init(controller_device_descriptor) orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: controller is missing its IO ports\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller is missing its IO ports\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
maybe_controller = controller;
|
||||||
|
maybe_interrupt_index = findInterruptResourceIndex(controller_device_descriptor);
|
||||||
|
|
||||||
controller.disablePort(.One);
|
controller.disablePort(.one);
|
||||||
controller.disablePort(.Two);
|
controller.disablePort(.two);
|
||||||
controller.flushOutputBuffer();
|
controller.flushOutputBuffer();
|
||||||
|
|
||||||
const current = controller.readConfigurationByte() orelse {
|
const current = controller.readConfigurationByte() orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller configuration timed out\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,93 +132,181 @@ pub fn main() void {
|
|||||||
ps2.configuration_first_port_translation);
|
ps2.configuration_first_port_translation);
|
||||||
|
|
||||||
if (controller.writeConfigurationByte(update) == null) {
|
if (controller.writeConfigurationByte(update) == null) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller configuration timed out\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controller.performSelfTest()) |reply| {
|
if (controller.performSelfTest()) |reply| {
|
||||||
if (reply != ps2.response_controller_test_passed) {
|
if (reply != ps2.response_controller_test_passed) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: perform controller self test failed\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: perform controller self test failed\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: controller self test timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller self test timed out\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
has_two_channels = controller.hasTwoChannels() orelse {
|
has_two_channels = controller.hasTwoChannels() orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: controller channels timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller channels timed out\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (has_two_channels) {
|
if (has_two_channels) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: has two channels\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: has two channels\n");
|
||||||
// keep the bus quiet until we have tested the ports and are ready to use them
|
// keep the bus quiet until we have tested the ports and are ready to use them
|
||||||
controller.disablePort(.Two);
|
controller.disablePort(.two);
|
||||||
} else {
|
} else {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: has one channel\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: has one channel\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// interface tests: always test port 1, test port 2 only if it exists
|
// interface tests: always test port 1, test port 2 only if it exists
|
||||||
const port_one_works = (controller.testPort(.One) orelse {
|
const port_one_works = (controller.testPort(.one) orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: port 1 test timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: port 1 test timed out\n");
|
||||||
return;
|
return;
|
||||||
}) == ps2.response_port_test_passed;
|
}) == ps2.response_port_test_passed;
|
||||||
|
|
||||||
var port_two_works = false;
|
var port_two_works = false;
|
||||||
if (has_two_channels) {
|
if (has_two_channels) {
|
||||||
port_two_works = (controller.testPort(.Two) orelse {
|
port_two_works = (controller.testPort(.two) orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: port 2 test timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: port 2 test timed out\n");
|
||||||
return;
|
return;
|
||||||
}) == ps2.response_port_test_passed;
|
}) == ps2.response_port_test_passed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!port_one_works and !port_two_works) {
|
if (!port_one_works and !port_two_works) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: no usable ports\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: no usable ports\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// enable the working ports and, via a read-modify-write, their interrupts
|
// Enable the working ports. Their interrupts stay off until IRQ1 is bound
|
||||||
controller.enablePort(.One);
|
// below — reset and identify use polled reads, which must never race the
|
||||||
if (port_two_works) controller.enablePort(.Two);
|
// interrupt-driven drain loop for bytes.
|
||||||
|
controller.enablePort(.one);
|
||||||
var configuration = controller.readConfigurationByte() orelse {
|
if (port_two_works) controller.enablePort(.two);
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if (port_one_works) configuration |= ps2.Port.One.interruptBit();
|
|
||||||
if (port_two_works) configuration |= ps2.Port.Two.interruptBit();
|
|
||||||
_ = controller.writeConfigurationByte(configuration);
|
|
||||||
|
|
||||||
// reset each working device; a failing device is logged but does not
|
// reset each working device; a failing device is logged but does not
|
||||||
// abort bring-up of the other one
|
// abort bring-up of the other one
|
||||||
if (port_one_works) {
|
if (port_one_works) {
|
||||||
if (controller.resetDevice(.One)) |passed| {
|
if (controller.resetDevice(.one)) |passed| {
|
||||||
if (!passed) _ = runtime.system.write("system/drivers/ps2-bus: port 1 device reset failed\n");
|
if (!passed) _ = runtime.system.write("/system/drivers/ps2-bus: port 1 device reset failed\n");
|
||||||
} else {
|
} else {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: port 1 device reset timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: port 1 device reset timed out\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (port_two_works) {
|
if (port_two_works) {
|
||||||
if (controller.resetDevice(.Two)) |passed| {
|
if (controller.resetDevice(.two)) |passed| {
|
||||||
if (!passed) _ = runtime.system.write("system/drivers/ps2-bus: port 2 device reset failed\n");
|
if (!passed) _ = runtime.system.write("/system/drivers/ps2-bus: port 2 device reset failed\n");
|
||||||
} else {
|
} else {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: port 2 device reset timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: port 2 device reset timed out\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identify the device on each working port and hand it off to the driver
|
// Identify the device on each working port and hand it off to the driver
|
||||||
// that matches what it reported — a port is not assumed to be a keyboard
|
// that matches what it reported — a port is not assumed to be a keyboard
|
||||||
// or a mouse by its number.
|
// or a mouse by its number.
|
||||||
if (port_one_works) spawnIdentifiedDriver(controller, .One);
|
if (port_one_works) port_device_types[@intFromEnum(ps2.Port.one)] = spawnIdentifiedDriver(controller, .one);
|
||||||
if (port_two_works) spawnIdentifiedDriver(controller, .Two);
|
if (port_two_works) port_device_types[@intFromEnum(ps2.Port.two)] = spawnIdentifiedDriver(controller, .two);
|
||||||
} else {
|
} else {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: no PS/2 controller found\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: no PS/2 controller found\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: ok\n");
|
const controller = maybe_controller.?;
|
||||||
while (true) runtime.system.sleep(1000);
|
const interrupt_index = maybe_interrupt_index orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller is missing its IRQ\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The endpoint the child drivers attach to and IRQ1 wakes. Registered under a
|
||||||
|
// well-known id so the children can find it, the way input subscribers find
|
||||||
|
// the input service.
|
||||||
|
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus: no endpoint\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (!ipc.register(.ps2_bus, endpoint)) {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus: register failed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From here on, only the interrupt path reads the data port. Drop anything a
|
||||||
|
// device sent between enable-scanning and now, bind the IRQs, and only then
|
||||||
|
// let the controller raise them — an interrupt with nobody bound is lost.
|
||||||
|
controller.drainOutputBuffer();
|
||||||
|
if (!device.irqBind(controller.device_id, interrupt_index, endpoint)) {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus: irq_bind failed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port 2's interrupt (IRQ12) is enumerated on the auxiliary device's own ACPI
|
||||||
|
// node (PNP0F13), not on the controller's — so if port 2 carries a device,
|
||||||
|
// claim that node too and route its IRQ to the same endpoint. The IRQ belongs
|
||||||
|
// to the *port*, whatever device identify found on it.
|
||||||
|
var maybe_auxiliary_interrupt: ?struct { device_id: u64, interrupt_index: u64, gsi: u64 } = null;
|
||||||
|
if (port_device_types[@intFromEnum(ps2.Port.two)] != null) {
|
||||||
|
if (device.findDeviceDescriptorByHid(buffer, acpi_ids.HardwareId.ps2_mouse.hid())) |descriptor| {
|
||||||
|
if (findInterruptResourceIndex(descriptor)) |auxiliary_index| {
|
||||||
|
if (device.claim(descriptor.id) and device.irqBind(descriptor.id, auxiliary_index, endpoint)) {
|
||||||
|
maybe_auxiliary_interrupt = .{
|
||||||
|
.device_id = descriptor.id,
|
||||||
|
.interrupt_index = auxiliary_index,
|
||||||
|
.gsi = descriptor.resources[auxiliary_index].start,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus: auxiliary irq_bind failed\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var configuration = controller.readConfigurationByte() orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller configuration timed out\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (port_device_types[@intFromEnum(ps2.Port.one)] != null) configuration |= ps2.Port.one.interruptBit();
|
||||||
|
if (maybe_auxiliary_interrupt != null) configuration |= ps2.Port.two.interruptBit();
|
||||||
|
_ = controller.writeConfigurationByte(configuration);
|
||||||
|
|
||||||
|
_ = runtime.system.write("/system/drivers/ps2-bus: ok\n");
|
||||||
|
|
||||||
|
// The forwarding loop: an IRQ1 notification drains the output buffer, routing
|
||||||
|
// each byte to the attached driver of the port it came from; a client message
|
||||||
|
// is a child driver's AttachRequest.
|
||||||
|
var reply_buffer: [@sizeOf(ps2.AttachReply)]u8 = undefined;
|
||||||
|
var reply_len: usize = 0;
|
||||||
|
var receive: [@sizeOf(ps2.AttachRequest)]u8 = undefined;
|
||||||
|
while (true) {
|
||||||
|
const got = ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
|
||||||
|
if (got.isNotification()) {
|
||||||
|
reply_len = 0;
|
||||||
|
if (got.isMessage() or got.isChildExit()) continue; // nothing sends us these
|
||||||
|
while (true) {
|
||||||
|
const current_status = ps2.status(controller.device_id, controller.status_index);
|
||||||
|
if (current_status & ps2.status_output_buffer_full == 0) break;
|
||||||
|
const byte = device.ioRead(controller.device_id, controller.data_index, 0, 1) orelse break;
|
||||||
|
const port: ps2.Port = if (current_status & ps2.status_auxiliary_output != 0) .two else .one;
|
||||||
|
if (port_endpoints[@intFromEnum(port)]) |child| {
|
||||||
|
const forwarded = ps2.ForwardedByte{ .port = @intFromEnum(port), .byte = byte };
|
||||||
|
_ = ipc.send(child, std.mem.asBytes(&forwarded));
|
||||||
|
}
|
||||||
|
// An unattached port's byte is dropped — e.g. a keystroke before
|
||||||
|
// the keyboard driver has attached.
|
||||||
|
}
|
||||||
|
// Re-arm the line that woke us: the notification badge carries the
|
||||||
|
// GSI, and IRQ1 and IRQ12 are acked through different device claims.
|
||||||
|
if (maybe_auxiliary_interrupt) |auxiliary| {
|
||||||
|
if (got.source() == auxiliary.gsi) {
|
||||||
|
_ = device.irqAck(auxiliary.device_id, auxiliary.interrupt_index);
|
||||||
|
} else {
|
||||||
|
_ = device.irqAck(controller.device_id, interrupt_index);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_ = device.irqAck(controller.device_id, interrupt_index);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
reply_len = handleAttach(receive[0..got.len], got, &reply_buffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const panic = runtime.panic;
|
pub const panic = runtime.panic;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! shared definitions between the different PS/2 drivers
|
//! shared definitions between the different PS/2 drivers
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const runtime = @import("runtime");
|
const runtime = @import("runtime");
|
||||||
|
const acpi_ids = @import("acpi-ids");
|
||||||
const device = runtime.device;
|
const device = runtime.device;
|
||||||
const system = runtime.system;
|
const system = runtime.system;
|
||||||
|
|
||||||
@@ -49,12 +50,15 @@ pub const cmd_write_second_port_output: u8 = 0xD3; // write next data byte to th
|
|||||||
pub const cmd_write_second_port_input: u8 = 0xD4; // write next data byte to the second port input buffer (to the mouse)
|
pub const cmd_write_second_port_input: u8 = 0xD4; // write next data byte to the second port input buffer (to the mouse)
|
||||||
pub const cmd_pulse_system_reset: u8 = 0xFE; // pulse output line 0 low: resets the CPU
|
pub const cmd_pulse_system_reset: u8 = 0xFE; // pulse output line 0 low: resets the CPU
|
||||||
|
|
||||||
/// PS/2 status register bits (read from the status port, 0x64). Bits 4 and 5
|
/// PS/2 status register bits (read from the status port, 0x64). Bit 4 is
|
||||||
/// are chipset-specific and intentionally omitted.
|
/// chipset-specific and intentionally omitted.
|
||||||
pub const status_output_buffer_full: u8 = 1 << 0; // 1 = a byte is waiting to be read from the data port
|
pub const status_output_buffer_full: u8 = 1 << 0; // 1 = a byte is waiting to be read from the data port
|
||||||
pub const status_input_buffer_full: u8 = 1 << 1; // 1 = the controller has not yet consumed the last write
|
pub const status_input_buffer_full: u8 = 1 << 1; // 1 = the controller has not yet consumed the last write
|
||||||
pub const status_system_flag: u8 = 1 << 2; // set once the controller passes POST
|
pub const status_system_flag: u8 = 1 << 2; // set once the controller passes POST
|
||||||
pub const status_command_or_data: u8 = 1 << 3; // 1 = last write was a command, 0 = data
|
pub const status_command_or_data: u8 = 1 << 3; // 1 = last write was a command, 0 = data
|
||||||
|
/// Chipset-specific in the original spec, universal in practice on dual-channel
|
||||||
|
/// controllers: set = the waiting byte came from the second port (the mouse).
|
||||||
|
pub const status_auxiliary_output: u8 = 1 << 5;
|
||||||
pub const status_timeout_error: u8 = 1 << 6; // 1 = time-out error
|
pub const status_timeout_error: u8 = 1 << 6; // 1 = time-out error
|
||||||
pub const status_parity_error: u8 = 1 << 7; // 1 = parity error
|
pub const status_parity_error: u8 = 1 << 7; // 1 = parity error
|
||||||
|
|
||||||
@@ -142,52 +146,53 @@ pub fn readData(id: u64, status_index: u64, data_index: u64, timeout_nanoseconds
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn writeData(id: u64, status_index: u64, data_index: u64, byte: u8, timeout_nanoseconds: u64) bool {
|
pub fn writeData(id: u64, status_index: u64, data_index: u64, byte: u8, timeout_nanoseconds: u64) bool {
|
||||||
// IBF lives in the status register (0x64); wait for it to clear there, then write the data port (0x60)
|
// IBF lives in the status register (0x64); wait for it to clear there, then write the data port
|
||||||
|
// (0x60)
|
||||||
if (!waitWritable(id, status_index, timeout_nanoseconds)) return false;
|
if (!waitWritable(id, status_index, timeout_nanoseconds)) return false;
|
||||||
return device.ioWrite(id, data_index, 0, 1, byte);
|
return device.ioWrite(id, data_index, 0, 1, byte);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Port = enum(u2) {
|
pub const Port = enum(u2) {
|
||||||
One,
|
one,
|
||||||
Two,
|
two,
|
||||||
|
|
||||||
/// Command register byte that disables this port.
|
/// Command register byte that disables this port.
|
||||||
fn disableCommand(self: Port) u8 {
|
fn disableCommand(self: Port) u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.One => cmd_disable_first_port,
|
.one => cmd_disable_first_port,
|
||||||
.Two => cmd_disable_second_port,
|
.two => cmd_disable_second_port,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Command register byte that enables this port (and its clock).
|
/// Command register byte that enables this port (and its clock).
|
||||||
fn enableCommand(self: Port) u8 {
|
fn enableCommand(self: Port) u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.One => cmd_enable_first_port,
|
.one => cmd_enable_first_port,
|
||||||
.Two => cmd_enable_second_port,
|
.two => cmd_enable_second_port,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Command register byte that runs this port's interface test.
|
/// Command register byte that runs this port's interface test.
|
||||||
fn testCommand(self: Port) u8 {
|
fn testCommand(self: Port) u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.One => cmd_test_first_port,
|
.one => cmd_test_first_port,
|
||||||
.Two => cmd_test_second_port,
|
.two => cmd_test_second_port,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration-byte bit that, when set, disables this port's clock.
|
/// Configuration-byte bit that, when set, disables this port's clock.
|
||||||
pub fn clockDisabledBit(self: Port) u8 {
|
pub fn clockDisabledBit(self: Port) u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.One => configuration_first_port_clock_disabled,
|
.one => configuration_first_port_clock_disabled,
|
||||||
.Two => configuration_second_port_clock_disabled,
|
.two => configuration_second_port_clock_disabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration-byte bit that, when set, enables this port's interrupt.
|
/// Configuration-byte bit that, when set, enables this port's interrupt.
|
||||||
pub fn interruptBit(self: Port) u8 {
|
pub fn interruptBit(self: Port) u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.One => configuration_first_port_interrupt,
|
.one => configuration_first_port_interrupt,
|
||||||
.Two => configuration_second_port_interrupt,
|
.two => configuration_second_port_interrupt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,8 +200,8 @@ pub const Port = enum(u2) {
|
|||||||
/// output buffer (makes a byte appear as if it came from the device).
|
/// output buffer (makes a byte appear as if it came from the device).
|
||||||
pub fn writeOutputBufferCommand(self: Port) u8 {
|
pub fn writeOutputBufferCommand(self: Port) u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.One => cmd_write_first_port_output,
|
.one => cmd_write_first_port_output,
|
||||||
.Two => cmd_write_second_port_output,
|
.two => cmd_write_second_port_output,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,24 +210,24 @@ pub const Port = enum(u2) {
|
|||||||
/// prefix (null); port 2 requires the "write second port input" command.
|
/// prefix (null); port 2 requires the "write second port input" command.
|
||||||
pub fn deviceInputCommand(self: Port) ?u8 {
|
pub fn deviceInputCommand(self: Port) ?u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.One => null,
|
.one => null,
|
||||||
.Two => cmd_write_second_port_input,
|
.two => cmd_write_second_port_input,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controller output-port bit driving this port's clock line.
|
/// Controller output-port bit driving this port's clock line.
|
||||||
pub fn outputPortClockBit(self: Port) u8 {
|
pub fn outputPortClockBit(self: Port) u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.One => output_port_first_port_clock,
|
.one => output_port_first_port_clock,
|
||||||
.Two => output_port_second_port_clock,
|
.two => output_port_second_port_clock,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controller output-port bit driving this port's data line.
|
/// Controller output-port bit driving this port's data line.
|
||||||
pub fn outputPortDataBit(self: Port) u8 {
|
pub fn outputPortDataBit(self: Port) u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.One => output_port_first_port_data,
|
.one => output_port_first_port_data,
|
||||||
.Two => output_port_second_port_data,
|
.two => output_port_second_port_data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,18 +235,19 @@ pub const Port = enum(u2) {
|
|||||||
/// (wired to the port's IRQ line).
|
/// (wired to the port's IRQ line).
|
||||||
pub fn outputPortBufferFullBit(self: Port) u8 {
|
pub fn outputPortBufferFullBit(self: Port) u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.One => output_port_first_port_output_full,
|
.one => output_port_first_port_output_full,
|
||||||
.Two => output_port_second_port_output_full,
|
.two => output_port_second_port_output_full,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The kind of device attached to a port, as reported by the device itself in
|
/// The kind of device attached to a port, as reported by the device itself in
|
||||||
/// response to the identify command — not assumed from the port number.
|
/// response to the identify command — not assumed from the port number. Fixed
|
||||||
pub const DeviceType = enum {
|
/// `u32` values because the type also travels in an `AttachRequest`.
|
||||||
keyboard,
|
pub const DeviceType = enum(u32) {
|
||||||
mouse,
|
keyboard = 0,
|
||||||
unknown,
|
mouse = 1,
|
||||||
|
unknown = 2,
|
||||||
|
|
||||||
/// Initial-ramdisk name of the driver that serves this device type, or null
|
/// Initial-ramdisk name of the driver that serves this device type, or null
|
||||||
/// if we could not classify it.
|
/// if we could not classify it.
|
||||||
@@ -257,13 +263,55 @@ pub const DeviceType = enum {
|
|||||||
/// its command-line argument, or null if we could not classify it.
|
/// its command-line argument, or null if we could not classify it.
|
||||||
pub fn hid(self: DeviceType) ?[]const u8 {
|
pub fn hid(self: DeviceType) ?[]const u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.keyboard => "PNP0303",
|
.keyboard => acpi_ids.HardwareId.ps2_keyboard.hid(),
|
||||||
.mouse => "PNP0F13",
|
.mouse => acpi_ids.HardwareId.ps2_mouse.hid(),
|
||||||
.unknown => null,
|
.unknown => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- the bus <-> child-driver forwarding protocol -----------------------------
|
||||||
|
//
|
||||||
|
// The 8042's ports and IRQ1 live on the PNP0303 node that only the ps2-bus driver
|
||||||
|
// claims, so the child device drivers (ps2-keyboard, ps2-mouse) cannot read port
|
||||||
|
// 0x60 themselves. Instead each child **attaches**: it calls the bus's well-known
|
||||||
|
// `ps2_bus` endpoint with an `AttachRequest`, handing over its own endpoint as the
|
||||||
|
// call's capability. From then on the bus forwards every byte the device sends as
|
||||||
|
// a `ForwardedByte` via the asynchronous `ipc.send` — the IRQ path in the bus can
|
||||||
|
// never block on a slow child, and the child never touches the controller.
|
||||||
|
|
||||||
|
/// A child driver registering for its device's bytes. `device_type` is a
|
||||||
|
/// `DeviceType` value; the child's receive endpoint travels as the call's
|
||||||
|
/// capability (`send_cap`).
|
||||||
|
pub const AttachRequest = extern struct {
|
||||||
|
device_type: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// How the bus answered an `AttachRequest` (`AttachReply.status`).
|
||||||
|
pub const AttachStatus = enum(i32) {
|
||||||
|
ok = 0,
|
||||||
|
/// The request was malformed (too short to be an `AttachRequest`).
|
||||||
|
invalid_request = -1,
|
||||||
|
/// The call carried no endpoint capability to forward to.
|
||||||
|
missing_endpoint = -2,
|
||||||
|
/// No port identified a device of the requested type.
|
||||||
|
no_such_device = -3,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Reply to an `AttachRequest`. `status` is an `AttachStatus` value.
|
||||||
|
pub const AttachReply = extern struct {
|
||||||
|
status: i32,
|
||||||
|
_padding: u32 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// One raw byte read from the data port, forwarded to the attached child whose
|
||||||
|
/// port it came from (routed by the status register's auxiliary-output bit).
|
||||||
|
pub const ForwardedByte = extern struct {
|
||||||
|
/// The `Port` the byte came from, as `@intFromEnum`.
|
||||||
|
port: u32,
|
||||||
|
byte: u32,
|
||||||
|
};
|
||||||
|
|
||||||
/// A single PS/2 (8042) controller. Construct one with `Controller.init` and
|
/// A single PS/2 (8042) controller. Construct one with `Controller.init` and
|
||||||
/// drive the controller through its methods; there is only ever one 8042 per
|
/// drive the controller through its methods; there is only ever one 8042 per
|
||||||
/// machine, but holding the resolved resource indices in an instance keeps the
|
/// machine, but holding the resolved resource indices in an instance keeps the
|
||||||
@@ -345,9 +393,9 @@ pub const Controller = struct {
|
|||||||
/// enabled; the caller should disable it again to keep the bus quiet until
|
/// enabled; the caller should disable it again to keep the bus quiet until
|
||||||
/// device bring-up.
|
/// device bring-up.
|
||||||
pub fn hasTwoChannels(self: Controller) ?bool {
|
pub fn hasTwoChannels(self: Controller) ?bool {
|
||||||
self.enablePort(.Two);
|
self.enablePort(.two);
|
||||||
const configuration = self.readConfigurationByte() orelse return null;
|
const configuration = self.readConfigurationByte() orelse return null;
|
||||||
return (configuration & Port.Two.clockDisabledBit()) == 0;
|
return (configuration & Port.two.clockDisabledBit()) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset the device attached to `port` (device command 0xFF) and wait for
|
/// Reset the device attached to `port` (device command 0xFF) and wait for
|
||||||
|
|||||||
@@ -0,0 +1,389 @@
|
|||||||
|
//! PS/2 scancode set 2 → USB HID usage decoding, plus the keyboard state a driver
|
||||||
|
//! needs on top of it (pressed keys, modifier tracking, caps-lock toggle).
|
||||||
|
//!
|
||||||
|
//! Set 2 is what a keyboard sends when the 8042's legacy set-1 translation is off —
|
||||||
|
//! which is how ps2-bus.zig deliberately configures the controller. A key's **make**
|
||||||
|
//! code is one byte (two with an `E0` prefix for the "extended" keys added after the
|
||||||
|
//! original AT layout); its **break** code is the same code behind an `F0` prefix.
|
||||||
|
//! Pause alone is an eight-byte `E1` sequence with no break.
|
||||||
|
//!
|
||||||
|
//! The output vocabulary is USB HID keyboard-page usages (a=4, enter=40, ...), the
|
||||||
|
//! same numbering the input protocol's `Keycode` and the xkeyboard-config layout
|
||||||
|
//! tables use — so a decoded usage indexes a layout directly.
|
||||||
|
//!
|
||||||
|
//! Everything here is pure (no imports beyond `std`, no IO), so it is host-testable:
|
||||||
|
//! the tests at the bottom run under `zig build test`.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// --- USB HID usages the state machine itself needs to recognize --------------
|
||||||
|
|
||||||
|
pub const usage_caps_lock: u8 = 0x39;
|
||||||
|
pub const usage_left_control: u8 = 0xE0;
|
||||||
|
pub const usage_left_shift: u8 = 0xE1;
|
||||||
|
pub const usage_left_alt: u8 = 0xE2;
|
||||||
|
pub const usage_right_control: u8 = 0xE4;
|
||||||
|
pub const usage_right_shift: u8 = 0xE5;
|
||||||
|
pub const usage_right_alt: u8 = 0xE6; // AltGr — selects XKB level 3
|
||||||
|
|
||||||
|
// --- scancode set 2 → HID usage tables ---------------------------------------
|
||||||
|
|
||||||
|
/// Single-byte (non-`E0`) make codes. Zero means "no key" — protocol bytes (ACK,
|
||||||
|
/// BAT results) and reserved codes land there and decode to nothing.
|
||||||
|
pub const set2_base: [256]u8 = blk: {
|
||||||
|
var table = [_]u8{0} ** 256;
|
||||||
|
// function row
|
||||||
|
table[0x01] = 0x42; // F9
|
||||||
|
table[0x03] = 0x3E; // F5
|
||||||
|
table[0x04] = 0x3C; // F3
|
||||||
|
table[0x05] = 0x3A; // F1
|
||||||
|
table[0x06] = 0x3B; // F2
|
||||||
|
table[0x07] = 0x45; // F12
|
||||||
|
table[0x09] = 0x43; // F10
|
||||||
|
table[0x0A] = 0x41; // F8
|
||||||
|
table[0x0B] = 0x3F; // F6
|
||||||
|
table[0x0C] = 0x3D; // F4
|
||||||
|
table[0x78] = 0x44; // F11
|
||||||
|
table[0x83] = 0x40; // F7
|
||||||
|
// letters
|
||||||
|
table[0x1C] = 0x04; // A
|
||||||
|
table[0x32] = 0x05; // B
|
||||||
|
table[0x21] = 0x06; // C
|
||||||
|
table[0x23] = 0x07; // D
|
||||||
|
table[0x24] = 0x08; // E
|
||||||
|
table[0x2B] = 0x09; // F
|
||||||
|
table[0x34] = 0x0A; // G
|
||||||
|
table[0x33] = 0x0B; // H
|
||||||
|
table[0x43] = 0x0C; // I
|
||||||
|
table[0x3B] = 0x0D; // J
|
||||||
|
table[0x42] = 0x0E; // K
|
||||||
|
table[0x4B] = 0x0F; // L
|
||||||
|
table[0x3A] = 0x10; // M
|
||||||
|
table[0x31] = 0x11; // N
|
||||||
|
table[0x44] = 0x12; // O
|
||||||
|
table[0x4D] = 0x13; // P
|
||||||
|
table[0x15] = 0x14; // Q
|
||||||
|
table[0x2D] = 0x15; // R
|
||||||
|
table[0x1B] = 0x16; // S
|
||||||
|
table[0x2C] = 0x17; // T
|
||||||
|
table[0x3C] = 0x18; // U
|
||||||
|
table[0x2A] = 0x19; // V
|
||||||
|
table[0x1D] = 0x1A; // W
|
||||||
|
table[0x22] = 0x1B; // X
|
||||||
|
table[0x35] = 0x1C; // Y
|
||||||
|
table[0x1A] = 0x1D; // Z
|
||||||
|
// digit row
|
||||||
|
table[0x16] = 0x1E; // 1
|
||||||
|
table[0x1E] = 0x1F; // 2
|
||||||
|
table[0x26] = 0x20; // 3
|
||||||
|
table[0x25] = 0x21; // 4
|
||||||
|
table[0x2E] = 0x22; // 5
|
||||||
|
table[0x36] = 0x23; // 6
|
||||||
|
table[0x3D] = 0x24; // 7
|
||||||
|
table[0x3E] = 0x25; // 8
|
||||||
|
table[0x46] = 0x26; // 9
|
||||||
|
table[0x45] = 0x27; // 0
|
||||||
|
// control and whitespace
|
||||||
|
table[0x5A] = 0x28; // Enter
|
||||||
|
table[0x76] = 0x29; // Escape
|
||||||
|
table[0x66] = 0x2A; // Backspace
|
||||||
|
table[0x0D] = 0x2B; // Tab
|
||||||
|
table[0x29] = 0x2C; // Space
|
||||||
|
// punctuation
|
||||||
|
table[0x4E] = 0x2D; // - _
|
||||||
|
table[0x55] = 0x2E; // = +
|
||||||
|
table[0x54] = 0x2F; // [ {
|
||||||
|
table[0x5B] = 0x30; // ] }
|
||||||
|
table[0x5D] = 0x31; // \ | (non-US hash on ISO boards, same position)
|
||||||
|
table[0x4C] = 0x33; // ; :
|
||||||
|
table[0x52] = 0x34; // ' "
|
||||||
|
table[0x0E] = 0x35; // ` ~
|
||||||
|
table[0x41] = 0x36; // , <
|
||||||
|
table[0x49] = 0x37; // . >
|
||||||
|
table[0x4A] = 0x38; // / ?
|
||||||
|
table[0x61] = 0x64; // non-US backslash (the extra ISO key between shift and Z)
|
||||||
|
// locks
|
||||||
|
table[0x58] = usage_caps_lock;
|
||||||
|
table[0x77] = 0x53; // Num Lock
|
||||||
|
table[0x7E] = 0x47; // Scroll Lock
|
||||||
|
// keypad
|
||||||
|
table[0x7C] = 0x55; // keypad *
|
||||||
|
table[0x7B] = 0x56; // keypad -
|
||||||
|
table[0x79] = 0x57; // keypad +
|
||||||
|
table[0x69] = 0x59; // keypad 1
|
||||||
|
table[0x72] = 0x5A; // keypad 2
|
||||||
|
table[0x7A] = 0x5B; // keypad 3
|
||||||
|
table[0x6B] = 0x5C; // keypad 4
|
||||||
|
table[0x73] = 0x5D; // keypad 5
|
||||||
|
table[0x74] = 0x5E; // keypad 6
|
||||||
|
table[0x6C] = 0x5F; // keypad 7
|
||||||
|
table[0x75] = 0x60; // keypad 8
|
||||||
|
table[0x7D] = 0x61; // keypad 9
|
||||||
|
table[0x70] = 0x62; // keypad 0
|
||||||
|
table[0x71] = 0x63; // keypad .
|
||||||
|
// modifiers
|
||||||
|
table[0x14] = usage_left_control;
|
||||||
|
table[0x12] = usage_left_shift;
|
||||||
|
table[0x11] = usage_left_alt;
|
||||||
|
table[0x59] = usage_right_shift;
|
||||||
|
break :blk table;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// `E0`-prefixed make codes. `E0 12` is the "fake shift" the keyboard wraps around
|
||||||
|
/// Print Screen and navigation keys when a real shift is involved; it maps to zero
|
||||||
|
/// here, so it decodes to nothing and only the real key comes through.
|
||||||
|
pub const set2_extended: [256]u8 = blk: {
|
||||||
|
var table = [_]u8{0} ** 256;
|
||||||
|
table[0x11] = usage_right_alt;
|
||||||
|
table[0x14] = usage_right_control;
|
||||||
|
table[0x1F] = 0xE3; // left GUI
|
||||||
|
table[0x27] = 0xE7; // right GUI
|
||||||
|
table[0x2F] = 0x65; // application (menu)
|
||||||
|
table[0x7C] = 0x46; // Print Screen (arrives as E0 12 E0 7C; the E0 12 decodes to nothing)
|
||||||
|
table[0x4A] = 0x54; // keypad /
|
||||||
|
table[0x5A] = 0x58; // keypad Enter
|
||||||
|
table[0x70] = 0x49; // Insert
|
||||||
|
table[0x6C] = 0x4A; // Home
|
||||||
|
table[0x7D] = 0x4B; // Page Up
|
||||||
|
table[0x71] = 0x4C; // Delete
|
||||||
|
table[0x69] = 0x4D; // End
|
||||||
|
table[0x7A] = 0x4E; // Page Down
|
||||||
|
table[0x74] = 0x4F; // right arrow
|
||||||
|
table[0x6B] = 0x50; // left arrow
|
||||||
|
table[0x72] = 0x51; // down arrow
|
||||||
|
table[0x75] = 0x52; // up arrow
|
||||||
|
break :blk table;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- the byte-stream decoder --------------------------------------------------
|
||||||
|
|
||||||
|
/// One decoded key transition: which key (as a USB HID usage) and whether this is
|
||||||
|
/// a make (press or typematic repeat) or a break (release).
|
||||||
|
pub const DecodedKey = struct {
|
||||||
|
usage: u8,
|
||||||
|
make: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Turns the raw set-2 byte stream into `DecodedKey`s. Feed it every byte the
|
||||||
|
/// keyboard sends; most bytes complete a key and return one, prefix bytes return
|
||||||
|
/// null and arm the state machine for the next byte.
|
||||||
|
pub const Decoder = struct {
|
||||||
|
const State = enum {
|
||||||
|
idle,
|
||||||
|
extended, // saw E0
|
||||||
|
break_prefix, // saw F0
|
||||||
|
extended_break, // saw E0 F0
|
||||||
|
pause_skip, // inside the 8-byte E1 Pause sequence
|
||||||
|
};
|
||||||
|
|
||||||
|
state: State = .idle,
|
||||||
|
/// Bytes still to swallow in `pause_skip`.
|
||||||
|
skip: u8 = 0,
|
||||||
|
|
||||||
|
/// The whole Pause make sequence is `E1 14 77 E1 F0 14 F0 77` — seven bytes
|
||||||
|
/// after the leading `E1`, and no break sequence ever follows.
|
||||||
|
const pause_bytes_after_e1: u8 = 7;
|
||||||
|
|
||||||
|
pub fn feed(self: *Decoder, byte: u8) ?DecodedKey {
|
||||||
|
switch (self.state) {
|
||||||
|
.idle => switch (byte) {
|
||||||
|
0xE0 => self.state = .extended,
|
||||||
|
0xF0 => self.state = .break_prefix,
|
||||||
|
0xE1 => {
|
||||||
|
self.state = .pause_skip;
|
||||||
|
self.skip = pause_bytes_after_e1;
|
||||||
|
},
|
||||||
|
// Anything else is a make code — or a protocol byte (0xFA ACK,
|
||||||
|
// 0xAA BAT-passed, 0xEE echo, ...), which the tables map to zero.
|
||||||
|
else => return decoded(set2_base[byte], true),
|
||||||
|
},
|
||||||
|
.extended => switch (byte) {
|
||||||
|
0xF0 => self.state = .extended_break,
|
||||||
|
else => {
|
||||||
|
self.state = .idle;
|
||||||
|
return decoded(set2_extended[byte], true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.break_prefix => {
|
||||||
|
self.state = .idle;
|
||||||
|
return decoded(set2_base[byte], false);
|
||||||
|
},
|
||||||
|
.extended_break => {
|
||||||
|
self.state = .idle;
|
||||||
|
return decoded(set2_extended[byte], false);
|
||||||
|
},
|
||||||
|
.pause_skip => {
|
||||||
|
self.skip -= 1;
|
||||||
|
if (self.skip == 0) self.state = .idle;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decoded(usage: u8, make: bool) ?DecodedKey {
|
||||||
|
if (usage == 0) return null; // unmapped or a protocol byte
|
||||||
|
return .{ .usage = usage, .make = make };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- driver-side keyboard state -----------------------------------------------
|
||||||
|
|
||||||
|
/// What a key transition did, plus the modifier state to stamp on the resulting
|
||||||
|
/// events (snapshotted after the transition was applied).
|
||||||
|
pub const Transition = struct {
|
||||||
|
pub const Action = enum {
|
||||||
|
pressed, // physical make of a key that was up
|
||||||
|
repeated, // typematic make of a key already down — no new key_down
|
||||||
|
released, // physical break
|
||||||
|
};
|
||||||
|
action: Action,
|
||||||
|
modifiers: ModifierSnapshot,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The modifier state at one instant, in both vocabularies a driver needs: the
|
||||||
|
/// input protocol's coarse bits (shift/control/alt) and the level-selection
|
||||||
|
/// inputs xkeyboard-config takes (shift, caps_lock, AltGr as level3).
|
||||||
|
pub const ModifierSnapshot = struct {
|
||||||
|
shift: bool, // either shift held
|
||||||
|
control: bool, // either control held
|
||||||
|
alt: bool, // either alt held (including AltGr)
|
||||||
|
right_alt: bool, // AltGr specifically — the XKB level-3 selector
|
||||||
|
caps_lock: bool, // the toggle, not the key
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Tracks which keys are physically down and the caps-lock toggle, and classifies
|
||||||
|
/// each decoded transition. Pure state — no IO — so repeat detection and modifier
|
||||||
|
/// snapshots are host-testable.
|
||||||
|
pub const KeyboardState = struct {
|
||||||
|
/// One bit per HID usage: set while the key is physically down.
|
||||||
|
pressed: [32]u8 = [_]u8{0} ** 32,
|
||||||
|
caps_lock: bool = false,
|
||||||
|
|
||||||
|
pub fn apply(self: *KeyboardState, key: DecodedKey) Transition {
|
||||||
|
const already_down = self.isPressed(key.usage);
|
||||||
|
if (key.make) {
|
||||||
|
if (!already_down) {
|
||||||
|
self.setPressed(key.usage, true);
|
||||||
|
if (key.usage == usage_caps_lock) self.caps_lock = !self.caps_lock;
|
||||||
|
}
|
||||||
|
return .{
|
||||||
|
.action = if (already_down) .repeated else .pressed,
|
||||||
|
.modifiers = self.snapshot(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
self.setPressed(key.usage, false);
|
||||||
|
return .{ .action = .released, .modifiers = self.snapshot() };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isPressed(self: *const KeyboardState, usage: u8) bool {
|
||||||
|
return self.pressed[usage / 8] & (@as(u8, 1) << @intCast(usage % 8)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setPressed(self: *KeyboardState, usage: u8, down: bool) void {
|
||||||
|
const bit = @as(u8, 1) << @intCast(usage % 8);
|
||||||
|
if (down) {
|
||||||
|
self.pressed[usage / 8] |= bit;
|
||||||
|
} else {
|
||||||
|
self.pressed[usage / 8] &= ~bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot(self: *const KeyboardState) ModifierSnapshot {
|
||||||
|
const right_alt = self.isPressed(usage_right_alt);
|
||||||
|
return .{
|
||||||
|
.shift = self.isPressed(usage_left_shift) or self.isPressed(usage_right_shift),
|
||||||
|
.control = self.isPressed(usage_left_control) or self.isPressed(usage_right_control),
|
||||||
|
.alt = self.isPressed(usage_left_alt) or right_alt,
|
||||||
|
.right_alt = right_alt,
|
||||||
|
.caps_lock = self.caps_lock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- tests (host-run via `zig build test`) ------------------------------------
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
/// Feed `bytes` and return the single DecodedKey they should produce (fails the
|
||||||
|
/// test if they produce none or more than one).
|
||||||
|
fn feedOne(decoder: *Decoder, bytes: []const u8) !DecodedKey {
|
||||||
|
var result: ?DecodedKey = null;
|
||||||
|
for (bytes) |byte| {
|
||||||
|
if (decoder.feed(byte)) |key| {
|
||||||
|
try testing.expect(result == null);
|
||||||
|
result = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result orelse error.TestExpectedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn feedNone(decoder: *Decoder, bytes: []const u8) !void {
|
||||||
|
for (bytes) |byte| try testing.expectEqual(@as(?DecodedKey, null), decoder.feed(byte));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "base make and break: A" {
|
||||||
|
var decoder = Decoder{};
|
||||||
|
try testing.expectEqual(DecodedKey{ .usage = 0x04, .make = true }, try feedOne(&decoder, &.{0x1C}));
|
||||||
|
try testing.expectEqual(DecodedKey{ .usage = 0x04, .make = false }, try feedOne(&decoder, &.{ 0xF0, 0x1C }));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "extended make and break: right arrow" {
|
||||||
|
var decoder = Decoder{};
|
||||||
|
try testing.expectEqual(DecodedKey{ .usage = 0x4F, .make = true }, try feedOne(&decoder, &.{ 0xE0, 0x74 }));
|
||||||
|
try testing.expectEqual(DecodedKey{ .usage = 0x4F, .make = false }, try feedOne(&decoder, &.{ 0xE0, 0xF0, 0x74 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "pause: the E1 sequence is consumed silently" {
|
||||||
|
var decoder = Decoder{};
|
||||||
|
try feedNone(&decoder, &.{ 0xE1, 0x14, 0x77, 0xE1, 0xF0, 0x14, 0xF0, 0x77 });
|
||||||
|
// The decoder is back in idle: an ordinary key still decodes.
|
||||||
|
try testing.expectEqual(DecodedKey{ .usage = 0x04, .make = true }, try feedOne(&decoder, &.{0x1C}));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "protocol bytes decode to nothing" {
|
||||||
|
var decoder = Decoder{};
|
||||||
|
try feedNone(&decoder, &.{ 0xFA, 0xAA, 0xEE }); // ACK, BAT-passed, echo
|
||||||
|
}
|
||||||
|
|
||||||
|
test "print screen: the fake-shift E0 12 decodes to nothing" {
|
||||||
|
var decoder = Decoder{};
|
||||||
|
try feedNone(&decoder, &.{ 0xE0, 0x12 });
|
||||||
|
try testing.expectEqual(DecodedKey{ .usage = 0x46, .make = true }, try feedOne(&decoder, &.{ 0xE0, 0x7C }));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "typematic repeat is classified, not re-pressed" {
|
||||||
|
var state = KeyboardState{};
|
||||||
|
const a = DecodedKey{ .usage = 0x04, .make = true };
|
||||||
|
try testing.expectEqual(Transition.Action.pressed, state.apply(a).action);
|
||||||
|
try testing.expectEqual(Transition.Action.repeated, state.apply(a).action);
|
||||||
|
try testing.expectEqual(Transition.Action.repeated, state.apply(a).action);
|
||||||
|
try testing.expectEqual(Transition.Action.released, state.apply(.{ .usage = 0x04, .make = false }).action);
|
||||||
|
try testing.expectEqual(Transition.Action.pressed, state.apply(a).action);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "shift held shows in the snapshot of other keys" {
|
||||||
|
var state = KeyboardState{};
|
||||||
|
_ = state.apply(.{ .usage = usage_left_shift, .make = true });
|
||||||
|
const transition = state.apply(.{ .usage = 0x04, .make = true });
|
||||||
|
try testing.expect(transition.modifiers.shift);
|
||||||
|
try testing.expect(!transition.modifiers.control);
|
||||||
|
_ = state.apply(.{ .usage = usage_left_shift, .make = false });
|
||||||
|
_ = state.apply(.{ .usage = 0x04, .make = false });
|
||||||
|
try testing.expect(!state.apply(.{ .usage = 0x04, .make = true }).modifiers.shift);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "right alt reports both alt and the level-3 selector" {
|
||||||
|
var state = KeyboardState{};
|
||||||
|
_ = state.apply(.{ .usage = usage_right_alt, .make = true });
|
||||||
|
const transition = state.apply(.{ .usage = 0x04, .make = true });
|
||||||
|
try testing.expect(transition.modifiers.alt);
|
||||||
|
try testing.expect(transition.modifiers.right_alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "caps lock toggles on make, not on repeat or break" {
|
||||||
|
var state = KeyboardState{};
|
||||||
|
try testing.expect(state.apply(.{ .usage = usage_caps_lock, .make = true }).modifiers.caps_lock);
|
||||||
|
try testing.expect(state.apply(.{ .usage = usage_caps_lock, .make = true }).modifiers.caps_lock); // repeat
|
||||||
|
try testing.expect(state.apply(.{ .usage = usage_caps_lock, .make = false }).modifiers.caps_lock);
|
||||||
|
try testing.expect(!state.apply(.{ .usage = usage_caps_lock, .make = true }).modifiers.caps_lock); // second press: off
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
//! Pure decoders for USB HID **boot-protocol** reports — the simplified,
|
||||||
|
//! fixed-format reports a boot keyboard and boot mouse send, the USB analog of
|
||||||
|
//! the PS/2 scancode and mouse-packet decoders. No I/O: these turn report bytes
|
||||||
|
//! into make/break transitions and motion, which the usb-hid drivers publish to
|
||||||
|
//! the input service. Host-testable in isolation (like mouse-packet.zig).
|
||||||
|
//!
|
||||||
|
//! "Boot protocol" is a USB HID term (USB HID 1.11 §B) — the device reports in
|
||||||
|
//! this fixed layout after SET_PROTOCOL(boot); it has nothing to do with system
|
||||||
|
//! boot.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// --- keyboard ---------------------------------------------------------------
|
||||||
|
|
||||||
|
/// The 8-byte boot keyboard report: a modifier bitmap, a reserved byte, and up
|
||||||
|
/// to six concurrently-pressed key usages.
|
||||||
|
pub const KeyboardReport = extern struct {
|
||||||
|
modifiers: u8 = 0,
|
||||||
|
reserved: u8 = 0,
|
||||||
|
keys: [6]u8 = .{ 0, 0, 0, 0, 0, 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// The modifier byte's bits (HID keyboard boot report).
|
||||||
|
pub const modifier_left_control: u8 = 1 << 0;
|
||||||
|
pub const modifier_left_shift: u8 = 1 << 1;
|
||||||
|
pub const modifier_left_alt: u8 = 1 << 2;
|
||||||
|
pub const modifier_left_gui: u8 = 1 << 3;
|
||||||
|
pub const modifier_right_control: u8 = 1 << 4;
|
||||||
|
pub const modifier_right_shift: u8 = 1 << 5;
|
||||||
|
pub const modifier_right_alt: u8 = 1 << 6;
|
||||||
|
pub const modifier_right_gui: u8 = 1 << 7;
|
||||||
|
|
||||||
|
pub const TransitionKind = enum { pressed, released };
|
||||||
|
|
||||||
|
/// One key going down or up. `usage` is a HID keyboard-page usage — modifier keys
|
||||||
|
/// map to usages 224..231 — which is exactly the input protocol's `Keycode`.
|
||||||
|
pub const Transition = struct { kind: TransitionKind, usage: u8 };
|
||||||
|
|
||||||
|
// A report can change at most all 8 modifiers and all 6 keys at once.
|
||||||
|
pub const max_transitions = 8 + 6;
|
||||||
|
|
||||||
|
pub const Transitions = struct {
|
||||||
|
items: [max_transitions]Transition = undefined,
|
||||||
|
count: usize = 0,
|
||||||
|
|
||||||
|
fn add(self: *Transitions, transition: Transition) void {
|
||||||
|
if (self.count < self.items.len) {
|
||||||
|
self.items[self.count] = transition;
|
||||||
|
self.count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn slice(self: *const Transitions) []const Transition {
|
||||||
|
return self.items[0..self.count];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Turns a stream of boot keyboard reports into make/break transitions by diffing
|
||||||
|
/// each report against the last.
|
||||||
|
pub const KeyboardDecoder = struct {
|
||||||
|
previous: KeyboardReport = .{},
|
||||||
|
|
||||||
|
pub fn feed(self: *KeyboardDecoder, current: KeyboardReport) Transitions {
|
||||||
|
var out = Transitions{};
|
||||||
|
|
||||||
|
// Rollover: 0x01 (ErrorRollOver) means more keys are held than the report
|
||||||
|
// can carry, so the key array is invalid. Emit nothing and keep the prior
|
||||||
|
// state (so the eventual releases still resolve against real keys).
|
||||||
|
for (current.keys) |key| {
|
||||||
|
if (key == 0x01) return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modifiers: one make/break per changed bit; modifier usages are 224..231.
|
||||||
|
const changed = current.modifiers ^ self.previous.modifiers;
|
||||||
|
var bit: u3 = 0;
|
||||||
|
while (true) : (bit += 1) {
|
||||||
|
const mask = @as(u8, 1) << bit;
|
||||||
|
if (changed & mask != 0) {
|
||||||
|
out.add(.{
|
||||||
|
.kind = if (current.modifiers & mask != 0) .pressed else .released,
|
||||||
|
.usage = 224 + @as(u8, bit),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (bit == 7) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys made: present now, absent before.
|
||||||
|
for (current.keys) |key| {
|
||||||
|
if (key != 0 and !contains(&self.previous.keys, key)) out.add(.{ .kind = .pressed, .usage = key });
|
||||||
|
}
|
||||||
|
// Keys broken: present before, absent now.
|
||||||
|
for (self.previous.keys) |key| {
|
||||||
|
if (key != 0 and !contains(¤t.keys, key)) out.add(.{ .kind = .released, .usage = key });
|
||||||
|
}
|
||||||
|
|
||||||
|
self.previous = current;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn contains(keys: *const [6]u8, value: u8) bool {
|
||||||
|
for (keys) |key| {
|
||||||
|
if (key == value) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- mouse ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A decoded boot mouse report: the button bitmap and relative motion. The wheel
|
||||||
|
/// byte is present only on 4-byte reports (QEMU's usb-mouse sends one).
|
||||||
|
pub const MouseReport = struct {
|
||||||
|
buttons: u8 = 0,
|
||||||
|
dx: i8 = 0,
|
||||||
|
dy: i8 = 0,
|
||||||
|
wheel: i8 = 0,
|
||||||
|
has_wheel: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const mouse_button_left: u8 = 1 << 0;
|
||||||
|
pub const mouse_button_right: u8 = 1 << 1;
|
||||||
|
pub const mouse_button_middle: u8 = 1 << 2;
|
||||||
|
|
||||||
|
/// Parse a 3- or 4-byte boot mouse report. Note HID reports Y in screen
|
||||||
|
/// convention (positive = down), so — unlike PS/2 — `dy` is NOT negated.
|
||||||
|
pub fn parseMouse(bytes: []const u8) ?MouseReport {
|
||||||
|
if (bytes.len < 3) return null;
|
||||||
|
return .{
|
||||||
|
.buttons = bytes[0],
|
||||||
|
.dx = @bitCast(bytes[1]),
|
||||||
|
.dy = @bitCast(bytes[2]),
|
||||||
|
.wheel = if (bytes.len >= 4) @bitCast(bytes[3]) else 0,
|
||||||
|
.has_wheel = bytes.len >= 4,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- tests ------------------------------------------------------------------
|
||||||
|
|
||||||
|
test "keyboard diff produces make and break transitions" {
|
||||||
|
var decoder = KeyboardDecoder{};
|
||||||
|
|
||||||
|
// Press 'a' (usage 4).
|
||||||
|
var t = decoder.feed(.{ .keys = .{ 4, 0, 0, 0, 0, 0 } });
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), t.count);
|
||||||
|
try std.testing.expectEqual(TransitionKind.pressed, t.items[0].kind);
|
||||||
|
try std.testing.expectEqual(@as(u8, 4), t.items[0].usage);
|
||||||
|
|
||||||
|
// Hold 'a', press 'b' (usage 5): only 'b' is new.
|
||||||
|
t = decoder.feed(.{ .keys = .{ 4, 5, 0, 0, 0, 0 } });
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), t.count);
|
||||||
|
try std.testing.expectEqual(@as(u8, 5), t.items[0].usage);
|
||||||
|
|
||||||
|
// Release everything: 'a' and 'b' both break.
|
||||||
|
t = decoder.feed(.{ .keys = .{ 0, 0, 0, 0, 0, 0 } });
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), t.count);
|
||||||
|
try std.testing.expectEqual(TransitionKind.released, t.items[0].kind);
|
||||||
|
|
||||||
|
// Press Left Shift (modifier bit 1 -> usage 225).
|
||||||
|
t = decoder.feed(.{ .modifiers = modifier_left_shift });
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), t.count);
|
||||||
|
try std.testing.expectEqual(@as(u8, 225), t.items[0].usage);
|
||||||
|
try std.testing.expectEqual(TransitionKind.pressed, t.items[0].kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "rollover report is ignored but state is preserved" {
|
||||||
|
var decoder = KeyboardDecoder{};
|
||||||
|
_ = decoder.feed(.{ .keys = .{ 4, 0, 0, 0, 0, 0 } }); // press 'a'
|
||||||
|
|
||||||
|
const rollover = decoder.feed(.{ .keys = .{ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 } });
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), rollover.count);
|
||||||
|
|
||||||
|
// 'a' is still considered down, so releasing all keys now breaks it.
|
||||||
|
const release = decoder.feed(.{ .keys = .{ 0, 0, 0, 0, 0, 0 } });
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), release.count);
|
||||||
|
try std.testing.expectEqual(@as(u8, 4), release.items[0].usage);
|
||||||
|
try std.testing.expectEqual(TransitionKind.released, release.items[0].kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "mouse report parses motion without inverting Y" {
|
||||||
|
const three = parseMouse(&.{ mouse_button_left, 5, 0xFB }).?; // dy = -5
|
||||||
|
try std.testing.expectEqual(mouse_button_left, three.buttons);
|
||||||
|
try std.testing.expectEqual(@as(i8, 5), three.dx);
|
||||||
|
try std.testing.expectEqual(@as(i8, -5), three.dy);
|
||||||
|
try std.testing.expect(!three.has_wheel);
|
||||||
|
|
||||||
|
const four = parseMouse(&.{ 0, 0, 0, 0xFF }).?; // wheel = -1
|
||||||
|
try std.testing.expect(four.has_wheel);
|
||||||
|
try std.testing.expectEqual(@as(i8, -1), four.wheel);
|
||||||
|
|
||||||
|
try std.testing.expect(parseMouse(&.{ 0, 0 }) == null); // too short
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
//! USB HID boot keyboard driver.
|
||||||
|
//!
|
||||||
|
//! Spawned by the device manager when the xHCI bus driver reports a HID / boot /
|
||||||
|
//! keyboard interface (class 3, subclass 1, protocol 1); its assigned device id
|
||||||
|
//! arrives as argv[1] and an optional layout name ("us", "gb", ...) as argv[2].
|
||||||
|
//! It owns no hardware: it opens its device through the USB transfer protocol
|
||||||
|
//! (`runtime.usb`), asks the device for the boot protocol, subscribes to its
|
||||||
|
//! interrupt-IN endpoint, and turns each 8-byte boot report into input-protocol
|
||||||
|
//! events, published to the input service — the USB analogue of ps2-bus/keyboard.
|
||||||
|
//!
|
||||||
|
//! interrupt report -> hid-report diff -> key_down / key_up
|
||||||
|
//! -> xkeyboard-config -> character -> key_press
|
||||||
|
//!
|
||||||
|
//! Because a USB keyboard's usages ARE the input protocol's keycodes (both are
|
||||||
|
//! HID keyboard page 0x07), the decode is nearly 1:1 — no scancode translation.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const runtime = @import("runtime");
|
||||||
|
const usb_abi = @import("usb-abi");
|
||||||
|
const xkb = @import("xkeyboard-config");
|
||||||
|
const hid = @import("hid-report.zig");
|
||||||
|
const ipc = runtime.ipc;
|
||||||
|
const process = runtime.process;
|
||||||
|
const input_protocol = runtime.input_protocol;
|
||||||
|
|
||||||
|
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||||
|
var line: [128]u8 = undefined;
|
||||||
|
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The modifier state a character lookup needs — derived from the report's
|
||||||
|
// modifier byte, plus the driver-tracked caps-lock toggle.
|
||||||
|
const ModifierSnapshot = struct {
|
||||||
|
shift: bool,
|
||||||
|
control: bool,
|
||||||
|
right_alt: bool,
|
||||||
|
caps_lock: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The character a key produces under `modifiers`, or 0 for none — the layout
|
||||||
|
/// lookup for printable keys, with ASCII control characters for the keys every
|
||||||
|
/// consumer expects (Enter, Tab, Backspace, Escape), exactly as ps2-bus/keyboard.
|
||||||
|
fn characterFor(layout: *const xkb.Layout, usage: u8, modifiers: ModifierSnapshot) u32 {
|
||||||
|
const mapping = xkb.map(layout, usage, .{
|
||||||
|
.shift = modifiers.shift,
|
||||||
|
.caps_lock = modifiers.caps_lock,
|
||||||
|
.level3 = modifiers.right_alt,
|
||||||
|
.control = modifiers.control,
|
||||||
|
});
|
||||||
|
if (mapping.character) |character| return character;
|
||||||
|
return switch (@as(input_protocol.Keycode, @enumFromInt(usage))) {
|
||||||
|
.enter, .keypad_enter => '\n',
|
||||||
|
.tab => '\t',
|
||||||
|
.backspace => 0x08,
|
||||||
|
.escape => 0x1B,
|
||||||
|
else => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modifierWord(modifiers: u8) u32 {
|
||||||
|
var word: u32 = 0;
|
||||||
|
if (modifiers & (hid.modifier_left_shift | hid.modifier_right_shift) != 0) word |= input_protocol.modifier_shift;
|
||||||
|
if (modifiers & (hid.modifier_left_control | hid.modifier_right_control) != 0) word |= input_protocol.modifier_control;
|
||||||
|
if (modifiers & (hid.modifier_left_alt | hid.modifier_right_alt) != 0) word |= input_protocol.modifier_alt;
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(init: runtime.process.Init) void {
|
||||||
|
const argument = init.arguments.get(1) orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: missing device id (argv[1])\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const device_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||||
|
writeLine("/system/drivers/usb-hid/keyboard: malformed device id '{s}'\n", .{argument});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const layout = xkb.byName(init.arguments.get(2) orelse "us") orelse xkb.us;
|
||||||
|
|
||||||
|
// Hello the manager first (meet the spawn deadline), then open the device.
|
||||||
|
if (!runtime.usb.helloManager(device_id)) {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: hello to device manager failed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var device = runtime.usb.open(device_id) orelse {
|
||||||
|
writeLine("/system/drivers/usb-hid/keyboard: could not open device {d}\n", .{device_id});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const endpoint = device.findEndpoint(runtime.usb.transfer_type_interrupt, true) orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: no interrupt-IN endpoint\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ask for the boot protocol and an indefinite idle (report only on change).
|
||||||
|
_ = device.controlOut(@bitCast(usb_abi.setProtocol(@enumFromInt(device.interface_number), .boot)));
|
||||||
|
_ = device.controlOut(@bitCast(usb_abi.setIdle(@enumFromInt(device.interface_number), 0, 0)));
|
||||||
|
|
||||||
|
if (!device.subscribeInterrupt(endpoint.address, endpoint.max_packet_size)) {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: interrupt subscribe failed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var source = runtime.input.connectSource() orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: input service unavailable\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
_ = process.bindSignals(device.endpoint);
|
||||||
|
writeLine("/system/drivers/usb-hid/keyboard: ok (device {d}, interface {d}, layout {s})\n", .{ device_id, device.interface_number, layout.name });
|
||||||
|
|
||||||
|
var decoder = hid.KeyboardDecoder{};
|
||||||
|
var caps_lock = false;
|
||||||
|
var receive: [64]u8 = undefined;
|
||||||
|
while (true) {
|
||||||
|
const got = ipc.replyWait(device.endpoint, &.{}, &receive, null);
|
||||||
|
if (!got.isNotification()) continue;
|
||||||
|
if (process.signalsFrom(got.badge)) |signals| {
|
||||||
|
if (signals.has(.terminate)) return;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!got.isMessage() or got.len < @sizeOf(runtime.usb.InterruptReport)) continue;
|
||||||
|
|
||||||
|
const message = std.mem.bytesToValue(runtime.usb.InterruptReport, receive[0..@sizeOf(runtime.usb.InterruptReport)]);
|
||||||
|
if (message.length < @sizeOf(hid.KeyboardReport)) continue;
|
||||||
|
const report = std.mem.bytesToValue(hid.KeyboardReport, message.data[0..@sizeOf(hid.KeyboardReport)]);
|
||||||
|
const transitions = decoder.feed(report);
|
||||||
|
|
||||||
|
// Caps Lock toggles on its own key-down (a stateful lock, not a modifier).
|
||||||
|
for (transitions.slice()) |transition| {
|
||||||
|
if (transition.kind == .pressed and @as(input_protocol.Keycode, @enumFromInt(transition.usage)) == .caps_lock) caps_lock = !caps_lock;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiers = ModifierSnapshot{
|
||||||
|
.shift = report.modifiers & (hid.modifier_left_shift | hid.modifier_right_shift) != 0,
|
||||||
|
.control = report.modifiers & (hid.modifier_left_control | hid.modifier_right_control) != 0,
|
||||||
|
.right_alt = report.modifiers & hid.modifier_right_alt != 0,
|
||||||
|
.caps_lock = caps_lock,
|
||||||
|
};
|
||||||
|
const modifier_word = modifierWord(report.modifiers);
|
||||||
|
|
||||||
|
for (transitions.slice()) |transition| {
|
||||||
|
switch (transition.kind) {
|
||||||
|
.pressed => {
|
||||||
|
_ = source.publishKeyboardEvent(.{
|
||||||
|
.kind = @intFromEnum(input_protocol.EventKind.key_down),
|
||||||
|
.keycode = transition.usage,
|
||||||
|
.character = 0,
|
||||||
|
.modifiers = modifier_word,
|
||||||
|
});
|
||||||
|
const character = characterFor(layout, transition.usage, modifiers);
|
||||||
|
if (character != 0) {
|
||||||
|
_ = source.publishKeyboardEvent(.{
|
||||||
|
.kind = @intFromEnum(input_protocol.EventKind.key_press),
|
||||||
|
.keycode = transition.usage,
|
||||||
|
.character = character,
|
||||||
|
.modifiers = modifier_word,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.released => {
|
||||||
|
_ = source.publishKeyboardEvent(.{
|
||||||
|
.kind = @intFromEnum(input_protocol.EventKind.key_up),
|
||||||
|
.keycode = transition.usage,
|
||||||
|
.character = 0,
|
||||||
|
.modifiers = modifier_word,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = runtime.panic;
|
||||||
|
comptime {
|
||||||
|
_ = &runtime.start._start;
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
//! USB HID boot mouse driver.
|
||||||
|
//!
|
||||||
|
//! Spawned by the device manager when the xHCI bus driver reports a HID / boot /
|
||||||
|
//! mouse interface (class 3, subclass 1, protocol 2); its assigned device id
|
||||||
|
//! arrives as argv[1]. Like the keyboard driver it owns no hardware: it opens its
|
||||||
|
//! device through the USB transfer protocol (`runtime.usb`), asks for the boot
|
||||||
|
//! protocol, subscribes to its interrupt-IN endpoint, and turns each 3- or 4-byte
|
||||||
|
//! boot report into input-protocol mouse events published to the input service.
|
||||||
|
//!
|
||||||
|
//! Unlike PS/2, HID reports Y in screen convention (positive = down), so motion
|
||||||
|
//! is passed straight through (the decode in hid-report.zig does not negate it).
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const runtime = @import("runtime");
|
||||||
|
const usb_abi = @import("usb-abi");
|
||||||
|
const hid = @import("hid-report.zig");
|
||||||
|
const ipc = runtime.ipc;
|
||||||
|
const process = runtime.process;
|
||||||
|
const input_protocol = runtime.input_protocol;
|
||||||
|
|
||||||
|
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||||
|
var line: [128]u8 = undefined;
|
||||||
|
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The current pressed-button bitmask in input-protocol terms.
|
||||||
|
fn buttonMask(buttons: u8) u32 {
|
||||||
|
var mask: u32 = 0;
|
||||||
|
if (buttons & hid.mouse_button_left != 0) mask |= input_protocol.mouse_button_left;
|
||||||
|
if (buttons & hid.mouse_button_right != 0) mask |= input_protocol.mouse_button_right;
|
||||||
|
if (buttons & hid.mouse_button_middle != 0) mask |= input_protocol.mouse_button_middle;
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(init: runtime.process.Init) void {
|
||||||
|
const argument = init.arguments.get(1) orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-hid/mouse: missing device id (argv[1])\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const device_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||||
|
writeLine("/system/drivers/usb-hid/mouse: malformed device id '{s}'\n", .{argument});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!runtime.usb.helloManager(device_id)) {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-hid/mouse: hello to device manager failed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var device = runtime.usb.open(device_id) orelse {
|
||||||
|
writeLine("/system/drivers/usb-hid/mouse: could not open device {d}\n", .{device_id});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const endpoint = device.findEndpoint(runtime.usb.transfer_type_interrupt, true) orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-hid/mouse: no interrupt-IN endpoint\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = device.controlOut(@bitCast(usb_abi.setProtocol(@enumFromInt(device.interface_number), .boot)));
|
||||||
|
|
||||||
|
if (!device.subscribeInterrupt(endpoint.address, endpoint.max_packet_size)) {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-hid/mouse: interrupt subscribe failed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var source = runtime.input.connectSource() orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-hid/mouse: input service unavailable\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
_ = process.bindSignals(device.endpoint);
|
||||||
|
writeLine("/system/drivers/usb-hid/mouse: ok (device {d}, interface {d})\n", .{ device_id, device.interface_number });
|
||||||
|
|
||||||
|
var previous_buttons: u8 = 0;
|
||||||
|
var receive: [64]u8 = undefined;
|
||||||
|
while (true) {
|
||||||
|
const got = ipc.replyWait(device.endpoint, &.{}, &receive, null);
|
||||||
|
if (!got.isNotification()) continue;
|
||||||
|
if (process.signalsFrom(got.badge)) |signals| {
|
||||||
|
if (signals.has(.terminate)) return;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!got.isMessage() or got.len < @sizeOf(runtime.usb.InterruptReport)) continue;
|
||||||
|
|
||||||
|
const message = std.mem.bytesToValue(runtime.usb.InterruptReport, receive[0..@sizeOf(runtime.usb.InterruptReport)]);
|
||||||
|
const length = @min(message.length, message.data.len);
|
||||||
|
const report = hid.parseMouse(message.data[0..length]) orelse continue;
|
||||||
|
const mask = buttonMask(report.buttons);
|
||||||
|
|
||||||
|
// Button transitions: one event per changed button bit.
|
||||||
|
const changed = report.buttons ^ previous_buttons;
|
||||||
|
inline for (.{
|
||||||
|
.{ hid.mouse_button_left, input_protocol.mouse_button_left },
|
||||||
|
.{ hid.mouse_button_right, input_protocol.mouse_button_right },
|
||||||
|
.{ hid.mouse_button_middle, input_protocol.mouse_button_middle },
|
||||||
|
}) |pair| {
|
||||||
|
if (changed & pair[0] != 0) {
|
||||||
|
_ = source.publishMouseEvent(.{
|
||||||
|
.kind = @intFromEnum(if (report.buttons & pair[0] != 0) input_protocol.MouseEventKind.button_down else input_protocol.MouseEventKind.button_up),
|
||||||
|
.button = pair[1],
|
||||||
|
.dx = 0,
|
||||||
|
.dy = 0,
|
||||||
|
.scroll_x = 0,
|
||||||
|
.scroll_y = 0,
|
||||||
|
.buttons = mask,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previous_buttons = report.buttons;
|
||||||
|
|
||||||
|
// Relative motion (dy straight through — HID Y is already screen convention).
|
||||||
|
if (report.dx != 0 or report.dy != 0) {
|
||||||
|
_ = source.publishMouseEvent(.{
|
||||||
|
.kind = @intFromEnum(input_protocol.MouseEventKind.motion),
|
||||||
|
.button = 0,
|
||||||
|
.dx = report.dx,
|
||||||
|
.dy = report.dy,
|
||||||
|
.scroll_x = 0,
|
||||||
|
.scroll_y = 0,
|
||||||
|
.buttons = mask,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wheel (4-byte reports only): positive = scroll up.
|
||||||
|
if (report.has_wheel and report.wheel != 0) {
|
||||||
|
_ = source.publishMouseEvent(.{
|
||||||
|
.kind = @intFromEnum(input_protocol.MouseEventKind.scroll),
|
||||||
|
.button = 0,
|
||||||
|
.dx = 0,
|
||||||
|
.dy = 0,
|
||||||
|
.scroll_x = 0,
|
||||||
|
.scroll_y = report.wheel,
|
||||||
|
.buttons = mask,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = runtime.panic;
|
||||||
|
comptime {
|
||||||
|
_ = &runtime.start._start;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
//! USB Mass Storage Bulk-Only Transport (BOT) wire structures — the Command and
|
||||||
|
//! Command Status Wrappers that bracket every command (USB MSC BOT §5). Pure data
|
||||||
|
//! definitions, host-testable in isolation. The command inside the CBW is a SCSI
|
||||||
|
//! CDB (see scsi.zig); the transport here just carries it and reports status.
|
||||||
|
//!
|
||||||
|
//! One command is three bulk transfers: CBW out, an optional data stage, CSW in.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// "USBC" — the signature at the head of every Command Block Wrapper.
|
||||||
|
pub const cbw_signature: u32 = 0x43425355;
|
||||||
|
/// "USBS" — the signature at the head of every Command Status Wrapper.
|
||||||
|
pub const csw_signature: u32 = 0x53425355;
|
||||||
|
|
||||||
|
/// CBW `flags`: set for a device-to-host (IN) data stage, clear for OUT.
|
||||||
|
pub const flag_data_in: u8 = 0x80;
|
||||||
|
|
||||||
|
/// The 31-byte Command Block Wrapper, sent on the bulk-OUT endpoint.
|
||||||
|
pub const CommandBlockWrapper = extern struct {
|
||||||
|
signature: u32 align(1) = cbw_signature,
|
||||||
|
tag: u32 align(1),
|
||||||
|
data_transfer_length: u32 align(1),
|
||||||
|
flags: u8,
|
||||||
|
lun: u8,
|
||||||
|
cdb_length: u8,
|
||||||
|
cdb: [16]u8 = [_]u8{0} ** 16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A device's answer to a command (the CSW `status` byte).
|
||||||
|
pub const CommandStatus = enum(u8) {
|
||||||
|
passed = 0,
|
||||||
|
failed = 1,
|
||||||
|
phase_error = 2,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The 13-byte Command Status Wrapper, read from the bulk-IN endpoint.
|
||||||
|
pub const CommandStatusWrapper = extern struct {
|
||||||
|
signature: u32 align(1) = csw_signature,
|
||||||
|
tag: u32 align(1),
|
||||||
|
data_residue: u32 align(1),
|
||||||
|
status: u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
std.debug.assert(@sizeOf(CommandBlockWrapper) == 31);
|
||||||
|
std.debug.assert(@sizeOf(CommandStatusWrapper) == 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "wrapper sizes and signatures match the specification" {
|
||||||
|
const cbw = CommandBlockWrapper{
|
||||||
|
.tag = 0x11223344,
|
||||||
|
.data_transfer_length = 512,
|
||||||
|
.flags = flag_data_in,
|
||||||
|
.lun = 0,
|
||||||
|
.cdb_length = 10,
|
||||||
|
};
|
||||||
|
const bytes = std.mem.asBytes(&cbw);
|
||||||
|
try std.testing.expectEqual(@as(usize, 31), bytes.len);
|
||||||
|
// "USBC" little-endian.
|
||||||
|
try std.testing.expectEqualSlices(u8, "USBC", bytes[0..4]);
|
||||||
|
try std.testing.expectEqual(flag_data_in, bytes[12]);
|
||||||
|
|
||||||
|
const csw = std.mem.bytesToValue(CommandStatusWrapper, &[_]u8{
|
||||||
|
0x55, 0x53, 0x42, 0x53, // "USBS"
|
||||||
|
0x44, 0x33, 0x22, 0x11, // tag
|
||||||
|
0x00, 0x00, 0x00, 0x00, // residue
|
||||||
|
0x00, // passed
|
||||||
|
});
|
||||||
|
try std.testing.expectEqual(csw_signature, csw.signature);
|
||||||
|
try std.testing.expectEqual(@as(u32, 0x11223344), csw.tag);
|
||||||
|
try std.testing.expectEqual(@as(u8, @intFromEnum(CommandStatus.passed)), csw.status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
//! The SCSI command descriptor blocks a transparent-SCSI (subclass 0x06) mass
|
||||||
|
//! storage device understands, and the parsers for what they return. Pure data —
|
||||||
|
//! host-testable. These CDBs go inside a Bulk-Only-Transport CBW (see
|
||||||
|
//! bulk-only-transport.zig).
|
||||||
|
//!
|
||||||
|
//! Every multi-byte SCSI field is **big-endian** — the opposite of the USB wire
|
||||||
|
//! ABI — so the LBA and transfer-length encodings are the load-bearing detail.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// SCSI operation codes.
|
||||||
|
const op_test_unit_ready: u8 = 0x00;
|
||||||
|
const op_request_sense: u8 = 0x03;
|
||||||
|
const op_inquiry: u8 = 0x12;
|
||||||
|
const op_read_capacity_10: u8 = 0x25;
|
||||||
|
const op_read_10: u8 = 0x28;
|
||||||
|
const op_write_10: u8 = 0x2A;
|
||||||
|
const op_synchronize_cache_10: u8 = 0x35;
|
||||||
|
|
||||||
|
/// INQUIRY: standard device data (36 bytes: peripheral type, removable, vendor
|
||||||
|
/// and product strings).
|
||||||
|
pub fn inquiry(allocation_length: u8) [6]u8 {
|
||||||
|
return .{ op_inquiry, 0, 0, 0, allocation_length, 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TEST UNIT READY: no data; success (CSW passed) means the unit is ready.
|
||||||
|
pub fn testUnitReady() [6]u8 {
|
||||||
|
return .{ op_test_unit_ready, 0, 0, 0, 0, 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// REQUEST SENSE: 18 bytes of sense data (sense key + ASC/ASCQ) explaining the
|
||||||
|
/// previous failure.
|
||||||
|
pub fn requestSense(allocation_length: u8) [6]u8 {
|
||||||
|
return .{ op_request_sense, 0, 0, 0, allocation_length, 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// READ CAPACITY(10): 8 bytes back — the last LBA and the block size, both u32
|
||||||
|
/// big-endian. Block count is last_lba + 1.
|
||||||
|
pub fn readCapacity10() [10]u8 {
|
||||||
|
return .{ op_read_capacity_10, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// READ(10): read `blocks` logical blocks starting at `lba` into the data stage.
|
||||||
|
pub fn read10(lba: u32, blocks: u16) [10]u8 {
|
||||||
|
var cdb = [_]u8{0} ** 10;
|
||||||
|
cdb[0] = op_read_10;
|
||||||
|
std.mem.writeInt(u32, cdb[2..6], lba, .big);
|
||||||
|
std.mem.writeInt(u16, cdb[7..9], blocks, .big);
|
||||||
|
return cdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WRITE(10): write `blocks` logical blocks starting at `lba` from the data stage.
|
||||||
|
pub fn write10(lba: u32, blocks: u16) [10]u8 {
|
||||||
|
var cdb = [_]u8{0} ** 10;
|
||||||
|
cdb[0] = op_write_10;
|
||||||
|
std.mem.writeInt(u32, cdb[2..6], lba, .big);
|
||||||
|
std.mem.writeInt(u16, cdb[7..9], blocks, .big);
|
||||||
|
return cdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SYNCHRONIZE CACHE(10): commit the device's write cache to stable media. LBA 0
|
||||||
|
/// and block count 0 mean "the whole medium". No data stage. Without this a write
|
||||||
|
/// can sit in the USB flash controller's cache and be lost if power is cut right
|
||||||
|
/// after — which is exactly what a shutdown-time log flush hits on real hardware.
|
||||||
|
pub fn synchronizeCache10() [10]u8 {
|
||||||
|
var cdb = [_]u8{0} ** 10;
|
||||||
|
cdb[0] = op_synchronize_cache_10;
|
||||||
|
return cdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode an 8-byte READ CAPACITY(10) reply.
|
||||||
|
pub fn parseCapacity(bytes: [8]u8) struct { last_lba: u32, block_size: u32 } {
|
||||||
|
return .{
|
||||||
|
.last_lba = std.mem.readInt(u32, bytes[0..4], .big),
|
||||||
|
.block_size = std.mem.readInt(u32, bytes[4..8], .big),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test "read/write CDBs encode the LBA and length big-endian" {
|
||||||
|
const read = read10(0x01020304, 8);
|
||||||
|
try std.testing.expectEqualSlices(u8, &.{ 0x28, 0x00, 0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x08, 0x00 }, &read);
|
||||||
|
|
||||||
|
const write = write10(0xAABBCCDD, 1);
|
||||||
|
try std.testing.expectEqualSlices(u8, &.{ 0x2A, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0x00, 0x00, 0x01, 0x00 }, &write);
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(u8, 0x25), readCapacity10()[0]);
|
||||||
|
try std.testing.expectEqual(@as(u8, 0x12), inquiry(36)[0]);
|
||||||
|
try std.testing.expectEqual(@as(u8, 36), inquiry(36)[4]);
|
||||||
|
try std.testing.expectEqual(@as(u8, 0x00), testUnitReady()[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "read capacity parses last LBA and block size" {
|
||||||
|
// last_lba = 0x0003FFFF (262144 blocks), block_size = 512.
|
||||||
|
const capacity = parseCapacity(.{ 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x02, 0x00 });
|
||||||
|
try std.testing.expectEqual(@as(u32, 0x0003FFFF), capacity.last_lba);
|
||||||
|
try std.testing.expectEqual(@as(u32, 512), capacity.block_size);
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
//! USB mass-storage class driver (Bulk-Only Transport + transparent SCSI).
|
||||||
|
//!
|
||||||
|
//! Spawned by the device manager when the xHCI bus driver reports a mass-storage
|
||||||
|
//! / SCSI / bulk-only interface (class 8, subclass 6, protocol 0x50); its device
|
||||||
|
//! id arrives as argv[1]. It owns no hardware: it opens its device through the
|
||||||
|
//! USB transfer protocol (`runtime.usb`), then drives it with the BOT command
|
||||||
|
//! cycle — CBW out, an optional data stage, CSW in — carrying SCSI commands
|
||||||
|
//! (READ CAPACITY, READ(10), WRITE(10)). Upward it is a block device: it serves
|
||||||
|
//! the block protocol under `.block`, the storage a FAT filesystem sits on.
|
||||||
|
//!
|
||||||
|
//! Block data never crosses IPC: read/write name a caller-owned DMA buffer by
|
||||||
|
//! physical address, which the data stage DMAs straight to/from.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const runtime = @import("runtime");
|
||||||
|
const scsi = @import("scsi.zig");
|
||||||
|
const bot = @import("bulk-only-transport.zig");
|
||||||
|
const block_protocol = @import("block-protocol");
|
||||||
|
const dma = runtime.dma;
|
||||||
|
|
||||||
|
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||||
|
var line: [128]u8 = undefined;
|
||||||
|
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||||
|
}
|
||||||
|
|
||||||
|
var device_id: u64 = 0;
|
||||||
|
var device: runtime.usb.Device = undefined;
|
||||||
|
var bulk_in: runtime.usb.Endpoint = undefined;
|
||||||
|
var bulk_out: runtime.usb.Endpoint = undefined;
|
||||||
|
|
||||||
|
// DMA buffers for the transport: the 31-byte CBW, the 13-byte CSW, and a page
|
||||||
|
// for the small command data (INQUIRY / READ CAPACITY / the self-check sector).
|
||||||
|
var command_wrapper: dma.Region = undefined;
|
||||||
|
var status_wrapper: dma.Region = undefined;
|
||||||
|
var command_data: dma.Region = undefined;
|
||||||
|
|
||||||
|
var next_tag: u32 = 1;
|
||||||
|
var block_size: u32 = 512;
|
||||||
|
var block_count: u64 = 0;
|
||||||
|
|
||||||
|
/// One Bulk-Only-Transport command: send the CBW, run the data stage (to/from
|
||||||
|
/// `data_physical`), read and validate the CSW. Returns true on a passed status.
|
||||||
|
fn transact(cdb: []const u8, direction_in: bool, data_physical: u64, data_length: u32) bool {
|
||||||
|
const tag = next_tag;
|
||||||
|
next_tag +%= 1;
|
||||||
|
|
||||||
|
const wrapper: *bot.CommandBlockWrapper = @ptrFromInt(command_wrapper.virtual);
|
||||||
|
wrapper.* = .{
|
||||||
|
.tag = tag,
|
||||||
|
.data_transfer_length = data_length,
|
||||||
|
.flags = if (direction_in) bot.flag_data_in else 0,
|
||||||
|
.lun = 0,
|
||||||
|
.cdb_length = @intCast(cdb.len),
|
||||||
|
};
|
||||||
|
@memcpy(wrapper.cdb[0..cdb.len], cdb);
|
||||||
|
|
||||||
|
if (device.bulk(bulk_out.address, command_wrapper.physical, @sizeOf(bot.CommandBlockWrapper)) == null) return false;
|
||||||
|
if (data_length > 0) {
|
||||||
|
const endpoint = if (direction_in) bulk_in.address else bulk_out.address;
|
||||||
|
if (device.bulk(endpoint, data_physical, data_length) == null) return false;
|
||||||
|
}
|
||||||
|
if (device.bulk(bulk_in.address, status_wrapper.physical, @sizeOf(bot.CommandStatusWrapper)) == null) return false;
|
||||||
|
|
||||||
|
const status: *const bot.CommandStatusWrapper = @ptrFromInt(status_wrapper.virtual);
|
||||||
|
if (status.signature != bot.csw_signature or status.tag != tag) return false;
|
||||||
|
return status.status == @intFromEnum(bot.CommandStatus.passed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||||
|
_ = endpoint;
|
||||||
|
if (!runtime.usb.helloManager(device_id)) {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-storage: hello to device manager failed\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
device = runtime.usb.open(device_id) orelse {
|
||||||
|
writeLine("/system/drivers/usb-storage: could not open device {d}\n", .{device_id});
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
bulk_in = device.findEndpoint(runtime.usb.transfer_type_bulk, true) orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-storage: no bulk-IN endpoint\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
bulk_out = device.findEndpoint(runtime.usb.transfer_type_bulk, false) orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-storage: no bulk-OUT endpoint\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
command_wrapper = dma.alloc(4096, dma.coherent) orelse return false;
|
||||||
|
status_wrapper = dma.alloc(4096, dma.coherent) orelse return false;
|
||||||
|
command_data = dma.alloc(4096, dma.coherent) orelse return false;
|
||||||
|
|
||||||
|
// Bring the LUN up: wait for it to be ready (clearing the initial unit-attention
|
||||||
|
// with REQUEST SENSE), identify it, and read its capacity.
|
||||||
|
var tries: u32 = 0;
|
||||||
|
while (tries < 10) : (tries += 1) {
|
||||||
|
const ready = scsi.testUnitReady();
|
||||||
|
if (transact(&ready, false, 0, 0)) break;
|
||||||
|
const sense = scsi.requestSense(18);
|
||||||
|
_ = transact(&sense, true, command_data.physical, 18);
|
||||||
|
runtime.system.sleep(50);
|
||||||
|
}
|
||||||
|
const inquiry = scsi.inquiry(36);
|
||||||
|
_ = transact(&inquiry, true, command_data.physical, 36);
|
||||||
|
|
||||||
|
const capacity_command = scsi.readCapacity10();
|
||||||
|
if (!transact(&capacity_command, true, command_data.physical, 8)) {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-storage: READ CAPACITY failed\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var capacity_bytes: [8]u8 = undefined;
|
||||||
|
const capacity_source: [*]const u8 = @ptrFromInt(command_data.virtual);
|
||||||
|
@memcpy(&capacity_bytes, capacity_source[0..8]);
|
||||||
|
const capacity = scsi.parseCapacity(capacity_bytes);
|
||||||
|
block_size = capacity.block_size;
|
||||||
|
block_count = @as(u64, capacity.last_lba) + 1;
|
||||||
|
writeLine("/system/drivers/usb-storage: ready ({d} blocks x {d} bytes)\n", .{ block_count, block_size });
|
||||||
|
|
||||||
|
// Self-check: read block 0 and log its trailing signature (0x55AA for a boot
|
||||||
|
// sector) — proof READ(10) works end to end over the bulk path.
|
||||||
|
const read0 = scsi.read10(0, 1);
|
||||||
|
if (block_size <= 4096 and transact(&read0, true, command_data.physical, block_size)) {
|
||||||
|
const sector: [*]const u8 = @ptrFromInt(command_data.virtual);
|
||||||
|
writeLine("/system/drivers/usb-storage: block 0 signature 0x{x:0>2}{x:0>2}\n", .{ sector[510], sector[511] });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serve the block protocol: geometry, and whole-block read/write to/from the
|
||||||
|
/// caller's DMA buffer (named by physical address).
|
||||||
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||||
|
_ = sender;
|
||||||
|
_ = capability;
|
||||||
|
if (message.len < block_protocol.request_size) return 0;
|
||||||
|
const request = std.mem.bytesToValue(block_protocol.Request, message[0..block_protocol.request_size]);
|
||||||
|
switch (request.operation) {
|
||||||
|
@intFromEnum(block_protocol.Operation.geometry) => {
|
||||||
|
return writeReply(reply, .{ .status = 0, .block_size = block_size, .block_count = block_count });
|
||||||
|
},
|
||||||
|
@intFromEnum(block_protocol.Operation.read) => {
|
||||||
|
const count: u16 = @intCast(request.count);
|
||||||
|
const cdb = scsi.read10(@intCast(request.lba), count);
|
||||||
|
const ok = transact(&cdb, true, request.physical, request.count * block_size);
|
||||||
|
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 });
|
||||||
|
},
|
||||||
|
@intFromEnum(block_protocol.Operation.write) => {
|
||||||
|
const count: u16 = @intCast(request.count);
|
||||||
|
const cdb = scsi.write10(@intCast(request.lba), count);
|
||||||
|
const ok = transact(&cdb, false, request.physical, request.count * block_size);
|
||||||
|
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 });
|
||||||
|
},
|
||||||
|
@intFromEnum(block_protocol.Operation.flush) => {
|
||||||
|
// SYNCHRONIZE CACHE: commit the device's write cache to flash. No data
|
||||||
|
// stage. Makes prior writes durable before a caller (init at shutdown)
|
||||||
|
// cuts power. A device without a volatile cache reports success anyway.
|
||||||
|
const cdb = scsi.synchronizeCache10();
|
||||||
|
const ok = transact(&cdb, false, 0, 0);
|
||||||
|
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = 0 });
|
||||||
|
},
|
||||||
|
else => return 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeReply(reply: []u8, value: block_protocol.Reply) usize {
|
||||||
|
const bytes = std.mem.asBytes(&value);
|
||||||
|
@memcpy(reply[0..bytes.len], bytes);
|
||||||
|
return bytes.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(init: runtime.process.Init) void {
|
||||||
|
const argument = init.arguments.get(1) orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-storage: missing device id (argv[1])\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
device_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||||
|
writeLine("/system/drivers/usb-storage: malformed device id '{s}'\n", .{argument});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
runtime.service.run(block_protocol.message_maximum, .{
|
||||||
|
.service = .block,
|
||||||
|
.init = initialise,
|
||||||
|
.on_message = onMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = runtime.panic;
|
||||||
|
comptime {
|
||||||
|
_ = &runtime.start._start;
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
//! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or
|
||||||
|
//! mass-storage driver) says to the xHCI bus driver over its well-known
|
||||||
|
//! `.usb_bus` endpoint to drive its device. The class driver owns no hardware —
|
||||||
|
//! it reaches its device entirely through these messages, the way a PS/2 keyboard
|
||||||
|
//! driver reaches the 8042 through the ps2-bus. Extern-struct messages tagged by
|
||||||
|
//! `Operation`, the vfs-protocol / device-manager-protocol pattern.
|
||||||
|
//!
|
||||||
|
//! The shape:
|
||||||
|
//! - **open** (a capability-passing `ipc.callCap`): the class driver hands over
|
||||||
|
//! its own endpoint (for asynchronous interrupt reports) and its assigned
|
||||||
|
//! device id, and receives a `device_token` plus its interface's endpoints.
|
||||||
|
//! - **control / bulk** (synchronous `ipc.call`): one transfer, answered when
|
||||||
|
//! it completes. Control data travels inline (descriptors, HID/MSC class
|
||||||
|
//! requests are all small); bulk data travels by **physical address** — the
|
||||||
|
//! class driver's own `dma_alloc`'d buffer — so a 512-byte sector never has
|
||||||
|
//! to cross the 256-byte IPC boundary.
|
||||||
|
//! - **interrupt_subscribe** (synchronous): arm periodic IN polling of an
|
||||||
|
//! interrupt endpoint; each report the device produces is then pushed to the
|
||||||
|
//! class driver's endpoint as an asynchronous `InterruptReport` (`ipc.send`),
|
||||||
|
//! exactly how the input service delivers events.
|
||||||
|
//!
|
||||||
|
//! Single controller assumption: one `.usb_bus` singleton serves QEMU's one xHCI.
|
||||||
|
//! A multi-controller machine would need a per-controller endpoint (the device
|
||||||
|
//! manager handing each class driver the right one); noted, not built.
|
||||||
|
|
||||||
|
/// Fits one synchronous IPC message (kernel MESSAGE_MAXIMUM).
|
||||||
|
pub const message_maximum: usize = 256;
|
||||||
|
|
||||||
|
/// The largest inline control-transfer payload. Sized so a whole message
|
||||||
|
/// (header + data) stays under `message_maximum`: descriptors and HID/MSC class
|
||||||
|
/// requests are all far smaller.
|
||||||
|
pub const max_inline_data: usize = 200;
|
||||||
|
|
||||||
|
/// The largest interrupt report pushed asynchronously. Sized so `InterruptReport`
|
||||||
|
/// fits an `ipc_send` payload slot (POST_MAXIMUM = 64): boot keyboard reports are
|
||||||
|
/// 8 bytes, boot mouse reports 3–4.
|
||||||
|
pub const max_report_data: usize = 48;
|
||||||
|
|
||||||
|
/// Endpoints per interface reported back in an open reply (a boot HID interface
|
||||||
|
/// has one interrupt endpoint, a mass-storage interface two bulk endpoints).
|
||||||
|
pub const max_reported_endpoints: usize = 4;
|
||||||
|
|
||||||
|
pub const Operation = enum(u32) {
|
||||||
|
open = 0,
|
||||||
|
control = 1,
|
||||||
|
interrupt_subscribe = 2,
|
||||||
|
bulk = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The endpoint facts a class driver needs, lifted from the endpoint descriptor
|
||||||
|
/// the bus driver already parsed during enumeration.
|
||||||
|
pub const Endpoint = extern struct {
|
||||||
|
/// EndpointDescriptor address: direction in bit 7, number in bits 3:0.
|
||||||
|
address: u8,
|
||||||
|
/// 0 control, 1 isochronous, 2 bulk, 3 interrupt.
|
||||||
|
transfer_type: u8,
|
||||||
|
max_packet_size: u16,
|
||||||
|
interval: u8,
|
||||||
|
reserved: [3]u8 = .{ 0, 0, 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
/// open: the class driver's receive endpoint rides as the call's capability, and
|
||||||
|
/// `device_id` is the interface's assigned id (its argv[1]).
|
||||||
|
pub const OpenRequest = extern struct {
|
||||||
|
operation: u32 = @intFromEnum(Operation.open),
|
||||||
|
reserved: u32 = 0,
|
||||||
|
device_id: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The answer to open: a token scoping every later request to this device, the
|
||||||
|
/// interface's class triple (a sanity check), and its endpoints.
|
||||||
|
pub const OpenReply = extern struct {
|
||||||
|
status: i32,
|
||||||
|
endpoint_count: u32,
|
||||||
|
device_token: u64,
|
||||||
|
interface_class: u8,
|
||||||
|
interface_subclass: u8,
|
||||||
|
interface_protocol: u8,
|
||||||
|
interface_number: u8,
|
||||||
|
reserved2: u32 = 0,
|
||||||
|
endpoints: [max_reported_endpoints]Endpoint = [_]Endpoint{.{ .address = 0, .transfer_type = 0, .max_packet_size = 0, .interval = 0 }} ** max_reported_endpoints,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// control: one EP0 control transfer. `setup` is a bit-cast `usb_abi.Request`.
|
||||||
|
/// For an OUT transfer `data[0..data_length]` is sent; for an IN transfer the
|
||||||
|
/// reply carries up to `data_length` bytes back.
|
||||||
|
pub const ControlRequest = extern struct {
|
||||||
|
operation: u32 = @intFromEnum(Operation.control),
|
||||||
|
reserved: u32 = 0,
|
||||||
|
device_token: u64,
|
||||||
|
setup: [8]u8,
|
||||||
|
direction_in: u8, // 1 = device-to-host (IN), 0 = host-to-device (OUT)
|
||||||
|
reserved2: u8 = 0,
|
||||||
|
data_length: u16,
|
||||||
|
reserved3: u32 = 0,
|
||||||
|
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ControlReply = extern struct {
|
||||||
|
status: i32, // 0 success, negative on failure/stall
|
||||||
|
actual_length: u32,
|
||||||
|
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// interrupt_subscribe: begin periodic IN polling of an interrupt endpoint. Each
|
||||||
|
/// report the device returns is pushed to the caller's endpoint (handed over at
|
||||||
|
/// open) as an asynchronous `InterruptReport`.
|
||||||
|
pub const InterruptSubscribeRequest = extern struct {
|
||||||
|
operation: u32 = @intFromEnum(Operation.interrupt_subscribe),
|
||||||
|
reserved: u32 = 0,
|
||||||
|
device_token: u64,
|
||||||
|
endpoint_address: u8,
|
||||||
|
reserved2: u8 = 0,
|
||||||
|
max_length: u16, // bytes to request per poll (the endpoint's max packet size)
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const InterruptSubscribeReply = extern struct {
|
||||||
|
status: i32,
|
||||||
|
reserved: u32 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// bulk: one bulk IN or OUT transfer. `physical_address` is the class driver's own
|
||||||
|
/// `dma_alloc`'d buffer — the controller DMAs straight to/from it, so the bulk
|
||||||
|
/// data never crosses IPC. `endpoint_address`'s bit 7 selects IN vs OUT.
|
||||||
|
pub const BulkRequest = extern struct {
|
||||||
|
operation: u32 = @intFromEnum(Operation.bulk),
|
||||||
|
reserved: u32 = 0,
|
||||||
|
device_token: u64,
|
||||||
|
physical_address: u64,
|
||||||
|
length: u32,
|
||||||
|
endpoint_address: u8,
|
||||||
|
reserved2: u8 = 0,
|
||||||
|
reserved3: u16 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const BulkReply = extern struct {
|
||||||
|
status: i32,
|
||||||
|
actual_length: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An asynchronous interrupt report, pushed with `ipc.send` to a subscriber's
|
||||||
|
/// endpoint. `Received.isMessage()` is set; there is no reply owed.
|
||||||
|
pub const InterruptReport = extern struct {
|
||||||
|
device_token: u64,
|
||||||
|
endpoint_address: u8,
|
||||||
|
length: u8,
|
||||||
|
reserved: u16 = 0,
|
||||||
|
data: [max_report_data]u8 = [_]u8{0} ** max_report_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
const std = @import("std");
|
||||||
|
// Every synchronous message must fit one IPC message; the async report must
|
||||||
|
// fit an ipc_send payload slot.
|
||||||
|
std.debug.assert(@sizeOf(ControlRequest) <= message_maximum);
|
||||||
|
std.debug.assert(@sizeOf(ControlReply) <= message_maximum);
|
||||||
|
std.debug.assert(@sizeOf(OpenReply) <= message_maximum);
|
||||||
|
std.debug.assert(@sizeOf(InterruptReport) <= 64);
|
||||||
|
}
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
//! /system/drivers/usb-xhci-bus — the xHCI (USB 3) host-controller bus driver.
|
||||||
|
//! The device manager spawns **one instance per controller** it discovers (a
|
||||||
|
//! machine can carry several), passing the controller's device-tree id as
|
||||||
|
//! argv[1]; this instance claims that device and no other, so multiple
|
||||||
|
//! instances never fight over hardware.
|
||||||
|
//!
|
||||||
|
//! M18.2 (this increment): after the hello, real hardware — map the xHC's
|
||||||
|
//! register window (the first memory BAR; resource 0 is the ECAM config
|
||||||
|
//! space), read the capability registers, and walk the root-hub ports: one
|
||||||
|
//! `child_added` report to the manager per connected port, carrying the port
|
||||||
|
//! number and the PORTSC speed class as identity. No transfer rings yet —
|
||||||
|
//! descriptors and USB class matching are the USB track; the connect bit and
|
||||||
|
//! speed come straight from PORTSC, which reflects hardware state whether or
|
||||||
|
//! not the controller is running.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const runtime = @import("runtime");
|
||||||
|
const protocol = runtime.device_manager_protocol;
|
||||||
|
const device = runtime.device;
|
||||||
|
const usb_ids = @import("usb-ids");
|
||||||
|
const usb_abi = @import("usb-abi");
|
||||||
|
const transfer = @import("usb-transfer-protocol");
|
||||||
|
const library = @import("usb-xhci-library.zig");
|
||||||
|
|
||||||
|
/// The controller engine (reset, rings, transfers), stood up in `initialise`.
|
||||||
|
var controller: ?library.Controller = null;
|
||||||
|
|
||||||
|
/// This driver's service endpoint (registered as `.usb_bus`), where class-driver
|
||||||
|
/// requests, signals, and the interrupt-poll timer all arrive.
|
||||||
|
var service_endpoint: runtime.ipc.Handle = 0;
|
||||||
|
|
||||||
|
/// How often the driver drains the event ring for interrupt reports (~125 Hz),
|
||||||
|
/// re-armed each tick. Frequent enough for responsive input.
|
||||||
|
const poll_interval_ms: u64 = 8;
|
||||||
|
|
||||||
|
/// The class driver endpoints that opened each device, so interrupt reports can
|
||||||
|
/// be pushed back to them. Keyed by the device token (the interface's device id).
|
||||||
|
const Open = struct {
|
||||||
|
used: bool = false,
|
||||||
|
device_token: u64 = 0,
|
||||||
|
report_endpoint: usize = 0,
|
||||||
|
};
|
||||||
|
var opens = [_]Open{.{}} ** 16;
|
||||||
|
|
||||||
|
fn recordOpen(device_token: u64, report_endpoint: usize) void {
|
||||||
|
for (&opens) |*open| {
|
||||||
|
if (open.used and open.device_token == device_token) {
|
||||||
|
open.report_endpoint = report_endpoint;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (&opens) |*open| {
|
||||||
|
if (!open.used) {
|
||||||
|
open.* = .{ .used = true, .device_token = device_token, .report_endpoint = report_endpoint };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reportEndpointFor(device_token: u64) ?usize {
|
||||||
|
for (&opens) |*open| {
|
||||||
|
if (open.used and open.device_token == device_token) return open.report_endpoint;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format one whole log line and emit it in a single `debug_write`, so
|
||||||
|
/// concurrent instances (one per controller) can never interleave mid-line.
|
||||||
|
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||||
|
var line: [128]u8 = undefined;
|
||||||
|
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||||
|
}
|
||||||
|
|
||||||
|
var controller_id: u64 = protocol.no_device;
|
||||||
|
|
||||||
|
/// Claim the assigned controller, find its register window, and hello the
|
||||||
|
/// manager. Any failure returns false: the process exits cleanly, which the
|
||||||
|
/// manager reads as "meant to stop" — a missing assignment is not a crash loop.
|
||||||
|
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||||
|
service_endpoint = endpoint;
|
||||||
|
if (!device.claim(controller_id)) {
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: unable to claim controller device {d}\n", .{controller_id});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch our own descriptor back for the controller's resources.
|
||||||
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-xhci-bus: out of memory\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const total = device.enumerate(buffer);
|
||||||
|
const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| {
|
||||||
|
if (d.id == controller_id) break d;
|
||||||
|
} else {
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: device {d} not in the device tree\n", .{controller_id});
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The xHC's registers live behind the first memory BAR. Resource 0 is the
|
||||||
|
// function's ECAM configuration space (M15), so the walk starts at 1.
|
||||||
|
var register_index: u64 = 0;
|
||||||
|
const register_window = for (descriptor.resources[1..@intCast(descriptor.resource_count)], 1..) |resource, index| {
|
||||||
|
if (resource.kind == @intFromEnum(device.ResourceKind.memory)) {
|
||||||
|
register_index = index;
|
||||||
|
break resource;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: controller device {d} has no register BAR\n", .{controller_id});
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: claimed controller device {d} (registers at 0x{x}, {d} bytes)\n", .{
|
||||||
|
controller_id,
|
||||||
|
register_window.start,
|
||||||
|
register_window.len,
|
||||||
|
});
|
||||||
|
register_base = device.mmioMap(controller_id, register_index) orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-xhci-bus: mmio_map failed\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bring the controller up: reset it, stand up the command and event rings,
|
||||||
|
// and start it running (the hardware half lives in usb-xhci-library.zig).
|
||||||
|
controller = library.Controller.init(register_base) orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-xhci-bus: controller reset/bring-up failed\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: controller running ({d} slots, {d}-byte contexts)\n", .{
|
||||||
|
controller.?.max_slots,
|
||||||
|
controller.?.context_size,
|
||||||
|
});
|
||||||
|
// The proof of life: a No-Op command round-trips the command ring, the event
|
||||||
|
// ring, the doorbell, and the cycle-bit bookkeeping. If this completes, the
|
||||||
|
// engine is sound; transfers build on exactly this machinery.
|
||||||
|
if (controller.?.noOpCommand()) {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-xhci-bus: command ring running (no-op ok)\n");
|
||||||
|
} else {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-xhci-bus: no-op command did not complete\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The handshake: role, protocol version, assignment — inside the manager's
|
||||||
|
// deadline (the lookup retries cover the manager still registering).
|
||||||
|
var manager: ?runtime.ipc.Handle = null;
|
||||||
|
var tries: u32 = 0;
|
||||||
|
while (manager == null and tries < 100) : (tries += 1) {
|
||||||
|
manager = runtime.ipc.lookup(.device_manager);
|
||||||
|
if (manager == null) runtime.system.sleep(20);
|
||||||
|
}
|
||||||
|
const h = manager orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-xhci-bus: no device manager to hello\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const hello = protocol.Hello{ .role = @intFromEnum(protocol.Role.bus), .device_id = controller_id };
|
||||||
|
var reply: [protocol.message_maximum]u8 = undefined;
|
||||||
|
const n = runtime.ipc.call(h, std.mem.asBytes(&hello), &reply) catch {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-xhci-bus: hello call failed\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (n < protocol.reply_size or std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0) {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-xhci-bus: hello refused\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-xhci-bus: hello acknowledged\n");
|
||||||
|
|
||||||
|
scanPorts(h);
|
||||||
|
|
||||||
|
// Arm the poll timer that drains interrupt reports from the event ring. It is
|
||||||
|
// re-armed on each tick in onNotification; class drivers subscribe later.
|
||||||
|
_ = runtime.system.timerOnce(service_endpoint, poll_interval_ms);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var register_base: usize = 0;
|
||||||
|
|
||||||
|
/// The xHCI default Protocol Speed IDs (the PORTSC port-speed field, bits 13:10)
|
||||||
|
/// decoded to human names — the boot-log breadcrumb for what actually enumerated on
|
||||||
|
/// a port, the USB analog of the pci-bus class-code line. A controller may redefine
|
||||||
|
/// these through its Supported Protocol capability, but the defaults cover every
|
||||||
|
/// speed QEMU and real hardware report at this (pre-descriptor) stage.
|
||||||
|
fn speedName(speed: u32) []const u8 {
|
||||||
|
return switch (speed) {
|
||||||
|
1 => "Full-speed (USB 2.0, 12 Mb/s)",
|
||||||
|
2 => "Low-speed (USB 2.0, 1.5 Mb/s)",
|
||||||
|
3 => "High-speed (USB 2.0, 480 Mb/s)",
|
||||||
|
4 => "SuperSpeed (USB 3.0, 5 Gb/s)",
|
||||||
|
5 => "SuperSpeedPlus (USB 3.1, 10 Gb/s)",
|
||||||
|
else => "unknown speed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The root-hub scan and enumeration: for each connected port, bring the device
|
||||||
|
/// up (reset → enable slot → address), read its descriptors, and register +
|
||||||
|
/// report one child per interface — carrying the interface's (class, subclass,
|
||||||
|
/// protocol) triple as identity, which is what the device manager matches a
|
||||||
|
/// class driver against.
|
||||||
|
fn scanPorts(manager: runtime.ipc.Handle) void {
|
||||||
|
const engine = if (controller) |*c| c else {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-xhci-bus: controller not initialised\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: {d} root-hub ports\n", .{engine.max_ports});
|
||||||
|
|
||||||
|
var port: u32 = 1;
|
||||||
|
var connected: u32 = 0;
|
||||||
|
while (port <= engine.max_ports) : (port += 1) {
|
||||||
|
const port_status = engine.portStatus(port);
|
||||||
|
if (port_status & 1 == 0) continue; // CCS: nothing connected
|
||||||
|
connected += 1;
|
||||||
|
const speed = (port_status >> 10) & 0xF; // the PORTSC port-speed class
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: port {d} connected — {s} (speed class {d})\n", .{ port, speedName(speed), speed });
|
||||||
|
|
||||||
|
const usb_device = engine.setupDevice(port, speed) orelse {
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: port {d} device setup failed\n", .{port});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if (!engine.enumerate(usb_device)) {
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: port {d} enumeration failed\n", .{port});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: port {d} device vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)\n", .{
|
||||||
|
port,
|
||||||
|
usb_device.device_descriptor.vendor_id,
|
||||||
|
usb_device.device_descriptor.product_id,
|
||||||
|
usb_device.interface_count,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
|
||||||
|
// Record the id each interface was registered as, so a class driver
|
||||||
|
// opening the interface (by that id) resolves to it.
|
||||||
|
if (reportInterface(manager, port, interface.*)) |registered| {
|
||||||
|
interface.registered_device_id = registered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (connected == 0) _ = runtime.system.write("/system/drivers/usb-xhci-bus: no devices connected\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register one interface as a resource-less child of the controller and report
|
||||||
|
/// it to the device manager. The identity is the packed USB class triple, so the
|
||||||
|
/// manager can match a class driver (HID keyboard, mouse, mass storage); the
|
||||||
|
/// registered device id becomes that driver's argv[1] assignment. Returns the
|
||||||
|
/// registered device id, or null if registration or the report failed.
|
||||||
|
fn reportInterface(manager: runtime.ipc.Handle, port: u32, interface: library.InterfaceInfo) ?u64 {
|
||||||
|
const identity = usb_ids.packTriple(interface.class, interface.subclass, interface.protocol);
|
||||||
|
|
||||||
|
// A USB device is reached through its controller, not by MMIO, so the child
|
||||||
|
// carries no resources; register() allows that. Its bus-local identity — the
|
||||||
|
// (port, interface) address, written as a short "P<port>I<interface>" tag in
|
||||||
|
// the hid field — makes each interface a distinct kernel node (the register
|
||||||
|
// dedup keys on class/pci_class/hid/resources, all otherwise identical here)
|
||||||
|
// and keeps re-registration idempotent across a bus restart: the same port
|
||||||
|
// and interface always map back to the same device id.
|
||||||
|
var descriptor = std.mem.zeroes(device.DeviceDescriptor);
|
||||||
|
descriptor.class = @intFromEnum(device.DeviceClass.usb_device);
|
||||||
|
descriptor.pci_class = device.no_pci_class;
|
||||||
|
descriptor.resource_count = 0;
|
||||||
|
var hid_buffer: [8]u8 = undefined;
|
||||||
|
const hid_text = std.fmt.bufPrint(&hid_buffer, "P{d}I{d}", .{ port, interface.number }) catch "";
|
||||||
|
descriptor.hid_len = hid_text.len;
|
||||||
|
@memcpy(descriptor.hid[0..hid_text.len], hid_text);
|
||||||
|
const registered = device.register(controller_id, &descriptor) orelse {
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: register refused for port {d} interface {d}\n", .{ port, interface.number });
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const report = protocol.ChildAdded{
|
||||||
|
.parent = controller_id,
|
||||||
|
.bus_address = (@as(u64, port) << 8) | interface.number,
|
||||||
|
.identity = identity,
|
||||||
|
.device_id = registered,
|
||||||
|
};
|
||||||
|
var reply: [protocol.message_maximum]u8 = undefined;
|
||||||
|
_ = runtime.ipc.call(manager, std.mem.asBytes(&report), &reply) catch {
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: child report for port {d} interface {d} failed\n", .{ port, interface.number });
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: port {d} interface {d} class {d}/{d}/{d} registered as device {d}\n", .{
|
||||||
|
port,
|
||||||
|
interface.number,
|
||||||
|
interface.class,
|
||||||
|
interface.subclass,
|
||||||
|
interface.protocol,
|
||||||
|
registered,
|
||||||
|
});
|
||||||
|
return registered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serve the USB transfer protocol: a class driver opens its device, then issues
|
||||||
|
/// control / interrupt-subscribe / bulk requests against it.
|
||||||
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||||
|
_ = sender;
|
||||||
|
if (message.len < 4) return 0;
|
||||||
|
const operation = std.mem.readInt(u32, message[0..4], .little);
|
||||||
|
return switch (operation) {
|
||||||
|
@intFromEnum(transfer.Operation.open) => handleOpen(message, reply, capability),
|
||||||
|
@intFromEnum(transfer.Operation.control) => handleControl(message, reply),
|
||||||
|
@intFromEnum(transfer.Operation.interrupt_subscribe) => handleSubscribe(message, reply),
|
||||||
|
@intFromEnum(transfer.Operation.bulk) => handleBulk(message, reply),
|
||||||
|
else => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeReply(reply: []u8, value: anytype) usize {
|
||||||
|
const bytes = std.mem.asBytes(&value);
|
||||||
|
@memcpy(reply[0..bytes.len], bytes);
|
||||||
|
return bytes.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// open: resolve the assigned device id to an interface, remember the caller's
|
||||||
|
/// endpoint (for interrupt reports), and answer with a device token + the
|
||||||
|
/// interface's endpoints so the class driver need not re-read the config.
|
||||||
|
fn handleOpen(message: []const u8, reply: []u8, capability: ?runtime.ipc.Handle) usize {
|
||||||
|
if (message.len < @sizeOf(transfer.OpenRequest)) return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
|
||||||
|
const request = std.mem.bytesToValue(transfer.OpenRequest, message[0..@sizeOf(transfer.OpenRequest)]);
|
||||||
|
const engine = if (controller) |*c| c else return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
|
||||||
|
const found = engine.findInterface(request.device_id) orelse return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
|
||||||
|
|
||||||
|
if (capability) |endpoint| recordOpen(request.device_id, endpoint);
|
||||||
|
|
||||||
|
var open_reply = transfer.OpenReply{
|
||||||
|
.status = 0,
|
||||||
|
.endpoint_count = found.interface.endpoint_count,
|
||||||
|
.device_token = request.device_id,
|
||||||
|
.interface_class = found.interface.class,
|
||||||
|
.interface_subclass = found.interface.subclass,
|
||||||
|
.interface_protocol = found.interface.protocol,
|
||||||
|
.interface_number = found.interface.number,
|
||||||
|
};
|
||||||
|
const count = @min(found.interface.endpoint_count, transfer.max_reported_endpoints);
|
||||||
|
for (found.interface.endpoints[0..count], 0..) |endpoint, index| {
|
||||||
|
open_reply.endpoints[index] = .{
|
||||||
|
.address = endpoint.address,
|
||||||
|
.transfer_type = endpoint.transfer_type,
|
||||||
|
.max_packet_size = endpoint.max_packet_size,
|
||||||
|
.interval = endpoint.interval,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return writeReply(reply, open_reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// control: one EP0 control transfer, small data inline both ways.
|
||||||
|
fn handleControl(message: []const u8, reply: []u8) usize {
|
||||||
|
if (message.len < @sizeOf(transfer.ControlRequest)) return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
|
||||||
|
const request = std.mem.bytesToValue(transfer.ControlRequest, message[0..@sizeOf(transfer.ControlRequest)]);
|
||||||
|
const engine = if (controller) |*c| c else return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
|
||||||
|
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
|
||||||
|
|
||||||
|
const setup = std.mem.bytesToValue(usb_abi.Request, &request.setup);
|
||||||
|
const direction_in = request.direction_in != 0;
|
||||||
|
const data_length = @min(request.data_length, transfer.max_inline_data);
|
||||||
|
var data: [transfer.max_inline_data]u8 = undefined;
|
||||||
|
if (!direction_in) @memcpy(data[0..data_length], request.data[0..data_length]);
|
||||||
|
|
||||||
|
const ok = engine.controlTransfer(found.device, setup, data[0..data_length], direction_in);
|
||||||
|
var control_reply = transfer.ControlReply{ .status = if (ok) 0 else -1, .actual_length = if (ok) data_length else 0 };
|
||||||
|
if (ok and direction_in) @memcpy(control_reply.data[0..data_length], data[0..data_length]);
|
||||||
|
return writeReply(reply, control_reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// interrupt_subscribe: arm periodic IN polling; reports flow back asynchronously.
|
||||||
|
fn handleSubscribe(message: []const u8, reply: []u8) usize {
|
||||||
|
if (message.len < @sizeOf(transfer.InterruptSubscribeRequest)) return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
|
||||||
|
const request = std.mem.bytesToValue(transfer.InterruptSubscribeRequest, message[0..@sizeOf(transfer.InterruptSubscribeRequest)]);
|
||||||
|
const engine = if (controller) |*c| c else return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
|
||||||
|
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
|
||||||
|
const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
|
||||||
|
const report_endpoint = reportEndpointFor(request.device_token) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
|
||||||
|
const ok = engine.subscribeInterrupt(found.device, endpoint, request.device_token, report_endpoint);
|
||||||
|
return writeReply(reply, transfer.InterruptSubscribeReply{ .status = if (ok) 0 else -1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// bulk: one bulk transfer to/from the class driver's own DMA buffer (by physical
|
||||||
|
/// address), so sector-sized data never crosses IPC.
|
||||||
|
fn handleBulk(message: []const u8, reply: []u8) usize {
|
||||||
|
if (message.len < @sizeOf(transfer.BulkRequest)) return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
|
||||||
|
const request = std.mem.bytesToValue(transfer.BulkRequest, message[0..@sizeOf(transfer.BulkRequest)]);
|
||||||
|
const engine = if (controller) |*c| c else return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
|
||||||
|
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
|
||||||
|
const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
|
||||||
|
const transferred = engine.bulkTransfer(found.device, endpoint, request.physical_address, request.length);
|
||||||
|
return writeReply(reply, transfer.BulkReply{ .status = if (transferred != null) 0 else -1, .actual_length = transferred orelse 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The poll timer landed: drain any interrupt reports off the event ring and push
|
||||||
|
/// each to the class driver that subscribed, then re-arm the timer.
|
||||||
|
fn onNotification(badge: u64) void {
|
||||||
|
if (badge & runtime.ipc.notify_timer_bit == 0) return;
|
||||||
|
if (controller) |*engine| {
|
||||||
|
engine.pump();
|
||||||
|
while (engine.takeReport()) |report| {
|
||||||
|
var message = transfer.InterruptReport{
|
||||||
|
.device_token = report.device_token,
|
||||||
|
.endpoint_address = report.endpoint_address,
|
||||||
|
.length = @intCast(@min(report.length, transfer.max_report_data)),
|
||||||
|
};
|
||||||
|
const n = @min(report.length, transfer.max_report_data);
|
||||||
|
@memcpy(message.data[0..n], report.data[0..n]);
|
||||||
|
_ = runtime.ipc.send(report.report_endpoint, std.mem.asBytes(&message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = runtime.system.timerOnce(service_endpoint, poll_interval_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(init: runtime.process.Init) void {
|
||||||
|
const argument = init.arguments.get(1) orelse {
|
||||||
|
_ = runtime.system.write("/system/drivers/usb-xhci-bus: missing controller device id (argv[1])\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
controller_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: malformed controller device id '{s}'\n", .{argument});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
runtime.service.run(transfer.message_maximum, .{
|
||||||
|
.service = .usb_bus,
|
||||||
|
.init = initialise,
|
||||||
|
.on_message = onMessage,
|
||||||
|
.on_notification = onNotification,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = runtime.panic;
|
||||||
|
comptime {
|
||||||
|
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -80,6 +80,35 @@ var timer_hz: u32 = 0;
|
|||||||
var tsc_hz: u64 = 0;
|
var tsc_hz: u64 = 0;
|
||||||
var tsc_base: u64 = 0;
|
var tsc_base: u64 = 0;
|
||||||
|
|
||||||
|
/// Whether the TSC is architecturally **invariant** — a constant rate regardless of
|
||||||
|
/// P/C-state transitions, and thus valid as a clocksource (CPUID leaf 0x80000007,
|
||||||
|
/// EDX bit 8). AMD and modern Intel set it; the bare qemu64 model does not. Measured
|
||||||
|
/// frequency alone is not enough: a non-invariant TSC speeds up and slows down with
|
||||||
|
/// the core clock, so reading it as wall time would drift.
|
||||||
|
var tsc_invariant: bool = false;
|
||||||
|
/// Cleared if the cross-core warp check (checkWarpSource) ever sees the TSC read
|
||||||
|
/// lower on one core than the max another core has already published — i.e. the
|
||||||
|
/// per-core TSCs are not synchronized, and a task migrating cores could see time go
|
||||||
|
/// backward. Starts true (assume synchronized until proven otherwise).
|
||||||
|
var tsc_synced: bool = true;
|
||||||
|
/// The worst backward skew the warp check observed, in TSC cycles (0 = none).
|
||||||
|
var tsc_warp_cycles: u64 = 0;
|
||||||
|
|
||||||
|
/// The monotonic clock's source. The TSC when it is invariant *and* synchronized —
|
||||||
|
/// the fast `rdtsc` path taken on real Intel/AMD and modern VMs. Otherwise the HPET
|
||||||
|
/// main counter: a single fixed-rate counter, immune to both per-core skew and
|
||||||
|
/// frequency scaling, so it stays accurate on a bare VM or a warped machine.
|
||||||
|
const ClockSource = enum { tsc, hpet };
|
||||||
|
var clock_source: ClockSource = .tsc;
|
||||||
|
|
||||||
|
/// HPET standby clocksource, set up in calibrate() whenever an HPET exists (whether
|
||||||
|
/// or not calibration itself measured against it): its frequency, the counter value
|
||||||
|
/// chosen as the zero point, and its width mask. Only a 64-bit HPET is used as a
|
||||||
|
/// clocksource — a 32-bit one wraps too fast to be monotonic without accumulation.
|
||||||
|
var hpet_clock_hz: u64 = 0;
|
||||||
|
var hpet_clock_base: u64 = 0;
|
||||||
|
var hpet_clock_mask: u64 = ~@as(u64, 0);
|
||||||
|
|
||||||
/// Read the 64-bit Time Stamp Counter.
|
/// Read the 64-bit Time Stamp Counter.
|
||||||
fn rdtsc() u64 {
|
fn rdtsc() u64 {
|
||||||
var low: u32 = undefined;
|
var low: u32 = undefined;
|
||||||
@@ -220,6 +249,31 @@ pub fn calibrate() void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tsc_base = rdtsc(); // the clock's zero point (boot)
|
tsc_base = rdtsc(); // the clock's zero point (boot)
|
||||||
|
|
||||||
|
// Decide whether the TSC is trustworthy as a clocksource. Frequency (measured
|
||||||
|
// above, possibly against the HPET/PIT) is necessary but not sufficient: the TSC
|
||||||
|
// must also be *invariant* (CPUID 0x80000007 EDX[8]). AMD and modern Intel set
|
||||||
|
// this; the bare qemu64 model does not.
|
||||||
|
tsc_invariant = tscIsInvariant();
|
||||||
|
|
||||||
|
// Bring up the HPET as a standby clocksource whenever one exists — even on the
|
||||||
|
// CPUID-0x15 path where calibration never touched it — so a non-invariant TSC
|
||||||
|
// (here) or an unsynchronized one (checkWarpSource, during SMP bring-up) can fall
|
||||||
|
// back to a source that is immune to both. hpetHz() maps + enables the counter
|
||||||
|
// and is idempotent if calibration already used it.
|
||||||
|
if (configuration_hpet_base != 0) {
|
||||||
|
if (hpetHz()) |hz| {
|
||||||
|
hpet_clock_mask = hpetMask();
|
||||||
|
if (hpet_clock_mask == ~@as(u64, 0)) { // only a 64-bit HPET is monotonic enough
|
||||||
|
hpet_clock_hz = hz;
|
||||||
|
hpet_clock_base = readHpet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the source: the fast TSC when invariant, else the HPET if we have one.
|
||||||
|
// (checkWarpSource may still demote TSC -> HPET later if the cores' TSCs skew.)
|
||||||
|
if (!tsc_invariant and hpet_clock_hz != 0) clock_source = .hpet;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the LAPIC timer one-shot from its maximum count while a monotonic reference
|
/// Run the LAPIC timer one-shot from its maximum count while a monotonic reference
|
||||||
@@ -275,7 +329,7 @@ fn calibratePit() void {
|
|||||||
// --- reference clocks ------------------------------------------------------
|
// --- reference clocks ------------------------------------------------------
|
||||||
|
|
||||||
/// TSC frequency from CPUID leaf 0x15 (crystal_hz * numerator / denominator), or
|
/// TSC frequency from CPUID leaf 0x15 (crystal_hz * numerator / denominator), or
|
||||||
/// null if the CPU doesn't enumerate it (common under QEMU).
|
/// null if the CPU doesn't enumerate it (common under QEMU, and on AMD).
|
||||||
fn cpuidTscHz() ?u64 {
|
fn cpuidTscHz() ?u64 {
|
||||||
if (cpuid(0).eax < 0x15) return null;
|
if (cpuid(0).eax < 0x15) return null;
|
||||||
const r = cpuid(0x15);
|
const r = cpuid(0x15);
|
||||||
@@ -283,6 +337,15 @@ fn cpuidTscHz() ?u64 {
|
|||||||
return @as(u64, r.ecx) * r.ebx / r.eax;
|
return @as(u64, r.ecx) * r.ebx / r.eax;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the CPU advertises an **invariant** TSC (CPUID leaf 0x80000007, EDX
|
||||||
|
/// bit 8) — the architectural guarantee, on both Intel and AMD, that the TSC ticks
|
||||||
|
/// at a constant rate across P/C-states and never stops. Requires the extended-leaf
|
||||||
|
/// range to reach 0x80000007 first.
|
||||||
|
fn tscIsInvariant() bool {
|
||||||
|
if (cpuid(0x80000000).eax < 0x80000007) return false;
|
||||||
|
return (cpuid(0x80000007).edx & (1 << 8)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
const CpuidRegs = struct { eax: u32, ebx: u32, ecx: u32, edx: u32 };
|
const CpuidRegs = struct { eax: u32, ebx: u32, ecx: u32, edx: u32 };
|
||||||
|
|
||||||
fn cpuid(leaf: u32) CpuidRegs {
|
fn cpuid(leaf: u32) CpuidRegs {
|
||||||
@@ -310,11 +373,20 @@ fn hpetWrite64(off: usize, value: u64) void {
|
|||||||
@as(*volatile u64, @ptrFromInt(configuration_hpet_base + off)).* = value;
|
@as(*volatile u64, @ptrFromInt(configuration_hpet_base + off)).* = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the HPET has been mapped into the physmap yet, so `configuration_hpet_base`
|
||||||
|
/// already holds the virtual address. `hpetHz` is called more than once (calibration
|
||||||
|
/// may use the HPET, and the standby-clocksource setup asks for it again), and mapping
|
||||||
|
/// an already-mapped base a second time would double-offset it into an overflow.
|
||||||
|
var hpet_mapped: bool = false;
|
||||||
|
|
||||||
/// Map + enable the HPET and return its tick frequency, or null if unusable.
|
/// Map + enable the HPET and return its tick frequency, or null if unusable.
|
||||||
/// Maps the HPET into the physmap and switches configuration_hpet_base to that virtual
|
/// Maps the HPET into the physmap and switches configuration_hpet_base to that virtual
|
||||||
/// address, so the register accessors reach it without the identity map.
|
/// address, so the register accessors reach it without the identity map. Idempotent.
|
||||||
fn hpetHz() ?u64 {
|
fn hpetHz() ?u64 {
|
||||||
|
if (!hpet_mapped) {
|
||||||
configuration_hpet_base = paging.mapMmio(configuration_hpet_base, 0x400, true);
|
configuration_hpet_base = paging.mapMmio(configuration_hpet_base, 0x400, true);
|
||||||
|
hpet_mapped = true;
|
||||||
|
}
|
||||||
const caps = hpetRead64(0x00);
|
const caps = hpetRead64(0x00);
|
||||||
const period_fs = caps >> 32; // femtoseconds per tick
|
const period_fs = caps >> 32; // femtoseconds per tick
|
||||||
if (period_fs == 0) return null;
|
if (period_fs == 0) return null;
|
||||||
@@ -364,24 +436,169 @@ pub fn tscHz() u64 {
|
|||||||
return tsc_hz;
|
return tsc_hz;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monotonic high-resolution clock, from the TSC. A function per resolution, each
|
// Monotonic high-resolution clock. A function per resolution, each scaling the
|
||||||
// scaling the cycle delta directly at its unit (the 128-bit intermediate avoids
|
// counter delta directly at its unit (the 128-bit intermediate avoids overflow
|
||||||
// overflow across a long uptime). nanos() resolves to a few ns; millis() is what
|
// across a long uptime). nanos() resolves to a few ns on the TSC; millis() is what
|
||||||
// the scheduler uses for sleep deadlines.
|
// the scheduler uses for sleep deadlines. The source is the TSC when it is invariant
|
||||||
|
// and synchronized, else the HPET counter (see clock_source) — the branch is one
|
||||||
|
// global load and the TSC path is unchanged from before.
|
||||||
|
|
||||||
|
/// The selected source's counter delta since its zero point.
|
||||||
|
fn clockCount() u64 {
|
||||||
|
return switch (clock_source) {
|
||||||
|
.tsc => rdtsc() -% tsc_base,
|
||||||
|
// A 64-bit HPET (the only kind we select) never wraps in any realistic
|
||||||
|
// uptime, so the wrapping subtraction is exact.
|
||||||
|
.hpet => readHpet() -% hpet_clock_base,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The selected source's frequency (0 if the clock is unavailable/uncalibrated).
|
||||||
|
fn clockHertz() u64 {
|
||||||
|
return switch (clock_source) {
|
||||||
|
.tsc => tsc_hz,
|
||||||
|
.hpet => hpet_clock_hz,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn nanos() u64 {
|
pub fn nanos() u64 {
|
||||||
if (tsc_hz == 0) return 0;
|
const hz = clockHertz();
|
||||||
return @intCast(@as(u128, rdtsc() -% tsc_base) * 1_000_000_000 / tsc_hz);
|
if (hz == 0) return 0;
|
||||||
|
return @intCast(@as(u128, clockCount()) * 1_000_000_000 / hz);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn micros() u64 {
|
pub fn micros() u64 {
|
||||||
if (tsc_hz == 0) return 0;
|
const hz = clockHertz();
|
||||||
return @intCast(@as(u128, rdtsc() -% tsc_base) * 1_000_000 / tsc_hz);
|
if (hz == 0) return 0;
|
||||||
|
return @intCast(@as(u128, clockCount()) * 1_000_000 / hz);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn millis() u64 {
|
pub fn millis() u64 {
|
||||||
if (tsc_hz == 0) return 0;
|
const hz = clockHertz();
|
||||||
return @intCast(@as(u128, rdtsc() -% tsc_base) * 1_000 / tsc_hz);
|
if (hz == 0) return 0;
|
||||||
|
return @intCast(@as(u128, clockCount()) * 1_000 / hz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the CPU advertises an invariant TSC (CPUID 0x80000007 EDX[8]).
|
||||||
|
pub fn tscInvariant() bool {
|
||||||
|
return tsc_invariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test hook: force the TSC clocksource on, as if the CPU had advertised an invariant
|
||||||
|
/// TSC. QEMU's TCG accelerator (the only one for an x86 guest on an Apple-Silicon
|
||||||
|
/// host) does not expose the invariant-TSC bit — its emulated TSC isn't invariant — so
|
||||||
|
/// the tsc-sync test can't reach the real-Intel/AMD/KVM path through CPUID. This lets
|
||||||
|
/// that test exercise the TSC clocksource and the cross-core warp check anyway. tsc_base
|
||||||
|
/// is left as-is so the switch from the HPET is continuous.
|
||||||
|
pub fn forceTscClocksourceForTest() void {
|
||||||
|
tsc_invariant = true;
|
||||||
|
clock_source = .tsc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many per-AP warp checks actually ran (a rendezvous completed) — lets a test
|
||||||
|
/// confirm the cross-core check executed rather than being skipped.
|
||||||
|
pub fn warpChecksRun() u32 {
|
||||||
|
return warp_checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the per-core TSCs are synchronized (no backward warp seen at bring-up).
|
||||||
|
pub fn tscSynced() bool {
|
||||||
|
return tsc_synced;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The active monotonic clocksource, for the boot log and tests.
|
||||||
|
pub fn clockSourceName() []const u8 {
|
||||||
|
return switch (clock_source) {
|
||||||
|
.tsc => "tsc",
|
||||||
|
.hpet => "hpet",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- cross-core TSC synchronization ("warp") check -------------------------
|
||||||
|
// Two cores hammer a shared "max seen" TSC value under a lock; if either reads a
|
||||||
|
// value below that max, its TSC lags the other's, and time would run backward for a
|
||||||
|
// task migrating between them (Linux calls this a warp). danos brings APs up one at a
|
||||||
|
// time, so this runs pairwise: the BSP (source) against each AP (target) as it comes
|
||||||
|
// online. It only matters — and only runs — while the TSC is the clocksource; on a
|
||||||
|
// machine already on the HPET (a bare VM) the whole rendezvous is skipped.
|
||||||
|
|
||||||
|
var warp_lock: u32 = 0;
|
||||||
|
var warp_last: u64 = 0;
|
||||||
|
var warp_bsp_ready: u32 = 0;
|
||||||
|
var warp_ap_ready: u32 = 0;
|
||||||
|
var warp_stop: u32 = 0;
|
||||||
|
var warp_checks: u32 = 0; // completed per-AP rendezvous count (for the tsc-sync test)
|
||||||
|
|
||||||
|
const warp_rounds: u32 = 1 << 20; // locked reads on the BSP: ~1 ms at GHz rates
|
||||||
|
const warp_spin_limit: u64 = 1 << 32; // bound every rendezvous wait so a lost core can't hang boot
|
||||||
|
|
||||||
|
fn warpTick() void {
|
||||||
|
while (@cmpxchgWeak(u32, &warp_lock, 0, 1, .acquire, .monotonic) != null) asm volatile ("pause");
|
||||||
|
const t = rdtsc();
|
||||||
|
if (t < warp_last) {
|
||||||
|
const delta = warp_last - t;
|
||||||
|
if (delta > tsc_warp_cycles) tsc_warp_cycles = delta;
|
||||||
|
tsc_synced = false;
|
||||||
|
} else {
|
||||||
|
warp_last = t;
|
||||||
|
}
|
||||||
|
@atomicStore(u32, &warp_lock, 0, .release);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spin (bounded) until `flag` is nonzero; false on timeout.
|
||||||
|
fn warpAwait(flag: *u32) bool {
|
||||||
|
var spins: u64 = 0;
|
||||||
|
while (@atomicLoad(u32, flag, .acquire) == 0) : (spins += 1) {
|
||||||
|
if (spins >= warp_spin_limit) return false;
|
||||||
|
asm volatile ("pause");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BSP side of the pairwise TSC warp check, run once per AP as it reports in. No-op
|
||||||
|
/// unless the TSC is the active clocksource. If the AP's TSC proves to lag, demote
|
||||||
|
/// the monotonic clock to the HPET without a discontinuity.
|
||||||
|
pub fn checkWarpSource() void {
|
||||||
|
if (clock_source != .tsc) return;
|
||||||
|
warp_last = 0;
|
||||||
|
@atomicStore(u32, &warp_stop, 0, .release);
|
||||||
|
@atomicStore(u32, &warp_ap_ready, 0, .release);
|
||||||
|
@atomicStore(u32, &warp_bsp_ready, 1, .release);
|
||||||
|
if (!warpAwait(&warp_ap_ready)) { // AP never joined the rendezvous; skip, don't hang
|
||||||
|
@atomicStore(u32, &warp_bsp_ready, 0, .release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < warp_rounds) : (i += 1) warpTick();
|
||||||
|
@atomicStore(u32, &warp_stop, 1, .release);
|
||||||
|
@atomicStore(u32, &warp_bsp_ready, 0, .release);
|
||||||
|
warp_checks += 1;
|
||||||
|
|
||||||
|
if (!tsc_synced and hpet_clock_hz != 0) demoteToHpet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AP side: join the BSP's warp check, then return so the core can enter the
|
||||||
|
/// scheduler. Bounded so a missing BSP can't strand the core.
|
||||||
|
pub fn checkWarpTarget() void {
|
||||||
|
if (clock_source != .tsc) return;
|
||||||
|
if (!warpAwait(&warp_bsp_ready)) return;
|
||||||
|
@atomicStore(u32, &warp_ap_ready, 1, .release);
|
||||||
|
var spins: u64 = 0;
|
||||||
|
while (@atomicLoad(u32, &warp_stop, .acquire) == 0) : (spins += 1) {
|
||||||
|
if (spins >= warp_spin_limit) return;
|
||||||
|
warpTick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch the clocksource from the TSC to the HPET without a discontinuity: choose
|
||||||
|
/// the HPET zero point so it reads the same nanosecond value the TSC does right now,
|
||||||
|
/// so time neither jumps nor runs backward across the switch. Called when the warp
|
||||||
|
/// check proves the per-core TSCs unsynchronized.
|
||||||
|
fn demoteToHpet() void {
|
||||||
|
const now_ns = @as(u128, rdtsc() -% tsc_base) * 1_000_000_000 / tsc_hz;
|
||||||
|
const equivalent_ticks: u64 = @intCast(now_ns * hpet_clock_hz / 1_000_000_000);
|
||||||
|
hpet_clock_base = readHpet() -% equivalent_ticks;
|
||||||
|
clock_source = .hpet;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Acknowledge the current interrupt so the LAPIC will deliver the next one.
|
/// Acknowledge the current interrupt so the LAPIC will deliver the next one.
|
||||||
|
|||||||
@@ -106,6 +106,12 @@ pub fn serialWrite(bytes: []const u8) void {
|
|||||||
serial.write(bytes);
|
serial.write(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a working UART was detected (loopback probe). When false the serial
|
||||||
|
/// sink is silently inert — a dead legacy COM1 costs nothing per byte.
|
||||||
|
pub fn serialPresent() bool {
|
||||||
|
return serial.present();
|
||||||
|
}
|
||||||
|
|
||||||
/// Emit a one-byte progress checkpoint to whatever hardware debug sink the
|
/// Emit a one-byte progress checkpoint to whatever hardware debug sink the
|
||||||
/// platform has — here the POST diagnostic port (0x80), which a POST card or BMC
|
/// platform has — here the POST diagnostic port (0x80), which a POST card or BMC
|
||||||
/// displays. The last-resort progress signal when there's no text output at all.
|
/// displays. The last-resort progress signal when there's no text output at all.
|
||||||
@@ -162,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
|
||||||
@@ -469,6 +483,124 @@ pub fn clockHz() u64 {
|
|||||||
return apic.tscHz();
|
return apic.tscHz();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- real-time clock (CMOS) --------------------------------------------------
|
||||||
|
//
|
||||||
|
// The battery-backed CMOS clock, read once at boot and thereafter anchored to the
|
||||||
|
// monotonic clock (see kernel/wall-clock.zig) — so this is never on a hot path and
|
||||||
|
// needs no lock. Wall-clock *seconds* are mechanism the kernel owns (the hardware's
|
||||||
|
// value), like the monotonic clock; calendars/timezones are policy layered on top.
|
||||||
|
|
||||||
|
fn cmosRead(register: u8) u8 {
|
||||||
|
io.outb(0x70, register);
|
||||||
|
return io.inb(0x71);
|
||||||
|
}
|
||||||
|
|
||||||
|
const RtcFields = struct { second: u8, minute: u8, hour: u8, day: u8, month: u8, year: u8 };
|
||||||
|
|
||||||
|
fn rtcRaw() RtcFields {
|
||||||
|
while (cmosRead(0x0A) & 0x80 != 0) {} // wait out any update in progress (status A bit 7)
|
||||||
|
return .{
|
||||||
|
.second = cmosRead(0x00),
|
||||||
|
.minute = cmosRead(0x02),
|
||||||
|
.hour = cmosRead(0x04),
|
||||||
|
.day = cmosRead(0x07),
|
||||||
|
.month = cmosRead(0x08),
|
||||||
|
.year = cmosRead(0x09),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bcdToBinary(v: u8) u8 {
|
||||||
|
return (v & 0x0F) + ((v >> 4) * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isLeapYear(y: u32) bool {
|
||||||
|
return (y % 4 == 0 and y % 100 != 0) or (y % 400 == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the CMOS real-time clock and convert it to Unix epoch seconds (UTC).
|
||||||
|
pub fn readRtcUnixSeconds() u64 {
|
||||||
|
// Read until two consecutive reads agree, so we never latch a half-updated time.
|
||||||
|
var a = rtcRaw();
|
||||||
|
while (true) {
|
||||||
|
const b = rtcRaw();
|
||||||
|
if (a.second == b.second and a.minute == b.minute and a.hour == b.hour and
|
||||||
|
a.day == b.day and a.month == b.month and a.year == b.year) break;
|
||||||
|
a = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status_b = cmosRead(0x0B);
|
||||||
|
const binary_mode = status_b & 0x04 != 0; // else BCD
|
||||||
|
const hour_24 = status_b & 0x02 != 0; // else 12-hour with a PM bit
|
||||||
|
|
||||||
|
var second = a.second;
|
||||||
|
var minute = a.minute;
|
||||||
|
var hour_field = a.hour;
|
||||||
|
var day = a.day;
|
||||||
|
var month = a.month;
|
||||||
|
var year = a.year;
|
||||||
|
if (!binary_mode) {
|
||||||
|
second = bcdToBinary(second);
|
||||||
|
minute = bcdToBinary(minute);
|
||||||
|
hour_field = bcdToBinary(hour_field & 0x7F) | (hour_field & 0x80); // preserve the PM bit
|
||||||
|
day = bcdToBinary(day);
|
||||||
|
month = bcdToBinary(month);
|
||||||
|
year = bcdToBinary(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hour: u32 = hour_field & 0x7F;
|
||||||
|
if (!hour_24) {
|
||||||
|
const pm = hour_field & 0x80 != 0;
|
||||||
|
hour %= 12; // 12 AM/PM -> 0
|
||||||
|
if (pm) hour += 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CMOS year is 0..99; QEMU and modern hardware mean 20xx (there is no
|
||||||
|
// reliable century register on QEMU). Treat < 70 as 20xx, else 19xx.
|
||||||
|
const full_year: u32 = if (year < 70) 2000 + @as(u32, year) else 1900 + @as(u32, year);
|
||||||
|
|
||||||
|
var days: u64 = 0;
|
||||||
|
var y: u32 = 1970;
|
||||||
|
while (y < full_year) : (y += 1) days += if (isLeapYear(y)) 366 else 365;
|
||||||
|
const month_lengths = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
||||||
|
var m: u8 = 1;
|
||||||
|
while (m < month) : (m += 1) {
|
||||||
|
days += month_lengths[m - 1];
|
||||||
|
if (m == 2 and isLeapYear(full_year)) days += 1;
|
||||||
|
}
|
||||||
|
days += @as(u64, day) - 1;
|
||||||
|
|
||||||
|
return ((days * 24 + hour) * 60 + minute) * 60 + second;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the CPU guarantees an **invariant** TSC (CPUID 0x80000007 EDX[8] on
|
||||||
|
/// x86; the analogous architectural guarantee elsewhere). When false the TSC is not
|
||||||
|
/// used as the clocksource.
|
||||||
|
pub fn clockInvariant() bool {
|
||||||
|
return apic.tscInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the per-core clock counters are synchronized (no backward warp observed
|
||||||
|
/// at SMP bring-up). When false the clock falls back off the TSC.
|
||||||
|
pub fn clockSynchronized() bool {
|
||||||
|
return apic.tscSynced();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The active monotonic clocksource, for the boot log ("tsc" or "hpet" on x86).
|
||||||
|
pub fn clockSourceName() []const u8 {
|
||||||
|
return apic.clockSourceName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test hook: force the TSC clocksource on, to exercise the TSC + warp-check path on
|
||||||
|
/// a hypervisor that won't advertise an invariant TSC (see apic.forceTscClocksourceForTest).
|
||||||
|
pub fn forceTscClocksourceForTest() void {
|
||||||
|
apic.forceTscClocksourceForTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many per-AP TSC warp checks completed (for the tsc-sync test).
|
||||||
|
pub fn warpChecksRun() u32 {
|
||||||
|
return apic.warpChecksRun();
|
||||||
|
}
|
||||||
|
|
||||||
/// Unmask maskable interrupts (`sti`) so device interrupts get delivered.
|
/// Unmask maskable interrupts (`sti`) so device interrupts get delivered.
|
||||||
pub fn enableInterrupts() void {
|
pub fn enableInterrupts() void {
|
||||||
asm volatile ("sti");
|
asm volatile ("sti");
|
||||||
@@ -490,8 +622,7 @@ pub fn saveInterrupts() u64 {
|
|||||||
\\cli
|
\\cli
|
||||||
: [f] "=r" (flags),
|
: [f] "=r" (flags),
|
||||||
:
|
:
|
||||||
: .{ .memory = true }
|
: .{ .memory = true });
|
||||||
);
|
|
||||||
return flags;
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,13 @@
|
|||||||
//! unmapped as a null guard. It also exposes map/unmap for on-demand mapping,
|
//! unmapped as a null guard. It also exposes map/unmap for on-demand mapping,
|
||||||
//! which the kernel heap will build on.
|
//! which the kernel heap will build on.
|
||||||
//!
|
//!
|
||||||
//! Everything is 4 KiB pages — precise and simple; the extra table memory is
|
//! The physmap (the permanent window onto all physical RAM) is built with 2 MiB
|
||||||
//! negligible against available RAM.
|
//! huge pages wherever the range is 2 MiB-aligned, falling back to 4 KiB for the
|
||||||
|
//! unaligned edges. On a big machine that is the difference between ~16.7M page-
|
||||||
|
//! table entries (128 MiB of tables) and ~32K — it makes both the build and the
|
||||||
|
//! footprint scale sanely with RAM. Everything else (kernel segments, heap, user
|
||||||
|
//! space, on-demand MMIO) stays 4 KiB: precise, and the table memory is
|
||||||
|
//! negligible there.
|
||||||
|
|
||||||
const boot_handoff = @import("boot-handoff");
|
const boot_handoff = @import("boot-handoff");
|
||||||
const abi = @import("abi");
|
const abi = @import("abi");
|
||||||
@@ -22,10 +27,22 @@ const writable: u64 = 1 << 1;
|
|||||||
const user: u64 = 1 << 2; // U/S: accessible from ring 3 (must be set at every level)
|
const user: u64 = 1 << 2; // U/S: accessible from ring 3 (must be set at every level)
|
||||||
const pwt: u64 = 1 << 3; // page write-through
|
const pwt: u64 = 1 << 3; // page write-through
|
||||||
const pcd: u64 = 1 << 4; // page cache disable (with PWT: strong-uncacheable under the default PAT)
|
const pcd: u64 = 1 << 4; // page cache disable (with PWT: strong-uncacheable under the default PAT)
|
||||||
|
const page_size_bit: u64 = 1 << 7; // PS: this PDPT/PD entry is a 1 GiB/2 MiB leaf, not a pointer to the next table
|
||||||
const device_grant: u64 = 1 << 9; // available bit: this leaf maps device MMIO, not RAM — do not reclaim
|
const device_grant: u64 = 1 << 9; // available bit: this leaf maps device MMIO, not RAM — do not reclaim
|
||||||
const no_execute: u64 = 1 << 63;
|
const no_execute: u64 = 1 << 63;
|
||||||
const address_mask: u64 = 0x000F_FFFF_FFFF_F000;
|
const address_mask: u64 = 0x000F_FFFF_FFFF_F000;
|
||||||
|
|
||||||
|
// The PAT-index bit. In a 4 KiB PTE it is bit 7; in a huge leaf (2 MiB PDE / 1 GiB
|
||||||
|
// PDPTE) bit 7 is PS, so the PAT bit moves to bit 12. With PCD=PWT=0 this selects
|
||||||
|
// PAT entry 4, which `setupPat` programs to write-combining (see mapRangePhysmap).
|
||||||
|
const pte_pat: u64 = 1 << 7;
|
||||||
|
const huge_pat: u64 = 1 << 12;
|
||||||
|
const ia32_pat: u32 = 0x277;
|
||||||
|
|
||||||
|
/// The physmap's page size for 2 MiB-aligned RAM: one PD leaf covers this instead
|
||||||
|
/// of 512 PT entries. 4 KiB pages fill the unaligned edges (see mapRangePhysmap).
|
||||||
|
const huge_page_size: u64 = 2 << 20; // 2 MiB
|
||||||
|
|
||||||
// ELF segment flags (p_flags).
|
// ELF segment flags (p_flags).
|
||||||
const pf_x: u32 = 1;
|
const pf_x: u32 = 1;
|
||||||
const pf_w: u32 = 2;
|
const pf_w: u32 = 2;
|
||||||
@@ -74,7 +91,15 @@ fn allocTable() u64 {
|
|||||||
/// entries are writable and executable so the leaf's bits govern (a page is
|
/// entries are writable and executable so the leaf's bits govern (a page is
|
||||||
/// writable only if every level is; non-executable if any level is).
|
/// writable only if every level is; non-executable if any level is).
|
||||||
fn descend(entry: *u64) u64 {
|
fn descend(entry: *u64) u64 {
|
||||||
if (entry.* & present != 0) return entry.* & address_mask;
|
if (entry.* & present != 0) {
|
||||||
|
// A present-but-huge entry is a leaf, not a table: descending would read
|
||||||
|
// its 2 MiB/1 GiB data frame as a page table and corrupt RAM. This only
|
||||||
|
// fires on a bug — a 4 KiB map landing inside a physmap huge page — and a
|
||||||
|
// loud panic beats silent corruption. (The physmap and the 4 KiB regions
|
||||||
|
// live in disjoint PML4 slots, so it should never happen.)
|
||||||
|
if (entry.* & page_size_bit != 0) @panic("paging: descend through a huge-page leaf");
|
||||||
|
return entry.* & address_mask;
|
||||||
|
}
|
||||||
const frame = allocTable();
|
const frame = allocTable();
|
||||||
entry.* = frame | present | writable;
|
entry.* = frame | present | writable;
|
||||||
return frame;
|
return frame;
|
||||||
@@ -96,15 +121,39 @@ fn mapPage(pml4: u64, virtual: u64, physical: u64, flags: u64) void {
|
|||||||
tableAt(pt)[(virtual >> 12) & 0x1FF] = (physical & address_mask) | flags | present;
|
tableAt(pt)[(virtual >> 12) & 0x1FF] = (physical & address_mask) | flags | present;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Map one 2 MiB huge page `virtual` -> `physical` with `flags` — a leaf at the PD
|
||||||
|
/// level (PS bit set), with no PT beneath it. Both addresses must be 2 MiB-aligned.
|
||||||
|
/// One of these replaces 512 `mapPage`s (and the PT frame they'd need).
|
||||||
|
fn mapHugePage(pml4: u64, virtual: u64, physical: u64, flags: u64) void {
|
||||||
|
const pml4e = &tableAt(pml4)[(virtual >> 39) & 0x1FF];
|
||||||
|
if (init_done and (virtual >> 63) == 1 and pml4e.* & present == 0)
|
||||||
|
@panic("paging: new higher-half PML4 entry after init");
|
||||||
|
const pdpt = descend(pml4e);
|
||||||
|
const pdpte = &tableAt(pdpt)[(virtual >> 30) & 0x1FF];
|
||||||
|
const pd = descend(pdpte);
|
||||||
|
tableAt(pd)[(virtual >> 21) & 0x1FF] = (physical & address_mask) | flags | present | page_size_bit;
|
||||||
|
}
|
||||||
|
|
||||||
/// Map [physical_base, physical_base+len) into the physmap (at physicalToVirtual(physical)) with
|
/// Map [physical_base, physical_base+len) into the physmap (at physicalToVirtual(physical)) with
|
||||||
/// `flags`, rounded out to whole pages. This is how the kernel keeps a permanent
|
/// `flags`, rounded out to whole pages. This is how the kernel keeps a permanent
|
||||||
/// window onto physical memory once the low identity map goes away.
|
/// window onto physical memory once the low identity map goes away. The 2 MiB-
|
||||||
fn mapRangePhysmap(pml4: u64, physical_base: u64, len: u64, flags: u64) void {
|
/// aligned interior is mapped with huge pages; the unaligned head/tail with 4 KiB.
|
||||||
|
/// `write_combining` selects the WC memory type (setupPat's PAT entry 4) via the
|
||||||
|
/// PAT bit — bit 7 in a 4 KiB PTE, bit 12 in a huge leaf — for the framebuffer.
|
||||||
|
fn mapRangePhysmap(pml4: u64, physical_base: u64, len: u64, flags: u64, write_combining: bool) void {
|
||||||
|
const pte_flags = if (write_combining) flags | pte_pat else flags;
|
||||||
|
const huge_flags = if (write_combining) flags | huge_pat else flags;
|
||||||
var address = physical_base & ~@as(u64, page_size - 1);
|
var address = physical_base & ~@as(u64, page_size - 1);
|
||||||
const end = physical_base + len;
|
const end = physical_base + len;
|
||||||
while (address < end) : (address += page_size) {
|
// Head: 4 KiB pages up to the next 2 MiB boundary.
|
||||||
mapPage(pml4, boot_handoff.physicalToVirtual(address), address, flags);
|
while (address < end and address & (huge_page_size - 1) != 0) : (address += page_size)
|
||||||
}
|
mapPage(pml4, boot_handoff.physicalToVirtual(address), address, pte_flags);
|
||||||
|
// Interior: 2 MiB huge pages while a whole one still fits.
|
||||||
|
while (address + huge_page_size <= end) : (address += huge_page_size)
|
||||||
|
mapHugePage(pml4, boot_handoff.physicalToVirtual(address), address, huge_flags);
|
||||||
|
// Tail: 4 KiB pages for whatever is left.
|
||||||
|
while (address < end) : (address += page_size)
|
||||||
|
mapPage(pml4, boot_handoff.physicalToVirtual(address), address, pte_flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn regions(mm: boot_handoff.MemoryMap) []const boot_handoff.MemoryRegion {
|
fn regions(mm: boot_handoff.MemoryMap) []const boot_handoff.MemoryRegion {
|
||||||
@@ -118,11 +167,26 @@ fn enableNx() void {
|
|||||||
io.wrmsr(efer_msr, io.rdmsr(efer_msr) | (1 << 11));
|
io.wrmsr(efer_msr, io.rdmsr(efer_msr) | (1 << 11));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Program this core's PAT so entry 4 (selected by the PAT bit with PCD=PWT=0) is
|
||||||
|
/// **write-combining**, leaving the other seven at their reset types. Nothing else
|
||||||
|
/// in danos sets the PAT bit, so this changes no existing mapping — it only gives
|
||||||
|
/// the framebuffer a write-combining type, which turns its full-screen clear from
|
||||||
|
/// glacial (uncached writes to a GPU BAR, the real-hardware default via MTRRs) into
|
||||||
|
/// a batched burst. Must run on **every** core (PAT is per-logical-processor) — the
|
||||||
|
/// framebuffer mapping lives in the shared kernel half, so a core with the reset
|
||||||
|
/// PAT would see it as write-back and alias. Called from `init` (BSP) and each AP.
|
||||||
|
pub fn setupPat() void {
|
||||||
|
// Reset PAT is PA0=WB PA1=WT PA2=UC- PA3=UC PA4=WB PA5=WT PA6=UC- PA7=UC; flip
|
||||||
|
// PA4 from WB (0x06) to WC (0x01). Type codes: UC=0 WC=1 WT=4 WP=5 WB=6 UC-=7.
|
||||||
|
io.wrmsr(ia32_pat, 0x0007_0401_0007_0406);
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the address space and switch onto it.
|
/// Build the address space and switch onto it.
|
||||||
pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot_information: *const boot_handoff.BootInformation) void {
|
pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot_information: *const boot_handoff.BootInformation) void {
|
||||||
alloc_frame = allocFrame;
|
alloc_frame = allocFrame;
|
||||||
free_frame = freeFrame;
|
free_frame = freeFrame;
|
||||||
enableNx();
|
enableNx();
|
||||||
|
setupPat(); // BSP: PAT entry 4 = write-combining, for the framebuffer window
|
||||||
const pml4 = allocTable();
|
const pml4 = allocTable();
|
||||||
|
|
||||||
// 1. All RAM in the physmap (physicalToVirtual(physical)) RW + NX. No identity/low-half
|
// 1. All RAM in the physmap (physicalToVirtual(physical)) RW + NX. No identity/low-half
|
||||||
@@ -130,13 +194,15 @@ pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot
|
|||||||
// mapped on demand (mapMmio) or explicitly below.
|
// mapped on demand (mapMmio) or explicitly below.
|
||||||
for (regions(boot_information.memory_map)) |r| {
|
for (regions(boot_information.memory_map)) |r| {
|
||||||
if (r.kind == .mmio) continue;
|
if (r.kind == .mmio) continue;
|
||||||
mapRangePhysmap(pml4, r.base, r.pages * page_size, present | writable | no_execute);
|
mapRangePhysmap(pml4, r.base, r.pages * page_size, present | writable | no_execute, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Physmap windows for the framebuffer and the Local APIC (device memory
|
// 2. Physmap windows for the framebuffer and the Local APIC (device memory
|
||||||
// the kernel touches directly), RW + NX.
|
// the kernel touches directly), RW + NX. The framebuffer is **write-
|
||||||
|
// combining** (see setupPat) so the console's full-screen clear is a burst,
|
||||||
|
// not millions of uncached single-word writes.
|
||||||
const fb = boot_information.framebuffer;
|
const fb = boot_information.framebuffer;
|
||||||
mapRangePhysmap(pml4, fb.base, @as(u64, fb.height) * fb.pitch, present | writable | no_execute);
|
mapRangePhysmap(pml4, fb.base, @as(u64, fb.height) * fb.pitch, present | writable | no_execute, true);
|
||||||
mapPage(pml4, boot_handoff.physicalToVirtual(0xFEE00000), 0xFEE00000, present | writable | no_execute);
|
mapPage(pml4, boot_handoff.physicalToVirtual(0xFEE00000), 0xFEE00000, present | writable | no_execute);
|
||||||
|
|
||||||
// 3. The kernel's own segments at their higher-half link addresses, mapped
|
// 3. The kernel's own segments at their higher-half link addresses, mapped
|
||||||
@@ -166,8 +232,7 @@ pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot
|
|||||||
asm volatile ("mov %[pml4], %%cr3"
|
asm volatile ("mov %[pml4], %%cr3"
|
||||||
:
|
:
|
||||||
: [pml4] "r" (pml4),
|
: [pml4] "r" (pml4),
|
||||||
: .{ .memory = true }
|
: .{ .memory = true });
|
||||||
);
|
|
||||||
on_own_tables = true; // now on the kernel's physmap (covers all RAM)
|
on_own_tables = true; // now on the kernel's physmap (covers all RAM)
|
||||||
init_done = true; // the kernel half is fixed from here
|
init_done = true; // the kernel half is fixed from here
|
||||||
}
|
}
|
||||||
@@ -255,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;
|
||||||
@@ -297,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
|
||||||
@@ -339,8 +435,8 @@ fn freeSubtree(physical: u64, level: u32) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Whether `virtual` is currently mapped **executable** — present with the NX bit
|
/// Whether `virtual` is currently mapped **executable** — present with the NX bit
|
||||||
/// clear. Walks the 4-level tables (all danos mappings are 4 KiB, so no huge-page
|
/// clear. Walks the 4-level tables, stopping at a 2 MiB huge-page leaf (the physmap
|
||||||
/// case). Returns false if unmapped. Used for W^X checks in tests.
|
/// uses them). Returns false if unmapped. Used for W^X checks in tests.
|
||||||
pub fn isExecutable(virtual: u64) bool {
|
pub fn isExecutable(virtual: u64) bool {
|
||||||
const pml4e = tableAt(kernel_pml4)[(virtual >> 39) & 0x1FF];
|
const pml4e = tableAt(kernel_pml4)[(virtual >> 39) & 0x1FF];
|
||||||
if (pml4e & present == 0) return false;
|
if (pml4e & present == 0) return false;
|
||||||
@@ -348,6 +444,7 @@ pub fn isExecutable(virtual: u64) bool {
|
|||||||
if (pdpte & present == 0) return false;
|
if (pdpte & present == 0) return false;
|
||||||
const pde = tableAt(pdpte & address_mask)[(virtual >> 21) & 0x1FF];
|
const pde = tableAt(pdpte & address_mask)[(virtual >> 21) & 0x1FF];
|
||||||
if (pde & present == 0) return false;
|
if (pde & present == 0) return false;
|
||||||
|
if (pde & page_size_bit != 0) return pde & no_execute == 0; // 2 MiB huge leaf
|
||||||
const pte = tableAt(pde & address_mask)[(virtual >> 12) & 0x1FF];
|
const pte = tableAt(pde & address_mask)[(virtual >> 12) & 0x1FF];
|
||||||
if (pte & present == 0) return false;
|
if (pte & present == 0) return false;
|
||||||
return pte & no_execute == 0;
|
return pte & no_execute == 0;
|
||||||
@@ -386,9 +483,9 @@ pub fn unmapInto(pml4: u64, virtual: u64) void {
|
|||||||
/// Resolve a virtual address to a physical one in the address space rooted at
|
/// Resolve a virtual address to a physical one in the address space rooted at
|
||||||
/// `pml4`, walking the tables through the physmap (CR3-independent — works for
|
/// `pml4`, walking the tables through the physmap (CR3-independent — works for
|
||||||
/// any address space, not just the live one). Returns null if `virtual` is not
|
/// any address space, not just the live one). Returns null if `virtual` is not
|
||||||
/// mapped at any level. All danos mappings are 4 KiB, so there is no huge-page
|
/// mapped at any level. Stops at a 2 MiB huge-page leaf (the physmap uses them),
|
||||||
/// case. The foundation for cross-address-space copies and for munmap (which
|
/// resolving the offset within it. The foundation for cross-address-space copies
|
||||||
/// needs the frame behind a user vaddr to free it).
|
/// and for munmap (which needs the frame behind a user vaddr to free it).
|
||||||
pub fn translateIn(pml4: u64, virtual: u64) ?u64 {
|
pub fn translateIn(pml4: u64, virtual: u64) ?u64 {
|
||||||
const pml4e = tableAt(pml4)[(virtual >> 39) & 0x1FF];
|
const pml4e = tableAt(pml4)[(virtual >> 39) & 0x1FF];
|
||||||
if (pml4e & present == 0) return null;
|
if (pml4e & present == 0) return null;
|
||||||
@@ -396,6 +493,8 @@ pub fn translateIn(pml4: u64, virtual: u64) ?u64 {
|
|||||||
if (pdpte & present == 0) return null;
|
if (pdpte & present == 0) return null;
|
||||||
const pde = tableAt(pdpte & address_mask)[(virtual >> 21) & 0x1FF];
|
const pde = tableAt(pdpte & address_mask)[(virtual >> 21) & 0x1FF];
|
||||||
if (pde & present == 0) return null;
|
if (pde & present == 0) return null;
|
||||||
|
if (pde & page_size_bit != 0) // 2 MiB huge leaf: frame base is bits 51:21
|
||||||
|
return (pde & address_mask & ~@as(u64, huge_page_size - 1)) | (virtual & (huge_page_size - 1));
|
||||||
const pte = tableAt(pde & address_mask)[(virtual >> 12) & 0x1FF];
|
const pte = tableAt(pde & address_mask)[(virtual >> 12) & 0x1FF];
|
||||||
if (pte & present == 0) return null;
|
if (pte & present == 0) return null;
|
||||||
return (pte & address_mask) | (virtual & (page_size - 1));
|
return (pte & address_mask) | (virtual & (page_size - 1));
|
||||||
@@ -409,6 +508,5 @@ fn invalidate(virtual: u64) void {
|
|||||||
\\invlpg (%%rax)
|
\\invlpg (%%rax)
|
||||||
:
|
:
|
||||||
: [v] "r" (virtual),
|
: [v] "r" (virtual),
|
||||||
: .{ .rax = true, .memory = true }
|
: .{ .rax = true, .memory = true });
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ const Access = enum { port, mmio };
|
|||||||
var access: Access = .port;
|
var access: Access = .port;
|
||||||
var base: u64 = 0x3F8; // COM1
|
var base: u64 = 0x3F8; // COM1
|
||||||
|
|
||||||
|
/// Whether `init`/`reconfigure` found a *working* UART at `base`. False on a
|
||||||
|
/// legacy-free machine whose COM1 is decoded but dead: writing to it is then a
|
||||||
|
/// no-op, so `write` never spins waiting for a transmit register that will never
|
||||||
|
/// drain. Cleared until proven by the loopback probe.
|
||||||
|
var uart_present: bool = false;
|
||||||
|
|
||||||
fn portOut(p: u16, value: u8) void {
|
fn portOut(p: u16, value: u8) void {
|
||||||
asm volatile ("outb %[value], %[p]"
|
asm volatile ("outb %[value], %[p]"
|
||||||
:
|
:
|
||||||
@@ -57,6 +63,34 @@ pub fn init() void {
|
|||||||
setRegister(3, 0x03); // 8 bits, no parity, one stop bit; DLAB off
|
setRegister(3, 0x03); // 8 bits, no parity, one stop bit; DLAB off
|
||||||
setRegister(2, 0xC7); // enable + clear FIFO, 14-byte threshold
|
setRegister(2, 0xC7); // enable + clear FIFO, 14-byte threshold
|
||||||
setRegister(4, 0x0B); // RTS/DSR set
|
setRegister(4, 0x0B); // RTS/DSR set
|
||||||
|
uart_present = probe();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect a *working* UART by internal loopback: route the transmitter back to
|
||||||
|
/// the receiver (MCR bit 4), send a byte, and check it comes back. A port that is
|
||||||
|
/// merely decoded but has nothing behind it (the common case on a legacy-free
|
||||||
|
/// board that still answers I/O at 0x3F8) never echoes, so this returns false.
|
||||||
|
///
|
||||||
|
/// This matters for speed, not just correctness: a dead UART's line-status
|
||||||
|
/// register reads back 0x00, so its transmit-holding-empty bit never sets, and
|
||||||
|
/// `writeByte` would otherwise spin its full guard — tens of milliseconds — on
|
||||||
|
/// *every* logged byte. On real hardware that alone can add ~a minute to boot.
|
||||||
|
fn probe() bool {
|
||||||
|
const saved_mcr = register(4);
|
||||||
|
setRegister(4, 0x1E); // MCR: LOOP | OUT2 | OUT1 | RTS — internal loopback
|
||||||
|
setRegister(0, 0xAE); // push a distinctive byte into the loopback path
|
||||||
|
var guard: u32 = 0;
|
||||||
|
while (register(5) & 0x01 == 0 and guard < 10_000) : (guard += 1) {} // await Data Ready
|
||||||
|
const echo = register(0);
|
||||||
|
setRegister(4, saved_mcr); // restore the modem-control lines
|
||||||
|
return echo == 0xAE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a working UART was detected (see `probe`). The log sink stays
|
||||||
|
/// registered regardless — it simply does nothing until this is true — so a UART
|
||||||
|
/// that only `reconfigure` discovers (via SPCR) still starts logging.
|
||||||
|
pub fn present() bool {
|
||||||
|
return uart_present;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Point the console at the UART ACPI's SPCR table names (MMIO or I/O port) and
|
/// Point the console at the UART ACPI's SPCR table names (MMIO or I/O port) and
|
||||||
@@ -70,15 +104,19 @@ pub fn reconfigure(is_mmio: bool, address: u64) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn writeByte(c: u8) void {
|
fn writeByte(c: u8) void {
|
||||||
// Wait for the transmit-holding register to empty — but bounded, so an absent
|
// Wait for the transmit-holding register to empty. `write` only reaches here
|
||||||
// UART (whose line-status register reads back as 0x00) can't hang the kernel.
|
// for a UART the loopback probe proved live, so this bounds a momentary stall
|
||||||
|
// (e.g. deasserted flow control), not an absent port: ~5000 legacy-port reads
|
||||||
|
// is a few ms — comfortably longer than one 38400-baud byte-time (~260 µs).
|
||||||
var guard: u32 = 0;
|
var guard: u32 = 0;
|
||||||
while (register(5) & 0x20 == 0 and guard < 100_000) : (guard += 1) {}
|
while (register(5) & 0x20 == 0 and guard < 5_000) : (guard += 1) {}
|
||||||
setRegister(0, c);
|
setRegister(0, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write bytes, translating LF to CRLF so terminals and logs line up.
|
/// Write bytes, translating LF to CRLF so terminals and logs line up. A no-op
|
||||||
|
/// when no working UART was detected, so a dead COM1 costs nothing per byte.
|
||||||
pub fn write(bytes: []const u8) void {
|
pub fn write(bytes: []const u8) void {
|
||||||
|
if (!uart_present) return;
|
||||||
for (bytes) |c| {
|
for (bytes) |c| {
|
||||||
if (c == '\n') writeByte('\r');
|
if (c == '\n') writeByte('\r');
|
||||||
writeByte(c);
|
writeByte(c);
|
||||||
|
|||||||
@@ -148,7 +148,14 @@ pub fn startAp(apic_id: u32, stack_top: usize, percpu: usize, index: usize, cr3:
|
|||||||
// Wait up to 100 ms for the AP to reach apEntry and set the flag.
|
// Wait up to 100 ms for the AP to reach apEntry and set the flag.
|
||||||
const deadline = apic.millis() + 100;
|
const deadline = apic.millis() + 100;
|
||||||
while (apic.millis() < deadline) {
|
while (apic.millis() < deadline) {
|
||||||
if (@atomicLoad(u32, &ap_alive, .acquire) != 0) return true;
|
if (@atomicLoad(u32, &ap_alive, .acquire) != 0) {
|
||||||
|
// Cross-check this core's TSC against the BSP's before it joins the run
|
||||||
|
// loop: an unsynchronized TSC must be caught before any task can migrate
|
||||||
|
// onto this core and observe time going backward. No-op unless the TSC is
|
||||||
|
// the clocksource (apic.checkWarpSource).
|
||||||
|
apic.checkWarpSource();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
asm volatile ("pause");
|
asm volatile ("pause");
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -166,6 +173,7 @@ fn delayMicros(us: u64) void {
|
|||||||
/// signals the BSP, then jumps to the generic scheduler entry. Never returns.
|
/// signals the BSP, then jumps to the generic scheduler entry. Never returns.
|
||||||
fn apEntry(percpu: usize) callconv(.c) noreturn {
|
fn apEntry(percpu: usize) callconv(.c) noreturn {
|
||||||
const cpu = boot_index;
|
const cpu = boot_index;
|
||||||
|
paging.setupPat(); // this core's PAT: entry 4 = write-combining, to match the BSP
|
||||||
gdt.loadOnThisCpu(cpu); // this core's GDT (with its own TSS slot)
|
gdt.loadOnThisCpu(cpu); // this core's GDT (with its own TSS slot)
|
||||||
tss.setupThisCpu(cpu); // this core's TSS + IST stack, loaded into TR
|
tss.setupThisCpu(cpu); // this core's TSS + IST stack, loaded into TR
|
||||||
idt.loadOnThisCpu(); // the shared IDT
|
idt.loadOnThisCpu(); // the shared IDT
|
||||||
@@ -177,6 +185,11 @@ fn apEntry(percpu: usize) callconv(.c) noreturn {
|
|||||||
|
|
||||||
@atomicStore(u32, &ap_alive, 1, .release); // "architecture state up" — BSP is polling this
|
@atomicStore(u32, &ap_alive, 1, .release); // "architecture state up" — BSP is polling this
|
||||||
|
|
||||||
|
// Rendezvous with the BSP for the TSC warp check (no-op unless the TSC is the
|
||||||
|
// clocksource) before joining the run loop, so this core's clock is vetted before
|
||||||
|
// it can run any task.
|
||||||
|
apic.checkWarpTarget();
|
||||||
|
|
||||||
if (secondary_entry) |enterScheduler| enterScheduler(); // joins the run loop
|
if (secondary_entry) |enterScheduler| enterScheduler(); // joins the run loop
|
||||||
while (true) asm volatile ("hlt"); // (only if no entry was registered)
|
while (true) asm volatile ("hlt"); // (only if no entry was registered)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -154,5 +167,3 @@ pub const Console = struct {
|
|||||||
while (x < self.fb.width) : (x += 1) destination[x] = source[x];
|
while (x < self.fb.width) : (x += 1) destination[x] = source[x];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -66,6 +116,7 @@ fn record(node: *platform.Device, parent_id: u64) u64 {
|
|||||||
d.id = count;
|
d.id = count;
|
||||||
d.parent = parent_id;
|
d.parent = parent_id;
|
||||||
d.class = @intFromEnum(node.class);
|
d.class = @intFromEnum(node.class);
|
||||||
|
d.pci_class = if (node.ids.pci_class) |code| code else device_abi.no_pci_class;
|
||||||
const h = node.hid();
|
const h = node.hid();
|
||||||
d.hid_len = @min(h.len, d.hid.len);
|
d.hid_len = @min(h.len, d.hid.len);
|
||||||
@memcpy(d.hid[0..d.hid_len], h[0..d.hid_len]);
|
@memcpy(d.hid[0..d.hid_len], h[0..d.hid_len]);
|
||||||
@@ -103,6 +154,19 @@ pub fn ownerOf(id: u64) ?u32 {
|
|||||||
return claimed[@intCast(id)];
|
return claimed[@intCast(id)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Release every claim held by `owner` — called by the process layer on every
|
||||||
|
/// path out of a process (exit, fault, kill), so a restarted driver can claim its
|
||||||
|
/// hardware again (docs/process-lifecycle.md iron rule 1: cleanup is the kernel's
|
||||||
|
/// job). The devices stay in the table — they describe hardware, which did not go
|
||||||
|
/// away — only their ownership clears.
|
||||||
|
pub fn releaseAllOwnedBy(owner: u32) void {
|
||||||
|
for (claimed[0..count]) |*slot| {
|
||||||
|
if (slot.*) |o| {
|
||||||
|
if (o == owner) slot.* = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Resource `index` of device `id`, or null if out of range.
|
/// Resource `index` of device `id`, or null if out of range.
|
||||||
pub fn resourceOf(id: u64, index: u64) ?device_abi.ResourceDescriptor {
|
pub fn resourceOf(id: u64, index: u64) ?device_abi.ResourceDescriptor {
|
||||||
if (id >= count) return null;
|
if (id >= count) return null;
|
||||||
@@ -117,7 +181,16 @@ pub fn resourceOf(id: u64, index: u64) ?device_abi.ResourceDescriptor {
|
|||||||
/// and would otherwise vacuously "fit" anywhere.
|
/// and would otherwise vacuously "fit" anywhere.
|
||||||
fn contains(parent: device_abi.ResourceDescriptor, child: device_abi.ResourceDescriptor) bool {
|
fn contains(parent: device_abi.ResourceDescriptor, child: device_abi.ResourceDescriptor) bool {
|
||||||
if (parent.kind != child.kind) return false;
|
if (parent.kind != child.kind) return false;
|
||||||
if (child.kind == @intFromEnum(device_abi.ResourceKind.irq)) return parent.start == child.start;
|
if (child.kind == @intFromEnum(device_abi.ResourceKind.irq)) {
|
||||||
|
// Range containment: an interrupt line is still indivisible (a child owns
|
||||||
|
// exactly one GSI), but a parent may own a *range* of lines so a broad
|
||||||
|
// owner — the acpi-tables node, whose firmware names any legacy IRQ —
|
||||||
|
// can contain its children's specific lines. A length-1 parent range is
|
||||||
|
// exactly the old equality rule, so existing single-IRQ parents are
|
||||||
|
// unaffected.
|
||||||
|
const span = if (parent.len == 0) 1 else parent.len;
|
||||||
|
return child.start >= parent.start and child.start < parent.start + span;
|
||||||
|
}
|
||||||
if (child.len == 0 or parent.len == 0) return false;
|
if (child.len == 0 or parent.len == 0) return false;
|
||||||
// No overflow: a resource that wraps the address space is not containable.
|
// No overflow: a resource that wraps the address space is not containable.
|
||||||
const child_end = std.math.add(u64, child.start, child.len) catch return false;
|
const child_end = std.math.add(u64, child.start, child.len) catch return false;
|
||||||
@@ -166,10 +239,31 @@ pub fn register(parent_id: u64, owner: u32, descriptor: *const device_abi.Device
|
|||||||
if (!ok) return error.NotContained;
|
if (!ok) return error.NotContained;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Idempotent on exact match (docs/device-manager.md): a restarted
|
||||||
|
// registering bus re-registers what it rediscovers, and the table has no
|
||||||
|
// unregister — an identical (class, identity, resources) child under the
|
||||||
|
// same parent returns the existing id instead of appending a duplicate.
|
||||||
|
for (devices[0..count]) |*existing| {
|
||||||
|
if (existing.parent != parent_id) continue;
|
||||||
|
if (existing.class != descriptor.class) continue;
|
||||||
|
if (existing.pci_class != descriptor.pci_class) continue;
|
||||||
|
if (existing.hid_len != descriptor.hid_len) continue;
|
||||||
|
if (!std.mem.eql(u8, existing.hid[0..@intCast(existing.hid_len)], descriptor.hid[0..@intCast(descriptor.hid_len)])) continue;
|
||||||
|
if (existing.resource_count != descriptor.resource_count) continue;
|
||||||
|
var same = true;
|
||||||
|
for (0..@intCast(descriptor.resource_count)) |i| {
|
||||||
|
const a = existing.resources[i];
|
||||||
|
const b = descriptor.resources[i];
|
||||||
|
if (a.kind != b.kind or a.start != b.start or a.len != b.len) same = false;
|
||||||
|
}
|
||||||
|
if (same) return existing.id;
|
||||||
|
}
|
||||||
|
|
||||||
var d = std.mem.zeroes(device_abi.DeviceDescriptor);
|
var d = std.mem.zeroes(device_abi.DeviceDescriptor);
|
||||||
d.id = count;
|
d.id = count;
|
||||||
d.parent = parent_id;
|
d.parent = parent_id;
|
||||||
d.class = descriptor.class;
|
d.class = descriptor.class;
|
||||||
|
d.pci_class = descriptor.pci_class;
|
||||||
d.hid_len = @min(descriptor.hid_len, d.hid.len);
|
d.hid_len = @min(descriptor.hid_len, d.hid.len);
|
||||||
@memcpy(d.hid[0..@intCast(d.hid_len)], descriptor.hid[0..@intCast(d.hid_len)]);
|
@memcpy(d.hid[0..@intCast(d.hid_len)], descriptor.hid[0..@intCast(d.hid_len)]);
|
||||||
d.resource_count = descriptor.resource_count;
|
d.resource_count = descriptor.resource_count;
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ const Task = scheduler.Task;
|
|||||||
pub const MESSAGE_MAXIMUM: usize = 256;
|
pub const MESSAGE_MAXIMUM: usize = 256;
|
||||||
|
|
||||||
pub const maximum_handles = scheduler.ipc_maximum_handles;
|
pub const maximum_handles = scheduler.ipc_maximum_handles;
|
||||||
pub const maximum_services = 8;
|
// The name registry is indexed directly by ServiceId, so this must exceed the
|
||||||
|
// largest id (currently fat = 8). Sized with headroom for new services.
|
||||||
|
pub const maximum_services = 16;
|
||||||
|
|
||||||
/// Errno-style failures, returned as `-value` in the system_call result register.
|
/// Errno-style failures, returned as `-value` in the system_call result register.
|
||||||
pub const EBADF: i64 = 1; // bad handle
|
pub const EBADF: i64 = 1; // bad handle
|
||||||
@@ -57,6 +59,29 @@ pub const EPERM: i64 = 9; // not permitted (process_kill by anyone but the super
|
|||||||
/// shared kernel↔user ABI (system/abi.zig), because ring 3 has to test the same bit.
|
/// shared kernel↔user ABI (system/abi.zig), because ring 3 has to test the same bit.
|
||||||
pub const notify_badge_bit: u64 = abi.notify_badge_bit;
|
pub const notify_badge_bit: u64 = abi.notify_badge_bit;
|
||||||
|
|
||||||
|
/// Set (with `notify_badge_bit`) when a `replyWait` wake carries a buffered payload
|
||||||
|
/// posted by `send` (`ipc_send`), rather than a bare IRQ/exit notification. Shared with
|
||||||
|
/// ring 3 through the ABI so the receiver can tell "a message arrived" from "the hardware
|
||||||
|
/// spoke".
|
||||||
|
pub const notify_message_bit: u64 = abi.notify_message_bit;
|
||||||
|
|
||||||
|
/// Largest payload a single `send` (`ipc_send`) may post. Kept small — the payload rides
|
||||||
|
/// inline in every `Endpoint`, and the async path is for events (a `KeyEvent` is 16
|
||||||
|
/// bytes), not bulk transfer, which is what `call` and future shared pages are for.
|
||||||
|
pub const POST_MAXIMUM: usize = 64;
|
||||||
|
|
||||||
|
/// Depth of an endpoint's async payload ring. Absorbs a burst while a receiver is briefly
|
||||||
|
/// busy; a full ring drops the *oldest* message (see `send`).
|
||||||
|
const post_capacity: usize = 16;
|
||||||
|
|
||||||
|
/// One buffered message: a length-prefixed payload plus the sender's task id (delivered
|
||||||
|
/// in the low bits of the receiver's badge).
|
||||||
|
const PostSlot = struct {
|
||||||
|
length: u16 = 0,
|
||||||
|
sender_id: u64 = 0,
|
||||||
|
bytes: [POST_MAXIMUM]u8 = undefined,
|
||||||
|
};
|
||||||
|
|
||||||
/// End of the user (low) canonical half — user buffers must lie below it.
|
/// End of the user (low) canonical half — user buffers must lie below it.
|
||||||
const user_half_end: u64 = 0x0000_8000_0000_0000;
|
const user_half_end: u64 = 0x0000_8000_0000_0000;
|
||||||
|
|
||||||
@@ -74,6 +99,12 @@ pub const Endpoint = struct {
|
|||||||
notify_buffer: [8]u64 = undefined,
|
notify_buffer: [8]u64 = undefined,
|
||||||
notify_head: u8 = 0,
|
notify_head: u8 = 0,
|
||||||
notify_tail: u8 = 0,
|
notify_tail: u8 = 0,
|
||||||
|
// Pending buffered messages (payloads posted by `send`), a small FIFO ring. Unlike
|
||||||
|
// notifications — which are a level and coalesce — these are discrete messages, so a
|
||||||
|
// full ring drops the oldest rather than merging.
|
||||||
|
post_buffer: [post_capacity]PostSlot = undefined,
|
||||||
|
post_head: u16 = 0,
|
||||||
|
post_tail: u16 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn createIpcEndpoint() ?*Endpoint {
|
pub fn createIpcEndpoint() ?*Endpoint {
|
||||||
@@ -265,12 +296,23 @@ pub fn replyWait(endpoint: *Endpoint, reply_ptr: u64, reply_len: u64, receive_pt
|
|||||||
scheduler.readyLocked(client); // its `call` now returns
|
scheduler.readyLocked(client); // its `call` now returns
|
||||||
}
|
}
|
||||||
|
|
||||||
// (2) Receive the next request (or notification), blocking until one is ready.
|
// (2) Receive the next request (or notification / buffered message), blocking until
|
||||||
|
// one is ready. Bare notifications (IRQ/exit) come first — they're latency-sensitive
|
||||||
|
// and carry no payload — then buffered messages, then synchronous client requests.
|
||||||
while (true) {
|
while (true) {
|
||||||
if (popNotify(endpoint)) |badge| {
|
if (popNotify(endpoint)) |badge| {
|
||||||
out_badge.* = badge | notify_badge_bit;
|
out_badge.* = badge | notify_badge_bit;
|
||||||
return 0; // notification: no payload, no reply owed, no cap
|
return 0; // notification: no payload, no reply owed, no cap
|
||||||
}
|
}
|
||||||
|
if (popPost(endpoint)) |slot| {
|
||||||
|
const n = @min(@as(usize, slot.length), receive_cap);
|
||||||
|
// Copy from the kernel-resident ring slot (source aspace 0) into the receiver.
|
||||||
|
if (!copyAcross(0, @intFromPtr(&slot.bytes), me.aspace, receive_ptr, n)) {
|
||||||
|
continue; // bad receive buffer: drop this message, keep serving
|
||||||
|
}
|
||||||
|
out_badge.* = slot.sender_id | notify_badge_bit | notify_message_bit;
|
||||||
|
return @intCast(n); // async message: payload delivered, no reply owed, no cap
|
||||||
|
}
|
||||||
if (dequeueSender(endpoint)) |caller| {
|
if (dequeueSender(endpoint)) |caller| {
|
||||||
const n = @min(caller.ipc_send_len, receive_cap);
|
const n = @min(caller.ipc_send_len, receive_cap);
|
||||||
if (!copyAcross(caller.aspace, caller.ipc_send_ptr, me.aspace, receive_ptr, n)) {
|
if (!copyAcross(caller.aspace, caller.ipc_send_ptr, me.aspace, receive_ptr, n)) {
|
||||||
@@ -306,6 +348,44 @@ fn popNotify(endpoint: *Endpoint) ?u64 {
|
|||||||
return badge;
|
return badge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Take the oldest buffered message from the post ring, or null if empty. Returns a
|
||||||
|
/// pointer into the endpoint's own storage — valid until the next `send`/`popPost` under
|
||||||
|
/// the same lock region, which is all the copy-out in `replyWait` needs.
|
||||||
|
fn popPost(endpoint: *Endpoint) ?*const PostSlot {
|
||||||
|
if (endpoint.post_head == endpoint.post_tail) return null;
|
||||||
|
const slot = &endpoint.post_buffer[endpoint.post_head % post_capacity];
|
||||||
|
endpoint.post_head +%= 1;
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Client-free side of async IPC (`ipc_send`): copy `[source_va, len)` from address space
|
||||||
|
/// `source_as` into `endpoint`'s post ring and wake a waiting receiver — **without
|
||||||
|
/// blocking the sender** and with no reply owed. `sender_id` rides along, delivered in the
|
||||||
|
/// low bits of the receiver's badge. Returns 0, or a negative errno (`-E2BIG` if the
|
||||||
|
/// payload exceeds `POST_MAXIMUM`, `-EFAULT` if the source buffer is unmapped / out of the
|
||||||
|
/// user half). A full ring drops the *oldest* message (advancing `post_head`), because a
|
||||||
|
/// buffered message is discrete, not a level: keeping the newest keeps input responsive.
|
||||||
|
/// Precondition: the big kernel lock is held.
|
||||||
|
pub fn sendLocked(endpoint: *Endpoint, source_as: u64, source_va: u64, len: u64, sender_id: u64) i64 {
|
||||||
|
if (len > POST_MAXIMUM) return -E2BIG;
|
||||||
|
// Drop the oldest if the ring is full, so this newest message always lands.
|
||||||
|
if (endpoint.post_tail -% endpoint.post_head >= post_capacity) endpoint.post_head +%= 1;
|
||||||
|
const slot = &endpoint.post_buffer[endpoint.post_tail % post_capacity];
|
||||||
|
if (!copyFromUser(source_as, source_va, slot.bytes[0..@intCast(len)])) return -EFAULT;
|
||||||
|
slot.length = @intCast(len);
|
||||||
|
slot.sender_id = sender_id;
|
||||||
|
endpoint.post_tail +%= 1;
|
||||||
|
scheduler.wakeLocked(&endpoint.receive_wait_queue);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `sendLocked` wrapped in its own critical section, for the `ipc_send` syscall path.
|
||||||
|
pub fn send(endpoint: *Endpoint, source_as: u64, source_va: u64, len: u64, sender_id: u64) i64 {
|
||||||
|
const flags = sync.enter();
|
||||||
|
defer sync.leave(flags);
|
||||||
|
return sendLocked(endpoint, source_as, source_va, len, sender_id);
|
||||||
|
}
|
||||||
|
|
||||||
/// Post an asynchronous notification carrying `badge` to `endpoint` and wake a waiting
|
/// Post an asynchronous notification carrying `badge` to `endpoint` and wake a waiting
|
||||||
/// receiver. Precondition: the big kernel lock is held.
|
/// receiver. Precondition: the big kernel lock is held.
|
||||||
///
|
///
|
||||||
|
|||||||
+108
-40
@@ -5,6 +5,7 @@ const parameters = @import("parameters");
|
|||||||
const architecture = @import("architecture");
|
const architecture = @import("architecture");
|
||||||
const console = @import("console.zig");
|
const console = @import("console.zig");
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
|
const wall_clock = @import("wall-clock.zig");
|
||||||
const pmm = @import("pmm.zig");
|
const pmm = @import("pmm.zig");
|
||||||
const heap = @import("heap.zig");
|
const heap = @import("heap.zig");
|
||||||
const scheduler = @import("scheduler.zig");
|
const scheduler = @import("scheduler.zig");
|
||||||
@@ -59,16 +60,34 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
// file on a ramdisk/USB/SSD), so a message survives as long as any is present.
|
// file on a ramdisk/USB/SSD), so a message survives as long as any is present.
|
||||||
// A headless, serial-less machine still boots correctly — it just goes quiet,
|
// A headless, serial-less machine still boots correctly — it just goes quiet,
|
||||||
// with port-0x80 checkpoints as the only progress signal.
|
// with port-0x80 checkpoints as the only progress signal.
|
||||||
|
//
|
||||||
|
// Serial is compiled in only under -Dserial (build.zig): a real machine often
|
||||||
|
// has no live legacy COM1, and the log survives in the RAM buffer (below) and
|
||||||
|
// is flushed to disk — so serial is now a QEMU/dev convenience the flashable
|
||||||
|
// image leaves out. When it *is* built in, `serialInit`'s loopback probe still
|
||||||
|
// guards against a dead port (so a -Dserial image is safe on real hardware).
|
||||||
|
if (build_options.serial) {
|
||||||
architecture.serialInit();
|
architecture.serialInit();
|
||||||
log.addSink(architecture.serialWrite);
|
log.addSink(architecture.serialWrite);
|
||||||
|
}
|
||||||
if (architecture.debugconPresent()) log.addSink(architecture.debugconWrite);
|
if (architecture.debugconPresent()) log.addSink(architecture.debugconWrite);
|
||||||
|
// Retain the whole stream in a RAM buffer too, so a user program can later
|
||||||
|
// read it back (klog_read) and persist the boot log to disk — the only way to
|
||||||
|
// see it on a headless/real machine with no host capturing serial.
|
||||||
|
log.addSink(log.ramSink);
|
||||||
|
|
||||||
// The **framebuffer** is deliberately *not* a log sink. It's a separate output
|
// The **framebuffer** is deliberately *not* a log sink. It's a separate output
|
||||||
// surface — a bootstrap text console today, a graphics device driver later — so
|
// surface — a bootstrap text console today, a graphics device driver later — so
|
||||||
// we never assume the OS is text-based. Only a few user-facing status lines
|
// we never assume the OS is text-based. Only a few user-facing status lines
|
||||||
// (via `status`) and panics are mirrored to it; the verbose log stays out.
|
// (via `status`) and panics are mirrored to it; the verbose log stays out.
|
||||||
|
//
|
||||||
|
// The console is brought up *after* paging (below), not here: its one-time
|
||||||
|
// full-screen clear then runs on the kernel's **write-combining** mapping of the
|
||||||
|
// framebuffer instead of the loader's uncached one — a fast burst rather than
|
||||||
|
// millions of uncached writes on real hardware. Until then, on-screen output is
|
||||||
|
// absent (an early panic still lands in the serial/RAM log); the trade is worth
|
||||||
|
// a near-instant boot. `console.write` is a safe no-op while the console is down.
|
||||||
const fb = boot_information.framebuffer;
|
const fb = boot_information.framebuffer;
|
||||||
console.init(fb);
|
|
||||||
|
|
||||||
log.checkpoint(cp_entry);
|
log.checkpoint(cp_entry);
|
||||||
|
|
||||||
@@ -77,12 +96,12 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
architecture.setFaultHandler(onException);
|
architecture.setFaultHandler(onException);
|
||||||
architecture.init();
|
architecture.init();
|
||||||
|
|
||||||
status("danos: initialising kernel...\n");
|
status("/system/kernel: initialising kernel...\n");
|
||||||
log.write(if (console.present())
|
if (build_options.serial) log.write(if (architecture.serialPresent())
|
||||||
"danos: framebuffer console online (bootstrap; graphics driver later)\n"
|
"/system/kernel: serial console online (COM1)\n"
|
||||||
else
|
else
|
||||||
"danos: no framebuffer (headless) -> logging to serial/debugcon only\n");
|
"/system/kernel: no serial UART (COM1 absent) -> log kept in RAM/debugcon\n");
|
||||||
log.write("danos: cpu tables online (GDT, IDT, TSS)\n");
|
log.write("/system/kernel: cpu tables online (GDT, IDT, TSS)\n");
|
||||||
log.print(" resolution : {d}x{d}\n", .{ fb.width, fb.height });
|
log.print(" resolution : {d}x{d}\n", .{ fb.width, fb.height });
|
||||||
log.print(" pitch : {d} bytes\n", .{fb.pitch});
|
log.print(" pitch : {d} bytes\n", .{fb.pitch});
|
||||||
log.print(" format : {s}\n", .{@tagName(fb.format)});
|
log.print(" format : {s}\n", .{@tagName(fb.format)});
|
||||||
@@ -105,7 +124,7 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
const total_bytes = total_pages * abi.page_size;
|
const total_bytes = total_pages * abi.page_size;
|
||||||
const gib = 1 << 30;
|
const gib = 1 << 30;
|
||||||
|
|
||||||
log.write("\ndanos: physical memory\n");
|
log.write("\n/system/kernel: physical memory\n");
|
||||||
log.print(" total RAM : {d}.{d:0>2} GiB ({d} MiB) - RAM the firmware reported\n", .{ total_bytes / gib, (total_bytes % gib) * 100 / gib, mib(total_pages) });
|
log.print(" total RAM : {d}.{d:0>2} GiB ({d} MiB) - RAM the firmware reported\n", .{ total_bytes / gib, (total_bytes % gib) * 100 / gib, mib(total_pages) });
|
||||||
log.print(" usable : {d} MiB - free RAM (incl. reclaimed boot-services memory)\n", .{mib(usable_pages)});
|
log.print(" usable : {d} MiB - free RAM (incl. reclaimed boot-services memory)\n", .{mib(usable_pages)});
|
||||||
log.print(" reserved : {d} MiB - kernel image, boot stack, ACPI, runtime services\n", .{mib(reserved_pages)});
|
log.print(" reserved : {d} MiB - kernel image, boot stack, ACPI, runtime services\n", .{mib(reserved_pages)});
|
||||||
@@ -119,7 +138,7 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
// until SMP bring-up; 0 means none was available (we stay uniprocessor).
|
// until SMP bring-up; 0 means none was available (we stay uniprocessor).
|
||||||
ap_trampoline_page = pmm.allocBelow(0x100000) orelse 0;
|
ap_trampoline_page = pmm.allocBelow(0x100000) orelse 0;
|
||||||
const s1 = pmm.stats();
|
const s1 = pmm.stats();
|
||||||
log.print("\ndanos: frame allocator online\n", .{});
|
log.print("\n/system/kernel: frame allocator online\n", .{});
|
||||||
log.print(" free frames: {d} ({d} MiB)\n", .{ s1.free_frames, mib(s1.free_frames) });
|
log.print(" free frames: {d} ({d} MiB)\n", .{ s1.free_frames, mib(s1.free_frames) });
|
||||||
const f0 = pmm.alloc();
|
const f0 = pmm.alloc();
|
||||||
const f1 = pmm.alloc();
|
const f1 = pmm.alloc();
|
||||||
@@ -133,14 +152,23 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
// Switch off the firmware's page tables onto our own (with real permissions).
|
// Switch off the firmware's page tables onto our own (with real permissions).
|
||||||
architecture.enablePaging(pmm.alloc, pmm.free, boot_information);
|
architecture.enablePaging(pmm.alloc, pmm.free, boot_information);
|
||||||
log.checkpoint(cp_paging);
|
log.checkpoint(cp_paging);
|
||||||
log.print("\ndanos: paging enabled\n", .{});
|
log.print("\n/system/kernel: paging enabled\n", .{});
|
||||||
log.print(" page tables: root = 0x{x:0>16}\n", .{architecture.activePageTable()});
|
log.print(" page tables: root = 0x{x:0>16}\n", .{architecture.activePageTable()});
|
||||||
log.print(" kernel segs: {d} (mapped with W^X permissions)\n", .{boot_information.kernel_segment_count});
|
log.print(" kernel segs: {d} (mapped with W^X permissions)\n", .{boot_information.kernel_segment_count});
|
||||||
|
|
||||||
|
// Now on our own tables, the framebuffer window is write-combining: bring up
|
||||||
|
// the on-screen console and clear it (a fast burst here, not the loader's
|
||||||
|
// uncached crawl). From here `status` reaches the screen as well as the log.
|
||||||
|
console.init(fb);
|
||||||
|
log.write(if (console.present())
|
||||||
|
"/system/kernel: framebuffer console online (bootstrap; graphics driver later)\n"
|
||||||
|
else
|
||||||
|
"/system/kernel: no framebuffer (headless) -> logging to serial/debugcon only\n");
|
||||||
|
|
||||||
// Bring up the kernel heap (dynamic allocation), built on the VMM.
|
// Bring up the kernel heap (dynamic allocation), built on the VMM.
|
||||||
heap.init();
|
heap.init();
|
||||||
log.checkpoint(cp_heap);
|
log.checkpoint(cp_heap);
|
||||||
log.write("\ndanos: kernel heap online\n");
|
log.write("\n/system/kernel: kernel heap online\n");
|
||||||
// Measure the amount of resources the kernel is actually using
|
// Measure the amount of resources the kernel is actually using
|
||||||
const s2 = pmm.stats();
|
const s2 = pmm.stats();
|
||||||
log.print(" Kernel footprint: {d} KiB\n", .{kib(s1.free_frames - s2.free_frames)});
|
log.print(" Kernel footprint: {d} KiB\n", .{kib(s1.free_frames - s2.free_frames)});
|
||||||
@@ -156,7 +184,7 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
};
|
};
|
||||||
if (platform.discover(boot_information, heap.allocator(), hal)) |devtree| {
|
if (platform.discover(boot_information, heap.allocator(), hal)) |devtree| {
|
||||||
var device_tree = devtree;
|
var device_tree = devtree;
|
||||||
log.write("\ndanos: device discovery online\n");
|
log.write("\n/system/kernel: device discovery online\n");
|
||||||
device_tree.dump(log.write);
|
device_tree.dump(log.write);
|
||||||
|
|
||||||
// Snapshot the device tree for user-space drivers (device_enumerate/claim/
|
// Snapshot the device tree for user-space drivers (device_enumerate/claim/
|
||||||
@@ -164,28 +192,27 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
devices_broker.init(&device_tree);
|
devices_broker.init(&device_tree);
|
||||||
if (devices_broker.dropped > 0) {
|
if (devices_broker.dropped > 0) {
|
||||||
// Otherwise entirely silent: drivers would just never see that hardware.
|
// Otherwise entirely silent: drivers would just never see that hardware.
|
||||||
log.print("danos: 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();
|
||||||
|
|
||||||
// Power register map extracted from the FADT + AML, for confidence it parsed.
|
// Power register map, from the FADT (the SLP_TYP sleep values live in AML,
|
||||||
|
// which the kernel doesn't parse — the ring-3 acpi service owns soft-off).
|
||||||
const pw = platform.powerInformation();
|
const pw = platform.powerInformation();
|
||||||
log.write("danos: power\n");
|
log.write("/system/kernel: power\n");
|
||||||
log.print(" pm1a_cnt : {s} 0x{x} (width {d})\n", .{ if (pw.pm1a_cnt.mmio) "mmio" else "io", pw.pm1a_cnt.address, pw.pm1a_cnt.width });
|
log.print(" pm1a_cnt : {s} 0x{x} (width {d})\n", .{ if (pw.pm1a_cnt.mmio) "mmio" else "io", pw.pm1a_cnt.address, pw.pm1a_cnt.width });
|
||||||
if (pw.s5) |s| {
|
|
||||||
log.print(" S5 slp_typ : a={d} b={d}\n", .{ s.slp_typ_a, s.slp_typ_b });
|
|
||||||
} else {
|
|
||||||
log.write(" S5 slp_typ : (not found)\n");
|
|
||||||
}
|
|
||||||
log.print(" reset : supported={} {s} 0x{x} val 0x{x}\n", .{ pw.reset_supported, if (pw.reset.mmio) "mmio" else "io", pw.reset.address, pw.reset_value });
|
log.print(" reset : supported={} {s} 0x{x} val 0x{x}\n", .{ pw.reset_supported, if (pw.reset.mmio) "mmio" else "io", pw.reset.address, pw.reset_value });
|
||||||
|
|
||||||
// AML namespace parse integrity: consumed should equal total.
|
|
||||||
const am = platform.amlStats();
|
|
||||||
log.print(" aml : {d} namespace nodes, parsed {d}/{d} bytes\n", .{ am.nodes, am.consumed, am.total });
|
|
||||||
|
|
||||||
// Feed the architecture layer the discovered addresses/facts so it makes no legacy
|
// Feed the architecture layer the discovered addresses/facts so it makes no legacy
|
||||||
// assumptions — the point of all this on UEFI Class 3 firmware. MMIO bases
|
// assumptions — the point of all this on UEFI Class 3 firmware. MMIO bases
|
||||||
// (HPET, I/O APIC) come from the device tree; scalar facts from ACPI.
|
// (HPET, I/O APIC) come from the device tree; scalar facts from ACPI.
|
||||||
@@ -221,7 +248,7 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
});
|
});
|
||||||
if (pinfo.spcr_uart) |u| architecture.serialReconfigure(u.mmio, u.address);
|
if (pinfo.spcr_uart) |u| architecture.serialReconfigure(u.mmio, u.address);
|
||||||
|
|
||||||
log.write("danos: platform\n");
|
log.write("/system/kernel: platform\n");
|
||||||
log.print(" 8259 PIC : {s}\n", .{if (pinfo.pic_present) "present" else "absent"});
|
log.print(" 8259 PIC : {s}\n", .{if (pinfo.pic_present) "present" else "absent"});
|
||||||
log.print(" lapic base : 0x{x}\n", .{pinfo.lapic_base});
|
log.print(" lapic base : 0x{x}\n", .{pinfo.lapic_base});
|
||||||
log.print(" hpet base : 0x{x}\n", .{hpet_base});
|
log.print(" hpet base : 0x{x}\n", .{hpet_base});
|
||||||
@@ -237,7 +264,7 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
if (platform.cpusDropped() > 0)
|
if (platform.cpusDropped() > 0)
|
||||||
log.print(" cpus : WARNING {d} core(s) beyond pool cap dropped\n", .{platform.cpusDropped()});
|
log.print(" cpus : WARNING {d} core(s) beyond pool cap dropped\n", .{platform.cpusDropped()});
|
||||||
} else |err| {
|
} else |err| {
|
||||||
log.print("\ndanos: device discovery failed: {s}\n", .{@errorName(err)});
|
log.print("\n/system/kernel: device discovery failed: {s}\n", .{@errorName(err)});
|
||||||
}
|
}
|
||||||
log.checkpoint(cp_discovery);
|
log.checkpoint(cp_discovery);
|
||||||
|
|
||||||
@@ -248,19 +275,43 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
// Register the current context as the first task before enabling preemption.
|
// Register the current context as the first task before enabling preemption.
|
||||||
scheduler.init(4);
|
scheduler.init(4);
|
||||||
log.checkpoint(cp_scheduler);
|
log.checkpoint(cp_scheduler);
|
||||||
log.write("\ndanos: scheduler online\n");
|
log.write("\n/system/kernel: scheduler online\n");
|
||||||
|
|
||||||
// Start the timer and unmask interrupts — the kernel now has a heartbeat, and
|
// Start the timer and unmask interrupts — the kernel now has a heartbeat, and
|
||||||
// the timer preempts among tasks.
|
// the timer preempts among tasks.
|
||||||
architecture.startTimer();
|
architecture.startTimer();
|
||||||
architecture.enableInterrupts();
|
architecture.enableInterrupts();
|
||||||
log.checkpoint(cp_timer);
|
log.checkpoint(cp_timer);
|
||||||
log.print("danos: timer online ({d} Hz tick; timer clock {d} MHz, clock {d} MHz; calibrated via {s})\n", .{ architecture.timer_hz, architecture.timerClockHz() / 1_000_000, architecture.clockHz() / 1_000_000, architecture.timerCalibrationSource() });
|
log.print("/system/kernel: timer online ({d} Hz tick; timer clock {d} MHz, clock {d} MHz; calibrated via {s})\n", .{ architecture.timer_hz, architecture.timerClockHz() / 1_000_000, architecture.clockHz() / 1_000_000, architecture.timerCalibrationSource() });
|
||||||
|
|
||||||
|
// The tsc-sync test forces the TSC clocksource on before the cores come up, so the
|
||||||
|
// TSC + warp-check path is exercised even under TCG (which won't advertise an
|
||||||
|
// invariant TSC). Inert in a normal build (docs/timers.md).
|
||||||
|
if (build_options.test_case) |tc| {
|
||||||
|
if (std.mem.eql(u8, tc, "tsc-sync")) architecture.forceTscClocksourceForTest();
|
||||||
|
}
|
||||||
|
|
||||||
// Wake the other cores (application processors). A no-op on a single-core
|
// Wake the other cores (application processors). A no-op on a single-core
|
||||||
// machine; on SMP each AP climbs to long mode and reports in (docs/smp.md).
|
// machine; on SMP each AP climbs to long mode and reports in (docs/smp.md). The
|
||||||
|
// per-core TSC warp check rides this: each AP is vetted before it joins the run
|
||||||
|
// loop (docs/timers.md).
|
||||||
bringUpSecondaries();
|
bringUpSecondaries();
|
||||||
|
|
||||||
|
// Report the monotonic clock's final reliability, now the warp check has run on
|
||||||
|
// every core. On real Intel/AMD this is the invariant, synchronized TSC; a bare
|
||||||
|
// VM (no invariant bit) or a machine whose cores' TSCs skew uses the HPET instead.
|
||||||
|
log.print("/system/kernel: clocksource {s} (TSC invariant: {s}, synchronized: {s})\n", .{
|
||||||
|
architecture.clockSourceName(),
|
||||||
|
if (architecture.clockInvariant()) "yes" else "no",
|
||||||
|
if (architecture.clockSynchronized()) "yes" else "no",
|
||||||
|
});
|
||||||
|
if (!architecture.clockSynchronized())
|
||||||
|
log.write("/system/kernel: WARNING: per-core TSCs are not synchronized; monotonic clock moved off the TSC\n");
|
||||||
|
|
||||||
|
// Anchor wall-clock time: read the RTC once, now the monotonic clock is final.
|
||||||
|
wall_clock.init();
|
||||||
|
log.print("/system/kernel: wall clock {d} (Unix epoch seconds, UTC, from the RTC)\n", .{wall_clock.nowSeconds()});
|
||||||
|
|
||||||
// In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
|
// In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
|
||||||
// Normal builds fall through to the idle halt.
|
// Normal builds fall through to the idle halt.
|
||||||
if (build_options.test_case) |case| {
|
if (build_options.test_case) |case| {
|
||||||
@@ -269,7 +320,7 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.checkpoint(cp_running);
|
log.checkpoint(cp_running);
|
||||||
status("kernel initialised.\n");
|
status("/system/kernel: initialised.\n");
|
||||||
|
|
||||||
// Publish the initial-ramdisk so user space can `system_spawn` its bundled
|
// Publish the initial-ramdisk so user space can `system_spawn` its bundled
|
||||||
// binaries by name. The kernel no longer launches them itself: init is the
|
// binaries by name. The kernel no longer launches them itself: init is the
|
||||||
@@ -282,10 +333,10 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
// manager then discovers the hardware and spawns each driver. init runs on its own
|
// manager then discovers the hardware and spawns each driver. init runs on its own
|
||||||
// address space, preemptively — this boot context becomes the BSP's idle loop.
|
// address space, preemptively — this boot context becomes the BSP's idle loop.
|
||||||
if (boot_information.init_len != 0) {
|
if (boot_information.init_len != 0) {
|
||||||
status("starting /system/services/init...\n");
|
status("/system/kernel: starting /system/services/init...\n");
|
||||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||||
process.spawnProcess(image, 4, &.{"/system/services/init"}) catch |err| {
|
process.spawnProcess(image, 4, &.{"/system/services/init"}) catch |err| {
|
||||||
statusPrint("/system/services/init failed to load: {s}\n", .{@errorName(err)});
|
statusPrint("/system/kernel: /system/services/init failed to load: {s}\n", .{@errorName(err)});
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
status("no /system/services/init on the boot volume.\n");
|
status("no /system/services/init on the boot volume.\n");
|
||||||
@@ -294,7 +345,7 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
// Become the idle task: drop below every real task and halt until an
|
// Become the idle task: drop below every real task and halt until an
|
||||||
// interrupt. The timer keeps preempting into init and any other work.
|
// interrupt. The timer keeps preempting into init and any other work.
|
||||||
scheduler.setPriority(0);
|
scheduler.setPriority(0);
|
||||||
status("\nkernel idle; user space is running.\n");
|
status("\n/system/kernel: kernel idle; user space is running.\n");
|
||||||
architecture.halt();
|
architecture.halt();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +372,7 @@ fn bringUpSecondaries() void {
|
|||||||
// vector addresses it). It's kept for the system's life — armed only during a
|
// vector addresses it). It's kept for the system's life — armed only during a
|
||||||
// wake, inert (zeroed, non-executable) otherwise — so cores can be re-woken later.
|
// wake, inert (zeroed, non-executable) otherwise — so cores can be re-woken later.
|
||||||
if (ap_trampoline_page == 0) {
|
if (ap_trampoline_page == 0) {
|
||||||
log.write("danos: smp: no low page for the AP trampoline; staying uniprocessor\n");
|
log.write("/system/kernel: smp: no low page for the AP trampoline; staying uniprocessor\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
architecture.setTrampolinePage(ap_trampoline_page);
|
architecture.setTrampolinePage(ap_trampoline_page);
|
||||||
@@ -333,7 +384,7 @@ fn bringUpSecondaries() void {
|
|||||||
if (std.mem.eql(u8, tc, "smp-retry")) architecture.testFailNextWakes(1);
|
if (std.mem.eql(u8, tc, "smp-retry")) architecture.testFailNextWakes(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.print("\ndanos: bringing up {d} application processor(s)\n", .{cores.len - 1});
|
log.print("\n/system/kernel: bringing up {d} application processor(s)\n", .{cores.len - 1});
|
||||||
const maximum_wake_attempts = 3; // a core that misses the first INIT-SIPI-SIPI gets retried
|
const maximum_wake_attempts = 3; // a core that misses the first INIT-SIPI-SIPI gets retried
|
||||||
for (cores[1..], 1..) |core, index| {
|
for (cores[1..], 1..) |core, index| {
|
||||||
const stack = heap.allocator().alloc(u8, parameters.kernel_stack_size) catch {
|
const stack = heap.allocator().alloc(u8, parameters.kernel_stack_size) catch {
|
||||||
@@ -344,7 +395,7 @@ fn bringUpSecondaries() void {
|
|||||||
// This core's dedicated fault stack — allocated only now that the core is
|
// This core's dedicated fault stack — allocated only now that the core is
|
||||||
// real, rather than reserved statically for every possible core.
|
// real, rather than reserved statically for every possible core.
|
||||||
const fault_stack = heap.allocator().alloc(u8, architecture.fault_stack_size) catch {
|
const fault_stack = heap.allocator().alloc(u8, architecture.fault_stack_size) catch {
|
||||||
log.print(" cpu apic_id {d}: no fault stack; skipped\n", .{core.apic_id});
|
log.print("/system/kernel: cpu apic_id {d}: no fault stack; skipped\n", .{core.apic_id});
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
architecture.setFaultStack(index, (@intFromPtr(fault_stack.ptr) + fault_stack.len) & ~@as(usize, 15));
|
architecture.setFaultStack(index, (@intFromPtr(fault_stack.ptr) + fault_stack.len) & ~@as(usize, 15));
|
||||||
@@ -353,14 +404,14 @@ fn bringUpSecondaries() void {
|
|||||||
while (attempt <= maximum_wake_attempts) : (attempt += 1) {
|
while (attempt <= maximum_wake_attempts) : (attempt += 1) {
|
||||||
if (architecture.startSecondary(core.apic_id, stack_top, @intFromPtr(pc), index)) {
|
if (architecture.startSecondary(core.apic_id, stack_top, @intFromPtr(pc), index)) {
|
||||||
pc.online = true;
|
pc.online = true;
|
||||||
log.print(" cpu apic_id {d}: online (attempt {d})\n", .{ core.apic_id, attempt });
|
log.print("/system/kernel: cpu apic_id {d}: online (attempt {d})\n", .{ core.apic_id, attempt });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (attempt == maximum_wake_attempts)
|
if (attempt == maximum_wake_attempts)
|
||||||
log.print(" cpu apic_id {d}: no response after {d} attempts (parked)\n", .{ core.apic_id, maximum_wake_attempts });
|
log.print("/system/kernel: cpu apic_id {d}: no response after {d} attempts (parked)\n", .{ core.apic_id, maximum_wake_attempts });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.print("danos: {d}/{d} cores online\n", .{ scheduler.onlineCount(), cores.len });
|
log.print("/system/kernel: {d}/{d} cores online\n", .{ scheduler.onlineCount(), cores.len });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A user-facing status line: to the diagnostic `log` *and* the on-screen console
|
/// A user-facing status line: to the diagnostic `log` *and* the on-screen console
|
||||||
@@ -412,16 +463,32 @@ fn recoverableFault(vector: u64) bool {
|
|||||||
/// plus a POST code and a persistent breadcrumb. (A ring-3 fault on a *borrowed*
|
/// plus a POST code and a persistent breadcrumb. (A ring-3 fault on a *borrowed*
|
||||||
/// kernel thread — process.run, the user-pf isolation probe — also lands here: there
|
/// kernel thread — process.run, the user-pf isolation probe — also lands here: there
|
||||||
/// is no scheduled process to kill.)
|
/// is no scheduled process to kill.)
|
||||||
|
/// Classify a CPU exception vector as the ExitReason a supervisor reads — the
|
||||||
|
/// fault classes of docs/process-lifecycle.md. Faults are exit reasons, never
|
||||||
|
/// signals delivered to the faulting process: recovery is restart, not a handler.
|
||||||
|
fn exitReasonForVector(vector: u64) abi.ExitReason {
|
||||||
|
return switch (vector) {
|
||||||
|
14 => .segmentation_fault, // page fault
|
||||||
|
6 => .illegal_instruction, // invalid opcode
|
||||||
|
0, 16, 19 => .arithmetic_fault, // divide error, x87, SIMD
|
||||||
|
13 => .protection_fault, // general protection
|
||||||
|
else => .fault,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn onException(state: *const architecture.CpuState) noreturn {
|
fn onException(state: *const architecture.CpuState) noreturn {
|
||||||
if (architecture.fromUser(state) and scheduler.currentIsUserProcess() and recoverableFault(state.vector)) {
|
if (architecture.fromUser(state) and scheduler.currentIsUserProcess() and recoverableFault(state.vector)) {
|
||||||
statusPrint("\ndanos: process {d} ({s}) killed by {s} (vector {d}) on core {d}\n", .{ scheduler.currentId(), scheduler.current().name(), architecture.exceptionName(state.vector), state.vector, scheduler.currentCpuIndex() });
|
statusPrint("\n/system/kernel: process {d} ({s}) killed by {s} (vector {d}) on core {d}\n", .{ scheduler.currentId(), scheduler.current().name(), architecture.exceptionName(state.vector), state.vector, scheduler.currentCpuIndex() });
|
||||||
statusPrint(" error code : 0x{x}\n", .{state.error_code});
|
statusPrint(" error code : 0x{x}\n", .{state.error_code});
|
||||||
statusPrint(" IP : 0x{x:0>16}\n", .{architecture.instructionPointer(state)});
|
statusPrint(" IP : 0x{x:0>16}\n", .{architecture.instructionPointer(state)});
|
||||||
if (architecture.faultAddress(state)) |address| statusPrint(" fault addr : 0x{x:0>16}\n", .{address});
|
if (architecture.faultAddress(state)) |address| statusPrint(" fault addr : 0x{x:0>16}\n", .{address});
|
||||||
process.killCurrentProcess(); // reclaims everything, reschedules; never returns
|
process.killCurrentProcess(exitReasonForVector(state.vector)); // reclaims everything, reschedules; never returns
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
@@ -444,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");
|
||||||
|
|||||||
@@ -41,6 +41,39 @@ pub fn write(bytes: []const u8) void {
|
|||||||
for (sinks[0..sink_count]) |sink| sink(bytes);
|
for (sinks[0..sink_count]) |sink| sink(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- the RAM sink: a retained copy of the whole diagnostic stream ------------
|
||||||
|
//
|
||||||
|
// A fixed in-image buffer that accumulates every logged byte, so a user program
|
||||||
|
// (`log-flush`, and init at shutdown) can read it back through `klog_read` and
|
||||||
|
// persist it to a file — the boot log survives on a headless/real machine that
|
||||||
|
// has no host capturing serial. It is a *sink like any other*: register it with
|
||||||
|
// `addSink(ramSink)` at boot. No allocation (works pre-heap and in a panic).
|
||||||
|
//
|
||||||
|
// It fills linearly and stops when full: the earliest output — the most valuable
|
||||||
|
// for diagnosing a boot — is kept, and the tail is still on the live serial sink.
|
||||||
|
// 256 KiB comfortably holds a full boot plus a long run (a boot is ~15 KiB).
|
||||||
|
|
||||||
|
const ram_capacity = 256 * 1024;
|
||||||
|
var ram_buffer: [ram_capacity]u8 = undefined;
|
||||||
|
var ram_len: usize = 0;
|
||||||
|
|
||||||
|
/// The RAM sink. Best-effort and self-guarding like every sink: appends what fits
|
||||||
|
/// and silently drops the rest once full. (Concurrency matches the other sinks —
|
||||||
|
/// the dominant writer, debug_write, already holds the kernel lock; a rare torn
|
||||||
|
/// append on a kernel-internal line is an accepted diagnostic imperfection.)
|
||||||
|
pub fn ramSink(bytes: []const u8) void {
|
||||||
|
const n = @min(ram_buffer.len - ram_len, bytes.len);
|
||||||
|
if (n != 0) {
|
||||||
|
@memcpy(ram_buffer[ram_len..][0..n], bytes[0..n]);
|
||||||
|
ram_len += n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The accumulated log so far — what `klog_read` copies out.
|
||||||
|
pub fn ramSnapshot() []const u8 {
|
||||||
|
return ram_buffer[0..ram_len];
|
||||||
|
}
|
||||||
|
|
||||||
/// A formatted log line. Truncates past 256 bytes; the buffer is on the stack, so
|
/// A formatted log line. Truncates past 256 bytes; the buffer is on the stack, so
|
||||||
/// this is safe to call from interrupt context and from a panic.
|
/// this is safe to call from interrupt context and from a panic.
|
||||||
pub fn print(comptime fmt: []const u8, args: anytype) void {
|
pub fn print(comptime fmt: []const u8, args: anytype) void {
|
||||||
|
|||||||
+319
-24
@@ -28,12 +28,14 @@ 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");
|
||||||
const irq = @import("irq.zig");
|
const irq = @import("irq.zig");
|
||||||
const initial_ramdisk = @import("initial-ramdisk");
|
const initial_ramdisk = @import("initial-ramdisk");
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
|
const wall_clock = @import("wall-clock.zig");
|
||||||
|
|
||||||
const page_size = abi.page_size;
|
const page_size = abi.page_size;
|
||||||
const SystemCall = abi.SystemCall;
|
const SystemCall = abi.SystemCall;
|
||||||
@@ -79,9 +81,11 @@ pub const device_arena_end: u64 = device_arena_base + (4 << 30);
|
|||||||
pub const dma_arena_base: u64 = 0x0000_7200_0000_0000;
|
pub const dma_arena_base: u64 = 0x0000_7200_0000_0000;
|
||||||
pub const dma_arena_end: u64 = dma_arena_base + (256 << 20); // 256 MiB per process
|
pub const dma_arena_end: u64 = dma_arena_base + (256 << 20); // 256 MiB per process
|
||||||
|
|
||||||
/// Largest single `mmap` grant, in pages (1 MiB). The user heap grows in small
|
/// Largest single `mmap` grant, in pages (32 MiB). Big enough for a display service's
|
||||||
/// chunks, so this bound is generous; it also caps the frame scratch array below.
|
/// back buffer at up to 4K (3840x2160x4 ≈ 8100 pages); the user heap otherwise grows in
|
||||||
const maximum_mmap_pages = 256;
|
/// small chunks. `systemMmap` maps page by page with rollback, so this is only a sanity
|
||||||
|
/// bound (and an overflow guard on the page count), not the size of any scratch array.
|
||||||
|
const maximum_mmap_pages = 8192;
|
||||||
|
|
||||||
/// Ceiling on a process's argv entries, including argv[0]. Arguments are spawn
|
/// Ceiling on a process's argv entries, including argv[0]. Arguments are spawn
|
||||||
/// parameters ("you are the driver for device 12"), not bulk data — IPC carries
|
/// parameters ("you are the driver for device 12"), not bulk data — IPC carries
|
||||||
@@ -137,6 +141,7 @@ pub fn init() void {
|
|||||||
architecture.setSystemCallHandler(system_call);
|
architecture.setSystemCallHandler(system_call);
|
||||||
scheduler.terminate_current_hook = terminateCurrentLocked;
|
scheduler.terminate_current_hook = terminateCurrentLocked;
|
||||||
scheduler.reap_task_hook = reapTaskLocked;
|
scheduler.reap_task_hook = reapTaskLocked;
|
||||||
|
scheduler.timer_tick_hook = timerSweepLocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return -1 (as an unsigned bit pattern) in the system_call result register.
|
/// Return -1 (as an unsigned bit pattern) in the system_call result register.
|
||||||
@@ -164,6 +169,7 @@ fn system_call(state: *architecture.CpuState) void {
|
|||||||
// A scheduled process tears down fully (terminateCurrent); a borrowed
|
// A scheduled process tears down fully (terminateCurrent); a borrowed
|
||||||
// test thread unwinds back to the kernel that entered it.
|
// test thread unwinds back to the kernel that entered it.
|
||||||
if (scheduler.currentIsUserProcess()) {
|
if (scheduler.currentIsUserProcess()) {
|
||||||
|
scheduler.current().exit_reason = .exited;
|
||||||
terminateCurrent();
|
terminateCurrent();
|
||||||
} else architecture.userExit();
|
} else architecture.userExit();
|
||||||
},
|
},
|
||||||
@@ -183,6 +189,7 @@ fn system_call(state: *architecture.CpuState) void {
|
|||||||
.ipc_lookup => systemIpcLookup(state),
|
.ipc_lookup => systemIpcLookup(state),
|
||||||
.ipc_call => systemIpcCall(state),
|
.ipc_call => systemIpcCall(state),
|
||||||
.ipc_reply_wait => systemIpcReplyWait(state),
|
.ipc_reply_wait => systemIpcReplyWait(state),
|
||||||
|
.ipc_send => systemIpcSend(state),
|
||||||
.device_enumerate => systemDeviceEnumerate(state),
|
.device_enumerate => systemDeviceEnumerate(state),
|
||||||
.device_claim => systemDeviceClaim(state),
|
.device_claim => systemDeviceClaim(state),
|
||||||
.mmio_map => systemMmioMap(state),
|
.mmio_map => systemMmioMap(state),
|
||||||
@@ -198,6 +205,13 @@ fn system_call(state: *architecture.CpuState) void {
|
|||||||
.clock => systemClock(state),
|
.clock => systemClock(state),
|
||||||
.process_enumerate => systemProcessEnumerate(state),
|
.process_enumerate => systemProcessEnumerate(state),
|
||||||
.process_kill => systemProcessKill(state),
|
.process_kill => systemProcessKill(state),
|
||||||
|
.process_exit_reason => systemProcessExitReason(state),
|
||||||
|
.process_subscribe => systemProcessSubscribe(state),
|
||||||
|
.signal_bind => systemSignalBind(state),
|
||||||
|
.process_signal => systemProcessSignal(state),
|
||||||
|
.timer_bind => systemTimerBind(state),
|
||||||
|
.klog_read => systemKlogRead(state),
|
||||||
|
.wall_clock => systemWallClock(state),
|
||||||
_ => fail(state),
|
_ => fail(state),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,6 +277,18 @@ fn systemIpcReplyWait(state: *architecture.CpuState) void {
|
|||||||
architecture.setSystemCallResult3(state, received_cap);
|
architecture.setSystemCallResult3(state, received_cap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ipc_send(handle, message_ptr, message_len) -> 0/-errno: post a payload to an
|
||||||
|
/// endpoint's async queue and wake a receiver, without blocking the caller. The async
|
||||||
|
/// counterpart of ipc_call — for broadcasts (the input service) where a rendezvous would
|
||||||
|
/// let one dead subscriber hang the sender. Delivered through ipc_reply_wait as a
|
||||||
|
/// buffered message (badge carries notify_message_bit and the caller's task id).
|
||||||
|
fn systemIpcSend(state: *architecture.CpuState) void {
|
||||||
|
const me = scheduler.current();
|
||||||
|
const endpoint = ipc.resolveHandle(me, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
||||||
|
const r = ipc.send(endpoint, me.aspace, architecture.systemCallArg(state, 1), architecture.systemCallArg(state, 2), me.id);
|
||||||
|
architecture.setSystemCallResult(state, @bitCast(r));
|
||||||
|
}
|
||||||
|
|
||||||
/// device_enumerate(buffer, maximum) -> total: snapshot the device table into the caller's
|
/// device_enumerate(buffer, maximum) -> total: snapshot the device table into the caller's
|
||||||
/// buffer (up to `maximum` entries), returning the total device count.
|
/// buffer (up to `maximum` entries), returning the total device count.
|
||||||
fn systemDeviceEnumerate(state: *architecture.CpuState) void {
|
fn systemDeviceEnumerate(state: *architecture.CpuState) void {
|
||||||
@@ -278,10 +304,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 {
|
||||||
if (devices_broker.claim(architecture.systemCallArg(state, 0), scheduler.current().id))
|
const device_id = architecture.systemCallArg(state, 0);
|
||||||
architecture.setSystemCallResult(state, 0)
|
const claim_flags = sync.enter();
|
||||||
else
|
defer sync.leave(claim_flags);
|
||||||
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
|
/// mmio_map(device_id, resource_index) -> vaddr: map a claimed device's MMIO window into
|
||||||
@@ -292,10 +327,22 @@ fn systemMmioMap(state: *architecture.CpuState) void {
|
|||||||
const resource_index = architecture.systemCallArg(state, 1);
|
const resource_index = architecture.systemCallArg(state, 1);
|
||||||
const t = scheduler.current();
|
const t = scheduler.current();
|
||||||
if (t.aspace == 0) return fail(state);
|
if (t.aspace == 0) return fail(state);
|
||||||
|
// Read the broker table under the lock: ring-3 device_register (M19) now
|
||||||
|
// mutates it concurrently on other cores, so a lock-free read here could
|
||||||
|
// see a torn resource (and a torn length used to panic the arithmetic
|
||||||
|
// below on integer overflow).
|
||||||
|
const r = blk: {
|
||||||
|
const flags = sync.enter();
|
||||||
|
defer sync.leave(flags);
|
||||||
const owner = devices_broker.ownerOf(device_id) orelse return fail(state);
|
const owner = devices_broker.ownerOf(device_id) orelse return fail(state);
|
||||||
if (owner != t.id) return fail(state); // not claimed by this process
|
if (owner != t.id) return fail(state); // not claimed by this process
|
||||||
const r = devices_broker.resourceOf(device_id, resource_index) orelse return fail(state);
|
break :blk devices_broker.resourceOf(device_id, resource_index) orelse return fail(state);
|
||||||
|
};
|
||||||
if (r.kind != @intFromEnum(device_abi.ResourceKind.memory)) return fail(state);
|
if (r.kind != @intFromEnum(device_abi.ResourceKind.memory)) return fail(state);
|
||||||
|
// A zero-length or wrapping window is not mappable — fail cleanly rather
|
||||||
|
// than underflow `r.len - 1`.
|
||||||
|
if (r.len == 0) return fail(state);
|
||||||
|
if (@addWithOverflow(r.start, r.len)[1] != 0) return fail(state);
|
||||||
|
|
||||||
if (t.device_map_next == 0) t.device_map_next = device_arena_base;
|
if (t.device_map_next == 0) t.device_map_next = device_arena_base;
|
||||||
const first = r.start & ~@as(u64, page_size - 1);
|
const first = r.start & ~@as(u64, page_size - 1);
|
||||||
@@ -304,7 +351,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
|
||||||
}
|
}
|
||||||
@@ -431,6 +481,11 @@ fn systemDeviceRegister(state: *architecture.CpuState) void {
|
|||||||
var descriptor: device_abi.DeviceDescriptor = undefined;
|
var descriptor: device_abi.DeviceDescriptor = undefined;
|
||||||
if (!ipc.copyFromUser(t.aspace, descriptor_ptr, std.mem.asBytes(&descriptor))) return fail(state);
|
if (!ipc.copyFromUser(t.aspace, descriptor_ptr, std.mem.asBytes(&descriptor))) return fail(state);
|
||||||
|
|
||||||
|
// Under the big kernel lock: the broker's table is also mutated by the
|
||||||
|
// death sweep (releaseAllOwnedBy) and read by enumerate on other cores —
|
||||||
|
// ring-3 registration (M19) made those genuinely concurrent.
|
||||||
|
const flags = sync.enter();
|
||||||
|
defer sync.leave(flags);
|
||||||
const id = devices_broker.register(parent_id, t.id, &descriptor) catch return fail(state);
|
const id = devices_broker.register(parent_id, t.id, &descriptor) catch return fail(state);
|
||||||
architecture.setSystemCallResult(state, id);
|
architecture.setSystemCallResult(state, id);
|
||||||
}
|
}
|
||||||
@@ -539,7 +594,11 @@ pub var fault_kill_count: u64 = 0;
|
|||||||
/// endpoint reference destroys the Endpoint, and a still-bound GSI would have an
|
/// endpoint reference destroys the Endpoint, and a still-bound GSI would have an
|
||||||
/// ISR call notifyFromIsr on freed memory the next time the device fired.
|
/// ISR call notifyFromIsr on freed memory the next time the device fired.
|
||||||
/// `releaseOwner` also leaves the line masked, so a dead driver's device goes
|
/// `releaseOwner` also leaves the line masked, so a dead driver's device goes
|
||||||
/// quiet rather than storming.
|
/// quiet rather than storming. (It drops MSI vectors by the same owner sweep.)
|
||||||
|
/// - Device claims are released with the IRQ bindings, so a restarted driver can
|
||||||
|
/// claim the same hardware again — the cleanup half of process-lifecycle.md's
|
||||||
|
/// iron rule 1. Claims hold no pointers, so ordering is free; they go here so
|
||||||
|
/// the exit notification (below, last) observes a fully-released child.
|
||||||
/// - A client this task still owes a reply to (it died between receive and reply)
|
/// - A client this task still owes a reply to (it died between receive and reply)
|
||||||
/// is failed with -EPEER rather than left blocked forever — a dead server must
|
/// is failed with -EPEER rather than left blocked forever — a dead server must
|
||||||
/// not hang its callers.
|
/// not hang its callers.
|
||||||
@@ -552,7 +611,36 @@ pub var fault_kill_count: u64 = 0;
|
|||||||
/// reference taken at spawn is dropped with it.
|
/// reference taken at spawn is dropped with it.
|
||||||
/// Precondition: the big kernel lock is held.
|
/// Precondition: the big kernel lock is held.
|
||||||
fn releaseTaskResourcesLocked(t: *scheduler.Task) void {
|
fn releaseTaskResourcesLocked(t: *scheduler.Task) void {
|
||||||
|
recordExitLocked(t);
|
||||||
irq.releaseOwner(t.id);
|
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)));
|
||||||
|
t.signal_endpoint = null;
|
||||||
|
}
|
||||||
|
t.pending_signals = 0;
|
||||||
|
for (&one_shot_timers) |*slot| {
|
||||||
|
if (slot.*) |timer| {
|
||||||
|
if (timer.owner == t.id) {
|
||||||
|
ipc.dropRef(timer.endpoint);
|
||||||
|
slot.* = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A dead subscriber's own subscriptions go first: it must not hear about
|
||||||
|
// itself, and the slots' endpoint references drop with it.
|
||||||
|
for (&exit_subscribers) |*slot| {
|
||||||
|
if (slot.*) |subscriber| {
|
||||||
|
if (subscriber.owner == t.id) {
|
||||||
|
ipc.dropRef(subscriber.endpoint);
|
||||||
|
slot.* = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (t.ipc_client) |client| {
|
if (t.ipc_client) |client| {
|
||||||
t.ipc_client = null;
|
t.ipc_client = null;
|
||||||
client.ipc_status = -ipc.EPEER;
|
client.ipc_status = -ipc.EPEER;
|
||||||
@@ -562,6 +650,12 @@ fn releaseTaskResourcesLocked(t: *scheduler.Task) void {
|
|||||||
scheduler.removeFromWaitQueueLocked(t);
|
scheduler.removeFromWaitQueueLocked(t);
|
||||||
scheduler.forgetIpcClientLocked(t);
|
scheduler.forgetIpcClientLocked(t);
|
||||||
ipc.closeHandles(t);
|
ipc.closeHandles(t);
|
||||||
|
// Publish the exit to every subscriber (docs/process-lifecycle.md): the same
|
||||||
|
// badge encoding as the supervisor's notification, and equally late, so a
|
||||||
|
// subscriber also observes a fully-released child.
|
||||||
|
for (&exit_subscribers) |*slot| {
|
||||||
|
if (slot.*) |subscriber| ipc.notifyLocked(subscriber.endpoint, abi.notify_exit_bit | t.id);
|
||||||
|
}
|
||||||
if (t.exit_endpoint) |raw| {
|
if (t.exit_endpoint) |raw| {
|
||||||
const endpoint: *ipc.Endpoint = @ptrCast(@alignCast(raw));
|
const endpoint: *ipc.Endpoint = @ptrCast(@alignCast(raw));
|
||||||
t.exit_endpoint = null;
|
t.exit_endpoint = null;
|
||||||
@@ -615,6 +709,7 @@ pub fn killProcess(caller_id: u32, target_id: u32) i64 {
|
|||||||
const target = scheduler.taskByIdLocked(target_id) orelse return -ipc.ESRCH;
|
const target = scheduler.taskByIdLocked(target_id) orelse return -ipc.ESRCH;
|
||||||
if (target.aspace == 0) return -ipc.ESRCH; // kernel tasks are not processes
|
if (target.aspace == 0) return -ipc.ESRCH; // kernel tasks are not processes
|
||||||
if (target.supervisor != caller_id) return -ipc.EPERM;
|
if (target.supervisor != caller_id) return -ipc.EPERM;
|
||||||
|
target.exit_reason = .killed;
|
||||||
if (target.state == .running) {
|
if (target.state == .running) {
|
||||||
target.kill_pending = true;
|
target.kill_pending = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -627,12 +722,170 @@ pub fn killProcess(caller_id: u32, target_id: u32) i64 {
|
|||||||
/// The fault is confined to the process — the kernel trapped it on the task's own
|
/// The fault is confined to the process — the kernel trapped it on the task's own
|
||||||
/// kernel stack and is intact — so everything the process held is reclaimed and the
|
/// kernel stack and is intact — so everything the process held is reclaimed and the
|
||||||
/// core reschedules. The system keeps running; only the faulting process dies
|
/// core reschedules. The system keeps running; only the faulting process dies
|
||||||
/// (docs/resilience.md: fault -> kill -> continue).
|
/// (docs/resilience.md: fault -> kill -> continue). `reason` is the fault class
|
||||||
pub fn killCurrentProcess() noreturn {
|
/// (from the vector), recorded for the supervisor's `process_exit_reason`.
|
||||||
|
pub fn killCurrentProcess(reason: abi.ExitReason) noreturn {
|
||||||
|
scheduler.current().exit_reason = reason;
|
||||||
fault_kill_count += 1;
|
fault_kill_count += 1;
|
||||||
terminateCurrent();
|
terminateCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The bounded record of recent deaths, for `process_exit_reason`: ids are never
|
||||||
|
/// reused, so a ring keyed by id is enough — a record evicted by wraparound reads
|
||||||
|
/// as -ESRCH, the same as an id that never lived, which a supervisor treats as
|
||||||
|
/// "too late to ask". Written under the big kernel lock by the reap.
|
||||||
|
const exit_record_capacity = 64;
|
||||||
|
const ExitRecord = struct { id: u32 = 0, supervisor: u32 = 0, reason: abi.ExitReason = .exited, valid: bool = false };
|
||||||
|
var exit_records: [exit_record_capacity]ExitRecord = .{ExitRecord{}} ** exit_record_capacity;
|
||||||
|
var exit_record_next: usize = 0;
|
||||||
|
|
||||||
|
/// Record a dying task's (id, supervisor, reason) — called by the reap before the
|
||||||
|
/// exit notification is posted, so a supervisor that hears the notification can
|
||||||
|
/// always still query the reason. Precondition: the big kernel lock is held.
|
||||||
|
fn recordExitLocked(t: *scheduler.Task) void {
|
||||||
|
exit_records[exit_record_next] = .{ .id = t.id, .supervisor = t.supervisor, .reason = t.exit_reason, .valid = true };
|
||||||
|
exit_record_next = (exit_record_next + 1) % exit_record_capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How dead process `id` ended, for `caller` — the kernel half of the
|
||||||
|
/// process_exit_reason system call. Returns the ExitReason value, -ESRCH (never
|
||||||
|
/// lived, still alive, or evicted from the ring), or -EPERM (the caller was not
|
||||||
|
/// its supervisor — the same authority gate as process_kill).
|
||||||
|
pub fn exitReasonOf(caller_id: u32, target_id: u32) i64 {
|
||||||
|
const flags = sync.enter();
|
||||||
|
defer sync.leave(flags);
|
||||||
|
for (&exit_records) |*record| {
|
||||||
|
if (record.valid and record.id == target_id) {
|
||||||
|
if (record.supervisor != caller_id) return -ipc.EPERM;
|
||||||
|
return @intFromEnum(record.reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -ipc.ESRCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The published exit events' subscribers (docs/process-lifecycle.md "Who learns
|
||||||
|
/// of a death"): stateful services — the VFS's file handles, input's
|
||||||
|
/// subscriptions — that must release what a dead client held and cannot learn it
|
||||||
|
/// any other way (a client that simply never calls again looks like silence).
|
||||||
|
/// Bounded like every kernel table; each entry holds its own endpoint reference.
|
||||||
|
const exit_subscriber_capacity = 8;
|
||||||
|
const ExitSubscriber = struct { endpoint: *ipc.Endpoint, owner: u32 };
|
||||||
|
var exit_subscribers: [exit_subscriber_capacity]?ExitSubscriber = .{null} ** exit_subscriber_capacity;
|
||||||
|
|
||||||
|
/// process_subscribe(endpoint): subscribe the caller's endpoint to published exit
|
||||||
|
/// events. Ungated, like process_enumerate — what is running (and dying) is not a
|
||||||
|
/// secret between cooperating processes. -ENOSPC when the table is full.
|
||||||
|
fn systemProcessSubscribe(state: *architecture.CpuState) void {
|
||||||
|
const t = scheduler.current();
|
||||||
|
if (t.aspace == 0) return fail(state);
|
||||||
|
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
||||||
|
const flags = sync.enter();
|
||||||
|
defer sync.leave(flags);
|
||||||
|
for (&exit_subscribers) |*slot| {
|
||||||
|
if (slot.* == null) {
|
||||||
|
endpoint.refcount += 1; // the slot's own reference, dropped on unsubscribe-by-death
|
||||||
|
slot.* = .{ .endpoint = endpoint, .owner = t.id };
|
||||||
|
return architecture.setSystemCallResult(state, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
failErr(state, ipc.ENOSPC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// signal_bind(endpoint): nominate where this process's signals arrive — the
|
||||||
|
/// IRQ-as-IPC pattern a fourth time (docs/process-lifecycle.md). Replacing a
|
||||||
|
/// binding drops the old reference; signals that pended while unbound are
|
||||||
|
/// delivered immediately on bind, coalesced into one notification.
|
||||||
|
fn systemSignalBind(state: *architecture.CpuState) void {
|
||||||
|
const t = scheduler.current();
|
||||||
|
if (t.aspace == 0) return fail(state);
|
||||||
|
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
||||||
|
const flags = sync.enter();
|
||||||
|
defer sync.leave(flags);
|
||||||
|
if (t.signal_endpoint) |raw| ipc.dropRef(@ptrCast(@alignCast(raw)));
|
||||||
|
endpoint.refcount += 1;
|
||||||
|
t.signal_endpoint = @ptrCast(endpoint);
|
||||||
|
if (t.pending_signals != 0) {
|
||||||
|
ipc.notifyLocked(endpoint, abi.notify_signal_bit | t.pending_signals);
|
||||||
|
t.pending_signals = 0;
|
||||||
|
}
|
||||||
|
architecture.setSystemCallResult(state, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// process_signal(id, signal): post a signal — a one-way, coalescing statement,
|
||||||
|
/// never a question (docs/process-lifecycle.md). The authority gate is the
|
||||||
|
/// supervision link, like kill; a process may also signal itself. Unbound
|
||||||
|
/// targets accumulate the signal in their pending mask.
|
||||||
|
fn systemProcessSignal(state: *architecture.CpuState) void {
|
||||||
|
const t = scheduler.current();
|
||||||
|
if (t.aspace == 0) return fail(state);
|
||||||
|
const id = architecture.systemCallArg(state, 0);
|
||||||
|
const signal = architecture.systemCallArg(state, 1);
|
||||||
|
if (id > std.math.maxInt(u32)) return failErr(state, ipc.ESRCH);
|
||||||
|
if (signal > 31) return failErr(state, ipc.EBADF); // not a Signal bit position
|
||||||
|
const flags = sync.enter();
|
||||||
|
defer sync.leave(flags);
|
||||||
|
const target = scheduler.taskByIdLocked(@intCast(id)) orelse return failErr(state, ipc.ESRCH);
|
||||||
|
if (target.aspace == 0) return failErr(state, ipc.ESRCH);
|
||||||
|
if (target.supervisor != t.id and target.id != t.id) return failErr(state, ipc.EPERM);
|
||||||
|
target.pending_signals |= @as(u32, 1) << @intCast(signal);
|
||||||
|
if (target.signal_endpoint) |raw| {
|
||||||
|
const endpoint: *ipc.Endpoint = @ptrCast(@alignCast(raw));
|
||||||
|
ipc.notifyLocked(endpoint, abi.notify_signal_bit | target.pending_signals);
|
||||||
|
target.pending_signals = 0;
|
||||||
|
}
|
||||||
|
architecture.setSystemCallResult(state, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The one-shot timers of timer_bind: the missing timed wait. A service arms a
|
||||||
|
/// deadline and keeps serving; the expiry arrives in the same replyWait as
|
||||||
|
/// everything else (notify_timer_bit). What stop-sequence escalation, hello
|
||||||
|
/// deadlines, and restart backoff are built from — and later, `alarm`.
|
||||||
|
const timer_capacity = 16;
|
||||||
|
const OneShotTimer = struct { deadline: u64, endpoint: *ipc.Endpoint, owner: u32 };
|
||||||
|
var one_shot_timers: [timer_capacity]?OneShotTimer = .{null} ** timer_capacity;
|
||||||
|
|
||||||
|
/// Sweep expired timers — hung on scheduler.timer_tick_hook, so it runs on every
|
||||||
|
/// tick with the big kernel lock held, like the sleeper wake it rides beside.
|
||||||
|
fn timerSweepLocked() void {
|
||||||
|
const now = architecture.millis();
|
||||||
|
for (&one_shot_timers) |*slot| {
|
||||||
|
if (slot.*) |timer| {
|
||||||
|
if (now >= timer.deadline) {
|
||||||
|
ipc.notifyLocked(timer.endpoint, abi.notify_timer_bit);
|
||||||
|
ipc.dropRef(timer.endpoint);
|
||||||
|
slot.* = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// timer_bind(endpoint, ms): arm a one-shot timer. -ENOSPC when the table is full.
|
||||||
|
fn systemTimerBind(state: *architecture.CpuState) void {
|
||||||
|
const t = scheduler.current();
|
||||||
|
if (t.aspace == 0) return fail(state);
|
||||||
|
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
||||||
|
const ms = architecture.systemCallArg(state, 1);
|
||||||
|
const flags = sync.enter();
|
||||||
|
defer sync.leave(flags);
|
||||||
|
for (&one_shot_timers) |*slot| {
|
||||||
|
if (slot.* == null) {
|
||||||
|
endpoint.refcount += 1;
|
||||||
|
slot.* = .{ .deadline = architecture.millis() + ms, .endpoint = endpoint, .owner = t.id };
|
||||||
|
return architecture.setSystemCallResult(state, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
failErr(state, ipc.ENOSPC);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn systemProcessExitReason(state: *architecture.CpuState) void {
|
||||||
|
const t = scheduler.current();
|
||||||
|
if (t.aspace == 0) return fail(state);
|
||||||
|
const id = architecture.systemCallArg(state, 0);
|
||||||
|
if (id > std.math.maxInt(u32)) return failErr(state, ipc.ESRCH);
|
||||||
|
const r = exitReasonOf(t.id, @intCast(id));
|
||||||
|
architecture.setSystemCallResult(state, @bitCast(r));
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve `(device_id, resource_index)` to a GSI this process is entitled to bind, or null.
|
/// Resolve `(device_id, resource_index)` to a GSI this process is entitled to bind, or null.
|
||||||
/// The two checks are the whole security story: the device must be *claimed* by the
|
/// The two checks are the whole security story: the device must be *claimed* by the
|
||||||
/// caller, and the resource must be one of that device's `irq` resources as recorded
|
/// caller, and the resource must be one of that device's `irq` resources as recorded
|
||||||
@@ -732,7 +985,6 @@ fn systemDebugWrite(state: *architecture.CpuState) void {
|
|||||||
write_len = len;
|
write_len = len;
|
||||||
write_from_user = architecture.fromUser(state);
|
write_from_user = architecture.fromUser(state);
|
||||||
write_count += 1;
|
write_count += 1;
|
||||||
if (write_at_line_start) log.write("DANOS-INIT: ");
|
|
||||||
log.write(source[0..len]);
|
log.write(source[0..len]);
|
||||||
if (len != 0) write_at_line_start = source[len - 1] == '\n';
|
if (len != 0) write_at_line_start = source[len - 1] == '\n';
|
||||||
architecture.setSystemCallResult(state, len);
|
architecture.setSystemCallResult(state, len);
|
||||||
@@ -741,6 +993,37 @@ fn systemDebugWrite(state: *architecture.CpuState) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// klog_read(offset, ptr, len) -> bytes copied: copy the kernel's in-memory
|
||||||
|
/// diagnostic log (the RAM sink in log.zig) out to the user buffer at `ptr`,
|
||||||
|
/// starting at `offset`. Returns the count copied — 0 once `offset` reaches the
|
||||||
|
/// end — so a program reads the whole log by looping from 0 until it gets 0.
|
||||||
|
///
|
||||||
|
/// The mirror of `debug_write`: the same overflow-safe user-half bounds check,
|
||||||
|
/// but the copy runs kernel -> user. Written under the kernel lock so the source
|
||||||
|
/// snapshot can't grow underneath the copy. A read-only diagnostic — it exposes
|
||||||
|
/// only the log the kernel already broadcasts to serial, nothing else.
|
||||||
|
fn systemKlogRead(state: *architecture.CpuState) void {
|
||||||
|
const offset = architecture.systemCallArg(state, 0);
|
||||||
|
const ptr = architecture.systemCallArg(state, 1);
|
||||||
|
const len = architecture.systemCallArg(state, 2);
|
||||||
|
// Confine the whole destination span to the user (low) half. `len <=
|
||||||
|
// user_half_end - ptr` bounds the length without an overflowing add.
|
||||||
|
if (ptr < user_half_end and len <= user_half_end - ptr) {
|
||||||
|
const flags = sync.enter();
|
||||||
|
defer sync.leave(flags);
|
||||||
|
const snapshot = log.ramSnapshot();
|
||||||
|
var n: usize = 0;
|
||||||
|
if (offset < snapshot.len) {
|
||||||
|
n = @min(len, snapshot.len - offset);
|
||||||
|
const dest: [*]u8 = @ptrFromInt(ptr);
|
||||||
|
@memcpy(dest[0..n], snapshot[offset..][0..n]);
|
||||||
|
}
|
||||||
|
architecture.setSystemCallResult(state, n);
|
||||||
|
} else {
|
||||||
|
fail(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// mmap(len, prot) -> base: grant `len` bytes (rounded up to whole pages) of
|
/// mmap(len, prot) -> base: grant `len` bytes (rounded up to whole pages) of
|
||||||
/// fresh, zeroed, writable+NX memory in the caller's mmap arena, and return the
|
/// fresh, zeroed, writable+NX memory in the caller's mmap arena, and return the
|
||||||
/// base virtual address. `prot` is accepted but not yet honoured (grants are
|
/// base virtual address. `prot` is accepted but not yet honoured (grants are
|
||||||
@@ -757,21 +1040,26 @@ fn systemMmap(state: *architecture.CpuState) void {
|
|||||||
const base = t.heap_next;
|
const base = t.heap_next;
|
||||||
if (base + pages * page_size > heap_arena_end) return fail(state); // arena exhausted
|
if (base + pages * page_size > heap_arena_end) return fail(state); // arena exhausted
|
||||||
|
|
||||||
// Reserve all frames up front so a mid-way exhaustion rolls back cleanly
|
// Map page by page. On mid-way frame exhaustion, roll back the pages already mapped
|
||||||
// (no partially-mapped grant leaks into the address space).
|
// (unmap + free) so no partial grant leaks into the address space — the same
|
||||||
var frames: [maximum_mmap_pages]u64 = undefined;
|
// all-or-nothing guarantee as before, but without a fixed scratch array, so the
|
||||||
var got: usize = 0;
|
// per-call size can be a multi-MiB framebuffer.
|
||||||
while (got < pages) : (got += 1) {
|
var mapped: usize = 0;
|
||||||
frames[got] = pmm.alloc() orelse {
|
while (mapped < pages) : (mapped += 1) {
|
||||||
for (frames[0..got]) |f| pmm.free(f);
|
const frame = pmm.alloc() orelse {
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < mapped) : (i += 1) {
|
||||||
|
const va = base + i * page_size;
|
||||||
|
if (architecture.translate(t.aspace, va)) |physical| {
|
||||||
|
architecture.unmapUserPageInto(t.aspace, va);
|
||||||
|
pmm.free(physical);
|
||||||
|
}
|
||||||
|
}
|
||||||
return fail(state);
|
return fail(state);
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
for (frames[0..pages], 0..) |frame, i| {
|
|
||||||
const destination: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(frame));
|
const destination: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(frame));
|
||||||
@memset(destination[0..page_size], 0); // hand out zeroed memory
|
@memset(destination[0..page_size], 0); // hand out zeroed memory
|
||||||
architecture.mapUserPageInto(t.aspace, base + i * page_size, frame, true, false); // RW + NX
|
architecture.mapUserPageInto(t.aspace, base + mapped * page_size, frame, true, false); // RW + NX
|
||||||
}
|
}
|
||||||
t.heap_next = base + pages * page_size;
|
t.heap_next = base + pages * page_size;
|
||||||
architecture.setSystemCallResult(state, base);
|
architecture.setSystemCallResult(state, base);
|
||||||
@@ -1071,3 +1359,10 @@ pub fn spawnProcessSupervised(image: []const u8, priority: u3, argv: []const []c
|
|||||||
fn systemClock(state: *architecture.CpuState) void {
|
fn systemClock(state: *architecture.CpuState) void {
|
||||||
architecture.setSystemCallResult(state, architecture.nanos());
|
architecture.setSystemCallResult(state, architecture.nanos());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// wall_clock() -> Unix epoch seconds (UTC). The RTC value, read at boot and offset
|
||||||
|
/// by the monotonic clock (wall-clock.zig) — mechanism, not policy: calendars and
|
||||||
|
/// timezones layer on top in user space. Needed for filesystem timestamps (mtime).
|
||||||
|
fn systemWallClock(state: *architecture.CpuState) void {
|
||||||
|
architecture.setSystemCallResult(state, wall_clock.nowSeconds());
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,6 +50,17 @@ pub const Task = struct {
|
|||||||
// null. Holds its own reference, dropped when the notification is posted.
|
// null. Holds its own reference, dropped when the notification is posted.
|
||||||
// Opaque here for the same reason as `handles` below.
|
// Opaque here for the same reason as `handles` below.
|
||||||
exit_endpoint: ?*anyopaque = null,
|
exit_endpoint: ?*anyopaque = null,
|
||||||
|
// How this process ended — set by the death paths (exit, fault, kill) just
|
||||||
|
// before the reap records it for `process_exit_reason`. Meaningless while
|
||||||
|
// the task lives.
|
||||||
|
exit_reason: abi.ExitReason = .exited,
|
||||||
|
// Endpoint this process's signals arrive on (signal_bind), or null — same
|
||||||
|
// ownership rules as exit_endpoint (holds a reference; opaque here).
|
||||||
|
signal_endpoint: ?*anyopaque = null,
|
||||||
|
// Signals posted but not yet delivered: the coalescing pending mask
|
||||||
|
// (docs/process-lifecycle.md). Bits are abi.Signal values. Signals pend here
|
||||||
|
// until an endpoint is bound; two pending terminates are one terminate.
|
||||||
|
pending_signals: u32 = 0,
|
||||||
// Set by process_kill on a task that is running on another core; the kernel
|
// Set by process_kill on a task that is running on another core; the kernel
|
||||||
// finishes the kill at that task's next system call or timer tick.
|
// finishes the kill at that task's next system call or timer tick.
|
||||||
kill_pending: bool = false,
|
kill_pending: bool = false,
|
||||||
@@ -339,8 +350,9 @@ pub fn spawnUserLocked(aspace: u64, entry: u64, user_sp: u64, priority: Priority
|
|||||||
/// context switch and lock release.
|
/// context switch and lock release.
|
||||||
fn startUserTask() void {
|
fn startUserTask() void {
|
||||||
const t = current();
|
const t = current();
|
||||||
var buffer: [96]u8 = undefined;
|
// No serial chatter here: this runs on every spawn, unserialized against
|
||||||
architecture.serialWrite(std.fmt.bufPrint(&buffer, "DBG startUserTask ip=0x{x} sp=0x{x} aspace=0x{x} kstack=0x{x}\n", .{ t.user_ip, t.user_sp, t.aspace, t.kstack_top }) catch "");
|
// user-space writes, and its output used to shear concurrent log lines in
|
||||||
|
// half — the largest source of corrupted markers in the QEMU scenarios.
|
||||||
architecture.jumpToUser(t.user_ip, t.user_sp); // noreturn
|
architecture.jumpToUser(t.user_ip, t.user_sp); // noreturn
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,9 +655,15 @@ fn reapKillPendingLocked() void {
|
|||||||
/// other critical section, but releases it *without* touching the interrupt flag
|
/// other critical section, but releases it *without* touching the interrupt flag
|
||||||
/// — the handler's `iretq` restores the interrupted context's flags, so
|
/// — the handler's `iretq` restores the interrupted context's flags, so
|
||||||
/// re-enabling here would open a nested-interrupt window before the return.
|
/// re-enabling here would open a nested-interrupt window before the return.
|
||||||
|
/// Called from the tick with the big kernel lock held — process.zig hangs the
|
||||||
|
/// one-shot timer sweep here (timer_bind), the same call-up pattern as the
|
||||||
|
/// teardown hooks below.
|
||||||
|
pub var timer_tick_hook: ?*const fn () void = null;
|
||||||
|
|
||||||
pub fn tick() void {
|
pub fn tick() void {
|
||||||
_ = sync.enter();
|
_ = sync.enter();
|
||||||
wakeExpired();
|
wakeExpired();
|
||||||
|
if (timer_tick_hook) |hook| hook();
|
||||||
reapKillPendingLocked();
|
reapKillPendingLocked();
|
||||||
if (preemption_enabled) schedule();
|
if (preemption_enabled) schedule();
|
||||||
sync.leaveIsr();
|
sync.leaveIsr();
|
||||||
|
|||||||
+1028
-202
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user