Skip to content

feat(cpu): send a PCI device's pin through its I/O APIC line when it has no MSI - #799

Merged
MasterLaplace merged 9 commits into
feat/59-pci-interrupt-routingfrom
feat/59-pci-interrupts-through-the-io-apic
Nov 8, 2026
Merged

MasterLaplace merged 9 commits into
feat/59-pci-interrupt-routingfrom
feat/59-pci-interrupts-through-the-io-apic

Conversation

@MasterLaplace

@MasterLaplace MasterLaplace commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

What

A PCI function without MSI now interrupts through its I/O APIC line:

  • The model. The boot tells firmware the APIC model when the MADT declares an I/O APIC and a device interrupt reaches a core in the mode the local APICs will run: xAPIC, or x2APIC under a hypervisor (§3.8 step 5).
  • The line. input_output_advanced_programmable_interrupt_controller_route_device takes the line the published _PRT route gives the function's pin (§3.11). It programs the route's trigger and polarity, on a device vector, to the slot it is given. The entry stays masked while it is written. A line held by an ISA route, by an ISA IRQ with an 8259 handler, or by another vector is refused. No entry past 0x77, the last an 8-bit index reaches, is written.
  • Audio capture takes MSI first, then its I/O APIC line. It takes its 8259 line only in the PIC model, never after a \_PIC (1) that fails.

Specification: said, says, because

Step 5 said the APIC model waits until "no PCI interrupt goes through the 8259 (#59)". It now states the rule, because on bare metal x2APIC no device interrupt reaches a core without remapping.

Verify

Satellite, intel-hda,msi=off, each line computed apart:

Machine Line Computed apart
q35 GSI 23, level, high acpiexec_routing.py on QEMU's DSDT
pc GSI 11 SeaBIOS rel-1.16.3 pci_irqs[3] for LNKD
q35 and pc, -cpu max,-hypervisor 8259 line 11 before the fix, q35 had no interrupt

The line fires 4 times on slot 0 and can be moved to slot 1 and back. Every boot ends with 0 fail:

  • satellite: q35 709 and 711, pc 703 and 704;
  • server: q35 with NVMe 863, pc 850;
  • ring0 quick and ring0 qemu: 9 of 9 and 4 of 4 agree with the host.

There are 13 mutants, and each one fails its test.

Budgets: W2 goes from 73,251 to 76,303, the cost of \_PIC (1). W5 goes from 518,853 to 354,430. W1 and the tick are unchanged.

Not in it

Stacked on #798. Refs #59

…has no MSI

The boot tells firmware the APIC model when the MADT declares an I/O APIC. A function's pin then goes to the line its published _PRT route gives, with the route's trigger and polarity, on a device vector, to the core that asks. The audio capture takes MSI first, then its I/O APIC line, and its 8259 line only in the PIC model.
…the APIC model

Step 5's \_PIC (1) adds 3,066 instructions to W2 (76,303). W5 drops from 537,507 to 373,080: in the APIC model q35's _PRT names its GSIx links, whose _CRS and _PRS are data, where its LNKx links run methods that read the PIRQ registers.
@MasterLaplace MasterLaplace added the type:feature A new capability label Nov 8, 2026
@MasterLaplace MasterLaplace self-assigned this Nov 8, 2026
@github-project-automation github-project-automation Bot moved this to To review in Laplace Nov 8, 2026
@MasterLaplace
MasterLaplace added this pull request to stack #800 November 8, 2026 03:18
…reaches a core

The boot told firmware the APIC model whenever the MADT declared an I/O APIC.
In x2APIC mode on bare metal no device interrupt reaches a core without
interrupt remapping, so the I/O APIC line and MSI were both refused, and the
8259 too, since firmware may have moved PCI interrupts off it: capture lost
its interrupt. The model is now APIC only when device interrupts are
delivered in the mode the local APICs will run, which apic_runs_x2apic_mode
tells before the local APIC is initialised. Step 5 of the specification
writes the rule.
…rves

An 8-bit register index reaches entry 0x77; entries 0x78 to 0x7F wrapped onto
the identifier, version and arbitration registers. A PCI route to a Global
System Interrupt that an ISA IRQ with an 8259 handler also drives would have
programmed that pin level-triggered under the ISA device.

The capture test now checks the line's slot, that it fired more than once,
that another vector and an ISA handler are refused, that the vector holding
it can aim it at another core, and the 8259 line where no device interrupt
reaches a core. The new macros spell out their acronym, and the W2 header
counts \_PIC (1) against the PIC model's 73,251 instructions.
…pts-through-the-io-apic

# Conflicts:
#	tools/budgets/acpi-interrupt-routing.budget
After a \_PIC (1) that fails, the routes answer for an unknown model:
firmware may have moved PCI interrupts off the 8259 or not, so audio
capture takes neither a line nor the 8259 there. The rule is the routing
module's, advanced_configuration_and_power_interface_interrupt_routing_
leaves_the_8259, and its test runs on the host and in ring 0.
…entified once

373,080 to 354,430 instructions, measured on the release server kernel.
@MasterLaplace
MasterLaplace merged commit b698259 into main Nov 8, 2026
13 checks passed
@MasterLaplace
MasterLaplace deleted the feat/59-pci-interrupts-through-the-io-apic branch November 8, 2026 14:21
@github-project-automation github-project-automation Bot moved this from To review to Done in Laplace Nov 8, 2026
MasterLaplace added a commit that referenced this pull request Nov 8, 2026
…d over x2APIC (#797)

Closes #795

## What changed

On bare metal (CPUID.1:ECX[31] clear) the local APIC stays in xAPIC mode
unless the firmware already set IA32_APIC_BASE.EXTD; under a hypervisor
x2APIC is turned on as before. The bootstrap processor chooses
(`apic_choose_mode`), every application processor applies that mode, and
each says why: `CPU APIC mode=xapic-active,
reason=bare-metal-without-interrupt-remapping, apic_id=1`, and
`apic_reason=` in `[LPLTLM] machine`. An AP the mode cannot address
(0xFF and up in xAPIC) is refused by name and counted as
`ap_unaddressable` in `[LPLTLM] smp`.

## Why

Without interrupt remapping no device interrupt reaches x2APIC mode on
bare metal (Intel SDM vol. 3, 325384-093US, §13.12.6-7), and
`device_interrupt_platform_delivers` refuses to aim one there.

## With main

Since #799 the boot chooses the interrupt model it tells firmware before
the local APIC is initialised, from `apic_runs_x2apic_mode`. That
prediction now follows this rule: before `apic_initialize_on_cpu`, the
mode `apic_choose_mode` would choose; after it, the chosen one.
`the_interrupt_model_follows_the_mode_the_local_apics_run` checks the
model against the mode the local APICs run, computed apart; the old
prediction fails it under `-cpu max,-hypervisor`. Merged with main,
every boot ends with 0 fail:

- server: q35 with NVMe 867, and 870 under `-cpu max,-hypervisor`; pc
854;
- satellite: q35 713 to 716 and pc 707 to 709, with MSI and on the I/O
APIC line (GSI 23 and 11), under a hypervisor and without.

## How to verify

`./build.sh --server`, an image from `python3 -I
tools/nvme-test-image.py nvme.img`, then

```
qemu-system-i386 -machine q35 -cpu max,-hypervisor -m 1G -smp 2 -kernel kernel/lpl.kernel -display none -serial stdio \
  -drive file=nvme.img,if=none,id=lplnvme,format=raw -device nvme,serial=lplnvme0,drive=lplnvme \
  -append 'lpl.test.require=non_volatile_memory_express.*,message_signaled_interrupts.*'
```

KTAP `# Totals`, the same command with the row's `-machine`/`-cpu`:

| bench | main | branch |
|---|---|---|
| q35 `-cpu max,-hypervisor` | 852 pass, 1 fail
(`each_queue_interrupts_the_core_its_entry_names`) | 857 pass, 0 fail |
| q35 `-cpu max` | 853 / 0 | 856 / 0 |
| q35, default CPU | 850 / 1, the intermittent failure below | 854 / 0 |
| pc, no drive | 838 / 0 | 841 / 0 |
| APIC IDs 0x0, 0xFF, 0x100 (the bench of #564), with and without
`-hypervisor` | 18 / 0 | 21 / 0 |

At 7f0e459, which keeps today's rule,
`bare_metal_keeps_xapic_unless_the_firmware_handed_over_x2apic` and
`the_bootstrap_processor_runs_the_mode_this_machine_gets` fail;
`every_processor_runs_the_mode_the_bootstrap_processor_chose` fails once
an AP keeps the mode it woke in. Under qboot (`-bios
/usr/share/qemu/qboot.rom`), which leaves xAPIC with IDs 0xFF and 0x100,
both APs are refused by name.

## Not in it

- Interrupt remapping (#108): where the firmware hands over x2APIC on
bare metal, device messages and I/O APIC lines stay refused, the NVMe
test fails as on main, and the boot keeps the PIC model.
- Linux trusts only hypervisors whose guest support says so; every one
is still trusted.
- On main the NVMe test failed 2 of 16 boots on q35 `-cpu max` or the
default CPU; not seen in 30 boots of the branch, not investigated.
MasterLaplace added a commit that referenced this pull request Nov 8, 2026
…7, and count them (#804)

## What

An I/O APIC's version register declares up to 256 inputs, but its 8-bit
register index reaches entry 0x77 at most (82093AA datasheet, §3.1.1 and
§3.2.4). Since #799 an entry past 0x77 gets no register. The routing
scaffold now also keeps only the first 120 inputs of each unit, so a
Global System Interrupt past them belongs to no unit and no route
reaches it.

The inputs left out are counted once, when the scaffold runs, rather
than per route asked: `ioapic_inputs_unreachable` in the `[LPLTLM]
machine` record.

## Verify

- `an_io_apic_keeps_the_inputs_its_index_reaches` was red on a stub that
kept every declared input: 121 and 256 stayed whole.
- Server: q35 861/0, `-cpu max` 863/0, pc 855/0. Satellite: q35 711/0.
Each record says `ioapic_inputs_unreachable=0`.
- Two of three mutants fail the test: the limit one entry further, and
counting every declared input. Keeping every declared input in the unit
survives, since QEMU's I/O APIC declares 24.
- No budget's window holds the scaffold.

## Not in it

- A run with more than 120 declared inputs: QEMU has none, so the ID
register keeping its value is read in the code, not seen.

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

Labels

type:feature A new capability

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant