Round trip and equivalence
The decompilation contract, how a gap surfaces, what each of the four verification layers answers, and three lessons in "all green is not the same as correct".
Decompilation is not a convenience export. It is a contract:
- any
UNiagaraSystemexports to a.dfs; - the export is byte-for-byte idempotent — build the export into an asset, export again, get the same text;
- anything the language cannot express is written into the file header as an explicit gap and
reported as a
DFX8xxx, never dropped in silence; Adoptre-exports and compares before taking an asset over, and refuses on any mismatch.
Point 3 carries the weight. An exporter that degrades silently leaves you staring at a wrong-looking effect three months later with nothing to pull on.
What a gap looks like
Round-trip gap — DFX8014
Emitter inheritance is flattened: the merged stack is carried in full, but "inherits from" is gone. Content is kept; the link is not.
Round-trip gap — DFX8015
More than one event handler on an emitter cannot be carried, and the export names what the rebuilt emitter will no longer receive.
Round-trip gap — DFX8016
A stage of a custom C++ stage class — not the engine's generic stage — has no DreamFXLang form.
Every one of them appears in two places: the exported file's header, and the log.
The four layers
| Layer | Command | Question it answers |
|---|---|---|
| L1 | mirror-diff | does the mirror's export match the original's, line by line? |
| L2 | mirror-diff | does the mirror compile clean? |
| L3 | the runtime-equivalence fixture | does the rebuilt system simulate the same? Fixed-step SimCache, per-frame particle counts |
| asset-diff | asset-diff | do the two agree as assets? Reflection-walked facts, independent of the exporter |
mirror-diff -NoCompile skips L2 — much faster, and the right thing when the mirrors were built in
the same session that is now diffing them.
Why the fourth layer has to exist
Of the first three, L1 compares two outputs of the same exporter. Something the exporter itself drops is dropped symmetrically on both sides: L1 is byte-identical, L2 still compiles clean, and L3 — which counts particles — sees nothing. The picture, meanwhile, is wrong.
That is not hypothetical. In one real case L1 was byte-identical, L3 reported exact, and the effect
looked wrong; the cause was a derived bit (bBindingExistsOnSource) that appears in no export at all.
So asset-diff takes another route entirely: it reflection-walks the facts of both assets,
including the compiler's own view (the compiled fact family), and never touches the exporter.
The corpus compares a fifth thing
On top of that, the 55-case corpus compares "the asset built from a fixture" against "the asset rebuilt from its export". That catches symmetric loss: something dropped identically on both sides of a text comparison, and therefore invisible to it.
Three measurement lessons
Measure one system per run. The L3 fixture can only measure one system per exec; more than that and the same asset gets reported as different things.
A random system with no determinism is not equal to itself. So L3 uses an A-vs-A self-comparison to decide decidability first: a system that disagrees with itself is judged undecidable, not different.
Behavioural divergence with everything statically green ⇒ measure the derivative. In the case
where birth speed was off by exactly half, the cause was a lost bFixedTickDelta — one missing entry
in the Settings table. A line diff reports only the first difference, so fixing one means re-running
the whole thing.
Measuring your own content
# how much of my content can be represented? bucketed by feature
pwsh -File Plugins/DreamFX/.skill/dfx.ps1 coverage
# export a whole content tree to source, in one editor boot
pwsh -File Plugins/DreamFX/.skill/dfx.ps1 decompile-all -Path=/Game/VFX
# L1 + L2
pwsh -File Plugins/DreamFX/.skill/dfx.ps1 mirror-diff -Path /Game/VFX
# asset facts, independent of the exporter; -DumpFacts writes both sides to Saved/DreamFX/*.facts
pwsh -File Plugins/DreamFX/.skill/dfx.ps1 asset-diff -Path /Game/VFX -DumpFactsasset-diff force-compiles both sides before comparing. Without that, the compiled fact
family reports each side's compile history rather than its content, which is not a comparison of
anything. -NoCompile is for diagnosis only.
The mirror namespace
Exports land in a Decompiled/<original path> namespace: the whole tree is first-class source that
rebuilds into mirrors and structurally cannot touch the originals. Unpacked in
Export vs Adopt.