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>
Semantic delta
No definition changed: this change is text, documentation or data.
3 files · +41 −24
diff --git a/scripts/efi-flicker-test.sh b/scripts/efi-flicker-test.sh
index 7a3de52b..edab5f95 100755
--- a/scripts/efi-flicker-test.sh
+++ b/scripts/efi-flicker-test.sh
@@ -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
diff --git a/scripts/hammer-live-install.sh b/scripts/hammer-live-install.sh
index d97ffa03..e5032b80 100755
--- a/scripts/hammer-live-install.sh
+++ b/scripts/hammer-live-install.sh
@@ -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
diff --git a/scripts/install-image-efi.sh b/scripts/install-image-efi.sh
index b682ad82..f9411a0e 100755
--- a/scripts/install-image-efi.sh
+++ b/scripts/install-image-efi.sh
@@ -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