.mat 改为流式解码(内存映射 + 逐 chunk 填充);MF4 支持 ##DL/##DZ/##HL;光标数据点被时间面板遮住时改三角

1) .mat(BIG_MAT.mat 1.29GB / 5 信号 x 16,142,471 采样)
   旧链路同时驻留三份内存:整文件 fread 进 vector、整个数据集解压成字节数组、再 memcpy 成
   double;外加 streams 缓存与每条信号各一份时间列拷贝。实测私有内存 +1233.9MB、117s。
   现改为:
   - MatHdf5::Open 用 CreateFileW + CreateFileMappingW + MapViewOfFile(MappedBytes 只读视图,
     解析代码零改动;虚拟地址不等于物理内存,未访问页不驻留);
   - 新增 ReadDatasetInfo(只读元信息)+ StreamDataset(ByteSink)(连续布局 4MB 切片、
     分块布局逐 chunk inflate)+ ReadDoubleStream(SDI #data# 字节流按 8 字节一组直接写 double,
     7 字节尾巴处理非 8 对齐 chunk);ReadNumbers 改为流式转换;
   - MatLoader 先收集信号清单,再按(分组,时间流)合并为一个 TableBlock:时间列按内容去重、
     数据列直接 move;进度改成 0-0.02 映射 / 0.02-0.98 逐流解码 / 1.0。
   结果:117.3s -> 12.5s,私有内存 +1233.9MB -> +740.4MB(最终列数据 738.9MB)。
   关键性能修正:SDI 的 #data# 是 dims=[N]、chunk=[N,1],归一化后每行只有 1 字节;旧的逐行回调
   (且每行分配 coordinate 向量)只有 11MB/s。把目标连续的行合并成一次回调后约 110MB/s。
   顺带把 inflate 的 Huffman 解码改成 9 位快查表,并抽到新的 Deflate.hpp/.cpp 供 MF4 复用。

2) MDF4 数据块
   Problem-MF4.mf4 的第二个数据组把记录写在 ##DL(数据列表,两个 ##DT:131040+504 字节)
   里,旧实现只认 ##DT,整组被跳过,于是 8 个信号只读出 4 个。
   新增 CollectDataSegments(递归展开 ##DL / ##HL,叶子识别 ##DT / ##DV / ##DZ)与
   ReadRecordSegments(跨段拼接、压缩段先 inflate、非压缩段仍按 4MB 分块读),
   压缩段的记录长度改用 max(cg_record_id_len*8, 各通道 bitOffset+bitCount) 推算(载荷是压缩长度)。
   实测 8 个信号:gVP_WindowLearnSt[0] 94 行、gVR_WinOpenReq[0] 9396 行,时间列单调。
   ##SD(已排序数据)仍未实现,这类数据组会被跳过。

   另按要求只做检查、不改代码:该文件中 gVP_WindowLearnSt[0] 与 gVR_WinOpenReq[0] 都存在
   值枚举文本(##CC,cc_type=7;links[4+i] 对应 cc_val[i]),清单已写入 Documents/agent_mem.md。

3) 光标数据点标记
   光标的时间读数与差值胶囊绘制在绘图区底部、且在取值标注之后绘制,靠近底部的数据点圆点
   会被完全盖住。新增 MarkerCoveredByCursorLabel(圆心落入面板或重叠面积达到半个圆)
   与 CoverHiddenMarker(改成向下的三角、锚点放到面板上沿上方),在原始信号与派生曲线胶囊
   两处调用,语义与“数据点在视界下方”一致。

验证(全部离屏:SW_HIDE 窗口 + 只注入合成消息 + BELOW_NORMAL,不占用鼠标键盘屏幕):
- 新增 .preflight/round4check 34/34;A/B:临时禁用 ##DL 支持 => 2 项如实失败;
  临时禁用 CoverHiddenMarker => 3 项如实失败(含像素断言为 0);
- 回归:.preflight/round3check 31/31、editcheck 20/20、check 137/137;
- make release / make debug 零警告;Build/Release/Target.exe 785,920 字节(< 800KB)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-19 20:13:17 +08:00
1 parent be76f523f3
commit 75f0fafd41
10 files changed
+1677 -795

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
#ifndef __DEFLATE_HPP__
#define __DEFLATE_HPP__
#include <cstddef>
#include <cstdint>
#include <vector>
namespace jdi
{
/**
* @brief 解压一个 zlib(RFC 1950)流(内含 DEFLATE / RFC 1951 数据)。
*
* 本项目**不链接 zlib**(`-lz` 会让 Release 产物多出约 46KB),只用这一份紧凑实现:
* * HDF5 分块数据集的 deflate 过滤器(MATLAB `.mat`);
* * MDF4 的 `##DZ` 压缩数据块。
*
* ⚠️ 只接受「无预设字典」的 deflate 流;末尾的 Adler32 校验值不做验证(只跳过)。
*
* @param [IN] source 压缩数据起始指针(含 2 字节 zlib 头)。
* @param [IN] sourceSize 压缩数据长度(含头与 4 字节尾)。
* @param [OUT] out 解压结果(函数内部先 `clear`,再追加写入)。
* @param [IN] expectedSize 期望的解压长度;>0 时预分配容量,避免反复扩容。
* @return 成功返回 true。
*/
bool InflateZlib(const std::uint8_t* source, std::size_t sourceSize,
std::vector<std::uint8_t>& out, std::size_t expectedSize = 0);
/**
* @brief 判断一段数据是否是 zlib 流(2 字节头:CM=8、FCHECK 正确、无预设字典)。
* @param [IN] source 数据起始指针。
* @param [IN] sourceSize 数据长度。
* @return 很像 zlib 流返回 true。
*/
bool LooksLikeZlib(const std::uint8_t* source, std::size_t sourceSize);
} // namespace jdi
#endif
+85 -7
View File
@@ -75,17 +75,37 @@ namespace jdi
std::uint64_t chunkAddress = 0; /**< 分块数据集的 B-tree 地址。 */
std::vector<std::size_t> chunkDims;
bool deflate = false; /**< 是否使用了 deflate(zlib)过滤器。 */
std::uint64_t dataAddress = 0; /**< 连续布局的数据起始地址(已加基地址)。 */
std::uint64_t dataBytes = 0; /**< 连续布局的数据字节数。 */
/** 紧凑布局的字节(在布局消息里,数据量很小)。 */
std::vector<std::uint8_t> compact;
bool ok = false;
/** @brief 元素总数(各维长度相乘)。 */
std::uint64_t ElementCount() const
{
std::uint64_t count = 1;
for (const std::size_t dim : dims)
{
count *= dim;
}
return dims.empty() ? 0 : count;
}
};
/** @brief 读文件进度回调:`已读字节 / 总字节`。返回 false 表示请求取消。 */
using OpenProgress = std::function<bool(std::uint64_t bytesRead, std::uint64_t totalBytes)>;
/**
* @brief 打开文件(一次性读入内存;`.mat` 通常几十 MB,可接受)。
* @brief 打开文件(**内存映射**,不把整个文件读进堆内存)。
*
* ⚠️ 早期版本把整个文件一次读入 `std::vector`(注释里假设“`.mat` 通常几十 MB”),
* 但 SDI 导出的录波文件可能是 **GB 级**:整文件读入 + 分块解码 + 再拷成 double 会同时
* 驻留“文件 + 未压缩字节 + double”三份内存。现改为:
* * 映射文件(只占虚拟地址空间,按页面需求驻留);
* * 分块数据集逐 chunk 解压并**直接写入**自持有的 double 列(见 `ReadDoubleStream`);
* * 进度回调在映射完成后立即报告一次(字节已全部可访问),后续解码阶段由 `MatLoader` 驱动。
*
* ⚠️ 按块读取并回调 `progress`:加载遮罩层的"已加载大小 / 总大小 / 百分比"
* 全靠它驱动。省略回调时行为不变。
* @param [IN] path 文件路径。
* @param [OUT] error 失败原因。
* @param [IN] progress 读取进度回调(可为空)。
@@ -95,6 +115,12 @@ namespace jdi
const OpenProgress& progress = OpenProgress());
void Close();
/** @brief 析构时释放映射(否则栈上对象的映射会留到进程退出)。 */
~MatHdf5() { Close(); }
/** @brief 文件字节数(映射长度)。 */
std::uint64_t FileBytes() const { return static_cast<std::uint64_t>(m_data.size()); }
/** @brief 根分组地址。 */
std::uint64_t Root() const { return m_root; }
@@ -110,15 +136,45 @@ namespace jdi
/** @brief 读取数据集定义(含解码后的原始数据)。 */
bool ReadDataset(std::uint64_t address, Dataset& out) const;
/** @brief 只读取数据集定义(不解码数据;大数据用 `StreamDataset`)。 */
bool ReadDatasetInfo(std::uint64_t address, Dataset& out) const;
/**
* @brief 字节流回调:把数据集内容按块吐出(**不生成整块缓冲**)。
* @param [IN] bytes 块起始指针(仅在回调期间有效)。
* @param [IN] byteCount 块字节数。
* @param [IN] elementOffset 该块在数据集中的起始**元素**下标(行优先)。
* @return 返回 false 中止遍历。
*/
using ByteSink = std::function<bool(const std::uint8_t* bytes, std::size_t byteCount,
std::uint64_t elementOffset)>;
/**
* @brief 流式遍历数据集字节(连续布局按 4MB 切片;分块布局逐个 chunk 解压)。
* @param [IN] dataset `ReadDatasetInfo` 得到的信息。
* @param [IN] sink 字节回调。
* @return 成功返回 true。
*/
bool StreamDataset(const Dataset& dataset, const ByteSink& sink) const;
/** @brief 读取字符串属性(如 `MATLAB_class`、`MATLAB_fields`)。 */
std::string AttributeString(std::uint64_t address, const char* name) const;
/** @brief 读取 `char` 数据集为宽字符串(MATLAB 的 char 是 uint16)。 */
bool ReadText(std::uint64_t address, std::wstring& out) const;
/** @brief 读取数值数据集为 double 序列。 */
/** @brief 读取数值数据集为 double 序列(流式转换,不生成整块字节缓冲)。 */
bool ReadNumbers(std::uint64_t address, std::vector<double>& out) const;
/**
* @brief 读取 Simulink SDI 的字节流数据集(`#data#`)为 double 序列。
*
* SDI 的 `#data#` 是 `uint8` 数据集,长度 = 采样数 × 8:按 8 字节一组重新解释为 double。
* 实现上逐 chunk 解压并直接写入输出缓冲(不产生整块字节数组,也不二次拷贝),
* 因此 GB 级文件也不会出现“文件 + 未压缩字节 + double”三份内存叠加。
*/
bool ReadDoubleStream(std::uint64_t address, std::vector<double>& out) const;
/** @brief 读取引用数据集(HDF5 对象引用 = 对象头地址,相对基地址)。 */
bool ReadReferences(std::uint64_t address, std::vector<std::uint64_t>& out) const;
@@ -139,14 +195,36 @@ namespace jdi
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;
bool DecodeChunked(const Dataset& dataset, std::size_t elementSize,
std::vector<std::uint8_t>& out) const;
void WalkBtree(std::uint64_t node, std::uint64_t heapData,
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
void WalkSymbolNode(std::uint64_t node, std::uint64_t heapData,
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
std::vector<std::uint8_t> m_data;
/**
* @brief 内存映射的文件视图(代替“整个文件读进 `vector`”)。
*
* ⚠️ 行为与 `std::vector<std::uint8_t>` 的**只读子集**一致(`size/data/begin/end/[]`),
* 因此所有“按偏移取字节”的解析代码一行都不用改;但**虚拟地址不等于物理内存**:
* 只有真正被解析到的页才驻留,GB 级文件不会再先吃掉一整个文件大小的私有内存。
*/
struct MappedBytes
{
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;
}
};
MappedBytes m_data; /**< 文件映射视图(只读;语义同字节数组)。 */
void* m_fileHandle = nullptr; /**< `HANDLE`(避免头文件引入 windows.h)。 */
void* m_mapHandle = nullptr;
std::uint64_t m_base = 0;
std::uint64_t m_root = 0;
int m_offsetSize = 8;
+427
View File
@@ -0,0 +1,427 @@
// 内置 inflate(RFC 1950 / 1951)—— 供 HDF5 分块与 MDF4 `##DZ` 共用。
//
// ⚠️ **不能用 zlib(`-lz`)**:链接 libz.a 会让 Release 产物多出约 46KB,
// 而这里只需要「解压」这一个功能,自带的紧凑实现只要几 KB。
#include "Deflate.hpp"
namespace jdi
{
namespace
{
/** @brief 位流读取器(LSB 优先,与 DEFLATE 一致)。 */
struct BitReader
{
const std::uint8_t* data = nullptr;
std::size_t size = 0;
std::size_t position = 0;
std::uint32_t buffer = 0;
int bits = 0;
bool error = false;
bool Fill(int count)
{
while (bits < count)
{
if (position >= size)
{
error = true;
return false;
}
buffer |= static_cast<std::uint32_t>(data[position++]) << bits;
bits += 8;
}
return true;
}
std::uint32_t Read(int count)
{
if (count <= 0)
{
return 0;
}
if (!Fill(count))
{
return 0;
}
const std::uint32_t value = buffer & ((1u << count) - 1u);
buffer >>= count;
bits -= count;
return value;
}
void Align()
{
buffer = 0;
bits = 0;
}
};
/** @brief 规范 Huffman 表(只存计数与按长度排好序的符号 + 快查表)。 */
constexpr int kFastBits = 9;
constexpr int kFastSize = 1 << kFastBits;
struct HuffmanTable
{
std::uint16_t count[16] = {};
std::uint16_t symbol[320] = {};
/**
* @brief 前缀码快查表(9 位):`fastLength[peek] != 0` 时可直接得到符号。
*
* ⚠️ 逐位解码(每读 1 bit 就查一次表)是解压的绝对瓶颈:GB 级 `.mat` 实测
* 只有 ~11MB/s,而 9 位查表一次就能吃掉绝大多数符号(典型单字节字面量长度 8~9)。
*/
std::uint16_t fastSymbol[kFastSize] = {};
std::uint8_t fastLength[kFastSize] = {};
};
bool BuildHuffman(HuffmanTable& table, const std::uint8_t* lengths, int total)
{
for (int index = 0; index < 16; ++index)
{
table.count[index] = 0;
}
for (int index = 0; index < total; ++index)
{
++table.count[lengths[index]];
}
if (table.count[0] == total)
{
return true; // 全部长度为 0:该表没有任何码(动态块里可能是距离表)
}
int left = 1;
for (int length = 1; length < 16; ++length)
{
left <<= 1;
left -= table.count[length];
if (left < 0)
{
return false; // 超订(over-subscribed)
}
}
std::uint16_t offsets[16] = {};
offsets[1] = 0;
for (int length = 1; length < 15; ++length)
{
offsets[length + 1] = static_cast<std::uint16_t>(offsets[length] + table.count[length]);
}
for (int index = 0; index < total; ++index)
{
if (lengths[index] != 0)
{
table.symbol[offsets[lengths[index]]++] = static_cast<std::uint16_t>(index);
}
}
// 建 9 位快查表:按规范码给符号分配 code,再按 DEFLATE 的 LSB-first 位序反转。
for (int index = 0; index < kFastSize; ++index)
{
table.fastLength[index] = 0;
}
int code = 0;
int symbolIndex = 0;
for (int length = 1; length < 16; ++length)
{
for (int i = 0; i < table.count[length]; ++i)
{
const int symbol = table.symbol[symbolIndex + i];
if (length <= kFastBits)
{
int reversed = 0;
for (int bit = 0; bit < length; ++bit)
{
reversed |= ((code >> bit) & 1) << (length - 1 - bit);
}
const int step = 1 << length;
for (int fill = reversed; fill < kFastSize; fill += step)
{
table.fastSymbol[fill] = static_cast<std::uint16_t>(symbol);
table.fastLength[fill] = static_cast<std::uint8_t>(length);
}
}
++code;
}
code <<= 1;
symbolIndex += table.count[length];
}
return true;
}
int DecodeSymbol(BitReader& reader, const HuffmanTable& table)
{
// 快路径:一次填到 9 位,查表命中则直接返回(绝大多数文字/长度符号)。
if (reader.bits >= kFastBits || reader.Fill(kFastBits))
{
const int peek = static_cast<int>(reader.buffer & (kFastSize - 1));
const int length = table.fastLength[peek];
if (length > 0)
{
reader.buffer >>= length;
reader.bits -= length;
return table.fastSymbol[peek];
}
}
else
{
// 结尾处不足 9 位:交给慢路径逐位解码(它才是"数据真的结束"的判据)。
reader.error = false;
}
int code = 0;
int first = 0;
int index = 0;
for (int length = 1; length < 16; ++length)
{
code |= static_cast<int>(reader.Read(1));
if (reader.error)
{
return -1;
}
const int count = table.count[length];
if (code - first < count)
{
return table.symbol[index + (code - first)];
}
index += count;
first = (first + count) << 1;
code <<= 1;
}
return -1;
}
const std::uint16_t kLengthBase[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13,
15, 17, 19, 23, 27, 31, 35, 43, 51, 59,
67, 83, 99, 115, 131, 163, 195, 227, 258};
const std::uint8_t kLengthExtra[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
const std::uint16_t kDistanceBase[30] = {1, 2, 3, 4, 5, 7, 9, 13,
17, 25, 33, 49, 65, 97, 129, 193,
257, 385, 513, 769, 1025, 1537, 2049, 3073,
4097, 6145, 8193, 12289, 16385, 24577};
const std::uint8_t kDistanceExtra[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6,
6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
} // namespace
bool LooksLikeZlib(const std::uint8_t* source, std::size_t sourceSize)
{
if (source == nullptr || sourceSize < 6)
{
return false;
}
const std::uint8_t cmf = source[0];
const std::uint8_t flg = source[1];
return (cmf & 0x0F) == 8 && ((static_cast<unsigned>(cmf) * 256u + flg) % 31u) == 0 &&
(flg & 0x20) == 0;
}
bool InflateZlib(const std::uint8_t* source, std::size_t sourceSize,
std::vector<std::uint8_t>& out, std::size_t expectedSize)
{
if (!LooksLikeZlib(source, sourceSize))
{
return false;
}
BitReader reader;
reader.data = source + 2;
reader.size = sourceSize - 6; // 末尾 4 字节是 Adler32
out.clear();
if (expectedSize > 0)
{
out.reserve(expectedSize);
}
HuffmanTable fixedLiterals;
HuffmanTable fixedDistances;
bool fixedReady = false;
for (int block = 0; block < 100000; ++block)
{
const std::uint32_t final = reader.Read(1);
const std::uint32_t type = reader.Read(2);
if (reader.error)
{
return false;
}
if (type == 0)
{
// 存储块(未压缩):对齐后 4 字节长度 + 反码 + 原始字节。
reader.Align();
if (reader.position + 4 > reader.size)
{
return false;
}
const std::uint16_t length = static_cast<std::uint16_t>(
reader.data[reader.position] | (reader.data[reader.position + 1] << 8));
reader.position += 4;
if (reader.position + length > reader.size)
{
return false;
}
out.insert(out.end(), reader.data + reader.position,
reader.data + reader.position + length);
reader.position += length;
}
else if (type == 1 || type == 2)
{
HuffmanTable literals;
HuffmanTable distances;
if (type == 1)
{
if (!fixedReady)
{
std::uint8_t lengths[288];
for (int index = 0; index < 144; ++index)
{
lengths[index] = 8;
}
for (int index = 144; index < 256; ++index)
{
lengths[index] = 9;
}
for (int index = 256; index < 280; ++index)
{
lengths[index] = 7;
}
for (int index = 280; index < 288; ++index)
{
lengths[index] = 8;
}
if (!BuildHuffman(fixedLiterals, lengths, 288))
{
return false;
}
std::uint8_t distanceLengths[30];
for (int index = 0; index < 30; ++index)
{
distanceLengths[index] = 5;
}
if (!BuildHuffman(fixedDistances, distanceLengths, 30))
{
return false;
}
fixedReady = true;
}
literals = fixedLiterals;
distances = fixedDistances;
}
else
{
const int literalCount = static_cast<int>(reader.Read(5)) + 257;
const int distanceCount = static_cast<int>(reader.Read(5)) + 1;
const int codeCount = static_cast<int>(reader.Read(4)) + 4;
if (reader.error || literalCount > 286 || distanceCount > 30)
{
return false;
}
static const std::uint8_t kOrder[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5,
11, 4, 12, 3, 13, 2, 14, 1, 15};
std::uint8_t codeLengths[19] = {};
for (int index = 0; index < codeCount; ++index)
{
codeLengths[kOrder[index]] = static_cast<std::uint8_t>(reader.Read(3));
}
HuffmanTable codeTable;
if (reader.error || !BuildHuffman(codeTable, codeLengths, 19))
{
return false;
}
std::uint8_t lengths[288 + 30] = {};
int produced = 0;
while (produced < literalCount + distanceCount)
{
const int symbol = DecodeSymbol(reader, codeTable);
if (symbol < 0)
{
return false;
}
if (symbol < 16)
{
lengths[produced++] = static_cast<std::uint8_t>(symbol);
continue;
}
int repeat = 0;
std::uint8_t value = 0;
if (symbol == 16)
{
if (produced == 0)
{
return false;
}
value = lengths[produced - 1];
repeat = 3 + static_cast<int>(reader.Read(2));
}
else if (symbol == 17)
{
repeat = 3 + static_cast<int>(reader.Read(3));
}
else
{
repeat = 11 + static_cast<int>(reader.Read(7));
}
if (reader.error || produced + repeat > literalCount + distanceCount)
{
return false;
}
for (int index = 0; index < repeat; ++index)
{
lengths[produced++] = value;
}
}
if (!BuildHuffman(literals, lengths, literalCount) ||
!BuildHuffman(distances, lengths + literalCount, distanceCount))
{
return false;
}
}
for (int symbolIndex = 0; symbolIndex < 100000000; ++symbolIndex)
{
const int symbol = DecodeSymbol(reader, literals);
if (symbol < 0)
{
return false;
}
if (symbol < 256)
{
out.push_back(static_cast<std::uint8_t>(symbol));
continue;
}
if (symbol == 256)
{
break; // 块结束
}
const int lengthCode = symbol - 257;
if (lengthCode >= 29)
{
return false;
}
const std::size_t length =
kLengthBase[lengthCode] + reader.Read(kLengthExtra[lengthCode]);
const int distanceCode = DecodeSymbol(reader, distances);
if (distanceCode < 0 || distanceCode >= 30 || reader.error)
{
return false;
}
const std::size_t distance =
kDistanceBase[distanceCode] + reader.Read(kDistanceExtra[distanceCode]);
if (reader.error || distance == 0 || distance > out.size())
{
return false;
}
const std::size_t start = out.size() - distance;
for (std::size_t index = 0; index < length; ++index)
{
out.push_back(out[start + index]);
}
}
}
else
{
return false; // 类型 3 非法
}
if (final != 0)
{
return true;
}
}
return false;
}
} // namespace jdi
+444 -587
View File
File diff suppressed because it is too large. Load diff
+196 -106
View File
@@ -8,6 +8,7 @@
#include "MatH5.hpp"
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <map>
#include <set>
@@ -66,21 +67,11 @@ namespace jdi
{
return false;
}
MatHdf5::Dataset dataset;
if (!file.ReadDataset(data, dataset) || !dataset.ok)
{
return false;
}
// ⚠️ SDI 的 `#data#` 是**字节流**(`uint8` 数据集,长度 = 采样数 × 8),
// 而不是 double 数据集:必须按 8 字节一组重新解释成 double(`#sampleSize#` 可印证)。
if (dataset.data.size() % sizeof(double) != 0)
{
return false;
}
const std::size_t count = dataset.data.size() / sizeof(double);
out.resize(count);
std::memcpy(out.data(), dataset.data.data(), count * sizeof(double));
return true;
// 而不是 double 数据集:必须按 8 字节一组重新解释成 double。
// `ReadDoubleStream` 会逐 chunk 解压并**直接写入** `out`:
// 不生成“整个数据集的未压缩字节”,也不做第二次拷贝(GB 级文件的关键)。
return file.ReadDoubleStream(data, out) && !out.empty();
}
/** @brief 解析 `#refs#` 里的一个元素(信号 / 子数据集)。 */
@@ -260,53 +251,19 @@ namespace jdi
stack.push_back(std::move(pending));
}
std::map<std::wstring, std::size_t> groupIndex;
std::vector<std::vector<double>> streams; // 流编号 → 样本
std::vector<bool> streamCached;
// 进度:`#sigstream#` 的总流数,用来把"解码样本流"这段映射成阶段内 0-1。
std::size_t streamTotal = 0;
for (const auto& member : file.Members(sigstream))
// ⚠️ **先收集信号清单、再解码**(用户 2026-10-19 要求:边解析边填充):
// * 旧实现把**每条**信号的时间列和时间流各拷一份(`streams` 缓存 + `AppendSignal`
// 里的整份复制)⇒ GB 级文件会出现“文件 + 解压字节 + 每条信号两份 double”叠加;
// * 现在按「分组 + 时间流」合并成一个 `TableBlock`:时间列只存一份、信号列直接
// `std::move` 进数据模型,且每条数据流解码完立刻进入最终缓冲。
struct PendingSignal
{
if (!member.first.empty() && member.first[0] != '#')
{
++streamTotal;
}
}
std::size_t streamDone = 0;
const auto streamFor = [&](int index) -> const std::vector<double>*
{
if (index < 0)
{
return nullptr;
}
if (streams.size() <= static_cast<std::size_t>(index))
{
streams.resize(static_cast<std::size_t>(index) + 1);
streamCached.resize(static_cast<std::size_t>(index) + 1, false);
}
if (!streamCached[static_cast<std::size_t>(index)])
{
const std::uint64_t streamObject =
file.Member(sigstream, std::to_string(index));
if (streamObject == 0 || !ReadStream(file, streamObject, streams[static_cast<std::size_t>(index)]))
{
streams[static_cast<std::size_t>(index)].clear();
}
streamCached[static_cast<std::size_t>(index)] = true;
// ⚠️ 必须在这里回报进度:读一个 42MB 的 `.mat` 时,"解码样本流"才是真正的
// 耗时大头(每个流都要解压 + 拷贝);不报进度的话界面会停在原地等
//(用户 2026-10-19 报告:加载 `.mat` 时已加载大小与百分比都不更新)。
++streamDone;
reporter.Report(streamTotal > 0
? static_cast<double>(streamDone) /
static_cast<double>(streamTotal)
: 1.0);
}
return streams[static_cast<std::size_t>(index)].empty()
? nullptr
: &streams[static_cast<std::size_t>(index)];
std::wstring groupName;
std::wstring signalName;
int timeStream = -1;
int dataStream = -1;
};
std::vector<PendingSignal> signals;
int unnamedSignals = 0;
while (!stack.empty())
{
@@ -335,68 +292,201 @@ namespace jdi
}
continue;
}
if (!element.isSignal)
if (!element.isSignal || element.dataStream < 0)
{
continue;
}
const std::vector<double>* time = streamFor(element.timeStream);
const std::vector<double>* values = streamFor(element.dataStream);
if (values == nullptr)
{
continue;
}
std::vector<double> timeCopy;
if (time != nullptr && time->size() == values->size())
{
timeCopy = *time;
}
else
{
// 时间流缺失/长度不符:退化为采样序号(秒),保证信号仍可显示。
timeCopy.resize(values->size());
for (std::size_t index = 0; index < values->size(); ++index)
{
timeCopy[index] = static_cast<double>(index);
}
}
std::wstring groupName;
PendingSignal signal;
for (const std::wstring& piece : pending.path)
{
if (!groupName.empty())
if (!signal.groupName.empty())
{
groupName += L"-";
signal.groupName += L"-";
}
groupName += piece;
signal.groupName += piece;
}
if (groupName.empty())
if (signal.groupName.empty())
{
groupName = stem;
signal.groupName = stem;
}
std::wstring signalName = element.name;
if (signalName.empty())
signal.signalName = element.name;
if (signal.signalName.empty())
{
signalName = L"信号" + std::to_wstring(++unnamedSignals);
signal.signalName = L"信号" + std::to_wstring(++unnamedSignals);
}
signal.timeStream = element.timeStream;
signal.dataStream = element.dataStream;
signals.push_back(std::move(signal));
}
std::size_t index = 0;
const auto existing = groupIndex.find(groupName);
// 2) 解码:每个(分组,时间流)一个时间轴块;每条数据流只解码一次并直接进列。
std::map<std::wstring, std::size_t> groupIndex;
std::map<std::pair<std::wstring, int>, std::size_t> blockIndex;
std::size_t decoded = 0;
const auto reportStream = [&]()
{
++decoded;
const double fraction = (signals.empty())
? 1.0
: static_cast<double>(decoded) /
static_cast<double>(signals.size());
return reporter.Report(std::min(1.0, fraction));
};
for (PendingSignal& signal : signals)
{
// 分组(每个分组名对应一个 `LoadedSourceGroup`)。
std::size_t groupSlot = 0;
const auto existing = groupIndex.find(signal.groupName);
if (existing == groupIndex.end())
{
LoadedSourceGroup group;
group.name = groupName;
group.source.sourceName = groupName;
group.name = signal.groupName;
group.source.sourceName = signal.groupName;
groups.push_back(std::move(group));
index = groups.size() - 1;
groupIndex.emplace(groupName, index);
groupSlot = groups.size() - 1;
groupIndex.emplace(signal.groupName, groupSlot);
}
else
{
index = existing->second;
groupSlot = existing->second;
}
LoadedSource& source = groups[groupSlot].source;
// 数据列:直接解码到最终 vector(不再经过 `Dataset.data` 的整块字节)。
std::vector<double> values;
const std::uint64_t dataObject = file.Member(sigstream, std::to_string(signal.dataStream));
if (dataObject == 0 || !ReadStream(file, dataObject, values) || values.empty())
{
if (!reportStream())
{
return false;
}
continue;
}
// 找(或建)该信号所属的时间轴块。
const std::pair<std::wstring, int> key{signal.groupName, signal.timeStream};
std::size_t blockSlot = 0;
const auto blockFound = blockIndex.find(key);
if (blockFound == blockIndex.end())
{
TableBlock block;
block.timeHeader = L"time";
std::vector<double> time;
bool haveTime = false;
if (signal.timeStream >= 0)
{
const std::uint64_t timeObject =
file.Member(sigstream, std::to_string(signal.timeStream));
haveTime = timeObject != 0 && ReadStream(file, timeObject, time) &&
!time.empty();
}
if (!haveTime)
{
// 时间流缺失:退化为采样序号(秒),保证信号仍可显示(长度在下面裁剪)。
time.resize(values.size());
for (std::size_t index = 0; index < values.size(); ++index)
{
time[index] = static_cast<double>(index);
}
}
// ⚠️ **时间轴按内容去重**:SDI 里每条 timeseries 都有自己的时间流,
// 但内容通常完全一致。若同一分组里已经有一模一样的时间列,就直接复用它
//(否则每条信号各存一份时间轴:1.29GB / 5 信号的文件会多占 ~500MB)。
std::size_t reused = SIZE_MAX;
for (std::size_t index = 0; index < source.blocks.size(); ++index)
{
const std::vector<double>& existingTime = source.blocks[index].table.columns[0];
if (existingTime.size() != time.size())
{
continue;
}
if (time.empty() ||
std::memcmp(existingTime.data(), time.data(),
time.size() * sizeof(double)) == 0)
{
reused = index;
break;
}
}
if (reused != SIZE_MAX)
{
blockIndex.emplace(key, reused);
blockSlot = reused;
}
else
{
block.table.headers.push_back(block.timeHeader);
block.table.columns.push_back(std::move(time));
source.blocks.push_back(std::move(block));
blockSlot = source.blocks.size() - 1;
blockIndex.emplace(key, blockSlot);
}
}
else
{
blockSlot = blockFound->second;
}
TableBlock& block = source.blocks[blockSlot];
// 时间列长度与信号不一致(损坏 / 异构)⇒ 单独成块,用采样序号当时间,
// 保持“每条信号都能显示”的既有语义,而不是把数据截掉或错位。
if (block.table.columns[0].size() != values.size())
{
TableBlock own;
own.timeHeader = L"time";
std::vector<double> time(values.size());
for (std::size_t index = 0; index < values.size(); ++index)
{
time[index] = static_cast<double>(index);
}
own.table.headers.push_back(own.timeHeader);
own.table.columns.push_back(std::move(time));
own.table.headers.push_back(signal.signalName);
own.table.columns.push_back(std::move(values));
source.blocks.push_back(std::move(own));
if (!reportStream())
{
return false;
}
continue;
}
block.table.headers.push_back(signal.signalName);
block.table.columns.push_back(std::move(values));
if (!reportStream())
{
return false;
}
}
// 3) 统一行数:同一块内取各列最小长度(时间流与数据流长度不一致时按最小裁剪)。
for (LoadedSourceGroup& group : groups)
{
for (TableBlock& block : group.source.blocks)
{
std::size_t rows = SIZE_MAX;
for (const std::vector<double>& column : block.table.columns)
{
rows = std::min(rows, column.size());
}
if (rows == SIZE_MAX)
{
rows = 0;
}
for (std::vector<double>& column : block.table.columns)
{
if (column.size() > rows)
{
column.resize(rows);
}
}
block.table.rowCount = rows;
}
AppendSignal(groups[index].source, signalName, std::move(timeCopy),
std::vector<double>(*values));
}
if (groups.empty())
@@ -475,17 +565,17 @@ namespace jdi
MatHdf5 file;
// ------------------------------------------------------------------
// 进度拆成三段(用户 2026-10-19 报告:加载 `.mat` 时已加载大小与百分比都不更新,
// 界面只是原地等待):
// 0.00 – 0.55 读文件字节(遮罩层显示"已加载 / 总大小")
// 0.55 – 0.98 解码样本流(每个流解码后推进,含解压与拷贝)
// 进度拆成三段:
// 0.00 – 0.02 映射文件(内存映射,几乎瞬时;遮罩层仍能看到“已加载 = 总大小”)
// 0.02 – 0.98 逐条解码样本流(每个流解压后直接填进数据模型,含解压开销)
// 0.98 – 1.00 收尾
// 读文件阶段把 `hasBytes` 置真;之后字节数已满,只推进百分比。
// ⚠️ 旧实现把 0–0.55 全给了“读文件字节”(因为旧实现真的整文件读入);
// 现在改成内存映射后那一段几乎没有耗时,若不调整权重,进度条会“瞬间跳到 55% 再慢慢走”。
// ------------------------------------------------------------------
ProgressReporter readReporter;
readReporter.sink = &progress;
readReporter.low = 0.0;
readReporter.high = 0.55;
readReporter.high = 0.02;
const MatHdf5::OpenProgress openProgress =
[&readReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
{
@@ -501,7 +591,7 @@ namespace jdi
}
ProgressReporter parseReporter;
parseReporter.sink = &progress;
parseReporter.low = 0.55;
parseReporter.low = 0.02;
parseReporter.high = 0.98;
parseReporter.read = readReporter.total;
parseReporter.total = 0; // 字节已读完:本阶段只报百分比
+252 -95
View File
@@ -1,5 +1,6 @@
#include "Mf4Loader.hpp"
#include "Deflate.hpp"
#include "Utf8Util.hpp"
#include <windows.h>
@@ -8,6 +9,7 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <functional>
#include <limits>
#include <string>
#include <vector>
@@ -499,6 +501,171 @@ namespace jdi
}
}
/**
* @brief 一段原始记录数据(MDF4 的 `##DT` / `##DZ` / `##DV` 块的数据区)。
*
* MDF4 允许把记录拆到多段里:
* * `##DT` 普通数据块(记录连续存放);
* * `##DZ` 压缩数据块(zlib 流,解压后是普通记录);
* * `##DV` 数据值块(`##DL` 列表指向的原始值);
* * `##DL` 数据列表(链接到多个 `##DT` / `##DZ`);
* * `##HL` 头部列表(链接到多个数据块,含压缩的)。
*/
struct DataSegment
{
std::uint64_t offset = 0; /**< 数据区起始绝对偏移。 */
std::uint64_t bytes = 0; /**< 数据区字节数(压缩段为压缩后长度)。 */
bool compressed = false; /**< 是否 need inflate。 */
};
/**
* @brief 递归展开数据块,收集所有数据段。
*
* ⚠️ 用户 2026-10-19 报告:Simulink SDI 导出的 MF4 把记录写在 `##DL`(数据列表)
* 里,而旧实现只认 `##DT` ⇒ 整个数据组被跳过(8 个信号只能读出 4 个)。
*/
bool CollectDataSegments(const MdfFile& file, std::uint64_t block,
std::vector<DataSegment>& out, int depth)
{
if (block == 0 || depth > 4 || !file.IsBlock(block, ""))
{
return false;
}
char id[5] = {};
file.BlockId(block, id);
const std::uint64_t data = file.DataOffset(block);
const std::uint64_t payload = file.BlockLength(block) - (data - block);
if (payload == 0)
{
return false;
}
if (std::memcmp(id, "##DT", 4) == 0 || std::memcmp(id, "##DV", 4) == 0)
{
out.push_back(DataSegment{data, payload, false});
return true;
}
if (std::memcmp(id, "##DZ", 4) == 0)
{
out.push_back(DataSegment{data, payload, true});
return true;
}
if (std::memcmp(id, "##DL", 4) == 0 || std::memcmp(id, "##HL", 4) == 0)
{
const std::uint64_t links = file.LinkCount(block);
bool any = false;
for (std::uint64_t index = 0; index < links && index < 1000000; ++index)
{
any = CollectDataSegments(file, file.Link(block, index), out, depth + 1) || any;
}
return any;
}
// `##SD`(已排序数据)、`##SI` 等不在支持范围内:跳过该数据组。
return false;
}
/**
* @brief 依次从若干数据段中读取连续记录,并对每条完整记录调用 `visit`。
*
* * 记录可能**跨段**(`##DT` 载荷不是记录长度的整数倍时)⇒ 用 `carry` 拼接;
* * 压缩段先用内置 inflate 展开(`##DZ`),再按普通字节流消费。
*
* @return 成功读取到至少 `cycles` 条记录返回 true。
*/
bool ReadRecordSegments(
const MdfFile& file, const std::vector<DataSegment>& segments, std::uint64_t cycles,
std::size_t recordBytes,
const std::function<void(const unsigned char* record, std::uint64_t row)>& visit)
{
if (recordBytes == 0 || cycles == 0)
{
return false;
}
std::vector<unsigned char> carry(recordBytes);
std::size_t carryBytes = 0;
std::uint64_t row = 0;
std::vector<std::uint8_t> inflated;
for (const DataSegment& segment : segments)
{
if (row >= cycles)
{
break;
}
const unsigned char* data = nullptr;
std::size_t bytes = 0;
if (segment.compressed)
{
std::vector<std::uint8_t> compressed(static_cast<std::size_t>(segment.bytes));
if (!file.ReadAt(segment.offset, compressed.data(), compressed.size()))
{
return false;
}
if (!InflateZlib(compressed.data(), compressed.size(), inflated))
{
return false;
}
data = inflated.data();
bytes = inflated.size();
}
else
{
if (segment.offset + segment.bytes > file.Size())
{
return false;
}
// 非压缩段同样分块读,避免一次分配整段(巨型数据组可能是 GB 级)。
std::vector<unsigned char> buffer;
std::uint64_t consumed = 0;
while (consumed < segment.bytes && row < cycles)
{
const std::size_t want = static_cast<std::size_t>(
std::min<std::uint64_t>(kReadChunkBytes, segment.bytes - consumed));
buffer.resize(std::max<std::size_t>(want, recordBytes));
if (!file.ReadAt(segment.offset + consumed, buffer.data(), want))
{
return false;
}
data = buffer.data();
bytes = want;
std::size_t position = 0;
while (position < bytes && row < cycles)
{
const std::size_t need = recordBytes - carryBytes;
const std::size_t take = std::min(need, bytes - position);
std::memcpy(carry.data() + carryBytes, data + position, take);
carryBytes += take;
position += take;
if (carryBytes == recordBytes)
{
visit(carry.data(), row++);
carryBytes = 0;
}
}
consumed += want;
}
continue;
}
// 压缩段:整段已在内存里,直接消费。
std::size_t position = 0;
while (position < bytes && row < cycles)
{
const std::size_t need = recordBytes - carryBytes;
const std::size_t take = std::min(need, bytes - position);
std::memcpy(carry.data() + carryBytes, data + position, take);
carryBytes += take;
position += take;
if (carryBytes == recordBytes)
{
visit(carry.data(), row++);
carryBytes = 0;
}
}
}
return row > 0;
}
/** @brief 一个通道组(CG)解析出的时间轴 + 信号列。 */
struct ChannelGroupData
{
@@ -517,103 +684,77 @@ namespace jdi
* @param [IN] recordBytes 单条记录字节数。
* @param [IN,OUT] group 输出(`timeName`/`channels` 已填好,函数填充 `time`/`values`)。
*/
bool ReadChannelGroupRecords(const MdfFile& file, std::uint64_t dt, std::uint64_t cycles,
std::size_t recordBytes, std::size_t masterIndex,
ChannelGroupData& group)
bool ReadChannelGroupRecords(const MdfFile& file, const std::vector<DataSegment>& segments,
std::uint64_t cycles, std::size_t recordBytes,
std::size_t masterIndex, ChannelGroupData& group)
{
if (cycles == 0 || recordBytes == 0)
if (cycles == 0 || recordBytes == 0 || segments.empty())
{
return false;
}
const std::uint64_t dataStart = file.DataOffset(dt);
const std::uint64_t payload = file.BlockLength(dt) - (dataStart - dt);
if (payload < cycles * recordBytes)
{
return false; // 数据不足(损坏或带压缩)
}
group.time.assign(static_cast<std::size_t>(cycles), kMissingValue);
group.values.assign(group.channels.size(),
std::vector<double>(static_cast<std::size_t>(cycles), kMissingValue));
std::vector<unsigned char> buffer(kReadChunkBytes);
std::uint64_t done = 0;
while (done < cycles)
{
const std::uint64_t remainingRecords = cycles - done;
const std::uint64_t wantBytes = std::min<std::uint64_t>(
remainingRecords * recordBytes, static_cast<std::uint64_t>(buffer.size()));
const std::uint64_t wantRecords = wantBytes / recordBytes;
if (wantRecords == 0)
{
break;
}
const std::size_t bytes = static_cast<std::size_t>(wantRecords * recordBytes);
if (!file.ReadAt(dataStart + done * recordBytes, buffer.data(), bytes))
{
return false;
}
for (std::uint64_t r = 0; r < wantRecords; ++r)
{
const unsigned char* record = buffer.data() + r * recordBytes;
const std::size_t row = static_cast<std::size_t>(done + r);
// ⚠️ 记录可能分散在多个 `##DT` / `##DZ`(经 `##DL` / `##HL` 列表)中,
// 由 `ReadRecordSegments` 负责跨段拼接与解压(见该函数的说明)。
return ReadRecordSegments(
file, segments, cycles, recordBytes,
[&group, masterIndex, recordBytes](const unsigned char* record, std::uint64_t row)
{
// 时间主通道。
{
const ChannelSpec& master = group.channels[masterIndex];
double value = DecodeRawValue(record, recordBytes, master);
if (master.allInvalid)
{
value = kMissingValue;
}
else if (master.hasInvalidBit)
{
const std::size_t byteIndex = master.invalidBitPos / 8u;
if (byteIndex < recordBytes &&
((record[byteIndex] >> (master.invalidBitPos % 8u)) & 1u) != 0u)
{
value = kMissingValue;
}
}
if (std::isfinite(value))
{
value = master.linearConversion ? (master.offset + master.factor * value) : value;
}
group.time[static_cast<std::size_t>(row)] = value;
}
// 时间主通道。
{
const ChannelSpec& master = group.channels[masterIndex];
double value = DecodeRawValue(record, recordBytes, master);
if (master.allInvalid)
{
value = kMissingValue;
}
else if (master.hasInvalidBit)
{
const std::size_t byteIndex = master.invalidBitPos / 8u;
if (byteIndex < recordBytes &&
((record[byteIndex] >> (master.invalidBitPos % 8u)) & 1u) != 0u)
{
value = kMissingValue;
}
}
if (std::isfinite(value))
{
value = master.linearConversion ? (master.offset + master.factor * value) : value;
}
group.time[row] = value;
}
// 各信号通道。
for (std::size_t c = 0; c < group.channels.size(); ++c)
{
if (c == masterIndex)
{
continue;
}
const ChannelSpec& spec = group.channels[c];
double value = DecodeRawValue(record, recordBytes, spec);
if (spec.allInvalid)
{
value = kMissingValue;
}
else if (spec.hasInvalidBit)
{
const std::size_t byteIndex = spec.invalidBitPos / 8u;
if (byteIndex < recordBytes &&
((record[byteIndex] >> (spec.invalidBitPos % 8u)) & 1u) != 0u)
{
value = kMissingValue;
}
}
if (std::isfinite(value))
{
value = spec.linearConversion ? (spec.offset + spec.factor * value) : value;
}
group.values[c][row] = value;
}
}
done += wantRecords;
}
return true;
// 各信号通道。
for (std::size_t c = 0; c < group.channels.size(); ++c)
{
if (c == masterIndex)
{
continue;
}
const ChannelSpec& spec = group.channels[c];
double value = DecodeRawValue(record, recordBytes, spec);
if (spec.allInvalid)
{
value = kMissingValue;
}
else if (spec.hasInvalidBit)
{
const std::size_t byteIndex = spec.invalidBitPos / 8u;
if (byteIndex < recordBytes &&
((record[byteIndex] >> (spec.invalidBitPos % 8u)) & 1u) != 0u)
{
value = kMissingValue;
}
}
if (std::isfinite(value))
{
value = spec.linearConversion ? (spec.offset + spec.factor * value) : value;
}
group.values[c][static_cast<std::size_t>(row)] = value;
}
});
}
/**
@@ -692,19 +833,35 @@ namespace jdi
{
++groupCount;
const std::uint64_t cg = file.Link(dg, 1);
const std::uint64_t dt = file.Link(dg, 2);
if (!file.IsBlock(cg, "##CG") || !file.IsBlock(dt, "##DT"))
const std::uint64_t dataBlock = file.Link(dg, 2);
if (!file.IsBlock(cg, "##CG") || dataBlock == 0)
{
return true; // 跳过不完整的数据组
}
// 记录可能写在 `##DT` / `##DL`(数据列表)/ `##HL`(头部列表)/ `##DZ`(压缩)里:
// 旧实现只认 `##DT`,于是用 `##DL` 存放记录的文件会**整组被跳过**
//(用户 2026-10-19 报告:Simulink SDI 导出的 8 个信号只读出 4 个)。
std::vector<DataSegment> segments;
if (!CollectDataSegments(file, dataBlock, segments, 0) || segments.empty())
{
return true; // 不支持的数据布局(如 ##SD)⇒ 跳过该组
}
const std::uint64_t cgData = file.DataOffset(cg);
const std::uint64_t cycles = file.U64(cgData + 8);
if (cycles == 0)
{
return true;
}
const std::uint64_t payload = file.BlockLength(dt) - (file.DataOffset(dt) - dt);
const std::uint64_t recordIdBytes = file.U8(cgData); // `cg_record_id_len`
std::uint64_t payload = 0;
bool anyCompressed = false;
for (const DataSegment& segment : segments)
{
payload += segment.bytes;
anyCompressed = anyCompressed || segment.compressed;
}
// 解析通道链。
std::vector<ChannelSpec> channels;
@@ -724,15 +881,15 @@ namespace jdi
}
// 记录长度:优先用「数据块大小 / 记录数」反推(最可靠),
// 否则按通道覆盖的位宽向上取整到 4 字节。
// ⚠️ 压缩段(`##DZ`)的载荷是压缩后长度,不能用来反推 ⇒ 改为按通道位宽推算。
std::size_t recordBytes = 0;
if (payload % cycles == 0 && payload / cycles > 0)
if (!anyCompressed && payload % cycles == 0 && payload / cycles > 0)
{
recordBytes = static_cast<std::size_t>(payload / cycles);
}
else
{
std::size_t bits = 0;
std::size_t bits = static_cast<std::size_t>(recordIdBytes) * 8u;
for (const ChannelSpec& spec : channels)
{
bits = std::max(bits, spec.bitOffset + spec.bitCount);
@@ -757,7 +914,7 @@ namespace jdi
group.timeName = L"time";
}
group.channels = std::move(channels);
if (!ReadChannelGroupRecords(file, dt, cycles, recordBytes, masterIndex, group))
if (!ReadChannelGroupRecords(file, segments, cycles, recordBytes, masterIndex, group))
{
return true;
}