mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 00:58:12 +08:00
Update rfc
This commit is contained in:
@@ -189,7 +189,7 @@ per-copy values:
|
|||||||
|
|
||||||
```rs
|
```rs
|
||||||
/// Instances the content a number of times, spaced by the direction vector.
|
/// Instances the content a number of times, spaced by the direction vector.
|
||||||
#[node_macro::node(category("Repeat"), level_extent = count)]
|
#[node_macro::node(category("Repeat"), extent = repat_extent)]
|
||||||
fn repeat<T>(
|
fn repeat<T>(
|
||||||
ctx: impl Ctx + ExtractIndex,
|
ctx: impl Ctx + ExtractIndex,
|
||||||
(element, transform): (T, Attr<Transform>),
|
(element, transform): (T, Attr<Transform>),
|
||||||
@@ -205,15 +205,8 @@ fn repeat<T>(
|
|||||||
```
|
```
|
||||||
|
|
||||||
The body is one lane of the declared list: the kernel reads its own copy
|
The body is one lane of the declared list: the kernel reads its own copy
|
||||||
index and produces that copy's values. `level_extent = count` declares
|
index and produces that copy's values. The `emit(...)` tail marks the one-lane form and
|
||||||
the size of the new level, and the compiler derives the structural parts
|
doubles as the tuple constructor, and it is optional.
|
||||||
from that one declaration: the multiplication with the carrier's extent,
|
|
||||||
and the index decomposition that routes an output lane to the right copy
|
|
||||||
and content item. The `emit(...)` tail marks the one-lane form and
|
|
||||||
doubles as the tuple constructor, and it is optional. A list return without
|
|
||||||
a `level_extent` is the store form, whose body produces the whole level
|
|
||||||
at once. That form is for nodes like string splitting, where the extent
|
|
||||||
cannot be known without running.
|
|
||||||
|
|
||||||
## Merging
|
## Merging
|
||||||
|
|
||||||
@@ -260,7 +253,13 @@ two-line pattern rather than new node kinds. The branches may carry
|
|||||||
different attribute sets. The output carries their union, filled with
|
different attribute sets. The output carries their union, filled with
|
||||||
defaults per branch. A lazy input with a concrete output type is an
|
defaults per branch. A lazy input with a concrete output type is an
|
||||||
ordinary value input: its value flows, the attributes on its wire do
|
ordinary value input: its value flows, the attributes on its wire do
|
||||||
not.
|
not. The tuple form composes with laziness: a lazy input declared
|
||||||
|
`Output = (T, Attr<A>)` yields the element and the declared reads at
|
||||||
|
each evaluation, so a kernel can branch on another input's attribute
|
||||||
|
without evaluating the branch it rejects. It is a read declaration
|
||||||
|
only: the lazy input's fields do not pass through to the output, since
|
||||||
|
the kernel controls whether and how often the edge is evaluated;
|
||||||
|
forwarding a lazy input's attributes is routing.
|
||||||
|
|
||||||
The rest of the authoring surface composes. Categories, per-parameter
|
The rest of the authoring surface composes. Categories, per-parameter
|
||||||
doc comments, `#[default]`, `#[hard]`, `#[expose]`, widget overrides, and
|
doc comments, `#[default]`, `#[hard]`, `#[expose]`, widget overrides, and
|
||||||
@@ -277,6 +276,20 @@ the record is also a valid pointer to the element. Element-only
|
|||||||
consumers are wired without adaptation, the wire keeps the element's type
|
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.
|
||||||
|
|
||||||
|
```
|
||||||
|
byte 0 4 8 12 16 20 24 28 31
|
||||||
|
┌───────────────┐
|
||||||
|
f64 │ element │
|
||||||
|
└───────────────┘
|
||||||
|
|
||||||
|
┌───────────────┬───────────────────────────────┬───────┬─┬───┐
|
||||||
|
+3 │ element │ Attr<&str> (ptr, len) │ u32 │b│pad│
|
||||||
|
└───────────────┴───────────────────────────────┴───────┴─┴───┘
|
||||||
|
|
||||||
|
Canonical order (descending alignment, then size) leaves no interior
|
||||||
|
padding here; 3 bytes of tail round the record up to align 8.
|
||||||
|
```
|
||||||
|
|
||||||
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 and not removed since, in a canonical order (descending alignment,
|
cone and not removed since, in a canonical order (descending alignment,
|
||||||
then size, then name and level), computed at graph compile time. Some
|
then size, then name and level), computed at graph compile time. Some
|
||||||
@@ -359,25 +372,33 @@ that decomposition is hoisted per run.
|
|||||||
## Runtime representation
|
## Runtime representation
|
||||||
|
|
||||||
- Every node's per-lane output is an activation frame on a per-thread
|
- Every node's per-lane output is an activation frame on a per-thread
|
||||||
record stack, the shape of an ordinary call stack: an evaluation
|
record stack, callee-fills-then-reclaims discipline: an evaluation
|
||||||
claims its frame at the stack pointer, evaluates its carrier beyond
|
claims its frame at the stack pointer, evaluates its inputs beyond it,
|
||||||
it, and releases on completion, leaving the returned record readable
|
writes its result into the frame, and then reclaims everything above
|
||||||
until the next claim. A node with several record sources lays their
|
the frame while keeping the frame itself for its consumer. So a node
|
||||||
regions side by side, so values held across sibling evaluations
|
advances the stack by exactly its own frame, and every already-
|
||||||
survive. "Allocating" a result is pointer arithmetic; frames are
|
evaluated input stays live until the node returns, which makes values
|
||||||
overwritten each lane, transients never touch the arena, and
|
held across sibling evaluations safe by construction. "Allocating" a
|
||||||
publishing into a cache copies out of the stack.
|
result is pointer arithmetic; transients never touch the arena, and
|
||||||
|
publishing into a cache copies out of the stack. An inline node
|
||||||
|
returns its output by value with no frame, so it reclaims its inputs
|
||||||
|
by rewinding to its entry pointer instead. A loop that re-evaluates a
|
||||||
|
subtree per iteration rewinds to a checkpoint each time, reusing the
|
||||||
|
slots.
|
||||||
- No global slot assignment exists: a node's wiring state is its own
|
- No global slot assignment exists: a node's wiring state is its own
|
||||||
frame size, so incremental recompiles and instance reuse cannot
|
frame size, so incremental recompiles and instance reuse cannot
|
||||||
invalidate storage, and the stack belongs to whichever thread runs
|
invalidate storage, and the stack belongs to whichever thread runs
|
||||||
the evaluation, created lazily in thread-local storage, so worker
|
the evaluation, created lazily in thread-local storage, so worker
|
||||||
counts never enter wiring. The total stack bound is derived by the
|
counts never enter wiring. The reserve is the peak of a per-path fold
|
||||||
wiring layer from the same layouts it computes (own frame plus
|
over the graph (a node's need is its own frame plus its inputs' frames
|
||||||
carrier need, maxed over value inputs, summed over sources) and
|
plus the deepest input's peak), computed once at wiring; it exceeds
|
||||||
reserved once per evaluation.
|
the plain sum of node frames because fan-out re-evaluation keeps
|
||||||
- This imposes one rule: a borrow of a released frame must not survive
|
several copies of a shared node's frame live at once.
|
||||||
the next claim. Consuming by copy is always fine, and the per-source
|
- Held record values are safe without a guard: a frame keeps its output
|
||||||
regions above make kernel-held record values safe by construction.
|
until its consumer reclaims it, so no input is released while a later
|
||||||
|
sibling evaluates. This relies on stack records being single-consumer,
|
||||||
|
which the frame-memo insertion at fan-out points guarantees by copying
|
||||||
|
a shared value off the stack rather than holding it across consumers.
|
||||||
- Batch results are per-field columns, each statically Varying (an
|
- Batch results are per-field columns, each statically Varying (an
|
||||||
array) or Uniform (a single value) per the residency analysis. A node
|
array) or Uniform (a single value) per the residency analysis. A node
|
||||||
that does not touch a column forwards the pointer, so bypass costs
|
that does not touch a column forwards the pointer, so bypass costs
|
||||||
@@ -385,6 +406,16 @@ that decomposition is hoisted per run.
|
|||||||
cost regardless of lane count. Both execution forms share one layout
|
cost regardless of lane count. Both execution forms share one layout
|
||||||
descriptor, and crossing from a batched producer to a per-lane consumer
|
descriptor, and crossing from a batched producer to a per-lane consumer
|
||||||
costs about 1.5ns per lane through a lane-view adapter.
|
costs about 1.5ns per lane through a lane-view adapter.
|
||||||
|
- A materialized level-N record carries its item count in one field at a
|
||||||
|
known offset in the layout, and each level-below column is a thin
|
||||||
|
pointer into the arena whose length is that count times the field
|
||||||
|
size. Erased consumers (the Data panel, capture, deep copy) read the
|
||||||
|
count off the record without reaching into the element type, and
|
||||||
|
Varying vs Uniform stays static in the layout, a Uniform column
|
||||||
|
addressing a single value. This is the same picture as a batch result,
|
||||||
|
so materializing a record and returning a batch are one format. Such
|
||||||
|
records spill: the count beside the element already fills the inline
|
||||||
|
budget, while scalar wires keep the two-word inline form.
|
||||||
- Alignment padding only exists in the per-lane view. In a row, a `u8`
|
- Alignment padding only exists in the per-lane view. In a row, a `u8`
|
||||||
element costs the same as a `u64`, while
|
element costs the same as a `u64`, while
|
||||||
packed columns keep the cost proportional to the element size (2x
|
packed columns keep the cost proportional to the element size (2x
|
||||||
@@ -408,6 +439,7 @@ that decomposition is hoisted per run.
|
|||||||
| `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; attributes on their wires do not flow |
|
| 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, Attr<A>, ..)>` | lazy input with attribute reads | each eval yields the element plus the declared reads, offsets resolved against that edge's layout; a read declaration only, and no field pass-through |
|
||||||
| `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 |
|
||||||
| `-> List<W>` without | store form | whole-level body, node owns storage |
|
| `-> List<W>` without | store form | whole-level body, node owns storage |
|
||||||
@@ -428,27 +460,37 @@ misalign them, and domain declarations stay with the extent system.
|
|||||||
## Structure shapes
|
## Structure shapes
|
||||||
|
|
||||||
A structure node pairs an extent composition with an index
|
A structure node pairs an extent composition with an index
|
||||||
decomposition, and both come from one declaration:
|
decomposition. The extent composition is an ordinary extent override:
|
||||||
|
the node macro's `extent(fn)` attribute names a function with the trait
|
||||||
|
method's signature (the node and the context to `GPoll<Extent>`), so
|
||||||
|
multiplicative, additive, and data-dependent extents are one mechanism
|
||||||
|
rather than a macro taxonomy, and the default stays the meet over the
|
||||||
|
value inputs.
|
||||||
|
|
||||||
- Multiplicative (Repeat, map/enter): the extent is the new level's
|
- Multiplicative (Repeat, map/enter): the extent override multiplies
|
||||||
count times the carrier's. A flat index splits by division into the
|
the new level's count by the carrier's. A flat index splits by
|
||||||
copy index (pushed as a level) and the content index. Batches split
|
division into the copy index (pushed as a level) and the content
|
||||||
into maximal per-copy runs.
|
index; the level push and the `emit` tail are the skeleton
|
||||||
- Additive (Merge): the extent is the sum of the inputs'. A flat index
|
declaration. Batches split into maximal per-copy runs.
|
||||||
range-splits into a segment and a local index, so per lane, merge is a
|
- Additive (Merge): an ordinary routing kernel whose selector condition
|
||||||
selector whose condition is the index, and the selector machinery
|
is the index. The kernel range-splits the flat index into a segment
|
||||||
below is reused as-is. Item rows union with per-segment default fill.
|
and a local index through the shared split helper and evaluates that
|
||||||
Each input's top row is pushed down one level onto that input's items
|
input at the shifted index via the derived-context lowering; the
|
||||||
via entries in the translation plan (a level remap computed at wiring;
|
extent override sums the inputs' extents through `Extent::sum`. An
|
||||||
no values are needed at compile time), composing by the declared
|
input with unbounded (Free) extent counts as exactly one item in the
|
||||||
combine rule; the fallback is inner wins iff the inner level wrote
|
sum, so merge is an extent-forcing boundary, which is the scalar base
|
||||||
the name, resolved from the write sets at wiring. The merged top row
|
case. Item rows union with per-segment default fill. Each input's top
|
||||||
starts empty. An explicit Wrap node is how the user nests instead. An
|
row is pushed down one level onto that input's items via entries in
|
||||||
input with unbounded (Free) extent contributes exactly one item, so
|
the translation plan (a level remap computed at wiring; no values are
|
||||||
merge is an extent-forcing boundary, which is the scalar base case.
|
needed at compile time), composing by the declared combine rule; the
|
||||||
Batched merge forwards per-segment sub-ranges to its inputs, so
|
fallback is inner wins iff the inner level wrote the name, resolved
|
||||||
column uniformity survives concatenation per segment, and default
|
from the write sets at wiring. The merged top row starts empty. An
|
||||||
materialization is only paid on the per-lane and store paths.
|
explicit Wrap node is how the user nests instead; a marker on the
|
||||||
|
merge node enables the push-down plan variant. Batched merge forwards
|
||||||
|
per-segment sub-ranges derived from the same split helper, so column
|
||||||
|
uniformity survives concatenation per segment, default
|
||||||
|
materialization is only paid on the per-lane and store paths, and
|
||||||
|
per-lane vs batched agreement is law-bound.
|
||||||
|
|
||||||
## Opaque record values
|
## Opaque record values
|
||||||
|
|
||||||
@@ -635,10 +677,8 @@ from shading languages for residency.
|
|||||||
- Where and how the combine rule is declared on the attribute marker.
|
- Where and how the combine rule is declared on the attribute marker.
|
||||||
Merge push-down and flatten both consume it, and inner-wins is the
|
Merge push-down and flatten both consume it, and inner-wins is the
|
||||||
implemented fallback.
|
implemented fallback.
|
||||||
- The macro spelling of the additive structure shape. Merge is
|
- The spelling of the push-down marker on the merge node, the one part
|
||||||
currently a hand-written reference lowering, and it has no domain
|
of the additive shape the extent override cannot express.
|
||||||
logic of its own, so it is not clear what a kernel for it would even
|
|
||||||
contain.
|
|
||||||
- The graph UX of the map/enter construct.
|
- The graph UX of the map/enter construct.
|
||||||
- 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user