feat: graph-ir (continueing #6) #10
9 changed files with 105 additions and 26 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,3 +4,5 @@
|
||||||
.pre-commit-config.yaml
|
.pre-commit-config.yaml
|
||||||
*.pdf
|
*.pdf
|
||||||
/docs/*.png
|
/docs/*.png
|
||||||
|
/testfiles/gen/*
|
||||||
|
!/testfiles/gen/.gitkeep
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub enum DynamicValue {
|
|
||||||
Image(DynamicImage),
|
|
||||||
}
|
|
|
@ -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
1
testfiles/gen/.gitkeep
Normal file
|
@ -0,0 +1 @@
|
||||||
|
the testfile scripts will place generated images and media here. thank you for your understanding.
|
|
@ -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,
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue