diff --git a/CMakeLists.txt b/CMakeLists.txt index a3aed88a2..b60e5f0bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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_binding.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/field_projection_materialization.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/field_profile_validator.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/field_profile_contract_json.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/particle/ingress/qbin_v2_decoder.cpp diff --git a/include/mbgl/particle/field/field_projection_materialization.hpp b/include/mbgl/particle/field/field_projection_materialization.hpp new file mode 100644 index 000000000..53fe3a538 --- /dev/null +++ b/include/mbgl/particle/field/field_projection_materialization.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#include + +namespace mbgl::particle { + +// Renderer-neutral canonical snapshots. QBIN stays confined to ingress and +// field Core; renderers receive only the existing canonical field shape. +struct MaterializedColorProjection { + ParticleFieldSnapshot t0; + ParticleFieldSnapshot t1; + ColorProjectionContentIdentity contentIdentity; + FieldSampleState sampleState; +}; + +struct MaterializedParticleProjection { + ParticleFieldSnapshot t0; + ParticleFieldSnapshot t1; + ParticleProjectionContentIdentity contentIdentity; + FieldSampleState sampleState; +}; + +std::optional materializeColorProjection(const ColorProjection&); +std::optional materializeParticleProjection(const ParticleProjection&); + +} // namespace mbgl::particle diff --git a/include/mbgl/style/sources/raster_array_source.hpp b/include/mbgl/style/sources/raster_array_source.hpp index cea0b8d00..9b2b38ca5 100644 --- a/include/mbgl/style/sources/raster_array_source.hpp +++ b/include/mbgl/style/sources/raster_array_source.hpp @@ -43,7 +43,11 @@ enum class RasterArrayPlaybackState : uint8_t { Ended }; -enum class RasterArrayTargetFps : uint8_t { Auto = 0, Fps30 = 30, Fps60 = 60 }; +enum class RasterArrayTargetFps : uint8_t { + Auto = 0, + Fps30 = 30, + Fps60 = 60 +}; struct RasterArrayTimelineFrame { std::shared_ptr t0; @@ -255,6 +259,9 @@ private: void tryLoadRuntimeField(size_t); void tryLoadLocalField(size_t); void tryInitializeTimeline(); + // Re-resolve and materialize the independent V4 render view. This never + // writes legacy timeline/field state. + void refreshV4RendererState(); RasterArrayRuntimeFieldUpdateResult rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult, const std::string& reason) const; diff --git a/integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js b/integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js index 16c2afe57..a52e0f2a2 100644 --- a/integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js +++ b/integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js @@ -6,7 +6,7 @@ const marker = "// MapLibreParticleRNTestHost NavSea Particle release SDK"; module.exports = function withParticleNativeAndroid(config) { return withProjectBuildGradle(config, (project) => { if (project.modResults.contents.includes(marker)) return project; - project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`; + project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.5")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`; return project; }); }; diff --git a/packages/maplibre-particle-rn/CHANGELOG.md b/packages/maplibre-particle-rn/CHANGELOG.md index 4cdaacc93..85e61d638 100644 --- a/packages/maplibre-particle-rn/CHANGELOG.md +++ b/packages/maplibre-particle-rn/CHANGELOG.md @@ -1,6 +1,21 @@ # 变更记录 -## 13.4.1-particle.3(当前 Android QBIN V4 基线) +## 13.4.1-particle.5(当前 Android QBIN V4 Renderer 基线) + +- 新增 QBIN V4 `BindingProjection` → renderer-facing canonical snapshot materialization,并由 `RasterParticle` / `WeatherColor` Renderer 正式消费。 +- 保持 V3/V4 隔离:V4 不回写旧 `field` / `nextField` / `timeline`,仅通过独立 `v4RendererState` 进入渲染路径。 +- V4 midpoint / playback 仅改变 MIX 时保持 projection content identity,不要求重复上传相同 T0/T1 GPU field。 +- materializer 对 Int16 / UInt16 / Float32 使用显式 little-endian 解码,并支持冻结 QBIN V2 `DIRECTION_TO` UInt16 方向角转向量。 +- 修复 `13.4.1-particle.4` 在 Android Test Host + frozen Golden QBIN 条件下控制面 ACCEPTED、但 Renderer 未获得有效 V4 field window 而无可见粒子的链路缺口。 +- RN package 与 Android Native artifact 统一升级到 `13.4.1-particle.5`。 + +## 13.4.1-particle.4(当前 Android QBIN V4 基线) + +- RN 公共入口正式导出 QBIN V4 Consumer 类型:Encoded Runtime Metadata、Resolved Field Profile Contract、Color/Particle Binding 与 Encoded Submit Result。 +- UTF-8 长度校验改为包内纯 TypeScript 实现,不再要求 Consumer TypeScript 环境提供 DOM `TextEncoder` 类型。 +- RN 与 Android Native 发布坐标同步升级到 `13.4.1-particle.4`;QBIN V4 API、Native ABI 与渲染语义不变。 + +## 13.4.1-particle.3 - RN package 与 Android Native artifact 已统一为 `13.4.1-particle.3`。 - 新增 QBIN V4 encoded ingress:`submitEncodedField`、Color/Particle Binding、Forecast Time/Playback、encoded clear。 diff --git a/packages/maplibre-particle-rn/README.md b/packages/maplibre-particle-rn/README.md index 5b146e5f2..b5e13348e 100644 --- a/packages/maplibre-particle-rn/README.md +++ b/packages/maplibre-particle-rn/README.md @@ -1,6 +1,6 @@ # @navsea/maplibre-particle-rn -当前版本:`13.4.1-particle.3`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3`。本包是 React Native 的薄控制面,不负责 Map 组件、provider、HTTP、manifest、下载、缓存或 NavSea 业务策略。 +当前版本:`13.4.1-particle.5`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.5`。本包是 React Native 的薄控制面,不负责 Map 组件、provider、HTTP、manifest、下载、缓存或 NavSea 业务策略。 ## 当前推荐主链:QBIN V4 @@ -31,7 +31,7 @@ Map 与 Camera 直接从 `@maplibre/maplibre-react-native@11.3.6` 使用。本 RN tgz: ```text -https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.3.tgz +https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.5.tgz ``` Native Maven repository: @@ -43,7 +43,7 @@ https://pbf.jpcn.ccwu.cc/maven Native coordinate: ```text -cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3 +cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.5 ``` -Expo plugin 与 `android/build.gradle` 必须指向同一 `.3`。正常 Consumer 不复制 AAR、不依赖 MapLibreParticleLab 源码路径,也不以 Maven Local 作为公网验证主路径。完整 Linux 发布与 Win11 验证流程见 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。 +Expo plugin 与 `android/build.gradle` 必须指向同一 `.4`。正常 Consumer 不复制 AAR、不依赖 MapLibreParticleLab 源码路径,也不以 Maven Local 作为公网验证主路径。完整 Linux 发布与 Win11 验证流程见 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。 diff --git a/packages/maplibre-particle-rn/android/build.gradle b/packages/maplibre-particle-rn/android/build.gradle index 4cf94ecf3..728ff059c 100644 --- a/packages/maplibre-particle-rn/android/build.gradle +++ b/packages/maplibre-particle-rn/android/build.gradle @@ -14,7 +14,7 @@ android { dependencies { implementation "com.facebook.react:react-android" implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.20" - implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3" + implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.5" compileOnly project(":maplibre_maplibre-react-native") } diff --git a/packages/maplibre-particle-rn/app.plugin.js b/packages/maplibre-particle-rn/app.plugin.js index bafe3f3ad..65b823e25 100644 --- a/packages/maplibre-particle-rn/app.plugin.js +++ b/packages/maplibre-particle-rn/app.plugin.js @@ -6,7 +6,7 @@ const marker = "// @navsea/maplibre-particle-rn Android OpenGL release"; module.exports = function withNavSeaMapLibreParticle(config) { return withProjectBuildGradle(config, (project) => { if (project.modResults.contents.includes(marker)) return project; - project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`; + project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.5")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`; return project; }); }; diff --git a/packages/maplibre-particle-rn/package.json b/packages/maplibre-particle-rn/package.json index f0e4ad9ae..fce2d1a25 100644 --- a/packages/maplibre-particle-rn/package.json +++ b/packages/maplibre-particle-rn/package.json @@ -1,6 +1,6 @@ { "name": "@navsea/maplibre-particle-rn", - "version": "13.4.1-particle.3", + "version": "13.4.1-particle.5", "description": "NavSea MapLibre Particle React Native pure Field Renderer API.", "license": "BSD-2-Clause", "main": "src/index.ts", diff --git a/packages/maplibre-particle-rn/src/EncodedFieldGuards.ts b/packages/maplibre-particle-rn/src/EncodedFieldGuards.ts index ed40d1809..770cc8700 100644 --- a/packages/maplibre-particle-rn/src/EncodedFieldGuards.ts +++ b/packages/maplibre-particle-rn/src/EncodedFieldGuards.ts @@ -1,9 +1,19 @@ const MAX_ENCODED_ID_UTF8_BYTES = 64; +/** UTF-8 byte length without requiring DOM/TextEncoder types from the consumer project. */ +export function utf8ByteLength(value: string): number { + let bytes = 0; + for (let index = 0; index < value.length; index += 1) { + const codePoint = value.codePointAt(index) ?? 0; + if (codePoint > 0xffff) index += 1; + bytes += codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4; + } + return bytes; +} + /** Transport guard only; Core retains its own field/profile structural rules. */ export function hasValidEncodedIdUtf8Length(value: unknown): value is string { - return typeof value === "string" && (() => { - const bytes = new TextEncoder().encode(value).length; - return bytes >= 1 && bytes <= MAX_ENCODED_ID_UTF8_BYTES; - })(); + if (typeof value !== "string") return false; + const bytes = utf8ByteLength(value); + return bytes >= 1 && bytes <= MAX_ENCODED_ID_UTF8_BYTES; } diff --git a/packages/maplibre-particle-rn/src/FieldRuntime.ts b/packages/maplibre-particle-rn/src/FieldRuntime.ts index 659e0a8b8..b8c77ac34 100644 --- a/packages/maplibre-particle-rn/src/FieldRuntime.ts +++ b/packages/maplibre-particle-rn/src/FieldRuntime.ts @@ -1,12 +1,12 @@ import { getNativeFieldRuntimeBridge } from "./NativeWeatherRuntimeBridge"; -import { hasValidEncodedIdUtf8Length } from "./EncodedFieldGuards"; +import { hasValidEncodedIdUtf8Length, utf8ByteLength } from "./EncodedFieldGuards"; import type { ColorChannel, CommandResult, EncodedColorBindingInput, EncodedFieldRuntimeMetadata, EncodedFieldSubmitResult, EncodedParticleBindingInput, FieldColorStop, FieldPayloadRefs, FieldSubmitResult, FieldValueSource, PlaybackState, ResolvedFieldProfileContractInput, RuntimeSnapshot, TargetFps } from "./types"; const MAX_SAFE = Number.MAX_SAFE_INTEGER; const MAX_CONTRACT_BYTES = 256 * 1024; const encodedString = (value: string, name: string) => { if (!hasValidEncodedIdUtf8Length(value)) throw new Error(`${name} must be a non-empty UTF-8 string of at most 64 bytes`); }; const uint = (value: number, name: string, maximum = MAX_SAFE, positive = false) => { if (!Number.isSafeInteger(value) || value < (positive ? 1 : 0) || value > maximum) throw new Error(`${name} must be a ${positive ? "positive" : "non-negative"} safe integer`); }; -function encodedContractJson(contract: ResolvedFieldProfileContractInput): string { encodedString(contract.profileID, "profileID"); uint(contract.contractSchemaVersion, "contractSchemaVersion", 0xffff, true); if (contract.contractSchemaVersion !== 1) throw new Error("unsupported contractSchemaVersion"); uint(contract.profileVersion, "profileVersion", 0xffffffff, true); if (!Array.isArray(contract.requiredPlanes) || contract.requiredPlanes.length === 0 || contract.requiredPlanes.length > 64) throw new Error("requiredPlanes must contain 1..64 entries"); for (const p of contract.requiredPlanes) { uint(p.semanticID, "semanticID", 0xffff, true); uint(p.unitID, "unitID", 0xffff); uint(p.groupID, "groupID", 0xffff); if (!p.requiredFlag || !["SCALAR", "VELOCITY_COMPONENT", "DIRECTION_ANGLE", "VALIDITY"].includes(p.valueClass) || !["NONE", "U_EAST", "V_NORTH", "W_UP"].includes(p.component)) throw new Error("invalid required plane contract"); } const json = JSON.stringify(contract); if (new TextEncoder().encode(json).length > MAX_CONTRACT_BYTES) throw new Error("contractJson exceeds 256 KiB"); return json; } +function encodedContractJson(contract: ResolvedFieldProfileContractInput): string { encodedString(contract.profileID, "profileID"); uint(contract.contractSchemaVersion, "contractSchemaVersion", 0xffff, true); if (contract.contractSchemaVersion !== 1) throw new Error("unsupported contractSchemaVersion"); uint(contract.profileVersion, "profileVersion", 0xffffffff, true); if (!Array.isArray(contract.requiredPlanes) || contract.requiredPlanes.length === 0 || contract.requiredPlanes.length > 64) throw new Error("requiredPlanes must contain 1..64 entries"); for (const p of contract.requiredPlanes) { uint(p.semanticID, "semanticID", 0xffff, true); uint(p.unitID, "unitID", 0xffff); uint(p.groupID, "groupID", 0xffff); if (!p.requiredFlag || !["SCALAR", "VELOCITY_COMPONENT", "DIRECTION_ANGLE", "VALIDITY"].includes(p.valueClass) || !["NONE", "U_EAST", "V_NORTH", "W_UP"].includes(p.component)) throw new Error("invalid required plane contract"); } const json = JSON.stringify(contract); if (utf8ByteLength(json) > MAX_CONTRACT_BYTES) throw new Error("contractJson exceeds 256 KiB"); return json; } function nativeBridge() { const bridge = getNativeFieldRuntimeBridge(); diff --git a/packages/maplibre-particle-rn/src/index.ts b/packages/maplibre-particle-rn/src/index.ts index a2dd4d4a0..f7e27bfdc 100644 --- a/packages/maplibre-particle-rn/src/index.ts +++ b/packages/maplibre-particle-rn/src/index.ts @@ -2,11 +2,17 @@ export { FieldRuntime } from "./FieldRuntime"; export type { ColorChannel, CommandResult, + EncodedColorBindingInput, + EncodedFieldRuntimeMetadata, + EncodedFieldSubmitResult, + EncodedParticleBindingInput, FieldColorStop, FieldPayloadRefs, FieldSubmitResult, FieldValueSource, PlaybackState, + RequiredPlaneContractInput, + ResolvedFieldProfileContractInput, RuntimeSnapshot, RuntimeState, TargetFps, diff --git a/platform/android/VERSION b/platform/android/VERSION index 0948fb20a..5c8b7f255 100644 --- a/platform/android/VERSION +++ b/platform/android/VERSION @@ -1 +1 @@ -13.4.1-particle.3 +13.4.1-particle.5 diff --git a/src/mbgl/particle/field/field_projection_materialization.cpp b/src/mbgl/particle/field/field_projection_materialization.cpp new file mode 100644 index 000000000..80c2884bd --- /dev/null +++ b/src/mbgl/particle/field/field_projection_materialization.cpp @@ -0,0 +1,164 @@ +#include + +#include +#include +#include + +namespace mbgl::particle { +namespace { +bool sameSelector(const FieldPlaneKey& a, const FieldPlaneKey& b) { + return a.semanticID == b.semanticID && a.groupID == b.groupID; +} +const DecodedFieldPlane* findPlane(const DecodedFieldFrame& frame, const FieldPlaneKey& key) { + for (const auto& plane : frame.planes) + if (sameSelector(plane.key, key)) return &plane; + return nullptr; +} +size_t byteWidth(const FieldDType type) { + switch (type) { + case FieldDType::Int16: + case FieldDType::UInt16: + return 2; + case FieldDType::UInt8: + case FieldDType::Int8: + return 1; + case FieldDType::Float32: + return 4; + default: + return 0; + } +} +uint16_t readU16LE(const uint8_t* data) { + return static_cast(data[0]) | (static_cast(data[1]) << 8); +} +uint32_t readU32LE(const uint8_t* data) { + return static_cast(data[0]) | (static_cast(data[1]) << 8) | + (static_cast(data[2]) << 16) | (static_cast(data[3]) << 24); +} +bool decodeRaw(const DecodedFieldPlane& plane, const size_t index, float& value) { + if (!plane.storage || !std::isfinite(plane.scale) || !std::isfinite(plane.offset) || index >= plane.count) + return false; + const auto width = byteWidth(plane.dtype); + if (!width || plane.count == 0 || plane.storage->size() != static_cast(plane.count) * width) return false; + const uint8_t* data = plane.storage->data() + index * width; + float raw = 0.0f; + switch (plane.dtype) { + case FieldDType::Int16: + raw = static_cast(readU16LE(data)); + break; + case FieldDType::UInt16: + raw = readU16LE(data); + break; + case FieldDType::UInt8: + raw = *data; + break; + case FieldDType::Int8: + raw = static_cast(*data); + break; + case FieldDType::Float32: { + const uint32_t bits = readU32LE(data); + std::memcpy(&raw, &bits, sizeof(raw)); + break; + } + default: + return false; + } + value = raw * plane.scale + plane.offset; + return std::isfinite(raw) && std::isfinite(value); +} +bool materializeValidity(const DecodedFieldFrame& frame, std::vector& output) { + const auto cells = static_cast(frame.grid.width) * frame.grid.height; + if (!frame.validityBits || frame.validityBits->size() != (cells + 7) / 8) return false; + output.resize(cells); + // QBIN V2 bitset order is least-significant-bit first within each byte. + for (size_t cell = 0; cell < cells; ++cell) output[cell] = ((*frame.validityBits)[cell / 8] >> (cell % 8)) & 1u; + return true; +} +ParticleFieldSnapshot baseSnapshot(const DecodedFieldFrame& frame, const std::string& identity) { + ParticleFieldSnapshot snapshot; + snapshot.fieldID = frame.frameContentIdentity.fieldID + ":" + identity; + snapshot.fieldType = "qbin-v4"; + snapshot.displayDomain = "GLOBAL"; + snapshot.validTime = frame.frameContentIdentity.validTimeMs; + snapshot.width = frame.grid.width; + snapshot.height = frame.grid.height; + snapshot.west = frame.grid.west; + snapshot.south = frame.grid.south; + snapshot.east = frame.grid.east; + snapshot.north = frame.grid.north; + snapshot.dataVersion = frame.frameContentIdentity.dataVersion; + snapshot.generation = frame.frameContentIdentity.generation; + snapshot.fieldSha256 = identity; + return snapshot; +} +bool compatible(const DecodedFieldFrame& a, const DecodedFieldFrame& b) { + return a.grid == b.grid && a.grid.isValid() && b.grid.isValid(); +} +bool scalarSnapshot(const DecodedFieldFrame& frame, const DecodedFieldPlane& plane, ParticleFieldSnapshot& output) { + const auto cells = static_cast(frame.grid.width) * frame.grid.height; + if (plane.count != cells || !materializeValidity(frame, output.valid)) return false; + output.scalar.resize(cells); + for (size_t cell = 0; cell < cells; ++cell) + if (!decodeRaw(plane, cell, output.scalar[cell])) return false; + return output.isValid(); +} +bool vectorSnapshot(const DecodedFieldFrame& frame, const ParticleBinding& binding, ParticleFieldSnapshot& output) { + const auto cells = static_cast(frame.grid.width) * frame.grid.height; + if (!materializeValidity(frame, output.valid)) return false; + output.vector.resize(cells * 2); + const auto* first = findPlane( + frame, binding.mode == ParticleBindingMode::VectorComponents ? binding.uPlane : binding.directionPlane); + const auto* second = binding.mode == ParticleBindingMode::VectorComponents ? findPlane(frame, binding.vPlane) + : nullptr; + if (!first || (binding.mode == ParticleBindingMode::VectorComponents && !second) || first->count != cells || + (second && second->count != cells)) + return false; + for (size_t cell = 0; cell < cells; ++cell) { + float u = 0, v = 0; + if (binding.mode == ParticleBindingMode::VectorComponents) { + if (!decodeRaw(*first, cell, u) || !decodeRaw(*second, cell, v)) return false; + output.vectorSemantics = VectorSemantics::Velocity; + } else { + // semantic 10 is the frozen QBIN V2 DIRECTION_TO degree encoding. + if (first->key.semanticID != 10 || first->dtype != FieldDType::UInt16 || !decodeRaw(*first, cell, u)) + return false; + constexpr float Pi = 3.14159265358979323846f; + const auto radians = u * (Pi / 180.0f); + u = std::sin(radians); + v = std::cos(radians); + if (!std::isfinite(u) || !std::isfinite(v)) return false; + output.vectorSemantics = VectorSemantics::Direction; + } + output.vector[cell * 2] = u; + output.vector[cell * 2 + 1] = v; + } + return output.isValid(); +} +} // namespace + +std::optional materializeColorProjection(const ColorProjection& projection) { + if (!projection.t0Frame || !projection.t1Frame || !compatible(*projection.t0Frame, *projection.t1Frame)) return {}; + const auto* p0 = findPlane(*projection.t0Frame, projection.contentIdentity.binding.plane); + const auto* p1 = findPlane(*projection.t1Frame, projection.contentIdentity.binding.plane); + if (!p0 || !p1) return {}; + MaterializedColorProjection result{baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane), + baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1Plane), + projection.contentIdentity, + projection.sampleState}; + if (!scalarSnapshot(*projection.t0Frame, *p0, result.t0) || !scalarSnapshot(*projection.t1Frame, *p1, result.t1)) + return {}; + return result; +} + +std::optional materializeParticleProjection(const ParticleProjection& projection) { + if (!projection.t0Frame || !projection.t1Frame || !compatible(*projection.t0Frame, *projection.t1Frame)) return {}; + MaterializedParticleProjection result{baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0FirstPlane), + baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1FirstPlane), + projection.contentIdentity, + projection.sampleState}; + if (!vectorSnapshot(*projection.t0Frame, projection.contentIdentity.binding, result.t0) || + !vectorSnapshot(*projection.t1Frame, projection.contentIdentity.binding, result.t1)) + return {}; + return result; +} +} // namespace mbgl::particle diff --git a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp index 7506bbc78..1af251749 100644 --- a/src/mbgl/renderer/layers/render_particle_validation_layer.cpp +++ b/src/mbgl/renderer/layers/render_particle_validation_layer.cpp @@ -101,12 +101,27 @@ ParticleSettings particleSettings(const Immutable& value) { layer.trailOpacity, layer.trailFade, layer.advectionScale, - layer.speedColorEnabled, layer.particleColor, layer.trailColor}; + layer.speedColorEnabled, + layer.particleColor, + layer.trailColor}; } assert(value->getTypeInfo() == style::ParticleValidationLayer::Impl::staticTypeInfo()); const auto& layer = static_cast(*value); - return {layer.active, layer.maskMode, layer.fixtureMode, false, 0.0f, 256, 2.0f, 0.018f, 1.0f, 1.0f, 0.965f, 1.0f, false, - {0.08f, 0.55f, 1.0f, 1.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}; + return {layer.active, + layer.maskMode, + layer.fixtureMode, + false, + 0.0f, + 256, + 2.0f, + 0.018f, + 1.0f, + 1.0f, + 0.965f, + 1.0f, + false, + {0.08f, 0.55f, 1.0f, 1.0f}, + {1.0f, 1.0f, 1.0f, 1.0f}}; } enum class ValidationPass : uint8_t { @@ -165,8 +180,8 @@ std::shared_ptr makeVelocityFieldImage(const particle::Parti for (uint32_t x = 0; x < field.width; ++x) { const auto cell = (static_cast(y) * field.width + x) * 2; const auto pixel = (static_cast(y) * field.width + x) * PremultipliedImage::channels; - image->data[pixel + 0] = static_cast(std::clamp(field.vector[cell] / scale * 0.5f + 0.5f, 0.0f, 1.0f) * - 255.0f); + image->data[pixel + 0] = static_cast( + std::clamp(field.vector[cell] / scale * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f); image->data[pixel + 1] = static_cast( std::clamp(field.vector[cell + 1] / scale * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f); image->data[pixel + 2] = 0; @@ -515,15 +530,14 @@ public: const auto dx = u * simulationStep / EarthCircumferenceMeters; const auto dy = -v * simulationStep / (EarthCircumferenceMeters * cosLatitude); Log::Info(Event::Render, - "ParticleMotion: fieldId=" + resources->field.fieldID + " sampleU=" + - std::to_string(u) + " sampleV=" + std::to_string(v) + + "ParticleMotion: fieldId=" + resources->field.fieldID + " sampleU=" + std::to_string(u) + + " sampleV=" + std::to_string(v) + " zoom=" + std::to_string(parameters.state.getZoom()) + " referenceZoom=" + std::to_string(VisualSimulationReferenceZoom) + " effectiveVisualSpeedScale=" + std::to_string(effectiveVisualSpeedScale) + - " wallDeltaSeconds=" + std::to_string(wallDeltaSeconds) + - " simulationDeltaSeconds=" + std::to_string(simulationStep) + - " estimatedStateDeltaWorld=" + std::to_string(dx) + "," + std::to_string(dy) + - " estimatedPixelDelta=" + + " wallDeltaSeconds=" + std::to_string(wallDeltaSeconds) + " simulationDeltaSeconds=" + + std::to_string(simulationStep) + " estimatedStateDeltaWorld=" + std::to_string(dx) + + "," + std::to_string(dy) + " estimatedPixelDelta=" + std::to_string(std::sqrt(dx * dx + dy * dy) * 512.0f * std::exp2(static_cast(parameters.state.getZoom()))) + " simulationFrame=" + std::to_string(resources->committedFrame)); @@ -534,10 +548,12 @@ public: Log::Info(Event::Render, "ParticleLifecycle: configuredSlots=" + std::to_string(resources->particleCount) + " initialAge=HASH_STAGGERED maxAge=HASH_JITTERED_0.75_1.25 " - "lifecycleBaseRealSeconds=" + std::to_string(ParticleLifecycleRealSeconds) + + "lifecycleBaseRealSeconds=" + + std::to_string(ParticleLifecycleRealSeconds) + " respawn=GPU_HASH_ON_INVALID_OR_AGE_EXPIRED" - " fieldId=" + resources->field.fieldID + " simulationFrame=" + - std::to_string(resources->committedFrame) + " readback=disabled"); + " fieldId=" + + resources->field.fieldID + + " simulationFrame=" + std::to_string(resources->committedFrame) + " readback=disabled"); } } @@ -747,14 +763,13 @@ RenderTargetPtr createValidationTarget(gfx::Context& context, } Log::Info(Event::Render, "ParticleStateTarget: event=CREATE_SUCCESS kind=" + std::string(targetKind) + - " dataType=" + textureDataTypeName(type) + " name=" + name + " size=" + - std::to_string(size.width) + "x" + std::to_string(size.height)); + " dataType=" + textureDataTypeName(type) + " name=" + name + + " size=" + std::to_string(size.width) + "x" + std::to_string(size.height)); return target; } catch (const std::exception& error) { Log::Error(Event::Render, - "ParticleStateTarget: event=CREATE_FAILED kind=" + std::string(targetKind) + - " dataType=" + textureDataTypeName(type) + " name=" + name + " stage=exception error=" + - error.what()); + "ParticleStateTarget: event=CREATE_FAILED kind=" + std::string(targetKind) + " dataType=" + + textureDataTypeName(type) + " name=" + name + " stage=exception error=" + error.what()); return {}; } catch (...) { Log::Error(Event::Render, @@ -820,8 +835,7 @@ bool buildQuad(gfx::Context& context, if (stateTexture) { builder->setTexture(stateTexture, shaders::idParticleGpuValidationStateTexture); } - builder->addTweaker(std::make_shared( - resources, + builder->addTweaker(std::make_shared(resources, pass, readSlot, writeSlot, @@ -972,8 +986,8 @@ void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) { Log::Info(Event::Render, "ParticleRender: state=" + std::string(settings.active ? "ACTIVE" : "STATIC") + - " maskMode=" + std::to_string(settings.maskMode) + " id=" + getID() + " fixture=" + - (formalSource && sourceField ? sourceField->fieldID : fixtureID(settings.fixtureMode)) + + " maskMode=" + std::to_string(settings.maskMode) + " id=" + getID() + + " fixture=" + (formalSource && sourceField ? sourceField->fieldID : fixtureID(settings.fixtureMode)) + " layerIndex=" + std::to_string(getLayerIndex()) + " timeMix=" + std::to_string(settings.timeMix) + " particleCount=" + std::to_string(settings.particleCount) + " lineWidth=" + std::to_string(settings.lineWidth) + " streakLength=" + std::to_string(settings.particleLength) + @@ -986,7 +1000,8 @@ void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) { bool RenderRasterParticleLayer::needsRepaint() const { const auto& properties = static_cast(*evaluatedProperties); if (!properties.active || (formalSource && (!sourceField || !sourceNextField)) || - baseImpl->visibility == style::VisibilityType::None) return false; + baseImpl->visibility == style::VisibilityType::None) + return false; return !formalSource || !sourceTimeline || sourceTimeline->cadenceDue(std::chrono::steady_clock::now()); } @@ -1044,6 +1059,17 @@ void RenderRasterParticleLayer::prepare(const LayerPrepareParameters& parameters return; } const auto& source = static_cast(*parameters.source); + sourceUsesV4Projection = false; + sourceProjectionMix = 0.0f; + if (source.impl().v4RendererState.particle) { + const auto& projection = *source.impl().v4RendererState.particle; + sourceField = projection.t0; + sourceNextField = projection.t1; + sourceProjectionMix = projection.sampleState.mix; + sourceTimeline.reset(); + sourceUsesV4Projection = true; + return; + } sourceTimeline = source.impl().timeline; if (sourceTimeline) { const auto frame = sourceTimeline->frame(); @@ -1074,8 +1100,10 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, if (formalSource && (!sourceField || !sourceNextField)) { if (layerGroup) static_cast(*layerGroup).clearDrawables(); if (gpuResources) { - Log::Info(Event::Render, "WeatherRuntime: event=PARTICLE_RESOURCE_RELEASE reason=FIELD_WINDOW_CLEAR layerId=" + getID()); - Log::Info(Event::Render, "WeatherRuntime: event=TRAIL_RESOURCE_RELEASE reason=FIELD_WINDOW_CLEAR layerId=" + getID()); + Log::Info(Event::Render, + "WeatherRuntime: event=PARTICLE_RESOURCE_RELEASE reason=FIELD_WINDOW_CLEAR layerId=" + getID()); + Log::Info(Event::Render, + "WeatherRuntime: event=TRAIL_RESOURCE_RELEASE reason=FIELD_WINDOW_CLEAR layerId=" + getID()); removeTargets(gpuResources->registeredTargets, changes); gpuResources->registeredTargets.clear(); // Targets contain tweakers retaining Resources; break that cycle. @@ -1089,9 +1117,9 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, } // Particle advection requires a compatible vector pair. Scalar-only // remains unavailable; VELOCITY and DIRECTION are resolved in the shader. - if (formalSource && (!sourceField->hasVector() || !sourceNextField->hasVector() || - !sourceField->vectorSemantics || !sourceNextField->vectorSemantics || - sourceField->vectorSemantics != sourceNextField->vectorSemantics)) { + if (formalSource && + (!sourceField->hasVector() || !sourceNextField->hasVector() || !sourceField->vectorSemantics || + !sourceNextField->vectorSemantics || sourceField->vectorSemantics != sourceNextField->vectorSemantics)) { if (layerGroup) static_cast(*layerGroup).clearDrawables(); if (gpuResources) { removeTargets(gpuResources->registeredTargets, changes); @@ -1107,7 +1135,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, const uint8_t fixtureMode = properties.fixtureMode; const uint32_t particleCount = properties.particleCount; const float advectionScale = properties.advectionScale; - float timelineMix = properties.timeMix; + float timelineMix = sourceUsesV4Projection ? sourceProjectionMix : properties.timeMix; bool cadenceAdvanced = true; uint64_t cadenceRevision = sourceTimelineCadenceRevision; if (formalSource && sourceTimeline) { @@ -1142,8 +1170,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, owned->stateSize = particleStateSize(owned->particleCount); rebuildParticleDrawResources(owned); for (uint8_t i = 0; i < 2; ++i) { - owned->stateTargets[i] = createValidationTarget( - context, + owned->stateTargets[i] = createValidationTarget(context, owned->stateSize, getID() + "-state-" + slotName(i), gfx::TextureFilterType::Nearest, @@ -1151,7 +1178,8 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, "STATE"); } if (!owned->shader || !owned->stateTargets[0] || !owned->stateTargets[1]) { - Log::Error(Event::Render, + Log::Error( + Event::Render, "ParticleStateTarget: event=CREATE_FAILED kind=STATE dataType=Float reason=FRAMEBUFFER_UNAVAILABLE"); return; } @@ -1240,12 +1268,12 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, " particleDrawCount=" + std::to_string(resources->particleCount) + " streakVertexCount=" + std::to_string(resources->particleVertices->elements()) + " fullParticleDraw=true resourceGeneration=" + std::to_string(resources->generation)); - Log::Info( - Event::Render, + Log::Info(Event::Render, "ParticleField: field loaded id=" + resources->field.fieldID + " type=" + resources->field.fieldType + - " bounds=[" + std::to_string(resources->field.west) + ',' + std::to_string(resources->field.east) + - "]x[" + std::to_string(resources->field.south) + ',' + std::to_string(resources->field.north) + - "] grid=" + std::to_string(resources->field.width) + 'x' + std::to_string(resources->field.height) + + " bounds=[" + std::to_string(resources->field.west) + ',' + + std::to_string(resources->field.east) + "]x[" + std::to_string(resources->field.south) + ',' + + std::to_string(resources->field.north) + "] grid=" + std::to_string(resources->field.width) + + 'x' + std::to_string(resources->field.height) + " dataVersion=" + std::to_string(resources->field.dataVersion) + " fieldGeneration=" + std::to_string(resources->fieldGeneration)); Log::Info(Event::Render, @@ -1284,8 +1312,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, const auto requestedSize = particleStateSize(particleCount); std::array replacementTargets; for (uint8_t i = 0; i < 2; ++i) { - replacementTargets[i] = createValidationTarget( - context, + replacementTargets[i] = createValidationTarget(context, requestedSize, getID() + "-state-" + slotName(i), gfx::TextureFilterType::Nearest, @@ -1539,7 +1566,8 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, } else if (transformChanged) { if (!resources->trailCameraInvalidated) { const auto reason = std::abs(resources->trailCameraZoom - transformState.getZoom()) > 0.0001 ? "zoom" - : std::abs(resources->trailCameraBearing - transformState.getBearing()) > 0.0001 ? "rotate" + : std::abs(resources->trailCameraBearing - transformState.getBearing()) > 0.0001 + ? "rotate" : std::abs(resources->trailCameraPitch - transformState.getPitch()) > 0.0001 ? "pitch" : "pan"; resources->trailCameraInvalidated = true; @@ -1553,7 +1581,8 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, } else if (resources->trailCameraInvalidated && cameraNow - resources->lastTrailCameraChange >= TrailCameraStableDelay) { resources->trailCameraInvalidated = false; - Log::Info(Event::Render, + Log::Info( + Event::Render, "ParticleTrail: event=CAMERA_STABLE_RESUME statePreserved=true fieldResubmit=false timelineReset=false"); } resources->trailCameraKnown = true; @@ -1576,10 +1605,10 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, "ParticleDensity: zoom=" + std::to_string(transformState.getZoom()) + " mode=" + std::string(respawnDomain.fallback ? "FIELD_FALLBACK" : "VIEWPORT_RESPAWN") + " cameraStable=true margin=" + std::to_string(ViewportRespawnMargin) + " respawnBounds=[" + - std::to_string(bounds[0]) + "," + std::to_string(bounds[1]) + "," + - std::to_string(bounds[2]) + "," + std::to_string(bounds[3]) + "] fieldBounds=[" + - std::to_string(currentFieldBounds[0]) + "," + std::to_string(currentFieldBounds[1]) + "," + - std::to_string(currentFieldBounds[2]) + "," + std::to_string(currentFieldBounds[3]) + + std::to_string(bounds[0]) + "," + std::to_string(bounds[1]) + "," + std::to_string(bounds[2]) + + "," + std::to_string(bounds[3]) + "] fieldBounds=[" + std::to_string(currentFieldBounds[0]) + + "," + std::to_string(currentFieldBounds[1]) + "," + std::to_string(currentFieldBounds[2]) + "," + + std::to_string(currentFieldBounds[3]) + "] fallback=" + (respawnDomain.fallback ? "true" : "false") + " configuredSlots=" + std::to_string(resources->particleCount)); resources->viewportDensityLogged = true; @@ -1591,8 +1620,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, const auto previousSize = resources->trailSize; resources->trailTargets = {}; for (uint8_t i = 0; i < 2; ++i) { - resources->trailTargets[i] = createValidationTarget( - context, + resources->trailTargets[i] = createValidationTarget(context, viewport, getID() + "-trail-" + slotName(i), gfx::TextureFilterType::Linear, @@ -1606,11 +1634,12 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, resources->trailSize = viewport; resources->trailRead = 0; resources->trailSeeded = false; - Log::Info(Event::Render, + Log::Info( + Event::Render, "ParticleStateTarget: stateAType=Float stateBType=Float trailAType=UnsignedByte trailBType=UnsignedByte " - "stateGrid=" + std::to_string(resources->stateSize.width) + "x" + - std::to_string(resources->stateSize.height) + " trailSize=" + - std::to_string(viewport.width) + "x" + std::to_string(viewport.height) + + "stateGrid=" + + std::to_string(resources->stateSize.width) + "x" + std::to_string(resources->stateSize.height) + + " trailSize=" + std::to_string(viewport.width) + "x" + std::to_string(viewport.height) + " resourceGeneration=" + std::to_string(resources->generation)); Log::Info(Event::Render, "ParticleGPU: Trail A/B " + std::string(previousSize.isEmpty() ? "create" : "resize") + @@ -1759,9 +1788,21 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, const auto center = transformState.getLatLng(LatLng::Unwrapped); const auto centerWrap = wrapForLongitude(center.longitude()); const auto wrap = static_cast(centerWrap); - buildQuad(context, resources, compositeGroup, getID() + "-composite-pass-" + std::to_string(wrap), - resources->trailTargets[resources->trailRead]->getTexture(), {}, resources->maskTexture, {}, 0.0f, - ValidationPass::Composite, resources->trailRead, resources->trailRead, wrap, maskMode, false); + buildQuad(context, + resources, + compositeGroup, + getID() + "-composite-pass-" + std::to_string(wrap), + resources->trailTargets[resources->trailRead]->getTexture(), + {}, + resources->maskTexture, + {}, + 0.0f, + ValidationPass::Composite, + resources->trailRead, + resources->trailRead, + wrap, + maskMode, + false); return; } @@ -1784,8 +1825,7 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, MinVisualSimulationSecondsPerRealSecond, MaxVisualSimulationSecondsPerRealSecond); const float visualSimulationDeltaSeconds = std::min( - std::clamp(wallDeltaSeconds, 1.0f / 240.0f, 0.050f) * effectiveVisualSpeedScale, - MaxSimulationDeltaSeconds); + std::clamp(wallDeltaSeconds, 1.0f / 240.0f, 0.050f) * effectiveVisualSpeedScale, MaxSimulationDeltaSeconds); const bool stateBuilt = buildQuad(context, resources, diff --git a/src/mbgl/renderer/layers/render_raster_particle_layer.hpp b/src/mbgl/renderer/layers/render_raster_particle_layer.hpp index 39c520ffa..2415c5324 100644 --- a/src/mbgl/renderer/layers/render_raster_particle_layer.hpp +++ b/src/mbgl/renderer/layers/render_raster_particle_layer.hpp @@ -36,6 +36,8 @@ private: std::optional sourceField; std::optional sourceNextField; std::shared_ptr sourceTimeline; + float sourceProjectionMix = 0.0f; + bool sourceUsesV4Projection = false; uint64_t sourceTimelineWindowRevision = 0; uint64_t sourceTimelineCadenceRevision = 0; bool formalSource = false; diff --git a/src/mbgl/renderer/layers/render_weather_color_layer.cpp b/src/mbgl/renderer/layers/render_weather_color_layer.cpp index da025dbb6..c72e8cbaf 100644 --- a/src/mbgl/renderer/layers/render_weather_color_layer.cpp +++ b/src/mbgl/renderer/layers/render_weather_color_layer.cpp @@ -31,8 +31,10 @@ double mercatorY(const double latitude) { } std::array bounds(const particle::ParticleFieldSnapshot& field) { - return {static_cast((field.west + 180.0) / 360.0), static_cast(mercatorY(field.north)), - static_cast((field.east + 180.0) / 360.0), static_cast(mercatorY(field.south))}; + return {static_cast((field.west + 180.0) / 360.0), + static_cast(mercatorY(field.north)), + static_cast((field.east + 180.0) / 360.0), + static_cast(mercatorY(field.south))}; } bool supportsValueSource(const particle::ParticleFieldSnapshot& field, style::FieldValueSource valueSource) { @@ -41,7 +43,8 @@ bool supportsValueSource(const particle::ParticleFieldSnapshot& field, style::Fi float scale(const particle::ParticleFieldSnapshot& field, style::FieldValueSource valueSource) { float result = 1.0f; - for (size_t index = 0; index < field.valid.size(); ++index) if (field.valid[index]) { + for (size_t index = 0; index < field.valid.size(); ++index) + if (field.valid[index]) { if (valueSource == style::FieldValueSource::Scalar) { result = std::max(result, std::abs(field.scalar[index])); } else { @@ -56,13 +59,16 @@ std::shared_ptr image(const particle::ParticleFieldSnapshot& const float fieldScale, style::FieldValueSource valueSource) { auto result = std::make_shared(Size{field.width, field.height}); - for (uint32_t y = 0; y < field.height; ++y) for (uint32_t x = 0; x < field.width; ++x) { + for (uint32_t y = 0; y < field.height; ++y) + for (uint32_t x = 0; x < field.width; ++x) { const auto cell = static_cast(y) * field.width + x; const auto pixel = (static_cast(y) * field.width + x) * PremultipliedImage::channels; - const float first = valueSource == style::FieldValueSource::Scalar ? field.scalar[cell] : field.vector[cell * 2]; + const float first = valueSource == style::FieldValueSource::Scalar ? field.scalar[cell] + : field.vector[cell * 2]; const float second = valueSource == style::FieldValueSource::Scalar ? 0.0f : field.vector[cell * 2 + 1]; result->data[pixel] = static_cast(std::clamp(first / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f); - result->data[pixel + 1] = static_cast(std::clamp(second / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f); + result->data[pixel + 1] = static_cast(std::clamp(second / fieldScale * .5f + .5f, 0.0f, 1.0f) * + 255.0f); result->data[pixel + 2] = 0; result->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0; } @@ -86,17 +92,29 @@ struct Resources { class Tweaker final : public gfx::DrawableTweaker { public: - Tweaker(std::shared_ptr resources_, float mix_, float opacity_, int16_t wrap_, - std::vector stops_, style::FieldValueSource valueSource_) - : resources(std::move(resources_)), mix(mix_), opacity(opacity_), wrap(wrap_), stops(std::move(stops_)), valueSource(valueSource_) {} + Tweaker(std::shared_ptr resources_, + float mix_, + float opacity_, + int16_t wrap_, + std::vector stops_, + style::FieldValueSource valueSource_) + : resources(std::move(resources_)), + mix(mix_), + opacity(opacity_), + wrap(wrap_), + stops(std::move(stops_)), + valueSource(valueSource_) {} void init(gfx::Drawable&) override {} void execute(gfx::Drawable& drawable, PaintParameters& parameters) override { drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationPreviousTexture); drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationFieldTexture); drawable.setTexture(resources->texture1, shaders::idParticleGpuValidationNextFieldTexture); shaders::ParticleGpuValidationUBO ubo = { - .pass = {5.0f, 0.0f, 0.0f, 0.0f}, .marker = {mix, 0.0f, 0.0f, 0.0f}, .mask = {0, 0, 0, 0}, - .visual = {opacity, 0, 0, 0}, .fieldBounds = bounds(resources->t0), + .pass = {5.0f, 0.0f, 0.0f, 0.0f}, + .marker = {mix, 0.0f, 0.0f, 0.0f}, + .mask = {0, 0, 0, 0}, + .visual = {opacity, 0, 0, 0}, + .fieldBounds = bounds(resources->t0), .respawnBounds = bounds(resources->t0), .fieldScale = {resources->scale0, resources->scale1, 0, 0}, // WeatherColor never performs particle advection. Keep the shared @@ -117,6 +135,7 @@ public: } drawable.mutableUniformBuffers().createOrUpdate(shaders::idParticleGpuValidationUBO, &ubo, parameters.context); } + private: std::shared_ptr resources; float mix; @@ -130,8 +149,12 @@ bool same(const particle::ParticleFieldSnapshot& a, const particle::ParticleFiel return particle::sameFieldGpuContent(a, b); } -bool uploadField(gfx::Context& context, const particle::ParticleFieldSnapshot& field, style::FieldValueSource valueSource, - std::shared_ptr& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) { +bool uploadField(gfx::Context& context, + const particle::ParticleFieldSnapshot& field, + style::FieldValueSource valueSource, + std::shared_ptr& outputImage, + gfx::Texture2DPtr& outputTexture, + float& outputScale) { outputScale = scale(field, valueSource); outputImage = image(field, outputScale, valueSource); outputTexture = context.createTexture2D(); @@ -167,31 +190,65 @@ bool RenderWeatherColorLayer::needsHiddenLayerSourceCleanup(const RenderSource& const auto& impl = source.impl(); return impl.hostFieldSubmission && !impl.field && !impl.nextField && !impl.timeline; } -void RenderWeatherColorLayer::markLayerRenderable(bool willRender, UniqueChangeRequestVec& changes) { RenderLayer::markLayerRenderable(willRender, changes); } -void RenderWeatherColorLayer::layerRemoved(UniqueChangeRequestVec& changes) { resources.reset(); RenderLayer::layerRemoved(changes); } -void RenderWeatherColorLayer::markContextDestroyed() { resources.reset(); } +void RenderWeatherColorLayer::markLayerRenderable(bool willRender, UniqueChangeRequestVec& changes) { + RenderLayer::markLayerRenderable(willRender, changes); +} +void RenderWeatherColorLayer::layerRemoved(UniqueChangeRequestVec& changes) { + resources.reset(); + RenderLayer::layerRemoved(changes); +} +void RenderWeatherColorLayer::markContextDestroyed() { + resources.reset(); +} void RenderWeatherColorLayer::prepare(const LayerPrepareParameters& parameters) { if (!parameters.source || parameters.source->baseImpl->type != style::SourceType::RasterArray) return; const auto& source = static_cast(*parameters.source); + sourceUsesV4Projection = false; + sourceProjectionMix = 0.0f; + if (source.impl().v4RendererState.color) { + const auto& projection = *source.impl().v4RendererState.color; + sourceField = projection.t0; + sourceNextField = projection.t1; + sourceProjectionMix = projection.sampleState.mix; + sourceTimeline.reset(); + sourceUsesV4Projection = true; + return; + } sourceTimeline = source.impl().timeline; if (sourceTimeline) { - const auto frame = sourceTimeline->frame(); sourceField = *frame.t0; sourceNextField = *frame.t1; + const auto frame = sourceTimeline->frame(); + sourceField = *frame.t0; + sourceNextField = *frame.t1; } else if (source.impl().field) { - sourceField = *source.impl().field; sourceNextField = source.impl().nextField ? *source.impl().nextField : *source.impl().field; + sourceField = *source.impl().field; + sourceNextField = source.impl().nextField ? *source.impl().nextField : *source.impl().field; } else { - sourceField.reset(); sourceNextField.reset(); + sourceField.reset(); + sourceNextField.reset(); } } -void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context& context, const TransformState& transformState, - const std::shared_ptr&, const RenderTree&, UniqueChangeRequestVec& changes) { - if (!layerGroup) if (auto group = context.createLayerGroup(layerIndex, 1, getID())) setLayerGroup(std::move(group), changes); +void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, + gfx::Context& context, + const TransformState& transformState, + const std::shared_ptr&, + const RenderTree&, + UniqueChangeRequestVec& changes) { + if (!layerGroup) + if (auto group = context.createLayerGroup(layerIndex, 1, getID())) setLayerGroup(std::move(group), changes); if (!sourceField || !sourceNextField) { if (layerGroup) static_cast(*layerGroup).clearDrawables(); - if (resources) Log::Info(Event::Render, "WeatherRuntime: event=WEATHER_COLOR_RESOURCE_RELEASE reason=FIELD_WINDOW_CLEAR layerId=" + getID()); + if (resources) + Log::Info( + Event::Render, + "WeatherRuntime: event=WEATHER_COLOR_RESOURCE_RELEASE reason=FIELD_WINDOW_CLEAR layerId=" + getID()); resources.reset(); return; } - const auto valueSource = static_cast(*baseImpl).valueSource; + // V4 ColorBinding has already selected and materialized its scalar plane; + // it must not be reinterpreted through the legacy style value source. + const auto valueSource = sourceUsesV4Projection + ? style::FieldValueSource::Scalar + : static_cast(*baseImpl).valueSource; if (!supportsValueSource(*sourceField, valueSource) || !supportsValueSource(*sourceNextField, valueSource)) { if (layerGroup) static_cast(*layerGroup).clearDrawables(); resources.reset(); @@ -209,52 +266,108 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context& } auto state = std::static_pointer_cast(resources); if (!state) { - state = std::make_shared(); state->shader = context.getGenericShader(shaders, ShaderName); - state->vertices = std::make_shared>(RenderStaticData::heatmapTextureVertices()); + state = std::make_shared(); + state->shader = context.getGenericShader(shaders, ShaderName); + state->vertices = std::make_shared>( + RenderStaticData::heatmapTextureVertices()); state->segments = RenderStaticData::heatmapTextureSegments(); state->valueSource = valueSource; - if (!state->shader || !uploadField(context, *sourceField, valueSource, state->image0, state->texture0, state->scale0) || - !uploadField(context, *sourceNextField, valueSource, state->image1, state->texture1, state->scale1)) return; - state->t0 = *sourceField; state->t1 = *sourceNextField; resources = state; - Log::Info(Event::Render, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID + - " T1=" + state->t1.fieldID + " valueSource=" + - (valueSource == style::FieldValueSource::Scalar ? "SCALAR" : "VECTOR_MAGNITUDE")); - } else if (state->valueSource != valueSource || !same(state->t0, *sourceField) || !same(state->t1, *sourceNextField)) { - const auto old0 = state->t0; const auto old1 = state->t1; - auto install = [&](const particle::ParticleFieldSnapshot& wanted, particle::ParticleFieldSnapshot& assigned, - std::shared_ptr& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) { + if (!state->shader || + !uploadField(context, *sourceField, valueSource, state->image0, state->texture0, state->scale0) || + !uploadField(context, *sourceNextField, valueSource, state->image1, state->texture1, state->scale1)) + return; + state->t0 = *sourceField; + state->t1 = *sourceNextField; + resources = state; + Log::Info( + Event::Render, + "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID + " T1=" + state->t1.fieldID + + " valueSource=" + (valueSource == style::FieldValueSource::Scalar ? "SCALAR" : "VECTOR_MAGNITUDE")); + } else if (state->valueSource != valueSource || !same(state->t0, *sourceField) || + !same(state->t1, *sourceNextField)) { + const auto old0 = state->t0; + const auto old1 = state->t1; + auto install = [&](const particle::ParticleFieldSnapshot& wanted, + particle::ParticleFieldSnapshot& assigned, + std::shared_ptr& outputImage, + gfx::Texture2DPtr& outputTexture, + float& outputScale) { // The decoded bundle may be identical while its selected GPU // representation changes (scalar versus vector magnitude). - if (state->valueSource == valueSource && same(old0, wanted)) { assigned = wanted; outputImage = state->image0; outputTexture = state->texture0; outputScale = state->scale0; return true; } - if (state->valueSource == valueSource && same(old1, wanted)) { assigned = wanted; outputImage = state->image1; outputTexture = state->texture1; outputScale = state->scale1; return true; } - assigned = wanted; return uploadField(context, assigned, valueSource, outputImage, outputTexture, outputScale); + if (state->valueSource == valueSource && same(old0, wanted)) { + assigned = wanted; + outputImage = state->image0; + outputTexture = state->texture0; + outputScale = state->scale0; + return true; + } + if (state->valueSource == valueSource && same(old1, wanted)) { + assigned = wanted; + outputImage = state->image1; + outputTexture = state->texture1; + outputScale = state->scale1; + return true; + } + assigned = wanted; + return uploadField(context, assigned, valueSource, outputImage, outputTexture, outputScale); }; - particle::ParticleFieldSnapshot t0, t1; std::shared_ptr i0, i1; gfx::Texture2DPtr x0, x1; float s0 = 1, s1 = 1; + particle::ParticleFieldSnapshot t0, t1; + std::shared_ptr i0, i1; + gfx::Texture2DPtr x0, x1; + float s0 = 1, s1 = 1; if (!install(*sourceField, t0, i0, x0, s0) || !install(*sourceNextField, t1, i1, x1, s1)) return; - state->t0 = std::move(t0); state->t1 = std::move(t1); state->image0 = std::move(i0); state->image1 = std::move(i1); - state->texture0 = std::move(x0); state->texture1 = std::move(x1); state->scale0 = s0; state->scale1 = s1; state->valueSource = valueSource; - Log::Info(Event::Render, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID + " T1=" + state->t1.fieldID); + state->t0 = std::move(t0); + state->t1 = std::move(t1); + state->image0 = std::move(i0); + state->image1 = std::move(i1); + state->texture0 = std::move(x0); + state->texture1 = std::move(x1); + state->scale0 = s0; + state->scale1 = s1; + state->valueSource = valueSource; + Log::Info( + Event::Render, + "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID + " T1=" + state->t1.fieldID); } - const float mix = sourceTimeline ? timelineFrame.mix : 0.0f; + const float mix = sourceTimeline ? timelineFrame.mix : (sourceUsesV4Projection ? sourceProjectionMix : 0.0f); const float opacity = static_cast(*baseImpl).opacity; - auto& group = static_cast(*layerGroup); group.clearDrawables(); + auto& group = static_cast(*layerGroup); + group.clearDrawables(); const auto center = transformState.getLatLng(LatLng::Unwrapped); const auto centerWrap = static_cast(std::floor((center.longitude() + 180.0) / 360.0)); for (int16_t wrap = centerWrap - 1; wrap <= centerWrap + 1; ++wrap) { auto attributes = context.createVertexAttributeArray(); if (const auto& attribute = attributes->set(shaders::idParticleGpuValidationPosVertexAttribute)) - attribute->setSharedRawData(state->vertices, offsetof(HeatmapTextureLayoutVertex, a1), 0, sizeof(HeatmapTextureLayoutVertex), gfx::AttributeDataType::Short2); + attribute->setSharedRawData(state->vertices, + offsetof(HeatmapTextureLayoutVertex, a1), + 0, + sizeof(HeatmapTextureLayoutVertex), + gfx::AttributeDataType::Short2); auto builder = context.createDrawableBuilder(getID() + "-weather-color-" + std::to_string(wrap)); - builder->setShader(state->shader); builder->setRenderPass(RenderPass::Translucent); builder->setEnableDepth(false); - builder->setColorMode(gfx::ColorMode::alphaBlended()); builder->setCullFaceMode(gfx::CullFaceMode::disabled()); - builder->setVertexAttributes(std::move(attributes)); builder->setRawVertices({}, state->vertices->elements(), gfx::AttributeDataType::Short2); - builder->setSegments(gfx::Triangles(), RenderStaticData::quadTriangleIndices().vector(), state->segments.data(), state->segments.size()); + builder->setShader(state->shader); + builder->setRenderPass(RenderPass::Translucent); + builder->setEnableDepth(false); + builder->setColorMode(gfx::ColorMode::alphaBlended()); + builder->setCullFaceMode(gfx::CullFaceMode::disabled()); + builder->setVertexAttributes(std::move(attributes)); + builder->setRawVertices({}, state->vertices->elements(), gfx::AttributeDataType::Short2); + builder->setSegments(gfx::Triangles(), + RenderStaticData::quadTriangleIndices().vector(), + state->segments.data(), + state->segments.size()); builder->setTexture(state->texture0, shaders::idParticleGpuValidationPreviousTexture); builder->setTexture(state->texture0, shaders::idParticleGpuValidationFieldTexture); builder->setTexture(state->texture1, shaders::idParticleGpuValidationNextFieldTexture); - builder->addTweaker(std::make_shared(state, mix, opacity, wrap, - static_cast(*baseImpl).colorStops, valueSource)); builder->flush(context); - auto drawables = builder->clearDrawables(); if (drawables.size() == 1 && drawables.front()) group.addDrawable(std::move(drawables.front())); + builder->addTweaker( + std::make_shared(state, + mix, + opacity, + wrap, + static_cast(*baseImpl).colorStops, + valueSource)); + builder->flush(context); + auto drawables = builder->clearDrawables(); + if (drawables.size() == 1 && drawables.front()) group.addDrawable(std::move(drawables.front())); } } } // namespace mbgl diff --git a/src/mbgl/renderer/layers/render_weather_color_layer.hpp b/src/mbgl/renderer/layers/render_weather_color_layer.hpp index 78ae29cad..c361868c3 100644 --- a/src/mbgl/renderer/layers/render_weather_color_layer.hpp +++ b/src/mbgl/renderer/layers/render_weather_color_layer.hpp @@ -9,8 +9,12 @@ class RenderWeatherColorLayer final : public RenderLayer { public: explicit RenderWeatherColorLayer(Immutable); ~RenderWeatherColorLayer() override; - void update(gfx::ShaderRegistry&, gfx::Context&, const TransformState&, const std::shared_ptr&, - const RenderTree&, UniqueChangeRequestVec&) override; + void update(gfx::ShaderRegistry&, + gfx::Context&, + const TransformState&, + const std::shared_ptr&, + const RenderTree&, + UniqueChangeRequestVec&) override; bool needsHiddenLayerSourceCleanup(const RenderSource&) const override; private: @@ -28,6 +32,8 @@ private: std::optional sourceField; std::optional sourceNextField; std::shared_ptr sourceTimeline; + float sourceProjectionMix = 0.0f; + bool sourceUsesV4Projection = false; }; } // namespace mbgl diff --git a/src/mbgl/renderer/sources/render_raster_array_source.hpp b/src/mbgl/renderer/sources/render_raster_array_source.hpp index 67786c9c2..8fa272110 100644 --- a/src/mbgl/renderer/sources/render_raster_array_source.hpp +++ b/src/mbgl/renderer/sources/render_raster_array_source.hpp @@ -26,7 +26,10 @@ class RenderRasterArraySource final : public RenderSource { public: explicit RenderRasterArraySource(Immutable impl) : RenderSource(std::move(impl)) {} - bool isLoaded() const override { return impl().field.has_value(); } + bool isLoaded() const override { + return impl().field.has_value() || impl().v4RendererState.color.has_value() || + impl().v4RendererState.particle.has_value(); + } void update(Immutable value, const std::vector>&, bool needsRendering, diff --git a/src/mbgl/style/sources/raster_array_source.cpp b/src/mbgl/style/sources/raster_array_source.cpp index 17b73763a..c61060c2a 100644 --- a/src/mbgl/style/sources/raster_array_source.cpp +++ b/src/mbgl/style/sources/raster_array_source.cpp @@ -36,10 +36,12 @@ bool validRuntimeMetadata(const particle::ingress::EncodedFieldRuntimeMetadata& RasterArrayEncodedFieldSubmitResult mapEncodedFieldStoreResult(const particle::FieldSeriesSubmitResult result) { switch (result) { - case particle::FieldSeriesSubmitResult::Inserted: return RasterArrayEncodedFieldSubmitResult::AcceptedInserted; + case particle::FieldSeriesSubmitResult::Inserted: + return RasterArrayEncodedFieldSubmitResult::AcceptedInserted; case particle::FieldSeriesSubmitResult::ReplacedRevision: return RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision; - case particle::FieldSeriesSubmitResult::Duplicate: return RasterArrayEncodedFieldSubmitResult::Duplicate; + case particle::FieldSeriesSubmitResult::Duplicate: + return RasterArrayEncodedFieldSubmitResult::Duplicate; case particle::FieldSeriesSubmitResult::RejectedInvalidFieldKey: return RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey; case particle::FieldSeriesSubmitResult::RejectedIdentityConflict: @@ -55,20 +57,42 @@ RasterArrayEncodedFieldSubmitResult mapEncodedFieldStoreResult(const particle::F return RasterArrayEncodedFieldSubmitResult::RejectedIncompatible; } +bool sameSnapshot(const particle::ParticleFieldSnapshot& a, const particle::ParticleFieldSnapshot& b) { + return a.fieldID == b.fieldID && a.dataVersion == b.dataVersion && a.generation == b.generation && + particle::sameFieldGpuContent(a, b); +} +template +bool sameProjection(const std::optional& a, const std::optional& b) { + if (a.has_value() != b.has_value()) return false; + return !a || (a->contentIdentity == b->contentIdentity && a->sampleState.mix == b->sampleState.mix && + a->sampleState.requestedForecastTimeMs == b->sampleState.requestedForecastTimeMs && + sameSnapshot(a->t0, b->t0) && sameSnapshot(a->t1, b->t1)); +} + } // namespace const char* rasterArrayEncodedFieldSubmitResultName(const RasterArrayEncodedFieldSubmitResult result) { switch (result) { - case RasterArrayEncodedFieldSubmitResult::AcceptedInserted: return "AcceptedInserted"; - case RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision: return "AcceptedReplacedRevision"; - case RasterArrayEncodedFieldSubmitResult::Duplicate: return "Duplicate"; - case RasterArrayEncodedFieldSubmitResult::RejectedDecode: return "RejectedDecode"; - case RasterArrayEncodedFieldSubmitResult::RejectedContract: return "RejectedContract"; - case RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey: return "RejectedInvalidFieldKey"; - case RasterArrayEncodedFieldSubmitResult::RejectedIdentityConflict: return "RejectedIdentityConflict"; - case RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision: return "RejectedStaleRevision"; - case RasterArrayEncodedFieldSubmitResult::RejectedIncompatible: return "RejectedIncompatible"; - case RasterArrayEncodedFieldSubmitResult::RejectedCapacity: return "RejectedCapacity"; + case RasterArrayEncodedFieldSubmitResult::AcceptedInserted: + return "AcceptedInserted"; + case RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision: + return "AcceptedReplacedRevision"; + case RasterArrayEncodedFieldSubmitResult::Duplicate: + return "Duplicate"; + case RasterArrayEncodedFieldSubmitResult::RejectedDecode: + return "RejectedDecode"; + case RasterArrayEncodedFieldSubmitResult::RejectedContract: + return "RejectedContract"; + case RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey: + return "RejectedInvalidFieldKey"; + case RasterArrayEncodedFieldSubmitResult::RejectedIdentityConflict: + return "RejectedIdentityConflict"; + case RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision: + return "RejectedStaleRevision"; + case RasterArrayEncodedFieldSubmitResult::RejectedIncompatible: + return "RejectedIncompatible"; + case RasterArrayEncodedFieldSubmitResult::RejectedCapacity: + return "RejectedCapacity"; } return "RejectedIncompatible"; } @@ -146,7 +170,8 @@ RasterArrayCadenceAdvance RasterArrayTimelineRuntime::advanceIfDue(const std::ch ++cadenceTickCount; if (lastCadenceLog.time_since_epoch().count() == 0 || now - lastCadenceLog >= std::chrono::seconds(1)) { lastCadenceLog = now; - Log::Info(Event::Render, "WeatherCadence: targetFps=" + std::to_string(static_cast(targetFps_)) + + Log::Info(Event::Render, + "WeatherCadence: targetFps=" + std::to_string(static_cast(targetFps_)) + " weatherTicks=" + std::to_string(cadenceTickCount)); } if (lastTick == std::chrono::steady_clock::time_point{}) { @@ -274,10 +299,13 @@ bool RasterArrayTimelineRuntime::seek(const size_t index) { bool RasterArrayTimelineRuntime::seekTimestamp(const uint64_t timestamp) { std::lock_guard lock(mutex); - if (gap || invalid || entries.size() < 2 || timestamp < entries.front().timestamp || timestamp > entries.back().timestamp) return false; + if (gap || invalid || entries.size() < 2 || timestamp < entries.front().timestamp || + timestamp > entries.back().timestamp) + return false; const auto previousT0 = t0; const auto previousState = state; - const auto upper = std::upper_bound(entries.begin(), entries.end(), timestamp, [](uint64_t value, const Entry& entry) { + const auto upper = std::upper_bound( + entries.begin(), entries.end(), timestamp, [](uint64_t value, const Entry& entry) { return value < entry.timestamp; }); if (upper == entries.begin()) return false; @@ -294,9 +322,9 @@ bool RasterArrayTimelineRuntime::seekTimestamp(const uint64_t timestamp) { } if (t0 != previousT0) ++windowRevision; lastTick = std::chrono::steady_clock::now(); - Log::Info(Event::Render, "ParticleTimeline: event=PLAYBACK_SEEK_TIMESTAMP timestamp=" + std::to_string(timestamp) + - " t0Index=" + std::to_string(t0) + " t1Index=" + std::to_string(t0 + 1) + - " mix=" + std::to_string(mix)); + Log::Info(Event::Render, + "ParticleTimeline: event=PLAYBACK_SEEK_TIMESTAMP timestamp=" + std::to_string(timestamp) + " t0Index=" + + std::to_string(t0) + " t1Index=" + std::to_string(t0 + 1) + " mix=" + std::to_string(mix)); return true; } bool RasterArrayTimelineRuntime::step(const int direction) { @@ -369,39 +397,84 @@ RasterArrayEncodedFieldSubmitResult RasterArraySource::submitEncodedField( if (!decoded.accepted()) return RasterArrayEncodedFieldSubmitResult::RejectedDecode; if (!particle::FieldProfileValidator::validate(contract, *decoded.frame).accepted()) return RasterArrayEncodedFieldSubmitResult::RejectedContract; - return mapEncodedFieldStoreResult(encodedFieldStore.submit(fieldKey, std::move(decoded.frame))); + const auto result = mapEncodedFieldStoreResult(encodedFieldStore.submit(fieldKey, std::move(decoded.frame))); + if (result == RasterArrayEncodedFieldSubmitResult::AcceptedInserted || + result == RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision) + refreshV4RendererState(); + return result; } -void RasterArraySource::setColorBinding(particle::ColorBinding binding) { colorBinding = std::move(binding); } +void RasterArraySource::setColorBinding(particle::ColorBinding binding) { + colorBinding = std::move(binding); + refreshV4RendererState(); +} -void RasterArraySource::setParticleBinding(particle::ParticleBinding binding) { particleBinding = std::move(binding); } +void RasterArraySource::setParticleBinding(particle::ParticleBinding binding) { + particleBinding = std::move(binding); + refreshV4RendererState(); +} bool RasterArraySource::setRequestedForecastTime(const uint64_t timestampMs) { - return forecastClock.setRequestedForecastTime(timestampMs); + const bool changed = forecastClock.setRequestedForecastTime(timestampMs); + if (changed) refreshV4RendererState(); + return changed; } void RasterArraySource::setForecastPlaybackState(const particle::ForecastPlaybackState state) { state == particle::ForecastPlaybackState::Playing ? forecastClock.play() : forecastClock.pause(); } -bool RasterArraySource::setForecastPlaybackSpeed(const float value) { return forecastClock.setPlaybackSpeed(value); } +bool RasterArraySource::setForecastPlaybackSpeed(const float value) { + return forecastClock.setPlaybackSpeed(value); +} -bool RasterArraySource::advanceForecastClock(const uint64_t elapsedMs) { return forecastClock.advance(elapsedMs); } +bool RasterArraySource::advanceForecastClock(const uint64_t elapsedMs) { + const bool changed = forecastClock.advance(elapsedMs); + if (changed) refreshV4RendererState(); + return changed; +} particle::ColorProjectionResult RasterArraySource::resolveColorProjection() const { if (!colorBinding) return {particle::BindingProjectionError::InvalidBinding, {}}; - return particle::BindingProjectionResolver::resolve(encodedFieldStore, *colorBinding, forecastClock.requestedForecastTime()); + return particle::BindingProjectionResolver::resolve( + encodedFieldStore, *colorBinding, forecastClock.requestedForecastTime()); } particle::ParticleProjectionResult RasterArraySource::resolveParticleProjection() const { if (!particleBinding) return {particle::BindingProjectionError::InvalidBinding, {}}; - return particle::BindingProjectionResolver::resolve(encodedFieldStore, *particleBinding, - forecastClock.requestedForecastTime()); + return particle::BindingProjectionResolver::resolve( + encodedFieldStore, *particleBinding, forecastClock.requestedForecastTime()); } -bool RasterArraySource::clearEncodedField(const std::string& fieldKey) { return encodedFieldStore.clear(fieldKey); } +bool RasterArraySource::clearEncodedField(const std::string& fieldKey) { + const bool changed = encodedFieldStore.clear(fieldKey); + if (changed) refreshV4RendererState(); + return changed; +} -void RasterArraySource::clearAllEncodedFields() { encodedFieldStore.clearAll(); } +void RasterArraySource::clearAllEncodedFields() { + encodedFieldStore.clearAll(); + refreshV4RendererState(); +} + +void RasterArraySource::refreshV4RendererState() { + Impl::V4RendererState next; + if (colorBinding) { + const auto resolved = resolveColorProjection(); + if (resolved.accepted()) next.color = particle::materializeColorProjection(*resolved.projection); + } + if (particleBinding) { + const auto resolved = resolveParticleProjection(); + if (resolved.accepted()) next.particle = particle::materializeParticleProjection(*resolved.projection); + } + const auto& current = impl().v4RendererState; + if (sameProjection(current.color, next.color) && sameProjection(current.particle, next.particle)) return; + auto updated = makeMutable(impl()); + updated->v4RendererState = std::move(next); + baseImpl = std::move(updated); + loaded = true; + observer->onSourceChanged(*this); +} const RasterArraySource::Impl& RasterArraySource::impl() const { return static_cast(*baseImpl); @@ -424,22 +497,28 @@ void RasterArraySource::loadDescription(FileSource& fileSource) { localFields.push_back(std::move(candidate)); const auto index = localFields.size() - 1; auto& local = localFields.back(); - local.metadataRequest = fileSource.request(Resource::source(local.descriptor.metadataURL), + local.metadataRequest = fileSource.request( + Resource::source(local.descriptor.metadataURL), [this, self = makeWeakPtr(), index](const Response& response) { if (auto guard = self.lock(); self) { if (response.error || response.noContent || !response.data) { - observer->onSourceError(*this, std::make_exception_ptr(std::runtime_error("particle local metadata request failed"))); + observer->onSourceError( + *this, + std::make_exception_ptr(std::runtime_error("particle local metadata request failed"))); return; } localFields[index].metadata = *response.data; tryLoadLocalField(index); } }); - local.fieldRequest = fileSource.request(Resource::source(local.descriptor.fieldURL), + local.fieldRequest = fileSource.request( + Resource::source(local.descriptor.fieldURL), [this, self = makeWeakPtr(), index](const Response& response) { if (auto guard = self.lock(); self) { if (response.error || response.noContent || !response.data) { - observer->onSourceError(*this, std::make_exception_ptr(std::runtime_error("particle local field request failed"))); + observer->onSourceError( + *this, + std::make_exception_ptr(std::runtime_error("particle local field request failed"))); return; } localFields[index].fieldBytes = std::vector(response.data->begin(), response.data->end()); @@ -576,8 +655,8 @@ void RasterArraySource::tryLoad() { observer->onSourceChanged(*this); tryInitializeTimeline(); } catch (const std::exception& error) { - Log::Warning(Event::Render, "ParticleField: event=ADAPTER_REJECTED sourceId=" + getID() + - " error=" + error.what()); + Log::Warning(Event::Render, + "ParticleField: event=ADAPTER_REJECTED sourceId=" + getID() + " error=" + error.what()); observer->onSourceError(*this, std::make_exception_ptr(std::runtime_error(error.what()))); } } @@ -589,18 +668,19 @@ void RasterArraySource::tryLoadRuntimeField(const size_t index) { // A candidate is already a Core-owned runtime snapshot when it leaves // the adapter. Do not defer the required revision assignment until // timeline construction; that made valid preload data fail closed. - candidate.snapshot = particle::OfflineFixtureParticleAdapter::load( - candidate.descriptor.fixtureID, *candidate.metadata, *candidate.fieldBytes, + candidate.snapshot = particle::OfflineFixtureParticleAdapter::load(candidate.descriptor.fixtureID, + *candidate.metadata, + *candidate.fieldBytes, ++nextRasterArrayFieldGeneration); Log::Info(Event::Render, "ParticleField: runtime candidate ready fieldId=" + candidate.snapshot->fieldID + " dataVersion=" + std::to_string(candidate.snapshot->dataVersion)); tryInitializeTimeline(); } catch (const std::exception& error) { - Log::Warning(Event::Render, "ParticleField: event=ADAPTER_REJECTED sourceId=" + getID() + - " candidateIndex=" + std::to_string(index) + " fieldId=" + - candidate.descriptor.fixtureID + " generation=" + - std::to_string(candidate.snapshot ? candidate.snapshot->generation : 0) + + Log::Warning(Event::Render, + "ParticleField: event=ADAPTER_REJECTED sourceId=" + getID() + + " candidateIndex=" + std::to_string(index) + " fieldId=" + candidate.descriptor.fixtureID + + " generation=" + std::to_string(candidate.snapshot ? candidate.snapshot->generation : 0) + " validTime=" + std::to_string(candidate.snapshot ? candidate.snapshot->validTime : 0) + " error=" + error.what()); observer->onSourceError(*this, std::make_exception_ptr(std::runtime_error(error.what()))); @@ -613,15 +693,16 @@ void RasterArraySource::tryLoadLocalField(const size_t index) { const auto result = particle::LocalParticleFieldFeed::load(*candidate.metadata, *candidate.fieldBytes); if (!result) { const auto fieldID = candidate.metadata->find("fieldId") == std::string::npos ? "" : "metadata"; - Log::Warning(Event::Render, "ParticleField: event=ADAPTER_REJECTED sourceId=" + getID() + - " errorCode=" + particle::particleFieldAdapterErrorName(result.error) + - " fieldId=" + fieldID + " payloadBytes=" + std::to_string(candidate.fieldBytes->size()) + - " error=" + result.message); + Log::Warning(Event::Render, + "ParticleField: event=ADAPTER_REJECTED sourceId=" + getID() + + " errorCode=" + particle::particleFieldAdapterErrorName(result.error) + " fieldId=" + fieldID + + " payloadBytes=" + std::to_string(candidate.fieldBytes->size()) + " error=" + result.message); observer->onSourceError(*this, std::make_exception_ptr(std::runtime_error(result.message))); return; } candidate.snapshot = std::move(*result.snapshot); - Log::Info(Event::Render, "ParticleField: event=PRODUCTION_FEED_LOAD fieldId=" + candidate.snapshot->fieldID + + Log::Info(Event::Render, + "ParticleField: event=PRODUCTION_FEED_LOAD fieldId=" + candidate.snapshot->fieldID + " generation=" + std::to_string(candidate.snapshot->generation) + " validTime=" + std::to_string(candidate.snapshot->validTime) + " payloadBytes=" + std::to_string(candidate.fieldBytes->size())); @@ -632,11 +713,13 @@ void RasterArraySource::tryInitializeTimeline() { if (timeline) return; if (!impl().localFields.empty()) { if (localFields.size() != impl().localFields.size() || localFields.size() < 2) return; - for (const auto& candidate : localFields) if (!candidate.snapshot) return; + for (const auto& candidate : localFields) + if (!candidate.snapshot) return; std::vector entries; entries.reserve(localFields.size()); for (const auto& candidate : localFields) { - entries.push_back({std::make_shared(*candidate.snapshot), candidate.snapshot->validTime}); + entries.push_back({std::make_shared(*candidate.snapshot), + candidate.snapshot->validTime}); } try { if (submitInitialRuntimeFieldWindow(*localFields[0].snapshot, *localFields[1].snapshot) != @@ -747,9 +830,8 @@ RasterArrayRuntimeFieldUpdateResult RasterArraySource::updateRuntimeFieldWindow( observer->onSourceChanged(*this); Log::Info(Event::Render, "ParticleField: event=FIELD_WINDOW_UPDATE_APPLIED result=APPLIED sourceId=" + getID() + - " sequence=" + std::to_string(runtimeUpdateSequence) + " newT0=" + impl().field->fieldID + - " newT1=" + impl().nextField->fieldID + - " generation=" + std::to_string(impl().nextField->generation)); + " sequence=" + std::to_string(runtimeUpdateSequence) + " newT0=" + impl().field->fieldID + " newT1=" + + impl().nextField->fieldID + " generation=" + std::to_string(impl().nextField->generation)); return RasterArrayRuntimeFieldUpdateResult::Applied; } @@ -842,14 +924,13 @@ RasterArrayHostFieldSubmitResult RasterArraySource::submitHostField(const std::s } RasterArrayHostFieldSubmitResult RasterArraySource::submitHostField( - const std::string& metadataText, - const particle::ParticleFieldPayloadBundle& payloads) { + const std::string& metadataText, const particle::ParticleFieldPayloadBundle& payloads) { const auto bytes = [](const std::optional>& plane) { return plane ? std::to_string(plane->size()) : std::string("absent"); }; Log::Info(Event::Render, - "ParticleField: event=HOST_FIELD_V2_SUBMIT_REQUEST sourceId=" + getID() + " metadataBytes=" + - std::to_string(metadataText.size()) + " scalarBytes=" + bytes(payloads.scalar) + + "ParticleField: event=HOST_FIELD_V2_SUBMIT_REQUEST sourceId=" + getID() + + " metadataBytes=" + std::to_string(metadataText.size()) + " scalarBytes=" + bytes(payloads.scalar) + " vectorBytes=" + bytes(payloads.vector) + " validityBytes=" + bytes(payloads.validity)); if (!impl().hostFieldSubmission) { return RasterArrayHostFieldSubmitResult::RejectedSourceMode; @@ -858,8 +939,8 @@ RasterArrayHostFieldSubmitResult RasterArraySource::submitHostField( if (!decoded) { Log::Warning(Event::Render, "ParticleField: event=HOST_FIELD_V2_SUBMIT_REJECTED sourceId=" + getID() + - " result=REJECTED_ADAPTER errorCode=" + particle::particleFieldAdapterErrorName(decoded.error) + - " error=" + decoded.message); + " result=REJECTED_ADAPTER errorCode=" + + particle::particleFieldAdapterErrorName(decoded.error) + " error=" + decoded.message); return RasterArrayHostFieldSubmitResult::RejectedAdapter; } return submitDecodedHostField(std::move(*decoded.snapshot)); @@ -870,9 +951,9 @@ RasterArrayHostFieldSubmitResult RasterArraySource::submitDecodedHostField(parti "ParticleField: event=HOST_FIELD_SNAPSHOT_READY sourceId=" + getID() + " fieldId=" + snapshot.fieldID + " generation=" + std::to_string(snapshot.generation) + " validTime=" + std::to_string(snapshot.validTime) + " width=" + std::to_string(snapshot.width) + - " height=" + std::to_string(snapshot.height) + " scalar=" + - (snapshot.hasScalar() ? "present" : "absent") + " vector=" + - (snapshot.hasVector() ? "present" : "absent") + " validity=present"); + " height=" + std::to_string(snapshot.height) + + " scalar=" + (snapshot.hasScalar() ? "present" : "absent") + + " vector=" + (snapshot.hasVector() ? "present" : "absent") + " validity=present"); std::shared_ptr previous; if (timeline) { @@ -956,7 +1037,8 @@ RasterArrayHostFieldSubmitResult RasterArraySource::submitDecodedHostField(parti bool RasterArraySource::clearHostFields() { if (!impl().hostFieldSubmission) { - Log::Warning(Event::Render, "WeatherRuntime: event=FIELD_WINDOW_CLEAR_REJECTED reason=SOURCE_MODE sourceId=" + getID()); + Log::Warning(Event::Render, + "WeatherRuntime: event=FIELD_WINDOW_CLEAR_REJECTED reason=SOURCE_MODE sourceId=" + getID()); return false; } if (timeline) timeline->pause(); @@ -978,12 +1060,14 @@ bool RasterArraySource::clearHostFields() { bool RasterArraySource::seekRuntimeTimelineToTimestamp(const uint64_t timestamp) { if (!timeline) { - Log::Warning(Event::Render, "WeatherRuntime: event=SEEK_TIMESTAMP_REJECTED reason=TIMELINE_NOT_READY sourceId=" + getID()); + Log::Warning(Event::Render, + "WeatherRuntime: event=SEEK_TIMESTAMP_REJECTED reason=TIMELINE_NOT_READY sourceId=" + getID()); return false; } const bool accepted = timeline->seekTimestamp(timestamp); if (!accepted) { - Log::Warning(Event::Render, "WeatherRuntime: event=SEEK_TIMESTAMP_REJECTED reason=OUT_OF_RANGE sourceId=" + getID() + + Log::Warning(Event::Render, + "WeatherRuntime: event=SEEK_TIMESTAMP_REJECTED reason=OUT_OF_RANGE sourceId=" + getID() + " timestamp=" + std::to_string(timestamp)); } return accepted; @@ -1021,10 +1105,17 @@ bool RasterArraySource::controlRuntimeTimeline(const int command, const float va bool RasterArraySource::setRuntimeTargetFps(const uint8_t requested) { RasterArrayTargetFps next; switch (requested) { - case 0: next = RasterArrayTargetFps::Auto; break; - case 30: next = RasterArrayTargetFps::Fps30; break; - case 60: next = RasterArrayTargetFps::Fps60; break; - default: return false; + case 0: + next = RasterArrayTargetFps::Auto; + break; + case 30: + next = RasterArrayTargetFps::Fps30; + break; + case 60: + next = RasterArrayTargetFps::Fps60; + break; + default: + return false; } targetFps = next; if (timeline) timeline->setTargetFps(next); diff --git a/src/mbgl/style/sources/raster_array_source_impl.hpp b/src/mbgl/style/sources/raster_array_source_impl.hpp index cb1ba80b7..e7e47d504 100644 --- a/src/mbgl/style/sources/raster_array_source_impl.hpp +++ b/src/mbgl/style/sources/raster_array_source_impl.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -8,6 +9,10 @@ namespace mbgl::style { class RasterArraySource::Impl final : public Source::Impl { public: + struct V4RendererState { + std::optional color; + std::optional particle; + }; Impl(std::string sourceID, std::string fixtureID_, std::string metadataURL_, @@ -25,7 +30,8 @@ public: nextFieldURL(std::move(nextFieldURL_)), runtimeFields(std::move(runtimeFields_)) {} Impl(std::string sourceID, std::vector localFields_) - : Source::Impl(SourceType::RasterArray, std::move(sourceID)), localFields(std::move(localFields_)) {} + : Source::Impl(SourceType::RasterArray, std::move(sourceID)), + localFields(std::move(localFields_)) {} explicit Impl(std::string sourceID) : Source::Impl(SourceType::RasterArray, std::move(sourceID)), hostFieldSubmission(true) {} @@ -42,6 +48,7 @@ public: localFields(other.localFields), hostFieldSubmission(other.hostFieldSubmission), timeline(other.timeline), + v4RendererState(other.v4RendererState), field(std::move(field_)) {} Impl(const Impl& other, particle::ParticleFieldSnapshot field_, particle::ParticleFieldSnapshot nextField_) : Impl(other, std::move(field_)) { @@ -59,6 +66,9 @@ public: std::vector localFields; bool hostFieldSubmission = false; std::shared_ptr timeline; + // Immutable renderer-facing V4 state. It is intentionally separate from + // the legacy V3 timeline/field pair. + V4RendererState v4RendererState; std::optional field; std::optional nextField; }; diff --git a/test/source/raster_array_v4_host_api.test.cpp b/test/source/raster_array_v4_host_api.test.cpp index 95cb8112f..cda62e8da 100644 --- a/test/source/raster_array_v4_host_api.test.cpp +++ b/test/source/raster_array_v4_host_api.test.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -19,7 +20,8 @@ constexpr uint64_t Base = 1'700'000'000'000ULL; constexpr uint64_t Hour = 3'600'000ULL; void u16(std::vector& bytes, size_t offset, uint16_t value) { - bytes[offset] = uint8_t(value); bytes[offset + 1] = uint8_t(value >> 8); + bytes[offset] = uint8_t(value); + bytes[offset + 1] = uint8_t(value >> 8); } void u32(std::vector& bytes, size_t offset, uint32_t value) { for (size_t i = 0; i < 4; ++i) bytes[offset + i] = uint8_t(value >> (8 * i)); @@ -27,18 +29,27 @@ void u32(std::vector& bytes, size_t offset, uint32_t value) { void u64(std::vector& bytes, size_t offset, uint64_t value) { for (size_t i = 0; i < 8; ++i) bytes[offset + i] = uint8_t(value >> (8 * i)); } -void i64(std::vector& bytes, size_t offset, int64_t value) { u64(bytes, offset, static_cast(value)); } +void i64(std::vector& bytes, size_t offset, int64_t value) { + u64(bytes, offset, static_cast(value)); +} void f32(std::vector& bytes, size_t offset, float value) { - uint32_t bits; std::memcpy(&bits, &value, sizeof(bits)); u32(bytes, offset, bits); + uint32_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + u32(bytes, offset, bits); } void f64(std::vector& bytes, size_t offset, double value) { - uint64_t bits; std::memcpy(&bits, &value, sizeof(bits)); u64(bytes, offset, bits); + uint64_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + u64(bytes, offset, bits); +} +size_t aligned16(size_t value) { + return (value + 15) & ~size_t(15); } -size_t aligned16(size_t value) { return (value + 15) & ~size_t(15); } uint32_t crc(const std::vector& bytes) { uint32_t result = ~0u; for (size_t i = 0; i < bytes.size(); ++i) { - uint8_t value = i >= 76 && i < 80 ? 0 : bytes[i]; result ^= value; + uint8_t value = i >= 76 && i < 80 ? 0 : bytes[i]; + result ^= value; for (int bit = 0; bit < 8; ++bit) result = (result >> 1) ^ ((result & 1) ? 0xedb88320u : 0); } return ~result; @@ -52,7 +63,8 @@ struct Plane { std::vector qbin(uint64_t validTime, bool scalar = false) { std::vector planes; - if (scalar) planes.push_back({99, 77, 0, 3, 0, 0, {1, 2, 3, 4}}); + if (scalar) + planes.push_back({99, 77, 0, 3, 0, 0, {1, 2, 3, 4}}); else { planes.push_back({1, 1, 1, 1, 1, 1, {1, 0, 2, 0, 3, 0, 4, 0}}); planes.push_back({2, 1, 1, 1, 1, 2, {5, 0, 6, 0, 7, 0, 8, 0}}); @@ -62,44 +74,76 @@ std::vector qbin(uint64_t validTime, bool scalar = false) { size_t payloadLength = 0; for (const auto& plane : planes) payloadLength = aligned16(payloadLength + plane.bytes.size()); std::vector bytes(Header + planes.size() * Descriptor + payloadLength); - std::memcpy(bytes.data(), "QBV2", 4); bytes[4] = 2; bytes[5] = 1; u16(bytes, 6, 999); - bytes[8] = bytes[9] = bytes[10] = 1; u16(bytes, 12, planes.size()); u32(bytes, 16, 2); u32(bytes, 20, 2); - i64(bytes, 24, static_cast(validTime / 1000)); i64(bytes, 32, static_cast(validTime / 1000)); - f64(bytes, 40, -10); f64(bytes, 48, -10); f64(bytes, 56, 10); f64(bytes, 64, 10); u32(bytes, 72, payloadLength); + std::memcpy(bytes.data(), "QBV2", 4); + bytes[4] = 2; + bytes[5] = 1; + u16(bytes, 6, 999); + bytes[8] = bytes[9] = bytes[10] = 1; + u16(bytes, 12, planes.size()); + u32(bytes, 16, 2); + u32(bytes, 20, 2); + i64(bytes, 24, static_cast(validTime / 1000)); + i64(bytes, 32, static_cast(validTime / 1000)); + f64(bytes, 40, -10); + f64(bytes, 48, -10); + f64(bytes, 56, 10); + f64(bytes, 64, 10); + u32(bytes, 72, payloadLength); u16(bytes, 80, 0xffff); size_t payloadOffset = 0; for (size_t i = 0; i < planes.size(); ++i) { - const auto& plane = planes[i]; const size_t descriptor = Header + i * Descriptor; - u16(bytes, descriptor, plane.semantic); bytes[descriptor + 2] = plane.type; bytes[descriptor + 3] = 1; - f32(bytes, descriptor + 4, 1); f32(bytes, descriptor + 8, 0); u32(bytes, descriptor + 12, 4); - u32(bytes, descriptor + 16, payloadOffset); u32(bytes, descriptor + 20, plane.bytes.size()); - u16(bytes, descriptor + 24, plane.unit); u16(bytes, descriptor + 26, plane.group); - bytes[descriptor + 28] = plane.valueClass; bytes[descriptor + 29] = plane.component; - std::copy(plane.bytes.begin(), plane.bytes.end(), bytes.begin() + Header + planes.size() * Descriptor + payloadOffset); + const auto& plane = planes[i]; + const size_t descriptor = Header + i * Descriptor; + u16(bytes, descriptor, plane.semantic); + bytes[descriptor + 2] = plane.type; + bytes[descriptor + 3] = 1; + f32(bytes, descriptor + 4, 1); + f32(bytes, descriptor + 8, 0); + u32(bytes, descriptor + 12, 4); + u32(bytes, descriptor + 16, payloadOffset); + u32(bytes, descriptor + 20, plane.bytes.size()); + u16(bytes, descriptor + 24, plane.unit); + u16(bytes, descriptor + 26, plane.group); + bytes[descriptor + 28] = plane.valueClass; + bytes[descriptor + 29] = plane.component; + std::copy(plane.bytes.begin(), + plane.bytes.end(), + bytes.begin() + Header + planes.size() * Descriptor + payloadOffset); payloadOffset = aligned16(payloadOffset + plane.bytes.size()); } - u32(bytes, 76, crc(bytes)); return bytes; + u32(bytes, 76, crc(bytes)); + return bytes; } particle::ingress::EncodedFieldRuntimeMetadata metadata(const char* id, uint64_t generation, uint64_t version = 1) { return {id, generation, version}; } particle::ResolvedFieldProfileContract windContract() { - particle::ResolvedFieldProfileContract result; result.profileID = "test.wind"; result.profileVersion = 1; - result.requiredPlanes = {{1, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::UEast, true}, + particle::ResolvedFieldProfileContract result; + result.profileID = "test.wind"; + result.profileVersion = 1; + result.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 result; } particle::ResolvedFieldProfileContract waveContract() { - particle::ResolvedFieldProfileContract result; result.profileID = "test.wave"; result.profileVersion = 1; + particle::ResolvedFieldProfileContract result; + result.profileID = "test.wave"; + result.profileVersion = 1; result.requiredPlanes = {{3, particle::FieldValueClass::Validity, 0, 0, particle::FieldComponent::None, true}, {99, particle::FieldValueClass::Scalar, 77, 0, particle::FieldComponent::None, true}}; return result; } -void submit(style::RasterArraySource& source, const char* key, const char* id, uint64_t time, uint64_t generation, +void submit(style::RasterArraySource& source, + const char* key, + const char* id, + uint64_t time, + uint64_t generation, bool scalar = false) { - EXPECT_EQ(source.submitEncodedField(key, metadata(id, generation), scalar ? waveContract() : windContract(), qbin(time, scalar)), + EXPECT_EQ(source.submitEncodedField( + key, metadata(id, generation), scalar ? waveContract() : windContract(), qbin(time, scalar)), style::RasterArrayEncodedFieldSubmitResult::AcceptedInserted); } @@ -116,6 +160,59 @@ void expectSameParticleProjection(const particle::ParticleProjection& expected, EXPECT_EQ(actual.t1ValidityStorage, expected.t1ValidityStorage); } +std::shared_ptr materializerFrame(const char* fieldID, + uint64_t validTime, + uint64_t generation, + uint64_t dataVersion, + particle::FieldPlaneKey key, + particle::FieldDType dtype, + particle::FieldValueClass valueClass, + uint16_t unitID, + float scale, + float offset, + std::vector raw, + uint8_t validity = 0x0f) { + auto frame = std::make_shared(); + frame->frameContentIdentity = {fieldID, generation, dataVersion, validTime}; + frame->referenceTimeMs = validTime; + frame->grid = {1, 1, 1, 1, 2, 2, -10, -10, 10, 10}; + frame->validityContentIdentity = "validity"; + frame->validityBits = std::make_shared>(std::vector{validity}); + frame->validityPlane = {{3, 0, particle::FieldComponent::None}, + particle::FieldDType::Bitset, + particle::FieldValueClass::Validity, + 0, + true, + 4, + 1.0f, + 0.0f, + "validity", + frame->validityBits}; + auto storage = std::make_shared>(std::move(raw)); + frame->planes.push_back({key, dtype, valueClass, unitID, true, 4, scale, offset, "plane", storage}); + return frame; +} + +std::vector float32LE(std::initializer_list values) { + std::vector bytes(values.size() * 4); + size_t offset = 0; + for (const float value : values) { + f32(bytes, offset, value); + offset += 4; + } + return bytes; +} + +std::vector uint16LE(std::initializer_list values) { + std::vector bytes(values.size() * 2); + size_t offset = 0; + for (const uint16_t value : values) { + u16(bytes, offset, value); + offset += 2; + } + return bytes; +} + } // namespace TEST(Source, RasterArrayV4HostApiRejectsWithoutPartialMutationAndMapsStoreResults) { @@ -128,8 +225,12 @@ TEST(Source, RasterArrayV4HostApiRejectsWithoutPartialMutationAndMapsStoreResult EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 2, 2), windContract(), first), style::RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision); submit(source, "wind", "wind", Base + Hour, 3); - source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, "wind", - {1, 1, particle::FieldComponent::UEast}, {2, 1, particle::FieldComponent::VNorth}, {}, 1}); + source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, + "wind", + {1, 1, particle::FieldComponent::UEast}, + {2, 1, particle::FieldComponent::VNorth}, + {}, + 1}); ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2)); const auto baseline = source.resolveParticleProjection(); ASSERT_TRUE(baseline.accepted()); @@ -143,7 +244,8 @@ TEST(Source, RasterArrayV4HostApiRejectsWithoutPartialMutationAndMapsStoreResult EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 1), windContract(), first), style::RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision); expectNoPartialMutation(); - auto corrupt = first; corrupt[0] = 'X'; + auto corrupt = first; + corrupt[0] = 'X'; EXPECT_EQ(source.submitEncodedField("wind", metadata("wind", 3), windContract(), corrupt), style::RasterArrayEncodedFieldSubmitResult::RejectedDecode); expectNoPartialMutation(); @@ -161,8 +263,12 @@ TEST(Source, RasterArrayV4HostApiRejectsKeepPublicProjectionStable) { const auto t1 = qbin(Base + Hour); submit(source, "wind", "wind", Base, 2); submit(source, "wind", "wind", Base + Hour, 3); - source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, "wind", - {1, 1, particle::FieldComponent::UEast}, {2, 1, particle::FieldComponent::VNorth}, {}, 1}); + source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, + "wind", + {1, 1, particle::FieldComponent::UEast}, + {2, 1, particle::FieldComponent::VNorth}, + {}, + 1}); ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2)); const auto baseline = source.resolveParticleProjection(); ASSERT_TRUE(baseline.accepted()); @@ -190,27 +296,40 @@ TEST(Source, RasterArrayV4HostApiRejectsKeepPublicProjectionStable) { TEST(Source, RasterArrayV4HostApiResolvesIndependentBindingsFromOneForecastClock) { style::RasterArraySource source{"v4"}; - submit(source, "wind", "wind", Base, 1); submit(source, "wind", "wind", Base + Hour, 2); + submit(source, "wind", "wind", Base, 1); + submit(source, "wind", "wind", Base + Hour, 2); submit(source, "wind", "wind", Base + 2 * Hour, 3); - submit(source, "wave", "wave", Base, 1, true); submit(source, "wave", "wave", Base + 3 * Hour, 2, true); + submit(source, "wave", "wave", Base, 1, true); + submit(source, "wave", "wave", Base + 3 * Hour, 2, true); submit(source, "wave", "wave", Base + 6 * Hour, 3, true); source.setColorBinding({"wave", {99, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 77}); - source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, "wind", - {1, 1, particle::FieldComponent::UEast}, {2, 1, particle::FieldComponent::VNorth}, {}, 1}); + source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, + "wind", + {1, 1, particle::FieldComponent::UEast}, + {2, 1, particle::FieldComponent::VNorth}, + {}, + 1}); ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour + Hour / 2)); - const auto color = source.resolveColorProjection(); const auto particles = source.resolveParticleProjection(); - ASSERT_TRUE(color.accepted()); ASSERT_TRUE(particles.accepted()); + const auto color = source.resolveColorProjection(); + const auto particles = source.resolveParticleProjection(); + ASSERT_TRUE(color.accepted()); + ASSERT_TRUE(particles.accepted()); EXPECT_FLOAT_EQ(color.projection->sampleState.mix, 0.5f); EXPECT_FLOAT_EQ(particles.projection->sampleState.mix, 0.5f); - EXPECT_NE(color.projection->contentIdentity.t1Coordinate.validTimeMs, particles.projection->contentIdentity.t1Coordinate.validTimeMs); + EXPECT_NE(color.projection->contentIdentity.t1Coordinate.validTimeMs, + particles.projection->contentIdentity.t1Coordinate.validTimeMs); const auto identity = particles.projection->contentIdentity; source.setForecastPlaybackState(particle::ForecastPlaybackState::Playing); - ASSERT_TRUE(source.advanceForecastClock(10)); const auto advanced = source.resolveParticleProjection(); - ASSERT_TRUE(advanced.accepted()); EXPECT_NE(advanced.projection->sampleState.mix, particles.projection->sampleState.mix); + ASSERT_TRUE(source.advanceForecastClock(10)); + const auto advanced = source.resolveParticleProjection(); + ASSERT_TRUE(advanced.accepted()); + EXPECT_NE(advanced.projection->sampleState.mix, particles.projection->sampleState.mix); EXPECT_EQ(advanced.projection->contentIdentity, identity); source.setForecastPlaybackState(particle::ForecastPlaybackState::Paused); - EXPECT_FALSE(source.advanceForecastClock(10)); EXPECT_TRUE(source.setForecastPlaybackSpeed(0.5f)); - EXPECT_TRUE(source.setForecastPlaybackSpeed(1.0f)); EXPECT_TRUE(source.setForecastPlaybackSpeed(2.0f)); + EXPECT_FALSE(source.advanceForecastClock(10)); + EXPECT_TRUE(source.setForecastPlaybackSpeed(0.5f)); + EXPECT_TRUE(source.setForecastPlaybackSpeed(1.0f)); + EXPECT_TRUE(source.setForecastPlaybackSpeed(2.0f)); } TEST(Source, RasterArrayV4HostApiClearRetainsBindingsAndNeverTouchesV3) { @@ -237,23 +356,170 @@ TEST(Source, RasterArrayV4HostApiClearRetainsBindingsAndNeverTouchesV3) { EXPECT_EQ(source.impl().nextField->generation, v3NextGeneration); EXPECT_EQ(source.impl().nextField->validTime, v3NextValidTime); }; - submit(source, "wind", "wind", Base, 1); submit(source, "wind", "wind", Base + Hour, 2); - submit(source, "wave", "wave", Base, 1, true); submit(source, "wave", "wave", Base + 3 * Hour, 2, true); + submit(source, "wind", "wind", Base, 1); + submit(source, "wind", "wind", Base + Hour, 2); + submit(source, "wave", "wave", Base, 1, true); + submit(source, "wave", "wave", Base + 3 * Hour, 2, true); source.setColorBinding({"wave", {99, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 77}); - source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, "wind", - {1, 1, particle::FieldComponent::UEast}, {2, 1, particle::FieldComponent::VNorth}, {}, 1}); - ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour)); ASSERT_TRUE(source.resolveColorProjection().accepted()); - EXPECT_TRUE(source.clearEncodedField("wave")); EXPECT_TRUE(source.resolveParticleProjection().accepted()); + source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, + "wind", + {1, 1, particle::FieldComponent::UEast}, + {2, 1, particle::FieldComponent::VNorth}, + {}, + 1}); + ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour)); + ASSERT_TRUE(source.resolveColorProjection().accepted()); + EXPECT_TRUE(source.clearEncodedField("wave")); + EXPECT_TRUE(source.resolveParticleProjection().accepted()); EXPECT_EQ(source.resolveColorProjection().error, particle::BindingProjectionError::FieldSeriesMissing); expectV3Unchanged(); - submit(source, "wave", "wave", Base, 1, true); submit(source, "wave", "wave", Base + 3 * Hour, 2, true); - EXPECT_TRUE(source.resolveColorProjection().accepted()); source.clearAllEncodedFields(); + submit(source, "wave", "wave", Base, 1, true); + submit(source, "wave", "wave", Base + 3 * Hour, 2, true); + EXPECT_TRUE(source.resolveColorProjection().accepted()); + source.clearAllEncodedFields(); EXPECT_EQ(source.resolveColorProjection().error, particle::BindingProjectionError::FieldSeriesMissing); EXPECT_EQ(source.resolveParticleProjection().error, particle::BindingProjectionError::FieldSeriesMissing); expectV3Unchanged(); - submit(source, "wind", "wind", Base, 1); submit(source, "wind", "wind", Base + Hour, 2); - submit(source, "wave", "wave", Base, 1, true); submit(source, "wave", "wave", Base + 3 * Hour, 2, true); + submit(source, "wind", "wind", Base, 1); + submit(source, "wind", "wind", Base + Hour, 2); + submit(source, "wave", "wave", Base, 1, true); + submit(source, "wave", "wave", Base + 3 * Hour, 2, true); EXPECT_TRUE(source.resolveColorProjection().accepted()); EXPECT_TRUE(source.resolveParticleProjection().accepted()); expectV3Unchanged(); } + +TEST(Source, RasterArrayV4RendererStateMaterializesBindingsAndMixWithoutTouchingV3) { + style::RasterArraySource source{"v4-render"}; + submit(source, "wind", "wind", Base, 1); + submit(source, "wind", "wind", Base + Hour, 2); + submit(source, "wave", "wave", Base, 1, true); + submit(source, "wave", "wave", Base + Hour, 2, true); + source.setParticleBinding({particle::ParticleBindingMode::VectorComponents, + "wind", + {1, 1, particle::FieldComponent::UEast}, + {2, 1, particle::FieldComponent::VNorth}, + {}, + 1}); + source.setColorBinding({"wave", {99, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 77}); + ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2)); + ASSERT_TRUE(source.impl().v4RendererState.particle); + ASSERT_TRUE(source.impl().v4RendererState.color); + const auto& particles = *source.impl().v4RendererState.particle; + const auto& color = *source.impl().v4RendererState.color; + EXPECT_FLOAT_EQ(particles.sampleState.mix, 0.5f); + EXPECT_FLOAT_EQ(color.sampleState.mix, 0.5f); + EXPECT_EQ(particles.t0.vector, (std::vector{1, 5, 2, 6, 3, 7, 4, 8})); + EXPECT_EQ(particles.t0.valid, (std::vector{1, 1, 1, 1})); + EXPECT_EQ(color.t0.scalar, (std::vector{1, 2, 3, 4})); + const auto particleIdentity = particles.contentIdentity; + ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 4)); + ASSERT_TRUE(source.impl().v4RendererState.particle); + EXPECT_EQ(source.impl().v4RendererState.particle->contentIdentity, particleIdentity); + EXPECT_FLOAT_EQ(source.impl().v4RendererState.particle->sampleState.mix, 0.25f); + EXPECT_FALSE(source.impl().field); + EXPECT_FALSE(source.impl().nextField); + EXPECT_FALSE(source.impl().timeline); + source.clearAllEncodedFields(); + EXPECT_FALSE(source.impl().v4RendererState.particle); + EXPECT_FALSE(source.impl().v4RendererState.color); +} +TEST(Source, RasterArrayV4MaterializerUsesExplicitLittleEndianScaleOffsetAndStableRevision) { + const auto t0 = materializerFrame("scalar", + Base, + 17, + 23, + {99, 0, particle::FieldComponent::None}, + particle::FieldDType::Float32, + particle::FieldValueClass::Scalar, + 77, + 2.0f, + 1.0f, + float32LE({1.5f, -2.0f, 3.25f, 4.0f}), + 0x0d); + const auto t1 = materializerFrame("scalar", + Base + Hour, + 18, + 24, + {99, 0, particle::FieldComponent::None}, + particle::FieldDType::Float32, + particle::FieldValueClass::Scalar, + 77, + 2.0f, + 1.0f, + float32LE({2.0f, -1.5f, 3.5f, 5.0f}), + 0x0f); + particle::ColorBinding binding{ + "scalar", {99, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 77}; + particle::ColorProjection projection; + projection.contentIdentity = + {binding, {"scalar", Base}, {"scalar", Base + Hour}, "plane-t0", "plane-t1", "validity-t0", "validity-t1"}; + projection.sampleState = {Base + Hour / 2, 0.5f}; + projection.t0Frame = t0; + projection.t1Frame = t1; + const auto materialized = particle::materializeColorProjection(projection); + ASSERT_TRUE(materialized); + EXPECT_EQ(materialized->t0.scalar, (std::vector{4.0f, -3.0f, 7.5f, 9.0f})); + EXPECT_EQ(materialized->t0.valid, (std::vector{1, 0, 1, 1})); + EXPECT_EQ(materialized->t0.generation, 17u); + EXPECT_EQ(materialized->t0.dataVersion, 23u); + EXPECT_EQ(materialized->t0.fieldSha256, "plane-t0"); + EXPECT_EQ(materialized->t1.generation, 18u); + EXPECT_EQ(materialized->t1.dataVersion, 24u); +} + +TEST(Source, RasterArrayV4MaterializerDecodesDirectionToUInt16LittleEndian) { + const auto t0 = materializerFrame("direction", + Base, + 31, + 41, + {10, 1, particle::FieldComponent::None}, + particle::FieldDType::UInt16, + particle::FieldValueClass::DirectionAngle, + particle::FieldUnitDegrees, + 0.01f, + 0.0f, + uint16LE({0, 9000, 18000, 27000})); + const auto t1 = materializerFrame("direction", + Base + Hour, + 32, + 42, + {10, 1, particle::FieldComponent::None}, + particle::FieldDType::UInt16, + particle::FieldValueClass::DirectionAngle, + particle::FieldUnitDegrees, + 0.01f, + 0.0f, + uint16LE({0, 9000, 18000, 27000})); + particle::ParticleBinding binding{particle::ParticleBindingMode::DirectionAngle, + "direction", + {}, + {}, + {10, 1, particle::FieldComponent::None}, + particle::FieldUnitDegrees}; + particle::ParticleProjection projection; + projection.contentIdentity = {binding, + {"direction", Base}, + {"direction", Base + Hour}, + "dir-t0", + "dir-t1", + {}, + {}, + "validity-t0", + "validity-t1"}; + projection.sampleState = {Base + Hour / 2, 0.5f}; + projection.t0Frame = t0; + projection.t1Frame = t1; + const auto materialized = particle::materializeParticleProjection(projection); + ASSERT_TRUE(materialized); + ASSERT_EQ(materialized->t0.vector.size(), 8u); + EXPECT_NEAR(materialized->t0.vector[0], 0.0f, 1e-5f); + EXPECT_NEAR(materialized->t0.vector[1], 1.0f, 1e-5f); + EXPECT_NEAR(materialized->t0.vector[2], 1.0f, 1e-5f); + EXPECT_NEAR(materialized->t0.vector[3], 0.0f, 1e-5f); + EXPECT_NEAR(materialized->t0.vector[4], 0.0f, 1e-5f); + EXPECT_NEAR(materialized->t0.vector[5], -1.0f, 1e-5f); + EXPECT_NEAR(materialized->t0.vector[6], -1.0f, 1e-5f); + EXPECT_NEAR(materialized->t0.vector[7], 0.0f, 1e-5f); + EXPECT_EQ(materialized->t0.vectorSemantics, particle::VectorSemantics::Direction); +}