diff --git a/ASFW.xcodeproj/project.pbxproj b/ASFW.xcodeproj/project.pbxproj index 47bc3d01..fdd59b77 100644 --- a/ASFW.xcodeproj/project.pbxproj +++ b/ASFW.xcodeproj/project.pbxproj @@ -114,6 +114,7 @@ 47C7D2797F3974490450A45A /* AVCNestedChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB92C50A509F45F51EFD3F03 /* AVCNestedChannelTests.swift */; }; 47F41CED6EB2EE793A31B6E5 /* ASFWAudioDriverControls.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 018751CF8CA051FDDE9B2B06 /* ASFWAudioDriverControls.cpp */; }; 48359CD69DF9A8F5868E12C0 /* ASFWMCPLiveDriverControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1391A71639068D8291B32AF /* ASFWMCPLiveDriverControl.swift */; }; + 49F6698714AAE6DA7A1CE7AE /* RMEFireface800Protocol.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B80BA7172EA0EC366CA7C8EC /* RMEFireface800Protocol.cpp */; }; 4A7F808CC9DBBBED16B2F07A /* ConfigROMBuilder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 290CF8A5D9D6CABD1B3BF37A /* ConfigROMBuilder.cpp */; }; 4B85BC02C027180D070F4DFF /* DICETcatProtocol.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3B6A6E8C021FEF7B9569D79A /* DICETcatProtocol.cpp */; }; 4C587E86B66E99AA77CF17A1 /* ASFWDriverUserClient.iig in Sources */ = {isa = PBXBuildFile; fileRef = AF46E21DCF97D1A3F7DFDA09 /* ASFWDriverUserClient.iig */; }; @@ -334,6 +335,7 @@ F118D01B178AD54981507B90 /* ROMScanSessionIRM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3D1606D8DEE1120C1646AB8D /* ROMScanSessionIRM.cpp */; }; F193984E9950C4C137BFAD6D /* MCPDiceTcatToolsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B7F3D2FEC0DA11D8D9F0AC /* MCPDiceTcatToolsTests.swift */; }; F302759145BDF336C186D2BC /* HardwareInterface.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9F7E564F41599A92B7D55B12 /* HardwareInterface.cpp */; }; + F34A587E2B103749D7490B97 /* ASFWMCPContinuousTxTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE56C6DD9B345116DD0699B /* ASFWMCPContinuousTxTools.swift */; }; F3EC20E3CDECF9FD90E56995 /* ASFWSCSIController.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3558EAA4E19D859BC8594170 /* ASFWSCSIController.cpp */; }; F4A99690858D37ACCC9CECB6 /* SystemLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB2E35051CCCFD3E4CD6E2F /* SystemLogsView.swift */; }; F576426B7995BB3474228911 /* ASFWDriverUserClient.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 5364BA023A0C10D7791261A1 /* ASFWDriverUserClient.cpp */; }; @@ -557,8 +559,10 @@ 3B0597EE0A4E4C496E0C2EA6 /* ASFWAudioDriverZts.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ASFWAudioDriverZts.cpp; sourceTree = ""; }; 3B294E73813DAE348F5D045A /* IDeviceManager.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = IDeviceManager.hpp; sourceTree = ""; }; 3B6A6E8C021FEF7B9569D79A /* DICETcatProtocol.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = DICETcatProtocol.cpp; sourceTree = ""; }; + 3BA650C15D778F327D36918D /* RMEAudioProfiles.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RMEAudioProfiles.hpp; sourceTree = ""; }; 3BC1BCBC9E52B53B87DEEA69 /* CIPHeader.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CIPHeader.hpp; sourceTree = ""; }; 3CCCE7A077DAD59020AD49F2 /* BusResetCoordinatorActions.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = BusResetCoordinatorActions.cpp; sourceTree = ""; }; + 3CE56C6DD9B345116DD0699B /* ASFWMCPContinuousTxTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASFWMCPContinuousTxTools.swift; sourceTree = ""; }; 3CF6AA538A8321AEB7DDB655 /* MCPHardwareSmokeRunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCPHardwareSmokeRunnerTests.swift; sourceTree = ""; }; 3D1606D8DEE1120C1646AB8D /* ROMScanSessionIRM.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ROMScanSessionIRM.cpp; sourceTree = ""; }; 3D26D51E676FCE0D7C7747FD /* InterruptDispatcher.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = InterruptDispatcher.hpp; sourceTree = ""; }; @@ -913,6 +917,7 @@ B7E03EB5573EA30EB8F2D42B /* PingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingView.swift; sourceTree = ""; }; B7E5492B36B350E48E9214A8 /* AudioNubPublisher.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AudioNubPublisher.cpp; sourceTree = ""; }; B808B46CF106706A04C126DB /* ASFWMCPAvcFcpTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASFWMCPAvcFcpTools.swift; sourceTree = ""; }; + B80BA7172EA0EC366CA7C8EC /* RMEFireface800Protocol.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = RMEFireface800Protocol.cpp; sourceTree = ""; }; B8B02346383E39CA66366AAE /* AudioDriverConfig.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AudioDriverConfig.cpp; sourceTree = ""; }; B9E220CDF43AD389BD345A8F /* DriverKitOwnership.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DriverKitOwnership.hpp; sourceTree = ""; }; BA6781D8E0522CE83CECCD4F /* Error.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Error.hpp; sourceTree = ""; }; @@ -926,6 +931,7 @@ BCD6B302DCC686B9FD46CE06 /* RolePolicy.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = RolePolicy.cpp; sourceTree = ""; }; BD88A8D5D9A4D4FDC7E5A71A /* ConfigROMParser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ConfigROMParser.hpp; sourceTree = ""; }; BD92B955A8BFE30B33FB1F45 /* ATManagerImpl.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ATManagerImpl.hpp; sourceTree = ""; }; + BDCF2C69EC4FAB205B581D26 /* RMEFireface800Protocol.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RMEFireface800Protocol.hpp; sourceTree = ""; }; BE308A43E548024E6623D4A8 /* bump.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = bump.sh; sourceTree = ""; }; BF49A4417B0371208665ECA7 /* StreamFormatParser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = StreamFormatParser.hpp; sourceTree = ""; }; BF9FE813F630AD0832D9E674 /* FWDevice.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = FWDevice.hpp; sourceTree = ""; }; @@ -1650,6 +1656,7 @@ 4D2DB64009F93C974F2DF20F /* DICE */, FD14F207887135CE102F8ACA /* Duplex */, 6DF35E1146D265DA8C2B7611 /* Oxford */, + 83A156BB194E66B40C582BD8 /* RME */, ); path = Protocols; sourceTree = ""; @@ -1807,6 +1814,7 @@ 077468963C2F7E7BB91E28CB /* ASFWMCPBeBoBTools.swift */, 9D6A93FFD51132B2A4F8B00E /* ASFWMCPBusResetTools.swift */, 969C58B339AB3E30DB3B3E45 /* ASFWMCPCmpTools.swift */, + 3CE56C6DD9B345116DD0699B /* ASFWMCPContinuousTxTools.swift */, 0417EE4C83805D8B15818D95 /* ASFWMCPCore.swift */, EE503DDF07D0510DECCEBD3B /* ASFWMCPDiceTcatTools.swift */, 42FD0F9803524954295461C0 /* ASFWMCPDriverControl.swift */, @@ -1993,6 +2001,15 @@ path = Discovery; sourceTree = ""; }; + 83A156BB194E66B40C582BD8 /* RME */ = { + isa = PBXGroup; + children = ( + B80BA7172EA0EC366CA7C8EC /* RMEFireface800Protocol.cpp */, + BDCF2C69EC4FAB205B581D26 /* RMEFireface800Protocol.hpp */, + ); + path = RME; + sourceTree = ""; + }; 8416E8873F248937DC1F725E /* Config */ = { isa = PBXGroup; children = ( @@ -2631,6 +2648,7 @@ F1BF9794C22F3E1693A6CCF0 /* FocusriteAudioProfiles.hpp */, ADA183665D23E96057546B82 /* MidasAudioProfiles.hpp */, 0C844DC3BD4E0AE7D1081F52 /* PreSonusAudioProfiles.hpp */, + 3BA650C15D778F327D36918D /* RMEAudioProfiles.hpp */, C9474A3E174E5254247327C6 /* TerraTecAudioProfiles.hpp */, ); path = Vendors; @@ -2884,8 +2902,8 @@ projectDirPath = ""; projectRoot = ""; targets = ( - EE5A446B3669D0171F9606EE /* ASFW */, 0EB9A8DA75D08971084A440A /* ASFWDriver */, + EE5A446B3669D0171F9606EE /* ASFW */, 348672D607701677ABCB567C /* ASFWTests */, ); }; @@ -3099,6 +3117,7 @@ 3ECDDF5924BE520E1750A8FF /* PostResetTimingCoordinator.cpp in Sources */, 270027C27155433C83E72998 /* PowerLinkPolicyCoordinator.cpp in Sources */, AE6B45BB973CF543649B851E /* PreSonusStudioLiveProfile.cpp in Sources */, + 49F6698714AAE6DA7A1CE7AE /* RMEFireface800Protocol.cpp in Sources */, 3455F76072D6ED0ED7E7B77E /* ROMReader.cpp in Sources */, 55D2ED0B592528E5950795D3 /* ROMScanSession.cpp in Sources */, 3125B234BDC6F30DF047EC94 /* ROMScanSessionDetails.cpp in Sources */, @@ -3191,6 +3210,7 @@ 86AAAB1DA5BB95E96BAFFBB7 /* ASFWMCPBeBoBTools.swift in Sources */, 6FA31DB6C2C615B5E0AFBB7C /* ASFWMCPBusResetTools.swift in Sources */, 963574C5C0B791D8C9BC3322 /* ASFWMCPCmpTools.swift in Sources */, + F34A587E2B103749D7490B97 /* ASFWMCPContinuousTxTools.swift in Sources */, 7A904CEDE54313921506AFCB /* ASFWMCPControlViewModel.swift in Sources */, EE252C976DEC6C30E860638C /* ASFWMCPCore.swift in Sources */, 960F055B6D7B32200D416CDB /* ASFWMCPDiceTcatTools.swift in Sources */, @@ -3410,7 +3430,7 @@ CODE_SIGN_ENTITLEMENTS = "$(ASFW_DEXT_ENTITLEMENTS_$(ASFW_ENABLE_SCSI))"; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 100; DEVELOPMENT_TEAM = F6YA6B56LR; DRIVERKIT_DEPLOYMENT_TARGET = 25.0; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3492,7 +3512,7 @@ CODE_SIGN_ENTITLEMENTS = "$(ASFW_DEXT_ENTITLEMENTS_$(ASFW_ENABLE_SCSI))"; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 100; DEVELOPMENT_TEAM = F6YA6B56LR; DRIVERKIT_DEPLOYMENT_TARGET = 25.0; ENABLE_USER_SCRIPT_SANDBOXING = NO; diff --git a/ASFW.xcodeproj/xcshareddata/xcschemes/ASFWDriver.xcscheme b/ASFW.xcodeproj/xcshareddata/xcschemes/ASFWDriver.xcscheme index 09c8b9be..0d79cf89 100644 --- a/ASFW.xcodeproj/xcshareddata/xcschemes/ASFWDriver.xcscheme +++ b/ASFW.xcodeproj/xcshareddata/xcschemes/ASFWDriver.xcscheme @@ -1,10 +1,11 @@ + version = "1.7"> + buildImplicitDependencies = "YES" + runPostActionsOnFailure = "NO"> @@ -26,18 +27,21 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + onlyGenerateCoverageForSpecifiedTargets = "NO"> + + + + + + diff --git a/ASFW.xctestplan b/ASFW.xctestplan index d5250085..5c2c6f93 100644 --- a/ASFW.xctestplan +++ b/ASFW.xctestplan @@ -1,7 +1,7 @@ { "configurations" : [ { - "id" : "68101470-79D0-48C5-ACA4-8865D9C3B598", + "id" : "50727156-0551-43F8-87AB-D75879B2CA5B", "name" : "Test Scheme Action", "options" : { @@ -9,9 +9,9 @@ } ], "defaultOptions" : { - "codeCoverage" : false, - "performanceAntipatternCheckerEnabled" : true, - "targetForVariableExpansion" : { + "codeCoverage" : false, + "performanceAntipatternCheckerEnabled" : true, + "targetForVariableExpansion" : { "containerPath" : "container:ASFW.xcodeproj", "identifier" : "EE5A446B3669D0171F9606EE", "name" : "ASFW" @@ -19,7 +19,6 @@ }, "testTargets" : [ { - "parallelizable" : true, "target" : { "containerPath" : "container:ASFW.xcodeproj", "identifier" : "348672D607701677ABCB567C", diff --git a/ASFW/ASFWDriverConnector.swift b/ASFW/ASFWDriverConnector.swift index 99d79b31..c7cc81dd 100644 --- a/ASFW/ASFWDriverConnector.swift +++ b/ASFW/ASFWDriverConnector.swift @@ -60,6 +60,10 @@ final class ASFWDriverConnector: ObservableObject { case requestUserBusReset = 61 case startAudioStreaming = 62 case stopAudioStreaming = 63 + // Stage 6 (dev-only): continuous silent isoch TX cadence. + case startContinuousIsochTxSilence = 64 + case stopContinuousIsochTxSilence = 65 + case getContinuousIsochTxHealth = 66 } // MARK: - Re-exported Models diff --git a/ASFW/DriverConnector+Status.swift b/ASFW/DriverConnector+Status.swift index 2a9471d8..37c5d364 100644 --- a/ASFW/DriverConnector+Status.swift +++ b/ASFW/DriverConnector+Status.swift @@ -221,4 +221,97 @@ extension ASFWDriverConnector { } return message } + + // MARK: - Stage 6 (dev-only continuous silence) + + /// Starts a continuous (not bounded-to-100ms) silent isoch TX cadence for + /// a device protocol that implements it (currently only RME Fireface + /// 800). Never reachable through real CoreAudio StartIO. + func startContinuousIsochTxSilence(guid: UInt64) -> Bool { + guard isConnected else { + log("startContinuousIsochTxSilence: Not connected", level: .warning) + return false + } + + var input: [UInt64] = [guid] + let kr = IOConnectCallScalarMethod( + connection, + Method.startContinuousIsochTxSilence.rawValue, + &input, + UInt32(input.count), + nil, + nil + ) + guard kr == KERN_SUCCESS else { + let message = "startContinuousIsochTxSilence failed: \(interpretIOReturn(kr))" + log(message, level: .error) + lastError = message + return false + } + return true + } + + func stopContinuousIsochTxSilence(guid: UInt64) -> Bool { + guard isConnected else { + log("stopContinuousIsochTxSilence: Not connected", level: .warning) + return false + } + + var input: [UInt64] = [guid] + let kr = IOConnectCallScalarMethod( + connection, + Method.stopContinuousIsochTxSilence.rawValue, + &input, + UInt32(input.count), + nil, + nil + ) + guard kr == KERN_SUCCESS else { + let message = "stopContinuousIsochTxSilence failed: \(interpretIOReturn(kr))" + log(message, level: .error) + lastError = message + return false + } + return true + } + + struct ContinuousIsochTxHealth: Equatable { + let running: Bool + let dead: Bool + let eventError: Bool + let anchorExecutions: UInt32 + let lastAnchorTimestamp: UInt16 + } + + func getContinuousIsochTxHealth(guid: UInt64) -> ContinuousIsochTxHealth? { + guard isConnected else { + log("getContinuousIsochTxHealth: Not connected", level: .warning) + return nil + } + + var input: [UInt64] = [guid] + var output = [UInt64](repeating: 0, count: 5) + var outputCount: UInt32 = 5 + let kr = IOConnectCallScalarMethod( + connection, + Method.getContinuousIsochTxHealth.rawValue, + &input, + UInt32(input.count), + &output, + &outputCount + ) + guard kr == KERN_SUCCESS, outputCount == 5 else { + let message = "getContinuousIsochTxHealth failed: \(interpretIOReturn(kr))" + log(message, level: .error) + lastError = message + return nil + } + return ContinuousIsochTxHealth( + running: output[0] != 0, + dead: output[1] != 0, + eventError: output[2] != 0, + anchorExecutions: UInt32(truncatingIfNeeded: output[3]), + lastAnchorTimestamp: UInt16(truncatingIfNeeded: output[4]) + ) + } } diff --git a/ASFW/MCP/ASFWMCPContinuousTxTools.swift b/ASFW/MCP/ASFWMCPContinuousTxTools.swift new file mode 100644 index 00000000..aa8041a9 --- /dev/null +++ b/ASFW/MCP/ASFWMCPContinuousTxTools.swift @@ -0,0 +1,81 @@ +import Foundation + +// Stage 6 (dev-only): continuous (not bounded-to-100ms) silent isoch TX +// cadence, for device protocols that implement IDeviceProtocol's continuous- +// playback hooks (currently only RME Fireface 800). Reachable only through +// this developer-gated MCP path -- never through real CoreAudio StartIO, +// which must keep returning kIOReturnUnsupported. + +extension ASFWMCPToolCatalog { + static let continuousTxTools: [ASFWMCPToolDefinition] = [ + ASFWMCPToolDefinition( + name: "asfw_continuous_tx_start_dev", + group: "isoch_dev", + visibility: .developerWrite, + readOnly: false, + idempotent: false, + summary: "Start a continuous (unbounded) silent isoch TX cadence on the held playback route (targetGuid required). Never enables real Core Audio streaming." + ), + ASFWMCPToolDefinition( + name: "asfw_continuous_tx_stop_dev", + group: "isoch_dev", + visibility: .developerWrite, + readOnly: false, + idempotent: true, + summary: "Stop an active continuous silent isoch TX cadence and release its held IRM route (targetGuid required)." + ), + ASFWMCPToolDefinition( + name: "asfw_continuous_tx_health_dev", + group: "isoch_dev", + visibility: .readOnly, + readOnly: true, + idempotent: false, + summary: "Non-blocking health snapshot of an active continuous silent isoch TX cadence (targetGuid required)." + ) + ] +} + +struct ASFWMCPContinuousTxReceipt: Equatable { + let targetGuid: UInt64 + let started: Bool + let status: Int32 + + var ok: Bool { status == 0 } + + var mcpValue: ASFWMCPValue { + .object([ + "targetGuid": .string(String(format: "0x%016llX", targetGuid)), + "action": .string(started ? "start" : "stop"), + "status": .int(Int(status)), + "ok": .bool(ok), + ]) + } +} + +struct ASFWMCPContinuousTxHealthReceipt: Equatable { + let targetGuid: UInt64 + let health: ASFWDriverConnector.ContinuousIsochTxHealth? + let status: Int32 + + var ok: Bool { status == 0 } + + var mcpValue: ASFWMCPValue { + guard let health else { + return .object([ + "targetGuid": .string(String(format: "0x%016llX", targetGuid)), + "status": .int(Int(status)), + "ok": .bool(false), + ]) + } + return .object([ + "targetGuid": .string(String(format: "0x%016llX", targetGuid)), + "status": .int(Int(status)), + "ok": .bool(ok), + "running": .bool(health.running), + "dead": .bool(health.dead), + "eventError": .bool(health.eventError), + "anchorExecutions": .int(Int(health.anchorExecutions)), + "lastAnchorTimestamp": .int(Int(health.lastAnchorTimestamp)), + ]) + } +} diff --git a/ASFW/MCP/ASFWMCPDriverControl.swift b/ASFW/MCP/ASFWMCPDriverControl.swift index 1dae5c86..f3606ac5 100644 --- a/ASFW/MCP/ASFWMCPDriverControl.swift +++ b/ASFW/MCP/ASFWMCPDriverControl.swift @@ -13,6 +13,8 @@ protocol ASFWDriverControlling { func executeCompareSwap(_ request: ASFWMCPCompareSwapRequest) async -> ASFWMCPTransactionResult func executeFCPCommand(_ request: ASFWMCPFcpCommandRequest) async -> ASFWMCPFcpCommandReceipt func executePhase88Streaming(targetGuid: UInt64, start: Bool) async -> ASFWMCPPhase88StreamingReceipt + func executeContinuousTxSilence(targetGuid: UInt64, start: Bool) async -> ASFWMCPContinuousTxReceipt + func executeContinuousTxHealth(targetGuid: UInt64) async -> ASFWMCPContinuousTxHealthReceipt func executeBusReset(_ request: ASFWMCPBusResetRequest) async -> ASFWMCPBusResetReceipt func executeIRMSnapshot(_ request: ASFWMCPIrmSnapshotRequest) async -> ASFWMCPIrmResourceSnapshot } @@ -31,6 +33,7 @@ actor MockASFWDriverControl: ASFWDriverControlling { private var duetInputFdf: UInt8 = 0x02 private var duetOutputFdf: UInt8 = 0x02 private var phase88Streaming = false + private var continuousTxSilenceRunning = false init( generation: UInt32 = 17, @@ -375,6 +378,25 @@ actor MockASFWDriverControl: ASFWDriverControlling { return ASFWMCPPhase88StreamingReceipt(targetGuid: targetGuid, started: start, status: 0) } + func executeContinuousTxSilence(targetGuid: UInt64, start: Bool) async -> ASFWMCPContinuousTxReceipt { + attemptedWriteCount += 1 + continuousTxSilenceRunning = start + return ASFWMCPContinuousTxReceipt(targetGuid: targetGuid, started: start, status: 0) + } + + func executeContinuousTxHealth(targetGuid: UInt64) async -> ASFWMCPContinuousTxHealthReceipt { + guard continuousTxSilenceRunning else { + return ASFWMCPContinuousTxHealthReceipt(targetGuid: targetGuid, health: nil, status: -536_870_213) + } + return ASFWMCPContinuousTxHealthReceipt( + targetGuid: targetGuid, + health: ASFWDriverConnector.ContinuousIsochTxHealth( + running: true, dead: false, eventError: false, + anchorExecutions: 42, lastAnchorTimestamp: 1234), + status: 0 + ) + } + func executeBusReset(_ request: ASFWMCPBusResetRequest) async -> ASFWMCPBusResetReceipt { let correlationId = "mock-bus-reset" guard request.generation == generation else { diff --git a/ASFW/MCP/ASFWMCPLiveDriverControl.swift b/ASFW/MCP/ASFWMCPLiveDriverControl.swift index 31bb3a39..ee2b4544 100644 --- a/ASFW/MCP/ASFWMCPLiveDriverControl.swift +++ b/ASFW/MCP/ASFWMCPLiveDriverControl.swift @@ -22,6 +22,8 @@ protocol ASFWLiveDriverBackend: AnyObject { func mcpSendRawFCPCommand(guid: UInt64, frame: Data, timeoutMs: UInt32) -> Data? func mcpSetAudioStreaming(guid: UInt64, enabled: Bool) -> Int32 func mcpRequestUserBusReset(expectedGeneration: UInt32, shortReset: Bool) -> UInt32? + func mcpSetContinuousIsochTxSilence(guid: UInt64, enabled: Bool) -> Int32 + func mcpGetContinuousIsochTxHealth(guid: UInt64) -> ASFWDriverConnector.ContinuousIsochTxHealth? } extension ASFWDriverConnector: ASFWLiveDriverBackend { @@ -113,6 +115,16 @@ extension ASFWDriverConnector: ASFWLiveDriverBackend { func mcpRequestUserBusReset(expectedGeneration: UInt32, shortReset: Bool) -> UInt32? { requestUserBusReset(expectedGeneration: expectedGeneration, shortReset: shortReset) } + + func mcpSetContinuousIsochTxSilence(guid: UInt64, enabled: Bool) -> Int32 { + let ok = enabled ? startContinuousIsochTxSilence(guid: guid) + : stopContinuousIsochTxSilence(guid: guid) + return ok ? 0 : -1 + } + + func mcpGetContinuousIsochTxHealth(guid: UInt64) -> ContinuousIsochTxHealth? { + getContinuousIsochTxHealth(guid: guid) + } } @MainActor @@ -386,6 +398,25 @@ final class LiveASFWDriverControl: ASFWDriverControlling { ) } + func executeContinuousTxSilence(targetGuid: UInt64, start: Bool) async -> ASFWMCPContinuousTxReceipt { + guard backend.mcpIsConnected else { + return ASFWMCPContinuousTxReceipt(targetGuid: targetGuid, started: start, status: -536_870_201) + } + return ASFWMCPContinuousTxReceipt( + targetGuid: targetGuid, + started: start, + status: backend.mcpSetContinuousIsochTxSilence(guid: targetGuid, enabled: start) + ) + } + + func executeContinuousTxHealth(targetGuid: UInt64) async -> ASFWMCPContinuousTxHealthReceipt { + guard backend.mcpIsConnected, + let health = backend.mcpGetContinuousIsochTxHealth(guid: targetGuid) else { + return ASFWMCPContinuousTxHealthReceipt(targetGuid: targetGuid, health: nil, status: -536_870_201) + } + return ASFWMCPContinuousTxHealthReceipt(targetGuid: targetGuid, health: health, status: 0) + } + func executePhase88Streaming(targetGuid: UInt64, start: Bool) async -> ASFWMCPPhase88StreamingReceipt { guard backend.mcpIsConnected else { return ASFWMCPPhase88StreamingReceipt(targetGuid: targetGuid, started: start, status: -536_870_201) diff --git a/ASFW/MCP/ASFWMCPToolCatalog.swift b/ASFW/MCP/ASFWMCPToolCatalog.swift index 91eec616..b1d91e3f 100644 --- a/ASFW/MCP/ASFWMCPToolCatalog.swift +++ b/ASFW/MCP/ASFWMCPToolCatalog.swift @@ -21,6 +21,7 @@ enum ASFWMCPToolCatalog { + sbp2Tools + diceTcatTools + bebobTools + + continuousTxTools } static let coreTools: [ASFWMCPToolDefinition] = [ diff --git a/ASFW/MCP/ASFWMCPToolDispatch.swift b/ASFW/MCP/ASFWMCPToolDispatch.swift index b50de4c4..e90061ae 100644 --- a/ASFW/MCP/ASFWMCPToolDispatch.swift +++ b/ASFW/MCP/ASFWMCPToolDispatch.swift @@ -115,6 +115,11 @@ extension ASFWMCPCore { case "asfw_phase88_start_48k", "asfw_phase88_stop": return await phase88StreamingResult(toolName: name, decoder: decoder, start: name == "asfw_phase88_start_48k") + case "asfw_continuous_tx_start_dev", "asfw_continuous_tx_stop_dev": + return await continuousTxStreamingResult(toolName: name, decoder: decoder, + start: name == "asfw_continuous_tx_start_dev") + case "asfw_continuous_tx_health_dev": + return await continuousTxHealthResult(toolName: name, decoder: decoder) default: return notImplementedToolResult(name, reason: "Catalog tool \(name) has no dispatch arm.") } @@ -780,6 +785,63 @@ private extension ASFWMCPCore { } } + func continuousTxStreamingResult( + toolName: String, + decoder: ASFWMCPToolArgumentDecoder, + start: Bool + ) async -> ASFWMCPToolCallResult { + do { + let targetGuid = try decoder.uint64("targetGuid") + let currentGen = await currentGeneration() + // No bus-generation pin applies to this GUID-keyed dev control (it + // drives a held OHCI/IRM session, not a node address) -- pin + // requested == current so the policy engine's staleness check is a + // no-op and only the developer-mode/address-space gate applies. + let policy = evaluateWritePolicy(ASFWMCPPolicyRequest( + operationType: .write, + addressSpace: .ohciController, + requestedGeneration: currentGen, + currentGeneration: currentGen, + dryRun: try decoder.bool("dryRun", default: false) + )) + guard policy.reachesDriverWritePath else { + return .failure(toolName: toolName, + code: policy.isDryRun ? .dryRunOnly : (policy.errorCode ?? .policyDenied), + reason: policy.reason, + data: .object(["policy": policy.mcpValue])) + } + let receipt = await driver.executeContinuousTxSilence(targetGuid: targetGuid, start: start) + return receipt.ok + ? .success(toolName: toolName, data: .object([ + "kind": .string("continuousTxSilenceCadence"), + "receipt": receipt.mcpValue, + "policy": policy.mcpValue, + ])) + : .failure(toolName: toolName, code: .rcodeError, + reason: "The driver rejected the continuous TX silence \(start ? "start" : "stop") request.", + data: .object(["receipt": receipt.mcpValue, "policy": policy.mcpValue])) + } catch { + return malformedToolResult(toolName, reason: error.localizedDescription) + } + } + + func continuousTxHealthResult( + toolName: String, + decoder: ASFWMCPToolArgumentDecoder + ) async -> ASFWMCPToolCallResult { + do { + let targetGuid = try decoder.uint64("targetGuid") + let receipt = await driver.executeContinuousTxHealth(targetGuid: targetGuid) + return receipt.ok + ? .success(toolName: toolName, data: receipt.mcpValue) + : .failure(toolName: toolName, code: .capabilityUnavailable, + reason: "No continuous TX silence cadence is active for this GUID.", + data: receipt.mcpValue) + } catch { + return malformedToolResult(toolName, reason: error.localizedDescription) + } + } + func cmpReadPcrResult( toolName: String, decoder: ASFWMCPToolArgumentDecoder diff --git a/ASFWDriver/ASFWDriver.cpp b/ASFWDriver/ASFWDriver.cpp index f77bb43e..a361db68 100644 --- a/ASFWDriver/ASFWDriver.cpp +++ b/ASFWDriver/ASFWDriver.cpp @@ -1022,6 +1022,167 @@ kern_return_t ASFWDriver::StopAudioStreaming(uint64_t guid) { return ivars->context->audioCoordinator->StopStreaming(guid); } +// PENDING HARDWARE VERIFICATION (as of this commit): the buffer-allocation +// fix below is believed correct by inspection but has not been exercised on +// real hardware end to end. Every live attempt so far failed earlier, at +// GetPlaybackPreflightRoute() returning false ("no verified playback +// route"), before ever reaching this function's body. Root cause: something +// on the system (observed pattern is consistent with coreaudiod) calls the +// real Core Audio StartIO -> Stage 5H path within about a second of every +// bus reset, consuming the single-use route RMEFireface800Protocol arms +// once per Stage 5F completion, before this dev-only MCP-triggered path +// (three sequential HTTP round trips: initialize, notifications/initialized, +// tools/call) can win the race. This is a pre-existing route-contention +// characteristic (Stage 5H has the identical symptom when something beats +// it to the route), not a Stage 6 regression. Next attempt should either +// (a) find and quiesce whatever is auto-probing the device, or (b) give this +// dev path its own independent IRM reservation instead of racing Stage 5H's. +kern_return_t ASFWDriver::StartContinuousIsochTxSilence(uint64_t guid) { + if (!ivars || !ivars->context) { + return kIOReturnNotReady; + } + auto& ctx = *ivars->context; + if (!ctx.deps.deviceRegistry || !ctx.deps.audioRuntimeRegistry || + !ctx.deps.hardware) { + return kIOReturnNotReady; + } + auto* record = ctx.deps.deviceRegistry->FindByGuid(guid); + auto protocol = ctx.deps.audioRuntimeRegistry->FindShared(guid); + if (!record || !protocol) { + return kIOReturnNotReady; + } + + ASFW::Audio::PlaybackPreflightRoute route{}; + if (!protocol->GetPlaybackPreflightRoute(route)) { + ASFW_LOG_WARNING(Audio, + "[RME] Continuous TX silence start refused: no verified playback route GUID=0x%016llx", + guid); + return kIOReturnNotReady; + } + + ASFW_LOG(Audio, "[RME] Continuous TX silence start (dev) GUID=0x%016llx", guid); + return protocol->StartContinuousPlaybackIntegration( + route, + [&ctx, route]() -> IOReturn { + // Unlike Stage 5H (triggered from inside a real Core Audio + // StartIO, which already allocated the shared TX slab before + // ever reaching the RME branch), this dev-only path is driven + // straight from UserClient with no CoreAudio session -- the + // shared payload/metadata/control memory PrepareTransmit expects + // was never allocated. Allocate it here with the same fixed + // no-CIP RME wire geometry validated since Stage 5A: 1536 + // bytes/packet (32 frames x 12 dbs x 4 bytes, no CIP header). + IOMemoryDescriptor* rawPayload = nullptr; + IOMemoryDescriptor* rawMetadata = nullptr; + IOMemoryDescriptor* rawControl = nullptr; + const uint32_t numSlots = + ASFW::IsochTransport::AudioTimingGeometry::kTxSharedSlotPackets; + const uint32_t interruptInterval = + ASFW::IsochTransport::AudioTimingGeometry::kTimingGroupPackets; + IOReturn kr = ctx.isoch.AllocateTxIsochResources( + 0U, numSlots, 1536U, interruptInterval, + &rawPayload, &rawMetadata, &rawControl); + if (rawPayload) { + rawPayload->release(); + } + if (rawMetadata) { + rawMetadata->release(); + } + if (rawControl) { + rawControl->release(); + } + if (kr == kIOReturnSuccess) { + kr = ctx.isoch.PrepareTransmit( + route.channel, *ctx.deps.hardware, 0U); + } + if (kr == kIOReturnSuccess) { + kr = ctx.isoch.PrimePreparedTransmitForPreflight(); + } + if (kr == kIOReturnSuccess) { + kr = ctx.isoch + .PrepareTransmitBoundedCircularSilenceCadenceForPreflight(); + } + return kr; + }, + [&ctx]() -> IOReturn { + return ctx.isoch.StartTransmitContinuousCircularSilenceCadence(); + }); +} + +kern_return_t ASFWDriver::StopContinuousIsochTxSilence(uint64_t guid) { + if (!ivars || !ivars->context) { + return kIOReturnNotReady; + } + auto& ctx = *ivars->context; + if (!ctx.deps.audioRuntimeRegistry) { + return kIOReturnNotReady; + } + auto protocol = ctx.deps.audioRuntimeRegistry->FindShared(guid); + if (!protocol) { + return kIOReturnNotReady; + } + + ASFW_LOG(Audio, "[RME] Continuous TX silence stop (dev) GUID=0x%016llx", guid); + const IOReturn kr = protocol->StopContinuousPlaybackIntegration( + [&ctx]() -> IOReturn { + return ctx.isoch.StopTransmitContinuousCircularSilenceCadence(); + }); + // Symmetric with the dev-only allocation in StartContinuousIsochTxSilence + // (this path never had a real CoreAudio StartIO/StopIO to own the shared + // slab's lifetime). Safe to free unconditionally: FreeTxIsochResources() + // clears all stream slots and no other consumer holds this dev slab. + ctx.isoch.FreeTxIsochResources(); + return kr; +} + +kern_return_t ASFWDriver::GetContinuousIsochTxHealth( + uint64_t guid, + bool* running, + bool* dead, + bool* eventError, + uint32_t* anchorExecutions, + uint16_t* lastAnchorTimestamp) { + if (!running || !dead || !eventError || !anchorExecutions || + !lastAnchorTimestamp) { + return kIOReturnBadArgument; + } + *running = false; + *dead = false; + *eventError = false; + *anchorExecutions = 0U; + *lastAnchorTimestamp = 0U; + + if (!ivars || !ivars->context) { + return kIOReturnNotReady; + } + auto& ctx = *ivars->context; + if (!ctx.deps.audioRuntimeRegistry) { + return kIOReturnNotReady; + } + auto protocol = ctx.deps.audioRuntimeRegistry->FindShared(guid); + if (!protocol) { + return kIOReturnNotReady; + } + + return protocol->GetContinuousPlaybackIntegrationHealth( + [&ctx, running, dead, eventError, anchorExecutions, + lastAnchorTimestamp](ASFW::Audio::ContinuousPlaybackHealth& health) -> IOReturn { + const auto txHealth = + ctx.isoch.PollTransmitContinuousCircularSilenceCadenceHealth(); + health.running = txHealth.running; + health.dead = txHealth.dead; + health.eventError = txHealth.eventError; + health.anchorExecutions = txHealth.anchorExecutions; + health.lastAnchorTimestamp = txHealth.lastAnchorTimestamp; + *running = health.running; + *dead = health.dead; + *eventError = health.eventError; + *anchorExecutions = health.anchorExecutions; + *lastAnchorTimestamp = health.lastAnchorTimestamp; + return kIOReturnSuccess; + }); +} + kern_return_t ASFWDriver::StartIsochReceive(uint8_t channel, uint32_t wireFormatRaw, uint32_t am824Slots) { if (!ivars || !ivars->context) { return kIOReturnNotReady; diff --git a/ASFWDriver/ASFWDriver.iig b/ASFWDriver/ASFWDriver.iig index 65f7fcf1..d0a98602 100644 --- a/ASFWDriver/ASFWDriver.iig +++ b/ASFWDriver/ASFWDriver.iig @@ -111,6 +111,19 @@ public: kern_return_t StartAudioStreaming(uint64_t guid) LOCALONLY; kern_return_t StopAudioStreaming(uint64_t guid) LOCALONLY; + // Stage 6 (dev-only): continuous silent isoch TX cadence for a device + // protocol that implements IDeviceProtocol's continuous-playback hooks + // (currently only RME Fireface 800). Never reachable from Core Audio + // StartIO -- reached only via this UserClient/MCP path. + kern_return_t StartContinuousIsochTxSilence(uint64_t guid) LOCALONLY; + kern_return_t StopContinuousIsochTxSilence(uint64_t guid) LOCALONLY; + kern_return_t GetContinuousIsochTxHealth(uint64_t guid, + bool* running, + bool* dead, + bool* eventError, + uint32_t* anchorExecutions, + uint16_t* lastAnchorTimestamp) LOCALONLY; + // Isochronous Receive Control kern_return_t StartIsochReceive(uint8_t channel, uint32_t wireFormatRaw, uint32_t am824Slots) LOCALONLY; kern_return_t StopIsochReceive() LOCALONLY; diff --git a/ASFWDriver/Audio/Core/AudioCoordinator.cpp b/ASFWDriver/Audio/Core/AudioCoordinator.cpp index 5e9a8b72..460d4438 100644 --- a/ASFWDriver/Audio/Core/AudioCoordinator.cpp +++ b/ASFWDriver/Audio/Core/AudioCoordinator.cpp @@ -46,6 +46,15 @@ void AudioCoordinator::SetCMPClient(ASFW::CMP::CMPClient* client) noexcept { void AudioCoordinator::OnDeviceAdded(std::shared_ptr device) { if (!device) return; const uint64_t guid = device->GetGUID(); + + const auto* record = registry_.FindByGuid(guid); + if (record && + DeviceProtocolFactory::LookupIntegrationMode(record->vendorId, record->modelId) == + DeviceIntegrationMode::kReadOnlyNub) { + EnsureRMEReadOnlyNub(guid); + return; + } + if (BackendForGuid(guid) == &dice_) { dice_.OnDeviceRecordUpdated(guid); } @@ -55,7 +64,12 @@ void AudioCoordinator::OnDeviceResumed(std::shared_ptr devi if (!device) return; const uint64_t guid = device->GetGUID(); auto* backend = BackendForGuid(guid); - if (backend == &dice_) { + const auto* record = registry_.FindByGuid(guid); + if (record && + DeviceProtocolFactory::LookupIntegrationMode(record->vendorId, record->modelId) == + DeviceIntegrationMode::kReadOnlyNub) { + EnsureRMEReadOnlyNub(guid); + } else if (backend == &dice_) { dice_.OnDeviceRecordUpdated(guid); } @@ -105,8 +119,15 @@ void AudioCoordinator::OnDeviceSuspended(std::shared_ptr de void AudioCoordinator::OnDeviceRemoved(Discovery::Guid64 guid) { if (guid == 0) return; + const auto* record = registry_.FindByGuid(guid); + const auto integration = record + ? DeviceProtocolFactory::LookupIntegrationMode(record->vendorId, record->modelId) + : DeviceIntegrationMode::kNone; + auto* backend = BackendForGuid(guid); - if (backend == &dice_) { + if (integration == DeviceIntegrationMode::kReadOnlyNub) { + publisher_.TerminateNub(guid, "RME-Stage3-Removed"); + } else if (backend == &dice_) { dice_.OnDeviceRemoved(guid); } else if (backend == &avc_) { avc_.OnDeviceRemoved(guid); @@ -168,6 +189,44 @@ void AudioCoordinator::HandleCycleInconsistent() noexcept { dice_.HandleRecoveryEvent(guid, DICE::DiceRestartReason::kRecoverAfterCycleInconsistent); } + +void AudioCoordinator::EnsureRMEReadOnlyNub(uint64_t guid) noexcept { + const auto* record = registry_.FindByGuid(guid); + if (!record || + record->vendorId != DeviceProfiles::Audio::kRMEVendorId || + record->modelId != DeviceProfiles::Audio::kFireface800ModelId) { + return; + } + + Model::ASFWAudioDevice dev{}; + dev.guid = record->guid; + dev.vendorId = record->vendorId; + dev.modelId = record->modelId; + dev.deviceName = "RME Fireface 800 (Stage 5C)"; + dev.inputChannelCount = 12; + dev.outputChannelCount = 12; + dev.channelCount = 12; + dev.inputPlugName = "Fireface 800 Inputs"; + dev.outputPlugName = "Fireface 800 Outputs"; + dev.sampleRates = {192000u}; + dev.currentSampleRate = 192000u; + dev.streamMode = Model::StreamMode::kBlocking; + + if (auto endpoint = runtime_.EnsureEndpointRuntime(guid)) { + endpoint->UpdateConfig(dev); + } + + if (publisher_.EnsureNub(guid, dev, "RME-Stage5C")) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5C Core Audio endpoint published: 192000 Hz, 12 inputs, 12 outputs; streaming disabled GUID=0x%016llx", + guid); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5C failed to publish Core Audio endpoint GUID=0x%016llx", + guid); + } +} + IAudioBackend* AudioCoordinator::BackendForGuid(uint64_t guid) noexcept { if (guid == 0) return nullptr; @@ -180,6 +239,9 @@ IAudioBackend* AudioCoordinator::BackendForGuid(uint64_t guid) noexcept { if (integration == DeviceIntegrationMode::kHardcodedNub) { return &dice_; } + if (integration == DeviceIntegrationMode::kReadOnlyNub) { + return nullptr; + } return &avc_; } @@ -187,6 +249,16 @@ IAudioBackend* AudioCoordinator::BackendForGuid(uint64_t guid) noexcept { IOReturn AudioCoordinator::StartStreaming(uint64_t guid) noexcept { if (guid == 0) return kIOReturnBadArgument; + const auto* record = registry_.FindByGuid(guid); + if (record && + DeviceProtocolFactory::LookupIntegrationMode(record->vendorId, record->modelId) == + DeviceIntegrationMode::kReadOnlyNub) { + ASFW_LOG_WARNING(Audio, + "[RME] Stage 5C endpoint is preflight-only; no-CIP wire format is verified but OHCI streaming remains blocked GUID=0x%016llx", + guid); + return kIOReturnUnsupported; + } + bool setActive = false; if (lock_) { IOLockLock(lock_); diff --git a/ASFWDriver/Audio/Core/AudioCoordinator.hpp b/ASFWDriver/Audio/Core/AudioCoordinator.hpp index 647e4cd6..54a5e750 100644 --- a/ASFWDriver/Audio/Core/AudioCoordinator.hpp +++ b/ASFWDriver/Audio/Core/AudioCoordinator.hpp @@ -69,6 +69,7 @@ class AudioCoordinator final : public Discovery::IDeviceObserver, private: [[nodiscard]] IAudioBackend* BackendForGuid(uint64_t guid) noexcept; + void EnsureRMEReadOnlyNub(uint64_t guid) noexcept; AudioNubPublisher publisher_; DiceAudioBackend dice_; diff --git a/ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp b/ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp index ddbb7317..40ddacb3 100644 --- a/ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp +++ b/ASFWDriver/Audio/DriverKit/ASFWAudioDevice.cpp @@ -13,6 +13,7 @@ #include "Config/AudioProfileRegistry.hpp" #include "../../Common/DriverKitOwnership.hpp" #include "../../Shared/Isoch/IsochAudioTransport.hpp" +#include "../../DeviceProfiles/Audio/AudioDeviceIds.hpp" #include #include @@ -64,6 +65,21 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) { static_cast(in_flags)); auto& ivars = *this->ivars->driverIvars; + + // Stage 5C deliberately lets the FF800 enter the generic TX allocation, + // mapping, packetizer and prefill path. The Dext-side StartAudioStreaming + // branch then primes descriptors only and returns unsupported before any + // OHCI CommandPtr/RUN write or device communication start. + const bool isRmeStage5C = + ivars.device.vendorId == ASFW::DeviceProfiles::Audio::kRMEVendorId && + ivars.device.modelId == ASFW::DeviceProfiles::Audio::kFireface800ModelId; + if (isRmeStage5C) { + ASFW_LOG_WARNING( + Audio, + "[RME] Stage 5C: Core Audio StartIO entering host TX DMA/descriptor preflight; live streaming remains blocked GUID=0x%016llx", + ivars.device.guid); + } + __block kern_return_t kr = kIOReturnSuccess; ivars.workQueue->DispatchSync(^{ @@ -177,7 +193,8 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) { const uint32_t numSlots = ASFW::IsochTransport::AudioTimingGeometry::kTxSharedSlotPackets; const uint32_t maxPacketBytes = - 8u + static_cast(txConfig.framesPerDataPacket) * txConfig.dbs * 4u; + (txConfig.includeCipHeader ? 8u : 0u) + + static_cast(txConfig.framesPerDataPacket) * txConfig.dbs * 4u; const uint32_t interruptInterval = ASFW::IsochTransport::AudioTimingGeometry::kTimingGroupPackets; @@ -231,6 +248,10 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) { ivars.runtime.txSlotProvider.controlBlock = controlBlock; ivars.runtime.txSlotProvider.numSlots = numSlots; ivars.runtime.txSlotProvider.slotStrideBytes = maxPacketBytes; + ivars.runtime.txSlotProvider.isochTag = + txConfig.includeCipHeader + ? ASFW::IsochTransport::IsochPacketTag::kCip + : ASFW::IsochTransport::IsochPacketTag::kNoCipHeader; ivars.runtime.txExecutionTimeline.controlBlock = controlBlock; @@ -282,7 +303,8 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) { const uint32_t numSlots2 = ASFW::IsochTransport::AudioTimingGeometry::kTxSharedSlotPackets; const uint32_t maxPacketBytes2 = - 8u + static_cast(txConfig2.framesPerDataPacket) * txConfig2.dbs * 4u; + (txConfig2.includeCipHeader ? 8u : 0u) + + static_cast(txConfig2.framesPerDataPacket) * txConfig2.dbs * 4u; const uint32_t interruptInterval2 = ASFW::IsochTransport::AudioTimingGeometry::kTimingGroupPackets; @@ -318,6 +340,10 @@ kern_return_t ASFWAudioDevice::StartIO(IOUserAudioStartStopFlags in_flags) { ivars.runtime.txSlotProviderSecondary.controlBlock = controlBlock2; ivars.runtime.txSlotProviderSecondary.numSlots = numSlots2; ivars.runtime.txSlotProviderSecondary.slotStrideBytes = maxPacketBytes2; + ivars.runtime.txSlotProviderSecondary.isochTag = + txConfig2.includeCipHeader + ? ASFW::IsochTransport::IsochPacketTag::kCip + : ASFW::IsochTransport::IsochPacketTag::kNoCipHeader; if (!ivars.runtime.txStreamEngineSecondary.Configure(*profile, txConfig2)) { kr = failStart(kIOReturnError, "ConfigureTxStreamEngine2"); @@ -545,6 +571,18 @@ kern_return_t ASFWAudioDevice::StopIO(IOUserAudioStartStopFlags in_flags) { static_cast(in_flags)); auto& ivars = *this->ivars->driverIvars; + + // Matching Stage 4A no-op teardown for a StartIO request that was rejected + // before the generic transport or AudioDriverKit state was entered. + if (ivars.device.vendorId == ASFW::DeviceProfiles::Audio::kRMEVendorId && + ivars.device.modelId == ASFW::DeviceProfiles::Audio::kFireface800ModelId) { + ASFW_LOG( + Audio, + "[RME] Stage 4A: StopIO acknowledged with no active stream GUID=0x%016llx", + ivars.device.guid); + return kIOReturnSuccess; + } + __block kern_return_t kr = kIOReturnSuccess; ivars.workQueue->DispatchSync(^{ diff --git a/ASFWDriver/Audio/DriverKit/ASFWAudioDriverPrivate.hpp b/ASFWDriver/Audio/DriverKit/ASFWAudioDriverPrivate.hpp index 30235828..157c2c4f 100644 --- a/ASFWDriver/Audio/DriverKit/ASFWAudioDriverPrivate.hpp +++ b/ASFWDriver/Audio/DriverKit/ASFWAudioDriverPrivate.hpp @@ -105,6 +105,8 @@ class DextTxSlotProvider final : public ASFW::Protocols::Audio::AMDTP::IAmdtpTxS ASFW::IsochTransport::TxStreamControl* controlBlock{nullptr}; uint32_t numSlots{0}; uint32_t slotStrideBytes{0}; + ASFW::IsochTransport::IsochPacketTag isochTag{ + ASFW::IsochTransport::IsochPacketTag::kCip}; bool AcquireWritableSlot( uint32_t packetIndex, @@ -132,27 +134,19 @@ class DextTxSlotProvider final : public ASFW::Protocols::Audio::AMDTP::IAmdtpTxS meta.packetIndex = packet.packetIndex; meta.payloadLength = packet.byteCount; - // immediateData[0] = isoch packet header: spd=2 (S400) at [18:16], - // tag=1 (standard CIP) at [15:14], tcode=0xA (isoch data block - // transmit) at [7:4], sy=0. The channel at [13:8] is deliberately - // left as a placeholder: the owning transport ring always stamps its - // configured channel immediately before publishing the descriptor. - // The speed field is mandatory — omitting it transmits at S100 and - // produces a header the device/analyzer treats as malformed. - // Cross-validated with Linux: firewire/ohci.h:277-286 and - // firewire/ohci.c:3377-3381. - const uint32_t isochHeaderQ0 = (static_cast(2 & 0x7) << 16) | - (static_cast(1 & 0x3) << 14) | - (static_cast(0xA & 0xF) << 4); + // immediateData[0] = OHCI isoch packet header. Tag 1 identifies a + // standard CIP packet; tag 0 identifies raw payload with no CIP header + // (the Fireface 800 former-generation wire format). The owning ring + // stamps the configured channel immediately before DMA publication. + const uint32_t isochHeaderQ0 = + ASFW::IsochTransport::BuildIsochTxHeaderQ0(isochTag); meta.immediateHeader[0] = OSSwapHostToLittleInt32(isochHeaderQ0); - // immediateData[1] = data_length (payload bytes) in bits [31:16]. The - // CIP header is the first 8 bytes of the payload buffer and is shipped - // by the OUTPUT_LAST descriptor — it does NOT belong in the packet - // header immediate. Cross-validated with Linux: - // firewire/ohci.h:287-288 and firewire/ohci.c:3383. + // immediateData[1] = data_length (all bytes transmitted after the OHCI + // immediate packet header) in bits [31:16]. This is 0 for a true empty + // no-CIP idle cycle, raw PCM bytes for tag 0, or CIP+payload for tag 1. meta.immediateHeader[1] = OSSwapHostToLittleInt32( - static_cast(packet.byteCount & 0xFFFF) << 16); + ASFW::IsochTransport::BuildIsochTxHeaderQ1(packet.byteCount)); // Compute expectedGen and release-store commitGen const uint64_t gen = ASFW::IsochTransport::ExpectedCommitGen(packet.packetIndex, numSlots); diff --git a/ASFWDriver/Audio/DriverKit/ASFWAudioDriverZts.cpp b/ASFWDriver/Audio/DriverKit/ASFWAudioDriverZts.cpp index 6a9ad5a5..3eabcc4a 100644 --- a/ASFWDriver/Audio/DriverKit/ASFWAudioDriverZts.cpp +++ b/ASFWDriver/Audio/DriverKit/ASFWAudioDriverZts.cpp @@ -12,6 +12,7 @@ #include "ASFWAudioDriverPrivate.hpp" #include "../../Common/TimingUtils.hpp" #include "../../Logging/Logging.hpp" +#include "../../DeviceProfiles/Audio/AudioDeviceIds.hpp" #include @@ -502,11 +503,20 @@ void PrefillTxRingBeforeStart(ASFWAudioDriver_IVars& ivars) noexcept { // the producer gets its first turn. Steady state still targets completion // + kTxPreparationLeadPackets. ASFW::Protocols::Audio::AMDTP::AmdtpTimingState timing{}; - timing.replayValid = true; timing.txClockValid = false; - timing.disposition = - ASFW::Protocols::Audio::AMDTP:: - AmdtpPacketDisposition::NoData; + const bool isRmeFireface800 = + ivars.device.vendorId == ASFW::DeviceProfiles::Audio::kRMEVendorId && + ivars.device.modelId == ASFW::DeviceProfiles::Audio::kFireface800ModelId; + + // Normal devices intentionally prefill NO-DATA through the replay override. + // The FF800 is different: it needs its native 192 kHz blocking cadence + // (6 data packets / 2 true empty cycles per 8 bus cycles). Leaving + // replayValid=true with replayDataBlocks=0 forces every packet empty and + // bypasses the cadence engine entirely. + timing.replayValid = !isRmeFireface800; + timing.disposition = isRmeFireface800 + ? ASFW::Protocols::Audio::AMDTP::AmdtpPacketDisposition::Data + : ASFW::Protocols::Audio::AMDTP::AmdtpPacketDisposition::NoData; uint32_t prepared = 0; for (uint64_t packetIndex = 0; @@ -527,9 +537,10 @@ void PrefillTxRingBeforeStart(ASFWAudioDriver_IVars& ivars) noexcept { } ASFW_LOG(DirectAudio, - "ADK DBG TX prefill seeded %u/%u committed NO-DATA packets before isoch start (steadyLead=%u)", + "ADK DBG TX prefill seeded %u/%u committed %{public}s packets before isoch start (steadyLead=%u)", prepared, numSlots, + isRmeFireface800 ? "FF800 blocking cadence" : "NO-DATA", ASFW::IsochTransport::AudioTimingGeometry:: kTxPreparationLeadPackets); } diff --git a/ASFWDriver/Audio/DriverKit/ASFWAudioNub.cpp b/ASFWDriver/Audio/DriverKit/ASFWAudioNub.cpp index 3c681590..87ff4525 100644 --- a/ASFWDriver/Audio/DriverKit/ASFWAudioNub.cpp +++ b/ASFWDriver/Audio/DriverKit/ASFWAudioNub.cpp @@ -496,7 +496,106 @@ kern_return_t IMPL(ASFWAudioNub, StartAudioStreaming) return kIOReturnNotReady; } + // Resolve the runtime binding before the global auto-start gate. Stage 5H + // is a bounded integration test: prepare one immutable circular 48-cycle + // D-D-D-S silence ring with RUN clear, start the already-verified FF800 + // communication engine, sustain the ring for exactly 100 ms, stop OHCI + // first, then stop the device and release the held IRM route. Core Audio + // streaming remains rejected after the bounded cleanup completes. + ProtocolRuntimeBinding binding{}; + const kern_return_t bindingStatus = ResolveProtocolRuntimeBinding(ivars, binding); + if (bindingStatus == kIOReturnSuccess && binding.device != nullptr) { + const auto integration = ASFW::Audio::DeviceProtocolFactory::LookupIntegrationMode( + binding.device->vendorId, binding.device->modelId); + if (integration == ASFW::Audio::DeviceIntegrationMode::kReadOnlyNub && + binding.device->vendorId == ASFW::DeviceProfiles::Audio::kRMEVendorId && + binding.device->modelId == ASFW::DeviceProfiles::Audio::kFireface800ModelId) { + ASFWDriver* parent = GetParentASFWDriver(ivars); + auto* ctx = parent + ? static_cast(parent->GetServiceContext()) + : nullptr; + ASFW::Audio::PlaybackPreflightRoute route{}; + if (!ctx || !ctx->deps.hardware || !binding.protocol || + !binding.protocol->GetPlaybackPreflightRoute(route)) { + ASFW_LOG_WARNING( + Audio, + "[RME] Stage 5H bounded circular silent cadence run deferred: verified IRM route is not ready GUID=0x%016llx", + ivars->guid); + return kIOReturnUnsupported; + } + if (route.channel > 63U || route.bandwidthUnits != 1286U || + route.sampleRateHz != 192000U || + !route.deviceCommunicationStopped) { + ASFW_LOG_ERROR( + Audio, + "[RME] Stage 5H bounded circular silent cadence run rejected invalid route channel=%u bandwidth=%u/1286 rate=%u deviceStopped=%u", + route.channel, + route.bandwidthUnits, + route.sampleRateHz, + route.deviceCommunicationStopped ? 1U : 0U); + return kIOReturnUnsupported; + } + + ASFW_LOG_WARNING( + Audio, + "[RME] Stage 5H bounded circular silent cadence run starting GUID=0x%016llx channel=%u bandwidth=%u rate=%u descriptors=48 dataPacketsPerSweep=36 skipPerSweep=12 durationMs=100 deviceEngineStarting=1", + ivars->guid, + route.channel, + route.bandwidthUnits, + route.sampleRateHz); + + const IOReturn integrationKr = + binding.protocol->RunBoundedPlaybackIntegrationPreflight( + route, + [ctx, route]() -> IOReturn { + IOReturn kr = ctx->isoch.PrepareTransmit( + route.channel, *ctx->deps.hardware, 0U); + if (kr == kIOReturnSuccess) { + kr = ctx->isoch.PrimePreparedTransmitForPreflight(); + } + if (kr == kIOReturnSuccess) { + kr = ctx->isoch + .PrepareTransmitBoundedCircularSilenceCadenceForPreflight(); + } + return kr; + }, + [ctx]() -> IOReturn { + // Stage 5H keeps the immutable 6 ms ring active for exactly + // 100 ms, proving repeated wraps without refill. + return ctx->isoch + .RunPreparedTransmitBoundedCircularSilenceCadenceForPreflight(100U); + }, + [ctx]() -> IOReturn { + // Idempotent final host cleanup. The RUN callback already + // clears RUN and waits for ACTIVE; this also handles every + // preparation/start failure path before device stop. + return ctx->isoch + .CleanupPreparedTransmitBoundedCircularSilenceCadenceForPreflight(); + }); + + // The protocol call above is synchronous: OHCI, FF800 and IRM + // cleanup all finish before StartAudioStreaming returns and before + // AudioDriverKit unwinds/frees the shared TX slabs. + if (integrationKr == kIOReturnSuccess) { + ASFW_LOG( + Audio, + "[RME] ✅ Stage 5H bounded circular silent playback integration completed GUID=0x%016llx channel=%u durationMs=100; Core Audio StartIO remains rejected", + ivars->guid, + route.channel); + } else { + ASFW_LOG_ERROR( + Audio, + "[RME] Stage 5H bounded circular silent playback integration failed GUID=0x%016llx channel=%u kr=0x%x; Core Audio StartIO remains rejected", + ivars->guid, + route.channel, + integrationKr); + } + return kIOReturnUnsupported; + } + } + // Auto-start gating (Info.plist + runtime), useful for debugging discovery without streams. + // Stage 5H has already been handled above; it executes one bounded 100 ms circular silence run and never enables live Core Audio streaming. if (!ASFW::LogConfig::Shared().IsAudioAutoStartEnabled()) { ASFW_LOG(Audio, "ASFWAudioNub: StartAudioStreaming skipped (auto-start disabled) GUID=0x%016llx", @@ -509,11 +608,15 @@ kern_return_t IMPL(ASFWAudioNub, StartAudioStreaming) // Linux resets FCP before its OXFW stream restart (oxfw.c:279-287), and // Apple likewise waits for its resumed state before reconnecting. DICE's // hardcoded-nub path does not use FCP and remains independent. - ProtocolRuntimeBinding binding{}; - const kern_return_t bindingStatus = ResolveProtocolRuntimeBinding(ivars, binding); if (bindingStatus == kIOReturnSuccess && binding.device != nullptr) { const auto integration = ASFW::Audio::DeviceProtocolFactory::LookupIntegrationMode( binding.device->vendorId, binding.device->modelId); + if (integration == ASFW::Audio::DeviceIntegrationMode::kReadOnlyNub) { + ASFW_LOG_WARNING(Audio, + "ASFWAudioNub: read-only audio endpoint transport remains disabled GUID=0x%016llx", + ivars->guid); + return kIOReturnUnsupported; + } if (integration != ASFW::Audio::DeviceIntegrationMode::kHardcodedNub) { auto* transport = binding.avcDiscovery ? binding.avcDiscovery->GetFCPTransportForNodeID(binding.device->nodeId) @@ -554,6 +657,27 @@ kern_return_t IMPL(ASFWAudioNub, StopAudioStreaming) return kIOReturnNotReady; } + ProtocolRuntimeBinding binding{}; + if (ResolveProtocolRuntimeBinding(ivars, binding) == kIOReturnSuccess && + binding.device != nullptr && + binding.device->vendorId == ASFW::DeviceProfiles::Audio::kRMEVendorId && + binding.device->modelId == ASFW::DeviceProfiles::Audio::kFireface800ModelId) { + ASFWDriver* parent = GetParentASFWDriver(ivars); + auto* ctx = parent + ? static_cast(parent->GetServiceContext()) + : nullptr; + if (ctx) { + (void)ctx->isoch.StopTransmit(); + } + if (auto endpoint = FindEndpointRuntime(ivars)) { + endpoint->MarkStreaming(false); + } + ASFW_LOG(Audio, + "[RME] Stage 5H stop cleanup complete; bounded circular transmit context is stopped and no live audio stream exists GUID=0x%016llx", + ivars->guid); + return kIOReturnSuccess; + } + auto* coordinator = GetAudioCoordinator(ivars); if (!coordinator) { return kIOReturnNotReady; diff --git a/ASFWDriver/Audio/DriverKit/Config/AudioProfileRegistry.cpp b/ASFWDriver/Audio/DriverKit/Config/AudioProfileRegistry.cpp index 6a6bef2f..780a0ca9 100644 --- a/ASFWDriver/Audio/DriverKit/Config/AudioProfileRegistry.cpp +++ b/ASFWDriver/Audio/DriverKit/Config/AudioProfileRegistry.cpp @@ -5,12 +5,109 @@ // Global profile registry dispatcher. #include "AudioProfileRegistry.hpp" +#include "AudioStreamProfile.hpp" #include "AVC/ApogeeDuetProfile.hpp" #include "AVC/Phase88Profile.hpp" #include "DICE/DiceProfileRegistry.hpp" #include "../../../DeviceProfiles/Audio/AudioDeviceIds.hpp" +namespace { + +class RMEFireface800Stage3Profile final : public ASFW::Isoch::Audio::IAudioStreamProfile { +public: + [[nodiscard]] const char* Name() const noexcept override { + return "RME Fireface 800 (Stage 5C)"; + } + + [[nodiscard]] ASFW::Encoding::AudioWireFormat TxWireFormat() const noexcept override { + return ASFW::Encoding::AudioWireFormat::kRawPcm24In32; + } + + [[nodiscard]] ASFW::Encoding::AudioWireFormat RxWireFormat() const noexcept override { + return ASFW::Encoding::AudioWireFormat::kRawPcm24In32; + } + + [[nodiscard]] uint32_t TxChannelCount() const noexcept override { return 12; } + [[nodiscard]] uint32_t RxChannelCount() const noexcept override { return 12; } + [[nodiscard]] uint32_t TxMidiSlots() const noexcept override { return 0; } + [[nodiscard]] uint32_t RxMidiSlots() const noexcept override { return 0; } + [[nodiscard]] uint32_t TxDbs() const noexcept override { return 12; } + [[nodiscard]] uint32_t RxDbs() const noexcept override { return 12; } + + [[nodiscard]] uint32_t TxSafetyOffsetFrames(double sampleRate) const noexcept override { + (void)sampleRate; + return 64; + } + + [[nodiscard]] uint32_t RxSafetyOffsetFrames(double sampleRate) const noexcept override { + (void)sampleRate; + return 64; + } + + [[nodiscard]] uint32_t TxReportedLatencyFrames(double sampleRate) const noexcept override { + (void)sampleRate; + return 128; + } + + [[nodiscard]] uint32_t RxReportedLatencyFrames(double sampleRate) const noexcept override { + (void)sampleRate; + return 128; + } + + [[nodiscard]] std::vector SupportedSampleRates() const override { + // Stage 5C intentionally publishes only the hardware-verified current mode. + return {192000u}; + } + + [[nodiscard]] bool BuildDefaultTxStreamConfig( + ASFW::Isoch::Audio::AudioStreamConfig& outConfig) const noexcept override { + outConfig = {}; + outConfig.direction = ASFW::Isoch::Audio::AudioStreamDirection::HostToDevice; + outConfig.sampleRate = 192000u; + outConfig.streamMode = ASFW::Encoding::StreamMode::kBlocking; + outConfig.sid = 0u; + outConfig.pcmChannels = 12u; + outConfig.dbs = 12u; + outConfig.midiSlots = 0u; + outConfig.framesPerDataPacket = 32u; + outConfig.fdf = 0u; + outConfig.fmt = 0u; + outConfig.includeCipHeader = false; + return true; + } + + [[nodiscard]] bool BuildDefaultRxStreamConfig( + ASFW::Isoch::Audio::AudioStreamConfig& outConfig) const noexcept override { + outConfig = {}; + outConfig.direction = ASFW::Isoch::Audio::AudioStreamDirection::DeviceToHost; + outConfig.sampleRate = 192000u; + outConfig.streamMode = ASFW::Encoding::StreamMode::kBlocking; + outConfig.sid = 0u; + outConfig.pcmChannels = 12u; + outConfig.dbs = 12u; + outConfig.midiSlots = 0u; + outConfig.framesPerDataPacket = 32u; + outConfig.fdf = 0u; + outConfig.fmt = 0u; + outConfig.includeCipHeader = false; + return true; + } + + [[nodiscard]] ASFW::Isoch::Audio::AudioStreamTxPolicy + TxStreamPolicy() const noexcept override { + ASFW::Isoch::Audio::AudioStreamTxPolicy policy{}; + policy.hostToDevicePcmEncoding = ASFW::Encoding::AudioWireFormat::kRawPcm24In32; + policy.variableDbs = false; + policy.initializeNonAudioSlots = false; + policy.preserveFdfInNoDataPackets = false; + policy.emptyPacketsDuringIdle = true; + return policy; + } +}; + +} // namespace + namespace ASFW::Isoch::Audio { const IAudioDeviceProfile* AudioProfileRegistry::FindProfile(uint32_t vendorId, @@ -28,6 +125,12 @@ const IAudioDeviceProfile* AudioProfileRegistry::FindProfile(uint32_t vendorId, return &phase88Profile; } + static RMEFireface800Stage3Profile fireface800Profile{}; + if (vendorId == DeviceProfiles::Audio::kRMEVendorId && + modelId == DeviceProfiles::Audio::kFireface800ModelId) { + return &fireface800Profile; + } + // Map identity to the DICE family structures DICE::DiceDeviceIdentity identity{ .guid = guid, diff --git a/ASFWDriver/Audio/DriverKit/Config/AudioStreamProfile.hpp b/ASFWDriver/Audio/DriverKit/Config/AudioStreamProfile.hpp index d36458cf..a7cb83d0 100644 --- a/ASFWDriver/Audio/DriverKit/Config/AudioStreamProfile.hpp +++ b/ASFWDriver/Audio/DriverKit/Config/AudioStreamProfile.hpp @@ -27,6 +27,12 @@ struct AudioStreamConfig final { uint8_t framesPerDataPacket{8}; uint8_t fdf{0x02}; uint8_t fmt{0x10}; + + // Most AMDTP streams carry the standard 8-byte CIP header. + // Former-generation RME Fireface streams use raw no-CIP payloads. + // Default true preserves every existing profile. + bool includeCipHeader{true}; + uint8_t sourceChannelOffset{0}; }; diff --git a/ASFWDriver/Audio/Engine/Direct/Tx/DiceTxStreamEngine.cpp b/ASFWDriver/Audio/Engine/Direct/Tx/DiceTxStreamEngine.cpp index 56524acd..afe5c2ac 100644 --- a/ASFWDriver/Audio/Engine/Direct/Tx/DiceTxStreamEngine.cpp +++ b/ASFWDriver/Audio/Engine/Direct/Tx/DiceTxStreamEngine.cpp @@ -16,11 +16,13 @@ AMDTP::AmdtpStreamConfig DiceStreamConfigMapper::ToAmdtpConfig( config.fmt = streamConfig.fmt; config.fdf = streamConfig.fdf; config.framesPerDataPacket = streamConfig.framesPerDataPacket; + config.includeCipHeader = streamConfig.includeCipHeader; config.sourceChannelOffset = streamConfig.sourceChannelOffset; - // Compute the true max packet size: CIP headers (8 bytes) + frames × DBS × 4 bytes/slot. - // Do not use the AmdtpStreamConfig default (512) — it is too small for high-channel - // devices (e.g. a 24-channel device with DBS=24 needs 776 bytes at 8 fpd). - config.maxPacketBytes = 8u + + // Compute the true max packet size. Standard AMDTP uses an 8-byte CIP + // header; former-generation RME Fireface streams use raw no-CIP payloads. + // Do not use the AmdtpStreamConfig default (512) — it is too small for + // high-channel devices. + config.maxPacketBytes = (streamConfig.includeCipHeader ? 8u : 0u) + static_cast(streamConfig.framesPerDataPacket) * streamConfig.dbs * 4u; return config; } diff --git a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp index 44f96a00..025b51ff 100644 --- a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp +++ b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.cpp @@ -8,6 +8,7 @@ #include "DICE/TCAT/DICETcatProtocol.hpp" #include "Oxford/Apogee/ApogeeDuetProtocol.hpp" #include "BeBoB/Phase88Protocol.hpp" +#include "RME/RMEFireface800Protocol.hpp" #include "../../Logging/Logging.hpp" namespace ASFW::Audio { @@ -79,6 +80,15 @@ std::unique_ptr DeviceProtocolFactory::Create( busOps, busInfo, nodeId, nullptr, irmClient, cmpClient, deviceGuid); } + if (vendorId == DeviceProfiles::Audio::kRMEVendorId && + modelId == DeviceProfiles::Audio::kFireface800ModelId) { + ASFW_LOG(Audio, + "Creating Stage 5C RME Fireface 800 no-CIP wire-format preflight vendor=0x%06x model=0x%06x node=0x%04x", + vendorId, modelId, nodeId); + return std::make_unique( + busOps, busInfo, nodeId, deviceGuid, irmClient); + } + if (vendorId == kTerraTecVendorId && modelId == kPhase88RackFwModelId) { ASFW_LOG(Audio, "Creating Phase88Protocol BeBoB/CMP backend vendor=0x%06x model=0x%06x node=0x%04x", diff --git a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp index 167c7918..dfc14343 100644 --- a/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp +++ b/ASFWDriver/Audio/Protocols/DeviceProtocolFactory.hpp @@ -46,6 +46,8 @@ class DeviceProtocolFactory { // Identity constants are defined in DeviceProfiles/Audio/AudioDeviceIds.hpp (single // source of truth) and re-exported here so existing DeviceProtocolFactory::kX call // sites keep resolving. + static constexpr uint32_t kRMEVendorId = DeviceProfiles::Audio::kRMEVendorId; + static constexpr uint32_t kFireface800ModelId = DeviceProfiles::Audio::kFireface800ModelId; static constexpr uint32_t kFocusriteVendorId = DeviceProfiles::Audio::kFocusriteVendorId; static constexpr uint32_t kSPro40ModelId = DeviceProfiles::Audio::kSPro40ModelId; static constexpr uint32_t kLiquidS56ModelId = DeviceProfiles::Audio::kLiquidS56ModelId; @@ -68,6 +70,8 @@ class DeviceProtocolFactory { DeviceProfiles::Audio::kStudioLive1602ModelId; static constexpr uint32_t kFocusriteGuidModelSPro40Tcd3070 = DeviceProfiles::Audio::kFocusriteGuidModelSPro40Tcd3070; + static constexpr const char* kRMEVendorName = DeviceProfiles::Audio::kRMEVendorName; + static constexpr const char* kFireface800ModelName = DeviceProfiles::Audio::kFireface800ModelName; static constexpr const char* kFocusriteVendorName = DeviceProfiles::Audio::kFocusriteVendorName; static constexpr const char* kSPro40ModelName = DeviceProfiles::Audio::kSPro40ModelName; static constexpr const char* kLiquidS56ModelName = DeviceProfiles::Audio::kLiquidS56ModelName; diff --git a/ASFWDriver/Audio/Protocols/IDeviceProtocol.hpp b/ASFWDriver/Audio/Protocols/IDeviceProtocol.hpp index 7396b938..56d11e40 100644 --- a/ASFWDriver/Audio/Protocols/IDeviceProtocol.hpp +++ b/ASFWDriver/Audio/Protocols/IDeviceProtocol.hpp @@ -27,6 +27,24 @@ class IDuplexDeviceControl; namespace ASFW::Audio { +struct PlaybackPreflightRoute final { + uint8_t channel{0xFFU}; + uint32_t bandwidthUnits{0U}; + uint32_t sampleRateHz{0U}; + bool deviceCommunicationStopped{false}; +}; + +/// Protocol-neutral snapshot of a continuous host transmit ring's health. +/// Mirrors (but does not include/depend on) the transport-layer health +/// struct -- IDeviceProtocol must not reach into Isoch/OHCI types. +struct ContinuousPlaybackHealth final { + bool running{false}; + bool dead{false}; + bool eventError{false}; + uint32_t anchorExecutions{0U}; + uint16_t lastAnchorTimestamp{0U}; +}; + /// Interface for device-specific protocol handlers /// /// Device protocols are instantiated by DeviceProtocolFactory when a @@ -74,6 +92,7 @@ class IDeviceProtocol { } using VoidCallback = std::function; + using BoundedPlaybackStep = std::function; /// Optional bring-up hook to prepare device-side duplex state at 48kHz. /// Drivers can call this before any IRM reservation or host IR/IT startup. @@ -109,6 +128,64 @@ class IDeviceProtocol { return nullptr; } + /// Optional bounded bring-up route held by a device protocol. This is only + /// for staged transport validation; it is not a general streaming API. + virtual bool GetPlaybackPreflightRoute(PlaybackPreflightRoute& outRoute) const { + (void)outRoute; + return false; + } + + /// Optional staged integration hook. The protocol starts its already- + /// verified device communication engine, invokes one bounded host transmit + /// validation window, then stops the device and releases the held playback + /// IRM route. + /// This is not a live streaming API and must never make StartIO succeed. + virtual IOReturn RunBoundedPlaybackIntegrationPreflight( + const PlaybackPreflightRoute& route, + BoundedPlaybackStep prepareHost, + BoundedPlaybackStep runHostBurst, + BoundedPlaybackStep cleanupHost) { + (void)route; + (void)prepareHost; + (void)runHostBurst; + (void)cleanupHost; + return kIOReturnUnsupported; + } + + using ContinuousHealthStep = std::function; + + /// Optional dev-only continuous variant of the staged integration hook + /// above: arms the device engine and starts the host ring, then returns + /// immediately -- the ring keeps running until StopContinuousPlaybackIntegration + /// is called. Never reachable from Core Audio StartIO; must never make + /// StartIO succeed. + virtual IOReturn StartContinuousPlaybackIntegration( + const PlaybackPreflightRoute& route, + BoundedPlaybackStep prepareHost, + BoundedPlaybackStep startHostRing) { + (void)route; + (void)prepareHost; + (void)startHostRing; + return kIOReturnUnsupported; + } + + /// Stops a continuous stream started above: stops the host ring, then + /// stops the device engine and releases its held IRM route. Idempotent + /// -- returns kIOReturnNotReady if nothing is in flight. + virtual IOReturn StopContinuousPlaybackIntegration( + BoundedPlaybackStep stopHostRing) { + (void)stopHostRing; + return kIOReturnUnsupported; + } + + /// Non-blocking health snapshot of an in-flight continuous stream. + /// Returns kIOReturnNotReady if nothing is in flight. + virtual IOReturn GetContinuousPlaybackIntegrationHealth( + ContinuousHealthStep pollHealth) { + (void)pollHealth; + return kIOReturnUnsupported; + } + /// Optional protocol-neutral duplex control interface used by the audio lifecycle. virtual IDuplexDeviceControl* AsDuplexDeviceControl() noexcept { return nullptr; diff --git a/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp new file mode 100644 index 00000000..93b20e54 --- /dev/null +++ b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.cpp @@ -0,0 +1,1690 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 ASFireWire Project +// +// RMEFireface800Protocol.cpp - Stage 5H bounded silent playback integration. +// +// This stage preserves the verified device start/stop handshake and the Stage +// 5G D-D-D-S packet shape, then runs the immutable 48-cycle silence program as +// a circular ring for exactly 100 ms. OHCI is stopped before the device and IRM +// cleanup. Core Audio StartIO remains rejected; refill and interrupts stay off. + +#include "RMEFireface800Protocol.hpp" + +#include "../../../Logging/Logging.hpp" +#include "../../../Bus/IRM/IRMClient.hpp" +#include "../../Wire/AMDTP/AmdtpTxPacketizer.hpp" +#include "../../Wire/AMDTP/AmdtpPacketTimeline.hpp" +#include "../../../Shared/Isoch/IsochAudioTransport.hpp" +#include "../../../Hardware/OHCIDescriptors.hpp" +#include "../Backends/SyncAsyncBridge.hpp" + +#include +#include +#include +#include +#include + +namespace ASFW::Audio::RME { +namespace { + +constexpr uint64_t kStfAddress = 0x0000fc88f000ULL; +constexpr uint64_t kSyncStatusAddress = 0x0000801c0000ULL; +constexpr uint64_t kClockConfigAddress = 0x0000801c0004ULL; +constexpr uint64_t kTxIsochChannelAddress = 0x0000801c0008ULL; +constexpr uint64_t kRxPacketFormatAddress = 0x0000fc88f004ULL; +constexpr uint64_t kAllocTxStreamAddress = 0x0000fc88f008ULL; +constexpr uint64_t kIsochCommStartAddress = 0x0000fc88f00cULL; +constexpr uint64_t kIsochCommStopAddress = 0x0000fc88f010ULL; + +Async::FWAddress MakeAddress(uint64_t value) noexcept { + return Async::FWAddress{Async::FWAddress::AddressParts{ + .addressHi = static_cast((value >> 32U) & 0xFFFFU), + .addressLo = static_cast(value & 0xFFFFFFFFU), + }}; +} + +} // namespace + +RMEFireface800Protocol::RMEFireface800Protocol( + Protocols::Ports::FireWireBusOps& busOps, + Protocols::Ports::FireWireBusInfo& busInfo, + uint16_t nodeId, + uint64_t deviceGuid, + ::ASFW::IRM::IRMClient* irmClient) noexcept + : busOps_(busOps), + busInfo_(busInfo), + nodeId_(nodeId), + deviceGuid_(deviceGuid), + irmClient_(irmClient) {} + +RMEFireface800Protocol::~RMEFireface800Protocol() { + BestEffortStopDeviceCommunication(); + active_.store(false, std::memory_order_release); + ReleaseReservedResources(); + if (probeHandle_.IsValid()) { + (void)busOps_.Cancel(probeHandle_); + } +} + +IOReturn RMEFireface800Protocol::Initialize() { + playbackPreflightRoute_.store(0U, std::memory_order_release); + stage5hInFlight_.store(false, std::memory_order_release); + active_.store(true, std::memory_order_release); + ASFW_LOG(Audio, + "[RME] Stage 5F no-CIP wire-format preflight starting GUID=0x%016llx node=%u gen=%u", + deviceGuid_, + static_cast(nodeId_ & 0x3FU), + busInfo_.GetGeneration().value); + ProbeClockConfig(); + return kIOReturnSuccess; +} + +IOReturn RMEFireface800Protocol::Shutdown() { + BestEffortStopDeviceCommunication(); + active_.store(false, std::memory_order_release); + stage5hInFlight_.store(false, std::memory_order_release); + ReleaseReservedResources(); + if (probeHandle_.IsValid()) { + (void)busOps_.Cancel(probeHandle_); + } + ASFW_LOG(Audio, "[RME] Stage 5F preflight stopped GUID=0x%016llx", deviceGuid_); + return kIOReturnSuccess; +} + +void RMEFireface800Protocol::UpdateRuntimeContext( + uint16_t nodeId, + Protocols::AVC::FCPTransport* transport) { + (void)transport; + nodeId_ = nodeId; +} + +bool RMEFireface800Protocol::GetPlaybackPreflightRoute( + PlaybackPreflightRoute& outRoute) const { + if (!active_.load(std::memory_order_acquire)) { + return false; + } + const uint64_t snapshot = + playbackPreflightRoute_.load(std::memory_order_acquire); + if (snapshot == 0U) { + return false; + } + + outRoute.channel = static_cast(snapshot & 0xFFU); + outRoute.bandwidthUnits = + static_cast((snapshot >> 8U) & 0x00FFFFFFU); + outRoute.sampleRateHz = static_cast(snapshot >> 32U); + outRoute.deviceCommunicationStopped = true; + return outRoute.channel <= 63U && outRoute.bandwidthUnits != 0U && + outRoute.sampleRateHz != 0U; +} + +IOReturn RMEFireface800Protocol::RunBoundedPlaybackIntegrationPreflight( + const PlaybackPreflightRoute& route, + BoundedPlaybackStep prepareHost, + BoundedPlaybackStep runHostBurst, + BoundedPlaybackStep cleanupHost) { + if (!active_.load(std::memory_order_acquire) || !prepareHost || + !runHostBurst || !cleanupHost || + route.channel > 63U || route.bandwidthUnits != 1286U || + route.sampleRateHz != 192000U || + !route.deviceCommunicationStopped) { + return kIOReturnBadArgument; + } + + PlaybackPreflightRoute heldRoute{}; + if (!GetPlaybackPreflightRoute(heldRoute) || + heldRoute.channel != route.channel || + heldRoute.bandwidthUnits != route.bandwidthUnits || + heldRoute.sampleRateHz != route.sampleRateHz || + !heldRoute.deviceCommunicationStopped) { + return kIOReturnNotReady; + } + + bool expected = false; + if (!stage5hInFlight_.compare_exchange_strong( + expected, true, std::memory_order_acq_rel)) { + ASFW_LOG_WARNING(Audio, + "[RME] Stage 5H bounded integration already in flight GUID=0x%016llx", + deviceGuid_); + return kIOReturnBusy; + } + + // Consume the held route so repeated StartIO retries cannot enter while the + // synchronous bounded test owns the FF800 engine and its IRM reservation. + playbackPreflightRoute_.store(0U, std::memory_order_release); + + IOReturn finalStatus = prepareHost(); + bool hostPrepared = finalStatus == kIOReturnSuccess; + bool startAccepted = false; + bool stopAccepted = false; + bool releaseAccepted = false; + + if (!hostPrepared) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H host cadence preparation failed kr=0x%x; proceeding with bounded cleanup", + finalStatus); + } + + const uint32_t dataBlockQuadlets = DecodeChannelCount(route.sampleRateHz); + if (hostPrepared && dataBlockQuadlets == 0U) { + finalStatus = kIOReturnBadArgument; + } else if (hostPrepared) { + // Exact Stage 4D/5F FF800 begin-session value: communication enable, + // S800 flag, and the 192 kHz data-block-quadlet count. + const uint32_t startValue = + 0x80000000U | 0x00000800U | dataBlockQuadlets; + const std::array startLE = { + static_cast(startValue & 0xFFU), + static_cast((startValue >> 8U) & 0xFFU), + static_cast((startValue >> 16U) & 0xFFU), + static_cast((startValue >> 24U) & 0xFFU), + }; + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + const auto startResult = ASFW::Audio::WaitForAsyncResult( + [this, generation, node, startLE](auto done) { + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kIsochCommStartAddress), + std::span{startLE}, + FW::FwSpeed::S400, + [done](Async::AsyncStatus status, + std::span payload) mutable { + (void)payload; + const bool accepted = + status == Async::AsyncStatus::kSuccess; + done(accepted ? kIOReturnSuccess : kIOReturnIOError, + accepted); + }); + if (!probeHandle_.IsValid()) { + done(kIOReturnNoResources, false); + } + }, + 500U, + kIOReturnTimeout); + startAccepted = + startResult.status == kIOReturnSuccess && startResult.value; + if (!startAccepted) { + if (startResult.status == kIOReturnTimeout && + probeHandle_.IsValid()) { + (void)busOps_.Cancel(probeHandle_); + } + finalStatus = startResult.status == kIOReturnSuccess + ? kIOReturnIOError + : startResult.status; + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H FF800 playback-engine start failed kr=0x%x gen=%u value=0x%08x", + startResult.status, + generation.value, + startValue); + } else { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5H FF800 playback engine started channel=%u rate=%u value=0x%08x", + route.channel, + route.sampleRateHz, + startValue); + finalStatus = runHostBurst(); + } + } + + // Cleanup is deliberately ordered on success and every failure path: + // host RUN clear/ACTIVE drain first, then FF800 stop, then IRM release. + const IOReturn hostCleanupStatus = cleanupHost(); + if (hostCleanupStatus != kIOReturnSuccess && + finalStatus == kIOReturnSuccess) { + finalStatus = hostCleanupStatus; + } + if (hostCleanupStatus != kIOReturnSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H host cleanup failed kr=0x%x before FF800 stop", + hostCleanupStatus); + } + + uint8_t reservedChannel = 0xFFU; + uint32_t reservedBandwidth = 0U; + const IOReturn stopReleaseStatus = StopDeviceEngineAndReleaseIrm( + stopAccepted, releaseAccepted, reservedChannel, reservedBandwidth); + if (stopReleaseStatus != kIOReturnSuccess && + finalStatus == kIOReturnSuccess) { + finalStatus = stopReleaseStatus; + } + + stage5hInFlight_.store(false, std::memory_order_release); + + if (stopAccepted && releaseAccepted) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5H FF800 playback engine stopped and IRM route released channel=%u bandwidth=%u", + reservedChannel, + reservedBandwidth); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H cleanup completed with errors startAccepted=%u stopAccepted=%u releaseAccepted=%u", + startAccepted ? 1U : 0U, + stopAccepted ? 1U : 0U, + releaseAccepted ? 1U : 0U); + } + + if (finalStatus == kIOReturnSuccess) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5H bounded circular silent playback integration passed; Core Audio StartIO remains rejected"); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5H bounded circular silent playback integration failed kr=0x%x; Core Audio StartIO remains rejected", + finalStatus); + } + return finalStatus; +} + +IOReturn RMEFireface800Protocol::StopDeviceEngineAndReleaseIrm( + bool& outStopAccepted, + bool& outReleaseAccepted, + uint8_t& outReleasedChannel, + uint32_t& outReleasedBandwidth) noexcept { + outStopAccepted = false; + outReleaseAccepted = false; + outReleasedChannel = 0xFFU; + outReleasedBandwidth = 0U; + + IOReturn status = kIOReturnSuccess; + + const std::array stopLE = {0x00U, 0x00U, 0x00U, 0x80U}; + const auto stopGeneration = busInfo_.GetGeneration(); + const auto stopNode = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + const auto stopResult = ASFW::Audio::WaitForAsyncResult( + [this, stopGeneration, stopNode, stopLE](auto done) { + probeHandle_ = busOps_.WriteBlock( + stopGeneration, + stopNode, + MakeAddress(kIsochCommStopAddress), + std::span{stopLE}, + FW::FwSpeed::S400, + [done](Async::AsyncStatus status, + std::span payload) mutable { + (void)payload; + const bool accepted = + status == Async::AsyncStatus::kSuccess; + done(accepted ? kIOReturnSuccess : kIOReturnIOError, + accepted); + }); + if (!probeHandle_.IsValid()) { + done(kIOReturnNoResources, false); + } + }, + 500U, + kIOReturnTimeout); + outStopAccepted = + stopResult.status == kIOReturnSuccess && stopResult.value; + if (!outStopAccepted) { + status = stopResult.status == kIOReturnSuccess + ? kIOReturnIOError + : stopResult.status; + ASFW_LOG_ERROR(Audio, + "[RME] FF800 stop command failed kr=0x%x gen=%u", + stopResult.status, + stopGeneration.value); + } + + deviceTxAllocationRequested_ = false; + deviceTxAllocated_ = false; + deviceTxChannel_ = 0xFFU; + + const bool hadReservation = playbackResourcesReserved_; + const uint8_t reservedChannel = reservedPlaybackChannel_; + const uint32_t reservedBandwidth = reservedPlaybackBandwidthUnits_; + playbackResourcesReserved_ = false; + reservedPlaybackChannel_ = 0xFFU; + reservedPlaybackBandwidthUnits_ = 0U; + outReleasedChannel = reservedChannel; + outReleasedBandwidth = reservedBandwidth; + + if (!hadReservation || irmClient_ == nullptr || reservedChannel > 63U || + reservedBandwidth == 0U) { + ASFW_LOG_ERROR(Audio, + "[RME] cleanup missing held IRM route actualChannel=%u actualBandwidth=%u", + reservedChannel, + reservedBandwidth); + if (status == kIOReturnSuccess) { + status = kIOReturnNotReady; + } + return status; + } + + const auto releaseResult = ASFW::Audio::WaitForAsyncResult( + [this, reservedChannel, reservedBandwidth](auto done) { + irmClient_->ReleaseResources( + reservedChannel, + reservedBandwidth, + [done](IRM::AllocationStatus status) mutable { + const bool accepted = + status == IRM::AllocationStatus::Success; + done(accepted ? kIOReturnSuccess : kIOReturnIOError, + accepted); + }); + }, + 1000U, + kIOReturnTimeout); + outReleaseAccepted = + releaseResult.status == kIOReturnSuccess && releaseResult.value; + if (!outReleaseAccepted) { + if (status == kIOReturnSuccess) { + status = releaseResult.status == kIOReturnSuccess + ? kIOReturnIOError + : releaseResult.status; + } + ASFW_LOG_ERROR(Audio, + "[RME] IRM release failed kr=0x%x channel=%u bandwidth=%u", + releaseResult.status, + reservedChannel, + reservedBandwidth); + } + + return status; +} + +IOReturn RMEFireface800Protocol::StartContinuousPlaybackIntegration( + const PlaybackPreflightRoute& route, + BoundedPlaybackStep prepareHost, + BoundedPlaybackStep startHostRing) { + if (!active_.load(std::memory_order_acquire) || !prepareHost || + !startHostRing || + route.channel > 63U || route.bandwidthUnits != 1286U || + route.sampleRateHz != 192000U || + !route.deviceCommunicationStopped) { + return kIOReturnBadArgument; + } + + PlaybackPreflightRoute heldRoute{}; + if (!GetPlaybackPreflightRoute(heldRoute) || + heldRoute.channel != route.channel || + heldRoute.bandwidthUnits != route.bandwidthUnits || + heldRoute.sampleRateHz != route.sampleRateHz || + !heldRoute.deviceCommunicationStopped) { + return kIOReturnNotReady; + } + + bool expectedStage5h = false; + bool expectedContinuous = false; + if (stage5hInFlight_.load(std::memory_order_acquire) || + !continuousPlaybackInFlight_.compare_exchange_strong( + expectedContinuous, true, std::memory_order_acq_rel)) { + ASFW_LOG_WARNING(Audio, + "[RME] Continuous integration start refused: already in flight (stage5h=%u continuous=%u) GUID=0x%016llx", + expectedStage5h ? 1U : 0U, + expectedContinuous ? 1U : 0U, + deviceGuid_); + return kIOReturnBusy; + } + + // Consume the held route, mirroring RunBoundedPlaybackIntegrationPreflight: + // repeated Start calls while a session is live must not re-enter here. + playbackPreflightRoute_.store(0U, std::memory_order_release); + + IOReturn finalStatus = prepareHost(); + const bool hostPrepared = finalStatus == kIOReturnSuccess; + if (!hostPrepared) { + ASFW_LOG_ERROR(Audio, + "[RME] Continuous integration host cadence preparation failed kr=0x%x", + finalStatus); + continuousPlaybackInFlight_.store(false, std::memory_order_release); + return finalStatus; + } + + const uint32_t dataBlockQuadlets = DecodeChannelCount(route.sampleRateHz); + if (dataBlockQuadlets == 0U) { + continuousPlaybackInFlight_.store(false, std::memory_order_release); + return kIOReturnBadArgument; + } + + // Exact Stage 4D/5F/5H FF800 begin-session value: communication enable, + // S800 flag, and the 192 kHz data-block-quadlet count. + const uint32_t startValue = + 0x80000000U | 0x00000800U | dataBlockQuadlets; + const std::array startLE = { + static_cast(startValue & 0xFFU), + static_cast((startValue >> 8U) & 0xFFU), + static_cast((startValue >> 16U) & 0xFFU), + static_cast((startValue >> 24U) & 0xFFU), + }; + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + const auto startResult = ASFW::Audio::WaitForAsyncResult( + [this, generation, node, startLE](auto done) { + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kIsochCommStartAddress), + std::span{startLE}, + FW::FwSpeed::S400, + [done](Async::AsyncStatus status, + std::span payload) mutable { + (void)payload; + const bool accepted = + status == Async::AsyncStatus::kSuccess; + done(accepted ? kIOReturnSuccess : kIOReturnIOError, + accepted); + }); + if (!probeHandle_.IsValid()) { + done(kIOReturnNoResources, false); + } + }, + 500U, + kIOReturnTimeout); + const bool startAccepted = + startResult.status == kIOReturnSuccess && startResult.value; + if (!startAccepted) { + if (startResult.status == kIOReturnTimeout && + probeHandle_.IsValid()) { + (void)busOps_.Cancel(probeHandle_); + } + ASFW_LOG_ERROR(Audio, + "[RME] Continuous integration FF800 playback-engine start failed kr=0x%x gen=%u value=0x%08x", + startResult.status, + generation.value, + startValue); + bool stopAccepted = false; + bool releaseAccepted = false; + uint8_t releasedChannel = 0xFFU; + uint32_t releasedBandwidth = 0U; + (void)StopDeviceEngineAndReleaseIrm( + stopAccepted, releaseAccepted, releasedChannel, releasedBandwidth); + continuousPlaybackInFlight_.store(false, std::memory_order_release); + return startResult.status == kIOReturnSuccess ? kIOReturnIOError + : startResult.status; + } + + ASFW_LOG(Audio, + "[RME] ✅ Continuous integration FF800 playback engine started channel=%u rate=%u value=0x%08x", + route.channel, + route.sampleRateHz, + startValue); + + finalStatus = startHostRing(); + if (finalStatus != kIOReturnSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Continuous integration host ring start failed kr=0x%x; stopping device engine", + finalStatus); + bool stopAccepted = false; + bool releaseAccepted = false; + uint8_t releasedChannel = 0xFFU; + uint32_t releasedBandwidth = 0U; + (void)StopDeviceEngineAndReleaseIrm( + stopAccepted, releaseAccepted, releasedChannel, releasedBandwidth); + continuousPlaybackInFlight_.store(false, std::memory_order_release); + return finalStatus; + } + + ASFW_LOG(Audio, + "[RME] ✅ Continuous circular silent playback integration started; Core Audio StartIO remains rejected GUID=0x%016llx", + deviceGuid_); + return kIOReturnSuccess; +} + +IOReturn RMEFireface800Protocol::StopContinuousPlaybackIntegration( + BoundedPlaybackStep stopHostRing) { + bool expected = true; + if (!continuousPlaybackInFlight_.compare_exchange_strong( + expected, false, std::memory_order_acq_rel)) { + return kIOReturnNotReady; + } + + const IOReturn hostStopStatus = stopHostRing ? stopHostRing() + : kIOReturnUnsupported; + if (hostStopStatus != kIOReturnSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Continuous integration host ring stop failed kr=0x%x; proceeding with device/IRM teardown", + hostStopStatus); + } + + bool stopAccepted = false; + bool releaseAccepted = false; + uint8_t releasedChannel = 0xFFU; + uint32_t releasedBandwidth = 0U; + const IOReturn stopReleaseStatus = StopDeviceEngineAndReleaseIrm( + stopAccepted, releaseAccepted, releasedChannel, releasedBandwidth); + + if (stopAccepted && releaseAccepted) { + ASFW_LOG(Audio, + "[RME] ✅ Continuous integration FF800 playback engine stopped and IRM route released channel=%u bandwidth=%u", + releasedChannel, + releasedBandwidth); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Continuous integration cleanup completed with errors stopAccepted=%u releaseAccepted=%u", + stopAccepted ? 1U : 0U, + releaseAccepted ? 1U : 0U); + } + + return hostStopStatus != kIOReturnSuccess ? hostStopStatus + : stopReleaseStatus; +} + +IOReturn RMEFireface800Protocol::GetContinuousPlaybackIntegrationHealth( + ContinuousHealthStep pollHealth) { + if (!continuousPlaybackInFlight_.load(std::memory_order_acquire)) { + return kIOReturnNotReady; + } + if (!pollHealth) { + return kIOReturnBadArgument; + } + ContinuousPlaybackHealth health{}; + return pollHealth(health); +} + +void RMEFireface800Protocol::ProbeClockConfig() noexcept { + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + probeHandle_ = busOps_.ReadBlock( + generation, + node, + MakeAddress(kClockConfigAddress), + 4, + FW::FwSpeed::S400, + [this, generation](Async::AsyncStatus status, + std::span payload) { + if (!active_.load(std::memory_order_acquire)) { + return; + } + + if (status != Async::AsyncStatus::kSuccess || payload.size() < 4U) { + ASFW_LOG_ERROR(Audio, + "[RME] Clock probe failed status=%{public}s bytes=%zu gen=%u", + Async::ToString(status), + payload.size(), + generation.value); + return; + } + + const uint32_t raw = ReadLE32(payload.data()); + const uint32_t rate = DecodeSampleRate(raw); + currentRate_ = rate; + const char* source = DecodeClockSource(raw); + + ASFW_LOG(Audio, + "[RME] ✅ Clock raw=0x%08x rate=%u source=%{public}s gen=%u", + raw, + rate, + source, + generation.value); + ASFW_LOG(Audio, + "[RME] Clock options: spdifOut=%{public}s emphasis=%{public}s opticalOut=%{public}s wordSingleSpeed=%{public}s spdifIn=%{public}s", + (raw & 0x00000020U) != 0U ? "professional" : "consumer", + (raw & 0x00000040U) != 0U ? "on" : "off", + (raw & 0x00000100U) != 0U ? "S/PDIF" : "ADAT", + (raw & 0x00002000U) != 0U ? "on" : "off", + (raw & 0x00000200U) != 0U ? "optical" : "coaxial"); + + LogStreamCaps(rate); + if (rate == 0U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F STF write skipped: decoded rate is zero"); + ProbeSyncStatus(); + return; + } + ValidateStfWritePath(rate); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, "[RME] Clock probe could not be submitted"); + } +} + +void RMEFireface800Protocol::ValidateStfWritePath(uint32_t rate) noexcept { + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + const std::array rateLE = { + static_cast(rate & 0xFFU), + static_cast((rate >> 8U) & 0xFFU), + static_cast((rate >> 16U) & 0xFFU), + static_cast((rate >> 24U) & 0xFFU), + }; + + ASFW_LOG(Audio, + "[RME] Stage 5F writing current STF=%u Hz (no rate change) gen=%u", + rate, + generation.value); + + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kStfAddress), + std::span{rateLE}, + FW::FwSpeed::S400, + [this, generation, rate](Async::AsyncStatus status, + std::span payload) { + (void)payload; + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F STF write failed status=%{public}s gen=%u", + Async::ToString(status), + generation.value); + ProbeSyncStatus(); + return; + } + + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F STF write accepted: %u Hz; waiting 100 ms before verification", + rate); + IOSleep(100); + VerifyClockAfterStfWrite(rate); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F STF write could not be submitted"); + ProbeSyncStatus(); + } +} + +void RMEFireface800Protocol::VerifyClockAfterStfWrite(uint32_t expectedRate) noexcept { + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + probeHandle_ = busOps_.ReadBlock( + generation, + node, + MakeAddress(kClockConfigAddress), + 4, + FW::FwSpeed::S400, + [this, generation, expectedRate](Async::AsyncStatus status, + std::span payload) { + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess || payload.size() < 4U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F clock verification failed status=%{public}s bytes=%zu gen=%u", + Async::ToString(status), + payload.size(), + generation.value); + ProbeSyncStatus(); + return; + } + + const uint32_t raw = ReadLE32(payload.data()); + const uint32_t actualRate = DecodeSampleRate(raw); + if (actualRate == expectedRate) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F STF write-path verified: expected=%u actual=%u raw=0x%08x", + expectedRate, + actualRate, + raw); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F STF verification mismatch: expected=%u actual=%u raw=0x%08x", + expectedRate, + actualRate, + raw); + } + ProbeSyncStatus(); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F clock verification could not be submitted"); + ProbeSyncStatus(); + } +} + +void RMEFireface800Protocol::ProbeSyncStatus() noexcept { + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + probeHandle_ = busOps_.ReadBlock( + generation, + node, + MakeAddress(kSyncStatusAddress), + 8, + FW::FwSpeed::S400, + [this, generation](Async::AsyncStatus status, + std::span payload) { + if (!active_.load(std::memory_order_acquire)) { + return; + } + + if (status != Async::AsyncStatus::kSuccess || payload.size() < 8U) { + ASFW_LOG_ERROR(Audio, + "[RME] Sync-status probe failed status=%{public}s bytes=%zu gen=%u", + Async::ToString(status), + payload.size(), + generation.value); + return; + } + + const uint32_t status0 = ReadLE32(payload.data()); + const uint32_t status1 = ReadLE32(payload.data() + 4U); + const char* referred = DecodeReferredClockSource(status0, status1); + const uint32_t referredRate = DecodeReferredRate(status0); + + ASFW_LOG(Audio, + "[RME] ✅ Sync status raw0=0x%08x raw1=0x%08x gen=%u", + status0, + status1, + generation.value); + ASFW_LOG(Audio, + "[RME] External sync: word=%{public}s spdif=%{public}s adat1=%{public}s adat2=%{public}s", + DecodeExternalState(status0, 0x40000000U, 0x20000000U), + DecodeExternalState(status0, 0x00080000U, 0x00040000U), + DecodeExternalState(status0, 0x00000400U, 0x00001000U), + DecodeExternalState(status0, 0x00000800U, 0x00002000U)); + ASFW_LOG(Audio, + "[RME] Referred clock: source=%{public}s rate=%u", + referred, + referredRate); + + ProbeTxIsochChannel(); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, "[RME] Sync-status probe could not be submitted"); + } +} + +void RMEFireface800Protocol::ProbeTxIsochChannel() noexcept { + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + probeHandle_ = busOps_.ReadBlock( + generation, + node, + MakeAddress(kTxIsochChannelAddress), + 4, + FW::FwSpeed::S400, + [this, generation](Async::AsyncStatus status, + std::span payload) { + if (!active_.load(std::memory_order_acquire)) { + return; + } + + if (status != Async::AsyncStatus::kSuccess || payload.size() < 4U) { + ASFW_LOG_ERROR(Audio, + "[RME] TX isoch-channel probe failed status=%{public}s bytes=%zu gen=%u", + Async::ToString(status), + payload.size(), + generation.value); + return; + } + + const uint32_t raw = ReadLE32(payload.data()); + if (raw == 0xFFFFFFFFU) { + ASFW_LOG(Audio, + "[RME] ✅ TX isoch channel: inactive/not allocated raw=0xffffffff gen=%u", + generation.value); + } else { + ASFW_LOG(Audio, + "[RME] ✅ TX isoch channel reported raw=0x%08x channel=%u gen=%u", + raw, + raw & 0x3FU, + generation.value); + } + ReservePlaybackResourcesPreflight(currentRate_); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, "[RME] TX isoch-channel probe could not be submitted"); + } +} + + +void RMEFireface800Protocol::ReservePlaybackResourcesPreflight(uint32_t rate, + uint32_t attempt) noexcept { + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (playbackResourcesReserved_) { + ASFW_LOG(Audio, + "[RME] Stage 5F playback resources already reserved channel=%u bandwidth=%u", + reservedPlaybackChannel_, + reservedPlaybackBandwidthUnits_); + return; + } + if (irmClient_ == nullptr) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F cannot reserve playback resources: IRM client unavailable"); + return; + } + + const uint32_t bandwidthUnits = CalculatePlaybackBandwidthUnits(rate); + if (bandwidthUnits == 0U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F cannot reserve playback resources: invalid rate=%u", + rate); + return; + } + + irmClient_->ReadResourcesSnapshot( + [this, rate, bandwidthUnits, attempt](IRM::AllocationStatus status, + IRM::ResourceSnapshot snapshot) { + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != IRM::AllocationStatus::Success) { + // Right after a bus reset the IRM node hasn't been re-resolved + // yet (IRMClient::ReadIRMWindow reports NotFound while + // irmNodeId_ == 0xFF) — this is a transient race against bus + // manager/Self-ID processing, not a permanent failure. Retry + // with the same bounded backoff PollDeviceTxIsochChannel uses + // below, instead of stranding playbackPreflightRoute_ at 0 + // until the next full rediscovery. + if (status == IRM::AllocationStatus::NotFound && attempt < 10U) { + ASFW_LOG_WARNING( + Audio, + "[RME] Stage 5F IRM snapshot not ready (IRM node not yet resolved) status=%{public}s attempt=%u; retrying", + IRM::ToString(status), + attempt); + IOSleep(50); + ReservePlaybackResourcesPreflight(rate, attempt + 1U); + return; + } + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F IRM snapshot failed status=%{public}s attempt=%u", + IRM::ToString(status), + attempt); + return; + } + + const uint8_t channel = + SelectAvailableChannel(snapshot.channelsAvailable31_0, + snapshot.channelsAvailable63_32); + ASFW_LOG(Audio, + "[RME] Stage 5F IRM snapshot bandwidthAvailable=%u requested=%u selectedChannel=%u", + snapshot.bandwidthAvailable, + bandwidthUnits, + channel); + + if (channel == 0xFFU || snapshot.bandwidthAvailable < bandwidthUnits) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F insufficient IRM resources bandwidthAvailable=%u requested=%u channel=%u", + snapshot.bandwidthAvailable, + bandwidthUnits, + channel); + return; + } + + irmClient_->AllocateResources( + channel, + bandwidthUnits, + [this, rate, channel, bandwidthUnits](IRM::AllocationStatus allocateStatus) { + if (allocateStatus != IRM::AllocationStatus::Success) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F IRM reservation failed status=%{public}s channel=%u bandwidth=%u", + IRM::ToString(allocateStatus), + channel, + bandwidthUnits); + return; + } + + if (!active_.load(std::memory_order_acquire)) { + irmClient_->ReleaseResources( + channel, bandwidthUnits, [](IRM::AllocationStatus) {}); + return; + } + + reservedPlaybackChannel_ = channel; + reservedPlaybackBandwidthUnits_ = bandwidthUnits; + playbackResourcesReserved_ = true; + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F IRM playback reservation acquired channel=%u bandwidth=%u", + channel, + bandwidthUnits); + ProgramRxPacketFormat(rate, channel, bandwidthUnits); + }); + }); +} + +void RMEFireface800Protocol::ProgramRxPacketFormat(uint32_t rate, + uint8_t channel, + uint32_t bandwidthUnits) noexcept { + const uint32_t dataBlockQuadlets = DecodeChannelCount(rate); + if (dataBlockQuadlets == 0U || channel > 63U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F RX packet-format skipped rate=%u dbq=%u channel=%u", + rate, + dataBlockQuadlets, + channel); + ReleaseReservedResources(); + return; + } + + // Linux ff-protocol-former.c programs ((DBQ << 3) << 8) | channel. + const uint32_t format = ((dataBlockQuadlets << 3U) << 8U) | channel; + const std::array formatLE = { + static_cast(format & 0xFFU), + static_cast((format >> 8U) & 0xFFU), + static_cast((format >> 16U) & 0xFFU), + static_cast((format >> 24U) & 0xFFU), + }; + + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kRxPacketFormatAddress), + std::span{formatLE}, + FW::FwSpeed::S400, + [this, generation, rate, channel, bandwidthUnits, format]( + Async::AsyncStatus status, std::span payload) { + (void)payload; + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F RX packet-format write failed status=%{public}s gen=%u", + Async::ToString(status), + generation.value); + ReleaseReservedResources(); + return; + } + + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F playback route armed rate=%u dbq=%u channel=%u bandwidth=%u format=0x%08x", + rate, + DecodeChannelCount(rate), + channel, + bandwidthUnits, + format); + RequestDeviceTxAllocation(rate); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F RX packet-format write could not be submitted"); + ReleaseReservedResources(); + } +} + +void RMEFireface800Protocol::RequestDeviceTxAllocation(uint32_t rate) noexcept { + const uint32_t dataBlockQuadlets = DecodeChannelCount(rate); + if (dataBlockQuadlets == 0U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F TX allocation skipped: invalid rate=%u", + rate); + ReleaseReservedResources(); + return; + } + + const std::array dbqLE = { + static_cast(dataBlockQuadlets & 0xFFU), + static_cast((dataBlockQuadlets >> 8U) & 0xFFU), + static_cast((dataBlockQuadlets >> 16U) & 0xFFU), + static_cast((dataBlockQuadlets >> 24U) & 0xFFU), + }; + + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kAllocTxStreamAddress), + std::span{dbqLE}, + FW::FwSpeed::S400, + [this, generation, rate, dataBlockQuadlets]( + Async::AsyncStatus status, std::span payload) { + (void)payload; + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F device TX allocation request failed status=%{public}s gen=%u", + Async::ToString(status), + generation.value); + ReleaseReservedResources(); + return; + } + + deviceTxAllocationRequested_ = true; + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F device TX allocation request accepted dbq=%u; polling channel", + dataBlockQuadlets); + PollDeviceTxIsochChannel(rate, 1U); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F device TX allocation request could not be submitted"); + ReleaseReservedResources(); + } +} + +void RMEFireface800Protocol::PollDeviceTxIsochChannel(uint32_t rate, + uint32_t attempt) noexcept { + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + probeHandle_ = busOps_.ReadBlock( + generation, + node, + MakeAddress(kTxIsochChannelAddress), + 4, + FW::FwSpeed::S400, + [this, generation, rate, attempt](Async::AsyncStatus status, + std::span payload) { + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess || payload.size() < 4U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F TX channel poll failed status=%{public}s bytes=%zu attempt=%u gen=%u", + Async::ToString(status), + payload.size(), + attempt, + generation.value); + StopDeviceCommunicationPreflight(); + return; + } + + const uint32_t raw = ReadLE32(payload.data()); + if (raw == 0xFFFFFFFFU) { + if (attempt >= 10U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F device TX allocation timed out after %u polls", + attempt); + StopDeviceCommunicationPreflight(); + return; + } + + IOSleep(50); + PollDeviceTxIsochChannel(rate, attempt + 1U); + return; + } + + const uint8_t channel = static_cast(raw & 0x3FU); + deviceTxChannel_ = channel; + deviceTxAllocated_ = true; + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F device TX channel allocated raw=0x%08x channel=%u polls=%u rate=%u", + raw, + channel, + attempt, + rate); + StartDeviceCommunicationPreflight(rate); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F TX channel poll could not be submitted attempt=%u", + attempt); + StopDeviceCommunicationPreflight(); + } +} + +void RMEFireface800Protocol::StartDeviceCommunicationPreflight(uint32_t rate) noexcept { + const uint32_t dataBlockQuadlets = DecodeChannelCount(rate); + if (dataBlockQuadlets == 0U) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F start command skipped: invalid rate=%u", + rate); + StopDeviceCommunicationPreflight(); + return; + } + + // Linux ff800_begin_session(): 0x80000000 | DBQ | S800 flag. + // This machine is linked at S800, verified by topology/self-ID. + const uint32_t startValue = 0x80000000U | 0x00000800U | dataBlockQuadlets; + const std::array startLE = { + static_cast(startValue & 0xFFU), + static_cast((startValue >> 8U) & 0xFFU), + static_cast((startValue >> 16U) & 0xFFU), + static_cast((startValue >> 24U) & 0xFFU), + }; + + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kIsochCommStartAddress), + std::span{startLE}, + FW::FwSpeed::S400, + [this, generation, rate, startValue](Async::AsyncStatus status, + std::span payload) { + (void)payload; + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F start command failed status=%{public}s gen=%u value=0x%08x", + Async::ToString(status), + generation.value, + startValue); + StopDeviceCommunicationPreflight(); + return; + } + + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F device isoch start accepted rate=%u dbq=%u txChannel=%u playbackChannel=%u value=0x%08x; running 100 ms without OHCI contexts", + rate, + DecodeChannelCount(rate), + deviceTxChannel_, + reservedPlaybackChannel_, + startValue); + IOSleep(100); + StopDeviceCommunicationPreflight(); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F start command could not be submitted"); + StopDeviceCommunicationPreflight(); + } +} + +void RMEFireface800Protocol::StopDeviceCommunicationPreflight() noexcept { + const std::array stopLE = {0x00U, 0x00U, 0x00U, 0x80U}; + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + probeHandle_ = busOps_.WriteBlock( + generation, + node, + MakeAddress(kIsochCommStopAddress), + std::span{stopLE}, + FW::FwSpeed::S400, + [this, generation](Async::AsyncStatus status, + std::span payload) { + (void)payload; + if (!active_.load(std::memory_order_acquire)) { + return; + } + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F stop command failed status=%{public}s gen=%u; releasing host IRM reservation", + Async::ToString(status), + generation.value); + deviceTxAllocationRequested_ = false; + deviceTxAllocated_ = false; + deviceTxChannel_ = 0xFFU; + ReleaseReservedResources(); + return; + } + + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F stop command accepted; FF800 TX channel may remain latched by design (channel=%u)", + deviceTxChannel_); + deviceTxAllocationRequested_ = false; + deviceTxAllocated_ = false; + deviceTxChannel_ = 0xFFU; + const uint64_t routeSnapshot = + (static_cast(currentRate_) << 32U) | + (static_cast( + reservedPlaybackBandwidthUnits_ & 0x00FFFFFFU) + << 8U) | + static_cast(reservedPlaybackChannel_); + playbackPreflightRoute_.store( + playbackResourcesReserved_ ? routeSnapshot : 0U, + std::memory_order_release); + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F playback route held for bounded host packet test channel=%u bandwidth=%u deviceStopped=1", + reservedPlaybackChannel_, + reservedPlaybackBandwidthUnits_); + RunNoCipWireFormatSelfTest(); + }); + + if (!probeHandle_.IsValid()) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F stop command could not be submitted; releasing host IRM reservation"); + deviceTxAllocationRequested_ = false; + deviceTxAllocated_ = false; + deviceTxChannel_ = 0xFFU; + ReleaseReservedResources(); + } +} + +void RMEFireface800Protocol::RunNoCipWireFormatSelfTest() noexcept { + using namespace ASFW::Protocols::Audio::AMDTP; + + constexpr uint32_t kCycles = 8U; + constexpr uint32_t kSlotCount = 4U; + constexpr uint32_t kFramesPerDataPacket = 32U; + constexpr uint32_t kDbq = 12U; + constexpr uint32_t kExpectedDataPackets = 6U; + constexpr uint32_t kExpectedEmptyPackets = 2U; + constexpr uint32_t kExpectedFrames = 192U; + constexpr uint32_t kDataPacketBytes = kFramesPerDataPacket * kDbq * 4U; + + std::array slots{}; + AmdtpPacketTimeline timeline{}; + if (!timeline.AttachSlots(slots.data(), static_cast(slots.size()))) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F wire self-test failed: timeline attach"); + return; + } + + auto packetStorage = std::unique_ptr( + new (std::nothrow) uint8_t[kSlotCount * kDataPacketBytes]); + if (!packetStorage) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F wire self-test failed: packet storage allocation"); + return; + } + + AmdtpStreamConfig config{}; + config.sampleRate = 192000U; + config.streamMode = StreamMode::Blocking; + config.sid = 1U; + config.dbs = static_cast(kDbq); + config.pcmChannels = static_cast(kDbq); + config.midiSlots = 0U; + config.framesPerDataPacket = static_cast(kFramesPerDataPacket); + config.maxPacketBytes = kDataPacketBytes; + config.includeCipHeader = false; + + AmdtpTxPolicy policy{}; + policy.hostToDevicePcmEncoding = PcmSlotEncoding::RawSigned24In32BE; + policy.initializeNonAudioSlots = false; + policy.emptyPacketsDuringIdle = true; + + AmdtpTxPacketizer packetizer{}; + packetizer.BindTimeline(&timeline); + if (!packetizer.Configure(config, policy)) { + ASFW_LOG_ERROR(Audio, "[RME] Stage 5F wire self-test failed: packetizer configure"); + return; + } + packetizer.Reset(0U, 0U); + + uint32_t dataPackets = 0U; + uint32_t emptyPackets = 0U; + uint32_t frames = 0U; + bool geometryValid = true; + bool payloadZero = true; + bool ohciMetadataValid = true; + uint32_t metadataCommits = 0U; + uint32_t skipCycles = 0U; + + Async::HW::OHCIDescriptor skipDescriptor{}; + Async::HW::ITDescriptorBuilder::BuildOutputLast( + skipDescriptor, + { + .dataIOVA = 0U, + .payloadSize = 0U, + .branchIOVA = 0x00001000U, + .zValue = 4U, + .interruptBits = Async::HW::OHCIDescriptor::kIntNever, + }); + const uint32_t skipControlHigh = + skipDescriptor.control >> + Async::HW::OHCIDescriptor::kControlHighShift; + const bool skipDescriptorValid = + ((skipControlHigh >> Async::HW::OHCIDescriptor::kCmdShift) & 0xFU) == + Async::HW::OHCIDescriptor::kCmdOutputLast && + ((skipControlHigh >> Async::HW::OHCIDescriptor::kKeyShift) & 0x7U) == + Async::HW::OHCIDescriptor::kKeyStandard && + ((skipControlHigh >> Async::HW::OHCIDescriptor::kBranchShift) & 0x3U) == + Async::HW::OHCIDescriptor::kBranchAlways && + (skipDescriptor.control & 0xFFFFU) == 0U && + skipDescriptor.dataAddress == 0U && + Async::HW::DecodeBranchPhys32_AT(skipDescriptor.branchWord) == + 0x00001000U && + (skipDescriptor.branchWord & 0xFU) == 4U && + !Async::HW::IsImmediate(skipDescriptor); + + for (uint32_t cycle = 0; cycle < kCycles; ++cycle) { + uint8_t* packetBytes = packetStorage.get() + + (cycle % kSlotCount) * kDataPacketBytes; + for (uint32_t i = 0; i < kDataPacketBytes; ++i) { + packetBytes[i] = 0xA5U; + } + + TxPacketSlotView slot{ + .packetIndex = cycle, + .bytes = packetBytes, + .capacityBytes = kDataPacketBytes, + }; + AmdtpTimingState timing{}; + timing.disposition = AmdtpPacketDisposition::Data; + + PreparedTxPacket packet{}; + if (!packetizer.PrepareNextPacket(slot, timing, packet)) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F wire self-test failed: packet preparation cycle=%u", + cycle); + return; + } + + if (packet.isData) { + ++dataPackets; + frames += packet.framesInPacket; + if (packet.byteCount != kDataPacketBytes || + packet.framesInPacket != kFramesPerDataPacket || + packet.dbs != kDbq) { + geometryValid = false; + } + for (uint32_t i = 0; i < packet.byteCount; ++i) { + if (packetBytes[i] != 0U) { + payloadZero = false; + break; + } + } + } else { + ++emptyPackets; + if (packet.byteCount != 0U || packet.framesInPacket != 0U) { + geometryValid = false; + } + } + + // Exercise the same plain helper used by DextTxSlotProvider to publish + // OHCI metadata. FF800 packets must use tag 0 (no CIP), S400, tcode A, + // and data_length equal to the raw payload size (or zero when idle). + const uint32_t headerQ0 = ASFW::IsochTransport::BuildIsochTxHeaderQ0( + ASFW::IsochTransport::IsochPacketTag::kNoCipHeader); + const uint32_t headerQ1 = + ASFW::IsochTransport::BuildIsochTxHeaderQ1(packet.byteCount); + const uint32_t expectedLength = packet.isData ? kDataPacketBytes : 0U; + const auto decodedTag = + ASFW::IsochTransport::DecodeIsochTxHeaderTag(headerQ0); + const bool shouldSkip = + ASFW::IsochTransport::ShouldSkipIsochTxPacket( + decodedTag, packet.byteCount); + if (shouldSkip) { + ++skipCycles; + } + if (decodedTag != + ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(headerQ0) != + ASFW::IsochTransport::kIsochSpeedS400 || + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(headerQ0) != + ASFW::IsochTransport::kIsochDataBlockTCode || + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(headerQ1) != + expectedLength || + shouldSkip != !packet.isData) { + ohciMetadataValid = false; + } else { + ++metadataCommits; + } + } + + if (dataPackets == kExpectedDataPackets && + emptyPackets == kExpectedEmptyPackets && + frames == kExpectedFrames && geometryValid && payloadZero && + ohciMetadataValid && metadataCommits == kCycles && + skipCycles == kExpectedEmptyPackets && skipDescriptorValid) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F FF800 no-CIP wire self-test passed cycles=%u data=%u empty=%u frames=%u dataBytes=%u dbq=%u headerBytes=0 payload=silence", + kCycles, + dataPackets, + emptyPackets, + frames, + kDataPacketBytes, + kDbq); + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F FF800 OHCI metadata/skip self-test passed cycles=%u tag=0 speed=S400 tcode=0x%x dataLength=%u skipCycles=%u descriptor=OUTPUT_LAST/standard/zero/Z4 commits=%u", + kCycles, + ASFW::IsochTransport::kIsochDataBlockTCode, + kDataPacketBytes, + skipCycles, + metadataCommits); + ASFW_LOG(Audio, + "[RME] Stage 5F complete: raw no-CIP packet bytes, OHCI tag/data-length metadata, and true idle skip semantics are verified; OHCI contexts and Core Audio streaming remain disabled"); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F wire self-test mismatch data=%u/%u empty=%u/%u frames=%u/%u geometry=%u zero=%u", + dataPackets, + kExpectedDataPackets, + emptyPackets, + kExpectedEmptyPackets, + frames, + kExpectedFrames, + geometryValid ? 1U : 0U, + payloadZero ? 1U : 0U); + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F OHCI metadata/skip mismatch valid=%u descriptor=%u skip=%u/%u commits=%u/%u", + ohciMetadataValid ? 1U : 0U, + skipDescriptorValid ? 1U : 0U, + skipCycles, + kExpectedEmptyPackets, + metadataCommits, + kCycles); + } +} + +void RMEFireface800Protocol::BestEffortStopDeviceCommunication() noexcept { + if (!deviceTxAllocationRequested_ && !deviceTxAllocated_) { + return; + } + + const std::array stopLE = {0x00U, 0x00U, 0x00U, 0x80U}; + const auto generation = busInfo_.GetGeneration(); + const auto node = FW::NodeId{static_cast(nodeId_ & 0x3FU)}; + + deviceTxAllocationRequested_ = false; + deviceTxAllocated_ = false; + deviceTxChannel_ = 0xFFU; + + const auto handle = busOps_.WriteBlock( + generation, + node, + MakeAddress(kIsochCommStopAddress), + std::span{stopLE}, + FW::FwSpeed::S400, + [generation](Async::AsyncStatus status, + std::span payload) { + (void)payload; + if (status == Async::AsyncStatus::kSuccess) { + ASFW_LOG(Audio, + "[RME] Stage 5F best-effort stop submitted during shutdown gen=%u", + generation.value); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F best-effort stop failed during shutdown status=%{public}s gen=%u", + Async::ToString(status), + generation.value); + } + }); + + if (!handle.IsValid()) { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F best-effort stop could not be submitted during shutdown"); + } +} + +void RMEFireface800Protocol::ReleaseReservedResources() noexcept { + playbackPreflightRoute_.store(0U, std::memory_order_release); + stage5hInFlight_.store(false, std::memory_order_release); + continuousPlaybackInFlight_.store(false, std::memory_order_release); + if (!playbackResourcesReserved_ || irmClient_ == nullptr) { + return; + } + + const uint8_t channel = reservedPlaybackChannel_; + const uint32_t bandwidthUnits = reservedPlaybackBandwidthUnits_; + playbackResourcesReserved_ = false; + reservedPlaybackChannel_ = 0xFFU; + reservedPlaybackBandwidthUnits_ = 0U; + + irmClient_->ReleaseResources( + channel, + bandwidthUnits, + [channel, bandwidthUnits](IRM::AllocationStatus status) { + if (status == IRM::AllocationStatus::Success) { + ASFW_LOG(Audio, + "[RME] ✅ Stage 5F released playback IRM reservation channel=%u bandwidth=%u", + channel, + bandwidthUnits); + } else { + ASFW_LOG_ERROR(Audio, + "[RME] Stage 5F playback IRM release status=%{public}s channel=%u bandwidth=%u", + IRM::ToString(status), + channel, + bandwidthUnits); + } + }); +} + +void RMEFireface800Protocol::LogStreamCaps(uint32_t rate) const noexcept { + const uint32_t channels = DecodeChannelCount(rate); + ASFW_LOG(Audio, + "[RME] Stream geometry: mode=%{public}s rate=%u captureChannels=%u playbackChannels=%u sampleFormat=PCM24-in-32", + DecodeStreamMode(rate), + rate, + channels, + channels); + ASFW_LOG(Audio, + "[RME] Supported FF800 geometry: low=28x28 mid=20x20 high=12x12 MIDI=1-in/1-out"); +} + +uint32_t RMEFireface800Protocol::ReadLE32(const uint8_t* bytes) noexcept { + return static_cast(bytes[0]) | + (static_cast(bytes[1]) << 8U) | + (static_cast(bytes[2]) << 16U) | + (static_cast(bytes[3]) << 24U); +} + +uint32_t RMEFireface800Protocol::DecodeSampleRate(uint32_t value) noexcept { + switch (value & 0x0000001EU) { + case 0x00000002U: return 32000; + case 0x00000000U: return 44100; + case 0x00000006U: return 48000; + case 0x0000000AU: return 64000; + case 0x00000008U: return 88200; + case 0x0000000EU: return 96000; + case 0x00000012U: return 128000; + case 0x00000010U: return 176400; + case 0x00000016U: return 192000; + default: return 0; + } +} + +const char* RMEFireface800Protocol::DecodeClockSource(uint32_t value) noexcept { + if ((value & 0x00000001U) != 0U) { + return "internal"; + } + + switch (value & 0x00001C00U) { + case 0x00000000U: return "ADAT1"; + case 0x00000400U: return "ADAT2"; + case 0x00000C00U: return "S/PDIF"; + case 0x00001000U: return "word clock"; + case 0x00001800U: return "LTC/TCO"; + default: return "unknown external"; + } +} + +const char* RMEFireface800Protocol::DecodeExternalState(uint32_t value, + uint32_t lockedMask, + uint32_t syncedMask) noexcept { + if ((value & lockedMask) == 0U) { + return "none"; + } + return (value & syncedMask) != 0U ? "sync" : "lock"; +} + +const char* RMEFireface800Protocol::DecodeReferredClockSource(uint32_t status0, + uint32_t status1) noexcept { + if ((status1 & 0x00000001U) != 0U) { + return "internal"; + } + + switch (status0 & 0x01C00000U) { + case 0x00000000U: return "ADAT1"; + case 0x00400000U: return "ADAT2"; + case 0x00C00000U: return "S/PDIF"; + case 0x01000000U: return "word clock"; + case 0x01400000U: return "TCO"; + default: return "none"; + } +} + +uint32_t RMEFireface800Protocol::DecodeReferredRate(uint32_t status0) noexcept { + switch (status0 & 0x1E000000U) { + case 0x02000000U: return 32000; + case 0x04000000U: return 44100; + case 0x06000000U: return 48000; + case 0x08000000U: return 64000; + case 0x0A000000U: return 88200; + case 0x0C000000U: return 96000; + case 0x0E000000U: return 128000; + case 0x10000000U: return 176400; + case 0x12000000U: return 192000; + default: return 0; + } +} + +const char* RMEFireface800Protocol::DecodeStreamMode(uint32_t rate) noexcept { + switch (rate) { + case 32000: + case 44100: + case 48000: + return "low/single-speed"; + case 64000: + case 88200: + case 96000: + return "mid/double-speed"; + case 128000: + case 176400: + case 192000: + return "high/quad-speed"; + default: + return "unknown"; + } +} + +uint32_t RMEFireface800Protocol::DecodeChannelCount(uint32_t rate) noexcept { + switch (rate) { + case 32000: + case 44100: + case 48000: + return 28; + case 64000: + case 88200: + case 96000: + return 20; + case 128000: + case 176400: + case 192000: + return 12; + default: + return 0; + } +} + +uint32_t RMEFireface800Protocol::DecodeSytInterval(uint32_t rate) noexcept { + switch (rate) { + case 32000: + case 44100: + case 48000: + return 8U; + case 64000: + case 88200: + case 96000: + return 16U; + case 128000: + case 176400: + case 192000: + return 32U; + default: + return 0U; + } +} + +uint32_t RMEFireface800Protocol::CalculatePlaybackBandwidthUnits(uint32_t rate) noexcept { + const uint32_t dataBlockQuadlets = DecodeChannelCount(rate); + const uint32_t sytInterval = DecodeSytInterval(rate); + if (dataBlockQuadlets == 0U || sytInterval == 0U) { + return 0U; + } + + // FF800 uses blocking raw PCM with no CIP header. The maximum payload is + // SYT interval * data-block quadlets * 4 bytes. Match the project's + // conservative Linux iso-resources fallback: 12-byte packet overhead, + // S800 scaling, then 512 allocation units of gap-count fallback margin. + const uint32_t maxPayloadBytes = sytInterval * dataBlockQuadlets * 4U; + const uint32_t alignedPayloadBytes = (maxPayloadBytes + 3U) & ~3U; + const uint32_t packetBytesAtS800 = (12U + alignedPayloadBytes + 1U) / 2U; + return packetBytesAtS800 + 512U; +} + +uint8_t RMEFireface800Protocol::SelectAvailableChannel(uint32_t channels31_0, + uint32_t channels63_32) noexcept { + for (uint8_t channel = 0; channel < 64U; ++channel) { + const uint32_t reg = channel < 32U ? channels31_0 : channels63_32; + const uint32_t mask = uint32_t{1} << (31U - (channel % 32U)); + if ((reg & mask) != 0U) { + return channel; + } + } + return 0xFFU; +} + +} // namespace ASFW::Audio::RME diff --git a/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp new file mode 100644 index 00000000..e1b55ae2 --- /dev/null +++ b/ASFWDriver/Audio/Protocols/RME/RMEFireface800Protocol.hpp @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 ASFireWire Project +// +// RMEFireface800Protocol.hpp - Stage 5H bounded circular silent playback integration. + +#pragma once + +#include "../IDeviceProtocol.hpp" +#include "../../../Protocols/Ports/FireWireBusPort.hpp" + +#include +#include + +namespace ASFW::IRM { +class IRMClient; +} + +namespace ASFW::Audio::RME { + +class RMEFireface800Protocol final : public IDeviceProtocol { +public: + RMEFireface800Protocol(Protocols::Ports::FireWireBusOps& busOps, + Protocols::Ports::FireWireBusInfo& busInfo, + uint16_t nodeId, + uint64_t deviceGuid, + ::ASFW::IRM::IRMClient* irmClient) noexcept; + ~RMEFireface800Protocol() override; + + IOReturn Initialize() override; + IOReturn Shutdown() override; + const char* GetName() const override { return "RME Fireface 800 (Stage 5H bounded circular silent playback integration)"; } + + void UpdateRuntimeContext(uint16_t nodeId, + Protocols::AVC::FCPTransport* transport) override; + bool GetPlaybackPreflightRoute(PlaybackPreflightRoute& outRoute) const override; + IOReturn RunBoundedPlaybackIntegrationPreflight( + const PlaybackPreflightRoute& route, + BoundedPlaybackStep prepareHost, + BoundedPlaybackStep runHostBurst, + BoundedPlaybackStep cleanupHost) override; + + IOReturn StartContinuousPlaybackIntegration( + const PlaybackPreflightRoute& route, + BoundedPlaybackStep prepareHost, + BoundedPlaybackStep startHostRing) override; + IOReturn StopContinuousPlaybackIntegration( + BoundedPlaybackStep stopHostRing) override; + IOReturn GetContinuousPlaybackIntegrationHealth( + ContinuousHealthStep pollHealth) override; + +private: + void ProbeClockConfig() noexcept; + void ValidateStfWritePath(uint32_t rate) noexcept; + void VerifyClockAfterStfWrite(uint32_t expectedRate) noexcept; + void ProbeSyncStatus() noexcept; + void ProbeTxIsochChannel() noexcept; + void ReservePlaybackResourcesPreflight(uint32_t rate, uint32_t attempt = 0U) noexcept; + void ProgramRxPacketFormat(uint32_t rate, uint8_t channel, uint32_t bandwidthUnits) noexcept; + void RequestDeviceTxAllocation(uint32_t rate) noexcept; + void PollDeviceTxIsochChannel(uint32_t rate, uint32_t attempt) noexcept; + void StartDeviceCommunicationPreflight(uint32_t rate) noexcept; + void StopDeviceCommunicationPreflight() noexcept; + void RunNoCipWireFormatSelfTest() noexcept; + void BestEffortStopDeviceCommunication() noexcept; + void ReleaseReservedResources() noexcept; + void LogStreamCaps(uint32_t rate) const noexcept; + + // Shared FF800-stop + IRM-release tail used by both the Stage 5H bounded + // burst and the Stage 6 continuous stream. Sends the FF800 isoch-comm + // stop command, clears device-TX-allocation state, then releases the + // held IRM reservation (consuming playbackResourcesReserved_/ + // reservedPlaybackChannel_/reservedPlaybackBandwidthUnits_). Returns + // kIOReturnSuccess only if both steps were accepted. + IOReturn StopDeviceEngineAndReleaseIrm(bool& outStopAccepted, + bool& outReleaseAccepted, + uint8_t& outReleasedChannel, + uint32_t& outReleasedBandwidth) noexcept; + + static uint32_t ReadLE32(const uint8_t* bytes) noexcept; + static uint32_t DecodeSampleRate(uint32_t value) noexcept; + static const char* DecodeClockSource(uint32_t value) noexcept; + static const char* DecodeExternalState(uint32_t value, + uint32_t lockedMask, + uint32_t syncedMask) noexcept; + static const char* DecodeReferredClockSource(uint32_t status0, + uint32_t status1) noexcept; + static uint32_t DecodeReferredRate(uint32_t status0) noexcept; + static const char* DecodeStreamMode(uint32_t rate) noexcept; + static uint32_t DecodeChannelCount(uint32_t rate) noexcept; + static uint32_t DecodeSytInterval(uint32_t rate) noexcept; + static uint32_t CalculatePlaybackBandwidthUnits(uint32_t rate) noexcept; + static uint8_t SelectAvailableChannel(uint32_t channels31_0, + uint32_t channels63_32) noexcept; + + Protocols::Ports::FireWireBusOps& busOps_; + Protocols::Ports::FireWireBusInfo& busInfo_; + uint16_t nodeId_{0}; + uint64_t deviceGuid_{0}; + ::ASFW::IRM::IRMClient* irmClient_{nullptr}; + Async::AsyncHandle probeHandle_{}; + std::atomic active_{false}; + std::atomic playbackPreflightRoute_{0U}; + uint32_t currentRate_{0}; + uint8_t reservedPlaybackChannel_{0xFF}; + uint32_t reservedPlaybackBandwidthUnits_{0}; + bool playbackResourcesReserved_{false}; + bool deviceTxAllocationRequested_{false}; + bool deviceTxAllocated_{false}; + uint8_t deviceTxChannel_{0xFF}; + std::atomic stage5hInFlight_{false}; + + // Stage 6 (dev-only continuous silence): separate in-flight guard from + // stage5hInFlight_ so Stage 5H's existing guard/call sites stay + // untouched. RunBoundedPlaybackIntegrationPreflight and + // StartContinuousPlaybackIntegration each check BOTH flags before + // proceeding -- they drive the same OHCI context/IRM route and must + // never run concurrently. Reuses reservedPlaybackChannel_/ + // reservedPlaybackBandwidthUnits_/playbackResourcesReserved_ above to + // hold state across the Start/Stop call gap (safe: mutually exclusive + // with Stage 5H by the two-flag guard). + std::atomic continuousPlaybackInFlight_{false}; +}; + +} // namespace ASFW::Audio::RME diff --git a/ASFWDriver/Audio/Wire/AMDTP/AmdtpPayloadWriter.cpp b/ASFWDriver/Audio/Wire/AMDTP/AmdtpPayloadWriter.cpp index 3865a672..d5b45625 100644 --- a/ASFWDriver/Audio/Wire/AMDTP/AmdtpPayloadWriter.cpp +++ b/ASFWDriver/Audio/Wire/AMDTP/AmdtpPayloadWriter.cpp @@ -123,7 +123,8 @@ void AmdtpPayloadWriter::WriteFloat32Interleaved( // field set, so this cannot underflow. const uint32_t frameInPacket = static_cast(absoluteFrame - snap.firstAudioFrame); - uint8_t* dest = snap.packetBytes + kCipHeaderBytes + + const uint32_t headerBytes = streamConfig_.includeCipHeader ? kCipHeaderBytes : 0U; + uint8_t* dest = snap.packetBytes + headerBytes + frameInPacket * snap.dbs * kBytesPerSlot; const uint32_t pcmSlots = (streamConfig_.pcmChannels < snap.dbs) diff --git a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp index 1ebe88dc..8194df3a 100644 --- a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp +++ b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.cpp @@ -67,8 +67,9 @@ bool AmdtpTxPacketizer::Configure(const AmdtpStreamConfig& streamConfig, } const uint32_t dataPacketBytes = - kCipHeaderBytes + static_cast(config.framesPerDataPacket) * - config.dbs * kBytesPerSlot; + (config.includeCipHeader ? kCipHeaderBytes : 0U) + + static_cast(config.framesPerDataPacket) * + config.dbs * kBytesPerSlot; if (dataPacketBytes > config.maxPacketBytes) { return false; } @@ -156,10 +157,13 @@ bool AmdtpTxPacketizer::PrepareNextPacket(TxPacketSlotView slot, const uint32_t payloadBytes = static_cast(frames) * streamConfig_.dbs * kBytesPerSlot; - const bool isEmptyPacket = !isData && txPolicy_.emptyPacketsDuringIdle; + // CIP_NO_HEADER streams have no representation for a header-only no-data + // packet; their idle cycles are genuine zero-length packets. + const bool isEmptyPacket = + !isData && (txPolicy_.emptyPacketsDuringIdle || !streamConfig_.includeCipHeader); const uint32_t byteCount = - isEmptyPacket ? 0 : (isData ? (kCipHeaderBytes + payloadBytes) : kCipHeaderBytes); + isEmptyPacket ? 0 : (isData ? (HeaderBytes() + payloadBytes) : HeaderBytes()); if (slot.capacityBytes < byteCount) { return false; // no state advanced; caller may retry @@ -181,7 +185,9 @@ bool AmdtpTxPacketizer::PrepareNextPacket(TxPacketSlotView slot, ? timing.nextDataSyt : IEC61883::SytFormatter::kNoInfo; - WriteCipHeader(slot.bytes, cipBuilder_.BuildData(dbc, outPacket.syt)); + if (streamConfig_.includeCipHeader) { + WriteCipHeader(slot.bytes, cipBuilder_.BuildData(dbc, outPacket.syt)); + } WriteDataPacketDefaults(slot.bytes, slot.capacityBytes, payloadBytes); if (!timeline_->ExposeDataPacket(outPacket, slot.bytes, @@ -199,6 +205,8 @@ bool AmdtpTxPacketizer::PrepareNextPacket(TxPacketSlotView slot, timeline_->MarkNoDataPacket(slot.packetIndex); } else { // CIP-header-only: no payload, even as padding (DICE-II rejects it). + // This branch is unreachable for CIP_NO_HEADER streams because + // those force isEmptyPacket above. WriteCipHeader(slot.bytes, cipBuilder_.BuildNoData(dbc)); timeline_->MarkNoDataPacket(slot.packetIndex); // DBC deliberately not advanced. @@ -226,7 +234,7 @@ void AmdtpTxPacketizer::WriteDataPacketDefaults(uint8_t* packetBytes, uint32_t payloadBytes) noexcept { (void)packetCapacityBytes; // capacity validated by the caller - uint8_t* payload = packetBytes + kCipHeaderBytes; + uint8_t* payload = packetBytes + HeaderBytes(); if (txPolicy_.clearPayloadBeforeExposure) { for (uint32_t i = 0; i < payloadBytes; ++i) { @@ -253,8 +261,12 @@ void AmdtpTxPacketizer::WriteCipHeader( WriteBE32(packetBytes + 4, header.q1); } +uint32_t AmdtpTxPacketizer::HeaderBytes() const noexcept { + return streamConfig_.includeCipHeader ? kCipHeaderBytes : 0U; +} + uint32_t AmdtpTxPacketizer::DataPacketBytes() const noexcept { - return kCipHeaderBytes + PayloadBytes(); + return HeaderBytes() + PayloadBytes(); } uint32_t AmdtpTxPacketizer::PayloadBytes() const noexcept { diff --git a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.hpp b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.hpp index 0936beca..26762b41 100644 --- a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.hpp +++ b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTxPacketizer.hpp @@ -55,6 +55,7 @@ class AmdtpTxPacketizer final { void WriteCipHeader(uint8_t* packetBytes, const IEC61883::CipHeaderWords& header) noexcept; + [[nodiscard]] uint32_t HeaderBytes() const noexcept; [[nodiscard]] uint32_t DataPacketBytes() const noexcept; [[nodiscard]] uint32_t PayloadBytes() const noexcept; diff --git a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTypes.hpp b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTypes.hpp index beca2ee9..3b662376 100644 --- a/ASFWDriver/Audio/Wire/AMDTP/AmdtpTypes.hpp +++ b/ASFWDriver/Audio/Wire/AMDTP/AmdtpTypes.hpp @@ -35,6 +35,12 @@ struct AmdtpStreamConfig final { uint8_t framesPerDataPacket{8}; uint32_t maxPacketBytes{512}; + // Most IEC 61883-6/AMDTP streams carry the standard 8-byte CIP header. + // RME Fireface former-generation streams use CIP_NO_HEADER: the isoch + // payload is raw PCM data blocks and no-data cycles are genuine empty + // packets. Keep the default true so every existing device is unchanged. + bool includeCipHeader{true}; + // First host buffer channel this stream encodes. For a multi-stream device // (Venice F32 = 2×16) the 32-ch host output buffer is split across streams: // stream 0 reads channels [0, pcmChannels), stream 1 reads [16, 16+pcmChannels), diff --git a/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp b/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp index 4a6ed946..c4566f51 100644 --- a/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp +++ b/ASFWDriver/DeviceProfiles/Audio/AudioDeviceIds.hpp @@ -56,7 +56,13 @@ inline constexpr uint32_t kStudioLive1642ModelId = 0x000010; inline constexpr uint32_t kStudioLive2442ModelId = 0x000012; inline constexpr uint32_t kStudioLive3242ModelId = 0x000014; +// ---- RME (vendor-specific Fireface family) ---- +inline constexpr uint32_t kRMEVendorId = 0x000a35; +inline constexpr uint32_t kFireface800ModelId = 0x101800; + // ---- Display names ---- +inline constexpr const char* kRMEVendorName = "RME"; +inline constexpr const char* kFireface800ModelName = "Fireface 800"; inline constexpr const char* kFocusriteVendorName = "Focusrite"; inline constexpr const char* kSPro40ModelName = "Saffire Pro 40"; inline constexpr const char* kLiquidS56ModelName = "Liquid Saffire 56"; diff --git a/ASFWDriver/DeviceProfiles/Audio/AudioProfileRegistry.hpp b/ASFWDriver/DeviceProfiles/Audio/AudioProfileRegistry.hpp index 2f8b1ee3..04f44290 100644 --- a/ASFWDriver/DeviceProfiles/Audio/AudioProfileRegistry.hpp +++ b/ASFWDriver/DeviceProfiles/Audio/AudioProfileRegistry.hpp @@ -19,6 +19,7 @@ #include "Vendors/FocusriteAudioProfiles.hpp" #include "Vendors/MidasAudioProfiles.hpp" #include "Vendors/PreSonusAudioProfiles.hpp" +#include "Vendors/RMEAudioProfiles.hpp" #include "Vendors/TerraTecAudioProfiles.hpp" #include @@ -36,6 +37,7 @@ class AudioProfileRegistry final { if (auto hint = Alesis::LookupIdentity(query)) { return hint; } if (auto hint = Midas::LookupIdentity(query)) { return hint; } if (auto hint = PreSonus::LookupIdentity(query)) { return hint; } + if (auto hint = RME::LookupIdentity(query)) { return hint; } if (auto hint = TerraTec::LookupIdentity(query)) { return hint; } if (auto hint = Focusrite::LookupIdentityByGuid(query)) { return hint; } return std::nullopt; @@ -51,6 +53,7 @@ class AudioProfileRegistry final { if (auto hint = Alesis::LookupAudioProfile(query)) { return hint; } if (auto hint = Midas::LookupAudioProfile(query)) { return hint; } if (auto hint = PreSonus::LookupAudioProfile(query)) { return hint; } + if (auto hint = RME::LookupAudioProfile(query)) { return hint; } if (auto hint = TerraTec::LookupAudioProfile(query)) { return hint; } return std::nullopt; } diff --git a/ASFWDriver/DeviceProfiles/Audio/AudioProfileTypes.hpp b/ASFWDriver/DeviceProfiles/Audio/AudioProfileTypes.hpp index f594802d..c8cd911d 100644 --- a/ASFWDriver/DeviceProfiles/Audio/AudioProfileTypes.hpp +++ b/ASFWDriver/DeviceProfiles/Audio/AudioProfileTypes.hpp @@ -21,6 +21,7 @@ enum class AudioIntegrationMode : uint8_t { kNone = 0, // Recognized but not driven (deferred multistream models). kHardcodedNub, // Vendor-specific audio backend (DICE/TCAT), no AV/C. kAVCDriven, // AV/C discovery drives topology; vendor protocol adds extra controls. + kReadOnlyNub, // Publish a Core Audio endpoint, but deliberately block streaming. }; /// Audio protocol family a device belongs to. Forward-looking: consumed today only for diff --git a/ASFWDriver/DeviceProfiles/Audio/Vendors/RMEAudioProfiles.hpp b/ASFWDriver/DeviceProfiles/Audio/Vendors/RMEAudioProfiles.hpp new file mode 100644 index 00000000..5b8a4cbf --- /dev/null +++ b/ASFWDriver/DeviceProfiles/Audio/Vendors/RMEAudioProfiles.hpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 ASFireWire Project +// +// RMEAudioProfiles.hpp - RME FireWire audio device identity and staged support policy. +// +// Stage 4B recognizes the Fireface 800, publishes a Core Audio endpoint, verifies STF, +// reserves the host-to-device IRM route, and programs RX packet geometry. Streaming +// remains deliberately blocked: no device TX allocation or isoch start is performed. + +#pragma once + +#include "../../Common/DeviceProfileTypes.hpp" +#include "../AudioDeviceIds.hpp" +#include "../AudioProfileTypes.hpp" + +#include + +namespace ASFW::DeviceProfiles::Audio::RME { + +[[nodiscard]] constexpr std::optional +LookupIdentity(const DeviceProfileQuery& query) noexcept { + if (query.vendorId != kRMEVendorId || query.modelId != kFireface800ModelId) { + return std::nullopt; + } + + return DeviceIdentityHint{.vendorId = query.vendorId, + .modelId = query.modelId, + .vendorName = kRMEVendorName, + .modelName = kFireface800ModelName, + .source = MatchSource::VendorModel}; +} + +[[nodiscard]] constexpr std::optional +LookupAudioProfile(const DeviceProfileQuery& query) noexcept { + if (query.vendorId != kRMEVendorId || query.modelId != kFireface800ModelId) { + return std::nullopt; + } + + // Publish a Core Audio endpoint with the verified 192 kHz / 12x12 geometry, + // but keep transport disabled until the RME-specific isoch backend is implemented. + return AudioProfileHint{.family = AudioProtocolFamily::VendorSpecific, + .mode = AudioIntegrationMode::kReadOnlyNub, + .source = MatchSource::VendorModel}; +} + +} // namespace ASFW::DeviceProfiles::Audio::RME diff --git a/ASFWDriver/Discovery/DeviceManager.cpp b/ASFWDriver/Discovery/DeviceManager.cpp index baae1ae9..3f5ff458 100644 --- a/ASFWDriver/Discovery/DeviceManager.cpp +++ b/ASFWDriver/Discovery/DeviceManager.cpp @@ -553,6 +553,11 @@ std::shared_ptr DeviceManager::ResumeExistingDevice(const std::shared_ return nullptr; } + // A rediscovery may enrich identity after the initial minimal ROM pass (for + // example, model_id found in a Unit Directory). Refresh even when the device + // is already Ready so the UserClient and protocol selection see the new IDs. + device->RefreshIdentity(record); + if (device->IsSuspended()) { device->Resume(record.gen, record.nodeId, record.link); UpdateOperationalIndex(record.guid, record.gen, record.nodeId, "update"); diff --git a/ASFWDriver/Discovery/DeviceRegistry.cpp b/ASFWDriver/Discovery/DeviceRegistry.cpp index 9d0450d2..318f26d4 100644 --- a/ASFWDriver/Discovery/DeviceRegistry.cpp +++ b/ASFWDriver/Discovery/DeviceRegistry.cpp @@ -3,6 +3,9 @@ #include #include "../Logging/Logging.hpp" #include "../DeviceProfiles/Audio/AudioProfileRegistry.hpp" +#include "../ConfigROM/Parse/ConfigROMParser.hpp" + +#include namespace ASFW::Discovery { @@ -17,6 +20,39 @@ constexpr uint32_t kUnitSwVersion_SBP2 = 0x010483; // SBP-2 Unit_Sw_Version namespace { +[[nodiscard]] std::optional FindUnitModelIdInRawROM(const ConfigROM& rom) { + if (rom.rawQuadlets.empty()) { + return std::nullopt; + } + + const uint32_t rootDirStart = 1U + static_cast(rom.bib.busInfoLength); + for (const auto& rootEntry : rom.rootDirMinimal) { + if (rootEntry.key != CfgKey::Unit_Directory || rootEntry.leafOffsetQuadlets == 0) { + continue; + } + + const uint32_t absoluteOffset = rootDirStart + rootEntry.leafOffsetQuadlets; + if (absoluteOffset >= rom.rawQuadlets.size()) { + continue; + } + + const auto raw = std::span(rom.rawQuadlets.data(), rom.rawQuadlets.size()); + const auto parsed = ConfigROMParser::ParseDirectory(raw.subspan(absoluteOffset), 32); + if (!parsed) { + continue; + } + + for (const auto& entry : *parsed) { + if (entry.keyType == ASFW::FW::EntryType::kImmediate && + entry.keyId == ASFW::FW::ConfigKey::kModelId) { + return entry.value; + } + } + } + + return std::nullopt; +} + void PopulateDeviceIdentity(DeviceRecord& device, const ConfigROM& rom) { for (const auto& entry : rom.rootDirMinimal) { if (entry.key == CfgKey::VendorId) { @@ -35,13 +71,37 @@ void PopulateDeviceIdentity(DeviceRecord& device, const ConfigROM& rom) { if (unit.unitSwVersion != 0) { device.unitSwVersion = unit.unitSwVersion; } - if (device.unitSpecId.has_value() && device.unitSwVersion.has_value()) { - break; + + // Some devices, including the RME Fireface 800, publish model_id in the + // Unit Directory instead of the Root Directory. Preserve a root-level + // model_id when present, otherwise use the first usable unit model_id. + if (device.modelId == 0 && unit.modelId.has_value()) { + device.modelId = *unit.modelId; + } + } + + // Details discovery can complete with the raw Unit Directory present while the + // higher-level UnitDirectory metadata is still incomplete. Parse the raw unit + // directory as a final, generic fallback so immediate model_id entries are not lost. + if (device.modelId == 0) { + if (const auto rawModelId = FindUnitModelIdInRawROM(rom); rawModelId.has_value()) { + device.modelId = *rawModelId; + ASFW_LOG(Discovery, + "Recovered unit-directory model_id from raw Config ROM: 0x%06x", + device.modelId); } } device.vendorName = rom.vendorName; device.modelName = rom.modelName; + if (device.modelName.empty()) { + for (const auto& unit : rom.unitDirectories) { + if (unit.modelName.has_value() && !unit.modelName->empty()) { + device.modelName = *unit.modelName; + break; + } + } + } } void MaybeInferKnownIdentityFromGuid(DeviceRecord& device, Guid64 guid) { @@ -137,7 +197,8 @@ DeviceRecord& DeviceRegistry::UpsertFromROM(const ConfigROM& rom, const LinkPoli device.vendorId, device.modelId, static_cast(integrationMode)); - if (integrationMode == DeviceProfiles::Audio::AudioIntegrationMode::kHardcodedNub) { + if (integrationMode == DeviceProfiles::Audio::AudioIntegrationMode::kHardcodedNub || + integrationMode == DeviceProfiles::Audio::AudioIntegrationMode::kReadOnlyNub) { device.kind = DeviceKind::VendorSpecificAudio; device.isAudioCandidate = true; } else { diff --git a/ASFWDriver/Discovery/FWDevice.cpp b/ASFWDriver/Discovery/FWDevice.cpp index 541a6fa5..169b7c7c 100644 --- a/ASFWDriver/Discovery/FWDevice.cpp +++ b/ASFWDriver/Discovery/FWDevice.cpp @@ -116,6 +116,11 @@ std::vector FWDevice::ExtractUnitDirectory( entries.push_back(RomEntry{CfgKey::Unit_Sw_Version, value, keyType, 0}); } break; + case 0x17: // Model_ID + if (keyType == 0) { // Immediate + entries.push_back(RomEntry{CfgKey::ModelId, value, keyType, 0}); + } + break; case 0x14: // Logical_Unit_Number if (keyType == 0) { // Immediate entries.push_back(RomEntry{CfgKey::Logical_Unit_Number, value, keyType, 0}); @@ -222,6 +227,31 @@ void FWDevice::Resume(Generation newGen, uint16_t newNodeId, const LinkPolicy& n } } +void FWDevice::RefreshIdentity(const DeviceRecord& record) +{ + if (record.guid != guid_) { + return; + } + + const uint32_t previousVendorId = vendorId_; + const uint32_t previousModelId = modelId_; + + vendorId_ = record.vendorId; + modelId_ = record.modelId; + kind_ = record.kind; + vendorName_ = record.vendorName; + modelName_ = record.modelName; + isAudioCandidate_ = record.isAudioCandidate; + supportsAMDTP_ = record.supportsAMDTP; + + if (previousVendorId != vendorId_ || previousModelId != modelId_) { + ASFW_LOG(Discovery, + "FWDevice identity refreshed GUID=0x%016llx vendor=0x%06x->0x%06x " + "model=0x%06x->0x%06x", + guid_, previousVendorId, vendorId_, previousModelId, modelId_); + } +} + void FWDevice::Terminate() { if (state_ == State::Terminated) { diff --git a/ASFWDriver/Discovery/FWDevice.hpp b/ASFWDriver/Discovery/FWDevice.hpp index 05aad4b0..88017aa4 100644 --- a/ASFWDriver/Discovery/FWDevice.hpp +++ b/ASFWDriver/Discovery/FWDevice.hpp @@ -56,6 +56,7 @@ class FWDevice : public std::enable_shared_from_this { void Publish(); void Suspend(); void Resume(Generation newGen, uint16_t newNodeId, const LinkPolicy& newLink); + void RefreshIdentity(const DeviceRecord& record); void Terminate(); private: @@ -69,9 +70,9 @@ class FWDevice : public std::enable_shared_from_this { ) const; const Guid64 guid_; - const uint32_t vendorId_; - const uint32_t modelId_; - const DeviceKind kind_; + uint32_t vendorId_; + uint32_t modelId_; + DeviceKind kind_; std::string vendorName_; std::string modelName_; diff --git a/ASFWDriver/Info.plist b/ASFWDriver/Info.plist index d8d388bf..12b6fa36 100644 --- a/ASFWDriver/Info.plist +++ b/ASFWDriver/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 42 + 17 IOKitPersonalities ASFWAudioDriverService @@ -51,7 +51,7 @@ ASFWAudioNub ASFWAutoStartAudioStreams - + ASFWControllerVerbosity 1 ASFWDICEVerbosity @@ -87,7 +87,7 @@ IOClass IOUserService IOPCIMatch - 0x590111c1 + 0x823f104c IOPCITunnelCompatible IOProbeScore @@ -113,7 +113,7 @@ IOMatchCategory ASFWSCSIController IOPCIMatch - 0x590111c1 + 0x823f104c IOPCITunnelCompatible IOProviderClass diff --git a/ASFWDriver/Isoch/IsochService.cpp b/ASFWDriver/Isoch/IsochService.cpp index 804d8cb0..9f1b6083 100644 --- a/ASFWDriver/Isoch/IsochService.cpp +++ b/ASFWDriver/Isoch/IsochService.cpp @@ -403,6 +403,135 @@ kern_return_t IsochService::PrepareTransmitStream(uint32_t streamIndex, uint8_t return kIOReturnSuccess; } +kern_return_t IsochService::PrimePreparedTransmitForPreflight() { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5E priming prepared IT before inert CommandPtr and all-skip RUN tests"); + return isochTransmitContext_->PrimeForPreflight(); +} + +kern_return_t IsochService::ProgramPreparedTransmitCommandPtrForPreflight() { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5E programming inert IT CommandPtr with RUN clear"); + return isochTransmitContext_->ProgramCommandPtrForPreflight(); +} + +kern_return_t IsochService::RunPreparedTransmitAllSkipForPreflight( + uint32_t durationMs) { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5E running prepared IT with 48 skip-only descriptors for %u ms", + durationMs); + return isochTransmitContext_->RunAllSkipForPreflight(durationMs); +} + +kern_return_t IsochService::RunPreparedTransmitSingleSilencePacketForPreflight( + uint32_t timeoutMs) { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5F running one finite silent no-CIP packet; completion timeout=%u ms", + timeoutMs); + return isochTransmitContext_->RunSingleSilencePacketForPreflight(timeoutMs); +} + +kern_return_t IsochService::PrepareTransmitFiniteSilenceCadenceForPreflight() { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5G preparing finite 48-cycle D-D-D-S silent cadence with RUN clear"); + return isochTransmitContext_->PrepareFiniteSilenceCadenceForPreflight(); +} + +kern_return_t +IsochService::RunPreparedTransmitFiniteSilenceCadenceForPreflight( + uint32_t timeoutMs) { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5G executing prepared finite silent cadence; completion timeout=%u ms", + timeoutMs); + return isochTransmitContext_->RunPreparedFiniteSilenceCadenceForPreflight( + timeoutMs); +} + +kern_return_t +IsochService::CleanupPreparedTransmitFiniteSilenceCadenceForPreflight() { + if (!isochTransmitContext_) { + return kIOReturnSuccess; + } + return isochTransmitContext_->CleanupFiniteSilenceCadenceForPreflight(); +} + +kern_return_t +IsochService::PrepareTransmitBoundedCircularSilenceCadenceForPreflight() { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5H preparing immutable circular 48-cycle D-D-D-S silence ring with RUN clear"); + return isochTransmitContext_ + ->PrepareBoundedCircularSilenceCadenceForPreflight(); +} + +kern_return_t +IsochService::RunPreparedTransmitBoundedCircularSilenceCadenceForPreflight( + uint32_t durationMs) { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: Stage 5H executing bounded circular silence ring durationMs=%u interrupts=0 refill=0", + durationMs); + return isochTransmitContext_ + ->RunPreparedBoundedCircularSilenceCadenceForPreflight(durationMs); +} + +kern_return_t +IsochService::CleanupPreparedTransmitBoundedCircularSilenceCadenceForPreflight() { + if (!isochTransmitContext_) { + return kIOReturnSuccess; + } + return isochTransmitContext_ + ->CleanupBoundedCircularSilenceCadenceForPreflight(); +} + +kern_return_t +IsochService::StartTransmitContinuousCircularSilenceCadence() { + if (!isochTransmitContext_) { + return kIOReturnNotReady; + } + ASFW_LOG(Isoch, + "IsochService: starting continuous circular silence ring (dev) interrupts=0 refill=0"); + return isochTransmitContext_->StartContinuousCircularSilenceCadence(); +} + +IsochTransmitContext::ContinuousCadenceHealth +IsochService::PollTransmitContinuousCircularSilenceCadenceHealth() { + if (!isochTransmitContext_) { + return {}; + } + return isochTransmitContext_ + ->PollContinuousCircularSilenceCadenceHealth(); +} + +kern_return_t IsochService::StopTransmitContinuousCircularSilenceCadence() { + if (!isochTransmitContext_) { + return kIOReturnSuccess; + } + return isochTransmitContext_->StopContinuousCircularSilenceCadence(); +} + kern_return_t IsochService::StartPreparedTransmit() { if (!isochTransmitContext_) { return kIOReturnNotReady; diff --git a/ASFWDriver/Isoch/IsochService.hpp b/ASFWDriver/Isoch/IsochService.hpp index cd7e823c..5f9ae11c 100644 --- a/ASFWDriver/Isoch/IsochService.hpp +++ b/ASFWDriver/Isoch/IsochService.hpp @@ -89,6 +89,25 @@ class IsochService { kern_return_t PrepareTransmitStream(uint32_t streamIndex, uint8_t channel, HardwareInterface& hardware, uint8_t sid); kern_return_t StartPreparedTransmit(); + kern_return_t PrimePreparedTransmitForPreflight(); + kern_return_t ProgramPreparedTransmitCommandPtrForPreflight(); + kern_return_t RunPreparedTransmitAllSkipForPreflight(uint32_t durationMs); + kern_return_t RunPreparedTransmitSingleSilencePacketForPreflight(uint32_t timeoutMs); + kern_return_t PrepareTransmitFiniteSilenceCadenceForPreflight(); + kern_return_t RunPreparedTransmitFiniteSilenceCadenceForPreflight( + uint32_t timeoutMs); + kern_return_t CleanupPreparedTransmitFiniteSilenceCadenceForPreflight(); + kern_return_t PrepareTransmitBoundedCircularSilenceCadenceForPreflight(); + kern_return_t RunPreparedTransmitBoundedCircularSilenceCadenceForPreflight( + uint32_t durationMs); + kern_return_t CleanupPreparedTransmitBoundedCircularSilenceCadenceForPreflight(); + + // Stage 6 (dev-only continuous silence): same immutable ring as above, + // run indefinitely instead of for a bounded duration. + kern_return_t StartTransmitContinuousCircularSilenceCadence(); + ASFW::Isoch::IsochTransmitContext::ContinuousCadenceHealth + PollTransmitContinuousCircularSilenceCadenceHealth(); + kern_return_t StopTransmitContinuousCircularSilenceCadence(); kern_return_t StopTransmit(); diff --git a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp index 78db2f9c..b5b4dbbc 100644 --- a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp +++ b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.cpp @@ -10,6 +10,7 @@ #include "../../Logging/LogConfig.hpp" #include "../../Common/TimingUtils.hpp" +#include #include #include @@ -58,6 +59,10 @@ kern_return_t IsochTransmitContext::Configure(uint8_t channel, uint8_t sid) noex channel_ = channel; ring_.SetChannel(channel_); + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + boundedCircularCadencePrepared_ = false; + boundedCircularCadenceProgram_ = {}; if (dmaMemory_) { // Allocate-once policy @@ -98,6 +103,10 @@ kern_return_t IsochTransmitContext::SetSharedMemoryDescriptors( payloadDmaCmd_ = nullptr; } payloadDmaMap_.Reset(); + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + boundedCircularCadencePrepared_ = false; + boundedCircularCadenceProgram_ = {}; // 1. Map Payload Slab IOMemoryMap* pMap = nullptr; @@ -249,6 +258,1184 @@ kern_return_t IsochTransmitContext::SetSharedMemoryDescriptors( return kIOReturnSuccess; } +kern_return_t IsochTransmitContext::PrimeForPreflight() noexcept { + if (state_ != State::Configured && state_ != State::Stopped) { + ASFW_LOG(Isoch, + "IT: Stage 5E descriptor preflight rejected - state=%{public}s", + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || !ring_.HasRings()) { + ASFW_LOG(Isoch, "IT: Stage 5E descriptor preflight missing hardware/ring"); + return kIOReturnNoResources; + } + if (!payloadBase_ || !payloadDmaMap_.IsValid() || !metadataRing_ || !controlBlock_ || + controlBlock_->numSlots == 0 || controlBlock_->slotStrideBytes == 0 || + controlBlock_->maxPacketBytes == 0) { + ASFW_LOG(Isoch, + "IT: Stage 5E descriptor preflight shared TX contract incomplete"); + return kIOReturnNotReady; + } + + ring_.ResetForStart(); + ring_.SeedCycleTracking(*hardware_); + + const uint64_t exposeCursor = + controlBlock_->exposeCursor.load(std::memory_order_acquire); + const auto primeStats = ring_.Prime(payloadDmaMap_, + controlBlock_->numSlots, + controlBlock_->slotStrideBytes, + metadataRing_, + exposeCursor); + if (primeStats.packetsAssembled != Tx::Layout::kNumPackets) { + ASFW_LOG(Isoch, + "IT: Stage 5E descriptor preflight prime failed assembled=%llu expected=%u expose=%llu", + primeStats.packetsAssembled, + Tx::Layout::kNumPackets, + exposeCursor); + return kIOReturnInternalError; + } + + constexpr uint32_t kExpectedDataPackets = 36U; + constexpr uint32_t kExpectedSkipPackets = 12U; + constexpr uint32_t kExpectedDataBytes = 1536U; + + uint32_t dataPackets = 0; + uint32_t skipPackets = 0; + bool metadataValid = true; + bool payloadShapeValid = true; + for (uint32_t packet = 0; packet < Tx::Layout::kNumPackets; ++packet) { + const uint32_t slot = packet % controlBlock_->numSlots; + const auto& meta = metadataRing_[slot]; + const uint32_t q0 = OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const auto tag = ASFW::IsochTransport::DecodeIsochTxHeaderTag(q0); + const bool skip = ASFW::IsochTransport::ShouldSkipIsochTxPacket( + tag, meta.payloadLength); + if (skip) { + ++skipPackets; + payloadShapeValid = payloadShapeValid && meta.payloadLength == 0U; + } else { + ++dataPackets; + payloadShapeValid = payloadShapeValid && + meta.payloadLength == kExpectedDataBytes; + } + if (tag != ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(q0) != + ASFW::IsochTransport::kIsochSpeedS400 || + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(q0) != + ASFW::IsochTransport::kIsochDataBlockTCode || + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength( + OSSwapLittleToHostInt32(meta.immediateHeader[1])) != + meta.payloadLength) { + metadataValid = false; + break; + } + } + + const bool cadenceValid = + dataPackets == kExpectedDataPackets && + skipPackets == kExpectedSkipPackets; + const uint64_t descriptorIOVA = ring_.Slab().DescriptorRegion().deviceBase; + if (!metadataValid || !payloadShapeValid || !cadenceValid || + descriptorIOVA == 0 || descriptorIOVA > 0xFFFFFFFFULL) { + ASFW_LOG(Isoch, + "IT: Stage 5E descriptor preflight validation failed metadata=%u payloadShape=%u cadence=%u data=%u/%u skip=%u/%u descriptorIOVA=0x%llx", + metadataValid ? 1U : 0U, + payloadShapeValid ? 1U : 0U, + cadenceValid ? 1U : 0U, + dataPackets, + kExpectedDataPackets, + skipPackets, + kExpectedSkipPackets, + descriptorIOVA); + return kIOReturnInternalError; + } + + packetsAssembled_ = primeStats.packetsAssembled; + ASFW_LOG(Isoch, + "IT: ✅ Stage 5E FF800 descriptor ring primed packets=%u data=%u skip=%u descriptorIOVA=0x%08x expose=%llu noCommandPtr=1 noRun=1", + Tx::Layout::kNumPackets, + dataPackets, + skipPackets, + static_cast(descriptorIOVA), + exposeCursor); + ring_.DumpDescriptorRing(0, 8); + return kIOReturnSuccess; +} + +kern_return_t IsochTransmitContext::ProgramCommandPtrForPreflight() noexcept { + if (state_ != State::Configured && state_ != State::Stopped) { + ASFW_LOG(Isoch, + "IT: Stage 5E CommandPtr preflight rejected - state=%{public}s", + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || !ring_.HasRings()) { + ASFW_LOG(Isoch, "IT: Stage 5E CommandPtr preflight missing hardware/ring"); + return kIOReturnNoResources; + } + + const uint64_t descriptorIOVA = ring_.Slab().DescriptorRegion().deviceBase; + if (descriptorIOVA == 0 || descriptorIOVA > 0xFFFFFFFFULL) { + ASFW_LOG(Isoch, + "IT: Stage 5E invalid descriptor IOVA 0x%llx", + descriptorIOVA); + return kIOReturnInternalError; + } + + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + // Make the safety invariant explicit before touching CommandPtr. No IT + // interrupt is enabled and RUN is forced clear; a CommandPtr by itself is + // inert and cannot launch DMA. + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + hardware_->Write(Register32::kIsoXmitIntMaskClear, (1u << contextIndex_)); + + const uint32_t expectedCmd = + static_cast(descriptorIOVA) | Tx::Layout::kBlocksPerPacket; + hardware_->Write(cmdPtrReg, expectedCmd); + + // The readbacks also flush posted MMIO writes. + const uint32_t readCmd = hardware_->Read(cmdPtrReg); + const uint32_t readCtl = hardware_->Read(ctrlReg); + const bool commandMatches = readCmd == expectedCmd; + const bool runClear = (readCtl & Driver::ContextControl::kRun) == 0; + const bool activeClear = (readCtl & Driver::ContextControl::kActive) == 0; + const bool deadClear = (readCtl & Driver::ContextControl::kDead) == 0; + + if (!commandMatches || !runClear || !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5E CommandPtr validation failed expected=0x%08x actual=0x%08x control=0x%08x runClear=%u activeClear=%u deadClear=%u", + expectedCmd, + readCmd, + readCtl, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return kIOReturnInternalError; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5E inert CommandPtr programmed expected=0x%08x actual=0x%08x descriptorIOVA=0x%08x Z=%u control=0x%08x noRun=1 noInterrupt=1 noPacket=1", + expectedCmd, + readCmd, + static_cast(descriptorIOVA), + Tx::Layout::kBlocksPerPacket, + readCtl); + return kIOReturnSuccess; +} + +kern_return_t IsochTransmitContext::RunAllSkipForPreflight( + uint32_t durationMs) noexcept { + if (state_ != State::Configured && state_ != State::Stopped) { + ASFW_LOG(Isoch, + "IT: Stage 5E finite all-skip completion rejected - state=%{public}s", + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || !ring_.HasRings()) { + ASFW_LOG(Isoch, + "IT: Stage 5E finite all-skip completion missing hardware/ring"); + return kIOReturnNoResources; + } + if (durationMs == 0U || durationMs > 20U) { + ASFW_LOG(Isoch, + "IT: Stage 5E finite all-skip completion invalid timeout=%u ms", + durationMs); + return kIOReturnBadArgument; + } + if (!ring_.ProgramAllSkipPacketsForRunPreflight()) { + ASFW_LOG(Isoch, + "IT: Stage 5E could not construct/publish finite 48-skip descriptor chain"); + return kIOReturnInternalError; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5E finite all-skip descriptor chain published descriptors=48 terminalBranch=0 bytes=%zu dmaPublish=1", + ring_.Slab().DescriptorRegion().size); + + const uint64_t descriptorIOVA = ring_.Slab().DescriptorRegion().deviceBase; + if (descriptorIOVA == 0U || descriptorIOVA > 0xFFFFFFFFULL) { + ASFW_LOG(Isoch, + "IT: Stage 5E invalid descriptor IOVA 0x%llx", + descriptorIOVA); + return kIOReturnInternalError; + } + + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlSetReg = static_cast( + DMAContextHelpers::IsoXmitContextControlSet(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kWritableBits); + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + + // The finite skip program begins with one descriptor, not the four + // physical slots reserved for a normal FF800 packet program. + const uint32_t expectedCmd = + static_cast(descriptorIOVA) | 1U; + hardware_->Write(cmdPtrReg, expectedCmd); + const uint32_t commandBeforeRun = hardware_->Read(cmdPtrReg); + const uint32_t controlBeforeRun = hardware_->Read(ctrlReg); + if (commandBeforeRun != expectedCmd || + (controlBeforeRun & Driver::ContextControl::kRun) != 0U || + (controlBeforeRun & Driver::ContextControl::kActive) != 0U || + (controlBeforeRun & Driver::ContextControl::kDead) != 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5E finite pre-RUN validation failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x", + expectedCmd, + commandBeforeRun, + controlBeforeRun); + return kIOReturnInternalError; + } + + hardware_->Write(ctrlSetReg, Driver::ContextControl::kRun); + + const uint32_t maxPolls = durationMs * 100U; // 10 us per poll. + bool activeObserved = false; + uint32_t completionPolls = 0U; + uint32_t controlAfterRun = hardware_->Read(ctrlReg); + auto completion = ring_.FetchAllSkipCompletionForRunPreflight(); + for (; + completionPolls < maxPolls && + completion.completedDescriptors < Tx::Layout::kNumPackets && + (controlAfterRun & Driver::ContextControl::kDead) == 0U; + ++completionPolls) { + activeObserved = activeObserved || + ((controlAfterRun & Driver::ContextControl::kActive) != 0U); + IODelay(10U); + controlAfterRun = hardware_->Read(ctrlReg); + completion = ring_.FetchAllSkipCompletionForRunPreflight(); + } + activeObserved = activeObserved || + ((controlAfterRun & Driver::ContextControl::kActive) != 0U); + + const bool deadSet = + (controlAfterRun & Driver::ContextControl::kDead) != 0U; + const bool completionProved = + completion.completedDescriptors == Tx::Layout::kNumPackets && + completion.lastTransferStatus != 0U; + if (deadSet || !completionProved) { + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + (void)hardware_->Read(ctrlReg); + ASFW_LOG(Isoch, + "IT: Stage 5E finite all-skip completion failed control=0x%08x dead=%u completed=%u/48 lastStatus=0x%04x lastTimestamp=0x%04x activeObserved=%u polls=%u", + controlAfterRun, + deadSet ? 1U : 0U, + completion.completedDescriptors, + completion.lastTransferStatus, + completion.lastTimestamp, + activeObserved ? 1U : 0U, + completionPolls); + return deadSet ? kIOReturnNotPermitted : kIOReturnTimeout; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5E finite all-skip DMA completion proved cmd=0x%08x control=0x%08x completed=48/48 lastStatus=0x%04x lastTimestamp=0x%04x activeObserved=%u dormantAccepted=%u polls=%u noInterrupt=1 noPacketDescriptor=1", + commandBeforeRun, + controlAfterRun, + completion.lastTransferStatus, + completion.lastTimestamp, + activeObserved ? 1U : 0U, + (controlAfterRun & Driver::ContextControl::kActive) == 0U ? 1U : 0U, + completionPolls); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + uint32_t controlStopped = hardware_->Read(ctrlReg); + for (uint32_t poll = 0; + poll < 1000U && + (controlStopped & Driver::ContextControl::kActive) != 0U; + ++poll) { + IODelay(10U); + controlStopped = hardware_->Read(ctrlReg); + } + + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + + const bool runClear = + (controlStopped & Driver::ContextControl::kRun) == 0U; + const bool activeClear = + (controlStopped & Driver::ContextControl::kActive) == 0U; + const bool deadClear = + (controlStopped & Driver::ContextControl::kDead) == 0U; + if (!runClear || !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5E finite all-skip stop validation failed control=0x%08x runClear=%u activeClear=%u deadClear=%u", + controlStopped, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return kIOReturnTimeout; + } + + state_ = State::Stopped; + ASFW_LOG(Isoch, + "IT: ✅ Stage 5E finite all-skip context stopped cleanly control=0x%08x runClear=1 activeClear=1 deadClear=1 completed=48 busPacketDescriptors=0", + controlStopped); + return kIOReturnSuccess; +} + +kern_return_t IsochTransmitContext::RunSingleSilencePacketForPreflight( + const uint32_t timeoutMs) noexcept { + if (state_ != State::Configured && state_ != State::Stopped) { + ASFW_LOG(Isoch, + "IT: Stage 5F finite silent packet rejected - state=%{public}s", + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || !ring_.HasRings() || !payloadBase_ || + !metadataRing_ || !controlBlock_) { + ASFW_LOG(Isoch, + "IT: Stage 5F finite silent packet missing hardware/ring/shared memory"); + return kIOReturnNoResources; + } + if (timeoutMs == 0U || timeoutMs > 20U) { + return kIOReturnBadArgument; + } + + Tx::IsochTxDmaRing::SingleSilencePacketProgram program{}; + if (!ring_.ProgramSingleSilencePacketForRunPreflight( + payloadBase_, + controlBlock_->numSlots, + controlBlock_->slotStrideBytes, + metadataRing_, + program)) { + ASFW_LOG(Isoch, + "IT: Stage 5F could not construct verified finite silent packet program"); + return kIOReturnInternalError; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5F finite silent packet published packet=%u producerSlot=%u cmd=0x%08x channel=%u payloadBytes=%u tag=0 noCIP=1 allZero=1 terminalBranch=0 dmaPublish=1", + program.packetSlot, + program.producerSlot, + program.commandPtr, + channel_, + program.payloadLength); + + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlSetReg = static_cast( + DMAContextHelpers::IsoXmitContextControlSet(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kWritableBits); + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + hardware_->Write(cmdPtrReg, program.commandPtr); + + const uint32_t commandBeforeRun = hardware_->Read(cmdPtrReg); + const uint32_t controlBeforeRun = hardware_->Read(ctrlReg); + if (commandBeforeRun != program.commandPtr || + (controlBeforeRun & Driver::ContextControl::kRun) != 0U || + (controlBeforeRun & Driver::ContextControl::kActive) != 0U || + (controlBeforeRun & Driver::ContextControl::kDead) != 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5F finite silent packet pre-RUN validation failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x", + program.commandPtr, + commandBeforeRun, + controlBeforeRun); + return kIOReturnInternalError; + } + + hardware_->Write(ctrlSetReg, Driver::ContextControl::kRun); + + const uint32_t maxPolls = timeoutMs * 100U; // 10 us per poll. + uint32_t polls = 0U; + uint32_t controlAfterRun = hardware_->Read(ctrlReg); + auto completion = + ring_.FetchSingleSilencePacketCompletionForRunPreflight( + program.packetSlot); + while (polls < maxPolls && completion.transferStatus == 0U && + (controlAfterRun & Driver::ContextControl::kDead) == 0U) { + IODelay(10U); + ++polls; + controlAfterRun = hardware_->Read(ctrlReg); + completion = + ring_.FetchSingleSilencePacketCompletionForRunPreflight( + program.packetSlot); + } + + const bool dead = + (controlAfterRun & Driver::ContextControl::kDead) != 0U; + const uint8_t eventCode = + static_cast(completion.transferStatus & 0x1FU); + const bool ackComplete = eventCode == 0x11U; + if (dead || completion.transferStatus == 0U || !ackComplete) { + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + (void)hardware_->Read(ctrlReg); + ASFW_LOG(Isoch, + "IT: Stage 5F finite silent packet completion failed control=0x%08x dead=%u xferStatus=0x%04x event=0x%02x timestamp=0x%04x polls=%u", + controlAfterRun, + dead ? 1U : 0U, + completion.transferStatus, + eventCode, + completion.timestamp, + polls); + return dead ? kIOReturnNotPermitted : kIOReturnTimeout; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5F finite silent packet DMA/transmit completed cmd=0x%08x control=0x%08x xferStatus=0x%04x event=ack_complete timestamp=0x%04x packet=%u channel=%u payloadBytes=%u actualBusPackets=1 payload=silence deviceEngineStopped=1 interrupts=0", + commandBeforeRun, + controlAfterRun, + completion.transferStatus, + completion.timestamp, + program.packetSlot, + channel_, + program.payloadLength); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + uint32_t controlStopped = hardware_->Read(ctrlReg); + for (uint32_t poll = 0U; + poll < 1000U && + (controlStopped & Driver::ContextControl::kActive) != 0U; + ++poll) { + IODelay(10U); + controlStopped = hardware_->Read(ctrlReg); + } + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + + const bool runClear = + (controlStopped & Driver::ContextControl::kRun) == 0U; + const bool activeClear = + (controlStopped & Driver::ContextControl::kActive) == 0U; + const bool deadClear = + (controlStopped & Driver::ContextControl::kDead) == 0U; + if (!runClear || !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5F finite silent packet stop validation failed control=0x%08x runClear=%u activeClear=%u deadClear=%u", + controlStopped, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return kIOReturnTimeout; + } + + state_ = State::Stopped; + ASFW_LOG(Isoch, + "IT: ✅ Stage 5F finite silent packet context stopped cleanly control=0x%08x runClear=1 activeClear=1 deadClear=1 actualBusPackets=1", + controlStopped); + return kIOReturnSuccess; +} + +kern_return_t IsochTransmitContext::PrepareFiniteSilenceCadenceForPreflight() + noexcept { + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + + if (state_ != State::Configured && state_ != State::Stopped) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence preparation rejected - state=%{public}s", + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || !ring_.HasRings()) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence preparation missing hardware/ring"); + return kIOReturnNoResources; + } + if (!payloadBase_ || !metadataRing_ || !controlBlock_ || + controlBlock_->numSlots == 0U || + controlBlock_->slotStrideBytes == 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence preparation shared TX contract incomplete"); + return kIOReturnNotReady; + } + + Tx::IsochTxDmaRing::FiniteSilenceCadenceProgram program{}; + if (!ring_.ProgramFiniteSilenceCadenceForRunPreflight( + payloadBase_, + controlBlock_->numSlots, + controlBlock_->slotStrideBytes, + metadataRing_, + program)) { + ASFW_LOG(Isoch, + "IT: Stage 5G could not construct verified finite D-D-D-S silence cadence"); + return kIOReturnInternalError; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5G descriptor chain published descriptors=%u dataPackets=%u skip=%u payloadBytes=%u pattern=DDD-S terminalBranch=0 dmaPublish=1", + program.descriptorCount, + program.dataPacketCount, + program.skipPacketCount, + program.payloadLength); + + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kWritableBits); + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + hardware_->Write(cmdPtrReg, program.commandPtr); + + const uint32_t commandReadback = hardware_->Read(cmdPtrReg); + const uint32_t controlReadback = hardware_->Read(ctrlReg); + const bool runClear = + (controlReadback & Driver::ContextControl::kRun) == 0U; + const bool activeClear = + (controlReadback & Driver::ContextControl::kActive) == 0U; + const bool deadClear = + (controlReadback & Driver::ContextControl::kDead) == 0U; + if (commandReadback != program.commandPtr || !runClear || + !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence pre-RUN validation failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x runClear=%u activeClear=%u deadClear=%u", + program.commandPtr, + commandReadback, + controlReadback, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return kIOReturnInternalError; + } + + finiteCadenceProgram_ = program; + finiteCadencePrepared_ = true; + return kIOReturnSuccess; +} + +kern_return_t +IsochTransmitContext::RunPreparedFiniteSilenceCadenceForPreflight( + const uint32_t timeoutMs) noexcept { + if (!finiteCadencePrepared_ || + (state_ != State::Configured && state_ != State::Stopped)) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence run rejected prepared=%u state=%{public}s", + finiteCadencePrepared_ ? 1U : 0U, + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || timeoutMs == 0U || timeoutMs > 50U) { + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + return hardware_ ? kIOReturnBadArgument : kIOReturnNoResources; + } + + const auto program = finiteCadenceProgram_; + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlSetReg = static_cast( + DMAContextHelpers::IsoXmitContextControlSet(contextIndex_)); + + const uint32_t commandBeforeRun = hardware_->Read(cmdPtrReg); + const uint32_t controlBeforeRun = hardware_->Read(ctrlReg); + if (commandBeforeRun != program.commandPtr || + (controlBeforeRun & Driver::ContextControl::kRun) != 0U || + (controlBeforeRun & Driver::ContextControl::kActive) != 0U || + (controlBeforeRun & Driver::ContextControl::kDead) != 0U) { + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence RUN gate failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x", + program.commandPtr, + commandBeforeRun, + controlBeforeRun); + return kIOReturnInternalError; + } + + hardware_->Write(ctrlSetReg, Driver::ContextControl::kRun); + + const uint32_t maxPolls = timeoutMs * 100U; // 10 us per poll. + uint32_t polls = 0U; + bool deadObserved = false; + bool activeObserved = false; + uint32_t controlAfterRun = hardware_->Read(ctrlReg); + auto completion = + ring_.FetchFiniteSilenceCadenceCompletionForRunPreflight( + program.startPacketSlot); + while (polls < maxPolls && + completion.completedDescriptors < program.descriptorCount) { + deadObserved = deadObserved || + (controlAfterRun & Driver::ContextControl::kDead) != 0U; + activeObserved = activeObserved || + (controlAfterRun & Driver::ContextControl::kActive) != 0U; + if (deadObserved) { + break; + } + IODelay(10U); + ++polls; + controlAfterRun = hardware_->Read(ctrlReg); + completion = + ring_.FetchFiniteSilenceCadenceCompletionForRunPreflight( + program.startPacketSlot); + } + deadObserved = deadObserved || + (controlAfterRun & Driver::ContextControl::kDead) != 0U; + activeObserved = activeObserved || + (controlAfterRun & Driver::ContextControl::kActive) != 0U; + + const bool completionValid = + !deadObserved && + completion.completedDescriptors == program.descriptorCount && + completion.completedDataDescriptors == program.dataPacketCount && + completion.actualBusPackets == program.dataPacketCount && + completion.eventErrors == 0U && + completion.cadenceValid; + + if (completionValid) { + ASFW_LOG(Isoch, + "IT: ✅ Stage 5G finite cadence burst completed descriptors=%u/%u dataCompleted=%u/%u actualBusPackets=%u eventErrors=0 deadObserved=0 activeObserved=%u polls=%u", + completion.completedDescriptors, + program.descriptorCount, + completion.completedDataDescriptors, + program.dataPacketCount, + completion.actualBusPackets, + activeObserved ? 1U : 0U, + polls); + ASFW_LOG(Isoch, + "IT: ✅ Stage 5G cadence verification passed timestampPattern=1,1,2"); + } else { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence burst failed control=0x%08x descriptors=%u/%u dataCompleted=%u/%u actualBusPackets=%u eventErrors=%u deadObserved=%u cadenceValid=%u lastStatus=0x%04x lastTimestamp=0x%04x polls=%u", + controlAfterRun, + completion.completedDescriptors, + program.descriptorCount, + completion.completedDataDescriptors, + program.dataPacketCount, + completion.actualBusPackets, + completion.eventErrors, + deadObserved ? 1U : 0U, + completion.cadenceValid ? 1U : 0U, + completion.lastTransferStatus, + completion.lastTimestamp, + polls); + } + + const kern_return_t cleanupKr = + CleanupFiniteSilenceCadenceForPreflight(); + if (cleanupKr != kIOReturnSuccess) { + return cleanupKr; + } + if (!completionValid) { + return deadObserved ? kIOReturnNotPermitted : kIOReturnTimeout; + } + return kIOReturnSuccess; +} + +kern_return_t +IsochTransmitContext::CleanupFiniteSilenceCadenceForPreflight() noexcept { + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + + if (!hardware_) { + if (state_ != State::Unconfigured) { + state_ = State::Stopped; + } + return kIOReturnNoResources; + } + + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + uint32_t controlStopped = hardware_->Read(ctrlReg); + for (uint32_t poll = 0U; + poll < 1000U && + (controlStopped & Driver::ContextControl::kActive) != 0U; + ++poll) { + IODelay(10U); + controlStopped = hardware_->Read(ctrlReg); + } + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + + const bool runClear = + (controlStopped & Driver::ContextControl::kRun) == 0U; + const bool activeClear = + (controlStopped & Driver::ContextControl::kActive) == 0U; + const bool deadClear = + (controlStopped & Driver::ContextControl::kDead) == 0U; + if (state_ != State::Unconfigured) { + state_ = State::Stopped; + } + refillInProgress_.clear(std::memory_order_release); + + if (!runClear || !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite cadence stop validation failed control=0x%08x runClear=%u activeClear=%u deadClear=%u", + controlStopped, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return deadClear ? kIOReturnTimeout : kIOReturnNotPermitted; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5G transmit context stopped cleanly control=0x%08x runClear=1 activeClear=1 deadClear=1", + controlStopped); + return kIOReturnSuccess; +} + +kern_return_t +IsochTransmitContext::PrepareBoundedCircularSilenceCadenceForPreflight() + noexcept { + boundedCircularCadencePrepared_ = false; + boundedCircularCadenceProgram_ = {}; + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + + if (state_ != State::Configured && state_ != State::Stopped) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular cadence preparation rejected state=%{public}s", + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || !ring_.HasRings()) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular cadence preparation missing hardware/ring"); + return kIOReturnNoResources; + } + if (!payloadBase_ || !metadataRing_ || !controlBlock_ || + controlBlock_->numSlots == 0U || + controlBlock_->slotStrideBytes == 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular cadence shared TX contract incomplete"); + return kIOReturnNotReady; + } + + Tx::IsochTxDmaRing::BoundedCircularSilenceCadenceProgram program{}; + if (!ring_.ProgramBoundedCircularSilenceCadenceForPreflight( + payloadBase_, + controlBlock_->numSlots, + controlBlock_->slotStrideBytes, + metadataRing_, + program)) { + ASFW_LOG(Isoch, + "IT: Stage 5H could not construct verified circular D-D-D-S silence cadence"); + return kIOReturnInternalError; + } + + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kWritableBits); + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + hardware_->Write(cmdPtrReg, program.commandPtr); + + const uint32_t commandReadback = hardware_->Read(cmdPtrReg); + const uint32_t controlReadback = hardware_->Read(ctrlReg); + const bool runClear = + (controlReadback & Driver::ContextControl::kRun) == 0U; + const bool activeClear = + (controlReadback & Driver::ContextControl::kActive) == 0U; + const bool deadClear = + (controlReadback & Driver::ContextControl::kDead) == 0U; + if (commandReadback != program.commandPtr || !runClear || + !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular cadence pre-RUN validation failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x runClear=%u activeClear=%u deadClear=%u", + program.commandPtr, + commandReadback, + controlReadback, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return kIOReturnInternalError; + } + + boundedCircularCadenceProgram_ = program; + boundedCircularCadencePrepared_ = true; + ASFW_LOG(Isoch, + "IT: ✅ Stage 5H circular cadence armed with RUN clear cmd=0x%08x startSlot=%u durationPending=1", + program.commandPtr, + program.startPacketSlot); + return kIOReturnSuccess; +} + +kern_return_t +IsochTransmitContext::RunPreparedBoundedCircularSilenceCadenceForPreflight( + const uint32_t durationMs) noexcept { + if (!boundedCircularCadencePrepared_ || + (state_ != State::Configured && state_ != State::Stopped)) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular cadence run rejected prepared=%u state=%{public}s", + boundedCircularCadencePrepared_ ? 1U : 0U, + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_ || durationMs < 50U || durationMs > 250U) { + return hardware_ ? kIOReturnBadArgument : kIOReturnNoResources; + } + + const auto program = boundedCircularCadenceProgram_; + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlSetReg = static_cast( + DMAContextHelpers::IsoXmitContextControlSet(contextIndex_)); + + const uint32_t commandBeforeRun = hardware_->Read(cmdPtrReg); + const uint32_t controlBeforeRun = hardware_->Read(ctrlReg); + if (commandBeforeRun != program.commandPtr || + (controlBeforeRun & Driver::ContextControl::kRun) != 0U || + (controlBeforeRun & Driver::ContextControl::kActive) != 0U || + (controlBeforeRun & Driver::ContextControl::kDead) != 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular cadence RUN gate failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x", + program.commandPtr, + commandBeforeRun, + controlBeforeRun); + return kIOReturnInternalError; + } + + hardware_->Write(ctrlSetReg, Driver::ContextControl::kRun); + + constexpr uint32_t kPollDelayUs = 250U; + const uint32_t maxPolls = (durationMs * 1000U) / kPollDelayUs; + uint32_t polls = 0U; + bool deadObserved = false; + bool activeObserved = false; + uint32_t consecutiveInactiveAfterActive = 0U; + uint32_t maxInactiveAfterActive = 0U; + uint32_t anchorExecutions = 0U; + uint32_t anchorEventErrors = 0U; + uint16_t previousAnchorTimestamp = 0U; + bool anchorSeen = false; + + for (; polls < maxPolls; ++polls) { + const uint32_t control = hardware_->Read(ctrlReg); + const bool active = + (control & Driver::ContextControl::kActive) != 0U; + const bool dead = + (control & Driver::ContextControl::kDead) != 0U; + deadObserved = deadObserved || dead; + if (active) { + activeObserved = true; + consecutiveInactiveAfterActive = 0U; + } else if (activeObserved) { + ++consecutiveInactiveAfterActive; + maxInactiveAfterActive = std::max( + maxInactiveAfterActive, consecutiveInactiveAfterActive); + } + + const auto anchor = + ring_.FetchCadenceAnchorCompletionForPreflight( + program.startPacketSlot); + if (anchor.transferStatus != 0U) { + const uint8_t eventCode = static_cast( + anchor.transferStatus & 0x1FU); + if (eventCode != 0x11U) { + ++anchorEventErrors; + } else if (!anchorSeen) { + anchorSeen = true; + previousAnchorTimestamp = anchor.timestamp; + anchorExecutions = 1U; + } else if (anchor.timestamp != previousAnchorTimestamp) { + previousAnchorTimestamp = anchor.timestamp; + ++anchorExecutions; + } + } + + if (dead) { + break; + } + IODelay(kPollDelayUs); + } + + // Stop the immutable ring synchronously before the protocol sends the + // FF800 stop command. The cleanup callback remains idempotent and will + // repeat this gate on every partial-failure path. + const kern_return_t stopStatus = + CleanupBoundedCircularSilenceCadenceForPreflight(); + const auto completion = + ring_.FetchFiniteSilenceCadenceCompletionForRunPreflight( + program.startPacketSlot); + + const uint32_t completedSweeps = + anchorExecutions > 0U ? anchorExecutions - 1U : 0U; + const uint32_t minimumBusPackets = + completedSweeps * program.dataPacketCount; + const uint32_t nominalSweeps = durationMs / 6U; + const uint32_t minimumRequiredSweeps = + std::max(4U, nominalSweeps / 2U); + + // Tolerate at most three consecutive inactive samples (750 us) to avoid + // treating a single asynchronous register read as a lost circular run. + const bool activeContinuous = maxInactiveAfterActive < 4U; + const bool repeatedRunValid = + stopStatus == kIOReturnSuccess && + !deadObserved && + activeObserved && + activeContinuous && + anchorEventErrors == 0U && + completedSweeps >= minimumRequiredSweeps && + completion.completedDescriptors == program.descriptorCount && + completion.completedDataDescriptors == program.dataPacketCount && + completion.actualBusPackets == program.dataPacketCount && + completion.eventErrors == 0U; + + if (!repeatedRunValid) { + ASFW_LOG(Isoch, + "IT: Stage 5H bounded circular run failed durationMs=%u controlPolls=%u activeObserved=%u maxInactivePolls=%u deadObserved=%u anchorExecutions=%u completedSweeps=%u/%u minimumBusPackets=%u anchorEventErrors=%u descriptors=%u/%u dataCompleted=%u/%u eventErrors=%u stopKr=0x%x", + durationMs, + polls, + activeObserved ? 1U : 0U, + maxInactiveAfterActive, + deadObserved ? 1U : 0U, + anchorExecutions, + completedSweeps, + minimumRequiredSweeps, + minimumBusPackets, + anchorEventErrors, + completion.completedDescriptors, + program.descriptorCount, + completion.completedDataDescriptors, + program.dataPacketCount, + completion.eventErrors, + stopStatus); + if (stopStatus != kIOReturnSuccess) { + return stopStatus; + } + return deadObserved ? kIOReturnNotPermitted : kIOReturnTimeout; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5H bounded circular silence sustained durationMs=%u anchorExecutions=%u completedSweeps=%u minimumBusPackets=%u activeContinuous=1 eventErrors=0 deadObserved=0 interrupts=0 refill=0", + durationMs, + anchorExecutions, + completedSweeps, + minimumBusPackets); + return kIOReturnSuccess; +} + +kern_return_t +IsochTransmitContext::CleanupBoundedCircularSilenceCadenceForPreflight() + noexcept { + boundedCircularCadencePrepared_ = false; + boundedCircularCadenceProgram_ = {}; + + if (!hardware_) { + if (state_ != State::Unconfigured) { + state_ = State::Stopped; + } + return kIOReturnNoResources; + } + + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlClrReg = static_cast( + DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); + + hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); + uint32_t controlStopped = hardware_->Read(ctrlReg); + for (uint32_t poll = 0U; + poll < 2000U && + (controlStopped & Driver::ContextControl::kActive) != 0U; + ++poll) { + IODelay(10U); + controlStopped = hardware_->Read(ctrlReg); + } + hardware_->Write(Register32::kIsoXmitIntMaskClear, + (1U << contextIndex_)); + hardware_->Write(Register32::kIsoXmitIntEventClear, + (1U << contextIndex_)); + + const bool runClear = + (controlStopped & Driver::ContextControl::kRun) == 0U; + const bool activeClear = + (controlStopped & Driver::ContextControl::kActive) == 0U; + const bool deadClear = + (controlStopped & Driver::ContextControl::kDead) == 0U; + if (state_ != State::Unconfigured) { + state_ = State::Stopped; + } + refillInProgress_.clear(std::memory_order_release); + + if (!runClear || !activeClear || !deadClear) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular context stop validation failed control=0x%08x runClear=%u activeClear=%u deadClear=%u", + controlStopped, + runClear ? 1U : 0U, + activeClear ? 1U : 0U, + deadClear ? 1U : 0U); + return deadClear ? kIOReturnTimeout : kIOReturnNotPermitted; + } + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5H circular transmit context stopped cleanly control=0x%08x runClear=1 activeClear=1 deadClear=1", + controlStopped); + return kIOReturnSuccess; +} + +kern_return_t +IsochTransmitContext::StartContinuousCircularSilenceCadence() noexcept { + if (!boundedCircularCadencePrepared_ || + (state_ != State::Configured && state_ != State::Stopped)) { + ASFW_LOG(Isoch, + "IT: Continuous cadence start rejected prepared=%u state=%{public}s", + boundedCircularCadencePrepared_ ? 1U : 0U, + TxStateName(state_)); + return kIOReturnNotReady; + } + if (!hardware_) { + return kIOReturnNoResources; + } + + const auto program = boundedCircularCadenceProgram_; + const Register32 cmdPtrReg = static_cast( + DMAContextHelpers::IsoXmitCommandPtr(contextIndex_)); + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const Register32 ctrlSetReg = static_cast( + DMAContextHelpers::IsoXmitContextControlSet(contextIndex_)); + + const uint32_t commandBeforeRun = hardware_->Read(cmdPtrReg); + const uint32_t controlBeforeRun = hardware_->Read(ctrlReg); + if (commandBeforeRun != program.commandPtr || + (controlBeforeRun & Driver::ContextControl::kRun) != 0U || + (controlBeforeRun & Driver::ContextControl::kActive) != 0U || + (controlBeforeRun & Driver::ContextControl::kDead) != 0U) { + ASFW_LOG(Isoch, + "IT: Continuous cadence RUN gate failed expectedCmd=0x%08x actualCmd=0x%08x control=0x%08x", + program.commandPtr, + commandBeforeRun, + controlBeforeRun); + return kIOReturnInternalError; + } + + continuousAnchorSeen_ = false; + continuousAnchorExecutions_ = 0U; + continuousPreviousAnchorTimestamp_ = 0U; + + hardware_->Write(ctrlSetReg, Driver::ContextControl::kRun); + + // One settle delay + single readback/anchor-fetch to confirm the ring + // actually started -- not a sustain-and-verify loop like Stage 5H's run + // method. The hardware keeps looping on its own from here. + IODelay(250U); + const uint32_t controlAfterRun = hardware_->Read(ctrlReg); + const bool active = + (controlAfterRun & Driver::ContextControl::kActive) != 0U; + const bool dead = + (controlAfterRun & Driver::ContextControl::kDead) != 0U; + const auto anchor = ring_.FetchCadenceAnchorCompletionForPreflight( + program.startPacketSlot); + if (anchor.transferStatus != 0U) { + const uint8_t eventCode = + static_cast(anchor.transferStatus & 0x1FU); + if (eventCode == 0x11U) { + continuousAnchorSeen_ = true; + continuousAnchorExecutions_ = 1U; + continuousPreviousAnchorTimestamp_ = anchor.timestamp; + } + } + + if (dead || !active) { + ASFW_LOG(Isoch, + "IT: Continuous cadence start failed to sustain control=0x%08x active=%u dead=%u", + controlAfterRun, + active ? 1U : 0U, + dead ? 1U : 0U); + (void)CleanupBoundedCircularSilenceCadenceForPreflight(); + return dead ? kIOReturnNotPermitted : kIOReturnTimeout; + } + + continuousCadenceRunning_ = true; + ASFW_LOG(Isoch, + "IT: ✅ Continuous circular silence cadence started channel/descriptors=%u dataPackets=%u skip=%u anchorConfirmed=%u", + program.descriptorCount, + program.dataPacketCount, + program.skipPacketCount, + continuousAnchorSeen_ ? 1U : 0U); + return kIOReturnSuccess; +} + +IsochTransmitContext::ContinuousCadenceHealth +IsochTransmitContext::PollContinuousCircularSilenceCadenceHealth() noexcept { + ContinuousCadenceHealth health{}; + if (!continuousCadenceRunning_ || !hardware_) { + return health; + } + health.running = true; + + const auto program = boundedCircularCadenceProgram_; + const Register32 ctrlReg = static_cast( + DMAContextHelpers::IsoXmitContextControl(contextIndex_)); + const uint32_t control = hardware_->Read(ctrlReg); + health.dead = (control & Driver::ContextControl::kDead) != 0U; + + const auto anchor = ring_.FetchCadenceAnchorCompletionForPreflight( + program.startPacketSlot); + if (anchor.transferStatus != 0U) { + const uint8_t eventCode = + static_cast(anchor.transferStatus & 0x1FU); + if (eventCode != 0x11U) { + health.eventError = true; + } else if (!continuousAnchorSeen_) { + continuousAnchorSeen_ = true; + continuousAnchorExecutions_ = 1U; + continuousPreviousAnchorTimestamp_ = anchor.timestamp; + } else if (anchor.timestamp != continuousPreviousAnchorTimestamp_) { + continuousPreviousAnchorTimestamp_ = anchor.timestamp; + ++continuousAnchorExecutions_; + } + } + + health.anchorExecutions = continuousAnchorExecutions_; + health.lastAnchorTimestamp = continuousPreviousAnchorTimestamp_; + return health; +} + +kern_return_t +IsochTransmitContext::StopContinuousCircularSilenceCadence() noexcept { + const bool wasRunning = continuousCadenceRunning_; + continuousCadenceRunning_ = false; + const kern_return_t status = + CleanupBoundedCircularSilenceCadenceForPreflight(); + if (wasRunning) { + ASFW_LOG(Isoch, + "IT: Continuous circular silence cadence stopped anchorExecutions=%u stopKr=0x%x", + continuousAnchorExecutions_, + status); + } + return status; +} + kern_return_t IsochTransmitContext::Start() noexcept { if (state_ != State::Configured && state_ != State::Stopped) { ASFW_LOG(Isoch, "IT: Start rejected - state=%{public}s", TxStateName(state_)); @@ -348,6 +1535,19 @@ kern_return_t IsochTransmitContext::Start() noexcept { } void IsochTransmitContext::Stop() noexcept { + // Safety net: a Stage 6 continuous cadence deliberately never enters + // State::Running (see StartContinuousCircularSilenceCadence), so the + // RUN-clear block below would otherwise never fire for it. Without this, + // a driver teardown/unload while a continuous stream is active would + // leave the OHCI IT context spinning in RUN state. + if (continuousCadenceRunning_) { + (void)StopContinuousCircularSilenceCadence(); + } + + finiteCadencePrepared_ = false; + finiteCadenceProgram_ = {}; + boundedCircularCadencePrepared_ = false; + boundedCircularCadenceProgram_ = {}; if (state_ == State::Running && hardware_) { Register32 ctrlClrReg = static_cast(DMAContextHelpers::IsoXmitContextControlClear(contextIndex_)); hardware_->Write(ctrlClrReg, Driver::ContextControl::kRun); diff --git a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp index 52b0ff08..50591b3d 100644 --- a/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp +++ b/ASFWDriver/Isoch/Transmit/IsochTransmitContext.hpp @@ -87,6 +87,89 @@ class IsochTransmitContext final { uint32_t ztsPeriodFrames) noexcept; kern_return_t Start() noexcept; + + // Stage 5E descriptor safety seam: build and validate the complete OHCI IT descriptor + // program against the already-prefilled shared packet ring, but do not + // write CommandPtr, set RUN, enable interrupts, or transmit a bus packet. + kern_return_t PrimeForPreflight() noexcept; + + // Stage 5E safety seam: program and read back the inert OHCI CommandPtr for + // the already-validated descriptor ring. RUN and interrupts remain clear, + // so the controller cannot fetch or transmit a packet. + kern_return_t ProgramCommandPtrForPreflight() noexcept; + + // Stage 5E: replace the validated packet ring with a finite chain of 48 + // true skip descriptors, start the OHCI IT context with interrupts masked, + // and prove execution from descriptor completion status. ACTIVE is sampled + // only for diagnostics because a completed finite program normally becomes + // dormant with RUN=1, ACTIVE=0 and DEAD=0. No bus packet descriptor exists. + kern_return_t RunAllSkipForPreflight(uint32_t durationMs) noexcept; + + // Stage 5F: reduce the already-validated FF800 ring to one finite, silent, + // no-CIP packet, run it with interrupts masked, require ack_complete, and + // stop the context. This emits exactly one isochronous packet on the + // protocol-held IRM channel; the FF800 communication engine remains stopped. + kern_return_t RunSingleSilencePacketForPreflight(uint32_t timeoutMs) noexcept; + + // Stage 5G: prepare one finite 48-cycle D-D-D-S silent cadence while RUN + // is clear, publish all payload/descriptor DMA state, and program/read back + // CommandPtr. The caller starts the FF800 engine only after this succeeds. + kern_return_t PrepareFiniteSilenceCadenceForPreflight() noexcept; + + // Stage 5G: execute the previously prepared finite cadence once with + // interrupts masked, validate descriptor writeback and 1,1,2 cycle timing, + // then stop the OHCI context cleanly. No descriptor refill or live stream. + kern_return_t RunPreparedFiniteSilenceCadenceForPreflight( + uint32_t timeoutMs) noexcept; + + // Stage 5G unconditional teardown seam. Clear RUN even when preparation or + // device start failed before the normal RUN path, wait boundedly for ACTIVE + // to drain, verify DEAD is clear, and discard all finite-cadence state. + kern_return_t CleanupFiniteSilenceCadenceForPreflight() noexcept; + + // Stage 5H: prepare the same verified 48-cycle D-D-D-S silence cadence as + // a circular immutable ring. RUN, interrupts, and refill remain disabled + // until the FF800 engine has been started by the protocol layer. + kern_return_t PrepareBoundedCircularSilenceCadenceForPreflight() noexcept; + + // Stage 5H: run the immutable circular ring for a strictly bounded window, + // prove repeated wraps from anchor-descriptor timestamp changes, reject + // DEAD/ACTIVE loss, and stop synchronously. + kern_return_t RunPreparedBoundedCircularSilenceCadenceForPreflight( + uint32_t durationMs) noexcept; + + // Stage 5H unconditional teardown seam used on success and every partial + // failure before the protocol stops the device and releases IRM. + kern_return_t CleanupBoundedCircularSilenceCadenceForPreflight() noexcept; + + // Stage 6 (dev-only continuous silence): reuses the exact same immutable + // self-looping ring PrepareBoundedCircularSilenceCadenceForPreflight() + // already built for Stage 5H -- that ring has no notion of "bounded", the + // duration limit lives entirely in the Stage 5H run/poll loop below. This + // sets RUN once, confirms one anchor execution, and returns immediately; + // the hardware keeps looping the ring with zero interrupts/software + // involvement until StopContinuousCircularSilenceCadence() is called. + kern_return_t StartContinuousCircularSilenceCadence() noexcept; + + struct ContinuousCadenceHealth final { + bool running{false}; + bool dead{false}; + bool eventError{false}; + uint32_t anchorExecutions{0U}; + uint16_t lastAnchorTimestamp{0U}; + }; + + // Non-blocking single-shot status read (one register read + one anchor + // fetch, no IODelay/poll loop) -- safe to call from a periodic watchdog + // tick. Returns a default (running=false) snapshot when no continuous + // cadence is active. + [[nodiscard]] ContinuousCadenceHealth + PollContinuousCircularSilenceCadenceHealth() noexcept; + + // Stops the ring via the same CleanupBoundedCircularSilenceCadenceForPreflight() + // teardown Stage 5H already uses (RUN clear, ACTIVE-drain poll, DEAD check). + kern_return_t StopContinuousCircularSilenceCadence() noexcept; + void Stop() noexcept; void Poll() noexcept; @@ -146,6 +229,22 @@ class IsochTransmitContext final { Tx::TxPayloadDmaMap payloadDmaMap_{}; OSSharedPtr payloadDmaCmd_{nullptr}; + + Tx::IsochTxDmaRing::FiniteSilenceCadenceProgram + finiteCadenceProgram_{}; + bool finiteCadencePrepared_{false}; + + Tx::IsochTxDmaRing::BoundedCircularSilenceCadenceProgram + boundedCircularCadenceProgram_{}; + bool boundedCircularCadencePrepared_{false}; + + // Stage 6 continuous-silence state. Mutually exclusive with the Stage 5H + // bounded run by construction (one OHCI IT context); reuses + // boundedCircularCadenceProgram_/boundedCircularCadencePrepared_ above. + bool continuousCadenceRunning_{false}; + bool continuousAnchorSeen_{false}; + uint32_t continuousAnchorExecutions_{0U}; + uint16_t continuousPreviousAnchorTimestamp_{0U}; }; } // namespace Isoch diff --git a/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.cpp b/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.cpp index 6b0c52b7..21b6ec20 100644 --- a/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.cpp +++ b/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.cpp @@ -5,6 +5,7 @@ #include "../../Common/TimingUtils.hpp" #include +#include #include namespace ASFW::Isoch::Tx { @@ -28,6 +29,21 @@ namespace { (static_cast(channel & 0x3F) << 8); return OSSwapHostToLittleInt32(h); } + +// The generic AT helper intentionally rejects Z=1 because the normal ASFW +// packet program always occupies four descriptor blocks. Stage 5E proved that +// a true OHCI skip program is exactly one descriptor, so Stage 5G needs a +// narrowly-scoped encoder that permits only the two program sizes used by the +// finite cadence: Z=4 for data and Z=1 for skip. +[[nodiscard]] inline uint32_t MakeFiniteCadenceBranchWord( + const uint32_t descriptorIOVA, + const uint32_t zBlocks) noexcept { + if (descriptorIOVA == 0U || (descriptorIOVA & 0xFU) != 0U || + (zBlocks != 1U && zBlocks != Layout::kBlocksPerPacket)) { + return 0U; + } + return (descriptorIOVA & 0xFFFFFFF0U) | (zBlocks & 0xFU); +} } // namespace void IsochTxDmaRing::ResetForStart() noexcept { @@ -55,11 +71,16 @@ void IsochTxDmaRing::ResetForStart() noexcept { void IsochTxDmaRing::GaugeWirePayload(uint64_t fillAbsIdx, const uint8_t* packetBytes, - uint32_t payloadLength) noexcept { + uint32_t payloadLength, + uint32_t immediateHeaderQ0LE) noexcept { constexpr uint32_t kCipHeaderBytes = 8; constexpr uint32_t kIdleSlotWord = 0x80000000u; // AM824 no-info / idle MIDI - if (payloadLength <= kCipHeaderBytes) { - return; // NO-DATA packet: CIP header only + const uint32_t headerQ0 = OSSwapLittleToHostInt32(immediateHeaderQ0LE); + const auto tag = ASFW::IsochTransport::DecodeIsochTxHeaderTag(headerQ0); + const uint32_t streamHeaderBytes = + tag == ASFW::IsochTransport::IsochPacketTag::kCip ? kCipHeaderBytes : 0U; + if (payloadLength <= streamHeaderBytes) { + return; // CIP header-only or a true empty no-CIP cycle. } const uint64_t dataPackets = @@ -75,8 +96,8 @@ void IsochTxDmaRing::GaugeWirePayload(uint64_t fillAbsIdx, counters_.wireLastInfoQuad.load(std::memory_order_relaxed)); } - const uint32_t quadCount = (payloadLength - kCipHeaderBytes) / 4; - const uint8_t* quadBytes = packetBytes + kCipHeaderBytes; + const uint32_t quadCount = (payloadLength - streamHeaderBytes) / 4; + const uint8_t* quadBytes = packetBytes + streamHeaderBytes; uint32_t infoQuads = 0; uint32_t lastInfoQuad = 0; uint32_t maxAbs24 = 0; @@ -183,18 +204,26 @@ void IsochTxDmaRing::ResyncCycleTracking(Driver::HardwareInterface& hw, const uint32_t lastProcessedPkt = (hwPacketIndex + Layout::kNumPackets - 1) % Layout::kNumPackets; out.completedPacketIndex = lastProcessedPkt; out.completedPacketCount = deltaConsumed; - auto* processedOL = slab_.GetDescriptorPtr( - lastProcessedPkt * Layout::kBlocksPerPacket + - Layout::kCompletionBlock); - + auto* processedFirst = slab_.GetDescriptorPtr( + lastProcessedPkt * Layout::kBlocksPerPacket); if (dmaMemory_) { - dmaMemory_->FetchFromDevice(reinterpret_cast(processedOL), sizeof(*processedOL)); + dmaMemory_->FetchFromDevice( + reinterpret_cast(processedFirst), + sizeof(*processedFirst)); + } + auto* processedCompletion = + CompletionDescriptorForPacket(lastProcessedPkt); + if (dmaMemory_ && processedCompletion != processedFirst) { + dmaMemory_->FetchFromDevice( + reinterpret_cast(processedCompletion), + sizeof(*processedCompletion)); } - const uint16_t hwTimestamp = static_cast(processedOL->statusWord & 0xFFFF); + const uint16_t hwTimestamp = static_cast( + processedCompletion->statusWord & 0xFFFFU); out.hwTimestamp = hwTimestamp; - if (processedOL->statusWord == 0) { + if (processedCompletion->statusWord == 0) { return; } @@ -218,6 +247,861 @@ void IsochTxDmaRing::CommitRefill(const uint32_t toFill) noexcept { counters_.packetsRefilled.fetch_add(toFill, std::memory_order_relaxed); } +void IsochTxDmaRing::ProgramSkipPacket(const uint32_t packetSlot) noexcept { + const uint32_t descBase = packetSlot * Layout::kBlocksPerPacket; + for (uint32_t block = 0; block < Layout::kBlocksPerPacket; ++block) { + auto* desc = slab_.GetDescriptorPtr(descBase + block); + std::memset(desc, 0, sizeof(OHCIDescriptor)); + } + + // A no-CIP idle cycle is a true OHCI skip: one zero-length standard + // OUTPUT_LAST descriptor and no immediate isoch header. Keep the fixed + // four-block slot geometry by branching directly to the next slot with + // Z=4; the remaining three descriptors in this slot stay zeroed. + auto* skip = slab_.GetDescriptorPtr(descBase); + const uint32_t nextPacketSlot = + (packetSlot + 1U) % Layout::kNumPackets; + ITDescriptorBuilder::BuildOutputLast( + *skip, + { + .dataIOVA = 0, + .payloadSize = 0, + .branchIOVA = slab_.GetDescriptorIOVA( + nextPacketSlot * Layout::kBlocksPerPacket), + .zValue = Layout::kBlocksPerPacket, + .interruptBits = + Core::IsTimingGroupBoundary(packetSlot) + ? OHCIDescriptor::kIntAlways + : OHCIDescriptor::kIntNever, + }); +} + +bool IsochTxDmaRing::ProgramSingleSilencePacketForRunPreflight( + uint8_t* payloadBase, + const uint32_t numSlots, + const uint32_t slotStrideBytes, + const ASFW::IsochTransport::TxPacketMeta* metadataRing, + SingleSilencePacketProgram& outProgram) noexcept { + outProgram = {}; + if (!slab_.IsValid() || payloadBase == nullptr || metadataRing == nullptr || + numSlots == 0U || slotStrideBytes == 0U) { + return false; + } + + constexpr uint32_t kExpectedPayloadBytes = 1536U; + for (uint32_t packetSlot = 0U; + packetSlot < Layout::kNumPackets; + ++packetSlot) { + const uint32_t producerSlot = packetSlot % numSlots; + const auto& meta = metadataRing[producerSlot]; + const uint32_t metaQ0 = + OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const auto tag = ASFW::IsochTransport::DecodeIsochTxHeaderTag(metaQ0); + if (ASFW::IsochTransport::ShouldSkipIsochTxPacket( + tag, meta.payloadLength)) { + continue; + } + if (tag != ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + meta.payloadLength != kExpectedPayloadBytes || + meta.payloadLength > slotStrideBytes) { + return false; + } + + const uint8_t* payload = payloadBase + + static_cast(producerSlot) * slotStrideBytes; + for (uint32_t byte = 0U; byte < meta.payloadLength; ++byte) { + if (payload[byte] != 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5F single-packet safety check rejected nonzero payload packet=%u slot=%u byte=%u value=0x%02x", + packetSlot, + producerSlot, + byte, + payload[byte]); + return false; + } + } + + const uint32_t descBase = packetSlot * Layout::kBlocksPerPacket; + auto* immDesc = reinterpret_cast( + slab_.GetDescriptorPtr(descBase)); + auto* desc2 = slab_.GetDescriptorPtr( + descBase + Layout::kFirstPayloadBlock); + auto* completion = slab_.GetDescriptorPtr( + descBase + Layout::kCompletionBlock); + + const uint32_t wireQ0 = + OSSwapLittleToHostInt32(immDesc->immediateData[0]); + const uint32_t wireQ1 = + OSSwapLittleToHostInt32(immDesc->immediateData[1]); + const uint8_t wireChannel = + static_cast((wireQ0 >> 8U) & 0x3FU); + if (ASFW::IsochTransport::DecodeIsochTxHeaderTag(wireQ0) != + ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(wireQ0) != + ASFW::IsochTransport::kIsochSpeedS400 || + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(wireQ0) != + ASFW::IsochTransport::kIsochDataBlockTCode || + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(wireQ1) != + meta.payloadLength || + wireChannel != channel_) { + ASFW_LOG(Isoch, + "IT: Stage 5F single-packet header validation failed packet=%u tag=%u speed=%u tcode=0x%x channel=%u/%u dataLength=%u/%u", + packetSlot, + static_cast( + ASFW::IsochTransport::DecodeIsochTxHeaderTag(wireQ0)), + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(wireQ0), + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(wireQ0), + wireChannel, + channel_, + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(wireQ1), + meta.payloadLength); + return false; + } + + // Convert the normal circular packet program to a finite one-packet + // program. The immediate descriptor's self-link remains the OHCI skip + // address for a missed cycle; the OUTPUT_LAST branch is the queue tail. + const uint32_t intMask = 0x3U << + (OHCIDescriptor::kIntShift + OHCIDescriptor::kControlHighShift); + completion->control &= ~intMask; + completion->branchWord = 0U; + AR_init_status(*completion, 0U); + desc2->statusWord = 0U; + immDesc->common.statusWord = 0U; + + if (dmaMemory_) { + dmaMemory_->PublishToDevice( + reinterpret_cast(payload), + meta.payloadLength); + dmaMemory_->PublishBarrier(); + dmaMemory_->PublishToDevice( + reinterpret_cast(immDesc), + sizeof(OHCIDescriptorImmediate)); + dmaMemory_->PublishToDevice( + reinterpret_cast(desc2), + sizeof(OHCIDescriptor)); + dmaMemory_->PublishToDevice( + reinterpret_cast(completion), + sizeof(OHCIDescriptor)); + } else { + ASFW::Driver::WriteBarrier(); + } + + const uint64_t descriptorIOVA = + slab_.GetDescriptorIOVA(descBase); + if (descriptorIOVA == 0U || descriptorIOVA > 0xFFFFFFFFULL) { + return false; + } + + outProgram.packetSlot = packetSlot; + outProgram.producerSlot = producerSlot; + outProgram.commandPtr = + static_cast(descriptorIOVA) | + Layout::kBlocksPerPacket; + outProgram.payloadLength = meta.payloadLength; + return true; + } + return false; +} + +IsochTxDmaRing::SingleSilencePacketCompletion +IsochTxDmaRing::FetchSingleSilencePacketCompletionForRunPreflight( + const uint32_t packetSlot) noexcept { + SingleSilencePacketCompletion out{}; + if (!slab_.IsValid() || packetSlot >= Layout::kNumPackets) { + return out; + } + + auto* completion = slab_.GetDescriptorPtr( + packetSlot * Layout::kBlocksPerPacket + + Layout::kCompletionBlock); + if (dmaMemory_) { + dmaMemory_->FetchFromDevice( + reinterpret_cast(completion), + sizeof(*completion)); + } else { + ASFW::Driver::ReadBarrier(); + } + out.transferStatus = AT_xferStatus(*completion); + out.timestamp = AT_timeStamp(*completion); + return out; +} + +bool IsochTxDmaRing::ProgramFiniteSilenceCadenceForRunPreflight( + uint8_t* payloadBase, + const uint32_t numSlots, + const uint32_t slotStrideBytes, + const ASFW::IsochTransport::TxPacketMeta* metadataRing, + FiniteSilenceCadenceProgram& outProgram) noexcept { + outProgram = {}; + if (!slab_.IsValid() || payloadBase == nullptr || metadataRing == nullptr || + numSlots == 0U || slotStrideBytes == 0U) { + return false; + } + + static_assert(Layout::kNumPackets == 48U); + constexpr uint32_t kExpectedPayloadBytes = 1536U; + constexpr uint32_t kExpectedDataPackets = 36U; + constexpr uint32_t kExpectedSkipPackets = 12U; + + // The FF800 packetizer's valid 192 kHz cadence may be committed with any + // of the four phase rotations. Stage 5G requires the finite command list + // itself to begin D-D-D-S, so select the phase whose first three physical + // slots are data and whose fourth is a skip. The previous implementation + // assumed physical slot zero was always data; on the real run it was the + // skip phase (S-D-D-D), causing packet 0 to be rejected before DMA. + uint32_t cadenceStartSlot = Layout::kNumPackets; + for (uint32_t candidate = 0U; candidate < 4U; ++candidate) { + bool candidateValid = true; + for (uint32_t logicalSlot = 0U; + logicalSlot < Layout::kNumPackets; + ++logicalSlot) { + const uint32_t physicalSlot = + (candidate + logicalSlot) % Layout::kNumPackets; + const uint32_t producerSlot = physicalSlot % numSlots; + const auto& meta = metadataRing[producerSlot]; + const uint32_t metaQ0 = + OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const uint32_t metaQ1 = + OSSwapLittleToHostInt32(meta.immediateHeader[1]); + const auto tag = + ASFW::IsochTransport::DecodeIsochTxHeaderTag(metaQ0); + const bool actualSkip = + ASFW::IsochTransport::ShouldSkipIsochTxPacket( + tag, meta.payloadLength); + const bool expectedSkip = (logicalSlot & 0x3U) == 0x3U; + if (actualSkip != expectedSkip || + tag != ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(metaQ0) != + ASFW::IsochTransport::kIsochSpeedS400 || + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(metaQ0) != + ASFW::IsochTransport::kIsochDataBlockTCode || + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(metaQ1) != + meta.payloadLength) { + candidateValid = false; + break; + } + } + if (candidateValid) { + cadenceStartSlot = candidate; + break; + } + } + + if (cadenceStartSlot >= Layout::kNumPackets) { + uint32_t observedSkipMask = 0U; + for (uint32_t physicalSlot = 0U; physicalSlot < 4U; ++physicalSlot) { + const auto& meta = metadataRing[physicalSlot % numSlots]; + const uint32_t metaQ0 = + OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const auto tag = + ASFW::IsochTransport::DecodeIsochTxHeaderTag(metaQ0); + if (ASFW::IsochTransport::ShouldSkipIsochTxPacket( + tag, meta.payloadLength)) { + observedSkipMask |= (1U << physicalSlot); + } + } + ASFW_LOG(Isoch, + "IT: Stage 5G could not phase-align committed cadence observedFirst4SkipMask=0x%x expectedRotatedDDD-S=1", + observedSkipMask); + return false; + } + + ASFW_LOG(Isoch, + "IT: Stage 5G cadence phase aligned physicalStartSlot=%u logicalPattern=DDD-S", + cadenceStartSlot); + + uint32_t dataPackets = 0U; + uint32_t skipPackets = 0U; + for (uint32_t logicalSlot = 0U; + logicalSlot < Layout::kNumPackets; + ++logicalSlot) { + const bool expectedSkip = (logicalSlot & 0x3U) == 0x3U; + const uint32_t physicalSlot = + (cadenceStartSlot + logicalSlot) % Layout::kNumPackets; + const uint32_t producerSlot = physicalSlot % numSlots; + const auto& meta = metadataRing[producerSlot]; + const uint32_t metaQ0 = + OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const uint32_t metaQ1 = + OSSwapLittleToHostInt32(meta.immediateHeader[1]); + const auto tag = ASFW::IsochTransport::DecodeIsochTxHeaderTag(metaQ0); + const bool actualSkip = + ASFW::IsochTransport::ShouldSkipIsochTxPacket( + tag, meta.payloadLength); + + if (actualSkip != expectedSkip || + tag != ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(metaQ0) != + ASFW::IsochTransport::kIsochSpeedS400 || + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(metaQ0) != + ASFW::IsochTransport::kIsochDataBlockTCode || + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(metaQ1) != + meta.payloadLength) { + ASFW_LOG(Isoch, + "IT: Stage 5G cadence metadata rejected logical=%u physical=%u expectedSkip=%u actualSkip=%u tag=%u speed=%u tcode=0x%x dataLength=%u/%u", + logicalSlot, + physicalSlot, + expectedSkip ? 1U : 0U, + actualSkip ? 1U : 0U, + static_cast(tag), + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(metaQ0), + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(metaQ0), + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(metaQ1), + meta.payloadLength); + return false; + } + + const uint32_t descBase = + physicalSlot * Layout::kBlocksPerPacket; + const bool isLast = logicalSlot + 1U == Layout::kNumPackets; + const bool nextIsSkip = !isLast && + (((logicalSlot + 1U) & 0x3U) == 0x3U); + const uint32_t nextZ = + nextIsSkip ? 1U : Layout::kBlocksPerPacket; + const uint32_t nextPhysicalSlot = isLast + ? 0U + : ((cadenceStartSlot + logicalSlot + 1U) % + Layout::kNumPackets); + const uint32_t nextDescriptorIOVA = isLast + ? 0U + : slab_.GetDescriptorIOVA( + nextPhysicalSlot * Layout::kBlocksPerPacket); + + if (expectedSkip) { + ++skipPackets; + if (meta.payloadLength != 0U) { + return false; + } + + for (uint32_t block = 0U; + block < Layout::kBlocksPerPacket; + ++block) { + auto* desc = slab_.GetDescriptorPtr(descBase + block); + std::memset(desc, 0, sizeof(OHCIDescriptor)); + } + + auto* skip = slab_.GetDescriptorPtr(descBase); + skip->control = OHCIDescriptor::BuildControl({ + .reqCount = 0U, + .command = OHCIDescriptor::kCmdOutputLast, + .key = OHCIDescriptor::kKeyStandard, + .interruptBits = OHCIDescriptor::kIntNever, + .branchBits = OHCIDescriptor::kBranchAlways, + }); + skip->control |= + (1U << (OHCIDescriptor::kStatusShift + + OHCIDescriptor::kControlHighShift)); + skip->dataAddress = 0U; + skip->branchWord = isLast + ? 0U + : MakeFiniteCadenceBranchWord(nextDescriptorIOVA, nextZ); + if (!isLast && skip->branchWord == 0U) { + return false; + } + AR_init_status(*skip, 0U); + continue; + } + + ++dataPackets; + if (meta.payloadLength != kExpectedPayloadBytes || + meta.payloadLength > slotStrideBytes) { + ASFW_LOG(Isoch, + "IT: Stage 5G data shape rejected logical=%u physical=%u producer=%u payloadBytes=%u/%u stride=%u", + logicalSlot, + physicalSlot, + producerSlot, + meta.payloadLength, + kExpectedPayloadBytes, + slotStrideBytes); + return false; + } + + const uint8_t* payload = payloadBase + + static_cast(producerSlot) * slotStrideBytes; + for (uint32_t byte = 0U; byte < meta.payloadLength; ++byte) { + if (payload[byte] != 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5G silence safety check rejected logical=%u physical=%u producer=%u byte=%u value=0x%02x", + logicalSlot, + physicalSlot, + producerSlot, + byte, + payload[byte]); + return false; + } + } + + auto* immDesc = reinterpret_cast( + slab_.GetDescriptorPtr(descBase)); + auto* payloadDesc = slab_.GetDescriptorPtr( + descBase + Layout::kFirstPayloadBlock); + auto* completion = slab_.GetDescriptorPtr( + descBase + Layout::kCompletionBlock); + const uint32_t wireQ0 = + OSSwapLittleToHostInt32(immDesc->immediateData[0]); + const uint32_t wireQ1 = + OSSwapLittleToHostInt32(immDesc->immediateData[1]); + const uint8_t wireChannel = + static_cast((wireQ0 >> 8U) & 0x3FU); + const uint32_t firstFragmentBytes = + payloadDesc->control & 0xFFFFU; + const uint32_t lastFragmentBytes = + completion->control & 0xFFFFU; + if (ASFW::IsochTransport::DecodeIsochTxHeaderTag(wireQ0) != + ASFW::IsochTransport::IsochPacketTag::kNoCipHeader || + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(wireQ0) != + ASFW::IsochTransport::kIsochSpeedS400 || + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(wireQ0) != + ASFW::IsochTransport::kIsochDataBlockTCode || + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(wireQ1) != + kExpectedPayloadBytes || + wireChannel != channel_ || + firstFragmentBytes + lastFragmentBytes != kExpectedPayloadBytes || + payloadDesc->dataAddress == 0U || + completion->dataAddress == 0U) { + ASFW_LOG(Isoch, + "IT: Stage 5G primed descriptor validation failed logical=%u physical=%u tag=%u speed=%u tcode=0x%x channel=%u/%u dataLength=%u fragments=%u+%u", + logicalSlot, + physicalSlot, + static_cast( + ASFW::IsochTransport::DecodeIsochTxHeaderTag(wireQ0)), + ASFW::IsochTransport::DecodeIsochTxHeaderSpeed(wireQ0), + ASFW::IsochTransport::DecodeIsochTxHeaderTCode(wireQ0), + wireChannel, + channel_, + ASFW::IsochTransport::DecodeIsochTxHeaderDataLength(wireQ1), + firstFragmentBytes, + lastFragmentBytes); + return false; + } + + const uint32_t intMask = 0x3U << + (OHCIDescriptor::kIntShift + + OHCIDescriptor::kControlHighShift); + completion->control &= ~intMask; + completion->branchWord = isLast + ? 0U + : MakeFiniteCadenceBranchWord(nextDescriptorIOVA, nextZ); + if (!isLast && completion->branchWord == 0U) { + return false; + } + AR_init_status(*completion, 0U); + payloadDesc->statusWord = 0U; + immDesc->common.statusWord = 0U; + + if (dmaMemory_) { + dmaMemory_->PublishToDevice( + reinterpret_cast(payload), + meta.payloadLength); + } + } + + if (dataPackets != kExpectedDataPackets || + skipPackets != kExpectedSkipPackets) { + return false; + } + + if (dmaMemory_) { + dmaMemory_->PublishBarrier(); + dmaMemory_->PublishToDevice( + slab_.DescriptorRegion().virtualBase, + slab_.DescriptorRegion().size); + } else { + ASFW::Driver::WriteBarrier(); + } + + const uint64_t descriptorIOVA = slab_.GetDescriptorIOVA( + cadenceStartSlot * Layout::kBlocksPerPacket); + if (descriptorIOVA == 0U || descriptorIOVA > 0xFFFFFFFFULL) { + return false; + } + + for (uint32_t logicalSlot = 0U; + logicalSlot < Layout::kNumPackets; + ++logicalSlot) { + const bool isSkip = (logicalSlot & 0x3U) == 0x3U; + const uint32_t physicalSlot = + (cadenceStartSlot + logicalSlot) % Layout::kNumPackets; + const auto* branchOwner = isSkip + ? slab_.GetDescriptorPtr( + physicalSlot * Layout::kBlocksPerPacket) + : slab_.GetDescriptorPtr( + physicalSlot * Layout::kBlocksPerPacket + + Layout::kCompletionBlock); + const bool isLast = logicalSlot + 1U == Layout::kNumPackets; + const bool nextIsSkip = !isLast && + (((logicalSlot + 1U) & 0x3U) == 0x3U); + const uint32_t nextPhysicalSlot = isLast + ? 0U + : ((cadenceStartSlot + logicalSlot + 1U) % + Layout::kNumPackets); + const uint32_t expectedBranch = isLast + ? 0U + : MakeFiniteCadenceBranchWord( + slab_.GetDescriptorIOVA( + nextPhysicalSlot * Layout::kBlocksPerPacket), + nextIsSkip ? 1U : Layout::kBlocksPerPacket); + if (branchOwner->branchWord != expectedBranch || + (!isLast && expectedBranch == 0U)) { + ASFW_LOG(Isoch, + "IT: Stage 5G finite branch validation failed logical=%u physical=%u skip=%u actual=0x%08x expected=0x%08x", + logicalSlot, + physicalSlot, + isSkip ? 1U : 0U, + branchOwner->branchWord, + expectedBranch); + return false; + } + } + + outProgram.commandPtr = + static_cast(descriptorIOVA) | + Layout::kBlocksPerPacket; + outProgram.startPacketSlot = cadenceStartSlot; + outProgram.descriptorCount = Layout::kNumPackets; + outProgram.dataPacketCount = dataPackets; + outProgram.skipPacketCount = skipPackets; + outProgram.payloadLength = kExpectedPayloadBytes; + return true; +} + +bool IsochTxDmaRing::ProgramBoundedCircularSilenceCadenceForPreflight( + uint8_t* payloadBase, + const uint32_t numSlots, + const uint32_t slotStrideBytes, + const ASFW::IsochTransport::TxPacketMeta* metadataRing, + BoundedCircularSilenceCadenceProgram& outProgram) noexcept { + outProgram = {}; + + FiniteSilenceCadenceProgram finite{}; + if (!ProgramFiniteSilenceCadenceForRunPreflight( + payloadBase, + numSlots, + slotStrideBytes, + metadataRing, + finite)) { + ASFW_LOG(Isoch, + "IT: Stage 5H could not construct the Stage 5G-validated silence cadence"); + return false; + } + + if (finite.descriptorCount != Layout::kNumPackets || + finite.dataPacketCount != 36U || + finite.skipPacketCount != 12U || + finite.payloadLength != 1536U || + finite.startPacketSlot >= Layout::kNumPackets) { + return false; + } + + // Logical slot 47 is always the skip in the twelfth D-D-D-S group. Its + // one-descriptor program must advertise Z=4 when branching back to the + // first logical slot, which is guaranteed to be a normal data program. + const uint32_t lastPhysicalSlot = + (finite.startPacketSlot + Layout::kNumPackets - 1U) % + Layout::kNumPackets; + auto* lastSkip = slab_.GetDescriptorPtr( + lastPhysicalSlot * Layout::kBlocksPerPacket); + const uint32_t firstDescriptorIOVA = slab_.GetDescriptorIOVA( + finite.startPacketSlot * Layout::kBlocksPerPacket); + const uint32_t circularBranch = MakeFiniteCadenceBranchWord( + firstDescriptorIOVA, Layout::kBlocksPerPacket); + if (circularBranch == 0U) { + return false; + } + lastSkip->branchWord = circularBranch; + + if (dmaMemory_) { + dmaMemory_->PublishBarrier(); + dmaMemory_->PublishToDevice( + slab_.DescriptorRegion().virtualBase, + slab_.DescriptorRegion().size); + } else { + ASFW::Driver::WriteBarrier(); + } + + // Re-read every branch owner after publication. The first 47 links retain + // Stage 5G's phase-correct Z values; only the final skip is now circular. + for (uint32_t logicalSlot = 0U; + logicalSlot < Layout::kNumPackets; + ++logicalSlot) { + const bool isSkip = (logicalSlot & 0x3U) == 0x3U; + const uint32_t physicalSlot = + (finite.startPacketSlot + logicalSlot) % Layout::kNumPackets; + const auto* branchOwner = isSkip + ? slab_.GetDescriptorPtr( + physicalSlot * Layout::kBlocksPerPacket) + : slab_.GetDescriptorPtr( + physicalSlot * Layout::kBlocksPerPacket + + Layout::kCompletionBlock); + const uint32_t nextLogicalSlot = + (logicalSlot + 1U) % Layout::kNumPackets; + const uint32_t nextPhysicalSlot = + (finite.startPacketSlot + nextLogicalSlot) % + Layout::kNumPackets; + const bool nextIsSkip = (nextLogicalSlot & 0x3U) == 0x3U; + const uint32_t expectedBranch = MakeFiniteCadenceBranchWord( + slab_.GetDescriptorIOVA( + nextPhysicalSlot * Layout::kBlocksPerPacket), + nextIsSkip ? 1U : Layout::kBlocksPerPacket); + if (expectedBranch == 0U || + branchOwner->branchWord != expectedBranch) { + ASFW_LOG(Isoch, + "IT: Stage 5H circular branch validation failed logical=%u physical=%u skip=%u actual=0x%08x expected=0x%08x", + logicalSlot, + physicalSlot, + isSkip ? 1U : 0U, + branchOwner->branchWord, + expectedBranch); + return false; + } + } + + outProgram.commandPtr = finite.commandPtr; + outProgram.startPacketSlot = finite.startPacketSlot; + outProgram.descriptorCount = finite.descriptorCount; + outProgram.dataPacketCount = finite.dataPacketCount; + outProgram.skipPacketCount = finite.skipPacketCount; + outProgram.payloadLength = finite.payloadLength; + + ASFW_LOG(Isoch, + "IT: ✅ Stage 5H bounded circular descriptor ring published descriptors=%u dataPackets=%u skip=%u payloadBytes=%u pattern=DDD-S finalBranchToStart=1 interrupts=0 refill=0 dmaPublish=1", + outProgram.descriptorCount, + outProgram.dataPacketCount, + outProgram.skipPacketCount, + outProgram.payloadLength); + return true; +} + +IsochTxDmaRing::CadenceAnchorCompletion +IsochTxDmaRing::FetchCadenceAnchorCompletionForPreflight( + const uint32_t startPacketSlot) noexcept { + CadenceAnchorCompletion out{}; + if (!slab_.IsValid() || startPacketSlot >= Layout::kNumPackets) { + return out; + } + const auto* completion = slab_.GetDescriptorPtr( + startPacketSlot * Layout::kBlocksPerPacket + + Layout::kCompletionBlock); + if (dmaMemory_) { + dmaMemory_->FetchFromDevice( + reinterpret_cast(completion), + sizeof(*completion)); + } else { + ASFW::Driver::ReadBarrier(); + } + out.transferStatus = AT_xferStatus(*completion); + out.timestamp = AT_timeStamp(*completion); + return out; +} + +IsochTxDmaRing::FiniteSilenceCadenceCompletion +IsochTxDmaRing::FetchFiniteSilenceCadenceCompletionForRunPreflight( + const uint32_t startPacketSlot) noexcept { + FiniteSilenceCadenceCompletion out{}; + if (!slab_.IsValid() || startPacketSlot >= Layout::kNumPackets) { + return out; + } + + const auto region = slab_.DescriptorRegion(); + if (dmaMemory_) { + dmaMemory_->FetchFromDevice(region.virtualBase, region.size); + } else { + ASFW::Driver::ReadBarrier(); + } + + uint32_t dataIndex = 0U; + for (uint32_t logicalSlot = 0U; + logicalSlot < Layout::kNumPackets; + ++logicalSlot) { + const bool isSkip = (logicalSlot & 0x3U) == 0x3U; + const uint32_t physicalSlot = + (startPacketSlot + logicalSlot) % Layout::kNumPackets; + const auto* completion = isSkip + ? slab_.GetDescriptorPtr( + physicalSlot * Layout::kBlocksPerPacket) + : slab_.GetDescriptorPtr( + physicalSlot * Layout::kBlocksPerPacket + + Layout::kCompletionBlock); + const uint16_t transferStatus = AT_xferStatus(*completion); + const uint16_t timestamp = AT_timeStamp(*completion); + if (transferStatus != 0U) { + ++out.completedDescriptors; + const uint8_t eventCode = + static_cast(transferStatus & 0x1FU); + if (eventCode != 0x11U) { + ++out.eventErrors; + } + if (!isSkip) { + ++out.completedDataDescriptors; + ++out.actualBusPackets; + if (dataIndex < out.dataTimestamps.size()) { + out.dataTimestamps[dataIndex] = timestamp; + } + ++dataIndex; + } + } + if (logicalSlot + 1U == Layout::kNumPackets) { + out.lastTransferStatus = transferStatus; + out.lastTimestamp = timestamp; + } + } + + if (out.completedDataDescriptors == out.dataTimestamps.size()) { + out.cadenceValid = true; + uint32_t previousCycle = + (out.dataTimestamps[0] & 0x1FFFU) % 8000U; + for (uint32_t data = 1U; + data < out.dataTimestamps.size(); + ++data) { + const uint32_t cycle = + (out.dataTimestamps[data] & 0x1FFFU) % 8000U; + const uint32_t gap = + (cycle + 8000U - previousCycle) % 8000U; + const uint32_t expectedGap = + (data % 3U) == 0U ? 2U : 1U; + if (gap != expectedGap) { + out.cadenceValid = false; + break; + } + previousCycle = cycle; + } + } + return out; +} + +bool IsochTxDmaRing::ProgramAllSkipPacketsForRunPreflight() noexcept { + if (!slab_.IsValid()) { + return false; + } + + for (uint32_t packetSlot = 0; + packetSlot < Layout::kNumPackets; + ++packetSlot) { + const uint32_t descBase = packetSlot * Layout::kBlocksPerPacket; + for (uint32_t block = 0; block < Layout::kBlocksPerPacket; ++block) { + auto* desc = slab_.GetDescriptorPtr(descBase + block); + std::memset(desc, 0, sizeof(OHCIDescriptor)); + } + + auto* skip = slab_.GetDescriptorPtr(descBase); + const bool isLast = packetSlot + 1U == Layout::kNumPackets; + const uint32_t nextDescriptorIOVA = isLast + ? 0U + : slab_.GetDescriptorIOVA( + (packetSlot + 1U) * Layout::kBlocksPerPacket); + + // A queued OHCI transmit skip is a one-descriptor program (Z=1), + // even though ASFW reserves four physical descriptor slots per audio + // cycle. Advertising Z=4 made the controller consume the first + // OUTPUT_LAST and then interpret the three zero padding descriptors as + // part of the same program, so only descriptor 0 ever completed. + skip->control = OHCIDescriptor::BuildControl({ + .reqCount = 0, + .command = OHCIDescriptor::kCmdOutputLast, + .key = OHCIDescriptor::kKeyStandard, + .interruptBits = OHCIDescriptor::kIntNever, + .branchBits = OHCIDescriptor::kBranchAlways, + }); + skip->control |= + (1u << (OHCIDescriptor::kStatusShift + + OHCIDescriptor::kControlHighShift)); + skip->dataAddress = 0U; + skip->branchWord = isLast + ? 0U + : ((nextDescriptorIOVA & 0xFFFFFFF0U) | 1U); + AR_init_status(*skip, 0U); + } + + // Publish the complete finite descriptor chain before CommandPtr/RUN. + if (dmaMemory_) { + dmaMemory_->PublishToDevice( + slab_.DescriptorRegion().virtualBase, + slab_.DescriptorRegion().size); + } else { + ASFW::Driver::WriteBarrier(); + } + + for (uint32_t packetSlot = 0; + packetSlot < Layout::kNumPackets; + ++packetSlot) { + if (!IsSkipDescriptor(packetSlot)) { + return false; + } + const auto* desc = slab_.GetDescriptorPtr( + packetSlot * Layout::kBlocksPerPacket); + const bool isLast = packetSlot + 1U == Layout::kNumPackets; + if (isLast) { + if (desc->branchWord != 0U) { + return false; + } + } else if (desc->branchWord == 0U || + (desc->branchWord & 0xFU) != 1U) { + return false; + } + } + return true; +} + +IsochTxDmaRing::AllSkipCompletionSnapshot +IsochTxDmaRing::FetchAllSkipCompletionForRunPreflight() noexcept { + AllSkipCompletionSnapshot out{}; + if (!slab_.IsValid()) { + return out; + } + + const auto region = slab_.DescriptorRegion(); + if (dmaMemory_) { + dmaMemory_->FetchFromDevice(region.virtualBase, region.size); + } else { + ASFW::Driver::ReadBarrier(); + } + + for (uint32_t packetSlot = 0; + packetSlot < Layout::kNumPackets; + ++packetSlot) { + const auto* desc = slab_.GetDescriptorPtr( + packetSlot * Layout::kBlocksPerPacket); + if (AT_xferStatus(*desc) != 0U) { + ++out.completedDescriptors; + } + if (packetSlot + 1U == Layout::kNumPackets) { + out.lastTransferStatus = AT_xferStatus(*desc); + out.lastTimestamp = AT_timeStamp(*desc); + } + } + return out; +} + +bool IsochTxDmaRing::IsSkipDescriptor( + const uint32_t packetSlot) const noexcept { + const auto* desc = slab_.GetDescriptorPtr( + packetSlot * Layout::kBlocksPerPacket); + const uint32_t controlHigh = + desc->control >> OHCIDescriptor::kControlHighShift; + const uint8_t command = static_cast( + (controlHigh >> OHCIDescriptor::kCmdShift) & 0xFU); + const uint8_t key = static_cast( + (controlHigh >> OHCIDescriptor::kKeyShift) & 0x7U); + const uint16_t requestCount = + static_cast(desc->control & 0xFFFFU); + return command == OHCIDescriptor::kCmdOutputLast && + key == OHCIDescriptor::kKeyStandard && + requestCount == 0U; +} + +IsochTxDmaRing::OHCIDescriptor* +IsochTxDmaRing::CompletionDescriptorForPacket( + const uint32_t packetSlot) noexcept { + if (IsSkipDescriptor(packetSlot)) { + return slab_.GetDescriptorPtr( + packetSlot * Layout::kBlocksPerPacket); + } + return slab_.GetDescriptorPtr( + packetSlot * Layout::kBlocksPerPacket + + Layout::kCompletionBlock); +} + IsochTxDmaRing::PrimeStats IsochTxDmaRing::Prime( const TxPayloadDmaMap& payloadDmaMap, const uint32_t numSlots, @@ -277,6 +1161,16 @@ IsochTxDmaRing::PrimeStats IsochTxDmaRing::Prime( return stats; } + const uint32_t headerQ0 = + OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const auto packetTag = + ASFW::IsochTransport::DecodeIsochTxHeaderTag(headerQ0); + if (ASFW::IsochTransport::ShouldSkipIsochTxPacket( + packetTag, meta.payloadLength)) { + ProgramSkipPacket(pktIdx); + continue; + } + std::array payloadFragments{}; const uint64_t payloadOffset = static_cast(producerSlot) * slotStrideBytes; @@ -525,15 +1419,23 @@ IsochTxDmaRing::RefillOutcome IsochTxDmaRing::Refill( const uint64_t currentAbsIdx = completedAbsIdx + i; const uint32_t completedPktSlot = static_cast(currentAbsIdx % Layout::kNumPackets); - auto* desc2 = slab_.GetDescriptorPtr( - completedPktSlot * Layout::kBlocksPerPacket + - Layout::kCompletionBlock); + auto* firstDesc = slab_.GetDescriptorPtr( + completedPktSlot * Layout::kBlocksPerPacket); if (dmaMemory_) { - dmaMemory_->FetchFromDevice(reinterpret_cast(desc2), sizeof(*desc2)); + dmaMemory_->FetchFromDevice( + reinterpret_cast(firstDesc), + sizeof(*firstDesc)); + } + auto* completionDesc = + CompletionDescriptorForPacket(completedPktSlot); + if (dmaMemory_ && completionDesc != firstDesc) { + dmaMemory_->FetchFromDevice( + reinterpret_cast(completionDesc), + sizeof(*completionDesc)); } - const uint16_t hwTimestamp = - static_cast(desc2->statusWord & 0xFFFF); + const uint16_t hwTimestamp = static_cast( + completionDesc->statusWord & 0xFFFFU); // OHCI OUTPUT_LAST reports sec[2:0]:cycle[12:0] and omits the // intra-cycle offset. Reconstruct the packet cycle at offset zero. @@ -671,10 +1573,33 @@ IsochTxDmaRing::RefillOutcome IsochTxDmaRing::Refill( return out; } + const uint32_t headerQ0 = + OSSwapLittleToHostInt32(meta.immediateHeader[0]); + const auto packetTag = + ASFW::IsochTransport::DecodeIsochTxHeaderTag(headerQ0); + if (ASFW::IsochTransport::ShouldSkipIsochTxPacket( + packetTag, payloadLength)) { + ProgramSkipPacket(hwSlot); + if (dmaMemory_) { + for (uint32_t block = 0; + block < Layout::kBlocksPerPacket; + ++block) { + const auto* desc = slab_.GetDescriptorPtr( + hwSlot * Layout::kBlocksPerPacket + block); + dmaMemory_->PublishToDevice( + reinterpret_cast(desc), + sizeof(*desc)); + } + } + ++packetsFilled; + continue; + } + if (payloadBase) { GaugeWirePayload(fillAbsIdx, payloadBase + payloadOffset, - payloadLength); + payloadLength, + meta.immediateHeader[0]); } std::array payloadFragments{}; @@ -703,6 +1628,14 @@ IsochTxDmaRing::RefillOutcome IsochTxDmaRing::Refill( const uint32_t descBase = hwSlot * Layout::kBlocksPerPacket; auto* immDesc = reinterpret_cast( slab_.GetDescriptorPtr(descBase)); + std::memset(immDesc, 0, sizeof(OHCIDescriptorImmediate)); + immDesc->common.control = OHCIDescriptor::BuildControl({ + .reqCount = 8, + .command = OHCIDescriptor::kCmdOutputMore, + .key = OHCIDescriptor::kKeyImmediate, + .interruptBits = OHCIDescriptor::kIntNever, + .branchBits = OHCIDescriptor::kBranchNever, + }); immDesc->immediateData[0] = StampHeaderChannel(meta.immediateHeader[0], channel_); immDesc->immediateData[1] = meta.immediateHeader[1]; @@ -712,6 +1645,7 @@ IsochTxDmaRing::RefillOutcome IsochTxDmaRing::Refill( auto* desc2 = slab_.GetDescriptorPtr(descBase + Layout::kFirstPayloadBlock); + std::memset(desc2, 0, sizeof(OHCIDescriptor)); desc2->control = OHCIDescriptor::BuildControl({ .reqCount = static_cast(payloadFragments[0].length), .command = OHCIDescriptor::kCmdOutputMore, @@ -725,6 +1659,7 @@ IsochTxDmaRing::RefillOutcome IsochTxDmaRing::Refill( auto* desc3 = slab_.GetDescriptorPtr(descBase + Layout::kCompletionBlock); + std::memset(desc3, 0, sizeof(OHCIDescriptor)); desc3->control = OHCIDescriptor::BuildControl({ .reqCount = static_cast(payloadFragments[1].length), .command = OHCIDescriptor::kCmdOutputLast, diff --git a/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.hpp b/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.hpp index 9abd1948..9adc17bb 100644 --- a/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.hpp +++ b/ASFWDriver/Isoch/Transmit/IsochTxDmaRing.hpp @@ -135,6 +135,109 @@ class IsochTxDmaRing final { const ASFW::IsochTransport::TxPacketMeta* metadataRing, uint64_t preFillCount) noexcept; + struct SingleSilencePacketProgram final { + uint32_t packetSlot{0U}; + uint32_t producerSlot{0U}; + uint32_t commandPtr{0U}; + uint32_t payloadLength{0U}; + }; + + struct SingleSilencePacketCompletion final { + uint16_t transferStatus{0U}; + uint16_t timestamp{0U}; + }; + + // Stage 5F safety seam: select one already-primed no-CIP FF800 data packet, + // verify that its 1536-byte payload is entirely zero, terminate its normal + // four-descriptor OHCI program, publish payload + descriptors, and return + // the exact CommandPtr. This is the first preflight that can emit a packet. + [[nodiscard]] bool ProgramSingleSilencePacketForRunPreflight( + uint8_t* payloadBase, + uint32_t numSlots, + uint32_t slotStrideBytes, + const ASFW::IsochTransport::TxPacketMeta* metadataRing, + SingleSilencePacketProgram& outProgram) noexcept; + [[nodiscard]] SingleSilencePacketCompletion + FetchSingleSilencePacketCompletionForRunPreflight( + uint32_t packetSlot) noexcept; + + struct FiniteSilenceCadenceProgram final { + uint32_t commandPtr{0U}; + uint32_t startPacketSlot{0U}; + uint32_t descriptorCount{0U}; + uint32_t dataPacketCount{0U}; + uint32_t skipPacketCount{0U}; + uint32_t payloadLength{0U}; + }; + + struct FiniteSilenceCadenceCompletion final { + uint32_t completedDescriptors{0U}; + uint32_t completedDataDescriptors{0U}; + uint32_t actualBusPackets{0U}; + uint32_t eventErrors{0U}; + uint16_t lastTransferStatus{0U}; + uint16_t lastTimestamp{0U}; + bool cadenceValid{false}; + std::array dataTimestamps{}; + }; + + // Stage 5G safety seam: convert the already-primed 48-cycle FF800 ring + // into one finite D-D-D-S cadence burst. All 36 data payloads must be + // 1536-byte no-CIP silence packets; all 12 idle cycles remain true OHCI + // skip programs. Queue branches are finite and the final skip terminates. + [[nodiscard]] bool ProgramFiniteSilenceCadenceForRunPreflight( + uint8_t* payloadBase, + uint32_t numSlots, + uint32_t slotStrideBytes, + const ASFW::IsochTransport::TxPacketMeta* metadataRing, + FiniteSilenceCadenceProgram& outProgram) noexcept; + [[nodiscard]] FiniteSilenceCadenceCompletion + FetchFiniteSilenceCadenceCompletionForRunPreflight( + uint32_t startPacketSlot) noexcept; + + struct BoundedCircularSilenceCadenceProgram final { + uint32_t commandPtr{0U}; + uint32_t startPacketSlot{0U}; + uint32_t descriptorCount{0U}; + uint32_t dataPacketCount{0U}; + uint32_t skipPacketCount{0U}; + uint32_t payloadLength{0U}; + }; + + struct CadenceAnchorCompletion final { + uint16_t transferStatus{0U}; + uint16_t timestamp{0U}; + }; + + // Stage 5H safety seam: reuse the Stage 5G-validated 48-cycle D-D-D-S + // silence program, then connect its final skip back to the first data + // descriptor. The ring remains immutable while RUN is set; the caller + // must stop it after a strictly bounded interval. + [[nodiscard]] bool ProgramBoundedCircularSilenceCadenceForPreflight( + uint8_t* payloadBase, + uint32_t numSlots, + uint32_t slotStrideBytes, + const ASFW::IsochTransport::TxPacketMeta* metadataRing, + BoundedCircularSilenceCadenceProgram& outProgram) noexcept; + [[nodiscard]] CadenceAnchorCompletion + FetchCadenceAnchorCompletionForPreflight( + uint32_t startPacketSlot) noexcept; + + struct AllSkipCompletionSnapshot final { + uint32_t completedDescriptors{0}; + uint16_t lastTransferStatus{0}; + uint16_t lastTimestamp{0}; + }; + + // Stage 5E safety seam: replace every packet program with a true OHCI + // transmit skip descriptor. The chain is deliberately finite: descriptors + // 0..46 branch forward and descriptor 47 terminates with branchAddress=0. + // This mirrors the normal OHCI queue model and lets completion status, + // rather than the transient ACTIVE bit, prove that DMA executed. + [[nodiscard]] bool ProgramAllSkipPacketsForRunPreflight() noexcept; + [[nodiscard]] AllSkipCompletionSnapshot + FetchAllSkipCompletionForRunPreflight() noexcept; + [[nodiscard]] RefillOutcome Refill(Driver::HardwareInterface& hw, uint8_t contextIndex, ASFW::IsochTransport::TxPacketMeta* metadataRing, @@ -164,13 +267,18 @@ class IsochTxDmaRing final { uint32_t deltaConsumed, RefillOutcome& out) noexcept; void CommitRefill(uint32_t toFill) noexcept; + void ProgramSkipPacket(uint32_t packetSlot) noexcept; + [[nodiscard]] bool IsSkipDescriptor(uint32_t packetSlot) const noexcept; + [[nodiscard]] OHCIDescriptor* CompletionDescriptorForPacket( + uint32_t packetSlot) noexcept; [[nodiscard]] bool DecodeHardwarePacketIndex(Driver::HardwareInterface& hw, uint8_t contextIndex, uint32_t& outPacketIndex, uint32_t& outCmdPtr) noexcept; void GaugeWirePayload(uint64_t fillAbsIdx, const uint8_t* packetBytes, - uint32_t payloadLength) noexcept; + uint32_t payloadLength, + uint32_t immediateHeaderQ0LE) noexcept; uint8_t channel_{0}; IsochTxDescriptorSlab slab_{}; diff --git a/ASFWDriver/Scheduling/WatchdogCoordinator.cpp b/ASFWDriver/Scheduling/WatchdogCoordinator.cpp index f4a2e45c..f201e137 100644 --- a/ASFWDriver/Scheduling/WatchdogCoordinator.cpp +++ b/ASFWDriver/Scheduling/WatchdogCoordinator.cpp @@ -10,6 +10,7 @@ #include "../Isoch/IsochReceiveContext.hpp" #include "../Isoch/Transmit/IsochTransmitContext.hpp" #include "../Logging/LogConfig.hpp" +#include "../Logging/Logging.hpp" namespace ASFW::Driver { namespace { @@ -27,6 +28,10 @@ constexpr uint32_t kPayloadWriterDrainIntervalTicks = 100; // so log the most recent decision once per ~1 s (1000 ticks). The line carries // the total decision count so collapsed updates are still visible. constexpr uint32_t kTxSytTraceIntervalTicks = 1000; +// Stage 6 (dev-only continuous silence) coarse heartbeat. The ring runs with +// zero interrupts by design, so this is the only liveness signal for it; +// anomalies (DEAD/event error) log immediately and independently below. +constexpr uint32_t kContinuousCadenceHeartbeatIntervalTicks = 1000; uint64_t MicrosecondsToMachTicks(uint64_t usec) { static mach_timebase_info_data_t timebase{0, 0}; @@ -189,6 +194,32 @@ void WatchdogCoordinator::TickIsochTransmit( isochTransmitContext->Poll(); } + // Stage 6 continuous silence cadence never enters State::Running (see + // IsochTransmitContext::StartContinuousCircularSilenceCadence), so it is + // independent of the isRunning/Poll() path above. Cheap no-op when no + // continuous cadence is active (single bool check inside the poll call). + const auto continuousHealth = + isochTransmitContext->PollContinuousCircularSilenceCadenceHealth(); + if (continuousHealth.running) { + if (continuousHealth.dead || continuousHealth.eventError) { + ASFW_LOG(Isoch, + "IT: ⚠️ Continuous cadence anomaly dead=%u eventError=%u anchorExecutions=%u", + continuousHealth.dead ? 1U : 0U, + continuousHealth.eventError ? 1U : 0U, + continuousHealth.anchorExecutions); + } + if (++continuousCadenceHeartbeatDivider_ >= + kContinuousCadenceHeartbeatIntervalTicks) { + continuousCadenceHeartbeatDivider_ = 0; + ASFW_LOG(Isoch, + "IT: Continuous cadence heartbeat anchorExecutions=%u lastAnchorTimestamp=%u", + continuousHealth.anchorExecutions, + continuousHealth.lastAnchorTimestamp); + } + } else { + continuousCadenceHeartbeatDivider_ = 0; + } + if (++itLogDivider_ < 1000) { return; } diff --git a/ASFWDriver/Scheduling/WatchdogCoordinator.hpp b/ASFWDriver/Scheduling/WatchdogCoordinator.hpp index 7f9c4112..4626fba1 100644 --- a/ASFWDriver/Scheduling/WatchdogCoordinator.hpp +++ b/ASFWDriver/Scheduling/WatchdogCoordinator.hpp @@ -56,6 +56,7 @@ class WatchdogCoordinator { uint32_t ztsLogDivider_{0}; uint32_t payloadWriterLogDivider_{0}; uint32_t txSytTraceDivider_{0}; + uint32_t continuousCadenceHeartbeatDivider_{0}; }; } // namespace ASFW::Driver diff --git a/ASFWDriver/Service/DriverContext.cpp b/ASFWDriver/Service/DriverContext.cpp index 48f0873e..19697406 100644 --- a/ASFWDriver/Service/DriverContext.cpp +++ b/ASFWDriver/Service/DriverContext.cpp @@ -303,11 +303,23 @@ kern_return_t DriverWiring::PrepareInterrupts(ASFWDriver& service, IOService* pr return kIOReturnBadArgument; } - auto status = pci->ConfigureInterrupts(kIOInterruptTypePCIMessagedX, 1, 1, 0); + auto status = pci->ConfigureInterrupts( + kIOInterruptTypePCIMessagedX, 1, 1, 0); + ASFW_LOG(Controller, + "PrepareInterrupts: MSI-X ConfigureInterrupts -> 0x%08x", + status); + if (status != kIOReturnSuccess) { - status = pci->ConfigureInterrupts(kIOInterruptTypePCIMessaged, 1, 1, 0); + status = pci->ConfigureInterrupts( + kIOInterruptTypePCIMessaged, 1, 1, 0); + ASFW_LOG(Controller, + "PrepareInterrupts: MSI ConfigureInterrupts -> 0x%08x", + status); + if (status != kIOReturnSuccess) { - return status; + ASFW_LOG(Controller, + "PrepareInterrupts: MSI-X/MSI unavailable; " + "trying existing interrupt index 0"); } } } @@ -320,7 +332,12 @@ kern_return_t DriverWiring::PrepareInterrupts(ASFWDriver& service, IOService* pr ctx.interruptAction = OSSharedPtr(action, OSNoRetain); } - auto kr = intrMgr->Initialise(provider, ctx.workQueue, ctx.interruptAction); + auto kr = intrMgr->Initialise( + provider, ctx.workQueue, ctx.interruptAction); + ASFW_LOG(Controller, + "PrepareInterrupts: InterruptManager::Initialise(index=0) " + "-> 0x%08x", + kr); if (kr != kIOReturnSuccess) { ctx.interruptAction.reset(); return kr; diff --git a/ASFWDriver/Shared/Isoch/IsochAudioTransport.hpp b/ASFWDriver/Shared/Isoch/IsochAudioTransport.hpp index 971963e0..98da3e07 100644 --- a/ASFWDriver/Shared/Isoch/IsochAudioTransport.hpp +++ b/ASFWDriver/Shared/Isoch/IsochAudioTransport.hpp @@ -29,6 +29,66 @@ namespace ASFW::IsochTransport { inline constexpr uint32_t kTransportAbiVersion = 4; + +// ============================================================================= +// OHCI isochronous transmit packet-header contract. +// ============================================================================= +// IEEE-1394 isoch tag 0 carries a stream with no CIP header; tag 1 carries a +// standard CIP packet. The Fireface 800 former-generation protocol uses tag 0 +// because its payload is raw PCM24-in-32 (Linux AMDTP CIP_NO_HEADER). +enum class IsochPacketTag : uint8_t { + kNoCipHeader = 0, + kCip = 1, +}; + +inline constexpr uint8_t kIsochSpeedS400 = 2; +inline constexpr uint8_t kIsochDataBlockTCode = 0xA; + +[[nodiscard]] constexpr uint32_t BuildIsochTxHeaderQ0( + IsochPacketTag tag, + uint8_t speedCode = kIsochSpeedS400, + uint8_t sy = 0) noexcept { + return (static_cast(speedCode & 0x7U) << 16U) | + (static_cast(tag) << 14U) | + (static_cast(kIsochDataBlockTCode) << 4U) | + static_cast(sy & 0xFU); +} + +[[nodiscard]] constexpr uint32_t BuildIsochTxHeaderQ1( + uint32_t payloadLength) noexcept { + return (payloadLength & 0xFFFFU) << 16U; +} + +[[nodiscard]] constexpr uint8_t DecodeIsochTxHeaderSpeed( + uint32_t headerQ0) noexcept { + return static_cast((headerQ0 >> 16U) & 0x7U); +} + +[[nodiscard]] constexpr IsochPacketTag DecodeIsochTxHeaderTag( + uint32_t headerQ0) noexcept { + return static_cast((headerQ0 >> 14U) & 0x3U); +} + +[[nodiscard]] constexpr uint8_t DecodeIsochTxHeaderTCode( + uint32_t headerQ0) noexcept { + return static_cast((headerQ0 >> 4U) & 0xFU); +} + +[[nodiscard]] constexpr uint32_t DecodeIsochTxHeaderDataLength( + uint32_t headerQ1) noexcept { + return (headerQ1 >> 16U) & 0xFFFFU; +} + +/// A no-CIP idle cycle is represented by no isochronous packet at all. The +/// OHCI core must program a zero-length standard OUTPUT_LAST skip descriptor, +/// not an immediate isoch header with data_length=0. CIP streams retain their +/// normal header-only packet semantics. +[[nodiscard]] constexpr bool ShouldSkipIsochTxPacket( + IsochPacketTag tag, + uint32_t payloadLength) noexcept { + return tag == IsochPacketTag::kNoCipHeader && payloadLength == 0U; +} + // ============================================================================= // Shared queue / buffer sizing (ADK §3.3 / §6.4). // These constants are the "Iron Rule" ground truth for both sides. @@ -57,7 +117,7 @@ struct alignas(64) TxPacketMeta final { uint32_t immediateHeader[2]; ///< Ready-to-blit OHCI IT header quadlets ///< (OUTPUT_MORE_IMMEDIATE immediate data). ///< The core never interprets these. - uint32_t payloadLength; ///< Bytes: 8 (NO-DATA) or 8 + frames·dbs·4. + uint32_t payloadLength; ///< Bytes: 0 for a true empty no-CIP cycle, raw payload for tag 0, or CIP header + payload for tag 1. uint32_t reserved0; uint64_t packetIndex; ///< Absolute index this entry describes. std::atomic commitGen; ///< Lap-numbered commit marker; 0 = never diff --git a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp index cf406100..48f60851 100644 --- a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp +++ b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp @@ -73,6 +73,12 @@ enum { kMethodRequestUserBusReset = 61, kMethodStartAudioStreaming = 62, kMethodStopAudioStreaming = 63, + + // Stage 6 (dev-only): continuous silent isoch TX cadence, reached only + // via this UserClient/MCP path -- never through real CoreAudio StartIO. + kMethodStartContinuousIsochTxSilence = 64, + kMethodStopContinuousIsochTxSilence = 65, + kMethodGetContinuousIsochTxHealth = 66, // TODO(ASFW-IRM): Remove temporary IRM test method after dedicated validation tooling exists. kMethodTestIRMAllocation = 26, kMethodTestIRMRelease = 27, @@ -340,6 +346,45 @@ MethodDispatchResult DispatchDriverControlMethods(ASFWDriver& driver, ? driver.StartAudioStreaming(*guid) : driver.StopAudioStreaming(*guid)}; } + case kMethodStartContinuousIsochTxSilence: + case kMethodStopContinuousIsochTxSilence: { + // Full 64-bit GUID -- GetFirstScalarInput() above truncates to + // uint32_t, which real Fireface 800 GUIDs (e.g. 0x000a3500f021a1b4) + // do not fit in. + if (!arguments || !arguments->scalarInput || + arguments->scalarInputCount < 1 || arguments->scalarInput[0] == 0) { + return MethodDispatchResult{kIOReturnBadArgument}; + } + const uint64_t guid = arguments->scalarInput[0]; + return MethodDispatchResult{ + selector == kMethodStartContinuousIsochTxSilence + ? driver.StartContinuousIsochTxSilence(guid) + : driver.StopContinuousIsochTxSilence(guid)}; + } + case kMethodGetContinuousIsochTxHealth: { + if (!arguments || !arguments->scalarInput || + arguments->scalarInputCount < 1 || arguments->scalarInput[0] == 0) { + return MethodDispatchResult{kIOReturnBadArgument}; + } + const uint64_t guid = arguments->scalarInput[0]; + bool running = false; + bool dead = false; + bool eventError = false; + uint32_t anchorExecutions = 0U; + uint16_t lastAnchorTimestamp = 0U; + const kern_return_t kr = driver.GetContinuousIsochTxHealth( + guid, &running, &dead, &eventError, &anchorExecutions, + &lastAnchorTimestamp); + if (arguments->scalarOutput && arguments->scalarOutputCount >= 5) { + arguments->scalarOutput[0] = running ? 1U : 0U; + arguments->scalarOutput[1] = dead ? 1U : 0U; + arguments->scalarOutput[2] = eventError ? 1U : 0U; + arguments->scalarOutput[3] = anchorExecutions; + arguments->scalarOutput[4] = lastAnchorTimestamp; + arguments->scalarOutputCount = 5; + } + return MethodDispatchResult{kr}; + } case kMethodGetAudioAutoStart: return HandleGetAudioAutoStart(driver, arguments); case kMethodGetLogConfig: diff --git a/project.yml b/project.yml index 3048aafb..2ee77c6d 100644 --- a/project.yml +++ b/project.yml @@ -157,7 +157,15 @@ targets: SWIFT_EMIT_LOC_STRINGS: YES SWIFT_VERSION: "6.0" MARKETING_VERSION: "0.2.0" - CURRENT_PROJECT_VERSION: 2 + # Bumped from 2: every rebuild-during-iteration otherwise produces a + # dext with the same CFBundleVersion as whatever is already active, + # and OSSystemExtensionRequest won't replace an equal-or-lower + # version -- the only way forward was an explicit + # systemextensionsctl uninstall, which got stuck in "terminating for + # uninstall but still running" (fixed only by a full reboot) + # repeatedly during Stage 6 development. A comfortably high fixed + # value means every future install is a normal automatic upgrade. + CURRENT_PROJECT_VERSION: 100 configs: Debug: SWIFT_OPTIMIZATION_LEVEL: -Onone diff --git a/tests/devices/CMakeLists.txt b/tests/devices/CMakeLists.txt index 193fd7c9..571517b7 100644 --- a/tests/devices/CMakeLists.txt +++ b/tests/devices/CMakeLists.txt @@ -53,6 +53,7 @@ add_device_test(AudioDuplexCoordinatorTests "${ASFW_DRIVER_DIR}/Audio/Core/AudioRuntimeRegistry.cpp" "${ASFW_DRIVER_DIR}/Discovery/DeviceRegistry.cpp" "${ASFW_DRIVER_DIR}/Bus/IRM/IRMClient.cpp" + "${ASFW_DRIVER_DIR}/ConfigROM/Parse/ConfigROMParser.cpp" ) # FW-74: resolver-owned duplex stream geometry, wire-format quirks, and host recipe. diff --git a/tests/protocols/CMakeLists.txt b/tests/protocols/CMakeLists.txt index b96252eb..2610230a 100644 --- a/tests/protocols/CMakeLists.txt +++ b/tests/protocols/CMakeLists.txt @@ -242,6 +242,7 @@ add_protocols_test(SessionRegistryTests "${ASFW_DRIVER_DIR}/Discovery/FWDevice.cpp" "${ASFW_DRIVER_DIR}/Discovery/FWUnit.cpp" "${ASFW_DRIVER_DIR}/Discovery/SpeedPolicy.cpp" + "${ASFW_DRIVER_DIR}/ConfigROM/Parse/ConfigROMParser.cpp" ) # SBP2Handler Tests (user-client boundary: session state + command ABI hardening) @@ -258,4 +259,5 @@ add_protocols_test(SBP2HandlerTests "${ASFW_DRIVER_DIR}/Discovery/FWDevice.cpp" "${ASFW_DRIVER_DIR}/Discovery/FWUnit.cpp" "${ASFW_DRIVER_DIR}/Discovery/SpeedPolicy.cpp" + "${ASFW_DRIVER_DIR}/ConfigROM/Parse/ConfigROMParser.cpp" )