This commit is contained in:
Rainer Leit
2026-09-25 17:02:24 +03:00
parent cc43ed8dc8
commit 9597629951
2149 changed files with 460234 additions and 1770 deletions
+185
View File
@@ -0,0 +1,185 @@
package check
import (
"runtime"
"testing"
"salty/terrain/internal/fluvial"
"salty/terrain/internal/manifest"
"salty/terrain/internal/region"
"salty/terrain/internal/template"
"salty/terrain/internal/thermal"
"salty/terrain/internal/uplift"
"salty/terrain/internal/world"
)
const planetLegend = `{"classes":[
{"name":"sea","rgb":[0,0,255],"sea":true,"depth_m":400},
{"name":"plain","rgb":[150,200,100],"uplift_mm_yr":0.08,"k_mult":1.0},
{"name":"range","rgb":[60,160,100],"uplift_mm_yr":0.9,"k_mult":0.6}
]}`
// syntheticPlanet paints a small world with three landmasses, one of them across the seam, and returns it
// classified and projected. It is the smallest thing that exercises everything a real bake does: a cylinder,
// several regions, a seam, and two uplift classes.
func syntheticPlanet(t *testing.T, seed int64) (*manifest.Manifest, *template.Map, *region.Partition) {
t.Helper()
lg, err := template.Parse([]byte(planetLegend))
if err != nil {
t.Fatal(err)
}
const w, paintH, pad = 128, 64, 6
p := world.Planet{CellM: 40, W: w, H: paintH + 2*pad, PadY: pad, NoisePeriodM: w * 40}
if err := p.Validate(); err != nil {
t.Fatal(err)
}
sea := uint8(lg.Index("sea"))
plain := uint8(lg.Index("plain"))
rng := uint8(lg.Index("range"))
m := &template.Map{P: p, L: lg, Class: make([]uint8, p.W*p.H), Sea: make([]bool, p.W*p.H)}
for i := range m.Class {
m.Class[i], m.Sea[i] = sea, true
}
put := func(x0, y0, w0, h0 int, c uint8) {
for y := y0; y < y0+h0; y++ {
for x := x0; x < x0+w0; x++ {
i := (y+pad)*p.W + p.WrapX(x)
m.Class[i], m.Sea[i] = c, false
}
}
}
put(20, 10, 30, 24, plain) // a plain
put(30, 16, 12, 10, rng) // with a range in it
put(70, 30, 22, 20, rng) // a mountainous island
put(-4, 44, 10, 12, plain) // and one across the seam
part, err := region.Build(m, 4, 4)
if err != nil {
t.Fatal(err)
}
if len(part.Regions) < 3 {
t.Fatalf("got %d regions, want at least 3", len(part.Regions))
}
seam := false
for _, r := range part.Regions {
seam = seam || r.Seam
}
if !seam {
t.Fatal("no region straddles the seam; the test is not testing what it claims")
}
man := manifest.Defaults()
man.Source.Seed = seed
man.Planet = &manifest.Planet{UpliftVariation: 0.3}
return man, m, part
}
// solvePlanet runs the whole painted path: cut each region, build its painted geology, solve it, composite
// the land back. It is deliberately the same sequence internal/planet uses.
func solvePlanet(t *testing.T, seed int64, steps int) []float32 {
t.Helper()
man, m, part := syntheticPlanet(t, seed)
rates, ks := m.L.Rates(), m.L.Erodibilities()
out := make([]float32, m.P.W*m.P.H)
params := fluvial.Params{
K: 5e-5, M: 0.5, N: 1, DtYr: 1500, Steps: steps, Diffusion: 0.02, FillEvery: 1,
TalusSlope: thermal.TalusFromDegrees(35), ThermalEvery: 4, ThermalPasses: 2,
CriticalSlope: thermal.TalusFromDegrees(35), SlopeCap: 0.9, MaxHillslopeSub: 24,
}
for _, rg := range part.Regions {
class, land := part.Cut(m, rg)
up := uplift.FromTemplate(uplift.Paint{
Frame: rg.Frame, Class: class, Land: land,
Rates: rates, Ks: ks, Variation: man.Planet.UpliftVariation,
}, man)
h := up.Height.Clone()
g := fluvial.NewGrid(rg.Frame.W, rg.Frame.H, rg.Frame.P.CellM, up.Base)
g.SetSeed(man.Source.Seed)
g.SetFrame(rg.Frame)
g.SetElevationRange(-2000, 4000)
g.Run(h.Data, up.Rate.Data, up.K.Data, params, nil)
part.Composite(out, m, rg, h.Data)
}
return out
}
// The painted path's half of cross-cutting rule 12. The square canvas already has this assertion; a planet
// adds three ways to break it that the square canvas cannot reach - the classifier's parallel reduction, the
// region flood, and regions solved several at a time - so it gets its own.
func TestPaintedPlanetIsDeterministicAcrossGOMAXPROCS(t *testing.T) {
was := runtime.GOMAXPROCS(1)
defer runtime.GOMAXPROCS(was)
var want string
for _, procs := range []int{1, 2, 4, 8, 16} {
runtime.GOMAXPROCS(procs)
got := hash(solvePlanet(t, 7, 60))
if want == "" {
want = got
continue
}
if got != want {
t.Fatalf("GOMAXPROCS %d gives %s, GOMAXPROCS 1 gives %s", procs, got, want)
}
}
}
func TestSameSeedSamePlanet(t *testing.T) {
a := hash(solvePlanet(t, 11, 40))
b := hash(solvePlanet(t, 11, 40))
if a != b {
t.Fatalf("two runs of the same seed differ: %s and %s", a, b)
}
if c := hash(solvePlanet(t, 12, 40)); c == a {
t.Fatal("two different seeds give the same planet")
}
}
// The invariant the whole per-landmass decomposition rests on, asserted directly.
//
// Solving a landmass in a box of its own is only the same answer as solving the planet whole because ocean
// cells are held fixed at sea level and nothing in the solve can move them: ComputeReceivers makes every
// outlet its own receiver, so no flow path crosses water, and StreamPower, both diffusions, the repose clamp
// and thermal all skip a fixed cell. If that ever stopped being true, regions would start lying to each
// other and nothing else in the suite would say so.
func TestOceanCellsAreUntouchedByTheSolve(t *testing.T) {
man, m, part := syntheticPlanet(t, 7)
rates, ks := m.L.Rates(), m.L.Erodibilities()
params := fluvial.Params{
K: 5e-5, M: 0.5, N: 1, DtYr: 1500, Steps: 80, Diffusion: 0.02, FillEvery: 1,
TalusSlope: thermal.TalusFromDegrees(35), ThermalEvery: 4, ThermalPasses: 2,
CriticalSlope: thermal.TalusFromDegrees(35), SlopeCap: 0.9, MaxHillslopeSub: 24,
}
checked := 0
for _, rg := range part.Regions {
class, land := part.Cut(m, rg)
up := uplift.FromTemplate(uplift.Paint{
Frame: rg.Frame, Class: class, Land: land,
Rates: rates, Ks: ks, Variation: 0.3,
}, man)
h := up.Height.Clone()
g := fluvial.NewGrid(rg.Frame.W, rg.Frame.H, rg.Frame.P.CellM, up.Base)
g.SetSeed(man.Source.Seed)
g.SetFrame(rg.Frame)
g.SetElevationRange(-2000, 4000)
g.Run(h.Data, up.Rate.Data, up.K.Data, params, nil)
for i, isBase := range up.Base {
if !isBase {
continue
}
checked++
if h.Data[i] != float32(man.SeaLevelM) {
t.Fatalf("region %d: ocean cell %d came out at %g m, not sea level. The composite writes "+
"only land for exactly this reason, and it is now unsafe", rg.ID, i, h.Data[i])
}
}
}
if checked == 0 {
t.Fatal("no ocean cells were checked")
}
}