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
+45 -4
View File
@@ -64,6 +64,11 @@ type Params struct {
CriticalSlope float64
SlopeCap float64 // where the flux stops stiffening, as a fraction of Sc
MaxHillslopeSub int // the sub-step budget that bound buys
// MFDExponent selects multiple-flow-direction drainage area over D8's single receiver, and is the
// exponent on the partition. 0 keeps the old Accumulate, which is what every bake before this ran and
// what the A/B comparison needs. See mfd.go for why one is the right default.
MFDExponent float64
}
// Grid holds the flow topology and the scratch it is built from. Allocated once and reused across every
@@ -85,7 +90,11 @@ type Grid struct {
Stack []int32 // every node after its receiver
Area []float32 // drainage area, m²
seed uint64 // the jitter's seed; see jitter.go and SetSeed
cancel <-chan struct{} // closed to abandon a run mid-solve; see SetCancel
seed uint64 // the jitter's seed; see jitter.go and SetSeed
originX int32 // where this grid sits on the planet; see SetFrame. Zero is "this grid is the world"
originY int32
planetW int32 // the cylinder's width, or 0 when there is no cylinder
donorOff []int32
donorList []int32
cursor []int32
@@ -93,6 +102,13 @@ type Grid struct {
pq *bucketPQ
fifo []int32
scratch []float32
// Multiple-flow accumulation. mfdPending is how many strictly higher neighbours a cell still owes before
// it may be released; a byte, because a cell has eight neighbours and cannot owe more. See mfd.go.
mfdPending []uint8
mfdQueue []int32
mfdMode mfdPow
mfdExp float64
}
// SetElevationRange sizes the flood's bucket queue. Called once, with the manifest's elevation range plus a
@@ -116,6 +132,7 @@ func NewGrid(w, h int, cellM float64, base []bool) *Grid {
Receiver: make([]int32, n), Length: make([]float32, n), Stack: make([]int32, 0, n),
Area: make([]float32, n), donorOff: make([]int32, n+1), donorList: make([]int32, n),
closed: make([]bool, n), fifo: make([]int32, 0, n), scratch: make([]float32, n),
mfdPending: make([]uint8, n),
}
g.fixed = make([]bool, n)
for i := range g.fixed {
@@ -178,7 +195,7 @@ func (g *Grid) FillDepressions(h []float32, epsilon float32) {
}
g.closed[ni] = true
if h[ni] <= celev {
h[ni] = celev + epsilon*(0.5+hash01(g.seed, ni))
h[ni] = celev + epsilon*(0.5+hashXY(g.seed, g.worldX(nx), g.worldY(ny), jitterFloodEpsilon))
g.fifo = append(g.fifo, ni)
} else {
g.pq.push(h[ni], ni)
@@ -239,7 +256,7 @@ func (g *Grid) ComputeReceivers(h []float32) {
}
// The tie-break, not a change of gradient: the comparison is jittered, the slope that
// is kept is not, so Length and the stream-power update see the true geometry.
sj := s * (1 + 1e-3*(hash01(g.seed, i*8+int32(k))-0.5))
sj := s * (1 + 1e-3*(hashXY(g.seed, g.worldX(x), g.worldY(y), int32(k)+jitterReceiverTie)-0.5))
if sj > bestJitter {
bestJitter, best, bestLen = sj, ni, l
}
@@ -298,6 +315,11 @@ func (g *Grid) BuildStack() {
}
}
// Scratch hands out the grid's spare float32 buffer, which is the width of the grid and is dead between
// steps. It is here so a pass that runs once after the solve - the edge-preserving smooth - does not allocate
// a second copy of the height field at planet scale just to have somewhere to write.
func (g *Grid) Scratch() []float32 { return g.scratch }
// scratchInt32 reuses the float32 scratch as int32 storage; same width, and it saves a 12 MB allocation per
// step at the geology grid.
func (g *Grid) scratchInt32() []int32 {
@@ -458,6 +480,14 @@ func clampAt(a []float32, w, h, x, y int) float32 {
return a[y*w+x]
}
// SetCancel gives the solve a way to be abandoned part way through.
//
// It is checked once a step rather than inside one, which is the right granularity: a step is milliseconds on
// a small region and a couple of seconds on a big one, so the longest a caller waits is one step, and nothing
// inside a step is safe to leave half done. The height field is left wherever the solve had got to, which is
// what a cancelled run means - it is not a checkpoint and nothing downstream should read it as one.
func (g *Grid) SetCancel(ch <-chan struct{}) { g.cancel = ch }
// Run is the whole solve. Progress is reported through log, which is what a five-minute budget needs to be
// steerable: a run that is going wrong should say so at step 500, not at the end.
func (g *Grid) Run(h []float32, uplift, k []float32, p Params, log func(step int, total int, elapsedPct float64)) {
@@ -466,12 +496,23 @@ func (g *Grid) Run(h []float32, uplift, k []float32, p Params, log func(step int
fill = 1
}
for step := 0; step < p.Steps; step++ {
if g.cancel != nil {
select {
case <-g.cancel:
return
default:
}
}
if step%fill == 0 {
g.FillDepressions(h, 1e-3)
}
g.ComputeReceivers(h)
g.BuildStack()
g.Accumulate()
if p.MFDExponent > 0 {
g.AccumulateMFD(h, p.MFDExponent)
} else {
g.Accumulate()
}
g.StreamPower(h, uplift, k, p)
if p.CriticalSlope > 0 {
// The clamp still runs, and it still has to: a belt rising at millimetres a year asks for slopes