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/streaming_terrain.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

433 lines
14 KiB
C++

#pragma once
// Duze pliki terenu (same node triangles, np. nmt100_warmaz_ter.scm):
// linia po linii, parse per-wezel, bez readRawFile / tokenizacji calego pliku.
#include <eu07/parser.hpp>
#include <eu07/scene/bake/mesh.hpp>
#include <eu07/scene/bake/module.hpp>
#include <eu07/scene/bake/pack_model_spool.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/common.hpp>
#include <eu07/scene/node/triangles.hpp>
#include <eu07/scene/parallel_models.hpp>
#include <atomic>
#include <condition_variable>
#include <cstddef>
#include <deque>
#include <filesystem>
#include <functional>
#include <mutex>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
namespace eu07::scene::bake::detail {
enum class TriangleTerrainLineKind {
Skip,
Header,
Vertex,
EndTri,
Invalid,
};
[[nodiscard]] inline TriangleTerrainLineKind classifyTriangleTerrainLine(std::string_view text) {
skipFieldSeparators(text);
if (text.empty()) {
return TriangleTerrainLineKind::Skip;
}
if (eu07::detail::isLineComment(text)) {
return TriangleTerrainLineKind::Skip;
}
if (eu07::scene::detail::segmentStartsWithKeyword(text, "node")) {
if (text.find("triangles") == std::string_view::npos) {
return TriangleTerrainLineKind::Invalid;
}
return TriangleTerrainLineKind::Header;
}
if (isKeyword(text, "endtri") || isKeyword(text, "endtriangles")) {
return TriangleTerrainLineKind::EndTri;
}
if (!text.empty() &&
(text.front() == '-' || text.front() == '+' ||
(text.front() >= '0' && text.front() <= '9'))) {
return TriangleTerrainLineKind::Vertex;
}
return TriangleTerrainLineKind::Invalid;
}
[[nodiscard]] inline bool parseTriangleTerrainBlock(
const std::vector<std::string>& lines,
const std::size_t header_line,
ParsedNodeTriangles& out) {
if (lines.size() < 2) {
return false;
}
std::vector<SourceToken> header_tokens;
tokenizeInto(lines.front(), header_tokens, header_line);
TokenStream stream(header_tokens);
NodeHeader header;
std::string subtype;
std::vector<SourceToken> raw;
if (!node::io::consumeHeader(stream, header, subtype, raw) ||
!isKeyword(subtype, node_triangles::kSubtype) ||
!node::io::takeString(stream, raw, out.texture)) {
return false;
}
out.header = header;
out.raw.clear();
out.vertices.clear();
out.vertices.reserve(lines.size() > 2 ? lines.size() - 2 : 0);
for (std::size_t i = 1; i + 1 < lines.size(); ++i) {
std::vector<SourceToken> vertex_tokens;
tokenizeInto(lines[i], vertex_tokens, header_line + i);
TokenStream vertex_stream(vertex_tokens);
MeshVertex vertex;
if (!node_triangles::takeVertex(vertex_stream, raw, vertex, !vertex_stream.empty())) {
return false;
}
out.vertices.push_back(vertex);
}
return !out.vertices.empty();
}
struct TriangleTerrainBlock {
std::size_t header_line = 0;
std::vector<std::string> lines;
};
constexpr std::size_t kTerrainParallelBatchSize = 8192;
constexpr std::size_t kTerrainParallelMinBlocks = 8;
constexpr std::size_t kTerrainPipelineMaxQueuedBatches = 2;
[[nodiscard]] inline unsigned resolveTerrainThreadCount(const unsigned max_threads) {
const unsigned hw =
std::thread::hardware_concurrency() == 0 ? 4u : std::thread::hardware_concurrency();
if (max_threads == 0) {
return std::max(1u, hw);
}
return std::max(1u, max_threads);
}
[[nodiscard]] inline bool bakeTriangleTerrainBatch(
const std::vector<TriangleTerrainBlock>& batch,
const unsigned max_threads,
std::vector<runtime::RuntimeShapeNode>& baked_out) {
baked_out.resize(batch.size());
if (batch.empty()) {
return true;
}
if (batch.size() < kTerrainParallelMinBlocks ||
resolveTerrainThreadCount(max_threads) <= 1) {
for (std::size_t i = 0; i < batch.size(); ++i) {
ParsedNodeTriangles parsed;
if (!parseTriangleTerrainBlock(batch[i].lines, batch[i].header_line, parsed)) {
return false;
}
baked_out[i] = bakeTriangles(parsed);
}
return true;
}
const unsigned worker_count = resolveTerrainThreadCount(max_threads);
std::atomic<std::size_t> next_index { 0 };
std::atomic<bool> ok { true };
const auto worker = [&]() {
while (ok.load(std::memory_order_relaxed)) {
const std::size_t index = next_index.fetch_add(1, std::memory_order_relaxed);
if (index >= batch.size()) {
return;
}
try {
ParsedNodeTriangles parsed;
if (!parseTriangleTerrainBlock(
batch[index].lines, batch[index].header_line, parsed)) {
ok.store(false, std::memory_order_relaxed);
return;
}
baked_out[index] = bakeTriangles(parsed);
} catch (...) {
ok.store(false, std::memory_order_relaxed);
return;
}
}
};
std::vector<std::thread> threads;
threads.reserve(worker_count);
for (unsigned i = 0; i < worker_count; ++i) {
threads.emplace_back(worker);
}
for (std::thread& thread : threads) {
thread.join();
}
return ok.load(std::memory_order_relaxed);
}
[[nodiscard]] inline bool scanTriangleTerrainOnlyStreaming(const std::filesystem::path& path) {
bool saw_header = false;
bool in_block = false;
bool ok = true;
eu07::scene::detail::forEachStreamingLogicalSegment(
path, [&](const LogicalSegment& segment) {
const TriangleTerrainLineKind kind = classifyTriangleTerrainLine(segment.text);
switch (kind) {
case TriangleTerrainLineKind::Skip:
return true;
case TriangleTerrainLineKind::Header:
saw_header = true;
in_block = true;
return true;
case TriangleTerrainLineKind::Vertex:
if (!in_block) {
ok = false;
return false;
}
return true;
case TriangleTerrainLineKind::EndTri:
if (!in_block) {
ok = false;
return false;
}
in_block = false;
return true;
case TriangleTerrainLineKind::Invalid:
default:
ok = false;
return false;
}
});
return ok && saw_header && !in_block;
}
[[nodiscard]] inline bool scanTriangleTerrainBlocksStreaming(
const std::filesystem::path& path,
const std::function<bool(std::vector<TriangleTerrainBlock>&&)>& on_batch) {
std::vector<TriangleTerrainBlock> batch;
batch.reserve(kTerrainParallelBatchSize);
TriangleTerrainBlock current_block;
bool in_block = false;
bool saw_header = false;
const auto finish_block = [&]() -> bool {
if (current_block.lines.empty()) {
return true;
}
batch.push_back(std::move(current_block));
current_block = {};
in_block = false;
if (batch.size() >= kTerrainParallelBatchSize) {
if (!on_batch(std::move(batch))) {
return false;
}
batch = {};
batch.reserve(kTerrainParallelBatchSize);
}
return true;
};
bool ok = true;
eu07::scene::detail::forEachStreamingLogicalSegment(
path, [&](const LogicalSegment& segment) {
const TriangleTerrainLineKind kind = classifyTriangleTerrainLine(segment.text);
switch (kind) {
case TriangleTerrainLineKind::Skip:
return true;
case TriangleTerrainLineKind::Header:
saw_header = true;
if (!finish_block()) {
ok = false;
return false;
}
current_block.header_line = segment.sourceLine;
current_block.lines.emplace_back(segment.text);
in_block = true;
return true;
case TriangleTerrainLineKind::Vertex:
if (!in_block) {
ok = false;
return false;
}
current_block.lines.emplace_back(segment.text);
return true;
case TriangleTerrainLineKind::EndTri:
if (!in_block) {
ok = false;
return false;
}
current_block.lines.emplace_back(segment.text);
if (!finish_block()) {
ok = false;
return false;
}
return true;
case TriangleTerrainLineKind::Invalid:
default:
ok = false;
return false;
}
});
if (ok && in_block) {
ok = finish_block();
}
if (ok && !batch.empty()) {
ok = on_batch(std::move(batch));
}
return ok && saw_header;
}
// Zwraca false gdy plik nie jest czystym terenem triangles (wtedy uzyj documentFor).
// Gdy shape_spool != nullptr, ksztalty trafiaja na dysk (module.scene.shapes puste).
[[nodiscard]] inline bool streamBakeTriangleTerrain(
const std::filesystem::path& path,
RuntimeModule& module,
ShapeSpoolFile* shape_spool = nullptr,
const unsigned max_threads = 0) {
module.scene.shapes.clear();
const unsigned bake_threads = resolveTerrainThreadCount(max_threads);
bool parsed_any = false;
const auto emit_baked = [&](std::vector<runtime::RuntimeShapeNode>& baked) -> bool {
if (baked.empty()) {
return true;
}
parsed_any = true;
if (shape_spool != nullptr) {
shape_spool->append_batch(std::move(baked));
} else {
module.scene.shapes.insert(
module.scene.shapes.end(),
std::make_move_iterator(baked.begin()),
std::make_move_iterator(baked.end()));
baked.clear();
}
return true;
};
const auto bake_and_emit = [&](std::vector<TriangleTerrainBlock>&& blocks) -> bool {
if (blocks.empty()) {
return true;
}
std::vector<runtime::RuntimeShapeNode> baked;
if (!bakeTriangleTerrainBatch(blocks, max_threads, baked)) {
return false;
}
return emit_baked(baked);
};
if (bake_threads <= 1) {
const bool ok = scanTriangleTerrainBlocksStreaming(path, bake_and_emit);
if (!ok || !parsed_any) {
module.scene.shapes.clear();
return false;
}
return true;
}
std::mutex queue_mutex;
std::condition_variable queue_cv;
std::deque<std::vector<TriangleTerrainBlock>> ready_batches;
std::atomic<bool> scan_ok { true };
std::atomic<bool> scan_done { false };
std::atomic<bool> bake_failed { false };
std::thread scanner([&]() {
try {
const bool ok = scanTriangleTerrainBlocksStreaming(
path, [&](std::vector<TriangleTerrainBlock>&& blocks) -> bool {
if (bake_failed.load(std::memory_order_relaxed)) {
return false;
}
std::unique_lock lock(queue_mutex);
queue_cv.wait(lock, [&]() {
return bake_failed.load(std::memory_order_relaxed) ||
ready_batches.size() < kTerrainPipelineMaxQueuedBatches;
});
if (bake_failed.load(std::memory_order_relaxed)) {
return false;
}
ready_batches.push_back(std::move(blocks));
lock.unlock();
queue_cv.notify_one();
return true;
});
scan_ok.store(ok, std::memory_order_relaxed);
} catch (...) {
scan_ok.store(false, std::memory_order_relaxed);
bake_failed.store(true, std::memory_order_relaxed);
queue_cv.notify_all();
}
scan_done.store(true, std::memory_order_release);
queue_cv.notify_all();
});
while (true) {
std::vector<TriangleTerrainBlock> batch;
{
std::unique_lock lock(queue_mutex);
queue_cv.wait(lock, [&]() {
return bake_failed.load(std::memory_order_relaxed) ||
!ready_batches.empty() ||
scan_done.load(std::memory_order_acquire);
});
if (bake_failed.load(std::memory_order_relaxed)) {
break;
}
if (ready_batches.empty() && scan_done.load(std::memory_order_acquire)) {
break;
}
if (ready_batches.empty()) {
continue;
}
batch = std::move(ready_batches.front());
ready_batches.pop_front();
}
queue_cv.notify_one();
std::vector<runtime::RuntimeShapeNode> baked;
if (!bakeTriangleTerrainBatch(batch, max_threads, baked)) {
bake_failed.store(true, std::memory_order_relaxed);
queue_cv.notify_all();
break;
}
if (!emit_baked(baked)) {
bake_failed.store(true, std::memory_order_relaxed);
queue_cv.notify_all();
break;
}
}
scanner.join();
if (bake_failed.load(std::memory_order_relaxed) ||
!scan_ok.load(std::memory_order_relaxed) || !parsed_any) {
module.scene.shapes.clear();
return false;
}
return true;
}
} // namespace eu07::scene::bake::detail