Files
envi-code/HRL-MODEL-refactor.md
T
company-desktop 39dbab85bf 修复相机 HLR 结果处理中的三处正确性缺陷
B1 mergeEnvelope:命中 isNull() 的 3DSOLID 时 pSolidAll 已赋值但未从 arptrs 摘除,
后续 deleteArrayVoidPtrs 会把它删掉,返回野指针。改为记下标、循环结束后统一 removeAt。

B2 process_result:outputWithoutID.append(entAr) 误写在 for 循环体内,每轮把累积数组
整体再追加一次,同一指针在 outputWithoutID 中出现多次,AddToModelSpace 第二次失败后
DELETE_PTR 会删掉已入库的实体。append 移到循环外,并先 entAr.removeAll()。

B3 process_result:erase 之后把 it 重置成 begin(),for 的 ++it 会跳过新的首元素,
多个构筑物时漏处理。改为 while + 先取 next 迭代器。

同时更新 HRL-MODEL-refactor.md:去掉未实施的内存账本方案,验证方法改为行为观察。
2026-10-26 21:12:27 +08:00

231 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HLR(相机剖切计算)模块问题记录与改造评估
> 范围:`SourceCode/Code2026/CoreLibrary/AecDbCamera.cpp`(约 2900 行)。
> 基线:main @ 7870a2b(PR #76「修复相机 HLR 计算链路上的几处内存泄漏」已合并)。下文行号均为该版本。
> 本文记录四件事:已修的泄漏、**尚未修**的泄漏、已修的正确性缺陷,以及一次评估后放弃的改造(组合设备块展开)。
> 目录:
> 1 调用链与所有权约定 · 2 已修复的泄漏(PR #76) · 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)` |
> L3 严格来说是**防御性**的:`clipResultBySection` 只清空 `it->second` 的内容、不 `erase` key,所以 `result1` / `result2` 的 map 不会变空,这处早退目前不可达。保留释放是为了以后改裁剪逻辑时不出事。
### 关于 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<AcDbEntity>(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<AcDbEntity>(arptrs); // ← 把这个 null-solid 也删了
return pSolidAll; // ← 返回已释放指针
```
触发条件:找到的"第一个 solid"恰好 `isNull()`,且之后再没有有效 solid。此时 `pSolidAll` 非空但指向一个空 solid,它仍留在 `arptrs` 里,被 :815 的 `deleteArrayVoidPtrs` 删掉,然后被当作结果返回。`preprocess` :921 立刻 `xData.Write(pEnvelope, ...)` → UAF。
**已修复**:改成“只记下标、循环结束后再 `removeAt`”——`nSolidIndex` 为 -1 表示没找到,`pSolidAll` 只在命中有效 solid 时才赋值。这样“指针非空”和“已从容器摘除”两件事不再脱节。
### 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` 里放别的补充实体就会炸。
**已修复**:在内层循环前加 `entAr.removeAll()`,把 `outputWithoutID.append(entAr)` 移到循环之后,并加 `if (!entAr.isEmpty())` 保护。这样 `entAr` 的清空节奏和 append 节奏一致,每个实体只出现一次。
### 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`;`erase` 之前先算好 `itNext`,`erase` 之后 `it = itNext`。
(没有用 `it = outputWithID.erase(it)` 的返回值,因为 ARX 20~22 走 C++03,那时 `map::erase` 返回 `void`。)
## 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. 验证方法
本工程 Debug 也链接 Release CRT(`cmakelists.txt` 里 `CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL"`,且 Debug 会剥掉 `_DEBUG`),所以 `_CrtSetDbgFlag` / `_CrtDumpMemoryLeaks` 用不了,只能靠行为观察。
1. **定点验证 L1/L2/L3**:把相机移到完全没有实体的位置(或把剖切面拖到模型外)连续刷新 50 次。修之前每次都会攒下一轮补充实体与轴网克隆,任务管理器里私有内存单调上升;修之后应回到基线并保持平稳。
2. **定点验证 L5**:选中一个块参照参与剖切(块打块失败的场景不易构造,主要看正常路径的内存曲线不随刷新次数增长)。
3. **回归(重点)**:B1~B3 改的是控制流,必须实测。
- 多个构筑物同时参与剖切(验证 B3 的 erase 循环不再跳元素,且每个构筑物都被正确处理);
- 块参照参与剖切(验证 B2 之后块内实体只被 append 一次,不会在后续刷新时出现"实体已被删除"的崩溃);
- 构筑物融合开 / 关各跑一遍,输出几何与修改前逐项比对。
4. **L1~L6 只加释放、不改数据流**,理论上输出不变;只要出现几何缺失或崩溃,优先怀疑释放点误删了数据库内实体。