mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 05:08:13 +08:00
Update rfc with new tuple syntax for inputs
This commit is contained in:
@@ -7,9 +7,9 @@ declare their attribute reads and writes in their signatures, and the
|
|||||||
compiler resolves every access to a byte offset during wiring, so there is
|
compiler resolves every access to a byte offset during wiring, so there is
|
||||||
no name lookup at runtime. Storage and batch results are per-attribute
|
no name lookup at runtime. Storage and batch results are per-attribute
|
||||||
columns. The contiguous record only exists as a per-lane view, assembled
|
columns. The contiguous record only exists as a per-lane view, assembled
|
||||||
into activation frames on a per-thread record stack. All of the machinery
|
into buffers the compiler assigns. All of the machinery that could
|
||||||
that could corrupt a layout is generated code, so getting it wrong is a
|
corrupt a layout is generated code, so getting it wrong is a type error or
|
||||||
type error or a graph compile error rather than undefined behavior.
|
a graph compile error rather than undefined behavior.
|
||||||
|
|
||||||
# Motivation
|
# Motivation
|
||||||
|
|
||||||
@@ -86,31 +86,83 @@ table at graph compile time, which is where every resolution happens
|
|||||||
anyway, and two user-supplied names colliding at different value types
|
anyway, and two user-supplied names colliding at different value types
|
||||||
is a graph compile error naming both nodes.
|
is a graph compile error naming both nodes.
|
||||||
|
|
||||||
|
A write can also be generic over both the name and the value type. The
|
||||||
|
attribute then arrives on its own wire, as an input whose element is
|
||||||
|
`()`:
|
||||||
|
|
||||||
|
```rs
|
||||||
|
/// Attaches the attribute to the content.
|
||||||
|
#[node_macro::node(category("Attributes"))]
|
||||||
|
fn set_attribute<T, A, Y>(
|
||||||
|
_: impl Ctx,
|
||||||
|
element: T,
|
||||||
|
(_, attr): ((), Attr<Custom<A, Y>>),
|
||||||
|
) -> (T, Attr<Custom<A, Y>>) {
|
||||||
|
(element, attr)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A unit value component means the edge exists and carries only its
|
||||||
|
attributes (`_: ()` still means no edge at all). The name enters the
|
||||||
|
graph at a source node holding the constant text input, whose output
|
||||||
|
type is filled at graph compile time, where user-supplied names join
|
||||||
|
the name table anyway; the compiler pairs `A` and `Y` through the wire
|
||||||
|
types, so the write set is derived from types alone, and the
|
||||||
|
one-name-one-type check covers the binding, making a declared name
|
||||||
|
targeted at a different type a graph compile error. A generic read
|
||||||
|
resolves only when the input wire's type determines the binding
|
||||||
|
uniquely, and anything else is a validation error. The node is one
|
||||||
|
compiled instance: `A` and `Y` instantiate with tokens and the value
|
||||||
|
rides the copy plan as a byte move, parked in the arena when its type
|
||||||
|
has drop glue, so no implementations list exists. A kernel that
|
||||||
|
computes on the value uses a bound and monomorphizes per its
|
||||||
|
implementations list as usual.
|
||||||
|
|
||||||
## Reading and writing attributes
|
## Reading and writing attributes
|
||||||
|
|
||||||
A node declares its attribute io in its signature. The opacity node
|
A node declares its attribute io in its signature. A parameter that
|
||||||
becomes:
|
reads attributes destructures its input into the wired value and the
|
||||||
|
reads taken from that input's wire. The opacity node becomes:
|
||||||
|
|
||||||
```rs
|
```rs
|
||||||
/// Modifies the opacity of the input by multiplying the existing value by this percentage.
|
/// Modifies the opacity of the input by multiplying the existing value by this percentage.
|
||||||
#[node_macro::node(category("Blending"))]
|
#[node_macro::node(category("Blending"))]
|
||||||
fn opacity<T>(
|
fn opacity<T>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
element: T,
|
(element, opacity): (T, Attr<Opacity>),
|
||||||
/// How visible the content should be, from 100% (fully opaque) to 0% (fully transparent).
|
/// How visible the content should be, from 100% (fully opaque) to 0% (fully transparent).
|
||||||
#[default(100.)]
|
#[default(100.)]
|
||||||
factor: Percentage,
|
factor: Percentage,
|
||||||
opacity: Attr<Opacity>,
|
|
||||||
) -> (T, Attr<Opacity>) {
|
) -> (T, Attr<Opacity>) {
|
||||||
(element, Attr(*opacity * factor / 100.))
|
(element, Attr(*opacity * factor / 100.))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`Attr<Opacity>` as a parameter is a read (it yields the declared default
|
An `Attr<A>` inside a parameter tuple is a read from that parameter's
|
||||||
if nothing upstream wrote the attribute), in the return tuple it is a
|
wire (it yields the declared default if nothing upstream wrote the
|
||||||
write, and the same marker on both sides is a modify. The first
|
attribute), an `Attr<A>` in the return tuple is a write, and the same
|
||||||
parameter after the context is the primary input, and an unbounded
|
marker on both sides is a modify. A `RemoveAttr<A>` in the return
|
||||||
generic `element: T` that is returned in the first tuple position means
|
tuple is a delete: the name leaves the output layout, downstream reads
|
||||||
|
yield the default again, and the column leaves the Data panel. A read
|
||||||
|
binds to the input it is destructured from, so which wire an attribute
|
||||||
|
comes from is always explicit in the signature, and secondary inputs
|
||||||
|
declare reads the same way:
|
||||||
|
|
||||||
|
```rs
|
||||||
|
(factor, out_of_100): (Percentage, Attr<OutOf100>),
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no implicit attribute flow between inputs. The primary
|
||||||
|
input's attributes pass through to the output, overwritten where the
|
||||||
|
node writes; a secondary input contributes exactly the reads its tuple
|
||||||
|
names. An input without reads stays a plain parameter. Parameter
|
||||||
|
attributes (`#[default]`, `#[implementations]`, doc comments) apply to
|
||||||
|
the value component; attribute markers are concrete types and never
|
||||||
|
enter monomorphization.
|
||||||
|
|
||||||
|
The first parameter after the context is the primary input, with or
|
||||||
|
without a read tuple, and an unbounded generic `element: T` in its
|
||||||
|
value position that is returned in the first tuple position means
|
||||||
"I pass the element through unchanged". The compiler lowers this to a
|
"I pass the element through unchanged". The compiler lowers this to a
|
||||||
byte copy (often to nothing, see below), and a single compiled instance
|
byte copy (often to nothing, see below), and a single compiled instance
|
||||||
covers every element type, with no trait bounds and no implementations
|
covers every element type, with no trait bounds and no implementations
|
||||||
@@ -140,13 +192,12 @@ per-copy values:
|
|||||||
#[node_macro::node(category("Repeat"), level_extent = count)]
|
#[node_macro::node(category("Repeat"), level_extent = count)]
|
||||||
fn repeat<T>(
|
fn repeat<T>(
|
||||||
ctx: impl Ctx + ExtractIndex,
|
ctx: impl Ctx + ExtractIndex,
|
||||||
element: T,
|
(element, transform): (T, Attr<Transform>),
|
||||||
#[default(1)]
|
#[default(1)]
|
||||||
#[hard(1..)]
|
#[hard(1..)]
|
||||||
count: u32,
|
count: u32,
|
||||||
#[default(100., 100.)]
|
#[default(100., 100.)]
|
||||||
direction: DVec2,
|
direction: DVec2,
|
||||||
transform: Attr<Transform>,
|
|
||||||
) -> List<(T, Attr<Transform>)> {
|
) -> List<(T, Attr<Transform>)> {
|
||||||
let offset = direction * ctx.innermost_index() as f64;
|
let offset = direction * ctx.innermost_index() as f64;
|
||||||
emit(element, Attr(DAffine2::from_translation(offset) * *transform))
|
emit(element, Attr(DAffine2::from_translation(offset) * *transform))
|
||||||
@@ -171,8 +222,10 @@ inputs', and an attribute missing on one side is filled with its declared
|
|||||||
default for that side's items, so the result is rectangular in every
|
default for that side's items, so the result is rectangular in every
|
||||||
attribute. A scalar input contributes one item. When lists are combined,
|
attribute. A scalar input contributes one item. When lists are combined,
|
||||||
each input's own top-level attributes are pushed down onto that input's
|
each input's own top-level attributes are pushed down onto that input's
|
||||||
items (composing by the attribute's declared rule where one exists,
|
items (composing by the attribute's declared rule where one exists;
|
||||||
otherwise the inner value wins), and the merged list starts with an empty
|
otherwise the pushed value fills the items that never wrote the name
|
||||||
|
and the inner value wins where they did, resolved from the write sets
|
||||||
|
at graph compile time), and the merged list starts with an empty
|
||||||
top level. If the user wants to keep the groups as groups, they wrap
|
top level. If the user wants to keep the groups as groups, they wrap
|
||||||
explicitly instead.
|
explicitly instead.
|
||||||
|
|
||||||
@@ -225,13 +278,15 @@ consumers are wired without adaptation, the wire keeps the element's type
|
|||||||
and colour, and the registry stays keyed on element types.
|
and colour, and the registry stays keyed on element types.
|
||||||
|
|
||||||
A wire's layout is the set of all attributes written in its upstream
|
A wire's layout is the set of all attributes written in its upstream
|
||||||
cone, in a canonical order (descending alignment, then size, then name
|
cone and not removed since, in a canonical order (descending alignment,
|
||||||
and level), computed at graph compile time. Some consequences:
|
then size, then name and level), computed at graph compile time. Some
|
||||||
|
consequences:
|
||||||
|
|
||||||
- Layout identity is captured by stable node ids, because the write set
|
- Layout identity is captured by stable node ids, because the write set
|
||||||
is part of the hashed upstream cone. An instance that survives an
|
is part of the hashed upstream cone. An instance that survives an
|
||||||
incremental recompile cannot meet a changed layout.
|
incremental recompile cannot meet a changed layout.
|
||||||
- Reads resolve to `Option<offset>` at wiring. Present means a field
|
- Reads resolve to `Option<offset>` at wiring, each against the layout
|
||||||
|
of the input wire its tuple destructures. Present means a field
|
||||||
access, and absent means the macro emits the default constant. Writes
|
access, and absent means the macro emits the default constant. Writes
|
||||||
always resolve. The runtime does no name lookup, no hashing, and no
|
always resolve. The runtime does no name lookup, no hashing, and no
|
||||||
downcasting. A resolved read costs the same as a native struct field
|
downcasting. A resolved read costs the same as a native struct field
|
||||||
@@ -242,6 +297,18 @@ and level), computed at graph compile time. Some consequences:
|
|||||||
- Layouts are derived data. The document stores only user-visible
|
- Layouts are derived data. The document stores only user-visible
|
||||||
structure, no attribute data is serialized, and representation changes
|
structure, no attribute data is serialized, and representation changes
|
||||||
never require a document migration.
|
never require a document migration.
|
||||||
|
- Semantically a wire value has every attribute at all times: a read of
|
||||||
|
a name nobody wrote yields the declared default, so a written default
|
||||||
|
and an absent name are indistinguishable at runtime. Presence
|
||||||
|
(membership in the layout) is representation, consulted only by merge
|
||||||
|
push-down's fallback and the Data panel, which presents the layout:
|
||||||
|
column presence is a pure function of the graph, stable across frames
|
||||||
|
and across the branches a selector takes.
|
||||||
|
- Writes are unconditional: presence never depends on a value, so a
|
||||||
|
conditionally relevant attribute is written at its default, and a
|
||||||
|
runtime `Option` around a value buys nothing (`None` could only mean
|
||||||
|
the default). A name that wants a distinguished unset declares an
|
||||||
|
`Option` value type on its marker.
|
||||||
|
|
||||||
Fields are `Copy`, and larger payloads go behind a pointer-sized field.
|
Fields are `Copy`, and larger payloads go behind a pointer-sized field.
|
||||||
Runtime-shaped data (CSV columns, arbitrary JSON) is a single dynamic
|
Runtime-shaped data (CSV columns, arbitrary JSON) is a single dynamic
|
||||||
@@ -252,7 +319,10 @@ A name's type is unique by construction. For declared markers the census
|
|||||||
admits one marker per name, checked when the registry is built. For
|
admits one marker per name, checked when the registry is built. For
|
||||||
user-supplied names the binding forms at graph compile time, carrying the
|
user-supplied names the binding forms at graph compile time, carrying the
|
||||||
marker's declared value type, and two names colliding at different types
|
marker's declared value type, and two names colliding at different types
|
||||||
is a graph compile error that names both nodes. We do not attempt
|
is a graph compile error that names both nodes. Generic-typed writes
|
||||||
|
join the same table, carrying the name and value type their bindings
|
||||||
|
resolve to, so the check runs over declared markers, user-supplied
|
||||||
|
names, and generic instantiations together. We do not attempt
|
||||||
coercion.
|
coercion.
|
||||||
|
|
||||||
## Levels and residency
|
## Levels and residency
|
||||||
@@ -261,8 +331,9 @@ Levels are numbered from the innermost out. This keeps layout keys
|
|||||||
stable when a structure node pushes a level (nothing renumbers) and
|
stable when a structure node pushes a level (nothing renumbers) and
|
||||||
matches how indices are already numbered. The binding rules are:
|
matches how indices are already numbered. The binding rules are:
|
||||||
|
|
||||||
- A read or write binds to the top level of the wire at the node's chain
|
- A read binds to the top level of the input wire it is destructured
|
||||||
position.
|
from at the node's chain position; a write binds to the top level of
|
||||||
|
the output wire.
|
||||||
- A structure node pushes a level and then writes its per-copy
|
- A structure node pushes a level and then writes its per-copy
|
||||||
attributes into the former top row, and the new top row starts empty.
|
attributes into the former top row, and the new top row starts empty.
|
||||||
- A node that reads the element (concrete type or bound) is pinned to
|
- A node that reads the element (concrete type or bound) is pinned to
|
||||||
@@ -330,10 +401,12 @@ that decomposition is hoisted per run.
|
|||||||
| `_: ()` | no primary input | no carrier edge |
|
| `_: ()` | no primary input | no carrier edge |
|
||||||
| `element: T` (unbounded, returned first) | explicit passthrough | erased byte carry, where `T` is instantiated with a zero-sized token, so the routing is checked by the type system and costs nothing |
|
| `element: T` (unbounded, returned first) | explicit passthrough | erased byte carry, where `T` is instantiated with a zero-sized token, so the routing is checked by the type system and costs nothing |
|
||||||
| `element: Concrete` / bound | element read | field read at offset 0, monomorphized per implementations list, binds level 0 |
|
| `element: Concrete` / bound | element read | field read at offset 0, monomorphized per implementations list, binds level 0 |
|
||||||
| `x: Attr<A>` | attribute read | offset read, or the default constant |
|
| `(x, a): (X, Attr<A>)` | input with attribute reads | the value as its ordinary lowering; each `Attr` an offset read into that input's record, or the default constant |
|
||||||
|
| `(_, a): ((), Attr<A>)` | attribute-only input | wired record edge with unit element; the attribute is the payload |
|
||||||
| `Attr<A>` in the return tuple | attribute write | offset write into the output record |
|
| `Attr<A>` in the return tuple | attribute write | offset write into the output record |
|
||||||
|
| `RemoveAttr<A>` in the return tuple | attribute delete | the name leaves the output layout; functionally a write of the default |
|
||||||
| `keys: List<K>` | whole-extent input | wired edge, evaluated over its extent into a view |
|
| `keys: List<K>` | whole-extent input | wired edge, evaluated over its extent into a view |
|
||||||
| plain parameters | wired value inputs | ordinary wired edges |
|
| plain parameters | wired value inputs | ordinary wired edges; attributes on their wires do not flow |
|
||||||
| `impl Node<Context<'_>, Output = Concrete>` | lazy value input | the value flows, attributes do not |
|
| `impl Node<Context<'_>, Output = Concrete>` | lazy value input | the value flows, attributes do not |
|
||||||
| `impl Node<Context<'_>, Output = T>` (unbounded) | source of an opaque record family | routing, see below |
|
| `impl Node<Context<'_>, Output = T>` (unbounded) | source of an opaque record family | routing, see below |
|
||||||
| `-> List<W>` with `level_extent =` | per-lane level production | structural skeleton emitted by the macro |
|
| `-> List<W>` with `level_extent =` | per-lane level production | structural skeleton emitted by the macro |
|
||||||
@@ -368,7 +441,8 @@ decomposition, and both come from one declaration:
|
|||||||
Each input's top row is pushed down one level onto that input's items
|
Each input's top row is pushed down one level onto that input's items
|
||||||
via entries in the translation plan (a level remap computed at wiring;
|
via entries in the translation plan (a level remap computed at wiring;
|
||||||
no values are needed at compile time), composing by the declared
|
no values are needed at compile time), composing by the declared
|
||||||
combine rule with inner-wins as the fallback. The merged top row
|
combine rule; the fallback is inner wins iff the inner level wrote
|
||||||
|
the name, resolved from the write sets at wiring. The merged top row
|
||||||
starts empty. An explicit Wrap node is how the user nests instead. An
|
starts empty. An explicit Wrap node is how the user nests instead. An
|
||||||
input with unbounded (Free) extent contributes exactly one item, so
|
input with unbounded (Free) extent contributes exactly one item, so
|
||||||
merge is an extent-forcing boundary, which is the scalar base case.
|
merge is an extent-forcing boundary, which is the scalar base case.
|
||||||
@@ -484,9 +558,9 @@ make semantic mistakes (evaluating an input it did not need to) but
|
|||||||
cannot misalign an offset. Kernels see contexts only as an opaque
|
cannot misalign an offset. Kernels see contexts only as an opaque
|
||||||
`impl Ctx + ...` they cannot construct, and lifetimes keep them from
|
`impl Ctx + ...` they cannot construct, and lifetimes keep them from
|
||||||
stashing handles in node state. The one remaining discipline lives
|
stashing handles in node state. The one remaining discipline lives
|
||||||
inside generated code (a borrow of a released frame must not survive the
|
inside generated code (a result buffer must not be borrowed across a
|
||||||
next claim), and debug assertions guard it at wiring boundaries,
|
sibling evaluation it could alias), and debug assertions guard it at
|
||||||
following the existing precedent for TypeId checks.
|
wiring boundaries, following the existing precedent for TypeId checks.
|
||||||
|
|
||||||
# Drawbacks
|
# Drawbacks
|
||||||
|
|
||||||
@@ -515,8 +589,8 @@ following the existing precedent for TypeId checks.
|
|||||||
per value, and no compile-time name checking. Interning the keys
|
per value, and no compile-time name checking. Interning the keys
|
||||||
improves the constant (about 1.9ns per access vs. 0.43 for a resolved
|
improves the constant (about 1.9ns per access vs. 0.43 for a resolved
|
||||||
offset) but keeps a per-access search and rules out the structural
|
offset) but keeps a per-access search and rules out the structural
|
||||||
optimizations that need static layouts: bypass, uniform columns,
|
optimizations that need static layouts: bypass, uniform columns, slot
|
||||||
stack-allocated activation frames.
|
coalescing.
|
||||||
- Attributes as separate graph edges, one channel per attribute: bypass
|
- Attributes as separate graph edges, one channel per attribute: bypass
|
||||||
and per-channel caching become graph structure. We prototyped and
|
and per-channel caching become graph structure. We prototyped and
|
||||||
measured this. Without caching at fan-outs, every channel re-evaluates
|
measured this. Without caching at fan-outs, every channel re-evaluates
|
||||||
@@ -565,10 +639,15 @@ from shading languages for residency.
|
|||||||
currently a hand-written reference lowering, and it has no domain
|
currently a hand-written reference lowering, and it has no domain
|
||||||
logic of its own, so it is not clear what a kernel for it would even
|
logic of its own, so it is not clear what a kernel for it would even
|
||||||
contain.
|
contain.
|
||||||
- The graph UX of the map/enter construct, and whether attribute-typed
|
- The graph UX of the map/enter construct.
|
||||||
parameters can ever be ordinary exposed inputs.
|
|
||||||
- Naming: `Attribute` trait vs. `Attr` wrapper, and whether the
|
- Naming: `Attribute` trait vs. `Attr` wrapper, and whether the
|
||||||
authoring `List` sharing the wire type's name helps or confuses.
|
authoring `List` sharing the wire type's name helps or confuses.
|
||||||
|
- Generic-typed writes: where the default for a generically written
|
||||||
|
name comes from (a `Default` bound on the value vs. an input on the
|
||||||
|
name source), what `A` instantiates to at the Rust level, whether
|
||||||
|
attribute-only wires carry exactly one attribute by construction or
|
||||||
|
uniqueness is checked per read, and the graph UX of the name source
|
||||||
|
node.
|
||||||
- Whether evaluating at a lane outside the input's extent is clamped,
|
- Whether evaluating at a lane outside the input's extent is clamped,
|
||||||
wrapped, or a debug assertion.
|
wrapped, or a debug assertion.
|
||||||
- `List<List<W>>` outputs, i.e. one node pushing two levels.
|
- `List<List<W>>` outputs, i.e. one node pushing two levels.
|
||||||
|
|||||||
Reference in New Issue
Block a user