Post 140

The Diff That Was Not There

Open a patch, change nothing, save it, and the file stops moving under you. Saves are byte-stable, so version control shows the one value you moved instead of a few hundred lines of reshuffled noise. And an open-and-save no longer quietly deletes the title, author and description you wrote.

Here is a thing that should be boring and was not. You open a patch. You change nothing. You hit save out of habit, the way everyone hits save out of habit. Git now reports that you have rewritten the file.

Not a little. The whole thing, top to bottom, several hundred lines of it. Every pin on every node, apparently touched, apparently different. And buried somewhere in that landslide is the one number you did change, which was the entire reason you opened the patch in the first place.

Where the noise came from

Pin values were kept in a hash map keyed by pin name, and a hash map has opinions about what order it hands its contents back. Those opinions come from a seed that gets scrambled fresh every time the app starts. Within a single session it was consistent enough to look fine. Quit Lux, reopen it, save the same untouched patch, and every pin came out in a new order.

Nothing was wrong with the patch. Every value was intact, every wire where you left it, and the thing rendered identically. The file simply described itself in a different order each time, and version control has no way of knowing that “roughness now appears four lines above albedo” is not a change. It just sees lines that moved.

The cost lands in the two places it hurts most. Reviewing your own history becomes archaeology: the commit says 380 lines changed, and finding the one that matters means reading all of them. And collaborating gets worse than that, because two people editing different corners of the same patch produce merge conflicts on pins neither of them went anywhere near. Those conflicts are not conflicts. They are two hash seeds disagreeing about alphabetical order, and you get to resolve them by hand.

The fix is a sort, in the one place it matters

The pins are now written out in sorted order. That is the whole idea. The map stays a hash map in memory, because that is the right shape for the thing the graph does with it ten thousand times a second, and nobody evaluating a patch cares what order the pins come back in. The order only has to be pinned down at the one moment it escapes into a file, so that is where it gets pinned down, on the type itself rather than at each place that writes one. The next writer to come along inherits the guarantee without having to know it exists.

Two thirds of this turned out to be already handled, which was a pleasant thing to discover at the bottom of a hole. Nodes were already sorted by id, and subpatches already took a scenic route through a data structure that sorts on the way past. The input pins were the last unsorted map in the document, and now they are not.

The worse thing standing behind it

Chasing the reshuffled pins turned up something that had been quietly doing real damage the whole time, in the same keystroke.

Every patch carries a little metadata: a title, an author, a description of what the thing is and why. Most of the patches that ship with Lux have all three, because someone sat down and wrote them. The save path was not reading any of it. It rebuilt the metadata from scratch on every save, and the only thing it knew how to fill in was the name, which it took from the filename.

So you opened a patch called sub_region.lux that was titled “Sub-Region TextureToLayer” and carried a paragraph explaining what it demonstrated. You pressed Ctrl+S. The title became sub_region, and the paragraph, the author, and the creation date were gone. Not corrupted, not mangled. Gone, cleanly, as though nobody had ever written them. The crash-recovery snapshot did the same thing, so restoring after a crash made it permanent.

Reshuffled pins are noise. This was deletion, and it went out under the same three keys. The document now carries the metadata it was loaded with and hands it straight back on save. A Save As on something you never titled still picks up the filename, because that is a reasonable guess for a thing with no name. A title you actually wrote is never a guess, and now it is never overwritten either.

Proving it, since “looks stable” is not stable

The tests come at it from two sides, because the obvious test is a trap. Saving twice in one session and comparing the bytes proves nothing: both saves share one hash seed, so they agree with each other and the suite goes green over a bug that only shows up after a restart.

So one test checks the emitted order against ascending, which is an expectation that exists outside the program. A file whose keys are shuffled identically twice would sail past a same-as-last-time check and fails this one immediately. The other builds ten separate copies of the same graph and demands they serialize identically, which works because freshly built maps get different seeds even inside one process. That is the test that actually catches it.

Then the whole thing runs across every patch that ships with Lux, and it runs down the same road you walk: open the file the way the app opens it, save it the way Ctrl+S saves it, reopen what landed, save again. The two saves have to match to the byte, and everything you authored has to come back. That last clause is the one that matters, because the first draft of this test compared two saves a layer below the app and sailed straight past a bug that was deleting descriptions. A check that cannot fail on the thing it is named for is a decoration. Pull either fix out now and it fails on essentially every patch in the box.

One honest note about your existing files. They were written before the sort, so each one still holds its pins in whatever order it was last given, and the first time you save it the keys settle into place. That is one diff, once, per patch, and it is the last one of its kind. Nothing about the patch changes, and nothing about how it renders changes. After that your saves stop moving.

Your patches are text files. They should behave like text files, and read like a record of what you did rather than a record of what your computer felt like doing that morning. And the things you wrote on them should still be there in the morning.

← All posts