Repository navigation
fix(config-rom): bus-reset cache lifecycle and generation-wrap fixes - #221
Merged
Merged
Conversation
mrmidi
added a commit
that referenced
this pull request
Oct 23, 2026
…o block is unchanged After every bus reset discovery read each node's whole Config ROM again: bus info block, root directory, text leaves and unit directories, even though the previous generation's copy sat in ConfigROMStore. This has been the case since the first snapshot; the store borrowed Apple's IOFireWireROMCache states but never the part that skips the read. On an LS-9000 the rescan was ~550 ms of the 650 ms between a reset and the SBP-2 reconnect, and every device on the bus waits for it. Apple reads only the bus info block after a reset and keeps the cached ROM when the block is byte-identical (IOFireWireROMCache::hasROMChanged, IOFireWireROMCache.cpp:240-320; resume in IOFireWireDevice.cpp:727-743). The block holds the GUID, the ROM CRC and the ROM generation a device advances when its ROM changes. Do the same: - OnTopologyReady hands the scan the ROMs the reset edge suspended (ConfigROMStore::SuspendedROMs). - After the BIB, the speed probe and the IRM check, a node whose BIB equals a previous copy completes from that copy with the new generation and node ID (UnchangedROMPolicy, ContinueToRootDirectory). The speed probe and IRM check still run: both are per-generation bus state, not ROM content. - Apple's exceptions are kept: a generation-0 ROM without units is read again (devices that publish units late), and so is a copy that holds only the block. - A copy built around a failed or short detail read is marked readIncomplete and never reused, so a lost read heals at the next reset as before instead of being kept. - A manual reread passes no previous ROMs and reads in full. Only one generation back is kept (#221's store bound): a reset that interrupts a scan leaves nothing to compare, and the next scan reads in full. Tests: ROMScannerUnchangedROMTests scans a device, then scans it again with that result as the previous generation: unchanged BIB reads only q0, q2-q4; new node number still matches; advanced ROM generation, new header CRC, another GUID, a copy with a failed read, generation 0 without units, and no previous ROMs all read in full; the 255 -> 0 wrap reuses; the IRM check and the speed probe still run. Store: SuspendedROMs is the previous generation only, and races the erasing calls. FSM: Complete is reachable from every step before the root directory. ControllerCore: the scan request carries the suspended ROMs (the harness's ROMScanner::Start stub can now route to a recorder). Every rule mutation-checked (11 mutations, each caught). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e1ae8f2)
(cherry picked from commit f10e4ce)
(cherry picked from commit 1847198)
SuspendAll() gets its first production caller in this PR, which makes the rest of the ROM lifecycle worth checking. ValidateROM() is the only transition out of Suspended and has no callers anywhere — not in the driver, not in tests — so ROMState::Validated was unreachable and the `state == Validated` arms of SuspendAll() were dead. Rediscovery already does the work ValidateROM() was written for. Insert() overwrites romsByGuid_[guid] wholesale, state included, and romsByGenNode_ is keyed by (generation, nodeId), so after a reset the new generation gets fresh keys and the suspended entries are simply never looked up again. Wiring ValidateROM() up instead would put a second writer on the same entries and require deciding which one wins when Insert()'s richer-ROM guard declines an update — a second source of truth for no behavior gained. Nothing observes the difference today: FindByGuid() does not filter on state, and FindByNode(gen, nodeId, allowSuspended=false) looks up the current generation, whose key never matches a suspended entry. Remove the function, the enum value, and the dead arms, and say in both the enum comment and ConfigROM/README.md how a device actually comes back. If the store later needs to report which devices did not return, that closes the Suspended → Invalid → PruneInvalid loop and deserves its own change with hardware validation. (cherry picked from commit 5f22663)
… July ControllerCoreBusResetTests links ControllerCoreInterrupts.cpp against local stubs. main's reset path now also calls CyclePolicyCoordinator::OnCycleTooLong and SpeedPolicy::Reset, so the cherry-picked test needs stubs for both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FindByNode, FindLatestForNode and FindByGuid returned pointers into the store's maps after releasing its lock. The store is shared across queues: discovery inserts and the bus-reset edge suspends on the driver queue, while ExportConfigROM and TriggerROMRead read on the user-client queue. Insert already replaces entries in place, and SuspendAll is about to erase entries, so a caller could read freed memory. Return std::optional<ConfigROM> copied under the lock. The only production callers are the two user-client handlers; both are cold paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-node index is keyed by (generation, node) and nothing removed old generations in normal operation, so it grew by one entry per node per bus reset. Hardware generations are 8 bits and wrap after 255 (OHCI 1.2 Table 11-2): once the generation came back, Insert kept the earlier "richer" entry under the reused key, so a node could carry a different device's ROM. SuspendAll now erases entries that were already suspended before suspending the current ones, so the index holds at most the previous generation plus whatever discovery inserts for the new one. Nothing reads older generations: selector 14 refuses suspended entries and discovery re-inserts current ROMs. Apple keeps one revalidated cache per device and accumulates no history (IOFireWireDevice.cpp:708,742). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bus-reset FSM skipped the scan abort when lastGeneration_ was 0, and
ROMScanner::Abort and IsBusyFor treated Generation{0} as "no scan". The
generation is the raw 8-bit Self-ID value and wraps to 0 after 255 (OHCI
1.2 Table 11-2), so after the 256th reset:
- the next reset left the generation-0 scan running, and its completion
could still run discovery for a stale generation;
- a second Start at generation 0 (for example a manual ROM read) replaced
a running discovery scan instead of being refused.
Abort now cancels whatever scan is active. No scan for the new
generation can exist before Self-ID, so any active session at a reset
edge is stale. It returns whether a scan was aborted, and abortCount now
counts real aborts. IsBusyFor compares generations without a 0 special
case.
Tests: a generation-0 scan is refused a second Start and is abortable
(ROMScannerAbortTests), and a reset after a generation-0 topology aborts
the scan (BusResetCoordinatorTests, via a counting scanner stub).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two-argument FindByNode returned suspended entries without saying so; calling it from selector 14 is how pre-reset ROMs were exported as current. Merge the overloads and give allowSuspended no default, so every caller states whether it accepts a ROM from before the last bus reset. Production callers already passed it; tests that relied on the permissive overload now pass true. Factor the find-or-nullopt copy into CopyAt and use it for FindByNode and FindByGuid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ROM Explorer took its generation and node IDs from the app's cached topology. After a bus reset the driver moves on: selector 14 no longer serves the old generation, and node IDs can be reassigned. The explorer then reported "not cached", and a Read ROM polled the old generation until it timed out. Worse, the topology refresh re-selected the same node ID, which may now be a different device, and left the previous device's ROM on screen. - Before a cache load, a ROM read and each poll step, compare the view's generation with the driver's (getBusResetCount reports the generation of the driver's latest topology). On a mismatch, clear the selection, refresh the topology and say why. - When the topology generation changes, drop the selection instead of re-selecting by node ID. Tests cover the generation-change selection drop, same-generation refresh, and the staleness check. The driver round trip needs a live driver and is not covered by host tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ROMScanSession used gen_ == Generation{0} to mean "no scan running".
The bus generation is the raw 8-bit Self-ID value and rolls over to 0
after 255 (OHCI 1.2 Table 11-2), so after the 256th reset a real scan
at generation 0 looked finished from the start: MaybeFinish never fired
the completion, and the IRM verify handlers dropped their replies. A
generation-0 scan with no remote nodes never completed either.
The session now keeps an explicit scanActive_ flag and compares
generations only for equality, as Linux (int generation, -1 as the
out-of-range "invalid", ohci.c:2143) and Apple (scan->generation ==
fBusGeneration, IOFireWireController.cpp:2750) do.
ROMScanner::IsBusyFor read the session's gen_, which is only set when
the session's start task runs on the dispatch queue. Until then every
new session reported generation 0, so a request for generation 0 was
refused as a duplicate. The scanner now records the generation itself
at Start.
Tests: the empty-root-directory completion and IRM verify scans run at
generation 0 as well as a nonzero one; a generation-0 scan with no
remote nodes completes; and a manual dispatch queue shows the busy
check before the start task runs. Each was mutation-checked against
the old sentinel (MaybeFinish, both IRM handlers, IsBusyFor).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ConfigROMStore::Insert filled ConfigROM::firstSeen and lastValidated
when they were Generation{0}, which also misread a real generation 0
as "unset". Nothing in the driver or the app reads either field, and
no producer sets them, so both always equalled rom.gen. Remove the
fields, the fill-in code, and the test fixtures that set them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ConfigROMStore kept a second index by GUID and decided which copy to keep with it->second.gen.value < romCopy.gen.value. FindLatestForNode picked "latest" with rom.gen.value > latest->gen.value. The bus generation is the raw 8-bit Self-ID value and wraps from 255 to 0 (OHCI 1.2 Table 11-2), so after a wrap both orderings are backwards: the GUID copy stayed at generation 255 for good, and "latest" chose the suspended entry over the current one. Nothing reads either result. ExportConfigROM's FindLatestForNode fallback was dropped in merge 805def0, and the selector 14 contract is now exact generation or nothing (b098ba6). FindByGuid, Snapshot, SnapshotByState and Clear have no callers either. The GUID index was read only by InvalidateROM to find the GUID, then PruneInvalid removed the entry it had just marked. The store is now one (generation, node) index with no generation ordering. TriggerROMRead calls EraseByGuid, which removes the device's cached ROMs in every generation, as InvalidateROM already did for the node index, so the manual reread is not refused in favour of a richer cached copy. ROMState::Invalid had no other use and is gone. Tests: the FindLatestForNode/FindByGuid tests go with the API; the node-cache cases keep their FindByNode checks; EraseByGuid removes both generations of one device and nothing else, and lets the shorter reread in (mutation-checked: erase nothing, erase Fresh only); a reset across the 255 -> 0 rollover serves the new generation; the concurrency test now races lookups against SuspendAll and EraseByGuid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing calls it, and it treated Generation{0} as "no generation",
which is wrong after the 8-bit bus generation wraps (OHCI 1.2 Table
11-2). The scan session now tracks an explicit scanActive_ instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bus-reset suspension test only needs the reset side effect; the ignored [[nodiscard]] result raised -Wunused-result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o block is unchanged After every bus reset discovery read each node's whole Config ROM again: bus info block, root directory, text leaves and unit directories, even though the previous generation's copy sat in ConfigROMStore. This has been the case since the first snapshot; the store borrowed Apple's IOFireWireROMCache states but never the part that skips the read. On an LS-9000 the rescan was ~550 ms of the 650 ms between a reset and the SBP-2 reconnect, and every device on the bus waits for it. Apple reads only the bus info block after a reset and keeps the cached ROM when the block is byte-identical (IOFireWireROMCache::hasROMChanged, IOFireWireROMCache.cpp:240-320; resume in IOFireWireDevice.cpp:727-743). The block holds the GUID, the ROM CRC and the ROM generation a device advances when its ROM changes. Do the same: - OnTopologyReady hands the scan the ROMs the reset edge suspended (ConfigROMStore::SuspendedROMs). - After the BIB, the speed probe and the IRM check, a node whose BIB equals a previous copy completes from that copy with the new generation and node ID (UnchangedROMPolicy, ContinueToRootDirectory). The speed probe and IRM check still run: both are per-generation bus state, not ROM content. - Apple's exceptions are kept: a generation-0 ROM without units is read again (devices that publish units late), and so is a copy that holds only the block. - A copy built around a failed or short detail read is marked readIncomplete and never reused, so a lost read heals at the next reset as before instead of being kept. - A manual reread passes no previous ROMs and reads in full. Only one generation back is kept (#221's store bound): a reset that interrupts a scan leaves nothing to compare, and the next scan reads in full. Tests: ROMScannerUnchangedROMTests scans a device, then scans it again with that result as the previous generation: unchanged BIB reads only q0, q2-q4; new node number still matches; advanced ROM generation, new header CRC, another GUID, a copy with a failed read, generation 0 without units, and no previous ROMs all read in full; the 255 -> 0 wrap reuses; the IRM check and the speed probe still run. Store: SuspendedROMs is the previous generation only, and races the erasing calls. FSM: Complete is reachable from every step before the root directory. ControllerCore: the scan request carries the suspended ROMs (the harness's ROMScanner::Start stub can now route to a recorder). Every rule mutation-checked (11 mutations, each caught). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main now reads the bus-reset generation from SelfIDCount.selfIDGeneration (9aa7682); the reserved 0x06C register is gone from RegisterMap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mrmidi
force-pushed
the
fix/config-rom-suspend-on-reset
branch
from
October 23, 2026 08:24
06709ae to
33de3f5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Config ROM cache and scanner fixes around bus resets and the 8-bit bus generation wrap.
The bus generation is the raw 8-bit Self-ID value and rolls over from 255 to 0 (OHCI 1.2 Table 11-2), so generation 0 is a real value and generations can only be compared for equality. Linux (
ohci.c:2143,-1as the invalid sentinel) and Apple (IOFireWireController.cpp:2750,scan->generation == fBusGeneration) both do it that way. Several places here usedGeneration{0}as "none" or ordered generations with</>.Builds on gly11's #87 (the first four commits are cherry-picked with
-x, authorship kept).Cache lifecycle across bus resets
ExportConfigROMis now exact generation or nothing.ValidateROMpath (gly11, fix(config-rom): reject suspended generation exports #87).FindByNodereturned pointers into the store's maps after unlocking, while the driver queue inserts/erases and the user-client queue reads.FindByNode(gen, node, allowSuspended)with no default, so every caller states whether it accepts a pre-reset ROM.Generation 0 and the wrap
Abort/IsBusyForno longer treat 0 as "no scan".ROMScanSessionusedgen_ == 0as "not running", so after the 256th resetMaybeFinishnever fired and the IRM verify replies were dropped. It now keeps an explicitscanActive_.IsBusyForuses the generation recorded atStart. The session only learns its generation when its start task runs, so a generation-0 request could be refused as a duplicate in between.FindLatestForNodeordered generations with</>, which is backwards after a wrap. Nothing read either: the export fallback was dropped in 805def0, andFindByGuid,Snapshot,SnapshotByState,Clearhad no callers. The store is now a single (generation, node) index; the manual ROM re-read usesEraseByGuid.firstSeen/lastValidated(unread, and filled in using 0 as "unset") and the unusedGenerationContextPolicy.Reuse unchanged ROMs after a reset
IOFireWireROMCache::hasROMChanged,IOFireWireROMCache.cpp:240-320;IOFireWireDevice.cpp:727-743). The block carries the GUID, the ROM CRC and the ROM generation a device advances when its ROM changes.ConfigROMStore::SuspendedROMs). After the bus info block, the speed probe and the IRM check (both still run: they are bus state, not ROM content), a matching node completes from its copy with the new generation and node ID (UnchangedROMPolicy).readIncomplete), so a lost read heals at the next reset; a manual re-read.App
Testing
EraseByGuid, concurrent lookups vsSuspendAll/EraseByGuid, ROM Explorer staleness checks.bus info block unchangedonce (/usr/bin/log show --predicate 'eventMessage CONTAINS "bus info block unchanged"').🤖 Generated with Claude Code