geopro/tools/gpr_poc/main.cpp

1356 lines
53 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// gpr_poc —— POC-B headless 度量 CLI。
//
// 串起整条地基:发现 14 通道 .iprb + .ord → assembleGprSurvey → buildGprVolume
// → ChunkedVolumeStore::write → buildPyramid → WholeVolumeSource(load)
// 在真实/合成数据上输出可测的真实指标(耗时/维度/体积/压缩比/加载/峰值内存)。
//
// 子命令:
// gpr_poc build <dir> [--line 001] [--cellXY 0.2] [--cellZ 0.05] [--out <storeDir>] [--levels 2]
// gpr_poc load <storeDir>
// gpr_poc selftest
// gpr_poc offscreen-smoke —— 离屏 GL 闸门冒烟
// gpr_poc renderB <storeDir> [--frames 120] —— 离屏体绘制/切片 fps 基准
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <string>
#include <vector>
#include "Probe.hpp"
#include "core/algo/GprVolumeBuilder.hpp"
#include "core/algo/IInterpolator.hpp"
#include "core/model/GprSurvey.hpp"
#include "core/model/ColorScale.hpp"
#include "core/model/ScalarVolumeI16.hpp"
#include "data/store/ChunkedVolumeStore.hpp"
#include "io/gpr/GprSurveyAssembler.hpp"
#include "render/actors/VoxelActor.hpp"
#include "render/source/OutOfCoreSource.hpp"
#include "render/source/WholeVolumeSource.hpp"
// ---- VTK 离屏渲染 ----
#include <vtkActor.h>
#include <vtkCamera.h>
#include <vtkCubeSource.h>
#include <vtkGPUVolumeRayCastMapper.h>
#include <vtkOpenGLGPUVolumeRayCastMapper.h>
#include <vtkImageActor.h>
#include <vtkImageData.h>
#include <vtkImageMapToColors.h>
#include <vtkImageMapper3D.h>
#include <vtkImageReslice.h>
#include <vtkLookupTable.h>
#include <vtkColorTransferFunction.h>
#include <vtkMultiBlockDataSet.h>
#include <vtkMultiBlockVolumeMapper.h>
#include <vtkNew.h>
#include <vtkPiecewiseFunction.h>
#include <vtkVolumeProperty.h>
#include <vtkObjectFactory.h>
#include <vtkOutputWindow.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
#include <vtkRenderWindow.h>
#include <vtkRenderer.h>
#include <vtkSmartPointer.h>
#include <vtkSmartVolumeMapper.h>
#include <vtkUnsignedCharArray.h>
#include <vtkVolume.h>
namespace fs = std::filesystem;
using geopro::tools::Probe;
using geopro::tools::Stopwatch;
namespace {
constexpr int kChannels = 14;
// ---- 命令行参数解析(极简 --key value----
struct Args {
std::map<std::string, std::string> kv;
std::vector<std::string> positional;
std::string get(const std::string& key, const std::string& def) const {
auto it = kv.find(key);
return it != kv.end() ? it->second : def;
}
};
Args parseArgs(int argc, char** argv, int start) {
Args a;
for (int i = start; i < argc; ++i) {
std::string tok = argv[i];
if (tok.rfind("--", 0) == 0 && i + 1 < argc) {
a.kv[tok.substr(2)] = argv[i + 1];
++i;
} else {
a.positional.push_back(tok);
}
}
return a;
}
// 把一行指标追加写入 last-metrics.txt与可执行同目录的工具源目录无关
// 写到当前工作目录便于汇总CSV 风格一行)。
void writeMetricLine(const std::string& line) {
std::ofstream f("last-metrics.txt", std::ios::app);
if (f) f << line << "\n";
}
// 发现某线 14 通道 .iprb按通道号 A01..A14 排序)+ 该线 .ord。
struct LineFiles {
std::vector<std::string> iprb; // 已按通道号排序
std::string ord;
};
LineFiles discoverLine(const std::string& dir, const std::string& line) {
LineFiles lf;
std::map<int, std::string> byChannel; // 通道号 → 路径(自动按号排序)
std::string ordPath;
for (const auto& e : fs::directory_iterator(dir)) {
if (!e.is_regular_file()) continue;
const std::string name = e.path().filename().string();
const std::string ext = e.path().extension().string();
// .ord优先匹配本线含 "_<line>" 且以 .ord 结尾),否则记下工区任一 .ord 作兜底。
if (ext == ".ord") {
if (name.find("_" + line + ".") != std::string::npos) {
ordPath = e.path().string();
} else if (ordPath.empty()) {
ordPath = e.path().string();
}
continue;
}
// .iprb匹配 "*_<line>_A<NN>.iprb"。
if (ext != ".iprb") continue;
const std::string tag = "_" + line + "_A";
const std::size_t pos = name.find(tag);
if (pos == std::string::npos) continue;
const std::size_t numStart = pos + tag.size();
std::size_t numEnd = numStart;
while (numEnd < name.size() &&
std::isdigit(static_cast<unsigned char>(name[numEnd]))) {
++numEnd;
}
if (numEnd == numStart) continue;
const int ch = std::stoi(name.substr(numStart, numEnd - numStart));
byChannel[ch] = e.path().string();
}
for (const auto& [ch, path] : byChannel) lf.iprb.push_back(path);
lf.ord = ordPath;
return lf;
}
// 由 survey 推 GridSpecX 沿测线Y 跨通道Z 深度。
geopro::core::GridSpec specFromSurvey(const geopro::core::GprSurvey& s,
double cellXY, double cellZ) {
geopro::core::GridSpec spec{};
const double rangeX =
(s.ntraces > 1) ? (s.ntraces - 1) * s.dx : 0.0;
const double y0 = s.channelY.empty() ? 0.0 : s.channelY.front();
const double y1 = s.channelY.empty() ? 0.0 : s.channelY.back();
const double rangeY = y1 - y0;
const double rangeZ =
(s.samples > 1) ? (s.samples - 1) * s.dz : 0.0;
auto cells = [](double range, double cell) {
if (cell <= 0.0) return 1;
return static_cast<int>(std::ceil(range / cell)) + 1;
};
spec.ox = s.x0;
spec.oy = y0;
spec.oz = s.z0;
spec.dx = cellXY;
spec.dy = cellXY;
spec.dz = cellZ;
spec.nx = cells(rangeX, cellXY);
spec.ny = cells(rangeY, cellXY);
spec.nz = cells(rangeZ, cellZ);
spec.power = 2.0;
spec.maxDist = cellXY * 2.0;
return spec;
}
// 落盘 data.bin 体积(所有 data*.bin 之和,含金字塔各级)。
std::int64_t storeDataBytes(const std::string& dir) {
std::int64_t total = 0;
for (const auto& e : fs::directory_iterator(dir)) {
if (!e.is_regular_file()) continue;
const std::string name = e.path().filename().string();
if (name.rfind("data", 0) == 0 &&
e.path().extension().string() == ".bin") {
total += static_cast<std::int64_t>(e.file_size());
}
}
return total;
}
int cmdBuild(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
"[--cellZ 0.05] [--out <storeDir>] [--levels 2]\n";
return 2;
}
const std::string dir = a.positional[0];
const std::string line = a.get("line", "001");
const double cellXY = std::stod(a.get("cellXY", "0.2"));
const double cellZ = std::stod(a.get("cellZ", "0.05"));
const int levels = std::stoi(a.get("levels", "2"));
const std::string out =
a.get("out", (fs::temp_directory_path() / ("gpr_store_" + line)).string());
std::cout << "[build] dir=" << dir << " line=" << line
<< " cellXY=" << cellXY << " cellZ=" << cellZ
<< " levels=" << levels << " out=" << out << "\n";
const LineFiles lf = discoverLine(dir, line);
std::cout << "[build] 发现通道数=" << lf.iprb.size()
<< " ord=" << (lf.ord.empty() ? "(无)" : lf.ord) << "\n";
if (lf.iprb.size() != static_cast<std::size_t>(kChannels)) {
std::cerr << "[build] 警告: 通道数 != " << kChannels
<< "(仍按发现数继续)\n";
}
if (lf.iprb.empty() || lf.ord.empty()) {
std::cerr << "[build] 错误: 未发现 .iprb 或 .ord\n";
return 1;
}
// 1) 装配
Stopwatch swAsm;
geopro::core::GprSurvey survey =
geopro::io::gpr::assembleGprSurvey(lf.iprb, lf.ord);
const double asmMs = swAsm.elapsedMs();
std::cout << "[build] 装配完成 ntraces=" << survey.ntraces
<< " samples=" << survey.samples
<< " channels=" << survey.channelY.size()
<< " dx=" << survey.dx << " dz=" << survey.dz << "\n";
// 2) 建体
const geopro::core::GridSpec spec = specFromSurvey(survey, cellXY, cellZ);
std::cout << "[build] GridSpec nx=" << spec.nx << " ny=" << spec.ny
<< " nz=" << spec.nz << " dx=" << spec.dx << " dy=" << spec.dy
<< " dz=" << spec.dz << " maxDist=" << spec.maxDist << "\n";
Stopwatch swBuild;
geopro::core::BuiltI16 built = geopro::core::buildGprVolume(survey, spec);
const double buildMs = swBuild.elapsedMs();
const std::int64_t nx = built.vol.nx(), ny = built.vol.ny(), nz = built.vol.nz();
const std::int64_t voxels = nx * ny * nz;
const std::int64_t rawBytes = voxels * 2; // int16
// 3) 落盘 + 金字塔
fs::create_directories(out);
Stopwatch swWrite;
geopro::data::ChunkedVolumeStore::write(out, built);
const double writeMs = swWrite.elapsedMs();
Stopwatch swPyr;
{
geopro::data::ChunkedVolumeStore store(out);
store.buildPyramid(levels);
}
const double pyrMs = swPyr.elapsedMs();
const std::int64_t dataBytes = storeDataBytes(out);
const double ratio =
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
const double peak = Probe::peakMemMB();
std::cout << "\n=== build 指标 ===\n";
std::cout << "装配耗时(ms) : " << asmMs << "\n";
std::cout << "建体耗时(ms) : " << buildMs << "\n";
std::cout << "落盘耗时(ms) : " << writeMs << "\n";
std::cout << "金字塔耗时(ms) : " << pyrMs << "\n";
std::cout << "体维度 : " << nx << " x " << ny << " x " << nz << "\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 << "峰值内存(MB) : " << peak << "\n";
writeMetricLine(
"build,line=" + line + ",cellXY=" + std::to_string(cellXY) +
",cellZ=" + std::to_string(cellZ) + ",nx=" + std::to_string(nx) +
",ny=" + std::to_string(ny) + ",nz=" + std::to_string(nz) +
",voxels=" + std::to_string(voxels) +
",rawB=" + std::to_string(rawBytes) +
",dataB=" + std::to_string(dataBytes) +
",ratio=" + std::to_string(ratio) + ",asmMs=" + std::to_string(asmMs) +
",buildMs=" + std::to_string(buildMs) +
",writeMs=" + std::to_string(writeMs) +
",pyrMs=" + std::to_string(pyrMs) + ",peakMB=" + std::to_string(peak));
return 0;
}
int cmdLoad(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc load <storeDir>\n";
return 2;
}
const std::string dir = a.positional[0];
std::cout << "[load] storeDir=" << dir << "\n";
Stopwatch sw;
geopro::render::WholeVolumeSource src(dir);
const double loadMs = sw.elapsedMs();
const auto& m = src.meta();
const std::int64_t voxels =
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
const double peak = Probe::peakMemMB();
std::cout << "\n=== load 指标 ===\n";
std::cout << "加载耗时(ms) : " << loadMs << "\n";
std::cout << "整卷维度 : " << m.nx << " x " << m.ny << " x " << m.nz
<< "\n";
std::cout << "整卷字节(B) : " << wholeBytes << " ("
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
std::cout << "峰值内存(MB) : " << peak << "\n";
writeMetricLine("load,dir=" + dir + ",nx=" + std::to_string(m.nx) +
",ny=" + std::to_string(m.ny) +
",nz=" + std::to_string(m.nz) +
",wholeB=" + std::to_string(wholeBytes) +
",loadMs=" + std::to_string(loadMs) +
",peakMB=" + std::to_string(peak));
return 0;
}
// ---- selftest合成极小数据走完整 build→load 管线 ----
// 写一个极小通道的 .iprb + .iprhsamples 采样、traces 道,值 = base + t + s
void writeSyntheticChannel(const fs::path& iprbPath, int samples, int traces,
std::int16_t base) {
const fs::path iprhPath =
fs::path(iprbPath).replace_extension(".iprh");
std::ofstream h(iprhPath);
h << "SAMPLES: " << samples << "\n";
h << "LAST TRACE: " << (traces - 1) << "\n";
h << "CHANNELS: 2\n";
h << "TIMEWINDOW: 100.0\n";
h << "SOIL VELOCITY: 100.0\n"; // m/µs → ×1e6 → 1e8 m/s
h << "DISTANCE INTERVAL: 0.05\n";
h.close();
std::ofstream b(iprbPath, std::ios::binary);
// 布局 [trace*samples + s]s 最快。
for (int t = 0; t < traces; ++t) {
for (int s = 0; s < samples; ++s) {
const std::int16_t v =
static_cast<std::int16_t>(base + t + s);
b.write(reinterpret_cast<const char*>(&v), sizeof(v));
}
}
}
int cmdSelftest() {
std::cout << "[selftest] 构造极小合成 survey2 通道)...\n";
const fs::path tmp =
fs::temp_directory_path() / "gpr_poc_selftest";
std::error_code ec;
fs::remove_all(tmp, ec);
fs::create_directories(tmp);
const int samples = 8;
const int traces = 12;
// 2 通道 .iprb/.iprh + .ord末列==1 标记有效通道,第 2 列为横偏 Y
writeSyntheticChannel(tmp / "syn_001_A01.iprb", samples, traces,
/*base=*/100);
writeSyntheticChannel(tmp / "syn_001_A02.iprb", samples, traces,
/*base=*/200);
{
std::ofstream ord(tmp / "syn_001.ord");
ord << "0 0.000000 -1.5 1\n";
ord << "1 1.000000 -1.5 1\n";
}
const std::vector<std::string> iprb = {
(tmp / "syn_001_A01.iprb").string(),
(tmp / "syn_001_A02.iprb").string()};
const std::string ord = (tmp / "syn_001.ord").string();
bool ok = true;
auto check = [&](bool cond, const std::string& msg) {
if (!cond) {
std::cerr << "[selftest] FAIL: " << msg << "\n";
ok = false;
}
};
try {
// 装配
geopro::core::GprSurvey survey =
geopro::io::gpr::assembleGprSurvey(iprb, ord);
check(survey.ntraces == traces, "ntraces");
check(survey.samples == samples, "samples");
check(survey.channelY.size() == 2, "channels");
// channelY 升序A01 偏移 0.0 在前A02 偏移 1.0 在后。
check(survey.channelY.front() < survey.channelY.back(), "channelY 升序");
// 建体cellXY 取通道间距 1.0 → ny=2cellZ 较细确保 nz>1。
const double cellXY = 1.0;
const double cellZ = std::max(survey.dz, 1e-12);
const geopro::core::GridSpec spec =
specFromSurvey(survey, cellXY, cellZ);
std::cout << "[selftest] GridSpec " << spec.nx << "x" << spec.ny << "x"
<< spec.nz << " dz=" << spec.dz << "\n";
check(spec.ny == 2, "ny==2");
geopro::core::BuiltI16 built =
geopro::core::buildGprVolume(survey, spec);
check(built.vol.nx() == spec.nx, "built nx");
check(built.vol.ny() == spec.ny, "built ny");
check(built.vol.nz() == spec.nz, "built nz");
// 落盘 + 金字塔
const std::string store = (tmp / "store").string();
fs::create_directories(store);
geopro::data::ChunkedVolumeStore::write(store, built, /*brick=*/4);
{
geopro::data::ChunkedVolumeStore s(store);
s.buildPyramid(1);
check(s.levels() == 2, "金字塔层数==2");
}
// 加载整卷,校验维度一致
geopro::render::WholeVolumeSource src(store);
check(src.meta().nx == spec.nx, "load nx");
check(src.meta().ny == spec.ny, "load ny");
check(src.meta().nz == spec.nz, "load nz");
// 某体素值合理性x0/y0 角点应有非 blank 量化值(落格命中首道首通道)。
const std::int16_t q = built.vol.at(0, 0, 0);
check(q != geopro::core::ScalarVolumeI16::kBlank, "(0,0,0) 非 blank");
} catch (const std::exception& e) {
std::cerr << "[selftest] 异常: " << e.what() << "\n";
ok = false;
}
fs::remove_all(tmp, ec);
std::cout << "[selftest] " << (ok ? "PASS" : "FAIL") << "\n";
return ok ? 0 : 1;
}
// ============================================================================
// 离屏 GPU 渲染基准POC-B
// ============================================================================
// 捕获 VTK 错误输出的 OutputWindow用于侦测体绘制时 vtkVolumeTexture 报的
// "Invalid texture dimensions" / "MAX_3D_TEXTURE_SIZE" —— 一旦出现,说明整卷
// 单张 3D 纹理上传失败,体绘制 fps 无意义,必须如实标 INVALID绝不当真上报
class CapturingOutputWindow : public vtkOutputWindow {
public:
static CapturingOutputWindow* New();
vtkTypeMacro(CapturingOutputWindow, vtkOutputWindow);
void DisplayText(const char* txt) override {
if (txt) {
const std::string s(txt);
captured_ += s;
if (s.find("texture dimensions") != std::string::npos ||
s.find("MAX_3D_TEXTURE_SIZE") != std::string::npos) {
textureError_ = true;
}
}
// 仍透传到 stderr便于人工查看。
if (txt) std::cerr << txt;
}
bool textureError() const { return textureError_; }
const std::string& captured() const { return captured_; }
private:
std::string captured_;
bool textureError_ = false;
};
vtkStandardNewMacro(CapturingOutputWindow);
// 创建一个离屏 vtkRenderWindowVTK9.6SetShowWindow(false)+OffScreenRenderingOn
vtkSmartPointer<vtkRenderWindow> makeOffscreenWindow(int w, int h) {
auto rw = vtkSmartPointer<vtkRenderWindow>::New();
rw->SetOffScreenRendering(1);
rw->SetShowWindow(false);
rw->SetSize(w, h);
return rw;
}
// 闸门:最小离屏渲染冒烟。返回 0=OK非 0=离屏 GL 起不来BLOCKED_OFFSCREEN
// 流程:离屏窗口 → 加一个 cube actor → Render() → 读回像素,确认非全黑/读得到。
int cmdOffscreenSmoke() {
std::cout << "[offscreen-smoke] 创建离屏 vtkRenderWindow...\n";
try {
auto rw = makeOffscreenWindow(256, 256);
vtkNew<vtkRenderer> ren;
ren->SetBackground(0.1, 0.1, 0.2);
rw->AddRenderer(ren);
vtkNew<vtkCubeSource> cube;
cube->SetXLength(1.0);
cube->SetYLength(1.0);
cube->SetZLength(1.0);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(cube->GetOutputPort());
vtkNew<vtkActor> actor;
actor->SetMapper(mapper);
actor->GetProperty()->SetColor(1.0, 0.6, 0.2);
ren->AddActor(actor);
ren->ResetCamera();
// Render():若 GL 上下文创建失败VTK 会输出错误(多数返回,少数抛)。
rw->Render();
// 读回像素验证:取整窗 RGB确认能读到且非全 0。
const int* sz = rw->GetSize();
const int w = sz[0], h = sz[1];
if (w <= 0 || h <= 0) {
std::cout << "[offscreen-smoke] FAIL: 窗口尺寸为 0上下文未建立\n";
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
return 1;
}
auto pixels = vtkSmartPointer<vtkUnsignedCharArray>::New();
// GetRGBACharPixelData(x0,y0,x1,y1,front,arr)front=1 读前缓冲。
const int ok =
rw->GetRGBACharPixelData(0, 0, w - 1, h - 1, /*front=*/1, pixels);
if (ok == 0 || pixels->GetNumberOfTuples() == 0) {
std::cout << "[offscreen-smoke] FAIL: 读不到像素\n";
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
return 1;
}
// 统计非背景像素cube 应渲出橙色,存在像素 R 通道明显高于背景)。
vtkIdType nonBlack = 0;
const vtkIdType n = pixels->GetNumberOfTuples();
for (vtkIdType i = 0; i < n; ++i) {
const double r = pixels->GetComponent(i, 0);
const double g = pixels->GetComponent(i, 1);
const double b = pixels->GetComponent(i, 2);
if (r > 80 || g > 80 || b > 80) ++nonBlack;
}
const char* caps = rw->ReportCapabilities();
std::cout << "[offscreen-smoke] 读回像素 " << n << " 个,非背景像素 "
<< nonBlack << "\n";
std::cout << "[offscreen-smoke] GL 能力:\n"
<< (caps ? caps : "(无)") << "\n";
if (nonBlack == 0) {
std::cout << "[offscreen-smoke] FAIL: 渲染结果全为背景actor 未画出)\n";
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
return 1;
}
std::cout << "[offscreen-smoke] OK离屏 GL 可用,可继续真实基准。\n";
std::cout << "STATUS=OK\n";
return 0;
} catch (const std::exception& e) {
std::cout << "[offscreen-smoke] FAIL: 异常 " << e.what() << "\n";
std::cout << "STATUS=BLOCKED_OFFSCREEN\n";
return 1;
}
}
// 体绘制 fps每帧绕 azimuth 旋相机再 Render(),避免被驱动优化成空渲染。
double benchVolumeFps(vtkRenderWindow* rw, vtkRenderer* ren, int frames) {
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
rw->Render(); // 预热一帧(首帧含上传显存/编译 shader不计时
Stopwatch sw;
for (int f = 0; f < frames; ++f) {
cam->Azimuth(360.0 / frames); // 每帧转一点,扫满一圈
rw->Render();
}
const double ms = sw.elapsedMs();
return ms > 0.0 ? frames * 1000.0 / ms : 0.0;
}
// 切片扫描 fps沿 K 轴(深度)逐偏移 reslice 取轴向切面 + 纹理渲染,每帧推进偏移。
double benchSliceFps(vtkRenderWindow* rw, vtkRenderer* ren,
vtkImageData* full, vtkLookupTable* lut, int frames) {
// reslice固定轴向(XY 平面),沿 Z 改变 ResliceAxesOrigin 扫过整卷。
vtkNew<vtkImageReslice> reslice;
reslice->SetInputData(full);
reslice->SetOutputDimensionality(2);
reslice->SetInterpolationModeToLinear();
vtkNew<vtkImageMapToColors> colorize;
colorize->SetLookupTable(lut);
colorize->SetInputConnection(reslice->GetOutputPort());
vtkNew<vtkImageActor> imgActor;
imgActor->GetMapper()->SetInputConnection(colorize->GetOutputPort());
ren->AddViewProp(imgActor);
ren->ResetCamera();
double bounds[6];
full->GetBounds(bounds);
const double zMin = bounds[4], zMax = bounds[5];
const double ox = 0.5 * (bounds[0] + bounds[1]);
const double oy = 0.5 * (bounds[2] + bounds[3]);
rw->Render(); // 预热
Stopwatch sw;
for (int f = 0; f < frames; ++f) {
const double t = static_cast<double>(f) / std::max(1, frames - 1);
const double z = zMin + (zMax - zMin) * t;
reslice->SetResliceAxesOrigin(ox, oy, z);
reslice->Modified();
rw->Render();
}
const double ms = sw.elapsedMs();
return ms > 0.0 ? frames * 1000.0 / ms : 0.0;
}
// 由 ColorScale 物理区间建 256 级 VTK LUT切片纹理着色用与体绘制色阶同源
vtkSmartPointer<vtkLookupTable> makeLut(const geopro::core::ColorScale& cs,
double vmin, double vmax) {
auto lut = vtkSmartPointer<vtkLookupTable>::New();
const int n = 256;
lut->SetNumberOfTableValues(n);
lut->SetRange(vmin, vmax);
for (int i = 0; i < n; ++i) {
const double v = vmin + (vmax - vmin) * i / (n - 1);
const auto c = cs.colorAt(v);
lut->SetTableValue(i, c.r / 255.0, c.g / 255.0, c.b / 255.0, 1.0);
}
lut->Build();
return lut;
}
// 简单蓝-白-红色阶(与 test_color_scale 同款最简构造)。
geopro::core::ColorScale makeColorScale(double vmin, double vmax) {
geopro::core::ColorScale cs;
const double mid = 0.5 * (vmin + vmax);
cs.addStop(vmin, geopro::core::Rgba{0, 0, 255, 255});
cs.addStop(mid, geopro::core::Rgba{255, 255, 255, 255});
cs.addStop(vmax, geopro::core::Rgba{255, 0, 0, 255});
return cs;
}
int cmdRenderB(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc renderB <storeDir> [--frames 120]\n";
return 2;
}
const std::string dir = a.positional[0];
const int frames = std::stoi(a.get("frames", "120"));
std::cout << "[renderB] storeDir=" << dir << " frames=" << frames << "\n";
// 闸门复检renderB 前先确认离屏可用(避免在不可渲染机上跑出假数据)。
std::cout << "[renderB] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[renderB] 闸门失败,中止,不产出 fps。\n";
return 1;
}
// 1) 加载整卷VTK_SHORT
Stopwatch swLoad;
geopro::render::WholeVolumeSource src(dir);
const double loadMs = swLoad.elapsedMs();
const auto& m = src.meta();
const std::int64_t voxels =
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
std::cout << "[renderB] 整卷 " << m.nx << "x" << m.ny << "x" << m.nz
<< " 体素=" << voxels << " 字节=" << wholeBytes << " ("
<< wholeBytes / (1024.0 * 1024.0) << " MB),加载 " << loadMs
<< "ms\n";
auto images = src.currentImages();
if (images.empty() || !images.front()) {
std::cerr << "[renderB] 错误: currentImages 为空\n";
return 1;
}
vtkImageData* shortImg = images.front().Get();
// 色阶用 meta 的物理区间。
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
// 2) 体绘制(离屏)。
auto rw = makeOffscreenWindow(1024, 768);
vtkNew<vtkRenderer> ren;
ren->SetBackground(0.0, 0.0, 0.0);
rw->AddRenderer(ren);
vtkSmartPointer<vtkVolume> volume =
geopro::render::buildVoxelI16FromImage(shortImg, m.quant, cs, vmin, vmax);
ren->AddVolume(volume);
// 装上捕获式 OutputWindow拦截体绘制时的 3D 纹理维度错误。
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
std::cout << "[renderB] 体绘制基准(" << frames << " 帧旋转相机)...\n";
const double volFpsRaw = benchVolumeFps(rw, ren, frames);
const bool textureErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr); // 还原默认输出窗口
// 进显存判据SmartVolumeMapper 实际用的渲染模式2=GPURenderMode
int renderMode = -1;
bool lowResResample = false;
if (auto* svm =
vtkSmartVolumeMapper::SafeDownCast(volume->GetMapper())) {
renderMode = svm->GetLastUsedRenderMode();
// 大体可能触发降质重采样GPU 显存不足时 SmartVolumeMapper 走低分辨率)。
lowResResample = (svm->GetInteractiveAdjustSampleDistances() == 0 &&
renderMode != vtkSmartVolumeMapper::GPURenderMode);
}
const bool onGpu = (renderMode == vtkSmartVolumeMapper::GPURenderMode);
// 任一维度超过 GL_MAX_3D_TEXTURE_SIZE本机实测 16384→ 整卷无法成单张 3D 纹理。
constexpr int kMax3DTexObserved = 16384;
const bool dimOversize =
(m.nx > kMax3DTexObserved || m.ny > kMax3DTexObserved ||
m.nz > kMax3DTexObserved);
// 体绘制 fps 是否可信:上传成功(无纹理错误且未超限)才算真实整卷体绘制帧率。
const bool volFpsValid = !textureErr && !dimOversize;
const double volFps = volFpsValid ? volFpsRaw : -1.0;
std::cout << "[renderB] 体绘制 raw_fps=" << volFpsRaw
<< " 渲染模式=" << renderMode << (onGpu ? "(GPU)" : "(非GPU)")
<< " 纹理维度错误=" << (textureErr ? "" : "")
<< " 超 16384=" << (dimOversize ? "" : "") << "\n";
if (!volFpsValid) {
std::cout << "[renderB] 警告: 整卷未能成单张 3D 纹理X=" << m.nx
<< " > " << kMax3DTexObserved
<< "),体绘制 fps 无意义 → 标 INVALID。\n";
}
// 3) 切片扫描(离屏,沿 Z 扫整卷)。
vtkNew<vtkRenderer> ren2;
ren2->SetBackground(0.0, 0.0, 0.0);
auto rw2 = makeOffscreenWindow(1024, 768);
rw2->AddRenderer(ren2);
vtkSmartPointer<vtkLookupTable> lut = makeLut(cs, vmin, vmax);
std::cout << "[renderB] 切片扫描基准(" << frames << " 帧沿 Z 推进)...\n";
const double sliceFps =
benchSliceFps(rw2, ren2, src.sliceSource(), lut, frames);
std::cout << "[renderB] 切片 fps=" << sliceFps << "\n";
const double peak = Probe::peakMemMB();
const std::string vram = "N/A"; // VTK 安装未带 GLEW 头,无法直查 NVX 显存
// 4) 汇总打印。
const std::string volFpsStr =
volFpsValid ? std::to_string(volFps) : "INVALID(整卷超 3D 纹理上限)";
std::cout << "\n=== renderB GPU 指标 ===\n";
std::cout << "离屏闸门 : OK\n";
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
<< "\n";
std::cout << "体素数 : " << voxels << "\n";
std::cout << "整卷字节(B) : " << wholeBytes << " ("
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
std::cout << "体绘制 fps : " << volFpsStr << "\n";
if (!volFpsValid) {
std::cout << " (raw_fps=" << volFpsRaw
<< " 为空纹理渲染X=" << m.nx << " > 16384不可信)\n";
}
std::cout << "切片扫描 fps : " << sliceFps << " (2D 纹理,无 3D 上限约束)\n";
std::cout << "渲染模式 : " << renderMode
<< (onGpu ? " (GPU 路径)" : " (非 GPU)") << "\n";
std::cout << "整卷进显存 : "
<< (volFpsValid && onGpu ? "是(单张 3D 纹理)"
: "否(超 GL_MAX_3D_TEXTURE_SIZE 16384")
<< "\n";
std::cout << "降质重采样 : " << (lowResResample ? "" : "") << "\n";
std::cout << "GPU 显存 : " << vram << "\n";
std::cout << "进程峰值内存(MB): " << peak << "\n";
writeMetricLine(
"renderB,dir=" + dir + ",nx=" + std::to_string(m.nx) +
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
",voxels=" + std::to_string(voxels) +
",wholeB=" + std::to_string(wholeBytes) +
",volFps=" + volFpsStr +
",volFpsRaw=" + std::to_string(volFpsRaw) +
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
",sliceFps=" + std::to_string(sliceFps) +
",renderMode=" + std::to_string(renderMode) +
",onGpu=" + std::to_string(onGpu ? 1 : 0) +
",loadMs=" + std::to_string(loadMs) +
",peakMB=" + std::to_string(peak));
return 0;
}
// ============================================================================
// 核外分块体绘制基准POC-C命门探针
// ============================================================================
// 量化域传函(与 VoxelActor::buildVoxelI16FromImage 同逻辑):颜色对每量化级 qv 用
// q.toPhys(qv) 反查 ColorScale;不透明度 kBlank→0、[qmin,qmax] 线性到 kMaxOpacity。
// MultiBlock 全块共用同一 vtkVolumeProperty(挂在单个 vtkVolume 上)。
vtkSmartPointer<vtkVolumeProperty> makeI16VolumeProperty(
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
double vminPhys, double vmaxPhys) {
constexpr int kTransferSamples = 64;
constexpr double kMaxOpacity = 0.15;
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
const double qminD = static_cast<double>(q.toQ(vminPhys));
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
vtkNew<vtkColorTransferFunction> color;
for (int t = 0; t < kTransferSamples; ++t) {
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
const double phys = q.toPhys(qvLevel);
const auto c = cs.colorAt(phys);
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
}
vtkNew<vtkPiecewiseFunction> opacity;
opacity->AddPoint(
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
opacity->AddPoint(qminD, 0.0);
opacity->AddPoint(qmaxD, kMaxOpacity);
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
prop->SetColor(color);
prop->SetScalarOpacity(opacity);
prop->SetInterpolationTypeToLinear();
prop->ShadeOff();
return prop;
}
// 由当前工作集图像组装 vtkMultiBlockDataSet(每块一个 vtkImageData)。
vtkSmartPointer<vtkMultiBlockDataSet> makeMultiBlock(
const std::vector<vtkSmartPointer<vtkImageData>>& imgs) {
auto mb = vtkSmartPointer<vtkMultiBlockDataSet>::New();
mb->SetNumberOfBlocks(static_cast<unsigned int>(imgs.size()));
for (unsigned int i = 0; i < imgs.size(); ++i) {
mb->SetBlock(i, imgs[i].Get());
}
return mb;
}
int cmdRenderC(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n";
return 2;
}
const std::string dir = a.positional[0];
const std::size_t budget =
static_cast<std::size_t>(std::stoul(a.get("budget", "64")));
const int frames = std::stoi(a.get("frames", "120"));
std::cout << "[renderC] storeDir=" << dir << " budget=" << budget
<< " frames=" << frames << "\n";
// 闸门复检:不可渲染机不产假 fps。
std::cout << "[renderC] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[renderC] 闸门失败,中止,不产出 fps。\n";
return 1;
}
// 1) 核外源(读 meta + 建 pager,不载整卷)。
Stopwatch swLoad;
geopro::render::OutOfCoreSource src(dir, budget);
const double loadMs = swLoad.elapsedMs();
const auto& m = src.meta();
const std::int64_t voxels =
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
const int winW = 1024, winH = 768;
src.setAspect(static_cast<double>(winW) / winH);
std::cout << "[renderC] 体 " << m.nx << "x" << m.ny << "x" << m.nz
<< " 体素=" << voxels << " (整卷 X=" << m.nx
<< " > 16384 → renderB INVALID),源构造 " << loadMs << "ms\n";
// 色阶用 meta 物理区间。
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
vtkSmartPointer<vtkVolumeProperty> prop =
makeI16VolumeProperty(m.quant, cs, vmin, vmax);
// 2) 离屏 + MultiBlock 体绘制。
auto rw = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> ren;
ren->SetBackground(0.0, 0.0, 0.0);
rw->AddRenderer(ren);
vtkNew<vtkMultiBlockVolumeMapper> mapper;
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
ren->AddVolume(volume);
// 装捕获式 OutputWindow:拦截每块上传时的 3D 纹理维度错误(应无,因块 ≤64³)。
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
// 相机:先以全体定向(看整卷),首帧 update 选出工作集后再 ResetCamera 到
// 实际驻留块的 mapper 包围盒(budget<视野总块时工作集只覆盖体的一部分,框住它
// 才能确证核外体绘制真渲出;这是 budget 受限下的诚实测法,报告说明)。
ren->ResetCamera(m.origin[0], m.origin[0] + m.nx * m.spacing[0],
m.origin[1], m.origin[1] + m.ny * m.spacing[1],
m.origin[2], m.origin[2] + m.nz * m.spacing[2]);
vtkCamera* cam = ren->GetActiveCamera();
auto refreshBlocks = [&]() {
src.update(cam);
auto imgs = src.currentImages();
auto mb = makeMultiBlock(imgs);
mapper->SetInputDataObject(mb);
mapper->Update();
return imgs.size();
};
const std::size_t warmBlocks = refreshBlocks();
// 用工作集(mapper)实际包围盒重置相机,框住驻留块。
{
double b[6];
mapper->GetBounds(b);
if (b[0] <= b[1]) {
ren->ResetCamera(b);
} else {
ren->ResetCamera();
}
}
rw->Render(); // 预热(上传显存 + 编译 shader,不计时)
{
double b[6];
mapper->GetBounds(b);
std::cout << "[renderC] 工作集包围盒 x[" << b[0] << "," << b[1] << "] y["
<< b[2] << "," << b[3] << "] z[" << b[4] << "," << b[5] << "]\n";
}
std::cout << "[renderC] 预热:level=" << src.lastLevel()
<< " 视野块=" << src.lastVisibleCount() << "/"
<< src.lastLevelBrickTotal()
<< " 驻留=" << src.residentCount() << " 渲染块=" << warmBlocks
<< "\n";
std::size_t maxResident = src.residentCount();
std::size_t sumBlocks = 0;
// 3a) 静态工作集体绘制 fps:工作集固定(不每帧换块),只旋相机 + Render。
// 隔离"纯 GPU MultiBlock 体绘制"成本(剔除分块换页/解压/重建 mapper 开销),
// 直接对照 renderB 整卷 fps,回答未知 #6(真实体绘制 fps)。
std::cout << "[renderC] 静态工作集体绘制基准(" << frames << " 帧旋相机)...\n";
Stopwatch swStatic;
for (int f = 0; f < frames; ++f) {
cam->Azimuth(360.0 / frames);
rw->Render(); // 工作集不变,仅旋转
}
const double staticMs = swStatic.elapsedMs();
const double staticFps = staticMs > 0 ? frames * 1000.0 / staticMs : 0.0;
std::cout << "[renderC] 静态工作集 fps=" << staticFps << "\n";
// 3b) 动态换页体绘制 fps:每帧 update(cam)(重选 LOD/视野块,含 qUncompress 解压
// 换入的块 + 重建 MultiBlock)+ Render。回答未知 #4(热路径解压是否拖垮 fps)
// 与 #5(内存恒定)。同时累计 update 耗时占比。
std::cout << "[renderC] 动态换页体绘制基准(" << frames << " 帧旋相机)...\n";
double updateMsTotal = 0.0;
Stopwatch swDyn;
for (int f = 0; f < frames; ++f) {
cam->Azimuth(360.0 / frames);
Stopwatch swU;
const std::size_t blocks = refreshBlocks(); // update + 重建 MultiBlock
updateMsTotal += swU.elapsedMs();
sumBlocks += blocks;
maxResident = std::max(maxResident, src.residentCount());
rw->Render();
}
const double dynMs = swDyn.elapsedMs();
const double dynFps = dynMs > 0 ? frames * 1000.0 / dynMs : 0.0;
const double rawFps = dynFps; // 主报告口径:含换页的真实交互 fps
std::cout << "[renderC] 动态换页 fps=" << dynFps
<< " (其中 update/换页/重建 平均 " << (updateMsTotal / frames)
<< " ms/帧)\n";
const bool textureErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
// 4) 正确性判据:渲出非空像素(非全背景)。
auto pixels = vtkSmartPointer<vtkUnsignedCharArray>::New();
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, pixels);
vtkIdType nonBlack = 0;
const vtkIdType npx = pixels->GetNumberOfTuples();
for (vtkIdType i = 0; i < npx; ++i) {
if (pixels->GetComponent(i, 0) > 10 || pixels->GetComponent(i, 1) > 10 ||
pixels->GetComponent(i, 2) > 10) {
++nonBlack;
}
}
const bool renderedNonEmpty = (nonBlack > 0);
// 渲染模式(MultiBlock 内部每块一个 SmartVolumeMapper;此处取一块代表性查询)。
// MultiBlock 不直接暴露 LastUsedRenderMode,故以纹理无错 + 非空像素为体绘制真出证据。
const bool volFpsValid = !textureErr && renderedNonEmpty;
const double peak = Probe::peakMemMB();
const double avgBlocks =
frames > 0 ? static_cast<double>(sumBlocks) / frames : 0.0;
std::cout << "\n=== renderC 核外体绘制指标 ===\n";
std::cout << "离屏闸门 : OK\n";
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
<< " (整卷 X 超 16384,renderB=INVALID)\n";
std::cout << "体素数 : " << voxels << "\n";
std::cout << "budget(块) : " << budget << "\n";
std::cout << "峰值驻留(块) : " << maxResident
<< (maxResident <= budget ? " (≤budget,内存恒定 OK)"
: " (!! 超 budget)")
<< "\n";
std::cout << "末帧 level : " << src.lastLevel() << "\n";
std::cout << "末帧视野块/总块 : " << src.lastVisibleCount() << " / "
<< src.lastLevelBrickTotal() << "\n";
std::cout << "平均渲染块/帧 : " << avgBlocks << "\n";
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "") << "\n";
std::cout << "渲出非空像素 : " << (renderedNonEmpty ? "" : "否(!!)")
<< " (非背景像素=" << nonBlack << ")\n";
std::cout << "静态工作集 fps : "
<< (volFpsValid ? std::to_string(staticFps)
: std::string("INVALID(纹理错或空渲染)"))
<< " (纯 GPU MultiBlock 体绘制)\n";
std::cout << "动态换页 fps : "
<< (volFpsValid ? std::to_string(dynFps)
: std::string("INVALID(纹理错或空渲染)"))
<< " (含每帧 update/解压/重建 mapper)\n";
std::cout << " 换页均耗时/帧 : " << (updateMsTotal / frames) << " ms\n";
std::cout << "进程峰值内存(MB) : " << peak << "\n";
std::cout << "源构造耗时(ms) : " << loadMs << "\n";
std::cout << "对照 renderB : 整卷 INVALID(超 3D 纹理上限);renderC "
<< (volFpsValid ? "真渲出 ✔" : "未渲出 ✘") << "\n";
writeMetricLine(
"renderC,dir=" + dir + ",nx=" + std::to_string(m.nx) +
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
",voxels=" + std::to_string(voxels) +
",budget=" + std::to_string(budget) +
",maxResident=" + std::to_string(maxResident) +
",lastLevel=" + std::to_string(src.lastLevel()) +
",lastVisible=" + std::to_string(src.lastVisibleCount()) +
",lastLevelTotal=" + std::to_string(src.lastLevelBrickTotal()) +
",avgBlocks=" + std::to_string(avgBlocks) +
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
",nonBlack=" + std::to_string(nonBlack) +
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
",staticFps=" + (volFpsValid ? std::to_string(staticFps) : "INVALID") +
",dynFps=" + (volFpsValid ? std::to_string(dynFps) : "INVALID") +
",updateMsPerFrame=" + std::to_string(updateMsTotal / frames) +
",rawFps=" + std::to_string(rawFps) +
",loadMs=" + std::to_string(loadMs) +
",peakMB=" + std::to_string(peak));
return volFpsValid ? 0 : 1;
}
// ============================================================================
// 单 mapper SetPartitions 整卷体绘制基准POC-C-partitioned,去风险探针)
// ============================================================================
//
// 验"对的架构":整卷喂【单个】vtkGPUVolumeRayCastMapper(其 OpenGL 实现 =
// vtkOpenGLGPUVolumeRayCastMapper),用 SetPartitions(ceil(nx/16384),...) 让同一
// mapper 内部把体沿轴分区上传(每区 ≤16384 绕过 GL_MAX_3D_TEXTURE_SIZE),一次
// ray cast。对照 9c 整卷单 SmartVolumeMapper(INVALID,纹理墙) 与 12 MultiBlock
// (每块一 mapper,9.5 静态/1.45 换页)。
//
// 双闸(同 9c,绝不把空纹理假帧率当性能):
// ① CapturingOutputWindow 捕获 3D 纹理维度错误;
// ② 真实回读像素,统计非背景像素 → 非空才算真渲出。
int cmdRenderCPartitioned(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr
<< "用法: gpr_poc renderC-partitioned <storeDir> [--frames 120]\n";
return 2;
}
const std::string dir = a.positional[0];
const int frames = std::stoi(a.get("frames", "120"));
std::cout << "[renderC-partitioned] storeDir=" << dir << " frames=" << frames
<< "\n";
// 闸门复检:不可渲染机不产假 fps。
std::cout << "[renderC-partitioned] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[renderC-partitioned] 闸门失败,中止,不产出 fps。\n";
return 1;
}
// 1) WholeVolumeSource 重组整卷 VTK_SHORT image(常驻内存,约 400MB)。
Stopwatch swLoad;
geopro::render::WholeVolumeSource src(dir);
const double loadMs = swLoad.elapsedMs();
const auto& m = src.meta();
const std::int64_t voxels =
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
std::cout << "[renderC-partitioned] 整卷 " << m.nx << "x" << m.ny << "x"
<< m.nz << " 体素=" << voxels << " 字节=" << wholeBytes << " ("
<< wholeBytes / (1024.0 * 1024.0) << " MB),加载 " << loadMs
<< "ms\n";
auto images = src.currentImages();
if (images.empty() || !images.front()) {
std::cerr << "[renderC-partitioned] 错误: currentImages 为空\n";
return 1;
}
vtkImageData* shortImg = images.front().Get();
// 2) 分区数:任一轴 > 16384 → ceil(dim/16384) 个分区,其余轴 1。
constexpr int kMax3DTex = 16384;
auto partCount = [](int dim) {
return static_cast<unsigned short>((dim + kMax3DTex - 1) / kMax3DTex);
};
const unsigned short px = partCount(m.nx);
const unsigned short py = partCount(m.ny);
const unsigned short pz = partCount(m.nz);
std::cout << "[renderC-partitioned] SetPartitions(" << px << "," << py << ","
<< pz << ") 每区上限 ≤" << kMax3DTex << " (沿线 " << m.nx << "/"
<< px << "=" << (m.nx + px - 1) / px << ")\n";
// 3) 量化域传函(复用现有 makeI16VolumeProperty:qmin/qmax + kBlank 透明)。
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
vtkSmartPointer<vtkVolumeProperty> prop =
makeI16VolumeProperty(m.quant, cs, vmin, vmax);
// 4) 离屏 + 单个 GPU ray cast mapper + SetPartitions。
const int winW = 1024, winH = 768;
auto rw = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> ren;
ren->SetBackground(0.0, 0.0, 0.0);
rw->AddRenderer(ren);
// vtkGPUVolumeRayCastMapper 抽象基类无 SetPartitions(在 OpenGL 实现上);
// 直接建 OpenGL 具体类(工厂默认产物同此),喂【整卷单 image】不预切块。
vtkNew<vtkOpenGLGPUVolumeRayCastMapper> mapper;
mapper->SetInputData(shortImg);
mapper->SetPartitions(px, py, pz);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
ren->AddVolume(volume);
// 装捕获式 OutputWindow:拦截分区上传时的 3D 纹理维度错误。
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
// 相机:用 mapper 实际包围盒定向(整卷,非工作集);体极扁长(44476:29:162),
// ResetCamera 全体后再倾斜抬高视角,让薄维度可见(否则边缘视角近乎不可见)。
{
double b[6];
mapper->GetBounds(b);
if (b[0] <= b[1]) {
ren->ResetCamera(b);
} else {
ren->ResetCamera();
}
}
vtkCamera* cam = ren->GetActiveCamera();
cam->Elevation(30.0); // 抬高,避免纯边缘视角看不到薄板
cam->Azimuth(30.0);
ren->ResetCameraClippingRange();
// 每帧旋相机 + Render 测 fps;同时多帧采样非背景像素取最大值
// (区分"真渲不出"与"末帧恰好边缘视角空"——后者只是采样时机)。
auto countNonBlack = [&]() -> vtkIdType {
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
vtkIdType nb = 0;
const vtkIdType np = px->GetNumberOfTuples();
for (vtkIdType i = 0; i < np; ++i) {
if (px->GetComponent(i, 0) > 10 || px->GetComponent(i, 1) > 10 ||
px->GetComponent(i, 2) > 10) {
++nb;
}
}
return nb;
};
std::cout << "[renderC-partitioned] 单 mapper 整卷体绘制基准(" << frames
<< " 帧旋相机)...\n";
rw->Render(); // 预热(分区上传 + 编译 shader,不计时)
vtkIdType maxNonBlack = countNonBlack();
const int sampleEvery = std::max(1, frames / 8);
Stopwatch swBench;
for (int f = 0; f < frames; ++f) {
cam->Azimuth(360.0 / frames);
rw->Render();
if (f % sampleEvery == 0) {
maxNonBlack = std::max(maxNonBlack, countNonBlack());
}
}
const double benchMs = swBench.elapsedMs();
const double volFpsRaw =
benchMs > 0.0 ? frames * 1000.0 / benchMs : 0.0;
const bool textureErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
// 5) 正确性判据:整个旋转扫描中的最大非背景像素(非空才算真渲出)。
const vtkIdType nonBlack = maxNonBlack;
const bool renderedNonEmpty = (nonBlack > 0);
// 双闸:无纹理错 + 非空像素 → fps 可信。
const bool volFpsValid = !textureErr && renderedNonEmpty;
const double volFps = volFpsValid ? volFpsRaw : -1.0;
const double peak = Probe::peakMemMB();
const bool interactive = volFpsValid && volFps >= 15.0;
const std::string volFpsStr =
volFpsValid ? std::to_string(volFps)
: std::string("INVALID(纹理错或空渲染)");
std::cout << "\n=== renderC-partitioned 单 mapper SetPartitions 指标 ===\n";
std::cout << "离屏闸门 : OK\n";
std::cout << "体维度 : " << m.nx << " x " << m.ny << " x " << m.nz
<< "\n";
std::cout << "体素数 : " << voxels << "\n";
std::cout << "整卷字节(B) : " << wholeBytes << " ("
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
std::cout << "分区数(px,py,pz) : " << px << "," << py << "," << pz << "\n";
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "") << "\n";
std::cout << "渲出非空像素 : " << (renderedNonEmpty ? "" : "否(!!)")
<< " (非背景像素=" << nonBlack << ")\n";
std::cout << "体绘制 fps : " << volFpsStr << "\n";
if (!volFpsValid) {
std::cout << " (raw_fps=" << volFpsRaw << " 不可信)\n";
}
std::cout << "达交互级(≥15fps) : "
<< (interactive ? "是 ✔" : "否 ✘") << "\n";
std::cout << "进程峰值内存(MB) : " << peak << "\n";
std::cout << "源构造耗时(ms) : " << loadMs << "\n";
std::cout << "对照 renderB : 整卷单 SmartVolumeMapper=INVALID(纹理墙);"
"renderC MultiBlock=9.5 静态/1.45 换页;本探针="
<< (volFpsValid ? volFpsStr + "fps" : "INVALID") << "\n";
writeMetricLine(
"renderC-partitioned,dir=" + dir + ",nx=" + std::to_string(m.nx) +
",ny=" + std::to_string(m.ny) + ",nz=" + std::to_string(m.nz) +
",voxels=" + std::to_string(voxels) +
",wholeB=" + std::to_string(wholeBytes) +
",px=" + std::to_string(px) + ",py=" + std::to_string(py) +
",pz=" + std::to_string(pz) +
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
",nonBlack=" + std::to_string(nonBlack) +
",volFpsValid=" + std::to_string(volFpsValid ? 1 : 0) +
",volFps=" + volFpsStr + ",volFpsRaw=" + std::to_string(volFpsRaw) +
",interactive=" + std::to_string(interactive ? 1 : 0) +
",loadMs=" + std::to_string(loadMs) + ",peakMB=" + std::to_string(peak));
// 写报告文件(覆盖式,含对照表)。
{
const fs::path repo =
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
fs::create_directories(repo.parent_path());
std::ofstream rf(repo.string());
if (rf) {
rf << "# POC-C 单 mapper SetPartitions 整卷体绘制探针结果\n\n";
rf << "## 体\n";
rf << "- 维度: " << m.nx << " x " << m.ny << " x " << m.nz << " (体素 "
<< voxels << ")\n";
rf << "- 整卷字节: " << wholeBytes << " B ("
<< wholeBytes / (1024.0 * 1024.0) << " MB, VTK_SHORT)\n";
rf << "- store: " << dir << "\n\n";
rf << "## 单 mapper SetPartitions\n";
rf << "- mapper: vtkOpenGLGPUVolumeRayCastMapper (整卷单 image,不预切块)\n";
rf << "- 分区数: SetPartitions(" << px << ", " << py << ", " << pz
<< ") 每区上限 ≤" << kMax3DTex << "\n";
rf << "- 纹理维度错误: " << (textureErr ? "" : "") << "\n";
rf << "- 渲出非空像素: " << (renderedNonEmpty ? "" : "") << " (非背景像素 "
<< nonBlack << ")\n";
rf << "- 体绘制 fps: " << volFpsStr << "\n";
rf << "- 达交互级(≥15fps): " << (interactive ? "" : "") << "\n";
rf << "- 进程峰值内存: " << peak << " MB\n";
rf << "- 源构造耗时: " << loadMs << " ms\n\n";
rf << "## 对照表\n\n";
rf << "| 路径 | 是否渲出 | fps |\n";
rf << "|---|---|---|\n";
rf << "| renderB 整卷单 SmartVolumeMapper | INVALID(纹理墙) | — |\n";
rf << "| renderC MultiBlock(每块一 mapper) | 渲出 | 9.5 静态/1.45 换页 |\n";
rf << "| renderC-partitioned 单 mapper SetPartitions | "
<< (volFpsValid ? "渲出" : "未渲出") << " | "
<< (volFpsValid ? volFpsStr : std::string("INVALID")) << " |\n\n";
rf << "## 判据结论\n";
if (volFpsValid && interactive) {
rf << "单 mapper SetPartitions 整卷体绘制【真渲出且达交互级】(" << volFps
<< " fps ≥15)。C production 路线钉死可行。\n";
} else if (volFpsValid) {
rf << "单 mapper SetPartitions 整卷体绘制【真渲出但未达交互级】(" << volFps
<< " fps <15)。VTK 这条路天花板暴露,需评估 OpenVDS/自建 GL。\n";
} else {
rf << "单 mapper SetPartitions 整卷体绘制【未真渲出】(纹理错="
<< (textureErr ? "" : "") << ",非空像素="
<< (renderedNonEmpty ? "" : "")
<< ")。SetPartitions 未能绕过纹理墙,如实记录。\n";
}
std::cout << "[renderC-partitioned] 报告写入 " << repo.string() << "\n";
}
}
return volFpsValid ? 0 : 1;
}
void usage() {
std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n"
" gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
"[--cellZ 0.05] [--out <storeDir>] [--levels 2]\n"
" gpr_poc load <storeDir>\n"
" gpr_poc selftest\n"
" gpr_poc offscreen-smoke\n"
" gpr_poc renderB <storeDir> [--frames 120]\n"
" gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n"
" gpr_poc renderC-partitioned <storeDir> [--frames 120]\n";
}
} // namespace
int main(int argc, char** argv) {
if (argc < 2) {
usage();
return 2;
}
const std::string cmd = argv[1];
try {
if (cmd == "build") return cmdBuild(argc, argv);
if (cmd == "load") return cmdLoad(argc, argv);
if (cmd == "selftest") return cmdSelftest();
if (cmd == "offscreen-smoke") return cmdOffscreenSmoke();
if (cmd == "renderB") return cmdRenderB(argc, argv);
if (cmd == "renderC") return cmdRenderC(argc, argv);
if (cmd == "renderC-partitioned")
return cmdRenderCPartitioned(argc, argv);
} catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << "\n";
return 1;
}
usage();
return 2;
}