Thanks for looking. This project has one unusual constraint and it is not the obvious one, so the first section is worth reading before you write any code.
The generator must match Minecraft. Everything else is ours to change.
GPBPF began as a port of an unmaintained Java tool, and for a while "correct" just meant "identical to that tool". It doesn't any more. The Java program's argument order, output format, error handling and assorted warts are not a specification, and we are not obliged to carry them. If a better interface, a nicer output, or a fixed footgun makes this a better tool, that is the point of the project.
What is not negotiable is the bedrock generator itself: the hash, the xoroshiro128++ stream, the splitmix64 seeding chain, the per-layer probabilities, and the exact floating-point comparison at the end. Those belong to Minecraft, not to us. A pattern finder whose RNG has drifted prints coordinates that do not exist in anybody's world — and it does so silently, because you cannot tell a wrong answer from a rare formation by looking at it. That is a far worse failure than any interface wart.
That part is pinned by recorded vectors — match counts and SHA-256 digests in
tools/vectors.json, captured from a build that was cross-checked against the
Java implementation the generator originally came from, and verified on every
build since. Nothing external is needed to run it:
make test # 24 cases + fp_proof. Only needs the binary and python3.A red gate means one of two things, and they are not interchangeable:
- The generator drifted. That is a bug. Fix the code, not the vectors. Re-record only when you have independently established the new output is right — "the gate is red and I would like it green" is not that.
- You changed the interface on purpose. Output format, CLI shape, one of the
inherited quirks below. Entirely legitimate: update the case in
tools/check.py, re-record, and add a row to Deliberate divergences so the next person can tell intent from regression.
sudo dnf install gcc openssl-devel python3 # or your distro's equivalent
git clone git@github.com:codedsword/GPBPF.git
cd GPBPF && make && make testThat is the whole thing. No JDK, no maven, no second checkout, no network — the gates run against a checked-in vector file.
For the GPU path you also need the CUDA toolkit and a host compiler nvcc
accepts (CUDA 13.x wants gcc ≤ 15; make cuda uses g++-15 automatically if it
is installed). Without a GPU, make alone builds the OpenMP path and everything
except the CUDA-specific behaviour is testable.
| command | what it proves | needs |
|---|---|---|
make test |
the generator still produces the recorded output, a second independent implementation agrees the output is right, and the float narrowing in bd_classify is exhaustively safe |
python3 |
make webtest |
the web server returns exactly what the CLI does, and its input validation holds | python3, and the binary it builds |
Both take seconds; run both.
A red make test means one of two things, and they are not the same: the
generator drifted (a bug — fix it), or you changed something on purpose
(fine — update the harness and record it under
Deliberate divergences).
Behaviour we carried over from the Java tool. All of it is now open to change — but which kind of change depends on which side of the line above it sits.
-
A missing
pattern/directory matches every column. An empty pattern makes the formation check vacuously true, so the tool prints one line per column searched. This is pure interface, and ours to fix: the web GUI already refuses it, the CLI still reproduces it. Making the CLI refuse it too is a perfectly good pull request. -
The Nether roof band looks inverted.Answered on 2026-08-19: the Java tool got it wrong, and the fix is in. It had the band solid aty=123thinning upward, plus a phantom always-bedrock layer aty=128that is outside the Nether entirely; vanilla wraps that rule innot, so bedrock is solid aty=127and thins downward toy=123. The negation cannot be folded into the probability either —!(f < d)andf < 1-dhave the same density but pick different columns.Two further things fell out of answering it. The rule was being applied in the Overworld, which has no roof rule at all, and the Nether — which does — runs both its rules on
java.util.Random, because its noise settings carrylegacy_random_source: true. So everyy >= 0result this tool printed before that date was wrong, and the Nether floor aty0…4 was being routed into the roof branch.Settled the way this section asked for: against the game's own generation data (
material_rule/bedrock_{floor,roof}.jsonandnoise_settings/{overworld,nether}.json), read at every release tag from 1.18 to 26.3-snapshot-9, where they are byte-identical. Not yet against a real world — see README, "Known limits".
Changes where we have knowingly stopped matching the Java tool. Add a row when you make one, so the next person can tell intent from regression.
| what | why |
|---|---|
| The web GUI refuses an empty pattern | The CLI's behaviour here is a footgun that emits one line per column searched |
| The Nether roof and floor follow vanilla, not the Java tool | The Java tool mirrored the roof band, put it in the wrong dimension and used the wrong RNG. The one rule wins: the generator matches Minecraft. Five vectors were re-recorded; see the provenance block in tools/vectors.json |
| The Java cross-check became recorded vectors | The project stands on its own; the vectors carry the same guarantee without needing the original to build or test |
Seven places diverge silently if you "simplify" them. These are the parts that belong to Minecraft rather than to us, so unlike everything above they are not open to redesign. All are commented in the source; this is the short list.
-
bd_hash(bedrock.h).x * 3129871is a 32-bit multiply that wraps and then sign-extends — it is not(long)x * 3129871L. Meanwhile(long)z * 116129781Lreally is 64-bit. The asymmetry is deliberate. The final>>is an arithmetic shift, not>>>. These only bite past |x| ≈ 686, which is why the harness tests coordinates out there. -
nextFloat()stays single precision.next(24)is exactly representable in binary32 and the multiplier is exactly 2⁻²⁴, so the multiply only adjusts the exponent — it is exact, and because it is exact, widening to double and rounding back cannot change it (enumerated: 0 of 2²⁴ draws differ, with either the exact 2⁻²⁴ or the decimal literal). Keep it in float anyway: it is what the generator specifies, it is free, and the moment the multiplier or the shift changes, the exactness argument goes with it and double stops being equivalent. -
The float comparison in
bd_probe. The generator specifies(double)nextFloat() < p; we compare in float. That narrowing is licensed by exhaustion, not by argument:tools/fp_proof.cenumerates all 2²⁴ possible draws against all four reachable probabilities. Iffp_proofever fails, revertbd_probeto(double)f < prather than adjusting the proof.fp_prooflicenses the precision of that comparison and nothing else — it has no opinion on the operator. The strictness is pinned separately, by the twofloat compare boundaryvectors, and it is a live concern rather than a theoretical one:nextFloat()returns k·2⁻²⁴, and p=0.8 and p=0.6 are themselves exact multiples of 2⁻²⁴ (13421773 and 10066330), so a draw can land exactly on p. p=0.4 and p=0.2 cannot. Seed 12345 does it at (269, 4168) fory=-63and (1533, 851) fory=-62. Turning<into<=looks like fixing an off-by-one, changes real output, and passed every other gate in this repo before those vectors existed. -
The two derivers in
derive()(main.c). Steps 1 and 4 are different entry points — step 1 runs the world seed through splitmix64, step 4 uses the two-argument constructor and does not. Routing both through one helper looks like a tidy-up and silently diverges. -
The roof rule is negated, and its band runs the other way.
bd_classifystores the gradient's probability, not the odds of bedrock, andbd_probereturns!(f < p)for a roof block. Folding the negation into the probability —f < 1-p— keeps the density and moves the columns, which is the loudest kind of wrong: the match count looks right.b->roofpicks the deriver and the comparison direction together on purpose; splitting them into two flags invites setting one and not the other. -
The classic era is a replay, not a sample (
bd_classic_chunk). Read out of Mojang's 1.0, 1.7.10 and 1.12.2 jars, where it is identical:rand.setSeed(chunkX * 341873128712L + chunkZ * 132897987541L); for (int z = 0; z < 16; z++) for (int x = 0; x < 16; x++) { rand.nextDouble(); rand.nextDouble(); rand.nextDouble(); for (int y = 127; y >= 0; y--) if (y >= 127 - rand.nextInt(5) || y <= rand.nextInt(5)) bedrock(); }
Four things in there are load-bearing and none of them look it. The outer loop is z and the inner is x, which the flat block index
(x * 16 + z) * 128 + yis what settles.||short-circuits, so the second draw only happens when the first test failed — that is why a column costs a variable number of draws and cannot be indexed into.nextInt(5)rejection-samples, and a retry consumes another draw.nextDouble()is two draws, not one; their values are discarded here but their cost is not. Get any of them wrong and the whole chunk shifts. -
The Nether is not on xoroshiro (
bd_jrand_float,derive_legacy).nether.jsoncarrieslegacy_random_source: true, so both of its rules run onjava.util.Random. Two traps in there:nextLong()is((long)next(32) << 32) + next(32)with both halves signed, so both sign-extend and it is not one 64-bit draw; andfromHashOfusesString.hashCode— a wrapping 32-bith*31 + c— where the xoroshiro path uses MD5.
Compiler flags that must stay: -ffp-contract=off (host) and --fmad=false
(device) stop the compiler fusing start + delta * (end - start) into an FMA
and changing a rounding. Never add --use_fast_math — it flushes denormals
and swaps in approximate reciprocals.
Write the test before the fix where you can, and make sure it fails first. This project has been bitten repeatedly by tests that could not fail:
- Break the thing on purpose and confirm the right test fails. Not "a test
fails" — the one you wrote, and ideally only that one. A one-line mutation
(
rate * area→rate * n, delete a guard, transpose an index) takes a minute and is the only evidence a test is load-bearing. - Assert that the test reaches the code path it claims to. An estimate test
here passed for a while because its search area happened to equal the sample
budget, so the "sample" covered everything and the extrapolation it was
written to check never ran. It now asserts
not exactfirst. - Assert on what the user sees, not on internal state. A grid-painting bug
shipped because the test read the model — which was correct — instead of the
rendered DOM, which was not. Browser tests should check rendered output and
collect
window.onerror. - Statistics cannot catch structural bugs. A 6% sampling bias is invisible inside a ±12% confidence interval. Test structure directly instead.
If your mutation does not make the test fail, you have not found a compiler quirk — you have found out that your test does not test anything.
Generator (bedrock.h, and derive() in main.c) — the untouchable part.
Semantics are fixed by Minecraft; only the implementation is ours. Every change
needs make test. bedrock.h is shared between host and device via the BD_FN
macro, so it must compile as both C and CUDA.
CLI and output (the rest of main.c) — argument parsing, pattern/*.txt
loading, sorting, formatting. This is interface, not generator, and it is open
to redesign. make test will go red when you change it; that is expected, so
update the harness and record the divergence.
CUDA (search.cu) — the GPU path must produce the same match set as the
CPU path; ordering does not matter because sort_matches runs afterwards. Watch
integer widths: the flattened thread index is 64-bit and is narrowed once, on
purpose. make cuda && make test exercises the parity harness against the GPU
build.
Web GUI (web/) — python3 stdlib only, no packages, no build step, no CDN.
Two rules:
- No C code in the serving path. The server shells out to the same
./gpbpfthe repo builds, so the GUI can never affect the generator. Keep it that way. - Validate at the boundary. The binary must only ever see integers that have
been range-checked in
parse_search. Nevershell=True; always an argv list.
Measure. Do not reason about it, and do not trust your intuition about which instruction is slow — three plausible hypotheses about this kernel were wrong:
| hypothesis | actual |
|---|---|
the 64-bit integer multiplies in bd_hash dominate |
worth 6% |
| the div/mod unflattening the thread index is expensive | worth nothing; nvcc hoists it |
| the FP64 work is minor | 67% of kernel time |
Include before/after numbers and how you got them. nsys works without elevated
perf counters; ncu needs them (ERR_NVGPUCTRPERM otherwise). For the CPU path,
vary OMP_NUM_THREADS — a change that looks like a win at 12 threads can be a
loss at 1, and one lock-contention bug here made the search slower with more
threads.
Include a correctness gate in the same PR. Speed without make test passing is
not a result.
Match the surrounding code rather than your own preference. Tabs, K&R braces,
kernel-ish C. Comments explain why, especially where the code looks wrong on
purpose — most of the comments in bedrock.h exist because the obvious
simplification is incorrect. If you remove one of those, you probably introduced
the bug it was warning about.
- One logical change per PR.
- Say which gates you ran and paste the summary lines.
make testneeds no special setup, so there is no excuse for not saying whether it passed.- Performance claims need numbers and the command that produced them.
Bug reports are welcome without any of this — a seed, a pattern, and what you expected is plenty.
MIT, same as the rest of the project. By contributing you agree your work is licensed under it.