Revision note (2026-09-30): This article records work from 2026-07-13. Early proxy designs, sample measurements, and evidence about the original algorithm are distinguished below. Pending work retains its historical validation scope. Later progress is covered in heightmap reconstruction and POI, Stamp, vegetation, and rock layouts.
— 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:
- 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.
- 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*.
This article ports a frozen JavaScript prototype to C++ and measures pixelwise height error and computation time. Numeric comparison checks the port, while timing estimates cost. Neither validates the original algorithm or complete engine loading.
This test ports the early JavaScript prototype to C++. The maximum height difference for matching test inputs is 0.000002 m: an error comparison, not bitwise equality. Timings were measured on a 32-core development machine. About 1.5 s on eight cores is a core-count estimate requiring target-hardware measurement.
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.
Local reconstruction reduces routine transmission of complete height fields, but a Seed needs matching algorithm versions, configuration, assets, and numeric rules. Session information, layouts, digests, and sometimes repair data still cross the network. This article did not validate a one-integer protocol, and a digest mismatch alone is not evidence of cheating.
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:
The server must validate generation inputs and outputs independently. A mismatch can come from versions, assets, execution paths, or numeric differences; it does not by itself establish cheating. Input errors and repairable content differences need separate handling.
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.

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.

Without complete calculation rules, the prototype used reference height distributions and shape statistics to constrain parameters. Similar selected metrics do not establish statistical indistinguishability or original-algorithm parity. This article tests the proxy port; high file entropy alone does not establish why inputs were unavailable.
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:

This project ports the prototype to an engine-callable C++ core for a shared implementation and independent server recomputation. Shared source does not automatically guarantee cross-platform bitwise agreement; inputs must be fixed and outputs compared. JavaScript remains the porting baseline for this prototype.
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
The port must preserve the frozen prototype, including its numeric behavior. Static structures, measured samples, and prototype choices need separate labels so that JavaScript-to-C++ agreement is not mistaken for original-algorithm parity.
From Reverse-Engineering to an Executable Model: Not Invention
The early prototype uses unpacked types and terrain measurements, but its height function includes engineering assumptions and statistical approximations. It explores implementation cost; it does not establish a recovered three-layer original algorithm.
- Voronoi tiling — partition the ground into irregular polygon tiles;
- This section describes the early proxy model: base terrain, surface variation, and local modification. It tests the JavaScript-to-C++ port, not an original six-step terrain algorithm. Later reconstruction uses more detailed stages and static height assets.
- 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:
Type definitions establish Voronoi, subregion categories, and support-shape structures; grid sizes and vertex spacing come from sample measurements. The early six-step interpretation was withdrawn and is not a directly recovered original parameter. This C++ port targets the proxy model used at the time.
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:
- Approximate parameters (statistically aligned, not exact): height distribution shape, crater-to-mound ratio, overall relief, degree of clustering/dispersion.

The Three-Layer Generation Model: Only the Parts Relevant to C++ Verification
- In this proxy, noise amplitude is kept below the chosen base-step spacing so that the prototype retains its intended shape. This is a proxy parameter constraint, not an established rule of the original algorithm.
- This proxy applies its local flattening step after noise to avoid disturbing the flat areas. That order is a prototype choice and must not replace the later recovered distinction between Location support, natural Stamp textures, and Location textures.
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.

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 prototype combines a base field, local variation, and height remapping. Reference statistics constrain its parameters, but agreement on selected statistics does not establish matching spatial structure or the original computation. It remains an experimental model.
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
The roughly 1.5 s figure is an eight-core estimate of height computation, not a measured complete loading time. Meshes, collision, materials, objects, and network readiness remain outside that timing.
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++ verification measures error against the frozen JavaScript prototype and records height-core timings. This establishes a porting baseline for engine integration; original-algorithm parity and complete runtime performance require separate tests.
③ 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.
Precomputation reduced height-computation time in this prototype. The roughly 1.5 s eight-core figure remains an estimate. This supports further engine integration and measurement, not acceptance of target-hardware or full loading performance.
Consistency tests first fix inputs, versions, and the representation being compared. Floating-point error between JavaScript and C++ is a different experiment from final R16 digests across server and clients; one does not validate the other.

5. Pixelwise Error Validation: 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 ishash((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>>> 0takes 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 indoublethen 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.
After fixing both porting errors, the maximum height difference at matching Seed and resolution is 0.000002 m (two microns). This is pixelwise error measurement, not bitwise equality. A nearly black difference image only indicates low contrast under its display scale; identifying a rounding cause needs intermediate-value analysis.

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 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.

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:
- The eight-core range of 0.4–1.5 s is estimated from development-machine measurements, not measured on eight cores. It excludes meshes, collision, objects, and network readiness. Complete loading cost needs target-hardware measurement.
- 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 naive eight-core timing is also a core-count estimate, not a target-machine measurement. Development-machine timings before and after optimization demonstrate the effect of reducing repeated work.
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.

9. Consistency Verification: Both Versions in the Same Preview Tool
Timing covers height computation, excluding meshes, collision, objects, and network readiness. The difference image visualizes the comparison; pixelwise statistics establish its numeric range. Neither represents complete loading acceptance.
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:


The main forms correspond under matching camera and lighting. The images help detect visible spatial shifts. Precision remains defined by the measured maximum pixelwise error of 0.000002 m; appearance does not establish bitwise equality.
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.
- The difference image and shared-camera renders make spatial errors easier to inspect. They complement numeric comparison, but a black image does not prove bitwise equality.
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.
Diagrams describe the early prototype and port; 3D images come from the preview tool. Performance was measured on a 32-core development machine, with eight-core figures estimated by core count. Matching test inputs produce a maximum height difference of 0.000002 m. This tests the JavaScript-to-C++ proxy port, not original-algorithm parity.