arranque-grafo: cero-parpadeo VERDE + fix marcador INIT-OK tras switch_root

efi-flicker-test valida por CONTRIBUCIÓN del kernel al framebuffer (2 capturas:
handoff-firmware vs post-boot; Δ≈0 ⇒ el kernel no tocó el FB). Con el kernel
metal cero-parpadeo: Δ=0.000 VERDE — el splash de la firmware sobrevive intacto,
sin texto de boot ni cursor (el kernel viejo saltaba de ~3 a ~11). No mide negro
absoluto: el splash de OVMF (TianoCore) daría falso-positivo y no existe en metal.

Fix: busybox switch_root NO mueve /dev ⇒ los wrappers /sbin/init montan devtmpfs
antes de tee'ar HAMMER-EFI-INIT-OK a /dev/ttyS0 (si no, el nodo no existe y el
marcador se perdía). efi-disk-boot-test VERDE con el kernel quiet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 21:04:05 -04:00
co-authored by Claude Opus 4.8
parent fd033fb920
commit 3ac329ec8f
3 changed files with 41 additions and 24 deletions
+36 -24
View File
@@ -1,22 +1,29 @@
#!/usr/bin/env bash
# efi-flicker-test.sh — valida el CERO-PARPADEO (ADR 0010 paso 4) del kernel metal, por captura de
# framebuffer en OVMF-GOP. Arma el disco EFI real (install-image-efi.sh, cmdline `quiet` horneada +
# FRAMEBUFFER_CONSOLE_DEFERRED_TAKEOVER), lo bootea en OVMF con una GPU emulada y hace `screendump` por
# QMP a los N s. Con deferred-takeover + quiet, fbcon NO toca el framebuffer ⇒ debe quedar NEGRO (media
# de brillo ≈ 0) hasta que un compositor (mirada, no presente acá) abra el DRM master.
# FRAMEBUFFER_CONSOLE_DEFERRED_TAKEOVER) y lo bootea en OVMF.
#
# Criterio (robusto y honesto con el metal): el kernel logra cero-parpadeo si NO TOCA el framebuffer —
# lo deja tal como la firmware lo entregó. Con deferred-takeover + quiet, fbcon nunca toma el FB, así que
# el contenido de la firmware (en OVMF: el splash TianoCore + líneas BdsDxe; en metal: el logo del
# fabricante) SOBREVIVE intacto hasta que un compositor (mirada, no presente acá) abre el DRM master. Por
# eso NO medimos "pantalla negra absoluta" (el splash de OVMF daría falso-positivo y NO existe en metal),
# sino la CONTRIBUCIÓN del kernel: capturamos temprano (t≈BASE_T, justo tras el handoff) y tarde (t≈WAIT)
# y exigimos que la media de brillo casi no cambie ⇒ el kernel no pintó nada. Un kernel verboso saltaría
# de ~3 (sólo splash) a ~11 (splash + texto de boot).
#
# NOTA: el *seamless* i915 (firmware GOP → simplefb → i915 sin re-modeset) sólo se valida en metal Intel
# real; OVMF no trae i915. Lo que esto prueba es la mitad "pantalla limpia": sin texto de boot ni cursor.
# real; OVMF no trae i915.
#
# Uso: ./scripts/efi-flicker-test.sh
# Vars: ROOTFS KERNEL MEM (def 2048) WAIT (def 16) MEAN_MAX (def 1.5) OVMF_CODE OVMF_VARS
# Vars: ROOTFS KERNEL MEM (def 2048) BASE_T (def 4) WAIT (def 16) DELTA_MAX (def 1.0) OVMF_CODE OVMF_VARS
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
MEM="${MEM:-2048}"; WAIT="${WAIT:-16}"; MEAN_MAX="${MEAN_MAX:-1.5}"
MEM="${MEM:-2048}"; BASE_T="${BASE_T:-4}"; WAIT="${WAIT:-16}"; DELTA_MAX="${DELTA_MAX:-1.0}"
OVMF_CODE="${OVMF_CODE:-/usr/share/edk2/x64/OVMF_CODE.4m.fd}"
OVMF_VARS="${OVMF_VARS:-/usr/share/edk2/x64/OVMF_VARS.4m.fd}"
WORK="$ROOT/work/efi-flicker-test"; mkdir -p "$WORK"
IMG="$WORK/hammer-efi-disk.img"; SHOT="$WORK/screen.ppm"; VARS="$WORK/vars.fd"; QMP="$WORK/qmp.sock"
IMG="$WORK/hammer-efi-disk.img"; BASE="$WORK/base.ppm"; SHOT="$WORK/screen.ppm"; VARS="$WORK/vars.fd"; QMP="$WORK/qmp.sock"
[ -r "$OVMF_CODE" ] && [ -r "$OVMF_VARS" ] || { echo "no encuentro OVMF (set OVMF_CODE/OVMF_VARS)"; exit 1; }
command -v identify >/dev/null || { echo "falta imagemagick (identify)"; exit 1; }
@@ -29,7 +36,7 @@ KCFG=$(ls -dt store/*-linux-metal/boot/config-* 2>/dev/null | head -1)
grep -q 'CONFIG_FRAMEBUFFER_CONSOLE_DEFERRED_TAKEOVER=y' "$KCFG" && echo " ✓ DEFERRED_TAKEOVER=y" \
|| echo " ! el kernel metal AÚN no tiene DEFERRED_TAKEOVER=y (¿rebuild pendiente?)"
echo "==> boot en OVMF-GOP + screendump a los ${WAIT}s"
echo "==> boot en OVMF-GOP + 2 screendumps (t=${BASE_T}s handoff-firmware · t=${WAIT}s post-boot)"
cp "$OVMF_VARS" "$VARS"; rm -f "$QMP"
accel=(); [ -w /dev/kvm ] && accel=(-enable-kvm -cpu host)
qemu-system-x86_64 -m "$MEM" -no-reboot "${accel[@]}" \
@@ -39,10 +46,10 @@ qemu-system-x86_64 -m "$MEM" -no-reboot "${accel[@]}" \
-vga std -display none -serial file:"$WORK/serial.log" \
-qmp "unix:$QMP,server,nowait" >/dev/null 2>&1 &
QPID=$!
# QMP: capabilities → esperar → screendump
python3 - "$QMP" "$SHOT" "$WAIT" <<'PY'
import socket, json, sys, time, os
qmp, shot, wait = sys.argv[1], sys.argv[2], int(sys.argv[3])
# QMP: capabilities → screendump a BASE_T → screendump a WAIT
python3 - "$QMP" "$BASE" "$BASE_T" "$SHOT" "$WAIT" <<'PY'
import socket, json, sys, time
qmp, base, base_t, shot, wait = sys.argv[1], sys.argv[2], int(sys.argv[3]), sys.argv[4], int(sys.argv[5])
s=None
for _ in range(60):
try: s=socket.socket(socket.AF_UNIX); s.connect(qmp); break
@@ -50,27 +57,32 @@ for _ in range(60):
if not s: print("NO-QMP"); sys.exit(2)
f=s.makefile("rwb",buffering=0); f.readline()
f.write(b'{"execute":"qmp_capabilities"}\n'); f.readline()
time.sleep(wait)
time.sleep(base_t)
f.write(json.dumps({"execute":"screendump","arguments":{"filename":base}}).encode()+b"\n"); f.readline()
time.sleep(max(0, wait-base_t))
f.write(json.dumps({"execute":"screendump","arguments":{"filename":shot}}).encode()+b"\n"); f.readline()
time.sleep(0.5)
PY
kill "$QPID" 2>/dev/null || true; wait "$QPID" 2>/dev/null || true
echo "==> analizando el framebuffer capturado"
[ -r "$SHOT" ] || { echo "✗ no se capturó el framebuffer"; exit 1; }
# %[fx:mean] = media de brillo NORMALIZADA 0..1 (fiable entre versiones/quantum; el 1er número de
# `identify -verbose` varía por quantum). ×255 para expresarla 0..255. Negro ≈ 0; texto de boot ≈ 11.
MEANNORM=$(identify -format '%[fx:mean]' "$SHOT")
MEAN255=$(python3 -c "print(float('$MEANNORM')*255)")
echo "==> analizando la CONTRIBUCIÓN del kernel al framebuffer"
[ -r "$BASE" ] && [ -r "$SHOT" ] || { echo "✗ no se capturaron ambos framebuffers"; exit 1; }
# %[fx:mean] = media de brillo NORMALIZADA 0..1 (fiable entre versiones/quantum). ×255 → 0..255.
GEO=$(identify -format '%wx%h' "$SHOT")
printf " framebuffer %s · media de brillo = %.3f / 255 (umbral cero-parpadeo: < %.1f)\n" "$GEO" "$MEAN255" "$MEAN_MAX"
BMEAN=$(python3 -c "print(float('$(identify -format '%[fx:mean]' "$BASE")')*255)")
FMEAN=$(python3 -c "print(float('$(identify -format '%[fx:mean]' "$SHOT")')*255)")
DELTA=$(python3 -c "print(abs($FMEAN-$BMEAN))")
printf " framebuffer %s · media handoff=%.3f post-boot=%.3f Δ=%.3f / 255 (umbral: Δ < %.1f)\n" \
"$GEO" "$BMEAN" "$FMEAN" "$DELTA" "$DELTA_MAX"
echo " (la base incluye el splash de la firmware —TianoCore en OVMF—; en metal es el logo del fabricante)"
echo
if python3 -c "import sys; sys.exit(0 if float('$MEAN255') < float('$MEAN_MAX') else 1)"; then
echo "✓✓ CERO-PARPADEO VERDE: el framebuffer queda NEGRO durante el arranque (sin texto de boot ni cursor)."
if python3 -c "import sys; sys.exit(0 if float('$DELTA') < float('$DELTA_MAX') else 1)"; then
echo "✓✓ CERO-PARPADEO VERDE: el kernel NO tocó el framebuffer (Δ≈0) — el contenido de la firmware"
echo " sobrevive intacto hasta que mirada abra el DRM. Sin texto de boot ni cursor pintados."
echo " (El seamless i915 firmware→escritorio sólo se valida en metal Intel real.)"
else
echo "✗ efi-flicker-test FALLÓ: el framebuffer tiene contenido (media ${MEAN255}${MEAN_MAX})." >&2
echo " Revisá $SHOT probablemente texto de boot (¿kernel sin rebuild?) o getty en tty0." >&2
echo "✗ efi-flicker-test FALLÓ: el kernel modificó el framebuffer (Δ=${DELTA}${DELTA_MAX})." >&2
echo " Revisá $SHOT — texto de boot (¿kernel sin rebuild / sin quiet?) o getty en tty0." >&2
exit 1
fi
+2
View File
@@ -162,6 +162,8 @@ INIT
# Wrapper PID1 (rama UEFI): el initramfs de pivote ya montó / /store /var/lib/hammer por LABEL antes del
# switch_root; sólo auto-sanamos un upgrade interrumpido y arrancamos el init real. El marcador a
# /dev/ttyS0 sobrevive `quiet` (que suprime el printk kernel-side) ⇒ prueba serie de que llegó a PID1.
# busybox switch_root NO mueve /dev ⇒ montamos devtmpfs antes (si no, /dev/ttyS0 aún no existe).
/bin/busybox mount -t devtmpfs dev /dev 2>/dev/null || true
echo "HAMMER-EFI-INIT-OK: arje-zero PID1" > /dev/ttyS0 2>/dev/null || true
[ -x /usr/sbin/hammer-recover ] && /usr/sbin/hammer-recover || true
exec /usr/bin/arje-zero
+3
View File
@@ -123,6 +123,9 @@ rm -f "$ROOT_TREE/sbin/init"
cat > "$ROOT_TREE/sbin/init" <<'RINIT'
#!/bin/sh
# PID1 real tras el switch_root del pivote (store/estado ya montados por LABEL en el initramfs).
# busybox switch_root NO mueve /dev al nuevo root ⇒ montamos devtmpfs antes del marcador (si no,
# /dev/ttyS0 no existe todavía y el echo se pierde). arje-zero re-monta lo que necesite, es idempotente.
/bin/busybox mount -t devtmpfs dev /dev 2>/dev/null || true
echo "HAMMER-EFI-INIT-OK: arje-zero PID1" > /dev/ttyS0 2>/dev/null || true
exec /usr/bin/arje-zero
RINIT