A chunk with Params.Region is generated on a board covering only its region and the context tiles around it and translated back to field coordinates, so owners, rules geometry and cloned boards are region- sized instead of G x G. Measured on a ring-1 chunk of the G=1000 daily (25 context tiles): 618 MB and 13.7 s -> 246 MB and 6.0 s (/usr/bin/time -l, max RSS). The field runner prints per chunk: region, context tiles and cells, wall, max RSS, tiles, fill/givens/settle ms, SAT calls, waves, attempts, regivens, redraws, fill retries; and per batch (ring): chunks, jobs, wall. The assertion checker accepts a seam clue two owners share under a third tile that arrived later as fixed context (chunked fields and growth do this by construction).
- commit
- 835132ad7a94b6ffa42664b580d07ae5dd775fd0
- parent
- 6133b0f54f8dafa9c80e2d669715951b65a22eb2
- author
- dami <gazey55@gmail.com>
| M | engine/ | 25 | ++++++++++- |
| M | engine/ | 69 | ++++++++++++++++++++++++++++++ |
| M | engine/ | 27 | +++++++++++- |
@@ -161,6 +161,9 @@ func SubprocessRunner(exe string, timeout time.Duration) Runner {
161161 cmd.Stdout = &out162162 cmd.Stderr = &errb163163 cmd.Env = os.Environ()164+ if Verbose && os.Getenv("MOSAIC_CHUNK_DUMP") != "" {165+ _ = os.WriteFile(fmt.Sprintf("%s/mosaic-chunk-%d.json", os.TempDir(), p.Chunk), in, 0o644)166+ }164167 t0 := time.Now()165168 runErr := cmd.Run()166169 if Verbose {@@ -181,10 +184,20 @@ func SubprocessRunner(exe string, timeout time.Duration) Runner {
181184 }182185 }183186 }184- fmt.Fprintf(os.Stderr, "field: chunk %d region %v fixed %d: %s, max rss %d MB\n", p.Chunk, p.Region, len(p.Fixed), time.Since(t0).Round(time.Millisecond), rss)185- if os.Getenv("MOSAIC_CHUNK_DUMP") != "" {186- _ = os.WriteFile(fmt.Sprintf("%s/mosaic-chunk-%d.json", os.TempDir(), p.Chunk), in, 0o644)187+ fixedCells := 0188+ for _, ft := range p.Fixed {189+ fixedCells += ft.Tile.Cells190+ }191+ line := fmt.Sprintf("field: chunk %d region %v fixed %d tiles %d cells: wall %s, max rss %d MB", p.Chunk, p.Region, len(p.Fixed), fixedCells, time.Since(t0).Round(time.Millisecond), rss)192+ if runErr == nil {193+ var o Output194+ if json.Unmarshal(out.Bytes(), &o) == nil {195+ st := o.Stats196+ line += fmt.Sprintf(", tiles %d, fill %d ms, givens %d ms, settle %d ms, sat %d calls, waves %d, attempts %d, regivens %d, redraws %d, fill_retries %d",197+ len(o.Manifest.Tiles), st.FillMs, st.GivensMs, st.SettleMs, st.SatCalls, st.Waves, st.Attempts, st.Regivens, st.Redraws, st.FillRetries)198+ }187199 }200+ fmt.Fprintln(os.Stderr, line)188201 }189202 if err := runErr; err != nil {190203 msg := strings.TrimSpace(errb.String())@@ -420,6 +433,12 @@ func ApplyGrowth(state, delta *Output) (*Output, error) {
420433 func generateChunks(ctx context.Context, p FieldParams, plan FieldPlan, chunks []latticeChunk, baseTiles []TilePublic, baseSol []string, salt string, idBase int, run Runner) ([]*Output, error) {421434 outs := make([]*Output, len(chunks))422435 var mu sync.Mutex436+ tBatch := time.Now()437+ defer func() {438+ if Verbose {439+ fmt.Fprintf(os.Stderr, "field: batch %s: %d chunks, jobs %d, wall %s\n", salt, len(chunks), p.Jobs, time.Since(tBatch).Round(time.Millisecond))440+ }441+ }()423442 for colour := 0; colour < 4; colour++ {424443 var idx []int425444 for i, c := range chunks {@@ -8,6 +8,7 @@
88 package gen99 1010 import (11+ "bytes"1112 "context"1213 "crypto/sha256"1314 "encoding/binary"@@ -283,7 +284,75 @@ func rngFor(parts ...string) *rand.Rand {
283284 // Generate runs the full pipeline. The layout is drawn once from the seed;284285 // attempts redraw the tile contents with a derived rng when an attempt285286 // cannot satisfy every assertion.287+// Generate runs the full pipeline. A chunk (Params.Region set) is288+// generated on a local board that covers just its region and the context289+// tiles around it, then translated back to field coordinates: the290+// per-cell structures (owners, rules geometry, boards cloned by291+// propagation) are region-sized instead of G x G, which at G=1000 was292+// hundreds of megabytes per chunk process for a 60x60 region.286293 func Generate(ctx context.Context, p Params) (*Output, error) {294+ if p.Region == [4]int{} || p.G == 0 {295+ return generate(ctx, p)296+ }297+ x0, y0 := p.Region[0], p.Region[1]298+ x1, y1 := p.Region[0]+p.Region[2], p.Region[1]+p.Region[3]299+ for _, f := range p.Fixed {300+ x0, y0 = min(x0, f.Tile.X0), min(y0, f.Tile.Y0)301+ x1, y1 = max(x1, f.Tile.X0+f.Tile.W), max(y1, f.Tile.Y0+f.Tile.H)302+ }303+ S := max(x1-x0, y1-y0)304+ lp := p305+ lp.G = S306+ lp.Region = [4]int{p.Region[0] - x0, p.Region[1] - y0, p.Region[2], p.Region[3]}307+ lp.Fixed = make([]FixedTile, len(p.Fixed))308+ for i, f := range p.Fixed {309+ t := f.Tile310+ t.X0, t.Y0 = t.X0-x0, t.Y0-y0311+ lp.Fixed[i] = FixedTile{Tile: t, Cells: f.Cells}312+ }313+ out, err := generate(ctx, lp)314+ if err != nil {315+ return nil, err316+ }317+ G := p.G318+ toGlobal := func(id int) int { return (id/S+y0)*G + id%S + x0 }319+ out.Manifest.G = G320+ for i := range out.Manifest.Tiles {321+ out.Manifest.Tiles[i].X0 += x0322+ out.Manifest.Tiles[i].Y0 += y0323+ }324+ for i := range out.Tiles {325+ for k := range out.Tiles[i].Givens {326+ out.Tiles[i].Givens[k][0] = toGlobal(out.Tiles[i].Givens[k][0])327+ }328+ for k := range out.Tiles[i].Flip {329+ out.Tiles[i].Flip[k] = toGlobal(out.Tiles[i].Flip[k])330+ }331+ }332+ sol := make([][]byte, G)333+ for y := range sol {334+ sol[y] = bytes.Repeat([]byte{'.'}, G)335+ }336+ for ly, row := range out.Solution {337+ gy := ly + y0338+ if gy < 0 || gy >= G {339+ continue340+ }341+ for lx := 0; lx < len(row); lx++ {342+ gx := lx + x0343+ if row[lx] != '.' && gx >= 0 && gx < G {344+ sol[gy][gx] = row[lx]345+ }346+ }347+ }348+ out.Solution = out.Solution[:0]349+ for y := range sol {350+ out.Solution = append(out.Solution, string(sol[y]))351+ }352+ return out, nil353+}354+355+func generate(ctx context.Context, p Params) (*Output, error) {287356 p, err := defaults(p)288357 if err != nil {289358 return nil, err@@ -38,6 +38,15 @@ func TestGenDeterministicPerSeed(t *testing.T) {
3838 3939 // checkAssertions re-verifies every per-tile and whole-mosaic assertion from4040 // the emitted JSON alone, independently of the generator's own bookkeeping.41+func priv0(tiles []TilePrivate, id int) TilePrivate {42+ for _, p := range tiles {43+ if p.ID == id {44+ return p45+ }46+ }47+ return TilePrivate{}48+}49+4150 func checkAssertions(t *testing.T, o *Output) {4251 t.Helper()4352 man := o.Manifest@@ -149,7 +158,23 @@ func checkAssertions(t *testing.T, o *Output) {
149158 t.Fatalf("tile %d: given %d outside the shape", i, g[0])150159 }151160 if len(owners[g[0]]) >= 3 {152- t.Fatalf("tile %d: given on cell %d with %d owners", i, g[0], len(owners[g[0]]))161+ // a junction is never planted or clued by its own tile alone;162+ // what is allowed is a seam clue two owners share, which a163+ // third tile then covers as fixed context (chunked fields164+ // and growth do this: the later chunk's assertions were165+ // computed with the cell fixed)166+ givers := 0167+ for _, own := range owners[g[0]] {168+ for _, og := range priv0(o.Tiles, own).Givens {169+ if og[0] == g[0] {170+ givers++171+ break172+ }173+ }174+ }175+ if givers < 2 {176+ t.Fatalf("tile %d: given on cell %d with %d owners but only %d giver", i, g[0], len(owners[g[0]]), givers)177+ }153178 }154179 if len(owners[g[0]]) >= 2 {155180 if !man.Rules.SeamClues {