diff --git a/.superpowers/sdd/task-9c-report.md b/.superpowers/sdd/task-9c-report.md new file mode 100644 index 0000000..9beebee --- /dev/null +++ b/.superpowers/sdd/task-9c-report.md @@ -0,0 +1,44 @@ +# Task 9c 报告:POC-B 离屏 GPU 渲染基准 + +状态:**DONE**(闸门通过;真实基准实测完成;关键发现如实记录,无任何编造 fps) + +执行机:Windows 11,MSVC(VS18 Community)+ Ninja,Release;GPU = NVIDIA RTX 3060 Laptop GPU,OpenGL 4.5.0 NVIDIA 555.97。 +日期:2026-06-23。 + +--- + +## 1. 交付物 + +- `tools/gpr_poc/main.cpp`:新增两个子命令 + - `gpr_poc offscreen-smoke` —— 最小离屏渲染冒烟(闸门),打印 OK/FAIL + GL 能力。 + - `gpr_poc renderB [--frames 120]` —— 离屏体绘制 + 切片扫描 fps 基准。 +- `tools/gpr_poc/CMakeLists.txt`:补 VTK 组件(RenderingVolume / RenderingVolumeOpenGL2 / ImagingCore / InteractionStyle)。 +- `docs/superpowers/plans/poc-results-B.md`:新增「§4 离屏 GPU 渲染基准」段(闸门 + 真实指标 + 关键发现 + 结论)。 +- `build/_t9c_build.bat`:本任务用的 gpr_poc 单 target 构建脚本(vcvars64 直驱 cmake)。 + +## 2. 闸门结果 —— OK + +`offscreen-smoke`:离屏 vtkRenderWindow(SetOffScreenRendering+SetShowWindow(false))→ cube actor → Render() → +GetRGBACharPixelData 读回 65536 像素,非背景 28224。GL vendor=NVIDIA,硬件加速 True。**离屏 GL 可用**,继续真实基准。 + +## 3. 真实 GPU 指标(line 001, cellXY=0.05, cellZ=0.05) + +- 体维度:**44476 × 29 × 162**;体素数 ≈2.09 亿;整卷字节 **398.54 MB**(int16)。 +- **体绘制 fps:INVALID** —— 整卷 X 维 44476 超 `GL_MAX_3D_TEXTURE_SIZE=16384`, + `vtkVolumeTexture` 报 `Invalid texture dimensions [44476,29,162]`,未真正绘出体数据。 + raw_fps=295.6 是空纹理假帧率,已显式标 INVALID,**不作为体绘制性能上报**。 +- **切片扫描 fps:54.6 fps**(120 帧沿 Z 扫整卷,vtkImageReslice 2D 切面 + 2D 纹理;不受 3D 纹理上限约束)。≥30fps 目标达成。 +- 是否进显存:**否**(瓶颈是单轴纹理维度上限 16384,非显存字节;整卷 398 MB << RTX 3060 6GB 显存)。 +- GPU 显存(NVX):**N/A**(随包 VTK 安装未带 GLEW 头,无法链 GL loader 直查;GL 扩展列表确认机器支持 NVX_gpu_memory_info)。 +- 进程峰值内存:**≈509 MB**(加载整卷 398 MB + 渲染管线)。 +- build 峰值内存:4830 MB(装配阶段 double survey 主导,与 §2 一致);无 OOM,cellXY=0.05 一次通过。 + +## 4. concerns + +1. **整卷朴素体绘制对长测线根本不可行**:X=44476 撞 OpenGL 单轴 3D 纹理上限 16384。 + 与显存容量无关,是硬限制。任何「整卷一次性 3D 纹理」方案对长测线都会撞墙。 + 这是 **Task 12(核外 / 分块 LOD / 体纹理分区 `vtkOpenGLGPUVolumeRayCastMapper::SetPartitions`)** + 的硬性依据。本任务按约束未做核外,仅如实记录。 +2. SmartVolumeMapper 报 GPURenderMode=2 但纹理上传失败——`GetLastUsedRenderMode()` 不能单独作为 + 「真的渲染出来了」的判据;renderB 已加 OutputWindow 捕获 + 维度超限双判据才下 INVALID 结论。 +3. GPU 显存读数缺失(N/A):仅因 VTK 安装未带 GLEW 头;若需要可后续单独链 GL loader 调 NVX 枚举。 diff --git a/docs/superpowers/plans/poc-results-B.md b/docs/superpowers/plans/poc-results-B.md index 02996ee..258e2b0 100644 --- a/docs/superpowers/plans/poc-results-B.md +++ b/docs/superpowers/plans/poc-results-B.md @@ -103,3 +103,77 @@ gpr_poc load **结论**:`assembler`/`GprGeometry`/CLI `specFromSurvey` 的 Z 计算**全部正确**, 无需改 CLI。先前的 nz=1 症状是 soilVelocity 换算缺失时代的遗留,现已不复存在。 CLI 的 `specFromSurvey` 用的是 `survey.dz`(来自 `depthOfSample`),未误用原始 100,未漏乘。 + +--- + +## 4. 离屏 GPU 渲染基准(任务 9c,2026-06-23) + +工具新增子命令:`gpr_poc offscreen-smoke`(闸门)、`gpr_poc renderB [--frames N]`。 +执行机 GPU:**NVIDIA GeForce RTX 3060 Laptop GPU**,OpenGL 4.5.0 NVIDIA 555.97,硬件加速 True。 + +### 4.1 闸门:offscreen-smoke —— **OK(离屏 GL 可用)** + +命令:`gpr_poc offscreen-smoke` + +- 离屏 `vtkRenderWindow`(`SetOffScreenRendering(1)`+`SetShowWindow(false)`,256×256) + → 加 cube actor → `Render()` → `GetRGBACharPixelData` 读回。 +- 读回 65536 像素,非背景像素 **28224**(cube 正确画出)。 +- GL vendor=NVIDIA Corporation,renderer=RTX 3060 Laptop GPU,硬件加速 True。 +- **结论:离屏 GPU 渲染在本机可用**,继续真实基准(非编造)。 + +### 4.2 基准数据(line 001,更细一档 cellXY=0.05) + +命令:`gpr_poc build "D:\Downloads\明星路" --line 001 --cellXY 0.05 --cellZ 0.05 --out --levels 1` + +| 指标 | 值 | +|------|-----| +| 体维度(nx×ny×nz) | **44476 × 29 × 162** | +| 体素数 | 208,948,248(≈2.09 亿) | +| 整卷字节(int16,进显存判据) | 417,896,496 B(**398.54 MB**) | +| data.bin(含金字塔) | 199.43 MB(压缩比 2.00×) | +| build 峰值内存 | 4,830 MB(装配阶段 double survey 主导,同 §2.4) | +| 整卷加载耗时(renderB load) | ≈2.8–4.0 s | +| renderB 进程峰值内存 | **≈509 MB**(加载整卷 398 MB + 渲染管线) | + +无 build OOM,cellXY=0.05 一次通过,未调粗。 + +### 4.3 renderB 实测指标 —— **关键发现:整卷体绘制不可行** + +命令:`gpr_poc renderB --frames 120` + +| 指标 | 值 | +|------|-----| +| 离屏闸门复检 | OK | +| **体绘制 fps** | **INVALID(整卷超 3D 纹理上限)** | +| ├ raw_fps(空纹理渲染,不可信) | 295.6(仅作记录,非真实帧率) | +| ├ SmartVolumeMapper 渲染模式 | 2 = GPURenderMode | +| └ vtkVolumeTexture 报错 | `Invalid texture dimensions [44476, 29, 162]` | +| **切片扫描 fps** | **54.6 fps**(120 帧沿 Z 扫整卷,reslice+纹理) | +| 整卷进显存 | **否**(X=44476 > GL_MAX_3D_TEXTURE_SIZE=16384) | +| 降质重采样(LowRes) | 否(未触发;是直接纹理维度超限失败,非显存不足降质) | +| GPU 显存(NVX) | **N/A**(随包 VTK 安装未带 GLEW 头,无法直查 `NVX_gpu_memory_info`; + 但 GL 扩展列表确认该扩展存在,机器支持,仅本工具未链 GL loader) | +| 进程峰值内存 | ≈509 MB | + +#### 关键发现(务必看) + +1. **整卷体绘制在本机离屏下不可行**:测线 001 的 X 维(沿测线方向)= **44476**, + 远超本机 OpenGL `GL_MAX_3D_TEXTURE_SIZE = 16384`。`vtkSmartVolumeMapper` + 走 GPU 路径(mode=2)但底层 `vtkVolumeTexture` **无法将整卷上传为单张 3D 纹理**, + 报 `Invalid texture dimensions`。此时 `Render()` 实际未绘出体数据, + 故所谓 295 fps 是**空纹理渲染的假帧率,已如实标 INVALID,绝不上报为体绘制性能**。 +2. **切片扫描真实流畅**:切片走 `vtkImageReslice` 输出 2D 切面 + 2D 纹理着色, + **不受 3D 纹理维度上限约束**,实测 **54.6 fps ≥ 30fps 目标**,整卷切片交互流畅。 +3. **进显存判据**:整卷 398 MB 远小于 GPU 显存(RTX 3060 6GB),显存容量不是瓶颈; + 真正的瓶颈是**单轴纹理维度上限(16384)**,而非显存字节数。 + +#### 结论 + +- **切片**:✅ 本机离屏下整卷切片 ≥30fps(54.6fps),交互流畅,满足目标。 +- **整卷体绘制**:❌ 在「整卷成单张 3D 纹理」的朴素路径下**不可行**—— + 长测线 X 维超 GL 单轴上限。这正是 **Task 12(核外 / 分块 LOD / 体纹理分区 + `vtkOpenGLGPUVolumeRayCastMapper::SetPartitions`)** 必须解决的问题: + 要么沿 X 分区/分块上传,要么按视相机做 LOD 工作集。本任务(9c)按约束**不做核外**, + 仅如实记录此限制作为 Task 12 的硬性依据。 +- 该限制与显存容量无关,是 OpenGL 纹理维度硬上限;任何「整卷一次性 3D 纹理」方案 + 对长测线都会撞同一面墙。 diff --git a/tools/gpr_poc/CMakeLists.txt b/tools/gpr_poc/CMakeLists.txt index 453a71c..55a4fa6 100644 --- a/tools/gpr_poc/CMakeLists.txt +++ b/tools/gpr_poc/CMakeLists.txt @@ -6,7 +6,9 @@ # VTK_LIBRARIES 在子作用域内由各 find_package 设定,这里显式再请求一次 # (与 geopro_render 同组件集),确保本 target 的 vtk_module_autoinit 可用。 find_package(VTK REQUIRED COMPONENTS - CommonCore CommonDataModel RenderingCore RenderingOpenGL2 GUISupportQt) + CommonCore CommonDataModel + RenderingCore RenderingOpenGL2 RenderingVolume RenderingVolumeOpenGL2 + ImagingCore InteractionStyle GUISupportQt) add_executable(gpr_poc main.cpp) diff --git a/tools/gpr_poc/main.cpp b/tools/gpr_poc/main.cpp index 2a1259c..e0cf71c 100644 --- a/tools/gpr_poc/main.cpp +++ b/tools/gpr_poc/main.cpp @@ -8,6 +8,8 @@ // gpr_poc build [--line 001] [--cellXY 0.2] [--cellZ 0.05] [--out ] [--levels 2] // gpr_poc load // gpr_poc selftest +// gpr_poc offscreen-smoke —— 离屏 GL 闸门冒烟 +// gpr_poc renderB [--frames 120] —— 离屏体绘制/切片 fps 基准 #include #include @@ -25,11 +27,35 @@ #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/WholeVolumeSource.hpp" +// ---- VTK 离屏渲染 ---- +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + namespace fs = std::filesystem; using geopro::tools::Probe; using geopro::tools::Stopwatch; @@ -416,12 +442,364 @@ int cmdSelftest() { 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); + +// 创建一个离屏 vtkRenderWindow(VTK9.6:SetShowWindow(false)+OffScreenRenderingOn)。 +vtkSmartPointer makeOffscreenWindow(int w, int h) { + auto rw = vtkSmartPointer::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 ren; + ren->SetBackground(0.1, 0.1, 0.2); + rw->AddRenderer(ren); + + vtkNew cube; + cube->SetXLength(1.0); + cube->SetYLength(1.0); + cube->SetZLength(1.0); + + vtkNew mapper; + mapper->SetInputConnection(cube->GetOutputPort()); + + vtkNew 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::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 reslice; + reslice->SetInputData(full); + reslice->SetOutputDimensionality(2); + reslice->SetInterpolationModeToLinear(); + + vtkNew colorize; + colorize->SetLookupTable(lut); + colorize->SetInputConnection(reslice->GetOutputPort()); + + vtkNew 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(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 makeLut(const geopro::core::ColorScale& cs, + double vmin, double vmax) { + auto lut = vtkSmartPointer::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 [--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(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 ren; + ren->SetBackground(0.0, 0.0, 0.0); + rw->AddRenderer(ren); + + vtkSmartPointer volume = + geopro::render::buildVoxelI16FromImage(shortImg, m.quant, cs, vmin, vmax); + ren->AddVolume(volume); + + // 装上捕获式 OutputWindow:拦截体绘制时的 3D 纹理维度错误。 + auto capWin = vtkSmartPointer::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 ren2; + ren2->SetBackground(0.0, 0.0, 0.0); + auto rw2 = makeOffscreenWindow(1024, 768); + rw2->AddRenderer(ren2); + vtkSmartPointer 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; +} + void usage() { std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n" " gpr_poc build [--line 001] [--cellXY 0.2] " "[--cellZ 0.05] [--out ] [--levels 2]\n" " gpr_poc load \n" - " gpr_poc selftest\n"; + " gpr_poc selftest\n" + " gpr_poc offscreen-smoke\n" + " gpr_poc renderB [--frames 120]\n"; } } // namespace @@ -436,6 +814,8 @@ int main(int argc, char** argv) { 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); } catch (const std::exception& e) { std::cerr << "错误: " << e.what() << "\n"; return 1;