feat(simulation): relief_q — a local relief signal the deep rungs can resolve (T-1213)

The District and Quarter rungs rendered as flat colour, and the cause was not
the biome work everyone assumed. Measured on Ferrath through the production
canvas builder: at District the mean |elev_q delta| between neighbouring
gridunits is 0.02, and NOT ONE PAIR in a 1290x540 frame differs by 2.

elev_q spans 0-100 across the body's whole 8 km elevation range, so ONE STEP IS
80 METRES. A District canvas covers 2,048 m of ground, where the rolling relief
a walker navigates by is metres to tens of metres -- a fraction of a single
step. The sub-district detail IS generated (invent_primitives' scatter and
relief bands compute it) and then rounded away. Confirmed by running the
diagnostic with the octave cutoff disabled: still 0.02.

relief_q carries that same invented fine component against a scale chosen to
resolve it: 0-100 about a flat 50, RELIEF_FULL_SCALE_M = 400 m either side, so
8 m per step -- ten times finer than elev_q. elev_q keeps its body-absolute
meaning and the Atlas legend stays true.

Measured effect, elev_q vs relief_q (distinct values / mean 4-cell delta):

  Region     49 / 2.38   ->   101 / 21.86
  District   10 / 0.08   ->    35 / 0.35
  Quarter     8 / 0.02   ->    19 / 0.06

FIXED metre scale, never per-canvas normalization: the value for a piece of
ground must not depend on what else is in frame, or the same hillside changes
tone as the viewer pans. And it excludes elev_pct deliberately -- this is the
departure from the surrounding land, not height above sea level; including the
base would re-introduce the body-scale dominance that makes elev_q unusable
down here.

50 at the orbital rungs, which skip invent_primitives by design. Nothing is
lost: Global and Region still have varied elev_q (101 and 49 distinct values),
and the client takes whichever field carries signal via a max, with no
rung-name branching.

The client's ruggedness driver changes with it. It was an elev_q GRADIENT,
which cannot work across rungs -- the same 4-cell delta reads 21.86 at Region
and 0.08 at District, so any single full-scale constant either saturates one or
vanishes on the other. relief_q states relief outright, so |relief_q - 50| is
the answer directly and a fixed metre scale is immune to that by construction.

An absent plane reads FLAT, not zero -- 0 on this field means maximum relief
BELOW flat, so a payload without it would have stippled the entire map. That is
reachable: the field is #[serde(default)] so old-shape payloads decode. Two
colorize tests whose fixtures predate the plane caught it.

2004 server tests, 1838 client tests, 0 failed. clippy clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-07 16:11:23 +02:00
co-authored by Claude Opus 5
parent 566b566519
commit 5eb394b36f
11 changed files with 178 additions and 5 deletions
+1
View File
@@ -807,6 +807,7 @@ mod tests {
precipitation_class: PrecipitationClass::Temperate,
slope_q,
elev_q,
relief_q: 50,
ocean_fraction_q,
lake_margin_q: 0,
river_threshold: 200,
+2
View File
@@ -499,6 +499,7 @@ mod tests {
precipitation_class: PrecipitationClass::Temperate,
slope_q: 5,
elev_q: 20,
relief_q: 50,
ocean_fraction_q: 15,
lake_margin_q: 0,
river_threshold: 200,
@@ -637,6 +638,7 @@ mod tests {
precipitation_class: PrecipitationClass::Arid,
slope_q: 3,
elev_q: 10,
relief_q: 50,
ocean_fraction_q: 0, // no water at all
lake_margin_q: 0,
river_threshold: 300,
+67
View File
@@ -223,6 +223,30 @@ pub struct DistrictProfile {
/// Average of `TerrainAnalysis.elev_pct` × 100 across district cells.
pub elev_q: i32,
/// **Local** relief on a FIXED fine metre scale — 0100 with 50 = flat,
/// each step [`RELIEF_FULL_SCALE_M`] / 50 metres (T-1213, 2026-08-07).
///
/// WHY THIS IS NOT `elev_q`. `elev_q` spans the body's whole elevation
/// range: 0100 across [`MAX_REGION_ELEVATION_KM`] = 8 km, so **one step is
/// 80 metres**. That is the right granularity for a whole-body map and
/// useless below it — a District canvas covers 2,048 m of ground, where the
/// rolling relief a walker navigates by is metres to tens of metres, i.e.
/// a fraction of one `elev_q` step. Measured on Ferrath through the
/// production canvas builder: at District the mean |elev_q delta| between
/// neighbouring gridunits is **0.02**, and not one pair in a 1290×540 frame
/// differs by 2. The sub-district detail IS generated — `scatter` and
/// `relief` below compute it — and then rounded away.
///
/// So this field carries that same invented fine component against a scale
/// chosen to resolve it, rather than against the body. It is the signal the
/// deep rungs draw texture from; `elev_q` keeps its body-absolute meaning
/// and the Atlas legend stays true.
///
/// Signed about 50 rather than a magnitude, because a hollow and a rise are
/// different ground and a consumer that wants only roughness can take
/// `|relief_q - 50|` — the reverse is not recoverable.
pub relief_q: i32,
/// Ocean fraction for this district (0100 scale, integer).
pub ocean_fraction_q: i32,
@@ -722,6 +746,17 @@ pub fn derive_morphology_zone(
/// span can replace it when body relief data is carried (T-1032).
const MAX_REGION_ELEVATION_KM: f64 = 8.0;
/// Full-scale swing of [`DistrictProfile::relief_q`], in metres either side of
/// flat. 400 m spans everything from a gentle roll to a substantial ridge at
/// the sub-district scale, and puts one quantization step at **8 m** — ten
/// times finer than `elev_q`'s 80 m, which is what makes the deep rungs
/// resolvable at all.
///
/// A FIXED metre scale on purpose, never per-canvas normalization: the value
/// for a piece of ground must not depend on what else is in frame, or the same
/// hillside would change tone as the viewer pans and the map would shimmer.
const RELIEF_FULL_SCALE_M: f64 = 400.0;
/// Climate tuning constants. `ClimateConstants::default()` holds the embedded
/// values and is the authoritative runtime source **today**; the source-canonical
/// `server/data/climate_constants.toml` mirrors those same values and is the file
@@ -1145,6 +1180,9 @@ const WINDOW_RELIEF_SALT: u64 = 0x5EED_C0DE;
struct InventedPrimitives {
slope_q: i32,
elev_q: i32,
/// Local sub-district relief on the fixed [`RELIEF_FULL_SCALE_M`] scale —
/// see [`DistrictProfile::relief_q`].
relief_q: i32,
ocean_fraction_q: i32,
}
@@ -1360,8 +1398,22 @@ fn invent_primitives(
// 0.0 at every cutoff ≥ 2,048 m (District and coarser), so this sum is
// byte-identical to the pre-T-1162 `elev_pct + scatter` wherever the
// cutoff discipline says it must be.
// The SAME invented fine component the elevation sum above folds in, kept
// on its own fixed metre scale instead of the body's (T-1213). `scatter`
// and `relief` are normalized fractions of MAX_REGION_ELEVATION_KM, so
// scaling by it gives metres; RELIEF_FULL_SCALE_M then maps +/-400 m onto
// 0..100 about a flat 50, i.e. 8 m per step against elev_q's 80 m.
//
// Deliberately excludes `elev_pct`: this is LOCAL relief, the departure
// from the surrounding land, not height above sea level. Including the
// base would re-introduce exactly the body-scale dominance that makes
// elev_q unusable down here.
let relief_m = (scatter + relief) * MAX_REGION_ELEVATION_KM * 1_000.0;
let relief_q = ((50.0 + (relief_m / RELIEF_FULL_SCALE_M) * 50.0).round() as i32).clamp(0, 100);
InventedPrimitives {
elev_q: (((elev_pct + scatter + relief) * 100.0).round() as i32).clamp(0, 100),
relief_q,
slope_q: (((local_slope + ruggedness * (scatter.abs() + relief.abs()) + carve) * 100.0)
.round() as i32)
.clamp(0, 100),
@@ -1636,6 +1688,11 @@ fn build_district_profile(
climate: &ClimateConstants,
slope_q: i32,
elev_q: i32,
// Local sub-district relief, fixed metre scale — see
// `DistrictProfile::relief_q`. Inert to every other field's derivation (a
// presentation-tier signal, not a classification input), so it is carried
// straight through to the output.
relief_q: i32,
ocean_fraction_q: i32,
region_baseline_c: Option<f32>,
basin_direction: BasinDirection,
@@ -1733,6 +1790,7 @@ fn build_district_profile(
precipitation_class,
slope_q,
elev_q,
relief_q,
ocean_fraction_q,
lake_margin_q,
river_threshold,
@@ -1986,6 +2044,7 @@ fn derive_at_metres_with_riparian(
climate,
prims.slope_q,
prims.elev_q,
prims.relief_q,
prims.ocean_fraction_q,
region_baseline_c,
BasinDirection::default(),
@@ -2182,6 +2241,14 @@ pub fn derive_orbital_at_metres(
climate,
slope_q,
elev_q,
// 50 = flat. The orbital rung skips `invent_primitives` altogether
// (that is the whole point of this path — envelope-only, no scatter,
// no warp), so no sub-district relief is computed here and reporting a
// neutral midpoint is the honest answer rather than a fabricated one.
// Nothing is lost visually: at Global and Region `elev_q` still varies
// properly — 101 and 49 distinct values respectively on Ferrath — and
// it is only below them that it flattens and `relief_q` takes over.
50,
ocean_fraction_q,
region_baseline_c,
BasinDirection::default(),
+1
View File
@@ -2381,6 +2381,7 @@ mod tests {
precipitation_class: PrecipitationClass::Arid,
slope_q: 0,
elev_q: elev,
relief_q: 50,
ocean_fraction_q: 0,
lake_margin_q: 0,
river_threshold: 200,
+51 -3
View File
@@ -381,6 +381,24 @@ pub struct EncodedStepCanvas {
/// before the version tag has already forced a fresh fetch.
#[serde(default)]
pub lake_margin_q: EncodedField,
/// T-1213 — LOCAL sub-district relief on a fixed metre scale (0100, 50 =
/// flat, 8 m per step). The signal the deep rungs draw relief texture from,
/// because `elev_q` structurally cannot: `elev_q` spans the body's whole
/// 8 km elevation range at 80 m per step, and a District canvas covers
/// 2,048 m of ground where real relief is a fraction of one such step —
/// measured at a mean neighbour delta of 0.02 there, with no pair in a
/// 1290×540 frame differing by 2. Same static-geometry plane as `elev_q`
/// (a pure function of position, not sim-state).
///
/// `50` at the orbital rungs, where `invent_primitives` is skipped by
/// design and no sub-district detail is computed — Global and Region get
/// their texture from `elev_q`, which is still varied at that scale.
///
/// `#[serde(default)]` for the same reason `lake_margin_q` carries it: an
/// old-shape payload decodes rather than hard-failing, and the version-
/// tagged cache forces a fresh fetch before anything reads it.
#[serde(default)]
pub relief_q: EncodedField,
// --- Sim-state plane (2 dense fields, see struct doc) ---
pub glaciation: EncodedField,
pub flooded_q: EncodedField,
@@ -469,6 +487,8 @@ struct StepCanvasCell {
glaciation: u8,
flooded_q: u8,
lake_margin_q: u8,
/// T-1213 local relief, fixed metre scale — see `EncodedStepCanvas::relief_q`.
relief_q: u8,
}
/// Derive one cell at `(wx, wy)` world metres, dispatching to the D-255(a)/(f)
@@ -523,6 +543,7 @@ fn derive_step_canvas_cell(
// every non-lake cell, same static-geometry-plane posture as
// `elev_q`/`morphology` (a pure function of position, not sim-state).
lake_margin_q: prof.lake_margin_q.clamp(0, 100) as u8,
relief_q: prof.relief_q.clamp(0, 100) as u8,
}
}
@@ -551,6 +572,8 @@ pub struct RawStepCanvas {
/// tone source ocean coastlines already get for free from
/// `ocean_fraction_q`. See `district_profile::DistrictProfile::lake_margin_q`.
pub lake_margin_q: Vec<u8>,
/// T-1213 — local sub-district relief, the deep rungs' texture source.
pub relief_q: Vec<u8>,
pub courses: Vec<RiverCourse>,
pub cliffs: Vec<CliffSegment>,
}
@@ -1078,6 +1101,7 @@ pub fn build_step_canvas(
let mut glaciation = vec![0u8; cells];
let mut flooded_q = vec![0u8; cells];
let mut lake_margin_q = vec![0u8; cells];
let mut relief_q = vec![0u8; cells];
for (row, row_cells) in rows.into_iter().enumerate() {
let base = row * width as usize;
@@ -1091,6 +1115,7 @@ pub fn build_step_canvas(
glaciation[i] = cell.glaciation;
flooded_q[i] = cell.flooded_q;
lake_margin_q[i] = cell.lake_margin_q;
relief_q[i] = cell.relief_q;
}
}
@@ -1141,6 +1166,7 @@ pub fn build_step_canvas(
glaciation,
flooded_q,
lake_margin_q,
relief_q,
courses,
cliffs,
}
@@ -1190,6 +1216,7 @@ pub fn encode_step_canvas(raw: &RawStepCanvas) -> EncodedStepCanvas {
values: raw.settlement_id.clone(),
},
lake_margin_q: png_encode_u8_plane(raw.width, raw.height, &raw.lake_margin_q),
relief_q: png_encode_u8_plane(raw.width, raw.height, &raw.relief_q),
glaciation: png_encode_u8_plane(raw.width, raw.height, &raw.glaciation),
flooded_q: png_encode_u8_plane(raw.width, raw.height, &raw.flooded_q),
courses: raw.courses.clone(),
@@ -1213,6 +1240,7 @@ pub fn decode_step_canvas(enc: &EncodedStepCanvas) -> RawStepCanvas {
vegetation: png_decode_u8_plane(&enc.vegetation),
settlement_id: enc.settlement_id.values.clone(),
lake_margin_q: png_decode_u8_plane(&enc.lake_margin_q),
relief_q: png_decode_u8_plane(&enc.relief_q),
glaciation: png_decode_u8_plane(&enc.glaciation),
flooded_q: png_decode_u8_plane(&enc.flooded_q),
courses: enc.courses.clone(),
@@ -2149,6 +2177,7 @@ mod tests {
vegetation: png_encode_u8_plane(1, 1, &[0]),
settlement_id: EncodedSettlementField { values: vec![0] },
lake_margin_q: png_encode_u8_plane(1, 1, &[0]),
relief_q: png_encode_u8_plane(1, 1, &[50]),
glaciation: png_encode_u8_plane(1, 1, &[0]),
flooded_q: png_encode_u8_plane(1, 1, &[0]),
courses: Vec::new(),
@@ -2574,9 +2603,7 @@ mod tests {
let spacing =
rung.spacing_m(raw.width, raw.height, params.body_radius_km.unwrap_or(0.0));
let uniq_u8 = |v: &Vec<u8>| {
v.iter().collect::<std::collections::BTreeSet<_>>().len()
};
let uniq_u8 = |v: &Vec<u8>| v.iter().collect::<std::collections::BTreeSet<_>>().len();
let span_u8 = |v: &Vec<u8>| match (v.iter().min(), v.iter().max()) {
(Some(a), Some(b)) => *b as i32 - *a as i32,
_ => 0,
@@ -2614,6 +2641,27 @@ mod tests {
);
}
// The same 4-cell gradient over relief_q — the T-1213 field that
// exists precisely because elev_q's 80 m step cannot resolve
// sub-district ground. If this does not move where elev_q's does
// not, the field has bought nothing.
let mut rsum = 0.0f64;
let mut rn = 0.0f64;
for r in 0..h {
for c in 0..w.saturating_sub(4) {
let a = raw.relief_q[r * w + c] as i32;
let b = raw.relief_q[r * w + c + 4] as i32;
rsum += (a - b).abs() as f64;
rn += 1.0;
}
}
let ruq = raw
.relief_q
.iter()
.collect::<std::collections::BTreeSet<_>>()
.len();
grad += &format!(" RELIEF[uniq {ruq:3} d4mean {:.2}]", rsum / rn.max(1.0));
println!(
"{:8?} {:>9.1} m/gu {}x{} elev[uniq {:3} span {:3}] moist[uniq {:3} span {:3}] \
morph[uniq {:2}] veg[uniq {:2}] glac[uniq {:2}] courses {}{grad}",
+16 -1
View File
@@ -2290,6 +2290,7 @@ mod tests {
precipitation_class: PrecipitationClass::Temperate,
slope_q: 5,
elev_q: 20,
relief_q: 50,
ocean_fraction_q: 15,
lake_margin_q: 0,
river_threshold: 200,
@@ -2312,6 +2313,7 @@ mod tests {
precipitation_class: PrecipitationClass::Temperate,
slope_q,
elev_q: 5,
relief_q: 50,
ocean_fraction_q: 90,
lake_margin_q: 0,
river_threshold: 200,
@@ -2858,6 +2860,7 @@ mod tests {
precipitation_class: crate::atlas::district_profile::PrecipitationClass::Temperate,
slope_q: 5,
elev_q: 20, // low elevation
relief_q: 50, // flat — this test measures the elevation blend, not relief
ocean_fraction_q: 0, // no channel — simpler tile layout for elevation check
lake_margin_q: 0,
river_threshold: 200,
@@ -2872,7 +2875,8 @@ mod tests {
glaciation_grade: crate::atlas::district_profile::GlaciationGrade::None,
precipitation_class: crate::atlas::district_profile::PrecipitationClass::Temperate,
slope_q: 5,
elev_q: 70, // high elevation — makes the blend measurable
elev_q: 70, // high elevation — makes the blend measurable
relief_q: 50, // flat — see the primary district above
ocean_fraction_q: 0,
lake_margin_q: 0,
river_threshold: 200,
@@ -2976,6 +2980,7 @@ mod tests {
ocean_fraction_q: 0,
slope_q: 8,
elev_q: 30,
relief_q: 50,
moisture_q: 20,
..alluvial_district()
}
@@ -3057,6 +3062,7 @@ mod tests {
glaciation_grade: GlaciationGrade::Moderate, // grade ≥ 2
slope_q: 60,
elev_q: 80,
relief_q: 50,
ocean_fraction_q: 30,
moisture_q: 70,
vegetation_class: VegetationClass::Barren,
@@ -3174,6 +3180,7 @@ mod tests {
tectonic_class: TectonicClass::Active,
slope_q: 70,
elev_q: 60,
relief_q: 50,
ocean_fraction_q: 40,
moisture_q: 50,
vegetation_class: VegetationClass::Barren,
@@ -3259,6 +3266,7 @@ mod tests {
morphology_zone: MorphologyZone::Delta,
slope_q: 2,
elev_q: 4,
relief_q: 50,
ocean_fraction_q: 50,
moisture_q: 80,
vegetation_class: VegetationClass::Scrub,
@@ -3376,6 +3384,7 @@ mod tests {
morphology_zone: MorphologyZone::DuneStrand,
slope_q: 8,
elev_q: 6,
relief_q: 50,
ocean_fraction_q: 30,
moisture_q: 40,
vegetation_class: VegetationClass::Barren,
@@ -3470,6 +3479,7 @@ mod tests {
tectonic_class: TectonicClass::Active,
slope_q: 80,
elev_q: 90,
relief_q: 50,
ocean_fraction_q: 5,
moisture_q: 50,
vegetation_class: VegetationClass::Barren,
@@ -3579,6 +3589,7 @@ mod tests {
morphology_zone: MorphologyZone::MeanderReach,
slope_q: 3,
elev_q: 15,
relief_q: 50,
ocean_fraction_q: 20,
moisture_q: 70,
vegetation_class: VegetationClass::Forest,
@@ -3623,6 +3634,7 @@ mod tests {
morphology_zone: MorphologyZone::MeanderReach,
slope_q: 5, // above wetland slope threshold
elev_q: 15,
relief_q: 50,
ocean_fraction_q: 20,
moisture_q: 55, // below wetland moisture threshold
vegetation_class: VegetationClass::Forest,
@@ -3715,6 +3727,7 @@ mod tests {
let shared_district_base = DistrictProfile {
slope_q: 5,
elev_q: 20,
relief_q: 50,
ocean_fraction_q: 20, // ensure has_active_channel = true
moisture_q: 55,
vegetation_class: VegetationClass::Forest,
@@ -3843,6 +3856,7 @@ mod tests {
glaciation_grade: GlaciationGrade::Moderate,
slope_q: 50,
elev_q: 40,
relief_q: 50,
ocean_fraction_q: 20,
moisture_q: 50,
..alluvial_district()
@@ -3878,6 +3892,7 @@ mod tests {
glaciation_grade: GlaciationGrade::Moderate,
slope_q: 50,
elev_q: 40,
relief_q: 50,
ocean_fraction_q: 5,
moisture_q: 30,
vegetation_class: VegetationClass::Barren,