From f349ed2dca6024e0515da93f30ec9885c9abd04b Mon Sep 17 00:00:00 2001 From: company-desktop-computer Date: Mon, 26 Oct 2026 20:30:56 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=20HLR=20=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E8=AE=B0=E5=BD=95=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 记录相机剖切计算链路的调用链与所有权约定、已修复的 5 处泄漏(PR #76)、 尚未修复的 L6 与正确性缺陷 B1~B3,以及评估后放弃的组合设备块展开改造 (含识别方式、关键结论与放弃原因),供后续重做时参考。 --- HRL-MODEL-refactor.md | 218 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 HRL-MODEL-refactor.md diff --git a/HRL-MODEL-refactor.md b/HRL-MODEL-refactor.md new file mode 100644 index 0000000..d7e6853 --- /dev/null +++ b/HRL-MODEL-refactor.md @@ -0,0 +1,218 @@ +# HLR(相机剖切计算)模块问题记录与改造评估 + +> 范围:`SourceCode/Code2026/CoreLibrary/AecDbCamera.cpp`(约 2900 行)。 +> 基线:main @ 7870a2b(PR #76「修复相机 HLR 计算链路上的几处内存泄漏」已合并)。下文行号均为该版本。 +> 本文记录三件事:已修的泄漏、**尚未修**的泄漏与正确性缺陷、以及一次评估后放弃的改造(组合设备块展开)。 + +> 目录: +> 1 调用链与所有权约定 · 2 已修复的泄漏 · 3 尚未修复的泄漏 · 4 尚未修复的正确性缺陷 +> 5 已放弃的改造:组合设备块展开 · 6 验证方法 + +## 1. 调用链与所有权约定 + +### 1.1 一帧调用链 + +``` +AecDbXCamera::refresh() :2121 +├─ eraseEnts(m_arResults) 删上一轮输出 +├─ collectEnities(arIds) / collectAxisObject(arAxis) +├─ getSectionPolygon() → new AcDbPolyline :2607,refresh 负责 delete +├─ 克隆轴网 → entAxis :2189 / :2215 +│ 所有权隐式移交给 hlrCalc +├─ hlrCalc(arIds, ..., entAxis) :2023 +│ ├─ preprocess(...) :907 +│ │ ├─ [构筑物融合] mergeEnvelope() → pEnvelope :717 +│ │ │ └─ 进 arAdditional_all + arAdditional +│ │ └─ 逐个实体 PreprocessForHlr() → 补充实体 +│ │ └─ mpAdditional[id] = 补充实体,同时进 arAdditional_all +│ ├─ makeViewPort() → shared_ptr :440 +│ ├─ HlrCalc(arIds, arAdditional_all, ...) :2066 +│ │ result1: id → 结果;result2: 补充实体指针 → 结果 +│ ├─ removeDegenerateArcs() :591 +│ ├─ transfromHlrResult() :2086(会一起移动 pSection) +│ ├─ clipResultBySection() :241(删除并替换 result 内容) +│ ├─ process_result() → outputWithID / outputWithoutID :965(全是 clone) +│ └─ 释放 result1 / result2 / arAdditional_all :2106-2116 +├─ DELETE_PTR(pSection) +└─ 输出:AddToModelSpace(),失败则 DELETE_PTR +``` + +### 1.2 三类数据 + +`hlrCalc` 的注释(:2035 附近)把参与 HLR 的数据分成三类,理解这三类就理解了整条链: + +| 类别 | 来源 | 中间容器 | 谁释放 | +| --- | --- | --- | --- | +| 1 数据库实体 | `arIds` | `result1` | hlrCalc :2108 | +| 2 数据库实体的补充实体 | `PreprocessForHlr` 产出 | `mpAdditional` + `result2` + `arAdditional_all` | hlrCalc :2113 / :2116 | +| 3 非数据库实体的补充实体(融合后的构筑物) | `mergeEnvelope` 产出 | `arAdditional` + `result2` + `arAdditional_all` | hlrCalc :2113 / :2116 | + +### 1.3 所有权三条约定(改这个模块前必须先确认) + +1. `PreprocessForHlr` / `GetDraw3dables` / `AcDbBlockReference::explode` / `AcDbEntity::clone` 产出的**都是纯堆对象**,不在数据库里,调用方负责 `delete`。 +2. `mpAdditional`(`IDMapEntities`)和 `arAdditional` 只做**查找**,全程没有任何释放它们的代码;真正的释放统一走 `arAdditional_all`(:2116)。 +3. 同一个指针只能进 `arAdditional_all` **一次**。因为 `deleteContainerOfArx` 是逐元素 `delete`,重复入容器就会重复释放。`preprocess` 里 `preEnt` 的项本来就已 `arAll.append` 过一次,所以不能再往 `preEnt` 里放同一批指针。 + +## 2. 已修复的泄漏(PR #76) + +| 编号 | 位置 | 漏什么 | 触发条件 | 修法 | +| --- | --- | --- | --- | --- | +| L1 | hlrCalc :2076 | `arAdditional_all` 全部 | HLR 结果为空(实体全在视锥外 / 剖切面切空) | 早退前 `deleteContainerOfArx` | +| L2 | hlrCalc :2077 | `arAxisPtrs`(即 refresh 的 `entAxis`) | 同上,早退发生在插入 `result1` 之前 | 早退前 `deleteContainerOfArx` | +| L3 | hlrCalc :2094 | `arAdditional_all` 全部 | 截面裁剪后结果为空 | 早退前 `deleteContainerOfArx` | +| L4 | mergeEnvelope :774 | `arptrs` 里全部克隆实体 | 勾了"构筑物融合",但构筑物里没有可用的 3DSOLID(全是 Region / 网格 / 空 solid) | `deleteArrayVoidPtrs` 后再返回 | +| L5 | process_result :1123 | `subAr` 里整块 HLR 结果 | `CoreTools::makeBlockReference` 失败(块表被锁 / 只读 / `tbl->add` 失败) | 失败时 `deleteArrayVoidPtrs(subAr)` | + +### 关于 L1/L2 的量级 + +补充实体是**每个选中实体**产出的(一条管道 1~3 个中心线/流向箭头,一个管件若干),所以这两处不是"漏一个对象",而是"漏一整个剖面的补充几何"。而且触发条件恰好是"刷新了但没出图"这种高频操作。 + +### 关于 L2 的所有权 + +`refresh` 在 :2189 / :2215 把轴网克隆进 `entAxis`,然后作为 `arAxisPtrs` 传给 `hlrCalc`: + +- 正常路径:hlrCalc :2083 把它塞进 `result1[kNull]`,最后被 :2108 的循环释放 —— 这是一次**隐式所有权转移**,refresh 自己从不释放。 +- 所以早退路径必须由 hlrCalc 补释放。两个调用方(`refresh` 和 `AecDbJoggedCamera::Camera::hlrCalc`,`AecDbJoggedCamera.cpp:573`)在 hlrCalc 返回后都不再使用这个数组,补释放是安全的。 + +## 3. 尚未修复的泄漏 + +### L6 `rollback` 的 `output` 从不释放 + +`AecDbCamera.cpp:638` + +```cpp +AcDbEntityArray ar, output; +EntMapEntities eme; +for (...) +{ + if (pEnt) pEnt->ProcessHlrResult(ar, eme, output); // ar 恒为空 + if (pCrv) pCrv->ProcessHlrResult(ar, eme, output); +} +// output 没有回收 +``` + +`rollback` 的实际目的是通过调 `ProcessHlrResult` 触发副作用(例如 `AecDbEwPipe::ProcessHlrResult` 会把 `HLR_CALC` 复位),并不是真的要结果。 + +因为 `ar` 恒为空,现有所有实现都不会往 `output` 里塞东西,所以**目前确实不漏**。但它是一个"结果容器"却没有任何回收,哪天某个 override 在空 `ar` 下也 append 就会开始漏。建议加一行 `deleteContainerOfArx(output);` 兜底。 + +## 4. 尚未修复的正确性缺陷 + +### B1 `mergeEnvelope` 可能返回悬空指针(use-after-free) + +`AecDbCamera.cpp:755-777` + +```cpp +AcDb3dSolid *pSolidAll(NULL); +for (int i(0); i < arptrs.length(); ++i) +{ + pSolidAll = AcDb3dSolid::cast(pEnt); + if (!pSolidAll) continue; + if (pSolidAll->isNull()) + { + acutPrintf(_T("\nThe 3DSOLID does not have a ShapeManager object. ")); + continue; // ← pSolidAll 已经非空,且没有 removeAt + } + arptrs.removeAt(i); + break; +} +if (!pSolidAll) { ... return NULL; } +if (arptrs.isEmpty()) return pSolidAll; +... +deleteArrayVoidPtrs(arptrs); // ← 把这个 null-solid 也删了 +return pSolidAll; // ← 返回已释放指针 +``` + +触发条件:找到的"第一个 solid"恰好 `isNull()`,且之后再没有有效 solid。此时 `pSolidAll` 非空但指向一个空 solid,它仍留在 `arptrs` 里,被 :815 的 `deleteArrayVoidPtrs` 删掉,然后被当作结果返回。`preprocess` :921 立刻 `xData.Write(pEnvelope, ...)` → UAF。 + +修法方向:把"找第一个可用 solid"和"是否成功"分开表达,例如用下标而不是裸指针记录命中位置,命中且非 `isNull()` 时才 `removeAt` 并置标志位。 + +### B2 `outputWithoutID.append(entAr)` 写在循环体内 + +`AecDbCamera.cpp:1015` + +```cpp +for (int i(0); i != arResult.length(); ++i) +{ + AcDbEntity *pEnt = (AcDbEntity *)arResult[i]->clone(); + ... + entAr.append(pEnt); + outputWithoutID.append(entAr); // ← entAr 不清空,每轮把累积数组整体再 append +} +``` + +第 n 轮把累积数组整体追加一次,同一指针会在 `outputWithoutID` 里出现多次: + +| 轮次 | `entAr` | `outputWithoutID` | +| --- | --- | --- | +| 1 | `[e1]` | `[e1]` | +| 2 | `[e1,e2]` | `[e1, e1,e2]` | +| 3 | `[e1,e2,e3]` | `[e1, e1,e2, e1,e2,e3]` | + +后果:`refresh` :2252 第二次 `AddToModelSpace` 必然失败,随后 `DELETE_PTR(pEnt)` **删掉已经入库的实体**。 + +目前打不到:`arAdditional` 里只有带 `_ENVELOPE_` 标记的融合实体,在 :998 走 `continue` 分支。属于埋着的雷,一旦以后往 `arAdditional` 里放别的补充实体就会炸。 + +修法:把 `outputWithoutID.append(entAr)` 移到内层循环之外(与 `entAr` 的清空节奏对齐)。 + +### B3 `outputWithID` 的 erase 循环会跳元素 + +`AecDbCamera.cpp:1157-1171` + +```cpp +for (; it != outputWithID.end(); ++it) +{ + if (!isEnvelope(it->first)) continue; + ... + outputWithID.erase(it); + it = outputWithID.begin(); // 然后 for 的 ++it 再自增 +} +``` + +`erase` 后重置到 `begin()`,紧接着 for 的 `++it` 又走一步 → **跳过新的首元素**。有多个构筑物 id 时会漏处理一个(不会被转成 `arEnvelope`,也不会从 `outputWithID` 里移除)。 + +修法:改成 `while (it != outputWithID.end())` 手动控制,或 `erase` 后用 `it = outputWithID.erase(it)` 的返回值。 + +## 5. 已放弃的改造:组合设备块展开 + +### 5.1 需求 + +组合设备(`pl_zhsb` 从库里插入的设备)在模型空间里是一个 `AcDbBlockReference`。相机做 HLR 时,这个块是**整块**参与计算的,结果又被 `process_result` 打回一个块。期望改成"把块内实体取出来,逐个参与 HLR",这样块内实体各自的剖切表现(中心线、系统颜色、遮挡关系)才能正确。 + +### 5.2 块的识别方式 + +标记写在 `AcDbBlockReference` 的 XData 上,注册应用名两套、机制相同: + +| 来源 | 应用名 | 写入点 | +| --- | --- | --- | +| 组合设备 | `ZH_SB_REF_FIELD` | `PipelineCmd/ZHSBLibDlg.cpp:351`、`PipelineCmd/EquipmentLibraryDlg.cpp:1148` | +| 模型库 | `MODEL_3D_REF_FIELD` | `PipelineCmd/Model3dLibDlg.cpp:351` | + +判据就是"这个 XData 应用名存不存在",参考 `CoreCommand/entityfilter.cpp:1419`。**不要**在 CoreLibrary 里调 `CEntityProperty::isZHSB`(在 CoreCommand 层,反向依赖)。 + +### 5.3 关键结论(重做时可以直接用) + +1. **块 id 必须从 `arids` 里摘掉**。只要块 id 还在,HLR 就会整块算一遍,`process_result` 里 `pRef` 分支(:1127 起)再 `makeBlockReference` 打回一个块,几何翻倍。`preprocess` 的 `arids` 本来就是"会被挑选修改"的(`arids = arOther` 已经这么干过)。 +2. **块内实体只能进 `mpAdditional` + `arAdditional_all`,绝不能同时进 `arAdditional`**,否则同一指针在 `arAdditional_all` 里出现两次 → 重复释放(见 1.3 第 3 条)。 +3. **叶子实体必须把自己也放进补充实体数组**。`PreprocessForHlr` 只追加"补充"几何,实体本身是通过 `arids` 参与 HLR 的;块内实体没有 id,只能把自己也当补充实体塞进去。 +4. **要支持叶子的 `ProcessHlrResult`,需要两层结构**。`process_result` 的约定是 `ProcessHlrResult(自己的结果, 补充实体→结果, output)`,而 `mpAdditional` 只有一层。要正确组装,必须额外维护 `EntMapEntities 叶子→它的补充实体`,并把它一路透传 `preprocess` → `hlrCalc` → `process_result`(三个函数都在匿名 namespace 里,签名可以改)。 +5. **`explode()` 的所有权**:返回的是纯堆对象,`delete` 即可释放;但只展开一层,嵌套块返回的仍是 `AcDbBlockReference`,需要递归;MINSERT 会返回 `eNotApplicable`,此时必须保留整块(否则几何直接消失)。仓库里 `AecDbOpening.cpp:2997 _explode` 是可参考的递归写法(它用 clone + `transformBy`,结果用 `deleteArrayVoidPtrs` 释放)。 +6. **属性继承要版本兼容**:`AcDbEntity::layer(AcString&)` / `linetype(AcString&)` 这两个重载 **R23.0(ACAD 2019)才有**,R18~R22 只有 `ACHAR* layer() const`(要 `acutDelString`)。想判"是否在 0 层"又不踩版本差异,可以用层表取 `"0"` 层的 id 再比 `layerId()`(写法参考 `CoreLibrary/Geom.cpp` 里的 `getLayerTable`)。 + +### 5.4 为什么放弃 + +改动要同时动 `preprocess`、`process_result`、`hlrCalc` 三个函数的签名,并引入一层新的 `EntMapEntities` 结构(叶子→补充实体),才能让块内上华实体走自己的 `ProcessHlrResult`。而收益只是"组合设备块剖切时的显示细节"。 + +期间实际踩出来的坑(都已复现并修掉): + +- `arAll.append(累积数组)` 写在循环里 → 同一指针重复入 `arAdditional_all` → 多次 `delete`,**任何含 2 个以上实体的块必崩**; +- 返回值语义写反("处理成功"返回 false,"处理失败"返回 true),配上块 id 摘除后会导致整块几何消失; +- `preprocess_block` 内部又 open 了一次调用方已打开的实体。 + +结论:性价比不足,回退。等真的需要在剖切里区分组合设备内部构造时再重做。 + +## 6. 验证方法 + +1. **泄漏量化**:Debug 版在 ARX 加载时设 `_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF)`,退出 CAD 会打印泄漏块数与地址;配合在 `hlrCalc` 出口打印 `arAdditional_all.length()`,可以量化"刷新 N 次漏多少"。 +2. **定点验证 L1/L3**:把相机移到完全没有实体的位置(或把剖切面拖到模型外)连续刷新 50 次,观察内存增量。 +3. **定点验证 L2**:只选轴网 + 一个远处实体,让 HLR 结果为空,刷新若干次。 +4. **回归**:正常剖切出图、构筑物融合开/关各跑一遍,确认输出几何与修改前一致(本次改动只加释放,不改数据流)。