From 0da5accebe5cf4f95b24048a87eba2feac5f1291 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 24 Jun 2026 09:09:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(lod):=20selectLod=20=E6=9C=80=E7=B2=97?= =?UTF-8?q?=E5=B1=82=E5=85=9C=E5=BA=95=E8=A3=81=E5=89=AA,=E4=BF=9D?= =?UTF-8?q?=E8=AF=81=E8=BF=94=E5=9B=9E=E5=8C=BA=E9=97=B4=E6=81=92=E4=B8=8D?= =?UTF-8?q?=E8=B6=85=20maxTextureDim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 最粗层 fits||maxLevel 分支原先无条件返回,不校验 fits;合并体最粗层 全可见区间仍 >maxTextureDim 时返回区间会突破单纹理硬上限,C2 重组撞 GL 16384 纹理墙退回慢路。 新增 clampAxisToMaxTexture,在所有返回路径前按可见中心对称裁三轴到 重组单纹理各轴 <= maxTextureDim 的子区间;brick 本身 > maxTextureDim 的退化情形返回单块并由 C2 体素级再裁(契约见 hpp)。补 3 例边界测试 (brick>maxTextureDim、最粗层整卷超限、改写 RespectsMaxTextureDimAlways 为 brick!=maxTextureDim),原有用例全绿。 --- src/render/lod/ViewAdaptiveLodPolicy.cpp | 28 ++++++++ src/render/lod/ViewAdaptiveLodPolicy.hpp | 9 +++ tests/render/test_view_adaptive_lod.cpp | 83 ++++++++++++++++++++---- 3 files changed, 107 insertions(+), 13 deletions(-) diff --git a/src/render/lod/ViewAdaptiveLodPolicy.cpp b/src/render/lod/ViewAdaptiveLodPolicy.cpp index c7dcb26..f26994f 100644 --- a/src/render/lod/ViewAdaptiveLodPolicy.cpp +++ b/src/render/lod/ViewAdaptiveLodPolicy.cpp @@ -160,6 +160,28 @@ int axisTexture(int b0, int b1, int brick, int dimL) { return std::max(0, end - start); } +// 把某轴的可见 brick 区间 [b0,b1) 裁到“重组单纹理 ≤ maxTextureDim”的子区间。 +// 以可见区间中心为锚向两侧对称收缩 brick,宁可概览只显中心部分也绝不超限。 +// 返回的子区间满足 axisTexture(...) ≤ maxTextureDim;恒保 b0 maxTextureDim 时,单块体素数已 > maxTextureDim,brick 粒度 +// 无法表达;此时返回单块并由 C2 重组时按体素上限再裁(见 hpp 契约),即重组实际 +// 体素数 = min(单块体素, maxTextureDim),仍恒 ≤ maxTextureDim。 +void clampAxisToMaxTexture(int& b0, int& b1, int brick, int dimL, + int maxTextureDim) { + if (axisTexture(b0, b1, brick, dimL) <= maxTextureDim) return; + // 该 level 该轴每块 brick 体素,能容纳的最多整块数(至少 1 块)。 + const int maxBricks = std::max(1, maxTextureDim / std::max(1, brick)); + const int span = b1 - b0; + if (span <= maxBricks) return; // 块数已够小(仅 brick>maxTextureDim 时到这)。 + // 以可见区间中心为锚,对称取 maxBricks 块。 + const int center = (b0 + b1) / 2; + int newB0 = center - maxBricks / 2; + newB0 = std::clamp(newB0, b0, b1 - maxBricks); + b0 = newB0; + b1 = newB0 + maxBricks; +} + } // namespace LodSelection selectLod(const VolumeView& vol, const CameraView& cam, @@ -216,6 +238,12 @@ LodSelection selectLod(const VolumeView& vol, const CameraView& cam, const bool fits = tx <= maxTextureDim && ty <= maxTextureDim && tz <= maxTextureDim; if (fits || level == maxLevel) { + // 最粗层兜底:即使全可见区间仍 >maxTextureDim,也必须把区间裁到 ≤maxTextureDim + // 的中心子区间——单纹理快路绝不能 >maxTextureDim(撞 GL 16384 纹理墙)。 + // 任何返回路径都先过 clamp,保证返回区间各轴重组恒 ≤ maxTextureDim。 + clampAxisToMaxTexture(bx0, bx1, vol.brick, dimX, maxTextureDim); + clampAxisToMaxTexture(by0, by1, vol.brick, dimY, maxTextureDim); + clampAxisToMaxTexture(bz0, bz1, vol.brick, dimZ, maxTextureDim); out.level = level; out.bx0 = bx0; out.bx1 = bx1; diff --git a/src/render/lod/ViewAdaptiveLodPolicy.hpp b/src/render/lod/ViewAdaptiveLodPolicy.hpp index 390240a..17114cb 100644 --- a/src/render/lod/ViewAdaptiveLodPolicy.hpp +++ b/src/render/lod/ViewAdaptiveLodPolicy.hpp @@ -58,6 +58,15 @@ struct LodSelection { // 这样:远观→粗层(区间≈全体)、近观→细层(区间是视锥内小块),且各轴恒 ≤ maxTextureDim。 // 视距-层单调:相机拉近 → worldPerPixel 与视距均减 → Lmin 不增 → level 不增。 // 体在视锥外 → empty=true。 +// +// **硬上限保证(架构命脉)**:返回的 brick 区间在选定 level 重组单纹理时,各轴体素数 +// 恒 ≤ maxTextureDim——即使最粗层全可见区间仍 >maxTextureDim,也按可见中心裁成刚好 +// ≤maxTextureDim 的中心子区间(宁可概览只显中心部分,绝不超限)。单纹理快路绝不能 +// >maxTextureDim,否则撞 GL 纹理墙退回慢路。 +// 退化情形:当 brick 本身 > maxTextureDim(单块体素数已超限,brick 粒度无法表达更小区间) +// 时返回单块,C2 重组须按体素上限再裁——重组实际体素数 = min(选定区间体素, maxTextureDim), +// 即重组纹理某轴起点对齐 b0*brick、终点取 min(b1*brick, dimL, b0*brick + maxTextureDim), +// 仍恒 ≤ maxTextureDim。 LodSelection selectLod(const VolumeView& vol, const CameraView& cam, int maxTextureDim = 16384); diff --git a/tests/render/test_view_adaptive_lod.cpp b/tests/render/test_view_adaptive_lod.cpp index 7a27e3c..1c7c466 100644 --- a/tests/render/test_view_adaptive_lod.cpp +++ b/tests/render/test_view_adaptive_lod.cpp @@ -113,27 +113,84 @@ TEST(ViewAdaptiveLod, NearViewPicksFineLevelSmallRegion) { EXPECT_TRUE(yShrunk || zShrunk); } +// 契约里的“重组单纹理某轴体素数”:起点对齐 b0*brick,终点取 +// min(b1*brick, dimL, b0*brick + maxTextureDim)(含 brick>maxTextureDim 的体素级再裁)。 +// 生产代码保证此值恒 ≤ maxTextureDim;测试用同一公式焊死不变量。 +static int reconstructedAxisTex(int b0, int b1, int brick, int dimL, + int maxTextureDim) { + const int start = b0 * brick; + const int end = std::min({b1 * brick, dimL, start + maxTextureDim}); + return std::max(0, end - start); +} + +static int dimAt(int n, int level) { + return (n + (1 << level) - 1) >> level; +} + // ── 选定层重组区间各轴恒 ≤ maxTextureDim(小 maxTextureDim 强制升层/缩区间)── +// brick=64、maxTextureDim=128(brick≠maxTextureDim,不靠 64=64 侥幸):允许 2 块/轴, +// 多于此须按中心裁掉,真正走多块 clamp 算术(而非边界相等巧合)。 TEST(ViewAdaptiveLod, RespectsMaxTextureDimAlways) { - const VolumeView v = makeVol(); - // 多组视距 × 极小 maxTextureDim,断言选定层重组单纹理各轴 ≤ maxTextureDim。 + const VolumeView v = makeVol(); // brick=64 + const int kMax = 128; // ≠ brick;容 2 块,逼出多块中心裁剪 for (double dist : {60.0, 300.0, 1500.0, 8000.0, 40000.0}) { const CameraView c = lookFromX(v, dist); - const LodSelection sel = selectLod(v, c, /*maxTextureDim=*/64); + const LodSelection sel = selectLod(v, c, kMax); if (sel.empty) continue; - // 选定 level 下,区间重组单纹理各轴体素数 = (块数 × brick) 截到该 level 维度。 - const int dimL = (512 + (1 << sel.level) - 1) >> sel.level; - auto axisTex = [&](int b0, int b1) { - const int start = b0 * v.brick; - const int end = std::min(b1 * v.brick, dimL); - return end - start; - }; - EXPECT_LE(axisTex(sel.bx0, sel.bx1), 64); - EXPECT_LE(axisTex(sel.by0, sel.by1), 64); - EXPECT_LE(axisTex(sel.bz0, sel.bz1), 64); + const int dimL = dimAt(512, sel.level); + EXPECT_LE(reconstructedAxisTex(sel.bx0, sel.bx1, v.brick, dimL, kMax), kMax); + EXPECT_LE(reconstructedAxisTex(sel.by0, sel.by1, v.brick, dimL, kMax), kMax); + EXPECT_LE(reconstructedAxisTex(sel.bz0, sel.bz1, v.brick, dimL, kMax), kMax); + // 区间合法、非空。 + EXPECT_LT(sel.bx0, sel.bx1); + EXPECT_LT(sel.by0, sel.by1); + EXPECT_LT(sel.bz0, sel.bz1); } } +// ── brick > maxTextureDim:单块体素数已超限,仍必须保证重组 ≤ maxTextureDim ────── +// brick=128、maxTextureDim=64:brick 粒度无法表达更小区间 → 返回单块 + C2 体素级再裁, +// 重组实际体素数 = min(单块, maxTextureDim) = 64 ≤ 64。各视距各轴恒不超限。 +TEST(ViewAdaptiveLod, BrickLargerThanMaxTextureDimStillClamped) { + const VolumeView v = makeVol(/*n=*/512, /*brick=*/128, /*levels=*/4); + const int kMax = 64; // < brick=128 + for (double dist : {60.0, 300.0, 1500.0, 8000.0, 40000.0}) { + const CameraView c = lookFromX(v, dist); + const LodSelection sel = selectLod(v, c, kMax); + if (sel.empty) continue; + const int dimL = dimAt(512, sel.level); + EXPECT_LE(reconstructedAxisTex(sel.bx0, sel.bx1, v.brick, dimL, kMax), kMax); + EXPECT_LE(reconstructedAxisTex(sel.by0, sel.by1, v.brick, dimL, kMax), kMax); + EXPECT_LE(reconstructedAxisTex(sel.bz0, sel.bz1, v.brick, dimL, kMax), kMax); + EXPECT_LT(sel.bx0, sel.bx1); // 恒至少一块 + } +} + +// ── 最粗层整卷 > maxTextureDim:远观也必须裁成子区间,绝不突破硬上限 ──────────── +// nx=156544、levels=3 → 最粗层 dim=ceil(156544/4)=39136 > 16384;旧实现在最粗层无条件 +// 返回会突破上限。修复后远观应裁成 ≤16384 的中心子区间,empty=false。 +TEST(ViewAdaptiveLod, CoarsestLevelOverflowIsClampedNotBreached) { + VolumeView v{}; + v.nx = v.ny = v.nz = 156544; + v.brick = 64; + v.levels = 3; // L0..L2;最粗层 dim = ceil(156544/4) = 39136 > 16384 + v.origin[0] = v.origin[1] = v.origin[2] = 0.0; + v.spacing[0] = v.spacing[1] = v.spacing[2] = 1.0; + v.exagg = 1.0; + // 极远观:worldPerPixel 大 → 选最粗层;整卷入视锥。 + const CameraView c = lookFromX(v, 5.0e6, 30.0, 1080); + const int kMax = 16384; + const LodSelection sel = selectLod(v, c, kMax); + ASSERT_FALSE(sel.empty); + const int dimL = dimAt(v.nx, sel.level); + EXPECT_LE(reconstructedAxisTex(sel.bx0, sel.bx1, v.brick, dimL, kMax), kMax); + EXPECT_LE(reconstructedAxisTex(sel.by0, sel.by1, v.brick, dimL, kMax), kMax); + EXPECT_LE(reconstructedAxisTex(sel.bz0, sel.bz1, v.brick, dimL, kMax), kMax); + EXPECT_LT(sel.bx0, sel.bx1); + EXPECT_LT(sel.by0, sel.by1); + EXPECT_LT(sel.bz0, sel.bz1); +} + TEST(ViewAdaptiveLod, RespectsDefaultMaxTextureDim) { const VolumeView v = makeVol(/*n=*/4096, /*brick=*/256, /*levels=*/1); // levels=1(只有 level0)→ 不能升层,只能靠缩区间满足 16384。4096 < 16384 必满足。