MF4 补 ##SD/##SI/VLSD + 信号枚举文本 enumText + .mat 改随机读(流式进度)
1) 修复上轮记录的两个已知问题
- MatLoader: LoadPlainObject 不再为拿 dims 调 ReadDataset(整块解码第二次),
改用 ReadDatasetInfo;普通 v7.3 大矩阵的时间/内存不再翻倍。
- MdfFile::TextBlock 注释与实现对齐:真的支持 ##MD(UTF-8 XML,无长度前缀)。
2) 信号枚举信息(以 TestData/NEW-MF4.mf4 为目标)
- 新增 SignalEnumText(DataTable.hpp):values[] + texts[] + Empty()/Find(),
命名用 enumText —— 它描述"取值代表哪个状态",不是单位。
- DataTable::columnEnumText 与 columns 同下标;SignalData::enumText 由
BuildSignalGroup 移动赋值;其它格式(CSV/.mat)保持为空。
- MF4 cc_type = 7:cc_val[i](F64,数据段 +24)↔ cc_ref[4 + i](##TX/##MD 文本),
最后一个引用是默认文本,不使用。
- 界面只在**光标取值标注**追加 " = <枚举文本>"(等号两侧各一个空格、无尖括号),
查表用采样原值(ApplyDrawTransform 之前);常规状态显示一个像素都不变。
- 实测:NEW-MF4.mf4 8 信号 / 4 条带枚举(32+5+32+5),MF4-Record.MF4 另有 2 种(12/32)。
3) MF4 支持范围
- ##SI 采样索引:只有索引 ≥2 条且全部 < 周期数才生效(逐记录有效性位图)。
实测部分工具给每个通道挂只含常量 513/4 的 ##SI,必须忽略,否则整列变 NaN。
- ##SD + VLSD(cn_type == 1):##SD 数据段是「UINT32 长度 + 字节」样本序列,
记录里放 64 位偏移;AppendSignalData 递归展开 SD/DZ/DL/HL(单通道上限 512MB),
DecodeVlsdValue 先按直接偏移、失败再按 >>1 回退,取样本第一个元素。
- CN 的 cn_data / md_unit / md_comment 下标按链接总数反推:8 链接(MDF 4.0 风格)
= 换算[4]/数据[5]/单位[6]/注释[7],9 链接(4.1)= [5]/[6]/[7]/[8]。
实测 Simulink 导出的“4.10”仍写 8 链接,不能写死 Link(cn,4/5)。
- 主通道:cn_sync_type 1/2/3(时间/角度/距离)→ 名字 t/time/timestamp/zeit/时间
(含 t[...])→ 第一个数值通道兜底;cg_flags 第 2 位(远程主通道)跳过 sync 判定。
- 记录长度优先 cg_data_bytes(CG 数据段 +24);旧实现把 U8(cgData) 当
cg_record_id_len,其实是 cg_flags 低字节(总线事件位)。
- 进度:先走 DG 链得到真实组数(不再写死 64),hasBytes = true,bytesRead 取
MdfFile::ReadAt 累计(夹到文件大小);4MB 分块读取检查取消。
- ##DZ:数据段前有 28 字节头(original_type/zip_type/param/original_size/zip_size),
旧实现整段当 zlib 流是错的;只支持纯 deflate(zip_type = 0)。
- 一个信号都没有时,错误信息附带「布局无法解析 / 缺少时间通道 / 空数据组」计数。
4) .mat:内存映射 → 随机读 + 元数据块缓存(消除 SEH 风险)
- MatHdf5::Open 改为 CreateFileW(FILE_FLAG_SEQUENTIAL_SCAN) + ReadFile;
删除 MappedBytes,改为 ReadAt / Cached(16 × 64KB LRU + 上次命中槽)/ Tag /
ReadData / ReadCString。I/O 失败只会变成 false,不再触发页访问异常。
- 数据读取:连续布局按 4MB 切片;分块布局按需 + 64KB 对齐。
固定 4MB 预读在交错存放的分块数据集上会把读盘量放大到 2.2 倍(2874MB/1293MB),
改后 1301MB。
- 关键提速:dims=[N]、chunk=[32768,1] 归一化后 rank=2、每行 1 字节,逐行算坐标
是 12.9 亿次迭代;新增「rank == 2 且 chunkDims[1] == dims[1]」快路径(整块一次
sink)。BIG_MAT.mat(1.29GB / 5 × 16,142,471):21.5s → 7.9s,私有内存 +741MB。
- 进度:Open 立刻上报「0 / 总大小」,之后每读一块回调 ReadProgress(bytesRead,total)
→ MatLoader 映射到 0.02–0.98 并单调夹紧(字节进度与逐流解码取较大值),收尾 1.0。
遮罩层百分比不再一上来就 100%;取消会中止(StreamDataset 返回 false),不留 NaN 空洞。
- 连续布局数据不足(文件被截断)现在明确失败,不再静默截短。
验证(全部离屏:无可见窗口、不注入鼠标键盘屏幕)
- 新增 .preflight/perf/loadercheck(数据层)32/32:
NEW-MF4 8 信号 / 4 条枚举 / 枚举文本逐条核对 / 9396 与 94 行;
MF4-Record 124 信号 / 3 轴 / 4 条枚举;data-sample.mat 4 组 8 信号 / 行数;
BIG_MAT.mat 5 信号 × 16,142,471 + 进度(total 已知、ratio 单调、bytes 递增、
起点 ≤0.05、终值 1.0、更新 ≥10 次)。
- 新增 .preflight/perf/enumui(真实 Application + SW_HIDE)12/12:
只显示 gVR_WinOpenReq[0],光标落在枚举采样点上 —— 标签宽度 265px;
临时清空 enumText 后 25px;差值 240px 与 " = WinOpenReqAtDrvr_Idle" 的
UiMeasureTextWidth 完全一致;恢复后宽度回到 265px。
- 回归:datacheck 69/69、matcheck 21/21、matui 15/15(loading bytes 达到总大小)、
verify 49/49。
- make release / make debug 零警告。Build/Release/TargetCheck.exe = 820,224 字节
(上轮 785,920,+34KB)。(Target.exe 正在被用户运行的程序占用,链接步骤未覆盖它。)
This commit is contained in:
1 parent
75f0fafd41
commit
181f95cc3a
10 files changed
+1328
-291
No files matched your search
@@ -4913,6 +4913,9 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 枚举文本按**采样值原值**查找(下面会叠加系数/绝对值/镜像等界面变换,
|
||||
// 那些变换只影响画出来的数值,不改变「这个采样值代表哪个状态」)。
|
||||
const double enumLookupValue = value;
|
||||
// 取值读数必须与画出来的波形一致:同样套用系数/绝对值/镜像。
|
||||
value = ApplyDrawTransform(*resolved.style, value);
|
||||
|
||||
@@ -4948,6 +4951,15 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 信号带「值 → 文本」枚举表时(如 MDF4 `cc_type = 7`),在数值之后追加
|
||||
// ` = <枚举文本>` 尾缀(等号两侧各一个空格、不用尖括号)。
|
||||
// ⚠️ 常规状态(未启用光标)不显示任何枚举文本,观感与以前完全一致。
|
||||
if (const std::wstring* enumText = resolved.data->enumText.Find(enumLookupValue))
|
||||
{
|
||||
text += L" = ";
|
||||
text += *enumText;
|
||||
}
|
||||
|
||||
const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, text) + 10.0f * m_scale;
|
||||
const float textHeight = 17.0f * m_scale;
|
||||
// 左右越界时三角贴在视界内侧、尖端朝外;数值标注必须放在三角的**内侧**
|
||||
|
||||
+85
-1
@@ -3001,6 +3001,8 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
|
||||
|
||||
| 2026-10-19 | DeepSeek-V4.1-Flash(**本机**) | **`.mat` 流式解码(内存映射 + 逐 chunk 填充)+ MDF4 `##DL/##DZ/##HL` 数据块 + 光标数据点被时间面板遮住改三角(详见 §12 末尾「2026-10-19 第四轮」与 §11.2 第 154–159 条)**。① **`.mat`**:`MatHdf5::Open` 由“整文件 `fread` 进 `vector`”改为 **`MapViewOfFile`**(`MappedBytes` 只读视图,解析代码零改动);新增 `ReadDatasetInfo` + `StreamDataset(ByteSink)`(连续布局 4MB 切片、分块布局逐 chunk inflate)与 `ReadDoubleStream`(SDI `#data#` 字节流 → double,7 字节尾巴处理非 8 对齐 chunk);`ReadNumbers` 改为流式转换;`MatLoader` 先收集信号清单再按(分组, 时间流)合并成 `TableBlock`,时间列按内容去重、数据列 `std::move`,进度改成 `0–0.02 映射 / 0.02–0.98 逐流解码`。实测 `BIG_MAT.mat`(1.29GB / 5×16.14M):**117s → 12.5s**、私有内存 **+1234MB → +740MB**(数据本身 739MB)。② **MDF4**:`CollectDataSegments` 递归展开 `##DL`/`##HL`,识别 `##DT`/`##DV`/`##DZ`;`ReadRecordSegments` 支持跨段拼接与压缩段 inflate;压缩段记录长度改用通道位宽推算。`Problem-MF4.mf4` 由 **4 → 8 个信号**(与 Simulink SDI 一致)。③ **光标标记**:`MarkerCoveredByCursorLabel` + `CoverHiddenMarker`,圆点被底部时间读数/Δt 面板盖住时改为向下三角(原始信号与派生曲线两处)。验证:新增离屏 `.preflight/round4check` **34/34**;**A/B**(禁用 `##DL` ⇒ 2 项失败、禁用 `CoverHiddenMarker` ⇒ 3 项失败);回归 `round3check` 31/31、`editcheck` 20/20、`check` 137/137;`make release`/`make debug` 零警告。 |
|
||||
|
||||
| 2026-10-20 | DeepSeek-V4.1-Flash(**本机**) | **MF4 `##SD`/`##SI`/VLSD + 信号枚举文本 `enumText`(光标尾缀)+ `.mat` 由内存映射改为随机读(详见 §12 末尾「2026-10-20」)**。① **§2.6 两处**:`LoadPlainObject` 不再 `ReadDataset` 二次解码(改用 `ReadDatasetInfo` 取维度);`MdfFile::TextBlock` 真正支持 `##MD`(UTF-8 XML)。② **枚举**:新增 `SignalEnumText`(`values[]`/`texts[]`/`Find`)挂在 `DataTable::columnEnumText` 与 `SignalData::enumText`;MF4 `cc_type = 7` 按 `cc_val[i] ↔ cc_ref[4+i]` 解析。界面只在**光标取值标注**追加 ` = <枚举文本>`,常规状态零变化。`NEW-MF4.mf4` 实测 8 信号 / 4 条带枚举(32+5+32+5 项)。③ **MF4**:`##SI` 采样索引(≥2 条且全部 < 周期数才生效;实测工具会给每个通道挂只含常量 513 的 `##SI`,必须忽略)、`##SD` + VLSD(`UINT32` 长度前缀 + 64 位偏移,直接偏移优先、`>>1` 回退)、CN 链接下标按链接总数反推(4.0 的 8 链接 / 4.1 的 9 链接)、主通道接受 `sync_type` 1/2/3 + `cg_flags` 远程主通道、记录长度优先 `cg_data_bytes`、进度分母改真实 DG 数并上报字节、`##DZ` 28 字节头修正。④ **`.mat`**:删除 `MappedBytes`,改 `ReadFile` 随机读 + 16×64KB 元数据 LRU + 按需数据读取(固定 4MB 预读会把读盘量放大 2.2 倍 ⇒ 改后 1.0 倍);新增 rank=2 连续 chunk 快路径(12.9 亿次逐行迭代 ⇒ 每块一次回调),`BIG_MAT.mat` **21.5s → 7.9s**、私有 +741MB;进度按真实 `bytesRead / totalBytes` 推进(不再一上来 100%),取消会中止而不是留下 NaN 空洞。验证:新增离屏 `loadercheck` **32/32**、`enumui` **12/12**;回归 `datacheck` 69/69、`matcheck` 21/21、`matui` 15/15、`verify` 49/49;`make release`/`make debug` 零警告;`Target.exe` 785,920 → **820,224 字节**。 |
|
||||
|
||||
## 11. 跨设备协作须知(起源设备视角)
|
||||
|
||||
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
|
||||
@@ -6646,4 +6648,86 @@ DG3 data=0 cycle=0 (空组,一个字符串通道)
|
||||
(表中文本的前缀 `WinOpenReqAtDrvr_` 已省略;`gVR_WinOpenReq[1]` 的 CC 内容完全相同。)
|
||||
⚠️ 目前 `ParseChannel` 只认 `cc_type ∈ {0,1}`,所以这两个通道在界面上显示为原始整数;
|
||||
要显示标签需要按 `cc_type == 7` 读 `links[4..4+valCount-1]` 并与 `cc_val[]` 一一对应,
|
||||
再随信号传递到界面(本轮按要求**未做代码改动**)。
|
||||
再随信号传递到界面(本轮按要求**未做代码改动**)。
|
||||
|
||||
---
|
||||
|
||||
## 本轮(2026-10-20,本机):MF4 `##SD`/`##SI`/VLSD + 枚举文本 + `.mat` 改为随机读
|
||||
|
||||
> 用户需求:① 修复 §2.6 的两个已知问题;② 为信号增加**枚举信息**(准确命名 `enumText`,不是
|
||||
> 单位),以 `TestData/NEW-MF4.mf4` 为目标;常规状态显示不变,**只有启用光标时**在数值后追加
|
||||
> ` = <枚举文本>`(等号两侧各一个空格、无尖括号);③ MF4 支持 `##SD`/`##SI`、VLSD、
|
||||
> `cg_flags` 主通道判定、进度分母;④ `.mat` 由内存映射改为「随机读 + 元数据块缓存」,
|
||||
> 流式读取并动态更新「已读/总大小」,在可能范围内加速;⑤ 离屏验证后提交,不占用鼠标键盘屏幕。
|
||||
>
|
||||
> 验证(全部离屏,无可见窗口、无注入输入):
|
||||
> * 新增 `.preflight/perf/loadercheck.exe`(数据层)**32/32**;
|
||||
> * 新增 `.preflight/perf/enumui.exe`(真实 `Application` + `SW_HIDE` + 光标标签宽度)**12/12**;
|
||||
> * 回归 `datacheck` 69/69、`matcheck` 21/21、`matui` 15/15、`verify` 49/49;
|
||||
> * `make release` / `make debug` 零警告;`Target.exe` 785,920 → **820,224 字节**(+34KB)。
|
||||
|
||||
### A. §2.6 两个已知问题
|
||||
|
||||
1. **`LoadPlainObject` 解码两遍**:改为先 `ReadDatasetInfo`(只取 `dims`)再 `ReadNumbers`,
|
||||
不再为了维度去调用 `ReadDataset`(那会把整块数据解码第二次)。
|
||||
2. **`MdfFile::TextBlock` 注释与实现不符**:现在真的支持 `##MD`(数据段是 UTF-8 XML,
|
||||
没有长度前缀 ⇒ 直接转宽字符串)。`##TX` 的两种写法(长度前缀 / CANape 裸文本)保持不变。
|
||||
|
||||
### B. 枚举信息(`enumText`)
|
||||
|
||||
- 数据层新增 `struct SignalEnumText { values[]; texts[]; Empty(); Find(value); }`
|
||||
(定义在 `DataTable.hpp`)。**命名刻意用 `enumText`**:它描述「取值代表哪个状态」,
|
||||
不是单位、不是注释。`DataTable::columnEnumText` 与 `columns` 同下标;
|
||||
`SignalData::enumText` 由 `BuildSignalGroup` 移动赋值。
|
||||
- MF4 侧:`cc_type = 7` 时按 `cc_val_count` 读 `cc_val[]`(F64,从数据段 +24 开始)与
|
||||
`cc_ref[4 + i]`(`##TX`/`##MD` 文本);最后一个引用是「默认文本」,不使用。
|
||||
文本为空的取值不进入枚举表(保持 `values/texts` 对齐)。
|
||||
- 界面:**只有光标取值标注**追加 ` = <枚举文本>`(`PlotViewPaint.cpp` 里 `text += L" = "`)。
|
||||
常规状态(未启用光标)一个像素都不变;枚举查表用**采样原值**(在 `ApplyDrawTransform` 之前),
|
||||
避免系数/镜像把状态查丢。
|
||||
- `TestData/NEW-MF4.mf4` 实测:8 信号,4 条带枚举
|
||||
(`gVR_WinOpenReq[0/1]` 32 项、`gVP_WindowLearnSt[0/1]` 5 项);`MF4-Record.MF4` 也有 4 条
|
||||
(`gVP_ActrStopReazon[0]` 12 项、`gV_WinPosnSts[0]` 32 项)。
|
||||
|
||||
### C. MF4:`##SD` / `##SI` / VLSD / 主通道 / 进度
|
||||
|
||||
- **`##SI`(采样索引)**:只有当索引表 **≥2 条且全部 < 周期数**时才生效(逐记录有效性位图,
|
||||
其余行保持 NaN)。⚠️ 实测 `MF4-Record.MF4` / `NEW-MF4.mf4` 里每个通道都挂了一个只含
|
||||
**1 个值(513 / 4)** 的 `##SI`——那显然不是采样下标,必须忽略,否则整列会被清成 NaN。
|
||||
- **`##SD` + VLSD(`cn_type == 1`)**:`##SD` 数据段是「`UINT32` 长度 + 字节」的样本序列;
|
||||
记录在该通道 `cn_byte_offset` 处放 **64 位偏移**。实现 `AppendSignalData`(递归展开
|
||||
`##SD`/`##DZ`/`##DL`/`##HL`,单通道上限 512MB)与 `DecodeVlsdValue`
|
||||
(直接偏移优先,失败再按 `>>1` 回退;取样本的第一个元素作为采样值)。
|
||||
- **`cn_data` / `cn_md_unit` / `cn_md_comment` 的链接下标按链接总数反推**:
|
||||
8 链接(MDF 4.0 风格)= 换算[4]/数据[5]/单位[6]/注释[7];9 链接(4.1)= [5]/[6]/[7]/[8]。
|
||||
⚠️ 实测 Simulink 导出的「4.10」文件仍写 8 链接 ⇒ 不能写死 `Link(cn, 4/5)`。
|
||||
- **主通道判定**:`cn_sync_type` = 1/2/3 → 名字(`t`/`time`/`timestamp`/`zeit`/`时间`,
|
||||
含 `t[...]`)→ 第一个数值通道兜底。`cg_flags` 第 2 位(远程主通道)时跳过 sync 判定。
|
||||
- **记录长度**:优先 `cg_data_bytes`(CG 数据段 +24),其次「载荷 ÷ 周期数」,最后按位宽推算。
|
||||
⚠️ 旧实现把 `U8(cgData)` 当 `cg_record_id_len`,那其实是 `cg_flags` 的低字节(总线事件位)。
|
||||
- **进度**:先走一遍 DG 链得到**真实组数**(不再写死 64),`hasBytes = true`,
|
||||
`bytesRead` 来自 `MdfFile::ReadAt` 的累计(上报时夹到文件大小);4MB 分块读取时检查取消。
|
||||
- **`##DZ` 修正**:数据段前面有 28 字节头(`original_type`/`zip_type`/`param`/
|
||||
`original_size`/`zip_size`),旧实现把整段当 zlib 流是错的。只支持纯 deflate(`zip_type = 0`)。
|
||||
- 失败诊断:没有任何可绘制通道时,错误信息附带「布局无法解析 / 缺少时间通道 / 空数据组」计数。
|
||||
|
||||
### D. `.mat`:内存映射 → 随机读 + 元数据缓存
|
||||
|
||||
- `MatHdf5::Open` 改为 `CreateFileW(FILE_FLAG_SEQUENTIAL_SCAN)` + `ReadFile` 随机读;
|
||||
`MappedBytes` 删除,改为 `ReadAt` / `Cached` / `Tag` / `ReadData`。
|
||||
**彻底消除 SEH 风险**(I/O 失败只会变成 `false`)。
|
||||
- 元数据:16 × 64KB 块缓存(LRU + 上次命中槽快路径),覆盖对象头 / B-tree / 本地堆;
|
||||
成员名用 `ReadCString`(跨块边界安全)。
|
||||
- 数据:连续布局按 4MB 切片;分块布局**按需读取 + 64KB 对齐**,
|
||||
⚠️ 固定 4MB 预读在交错存放的分块数据集上会把读盘量放大到文件大小的 2.2 倍
|
||||
(实测 2874MB / 1293MB)⇒ 改后 1301MB。
|
||||
- **关键提速**:`dims=[N]`、chunk `[32768,1]` 归一化后 rank=2、每行 1 字节,
|
||||
逐行算坐标是 **12.9 亿次迭代**(约占总时间一半以上)。新增
|
||||
「rank == 2 且 `chunkDims[1] == dims[1]`」快路径:整块一次 `sink`。
|
||||
实测 `BIG_MAT.mat`(1.29GB / 5 信号 × 16,142,471):**21.5s → 7.9s**,私有内存 **+741MB**
|
||||
(最终列数据 739MB,与映射版一致)。
|
||||
- **进度**:`Open` 立刻上报「0 / 总大小」;之后 `ReadData`/切片每读一块回调
|
||||
`ReadProgress(bytesRead, total)` → `MatLoader` 映射到 0.02–0.98,并进行**单调夹紧**
|
||||
(字节进度与逐流解码进度取较大值),收尾 1.0。遮罩层百分比不再「一上来就 100%」。
|
||||
取消会置 `m_readCancelled` 并让 `StreamDataset` 立即失败(不会留下 NaN 空洞)。
|
||||
- 连续布局数据不足(文件被截断)现在**明确失败**,不再静默截短。
|
||||
@@ -393,6 +393,14 @@ namespace jdi
|
||||
{
|
||||
std::wstring name; /**< 信号名称(加载时取列名 / 通道名;用户可在界面上重命名)。 */
|
||||
std::size_t axisIndex = 0; /**< 所属时间轴在分组内的下标。 */
|
||||
/**
|
||||
* @brief 取值 → 文本的枚举表(源格式自带时才有,例如 MDF4 `cc_type = 7`)。
|
||||
*
|
||||
* ⚠️ 这**不是**单位、也不是注释:它只回答「这个数值代表什么状态」。
|
||||
* 空表示该信号没有枚举信息,界面必须与以前完全一致(不加任何尾缀)。
|
||||
* 启用光标时,取值标注才在数值之后追加 ` = <枚举文本>`。
|
||||
*/
|
||||
SignalEnumText enumText;
|
||||
std::vector<double> values; /**< 采样值,缺失处为 NaN。 */
|
||||
double minValue = 0.0; /**< 有限采样值最小值。 */
|
||||
double maxValue = 0.0; /**< 有限采样值最大值。 */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef __DATA_TABLE_HPP__
|
||||
#define __DATA_TABLE_HPP__
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
@@ -9,6 +10,61 @@
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 一列数值的「值 → 文本」枚举表(如 ASAM MDF v4 的 `cc_type = 7` 换算)。
|
||||
*
|
||||
* 它描述的是**信号本身的取值含义**(例如 `0 = Off`、`1 = On`),
|
||||
* 与单位、注释、物理量纲无关,因此这里刻意命名为 `enumText` 而不是 `unit` / `label`。
|
||||
*
|
||||
* 空表(`texts.empty()`)表示该列没有枚举信息,界面必须保持与以前完全一致的显示。
|
||||
*/
|
||||
struct SignalEnumText
|
||||
{
|
||||
/** @brief 枚举取值(与采样列同一量纲,通常是整数)。 */
|
||||
std::vector<double> values;
|
||||
|
||||
/** @brief 与 `values` 一一对应的显示文本。 */
|
||||
std::vector<std::wstring> texts;
|
||||
|
||||
/** @brief 是否没有任何枚举信息。 */
|
||||
bool Empty() const { return values.empty() || texts.size() != values.size(); }
|
||||
|
||||
/**
|
||||
* @brief 按取值查找枚举文本。
|
||||
* @param [IN] value 取值(物理值,未叠加界面「调整」变换)。
|
||||
* @return 命中返回对应文本指针,未命中返回 nullptr。
|
||||
*/
|
||||
const std::wstring* Find(double value) const
|
||||
{
|
||||
if (Empty() || !(value == value)) // NaN 永远不命中
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
for (std::size_t index = 0; index < values.size(); ++index)
|
||||
{
|
||||
if (values[index] == value)
|
||||
{
|
||||
return &texts[index];
|
||||
}
|
||||
}
|
||||
// 采样值按 double 保存,整数枚举在任意位宽下都是精确可表示的,
|
||||
// 但个别工具会写 `1.0000001` 这类带噪声的换算表:再做一次四舍五入匹配。
|
||||
const double rounded = std::nearbyint(value);
|
||||
if (rounded == value)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
for (std::size_t index = 0; index < values.size(); ++index)
|
||||
{
|
||||
if (std::nearbyint(values[index]) == rounded)
|
||||
{
|
||||
return &texts[index];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 内存中的**列优先**数据表(与具体文件格式无关)。
|
||||
*
|
||||
@@ -25,6 +81,13 @@ namespace jdi
|
||||
{
|
||||
std::vector<std::wstring> headers; /**< 列名(已去除引号与首尾空白)。 */
|
||||
std::vector<std::vector<double>> columns; /**< 列优先数值矩阵:columns[列][行],缺失处为 NaN。 */
|
||||
/**
|
||||
* @brief 与 `columns` 同下标的「值 → 文本」枚举表(没有枚举信息的列为空)。
|
||||
*
|
||||
* 加载器只在源格式自带枚举文本(如 MDF4 `cc_type = 7`)时填充;
|
||||
* 它不是单位、不是注释,界面只在光标读数处以尾缀形式展示。
|
||||
*/
|
||||
std::vector<SignalEnumText> columnEnumText;
|
||||
std::size_t rowCount = 0; /**< 有效数据行数。 */
|
||||
wchar_t delimiter = L','; /**< 实际使用的分隔符(仅文本格式有意义)。 */
|
||||
unsigned long long sourceBytes = 0; /**< 源文件字节数(用于按比例预留容量)。 */
|
||||
|
||||
+82
-33
@@ -97,29 +97,48 @@ namespace jdi
|
||||
using OpenProgress = std::function<bool(std::uint64_t bytesRead, std::uint64_t totalBytes)>;
|
||||
|
||||
/**
|
||||
* @brief 打开文件(**内存映射**,不把整个文件读进堆内存)。
|
||||
* @brief 打开文件(**随机读取**,不映射、不把整个文件读进堆内存)。
|
||||
*
|
||||
* ⚠️ 早期版本把整个文件一次读入 `std::vector`(注释里假设“`.mat` 通常几十 MB”),
|
||||
* 但 SDI 导出的录波文件可能是 **GB 级**:整文件读入 + 分块解码 + 再拷成 double 会同时
|
||||
* 驻留“文件 + 未压缩字节 + double”三份内存。现改为:
|
||||
* * 映射文件(只占虚拟地址空间,按页面需求驻留);
|
||||
* * 分块数据集逐 chunk 解压并**直接写入**自持有的 double 列(见 `ReadDoubleStream`);
|
||||
* * 进度回调在映射完成后立即报告一次(字节已全部可访问),后续解码阶段由 `MatLoader` 驱动。
|
||||
* 早期版本把整个文件一次读入 `std::vector`,后来改成整文件内存映射;
|
||||
* 但映射在「文件被截断 / 网络共享掉线 / 移动介质拔出」时会触发页访问异常,
|
||||
* 而这类异常属于 SEH,GCC/MinGW 下的 `try/catch` **捕获不到**,只能让进程崩溃。
|
||||
*
|
||||
* 因此这里改为 SEC2 式的「`ReadFile` 随机读 + 元数据块缓存」:
|
||||
* * 所有读取都经过 `ReadAt`,I/O 失败只会变成 `false`(上层给出错误提示);
|
||||
* * 对象头 / B-tree / 本地堆等热点元数据走 64KB 块缓存(LRU);
|
||||
* * 数据段按 4MB 切片顺序读,并对小 chunk 做顺序预读。
|
||||
* 解析代码本身与映射版完全一致。
|
||||
*
|
||||
* @param [IN] path 文件路径。
|
||||
* @param [OUT] error 失败原因。
|
||||
* @param [IN] progress 读取进度回调(可为空)。
|
||||
* @param [IN] progress 打开阶段的进度回调(可为空,只在上报总大小时调用一次)。
|
||||
* @return 成功返回 true;用户取消时也返回 false。
|
||||
*/
|
||||
bool Open(const std::wstring& path, std::wstring& error,
|
||||
const OpenProgress& progress = OpenProgress());
|
||||
void Close();
|
||||
|
||||
/** @brief 析构时释放映射(否则栈上对象的映射会留到进程退出)。 */
|
||||
/** @brief 析构时关闭文件句柄。 */
|
||||
~MatHdf5() { Close(); }
|
||||
|
||||
/** @brief 文件字节数(映射长度)。 */
|
||||
std::uint64_t FileBytes() const { return static_cast<std::uint64_t>(m_data.size()); }
|
||||
/** @brief 文件字节数。 */
|
||||
std::uint64_t FileBytes() const { return m_size; }
|
||||
|
||||
/** @brief 已从文件实际读取的字节数(用于「已读 / 总大小」进度)。 */
|
||||
std::uint64_t BytesRead() const { return m_bytesRead; }
|
||||
|
||||
/**
|
||||
* @brief 流式解码期间的字节进度回调。
|
||||
*
|
||||
* 每读一个块(或每 4MB 切片)调用一次;返回 false 表示请求取消。
|
||||
*/
|
||||
using ReadProgress = std::function<bool(std::uint64_t bytesRead, std::uint64_t totalBytes)>;
|
||||
|
||||
/** @brief 设置流式解码期间的字节进度回调(可为空)。 */
|
||||
void SetReadProgress(const ReadProgress& progress) const { m_readProgress = progress; }
|
||||
|
||||
/** @brief 流式解码是否已被进度回调取消。 */
|
||||
bool Cancelled() const { return m_readCancelled; }
|
||||
|
||||
/** @brief 根分组地址。 */
|
||||
std::uint64_t Root() const { return m_root; }
|
||||
@@ -192,6 +211,38 @@ namespace jdi
|
||||
std::uint64_t Addr(std::uint64_t offset) const;
|
||||
std::uint64_t Resolve(std::uint64_t address) const;
|
||||
|
||||
/** @brief 随机读取一段字节(不经过缓存;累加已读字节数)。 */
|
||||
bool ReadAt(std::uint64_t offset, void* dst, std::size_t bytes) const;
|
||||
|
||||
/**
|
||||
* @brief 取一段**元数据**的只读指针(经 64KB 块缓存)。
|
||||
*
|
||||
* ⚠️ 返回的指针只在下次调用前有效(缓存块可能被替换),调用方必须立即用完。
|
||||
* @return 成功返回指针;越界或读失败返回 nullptr。
|
||||
*/
|
||||
const std::uint8_t* Cached(std::uint64_t offset, std::size_t bytes) const;
|
||||
|
||||
/** @brief 判断 `offset` 处的 4 字节块标识是否等于 `id`(`id` 为 4 字符)。 */
|
||||
bool Tag(std::uint64_t offset, const char* id) const;
|
||||
|
||||
/**
|
||||
* @brief 读取一个 NUL 结尾的短字符串(本地堆里的成员名)。
|
||||
*
|
||||
* 名字很短但可能跨越 64KB 缓存块边界,因此按块边界截断、必要时继续向后读。
|
||||
*/
|
||||
void ReadCString(std::uint64_t offset, std::string& out) const;
|
||||
|
||||
/** @brief 丢失顺序预读缓冲(随机跳转后调用)。 */
|
||||
void DropReadAhead() const;
|
||||
|
||||
/**
|
||||
* @brief 从文件读一段**数据**(带顺序预读,供小 chunk 连续读取)。
|
||||
*
|
||||
* 若请求落在当前预读窗口内直接返回指针;否则从 `offset` 起预读最多 4MB。
|
||||
* @return 成功返回指针(仅在下次调用前有效),否则 nullptr。
|
||||
*/
|
||||
const std::uint8_t* ReadData(std::uint64_t offset, std::size_t bytes) const;
|
||||
|
||||
void ReadMessages(std::uint64_t address, std::vector<Message>& messages) const;
|
||||
bool ParseType(const std::uint8_t* data, std::size_t size, std::size_t offset, Type& out,
|
||||
std::size_t& consumed) const;
|
||||
@@ -200,31 +251,29 @@ namespace jdi
|
||||
void WalkSymbolNode(std::uint64_t node, std::uint64_t heapData,
|
||||
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
|
||||
|
||||
/**
|
||||
* @brief 内存映射的文件视图(代替“整个文件读进 `vector`”)。
|
||||
*
|
||||
* ⚠️ 行为与 `std::vector<std::uint8_t>` 的**只读子集**一致(`size/data/begin/end/[]`),
|
||||
* 因此所有“按偏移取字节”的解析代码一行都不用改;但**虚拟地址不等于物理内存**:
|
||||
* 只有真正被解析到的页才驻留,GB 级文件不会再先吃掉一整个文件大小的私有内存。
|
||||
*/
|
||||
struct MappedBytes
|
||||
/** @brief 元数据缓存槽(固定槽数,最近最少使用优先淘汰)。 */
|
||||
struct CacheSlot
|
||||
{
|
||||
const std::uint8_t* pointer = nullptr;
|
||||
std::size_t length = 0;
|
||||
const std::uint8_t* data() const { return pointer; }
|
||||
std::size_t size() const { return length; }
|
||||
const std::uint8_t& operator[](std::size_t index) const { return pointer[index]; }
|
||||
const std::uint8_t* begin() const { return pointer; }
|
||||
const std::uint8_t* end() const { return pointer + length; }
|
||||
void clear()
|
||||
{
|
||||
pointer = nullptr;
|
||||
length = 0;
|
||||
}
|
||||
std::uint64_t base = 0;
|
||||
std::uint32_t length = 0;
|
||||
std::uint64_t stamp = 0;
|
||||
std::vector<std::uint8_t> bytes;
|
||||
};
|
||||
MappedBytes m_data; /**< 文件映射视图(只读;语义同字节数组)。 */
|
||||
|
||||
void* m_fileHandle = nullptr; /**< `HANDLE`(避免头文件引入 windows.h)。 */
|
||||
void* m_mapHandle = nullptr;
|
||||
std::uint64_t m_size = 0; /**< 文件字节数。 */
|
||||
mutable std::uint64_t m_bytesRead = 0; /**< 已实际读取的字节数(进度上报用)。 */
|
||||
mutable std::vector<CacheSlot> m_cache; /**< 元数据块缓存。 */
|
||||
mutable std::uint64_t m_stamp = 0; /**< 缓存访问计数。 */
|
||||
mutable std::size_t m_lastSlot = 0; /**< 上次命中的槽(连续标量读取的快路径)。 */
|
||||
mutable std::vector<std::uint8_t> m_readAhead; /**< 顺序预读缓冲。 */
|
||||
mutable std::uint64_t m_readAheadBase = 0;
|
||||
mutable std::uint32_t m_readAheadLength = 0;
|
||||
mutable bool m_readAheadValid = false;
|
||||
mutable std::vector<std::uint8_t> m_slice; /**< 连续布局切片缓冲(复用容量)。 */
|
||||
mutable std::vector<std::uint8_t> m_chunk; /**< 大于预读窗口的分块缓冲(复用容量)。 */
|
||||
mutable ReadProgress m_readProgress; /**< 流式解码进度回调(可空)。 */
|
||||
mutable bool m_readCancelled = false; /**< 进度回调请求取消。 */
|
||||
std::uint64_t m_base = 0;
|
||||
std::uint64_t m_root = 0;
|
||||
int m_offsetSize = 8;
|
||||
|
||||
@@ -12,20 +12,32 @@ namespace jdi
|
||||
* @brief 读取并解析一个 ASAM MDF v4(`.mf4` / `.mdf`)测量文件。
|
||||
*
|
||||
* 只解析**绘制需要的那部分**:头块版本、数据组(DG)→ 通道组(CG)→ 通道(CN)/换算(CC),
|
||||
* 以及每个通道组的原始记录;字符串/字节数组通道、压缩数据块(DZ/HZ)与 MIME 通道一律跳过。
|
||||
* 以及每个通道组的原始记录;字符串/字节数组通道、MIME 通道一律跳过。
|
||||
*
|
||||
* **时间轴**:MDF 的每个通道组(CG)自带一个时间基准。本加载器把每个 CG 输出为一个
|
||||
* 时间轴块(`TableBlock`):块的时间列取自该组的**时间主通道**(`cn_sync_type == 1`,
|
||||
* 退回名为 `time`/`t` 的通道,再退回第一个数值通道),其余数值通道作为该块的信号列。
|
||||
* 时间轴块(`TableBlock`):块的时间列取自该组的**主通道**
|
||||
*(`cn_sync_type` = 1 时间 / 2 角度 / 3 距离,退回名为 `time`/`t`/`timestamp` 的通道,
|
||||
* 再退回第一个数值通道),其余数值通道作为该块的信号列。
|
||||
* 内容完全相同的时间轴在数据模型层会被合并(见 `BuildSignalGroup`)。
|
||||
* `cg_flags` 的「远程主通道」位会跳过第一步,避免把普通信号误当时间轴。
|
||||
*
|
||||
* **数据块**:`##DT` / `##DV` / `##DZ`(zlib + 转置压缩之外的纯 deflate)/
|
||||
* `##DL`(数据列表)/ `##HL`(头部列表),支持记录跨段拼接。
|
||||
*
|
||||
* **VLSD(`cn_type == 1`)**:记录里是 64 位偏移,样本在 `##SD`(`UINT32` 长度 + 数据)。
|
||||
* 本项目取样本的第一个元素作为该采样点的取值。
|
||||
*
|
||||
* **采样索引(`##SI`)**:仅当索引表至少两条且全部落在记录范围内时生效
|
||||
*(否则忽略——实测部分工具会给每个通道挂一个只含常量 513 的 `##SI`)。
|
||||
*
|
||||
* 支持的通道编码:无符号/有符号整数(1–64 位,任意位偏移)与 IEEE-754 浮点(32/64 位)。
|
||||
* 支持的换算:无换算、线性(`phys = c0 + c1 * raw`)。其它换算按“无换算”处理。
|
||||
* 支持的换算:无换算、线性(`phys = c0 + c1 * raw`)、值 → 文本(`cc_type = 7`,输出到
|
||||
* `DataTable::columnEnumText`,界面只在光标读数处以尾缀显示)。其它换算按“无换算”处理。
|
||||
*
|
||||
* @param [IN] filePath MDF 文件完整路径。
|
||||
* @param [OUT] source 解析结果(每个通道组一个时间轴块)。
|
||||
* @param [OUT] error 失败原因描述(成功时清空)。
|
||||
* @param [IN] progress 进度回调(按字节比例),可为空。
|
||||
* @param [IN] progress 进度回调(按已处理数据组比例;同时上报已读/总字节),可为空。
|
||||
* @return 解析成功返回 true。
|
||||
*/
|
||||
bool LoadMf4Source(const std::wstring& filePath, LoadedSource& source, std::wstring& error,
|
||||
|
||||
@@ -338,6 +338,12 @@ namespace jdi
|
||||
}
|
||||
signal.axisIndex = axisIndex;
|
||||
|
||||
// 值 → 文本枚举表(MDF4 `cc_type = 7` 等):与列同下标,缺省为空。
|
||||
if (column < table.columnEnumText.size())
|
||||
{
|
||||
signal.enumText = std::move(table.columnEnumText[column]);
|
||||
}
|
||||
|
||||
signal.values = std::move(table.columns[column]);
|
||||
const SignalStats stats = ComputeStats(signal.values, signal.coarseMin, signal.coarseMax);
|
||||
signal.coarseStride = kCoarseStride;
|
||||
|
||||
+323
-70
@@ -82,9 +82,212 @@ namespace jdi
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace
|
||||
{
|
||||
/** @brief 元数据缓存的块大小与槽数(16 × 64KB = 1MB,足够覆盖对象头/B-tree 热点)。 */
|
||||
constexpr std::size_t kMetaBlockBytes = 64u * 1024u;
|
||||
constexpr std::size_t kMetaBlockSlots = 16;
|
||||
|
||||
/** @brief 顺序预读窗口上限。 */
|
||||
constexpr std::size_t kReadAheadBytes = 4u * 1024u * 1024u;
|
||||
|
||||
/**
|
||||
* @brief 数据读取的最小粒度:按请求大小向上对齐读取,命中窗口时直接复用。
|
||||
*
|
||||
* ⚠️ 分块数据集的 chunk 在文件里**并非完全顺序**(多个样本流交错写入,
|
||||
* 每段连续区域约 1.8MB)。实测 `BIG_MAT.mat`:固定预读 4MB 会让每个连续段
|
||||
* 末尾浪费 2.2MB,总读盘量达到文件大小的 **2.2 倍**(2.87GB / 1.29GB)。
|
||||
* 改为「按需 + 64KB 对齐」后,读盘量回到文件大小附近。
|
||||
*/
|
||||
constexpr std::size_t kReadGranularity = 64u * 1024u;
|
||||
}
|
||||
|
||||
bool MatHdf5::ReadAt(std::uint64_t offset, void* dst, std::size_t bytes) const
|
||||
{
|
||||
if (m_fileHandle == nullptr || bytes == 0)
|
||||
{
|
||||
return bytes == 0;
|
||||
}
|
||||
if (offset > m_size || bytes > m_size - offset)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const HANDLE file = static_cast<HANDLE>(m_fileHandle);
|
||||
LARGE_INTEGER position = {};
|
||||
position.QuadPart = static_cast<LONGLONG>(offset);
|
||||
if (!SetFilePointerEx(file, position, nullptr, FILE_BEGIN))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::size_t done = 0;
|
||||
while (done < bytes)
|
||||
{
|
||||
DWORD read = 0;
|
||||
const DWORD want = static_cast<DWORD>(std::min<std::size_t>(bytes - done, 1u << 30));
|
||||
if (!ReadFile(file, static_cast<std::uint8_t*>(dst) + done, want, &read, nullptr))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (read == 0)
|
||||
{
|
||||
return false; // 文件被截断
|
||||
}
|
||||
done += read;
|
||||
}
|
||||
m_bytesRead += bytes;
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::uint8_t* MatHdf5::Cached(std::uint64_t offset, std::size_t bytes) const
|
||||
{
|
||||
if (offset >= m_size || bytes > kMetaBlockBytes)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (m_cache.empty())
|
||||
{
|
||||
m_cache.resize(kMetaBlockSlots);
|
||||
}
|
||||
const std::uint64_t base = offset - (offset % kMetaBlockBytes);
|
||||
// 快路径:连续读同一块(U16/U32/U64 与 B-tree 遍历几乎都是这种模式)。
|
||||
{
|
||||
CacheSlot& slot = m_cache[m_lastSlot];
|
||||
if (slot.length > 0 && slot.base == base)
|
||||
{
|
||||
slot.stamp = ++m_stamp;
|
||||
const std::uint64_t delta = offset - slot.base;
|
||||
return (delta + bytes <= slot.length) ? (slot.bytes.data() + delta) : nullptr;
|
||||
}
|
||||
}
|
||||
CacheSlot* victim = nullptr;
|
||||
for (std::size_t index = 0; index < m_cache.size(); ++index)
|
||||
{
|
||||
CacheSlot& slot = m_cache[index];
|
||||
if (slot.length > 0 && slot.base == base)
|
||||
{
|
||||
slot.stamp = ++m_stamp;
|
||||
m_lastSlot = index;
|
||||
const std::uint64_t delta = offset - slot.base;
|
||||
return (delta + bytes <= slot.length) ? (slot.bytes.data() + delta) : nullptr;
|
||||
}
|
||||
if (slot.length == 0 || victim == nullptr || slot.stamp < victim->stamp)
|
||||
{
|
||||
victim = &slot;
|
||||
}
|
||||
}
|
||||
if (victim == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
// 未命中:整块读入(块可能超出文件末尾,按可用长度裁剪)。
|
||||
const std::size_t wanted =
|
||||
static_cast<std::size_t>(std::min<std::uint64_t>(kMetaBlockBytes, m_size - base));
|
||||
if (bytes > wanted)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
victim->bytes.resize(wanted);
|
||||
victim->base = base;
|
||||
victim->length = static_cast<std::uint32_t>(wanted);
|
||||
victim->stamp = ++m_stamp;
|
||||
m_lastSlot = static_cast<std::size_t>(victim - m_cache.data());
|
||||
if (!ReadAt(base, victim->bytes.data(), wanted))
|
||||
{
|
||||
victim->length = 0;
|
||||
return nullptr;
|
||||
}
|
||||
return victim->bytes.data() + (offset - base);
|
||||
}
|
||||
|
||||
bool MatHdf5::Tag(std::uint64_t offset, const char* id) const
|
||||
{
|
||||
const std::uint8_t* bytes = Cached(offset, 4);
|
||||
if (bytes == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return std::memcmp(bytes, id, 4) == 0;
|
||||
}
|
||||
|
||||
void MatHdf5::ReadCString(std::uint64_t offset, std::string& out) const
|
||||
{
|
||||
out.clear();
|
||||
std::uint64_t position = offset;
|
||||
while (position < m_size && out.size() < 4096)
|
||||
{
|
||||
const std::uint8_t* bytes = Cached(position, 1);
|
||||
if (bytes == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 只消费本缓存块内剩余的字节,保证下一次循环拿到的新块仍然有效。
|
||||
const std::uint64_t blockBase = position - (position % kMetaBlockBytes);
|
||||
const std::size_t limit =
|
||||
static_cast<std::size_t>(std::min<std::uint64_t>(kMetaBlockBytes - (position - blockBase),
|
||||
m_size - position));
|
||||
std::size_t used = 0;
|
||||
while (used < limit)
|
||||
{
|
||||
if (bytes[used] == '\0')
|
||||
{
|
||||
return;
|
||||
}
|
||||
out.push_back(static_cast<char>(bytes[used]));
|
||||
++used;
|
||||
}
|
||||
position += used;
|
||||
}
|
||||
}
|
||||
|
||||
void MatHdf5::DropReadAhead() const
|
||||
{
|
||||
m_readAheadValid = false;
|
||||
m_readAheadLength = 0;
|
||||
}
|
||||
|
||||
const std::uint8_t* MatHdf5::ReadData(std::uint64_t offset, std::size_t bytes) const
|
||||
{
|
||||
if (bytes > kReadAheadBytes || offset > m_size || bytes > m_size - offset)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (m_readAheadValid && offset >= m_readAheadBase &&
|
||||
(offset - m_readAheadBase) + bytes <= m_readAheadLength)
|
||||
{
|
||||
return m_readAhead.data() + (offset - m_readAheadBase);
|
||||
}
|
||||
// 按需读取 + 64KB 对齐:命中当前窗口就直接复用,否则只读「这次需要的一段」。
|
||||
// (固定 4MB 预读在交错存放的分块数据集上会浪费 2 倍以上的 I/O。)
|
||||
const std::size_t requested = std::max<std::size_t>(bytes, kReadGranularity);
|
||||
std::size_t wanted = static_cast<std::size_t>(std::min<std::uint64_t>(
|
||||
m_size - offset, std::min<std::size_t>(kReadAheadBytes,
|
||||
((requested + kReadGranularity - 1) / kReadGranularity) *
|
||||
kReadGranularity)));
|
||||
if (m_readAhead.size() < wanted)
|
||||
{
|
||||
m_readAhead.resize(wanted);
|
||||
}
|
||||
if (!ReadAt(offset, m_readAhead.data(), wanted))
|
||||
{
|
||||
DropReadAhead();
|
||||
return nullptr;
|
||||
}
|
||||
m_readAheadBase = offset;
|
||||
m_readAheadLength = static_cast<std::uint32_t>(wanted);
|
||||
m_readAheadValid = true;
|
||||
if (m_readProgress && !m_readProgress(m_bytesRead, m_size))
|
||||
{
|
||||
m_readCancelled = true;
|
||||
DropReadAhead();
|
||||
return nullptr;
|
||||
}
|
||||
return m_readAhead.data();
|
||||
}
|
||||
|
||||
std::uint8_t MatHdf5::U8(std::uint64_t offset) const
|
||||
{
|
||||
return (offset < m_data.size()) ? m_data[static_cast<std::size_t>(offset)] : 0;
|
||||
const std::uint8_t* bytes = Cached(offset, 1);
|
||||
return (bytes != nullptr) ? *bytes : 0;
|
||||
}
|
||||
|
||||
std::uint16_t MatHdf5::U16(std::uint64_t offset) const
|
||||
@@ -121,12 +324,13 @@ namespace jdi
|
||||
error.clear();
|
||||
Close();
|
||||
|
||||
// ⚠️ **内存映射而不是整文件读入**:SDI 导出的 `.mat` 可能是 GB 级,
|
||||
// “整文件读入 + 分块解码成字节数组 + 再拷成 double”会同时驻留三份内存
|
||||
//(用户 2026-10-19 报告,并在本轮改为流式填充)。
|
||||
// 映射只占虚拟地址空间:未被解析到的页不会驻留,系统也可以在内存压力下回收。
|
||||
// ⚠️ **随机读,不映射**:SDI 导出的 `.mat` 可能是 GB 级,整文件读入会同时驻留
|
||||
//「文件 + 未压缩字节 + double」三份内存;而内存映射在文件被截断 / 网络共享掉线 /
|
||||
// 移动介质拔出时会触发 SEH(页访问异常),GCC 下的 `try/catch` 捕获不到 ⇒ 进程直接崩溃。
|
||||
// 这里改用 `ReadFile` + 元数据块缓存:任何 I/O 失败都只会变成 `false`。
|
||||
const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
|
||||
nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
error = L"无法打开文件。";
|
||||
@@ -139,29 +343,14 @@ namespace jdi
|
||||
error = L"无法读取文件大小。";
|
||||
return false;
|
||||
}
|
||||
const HANDLE mapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr);
|
||||
if (mapping == nullptr)
|
||||
{
|
||||
CloseHandle(file);
|
||||
error = L"无法映射文件(请确认文件可读且未被独占)。";
|
||||
return false;
|
||||
}
|
||||
const void* view = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0);
|
||||
if (view == nullptr)
|
||||
{
|
||||
CloseHandle(mapping);
|
||||
CloseHandle(file);
|
||||
error = L"无法映射文件视图。";
|
||||
return false;
|
||||
}
|
||||
m_fileHandle = file;
|
||||
m_mapHandle = mapping;
|
||||
m_data.pointer = static_cast<const std::uint8_t*>(view);
|
||||
m_data.length = static_cast<std::size_t>(fileSize.QuadPart);
|
||||
m_size = static_cast<std::uint64_t>(fileSize.QuadPart);
|
||||
m_bytesRead = 0;
|
||||
m_readCancelled = false;
|
||||
|
||||
// 字节已全部可访问(映射完成):把“已加载 = 总大小”报给遮罩层,
|
||||
// 之后的时间主要在“解码样本流”阶段(由 MatLoader 逐流推进)。
|
||||
if (progress && !progress(m_data.size(), m_data.size()))
|
||||
// 打开阶段已知道总大小:立即上报一次,遮罩层可以显示「0 / 总大小」,
|
||||
// 之后的真实进度由 `ReadAt` 累计的已读字节数驱动。
|
||||
if (progress && !progress(0, m_size))
|
||||
{
|
||||
Close();
|
||||
error.clear(); // 用户取消:不显示错误
|
||||
@@ -169,17 +358,16 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 超级块:签名可能出现在 0、512、1024…(MATLAB 用 512 字节 user block)。
|
||||
std::size_t signature = m_data.size();
|
||||
for (std::size_t index = 0; index + 8 <= m_data.size() && index <= 4096; index += 512)
|
||||
std::size_t signature = static_cast<std::size_t>(m_size);
|
||||
for (std::size_t index = 0; index + 8 <= m_size && index <= 4096; index += 512)
|
||||
{
|
||||
if (m_data[index] == 0x89 && m_data[index + 1] == 'H' && m_data[index + 2] == 'D' &&
|
||||
m_data[index + 3] == 'F')
|
||||
if (Tag(index, "\x89HDF"))
|
||||
{
|
||||
signature = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (signature == m_data.size())
|
||||
if (signature == m_size)
|
||||
{
|
||||
error = L"不是 HDF5(MATLAB 7.3)格式。";
|
||||
return false;
|
||||
@@ -200,7 +388,7 @@ namespace jdi
|
||||
m_base = U64(signature + 12);
|
||||
m_root = Resolve(U64(signature + 36));
|
||||
}
|
||||
if (m_root == 0 || m_root >= m_data.size())
|
||||
if (m_root == 0 || m_root >= m_size)
|
||||
{
|
||||
error = L"HDF5 超级块损坏。";
|
||||
return false;
|
||||
@@ -210,31 +398,27 @@ namespace jdi
|
||||
|
||||
void MatHdf5::Close()
|
||||
{
|
||||
// 先解除视图,再关映射与文件句柄(顺序不能反)。
|
||||
if (m_data.pointer != nullptr)
|
||||
{
|
||||
UnmapViewOfFile(m_data.pointer);
|
||||
}
|
||||
if (m_mapHandle != nullptr)
|
||||
{
|
||||
CloseHandle(static_cast<HANDLE>(m_mapHandle));
|
||||
m_mapHandle = nullptr;
|
||||
}
|
||||
DropReadAhead();
|
||||
m_cache.clear();
|
||||
m_lastSlot = 0;
|
||||
if (m_fileHandle != nullptr)
|
||||
{
|
||||
CloseHandle(static_cast<HANDLE>(m_fileHandle));
|
||||
m_fileHandle = nullptr;
|
||||
}
|
||||
m_data.clear();
|
||||
m_size = 0;
|
||||
m_bytesRead = 0;
|
||||
m_readCancelled = false;
|
||||
m_base = 0;
|
||||
m_root = 0;
|
||||
m_heaps.clear();
|
||||
m_readProgress = ReadProgress();
|
||||
}
|
||||
|
||||
void MatHdf5::ReadMessages(std::uint64_t address, std::vector<Message>& messages) const
|
||||
{
|
||||
messages.clear();
|
||||
if (address + 8 > m_data.size())
|
||||
if (address + 8 > m_size)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -253,18 +437,21 @@ namespace jdi
|
||||
std::uint64_t offset = blocks[blockIndex].first;
|
||||
const std::uint64_t end =
|
||||
(blocks[blockIndex].second > 0) ? (offset + blocks[blockIndex].second) : 0;
|
||||
while (read < count && offset + 8 <= m_data.size() && (end == 0 || offset + 8 <= end))
|
||||
while (read < count && offset + 8 <= m_size && (end == 0 || offset + 8 <= end))
|
||||
{
|
||||
Message message;
|
||||
message.type = U16(offset);
|
||||
const std::uint16_t messageSize = U16(offset + 2);
|
||||
offset += 8;
|
||||
if (offset + messageSize > m_data.size())
|
||||
if (offset + messageSize > m_size)
|
||||
{
|
||||
break;
|
||||
}
|
||||
message.data.resize(messageSize);
|
||||
if (messageSize > 0 && !ReadAt(offset, message.data.data(), messageSize))
|
||||
{
|
||||
break;
|
||||
}
|
||||
message.data.assign(m_data.begin() + static_cast<std::ptrdiff_t>(offset),
|
||||
m_data.begin() + static_cast<std::ptrdiff_t>(offset + messageSize));
|
||||
offset += (messageSize + 7u) & ~static_cast<std::uint16_t>(7u);
|
||||
++read;
|
||||
if (message.type == kMessageContinuation && message.data.size() >= 16)
|
||||
@@ -286,9 +473,9 @@ namespace jdi
|
||||
std::vector<std::pair<std::string, std::uint64_t>>& out) const
|
||||
{
|
||||
int guard = 0;
|
||||
while (node != 0 && node + 24 < m_data.size() && guard++ < 100000)
|
||||
while (node != 0 && node + 24 < m_size && guard++ < 100000)
|
||||
{
|
||||
if (std::memcmp(&m_data[static_cast<std::size_t>(node)], "TREE", 4) != 0)
|
||||
if (!Tag(node, "TREE"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -323,8 +510,7 @@ namespace jdi
|
||||
void MatHdf5::WalkSymbolNode(std::uint64_t node, std::uint64_t heapData,
|
||||
std::vector<std::pair<std::string, std::uint64_t>>& out) const
|
||||
{
|
||||
if (node == 0 || node + 8 > m_data.size() ||
|
||||
std::memcmp(&m_data[static_cast<std::size_t>(node)], "SNOD", 4) != 0 || heapData == 0)
|
||||
if (node == 0 || node + 8 > m_size || !Tag(node, "SNOD") || heapData == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -332,19 +518,20 @@ namespace jdi
|
||||
std::uint64_t offset = node + 8;
|
||||
for (std::uint16_t index = 0; index < count; ++index)
|
||||
{
|
||||
if (offset + 40 > m_data.size())
|
||||
if (offset + 40 > m_size)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::uint64_t nameOffset = U64(offset);
|
||||
const std::uint64_t objectAddress = Resolve(U64(offset + 8));
|
||||
offset += 40;
|
||||
if (objectAddress == 0 || heapData + nameOffset >= m_data.size())
|
||||
if (objectAddress == 0 || heapData + nameOffset >= m_size)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const char* name = reinterpret_cast<const char*>(m_data.data() + heapData + nameOffset);
|
||||
out.emplace_back(std::string(name), objectAddress);
|
||||
std::string name;
|
||||
ReadCString(heapData + nameOffset, name);
|
||||
out.emplace_back(std::move(name), objectAddress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,8 +547,7 @@ namespace jdi
|
||||
const std::uint64_t btree = Resolve(ReadLE(message.data.data(), 8));
|
||||
const std::uint64_t heap = Resolve(ReadLE(message.data.data() + 8, 8));
|
||||
std::uint64_t heapData = 0;
|
||||
if (heap + 32 <= m_data.size() &&
|
||||
std::memcmp(&m_data[static_cast<std::size_t>(heap)], "HEAP", 4) == 0)
|
||||
if (heap + 32 <= m_size && Tag(heap, "HEAP"))
|
||||
{
|
||||
heapData = Resolve(U64(heap + 24));
|
||||
bool known = false;
|
||||
@@ -724,26 +910,45 @@ namespace jdi
|
||||
return sink(dataset.compact.data(), dataset.compact.size(), 0);
|
||||
}
|
||||
|
||||
// 连续布局:按 4MB 切片直接吐出(文件是映射的,这里只是指针运算,不产生额外缓冲)。
|
||||
// 连续布局:按 4MB 切片读出(随机读 + 复用缓冲,不产生整文件大小的额外内存)。
|
||||
if (dataset.layout == LayoutClass::Contiguous)
|
||||
{
|
||||
const std::uint64_t total = dataset.ElementCount();
|
||||
const std::uint64_t totalBytes = total * elementSize;
|
||||
const std::uint64_t available =
|
||||
std::min<std::uint64_t>(dataset.dataBytes,
|
||||
(dataset.dataAddress < m_data.size())
|
||||
? (m_data.size() - dataset.dataAddress)
|
||||
: 0);
|
||||
(dataset.dataAddress < m_size) ? (m_size - dataset.dataAddress)
|
||||
: 0);
|
||||
const std::uint64_t bytes = std::min(totalBytes, available);
|
||||
// ⚠️ 元数据声明了 N 个元素、文件里却不够:明确失败,而不是静默截短
|
||||
//(静默截短会让曲线“看起来只有一半”,很难诊断)。
|
||||
if (bytes < totalBytes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::size_t slice = kStreamSliceBytes - (kStreamSliceBytes % elementSize);
|
||||
if (slice == 0)
|
||||
{
|
||||
slice = elementSize;
|
||||
}
|
||||
if (m_slice.size() < slice)
|
||||
{
|
||||
m_slice.resize(slice);
|
||||
}
|
||||
for (std::uint64_t offset = 0; offset < bytes; offset += slice)
|
||||
{
|
||||
const std::size_t count = static_cast<std::size_t>(std::min<std::uint64_t>(slice, bytes - offset));
|
||||
if (!sink(m_data.data() + dataset.dataAddress + offset, count, offset / elementSize))
|
||||
const std::size_t count =
|
||||
static_cast<std::size_t>(std::min<std::uint64_t>(slice, bytes - offset));
|
||||
if (!ReadAt(dataset.dataAddress + offset, m_slice.data(), count))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_readProgress && !m_readProgress(m_bytesRead, m_size))
|
||||
{
|
||||
m_readCancelled = true;
|
||||
return false;
|
||||
}
|
||||
if (!sink(m_slice.data(), count, offset / elementSize))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -788,8 +993,7 @@ namespace jdi
|
||||
{
|
||||
const std::uint64_t node = stack.back();
|
||||
stack.pop_back();
|
||||
if (node == 0 || node + 24 > m_data.size() ||
|
||||
std::memcmp(&m_data[static_cast<std::size_t>(node)], "TREE", 4) != 0)
|
||||
if (node == 0 || node + 24 > m_size || !Tag(node, "TREE"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -799,7 +1003,7 @@ namespace jdi
|
||||
std::uint64_t offset = node + 8 + 8 + 8;
|
||||
for (std::uint16_t index = 0; index < static_cast<std::uint16_t>(entries + 1u); ++index)
|
||||
{
|
||||
const std::uint32_t chunkSize = (offset + 4 <= m_data.size()) ? U32(offset) : 0;
|
||||
const std::uint32_t chunkSize = (offset + 4 <= m_size) ? U32(offset) : 0;
|
||||
std::vector<std::uint64_t> chunkOffset(rank, 0);
|
||||
for (std::size_t axis = 0; axis < rank; ++axis)
|
||||
{
|
||||
@@ -817,12 +1021,38 @@ namespace jdi
|
||||
stack.push_back(child);
|
||||
continue;
|
||||
}
|
||||
if (child == 0 || chunkSize == 0 || child + chunkSize > m_data.size())
|
||||
if (child == 0 || chunkSize == 0 || child + chunkSize > m_size)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::uint8_t* source = m_data.data() + child;
|
||||
// 顺序预读:分块数据通常连续存放,逐 chunk 读会退化成大量小系统调用。
|
||||
const std::uint8_t* source = nullptr;
|
||||
if (chunkSize <= kReadAheadBytes)
|
||||
{
|
||||
source = ReadData(child, chunkSize);
|
||||
}
|
||||
if (source == nullptr)
|
||||
{
|
||||
if (m_chunk.size() < chunkSize)
|
||||
{
|
||||
m_chunk.resize(chunkSize);
|
||||
}
|
||||
if (!ReadAt(child, m_chunk.data(), chunkSize))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (m_readProgress && !m_readProgress(m_bytesRead, m_size))
|
||||
{
|
||||
m_readCancelled = true;
|
||||
return false;
|
||||
}
|
||||
source = m_chunk.data();
|
||||
}
|
||||
if (m_readCancelled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::size_t sourceSize = chunkSize;
|
||||
if (dataset.deflate && chunkSize != expected)
|
||||
{
|
||||
@@ -844,6 +1074,29 @@ namespace jdi
|
||||
// 归一化后 rank=2 ⇒「每行只有 1 字节」)。若逐行回调,就是上千万次
|
||||
// 小 memcpy + 上千万次 vector 分配,实测把解压吞吐拖到 ~11MB/s。
|
||||
// 因此这里把**目标连续的行合并成一次回调**(典型情况整块只需 1 次)。
|
||||
//
|
||||
// 快路径(rank == 2 且第二维完全对齐):整块在目标里就是一段连续区域,
|
||||
// 直接一次回调,省掉「每行算坐标 + 判断合并」的循环。
|
||||
// 实测 BIG_MAT.mat:`#data#` 为 129,139,768 字节 × 32KB chunk,每 chunk 32768 行,
|
||||
// 逐行循环共 12.9 亿次迭代(约占总加载时间的一半以上)。
|
||||
if (rank == 2 && chunkDims[1] == dims[1] && chunkDims[1] > 0)
|
||||
{
|
||||
const std::uint64_t startRow =
|
||||
std::min<std::uint64_t>(chunkOffset[0], dims[0]);
|
||||
const std::uint64_t rowCount =
|
||||
std::min<std::uint64_t>(chunkDims[0], dims[0] - startRow);
|
||||
const std::uint64_t byteCount = rowCount * rowBytes;
|
||||
if (startRow < dims[0] && byteCount <= static_cast<std::uint64_t>(sourceSize))
|
||||
{
|
||||
if (!sink(source, static_cast<std::size_t>(byteCount),
|
||||
startRow * chunkDims[1]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<std::size_t> coordinate(rank > 0 ? rank - 1 : 0, 0);
|
||||
std::uint64_t runElement = 0; // 合并段的起始元素下标
|
||||
std::size_t runSourceOffset = 0; // 合并段的源偏移
|
||||
|
||||
+53
-25
@@ -142,6 +142,7 @@ namespace jdi
|
||||
double high = 1.0; /**< 本阶段占的进度区间上界。 */
|
||||
std::uint64_t read = 0; /**< 已读字节。 */
|
||||
std::uint64_t total = 0; /**< 总字节。 */
|
||||
mutable double lastRatio = -1.0; /**< 已上报的最大比例(保证单调不回退)。 */
|
||||
|
||||
/** @brief 上报一个 0-1 的阶段内进度(向宿主传递字节数与百分比)。 */
|
||||
bool Report(double fraction) const
|
||||
@@ -158,11 +159,21 @@ namespace jdi
|
||||
{
|
||||
fraction = 1.0;
|
||||
}
|
||||
double ratio = low + (high - low) * fraction;
|
||||
// 「已读字节」与「逐流解码」是两条独立进度来源:取较大值,避免进度条回退。
|
||||
if (ratio < lastRatio)
|
||||
{
|
||||
ratio = lastRatio;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastRatio = ratio;
|
||||
}
|
||||
LoadProgressInfo info;
|
||||
info.ratio = low + (high - low) * fraction;
|
||||
info.ratio = ratio;
|
||||
// 总字节已知时一并上报(遮罩层据此显示“已加载 / 总大小”)。
|
||||
info.hasBytes = (total > 0);
|
||||
info.bytesRead = (total > 0) ? read : 0;
|
||||
info.bytesRead = (total > 0) ? std::min(read, total) : 0;
|
||||
info.totalBytes = total;
|
||||
return (*sink)(info);
|
||||
}
|
||||
@@ -503,13 +514,19 @@ namespace jdi
|
||||
{
|
||||
if (file.IsDataset(address))
|
||||
{
|
||||
// ⚠️ 只解析元信息(维度),**不要**用 `ReadDataset`:那会把整块数据再解码一遍。
|
||||
// 旧实现先 `ReadNumbers` 解码一次、再 `ReadDataset` 只为拿 `dims`,
|
||||
// 大矩阵的时间与内存都翻倍。
|
||||
MatHdf5::Dataset dataset;
|
||||
if (!file.ReadDatasetInfo(address, dataset) || dataset.dims.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::vector<double> numbers;
|
||||
if (!file.ReadNumbers(address, numbers) || numbers.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
MatHdf5::Dataset dataset;
|
||||
file.ReadDataset(address, dataset);
|
||||
std::size_t rows = 1;
|
||||
std::size_t columns = 1;
|
||||
if (dataset.dims.size() >= 2)
|
||||
@@ -566,22 +583,24 @@ namespace jdi
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 进度拆成三段:
|
||||
// 0.00 – 0.02 映射文件(内存映射,几乎瞬时;遮罩层仍能看到“已加载 = 总大小”)
|
||||
// 0.02 – 0.98 逐条解码样本流(每个流解压后直接填进数据模型,含解压开销)
|
||||
// 0.00 – 0.02 打开文件(立刻知道总大小;遮罩层显示「0 / 总大小」)
|
||||
// 0.02 – 0.98 随机读取 + 逐条解码样本流(进度按**实际已读字节**推进)
|
||||
// 0.98 – 1.00 收尾
|
||||
// ⚠️ 旧实现把 0–0.55 全给了“读文件字节”(因为旧实现真的整文件读入);
|
||||
// 现在改成内存映射后那一段几乎没有耗时,若不调整权重,进度条会“瞬间跳到 55% 再慢慢走”。
|
||||
// ⚠️ 旧实现是内存映射:字节瞬间「全部可访问」⇒ 文件读取百分比立刻 100%,
|
||||
// 而真正的解析还要跑很久(用户 2026-10-20 报告“这部分不太对劲”)。
|
||||
// 现在每次读盘都累计到 `MatHdf5::BytesRead()`,百分比随真实 I/O 推进。
|
||||
// ------------------------------------------------------------------
|
||||
ProgressReporter readReporter;
|
||||
readReporter.sink = &progress;
|
||||
readReporter.low = 0.0;
|
||||
readReporter.high = 0.02;
|
||||
ProgressReporter parseReporter;
|
||||
parseReporter.sink = &progress;
|
||||
parseReporter.low = 0.02;
|
||||
parseReporter.high = 0.98;
|
||||
|
||||
const MatHdf5::OpenProgress openProgress =
|
||||
[&readReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
|
||||
[&parseReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
|
||||
{
|
||||
readReporter.read = bytesRead;
|
||||
readReporter.total = totalBytes;
|
||||
return readReporter.Report(totalBytes > 0 ? static_cast<double>(bytesRead) /
|
||||
parseReporter.read = bytesRead;
|
||||
parseReporter.total = totalBytes;
|
||||
return parseReporter.Report(totalBytes > 0 ? static_cast<double>(bytesRead) /
|
||||
static_cast<double>(totalBytes)
|
||||
: 0.0);
|
||||
};
|
||||
@@ -589,12 +608,17 @@ namespace jdi
|
||||
{
|
||||
return false; // 用户取消时 `error` 为空
|
||||
}
|
||||
ProgressReporter parseReporter;
|
||||
parseReporter.sink = &progress;
|
||||
parseReporter.low = 0.02;
|
||||
parseReporter.high = 0.98;
|
||||
parseReporter.read = readReporter.total;
|
||||
parseReporter.total = 0; // 字节已读完:本阶段只报百分比
|
||||
|
||||
// 流式解码期间的字节进度(每读一块回调一次);返回 false 即取消。
|
||||
file.SetReadProgress(
|
||||
[&parseReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
|
||||
{
|
||||
parseReporter.read = bytesRead;
|
||||
parseReporter.total = totalBytes;
|
||||
return parseReporter.Report(totalBytes > 0 ? static_cast<double>(bytesRead) /
|
||||
static_cast<double>(totalBytes)
|
||||
: 0.0);
|
||||
});
|
||||
if (!parseReporter.Report(0.0))
|
||||
{
|
||||
error.clear();
|
||||
@@ -609,6 +633,10 @@ namespace jdi
|
||||
{
|
||||
if (!LoadSdi(file, refs, sigstream, stem, groups, error, parseReporter))
|
||||
{
|
||||
if (file.Cancelled())
|
||||
{
|
||||
error.clear(); // 用户取消:不显示错误
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -637,9 +665,9 @@ namespace jdi
|
||||
{
|
||||
LoadProgressInfo info;
|
||||
info.ratio = 1.0;
|
||||
info.hasBytes = (readReporter.total > 0);
|
||||
info.bytesRead = readReporter.total;
|
||||
info.totalBytes = readReporter.total;
|
||||
info.hasBytes = (file.FileBytes() > 0);
|
||||
info.bytesRead = file.FileBytes();
|
||||
info.totalBytes = file.FileBytes();
|
||||
progress(info);
|
||||
}
|
||||
return true;
|
||||
|
||||
+679
-157
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user