feat(poc): build-stream 多线合并流式建体 + Track B 总验收实测

This commit is contained in:
gaozheng 2026-06-24 08:08:28 +08:00
parent 77cbe4a305
commit ba59c8861a
2 changed files with 461 additions and 0 deletions

View File

@ -0,0 +1,94 @@
# Track B 总验收实测结果build-stream 多线合并流式建体)
工具:`tools/gpr_poc`CLI子命令 `build-stream`
执行机Windows 11MSVCVS18 Community+ NinjaRelease/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 GB20 线 ≈84 GB → 装配阶段必然 OOM。
---
## 1. 验收命令
```
gpr_poc build-stream "D:\Downloads\明星路" --cellXY 0.2 --cellZ 0.05 --out <storeDir> --levels 2
gpr_poc load <storeDir>
```
- 合并方式:**沿 X 顺序排列**(退路近似)——各线按 brick 对齐的 X 偏移依次拼入一个连续 store。
真实 RTK 几何拼接(按各线实际平面坐标拼成路网大体)留 **Track D**
- 量化 scale/offset **全局一致**:先扫一遍全部 20 线得全局 min/max流式下不能每 slab 各算),
再以全局值域逐线逐 slab 写 brick。
---
## 2. build-stream 实测指标20 线合并大体cellXY=0.2cellZ=0.05levels=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 <storeDir>`
| 指标 | 值 |
|------|-----|
| 加载耗时 | **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」问题。

View File

@ -31,6 +31,7 @@
#include "core/model/ScalarVolumeI16.hpp" #include "core/model/ScalarVolumeI16.hpp"
#include "data/store/ChunkedVolumeStore.hpp" #include "data/store/ChunkedVolumeStore.hpp"
#include "io/gpr/GprSurveyAssembler.hpp" #include "io/gpr/GprSurveyAssembler.hpp"
#include "io/gpr/IprHeader.hpp"
#include "render/actors/VoxelActor.hpp" #include "render/actors/VoxelActor.hpp"
#include "render/source/OutOfCoreSource.hpp" #include "render/source/OutOfCoreSource.hpp"
#include "render/source/WholeVolumeSource.hpp" #include "render/source/WholeVolumeSource.hpp"
@ -315,6 +316,368 @@ int cmdBuild(int argc, char** argv) {
return 0; 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取 "*_<NNN>.ord" 的 NNN。
std::vector<std::string> discoverLines(const std::string& dir) {
std::vector<std::string> 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<unsigned char>(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<double> channelY; // 升序
int nx = 0, ny = 0, nz = 0; // 该线在合并网格下的体素维度X 未对齐 brick
};
// 全线总道数 = min 通道(fileBytes/(samples*2)),与 assembleGprSurvey 对齐口径一致。
std::int64_t totalTracesOf(const std::vector<std::string>& iprb, int samples) {
std::int64_t minTr = std::numeric_limits<std::int64_t>::max();
const std::int64_t per = static_cast<std::int64_t>(samples) * 2;
for (const auto& p : iprb) {
const std::int64_t bytes = static_cast<std::int64_t>(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 <dir> [--cellXY 0.05] "
"[--cellZ 0.05] [--out <storeDir>] [--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<std::string> lineNos = discoverLines(dir);
if (maxLines > 0 && static_cast<int>(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<LineFiles> files;
std::vector<LineGeom> 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<int>(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<int> 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";
// 每线网格 specorigin 沿 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<double>::infinity();
double vmax = -std::numeric_limits<double>::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 分 slabbrick 对齐),每 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<std::int64_t>::max();
std::int64_t t1 = std::numeric_limits<std::int64_t>::min();
for (int gi = gx0; gi < gx1; ++gi) {
const double worldX = gi * cellXY; // 线局部世界 Xspec.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;
// 线局部 specx0=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 覆盖的合并 brickX 列 [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<std::int16_t> voxels(
static_cast<std::size_t>(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<std::int64_t>(mergedNx) * mergedNy * mergedNz;
const std::int64_t rawBytes = voxels * 2;
const std::int64_t dataBytes = storeDataBytes(out);
const double ratio =
dataBytes > 0 ? static_cast<double>(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) { int cmdLoad(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2); const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) { if (a.positional.empty()) {
@ -3357,6 +3720,9 @@ void usage() {
std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n" std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n"
" gpr_poc build <dir> [--line 001] [--cellXY 0.2] " " gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
"[--cellZ 0.05] [--out <storeDir>] [--levels 2]\n" "[--cellZ 0.05] [--out <storeDir>] [--levels 2]\n"
" gpr_poc build-stream <dir> [--cellXY 0.05] [--cellZ 0.05] "
"[--out <storeDir>] [--levels 3] [--sliceXBricks 8] "
"[--maxLines N]\n"
" gpr_poc load <storeDir>\n" " gpr_poc load <storeDir>\n"
" gpr_poc selftest\n" " gpr_poc selftest\n"
" gpr_poc offscreen-smoke\n" " gpr_poc offscreen-smoke\n"
@ -3389,6 +3755,7 @@ int main(int argc, char** argv) {
const std::string cmd = argv[1]; const std::string cmd = argv[1];
try { try {
if (cmd == "build") return cmdBuild(argc, argv); if (cmd == "build") return cmdBuild(argc, argv);
if (cmd == "build-stream") return cmdBuildStream(argc, argv);
if (cmd == "load") return cmdLoad(argc, argv); if (cmd == "load") return cmdLoad(argc, argv);
if (cmd == "selftest") return cmdSelftest(); if (cmd == "selftest") return cmdSelftest();
if (cmd == "offscreen-smoke") return cmdOffscreenSmoke(); if (cmd == "offscreen-smoke") return cmdOffscreenSmoke();