acdream/src/AcDream.Bake/Program.cs
Erik 86e0dc4655 fix(pipeline): MP1b review - bake CLI determinism + bounded memory
Adversarially-verified review findings 1 and 10:

(1) The bake pipeline no longer accumulates every decoded ObjectMeshData
in one ConcurrentBag before writing (multi-GB OOM risk on the full
bake), and no longer writes blobs in thread-completion order (which
violated the plan's "bakes must be byte-reproducible run-to-run").
New shape in BakeRunner: build the FULL id list, sort by PakKey, chunk
into 512-id batches; Parallel.ForEach WITHIN each batch; sort each
batch's results by key and AddBlob sequentially; release the batch.
Batches are contiguous key ranges, so the blob region lands in global
key order regardless of thread scheduling, and peak memory is one
batch's output. Side-staged particle-preload meshes drain per batch
into a key-deduped map (first instance wins — per-id extraction output
is deterministic, so instance choice cannot affect bytes) and are
written after all batches, sorted by key, skipping keys already
written.

Program.cs is now a thin arg-parsing shell over the public BakeRunner
so the new dat-gated byte-reproducibility test can drive the REAL
pipeline: tests/AcDream.Bake.Tests (new project, rule 6; registered in
slnx; no Silk.NET in its resolved dependency graph — verified) bakes
the same 9-id mixed fixture twice with DIFFERENT thread counts (8 vs
3 — thread scheduling was the nondeterminism source) and asserts the
two pak files are byte-identical. Ran for real against the dats on
this machine: green.

(10) The isSetup argument for EnvCell extraction now matches at both
call sites (BakeRunner and PakEquivalenceTests both pass false) and is
documented at each: the runtime's own request sites
(WbMeshAdapter.IncrementRefCount / EnsureLoaded) pass isSetup: false
for every MeshRef id including cell-geometry ids. The parameter is
currently dead in MeshExtractor.PrepareMeshData (dispatch is on the
resolved dat type), but two disagreeing call sites were a latent trap.

Header FormatVersion is no longer set by the bake (PakWriter stamps it
per review finding 2, previous commit).
2026-07-05 22:14:31 +02:00

78 lines
2.6 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AcDream.Bake;
// acdream-bake: offline CLI producing a versioned pak file containing every
// ObjectMeshData the client's decode workers would otherwise produce at
// runtime (GfxObj / Setup / EnvCell mesh+texture payloads).
//
// Thin arg-parsing shell over BakeRunner (the real pipeline lives there so
// the dat-gated byte-reproducibility test can drive it directly).
//
// Plan: docs/superpowers/plans/2026-07-05-mp1b-pak-and-bake.md, Task 5.
string? datDir = null;
string? outPath = null;
HashSet<uint>? idFilter = null;
HashSet<uint>? landblockFilter = null;
int threads = Environment.ProcessorCount;
for (int i = 0; i < args.Length; i++) {
switch (args[i]) {
case "--dat-dir":
datDir = args.ElementAtOrDefault(++i);
break;
case "--out":
outPath = args.ElementAtOrDefault(++i);
break;
case "--ids":
idFilter = ParseHexList(args.ElementAtOrDefault(++i));
break;
case "--landblocks":
landblockFilter = ParseHexList(args.ElementAtOrDefault(++i));
break;
case "--threads":
if (int.TryParse(args.ElementAtOrDefault(++i), out var t) && t > 0) threads = t;
break;
default:
Console.Error.WriteLine($"unrecognized argument: {args[i]}");
return 2;
}
}
if (string.IsNullOrWhiteSpace(datDir)) {
Console.Error.WriteLine("usage: acdream-bake --dat-dir <path> [--out <file>] [--ids 0xId,0xId,...] [--landblocks 0xId,...] [--threads <n>]");
return 2;
}
if (!Directory.Exists(datDir)) {
Console.Error.WriteLine($"error: directory not found: {datDir}");
return 2;
}
outPath ??= Path.Combine(datDir, "acdream.pak");
return BakeRunner.Run(new BakeOptions {
DatDir = datDir,
OutPath = outPath,
IdFilter = idFilter,
LandblockFilter = landblockFilter,
Threads = threads,
});
static HashSet<uint> ParseHexList(string? raw) {
var result = new HashSet<uint>();
if (string.IsNullOrWhiteSpace(raw)) return result;
foreach (var token in raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) {
var hex = token.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? token[2..] : token;
if (uint.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out var value)) {
result.Add(value);
}
else {
Console.Error.WriteLine($"warning: could not parse id '{token}' — skipped");
}
}
return result;
}