Files
envi-code/HRL-MODEL-refactor.md
T
company-desktop f349ed2dca 补充 HLR 模块问题记录文档
记录相机剖切计算链路的调用链与所有权约定、已修复的 5 处泄漏(PR #76)、
尚未修复的 L6 与正确性缺陷 B1~B3,以及评估后放弃的组合设备块展开改造
(含识别方式、关键结论与放弃原因),供后续重做时参考。
2026-10-26 20:30:56 +08:00

13 KiB
Raw Blame History

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

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

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

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

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. 回归:正常剖切出图、构筑物融合开/关各跑一遍,确认输出几何与修改前一致(本次改动只加释放,不改数据流)。