# ==============================================================================
# kernel/Makefile — StarKernel UEFI Build System
# Multi-architecture: amd64 (x86_64), aarch64 (ARM64), riscv64 (RISC-V 64)
#
# Quick start:
#   make -f kernel/Makefile                        — build for host arch
#   make -f kernel/Makefile ARCH=amd64 qemu        — build + boot (x86_64)
#   make -f kernel/Makefile ARCH=aarch64 qemu      — build + boot (aarch64)
#   make -f kernel/Makefile ARCH=riscv64 qemu      — build + boot (riscv64)
#   make -f kernel/Makefile help                   — show all targets
# ==============================================================================

# ==============================================================================
# CONFIGURATION
# ==============================================================================

# Architecture normalization — accept common aliases
#   x86_64  → amd64
#   arm64   → aarch64
#   riscv   → riscv64
canon_arch = $(strip \
    $(if $(filter x86_64,$1),amd64, \
    $(if $(filter amd64,$1),amd64, \
    $(if $(filter arm64,$1),aarch64, \
    $(if $(filter aarch64,$1),aarch64, \
    $(if $(filter riscv riscv64,$1),riscv64,$1))))))

UNAME_M        := $(shell uname -m)
ARCH           ?= $(call canon_arch,$(UNAME_M))
override ARCH  := $(call canon_arch,$(ARCH))

ifeq ($(filter amd64 aarch64 riscv64,$(ARCH)),)
    $(error Unsupported ARCH='$(ARCH)'. Use: amd64, aarch64, riscv64)
endif

# Kconfig bridge (menuconfig/xconfig/config/oldconfig/*_defconfig). See
# mk/Kconfig.mk for why KCONFIG_ARCH_DIR must be set here, before include.
# .DEFAULT_GOAL is pinned explicitly because mk/Kconfig.mk's first rule
# would otherwise silently become Make's default goal, since it's included
# before this Makefile's own `all:` target is defined below.
.DEFAULT_GOAL := all
KCONFIG_ARCH_DIR := $(ARCH)
include mk/Kconfig.mk

# Target type (default: kernel)
TARGET    ?= kernel

# Kernel-only flags (Phase 6): repointed through the Kconfig bridge, same
# drop-in-replacement-for-`?=` behavior as every other family in this
# migration. Kconfig.kernel's symbols only exist for a kernel-variant
# .config (`if STARFORTH_VARIANT_KERNEL`), so a hosted-variant .config
# leaves CONFIG_PARITY_MODE/etc undefined and kconfig_bool falls back to
# its $2 default exactly as if Kconfig were inactive -- this file is never
# built from a hosted-variant config in practice, but the fallback is
# correct either way.

# Parity mode (0 = normal, 1 = parity harness)
$(eval $(call kconfig_bool,PARITY_MODE,0))

# StarForth VM integration (default: enabled)
$(eval $(call kconfig_bool,STARFORTH_ENABLE_VM,1))

# StarForth v4 built in (default: on, 2026-10-14 -- v3 is being retired). With
# it the kernel carries v4 and v3 and boots v4 unless the boot arguments say
# system=v3 (V4_DEFAULT below). STARFORTH_V4=0 builds v3 alone.
$(eval $(call kconfig_bool,STARFORTH_V4,1))

# Monolithic build — loader + kernel compiled together (default: enabled)
MONOLITHIC ?= 1

# Parallel build — use all available cores when not cleaning
# (adding -j when 'clean' is also a goal causes a race on v3/include/version.h)
NPROC := $(shell nproc 2>/dev/null || echo 4)
ifeq ($(filter clean,$(MAKECMDGOALS)),)
MAKEFLAGS += -j$(NPROC)
endif

# Version
# Versioning policy (FABRIC-3.5.md §XXX, ratified 2026-09-19, replaces the old
# X.0.0/X.5.0 QEMU/hardware encoding this comment used to carry):
#   Major: ODD = LTS line (non-breaking fixes only). EVEN = working line
#          (breaking changes land here). Minor: release within the line.
#          Patch: working builds within a release.
#   v1.0.x  — serial-only production (released, historical encoding)
#   v1.5.x  — framebuffer VT100 console milestone (released, historical encoding)
#   v2.0.0  — QEMU release: three-arch QEMU story complete (released)
#   v2.0.1  — SER5 hardware-track line (historical; superseded by this policy)
#   v2.1.0  — Tripod/kernel reshuffle: Hermes into the kernel, Tripod = Hera/Artemis/Hestia
#   v2.2.0  — amd64 bare-metal bring-up (Beelink SER5)
#   v2.x    — further working releases; riscv64 / aarch64 board bring-up
#   v3.0.0  — FIRST LTS, when and only when FABRIC-3.5.md §XXX.5's 5 criteria are met
# LITHOS_VERSION (kernel) and VERSION (embedded StarForth engine) are independent
# and do not auto-sync (§XXX.4) — do not infer a relationship if they ever coincide.
# Engine VERSION bumps at tag time by what actually changed (§XXX.6): major = any
# FORTH-79-visible word semantics change; minor = words added/removed/relocated;
# patch = build-only. This reshuffle is a minor bump: BIRTH's registration widened,
# the messaging layer was replaced by kernel-Hermes, PLOT/FB-* relocated to Hestia —
# all dictionary-visible, no FORTH-79 word semantics changed.
VERSION       ?= 3.2.0
LITHOS_VERSION ?= 2.1.0

# ==============================================================================
# BUILD PATHS
# ==============================================================================

BUILD_DIR          := build/$(ARCH)/$(TARGET)
OBJ_DIR            := $(BUILD_DIR)/obj
LOADER_OBJ_DIR     := $(OBJ_DIR)/loader
KERNEL_OBJ_DIR     := $(OBJ_DIR)/kernel

LOADER_EFI         := $(BUILD_DIR)/starkernel_loader.efi
KERNEL_ELF         := $(BUILD_DIR)/starkernel_kernel.elf

KERNEL_SRC         := kernel/src
KERNEL_INC         := kernel/include/starkernel
STARFORTH_CONFIG_HEADER := v3/include/starforth_config.h

QEMU_LOG_DIR       := logs
QEMU_ISO           := $(BUILD_DIR)/starkernel.iso
DOE_RUNS_DIR       := experiments/bare_metal/runs
DOE_LATEST_DIR     := experiments/bare_metal/latest

CAPSULES_DIR       ?= capsules
ARTDISK            ?= disk/artemis.img
# XHCI_PORTS -- real top-level port count the qemu-xhci controller exposes on the
# xhci0.0 bus (p2 == p3 == this value; these are the SAME physical dual-role ports
# counted from the USB2 and USB3 register sets respectively, NOT additive to p2+p3 --
# confirmed live 2026-09-06, the stock default p2=4,p3=4 exposes exactly 4 real ports,
# not 8). Every usb-storage device attached below or via QEMU_EXTRA must get its own
# explicit port=N less than this value: exceed it and QEMU silently drops the overflow
# onto an auto-inserted internal USB2 hub, which the xHCI/BOT driver correctly refuses
# to treat as Mass Storage (root cause of the FABRIC-3.md §IX.5 "4th-device enumeration
# failure" -- misread as a driver bug at the time, it never was one). Sized with real
# headroom above the current 9-device identity roster (Zuse + 8 minted identities)
# rather than matching it exactly, per Bob's standing ruling against hardcoding a bound
# to today's scale (FABRIC-3.md §VII.4). Override if a future test needs more, e.g.
# `make -f kernel/Makefile qemu XHCI_PORTS=32`.
XHCI_PORTS         ?= 16
# XHCI_QEMU_ARGS -- the USB controller QEMU is given. Empty (the default) is
# qemu-xhci on the root bus, as always. Set it to rehearse a controller the
# way real hardware has one -- another vendor, behind a bridge (2026-10-14,
# boards/BRINGUP.md A1), e.g.
#   XHCI_QEMU_ARGS="-device pcie-root-port,id=rp1,bus=pcie.0,chassis=1,addr=0x6 \
#                   -device nec-usb-xhci,id=xhci0,bus=rp1,p2=16,p3=16"
# Keep id=xhci0: ZUSEDISK and QEMU_EXTRA attach to bus xhci0.0.
XHCI_QEMU_ARGS     ?=
XHCI_DEFAULT_ROOT  := -device qemu-xhci,id=xhci0,p2=$(XHCI_PORTS),p3=$(XHCI_PORTS)
XHCI_DEFAULT_SLOT4 := -device qemu-xhci,id=xhci0,addr=0x4,p2=$(XHCI_PORTS),p3=$(XHCI_PORTS)
XHCI_ARGS_ROOT     := $(if $(strip $(XHCI_QEMU_ARGS)),$(XHCI_QEMU_ARGS),$(XHCI_DEFAULT_ROOT))
XHCI_ARGS_SLOT4    := $(if $(strip $(XHCI_QEMU_ARGS)),$(XHCI_QEMU_ARGS),$(XHCI_DEFAULT_SLOT4))
# ZUSEDISK -- Zuse's own minted USB thumbdrive, attached by default so a
# plain `make qemu` lands already authenticated into the Zuse identity
# (Captain Bob, 2026-08-28). Attached at QEMU launch time via -drive/
# -device on the qemu-xhci controller each arch's qemu target already
# creates; xhci_bringup()'s initial port scan (kernel/src/usb/xhci.c)
# is what makes an already-connected-at-launch device visible at all --
# xhci_poll_events() alone is purely hotplug-event-driven and would never
# see a device present before controller reset. Override to "" (empty) to
# boot without Zuse attached, e.g. `make -f kernel/Makefile qemu ZUSEDISK=`.
ZUSEDISK           ?= disk/thumbdrives/zuse-thumb-ident.img
# Precomputed (not inlined as $(if ...,...)) because the drive/device specs
# below are comma-heavy and GNU make's $(if) function splits its own
# arguments on every top-level comma -- inlining it silently truncated the
# flags to just "-drive if=none" with everything past the next comma
# dropped, confirmed live via `ps aux` showing the truncated qemu command
# line (2026-08-28).
ifneq ($(strip $(ZUSEDISK)),)
ZUSEDISK_QEMU_ARGS := -drive if=none,id=zusedrv,file=$(ZUSEDISK),format=raw -device usb-storage,bus=xhci0.0,port=1,drive=zusedrv
else
ZUSEDISK_QEMU_ARGS :=
endif
MKCAPSULE_SRC       = tools/mkcapsule.c tools/pkcs8_ed25519.c \
                      kernel/src/crypto/ed25519.c \
                      kernel/src/crypto/fe25519.c \
                      kernel/src/crypto/scalar25519.c \
                      kernel/src/crypto/sha512.c
MKCAPSULE_BIN       = $(BUILD_DIR)/tools/mkcapsule
# Milestone 6 (Phase 8): the snakeoil intermediate's private key, generated
# offline outside this repo entirely (see FABRIC-2.md's Phase 8 §Milestone 6
# writeup) -- not present in CI or a fresh checkout, so signing is skipped
# gracefully ($(wildcard ...) below) rather than failing the build. Override
# with `make SIGN_KEY=/path/to/key.pem ...` on a machine that holds it.
SIGN_KEY           ?= /home/rajames/CLionProjects/lithosananke-ca/intermediate/snakeoil-intermediate.key
SIGN_KEY_ARGS       = $(if $(wildcard $(SIGN_KEY)),--sign-key $(SIGN_KEY),)
CAPSULE_GENERATED   = $(BUILD_DIR)/capsule_generated.c
CAPSULE_GENERATED_OBJ  = $(BUILD_DIR)/capsule_generated.o
CAPSULE_GENERATED_KOBJ = $(KERNEL_OBJ_DIR)/capsule_generated.o

# ==============================================================================
# BOARD (boot_image)
# ==============================================================================
# BOARD names a directory under boards/ (pc, raspi, milkv, ...). Its
# board.mk sets BOARD_ARCH, BOARD_BOOT (how the firmware finds us) and
# BOARD_IMAGE (the single file written to the boot medium). The root
# Makefile's `make boot_image TARGET=<NAME>` maps NAME to BOARD and passes
# ARCH=$(BOARD_ARCH); the kernel objects themselves are the ordinary
# build/$(ARCH)/kernel ones, shared with `make qemu`.
BOARD ?=
ifneq ($(strip $(BOARD)),)
ifeq ($(wildcard boards/$(BOARD)/board.mk),)
    $(error Unknown BOARD='$(BOARD)': no boards/$(BOARD)/board.mk)
endif
include boards/$(BOARD)/board.mk
ifneq ($(BOARD_ARCH),$(ARCH))
    $(error BOARD=$(BOARD) is ARCH=$(BOARD_ARCH), but ARCH=$(ARCH) was requested)
endif
BOARD_OUT := build/boards/$(BOARD)
endif
BOOT_IMAGE_SIZE_MIB ?= 128

# ==============================================================================
# TOOLCHAIN
# ==============================================================================

ifeq ($(ARCH),amd64)
    ifneq ($(shell which x86_64-elf-gcc 2>/dev/null),)
        CC      := x86_64-elf-gcc
        LD      := x86_64-elf-ld
        OBJCOPY := x86_64-elf-objcopy
    else
        CC      := gcc
        LD      := ld
        OBJCOPY := objcopy
    endif
    # -fno-pic -fno-pie: this is a fixed-base, statically-linked freestanding
    # image with no dynamic linker to populate a GOT at load time. -fPIC (set
    # in COMMON_CFLAGS, needed by riscv64's -shared link pipeline) makes GCC
    # emit R_X86_64_REX_GOTPCRELX (GOT-indirect) addressing for some cross-TU
    # extern globals depending on per-call-site codegen heuristics; the "GOT
    # slot" is just an unpopulated .bss cell here, so those reads silently
    # return NULL instead of the real address. Root-caused 2026-08-06 via
    # dict_find_word_heat_aware() reading sf_fc_count as NULL while
    # vm_find_word() (same globals, same TU as the definition) read it
    # correctly. These flags come after COMMON_CFLAGS on the command line so
    # they win for amd64 only; riscv64/aarch64 keep -fPIC.
    ARCH_CFLAGS          := -m64 -march=x86-64 -mno-red-zone -DARCH_AMD64 -fno-pic -fno-pie
    LOADER_LINKER_SCRIPT := kernel/linker/starkernel-loader-amd64.ld
    KERNEL_LINKER_SCRIPT := kernel/linker/starkernel-kernel-amd64.ld

else ifeq ($(ARCH),aarch64)
    ifneq ($(shell which aarch64-linux-gnu-gcc 2>/dev/null),)
        CC      := aarch64-linux-gnu-gcc
        LD      := aarch64-linux-gnu-ld
        OBJCOPY := aarch64-linux-gnu-objcopy
    else ifneq ($(shell which aarch64-none-elf-gcc 2>/dev/null),)
        CC      := aarch64-none-elf-gcc
        LD      := aarch64-none-elf-ld
        OBJCOPY := aarch64-none-elf-objcopy
    else
        $(error No ARM64 toolchain found. Install aarch64-linux-gnu-gcc or aarch64-none-elf-gcc)
    endif
    # aarch64 loader uses clang + lld-link (COFF target — GCC cannot produce aarch64 PE)
    # Any clang with the aarch64 COFF backend works; the version is not
    # pinned (Ubuntu 26.04 ships clang-21, older hosts clang-18).
    LOADER_CC            := $(shell which clang 2>/dev/null || ls /usr/bin/clang-[0-9]* 2>/dev/null | sort -V | tail -1 || echo clang)
    # Debian/Ubuntu package only installs the versioned lld-link-NN on PATH by
    # default (unversioned lld-link lives under /usr/lib/llvm-NN/bin, which
    # isn't on PATH unless explicitly prepended) — detect whichever resolves.
    LOADER_LD            := $(shell which lld-link 2>/dev/null || ls /usr/bin/lld-link-[0-9]* /usr/lib/llvm-*/bin/lld-link 2>/dev/null | sort -V | tail -1 || echo lld-link)
    ARCH_CFLAGS          := -march=armv8-a -mcpu=cortex-a72 -DARCH_AARCH64 -mno-outline-atomics
    LOADER_LINKER_SCRIPT := kernel/linker/starkernel-loader-aarch64.ld
    KERNEL_LINKER_SCRIPT := kernel/linker/starkernel-kernel-aarch64.ld

else ifeq ($(ARCH),riscv64)
    ifneq ($(shell which riscv64-unknown-elf-gcc 2>/dev/null),)
        CC      := riscv64-unknown-elf-gcc
        LD      := riscv64-unknown-elf-ld
        OBJCOPY := riscv64-unknown-elf-objcopy
    else ifneq ($(shell which riscv64-linux-gnu-gcc 2>/dev/null),)
        CC      := riscv64-linux-gnu-gcc
        LD      := riscv64-linux-gnu-ld
        OBJCOPY := riscv64-linux-gnu-objcopy
    else
        $(error No RISC-V toolchain found. Install riscv64-unknown-elf-gcc or riscv64-linux-gnu-gcc)
    endif
    ARCH_CFLAGS          := -march=rv64gc -mabi=lp64d -DARCH_RISCV64
    LOADER_LINKER_SCRIPT := kernel/linker/starkernel-loader-riscv64.ld
    KERNEL_LINKER_SCRIPT := kernel/linker/starkernel-kernel-riscv64.ld
endif

# Default loader toolchain = kernel toolchain (overridden for aarch64 above)
LOADER_CC ?= $(CC)
LOADER_LD ?= $(LD)

# ==============================================================================
# COMPILER FLAGS
# ==============================================================================

# Common flags for all arches / both loader and kernel.
# -fPIC + -fvisibility=hidden: needed by riscv64's -shared loader link
#   pipeline (amd64 overrides back to -fno-pic/-fno-pie in its ARCH_CFLAGS
#   above -- see that comment for why: -fPIC made GCC emit GOT-indirect
#   addressing for some cross-TU extern globals, and this freestanding image
#   has no dynamic linker to populate a GOT, so those reads silently returned
#   NULL. PLATFORM_TIME_NO_INLINE (removed 2026-08-06) was a symbol-specific
#   workaround for the same underlying bug, made unnecessary once amd64 got
#   the real fix; see FABRIC-0.md for the write-up).
COMMON_CFLAGS := \
    -std=c99 -Wall -Werror -Wextra \
    -O2 -U_FORTIFY_SOURCE \
    -ffreestanding -nostdlib -fno-builtin \
    -fPIC -fvisibility=hidden -fno-stack-protector -fshort-wchar \
    $(ARCH_CFLAGS) \
    -I$(KERNEL_INC) -Iv3/include -Ikernel/include -Iv3/src \
    -include $(STARFORTH_CONFIG_HEADER) \
    -DPARITY_MODE=$(PARITY_MODE)

VMCORE_CFLAGS_COMMON := \
    $(filter-out -I$(KERNEL_INC),$(COMMON_CFLAGS)) \
    -Iv3/include -Ikernel/include -I. -Iv3/src/word_source -Iv3/src/test_runner/include \
    -Wno-error=unused-parameter -Wno-error=shift-negative-value \
    -Wno-error=sign-compare -Wno-error=missing-field-initializers

# aarch64 loader flags: clang COFF target — drop ELF-only flags (-fPIC,
# -fvisibility=hidden), add COFF-specific flags instead.
ifeq ($(ARCH),aarch64)
LOADER_BASE_CFLAGS := \
    -std=c99 -Wall -Werror -Wextra \
    --target=aarch64-pc-windows-msvc \
    -ffreestanding -fno-builtin \
    -fno-stack-protector -fshort-wchar \
    -mno-stack-arg-probe \
    -march=armv8-a \
    -DARCH_AARCH64 \
    -I$(KERNEL_INC) -Iv3/include -Ikernel/include -Iv3/src \
    -Ikernel/include/starkernel/freestanding \
    -include $(STARFORTH_CONFIG_HEADER) \
    -DPARITY_MODE=$(PARITY_MODE) \
    -DPLATFORM_TIME_NO_INLINE
else
LOADER_BASE_CFLAGS := $(COMMON_CFLAGS)
endif

DOE_INJECT                ?= 0
DOE_REPS                  ?= 3
DOE_SEED                  ?= 12345
# EMERGENCY_CONSOLE_ENABLED is blanket-forwarded (unlike the opt-in-only
# knobs below), so it's repointed through the Kconfig bridge the same way
# as the hosted Makefile's STRICT_PTR (Phase 3) -- Phase 4 heartbeat family.
# Default flipped 0 (was 1) 2026-09-05: headless-until-login is now the
# production default (Kconfig.heartbeat's own default n, same decision).
$(eval $(call kconfig_bool,EMERGENCY_CONSOLE_ENABLED,0))
LOADER_CFLAGS := $(LOADER_BASE_CFLAGS) -I$(KERNEL_SRC)/vm
KERNEL_CFLAGS := $(COMMON_CFLAGS) -D__STARKERNEL__ -I$(KERNEL_SRC)/vm

# Linker flags
ifeq ($(ARCH),aarch64)
    LOADER_LDFLAGS_PE :=   # lld-link called directly in link rule
else ifeq ($(ARCH),riscv64)
    LOADER_LDFLAGS_PE :=   # ELF→objcopy pipeline in link rule
else
    LOADER_LINKER_SCRIPT_PE := kernel/linker/starkernel-loader-amd64-pe.ld
    LOADER_LDFLAGS_PE := \
        -T $(LOADER_LINKER_SCRIPT_PE) -m i386pep -nostdlib \
        --enable-reloc-section --image-base 0 --subsystem 10 -e efi_main
endif

LOADER_LDFLAGS := \
    -T $(LOADER_LINKER_SCRIPT) -nostdlib --build-id=none \
    --emit-relocs -shared -Bsymbolic -znocombreloc -e efi_main
KERNEL_LDFLAGS := \
    -T $(KERNEL_LINKER_SCRIPT) -nostdlib --build-id=none \
    --emit-relocs -e kernel_main

# ==============================================================================
# VM INTEGRATION FLAGS
# ==============================================================================

ifeq ($(STARFORTH_ENABLE_VM),1)
KERNEL_CFLAGS += -DSTARFORTH_ENABLE_VM=1 -DSTARFORTH_MINIMAL=1
LOADER_CFLAGS += -DSTARFORTH_ENABLE_VM=1 -DSTARFORTH_MINIMAL=1

# Physics/SSM knob family: pre-assign from the Kconfig-fed CONFIG_* value
# when a build/$(ARCH)/.config exists (Kconfig migration Phase 3). This is
# additive to, not a replacement for, this Makefile's existing opt-in
# philosophy below: a plain `:=`/derived assignment here makes
# `origin($1)` "file" (not "undefined"), so `add_vm_flag` picks it up and
# forwards it -- exactly as if a developer had passed it on the command
# line. When no .config exists (KCONFIG_ACTIVE unset), none of this fires,
# and a knob is only forwarded if a developer explicitly sets it, same as
# before this migration touched the kernel build at all. Command-line
# values always win regardless, per ordinary Make semantics.
ifeq ($(KCONFIG_ACTIVE),1)
$(eval $(call kconfig_bool,STRICT_PTR,1))
$(eval $(call kconfig_bool,ENABLE_HOTWORDS_CACHE,0))
$(eval $(call kconfig_bool,ENABLE_PIPELINING,0))
$(eval $(call kconfig_int,ROLLING_WINDOW_SIZE,4096))
$(eval $(call kconfig_int,TRANSITION_WINDOW_SIZE,8))
$(eval $(call kconfig_int,ADAPTIVE_SHRINK_RATE,50))
$(eval $(call kconfig_int,ADAPTIVE_MIN_WINDOW_SIZE,256))
$(eval $(call kconfig_int,ADAPTIVE_CHECK_FREQUENCY,512))
$(eval $(call kconfig_int,ADAPTIVE_GROWTH_THRESHOLD,5))
$(eval $(call kconfig_int,INITIAL_DECAY_SLOPE_Q48,21845))
$(eval $(call kconfig_int,DECAY_MIN_INTERVAL,500))
$(eval $(call kconfig_int,DECAY_RATE_PER_US_Q16,1))
$(eval $(call kconfig_int,HEARTBEAT_INFERENCE_FREQUENCY,1000))
$(eval $(call kconfig_int,STADIUM_MAX_VM_COUNT,4))
$(eval $(call kconfig_int,STADIUM_CONTAINS_DEPTH_MAX,5))
$(eval $(call kconfig_int,STADIUM_CAPACITY_TICK,1000))
$(eval $(call kconfig_int,STADIUM_MEMORY_PERCENT,1))
$(eval $(call kconfig_int,STADIUM_WORD_HEAT_QUANTUM,2048))
$(eval $(call kconfig_int,STADIUM_WORD_COOL_RATE_Q48,21845))
$(eval $(call kconfig_int,STADIUM_BLOCK_HEAT_QUANTUM,2048))
$(eval $(call kconfig_int,STADIUM_BLOCK_COOL_RATE_Q48,21845))
$(eval $(call kconfig_int,STADIUM_BLOCK_TRACK_CAP_MULT,2))
$(eval $(call kconfig_int,SSM_ENTROPY_HIGH_THRESHOLD,0.75))
$(eval $(call kconfig_int,SSM_CV_HIGH_THRESHOLD,0.15))
$(eval $(call kconfig_int,SSM_TEMPORAL_DECAY_THRESHOLD,0.5))
$(eval $(call kconfig_int,SSM_TEMPORAL_DECAY_LOW_THRESHOLD,0.3))
$(eval $(call kconfig_int,SSM_HYSTERESIS_TICKS,5))
# Heartbeat family (Phase 4). HEARTBEAT_THREAD_ENABLED/HEARTBEAT_TICK_NS
# `depends on STARFORTH_VARIANT_HOSTED` in Kconfig.heartbeat, so for a
# kernel-variant .config CONFIG_HEARTBEAT_THREAD_ENABLED is simply absent
# and kconfig_bool resolves it to 0 -- correctly, since LithosAnanke has no
# pthreads -- with no special case needed here. The unconditional
# `VM_FEATURE_OVERRIDES += -DHEARTBEAT_THREAD_ENABLED=0` below is kept as a
# deliberate second, independent enforcement of the same fact; see
# Kconfig.heartbeat's help text for why both layers exist.
$(eval $(call kconfig_bool,HEARTBEAT_THREAD_ENABLED,0))
$(eval $(call kconfig_int,HEARTBEAT_TICK_NS,10000))
$(eval $(call kconfig_int,HEARTBEAT_CHECK_FREQUENCY,256))
$(eval $(call kconfig_int,HEARTBEAT_WINDOW_TUNING_FREQUENCY,1000))
$(eval $(call kconfig_int,HEARTBEAT_SLOPE_VALIDATION_FREQUENCY,5000))
# Remaining physics_pipelining_metrics.h constants (Phase 5). Previously
# hardcoded, non-overridable #defines; see Kconfig.physics for the exact
# hex/decimal defaults. MINIMUM_PREFETCH_ROI's default was corrected from
# the long-shipped 0x11999A to 0x1199A (rev y) -- the former was ~17.6 in
# Q48.16, not the documented 1.10.
$(eval $(call kconfig_int,SPECULATION_THRESHOLD_Q48,0x8000))
$(eval $(call kconfig_int,SPECULATION_DEPTH,1))
$(eval $(call kconfig_int,MIN_SAMPLES_FOR_SPECULATION,10))
$(eval $(call kconfig_int,MISPREDICTION_COST_Q48,0x190000))
$(eval $(call kconfig_int,MINIMUM_PREFETCH_ROI,0x1199A))
# SK_PARITY_DEBUG (Phase 6): previously opt-in-only via VM_FEATURE_FLAG_VARS
# below with no Kconfig presence at all.
$(eval $(call kconfig_bool,SK_PARITY_DEBUG,0))
endif

VM_FEATURE_FLAG_VARS := \
    STRICT_PTR ENABLE_HOTWORDS_CACHE ENABLE_PIPELINING \
    ROLLING_WINDOW_SIZE TRANSITION_WINDOW_SIZE \
    ADAPTIVE_SHRINK_RATE ADAPTIVE_MIN_WINDOW_SIZE \
    ADAPTIVE_CHECK_FREQUENCY ADAPTIVE_GROWTH_THRESHOLD \
    INITIAL_DECAY_SLOPE_Q48 DECAY_MIN_INTERVAL \
    DECAY_RATE_PER_US_Q16 HEARTBEAT_THREAD_ENABLED \
    HEARTBEAT_TICK_NS HEARTBEAT_INFERENCE_FREQUENCY \
    HEARTBEAT_CHECK_FREQUENCY HEARTBEAT_WINDOW_TUNING_FREQUENCY \
    HEARTBEAT_SLOPE_VALIDATION_FREQUENCY SK_PARITY_DEBUG \
    EMERGENCY_CONSOLE_ENABLED STADIUM_MAX_VM_COUNT \
    STADIUM_CONTAINS_DEPTH_MAX STADIUM_CAPACITY_TICK \
    STADIUM_MEMORY_PERCENT \
    SSM_ENTROPY_HIGH_THRESHOLD SSM_CV_HIGH_THRESHOLD \
    SSM_TEMPORAL_DECAY_THRESHOLD SSM_TEMPORAL_DECAY_LOW_THRESHOLD \
    SSM_HYSTERESIS_TICKS \
    SPECULATION_THRESHOLD_Q48 SPECULATION_DEPTH \
    MIN_SAMPLES_FOR_SPECULATION MISPREDICTION_COST_Q48 \
    MINIMUM_PREFETCH_ROI

VM_FEATURE_OVERRIDES :=
define add_vm_flag
ifneq ($$(origin $1),undefined)
VM_FEATURE_OVERRIDES += -D$1=$$($1)
endif
endef
$(foreach f,$(VM_FEATURE_FLAG_VARS),$(eval $(call add_vm_flag,$f)))
VM_FEATURE_OVERRIDES += -DHEARTBEAT_THREAD_ENABLED=0

KERNEL_CFLAGS += $(VM_FEATURE_OVERRIDES)
LOADER_CFLAGS += $(VM_FEATURE_OVERRIDES)
endif

# SK_CMD — optional startup FORTH script injected before the interactive REPL.
# Usage: make -f kernel/Makefile qemu SK_CMD="TIME-TICKS . BYE"
ifdef SK_CMD
KERNEL_CFLAGS += -DSK_STARTUP_FORTH='"$(SK_CMD)"'
LOADER_CFLAGS += -DSK_STARTUP_FORTH='"$(SK_CMD)"'
endif

# KERNEL_ARGS — runtime boot flags written into starforth.cfg on the ESP image.
# The loader reads this file from the EFI partition and parses it at boot.
# Supported flags: --doe  --log-level=<debug|info|warn|error>
#                  --stack=<N>[MG]  --heap=<N>[MG]
# Usage: make -f kernel/Makefile qemu KERNEL_ARGS="--doe --log-level=info"
KERNEL_ARGS ?=

# ==============================================================================
# SOURCE FILES
# ==============================================================================

ifeq ($(MONOLITHIC),1)
LOADER_CFLAGS += -DMONOLITHIC_BUILD -D__STARKERNEL__

LOADER_SRCS_BASE := \
    $(KERNEL_SRC)/boot/uefi_loader.c \
    $(KERNEL_SRC)/boot/elf_loader.c \
    $(KERNEL_SRC)/boot/reloc_stub.c \
    $(KERNEL_SRC)/boot/cmdline.c \
    $(KERNEL_SRC)/kernel_main.c \
    $(wildcard $(KERNEL_SRC)/hal/*.c) \
    $(wildcard $(KERNEL_SRC)/memory/*.c) \
    $(wildcard $(KERNEL_SRC)/math/*.c) \
    $(wildcard $(KERNEL_SRC)/hash/*.c) \
    $(wildcard $(KERNEL_SRC)/crypto/*.c) \
    $(wildcard $(KERNEL_SRC)/capsule/*.c) \
    $(wildcard $(KERNEL_SRC)/pci/*.c) \
    $(wildcard $(KERNEL_SRC)/virtio/*.c) \
    $(wildcard $(KERNEL_SRC)/rng/*.c) \
    $(wildcard $(KERNEL_SRC)/usb/*.c) \
    $(KERNEL_SRC)/repl.c \
    $(KERNEL_SRC)/doe_log.c \
    $(KERNEL_SRC)/heartbeat.c \
    $(KERNEL_SRC)/homeblocks_sig.c \
    $(KERNEL_SRC)/artemis_sig.c \
    $(KERNEL_SRC)/log_region.c

LOADER_ASM := \
    $(KERNEL_SRC)/arch/$(ARCH)/boot.S \
    $(KERNEL_SRC)/arch/$(ARCH)/isr.S \
    $(KERNEL_SRC)/arch/$(ARCH)/switch.S

ifeq ($(ARCH),amd64)
LOADER_ASM += $(KERNEL_SRC)/arch/$(ARCH)/kernel_entry.S
endif
ifeq ($(ARCH),riscv64)
LOADER_ASM += $(KERNEL_SRC)/arch/$(ARCH)/kernel_entry.S
endif

LOADER_ARCH_SRCS := \
    $(KERNEL_SRC)/arch/$(ARCH)/arch.c \
    $(KERNEL_SRC)/arch/$(ARCH)/interrupts.c \
    $(KERNEL_SRC)/arch/$(ARCH)/apic.c \
    $(KERNEL_SRC)/arch/$(ARCH)/timer.c

ifeq ($(ARCH),amd64)
LOADER_ARCH_SRCS += \
    $(KERNEL_SRC)/arch/$(ARCH)/ioapic.c \
    $(KERNEL_SRC)/arch/$(ARCH)/i8042.c
endif
ifeq ($(ARCH),riscv64)
LOADER_ARCH_SRCS += \
    $(KERNEL_SRC)/arch/$(ARCH)/plic.c
endif
else
LOADER_SRCS_BASE := \
    $(KERNEL_SRC)/boot/uefi_loader.c \
    $(KERNEL_SRC)/boot/elf_loader.c \
    $(KERNEL_SRC)/boot/reloc_stub.c
LOADER_ASM       :=
LOADER_ARCH_SRCS := $(KERNEL_SRC)/arch/$(ARCH)/arch.c
endif

KERNEL_SRCS_BASE := \
    $(KERNEL_SRC)/kernel_main.c \
    $(KERNEL_SRC)/boot/cmdline.c \
    $(wildcard $(KERNEL_SRC)/hal/*.c) \
    $(wildcard $(KERNEL_SRC)/memory/*.c) \
    $(wildcard $(KERNEL_SRC)/math/*.c) \
    $(wildcard $(KERNEL_SRC)/hash/*.c) \
    $(wildcard $(KERNEL_SRC)/crypto/*.c) \
    $(wildcard $(KERNEL_SRC)/vm_arbiter/*.c) \
    $(wildcard $(KERNEL_SRC)/capsule/*.c) \
    $(wildcard $(KERNEL_SRC)/pci/*.c) \
    $(wildcard $(KERNEL_SRC)/virtio/*.c) \
    $(wildcard $(KERNEL_SRC)/rng/*.c) \
    $(wildcard $(KERNEL_SRC)/usb/*.c) \
    $(wildcard $(KERNEL_SRC)/arch/$(ARCH)/*.c) \
    $(KERNEL_SRC)/repl.c \
    $(KERNEL_SRC)/doe_log.c \
    $(KERNEL_SRC)/heartbeat.c \
    $(KERNEL_SRC)/homeblocks_sig.c \
    $(KERNEL_SRC)/artemis_sig.c \
    $(KERNEL_SRC)/log_region.c

KERNEL_ASM := $(wildcard $(KERNEL_SRC)/arch/$(ARCH)/*.S)

ifeq ($(STARFORTH_ENABLE_VM),1)
VM_ALL_SRCS := \
    $(wildcard v3/src/*.c) \
    $(wildcard v3/src/word_source/*.c) \
    $(wildcard v3/src/test_runner/*.c) \
    $(wildcard v3/src/test_runner/modules/*.c)

VM_EXCLUDE := \
    v3/src/main.c v3/src/cli.c v3/src/repl.c \
    v3/src/platform/% \
    v3/src/blkio_factory.c v3/src/blkio_file.c \
    v3/src/log.c v3/src/doe_metrics.c \
    v3/src/vm.c v3/src/vm_core.c v3/src/vm_bootstrap.c v3/src/vm_runtime.c v3/src/vm_time.c \
    v3/src/word_source/q48_16_words.c \
    v3/src/test_runner/modules/break_me_tests.c

VM_CORE_SRCS := $(filter-out $(VM_EXCLUDE),$(VM_ALL_SRCS))

LOADER_EXTRA_SRCS := \
    $(KERNEL_SRC)/vm/arena.c \
    $(KERNEL_SRC)/vm/switch.c \
    $(KERNEL_SRC)/vm/parity.c \
    $(KERNEL_SRC)/vm/bootstrap/sk_vm_bootstrap.c \
    $(KERNEL_SRC)/vm/host/shim.c \
    $(KERNEL_SRC)/vm/vm_core.c \
    $(KERNEL_SRC)/vm/vm_bootstrap.c \
    $(KERNEL_SRC)/vm/vm_runtime.c \
    $(KERNEL_SRC)/vm/alloc_kernel.c \
    $(KERNEL_SRC)/vm/q48_stubs.c \
    $(KERNEL_SRC)/vm/stadium.c \
    $(KERNEL_SRC)/vm/stadium_words.c \
    $(KERNEL_SRC)/vm/stadium_blocks.c \
    $(KERNEL_SRC)/vm/session.c \
    $(KERNEL_SRC)/vm/kernel_hermes.c \
    $(KERNEL_SRC)/vm/vm_log_buffer.c

KERNEL_EXTRA_SRCS := $(LOADER_EXTRA_SRCS)

LOADER_VM_OBJS := $(patsubst v3/src/%.c,$(LOADER_OBJ_DIR)/vmcore/%.o,$(VM_CORE_SRCS))
KERNEL_VM_OBJS := $(patsubst v3/src/%.c,$(KERNEL_OBJ_DIR)/vmcore/%.o,$(VM_CORE_SRCS))
endif

# ------------------------------------------------------------------------------
# StarForth v4 (STARFORTH_V4=1): the golden model of the F18-derived engine,
# v4/src, with its ROM and the memory map's description of the system, which
# v4's own Makefile builds on this machine (docs/v4.0.0/MACHINE.md).  The ROM
# assembles the system's source, capsules/v4/sys, from this kernel's capsule
# directory when the machine starts.  The node is the host node's size, with
# 64-bit cells, as every ISA this kernel boots on has (DECOMPOSITION.md D-5),
# so the ROM and the map are the same files for all three.
# ------------------------------------------------------------------------------
# V4_DEFAULT: with v4 built in, whether it runs when the boot arguments
# name no system (A6, 2026-10-14). 1 -- v4 -- for every build: QEMU and the
# board images alike (Captain Bob, 2026-10-14: "v4 is the default build ...
# we are effectively trying to retire v3"); the board images also write
# `system=v4` into their boot arguments. `system=v3` boots v3.
V4_DEFAULT ?= 1
ifeq ($(STARFORTH_V4),1)
V4_DEFS := -DSTARFORTH_V4=1 -DSK_V4_DEFAULT=$(V4_DEFAULT) -Iv4/include \
    -DV4_CELL_BITS=64 -DV4_NODE_WORDS=16384 -DV4_DATA_RING=30 -DV4_RET_RING=31 -DV4_ROM_WORDS=4096
KERNEL_CFLAGS += $(V4_DEFS)
LOADER_CFLAGS += $(V4_DEFS)

V4_ENGINE_SRCS := $(addprefix v4/src/,node.c exec.c stack.c iword.c heat.c guard.c image.c fabric.c capsule.c message.c wire.c)
V4_ROM_C       := $(BUILD_DIR)/v4_rom_64.c
V4_MAP_C       := $(BUILD_DIR)/v4_map_64.c

LOADER_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c
KERNEL_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c
# v4/system/boot.c is the boot the hosted v4 system runs too: the ROM
# assembles the system, then the capsules from this kernel's own capsule
# directory, then the prompt.
LOADER_V4_OBJS := $(patsubst v4/src/%.c,$(LOADER_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(LOADER_OBJ_DIR)/v4engine/v4_rom.o $(LOADER_OBJ_DIR)/v4engine/v4_map.o $(LOADER_OBJ_DIR)/v4system/boot.o $(LOADER_OBJ_DIR)/v4system/blocks.o $(LOADER_OBJ_DIR)/v4system/post.o $(LOADER_OBJ_DIR)/v4system/post_cases.o
KERNEL_V4_OBJS := $(patsubst v4/src/%.c,$(KERNEL_OBJ_DIR)/v4engine/%.o,$(V4_ENGINE_SRCS)) $(KERNEL_OBJ_DIR)/v4engine/v4_rom.o $(KERNEL_OBJ_DIR)/v4engine/v4_map.o $(KERNEL_OBJ_DIR)/v4system/boot.o $(KERNEL_OBJ_DIR)/v4system/blocks.o $(KERNEL_OBJ_DIR)/v4system/post.o $(KERNEL_OBJ_DIR)/v4system/post_cases.o
endif

LOADER_SRCS := $(LOADER_SRCS_BASE) $(LOADER_EXTRA_SRCS)
KERNEL_SRCS := $(KERNEL_SRCS_BASE) $(KERNEL_EXTRA_SRCS)

LOADER_OBJS := \
    $(patsubst $(KERNEL_SRC)/%.c,$(LOADER_OBJ_DIR)/%.o,$(LOADER_SRCS)) \
    $(patsubst $(KERNEL_SRC)/%.c,$(LOADER_OBJ_DIR)/%.o,$(LOADER_ARCH_SRCS)) \
    $(patsubst $(KERNEL_SRC)/%.S,$(LOADER_OBJ_DIR)/%.o,$(LOADER_ASM)) \
    $(LOADER_VM_OBJS) \
    $(LOADER_V4_OBJS) \
    $(CAPSULE_GENERATED_OBJ)

KERNEL_OBJS := \
    $(patsubst $(KERNEL_SRC)/%.c,$(KERNEL_OBJ_DIR)/%.o,$(KERNEL_SRCS)) \
    $(patsubst $(KERNEL_SRC)/%.S,$(KERNEL_OBJ_DIR)/%.o,$(KERNEL_ASM)) \
    $(KERNEL_VM_OBJS) \
    $(KERNEL_V4_OBJS) \
    $(CAPSULE_GENERATED_KOBJ)

# ==============================================================================
# PHONY TARGETS
# ==============================================================================

.PHONY: all clean clean-kernel kernel kernel-all
.PHONY: qemu qemu-esp qemu-gdb
.PHONY: thumbdrive iso-usb
.PHONY: info help
.PHONY: FORCE

# ==============================================================================
# MAIN TARGETS
# ==============================================================================

all: v3/include/version.h $(CAPSULE_GENERATED_OBJ) $(CAPSULE_GENERATED_KOBJ) $(LOADER_EFI) $(KERNEL_ELF)
	@echo "StarKernel built successfully for $(ARCH)"
	@echo "  Loader: $(LOADER_EFI)"
	@echo "  Kernel: $(KERNEL_ELF)"

kernel: all

kernel-all:
	@$(MAKE) -f kernel/Makefile ARCH=amd64   TARGET=$(TARGET)
	@$(MAKE) -f kernel/Makefile ARCH=aarch64 TARGET=$(TARGET)
	@$(MAKE) -f kernel/Makefile ARCH=riscv64 TARGET=$(TARGET)

clean:
	rm -rf build/$(ARCH)/$(TARGET)
	rm -f v3/include/version.h

clean-kernel:
	@echo "Cleaning all StarKernel build artifacts..."
	@rm -rf build/*/kernel
	@echo "Done."

# ==============================================================================
# BUILD RULES
# ==============================================================================

FORCE:

# FORCE prerequisite (matches the hosted Makefile's own pattern, Makefile:613/626):
# this file is also written by the hosted Makefile with different, incompatible
# content (no LITHOS_VERSION/LITHOS_VERSION_STR) -- without FORCE, a bare build
# here after a hosted `make` run silently reuses that stale/wrong-flavored file
# and fails with "LITHOS_VERSION_STR undeclared" deep in kernel_main.c, instead
# of regenerating its own correct version.
v3/include/version.h: FORCE
	@mkdir -p v3/include
	@BUILD_TS=$$(date -Iseconds 2>/dev/null || echo "unknown"); \
	printf '#ifndef STARFORTH_VERSION_H\n#define STARFORTH_VERSION_H\n\n' > $@; \
	printf '#define STARFORTH_VERSION "%s"\n' "$(VERSION)" >> $@; \
	printf '#define STARFORTH_ARCH "%s"\n' "$(ARCH)" >> $@; \
	printf '#define STARFORTH_TARGET "%s"\n' "kernel" >> $@; \
	printf '#define STARFORTH_TIMESTAMP "%s"\n' "$$BUILD_TS" >> $@; \
	printf '#define STARFORTH_VERSION_FULL "StarForth v%s %s kernel %s"\n\n' "$(VERSION)" "$(ARCH)" "$$BUILD_TS" >> $@; \
	printf '#define LITHOS_VERSION "%s"\n' "$(LITHOS_VERSION)" >> $@; \
	printf '#define LITHOS_VERSION_STR "LithosAnanke v%s %s"\n\n' "$(LITHOS_VERSION)" "$(ARCH)" >> $@; \
	printf '#endif /* STARFORTH_VERSION_H */\n' >> $@

# Host tool: capsule packer
$(MKCAPSULE_BIN): $(MKCAPSULE_SRC)
	@mkdir -p $(dir $@)
	@echo "HOSTCC  $(MKCAPSULE_SRC)"
	@cc -std=c99 -Wall -Wextra -O2 -Iv3/include -Ikernel/include -Itools -o $@ $(MKCAPSULE_SRC)

# Generate capsule_generated.c from capsules/
CAPSULE_SRCS := $(shell find $(CAPSULES_DIR) -type f ! -name '.*' 2>/dev/null)
$(CAPSULE_GENERATED): $(MKCAPSULE_BIN) $(CAPSULE_SRCS)
	@mkdir -p $(dir $@)
	@echo "  MKCAP   $(CAPSULES_DIR) -> $@"
	@$(MKCAPSULE_BIN) $(SIGN_KEY_ARGS) $(CAPSULES_DIR) $@
	@echo "  MKCAP   $(CAPSULES_DIR) -> $(CAPSULES_DIR)/BLOCK_MAP.md"
	@$(MKCAPSULE_BIN) --manifest $(SIGN_KEY_ARGS) $(CAPSULES_DIR) $(CAPSULES_DIR)/BLOCK_MAP.md

# manifest — regenerate capsules/BLOCK_MAP.md without a full kernel build
.PHONY: manifest
manifest: $(MKCAPSULE_BIN)
	@$(MKCAPSULE_BIN) --manifest $(SIGN_KEY_ARGS) $(CAPSULES_DIR) $(CAPSULES_DIR)/BLOCK_MAP.md
	@echo "  Wrote $(CAPSULES_DIR)/BLOCK_MAP.md"

$(CAPSULE_GENERATED_OBJ): $(CAPSULE_GENERATED)
	@echo "  CC  $<"
	@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@

$(CAPSULE_GENERATED_KOBJ): $(CAPSULE_GENERATED) | $(KERNEL_OBJ_DIR)
	@echo "  CC (kernel)  $<"
	@$(CC) $(KERNEL_CFLAGS) -c $< -o $@

# Create build directories
$(BUILD_DIR) $(LOADER_OBJ_DIR) $(KERNEL_OBJ_DIR):
	@mkdir -p \
	    $(LOADER_OBJ_DIR)/boot $(LOADER_OBJ_DIR)/arch/$(ARCH) \
	    $(LOADER_OBJ_DIR)/vm  $(LOADER_OBJ_DIR)/capsule $(LOADER_OBJ_DIR)/hash
	@mkdir -p \
	    $(KERNEL_OBJ_DIR)/boot $(KERNEL_OBJ_DIR)/hal $(KERNEL_OBJ_DIR)/memory \
	    $(KERNEL_OBJ_DIR)/vm_arbiter $(KERNEL_OBJ_DIR)/capsule \
	    $(KERNEL_OBJ_DIR)/hash $(KERNEL_OBJ_DIR)/arch/$(ARCH) $(KERNEL_OBJ_DIR)/vm

# Compile loader C sources
$(LOADER_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.c | $(LOADER_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "CC (loader) $<"
	@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@

# Compile loader VM core sources
$(LOADER_OBJ_DIR)/vmcore/%.o: v3/src/%.c v3/include/version.h | $(LOADER_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "CC (loader) $<"
ifeq ($(ARCH),aarch64)
	@$(LOADER_CC) $(LOADER_CFLAGS) \
	    -Iv3/include -Ikernel/include -I. -Iv3/src/test_runner/include \
	    -Wno-error=unused-parameter -Wno-error=shift-negative-value \
	    -Wno-error=sign-compare -Wno-error=missing-field-initializers \
	    -Wno-unused-function \
	    -c $< -o $@
else
	@$(LOADER_CC) $(VMCORE_CFLAGS_COMMON) $(filter-out -I$(KERNEL_INC),$(LOADER_CFLAGS)) -c $< -o $@
endif

# StarForth v4: the ROM and the map, built on this machine by v4's Makefile,
# and the engine, compiled like any other kernel source.
ifeq ($(STARFORTH_V4),1)
$(V4_ROM_C) $(V4_MAP_C): FORCE
	@mkdir -p $(dir $@)
	@echo "  V4ROM   capsules/v4/rom -> $@"
	@$(MAKE) --no-print-directory -C v4 CC=cc $(CURDIR)/v4/build/rom-64.c $(CURDIR)/v4/build/v4_map_64.c
	@cmp -s v4/build/rom-64.c $(V4_ROM_C) || cp v4/build/rom-64.c $(V4_ROM_C)
	@cmp -s v4/build/v4_map_64.c $(V4_MAP_C) || cp v4/build/v4_map_64.c $(V4_MAP_C)

# The system's memory map, capsules/v4/sys/map-<w>.f18, is generated from
# the same description as the ROM and v4_map_image (v4/tests/host_map.h), and
# the ROM assembles it from this kernel's capsule directory: it is made
# again before the directory is packed, so the three always agree.
$(CAPSULES_DIR)/v4/sys/map-64.f18 $(CAPSULES_DIR)/v4/sys/map-32.f18: FORCE
	@$(MAKE) --no-print-directory -C v4 CC=cc sysmap

$(LOADER_OBJ_DIR)/v4engine/v4_rom.o: $(V4_ROM_C) | $(LOADER_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "CC (loader) $<"
	@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
$(KERNEL_OBJ_DIR)/v4engine/v4_rom.o: $(V4_ROM_C) | $(KERNEL_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "CC (kernel) $<"
	@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
$(LOADER_OBJ_DIR)/v4engine/v4_map.o: $(V4_MAP_C) | $(LOADER_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "CC (loader) $<"
	@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
$(KERNEL_OBJ_DIR)/v4engine/v4_map.o: $(V4_MAP_C) | $(KERNEL_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "CC (kernel) $<"
	@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
$(LOADER_OBJ_DIR)/v4system/%.o: v4/system/%.c | $(LOADER_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "CC (loader) $<"
	@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
$(KERNEL_OBJ_DIR)/v4system/%.o: v4/system/%.c | $(KERNEL_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "CC (kernel) $<"
	@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
$(LOADER_OBJ_DIR)/v4engine/%.o: v4/src/%.c | $(LOADER_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "CC (loader) $<"
	@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@
$(KERNEL_OBJ_DIR)/v4engine/%.o: v4/src/%.c | $(KERNEL_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "CC (kernel) $<"
	@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
endif

# Compile loader assembly sources
$(LOADER_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.S | $(LOADER_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "AS (loader) $<"
	@$(LOADER_CC) $(LOADER_CFLAGS) -c $< -o $@

# Compile kernel C sources
$(KERNEL_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.c | $(KERNEL_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "CC (kernel) $<"
	@$(CC) $(KERNEL_CFLAGS) -c $< -o $@

# Compile kernel VM core sources
$(KERNEL_OBJ_DIR)/vmcore/%.o: v3/src/%.c v3/include/version.h | $(KERNEL_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "CC (kernel) $<"
	@$(CC) $(VMCORE_CFLAGS_COMMON) $(filter-out -I$(KERNEL_INC),$(KERNEL_CFLAGS)) -Ikernel/src/vm -c $< -o $@

# Compile kernel assembly sources
$(KERNEL_OBJ_DIR)/%.o: $(KERNEL_SRC)/%.S | $(KERNEL_OBJ_DIR)
	@mkdir -p $(dir $@)
	@echo "AS (kernel) $<"
	@$(CC) $(KERNEL_CFLAGS) -c $< -o $@

# ==============================================================================
# LINK RULES
# ==============================================================================

# Link loader to PE32+
# amd64:   ld -m i386pep → PE directly
# aarch64: lld-link links COFF objects → PE (auto-generates .reloc)
# riscv64: ELF → gen_pe_reloc.py patches addends → objcopy → pei-riscv64-little
$(LOADER_EFI): $(LOADER_OBJS) | $(BUILD_DIR)
ifeq ($(ARCH),aarch64)
	@echo "LLD-LINK (loader PE) $@"
	@$(LOADER_LD) /subsystem:EFI_APPLICATION /entry:efi_main /nodefaultlib \
	    /align:4096 /base:0 /out:$@ $(LOADER_OBJS)
	@echo "Loader built: $@"
else ifeq ($(ARCH),riscv64)
	@echo "LD+OBJCOPY (loader ELF→PE) $@"
	@$(LD) -T $(LOADER_LINKER_SCRIPT) -nostdlib --build-id=none \
	    -shared -Bsymbolic -znocombreloc -e efi_main -m elf64lriscv \
	    $(LOADER_OBJS) -o $(BUILD_DIR)/starkernel_loader_tmp.elf
	@python3 scripts/gen_pe_reloc.py \
	    $(BUILD_DIR)/starkernel_loader_tmp.elf \
	    $(BUILD_DIR)/starkernel_loader_patched.elf \
	    $(BUILD_DIR)/reloc_rv64.bin
	@$(OBJCOPY) \
	    --remove-section=.rela.dyn \
	    --remove-section=.dynstr \
	    --remove-section=.hash \
	    --remove-section=.gnu.hash \
	    --remove-section=.dynsym \
	    --remove-section=.dynamic \
	    --remove-section=.riscv.attributes \
	    --update-section=.reloc=$(BUILD_DIR)/reloc_rv64.bin \
	    $(BUILD_DIR)/starkernel_loader_patched.elf \
	    $(BUILD_DIR)/starkernel_loader_stripped.elf
	@$(OBJCOPY) -O pei-riscv64-little --subsystem 10 \
	    $(BUILD_DIR)/starkernel_loader_stripped.elf $@
	@echo "Loader built: $@"
else
	@echo "LD (loader PE) $@"
	@$(LD) $(LOADER_LDFLAGS_PE) $(LOADER_OBJS) -o $@
	@echo "Loader built: $@"
endif

# Keep ELF for debugging
$(BUILD_DIR)/starkernel_loader.elf: $(LOADER_OBJS) $(LOADER_LINKER_SCRIPT) | $(BUILD_DIR)
	@echo "LD (loader ELF) $@"
	@$(LD) $(LOADER_LDFLAGS) $(LOADER_OBJS) -o $@

# Link kernel ELF
$(KERNEL_ELF): $(KERNEL_OBJS) $(KERNEL_LINKER_SCRIPT) | $(BUILD_DIR)
	@echo "LD (kernel) $@"
	@$(LD) $(KERNEL_LDFLAGS) $(KERNEL_OBJS) -o $@
	@echo "Kernel built: $@"

# ==============================================================================
# QEMU TARGETS
# ==============================================================================

# qemu — full build, pack boot media, boot under QEMU, serial → timestamped log
#
# amd64:   El Torito ISO + OVMF pflash
# aarch64: El Torito ISO + AAVMF -bios
# riscv64: GPT/FAT32 disk image + EDK2 pflash (cdrom/bios don't work on virt)
#
# Requires: qemu-system-* ovmf/qemu-efi-* xorriso mtools gdisk dosfstools
OVMF_CODE    := /usr/share/OVMF/OVMF_CODE_4M.fd
OVMF_VARS_RO := /usr/share/OVMF/OVMF_VARS_4M.fd

# QEMU_DISPLAY — window backend for the framebuffer console (gtk/sdl/none/...).
# Usage: make -f kernel/Makefile qemu QEMU_DISPLAY=none   # headless/CI
QEMU_DISPLAY ?= gtk

# QEMU_EXTRA — extra raw qemu-system-* args appended verbatim to every arch's
# qemu invocation below. Empty by default (no behavior change for a normal
# boot). Existing use: attaching a disposable USB test image for WRITE(10)
# validation, e.g.
#   QEMU_EXTRA="-drive if=none,id=usbtest,file=disk/usbwrite-test.img,format=raw \
#               -device usb-storage,bus=xhci0.0,port=2,drive=usbtest"
# Give every extra usb-storage device its own explicit port= (ZUSEDISK_QEMU_ARGS
# above already claims port 1), distinct from every other device attached on
# bus=xhci0.0 -- without it, QEMU's own port auto-assignment fills the
# controller's real top-level ports in device order and then silently drops
# any further device behind an auto-inserted internal USB2 hub, which the
# xHCI/BOT driver correctly refuses to treat as Mass Storage (confirmed live,
# 2026-09-06, root cause of the FABRIC-3.md §IX.5 "4th-device enumeration
# failure" -- it was never a driver bug, the emulated controller only exposes
# 4 real ports for p2=4,p3=4 despite the port count parameters implying 8).
# The qemu-xhci controller below is sized via XHCI_PORTS (see its own doc comment
# above) to give real ports to Zuse plus every identity drive attached this way.
QEMU_EXTRA ?=

qemu: all
ifeq ($(ARCH),amd64)
	@echo "=== Building ISO (amd64) ==="
	@mkdir -p $(BUILD_DIR)/iso
	@dd if=/dev/zero of=$(BUILD_DIR)/iso/efi.img bs=512 count=8192 2>/dev/null
	@mformat -i $(BUILD_DIR)/iso/efi.img ::
	@mmd     -i $(BUILD_DIR)/iso/efi.img ::/EFI
	@mmd     -i $(BUILD_DIR)/iso/efi.img ::/EFI/BOOT
	@mcopy   -i $(BUILD_DIR)/iso/efi.img $(LOADER_EFI) ::/EFI/BOOT/BOOTX64.EFI
	$(if $(KERNEL_ARGS),\
		@printf '$(KERNEL_ARGS)\n' > $(BUILD_DIR)/iso/starforth.cfg && \
		mcopy -i $(BUILD_DIR)/iso/efi.img $(BUILD_DIR)/iso/starforth.cfg ::/starforth.cfg,)
	@xorriso -as mkisofs -r -J \
		-e efi.img -no-emul-boot \
		-o $(QEMU_ISO) $(BUILD_DIR)/iso 2>&1
	@if [ ! -f $(OVMF_CODE) ];    then echo "Error: $(OVMF_CODE) not found";    exit 1; fi
	@if [ ! -f $(OVMF_VARS_RO) ]; then echo "Error: $(OVMF_VARS_RO) not found"; exit 1; fi
	@if [ ! -f $(BUILD_DIR)/OVMF_VARS.fd ]; then \
		cp $(OVMF_VARS_RO) $(BUILD_DIR)/OVMF_VARS.fd; \
	fi
	@mkdir -p $(QEMU_LOG_DIR) $(DOE_RUNS_DIR) $(DOE_LATEST_DIR)
	@TS=$$(date +%Y%m%d-%H%M%S); \
	SESSION=$(QEMU_LOG_DIR)/$$TS; \
	mkdir -p $$SESSION/amd64; \
	LOG=$$SESSION/amd64/qemu-amd64-$$TS.log; \
	CSV=$(DOE_RUNS_DIR)/doe-amd64-$$TS.csv; \
	SERIAL_SOCK=$(BUILD_DIR)/qemu-serial-amd64-$$TS.sock; \
	QMP_SOCK=$(BUILD_DIR)/qemu-qmp-amd64-$$TS.sock; \
	echo "=== Launching QEMU (amd64) ==="; \
	echo "  Serial log: $$LOG"; \
	echo "  QMP socket: $$QMP_SOCK"; \
	echo "  (interactive — close the window or QEMU monitor 'quit' to exit)"; \
	echo ""; \
	touch $$LOG; \
	tail -n +1 -f $$LOG & TAILPID=$$!; \
	qemu-system-x86_64 \
		-machine q35,accel=tcg \
		-cpu qemu64 \
		-m 1024 \
		-vga std \
		-drive if=pflash,format=raw,readonly=on,file=$(OVMF_CODE) \
		-drive if=pflash,format=raw,file=$(BUILD_DIR)/OVMF_VARS.fd \
		-cdrom $(QEMU_ISO) -boot d \
		-drive id=artdisk,file=$(ARTDISK),format=raw,if=none \
		-device virtio-blk-pci,drive=artdisk \
		$(XHCI_ARGS_ROOT) \
		$(ZUSEDISK_QEMU_ARGS) \
		-object rng-random,id=rng0,filename=/dev/urandom \
		-device virtio-rng-pci,rng=rng0 \
		-chardev socket,id=cserial,path=$$SERIAL_SOCK,server=on,wait=off,logfile=$$LOG \
		-serial chardev:cserial \
		-qmp unix:$$QMP_SOCK,server=on,wait=off \
		$(QEMU_EXTRA) \
		-display $(QEMU_DISPLAY) \
		-no-reboot; \
	kill $$TAILPID 2>/dev/null; wait $$TAILPID 2>/dev/null || true; \
	rm -f $$SERIAL_SOCK $$QMP_SOCK; \
	echo ""; \
	echo "=== Serial log: $$LOG ==="; \
	echo "=== Extracting DOE CSV ==="; \
	bash scripts/extract_doe.sh $$LOG $$CSV || true; \
	cp $$CSV $(DOE_LATEST_DIR)/amd64.csv 2>/dev/null || true

else ifeq ($(ARCH),aarch64)
	@echo "=== Building ISO (aarch64) ==="
	@mkdir -p $(BUILD_DIR)/iso/EFI/BOOT
	@cp $(LOADER_EFI) $(BUILD_DIR)/iso/EFI/BOOT/BOOTAA64.EFI
	@printf "FS0:\\EFI\\BOOT\\BOOTAA64.EFI\r\n" > $(BUILD_DIR)/iso/startup.nsh
	@dd if=/dev/zero of=$(BUILD_DIR)/iso/efi.img bs=512 count=8192 2>/dev/null
	@mformat -i $(BUILD_DIR)/iso/efi.img ::
	@mmd     -i $(BUILD_DIR)/iso/efi.img ::/EFI
	@mmd     -i $(BUILD_DIR)/iso/efi.img ::/EFI/BOOT
	@mcopy   -i $(BUILD_DIR)/iso/efi.img $(LOADER_EFI) ::/EFI/BOOT/BOOTAA64.EFI
	$(if $(KERNEL_ARGS),\
		@printf '$(KERNEL_ARGS)\n' > $(BUILD_DIR)/iso/starforth.cfg && \
		mcopy -i $(BUILD_DIR)/iso/efi.img $(BUILD_DIR)/iso/starforth.cfg ::/starforth.cfg,)
	@xorriso -as mkisofs -r -J \
		-e efi.img -no-emul-boot \
		-o $(QEMU_ISO) $(BUILD_DIR)/iso 2>&1
	@mkdir -p $(QEMU_LOG_DIR) $(DOE_RUNS_DIR) $(DOE_LATEST_DIR)
	@if [ -f /usr/share/AAVMF/AAVMF_CODE.fd ]; then \
		AAVMF_CODE=/usr/share/AAVMF/AAVMF_CODE.fd; \
		AAVMF_VARS_RO=/usr/share/AAVMF/AAVMF_VARS.fd; \
	elif [ -f /usr/share/qemu-efi-aarch64/QEMU_EFI.fd ]; then \
		AAVMF_CODE=/usr/share/qemu-efi-aarch64/QEMU_EFI.fd; \
		AAVMF_VARS_RO=/usr/share/qemu-efi-aarch64/vars-template-pflash.raw; \
	elif [ -f /usr/share/qemu/edk2-aarch64-code.fd ]; then \
		AAVMF_CODE=/usr/share/qemu/edk2-aarch64-code.fd; \
		AAVMF_VARS_RO=/usr/share/qemu/edk2-aarch64-vars.fd; \
	else \
		echo "Error: aarch64 UEFI firmware not found. Install qemu-efi-aarch64."; \
		exit 1; \
	fi; \
	if [ -f "$$AAVMF_VARS_RO" ]; then \
		if [ ! -f $(BUILD_DIR)/AAVMF_VARS.fd ]; then \
			cp $$AAVMF_VARS_RO $(BUILD_DIR)/AAVMF_VARS.fd; \
		fi; \
		AAVMF_VARS=$(BUILD_DIR)/AAVMF_VARS.fd; \
	else \
		echo "  (no split VARS template found for this firmware -- NVRAM will not persist across runs)"; \
		AAVMF_VARS=""; \
	fi; \
	TS=$$(date +%Y%m%d-%H%M%S); \
	SESSION=$(QEMU_LOG_DIR)/$$TS; \
	mkdir -p $$SESSION/aarch64; \
	LOG=$$SESSION/aarch64/qemu-aarch64-$$TS.log; \
	CSV=$(DOE_RUNS_DIR)/doe-aarch64-$$TS.csv; \
	SERIAL_SOCK=$(BUILD_DIR)/qemu-serial-aarch64-$$TS.sock; \
	QMP_SOCK=$(BUILD_DIR)/qemu-qmp-aarch64-$$TS.sock; \
	echo "=== Launching QEMU (aarch64) ==="; \
	echo "  ISO: $(QEMU_ISO)"; \
	echo "  Serial log: $$LOG"; \
	echo "  QMP socket: $$QMP_SOCK"; \
	echo "  (interactive — close the window or QEMU monitor 'quit' to exit)"; \
	echo ""; \
	touch $$LOG; \
	tail -n +1 -f $$LOG & TAILPID=$$!; \
	qemu-system-aarch64 \
		-machine virt \
		-cpu cortex-a57 \
		$$(if [ -n "$$AAVMF_VARS" ]; then \
			echo "-drive if=pflash,format=raw,readonly=on,file=$$AAVMF_CODE -drive if=pflash,format=raw,file=$$AAVMF_VARS"; \
		else \
			echo "-bios $$AAVMF_CODE"; \
		fi) \
		-drive if=virtio,format=raw,file=$(QEMU_ISO),media=cdrom \
		-drive id=artdisk,file=$(ARTDISK),format=raw,if=none \
		-device virtio-blk-pci,drive=artdisk \
		-device virtio-keyboard-pci,addr=0x3 \
		$(XHCI_ARGS_SLOT4) \
		$(ZUSEDISK_QEMU_ARGS) \
		-object rng-random,id=rng0,filename=/dev/urandom \
		-device virtio-rng-pci,rng=rng0,addr=0x5 \
		-device ramfb \
		-chardev socket,id=cserial,path=$$SERIAL_SOCK,server=on,wait=off,logfile=$$LOG \
		-serial chardev:cserial \
		-qmp unix:$$QMP_SOCK,server=on,wait=off \
		$(QEMU_EXTRA) \
		-display $(QEMU_DISPLAY) \
		-m 4096 \
		-no-reboot \
		-d guest_errors; \
	kill $$TAILPID 2>/dev/null; wait $$TAILPID 2>/dev/null || true; \
	rm -f $$SERIAL_SOCK $$QMP_SOCK; \
	echo ""; \
	echo "=== Serial log: $$LOG ==="; \
	echo "=== Extracting DOE CSV ==="; \
	bash scripts/extract_doe.sh $$LOG $$CSV || true; \
	cp $$CSV $(DOE_LATEST_DIR)/aarch64.csv 2>/dev/null || true

else ifeq ($(ARCH),riscv64)
	@echo "=== Building GPT disk image (riscv64) ==="
	@# riscv64 QEMU virt: pflash firmware + virtio-blk-pci GPT disk.
	@# El Torito ISO and -bios mode do NOT work on riscv64 virt.
	@mkdir -p $(QEMU_LOG_DIR) $(DOE_RUNS_DIR) $(DOE_LATEST_DIR)
	@if ! which sgdisk >/dev/null 2>&1; then echo "Error: sgdisk not found. Install gdisk (apt-get install gdisk)."; exit 1; fi
	@if ! which mkfs.fat >/dev/null 2>&1; then echo "Error: mkfs.fat not found. Install dosfstools (apt-get install dosfstools)."; exit 1; fi
	@if [ -f /usr/share/qemu-efi-riscv64/RISCV_VIRT_CODE.fd ]; then \
		EDK2_RISCV_CODE=/usr/share/qemu-efi-riscv64/RISCV_VIRT_CODE.fd; \
	elif [ -f /usr/share/qemu/edk2-riscv64-code.fd ]; then \
		EDK2_RISCV_CODE=/usr/share/qemu/edk2-riscv64-code.fd; \
	else \
		echo "Error: riscv64 UEFI firmware not found. Install qemu-efi-riscv64."; \
		exit 1; \
	fi; \
	EDK2_RISCV_VARS=""; \
	if [ -f /usr/share/qemu-efi-riscv64/RISCV_VIRT_VARS.fd ]; then \
		if [ ! -f $(BUILD_DIR)/RISCV_VARS.fd ]; then \
			cp /usr/share/qemu-efi-riscv64/RISCV_VIRT_VARS.fd $(BUILD_DIR)/RISCV_VARS.fd; \
		fi; \
		EDK2_RISCV_VARS=$(BUILD_DIR)/RISCV_VARS.fd; \
	fi; \
	DISK=$(BUILD_DIR)/starkernel_rv64.img; \
	dd if=/dev/zero of=$$DISK bs=1M count=64 2>/dev/null; \
	sgdisk -n 1:2048:131038 -t 1:ef00 -c 1:"EFI System" $$DISK >/dev/null 2>&1; \
	dd if=/dev/zero of=$(BUILD_DIR)/esp_rv64.img bs=512 count=128991 2>/dev/null; \
	mkfs.fat -F 32 -n "ESP" $(BUILD_DIR)/esp_rv64.img >/dev/null 2>&1; \
	mmd   -i $(BUILD_DIR)/esp_rv64.img ::/EFI; \
	mmd   -i $(BUILD_DIR)/esp_rv64.img ::/EFI/BOOT; \
	mcopy -i $(BUILD_DIR)/esp_rv64.img $(LOADER_EFI) ::/EFI/BOOT/BOOTRISCV64.EFI; \
	mcopy -i $(BUILD_DIR)/esp_rv64.img $(KERNEL_ELF) ::/kernel.elf; \
	printf 'FS0:\\EFI\\BOOT\\BOOTRISCV64.EFI\r\n' > $(BUILD_DIR)/startup.nsh; \
	mcopy -i $(BUILD_DIR)/esp_rv64.img $(BUILD_DIR)/startup.nsh ::/startup.nsh; \
	if [ -n "$(KERNEL_ARGS)" ]; then \
		printf '$(KERNEL_ARGS)\n' > $(BUILD_DIR)/starforth.cfg; \
		mcopy -i $(BUILD_DIR)/esp_rv64.img $(BUILD_DIR)/starforth.cfg ::/starforth.cfg; \
	fi; \
	dd if=$(BUILD_DIR)/esp_rv64.img of=$$DISK bs=512 seek=2048 conv=notrunc 2>/dev/null; \
	echo "  Disk: $$DISK"; \
	echo ""; \
	PFLASH_VARS_ARG=""; \
	if [ -n "$$EDK2_RISCV_VARS" ]; then \
		PFLASH_VARS_ARG="-drive if=pflash,format=raw,unit=1,file=$$EDK2_RISCV_VARS"; \
	fi; \
	TS=$$(date +%Y%m%d-%H%M%S); \
	SESSION=$(QEMU_LOG_DIR)/$$TS; \
	mkdir -p $$SESSION/riscv64; \
	LOG=$$SESSION/riscv64/qemu-riscv64-$$TS.log; \
	CSV=$(DOE_RUNS_DIR)/doe-riscv64-$$TS.csv; \
	SERIAL_SOCK=$(BUILD_DIR)/qemu-serial-riscv64-$$TS.sock; \
	QMP_SOCK=$(BUILD_DIR)/qemu-qmp-riscv64-$$TS.sock; \
	echo "=== Launching QEMU (riscv64) ==="; \
	echo "  Serial log: $$LOG"; \
	echo "  QMP socket: $$QMP_SOCK"; \
	echo "  (interactive — close the window or QEMU monitor 'quit' to exit)"; \
	echo ""; \
	touch $$LOG; \
	tail -n +1 -f $$LOG & TAILPID=$$!; \
	qemu-system-riscv64 \
		-machine virt \
		-cpu rv64 \
		-m 1024 \
		-drive if=pflash,format=raw,unit=0,file=$$EDK2_RISCV_CODE,readonly=on \
		$$PFLASH_VARS_ARG \
		-drive id=hd0,file=$$DISK,format=raw,if=none \
		-device virtio-blk-pci,drive=hd0,addr=0x2 \
		-drive id=artdisk,file=$(ARTDISK),format=raw,if=none \
		-device virtio-blk-pci,drive=artdisk,addr=0x1 \
		-device virtio-keyboard-pci,addr=0x3 \
		$(XHCI_ARGS_SLOT4) \
		$(ZUSEDISK_QEMU_ARGS) \
		-object rng-random,id=rng0,filename=/dev/urandom \
		-device virtio-rng-pci,rng=rng0,addr=0x5 \
		-device ramfb \
		-chardev socket,id=cserial,path=$$SERIAL_SOCK,server=on,wait=off,logfile=$$LOG \
		-serial chardev:cserial \
		-qmp unix:$$QMP_SOCK,server=on,wait=off \
		$(QEMU_EXTRA) \
		-display $(QEMU_DISPLAY) \
		-no-reboot \
		-d guest_errors; \
	kill $$TAILPID 2>/dev/null; wait $$TAILPID 2>/dev/null || true; \
	rm -f $$SERIAL_SOCK $$QMP_SOCK; \
	echo ""; \
	echo "=== Serial log: $$LOG ==="; \
	echo "=== Extracting DOE CSV ==="; \
	bash scripts/extract_doe.sh $$LOG $$CSV || true; \
 	cp $$CSV $(DOE_LATEST_DIR)/riscv64.csv 2>/dev/null || true
 endif

# thumbdrive — build a generic UEFI-bootable GPT/FAT32 disk image that can be
# written directly to a USB thumbdrive (dd) and boots on real amd64 UEFI
# hardware (e.g. Beelink SER5) as well as under QEMU. This is the "generic
# thumbdrive bootable OS" image for the v2.5.0/SER5 bare-metal path: the
# monolithic loader embeds the whole kernel, so the ESP needs only the UEFI
# fallback boot path EFI/BOOT/BOOT<ARCH>.EFI. Storage on real hardware is the
# USB BOT/xHCI path (already live), not virtio.
thumbdrive: all
	@printf 'FS0:\\EFI\\BOOT\\%s\r\n' "$(EFI_BOOTNAME)" > $(BUILD_DIR)/thumbdrive-startup.nsh
	@bash scripts/mkdiskimage.sh --out $(BUILD_DIR)/starkernel-thumbdrive.img --table gpt \
	    --size-mib 128 --label STARKERNEL \
	    $(LOADER_EFI):/EFI/BOOT/$(EFI_BOOTNAME) \
	    $(BUILD_DIR)/thumbdrive-startup.nsh:/startup.nsh
	@rm -f $(BUILD_DIR)/thumbdrive-startup.nsh
	@echo "Write to a USB stick with: dd if=$(BUILD_DIR)/starkernel-thumbdrive.img of=/dev/sdX bs=4M status=progress"

# ==============================================================================
# BOOT IMAGES (one per board)
# ==============================================================================
# boot_image -- the single file written to a board's boot medium. Driven by
# BOARD (boards/<board>/board.mk); normally invoked from the repo root as
#   make boot_image TARGET=PC|RASPI|MILKV|ZYNQ7020
# Output: build/boards/<board>/$(BOARD_IMAGE).
#
# BOARD_BOOT selects the recipe:
#   uefi-esp   -- GPT disk, one FAT32 EFI System Partition holding the
#                 monolithic loader as EFI/BOOT/BOOT<ARCH>.EFI (+ startup.nsh
#                 for the UEFI shell, + starforth.cfg when KERNEL_ARGS is set).
#                 Real UEFI firmware (SER5) and U-Boot's bootefi (Milk-V) both
#                 boot it through the removable-media fallback path.
#   rpi-native -- MBR disk, one FAT32 (LBA) partition holding config.txt,
#                 kernel_2712.img (kernel relinked at 0x80000 and flattened)
#                 and the stock BCM2712 DTB (+ cmdline.txt, which firmware
#                 copies into /chosen/bootargs, when KERNEL_ARGS is set).
ifeq ($(ARCH),amd64)
EFI_BOOTNAME := BOOTX64.EFI
else ifeq ($(ARCH),aarch64)
EFI_BOOTNAME := BOOTAA64.EFI
else ifeq ($(ARCH),riscv64)
EFI_BOOTNAME := BOOTRISCV64.EFI
endif

RPI5_NATIVE_LINKER_SCRIPT := kernel/linker/starkernel-native-rpi5.ld
RPI5_NATIVE_ELF           := $(BUILD_DIR)/starkernel_rpi5.elf
RPI5_NATIVE_IMG           := $(BUILD_DIR)/kernel_2712.img

# RPI5_NATIVE_BASE: the image's load address. 0x80000 is the Pi 5's
# (boards/raspi/config.txt kernel_address). Change it only to rehearse the
# native path on QEMU's virt machine at EL2:
#   make -f kernel/Makefile ARCH=aarch64 RPI5_NATIVE_BASE=0x40080000 rpi5-rehearsal
RPI5_NATIVE_BASE ?= 0x80000

$(RPI5_NATIVE_ELF): $(KERNEL_OBJS) $(RPI5_NATIVE_LINKER_SCRIPT) | $(BUILD_DIR)
	@echo "LD (rpi5 native) $@"
	@$(LD) -T $(RPI5_NATIVE_LINKER_SCRIPT) -nostdlib --build-id=none \
	    --defsym=RPI5_NATIVE_BASE=$(RPI5_NATIVE_BASE) \
	    -e rpi5_native_start $(KERNEL_OBJS) -o $@

$(RPI5_NATIVE_IMG): $(RPI5_NATIVE_ELF)
	@echo "OBJCOPY (raw) $@"
	@$(OBJCOPY) -O binary $< $@

# rpi5-rehearsal: the Pi 5's native path -- no UEFI, raw image, entered at
# EL2 with the MMU off and x0 = the devicetree -- rehearsed on QEMU's virt
# machine, which has no Pi 5 model but enters a raw image exactly that way
# with virtualization=on. Linked at virt's 0x40080000 instead of 0x80000;
# everything else is the Pi 5 image. One QEMU, foreground, serial to
# logs/<TS>/aarch64-rpi5/ (2026-10-14, FABRIC-3.md SXXXIX).
.PHONY: rpi5-rehearsal
rpi5-rehearsal:
	@if [ "$(ARCH)" != "aarch64" ] || [ "$(RPI5_NATIVE_BASE)" != "0x40080000" ]; then \
		echo "rpi5-rehearsal: ARCH=aarch64 RPI5_NATIVE_BASE=0x40080000"; exit 1; fi
	@rm -f $(RPI5_NATIVE_ELF) $(RPI5_NATIVE_IMG)
	@$(MAKE) -f kernel/Makefile ARCH=aarch64 RPI5_NATIVE_BASE=$(RPI5_NATIVE_BASE) $(RPI5_NATIVE_IMG)
	@mkdir -p $(QEMU_LOG_DIR)
	@TS=$$(date +%Y%m%d-%H%M%S); \
	mkdir -p $(QEMU_LOG_DIR)/$$TS/aarch64-rpi5; \
	LOG=$(QEMU_LOG_DIR)/$$TS/aarch64-rpi5/qemu-aarch64-rpi5-$$TS.log; \
	SERIAL_SOCK=$(BUILD_DIR)/qemu-serial-aarch64-$$TS.sock; \
	QMP_SOCK=$(BUILD_DIR)/qemu-qmp-aarch64-$$TS.sock; \
	echo "=== Rehearsing the Pi 5 native path on QEMU virt at EL2 ==="; \
	echo "  Serial log: $$LOG"; \
	touch $$LOG; \
	qemu-system-aarch64 \
		-machine virt,virtualization=on \
		-cpu cortex-a76 \
		-kernel $(RPI5_NATIVE_IMG) \
		-drive id=artdisk,file=$(ARTDISK),format=raw,if=none \
		-device virtio-blk-pci,drive=artdisk \
		-device virtio-keyboard-pci,addr=0x3 \
		$(XHCI_ARGS_SLOT4) \
		$(ZUSEDISK_QEMU_ARGS) \
		-object rng-random,id=rng0,filename=/dev/urandom \
		-device virtio-rng-pci,rng=rng0,addr=0x5 \
		-chardev socket,id=cserial,path=$$SERIAL_SOCK,server=on,wait=off,logfile=$$LOG \
		-serial chardev:cserial \
		-qmp unix:$$QMP_SOCK,server=on,wait=off \
		$(QEMU_EXTRA) \
		-display none \
		-m 4096 \
		-no-reboot \
		-d guest_errors; \
	rm -f $$SERIAL_SOCK $$QMP_SOCK; \
	echo "=== Serial log: $$LOG ==="

.PHONY: boot_image
ifeq ($(strip $(BOARD)),)
boot_image:
	@echo "boot_image needs a board. From the repo root:"
	@echo "  make boot_image TARGET=PC|RASPI|MILKV|ZYNQ7020"
	@exit 1
else ifeq ($(BOARD_BOOT),uefi-esp)
boot_image: all
	@mkdir -p $(BOARD_OUT)
	@printf 'FS0:\\EFI\\BOOT\\%s\r\n' "$(EFI_BOOTNAME)" > $(BOARD_OUT)/startup.nsh
	@# The boot arguments, always written: system=v4 first -- v4 is the
	@# default -- so that which system runs is one word to change on the
	@# stick (A6); KERNEL_ARGS after it, and a later system= wins.
	@printf '%s\n' 'system=v4 $(KERNEL_ARGS)' > $(BOARD_OUT)/starforth.cfg
	@bash scripts/mkdiskimage.sh --out $(BOARD_OUT)/$(BOARD_IMAGE) --table gpt \
	    --size-mib $(BOOT_IMAGE_SIZE_MIB) --label LITHOS \
	    $(LOADER_EFI):/EFI/BOOT/$(EFI_BOOTNAME) \
	    $(BOARD_OUT)/startup.nsh:/startup.nsh \
	    $(BOARD_OUT)/starforth.cfg:/starforth.cfg
	@echo "$(BOARD_DESC)"
	@echo "Write with: dd if=$(BOARD_OUT)/$(BOARD_IMAGE) of=/dev/sdX bs=4M conv=fsync status=progress"
else ifeq ($(BOARD_BOOT),rpi-native)
$(RPI5_DTB):
	@mkdir -p $(dir $@)
	@echo "  FETCH   $(RPI5_DTB_URL)"
	@curl -fsSL -o $@.tmp $(RPI5_DTB_URL) || { rm -f $@.tmp; \
	    echo "Error: could not fetch the Pi 5 DTB. Copy bcm2712-rpi-5-b.dtb from a Raspberry Pi"; \
	    echo "firmware release and pass RPI5_DTB=/path/to/bcm2712-rpi-5-b.dtb"; exit 1; }
	@mv $@.tmp $@

boot_image: v3/include/version.h $(RPI5_NATIVE_IMG) $(RPI5_DTB)
	@mkdir -p $(BOARD_OUT)
	@# cmdline.txt, always: the firmware hands it to the kernel as the
	@# devicetree's bootargs. system=v4 first, one word to change (A6).
	@printf '%s\n' 'system=v4 $(KERNEL_ARGS)' > $(BOARD_OUT)/cmdline.txt
	@bash scripts/mkdiskimage.sh --out $(BOARD_OUT)/$(BOARD_IMAGE) --table mbr \
	    --size-mib $(BOOT_IMAGE_SIZE_MIB) --label LITHOS \
	    boards/$(BOARD)/config.txt:/config.txt \
	    $(RPI5_NATIVE_IMG):/kernel_2712.img \
	    $(RPI5_DTB):/bcm2712-rpi-5-b.dtb \
	    $(BOARD_OUT)/cmdline.txt:/cmdline.txt
	@echo "$(BOARD_DESC)"
	@echo "Write with: dd if=$(BOARD_OUT)/$(BOARD_IMAGE) of=/dev/mmcblkX bs=4M conv=fsync status=progress"
else
boot_image:
	@echo "Error: BOARD=$(BOARD) has BOARD_BOOT='$(BOARD_BOOT)', which has no boot_image recipe"
	@exit 1
endif

# iso-usb — build a PURE UEFI isohybrid ISO for the Iso Image Writer workflow
# (GNOME Disks "Restore Disk Image...", or dd). Writes a single .iso to a USB
# stick as a raw image; the ISO carries a clean GPT with an EFI System
# Partition holding EFI/BOOT/BOOT<ARCH>.EFI, so real UEFI firmware (e.g.
# Beelink SER5) scans the ESP and boots it. No GRUB, no isolinux, no MBR boot
# code — the protective MBR template is zeroed (partition metadata only).
# Also still boots under QEMU via -cdrom. This is the novice path: one ISO,
# picked with a GUI, written to the stick.
iso-usb: all
	@mkdir -p $(BUILD_DIR)
	@if ! which xorriso >/dev/null 2>&1; then echo "Error: xorriso not found. Install xorriso (apt-get install xorriso)."; exit 1; fi
	@if ! which mformat >/dev/null 2>&1 || ! which mmd >/dev/null 2>&1 || ! which mcopy >/dev/null 2>&1; then echo "Error: mtools not found. Install mtools (apt-get install mtools)."; exit 1; fi
	@case "$(ARCH)" in \
		amd64)   BOOTNAME="BOOTX64.EFI"    ;; \
		aarch64) BOOTNAME="BOOTAA64.EFI"  ;; \
		*) echo "Error: iso-usb EFI boot name only defined for amd64/aarch64 yet"; exit 1 ;; \
	esac; \
	ISODIR=$(BUILD_DIR)/iso-usb; \
	rm -rf $$ISODIR; mkdir -p $$ISODIR; \
	dd if=/dev/zero of=$$ISODIR/efi.img bs=512 count=8192 2>/dev/null; \
	mformat -i $$ISODIR/efi.img :: >/dev/null 2>&1; \
	mmd -i $$ISODIR/efi.img ::/EFI; mmd -i $$ISODIR/efi.img ::/EFI/BOOT; \
	mcopy -i $$ISODIR/efi.img $(LOADER_EFI) ::/EFI/BOOT/$$BOOTNAME; \
	printf 'FS0:\\EFI\\BOOT\\%s\r\n' "$$BOOTNAME" > $$ISODIR/startup.nsh; \
	mcopy -i $$ISODIR/efi.img $$ISODIR/startup.nsh ::/startup.nsh; \
	head -c 432 /dev/zero > $$ISODIR/protmbr.bin; \
	ISO=$(BUILD_DIR)/starkernel-iso-usb.iso; \
	rm -f $$ISO; \
	xorriso -as mkisofs \
		-V STARKERNEL -r -J \
		-isohybrid-mbr $$ISODIR/protmbr.bin \
		-eltorito-alt-boot -e efi.img -no-emul-boot \
		-isohybrid-gpt-basdat \
		-o $$ISO $$ISODIR >/dev/null 2>&1; \
	rm -rf $$ISODIR; \
	echo "Iso Image Writer ISO (pure UEFI): $$ISO"; \
	echo "  In GNOME Disks, pick the ISO with 'Restore Disk Image...' and select your USB stick."; \
	echo "  Or: dd if=$$ISO of=/dev/sdX bs=4M status=progress"


# qemu-esp — interactive dev boot from FAT directory (no disk image rebuild,
# no auto-kill/timeout/DOE-injection logic — stays up until you quit it
# yourself with Ctrl-A X or by closing the window).
# amd64/aarch64: serial → stdio (bidirectional, you can type into the REPL);
# riscv64: uses proper GPT (esp dir not supported)
qemu-esp: v3/include/version.h $(LOADER_EFI) $(KERNEL_ELF)
ifeq ($(ARCH),amd64)
	@rm -rf $(BUILD_DIR)/esp
	@mkdir -p $(BUILD_DIR)/esp/EFI/BOOT
	@cp $(LOADER_EFI) $(BUILD_DIR)/esp/EFI/BOOT/BOOTX64.EFI
	@cp $(KERNEL_ELF) $(BUILD_DIR)/esp/kernel.elf
	@printf "FS0:\\EFI\\BOOT\\BOOTX64.EFI\r\n" > $(BUILD_DIR)/esp/startup.nsh
	$(if $(KERNEL_ARGS),@printf '$(KERNEL_ARGS)\n' > $(BUILD_DIR)/esp/starforth.cfg,)
	@OVMF_CODE=$(OVMF_CODE); \
	if [ ! -f $$OVMF_CODE ]; then echo "Error: $$OVMF_CODE not found"; exit 1; fi; \
	cp $(OVMF_VARS_RO) $(BUILD_DIR)/OVMF_VARS.fd; \
	qemu-system-x86_64 \
		-nodefaults -display $(QEMU_DISPLAY) -vga std \
		-machine q35,accel=tcg \
		-cpu qemu64 \
		-m 1024 \
		-chardev stdio,id=ser0,signal=off \
		-device isa-serial,chardev=ser0,iobase=0x3f8 \
		-drive if=pflash,format=raw,readonly=on,file=$$OVMF_CODE \
		-drive if=pflash,format=raw,file=$(BUILD_DIR)/OVMF_VARS.fd \
		-drive format=raw,file=fat:rw:$(BUILD_DIR)/esp \
		-drive id=artdisk,file=$(ARTDISK),format=raw,if=none \
		-device virtio-blk-pci,drive=artdisk \
		-no-reboot

else ifeq ($(ARCH),aarch64)
	@rm -rf $(BUILD_DIR)/esp
	@mkdir -p $(BUILD_DIR)/esp/EFI/BOOT
	@cp $(LOADER_EFI) $(BUILD_DIR)/esp/EFI/BOOT/BOOTAA64.EFI
	@cp $(KERNEL_ELF) $(BUILD_DIR)/esp/kernel.elf
	@printf "FS0:\\EFI\\BOOT\\BOOTAA64.EFI\r\n" > $(BUILD_DIR)/esp/startup.nsh
	@if [ -f /usr/share/AAVMF/AAVMF_CODE.fd ]; then \
		AAVMF_CODE=/usr/share/AAVMF/AAVMF_CODE.fd; \
	elif [ -f /usr/share/qemu-efi-aarch64/QEMU_EFI.fd ]; then \
		AAVMF_CODE=/usr/share/qemu-efi-aarch64/QEMU_EFI.fd; \
	elif [ -f /usr/share/qemu/edk2-aarch64-code.fd ]; then \
		AAVMF_CODE=/usr/share/qemu/edk2-aarch64-code.fd; \
	else \
		echo "Error: aarch64 UEFI firmware not found. Install qemu-efi-aarch64."; exit 1; \
	fi; \
	qemu-system-aarch64 \
		-machine virt \
		-cpu cortex-a57 \
		-bios $$AAVMF_CODE \
		-drive format=raw,file=fat:rw:$(BUILD_DIR)/esp \
		-serial mon:stdio \
		-nographic \
		-m 2048 \
		-d guest_errors

else ifeq ($(ARCH),riscv64)
	@echo "riscv64: qemu-esp not supported (FAT dir mode broken on virt); use 'make qemu'"
	@exit 1
endif

# qemu-gdb — launch QEMU with GDB stub on port 1234
qemu-gdb: v3/include/version.h $(LOADER_EFI) $(KERNEL_ELF)
ifeq ($(ARCH),amd64)
	@mkdir -p $(BUILD_DIR)/esp/EFI/BOOT
	@cp $(LOADER_EFI) $(BUILD_DIR)/esp/EFI/BOOT/BOOTX64.EFI
	@cp $(KERNEL_ELF) $(BUILD_DIR)/esp/kernel.elf
	@echo "Launching QEMU with GDB server on :1234 (amd64)"
	@echo "  gdb $(KERNEL_ELF) -ex 'target remote :1234'"
	@qemu-system-x86_64 \
		-bios $(OVMF_CODE) \
		-drive format=raw,file=fat:rw:$(BUILD_DIR)/esp \
		-serial stdio \
		-m 1024 \
		-s -S
else ifeq ($(ARCH),aarch64)
	@mkdir -p $(BUILD_DIR)/esp/EFI/BOOT
	@cp $(LOADER_EFI) $(BUILD_DIR)/esp/EFI/BOOT/BOOTAA64.EFI
	@cp $(KERNEL_ELF) $(BUILD_DIR)/esp/kernel.elf
	@echo "Launching QEMU with GDB server on :1234 (aarch64)"
	@echo "  gdb $(KERNEL_ELF) -ex 'target remote :1234'"
	@qemu-system-aarch64 \
		-machine virt -cpu cortex-a57 \
		-bios /usr/share/AAVMF/AAVMF_CODE.fd \
		-drive format=raw,file=fat:rw:$(BUILD_DIR)/esp \
		-serial stdio -m 2048 -s -S
else
	@echo "Error: qemu-gdb not yet implemented for $(ARCH)"; exit 1
endif

# ==============================================================================
# INFO / HELP
# ==============================================================================


info:
	@echo "StarKernel Build Configuration"
	@echo "==============================="
	@echo "Architecture:    $(ARCH)"
	@echo "Target:          $(TARGET)"
	@echo "Compiler:        $(CC)"
	@echo "Loader CC:       $(LOADER_CC)"
	@echo "Linker:          $(LD)"
	@echo "Build dir:       $(BUILD_DIR)"
	@echo "Loader script:   $(LOADER_LINKER_SCRIPT)"
	@echo "Kernel script:   $(KERNEL_LINKER_SCRIPT)"
	@echo "Loader output:   $(LOADER_EFI)"
	@echo "Kernel output:   $(KERNEL_ELF)"
	@echo "VM integration:  $(STARFORTH_ENABLE_VM)"
	@echo "StarForth v4:    $(STARFORTH_V4)"
	@echo "Monolithic:      $(MONOLITHIC)"

help:
	@echo "StarKernel Build System"
	@echo "======================="
	@echo ""
	@echo "Usage: make -f kernel/Makefile [ARCH=<arch>] [TARGET=<target>] <goal>"
	@echo ""
	@echo "Architectures (aliases accepted):"
	@echo "  ARCH=amd64    / x86_64   — x86-64 (default on x86 host)"
	@echo "  ARCH=aarch64  / arm64    — ARM64"
	@echo "  ARCH=riscv64  / riscv    — RISC-V 64-bit"
	@echo ""
	@echo "Build goals:"
	@echo "  all           — build loader EFI + kernel ELF (default)"
	@echo "  kernel        — alias for all"
	@echo "  kernel-all    — build all three architectures"
	@echo "  clean         — remove build/\$(ARCH)/\$(TARGET)"
	@echo "  clean-kernel  — remove build/*/kernel for all arches"
	@echo ""
	@echo "QEMU goals:"
	@echo "  qemu          — clean boot, serial tee'd live + logs/<session>/<arch>/ + DOE CSV → doe/ (all arches)"
	@echo "  qemu-esp      — quick boot from FAT directory (amd64, aarch64)"
	@echo "  qemu-gdb      — boot with GDB stub on :1234"
	@echo "  thumbdrive    — build generic GPT/FAT32 UEFI disk image (write to a USB stick with dd; real-hardware/SER5 path)"
	@echo "  iso-usb       — build UEFI isohybrid ISO for Iso Image Writer / GNOME Disks 'Restore Disk Image...' (novice path; real-hardware/SER5)"
	@echo ""
	@echo "QEMU display:"
	@echo "  QEMU_DISPLAY=gtk   — framebuffer window backend for 'qemu' goal (default: gtk)"
	@echo "  QEMU_DISPLAY=none  — headless (no window), e.g. for CI"
	@echo ""
	@echo "Info goals:"
	@echo "  info          — show build configuration"
	@echo "  help          — show this message"
	@echo ""
	@echo "Examples:"
	@echo "  make -f kernel/Makefile ARCH=amd64 qemu"
	@echo "  make -f kernel/Makefile ARCH=aarch64 clean qemu"
	@echo "  make -f kernel/Makefile ARCH=riscv64 clean qemu"
	@echo "  make -f kernel/Makefile ARCH=x86_64 qemu   # alias works"
	@echo "  make -f kernel/Makefile kernel-all"
