mittens-engine 0.7.0

A Vulkan and OpenXR scene engine with ECS, reactive signals, and Meow Meow scripting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
# Transform pipeline

This doc describes the current transform-pipeline direction in the engine.

Current authored usage: `TransformForkTRS` is the pipeline root. `TransformPipeline` and `TransformPipelineOutput` were removed from the authored API; any driven transform or content now attaches directly as a non-map child of the fork root.

Conceptually, a transform pipeline should mean one or more transform operators inserted into a transform hierarchy between an upstream transform basis and a downstream subtree.

The important consequence of that model is:

- operators compose through ordinary parent/child structure
- each operator consumes the transform output of its parent
- each operator passes its own transform output to its children

So “pipeline” should describe the inserted operator chain, not require a separate wrapper-centric authored model.

It is broader than XR. The same transform-pipeline machinery should be able to cover:

- XR controller / hand transform stabilization
- gizmo inheritance shaping
- stabilized helper transforms
- dynamic-bone-style follow later
- secondary motion later

The important current design shift is:

- authored transform shaping is primitive-only
- there is no filter sugar/desugaring layer
- the runtime evaluator is `TransformStreamSystem`

---

## Goals

- Keep transform processing explicit in topology.
- Keep the conceptual model simple: operators wedged into the hierarchy, not a parallel graph language for common cases.
- Support per-channel transform operations.
- Support temporal operators without pushing runtime state into serialized authored data.
- Keep the current runtime shape simple enough for the use cases we actually have.

## Non-goals

- This doc does not define a final editor authoring UI.
- This doc does not fully specify future spring / constraint / blending operators.
- This doc does not assume we already need a general multi-input graph runtime.

---

## 1. Current authored model

Authoring is done with explicit pipeline primitives in the component tree.

The current authored surface is more wrapper-heavy than the intended conceptual model. The longer-term direction is that a transform pipeline should read as an operator chain inserted between two parts of the transform hierarchy.

Current primitive components are:

- `TransformForkTRSComponent`
- `TransformMapTranslationComponent`
- `TransformMapRotationComponent`
- `TransformMapScaleComponent`
- `TransformDropComponent`
- `TransformMergeTRSComponent`
- `Vector3TemporalFilterComponent`
- `QuatTemporalFilterComponent`

The common authored shape today is:

```text
TransformComponent
  TransformForkTRS
    TransformMapTranslation
    TransformMapRotation
      QuatTemporalFilter
    TransformMapScale
    TransformComponent
      ... driven subtree ...
```

The important cleanup direction is:

- the inserted operators are the interesting part
- the mandatory wrapper and explicit output marker are not the desired long-term conceptual center

Two important current examples:

- `examples/vr-input.rs` uses a pipeline to smooth controller rotation while leaving translation and scale effectively passthrough.
- `src/engine/ecs/system/gizmo_system.rs` uses pipelines to keep or drop inherited translation / rotation / scale for gizmo visual groups.

So the authored model is already doing real work; this is not just speculative vocabulary anymore.

---

## 2. Runtime ownership: `TransformStreamSystem`

There is no separate `TransformPipelineProcessor` type in the codebase.

That term is redundant for the current architecture.

The runtime evaluator is simply:

- `TransformStreamSystem`

Today it is responsible for:

- parsing authored pipeline topology rooted at a `TransformForkTRSComponent`
- evaluating the parsed pipeline against an inherited world transform
- owning temporal operator state
- returning the processed world transform and the fork root's downstream children

`TransformSystem` stays responsible for ordinary transform-tree propagation.

Its relationship to the pipeline system is:

1. walk the normal transform subtree
2. when a node is a transform-stream boundary, ask `TransformStreamSystem` to evaluate it
3. continue traversal from the fork root's non-operator children

So the clean runtime split is:

- `TransformSystem`: subtree propagation / cached world matrices / side effects
- `TransformStreamSystem`: transform-stream evaluation

That is enough. We do not need a second “processor” abstraction unless we later introduce a genuinely separate compiled-runtime layer.

---

## 3. What shape pipelines have in memory today

The current in-memory shape is not a general graph.

It is a parsed rooted program-like tree:

```rust
pub struct TransformPipelinePlan {
    pub owner_component: Option<ComponentId>,
    pub input: TransformPipelineInput,
    pub stages: Vec<TransformPipelineStage>,
}

pub enum TransformPipelineStage {
    ForkTrs(TransformForkTrsStage),
}

pub struct TransformForkTrsStage {
    pub translation_ops: Vec<TransformPipelineVec3Op>,
    pub rotation_ops: Vec<TransformPipelineQuatOp>,
    pub scale_ops: Vec<TransformPipelineVec3Op>,
}
```

And channel values are carried in:

```rust
pub struct TransformPipelineChannels {
    pub translation: [f32; 3],
    pub rotation_quat_xyzw: [f32; 4],
    pub scale: [f32; 3],
}
```

The important properties of this runtime shape are:

- one parsed block represents one rooted pipeline subtree
- input is currently only `ParentWorld`
- evaluation is stage-ordered, not edge-scheduled
- the main stage type today is TRS fork + per-channel op lists
- downstream traversal is whatever non-operator children are attached under the fork root

So the current runtime model is much closer to:

- a small compiled pipeline program

than to:

- a graph of arbitrary nodes and edges

---

## 4. What `TransformPipeline` does in memory

`TransformPipelinePlan` is the runtime representation of one authored pipeline root.

Conceptually it means:

- take one input transform stream
- run an ordered list of processing stages on it
- produce one processed transform stream
- continue traversal through the fork root's downstream children

In other words, a block is not “a transform node” in world topology.

It is an internal executable description for:

- one pipeline boundary
- one input
- one ordered body of work
- one output routing decision

That means the in-memory `TransformPipelinePlan` type is not an authored ECS component.

It is a parsed runtime pipeline description whose semantics are closer to:

- pipeline program
- compiled subtree
- executable transform-processing block

than to a reusable graph block with free-form incoming and outgoing edges.

### Why blocks exist at all

Blocks let the runtime separate two concerns:

- authored topology shape
- execution shape

The authored topology is a component subtree.

The runtime does not want to evaluate that raw topology directly every time at the level of individual ECS component checks while also applying math.

So it parses the topology into a smaller internal representation that says, in effect:

- here is the input source
- here are the stages
- here are the channel ops
- here are the downstream children that should receive the processed basis

That is what the block is for.

### Why the current block shape is a bit awkward

The current parsed `TransformPipelinePlan` shape works, but it is slightly more abstract than what we actually need today.

Current reality:

- we have one input source
- we have ordered stages
- we mostly have one real stage family: TRS fork with per-channel ops
- we are not doing graph scheduling
- we are not doing multi-input composition
- we are not sharing subgraphs across pipelines

So the current `Pipeline(Box<TransformPipeline>)` recursion is more general than our current authored use cases require.

That does not make it wrong, but it does mean:

- the runtime shape is currently “tree of blocks/stages”
- not “graph of nodes/edges”
- and not “flat instruction list” either

---

## 5. How evaluation works today

At runtime, evaluation is:

1. parse the rooted component subtree if the current node is a `TransformPipelineComponent`
1. parse the rooted component subtree if the current node is a `TransformForkTRSComponent`
2. decompose the inherited world matrix into channels:
   - translation
   - rotation quaternion
   - scale
3. apply stages in order
4. recompose a world matrix
5. return the processed world matrix plus downstream children

The main evaluator functions are:

- `parse_component_tree(...)`
- `parse_fork_trs(...)`
- `evaluate_pipeline_node(...)`
- `evaluate_block(...)`
- `evaluate_stage(...)`
- `evaluate_fork_trs(...)`

Current operator behavior is intentionally simple:

- vec3 ops: `Pass`, `Drop`, `TemporalSmooth`
- quat ops: `Pass`, `Drop`, `TemporalFilter`

Current merge behavior is also simple:

- `ImplicitPassthrough`
- `Explicit`

But today both modes reassemble the same channel set in practice; explicit merge is mostly an authored/runtime marker, not a distinct recomposition algorithm yet.

---

## 6. Temporal state shape

Temporal state currently lives in `TransformStreamSystem`, not on authored components.

It is keyed by:

- `owner_component`
- parsed `stage_path`

via:

```rust
struct TransformPipelineStageKey {
    owner_component: Option<ComponentId>,
    stage_path: Vec<usize>,
}
```

And the system stores separate state maps for:

- vec3 temporal filters
- quat temporal filters

This is a good match for the current goals:

- authored components stay declarative
- runtime state is not serialized
- state identity follows the parsed operator location inside a pipeline root

The tradeoff is that reparsing identity has to remain stable enough for temporal continuity.

---

## 7. What our current use cases actually need

The current real use cases are narrower than a general graph system.

### A. XR controller rotation smoothing

Needed behavior:

- single input transform
- decompose to TRS
- smooth rotation only
- pass translation through
- pass scale through
- drive an output subtree

This is exactly what the current block + fork + per-channel-op shape is good at.

### B. Gizmo inheritance shaping

Needed behavior:

- single input transform
- keep or drop selected channels
- optionally produce multiple authored visual groups with different inheritance behavior
- continue traversal from explicit output roots

This is also a strong fit for the current shape.

### C. Likely near-future use cases

Still likely to fit the current shape:

- translation smoothing
- no-scale attachment helpers
- world/local visual-space shaping
- simple follow or damping operators

### D. Not required yet

Not actually needed yet:

- multiple input streams blended into one output
- arbitrary fan-in between operators
- arbitrary fan-out between internal operators
- edge-level routing semantics
- shared reusable subgraphs
- cycle handling
- topological scheduling of arbitrary node graphs

That distinction matters. It suggests we should not over-design the runtime around graph problems we do not have yet.

---

## 8. Should this be `TransformPipelineNode` / `TransformPipelineEdge` instead?

Probably not yet.

A node/edge runtime becomes attractive when at least one of these becomes real:

- multiple upstream sources can feed one operator
- one operator’s result can feed multiple downstream operators independently
- subgraphs are shared or instanced internally
- evaluation order is no longer just rooted-tree traversal
- operators need explicit typed ports and edge routing

That is not the current system.

Current reality is:

- rooted authored subtree
- one inherited input transform
- ordered stage evaluation
- per-channel op lists
- optional nested pipeline blocks

So a `Node` / `Edge` runtime would mostly add ceremony right now.

It would likely complicate:

- parsing
- temporal-state identity
- debugging
- authored/runtime correspondence

without clearly helping the current use cases.

### When a graph model would start making sense

If we later add real features like:

- blend between parent world and another transform source
- mix multiple constraints into one output
- branch one channel stream into multiple independent consumers
- explicit retargeting and recombination flows

then a graph runtime may become the better abstraction.

At that point, a shape like this could make sense:

```rust
struct TransformPipelineNode {
    id: TransformPipelineNodeId,
    op: TransformPipelineOp,
    inputs: Vec<TransformPipelinePort>,
    outputs: Vec<TransformPipelinePort>,
}

struct TransformPipelineEdge {
    from_node: TransformPipelineNodeId,
    from_port: usize,
    to_node: TransformPipelineNodeId,
    to_port: usize,
}
```

But that should be introduced when the use cases demand it, not in advance.

---

## 9. What would be a better flexible shape if we want to evolve carefully

The best near-term direction is probably:

- keep the authored topology exactly as it is now
- keep `TransformStreamSystem` as the sole runtime evaluator
- keep the internal representation rooted and tree-shaped
- make the internal names a little more honest about what they are

In practice there are two reasonable choices.

### Option A: keep the current shapes, just clarify semantics

That means:

- keep the current parsed `TransformPipeline` type
- keep `TransformPipelineStage`
- keep `TransformForkTrsStage`

and document that:

- a block is an executable rooted pipeline description
- not a general graph block

This is the smallest change and is probably good enough today.

### Option B: rename toward “program” / “plan” terminology later

If names start feeling misleading, a future rename could be cleaner:

- parsed `TransformPipeline` -> `TransformPipelineProgram` or `TransformPipelinePlan`
- `TransformPipelineStage` -> `TransformPipelineOpGroup` or `TransformPipelineStep`

That would better reflect current semantics:

- one input
- ordered execution
- rooted subtree
- no explicit edges

This may read more clearly than “block inside block inside stage”.

---

## 10. Recommended direction

The strongest current recommendation is:

1. keep the authored model primitive-only
2. treat `TransformStreamSystem` as the runtime evaluator; do not introduce a separate `TransformPipelineProcessor` concept
3. treat the current internal shape as a rooted parsed execution plan, not a general graph
4. keep `TransformPipeline` for now unless the parsed/runtime-vs-authored name overlap starts getting in the way
5. only move to `Node` / `Edge` if we gain real multi-input or shared-subgraph use cases

This keeps the architecture aligned with what we actually need today:

- XR smoothing
- gizmo inheritance shaping
- simple future follow/damping operators

without prematurely paying graph-runtime complexity costs.

---

## 11. Summary

Today, transform pipelines are:

- authored as explicit topology primitives
- evaluated by `TransformStreamSystem`
- represented in memory as rooted parsed blocks/stages
- executed as ordered channel-processing steps over one inherited world transform

The parsed in-memory `TransformPipeline` currently means:

- one compiled pipeline boundary
- one input
- ordered processing stages
- one output routing decision

That is a good fit for the real use cases we have now.

`TransformPipelineNode` / `TransformPipelineEdge` is a plausible future direction only if we start needing true graph semantics such as multi-input blending, shared subgraphs, or explicit port routing.