geopro/tools/gpr_poc/main.cpp

2528 lines
106 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 <vtkPNGWriter.h>
#include <vtkPointData.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkInteractorStyleTrackballCamera.h>
#include <vtkCallbackCommand.h>
#include <vtkTextActor.h>
#include <vtkTextProperty.h>
#include <vtkRenderer.h>
#include <vtkShortArray.h>
#include <vtkSmartPointer.h>
#include <vtkSmartVolumeMapper.h>
#include <vtkUnsignedCharArray.h>
#include <vtkVolume.h>
#include <vtkWindowToImageFilter.h>
#ifdef _WIN32
#include <windows.h> // SetConsoleOutputCP修中文控制台 GBK 乱码)
#endif
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;
}
// ============================================================================
// 视觉调优共享构件Task 12d ①)
// ============================================================================
//
// 结构化配色:地震/雷达体常用的「结构色阶」——深蓝(强负)→青→白(零)→黄→红(强正)
// 比单纯蓝-白-红更易拉开正负反射层次。值域用数据 vmin/vmax无需手调控制点。
geopro::core::ColorScale makeStructuralColorScale(double vmin, double vmax) {
geopro::core::ColorScale cs;
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
auto at = [&](double t) { return vmin + span * t; };
cs.addStop(at(0.00), geopro::core::Rgba{0, 0, 140, 255}); // 深蓝
cs.addStop(at(0.25), geopro::core::Rgba{0, 160, 220, 255}); // 青
cs.addStop(at(0.50), geopro::core::Rgba{245, 245, 245, 255}); // 白(零附近)
cs.addStop(at(0.75), geopro::core::Rgba{250, 190, 30, 255}); // 黄
cs.addStop(at(1.00), geopro::core::Rgba{170, 0, 0, 255}); // 暗红
return cs;
}
// 参数化量化域传函:与 makeI16VolumeProperty 同逻辑,但 kMaxOpacity 可由 --opacity 控。
// 不透明度调高时光线提前终止fps 近乎中性甚至更快(探针认知,报告打印前后对照证实)。
vtkSmartPointer<vtkVolumeProperty> makeTunedVolumeProperty(
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
double vminPhys, double vmaxPhys, double maxOpacity,
bool structuralOpacity = true) {
constexpr int kTransferSamples = 64;
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);
}
// 不透明度:
// - 原始(structuralOpacity=false):线性单斜坡 [qmin,qmax]→[0,maxOpacity]
// 与 VoxelActor 默认一致,作调优前对照基线。
// - 调优(structuralOpacity=true)双端斜坡。GPR/地震体值多集中在零附近(背景)
// 强反射在正负两端;线性单斜坡会让占多数的近零背景填满体、遮住结构。改为
// 「中段(零附近)透明 + 正负两端不透明」——抑制背景、凸显强反射层,截面结构才看得出。
vtkNew<vtkPiecewiseFunction> opacity;
opacity->AddPoint(
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
if (structuralOpacity) {
const double qmid = 0.5 * (qminD + qmaxD);
const double half = 0.5 * (qmaxD - qminD);
opacity->AddPoint(qminD, maxOpacity); // 强负反射:不透明
opacity->AddPoint(qmid - 0.30 * half, 0.0); // 近零背景:透明
opacity->AddPoint(qmid + 0.30 * half, 0.0);
opacity->AddPoint(qmaxD, maxOpacity); // 强正反射:不透明
} else {
opacity->AddPoint(qminD, 0.0);
opacity->AddPoint(qmaxD, maxOpacity);
}
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
prop->SetColor(color);
prop->SetScalarOpacity(opacity);
prop->SetInterpolationTypeToLinear();
prop->ShadeOff();
return prop;
}
// 由预构建 VTK_SHORT 图像建一个「视觉调优」体:自定义不透明度 + 垂向夸张。
// 垂向夸张用 vtkVolume::SetScale(1, exagg, exagg) 缩放跨通道(Y)与深度(Z)两薄轴,
// 不改图像数据;体物理极扁(X≈2.2km vs Y≈1.5m/Z≈8m),放大薄轴截面结构才看得出。
vtkSmartPointer<vtkVolume> buildTunedVolume(vtkImageData* shortImg,
const geopro::core::Quant& q,
const geopro::core::ColorScale& cs,
double vminPhys, double vmaxPhys,
double maxOpacity, double exagg,
bool structuralOpacity = true) {
vtkNew<vtkSmartVolumeMapper> mapper;
mapper->SetInputData(shortImg);
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
mapper->SetAutoAdjustSampleDistances(0);
mapper->SetInteractiveAdjustSampleDistances(0);
auto prop = makeTunedVolumeProperty(q, cs, vminPhys, vmaxPhys, maxOpacity,
structuralOpacity);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
volume->SetScale(1.0, exagg, exagg); // 垂向夸张:放大 Y/Z 薄轴
return volume;
}
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;
}
// ============================================================================
// LOD-fps 探针POC-C 最后一根链子Task 12c
// ============================================================================
//
// 12b 已证整卷全分辨率 ray cast(2.08 亿体素)~10fps 是硬上限fps 杠杆只有 LOD
// (渲更少体素)。本探针在【真实金字塔 store】上验四件事全离屏、双闸防假帧率
// (a) 粗层概览 fpslevel2 整卷(单轴 <16384 → 单 SmartVolumeMapper
// (b) 全分辨率局部 fpslevel0 一段 brick 列(沿线局部)。
// (c) LOD 切换动态过渡:相机从远观(level2)逐步拉近到近观局部(level0),跨越
// LOD 切换那一下逐帧记帧耗时,标切换帧尖峰/stall。
// (d) 截图lod-overview.png / lod-fullres-local.png / lod-transition-mid.png。
//
// 双闸(同 9c绝不把空纹理假帧率当性能
// ① CapturingOutputWindow 捕获 3D 纹理维度错误;
// ② 真实回读像素,统计非背景像素 → 非空才算真渲出。
// 把金字塔某 level 重组成整卷 VTK_SHORT vtkImageData逻辑同 WholeVolumeSource
// 但按 level 维度 + spacing×2^level使物理范围与 level0 一致)。
vtkSmartPointer<vtkImageData> buildLevelImage(
const geopro::data::ChunkedVolumeStore& store, int level,
const geopro::data::StoreMeta& m) {
int nx = 0, ny = 0, nz = 0;
store.dims(level, nx, ny, nz);
const int brick = m.brick;
const double sc = static_cast<double>(1 << level); // 2^level
auto img = vtkSmartPointer<vtkImageData>::New();
img->SetDimensions(nx, ny, nz);
img->SetOrigin(m.origin[0], m.origin[1], m.origin[2]);
img->SetSpacing(m.spacing[0] * sc, m.spacing[1] * sc, m.spacing[2] * sc);
vtkNew<vtkShortArray> arr;
arr->SetName("v");
arr->SetNumberOfTuples(static_cast<vtkIdType>(nx) * ny * nz);
for (int bz = 0; bz < store.bricksZ(level); ++bz) {
for (int by = 0; by < store.bricksY(level); ++by) {
for (int bx = 0; bx < store.bricksX(level); ++bx) {
const std::vector<std::int16_t> raw = store.readBrick(level, bx, by, bz);
const int i0 = bx * brick, j0 = by * brick, k0 = bz * brick;
const int bw = (nx - i0 < brick) ? (nx - i0) : brick;
const int bh = (ny - j0 < brick) ? (ny - j0) : brick;
const int bd = (nz - k0 < brick) ? (nz - k0) : brick;
std::size_t w = 0;
for (int kk = 0; kk < bd; ++kk) {
const vtkIdType gk = static_cast<vtkIdType>(k0 + kk);
for (int jj = 0; jj < bh; ++jj) {
const vtkIdType gj = static_cast<vtkIdType>(j0 + jj);
vtkIdType id = (gk * ny + gj) * nx + i0;
for (int ii = 0; ii < bw; ++ii) arr->SetValue(id++, raw[w++]);
}
}
}
}
}
img->GetPointData()->SetScalars(arr);
return img;
}
// 取 level0 一段 brick 列 [bx0, bx0+bxCount) × 全 Y × 全 Z 重组成局部整卷
// VTK_SHORT imageX 维 = bxCount*brick ≤ 几百,远 <16384单 3D 纹理)。
// Origin 沿 X 偏移到该段起点spacing 用 level0 原值。
vtkSmartPointer<vtkImageData> buildLocalLevel0Image(
const geopro::data::ChunkedVolumeStore& store,
const geopro::data::StoreMeta& m, int bx0, int bxCount) {
const int brick = m.brick;
const int nx0 = m.nx, ny0 = m.ny, nz0 = m.nz;
const int totBx = store.bricksX(0);
bx0 = std::max(0, std::min(bx0, totBx - 1));
bxCount = std::max(1, std::min(bxCount, totBx - bx0));
const int i0Global = bx0 * brick;
const int localNx = std::min(bxCount * brick, nx0 - i0Global);
auto img = vtkSmartPointer<vtkImageData>::New();
img->SetDimensions(localNx, ny0, nz0);
img->SetOrigin(m.origin[0] + i0Global * m.spacing[0], m.origin[1],
m.origin[2]);
img->SetSpacing(m.spacing[0], m.spacing[1], m.spacing[2]);
vtkNew<vtkShortArray> arr;
arr->SetName("v");
arr->SetNumberOfTuples(static_cast<vtkIdType>(localNx) * ny0 * nz0);
for (int bz = 0; bz < store.bricksZ(0); ++bz) {
for (int by = 0; by < store.bricksY(0); ++by) {
for (int bx = bx0; bx < bx0 + bxCount; ++bx) {
const std::vector<std::int16_t> raw = store.readBrick(0, bx, by, bz);
const int gi0 = bx * brick, j0 = by * brick, k0 = bz * brick;
const int li0 = gi0 - i0Global; // 局部 X 起点
const int bw = (nx0 - gi0 < brick) ? (nx0 - gi0) : brick;
const int bh = (ny0 - j0 < brick) ? (ny0 - j0) : brick;
const int bd = (nz0 - k0 < brick) ? (nz0 - k0) : brick;
std::size_t w = 0;
for (int kk = 0; kk < bd; ++kk) {
const vtkIdType gk = static_cast<vtkIdType>(k0 + kk);
for (int jj = 0; jj < bh; ++jj) {
const vtkIdType gj = static_cast<vtkIdType>(j0 + jj);
vtkIdType id = (gk * ny0 + gj) * localNx + li0;
for (int ii = 0; ii < bw; ++ii) arr->SetValue(id++, raw[w++]);
}
}
}
}
}
img->GetPointData()->SetScalars(arr);
return img;
}
// 统计当前窗口前缓冲非背景像素(>10 任一通道)。
vtkIdType countNonBlackPixels(vtkRenderWindow* rw, int w, int h) {
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
rw->GetRGBACharPixelData(0, 0, w - 1, h - 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;
}
// 离屏窗口截图 → PNG。
void savePng(vtkRenderWindow* rw, const std::string& path) {
rw->Render();
vtkNew<vtkWindowToImageFilter> w2i;
w2i->SetInput(rw);
w2i->SetInputBufferTypeToRGB();
w2i->ReadFrontBufferOff();
w2i->Update();
vtkNew<vtkPNGWriter> writer;
writer->SetFileName(path.c_str());
writer->SetInputConnection(w2i->GetOutputPort());
writer->Write();
}
int cmdRenderLOD(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc renderLOD <storeDir> [--frames 120]\n";
return 2;
}
const std::string dir = a.positional[0];
const int frames = std::stoi(a.get("frames", "120"));
std::cout << "[renderLOD] storeDir=" << dir << " frames=" << frames << "\n";
// 闸门复检:不可渲染机不产假 fps。
std::cout << "[renderLOD] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[renderLOD] 闸门失败,中止,不产出 fps。\n";
return 1;
}
geopro::data::ChunkedVolumeStore store(dir);
const geopro::data::StoreMeta& m = store.meta();
const int totLevels = store.levels();
std::cout << "[renderLOD] level0=" << m.nx << "x" << m.ny << "x" << m.nz
<< " 总层数=" << totLevels << "\n";
if (totLevels < 3) {
std::cout << "[renderLOD] 警告: 金字塔层数 <3需 build --levels 3\n";
}
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale cs = makeColorScale(vmin, vmax);
const fs::path shotDir =
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
fs::create_directories(shotDir);
const int winW = 1024, winH = 768;
// 共用一个捕获式 OutputWindow贯穿三段渲染。
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
// ---- (a) 粗层概览 fpslevel2 整卷 ----
const int ovLevel = std::min(2, totLevels - 1);
std::cout << "[renderLOD] (a) 建 level" << ovLevel << " 整卷 image...\n";
vtkSmartPointer<vtkImageData> ovImg = buildLevelImage(store, ovLevel, m);
int ovNx, ovNy, ovNz;
store.dims(ovLevel, ovNx, ovNy, ovNz);
auto rwOv = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> renOv;
renOv->SetBackground(0.0, 0.0, 0.0);
rwOv->AddRenderer(renOv);
vtkSmartPointer<vtkVolume> ovVol =
geopro::render::buildVoxelI16FromImage(ovImg.Get(), m.quant, cs, vmin,
vmax);
renOv->AddVolume(ovVol);
// 先测 fpsbenchVolumeFps 内部会 ResetCamera + 旋满一圈)。
const double ovFps = benchVolumeFps(rwOv.Get(), renOv, frames);
// 截图前重设一个利于人眼的取景:整线物理纵横比极扁(~2200m×1.5m×8m),俯视角
// 看宽面才能呈现整条带(而非边缘线)。
renOv->ResetCamera();
renOv->GetActiveCamera()->Elevation(55.0);
renOv->GetActiveCamera()->Azimuth(20.0);
renOv->ResetCameraClippingRange();
rwOv->Render();
const vtkIdType ovNonBlack = countNonBlackPixels(rwOv.Get(), winW, winH);
savePng(rwOv.Get(), (shotDir / "lod-overview.png").string());
std::cout << "[renderLOD] (a) 概览 fps=" << ovFps << " 非空像素=" << ovNonBlack
<< " (level" << ovLevel << " " << ovNx << "x" << ovNy << "x" << ovNz
<< ")\n";
// ---- (b) 全分辨率局部 fpslevel0 一段 brick 列 ----
const int totBx = store.bricksX(0);
const int localBx = std::min(4, totBx); // 4 brick 列 ≈ 256 体素宽
const int bx0 = std::max(0, totBx / 2 - localBx / 2); // 取沿线中段
std::cout << "[renderLOD] (b) 建 level0 局部 image (brick列 [" << bx0 << ","
<< (bx0 + localBx) << ") / " << totBx << ")...\n";
vtkSmartPointer<vtkImageData> locImg =
buildLocalLevel0Image(store, m, bx0, localBx);
int locDims[3];
locImg->GetDimensions(locDims);
auto rwLoc = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> renLoc;
renLoc->SetBackground(0.0, 0.0, 0.0);
rwLoc->AddRenderer(renLoc);
vtkSmartPointer<vtkVolume> locVol =
geopro::render::buildVoxelI16FromImage(locImg.Get(), m.quant, cs, vmin,
vmax);
renLoc->AddVolume(locVol);
const double locFps = benchVolumeFps(rwLoc.Get(), renLoc, frames);
// 截图取景:局部块(256×29×162)斜俯视,呈现全分辨率细节供与概览对比。
renLoc->ResetCamera();
renLoc->GetActiveCamera()->Elevation(35.0);
renLoc->GetActiveCamera()->Azimuth(25.0);
renLoc->ResetCameraClippingRange();
rwLoc->Render();
const vtkIdType locNonBlack = countNonBlackPixels(rwLoc.Get(), winW, winH);
savePng(rwLoc.Get(), (shotDir / "lod-fullres-local.png").string());
std::cout << "[renderLOD] (b) 局部 fps=" << locFps << " 非空像素="
<< locNonBlack << " (level0 局部 " << locDims[0] << "x" << locDims[1]
<< "x" << locDims[2] << ")\n";
// ---- (c) LOD 切换动态过渡 ----
// 同一窗口:相机从远观(看整卷,用 level2 概览体)逐步 dolly 拉近,到一半处
// 跨越 LOD 切换——把体从 level2 整卷换成 level0 局部体(重设 mapper 输入/相机
// 目标),逐帧记帧耗时,标切换帧尖峰。
std::cout << "[renderLOD] (c) LOD 切换动态过渡(" << frames << " 帧 dolly...\n";
auto rwTr = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> renTr;
renTr->SetBackground(0.0, 0.0, 0.0);
rwTr->AddRenderer(renTr);
// 远观体 = level2 概览(新建一份,避免与 (a) 共享 actor 状态)。
vtkSmartPointer<vtkVolume> farVol =
geopro::render::buildVoxelI16FromImage(ovImg.Get(), m.quant, cs, vmin,
vmax);
// 近观体 = level0 局部(复用 (b) 的 image
vtkSmartPointer<vtkVolume> nearVol =
geopro::render::buildVoxelI16FromImage(locImg.Get(), m.quant, cs, vmin,
vmax);
renTr->AddVolume(farVol);
renTr->ResetCamera(); // 框住整卷level2 与 level0 物理范围一致)
vtkCamera* camTr = renTr->GetActiveCamera();
camTr->Elevation(20.0);
renTr->ResetCameraClippingRange();
rwTr->Render(); // 预热远观
// dolly 目标:从当前(远)拉近到局部段中心。
double locCenter[3];
locImg->GetCenter(locCenter);
const int switchFrame = frames / 2;
const double dollyPerFrame =
std::pow(6.0, 1.0 / std::max(1, switchFrame)); // 切换前累计 dolly≈6×
std::vector<double> frameMs(frames, 0.0);
bool switched = false;
double switchStallMs = 0.0;
for (int f = 0; f < frames; ++f) {
Stopwatch swF;
if (f == switchFrame && !switched) {
// —— LOD 切换那一下 ——:换体 + 把相机焦点移到局部段中心。
renTr->RemoveVolume(farVol);
renTr->AddVolume(nearVol);
camTr->SetFocalPoint(locCenter[0], locCenter[1], locCenter[2]);
renTr->ResetCameraClippingRange();
switched = true;
}
// 渐进拉近(切换前 dolly 进;切换后继续推近 + 轻微环绕,逐步框满局部块)。
camTr->Dolly(switched ? 1.04 : dollyPerFrame);
if (switched) camTr->Azimuth(0.5);
renTr->ResetCameraClippingRange();
rwTr->Render();
frameMs[f] = swF.elapsedMs();
if (f == switchFrame) switchStallMs = frameMs[f];
// 切换后推近一小段再截“过渡中间帧”,使局部块已明显呈现(而非切换瞬间仍很远)。
if (f == switchFrame + (frames - switchFrame) / 3) {
savePng(rwTr.Get(), (shotDir / "lod-transition-mid.png").string());
}
}
// 过渡帧耗时统计:平均、最大、切换帧、切换帧相对邻帧的尖峰倍数。
double sum = 0, mx = 0;
for (double v : frameMs) {
sum += v;
mx = std::max(mx, v);
}
const double avgMs = frames > 0 ? sum / frames : 0.0;
const double preMs =
switchFrame > 0 ? frameMs[switchFrame - 1] : avgMs;
const double spikeRatio = preMs > 0 ? switchStallMs / preMs : 0.0;
// 可感知卡顿判据(绝对耗时为准,尖峰倍数仅作次级信号):当两端帧耗时是亚毫秒
// 时,一次性换体的 ~9ms 抖动倍数虽大但仍 <1 个 60Hz 帧(16.7ms),人眼不可感。
// 故:切换帧 >1 个 60Hz 帧(16.7ms)才记“轻微”,>2 帧(33ms)记“可感知卡顿”。
constexpr double kFrame60Ms = 1000.0 / 60.0; // 16.7ms
const bool perceptibleStall = switchStallMs > 2.0 * kFrame60Ms; // >33ms
const bool minorHitch =
!perceptibleStall && switchStallMs > kFrame60Ms; // 16.7~33ms 轻微
const vtkIdType trNonBlack = countNonBlackPixels(rwTr.Get(), winW, winH);
const bool textureErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
// 双闸:无纹理错 + 三段均渲出非空像素。
const bool renderedNonEmpty =
(ovNonBlack > 0) && (locNonBlack > 0) && (trNonBlack > 0);
const bool valid = !textureErr && renderedNonEmpty;
const double ovFpsV = valid ? ovFps : -1.0;
const double locFpsV = valid ? locFps : -1.0;
const bool ovInteractive = valid && ovFps >= 15.0;
const bool locInteractive = valid && locFps >= 15.0;
const double peak = Probe::peakMemMB();
const char* stallTxt =
perceptibleStall ? "可感知卡顿" : (minorHitch ? "轻微抖动(<2帧)" : "");
std::cout << "[renderLOD] (c) 过渡帧耗时 avg=" << avgMs << "ms max=" << mx
<< "ms 切换帧=" << switchStallMs << "ms (邻帧 " << preMs << "ms, 尖峰 "
<< spikeRatio << "×) 卡顿=" << stallTxt << "\n";
std::cout << "\n=== renderLOD LOD-fps 探针指标 ===\n";
std::cout << "离屏闸门 : OK\n";
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "") << "\n";
std::cout << "三段均渲出非空 : " << (renderedNonEmpty ? "" : "否(!!)")
<< " (概览=" << ovNonBlack << " 局部=" << locNonBlack
<< " 过渡=" << trNonBlack << ")\n";
std::cout << "(a) 粗层概览 fps : "
<< (valid ? std::to_string(ovFpsV) : std::string("INVALID"))
<< " (level" << ovLevel << " " << ovNx << "x" << ovNy << "x" << ovNz
<< ") 交互级=" << (ovInteractive ? "是 ✔" : "否 ✘") << "\n";
std::cout << "(b) 全分辨率局部fps: "
<< (valid ? std::to_string(locFpsV) : std::string("INVALID"))
<< " (level0 局部 " << locDims[0] << "x" << locDims[1] << "x"
<< locDims[2] << ") 交互级=" << (locInteractive ? "是 ✔" : "否 ✘")
<< "\n";
std::cout << "(c) 过渡平均/最大 : " << avgMs << " / " << mx << " ms\n";
std::cout << " 切换帧耗时 : " << switchStallMs << " ms (邻帧 " << preMs
<< " ms, 尖峰 " << spikeRatio << "×)\n";
std::cout << " 可感知卡顿 : " << stallTxt
<< (perceptibleStall ? "" : "") << " (判据:切换帧 >33ms 才记卡顿"
"; 1 帧 60Hz=16.7ms)\n";
std::cout << "进程峰值内存(MB) : " << peak << "\n";
std::cout << "截图 : " << shotDir.string()
<< " (lod-overview / lod-fullres-local / lod-transition-mid)\n";
writeMetricLine(
"renderLOD,dir=" + dir + ",totLevels=" + std::to_string(totLevels) +
",ovLevel=" + std::to_string(ovLevel) +
",ovDims=" + std::to_string(ovNx) + "x" + std::to_string(ovNy) + "x" +
std::to_string(ovNz) +
",ovFps=" + (valid ? std::to_string(ovFpsV) : "INVALID") +
",ovNonBlack=" + std::to_string(ovNonBlack) +
",locDims=" + std::to_string(locDims[0]) + "x" +
std::to_string(locDims[1]) + "x" + std::to_string(locDims[2]) +
",locFps=" + (valid ? std::to_string(locFpsV) : "INVALID") +
",locNonBlack=" + std::to_string(locNonBlack) +
",trAvgMs=" + std::to_string(avgMs) + ",trMaxMs=" + std::to_string(mx) +
",switchMs=" + std::to_string(switchStallMs) +
",switchSpike=" + std::to_string(spikeRatio) +
",stall=" + std::to_string(perceptibleStall ? 1 : 0) +
",trNonBlack=" + std::to_string(trNonBlack) +
",textureErr=" + std::to_string(textureErr ? 1 : 0) +
",valid=" + std::to_string(valid ? 1 : 0) +
",peakMB=" + std::to_string(peak));
// 写 poc-results-C.md 的 LOD 段(追加,不覆盖 renderC-partitioned 段)。
{
const fs::path repo =
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
fs::create_directories(repo.parent_path());
std::ofstream rf(repo.string(), std::ios::app);
if (rf) {
rf << "\n\n# POC-C LOD-fps 探针结果Task 12c\n\n";
rf << "金字塔 store: " << dir << "level0=" << m.nx << "x" << m.ny << "x"
<< m.nz << ",总 " << totLevels << " 层)\n\n";
rf << "| 项 | 维度 | 结果 |\n|---|---|---|\n";
rf << "| (a) 粗层概览 fps | level" << ovLevel << " " << ovNx << "x" << ovNy
<< "x" << ovNz << " | " << (valid ? std::to_string(ovFpsV) : "INVALID")
<< " fps " << (ovInteractive ? "(交互级)" : "(未达交互级)") << " |\n";
rf << "| (b) 全分辨率局部 fps | level0 局部 " << locDims[0] << "x"
<< locDims[1] << "x" << locDims[2] << " | "
<< (valid ? std::to_string(locFpsV) : "INVALID") << " fps "
<< (locInteractive ? "(交互级)" : "(未达交互级)") << " |\n";
rf << "| (c) LOD 切换过渡 | 切换帧 " << switchFrame << "/" << frames
<< " | 平均 " << avgMs << "ms切换帧 " << switchStallMs << "ms尖峰 "
<< spikeRatio << "×"
<< (perceptibleStall ? "可感知卡顿"
: (minorHitch ? "轻微抖动" : "无可感知卡顿"))
<< " |\n\n";
rf << "- 卡顿判据:切换帧绝对耗时 >33ms(2 个 60Hz 帧)才记可感知卡顿;"
"16.7~33ms 记轻微抖动;亚毫秒基线下尖峰倍数大但绝对值低不算卡顿。\n";
rf << "- 双闸:纹理维度错误=" << (textureErr ? "" : "")
<< ";三段均渲出非空像素=" << (renderedNonEmpty ? "" : "")
<< "(概览 " << ovNonBlack << " / 局部 " << locNonBlack << " / 过渡 "
<< trNonBlack << ")。\n";
rf << "- 截图人眼判“概览糊→拉近清晰”docs/superpowers/plans/poc-lod-shots/"
"lod-overview.png、lod-fullres-local.png、lod-transition-mid.png\n";
rf << "- 进程峰值内存: " << peak << " MB\n\n";
rf << "## 判据结论\n";
if (valid && ovInteractive && locInteractive && !perceptibleStall) {
rf << "粗层概览 + 全分辨率局部【都达交互级】且切换【无不可接受卡顿】→ "
"LOD-based C 路线钉死可行。\n";
} else if (valid && ovInteractive && !locInteractive) {
rf << "粗层快但全分辨率局部仍慢 → VTK 体绘制有真实天花板,记录,"
"评估 OpenVDS/自建 GL。\n";
} else if (valid && perceptibleStall) {
rf << "两端 fps 可接受但切换卡顿明显(切换帧 " << switchStallMs
<< "ms→ 为后续 morphing/淡入提供依据。\n";
} else if (!valid) {
rf << "双闸未过(纹理错或空渲染)→ 数字不可信,如实标 INVALID。\n";
} else {
rf << "部分达标,详见上表。\n";
}
rf << "\n**最低配未验声明**本探针仅在本机RTX 3060跑得上限数字"
"最低配机器未验证,需用户在目标机跑或提供型号。\n";
}
std::cout << "[renderLOD] 报告追加写入 " << repo.string() << "\n";
}
return valid ? 0 : 1;
}
// ============================================================================
// ① 视觉调优:出一帧能看结构的图 + 调优前后 fps 对照Task 12d
// ============================================================================
//
// 在【真实金字塔 store】上对局部段(level0 一段 brick 列)与粗层概览(level2 整卷)
// 各跑两遍体绘制 fps调优前(默认色阶 0.15 不透明度 无夸张) vs 调优后(结构色阶 +
// --opacity + --exagg 垂向夸张),离屏存 lod-tuned-local.png / lod-tuned-overview.png
// 并打印前后 fps 对照——证实「视觉调优对 fps 近乎中性」这一探针认知。双闸防假帧率。
int cmdTune(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc tune <storeDir> [--opacity 0.5] [--exagg 8] "
"[--frames 120] [--localBricks 4]\n";
return 2;
}
const std::string dir = a.positional[0];
const double opacity = std::stod(a.get("opacity", "0.5"));
const double exagg = std::stod(a.get("exagg", "8"));
const int frames = std::stoi(a.get("frames", "120"));
const int localBricks = std::stoi(a.get("localBricks", "4"));
std::cout << "[tune] storeDir=" << dir << " opacity=" << opacity
<< " exagg=" << exagg << " frames=" << frames << "\n";
std::cout << "[tune] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[tune] 闸门失败,中止。\n";
return 1;
}
geopro::data::ChunkedVolumeStore store(dir);
const geopro::data::StoreMeta& m = store.meta();
const int totLevels = store.levels();
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale csPlain = makeColorScale(vmin, vmax);
const geopro::core::ColorScale csTuned = makeStructuralColorScale(vmin, vmax);
const fs::path shotDir =
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
fs::create_directories(shotDir);
const int winW = 1024, winH = 768;
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
// ---- 局部段level0 一段 brick 列(沿线中段)----
const int totBx = store.bricksX(0);
const int localBx = std::min(localBricks, totBx);
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
vtkSmartPointer<vtkImageData> locImg =
buildLocalLevel0Image(store, m, bx0, localBx);
int locDims[3];
locImg->GetDimensions(locDims);
// 调优前局部 fps默认色阶 0.15 无夸张)。
auto rwA = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> renA;
renA->SetBackground(0.0, 0.0, 0.0);
rwA->AddRenderer(renA);
vtkSmartPointer<vtkVolume> volA =
buildTunedVolume(locImg.Get(), m.quant, csPlain, vmin, vmax, 0.15, 1.0,
/*structuralOpacity=*/false); // 原始线性单斜坡基线
renA->AddVolume(volA);
const double locFpsBefore = benchVolumeFps(rwA.Get(), renA, frames);
// 调优后局部 fps结构色阶 + opacity + exagg
auto rwB = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> renB;
renB->SetBackground(0.04, 0.04, 0.08); // 深蓝灰背景,衬托体
rwB->AddRenderer(renB);
vtkSmartPointer<vtkVolume> volB =
buildTunedVolume(locImg.Get(), m.quant, csTuned, vmin, vmax, opacity,
exagg);
renB->AddVolume(volB);
const double locFpsAfter = benchVolumeFps(rwB.Get(), renB, frames);
// 调优后取景:夸张后块更"立体"斜俯视呈现截面层次Zoom 拉近填满画面。
renB->ResetCamera();
renB->GetActiveCamera()->Elevation(28.0);
renB->GetActiveCamera()->Azimuth(30.0);
renB->GetActiveCamera()->Zoom(1.7);
renB->ResetCameraClippingRange();
rwB->Render();
const vtkIdType locNonBlack = countNonBlackPixels(rwB.Get(), winW, winH);
savePng(rwB.Get(), (shotDir / "lod-tuned-local.png").string());
// ---- 概览level2 整卷(接受它就是细带)----
const int ovLevel = std::min(2, totLevels - 1);
vtkSmartPointer<vtkImageData> ovImg = buildLevelImage(store, ovLevel, m);
auto rwO = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> renO;
renO->SetBackground(0.04, 0.04, 0.08);
rwO->AddRenderer(renO);
vtkSmartPointer<vtkVolume> volO =
buildTunedVolume(ovImg.Get(), m.quant, csTuned, vmin, vmax, opacity,
exagg);
renO->AddVolume(volO);
const double ovFpsAfter = benchVolumeFps(rwO.Get(), renO, frames);
renO->ResetCamera();
renO->GetActiveCamera()->Elevation(50.0);
renO->GetActiveCamera()->Azimuth(20.0);
renO->ResetCameraClippingRange();
rwO->Render();
const vtkIdType ovNonBlack = countNonBlackPixels(rwO.Get(), winW, winH);
savePng(rwO.Get(), (shotDir / "lod-tuned-overview.png").string());
const bool textureErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
const bool valid =
!textureErr && locNonBlack > 0 && ovNonBlack > 0;
const double dropPct =
locFpsBefore > 0 ? (locFpsBefore - locFpsAfter) / locFpsBefore * 100.0
: 0.0;
std::cout << "\n=== tune 视觉调优指标 ===\n";
std::cout << "局部段维度 : " << locDims[0] << "x" << locDims[1] << "x"
<< locDims[2] << " (level0)\n";
std::cout << "调优前局部 fps : "
<< (valid ? std::to_string(locFpsBefore) : "INVALID")
<< " (默认蓝白红, 不透明度 0.15, 无夸张)\n";
std::cout << "调优后局部 fps : "
<< (valid ? std::to_string(locFpsAfter) : "INVALID")
<< " (结构色阶, 不透明度 " << opacity << ", 夸张 " << exagg
<< "x)\n";
std::cout << "fps 变化 : " << dropPct
<< "% (正=变慢/负=变快; 探针预期近乎中性)\n";
std::cout << "调优后概览 fps : "
<< (valid ? std::to_string(ovFpsAfter) : "INVALID") << " (level"
<< ovLevel << ")\n";
std::cout << "双闸 : 纹理错=" << (textureErr ? "" : "")
<< " 局部非空=" << locNonBlack << " 概览非空=" << ovNonBlack
<< "" << (valid ? "可信" : "INVALID") << "\n";
std::cout << "截图 : " << shotDir.string()
<< " (lod-tuned-local.png / lod-tuned-overview.png)\n";
writeMetricLine(
"tune,dir=" + dir + ",opacity=" + std::to_string(opacity) +
",exagg=" + std::to_string(exagg) +
",locDims=" + std::to_string(locDims[0]) + "x" +
std::to_string(locDims[1]) + "x" + std::to_string(locDims[2]) +
",locFpsBefore=" + (valid ? std::to_string(locFpsBefore) : "INVALID") +
",locFpsAfter=" + (valid ? std::to_string(locFpsAfter) : "INVALID") +
",dropPct=" + std::to_string(dropPct) +
",ovFpsAfter=" + (valid ? std::to_string(ovFpsAfter) : "INVALID") +
",locNonBlack=" + std::to_string(locNonBlack) +
",ovNonBlack=" + std::to_string(ovNonBlack) +
",valid=" + std::to_string(valid ? 1 : 0));
return valid ? 0 : 1;
}
// ============================================================================
// ② fps 预算:递增全分辨率(level0)窗口找「每帧体素预算」Task 12d
// ============================================================================
//
// 对递增的 level0 brick 列段4,16,64,128,256 brick可 --bricks 覆盖)各重组成
// 局部整卷 image 跑体绘制 fps输出表 brick数/体素数/fps找出 fps 跌破 30 的体素
// 阈值 = production LOD 每帧渲染的全分辨率块数上限。双闸防假帧率。
int cmdFpsBudget(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc fps-budget <storeDir> [--frames 90] "
"[--bricks 4,16,64,128,256]\n";
return 2;
}
const std::string dir = a.positional[0];
const int frames = std::stoi(a.get("frames", "90"));
const double opacity = std::stod(a.get("opacity", "0.5"));
const double exagg = std::stod(a.get("exagg", "8"));
// 解析 brick 段列表(逗号分隔)。
std::vector<int> brickSteps;
{
const std::string raw = a.get("bricks", "4,16,64,128,256");
std::string cur;
for (char ch : raw) {
if (ch == ',') {
if (!cur.empty()) brickSteps.push_back(std::stoi(cur));
cur.clear();
} else {
cur.push_back(ch);
}
}
if (!cur.empty()) brickSteps.push_back(std::stoi(cur));
}
std::cout << "[fps-budget] storeDir=" << dir << " frames=" << frames << "\n";
std::cout << "[fps-budget] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[fps-budget] 闸门失败,中止,不产出 fps。\n";
return 1;
}
geopro::data::ChunkedVolumeStore store(dir);
const geopro::data::StoreMeta& m = store.meta();
const int totBx = store.bricksX(0);
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale cs = makeStructuralColorScale(vmin, vmax);
std::cout << "[fps-budget] level0=" << m.nx << "x" << m.ny << "x" << m.nz
<< " 总 brick列=" << totBx << " brick=" << m.brick << "\n";
struct Row {
int bricks;
long long voxels;
double fps;
bool valid;
};
std::vector<Row> rows;
constexpr double kTargetFps = 30.0;
long long budgetVoxels = -1; // fps 跌破 30 前的最大体素数
int budgetBricks = -1;
long long firstBelowVoxels = -1;
int firstBelowBricks = -1;
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
for (int nb : brickSteps) {
const int localBx = std::min(nb, totBx);
if (localBx <= 0) continue;
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
vtkSmartPointer<vtkImageData> img =
buildLocalLevel0Image(store, m, bx0, localBx);
int d[3];
img->GetDimensions(d);
const long long voxels =
static_cast<long long>(d[0]) * d[1] * d[2];
auto rw = makeOffscreenWindow(1024, 768);
vtkNew<vtkRenderer> ren;
ren->SetBackground(0.0, 0.0, 0.0);
rw->AddRenderer(ren);
vtkSmartPointer<vtkVolume> vol =
buildTunedVolume(img.Get(), m.quant, cs, vmin, vmax, opacity, exagg);
ren->AddVolume(vol);
const double fps = benchVolumeFps(rw.Get(), ren, frames);
// 双闸:纹理无错 + 该段渲出非空像素。
ren->ResetCamera();
rw->Render();
const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), 1024, 768);
const bool valid = !capWin->textureError() && nonBlack > 0;
rows.push_back({localBx, voxels, fps, valid});
std::cout << "[fps-budget] brick=" << localBx << " (" << d[0] << "x" << d[1]
<< "x" << d[2] << ") 体素=" << voxels << " fps="
<< (valid ? std::to_string(fps) : "INVALID")
<< " 非空=" << nonBlack << "\n";
if (valid) {
if (fps >= kTargetFps) {
if (voxels > budgetVoxels) {
budgetVoxels = voxels;
budgetBricks = localBx;
}
} else if (firstBelowVoxels < 0) {
firstBelowVoxels = voxels;
firstBelowBricks = localBx;
}
}
}
const bool textureErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
const double peak = Probe::peakMemMB();
std::cout << "\n=== fps-budget 每帧体素预算表 ===\n";
std::cout << "| brick段 | 维度体素数 | 体绘制 fps | ≥30 |\n";
std::cout << "|---|---|---|---|\n";
for (const auto& r : rows) {
std::cout << "| " << r.bricks << " | " << r.voxels << " | "
<< (r.valid ? std::to_string(r.fps) : std::string("INVALID"))
<< " | " << (r.valid && r.fps >= kTargetFps ? "" : "")
<< " |\n";
}
std::cout << "\n每帧体素预算(fps≥30 上限) : "
<< (budgetVoxels >= 0 ? std::to_string(budgetVoxels) +
" 体素 (" + std::to_string(budgetBricks) +
" brick列)"
: std::string("未触达(所有测点均 ≥30)"))
<< "\n";
std::cout << "首个跌破 30 的窗口 : "
<< (firstBelowVoxels >= 0
? std::to_string(firstBelowVoxels) + " 体素 (" +
std::to_string(firstBelowBricks) + " brick列)"
: std::string("无(测点未跌破; 需更大 --bricks)"))
<< "\n";
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "")
<< "\n";
std::cout << "进程峰值内存(MB) : " << peak << "\n";
// 落 last-metrics + 追加写 poc-results-C.md。
for (const auto& r : rows) {
writeMetricLine(
"fps-budget,dir=" + dir + ",bricks=" + std::to_string(r.bricks) +
",voxels=" + std::to_string(r.voxels) +
",fps=" + (r.valid ? std::to_string(r.fps) : "INVALID") +
",valid=" + std::to_string(r.valid ? 1 : 0));
}
{
const fs::path repo =
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
fs::create_directories(repo.parent_path());
std::ofstream rf(repo.string(), std::ios::app);
if (rf) {
rf << "\n\n# POC-C fps 预算探针结果Task 12d ②)\n\n";
rf << "金字塔 store: " << dir << "level0=" << m.nx << "x" << m.ny << "x"
<< m.nz << "brick=" << m.brick << "\n\n";
rf << "递增 level0 局部窗口(沿线中段 brick 列)体绘制 fps\n\n";
rf << "| brick段 | 体素数 | 体绘制 fps | ≥30fps |\n|---|---|---|---|\n";
for (const auto& r : rows) {
rf << "| " << r.bricks << " | " << r.voxels << " | "
<< (r.valid ? std::to_string(r.fps) : "INVALID") << " | "
<< (r.valid && r.fps >= kTargetFps ? "" : "") << " |\n";
}
rf << "\n- **每帧体素预算fps≥30 上限)**: "
<< (budgetVoxels >= 0
? std::to_string(budgetVoxels) + " 体素(" +
std::to_string(budgetBricks) + " brick 列)"
: "未触达,所有测点 ≥30fps")
<< "\n";
rf << "- 首个跌破 30 的窗口: "
<< (firstBelowVoxels >= 0
? std::to_string(firstBelowVoxels) + " 体素(" +
std::to_string(firstBelowBricks) + " brick 列)"
: "无(需更大 --bricks 段触达天花板)")
<< "\n";
rf << "- 双闸:纹理维度错误=" << (textureErr ? "" : "")
<< ";每段均按非空像素校验。\n";
rf << "- production LOD 应把【每帧渲染的全分辨率块】卡在此预算以内。\n";
rf << "- **本机 RTX 3060 上限数;最低配需用户在目标机跑 fps-budget/view。**\n";
}
std::cout << "[fps-budget] 报告追加写入 " << repo.string() << "\n";
}
return textureErr ? 1 : 0;
}
// ============================================================================
// ③ view真窗口可交互给用户肉眼测 + 最低配机跑Task 12d
// ============================================================================
//
// 真 vtkRenderWindow + vtkRenderWindowInteractor(TrackballCamera),挂
// OutOfCoreSource相机变化时 source.update(camera) 重选 LOD/视野块再渲(确保
// 拖动/缩放时 LOD 真切换);屏幕左上角 vtkTextActor 实时显示 fps + 当前 level。
// 默认取景对准局部段 + 默认垂向夸张/不透明度(同 ①)。
//
// 离屏 smoke--smoke 时不开真窗口,只离屏建管线 + 渲一帧 + 验非空像素,确保不崩。
// 整卷单张 3D 纹理的轴上限(同 renderLOD/renderB 实测 GL_MAX_3D_TEXTURE_SIZE
constexpr int kViewMax3DTex = 16384;
// view 的每帧回调共享状态(挂到 interactor 的 EndInteraction/Timer/Render 上)。
struct ViewState {
geopro::render::OutOfCoreSource* src = nullptr;
geopro::data::ChunkedVolumeStore* store = nullptr; // 整卷粗层渲染走它
vtkMultiBlockVolumeMapper* mapper = nullptr;
vtkCamera* cam = nullptr;
vtkTextActor* fpsText = nullptr;
vtkRenderWindow* rw = nullptr;
Stopwatch frameTimer;
double exagg = 8.0;
int lastLevel = -1;
// 整卷粗层 image 缓存(按 level 缓存,避免每帧重组整卷)。
int cachedWholeLevel = -1;
vtkSmartPointer<vtkImageData> cachedWholeImg;
};
// 某 level 整卷各轴是否都 ≤16384可成单张 3D 纹理 → 整卷单 mapper 渲染)。
bool levelFitsSingleTexture(const geopro::data::ChunkedVolumeStore& store,
int level) {
int nx = 0, ny = 0, nz = 0;
store.dims(level, nx, ny, nz);
return nx <= kViewMax3DTex && ny <= kViewMax3DTex && nz <= kViewMax3DTex;
}
// 给定相机选中的 level返回真正用于整卷渲染的 level从 picked 起向粗逐层找,
// 取第一个整卷各轴 ≤16384 的层(如 level0/1 长线 X 超 16384则升到 level2
// 找不到(极端情况)返回 -1调用方退回分块路径。
int wholeVolumeLevelFor(const geopro::data::ChunkedVolumeStore& store,
int picked) {
const int maxLevel = store.levels() - 1;
for (int lv = std::max(0, picked); lv <= maxLevel; ++lv) {
if (levelFitsSingleTexture(store, lv)) {
return lv;
}
}
return -1;
}
// 用 source 当前工作集刷新 mapper 输入。返回喂给 mapper 的块数。
//
// 策略(同 12c renderLOD 已验,修概览空窗):
// - 概览/中远视角(相机选中粗层)→ 升到最细的“整卷各轴 ≤16384”层整卷重组成
// 单张 image 单块喂 mapper忽略 budget粗层本就小任何缩放都显示完整体
// 不再是 budget 砍后 9% 稀疏。本数据 level0/1 的 X(44476/22238)>16384 → 升 level2。
// - 只有拉近到要全分辨率(相机选中 level0X=44476 无法成单纹理)→ 退回分块 +
// budget 路径(核外 LRU 驻留,只渲视野内子区域)。
std::size_t viewRefreshBlocks(ViewState* st) {
// 先让 source 按相机选好 LOD同时刷新 lastLevel/视野统计供 fps 文本用)。
st->src->update(st->cam);
const int picked = st->src->lastLevel();
// 仅当相机选中 level0最近、要全分辨率才分块其余概览/中远)整卷渲染。
if (st->store != nullptr && picked > 0) {
const int wlv = wholeVolumeLevelFor(*st->store, picked);
if (wlv >= 0) {
// 整卷粗层:按 level 缓存整卷 image仅在 level 变化时重组。
if (st->cachedWholeLevel != wlv || st->cachedWholeImg == nullptr) {
st->cachedWholeImg =
buildLevelImage(*st->store, wlv, st->store->meta());
st->cachedWholeLevel = wlv;
}
std::vector<vtkSmartPointer<vtkImageData>> one{st->cachedWholeImg};
auto mb = makeMultiBlock(one);
st->mapper->SetInputDataObject(mb);
st->mapper->Update();
return one.size();
}
}
// 全分辨率长线:分块 + budget。
auto imgs = st->src->currentImages();
auto mb = makeMultiBlock(imgs);
st->mapper->SetInputDataObject(mb);
st->mapper->Update();
return imgs.size();
}
// interactor 回调:每次交互(旋转/缩放)结束后重选 LOD + 刷新 fps 文本。
void viewOnInteract(vtkObject*, unsigned long, void* clientData, void*) {
auto* st = static_cast<ViewState*>(clientData);
const double frameMs = st->frameTimer.elapsedMs();
const std::size_t blocks = viewRefreshBlocks(st);
const int lvl = st->src->lastLevel();
const double fps = frameMs > 0 ? 1000.0 / frameMs : 0.0;
char buf[256];
std::snprintf(buf, sizeof(buf),
"fps: %.1f | LOD level: %d | blocks: %zu | exagg: %.0fx",
fps, lvl, blocks, st->exagg);
st->fpsText->SetInput(buf);
st->lastLevel = lvl;
st->rw->Render();
st->frameTimer.reset();
}
int cmdView(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
"[--budget 64] [--smoke]\n";
return 2;
}
const std::string dir = a.positional[0];
const double exagg = std::stod(a.get("exagg", "8"));
const double opacity = std::stod(a.get("opacity", "0.5"));
const std::size_t budget =
static_cast<std::size_t>(std::stoul(a.get("budget", "64")));
const bool smoke = a.kv.count("smoke") > 0 ||
std::find(a.positional.begin(), a.positional.end(),
"--smoke") != a.positional.end();
std::cout << "[view] storeDir=" << dir << " exagg=" << exagg
<< " opacity=" << opacity << " budget=" << budget
<< (smoke ? " [SMOKE 离屏]" : " [真窗口交互]") << "\n";
const int winW = 1280, winH = 800;
// 核外源(读 meta + 建 pager不载整卷
geopro::render::OutOfCoreSource src(dir, budget);
// 整卷粗层渲染另开 store粗层各轴 ≤16384 时整卷单 mapper 渲,绕过 budget 稀疏)。
geopro::data::ChunkedVolumeStore store(dir);
const auto& m = src.meta();
src.setAspect(static_cast<double>(winW) / winH);
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale cs = makeStructuralColorScale(vmin, vmax);
vtkSmartPointer<vtkVolumeProperty> prop =
makeTunedVolumeProperty(m.quant, cs, vmin, vmax, opacity);
// 渲染窗口smoke 走离屏,否则真窗口。
vtkSmartPointer<vtkRenderWindow> rw;
if (smoke) {
rw = makeOffscreenWindow(winW, winH);
} else {
rw = vtkSmartPointer<vtkRenderWindow>::New();
rw->SetSize(winW, winH);
rw->SetWindowName("gpr_poc view —— 核外 LOD 体绘制 (滚轮缩放切 LOD, 左键旋转)");
}
vtkNew<vtkRenderer> ren;
ren->SetBackground(0.04, 0.04, 0.08);
rw->AddRenderer(ren);
vtkNew<vtkMultiBlockVolumeMapper> mapper;
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
volume->SetScale(1.0, exagg, exagg); // 垂向夸张(同 ①)
ren->AddVolume(volume);
// 屏幕左上角实时 fps 文本。
vtkNew<vtkTextActor> fpsText;
fpsText->SetInput("fps: -- | LOD level: --");
fpsText->GetTextProperty()->SetFontSize(20);
fpsText->GetTextProperty()->SetColor(1.0, 1.0, 0.4);
fpsText->SetDisplayPosition(12, winH - 30);
ren->AddViewProp(fpsText);
// 捕获式 OutputWindow拦截块上传纹理错
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
ViewState st;
st.src = &src;
st.store = &store;
st.mapper = mapper.Get();
st.fpsText = fpsText.Get();
st.rw = rw.Get();
st.exagg = exagg;
// 相机初始定向:先框整体选出工作集,再 ResetCamera 到工作集包围盒(同 renderC
ren->ResetCamera(m.origin[0], m.origin[0] + m.nx * m.spacing[0],
m.origin[1], m.origin[1] + m.ny * m.spacing[1] * exagg,
m.origin[2], m.origin[2] + m.nz * m.spacing[2] * exagg);
st.cam = ren->GetActiveCamera();
const std::size_t warm = viewRefreshBlocks(&st);
{
double b[6];
mapper->GetBounds(b);
if (b[0] <= b[1]) {
// 工作集包围盒需按 exagg 缩放后再框actor 已 SetScale
ren->ResetCamera();
}
}
st.cam->Elevation(25.0);
st.cam->Azimuth(25.0);
ren->ResetCameraClippingRange();
rw->Render();
std::cout << "[view] 预热: level=" << src.lastLevel() << " 视野块="
<< src.lastVisibleCount() << "/" << src.lastLevelBrickTotal()
<< " 驻留=" << src.residentCount() << " 渲染块=" << warm << "\n";
const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), winW, winH);
const bool textureErr = capWin->textureError();
const bool renderedOk = !textureErr && nonBlack > 0;
if (smoke) {
// 离屏 smoke模拟一次缩放 → 验 LOD 切换 + 不崩。
const int lvlNear = src.lastLevel();
st.cam->Dolly(0.2); // 拉远 → 期望切粗 LOD
ren->ResetCameraClippingRange();
const std::size_t blocksFar = viewRefreshBlocks(&st);
const int lvlFar = src.lastLevel();
rw->Render();
st.cam->Dolly(8.0); // 拉近 → 期望切细 LOD
ren->ResetCameraClippingRange();
viewRefreshBlocks(&st);
const int lvlNear2 = src.lastLevel();
rw->Render();
const vtkIdType nb2 = countNonBlackPixels(rw.Get(), winW, winH);
vtkOutputWindow::SetInstance(nullptr);
const bool lodSwitched = (lvlFar != lvlNear) || (lvlNear2 != lvlFar);
const bool ok = renderedOk && nb2 > 0 && !capWin->textureError();
std::cout << "\n=== view --smoke 离屏冒烟 ===\n";
std::cout << "近观 level=" << lvlNear << " → 拉远 level=" << lvlFar
<< " → 再拉近 level=" << lvlNear2 << "\n";
std::cout << "LOD 随缩放切换 : " << (lodSwitched ? "是 ✔" : "否(测点档位未跨界)")
<< " (blocksFar=" << blocksFar << ")\n";
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "") << "\n";
std::cout << "渲出非空像素 : " << (renderedOk ? "" : "否(!!)")
<< " (近=" << nonBlack << " 远拉近=" << nb2 << ")\n";
std::cout << "smoke 结果 : " << (ok ? "OK ✔ 不崩" : "FAIL ✘") << "\n";
return ok ? 0 : 1;
}
vtkOutputWindow::SetInstance(nullptr);
if (!renderedOk) {
std::cout << "[view] 警告: 首帧未渲出非空像素(纹理错=" << textureErr
<< ");窗口仍开,供人工排查。\n";
}
// 真窗口交互TrackballCamera + 每次交互结束重选 LOD + 刷 fps 文本。
vtkNew<vtkRenderWindowInteractor> iren;
iren->SetRenderWindow(rw);
vtkNew<vtkInteractorStyleTrackballCamera> style;
iren->SetInteractorStyle(style);
vtkNew<vtkCallbackCommand> cb;
cb->SetCallback(viewOnInteract);
cb->SetClientData(&st);
// EndInteraction旋转/缩放松手后重选 LOD保证 LOD 真切换 + fps 刷新)。
iren->AddObserver(vtkCommand::EndInteractionEvent, cb);
// 每帧 Render 后也更新一次 fps 文本(连续拖动时实时反馈)。
rw->AddObserver(vtkCommand::EndEvent, cb);
std::cout << "[view] 打开真窗口。左键旋转 / 滚轮缩放(切 LOD) / q 退出。\n";
st.frameTimer.reset();
iren->Initialize();
rw->Render();
iren->Start();
std::cout << "[view] 窗口关闭,退出。\n";
return 0;
}
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"
" gpr_poc renderLOD <storeDir> [--frames 120]\n"
" gpr_poc tune <storeDir> [--opacity 0.5] [--exagg 8] "
"[--frames 120] [--localBricks 4]\n"
" gpr_poc fps-budget <storeDir> [--frames 90] "
"[--bricks 4,16,64,128,256]\n"
" gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
"[--budget 64] [--smoke]\n";
}
} // namespace
int main(int argc, char** argv) {
#ifdef _WIN32
// Windows 控制台默认 GBK会把 UTF-8 中文输出显示为乱码。设为 UTF-8 码页修复。
SetConsoleOutputCP(CP_UTF8);
#endif
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);
if (cmd == "renderLOD") return cmdRenderLOD(argc, argv);
if (cmd == "tune") return cmdTune(argc, argv);
if (cmd == "fps-budget") return cmdFpsBudget(argc, argv);
if (cmd == "view") return cmdView(argc, argv);
} catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << "\n";
return 1;
}
usage();
return 2;
}