feat: connect QBIN V4 renderer and release particle.5
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-15 14:40:17 +08:00
parent 3896c461fa
commit f23f4143b9
22 changed files with 1011 additions and 248 deletions

View File

@@ -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

View File

@@ -0,0 +1,29 @@
#pragma once
#include <mbgl/particle/field/field_binding.hpp>
#include <mbgl/particle/field/field_data_adapter.hpp>
#include <optional>
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<MaterializedColorProjection> materializeColorProjection(const ColorProjection&);
std::optional<MaterializedParticleProjection> materializeParticleProjection(const ParticleProjection&);
} // namespace mbgl::particle

View File

@@ -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<const particle::ParticleFieldSnapshot> 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;

View File

@@ -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;
});
};

View File

@@ -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。

View File

@@ -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`。

View File

@@ -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")
}

View File

@@ -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;
});
};

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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,

View File

@@ -1 +1 @@
13.4.1-particle.3
13.4.1-particle.5

View File

@@ -0,0 +1,164 @@
#include <mbgl/particle/field/field_projection_materialization.hpp>
#include <cmath>
#include <cstring>
#include <limits>
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<uint16_t>(data[0]) | (static_cast<uint16_t>(data[1]) << 8);
}
uint32_t readU32LE(const uint8_t* data) {
return static_cast<uint32_t>(data[0]) | (static_cast<uint32_t>(data[1]) << 8) |
(static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(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<size_t>(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<int16_t>(readU16LE(data));
break;
case FieldDType::UInt16:
raw = readU16LE(data);
break;
case FieldDType::UInt8:
raw = *data;
break;
case FieldDType::Int8:
raw = static_cast<int8_t>(*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<uint8_t>& output) {
const auto cells = static_cast<size_t>(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<size_t>(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<size_t>(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<MaterializedColorProjection> 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<MaterializedParticleProjection> 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

View File

@@ -101,12 +101,27 @@ ParticleSettings particleSettings(const Immutable<style::Layer::Impl>& 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<const style::ParticleValidationLayer::Impl&>(*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<PremultipliedImage> makeVelocityFieldImage(const particle::Parti
for (uint32_t x = 0; x < field.width; ++x) {
const auto cell = (static_cast<size_t>(y) * field.width + x) * 2;
const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels;
image->data[pixel + 0] = static_cast<uint8_t>(std::clamp(field.vector[cell] / scale * 0.5f + 0.5f, 0.0f, 1.0f) *
255.0f);
image->data[pixel + 0] = static_cast<uint8_t>(
std::clamp(field.vector[cell] / scale * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f);
image->data[pixel + 1] = static_cast<uint8_t>(
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<float>(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<ParticleGpuValidationTweaker>(
resources,
builder->addTweaker(std::make_shared<ParticleGpuValidationTweaker>(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<const style::ParticleRenderLayerProperties&>(*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<const RenderRasterArraySource&>(*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&>(*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&>(*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<RenderTargetPtr, 2> 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<int16_t>(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,

View File

@@ -36,6 +36,8 @@ private:
std::optional<particle::ParticleFieldSnapshot> sourceField;
std::optional<particle::ParticleFieldSnapshot> sourceNextField;
std::shared_ptr<style::RasterArrayTimelineRuntime> sourceTimeline;
float sourceProjectionMix = 0.0f;
bool sourceUsesV4Projection = false;
uint64_t sourceTimelineWindowRevision = 0;
uint64_t sourceTimelineCadenceRevision = 0;
bool formalSource = false;

View File

@@ -31,8 +31,10 @@ double mercatorY(const double latitude) {
}
std::array<float, 4> bounds(const particle::ParticleFieldSnapshot& field) {
return {static_cast<float>((field.west + 180.0) / 360.0), static_cast<float>(mercatorY(field.north)),
static_cast<float>((field.east + 180.0) / 360.0), static_cast<float>(mercatorY(field.south))};
return {static_cast<float>((field.west + 180.0) / 360.0),
static_cast<float>(mercatorY(field.north)),
static_cast<float>((field.east + 180.0) / 360.0),
static_cast<float>(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<PremultipliedImage> image(const particle::ParticleFieldSnapshot&
const float fieldScale,
style::FieldValueSource valueSource) {
auto result = std::make_shared<PremultipliedImage>(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<size_t>(y) * field.width + x;
const auto pixel = (static_cast<size_t>(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<uint8_t>(std::clamp(first / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
result->data[pixel + 1] = static_cast<uint8_t>(std::clamp(second / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
result->data[pixel + 1] = static_cast<uint8_t>(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> resources_, float mix_, float opacity_, int16_t wrap_,
std::vector<style::WeatherColorStop> 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> resources_,
float mix_,
float opacity_,
int16_t wrap_,
std::vector<style::WeatherColorStop> 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> 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<PremultipliedImage>& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) {
bool uploadField(gfx::Context& context,
const particle::ParticleFieldSnapshot& field,
style::FieldValueSource valueSource,
std::shared_ptr<PremultipliedImage>& 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<const RenderRasterArraySource&>(*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<UpdateParameters>&, 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<UpdateParameters>&,
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&>(*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<const style::WeatherColorLayer::Impl&>(*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<const style::WeatherColorLayer::Impl&>(*baseImpl).valueSource;
if (!supportsValueSource(*sourceField, valueSource) || !supportsValueSource(*sourceNextField, valueSource)) {
if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables();
resources.reset();
@@ -209,52 +266,108 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context&
}
auto state = std::static_pointer_cast<Resources>(resources);
if (!state) {
state = std::make_shared<Resources>(); state->shader = context.getGenericShader(shaders, ShaderName);
state->vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>(RenderStaticData::heatmapTextureVertices());
state = std::make_shared<Resources>();
state->shader = context.getGenericShader(shaders, ShaderName);
state->vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>(
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<PremultipliedImage>& 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<PremultipliedImage>& 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<PremultipliedImage> i0, i1; gfx::Texture2DPtr x0, x1; float s0 = 1, s1 = 1;
particle::ParticleFieldSnapshot t0, t1;
std::shared_ptr<PremultipliedImage> 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<const style::WeatherColorLayer::Impl&>(*baseImpl).opacity;
auto& group = static_cast<LayerGroup&>(*layerGroup); group.clearDrawables();
auto& group = static_cast<LayerGroup&>(*layerGroup);
group.clearDrawables();
const auto center = transformState.getLatLng(LatLng::Unwrapped);
const auto centerWrap = static_cast<int16_t>(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<Tweaker>(state, mix, opacity, wrap,
static_cast<const style::WeatherColorLayer::Impl&>(*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<Tweaker>(state,
mix,
opacity,
wrap,
static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).colorStops,
valueSource));
builder->flush(context);
auto drawables = builder->clearDrawables();
if (drawables.size() == 1 && drawables.front()) group.addDrawable(std::move(drawables.front()));
}
}
} // namespace mbgl

View File

@@ -9,8 +9,12 @@ class RenderWeatherColorLayer final : public RenderLayer {
public:
explicit RenderWeatherColorLayer(Immutable<style::Layer::Impl>);
~RenderWeatherColorLayer() override;
void update(gfx::ShaderRegistry&, gfx::Context&, const TransformState&, const std::shared_ptr<UpdateParameters>&,
const RenderTree&, UniqueChangeRequestVec&) override;
void update(gfx::ShaderRegistry&,
gfx::Context&,
const TransformState&,
const std::shared_ptr<UpdateParameters>&,
const RenderTree&,
UniqueChangeRequestVec&) override;
bool needsHiddenLayerSourceCleanup(const RenderSource&) const override;
private:
@@ -28,6 +32,8 @@ private:
std::optional<particle::ParticleFieldSnapshot> sourceField;
std::optional<particle::ParticleFieldSnapshot> sourceNextField;
std::shared_ptr<style::RasterArrayTimelineRuntime> sourceTimeline;
float sourceProjectionMix = 0.0f;
bool sourceUsesV4Projection = false;
};
} // namespace mbgl

View File

@@ -26,7 +26,10 @@ class RenderRasterArraySource final : public RenderSource {
public:
explicit RenderRasterArraySource(Immutable<style::RasterArraySource::Impl> 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<style::Source::Impl> value,
const std::vector<Immutable<style::LayerProperties>>&,
bool needsRendering,

View File

@@ -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 <class Projection>
bool sameProjection(const std::optional<Projection>& a, const std::optional<Projection>& 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<uint8_t>(targetFps_)) +
Log::Info(Event::Render,
"WeatherCadence: targetFps=" + std::to_string(static_cast<uint8_t>(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<std::mutex> 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>(impl());
updated->v4RendererState = std::move(next);
baseImpl = std::move(updated);
loaded = true;
observer->onSourceChanged(*this);
}
const RasterArraySource::Impl& RasterArraySource::impl() const {
return static_cast<const Impl&>(*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<char>(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<RasterArrayTimelineRuntime::Entry> entries;
entries.reserve(localFields.size());
for (const auto& candidate : localFields) {
entries.push_back({std::make_shared<particle::ParticleFieldSnapshot>(*candidate.snapshot), candidate.snapshot->validTime});
entries.push_back({std::make_shared<particle::ParticleFieldSnapshot>(*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<std::vector<char>>& 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<const particle::ParticleFieldSnapshot> 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);

View File

@@ -1,6 +1,7 @@
#pragma once
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
#include <mbgl/particle/field/field_projection_materialization.hpp>
#include <mbgl/style/sources/raster_array_source.hpp>
#include <mbgl/style/source_impl.hpp>
@@ -8,6 +9,10 @@ namespace mbgl::style {
class RasterArraySource::Impl final : public Source::Impl {
public:
struct V4RendererState {
std::optional<particle::MaterializedColorProjection> color;
std::optional<particle::MaterializedParticleProjection> 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<RasterArrayLocalFieldDescriptor> 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<RasterArrayLocalFieldDescriptor> localFields;
bool hostFieldSubmission = false;
std::shared_ptr<RasterArrayTimelineRuntime> timeline;
// Immutable renderer-facing V4 state. It is intentionally separate from
// the legacy V3 timeline/field pair.
V4RendererState v4RendererState;
std::optional<particle::ParticleFieldSnapshot> field;
std::optional<particle::ParticleFieldSnapshot> nextField;
};

View File

@@ -1,4 +1,5 @@
#include <mbgl/particle/ingress/qbin_v2_decoder.hpp>
#include <mbgl/particle/field/field_projection_materialization.hpp>
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
#include <mbgl/style/sources/raster_array_source.hpp>
#include <mbgl/style/sources/raster_array_source_impl.hpp>
@@ -19,7 +20,8 @@ constexpr uint64_t Base = 1'700'000'000'000ULL;
constexpr uint64_t Hour = 3'600'000ULL;
void u16(std::vector<uint8_t>& 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<uint8_t>& 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<uint8_t>& bytes, size_t offset, uint32_t value) {
void u64(std::vector<uint8_t>& 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<uint8_t>& bytes, size_t offset, int64_t value) { u64(bytes, offset, static_cast<uint64_t>(value)); }
void i64(std::vector<uint8_t>& bytes, size_t offset, int64_t value) {
u64(bytes, offset, static_cast<uint64_t>(value));
}
void f32(std::vector<uint8_t>& 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<uint8_t>& 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<uint8_t>& 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<uint8_t> qbin(uint64_t validTime, bool scalar = false) {
std::vector<Plane> 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<uint8_t> qbin(uint64_t validTime, bool scalar = false) {
size_t payloadLength = 0;
for (const auto& plane : planes) payloadLength = aligned16(payloadLength + plane.bytes.size());
std::vector<uint8_t> 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<int64_t>(validTime / 1000)); i64(bytes, 32, static_cast<int64_t>(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<int64_t>(validTime / 1000));
i64(bytes, 32, static_cast<int64_t>(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<const particle::DecodedFieldFrame> 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<uint8_t> raw,
uint8_t validity = 0x0f) {
auto frame = std::make_shared<particle::DecodedFieldFrame>();
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<const std::vector<uint8_t>>(std::vector<uint8_t>{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<const std::vector<uint8_t>>(std::move(raw));
frame->planes.push_back({key, dtype, valueClass, unitID, true, 4, scale, offset, "plane", storage});
return frame;
}
std::vector<uint8_t> float32LE(std::initializer_list<float> values) {
std::vector<uint8_t> bytes(values.size() * 4);
size_t offset = 0;
for (const float value : values) {
f32(bytes, offset, value);
offset += 4;
}
return bytes;
}
std::vector<uint8_t> uint16LE(std::initializer_list<uint16_t> values) {
std::vector<uint8_t> 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<float>{1, 5, 2, 6, 3, 7, 4, 8}));
EXPECT_EQ(particles.t0.valid, (std::vector<uint8_t>{1, 1, 1, 1}));
EXPECT_EQ(color.t0.scalar, (std::vector<float>{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<float>{4.0f, -3.0f, 7.5f, 9.0f}));
EXPECT_EQ(materialized->t0.valid, (std::vector<uint8_t>{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);
}