feat: connect QBIN V4 renderer and release particle.5
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -215,6 +215,7 @@ list(APPEND SRC_FILES
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/field_data_adapter.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/field_runtime.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/particle/field/field_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
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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`。
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1 +1 @@
|
||||
13.4.1-particle.3
|
||||
13.4.1-particle.5
|
||||
|
||||
164
src/mbgl/particle/field/field_projection_materialization.cpp
Normal file
164
src/mbgl/particle/field/field_projection_materialization.cpp
Normal 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
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user