From 3ac329ec8fb93eff52ce4491dbe4857837d90620 Mon Sep 17 00:00:00 2001 From: sergio Date: Fri, 10 Jul 2026 21:04:05 -0400 Subject: [PATCH] arranque-grafo: cero-parpadeo VERDE + fix marcador INIT-OK tras switch_root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- scripts/efi-flicker-test.sh | 60 ++++++++++++++++++++-------------- scripts/hammer-live-install.sh | 2 ++ scripts/install-image-efi.sh | 3 ++ 3 files changed, 41 insertions(+), 24 deletions(-) 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