Skip to content

Offer safe graphics boot for black-screen installs - #110

Open
yashranaway wants to merge 2 commits into
omacom:quattrofrom
yashranaway:fix/safe-graphics-boot
Open

Offer safe graphics boot for black-screen installs#110
yashranaway wants to merge 2 commits into
omacom:quattrofrom
yashranaway:fix/safe-graphics-boot

Conversation

@yashranaway

@yashranaway yashranaway commented Aug 16, 2026

Copy link
Copy Markdown

Some newer GPUs can black-screen before the configurator starts. The reporter in omacom/omarchy#7045 confirmed the same Quattro ISO boots after adding nomodeset.

Keep the normal accelerated entry as the default, but show the boot menu for three seconds and add a separate safe graphics entry with nomodeset. Mirror the fallback for GRUB loopback and legacy BIOS boot.

The regression test keeps nomodeset isolated to the safe entries, checks unique GRUB IDs, and ensures normal boot remains the default.

Fixes omacom/omarchy#7045.

Tested with:

  • bash test/unit/safe-graphics-boot-test.sh
  • grub-script-check configs/grub/grub.cfg
  • grub-script-check configs/grub/loopback.cfg
  • Full x86 ISO build

@archisman-panigrahi

Copy link
Copy Markdown

nomodeset is also necessary to boot unsupported AMD graphics card such as AMD ATI Mobility Radeon HD 4850, found in old iMacs (see https://askubuntu.com/questions/1503557/how-to-set-brightness-and-color-temperature-in-imac-late-2009-after-setting-nomo for details).

The syslinux half of this change already assigns one ("with safe
^graphics"), and the accessibility entry beside it in both GRUB configs
carries --hotkey s, so the GRUB entries were the odd ones out.

It also decouples reachability from timeout_style. GRUB matches hotkeys
inside the countdown/hidden wait loop, not only on a drawn menu, so with
a hotkey the entry stays selectable if the timeout is kept silent
(timeout_style=hidden) rather than shown as a menu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@omarchybot

Copy link
Copy Markdown
Collaborator

Reviewed this against the boot paths the ISO actually ships, and ran ./test/all on a throwaway VM — green, including your new test.

Pushed one commit, d287cef: --hotkey g on the two GRUB safe-graphics entries. Your syslinux entry already assigns one via with safe ^graphics, and the accessibility entry beside it in both GRUB files carries --hotkey s, so the GRUB entries were the odd ones out. It also matters for the point below.

The timeout change is not scope creep, and the PR body should say so. With the original timeout=0 the new entry is not reachable at all. In grub-core/normal/menu.c, run_menu() under timeout_style=hidden enters a wait loop that does a single grub_getkey_noblock() and then hits if (timeout == 0) break;, because no second can elapse; control falls straight to /* If timeout is 0, drawing is pointless (and ugly). */ if (timeout == 0) { *auto_boot = 1; ... return default_entry; }. There is no keypress window. The shift/keystatus escape hatch people remember is Debian and Ubuntu's generated /etc/grub.d/00_header, not GRUB core, and this repo's grub.cfg is hand-written and has none. A nonzero timeout is therefore a necessary part of the fix.

What is separable is timeout_style, not timeout. timeout=3 with timeout_style=hidden gives identical reachability — the same loop matches --hotkey and treats Esc as an interrupt — while keeping the screen blank, and it also suppresses the "Booting…" line, since *notify_boot = timeout_style != TIMEOUT_STYLE_HIDDEN. With the hotkey now in place, g goes straight to safe graphics without a menu ever being drawn. Either way every user pays the same three seconds; the visible menu buys discoverability, which is a product call for the maintainer. Worth offering both options in the PR body so it can be decided on its merits.

Things I found but deliberately did not change:

  • test/unit/safe-graphics-boot-test.sh:63 prints "Syslinux offers safe graphics without changing the default", but nothing in the test reads configs/syslinux/archiso_sys.cfg, where DEFAULT arch64 actually lives. Flipping it to DEFAULT arch64safe would keep the suite green. The GRUB half asserts default=archlinux, so this is just the missing other side.
  • The test pins timeout=3 and timeout_style=menu by exact string. If the decision above lands anywhere else, the suite fails on a product choice rather than on the invariant, which is really "the timeout is nonzero, so the entry is reachable".
  • The BIOS menu goes from seven entries to eight, against MENU ROWS 7 in archiso_head.cfg:8. The list now scrolls and Power Off drops off the first page. Bumping it is not a one-liner — MENU VSHIFT 10, MENU TABMSGROW 14 and MENU CMDLINEROW 14 sit right underneath — so I left it, but it is worth a look.
  • configs/efiboot/loader/entries/01-archiso-x86_64-linux.conf is the fourth boot config, and every previous kernel-cmdline change updated it in lockstep (94494c8 for xe.enable_panel_replay=0, f97a664 for initramfs_async=0). It is dead in the current ISO — profiledef.sh declares only bios.syslinux and uefi.grub, and current archiso stubs _make_bootmode_uefi-x64.systemd-boot.esp to return 1 — so nobody loses a boot path. Raising it only because it is now the one boot config out of step.
  • quiet splash makes this an alternate graphics entry rather than a diagnostic one. Plymouth should still render on the firmware framebuffer, so it is not a defect, but if the safe entry also comes up black the user is left with nothing to report. Dropping quiet splash there is a small change with real troubleshooting value.
  • PXE (archiso_pxe-linux.cfg) has no safe-graphics variant. Those entries already carry a different command line and buildmodes=('iso') builds no netboot artifact here, so I would call it out of scope unless you want it.

Two things that check out and needed checking: gfxpayload=keep and nomodeset do not conflict — keep preserves the GOP/VBE framebuffer that nomodeset then makes the only display path, so they are complementary. And xe.enable_panel_replay=0 is inert under nomodeset (xe bails in xe_check_nomodeset()), but keeping it costs nothing and keeps the four command lines comparable.

I could not boot the ISO, so everything here about boot behaviour is read from the GRUB, syslinux and kernel sources rather than observed on hardware.

@yashranaway

Copy link
Copy Markdown
Author

Thanks omarchybot

@omarchybot

Copy link
Copy Markdown
Collaborator

Re-reviewed after your reply and archisman-panigrahi's. d287cef is still head and no code changed, so this is not a second review of the diff — but I have to correct something I wrote last time, because the maintainer is being asked to decide on it.

I was wrong about GRUB's escape hatch. I said the shift/keystatus reveal was Debian and Ubuntu's generated 00_header rather than GRUB core. It is GRUB core. grub_key_is_interrupt() in grub-core/kern/term.c:142-160 returns 1 for Esc, for F4, and for held Shift via grub_getkeystatus(), and run_menu() calls it inside the hidden-timeout loop at grub-core/normal/menu.c:629. Both are in the 2.12 release, which is the grub that builder/build-iso.sh:33 installs. So "there is no keypress window" under timeout=0 was too absolute: the loop does exactly one non-blocking poll, but grub_key_is_interrupt(GRUB_TERM_NO_KEY) still reads key status, and Shift held at that instant sets timeout = -1, so the zero-timeout early return at menu.c:657 no longer fires and control falls through to menu_init() with the timeout environment already unset — the menu draws and waits indefinitely. Today's Quattro ISO is reachable by holding Shift, not unreachable. Codex caught this on the re-review; I verified it against kern/term.c and normal/menu.c at tag grub-2.12.

What that changes and what it does not:

  • Not the verdict. Holding Shift is undocumented, firmware-dependent on UEFI — grub_console_getkeystatus() at grub-core/term/efi/console.c:323 needs the Simple Text Input Ex protocol and returns 0 when nothing is in the key queue — and it gets you a menu with no nomodeset entry on it. A labelled entry is strictly better than a hidden key.
  • It does make timeout=3 with timeout_style=hidden more defensible than I implied. A nonzero timeout turns that single poll into three real seconds, so g, Esc, F4 and Shift all become usable: silent boot preserved, entry reachable, just undiscoverable.

As the PR stands the entry is reachable, and here is the trace. With timeout=3 / timeout_style=menu at grub.cfg:49-50 and loopback.cfg:25-26, run_menu() skips the hidden branch at menu.c:603, skips the zero-timeout return at 657, draws via menu_init() at 667, prints the countdown at 675 and polls for three seconds from 679. Enter boots the highlighted entry at 760; plain g falls to default: at 794 into get_entry_index_by_hotkey(). Nothing shadows it — menu.c intercepts c, e, v and ^ before that lookup, and 744 is Ctrl-G, a different key value — so d287cef's g and the existing --hotkey s are both clear.

One fact worth having in front of the menu vs hidden decision. configs/grub/grub.cfg was created whole in ea707f6 (2025-09-12) with timeout=0 / timeout_style=hidden, never changed since, and it already carried the speakup screen reader entry plus Memtest86+, UEFI Shell, UEFI Firmware Settings, shutdown and restart. All of those have been Shift-hold-only on UEFI for eleven months. BIOS was never affected — archiso_sys.cfg:3-4 is DEFAULT arch64 with TIMEOUT 150 and UI vesamenu.c32, a visible menu for fifteen seconds — but profiledef.sh:11 declares bios.syslinux and uefi.grub, and most installs are the second. A screen reader entry you can only reach by holding a key nobody told you about is the case where that costs most.

@archisman-panigrahi your point holds and needs no code change: nomodeset is vendor-neutral, and radeon_module_init() bails on firmware-only graphics the same way xe does, so an HD 4850 takes the same path a 50-series card does. A second hardware class for the same entry.

Nothing pushed this pass, and no tests re-run — head has not moved since ./test/all went green on a throwaway VM, so that result still describes this commit. The five items from last time are unchanged, and the menu vs hidden call is still the maintainer's.

@omarchybot

Copy link
Copy Markdown
Collaborator

Built the ISO from d287cef and booted it, which the previous two passes could not do — so this is the first time anything here has been observed rather than read out of GRUB and kernel sources. Both firmware paths, on a disposable VM, QEMU with OVMF and stdvga.

The menus are right. The UEFI safe-graphics entry does not work.

Selecting it produces a garbled framebuffer that never recovers — still garbled at three minutes, when the BIOS safe entry had reached the configurator in about one. The BIOS half of this PR is fine: it boots, it is readable, it is exactly the low-resolution fallback it should be. The UEFI half hands the user a scrambled screen, which is a worse outcome than the black screen it is meant to rescue, because a black screen at least tells you it failed.

Four boots, one variable at a time, same ISO and same QEMU configuration each time:

entry result
UEFI normal (gfxpayload=keep, no nomodeset) clean Plymouth, mode switches to 1024x768 when KMS loads
UEFI safe (gfxpayload=keep + nomodeset) garbled, never recovers
BIOS safe (nomodeset) boots to the configurator
ISO's own kernel and initramfs, nomodeset, loaded directly under OVMF so GRUB never runs — tried at 1024x768 and 800x600 clean Plymouth both times

I have to correct myself before drawing a conclusion from that. Last pass I wrote that gfxpayload=keep and nomodeset "do not conflict — they are complementary." That was a source read stated with more confidence than a source read earns, and the boot above is not consistent with it. My first instinct on seeing the garbling was to blame gfxpayload=keep and recommend gfxpayload=text; codex at xhigh refuted both halves of that and it was right to. Loading a kernel directly under OVMF is a different loader path from GRUB's linux command, so it does not isolate the payload setting — it changes who supplies the framebuffer metadata and how the kernel is entered. A competing explanation fits every observation above just as well: GRUB or gfxterm hands over framebuffer geometry that is wrong, nomodeset leaves that framebuffer in service, and the normal entry looks fine only because native KMS replaces it moments later. Testing two resolutions rules out resolution; it does not rule out stride, pixel format or base address. And keep with nomodeset is not inherently invalid: codex points to other distributions shipping exactly that pairing, and to gfxpayload=text being a poor choice on UEFI in the first place, where there is no native text mode to fall back to. I have not verified those two citations myself; the loader-path objection above is enough on its own to say the variable is not isolated.

So: the entry is broken, the cause is inside the GRUB-to-kernel handoff, and which part is not yet established. What settles it is an A/B through GRUB itself, one variable per build — gfxterm with keep as it stands, then gfxpayload explicitly auto or unset, then terminal_output console with keep — with quiet splash dropped so the console is readable and the early framebuffer geometry can be compared. I can run that matrix if you want it.

Which leads to the thing worth more than the bug. This repo sets terminal_output gfxterm at configs/grub/grub.cfg:17. Upstream archiso sets console, and the changelog in our own vendored submodule says why, at archiso/CHANGELOG.rst:436: "Use console as grub's terminal_output, as gfxterm leads to a blank screen on some hardware." gfxterm is initialised at grub.cfg:13-18 on every boot regardless of timeout style, so this PR does not create that exposure — what it changes is that the menu becomes the thing a user has to see for the fallback to exist for them. The user this PR is for is the user whose graphics stack is already fragile, and on the hardware where gfxterm blanks, the three-second window shows them nothing, they never learn the fallback exists, and the boot proceeds into the KMS failure anyway. That is the case the PR is aimed at, and it is the case where the visible menu is least likely to reach them. The g hotkey works whether or not the menu is ever painted, which is why the hidden variant is no worse on that hardware; a visible menu is only worth its three seconds if it is actually visible. If the menu stays visible, terminal_output console is worth taking with it.

The test passes in four states where the feature is absent or reversed. Codex found these; I confirmed each by mutating the tree and running test/unit/safe-graphics-boot-test.sh unchanged:

  • Delete the GRUB safe entry and leave a bare comment # --id 'archlinux-safe-graphics' nomodeset in its place — passes. grub_entry() starts capturing at any line containing the id, comments included, and runs to the next unindented }, so it captures the following entry's body.
  • Gut the syslinux stanza to LABEL arch64safe plus help text mentioning nomodeset, with no LINUX, INITRD or APPEND — passes. syslinux_entry() captures the TEXT HELP block, so the word in prose satisfies the assertion.
  • Flip configs/syslinux/archiso_sys.cfg:3 to DEFAULT arch64safe — passes, and prints "without changing the default". I raised this one last pass from reading; it is now proven, and it is a false green on every BIOS boot.
  • Append timeout=0 and timeout_style=hidden after the existing assignments — passes. The assertions use grep -Fxq, which asks whether the line occurs anywhere, not whether it is the effective value.

The fix is for the test to parse an active menuentry and require nomodeset as a token on its linux line, require a real APPEND for the syslinux stanza, read archiso_sys.cfg for the BIOS default, and check the last assignment of default, timeout and timeout_style rather than any.

Where this leaves the PR. The idea is right and the BIOS side works. The UEFI side needs a boot that produces a usable screen before it can land, and profiledef.sh builds both modes but UEFI is how most of these machines install. Nothing was pushed this pass. Reviewed by Claude Opus 5 in Claude Code with a second opinion from Codex at xhigh, which refuted my initial diagnosis and supplied the competing explanation above; codex ran read-only against this branch and its independence is not fully guaranteed, though the two findings it changed my mind on are its own reasoning rather than agreement with mine.

@fidecastro

Copy link
Copy Markdown

Confirmed on real hardware with Omarchy 4.0.0:
• ASRock Z690-C/D5, i9-13900K, RTX 4090, DisplayPort, Secure Boot off
• Stock USB: black screen after UEFI handoff; Caps Lock still toggled (kernel up, KMS/Nouveau blanked video)
• GRUB never visible because of timeout=0 / timeout_style=hidden
• Remastered ISO with nomodeset (+ optional nouveau.modeset=0 / NVIDIA module blacklist) → installer worked; Omarchy installed successfully with the 4090 still connected

Strong +1 for a discoverable safe graphics entry and a nonzero timeout (or hotkey g as in this PR).

@omarchybot

Copy link
Copy Markdown
Collaborator

@fidecastro's report changes something I wrote last pass, so this is a correction rather than a fourth review of the diff — head is still d287cef and no code has moved.

The failure he describes is the one this entry addresses. Black screen with the kernel demonstrably alive (Caps Lock LED still toggling), discrete NVIDIA on DisplayPort: that is native KMS taking over a working firmware framebuffer and losing scanout. nomodeset is registered by disable_modeset() in drivers/video/nomodeset.c, surfaces through drm_firmware_drivers_only(), and nouveau_drm_init() checks it and returns before pci_register_driver(), leaving simpledrm (or efifb) driving the firmware framebuffer. Mechanism matches. Those kernel references are codex's; I had no kernel tree to check them against from here.

And it undercuts the blocking objection I raised on 24 August. I wrote then that "the UEFI side needs a boot that produces a usable screen before it can land," on the strength of a QEMU/OVMF/stdvga boot that came up garbled. But normalise the title, id and hotkey and the safe entry at configs/grub/grub.cfg:61-65 differs from the shipped default at 55-59 by exactly one token — the trailing nomodeset. set gfxpayload=keep is identical in both, on quattro and on main, so every released 4.0.0 stick carries it too. Step 2 of your own omacom/omarchy#8287 tells people to edit the stick's linux line to include nomodeset, which produces precisely that configuration. If that is what you did, then you booted this PR's entry — through GRUB, on UEFI, on a 4090 — and reached the installer.

That does not make the QEMU result wrong; it makes the conclusion I drew from it too broad. What is defensible now is narrower: the safe entry garbles under OVMF with QEMU stdvga, and at least one real NVIDIA GOP boots it clean. The likeliest reading is a framebuffer-geometry disagreement in the OVMF/stdvga GOP handoff that nomodeset then leaves permanently on screen, where the normal entry hides it because native KMS replaces that framebuffer moments later. Still inference — nothing captured screen_info or the stride from the garbled run.

@fidecastro, two questions only you can answer, and they close this out. First: did your remaster leave set gfxpayload=keep in the entry you booted, i.e. did you append nomodeset and change nothing else — and did nomodeset alone do it, without nouveau.modeset=0 or the module blacklist? Calling those optional reads as "not needed", but it does not quite prove it, and the difference decides whether your boot and this PR's entry are the same thing. Second, if you are willing to build this branch's ISO on that machine: does the three-second GRUB menu actually paint on the 4090 over DisplayPort? That is the question this PR cannot answer for itself. configs/grub/grub.cfg:17 sets terminal_output gfxterm; upstream archiso moved to console because gfxterm blanks on some hardware, and a menu that does not paint delivers no fallback to exactly the person who needs one. --hotkey g still works when nothing is drawn — input stays on console at line 16 and hotkey matching happens in the menu loop independently of rendering — but that only helps someone who already knows to press it.

The cheapest test on my side needs no hardware: re-run the same OVMF boot with virtio-vga in place of stdvga and pick the same entry. Clean output would put the garbling on stdvga's GOP handoff and take the last objection off this PR. I have not run it — the ISO from the last pass went with its worker, and a rebuild for a commit that has not moved was not worth the slot.

On the two repositories. omarchy-iso#110 and omacom/omarchy#8287 complement rather than compete: this ships the fallback for future images, that documents recovery on the images already in people's hands, and they share no files. One thing worth scoping in #8287, though — "Stock live images currently hide the boot menu (timeout=0)" is written unversioned, and stops being true of the first image built after this merges. Pinning those sentences to 4.0.0 and older keeps them correct rather than stale. Also worth knowing: omacom/omarchy#7063 already documents the same nomodeset workaround in the same file, appending at the end where yours inserts at the top — so both would merge without a git conflict and leave the manual carrying two sections on one symptom.

I went to tell you the "no chance to press e" line was too pessimistic and it turns out it holds. e is not itself a hidden-timeout interrupt, so it does nothing until the menu has been revealed, and at timeout=0 GRUB polls once — Esc, F4 or g must already be queued at that instant. Held Shift is the one route detectable as state rather than timing, and on UEFI even that needs the firmware to expose Simple Text Input Ex. Worth trying before remastering a stick; not worth documenting as the answer.

Nothing pushed and no tests re-run this pass — the head has not moved since ./test/all went green on a throwaway VM, so that result still describes this commit. Unchanged and still open from before: test/unit/safe-graphics-boot-test.sh passes in four states where the feature is absent or reversed, and MENU ROWS 7 at configs/syslinux/archiso_head.cfg:8 overflows now that the BIOS menu has eight entries. Waiting on the maintainer for timeout_style=menu versus hidden, and on the author for the test.

Reviewed by Claude Opus 5 in Claude Code, with a second opinion from Codex at xhigh which contributed the kernel mechanism above, the stdvga geometry hypothesis, and the correction about e — its read-only sandbox does not fully guarantee independence, but those three are its own reasoning rather than agreement with mine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NVIDIA 50xx Users - Can't Install - Boots to black screen

4 participants