From ba59c8861a713cfca1b673ff0478c494beaad989 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 24 Jun 2026 08:08:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(poc):=20build-stream=20=E5=A4=9A=E7=BA=BF?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E6=B5=81=E5=BC=8F=E5=BB=BA=E4=BD=93=20+=20Tr?= =?UTF-8?q?ack=20B=20=E6=80=BB=E9=AA=8C=E6=94=B6=E5=AE=9E=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/superpowers/plans/poc-results-trackB.md | 94 +++++ tools/gpr_poc/main.cpp | 367 +++++++++++++++++++ 2 files changed, 461 insertions(+) create mode 100644 docs/superpowers/plans/poc-results-trackB.md diff --git a/docs/superpowers/plans/poc-results-trackB.md b/docs/superpowers/plans/poc-results-trackB.md new file mode 100644 index 0000000..39f27d4 --- /dev/null +++ b/docs/superpowers/plans/poc-results-trackB.md @@ -0,0 +1,94 @@ +# Track B 总验收实测结果(build-stream 多线合并流式建体) + +工具:`tools/gpr_poc`(CLI),子命令 `build-stream`。 +执行机:Windows 11,MSVC(VS18 Community)+ Ninja,Release(/O2),开发机(RTX 3060 Laptop GPU 旁)。 +日期:2026-06-23。 + +整条流式链路(B1→B5 成果在 B6 串起来): +`assembleGprSurveySlab(道窗口)→ sampleGprPoint(结构化插值)→ StreamingVolumeWriter(增量 zlib 写 brick)→ buildPyramidStreaming(从盘 brick 逐级降采样)→ WholeVolumeSource(load)`。 + +> Track B 的核心命题:把建体管线改成沿 X 分段(slab)流式(逐段读道→插值→写 brick→释放), +> 使建「20 线合并大体」的峰值内存**有界、不随线数增长**、无 OOM,且产物可被 `load` 读回。 +> 对比基线:旧 double-survey 非流式方案,单线装配峰值 ≈4.2 GB,20 线 ≈84 GB → 装配阶段必然 OOM。 + +--- + +## 1. 验收命令 + +``` +gpr_poc build-stream "D:\Downloads\明星路" --cellXY 0.2 --cellZ 0.05 --out --levels 2 +gpr_poc load +``` + +- 合并方式:**沿 X 顺序排列**(退路近似)——各线按 brick 对齐的 X 偏移依次拼入一个连续 store。 + 真实 RTK 几何拼接(按各线实际平面坐标拼成路网大体)留 **Track D**。 +- 量化 scale/offset **全局一致**:先扫一遍全部 20 线得全局 min/max(流式下不能每 slab 各算), + 再以全局值域逐线逐 slab 写 brick。 + +--- + +## 2. build-stream 实测指标(20 线合并大体,cellXY=0.2,cellZ=0.05,levels=2) + +| 指标 | 值 | +|------|-----| +| 合并测线数 | **20** | +| 合并体维度(nx×ny×nz) | **156544 × 8 × 82** | +| 体素数 | **102,692,864**(≈1.03 亿) | +| 原始体积(int16,进显存判据) | **195.871 MB** | +| 落盘 data.bin(含金字塔各级) | **105.422 MB** | +| 压缩比(原始/落盘) | **1.858×** | +| 扫全局量化区间耗时 | **181,288 ms**(一次性读全 14 GB 原始道) | +| 建体(流式 slab→写 brick)耗时 | **89,927 ms** | +| 流式金字塔耗时 | **6,972 ms** | +| build-stream 端到端墙钟 | **278,788 ms(≈4.6 min)** | +| **峰值内存** | **246.164 MB** | + +### 峰值内存全程稳定(核心结论证据) + +20 线全程峰值内存稳在 **≈246 MB**(245.973 MB → 246.164 MB),**不随合并线数增长**: + +| 阶段 | 峰值内存 | +|------|----------| +| 扫全局量化 / 起始线 | 245.973 MB | +| 第 20 线写入完成 | 246.164 MB | + +即流式建体的峰值内存**有界**——只随单个 slab + 一行 brick 的工作集,与合并总量(线数/体素数)无关。 +对比旧 double-survey 非流式方案 20 线需 ≈84 GB,本方案以 246 MB 建出 1 亿体素的合并大体,**无 OOM**。 + +--- + +## 3. load 实测指标 —— 产物可读 ✓ + +命令:`gpr_poc load ` + +| 指标 | 值 | +|------|-----| +| 加载耗时 | **1,756 ms** | +| 整卷维度 | **156544 × 8 × 82** | +| 峰值内存 | **214.55 MB** | + +流式产出的合并大体 store 可被 `WholeVolumeSource` 完整加载、维度一致、**产物可读**。 + +--- + +## 4. Track B 总验收结论 + +- ✅ **峰值内存有界、与总量无关**:20 线合并大体建体全程峰值稳在 ≈246 MB + (245.973→246.164),不随线数增长。这是 Track B 的核心目标,达成。 +- ✅ **无 OOM**:旧非流式方案 20 线 ≈84 GB 必 OOM;流式方案 246 MB 即建出 1 亿体素合并体。 +- ✅ **产物可读**:`load` 在 1,756 ms 内读回,维度一致(156544×8×82),峰值 214.55 MB。 +- ✅ **压缩与落盘正常**:原始 195.871 MB → data.bin 105.422 MB(含金字塔各级),压缩比 1.858×。 + +### 已知约束 / 留待后续 + +1. **扫全局量化 181 s 是「读全 14 GB 原始道」的 I/O 开销**——为保证量化 scale/offset + 全局一致必须先全扫一遍 min/max。可后续优化(如复用建体阶段的单遍读、或用头估值域),本次不做。 +2. **合并几何用退路近似(沿 X 顺序排列,brick 对齐)**——非真实路网平面几何。 + 真实 RTK 几何拼接(按各线实际平面坐标拼成路网大体)留 **Track D**。 +3. **最低配未验**:仅在本机(RTX 3060 Laptop 开发机)实测;最低配机器内存/磁盘表现待补测。 + +### 与 Track C 的衔接 + +Track B 已证「大体能建(246 MB 有界)+ 能读(1.76 s 加载)」。Track C 在此大 store 上做 +视野自适应 LOD 体绘制(视锥裁剪+视距选层 → 视野区域单纹理重组 → 平滑切换+后台预取), +解决 POC-B §4 记录的「整卷 X 维超 GL 单轴纹理上限(16384)」问题。 diff --git a/tools/gpr_poc/main.cpp b/tools/gpr_poc/main.cpp index 20c4951..c8cd305 100644 --- a/tools/gpr_poc/main.cpp +++ b/tools/gpr_poc/main.cpp @@ -31,6 +31,7 @@ #include "core/model/ScalarVolumeI16.hpp" #include "data/store/ChunkedVolumeStore.hpp" #include "io/gpr/GprSurveyAssembler.hpp" +#include "io/gpr/IprHeader.hpp" #include "render/actors/VoxelActor.hpp" #include "render/source/OutOfCoreSource.hpp" #include "render/source/WholeVolumeSource.hpp" @@ -315,6 +316,368 @@ int cmdBuild(int argc, char** argv) { return 0; } +// ============================================================================ +// build-stream:多线合并流式建大体(Track B 总验收) +// ============================================================================ +// +// 把工区目录下全部测线(各 14 通道 .iprb + 该线 .ord)流式建成【一个连续合并大体】: +// 1) 扫目录发现所有线号; +// 2) 定合并网格:沿 X 顺序排列(线 i 接在线 i-1 之后,按 brick 对齐对齐到 64 格边界), +// Y/Z 取各线最大值——退路近似(report 标注),证明大体流式建得出来且内存有界; +// 3) 全局量化:单遍扫所有线所有 slab 定全局 vmin/vmax(一次只持一个 64 道 slab); +// 4) 单个 StreamingVolumeWriter 跨线逐 slab 逐 brick 写(各线落在合并网格对应 X 区域), +// 全程不持整卷/不持整线 survey; +// 5) buildPyramidStreaming → finalize。 +// +// 复用 buildGprVolumeStreaming 的 slab/采样核机制(sampleGprPoint + StreamingVolumeWriter), +// 仅在 X 方向把多线拼到同一 store。 + +constexpr int kStreamBrick = 64; // 与 StreamingVolumeWriter/Store 内部 brick 一致 + +int ceilDivInt(int n, int b) { return (n + b - 1) / b; } +int extentOf(int n, int b, int brick) { + const int got = n - b * brick; + return got < brick ? got : brick; +} + +// 发现工区内全部线号(三位零填充,如 "001"):扫 .ord,取 "*_.ord" 的 NNN。 +std::vector discoverLines(const std::string& dir) { + std::vector lines; + for (const auto& e : fs::directory_iterator(dir)) { + if (!e.is_regular_file()) continue; + if (e.path().extension().string() != ".ord") continue; + const std::string stem = e.path().stem().string(); // 明星路_001 + const std::size_t us = stem.find_last_of('_'); + if (us == std::string::npos) continue; + const std::string num = stem.substr(us + 1); + bool allDigit = !num.empty(); + for (char c : num) + if (!std::isdigit(static_cast(c))) allDigit = false; + if (allDigit) lines.push_back(num); + } + std::sort(lines.begin(), lines.end()); + return lines; +} + +// 一条线的几何 + 道距 + 全线总道数(不持整线:1 道 slab 取标尺,header 取总道数)。 +struct LineGeom { + int samples = 0; + std::int64_t totalTraces = 0; + double dx = 1.0, dz = 1.0; + std::vector channelY; // 升序 + int nx = 0, ny = 0, nz = 0; // 该线在合并网格下的体素维度(X 未对齐 brick) +}; + +// 全线总道数 = min 通道(fileBytes/(samples*2)),与 assembleGprSurvey 对齐口径一致。 +std::int64_t totalTracesOf(const std::vector& iprb, int samples) { + std::int64_t minTr = std::numeric_limits::max(); + const std::int64_t per = static_cast(samples) * 2; + for (const auto& p : iprb) { + const std::int64_t bytes = static_cast(fs::file_size(p)); + if (per <= 0) throw std::runtime_error("samples<=0"); + minTr = std::min(minTr, bytes / per); + } + return minTr; +} + +int cmdBuildStream(int argc, char** argv) { + const Args a = parseArgs(argc, argv, 2); + if (a.positional.empty()) { + std::cerr << "用法: gpr_poc build-stream [--cellXY 0.05] " + "[--cellZ 0.05] [--out ] [--levels 3] " + "[--sliceXBricks 8] [--maxLines N]\n"; + return 2; + } + const std::string dir = a.positional[0]; + const double cellXY = std::stod(a.get("cellXY", "0.05")); + const double cellZ = std::stod(a.get("cellZ", "0.05")); + const int levels = std::stoi(a.get("levels", "3")); + int sliceXBricks = std::stoi(a.get("sliceXBricks", "8")); + if (sliceXBricks <= 0) sliceXBricks = 1; + const int maxLines = std::stoi(a.get("maxLines", "0")); // 0=全部 + const std::string out = + a.get("out", (fs::temp_directory_path() / "gpr_store_merged").string()); + + std::cout << "[build-stream] dir=" << dir << " cellXY=" << cellXY + << " cellZ=" << cellZ << " levels=" << levels + << " sliceXBricks=" << sliceXBricks << " out=" << out << "\n"; + + // 1) 发现线号。 + std::vector lineNos = discoverLines(dir); + if (maxLines > 0 && static_cast(lineNos.size()) > maxLines) + lineNos.resize(maxLines); + std::cout << "[build-stream] 发现测线数=" << lineNos.size() << "\n"; + if (lineNos.empty()) { + std::cerr << "[build-stream] 错误: 未发现任何 .ord 测线\n"; + return 1; + } + + Stopwatch swTotal; + + // 2) 各线文件 + 几何(1 道 slab 取标尺,不持整线)。 + std::vector files; + std::vector geom; + files.reserve(lineNos.size()); + geom.reserve(lineNos.size()); + for (const std::string& ln : lineNos) { + LineFiles lf = discoverLine(dir, ln); + if (lf.iprb.empty() || lf.ord.empty()) { + std::cerr << "[build-stream] 警告: 线 " << ln << " 缺 iprb/ord,跳过\n"; + continue; + } + LineGeom g; + // 1 道 slab:取 dx/dz/samples/channelY(内存只随 1 道)。 + const geopro::core::GprSurvey s0 = + geopro::io::gpr::assembleGprSurveySlab(lf.iprb, lf.ord, 0, 1); + g.samples = s0.samples; + g.dx = s0.dx; + g.dz = s0.dz; + g.channelY = s0.channelY; + g.totalTraces = totalTracesOf(lf.iprb, g.samples); + + // 该线在合并网格下维度(X/Z 落格,Y 跨通道):与 specFromSurvey 同式。 + const double rangeX = (g.totalTraces > 1) ? (g.totalTraces - 1) * g.dx : 0.0; + const double rangeY = + g.channelY.empty() ? 0.0 : (g.channelY.back() - g.channelY.front()); + const double rangeZ = (g.samples > 1) ? (g.samples - 1) * g.dz : 0.0; + auto cells = [](double range, double cell) { + if (cell <= 0.0) return 1; + return static_cast(std::ceil(range / cell)) + 1; + }; + g.nx = cells(rangeX, cellXY); + g.ny = cells(rangeY, cellXY); + g.nz = cells(rangeZ, cellZ); + + std::cout << "[build-stream] 线 " << ln << " 通道=" << lf.iprb.size() + << " 道数=" << g.totalTraces << " samples=" << g.samples + << " nx=" << g.nx << " ny=" << g.ny << " nz=" << g.nz << "\n"; + files.push_back(std::move(lf)); + geom.push_back(std::move(g)); + } + if (files.empty()) { + std::cerr << "[build-stream] 错误: 无可用测线\n"; + return 1; + } + + // 3) 合并网格(沿 X 顺序排列;各线 X 起点对齐到 brick 边界)。 + // 每线占 [xBrickOffset, xBrickOffset + ceil(nx/brick)) 的 brick 列。 + std::vector xBrickOffset(files.size()); + int mergedBx = 0, mergedNy = 0, mergedNz = 0; + for (std::size_t i = 0; i < files.size(); ++i) { + xBrickOffset[i] = mergedBx; + mergedBx += ceilDivInt(geom[i].nx, kStreamBrick); + mergedNy = std::max(mergedNy, geom[i].ny); + mergedNz = std::max(mergedNz, geom[i].nz); + } + const int mergedNx = mergedBx * kStreamBrick; // 末线 brick 对齐后整宽 + const int bY = ceilDivInt(mergedNy, kStreamBrick); + const int bZ = ceilDivInt(mergedNz, kStreamBrick); + + std::cout << "[build-stream] 合并网格 nx=" << mergedNx << " ny=" << mergedNy + << " nz=" << mergedNz << " (bX=" << mergedBx << " bY=" << bY + << " bZ=" << bZ << ")\n"; + + // 每线网格 spec(origin 沿 X 平移到该线 brick 起点的世界 X)。 + auto specForLine = [&](std::size_t i) { + geopro::core::GridSpec spec{}; + spec.ox = xBrickOffset[i] * kStreamBrick * cellXY; // 该线在合并体的世界 X 起点 + spec.oy = geom[i].channelY.empty() ? 0.0 : geom[i].channelY.front(); + spec.oz = 0.0; + spec.dx = cellXY; + spec.dy = cellXY; + spec.dz = cellZ; + spec.nx = geom[i].nx; + spec.ny = geom[i].ny; + spec.nz = geom[i].nz; + spec.power = 2.0; + spec.maxDist = cellXY * 2.0; + return spec; + }; + + // 4) 全局量化:单遍扫所有线所有 slab(一次只持一个 64 道 slab)。 + std::cout << "[build-stream] 扫全局量化区间...\n"; + Stopwatch swScan; + double vmin = std::numeric_limits::infinity(); + double vmax = -std::numeric_limits::infinity(); + constexpr std::int64_t kScanChunk = 64; + for (std::size_t i = 0; i < files.size(); ++i) { + const std::int64_t total = geom[i].totalTraces; + for (std::int64_t t0 = 0; t0 < total; t0 += kScanChunk) { + const std::int64_t t1 = std::min(total, t0 + kScanChunk); + const auto slab = geopro::io::gpr::assembleGprSurveySlab( + files[i].iprb, files[i].ord, t0, t1); + for (double v : slab.values) { + if (std::isnan(v)) continue; + if (v < vmin) vmin = v; + if (v > vmax) vmax = v; + } + } + } + if (!(vmin <= vmax)) { + vmin = 0.0; + vmax = 0.0; + } + const double scanMs = swScan.elapsedMs(); + std::cout << "[build-stream] 全局值域 [" << vmin << ", " << vmax << "] 扫描 " + << scanMs << "ms\n"; + + geopro::core::Quant quant; + quant.scale = (vmax > vmin) ? (vmax - vmin) / 64000.0 : 1.0; + quant.offset = 0.5 * (vmin + vmax); + + // 5) 合并 StoreMeta + 单个 StreamingVolumeWriter。 + geopro::data::StoreMeta meta; + meta.nx = mergedNx; + meta.ny = mergedNy; + meta.nz = mergedNz; + meta.brick = kStreamBrick; + meta.origin = {0.0, 0.0, 0.0}; + meta.spacing = {cellXY, cellXY, cellZ}; + meta.quant = quant; + meta.vminPhys = vmin; + meta.vmaxPhys = vmax; + + fs::create_directories(out); + geopro::data::StreamingVolumeWriter writer(out, meta); + + // 6) 跨线逐 slab 逐 brick 写。每线在自己的 brick 列区间内沿 X 分 slab; + // 合并网格 brick (mergedBx, by, bz): + // - 落在某线列区间内且该线有覆盖 → sampleGprPoint(线局部索引); + // - 否则 → blank(线间填充 + 该线 ny/nz 之外的合并余量)。 + std::cout << "[build-stream] 流式写合并大体...\n"; + Stopwatch swBuild; + for (std::size_t i = 0; i < files.size(); ++i) { + const geopro::core::GridSpec spec = specForLine(i); + const int lineBx = ceilDivInt(geom[i].nx, kStreamBrick); + const int lineBy = ceilDivInt(geom[i].ny, kStreamBrick); + const int lineBz = ceilDivInt(geom[i].nz, kStreamBrick); + const std::int64_t total = geom[i].totalTraces; + const double surveyDx = geom[i].dx > 0.0 ? geom[i].dx : 1.0; + + // 沿 X 分 slab(brick 对齐),每 slab 含 sliceXBricks 个 X brick。 + for (int bcol = 0; bcol < lineBx; bcol += sliceXBricks) { + const int bxEnd = std::min(lineBx, bcol + sliceXBricks); + const int gx0 = bcol * kStreamBrick; + const int gx1 = std::min(spec.nx, bxEnd * kStreamBrick); + + // 该 slab 网格 X 列 → 全局道范围(夹到 [0,total)),可能全越界。 + std::int64_t t0 = std::numeric_limits::max(); + std::int64_t t1 = std::numeric_limits::min(); + for (int gi = gx0; gi < gx1; ++gi) { + const double worldX = gi * cellXY; // 线局部世界 X(spec.ox 已含偏移,但落格用线内 x0=0) + const std::int64_t g = std::llround(worldX / surveyDx); + if (g < 0 || g >= total) continue; + t0 = std::min(t0, g); + t1 = std::max(t1, g); + } + const bool hasTraces = (t0 <= t1); + geopro::core::GprSurvey slab; + // 线局部 spec:x0=0 落格(与 assembleGprSurveySlab 的 x0=t0*dx 对齐靠 worldX)。 + geopro::core::GridSpec localSpec = spec; + localSpec.ox = 0.0; // 采样核用线局部坐标 + if (hasTraces) { + slab = geopro::io::gpr::assembleGprSurveySlab(files[i].iprb, + files[i].ord, t0, t1 + 1); + } + + // 写该 slab 覆盖的合并 brick:X 列 [bcol,bxEnd) → 合并列 +xBrickOffset[i], + // Y/Z 全程(含该线 ny/nz 之外的合并余量 → blank)。 + for (int bz = 0; bz < bZ; ++bz) { + for (int by = 0; by < bY; ++by) { + for (int lbx = bcol; lbx < bxEnd; ++lbx) { + const int mbx = xBrickOffset[i] + lbx; // 合并 brick X 索引 + const int bw = extentOf(mergedNx, mbx, kStreamBrick); + const int bh = extentOf(mergedNy, by, kStreamBrick); + const int bd = extentOf(mergedNz, bz, kStreamBrick); + std::vector voxels( + static_cast(bw) * bh * bd); + // 该 brick 是否落在线自身覆盖范围内(线 brick 网格内)。 + const bool inLine = + (lbx < lineBx && by < lineBy && bz < lineBz && hasTraces); + if (!inLine) { + std::fill(voxels.begin(), voxels.end(), + geopro::core::ScalarVolumeI16::kBlank); + } else { + const int i0 = lbx * kStreamBrick, j0 = by * kStreamBrick, + k0 = bz * kStreamBrick; + std::size_t wi = 0; + for (int kk = 0; kk < bd; ++kk) { + for (int jj = 0; jj < bh; ++jj) { + for (int ii = 0; ii < bw; ++ii) { + const int gi = i0 + ii, gj = j0 + jj, gk = k0 + kk; + // 线网格之外的余格(合并 brick 比线 brick 大)→ blank。 + if (gi >= spec.nx || gj >= spec.ny || gk >= spec.nz) { + voxels[wi++] = geopro::core::ScalarVolumeI16::kBlank; + } else { + voxels[wi++] = geopro::core::sampleGprPoint( + slab, localSpec, gi, gj, gk, quant); + } + } + } + } + } + writer.writeBrick(mbx, by, bz, voxels); + } + } + } + } + std::cout << "[build-stream] 线 " << lineNos[i] << " 写入完成 (" + << (i + 1) << "/" << files.size() + << ") 峰值内存(MB)=" << Probe::peakMemMB() << "\n"; + } + writer.finalize(); + const double buildMs = swBuild.elapsedMs(); + + // 7) 流式金字塔。 + std::cout << "[build-stream] 流式金字塔 levels=" << levels << "...\n"; + Stopwatch swPyr; + { + geopro::data::ChunkedVolumeStore store(out); + store.buildPyramidStreaming(levels); + } + const double pyrMs = swPyr.elapsedMs(); + + const std::int64_t voxels = + static_cast(mergedNx) * mergedNy * mergedNz; + const std::int64_t rawBytes = voxels * 2; + const std::int64_t dataBytes = storeDataBytes(out); + const double ratio = + dataBytes > 0 ? static_cast(rawBytes) / dataBytes : 0.0; + const double totalMs = swTotal.elapsedMs(); + const double peak = Probe::peakMemMB(); + + std::cout << "\n=== build-stream 指标(多线合并大体)===\n"; + std::cout << "合并方式 : 沿 X 顺序排列(退路近似,brick 对齐)\n"; + std::cout << "测线数 : " << files.size() << "\n"; + std::cout << "合并体维度 : " << mergedNx << " x " << mergedNy << " x " + << mergedNz << "\n"; + std::cout << "体素数 : " << voxels << "\n"; + std::cout << "原始体积(B) : " << rawBytes << " (" + << rawBytes / (1024.0 * 1024.0) << " MB)\n"; + std::cout << "data.bin(B) : " << dataBytes << " (" + << dataBytes / (1024.0 * 1024.0) << " MB)\n"; + std::cout << "压缩比 : " << ratio << " x\n"; + std::cout << "扫量化耗时(ms) : " << scanMs << "\n"; + std::cout << "建体耗时(ms) : " << buildMs << "\n"; + std::cout << "金字塔耗时(ms) : " << pyrMs << "\n"; + std::cout << "总耗时(ms) : " << totalMs << "\n"; + std::cout << "峰值内存(MB) : " << peak << "\n"; + + writeMetricLine( + "build-stream,lines=" + std::to_string(files.size()) + + ",cellXY=" + std::to_string(cellXY) + ",cellZ=" + std::to_string(cellZ) + + ",nx=" + std::to_string(mergedNx) + ",ny=" + std::to_string(mergedNy) + + ",nz=" + std::to_string(mergedNz) + ",voxels=" + std::to_string(voxels) + + ",rawB=" + std::to_string(rawBytes) + + ",dataB=" + std::to_string(dataBytes) + + ",ratio=" + std::to_string(ratio) + ",scanMs=" + std::to_string(scanMs) + + ",buildMs=" + std::to_string(buildMs) + + ",pyrMs=" + std::to_string(pyrMs) + + ",totalMs=" + std::to_string(totalMs) + + ",peakMB=" + std::to_string(peak)); + return 0; +} + int cmdLoad(int argc, char** argv) { const Args a = parseArgs(argc, argv, 2); if (a.positional.empty()) { @@ -3357,6 +3720,9 @@ void usage() { std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n" " gpr_poc build [--line 001] [--cellXY 0.2] " "[--cellZ 0.05] [--out ] [--levels 2]\n" + " gpr_poc build-stream [--cellXY 0.05] [--cellZ 0.05] " + "[--out ] [--levels 3] [--sliceXBricks 8] " + "[--maxLines N]\n" " gpr_poc load \n" " gpr_poc selftest\n" " gpr_poc offscreen-smoke\n" @@ -3389,6 +3755,7 @@ int main(int argc, char** argv) { const std::string cmd = argv[1]; try { if (cmd == "build") return cmdBuild(argc, argv); + if (cmd == "build-stream") return cmdBuildStream(argc, argv); if (cmd == "load") return cmdLoad(argc, argv); if (cmd == "selftest") return cmdSelftest(); if (cmd == "offscreen-smoke") return cmdOffscreenSmoke();