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).
78 lines
2.6 KiB
C#
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;
|
|
}
|