Files
3d_audio/lib_audio_dsp/python/audio_dsp/design/pipeline.py
Steven Dan d8b2974133 init
2025-12-11 09:43:42 +08:00

1180 lines
47 KiB
Python

# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
"""Top level pipeline design class and code generation functions."""
from enum import IntEnum
from pathlib import Path
from audio_dsp.design.composite_stage import CompositeStage
from audio_dsp.design.pipeline_executor import PipelineExecutor, PipelineView
from audio_dsp.design.graph import Graph
from audio_dsp.design.stage import Stage, StageOutput, StageOutputList, find_config
from audio_dsp.design.thread import Thread
from IPython import display
import hashlib
import json
from uuid import uuid4
from ._draw import new_record_digraph
from functools import wraps
from typing import NamedTuple, Type
class _ResolvedEdge(NamedTuple):
"""Resolved representation of an edge, used in code gen."""
source: tuple[int, int]
dest: tuple[int, int]
fs: float
frame_size: int
def callonce(f):
"""Decorate functions to ensure they only execute once despite being called multiple times."""
attr_name = "_called_funcs"
def called_funcs_of_instance(instance) -> set:
called_funcs = getattr(instance, attr_name, set())
if not called_funcs:
setattr(instance, attr_name, called_funcs)
return called_funcs
@wraps(f)
def wrapper(*args, **kwargs):
self = args[0]
called_funcs = called_funcs_of_instance(self)
if f not in called_funcs:
called_funcs.add(f)
wrapper.called = True
return f(*args, **kwargs)
return wrapper
class PipelineStage(Stage):
"""
Stage for the pipelne. Does not support processing of data through it. Only
used for pipeline level control commands, for example, querying the pipeline
checksum.
"""
def __init__(self, **kwargs):
super().__init__(config=find_config("pipeline"), **kwargs)
self.create_outputs(0)
def add_to_dot(self, dot): # Override this to not add the stage to the diagram
"""
Override the CompositeStage.add_to_dot() function to ensure PipelineStage
type stages are not added to the dot diagram.
Parameters
----------
dot : graphviz.Diagraph
dot instance to add edges to.
"""
return
class Pipeline:
"""
Top level class which is a container for a list of threads that
are connected in series.
Parameters
----------
n_in : int
Number of input channels into the pipeline
identifier: string
Unique identifier for this pipeline. This identifier will be included in
the generated header file name (as "adsp_generated_<identifier>.h"), the
generated source file name (as "adsp_generated_<identifier>.c"), and the
pipeline's generated initialisation and main functions (as
"adsp_<identifier>_pipeline_init" and
"adsp_<identifier>_pipeline_main")
frame_size : int
Size of the input frame of all input channels
fs : int
Sample rate of the input channels
generate_xscope_task : bool
Determines whether the generated pipeline automatically instantiates a
task to handle tuning over xscope. False by default. If False, the
application code will need to explicitly call the
"adsp_control_xscope_*" functions defined in adsp_control.h in order to
handle tuning over xscope, such as that undertaken by the
XScopeTransport() class.
Attributes
----------
i : list(StageOutput)
The inputs to the pipeline should be passed as the inputs to the
first stages in the pipeline
threads : list(Thread)
List of all the threads in the pipeline
pipeline_stage : PipelineStage | None
Stage corresponding to the pipeline. Needed for handling
pipeline level control commands
"""
def __init__(
self, n_in, identifier="auto", frame_size=1, fs=48000, generate_xscope_task=False
):
self._graph = Graph()
self.threads = []
self._n_in = n_in
self._n_out = 0
self._id = identifier
self.pipeline_stage: None | PipelineStage
self._labelled_stages = {}
self._generate_xscope_task = generate_xscope_task
self.frame_size = frame_size
self.fs = fs
self.stage_dict = {}
self.i = StageOutputList([StageOutput(fs=fs, frame_size=frame_size) for _ in range(n_in)])
self.o: StageOutputList | None = None
for i, input in enumerate(self.i.edges):
self._graph.add_edge(input)
input.source_index = i
self.next_thread()
@staticmethod
def begin(n_in, identifier="auto", frame_size=1, fs=48000, generate_xscope_task=False):
"""Create a new Pipeline and get the attributes required for design.
Returns
-------
Pipeline, Thread, StageOutputList
The pipeline instance, the initial thread and the pipeline input edges.
"""
p = Pipeline(n_in, identifier, frame_size, fs, generate_xscope_task)
return p, p.i
def _add_thread(self) -> Thread:
"""
Create a new instance of audio_dsp.thread.Thread and add it to
the pipeline. Stages can then be instantiated in the thread.
Returns
-------
Thread
a new thread instance.
"""
ret = Thread(id=len(self.threads), graph=self._graph)
self.add_pipeline_stage(ret)
ret.add_thread_stage()
self.threads.append(ret)
return ret
def next_thread(self) -> None:
"""
Update the thread which stages will be added to.
This will always create a new thread.
"""
thread = self._add_thread()
self._current_thread = thread
def stage(
self,
stage_type: Type[Stage | CompositeStage],
inputs: StageOutputList,
label: str | None = None,
thread: int | None = None,
**kwargs,
) -> StageOutputList:
"""
Add a new stage to the pipeline.
Parameters
----------
stage_type
The type of stage to add.
inputs
A StageOutputList containing edges in this pipeline.
label
An optional label that can be used for tuning and will also be converted
into a macro in the generated pipeline. Label must be set if tuning or
run time control is required for this stage.
"""
# keep a running count of the number of each stage type in the pipeline
type_name = stage_type.__name__
if type_name not in self.stage_dict:
self.stage_dict[type_name] = 1
else:
self.stage_dict[type_name] += 1
# If label is None, use the type name and count to generate a label
# If a label is added later, this should keep subsequent labels the same
if label is None:
label = f"{type_name}_{self.stage_dict[type_name] - 1}"
if thread is not None:
s = self.threads[thread].stage(stage_type, inputs, label=label, **kwargs)
else:
s = self._current_thread.stage(stage_type, inputs, label=label, **kwargs)
if label in self._labelled_stages:
raise RuntimeError(f"Label {label} is already in use.")
self._labelled_stages[label] = s
return s.o
def __getitem__(self, key: str):
"""Get the labelled stage from the pipeline."""
return self._labelled_stages[key]
@callonce
def add_pipeline_stage(self, thread):
"""Add a PipelineStage stage for the pipeline."""
self.pipeline_stage = thread.stage(PipelineStage, StageOutputList())
def set_outputs(self, output_edges: StageOutputList):
"""
Set the pipeline outputs, configures the output channel index.
Parameters
----------
output_edges : Iterable(None | StageOutput)
configure the output channels and their indices. Outputs of the pipeline
will be in the same indices as the input to this function. To have an empty
output index, pass in None.
"""
if not output_edges:
raise RuntimeError("Pipeline must have at least 1 output")
i = -1
for i, edge in enumerate(output_edges.edges):
if edge is not None:
edge.dest_index = i
self.o = output_edges
if len(self.o.edges) >= 1:
thread_crossings = []
for edge in output_edges.edges:
thread_crossings.append(len(set(edge.crossings))) # pyright: ignore checked above
if not all(x == thread_crossings[0] for x in thread_crossings):
input_msg = "\n"
for i, edge in enumerate(self.o.edges):
crossings_set = set(edge.crossings) # pyright: ignore checked above
if not crossings_set:
input_msg += f"Output {i} does not cross any threads.\n"
else:
input_msg += f"Output {i} crosses threads {crossings_set}.\n"
raise RuntimeError(
f"\nAll edges passed to pipeline.set_outputs"
" must cross the same number of threads.\n"
f"Currently, outputs cross {thread_crossings} threads.\nOutputs with less than "
f"{max(thread_crossings)} thread crossings must pass through Stages on"
" earlier threads to avoid a latency mismatch and thread blocking.\n"
"A Bypass Stage can be added on an earlier thread if no additional DSP is needed."
+ input_msg
)
self._n_out = i + 1
self.resolve_pipeline() # Call it here to generate the pipeline hash
def executor(self) -> PipelineExecutor:
"""Create an executor instance which can be used to simulate the pipeline."""
def view():
if self.o is None:
raise RuntimeError(
"Pipeline outputs must be set with `set_outputs` before simulating"
)
return PipelineView(self._graph.nodes, self.i.edges, self.o.edges)
return PipelineExecutor(self._graph, view)
def validate(self):
"""
Validate pipeline assumptions.
- Thread connections must not lead to a scenario where the pipeline hangs
- Stages must fit on thread
- feedback must be within a thread (future)
- All edges have the same fs and frame_size (until future enhancements)
"""
# TODO: Implement validation checks
def draw(self, path: Path | None = None):
"""Render a dot diagram of this pipeline.
If `path` is not none then the image will be saved to the named file instead of drawing to the jupyter notebook.
"""
dot = new_record_digraph()
for thread in self.threads:
thread.add_to_dot(dot)
start_label = f"{{ start | {{ {'|'.join(f'<o{i}> {i}' for i in range(self._n_in))} }} }}"
end_label = f"{{ {{ {'|'.join(f'<i{i}> {i}' for i in range(self._n_out))} }} | end }}"
dot.node("start", label=start_label)
dot.node("end", label=end_label)
for e in self._graph.edges:
source = e.source.id.hex if e.source is not None else "start"
source = (
f"{source}:o{e.source_index}:s" # "s" means connect to the "south" of the port
)
dest = e.dest.id.hex if e.dest is not None else "end"
dest = f"{dest}:i{e.dest_index}:n" # "n" means connect to the "north" of the port
if e.dest is None and e.dest_index is None:
# unconnected
dest = uuid4().hex
dot.node(dest, "", shape="point")
dot.edge(source, dest)
if path is None:
display.display_svg(dot)
else:
path = Path(path)
if not path.suffix:
path = path.with_suffix(".png")
dot.format = path.suffix.lstrip(".")
dot.render(path.with_suffix(""))
@property
def stages(self):
"""Flattened list of all the stages in the pipeline."""
return self._graph.nodes[:]
@callonce
def generate_pipeline_hash(self, threads: list, edges: list):
"""
Generate a hash unique to the pipeline and save it in the 'checksum' control field of the
pipeline stage.
Parameters
----------
"threads": list of [[(stage index, stage type name), ...], ...] for all threads in the pipeline
"edges": list of [[[source stage, source index], [dest stage, dest index]], ...] for all edges in the pipeline
"""
def to_tuple(lst):
return tuple(to_tuple(i) if isinstance(i, list) else i for i in lst)
tuple_threads = to_tuple(threads)
tuple_edges = to_tuple(edges)
tuple_thread_edges = (tuple_threads, tuple_edges)
a = (json.dumps(tuple_thread_edges)).encode()
m = hashlib.md5()
m.update(a)
hash_a = m.digest()
hash_values = [i for i in bytearray(hash_a)]
assert self.pipeline_stage is not None # To stop ruff from complaining
self.pipeline_stage["checksum"] = hash_values
# lock the graph now that the hash is generated
self._graph.lock()
def resolve_pipeline(self):
"""
Generate a dictionary with all of the information about the thread.
Actual stage instances not included.
Returns
-------
dict
'identifier': string identifier for the pipeline
"threads": list of [[(stage index, stage type name, stage memory use), ...], ...] for all threads
"edges": list of [[[source stage, source index], [dest stage, dest index]], ...] for all edges
"configs": list of dicts containing stage config for each stage.
"modules": list of stage yaml configs for all types of stage that are present
"labels": dictionary {label: instance_id} defining mapping between the user defined stage labels and the index of the stage
"xscope": bool indicating whether or not to create an xscope task for control
"""
# 1. Order the graph
sorted_nodes = self._graph.sort()
# 2. assign nodes to threads
threads = [[] for _ in range(len(self.threads))]
for i, thread in enumerate(self.threads):
for node in sorted_nodes:
if thread.contains_stage(node):
threads[i].append([node.index, node.name, node.get_required_allocator_size()])
edges = []
for edge in self._graph.edges:
source = (
[edge.source.index, edge.source_index]
if edge.source is not None
else [None, edge.source_index]
)
dest = (
[edge.dest.index, edge.dest_index]
if edge.dest is not None
else [None, edge.dest_index]
)
edges.append(_ResolvedEdge(source, dest, frame_size=edge.frame_size, fs=edge.fs))
self.generate_pipeline_hash(threads, edges)
node_configs = {node.index: node.get_config() for node in self._graph.nodes}
module_definitions = {
node.index: {
"name": node.name,
"yaml_dict": node.yaml_dict,
"constants": node.constants,
}
for node in self._graph.nodes
}
labels = {node.label: node.index for node in self._graph.nodes if node.label is not None}
return {
"identifier": self._id,
"threads": threads,
"edges": edges,
"configs": node_configs,
"modules": module_definitions,
"labels": labels,
"xscope": self._generate_xscope_task,
"frame_size": self.frame_size,
"n_inputs": len(self.i),
"n_outputs": len(self.o),
}
def _filter_edges_by_thread(resolved_pipeline):
"""
Get thread input edges, output edges and internal edges for all threads.
Returns
-------
list[tuple[]]
input_edges, internal_edges, output_edges, dead_edges for all threads in
the resolved pipeline.
input_edges and output_edges are dictionaries of {source_or_dest_thread: [edges]}
internal and dead edges are list of edges.
"""
def dest_in_thread(edge, thread):
return edge[1][0] in (t[0] for t in thread)
def source_in_thread(edge, thread):
return edge[0][0] in (t[0] for t in thread)
ret = []
for thread in resolved_pipeline["threads"]:
input_edges = {}
output_edges = {}
internal_edges = []
dead_edges = []
for edge in resolved_pipeline["edges"]:
sit = source_in_thread(edge, thread)
dit = dest_in_thread(edge, thread)
if sit and dit:
internal_edges.append(edge)
elif (edge[1][0] is None and edge[1][1] is None) and (dit or sit):
# This edge is not connected to an output
dead_edges.append(edge)
elif sit:
if edge[1][0] is None:
# pipeline output
di = "pipeline_out"
else:
for di, dthread in enumerate(resolved_pipeline["threads"]):
if dest_in_thread(edge, dthread):
break
try:
output_edges[di].append(edge)
except KeyError:
output_edges[di] = [edge]
elif dit:
if edge[0][0] is None:
# pipeline input
si = "pipeline_in"
else:
for si, sthread in enumerate(resolved_pipeline["threads"]):
if source_in_thread(edge, sthread):
break
try:
input_edges[si].append(edge)
except KeyError:
input_edges[si] = [edge]
ret.append((input_edges, internal_edges, output_edges, dead_edges))
return ret
def _gen_chan_buf_read(channel, edge, frame_size):
"""Generate the C code to read from a channel."""
return f"chan_in_buf_word({channel}, (uint32_t*){edge}, {frame_size});\n"
def _gen_q31_to_q27(edge, frame_size):
"""Generate the C code to convert from q31 to q27."""
return (
f"for(int idx = 0; idx < {frame_size}; ++idx) {edge}[idx] = adsp_from_q31({edge}[idx]);\n"
)
def _gen_q27_to_q31(channel, edge, frame_size):
"""Generate the C code to convert from q27 to q31 and saturate."""
return f"for(int idx = 0; idx < {frame_size}; ++idx) {edge}[idx] = adsp_to_q31({edge}[idx]);\n"
def _gen_chan_buf_write(channel, edge, frame_size):
"""Generate the C code to write to a channel."""
return f"chan_out_buf_word({channel}, (uint32_t*){edge}, {frame_size});\n"
def _generate_dsp_threads(resolved_pipeline):
"""
Create the source string for all of the dsp threads.
Output looks approximately like the below::
void dsp_thread(void* input_c, chanend_t* output_c, module_states, module_configs) {
int32_t edge0[BLOCK_SIZE];
int32_t edge1[BLOCK_SIZE];
int32_t edge2[BLOCK_SIZE];
int32_t edge3[BLOCK_SIZE];
for(;;) {
do control;
// input from 2 source threads
int read_count = 2;
while(read_count) {
select {
input_c[0]: chan_in_buf(edge0); read_count--;
input_c[1]: chan_in_buf(edge1); read_count--;
default: do control;
}
}
modules[0]->process_sample(
(int32_t*[]){edge0, edge1},
(int32_t*[]){edge2, edge3},
modules[0]->state,
&modules[0]->control
);
chan_out_buf(output_c[0], edge2);
chan_out_buf(output_c[1], edge3);
}
}
"""
all_thread_edges = _filter_edges_by_thread(resolved_pipeline)
file_str = ""
for thread_index, (thread_edges, thread) in enumerate(
zip(all_thread_edges, resolved_pipeline["threads"])
):
func = f"DECLARE_JOB(dsp_{resolved_pipeline['identifier']}_thread{thread_index}, (void**, chanend_t*, module_instance_t**));\n"
func += f"void dsp_{resolved_pipeline['identifier']}_thread{thread_index}(void** c_source, chanend_t* c_dest, module_instance_t** modules) {{\n"
# set high priority thread bit to ensure we get the required
# MIPS
func += "\tlocal_thread_mode_set_bits(thread_mode_high_priority); \n"
in_edges, internal_edges, all_output_edges, dead_edges = thread_edges
is_input_thread = "pipeline_in" in in_edges
all_edges = []
for temp_in_e in in_edges.values():
all_edges.extend(temp_in_e)
all_edges.extend(internal_edges)
for temp_out_e in all_output_edges.values():
all_edges.extend(temp_out_e)
all_edges.extend(dead_edges)
for i, edge in enumerate(all_edges):
func += f"\tint32_t edge{i}[{edge.frame_size}] = {{0}};\n"
# get the dsp_thread stage index in the thread
dsp_thread_index = [i for i, (_, name, _) in enumerate(thread) if name == "dsp_thread"][0]
for stage_thread_index, stage in enumerate(thread):
# thread stages are already ordered during pipeline resolution
input_edges = [edge for edge in all_edges if edge[1][0] == stage[0]]
output_edges = [edge for edge in all_edges if edge[0][0] == stage[0]]
if not (input_edges or output_edges):
# stages with no inputs and outputs also have no process method
# so they don't need input and output variables
continue
else:
if len(input_edges) > 0: # To avoid compilation warnings
input_edges.sort(key=lambda e: e[1][1])
input_edges = ", ".join(f"edge{all_edges.index(e)}" for e in input_edges)
func += f"\tint32_t* stage_{stage_thread_index}_input[] = {{{input_edges}}};\n"
else:
func += f"\tint32_t** stage_{stage_thread_index}_input = NULL;\n"
if len(output_edges) > 0:
output_edges.sort(key=lambda e: e[0][1])
output_edges = ", ".join(f"edge{all_edges.index(e)}" for e in output_edges)
func += (
f"\tint32_t* stage_{stage_thread_index}_output[] = {{{output_edges}}};\n"
)
else:
func += f"\tint32_t** stage_{stage_thread_index}_output = NULL;\n"
func += "\tuint32_t start_ts, end_ts, start_control_ts, control_ticks;\n"
func += "\tbool control_done;\n"
func += "\twhile(1) {\n"
func += "\tcontrol_done = false;\n"
# Each thread must process the pending control requests at least once per loop.
# It will be done once before select to ensure it happens, then in the default
# case of the select so that control will be processed if no audio is playing.
control = ""
for i, (stage_index, name, _) in enumerate(thread):
if resolved_pipeline["modules"][stage_index]["yaml_dict"]:
control += f"\t\t{name}_control(modules[{i}]->state, &modules[{i}]->control);\n"
input_fifos = [o for o in in_edges if o == "pipeline_in"]
channels = [o for o in in_edges if o != "pipeline_in"]
assert not (input_fifos and channels), "Pipeline input cannot be a fifo and a channel"
read = ""
read += f"\tint read_count = {len(in_edges)};\n" # TODO use bitfield and guarded cases to prevent
# the same channel being read twice
if len(in_edges.values()):
read += "\tSELECT_RES(\n"
for i, source in enumerate(in_edges.keys()):
if source == "pipeline_in":
read += f"\t\tCASE_THEN(((adsp_fifo_t*)c_source[{i}])->rx_end, case_{i}),\n"
else:
read += f"\t\tCASE_THEN((chanend_t)c_source[{i}], case_{i}),\n"
read += "\t\tDEFAULT_THEN(do_control)\n"
read += "\t) {\n"
for i, (origin, edges) in enumerate(in_edges.items()):
read += f"\t\tcase_{i}: {{\n"
if origin != "pipeline_in":
for edge in edges:
# do all the chan reads first to avoid blocking
# if origin == "pipeline_in":
read += "\t\t\t" + _gen_chan_buf_read(
f"(chanend_t)c_source[{i}]",
f"edge{all_edges.index(edge)}",
edge.frame_size,
)
else:
read += f"\t\t\tadsp_fifo_read_start(c_source[{i}]);\n"
for edge in edges:
read += f"\t\t\tadsp_fifo_read(c_source[{i}], edge{all_edges.index(edge)}, {4 * edge.frame_size});\n"
read += f"\t\t\tadsp_fifo_read_done(c_source[{i}]);\n"
for edge in edges:
read += "\t\t\t" + _gen_q31_to_q27(
f"edge{all_edges.index(edge)}", edge.frame_size
)
read += "\t\t\tif(!--read_count) break;\n\t\t\telse continue;\n\t\t}\n"
read += "\t\tdo_control: {\n"
read += "\t\tstart_control_ts = get_reference_time();\n"
read += control
read += "\t\tcontrol_done = true;\n"
read += "\t\tcontrol_ticks = get_reference_time() - start_control_ts;\n"
read += "\t\tcontinue; }\n"
read += "\t}\n"
read += "\tif(!control_done){\n"
read += "\t\tstart_control_ts = get_reference_time();\n"
read += control
read += "\t\tcontrol_ticks = get_reference_time() - start_control_ts;\n"
read += "\t}\n"
process = "\tstart_ts = get_reference_time();\n\n"
for stage_thread_index, (stage_index, name, _) in enumerate(thread):
input_edges = [edge for edge in all_edges if edge[1][0] == stage_index]
output_edges = [edge for edge in all_edges if edge[0][0] == stage_index]
# thread stages are already ordered during pipeline resolution
if len(input_edges) > 0 or len(output_edges) > 0:
process += f"\t{name}_process(\n"
process += f"\t\tstage_{stage_thread_index}_input,\n"
process += f"\t\tstage_{stage_thread_index}_output,\n"
process += f"\t\tmodules[{stage_thread_index}]->state);\n"
process += "\n\tend_ts = get_reference_time();\n"
profile = "\tuint32_t process_plus_control_ticks = (end_ts - start_ts) + control_ticks;\n"
profile += f"\tif(process_plus_control_ticks > ((dsp_thread_state_t*)(modules[{dsp_thread_index}]->state))->max_cycles)\n"
profile += "\t{\n"
profile += f"\t\t((dsp_thread_state_t*)(modules[{dsp_thread_index}]->state))->max_cycles = process_plus_control_ticks;\n"
profile += "\t}\n"
out = ""
for out_index, (dest, edges) in enumerate(all_output_edges.items()):
for edge in edges:
# do q format conversion first
if dest == "pipeline_out":
out += "\t" + _gen_q27_to_q31(
f"c_dest[{out_index}]", f"edge{all_edges.index(edge)}", edge.frame_size
)
for edge in edges:
# then send over channels
if dest == "pipeline_out":
out += "\t" + _gen_chan_buf_write(
f"c_dest[{out_index}]", f"edge{all_edges.index(edge)}", edge.frame_size
)
for edge in edges:
# finally do other edges
if dest == "pipeline_out":
pass
else:
out += f"\tchan_out_buf_word(c_dest[{out_index}], (void*)edge{all_edges.index(edge)}, {edge.frame_size});\n"
file_str += func + out + read + process + profile + "\t}\n}\n"
return file_str
def _determine_channels(resolved_pipeline):
"""Create list of the required channels from the resolved pipeline structure."""
all_thread_edges = _filter_edges_by_thread(resolved_pipeline)
ret = []
for s_idx, s_thread_edges in enumerate(all_thread_edges):
s_in_edges, _, _, _ = s_thread_edges
# add pipeline entry channels
for source, edge_list in s_in_edges.items():
if source == "pipeline_in":
ret.append((source, s_idx, len(edge_list)))
for s_idx, s_thread_edges in enumerate(all_thread_edges):
_, _, s_out_edges, _ = s_thread_edges
ret.extend((s_idx, dest, len(edge_list)) for dest, edge_list in s_out_edges.items())
return ret
def _resolved_pipeline_num_modules(resolved_pipeline):
"""Determine total number of module instances in the resolved pipeline across all threads."""
return sum(len(t) for t in resolved_pipeline["threads"])
def _generate_dsp_header(resolved_pipeline, out_dir=Path("build/dsp_pipeline")):
"""Generate "adsp_generated_<x>.h" and save to disk."""
out_dir = Path(out_dir)
out_dir.mkdir(exist_ok=True)
header = (
"#pragma once\n"
"#include <stages/adsp_pipeline.h>\n"
"#include <xcore/parallel.h>\n"
"\n"
f"#define ADSP_{resolved_pipeline['identifier'].upper()}_FRAME_SIZE ({resolved_pipeline['frame_size']})\n"
f"#define ADSP_{resolved_pipeline['identifier'].upper()}_N_INPUTS ({resolved_pipeline['n_inputs']})\n"
f"#define ADSP_{resolved_pipeline['identifier'].upper()}_N_OUTPUTS ({resolved_pipeline['n_outputs']})\n\n"
f"/// Autogenerated. Initialises the DSP pipeline.\n"
f"/// @retval A pointer to the initialised DSP pipeline.\n"
f"adsp_pipeline_t * adsp_{resolved_pipeline['identifier']}_pipeline_init();\n\n"
f"DECLARE_JOB(adsp_{resolved_pipeline['identifier']}_pipeline_main, (adsp_pipeline_t*));\n\n"
f"/// Autogenerated main function for the DSP pipeline\n"
f"///\n"
f"/// @param adsp The initialised pipeline.\n"
f"void adsp_{resolved_pipeline['identifier']}_pipeline_main(adsp_pipeline_t* adsp);\n\n"
f"/// Autogenerated. Prints the maximum ticks for each thread in the DSP pipeline.\n"
f"/// This function must be called from the control thread. It cannot be called \n"
f"/// from the DSP thread.\n"
f"void adsp_{resolved_pipeline['identifier']}_print_thread_max_ticks(void);\n\n"
f"/// Autogenerated. Prints the maximum ticks for each thread in the DSP pipeline.\n"
f"/// This function can be called from the same thread as the DSP pipeline.\n"
f"///\n"
f"/// @param adsp The initialised pipeline.\n"
f"void adsp_{resolved_pipeline['identifier']}_fast_print_thread_max_ticks(adsp_pipeline_t* adsp);\n"
)
(out_dir / f"adsp_generated_{resolved_pipeline['identifier']}.h").write_text(header)
def _generate_instance_id_defines(resolved_pipeline, out_dir=Path("build/dsp_pipeline")):
"""Generate "adsp_instance_id.h" that defines the stage indexes for stages labelled by the user and save to disk."""
out_dir = Path(out_dir)
out_dir.mkdir(exist_ok=True)
pipeline_id = resolved_pipeline["identifier"]
n_threads = len(resolved_pipeline["threads"])
header = "#pragma once\n\n"
for label, index in resolved_pipeline["labels"].items():
header += f"#define {label}_stage_index\t\t({index})\n"
thread_stage_ids = ", ".join(f"thread{i}_stage_index" for i in range(n_threads))
header += f"#define {pipeline_id}_thread_stage_indices {{ {thread_stage_ids} }}\n"
(out_dir / f"adsp_instance_id_{pipeline_id}.h").write_text(header)
def _generate_dsp_max_thread_ticks(resolved_pipeline):
"""Generate a function to print the max ticks for each thread."""
identifier = resolved_pipeline["identifier"]
n_threads = len(resolved_pipeline["threads"])
read_threads_code = "\n\t".join(
f"do_read(thread{i}_stage_index, CMD_DSP_THREAD_MAX_CYCLES, sizeof(int), &thread_ticks[{i}]);"
for i in range(n_threads)
)
fmt_str = "\\n".join(f"{i}:\\t%d" for i in range(n_threads))
fmt_args = ", ".join(f"thread_ticks[{i}]" for i in range(n_threads))
ret = f"""
#include "adsp_instance_id_{identifier}.h"
#include <stdio.h>
static void do_read(int instance, int cmd_id, int size, void* data) {{
adsp_stage_control_cmd_t cmd = {{
.instance_id = instance,
.cmd_id = cmd_id,
.payload_len = size,
.payload = data
}};
xassert(m_control);
for(;;) {{
adsp_control_status_t ret = adsp_read_module_config(
m_control,
&cmd);
if(ADSP_CONTROL_SUCCESS == ret) {{
return;
}}
}}
}}
void adsp_{identifier}_print_thread_max_ticks(void) {{
int thread_ticks[{n_threads}];
{read_threads_code}
printf("DSP Thread Ticks:\\n{fmt_str}\\n", {fmt_args});
}}
"""
ret += f"void adsp_{identifier}_fast_print_thread_max_ticks(adsp_pipeline_t* adsp) {{\n"
for i in range(n_threads):
ret += f" module_instance_t module_ptr_{i} = (module_instance_t)adsp->modules[thread{i}_stage_index];\n"
ret += f" uint32_t ticks_{i} = ((dsp_thread_state_t *)(module_ptr_{i}.state)) -> max_cycles;\n"
ret += f' printstr("Thread {i} DSP ticks: ");\n'
ret += f" printuintln(ticks_{i});\n"
ret += "}\n\n"
return ret
def _generate_dsp_init(resolved_pipeline):
"""Create the init function which initialised all modules and channels."""
chans = _determine_channels(resolved_pipeline)
adsp = f"adsp_{resolved_pipeline['identifier']}"
ret = f"adsp_pipeline_t * {adsp}_pipeline_init() {{\n"
ret += f"\tstatic adsp_pipeline_t {adsp};\n"
ret += f"\tstatic adsp_controller_t {adsp}_controller;\n"
ret += f"\tm_control = &{adsp}_controller;\n"
# Track the number of channels so we can initialise them
input_channels = 0
input_channel_edge_counts = []
link_channels = 0
output_channels = 0
frame_size = resolved_pipeline["frame_size"]
for chan_s, chan_d, chan_num_edges in chans:
# We assume that there is no channel pipeline_in -> pipeline_out
if chan_s == "pipeline_in":
input_channels += 1
input_channel_edge_counts.append(chan_num_edges)
elif chan_d == "pipeline_out":
output_channels += 1
else:
link_channels += 1
ret += f"\tstatic adsp_fifo_t {adsp}_in_chans[{input_channels}];\n"
ret += f"\tstatic channel_t {adsp}_out_chans[{output_channels}];\n"
ret += f"\tstatic channel_t {adsp}_link_chans[{link_channels}];\n"
num_modules = _resolved_pipeline_num_modules(resolved_pipeline)
ret += f"\tstatic module_instance_t {adsp}_modules[{num_modules}];\n"
# We assume that this function generates the arrays adsp_<x>_(in|out)_mux_cfgs
# and that it will initialise the .(input|output)_mux members of adsp
ret += _generate_dsp_muxes(resolved_pipeline)
for chan in range(input_channels):
ret += f"\tstatic int32_t in_buf_{chan}[{frame_size * input_channel_edge_counts[chan]}];\n"
ret += f"\tadsp_fifo_init(&{adsp}_in_chans[{chan}], in_buf_{chan});\n"
for chan in range(output_channels):
ret += f"\t{adsp}_out_chans[{chan}] = chan_alloc();\n"
for chan in range(link_channels):
ret += f"\t{adsp}_link_chans[{chan}] = chan_alloc();\n"
ret += f"\t{adsp}.p_in = {adsp}_in_chans;\n"
ret += f"\t{adsp}.n_in = {input_channels};\n"
ret += f"\t{adsp}.p_out = (channel_t *) {adsp}_out_chans;\n"
ret += f"\t{adsp}.n_out = {output_channels};\n"
ret += f"\t{adsp}.p_link = (channel_t *) {adsp}_link_chans;\n"
ret += f"\t{adsp}.n_link = {link_channels};\n"
ret += f"\t{adsp}.modules = {adsp}_modules;\n"
ret += f"\t{adsp}.n_modules = {num_modules};\n"
# initialise the modules
for thread in resolved_pipeline["threads"]:
for stage_index, stage_name, stage_mem in thread:
in_edges = [e for e in resolved_pipeline["edges"] if e.dest[0] == stage_index]
out_edges = [e for e in resolved_pipeline["edges"] if e.source[0] == stage_index]
stage_n_in = len(in_edges)
stage_n_out = len(out_edges)
# TODO this is naive, stage could have different frame sizes on each edge
try:
stage_frame_size = max(e.frame_size for e in in_edges + out_edges)
except ValueError:
# the stage has no edges
stage_frame_size = 1
defaults = {}
for config_field, value in resolved_pipeline["configs"][stage_index].items():
if isinstance(value, list) or isinstance(value, tuple):
defaults[config_field] = "{" + ", ".join(str(i) for i in value) + "}"
else:
defaults[config_field] = str(value)
if resolved_pipeline["modules"][stage_index]["constants"]:
ret += f"\tstatic {stage_name}_constants_t {stage_name}_{stage_index}_constants;\n"
this_dict = resolved_pipeline["modules"][stage_index]["constants"]
const_struct = f"{stage_name}_{stage_index}_constants"
for key in this_dict:
this_array = this_dict[key]
this_constant_name = f"{stage_name}_{stage_index}_{key}"
if hasattr(this_array, "__len__"):
# if an array/list, code the array then add the pointer to the const_struct
ret += f"\tstatic typeof(({stage_name}_constants_t){{}}.{key}[0]) {this_constant_name}[] = {{{', '.join(map(str, this_array))}}};\n"
ret += f"\t{const_struct}.{key} = {this_constant_name};\n"
else:
# if a scalar, just hard code into const_struct
ret += f"\t{const_struct}.{key} = {this_array};\n"
# point the module.constants to the const_struct instance
ret += f"\t{adsp}.modules[{stage_index}].constants = &{const_struct};\n"
struct_val = ", ".join(f".{field} = {value}" for field, value in defaults.items())
# default_str = f"&({stage_name}_config_t){{{struct_val}}}"
if resolved_pipeline["modules"][stage_index]["yaml_dict"]:
ret += (
f"\tstatic {stage_name}_config_t config{stage_index} = {{ {struct_val} }};\n"
)
ret += f"""
static {stage_name}_state_t state{stage_index};
static uint8_t memory{stage_index}[{stage_mem}];
static adsp_bump_allocator_t allocator{stage_index} = ADSP_BUMP_ALLOCATOR_INITIALISER(memory{stage_index});
{adsp}.modules[{stage_index}].state = (void*)&state{stage_index};
// Control stuff
{adsp}.modules[{stage_index}].control.id = {stage_index};
{adsp}.modules[{stage_index}].control.config_rw_state = config_none_pending;
"""
if resolved_pipeline["modules"][stage_index]["yaml_dict"]:
ret += f"""
{adsp}.modules[{stage_index}].control.config = (void*)&config{stage_index};
{adsp}.modules[{stage_index}].control.module_type = e_dsp_stage_{stage_name};
{adsp}.modules[{stage_index}].control.num_control_commands = NUM_CMDS_{stage_name.upper()};
"""
else:
ret += f"""
{adsp}.modules[{stage_index}].control.config = NULL;
{adsp}.modules[{stage_index}].control.num_control_commands = 0;
"""
ret += f"{stage_name}_init(&{adsp}.modules[{stage_index}], &allocator{stage_index}, {stage_index}, {stage_n_in}, {stage_n_out}, {stage_frame_size});\n"
ret += f"\tadsp_controller_init(&{adsp}_controller, &{adsp});\n"
ret += f"\treturn &{adsp};\n"
ret += "}\n\n"
return ret
def _generate_dsp_muxes(resolved_pipeline):
# We assume that this function generates the arrays adsp_<x>_(in|out)_mux_cfgs
# and that it will initialise the .(input|output)_mux members of adsp
# We assume that dictionaries are ordered (which is true as of Python 3.7)
all_edges = _filter_edges_by_thread(resolved_pipeline)
all_in_edges = [e[0] for e in all_edges]
all_out_edges = [e[2] for e in all_edges]
adsp = f"adsp_{resolved_pipeline['identifier']}"
# We're basically assuming here that the dictionary is ordered the same way
# as it's going to be when we construct main, so these thread relationships
# are always going to be the same. I think this is true.
input_chan_idx = 0
num_input_mux_cfgs = 0
ret = f"\tstatic adsp_mux_elem_t {adsp}_in_mux_cfgs[] = {{\n"
for thread_input_edges in all_in_edges:
try:
edges = thread_input_edges["pipeline_in"]
for edge in edges:
ret += f"\t\t{{ .channel_idx = {input_chan_idx}, .data_idx = {edge[0][1]}, .frame_size = {edge.frame_size}}},\n"
num_input_mux_cfgs += 1
input_chan_idx += 1
except KeyError:
pass # this thread doesn't consume from the pipeline input
ret += "\t};\n"
output_chan_idx = 0
num_output_mux_cfgs = 0
ret += f"\tstatic adsp_mux_elem_t {adsp}_out_mux_cfgs[] = {{\n"
for thread_output_edges in all_out_edges:
try:
edges = thread_output_edges["pipeline_out"]
for edge in edges:
ret += f"\t\t{{ .channel_idx = {output_chan_idx}, .data_idx = {edge[1][1]}, .frame_size = {edge.frame_size}}},\n"
num_output_mux_cfgs += 1
output_chan_idx += 1
except KeyError:
pass # this thread doesn't consume from the pipeline input
ret += "\t};\n"
ret += f"\t{adsp}.input_mux.n_chan = {num_input_mux_cfgs};\n"
ret += f"\t{adsp}.input_mux.chan_cfg = (adsp_mux_elem_t *) {adsp}_in_mux_cfgs;\n"
ret += f"\t{adsp}.output_mux.n_chan = {num_output_mux_cfgs};\n"
ret += f"\t{adsp}.output_mux.chan_cfg = (adsp_mux_elem_t *) {adsp}_out_mux_cfgs;\n"
return ret
def _generate_dsp_ctrl() -> str:
ret = """
/* xscope setup, remove by setting generate_xscope_task to False on Pipeline init
This will also remove the call to adsp_control_xscope from the PAR_JOBS below */
#include <xscope.h>
void xscope_user_init()
{
adsp_control_xscope_register_probe();
}
"""
return ret
def generate_dsp_main(pipeline: Pipeline, out_dir="build/dsp_pipeline"):
"""
Generate the source code for adsp_generated_<x>.c.
Parameters
----------
pipeline : Pipeline
The pipeline to generate code for.
out_dir : str
Directory to store generated code in.
"""
out_dir = Path(out_dir)
out_dir.mkdir(exist_ok=True, parents=True)
resolved_pipe = pipeline.resolve_pipeline()
_generate_instance_id_defines(resolved_pipe, out_dir)
_generate_dsp_header(resolved_pipe, out_dir)
threads = resolved_pipe["threads"]
dsp_main = """
#include <stages/adsp_pipeline.h>
#include <stages/adsp_control.h>
#include <xcore/select.h>
#include <xcore/channel.h>
#include <xcore/assert.h>
#include <xcore/hwtimer.h>
#include <xcore/thread.h>
#include <platform.h>
#include <print.h>
#include "cmds.h" // Autogenerated
#include "cmd_offsets.h" // Autogenerated
#include <stages/bump_allocator.h>
#include <dsp/signal_chain.h>
// used in print_max_ticks
static adsp_controller_t* m_control;
"""
# add includes for each stage type in the pipeline
dsp_main += "".join(
f"#include <stages/{resolved_pipe['modules'][node_index]['name']}.h>\n"
for node_index in resolved_pipe["modules"].keys()
)
dsp_main += "".join(
f"#include <{resolved_pipe['modules'][node_index]['name']}_config.h>\n"
for node_index in resolved_pipe["modules"].keys()
if resolved_pipe["modules"][node_index]["yaml_dict"] is not None
)
if resolved_pipe["xscope"]:
dsp_main += _generate_dsp_ctrl()
dsp_main += _generate_dsp_threads(resolved_pipe)
dsp_main += _generate_dsp_init(resolved_pipe)
dsp_main += _generate_dsp_max_thread_ticks(resolved_pipe)
dsp_main += (
f"void adsp_{resolved_pipe['identifier']}_pipeline_main(adsp_pipeline_t* adsp) {{\n"
)
input_chan_idx = 0
output_chan_idx = 0
all_thread_edges = _filter_edges_by_thread(resolved_pipe)
determined_channels = _determine_channels(resolved_pipe)
for thread_idx, (thread, thread_edges) in enumerate(zip(threads, all_thread_edges)):
thread_input_edges, _, thread_output_edges, _ = thread_edges
# thread stages
dsp_main += f"\tmodule_instance_t* thread_{thread_idx}_modules[] = {{\n"
for stage_idx, _, _ in thread:
dsp_main += f"\t\t&adsp->modules[{stage_idx}],\n"
dsp_main += "\t};\n"
# thread input chanends
input_channel_array = []
for source in thread_input_edges.keys():
if source == "pipeline_in":
array_source = "adsp->p_in"
idx_num = input_chan_idx
input_channel_array.append(f"&{array_source}[{idx_num}]")
input_chan_idx += 1
else:
array_source = "adsp->p_link"
link_idx = 0
for chan_s, chan_d, _ in determined_channels:
if chan_s != "pipeline_in" and chan_d != "pipeline_out":
if source == chan_s and thread_idx == chan_d:
idx_num = link_idx
break
else:
link_idx += 1
else:
raise RuntimeError("Channel not found")
input_channel_array.append(f"(void*){array_source}[{idx_num}].end_b")
input_channels = ",\n\t\t".join(input_channel_array)
dsp_main += f"\tvoid* thread_{thread_idx}_inputs[] = {{\n\t\t{input_channels}}};\n"
# thread output chanends
output_channel_array = []
for dest in thread_output_edges.keys():
if dest == "pipeline_out":
array_source = "adsp->p_out"
idx_num = output_chan_idx
output_chan_idx += 1
else:
array_source = "adsp->p_link"
link_idx = 0
for chan_s, chan_d, _ in determined_channels:
if chan_s != "pipeline_in" and chan_d != "pipeline_out":
if chan_s == thread_idx and chan_d == dest:
idx_num = link_idx
break
else:
link_idx += 1
else:
raise RuntimeError("Channel not found")
output_channel_array.append(f"{array_source}[{idx_num}].end_a")
output_channels = ",\n\t\t".join(output_channel_array)
dsp_main += f"\tchanend_t thread_{thread_idx}_outputs[] = {{\n\t\t{output_channels}}};\n"
dsp_main += "\tPAR_JOBS(\n\t\t"
dsp_main += ",\n\t\t".join(
f"PJOB(dsp_{resolved_pipe['identifier']}_thread{ti}, (thread_{ti}_inputs, thread_{ti}_outputs, thread_{ti}_modules))"
for ti in range(len(threads))
)
if resolved_pipe["xscope"]:
dsp_main += ",\n\t\tPJOB(adsp_control_xscope, (adsp))"
dsp_main += "\n\t);\n"
dsp_main += "}\n"
(out_dir / f"adsp_generated_{resolved_pipe['identifier']}.c").write_text(dsp_main)