16
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-07-17 23:39:18 +02:00
Files
maszyna/eu07-parser/include/eu07/scene/bake/bake_tree.hpp
maj00r beacc00932 Add headless parallel eu7v2 scenario bake with streaming and PLCE placements
Enable --eu7v2-bake from the main binary: parallel module pool, bounded-RAM
spool flush, streaming terrain triangles, flat include/model parsing, and
eu7v2 emit/load with optional verify. Large placement .scm files emit lean
PLCE records and bake referenced .inc modules separately for reuse.

- CLI: --eu7v2-bake, --eu7v2-verify, --eu7v2-mem-limit-gb, --eu7v2-threads,
  --eu7v2-max-parse; wire max_threads through to the bake parser
- eu7v2 v2 records: PLCE placements, runtime emitter/loader, batch verify
- Parallel bake pool with session cache; drop heavy-serial parse gate in spool
  mode; parse concurrency matches thread count
- Streaming terrain: batched parallel parse+bake, scan/bake pipeline, shape
  spool with persistent buffered I/O and flush-before-read
- Parallel flat-file streaming for models/includes; pack/model spool for
  low-memory incremental flush
- Optional 50 GB private-bytes guard during headless bake

Braniewo_szeroki: 160 modules, verify PASS, ~34s bake (nmt100 ~17s vs ~190s
serial baseline).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 21:15:42 +02:00

1022 lines
25 KiB
C++

#pragma once
// Rekurencyjny bake: kazdy plik tekstowy → wlasny .eu7, dzieci z INCL tez.
// Znaleziony include → od razu na kolejke i wolny watek (bez osobnej fazy odkrywania).
#include <eu07/parser.hpp>
#include <eu07/scene/bake/compose_pack_models.hpp>
#include <eu07/scene/bake/module.hpp>
#include <eu07/scene/bake/pack_model_spool.hpp>
#include <eu07/scene/bake/streaming_terrain.hpp>
#include <eu07/scene/binary/runtime_module.hpp>
#include <eu07/scene/include_resolve.hpp>
#include <eu07/scene/processor.hpp>
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <exception>
#include <filesystem>
#include <functional>
#include <iostream>
#include <memory>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <string>
#include <thread>
#include <unordered_set>
#include <utility>
#include <vector>
namespace eu07::scene::bake {
struct BakeTreeStats {
std::size_t modulesBaked = 0;
std::vector<std::filesystem::path> writtenPaths;
bool has_pack_diagnostics = false;
PackComposeStatsSnapshot pack_compose;
binary::PackWriteStats pack_write;
};
enum class BakeProgressPhase : std::uint8_t {
Starting,
Start,
PackModels,
PackCompose,
PackComposeDone,
Bake,
Done,
};
struct BakeProgress {
BakeProgressPhase phase = BakeProgressPhase::Bake;
std::size_t current = 0;
std::size_t total = 0;
std::filesystem::path path;
unsigned threads = 1;
const PackComposeStats* pack_stats = nullptr;
};
using BakeProgressCallback = std::function<void(const BakeProgress&)>;
struct BakeTreeOptions {
unsigned maxThreads = 0;
BakeProgressCallback onProgress;
// Hook wywolywany po zbakowaniu kazdego modulu (przed/zamiast zapisu .eu7).
// Dla roota packBatches != nullptr (sploszczone modele PACK). isRoot == true
// tylko dla pliku-korzenia drzewa bake. Moze byc wolany z wielu watkow.
std::function<void(
const RuntimeModule& module,
const std::filesystem::path& textPath,
bool isRoot,
const std::vector<binary::codec::ModelSectionBatch>* packBatches,
ShapeSpoolFile* shapeSpool)>
onModuleBaked;
// Gdy true: pomijamy zapis legacy .eu7 (writeRuntimeModule).
bool skipLegacyWrite = false;
// eu7v2 PACK: modele dzieci sa juz w root PACK — nie duplikuj INST w .eu7v2 dziecka.
bool omitChildModuleModels = false;
// Max rownoleglych pelnych parseFile/processScene w session cache (0 = auto: 1).
// Ogranicza peak RAM gdy wiele ciezkich .scm startuje naraz (Braniewo / linie).
unsigned maxConcurrentParses = 0;
// Mniej RAM, wolniej: 1 watek bake/compose, agresywne zwalnianie cache PACK.
bool lowMemoryMode = false;
// Pliki wieksze niz X MB: parse/bake tylko pojedynczo (0 = wylaczone).
unsigned heavyParseThresholdMb = 0;
// PACK compose: spłucz sekcje modeli poza RAM gdy przekroczony prog (lowMemoryMode).
std::size_t packFlushThreshold = 0;
bool packFlushPerFile = false;
std::function<void(std::vector<runtime::RuntimeModelInstance>&&)> onPackModelsFlush;
};
[[nodiscard]] inline bool useIncrementalRootPack(const BakeTreeOptions& options) {
return options.lowMemoryMode;
}
struct BakeTreeContext {
std::filesystem::path sceneryRoot;
std::string currentRelativeFile;
};
namespace detail {
struct BakeWorkItem {
std::filesystem::path path;
std::string relativeFile;
std::vector<std::filesystem::path> includeStack;
};
struct BakePoolState {
std::mutex queueMutex;
std::condition_variable queueCv;
std::deque<BakeWorkItem> queue;
std::mutex seenMutex;
std::unordered_set<std::string> seen;
std::atomic<std::size_t> inFlight{0};
std::atomic<std::size_t> completed{0};
std::atomic<bool> stop{false};
std::mutex statsMutex;
std::mutex rootMutex;
std::mutex progressMutex;
std::mutex errorMutex;
std::exception_ptr firstError;
};
[[nodiscard]] inline BakeTreeContext makeBakeTreeContext(const std::filesystem::path& inputPath) {
const std::filesystem::path resolved = inputPath.lexically_normal();
BakeTreeContext context;
if (const std::optional<std::filesystem::path> sceneryRoot =
eu07::scene::detail::findSceneryRootInPath(resolved)) {
context.sceneryRoot = *sceneryRoot;
context.currentRelativeFile =
eu07::scene::detail::relativeSceneryFile(context.sceneryRoot, resolved);
} else {
context.sceneryRoot = resolved.parent_path();
context.currentRelativeFile = resolved.filename().generic_string();
}
return context;
}
[[nodiscard]] inline std::string canonicalPathKey(const std::filesystem::path& path) {
return path.lexically_normal().generic_string();
}
[[nodiscard]] inline unsigned defaultPoolThreadCount() {
const unsigned hw =
std::thread::hardware_concurrency() == 0 ? 4u : std::thread::hardware_concurrency();
return std::max(1u, hw);
}
[[nodiscard]] inline unsigned resolvePoolThreadCount(const unsigned maxThreads) {
if (maxThreads == 0) {
return defaultPoolThreadCount();
}
return std::max(1u, maxThreads);
}
[[nodiscard]] inline unsigned resolveParseConcurrencyLimit(const unsigned maxConcurrentParses) {
if (maxConcurrentParses != 0) {
return std::max(1u, maxConcurrentParses);
}
return 1u;
}
[[nodiscard]] inline std::size_t heavyParseThresholdBytes(const unsigned threshold_mb) {
return threshold_mb == 0 ? 0u : static_cast<std::size_t>(threshold_mb) * 1024u * 1024u;
}
inline void recordError(BakePoolState& pool) {
std::lock_guard lock(pool.errorMutex);
if (!pool.firstError) {
pool.firstError = std::current_exception();
}
pool.stop.store(true, std::memory_order_release);
pool.queueCv.notify_all();
}
inline void enqueueWork(BakePoolState& pool, BakeWorkItem item) {
const std::string key = canonicalPathKey(item.path);
{
std::lock_guard lock(pool.seenMutex);
if (!pool.seen.insert(key).second) {
return;
}
}
{
std::lock_guard lock(pool.queueMutex);
pool.queue.push_back(std::move(item));
}
pool.queueCv.notify_one();
}
inline void bakeWorkItem(
const BakeWorkItem& item,
const BakeTreeContext& context,
const std::string& rootKey,
BakePoolState& pool,
BakeTreeStats* stats,
RuntimeModule& rootModuleOut,
const BakeTreeOptions& options,
const unsigned threadCount,
BakeParseSessionCache& sessionCache) {
if (pool.stop.load(std::memory_order_acquire)) {
return;
}
if (eu07::scene::detail::isIncludeCycle(item.path, item.includeStack)) {
throw std::runtime_error("EU7 bake: cykliczny include: " + item.path.string());
}
if (!std::filesystem::exists(item.path)) {
if (item.includeStack.empty()) {
throw std::runtime_error("EU7 bake: brak pliku: " + item.path.string());
}
return;
}
pool.inFlight.fetch_add(1, std::memory_order_relaxed);
bool models_pending_pack = false;
try {
RuntimeModule module;
const bool emitPackModels = canonicalPathKey(item.path) == rootKey;
std::vector<ParsedInclude> deferred_child_includes;
eu07::scene::detail::FlatFileKind flat_kind =
eu07::scene::detail::FlatFileKind::None;
std::unique_ptr<ShapeSpoolFile> triangle_shape_spool;
{
if (options.onProgress) {
std::lock_guard lock(pool.progressMutex);
options.onProgress(
BakeProgress{
BakeProgressPhase::Start,
pool.completed.load(std::memory_order_relaxed),
0,
item.path,
threadCount,
});
}
const bool prefer_lightweight_document =
useIncrementalRootPack(options) ||
eu07::scene::detail::shouldStreamFlatSourceFile(item.path);
if (prefer_lightweight_document) {
flat_kind = eu07::scene::detail::scanFlatFileKindStreaming(item.path);
}
const bool try_triangle_terrain_stream =
prefer_lightweight_document &&
flat_kind == eu07::scene::detail::FlatFileKind::None &&
eu07::scene::detail::shouldStreamFlatSourceFile(item.path);
bool used_triangle_terrain_stream = false;
SceneDocument empty_document;
const SceneDocument* document_ptr = nullptr;
if (try_triangle_terrain_stream) {
if (options.lowMemoryMode) {
const std::size_t path_hash = std::hash<std::string> {}(
item.path.lexically_normal().generic_string());
triangle_shape_spool = std::make_unique<ShapeSpoolFile>(
std::filesystem::temp_directory_path() /
("eu7v2_shape_" + std::to_string(path_hash) + ".bin"));
}
if (detail::streamBakeTriangleTerrain(
item.path, module, triangle_shape_spool.get(), threadCount)) {
used_triangle_terrain_stream = true;
document_ptr = &empty_document;
}
}
if (!used_triangle_terrain_stream) {
const SceneDocument& document =
(prefer_lightweight_document &&
(flat_kind == eu07::scene::detail::FlatFileKind::Models ||
flat_kind == eu07::scene::detail::FlatFileKind::Includes))
? sessionCache.documentForPackCompose(
item.path, context.sceneryRoot, nullptr, true)
: sessionCache.documentFor(item.path, context.sceneryRoot, nullptr);
document_ptr = &document;
}
const SceneDocument& document = *document_ptr;
const auto enqueue_unique_inc_modules = [&](const SceneDocument& doc) {
if (!options.onModuleBaked) {
return;
}
BakeWorkItem child;
child.includeStack = item.includeStack;
child.includeStack.push_back(item.path);
for (const ParsedInclude& include : doc.include) {
if (!eu07::scene::detail::isIncFile(include.file)) {
continue;
}
child.path = eu07::scene::detail::resolveIncludeSourcePath(
context.sceneryRoot, item.relativeFile, include.file);
child.relativeFile = eu07::scene::detail::relativeSceneryFile(
context.sceneryRoot, child.path);
enqueueWork(pool, child);
}
};
const auto enqueue_discovered_includes = [&](const SceneDocument& doc) {
BakeWorkItem child;
child.includeStack = item.includeStack;
child.includeStack.push_back(item.path);
for (const ParsedInclude& include : doc.include) {
// Placementy .inc (flora, szablony) — tylko do PACK compose,
// nie osobne moduly bake (3_rosl1-leo: 17k+ linii).
if (eu07::scene::detail::isIncFile(include.file)) {
continue;
}
child.path = eu07::scene::detail::resolveIncludeSourcePath(
context.sceneryRoot, item.relativeFile, include.file);
child.relativeFile = eu07::scene::detail::relativeSceneryFile(
context.sceneryRoot, child.path);
enqueueWork(pool, child);
}
};
// Root PACK compose jest bardzo pamieciochlonny — nie startuj rownolegle
// pelnych parse linii (l204/l220/…) az compose roota sie nie skonczy.
// lowMemoryMode + spool: jeden modul = jeden plik .scm, PACK skladany per modul.
if (emitPackModels) {
if (useIncrementalRootPack(options)) {
enqueue_discovered_includes(document);
} else {
deferred_child_includes = document.include;
}
} else {
enqueue_discovered_includes(document);
}
enqueue_unique_inc_modules(document);
const bool skipLocalModels =
emitPackModels ||
(options.omitChildModuleModels && isModelOnlyDocument(document));
if (!used_triangle_terrain_stream) {
module = bakeModule(document, {.skipLocalModels = skipLocalModels});
}
models_pending_pack =
!used_triangle_terrain_stream &&
skipLocalModels &&
(isModelOnlyDocument(document) ||
flat_kind == eu07::scene::detail::FlatFileKind::Models);
if (!used_triangle_terrain_stream) {
sessionCache.releaseHeavyParseStorageAfterModuleBake(
item.path, models_pending_pack);
}
}
const std::filesystem::path eu7Path =
item.path.parent_path() / (item.path.stem().string() + ".eu7");
const std::string key = canonicalPathKey(item.path);
binary::WriteRuntimeModuleOptions writeOpts;
writeOpts.emitPackModels = emitPackModels;
std::vector<binary::codec::ModelSectionBatch> packBatches;
if (writeOpts.emitPackModels && !useIncrementalRootPack(options)) {
if (options.onProgress) {
std::lock_guard lock(pool.progressMutex);
options.onProgress(
BakeProgress{
BakeProgressPhase::PackModels,
pool.completed.load(std::memory_order_relaxed),
0,
item.path,
threadCount,
});
}
PackComposeStats pack_stats;
PackComposeProgress pack_progress;
if (options.onProgress) {
pack_progress.on_progress =
[&](const std::filesystem::path& file,
const std::size_t files_visited,
const std::size_t models_total) {
std::lock_guard lock(pool.progressMutex);
BakeProgress progress{
BakeProgressPhase::PackCompose,
files_visited,
models_total,
file,
threadCount,
};
progress.pack_stats = &pack_stats;
options.onProgress(progress);
};
}
PackComposeOptions pack_options;
pack_options.progress = &pack_progress;
pack_options.stats = &pack_stats;
pack_options.max_threads = threadCount;
pack_options.low_memory = options.lowMemoryMode;
pack_options.pack_flush_threshold = options.packFlushThreshold;
pack_options.pack_flush_per_file = options.packFlushPerFile;
pack_options.on_pack_models_flush = options.onPackModelsFlush;
pack_options.pack_batches = &packBatches;
pack_options.session_cache = &sessionCache;
if (options.onProgress) {
std::cerr << "[EU7] PACK compose: " << threadCount << " watkow\n" << std::flush;
}
const std::size_t pack_model_count = composePackModels(
item.path, context.sceneryRoot, item.relativeFile, pack_options);
writeOpts.pack_batches = &packBatches;
if (options.onProgress) {
std::lock_guard lock(pool.progressMutex);
options.onProgress(
BakeProgress{
BakeProgressPhase::PackComposeDone,
pack_progress.files_visited,
pack_model_count,
item.path,
threadCount,
});
std::cerr << "[EU7] PACK gotowe: modele=" << pack_model_count
<< " sekcji=" << packBatches.size() << '\n'
<< std::flush;
}
printPackComposeStats(pack_stats, std::cerr);
printBakeParseSessionStats(sessionCache, std::cerr);
if (stats != nullptr) {
stats->has_pack_diagnostics = true;
stats->pack_compose = snapshotPackComposeStats(pack_stats);
}
}
// lowMemoryMode: jeden plik = jeden modul — PACK tylko z tego .scm, potem spool/RAM zwolnione.
if (useIncrementalRootPack(options) && models_pending_pack && !emitPackModels) {
if (flat_kind == eu07::scene::detail::FlatFileKind::Models &&
options.onPackModelsFlush) {
detail::streamFlatModelsToPackFlush(item.path, options.onPackModelsFlush);
} else {
std::vector<binary::codec::ModelSectionBatch> module_pack_batches;
PackComposeOptions pack_options;
pack_options.low_memory = true;
pack_options.pack_flush_per_file = options.packFlushPerFile;
pack_options.on_pack_models_flush = options.onPackModelsFlush;
pack_options.pack_batches = &module_pack_batches;
pack_options.session_cache = &sessionCache;
pack_options.max_threads = 1;
(void)composePackModels(
item.path, context.sceneryRoot, item.relativeFile, pack_options);
if (options.onPackModelsFlush) {
std::vector<runtime::RuntimeModelInstance> tail;
for (binary::codec::ModelSectionBatch& batch : module_pack_batches) {
tail.insert(
tail.end(),
std::make_move_iterator(batch.models.begin()),
std::make_move_iterator(batch.models.end()));
}
if (!tail.empty()) {
options.onPackModelsFlush(std::move(tail));
}
}
}
sessionCache.dropPackCacheEntry(item.path);
}
// Hook: pozwala silnikowi wyemitowac wlasny format (eu7v2) bezposrednio
// z RuntimeModule, z dostepem do sploszczonych modeli PACK dla roota.
if (options.onModuleBaked) {
const bool root_with_pack =
emitPackModels && !useIncrementalRootPack(options);
const std::vector<binary::codec::ModelSectionBatch>* batchesPtr =
root_with_pack ? &packBatches : nullptr;
options.onModuleBaked(
module, item.path, emitPackModels, batchesPtr, triangle_shape_spool.get());
}
if (emitPackModels && !deferred_child_includes.empty() &&
!useIncrementalRootPack(options)) {
SceneDocument stub;
stub.include = std::move(deferred_child_includes);
BakeWorkItem child;
child.includeStack = item.includeStack;
child.includeStack.push_back(item.path);
for (const ParsedInclude& include : stub.include) {
if (eu07::scene::detail::isIncFile(include.file)) {
continue;
}
child.path = eu07::scene::detail::resolveIncludeSourcePath(
context.sceneryRoot, item.relativeFile, include.file);
child.relativeFile =
eu07::scene::detail::relativeSceneryFile(context.sceneryRoot, child.path);
enqueueWork(pool, child);
}
}
if (!models_pending_pack) {
sessionCache.evictEntryIfNotNeeded(item.path);
}
if (!options.skipLegacyWrite) {
binary::PackWriteStats pack_write_stats;
if (writeOpts.emitPackModels) {
writeOpts.pack_write_stats = &pack_write_stats;
std::cerr << "[EU7] zapis .eu7 (PACK)...\n" << std::flush;
}
binary::writeRuntimeModule(eu7Path, module, writeOpts);
if (writeOpts.emitPackModels) {
printPackWriteStats(pack_write_stats, std::cerr);
if (stats != nullptr) {
stats->pack_write = pack_write_stats;
}
}
}
const std::size_t done = pool.completed.fetch_add(1, std::memory_order_relaxed) + 1;
if (stats != nullptr) {
std::lock_guard lock(pool.statsMutex);
++stats->modulesBaked;
stats->writtenPaths.push_back(eu7Path);
}
if (key == rootKey) {
std::lock_guard lock(pool.rootMutex);
rootModuleOut = std::move(module);
}
if (options.onProgress) {
std::lock_guard lock(pool.progressMutex);
options.onProgress(
BakeProgress{
BakeProgressPhase::Bake,
done,
0,
item.path,
threadCount,
});
}
} catch (...) {
recordError(pool);
pool.inFlight.fetch_sub(1, std::memory_order_relaxed);
pool.queueCv.notify_all();
return;
}
pool.inFlight.fetch_sub(1, std::memory_order_relaxed);
pool.queueCv.notify_all();
}
inline void bakePoolWorker(
BakePoolState& pool,
const BakeTreeContext& context,
const std::string& rootKey,
BakeTreeStats* stats,
RuntimeModule& rootModuleOut,
const BakeTreeOptions& options,
const unsigned threadCount,
BakeParseSessionCache& sessionCache) {
while (true) {
BakeWorkItem item;
{
std::unique_lock lock(pool.queueMutex);
pool.queueCv.wait(lock, [&]() {
return pool.stop.load(std::memory_order_acquire) || !pool.queue.empty() ||
pool.inFlight.load(std::memory_order_relaxed) == 0;
});
if (pool.stop.load(std::memory_order_acquire)) {
return;
}
if (pool.queue.empty()) {
if (pool.inFlight.load(std::memory_order_relaxed) == 0) {
return;
}
continue;
}
item = std::move(pool.queue.front());
pool.queue.pop_front();
}
try {
bakeWorkItem(
item, context, rootKey, pool, stats, rootModuleOut, options, threadCount, sessionCache);
} catch (...) {
recordError(pool);
}
if (pool.stop.load(std::memory_order_acquire)) {
return;
}
}
}
inline void runBakePool(
const std::filesystem::path& rootSourcePath,
const BakeTreeContext& context,
BakeTreeStats* stats,
RuntimeModule& rootModuleOut,
const BakeTreeOptions& options) {
const std::string rootKey = canonicalPathKey(rootSourcePath.lexically_normal());
const unsigned threadCount = resolvePoolThreadCount(options.maxThreads);
if (options.onProgress) {
options.onProgress(
BakeProgress{
BakeProgressPhase::Starting,
0,
0,
rootSourcePath,
threadCount,
});
}
BakePoolState pool;
BakeParseSessionCache sessionCache {
detail::resolveParseConcurrencyLimit(options.maxConcurrentParses),
options.lowMemoryMode,
detail::heavyParseThresholdBytes(options.heavyParseThresholdMb) };
enqueueWork(
pool,
BakeWorkItem{
rootSourcePath.lexically_normal(),
context.currentRelativeFile,
{},
});
std::vector<std::thread> workers;
workers.reserve(threadCount);
for (unsigned i = 0; i < threadCount; ++i) {
workers.emplace_back(
bakePoolWorker,
std::ref(pool),
std::cref(context),
std::cref(rootKey),
stats,
std::ref(rootModuleOut),
std::cref(options),
threadCount,
std::ref(sessionCache));
}
for (std::thread& worker : workers) {
worker.join();
}
if (pool.firstError) {
std::rethrow_exception(pool.firstError);
}
if (options.onProgress) {
const std::size_t done = pool.completed.load(std::memory_order_relaxed);
options.onProgress(
BakeProgress{
BakeProgressPhase::Done,
done,
done,
rootSourcePath,
threadCount,
});
}
}
} // namespace detail
[[nodiscard]] inline RuntimeModule bakeModuleTree(
const std::filesystem::path& inputPath,
BakeTreeStats* stats = nullptr,
const BakeTreeOptions& options = {}) {
const BakeTreeContext context = detail::makeBakeTreeContext(inputPath);
RuntimeModule rootModule;
detail::runBakePool(inputPath, context, stats, rootModule, options);
return rootModule;
}
} // namespace eu07::scene::bake