Merge pull request '修复相机 HLR 结果处理中的三处正确性缺陷' (#79) from company-desktop-computer/fix-hlr-b-series into main

Reviewed-on: #79
Reviewed-by: gjm <littleoldpan@163.com>
This commit was merged in pull request #79.
This commit is contained in:
gjm committed 2026-10-26 21:23:06 +08:00
commit 1d9e94481f
2 files changed
+54 -21

No files matched your search

+22 -10
View File
@@ -2,10 +2,10 @@
> 范围:`SourceCode/Code2026/CoreLibrary/AecDbCamera.cpp`(约 2900 行)。
> 基线:main @ 7870a2b(PR #76「修复相机 HLR 计算链路上的几处内存泄漏」已合并)。下文行号均为该版本。
> 本文记录三件事:已修的泄漏、**尚未修**的泄漏与正确性缺陷、以及一次评估后放弃的改造(组合设备块展开)。
> 本文记录四件事:已修的泄漏、**尚未修**的泄漏、已修的正确性缺陷,以及一次评估后放弃的改造(组合设备块展开)。
> 目录:
> 1 调用链与所有权约定 · 2 已修复的泄漏 · 3 尚未修复的泄漏 · 4 尚未修复的正确性缺陷
> 1 调用链与所有权约定 · 2 已修复的泄漏(PR #76) · 3 尚未修复的泄漏 · 4 已修复的正确性缺陷
> 5 已放弃的改造:组合设备块展开 · 6 验证方法
## 1. 调用链与所有权约定
@@ -63,6 +63,8 @@ AecDbXCamera::refresh() :2121
| 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 个中心线/流向箭头,一个管件若干),所以这两处不是"漏一个对象",而是"漏一整个剖面的补充几何"。而且触发条件恰好是"刷新了但没出图"这种高频操作。
@@ -95,7 +97,9 @@ for (...)
因为 `ar` 恒为空,现有所有实现都不会往 `output` 里塞东西,所以**目前确实不漏**。但它是一个"结果容器"却没有任何回收,哪天某个 override 在空 `ar` 下也 append 就会开始漏。建议加一行 `deleteContainerOfArx<AcDbEntity>(output);` 兜底。
## 4. 尚未修复的正确性缺陷
## 4. 已修复的正确性缺陷
下面三条都是**原代码**的问题,已在本分支修复。保留分析过程是为了说明为什么这么改。
### B1 `mergeEnvelope` 可能返回悬空指针(use-after-free)
@@ -124,7 +128,7 @@ return pSolidAll; // ← 返回已释放指针
触发条件:找到的"第一个 solid"恰好 `isNull()`,且之后再没有有效 solid。此时 `pSolidAll` 非空但指向一个空 solid,它仍留在 `arptrs` 里,被 :815 的 `deleteArrayVoidPtrs` 删掉,然后被当作结果返回。`preprocess` :921 立刻 `xData.Write(pEnvelope, ...)` → UAF。
修法方向:把"找第一个可用 solid"和"是否成功"分开表达,例如用下标而不是裸指针记录命中位置,命中且非 `isNull()` 时才 `removeAt` 并置标志位。
**已修复**:改成“只记下标、循环结束后再 `removeAt`”——`nSolidIndex` 为 -1 表示没找到,`pSolidAll` 只在命中有效 solid 时才赋值。这样“指针非空”和“已从容器摘除”两件事不再脱节。
### B2 `outputWithoutID.append(entAr)` 写在循环体内
@@ -152,7 +156,7 @@ for (int i(0); i != arResult.length(); ++i)
目前打不到:`arAdditional` 里只有带 `_ENVELOPE_` 标记的融合实体,在 :998 走 `continue` 分支。属于埋着的雷,一旦以后往 `arAdditional` 里放别的补充实体就会炸。
修法:把 `outputWithoutID.append(entAr)` 移到内层循环之外(与 `entAr` 的清空节奏对齐)。
**已修复**:在内层循环前加 `entAr.removeAll()`,把 `outputWithoutID.append(entAr)` 移到循环之后,并加 `if (!entAr.isEmpty())` 保护。这样 `entAr` 的清空节奏和 append 节奏一致,每个实体只出现一次。
### B3 `outputWithID` 的 erase 循环会跳元素
@@ -170,10 +174,13 @@ for (; it != outputWithID.end(); ++it)
`erase` 后重置到 `begin()`,紧接着 for 的 `++it` 又走一步 → **跳过新的首元素**。有多个构筑物 id 时会漏处理一个(不会被转成 `arEnvelope`,也不会从 `outputWithID` 里移除)。
修法:改成 `while (it != outputWithID.end())` 手动控制,或 `erase` 后用 `it = outputWithID.erase(it)` 的返回值。
**已修复**:改成 `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",这样块内实体各自的剖切表现(中心线、系统颜色、遮挡关系)才能正确。
@@ -212,7 +219,12 @@ for (; it != outputWithID.end(); ++it)
## 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. **回归**:正常剖切出图、构筑物融合开/关各跑一遍,确认输出几何与修改前一致(本次改动只加释放,不改数据流)。
本工程 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 只加释放、不改数据流**,理论上输出不变;只要出现几何缺失或崩溃,优先怀疑释放点误删了数据库内实体。
+32 -11
View File
@@ -750,16 +750,20 @@ namespace
if (arptrs.isEmpty()) return NULL;
// 找到第一个 3dsolid
// 找到第一个可用的 3dsolid。
// 这里只记下标、循环结束后再 removeAt:如果命中一个 isNull() 的 solid,
// pSolidAll 已经非空但没被摘掉,后面的 deleteArrayVoidPtrs(arptrs) 会把
// 它一起删掉,返回的就是野指针。
AcDb3dSolid *pSolidAll(NULL);
int nSolidIndex(-1);
for (int i(0); i < arptrs.length(); ++i)
{
AcDbEntity *pEnt((AcDbEntity *)arptrs[i]);
if (!pEnt) continue;
pSolidAll = AcDb3dSolid::cast(pEnt);
if (!pSolidAll) continue;
if (pSolidAll->isNull())
AcDb3dSolid *pSolid(AcDb3dSolid::cast(pEnt));
if (!pSolid) continue;
if (pSolid->isNull())
{
#ifndef NDEBUG
acutPrintf(_T("\nThe 3DSOLID does not have a ShapeManager object. "));
@@ -767,16 +771,19 @@ namespace
continue;
}
arptrs.removeAt(i);
pSolidAll = pSolid;
nSolidIndex = i;
break;
}
if (!pSolidAll)
if (nSolidIndex < 0)
{
deleteArrayVoidPtrs<AcDbEntity>(arptrs);
return NULL;
}
arptrs.removeAt(nSolidIndex);
if (arptrs.isEmpty()) return pSolidAll;
std::map<void *, AcDbHandle>::const_iterator debugIt(debugMM.end());
@@ -1003,6 +1010,7 @@ namespace
continue;
}
entAr.removeAll();
for (int i(0); i != arResult.length(); ++i)
{
AcDbEntity *pEnt = (AcDbEntity *)arResult[i]->clone();
@@ -1011,9 +1019,13 @@ namespace
pEnt->setLinetype(pSrc->linetypeId());
pEnt->setColorIndex(pSrc->colorIndex());
entAr.append(pEnt);
outputWithoutID.append(entAr);
}
// 必须放在循环外:entAr 不清空,放在循环里等于每轮把累积数组整体再追加一次,
// 同一指针会在 outputWithoutID 里出现多次,AddToModelSpace 第二次必然失败,
// 随后 DELETE_PTR 会删掉已经入库的实体。
if (!entAr.isEmpty())
outputWithoutID.append(entAr);
}
}
}
@@ -1157,16 +1169,25 @@ namespace
}
// 收集构筑物产生的结果(一般是不对构筑物进行融合的情况下)
// 注意:erase 之后不能把 it 重置成 begin(),否则 for 的 ++it 会跳过新的首元素。
IDMapEntities::iterator it(outputWithID.begin());
for (; it != outputWithID.end(); ++it)
while (it != outputWithID.end())
{
if (!isEnvelope(it->first)) continue;
if (!isEnvelope(it->first))
{
++it;
continue;
}
// 是构筑物结果,复制出来,并且删掉outputWithID里面的内容
for (int i(0); i < it->second.length(); ++i)
arEnvelope.append((AcDbEntity*)(it->second[i]->clone()));
deleteContainerOfArx<AcDbEntity>(it->second);
IDMapEntities::iterator itNext(it);
++itNext;
outputWithID.erase(it);
it = outputWithID.begin();
it = itNext;
}
// 处理构筑物