feat/vtk-3d-view #7
|
|
@ -0,0 +1,214 @@
|
|||
# POC-C 实测结果(核外分块体绘制,命门探针)
|
||||
|
||||
工具:`tools/gpr_poc renderC`,产物 `build/release/tools/gpr_poc/gpr_poc.exe`。
|
||||
执行机:Windows 11,MSVC(VS Community)+ Ninja,Release(/O2)。
|
||||
GPU:**NVIDIA GeForce RTX 3060 Laptop GPU**,OpenGL 4.5.0 NVIDIA 555.97,硬件加速 True。
|
||||
日期:2026-06-23。
|
||||
|
||||
被测 store(9c 建的单线全分辨率):`build/tmp/gpr_store_B_001`
|
||||
- 体维度 **44476 × 29 × 162**(≈2.09 亿体素),brick=64,金字塔 2 级
|
||||
(level0=2085 块 / level1=696 块)。
|
||||
- **沿测线 X=44476 ≫ GL_MAX_3D_TEXTURE_SIZE(16384)** —— 即 renderB 标 INVALID
|
||||
的那条线,本任务要分块核外把它真渲出。
|
||||
|
||||
实现:
|
||||
- `geopro::render::OutOfCoreSource`(实现 `IVolumeRenderSource`):选 LOD(相机到体中心
|
||||
距离 / 体对角线 粗分档)+ 视锥裁剪选视野块 → `BrickPager.requestVisible`(LRU,内存恒定)
|
||||
→ 每块构造带世界坐标的 ≤64³ `vtkImageData`(VTK_SHORT)。
|
||||
- `renderC` 把工作集各块装进 `vtkMultiBlockDataSet`,交 `vtkMultiBlockVolumeMapper`
|
||||
(内部每块一个 `vtkSmartVolumeMapper`,back-to-front 排序 + 抖动压接缝)。
|
||||
- 用 9c 同款 `CapturingOutputWindow` 捕获 3D 纹理维度错误;以**纹理无错 + 渲出非空像素**
|
||||
为体绘制真出判据(绝不把空纹理假帧率当性能)。
|
||||
|
||||
主配置命令:`gpr_poc renderC <store> --budget 64 --frames 120`
|
||||
|
||||
---
|
||||
|
||||
## 0. 总结论(一句话)
|
||||
|
||||
**核外分块体绘制可行、内存恒定、绕开了 16384 纹理墙——但 `vtkMultiBlockVolumeMapper`
|
||||
的「每块一个 SmartVolumeMapper」架构使 fps 随工作集块数急剧下降,且每帧重建 mapper +
|
||||
qUncompress 解压换页是更狠的瓶颈。** renderB 整卷 INVALID(根本上传不了),renderC
|
||||
能真渲出(budget=64 静态 9.5 fps),但要达交互级 fps 必须换更省的核外管线
|
||||
(见 §7 阻塞 / 缓解)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 六个未知的逐条实测结论
|
||||
|
||||
### 未知 1:vtkMultiBlockVolumeMapper 能否把动态工作集块渲成正确合成体 —— ✅ 能
|
||||
|
||||
- budget=64:纹理维度错误=**否**(每块 ≤64³ ≪ 16384,逐块上传全部成功),
|
||||
渲出非空像素=**是**(非背景像素 748),退出码 0。
|
||||
- **对照 renderB**:renderB 整卷报 `Invalid texture dimensions [44476,29,162]`、
|
||||
体绘制 INVALID(空纹理假帧率);renderC 把同一条线切成 ≤64³ 的块逐块上传,
|
||||
**真渲出**。核心可行性成立——**分块核外绕开了 GL 单轴纹理上限。**
|
||||
- 注:`vtkMultiBlockDataSet` + `vtkMultiBlockVolumeMapper` 直接吃多块 `vtkImageData`,
|
||||
颜色/不透明度传函挂在单个 `vtkVolume` 的 `vtkVolumeProperty` 上、全块共用,工作正常。
|
||||
|
||||
### 未知 2:块边接缝 —— ✅ 未见明显接缝(MultiBlock 内置抖动生效)
|
||||
|
||||
- `vtkMultiBlockVolumeMapper` 默认对块交界开抖动(jittering,仅 GPURenderMode 下),
|
||||
实测渲出图像未见可见接缝条纹。本任务在等值密度的连续 GPR 体上,块边连续性可接受。
|
||||
- 局限:不同 LOD level 相邻(接缝处分辨率突变)的接缝未单独压测——本任务同一帧同一 level,
|
||||
跨 level 接缝留待 Task 13/14。
|
||||
|
||||
### 未知 3:LOD 切换 —— ✅ 机制可用,闪烁未量化
|
||||
|
||||
- `pickLevel` 按相机距离/体对角线比值粗分档(<1×→L0,<2×→L1…,clamp 到可用层)。
|
||||
- 实测:预热相机框全体 → 选 **level 1**(视野块 696/696);ResetCamera 框住工作集后相机贴近
|
||||
→ 转为 **level 0**(末帧视野块 456/2085)。LOD 选择随相机距离正确切换。
|
||||
- 闪烁:每帧重选 level/视野块 + 重建 mapper,块集合跳变会带来视觉跳变,但本探针未做
|
||||
逐帧像素差量化。**结论:机制可用;平滑度(hysteresis/淡入淡出)是后续优化,非本探针判据。**
|
||||
|
||||
### 未知 4:热路径解压(qUncompress)—— ⚠️ **这是更狠的瓶颈**
|
||||
|
||||
- budget=64:**动态换页 1.45 fps**,其中 `update(cam)`(重选块 + `BrickPager.requestVisible`
|
||||
里 `store.readBrick`→`qUncompress` 解压换入块 + 重建 `vtkMultiBlockDataSet`)平均
|
||||
**177.8 ms/帧**,占整帧绝大部分(静态工作集同 64 块只旋转时是 9.5 fps≈105 ms/帧)。
|
||||
- 即换页/解压 + 每帧重建 mapper 把 9.5 fps 拖到 1.45 fps。**热路径解压确实拖垮帧率**——
|
||||
当相机移动导致工作集大量换入时,每帧要解压几十个块 + MultiBlock 重新创建所有子 mapper
|
||||
并重传纹理。**实锤未知 4:撞墙。** 缓解见 §7。
|
||||
|
||||
### 未知 5:内存恒定(residentCount ≤ budget,与体总量无关)—— ✅ 成立
|
||||
|
||||
| budget | 峰值驻留块 | 进程峰值内存 |
|
||||
|--------|-----------|--------------|
|
||||
| 64 | **64**(≤budget ✔) | **220 MB** |
|
||||
| 256 | **256**(≤budget ✔) | 282 MB |
|
||||
|
||||
- 驻留块数严格 ≤ budget,由 `BrickPager` LRU 保证;与体总量(2.09 亿体素 / 整卷 398 MB)无关。
|
||||
- 对照 renderB 整卷常驻 ≈509 MB;renderC budget=64 仅 220 MB 且**不随体增大**。内存恒定达成。
|
||||
|
||||
### 未知 6:全分辨率长线体绘制真实 fps(renderB INVALID 的那个)—— ✅ 真渲出,附实测 fps
|
||||
|
||||
| 口径 | budget=64 | 说明 |
|
||||
|------|-----------|------|
|
||||
| **静态工作集 fps** | **9.49 fps** | 工作集固定(64 块),仅旋相机 + Render,纯 GPU MultiBlock 体绘制 |
|
||||
| **动态换页 fps** | **1.45 fps** | 每帧 update(重选/解压换页)+ 重建 mapper + Render |
|
||||
| 对照 renderB | **INVALID** | 整卷超 3D 纹理上限,根本上传不了,假帧率 295 不可信 |
|
||||
|
||||
**renderB INVALID → renderC 真渲出**(非空像素、无纹理错),命门探针的核心目标达成。
|
||||
fps 离交互级(≥30)尚远(见 §7)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 关键数据表(budget=64,主配置)
|
||||
|
||||
| 指标 | 值 |
|
||||
|------|-----|
|
||||
| 体维度 | 44476 × 29 × 162(整卷 X 超 16384,renderB=INVALID) |
|
||||
| 体素数 | 208,948,248 |
|
||||
| budget(块) | 64 |
|
||||
| 峰值驻留(块) | **64(≤budget,内存恒定 OK)** |
|
||||
| 末帧 level / 视野块 / 该 level 总块 | 0 / 456 / 2085 |
|
||||
| 平均渲染块/帧 | 64 |
|
||||
| 纹理维度错误 | **否** |
|
||||
| 渲出非空像素 | **是**(非背景像素 748) |
|
||||
| 静态工作集 fps | **9.49** |
|
||||
| 动态换页 fps | **1.45**(换页均 177.8 ms/帧) |
|
||||
| 进程峰值内存 | **220 MB** |
|
||||
| 源构造耗时(读 meta + 建 pager,不载整卷) | 28 ms |
|
||||
| 离屏闸门 | OK(RTX 3060,OpenGL 4.5) |
|
||||
|
||||
---
|
||||
|
||||
## 3. budget 扫描 —— MultiBlock fps 随块数急剧劣化(重要)
|
||||
|
||||
| budget | 峰值驻留 | 静态工作集 fps | 动态换页 fps | 渲出非空 | 备注 |
|
||||
|--------|---------|---------------|-------------|---------|------|
|
||||
| 64 | 64 | **9.49** | 1.45 | 是(748) | 主配置 |
|
||||
| 256 | 256 | **0.47** | 0.51 | **否(0)** | 见下 |
|
||||
|
||||
**发现**:
|
||||
1. **fps 与工作集块数近似反比**:64 块 9.5 fps → 256 块 0.47 fps(≈20×慢)。
|
||||
因 `vtkMultiBlockVolumeMapper` 给每块建一个独立 `vtkSmartVolumeMapper`、逐块单独
|
||||
ray-cast + back-to-front 排序,开销随块数线性甚至更糟增长。**核外工作集块数必须压到
|
||||
几十量级(靠紧视锥裁剪 + 合理 LOD),不能盲目放大 budget。**
|
||||
2. budget=256 时渲出非空像素=否:工作集横跨 x[588→2223]≈1635 m,ResetCamera 框这么宽
|
||||
的薄长体 + 不透明度 0.15,末帧像素判据落到 0(投影过薄/过淡)。这暴露**正确性判据
|
||||
对「宽而薄工作集 + 低不透明度」敏感**——非"没渲",而是末帧那一姿态太淡。budget=64
|
||||
工作集窄(x[1817→2223]≈406 m)时稳定非空。后续应改用累计多帧非空 or 调不透明度判据。
|
||||
|
||||
---
|
||||
|
||||
## 4. 实现做到哪步(按 brief 要求说明)
|
||||
|
||||
- **LOD**:已实现(相机距离粗分档,clamp 到可用层),非仅固定 level 0。
|
||||
- **视野块**:已实现**视锥裁剪**(`vtkCamera::GetFrustumPlanes` + AABB 保守剔除),
|
||||
非"该 level 全部块"。cam==nullptr(headless 测试)时回退取全块、由 budget/LRU 限制。
|
||||
- **渲染**:`vtkMultiBlockDataSet` + `vtkMultiBlockVolumeMapper`(方案二「N 块成一个 multiblock
|
||||
数据集交单个 mapper」,非手搓 N 个 vtkVolume)。
|
||||
- **块世界坐标**:按 brief 公式,level L 间距 = meta.spacing × 2^L,origin = meta.origin +
|
||||
块起始体素 × 该 level 间距;块索引/偏移 64 位。单元测试覆盖 level0/level1 世界坐标。
|
||||
- **fps/内存**:静态 + 动态两段 fps、Psapi 峰值内存、residentCount、换页耗时占比均实测打印。
|
||||
|
||||
---
|
||||
|
||||
## 5. 单元测试(headless,不需 GPU 部分)
|
||||
|
||||
`tests/render/test_outofcore_source.cpp`,3 用例全 PASS:
|
||||
- `WorkingSetBricksAreTextureSafeAndBounded`:update 后工作集每块各轴 ≤64(纹理安全)、
|
||||
VTK_SHORT、residentCount ≤ budget、工作集图像数 ≤ budget。
|
||||
- `BrickWorldCoordsLevel0`:level0 块 (bx=1) 世界 origin = meta.origin + 64×spacing,
|
||||
spacing == meta.spacing。
|
||||
- `BrickWorldCoordsLevel1Spacing`:金字塔 level1 dims = ceil(level0/2),存在 ≥2 级。
|
||||
|
||||
---
|
||||
|
||||
## 6. 通过判据对照
|
||||
|
||||
| 判据 | 结果 |
|
||||
|------|------|
|
||||
| 工作集核外体绘制能正确渲出全分辨率长线(renderB INVALID 的那个,渲出非空) | ✅ 是(budget=64,非空像素 748,无纹理错) |
|
||||
| 内存恒定(residentCount ≤ budget) | ✅ 是(64/64、256/256,内存 220/282 MB,不随体增大) |
|
||||
| 接缝可接受或明确缓解 | ✅ 同 level 无明显接缝(MultiBlock 抖动);跨 level 留待后续 |
|
||||
| 闪烁可接受或明确缓解 | ⚠️ LOD 切换机制可用,平滑度未量化(hysteresis 为后续优化) |
|
||||
| 解压可接受或明确缓解 | ❌ **撞墙**:换页解压 + 每帧重建 mapper = 177.8 ms/帧,是主瓶颈(见 §7 缓解) |
|
||||
|
||||
核心两条(绕开纹理墙真渲出 + 内存恒定)**达成**;解压热路径**撞墙**,按 brief 如实记录。
|
||||
|
||||
---
|
||||
|
||||
## 7. 阻塞 / 撞墙 + 缓解(反馈 spec C,命门探针的产出)
|
||||
|
||||
### 阻塞 A:vtkMultiBlockVolumeMapper 不为「数十~数百动态块 + 每帧换页」设计
|
||||
|
||||
- **现象**:① fps 随块数近似反比劣化(64→9.5、256→0.47 fps);② 每帧 `SetInputDataObject`
|
||||
换 multiblock → 内部 `CreateMappers` 重建所有子 SmartVolumeMapper 并重传纹理,叠加
|
||||
qUncompress,达 177.8 ms/帧;③ 静态工作集 9.5 fps 也低于整卷(renderB 在能渲的更小体上
|
||||
可上百 fps)。
|
||||
- **缓解方向(供 spec C / Task 14 选型)**:
|
||||
1. **复用 mapper、增量换块**:不每帧重建 `vtkMultiBlockDataSet`;保持稳定块集合,
|
||||
仅对真正换入/换出的块做局部更新(避免内部全量重建子 mapper)。
|
||||
2. **换核外管线**:评估 `vtkOpenGLGPUVolumeRayCastMapper::SetPartitions`(单 mapper 沿轴
|
||||
分区上传,绕纹理墙且无 N 个 mapper 的排序/重传开销)作为 MultiBlock 的替代。
|
||||
3. **后台解压线程 + 双缓冲**:把 `qUncompress` 移出渲染线程(pager 预取 + worker 解压),
|
||||
渲染线程只切换已就绪块,杜绝换页阻塞渲染帧。
|
||||
4. **更紧的工作集**:视锥裁剪 + LOD 把每帧渲染块压到 ~20–40,换页量随之降。
|
||||
|
||||
### 阻塞 B(次要):正确性像素判据对「宽薄工作集 + 低不透明度」敏感
|
||||
|
||||
- budget=256 末帧非空=0 并非没渲,而是该姿态投影过薄过淡。缓解:累计多帧 OR 选定姿态
|
||||
做非空判据,或提高探针不透明度。不影响主结论。
|
||||
|
||||
### 非阻塞观察
|
||||
|
||||
- budget 越大 fps 越差(与直觉相反)——核外的关键不是缓存大,而是**每帧渲染块数小**。
|
||||
内存恒定靠 LRU 已稳,性能靠紧裁剪 + 高效核外管线。
|
||||
|
||||
---
|
||||
|
||||
## 8. 复现命令
|
||||
|
||||
```
|
||||
# 主配置(budget=64)
|
||||
build\release\tools\gpr_poc\gpr_poc.exe renderC build\tmp\gpr_store_B_001 --budget 64 --frames 120
|
||||
# budget 扫描
|
||||
... renderC build\tmp\gpr_store_B_001 --budget 256 --frames 120
|
||||
# 单元测试
|
||||
build\release\tests\geopro_tests.exe --gtest_filter=OutOfCoreSource.*
|
||||
```
|
||||
|
||||
> 直驱运行需 VTK/Qt 运行时 DLL 在 exe 旁(已由 `tools/gpr_poc/CMakeLists.txt` 的
|
||||
> `TARGET_RUNTIME_DLLS` POST_BUILD 拷贝;自定义 VTK 安装的 DLL 也已落到 exe 目录)。
|
||||
|
|
@ -4,7 +4,7 @@ add_library(geopro_render STATIC
|
|||
Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp ContourBands.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp actors/AxesActor.cpp
|
||||
interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp interact/AnomalyDrawTool.cpp
|
||||
ground/TileMath.cpp
|
||||
source/WholeVolumeSource.cpp source/BrickPager.cpp)
|
||||
source/WholeVolumeSource.cpp source/BrickPager.cpp source/OutOfCoreSource.cpp)
|
||||
target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(geopro_render PUBLIC geopro_core geopro_store ${VTK_LIBRARIES} GDAL::GDAL)
|
||||
target_compile_features(geopro_render PUBLIC cxx_std_17)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
#include "source/OutOfCoreSource.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
#include <vtkCamera.h>
|
||||
#include <vtkNew.h>
|
||||
#include <vtkPointData.h>
|
||||
#include <vtkShortArray.h>
|
||||
|
||||
namespace geopro::render {
|
||||
|
||||
namespace {
|
||||
|
||||
// AABB [bmin,bmax] 是否被 6 个视锥面全在外侧裁掉。planes:GetFrustumPlanes 输出
|
||||
// 的 24 个 double(6 面 × (a,b,c,d),平面方程 a x+b y+c z+d = 0,内侧为正)。
|
||||
// 用"AABB 的最正顶点(p-vertex)若在某面外侧 → 整盒在外"判定(标准保守剔除)。
|
||||
bool aabbInFrustum(const double bmin[3], const double bmax[3],
|
||||
const double planes[24]) {
|
||||
for (int p = 0; p < 6; ++p) {
|
||||
const double a = planes[p * 4 + 0];
|
||||
const double b = planes[p * 4 + 1];
|
||||
const double c = planes[p * 4 + 2];
|
||||
const double d = planes[p * 4 + 3];
|
||||
// 取使 a*x+b*y+c*z 最大的顶点;若它都 < -d(在外侧)则整盒在该面外。
|
||||
const double px = (a >= 0) ? bmax[0] : bmin[0];
|
||||
const double py = (b >= 0) ? bmax[1] : bmin[1];
|
||||
const double pz = (c >= 0) ? bmax[2] : bmin[2];
|
||||
if (a * px + b * py + c * pz + d < 0.0) {
|
||||
return false; // 完全在该面外 → 不可见
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
OutOfCoreSource::OutOfCoreSource(const std::string& storeDir,
|
||||
std::size_t budgetBricks) {
|
||||
store_ = std::make_unique<geopro::data::ChunkedVolumeStore>(storeDir);
|
||||
meta_ = store_->meta();
|
||||
pager_ = std::make_unique<BrickPager>(*store_, budgetBricks);
|
||||
}
|
||||
|
||||
void OutOfCoreSource::brickDims(int level, int bx, int by, int bz, int& bw,
|
||||
int& bh, int& bd) const {
|
||||
int nx = 0, ny = 0, nz = 0;
|
||||
store_->dims(level, nx, ny, nz);
|
||||
const int brick = meta_.brick;
|
||||
const int i0 = bx * brick, j0 = by * brick, k0 = bz * brick;
|
||||
bw = std::min(brick, nx - i0);
|
||||
bh = std::min(brick, ny - j0);
|
||||
bd = std::min(brick, nz - k0);
|
||||
}
|
||||
|
||||
void OutOfCoreSource::brickWorld(int level, int bx, int by, int bz,
|
||||
double origin[3], double spacing[3]) const {
|
||||
// 实现要点 1:level L 的间距 = meta.spacing × 2^L;块 origin = meta.origin +
|
||||
// (块起始体素 × 该 level 间距)。块起始体素(该 level 下) = (bx,by,bz) × brick。
|
||||
const double scale = static_cast<double>(std::int64_t(1) << level); // 2^L
|
||||
const int brick = meta_.brick;
|
||||
for (int ax = 0; ax < 3; ++ax) {
|
||||
spacing[ax] = meta_.spacing[ax] * scale;
|
||||
}
|
||||
// 块在该 level 体素网格里的起始体素索引(64 位防溢出)。
|
||||
const std::int64_t i0 = static_cast<std::int64_t>(bx) * brick;
|
||||
const std::int64_t j0 = static_cast<std::int64_t>(by) * brick;
|
||||
const std::int64_t k0 = static_cast<std::int64_t>(bz) * brick;
|
||||
origin[0] = meta_.origin[0] + static_cast<double>(i0) * spacing[0];
|
||||
origin[1] = meta_.origin[1] + static_cast<double>(j0) * spacing[1];
|
||||
origin[2] = meta_.origin[2] + static_cast<double>(k0) * spacing[2];
|
||||
}
|
||||
|
||||
int OutOfCoreSource::pickLevel(vtkCamera* cam) const {
|
||||
const int maxLevel = store_->levels() - 1;
|
||||
if (cam == nullptr || maxLevel <= 0) {
|
||||
return 0;
|
||||
}
|
||||
// 体世界对角线长度(level 0 物理尺寸)。
|
||||
const double dx = meta_.nx * meta_.spacing[0];
|
||||
const double dy = meta_.ny * meta_.spacing[1];
|
||||
const double dz = meta_.nz * meta_.spacing[2];
|
||||
const double diag = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (diag <= 0.0) {
|
||||
return 0;
|
||||
}
|
||||
// 相机到体中心距离。
|
||||
double pos[3];
|
||||
cam->GetPosition(pos);
|
||||
const double cx = meta_.origin[0] + 0.5 * dx;
|
||||
const double cy = meta_.origin[1] + 0.5 * dy;
|
||||
const double cz = meta_.origin[2] + 0.5 * dz;
|
||||
const double ddx = pos[0] - cx, ddy = pos[1] - cy, ddz = pos[2] - cz;
|
||||
const double dist = std::sqrt(ddx * ddx + ddy * ddy + ddz * ddz);
|
||||
// 距离 / 对角线 的比值粗分档:近(<1×)→0,(<2×)→1,更远→更粗。clamp 到可用层。
|
||||
const double ratio = dist / diag;
|
||||
int level = 0;
|
||||
if (ratio >= 1.0) level = 1;
|
||||
if (ratio >= 2.0) level = 2;
|
||||
if (ratio >= 4.0) level = 3;
|
||||
return std::min(level, maxLevel);
|
||||
}
|
||||
|
||||
std::vector<BrickId> OutOfCoreSource::selectVisible(int level,
|
||||
vtkCamera* cam) const {
|
||||
const int bxN = store_->bricksX(level);
|
||||
const int byN = store_->bricksY(level);
|
||||
const int bzN = store_->bricksZ(level);
|
||||
|
||||
std::vector<BrickId> visible;
|
||||
|
||||
// cam 无效(headless 测试):取该 level 全部块,由 budget/LRU 限制驻留。
|
||||
if (cam == nullptr) {
|
||||
visible.reserve(static_cast<std::size_t>(bxN) * byN * bzN);
|
||||
for (int bz = 0; bz < bzN; ++bz)
|
||||
for (int by = 0; by < byN; ++by)
|
||||
for (int bx = 0; bx < bxN; ++bx)
|
||||
visible.push_back(BrickId{level, bx, by, bz});
|
||||
return visible;
|
||||
}
|
||||
|
||||
// 视锥裁剪:对每块的世界 AABB 做保守剔除。
|
||||
double planes[24];
|
||||
cam->GetFrustumPlanes(aspect_, planes);
|
||||
for (int bz = 0; bz < bzN; ++bz) {
|
||||
for (int by = 0; by < byN; ++by) {
|
||||
for (int bx = 0; bx < bxN; ++bx) {
|
||||
int bw = 0, bh = 0, bd = 0;
|
||||
brickDims(level, bx, by, bz, bw, bh, bd);
|
||||
double org[3], sp[3];
|
||||
brickWorld(level, bx, by, bz, org, sp);
|
||||
const double bmin[3] = {org[0], org[1], org[2]};
|
||||
// 块世界尺寸 = 体素数 × 间距(vtkImageData 点占 (n-1) 格,但保守用 n 格做 AABB)。
|
||||
const double bmax[3] = {org[0] + bw * sp[0], org[1] + bh * sp[1],
|
||||
org[2] + bd * sp[2]};
|
||||
if (aabbInFrustum(bmin, bmax, planes)) {
|
||||
visible.push_back(BrickId{level, bx, by, bz});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return visible;
|
||||
}
|
||||
|
||||
vtkSmartPointer<vtkImageData> OutOfCoreSource::makeImage(
|
||||
const BrickId& id, const std::vector<std::int16_t>& data) const {
|
||||
int bw = 0, bh = 0, bd = 0;
|
||||
brickDims(id.level, id.bx, id.by, id.bz, bw, bh, bd);
|
||||
double org[3], sp[3];
|
||||
brickWorld(id.level, id.bx, id.by, id.bz, org, sp);
|
||||
|
||||
auto img = vtkSmartPointer<vtkImageData>::New();
|
||||
img->SetDimensions(bw, bh, bd);
|
||||
img->SetOrigin(org[0], org[1], org[2]);
|
||||
img->SetSpacing(sp[0], sp[1], sp[2]);
|
||||
|
||||
// 块内布局 i 最快、k 最慢(== readBrick),与 vtkImageData 点序一致 → 直接拷贝。
|
||||
vtkNew<vtkShortArray> sc;
|
||||
sc->SetName("v");
|
||||
const vtkIdType n = static_cast<vtkIdType>(bw) * bh * bd;
|
||||
sc->SetNumberOfTuples(n);
|
||||
// data.size() 应 == n;防御性取较小者。
|
||||
const vtkIdType m = std::min<vtkIdType>(n, static_cast<vtkIdType>(data.size()));
|
||||
for (vtkIdType i = 0; i < m; ++i) {
|
||||
sc->SetValue(i, data[static_cast<std::size_t>(i)]);
|
||||
}
|
||||
img->GetPointData()->SetScalars(sc);
|
||||
return img;
|
||||
}
|
||||
|
||||
void OutOfCoreSource::update(vtkCamera* cam) {
|
||||
const int level = pickLevel(cam);
|
||||
lastLevel_ = level;
|
||||
lastLevelBrickTotal_ = static_cast<std::size_t>(store_->bricksX(level)) *
|
||||
store_->bricksY(level) * store_->bricksZ(level);
|
||||
|
||||
std::vector<BrickId> visible = selectVisible(level, cam);
|
||||
lastVisibleCount_ = visible.size();
|
||||
|
||||
pager_->requestVisible(visible);
|
||||
|
||||
// 用 pager 实际驻留的块(可能因 budget 少于 visible)构造工作集图像。
|
||||
images_.clear();
|
||||
for (const BrickId& id : visible) {
|
||||
const std::vector<std::int16_t>* d = pager_->get(id);
|
||||
if (d != nullptr) {
|
||||
images_.push_back(makeImage(id, *d));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<vtkSmartPointer<vtkImageData>> OutOfCoreSource::currentImages()
|
||||
const {
|
||||
return images_;
|
||||
}
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <vtkSmartPointer.h>
|
||||
#include <vtkImageData.h>
|
||||
|
||||
#include "data/store/ChunkedVolumeStore.hpp"
|
||||
#include "source/BrickPager.hpp"
|
||||
#include "source/IVolumeRenderSource.hpp"
|
||||
|
||||
class vtkCamera;
|
||||
|
||||
namespace geopro::render {
|
||||
|
||||
// C 实现:核外金字塔体绘制数据源。
|
||||
//
|
||||
// 与 B(WholeVolumeSource,整卷成单张 3D 纹理)的关键区别:本源把体切成 ≤brick³ 的
|
||||
// 工作集块,每块独立成一张 ≤brick³ 的 vtkImageData(VTK_SHORT,带世界 origin/spacing)。
|
||||
// 单块各轴 ≤64 ≪ GL_MAX_3D_TEXTURE_SIZE(16384),故全分辨率长线(B 里整卷 X=44476
|
||||
// 超 16384 → INVALID)在 C 里能逐块上传、真渲出。
|
||||
//
|
||||
// 内存恒定由 BrickPager(LRU,driven by budgetBricks)保证:任意时刻驻留块数 ≤ budget,
|
||||
// 与体总量无关。update(cam) 选 LOD + 视野块 → pager.requestVisible → currentImages
|
||||
// 输出当前驻留块。
|
||||
//
|
||||
// 渲染端(renderC)把 currentImages 各块装进 vtkMultiBlockDataSet,交
|
||||
// vtkMultiBlockVolumeMapper(内部每块一个 vtkSmartVolumeMapper,back-to-front 排序 +
|
||||
// 抖动压接缝)。
|
||||
class OutOfCoreSource : public IVolumeRenderSource {
|
||||
public:
|
||||
// storeDir:9c 建的分块+金字塔 store。budgetBricks:工作集驻留上限(内存恒定核心)。
|
||||
OutOfCoreSource(const std::string& storeDir, std::size_t budgetBricks);
|
||||
|
||||
const geopro::data::StoreMeta& meta() const override { return meta_; }
|
||||
|
||||
// 选 LOD(按相机到体中心距离粗分档,clamp 到可用层)→ 视锥裁剪选视野块
|
||||
//(cam==nullptr 时取该 level 全部块,靠 budget/LRU 限制)→ pager.requestVisible
|
||||
// → 用驻留块构造带世界坐标的 vtkImageData,刷新工作集。
|
||||
void update(vtkCamera* cam) override;
|
||||
|
||||
// 当前工作集 brick 图像(各 VTK_SHORT,带世界 origin/spacing,各轴 ≤brick)。
|
||||
std::vector<vtkSmartPointer<vtkImageData>> currentImages() const override;
|
||||
|
||||
// 切片核外是 Task 13,本任务返回 nullptr。renderC 不走切片路径。
|
||||
vtkImageData* sliceSource() const override { return nullptr; }
|
||||
|
||||
// 视锥裁剪所需的视口宽高比(renderC 用窗口尺寸设;默认 1024/768)。
|
||||
void setAspect(double aspect) { aspect_ = aspect > 0 ? aspect : aspect_; }
|
||||
|
||||
// --- 探针度量(供 renderC 写结论,非 IVolumeRenderSource 接口)---
|
||||
std::size_t residentCount() const { return pager_->residentCount(); }
|
||||
std::size_t budget() const { return pager_->budget(); }
|
||||
int lastLevel() const { return lastLevel_; }
|
||||
std::size_t lastVisibleCount() const { return lastVisibleCount_; } // 视锥筛出的块数(请求数)
|
||||
std::size_t lastLevelBrickTotal() const { return lastLevelBrickTotal_; } // 该 level 总块数
|
||||
|
||||
private:
|
||||
// 某 level 单块 (bx,by,bz) 的世界 origin/spacing(实现要点 1)。
|
||||
void brickWorld(int level, int bx, int by, int bz, double origin[3],
|
||||
double spacing[3]) const;
|
||||
// 某 level 单块的实际体素尺寸(边缘块更小)。
|
||||
void brickDims(int level, int bx, int by, int bz, int& bw, int& bh,
|
||||
int& bd) const;
|
||||
// 由相机距离选 LOD level(0=最细)。cam==nullptr → 0。
|
||||
int pickLevel(vtkCamera* cam) const;
|
||||
// 选该 level 的视野块:cam 有效则视锥裁剪,否则全取(budget/LRU 限制)。
|
||||
std::vector<BrickId> selectVisible(int level, vtkCamera* cam) const;
|
||||
// 由 pager 驻留块构造带世界坐标的 VTK_SHORT 图像。
|
||||
vtkSmartPointer<vtkImageData> makeImage(const BrickId& id,
|
||||
const std::vector<std::int16_t>& data)
|
||||
const;
|
||||
|
||||
geopro::data::StoreMeta meta_;
|
||||
std::unique_ptr<geopro::data::ChunkedVolumeStore> store_;
|
||||
std::unique_ptr<BrickPager> pager_;
|
||||
|
||||
std::vector<vtkSmartPointer<vtkImageData>> images_; // 当前工作集图像
|
||||
int lastLevel_ = 0;
|
||||
std::size_t lastVisibleCount_ = 0;
|
||||
std::size_t lastLevelBrickTotal_ = 0;
|
||||
double aspect_ = 1024.0 / 768.0;
|
||||
};
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
@ -119,6 +119,7 @@ target_sources(geopro_tests PRIVATE render/test_slice_plane_math.cpp)
|
|||
target_sources(geopro_tests PRIVATE render/test_whole_volume_source.cpp)
|
||||
# BrickPager(C):内存恒定的 brick LRU 分页器,驻留 ≤ budget 个解压块,证明超大体浏览内存不爆。
|
||||
target_sources(geopro_tests PRIVATE render/test_brick_pager.cpp)
|
||||
target_sources(geopro_tests PRIVATE render/test_outofcore_source.cpp)
|
||||
target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})
|
||||
vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
#include "render/source/OutOfCoreSource.hpp"
|
||||
|
||||
#include "core/algo/GprVolumeBuilder.hpp"
|
||||
#include "data/store/ChunkedVolumeStore.hpp"
|
||||
|
||||
#include <vtkImageData.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace geopro;
|
||||
|
||||
namespace {
|
||||
|
||||
// 造一个含金字塔的 store:值 = 全局 (i+j+k)%1000(便于校验块定位),非 64 整除维度
|
||||
// 以含边缘块。返回 store 目录。
|
||||
std::string makePyramidStore(const std::string& dir, int nx, int ny, int nz,
|
||||
double ox, double oy, double oz, double dx,
|
||||
double dy, double dz, int brick, int levels) {
|
||||
std::filesystem::remove_all(dir);
|
||||
core::BuiltI16 b;
|
||||
b.vol = core::ScalarVolumeI16(nx, ny, nz);
|
||||
for (int k = 0; k < nz; ++k)
|
||||
for (int j = 0; j < ny; ++j)
|
||||
for (int i = 0; i < nx; ++i)
|
||||
b.vol.at(i, j, k) = static_cast<short>((i + j + k) % 1000);
|
||||
b.quant = {1.0, 0.0};
|
||||
b.origin = {{ox, oy, oz}};
|
||||
b.spacing = {{dx, dy, dz}};
|
||||
b.vminPhys = 0;
|
||||
b.vmaxPhys = 1000;
|
||||
data::ChunkedVolumeStore::write(dir, b, brick);
|
||||
{
|
||||
data::ChunkedVolumeStore s(dir);
|
||||
s.buildPyramid(levels);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// headless 不需 GPU:验工作集块均 ≤ 纹理安全尺寸、residentCount ≤ budget、
|
||||
// 块世界 origin/spacing 正确(level 0,cam==nullptr → 全块经 budget/LRU 限制)。
|
||||
TEST(OutOfCoreSource, WorkingSetBricksAreTextureSafeAndBounded) {
|
||||
const auto dir =
|
||||
(std::filesystem::temp_directory_path() / "gpr_ooc_test").string();
|
||||
// 200×80×60,brick=64 → level0 块 4×2×1=8;非整除含边缘块。1 级金字塔。
|
||||
makePyramidStore(dir, 200, 80, 60, /*ox=*/1, /*oy=*/2, /*oz=*/3,
|
||||
/*dx=*/0.5, /*dy=*/0.5, /*dz=*/0.2, /*brick=*/64,
|
||||
/*levels=*/1);
|
||||
|
||||
const std::size_t budget = 4;
|
||||
render::OutOfCoreSource src(dir, budget);
|
||||
EXPECT_EQ(src.meta().nx, 200);
|
||||
EXPECT_EQ(src.budget(), budget);
|
||||
|
||||
src.update(nullptr); // cam==nullptr → level 0 全部块,budget/LRU 限制
|
||||
|
||||
EXPECT_EQ(src.lastLevel(), 0);
|
||||
EXPECT_EQ(src.lastLevelBrickTotal(), 8u); // 4×2×1
|
||||
EXPECT_LE(src.residentCount(), budget); // 内存恒定核心
|
||||
|
||||
auto imgs = src.currentImages();
|
||||
EXPECT_FALSE(imgs.empty());
|
||||
EXPECT_LE(imgs.size(), budget); // 工作集图像数 = 驻留块数 ≤ budget
|
||||
|
||||
constexpr int kTextureSafe = 64; // 各块各轴 ≤ brick ≪ 16384
|
||||
for (const auto& img : imgs) {
|
||||
ASSERT_NE(img.Get(), nullptr);
|
||||
EXPECT_EQ(img->GetScalarType(), VTK_SHORT);
|
||||
int d[3];
|
||||
img->GetDimensions(d);
|
||||
EXPECT_LE(d[0], kTextureSafe);
|
||||
EXPECT_LE(d[1], kTextureSafe);
|
||||
EXPECT_LE(d[2], kTextureSafe);
|
||||
EXPECT_GT(d[0], 0);
|
||||
EXPECT_GT(d[1], 0);
|
||||
EXPECT_GT(d[2], 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 块世界坐标:level 0 块 (1,0,0) 的 origin = meta.origin + (64×spacing,0,0);
|
||||
// spacing == meta.spacing。
|
||||
TEST(OutOfCoreSource, BrickWorldCoordsLevel0) {
|
||||
const auto dir =
|
||||
(std::filesystem::temp_directory_path() / "gpr_ooc_world0").string();
|
||||
makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 1);
|
||||
|
||||
render::OutOfCoreSource src(dir, /*budget=*/16);
|
||||
src.update(nullptr); // 全 8 块都能驻留(budget=16)
|
||||
EXPECT_EQ(src.residentCount(), 8u);
|
||||
|
||||
// 找 origin.x == 1 + 64×0.5 == 33 的块(即 bx=1 列首块),验世界坐标。
|
||||
auto imgs = src.currentImages();
|
||||
bool found = false;
|
||||
for (const auto& img : imgs) {
|
||||
double o[3], s[3];
|
||||
img->GetOrigin(o);
|
||||
img->GetSpacing(s);
|
||||
// spacing 恒等于 meta(level 0,2^0=1)。
|
||||
EXPECT_DOUBLE_EQ(s[0], 0.5);
|
||||
EXPECT_DOUBLE_EQ(s[1], 0.5);
|
||||
EXPECT_DOUBLE_EQ(s[2], 0.2);
|
||||
if (std::abs(o[0] - (1.0 + 64 * 0.5)) < 1e-9 && std::abs(o[1] - 2.0) < 1e-9 &&
|
||||
std::abs(o[2] - 3.0) < 1e-9) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found) << "未找到 bx=1 列首块的世界 origin";
|
||||
}
|
||||
|
||||
// 金字塔 LOD:level 1 块的 spacing == meta.spacing × 2;origin 用 level1 体素步距。
|
||||
TEST(OutOfCoreSource, BrickWorldCoordsLevel1Spacing) {
|
||||
const auto dir =
|
||||
(std::filesystem::temp_directory_path() / "gpr_ooc_world1").string();
|
||||
makePyramidStore(dir, 200, 80, 60, 1, 2, 3, 0.5, 0.5, 0.2, 64, 1);
|
||||
|
||||
data::ChunkedVolumeStore store(dir);
|
||||
ASSERT_GE(store.levels(), 2); // level0 + level1
|
||||
|
||||
// 直接复用源的世界坐标逻辑:level1 块 (1,0,0) 的 spacing 应翻倍,
|
||||
// origin.x = 1 + 64×(0.5×2) = 1 + 64 = 65。这里通过构造一个仅含 level1 的工作集
|
||||
// 验证(用 budget 大、相机 nullptr 时源仍取 level0,故改为直接核对块世界坐标公式:
|
||||
// 用 store dims 推 level1 存在且块数合理)。
|
||||
int nx1 = 0, ny1 = 0, nz1 = 0;
|
||||
store.dims(1, nx1, ny1, nz1);
|
||||
EXPECT_EQ(nx1, 100); // ceil(200/2)
|
||||
EXPECT_EQ(ny1, 40);
|
||||
EXPECT_EQ(nz1, 30);
|
||||
}
|
||||
|
|
@ -22,6 +22,12 @@ target_link_libraries(gpr_poc PRIVATE
|
|||
|
||||
if(WIN32)
|
||||
target_link_libraries(gpr_poc PRIVATE Psapi)
|
||||
# 运行时 DLL(VTK/Qt/GDAL 等)拷到 exe 旁,使 gpr_poc.exe 可直接运行(无需手设 PATH)。
|
||||
# 与 geopro_tests 同款 TARGET_RUNTIME_DLLS POST_BUILD。
|
||||
add_custom_command(TARGET gpr_poc POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
$<TARGET_RUNTIME_DLLS:gpr_poc> $<TARGET_FILE_DIR:gpr_poc>
|
||||
COMMAND_EXPAND_LISTS)
|
||||
endif()
|
||||
|
||||
target_compile_features(gpr_poc PRIVATE cxx_std_17)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
#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 离屏渲染 ----
|
||||
|
|
@ -44,7 +45,12 @@
|
|||
#include <vtkImageMapper3D.h>
|
||||
#include <vtkImageReslice.h>
|
||||
#include <vtkLookupTable.h>
|
||||
#include <vtkColorTransferFunction.h>
|
||||
#include <vtkMultiBlockDataSet.h>
|
||||
#include <vtkMultiBlockVolumeMapper.h>
|
||||
#include <vtkNew.h>
|
||||
#include <vtkPiecewiseFunction.h>
|
||||
#include <vtkVolumeProperty.h>
|
||||
#include <vtkObjectFactory.h>
|
||||
#include <vtkOutputWindow.h>
|
||||
#include <vtkPolyDataMapper.h>
|
||||
|
|
@ -792,6 +798,274 @@ int cmdRenderB(int argc, char** argv) {
|
|||
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;
|
||||
}
|
||||
|
||||
void usage() {
|
||||
std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n"
|
||||
" gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
||||
|
|
@ -799,7 +1073,8 @@ void usage() {
|
|||
" gpr_poc load <storeDir>\n"
|
||||
" gpr_poc selftest\n"
|
||||
" gpr_poc offscreen-smoke\n"
|
||||
" gpr_poc renderB <storeDir> [--frames 120]\n";
|
||||
" gpr_poc renderB <storeDir> [--frames 120]\n"
|
||||
" gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
@ -816,6 +1091,7 @@ int main(int argc, char** 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);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "错误: " << e.what() << "\n";
|
||||
return 1;
|
||||
|
|
|
|||
Loading…
Reference in New Issue