Farmland texturing: parcels, monoculture crop plots, and a five-way preset - #1264
Farmland texturing: parcels, monoculture crop plots, and a five-way preset#1264Teddy563 wants to merge 6 commits into
Conversation
|
retrigger-benchmark |
|
⏱️ Benchmark run finished in 0m 53s 📈 Compared against baseline: 18s time, 1160 MB memory 🟢 Generation time is unchanged. 📅 Last benchmark: 2026-08-18 18:22:05 UTC You can retrigger the benchmark by commenting |
landuse=farmland currently renders as one uniform sheet of crops. Real farmland is a patchwork of separate plots, each worked on its own and each growing a single thing, so from above a rural region reads as one flat colour instead of a field system. Split farmland into rectangular parcels with dirt-track boundaries and a fine internal sub-noise, and give every farm parcel exactly one crop (wheat, potato, carrot, beetroot, sunflower, pumpkin or fallow). Plots carry interior character: worn coarse-dirt spots, a mid-plot working path on large parcels, sunflower rows on dirt, a pumpkin patch on a grass and coarse mosaic. Parcel grids take their rotation from the dominant nearby road, since real fields are laid out off their access road, and fall back to one of six hashed angles where no road is close. Sunflower plots favour the low ground, sampled at the parcel centre so a plot never splits its crop. One new argument, --fields, with a matching segmented control in the settings panel: classic current behaviour, no field pass at all smallholding many small plots, full crop variety, dense tracks patchwork balanced mixed farmland (default) prairie large industrial fields, wheat-led, few tracks pasture grazing land, grass and wildflowers, a few crop plots Parcel sizes are defined in real-world metres and rescale with --scale, so a 1:10 world gets parcels twice as many blocks across as a 1:20 world and both cover the same amount of ground. Everything is a pure function of (x, z): no per-run state and no RNG threading, so the layout is identical across tiles and across separate runs of overlapping bounding boxes. The road-bearing and lowland grids are both anchored to a world lattice for the same reason. Verified against a pristine upstream build on a Swiss farmland bbox (47.400,8.500,47.420,8.530, 25600 chunks). Upstream is not fully deterministic run to run, so parity was measured only on the 20761 chunks upstream reproduces identically across two of its own runs: classic matches upstream on all of them, while patchwork changes 1759. 14 new block definitions (13 distinct blocks; sunflower needs an upper and a lower half) and 10 unit tests.
The rebase onto main picks up the Mineclonia coverage test added in "Harden the overhaul", which fails on the 14 new farmland blocks: without a mapping each one falls through to the mcl_core:stone fallback and a Luanti export renders whole fields as stone. Node names checked against Mineclonia main (mcl_lush_caves/nodes.lua, mcl_mud, mcl_farming, mcl_flowers/register.lua). Also give packed_mud and azalea explicit map-preview colours; the rest of the new palette already had entries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9835ef8 to
59b85c7
Compare
|
Rebased onto main and pushed to this branch. The only conflict was the 359-366 id renumbering. Holding the merge for now because it increases the peak RAM memory from 702 MB to 4.3 GB, and the generation time 2.3x. I measured on a farmland dense bbox (52.520,5.600,52.550,5.660, 61 farmland polygons, ARNIS_STREAM_TO_DISK=0 to pin the code path). Since I'll try to publish a new release very soon, let's try to fix this without time pressure :) |
Field texturing was holding several gigabytes and taking three times as long as an untextured render, which is why upstream is holding PR louis-e#1264. Every render in this commit is byte-identical to 3.1.0: the farmland world hash is 565924fa6b350a61 before and after, all five golden fixtures are unchanged, and the canonical Bucharest cell still hashes de18b3c6aef14311. Measured on the bbox upstream reported it from (52.520,5.600,52.550,5.660, Flevoland, 61 farmland polygons, ARNIS_STREAM_TO_DISK=0), best of three: peak 4929 MB -> 1333 MB, time 24.5 s -> 11.8 s. Attribution came from bisecting the flags, and it corrected two wrong guesses before any code was written. `--farm-crops` on its own costs nothing, which made it look like crops were innocent; they are not, the flag is simply inert until `--field-mix` activates the profile. And an early suspicion that the hay-bale scatter's whole-polygon HashSet was the hog was tested and refuted: disabling it saved 106 MB of 4929. The split that actually located it was `--field-mix farm=100` with `--farm-crops fallow=100`, which places no crop blocks: 4975 MB -> 1150 MB, naming crop placement as the cost. - src/element_processing/landuse.rs: intern the crop `age` compounds. A crop's NBT is always one of eight values, but place_crop built a fresh HashMap, two Strings and an Arc per block, and the editor then stored that per-block Arc in its section property map. Roughly 3.8 GB of the peak. Placement is now a refcount bump. An out-of-range age clamps to the ripest compound rather than panicking part-way through a render. - src/world_editor/common.rs: memoise the section palette lookup by property identity. It formatted each cell's compound to a String to key the lookup, so a crop field paid 4096 formats per section to rediscover the same eight entries. The content-keyed map stays authoritative, so two equal compounds from separate allocations still share one palette entry - tested explicitly, since keying on pointer alone would have split palettes in every world that writes NBT, not just farmland ones. - src/road_bearings.rs: memoise bearing_at per thread and per lattice cell. It is called per block but only ever about a domain centre, so ~36,000 consecutive blocks asked the same question, each taking an RwLock read contended across all 21 rendering threads plus an atan2, a sin and a cos. The memo is versioned and every write to the grid now goes through one store_grid helper that bumps the version, because the tests build several grids in one process and a memo outliving its grid would have gone unnoticed. Verified: cargo fmt --check clean (it was not run during wave 3 and CI caught that; it is part of the loop now); clippy --all-targets -- -D warnings clean; 402 tests pass, 8 new; golden harness 5/5; farmland and canonical hashes unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…block
A crop block's properties are always {age: "<0..=7>"}, so a whole field shares
one of eight possible compounds. place_crop built a fresh HashMap, two Strings
and an Arc for every block it placed, and the world editor then held that
per-block Arc in its section property map until save.
On a farmland-dense bbox that is millions of allocations whose contents are all
identical. Measured on 52.520,5.600,52.550,5.660 with ARNIS_STREAM_TO_DISK=0,
this accounted for roughly 3.8 GB of the peak on its own.
The eight compounds are now built once and shared, so placement is a refcount
bump. The NBT written is byte-identical: the farmland world hash is unchanged.
An age beyond the table clamps to the ripest compound rather than indexing
blindly, so a future crop with a wider age range degrades instead of panicking
part-way through a render.
bearing_at is called once per block by the parcel layout, but it is only ever asked about the centre of a 192x192 orientation domain. Every one of the ~36,000 blocks in a domain therefore asked the identical question, and each one took an RwLock read contended across every rendering thread, a hash lookup, and an atan2 plus a sin and a cos. The answer is now memoised per thread, keyed by the lattice cell it belongs to. Scanline order means the previous answer is nearly always the right one. The memo is versioned against the grid, and every write to the grid now goes through one store_grid helper that bumps the version, because the tests build several grids in one process and a memo that outlived its grid would have gone unnoticed there.
Building a section's palette formatted each cell's property compound to a String to key the lookup, so a section whose cells share one compound paid 4096 string allocations and Debug formats to rediscover the same handful of palette entries. With crop properties interned this is the remaining per-cell cost of saving a farmland-heavy world. An identity memo now sits in front of the content-keyed map. The content map stays authoritative, so two equal compounds arriving as separate allocations still collapse into one palette entry exactly as before. There is a test for that specifically: keying on pointer alone would have silently split palettes in every world that writes NBT, not only farmland ones. This one is separable from the other two commits on this branch - it is a general improvement to the world editor rather than part of the farmland feature, and can be dropped or split into its own PR if preferred.
The Farmland Style control already offers Classic (the pre-3.1 uniform sheet of crops) alongside the four parcel presets, but nothing told you that the parcel presets are more expensive, so the only way to find out was to run a large farmland world twice. Both the tooltip and the --fields help now give the figure: on farmland-heavy areas the parcel presets cost roughly 15% more generation time and about 1.5x the peak memory of Classic, and Classic is the one to pick for the cheapest run or the old look. Measured on 52.520,5.600,52.550,5.660 with ARNIS_STREAM_TO_DISK=0, best of three interleaved runs: classic 18.4 s / 757 MB, patchwork 21.3 s / 1151 MB. Also states that parcels are separated by dirt tracks, which the help described only indirectly.
|
Fixed. Reproduced your case first (same bbox, This branch, before and afterBest of three interleaved runs.
Byte identical output. Nothing about how farmland looks has changed. Feature cost against classic
|
Farmland texturing: parcels, monoculture crop plots, and a five-way preset
The problem
landuse=farmlandrenders as one uniform sheet of crops. Real farmland is a patchwork of separate plots, each worked on its own and each growing a single thing. From above, a rural area currently reads as one flat colour rather than as a field system, which stands out against how accurate the rest of the world is.What this does
Splits farmland into rectangular parcels with dirt-track boundaries and a fine internal sub-noise, and gives every farm parcel exactly one crop: wheat, potato, carrot, beetroot, sunflower, pumpkin or fallow. Plots carry interior character, including worn coarse-dirt spots, a mid-plot working path on large parcels, sunflower rows planted on dirt, and a pumpkin patch on a grass and coarse mosaic.
Parcel grids take their rotation from the dominant nearby road, since real fields are laid out off their access road, and fall back to one of six hashed angles where no road is close. Sunflower plots favour the low ground. That is sampled at the parcel centre rather than per block, so a plot lying across a lowland border never grows two crops.
Fallow plots deliberately use a bare palette of coarse dirt, rooted dirt and packed mud rather than farmland, because farmland with no crop above it reverts to dirt once the world is loaded in game. The same reasoning picks packed mud for the strips between sunflower rows: it stays bare instead of regrowing grass.
A preset, not a pile of flags
One new argument,
--fields, with a matching segmented control in the settings panel, following the same pattern as the existing Max Tree Size control:classicsmallholdingpatchworkprairiepastureEach preset sets three things at once: the share of each parcel style, the parcel size band, and the crop weights. I went with a single preset rather than exposing those as separate list-valued flags, since five named looks are easier to reason about than three orthogonal knobs, and it keeps the GUI to one row.
Parcel sizes are defined in real-world metres and rescale with
--scale, so a 1:10 world gets parcels roughly twice as many blocks across as a 1:20 world, and both cover the same amount of actual ground. There is a test pinning that.Seeing it
I do not have screenshots that would survive compression well enough to be useful, so here is a one-command repro on a bbox with real OSM farmland polygons:
Swapping
--fields=classicon the same bbox gives the current output for comparison.prairieandsmallholdingare the clearest contrast pair if you only want to look at two.Determinism and seams
Everything is a pure function of
(x, z). No per-run state, no RNG threading, no dependence on which tile resolves a given block. The layout is therefore identical across tile boundaries and across separate runs of overlapping bounding boxes.The two supporting lookup grids follow the same rule and each ships a test pinning it:
road_bearingsaggregates highway segment directions into a world-anchored 64-block lattice. The cell size is deliberately small: a domain queries the bearing at its own centre, which can sit up to 96 blocks outside the area a tile keeps, so reaching further than one cell would let two tiles with different clipped road sets resolve different rotations and visibly rotate the parcel grid at a seam.lowlandsamples terrain height on a world-anchored 128-block lattice and marks the low band.Both are
div_euclidon absolute coordinates rather than being indexed from a bbox origin, which is the part that matters.Verification
Checked against a pristine build of
mainon the Swiss farmland bbox above, 25600 chunks.mainis not fully deterministic run to run: rendering the same bbox twice with the same unmodified binary produced different block content in 4839 of those chunks. So rather than claim byte-for-byte parity, I measured only on the 20761 chunksmainreproduces identically across two of its own runs. On that stable set,classicmatchedmainexactly, whilepatchworkchanged 1759 chunks. Comparison was done on parsed section palettes and block-state arrays, since chunk NBT tag order also varies per run.Also:
cargo testpasses (431, of which 13 are new),cargo clippy --all-targets -- -D warningsis clean, andcargo fmt --checkis clean.On performance, I could not separate the presets from
mainabove measurement noise on my machine, where repeated identical runs varied between 17 and 40 seconds. So rather than quote a figure, here is the actual work added: one parcel resolution per farmland block, which is a few integer hashes plus three value-noise samples with no allocation, and one read-lock acquisition on each of the two lookup grids. It only runs on blocks inside alanduse=farmlandpolygon, and not at all underclassic. If you would like a proper measurement on quieter hardware, tell me which bbox you benchmark with and I will run it.Scope
src/element_processing/field_texture.rs, holding the parcel lattice, orientation domains, crop assignment and surface resolutionsrc/road_bearings.rsandsrc/lowland.rs, two small world-anchored lookup grids built once per runlanduse.rs, parcel-aware surface plus per-crop decoration with growth stagesRoughly +1546 and -1 lines. Nothing existing is removed, and no existing block id is reused.
Two things that are your call
The default. I have it shipping as
patchwork, on the grounds that a feature defaulted to off is a feature nobody sees. But it does change how every existing user's farmland looks. Changing the default toClassicis a one-word diff and I am glad to push that instead if you would rather the merge be visually inert.Follow-up scope. While testing I found that most rural farmland does not come from OSM at all. On a bbox in southern Romania with zero
landuse=farmlandways, the ground still rendered as farmland, all of it from the ESA cropland branch inground_generation.rs. Applying the same parcel system there is about 230 more lines and is what would make this visible in areas mappers have not drawn field polygons for. I kept it out so this PR stays reviewable, and can send it as a follow-up if this direction looks right to you.Happy to split, rename, or rework any of it. Thanks for the project.