Merge pull request '补充 HLR 模块问题记录文档' (#77) from company-desktop-computer/hlr-model-refactor-doc into main
Reviewed-on: #77 Reviewed-by: gjm <littleoldpan@163.com>
This commit was merged in pull request #77.
This commit is contained in:
commit
ce886ff952
1 file changed
+218
@@ -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<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。
|
||||
|
||||
修法方向:把"找第一个可用 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. **回归**:正常剖切出图、构筑物融合开/关各跑一遍,确认输出几何与修改前一致(本次改动只加释放,不改数据流)。
|
||||
Reference in new issue
Block a user