feat: graph-ir (continueing #6) #10

Merged
schrottkatze merged 37 commits from schrottkatze/iowo:graph-ir into main 2024-01-23 12:55:01 +00:00
9 changed files with 105 additions and 26 deletions
Showing only changes of commit 3e208335c3 - Show all commits

2
.gitignore vendored
View file

@ -4,3 +4,5 @@
.pre-commit-config.yaml .pre-commit-config.yaml
*.pdf *.pdf
/docs/*.png /docs/*.png
/testfiles/gen/*
!/testfiles/gen/.gitkeep

View file

@ -1,31 +1,39 @@
use std::mem;
use ir::{ use ir::{
id,
instruction::{Filter, Kind}, instruction::{Filter, Kind},
GraphIr, Instruction, InstructionRef, GraphIr, Instruction, Map,
}; };
use crate::value::Dynamic; use crate::value::Variant;
mod instr; mod instr;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Evaluator { pub struct Evaluator {
ir: GraphIr, ir: GraphIr,
/// What the output of each individual streamer, and as result its output sockets, is.
/// Grows larger as evaluation continues,
/// as there's no mechanism for purging never-to-be-used-anymore instructions yet.
evaluated: Map<id::Output, Variant>,
} }
impl crate::Evaluator for Evaluator { impl crate::Evaluator for Evaluator {
fn feed(&mut self, ir: GraphIr) { fn feed(&mut self, ir: GraphIr) {
// TODO: should add instead of replace, see note in Evaluator trait above this method // TODO: should add instead of replace, see note in Evaluator trait above this method
self.ir = ir; self.ir = ir;
self.evaluated.clear();
} }
fn eval_full(&mut self) { fn eval_full(&mut self) {
// GraphIr::topological_sort returns InstructionRefs, which are mostly cool
// but we'd like to have them owned, so we can call Self::step without lifetime hassle
let queue: Vec<Instruction> = self let queue: Vec<Instruction> = self
.ir .ir
.topological_sort() .topological_sort()
.into_iter() .into_iter()
.map(Into::into) .map(Into::into)
.collect(); .collect();
for instr in queue { for instr in queue {
self.step(instr); self.step(instr);
} }
@ -35,23 +43,64 @@ impl crate::Evaluator for Evaluator {
impl Evaluator { impl Evaluator {
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
fn step(&mut self, instr: Instruction) { fn step(&mut self, instr: Instruction) {
let _ = match instr.kind { // what inputs does this instr need? fetch them
Kind::Read(details) => Some(Dynamic::Image(instr::read::read(details))), let inputs: Vec<_> = instr
.input_sources()
.iter()
.map(|source| {
let source_socket = source
.as_ref()
.expect("all inputs to be connected when an instruction is ran");
self.evaluated
.get(source_socket)
.expect("toposort to yield later instrs only after previous ones")
})
.collect();
// then actually do whatever the instruction should do
// NOTE: makes heavy use of index slicing,
// on the basis that ir::instruction::Kind::socket_count is correct
// TODO: make this a more flexible dispatch-ish arch
let output = match instr.kind {
Kind::Read(details) => Some(Variant::Image(instr::read::read(details))),
Kind::Write(details) => { Kind::Write(details) => {
instr::write::write(details, todo!()); #[allow(irrefutable_let_patterns)] // will necessarily change
let Variant::Image(input) = inputs[0] else {
panic!("cannot only write images, but received: `{:?}`", inputs[0]);
};
instr::write::write(details, input);
None None
} }
Kind::Math(_) => todo!(), Kind::Math(_) => todo!(),
Kind::Blend(_) => todo!(), Kind::Blend(_) => todo!(),
Kind::Noise(_) => todo!(), Kind::Noise(_) => todo!(),
Kind::Filter(filter_instruction) => match filter_instruction { Kind::Filter(filter_instruction) => match filter_instruction {
Filter::Invert => Some(Dynamic::Image(instr::filters::invert::invert( Filter::Invert => {
match todo!() { #[allow(irrefutable_let_patterns)]
Some(Dynamic::Image(img)) => img, let Variant::Image(input) = inputs[0] else {
_ => panic!("invalid value type for invert"), panic!(
}, "cannot only filter invert images, but received: `{:?}`",
))), inputs[0]
);
};
Some(Variant::Image(instr::filters::invert::invert(
input.clone(),
)))
}
}, },
}; };
if let Some(output) = output {
// TODO: very inaccurate, actually respect individual instructions.
// should be implied by a different arch
// TODO: all of those should not be public, offer some methods to get this on
// `Instruction` instead (can infer short-term based on Kind::socket_count)
let socket = id::Output(id::Socket {
belongs_to: instr.id,
idx: id::SocketIdx(0),
});
self.evaluated.insert(socket, output);
}
} }
} }

View file

@ -1,5 +1,12 @@
use image::DynamicImage; use image::DynamicImage;
pub enum Dynamic { /// Any runtime value that an instruction can input or output.
///
/// The name is taken from [Godot's `Variant` type],
/// which is very similar to this one.
///
/// [Godot's `Variant` type]: https://docs.godotengine.org/en/stable/classes/class_variant.html
#[derive(Clone, Debug)]
pub enum Variant {
Image(DynamicImage), Image(DynamicImage),
} }

View file

@ -49,7 +49,7 @@ impl Input {
/// ///
/// In contrast to [`Input`]s, [`Output`]s may be used or unused. /// In contrast to [`Input`]s, [`Output`]s may be used or unused.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Output(pub(super) Socket); pub struct Output(pub Socket); // TODO: Restrict publicness to super
impl Output { impl Output {
#[must_use] #[must_use]
@ -75,7 +75,7 @@ pub struct Socket {
/// This really only serves for denoting where a socket is, /// This really only serves for denoting where a socket is,
/// when it's already clear which instruction is referred to. /// when it's already clear which instruction is referred to.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct SocketIdx(pub u16); pub struct SocketIdx(pub u16); // TODO: Restrict publicness to super
impl fmt::Debug for SocketIdx { impl fmt::Debug for SocketIdx {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

View file

@ -27,6 +27,8 @@ pub fn from_ron(source: &str) -> ron::error::SpannedResult<GraphIr> {
/// The toplevel representation of a whole pipeline. /// The toplevel representation of a whole pipeline.
/// ///
/// # DAGs
///
/// Pipelines may not be fully linear. They may branch out and recombine later on. /// Pipelines may not be fully linear. They may branch out and recombine later on.
/// As such, the representation for them which is currently used is a /// As such, the representation for them which is currently used is a
/// [**D**irected **A**cyclic **G**raph](https://en.wikipedia.org/wiki/Directed_acyclic_graph). /// [**D**irected **A**cyclic **G**raph](https://en.wikipedia.org/wiki/Directed_acyclic_graph).
@ -56,6 +58,27 @@ pub fn from_ron(source: &str) -> ron::error::SpannedResult<GraphIr> {
/// So the vertices of the DAG are the **sockets** /// So the vertices of the DAG are the **sockets**
/// (which are either [`id::Input`] or [`id::Output`] depending on the direction), /// (which are either [`id::Input`] or [`id::Output`] depending on the direction),
/// and each **socket** in turn belongs to an **instruction**. /// and each **socket** in turn belongs to an **instruction**.
///
/// # Usage
///
/// - If you want to build one from scratch,
/// add a few helper methods like
/// constructing an empty one,
/// adding instructions and
/// adding edges
/// - If you want to construct one from an existing repr,
/// maybe you want to use [`semi_human::GraphIr`].
///
/// # Storing additional data
///
/// Chances are the graph IR seems somewhat fit to put metadata in it.
/// However, most likely you're interacting in context of some other system,
/// and also want to manage and index that data on your own.
///
/// As such, consider using _secondary_ maps instead.
/// That is, store in a data structure _you_ own a mapping
/// from [`id`]s
/// to whatever data you need.
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct GraphIr { pub struct GraphIr {
/// "Backbone" storage of all **instruction** IDs to /// "Backbone" storage of all **instruction** IDs to

View file

@ -1,3 +0,0 @@
pub enum DynamicValue {
Image(DynamicImage),
}

View file

@ -1,12 +1,12 @@
( (
instructions: { instructions: {
0: Read(( 0: Read((
source: File("testfiles/juan.jpg"), source: File("testfiles/rails.png"),
format: Jpeg, format: Png,
)), )),
1: Write(( 1: Write((
target: File("testfiles/out.png"), target: File("testfiles/gen/out.jpg"),
format: Png, format: Jpeg,
)), )),
}, },
edges: { edges: {

1
testfiles/gen/.gitkeep Normal file
View file

@ -0,0 +1 @@
the testfile scripts will place generated images and media here. thank you for your understanding.

View file

@ -1,12 +1,12 @@
( (
instructions: { instructions: {
0: Read(( 0: Read((
source: File("testfiles/juan.jpg"), source: File("testfiles/rails.png"),
format: Jpeg, format: Png,
)), )),
1: Filter(Invert), 1: Filter(Invert),
2: Write(( 2: Write((
target: File("testfiles/inverted.png"), target: File("testfiles/gen/inverted.png"),
format: Png, format: Png,
)), )),
}, },