diff --git a/HRL-MODEL-refactor.md b/HRL-MODEL-refactor.md index d7e6853..d4094e7 100644 --- a/HRL-MODEL-refactor.md +++ b/HRL-MODEL-refactor.md @@ -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(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 只加释放、不改数据流**,理论上输出不变;只要出现几何缺失或崩溃,优先怀疑释放点误删了数据库内实体。 diff --git a/SourceCode/Code2026/CoreLibrary/AecDbCamera.cpp b/SourceCode/Code2026/CoreLibrary/AecDbCamera.cpp index 32afb5c..bf435b7 100644 --- a/SourceCode/Code2026/CoreLibrary/AecDbCamera.cpp +++ b/SourceCode/Code2026/CoreLibrary/AecDbCamera.cpp @@ -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(arptrs); return NULL; } + arptrs.removeAt(nSolidIndex); + if (arptrs.isEmpty()) return pSolidAll; std::map::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(it->second); + + IDMapEntities::iterator itNext(it); + ++itNext; outputWithID.erase(it); - it = outputWithID.begin(); + it = itNext; } // 处理构筑物