From Rules to a World: A C++ Runtime Verification of Procedural Terrain

— From a reverse-engineered generation model to pre-landing engineering verification

AI-Assisted Game Development · Asset Reverse-Engineering Series · Terrain Pre-Landing Verification

Where this fits: earlier in this series we took apart a game’s procedural level generation — the terrain article read out the recipe that “terrain isn’t stored as a heightmap, but computed on the fly from rules and a seed,” and the UE5 implementation article laid that landing plan out as theory. But reverse-engineering the rules is only half the job. This article uses one real C++ landing test to answer: “can this rule set leave the analysis environment and be reproduced in a production language and at runtime?” and “is the generation cost cheap enough to fit a real game’s loading flow?” It is the bridge across the whole chain — from “understanding a mature system” to “implementing one yourself” — the engineering verification that comes before landing.

Prologue: The Theory Holds — But Does It Hold Up in Engineering?

In the earlier procedural-level reverse-engineering series, we answered one question: a planet’s terrain is not a map, but a recipe that can be generated from rules and a seed. The terrain article read out that recipe — terrain isn’t stored as a heightmap; you give it a rule set and a seed and it computes the ground on the fly at runtime. The UE5 implementation article followed that thread and boiled the whole ground down to a pure function: given a coordinate (x, y) and a seed, return the height at that point — three layers from the inside out: discrete platforms for the skeleton, noise for the texture, stamps for the controllable content.

But getting the rules is only half the job. To actually turn it into a piece of game development, two engineering questions still need answers:

  1. Can this rule set leave the analysis environment and be reproduced in a production language and at runtime? — The prototype is JavaScript in a browser; the production engine runs C++, and the two must compute the *same* piece of ground.
  2. Is the generation cost enough to support a real game’s loading flow? — Runtime generation means tens of millions of evaluations the instant the map opens; too slow and the player just waits.

This article is the verification of those two questions. Each of them hides a follow-up that demands a real number. On the consistency side: the prototype is browser JavaScript — great for live preview and tuning, but it isn’t the engine; when you rewrite it in C++ and move it into the engine, “rewrite” hides a subtle risk — a one-pixel difference is fine, but if some step is mistranslated, the whole terrain grows into a *different* piece of ground, and months of tuning against the prototype are wasted. On the performance side: the theory article’s answer was “just bake it in parallel in the background,” but under that “just” sits an unavoidable number — on an ordinary player’s machine, how many seconds does it take to generate a 2K-resolution terrain?

Let me be honest about the framing up front: this algorithm and this demo are both still early. This is a scouting run — verifying whether “runtime terrain generation” is engineering-feasible at all, not a polished finished product. The terrain is still converging toward the real game’s look, and some parameters are still being iterated (Section 3 explains that part of them can, at present, only be approached by inference — they cannot be exactly reproduced). What this article delivers is evidence that *the path is viable*, not a claim that *the terrain is done*.

So here’s the plan, plainly: port the JavaScript prototype’s terrain function into C++, character for character, then verify two things — it is pixel-identical to the prototype (correct form), and it is fast enough on an ordinary machine (acceptable cost). The former is proven with a “difference image,” the latter with the load-phase timing.

The conclusion first: both answers are yes. The C++ version is pixel-identical to the prototype, with error only in the last bit of floating-point rounding; on performance, a 2K terrain generates in about 1.5 seconds on an eight-core machine — no pressure to fit into loading. But the road to those two conclusions ran through two precision traps that could each “distort the whole terrain,” and exposed one “twenty-times-too-slow” naive implementation — and *those* are what this article is really about.

1. Choosing the Approach: Why Runtime CPU Generation

Before diving in, let me address a premise that could be questioned. “Runtime terrain generation” sounds like a lot of work, and there are in fact three roads on the table: offline full-bake (art bakes the heightmaps, packs them into the game, reads them on open); GPU generation (compute it live with the graphics card’s parallelism); and runtime CPU generation (this article’s choice). The first two are many people’s first instinct — and both were ruled out. Making “why not them” clear is what shows runtime CPU generation isn’t the only thing we knew how to do, but a choice made after thinking it through.

Why “Pre-Baking Everything” Doesn’t Work

Most games do exactly this — art bakes a heightmap, saves it, loads it on open. Stable, fast, worry-free. But that road doesn’t work here; it’s blocked by three hard requirements:

One — it has to “re-generate on a new seed.” This is the core mechanic of games like this — the same batch of planets is replayed, and each run’s terrain must differ, or the second playthrough is dull. If terrain is one baked static image, changing the seed produces no new ground. Only runtime generation lets the seed truly drive the terrain — change the seed and the Voronoi partition changes, and the whole layout of the ground changes with it. Terrain is “alive,” and that’s the premise of the gameplay; the price of “alive” is that it can’t be baked dead into a static image.

Two — terrain can’t eat bandwidth in multiplayer. A 2K heightmap saved as a file is several MB, and to guarantee every client has identical terrain in multiplayer, the most direct approach — the host sends those several MB to everyone — is unacceptable traffic in a real-time game. The “pure function + seed” approach gives an elegant answer: the server sends just one integer seed, and each client computes the same ground with the same function, down to the bit. Terrain never touches the network; bandwidth cost is zero. This is “compute”‘s decisive advantage over “store” in multiplayer — but it has a prerequisite: the function must be deterministic — the same input, on any machine, at any time, must give bit-identical output. That “determinism” requirement becomes a very concrete technical constraint in the consistency-verification section later, and it’s also key to making CPU the authoritative path below.

Three — hundreds of planets can’t be baked, or maintained. The terrain article covered this — no team is going to hand-bake as many terrain images as there are hundreds of planets. Only rule-based generation can hold up an entire star system on a tiny amount of data.

One point worth clarifying, since it’s easy to confuse: ruling out “full-bake” is not ruling out “baking” itself. Landing still has an “editor bake” step — a development-time tool that solidifies one planet’s terrain into an asset for art to polish and to serve as a “reference answer”; it’s part of the toolchain. What’s ruled out here is the *shipping form* of “bake the finished terrain dead ahead of time, read-only at runtime,” not the act of baking. The distinction is made clear in the Section 4 landing roadmap.

Why We Choose CPU as the Authoritative Generation Path (For Now)

Since terrain is essentially “compute a function in parallel per pixel,” a natural thought is: isn’t this exactly what GPUs are best at? Tens of millions of pixels computed at once — the GPU’s parallelism far outstrips the CPU. Why not write the function as a compute shader and hand it to the GPU?

The thought itself isn’t wrong, but there are a few places where it and the “determinism” bottom line from the last section only reconcile at extra cost. On balance, making the CPU the authoritative generation path *at this stage* is the safer choice. Three considerations:

One — cross-device determinism has extra cost. Multiplayer’s premise is that every client computes bit-identical ground — that’s what lets the server decide who’s cheating. GPUs *can* do deterministic computation, but keeping bit-for-bit consistency across GPU vendors and driver versions requires additional constraints on the compute path (avoiding transcendental functions and fused multiply-add whose implementations differ) — a nontrivial engineering investment. For rendering, last-bit differences don’t matter; but for authoritative terrain computation that “must be bit-identical,” piling that constraint onto the GPU is less clean than just using the CPU’s controllable integer / normative floating-point behavior — which is exactly the basis this article leans on when verifying “pixel-identical.”

Two — an authoritative server needs to replay independently. Anti-cheat requires the server to compute the same ground independently, without rendering, and compare against the client’s result. And game servers are usually GPU-less rack machines — so the generation logic shouldn’t depend on the client’s GPU environment, or the server can’t recompute, and thus can’t verify whether the client tampered with the terrain. Authoritative computation must be replayable on a pure-CPU server — this makes the CPU the natural choice for the authoritative compute source.

Three — separate the authority from the rendering, rather than picking one. Taking the GPU out of authoritative computation doesn’t mean it has nothing to do — it still handles what it’s best at: rendering, lighting, and shading enhancement of the ground material. The real division of labor is “the CPU computes authoritative height (deterministic, verifiable, replayable across the network), the GPU does the rendering (fast, pretty, not part of any ruling).” Splitting “what the ground is” and “how to render the ground beautifully” onto CPU and GPU — rather than having the GPU both compute and draw — is the responsibility boundary of this approach. (That is: if a future platform moves generation onto the GPU too, that’s a render-side acceleration copy; authority still lives on the CPU.)

Three requirements ruled out full-bake, three considerations pushed the GPU back to the render side, and what’s left — runtime CPU generation — is at this stage the one road that simultaneously satisfies “seed-changeable, network-safe, verifiable” at the lowest engineering cost. And that approach’s performance bill lands squarely on the CPU function this article verifies.

Three-approach comparison: offline full-bake (seed✗/bandwidth✗/maintenance✗), GPU generation (cross-device✗/no server GPU✗), runtime CPU generation (all green), with GPU still handling rendering noted at the bottom

2. The Prototype Is Right — But the Prototype Is JavaScript

The last few months of work produced a working terrain function — but it lives in the browser, written in JavaScript.

That choice was right at the time. Tuning terrain is intensely visual work: whether the height distribution is right, whether the slopes are steep enough, whether the craters are too deep — all of it is done by looking at the image and iterating. Browser + JavaScript gave the fastest “change one line, see the result immediately” loop — in one web page, sliders on the left tune parameters, a live 3D preview renders the terrain on the right, and you can switch the data source from a dropdown to compare the real game’s terrain images against ours side by side. This prototype environment drove the iteration cost of “aligning to the real game’s look” to the minimum.

In the preview tool: left is the real game's terrain heightmap (reverse-exported), right is our generated terrain, drawn by the same renderer

(The alignment work itself — how the height distribution went from “clumped” to “spread out,” how craters went from “deep pits” to “shallow dishes” — is another story; here it’s enough to know that the prototype is already tuned in, and the terrain it generates has been verified to be statistically indistinguishable from the real game’s high-relief terrain.)

And “re-generating on a new seed” can be seen directly in the tool too — the same recipe, only the random seed changed, grows ground that’s completely different in layout but consistent in style:

Terrain generated from the same recipe with three different seeds — layouts differ, style is unified

But a prototype is, in the end, a prototype. To land it, one bottom line is inescapable: the authoritative terrain computation must run in C++, on the CPU, inside the engine. The reason goes back to the three requirements in Section 1 — network determinism above all: for every client to compute bit-identical ground, you can’t have one running JavaScript and another running C++ each computing their own; there can be only one authoritative version, and that version must be C++ the engine can call directly. The JavaScript prototype’s historical mission was to get the terrain look right; its endpoint is to be succeeded by a pixel-equivalent C++ implementation.

So the task becomes very concrete: translate this JavaScript function, character for character, into C++. It sounds mechanical — just copy it. But “just copy it” is exactly the most dangerous illusion. This function uses hashing and noise heavily — they’re sensitive to every single bit, and if some integer operation’s behavior differs even slightly between the two languages, the output isn’t “a little different,” it’s “unrecognizable.” The difficulty of the translation isn’t in the logic; it’s that the subtle behavioral differences between the two languages in integers, floating point, and bitwise operations must be aligned one by one.

Those subtle seams are exactly where two traps hide — their shared feature: no error, no crash, they just make the terrain quietly grow wrong — and wrong across the whole thing (detailed in Section 5). But before diving into those two traps, we need to first frame one thing: exactly which rule set does C++ need to reproduce, which parts of it are certain, and which are still only approximate — this decides what “identical” should even be measured against. That’s the next section.

3. The Port’s Prerequisite: Which Terrain Rules Need Reproducing

Before moving the function into C++, one thing must be framed: what exactly is being reproduced. The task of this C++ step is not “design a terrain algorithm,” but “rebuild the reverse-engineered rule set, faithfully, in a production language” — so first we have to make clear where this rule set comes from, which parts are certain, and which are approximate. Otherwise verifying “C++ matches the prototype” becomes “matching something of unknown origin,” and the verification loses its meaning. This section lays out the target the port must reproduce.

From Reverse-Engineering to an Executable Model: Not Invention

The skeleton of this terrain algorithm is reverse-engineered, not something we thought up at a whiteboard. That’s exactly what the earlier articles in this series did: take apart the game’s terrain generation and read out its “recipe” — terrain isn’t stored as a heightmap, it’s computed on the fly from a rule set; and the core of the rules is three things:

  • Voronoi tiling — partition the ground into irregular polygon tiles;
  • 6 discrete height tiers — each tile picks one of 6 preset heights, and tile-to-tile the height jumps (this is the source of that “large platforms + steep slopes” terrain);
  • Stamps — use circles, rectangles, and splines to lay down bases, roads, craters — the “must-be-fixed” content.

These three are plaintext rules read out from reverse-engineering, and they’re the starting point of our algorithm. So strictly speaking, what we did is not “design a terrain algorithm,” but “implement the reverse-engineered rules into a runnable function, and tune it close enough to the real game’s look.” There’s little invention in it, and a great deal of reconstruction and approximation.

Getting the Parameters: Direct Read vs. Statistical Approximation

The rule skeleton is plaintext, but the “specific parameters” are another matter — some can be read directly, some can only be approached statistically from the finished product. This directly dictates the two different methods we use to get parameters:

Method one: forward analysis — read directly from the plaintext rules. The game’s type definitions (the metadata describing all data structures) are plaintext, and a batch of definite parameters can be read out: terrain tiles are 6 discrete height tiers, sizes double in a quadtree (31→63→127→255 m), vertex density is a constant one vertex per 0.5 m, stamps come in circle/rectangle/spline kinds. These are hard parameters given directly by reverse-engineering; implement them as-is, no guesswork.

Method two: inference from results — back out the statistical patterns from the finished heightmaps. The problem is, the batch of generation parameters that actually decides “what this planet looks like” (the specific region-rule values, tiling parameters, height distribution) is locked behind authenticated encryption — the files have near-maximal entropy, each is independently salted, and static cracking has no entry point. That road being closed, take another: since we can’t get the generation parameters, infer them from the finished product (the heightmaps) it generates. Concretely — export a batch of the real game’s heightmaps, measure their statistical fingerprint (relief span, height distribution shape, density and depth of craters and mounds, degree of clustering — a dozen or so metrics), then tune our own algorithm’s parameters so that our output falls within the real heightmaps’ range on those metrics — once it does, it means “you can’t statistically tell whether this was generated by us or by the game.”

This inference has a self-check framework answering three progressively deeper questions: can it align (do our stats fall in the real range), will it collide (do different seeds truly produce different ground, rather than being all-the-same), and does it cover (can changing the seed cover the real range’s diversity, rather than only ever producing one standard image). The framework itself is reusable for any reverse-engineering scenario where “you can’t get the source algorithm, only infer from the finished product.”

In practice the two kinds of parameters split roughly like this:

  • Definite parameters (read directly, exact): the Voronoi tiling scheme, 6 discrete height tiers, tile quadtree sizes, vertex density, stamp types.
  • Approximate parameters (statistically aligned, not exact): height distribution shape, crater-to-mound ratio, overall relief, degree of clustering/dispersion.

And here’s where the boundary must be marked honestly: the forward-read parameters are exact; the inferred ones are approximate — we can only reach “statistically indistinguishable,” not “tile-for-tile identical to a specific planet” (that would need its seed and the encrypted generation data, which we can’t get). This is exactly why this algorithm is still early: half the parameters are approached by inference, and they’ll keep being adjusted as analysis deepens. The current result is “the terrain is statistically indistinguishable from the real game’s high-relief terrain” — enough to support the “runtime generation” feasibility verification, but not the endpoint.

Two parameter-getting methods: forward analysis (read plaintext type defs → hard params, exact) vs. inference from results (finished heightmaps → statistical fingerprint → tune-to-approximate, because generation params are encrypted), marked "approximation, not exact reproduction"

The Three-Layer Generation Model: Only the Parts Relevant to C++ Verification

With parameters in hand, the algorithm boils terrain down to a pure function — given a coordinate (x, y) and a seed, return the height there. It nests three layers, evaluated from the inside out: skeleton (Voronoi tiling + 6 discrete height tiers, deciding the “large platforms + steep slopes” signature) → texture (multi-layer noise added on each platform, refining into fightable gentle slopes) → controllable content (stamps flatten the drop zone/bases, connect the roads, place craters at designed positions). The full breakdown of this model is in the earlier articles; here I pull out just the two constraints that bear directly on the verification ahead:

  • Numerical order: the texture layer’s noise amplitude must be far smaller than the skeleton layer’s tier spacing. Once the relief overpowers the step drop, the “large platform” semantics collapse into ordinary noise hills — this constraint must be preserved exactly in the port; get one coefficient wrong and the terrain loses its recognizability.
  • Stacking order: stamps must come after noise. Flatten first then add noise, and the flattened base gets disturbed again. Get the order wrong and it won’t error — it’ll just make the terrain quietly grow crooked, exactly the kind of “silent wrong” Section 5 guards against.

And this “skeleton layer” is precisely where the performance bottleneck in Section 6 lives — it nests dozens of Voronoi-site queries per pixel. Remember for now that it’s the heaviest of the three layers.

Three-layer generation model cross-section: skeleton (Voronoi + 6 discrete tiers) → texture (noise, amplitude ≪ tier spacing) → controllable content (stamps flatten/carve), marked with the two constraints the port must preserve

The Structural Problem: Height Distribution “Clumps” — And How to Solve It

The ground the three layers assemble has the right *shape*, but one recurring flaw that no amount of parameter tuning could shake: the height distribution “clumps.” The real game’s heights are “spread out” — fairly even from low to high; ours always crowd into the middle, sparse at both ends. Rendered, that reads as “one big flat swath at the same height, scattered highs and lows around it” — unlike the real game’s clearly layered terrain.

At first I assumed some parameter was off, and tuned it round after round — none of it worked. Only later did it click that this isn’t a parameter problem, it’s a structural one: our heights are the sum of multiple noise layers, and summing multi-scale noise tends to make the height values cluster toward the middle — the same class of phenomenon as “the sum of several random quantities tends toward a concentrated value.” In other words, “clumping” is a tendency baked into this composition method, not some parameter left untuned. As long as the composition stays “sum of multiple noise layers,” no matter how you tune each layer’s parameters, it’s hard to flatten that concentration tendency.

Given that, the question shouldn’t be “keep tuning the noise parameters,” but should pivot to “adjust the way the distribution is mapped” — and the solution lands on histogram matching. The method: take the whole generated height field and, by each point’s height rank (quantile) within the image, remap it onto the real game’s heightmap distribution curve. In plain terms: if in the real image “the highest 10% of the area” covers some height range, then pull our image’s “highest 10%” into that same range too. The spatial layout (which parts are high, which low) doesn’t move at all; only the shape of the height distribution is swapped for the real one. After this step, the height distribution goes from “clumped” to “spread out,” matching the real image tier by tier.

For the reader who wants to dig deeper: this is a concrete instance of the earlier “inference from results” — the histogram-matching target curve is precisely the distribution measured from the real heightmaps (more precisely, the average cumulative distribution of a group of same-class real images). There’s also a side effect to handle: histogram matching pulls the *bottoms of craters* to the lowest quantile too, making craters too deep. The fix is to extract each crater’s depression amount separately, match only the “crater-removed terrain,” and add the craters back afterward — the terrain distribution spreads out, crater depth stays as it was. This “solve the structural problem not by tuning parameters but by changing the composition method” line of thinking is the single most important breakthrough in the whole algorithm.

The height-distribution structural problem and histogram matching: left "sum of noise layers → heights cluster toward the middle," center "real image = spread out," right "remap by quantile to the real distribution → spread out, spatial layout unchanged," with crater-protection note

By now the “terrain function” — the black box mentioned repeatedly up to here — is fully accounted for: its rules are reverse-engineered, its parameters half forward-read and half inferred-and-approximated, its composition a three-layer pure function, and it finally uses histogram matching to align the height distribution to the real image. The terrain it computes is “right” because it *statistically hugs the real game* — not because it looks good. And precisely because the inferred part is still approaching, it remains an early, still-iterating version.

Now — finally — comes moving this (early but verified-effective) function, pixel-equivalently, into C++ — the main line of this article. But before diving in, let’s spend one section placing this step back into the whole landing plan, to see which of five stages it is.

4. Where This Article Sits in the Whole Landing Plan

First, a boundary, so the performance numbers later aren’t misread: what this article verifies is the “terrain generation core,” not “a complete planet’s runtime.” Later it gives “2K generates in about 1.5 s on an ordinary machine” — that refers to “computing the height field,” not “one map-open loading finishing in 1.5 s.” Real loading also stacks on mesh building, collision, materials, streaming, plus the buildings and vegetation on top of the terrain. This article only nails down the most core, and most uncertain, link in that chain.

Taking terrain from “browser prototype” to “a shippable, network-capable game feature” isn’t one leap — it’s a chain of five stages, each with a definite output, each depending on the previous. This article sits at the chain’s second stage, the verification checkpoint that crosses from “prototype” to “engine.” The whole chain:

① Algorithm prototype (JavaScript, done). Tune and freeze the terrain function in the browser — this is the single algorithmic authority of the whole system. All later implementations use it as the “reference answer.” The months of aligning to the real game’s look produced exactly this stage.

② C++ port and verification (this article). Port the prototype into C++ character for character, and prove two things — pixel-identical (form not mistranslated), acceptable cost (fast enough). This stage produces no playable terrain; what it produces is the confidence to “proceed safely,” plus a list of the traps stepped in. It’s the verification checkpoint between “prototype” and “engine landing” — this article is exactly that checkpoint.

③ Editor bake (later). Wire the verified C++ core into the engine editor, and bake one planet’s terrain into a proper terrain asset (Landscape) with one click. The point of this stage is twofold: one, to give art a “reference answer” — the runtime-assembled ground must match this editor-baked one; two, to leave an entry for art polish (official brushes for local touch-ups, materials and vegetation generated procedurally). Note this “bake” is a development-time tool, not the shipping form — exactly the distinction clarified in Section 1.

④ Runtime assembly (later). Move the same C++ core to runtime, generating and assembling tiles on demand as the player opens the map — rendering, physics, and LOD all land in this layer. The “2K generates in about 1.5 s on an ordinary machine” this article verifies is exactly what clears the performance doubt for this stage. This is the layer the player actually experiences.

⑤ Server verification (later, spanning ④). This stage is the easiest to overlook, yet it’s the linchpin of multiplayer. The server carries no GPU and does no rendering, but it must compute the same ground independently and compare it against what the client reports — a mismatch means the client is tampering with the terrain. This also explains why this article is so exacting about “pixel-identical”: not fussiness, but because in the end four roles (browser prototype, editor, player client, server) must compute bit-identical ground before the server dares rule on legitimacy. Consistency isn’t a nicety; it’s the bedrock of the whole multiplayer anti-cheat.

Connect the five steps and one red thread runs through all of them — determinism: from browser prototype, to editor, to client, to server, the same seed must produce bit-identical ground everywhere. That red thread is the shared reason Section 1 made CPU the authoritative path and this article agonizes over “pixel-identical.” This article (stage two) is the first “cross-language” verification point on that red thread: if even the JavaScript-to-C++ hop doesn’t line up, the editor/client/server alignment further down is out of the question.

The five-stage landing roadmap for procedural terrain: ①JS prototype (algorithm authority) → ②C++ port verification (this article) → ③editor bake (dev-time tool) → ④runtime assembly (player experience) → ⑤server verification (anti-cheat), with a "Determinism: bit-aligned across the whole chain" band running along the bottom

5. Pixel-Identical: Two Precision Traps That Could Distort the Whole Terrain

After translating JavaScript to C++, the first time I compared the two versions’ outputs — the whole terrain was off. Not some corner; the heights across the board didn’t match, with a max deviation of twenty-odd meters (for reference, the terrain’s total relief is only thirty-odd meters — so this amounts to translating the terrain into a *different* piece of ground).

There are two ways to debug at this point. One is “stare at the most-wrong point and reason back to which step miscomputed”; the other is “print out the intermediate value of every layer, compare layer by layer, and see where they start to diverge.” The first looks direct but is really guessing inside a coupled tangle; the second is dull, but every step is deterministic. I chose the second — print the intermediate values layer by layer, bisect to locate. Once the divergence point is pinned, the culprit can’t hide. This time we caught two traps, both extremely subtle.

Trap One: Two Kinds of “Salting” That Look Almost Identical

The terrain function uses “salted hashing” everywhere — mix a coordinate and a fixed “salt” (a magic constant) into the hash function to get a pseudo-random value. The catch: the prototype has two salting styles that look almost identical in JavaScript but mean entirely different things:

  • One is “direct salt“: XOR the seed with the salt and use it directly.
  • One is “hashed salt“: XOR the seed with the salt, then run it through another hash before using it.

The difference is that “another hash.” If in translation you mistake a “direct salt” point for a “hashed salt” one and wrap an extra hash, that point’s pseudo-random value changes entirely — and it happens to feed the step that decides a tile’s “tier” and “displacement multiplier,” so one error misplaces the skeleton of the whole terrain. This isn’t a bug in the algorithm; it’s semantics lost in the port: the prototype’s logic is fine; only the information “should this spot be hashed or not” got mistranslated when copied from one language to another.

For the reader who wants to dig deeper: the two styles in the prototype are — direct salt is (seed ^ SALT) >>> 0, hashed salt is hash((seed ^ SALT) >>> 0). In translation, the former must be written as a bare XOR in C++, no hash wrap; only the latter wraps. Across the whole function, seven “direct salt” spots were wrongly hash-wrapped by me at first — spread across deciding sub-region type, sub-region jitter, site jitter, and edge warp. Locating it took printing the intermediate variables of both versions all the way down at the most-deviated pixel, until I found one step’s “site tier” reading 0.1 in one version and 2.8 in the other — chasing that 2.8 back turned up the extra hash. The lesson is plain: before translating any salting spot, go back to the source and confirm which kind it is; don’t go by feel.

Trap Two: JavaScript’s “Wrong Answer” Is the Correct One

After fixing the salting, the deviation dropped from twenty-odd meters, but comparing the full pipeline threw up a fresh six-meter whole-image offset. This culprit is, arguably, the most counterintuitive of the entire port.

Terrain’s final step picks one of five presets by the seed (which one it picks decides the whole image’s height distribution shape). “Which one” is derived by multiplying the seed by a large constant and taking a modulo. C++ dutifully used exact integer multiplication — and picked the wrong preset, so the whole image’s height distribution went wrong with it.

The problem: the result of this multiplication exceeds the range JavaScript numbers can represent exactly. In JavaScript all numbers are floating point, and once the product grows past a certain threshold it quietly drops low-order precision — what it computes is an *approximation*. And this terrain prototype was tuned and verified *in JavaScript* — meaning JavaScript’s “wrong approximation” is the value this terrain actually uses, the one signed off on as “true.” The “true value” here needs to be stated precisely: it means an engineering spec compatible with the existing prototype behavior, not the mathematically correct value. The “mathematically correct result” C++ computed with exact integers *doesn’t* match the prototype — because the prototype never used that correct result; it used that approximation, and the whole terrain was tuned to the approximation.

This spins out a rather philosophical conclusion: the goal of a port isn’t “compute correctly,” but “compute exactly the same as the prototype” — including making the exact same mistake. The fix is to have C++ deliberately reproduce JavaScript’s floating-point precision loss, rather than use its own more-precise integer arithmetic.

For the reader who wants to dig deeper: the selector is (seed * 2246822519) >>> 0. The seed times that constant easily exceeds 2⁵³ — the upper limit of integers JavaScript floating point (IEEE 754 double) can represent exactly. Past that, the mantissa is rounded, and when >>> 0 takes the low 32 bits, it takes “the low 32 bits of the *rounded approximation*.” C++ computing exactly with 64-bit integers picks a preset one off from JavaScript’s — and one preset apart, the height distribution shape is completely different, the whole image off by six meters. The fix: C++ does the multiply in double then mods by 2³², actively reproducing JavaScript’s floating-point rounding. Rule of thumb: when porting any “large-multiply-then-truncate” hash or selector, estimate the product’s magnitude first — once it crosses 2⁵³, the source language (JavaScript) necessarily loses precision, and that precision-losing behavior is itself the spec to reproduce.

With both traps fixed, comparing again — pixel-identical. For the same seed and resolution, the C++ and prototype outputs deviate by at most 0.000002 m (two microns, i.e. two-thousandths of a millimeter), and that residual is purely the last-bit rounding of floating-point arithmetic, unrelated to correctness. Amplify the two versions’ difference two-thousand-fold and draw it as an image — pure black, not one bright spot to be found.

The difference image of C++ vs. the prototype: a pure-black square marked "amplified 2000× still all-black, max deviation 0.000002 m = float rounding," with two fixed traps listed in small text alongside

Consistency — this gate is passed. Next is performance.

6. From “It Runs” to “It’s Usable”: Tracing the Performance Bottleneck

The C++ version was pixel-aligned, but the first performance test — shockingly slow. A 2K terrain took over seven seconds even with multi-core parallelism; single-core, two minutes. If that number were the endpoint, the runtime-generation approach would be out — no player will wait two minutes for a piece of ground.

But “slow” is worth first asking “slow where,” rather than rushing to change languages or add machines. The move is to count one thing: to generate one pixel, how many times does it call the low-level noise function? (The noise function is the basic brick of the whole terrain computation, and the vast majority of time is spent in it.) The result is startling: generating one pixel calls the noise function about 2742 times. And 98% of that — about 2700 calls — is all piled into one function in the “skeleton layer.”

Why 2700 for one pixel? Because this skeleton-layer function nests layer upon layer:

  • Each pixel looks at the 7×7 = 49 “sites” around it (Voronoi seed points) and does a weighted average;
  • Each site then finds the nearest among the 3×3 = 9 sub-regions around it;
  • Each sub-region computes noise twice (once for the tier, once for the elevation base), and each noise call unfolds into 3 layers internally — 6 low-level noise calls.

Multiply it out: 49 × 9 × 6 ≈ 2700. For one pixel. A 2K terrain has four million pixels — that’s tens of billions of noise calls. Slow, of course.

But the real problem isn’t “computing a lot,” it’s “computing all-repeated stuff.” The key: a site’s tier and type depend only on its own integer coordinate — independent of which pixel is looking at it. Two adjacent pixels: of the 49 sites each looks at, 48 overlap, completely identical. Yet the current code recomputes all surrounding sites from scratch for every pixel.

An analogy: it’s like running a fresh door-to-door census every time you want to know how many people live next door — when you only needed to look it up in a register once. Between adjacent pixels, this “census” is redone hundreds of times over.

The nesting-explosion diagram of 2742 noise calls per pixel: skeleton layer is 98%, expanding to 7×7 sites × 3×3 sub-regions × 6 noise ≈ 2700, with a "adjacent pixels overlap in 48/49 sites yet recompute" note

The root of the performance bottleneck is thereby located: it’s not “C++ isn’t fast enough,” nor “the algorithm is too heavy,” but a naive implementation that keeps throwing away results and recomputing them. The root being “repetition,” the fix follows naturally.

7. The Site Precompute Table: Twenty-Times Speedup

The fix, in one sentence: stop running a census per pixel; look up the register once at the start, and every pixel reads the table thereafter.

Concretely: since each site’s tier and type are decided only by its integer coordinate, before generation begins, compute every site’s tier once and store it in a small table. Whenever a pixel needs some site’s tier afterward, it reads the table — one O(1) memory read, instead of recomputing that site’s 9 sub-regions and dozens of noise calls from scratch.

How small and fast is the table? A 2K terrain covers about 1936 sites, each storing a tier and a multiplier — a few-KB table, 1 ms to build. With that 1 ms, 98% of the skeleton layer’s repeated computation is eliminated.

The effect is dramatic:

Resolution Naive port (32 cores) With precompute table (32 cores) Speedup
1024² 1341 ms 92 ms 14.6×
2048² (2K) 7153 ms 352 ms 20.3×
4096² (4K) 39810 ms 1272 ms 31.3×

2K drops from over seven seconds to 0.35 seconds — twenty times. And the larger the resolution, the greater the speedup — because a bigger image means more overlapping sites between adjacent pixels, and more repeated computation saved. With the table, the skeleton layer’s share of time drops from 98% to nearly negligible, and the terrain’s cost is mostly in the necessary displacement layer’s 40 noise calls — *that’s* this algorithm’s “true cost.”

Worth noting: this optimization isn’t some deep trick — the JavaScript prototype had actually already done half of it (it used a cache to avoid recomputing sites). The first C++ version deliberately skipped that cache to get it running and aligned on consistency first — which is exactly what exposed how high the true cost of a “naive port” is. This also shows, from the side: performance anxiety often comes from “a lazy implementation,” not “a slow algorithm” — pinning the root to “repeated computation” is far more effective than blindly switching languages or piling on hardware.

Before/after of the precompute table: left "recompute 2700 noise per pixel," right "build table 1ms at start → look up per pixel," with a bar chart in the middle showing 2K cut from 7153ms to 352ms

8. The Performance Verdict: A Load-Phase Time Budget

With the optimized numbers, the key question from the prologue — “on an ordinary player’s machine, how many seconds to generate a 2K terrain” — can finally be answered head-on.

The test machine is a 32-core dev box, but players won’t all have that many cores. Here’s a rough estimate by core scale: taking an ordinary player machine as eight cores (about a quarter of the test machine’s cores), and scaling the time proportionally — this is only an order-of-magnitude conversion, and the actual numbers need re-testing on target hardware (memory bandwidth, cache, single-core clock all factor in; it isn’t strictly linear):

Resolution 32-core dev box (measured) Eight-core player machine (rough estimate)
1024² 0.09 s ~0.4 s
2048² (2K) 0.35 s ~1.5 s
4096² (4K) 1.27 s ~5 s

The verdict is clear:

  • 1K, 2K fully viable. 0.4 to 1.5 s on an eight-core machine — no pressure at all to fit into loading; the player wouldn’t even sense the terrain is runtime-generated.
  • 4K also viable. About 5 s on eight cores, acceptable for one map-open load; and in practice you can spread those 5 s out with a “compute the spawn surroundings first, fill the rest as you play” strategy.
  • The prerequisite is the precompute table. The naive port’s 2K takes nearly 30 seconds on an eight-core machine — unacceptable. Between viable and non-viable sits Section 7’s 1-ms table.

Back to the theory article’s line “just bake it in parallel in the background” — now “just” has concrete backing: not “any old computation will do,” but “after eliminating repeated computation with a precompute table, 2K generates in 1.5 s on an ordinary machine.” The runtime-generation path holds up on performance.

One boundary to mark honestly: what’s measured here is the height-field computation itself — getting each point’s height. In real landing, beyond the height field there are steps like “build mesh from height,” “generate collision,” and “attach material,” each with its own cost, to be measured separately in a real engine. But the height field is the heaviest link in the whole chain, and the only one that “must be runtime-generated, can’t be pre-stored” — its passing means the most critical piece is settled.

Performance verdict chart: three resolutions' seconds on an eight-core machine (0.4/1.5/5s), 1K/2K marked green "no pressure into loading," 4K marked yellow "acceptable," with a note "naive port 2K needs 30s = non-viable, the gap is that 1ms table" at the bottom

9. Consistency Verification: Both Versions in the Same Preview Tool

The numbers proved the cost is acceptable, but the form-consistency still lacks an intuitive account — a pure-black difference image is ironclad proof, but a bit abstract. The most intuitive verification is drawing the C++-generated terrain and the prototype-generated terrain into 3D with the same renderer, and comparing side by side.

The method: have C++ generate one complete terrain, encode it into a format the preview tool can read, then import it from the tool’s data source — going through the exact same 3D render pipeline as the prototype. So in the one tool, you can switch back and forth between “the C++-generated ground” and “the prototype-generated ground,” rendered from the same angle under the same lighting.

The result — top is the C++ version, bottom is the prototype:

C++-generated terrain, imported into the preview tool's 3D render
The prototype-generated same terrain, imported into the same preview tool

The two images are visually identical — under the same seed, C++ and the prototype render the same piece of ground. Every layer’s contribution to the full pipeline can be read in the image: the green basins at bottom-right and along the bottom are low ground spread out by the height remapping overlaid with clustered crater fields; the scattered small rings are crater stamps; the clumped white bumps are the mound-cluster layer; the large flat white area at top-left is the skeleton layer’s high platforms. These details — craters, mounds, platforms, basins — correspond one to one across the two versions, identical in position, shape, and count.

At this point, both suspenses have answers: the C++ version is pixel-identical to the prototype (correct form), and a 2K terrain generates in 1.5 s on an ordinary machine (acceptable cost). Procedural terrain generated at runtime — its feasibility — verified.

10. This Article’s Place, and Transferable Lessons

To sum it up in one line: it proves “runtime terrain generation” isn’t a paper idea — the prototype’s terrain function can be ported pixel-equivalently into C++, and after optimization is fast enough to fit into loading.

Section 4 looked at this article’s place from the development-stage angle (stage two of five); here, from another angle, its place among the series articles:

  • Teardown article — figure out how this game does procedural levels with “rules + seed”;
  • Terrain article — terrain stored not as heightmaps but as recipes: tiling + discrete heights + stamps + procedural coloring;
  • UE5 implementation article — lay the landing plan out as theory: three-layer pure function, background parallel bake, player-position-driven streaming;
  • This article (pre-landing verification) — use one real C++ landing to nail down the theory article’s two suspenses (is the cost acceptable, is the form consistent);
  • Landing article (later) — actually move this verified thing into UE5, running mesh, collision, materials, and streaming together.

This article is that verification checkpoint between “theory” and “landing.” It produces no playable terrain; it produces the confidence to “proceed safely” — plus a list of the traps stepped in, so the real landing needn’t step in them again.

For anyone doing procedural terrain, three transferable lessons from this article:

  • Between prototype and engine implementation, the biggest risk is “quietly mistranslating.” Hashing and noise are sensitive to every bit; the two languages’ integer/float behavior differences must be aligned one by one, and the only reliable verification is “pixel-by-pixel comparison + difference image” — eyeballing the render is not enough.
  • “The goal of a port is to compute correctly” is an illusion — the goal is “compute exactly the same as the prototype.” If the prototype computed some “approximation” due to a language quirk and tuned the result to it, that approximation *is* the spec, and C++ must reproduce it — even if that means deliberately reproducing an “error.”
  • Performance anxiety often comes from a lazy implementation, not the algorithm itself. First measure “how many times one pixel actually computes,” pin the root to “repeated computation,” and often one precompute table buys an order-of-magnitude speedup — far more effective than blindly switching languages or piling on hardware.

11. AI Collaboration Retrospective

This is the series’ standing section: a record of how human and AI actually worked together on this piece, where it went off the rails, and how the human patched it.

Where the AI helped:

  • Aligning the pixel-by-pixel port is mechanical cross-checking, which the AI is good at. Translating a function of a hundred-plus lines, dense with hashing and noise, character for character from one language to another, and comparing intermediate values layer by layer until the divergence point converges — this “highly deterministic, extremely patience-demanding, no-vagueness-tolerated” work is exactly where the AI performs steadily. Locating both precision traps came from “printing intermediate variables all the way down at the most-deviated pixel,” not from guessing.
  • Locating the performance root came from “measure first, then change.” Facing “slow,” the AI didn’t rush to switch languages or add threads, but first counted “how many times each pixel calls noise,” pinning 98% of the cost precisely to the skeleton layer’s repeated computation — once that number was out, “precompute table” was the natural solution, and the twenty-times speedup was to be expected. Quantitative attribution beats intuitive guessing — this keeps being confirmed.
  • Consistency verification reached “visualized ironclad proof.” Not settling for “close enough on the numbers,” but amplifying the two versions’ difference two-thousand-fold into an image and importing both into the same 3D renderer side by side — turning “identical” from a number into a visibly black image and two overlapping terrains.

Missteps and the human’s patch:

  • The “deep crater” bug was fixed wrong three times running, because I stared at one spot and guessed. A “some spots deeply sunken” problem came up mid-iteration, and I first stared at the “crater field” logic and changed it three times (the protection logic, the bowl shape, the mutual exclusion) — all ineffective, because the culprit wasn’t in that section at all, but in another early-legacy, overlooked generation spot. It was the dull move of “search out every place that generates a crater and print the actual depth values” that located the culprit in two seconds. Lesson: to fix a bug, first find every relevant generation spot and print the actual values; don’t stare at the most suspicious one and guess repeatedly. This lesson was pointed out by the human watching from the side as I kept failing.
  • The distinction between “compute correctly” and “compute the same” only dawned mid-debug. For that float-precision-loss trap, the first reaction was “C++ computes more precisely, the prototype must be wrong” — nearly went off to “fix” the prototype. It was the realization “the prototype is the signed-off true value, and C++ must reproduce it including its approximation” that turned the direction around. **The AI has an instinct to “pursue correctness,” but in a porting context, “faithfully reproduce the source implementation” *is* correct — this value ordering needs the human to anchor.**
  • The data-vs-code boundary at archiving was gatekept by an established rule. This produced both code and docs that belong in version control, and bulky comparison images and heightmap data. Under the “commit code, don’t push data” rule, the comparison images and the heightmaps used for import were excluded from the repo, kept local only — a rule the human set earlier, which the AI needs to actively enforce each time it archives, rather than committing everything at once.

How it was resolved: make “print intermediate values layer by layer, bisect to locate” the default debugging move for a port (rather than staring at the final value and guessing); establish “the goal of a port is to faithfully reproduce the source implementation, including its approximate behavior” as a principle; make “quantify and attribute before optimizing” the first reaction to a performance problem; leave the data-vs-code archiving boundary to the human in the loop. This whole set is what makes “pixel-identical + runtime-viable” both ironclad and unexaggerated.


*The infographics are all self-drawn (light theme, corresponding line by line to the text); the 3D terrain comparisons are actual render captures from the preview tool. The performance data comes from the C++ implementation measured on a dev box (32 cores); the eight-core player-machine numbers are conservative estimates by core-count ratio, to be re-measured on real engine hardware at landing. The consistency data (max deviation 0.000002 m, pure-black difference image) comes from a pixel-by-pixel comparison of C++ and prototype outputs under the same seed. The specific game terrain forms mentioned are all rule-granularity reconstructions and do not involve the original’s encrypted specific generation parameters.*

Leave a Reply

Discover more from AI Native Game Development

Subscribe now to keep reading and get access to the full archive.

Continue reading