Skip to content

Latest commit

 

History

History
286 lines (232 loc) · 14.9 KB

File metadata and controls

286 lines (232 loc) · 14.9 KB

Contributing to GPBPF

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 one rule

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.

Setup

sudo dnf install gcc openssl-devel python3   # or your distro's equivalent
git clone git@github.com:codedsword/GPBPF.git
cd GPBPF && make && make test

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

The two gates

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

Inherited quirks

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 at y=123 thinning upward, plus a phantom always-bedrock layer at y=128 that is outside the Nether entirely; vanilla wraps that rule in not, so bedrock is solid at y=127 and thins downward to y=123. The negation cannot be folded into the probability either — !(f < d) and f < 1-d have 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 carry legacy_random_source: true. So every y >= 0 result this tool printed before that date was wrong, and the Nether floor at y 0…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}.json and noise_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".

Deliberate divergences

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

Generator internals that must not drift

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.

  1. bd_hash (bedrock.h). x * 3129871 is a 32-bit multiply that wraps and then sign-extends — it is not (long)x * 3129871L. Meanwhile (long)z * 116129781L really 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.

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

  3. 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.c enumerates all 2²⁴ possible draws against all four reachable probabilities. If fp_proof ever fails, revert bd_probe to (double)f < p rather than adjusting the proof.

    fp_proof licenses the precision of that comparison and nothing else — it has no opinion on the operator. The strictness is pinned separately, by the two float compare boundary vectors, 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) for y=-63 and (1533, 851) for y=-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.

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

  5. The roof rule is negated, and its band runs the other way. bd_classify stores the gradient's probability, not the odds of bedrock, and bd_probe returns !(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->roof picks the deriver and the comparison direction together on purpose; splitting them into two flags invites setting one and not the other.

  6. 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 + y is 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.

  7. The Nether is not on xoroshiro (bd_jrand_float, derive_legacy). nether.json carries legacy_random_source: true, so both of its rules run on java.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; and fromHashOf uses String.hashCode — a wrapping 32-bit h*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.

Testing

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 * arearate * 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 exact first.
  • 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.

Areas

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 ./gpbpf the 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. Never shell=True; always an argv list.

Performance changes

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.

Style

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.

Pull requests

  • One logical change per PR.
  • Say which gates you ran and paste the summary lines.
  • make test needs 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.

Licence

MIT, same as the rest of the project. By contributing you agree your work is licensed under it.