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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#include <audioapi/core/utils/graph/AudioGraph.h>
#include <utility>
#include <algorithm>

namespace audioapi::utils::graph {

AudioGraph::AudioGraph() {
first_free_slot = 0;
}

AudioGraph::AudioGraph(AudioGraph&& other) noexcept
: nodes(std::move(other.nodes)),
executionOrder(std::move(other.executionOrder)),
first_free_slot(other.first_free_slot),
isDirty(other.isDirty) {
other.isDirty = false;
other.first_free_slot = 0;
}

AudioGraph& AudioGraph::operator=(AudioGraph&& other) noexcept {
if (this != &other) {
nodes = std::move(other.nodes);
executionOrder = std::move(other.executionOrder);
isDirty = other.isDirty;
first_free_slot = other.first_free_slot;
other.isDirty = false;
other.first_free_slot = 0;
}
return *this;
}

uint32_t AudioGraph::createNode() {
// Check if we have a free slot
// If first_free_slot is within bounds, it's a hole.
// If first_free_slot == size, we are at end.
if (first_free_slot < static_cast<int32_t>(nodes.size())) {
uint32_t idx = static_cast<uint32_t>(first_free_slot);

// Move head to next
first_free_slot = nodes[idx].next_free_slot;

// Mark as taken
nodes[idx].next_free_slot = -1;
// Reset state
nodes[idx].inputs.clear();
nodes[idx].topo_out_degree = 0;

return idx;
}

// No free slot, append
nodes.emplace_back();
uint32_t idx = static_cast<uint32_t>(nodes.size() - 1);
nodes[idx].next_free_slot = -1; // Active

// Maintain first_free_slot pointing to the "next potential" (which is now new size)
first_free_slot = static_cast<int32_t>(nodes.size());

return idx;
}

void AudioGraph::releaseNode(uint32_t index) {
if (index >= nodes.size()) return;

// Check if already free
if (!nodes[index].isActive()) return;

// Clear data
nodes[index].inputs.clear();

// Add to head of free list
nodes[index].next_free_slot = first_free_slot;
first_free_slot = static_cast<int32_t>(index);
}

void AudioGraph::process() {
if (isDirty) {
recomputeTopologicalOrder();
isDirty = false;
}

// Actually execute. For now this is just structure maintenance.
// In a real audio graph, we would do:
// for (uint32_t idx : executionOrder) {
// nodes[idx].process();
// }
}

void AudioGraph::recomputeTopologicalOrder() {
executionOrder.clear();
if (nodes.empty()) return;
executionOrder.reserve(nodes.size());

// Khan's Algorithm (Reverse Mode on Reversed Graph)
// Our graph stores Inputs (Back-Edges).
// Node U stores [V1, V2] meaning V1->U and V2->U.

// Reverse Kahn: Needs Out-Degree and Backward Edges (V -> U).

// 1. Compute Out-Degree (Number of Dependents) for each node.
// NOTE: here out degree will always be 0 so we do not need to init it to 0

// Iterate all nodes to count usage.
for (const auto& node : nodes) {
if (!node.isActive()) continue;

for (uint32_t inputIdx : node.inputs) {
if (inputIdx < nodes.size() && nodes[inputIdx].isActive()) {
nodes[inputIdx].topo_out_degree++;
}
}
}

// 2. Queue of 0-Out-Degree Nodes (Leaves).
// These are nodes that nothing depends on (or final destinations).
// We collect them into `executionOrder` temporarily acting as queue.
size_t queueStart = 0;

for (size_t i = 0; i < nodes.size(); ++i) {
if (nodes[i].isActive() && nodes[i].topo_out_degree == 0) {
executionOrder.push_back(static_cast<uint32_t>(i));
}
}

// 3. Process Queue
while (queueStart < executionOrder.size()) {
uint32_t vIdx = executionOrder[queueStart++];

// For each node U that is an input of V (U -> V)
// Since we process leaves first (V is a leaf in the remaining sub-graph),
// V is 'satisfied' or 'removed'. We reduce the Out-Degree of U.

const Node& v = nodes[vIdx];
for (uint32_t uIdx : v.inputs) {
if (uIdx < nodes.size()) {
Node& u = nodes[uIdx];
if (u.isActive()) {
if (u.topo_out_degree > 0) {
u.topo_out_degree--;
if (u.topo_out_degree == 0) {
executionOrder.push_back(uIdx);
}
}
}
}
}
}

// We now have a Reverse Topological Order in `executionOrder`.
// Leaf (Destination) -> Source.
// We want Source -> Destination for execution (Dependency -> Dependent).
std::reverse(executionOrder.begin(), executionOrder.end());
}

} // namespace audioapi::utils::graph
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#pragma once
#include <concepts>
#include <memory>
#include <vector>

namespace audioapi::utils::graph {

// Forward declarations
class HostGraph;
class AudioGraph;
class TestGraphUtils;

/// @brief AudioGraph is only a structure allowing topological traversal
/// @note it is fully managed by events provided by HostGraph
class AudioGraph {
public:
struct Node {
// std::unique_ptr<AudioNode> audioNode; // The actual audio node data, managed externally by HostGraph events
std::vector<uint32_t> inputs; // indices of input nodes
uint32_t topo_out_degree = 0; // scratch space for topological sort (used as Out-Degree counter)

// Memory pool optimization:
// -1 means the slot is taken (node is active).
// otherwise, it points to the next free slot index.
// if equal to nodes.size(), it is the last free slot.
int32_t next_free_slot = -1;

#if RN_AUDIO_API_TEST
// Identifier for testing purposes only
size_t test_node_identifier__ = 0;
#endif // RN_AUDIO_API_TEST

bool isActive() const {
return next_free_slot == -1;
}
};

AudioGraph();
~AudioGraph() = default;

AudioGraph(const AudioGraph &) = delete;
AudioGraph &operator=(const AudioGraph &) = delete;

AudioGraph(AudioGraph &&other) noexcept;
AudioGraph &operator=(AudioGraph &&other) noexcept;

// The main storage. Be careful with pointer invalidation if resizing.
std::vector<Node> nodes;
std::vector<uint32_t> executionOrder;

// Points to the first free slot in the `nodes` vector.
// If equal to nodes.size(), it means there are no free slots (and we should append).
// Implicitly initialized to 0 (logical empty).
int32_t first_free_slot = 0;

// Helpers
void markDirty() {
isDirty = true;
}
void process(); // Recomputes topo order if dirty

// Allocator helpers
uint32_t createNode();
void releaseNode(uint32_t index);

private:
bool isDirty = false;
friend class HostGraph;
friend class TestGraphUtils;

void recomputeTopologicalOrder();
};

} // namespace audioapi::utils::graph
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

#include <audioapi/core/utils/graph/AudioGraph.h>

namespace audioapi::utils::graph {

class Disposer {
public:
virtual ~Disposer() = default;

/// @brief Disposes the given audio node.
/// @param node Pointer to the AudioGraph::Node to be disposed.
virtual void dispose(AudioGraph::Node *node) = 0;
};

} // namespace audioapi::utils::graph
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#pragma once

#include <audioapi/core/utils/graph/AudioGraph.h>
#include <audioapi/core/utils/graph/Disposer.hpp>
#include <audioapi/core/utils/graph/HostGraph.h>

#include <audioapi/utils/FatFunction.hpp>
#include <audioapi/utils/SpscChannel.hpp>

#include <audioapi/utils/Result.hpp>

#include <memory>
#include <utility>

namespace audioapi::utils::graph {

class Graph {
using Receiver = audioapi::channels::spsc::Receiver<
HostGraph::AGEvent,
audioapi::channels::spsc::OverflowStrategy::WAIT_ON_FULL,
audioapi::channels::spsc::WaitStrategy::BUSY_LOOP>;
using Sender = audioapi::channels::spsc::Sender<
HostGraph::AGEvent,
audioapi::channels::spsc::OverflowStrategy::WAIT_ON_FULL,
audioapi::channels::spsc::WaitStrategy::BUSY_LOOP>;

using HNode = HostGraph::Node;
using ANode = AudioGraph::Node;

public:
using ResultError = HostGraph::ResultError;
using Res = Result<NoneType, ResultError>;

Graph(size_t eventQueueCapacity, std::unique_ptr<Disposer> disposer) {
auto [sender, receiver] = audioapi::channels::spsc::channel<
HostGraph::AGEvent,
audioapi::channels::spsc::OverflowStrategy::WAIT_ON_FULL,
audioapi::channels::spsc::WaitStrategy::BUSY_LOOP>(eventQueueCapacity);
eventSender = std::move(sender);
eventReceiver = std::move(receiver);
this->disposer = std::move(disposer);
}
~Graph() = default;

/// @brief Processes all scheduled events
/// @note IMPORTANT: Should be called only from the audio thread
void processEvents() {
HostGraph::AGEvent event;
while (eventReceiver.try_receive(event) == audioapi::channels::spsc::ResponseStatus::SUCCESS) {
if (event) {
event(audioGraph, *disposer);
}
}
}

/// @brief Adds a new node to the graph and returns a pointer to it.
/// @return pointer to the newly added node
/// TODO: in future it will get parameters for node creation
HNode *addNode() {
uint32_t audioNodeIndex = audioGraph.createNode();
auto [hostNode, event] = hostGraph.addNode(audioNodeIndex);
eventSender.send(std::move(event));
return hostNode;
}

/// @brief Removes a node and all its edges from the graph. Does nothing if the node does not exist.
/// @param node pointer to the node to be removed
/// @return Result indicating success or failure (e.g., if node was not found)
/// @note This will also destroy this HostGraph::Node and dealocate its memory, so the pointer will become invalid after this call. Be careful with dangling pointers if you keep references to nodes outside of HostGraph.
Res removeNode(HNode *node) {
return hostGraph.removeNode(node).map([&](HostGraph::AGEvent event) {
eventSender.send(std::move(event));
return NoneType{};
});
}

/// @brief Adds an edge from `from` to `to` if it does not create a cycle.
/// @param from
/// @param to
/// @return Result indicating success or failure (e.g., if edge would create a cycle)
Res addEdge(HNode *from, HNode *to) {
return hostGraph.addEdge(from, to).map([&](HostGraph::AGEvent event) {
eventSender.send(std::move(event));
return NoneType{};
});
}

/// @brief Removes an edge from `from` to `to`. Does nothing if the edge does not exist.
/// @param from
/// @param to
/// @return Result indicating success or failure (e.g., if edge was not found)
Res removeEdge(HNode *from, HNode *to) {
return hostGraph.removeEdge(from, to).map([&](HostGraph::AGEvent event) {
eventSender.send(std::move(event));
return NoneType{};
});
}

private:
// Aligning to cache line size to prevent false sharing between audio and main thread
alignas(64) AudioGraph audioGraph;
alignas(64) HostGraph hostGraph;
alignas(64) std::unique_ptr<Disposer> disposer;

// These are const and their memory won't be modified after initialization, so no false sharing here
Sender eventSender;
Receiver eventReceiver;

friend class GraphTest;
};

} // namespace audioapi::utils::graph
Loading