Refactor the DAG system to clearly separate concerns:
dag.py: Manages all connection logic, node topology, input/output mapping, and feedback configurationdag_renderer.py: Stateless utility that walks the DAG and orchestrates OpenGL rendering
The DAG will track explicit input/output channel connections:
# New connection structure
Connection = {
source_node: Node,
source_output: int, # Always 0 for now (single output)
target_node: Node,
target_input_channel: int, # iChannel0, iChannel1, etc.
connection_type: 'node_output' | 'feedback'
}Note: External sources (video, images, webcam) are implemented as special node types, so their outputs connect like any other node output. The distinction between "external" and "node" is at the node type level, not the connection level.
Nodes will support variable input channels with metadata:
- External sources: Video files, images, webcam (via special source nodes)
- Node outputs: Output texture from another node
- Feedback: Internal node feedback (stored as node metadata, not DAG connection)
Create YAML config files alongside shader files (e.g., shader.glsl → shader.yml):
# shader.yml
inputs:
- channel: 0
name: iChannel0
type: node_output # or 'external', 'feedback'
required: true
- channel: 1
name: iChannel1
type: external
required: false
feedback:
enabled: false
channel: 0 # Which input channel receives feedbackCreate new node types for external data:
VideoSourceNode: Loads video filesImageSourceNode: Loads static imagesWebcamSourceNode: Captures webcam frames
These connect like regular nodes but have special texture update logic.
Feedback is internal to nodes (not a DAG cycle):
- Nodes have
feedback_configdict specifying which input channel receives their own output - DAG remains acyclic
- Renderer handles feedback by binding node's output texture to its own input channel
File: src/cube/dag/dag.py
-
Add connection tracking:
- `_connections: List[Connection]` - tracks all input/output connections - `connect_output_to_input(source_node, target_node, target_channel)` - explicit connection method - `get_input_connections(node, channel)` - get what feeds into a channel - `get_output_connections(node)` - get what consumes this node's output -
Remove rendering logic:
- Remove `render()` method (moved to DAGRenderer) - Keep topological sort for dependency ordering -
Add node metadata tracking:
- Track node input channel requirements - Track feedback configuration per node
File: src/cube/dag/node.py
-
Add input channel configuration:
- `input_channels: Dict[int, InputChannelConfig]` - metadata for each input channel - `feedback_config: Optional[FeedbackConfig]` - internal feedback setup -
Update base Node class to support variable inputs
New File: src/cube/shader/shader_config_loader.py
- Load YAML config files for shaders
- Parse input channel requirements
- Parse feedback configuration
- Fallback to defaults if config missing
New File: src/cube/shader/shader_config.py
-
Data classes for config structure:
- `InputChannelConfig` - `FeedbackConfig` - `ShaderConfig`
New File: src/cube/dag/external_source_nodes.py
VideoSourceNode(Node): Loads video framesImageSourceNode(Node): Loads static imagesWebcamSourceNode(Node): Captures webcam
These nodes:
- Have no input dependencies (they're sources)
- Update their output texture from external data each frame
- Can be connected to any node's input channel
File: src/cube/dag/source_node.py
- Remove hardcoded input texture handling
- Use DAG connection system to determine inputs
- Support variable input channels from config
File: src/cube/dag/effect_node.py
- Remove hardcoded
input_textureparameter - Use DAG connections to determine input sources
- Support multiple input channels
- Support feedback configuration
File: src/cube/render/dag_renderer.py
-
Remove DAG construction logic (moved to DAG)
-
Make renderer stateless - accept DAG as parameter
-
render(dag: DAG, t: float, uniforms: dict) -> np.ndarray:- Walk DAG in topological order - For each node, resolve input textures from connections - Bind textures to shader channels - Render node - Handle feedback by binding output to input channel -
Remove node management (add_node, remove_node, etc.)
-
Keep OpenGL resource management (texture loading, VAO, etc.)
File: src/cube/render/effect_manager.py
- Update to work with new DAG connection API
- Use
dag.connect_output_to_input()instead of direct manipulation - Remove DAG construction logic
File: src/cube/controller.py
- Update to use new DAG API
- Support "deploy" workflow: build DAG, then pass to renderer
@dataclass
class Connection:
source_node: Node
target_node: Node
target_input_channel: int # 0-3 for iChannel0-3
connection_type: Literal['node_output', 'external', 'feedback']@dataclass
class InputChannelConfig:
channel: int
name: str # e.g., 'iChannel0'
type: Literal['node_output', 'external', 'feedback']
required: bool
source: Optional[Node] = None # Set by DAG connections@dataclass
class FeedbackConfig:
enabled: bool
input_channel: int # Which input receives feedback
delay_frames: int = 0 # Optional frame delay for feedback- Keep existing code working during refactor
- Add new APIs alongside old ones
- Update callers incrementally
- Remove old APIs once migration complete
- Test DAG connection tracking
- Test topological sort with new connection model
- Test feedback rendering
- Test external source nodes
- Test shader config loading
- Test stateless renderer with various DAG structures
PROMPT
@src/cube/dag/dag.py and @src/cube/render/dag_renderer.py have some bad overlapping responsibility. I want the @src/cube/dag/dag.py to handle tracking the connections between nodes - that is, i want the @src/cube/dag/dag.py to determine what output textures are connected to what input textures, across source and effect nodes. I also want the DAG to encode information about (eventual) feedback connections between nodes (that is, if the output of a node should be used to feed back in as an input texture for e.g. accumulated state computation). When we go to render a DAG, the @src/cube/render/dag_renderer.py can handle all of the openGL calls required to actually load textures and allocate things, but the actual structure is fully encoded in the DAG. Some things to note:
- Nodes (@src/cube/dag/effect_node.py and @src/cube/dag/source_node.py ) can have variable number of input channels, some of which come from external sources (images, webcam frames, etc) and some of which might be feedback loops (e.g. the output texture of Node A is fed back to an input channel on Node A, and also passed down the chain to Node B)
- I will eventually build a GUI that allows for direct manipulation of the DAG, so we can live remap inputs and outputs and add/remove nodes to the graph
- the input/feedback configuration should be determined by the actual Shader program that a node is running. We need a way to discover the input dependencies (textures, feedback, etc) from the shader definition. We could either parse the shader .glsl file or write a config file.
- we will have a few different sources of external data - this could be video frames from a video file, still image files, or a webcam video source.
- I want to be able to iteratively build a DAG in the GUI, and then 'deploy' it all at once, at which point we send the updated DAG to the @src/cube/render/dag_renderer.py for rendering
Can you architect this system? In general, I want @src/cube/dag/dag.py to handle all connection logic, addition/removal of nodes, remapping of input/output dependencies, etc. I want @src/cube/render/dag_renderer.py to be a "dumb" (stateless) utility that crawls the DAG, orchestrating the OpenGL primitives required to render the full DAG pipeline.