From 824898a65cbcd27da20b8ba9dd996ac74012a56a Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 23 Jun 2026 17:49:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(poc):=20gpr=5Fpoc=20renderLOD=20=E6=8E=A2?= =?UTF-8?q?=E9=92=88=E9=AA=8C=E8=AF=81=20LOD-fps=20=E5=85=A8=E9=87=8F?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E6=B8=B2=E6=9F=93=E5=8F=AF=E8=A1=8C=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 四件事全离屏双闸实测(本机 RTX3060): (a)粗层概览 level2 ~752fps (b)全分辨率局部 level0 ~380fps (c)LOD 切换过渡切换帧 ~5.5ms 无可感知卡顿 (d)存 3 张对比 PNG。 双闸:无 3D 纹理维度错误 + 三段均回读非空像素,fps 可信。 判据:两端均达交互级且切换无卡顿 -> LOD-based C 路线钉死可行。 最低配未验,需目标机复测。tools CMake 加 IOImage 供 PNG 截图。 --- .superpowers/sdd/task-12c-report.md | 80 ++++ .../plans/poc-lod-shots/lod-fullres-local.png | Bin 0 -> 41211 bytes .../plans/poc-lod-shots/lod-overview.png | Bin 0 -> 7006 bytes .../poc-lod-shots/lod-transition-mid.png | Bin 0 -> 3237 bytes docs/superpowers/plans/poc-results-C.md | 22 + tools/gpr_poc/CMakeLists.txt | 2 +- tools/gpr_poc/main.cpp | 448 +++++++++++++++++- 7 files changed, 550 insertions(+), 2 deletions(-) create mode 100644 .superpowers/sdd/task-12c-report.md create mode 100644 docs/superpowers/plans/poc-lod-shots/lod-fullres-local.png create mode 100644 docs/superpowers/plans/poc-lod-shots/lod-overview.png create mode 100644 docs/superpowers/plans/poc-lod-shots/lod-transition-mid.png diff --git a/.superpowers/sdd/task-12c-report.md b/.superpowers/sdd/task-12c-report.md new file mode 100644 index 0000000..f9876b2 --- /dev/null +++ b/.superpowers/sdd/task-12c-report.md @@ -0,0 +1,80 @@ +# Task 12c 报告:LOD-fps 探针(全量交互渲染最后一根链子) + +## 状态 + +**完成 / PASS** —— 四件事(a/b/c/d)全做,双闸通过(无纹理维度错误 + 三段均回读非空像素), +真实实测,未编造。LOD-based C 路线在本机判据下钉死可行。 + +## 实测数字(本机 RTX 3060 Laptop GPU,离屏,frames=120,多次重跑稳定) + +| 项 | 维度 | 结果 | 交互级判据 | +|---|---|---|---| +| (a) 粗层概览 fps | level2 整卷 11119×8×41 (~3.6M 体素) | **~752 fps**(多跑 590~759) | ✔ 远超 ≥30 | +| (b) 全分辨率局部 fps | level0 局部 256×29×162 (~120 万体素,4 brick 列) | **~380 fps**(多跑 374~422) | ✔ 远超 ≥30 | +| (c) LOD 切换过渡 | 切换帧 60/120,从远观(level2)dolly 拉近到近观局部(level0) | 平均 **1.09ms/帧**,切换帧 **~5.5ms**(尖峰 ~6×邻帧),最大 ~6.95ms | 无可感知卡顿 ✔ | + +- **粗层概览 fps**:~752 fps(达交互级 ✔) +- **全分辨率局部 fps**:~380 fps(达交互级 ✔) +- **LOD 切换过渡帧耗时 / 是否卡顿**:切换帧 ~5.5ms(仍 <1 个 60Hz 帧 16.7ms)→ **无可感知卡顿** +- **截图路径**:`docs/superpowers/plans/poc-lod-shots/` + - `lod-overview.png`(level2 整线概览,全 2200m 线呈细带) + - `lod-fullres-local.png`(level0 局部,全分辨率板面有细节) + - `lod-transition-mid.png`(切换后推近的过渡中间帧) +- **是否都达交互级**:**是**。(a)/(b) 均 >>30fps;(c) 切换无可感知卡顿。 + +## 设计与诚实测法 + +- 在真实金字塔 store(`gpr_poc build ... --levels 3`,level0=44476×29×162, + level1=22238×15×81,level2=11119×8×41,level3=5560×4×21)上跑,非合成。 +- (a)/(b):把对应 level 的所有 brick 重组成单张 VTK_SHORT vtkImageData + (逻辑同 `WholeVolumeSource`,按 level 维度 + spacing×2^level / 局部段 X 偏移), + 喂 `buildVoxelI16FromImage`(SmartVolumeMapper,GPU 路径),旋相机 120 帧测 fps。 + level2/局部段单轴均 <16384 → 单 3D 纹理可成,无纹理墙。 +- (c):同一窗口,相机从远观(level2 整卷)dolly 拉近;第 60 帧跨越 LOD 切换那一下 + 把体从 level2 概览换成 level0 局部 + 焦点移到局部段中心,**逐帧记帧耗时**, + 标切换帧尖峰。这是审核人加的验收点①(测切换动态,非两端静态)。 +- (d):`vtkWindowToImageFilter`+`vtkPNGWriter` 存 3 张 PNG,供人眼判 + “概览糊→拉近清晰”(审核人验收点②)。 +- **双闸(同 9c,绝不把空纹理假帧率当性能)**: + ① `CapturingOutputWindow` 捕获 3D 纹理维度错误(实测=否); + ② 真实回读前缓冲像素,统计非背景像素(概览 1889 / 局部 167612 / 过渡 21924, + 三段均非空)。两闸全过,fps 可信。 + +## 卡顿判据说明(避免误报) + +切换帧含一次性建 actor / 换 mapper 输入,~5.5ms,是邻帧(~0.9ms)的 ~6×;但绝对值 +仍 < 1 个 60Hz 帧(16.7ms),人眼不可感。故采用**绝对耗时判据**:切换帧 >33ms(2 帧) +才记“可感知卡顿”,16.7~33ms 记“轻微抖动”,亚毫秒基线下尖峰倍数虽大但绝对值低不算 +卡顿。本机切换帧 ~5.5ms → 无可感知卡顿。 + +## 判据结论 + +粗层概览 + 全分辨率局部**都达交互级**(≥30fps,远超)且切换**无不可接受卡顿** +→ 命中 brief 第一条判据:**LOD-based C 路线钉死可行**。 + +对照 12b:整卷全分辨率 ray cast(2.08 亿体素)~10fps 是硬上限;本探针证实 +“渲更少体素 = LOD” 这根杠杆有效——粗层 ~752fps、全分辨率局部 ~380fps,两端都远 +在交互级,且 LOD 切换瞬态 ~5.5ms 无卡顿。 + +## 最低配未验声明(审核人验收点③) + +本探针**仅在本机(RTX 3060 Laptop GPU,NVIDIA 555.97,OpenGL 4.5)跑得上限数字**。 +**最低配机器未验证**,需用户在目标机跑 `gpr_poc renderLOD ` 或提供型号后再评估。 +本机数字是上限,最低配可能更低。 + +## 进程峰值内存 + +~99 MB(探针逐 level 重组单张 image,未常驻整卷;level0 局部仅取 4 brick 列)。 + +## Concerns + +1. **截图视觉偏暗/偏细**:体绘制 `kMaxOpacity=0.15`(复用探针传函)+ 整线物理纵横比 + 极扁(2200m × ~1.5m × 8m),故概览图中整线呈一条细带、过渡中间帧呈小斜板。 + 这是物理真实呈现(整线本就是长薄带),非渲染缺陷;但作为“人眼判可接受度”素材 + 偏素净。若需更醒目的生产视觉,需后续调传函不透明度/着色与取景,超出探针范畴(YAGNI)。 +2. **(c) 为单次脚本化切换**:测的是“从 level2 直切 level0 局部”一次硬切的瞬态; + 生产里多级连续 LOD/视野自适应的换页节奏、预取与 morphing/淡入是探针过了之后的 + 工程(brief 明确不在本探针范围)。 +3. **(b) 局部仅取 4 brick 列(256 体素宽)**:证“全分辨率局部块快”;若生产需更宽的 + 全分辨率窗口(仍需 <16384 或分区/分块),fps 会随体素数下降,需届时按窗口大小复测。 +4. **最低配仍是最大未知**(见上声明)。 diff --git a/docs/superpowers/plans/poc-lod-shots/lod-fullres-local.png b/docs/superpowers/plans/poc-lod-shots/lod-fullres-local.png new file mode 100644 index 0000000000000000000000000000000000000000..0fab3464c3c85619282d0474e90d2156774dbcba GIT binary patch literal 41211 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 literal 0 HcmV?d00001 diff --git a/docs/superpowers/plans/poc-lod-shots/lod-overview.png b/docs/superpowers/plans/poc-lod-shots/lod-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..8daf117debdb0cfd24608c0d75f9d7a2bef5293d GIT binary patch literal 7006 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 literal 0 HcmV?d00001 diff --git a/docs/superpowers/plans/poc-lod-shots/lod-transition-mid.png b/docs/superpowers/plans/poc-lod-shots/lod-transition-mid.png new file mode 100644 index 0000000000000000000000000000000000000000..c4e48113b6dc795167de5a9c2ae123f94f355ebf GIT binary patch literal 3237 zcmeIx|1;Zn90%}s;>&2}${l7=b>au2uF=KZq9j_eluZyssYIQ%HL8`^d}}JE%Gw=P z-$zCqDVs&BW$|SyZriEaFsX7&Q(u!J#FsSvG>PQ1bbmvC%=?$u``$l1?{$yY>wPPl zO0heHJ_G=;3qM1q0bpsmS^yiQseHUpk_5o}m&3`Y;_{i>vQv300gl!)UN!+<9(WkW z<<8|+*Njpt4h05SlbKe)|BJ!o zPdjQcg0Fp9 zVBo{+@P&~g-Vn@dk3bS$hFUiUOdfbxk*U~e#F96uHq_F(IHeSTY}fk99_7)5?($h3 zrFKl3UsOSGIn&RW;sV~7%k0fNNVB-QNw=`7v3 zAa6Uv-immGL&)fTthwl9yV+mjow~$;NU|G_4h5s`%#?EVs4ZxXFtvqXKnqZF&$Y8c zmFK4Fpi9=ue5oKdRAdkpyY}(!u(O3?q@48wt{b6wQEgA%^qf_zuOdQfEJ5)zLEn}> zaz2aRt(QM&Rwx(_@tNEQHqk@-jopgBC6C}WY8#Eqxja(6>m2mNwLZ?TQV;m?ND!iY zro_cU+&mG{f9(YPSl|;zo}!HLgRrljdxl088ruD$MaCG7E+&|@&XA-i(uNHNgBGNl zcxL+KY}wb%bDOFCg#&bbn~#^{Prja?JAapva+{}I$S#~1Wm&NrOJ%!xnG#7X|9ksc`Bz7e$sD=R2$D$7MkYdtP zs7ft|^=xTW=BE56>qrW-MI^oCZUNHi{VS-D&uBxH2qUHkV6{2Lu5 z?sNbEJqMXOY*VUsRJzRm3YYTb0+*K8`tAM*K&&`d44Ue5wsLU}SV1Fk@Z@0(Mi6JM zc@cFdcepb(UuTrbxy5&F0jOwF2^V)ndc*g#%Wc6Deg?GY_07XOOI!R}1B%^p9OmX| z-T5q&r=)CIJKkgrJeqvQs^L9($UiJrlp#Z$8`?2Gu yp>BUr)2cWA$+LThzu6pR7BCB#1^yQ>9^)cnHGTQ*CuJ^gAHu_^33ms(2 个 60Hz 帧)才记可感知卡顿;16.7~33ms 记轻微抖动;亚毫秒基线下尖峰倍数大但绝对值低不算卡顿。 +- 双闸:纹理维度错误=否;三段均渲出非空像素=是(概览 1889 / 局部 167612 / 过渡 21924)。 +- 截图(人眼判“概览糊→拉近清晰”):docs/superpowers/plans/poc-lod-shots/lod-overview.png、lod-fullres-local.png、lod-transition-mid.png +- 进程峰值内存: 99.2266 MB + +## 判据结论 +粗层概览 + 全分辨率局部【都达交互级】且切换【无不可接受卡顿】→ LOD-based C 路线钉死可行。 + +**最低配未验声明**:本探针仅在本机(RTX 3060)跑得上限数字,最低配机器未验证,需用户在目标机跑或提供型号。 diff --git a/tools/gpr_poc/CMakeLists.txt b/tools/gpr_poc/CMakeLists.txt index 990a958..66cd998 100644 --- a/tools/gpr_poc/CMakeLists.txt +++ b/tools/gpr_poc/CMakeLists.txt @@ -8,7 +8,7 @@ find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel RenderingCore RenderingOpenGL2 RenderingVolume RenderingVolumeOpenGL2 - ImagingCore InteractionStyle GUISupportQt) + ImagingCore InteractionStyle GUISupportQt IOImage) add_executable(gpr_poc main.cpp) diff --git a/tools/gpr_poc/main.cpp b/tools/gpr_poc/main.cpp index ee62679..e3afe16 100644 --- a/tools/gpr_poc/main.cpp +++ b/tools/gpr_poc/main.cpp @@ -55,14 +55,18 @@ #include #include #include +#include +#include #include #include #include #include +#include #include #include #include #include +#include namespace fs = std::filesystem; using geopro::tools::Probe; @@ -1317,6 +1321,446 @@ int cmdRenderCPartitioned(int argc, char** argv) { return volFpsValid ? 0 : 1; } +// ============================================================================ +// LOD-fps 探针(POC-C 最后一根链子,Task 12c) +// ============================================================================ +// +// 12b 已证整卷全分辨率 ray cast(2.08 亿体素)~10fps 是硬上限,fps 杠杆只有 LOD +// (渲更少体素)。本探针在【真实金字塔 store】上验四件事,全离屏、双闸防假帧率: +// (a) 粗层概览 fps:level2 整卷(单轴 <16384 → 单 SmartVolumeMapper)。 +// (b) 全分辨率局部 fps:level0 一段 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 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(1 << level); // 2^level + + auto img = vtkSmartPointer::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 arr; + arr->SetName("v"); + arr->SetNumberOfTuples(static_cast(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 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(k0 + kk); + for (int jj = 0; jj < bh; ++jj) { + const vtkIdType gj = static_cast(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 image(X 维 = bxCount*brick ≤ 几百,远 <16384,单 3D 纹理)。 +// Origin 沿 X 偏移到该段起点,spacing 用 level0 原值。 +vtkSmartPointer 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::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 arr; + arr->SetName("v"); + arr->SetNumberOfTuples(static_cast(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 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(k0 + kk); + for (int jj = 0; jj < bh; ++jj) { + const vtkIdType gj = static_cast(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::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 w2i; + w2i->SetInput(rw); + w2i->SetInputBufferTypeToRGB(); + w2i->ReadFrontBufferOff(); + w2i->Update(); + vtkNew 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 [--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::New(); + vtkOutputWindow::SetInstance(capWin); + + // ---- (a) 粗层概览 fps:level2 整卷 ---- + const int ovLevel = std::min(2, totLevels - 1); + std::cout << "[renderLOD] (a) 建 level" << ovLevel << " 整卷 image...\n"; + vtkSmartPointer ovImg = buildLevelImage(store, ovLevel, m); + int ovNx, ovNy, ovNz; + store.dims(ovLevel, ovNx, ovNy, ovNz); + + auto rwOv = makeOffscreenWindow(winW, winH); + vtkNew renOv; + renOv->SetBackground(0.0, 0.0, 0.0); + rwOv->AddRenderer(renOv); + vtkSmartPointer ovVol = + geopro::render::buildVoxelI16FromImage(ovImg.Get(), m.quant, cs, vmin, + vmax); + renOv->AddVolume(ovVol); + // 先测 fps(benchVolumeFps 内部会 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) 全分辨率局部 fps:level0 一段 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 locImg = + buildLocalLevel0Image(store, m, bx0, localBx); + int locDims[3]; + locImg->GetDimensions(locDims); + + auto rwLoc = makeOffscreenWindow(winW, winH); + vtkNew renLoc; + renLoc->SetBackground(0.0, 0.0, 0.0); + rwLoc->AddRenderer(renLoc); + vtkSmartPointer 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 renTr; + renTr->SetBackground(0.0, 0.0, 0.0); + rwTr->AddRenderer(renTr); + + // 远观体 = level2 概览(新建一份,避免与 (a) 共享 actor 状态)。 + vtkSmartPointer farVol = + geopro::render::buildVoxelI16FromImage(ovImg.Get(), m.quant, cs, vmin, + vmax); + // 近观体 = level0 局部(复用 (b) 的 image)。 + vtkSmartPointer 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 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; +} + void usage() { std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n" " gpr_poc build [--line 001] [--cellXY 0.2] " @@ -1326,7 +1770,8 @@ void usage() { " gpr_poc offscreen-smoke\n" " gpr_poc renderB [--frames 120]\n" " gpr_poc renderC [--budget 64] [--frames 120]\n" - " gpr_poc renderC-partitioned [--frames 120]\n"; + " gpr_poc renderC-partitioned [--frames 120]\n" + " gpr_poc renderLOD [--frames 120]\n"; } } // namespace @@ -1346,6 +1791,7 @@ int main(int argc, char** argv) { if (cmd == "renderC") return cmdRenderC(argc, argv); if (cmd == "renderC-partitioned") return cmdRenderCPartitioned(argc, argv); + if (cmd == "renderLOD") return cmdRenderLOD(argc, argv); } catch (const std::exception& e) { std::cerr << "错误: " << e.what() << "\n"; return 1;