|
1 | 1 | #pragma once |
2 | 2 |
|
3 | 3 | #include <audioapi/core/utils/graph/AudioGraph.hpp> |
| 4 | +#include <audioapi/core/utils/graph/BridgeNode.hpp> |
4 | 5 | #include <audioapi/core/utils/graph/Disposer.hpp> |
5 | 6 | #include <audioapi/core/utils/graph/HostGraph.hpp> |
6 | 7 | #include <audioapi/core/utils/graph/InputPool.hpp> |
|
13 | 14 | #include <algorithm> |
14 | 15 | #include <cstdint> |
15 | 16 | #include <memory> |
| 17 | +#include <unordered_map> |
16 | 18 | #include <utility> |
17 | 19 |
|
| 20 | +namespace audioapi { |
| 21 | +class AudioParam; |
| 22 | +} |
| 23 | + |
18 | 24 | namespace audioapi::utils::graph { |
19 | 25 |
|
20 | 26 | /// @brief Thread-safe graph coordinator that bridges HostGraph (main thread) |
@@ -165,6 +171,115 @@ class Graph { |
165 | 171 | }); |
166 | 172 | } |
167 | 173 |
|
| 174 | + // ── Param bridge API ─────────────────────────────────────────────────── |
| 175 | + |
| 176 | + /// @brief Creates a bridge node representing: source → bridge → owner. |
| 177 | + /// |
| 178 | + /// The bridge encodes a param connection in the graph for cycle detection |
| 179 | + /// and topological ordering. The bridge itself is not processable. |
| 180 | + /// |
| 181 | + /// @param source the node whose output feeds the param |
| 182 | + /// @param owner the node that owns the param |
| 183 | + /// @param param raw pointer to the AudioParam (lifetime guaranteed by owner) |
| 184 | + /// @return Ok on success, Err on cycle/duplicate/not-found |
| 185 | + Res connectParam(HNode *source, HNode *owner, AudioParam *param) { |
| 186 | + hostGraph.collectDisposedNodes(); |
| 187 | + |
| 188 | + BridgeKey key{source, param}; |
| 189 | + if (bridgeMap_.count(key)) { |
| 190 | + return Res::Err(ResultError::EDGE_ALREADY_EXISTS); |
| 191 | + } |
| 192 | + |
| 193 | + // Create bridge node |
| 194 | + auto bridgeObj = std::make_unique<BridgeNode>(param); |
| 195 | + auto bridgeHandle = std::make_shared<NodeHandle>(0, std::move(bridgeObj)); |
| 196 | + auto [bridgeHostNode, addEvent] = hostGraph.addNode(bridgeHandle); |
| 197 | + |
| 198 | + // source → bridge |
| 199 | + auto edgeRes1 = hostGraph.addEdge(source, bridgeHostNode); |
| 200 | + if (edgeRes1.is_err()) { |
| 201 | + // Rollback: remove bridge node |
| 202 | + (void)hostGraph.removeNode(bridgeHostNode); |
| 203 | + return Res::Err(edgeRes1.unwrap_err()); |
| 204 | + } |
| 205 | + |
| 206 | + // bridge → owner |
| 207 | + auto edgeRes2 = hostGraph.addEdge(bridgeHostNode, owner); |
| 208 | + if (edgeRes2.is_err()) { |
| 209 | + // Rollback: remove source→bridge edge and bridge node |
| 210 | + (void)hostGraph.removeEdge(source, bridgeHostNode); |
| 211 | + (void)hostGraph.removeNode(bridgeHostNode); |
| 212 | + return Res::Err(edgeRes2.unwrap_err()); |
| 213 | + } |
| 214 | + |
| 215 | + // All succeeded — send events through SPSC |
| 216 | + sendNodeGrowIfNeeded(); |
| 217 | + eventSender_.send(std::move(addEvent)); |
| 218 | + |
| 219 | + sendPoolGrowIfNeeded(); |
| 220 | + eventSender_.send(std::move(edgeRes1).unwrap()); |
| 221 | + |
| 222 | + sendPoolGrowIfNeeded(); |
| 223 | + eventSender_.send(std::move(edgeRes2).unwrap()); |
| 224 | + |
| 225 | + // Track bridge |
| 226 | + bridgeMap_[key] = bridgeHostNode; |
| 227 | + bridgeOwners_[bridgeHostNode] = owner; |
| 228 | + |
| 229 | + return Res::Ok(NoneType{}); |
| 230 | + } |
| 231 | + |
| 232 | + /// @brief Removes a bridge node for the given (source, param) pair. |
| 233 | + Res disconnectParam(HNode *source, HNode * /*owner*/, AudioParam *param) { |
| 234 | + hostGraph.collectDisposedNodes(); |
| 235 | + |
| 236 | + BridgeKey key{source, param}; |
| 237 | + auto it = bridgeMap_.find(key); |
| 238 | + if (it == bridgeMap_.end()) { |
| 239 | + return Res::Err(ResultError::EDGE_NOT_FOUND); |
| 240 | + } |
| 241 | + |
| 242 | + HNode *bridge = it->second; |
| 243 | + removeBridge(source, bridge); |
| 244 | + bridgeMap_.erase(it); |
| 245 | + |
| 246 | + return Res::Ok(NoneType{}); |
| 247 | + } |
| 248 | + |
| 249 | + /// @brief Removes a node and cascade-removes any bridges where this node |
| 250 | + /// is the source or owner. |
| 251 | + Res removeNodeWithBridges(HNode *node) { |
| 252 | + hostGraph.collectDisposedNodes(); |
| 253 | + |
| 254 | + // Cascade: remove bridges where this node is source |
| 255 | + for (auto it = bridgeMap_.begin(); it != bridgeMap_.end();) { |
| 256 | + if (it->first.source == node) { |
| 257 | + HNode *bridge = it->second; |
| 258 | + removeBridge(node, bridge); |
| 259 | + bridgeOwners_.erase(bridge); |
| 260 | + it = bridgeMap_.erase(it); |
| 261 | + } else { |
| 262 | + ++it; |
| 263 | + } |
| 264 | + } |
| 265 | + |
| 266 | + // Cascade: remove bridges where this node is owner |
| 267 | + for (auto it = bridgeMap_.begin(); it != bridgeMap_.end();) { |
| 268 | + auto ownerIt = bridgeOwners_.find(it->second); |
| 269 | + if (ownerIt != bridgeOwners_.end() && ownerIt->second == node) { |
| 270 | + HNode *bridge = it->second; |
| 271 | + HNode *source = it->first.source; |
| 272 | + removeBridge(source, bridge); |
| 273 | + bridgeOwners_.erase(ownerIt); |
| 274 | + it = bridgeMap_.erase(it); |
| 275 | + } else { |
| 276 | + ++it; |
| 277 | + } |
| 278 | + } |
| 279 | + |
| 280 | + return removeNode(node); |
| 281 | + } |
| 282 | + |
168 | 283 | private: |
169 | 284 | static constexpr size_t kDisposerPayloadSize = HostGraph::kDisposerPayloadSize; |
170 | 285 |
|
@@ -223,6 +338,57 @@ class Graph { |
223 | 338 | } |
224 | 339 | } |
225 | 340 |
|
| 341 | + // ── Bridge tracking (main thread only) ────────────────────────────────── |
| 342 | + |
| 343 | + struct BridgeKey { |
| 344 | + HNode *source; |
| 345 | + AudioParam *param; |
| 346 | + |
| 347 | + bool operator==(const BridgeKey &other) const { |
| 348 | + return source == other.source && param == other.param; |
| 349 | + } |
| 350 | + }; |
| 351 | + |
| 352 | + struct BridgeKeyHash { |
| 353 | + size_t operator()(const BridgeKey &k) const { |
| 354 | + auto h1 = std::hash<HNode *>{}(k.source); |
| 355 | + auto h2 = std::hash<AudioParam *>{}(k.param); |
| 356 | + return h1 ^ (h2 << 1); |
| 357 | + } |
| 358 | + }; |
| 359 | + |
| 360 | + /// Maps (source, param) → bridge host node |
| 361 | + std::unordered_map<BridgeKey, HNode *, BridgeKeyHash> bridgeMap_; |
| 362 | + |
| 363 | + /// Maps bridge host node → owner host node (for cascade removal) |
| 364 | + std::unordered_map<HNode *, HNode *> bridgeOwners_; |
| 365 | + |
| 366 | + /// @brief Removes a bridge node: tears down edges and marks for removal. |
| 367 | + void removeBridge(HNode *source, HNode *bridge) { |
| 368 | + // Find the owner from bridgeOwners_ |
| 369 | + auto ownerIt = bridgeOwners_.find(bridge); |
| 370 | + HNode *owner = (ownerIt != bridgeOwners_.end()) ? ownerIt->second : nullptr; |
| 371 | + |
| 372 | + // Remove edges: source→bridge, bridge→owner |
| 373 | + auto res1 = hostGraph.removeEdge(source, bridge); |
| 374 | + if (res1.is_ok()) { |
| 375 | + eventSender_.send(std::move(res1).unwrap()); |
| 376 | + } |
| 377 | + |
| 378 | + if (owner) { |
| 379 | + auto res2 = hostGraph.removeEdge(bridge, owner); |
| 380 | + if (res2.is_ok()) { |
| 381 | + eventSender_.send(std::move(res2).unwrap()); |
| 382 | + } |
| 383 | + } |
| 384 | + |
| 385 | + // Remove bridge node |
| 386 | + auto res3 = hostGraph.removeNode(bridge); |
| 387 | + if (res3.is_ok()) { |
| 388 | + eventSender_.send(std::move(res3).unwrap()); |
| 389 | + } |
| 390 | + } |
| 391 | + |
226 | 392 | friend class GraphTest; |
227 | 393 | }; |
228 | 394 |
|
|
0 commit comments