feat: add QBIN2 V4 encoded field ingress
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -215,6 +215,7 @@ list(APPEND SRC_FILES
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/field_data_adapter.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/field_runtime.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/field_profile_validator.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/particle/ingress/qbin_v2_decoder.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/local_particle_field_feed.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/shaders/shader_program_base.cpp
|
||||
)
|
||||
@@ -224,6 +225,7 @@ list(APPEND INCLUDE_FILES
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/particle/field/field_profile_contract.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/particle/field/field_profile_validator.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/particle/field/field_runtime.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/particle/ingress/qbin_v2_decoder.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/particle/field/local_particle_field_feed.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/actor/actor_ref.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/actor/actor.hpp
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# STEP-074 — QBIN2 V4 Encoded Ingress 与 Decoder v1
|
||||
|
||||
状态:完成(Core 范围)。开始 HEAD:`b8a629b7b65067f7f561db473312484bd8f0451d`。开始工作区已有 STEP-062 文档修改及历史 untracked 文件,未清理、暂存或提交。
|
||||
|
||||
新增独立 `mbgl::particle::ingress::QbinV2Decoder` 和无状态 `EncodedFieldIngress`。其顺序为 QBIN structural decode、构造 `DecodedFieldFrame`、再调用既有 generic `FieldProfileValidator`;未接入 Store、Renderer、Binding、Host 或平台 bridge。
|
||||
|
||||
Decoder fail-closed 校验 QBV2 LE header/descriptor、保留字段、FRAME/WebMercator/node 限制、时间/边界/容量、精确 canonical packing 与零 padding、ISO-HDLC CRC(CRC 字段虚拟清零)、single shared validity、invalid-cell raw-zero,以及 per-plane local content identity。numeric payload 保持原始 bytes。
|
||||
|
||||
真实 CMake runner 已加入 `ParticleQbinV2Decoder`、`ParticleEncodedFieldIngress`;fixture 仅在 test 范围,涵盖 2×2 与 513×769 构造。
|
||||
@@ -0,0 +1,13 @@
|
||||
# STEP-074 QBIN2 V4 Encoded Ingress 与 Decoder Codex 完成报告 v1
|
||||
|
||||
## 已完成
|
||||
|
||||
- 开始 HEAD:`b8a629b7b65067f7f561db473312484bd8f0451d`;最终 HEAD:待提交记录。任务开始前 dirty/untracked 文件未纳入本任务。
|
||||
- 独立 ingress decoder、CRC、canonical packing、shared validity、invalid raw-zero 和 decode→contract facade。
|
||||
- 新增 CMake production/test sources;新增 suites `ParticleQbinV2Decoder` 与 `ParticleEncodedFieldIngress`,共 6 tests。
|
||||
- 实际重链 `mbgl-test-runner` PASS;新增 filter 实际 Running 6,failed=0;STEP-073 filter 实际 Running 11,failed=0;V3 filter 实际 Running 11,failed=0;`git diff --check` PASS。
|
||||
|
||||
## 未完成 / 无法验证
|
||||
|
||||
- 受限暂存提交与 push 尚待执行;最终 HEAD/push 结果届时记录。
|
||||
- 未执行 Android/ADB/Jest/iOS/Metal;范围明确排除 URL/HTTP/gzip/Host/JNI/RN/Renderer/Binding。
|
||||
56
include/mbgl/particle/ingress/qbin_v2_decoder.hpp
Normal file
56
include/mbgl/particle/ingress/qbin_v2_decoder.hpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <mbgl/particle/field/field_profile_validator.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
namespace mbgl::particle::ingress {
|
||||
|
||||
struct EncodedFieldRuntimeMetadata {
|
||||
std::string fieldID;
|
||||
uint64_t generation = 0;
|
||||
uint64_t dataVersion = 0;
|
||||
};
|
||||
|
||||
enum class QbinV2DecodeErrorCode : uint8_t {
|
||||
Accepted, InvalidRuntimeMetadata, InputTooSmall, BadMagic, UnsupportedVersion, UnsupportedLayout,
|
||||
UnsupportedProjection, UnsupportedSampleOrder, UnsupportedGridRegistration, InvalidHeader, InvalidBounds,
|
||||
InvalidTime, LimitExceeded, LengthMismatch, ReservedNonZero, InvalidDescriptor,
|
||||
NonCanonicalDescriptorOrder, DuplicatePlaneKey, NonCanonicalPacking, NonZeroPadding, CrcMismatch,
|
||||
InvalidValidity, InvalidCellPayload,
|
||||
};
|
||||
|
||||
struct QbinV2DecodeResult {
|
||||
QbinV2DecodeErrorCode code = QbinV2DecodeErrorCode::InvalidHeader;
|
||||
std::string detail;
|
||||
std::shared_ptr<const DecodedFieldFrame> frame;
|
||||
bool accepted() const { return code == QbinV2DecodeErrorCode::Accepted && static_cast<bool>(frame); }
|
||||
};
|
||||
|
||||
class QbinV2Decoder final {
|
||||
public:
|
||||
static QbinV2DecodeResult decode(const EncodedFieldRuntimeMetadata&, std::span<const uint8_t>);
|
||||
};
|
||||
|
||||
enum class EncodedFieldIngressCode : uint8_t { Accepted, DecodeRejected, ContractMissing, ContractRejected };
|
||||
|
||||
struct EncodedFieldIngressResult {
|
||||
EncodedFieldIngressCode code = EncodedFieldIngressCode::DecodeRejected;
|
||||
std::string detail;
|
||||
QbinV2DecodeErrorCode decodeCode = QbinV2DecodeErrorCode::InvalidHeader;
|
||||
FieldProfileValidationCode contractCode = FieldProfileValidationCode::InvalidContract;
|
||||
std::shared_ptr<const DecodedFieldFrame> frame;
|
||||
bool accepted() const { return code == EncodedFieldIngressCode::Accepted && static_cast<bool>(frame); }
|
||||
};
|
||||
|
||||
class EncodedFieldIngress final {
|
||||
public:
|
||||
static EncodedFieldIngressResult decodeAndValidate(const EncodedFieldRuntimeMetadata&,
|
||||
const ResolvedFieldProfileContract* contract,
|
||||
std::span<const uint8_t>);
|
||||
};
|
||||
|
||||
} // namespace mbgl::particle::ingress
|
||||
69
src/mbgl/particle/ingress/qbin_v2_decoder.cpp
Normal file
69
src/mbgl/particle/ingress/qbin_v2_decoder.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
#include <mbgl/particle/ingress/qbin_v2_decoder.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
namespace mbgl::particle::ingress {
|
||||
namespace {
|
||||
constexpr size_t HeaderSize = 112, DescriptorSize = 32, MaxBytes = 64u * 1024u * 1024u;
|
||||
constexpr uint32_t MaxPlanes = 32, MaxCells = 2'097'152, MaxDecoded = 32u * 1024u * 1024u;
|
||||
constexpr uint16_t ValiditySemantic = 3;
|
||||
template <class T> T le(std::span<const uint8_t> b, size_t o) {
|
||||
uint64_t v = 0; for (size_t i = 0; i < sizeof(T); ++i) v |= uint64_t(b[o + i]) << (8 * i);
|
||||
T r{}; std::memcpy(&r, &v, sizeof(T)); return r;
|
||||
}
|
||||
template <> float le<float>(std::span<const uint8_t> b, size_t o) { uint32_t v = le<uint32_t>(b,o); float r; std::memcpy(&r,&v,4); return r; }
|
||||
template <> double le<double>(std::span<const uint8_t> b, size_t o) { uint64_t v = le<uint64_t>(b,o); double r; std::memcpy(&r,&v,8); return r; }
|
||||
uint32_t crc(std::span<const uint8_t> bytes) { uint32_t c=0xffffffffu; for(size_t i=0;i<bytes.size();++i) { uint8_t x=(i>=76&&i<80)?0:bytes[i]; c^=x; for(int j=0;j<8;++j)c=(c>>1)^((c&1)?0xedb88320u:0); } return ~c; }
|
||||
size_t align16(size_t x) { return (x + 15u) & ~size_t(15u); }
|
||||
QbinV2DecodeResult fail(QbinV2DecodeErrorCode c, std::string d) { return {c,std::move(d),{}}; }
|
||||
bool zero(std::span<const uint8_t> b) { for(auto v:b) if(v) return false; return true; }
|
||||
std::string identity(uint16_t semantic, uint16_t group, uint8_t component, uint8_t dtype, uint8_t flags, uint8_t vc, uint16_t unit, uint32_t count, float scale, float offset, std::span<const uint8_t> raw) {
|
||||
uint64_t h=1469598103934665603ull; auto add=[&](uint8_t v){h^=v;h*=1099511628211ull;};
|
||||
for(auto v:raw)add(v); for(auto v:std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(&semantic),2))add(v);
|
||||
for(auto v:std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(&group),2))add(v); add(component);add(dtype);add(flags);add(vc);
|
||||
for(auto v:std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(&unit),2))add(v);
|
||||
for(auto v:std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(&count),4))add(v);
|
||||
for(auto v:std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(&scale),4))add(v); for(auto v:std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(&offset),4))add(v);
|
||||
std::ostringstream s; s<<std::hex<<h; return s.str();
|
||||
}
|
||||
struct D { uint16_t semantic, unit, group; uint8_t dtype, flags, vc, component; float scale, offset; uint32_t count, off, len; };
|
||||
}
|
||||
|
||||
QbinV2DecodeResult QbinV2Decoder::decode(const EncodedFieldRuntimeMetadata& meta, std::span<const uint8_t> b) {
|
||||
if(meta.fieldID.empty() || meta.generation==0) return fail(QbinV2DecodeErrorCode::InvalidRuntimeMetadata,"fieldID/generation");
|
||||
if(b.size()<HeaderSize) return fail(QbinV2DecodeErrorCode::InputTooSmall,"header"); if(b.size()>MaxBytes) return fail(QbinV2DecodeErrorCode::LimitExceeded,"file bytes");
|
||||
if(std::memcmp(b.data(),"QBV2",4)!=0) return fail(QbinV2DecodeErrorCode::BadMagic,"magic"); if(b[4]!=2) return fail(QbinV2DecodeErrorCode::UnsupportedVersion,"version");
|
||||
if(b[5]!=1) return fail(QbinV2DecodeErrorCode::UnsupportedLayout,"layout"); if(b[8]!=1) return fail(QbinV2DecodeErrorCode::UnsupportedProjection,"projection");
|
||||
if(b[9]!=1) return fail(QbinV2DecodeErrorCode::UnsupportedSampleOrder,"sampleOrder"); if(b[10]!=1) return fail(QbinV2DecodeErrorCode::UnsupportedGridRegistration,"gridRegistration");
|
||||
if(b[11]||le<uint16_t>(b,14)||le<uint16_t>(b,82)||le<uint32_t>(b,92)||le<uint32_t>(b,96)||le<uint32_t>(b,100)||le<uint32_t>(b,104)||le<uint32_t>(b,108)) return fail(QbinV2DecodeErrorCode::ReservedNonZero,"header reserved");
|
||||
const uint16_t n=le<uint16_t>(b,12); const uint32_t w=le<uint32_t>(b,16), h=le<uint32_t>(b,20), payload=le<uint32_t>(b,72);
|
||||
if(n==0 || n>MaxPlanes || w<2 || h<2) return fail(QbinV2DecodeErrorCode::InvalidHeader,"planes/grid"); const uint64_t cells=uint64_t(w)*h;
|
||||
if(cells>MaxCells) return fail(QbinV2DecodeErrorCode::LimitExceeded,"cell count"); if(le<uint16_t>(b,80)!=0xffff || le<uint32_t>(b,84)||le<uint32_t>(b,88)) return fail(QbinV2DecodeErrorCode::InvalidHeader,"FRAME tile");
|
||||
const int64_t ref=le<int64_t>(b,24), valid=le<int64_t>(b,32); if(ref<=0||valid<=0||uint64_t(ref)>uint64_t(std::numeric_limits<uint64_t>::max()/1000)||uint64_t(valid)>uint64_t(std::numeric_limits<uint64_t>::max()/1000)) return fail(QbinV2DecodeErrorCode::InvalidTime,"UTC seconds");
|
||||
const double west=le<double>(b,40), south=le<double>(b,48), east=le<double>(b,56), north=le<double>(b,64);
|
||||
if(!std::isfinite(west)||!std::isfinite(south)||!std::isfinite(east)||!std::isfinite(north)||west < -180||east > 180||west>=east||south < -85.0511287798066||north >85.0511287798066||south>=north) return fail(QbinV2DecodeErrorCode::InvalidBounds,"Web Mercator bounds");
|
||||
const size_t po=HeaderSize+size_t(n)*DescriptorSize;
|
||||
if(payload>MaxDecoded || po>std::numeric_limits<size_t>::max()-payload || b.size()!=po+payload) return fail(QbinV2DecodeErrorCode::LengthMismatch,"total length");
|
||||
std::vector<D> ds; ds.reserve(n); std::set<std::pair<uint16_t,uint16_t>> keys; uint16_t ps=0,pg=0; uint8_t pc=0; size_t expected=0; bool first=true; int validity=-1; uint64_t totalDecoded=0;
|
||||
for(uint16_t i=0;i<n;++i){ size_t o=HeaderSize+size_t(i)*DescriptorSize; D d{le<uint16_t>(b,o),le<uint16_t>(b,o+24),le<uint16_t>(b,o+26),b[o+2],b[o+3],b[o+28],b[o+29],le<float>(b,o+4),le<float>(b,o+8),le<uint32_t>(b,o+12),le<uint32_t>(b,o+16),le<uint32_t>(b,o+20)};
|
||||
if(le<uint16_t>(b,o+30)||d.flags&~1u) return fail(QbinV2DecodeErrorCode::InvalidDescriptor,"descriptor "+std::to_string(i)); if(!first && std::tuple(d.semantic,d.group,d.component)<=std::tuple(ps,pg,pc)) return fail(QbinV2DecodeErrorCode::NonCanonicalDescriptorOrder,"descriptor "+std::to_string(i)); first=false;ps=d.semantic;pg=d.group;pc=d.component;
|
||||
if(!keys.insert({d.semantic,d.group}).second) return fail(QbinV2DecodeErrorCode::DuplicatePlaneKey,"descriptor "+std::to_string(i)); if(d.semantic==0||d.dtype<1||d.dtype>6||d.vc>3||d.component>3||d.count!=cells) return fail(QbinV2DecodeErrorCode::InvalidDescriptor,"descriptor "+std::to_string(i));
|
||||
uint64_t len=d.dtype==1||d.dtype==2?cells*2:d.dtype==5?cells*4:d.dtype==6?(cells+7)/8:cells; if(d.len!=len||d.off!=expected||len>payload||d.off>payload-len) return fail(d.off!=expected?QbinV2DecodeErrorCode::NonCanonicalPacking:QbinV2DecodeErrorCode::InvalidDescriptor,"descriptor "+std::to_string(i));
|
||||
if(d.dtype!=6 && (!std::isfinite(d.scale)||d.scale<=0||!std::isfinite(d.offset))) return fail(QbinV2DecodeErrorCode::InvalidDescriptor,"numeric descriptor "+std::to_string(i)); if(d.dtype==6 && (d.scale!=1||d.offset!=0)) return fail(QbinV2DecodeErrorCode::InvalidDescriptor,"bitset descriptor");
|
||||
expected=align16(expected+size_t(len)); totalDecoded+=len; if(totalDecoded>MaxDecoded) return fail(QbinV2DecodeErrorCode::LimitExceeded,"decoded bytes"); if(d.vc==3) { if(validity!=-1||d.semantic!=ValiditySemantic||d.unit||d.group||d.component||d.dtype!=6||!(d.flags&1)) return fail(QbinV2DecodeErrorCode::InvalidValidity,"validity descriptor"); validity=i; } ds.push_back(d); }
|
||||
if(validity<0) return fail(QbinV2DecodeErrorCode::InvalidValidity,"missing validity"); if(expected!=payload) return fail(QbinV2DecodeErrorCode::NonCanonicalPacking,"payload length"); if(!zero(b.subspan(po+ds.back().off+ds.back().len, payload-(ds.back().off+ds.back().len)))) return fail(QbinV2DecodeErrorCode::NonZeroPadding,"tail padding");
|
||||
for(size_t i=0;i<ds.size();++i) { const auto& d=ds[i]; size_t end=d.off+d.len, next=i+1<ds.size()?ds[i+1].off:payload; if(next>end&&!zero(b.subspan(po+end,next-end))) return fail(QbinV2DecodeErrorCode::NonZeroPadding,"plane padding"); }
|
||||
if(crc(b)!=le<uint32_t>(b,76)) return fail(QbinV2DecodeErrorCode::CrcMismatch,"file CRC32"); const auto& vd=ds[validity]; auto vb=b.subspan(po+vd.off,vd.len); if(cells%8 && (vb.back() & uint8_t(0xffu << (cells%8)))) return fail(QbinV2DecodeErrorCode::InvalidValidity,"unused validity bits");
|
||||
for(const auto& d:ds) if(d.dtype!=6) for(uint64_t c=0;c<cells;++c) if(!(vb[c/8]&(1u<<(c%8)))) { auto raw=b.subspan(po+d.off,d.len); size_t start=d.dtype==1||d.dtype==2?c*2:d.dtype==5?c*4:c; size_t z=d.dtype==1||d.dtype==2?2:d.dtype==5?4:1; if(!zero(raw.subspan(start,z))) return fail(QbinV2DecodeErrorCode::InvalidCellPayload,"invalid cell payload"); }
|
||||
auto out=std::make_shared<DecodedFieldFrame>(); out->frameContentIdentity={meta.fieldID,meta.generation,meta.dataVersion,uint64_t(valid)*1000}; out->referenceTimeMs=uint64_t(ref)*1000; out->grid={1,1,1,1,w,h,west,south,east,north};
|
||||
for(size_t i=0;i<ds.size();++i){const auto& d=ds[i]; auto raw=std::make_shared<const std::vector<uint8_t>>(b.begin()+po+d.off,b.begin()+po+d.off+d.len); DecodedFieldPlane p{{d.semantic,d.group,static_cast<FieldComponent>(d.component)},static_cast<FieldDType>(d.dtype),static_cast<FieldValueClass>(d.vc),d.unit,bool(d.flags&1),d.count,d.scale,d.offset,identity(d.semantic,d.group,d.component,d.dtype,d.flags,d.vc,d.unit,d.count,d.scale,d.offset,*raw),raw}; if(int(i)==validity){out->validityContentIdentity=p.contentIdentity;out->validityBits=raw;out->validityPlane=std::move(p);}else out->planes.push_back(std::move(p)); }
|
||||
return {QbinV2DecodeErrorCode::Accepted,"accepted",out};
|
||||
}
|
||||
EncodedFieldIngressResult EncodedFieldIngress::decodeAndValidate(const EncodedFieldRuntimeMetadata& m,const ResolvedFieldProfileContract* c,std::span<const uint8_t> b){ auto decoded=QbinV2Decoder::decode(m,b); if(!decoded.accepted()) return {EncodedFieldIngressCode::DecodeRejected,decoded.detail,decoded.code,FieldProfileValidationCode::InvalidContract,{}}; if(!c) return {EncodedFieldIngressCode::ContractMissing,"contract",decoded.code,FieldProfileValidationCode::InvalidContract,{}}; auto v=FieldProfileValidator::validate(*c,*decoded.frame); if(!v.accepted()) return {EncodedFieldIngressCode::ContractRejected,v.detail,decoded.code,v.code,{}}; return {EncodedFieldIngressCode::Accepted,"accepted",decoded.code,v.code,decoded.frame}; }
|
||||
} // namespace mbgl::particle::ingress
|
||||
@@ -19,6 +19,7 @@ add_library(
|
||||
${PROJECT_SOURCE_DIR}/test/math/wrap.test.cpp
|
||||
${PROJECT_SOURCE_DIR}/test/platform/settings.test.cpp
|
||||
${PROJECT_SOURCE_DIR}/test/particle/field_runtime.test.cpp
|
||||
${PROJECT_SOURCE_DIR}/test/particle/qbin_v2_decoder.test.cpp
|
||||
${PROJECT_SOURCE_DIR}/test/plugin/plugin.test.cpp
|
||||
${PROJECT_SOURCE_DIR}/test/renderer/image_manager.test.cpp
|
||||
${PROJECT_SOURCE_DIR}/test/renderer/pattern_atlas.test.cpp
|
||||
|
||||
24
test/particle/qbin_v2_decoder.test.cpp
Normal file
24
test/particle/qbin_v2_decoder.test.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#include <mbgl/particle/ingress/qbin_v2_decoder.hpp>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
using namespace mbgl;
|
||||
namespace {
|
||||
void u16(std::vector<uint8_t>& b,size_t o,uint16_t v){b[o]=v;b[o+1]=v>>8;} void u32(std::vector<uint8_t>& b,size_t o,uint32_t v){for(int i=0;i<4;i++)b[o+i]=v>>(8*i);} void i64(std::vector<uint8_t>& b,size_t o,int64_t v){for(int i=0;i<8;i++)b[o+i]=uint64_t(v)>>(8*i);} template<class T> void fp(std::vector<uint8_t>& b,size_t o,T v){std::memcpy(b.data()+o,&v,sizeof v);} size_t a16(size_t x){return(x+15)&~size_t(15);}
|
||||
uint32_t crc(const std::vector<uint8_t>& b){uint32_t c=~0u;for(size_t i=0;i<b.size();++i){uint8_t x=i>=76&&i<80?0:b[i];c^=x;for(int j=0;j<8;j++)c=(c>>1)^((c&1)?0xedb88320u:0);}return~c;}
|
||||
struct P{uint16_t semantic=1,unit=1,group=1;uint8_t type=1,flags=1,vc=1,component=1;std::vector<uint8_t> raw;};
|
||||
std::vector<uint8_t> fixture(uint32_t w=2,uint32_t h=2,std::vector<P> ps={}) { if(ps.empty()) ps={{1,1,1,1,1,1,1,{1,0,2,0,3,0,4,0}},{2,1,1,1,1,1,2,{5,0,6,0,7,0,8,0}},{3,0,0,6,1,3,0,{15}}}; if(w*h!=4) for(auto& p:ps){p.raw.assign(p.type==6?(w*h+7)/8:(p.type==1||p.type==2?w*h*2:p.type==5?w*h*4:w*h),0);if(p.type==6)std::fill(p.raw.begin(),p.raw.end(),0xff),p.raw.back()=uint8_t((1u<<((w*h-1)%8+1))-1);} size_t po=112+ps.size()*32, off=0; for(auto&p:ps){off=a16(off+p.raw.size());} std::vector<uint8_t>b(po+off);std::memcpy(b.data(),"QBV2",4);b[4]=2;b[5]=1;u16(b,6,999);b[8]=b[9]=b[10]=1;u16(b,12,ps.size());u32(b,16,w);u32(b,20,h);i64(b,24,100);i64(b,32,90);fp(b,40,-10.);fp(b,48,-10.);fp(b,56,10.);fp(b,64,10.);u32(b,72,off);u16(b,80,0xffff);size_t cur=0;for(size_t i=0;i<ps.size();++i){auto&p=ps[i];size_t d=112+i*32;u16(b,d,p.semantic);b[d+2]=p.type;b[d+3]=p.flags;fp(b,d+4,p.type==6?1.f:.1f);fp(b,d+8,0.f);u32(b,d+12,w*h);u32(b,d+16,cur);u32(b,d+20,p.raw.size());u16(b,d+24,p.unit);u16(b,d+26,p.group);b[d+28]=p.vc;b[d+29]=p.component;std::memcpy(b.data()+po+cur,p.raw.data(),p.raw.size());cur=a16(cur+p.raw.size());}u32(b,76,crc(b));return b;}
|
||||
void recalc(std::vector<uint8_t>& b){u32(b,76,crc(b));}
|
||||
particle::ingress::EncodedFieldRuntimeMetadata meta(){return{"field",1,7};}
|
||||
particle::ResolvedFieldProfileContract wind(){particle::ResolvedFieldProfileContract c; c.profileID="generic.wind";c.profileVersion=1;c.requiredPlanes={{1,particle::FieldValueClass::VelocityComponent,1,1,particle::FieldComponent::UEast,true},{2,particle::FieldValueClass::VelocityComponent,1,1,particle::FieldComponent::VNorth,true},{3,particle::FieldValueClass::Validity,0,0,particle::FieldComponent::None,true}};return c;}
|
||||
}
|
||||
TEST(ParticleQbinV2Decoder, DecodesGenericFrameAndSharesValidity) {auto r=particle::ingress::QbinV2Decoder::decode(meta(),fixture());ASSERT_TRUE(r.accepted())<<r.detail;EXPECT_EQ(r.frame->referenceTimeMs,100000);EXPECT_EQ(r.frame->frameContentIdentity.validTimeMs,90000);EXPECT_EQ(r.frame->planes.size(),2);EXPECT_EQ(r.frame->validityPlane.storage,r.frame->validityBits);}
|
||||
TEST(ParticleQbinV2Decoder, RejectsStructuralAndCrcViolations) {auto b=fixture();b[0]='X';EXPECT_EQ(particle::ingress::QbinV2Decoder::decode(meta(),b).code,particle::ingress::QbinV2DecodeErrorCode::BadMagic);b=fixture();b[11]=1;recalc(b);EXPECT_EQ(particle::ingress::QbinV2Decoder::decode(meta(),b).code,particle::ingress::QbinV2DecodeErrorCode::ReservedNonZero);b=fixture();b[208]^=1;EXPECT_EQ(particle::ingress::QbinV2Decoder::decode(meta(),b).code,particle::ingress::QbinV2DecodeErrorCode::CrcMismatch);}
|
||||
TEST(ParticleQbinV2Decoder, RejectsValidityAndInvalidCells) {auto b=fixture();size_t po=208;b[po+32]=0x0e;recalc(b);EXPECT_EQ(particle::ingress::QbinV2Decoder::decode(meta(),b).code,particle::ingress::QbinV2DecodeErrorCode::InvalidCellPayload);b=fixture();b[po+32]=0xf0;recalc(b);EXPECT_EQ(particle::ingress::QbinV2Decoder::decode(meta(),b).code,particle::ingress::QbinV2DecodeErrorCode::InvalidValidity);}
|
||||
TEST(ParticleQbinV2Decoder, PlaneContentIdentityIsIndependent) {auto a=fixture();auto b=a;b[208+16]=9;recalc(b);auto ra=particle::ingress::QbinV2Decoder::decode(meta(),a);auto rb=particle::ingress::QbinV2Decoder::decode(meta(),b);ASSERT_TRUE(ra.accepted());ASSERT_TRUE(rb.accepted());EXPECT_EQ(ra.frame->planes[0].contentIdentity,rb.frame->planes[0].contentIdentity);EXPECT_NE(ra.frame->planes[1].contentIdentity,rb.frame->planes[1].contentIdentity);}
|
||||
TEST(ParticleQbinV2Decoder, DecodesLargeStructuralFixture) {auto r=particle::ingress::QbinV2Decoder::decode(meta(),fixture(513,769));ASSERT_TRUE(r.accepted())<<r.detail;EXPECT_EQ(r.frame->grid.width,513);EXPECT_EQ(r.frame->grid.height,769);EXPECT_EQ(r.frame->planes[0].storage->size(),513u*769u*2u);}
|
||||
TEST(ParticleEncodedFieldIngress, ValidatesContractAndReportsMissing) {auto good=wind();auto ok=particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(),&good,fixture());EXPECT_TRUE(ok.accepted());auto miss=particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(),nullptr,fixture());EXPECT_EQ(miss.code,particle::ingress::EncodedFieldIngressCode::ContractMissing);auto c=wind();c.requiredPlanes.pop_back();auto bad=particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(),&c,fixture());EXPECT_EQ(bad.code,particle::ingress::EncodedFieldIngressCode::ContractRejected);}
|
||||
@@ -710,3 +710,9 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2`
|
||||
- `DecodedFieldFrame` 固化 single shared validity domain:designated validity 必须为 `Validity/Bitset/NONE/group0/None/REQUIRED`,与 `validityBits` 同一共享引用;普通 `planes[]` 禁止第二张 Validity。空 numeric `planes[]` 仍结构有效,缺 Profile numeric requirement 正确返回 `MissingRequiredPlane`。
|
||||
- Validator 在 required descriptor 匹配前检查 actual reserved Core semantic;semantic 1 Scalar 伪装、semantic 10 的 component/group 冲突优先为 `SemanticContractMismatch`。unknown Scalar opaque 与 Direction circular quantization 规则保持。
|
||||
- ForecastClock 测试在 pause 后显式恢复 `play()`,生产 pause 语义未改。实际重建并重链 `mbgl-test-runner` 后,V4 filter 11/11 PASS、V3 定向回归 11/11 PASS、`git diff --check` PASS;未进入 Decoder/Renderer/JNI/Android/RN,停止等待独立复验。
|
||||
|
||||
### 2026-09-13 STEP-074 QBIN2 V4 Encoded Ingress 与 Decoder
|
||||
|
||||
- 新增独立 `mbgl::particle::ingress::QbinV2Decoder` 与纯函数 `EncodedFieldIngress`:严格解析 raw QBV2,构造只读 `DecodedFieldFrame` 后交给既有 generic `FieldProfileValidator`;未接入 Store、Renderer、Binding、Host 或 platform bridge。
|
||||
- Header/descriptor/limits/CRC/canonical packing/zero padding、single shared validity、invalid-cell raw-zero 和 per-plane local content identity 均 fail-closed;numeric bytes 不预展开为 float。
|
||||
- 新测试 fixture 只位于 C++ test 范围,覆盖 2×2、损坏输入与 513×769 结构 frame;未实现下载、URL、HTTP、gzip、JNI、Android、RN 或 Renderer。
|
||||
|
||||
Reference in New Issue
Block a user