Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,29 @@ jobs:
# ASFW.xcodeproj is generated from project.yml (XcodeGen) and committed.
# Fail if they drifted apart — someone edited project.yml or added source
# files without regenerating, or hand-edited the pbxproj. If this fails
# after an xcodegen version bump changed the output format, regenerate
# locally with the new version and commit.
# after the pinned xcodegen version changes, regenerate locally with the
# same version and commit.
- name: Verify ASFW.xcodeproj matches project.yml (XcodeGen)
run: |
brew install xcodegen
xcodegen --version
xcodegen generate --quiet
# Single source of truth, shared with build.sh, so a contributor's
# locally installed xcodegen can never disagree with this check.
. ./.xcodegen-version
XCODEGEN_ARCHIVE="$RUNNER_TEMP/xcodegen.zip"
XCODEGEN_ROOT="$RUNNER_TEMP/xcodegen-release"
curl --fail --location --retry 3 \
--output "$XCODEGEN_ARCHIVE" \
"https://github.com/yonaskolb/XcodeGen/releases/download/${XCODEGEN_VERSION}/xcodegen.zip"
echo "${XCODEGEN_SHA256} ${XCODEGEN_ARCHIVE}" | shasum -a 256 -c -
ditto -x -k "$XCODEGEN_ARCHIVE" "$XCODEGEN_ROOT"
XCODEGEN_BIN="$XCODEGEN_ROOT/xcodegen/bin/xcodegen"
"$XCODEGEN_BIN" --version
"$XCODEGEN_BIN" generate --quiet
# Diff only the pbxproj: scheme files are cosmetically rewritten by
# any open Xcode (version attr, BuildableName flavor, empty blocks),
# so they flip-flop between xcodegen and Xcode styles; their semantic
# content comes from project.yml either way.
if ! git diff --exit-code --stat -- ASFW.xcodeproj/project.pbxproj project.yml; then
echo "::error::ASFW.xcodeproj is out of sync with project.yml. Run 'xcodegen generate' locally and commit the regenerated project (see README → Building)."
echo "::error::ASFW.xcodeproj is out of sync with project.yml. Regenerate it with XcodeGen ${XCODEGEN_VERSION} and commit the result (see README → Building)."
exit 1
fi

Expand Down
18 changes: 18 additions & 0 deletions .xcodegen-version
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# XcodeGen release pinned for generating ASFW.xcodeproj from project.yml.
#
# Both CI (.github/workflows/build-and-test.yml) and build.sh read this file, so
# the version that regenerates the committed project is the same everywhere.
# Homebrew's xcodegen floats and its output is NOT byte-identical across
# releases (2.45.4 -> 2.46.0 reorders the pbxproj `targets` list), which is why
# the pin exists and why build.sh refuses to regenerate with a different one.
#
# Upgrading — do all three steps together, or CI's drift check will fail:
# 1. bump XCODEGEN_VERSION below
# 2. curl the release zip, put its `shasum -a 256` in XCODEGEN_SHA256
# curl -fL -o /tmp/xcodegen.zip \
# https://github.com/yonaskolb/XcodeGen/releases/download/<ver>/xcodegen.zip
# shasum -a 256 /tmp/xcodegen.zip
# 3. regenerate ASFW.xcodeproj with that exact version and commit it alongside
#
XCODEGEN_VERSION=2.46.0
XCODEGEN_SHA256=4d9e34b62172d645eed6457cac13fc222569974098ef4ee9c3368bedf0196806
2 changes: 1 addition & 1 deletion ASFW.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3000,8 +3000,8 @@
projectDirPath = "";
projectRoot = "";
targets = (
EE5A446B3669D0171F9606EE /* ASFW */,
0EB9A8DA75D08971084A440A /* ASFWDriver */,
EE5A446B3669D0171F9606EE /* ASFW */,
348672D607701677ABCB567C /* ASFWTests */,
);
};
Expand Down
22 changes: 15 additions & 7 deletions ASFW.xcodeproj/xcshareddata/xcschemes/ASFWDriver.xcscheme
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.3">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
Expand All @@ -15,7 +16,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0EB9A8DA75D08971084A440A"
BuildableName = "net.mrmidi.ASFW.ASFWDriver.dext"
BuildableName = "ASFWDriver.dext"
BlueprintName = "ASFWDriver"
ReferencedContainer = "container:ASFW.xcodeproj">
</BuildableReference>
Expand All @@ -26,18 +27,21 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
onlyGenerateCoverageForSpecifiedTargets = "NO">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0EB9A8DA75D08971084A440A"
BuildableName = "net.mrmidi.ASFW.ASFWDriver.dext"
BuildableName = "ASFWDriver.dext"
BlueprintName = "ASFWDriver"
ReferencedContainer = "container:ASFW.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
Expand All @@ -54,11 +58,13 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0EB9A8DA75D08971084A440A"
BuildableName = "net.mrmidi.ASFW.ASFWDriver.dext"
BuildableName = "ASFWDriver.dext"
BlueprintName = "ASFWDriver"
ReferencedContainer = "container:ASFW.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
Expand All @@ -71,11 +77,13 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0EB9A8DA75D08971084A440A"
BuildableName = "net.mrmidi.ASFW.ASFWDriver.dext"
BuildableName = "ASFWDriver.dext"
BlueprintName = "ASFWDriver"
ReferencedContainer = "container:ASFW.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
Expand Down
139 changes: 125 additions & 14 deletions ASFWDriver/Async/Contexts/ATContextBase.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,9 @@ class ATContextBase : public ContextBase<Derived, Tag> {
/**
* \brief Scan for completed descriptors and extract completion status.
*
* Walks the descriptor ring from head index, checking xferStatus field
* for hardware completion. Extracts event code, timestamp, and tLabel
* on first completed descriptor found.
* Walks the descriptor ring from head index, checking the packet chain's
* OUTPUT_LAST xferStatus for hardware completion. Extracts event code,
* timestamp, and tLabel on the first completed packet found.
*
* \return TxCompletion if descriptor completed, std::nullopt if none ready
*
Expand All @@ -245,19 +245,26 @@ class ATContextBase : public ContextBase<Derived, Tag> {
* 2. Load head index (atomic acquire)
* 3. If head == tail, ring is empty → return nullopt
* 4. Read descriptor at head index
* 5. If xferStatus == 0, descriptor not yet completed → return nullopt
* 6. Extract event code from xferStatus[4:0]
* 7. Extract timestamp from timeStamp field
* 8. If OUTPUT_LAST_Immediate, extract tLabel from packet header
* 9. Advance head index: (head + N) % capacity, where N = descriptor block count
* 10. Unlock context, return TxCompletion
* 5. For OUTPUT_MORE, check the following OUTPUT_LAST completion status
* 6. If the packet's terminal xferStatus == 0, return nullopt
* 7. Extract event code from xferStatus[4:0]
* 8. Extract timestamp from timeStamp field
* 9. Extract tLabel from the immediate packet header
* 10. Advance head beyond the completed packet chain
* 11. Unlock context, return TxCompletion
*
* **Apple Pattern**
* ChannelBundle::ScanNextATReqCompletion():
* - Checks xferStatus != 0 for completion
* - Extracts ack code and event code from status word
* - Extracts tLabel from packet header for response matching
* - Advances completion cursor
* AppleFWOHCI_AsyncTransmit::checkForCompletedElements():
* - Checks the terminal descriptor status of each pending ATxElement
* - Leaves the element queued while the terminal status is zero
* - Dispatches completion only after the terminal status becomes non-zero
*
* Apple walks a linked list of ATxElements, so a zero-status element only
* clears its "reap the predecessor" flag and iteration continues past it
* (symbol offsets 0xf0e8-0xf10a, 0xf188-0xf195). This ring has a single
* head cursor instead, so a zero-status chain at head necessarily blocks
* everything behind it — hence the quiesced-context escape in
* InspectChainTail(), which Apple gets from resetDMA() instead.
*
* **Thread Safety**
* Serialized via IOLock. Safe to call concurrently with SubmitChain().
Expand Down Expand Up @@ -314,6 +321,12 @@ class ATContextBase : public ContextBase<Derived, Tag> {
uint16_t timeStamp{0};
};

enum class ChainTailScanResult : uint8_t {
NotRecognized,
Pending,
Advanced,
};

/// Descriptor ring for tracking in-flight chains
DescriptorRing* ring_{nullptr};

Expand Down Expand Up @@ -341,6 +354,9 @@ class ATContextBase : public ContextBase<Derived, Tag> {
size_t capacity) noexcept;
[[nodiscard]] bool LoadScanState(ScanState& state) noexcept;
void FetchScanDescriptor(const ScanState& state) noexcept;
[[nodiscard]] ChainTailScanResult InspectChainTail(
const ScanState& state) noexcept;
[[nodiscard]] bool IsContextQuiesced() noexcept;
void HandlePendingDescriptor(const ScanState& state) noexcept;
[[nodiscard]] bool IsOrphanedDescriptor(const ScanState& state,
uint32_t& commandPtrAddr,
Expand Down Expand Up @@ -606,6 +622,16 @@ std::optional<TxCompletion> ATContextBase<Derived, Tag>::ScanCompletion() noexce
}

if (state.xferStatus == 0) {
switch (InspectChainTail(state)) {
case ChainTailScanResult::Advanced:
continue;
case ChainTailScanResult::Pending:
unlock();
return std::nullopt;
case ChainTailScanResult::NotRecognized:
break;
}

HandlePendingDescriptor(state);
unlock();
return std::nullopt;
Expand Down Expand Up @@ -826,6 +852,91 @@ void ATContextBase<Derived, Tag>::FetchScanDescriptor(const ScanState& state) no
}
}

template<typename Derived, ContextRole Tag>
typename ATContextBase<Derived, Tag>::ChainTailScanResult
ATContextBase<Derived, Tag>::InspectChainTail(const ScanState& state) noexcept {
const uint16_t controlHi = static_cast<uint16_t>(
state.desc->control >> HW::OHCIDescriptor::kControlHighShift);
const uint8_t command = static_cast<uint8_t>(
(controlHi >> HW::OHCIDescriptor::kCmdShift) & 0xF);
if (command != HW::OHCIDescriptor::kCmdOutputMore) {
return ChainTailScanResult::NotRecognized;
}

const uint8_t key = static_cast<uint8_t>(
(controlHi >> HW::OHCIDescriptor::kKeyShift) & 0x7);
const uint8_t precursorBlocks =
(key == HW::OHCIDescriptor::kKeyImmediate) ? 2 : 1;
const size_t tailIndex =
(state.headIndex + precursorBlocks) % state.capacity;
if (tailIndex == state.tailIndex) {
return ChainTailScanResult::NotRecognized;
}

ScanState tailState;
tailState.capacity = state.capacity;
tailState.headIndex = tailIndex;
tailState.desc = ring_->At(tailIndex);
if (!tailState.desc) {
return ChainTailScanResult::NotRecognized;
}
tailState.isImmediate = HW::IsImmediate(*tailState.desc);
FetchScanDescriptor(tailState);
const uint16_t tailControlHi = static_cast<uint16_t>(
tailState.desc->control >> HW::OHCIDescriptor::kControlHighShift);
const uint8_t tailCommand = static_cast<uint8_t>(
(tailControlHi >> HW::OHCIDescriptor::kCmdShift) & 0xF);
if (tailCommand != HW::OHCIDescriptor::kCmdOutputLast) {
return ChainTailScanResult::NotRecognized;
}

if (HW::AT_xferStatus(*tailState.desc) == 0) {
// A running context has simply not finished this packet yet. The command
// pointer may already reference the pending tail, so the OUTPUT_MORE
// precursor is NOT orphaned and must stay queued.
// AppleFWOHCI 5.5.9 checkForCompletedElements() likewise leaves an
// ATxElement queued while its terminal descriptor status is zero
// (symbol offsets 0xf0e8-0xf10a), and Linux returns 0 from
// handle_at_packet() to stop iteration:
// references/linux-ohci-firewire-low-level-stack/drivers/firewire/ohci.c:1354-1366.
//
// A quiesced context is different: hardware will never write a status,
// so treating the chain as merely pending would wedge the ring head
// forever. Linux gates the same early return on !ctx->flushing
// (ohci.c:1364) and drains via at_context_flush() after context_stop()
// (ohci.c:2000-2010); AppleFWOHCI calls resetDMA() after stopDMA() in
// handleBusResetInt() (symbol offsets 0x5fae-0x5fd2), which frees every
// pending element regardless of status. OHCI 1.2 draft clause 7.2.3.3:
// hardware may leave unsent packets in the AT queues for software to
// drain. ASFW has no equivalent flush entry point yet, so fall through
// to the orphan path, which is what releases the ring today.
if (IsContextQuiesced()) {
return ChainTailScanResult::NotRecognized;
}
return ChainTailScanResult::Pending;
}

// Linux records completion on the packet's OUTPUT_LAST descriptor:
// references/linux-ohci-firewire-low-level-stack/drivers/firewire/ohci.c:1298-1310,1354-1366.
ClearDescriptorBlocks(state.headIndex, precursorBlocks, state.capacity);
ring_->SetHead(tailIndex);
ASFW_LOG_V2(Async,
"ScanCompletion: head %zu→%zu (completed OUTPUT_LAST after OUTPUT_MORE)",
state.headIndex, tailIndex);
return ChainTailScanResult::Advanced;
}

template<typename Derived, ContextRole Tag>
bool ATContextBase<Derived, Tag>::IsContextQuiesced() noexcept {
// Deliberately narrower than IsOrphanedDescriptor(): only run==0 && active==0
// proves hardware will never write another status. That function's second
// clause (commandPtr != headIOVA) also fires for a *live* chain whose command
// pointer has already advanced to a pending OUTPUT_LAST, which is exactly the
// false positive this scan path must not reintroduce.
const uint32_t controlReg = this->ReadControl();
return (controlReg & (kContextControlRunBit | kContextControlActiveBit)) == 0;
}

template<typename Derived, ContextRole Tag>
void ATContextBase<Derived, Tag>::HandlePendingDescriptor(const ScanState& state) noexcept {
uint32_t commandPtrAddr = 0;
Expand Down
10 changes: 6 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ Two components:

**`ASFW.xcodeproj` is GENERATED from the root `project.yml` (XcodeGen).** Never
edit the pbxproj or hand-tune settings in Xcode — change `project.yml` and run
`xcodegen generate` (`./build.sh` does it automatically when xcodegen is
installed). After adding/removing/renaming source files, regenerate and commit
the updated `ASFW.xcodeproj` together with `project.yml`. Output is
deterministic; the generated project stays committed so CI builds without
`xcodegen generate` with the version pinned in `.xcodegen-version`
(`./build.sh` checks an installed xcodegen automatically; use
`--no-xcodegen` only to build the committed project as-is). After
adding/removing/renaming source files, regenerate and commit the updated
`ASFW.xcodeproj` together with `project.yml`. Output is deterministic for the
pinned version; the generated project stays committed so CI builds without
xcodegen. (`ADKVirtualAudioLab/` has its own separate `project.yml`.)

**Primary build (Xcode — required for signing and producing `.dext`):**
Expand Down
Loading