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
+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 (0–100, 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}",