The town square had a fountain. I knew this because I had painted it, tile by tile, into the 2D game’s village map, and because the dialogue, the shop and a story beat all referred to it. The 3D game, a village built in Godot to answer whether the same story should move to three dimensions, had a well in the square. It had a well because the first 3D map was a text grid with a legend, and whoever typed the legend wrote a well. Both games ran. Both passed every check they had. The playtester walked from one to the other and asked why the square kept changing, and that was the first anyone knew. The decision took a minute: it is a fountain, and the well is a side-yard detail. The fix took longer, because the point was never which one. It was that the word had to be written once, somewhere both games read, and rendered by each.

The 3D game was not supposed to have its own opinion about the square. From the start it had read the 2D game’s own files: on every refresh a script copies a hundred and twenty of them, the story graph, twenty-eight dialogue files, every cutscene and battle, and records a SHA-256 for each. One file in the 3D code knows the 2D schema, and its header carries the whole mapping. So the story, the cast and the words were guaranteed to agree. The square disagreed anyway, because the square is not in the story files. It is in a picture, and pictures were the one thing I had decided the two games would never share.

That decision had a second half I was equally sure of. A side-scrolling level is an elevation, a picture of a cliff face; a 3D level is a plan. The platformer levels could not port, I wrote, because the player has gained an axis. A month later the 3D game grew a planar mode, movement locked to one axis under an orthographic side-on camera, and in that mode the 3D game is an elevation too. The seaside crossing is now derived from the 2D level’s own tilemap: shelf, pits, planks, ledges, creatures, chests and exit. What genuinely refused to port turned out to be one number. The 2D hero’s jump peaks at 2.0 times her own height; the 3D character’s measured peak is 0.61 of hers, and no scale satisfies both the level’s proportions and its reach.

Diagram: the shared layer is the dashed middle column; the two presentations flank it. Walk the four views in order: the setup, the square that was a well, why nothing caught it, and the manifest that fixed it.

The principle

Two renderers can share what a thing is. They cannot share where it sits or how it looks, because the moment the shared layer holds a coordinate or a colour, it has taken one renderer’s side, and to the other renderer that coordinate reads as content when it is actually presentation. Share the meaning. Fork the presentation. Everything that goes wrong in a two-renderer project is one of those two halves leaking into the other.

Why the failure hides

Nothing was broken. That is the whole mechanism. The 2D game was consistent with itself and the 3D game was consistent with itself, and a test can only compare a thing against a fact, so a disagreement between two self-consistent systems has no fact to fail against. The fountain and the well were not a bug in either game. They were a bug in the space between them, and neither game owned that space.

The second reason it hides is that the obvious first attempt at sharing makes it worse. When two implementations of one game need to agree, the natural move is to lift one implementation’s data and hand it to the other: here is the village map, twenty-six by fifteen tiles of sixty-four pixels, read it. The 3D game can read that. Its grid is nineteen by sixteen cells of two and a half metres, and a tile does not become a cell, but it will render something, and something looks like agreement. An abstraction extracted from one implementation is shaped like that implementation, and it will pass every check until the day the other side needs to change.

The third reason is that a shared layer wants to grow. Any feature only one renderer can do, a shader, a parallax layer, a spring bone, a jump arc, either leaks into the shared layer and rots it or lives outside it and makes it partial. Both outcomes look like progress. The shared layer has more fields; the two sides agree on more things. What has actually happened is that the shared layer has learned what a sprite is, and from then on one renderer is reading the other’s drawing.

The shape of the fix

The fix is a manifest per place that records nouns and never positions, and it costs one JSON file per location plus a lint on one side and a bind on the other.

First, find the space between the systems by listing every thing both renderers describe. The list is short and it is where every fountain lives: the landmark of each place, its water, its buildings and what kind of room is inside each, its market, its exits, its flora. Not one of those has a coordinate in it.

Second, write one manifest per place with exactly those fields. The square’s manifest says landmark: fountain, one word written once, and the well is listed as side-yard flavour. Both games render that word now. The 3D game binds its map glyphs to the manifest, so the glyph for a landmark renders whatever the place declares its landmark to be. The 2D game does not read the manifest to draw; it lints its hand-painted tilemap against it, and a painted well where the manifest says fountain fails the build. Layouts stay human-owned on each side. The manifest is only what they may not contradict.

Third, decide the two failure modes at design time, because they are opposite. A missing manifest is fail-soft: the place renders as it did before, and the log says so once. An unknown field is fail-loud, because a typo in a field name must not become a silent no-op that both sides ignore in different ways.

Fourth, when one number genuinely cannot share, put it in exactly one place and say why. The crossing is built at 0.758 metres per tile, stated in that file’s header with the two jump heights beside it. The number is presentation and it lives with the presentation. It is not in the manifest, and it never will be.

Fifth, apply the same rule to the save. The save is the 2D game’s own file, schema 2, fourteen fields. The 3D game loads it, holds the fields it has no system for, hearts and pets and the chosen hero, and writes them back unchanged. A field you do not understand is not yours to drop; it is meaning that belongs to the other side.

Sixth, test the contract, not the renderers. The shared package carries sixteen conformance vectors for a fishing table both games read, seed to XP, so a value edited in one game and read differently by the other fails in a check rather than in a player’s hands. One of those vectors hashes the seed off its exact decimal string, because JSON has one number type and it quietly loses a sixty-two-bit seed. That vector is the fountain again, one level down: two sides agreeing on a number that was never the same number.

The cost was a day. One repo of JSON, a lint, a bind, and a habit of asking, at every new field, whether it says what a thing is or where it is.

Share the meaning, fork the presentation, and never let the shared layer learn what a sprite is.