Compare commits
39
Commits
7979acc2eb
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51a2b3a932 | ||
|
|
8b4a938164 | ||
|
|
be84506fdf | ||
|
|
559c3e2875 | ||
|
|
36291cd1cc | ||
|
|
b781cf8c97 | ||
|
|
7e036c94e7 | ||
|
|
1d00c1c016 | ||
|
|
3310b3d6de | ||
|
|
93d8becce3 | ||
|
|
ffee9002a6 | ||
|
|
808e1d66fd | ||
|
|
8c09ccef66 | ||
|
|
8b6379fb7e | ||
|
|
aae4db01ad | ||
|
|
1bae77ec84 | ||
|
|
dab7d5427b | ||
|
|
0a20385e12 | ||
|
|
8ae19258b1 | ||
|
|
8de98f3ab3 | ||
|
|
d3e49a925d | ||
|
|
07191b1286 | ||
|
|
e047c1e0c0 | ||
|
|
23cc56b933 | ||
|
|
3c2eefe7f4 | ||
|
|
14c7099e3b | ||
|
|
e87bd358a6 | ||
|
|
9e66c3b249 | ||
|
|
31edff3da4 | ||
|
|
f3d475a0d5 | ||
|
|
278b208258 | ||
|
|
f8b8f9610f | ||
|
|
a81298fb24 | ||
|
|
abfd9de8f6 | ||
|
|
60ca9c78c7 | ||
|
|
2aa6360b3d | ||
|
|
32660ddd1a | ||
|
|
a51ee3339f | ||
|
|
f9e21e0f13 |
No files matched your search
@@ -6,12 +6,21 @@
|
||||
archives/isos/
|
||||
montaukos.org/downloads/*.iso
|
||||
*.hdd
|
||||
*.img
|
||||
rpi4-firmware/
|
||||
qemu.log
|
||||
toolchain/local/
|
||||
toolchain/build/
|
||||
toolchain/src/
|
||||
toolchain/sysroot/
|
||||
toolchain/sysroot-*/
|
||||
programs/bin/
|
||||
# Non-x86 userspace builds (programs/mk/arch.mk)
|
||||
programs/bin-*/
|
||||
programs/obj-*/
|
||||
programs/src/*/obj-*/
|
||||
programs/lib/*/obj-*/
|
||||
programs/lib/*/build-*/
|
||||
programs/obj/
|
||||
programs/src/*/obj/
|
||||
programs/libs/*/obj/
|
||||
|
||||
+124
-20
@@ -10,6 +10,32 @@ QEMUFLAGS := -smp 4 -m 2G -d int -D qemu.log -no-reboot
|
||||
|
||||
override IMAGE_NAME := montauk-$(ARCH)
|
||||
|
||||
# KERNEL_ONLY=1 builds a ramdisk holding just the boot files (no
|
||||
# userspace), for bringing up a kernel on a new architecture.
|
||||
KERNEL_ONLY ?= 0
|
||||
|
||||
# programs/ builds x86_64 into bin/, other architectures into bin-<arch>/.
|
||||
ifeq ($(ARCH),x86_64)
|
||||
PROGRAMS_BIN := programs/bin
|
||||
else
|
||||
PROGRAMS_BIN := programs/bin-$(ARCH)
|
||||
endif
|
||||
|
||||
override EFI_LOADER_x86_64 := BOOTX64.EFI
|
||||
override EFI_LOADER_aarch64 := BOOTAA64.EFI
|
||||
override EFI_LOADER_riscv64 := BOOTRISCV64.EFI
|
||||
override EFI_LOADER_loongarch64 := BOOTLOONGARCH64.EFI
|
||||
|
||||
# The host strip handles x86_64 images; other targets use their toolchain's.
|
||||
ifeq ($(ARCH),x86_64)
|
||||
STRIP := strip
|
||||
else
|
||||
STRIP := toolchain/local/bin/$(ARCH)-montauk-strip
|
||||
endif
|
||||
|
||||
# AArch64 UEFI firmware for QEMU (Debian/Ubuntu: qemu-efi-aarch64).
|
||||
AARCH64_EFI_FIRMWARE ?= /usr/share/qemu-efi-aarch64/QEMU_EFI.fd
|
||||
|
||||
# Toolchain for building the 'limine' executable for the host.
|
||||
HOST_CC := cc
|
||||
HOST_CFLAGS := -g -O2 -pipe
|
||||
@@ -51,29 +77,72 @@ run-hdd-x86_64: $(IMAGE_NAME).hdd
|
||||
$(QEMUFLAGS)
|
||||
|
||||
.PHONY: run-aarch64
|
||||
# The machine mirrors the Raspberry Pi 4/400: Cortex-A72 cores, a GICv2, and
|
||||
# keyboard and mouse behind a hub (every USB 2 device on the Pi is: the
|
||||
# VL805's one USB 2 port carries an on-board hub). QEMU's hub is full-speed,
|
||||
# so the transaction-translator path is only exercised on the real board.
|
||||
# The kernel console is mirrored to the PL011 (-serial stdio).
|
||||
run-aarch64: $(IMAGE_NAME).iso
|
||||
qemu-system-$(ARCH) \
|
||||
-M virt \
|
||||
-M virt,gic-version=2 \
|
||||
-cpu cortex-a72 \
|
||||
-device ramfb \
|
||||
-device qemu-xhci,id=xhci \
|
||||
-device usb-kbd,bus=xhci.0 \
|
||||
-device usb-mouse,bus=xhci.0 \
|
||||
-bios /usr/share/ovmf/OVMF.fd \
|
||||
-device usb-hub,bus=xhci.0,port=1 \
|
||||
-device usb-kbd,bus=xhci.0,port=1.1 \
|
||||
-device usb-mouse,bus=xhci.0,port=1.2 \
|
||||
-bios $(AARCH64_EFI_FIRMWARE) \
|
||||
-cdrom $(IMAGE_NAME).iso \
|
||||
-serial stdio \
|
||||
$(QEMUFLAGS)
|
||||
|
||||
.PHONY: run-hdd-aarch64
|
||||
run-hdd-aarch64: $(IMAGE_NAME).hdd
|
||||
qemu-system-$(ARCH) \
|
||||
-M virt \
|
||||
-M virt,gic-version=2 \
|
||||
-cpu cortex-a72 \
|
||||
-device ramfb \
|
||||
-device qemu-xhci,id=xhci \
|
||||
-device usb-kbd,bus=xhci.0 \
|
||||
-device usb-mouse,bus=xhci.0 \
|
||||
-bios /usr/share/ovmf/OVMF.fd \
|
||||
-hda $(IMAGE_NAME).hdd \
|
||||
-device usb-hub,bus=xhci.0,port=1 \
|
||||
-device usb-kbd,bus=xhci.0,port=1.1 \
|
||||
-device usb-mouse,bus=xhci.0,port=1.2 \
|
||||
-bios $(AARCH64_EFI_FIRMWARE) \
|
||||
-drive if=none,id=hd0,format=raw,file=$(IMAGE_NAME).hdd \
|
||||
-device virtio-blk-pci,drive=hd0 \
|
||||
-serial stdio \
|
||||
$(QEMUFLAGS)
|
||||
|
||||
# The SD card image on QEMU, with the root read through the SDHCI driver:
|
||||
# the card goes on sdhci-pci as a padded copy (QEMU wants a power-of-two card;
|
||||
# RPI4_QEMU_CARD_MIB, default 4 GiB, leaves the first-boot grow room to work
|
||||
# with; every run starts from a fresh copy, so every run is a first boot),
|
||||
# and since QEMU's UEFI has no SD driver, the card's ESP is also
|
||||
# attached alone (the card's first partition, as its own disk) over virtio
|
||||
# for the firmware to boot Limine from.
|
||||
RPI4_QEMU_CARD_MIB ?= 4096
|
||||
.PHONY: run-rpi4-sd
|
||||
run-rpi4-sd: montauk-rpi4.img
|
||||
cp --sparse=always montauk-rpi4.img rpi4-sd-qemu.img
|
||||
size=$$(stat -c %s rpi4-sd-qemu.img); p=$$(($(RPI4_QEMU_CARD_MIB) * 1048576)); \
|
||||
while [ $$p -lt $$size ]; do p=$$((p * 2)); done; truncate -s $$p rpi4-sd-qemu.img
|
||||
end=$$(PATH=$$PATH:/usr/sbin:/sbin sfdisk -d montauk-rpi4.img | awk -F'[=,]' '/montauk-rpi4.img1/ {print $$2 + $$4}'); \
|
||||
dd if=montauk-rpi4.img of=rpi4-esp-qemu.img bs=512 count=$$end status=none
|
||||
PATH=$$PATH:/usr/sbin:/sbin sfdisk --quiet --delete rpi4-esp-qemu.img 2 2>/dev/null || true
|
||||
qemu-system-aarch64 \
|
||||
-M virt,gic-version=2 \
|
||||
-cpu cortex-a72 \
|
||||
-device ramfb \
|
||||
-device qemu-xhci,id=xhci \
|
||||
-device usb-hub,bus=xhci.0,port=1 \
|
||||
-device usb-kbd,bus=xhci.0,port=1.1 \
|
||||
-device usb-mouse,bus=xhci.0,port=1.2 \
|
||||
-bios $(AARCH64_EFI_FIRMWARE) \
|
||||
-drive if=none,id=esp,format=raw,file=rpi4-esp-qemu.img \
|
||||
-device virtio-blk-pci,drive=esp \
|
||||
-device sdhci-pci \
|
||||
-drive if=none,id=sd,format=raw,file=rpi4-sd-qemu.img \
|
||||
-device sd-card,drive=sd \
|
||||
-serial stdio \
|
||||
$(QEMUFLAGS)
|
||||
|
||||
.PHONY: run-riscv64
|
||||
@@ -196,13 +265,23 @@ ports: programs
|
||||
fi
|
||||
|
||||
.PHONY: ramdisk
|
||||
ifeq ($(KERNEL_ONLY),1)
|
||||
ramdisk: limine/limine kernel
|
||||
rm -rf .kernel-only-root
|
||||
mkdir -p .kernel-only-root/boot/limine
|
||||
cp -v limine/$(EFI_LOADER_$(ARCH)) limine.conf .kernel-only-root/boot/limine/
|
||||
cp -v kernel/bin-$(ARCH)/kernel .kernel-only-root/boot/
|
||||
./scripts/mkramdisk.sh .kernel-only-root ramdisk.tar
|
||||
rm -rf .kernel-only-root
|
||||
else
|
||||
ramdisk: limine/limine kernel programs
|
||||
mkdir -p programs/bin/boot/limine
|
||||
cp -v limine/BOOTX64.EFI programs/bin/boot/limine/
|
||||
cp -v limine.conf programs/bin/boot/limine/
|
||||
cp -v kernel/bin-$(ARCH)/kernel programs/bin/boot/
|
||||
rm -f programs/bin/boot/ramdisk.tar
|
||||
./scripts/mkramdisk.sh programs/bin ramdisk.tar
|
||||
mkdir -p $(PROGRAMS_BIN)/boot/limine
|
||||
cp -v limine/$(EFI_LOADER_$(ARCH)) $(PROGRAMS_BIN)/boot/limine/
|
||||
cp -v limine.conf $(PROGRAMS_BIN)/boot/limine/
|
||||
cp -v kernel/bin-$(ARCH)/kernel $(PROGRAMS_BIN)/boot/
|
||||
rm -f $(PROGRAMS_BIN)/boot/ramdisk.tar
|
||||
./scripts/mkramdisk.sh $(PROGRAMS_BIN) ramdisk.tar
|
||||
endif
|
||||
|
||||
$(IMAGE_NAME).iso: limine/limine kernel ramdisk
|
||||
rm -rf iso_root
|
||||
@@ -252,13 +331,17 @@ ifeq ($(ARCH),loongarch64)
|
||||
endif
|
||||
rm -rf iso_root
|
||||
|
||||
# Example and regression-test programs that are built into 0:/os for
|
||||
# development but have no place on a release image.
|
||||
RELEASE_EXCLUDE_PROGRAMS := rusthello.elf futextest.elf memtest.elf
|
||||
|
||||
.PHONY: release
|
||||
release: limine/limine kernel ramdisk
|
||||
rm -rf iso_root .release-tmp
|
||||
mkdir -p iso_root/boot
|
||||
cp kernel/bin-$(ARCH)/kernel iso_root/boot/
|
||||
strip iso_root/boot/kernel
|
||||
cp -r programs/bin .release-tmp
|
||||
$(STRIP) iso_root/boot/kernel
|
||||
cp -r $(PROGRAMS_BIN) .release-tmp
|
||||
# A release ISO ships only in-tree apps, so drop any out-of-tree port bundles
|
||||
# that happen to be staged in programs/bin/apps (each carries a .port marker).
|
||||
# Pass RELEASE_WITH_PORTS=1 to deliberately cut a ports-included image.
|
||||
@@ -281,11 +364,12 @@ ifeq ($(RELEASE_WITHOUT_SDK),1)
|
||||
rm -rf .release-tmp/sdk/bin .release-tmp/sdk/lib .release-tmp/sdk/include \
|
||||
.release-tmp/sdk/libexec .release-tmp/sdk/x86_64-montauk
|
||||
endif
|
||||
find .release-tmp -name '*.elf' -exec strip {} +
|
||||
rm -f $(addprefix .release-tmp/os/,$(RELEASE_EXCLUDE_PROGRAMS))
|
||||
find .release-tmp -name '*.elf' -exec $(STRIP) {} +
|
||||
# The ramdisk carries its own copy of the kernel at 0:/boot/kernel -- that is
|
||||
# the image the Installer lays down on the target disk -- and it is not named
|
||||
# *.elf, so the find above misses it. Unstripped it is ~6 MB against ~0.7 MB.
|
||||
strip .release-tmp/boot/kernel
|
||||
$(STRIP) .release-tmp/boot/kernel
|
||||
./scripts/mkramdisk.sh .release-tmp iso_root/boot/ramdisk.tar
|
||||
rm -rf .release-tmp
|
||||
mkdir -p iso_root/boot/limine
|
||||
@@ -331,6 +415,26 @@ ifeq ($(ARCH),loongarch64)
|
||||
endif
|
||||
rm -rf iso_root
|
||||
|
||||
# Raspberry Pi 4/400 SD card image (UEFI firmware + Limine + MontaukOS).
|
||||
# ROOTFS=ext2 (default) puts the system on an ext2 partition read through the
|
||||
# kernel's SD driver; ROOTFS=ramdisk has Limine load ramdisk.tar instead.
|
||||
# Phony so switching ROOTFS always rebuilds the image.
|
||||
ROOTFS ?= ext2
|
||||
.PHONY: rpi4-sd montauk-rpi4.img
|
||||
rpi4-sd: montauk-rpi4.img
|
||||
|
||||
montauk-rpi4.img: limine/limine kernel ramdisk
|
||||
ifneq ($(ARCH),aarch64)
|
||||
$(error rpi4-sd needs ARCH=aarch64)
|
||||
endif
|
||||
./scripts/mk-rpi4-sd.sh kernel/bin-$(ARCH)/kernel ramdisk.tar $@ $(ROOTFS) $(PROGRAMS_BIN)-rpi
|
||||
|
||||
# Graphical imager: write montauk-rpi4.img to an SD card, or update the
|
||||
# installation on one in place (scripts/montauk-imager.py, docs/raspberry-pi.md).
|
||||
.PHONY: rpi4-imager
|
||||
rpi4-imager:
|
||||
./scripts/montauk-imager.py
|
||||
|
||||
$(IMAGE_NAME).hdd: limine/limine kernel ramdisk
|
||||
rm -f $(IMAGE_NAME).hdd
|
||||
dd if=/dev/zero bs=1M count=0 seek=64 of=$(IMAGE_NAME).hdd
|
||||
@@ -362,7 +466,7 @@ endif
|
||||
clean:
|
||||
$(MAKE) -C kernel clean
|
||||
# $(MAKE) -C programs clean
|
||||
rm -rf iso_root .release-tmp $(IMAGE_NAME).iso $(IMAGE_NAME).hdd ramdisk.tar
|
||||
rm -rf iso_root .release-tmp $(IMAGE_NAME).iso $(IMAGE_NAME).hdd montauk-rpi4.img rpi4-sd-qemu.img rpi4-esp-qemu.img ramdisk.tar
|
||||
|
||||
.PHONY: distclean
|
||||
distclean:
|
||||
|
||||
@@ -1,3 +1,78 @@
|
||||
MontaukOS 0.2.0 (October 14, 2026)
|
||||
(278b20825815ec04a22fd1c2fcfedbd38e116e6d)
|
||||
|
||||
New
|
||||
====
|
||||
* Capability (application permissions) model added
|
||||
* Consent prompts for missing app permissions
|
||||
* New Capabilities app for reviewing and managing app permissions
|
||||
* Unified system status codes; the shell and Files now print a readable message on failure
|
||||
* Text is now UTF-8 throughout, with Unicode key events and dead keys
|
||||
* Keyboard layout support, with English, Norwegian, Czech, Swedish, and Danish layouts
|
||||
* NumLock support
|
||||
* Battery percentage reporting via ACPI, with a battery indicator in the desktop panel
|
||||
* Display brightness control, including on hybrid graphics laptops, with a slider in the desktop panel
|
||||
* Discrete GPUs are powered down automatically on hybrid graphics laptops
|
||||
* ext2 block cache with readahead, speeding up disk-heavy work such as on-OS compilation
|
||||
* Userspace system logging: klog is now syslog, logs identify the user and program, and system services log through montauk::service_log
|
||||
* The system log is written to disk (0:/state/logs) during shutdown
|
||||
* Desktop sessions now own the processes started in them, and those processes are ended on logout
|
||||
* Rust userspace support (no_std)
|
||||
* Futex syscalls (SYS_FUTEX_WAIT/SYS_FUTEX_WAKE) as groundwork for Rust std and pthreads
|
||||
* Process termination codes; abort() now ends the whole process from any thread
|
||||
* isatty() now reports whether a terminal is actually attached
|
||||
* Settings panel and a Pure Black theme in Terminal (the default for Console sessions)
|
||||
* Syntax highlighting for C++, HTML, CSS, and JavaScript in edit and Text Editor
|
||||
* Expanded SVG renderer
|
||||
* New vol command for listing mounted volumes
|
||||
* Revamped desktop application categories
|
||||
* Fonts, icons, and manual pages moved to 0:/os; persistent state now lives in 0:/state
|
||||
* RTL-SDR driver moved from the kernel to userspace
|
||||
|
||||
Fixed
|
||||
=====
|
||||
* Opening a directory as a file is now rejected on ext2 and FAT32, fixing on-OS gcc on installed systems
|
||||
* Fixed several syscall result and status code comparisons
|
||||
* Fixed a TLS layout bug for binaries whose TLS segment has an alignment below 16
|
||||
* A fault in one thread now ends the whole process instead of leaving its sibling threads running
|
||||
* Fixed an ACPI hardware ID endianness bug
|
||||
* Fixed a race in Wi-Fi event processing and other Wi-Fi bugs
|
||||
* DHCP no longer runs when no network device has an active link
|
||||
* Reduced idle CPU wakeups and deferred-work polling
|
||||
* Redirected consoles are now inherited by processes spawned with capabilities
|
||||
* Fixed numpad navigation
|
||||
* Fixed a timezone formatting bug
|
||||
* Fixed the login wallpaper startup delay
|
||||
* Fixed text overlap in file dialogs
|
||||
* Persisted kernel logs are no longer double-spaced
|
||||
* Files lists now scroll with the arrow keys, and volume labels are consistent
|
||||
* mbrtowc added to libc
|
||||
|
||||
Included in ISO
|
||||
================
|
||||
|
||||
Two x86_64 images are published for this release.
|
||||
montauk-0.2.0-sdk-x86_64.iso additionally carries the
|
||||
Montauk SDK (gcc, binutils, etc.)
|
||||
|
||||
* MontaukOS SMP kernel
|
||||
* Userspace bootstrap (0:/os/init.elf) and other essential system utilities
|
||||
* MontaukOS desktop environment and graphical apps
|
||||
* NetSurf web browser (0:/apps/netsurf)
|
||||
* Manual pages (0:/os/man), readable via man command
|
||||
* Service and system configuration (0:/config: init.toml, capabilities.toml, ssh.toml, ntp.toml)
|
||||
* System CA certificates (0:/os/certs/ca-certificates.crt)
|
||||
* Intel Bluetooth controller and Wi-Fi (iwlwifi) firmware (0:/os/firmware/intel)
|
||||
* Third-party license texts and notices (0:/os/licenses)
|
||||
* Tiny C Compiler (tcc) with MontaukOS headers and libraries (0:/sdk/tcc)
|
||||
* Lua 5.4 interpreter (lua) with headers and static library (0:/sdk/lua)
|
||||
* Montauk SDK - native GCC/G++ and binutils (0:/sdk) - SDK image only
|
||||
* Flat Remix icon pack
|
||||
* JetBrains Mono, Noto Serif, Roboto, and C059 Roman (open Century Schoolbook) TrueType fonts
|
||||
* DOOM engine with SDL_mixer audio support (game data not included)
|
||||
* Experimental HTTP server (httpd) and default page (0:/www/index.html)
|
||||
* Default wallpaper - blossoms by Nikhil Kumar, Unsplash (0:/os/wallpapers)
|
||||
|
||||
MontaukOS 0.1.9 (August 13, 2026)
|
||||
(a28b8ad0b2932203b812e967f35f029f060fd6b5)
|
||||
|
||||
|
||||
+10
-4
@@ -70,10 +70,16 @@ sudo apt install build-essential nasm git curl xorriso mtools gdisk qemu-system-
|
||||
|
||||
### Cross-Architecture Builds
|
||||
|
||||
To build for a different architecture, pass `ARCH=`:
|
||||
The kernel is split into architecture-neutral code and per-architecture
|
||||
ports under `kernel/src/Arch/` (see [porting.md](porting.md)). x86_64 is the
|
||||
only complete port. `aarch64` is a skeleton: the whole kernel compiles and
|
||||
links for it, but paging, exception vectors, the GIC and the timer are not
|
||||
written yet, so it does not boot. Userspace is x86_64-only.
|
||||
|
||||
Non-x86 kernels are built with LLVM (`clang`, plus `ld.lld` for the link):
|
||||
|
||||
```bash
|
||||
make ARCH=aarch64
|
||||
make ARCH=riscv64
|
||||
make ARCH=loongarch64
|
||||
make kernel ARCH=aarch64 # kernel/bin-aarch64/kernel
|
||||
make kernel ARCH=aarch64 KERNEL_LD=/path/to/ld.lld
|
||||
./scripts/check-arch-neutral.sh # boundary check + aarch64 compile
|
||||
```
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
# Porting the Kernel to a New Architecture
|
||||
|
||||
The kernel is written so that everything outside `kernel/src/Arch/` is
|
||||
architecture-neutral. A port is one directory, `kernel/src/Arch/<arch>/`,
|
||||
implementing the interfaces declared in `kernel/src/Arch/*.hpp` (plus a few
|
||||
interface headers elsewhere, listed below). The build compiles the generic
|
||||
tree plus `Arch/$(ARCH)/` only.
|
||||
|
||||
`scripts/check-arch-neutral.sh` enforces the boundary and should pass before
|
||||
every commit that touches the kernel:
|
||||
|
||||
- no inline assembly, assembly sources or architecture `#if`s outside
|
||||
`src/Arch/` (the Limine adapter is the one exception: `limine.h` has
|
||||
per-architecture SMP structures);
|
||||
- no generic file includes a port's private headers (`<Arch/x86_64/...>`);
|
||||
- with clang installed, the kernel compiles for `aarch64` and every symbol
|
||||
the generic code references is defined by the port.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
kernel/src/Arch/
|
||||
Arch.hpp target selection (MONTAUK_ARCH_*), MONTAUK_ARCH_NAME
|
||||
Cpu.hpp inline CPU primitives (interrupt state, halt, barriers,
|
||||
timestamp counter, TLB, page-table root)
|
||||
Interrupts.hpp IRQ slots, handler registration, legacy line routing,
|
||||
MSI message composition, IPIs
|
||||
Smp.hpp Smp::CpuData (generic fields + arch sub-structs),
|
||||
per-CPU base access, InitBsp / BootAPs
|
||||
Context.hpp thread context: FPU area, initial kernel stack,
|
||||
SwitchContext, ActivateThread, EnterUserMode
|
||||
Abi.hpp user ABI: ELF machine, relocation kinds, TLS variant,
|
||||
exit stub, initial user stack, EFI calling convention
|
||||
Syscall.hpp SyscallFrame contract and entry-point installation
|
||||
Timer.hpp per-CPU tick timer hardware
|
||||
Platform.hpp boot-sequence hooks, platform device list, reboot
|
||||
CpuPower.hpp frequency scaling / thermal governor (may be no-ops)
|
||||
x86_64/ the complete port
|
||||
aarch64/ the port in progress (see below)
|
||||
```
|
||||
|
||||
Other interfaces a port implements:
|
||||
|
||||
| Header | What the port provides |
|
||||
|---|---|
|
||||
| `Memory/Paging.hpp` | the whole `Memory::VMM::Paging` class: page-table format, kernel and user mappings, address-space creation and teardown |
|
||||
| `Io/IoPort.hpp` | port I/O (`Io::In8`...), or the absent-device fallback with `HasPortIo = false` |
|
||||
| `ACPI/AcpiSleep.hpp` | `Initialize` / `IsS3Available` / `Suspend` (S3 via the FACS waking vector is an x86 mechanism) |
|
||||
| `Common/Panic.hpp` | `Arch::PanicFrame` and `Arch::PrintPanicFrame` |
|
||||
|
||||
Interface headers that need inline definitions include the port's header of
|
||||
the same name at their end (`Arch/Cpu.hpp` -> `Arch/<arch>/Cpu.hpp`); those
|
||||
are `Cpu.hpp`, `IoPort.hpp`, `PerCpu.hpp`, `Context.hpp`, `Abi.hpp` and
|
||||
`SyscallFrame.hpp`. Everything else is an ordinary out-of-line function.
|
||||
|
||||
## Boot sequence
|
||||
|
||||
`kmain` (`Boot/Main.cpp`) owns the order and calls the port at fixed points:
|
||||
|
||||
1. `Arch::EarlyInitialize()` - descriptor tables, FPU/SIMD enable. No allocator.
|
||||
2. Frame allocator and heap come up (generic).
|
||||
3. `Arch::InitializeTraps()` - exception vectors.
|
||||
4. `Paging::Init()` switches to the kernel's own page tables, then
|
||||
`Arch::InitializeMemoryAttributes()` (x86: PAT for write-combining).
|
||||
5. ACPI tables are parsed (generic). `Arch::InitializeInterrupts(xsdt)` brings
|
||||
up interrupt controllers, calls `Smp::InitBsp()`, idle and power
|
||||
management; kmain then calls `Arch::EnableInterrupts()`.
|
||||
6. PCI, AML, drivers (generic); `Timekeeping::TimerInitialize()` registers
|
||||
`IRQ_TIMER` and calls `Arch::Timer::InitializeBsp(1)`.
|
||||
7. `Arch::InitializePlatformDevices()` (x86: PS/2).
|
||||
8. `Arch::PrepareUserMode()`, `montauk::abi::InitializeSyscalls()` (calls
|
||||
`Arch::InitializeSyscallEntry()`), scheduler, IPC.
|
||||
9. `Smp::BootAPs()`, `Arch::LateInitialize()`, first process, idle loop.
|
||||
|
||||
## Assumptions the generic kernel makes
|
||||
|
||||
- 64-bit, little-endian, 4 KiB pages.
|
||||
- A higher-half direct map of physical memory (`Memory::HHDM`), as Limine
|
||||
provides on every architecture.
|
||||
- User space in the lower half of each address space, kernel in the upper
|
||||
half; an address space is identified by the physical address of its root
|
||||
table (on ARM, the TTBR0 table; the kernel half lives in TTBR1).
|
||||
- Firmware tables via ACPI (`Hal::ACPI`); device-tree-only platforms would
|
||||
need a discovery path first.
|
||||
- Interrupts are numbered IRQ slots (`Arch::IRQ_COUNT`); slot numbers for
|
||||
MSI-capable drivers are fixed in the drivers.
|
||||
|
||||
## The aarch64 port
|
||||
|
||||
Real: CPU primitives (DAIF, WFI, `CNTVCT_EL0`, TLBI, TTBR0), per-CPU data via
|
||||
`TPIDR_EL1`, the context switch and EL0 entry (`Context.S`), TLS through
|
||||
`TPIDR_EL0`, the ELF/TLS/exit-stub ABI (syscall number in x8), the IRQ slot
|
||||
dispatch table (`ArchIrqDispatch`), panic-frame printing, and:
|
||||
|
||||
- Exception vectors (`Vectors.S`, installed in `EarlyInitialize`). Every
|
||||
exception saves one 288-byte frame that is both `Arch::PanicFrame` and
|
||||
`Arch::SyscallFrame`. EL0 `svc` goes to `SyscallDispatch`; other
|
||||
synchronous exceptions go to `ArchSyncException` (`Exceptions.cpp`), which
|
||||
grows user stacks and lazy anonymous memory, kills faulting processes with
|
||||
a crash report, and panics on kernel faults.
|
||||
- Translation tables (`Paging.cpp`): 4 KiB granule, 48-bit VAs. The kernel
|
||||
half lives in its own TTBR1 table; `KernelRoot()` is the TTBR0 table of
|
||||
kernel contexts (EFI identity mappings only, so null pointers fault). The
|
||||
HHDM maps only the memory map's RAM and firmware regions, never the holes
|
||||
between them (device space on ARM). MAIR: 0 Normal WB, 1 Normal NC
|
||||
(`MapWC`), 2 Device-nGnRE (`MapMMIO`).
|
||||
- GICv2 (`Interrupts.cpp`, from the MADT): IRQ slots map onto INTIDs --
|
||||
`IRQ_TIMER` onto the generic timer's PPI, the MSI slots (24-45) onto the
|
||||
SPIs of a GICv2m MSI frame (`ComposeMsi`), the IPI slots onto SGIs 0/1,
|
||||
and the platform lines (1-23) onto the SPIs `AssignPlatformLine` gives
|
||||
them (see "Interrupt lines" below).
|
||||
Interrupts are completed (EOI) before the handler runs, as on x86; a
|
||||
level-triggered source drops its line first in an ack hook.
|
||||
- Generic timer tick (`Timer.cpp`): the EL1 virtual timer at
|
||||
`CNTFRQ_EL0`, its PPI from the GTDT, re-armed from the ack hook.
|
||||
- PSCI `SYSTEM_RESET` / `SYSTEM_OFF` (`Arch::Reboot` / `Arch::PowerOff`)
|
||||
through the conduit the FADT's ARM_BOOT_ARCH names. Hardware-reduced
|
||||
ACPI has no PM1 block, so `AcpiShutdown` leaves power-off to the port.
|
||||
- `Arch::SyncInstructionCache` after the ELF loader writes code: ARM
|
||||
instruction fetch does not snoop data writes (QEMU hides this; real
|
||||
cores do not). EL0 may maintain caches itself (`SCTLR_EL1.UCI`).
|
||||
|
||||
- SMP (`Smp.cpp`): Limine wakes each secondary CPU (PSCI `CPU_ON`
|
||||
underneath) on the bootloader's tables; it programs its own system
|
||||
registers and the kernel translation regime (`Bringup.hpp`), its banked
|
||||
GIC CPU interface and timer PPI, then idles until a reschedule SGI.
|
||||
TLB maintenance is broadcast (`tlbi ...is`).
|
||||
|
||||
The generic kernel is compiled `-fsigned-char` for AArch64: it was written
|
||||
for x86's signed plain `char`.
|
||||
|
||||
Building it needs `clang` and `ld.lld`; without a system `ld.lld` the
|
||||
kernel makefile uses the one rustup ships
|
||||
(`~/.rustup/toolchains/*/lib/rustlib/*/bin/gcc-ld/ld.lld`), and `KERNEL_LD=`
|
||||
overrides both. `make run ARCH=aarch64` boots the full image (desktop, apps,
|
||||
USB keyboard and mouse) on QEMU `virt` with a GICv2 and Cortex-A72 cores, the
|
||||
Raspberry Pi 4/400's configuration; it needs `qemu-system-aarch64` and
|
||||
`qemu-efi-aarch64`, and the userspace compiler
|
||||
(`./toolchain/build-montauk-toolchain.sh aarch64`). `KERNEL_ONLY=1` builds an
|
||||
image without userspace.
|
||||
|
||||
The kernel console is mirrored to the serial port the ACPI SPCR table names
|
||||
(PL011, SBSA UART, 16550 or the BCM2835 mini UART; `Drivers/Serial/Uart.cpp`),
|
||||
including the log written before the port was found. On QEMU that is
|
||||
`-serial stdio`.
|
||||
|
||||
## Interrupt lines
|
||||
|
||||
Drivers for PCI functions without MSI read the Interrupt Line register and
|
||||
pass it to `Arch::EnableLegacyIrq`. On x86 the firmware fills that register
|
||||
with the function's I/O APIC input (`Arch::PlatformLinesAreFixed()`). On
|
||||
other platforms `Pci::AssignInterruptLines` (`Pci/PciAcpi.cpp`) fills it
|
||||
instead: it finds each pin's global system interrupt in the root bridge's
|
||||
`_PRT` (swizzling through PCI-to-PCI bridges, resolving PNP0C0F link
|
||||
devices through `_CRS`), binds the GSI to a platform line with
|
||||
`Hal::GsiRouting::LineForGsi` and writes the line number into the register.
|
||||
Devices that ACPI describes directly (platform devices) get their line from
|
||||
`LineForGsi` too.
|
||||
|
||||
## DMA
|
||||
|
||||
`Memory/Dma.hpp` serves devices whose DMA does not snoop the CPU caches
|
||||
(ACPI `_CCA = 0`): pages for rings and descriptors come uncached, and
|
||||
`SyncForDevice` / `SyncForCpu` clean and invalidate streaming buffers
|
||||
around a transfer (`Arch::CleanDataCache` / `InvalidateDataCache`). For
|
||||
coherent devices every call is the plain allocation or a no-op. The xHCI
|
||||
driver uses it; it also orders its TRB writes with `Arch::DmaWriteFence`
|
||||
(the cycle bit last, a control TD's Setup TRB after its Data and Status
|
||||
TRBs) and its event reads with `Arch::DmaReadFence`. Those fences cost
|
||||
nothing on x86 and are required on ARM, where stores to normal memory and
|
||||
device registers may otherwise be observed out of order. The other PCI
|
||||
drivers (AHCI, NVMe, e1000, HDA) do not have them yet: they work on QEMU,
|
||||
which does not reorder, but need them before they meet real ARM hardware.
|
||||
|
||||
## Raspberry Pi 4 / 400
|
||||
|
||||
See `docs/raspberry-pi.md`: `make ARCH=aarch64 rpi4-sd`, the firmware it
|
||||
expects and how the Pi's hardware is reached.
|
||||
|
||||
## Userspace
|
||||
|
||||
The programs are architecture-neutral C/C++ built by `<arch>-montauk-gcc`
|
||||
(see `toolchain/README.md`). `programs/mk/arch.mk` holds everything
|
||||
architecture-specific in the build: the toolchain prefix, FP/SIMD flag sets
|
||||
(`ARCH_FP_FLAGS` for GUI apps, `ARCH_NOFP_FLAGS` for system programs) and the
|
||||
output layout. x86_64 keeps `bin/`, `obj/` and libraries next to their
|
||||
sources; other architectures build beside them into `bin-<arch>/`,
|
||||
`obj-<arch>/` and `lib/<name>/build-<arch>/`, so both can be built from one
|
||||
tree. The few architecture-specific sources are the raw system-call
|
||||
wrappers (`#if defined(__x86_64__)` / `__aarch64__` blocks in
|
||||
`montauk/syscall.h`, `libc/montauk.h`, `lib/libc/libc.c` and
|
||||
`lib/libc/crt/crt1.c`), `lib/libc/arch/setjmp-<arch>.S` and the scanout
|
||||
store fence in `gui/framebuffer.hpp`.
|
||||
|
||||
Not built for AArch64: tcc (it generates x86 code), the on-OS SDK and the
|
||||
Rust programs (their `x86_64-unknown-montauk` target).
|
||||
|
||||
## Adding another architecture
|
||||
|
||||
1. Create `kernel/src/Arch/<arch>/` with the six inline headers listed above
|
||||
and stubs for the out-of-line functions (copy `aarch64/` as a template).
|
||||
2. Add the target to `Arch/Arch.hpp` and to the `#if` chains at the end of
|
||||
the dispatching interface headers.
|
||||
3. Add `KernelStartSymbol` / `KernelEndSymbol` to `kernel/linker-<arch>.ld`,
|
||||
and the `ld.lld` emulation to `LD_EMULATION_<arch>` in `kernel/GNUmakefile`.
|
||||
4. Teach `Boot/Protocols/LimineProtocol.cpp` the architecture's MP fields.
|
||||
5. Run `./scripts/check-arch-neutral.sh <arch>` until it passes.
|
||||
@@ -0,0 +1,344 @@
|
||||
# Raspberry Pi 4 / 400
|
||||
|
||||
MontaukOS runs on the Raspberry Pi 4 and 400 through the Pi Firmware Task
|
||||
Force's UEFI firmware ([pftf/RPi4](https://github.com/pftf/RPi4), an EDK2
|
||||
build) in ACPI mode, booting Limine like any other UEFI machine.
|
||||
|
||||
## Building and writing the SD card
|
||||
|
||||
```bash
|
||||
./toolchain/build-montauk-toolchain.sh aarch64 # once
|
||||
make ARCH=aarch64 rpi4-sd # -> montauk-rpi4.img
|
||||
sudo dd if=montauk-rpi4.img of=/dev/sdX bs=4M conv=fsync
|
||||
```
|
||||
|
||||
Or use the imager, which also updates a card in place:
|
||||
|
||||
```bash
|
||||
make rpi4-imager # = ./scripts/montauk-imager.py
|
||||
```
|
||||
|
||||
### The imager
|
||||
|
||||
`scripts/montauk-imager.py` is a Tk window (`python3-tk`) that lists the SD
|
||||
cards and USB drives attached (never the host's own disks) and what is on
|
||||
them, and either writes a fresh image or updates the MontaukOS on a card. It
|
||||
runs as the user and does the writing by starting itself again through
|
||||
`pkexec` (it asks for the password each time). The same operations work from
|
||||
a terminal: `list`, and as root `flash`, `update` and `expand`
|
||||
(`--help` for the options). Images can be raw or `.gz`, `.xz` or `.zip`
|
||||
compressed; **Build** runs `make ARCH=aarch64 rpi4-sd`.
|
||||
|
||||
- **Write a fresh image** erases the card, writes the image, clears the last
|
||||
MiB of the card (a backup GPT from an earlier OS) and reads the card back to
|
||||
verify it. *Expand* grows the root to fill the card right away instead of
|
||||
leaving it to `firstboot.elf`.
|
||||
- **Update** leaves the partition table alone and finds both partitions by
|
||||
label, so a root that has grown to fill the card (or was resized by hand)
|
||||
stays as it is. On the ESP every file is written except `RPI_EFI.fd`, which
|
||||
holds the firmware's own settings (*Update the UEFI firmware* replaces it);
|
||||
a changed `config.txt` is kept as `config.txt.bak`. On the root, `os/`,
|
||||
`apps/`, `sdk/`, `boot/` get the image's new and changed files; `config/`
|
||||
and `www/` only get files the card lacks, `users/` and `state/` are not
|
||||
touched. `init.toml` becomes the image's, keeping each service's `enabled`
|
||||
from the card and appending services only the card has; `capabilities.toml`
|
||||
becomes the image's plus grants only the card has (old copies: `.old`).
|
||||
`os/firstboot.elf` is only written if the card still has it, so a grown
|
||||
card does not grow again. *Remove system files the new image no longer
|
||||
ships* prunes `os/`, `apps/`, `sdk/` and `boot/` (keeping `os/btkeys.bin`,
|
||||
the Bluetooth bonds). Unchanged files are skipped and each file is
|
||||
replaced by a rename; the volume is checked with `e2fsck` before and after,
|
||||
and its features against what `Fs/Ext2.cpp` mounts.
|
||||
- **Expand** (a host-side first boot) grows the root partition to the end of
|
||||
the card, at most 1 TiB, and the volume with `resize2fs`; `firstboot.elf`
|
||||
then finds nothing to do and deletes itself.
|
||||
|
||||
While it works, a runtime udev rule keeps the desktop from automounting the
|
||||
card's partitions (Linux writes to an ext2 volume as soon as it mounts it).
|
||||
|
||||
`scripts/mk-rpi4-sd.sh` downloads the firmware release once into
|
||||
`rpi4-firmware/` (`PFTF_VERSION=v1.53` by default) and builds an MBR image:
|
||||
|
||||
| Partition | Type | Contents |
|
||||
|---|---|---|
|
||||
| 1 | 0xEF, FAT32 | the firmware files, Limine (`EFI/BOOT/BOOTAA64.EFI`), `boot/kernel` |
|
||||
| 2 | 0x83, ext2, label `MONTAUKROOT` | the system tree (the contents of `ramdisk.tar`) |
|
||||
|
||||
Limine loads only the kernel; the `limine.conf` on the card has no ramdisk
|
||||
module. Finding no ramdisk, the kernel mounts the partition labelled
|
||||
`MONTAUKROOT` as `0:/` (`Fs/Boot.cpp`), read through the SD driver
|
||||
(`Drivers/Storage/Sdhci`), and the ESP after it. Files load as they are
|
||||
used instead of the firmware reading the whole 80 MB tree before boot.
|
||||
The card can also boot from a USB card reader (or the image from a USB
|
||||
stick): the root then comes through `Drivers/USB/MassStorage.cpp`. A USB
|
||||
root can register after the boot scan (a SuperSpeed link still training, a
|
||||
reader still bringing its card up), so a boot that finds no `MONTAUKROOT`
|
||||
waits up to 10 s for one, servicing USB hot-plug meanwhile.
|
||||
Unlike the ramdisk, the root is persistent: changes to `0:/` survive a
|
||||
reboot, so shut down from the desktop rather than pulling power.
|
||||
The image is sized to its contents (`ROOT_FREE_MIB`, default 64, sets the
|
||||
free space on the root) and grows to fill the card on first boot.
|
||||
|
||||
### First boot: growing the root
|
||||
|
||||
Only the SD image ships `0:/os/firstboot.elf` (`programs/src/firstboot`,
|
||||
built into `programs/bin-aarch64-rpi/`, which the script lays over the
|
||||
system tree), and the script adds its init service, ahead of all others,
|
||||
and its capability grant (`storage_admin`, `system_image`) to the image's
|
||||
`init.toml` and `capabilities.toml`. On first boot it shows a status card
|
||||
in the login screen's style while it calls `SYS_FS_GROW` on drive 0 in a
|
||||
loop:
|
||||
|
||||
1. The kernel grows the MBR entry of the root partition as far as the
|
||||
filesystem can use (to the end of the card, or 1 TiB), on a 1 MiB
|
||||
boundary (`Gpt::ResizeMbrPartition`).
|
||||
2. The ext2 driver grows the mounted volume (`Fs/Ext2.cpp`, "Online
|
||||
grow"): each call writes the bitmaps and zeroed inode tables of a few
|
||||
new block groups past the old end, then the last call commits the
|
||||
bitmaps, descriptor table and superblock. The descriptor table grows
|
||||
into the blocks `mke2fs -O resize_inode -E resize=2^28` reserved; the
|
||||
leftover reservation is freed and `resize_inode` cleared, so the volume
|
||||
grows once.
|
||||
|
||||
The new groups' inode tables are the bulk of the writes (0.1% of the card,
|
||||
one inode per 256 KiB), written as long multi-block runs. Once the volume has grown, `firstboot.elf` deletes
|
||||
itself and init skips its (optional) service from then on. If it fails it
|
||||
stays and the next boot tries again; an interrupted grow is safe, since the
|
||||
new groups are only referenced once the superblock is written, and a
|
||||
partition larger than its filesystem is valid.
|
||||
|
||||
`make ARCH=aarch64 rpi4-sd ROOTFS=ramdisk` builds the old layout instead:
|
||||
one FAT32 partition with `boot/ramdisk.tar`, which Limine loads through the
|
||||
firmware. It boots slowly but does not depend on the kernel's SD driver.
|
||||
|
||||
`make ARCH=aarch64 run-rpi4-sd` boots the ext2 image on QEMU with the card
|
||||
on `sdhci-pci`, so the root comes through the same driver. QEMU's UEFI has
|
||||
no SD driver, so the card's ESP is also attached on its own over virtio for
|
||||
the firmware to load Limine from. The card is padded to
|
||||
`RPI4_QEMU_CARD_MIB` (default 4096), and every run starts from a fresh
|
||||
copy, so every run is a first boot.
|
||||
|
||||
The image's `RPI_EFI.fd` comes with a **MontaukOS** boot entry first in the
|
||||
firmware's boot order (`scripts/rpi4-efi-bootorder.py` writes it into the
|
||||
release's empty variable store), so the card boots on its own instead of
|
||||
falling behind the PXE/HTTP entries the firmware creates. The entry names the
|
||||
ESP by the image's MBR disk ID and partition number, so it works from the SD
|
||||
slot or a USB drive. A card whose `RPI_EFI.fd` predates it keeps its old
|
||||
boot order until it is updated with *Update the UEFI firmware*
|
||||
(`--firmware`), which also copies the image's disk ID to the card.
|
||||
|
||||
The firmware defaults are what MontaukOS needs; leave them:
|
||||
|
||||
- **System Table Mode: ACPI.** The kernel reads ACPI only.
|
||||
- **Limit RAM to 3 GB: enabled.** The BCM2711's PCIe can only reach the
|
||||
first 3 GB (its tables say so in `_DMA`). The kernel warns at boot when
|
||||
RAM extends past a device's `_DMA` range.
|
||||
|
||||
## Serial console
|
||||
|
||||
The kernel console (the whole boot log, including what was printed before
|
||||
the port was found) is mirrored to the UART the firmware's SPCR table names.
|
||||
The firmware's `config.txt` applies the `miniuart-bt` overlay, which puts the
|
||||
PL011 on GPIO 14/15: header pins 8 (TX) and 10 (RX), ground on pin 6, 115200
|
||||
8N1, 3.3 V levels. On the Pi 400 the header is on the back.
|
||||
|
||||
## How the hardware is reached
|
||||
|
||||
| Part | Source | Driver |
|
||||
|---|---|---|
|
||||
| 4x Cortex-A72 | MADT, PSCI (TF-A, SMC) | `Arch/aarch64` SMP via Limine MP |
|
||||
| GIC-400 (GICv2) | MADT | `Arch/aarch64/Interrupts.cpp` |
|
||||
| Generic timer | GTDT | `Arch/aarch64/Timer.cpp` |
|
||||
| Display | UEFI GOP (VideoCore) | Limine framebuffer |
|
||||
| Keyboard (built in), USB ports | VL805 xHCI as ACPI `\_SB.SCB0.XHC0` (`PNP0D10`) | `Drivers/USB/Xhci.cpp` platform probe |
|
||||
| USB 2 devices (built-in keyboard too) | VIA hub on the VL805's USB 2 root port | `Drivers/USB/Hub.cpp` |
|
||||
| Reset, power-off | PSCI | `Arch::Reboot`, `Arch::PowerOff` |
|
||||
| Bluetooth (CYW43455 on the 4B, CYW43456 on the 400) | `BCM2EA6` (`\_SB.GDV0.BTH0`) on the mini UART (`BCM2836`) | `Drivers/Bluetooth/Broadcom.cpp`, `Drivers/Serial/BtUart.cpp` |
|
||||
| VideoCore firmware mailbox (radio power, clocks, board MAC) | `BCM2849` (`RPIQ`) | `Drivers/Platform/RpiFirmware.cpp` |
|
||||
| GPIO pin functions | `BCM2845` (`GPI0`) | `Drivers/Platform/RpiGpio.cpp` |
|
||||
| Ethernet (GENET v5 MAC, BCM54213PE PHY) | `BCM6E4E` (`\_SB.ETH0`) | `Drivers/Net/Genet.cpp` |
|
||||
| Wi-Fi (the same CYW4345x, SDIO) | Arasan SD controller `BCM2847`, GPIO 34-39 | `Drivers/Net/Wifi/Brcmf*.cpp` |
|
||||
|
||||
The VL805 sits behind the BCM2711's PCIe, whose config space is not ECAM
|
||||
(the root bridge's `_DSD` says `linux-ecam-quirk-id = "bcm2711"`), so the
|
||||
kernel does not enumerate PCI there. The firmware has already brought up the
|
||||
link and the VL805 and describes the controller as a platform device: a
|
||||
register block at `0x600000000`, GSI 175, and `_CCA = 0`, i.e. its DMA does
|
||||
not snoop the CPU caches. `XHC0._INI` (run by the AML interpreter) enables
|
||||
the controller's PCI memory decoding and bus mastering. The xHCI driver then
|
||||
keeps its rings and contexts in uncached memory, bounces control-transfer
|
||||
data through an uncached page, and cleans/invalidates the caller buffers of
|
||||
bulk transfers (`Memory/Dma.hpp`). It also watches the event ring from the
|
||||
timer tick, so input keeps working if the INTx line never fires.
|
||||
|
||||
The VL805 has one USB 2 root port (port 1) and four USB 3 ones. An on-board
|
||||
VIA hub (`2109:3431`) sits on the USB 2 port, and the USB 2 lines of every
|
||||
socket, and the Pi 400's internal keyboard (`04d9:0007`), hang off it: a
|
||||
keyboard or mouse is always a low/full-speed device behind a high-speed hub,
|
||||
reached through that hub's transaction translator.
|
||||
|
||||
## Bluetooth
|
||||
|
||||
The radio's Bluetooth half speaks HCI over a UART (H4) on GPIO 30-33 (CTS,
|
||||
RTS, TXD, RXD); its power switch, `BT_ON`, is line 0 of the VideoCore
|
||||
firmware's GPIO expander, reached through the mailbox. The firmware tables
|
||||
describe it as `BTH0`, whose `UARTSerialBus` names the UART it uses: with
|
||||
the `miniuart-bt` overlay the PL011 is the console, so Bluetooth has the mini
|
||||
UART. The kernel refuses to take a UART that is the SPCR console.
|
||||
|
||||
At boot the driver probe routes the pins (`ALT5` for the mini UART, `ALT3`
|
||||
for the PL011), starts the UART (interrupt driven, 16 KiB receive and
|
||||
transmit rings) and the H4 transport under the shared HCI layer. Once the
|
||||
root is mounted the idle loop finishes, as for USB adapters:
|
||||
|
||||
1. power-cycle the controller through `BT_ON`, HCI Reset at 115200 baud;
|
||||
2. Read Local Version: the ROM's LMP subversion picks the patch file,
|
||||
`0:/os/firmware/brcm/BCM4345C0.hcd` (0x6119, 4B) or `BCM4345C5.hcd`
|
||||
(0x6606, 400), shipped unmodified from Raspberry Pi OS's bluez-firmware;
|
||||
3. raise both ends to the operating speed (vendor `0xFC18`);
|
||||
4. Download Minidriver (`0xFC2E`), then every record of the `.hcd` file
|
||||
(Write RAM, then Launch RAM), as Linux's `btbcm` does;
|
||||
5. the patched firmware restarts at 115200: reset, raise the speed again.
|
||||
|
||||
The mini UART's FIFO is 8 bytes deep. Its hardware RTS is tried first; if
|
||||
the controller answers with it on, the link runs at 921600 baud, otherwise
|
||||
at 460800 without flow control (what Raspberry Pi OS uses), and at 115200
|
||||
if the UART has no interrupt. The baud rate derives from the VPU core
|
||||
clock, so `mk-rpi4-sd.sh` pins it (`core_freq=500`, `core_freq_min=500` in
|
||||
`config.txt`), as the overlay's documentation requires. A failed fast
|
||||
bring-up is retried entirely at 115200.
|
||||
|
||||
The CYW43456 reports a chip-wide default address (`43:45:C5:00:1F:AC`);
|
||||
the driver replaces it with the board's Ethernet MAC from the firmware.
|
||||
`0:/config/bluetooth.toml` still overrides both (Broadcom `0xFC01`).
|
||||
|
||||
## Ethernet
|
||||
|
||||
The BCM2711's own Gigabit MAC, GENET v5 (a UniMAC behind a DMA engine),
|
||||
talks RGMII to a BCM54213PE PHY at MDIO address 1. The firmware describes
|
||||
it as `BCM6E4E`: a 64 KiB register block, two interrupts (the driver uses
|
||||
the first, which carries the default queue), `_CCA = 0`, and a `_DSD` with
|
||||
`phy-mode` (`rgmii-rxid`: the PHY delays the receive clock, the MAC the
|
||||
transmit clock) and `brcm,max-dma-burst-size`.
|
||||
|
||||
The driver uses only the default queue (ring 16), with all 256 descriptors
|
||||
of each direction. The descriptors live in the MAC's own SRAM, so only the
|
||||
2 KiB packet buffers need cache maintenance. Received frames start two
|
||||
bytes into their buffer (`RBUF_ALIGN_2B`); neither direction uses the
|
||||
64-byte status blocks, so there is no checksum offload. The UniMAC accepts
|
||||
broadcast and its own address only (the MDF filter).
|
||||
|
||||
The interrupt only acknowledges and masks; frames are dispatched from the
|
||||
idle bottom half, which also checks the ring itself, so receive works if
|
||||
the interrupt never arrives. The PHY is polled once a second (its interrupt
|
||||
pin is not described); a link change reprograms the RGMII block and the
|
||||
UniMAC's speed and duplex. A link the firmware already negotiated is kept,
|
||||
otherwise the PHY is reset and advertises 10/100 and 1000 full duplex.
|
||||
|
||||
The MAC address is the board's, read from OTP through the firmware mailbox
|
||||
(the one the Bluetooth driver uses too); the address the firmware's own
|
||||
driver programmed is the fallback. `dhcp.elf` waits up to 10 s for a wired
|
||||
link, since autonegotiation takes about 3 s after the PHY reset.
|
||||
|
||||
## Wi-Fi
|
||||
|
||||
The radio's WLAN half is an SDIO card on the BCM2711's older SD controller,
|
||||
the Arasan-derived EMMC (`BCM2847`, `0x7E300000`); the card slot is on
|
||||
EMMC2, so the two never share a bus. Its pins are GPIO 34-39 (ALT3; clock
|
||||
without a pull, the rest pulled up, as Linux's `sdio_pins`) and its power
|
||||
switch, `WL_ON`, is line 1 of the firmware's GPIO expander. The probe
|
||||
claims the controller only while the slot is on EMMC2 (`BRCME88C` present);
|
||||
if the tables do not describe the Arasan, its address is taken as EMMC2's
|
||||
less 256 KiB.
|
||||
|
||||
It is a FullMAC radio: the firmware does the 802.11 MLME itself and the host
|
||||
sees Ethernet frames. The driver follows Linux's brcmfmac (ISC license,
|
||||
Broadcom; its notice is in `NOTICES.txt`):
|
||||
|
||||
| File | Part |
|
||||
|---|---|
|
||||
| `BrcmfSdio.cpp` | the Arasan as an SDIO host: CMD5/3/7, 4-bit at 25 MHz, CMD52/CMD53, polled PIO, 32-bit register access |
|
||||
| `BrcmfChip.cpp` | backplane window (function 1), clocks, the enumeration ROM's core list, firmware + NVRAM download, function 2, the firmware console |
|
||||
| `BrcmfProto.cpp` | SDPCM framing and transmit credit, CDC ioctls, BDC data, events, the receive pump, the transmit queue |
|
||||
| `BrcmfConnect.cpp` | preinit and CLM, escan, join, keys, link state |
|
||||
|
||||
Once the root is mounted the idle loop:
|
||||
|
||||
1. routes the pins, switches the EMMC clock on, pulses `WL_ON`, enumerates
|
||||
the card (function 1 blocks of 64 bytes, function 2 of 512);
|
||||
2. requests the ALP clock, reads the chip ID: `0x4345` revision 9 is the
|
||||
CYW43456 (Pi 400), any other revision the CYW43455 (Pi 4B), which picks
|
||||
`0:/os/firmware/brcm/brcmfmac43456-sdio.*` or `brcmfmac43455-sdio.*`
|
||||
(shipped unmodified from Raspberry Pi OS's firmware-nonfree);
|
||||
3. halts the ARM Cortex-R4, holds the 802.11 core in reset, sizes the RAM
|
||||
from the CR4's banks, writes the firmware at `0x198000` and the NVRAM
|
||||
(stripped to `key=value\0` pairs and a length token) at the top of RAM,
|
||||
reading both back to verify, puts the reset vector at address 0 and
|
||||
releases the CPU;
|
||||
4. waits for the HT clock, sends the SDPCM protocol version through the
|
||||
mailbox, enables function 2, the F2 watermark and MES busy signalling of
|
||||
Linux's CYW43455 settings, save/restore if the chip has it, and the card
|
||||
interrupt;
|
||||
5. reads `cur_etheraddr` (the NVRAM's shared placeholder address
|
||||
`b8:27:eb:74:f2:6c` is replaced with the board address + 1, as on
|
||||
Raspberry Pi OS), loads the CLM blob (`clmload`, 1400-byte chunks: these
|
||||
firmware builds carry no regulatory data of their own), turns off glom,
|
||||
roaming, ARP/ND offload and power save, enables the events it handles and
|
||||
brings the interface up (`WLC_UP`).
|
||||
|
||||
Nothing uses the controller's interrupt: the bus is swept from the idle
|
||||
loop (and from the timer tick when the cores never idle), every 10 ms while
|
||||
a scan or a connection is active and every 250 ms otherwise, and at once when
|
||||
the controller shows the card interrupt. Every sweep reads the SDIO core's
|
||||
interrupt status, then every frame waiting in function 2 (a 4-byte header
|
||||
read, then the rest). Ioctls wait for transmit credit and their answer by
|
||||
sweeping the bus themselves. Frames are delivered with the bus unlocked, and
|
||||
transmitting never waits for the bus: data frames go into a 32-frame queue
|
||||
that whoever holds the bus, or the next sweep, sends.
|
||||
|
||||
Scans are escans (all channels, active, firmware dwell times); each result
|
||||
event carries a `brcmf_bss_info_le` whose information elements go into the
|
||||
same scan table the Intel driver fills from beacons. A join sets `wpaie`
|
||||
(the RSN element our supplicant builds), `wpa_auth`, `auth` and `wsec`, then
|
||||
`join` with the BSSID and the chanspec escan reported. The 4-way handshake
|
||||
runs in the host's supplicant (`Wpa.cpp`) as for the Intel adapter: EAPOL
|
||||
frames are ordinary Ethernet frames here, and the keys go to the firmware
|
||||
with `wsec_key` once message 4 has left the transmit queue. The radio does
|
||||
AES-CCMP only: GCMP networks are refused, as are WPA3-only (SAE), WEP and
|
||||
TKIP ones.
|
||||
|
||||
Unanswered ioctls (a leaky bucket of three), a halt reported through
|
||||
the mailbox, or repeated bus errors mark the radio failed: the firmware's
|
||||
console and any trap record are printed to the kernel log (`WiFi-FW`), and
|
||||
the facade's recovery powers the radio off and repeats the bring-up (at
|
||||
most three times).
|
||||
|
||||
`tests/wifi/run.sh` drives `BrcmfProto.cpp` and `BrcmfConnect.cpp` with the
|
||||
real supplicant against a simulated firmware: preinit and the CLM chunks,
|
||||
escan parsing, open and WPA2 joins (an independent authenticator checks
|
||||
messages 2 and 4 and the installed keys), the wrong passphrase, refusals,
|
||||
join timeouts and a firmware that stops answering.
|
||||
|
||||
## Not supported yet
|
||||
|
||||
- SD high-speed mode: the driver transfers by polled PIO at 25 MHz on a
|
||||
4-bit bus (multi-block CMD18/CMD25 with Auto CMD12), at most 12.5 MB/s;
|
||||
switching the card to 50 MHz (CMD6) would double that.
|
||||
- Wi-Fi beyond 802.11 station mode on one interface: no access point mode,
|
||||
no SDIO high speed (50 MHz), no glom, no firmware supplicant.
|
||||
- Audio (HDMI via VideoCore, the 3.5 mm jack via PWM).
|
||||
|
||||
## Status
|
||||
|
||||
Bluetooth works on a Pi 400 (CYW43456), including A2DP music to
|
||||
headphones. Ethernet has not run yet: QEMU has no GENET model. Wi-Fi has
|
||||
not run on hardware yet either (QEMU has no model of the radio); only its
|
||||
protocol and connection layers are tested, in the host harness. It is untested on a Pi 4B (CYW43455), and QEMU has no model of
|
||||
the radio.
|
||||
|
||||
Everything else above is verified on QEMU only, including the Pi-specific path:
|
||||
the firmware's own XHC0 table, retargeted at QEMU's xHCI and loaded with the
|
||||
`bcm2711` quirk on QEMU's root bridge, boots to the desktop with non-coherent
|
||||
DMA. Real hardware has not been tried yet. The ext2 root on the SD card boots on QEMU (`run-rpi4-sd`); the
|
||||
first-boot grow has been tested only against the kernel's ext2 code in a
|
||||
host harness (images grown, then checked with `e2fsck`).
|
||||
+53
-2
@@ -27,6 +27,20 @@ ifeq ($(ARCH),x86_64)
|
||||
endif
|
||||
endif
|
||||
|
||||
# The other architectures have no in-tree cross GCC: default to LLVM, which
|
||||
# cross-compiles to every target (linking needs ld.lld; see docs/porting.md).
|
||||
ifneq ($(ARCH),x86_64)
|
||||
ifeq ($(filter command line environment,$(origin CC)),)
|
||||
CC := clang
|
||||
endif
|
||||
ifeq ($(filter command line environment,$(origin CXX)),)
|
||||
CXX := clang++
|
||||
endif
|
||||
ifeq ($(filter command line environment,$(origin AR)),)
|
||||
AR := ar
|
||||
endif
|
||||
endif
|
||||
|
||||
# User controllable C compiler command.
|
||||
CC ?= cc
|
||||
|
||||
@@ -139,8 +153,10 @@ ifeq ($(ARCH),aarch64)
|
||||
override CXX += \
|
||||
-target aarch64-unknown-none
|
||||
endif
|
||||
# Plain char is signed on x86; the generic kernel is written for that.
|
||||
override SHARED_FLAGS += \
|
||||
-mgeneral-regs-only
|
||||
-mgeneral-regs-only \
|
||||
-fsigned-char
|
||||
override LDFLAGS += \
|
||||
-Wl,-m,aarch64elf
|
||||
endif
|
||||
@@ -197,8 +213,10 @@ override CXXFLAGS += \
|
||||
-std=gnu++20 \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
-fmodules-ts \
|
||||
$(SHARED_FLAGS)
|
||||
ifneq ($(CXX_IS_CLANG),1)
|
||||
override CXXFLAGS += -fmodules-ts
|
||||
endif
|
||||
|
||||
# Internal linker flags that should not be changed by the user.
|
||||
override LDFLAGS += \
|
||||
@@ -212,6 +230,9 @@ override LDFLAGS += \
|
||||
# Use "find" to glob all *.c, *.cpp, *.S, and *.asm files in the tree and obtain the
|
||||
# object and header dependency file names.
|
||||
override SRCFILES := $(shell cd src && find -L * -type f | LC_ALL=C sort)
|
||||
# Architecture-specific code lives in src/Arch/<arch>/; only the target's
|
||||
# directory is built. Everything else in src/ is architecture-neutral.
|
||||
override SRCFILES := $(filter-out Arch/%,$(SRCFILES)) $(filter Arch/$(ARCH)/%,$(SRCFILES))
|
||||
override CFILES := $(filter %.c,$(SRCFILES))
|
||||
override CXXFILES := $(filter %.cpp,$(SRCFILES))
|
||||
override OBJCFILES := $(filter %.mm,$(SRCFILES))
|
||||
@@ -253,6 +274,11 @@ $(BUILDNO_HDR): $(filter-out $(BUILDNO_OBJ),$(OBJ)) $(PROG_SRC)
|
||||
# on a clean build (where no .d dependency exists yet).
|
||||
$(BUILDNO_OBJ): $(BUILDNO_HDR)
|
||||
|
||||
# Compile everything without linking (used by scripts/check-arch-neutral.sh
|
||||
# to verify a port's objects before a cross linker is available).
|
||||
.PHONY: objects
|
||||
objects: $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a
|
||||
|
||||
# Link rules for building the C compiler runtime.
|
||||
cc-runtime-$(ARCH)/cc-runtime.a: GNUmakefile cc-runtime/*
|
||||
rm -rf cc-runtime-$(ARCH)
|
||||
@@ -263,10 +289,35 @@ cc-runtime-$(ARCH)/cc-runtime.a: GNUmakefile cc-runtime/*
|
||||
CFLAGS="$(CFLAGS)" \
|
||||
CPPFLAGS='-isystem ../freestnd-c-hdrs/$(ARCH)/include -DCC_RUNTIME_NO_FLOAT'
|
||||
|
||||
# Clang's generic-ELF driver links cross targets through the host's GCC,
|
||||
# so a clang-built non-x86 kernel is linked by ld.lld directly.
|
||||
ifneq ($(ARCH),x86_64)
|
||||
ifeq ($(CXX_IS_CLANG),1)
|
||||
# Without a system lld, fall back to the one rustup ships.
|
||||
KERNEL_LD ?= $(firstword $(shell command -v ld.lld) \
|
||||
$(wildcard $(HOME)/.rustup/toolchains/*/lib/rustlib/*/bin/gcc-ld/ld.lld))
|
||||
override LD_EMULATION_aarch64 := aarch64elf
|
||||
override LD_EMULATION_riscv64 := elf64lriscv
|
||||
override LD_EMULATION_loongarch64 := elf64loongarch
|
||||
override KERNEL_LDFLAGS := \
|
||||
-m $(LD_EMULATION_$(ARCH)) \
|
||||
--build-id=none \
|
||||
--no-relax \
|
||||
-static \
|
||||
-z max-page-size=0x1000 \
|
||||
--gc-sections \
|
||||
-T linker-$(ARCH).ld
|
||||
endif
|
||||
endif
|
||||
|
||||
# Link rules for the final executable.
|
||||
bin-$(ARCH)/$(OUTPUT): GNUmakefile linker-$(ARCH).ld $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a
|
||||
mkdir -p "$$(dirname $@)"
|
||||
ifeq ($(KERNEL_LD),)
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a -o $@
|
||||
else
|
||||
$(KERNEL_LD) $(KERNEL_LDFLAGS) $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a -o $@
|
||||
endif
|
||||
|
||||
# Compilation rules for *.c files.
|
||||
obj-$(ARCH)/%.c.o: src/%.c GNUmakefile
|
||||
|
||||
@@ -22,6 +22,7 @@ SECTIONS
|
||||
/* Any address in this region will do, but often 0xffffffff80000000 is chosen as */
|
||||
/* that is the beginning of the region. */
|
||||
. = 0xffffffff80000000;
|
||||
KernelStartSymbol = .;
|
||||
|
||||
/* Define a section to contain the Limine requests and assign it to its own PHDR */
|
||||
.limine_requests : {
|
||||
@@ -74,4 +75,6 @@ SECTIONS
|
||||
*(.eh_frame*)
|
||||
*(.note .note.*)
|
||||
}
|
||||
|
||||
KernelEndSymbol = .;
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Memory/Heap.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
#include "AcpiEvents.hpp"
|
||||
#include <ACPI/AcpiSleep.hpp>
|
||||
#include <Io/IoPort.hpp>
|
||||
#include <Hal/Apic/Apic.hpp>
|
||||
#include <Hal/Apic/IoApic.hpp>
|
||||
#include <Hal/Apic/Interrupts.hpp>
|
||||
#include <Arch/Interrupts.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
@@ -191,24 +189,21 @@ namespace Hal {
|
||||
// routed. Clear stale status only after the enables are down.
|
||||
DisableAndClearGpes();
|
||||
|
||||
// Route SCI to an IRQ vector. The SCI is level-triggered,
|
||||
// Route the SCI to its IRQ slot. The SCI is level-triggered,
|
||||
// active-low (ACPI spec requirement). The MADT may have an
|
||||
// Interrupt Source Override for this IRQ that already sets
|
||||
// these flags; RouteIrq applies overrides automatically.
|
||||
uint8_t sciVector = IRQ_VECTOR_BASE + (uint8_t)g_sciIrq;
|
||||
uint8_t bspApicId = (uint8_t)LocalApic::GetId();
|
||||
// these flags; RouteLegacyIrq applies overrides automatically.
|
||||
|
||||
// Register the handler before routing to avoid missing events
|
||||
RegisterIrqHandler((uint8_t)g_sciIrq, SciHandler);
|
||||
Arch::RegisterIrqHandler((uint8_t)g_sciIrq, SciHandler);
|
||||
|
||||
// Route via IoApic (applies MADT overrides for polarity/trigger)
|
||||
IoApic::RouteIrq((uint8_t)g_sciIrq, sciVector, bspApicId);
|
||||
// Route to the boot CPU (applies firmware polarity/trigger overrides)
|
||||
Arch::RouteLegacyIrq((uint8_t)g_sciIrq);
|
||||
|
||||
g_initialized = true;
|
||||
|
||||
KernelLogStream(OK, "ACPI") << "ACPI events initialized (SCI IRQ "
|
||||
<< base::dec << (uint64_t)g_sciIrq << ", vector "
|
||||
<< (uint64_t)sciVector << ")";
|
||||
<< base::dec << (uint64_t)g_sciIrq << ")";
|
||||
}
|
||||
|
||||
void Reinitialize() {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
#include "AcpiShutdown.hpp"
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include "AcpiSleep.hpp"
|
||||
#include "AcpiEvents.hpp"
|
||||
#include <ACPI/FADT.hpp>
|
||||
@@ -37,8 +38,24 @@ namespace Hal {
|
||||
return;
|
||||
}
|
||||
|
||||
// Map and parse the DSDT to find \_S5_
|
||||
auto* dsdt = (void*)Memory::HHDM(fadt.DsdtAddress);
|
||||
|
||||
AML::InitializeInterpreter(dsdt, xsdt);
|
||||
|
||||
if (AML::GetInterpreter().IsInitialized()) {
|
||||
AcpiDevices::DeviceList devices{};
|
||||
AcpiDevices::EnumerateAll(devices);
|
||||
}
|
||||
|
||||
AcpiSleep::Initialize(xsdt);
|
||||
|
||||
// Hardware-reduced ACPI (ARM) has no PM1 registers: the
|
||||
// architecture powers off instead (Arch::PowerOff).
|
||||
if ((fadt.Flags & FADT::FlagHardwareReduced) || fadt.PM1aControlBlock == 0) {
|
||||
KernelLogStream(INFO, "ACPI") << "No PM1 control block - ACPI S5 shutdown unavailable";
|
||||
return;
|
||||
}
|
||||
|
||||
AML::S5Object s5 = AML::FindS5(dsdt);
|
||||
|
||||
if (!s5.Valid) {
|
||||
@@ -54,23 +71,6 @@ namespace Hal {
|
||||
|
||||
KernelLogStream(OK, "ACPI") << "ACPI shutdown initialized (PM1a=" << base::hex
|
||||
<< (uint64_t)g_pm1aControlBlock << " SLP_TYPa=" << (uint64_t)g_slpTypA << ")";
|
||||
|
||||
// Initialize the full AML interpreter with the DSDT.
|
||||
// This loads the entire DSDT namespace, enabling device enumeration,
|
||||
// method evaluation, and field I/O for the rest of the kernel.
|
||||
AML::InitializeInterpreter(dsdt, xsdt);
|
||||
|
||||
// Enumerate ACPI devices now that the namespace is loaded
|
||||
if (AML::GetInterpreter().IsInitialized()) {
|
||||
AcpiDevices::DeviceList devices{};
|
||||
AcpiDevices::EnumerateAll(devices);
|
||||
}
|
||||
|
||||
// Initialize S3 suspend support (reads FACS and \_S3_ from namespace)
|
||||
AcpiSleep::Initialize(xsdt);
|
||||
|
||||
// Note: ACPI event handling (SCI, power button) is initialized
|
||||
// from Main.cpp after the APIC subsystem is ready.
|
||||
}
|
||||
|
||||
bool IsAvailable() {
|
||||
@@ -95,7 +95,7 @@ namespace Hal {
|
||||
}
|
||||
}
|
||||
|
||||
asm volatile("cli");
|
||||
Arch::DisableInterrupts();
|
||||
|
||||
// Phase 1: Write SLP_TYP to PM1x_CNT without SLP_EN,
|
||||
// preserving existing register bits
|
||||
@@ -110,7 +110,7 @@ namespace Hal {
|
||||
}
|
||||
|
||||
// Flush all caches before entering sleep state
|
||||
asm volatile("wbinvd");
|
||||
Arch::WritebackInvalidateCaches();
|
||||
|
||||
// Phase 2: Assert SLP_EN to trigger the transition
|
||||
Io::Out16(pm1a | SLP_EN, (uint16_t)g_pm1aControlBlock);
|
||||
@@ -122,7 +122,7 @@ namespace Hal {
|
||||
|
||||
// Halt and wait for the hardware to power off
|
||||
for (;;) {
|
||||
asm volatile("hlt");
|
||||
Arch::Halt();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,21 +75,6 @@ namespace Hal {
|
||||
|
||||
// CPU State (saved across S3)
|
||||
|
||||
// ============================================================================
|
||||
// Layout must match S3Wake.asm offsets exactly.
|
||||
// No FPU/SSE state — the kernel is compiled with -mno-sse.
|
||||
struct CpuState {
|
||||
uint64_t Rax, Rbx, Rcx, Rdx; // 0x00 - 0x18
|
||||
uint64_t Rsi, Rdi, Rbp, Rsp; // 0x20 - 0x38
|
||||
uint64_t R8, R9, R10, R11; // 0x40 - 0x58
|
||||
uint64_t R12, R13, R14, R15; // 0x60 - 0x78
|
||||
uint64_t Rflags; // 0x80
|
||||
uint64_t Rip; // 0x88
|
||||
uint64_t Cr3; // 0x90
|
||||
uint64_t Cr0, Cr4; // 0x98, 0xA0
|
||||
uint8_t GdtPtr[10]; // 0xA8
|
||||
uint8_t IdtPtr[10]; // 0xB2
|
||||
} __attribute__((aligned(16)));
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -109,5 +94,9 @@ namespace Hal {
|
||||
// Returns 0 on successful resume, or -1 if S3 is unavailable.
|
||||
int Suspend();
|
||||
|
||||
// Initialize/IsS3Available/Suspend are implemented by the
|
||||
// architecture (the FACS waking-vector path is an x86 platform
|
||||
// mechanism); other targets report S3 as unavailable.
|
||||
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* AcpiWmi.cpp
|
||||
* ACPI-WMI mapper (PNP0C14): find a WMI GUID's data block in _WDG and call
|
||||
* its WMxx method
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "AcpiWmi.hpp"
|
||||
#include "AcpiDevices.hpp"
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Hal {
|
||||
namespace AcpiWmi {
|
||||
|
||||
static constexpr const char* HidWmi = "PNP0C14";
|
||||
static constexpr uint32_t WdgEntrySize = 20; // GUID, id, count, flags
|
||||
|
||||
static int HexDigit(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool ParseGuid(const char* text, uint8_t out[16]) {
|
||||
// Bytes in text order, then the first three fields byte-swapped.
|
||||
uint8_t bytes[16];
|
||||
int count = 0;
|
||||
for (int i = 0; text[i]; i++) {
|
||||
if (text[i] == '-') continue;
|
||||
int hi = HexDigit(text[i]);
|
||||
int lo = text[i + 1] ? HexDigit(text[i + 1]) : -1;
|
||||
if (hi < 0 || lo < 0 || count == 16) return false;
|
||||
bytes[count++] = (uint8_t)(hi << 4 | lo);
|
||||
i++;
|
||||
}
|
||||
if (count != 16) return false;
|
||||
static constexpr int order[16] = {3, 2, 1, 0, 5, 4, 7, 6,
|
||||
8, 9, 10, 11, 12, 13, 14, 15};
|
||||
for (int i = 0; i < 16; i++) out[i] = bytes[order[i]];
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool SearchDevice(int32_t device, const uint8_t guid[16], Block& out) {
|
||||
auto& interp = AML::GetInterpreter();
|
||||
AML::Object wdg{};
|
||||
if (!interp.EvaluateChild(device, "_WDG", nullptr, 0, wdg)
|
||||
|| wdg.Type != AML::ObjectType::Buffer)
|
||||
return false;
|
||||
|
||||
// The interpreter caps buffers; a _WDG that fills the cap may have
|
||||
// lost entries past it.
|
||||
if (wdg.Buffer.Length >= (uint32_t)AML::MaxBufferLen) {
|
||||
char path[128];
|
||||
interp.GetNamespace().GetNodePath(device, path, sizeof(path));
|
||||
KernelLogStream(WARNING, "WMI") << path << "._WDG may be truncated ("
|
||||
<< base::dec << (uint64_t)wdg.Buffer.Length << " bytes read)";
|
||||
}
|
||||
|
||||
for (uint32_t off = 0; off + WdgEntrySize <= wdg.Buffer.Length; off += WdgEntrySize) {
|
||||
const uint8_t* entry = wdg.Buffer.Data + off;
|
||||
if (memcmp(entry, guid, 16) != 0) continue;
|
||||
out.Device = device;
|
||||
out.ObjectId[0] = (char)entry[16];
|
||||
out.ObjectId[1] = (char)entry[17];
|
||||
out.InstanceCount = entry[18];
|
||||
out.Flags = entry[19];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Find(const uint8_t guid[16], Block& out) {
|
||||
auto& interp = AML::GetInterpreter();
|
||||
if (!interp.IsInitialized()) return false;
|
||||
auto& ns = interp.GetNamespace();
|
||||
|
||||
bool found = false;
|
||||
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
|
||||
[&](int32_t idx, const AML::NamespaceNode*) {
|
||||
if (found) return;
|
||||
char hid[16];
|
||||
if (!AcpiDevices::EvaluateHid(idx, hid, sizeof(hid))) return;
|
||||
if (memcmp(hid, HidWmi, sizeof("PNP0C14")) != 0) return;
|
||||
if (!(AcpiDevices::EvaluateSta(idx) & AcpiDevices::STA_PRESENT)) return;
|
||||
found = SearchDevice(idx, guid, out);
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
bool EvaluateMethod(const Block& block, uint8_t instance, uint32_t methodId,
|
||||
const uint8_t* input, uint32_t inputLength,
|
||||
AML::Object& result) {
|
||||
if (!(block.Flags & FlagMethod) || instance >= block.InstanceCount)
|
||||
return false;
|
||||
|
||||
AML::Object args[3];
|
||||
args[0].Type = AML::ObjectType::Integer;
|
||||
args[0].Integer = instance;
|
||||
args[1].Type = AML::ObjectType::Integer;
|
||||
args[1].Integer = methodId;
|
||||
if (block.Flags & FlagString) {
|
||||
uint32_t n = inputLength;
|
||||
if (n >= (uint32_t)AML::MaxStringLen) n = AML::MaxStringLen - 1;
|
||||
args[2].Type = AML::ObjectType::String;
|
||||
if (n) memcpy(args[2].String.Data, input, n);
|
||||
args[2].String.Data[n] = '\0';
|
||||
args[2].String.Length = (uint16_t)n;
|
||||
} else {
|
||||
uint32_t n = inputLength;
|
||||
if (n > (uint32_t)AML::MaxBufferLen) n = AML::MaxBufferLen;
|
||||
args[2].Type = AML::ObjectType::Buffer;
|
||||
if (n) memcpy(args[2].Buffer.Data, input, n);
|
||||
args[2].Buffer.Length = n;
|
||||
}
|
||||
|
||||
char method[5] = {'W', 'M', block.ObjectId[0], block.ObjectId[1], '\0'};
|
||||
return AML::GetInterpreter().EvaluateChild(block.Device, method, args, 3, result);
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* AcpiWmi.hpp
|
||||
* ACPI-WMI mapper (PNP0C14): find a WMI GUID's data block in _WDG and call
|
||||
* its WMxx method
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <ACPI/AML/AmlNamespace.hpp>
|
||||
|
||||
namespace Hal {
|
||||
namespace AcpiWmi {
|
||||
|
||||
// _WDG flags (Microsoft "Windows Instrumentation: WMI and ACPI").
|
||||
static constexpr uint8_t FlagExpensive = 0x1;
|
||||
static constexpr uint8_t FlagMethod = 0x2;
|
||||
static constexpr uint8_t FlagString = 0x4;
|
||||
static constexpr uint8_t FlagEvent = 0x8;
|
||||
|
||||
// One _WDG entry, bound to the PNP0C14 device that declared it.
|
||||
struct Block {
|
||||
int32_t Device; // namespace node of the PNP0C14 device
|
||||
char ObjectId[2]; // names the WQxx / WSxx / WMxx methods
|
||||
uint8_t InstanceCount;
|
||||
uint8_t Flags;
|
||||
};
|
||||
|
||||
// Parse "603E9613-EF25-4338-A3D0-C46177516DB7" into the byte order
|
||||
// _WDG stores (first three fields little-endian). False if malformed.
|
||||
bool ParseGuid(const char* text, uint8_t out[16]);
|
||||
|
||||
// Search every present PNP0C14 device's _WDG for `guid`. The first
|
||||
// declaration wins, as on Linux.
|
||||
bool Find(const uint8_t guid[16], Block& out);
|
||||
|
||||
// Evaluate WMxx(instance, methodId, input) on a method block. The input
|
||||
// is passed as a String instead of a Buffer when the block says so.
|
||||
bool EvaluateMethod(const Block& block, uint8_t instance, uint32_t methodId,
|
||||
const uint8_t* input, uint32_t inputLength,
|
||||
AML::Object& result);
|
||||
|
||||
};
|
||||
};
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "AcpiDevices.hpp"
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
|
||||
@@ -107,6 +107,8 @@ namespace Hal {
|
||||
result.AcpiEnable = fadt->AcpiEnable;
|
||||
result.AcpiDisable = fadt->AcpiDisable;
|
||||
result.Flags = fadt->Flags;
|
||||
result.ArmBootArchFlags = fadt->Header.Length >= offsetof(Table, ArmBootArchFlags) + 2
|
||||
? fadt->ArmBootArchFlags : 0;
|
||||
result.Valid = true;
|
||||
|
||||
KernelLogStream(INFO, "FADT") << "DSDT at " << base::hex << result.DsdtAddress;
|
||||
|
||||
@@ -69,6 +69,13 @@ namespace Hal {
|
||||
uint8_t X_GPE1Block[12];
|
||||
} __attribute__((packed));
|
||||
|
||||
// FADT Flags
|
||||
constexpr uint32_t FlagHardwareReduced = 1u << 20;
|
||||
|
||||
// ARM_BOOT_ARCH flags (ACPI 5.1+)
|
||||
constexpr uint16_t ArmBootPsciCompliant = 1u << 0;
|
||||
constexpr uint16_t ArmBootPsciUseHvc = 1u << 1;
|
||||
|
||||
struct ParsedFADT {
|
||||
uint64_t DsdtAddress;
|
||||
uint64_t FacsAddress;
|
||||
@@ -91,6 +98,7 @@ namespace Hal {
|
||||
uint8_t AcpiEnable;
|
||||
uint8_t AcpiDisable;
|
||||
uint32_t Flags;
|
||||
uint16_t ArmBootArchFlags; // 0 when the table predates the field
|
||||
bool Valid;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* GsiRouting.cpp
|
||||
* Platform interrupt lines for ACPI global system interrupts
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "GsiRouting.hpp"
|
||||
#include <Arch/Interrupts.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
|
||||
namespace Hal::GsiRouting {
|
||||
|
||||
static constexpr uint8_t FirstLine = 1;
|
||||
static constexpr uint8_t LineCount = 24;
|
||||
|
||||
static uint32_t g_lineGsi[LineCount] = {};
|
||||
static uint8_t g_nextLine = FirstLine;
|
||||
static kcp::Mutex g_lock;
|
||||
|
||||
uint8_t LineForGsi(uint32_t gsi, bool levelTriggered, bool activeLow) {
|
||||
if (Arch::PlatformLinesAreFixed()) return 0;
|
||||
|
||||
g_lock.Acquire();
|
||||
for (uint8_t line = FirstLine; line < g_nextLine; line++) {
|
||||
if (g_lineGsi[line] == gsi) {
|
||||
g_lock.Release();
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t line = 0;
|
||||
if (g_nextLine < LineCount &&
|
||||
Arch::AssignPlatformLine(g_nextLine, gsi, levelTriggered, activeLow)) {
|
||||
line = g_nextLine++;
|
||||
g_lineGsi[line] = gsi;
|
||||
}
|
||||
g_lock.Release();
|
||||
return line;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* GsiRouting.hpp
|
||||
* Platform interrupt lines for ACPI global system interrupts
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Hal::GsiRouting {
|
||||
|
||||
// The platform line (1-23) that delivers global system interrupt `gsi`,
|
||||
// assigning one on first use (Arch::AssignPlatformLine); every device on
|
||||
// one GSI shares its line. Drivers register their handler on the line
|
||||
// and call Arch::EnableLegacyIrq. Returns 0 where the platform's lines
|
||||
// are wired by the hardware (x86) or when none is left.
|
||||
uint8_t LineForGsi(uint32_t gsi, bool levelTriggered, bool activeLow);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* WmiEcBacklight.cpp
|
||||
* Firmware (EC) panel backlight behind the WMI interface NVIDIA specified
|
||||
* for display-mux laptops
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "WmiEcBacklight.hpp"
|
||||
#include "AcpiWmi.hpp"
|
||||
#include "AcpiPower.hpp"
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Hal {
|
||||
namespace WmiEcBacklight {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Interface
|
||||
|
||||
// ============================================================================
|
||||
static constexpr const char* Guid = "603E9613-EF25-4338-A3D0-C46177516DB7";
|
||||
|
||||
// WMxx method IDs.
|
||||
static constexpr uint32_t MethodLevel = 1;
|
||||
static constexpr uint32_t MethodSource = 2;
|
||||
|
||||
// `mode` values in the argument block.
|
||||
static constexpr uint32_t ModeGet = 0;
|
||||
static constexpr uint32_t ModeSet = 1;
|
||||
static constexpr uint32_t ModeGetMaxLevel = 2;
|
||||
|
||||
static constexpr uint64_t SourceEc = 2; // 1 = GPU, 3 = eDP AUX
|
||||
|
||||
// Lowest level driven, so the panel never goes fully dark.
|
||||
static constexpr int MinPercent = 1;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// State
|
||||
|
||||
// ============================================================================
|
||||
static kcp::Mutex g_lock;
|
||||
static bool g_probed = false;
|
||||
static bool g_available = false;
|
||||
static AcpiWmi::Block g_block{};
|
||||
static uint32_t g_maxLevel = 0;
|
||||
|
||||
// Argument block: { u32 mode; u32 val; u32 ret; u32 reserved[3]; }.
|
||||
// The method answers with an Integer. Linux reads it back as the
|
||||
// `ret` field of the same block, which is where ACPICA's copy-out of
|
||||
// an Integer puts the value; a firmware returning the block itself as
|
||||
// a Buffer is read at that same offset.
|
||||
static bool Call(uint32_t method, uint32_t mode, uint32_t value, uint32_t& out) {
|
||||
uint8_t args[24] = {};
|
||||
args[0] = (uint8_t)mode;
|
||||
args[1] = (uint8_t)(mode >> 8);
|
||||
args[2] = (uint8_t)(mode >> 16);
|
||||
args[3] = (uint8_t)(mode >> 24);
|
||||
args[4] = (uint8_t)value;
|
||||
args[5] = (uint8_t)(value >> 8);
|
||||
args[6] = (uint8_t)(value >> 16);
|
||||
args[7] = (uint8_t)(value >> 24);
|
||||
|
||||
AML::Object result{};
|
||||
if (!AcpiWmi::EvaluateMethod(g_block, 0, method, args, sizeof(args), result))
|
||||
return false;
|
||||
if (result.Type == AML::ObjectType::Integer) {
|
||||
out = (uint32_t)result.Integer;
|
||||
return true;
|
||||
}
|
||||
if (result.Type == AML::ObjectType::Buffer && result.Buffer.Length >= 12) {
|
||||
const uint8_t* r = result.Buffer.Data + 8;
|
||||
out = r[0] | (uint32_t)r[1] << 8 | (uint32_t)r[2] << 16 | (uint32_t)r[3] << 24;
|
||||
return true;
|
||||
}
|
||||
// A set may legitimately return nothing.
|
||||
out = 0;
|
||||
return mode == ModeSet;
|
||||
}
|
||||
|
||||
static void Probe() {
|
||||
g_probed = true;
|
||||
uint8_t guid[16];
|
||||
if (!AcpiWmi::ParseGuid(Guid, guid) || !AcpiWmi::Find(guid, g_block)) return;
|
||||
if (!(g_block.Flags & AcpiWmi::FlagMethod) || g_block.InstanceCount == 0) return;
|
||||
|
||||
char path[128];
|
||||
AML::GetInterpreter().GetNamespace().GetNodePath(g_block.Device, path, sizeof(path));
|
||||
|
||||
// The interface also exists on machines where the GPU drives the
|
||||
// backlight; there it must be left alone.
|
||||
uint32_t source = 0;
|
||||
if (!Call(MethodSource, ModeGet, 0, source)) {
|
||||
KernelLogStream(INFO, "Backlight") << "WMI EC backlight at " << path
|
||||
<< ": source query failed";
|
||||
return;
|
||||
}
|
||||
if (source != SourceEc) {
|
||||
KernelLogStream(INFO, "Backlight") << "WMI EC backlight at " << path
|
||||
<< " reports source " << base::dec << (uint64_t)source << ", not EC";
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t maximum = 0;
|
||||
if (!Call(MethodLevel, ModeGetMaxLevel, 0, maximum) || maximum == 0) {
|
||||
KernelLogStream(WARNING, "Backlight") << "WMI EC backlight at " << path
|
||||
<< ": no maximum level";
|
||||
return;
|
||||
}
|
||||
g_maxLevel = maximum;
|
||||
g_available = true;
|
||||
char method[5] = {'W', 'M', g_block.ObjectId[0], g_block.ObjectId[1], '\0'};
|
||||
KernelLogStream(OK, "Backlight") << "Panel brightness via EC (WMI "
|
||||
<< path << "." << method << ", " << base::dec << (uint64_t)maximum
|
||||
<< " levels)";
|
||||
}
|
||||
|
||||
static int ToPercent(uint32_t level) {
|
||||
if (level > g_maxLevel) level = g_maxLevel;
|
||||
return (int)(((uint64_t)level * 100 + g_maxLevel / 2) / g_maxLevel);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Public API
|
||||
|
||||
// ============================================================================
|
||||
int Brightness(int percent) {
|
||||
g_lock.Acquire();
|
||||
if (!g_probed) Probe();
|
||||
int result = -1;
|
||||
if (g_available) {
|
||||
uint32_t level = 0;
|
||||
if (percent >= 0) {
|
||||
if (percent > 100) percent = 100;
|
||||
if (percent < MinPercent) percent = MinPercent;
|
||||
uint32_t target = (uint32_t)(((uint64_t)g_maxLevel * (uint32_t)percent + 50) / 100);
|
||||
if (target == 0) target = 1;
|
||||
uint32_t ignored = 0;
|
||||
if (!Call(MethodLevel, ModeSet, target, ignored)) {
|
||||
result = -2;
|
||||
} else {
|
||||
result = Call(MethodLevel, ModeGet, 0, level) ? ToPercent(level)
|
||||
: ToPercent(target);
|
||||
}
|
||||
} else {
|
||||
result = Call(MethodLevel, ModeGet, 0, level) ? ToPercent(level) : -2;
|
||||
}
|
||||
}
|
||||
g_lock.Release();
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* WmiEcBacklight.hpp
|
||||
* Firmware (EC) panel backlight behind the WMI interface NVIDIA specified
|
||||
* for display-mux laptops
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Hal {
|
||||
namespace WmiEcBacklight {
|
||||
|
||||
// On a laptop whose panel can be switched between GPUs, firmware may
|
||||
// wire the backlight to the embedded controller instead of either
|
||||
// GPU and expose it through WMI GUID 603E9613-EF25-4338-A3D0-
|
||||
// C46177516DB7. Despite the name the interface is pure firmware: it
|
||||
// works with the discrete GPU powered off. It is used only when the
|
||||
// firmware itself reports the EC as the brightness source, which is
|
||||
// also when GPU PWM writes stop reaching the panel.
|
||||
//
|
||||
// percent < 0 queries. Returns 0..100, -1 when this interface is not
|
||||
// the panel's brightness source, -2 when the firmware call failed.
|
||||
// Discovery runs on first use. May sleep (AML).
|
||||
int Brightness(int percent);
|
||||
|
||||
};
|
||||
};
|
||||
@@ -11,8 +11,8 @@
|
||||
#include <Drivers/Audio/IntelHda.hpp>
|
||||
#include <Drivers/Audio/Mixer.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
||||
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/Bluetooth/A2dp.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
|
||||
#include "Syscall.hpp"
|
||||
@@ -55,10 +55,10 @@ namespace montauk::abi {
|
||||
return Drivers::Audio::Mixer::SetOutput(
|
||||
(Drivers::Audio::Mixer::Output)value);
|
||||
if (cmd == AUDIO_CTL_BT_STATUS) {
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
||||
if (Drivers::USB::Bluetooth::A2dp::IsReady()) return 2;
|
||||
return Drivers::USB::Bluetooth::A2dp::GetState()
|
||||
== Drivers::USB::Bluetooth::A2dp::State::Idle
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return 0;
|
||||
if (Drivers::Bluetooth::A2dp::IsReady()) return 2;
|
||||
return Drivers::Bluetooth::A2dp::GetState()
|
||||
== Drivers::Bluetooth::A2dp::State::Idle
|
||||
? 0 : 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/USB/Bluetooth/Hci.hpp>
|
||||
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/Bluetooth/Hci.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
@@ -19,14 +19,14 @@ namespace montauk::abi {
|
||||
|
||||
static int64_t Sys_BtScan(BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
|
||||
if (!buf || maxCount <= 0) return InvalidArgument;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
// Use a stack buffer for kernel-side results (max 16)
|
||||
Drivers::USB::Bluetooth::Hci::InquiryDevice tmpBuf[16];
|
||||
Drivers::Bluetooth::Hci::InquiryDevice tmpBuf[16];
|
||||
int kMax = maxCount;
|
||||
if (kMax > 16) kMax = 16;
|
||||
|
||||
int found = Drivers::USB::Bluetooth::Scan(tmpBuf, kMax, timeoutMs);
|
||||
int found = Drivers::Bluetooth::Scan(tmpBuf, kMax, timeoutMs);
|
||||
if (found < 0) return found;
|
||||
|
||||
// Convert kernel InquiryDevice to userspace BtScanResult
|
||||
@@ -47,38 +47,38 @@ namespace montauk::abi {
|
||||
|
||||
static int64_t Sys_BtConnect(const uint8_t* bdAddr) {
|
||||
if (!bdAddr) return InvalidArgument;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
return (int64_t)Drivers::USB::Bluetooth::Connect(bdAddr);
|
||||
return (int64_t)Drivers::Bluetooth::Connect(bdAddr);
|
||||
}
|
||||
|
||||
static int64_t Sys_BtDisconnect(const uint8_t* bdAddr) {
|
||||
if (!bdAddr) return InvalidArgument;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
|
||||
<< (uint64_t)Sched::GetCurrentPid()
|
||||
<< " entered Bluetooth disconnect stage";
|
||||
return (int64_t)Drivers::USB::Bluetooth::Disconnect(bdAddr);
|
||||
return (int64_t)Drivers::Bluetooth::Disconnect(bdAddr);
|
||||
}
|
||||
|
||||
static int64_t Sys_BtSetAddr(const uint8_t* bdAddr) {
|
||||
if (!bdAddr) return InvalidArgument;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
// bdAddr is a 6-byte buffer, addr[0] = least-significant octet.
|
||||
return Drivers::USB::Bluetooth::SetAddress(bdAddr) ? Success : GenericFailure;
|
||||
return Drivers::Bluetooth::SetAddress(bdAddr) ? Success : GenericFailure;
|
||||
}
|
||||
|
||||
static int64_t Sys_BtBonds(BtBondInfo* buf, int maxCount) {
|
||||
if (!buf || maxCount <= 0) return 0;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return 0;
|
||||
|
||||
Drivers::USB::Bluetooth::Hci::BondInfo tmpBuf[8];
|
||||
Drivers::Bluetooth::Hci::BondInfo tmpBuf[8];
|
||||
int kMax = maxCount;
|
||||
if (kMax > 8) kMax = 8;
|
||||
|
||||
int count = Drivers::USB::Bluetooth::ListBonded(tmpBuf, kMax);
|
||||
int count = Drivers::Bluetooth::ListBonded(tmpBuf, kMax);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
memcpy(buf[i].bdAddr, tmpBuf[i].Addr, 6);
|
||||
@@ -91,20 +91,20 @@ namespace montauk::abi {
|
||||
|
||||
static int64_t Sys_BtForget(const uint8_t* bdAddr) {
|
||||
if (!bdAddr) return InvalidArgument;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
return (int64_t)Drivers::USB::Bluetooth::ForgetDevice(bdAddr);
|
||||
return (int64_t)Drivers::Bluetooth::ForgetDevice(bdAddr);
|
||||
}
|
||||
|
||||
static int64_t Sys_BtList(BtDevInfo* buf, int maxCount) {
|
||||
if (!buf || maxCount <= 0) return 0;
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
||||
if (!Drivers::Bluetooth::IsInitialized()) return 0;
|
||||
|
||||
Drivers::USB::Bluetooth::Hci::ConnectionInfo tmpBuf[4];
|
||||
Drivers::Bluetooth::Hci::ConnectionInfo tmpBuf[4];
|
||||
int kMax = maxCount;
|
||||
if (kMax > 4) kMax = 4;
|
||||
|
||||
int count = Drivers::USB::Bluetooth::ListConnected(tmpBuf, kMax);
|
||||
int count = Drivers::Bluetooth::ListConnected(tmpBuf, kMax);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
memcpy(buf[i].bdAddr, tmpBuf[i].BdAddr, 6);
|
||||
@@ -123,14 +123,14 @@ namespace montauk::abi {
|
||||
|
||||
memset(buf, 0, sizeof(BtAdapterInfo));
|
||||
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) {
|
||||
if (!Drivers::Bluetooth::IsInitialized()) {
|
||||
buf->initialized = 0;
|
||||
return NoDevice;
|
||||
}
|
||||
|
||||
buf->initialized = 1;
|
||||
memcpy(buf->bdAddr, Drivers::USB::Bluetooth::GetBdAddr(), 6);
|
||||
buf->scanning = Drivers::USB::Bluetooth::Hci::IsInquiryActive() ? 1 : 0;
|
||||
memcpy(buf->bdAddr, Drivers::Bluetooth::GetBdAddr(), 6);
|
||||
buf->scanning = Drivers::Bluetooth::Hci::IsInquiryActive() ? 1 : 0;
|
||||
|
||||
// Copy adapter name
|
||||
const char* name = "MontaukOS";
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 287
|
||||
#define MONTAUK_BUILD_NUMBER 123
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Clipboard.hpp
|
||||
* SYS_CLIPBOARD_* plain-text clipboard syscalls
|
||||
* SYS_CLIPBOARD_* syscalls: per-user multi-format clipboard
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
@@ -11,24 +11,25 @@
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
|
||||
#include "Syscall.hpp"
|
||||
#include "DataTransfer.hpp"
|
||||
|
||||
namespace montauk::abi {
|
||||
|
||||
static constexpr int ClipboardMaxSlots = 32;
|
||||
|
||||
// One clipboard per user. It holds a multi-format transfer; the text
|
||||
// syscalls read and replace its TRANSFER_MIME_TEXT format, so text-only
|
||||
// apps and multi-format apps share the same clipboard.
|
||||
struct ClipboardSlot {
|
||||
bool inUse;
|
||||
char user[32];
|
||||
char* text;
|
||||
uint32_t len;
|
||||
DataTransfer::Transfer transfer;
|
||||
uint64_t serial;
|
||||
};
|
||||
|
||||
static ClipboardSlot g_clipboardSlots[ClipboardMaxSlots];
|
||||
static kcp::Mutex g_clipboardLock;
|
||||
|
||||
static int Sys_ClipboardClear();
|
||||
|
||||
static const char* ClipboardCurrentUser() {
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
if (proc == nullptr) return nullptr;
|
||||
@@ -81,57 +82,73 @@ namespace montauk::abi {
|
||||
|
||||
freeSlot->inUse = true;
|
||||
ClipboardCopyUser(freeSlot->user, user);
|
||||
freeSlot->text = nullptr;
|
||||
freeSlot->len = 0;
|
||||
freeSlot->transfer.count = 0;
|
||||
freeSlot->serial = 0;
|
||||
return freeSlot;
|
||||
}
|
||||
|
||||
// Swap `incoming` into the caller's clipboard (it becomes empty) and free
|
||||
// what was there. An empty transfer clears the clipboard.
|
||||
static int ClipboardReplace(DataTransfer::Transfer& incoming) {
|
||||
const char* currentUser = ClipboardCurrentUser();
|
||||
if (currentUser == nullptr) {
|
||||
DataTransfer::Free(incoming);
|
||||
return GenericFailure;
|
||||
}
|
||||
|
||||
char user[32];
|
||||
ClipboardCopyUser(user, currentUser);
|
||||
|
||||
DataTransfer::Transfer old;
|
||||
old.count = 0;
|
||||
|
||||
g_clipboardLock.Acquire();
|
||||
ClipboardSlot* slot = ClipboardEnsureSlotLocked(user);
|
||||
if (slot == nullptr) {
|
||||
g_clipboardLock.Release();
|
||||
DataTransfer::Free(incoming);
|
||||
return Busy;
|
||||
}
|
||||
|
||||
old = slot->transfer;
|
||||
slot->transfer = incoming;
|
||||
slot->serial = ClipboardNextSerial(slot->serial);
|
||||
g_clipboardLock.Release();
|
||||
|
||||
incoming.count = 0;
|
||||
DataTransfer::Free(old);
|
||||
return Success;
|
||||
}
|
||||
|
||||
static int Sys_ClipboardClear() {
|
||||
DataTransfer::Transfer empty;
|
||||
empty.count = 0;
|
||||
return ClipboardReplace(empty);
|
||||
}
|
||||
|
||||
static int Sys_ClipboardSetText(const char* data, uint32_t len) {
|
||||
if (len == 0)
|
||||
return Sys_ClipboardClear();
|
||||
if (data == nullptr || len > CLIPBOARD_MAX_TEXT_BYTES)
|
||||
return InvalidArgument;
|
||||
if (Memory::g_heap == nullptr)
|
||||
return OutOfMemory;
|
||||
|
||||
for (uint32_t i = 0; i < len; i++) {
|
||||
if (data[i] == '\0')
|
||||
return InvalidArgument;
|
||||
}
|
||||
|
||||
const char* currentUser = ClipboardCurrentUser();
|
||||
if (currentUser == nullptr) return GenericFailure;
|
||||
|
||||
char user[32];
|
||||
ClipboardCopyUser(user, currentUser);
|
||||
|
||||
char* newText = (char*)Memory::g_heap->Request(len + 1);
|
||||
if (newText == nullptr) return OutOfMemory;
|
||||
for (uint32_t i = 0; i < len; i++)
|
||||
newText[i] = data[i];
|
||||
newText[len] = '\0';
|
||||
|
||||
char* oldText = nullptr;
|
||||
|
||||
g_clipboardLock.Acquire();
|
||||
ClipboardSlot* slot = ClipboardEnsureSlotLocked(user);
|
||||
if (slot == nullptr) {
|
||||
g_clipboardLock.Release();
|
||||
Memory::g_heap->Free(newText);
|
||||
return -1;
|
||||
DataTransfer::Transfer transfer;
|
||||
int rc = DataTransfer::BuildText(data, len, transfer);
|
||||
if (rc != Success) return rc;
|
||||
return ClipboardReplace(transfer);
|
||||
}
|
||||
|
||||
oldText = slot->text;
|
||||
slot->text = newText;
|
||||
slot->len = len;
|
||||
slot->serial = ClipboardNextSerial(slot->serial);
|
||||
g_clipboardLock.Release();
|
||||
|
||||
if (oldText != nullptr)
|
||||
Memory::g_heap->Free(oldText);
|
||||
|
||||
return Success;
|
||||
static int Sys_ClipboardSetData(const TransferItem* items, uint32_t count) {
|
||||
if (count == 0) return Sys_ClipboardClear();
|
||||
DataTransfer::Transfer transfer;
|
||||
int rc = DataTransfer::BuildFromUser(items, count, transfer);
|
||||
if (rc != Success) return rc;
|
||||
return ClipboardReplace(transfer);
|
||||
}
|
||||
|
||||
static int Sys_ClipboardGetInfo(ClipboardInfo* out) {
|
||||
@@ -150,7 +167,8 @@ namespace montauk::abi {
|
||||
g_clipboardLock.Acquire();
|
||||
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
|
||||
if (slot != nullptr) {
|
||||
out->textBytes = slot->len;
|
||||
const DataTransfer::Format* text = DataTransfer::Find(slot->transfer, TRANSFER_MIME_TEXT);
|
||||
out->textBytes = text ? text->len : 0;
|
||||
out->serial = slot->serial;
|
||||
}
|
||||
g_clipboardLock.Release();
|
||||
@@ -171,52 +189,57 @@ namespace montauk::abi {
|
||||
|
||||
g_clipboardLock.Acquire();
|
||||
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
|
||||
if (slot == nullptr || slot->len == 0) {
|
||||
if (slot != nullptr && outSerial != nullptr)
|
||||
*outSerial = slot->serial;
|
||||
const DataTransfer::Format* text =
|
||||
slot ? DataTransfer::Find(slot->transfer, TRANSFER_MIME_TEXT) : nullptr;
|
||||
if (text == nullptr || text->len == 0) {
|
||||
g_clipboardLock.Release();
|
||||
return Success;
|
||||
}
|
||||
|
||||
*outLen = slot->len;
|
||||
if (outSerial != nullptr) *outSerial = slot->serial;
|
||||
if (buffer == nullptr || bufferLen < slot->len) {
|
||||
int rc = DataTransfer::CopyOut(*text, buffer, bufferLen, outLen);
|
||||
g_clipboardLock.Release();
|
||||
return BufferTooSmall;
|
||||
return rc;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < slot->len; i++)
|
||||
buffer[i] = slot->text[i];
|
||||
g_clipboardLock.Release();
|
||||
return Success;
|
||||
}
|
||||
static int Sys_ClipboardFormats(TransferFormatInfo* out, uint32_t max, uint64_t* outSerial) {
|
||||
if (outSerial != nullptr) *outSerial = 0;
|
||||
|
||||
static int Sys_ClipboardClear() {
|
||||
const char* currentUser = ClipboardCurrentUser();
|
||||
if (currentUser == nullptr) return GenericFailure;
|
||||
|
||||
char user[32];
|
||||
ClipboardCopyUser(user, currentUser);
|
||||
|
||||
char* oldText = nullptr;
|
||||
|
||||
g_clipboardLock.Acquire();
|
||||
ClipboardSlot* slot = ClipboardEnsureSlotLocked(user);
|
||||
if (slot == nullptr) {
|
||||
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
|
||||
uint32_t n = 0;
|
||||
if (slot != nullptr) {
|
||||
if (outSerial != nullptr) *outSerial = slot->serial;
|
||||
n = out ? DataTransfer::ListFormats(slot->transfer, out, max) : 0;
|
||||
if (out == nullptr || max == 0) n = slot->transfer.count;
|
||||
}
|
||||
g_clipboardLock.Release();
|
||||
return -1;
|
||||
return (int)n;
|
||||
}
|
||||
|
||||
oldText = slot->text;
|
||||
slot->text = nullptr;
|
||||
slot->len = 0;
|
||||
slot->serial = ClipboardNextSerial(slot->serial);
|
||||
static int Sys_ClipboardGetData(const char* mime, void* buffer, uint32_t bufferLen, uint32_t* outLen) {
|
||||
if (outLen == nullptr) return InvalidArgument;
|
||||
*outLen = 0;
|
||||
|
||||
const char* currentUser = ClipboardCurrentUser();
|
||||
if (currentUser == nullptr) return GenericFailure;
|
||||
|
||||
char user[32];
|
||||
ClipboardCopyUser(user, currentUser);
|
||||
|
||||
g_clipboardLock.Acquire();
|
||||
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
|
||||
const DataTransfer::Format* format = slot ? DataTransfer::Find(slot->transfer, mime) : nullptr;
|
||||
int rc = format ? DataTransfer::CopyOut(*format, buffer, bufferLen, outLen) : NotFound;
|
||||
g_clipboardLock.Release();
|
||||
|
||||
if (oldText != nullptr)
|
||||
Memory::g_heap->Free(oldText);
|
||||
|
||||
return Success;
|
||||
return rc;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* DataTransfer.cpp
|
||||
* Multi-format payloads shared by the clipboard and drag and drop
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "DataTransfer.hpp"
|
||||
#include "UserMemory.hpp"
|
||||
#include <Memory/Heap.hpp>
|
||||
|
||||
namespace DataTransfer {
|
||||
|
||||
using namespace montauk::abi;
|
||||
|
||||
static bool MimeEquals(const char* a, const char* b) {
|
||||
int i = 0;
|
||||
for (; a[i] && b[i]; i++) {
|
||||
char ca = (a[i] >= 'A' && a[i] <= 'Z') ? a[i] + 32 : a[i];
|
||||
char cb = (b[i] >= 'A' && b[i] <= 'Z') ? b[i] + 32 : b[i];
|
||||
if (ca != cb) return false;
|
||||
}
|
||||
return a[i] == b[i];
|
||||
}
|
||||
|
||||
// Length of the "type/subtype" part, before any ";parameters".
|
||||
static int MimeBaseLen(const char* mime) {
|
||||
int n = 0;
|
||||
while (mime[n] && mime[n] != ';') n++;
|
||||
while (n > 0 && mime[n - 1] == ' ') n--;
|
||||
return n;
|
||||
}
|
||||
|
||||
static bool MimeBaseEquals(const char* a, const char* b) {
|
||||
int la = MimeBaseLen(a);
|
||||
int lb = MimeBaseLen(b);
|
||||
if (la != lb) return false;
|
||||
for (int i = 0; i < la; i++) {
|
||||
char ca = (a[i] >= 'A' && a[i] <= 'Z') ? a[i] + 32 : a[i];
|
||||
char cb = (b[i] >= 'A' && b[i] <= 'Z') ? b[i] + 32 : b[i];
|
||||
if (ca != cb) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ValidMime(const char* mime) {
|
||||
if (mime == nullptr) return false;
|
||||
bool slash = false;
|
||||
uint32_t i = 0;
|
||||
for (; mime[i]; i++) {
|
||||
if (i + 1 >= TRANSFER_MIME_MAX) return false;
|
||||
char c = mime[i];
|
||||
if (c < 0x20 || c > 0x7E) return false;
|
||||
if (c == '/') slash = true;
|
||||
}
|
||||
return i > 0 && slash;
|
||||
}
|
||||
|
||||
void Free(Transfer& transfer) {
|
||||
for (uint32_t i = 0; i < transfer.count; i++) {
|
||||
if (transfer.formats[i].data != nullptr) Memory::g_heap->Free(transfer.formats[i].data);
|
||||
transfer.formats[i].data = nullptr;
|
||||
transfer.formats[i].len = 0;
|
||||
}
|
||||
transfer.count = 0;
|
||||
}
|
||||
|
||||
static int AddFormat(Transfer& transfer, const char* mime, const uint8_t* data, uint32_t len) {
|
||||
if (transfer.count >= TRANSFER_MAX_FORMATS) return InvalidArgument;
|
||||
Format& f = transfer.formats[transfer.count];
|
||||
uint32_t m = 0;
|
||||
for (; mime[m] && m + 1 < TRANSFER_MIME_MAX; m++) f.mime[m] = mime[m];
|
||||
f.mime[m] = '\0';
|
||||
|
||||
// Never hand out a null pointer for an empty format; one byte keeps
|
||||
// "present but empty" distinct from "absent".
|
||||
f.data = (uint8_t*)Memory::g_heap->Request(len > 0 ? len : 1);
|
||||
if (f.data == nullptr) return OutOfMemory;
|
||||
for (uint32_t i = 0; i < len; i++) f.data[i] = data[i];
|
||||
f.len = len;
|
||||
transfer.count++;
|
||||
return Success;
|
||||
}
|
||||
|
||||
int BuildFromUser(const TransferItem* items, uint32_t count, Transfer& out) {
|
||||
out.count = 0;
|
||||
if (items == nullptr || count == 0 || count > TRANSFER_MAX_FORMATS) return InvalidArgument;
|
||||
if (Memory::g_heap == nullptr) return OutOfMemory;
|
||||
|
||||
// Validate everything before allocating anything.
|
||||
uint64_t total = 0;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
if (!UserMemory::String(items[i].mime, TRANSFER_MIME_MAX)) return BadPointer;
|
||||
if (!ValidMime((const char*)items[i].mime)) return InvalidArgument;
|
||||
if (items[i].len > 0 && !UserMemory::Range(items[i].data, items[i].len, false)) return BadPointer;
|
||||
total += items[i].len;
|
||||
for (uint32_t j = 0; j < i; j++) {
|
||||
if (MimeEquals((const char*)items[i].mime, (const char*)items[j].mime))
|
||||
return InvalidArgument;
|
||||
}
|
||||
}
|
||||
if (total > TRANSFER_MAX_BYTES) return InvalidArgument;
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
int rc = AddFormat(out, (const char*)items[i].mime,
|
||||
(const uint8_t*)items[i].data, items[i].len);
|
||||
if (rc != Success) {
|
||||
Free(out);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
return Success;
|
||||
}
|
||||
|
||||
int BuildText(const char* text, uint32_t len, Transfer& out) {
|
||||
out.count = 0;
|
||||
if (Memory::g_heap == nullptr) return OutOfMemory;
|
||||
int rc = AddFormat(out, TRANSFER_MIME_TEXT, (const uint8_t*)text, len);
|
||||
if (rc != Success) Free(out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
const Format* Find(const Transfer& transfer, const char* mime) {
|
||||
if (mime == nullptr) return nullptr;
|
||||
for (uint32_t i = 0; i < transfer.count; i++) {
|
||||
if (MimeEquals(transfer.formats[i].mime, mime)) return &transfer.formats[i];
|
||||
}
|
||||
bool has_params = false;
|
||||
for (int i = 0; mime[i]; i++) {
|
||||
if (mime[i] == ';') { has_params = true; break; }
|
||||
}
|
||||
if (has_params) return nullptr;
|
||||
for (uint32_t i = 0; i < transfer.count; i++) {
|
||||
if (MimeBaseEquals(transfer.formats[i].mime, mime)) return &transfer.formats[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t ListFormats(const Transfer& transfer, TransferFormatInfo* out, uint32_t max) {
|
||||
uint32_t n = 0;
|
||||
for (; n < transfer.count && n < max; n++) {
|
||||
for (uint32_t c = 0; c < TRANSFER_MIME_MAX; c++) out[n].mime[c] = transfer.formats[n].mime[c];
|
||||
out[n].len = transfer.formats[n].len;
|
||||
out[n]._reserved = 0;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
int CopyOut(const Format& format, void* buffer, uint32_t bufferLen, uint32_t* outLen) {
|
||||
if (outLen != nullptr) *outLen = format.len;
|
||||
if (format.len == 0) return Success;
|
||||
if (buffer == nullptr || bufferLen < format.len) return BufferTooSmall;
|
||||
uint8_t* dst = (uint8_t*)buffer;
|
||||
for (uint32_t i = 0; i < format.len; i++) dst[i] = format.data[i];
|
||||
return Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* DataTransfer.hpp
|
||||
* Multi-format payloads shared by the clipboard and drag and drop
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include "Syscall.hpp"
|
||||
|
||||
// A transfer is a small set of formats, each one MIME type plus bytes, all
|
||||
// owned by the kernel heap. Producers offer every representation they can
|
||||
// (a file list and its paths as text, say) and each consumer reads the one it
|
||||
// understands. The clipboard holds one transfer per user; the window server
|
||||
// holds the active drag's.
|
||||
namespace DataTransfer {
|
||||
|
||||
struct Format {
|
||||
char mime[montauk::abi::TRANSFER_MIME_MAX];
|
||||
uint8_t* data;
|
||||
uint32_t len;
|
||||
};
|
||||
|
||||
struct Transfer {
|
||||
Format formats[montauk::abi::TRANSFER_MAX_FORMATS];
|
||||
uint32_t count;
|
||||
};
|
||||
|
||||
// Copy `count` formats in from user memory, validating every pointer the
|
||||
// items carry (the item array itself must already be validated). On
|
||||
// failure `out` is left empty. Returns a StatusCodes value.
|
||||
int BuildFromUser(const montauk::abi::TransferItem* items, uint32_t count, Transfer& out);
|
||||
|
||||
// A transfer holding just `text` as TRANSFER_MIME_TEXT.
|
||||
int BuildText(const char* text, uint32_t len, Transfer& out);
|
||||
|
||||
void Free(Transfer& transfer);
|
||||
|
||||
// Exact match first; a request without parameters ("text/plain") also
|
||||
// matches a stored format that has them ("text/plain;charset=utf-8").
|
||||
const Format* Find(const Transfer& transfer, const char* mime);
|
||||
|
||||
uint32_t ListFormats(const Transfer& transfer, montauk::abi::TransferFormatInfo* out, uint32_t max);
|
||||
|
||||
// Copy a format to a (validated) user buffer. *outLen always receives the
|
||||
// full length; BufferTooSmall when it does not fit, nothing copied then.
|
||||
int CopyOut(const Format& format, void* buffer, uint32_t bufferLen, uint32_t* outLen);
|
||||
|
||||
// MIME string checks shared by every entry point: "type/subtype" with
|
||||
// optional parameters, printable ASCII, shorter than TRANSFER_MIME_MAX.
|
||||
bool ValidMime(const char* mime);
|
||||
}
|
||||
+30
-18
@@ -6,18 +6,20 @@
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Hal/Apic/ApicInit.hpp>
|
||||
#include <Drivers/PS2/PS2Controller.hpp>
|
||||
#include <Drivers/USB/Xhci.hpp>
|
||||
#include <Drivers/USB/UsbDevice.hpp>
|
||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/Net/E1000.hpp>
|
||||
#include <Drivers/Net/E1000E.hpp>
|
||||
#include <Drivers/Net/Genet.hpp>
|
||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||
#include <Drivers/Graphics/IntelGPU.hpp>
|
||||
#include <Drivers/Storage/Ahci.hpp>
|
||||
#include <Drivers/Audio/IntelHda.hpp>
|
||||
#include <Pci/Pci.hpp>
|
||||
#include <Arch/Arch.hpp>
|
||||
#include <Arch/Platform.hpp>
|
||||
#include <Arch/Smp.hpp>
|
||||
|
||||
#include "Syscall.hpp"
|
||||
#include <Drivers/Storage/BlockDevice.hpp>
|
||||
@@ -69,28 +71,21 @@ namespace montauk::abi {
|
||||
};
|
||||
|
||||
// CPU cores (category 0)
|
||||
int cpuCount = Hal::GetDetectedCpuCount();
|
||||
int cpuCount = Smp::GetDetectedCpuCount();
|
||||
if (cpuCount > 0) {
|
||||
char detail[48];
|
||||
int p = 0;
|
||||
p = dl_append(detail, p, "x86_64, ", 48);
|
||||
p = dl_append(detail, p, MONTAUK_ARCH_NAME ", ", 48);
|
||||
p = dl_append_dec(detail, p, cpuCount, 48);
|
||||
p = dl_append(detail, p, " core(s)", 48);
|
||||
add(0, "Processor", detail);
|
||||
}
|
||||
|
||||
// Interrupt controllers (category 1)
|
||||
add(1, "Local APIC", "");
|
||||
add(1, "I/O APIC", "");
|
||||
|
||||
// Timer (category 2)
|
||||
add(2, "LAPIC Timer", "Local APIC periodic timer");
|
||||
|
||||
// PS/2 Input (category 3)
|
||||
add(3, "PS/2 Keyboard", "IRQ 1");
|
||||
if (Drivers::PS2::IsDualChannel()) {
|
||||
add(3, "PS/2 Mouse", "IRQ 12");
|
||||
}
|
||||
// Interrupt controllers (category 1), timer (2), platform input (3)
|
||||
Arch::ListPlatformDevices([](void* ctx, uint8_t category,
|
||||
const char* name, const char* detail) {
|
||||
(*(decltype(add)*)ctx)(category, name, detail);
|
||||
}, &add);
|
||||
|
||||
// USB devices (category 4)
|
||||
if (Drivers::USB::Xhci::IsInitialized()) {
|
||||
@@ -128,11 +123,24 @@ namespace montauk::abi {
|
||||
if (Drivers::Net::E1000E::IsInitialized()) {
|
||||
add(5, "Intel E1000E", "Gigabit Ethernet (82574L)");
|
||||
}
|
||||
if (Drivers::Net::Genet::IsInitialized()) {
|
||||
uint32_t speed = Drivers::Net::Genet::GetLinkSpeed();
|
||||
add(5, "Broadcom GENET", speed == 1000 ? "Gigabit Ethernet (1000 Mbit/s)"
|
||||
: speed == 100 ? "Gigabit Ethernet (100 Mbit/s)"
|
||||
: speed == 10 ? "Gigabit Ethernet (10 Mbit/s)"
|
||||
: "Gigabit Ethernet (no link)");
|
||||
}
|
||||
if (Drivers::Net::Wifi::IsPresent()) {
|
||||
add(5, "Intel Wi-Fi", Drivers::Net::Wifi::IsLinkUp()
|
||||
? "802.11 wireless (connected)"
|
||||
: "802.11 wireless");
|
||||
}
|
||||
// An on-board UART Bluetooth controller (Raspberry Pi) is no USB
|
||||
// device, so it is listed here; USB adapters appear above.
|
||||
if (Drivers::Bluetooth::IsInitialized() &&
|
||||
Drivers::Bluetooth::Hci::GetTransport() == Drivers::Bluetooth::Hci::Transport::Uart) {
|
||||
add(5, "Bluetooth Adapter", "Broadcom, HCI over UART");
|
||||
}
|
||||
|
||||
// Display (category 6)
|
||||
if (Drivers::Graphics::IntelGPU::IsInitialized()) {
|
||||
@@ -154,7 +162,7 @@ namespace montauk::abi {
|
||||
add(9, "Intel HDA", detail);
|
||||
}
|
||||
|
||||
if (Drivers::USB::Bluetooth::IsInitialized()) {
|
||||
if (Drivers::Bluetooth::IsInitialized()) {
|
||||
add(9, "Bluetooth Audio", "A2DP (SBC)");
|
||||
}
|
||||
|
||||
@@ -254,6 +262,10 @@ namespace montauk::abi {
|
||||
case Drivers::Storage::BLOCK_KIND_USB_MSC:
|
||||
buf->type = 4;
|
||||
break;
|
||||
case Drivers::Storage::BLOCK_KIND_SD:
|
||||
buf->type = 5;
|
||||
buf->rpm = 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,23 @@ namespace montauk::abi {
|
||||
return required == 0 || Sched::HasCapability(required);
|
||||
}
|
||||
|
||||
// Drivers fail a create under a missing parent with a bare -1; report it
|
||||
// as NoSuchPath so callers can tell it apart (Rust's create_dir_all only
|
||||
// creates the missing ancestors when mkdir says NotFound).
|
||||
static bool ParentDirExists(const char* resolved) {
|
||||
char parent[256];
|
||||
int len = Lib::strlen(resolved);
|
||||
if (len >= (int)sizeof(parent)) return false;
|
||||
memcpy(parent, resolved, len + 1);
|
||||
while (len > 0 && parent[len - 1] == '/') len--;
|
||||
while (len > 0 && parent[len - 1] != '/') len--;
|
||||
while (len > 0 && parent[len - 1] == '/') len--;
|
||||
if (len == 0 || parent[len - 1] == ':') return true; // drive root
|
||||
parent[len] = '\0';
|
||||
Fs::Vfs::StatInfo info;
|
||||
return Fs::Vfs::VfsStat(parent, info) == 0 && info.isDir;
|
||||
}
|
||||
|
||||
static int Sys_Open(const char* path) {
|
||||
char resolved[256];
|
||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
|
||||
@@ -77,14 +94,14 @@ namespace montauk::abi {
|
||||
proc->readdirCursor = (slot + 1) % Sched::UserReadDirSlots;
|
||||
|
||||
uint64_t userVa = Sched::UserReadDirBase + (uint64_t)slot * 0x1000ULL;
|
||||
uint64_t physAddr = Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys, userVa);
|
||||
uint64_t physAddr = Memory::VMM::Paging::GetPhysAddr(proc->pageTableRoot, userVa);
|
||||
uint8_t* pageBuf = nullptr;
|
||||
|
||||
if (physAddr == 0) {
|
||||
void* page = Memory::g_pfa->AllocateZeroed();
|
||||
if (page == nullptr) return OutOfMemory;
|
||||
physAddr = Memory::SubHHDM((uint64_t)page);
|
||||
if (!Memory::VMM::Paging::MapUserIn(proc->pml4Phys, physAddr, userVa)) {
|
||||
if (!Memory::VMM::Paging::MapUserIn(proc->pageTableRoot, physAddr, userVa)) {
|
||||
Memory::g_pfa->Free(page);
|
||||
return OutOfMemory;
|
||||
}
|
||||
@@ -117,6 +134,7 @@ namespace montauk::abi {
|
||||
char resolved[256];
|
||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
|
||||
if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
|
||||
if (!ParentDirExists(resolved)) return NoSuchPath;
|
||||
return Ipc::CreateFileHandle(resolved);
|
||||
}
|
||||
|
||||
@@ -171,6 +189,7 @@ namespace montauk::abi {
|
||||
char resolved[256];
|
||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
|
||||
if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
|
||||
if (!ParentDirExists(resolved)) return NoSuchPath;
|
||||
return Fs::Vfs::VfsMkdir(resolved);
|
||||
}
|
||||
|
||||
|
||||
+23
-13
@@ -12,6 +12,7 @@
|
||||
#include <Graphics/Framebuffer.hpp>
|
||||
#include <Drivers/Graphics/IntelGPU.hpp>
|
||||
#include <ACPI/AcpiVideo.hpp>
|
||||
#include <ACPI/WmiEcBacklight.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
|
||||
#include "Syscall.hpp"
|
||||
@@ -82,7 +83,7 @@ namespace montauk::abi {
|
||||
|
||||
for (uint64_t i = 0; i < numPages; i++) {
|
||||
if (!Memory::VMM::Paging::MapUserInWC(
|
||||
proc->pml4Phys,
|
||||
proc->pageTableRoot,
|
||||
fbPhys + i * 0x1000,
|
||||
userVa + i * 0x1000
|
||||
)) {
|
||||
@@ -97,7 +98,7 @@ namespace montauk::abi {
|
||||
uint64_t buf1Phys = Drivers::Graphics::IntelGPU::GetBufferPhys(1);
|
||||
for (uint64_t i = 0; i < numPages; i++) {
|
||||
if (!Memory::VMM::Paging::MapUserInWC(
|
||||
proc->pml4Phys,
|
||||
proc->pageTableRoot,
|
||||
buf1Phys + i * 0x1000,
|
||||
userVa + (numPages + i) * 0x1000
|
||||
)) {
|
||||
@@ -129,29 +130,38 @@ namespace montauk::abi {
|
||||
return GPU::Flip((int)index, (flags & 1) != 0, proc->pid);
|
||||
}
|
||||
|
||||
// The firmware's ACPI brightness methods drive the built-in panel, so
|
||||
// they stand in only when that panel is what is on screen (or no native
|
||||
// driver knows what is): never for an external monitor without DDC/CI.
|
||||
static bool AcpiBrightnessApplies() {
|
||||
// Firmware brightness interfaces (EC via WMI, ACPI _BCM) drive the
|
||||
// built-in panel, so they apply only while that panel is what is on
|
||||
// screen (or no native driver knows what is): never for an external
|
||||
// monitor, where they would silently dim a closed lid instead.
|
||||
static bool PanelIsActiveOutput() {
|
||||
if (!Drivers::Graphics::IntelGPU::IsInitialized()) return true;
|
||||
uint8_t type = Drivers::Graphics::IntelGPU::ActiveConnectorType();
|
||||
return type == DISPLAY_CONNECTOR_EDP || type == DISPLAY_CONNECTOR_LVDS;
|
||||
}
|
||||
|
||||
// Order follows who actually owns the backlight. A firmware that names
|
||||
// the EC as the source has taken it away from the GPU, whose PWM then
|
||||
// still accepts writes that reach nothing; so the EC interface goes
|
||||
// first, then the GPU's own controls, then ACPI _BCM as a last resort.
|
||||
static int DisplayBrightness(int percent) {
|
||||
bool panel = PanelIsActiveOutput();
|
||||
if (panel) {
|
||||
int rc = Hal::WmiEcBacklight::Brightness(percent);
|
||||
if (rc != -1) return rc;
|
||||
}
|
||||
int rc = Drivers::Graphics::IntelGPU::Brightness(percent);
|
||||
if (rc == -1 && AcpiBrightnessApplies())
|
||||
rc = Hal::AcpiVideo::Brightness(percent);
|
||||
if (rc == -1 && panel) rc = Hal::AcpiVideo::Brightness(percent);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int64_t Sys_DisplayInfo(DisplayInfo* out) {
|
||||
if (!Drivers::Graphics::IntelGPU::GetDisplayInfo(out)) return NoDevice;
|
||||
if (out->brightness < 0 && AcpiBrightnessApplies()) {
|
||||
out->brightness = Hal::AcpiVideo::Brightness(-1);
|
||||
if (out->brightness >= 0) out->capabilities |= DISPLAY_CAP_BRIGHTNESS;
|
||||
else out->brightness = -1;
|
||||
}
|
||||
// The driver only knows its own backends; report what a set would use.
|
||||
int level = DisplayBrightness(-1);
|
||||
out->brightness = level >= 0 ? level : -1;
|
||||
if (level >= 0) out->capabilities |= DISPLAY_CAP_BRIGHTNESS;
|
||||
else out->capabilities &= ~DISPLAY_CAP_BRIGHTNESS;
|
||||
return Success;
|
||||
}
|
||||
|
||||
|
||||
+15
-15
@@ -107,7 +107,7 @@ namespace montauk::abi {
|
||||
if (page == nullptr) break;
|
||||
uint64_t phys = Memory::SubHHDM((uint64_t)page);
|
||||
if (!Memory::VMM::Paging::MapUserInPermissions(
|
||||
proc->pml4Phys, phys, pageVa, writable, executable)) {
|
||||
proc->pageTableRoot, phys, pageVa, writable, executable)) {
|
||||
Memory::g_pfa->Free(page);
|
||||
break;
|
||||
}
|
||||
@@ -131,7 +131,7 @@ namespace montauk::abi {
|
||||
|
||||
// Reset heap allocation tracking for a process slot.
|
||||
// The actual physical pages are freed by Paging::FreeUserHalf() during process cleanup.
|
||||
inline void CleanupHeapForSlot(int slot, uint64_t /*pml4Phys*/) {
|
||||
inline void CleanupHeapForSlot(int slot, uint64_t /*pageTableRoot*/) {
|
||||
if (slot < 0 || slot >= Sched::MaxProcesses) return;
|
||||
g_heapLocks[slot].Acquire();
|
||||
HeapAlloc* area = g_heapAllocs[slot];
|
||||
@@ -182,7 +182,7 @@ namespace montauk::abi {
|
||||
}
|
||||
for (uint64_t i = 0; i < area->numPages; i++)
|
||||
if (Memory::VMM::Paging::GetPhysAddr(
|
||||
proc->pml4Phys, area->va + i * 0x1000ULL) != 0)
|
||||
proc->pageTableRoot, area->va + i * 0x1000ULL) != 0)
|
||||
resident++;
|
||||
*link = area->next;
|
||||
area->next = released;
|
||||
@@ -194,7 +194,7 @@ namespace montauk::abi {
|
||||
// user TLB entry can otherwise corrupt the frame's next owner.
|
||||
while (released != nullptr) {
|
||||
HeapAlloc* next = released->next;
|
||||
Memory::UnmapAndFreeUserRange(proc->pml4Phys, released->va,
|
||||
Memory::UnmapAndFreeUserRange(proc->pageTableRoot, released->va,
|
||||
released->numPages);
|
||||
Sched::ReleaseUserHeapRange(slot, released->va,
|
||||
released->numPages * 0x1000ULL);
|
||||
@@ -253,11 +253,11 @@ namespace montauk::abi {
|
||||
|
||||
uint64_t resident = 0;
|
||||
for (uint64_t i = 0; i < pages; i++)
|
||||
if (Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys, addr + i * 0x1000ULL) != 0)
|
||||
if (Memory::VMM::Paging::GetPhysAddr(proc->pageTableRoot, addr + i * 0x1000ULL) != 0)
|
||||
resident++;
|
||||
g_heapLocks[slot].Release();
|
||||
|
||||
Memory::UnmapAndFreeUserRange(proc->pml4Phys, addr, pages);
|
||||
Memory::UnmapAndFreeUserRange(proc->pageTableRoot, addr, pages);
|
||||
Sched::ReleaseUserHeapRange(slot, addr, size);
|
||||
g_heapLocks[slot].Acquire();
|
||||
Sched::g_allocatedPages[slot] -= resident;
|
||||
@@ -336,10 +336,10 @@ namespace montauk::abi {
|
||||
bool writable = (prot & VmProtWrite) != 0;
|
||||
bool executable = (prot & VmProtExec) != 0;
|
||||
for (uint64_t i = 0; i < pages; i++) {
|
||||
if (Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys,
|
||||
if (Memory::VMM::Paging::GetPhysAddr(proc->pageTableRoot,
|
||||
addr + i * 0x1000ULL) == 0)
|
||||
continue;
|
||||
if (!Memory::VMM::Paging::ProtectUserIn(proc->pml4Phys,
|
||||
if (!Memory::VMM::Paging::ProtectUserIn(proc->pageTableRoot,
|
||||
addr + i * 0x1000ULL,
|
||||
writable, executable)) {
|
||||
g_heapLocks[slot].Release();
|
||||
@@ -347,17 +347,17 @@ namespace montauk::abi {
|
||||
}
|
||||
}
|
||||
g_heapLocks[slot].Release();
|
||||
Memory::ShootdownUserRange(proc->pml4Phys, addr, (uint32_t)pages);
|
||||
Memory::ShootdownUserRange(proc->pageTableRoot, addr, (uint32_t)pages);
|
||||
return Success;
|
||||
}
|
||||
|
||||
// Resolve a non-present access inside an anonymous VMA. This is used for
|
||||
// both ring-3 faults and kernel copies to untouched userspace buffers.
|
||||
inline bool TryHandleAnonymousPageFault(uint64_t faultAddr, uint64_t errorCode) {
|
||||
inline bool TryHandleAnonymousPageFault(uint64_t faultAddr, const Memory::VMM::PageFault& fault) {
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
int slot = GetCurrentSlot();
|
||||
if (proc == nullptr || slot < 0 || faultAddr < Sched::UserHeapBase ||
|
||||
faultAddr >= Sched::UserHeapLimit || (errorCode & 1) != 0) return false;
|
||||
faultAddr >= Sched::UserHeapLimit || fault.present) return false;
|
||||
|
||||
uint64_t pageVa = faultAddr & ~0xFFFULL;
|
||||
g_heapLocks[slot].Acquire();
|
||||
@@ -370,14 +370,14 @@ namespace montauk::abi {
|
||||
}
|
||||
}
|
||||
if (found == nullptr ||
|
||||
((errorCode & (1ULL << 1)) != 0 && (found->prot & VmProtWrite) == 0) ||
|
||||
((errorCode & (1ULL << 4)) != 0 && (found->prot & VmProtExec) == 0)) {
|
||||
(fault.write && (found->prot & VmProtWrite) == 0) ||
|
||||
(fault.execute && (found->prot & VmProtExec) == 0)) {
|
||||
g_heapLocks[slot].Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
// A sibling may have resolved the same fault while this CPU waited.
|
||||
if (Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys, pageVa) != 0) {
|
||||
if (Memory::VMM::Paging::GetPhysAddr(proc->pageTableRoot, pageVa) != 0) {
|
||||
g_heapLocks[slot].Release();
|
||||
return true;
|
||||
}
|
||||
@@ -389,7 +389,7 @@ namespace montauk::abi {
|
||||
}
|
||||
uint64_t phys = Memory::SubHHDM((uint64_t)page);
|
||||
bool mapped = Memory::VMM::Paging::MapUserInPermissions(
|
||||
proc->pml4Phys, phys, pageVa,
|
||||
proc->pageTableRoot, phys, pageVa,
|
||||
(found->prot & VmProtWrite) != 0, (found->prot & VmProtExec) != 0);
|
||||
if (!mapped) Memory::g_pfa->Free(page);
|
||||
else Sched::g_allocatedPages[slot]++;
|
||||
|
||||
@@ -17,13 +17,13 @@ namespace montauk::abi {
|
||||
|
||||
// Copy strings into fixed-size arrays (user-accessible)
|
||||
const char* name = "MontaukOS";
|
||||
const char* ver = "0.2.0";
|
||||
const char* ver = "0.2.1";
|
||||
for (int i = 0; name[i]; i++) outInfo->osName[i] = name[i];
|
||||
outInfo->osName[9] = '\0';
|
||||
for (int i = 0; ver[i]; i++) outInfo->osVersion[i] = ver[i];
|
||||
outInfo->osVersion[5] = '\0';
|
||||
|
||||
outInfo->apiVersion = 11;
|
||||
outInfo->apiVersion = 12;
|
||||
outInfo->maxProcesses = Sched::MaxProcesses;
|
||||
outInfo->buildNumber = MONTAUK_BUILD_NUMBER;
|
||||
}
|
||||
|
||||
@@ -122,8 +122,9 @@ namespace montauk::abi {
|
||||
return WriteAllToStream(stream, (const uint8_t*)data, len);
|
||||
}
|
||||
|
||||
static int Sys_ChildIoWriteKey(int childPid, const KeyEvent* key) {
|
||||
if (key == nullptr) return InvalidArgument;
|
||||
// Queue a key event (raw bytes, `len` of them) on a redirected child's key
|
||||
// mailbox. Readers accept both the 9-byte legacy and the full format.
|
||||
static int ChildIoQueueKey(int childPid, const void* key, uint16_t len) {
|
||||
auto* child = Sched::GetProcessByPid(childPid);
|
||||
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
|
||||
return PermissionDenied;
|
||||
@@ -133,7 +134,7 @@ namespace montauk::abi {
|
||||
|
||||
for (;;) {
|
||||
uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
|
||||
int rc = Ipc::MailboxSend(mailbox, 0, key, sizeof(KeyEvent));
|
||||
int rc = Ipc::MailboxSend(mailbox, 0, key, len);
|
||||
if (rc < 0) return GenericFailure;
|
||||
if (rc == 0) {
|
||||
Sched::BlockOnObjectSince(mailbox, 0, observedWake);
|
||||
@@ -143,6 +144,22 @@ namespace montauk::abi {
|
||||
}
|
||||
}
|
||||
|
||||
static int Sys_ChildIoWriteKey(int childPid, const KeyEventV1* key) {
|
||||
if (key == nullptr) return InvalidArgument;
|
||||
KeyEventV1 copy = *key;
|
||||
return ChildIoQueueKey(childPid, ©, sizeof(copy));
|
||||
}
|
||||
|
||||
// Size-versioned: copies min(size, sizeof(KeyEvent)) bytes (validated
|
||||
// readable by the dispatcher) and zero-fills the rest.
|
||||
static int Sys_ChildIoWriteKeyEx(int childPid, const void* key, uint64_t size) {
|
||||
if (key == nullptr || size < sizeof(KeyEventV1)) return InvalidArgument;
|
||||
KeyEvent ev;
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
memcpy(&ev, key, size < sizeof(ev) ? size : sizeof(ev));
|
||||
return ChildIoQueueKey(childPid, &ev, sizeof(ev));
|
||||
}
|
||||
|
||||
static int Sys_ChildIoSetTermsz(int childPid, int cols, int rows) {
|
||||
auto* child = Sched::GetProcessByPid(childPid);
|
||||
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
|
||||
|
||||
+137
-22
@@ -12,6 +12,107 @@
|
||||
#include "Common.hpp"
|
||||
|
||||
namespace montauk::abi {
|
||||
// ---- Key text ----------------------------------------------------------
|
||||
// Keyboard layouts are applied in userspace (desktop, terminal), which send
|
||||
// KeyEvents carrying the Unicode codepoint. Binaries built before that send
|
||||
// 9-byte KeyEventV1s whose `ascii` is a Windows-1252 byte; both kinds share
|
||||
// a redirected child's key mailbox, told apart by message length.
|
||||
|
||||
static uint32_t KeyCp1252ToUnicode(uint8_t b) {
|
||||
static constexpr uint16_t high[32] = {
|
||||
0x20AC, 0x81, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021,
|
||||
0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x8D, 0x017D, 0x8F,
|
||||
0x90, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
|
||||
0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x9D, 0x017E, 0x0178,
|
||||
};
|
||||
return (b >= 0x80 && b <= 0x9F) ? high[b - 0x80] : b;
|
||||
}
|
||||
|
||||
// The character a key event types, or 0 for control/navigation keys.
|
||||
static uint32_t KeyTextCodepoint(const KeyEvent& ev) {
|
||||
if (ev.codepoint) return ev.codepoint;
|
||||
uint8_t b = (uint8_t)ev.ascii;
|
||||
if (b < 0x20 || b == 0x7F) return 0;
|
||||
return KeyCp1252ToUnicode(b);
|
||||
}
|
||||
|
||||
static int KeyEncodeUtf8(uint32_t cp, uint8_t* out) {
|
||||
if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) cp = 0xFFFD;
|
||||
if (cp < 0x80) { out[0] = (uint8_t)cp; return 1; }
|
||||
if (cp < 0x800) {
|
||||
out[0] = (uint8_t)(0xC0 | (cp >> 6));
|
||||
out[1] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||
return 2;
|
||||
}
|
||||
if (cp < 0x10000) {
|
||||
out[0] = (uint8_t)(0xE0 | (cp >> 12));
|
||||
out[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
|
||||
out[2] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||
return 3;
|
||||
}
|
||||
out[0] = (uint8_t)(0xF0 | (cp >> 18));
|
||||
out[1] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
|
||||
out[2] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
|
||||
out[3] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||
return 4;
|
||||
}
|
||||
|
||||
static void KeyFromDriver(const Drivers::PS2::Keyboard::KeyEvent& k, KeyEvent* out) {
|
||||
memset(out, 0, sizeof(KeyEvent));
|
||||
out->scancode = k.Scancode;
|
||||
out->ascii = k.Ascii;
|
||||
out->pressed = k.Pressed;
|
||||
out->shift = k.Shift;
|
||||
out->ctrl = k.Ctrl;
|
||||
out->alt = k.Alt;
|
||||
out->capslock = k.CapsLock;
|
||||
out->altgr = k.AltGr;
|
||||
out->extended = k.Extended;
|
||||
// The kernel's own scancode table is US ASCII.
|
||||
uint8_t a = (uint8_t)k.Ascii;
|
||||
out->codepoint = (a >= 0x20 && a < 0x7F) ? a : 0;
|
||||
}
|
||||
|
||||
// Receive one key from a redirected child's mailbox as a full KeyEvent,
|
||||
// upgrading legacy 9-byte messages. Returns MailboxRecv's result.
|
||||
static int KeyMailboxRecv(Ipc::Mailbox* mailbox, KeyEvent* out) {
|
||||
memset(out, 0, sizeof(KeyEvent));
|
||||
uint16_t len = sizeof(KeyEvent);
|
||||
int rc = Ipc::MailboxRecv(mailbox, nullptr, out, &len, true);
|
||||
if (rc > 0 && len < sizeof(KeyEvent)) {
|
||||
memset((uint8_t*)out + len, 0, sizeof(KeyEvent) - len);
|
||||
out->codepoint = KeyTextCodepoint(*out);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
// Turn a pressed key into the next getchar byte. Characters above ASCII are
|
||||
// returned as UTF-8, one byte per call; the rest is parked in the process.
|
||||
static bool KeyTakeChar(Sched::Process* proc, const KeyEvent& ev, char* out) {
|
||||
if (!ev.pressed) return false;
|
||||
uint32_t cp = KeyTextCodepoint(ev);
|
||||
if (cp >= 0x80 && proc != nullptr) {
|
||||
int n = KeyEncodeUtf8(cp, proc->getcharUtf8);
|
||||
proc->getcharUtf8Len = (uint8_t)n;
|
||||
proc->getcharUtf8Pos = 1;
|
||||
*out = (char)proc->getcharUtf8[0];
|
||||
return true;
|
||||
}
|
||||
if (ev.ascii != 0 && ev.ascii != KEY_ASCII_SUBSTITUTE) {
|
||||
*out = ev.ascii;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool KeyPendingChar(Sched::Process* proc, char* out) {
|
||||
if (proc == nullptr || proc->getcharUtf8Pos >= proc->getcharUtf8Len) return false;
|
||||
*out = (char)proc->getcharUtf8[proc->getcharUtf8Pos++];
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- Syscalls ------------------------------------------------------------
|
||||
|
||||
static bool Sys_IsKeyAvailable() {
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
if (proc && proc->redirected) {
|
||||
@@ -27,8 +128,8 @@ namespace montauk::abi {
|
||||
return Drivers::PS2::Keyboard::IsKeyAvailable();
|
||||
}
|
||||
|
||||
static void Sys_GetKey(KeyEvent* outEvent) {
|
||||
if (outEvent == nullptr) return;
|
||||
// Blocking read of the next key event, in the full format.
|
||||
static void ReadKeyEvent(KeyEvent* outEvent) {
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
if (proc && proc->redirected) {
|
||||
Ipc::HandleSnapshot snapshot;
|
||||
@@ -36,8 +137,7 @@ namespace montauk::abi {
|
||||
if (mailbox != nullptr) {
|
||||
for (;;) {
|
||||
uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
|
||||
uint16_t len = sizeof(KeyEvent);
|
||||
int rc = Ipc::MailboxRecv(mailbox, nullptr, outEvent, &len, true);
|
||||
int rc = KeyMailboxRecv(mailbox, outEvent);
|
||||
if (rc > 0) return;
|
||||
if (rc < 0) {
|
||||
memset(outEvent, 0, sizeof(KeyEvent));
|
||||
@@ -47,20 +147,33 @@ namespace montauk::abi {
|
||||
}
|
||||
}
|
||||
}
|
||||
auto k = Drivers::PS2::Keyboard::GetKey();
|
||||
outEvent->scancode = k.Scancode;
|
||||
outEvent->ascii = k.Ascii;
|
||||
outEvent->pressed = k.Pressed;
|
||||
outEvent->shift = k.Shift;
|
||||
outEvent->ctrl = k.Ctrl;
|
||||
outEvent->alt = k.Alt;
|
||||
outEvent->capslock = k.CapsLock;
|
||||
outEvent->altgr = k.AltGr;
|
||||
outEvent->extended = k.Extended;
|
||||
KeyFromDriver(Drivers::PS2::Keyboard::GetKey(), outEvent);
|
||||
}
|
||||
|
||||
// SYS_GETKEY: legacy 9-byte event. A new-format event's first 9 bytes are
|
||||
// already a valid KeyEventV1 (ascii holds the 1252 byte or SUB).
|
||||
static void Sys_GetKey(KeyEventV1* outEvent) {
|
||||
if (outEvent == nullptr) return;
|
||||
KeyEvent ev;
|
||||
ReadKeyEvent(&ev);
|
||||
memcpy(outEvent, &ev, sizeof(KeyEventV1));
|
||||
}
|
||||
|
||||
// SYS_GETKEY_EX: size-versioned. The caller's size was validated by the
|
||||
// dispatcher (>= sizeof(KeyEventV1), writable).
|
||||
static int Sys_GetKeyEx(void* outEvent, uint64_t size) {
|
||||
KeyEvent ev;
|
||||
ReadKeyEvent(&ev);
|
||||
uint64_t n = size < sizeof(KeyEvent) ? size : sizeof(KeyEvent);
|
||||
memcpy(outEvent, &ev, n);
|
||||
if (size > n) memset((uint8_t*)outEvent + n, 0, size - n);
|
||||
return Success;
|
||||
}
|
||||
|
||||
static char Sys_GetChar() {
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
char pending;
|
||||
if (KeyPendingChar(proc, &pending)) return pending;
|
||||
if (proc && proc->redirected) {
|
||||
Ipc::HandleSnapshot inputSnapshot;
|
||||
Ipc::HandleSnapshot mailboxSnapshot;
|
||||
@@ -78,11 +191,11 @@ namespace montauk::abi {
|
||||
}
|
||||
|
||||
if (mailbox != nullptr) {
|
||||
KeyEvent ev{};
|
||||
uint16_t len = sizeof(ev);
|
||||
int rc = Ipc::MailboxRecv(mailbox, nullptr, &ev, &len, true);
|
||||
KeyEvent ev;
|
||||
int rc = KeyMailboxRecv(mailbox, &ev);
|
||||
if (rc > 0) {
|
||||
if (ev.pressed && ev.ascii != 0) return ev.ascii;
|
||||
char c;
|
||||
if (KeyTakeChar(proc, ev, &c)) return c;
|
||||
continue;
|
||||
}
|
||||
if (rc < 0 && input == nullptr) return 0;
|
||||
@@ -115,6 +228,8 @@ namespace montauk::abi {
|
||||
*/
|
||||
static char Sys_GetCharNb() {
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
char pending;
|
||||
if (KeyPendingChar(proc, &pending)) return pending;
|
||||
if (proc && proc->redirected) {
|
||||
Ipc::HandleSnapshot inputSnapshot;
|
||||
Ipc::HandleSnapshot mailboxSnapshot;
|
||||
@@ -127,10 +242,10 @@ namespace montauk::abi {
|
||||
}
|
||||
|
||||
while (mailbox != nullptr) {
|
||||
KeyEvent ev{};
|
||||
uint16_t len = sizeof(ev);
|
||||
if (Ipc::MailboxRecv(mailbox, nullptr, &ev, &len, true) <= 0) break;
|
||||
if (ev.pressed && ev.ascii != 0) return ev.ascii;
|
||||
KeyEvent ev;
|
||||
if (KeyMailboxRecv(mailbox, &ev) <= 0) break;
|
||||
char c;
|
||||
if (KeyTakeChar(proc, ev, &c)) return c;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace montauk::abi {
|
||||
}
|
||||
|
||||
// Load the library into the process's address space
|
||||
uint64_t loadBias = Sched::ElfLoadLib(path, proc->pml4Phys, libSlot);
|
||||
uint64_t loadBias = Sched::ElfLoadLib(path, proc->pageTableRoot, libSlot);
|
||||
if (loadBias == 0) {
|
||||
Kt::KernelLogStream(Kt::ERROR, "Lib") << "Failed to load library: " << path;
|
||||
g_libLocks[slot].Release();
|
||||
@@ -134,7 +134,7 @@ namespace montauk::abi {
|
||||
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
if (proc != nullptr) {
|
||||
Memory::UnmapAndFreeUserRange(proc->pml4Phys, libBase,
|
||||
Memory::UnmapAndFreeUserRange(proc->pageTableRoot, libBase,
|
||||
(libEnd - libBase) / 0x1000ULL);
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ namespace montauk::abi {
|
||||
uint64_t libBase = GetLibSlotBase(i);
|
||||
uint64_t libEnd = libBase + Sched::LIB_MAX_SIZE;
|
||||
|
||||
Memory::UnmapAndFreeUserRange(proc->pml4Phys, libBase,
|
||||
Memory::UnmapAndFreeUserRange(proc->pageTableRoot, libBase,
|
||||
(libEnd - libBase) / 0x1000ULL);
|
||||
|
||||
g_libTable[slot][i].inUse = false;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
#include <Net/Icmp.hpp>
|
||||
#include <Net/Dns.hpp>
|
||||
#include <Net/Ipv4.hpp>
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <Net/NetIf.hpp>
|
||||
#include <Drivers/Net/E1000.hpp>
|
||||
#include <Drivers/Net/E1000E.hpp>
|
||||
#include <Drivers/Net/Genet.hpp>
|
||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||
|
||||
#include "Syscall.hpp"
|
||||
@@ -157,6 +158,10 @@ namespace montauk::abi {
|
||||
out->rxPackets = Drivers::Net::E1000E::GetRxPacketCount();
|
||||
out->txPackets = Drivers::Net::E1000E::GetTxPacketCount();
|
||||
CopyNetStatusDriver(out->driver, "Intel e1000e");
|
||||
} else if (Drivers::Net::Genet::IsInitialized()) {
|
||||
out->rxPackets = Drivers::Net::Genet::GetRxPacketCount();
|
||||
out->txPackets = Drivers::Net::Genet::GetTxPacketCount();
|
||||
CopyNetStatusDriver(out->driver, "Broadcom GENET");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-15
@@ -8,13 +8,13 @@
|
||||
#include <cstdint>
|
||||
#include <Efi/UEFI.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Io/IoPort.hpp>
|
||||
#include <Graphics/Framebuffer.hpp>
|
||||
#include <ACPI/AcpiShutdown.hpp>
|
||||
#include <ACPI/AcpiSleep.hpp>
|
||||
#include <ACPI/CpuIdle.hpp>
|
||||
#include <ACPI/Battery.hpp>
|
||||
#include <Hal/CpuPower.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/CpuPower.hpp>
|
||||
#include <Arch/Platform.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
@@ -56,15 +56,13 @@ namespace montauk::abi {
|
||||
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
|
||||
<< (uint64_t)Sched::GetCurrentPid() << " entered final reboot stage";
|
||||
if (Efi::g_ResetSystem) {
|
||||
/* Switch to kernel PML4 which has identity-mapped UEFI runtime regions */
|
||||
Memory::VMM::LoadCR3(Memory::VMM::g_paging->PML4);
|
||||
/* Switch to the kernel address space, which has identity-mapped UEFI runtime regions */
|
||||
Memory::VMM::ActivateKernelAddressSpace();
|
||||
Efi::g_ResetSystem(Efi::EfiResetCold, 0, 0, nullptr);
|
||||
}
|
||||
|
||||
/* Fallback: triple fault via null IDT */
|
||||
struct [[gnu::packed]] { uint16_t limit; uint64_t base; } nullIdt = {0, 0};
|
||||
asm volatile("lidt %0; int $0x03" :: "m"(nullIdt));
|
||||
__builtin_unreachable();
|
||||
/* Fallback: architecture-specific reset */
|
||||
Arch::Reboot();
|
||||
}
|
||||
|
||||
static void Sys_Shutdown() {
|
||||
@@ -75,15 +73,17 @@ namespace montauk::abi {
|
||||
Hal::AcpiShutdown::Shutdown();
|
||||
}
|
||||
|
||||
/* Hardware-reduced platforms (PSCI on ARM) */
|
||||
Arch::PowerOff();
|
||||
|
||||
/* Fallback: EFI ResetSystem */
|
||||
if (Efi::g_ResetSystem) {
|
||||
Memory::VMM::LoadCR3(Memory::VMM::g_paging->PML4);
|
||||
Memory::VMM::ActivateKernelAddressSpace();
|
||||
Efi::g_ResetSystem(Efi::EfiResetShutdown, 0, 0, nullptr);
|
||||
}
|
||||
|
||||
/* Last resort: halt the CPU */
|
||||
asm volatile("cli; hlt");
|
||||
__builtin_unreachable();
|
||||
Arch::HaltForever();
|
||||
}
|
||||
|
||||
static int64_t Sys_Suspend() {
|
||||
@@ -96,8 +96,8 @@ namespace montauk::abi {
|
||||
// SYS_POWERINFO. Fills a CPU power/thermal snapshot. Returns -1 when the
|
||||
// CPU exposes no power management features (e.g. under QEMU).
|
||||
static int64_t Sys_PowerInfo(PowerInfo* out) {
|
||||
Hal::CpuPower::Snapshot s{};
|
||||
if (!Hal::CpuPower::GetSnapshot(s)) {
|
||||
Arch::CpuPower::Snapshot s{};
|
||||
if (!Arch::CpuPower::GetSnapshot(s)) {
|
||||
return NoDevice;
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace montauk::abi {
|
||||
out->baseMHz = s.BaseMHz;
|
||||
out->maxMHz = s.MaxMHz;
|
||||
out->effMHz = s.EffMHz;
|
||||
out->apIdleHint = Hal::CpuIdle::GetApIdleHint();
|
||||
out->apIdleHint = s.ApIdleHint;
|
||||
return Success;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
@@ -220,7 +220,26 @@ namespace montauk::abi {
|
||||
return count;
|
||||
}
|
||||
|
||||
// Flag every thread of the caller's process with TerminatedAborted, then
|
||||
// exit this one. A sibling's ExitProcess ends only itself; the main
|
||||
// thread and the other siblings follow via killPending.
|
||||
static void Sys_Abort() {
|
||||
Sched::KillProcess(Sched::GetCurrentPid(), TerminatedAborted);
|
||||
Sched::ExitProcess();
|
||||
}
|
||||
|
||||
// Like Sys_Abort, but a normal exit with the caller's code: every thread
|
||||
// goes, and waiters see exitCode & 0xFF, the same as SYS_EXIT on the main
|
||||
// thread.
|
||||
static void Sys_ExitGroup(int exitCode) {
|
||||
Sched::KillProcess(Sched::GetCurrentPid(), exitCode & 0xFF);
|
||||
Sched::ExitProcess();
|
||||
}
|
||||
|
||||
static int Sys_Kill(int pid) {
|
||||
// A process may always kill itself (kill(getpid(), ...)); waiters
|
||||
// see TerminatedKilled. abort() uses SYS_ABORT instead.
|
||||
if (pid == Sched::GetCurrentPid()) return Sched::KillProcess(pid);
|
||||
if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) {
|
||||
int ancestor = pid;
|
||||
bool descendant = false;
|
||||
@@ -302,6 +321,14 @@ namespace montauk::abi {
|
||||
return Sched::GetCurrentTid();
|
||||
}
|
||||
|
||||
static int Sys_FutexWait(uint64_t addr, uint32_t expected, uint64_t timeoutNs) {
|
||||
return Sched::FutexWait(addr, expected, timeoutNs);
|
||||
}
|
||||
|
||||
static int Sys_FutexWake(uint64_t addr, uint32_t count) {
|
||||
return Sched::FutexWake(addr, count);
|
||||
}
|
||||
|
||||
static int Sys_Chdir(const char* path) {
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
if (proc == nullptr || path == nullptr) return InvalidArgument;
|
||||
|
||||
@@ -6,23 +6,23 @@
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Arch/Cpu.hpp>
|
||||
|
||||
namespace montauk::abi {
|
||||
|
||||
// ---- Random number generation ----
|
||||
// Uses RDTSC mixed with xorshift64* PRNG for entropy.
|
||||
// RDRAND is intentionally avoided: some firmware disables the RDRAND
|
||||
// hardware unit while CPUID still advertises support (bit 30 of ECX),
|
||||
// causing #UD on real hardware. RDTSC-based entropy is sufficient for
|
||||
// seeding BearSSL's PRNG for TLS session keys.
|
||||
// Uses the CPU timestamp counter mixed with xorshift64* PRNG for entropy.
|
||||
// RDRAND is intentionally avoided on x86: some firmware disables the
|
||||
// RDRAND hardware unit while CPUID still advertises support (bit 30 of
|
||||
// ECX), causing #UD on real hardware. Timestamp-based entropy is
|
||||
// sufficient for seeding BearSSL's PRNG for TLS session keys.
|
||||
|
||||
static int64_t Sys_GetRandom(uint8_t* buf, uint64_t len) {
|
||||
uint64_t tsc;
|
||||
asm volatile("rdtsc; shl $32, %%rdx; or %%rdx, %%rax" : "=a"(tsc) :: "rdx");
|
||||
uint64_t tsc = Arch::ReadTimestamp();
|
||||
uint64_t state = tsc;
|
||||
|
||||
for (uint64_t i = 0; i < len; i += 8) {
|
||||
asm volatile("rdtsc; shl $32, %%rdx; or %%rdx, %%rax" : "=a"(tsc) :: "rdx");
|
||||
tsc = Arch::ReadTimestamp();
|
||||
state ^= tsc;
|
||||
state ^= state >> 12;
|
||||
state ^= state << 25;
|
||||
|
||||
@@ -163,6 +163,45 @@ namespace montauk::abi {
|
||||
return (int64_t)Fs::FsProbe::SyncAndUnmountAll();
|
||||
}
|
||||
|
||||
// Grow a mounted volume to fill the free space after its partition: the
|
||||
// MBR entry first, only as far as the filesystem can use, then the
|
||||
// filesystem a slice per call. Interrupted grows resume from the next
|
||||
// call (or boot), since a partition larger than its filesystem is valid.
|
||||
static int64_t Sys_FsGrow(int drive, FsGrowStatus* out, bool apply) {
|
||||
int partIndex = Fs::FsProbe::GetPartitionForDrive(drive);
|
||||
auto* part = Drivers::Storage::Gpt::GetPartition(partIndex);
|
||||
if (part == nullptr) return UnsupportedOp; // ramdisk, or no such drive
|
||||
|
||||
uint64_t maxSectors = Drivers::Storage::Gpt::GetMbrPartitionMaxSectors(partIndex);
|
||||
if (maxSectors < part->SectorCount) maxSectors = part->SectorCount;
|
||||
|
||||
Fs::Vfs::GrowStatus gs{};
|
||||
int rc = Fs::Vfs::VfsGrow(drive, maxSectors, false, &gs);
|
||||
if (rc == -2) return UnsupportedOp;
|
||||
if (rc < 0) return IoFailure;
|
||||
|
||||
// Leave whatever the filesystem cannot use unpartitioned, with the end
|
||||
// on a 1 MiB boundary like the partitions mk-rpi4-sd.sh makes.
|
||||
uint64_t wanted = (part->StartLba + (gs.targetBytes + 511) / 512 + 2047) / 2048 * 2048
|
||||
- part->StartLba;
|
||||
if (wanted > maxSectors) wanted = maxSectors;
|
||||
|
||||
if (apply && wanted > part->SectorCount) {
|
||||
if (Drivers::Storage::Gpt::ResizeMbrPartition(partIndex, wanted) != 0) return IoFailure;
|
||||
}
|
||||
|
||||
rc = apply ? Fs::Vfs::VfsGrow(drive, part->SectorCount, true, &gs) : 0;
|
||||
if (rc == -2) return UnsupportedOp;
|
||||
if (rc < 0) return IoFailure;
|
||||
|
||||
out->fsBytes = gs.fsBytes;
|
||||
out->partBytes = part->SectorCount * 512;
|
||||
out->targetBytes = gs.targetBytes;
|
||||
out->stepsDone = gs.stepsDone;
|
||||
out->stepsTotal = gs.stepsTotal;
|
||||
return rc;
|
||||
}
|
||||
|
||||
// Format a partition with a filesystem. Returns 0 on success, GenericFailure on error.
|
||||
static int64_t Sys_FsFormat(const FsFormatParams* params) {
|
||||
if (params == nullptr) return InvalidArgument;
|
||||
|
||||
+83
-32
@@ -6,8 +6,6 @@
|
||||
|
||||
#include "Syscall.hpp"
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <Hal/MSR.hpp>
|
||||
#include <Hal/GDT.hpp>
|
||||
|
||||
/* For common functions used by multiple syscall implementations*/
|
||||
#include "Common.hpp"
|
||||
@@ -18,7 +16,7 @@
|
||||
#include "Terminal.hpp" // SYS_PRINT, SYS_PUTCHAR
|
||||
#include "Filesystem.hpp" // SYS_OPEN, SYS_READ, SYS_GETSIZE, SYS_CLOSE, SYS_READDIR, SYS_FWRITE, SYS_FCREATE
|
||||
#include "Heap.hpp" // SYS_ALLOC, SYS_FREE
|
||||
#include "Time.hpp" // SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME
|
||||
#include "Time.hpp" // SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME, SYS_GETUNIXTIME
|
||||
#include "Keyboard.hpp" // SYS_ISKEYAVAILABLE, SYS_GETKEY, SYS_GETCHAR
|
||||
#include "Info.hpp" // SYS_GETINFO
|
||||
#include "Graphics.hpp" // SYS_FBINFO, SYS_FBMAP, SYS_TERMSIZE, SYS_TERMSCALE
|
||||
@@ -31,7 +29,7 @@
|
||||
#include "Device.hpp" // SYS_DEVLIST, SYS_DISKINFO
|
||||
#include "Input.hpp" // SYS_INPUT_WAIT
|
||||
#include "Storage.hpp" // SYS_PARTLIST, SYS_VOLINFO, SYS_DISKREAD, SYS_DISKWRITE
|
||||
#include "Window.hpp" // SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE
|
||||
#include "Window.hpp" // SYS_WIN_REQUEST*, SYS_WIN_DRAG_*, SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE
|
||||
#include "Audio.hpp" // SYS_AUDIOOPEN, SYS_AUDIOCLOSE, SYS_AUDIOWRITE, SYS_AUDIOCTL
|
||||
#include "BluetoothSyscall.hpp" // SYS_BTSCAN, SYS_BTCONNECT, SYS_BTDISCONNECT, SYS_BTLIST, SYS_BTINFO
|
||||
#include "Usb.hpp" // generic process-owned USB interface access
|
||||
@@ -39,12 +37,9 @@
|
||||
#include "IpcSyscall.hpp" // SYS_DUPHANDLE, SYS_WAIT_HANDLE, SYS_STREAM_CREATE, SYS_STREAM_READ, SYS_STREAM_WRITE, SYS_MAILBOX_CREATE, SYS_MAILBOX_SEND, SYS_MAILBOX_RECV, SYS_WAITSET_CREATE, SYS_WAITSET_ADD, SYS_WAITSET_REMOVE, SYS_WAITSET_WAIT, SYS_PROC_OPEN, SYS_SURFACE_CREATE, SYS_SURFACE_MAP, SYS_SURFACE_RESIZE
|
||||
#include "LibSyscall.hpp" // SYS_LOAD_LIB, SYS_UNLOAD_LIB, SYS_DLSYM
|
||||
#include "CrashReportSyscall.hpp" // SYS_CRASH_REPORT
|
||||
#include "Clipboard.hpp" // SYS_CLIPBOARD_*
|
||||
#include "Clipboard.hpp" // SYS_CLIPBOARD_* (text and multi-format)
|
||||
#include "UserMemory.hpp"
|
||||
|
||||
// Assembly entry point
|
||||
extern "C" void SyscallEntry();
|
||||
|
||||
namespace montauk::abi {
|
||||
|
||||
static constexpr uint64_t kMaxPrintableStringBytes = 4096;
|
||||
@@ -139,8 +134,8 @@ namespace montauk::abi {
|
||||
case SYS_ISKEYAVAILABLE:
|
||||
return (int64_t)Sys_IsKeyAvailable();
|
||||
case SYS_GETKEY:
|
||||
if (!UserMemory::Writable<KeyEvent>(frame->arg1)) return BadPointer;
|
||||
Sys_GetKey((KeyEvent*)frame->arg1);
|
||||
if (!UserMemory::Writable<KeyEventV1>(frame->arg1)) return BadPointer;
|
||||
Sys_GetKey((KeyEventV1*)frame->arg1);
|
||||
return Success;
|
||||
case SYS_GETCHAR:
|
||||
return (int64_t)Sys_GetChar();
|
||||
@@ -250,6 +245,8 @@ namespace montauk::abi {
|
||||
if (!UserMemory::Writable<DateTime>(frame->arg1)) return BadPointer;
|
||||
Sys_GetTime((DateTime*)frame->arg1);
|
||||
return Success;
|
||||
case SYS_GETUNIXTIME:
|
||||
return Sys_GetUnixTime();
|
||||
case SYS_SETUNIXTIME:
|
||||
if (!Sched::Consent::Require(CAP_SET_TIME)) return CapabilityDenied;
|
||||
return Sys_SetUnixTime((int64_t)frame->arg1);
|
||||
@@ -365,8 +362,8 @@ namespace montauk::abi {
|
||||
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return BadPointer;
|
||||
return (int64_t)Sys_ChildIoWrite((int)frame->arg1, (const char*)frame->arg2, (int)frame->arg3);
|
||||
case SYS_CHILDIO_WRITEKEY:
|
||||
if (!UserMemory::Readable<KeyEvent>(frame->arg2)) return BadPointer;
|
||||
return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEvent*)frame->arg2);
|
||||
if (!UserMemory::Readable<KeyEventV1>(frame->arg2)) return BadPointer;
|
||||
return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEventV1*)frame->arg2);
|
||||
case SYS_CHILDIO_SETTERMSZ:
|
||||
return (int64_t)Sys_ChildIoSetTermsz((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
|
||||
case SYS_WINCREATE:
|
||||
@@ -400,6 +397,12 @@ namespace montauk::abi {
|
||||
return (int64_t)Sys_ProcList((ProcInfo*)frame->arg1, (int)frame->arg2);
|
||||
case SYS_KILL:
|
||||
return (int64_t)Sys_Kill((int)frame->arg1);
|
||||
case SYS_ABORT:
|
||||
Sys_Abort();
|
||||
return Success;
|
||||
case SYS_EXIT_GROUP:
|
||||
Sys_ExitGroup((int)frame->arg1);
|
||||
return Success;
|
||||
case SYS_SETSESSION:
|
||||
return (int64_t)Sys_SetSession();
|
||||
case SYS_KILLSESSION:
|
||||
@@ -453,6 +456,10 @@ namespace montauk::abi {
|
||||
case SYS_FS_SYNC:
|
||||
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
|
||||
return Sys_FsSync();
|
||||
case SYS_FS_GROW:
|
||||
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
|
||||
if (!UserMemory::Range(frame->arg2, sizeof(FsGrowStatus), true)) return BadPointer;
|
||||
return Sys_FsGrow((int)frame->arg1, (FsGrowStatus*)frame->arg2, frame->arg3 != 0);
|
||||
case SYS_FSFORMAT:
|
||||
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
|
||||
if (!UserMemory::Readable<FsFormatParams>(frame->arg1)) return BadPointer;
|
||||
@@ -527,6 +534,14 @@ namespace montauk::abi {
|
||||
return Sched::Consent::Next((ConsentRequest*)frame->arg1);
|
||||
case SYS_CONSENT_REPLY:
|
||||
return Sched::Consent::Reply(frame->arg1, (int)frame->arg2);
|
||||
case SYS_GETKEY_EX:
|
||||
if (frame->arg2 < sizeof(KeyEventV1) || frame->arg2 > 4096) return InvalidArgument;
|
||||
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
|
||||
return Sys_GetKeyEx((void*)frame->arg1, frame->arg2);
|
||||
case SYS_CHILDIO_WRITEKEY_EX:
|
||||
if (frame->arg3 < sizeof(KeyEventV1) || frame->arg3 > 4096) return InvalidArgument;
|
||||
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return BadPointer;
|
||||
return (int64_t)Sys_ChildIoWriteKeyEx((int)frame->arg1, (const void*)frame->arg2, frame->arg3);
|
||||
case SYS_THREAD_SPAWN:
|
||||
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
|
||||
case SYS_THREAD_EXIT:
|
||||
@@ -537,6 +552,13 @@ namespace montauk::abi {
|
||||
return Sys_ThreadJoin((int)frame->arg1, (int*)frame->arg2);
|
||||
case SYS_THREAD_SELF:
|
||||
return Sys_ThreadSelf();
|
||||
case SYS_FUTEX_WAIT:
|
||||
// Validation demand-commits the page so the scheduler can
|
||||
// read the word with its lock held without faulting.
|
||||
if (!UserMemory::Readable<uint32_t>(frame->arg1)) return BadPointer;
|
||||
return Sys_FutexWait(frame->arg1, (uint32_t)frame->arg2, frame->arg3);
|
||||
case SYS_FUTEX_WAKE:
|
||||
return Sys_FutexWake(frame->arg1, (uint32_t)frame->arg2);
|
||||
case SYS_BTSCAN:
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
|
||||
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
|
||||
@@ -705,6 +727,51 @@ namespace montauk::abi {
|
||||
UserMemory::IsUserPtr(frame->arg4) ? (uint64_t*)frame->arg4 : nullptr);
|
||||
case SYS_CLIPBOARD_CLEAR:
|
||||
return Sys_ClipboardClear();
|
||||
case SYS_CLIPBOARD_SET_DATA:
|
||||
if (frame->arg2 > TRANSFER_MAX_FORMATS) return InvalidArgument;
|
||||
if (!UserMemory::Range(frame->arg1, frame->arg2 * sizeof(TransferItem), false)) return BadPointer;
|
||||
return Sys_ClipboardSetData((const TransferItem*)frame->arg1, (uint32_t)frame->arg2);
|
||||
case SYS_CLIPBOARD_FORMATS: {
|
||||
uint64_t max = frame->arg2 > TRANSFER_MAX_FORMATS ? TRANSFER_MAX_FORMATS : frame->arg2;
|
||||
if (!UserMemory::OptionalRange(frame->arg1, max * sizeof(TransferFormatInfo), true)) return BadPointer;
|
||||
if (!UserMemory::OptionalRange(frame->arg3, sizeof(uint64_t), true)) return BadPointer;
|
||||
return Sys_ClipboardFormats(frame->arg1 ? (TransferFormatInfo*)frame->arg1 : nullptr,
|
||||
(uint32_t)max,
|
||||
frame->arg3 ? (uint64_t*)frame->arg3 : nullptr);
|
||||
}
|
||||
case SYS_CLIPBOARD_GET_DATA:
|
||||
if (!UserMemory::String(frame->arg1, TRANSFER_MIME_MAX)) return BadPointer;
|
||||
if (frame->arg3 > 0 && !UserMemory::Range(frame->arg2, frame->arg3, true)) return BadPointer;
|
||||
if (!UserMemory::Writable<uint32_t>(frame->arg4)) return BadPointer;
|
||||
return Sys_ClipboardGetData((const char*)frame->arg1,
|
||||
frame->arg3 > 0 ? (void*)frame->arg2 : nullptr,
|
||||
(uint32_t)frame->arg3, (uint32_t*)frame->arg4);
|
||||
case SYS_WIN_REQUEST:
|
||||
if (!UserMemory::Readable<WinRequest>(frame->arg2)) return BadPointer;
|
||||
return Sys_WinRequest((int)frame->arg1, (const WinRequest*)frame->arg2);
|
||||
case SYS_WIN_REQUEST_POLL:
|
||||
if (!UserMemory::Writable<WinRequest>(frame->arg1)) return BadPointer;
|
||||
return Sys_WinRequestPoll((WinRequest*)frame->arg1);
|
||||
case SYS_WIN_DRAG_BEGIN:
|
||||
if (!UserMemory::Readable<WinDragBegin>(frame->arg2)) return BadPointer;
|
||||
if (frame->arg4 == 0 || frame->arg4 > TRANSFER_MAX_FORMATS) return InvalidArgument;
|
||||
if (!UserMemory::Range(frame->arg3, frame->arg4 * sizeof(TransferItem), false)) return BadPointer;
|
||||
return Sys_WinDragBegin((int)frame->arg1, (const WinDragBegin*)frame->arg2,
|
||||
(const TransferItem*)frame->arg3, (uint32_t)frame->arg4);
|
||||
case SYS_WIN_DRAG_FORMATS: {
|
||||
uint64_t max = frame->arg3 > TRANSFER_MAX_FORMATS ? TRANSFER_MAX_FORMATS : frame->arg3;
|
||||
if (!UserMemory::OptionalRange(frame->arg2, max * sizeof(TransferFormatInfo), true)) return BadPointer;
|
||||
return Sys_WinDragFormats((uint32_t)frame->arg1,
|
||||
frame->arg2 ? (TransferFormatInfo*)frame->arg2 : nullptr,
|
||||
(uint32_t)max);
|
||||
}
|
||||
case SYS_WIN_DRAG_GET_DATA:
|
||||
if (!UserMemory::String(frame->arg2, TRANSFER_MIME_MAX)) return BadPointer;
|
||||
if (frame->arg4 > 0 && !UserMemory::Range(frame->arg3, frame->arg4, true)) return BadPointer;
|
||||
if (!UserMemory::Writable<uint32_t>(frame->arg5)) return BadPointer;
|
||||
return Sys_WinDragGetData((uint32_t)frame->arg1, (const char*)frame->arg2,
|
||||
frame->arg4 > 0 ? (void*)frame->arg3 : nullptr,
|
||||
(uint32_t)frame->arg4, (uint32_t*)frame->arg5);
|
||||
case SYS_INPUT_WAIT:
|
||||
return (int64_t)Sys_InputWait(frame->arg1, frame->arg2);
|
||||
case SYS_LOG_WRITE: {
|
||||
@@ -730,29 +797,13 @@ namespace montauk::abi {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- SYSCALL MSR initialization ----
|
||||
// ---- Entry point initialization ----
|
||||
|
||||
void InitializeSyscalls() {
|
||||
// Enable SYSCALL/SYSRET and no-execute page permissions in EFER.
|
||||
uint64_t efer = Hal::ReadMSR(Hal::IA32_EFER);
|
||||
efer |= 1 | (1ULL << 11); // SCE | NXE
|
||||
Hal::WriteMSR(Hal::IA32_EFER, efer);
|
||||
Arch::InitializeSyscallEntry();
|
||||
|
||||
// STAR: kernel CS in [47:32], sysret base in [63:48]
|
||||
// SYSCALL: CS=0x08, SS=0x10
|
||||
// SYSRET: CS=0x10+16=0x20|RPL3=0x23, SS=0x10+8=0x18|RPL3=0x1B
|
||||
uint64_t star = (0x0010ULL << 48) | (0x0008ULL << 32);
|
||||
Hal::WriteMSR(Hal::IA32_STAR, star);
|
||||
|
||||
// LSTAR: SYSCALL entry point
|
||||
Hal::WriteMSR(Hal::IA32_LSTAR, (uint64_t)SyscallEntry);
|
||||
|
||||
// FMASK: mask IF on SYSCALL entry (bit 9 = IF)
|
||||
Hal::WriteMSR(Hal::IA32_FMASK, 0x200);
|
||||
|
||||
Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR="
|
||||
<< kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", "
|
||||
<< (SYS_SPAWN_REDIR_CAPS + 1) << " syscall slots)";
|
||||
Kt::KernelLogStream(Kt::OK, "Syscall") << "System call entry installed ("
|
||||
<< kcp::dec << (SYS_SPAWN_REDIR_CAPS + 1) << " syscall slots)";
|
||||
}
|
||||
|
||||
}
|
||||
+245
-13
@@ -8,6 +8,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <Common/StatusCodes.hpp>
|
||||
#include <Arch/Syscall.hpp>
|
||||
|
||||
namespace montauk::abi {
|
||||
|
||||
@@ -358,6 +359,55 @@ namespace montauk::abi {
|
||||
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
||||
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
||||
|
||||
/* Unicode key events. Size-versioned: the caller passes sizeof(its
|
||||
KeyEvent); the kernel copies min(size, its own) bytes and zero-fills the
|
||||
rest, so the struct can grow without another syscall. */
|
||||
static constexpr uint64_t SYS_GETKEY_EX = 194; // (KeyEvent*, size) -> 0; blocks like SYS_GETKEY
|
||||
static constexpr uint64_t SYS_CHILDIO_WRITEKEY_EX = 195; // (childPid, const KeyEvent*, size) -> status
|
||||
|
||||
/* Futexes: process-private wait queues keyed by a user address. WAIT
|
||||
blocks only if *addr still equals `expected` (checked atomically with
|
||||
enrollment). timeoutNs: FUTEX_WAIT_FOREVER = no deadline, 0 = check
|
||||
only; rounded up to the 1 ms scheduler tick. WAIT returns 0 when woken,
|
||||
WouldBlock on value mismatch, TimedOut on deadline. WAKE returns the
|
||||
number of threads woken (count = FUTEX_WAKE_ALL wakes every waiter). */
|
||||
static constexpr uint64_t SYS_FUTEX_WAIT = 196; // (uint32_t* addr, expected, timeoutNs) -> status
|
||||
static constexpr uint64_t SYS_FUTEX_WAKE = 197; // (uint32_t* addr, count) -> woken, or status
|
||||
static constexpr uint64_t FUTEX_WAIT_FOREVER = ~0ULL;
|
||||
static constexpr uint32_t FUTEX_WAKE_ALL = 0xFFFFFFFFu;
|
||||
|
||||
/* Abnormal termination from any thread: ends the whole process (every
|
||||
thread) with exit code TerminatedAborted (256+SIGABRT). SYS_EXIT from a
|
||||
sibling thread ends only that thread, so abort() needs its own call. */
|
||||
static constexpr uint64_t SYS_ABORT = 198; // () -> does not return
|
||||
|
||||
/* Normal termination of the whole process from any thread, with an exit
|
||||
code (0..255) instead of SYS_ABORT's fixed TerminatedAborted. libc
|
||||
exit() and Rust std::process::exit() use it, so exiting from a sibling
|
||||
thread ends the process rather than only that thread. */
|
||||
static constexpr uint64_t SYS_EXIT_GROUP = 199; // (exitCode) -> does not return
|
||||
|
||||
/* Multi-format clipboard (see "Window server messaging" below). The text
|
||||
calls above operate on its TRANSFER_MIME_TEXT format. */
|
||||
static constexpr uint64_t SYS_CLIPBOARD_SET_DATA = 200; // (const TransferItem*, count) -> status
|
||||
static constexpr uint64_t SYS_CLIPBOARD_FORMATS = 201; // (TransferFormatInfo*, max, uint64_t* serial) -> count
|
||||
static constexpr uint64_t SYS_CLIPBOARD_GET_DATA = 202; // (mime, buf, max, uint32_t* len) -> status
|
||||
|
||||
/* Window server requests and drag and drop. */
|
||||
static constexpr uint64_t SYS_WIN_REQUEST = 203; // (windowId, const WinRequest*) -> status
|
||||
static constexpr uint64_t SYS_WIN_REQUEST_POLL = 204; // (WinRequest*) -> 1 / 0; compositor only
|
||||
static constexpr uint64_t SYS_WIN_DRAG_BEGIN = 205; // (windowId or -1 for the compositor's own surface, const WinDragBegin*, const TransferItem*, count) -> serial
|
||||
static constexpr uint64_t SYS_WIN_DRAG_FORMATS = 206; // (serial, TransferFormatInfo*, max) -> count
|
||||
static constexpr uint64_t SYS_WIN_DRAG_GET_DATA = 207; // (serial, mime, buf, max, uint32_t* len) -> status
|
||||
|
||||
/* Storage.hpp -- grow a mounted volume (and its MBR partition) to fill the
|
||||
free space after it. apply=0 only reports; apply=1 does one bounded slice
|
||||
and returns 1 while more remains, 0 once done. CAP_STORAGE_ADMIN. */
|
||||
static constexpr uint64_t SYS_FS_GROW = 208; // (drive, FsGrowStatus*, apply) -> 1 / 0 / status
|
||||
|
||||
/* Time.hpp -- wall clock as UTC unix seconds (SYS_GETTIME is local time). */
|
||||
static constexpr uint64_t SYS_GETUNIXTIME = 209; // () -> seconds
|
||||
|
||||
/* Kernel-owned process capabilities. User identities may namespace
|
||||
per-user resources, but never participate in authorization decisions. */
|
||||
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
||||
@@ -600,7 +650,10 @@ namespace montauk::abi {
|
||||
uint64_t txPackets;
|
||||
};
|
||||
|
||||
struct KeyEvent {
|
||||
// Legacy 9-byte key event, read by SYS_GETKEY and written by
|
||||
// SYS_CHILDIO_WRITEKEY. Kept so binaries built before codepoints still work.
|
||||
// `ascii` is a Windows-1252 byte.
|
||||
struct KeyEventV1 {
|
||||
uint8_t scancode;
|
||||
char ascii;
|
||||
bool pressed;
|
||||
@@ -616,6 +669,32 @@ namespace montauk::abi {
|
||||
bool extended;
|
||||
};
|
||||
|
||||
// Key event with the Unicode text it types (SYS_GETKEY_EX,
|
||||
// SYS_CHILDIO_WRITEKEY_EX, WinEvent). Its first 9 bytes ARE a KeyEventV1, so
|
||||
// a legacy reader sees a valid event. `codepoint` is the character the key
|
||||
// produces (0 for control/navigation keys and dead keys). `ascii` is its
|
||||
// Windows-1252 byte, or KEY_ASCII_SUBSTITUTE when 1252 has none.
|
||||
struct KeyEvent {
|
||||
uint8_t scancode;
|
||||
char ascii;
|
||||
bool pressed;
|
||||
bool shift;
|
||||
bool ctrl;
|
||||
bool alt;
|
||||
bool capslock;
|
||||
bool altgr;
|
||||
bool extended;
|
||||
uint8_t _reserved[3];
|
||||
uint32_t codepoint;
|
||||
};
|
||||
static_assert(sizeof(KeyEventV1) == 9, "KeyEventV1 is frozen ABI");
|
||||
static_assert(sizeof(KeyEvent) == 16, "KeyEvent must fit the WinEvent union");
|
||||
|
||||
// ASCII SUB: `ascii` of a text key whose character has no Windows-1252 byte.
|
||||
// A control character, so legacy code that only accepts ascii >= 0x20 skips
|
||||
// it, while code testing `ascii == 0` for navigation keys is not confused.
|
||||
static constexpr char KEY_ASCII_SUBSTITUTE = 0x1A;
|
||||
|
||||
struct MouseState {
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
@@ -649,16 +728,55 @@ namespace montauk::abi {
|
||||
};
|
||||
|
||||
// Window server shared types
|
||||
enum WinEventType : uint8_t {
|
||||
WIN_EVENT_KEY = 0,
|
||||
WIN_EVENT_MOUSE = 1,
|
||||
WIN_EVENT_RESIZE = 2,
|
||||
WIN_EVENT_CLOSE = 3,
|
||||
WIN_EVENT_SCALE = 4,
|
||||
// Drag and drop (WinEvent::drag). A window may read the drag's data
|
||||
// (SYS_WIN_DRAG_*) once it has received one of these for that serial.
|
||||
WIN_EVENT_DRAG_ENTER = 5, // pointer entered the window during a drag
|
||||
WIN_EVENT_DRAG_OVER = 6, // pointer moved; coalesced like mouse moves
|
||||
WIN_EVENT_DRAG_LEAVE = 7, // pointer left, or the drag was cancelled
|
||||
WIN_EVENT_DROP = 8, // released here; drag.actions = chosen action
|
||||
WIN_EVENT_DRAG_END = 9, // to the source only; drag.actions = action
|
||||
// taken (0 = cancelled / not accepted)
|
||||
};
|
||||
|
||||
// Modifier keys held at the time of a drag event (WinEvent::drag.modifiers).
|
||||
static constexpr uint8_t WIN_MOD_SHIFT = 1u << 0;
|
||||
static constexpr uint8_t WIN_MOD_CTRL = 1u << 1;
|
||||
static constexpr uint8_t WIN_MOD_ALT = 1u << 2;
|
||||
|
||||
// Drag actions; a source offers a set, a drop carries exactly one.
|
||||
static constexpr uint8_t WIN_DRAG_COPY = 1u << 0;
|
||||
static constexpr uint8_t WIN_DRAG_MOVE = 1u << 1;
|
||||
static constexpr uint8_t WIN_DRAG_LINK = 1u << 2;
|
||||
|
||||
struct WinDragEvent {
|
||||
int32_t x, y; // window-local; -1,-1 when not over the window
|
||||
uint32_t serial; // identifies the drag
|
||||
uint8_t actions; // WIN_DRAG_* (see WinEventType)
|
||||
uint8_t modifiers; // WIN_MOD_*
|
||||
uint8_t _pad[2];
|
||||
};
|
||||
static_assert(sizeof(WinDragEvent) == 16, "WinDragEvent must fit the WinEvent union");
|
||||
|
||||
struct WinEvent {
|
||||
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
||||
uint8_t type; // WinEventType
|
||||
uint8_t _pad[3];
|
||||
union {
|
||||
KeyEvent key;
|
||||
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
|
||||
KeyEvent key; // 16 bytes: the union was already that size (mouse)
|
||||
// modifiers (WIN_MOD_*) was padding before; compositors that
|
||||
// predate it send 0.
|
||||
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons, modifiers; } mouse;
|
||||
struct { int32_t w, h; } resize;
|
||||
struct { int32_t scale; } scale;
|
||||
WinDragEvent drag; // WIN_EVENT_DRAG_* / WIN_EVENT_DROP
|
||||
};
|
||||
};
|
||||
static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries");
|
||||
|
||||
struct WinInfo {
|
||||
int32_t id;
|
||||
@@ -679,6 +797,116 @@ namespace montauk::abi {
|
||||
uint64_t pixelVa; // VA of pixel buffer in caller's address space
|
||||
};
|
||||
|
||||
// ---- Window server messaging ----------------------------------------
|
||||
//
|
||||
// Three pieces, shared by the compositor (the desktop) and every app:
|
||||
// * WinEvent types 5-9 below: drag-and-drop events, compositor -> app.
|
||||
// * WinRequest: typed records an app posts about one of its own windows
|
||||
// (SYS_WIN_REQUEST); only the compositor can collect them
|
||||
// (SYS_WIN_REQUEST_POLL). Unknown types are ignored, so new requests
|
||||
// can be added without breaking older compositors or apps.
|
||||
// * Transfers: multi-format payloads (one MIME type per format) carried
|
||||
// by the clipboard (SYS_CLIPBOARD_*_DATA) and by drags
|
||||
// (SYS_WIN_DRAG_*). The older text clipboard calls read and write the
|
||||
// TRANSFER_MIME_TEXT format of the same clipboard.
|
||||
|
||||
// Transfers -----------------------------------------------------------
|
||||
|
||||
static constexpr uint32_t TRANSFER_MAX_FORMATS = 8;
|
||||
static constexpr uint32_t TRANSFER_MIME_MAX = 64; // incl. NUL
|
||||
static constexpr uint32_t TRANSFER_MAX_BYTES = 256 * 1024; // all formats together
|
||||
|
||||
// Well-known formats. Any other "type/subtype" string is allowed.
|
||||
static constexpr const char* TRANSFER_MIME_TEXT = "text/plain;charset=utf-8";
|
||||
// UTF-8 absolute paths ("0:/..."), one per line.
|
||||
static constexpr const char* TRANSFER_MIME_FILE_LIST = "application/x-montauk-file-list";
|
||||
// Clipboard companion to a file list: "copy" or "cut".
|
||||
static constexpr const char* TRANSFER_MIME_FILE_OP = "application/x-montauk-file-op";
|
||||
|
||||
// One format to store. Pointers are carried as 64-bit values so the
|
||||
// layout is the same for every caller.
|
||||
struct TransferItem {
|
||||
uint64_t mime; // const char*, NUL-terminated, < TRANSFER_MIME_MAX
|
||||
uint64_t data; // const void*
|
||||
uint32_t len;
|
||||
uint32_t _reserved;
|
||||
};
|
||||
static_assert(sizeof(TransferItem) == 24, "TransferItem is ABI");
|
||||
|
||||
struct TransferFormatInfo {
|
||||
char mime[TRANSFER_MIME_MAX];
|
||||
uint32_t len;
|
||||
uint32_t _reserved;
|
||||
};
|
||||
static_assert(sizeof(TransferFormatInfo) == 72, "TransferFormatInfo is ABI");
|
||||
|
||||
// Requests --------------------------------------------------------------
|
||||
|
||||
static constexpr uint32_t WIN_REQUEST_PAYLOAD_MAX = 240;
|
||||
|
||||
struct WinRequest {
|
||||
uint16_t type; // WinRequestType
|
||||
uint16_t size; // payload bytes in use
|
||||
int32_t windowId; // window the request is about (the sender must own it
|
||||
// when posting); -1 if it was destroyed since
|
||||
int32_t senderPid; // filled in by the kernel
|
||||
uint32_t serial; // filled in by the kernel; increases per request
|
||||
uint8_t payload[WIN_REQUEST_PAYLOAD_MAX];
|
||||
};
|
||||
static_assert(sizeof(WinRequest) == 256, "WinRequest is ABI");
|
||||
|
||||
enum WinRequestType : uint16_t {
|
||||
WIN_REQ_LAUNCH_ITEM = 1, // WinReqLaunchItem
|
||||
WIN_REQ_SET_PARENT = 2, // WinReqSetParent
|
||||
WIN_REQ_DRAG_STATUS = 3, // WinReqDragStatus
|
||||
WIN_REQ_ACTIVATE = 4, // no payload: raise and focus the window
|
||||
// Types from here on are generated by the kernel; SYS_WIN_REQUEST
|
||||
// refuses them so an app cannot forge one.
|
||||
WIN_REQ_KERNEL_BASE = 0x8000,
|
||||
WIN_REQ_DRAG_BEGIN = 0x8001, // WinReqDragBegin (from SYS_WIN_DRAG_BEGIN)
|
||||
};
|
||||
|
||||
// Open a desktop catalog item (an app manifest id, or a built-in such as
|
||||
// "desktop-settings"), as if the user had picked it from the launcher, so
|
||||
// it runs with the authority the desktop would give it. `args` reach only
|
||||
// apps whose manifest declares files.types (they take a document path);
|
||||
// anything else starts without them, so a request cannot steer a
|
||||
// privileged app through its command line.
|
||||
struct WinReqLaunchItem {
|
||||
char itemId[48];
|
||||
char args[192];
|
||||
};
|
||||
|
||||
// Mark the window as a dialog of another window owned by the same
|
||||
// process: the compositor centres it over the parent.
|
||||
struct WinReqSetParent {
|
||||
int32_t parentWindowId;
|
||||
};
|
||||
|
||||
// Answer to DRAG_ENTER/DRAG_OVER: the action this window would perform
|
||||
// on a drop at the current position (0 = would refuse). Advisory; it
|
||||
// drives the compositor's drag feedback.
|
||||
struct WinReqDragStatus {
|
||||
uint32_t serial;
|
||||
uint8_t action; // one WIN_DRAG_* or 0
|
||||
uint8_t _pad[3];
|
||||
};
|
||||
|
||||
// Describes a drag for the compositor's feedback. Supplied by the app to
|
||||
// SYS_WIN_DRAG_BEGIN and relayed (with the serial) as WIN_REQ_DRAG_BEGIN.
|
||||
struct WinDragBegin {
|
||||
uint8_t actions; // WIN_DRAG_* offered
|
||||
uint8_t _pad[3];
|
||||
uint32_t itemCount; // for a "3 items" badge; 0 = do not show
|
||||
char label[64]; // short description, e.g. the file name
|
||||
};
|
||||
static_assert(sizeof(WinDragBegin) == 72, "WinDragBegin is ABI");
|
||||
|
||||
struct WinReqDragBegin {
|
||||
uint32_t serial;
|
||||
WinDragBegin info;
|
||||
};
|
||||
|
||||
struct MemStats {
|
||||
uint64_t totalBytes;
|
||||
uint64_t freeBytes;
|
||||
@@ -695,7 +923,7 @@ namespace montauk::abi {
|
||||
|
||||
struct DiskInfo {
|
||||
uint8_t port; // block device index
|
||||
uint8_t type; // 0=none, 1=SATA, 2=SATAPI, 3=NVMe, 4=USB mass storage
|
||||
uint8_t type; // 0=none, 1=SATA, 2=SATAPI, 3=NVMe, 4=USB mass storage, 5=SD card
|
||||
uint8_t sataGen; // SATA gen (1/2/3)
|
||||
uint8_t _pad0;
|
||||
uint64_t sectorCount; // Total user-addressable sectors
|
||||
@@ -777,6 +1005,14 @@ namespace montauk::abi {
|
||||
char label[32]; // volume label
|
||||
};
|
||||
|
||||
struct FsGrowStatus {
|
||||
uint64_t fsBytes; // filesystem size now
|
||||
uint64_t partBytes; // partition size now
|
||||
uint64_t targetBytes; // filesystem size once grown; == fsBytes if it cannot grow
|
||||
uint32_t stepsDone; // progress of the grow, out of stepsTotal
|
||||
uint32_t stepsTotal;
|
||||
};
|
||||
|
||||
struct ProcInfo {
|
||||
int32_t pid;
|
||||
int32_t parentPid;
|
||||
@@ -1010,15 +1246,11 @@ namespace montauk::abi {
|
||||
uint64_t timestampTick;
|
||||
};
|
||||
|
||||
// Stack frame pushed by SyscallEntry.asm
|
||||
struct SyscallFrame {
|
||||
uint64_t r15, r14, r13, r12, rbp, rbx; // callee-saved
|
||||
uint64_t arg6, arg5, arg4, arg3, arg2, arg1;
|
||||
uint64_t syscall_nr;
|
||||
uint64_t user_rflags, user_rip, user_rsp;
|
||||
};
|
||||
// Saved user context of a system call; its layout is the
|
||||
// architecture's (see Arch/Syscall.hpp).
|
||||
using SyscallFrame = Arch::SyscallFrame;
|
||||
|
||||
// Kernel-only: set up SYSCALL MSRs and initialize dispatch
|
||||
// Kernel-only: install the system-call entry point and initialize dispatch
|
||||
void InitializeSyscalls();
|
||||
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* Time.hpp
|
||||
* SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME syscalls
|
||||
* SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME, SYS_GETUNIXTIME syscalls
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
#include <Timekeeping/Time.hpp>
|
||||
|
||||
#include "Syscall.hpp"
|
||||
@@ -39,6 +39,10 @@ namespace montauk::abi {
|
||||
return (int64_t)Timekeeping::GetTZOffset();
|
||||
}
|
||||
|
||||
static int64_t Sys_GetUnixTime() {
|
||||
return Timekeeping::GetUnixTimestamp();
|
||||
}
|
||||
|
||||
static int64_t Sys_SetUnixTime(int64_t unixSeconds) {
|
||||
return Timekeeping::SetUnixTimestamp(unixSeconds) ? Success : InvalidArgument;
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ namespace montauk::abi::UserMemory {
|
||||
return addr != 0 && addr < USER_SPACE_END;
|
||||
}
|
||||
|
||||
inline uint64_t CurrentPml4() {
|
||||
inline uint64_t CurrentRoot() {
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
return proc ? proc->pml4Phys : 0;
|
||||
return proc ? proc->pageTableRoot : 0;
|
||||
}
|
||||
|
||||
inline bool Range(uint64_t addr, uint64_t size, bool requireWrite = false) {
|
||||
@@ -30,10 +30,10 @@ namespace montauk::abi::UserMemory {
|
||||
if (addr + size < addr) return false;
|
||||
if (addr + size > USER_SPACE_END) return false;
|
||||
|
||||
uint64_t pml4Phys = CurrentPml4();
|
||||
if (pml4Phys == 0) return false;
|
||||
uint64_t pageTableRoot = CurrentRoot();
|
||||
if (pageTableRoot == 0) return false;
|
||||
|
||||
if (Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, addr, size, requireWrite))
|
||||
if (Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, addr, size, requireWrite))
|
||||
return true;
|
||||
|
||||
// Anonymous VMAs are demand-committed. Validation precedes the actual
|
||||
@@ -42,13 +42,13 @@ namespace montauk::abi::UserMemory {
|
||||
uint64_t page = addr & ~0xFFFULL;
|
||||
uint64_t last = (addr + size - 1) & ~0xFFFULL;
|
||||
for (;;) {
|
||||
if (!Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, page, 1, requireWrite) &&
|
||||
!TryHandleAnonymousPageFault(page, requireWrite ? 2ULL : 0ULL))
|
||||
if (!Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, page, 1, requireWrite) &&
|
||||
!TryHandleAnonymousPageFault(page, {false, requireWrite, false}))
|
||||
return false;
|
||||
if (page == last) break;
|
||||
page += 0x1000ULL;
|
||||
}
|
||||
return Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, addr, size, requireWrite);
|
||||
return Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, addr, size, requireWrite);
|
||||
}
|
||||
|
||||
inline bool OptionalRange(uint64_t addr, uint64_t size, bool requireWrite = false) {
|
||||
@@ -68,13 +68,13 @@ namespace montauk::abi::UserMemory {
|
||||
inline bool String(uint64_t addr, uint64_t maxLen) {
|
||||
if (!ValidUserPtr(addr) || maxLen == 0) return false;
|
||||
|
||||
uint64_t pml4Phys = CurrentPml4();
|
||||
if (pml4Phys == 0) return false;
|
||||
uint64_t pageTableRoot = CurrentRoot();
|
||||
if (pageTableRoot == 0) return false;
|
||||
|
||||
const char* str = (const char*)addr;
|
||||
for (uint64_t i = 0; i < maxLen; i++) {
|
||||
if (!Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, addr + i, 1, false)) {
|
||||
if (!TryHandleAnonymousPageFault(addr + i, 0)) return false;
|
||||
if (!Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, addr + i, 1, false)) {
|
||||
if (!TryHandleAnonymousPageFault(addr + i, {false, false, false})) return false;
|
||||
}
|
||||
if (str[i] == '\0') return true;
|
||||
}
|
||||
|
||||
+208
-16
@@ -54,13 +54,13 @@ namespace WinServer {
|
||||
Ipc::ReleaseSurface(surface);
|
||||
}
|
||||
|
||||
static int UnmapRetiredSnapshotsLocked(int windowId, int callerPid, uint64_t callerPml4) {
|
||||
static int UnmapRetiredSnapshotsLocked(int windowId, int callerPid, uint64_t callerRoot) {
|
||||
int result = InvalidArgument;
|
||||
for (int i = 0; i < MaxRetiredSnapshots; i++) {
|
||||
if (!g_retiredSnapshots[i].used || g_retiredSnapshots[i].windowId != windowId) continue;
|
||||
|
||||
if (g_retiredSnapshots[i].surface != nullptr) {
|
||||
if (Ipc::UnmapSurfaceForPid(g_retiredSnapshots[i].surface, callerPid, callerPml4) == 0) {
|
||||
if (Ipc::UnmapSurfaceForPid(g_retiredSnapshots[i].surface, callerPid, callerRoot) == 0) {
|
||||
result = Success;
|
||||
}
|
||||
Ipc::ReleaseSurface(g_retiredSnapshots[i].surface);
|
||||
@@ -73,11 +73,18 @@ namespace WinServer {
|
||||
return result;
|
||||
}
|
||||
|
||||
static void OrphanRequestsLocked(int windowId);
|
||||
|
||||
static void ReleaseSlotResourcesLocked(WindowSlot& slot, bool unmapOwner) {
|
||||
// Queued requests stay deliverable (a dialog may post "open this
|
||||
// app" and close at once), but no longer name a window that could be
|
||||
// handed to someone else.
|
||||
OrphanRequestsLocked((int)(&slot - g_slots));
|
||||
|
||||
if (unmapOwner && slot.liveSurface != nullptr) {
|
||||
auto* owner = Sched::GetProcessByPid(slot.ownerPid);
|
||||
if (owner != nullptr) {
|
||||
Ipc::UnmapSurfaceForPid(slot.liveSurface, slot.ownerPid, owner->pml4Phys);
|
||||
Ipc::UnmapSurfaceForPid(slot.liveSurface, slot.ownerPid, owner->pageTableRoot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,19 +101,63 @@ namespace WinServer {
|
||||
ResetSlotLocked(slot);
|
||||
}
|
||||
|
||||
// ---- Messaging state ----
|
||||
|
||||
static constexpr int MaxRequests = 64;
|
||||
// One app may not hold more than this many queued requests, so a single
|
||||
// misbehaving process cannot starve the others out of the queue.
|
||||
static constexpr int MaxRequestsPerSender = 16;
|
||||
static constexpr int MaxDragReaders = 8;
|
||||
|
||||
static montauk::abi::WinRequest g_requests[MaxRequests];
|
||||
static int g_requestHead = 0; // oldest
|
||||
static int g_requestCount = 0;
|
||||
static uint32_t g_requestSerial = 0;
|
||||
|
||||
struct DragState {
|
||||
bool active;
|
||||
uint32_t serial;
|
||||
int sourceWindow;
|
||||
int sourcePid;
|
||||
DataTransfer::Transfer transfer;
|
||||
int readers[MaxDragReaders];
|
||||
int readerCount;
|
||||
};
|
||||
static DragState g_drag;
|
||||
static uint32_t g_dragSerial = 0;
|
||||
|
||||
static bool IsDragEvent(uint8_t type) {
|
||||
return type >= montauk::abi::WIN_EVENT_DRAG_ENTER && type <= montauk::abi::WIN_EVENT_DRAG_END;
|
||||
}
|
||||
|
||||
static void GrantDragReaderLocked(uint32_t serial, int pid) {
|
||||
if (!g_drag.active || g_drag.serial != serial || pid <= 0) return;
|
||||
if (pid == g_drag.sourcePid) return;
|
||||
for (int i = 0; i < g_drag.readerCount; i++) {
|
||||
if (g_drag.readers[i] == pid) return;
|
||||
}
|
||||
if (g_drag.readerCount < MaxDragReaders) g_drag.readers[g_drag.readerCount++] = pid;
|
||||
}
|
||||
|
||||
static int SendEventLocked(int windowId, const montauk::abi::WinEvent* event) {
|
||||
if (windowId < 0 || windowId >= MaxWindows || event == nullptr) return InvalidArgument;
|
||||
WindowSlot& slot = g_slots[windowId];
|
||||
if (!slot.used || slot.eventMailbox == nullptr) return InvalidArgument;
|
||||
|
||||
int rc = (event->type == 1)
|
||||
? Ipc::MailboxSendCoalescedMouse(slot.eventMailbox, 0, event, sizeof(*event))
|
||||
// Delivering a drag event is what lets the receiving app read the
|
||||
// drag's data: it needs the formats to decide whether to accept.
|
||||
if (IsDragEvent(event->type)) GrantDragReaderLocked(event->drag.serial, slot.ownerPid);
|
||||
|
||||
bool motion = event->type == montauk::abi::WIN_EVENT_MOUSE ||
|
||||
event->type == montauk::abi::WIN_EVENT_DRAG_OVER;
|
||||
int rc = motion
|
||||
? Ipc::MailboxSendCoalescedMotion(slot.eventMailbox, 0, event, sizeof(*event))
|
||||
: Ipc::MailboxSend(slot.eventMailbox, 0, event, sizeof(*event));
|
||||
if (rc < 0) return GenericFailure;
|
||||
return rc > 0 ? Success : Busy;
|
||||
}
|
||||
|
||||
int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h,
|
||||
int Create(int ownerPid, uint64_t ownerRoot, const char* title, int w, int h,
|
||||
uint64_t& heapNext, uint64_t& outVa) {
|
||||
WsGuard guard;
|
||||
|
||||
@@ -143,7 +194,7 @@ namespace WinServer {
|
||||
FillSurfaceLocked(snapshot, w, h, InitialWindowPixel);
|
||||
|
||||
uint64_t userVa = 0;
|
||||
if (Ipc::MapSurfaceForPid(live, ownerPid, ownerPml4, heapNext, userVa, false) != 0) {
|
||||
if (Ipc::MapSurfaceForPid(live, ownerPid, ownerRoot, heapNext, userVa, false) != 0) {
|
||||
Ipc::ReleaseMailbox(mailbox, true, true);
|
||||
Ipc::ReleaseSurface(snapshot);
|
||||
Ipc::ReleaseSurface(live);
|
||||
@@ -236,7 +287,7 @@ namespace WinServer {
|
||||
return count;
|
||||
}
|
||||
|
||||
uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext) {
|
||||
uint64_t Map(int windowId, int callerPid, uint64_t callerRoot, uint64_t& heapNext) {
|
||||
WsGuard guard;
|
||||
if (windowId < 0 || windowId >= MaxWindows) return 0;
|
||||
|
||||
@@ -244,23 +295,23 @@ namespace WinServer {
|
||||
if (!slot.used || slot.snapshotSurface == nullptr) return 0;
|
||||
|
||||
uint64_t outVa = 0;
|
||||
if (Ipc::MapSurfaceForPid(slot.snapshotSurface, callerPid, callerPml4, heapNext, outVa, false) != 0) {
|
||||
if (Ipc::MapSurfaceForPid(slot.snapshotSurface, callerPid, callerRoot, heapNext, outVa, false) != 0) {
|
||||
return 0;
|
||||
}
|
||||
return outVa;
|
||||
}
|
||||
|
||||
int Unmap(int windowId, int callerPid, uint64_t callerPml4) {
|
||||
int Unmap(int windowId, int callerPid, uint64_t callerRoot) {
|
||||
WsGuard guard;
|
||||
if (windowId < 0 || windowId >= MaxWindows) {
|
||||
return UnmapRetiredSnapshotsLocked(windowId, callerPid, callerPml4);
|
||||
return UnmapRetiredSnapshotsLocked(windowId, callerPid, callerRoot);
|
||||
}
|
||||
|
||||
int result = UnmapRetiredSnapshotsLocked(windowId, callerPid, callerPml4);
|
||||
int result = UnmapRetiredSnapshotsLocked(windowId, callerPid, callerRoot);
|
||||
WindowSlot& slot = g_slots[windowId];
|
||||
if (!slot.used || slot.snapshotSurface == nullptr) return result;
|
||||
|
||||
int current = Ipc::UnmapSurfaceForPid(slot.snapshotSurface, callerPid, callerPml4);
|
||||
int current = Ipc::UnmapSurfaceForPid(slot.snapshotSurface, callerPid, callerRoot);
|
||||
if (current == 0) return Success;
|
||||
return result;
|
||||
}
|
||||
@@ -270,7 +321,7 @@ namespace WinServer {
|
||||
return SendEventLocked(windowId, event);
|
||||
}
|
||||
|
||||
int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH,
|
||||
int Resize(int windowId, int callerPid, uint64_t ownerRoot, int newW, int newH,
|
||||
uint64_t& heapNext, uint64_t& outVa) {
|
||||
WsGuard guard;
|
||||
if (windowId < 0 || windowId >= MaxWindows) return InvalidArgument;
|
||||
@@ -297,7 +348,7 @@ namespace WinServer {
|
||||
Ipc::RetainSurface(newSnapshot);
|
||||
|
||||
uint64_t newOwnerVa = 0;
|
||||
if (Ipc::MapSurfaceForPid(newLive, callerPid, ownerPml4, heapNext, newOwnerVa, false) != 0) {
|
||||
if (Ipc::MapSurfaceForPid(newLive, callerPid, ownerRoot, heapNext, newOwnerVa, false) != 0) {
|
||||
Ipc::ReleaseSurface(newSnapshot);
|
||||
Ipc::ReleaseSurface(newLive);
|
||||
return GenericFailure;
|
||||
@@ -315,7 +366,7 @@ namespace WinServer {
|
||||
}
|
||||
|
||||
if (slot.liveSurface != nullptr) {
|
||||
Ipc::UnmapSurfaceForPid(slot.liveSurface, callerPid, ownerPml4);
|
||||
Ipc::UnmapSurfaceForPid(slot.liveSurface, callerPid, ownerRoot);
|
||||
Ipc::ReleaseSurface(slot.liveSurface);
|
||||
}
|
||||
if (slot.snapshotSurface != nullptr) {
|
||||
@@ -376,12 +427,153 @@ namespace WinServer {
|
||||
return g_uiScale;
|
||||
}
|
||||
|
||||
static void OrphanRequestsLocked(int windowId) {
|
||||
for (int i = 0; i < g_requestCount; i++) {
|
||||
montauk::abi::WinRequest& r = g_requests[(g_requestHead + i) % MaxRequests];
|
||||
if (r.windowId == windowId) r.windowId = -1;
|
||||
}
|
||||
}
|
||||
|
||||
static void EndDragLocked() {
|
||||
DataTransfer::Free(g_drag.transfer);
|
||||
g_drag.active = false;
|
||||
g_drag.readerCount = 0;
|
||||
}
|
||||
|
||||
void CleanupProcess(int pid) {
|
||||
WsGuard guard;
|
||||
for (int i = 0; i < MaxWindows; i++) {
|
||||
if (!g_slots[i].used || g_slots[i].ownerPid != pid) continue;
|
||||
ReleaseSlotResourcesLocked(g_slots[i], false);
|
||||
}
|
||||
// Requests the process already posted are still delivered: they are
|
||||
// messages in flight, and a process may well exit right after its
|
||||
// last one ("open this file" from a dialog-only Files).
|
||||
if (g_drag.active && g_drag.sourcePid == pid) EndDragLocked();
|
||||
}
|
||||
|
||||
static int EnqueueRequestLocked(montauk::abi::WinRequest& request) {
|
||||
if (g_requestCount >= MaxRequests) return WouldBlock;
|
||||
request.serial = ++g_requestSerial;
|
||||
if (request.serial == 0) request.serial = ++g_requestSerial;
|
||||
g_requests[(g_requestHead + g_requestCount) % MaxRequests] = request;
|
||||
g_requestCount++;
|
||||
return Success;
|
||||
}
|
||||
|
||||
static bool OwnsWindowLocked(int windowId, int pid) {
|
||||
return windowId >= 0 && windowId < MaxWindows && g_slots[windowId].used &&
|
||||
g_slots[windowId].ownerPid == pid;
|
||||
}
|
||||
|
||||
int PostRequest(int windowId, int callerPid, const montauk::abi::WinRequest& request) {
|
||||
using namespace montauk::abi;
|
||||
if (request.type == 0 || request.type >= WIN_REQ_KERNEL_BASE) return InvalidArgument;
|
||||
if (request.size > WIN_REQUEST_PAYLOAD_MAX) return InvalidArgument;
|
||||
|
||||
WsGuard guard;
|
||||
if (!OwnsWindowLocked(windowId, callerPid)) return PermissionDenied;
|
||||
|
||||
int mine = 0;
|
||||
for (int i = 0; i < g_requestCount; i++) {
|
||||
if (g_requests[(g_requestHead + i) % MaxRequests].senderPid == callerPid) mine++;
|
||||
}
|
||||
if (mine >= MaxRequestsPerSender) return WouldBlock;
|
||||
|
||||
WinRequest copy = request;
|
||||
copy.windowId = windowId;
|
||||
copy.senderPid = callerPid;
|
||||
// Bytes past `size` are not the sender's business to pass along.
|
||||
for (uint32_t i = copy.size; i < WIN_REQUEST_PAYLOAD_MAX; i++) copy.payload[i] = 0;
|
||||
return EnqueueRequestLocked(copy);
|
||||
}
|
||||
|
||||
int PollRequest(montauk::abi::WinRequest* out) {
|
||||
WsGuard guard;
|
||||
while (g_requestCount > 0) {
|
||||
montauk::abi::WinRequest& r = g_requests[g_requestHead];
|
||||
g_requestHead = (g_requestHead + 1) % MaxRequests;
|
||||
g_requestCount--;
|
||||
// A destroyed window's requests arrive with windowId -1 (see
|
||||
// OrphanRequestsLocked); anything else must still match.
|
||||
if (r.windowId >= 0 && !OwnsWindowLocked(r.windowId, r.senderPid)) continue;
|
||||
*out = r;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int DragBegin(int windowId, int callerPid, bool compositor,
|
||||
const montauk::abi::WinDragBegin& info, DataTransfer::Transfer& transfer) {
|
||||
using namespace montauk::abi;
|
||||
WsGuard guard;
|
||||
// The compositor drags from its own surface (desktop icons) with
|
||||
// windowId -1; it already knows about such a drag, so no request.
|
||||
bool fromSurface = compositor && windowId == -1;
|
||||
if (!fromSurface && !OwnsWindowLocked(windowId, callerPid)) {
|
||||
DataTransfer::Free(transfer);
|
||||
return PermissionDenied;
|
||||
}
|
||||
|
||||
uint32_t serial = ++g_dragSerial;
|
||||
if (serial == 0) serial = ++g_dragSerial;
|
||||
|
||||
if (!fromSurface) {
|
||||
WinRequest request = {};
|
||||
request.type = WIN_REQ_DRAG_BEGIN;
|
||||
request.size = sizeof(WinReqDragBegin);
|
||||
request.windowId = windowId;
|
||||
request.senderPid = callerPid;
|
||||
WinReqDragBegin* payload = (WinReqDragBegin*)request.payload;
|
||||
payload->serial = serial;
|
||||
payload->info = info;
|
||||
payload->info.label[sizeof(payload->info.label) - 1] = '\0';
|
||||
int rc = EnqueueRequestLocked(request);
|
||||
if (rc != Success) {
|
||||
DataTransfer::Free(transfer);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
// A new drag replaces the previous one (only one pointer exists).
|
||||
if (g_drag.active) EndDragLocked();
|
||||
g_drag.active = true;
|
||||
g_drag.serial = serial;
|
||||
g_drag.sourceWindow = windowId;
|
||||
g_drag.sourcePid = callerPid;
|
||||
g_drag.transfer = transfer;
|
||||
g_drag.readerCount = 0;
|
||||
transfer.count = 0;
|
||||
return (int)serial;
|
||||
}
|
||||
|
||||
static bool DragReadableLocked(uint32_t serial, int callerPid, bool compositor) {
|
||||
if (!g_drag.active || g_drag.serial != serial) return false;
|
||||
if (compositor || callerPid == g_drag.sourcePid) return true;
|
||||
for (int i = 0; i < g_drag.readerCount; i++) {
|
||||
if (g_drag.readers[i] == callerPid) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int DragFormats(uint32_t serial, int callerPid, bool compositor,
|
||||
montauk::abi::TransferFormatInfo* out, uint32_t max) {
|
||||
WsGuard guard;
|
||||
if (!g_drag.active || g_drag.serial != serial) return NotFound;
|
||||
if (!DragReadableLocked(serial, callerPid, compositor)) return PermissionDenied;
|
||||
if (out == nullptr || max == 0) return (int)g_drag.transfer.count;
|
||||
return (int)DataTransfer::ListFormats(g_drag.transfer, out, max);
|
||||
}
|
||||
|
||||
int DragGetData(uint32_t serial, int callerPid, bool compositor, const char* mime,
|
||||
void* buffer, uint32_t bufferLen, uint32_t* outLen) {
|
||||
WsGuard guard;
|
||||
if (outLen != nullptr) *outLen = 0;
|
||||
if (!g_drag.active || g_drag.serial != serial) return NotFound;
|
||||
if (!DragReadableLocked(serial, callerPid, compositor)) return PermissionDenied;
|
||||
const DataTransfer::Format* format = DataTransfer::Find(g_drag.transfer, mime);
|
||||
if (format == nullptr) return NotFound;
|
||||
return DataTransfer::CopyOut(*format, buffer, bufferLen, outLen);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "Syscall.hpp"
|
||||
#include <cstdint>
|
||||
#include <Ipc/Ipc.hpp>
|
||||
#include "DataTransfer.hpp"
|
||||
|
||||
namespace WinServer {
|
||||
|
||||
@@ -27,16 +28,16 @@ namespace WinServer {
|
||||
uint32_t flags;
|
||||
};
|
||||
|
||||
int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h,
|
||||
int Create(int ownerPid, uint64_t ownerRoot, const char* title, int w, int h,
|
||||
uint64_t& heapNext, uint64_t& outVa);
|
||||
int Destroy(int windowId, int callerPid);
|
||||
int Present(int windowId, int callerPid);
|
||||
int Poll(int windowId, int callerPid, montauk::abi::WinEvent* outEvent);
|
||||
int Enumerate(montauk::abi::WinInfo* outArray, int maxCount);
|
||||
uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext);
|
||||
int Unmap(int windowId, int callerPid, uint64_t callerPml4);
|
||||
uint64_t Map(int windowId, int callerPid, uint64_t callerRoot, uint64_t& heapNext);
|
||||
int Unmap(int windowId, int callerPid, uint64_t callerRoot);
|
||||
int SendEvent(int windowId, const montauk::abi::WinEvent* event);
|
||||
int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH,
|
||||
int Resize(int windowId, int callerPid, uint64_t ownerRoot, int newW, int newH,
|
||||
uint64_t& heapNext, uint64_t& outVa);
|
||||
void CleanupProcess(int pid);
|
||||
int SetCursor(int windowId, int callerPid, int cursor);
|
||||
@@ -44,4 +45,26 @@ namespace WinServer {
|
||||
int SetScale(int scale);
|
||||
int GetScale();
|
||||
|
||||
// ---- Messaging (see "Window server messaging" in Api/Syscall.hpp) ----
|
||||
|
||||
// Queue an app request about one of the caller's own windows.
|
||||
int PostRequest(int windowId, int callerPid, const montauk::abi::WinRequest& request);
|
||||
// Pop the oldest request (compositor only; the caller checks the cap).
|
||||
// Returns 1 with *out filled, or 0 when the queue is empty.
|
||||
int PollRequest(montauk::abi::WinRequest* out);
|
||||
|
||||
// Start a drag from the caller's window, or (compositor only, windowId
|
||||
// -1) from the desktop surface itself. Takes ownership of `transfer` (it
|
||||
// is emptied either way) and returns the new drag serial (> 0) or a
|
||||
// negative StatusCodes value.
|
||||
int DragBegin(int windowId, int callerPid, bool compositor,
|
||||
const montauk::abi::WinDragBegin& info, DataTransfer::Transfer& transfer);
|
||||
// Read the drag's formats or data. Allowed for the source process, the
|
||||
// compositor, and processes the compositor has sent a drag event for this
|
||||
// serial to.
|
||||
int DragFormats(uint32_t serial, int callerPid, bool compositor,
|
||||
montauk::abi::TransferFormatInfo* out, uint32_t max);
|
||||
int DragGetData(uint32_t serial, int callerPid, bool compositor, const char* mime,
|
||||
void* buffer, uint32_t bufferLen, uint32_t* outLen);
|
||||
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
* Window.hpp
|
||||
* SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL,
|
||||
* SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE,
|
||||
* SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE syscalls
|
||||
* SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE syscalls,
|
||||
* and window server messaging: SYS_WIN_REQUEST, SYS_WIN_REQUEST_POLL,
|
||||
* SYS_WIN_DRAG_BEGIN, SYS_WIN_DRAG_FORMATS, SYS_WIN_DRAG_GET_DATA
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
@@ -20,7 +22,7 @@ namespace montauk::abi {
|
||||
if (proc == nullptr) return GenericFailure;
|
||||
|
||||
uint64_t outVa = 0;
|
||||
int id = WinServer::Create(proc->pid, proc->pml4Phys, title, w, h,
|
||||
int id = WinServer::Create(proc->pid, proc->pageTableRoot, title, w, h,
|
||||
proc->heapNext, outVa);
|
||||
result->id = id;
|
||||
result->pixelVa = (id >= 0) ? outVa : 0;
|
||||
@@ -53,13 +55,13 @@ namespace montauk::abi {
|
||||
if (!Sched::HasCapability(CAP_COMPOSITOR)) return 0;
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
if (proc == nullptr) return 0;
|
||||
return WinServer::Map(windowId, proc->pid, proc->pml4Phys, proc->heapNext);
|
||||
return WinServer::Map(windowId, proc->pid, proc->pageTableRoot, proc->heapNext);
|
||||
}
|
||||
|
||||
static int Sys_WinUnmap(int windowId) {
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
if (proc == nullptr) return GenericFailure;
|
||||
return WinServer::Unmap(windowId, proc->pid, proc->pml4Phys);
|
||||
return WinServer::Unmap(windowId, proc->pid, proc->pageTableRoot);
|
||||
}
|
||||
|
||||
static int Sys_WinSendEvent(int windowId, const WinEvent* event) {
|
||||
@@ -72,7 +74,7 @@ namespace montauk::abi {
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
if (proc == nullptr) return 0;
|
||||
uint64_t outVa = 0;
|
||||
int r = WinServer::Resize(windowId, proc->pid, proc->pml4Phys, newW, newH,
|
||||
int r = WinServer::Resize(windowId, proc->pid, proc->pageTableRoot, newW, newH,
|
||||
proc->heapNext, outVa);
|
||||
return (r == 0) ? outVa : 0;
|
||||
}
|
||||
@@ -92,4 +94,41 @@ namespace montauk::abi {
|
||||
static int Sys_WinGetScale() {
|
||||
return WinServer::GetScale();
|
||||
}
|
||||
|
||||
// Any app may post requests, but only about its own windows (checked in
|
||||
// WinServer); only the compositor may read them back.
|
||||
static int Sys_WinRequest(int windowId, const WinRequest* request) {
|
||||
if (request == nullptr) return InvalidArgument;
|
||||
WinRequest copy = *request;
|
||||
return WinServer::PostRequest(windowId, Sched::GetCurrentPid(), copy);
|
||||
}
|
||||
|
||||
static int Sys_WinRequestPoll(WinRequest* out) {
|
||||
if (!Sched::HasCapability(CAP_COMPOSITOR)) return CapabilityDenied;
|
||||
if (out == nullptr) return InvalidArgument;
|
||||
return WinServer::PollRequest(out);
|
||||
}
|
||||
|
||||
static int Sys_WinDragBegin(int windowId, const WinDragBegin* info,
|
||||
const TransferItem* items, uint32_t count) {
|
||||
if (info == nullptr) return InvalidArgument;
|
||||
WinDragBegin infoCopy = *info;
|
||||
DataTransfer::Transfer transfer;
|
||||
int rc = DataTransfer::BuildFromUser(items, count, transfer);
|
||||
if (rc != Success) return rc;
|
||||
return WinServer::DragBegin(windowId, Sched::GetCurrentPid(),
|
||||
Sched::HasCapability(CAP_COMPOSITOR), infoCopy, transfer);
|
||||
}
|
||||
|
||||
static int Sys_WinDragFormats(uint32_t serial, TransferFormatInfo* out, uint32_t max) {
|
||||
return WinServer::DragFormats(serial, Sched::GetCurrentPid(),
|
||||
Sched::HasCapability(CAP_COMPOSITOR), out, max);
|
||||
}
|
||||
|
||||
static int Sys_WinDragGetData(uint32_t serial, const char* mime, void* buffer,
|
||||
uint32_t bufferLen, uint32_t* outLen) {
|
||||
return WinServer::DragGetData(serial, Sched::GetCurrentPid(),
|
||||
Sched::HasCapability(CAP_COMPOSITOR), mime,
|
||||
buffer, bufferLen, outLen);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Abi.hpp
|
||||
* Architecture-specific parts of the user-mode ABI
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Arch/Arch.hpp>
|
||||
|
||||
// What the ELF loader and the process/thread setup code need to know about
|
||||
// the user-mode binary interface of the target. Defined inline by
|
||||
// Arch/<arch>/Abi.hpp.
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// ELF e_machine value accepted for user programs and libraries, and a
|
||||
// name for diagnostics.
|
||||
// constexpr uint16_t ElfMachine;
|
||||
// constexpr const char* ElfMachineName;
|
||||
|
||||
// Calling-convention attribute for UEFI runtime services:
|
||||
// #define ARCH_EFIAPI
|
||||
|
||||
// How the loader must treat a dynamic relocation type.
|
||||
enum class RelocationKind {
|
||||
None, // no-op (type 0)
|
||||
Relative, // *target = loadBias + addend
|
||||
Symbol, // *target = symbol address + addend (64-bit absolute)
|
||||
Unsupported,
|
||||
};
|
||||
inline RelocationKind ClassifyRelocation(uint32_t type);
|
||||
|
||||
// Geometry of one thread's TLS block, as offsets from its page-aligned
|
||||
// base. The variant (I: TLS above the thread pointer, II: below it) is
|
||||
// fixed by the architecture's ELF TLS ABI.
|
||||
struct TlsLayout {
|
||||
uint64_t templateOffset; // where .tdata/.tbss start
|
||||
uint64_t tpOffset; // thread pointer; [TP] holds the self-pointer
|
||||
uint64_t totalSize; // bytes to map, incl. the TCB
|
||||
};
|
||||
inline TlsLayout ComputeTlsLayout(uint64_t memSize, uint64_t pAlign);
|
||||
|
||||
// Emit the user-mode code that calls SYS_EXIT(0). A process's entry
|
||||
// function returns into it.
|
||||
inline void WriteExitStub(uint8_t* page);
|
||||
|
||||
// Prepare the main thread's initial user stack so that returning from
|
||||
// the entry function lands at `returnAddress`. `topPage` is the kernel
|
||||
// mapping of the page just below `stackTop`. Returns the initial user SP.
|
||||
inline uint64_t SetupUserEntryStack(uint8_t* topPage, uint64_t stackTop,
|
||||
uint64_t returnAddress);
|
||||
|
||||
// Initial user SP for a sibling thread whose stack ends at `stackTop`
|
||||
// (the thread trampoline never returns, so no return address is set).
|
||||
inline uint64_t ThreadEntryStackPointer(uint64_t stackTop);
|
||||
|
||||
}
|
||||
|
||||
#if MONTAUK_ARCH_X86_64
|
||||
#include <Arch/x86_64/Abi.hpp>
|
||||
#elif MONTAUK_ARCH_AARCH64
|
||||
#include <Arch/aarch64/Abi.hpp>
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Arch.hpp
|
||||
* Target architecture selection
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// Everything outside src/Arch/ is architecture-neutral and talks to the CPU
|
||||
// only through the interfaces in src/Arch/*.hpp. Each supported architecture
|
||||
// implements them under src/Arch/<arch>/; the build compiles only the
|
||||
// directory matching ARCH. See docs/porting.md.
|
||||
//
|
||||
// Interface headers that need inline (per-arch) definitions include the
|
||||
// matching implementation header at their end, selected below.
|
||||
|
||||
#if defined(__x86_64__)
|
||||
#define MONTAUK_ARCH_X86_64 1
|
||||
#define MONTAUK_ARCH_NAME "x86_64"
|
||||
#elif defined(__aarch64__)
|
||||
#define MONTAUK_ARCH_AARCH64 1
|
||||
#define MONTAUK_ARCH_NAME "aarch64"
|
||||
#else
|
||||
#error "MontaukOS: unsupported target architecture (add one under src/Arch/)"
|
||||
#endif
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Context.hpp
|
||||
* Architecture-neutral thread context interface
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Arch/Arch.hpp>
|
||||
#include <Arch/Smp.hpp>
|
||||
|
||||
// The scheduler's view of a thread's CPU context: a kernel stack pointer
|
||||
// saved by SwitchContext, an FPU/SIMD save area, and a page-table root.
|
||||
|
||||
#if MONTAUK_ARCH_X86_64
|
||||
#include <Arch/x86_64/Context.hpp>
|
||||
#elif MONTAUK_ARCH_AARCH64
|
||||
#include <Arch/aarch64/Context.hpp>
|
||||
#endif
|
||||
|
||||
// The architecture provides (in Arch/<arch>/Context.hpp):
|
||||
// constexpr uint64_t Arch::FpuStateSize, Arch::FpuStateAlign
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// Reset an FPU/SIMD save area to the state a new thread starts with.
|
||||
void InitFpuState(uint8_t* area);
|
||||
|
||||
// Build the initial kernel stack of a new thread so that the first
|
||||
// SwitchContext to it "returns" into `entry`. Returns the saved SP.
|
||||
uint64_t PrepareThreadStack(uint64_t kernelStackTop, void (*entry)());
|
||||
|
||||
// Save the running context (stack pointer to *oldSp, FPU state to
|
||||
// oldFpu when non-null), switch to page-table root `newRoot` if it is
|
||||
// not already active, and resume the context saved at newSp (restoring
|
||||
// newFpu when non-null). Returns when the old context is resumed.
|
||||
void SwitchContext(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
|
||||
uint8_t* oldFpu, uint8_t* newFpu);
|
||||
|
||||
// Point this CPU at the thread about to run: the kernel stack used when
|
||||
// it enters the kernel from user mode, and its TLS thread pointer.
|
||||
void ActivateThread(Smp::CpuData* cpu, uint64_t kernelStackTop,
|
||||
uint64_t threadPointer);
|
||||
|
||||
// Drop to user mode at `entry` with stack `userSp`; `arg` is the first
|
||||
// argument register. Interrupts are enabled in user mode.
|
||||
[[noreturn]] void EnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Cpu.hpp
|
||||
* Architecture-neutral CPU primitives
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Arch/Arch.hpp>
|
||||
|
||||
// Every function here is defined inline by the architecture
|
||||
// (Arch/<arch>/Cpu.hpp), so the primitives cost what the raw instruction
|
||||
// costs. Generic kernel code must use these instead of inline assembly.
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// Saved interrupt-enable state, as returned by SaveAndDisableInterrupts.
|
||||
// Opaque to generic code: only hand it back to RestoreInterrupts.
|
||||
using IrqFlags = uint64_t;
|
||||
|
||||
// ---- Spinning and barriers ----
|
||||
|
||||
// Spin-loop hint (x86 PAUSE, ARM YIELD).
|
||||
inline void Pause();
|
||||
|
||||
// Stop the compiler from caching memory across this point. Emits no
|
||||
// instruction; use it in polling loops on device-written memory.
|
||||
inline void CompilerBarrier() {
|
||||
asm volatile("" ::: "memory");
|
||||
}
|
||||
|
||||
// Order all earlier stores before all later stores, including stores to
|
||||
// write-combining memory (framebuffers) that must become visible.
|
||||
inline void StoreFence();
|
||||
|
||||
// Full read/write memory barrier.
|
||||
inline void MemoryFence();
|
||||
|
||||
// Order this CPU's earlier writes to DMA memory (descriptors, buffers)
|
||||
// before a later write the device acts on: the descriptor's valid/cycle
|
||||
// bit, or a doorbell register. x86 keeps stores in order already.
|
||||
inline void DmaWriteFence();
|
||||
|
||||
// Order the read of a device-written valid/cycle/status field before the
|
||||
// reads of the data it guards.
|
||||
inline void DmaReadFence();
|
||||
|
||||
// ---- Interrupt state ----
|
||||
|
||||
inline void EnableInterrupts();
|
||||
inline void DisableInterrupts();
|
||||
inline bool InterruptsEnabled();
|
||||
|
||||
// Disable interrupts and return the previous state.
|
||||
inline IrqFlags SaveAndDisableInterrupts();
|
||||
|
||||
// Restore an interrupt state returned by SaveAndDisableInterrupts.
|
||||
inline void RestoreInterrupts(IrqFlags flags);
|
||||
|
||||
// ---- Halting ----
|
||||
|
||||
// Wait for the next interrupt. Interrupt state is left as it was; with
|
||||
// interrupts disabled this only returns on an NMI-class event.
|
||||
inline void Halt();
|
||||
|
||||
// Called with interrupts disabled: atomically enable them and wait for
|
||||
// an interrupt, then disable them again before returning. The atomicity
|
||||
// closes the race where a wakeup IRQ lands between the readiness check
|
||||
// and the wait, leaving the CPU asleep until the next timer deadline.
|
||||
inline void HaltWithInterruptsDisabled();
|
||||
|
||||
// Disable interrupts and stop this CPU for good.
|
||||
[[noreturn]] inline void HaltForever();
|
||||
|
||||
// ---- Time and delays ----
|
||||
|
||||
// A free-running, monotonically increasing cycle counter (x86 TSC, ARM
|
||||
// CNTVCT). Its rate is architecture-defined; use it for entropy and
|
||||
// relative timing, not wall-clock time.
|
||||
inline uint64_t ReadTimestamp();
|
||||
|
||||
// Delay for at least about one microsecond. Works with interrupts
|
||||
// disabled and before any clock is calibrated; errs long.
|
||||
inline void IoDelay();
|
||||
|
||||
// ---- Caches and translation ----
|
||||
|
||||
// Write back and invalidate all CPU caches (before power state changes).
|
||||
inline void WritebackInvalidateCaches();
|
||||
|
||||
// Cache maintenance for devices whose DMA does not snoop the CPU caches
|
||||
// (ACPI _CCA = 0). Clean: write dirty lines back so the device reads
|
||||
// what the CPU wrote. Invalidate: write back and discard, so the CPU
|
||||
// then reads what the device wrote. No-ops where DMA is coherent (x86).
|
||||
inline void CleanDataCache(const void* address, uint64_t size);
|
||||
inline void InvalidateDataCache(const void* address, uint64_t size);
|
||||
|
||||
// Make instructions just written to [address, address+size) (any
|
||||
// mapping of them) visible to instruction fetch on every CPU. A no-op
|
||||
// where instruction fetch snoops data writes (x86).
|
||||
inline void SyncInstructionCache(const void* address, uint64_t size);
|
||||
|
||||
// Invalidate this CPU's translation for one virtual page.
|
||||
inline void FlushTlbPage(uint64_t virtualAddress);
|
||||
|
||||
// Invalidate this CPU's non-global translations.
|
||||
inline void FlushTlbAll();
|
||||
|
||||
// Physical address of the active page-table root (x86 CR3, ARM TTBR0).
|
||||
inline uint64_t ReadPageTableRoot();
|
||||
|
||||
// Switch to the page-table root at the given physical address.
|
||||
inline void WritePageTableRoot(uint64_t rootPhys);
|
||||
|
||||
}
|
||||
|
||||
#if MONTAUK_ARCH_X86_64
|
||||
#include <Arch/x86_64/Cpu.hpp>
|
||||
#elif MONTAUK_ARCH_AARCH64
|
||||
#include <Arch/aarch64/Cpu.hpp>
|
||||
#endif
|
||||
@@ -1,14 +1,18 @@
|
||||
/*
|
||||
* CpuPower.hpp
|
||||
* Intel CPU power management: HWP frequency scaling, C1E promotion,
|
||||
* and the package thermal governor.
|
||||
* Architecture-neutral CPU power/thermal management interface
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Hal {
|
||||
// Frequency scaling, idle-state policy and the thermal governor are CPU
|
||||
// specific (x86_64: Intel HWP, C1E promotion and a package-temperature
|
||||
// governor driven through MSRs). An architecture without them implements
|
||||
// these as no-ops and returns false from GetSnapshot.
|
||||
|
||||
namespace Arch {
|
||||
namespace CpuPower {
|
||||
|
||||
// Snapshot of the power/thermal state for SYS_POWERINFO and logging.
|
||||
@@ -24,6 +28,7 @@ namespace Hal {
|
||||
uint32_t BaseMHz; // CPUID base frequency (0 = unknown)
|
||||
uint32_t MaxMHz; // CPUID max turbo frequency
|
||||
uint32_t EffMHz; // measured average active frequency (BSP)
|
||||
uint8_t ApIdleHint; // idle state secondary CPUs enter (arch-defined)
|
||||
};
|
||||
|
||||
// BSP: detect features, enable HWP with a balanced energy preference,
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Interrupts.hpp
|
||||
* Architecture-neutral hardware interrupt interface
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// Devices and kernel services see interrupts as numbered IRQ slots. The
|
||||
// architecture maps each slot onto its interrupt controller (x86: an IDT
|
||||
// vector fed by the I/O APIC, MSIs and local APIC IPIs; ARM: a GIC INTID)
|
||||
// and calls the registered handler.
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// IRQ handler function type. The parameter is the IRQ slot number.
|
||||
// `fromUser` is derived directly from the interrupted context. Do not
|
||||
// cache it in per-CPU data: an IRQ handler may context-switch and later
|
||||
// resume on another CPU, making per-CPU saved-origin state stale.
|
||||
using IrqHandler = void(*)(uint8_t irq, bool fromUser);
|
||||
|
||||
// Number of IRQ slots: 0-23 platform interrupt lines, 24-45 message-
|
||||
// signalled interrupts (one slot per MSI-capable driver), 46-47 IPIs.
|
||||
constexpr int IRQ_COUNT = 48;
|
||||
|
||||
// The architecture's periodic scheduler/timekeeping timer.
|
||||
constexpr uint8_t IRQ_TIMER = 0;
|
||||
|
||||
// Platform interrupt lines with PC/ISA meaning. Only drivers for PC
|
||||
// devices (PS/2, legacy PCI INTx through the I/O APIC) use these.
|
||||
constexpr uint8_t IRQ_KEYBOARD = 1;
|
||||
constexpr uint8_t IRQ_CASCADE = 2;
|
||||
constexpr uint8_t IRQ_COM2 = 3;
|
||||
constexpr uint8_t IRQ_COM1 = 4;
|
||||
constexpr uint8_t IRQ_FLOPPY = 6;
|
||||
constexpr uint8_t IRQ_RTC = 8;
|
||||
constexpr uint8_t IRQ_MOUSE = 12;
|
||||
constexpr uint8_t IRQ_ATA1 = 14;
|
||||
constexpr uint8_t IRQ_ATA2 = 15;
|
||||
|
||||
// Inter-processor interrupts.
|
||||
constexpr uint8_t IRQ_TLB_SHOOTDOWN = 46;
|
||||
constexpr uint8_t IRQ_RESCHEDULE = 47;
|
||||
|
||||
// Register the handler for an IRQ slot.
|
||||
void RegisterIrqHandler(uint8_t irq, IrqHandler handler);
|
||||
|
||||
// Route platform interrupt line `irq` to IRQ slot `irq` on the boot CPU,
|
||||
// applying the firmware's polarity/trigger overrides, and unmask it.
|
||||
void RouteLegacyIrq(uint8_t irq);
|
||||
|
||||
// Unmask platform interrupt line `irq` (already routed at boot).
|
||||
void EnableLegacyIrq(uint8_t irq);
|
||||
|
||||
// Whether the platform interrupt lines are wired by the hardware (x86:
|
||||
// ISA IRQs and I/O APIC pins, as the firmware programs them into PCI
|
||||
// Interrupt Line registers). Where they are not (ARM), the PCI layer
|
||||
// assigns lines to PCI INTx pins from ACPI _PRT with AssignPlatformLine.
|
||||
bool PlatformLinesAreFixed();
|
||||
|
||||
// Make platform line `irq` (1-23) deliver global system interrupt `gsi`
|
||||
// (ACPI numbering) with the given trigger mode and polarity, so that
|
||||
// EnableLegacyIrq(irq) unmasks it. Only where !PlatformLinesAreFixed().
|
||||
bool AssignPlatformLine(uint8_t irq, uint32_t gsi, bool levelTriggered, bool activeLow);
|
||||
|
||||
// A message-signalled interrupt targeting IRQ slot `irq` on the boot
|
||||
// CPU: the device writes `data` to `address` to raise it. Program these
|
||||
// into a PCI MSI capability or MSI-X table entry. Returns false when the
|
||||
// platform cannot receive MSIs (drivers then fall back to INTx/polling).
|
||||
struct MsiMessage {
|
||||
uint64_t address;
|
||||
uint32_t data;
|
||||
};
|
||||
bool ComposeMsi(uint8_t irq, MsiMessage& out);
|
||||
|
||||
// Raise IPI slot `irq` (IRQ_RESCHEDULE / IRQ_TLB_SHOOTDOWN) on the CPU
|
||||
// with the given Smp index. Returns false if delivery did not complete.
|
||||
bool SendIpi(int cpuIndex, uint8_t irq);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Platform.hpp
|
||||
* Architecture hooks for kernel bring-up and power-off paths
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <ACPI/ACPI.hpp>
|
||||
|
||||
// kmain (Boot/Main.cpp) owns the boot sequence and calls into the
|
||||
// architecture at these points, in this order.
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// First thing after the boot console exists: CPU descriptor tables,
|
||||
// FPU/SIMD enablement. No memory allocation is available yet.
|
||||
void EarlyInitialize();
|
||||
|
||||
// The frame allocator and heap exist: install exception/trap vectors.
|
||||
void InitializeTraps();
|
||||
|
||||
// The kernel page tables are live: program memory-type attributes that
|
||||
// MapWC/MapMMIO rely on (x86 PAT).
|
||||
void InitializeMemoryAttributes();
|
||||
|
||||
// ACPI tables are available: bring up interrupt controllers, the boot
|
||||
// CPU's per-CPU data (Smp::InitBsp), idle-state and power management.
|
||||
// Interrupts stay disabled; kmain enables them afterwards.
|
||||
void InitializeInterrupts(Hal::ACPI::CommonSDTHeader* xsdt);
|
||||
|
||||
// Platform devices that are not discovered on PCI (x86: the PS/2
|
||||
// controller, keyboard and mouse). Called once the timer is running.
|
||||
void InitializePlatformDevices();
|
||||
|
||||
// Right before the first user process is created: anything user-mode
|
||||
// entry needs (x86: load the task register for ring-3 interrupt stacks).
|
||||
void PrepareUserMode();
|
||||
|
||||
// Secondary CPUs are up and the first process is about to be spawned.
|
||||
void LateInitialize();
|
||||
|
||||
// Report devices that belong to the platform rather than a bus (for
|
||||
// SYS_DEVLIST): interrupt controllers (category 1), timers (2) and
|
||||
// platform input devices (3).
|
||||
using PlatformDeviceSink = void (*)(void* context, uint8_t category,
|
||||
const char* name, const char* detail);
|
||||
void ListPlatformDevices(PlatformDeviceSink sink, void* context);
|
||||
|
||||
// Reset the machine without firmware help (last resort after EFI
|
||||
// ResetSystem). Never returns.
|
||||
[[noreturn]] void Reboot();
|
||||
|
||||
// Switch the machine off where ACPI S5 does not apply (hardware-reduced
|
||||
// ACPI: ARM's PSCI SYSTEM_OFF). Returns if the platform cannot.
|
||||
void PowerOff();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Smp.hpp
|
||||
* Architecture-neutral per-CPU data and SMP bring-up interface
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Arch/Arch.hpp>
|
||||
#include <Boot/BootInfo.hpp>
|
||||
|
||||
#if MONTAUK_ARCH_X86_64
|
||||
#include <Arch/x86_64/PerCpu.hpp>
|
||||
#elif MONTAUK_ARCH_AARCH64
|
||||
#include <Arch/aarch64/PerCpu.hpp>
|
||||
#endif
|
||||
|
||||
// The architecture provides (in Arch/<arch>/PerCpu.hpp):
|
||||
// struct Arch::CpuEntryState -- fields kernel-entry assembly reads at a
|
||||
// fixed offset (8) from the per-CPU base
|
||||
// struct Arch::CpuState -- everything else the port keeps per CPU
|
||||
// void* Arch::CurrentCpuLocal() -- this CPU's CpuData (fast path)
|
||||
// void* Arch::TryCurrentCpuLocal() -- same, or nullptr before InitBsp
|
||||
|
||||
namespace Smp {
|
||||
|
||||
static constexpr int MaxCPUs = 64;
|
||||
|
||||
struct CpuData {
|
||||
// === Fixed layout: read by low-level entry code ===
|
||||
uint64_t selfPtr; // offset 0: pointer to self
|
||||
Arch::CpuEntryState entry; // offset 8: arch kernel-entry fields
|
||||
|
||||
// === Generic fields ===
|
||||
int32_t currentSlot; // process table slot, -1 = idle
|
||||
int cpuIndex; // 0-based CPU index (0 = boot CPU)
|
||||
uint64_t idleSavedSp; // idle context's stack pointer while a process runs
|
||||
volatile bool started; // set by an AP once its init is complete
|
||||
|
||||
// When set, the scheduler tick must NOT pull this (idle) CPU away to
|
||||
// run a ready process: its idle context is busy with long-running
|
||||
// kernel work whose wall-clock-bounded waits would spuriously expire
|
||||
// if the context only ran when nothing else is ready (observed: the
|
||||
// deferred Bluetooth firmware download racing login-screen startup).
|
||||
// Other CPUs schedule normally while this is held.
|
||||
volatile bool reservedForKernelWork = false;
|
||||
|
||||
// Set by the BSP timer when an IRQ bottom half needs process-safe
|
||||
// execution even though runnable user processes keep this CPU busy.
|
||||
// Schedule() consumes it by yielding once to the idle kernel context.
|
||||
bool kernelWorkPending = false;
|
||||
|
||||
// === Architecture-private state ===
|
||||
Arch::CpuState arch;
|
||||
};
|
||||
|
||||
static_assert(__builtin_offsetof(CpuData, selfPtr) == 0,
|
||||
"CpuData: per-CPU base must point at selfPtr");
|
||||
static_assert(__builtin_offsetof(CpuData, entry) == 8,
|
||||
"CpuData: entry-assembly fields must start at offset 8");
|
||||
|
||||
// The current CPU's data.
|
||||
inline CpuData* GetCurrentCpuData() {
|
||||
return (CpuData*)Arch::CurrentCpuLocal();
|
||||
}
|
||||
|
||||
// Exception paths may run before InitBsp() has installed the per-CPU
|
||||
// base. This variant returns nullptr in that state instead of faulting
|
||||
// recursively on a bogus base.
|
||||
inline CpuData* TryGetCurrentCpuData() {
|
||||
return (CpuData*)Arch::TryCurrentCpuLocal();
|
||||
}
|
||||
|
||||
// Get CPU data by index
|
||||
CpuData* GetCpuData(int index);
|
||||
|
||||
// Number of online CPUs
|
||||
int GetCpuCount();
|
||||
|
||||
// Number of CPUs the firmware describes (available once interrupts are
|
||||
// initialized; may exceed GetCpuCount if some failed to start).
|
||||
// Implemented by the architecture.
|
||||
int GetDetectedCpuCount();
|
||||
|
||||
// Initialize the boot CPU's per-CPU data (call before interrupts are
|
||||
// enabled). Implemented by the architecture.
|
||||
void InitBsp();
|
||||
|
||||
// Boot all secondary CPUs (call after all subsystems are ready).
|
||||
// `smp` is the boot contract's SMP block; if FeatureSmp was not
|
||||
// advertised the caller may still pass it (cpuCount<=1 is a no-op).
|
||||
// Implemented by the architecture.
|
||||
void BootAPs(const montauk::boot::SmpInfo& smp);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Syscall.hpp
|
||||
* Architecture-neutral system-call entry interface
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Arch/Arch.hpp>
|
||||
|
||||
#if MONTAUK_ARCH_X86_64
|
||||
#include <Arch/x86_64/SyscallFrame.hpp>
|
||||
#elif MONTAUK_ARCH_AARCH64
|
||||
#include <Arch/aarch64/SyscallFrame.hpp>
|
||||
#endif
|
||||
|
||||
// The architecture's system-call entry code saves the user context into an
|
||||
// Arch::SyscallFrame on the thread's kernel stack (Arch/<arch>/
|
||||
// SyscallFrame.hpp defines it, exposing at least `syscall_nr` and `arg1` ..
|
||||
// `arg6`), enables interrupts, and calls
|
||||
//
|
||||
// extern "C" int64_t SyscallDispatch(Arch::SyscallFrame* frame);
|
||||
//
|
||||
// whose result it returns to user mode. Before returning it calls
|
||||
//
|
||||
// extern "C" void SchedExitIfKilled();
|
||||
//
|
||||
// so a kill that arrived during the syscall takes effect at a point where
|
||||
// the syscall holds no locks.
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// Install the system-call entry point on the calling CPU. Called on the
|
||||
// boot CPU by montauk::abi::InitializeSyscalls and by every secondary
|
||||
// CPU during its bring-up.
|
||||
void InitializeSyscallEntry();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Timer.hpp
|
||||
* Architecture-neutral per-CPU tick timer interface
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// The hardware behind Timekeeping (Timekeeping/Timer.hpp): a per-CPU timer
|
||||
// that raises Arch::IRQ_TIMER periodically. Timekeeping registers the IRQ
|
||||
// handler and owns all tick accounting; the architecture only calibrates
|
||||
// and programs the hardware (x86: local APIC timer calibrated against the
|
||||
// PIT; ARM: the generic timer).
|
||||
|
||||
namespace Arch::Timer {
|
||||
|
||||
// Calibrate the timer and start the calling (boot) CPU's periodic tick
|
||||
// with the given period. Returns false if no usable timer was found.
|
||||
bool InitializeBsp(uint32_t periodMs);
|
||||
|
||||
// (Re)start the calling CPU's periodic tick, reusing the boot CPU's
|
||||
// calibration: secondary CPU bring-up, leaving idle, and the boot CPU
|
||||
// after S3 resume (timer state does not survive suspend). Only called
|
||||
// after InitializeBsp succeeded.
|
||||
void StartPeriodic(uint32_t periodMs);
|
||||
|
||||
// Mask the calling CPU's tick (idle secondary CPUs rely on IPIs).
|
||||
// Cheap when it is already masked.
|
||||
void Stop();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Abi.hpp
|
||||
* aarch64 user-mode ABI details (implements Arch/Abi.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// UEFI on AArch64 uses the standard AAPCS64 calling convention.
|
||||
#define ARCH_EFIAPI
|
||||
|
||||
namespace Arch {
|
||||
|
||||
constexpr uint16_t ElfMachine = 183; // EM_AARCH64
|
||||
constexpr const char* ElfMachineName = "aarch64";
|
||||
|
||||
inline RelocationKind ClassifyRelocation(uint32_t type) {
|
||||
switch (type) {
|
||||
case 0: return RelocationKind::None;
|
||||
case 1027: return RelocationKind::Relative; // R_AARCH64_RELATIVE
|
||||
case 257: // R_AARCH64_ABS64
|
||||
case 1025: // R_AARCH64_GLOB_DAT
|
||||
case 1026: return RelocationKind::Symbol; // R_AARCH64_JUMP_SLOT
|
||||
default: return RelocationKind::Unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
// Variant I: TP points at a 16-byte TCB and the TLS block follows it,
|
||||
// starting at the next p_align boundary.
|
||||
inline TlsLayout ComputeTlsLayout(uint64_t memSize, uint64_t pAlign) {
|
||||
uint64_t align = pAlign ? pAlign : 1;
|
||||
uint64_t templateOffset = (16 + align - 1) & ~(align - 1);
|
||||
return {templateOffset, 0, templateOffset + memSize};
|
||||
}
|
||||
|
||||
// System calls: number in x8, arguments in x0-x5, `svc #0`.
|
||||
inline void WriteExitStub(uint8_t* page) {
|
||||
uint32_t* code = (uint32_t*)page;
|
||||
code[0] = 0xD2800000; // mov x0, #0 (exit code 0)
|
||||
code[1] = 0xD2800008; // mov x8, #0 (SYS_EXIT = 0)
|
||||
code[2] = 0xD4000001; // svc #0
|
||||
}
|
||||
|
||||
// The return address travels in x30, not on the stack: EnterUserMode
|
||||
// points x30 at the exit stub. The stack only needs 16-byte alignment.
|
||||
inline uint64_t SetupUserEntryStack(uint8_t*, uint64_t stackTop, uint64_t) {
|
||||
return stackTop & ~0xFULL;
|
||||
}
|
||||
|
||||
inline uint64_t ThreadEntryStackPointer(uint64_t stackTop) {
|
||||
return stackTop & ~0xFULL;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* AcpiSleep.cpp
|
||||
* aarch64: no ACPI S3 (implements ACPI/AcpiSleep.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <ACPI/AcpiSleep.hpp>
|
||||
#include <Common/StatusCodes.hpp>
|
||||
|
||||
// Suspend on ARM goes through PSCI (SYSTEM_SUSPEND), not the FACS waking
|
||||
// vector; until that exists, S3 is reported as unavailable.
|
||||
|
||||
namespace Hal::AcpiSleep {
|
||||
|
||||
void Initialize(ACPI::CommonSDTHeader*) {
|
||||
}
|
||||
|
||||
bool IsS3Available() {
|
||||
return false;
|
||||
}
|
||||
|
||||
int Suspend() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Bringup.hpp
|
||||
* aarch64 per-CPU bring-up shared by the boot CPU and secondaries
|
||||
* (private to the port)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// System registers every CPU programs for itself: SP_EL1 as the stack,
|
||||
// the exception vectors, SCTLR_EL1 policy bits, FP/SIMD access.
|
||||
// Platform.cpp.
|
||||
void InitializeCpuRegisters();
|
||||
|
||||
// Load the kernel's translation regime (MAIR, TCR, TTBR1 kernel half,
|
||||
// TTBR0 kernel-context table) on the calling CPU. Paging::Init builds
|
||||
// the tables and calls it on the boot CPU. Paging.cpp.
|
||||
void ActivateKernelTranslation();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
Context.S
|
||||
aarch64 context switch and first entry to EL0 (see Arch/Context.hpp)
|
||||
Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
/* The kernel is built -mgeneral-regs-only; this file alone touches
|
||||
the FP/SIMD registers, to save and restore user state. */
|
||||
.arch_extension fp
|
||||
.arch_extension simd
|
||||
|
||||
.text
|
||||
|
||||
/*
|
||||
* void ArchContextSwitch(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
|
||||
* uint8_t* oldFpu, uint8_t* newFpu)
|
||||
* x0 = where to save the old SP x1 = SP to resume
|
||||
* x2 = TTBR0 root for the new side x3/x4 = FP/SIMD save areas (may be 0)
|
||||
*
|
||||
* Frame on the kernel stack (Context.cpp's PrepareThreadStack builds the
|
||||
* same shape): x19..x28, x29, x30 -- 96 bytes. The kernel is built with
|
||||
* -mgeneral-regs-only, so q0-q31/FPCR/FPSR only ever hold user state.
|
||||
*/
|
||||
.global ArchContextSwitch
|
||||
.type ArchContextSwitch, %function
|
||||
ArchContextSwitch:
|
||||
cbz x3, 1f
|
||||
stp q0, q1, [x3, #0]
|
||||
stp q2, q3, [x3, #32]
|
||||
stp q4, q5, [x3, #64]
|
||||
stp q6, q7, [x3, #96]
|
||||
stp q8, q9, [x3, #128]
|
||||
stp q10, q11, [x3, #160]
|
||||
stp q12, q13, [x3, #192]
|
||||
stp q14, q15, [x3, #224]
|
||||
stp q16, q17, [x3, #256]
|
||||
stp q18, q19, [x3, #288]
|
||||
stp q20, q21, [x3, #320]
|
||||
stp q22, q23, [x3, #352]
|
||||
stp q24, q25, [x3, #384]
|
||||
stp q26, q27, [x3, #416]
|
||||
stp q28, q29, [x3, #448]
|
||||
stp q30, q31, [x3, #480]
|
||||
mrs x9, fpcr
|
||||
mrs x10, fpsr
|
||||
add x11, x3, #512
|
||||
stp x9, x10, [x11]
|
||||
1:
|
||||
sub sp, sp, #96
|
||||
stp x19, x20, [sp, #0]
|
||||
stp x21, x22, [sp, #16]
|
||||
stp x23, x24, [sp, #32]
|
||||
stp x25, x26, [sp, #48]
|
||||
stp x27, x28, [sp, #64]
|
||||
stp x29, x30, [sp, #80]
|
||||
mov x9, sp
|
||||
str x9, [x0]
|
||||
|
||||
mov sp, x1
|
||||
|
||||
/* Switch the process half only when it differs (no ASIDs yet, so a
|
||||
switch invalidates this CPU's TLB). */
|
||||
mrs x9, ttbr0_el1
|
||||
and x9, x9, #0x0000fffffffffffe
|
||||
cmp x9, x2
|
||||
b.eq 2f
|
||||
dsb ishst
|
||||
msr ttbr0_el1, x2
|
||||
isb
|
||||
tlbi vmalle1
|
||||
dsb nsh
|
||||
isb
|
||||
2:
|
||||
ldp x19, x20, [sp, #0]
|
||||
ldp x21, x22, [sp, #16]
|
||||
ldp x23, x24, [sp, #32]
|
||||
ldp x25, x26, [sp, #48]
|
||||
ldp x27, x28, [sp, #64]
|
||||
ldp x29, x30, [sp, #80]
|
||||
add sp, sp, #96
|
||||
|
||||
cbz x4, 3f
|
||||
ldp q0, q1, [x4, #0]
|
||||
ldp q2, q3, [x4, #32]
|
||||
ldp q4, q5, [x4, #64]
|
||||
ldp q6, q7, [x4, #96]
|
||||
ldp q8, q9, [x4, #128]
|
||||
ldp q10, q11, [x4, #160]
|
||||
ldp q12, q13, [x4, #192]
|
||||
ldp q14, q15, [x4, #224]
|
||||
ldp q16, q17, [x4, #256]
|
||||
ldp q18, q19, [x4, #288]
|
||||
ldp q20, q21, [x4, #320]
|
||||
ldp q22, q23, [x4, #352]
|
||||
ldp q24, q25, [x4, #384]
|
||||
ldp q26, q27, [x4, #416]
|
||||
ldp q28, q29, [x4, #448]
|
||||
ldp q30, q31, [x4, #480]
|
||||
add x11, x4, #512
|
||||
ldp x9, x10, [x11]
|
||||
msr fpcr, x9
|
||||
msr fpsr, x10
|
||||
3:
|
||||
ret
|
||||
.size ArchContextSwitch, . - ArchContextSwitch
|
||||
|
||||
/*
|
||||
* void ArchEnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg,
|
||||
* uint64_t returnAddress, uint64_t kernelStackTop)
|
||||
*
|
||||
* Drops to EL0t with interrupts unmasked. The kernel stack is reset to its
|
||||
* top: SP_EL1 is what the next exception from EL0 runs on.
|
||||
*/
|
||||
.global ArchEnterUserMode
|
||||
.type ArchEnterUserMode, %function
|
||||
ArchEnterUserMode:
|
||||
msr daifset, #0xf
|
||||
mov sp, x4
|
||||
msr elr_el1, x0
|
||||
msr sp_el0, x1
|
||||
msr spsr_el1, xzr /* EL0t, DAIF clear */
|
||||
mov x0, x2
|
||||
mov x30, x3
|
||||
mov x1, xzr
|
||||
mov x2, xzr
|
||||
mov x3, xzr
|
||||
mov x4, xzr
|
||||
mov x5, xzr
|
||||
mov x6, xzr
|
||||
mov x7, xzr
|
||||
mov x8, xzr
|
||||
mov x9, xzr
|
||||
mov x10, xzr
|
||||
mov x11, xzr
|
||||
mov x12, xzr
|
||||
mov x13, xzr
|
||||
mov x14, xzr
|
||||
mov x15, xzr
|
||||
mov x16, xzr
|
||||
mov x17, xzr
|
||||
mov x18, xzr
|
||||
mov x29, xzr
|
||||
eret
|
||||
.size ArchEnterUserMode, . - ArchEnterUserMode
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Context.cpp
|
||||
* aarch64 thread context (implements Arch/Context.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/Context.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
|
||||
// Context.S
|
||||
extern "C" void ArchContextSwitch(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
|
||||
uint8_t* oldFpu, uint8_t* newFpu);
|
||||
extern "C" [[noreturn]] void ArchEnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg,
|
||||
uint64_t returnAddress, uint64_t kernelStackTop);
|
||||
|
||||
namespace Arch {
|
||||
|
||||
void InitFpuState(uint8_t* area) {
|
||||
// All-zero is the architectural reset state: round-to-nearest, no
|
||||
// exceptions trapped, no flags raised.
|
||||
memset(area, 0, FpuStateSize);
|
||||
}
|
||||
|
||||
uint64_t PrepareThreadStack(uint64_t kernelStackTop, void (*entry)()) {
|
||||
// The frame Context.S pops: x19..x28, x29, x30 (= return address).
|
||||
uint64_t* sp = (uint64_t*)kernelStackTop - 12;
|
||||
for (int i = 0; i < 11; i++) {
|
||||
sp[i] = 0;
|
||||
}
|
||||
sp[11] = (uint64_t)entry;
|
||||
return (uint64_t)sp;
|
||||
}
|
||||
|
||||
void SwitchContext(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
|
||||
uint8_t* oldFpu, uint8_t* newFpu) {
|
||||
ArchContextSwitch(oldSp, newSp, newRoot, oldFpu, newFpu);
|
||||
}
|
||||
|
||||
void ActivateThread(Smp::CpuData* cpu, uint64_t kernelStackTop,
|
||||
uint64_t threadPointer) {
|
||||
cpu->entry.kernelStackTop = kernelStackTop;
|
||||
asm volatile("msr tpidr_el0, %0" :: "r"(threadPointer));
|
||||
}
|
||||
|
||||
void EnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg) {
|
||||
// Returning from the entry function lands in the exit stub (see
|
||||
// SetupUserEntryStack in Abi.hpp).
|
||||
ArchEnterUserMode(entry, userSp, arg, Sched::ExitStubAddr,
|
||||
Smp::GetCurrentCpuData()->entry.kernelStackTop);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Context.hpp
|
||||
* aarch64 thread context constants (see Arch/Context.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Arch {
|
||||
// q0-q31 followed by FPCR and FPSR (Context.S).
|
||||
constexpr uint64_t FpuStateSize = 32 * 16 + 16;
|
||||
constexpr uint64_t FpuStateAlign = 16;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Cpu.hpp
|
||||
* aarch64 implementation of the Arch/Cpu.hpp primitives
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// PSTATE.I in the DAIF view
|
||||
static constexpr uint64_t DaifIrqMask = 1ULL << 7;
|
||||
|
||||
inline void Pause() {
|
||||
asm volatile("yield" ::: "memory");
|
||||
}
|
||||
|
||||
// Device-nGnRE / Normal-NC (write-combining) stores complete in order
|
||||
// once a DSB has been issued.
|
||||
inline void StoreFence() {
|
||||
asm volatile("dsb st" ::: "memory");
|
||||
}
|
||||
|
||||
inline void MemoryFence() {
|
||||
asm volatile("dsb sy" ::: "memory");
|
||||
}
|
||||
|
||||
// Devices are outer-shareable observers.
|
||||
inline void DmaWriteFence() {
|
||||
asm volatile("dmb oshst" ::: "memory");
|
||||
}
|
||||
|
||||
inline void DmaReadFence() {
|
||||
asm volatile("dmb oshld" ::: "memory");
|
||||
}
|
||||
|
||||
inline void EnableInterrupts() {
|
||||
asm volatile("msr daifclr, #2" ::: "memory");
|
||||
}
|
||||
|
||||
inline void DisableInterrupts() {
|
||||
asm volatile("msr daifset, #2" ::: "memory");
|
||||
}
|
||||
|
||||
inline bool InterruptsEnabled() {
|
||||
uint64_t daif;
|
||||
asm volatile("mrs %0, daif" : "=r"(daif));
|
||||
return (daif & DaifIrqMask) == 0;
|
||||
}
|
||||
|
||||
inline IrqFlags SaveAndDisableInterrupts() {
|
||||
uint64_t daif;
|
||||
asm volatile("mrs %0, daif\n\tmsr daifset, #2" : "=r"(daif) :: "memory");
|
||||
return daif;
|
||||
}
|
||||
|
||||
inline void RestoreInterrupts(IrqFlags flags) {
|
||||
asm volatile("msr daif, %0" :: "r"(flags) : "memory");
|
||||
}
|
||||
|
||||
inline void Halt() {
|
||||
asm volatile("wfi" ::: "memory");
|
||||
}
|
||||
|
||||
// WFI wakes on a pending interrupt even while PSTATE.I masks it, so
|
||||
// checking for work and then waiting with IRQs masked cannot lose a
|
||||
// wakeup. Unmask briefly afterwards so the pending IRQ is taken.
|
||||
inline void HaltWithInterruptsDisabled() {
|
||||
asm volatile("wfi\n\tmsr daifclr, #2\n\tisb\n\tmsr daifset, #2" ::: "memory");
|
||||
}
|
||||
|
||||
[[noreturn]] inline void HaltForever() {
|
||||
for (;;) {
|
||||
asm volatile("msr daifset, #0xf\n\twfi" ::: "memory");
|
||||
}
|
||||
}
|
||||
|
||||
inline uint64_t ReadTimestamp() {
|
||||
uint64_t value;
|
||||
asm volatile("isb\n\tmrs %0, cntvct_el0" : "=r"(value) :: "memory");
|
||||
return value;
|
||||
}
|
||||
|
||||
// The generic timer runs from boot at CNTFRQ_EL0 Hz, so it times short
|
||||
// delays without calibration.
|
||||
inline void IoDelay() {
|
||||
uint64_t frequency;
|
||||
asm volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
|
||||
uint64_t ticks = frequency / 1000000 + 1;
|
||||
uint64_t start = ReadTimestamp();
|
||||
while (ReadTimestamp() - start < ticks) {
|
||||
Pause();
|
||||
}
|
||||
}
|
||||
|
||||
// ARMv8 has no "clean the whole cache" instruction: set/way maintenance
|
||||
// is only safe from firmware, which PSCI performs on power transitions.
|
||||
// Order outstanding accesses so the firmware call sees them.
|
||||
inline void WritebackInvalidateCaches() {
|
||||
asm volatile("dsb sy\n\tisb" ::: "memory");
|
||||
}
|
||||
|
||||
// The TLBI operand is VA[55:12] in bits [43:0]; the bits above are RES0.
|
||||
inline uint64_t DataCacheLineSize() {
|
||||
uint64_t ctr;
|
||||
asm volatile("mrs %0, ctr_el0" : "=r"(ctr));
|
||||
return 4ULL << ((ctr >> 16) & 0xF); // DminLine
|
||||
}
|
||||
|
||||
// To the point of coherency: where non-snooping device DMA looks.
|
||||
inline void CleanDataCache(const void* address, uint64_t size) {
|
||||
uint64_t line = DataCacheLineSize();
|
||||
uint64_t end = (uint64_t)address + size;
|
||||
for (uint64_t p = (uint64_t)address & ~(line - 1); p < end; p += line) {
|
||||
asm volatile("dc cvac, %0" :: "r"(p) : "memory");
|
||||
}
|
||||
asm volatile("dsb sy" ::: "memory");
|
||||
}
|
||||
|
||||
// Clean too: a partial line at either end may hold CPU data next to the
|
||||
// buffer, which a plain invalidate would throw away.
|
||||
inline void InvalidateDataCache(const void* address, uint64_t size) {
|
||||
uint64_t line = DataCacheLineSize();
|
||||
uint64_t end = (uint64_t)address + size;
|
||||
for (uint64_t p = (uint64_t)address & ~(line - 1); p < end; p += line) {
|
||||
asm volatile("dc civac, %0" :: "r"(p) : "memory");
|
||||
}
|
||||
asm volatile("dsb sy" ::: "memory");
|
||||
}
|
||||
|
||||
// Clean the data cache to the point of unification, then invalidate
|
||||
// the whole instruction cache: invalidating by address would only
|
||||
// reach the alias it names on an aliasing (VIPT) instruction cache.
|
||||
inline void SyncInstructionCache(const void* address, uint64_t size) {
|
||||
uint64_t line = DataCacheLineSize();
|
||||
uint64_t start = (uint64_t)address & ~(line - 1);
|
||||
uint64_t end = (uint64_t)address + size;
|
||||
for (uint64_t p = start; p < end; p += line) {
|
||||
asm volatile("dc cvau, %0" :: "r"(p) : "memory");
|
||||
}
|
||||
asm volatile("dsb ish\n\tic ialluis\n\tdsb ish\n\tisb" ::: "memory");
|
||||
}
|
||||
|
||||
inline void FlushTlbPage(uint64_t virtualAddress) {
|
||||
asm volatile("dsb ishst\n\ttlbi vaae1is, %0\n\tdsb ish\n\tisb"
|
||||
:: "r"((virtualAddress >> 12) & ((1ULL << 44) - 1)) : "memory");
|
||||
}
|
||||
|
||||
inline void FlushTlbAll() {
|
||||
asm volatile("dsb ishst\n\ttlbi vmalle1is\n\tdsb ish\n\tisb" ::: "memory");
|
||||
}
|
||||
|
||||
// The process (lower) half of the address space is translated through
|
||||
// TTBR0_EL1; the kernel half through TTBR1_EL1, which never changes.
|
||||
static constexpr uint64_t TtbrAddressMask = 0x0000FFFFFFFFFFFEULL;
|
||||
|
||||
inline uint64_t ReadPageTableRoot() {
|
||||
uint64_t ttbr;
|
||||
asm volatile("mrs %0, ttbr0_el1" : "=r"(ttbr));
|
||||
return ttbr & TtbrAddressMask;
|
||||
}
|
||||
|
||||
// No ASIDs yet: every switch invalidates the old process's entries.
|
||||
inline void WritePageTableRoot(uint64_t rootPhys) {
|
||||
asm volatile("dsb ishst\n\tmsr ttbr0_el1, %0\n\tisb\n\ttlbi vmalle1\n\tdsb nsh\n\tisb"
|
||||
:: "r"(rootPhys) : "memory");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* CpuPower.cpp
|
||||
* aarch64 CPU power management (implements Arch/CpuPower.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/CpuPower.hpp>
|
||||
|
||||
// No frequency scaling or thermal governor yet (on ARM these come from
|
||||
// ACPI CPPC / SCMI rather than CPU registers).
|
||||
|
||||
namespace Arch::CpuPower {
|
||||
|
||||
void InitializeBsp() {}
|
||||
void InitializeAp() {}
|
||||
void ApplyPolicyIfChanged() {}
|
||||
void ThermalTick(uint64_t) {}
|
||||
void ServiceDeferredDiagnostics() {}
|
||||
void ReapplyAfterWake() {}
|
||||
|
||||
bool GetSnapshot(Snapshot& out) {
|
||||
out = {};
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Exceptions.cpp
|
||||
* aarch64 synchronous exceptions: faults, user crashes, kernel panics
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/aarch64/InterruptFrame.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/Smp.hpp>
|
||||
#include <Common/Panic.hpp>
|
||||
#include <Common/TerminationCodes.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Sched/CrashReport.hpp>
|
||||
#include <Api/Heap.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace {
|
||||
|
||||
// ESR_EL1.EC exception classes
|
||||
constexpr uint64_t EcUnknown = 0x00;
|
||||
constexpr uint64_t EcIllegalState = 0x0E;
|
||||
constexpr uint64_t EcInstAbortLower = 0x20;
|
||||
constexpr uint64_t EcInstAbortSame = 0x21;
|
||||
constexpr uint64_t EcPcAlignment = 0x22;
|
||||
constexpr uint64_t EcDataAbortLower = 0x24;
|
||||
constexpr uint64_t EcDataAbortSame = 0x25;
|
||||
constexpr uint64_t EcSpAlignment = 0x26;
|
||||
constexpr uint64_t EcFpException = 0x2C;
|
||||
constexpr uint64_t EcBreakpointLower = 0x30;
|
||||
constexpr uint64_t EcBrk = 0x3C;
|
||||
|
||||
constexpr uint64_t UserSpaceEnd = 0x0000800000000000ULL;
|
||||
|
||||
uint64_t ExceptionClass(const Arch::PanicFrame* frame) {
|
||||
return (frame->esr >> 26) & 0x3F;
|
||||
}
|
||||
|
||||
bool IsAbort(uint64_t ec) {
|
||||
return ec == EcInstAbortLower || ec == EcInstAbortSame
|
||||
|| ec == EcDataAbortLower || ec == EcDataAbortSame;
|
||||
}
|
||||
|
||||
// Fault status code (ISS[5:0]) of an abort: translation faults mean
|
||||
// the page is not mapped, permission faults that it is.
|
||||
bool IsTranslationFault(uint64_t esr) {
|
||||
uint64_t fsc = esr & 0x3F;
|
||||
return fsc >= 0x04 && fsc <= 0x07;
|
||||
}
|
||||
|
||||
bool IsPermissionFault(uint64_t esr) {
|
||||
uint64_t fsc = esr & 0x3F;
|
||||
return fsc >= 0x0C && fsc <= 0x0F;
|
||||
}
|
||||
|
||||
const char* ExceptionName(uint64_t ec) {
|
||||
switch (ec) {
|
||||
case EcUnknown: return "Undefined Instruction";
|
||||
case EcIllegalState: return "Illegal Execution State";
|
||||
case EcInstAbortLower:
|
||||
case EcInstAbortSame: return "Instruction Abort";
|
||||
case EcPcAlignment: return "PC Alignment Fault";
|
||||
case EcDataAbortLower:
|
||||
case EcDataAbortSame: return "Data Abort";
|
||||
case EcSpAlignment: return "SP Alignment Fault";
|
||||
case EcFpException: return "Floating-Point Exception";
|
||||
case EcBrk: return "Breakpoint (BRK)";
|
||||
default:
|
||||
if (ec >= EcBreakpointLower && ec <= 0x35) return "Debug Exception";
|
||||
return "Synchronous Exception";
|
||||
}
|
||||
}
|
||||
|
||||
TerminationCodes TerminationFromClass(uint64_t ec) {
|
||||
if (ec == EcUnknown || ec == EcIllegalState) return TerminatedIllegalInstruction;
|
||||
if (ec == EcBrk || (ec >= EcBreakpointLower && ec <= 0x35)) return TerminatedBreakpoint;
|
||||
if (ec == EcFpException) return TerminatedArithmeticError;
|
||||
return TerminatedAccessViolation;
|
||||
}
|
||||
|
||||
template<size_t N>
|
||||
void CopyName(char (&dst)[N], const char* src) {
|
||||
size_t i = 0;
|
||||
for (; i + 1 < N && src[i]; i++) dst[i] = src[i];
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
// Kill the faulting process with a crash report. Never returns.
|
||||
[[noreturn]] void TerminateUserProcess(Arch::PanicFrame* frame, uint64_t ec) {
|
||||
// Teardown can wait on devices and other CPUs; keep the clock running.
|
||||
Arch::EnableInterrupts();
|
||||
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
const char* name = ExceptionName(ec);
|
||||
KernelLogStream(ERROR, "Exception") << name << " in process \"" << proc->name
|
||||
<< "\" (pid " << proc->pid << ", ESR 0x" << base::hex << frame->esr
|
||||
<< ", PC 0x" << frame->elr << ") - process terminated";
|
||||
|
||||
CrashReport::Report rep{};
|
||||
rep.valid = true;
|
||||
rep.pid = proc->pid;
|
||||
CopyName(rep.processName, proc->name);
|
||||
rep.exceptionVector = (uint8_t)ec;
|
||||
CopyName(rep.exceptionName, name);
|
||||
rep.instructionPointer = frame->elr;
|
||||
rep.flags = frame->spsr;
|
||||
rep.stackPointer = frame->sp;
|
||||
|
||||
constexpr uint64_t snapshotBytes = CrashReport::StackSnapshotWords * sizeof(uint64_t);
|
||||
if (Memory::VMM::Paging::IsUserRangeAccessible(proc->pageTableRoot, frame->sp,
|
||||
snapshotBytes, false)) {
|
||||
auto* userStack = (const volatile uint64_t*)frame->sp;
|
||||
for (int word = 0; word < CrashReport::StackSnapshotWords; word++)
|
||||
rep.stackWords[word] = userStack[word];
|
||||
rep.stackWordCount = CrashReport::StackSnapshotWords;
|
||||
}
|
||||
|
||||
if (IsAbort(ec)) {
|
||||
rep.faultingAddress = frame->far;
|
||||
rep.pfPresent = IsPermissionFault(frame->esr);
|
||||
rep.pfWrite = ec == EcDataAbortLower && (frame->esr & (1ULL << 6)) != 0;
|
||||
rep.pfUser = 1;
|
||||
rep.pfInstructionFetch = ec == EcInstAbortLower;
|
||||
}
|
||||
rep.timestampTick = Timekeeping::GetTicks();
|
||||
|
||||
CrashReport::AddReport(rep);
|
||||
Sched::SpawnCrashPad(proc->pid);
|
||||
|
||||
// As on x86: a sibling thread's fault must end the whole process.
|
||||
int code = TerminationFromClass(ec);
|
||||
if (Sched::GetCurrentThreadPtr() != proc) {
|
||||
Sched::KillProcess(proc->pid, code);
|
||||
} else {
|
||||
Sched::SetProcessExitCode(proc->pid, code);
|
||||
}
|
||||
|
||||
Sched::ExitProcess();
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
// A not-present user page: anonymous memory mapped lazily or the user
|
||||
// stack growing. Also reached from the kernel touching user buffers.
|
||||
bool TryResolveUserFault(const Arch::PanicFrame* frame, uint64_t ec) {
|
||||
if (!IsAbort(ec) || !IsTranslationFault(frame->esr)) return false;
|
||||
if (frame->far >= UserSpaceEnd) return false;
|
||||
|
||||
auto* cpu = Smp::TryGetCurrentCpuData();
|
||||
if (cpu == nullptr || cpu->currentSlot < 0 || Sched::GetCurrentPid() < 0) return false;
|
||||
|
||||
bool isData = ec == EcDataAbortLower || ec == EcDataAbortSame;
|
||||
Memory::VMM::PageFault fault{false, isData && (frame->esr & (1ULL << 6)) != 0, !isData};
|
||||
return montauk::abi::TryHandleAnonymousPageFault(frame->far, fault)
|
||||
|| Sched::TryGrowUserStack(frame->far);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void ArchSyncException(Arch::PanicFrame* frame, bool fromUser) {
|
||||
uint64_t ec = ExceptionClass(frame);
|
||||
|
||||
if (TryResolveUserFault(frame, ec)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fromUser && Sched::GetCurrentPid() >= 0) {
|
||||
TerminateUserProcess(frame, ec);
|
||||
}
|
||||
|
||||
Panic(ExceptionName(ec), frame);
|
||||
}
|
||||
|
||||
// FIQs are never routed, SErrors are asynchronous external aborts, and the
|
||||
// kernel neither runs on SP_EL0 nor supports AArch32 user code.
|
||||
extern "C" void ArchUnexpectedException(Arch::PanicFrame* frame, uint64_t kind) {
|
||||
static const char* const names[] = {
|
||||
"Exception on SP_EL0", "Unexpected FIQ", "SError (asynchronous abort)",
|
||||
"AArch32 exception",
|
||||
};
|
||||
Panic(kind < 4 ? names[kind] : "Unexpected exception", frame);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Gic.hpp
|
||||
* aarch64 GICv2 interrupt controller (private to the port)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <ACPI/ACPI.hpp>
|
||||
|
||||
namespace Arch::Gic {
|
||||
|
||||
// Bring up the distributor and the calling CPU's interface from the
|
||||
// ACPI MADT. Returns false when no usable GIC is described.
|
||||
bool Initialize(Hal::ACPI::CommonSDTHeader* xsdt);
|
||||
|
||||
bool IsInitialized();
|
||||
|
||||
// The XSDT Initialize was given (for the other ACPI tables the port reads).
|
||||
Hal::ACPI::CommonSDTHeader* CurrentXsdt();
|
||||
|
||||
// Per-CPU part of Initialize, for secondary CPUs.
|
||||
void InitializeCpuInterface();
|
||||
|
||||
// Map a private peripheral interrupt (16-31) onto an IRQ slot and enable
|
||||
// it on the calling CPU.
|
||||
void RoutePpi(uint32_t intid, uint8_t irq, bool edgeTriggered);
|
||||
|
||||
// Called for an IRQ slot after the interrupt is acknowledged and before
|
||||
// it is completed (EOI): level-triggered sources deassert here.
|
||||
using AckHook = void (*)();
|
||||
void SetAckHook(uint8_t irq, AckHook hook);
|
||||
|
||||
// The calling CPU's target bit, as the distributor sees it.
|
||||
uint8_t CurrentTargetMask();
|
||||
|
||||
// Find an ACPI table in the XSDT by signature.
|
||||
Hal::ACPI::CommonSDTHeader* FindTable(Hal::ACPI::CommonSDTHeader* xsdt, const char* signature);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* InterruptFrame.hpp
|
||||
* aarch64 exception frame
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// What the exception vectors (Vectors.S) save before calling into C++.
|
||||
struct PanicFrame {
|
||||
uint64_t x[31]; // x0-x30
|
||||
uint64_t sp; // interrupted SP (SP_EL0 for EL0, else SP_EL1)
|
||||
uint64_t elr; // ELR_EL1: faulting/preferred-return PC
|
||||
uint64_t spsr; // SPSR_EL1
|
||||
uint64_t esr; // ESR_EL1: exception syndrome
|
||||
uint64_t far; // FAR_EL1: faulting address
|
||||
};
|
||||
|
||||
static_assert(sizeof(PanicFrame) == 288, "must match Vectors.S");
|
||||
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
* Interrupts.cpp
|
||||
* aarch64 GICv2 driver and IRQ slot dispatch (implements Arch/Interrupts.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/Interrupts.hpp>
|
||||
#include <Arch/Smp.hpp>
|
||||
#include <Arch/aarch64/Gic.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
// IRQ slots map onto GIC interrupt IDs (INTIDs):
|
||||
// IRQ_TIMER the generic timer's PPI (Timer.cpp routes it)
|
||||
// 24-45 (MSI slots) SPIs of the GICv2m MSI frame, one per slot
|
||||
// IRQ_TLB_SHOOTDOWN,
|
||||
// IRQ_RESCHEDULE SGIs 0 and 1
|
||||
// 1-23 (platform lines) SPIs the PCI layer assigns to INTx pins from
|
||||
// ACPI _PRT (AssignPlatformLine)
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// Distributor registers
|
||||
static constexpr uint32_t GICD_CTLR = 0x000;
|
||||
static constexpr uint32_t GICD_TYPER = 0x004;
|
||||
static constexpr uint32_t GICD_ISENABLER = 0x100;
|
||||
static constexpr uint32_t GICD_ICENABLER = 0x180;
|
||||
static constexpr uint32_t GICD_ICPENDR = 0x280;
|
||||
static constexpr uint32_t GICD_ICACTIVER = 0x380;
|
||||
static constexpr uint32_t GICD_IPRIORITYR = 0x400;
|
||||
static constexpr uint32_t GICD_ITARGETSR = 0x800;
|
||||
static constexpr uint32_t GICD_ICFGR = 0xC00;
|
||||
static constexpr uint32_t GICD_SGIR = 0xF00;
|
||||
|
||||
// CPU interface registers
|
||||
static constexpr uint32_t GICC_CTLR = 0x00;
|
||||
static constexpr uint32_t GICC_PMR = 0x04;
|
||||
static constexpr uint32_t GICC_BPR = 0x08;
|
||||
static constexpr uint32_t GICC_IAR = 0x0C;
|
||||
static constexpr uint32_t GICC_EOIR = 0x10;
|
||||
|
||||
// GICv2m MSI frame registers
|
||||
static constexpr uint32_t V2M_MSI_TYPER = 0x008;
|
||||
static constexpr uint32_t V2M_MSI_SETSPI_NS = 0x040;
|
||||
|
||||
static constexpr uint32_t SpuriousIntid = 1020;
|
||||
static constexpr uint32_t MaxIntids = 1020;
|
||||
static constexpr uint8_t DefaultPriority = 0xA0;
|
||||
static constexpr uint8_t NoSlot = 0xFF;
|
||||
|
||||
static constexpr uint8_t FirstMsiSlot = 24;
|
||||
static constexpr uint8_t LastMsiSlot = 45;
|
||||
|
||||
// MADT interrupt-controller structures (ACPI 6.x)
|
||||
static constexpr uint8_t MadtGicc = 0x0B;
|
||||
static constexpr uint8_t MadtGicd = 0x0C;
|
||||
static constexpr uint8_t MadtGicMsiFrame = 0x0D;
|
||||
|
||||
struct MadtEntry {
|
||||
uint8_t Type;
|
||||
uint8_t Length;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MadtGiccEntry {
|
||||
MadtEntry Header;
|
||||
uint16_t Reserved;
|
||||
uint32_t CpuInterfaceNumber;
|
||||
uint32_t AcpiProcessorUid;
|
||||
uint32_t Flags;
|
||||
uint32_t ParkingProtocolVersion;
|
||||
uint32_t PerformanceInterruptGsiv;
|
||||
uint64_t ParkedAddress;
|
||||
uint64_t PhysicalBaseAddress; // GICC
|
||||
uint64_t Gicv;
|
||||
uint64_t Gich;
|
||||
uint32_t VgicMaintenanceInterrupt;
|
||||
uint64_t GicrBaseAddress;
|
||||
uint64_t Mpidr;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MadtGicdEntry {
|
||||
MadtEntry Header;
|
||||
uint16_t Reserved;
|
||||
uint32_t GicId;
|
||||
uint64_t PhysicalBaseAddress;
|
||||
uint32_t SystemVectorBase;
|
||||
uint8_t GicVersion;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MadtMsiFrameEntry {
|
||||
MadtEntry Header;
|
||||
uint16_t Reserved;
|
||||
uint32_t FrameId;
|
||||
uint64_t PhysicalBaseAddress;
|
||||
uint32_t Flags; // bit 0: SpiCount/SpiBase override TYPER
|
||||
uint16_t SpiCount;
|
||||
uint16_t SpiBase;
|
||||
} __attribute__((packed));
|
||||
|
||||
static constexpr uint32_t MadtHeaderSize = 44; // SDT header + LAPIC address + flags
|
||||
|
||||
static IrqHandler g_irqHandlers[IRQ_COUNT] = {};
|
||||
static Gic::AckHook g_ackHooks[IRQ_COUNT] = {};
|
||||
static uint8_t g_intidToSlot[MaxIntids];
|
||||
static uint32_t g_slotToIntid[IRQ_COUNT];
|
||||
|
||||
static volatile uint8_t* g_gicd = nullptr;
|
||||
static volatile uint8_t* g_gicc = nullptr;
|
||||
static uint32_t g_intidCount = 0;
|
||||
|
||||
static Hal::ACPI::CommonSDTHeader* g_xsdt = nullptr;
|
||||
|
||||
struct PlatformLine {
|
||||
uint32_t gsi; // 0: unassigned
|
||||
bool edgeTriggered;
|
||||
};
|
||||
static PlatformLine g_platformLines[24] = {};
|
||||
|
||||
static uint64_t g_msiFramePhys = 0;
|
||||
static uint32_t g_msiSpiBase = 0;
|
||||
static uint32_t g_msiSpiCount = 0;
|
||||
|
||||
static inline uint32_t GicdRead(uint32_t reg) {
|
||||
return *(volatile uint32_t*)(g_gicd + reg);
|
||||
}
|
||||
|
||||
static inline void GicdWrite(uint32_t reg, uint32_t value) {
|
||||
*(volatile uint32_t*)(g_gicd + reg) = value;
|
||||
}
|
||||
|
||||
static inline void GicdWrite8(uint32_t reg, uint8_t value) {
|
||||
*(volatile uint8_t*)(g_gicd + reg) = value;
|
||||
}
|
||||
|
||||
static inline uint8_t GicdRead8(uint32_t reg) {
|
||||
return *(volatile uint8_t*)(g_gicd + reg);
|
||||
}
|
||||
|
||||
static inline uint32_t GiccRead(uint32_t reg) {
|
||||
return *(volatile uint32_t*)(g_gicc + reg);
|
||||
}
|
||||
|
||||
static inline void GiccWrite(uint32_t reg, uint32_t value) {
|
||||
*(volatile uint32_t*)(g_gicc + reg) = value;
|
||||
}
|
||||
|
||||
static volatile uint8_t* MapRegisters(uint64_t phys, uint64_t size) {
|
||||
for (uint64_t offset = 0; offset < size; offset += 0x1000) {
|
||||
Memory::VMM::g_paging->MapMMIO(phys + offset, Memory::HHDM(phys + offset));
|
||||
}
|
||||
return (volatile uint8_t*)Memory::HHDM(phys);
|
||||
}
|
||||
|
||||
static void SetConfig(uint32_t intid, bool edgeTriggered) {
|
||||
uint32_t reg = GICD_ICFGR + (intid / 16) * 4;
|
||||
uint32_t shift = (intid % 16) * 2 + 1;
|
||||
uint32_t value = GicdRead(reg);
|
||||
if (edgeTriggered) value |= 1u << shift;
|
||||
else value &= ~(1u << shift);
|
||||
GicdWrite(reg, value);
|
||||
}
|
||||
|
||||
static void EnableIntid(uint32_t intid) {
|
||||
GicdWrite(GICD_ISENABLER + (intid / 32) * 4, 1u << (intid % 32));
|
||||
}
|
||||
|
||||
static void Bind(uint32_t intid, uint8_t irq) {
|
||||
if (intid >= MaxIntids || irq >= IRQ_COUNT) return;
|
||||
g_intidToSlot[intid] = irq;
|
||||
g_slotToIntid[irq] = intid;
|
||||
}
|
||||
|
||||
// Route a shared peripheral interrupt to the boot CPU and unmask it.
|
||||
static void RouteSpi(uint32_t intid, uint8_t irq, bool edgeTriggered) {
|
||||
if (intid < 32 || intid >= g_intidCount) return;
|
||||
Bind(intid, irq);
|
||||
GicdWrite8(GICD_IPRIORITYR + intid, DefaultPriority);
|
||||
GicdWrite8(GICD_ITARGETSR + intid, Smp::GetCpuData(0)->arch.gicTargetMask);
|
||||
SetConfig(intid, edgeTriggered);
|
||||
EnableIntid(intid);
|
||||
}
|
||||
|
||||
namespace Gic {
|
||||
|
||||
Hal::ACPI::CommonSDTHeader* FindTable(Hal::ACPI::CommonSDTHeader* xsdt, const char* signature) {
|
||||
if (xsdt == nullptr) return nullptr;
|
||||
uint32_t count = (xsdt->Length - sizeof(Hal::ACPI::CommonSDTHeader)) / 8;
|
||||
auto* entries = (uint64_t*)((uint64_t)xsdt + sizeof(Hal::ACPI::CommonSDTHeader));
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
auto* header = (Hal::ACPI::CommonSDTHeader*)Memory::HHDM(entries[i]);
|
||||
if (header->Signature[0] == signature[0] && header->Signature[1] == signature[1]
|
||||
&& header->Signature[2] == signature[2] && header->Signature[3] == signature[3]) {
|
||||
return header;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ITARGETSR0-7 are banked: each byte reads back the reading CPU's
|
||||
// own interface bit.
|
||||
uint8_t CurrentTargetMask() {
|
||||
uint32_t value = GicdRead(GICD_ITARGETSR);
|
||||
return (uint8_t)(value | value >> 8 | value >> 16 | value >> 24);
|
||||
}
|
||||
|
||||
void InitializeCpuInterface() {
|
||||
// SGIs and PPIs are banked per CPU: disable the PPIs until a
|
||||
// driver routes one, keep the SGIs (IPIs) enabled.
|
||||
GicdWrite(GICD_ICENABLER, 0xFFFF0000u);
|
||||
GicdWrite(GICD_ISENABLER, 0x0000FFFFu);
|
||||
GicdWrite(GICD_ICPENDR, 0xFFFFFFFFu);
|
||||
for (uint32_t intid = 0; intid < 32; intid++) {
|
||||
GicdWrite8(GICD_IPRIORITYR + intid, DefaultPriority);
|
||||
}
|
||||
|
||||
GiccWrite(GICC_PMR, 0xF0);
|
||||
GiccWrite(GICC_BPR, 0);
|
||||
GiccWrite(GICC_CTLR, 1);
|
||||
|
||||
auto* cpu = Smp::GetCurrentCpuData();
|
||||
cpu->arch.gicTargetMask = CurrentTargetMask();
|
||||
}
|
||||
|
||||
void RoutePpi(uint32_t intid, uint8_t irq, bool edgeTriggered) {
|
||||
if (intid < 16 || intid >= 32) return;
|
||||
Bind(intid, irq);
|
||||
SetConfig(intid, edgeTriggered);
|
||||
EnableIntid(intid);
|
||||
}
|
||||
|
||||
void SetAckHook(uint8_t irq, AckHook hook) {
|
||||
if (irq < IRQ_COUNT) g_ackHooks[irq] = hook;
|
||||
}
|
||||
|
||||
bool IsInitialized() {
|
||||
return g_gicc != nullptr;
|
||||
}
|
||||
|
||||
Hal::ACPI::CommonSDTHeader* CurrentXsdt() {
|
||||
return g_xsdt;
|
||||
}
|
||||
|
||||
bool Initialize(Hal::ACPI::CommonSDTHeader* xsdt) {
|
||||
g_xsdt = xsdt;
|
||||
auto* madt = FindTable(xsdt, "APIC");
|
||||
if (madt == nullptr) {
|
||||
KernelLogStream(ERROR, "GIC") << "No MADT";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t gicdPhys = 0, giccPhys = 0;
|
||||
uint8_t version = 0;
|
||||
auto* cursor = (uint8_t*)madt + MadtHeaderSize;
|
||||
auto* end = (uint8_t*)madt + madt->Length;
|
||||
while (cursor + sizeof(MadtEntry) <= end) {
|
||||
auto* entry = (MadtEntry*)cursor;
|
||||
if (entry->Length < sizeof(MadtEntry)) break;
|
||||
|
||||
if (entry->Type == MadtGicd) {
|
||||
auto* gicd = (MadtGicdEntry*)entry;
|
||||
gicdPhys = gicd->PhysicalBaseAddress;
|
||||
version = gicd->GicVersion;
|
||||
} else if (entry->Type == MadtGicc && giccPhys == 0) {
|
||||
giccPhys = ((MadtGiccEntry*)entry)->PhysicalBaseAddress;
|
||||
} else if (entry->Type == MadtGicMsiFrame && g_msiFramePhys == 0) {
|
||||
auto* frame = (MadtMsiFrameEntry*)entry;
|
||||
g_msiFramePhys = frame->PhysicalBaseAddress;
|
||||
if (frame->Flags & 1) {
|
||||
g_msiSpiBase = frame->SpiBase;
|
||||
g_msiSpiCount = frame->SpiCount;
|
||||
}
|
||||
}
|
||||
cursor += entry->Length;
|
||||
}
|
||||
|
||||
// Version 0 means "infer from the hardware"; 1 and 2 share a
|
||||
// programming model.
|
||||
if (gicdPhys == 0 || giccPhys == 0 || version > 2) {
|
||||
KernelLogStream(ERROR, "GIC") << "No GICv2 in the MADT (version "
|
||||
<< base::dec << (uint64_t)version << ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < MaxIntids; i++) g_intidToSlot[i] = NoSlot;
|
||||
for (int i = 0; i < IRQ_COUNT; i++) g_slotToIntid[i] = SpuriousIntid;
|
||||
Bind(0, IRQ_TLB_SHOOTDOWN);
|
||||
Bind(1, IRQ_RESCHEDULE);
|
||||
|
||||
g_gicd = MapRegisters(gicdPhys, 0x1000);
|
||||
g_gicc = MapRegisters(giccPhys, 0x2000);
|
||||
|
||||
g_intidCount = ((GicdRead(GICD_TYPER) & 0x1F) + 1) * 32;
|
||||
if (g_intidCount > MaxIntids) g_intidCount = MaxIntids;
|
||||
|
||||
// Distributor: everything masked, idle, level-triggered and
|
||||
// aimed at no CPU until RouteSpi claims it.
|
||||
GicdWrite(GICD_CTLR, 0);
|
||||
for (uint32_t intid = 32; intid < g_intidCount; intid += 32) {
|
||||
GicdWrite(GICD_ICENABLER + (intid / 32) * 4, 0xFFFFFFFFu);
|
||||
GicdWrite(GICD_ICPENDR + (intid / 32) * 4, 0xFFFFFFFFu);
|
||||
GicdWrite(GICD_ICACTIVER + (intid / 32) * 4, 0xFFFFFFFFu);
|
||||
}
|
||||
for (uint32_t intid = 32; intid < g_intidCount; intid++) {
|
||||
GicdWrite8(GICD_IPRIORITYR + intid, DefaultPriority);
|
||||
GicdWrite8(GICD_ITARGETSR + intid, 0);
|
||||
}
|
||||
for (uint32_t intid = 32; intid < g_intidCount; intid += 16) {
|
||||
GicdWrite(GICD_ICFGR + (intid / 16) * 4, 0);
|
||||
}
|
||||
GicdWrite(GICD_CTLR, 1);
|
||||
|
||||
InitializeCpuInterface();
|
||||
|
||||
if (g_msiFramePhys != 0) {
|
||||
volatile uint8_t* frame = MapRegisters(g_msiFramePhys, 0x1000);
|
||||
if (g_msiSpiCount == 0) {
|
||||
uint32_t typer = *(volatile uint32_t*)(frame + V2M_MSI_TYPER);
|
||||
g_msiSpiBase = (typer >> 16) & 0x3FF;
|
||||
g_msiSpiCount = typer & 0x3FF;
|
||||
}
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "GIC") << "GICv2: distributor 0x" << base::hex << gicdPhys
|
||||
<< ", CPU interface 0x" << giccPhys << ", " << base::dec
|
||||
<< (uint64_t)g_intidCount << " interrupt IDs";
|
||||
if (g_msiSpiCount != 0) {
|
||||
KernelLogStream(OK, "GIC") << "GICv2m MSI frame 0x" << base::hex << g_msiFramePhys
|
||||
<< ": SPIs " << base::dec << (uint64_t)g_msiSpiBase << "-"
|
||||
<< (uint64_t)(g_msiSpiBase + g_msiSpiCount - 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void RegisterIrqHandler(uint8_t irq, IrqHandler handler) {
|
||||
if (irq >= IRQ_COUNT) return;
|
||||
g_irqHandlers[irq] = handler;
|
||||
KernelLogStream(DEBUG, "IRQ") << "Registered handler for IRQ "
|
||||
<< base::dec << (uint64_t)irq;
|
||||
}
|
||||
|
||||
bool PlatformLinesAreFixed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ACPI GSIs are GIC INTIDs. The GIC knows only active-high levels and
|
||||
// rising edges; an active-low line is inverted on its way in.
|
||||
bool AssignPlatformLine(uint8_t irq, uint32_t gsi, bool levelTriggered, bool) {
|
||||
if (irq == 0 || irq >= 24 || gsi < 32 || gsi >= g_intidCount) return false;
|
||||
g_platformLines[irq] = PlatformLine{ gsi, !levelTriggered };
|
||||
return true;
|
||||
}
|
||||
|
||||
void RouteLegacyIrq(uint8_t irq) {
|
||||
if (irq >= 24 || g_platformLines[irq].gsi == 0) {
|
||||
KernelLogStream(WARNING, "IRQ") << "aarch64: platform line "
|
||||
<< base::dec << (uint64_t)irq << " is not assigned";
|
||||
return;
|
||||
}
|
||||
RouteSpi(g_platformLines[irq].gsi, irq, g_platformLines[irq].edgeTriggered);
|
||||
}
|
||||
|
||||
void EnableLegacyIrq(uint8_t irq) {
|
||||
RouteLegacyIrq(irq);
|
||||
}
|
||||
|
||||
bool ComposeMsi(uint8_t irq, MsiMessage& out) {
|
||||
if (g_gicd == nullptr || irq < FirstMsiSlot || irq > LastMsiSlot) return false;
|
||||
uint32_t index = irq - FirstMsiSlot;
|
||||
if (index >= g_msiSpiCount) return false;
|
||||
|
||||
uint32_t spi = g_msiSpiBase + index;
|
||||
if (g_slotToIntid[irq] != spi) {
|
||||
RouteSpi(spi, irq, true);
|
||||
}
|
||||
out = MsiMessage{ g_msiFramePhys + V2M_MSI_SETSPI_NS, spi };
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SendIpi(int cpuIndex, uint8_t irq) {
|
||||
if (g_gicd == nullptr) return false;
|
||||
auto* cpu = Smp::GetCpuData(cpuIndex);
|
||||
if (cpu == nullptr || cpu->arch.gicTargetMask == 0) return false;
|
||||
uint32_t sgi = g_slotToIntid[irq];
|
||||
if (sgi >= 16) return false;
|
||||
|
||||
// Make the IPI's memory effects visible before it is raised.
|
||||
asm volatile("dsb ishst" ::: "memory");
|
||||
GicdWrite(GICD_SGIR, ((uint32_t)cpu->arch.gicTargetMask << 16) | sgi);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// IRQ exception vector (Vectors.S). As on x86, the interrupt is completed
|
||||
// BEFORE the handler runs: the handler may context-switch away and not
|
||||
// come back here for a long time, and an un-EOId interrupt would hold this
|
||||
// CPU's running priority up (no further ticks). Level-triggered sources
|
||||
// get the chance to drop their line first through an ack hook.
|
||||
extern "C" void ArchIrqException(void*, bool fromUser) {
|
||||
using namespace Arch;
|
||||
uint32_t iar = GiccRead(GICC_IAR);
|
||||
uint32_t intid = iar & 0x3FF;
|
||||
if (intid >= SpuriousIntid) return;
|
||||
|
||||
uint8_t irq = g_intidToSlot[intid];
|
||||
if (irq != NoSlot && g_ackHooks[irq] != nullptr) {
|
||||
g_ackHooks[irq]();
|
||||
}
|
||||
GiccWrite(GICC_EOIR, iar);
|
||||
|
||||
if (irq != NoSlot && g_irqHandlers[irq] != nullptr) {
|
||||
g_irqHandlers[irq](irq, fromUser);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* IoPort.hpp
|
||||
* aarch64 port I/O (implements Io/IoPort.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Arch/Cpu.hpp>
|
||||
|
||||
// ARM has no I/O address space. Until a PCI host bridge driver maps its I/O
|
||||
// aperture, port accesses behave like accesses to an absent device.
|
||||
|
||||
namespace Io {
|
||||
static constexpr bool HasPortIo = false;
|
||||
|
||||
inline void Out8(uint8_t, uint16_t) {}
|
||||
inline void Out16(uint16_t, uint16_t) {}
|
||||
inline void Out32(uint32_t, uint16_t) {}
|
||||
|
||||
inline uint8_t In8(uint16_t) { return 0xFF; }
|
||||
inline uint16_t In16(uint16_t) { return 0xFFFF; }
|
||||
inline uint32_t In32(uint16_t) { return 0xFFFFFFFF; }
|
||||
|
||||
inline void IoPortWait() {
|
||||
Arch::IoDelay();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,528 @@
|
||||
/*
|
||||
* Paging.cpp
|
||||
* aarch64 virtual memory (implements Memory/Paging.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Common/Panic.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Arch/aarch64/Bringup.hpp>
|
||||
|
||||
// 4 KiB granule, 48-bit virtual addresses, four levels (0-3). The kernel
|
||||
// half is translated through TTBR1_EL1 (g_kernelHalfRoot), which never
|
||||
// changes after Init. The "address space roots" the generic kernel switches
|
||||
// between are TTBR0_EL1 tables: a process's root holds only its own lower
|
||||
// half, and KernelRoot() is the TTBR0 table of kernel contexts, empty apart
|
||||
// from the EFI runtime identity mappings (so null pointers fault).
|
||||
//
|
||||
// The HHDM covers only what the memory map lists as RAM or firmware data:
|
||||
// unlike on x86, the holes between are device space on ARM machines, and a
|
||||
// cacheable mapping of device registers invites speculative accesses.
|
||||
|
||||
namespace Memory::VMM {
|
||||
|
||||
Paging* g_paging = nullptr;
|
||||
|
||||
static std::uint64_t g_hhdmLimit = 0;
|
||||
|
||||
static std::uint64_t g_kernelHalfRoot = 0;
|
||||
|
||||
std::uint64_t Paging::HhdmLimit() { return g_hhdmLimit; }
|
||||
|
||||
// Translation table descriptor bits (VMSAv8-64, stage 1)
|
||||
static constexpr uint64_t DescValid = 1ULL << 0;
|
||||
static constexpr uint64_t DescTable = 1ULL << 1; // table (levels 0-2) or page (level 3)
|
||||
static constexpr uint64_t DescAttrShift = 2; // AttrIndx[2:0], into MAIR_EL1
|
||||
static constexpr uint64_t DescAttrMask = 7ULL << DescAttrShift;
|
||||
static constexpr uint64_t DescApUser = 1ULL << 6; // AP[1]: EL0 may access
|
||||
static constexpr uint64_t DescApReadOnly = 1ULL << 7; // AP[2]
|
||||
static constexpr uint64_t DescShInner = 3ULL << 8;
|
||||
static constexpr uint64_t DescAf = 1ULL << 10; // access flag (no AF faults)
|
||||
static constexpr uint64_t DescNotGlobal = 1ULL << 11;
|
||||
static constexpr uint64_t DescPxn = 1ULL << 53;
|
||||
static constexpr uint64_t DescUxn = 1ULL << 54;
|
||||
static constexpr uint64_t DescAddrMask = 0x0000FFFFFFFFF000ULL;
|
||||
|
||||
// MAIR_EL1 attribute indices
|
||||
static constexpr uint64_t AttrNormal = 0; // write-back, read/write-allocate
|
||||
static constexpr uint64_t AttrNormalNc = 1; // non-cacheable (write-combining)
|
||||
static constexpr uint64_t AttrDevice = 2; // Device-nGnRE
|
||||
static constexpr uint64_t MairValue = 0xFFULL << (8 * AttrNormal)
|
||||
| 0x44ULL << (8 * AttrNormalNc)
|
||||
| 0x04ULL << (8 * AttrDevice);
|
||||
|
||||
static constexpr uint64_t KernelHalfBase = 0xFFFF000000000000ULL;
|
||||
static constexpr uint64_t UserSpaceEnd = 0x0000800000000000ULL;
|
||||
|
||||
// EFI memory types whose runtime regions are device registers
|
||||
static constexpr uint32_t EfiMemoryMappedIo = 11;
|
||||
static constexpr uint32_t EfiMemoryMappedIoPortSpace = 12;
|
||||
|
||||
static kcp::Mutex pagingLock;
|
||||
|
||||
void LockUserPaging() { pagingLock.Acquire(); }
|
||||
void UnlockUserPaging() { pagingLock.Release(); }
|
||||
|
||||
void ActivateKernelAddressSpace() {
|
||||
Arch::WritePageTableRoot(g_paging->KernelRoot());
|
||||
}
|
||||
|
||||
void FlushTLB() {
|
||||
Arch::FlushTlbAll();
|
||||
}
|
||||
|
||||
using Descriptor = uint64_t;
|
||||
|
||||
static inline volatile Descriptor* TableAt(uint64_t phys) {
|
||||
return (volatile Descriptor*)Memory::HHDM(phys);
|
||||
}
|
||||
|
||||
static inline uint64_t Index(uint64_t va, int level) {
|
||||
return (va >> (39 - 9 * level)) & 0x1FF;
|
||||
}
|
||||
|
||||
// Order descriptor writes before the table walker (and later
|
||||
// instructions) can observe them.
|
||||
static inline void PublishTables() {
|
||||
asm volatile("dsb ishst\n\tisb" ::: "memory");
|
||||
}
|
||||
|
||||
static inline uint64_t ReadTtbr1() {
|
||||
uint64_t ttbr;
|
||||
asm volatile("mrs %0, ttbr1_el1" : "=r"(ttbr));
|
||||
return ttbr & Arch::TtbrAddressMask;
|
||||
}
|
||||
|
||||
// The level-3 descriptor for `va`, creating intermediate tables.
|
||||
// Returns nullptr when out of memory or when a block mapping covers
|
||||
// `va` (only the bootloader's tables contain blocks).
|
||||
static volatile Descriptor* WalkCreate(uint64_t root, uint64_t va) {
|
||||
uint64_t table = root;
|
||||
for (int level = 0; level < 3; level++) {
|
||||
volatile Descriptor* entry = &TableAt(table)[Index(va, level)];
|
||||
Descriptor desc = *entry;
|
||||
if ((desc & DescValid) == 0) {
|
||||
void* page = Memory::g_pfa->AllocateZeroed();
|
||||
if (page == nullptr) return nullptr;
|
||||
table = Memory::SubHHDM(page);
|
||||
// The zeroed table must be visible before the walker can
|
||||
// reach it.
|
||||
asm volatile("dsb ishst" ::: "memory");
|
||||
*entry = table | DescTable | DescValid;
|
||||
} else if ((desc & DescTable) == 0) {
|
||||
return nullptr;
|
||||
} else {
|
||||
table = desc & DescAddrMask;
|
||||
}
|
||||
}
|
||||
return &TableAt(table)[Index(va, 3)];
|
||||
}
|
||||
|
||||
// The level-3 descriptor for `va`, or nullptr if a table is missing.
|
||||
static volatile Descriptor* WalkExisting(uint64_t root, uint64_t va) {
|
||||
uint64_t table = root;
|
||||
for (int level = 0; level < 3; level++) {
|
||||
Descriptor desc = TableAt(table)[Index(va, level)];
|
||||
if ((desc & (DescValid | DescTable)) != (DescValid | DescTable)) return nullptr;
|
||||
table = desc & DescAddrMask;
|
||||
}
|
||||
return &TableAt(table)[Index(va, 3)];
|
||||
}
|
||||
|
||||
// Install a leaf. Replacing a live translation (other address or
|
||||
// attributes) needs break-before-make: invalidate, flush, then write.
|
||||
static void SetLeaf(volatile Descriptor* entry, Descriptor desc, uint64_t va) {
|
||||
Descriptor old = *entry;
|
||||
if (old == desc) return;
|
||||
if (old & DescValid) {
|
||||
*entry = 0;
|
||||
Arch::FlushTlbPage(va);
|
||||
}
|
||||
*entry = desc;
|
||||
PublishTables();
|
||||
}
|
||||
|
||||
static Descriptor KernelLeaf(uint64_t phys, uint64_t attr) {
|
||||
Descriptor desc = (phys & DescAddrMask) | DescTable | DescValid | DescAf | DescShInner
|
||||
| (attr << DescAttrShift) | DescUxn;
|
||||
// Kernel memory stays executable, as on x86; device memory never is.
|
||||
if (attr != AttrNormal) desc |= DescPxn;
|
||||
return desc;
|
||||
}
|
||||
|
||||
static Descriptor UserLeaf(uint64_t phys, uint64_t attr, bool writable, bool executable) {
|
||||
Descriptor desc = (phys & DescAddrMask) | DescTable | DescValid | DescAf | DescShInner
|
||||
| DescNotGlobal | (attr << DescAttrShift) | DescApUser | DescPxn;
|
||||
if (!writable) desc |= DescApReadOnly;
|
||||
if (!executable) desc |= DescUxn;
|
||||
return desc;
|
||||
}
|
||||
|
||||
static inline bool IsUserLeaf(Descriptor desc) {
|
||||
return (desc & (DescValid | DescTable)) == (DescValid | DescTable) && (desc & DescApUser);
|
||||
}
|
||||
|
||||
// Translate through a table tree that may contain block mappings (the
|
||||
// bootloader's). Returns 0 if unmapped.
|
||||
static uint64_t Translate(uint64_t root, uint64_t va) {
|
||||
uint64_t table = root;
|
||||
for (int level = 0; level <= 3; level++) {
|
||||
Descriptor desc = TableAt(table)[Index(va, level)];
|
||||
if ((desc & DescValid) == 0) return 0;
|
||||
|
||||
uint64_t span = 1ULL << (39 - 9 * level);
|
||||
bool leaf = level == 3 || (desc & DescTable) == 0;
|
||||
if (leaf) {
|
||||
if (level == 0) return 0; // no level-0 blocks with 4 KiB granules
|
||||
return ((desc & DescAddrMask) & ~(span - 1)) | (va & (span - 1));
|
||||
}
|
||||
table = desc & DescAddrMask;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Paging::MapEfiRuntime(const montauk::boot::EfiInfo& efi) {
|
||||
if (efi.memoryMap == nullptr || efi.descriptorSize == 0) return;
|
||||
|
||||
struct EfiMemDesc {
|
||||
uint32_t Type;
|
||||
uint64_t PhysicalStart;
|
||||
uint64_t VirtualStart;
|
||||
uint64_t NumberOfPages;
|
||||
uint64_t Attribute;
|
||||
};
|
||||
|
||||
static constexpr uint64_t EFI_MEMORY_RUNTIME = 0x8000000000000000ULL;
|
||||
|
||||
auto* base = (uint8_t*)efi.memoryMap;
|
||||
uint64_t count = efi.memoryMapSize / efi.descriptorSize;
|
||||
|
||||
for (uint64_t i = 0; i < count; i++) {
|
||||
auto* desc = (EfiMemDesc*)(base + i * efi.descriptorSize);
|
||||
if ((desc->Attribute & EFI_MEMORY_RUNTIME) == 0) continue;
|
||||
|
||||
bool device = desc->Type == EfiMemoryMappedIo || desc->Type == EfiMemoryMappedIoPortSpace;
|
||||
for (uint64_t p = 0; p < desc->NumberOfPages; p++) {
|
||||
uint64_t addr = desc->PhysicalStart + p * 0x1000;
|
||||
// Firmware code runs at its HHDM alias (Efi::Init calls the
|
||||
// services there) and reaches its data at physical addresses.
|
||||
// Runtime regions are reserved memory, outside the HHDM.
|
||||
if (device) {
|
||||
MapMMIO(addr, addr);
|
||||
MapMMIO(addr, Memory::HHDM(addr));
|
||||
} else {
|
||||
Map(addr, addr);
|
||||
Map(addr, Memory::HHDM(addr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlushTLB();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// The new tables map the code running here at the same address as the
|
||||
// bootloader's did, so the switch is a register write followed by a
|
||||
// full invalidate.
|
||||
void ActivateKernelTranslation() {
|
||||
using namespace Memory::VMM;
|
||||
|
||||
// 48-bit VAs in both halves, 4 KiB granules, write-back walks of
|
||||
// inner-shareable tables; the physical size is what the CPU has.
|
||||
uint64_t mmfr0;
|
||||
asm volatile("mrs %0, id_aa64mmfr0_el1" : "=r"(mmfr0));
|
||||
uint64_t ips = mmfr0 & 0xF;
|
||||
if (ips > 5) ips = 5; // 52-bit output needs LPA2 descriptors
|
||||
uint64_t tcr = 16ULL // T0SZ
|
||||
| 1ULL << 8 | 1ULL << 10 // IRGN0, ORGN0: write-back
|
||||
| 3ULL << 12 // SH0: inner shareable
|
||||
| 0ULL << 14 // TG0: 4 KiB
|
||||
| 16ULL << 16 // T1SZ
|
||||
| 1ULL << 24 | 1ULL << 26 // IRGN1, ORGN1
|
||||
| 3ULL << 28 // SH1
|
||||
| 2ULL << 30 // TG1: 4 KiB
|
||||
| ips << 32; // IPS
|
||||
|
||||
asm volatile(
|
||||
"dsb ish\n\t"
|
||||
"msr mair_el1, %0\n\t"
|
||||
"msr tcr_el1, %1\n\t"
|
||||
"isb\n\t"
|
||||
"msr ttbr1_el1, %2\n\t"
|
||||
"msr ttbr0_el1, %3\n\t"
|
||||
"isb\n\t"
|
||||
"tlbi vmalle1\n\t"
|
||||
"dsb nsh\n\t"
|
||||
"isb"
|
||||
:: "r"(MairValue), "r"(tcr), "r"(g_kernelHalfRoot), "r"(g_paging->KernelRoot()) : "memory");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Memory::VMM {
|
||||
|
||||
Paging::Paging() {
|
||||
rootPhys = Memory::SubHHDM(Memory::g_pfa->AllocateZeroed());
|
||||
g_kernelHalfRoot = Memory::SubHHDM(Memory::g_pfa->AllocateZeroed());
|
||||
}
|
||||
|
||||
// Kernel mappings of lower-half addresses (EFI identity mappings) go to
|
||||
// the kernel contexts' TTBR0 table.
|
||||
static uint64_t KernelRootFor(uint64_t kernelContextRoot, uint64_t va) {
|
||||
return va >= KernelHalfBase ? g_kernelHalfRoot : kernelContextRoot;
|
||||
}
|
||||
|
||||
void Paging::Init(std::uint64_t kernelBaseVirt, std::uint64_t kernelSize, const montauk::boot::MemoryMap& memMap,
|
||||
const montauk::boot::Framebuffer& framebuffer) {
|
||||
if (Memory::HHDMBase < KernelHalfBase) {
|
||||
Panic("aarch64: HHDM outside the 48-bit kernel half", nullptr);
|
||||
}
|
||||
|
||||
uint64_t bootTtbr1 = ReadTtbr1();
|
||||
uint64_t bootTcr, bootMair;
|
||||
asm volatile("mrs %0, tcr_el1" : "=r"(bootTcr));
|
||||
asm volatile("mrs %0, mair_el1" : "=r"(bootMair));
|
||||
Kt::KernelLogStream(Kt::DEBUG, "VMM") << "Bootloader TCR 0x" << base::hex << bootTcr
|
||||
<< ", MAIR 0x" << bootMair << ", TTBR1 0x" << bootTtbr1;
|
||||
|
||||
// The kernel image, at the physical pages the bootloader loaded it to.
|
||||
for (uint64_t va = kernelBaseVirt; va < kernelBaseVirt + kernelSize; va += 0x1000) {
|
||||
uint64_t phys = Translate(bootTtbr1, va);
|
||||
if (phys == 0) Panic("aarch64: kernel page missing from the bootloader's tables", nullptr);
|
||||
Map(phys, va);
|
||||
}
|
||||
|
||||
// HHDM: every region that is real backing store (see the top of
|
||||
// this file). The limit is published for the AML interpreter.
|
||||
uint64_t maxPhysAddr = 0;
|
||||
for (size_t i = 0; i < memMap.count; i++) {
|
||||
const auto& region = memMap.regions[i];
|
||||
if (region.kind == montauk::boot::MemoryKind::Reserved
|
||||
|| region.kind == montauk::boot::MemoryKind::Unknown) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint64_t start = region.base & ~0xFFFULL;
|
||||
uint64_t end = (region.base + region.length + 0xFFF) & ~0xFFFULL;
|
||||
bool framebufferRegion = region.kind == montauk::boot::MemoryKind::Framebuffer;
|
||||
for (uint64_t phys = start; phys < end; phys += 0x1000) {
|
||||
if (framebufferRegion) MapWC(phys, Memory::HHDM(phys));
|
||||
else Map(phys, Memory::HHDM(phys));
|
||||
}
|
||||
if (end > maxPhysAddr) maxPhysAddr = end;
|
||||
}
|
||||
g_hhdmLimit = maxPhysAddr;
|
||||
|
||||
// The framebuffer may not be listed in the memory map at all.
|
||||
if (framebuffer.address != 0) {
|
||||
uint64_t fbPhys = Memory::SubHHDM(framebuffer.address) & ~0xFFFULL;
|
||||
uint64_t fbPages = (framebuffer.height * framebuffer.pitch + 0xFFF) / 0x1000;
|
||||
for (uint64_t p = 0; p < fbPages; p++) {
|
||||
uint64_t phys = fbPhys + p * 0x1000;
|
||||
MapWC(phys, Memory::HHDM(phys));
|
||||
}
|
||||
}
|
||||
|
||||
// The bootloader wrote the image through the data cache. Push it to
|
||||
// the point of unification and drop every instruction-cache line
|
||||
// before the kernel relies on fetching code it has not run yet: a
|
||||
// line still dirty in L1D is invisible to instruction fetch, which
|
||||
// then reads stale DRAM (an "unknown reason" exception on a valid
|
||||
// instruction).
|
||||
Arch::SyncInstructionCache((const void*)kernelBaseVirt, kernelSize);
|
||||
|
||||
Arch::ActivateKernelTranslation();
|
||||
|
||||
Kt::KernelLogStream(Kt::OK, "VMM") << "Switched to kernel translation tables (HHDM limit 0x"
|
||||
<< base::hex << g_hhdmLimit << ")";
|
||||
}
|
||||
|
||||
static void MapKernelPage(uint64_t root, uint64_t phys, uint64_t va, uint64_t attr, const char* what) {
|
||||
if (va % 0x1000 != 0 || phys % 0x1000 != 0) {
|
||||
Panic(what, nullptr);
|
||||
}
|
||||
volatile Descriptor* entry = WalkCreate(KernelRootFor(root, va), va);
|
||||
if (entry == nullptr) {
|
||||
Panic("OOM in Paging (kernel page table allocation)", nullptr);
|
||||
}
|
||||
SetLeaf(entry, KernelLeaf(phys, attr), va);
|
||||
}
|
||||
|
||||
void Paging::Map(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||
MapKernelPage(rootPhys, physicalAddress, virtualAddress, AttrNormal,
|
||||
"Value that isn't page-aligned passed as address to Paging::Map!");
|
||||
}
|
||||
|
||||
void Paging::MapMMIO(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||
MapKernelPage(rootPhys, physicalAddress, virtualAddress, AttrDevice,
|
||||
"Value that isn't page-aligned passed as address to Paging::MapMMIO!");
|
||||
}
|
||||
|
||||
void Paging::MapWC(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||
MapKernelPage(rootPhys, physicalAddress, virtualAddress, AttrNormalNc,
|
||||
"Value that isn't page-aligned passed as address to Paging::MapWC!");
|
||||
}
|
||||
|
||||
void Paging::MapUser(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||
if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) {
|
||||
Panic("Value that isn't page-aligned passed as address to Paging::MapUser!", nullptr);
|
||||
}
|
||||
volatile Descriptor* entry = WalkCreate(rootPhys, virtualAddress);
|
||||
if (entry == nullptr) {
|
||||
Panic("OOM in Paging::MapUser (page table allocation)", nullptr);
|
||||
}
|
||||
SetLeaf(entry, UserLeaf(physicalAddress, AttrNormal, true, true), virtualAddress);
|
||||
}
|
||||
|
||||
std::uint64_t Paging::GetPhysAddr(std::uint64_t root, std::uint64_t virtualAddress) {
|
||||
if (virtualAddress >= KernelHalfBase) {
|
||||
root = g_kernelHalfRoot;
|
||||
}
|
||||
return Translate(root, virtualAddress);
|
||||
}
|
||||
|
||||
std::uint64_t Paging::GetPhysAddr(std::uint64_t virtualAddress) {
|
||||
return GetPhysAddr(rootPhys, virtualAddress);
|
||||
}
|
||||
|
||||
// The kernel half lives in TTBR1, so a new address space starts empty.
|
||||
std::uint64_t Paging::CreateUserAddressSpace() {
|
||||
void* page = Memory::g_pfa->AllocateZeroed();
|
||||
if (page == nullptr) return 0;
|
||||
return Memory::SubHHDM(page);
|
||||
}
|
||||
|
||||
bool Paging::MapUserIn(std::uint64_t root, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||
return MapUserInPermissions(root, physicalAddress, virtualAddress, true, true);
|
||||
}
|
||||
|
||||
static bool MapUserLeaf(uint64_t root, uint64_t phys, uint64_t va, Descriptor desc, const char* what) {
|
||||
pagingLock.Acquire();
|
||||
if (va % 0x1000 != 0 || phys % 0x1000 != 0) {
|
||||
pagingLock.Release();
|
||||
Panic(what, nullptr);
|
||||
}
|
||||
volatile Descriptor* entry = WalkCreate(root, va);
|
||||
if (entry == nullptr) {
|
||||
pagingLock.Release();
|
||||
return false;
|
||||
}
|
||||
SetLeaf(entry, desc, va);
|
||||
pagingLock.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Paging::MapUserInPermissions(std::uint64_t root, std::uint64_t physicalAddress,
|
||||
std::uint64_t virtualAddress, bool writable, bool executable) {
|
||||
return MapUserLeaf(root, physicalAddress, virtualAddress,
|
||||
UserLeaf(physicalAddress, AttrNormal, writable, executable),
|
||||
"Non-aligned address in Paging::MapUserIn!");
|
||||
}
|
||||
|
||||
bool Paging::MapUserInWC(std::uint64_t root, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||
return MapUserLeaf(root, physicalAddress, virtualAddress,
|
||||
UserLeaf(physicalAddress, AttrNormalNc, true, false),
|
||||
"Non-aligned address in Paging::MapUserInWC!");
|
||||
}
|
||||
|
||||
// Permission changes need no break-before-make, only the TLB flush.
|
||||
bool Paging::ProtectUserIn(std::uint64_t root, std::uint64_t virtualAddress,
|
||||
bool writable, bool executable) {
|
||||
if ((virtualAddress & 0xFFFULL) != 0) return false;
|
||||
pagingLock.Acquire();
|
||||
volatile Descriptor* entry = WalkExisting(root, virtualAddress);
|
||||
if (entry == nullptr || !IsUserLeaf(*entry)) {
|
||||
pagingLock.Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
Descriptor desc = *entry;
|
||||
if (writable) desc &= ~DescApReadOnly;
|
||||
else desc |= DescApReadOnly;
|
||||
if (executable) desc &= ~DescUxn;
|
||||
else desc |= DescUxn;
|
||||
*entry = desc;
|
||||
Arch::FlushTlbPage(virtualAddress);
|
||||
pagingLock.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Paging::UnmapUserIn(std::uint64_t root, std::uint64_t virtualAddress) {
|
||||
pagingLock.Acquire();
|
||||
volatile Descriptor* entry = WalkExisting(root, virtualAddress);
|
||||
if (entry != nullptr && (*entry & DescValid)) {
|
||||
*entry = 0;
|
||||
Arch::FlushTlbPage(virtualAddress);
|
||||
}
|
||||
pagingLock.Release();
|
||||
}
|
||||
|
||||
void Paging::FreeUserHalf(std::uint64_t root) {
|
||||
// User space is the lower 128 TiB: level-0 entries 0-255.
|
||||
for (int i0 = 0; i0 < 256; i0++) {
|
||||
Descriptor d0 = TableAt(root)[i0];
|
||||
if ((d0 & DescValid) == 0) continue;
|
||||
uint64_t l1 = d0 & DescAddrMask;
|
||||
|
||||
for (int i1 = 0; i1 < 512; i1++) {
|
||||
Descriptor d1 = TableAt(l1)[i1];
|
||||
if ((d1 & DescValid) == 0) continue;
|
||||
uint64_t l2 = d1 & DescAddrMask;
|
||||
|
||||
for (int i2 = 0; i2 < 512; i2++) {
|
||||
Descriptor d2 = TableAt(l2)[i2];
|
||||
if ((d2 & DescValid) == 0) continue;
|
||||
uint64_t l3 = d2 & DescAddrMask;
|
||||
|
||||
for (int i3 = 0; i3 < 512; i3++) {
|
||||
Descriptor leaf = TableAt(l3)[i3];
|
||||
if ((leaf & DescValid) == 0) continue;
|
||||
|
||||
// Write-combining/device pages are not the process's.
|
||||
if (((leaf & DescAttrMask) >> DescAttrShift) != AttrNormal) continue;
|
||||
|
||||
uint64_t pagePhys = leaf & DescAddrMask;
|
||||
if (pagePhys != 0) {
|
||||
Memory::g_pfa->Free((void*)Memory::HHDM(pagePhys));
|
||||
}
|
||||
}
|
||||
|
||||
Memory::g_pfa->Free((void*)Memory::HHDM(l3));
|
||||
}
|
||||
|
||||
Memory::g_pfa->Free((void*)Memory::HHDM(l2));
|
||||
}
|
||||
|
||||
Memory::g_pfa->Free((void*)Memory::HHDM(l1));
|
||||
}
|
||||
}
|
||||
|
||||
bool Paging::IsUserRangeAccessible(std::uint64_t root, std::uint64_t virtualAddress,
|
||||
std::uint64_t size, bool requireWrite) {
|
||||
if (size == 0) return true;
|
||||
if (virtualAddress >= UserSpaceEnd) return false;
|
||||
if (virtualAddress + size < virtualAddress) return false;
|
||||
if (virtualAddress + size > UserSpaceEnd) return false;
|
||||
|
||||
uint64_t page = virtualAddress & ~0xFFFULL;
|
||||
uint64_t lastPage = (virtualAddress + size - 1) & ~0xFFFULL;
|
||||
for (;;) {
|
||||
volatile Descriptor* entry = WalkExisting(root, page);
|
||||
if (entry == nullptr) return false;
|
||||
Descriptor desc = *entry;
|
||||
if (!IsUserLeaf(desc)) return false;
|
||||
if (requireWrite && (desc & DescApReadOnly)) return false;
|
||||
if (page == lastPage) break;
|
||||
page += 0x1000;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* PanicFrame.cpp
|
||||
* aarch64 CPU state for the panic screen
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Common/Panic.hpp>
|
||||
#include <Arch/aarch64/InterruptFrame.hpp>
|
||||
#include <CppLib/BoxUI.hpp>
|
||||
|
||||
static constexpr int BoxWidth = PanicBoxWidth;
|
||||
|
||||
namespace Arch {
|
||||
void PrintPanicFrame(PanicFrame* frame) {
|
||||
PrintBoxedSeparator(kerr, BoxWidth);
|
||||
PrintBoxedLine(kerr, "CPU State:", BoxWidth, true);
|
||||
PrintBoxedHex(kerr, "Exception Class (ESR)", frame->esr >> 26, BoxWidth);
|
||||
PrintBoxedHex(kerr, "Syndrome (ESR)", frame->esr, BoxWidth);
|
||||
PrintBoxedHex(kerr, "Fault Address (FAR)", frame->far, BoxWidth);
|
||||
|
||||
PrintBoxedSeparator(kerr, BoxWidth);
|
||||
PrintBoxedLine(kerr, "Registers:", BoxWidth, true);
|
||||
PrintBoxedHex(kerr, "Program Counter (ELR)", frame->elr, BoxWidth);
|
||||
PrintBoxedHex(kerr, "PSTATE (SPSR)", frame->spsr, BoxWidth);
|
||||
PrintBoxedHex(kerr, "Stack Pointer", frame->sp, BoxWidth);
|
||||
PrintBoxedHex(kerr, "Link Register (x30)", frame->x[30], BoxWidth);
|
||||
|
||||
// An "unknown reason" exception on a kernel PC: show the word the
|
||||
// data side sees there. A valid instruction means instruction fetch
|
||||
// saw something else (cache maintenance); garbage means the code
|
||||
// itself was overwritten. The fetch succeeded, so the page is mapped.
|
||||
if ((frame->esr >> 26) == 0 && frame->elr >= 0xFFFF000000000000ULL && (frame->elr & 3) == 0) {
|
||||
PrintBoxedHex(kerr, "Instruction at PC", *(volatile uint32_t*)frame->elr, BoxWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* PerCpu.hpp
|
||||
* aarch64 per-CPU state (see Arch/Smp.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// Read by the exception vectors through TPIDR_EL1.
|
||||
struct CpuEntryState {
|
||||
uint64_t kernelStackTop; // SP_EL1 to use on entry from EL0
|
||||
uint64_t scratch; // vector-private scratch slot
|
||||
};
|
||||
|
||||
struct CpuState {
|
||||
uint64_t mpidr; // MPIDR_EL1 affinity of this CPU
|
||||
uint8_t gicTargetMask; // GICv2 CPU-interface bit (SGI/SPI targets)
|
||||
uint64_t timerPeriod; // tick period in counter ticks, 0 = stopped
|
||||
};
|
||||
|
||||
// TPIDR_EL1 holds the CpuData pointer (zeroed in EarlyInitialize until
|
||||
// Smp::InitBsp installs it).
|
||||
inline void* CurrentCpuLocal() {
|
||||
void* ptr;
|
||||
asm volatile("mrs %0, tpidr_el1" : "=r"(ptr));
|
||||
return ptr;
|
||||
}
|
||||
|
||||
inline void* TryCurrentCpuLocal() {
|
||||
return CurrentCpuLocal();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Platform.cpp
|
||||
* aarch64 bring-up hooks (implements Arch/Platform.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/Platform.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/Smp.hpp>
|
||||
#include <Arch/aarch64/Gic.hpp>
|
||||
#include <Arch/aarch64/Bringup.hpp>
|
||||
#include <ACPI/FADT.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
extern "C" char ArchExceptionVectors[]; // Vectors.S
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// SCTLR_EL1.WXN: writable memory is execute-never
|
||||
static constexpr uint64_t SctlrWxn = 1ULL << 19;
|
||||
// SCTLR_EL1.UCT/UCI: EL0 may read CTR_EL0 and clean/invalidate caches
|
||||
// by address (what __builtin___clear_cache needs for generated code).
|
||||
static constexpr uint64_t SctlrUct = 1ULL << 15;
|
||||
static constexpr uint64_t SctlrUci = 1ULL << 26;
|
||||
|
||||
// PSCI 0.2+ function IDs (SMC32 calling convention)
|
||||
static constexpr uint64_t PsciSystemOff = 0x84000008;
|
||||
static constexpr uint64_t PsciSystemReset = 0x84000009;
|
||||
|
||||
// How the firmware takes PSCI calls, from the FADT's ARM_BOOT_ARCH.
|
||||
enum class PsciConduit { None, Smc, Hvc };
|
||||
static PsciConduit g_psci = PsciConduit::None;
|
||||
|
||||
static uint64_t PsciCall(uint64_t function) {
|
||||
register uint64_t x0 asm("x0") = function;
|
||||
register uint64_t x1 asm("x1") = 0;
|
||||
register uint64_t x2 asm("x2") = 0;
|
||||
register uint64_t x3 asm("x3") = 0;
|
||||
if (g_psci == PsciConduit::Hvc) {
|
||||
asm volatile("hvc #0" : "+r"(x0), "+r"(x1), "+r"(x2), "+r"(x3) :: "memory");
|
||||
} else if (g_psci == PsciConduit::Smc) {
|
||||
asm volatile("smc #0" : "+r"(x0), "+r"(x1), "+r"(x2), "+r"(x3) :: "memory");
|
||||
}
|
||||
return x0;
|
||||
}
|
||||
|
||||
static void InitializePsci(Hal::ACPI::CommonSDTHeader* xsdt) {
|
||||
Hal::FADT::ParsedFADT fadt{};
|
||||
if (!Hal::FADT::Parse(xsdt, fadt) || !fadt.Valid) return;
|
||||
if (!(fadt.ArmBootArchFlags & Hal::FADT::ArmBootPsciCompliant)) {
|
||||
KernelLogStream(WARNING, "PSCI") << "Firmware is not PSCI compliant: no reset or power-off";
|
||||
return;
|
||||
}
|
||||
g_psci = (fadt.ArmBootArchFlags & Hal::FADT::ArmBootPsciUseHvc) ? PsciConduit::Hvc : PsciConduit::Smc;
|
||||
KernelLogStream(OK, "PSCI") << "PSCI through " << (g_psci == PsciConduit::Hvc ? "HVC" : "SMC");
|
||||
}
|
||||
|
||||
void InitializeCpuRegisters() {
|
||||
// Run on SP_EL1 so exceptions taken from the kernel arrive on the
|
||||
// stack it is already using (Vectors.S has no SP_EL0 handlers).
|
||||
asm volatile("mov x9, sp\n\tmsr spsel, #1\n\tmov sp, x9" ::: "x9", "memory");
|
||||
|
||||
// The vectors need no allocation, so take over from the
|
||||
// bootloader's right away.
|
||||
asm volatile("msr vbar_el1, %0\n\tisb" :: "r"(ArchExceptionVectors) : "memory");
|
||||
|
||||
// Kernel mappings are RWX (Paging.cpp), so writable must not
|
||||
// imply execute-never; user code may maintain its own caches.
|
||||
uint64_t sctlr;
|
||||
asm volatile("mrs %0, sctlr_el1" : "=r"(sctlr));
|
||||
sctlr &= ~SctlrWxn;
|
||||
sctlr |= SctlrUct | SctlrUci;
|
||||
asm volatile("msr sctlr_el1, %0\n\tisb" :: "r"(sctlr));
|
||||
|
||||
// CPACR_EL1.FPEN = 0b11: FP/SIMD usable at EL0 and EL1 (the kernel
|
||||
// itself is built -mgeneral-regs-only; Context.S saves user state).
|
||||
uint64_t cpacr;
|
||||
asm volatile("mrs %0, cpacr_el1" : "=r"(cpacr));
|
||||
cpacr |= 3ULL << 20;
|
||||
asm volatile("msr cpacr_el1, %0\n\tisb" :: "r"(cpacr));
|
||||
}
|
||||
|
||||
void EarlyInitialize() {
|
||||
// No per-CPU data yet: Smp::TryGetCurrentCpuData must see null.
|
||||
asm volatile("msr tpidr_el1, xzr");
|
||||
|
||||
InitializeCpuRegisters();
|
||||
}
|
||||
|
||||
// Installed by EarlyInitialize.
|
||||
void InitializeTraps() {
|
||||
KernelLogStream(OK, "Arch") << "Installed exception vectors at 0x" << base::hex
|
||||
<< (uint64_t)ArchExceptionVectors;
|
||||
}
|
||||
|
||||
// MAIR_EL1 attribute indices are chosen together with the page-table
|
||||
// format (Paging.cpp), so there is nothing to program separately.
|
||||
void InitializeMemoryAttributes() {
|
||||
}
|
||||
|
||||
void InitializeInterrupts(Hal::ACPI::CommonSDTHeader* xsdt) {
|
||||
Smp::InitBsp();
|
||||
if (!Gic::Initialize(xsdt)) {
|
||||
KernelLogStream(ERROR, "Arch") << "aarch64: no interrupt controller, running without IRQs";
|
||||
}
|
||||
InitializePsci(xsdt);
|
||||
}
|
||||
|
||||
void InitializePlatformDevices() {
|
||||
}
|
||||
|
||||
// EL0 entry needs no global state beyond Arch::ActivateThread.
|
||||
void PrepareUserMode() {
|
||||
}
|
||||
|
||||
void LateInitialize() {
|
||||
}
|
||||
|
||||
void ListPlatformDevices(PlatformDeviceSink sink, void* context) {
|
||||
if (Gic::IsInitialized()) {
|
||||
sink(context, 1, "GICv2", "ARM Generic Interrupt Controller");
|
||||
sink(context, 2, "Generic Timer", "ARM architected timer (EL1 virtual)");
|
||||
}
|
||||
}
|
||||
|
||||
void Reboot() {
|
||||
PsciCall(PsciSystemReset);
|
||||
HaltForever();
|
||||
}
|
||||
|
||||
void PowerOff() {
|
||||
if (g_psci != PsciConduit::None) {
|
||||
PsciCall(PsciSystemOff);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Smp.cpp
|
||||
* aarch64 per-CPU data and secondary CPU start-up (implements the arch half of Arch/Smp.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/Smp.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/Syscall.hpp>
|
||||
#include <Arch/aarch64/Bringup.hpp>
|
||||
#include <Arch/aarch64/Gic.hpp>
|
||||
#include <Boot/BootInfo.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Smp {
|
||||
|
||||
static CpuData g_cpus[MaxCPUs];
|
||||
static int g_cpuCount = 0;
|
||||
static int g_detectedCpuCount = 1;
|
||||
|
||||
CpuData* GetCpuData(int index) {
|
||||
if (index < 0 || index >= g_cpuCount) return nullptr;
|
||||
return &g_cpus[index];
|
||||
}
|
||||
|
||||
int GetCpuCount() {
|
||||
return g_cpuCount;
|
||||
}
|
||||
|
||||
int GetDetectedCpuCount() {
|
||||
return g_detectedCpuCount;
|
||||
}
|
||||
|
||||
void InitBsp() {
|
||||
memset(g_cpus, 0, sizeof(g_cpus));
|
||||
|
||||
CpuData& bsp = g_cpus[0];
|
||||
bsp.selfPtr = (uint64_t)&bsp;
|
||||
bsp.cpuIndex = 0;
|
||||
bsp.currentSlot = -1;
|
||||
bsp.started = true;
|
||||
asm volatile("mrs %0, mpidr_el1" : "=r"(bsp.arch.mpidr));
|
||||
|
||||
asm volatile("msr tpidr_el1, %0" :: "r"(&bsp));
|
||||
g_cpuCount = 1;
|
||||
|
||||
KernelLogStream(OK, "SMP") << "BSP initialized (MPIDR " << base::hex
|
||||
<< bsp.arch.mpidr << ")";
|
||||
}
|
||||
|
||||
// A secondary CPU, woken by the bootloader on its own stack and still
|
||||
// on the bootloader's translation tables. The kernel stashed this CPU's
|
||||
// CpuData in extraArgument.
|
||||
static void ApEntry(montauk::boot::BootCpu* bootCpu) {
|
||||
CpuData* cpu = (CpuData*)bootCpu->extraArgument;
|
||||
|
||||
Arch::InitializeCpuRegisters();
|
||||
Arch::ActivateKernelTranslation();
|
||||
asm volatile("msr tpidr_el1, %0" :: "r"(cpu));
|
||||
|
||||
Arch::Gic::InitializeCpuInterface();
|
||||
Arch::InitializeSyscallEntry();
|
||||
Timekeeping::TimerInitializeAP();
|
||||
|
||||
__atomic_store_n(&cpu->started, true, __ATOMIC_RELEASE);
|
||||
Arch::EnableInterrupts();
|
||||
|
||||
// Runnable work arrives as a reschedule IPI; keep the periodic tick
|
||||
// masked while idle.
|
||||
for (;;) {
|
||||
Timekeeping::TimerEnterApIdle();
|
||||
Timekeeping::ServiceDeferredWork();
|
||||
Arch::DisableInterrupts();
|
||||
Arch::HaltWithInterruptsDisabled();
|
||||
Arch::EnableInterrupts();
|
||||
}
|
||||
}
|
||||
|
||||
void BootAPs(const montauk::boot::SmpInfo& smp) {
|
||||
if (smp.cpus == nullptr || smp.startCpu == nullptr || smp.cpuCount <= 1) {
|
||||
KernelLogStream(INFO, "SMP") << "Single CPU system - no secondary CPUs to start";
|
||||
return;
|
||||
}
|
||||
g_detectedCpuCount = (int)smp.cpuCount;
|
||||
|
||||
if (!Arch::Gic::IsInitialized()) {
|
||||
KernelLogStream(WARNING, "SMP") << "No interrupt controller: secondary CPUs left parked";
|
||||
return;
|
||||
}
|
||||
|
||||
int apIndex = 1;
|
||||
for (uint64_t i = 0; i < smp.cpuCount && apIndex < MaxCPUs; i++) {
|
||||
montauk::boot::BootCpu& info = smp.cpus[i];
|
||||
if (info.hardwareId == smp.bspHardwareId) continue;
|
||||
|
||||
CpuData& ap = g_cpus[apIndex];
|
||||
ap.selfPtr = (uint64_t)≈
|
||||
ap.cpuIndex = apIndex;
|
||||
ap.currentSlot = -1;
|
||||
ap.started = false;
|
||||
ap.arch.mpidr = info.hardwareId;
|
||||
|
||||
info.extraArgument = (uint64_t)≈
|
||||
// Publish the CpuData before the CPU that reads it is woken.
|
||||
asm volatile("dsb ish" ::: "memory");
|
||||
smp.startCpu(&info, ApEntry);
|
||||
apIndex++;
|
||||
}
|
||||
g_cpuCount = apIndex;
|
||||
|
||||
int online = 1;
|
||||
for (int i = 1; i < g_cpuCount; i++) {
|
||||
uint64_t start = Arch::ReadTimestamp();
|
||||
uint64_t frequency;
|
||||
asm volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
|
||||
while (!__atomic_load_n(&g_cpus[i].started, __ATOMIC_ACQUIRE) &&
|
||||
Arch::ReadTimestamp() - start < frequency) {
|
||||
Arch::Pause();
|
||||
}
|
||||
if (g_cpus[i].started) {
|
||||
online++;
|
||||
} else {
|
||||
KernelLogStream(ERROR, "SMP") << "CPU " << base::dec << (uint64_t)i
|
||||
<< " (MPIDR " << base::hex << g_cpus[i].arch.mpidr << ") failed to start";
|
||||
}
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "SMP") << base::dec << (uint64_t)online << " CPU(s) online";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Syscall.cpp
|
||||
* aarch64 system-call entry (implements Arch/Syscall.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/Syscall.hpp>
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// `svc #0` arrives through the EL0 synchronous vector installed by
|
||||
// InitializeTraps; there is no per-CPU register to program.
|
||||
void InitializeSyscallEntry() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SyscallFrame.hpp
|
||||
* aarch64 system-call frame (see Arch/Syscall.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Arch {
|
||||
|
||||
// The exception frame Vectors.S saves for `svc #0` (the same layout as
|
||||
// Arch::PanicFrame): x0-x5 carry the arguments and x8 the system-call
|
||||
// number; the result is written back to x0.
|
||||
struct SyscallFrame {
|
||||
uint64_t arg1, arg2, arg3, arg4, arg5, arg6; // x0-x5
|
||||
uint64_t x6, x7;
|
||||
uint64_t syscall_nr; // x8
|
||||
uint64_t x9_x30[22];
|
||||
uint64_t sp_el0, elr, spsr, esr, far;
|
||||
};
|
||||
|
||||
static_assert(sizeof(SyscallFrame) == 288, "must match Vectors.S");
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Timer.cpp
|
||||
* aarch64 tick timer: the EL1 virtual generic timer (implements Arch/Timer.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/Timer.hpp>
|
||||
#include <Arch/Interrupts.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/Smp.hpp>
|
||||
#include <Arch/aarch64/Gic.hpp>
|
||||
#include <ACPI/ACPI.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
// The generic timer counts at CNTFRQ_EL0 on every CPU, so it needs no
|
||||
// calibration. Each CPU's virtual timer raises a PPI (the GTDT names it,
|
||||
// normally INTID 27); the tick is re-armed from the interrupt's ack hook,
|
||||
// which also drops the level-triggered line before the EOI.
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Arch::Timer {
|
||||
|
||||
// CNTV_CTL_EL0
|
||||
static constexpr uint64_t CtlEnable = 1ULL << 0;
|
||||
static constexpr uint64_t CtlMask = 1ULL << 1;
|
||||
|
||||
// GTDT timer flags
|
||||
static constexpr uint32_t GtdtEdgeTriggered = 1u << 0;
|
||||
|
||||
static constexpr uint32_t DefaultVirtualTimerIntid = 27;
|
||||
|
||||
struct Gtdt {
|
||||
Hal::ACPI::CommonSDTHeader Header;
|
||||
uint64_t CntControlBase;
|
||||
uint32_t Reserved;
|
||||
uint32_t SecureEl1Gsiv;
|
||||
uint32_t SecureEl1Flags;
|
||||
uint32_t NonSecureEl1Gsiv;
|
||||
uint32_t NonSecureEl1Flags;
|
||||
uint32_t VirtualTimerGsiv;
|
||||
uint32_t VirtualTimerFlags;
|
||||
} __attribute__((packed));
|
||||
|
||||
static uint64_t g_ticksPerMs = 0;
|
||||
static uint32_t g_intid = DefaultVirtualTimerIntid;
|
||||
static bool g_edgeTriggered = false;
|
||||
|
||||
static inline void WriteCtl(uint64_t value) {
|
||||
asm volatile("msr cntv_ctl_el0, %0\n\tisb" :: "r"(value) : "memory");
|
||||
}
|
||||
|
||||
static inline uint64_t ReadCval() {
|
||||
uint64_t value;
|
||||
asm volatile("mrs %0, cntv_cval_el0" : "=r"(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
static inline void WriteCval(uint64_t value) {
|
||||
asm volatile("msr cntv_cval_el0, %0\n\tisb" :: "r"(value) : "memory");
|
||||
}
|
||||
|
||||
// Advance the deadline by one period from the previous one (no drift),
|
||||
// skipping ticks the CPU missed rather than replaying them.
|
||||
static void Rearm() {
|
||||
uint64_t period = Smp::GetCurrentCpuData()->arch.timerPeriod;
|
||||
if (period == 0) {
|
||||
WriteCtl(CtlMask);
|
||||
return;
|
||||
}
|
||||
uint64_t now = ReadTimestamp();
|
||||
uint64_t next = ReadCval() + period;
|
||||
if ((int64_t)(next - now) <= 0) next = now + period;
|
||||
WriteCval(next);
|
||||
}
|
||||
|
||||
static void ReadGtdt() {
|
||||
auto* gtdt = (Gtdt*)Gic::FindTable(Gic::CurrentXsdt(), "GTDT");
|
||||
if (gtdt == nullptr || gtdt->Header.Length < sizeof(Gtdt)) return;
|
||||
if (gtdt->VirtualTimerGsiv >= 16 && gtdt->VirtualTimerGsiv < 32) {
|
||||
g_intid = gtdt->VirtualTimerGsiv;
|
||||
g_edgeTriggered = (gtdt->VirtualTimerFlags & GtdtEdgeTriggered) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool InitializeBsp(uint32_t periodMs) {
|
||||
uint64_t frequency;
|
||||
asm volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
|
||||
if (frequency < 1000 || !Gic::IsInitialized()) return false;
|
||||
g_ticksPerMs = frequency / 1000;
|
||||
|
||||
ReadGtdt();
|
||||
Gic::SetAckHook(IRQ_TIMER, Rearm);
|
||||
StartPeriodic(periodMs);
|
||||
|
||||
KernelLogStream(OK, "Timer") << "Generic timer: " << base::dec << frequency
|
||||
<< " Hz counter, virtual timer INTID " << (uint64_t)g_intid;
|
||||
return true;
|
||||
}
|
||||
|
||||
// PPIs are banked: each CPU enables its own timer interrupt.
|
||||
void StartPeriodic(uint32_t periodMs) {
|
||||
Gic::RoutePpi(g_intid, IRQ_TIMER, g_edgeTriggered);
|
||||
uint64_t period = g_ticksPerMs * periodMs;
|
||||
Smp::GetCurrentCpuData()->arch.timerPeriod = period;
|
||||
WriteCval(ReadTimestamp() + period);
|
||||
WriteCtl(CtlEnable);
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
Smp::GetCurrentCpuData()->arch.timerPeriod = 0;
|
||||
WriteCtl(CtlMask);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
Vectors.S
|
||||
aarch64 exception vector table (VBAR_EL1)
|
||||
Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
/*
|
||||
* Every exception saves the same 288-byte frame on SP_EL1, laid out as
|
||||
* Arch::PanicFrame (InterruptFrame.hpp) and Arch::SyscallFrame:
|
||||
*
|
||||
* 0 x0 .. x30 (x30 at 240)
|
||||
* 248 interrupted SP (SP_EL0 from EL0, the pre-exception SP_EL1 from EL1)
|
||||
* 256 ELR_EL1
|
||||
* 264 SPSR_EL1
|
||||
* 272 ESR_EL1
|
||||
* 280 FAR_EL1
|
||||
*
|
||||
* The kernel runs on SP_EL1 (SPSel = 1, see Platform.cpp). From EL0,
|
||||
* SP_EL1 is the top of the thread's kernel stack: ArchEnterUserMode sets
|
||||
* it before the first ERET and every return to EL0 restores it.
|
||||
*/
|
||||
|
||||
#define FRAME_SIZE 288
|
||||
#define FRAME_SP 248
|
||||
#define FRAME_ELR 256
|
||||
#define FRAME_SPSR 264
|
||||
#define FRAME_ESR 272
|
||||
#define FRAME_FAR 280
|
||||
|
||||
.text
|
||||
|
||||
.macro SAVE_FRAME el
|
||||
sub sp, sp, #FRAME_SIZE
|
||||
stp x0, x1, [sp, #0]
|
||||
stp x2, x3, [sp, #16]
|
||||
stp x4, x5, [sp, #32]
|
||||
stp x6, x7, [sp, #48]
|
||||
stp x8, x9, [sp, #64]
|
||||
stp x10, x11, [sp, #80]
|
||||
stp x12, x13, [sp, #96]
|
||||
stp x14, x15, [sp, #112]
|
||||
stp x16, x17, [sp, #128]
|
||||
stp x18, x19, [sp, #144]
|
||||
stp x20, x21, [sp, #160]
|
||||
stp x22, x23, [sp, #176]
|
||||
stp x24, x25, [sp, #192]
|
||||
stp x26, x27, [sp, #208]
|
||||
stp x28, x29, [sp, #224]
|
||||
.if \el == 0
|
||||
mrs x21, sp_el0
|
||||
.else
|
||||
add x21, sp, #FRAME_SIZE
|
||||
.endif
|
||||
stp x30, x21, [sp, #240]
|
||||
mrs x22, elr_el1
|
||||
mrs x23, spsr_el1
|
||||
stp x22, x23, [sp, #FRAME_ELR]
|
||||
mrs x24, esr_el1
|
||||
mrs x25, far_el1
|
||||
stp x24, x25, [sp, #FRAME_ESR]
|
||||
.endm
|
||||
|
||||
/* Interrupts are masked again before ELR/SPSR are reloaded: an IRQ taken
|
||||
in between would overwrite them. */
|
||||
.macro RESTORE_FRAME el
|
||||
msr daifset, #2
|
||||
ldp x22, x23, [sp, #FRAME_ELR]
|
||||
msr elr_el1, x22
|
||||
msr spsr_el1, x23
|
||||
.if \el == 0
|
||||
ldr x21, [sp, #FRAME_SP]
|
||||
msr sp_el0, x21
|
||||
.endif
|
||||
ldp x0, x1, [sp, #0]
|
||||
ldp x2, x3, [sp, #16]
|
||||
ldp x4, x5, [sp, #32]
|
||||
ldp x6, x7, [sp, #48]
|
||||
ldp x8, x9, [sp, #64]
|
||||
ldp x10, x11, [sp, #80]
|
||||
ldp x12, x13, [sp, #96]
|
||||
ldp x14, x15, [sp, #112]
|
||||
ldp x16, x17, [sp, #128]
|
||||
ldp x18, x19, [sp, #144]
|
||||
ldp x20, x21, [sp, #160]
|
||||
ldp x22, x23, [sp, #176]
|
||||
ldp x24, x25, [sp, #192]
|
||||
ldp x26, x27, [sp, #208]
|
||||
ldp x28, x29, [sp, #224]
|
||||
ldr x30, [sp, #240]
|
||||
add sp, sp, #FRAME_SIZE
|
||||
eret
|
||||
.endm
|
||||
|
||||
/* Each vector slot is 128 bytes; the handlers live outside the table. */
|
||||
.macro VECTOR target
|
||||
.balign 0x80
|
||||
b \target
|
||||
.endm
|
||||
|
||||
.balign 0x800
|
||||
.global ArchExceptionVectors
|
||||
ArchExceptionVectors:
|
||||
/* Current EL with SP_EL0: the kernel never runs on SP_EL0. */
|
||||
VECTOR el1t_unexpected
|
||||
VECTOR el1t_unexpected
|
||||
VECTOR el1t_unexpected
|
||||
VECTOR el1t_unexpected
|
||||
/* Current EL with SP_ELx */
|
||||
VECTOR el1h_sync
|
||||
VECTOR el1h_irq
|
||||
VECTOR el1h_fiq
|
||||
VECTOR el1h_serror
|
||||
/* Lower EL, AArch64 */
|
||||
VECTOR el0_sync
|
||||
VECTOR el0_irq
|
||||
VECTOR el0_fiq
|
||||
VECTOR el0_serror
|
||||
/* Lower EL, AArch32: not supported */
|
||||
VECTOR el0_aarch32
|
||||
VECTOR el0_aarch32
|
||||
VECTOR el0_aarch32
|
||||
VECTOR el0_aarch32
|
||||
|
||||
/* ---- Exceptions from the kernel ---- */
|
||||
|
||||
el1t_unexpected:
|
||||
SAVE_FRAME 1
|
||||
mov x0, sp
|
||||
mov x1, #0
|
||||
bl ArchUnexpectedException
|
||||
|
||||
el1h_sync:
|
||||
SAVE_FRAME 1
|
||||
mov x0, sp
|
||||
mov x1, #0
|
||||
bl ArchSyncException
|
||||
RESTORE_FRAME 1
|
||||
|
||||
el1h_irq:
|
||||
SAVE_FRAME 1
|
||||
mov x0, sp
|
||||
mov x1, #0
|
||||
bl ArchIrqException
|
||||
RESTORE_FRAME 1
|
||||
|
||||
el1h_fiq:
|
||||
SAVE_FRAME 1
|
||||
mov x0, sp
|
||||
mov x1, #1
|
||||
bl ArchUnexpectedException
|
||||
|
||||
el1h_serror:
|
||||
SAVE_FRAME 1
|
||||
mov x0, sp
|
||||
mov x1, #2
|
||||
bl ArchUnexpectedException
|
||||
|
||||
/* ---- Exceptions from user mode ---- */
|
||||
|
||||
el0_sync:
|
||||
SAVE_FRAME 0
|
||||
lsr x9, x24, #26 /* ESR_EL1.EC */
|
||||
cmp x9, #0x15 /* SVC from AArch64 */
|
||||
b.ne 1f
|
||||
|
||||
msr daifclr, #2
|
||||
mov x0, sp
|
||||
bl SyscallDispatch
|
||||
str x0, [sp, #0]
|
||||
bl SchedExitIfKilled
|
||||
RESTORE_FRAME 0
|
||||
|
||||
1: mov x0, sp
|
||||
mov x1, #1
|
||||
bl ArchSyncException
|
||||
RESTORE_FRAME 0
|
||||
|
||||
el0_irq:
|
||||
SAVE_FRAME 0
|
||||
mov x0, sp
|
||||
mov x1, #1
|
||||
bl ArchIrqException
|
||||
RESTORE_FRAME 0
|
||||
|
||||
el0_fiq:
|
||||
SAVE_FRAME 0
|
||||
mov x0, sp
|
||||
mov x1, #1
|
||||
bl ArchUnexpectedException
|
||||
|
||||
el0_serror:
|
||||
SAVE_FRAME 0
|
||||
mov x0, sp
|
||||
mov x1, #2
|
||||
bl ArchUnexpectedException
|
||||
|
||||
el0_aarch32:
|
||||
SAVE_FRAME 0
|
||||
mov x0, sp
|
||||
mov x1, #3
|
||||
bl ArchUnexpectedException
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Abi.hpp
|
||||
* x86_64 user-mode ABI details (implements Arch/Abi.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// UEFI uses the Microsoft x64 calling convention.
|
||||
#define ARCH_EFIAPI __attribute__((__ms_abi__))
|
||||
|
||||
namespace Arch {
|
||||
|
||||
constexpr uint16_t ElfMachine = 62; // EM_X86_64
|
||||
constexpr const char* ElfMachineName = "x86_64";
|
||||
|
||||
inline RelocationKind ClassifyRelocation(uint32_t type) {
|
||||
switch (type) {
|
||||
case 0: return RelocationKind::None;
|
||||
case 8: return RelocationKind::Relative; // R_X86_64_RELATIVE
|
||||
case 1: // R_X86_64_64
|
||||
case 6: // R_X86_64_GLOB_DAT
|
||||
case 7: return RelocationKind::Symbol; // R_X86_64_JUMP_SLOT
|
||||
default: return RelocationKind::Unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
// Variant II. The linker addresses TLS variables at
|
||||
// TP - align_up(memSize, p_align) + offset, so the template must start
|
||||
// exactly that far below TP; rounding p_align up first (as an older
|
||||
// version did) shifts every variable away from its initial value.
|
||||
// TP itself is kept at least 16-byte aligned, which the gap between
|
||||
// base and the template absorbs. The 16 bytes above TP are the TCB.
|
||||
inline TlsLayout ComputeTlsLayout(uint64_t memSize, uint64_t pAlign) {
|
||||
uint64_t align = pAlign ? pAlign : 1;
|
||||
uint64_t block = (memSize + align - 1) & ~(align - 1);
|
||||
uint64_t tpAlign = align < 16 ? 16 : align;
|
||||
uint64_t tp = (block + tpAlign - 1) & ~(tpAlign - 1);
|
||||
return {tp - block, tp, tp + 16};
|
||||
}
|
||||
|
||||
inline void WriteExitStub(uint8_t* stub) {
|
||||
stub[0] = 0x31; stub[1] = 0xFF; // xor edi, edi (exit code 0)
|
||||
stub[2] = 0x31; stub[3] = 0xC0; // xor eax, eax (SYS_EXIT = 0)
|
||||
stub[4] = 0x0F; stub[5] = 0x05; // syscall
|
||||
}
|
||||
|
||||
// The entry function is entered as if CALLed: the return address sits
|
||||
// at [RSP], with RSP = 16n - 8.
|
||||
inline uint64_t SetupUserEntryStack(uint8_t* topPage, uint64_t stackTop,
|
||||
uint64_t returnAddress) {
|
||||
*(uint64_t*)(topPage + 0xFF8) = returnAddress;
|
||||
return stackTop - 8;
|
||||
}
|
||||
|
||||
// SystemV alignment as if CALLed (16n - 8); the slot is never read.
|
||||
inline uint64_t ThreadEntryStackPointer(uint64_t stackTop) {
|
||||
return (stackTop & ~0xFULL) - 8;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,8 +4,10 @@
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "AcpiSleep.hpp"
|
||||
#include "AcpiEvents.hpp"
|
||||
#include <ACPI/AcpiSleep.hpp>
|
||||
#include <ACPI/AcpiEvents.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/x86_64/Acpi/S3State.hpp>
|
||||
#include <ACPI/FADT.hpp>
|
||||
#include <ACPI/AML/AmlParser.hpp>
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
@@ -14,19 +16,19 @@
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Hal/GDT.hpp>
|
||||
#include <Hal/Apic/Apic.hpp>
|
||||
#include <Hal/Apic/IoApic.hpp>
|
||||
#include <Hal/Apic/Pic.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Hal/CpuPower.hpp>
|
||||
#include <Arch/x86_64/GDT.hpp>
|
||||
#include <Arch/x86_64/Apic/Apic.hpp>
|
||||
#include <Arch/x86_64/Apic/IoApic.hpp>
|
||||
#include <Arch/x86_64/Apic/Pic.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
#include <Arch/CpuPower.hpp>
|
||||
#include <Drivers/Graphics/IntelGPU.hpp>
|
||||
#include <Drivers/Graphics/DgpuPower.hpp>
|
||||
#include <Drivers/PS2/PS2Controller.hpp>
|
||||
#include <Graphics/Framebuffer.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Hal/MSR.hpp>
|
||||
#include <Hal/SmpBoot.hpp>
|
||||
#include <Arch/x86_64/MSR.hpp>
|
||||
#include <Arch/Smp.hpp>
|
||||
#include <Api/Syscall.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
@@ -186,7 +188,7 @@ namespace Hal {
|
||||
<< base::hex << (uint64_t)g_s3SlpTypA
|
||||
<< " SLP_TYPb=" << base::hex << (uint64_t)g_s3SlpTypB << ")";
|
||||
KernelLogStream(INFO, "S3") << "Kernel PML4 phys = " << base::hex
|
||||
<< (uint64_t)Memory::VMM::g_paging->PML4;
|
||||
<< Memory::VMM::g_paging->KernelRoot();
|
||||
KernelLogStream(INFO, "S3") << "FACS at phys " << base::hex
|
||||
<< (uint64_t)fadt.FacsAddress << " version=" << base::dec
|
||||
<< (uint64_t)g_facs->Version << " flags=" << base::hex
|
||||
@@ -314,7 +316,7 @@ namespace Hal {
|
||||
// shadow copy of the PML4 at a fixed low-memory address.
|
||||
// Only the PML4 page itself needs to be below 4GB — its entries
|
||||
// contain full 52-bit physical addresses for sub-tables.
|
||||
uint64_t kernelPml4Phys = (uint64_t)Memory::VMM::g_paging->PML4;
|
||||
uint64_t kernelPml4Phys = Memory::VMM::g_paging->KernelRoot();
|
||||
uint8_t* kernelPml4Virt = (uint8_t*)Memory::HHDM(kernelPml4Phys);
|
||||
uint8_t* shadowPml4Virt = (uint8_t*)Memory::HHDM((uint64_t)SHADOW_PML4_PHYS);
|
||||
memcpy(shadowPml4Virt, kernelPml4Virt, 4096);
|
||||
@@ -439,7 +441,7 @@ namespace Hal {
|
||||
Hal::InitializePAT();
|
||||
// HWP enable and POWER_CTL reset across S3; re-apply them and
|
||||
// bump the policy epoch so APs re-apply on their next tick.
|
||||
Hal::CpuPower::ReapplyAfterWake();
|
||||
Arch::CpuPower::ReapplyAfterWake();
|
||||
reinitProgress(0x05); // PIC
|
||||
Hal::DisableLegacyPic();
|
||||
reinitProgress(0x06); // Local APIC
|
||||
@@ -447,7 +449,7 @@ namespace Hal {
|
||||
reinitProgress(0x07); // IO APIC
|
||||
Hal::IoApic::Reinitialize();
|
||||
reinitProgress(0x08); // APIC Timer
|
||||
Timekeeping::ApicTimerReinitialize();
|
||||
Timekeeping::TimerReinitialize();
|
||||
reinitProgress(0x09); // PM1 clear
|
||||
ClearPM1Status();
|
||||
reinitProgress(0x0A); // _WAK
|
||||
@@ -463,7 +465,7 @@ namespace Hal {
|
||||
reinitProgress(0x0D); // discrete GPU back off
|
||||
Drivers::Graphics::DgpuPower::Resume();
|
||||
reinitProgress(0x0E); // sti
|
||||
asm volatile("sti");
|
||||
Arch::EnableInterrupts();
|
||||
|
||||
KernelLogStream(OK, "S3") << "Resumed from S3 suspend";
|
||||
|
||||
@@ -517,7 +519,7 @@ namespace Hal {
|
||||
Io::Out8(0x00, 0x71);
|
||||
|
||||
// 3. Disable interrupts
|
||||
asm volatile("cli");
|
||||
Arch::DisableInterrupts();
|
||||
|
||||
// 4. Identity-map the trampoline page so the CPU can execute it
|
||||
// after paging is re-enabled with our CR3 during wake.
|
||||
@@ -525,7 +527,7 @@ namespace Hal {
|
||||
Memory::VMM::g_paging->Map(TRAMPOLINE_PHYS, TRAMPOLINE_PHYS);
|
||||
|
||||
// 5. Flush all caches
|
||||
asm volatile("wbinvd");
|
||||
Arch::WritebackInvalidateCaches();
|
||||
|
||||
// 6. Set the waking vector in FACS
|
||||
SetWakingVector();
|
||||
@@ -548,7 +550,7 @@ namespace Hal {
|
||||
}
|
||||
|
||||
// 10. Flush caches again
|
||||
asm volatile("wbinvd");
|
||||
Arch::WritebackInvalidateCaches();
|
||||
|
||||
// 11. Assert SLP_EN — this triggers S3 entry
|
||||
Io::Out16(pm1a | PM1_SLP_EN, (uint16_t)g_pm1aControlBlock);
|
||||
@@ -563,7 +565,7 @@ namespace Hal {
|
||||
// on QEMU/KVM the vCPU must reach HLT before QEMU finalizes S3.
|
||||
// On resume, firmware jumps to our waking vector.
|
||||
for (;;) {
|
||||
asm volatile("hlt");
|
||||
Arch::Halt();
|
||||
// If we wake from HLT (spurious or real wake), check WAK_STS
|
||||
if (g_pm1aEventBlock != 0) {
|
||||
uint16_t sts = Io::In16((uint16_t)g_pm1aEventBlock);
|
||||
@@ -574,7 +576,7 @@ namespace Hal {
|
||||
// If we get here, either S3 failed or we woke immediately.
|
||||
// The normal wake path goes through AcpiResumeLongMode instead.
|
||||
// Re-enable interrupts as a safety measure.
|
||||
asm volatile("sti");
|
||||
Arch::EnableInterrupts();
|
||||
|
||||
KernelLogStream(WARNING, "S3") << "S3 entry may have failed (fell through)";
|
||||
return 0;
|
||||
@@ -5,10 +5,11 @@
|
||||
*/
|
||||
|
||||
#include "CpuIdle.hpp"
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <ACPI/FADT.hpp>
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <Hal/Cpu.hpp>
|
||||
#include <Hal/SmpBoot.hpp>
|
||||
#include <Arch/x86_64/CpuFeatures.hpp>
|
||||
#include <Arch/Smp.hpp>
|
||||
#include <Io/IoPort.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
@@ -480,7 +481,7 @@ namespace Hal {
|
||||
if (hasMwait) {
|
||||
Hal::IdleWaitWithInterruptsDisabled(addr, CurrentIdleHint());
|
||||
} else {
|
||||
Hal::HaltWithInterruptsDisabled();
|
||||
Arch::HaltWithInterruptsDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* S3State.hpp
|
||||
* CPU state saved across S3 by S3Wake.asm
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Hal {
|
||||
namespace AcpiSleep {
|
||||
|
||||
// ============================================================================
|
||||
// Layout must match S3Wake.asm offsets exactly.
|
||||
// No FPU/SSE state — the kernel is compiled with -mno-sse.
|
||||
struct CpuState {
|
||||
uint64_t Rax, Rbx, Rcx, Rdx; // 0x00 - 0x18
|
||||
uint64_t Rsi, Rdi, Rbp, Rsp; // 0x20 - 0x38
|
||||
uint64_t R8, R9, R10, R11; // 0x40 - 0x58
|
||||
uint64_t R12, R13, R14, R15; // 0x60 - 0x78
|
||||
uint64_t Rflags; // 0x80
|
||||
uint64_t Rip; // 0x88
|
||||
uint64_t Cr3; // 0x90
|
||||
uint64_t Cr0, Cr4; // 0x98, 0xA0
|
||||
uint8_t GdtPtr[10]; // 0xA8
|
||||
uint8_t IdtPtr[10]; // 0xB2
|
||||
} __attribute__((aligned(16)));
|
||||
|
||||
};
|
||||
};
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -10,7 +10,7 @@
|
||||
#include "IoApic.hpp"
|
||||
#include "Interrupts.hpp"
|
||||
#include <ACPI/MADT.hpp>
|
||||
#include <Hal/IDT.hpp>
|
||||
#include <Arch/x86_64/IDT.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
@@ -67,8 +67,8 @@ namespace Hal {
|
||||
// Step 7: Route keyboard (IRQ1) and mouse (IRQ12) to BSP
|
||||
uint8_t bspApicId = (uint8_t)LocalApic::GetId();
|
||||
|
||||
IoApic::RouteIrq(IRQ_KEYBOARD, IRQ_VECTOR_BASE + IRQ_KEYBOARD, bspApicId);
|
||||
IoApic::RouteIrq(IRQ_MOUSE, IRQ_VECTOR_BASE + IRQ_MOUSE, bspApicId);
|
||||
IoApic::RouteIrq(Arch::IRQ_KEYBOARD, IRQ_VECTOR_BASE + Arch::IRQ_KEYBOARD, bspApicId);
|
||||
IoApic::RouteIrq(Arch::IRQ_MOUSE, IRQ_VECTOR_BASE + Arch::IRQ_MOUSE, bspApicId);
|
||||
|
||||
// Note: sti is NOT called here. The caller (Main.cpp) enables
|
||||
// interrupts after setting up per-CPU GS base for SMP/SWAPGS.
|
||||
File renamed without changes.
@@ -6,7 +6,9 @@
|
||||
|
||||
#include "Interrupts.hpp"
|
||||
#include "Apic.hpp"
|
||||
#include <Hal/IDT.hpp>
|
||||
#include "IoApic.hpp"
|
||||
#include <Arch/x86_64/IDT.hpp>
|
||||
#include <Arch/Smp.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
@@ -14,19 +16,15 @@
|
||||
extern "C" void* IrqStubTable[Hal::IRQ_COUNT];
|
||||
extern "C" void IrqStubSpurious();
|
||||
|
||||
// Local APIC message address range; destination APIC ID in bits 19:12.
|
||||
static constexpr uint64_t MsiAddressBase = 0xFEE00000;
|
||||
|
||||
namespace Hal {
|
||||
// Dispatch table: one handler per IRQ
|
||||
static IrqHandler g_irqHandlers[IRQ_COUNT] = {};
|
||||
|
||||
void RegisterIrqHandler(uint8_t irq, IrqHandler handler) {
|
||||
if (irq >= IRQ_COUNT) return;
|
||||
g_irqHandlers[irq] = handler;
|
||||
Kt::KernelLogStream(Kt::DEBUG, "IRQ") << "Registered handler for IRQ " << base::dec << (uint64_t)irq
|
||||
<< " (vector " << (uint64_t)(IRQ_VECTOR_BASE + irq) << ")";
|
||||
}
|
||||
static Arch::IrqHandler g_irqHandlers[IRQ_COUNT] = {};
|
||||
|
||||
void InitializeIrqHandlers() {
|
||||
// Install IRQ stubs into IDT vectors 32..55
|
||||
// Install IRQ stubs into IDT vectors 32..79
|
||||
for (int i = 0; i < IRQ_COUNT; i++) {
|
||||
IDTEncodeInterrupt(IRQ_VECTOR_BASE + i, IrqStubTable[i], 0x8E);
|
||||
}
|
||||
@@ -41,6 +39,49 @@ namespace Hal {
|
||||
|
||||
};
|
||||
|
||||
namespace Arch {
|
||||
void RegisterIrqHandler(uint8_t irq, IrqHandler handler) {
|
||||
if (irq >= IRQ_COUNT) return;
|
||||
Hal::g_irqHandlers[irq] = handler;
|
||||
Kt::KernelLogStream(Kt::DEBUG, "IRQ") << "Registered handler for IRQ " << base::dec << (uint64_t)irq
|
||||
<< " (vector " << (uint64_t)(Hal::IRQ_VECTOR_BASE + irq) << ")";
|
||||
}
|
||||
|
||||
void RouteLegacyIrq(uint8_t irq) {
|
||||
// RouteIrq applies the MADT's Interrupt Source Overrides (polarity,
|
||||
// trigger mode, GSI remapping).
|
||||
Hal::IoApic::RouteIrq(irq, Hal::IRQ_VECTOR_BASE + irq,
|
||||
(uint8_t)Hal::LocalApic::GetId());
|
||||
}
|
||||
|
||||
void EnableLegacyIrq(uint8_t irq) {
|
||||
Hal::IoApic::UnmaskIrq(Hal::IoApic::GetGsiForIrq(irq));
|
||||
}
|
||||
|
||||
bool PlatformLinesAreFixed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AssignPlatformLine(uint8_t, uint32_t, bool, bool) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ComposeMsi(uint8_t irq, MsiMessage& out) {
|
||||
// Physical destination mode, fixed delivery, edge triggered, to the
|
||||
// BSP (APIC ID 0 -- the destination field is left zero, as every
|
||||
// driver has always programmed it).
|
||||
out = MsiMessage{ MsiAddressBase, (uint32_t)(Hal::IRQ_VECTOR_BASE + irq) };
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SendIpi(int cpuIndex, uint8_t irq) {
|
||||
auto* cpu = Smp::GetCpuData(cpuIndex);
|
||||
if (cpu == nullptr) return false;
|
||||
return Hal::LocalApic::SendFixedIpi(cpu->arch.lapicId,
|
||||
Hal::IRQ_VECTOR_BASE + irq);
|
||||
}
|
||||
};
|
||||
|
||||
// C linkage dispatch function called from assembly stubs
|
||||
extern "C" void HalIrqDispatch(uint64_t irqNumber, uint64_t savedCs) {
|
||||
// Send EOI BEFORE calling the handler. The handler may context-switch
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Interrupts.hpp
|
||||
* x86_64 IRQ slot <-> IDT vector mapping
|
||||
* Copyright (c) 2025 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Arch/Interrupts.hpp>
|
||||
|
||||
namespace Hal {
|
||||
using Arch::IRQ_COUNT;
|
||||
|
||||
// IRQ vector base: IRQ slot N is delivered on IDT vector 32 + N.
|
||||
constexpr uint8_t IRQ_VECTOR_BASE = 32;
|
||||
|
||||
// Install IRQ stubs into the IDT and set up the dispatch table
|
||||
void InitializeIrqHandlers();
|
||||
|
||||
};
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* ApicTimer.cpp
|
||||
* Local APIC timer, PIT-calibrated (implements Arch/Timer.hpp)
|
||||
* Copyright (c) 2025 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/Timer.hpp>
|
||||
#include <Arch/Cpu.hpp>
|
||||
#include <Arch/x86_64/Apic/Apic.hpp>
|
||||
#include <Arch/x86_64/Apic/Interrupts.hpp>
|
||||
#include <Io/IoPort.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Arch::Timer {
|
||||
// PIT constants
|
||||
static constexpr uint32_t PIT_FREQUENCY = 1193182; // PIT oscillator frequency in Hz
|
||||
static constexpr uint16_t PIT_CHANNEL2_DATA = 0x42;
|
||||
static constexpr uint16_t PIT_COMMAND = 0x43;
|
||||
static constexpr uint16_t PIT_GATE_PORT = 0x61;
|
||||
|
||||
// APIC timer LVT mode bits
|
||||
static constexpr uint32_t LVT_MASKED = (1 << 16);
|
||||
static constexpr uint32_t LVT_PERIODIC = (1 << 17);
|
||||
|
||||
// APIC timer divide configuration values
|
||||
static constexpr uint32_t DIVIDE_BY_16 = 0x03;
|
||||
|
||||
static uint32_t g_ticksPerMs = 0;
|
||||
|
||||
static uint32_t CountForIntervalMs(uint32_t intervalMs) {
|
||||
return g_ticksPerMs * intervalMs;
|
||||
}
|
||||
|
||||
static void ProgramTimer(bool periodic, uint32_t intervalMs) {
|
||||
uint32_t lvt = (Hal::IRQ_VECTOR_BASE + Arch::IRQ_TIMER);
|
||||
if (periodic) {
|
||||
lvt |= LVT_PERIODIC;
|
||||
}
|
||||
|
||||
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_DIVIDE, DIVIDE_BY_16);
|
||||
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_LVT, lvt);
|
||||
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_INITIAL,
|
||||
CountForIntervalMs(intervalMs));
|
||||
}
|
||||
|
||||
// Use PIT channel 2 to create a precise delay for calibration.
|
||||
// Returns the number of APIC timer ticks that elapsed during ~10ms.
|
||||
static uint32_t CalibratePit() {
|
||||
// We want to measure ~10ms worth of APIC timer ticks.
|
||||
// PIT count for 10ms: PIT_FREQUENCY / 100 = 11931
|
||||
uint16_t pitCount = PIT_FREQUENCY / 100;
|
||||
|
||||
// Set up PIT channel 2 in one-shot mode (mode 0), lobyte/hibyte
|
||||
// Command: channel 2 (bits 7-6 = 10), lobyte/hibyte (bits 5-4 = 11),
|
||||
// mode 0 (bits 3-1 = 000), binary (bit 0 = 0) => 0xB0
|
||||
Io::Out8(0xB0, PIT_COMMAND);
|
||||
|
||||
// Disable PIT channel 2 gate to prepare
|
||||
uint8_t gate = Io::In8(PIT_GATE_PORT);
|
||||
gate &= ~0x01; // Disable gate (bit 0 = 0)
|
||||
gate &= ~0x02; // Disable speaker (bit 1 = 0)
|
||||
Io::Out8(gate, PIT_GATE_PORT);
|
||||
|
||||
// Load the count value (low byte first, then high byte)
|
||||
Io::Out8((uint8_t)(pitCount & 0xFF), PIT_CHANNEL2_DATA);
|
||||
Io::IoPortWait();
|
||||
Io::Out8((uint8_t)(pitCount >> 8), PIT_CHANNEL2_DATA);
|
||||
|
||||
// Set APIC timer to max count with divide-by-16, one-shot (masked so no IRQ)
|
||||
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_DIVIDE, DIVIDE_BY_16);
|
||||
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_LVT, LVT_MASKED);
|
||||
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_INITIAL, 0xFFFFFFFF);
|
||||
|
||||
// Enable PIT channel 2 gate to start counting
|
||||
gate = Io::In8(PIT_GATE_PORT);
|
||||
gate |= 0x01; // Enable gate (bit 0 = 1)
|
||||
Io::Out8(gate, PIT_GATE_PORT);
|
||||
|
||||
// Wait for PIT channel 2 output to go high (bit 5 of port 0x61)
|
||||
while (!(Io::In8(PIT_GATE_PORT) & 0x20)) {
|
||||
Arch::Pause();
|
||||
}
|
||||
|
||||
// Read how many APIC timer ticks elapsed
|
||||
uint32_t currentCount = Hal::LocalApic::ReadRegister(Hal::LocalApic::REG_TIMER_CURRENT);
|
||||
uint32_t elapsed = 0xFFFFFFFF - currentCount;
|
||||
|
||||
// Stop the APIC timer
|
||||
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_INITIAL, 0);
|
||||
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
bool InitializeBsp(uint32_t periodMs) {
|
||||
KernelLogStream(INFO, "Timer") << "Calibrating APIC timer using PIT channel 2";
|
||||
|
||||
// Calibrate: measure APIC ticks over ~10ms
|
||||
uint32_t ticksIn10ms = CalibratePit();
|
||||
g_ticksPerMs = ticksIn10ms / 10;
|
||||
|
||||
if (g_ticksPerMs == 0) {
|
||||
KernelLogStream(ERROR, "Timer") << "APIC timer calibration failed (0 ticks/ms)";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t timerFreqHz = (uint64_t)g_ticksPerMs * 1000;
|
||||
|
||||
KernelLogStream(OK, "Timer") << "APIC timer: " << base::dec << (uint64_t)g_ticksPerMs
|
||||
<< " ticks/ms (" << timerFreqHz << " Hz, divide-by-16)";
|
||||
|
||||
// Periodic mode on vector IRQ_VECTOR_BASE + IRQ_TIMER
|
||||
ProgramTimer(true, periodMs);
|
||||
|
||||
KernelLogStream(OK, "Timer") << "APIC timer started: initial count="
|
||||
<< base::dec << (uint64_t)CountForIntervalMs(periodMs);
|
||||
return true;
|
||||
}
|
||||
|
||||
void StartPeriodic(uint32_t periodMs) {
|
||||
ProgramTimer(true, periodMs);
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
uint32_t lvt = Hal::LocalApic::ReadRegister(Hal::LocalApic::REG_TIMER_LVT);
|
||||
if ((lvt & LVT_MASKED) == 0) {
|
||||
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_LVT,
|
||||
lvt | LVT_MASKED);
|
||||
}
|
||||
}
|
||||
};
|
||||
File renamed without changes.
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Context.cpp
|
||||
* x86_64 thread context (implements Arch/Context.hpp)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Arch/Context.hpp>
|
||||
#include <Arch/x86_64/MSR.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
|
||||
// Context.asm: save callee-saved registers + RSP + FPU, switch CR3, restore.
|
||||
extern "C" void SchedContextSwitch(uint64_t* oldRsp, uint64_t newRsp, uint64_t newCR3,
|
||||
uint8_t* oldFpuArea, uint8_t* newFpuArea);
|
||||
|
||||
// SyscallEntry.asm: jump to user mode via IRETQ.
|
||||
// `arg` is delivered as the user-mode RDI (SystemV first argument).
|
||||
extern "C" [[noreturn]] void JumpToUserMode(uint64_t rip, uint64_t rsp, uint64_t arg);
|
||||
|
||||
namespace Arch {
|
||||
|
||||
static constexpr uint32_t IA32_FS_BASE = 0xC0000100;
|
||||
|
||||
void InitFpuState(uint8_t* area) {
|
||||
memset(area, 0, FpuStateSize);
|
||||
*(uint16_t*)&area[0] = 0x037F; // FCW: default x87 control word
|
||||
*(uint32_t*)&area[24] = 0x1F80; // MXCSR: default SSE control/status
|
||||
}
|
||||
|
||||
uint64_t PrepareThreadStack(uint64_t kernelStackTop, void (*entry)()) {
|
||||
// The frame Context.asm pops: r15, r14, r13, r12, rbx, rbp, then the
|
||||
// return address it RETs to.
|
||||
uint64_t* sp = (uint64_t*)kernelStackTop;
|
||||
*(--sp) = (uint64_t)entry; // return addr
|
||||
*(--sp) = 0; // rbp
|
||||
*(--sp) = 0; // rbx
|
||||
*(--sp) = 0; // r12
|
||||
*(--sp) = 0; // r13
|
||||
*(--sp) = 0; // r14
|
||||
*(--sp) = 0; // r15
|
||||
return (uint64_t)sp;
|
||||
}
|
||||
|
||||
void SwitchContext(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
|
||||
uint8_t* oldFpu, uint8_t* newFpu) {
|
||||
SchedContextSwitch(oldSp, newSp, newRoot, oldFpu, newFpu);
|
||||
}
|
||||
|
||||
void ActivateThread(Smp::CpuData* cpu, uint64_t kernelStackTop,
|
||||
uint64_t threadPointer) {
|
||||
// Kernel RSP for SYSCALL entry
|
||||
cpu->entry.kernelRsp = kernelStackTop;
|
||||
|
||||
// TSS RSP0 for hardware interrupts from ring 3
|
||||
cpu->arch.tss->rsp0 = kernelStackTop;
|
||||
|
||||
// The TLS thread pointer is IA32_FS_BASE. Cached per CPU: the MSR
|
||||
// write is skipped when unchanged, so TLS-free processes (pointer 0)
|
||||
// cost one write per CPU ever. Userspace cannot move FS base itself
|
||||
// (CR4.FSGSBASE is off), so the cache cannot go stale.
|
||||
if (cpu->arch.currentFsBase != threadPointer) {
|
||||
cpu->arch.currentFsBase = threadPointer;
|
||||
Hal::WriteMSR(IA32_FS_BASE, threadPointer);
|
||||
}
|
||||
}
|
||||
|
||||
void EnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg) {
|
||||
JumpToUserMode(entry, userSp, arg);
|
||||
}
|
||||
|
||||
}
|
||||
Loaded 100 of 552 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user