feat: add host particle field submission contract
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:
@@ -0,0 +1,109 @@
|
||||
# STEP-053 Windy-style Canonical Field 与 Host→MapLibre Submission Contract
|
||||
|
||||
- 状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`
|
||||
- Starting HEAD:`285d23a58f6ecb16f360748df688d9d03dad6b3d`
|
||||
- 分支:`particle/main`
|
||||
- 目标:让已经取得并规范化规则二维 U/V Field 的外部 Host,以低频 `metadata + contiguous binary payload` 提交给 MapLibre Native,并复用 STEP-049/051 的 Adapter、Snapshot、Timeline、Runtime Window 与 Renderer。
|
||||
|
||||
## 设计结果
|
||||
|
||||
正式入口为:
|
||||
|
||||
`Host metadata + binary UV bytes → RasterArraySource::submitHostField() → LocalParticleFieldFeed → ParticleFieldDataAdapter → ParticleFieldSnapshot → existing initial window / Timeline → RasterParticleLayer`
|
||||
|
||||
没有新增第二套 Descriptor、Snapshot、Timeline、Runtime Window 或 Field normalizer。`ParticleFieldDescriptor`、`ParticleFieldDataAdapter`、`ParticleFieldSnapshot` 继续是唯一 canonical 数据契约;`LocalParticleFieldFeed` 继续只负责把 `maplibre-particle-field/v1` metadata 映射到 Descriptor 并调用正式 Adapter。
|
||||
|
||||
新增 `raster-array` style validation seam:
|
||||
|
||||
```json
|
||||
{"type":"raster-array","host-field-submission":true}
|
||||
```
|
||||
|
||||
该 Source 不读取 URL、asset 或网络。Host 通过 Android `MapLibreMap.submitRasterArraySourceHostField(sourceId, metadata, byte[])` 或 Core `RasterArraySource::submitHostField(metadata, payload)` 一次提交一个独立时间片。
|
||||
|
||||
## Canonical Field Contract
|
||||
|
||||
第一版 canonical contract 固定为:
|
||||
|
||||
- 规则二维 Grid,`cell = y * width + x`,不为每个 cell 重复携带 lat/lon。
|
||||
- grid origin 为 north-west;row 为 north-to-south;column 为 west-to-east。
|
||||
- payload 为 contiguous `float32-le`、`interleaved-uv`;每 cell 8 bytes。
|
||||
- `velocity[2 * cell] = U`,U 为 eastward-positive。
|
||||
- `velocity[2 * cell + 1] = V`,V 为 northward-positive。
|
||||
- bounds、width/height、stride/noData、expected byte length 继续由既有 Descriptor 表达。
|
||||
- `fieldId`、`generation`、`validTime`、`dataVersion` 均由 Host/producer 在进入 Adapter 前拥有并稳定赋值;MapLibre 不猜默认值。
|
||||
- 同一提交序列的 `validTime` 使用同一时间基准并严格递增;Timeline 按毫秒差值推进。
|
||||
- payload 不使用巨大 JSON、字符串 float list 或 `Array<number>`;Android 当前直接使用 `byte[]`,为 STEP-054 的 ArrayBuffer/TypedArray/native buffer/file reference 保留二进制边界。
|
||||
|
||||
`ParticleFieldDescriptor.fieldSha256` 仍是可选字段,但当前 `maplibre-particle-field/v1` Host metadata 未启用 checksum 校验;本阶段没有伪造 checksum mismatch PASS。若后续正式启用,必须在 Adapter ingress 增加明确校验和负向测试。
|
||||
|
||||
## Submission Routing
|
||||
|
||||
- 第一个合法 Snapshot:暂存为 pending T0,返回 `ACCEPTED_PENDING_INITIAL_WINDOW`。
|
||||
- 第二个合法 Snapshot:调用既有 `submitInitialRuntimeFieldWindow(T0,T1)`,建立唯一 authoritative initial window 和 Core-owned Timeline,返回 `INITIAL_WINDOW_READY`。
|
||||
- 后续合法 Snapshot:受 Timeline 既有 capacity=8 约束,通过线程安全 append 加入同一个 Timeline,返回 `ACCEPTED_NEXT_SNAPSHOT`。
|
||||
- Duplicate:按既有 canonical GPU content identity 返回 `DUPLICATE_NOOP`,不追加、不通知 Renderer、不上传 Field。
|
||||
- Invalid:Adapter、compatibility、generation/validTime monotonicity 或 capacity 失败均结构化拒绝;pending/Timeline/最后有效 Field 不变。
|
||||
|
||||
Timeline append 仅扩展低频 Snapshot arrival;cursor、speed、pause、gap/invalid、playback clock 和每帧 tick 状态机均未改。已结束的尾窗口收到后续 Snapshot 时回到 Paused tail,可由 Host 的低频 PLAY 命令继续,不从 T0 重建第二套 Timeline。
|
||||
|
||||
## STEP-051 兼容与冻结边界
|
||||
|
||||
- 原 `production-fields` asset 路径、fixture 路径和 Validation 入口保留。
|
||||
- `OLD_T1 → NEW_T0` reuse、only NEW_T1 upload、Duplicate NOOP、Invalid fail-closed、MIX property-only 保持。
|
||||
- 正常 boundary 的 `simulationRebuild=false`、`trailCleared=false` 语义未改。
|
||||
- Timeline/Playback 继续 Core-owned;Android 不持有 cursor/window,不做 Timer/Handler/Coroutine per-frame JNI。
|
||||
- `RasterParticleLayer` 未加入 provider、network、vendor、manifest、下载、缓存或解析逻辑。
|
||||
- `WeatherColorLayer` 与 `RasterParticleLayer` 继续分离。
|
||||
|
||||
## Android Validation Host
|
||||
|
||||
MORE 菜单新增:
|
||||
|
||||
- `HOST FIELD SUBMIT`:Validation Host 自己读取 STEP-051 的 T0~T3 metadata 和 canonical binary bytes,然后逐片调用 Host submission API。数据顺序为 uniform east、uniform north、vortex、uniform east,可观察 U/V 和时间变化。
|
||||
- `HOST DUPLICATE`:重新提交当前最后一个 Host Field,预期 `DUPLICATE_NOOP`。
|
||||
- `INVALID HOST`:提交既有 `field-invalid.json`,预期 Adapter reject 且保持最后有效 Field。
|
||||
|
||||
旧 `PRODUCTION FEED`、`INVALID PROD`、fixture、TIME、VIS、COUNT、MAP 等入口不删除。Validation Host 只模拟未来 React Native Host 已经持有数据,不 fetch 网络,不实现 Timeline。
|
||||
|
||||
## 实际修改文件
|
||||
|
||||
- `include/mbgl/particle/field/field_data_adapter.hpp`
|
||||
- `include/mbgl/style/sources/raster_array_source.hpp`
|
||||
- `src/mbgl/style/sources/raster_array_source.cpp`
|
||||
- `src/mbgl/style/sources/raster_array_source_impl.hpp`
|
||||
- `src/mbgl/style/conversion/source.cpp`
|
||||
- `test/style/source.test.cpp`
|
||||
- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java`
|
||||
- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java`
|
||||
- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java`
|
||||
- `platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp`
|
||||
- `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp`
|
||||
- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt`
|
||||
- `platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_particle_core_layer_validation.xml`
|
||||
- 本 Step、`粒子开发记录.md` 与中文完成报告。
|
||||
|
||||
没有新增或修改 canonical dataset asset;用户正式测试数据尚未提供,本阶段按任务书复用 STEP-051 T0~T3 binary validation dataset。
|
||||
|
||||
## Linux/Core/OpenGL 验证
|
||||
|
||||
- `mbgl-test-runner` 精确构建:PASS。
|
||||
- `mbgl-render` 精确构建:PASS。
|
||||
- `mbgl-render-test-runner` 精确构建:PASS。
|
||||
- 新增 Host/canonical 定向测试:4/4 PASS。
|
||||
- `Source.*` 回归:44/44 PASS。
|
||||
- `Layer.RasterParticleCountProperties`、`Layer.RasterParticleVisualProperties`:2/2 PASS。
|
||||
- JNI Java/interface/native declaration/registration 与 Validation resource ID 静态核对:PASS。
|
||||
- `xmllint`:环境未安装,未执行;不伪报 PASS。
|
||||
- Android Gradle/APK/ADB/App/设备:未执行。
|
||||
- Jest:未执行。
|
||||
|
||||
## Android 待验收
|
||||
|
||||
用户需在 Windows 11 对设备 `192.168.1.146` 完成一次 OpenGL APK 综合验收:Host T0~T3 submit、Timeline READY/PLAY、east/north 方向、MIX、至少两次 boundary、reuse/new-T1-only、Duplicate、Invalid fail-closed、Renderer continuity、无黑屏、无 crash。用户正式数据若后续提供,仍需用同一 contract 单独完成视觉验收。
|
||||
|
||||
## 非目标与技术债
|
||||
|
||||
未实现 Weather Service、HTTP/provider、React Native binding、Manifest/Catalog、下载/重试/cache、GRIB/NetCDF/vendor decoder、WeatherColorLayer、第二套状态机。Validation UI MIX/WAITING/INVALID observability 继续为 NON-BLOCKING;Rotation/Viewport 技术债未处理。
|
||||
|
||||
当前状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。
|
||||
@@ -0,0 +1,137 @@
|
||||
# STEP-053 Windy-style Field 与 Host Submission Codex 完成报告
|
||||
|
||||
## 1. 基线与状态
|
||||
|
||||
- Starting HEAD:`285d23a58f6ecb16f360748df688d9d03dad6b3d`
|
||||
- 分支:`particle/main`
|
||||
- STEP-051:`CLOSED / PASS`
|
||||
- STEP-052:调查完成;STEP-053 按任务书修正了 Weather/Manifest 所有权,MapLibre 仅从 Host 已规范化 Field 开始负责。
|
||||
- 当前状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`
|
||||
|
||||
## 2. 最终实际设计
|
||||
|
||||
新增唯一低频入口 `RasterArraySource::submitHostField(metadataText, payload)`。Host 每次提交一个 `maplibre-particle-field/v1` metadata 和一段 contiguous binary U/V payload。入口复用 `LocalParticleFieldFeed`、`ParticleFieldDataAdapter` 和 immutable `ParticleFieldSnapshot`,再路由到既有 initial T0/T1 与同一个 Core-owned Timeline。
|
||||
|
||||
没有新增 Descriptor 或 Snapshot,因为 STEP-049 的结构已经表达 identity、generation、validTime、grid geometry、bounds、layout、encoding、noData、byte length、dataVersion 和可选 checksum。也没有建立第二套 Adapter、Timeline、Window 或 cache。
|
||||
|
||||
Canonical layout 为 north-west origin、row-major、north-to-south rows、west-to-east columns,`float32-le interleaved-uv`:U=eastward-positive,V=northward-positive。大 Field 走二进制 bytes,不走巨大 JSON/JS number array。metadata 可以是小型 JSON;STEP-054 可把 ArrayBuffer/TypedArray/native buffer/file reference 映射到同一 Native bytes boundary。
|
||||
|
||||
## 3. API 与路径
|
||||
|
||||
### Core/C++
|
||||
|
||||
- `RasterArraySource(std::string id)`:创建空 Host ingress Source。
|
||||
- `RasterArraySource::submitHostField(metadata, payload)`:单 Field 正式提交。
|
||||
- `RasterArrayHostFieldSubmitResult`:区分 pending initial、initial ready、next accepted、duplicate,以及 source mode/adapter/incompatible/non-monotonic/capacity rejection。
|
||||
- `RasterArrayTimelineRuntime::append()`:仅追加低频、严格递增、capacity-bound Snapshot;不改变 Playback 状态机所有权。
|
||||
- style validation seam:`{"type":"raster-array","host-field-submission":true}`。
|
||||
|
||||
### Android/Validation
|
||||
|
||||
- `MapLibreMap.submitRasterArraySourceHostField(String, String, byte[])`。
|
||||
- Java `NativeMap`/`NativeMapView` → JNI `NativeMapView` → `RasterArraySource::submitHostField()`。
|
||||
- API 返回与 C++ structured result 对齐的整数常量;JNI 只在一次 Field arrival 时复制 contiguous bytes,不做 per-frame JNI。
|
||||
- MORE 菜单新增 `HOST FIELD SUBMIT`、`HOST DUPLICATE`、`INVALID HOST`。
|
||||
|
||||
### Initial / Next / Duplicate / Invalid
|
||||
|
||||
- Field #0:pending T0。
|
||||
- Field #1:复用 `submitInitialRuntimeFieldWindow()`,建立 T0/T1 和 Timeline READY。
|
||||
- Field #2/#3:append 到同一个 Timeline,继续由既有 renderer window revision 路径消费。
|
||||
- Duplicate:`DUPLICATE_NOOP`,Timeline size 与 active field 不变。
|
||||
- Invalid/unsupported/non-monotonic:结构化拒绝,最后有效 Snapshot/Timeline frame 不变,符合 fail-closed。
|
||||
|
||||
## 4. 架构影响
|
||||
|
||||
- Public/internal API:新增 Core Host submission API、Android MapLibreMap binary submission API、Host Source style validation flag;Timeline 增加内部有界 append/read helpers。
|
||||
- Adapter contract:未放宽、未复制;仍只接受合法 `interleaved-uv/float32-le`。
|
||||
- Runtime Field Window:未重写,`OLD_T1 → NEW_T0` 与 only NEW_T1 upload 不变。
|
||||
- Timeline/Playback:状态机、clock、cursor、speed、pause/gap/invalid ownership 未改变。
|
||||
- Renderer/GPU/Trail:未修改;不含 provider/network/vendor/manifest/cache 业务。
|
||||
- WeatherColorLayer:未修改,继续与 RasterParticleLayer 分离。
|
||||
- Network/Provider/Weather Service/React Native:未实现。
|
||||
|
||||
## 5. 测试与结果
|
||||
|
||||
### 构建
|
||||
|
||||
- `cmake --build build-linux-opengl --target mbgl-test-runner -j3`:PASS。
|
||||
- `cmake --build build-linux-opengl --target mbgl-render mbgl-render-test-runner -j3`:PASS。
|
||||
|
||||
### 定向测试
|
||||
|
||||
- `Source.CanonicalFieldUsesEastwardUAndNorthwardV`
|
||||
- `Source.HostFieldSubmissionBuildsOneCoreOwnedTimeline`
|
||||
- `Source.HostFieldSubmissionStyleContractCreatesEmptyIngress`
|
||||
- `Source.HostFieldSubmissionRejectsInvalidWithoutChangingLastValidField`
|
||||
|
||||
结果:4/4 PASS。覆盖 east U / north V、vortex corner orientation、initial T0/T1、T2/T3、strict validTime/generation、Duplicate、invalid identity/dimensions、wrong byte length、unsupported layout/encoding、non-monotonic 和 last-valid hold。
|
||||
|
||||
- `Source.*`:44/44 PASS,包含既有 Adapter、STEP-051 local feed、Timeline READY/Playback、Runtime Window/reuse/NOOP 回归。
|
||||
- RasterParticle property 回归:2/2 PASS。
|
||||
- JNI signature/registration、Host style property、Validation action resource ID:静态核对 PASS。
|
||||
- `xmllint`:环境未安装,未执行。
|
||||
- checksum mismatch:Host contract 当前未启用 checksum 校验,按任务书条件未伪造测试结果。
|
||||
- `git diff --check`:最终提交前执行。
|
||||
|
||||
最初新增 style conversion 测试使用 `dynamic_cast`,精确编译暴露项目 `-fno-rtti` 约束;已改为项目正式 `Source::as<T>()`,后续构建/测试通过,无遗留阻塞。
|
||||
|
||||
## 6. 未执行项
|
||||
|
||||
- Android Gradle:未执行。
|
||||
- APK build/copy/install:未执行。
|
||||
- ADB/App 启动/设备测试:未执行。
|
||||
- Jest:未新增、未修改、未执行。
|
||||
- 用户正式 canonical dataset:尚未提供,未声称已验证。
|
||||
|
||||
## 7. 用户 Android 手工验收
|
||||
|
||||
1. 在 Windows 11 构建并安装新的 Android OpenGL Debug APK。
|
||||
2. 打开 `Particle Core Layer Validation → MORE → HOST FIELD SUBMIT`。
|
||||
3. 确认日志依次出现 `HOST_FIELD_SUBMIT_REQUEST`、`HOST_FIELD_SNAPSHOT_READY`、`HOST_FIELD_INITIAL_WINDOW_READY`、`HOST_FIELD_NEXT_SUBMIT` 与 `ParticleTimeline: event=READY`。
|
||||
4. 进入 TIME,执行 `PLAY / RESTART`,观察 east/north/vortex 变化、自动 MIX 和至少两个 Window Boundary。
|
||||
5. 确认 `reuse=OLD_T1_TO_NEW_T0`、only NEW_T1 upload、`simulationRebuild=false`、`trailCleared=false`。
|
||||
6. 执行 `MORE → HOST DUPLICATE`,确认 `DUPLICATE_NOOP` 且 upload/reset 不增长。
|
||||
7. 执行 `MORE → INVALID HOST`,确认 Adapter reject、最后有效 Field 保持、Renderer 连续、无黑屏、无 crash。
|
||||
|
||||
日志采集必须显式指定设备,例如:
|
||||
|
||||
```bat
|
||||
adb -s 192.168.1.146 logcat | findstr /I "HOST_FIELD ParticleTimeline WINDOW_BOUNDARY FIELD_REUSE FIELD_UPLOAD DUPLICATE INVALID simulationRebuild trailCleared"
|
||||
```
|
||||
|
||||
## 8. 文件与 Git
|
||||
|
||||
实际代码/测试修改文件:
|
||||
|
||||
- `include/mbgl/particle/field/field_data_adapter.hpp`
|
||||
- `include/mbgl/style/sources/raster_array_source.hpp`
|
||||
- `src/mbgl/style/sources/raster_array_source.cpp`
|
||||
- `src/mbgl/style/sources/raster_array_source_impl.hpp`
|
||||
- `src/mbgl/style/conversion/source.cpp`
|
||||
- `test/style/source.test.cpp`
|
||||
- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMap.java`
|
||||
- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java`
|
||||
- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java`
|
||||
- `platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp`
|
||||
- `platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp`
|
||||
- `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt`
|
||||
- `platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_particle_core_layer_validation.xml`
|
||||
|
||||
文档修改:本 Step、`粒子开发记录.md` 和本报告。
|
||||
|
||||
- commit hash:待 Git 收尾后回填。
|
||||
- commit message:`feat: add host particle field submission contract`
|
||||
- push result:待执行。
|
||||
- remote branch:`origin/particle/main`
|
||||
- Final HEAD:待 push 后回填。
|
||||
- 无关 untracked 文件:保持未修改、未 stage、未提交;包括历史 diff、任务书、临时文档等。
|
||||
|
||||
## 9. Remaining risks
|
||||
|
||||
- Android JNI/Kotlin/visual 行为尚待用户同一 APK 手工验证。
|
||||
- 当前 validation dataset 复用 STEP-051 T0~T3;用户指定正式数据尚未提供。
|
||||
- Host metadata checksum 尚未启用;若正式要求 integrity,需后续以同一 Descriptor 字段实现,不另建 contract。
|
||||
- Validation UI MIX/WAITING/INVALID observability 与 Rotation/Viewport 继续是 NON-BLOCKING 技术债。
|
||||
|
||||
阻塞项:无代码或 Linux 验证阻塞。当前不能写 `CLOSED / PASS`。
|
||||
@@ -8,6 +8,9 @@
|
||||
namespace mbgl::particle {
|
||||
|
||||
// Provider-neutral description of the canonical field bytes consumed by Core.
|
||||
// maplibre-particle-field/v1 is a north-west-origin regular grid: rows run
|
||||
// north-to-south, columns west-to-east, and cell = y * width + x. The first
|
||||
// float of each interleaved pair is eastward U; the second is northward V.
|
||||
// Provider names, URLs and transport metadata deliberately do not cross this boundary.
|
||||
struct ParticleFieldDescriptor {
|
||||
std::string fieldID;
|
||||
|
||||
@@ -61,6 +61,7 @@ public:
|
||||
};
|
||||
|
||||
explicit RasterArrayTimelineRuntime(std::vector<Entry>);
|
||||
bool append(Entry);
|
||||
RasterArrayTimelineFrame tick();
|
||||
bool play();
|
||||
bool pause();
|
||||
@@ -72,6 +73,8 @@ public:
|
||||
bool recover();
|
||||
RasterArrayTimelineFrame frame() const;
|
||||
float playbackSpeed() const;
|
||||
size_t size() const;
|
||||
Entry lastEntry() const;
|
||||
size_t cacheCapacity() const { return cacheCapacity_; }
|
||||
|
||||
private:
|
||||
@@ -102,6 +105,22 @@ enum class RasterArrayRuntimeFieldUpdateResult : uint8_t {
|
||||
RejectedNoCandidate,
|
||||
};
|
||||
|
||||
// Stable low-frequency Host -> Core submission result. Positive values are
|
||||
// accepted outcomes, while negative values are structured fail-closed errors.
|
||||
enum class RasterArrayHostFieldSubmitResult : int8_t {
|
||||
RejectedSourceMode = -1,
|
||||
RejectedAdapter = -2,
|
||||
RejectedIncompatibleSnapshot = -3,
|
||||
RejectedNonMonotonicSnapshot = -4,
|
||||
RejectedCapacity = -5,
|
||||
AcceptedPendingInitialWindow = 1,
|
||||
InitialWindowReady = 2,
|
||||
AcceptedNextSnapshot = 3,
|
||||
Duplicate = 4,
|
||||
};
|
||||
|
||||
const char* rasterArrayHostFieldSubmitResultName(RasterArrayHostFieldSubmitResult);
|
||||
|
||||
class RasterArraySource final : public Source {
|
||||
public:
|
||||
RasterArraySource(std::string id,
|
||||
@@ -113,6 +132,9 @@ public:
|
||||
std::string nextFieldURL = {},
|
||||
std::vector<RasterArrayRuntimeFieldDescriptor> runtimeFields = {});
|
||||
RasterArraySource(std::string id, std::vector<RasterArrayLocalFieldDescriptor> localFields);
|
||||
// Creates an empty Source ingress. The Host submits one canonical Field at
|
||||
// a time as metadata plus contiguous interleaved U/V bytes.
|
||||
explicit RasterArraySource(std::string id);
|
||||
~RasterArraySource() override;
|
||||
|
||||
class Impl;
|
||||
@@ -131,6 +153,7 @@ public:
|
||||
// Validation convenience: resubmit the current T1 through the formal
|
||||
// update API. This must return Noop without changing the Source window.
|
||||
RasterArrayRuntimeFieldUpdateResult repeatRuntimeFieldWindow();
|
||||
RasterArrayHostFieldSubmitResult submitHostField(const std::string& metadataText, const std::vector<char>& payload);
|
||||
// Low-frequency JNI commands for the Source-owned validation timeline.
|
||||
bool controlRuntimeTimeline(int command, float value = 0.0f);
|
||||
bool supportsLayerType(const LayerTypeInfo*) const override;
|
||||
@@ -174,6 +197,7 @@ private:
|
||||
std::optional<std::vector<char>> nextFieldBytes;
|
||||
std::vector<RuntimeFieldCandidate> runtimeFields;
|
||||
std::vector<LocalFieldCandidate> localFields;
|
||||
std::optional<particle::ParticleFieldSnapshot> pendingHostField;
|
||||
size_t nextRuntimeField = 0;
|
||||
uint64_t runtimeUpdateSequence = 0;
|
||||
std::shared_ptr<RasterArrayTimelineRuntime> timeline;
|
||||
|
||||
@@ -1204,6 +1204,29 @@ jni::jboolean NativeMapView::repeatRasterArraySourceFieldWindow(JNIEnv& env, con
|
||||
return success;
|
||||
}
|
||||
|
||||
jni::jint NativeMapView::submitRasterArraySourceHostField(JNIEnv& env,
|
||||
const jni::String& sourceId,
|
||||
const jni::String& metadata,
|
||||
const jni::Array<jbyte>& payload) {
|
||||
const auto id = jni::Make<std::string>(env, sourceId);
|
||||
auto* source = dynamic_cast<mbgl::style::RasterArraySource*>(map->getStyle().getSource(id));
|
||||
if (!source) {
|
||||
mbgl::Log::Error(mbgl::Event::JNI,
|
||||
"ParticleField: event=HOST_FIELD_SUBMIT_REJECTED reason=SOURCE_NOT_FOUND sourceId=" + id);
|
||||
return static_cast<jni::jint>(mbgl::style::RasterArrayHostFieldSubmitResult::RejectedSourceMode);
|
||||
}
|
||||
NullCheck(env, &payload);
|
||||
std::vector<char> bytes(payload.Length(env));
|
||||
if (!bytes.empty()) {
|
||||
jni::GetArrayRegion(env, *payload, 0, bytes.size(), reinterpret_cast<jbyte*>(bytes.data()));
|
||||
}
|
||||
const auto result = source->submitHostField(jni::Make<std::string>(env, metadata), bytes);
|
||||
mbgl::Log::Info(mbgl::Event::JNI,
|
||||
"ParticleField: event=HOST_FIELD_SUBMIT_BRIDGE sourceId=" + id +
|
||||
" result=" + mbgl::style::rasterArrayHostFieldSubmitResultName(result));
|
||||
return static_cast<jni::jint>(result);
|
||||
}
|
||||
|
||||
jni::jboolean NativeMapView::controlRasterArraySourceTimeline(JNIEnv& env,
|
||||
const jni::String& sourceId,
|
||||
const jni::jint command,
|
||||
@@ -1604,6 +1627,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
|
||||
METHOD(&NativeMapView::setStyleLayerFloatProperty, "nativeSetStyleLayerFloatProperty"),
|
||||
METHOD(&NativeMapView::advanceRasterArraySourceFieldWindow, "nativeAdvanceRasterArraySourceFieldWindow"),
|
||||
METHOD(&NativeMapView::repeatRasterArraySourceFieldWindow, "nativeRepeatRasterArraySourceFieldWindow"),
|
||||
METHOD(&NativeMapView::submitRasterArraySourceHostField, "nativeSubmitRasterArraySourceHostField"),
|
||||
METHOD(&NativeMapView::controlRasterArraySourceTimeline, "nativeControlRasterArraySourceTimeline"),
|
||||
METHOD(&NativeMapView::addLayer, "nativeAddLayer"),
|
||||
METHOD(&NativeMapView::addLayerAbove, "nativeAddLayerAbove"),
|
||||
|
||||
@@ -284,6 +284,10 @@ public:
|
||||
jni::jboolean setStyleLayerFloatProperty(JNIEnv&, const jni::String&, const jni::String&, jni::jfloat);
|
||||
jni::jboolean advanceRasterArraySourceFieldWindow(JNIEnv&, const jni::String&);
|
||||
jni::jboolean repeatRasterArraySourceFieldWindow(JNIEnv&, const jni::String&);
|
||||
jni::jint submitRasterArraySourceHostField(JNIEnv&,
|
||||
const jni::String&,
|
||||
const jni::String&,
|
||||
const jni::Array<jbyte>&);
|
||||
jni::jboolean controlRasterArraySourceTimeline(JNIEnv&, const jni::String&, jni::jint, jni::jfloat);
|
||||
|
||||
void addLayer(JNIEnv&, jlong, const jni::String&);
|
||||
|
||||
@@ -301,11 +301,29 @@ public final class MapLibreMap {
|
||||
return nativeMapView.repeatRasterArraySourceFieldWindow(sourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits one canonical particle Field as provider-neutral metadata plus contiguous binary U/V bytes.
|
||||
* This is a low-frequency ingress; Timeline and Runtime Field Window ownership remain in Core.
|
||||
*/
|
||||
public int submitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload) {
|
||||
return nativeMapView.submitRasterArraySourceHostField(sourceId, metadata, payload);
|
||||
}
|
||||
|
||||
/** Sends a low-frequency playback command to the Core-owned raster-array timeline. */
|
||||
public boolean controlRasterArraySourceTimeline(String sourceId, int command, float value) {
|
||||
return nativeMapView.controlRasterArraySourceTimeline(sourceId, command, value);
|
||||
}
|
||||
|
||||
public static final int HOST_FIELD_REJECTED_SOURCE_MODE = -1;
|
||||
public static final int HOST_FIELD_REJECTED_ADAPTER = -2;
|
||||
public static final int HOST_FIELD_REJECTED_INCOMPATIBLE = -3;
|
||||
public static final int HOST_FIELD_REJECTED_NON_MONOTONIC = -4;
|
||||
public static final int HOST_FIELD_REJECTED_CAPACITY = -5;
|
||||
public static final int HOST_FIELD_ACCEPTED_PENDING_INITIAL_WINDOW = 1;
|
||||
public static final int HOST_FIELD_INITIAL_WINDOW_READY = 2;
|
||||
public static final int HOST_FIELD_ACCEPTED_NEXT_SNAPSHOT = 3;
|
||||
public static final int HOST_FIELD_DUPLICATE_NOOP = 4;
|
||||
|
||||
// Style
|
||||
|
||||
/**
|
||||
|
||||
@@ -145,6 +145,8 @@ interface NativeMap {
|
||||
|
||||
boolean repeatRasterArraySourceFieldWindow(String sourceId);
|
||||
|
||||
int submitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload);
|
||||
|
||||
boolean controlRasterArraySourceTimeline(String sourceId, int command, float value);
|
||||
|
||||
boolean removeLayer(@NonNull String layerId);
|
||||
|
||||
@@ -968,6 +968,13 @@ final class NativeMapView implements NativeMap {
|
||||
&& nativeRepeatRasterArraySourceFieldWindow(sourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int submitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload) {
|
||||
return checkState("submitRasterArraySourceHostField")
|
||||
? MapLibreMap.HOST_FIELD_REJECTED_SOURCE_MODE
|
||||
: nativeSubmitRasterArraySourceHostField(sourceId, metadata, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean controlRasterArraySourceTimeline(String sourceId, int command, float value) {
|
||||
return !checkState("controlRasterArraySourceTimeline")
|
||||
@@ -1714,6 +1721,9 @@ final class NativeMapView implements NativeMap {
|
||||
@Keep
|
||||
private native boolean nativeRepeatRasterArraySourceFieldWindow(String sourceId);
|
||||
|
||||
@Keep
|
||||
private native int nativeSubmitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload);
|
||||
|
||||
@Keep
|
||||
private native boolean nativeControlRasterArraySourceTimeline(String sourceId, int command, float value);
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private var speedColorEnabled = true
|
||||
private var timeWindowMode = false
|
||||
private var productionFeedMode = false
|
||||
private var hostSubmissionMode = false
|
||||
private var styleLoadEpoch = 0L
|
||||
private var styleReady = false
|
||||
// Display mirror only; Core remains the playback state authority.
|
||||
@@ -99,24 +100,28 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
findViewById<Button>(R.id.validationUniformEast).setOnClickListener {
|
||||
timeWindowMode = false
|
||||
productionFeedMode = false
|
||||
hostSubmissionMode = false
|
||||
fixtureMode = FIXTURE_UNIFORM_EAST
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "UNIFORM_EAST")
|
||||
}
|
||||
findViewById<Button>(R.id.validationUniformNorth).setOnClickListener {
|
||||
timeWindowMode = false
|
||||
productionFeedMode = false
|
||||
hostSubmissionMode = false
|
||||
fixtureMode = FIXTURE_UNIFORM_NORTH
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "UNIFORM_NORTH")
|
||||
}
|
||||
findViewById<Button>(R.id.validationVortex).setOnClickListener {
|
||||
timeWindowMode = false
|
||||
productionFeedMode = false
|
||||
hostSubmissionMode = false
|
||||
fixtureMode = FIXTURE_VORTEX
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "VORTEX")
|
||||
}
|
||||
findViewById<Button>(R.id.validationTimeWindow).setOnClickListener {
|
||||
timeWindowMode = true
|
||||
productionFeedMode = true
|
||||
hostSubmissionMode = false
|
||||
fixtureMode = FIXTURE_UNIFORM_EAST
|
||||
timeMix = 0.0f
|
||||
timelineState = "NOT READY"
|
||||
@@ -140,10 +145,26 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
findViewById<Button>(R.id.validationProductionInvalid).setOnClickListener {
|
||||
productionFeedMode = true
|
||||
hostSubmissionMode = false
|
||||
timeWindowMode = true
|
||||
timelineState = "NOT READY"
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "INVALID_PRODUCTION_METADATA")
|
||||
}
|
||||
findViewById<Button>(R.id.validationHostFieldSubmit).setOnClickListener {
|
||||
timeWindowMode = true
|
||||
productionFeedMode = false
|
||||
hostSubmissionMode = true
|
||||
fixtureMode = FIXTURE_UNIFORM_EAST
|
||||
timeMix = 0.0f
|
||||
timelineState = "NOT READY"
|
||||
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "HOST_FIELD_SUBMIT")
|
||||
}
|
||||
findViewById<Button>(R.id.validationHostFieldDuplicate).setOnClickListener {
|
||||
if (this::maplibreMap.isInitialized) submitHostValidationField(3, duplicate = true)
|
||||
}
|
||||
findViewById<Button>(R.id.validationHostFieldInvalid).setOnClickListener {
|
||||
if (this::maplibreMap.isInitialized) submitInvalidHostValidationField()
|
||||
}
|
||||
findViewById<Button>(R.id.validationVisualDefault).setOnClickListener { resetVisualDefaults() }
|
||||
findViewById<Button>(R.id.validationVisualSizeDown).setOnClickListener { setVisualFloat("raster-particle-line-width", lineWidth - 1.0f) { lineWidth = it.coerceIn(1.0f, 8.0f) } }
|
||||
findViewById<Button>(R.id.validationVisualSizeUp).setOnClickListener { setVisualFloat("raster-particle-line-width", lineWidth + 1.0f) { lineWidth = it.coerceIn(1.0f, 8.0f) } }
|
||||
@@ -168,7 +189,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
findViewById<Button>(R.id.validationMenuVisual).setOnClickListener { showMenu(it, "VIS", intArrayOf(R.id.validationVisualDefault, R.id.validationVisualColor, R.id.validationVisualSizeDown, R.id.validationVisualSizeUp, R.id.validationVisualLengthDown, R.id.validationVisualLengthUp, R.id.validationVisualOpacityDown, R.id.validationVisualOpacityUp, R.id.validationVisualTrailDown, R.id.validationVisualTrailUp, R.id.validationVisualFadeDown, R.id.validationVisualFadeUp)) }
|
||||
findViewById<Button>(R.id.validationMenuCount).setOnClickListener { showMenu(it, "COUNT", intArrayOf(R.id.validationParticle256, R.id.validationParticle1024, R.id.validationParticle4096, R.id.validationParticle16384, R.id.validationParticle65536, R.id.validationParticle262144, R.id.validationParticle1048576)) }
|
||||
findViewById<Button>(R.id.validationMenuMap).setOnClickListener { showMenu(it, "MAP", intArrayOf(R.id.validationGlobal, R.id.validationOceanA, R.id.validationOceanB, R.id.validationDateline, R.id.validationWorldWrap)) }
|
||||
findViewById<Button>(R.id.validationMenuMore).setOnClickListener { showMenu(it, "MORE", intArrayOf(R.id.validationActive, R.id.validationStatic, R.id.validationDisable, R.id.validationEnable, R.id.validationUniformEast, R.id.validationUniformNorth, R.id.validationVortex, R.id.validationTimeWindow, R.id.validationProductionInvalid, R.id.validationAdvanceWindow, R.id.validationDuplicateWindow)) }
|
||||
findViewById<Button>(R.id.validationMenuMore).setOnClickListener { showMenu(it, "MORE", intArrayOf(R.id.validationActive, R.id.validationStatic, R.id.validationDisable, R.id.validationEnable, R.id.validationUniformEast, R.id.validationUniformNorth, R.id.validationVortex, R.id.validationTimeWindow, R.id.validationProductionInvalid, R.id.validationHostFieldSubmit, R.id.validationHostFieldDuplicate, R.id.validationHostFieldInvalid, R.id.validationAdvanceWindow, R.id.validationDuplicateWindow)) }
|
||||
}
|
||||
|
||||
private fun showMenu(anchor: android.view.View, title: String, actions: IntArray) {
|
||||
@@ -218,7 +239,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun actionLabel(id: Int): String = when (id) {
|
||||
R.id.validationVisualDefault -> "VIS DEFAULT"; R.id.validationVisualSizeDown -> "SIZE -"; R.id.validationVisualSizeUp -> "SIZE +"; R.id.validationVisualLengthDown -> "LENGTH -"; R.id.validationVisualLengthUp -> "LENGTH +"; R.id.validationVisualOpacityDown -> "OPACITY -"; R.id.validationVisualOpacityUp -> "OPACITY +"; R.id.validationVisualTrailDown -> "TRAIL -"; R.id.validationVisualTrailUp -> "TRAIL +"; R.id.validationVisualFadeDown -> "FADE -"; R.id.validationVisualFadeUp -> "FADE +"; R.id.validationVisualColor -> "COLOR"; R.id.validationParticle256 -> "256"; R.id.validationParticle1024 -> "1K"; R.id.validationParticle4096 -> "4K"; R.id.validationParticle16384 -> "16K"; R.id.validationParticle65536 -> "64K"; R.id.validationParticle262144 -> "256K"; R.id.validationParticle1048576 -> "1M"; R.id.validationGlobal -> "GLOBAL"; R.id.validationOceanA -> "OCEAN A"; R.id.validationOceanB -> "OCEAN B"; R.id.validationDateline -> "DATELINE"; R.id.validationWorldWrap -> "WORLD WRAP"; R.id.validationActive -> "ACTIVE"; R.id.validationStatic -> "STATIC"; R.id.validationDisable -> "DISABLE"; R.id.validationEnable -> "ENABLE"; R.id.validationUniformEast -> "UNIFORM EAST"; R.id.validationUniformNorth -> "UNIFORM NORTH"; R.id.validationVortex -> "VORTEX"; R.id.validationTimeWindow -> "PRODUCTION FEED"; R.id.validationProductionInvalid -> "INVALID PROD"; R.id.validationAdvanceWindow -> "ADVANCE WINDOW"; else -> "DUPLICATE T1"
|
||||
R.id.validationVisualDefault -> "VIS DEFAULT"; R.id.validationVisualSizeDown -> "SIZE -"; R.id.validationVisualSizeUp -> "SIZE +"; R.id.validationVisualLengthDown -> "LENGTH -"; R.id.validationVisualLengthUp -> "LENGTH +"; R.id.validationVisualOpacityDown -> "OPACITY -"; R.id.validationVisualOpacityUp -> "OPACITY +"; R.id.validationVisualTrailDown -> "TRAIL -"; R.id.validationVisualTrailUp -> "TRAIL +"; R.id.validationVisualFadeDown -> "FADE -"; R.id.validationVisualFadeUp -> "FADE +"; R.id.validationVisualColor -> "COLOR"; R.id.validationParticle256 -> "256"; R.id.validationParticle1024 -> "1K"; R.id.validationParticle4096 -> "4K"; R.id.validationParticle16384 -> "16K"; R.id.validationParticle65536 -> "64K"; R.id.validationParticle262144 -> "256K"; R.id.validationParticle1048576 -> "1M"; R.id.validationGlobal -> "GLOBAL"; R.id.validationOceanA -> "OCEAN A"; R.id.validationOceanB -> "OCEAN B"; R.id.validationDateline -> "DATELINE"; R.id.validationWorldWrap -> "WORLD WRAP"; R.id.validationActive -> "ACTIVE"; R.id.validationStatic -> "STATIC"; R.id.validationDisable -> "DISABLE"; R.id.validationEnable -> "ENABLE"; R.id.validationUniformEast -> "UNIFORM EAST"; R.id.validationUniformNorth -> "UNIFORM NORTH"; R.id.validationVortex -> "VORTEX"; R.id.validationTimeWindow -> "PRODUCTION FEED"; R.id.validationProductionInvalid -> "INVALID PROD"; R.id.validationHostFieldSubmit -> "HOST FIELD SUBMIT"; R.id.validationHostFieldDuplicate -> "HOST DUPLICATE"; R.id.validationHostFieldInvalid -> "INVALID HOST"; R.id.validationAdvanceWindow -> "ADVANCE WINDOW"; else -> "DUPLICATE T1"
|
||||
}
|
||||
|
||||
private fun loadValidationStyle(active: Boolean, enabled: Boolean, reason: String = "STYLE_CHANGE") {
|
||||
@@ -230,7 +251,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
Log.i(TAG, "ParticleGPU: event=STYLE_LOAD epoch=$styleLoadEpoch reason=$reason")
|
||||
maplibreMap.setStyle(Style.Builder().fromJson(validationStyle(active, enabled, reason))) {
|
||||
styleReady = true
|
||||
if (timeWindowMode) timelineState = "READY"
|
||||
if (hostSubmissionMode) {
|
||||
submitHostValidationDataset()
|
||||
} else if (timeWindowMode) {
|
||||
timelineState = "READY"
|
||||
}
|
||||
status.text = statusText()
|
||||
}
|
||||
}
|
||||
@@ -376,6 +401,52 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
Log.i(TAG, "ParticleField: event=FIELD_WINDOW_UPDATE_APPLIED result=NOOP styleEpoch=$styleLoadEpoch")
|
||||
}
|
||||
|
||||
private fun submitHostValidationDataset() {
|
||||
val results = (0..3).map { submitHostValidationField(it, duplicate = false) }
|
||||
timelineState = if (results.size == 4 && results.all { it > 0 }) "READY" else "NOT READY"
|
||||
status.text = statusText()
|
||||
Log.i(TAG, "ParticleField: event=HOST_FIELD_DATASET_SUBMIT results=${results.joinToString(",")} timeline=$timelineState")
|
||||
}
|
||||
|
||||
private fun submitHostValidationField(index: Int, duplicate: Boolean): Int {
|
||||
if (!styleReady || !hostSubmissionMode) {
|
||||
Log.i(TAG, "ParticleField: event=HOST_FIELD_SUBMIT_REJECTED reason=HOST_MODE_NOT_READY index=$index")
|
||||
return MapLibreMap.HOST_FIELD_REJECTED_SOURCE_MODE
|
||||
}
|
||||
return try {
|
||||
val metadata = assets.open("integration/particle-production/field-0$index.json")
|
||||
.bufferedReader().use { it.readText() }
|
||||
val payloadAsset = when (index) {
|
||||
1 -> "uniform-north"
|
||||
2 -> "vortex"
|
||||
else -> "uniform-east"
|
||||
}
|
||||
val payload = assets.open("integration/particle-fixtures/$payloadAsset/field.f32").use { it.readBytes() }
|
||||
val result = maplibreMap.submitRasterArraySourceHostField(PARTICLE_SOURCE_ID, metadata, payload)
|
||||
Log.i(TAG, "ParticleField: event=HOST_FIELD_SUBMIT index=$index duplicate=$duplicate payloadBytes=${payload.size} result=$result")
|
||||
result
|
||||
} catch (error: Exception) {
|
||||
Log.e(TAG, "ParticleField: event=HOST_FIELD_SUBMIT_REJECTED reason=ASSET_READ_FAILED index=$index", error)
|
||||
MapLibreMap.HOST_FIELD_REJECTED_ADAPTER
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitInvalidHostValidationField() {
|
||||
if (!styleReady || !hostSubmissionMode) {
|
||||
Log.i(TAG, "ParticleField: event=HOST_FIELD_SUBMIT_REJECTED reason=HOST_MODE_NOT_READY invalid=true")
|
||||
return
|
||||
}
|
||||
try {
|
||||
val metadata = assets.open("integration/particle-production/field-invalid.json")
|
||||
.bufferedReader().use { it.readText() }
|
||||
val payload = assets.open("integration/particle-fixtures/uniform-east/field.f32").use { it.readBytes() }
|
||||
val result = maplibreMap.submitRasterArraySourceHostField(PARTICLE_SOURCE_ID, metadata, payload)
|
||||
Log.i(TAG, "ParticleField: event=HOST_FIELD_INVALID_SUBMIT payloadBytes=${payload.size} result=$result")
|
||||
} catch (error: Exception) {
|
||||
Log.e(TAG, "ParticleField: event=HOST_FIELD_SUBMIT_REJECTED reason=ASSET_READ_FAILED invalid=true", error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun statusText(): String {
|
||||
val mask = when (maskMode) {
|
||||
MASK_OCEAN_A -> "OCEAN A"
|
||||
@@ -392,7 +463,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
} else {
|
||||
"EAST"
|
||||
}
|
||||
val playback = if (timeWindowMode) " · ${if (productionFeedMode) "PRODUCTION" else "FIXTURE"} · TL $timelineState · $timelineSpeed" else ""
|
||||
val playback = if (timeWindowMode) " · ${if (hostSubmissionMode) "HOST" else if (productionFeedMode) "PRODUCTION" else "FIXTURE"} · TL $timelineState · $timelineSpeed" else ""
|
||||
return "$mask · $runtime · $fields$playback · MIX ${(timeMix * 100).toInt()}% · $particleCount · W ${"%.1f".format(lineWidth)} L ${"%.3f".format(particleLength)} O ${(particleOpacity * 100).toInt()} T ${(trailOpacity * 100).toInt()} F ${(trailFade * 100).toInt()} C ${if (speedColorEnabled) "ON" else "OFF"}"
|
||||
}
|
||||
|
||||
@@ -413,7 +484,9 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val source = if (productionFeedMode) {
|
||||
val source = if (hostSubmissionMode) {
|
||||
"""{"type":"raster-array","host-field-submission":true}"""
|
||||
} else if (productionFeedMode) {
|
||||
"""{"type":"raster-array","production-fields":[
|
||||
{"metadata":"asset://integration/particle-production/${if (reason == "INVALID_PRODUCTION_METADATA") "field-invalid.json" else "field-00.json"}","field":"asset://integration/particle-fixtures/uniform-east/field.f32"},
|
||||
{"metadata":"asset://integration/particle-production/field-01.json","field":"asset://integration/particle-fixtures/uniform-north/field.f32"},
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<Button style="@style/ParticleValidationButton" android:id="@+id/validationActive"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationStatic"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDisable"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationEnable"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationGlobal"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationOceanA"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationOceanB"/>
|
||||
<Button style="@style/ParticleValidationButton" android:id="@+id/validationUniformEast"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationUniformNorth"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVortex"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationTimeWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix0"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix50"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix100"/>
|
||||
<Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle256"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle1024"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle4096"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle16384"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle65536"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle262144"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle1048576"/>
|
||||
<Button style="@style/ParticleValidationButton" android:id="@+id/validationAdvanceWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDuplicateWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationProductionInvalid"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualDefault"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualColor"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDateline"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationWorldWrap"/>
|
||||
<Button style="@style/ParticleValidationButton" android:id="@+id/validationAdvanceWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDuplicateWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationProductionInvalid"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldSubmit"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldDuplicate"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldInvalid"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualDefault"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualColor"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDateline"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationWorldWrap"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -207,6 +207,14 @@ std::optional<std::unique_ptr<Source>> convertImageSource(const std::string& id,
|
||||
std::optional<std::unique_ptr<Source>> convertRasterArraySource(const std::string& id,
|
||||
const Convertible& value,
|
||||
Error& error) {
|
||||
if (const auto hostSubmissionValue = objectMember(value, "host-field-submission")) {
|
||||
const auto hostSubmission = toBool(*hostSubmissionValue);
|
||||
if (!hostSubmission || !*hostSubmission) {
|
||||
error.message = "raster-array host-field-submission must be true";
|
||||
return std::nullopt;
|
||||
}
|
||||
return {std::make_unique<RasterArraySource>(id)};
|
||||
}
|
||||
if (const auto localFieldsValue = objectMember(value, "production-fields")) {
|
||||
if (!isArray(*localFieldsValue) || arrayLength(*localFieldsValue) < 2) {
|
||||
error.message = "raster-array production-fields must contain at least two entries";
|
||||
|
||||
@@ -38,6 +38,21 @@ RasterArrayTimelineRuntime::RasterArrayTimelineRuntime(std::vector<Entry> entrie
|
||||
}
|
||||
}
|
||||
|
||||
bool RasterArrayTimelineRuntime::append(Entry entry) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
if (!entry.field || !entry.field->isValid() || entries.size() >= cacheCapacity_ ||
|
||||
entries.back().timestamp >= entry.timestamp) {
|
||||
return false;
|
||||
}
|
||||
const bool continuesEndedTail = state == RasterArrayPlaybackState::Ended && t0 + 1 == entries.size() - 1;
|
||||
entries.push_back(std::move(entry));
|
||||
if (continuesEndedTail) {
|
||||
state = RasterArrayPlaybackState::Paused;
|
||||
lastTick = {};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
RasterArrayTimelineFrame RasterArrayTimelineRuntime::frameLocked() const {
|
||||
return {entries[t0].field, entries[t0 + 1].field, mix, t0, t0 + 1, windowRevision, state};
|
||||
}
|
||||
@@ -52,6 +67,16 @@ float RasterArrayTimelineRuntime::playbackSpeed() const {
|
||||
return speed;
|
||||
}
|
||||
|
||||
size_t RasterArrayTimelineRuntime::size() const {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
RasterArrayTimelineRuntime::Entry RasterArrayTimelineRuntime::lastEntry() const {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return entries.back();
|
||||
}
|
||||
|
||||
bool RasterArrayTimelineRuntime::moveToLocked(const size_t nextT0) {
|
||||
if (nextT0 + 1 >= entries.size()) return false;
|
||||
t0 = nextT0;
|
||||
@@ -207,6 +232,9 @@ RasterArraySource::RasterArraySource(std::string id,
|
||||
RasterArraySource::RasterArraySource(std::string id, std::vector<RasterArrayLocalFieldDescriptor> localFields)
|
||||
: Source(makeMutable<Impl>(std::move(id), std::move(localFields))) {}
|
||||
|
||||
RasterArraySource::RasterArraySource(std::string id)
|
||||
: Source(makeMutable<Impl>(std::move(id))) {}
|
||||
|
||||
RasterArraySource::~RasterArraySource() {
|
||||
weakFactory.invalidateWeakPtrs();
|
||||
}
|
||||
@@ -216,6 +244,13 @@ const RasterArraySource::Impl& RasterArraySource::impl() const {
|
||||
}
|
||||
|
||||
void RasterArraySource::loadDescription(FileSource& fileSource) {
|
||||
if (impl().hostFieldSubmission) {
|
||||
if (loaded) return;
|
||||
loaded = true;
|
||||
Log::Info(Event::Render, "ParticleField: event=HOST_FIELD_INGRESS_READY sourceId=" + getID());
|
||||
observer->onSourceLoaded(*this);
|
||||
return;
|
||||
}
|
||||
if (metadataRequest || fieldRequest) return;
|
||||
if (!impl().localFields.empty()) {
|
||||
localFields.reserve(impl().localFields.size());
|
||||
@@ -493,8 +528,8 @@ void RasterArraySource::tryInitializeTimeline() {
|
||||
|
||||
RasterArrayRuntimeFieldUpdateResult RasterArraySource::submitInitialRuntimeFieldWindow(
|
||||
particle::ParticleFieldSnapshot t0, particle::ParticleFieldSnapshot t1) {
|
||||
if (loaded || !t0.isValid() || !t1.isValid() || t0.generation == 0 || t1.generation == 0 ||
|
||||
t0.validTime >= t1.validTime || !t0.isTimeInterpolationCompatibleWith(t1)) {
|
||||
if (impl().field || impl().nextField || !t0.isValid() || !t1.isValid() || t0.generation == 0 ||
|
||||
t1.generation == 0 || t0.validTime >= t1.validTime || !t0.isTimeInterpolationCompatibleWith(t1)) {
|
||||
return rejectRuntimeFieldUpdate(RasterArrayRuntimeFieldUpdateResult::RejectedInvalidSnapshot,
|
||||
"INVALID_INITIAL_WINDOW");
|
||||
}
|
||||
@@ -588,6 +623,139 @@ RasterArrayRuntimeFieldUpdateResult RasterArraySource::repeatRuntimeFieldWindow(
|
||||
return updateRuntimeFieldWindow(*impl().nextField);
|
||||
}
|
||||
|
||||
const char* rasterArrayHostFieldSubmitResultName(const RasterArrayHostFieldSubmitResult result) {
|
||||
switch (result) {
|
||||
case RasterArrayHostFieldSubmitResult::RejectedSourceMode:
|
||||
return "REJECTED_SOURCE_MODE";
|
||||
case RasterArrayHostFieldSubmitResult::RejectedAdapter:
|
||||
return "REJECTED_ADAPTER";
|
||||
case RasterArrayHostFieldSubmitResult::RejectedIncompatibleSnapshot:
|
||||
return "REJECTED_INCOMPATIBLE_SNAPSHOT";
|
||||
case RasterArrayHostFieldSubmitResult::RejectedNonMonotonicSnapshot:
|
||||
return "REJECTED_NON_MONOTONIC_SNAPSHOT";
|
||||
case RasterArrayHostFieldSubmitResult::RejectedCapacity:
|
||||
return "REJECTED_CAPACITY";
|
||||
case RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow:
|
||||
return "ACCEPTED_PENDING_INITIAL_WINDOW";
|
||||
case RasterArrayHostFieldSubmitResult::InitialWindowReady:
|
||||
return "INITIAL_WINDOW_READY";
|
||||
case RasterArrayHostFieldSubmitResult::AcceptedNextSnapshot:
|
||||
return "ACCEPTED_NEXT_SNAPSHOT";
|
||||
case RasterArrayHostFieldSubmitResult::Duplicate:
|
||||
return "DUPLICATE_NOOP";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
RasterArrayHostFieldSubmitResult RasterArraySource::submitHostField(const std::string& metadataText,
|
||||
const std::vector<char>& payload) {
|
||||
Log::Info(Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_SUBMIT_REQUEST sourceId=" + getID() + " metadataBytes=" +
|
||||
std::to_string(metadataText.size()) + " payloadBytes=" + std::to_string(payload.size()));
|
||||
if (!impl().hostFieldSubmission) {
|
||||
Log::Warning(
|
||||
Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_SUBMIT_REJECTED sourceId=" + getID() + " result=REJECTED_SOURCE_MODE");
|
||||
return RasterArrayHostFieldSubmitResult::RejectedSourceMode;
|
||||
}
|
||||
|
||||
auto decoded = particle::LocalParticleFieldFeed::load(metadataText, payload);
|
||||
if (!decoded) {
|
||||
Log::Warning(
|
||||
Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_SUBMIT_REJECTED sourceId=" + getID() +
|
||||
" result=REJECTED_ADAPTER errorCode=" + particle::particleFieldAdapterErrorName(decoded.error) +
|
||||
" payloadBytes=" + std::to_string(payload.size()) + " error=" + decoded.message);
|
||||
return RasterArrayHostFieldSubmitResult::RejectedAdapter;
|
||||
}
|
||||
|
||||
auto snapshot = std::move(*decoded.snapshot);
|
||||
Log::Info(Event::Render,
|
||||
"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) +
|
||||
" layout=interleaved-uv encoding=float32-le payloadBytes=" + std::to_string(payload.size()));
|
||||
|
||||
std::shared_ptr<const particle::ParticleFieldSnapshot> previous;
|
||||
if (timeline) {
|
||||
previous = timeline->lastEntry().field;
|
||||
} else if (pendingHostField) {
|
||||
previous = std::make_shared<const particle::ParticleFieldSnapshot>(*pendingHostField);
|
||||
}
|
||||
if (previous && particle::sameFieldGpuContent(*previous, snapshot)) {
|
||||
Log::Info(Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_SUBMIT_DUPLICATE sourceId=" + getID() +
|
||||
" fieldId=" + snapshot.fieldID + " generation=" + std::to_string(snapshot.generation) +
|
||||
" validTime=" + std::to_string(snapshot.validTime) + " result=DUPLICATE_NOOP");
|
||||
return RasterArrayHostFieldSubmitResult::Duplicate;
|
||||
}
|
||||
if (previous && (snapshot.validTime <= previous->validTime || snapshot.generation <= previous->generation)) {
|
||||
Log::Warning(Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_SUBMIT_REJECTED sourceId=" + getID() +
|
||||
" fieldId=" + snapshot.fieldID + " generation=" + std::to_string(snapshot.generation) +
|
||||
" validTime=" + std::to_string(snapshot.validTime) +
|
||||
" result=REJECTED_NON_MONOTONIC_SNAPSHOT");
|
||||
return RasterArrayHostFieldSubmitResult::RejectedNonMonotonicSnapshot;
|
||||
}
|
||||
if (previous && !previous->isTimeInterpolationCompatibleWith(snapshot)) {
|
||||
Log::Warning(Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_SUBMIT_REJECTED sourceId=" + getID() +
|
||||
" fieldId=" + snapshot.fieldID + " generation=" + std::to_string(snapshot.generation) +
|
||||
" validTime=" + std::to_string(snapshot.validTime) + " result=REJECTED_INCOMPATIBLE_SNAPSHOT");
|
||||
return RasterArrayHostFieldSubmitResult::RejectedIncompatibleSnapshot;
|
||||
}
|
||||
|
||||
if (!pendingHostField && !timeline) {
|
||||
pendingHostField = std::move(snapshot);
|
||||
Log::Info(Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_SUBMIT_ACCEPTED sourceId=" + getID() +
|
||||
" kind=PENDING_INITIAL_T0 fieldId=" + pendingHostField->fieldID +
|
||||
" result=ACCEPTED_PENDING_INITIAL_WINDOW");
|
||||
return RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow;
|
||||
}
|
||||
|
||||
if (!timeline) {
|
||||
const auto initialT0 = *pendingHostField;
|
||||
const auto initialT1 = snapshot;
|
||||
if (submitInitialRuntimeFieldWindow(initialT0, initialT1) != RasterArrayRuntimeFieldUpdateResult::Applied) {
|
||||
return RasterArrayHostFieldSubmitResult::RejectedIncompatibleSnapshot;
|
||||
}
|
||||
std::vector<RasterArrayTimelineRuntime::Entry> entries;
|
||||
entries.push_back({std::make_shared<const particle::ParticleFieldSnapshot>(initialT0), initialT0.validTime});
|
||||
entries.push_back({std::make_shared<const particle::ParticleFieldSnapshot>(initialT1), initialT1.validTime});
|
||||
timeline = std::make_shared<RasterArrayTimelineRuntime>(std::move(entries));
|
||||
auto updated = makeMutable<Impl>(impl());
|
||||
updated->timeline = timeline;
|
||||
baseImpl = std::move(updated);
|
||||
pendingHostField.reset();
|
||||
observer->onSourceChanged(*this);
|
||||
Log::Info(Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_INITIAL_WINDOW_READY sourceId=" + getID() +
|
||||
" T0=" + initialT0.fieldID + " T1=" + initialT1.fieldID + " result=INITIAL_WINDOW_READY");
|
||||
Log::Info(Event::Render, "ParticleTimeline: event=READY snapshots=2 source=HOST_FIELD_SUBMISSION");
|
||||
return RasterArrayHostFieldSubmitResult::InitialWindowReady;
|
||||
}
|
||||
|
||||
if (timeline->size() >= timeline->cacheCapacity()) {
|
||||
Log::Warning(Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_SUBMIT_REJECTED sourceId=" + getID() +
|
||||
" fieldId=" + snapshot.fieldID + " result=REJECTED_CAPACITY");
|
||||
return RasterArrayHostFieldSubmitResult::RejectedCapacity;
|
||||
}
|
||||
const auto fieldID = snapshot.fieldID;
|
||||
const auto generation = snapshot.generation;
|
||||
const auto validTime = snapshot.validTime;
|
||||
if (!timeline->append({std::make_shared<const particle::ParticleFieldSnapshot>(std::move(snapshot)), validTime})) {
|
||||
return RasterArrayHostFieldSubmitResult::RejectedCapacity;
|
||||
}
|
||||
Log::Info(Event::Render,
|
||||
"ParticleField: event=HOST_FIELD_NEXT_SUBMIT sourceId=" + getID() + " fieldId=" + fieldID +
|
||||
" generation=" + std::to_string(generation) + " validTime=" + std::to_string(validTime) +
|
||||
" snapshots=" + std::to_string(timeline->size()) + " result=ACCEPTED_NEXT_SNAPSHOT");
|
||||
return RasterArrayHostFieldSubmitResult::AcceptedNextSnapshot;
|
||||
}
|
||||
|
||||
bool RasterArraySource::controlRuntimeTimeline(const int command, const float value) {
|
||||
if (!timeline) {
|
||||
Log::Warning(Event::Render, "ParticleTimeline: event=REJECTED reason=TIMELINE_NOT_READY sourceId=" + getID());
|
||||
|
||||
@@ -26,6 +26,9 @@ public:
|
||||
runtimeFields(std::move(runtimeFields_)) {}
|
||||
Impl(std::string id, std::vector<RasterArrayLocalFieldDescriptor> localFields_)
|
||||
: Source::Impl(SourceType::RasterArray, std::move(id)), localFields(std::move(localFields_)) {}
|
||||
explicit Impl(std::string id)
|
||||
: Source::Impl(SourceType::RasterArray, std::move(id)),
|
||||
hostFieldSubmission(true) {}
|
||||
Impl(const Impl&) = default;
|
||||
Impl(const Impl& other, particle::ParticleFieldSnapshot field_)
|
||||
: Source::Impl(other),
|
||||
@@ -37,6 +40,7 @@ public:
|
||||
nextFieldURL(other.nextFieldURL),
|
||||
runtimeFields(other.runtimeFields),
|
||||
localFields(other.localFields),
|
||||
hostFieldSubmission(other.hostFieldSubmission),
|
||||
timeline(other.timeline),
|
||||
field(std::move(field_)) {}
|
||||
Impl(const Impl& other, particle::ParticleFieldSnapshot field_, particle::ParticleFieldSnapshot nextField_)
|
||||
@@ -53,6 +57,7 @@ public:
|
||||
std::string nextFieldURL;
|
||||
std::vector<RasterArrayRuntimeFieldDescriptor> runtimeFields;
|
||||
std::vector<RasterArrayLocalFieldDescriptor> localFields;
|
||||
bool hostFieldSubmission = false;
|
||||
std::shared_ptr<RasterArrayTimelineRuntime> timeline;
|
||||
std::optional<particle::ParticleFieldSnapshot> field;
|
||||
std::optional<particle::ParticleFieldSnapshot> nextField;
|
||||
|
||||
@@ -346,6 +346,160 @@ TEST(Source, LocalProductionFeedRejectsMissingGeneration) {
|
||||
EXPECT_EQ(result.error, particle::ParticleFieldAdapterError::InvalidTimestampOrIdentity);
|
||||
}
|
||||
|
||||
TEST(Source, CanonicalFieldUsesEastwardUAndNorthwardV) {
|
||||
const auto east = fixtureBytes("uniform-east", "field.f32");
|
||||
const auto north = fixtureBytes("uniform-north", "field.f32");
|
||||
const auto vortex = fixtureBytes("vortex", "field.f32");
|
||||
const auto eastResult = particle::LocalParticleFieldFeed::load(localMetadata(0),
|
||||
std::vector<char>(east.begin(), east.end()));
|
||||
const auto northResult = particle::LocalParticleFieldFeed::load(localMetadata(1),
|
||||
std::vector<char>(north.begin(), north.end()));
|
||||
const auto vortexResult = particle::LocalParticleFieldFeed::load(localMetadata(2),
|
||||
std::vector<char>(vortex.begin(), vortex.end()));
|
||||
ASSERT_TRUE(eastResult);
|
||||
ASSERT_TRUE(northResult);
|
||||
ASSERT_TRUE(vortexResult);
|
||||
ASSERT_TRUE(eastResult.snapshot);
|
||||
ASSERT_TRUE(northResult.snapshot);
|
||||
ASSERT_GE(eastResult.snapshot->velocity.size(), 2u);
|
||||
ASSERT_GE(northResult.snapshot->velocity.size(), 2u);
|
||||
EXPECT_FLOAT_EQ(eastResult.snapshot->velocity[0], 1.0f);
|
||||
EXPECT_FLOAT_EQ(eastResult.snapshot->velocity[1], 0.0f);
|
||||
EXPECT_FLOAT_EQ(northResult.snapshot->velocity[0], 0.0f);
|
||||
EXPECT_FLOAT_EQ(northResult.snapshot->velocity[1], 1.0f);
|
||||
ASSERT_TRUE(vortexResult.snapshot);
|
||||
EXPECT_FLOAT_EQ(vortexResult.snapshot->velocity[0], 0.9375f);
|
||||
EXPECT_FLOAT_EQ(vortexResult.snapshot->velocity[1], -0.9375f);
|
||||
EXPECT_FLOAT_EQ(vortexResult.snapshot->velocity[vortexResult.snapshot->velocity.size() - 2], -0.9375f);
|
||||
EXPECT_FLOAT_EQ(vortexResult.snapshot->velocity.back(), 0.9375f);
|
||||
}
|
||||
|
||||
TEST(Source, HostFieldSubmissionBuildsOneCoreOwnedTimeline) {
|
||||
SourceTest test;
|
||||
style::RasterArraySource source{"host-submission"};
|
||||
source.setObserver(&test.styleObserver);
|
||||
source.loadDescription(*test.fileSource);
|
||||
EXPECT_TRUE(source.loaded);
|
||||
EXPECT_TRUE(source.impl().hostFieldSubmission);
|
||||
EXPECT_FALSE(source.impl().field);
|
||||
|
||||
const auto east = fixtureBytes("uniform-east", "field.f32");
|
||||
const auto north = fixtureBytes("uniform-north", "field.f32");
|
||||
const auto vortex = fixtureBytes("vortex", "field.f32");
|
||||
const std::vector<char> eastPayload(east.begin(), east.end());
|
||||
const std::vector<char> northPayload(north.begin(), north.end());
|
||||
const std::vector<char> vortexPayload(vortex.begin(), vortex.end());
|
||||
|
||||
EXPECT_EQ(source.submitHostField(localMetadata(0), eastPayload),
|
||||
style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow);
|
||||
EXPECT_FALSE(source.impl().field);
|
||||
EXPECT_EQ(source.submitHostField(localMetadata(1), northPayload),
|
||||
style::RasterArrayHostFieldSubmitResult::InitialWindowReady);
|
||||
ASSERT_TRUE(source.impl().field);
|
||||
ASSERT_TRUE(source.impl().nextField);
|
||||
ASSERT_TRUE(source.impl().timeline);
|
||||
EXPECT_EQ(source.impl().field->fieldID, "production-t0");
|
||||
EXPECT_EQ(source.impl().nextField->fieldID, "production-t1");
|
||||
EXPECT_EQ(source.impl().timeline->frame().state, style::RasterArrayPlaybackState::Ready);
|
||||
|
||||
EXPECT_EQ(source.submitHostField(localMetadata(2), vortexPayload),
|
||||
style::RasterArrayHostFieldSubmitResult::AcceptedNextSnapshot);
|
||||
EXPECT_EQ(source.submitHostField(localMetadata(3), eastPayload),
|
||||
style::RasterArrayHostFieldSubmitResult::AcceptedNextSnapshot);
|
||||
EXPECT_EQ(source.impl().timeline->size(), 4u);
|
||||
EXPECT_EQ(source.impl().timeline->lastEntry().field->fieldID, "production-t3");
|
||||
|
||||
EXPECT_EQ(source.submitHostField(localMetadata(3), eastPayload),
|
||||
style::RasterArrayHostFieldSubmitResult::Duplicate);
|
||||
EXPECT_EQ(source.impl().timeline->size(), 4u);
|
||||
EXPECT_TRUE(source.controlRuntimeTimeline(4));
|
||||
EXPECT_EQ(source.impl().timeline->frame().t0Index, 1u);
|
||||
EXPECT_TRUE(source.controlRuntimeTimeline(4));
|
||||
EXPECT_EQ(source.impl().timeline->frame().t0Index, 2u);
|
||||
}
|
||||
|
||||
TEST(Source, HostFieldSubmissionStyleContractCreatesEmptyIngress) {
|
||||
SourceTest test;
|
||||
test.style.loadJSON(R"STYLE({
|
||||
"version": 8,
|
||||
"sources": {
|
||||
"host-submission": {"type": "raster-array", "host-field-submission": true}
|
||||
},
|
||||
"layers": []
|
||||
})STYLE");
|
||||
auto* baseSource = test.style.getSource("host-submission");
|
||||
ASSERT_NE(baseSource, nullptr);
|
||||
auto* source = baseSource->as<style::RasterArraySource>();
|
||||
ASSERT_NE(source, nullptr);
|
||||
EXPECT_TRUE(source->impl().hostFieldSubmission);
|
||||
EXPECT_FALSE(source->impl().field);
|
||||
EXPECT_FALSE(source->impl().timeline);
|
||||
}
|
||||
|
||||
TEST(Source, HostFieldSubmissionRejectsInvalidWithoutChangingLastValidField) {
|
||||
SourceTest test;
|
||||
style::RasterArraySource source{"host-submission"};
|
||||
source.setObserver(&test.styleObserver);
|
||||
source.loadDescription(*test.fileSource);
|
||||
const auto field = fixtureBytes("uniform-east", "field.f32");
|
||||
const std::vector<char> payload(field.begin(), field.end());
|
||||
|
||||
EXPECT_EQ(source.submitHostField(localMetadata(0), payload),
|
||||
style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow);
|
||||
EXPECT_EQ(source.submitHostField(localMetadata(1), payload),
|
||||
style::RasterArrayHostFieldSubmitResult::InitialWindowReady);
|
||||
ASSERT_TRUE(source.impl().timeline);
|
||||
const auto lastValid = source.impl().timeline->lastEntry().field;
|
||||
|
||||
auto invalidIdentity = localMetadata(2);
|
||||
const auto fieldID = invalidIdentity.find("production-t2");
|
||||
ASSERT_NE(fieldID, std::string::npos);
|
||||
invalidIdentity.replace(fieldID, std::string("production-t2").size(), "");
|
||||
EXPECT_EQ(source.submitHostField(invalidIdentity, payload),
|
||||
style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
|
||||
|
||||
auto unsupportedLayout = localMetadata(2);
|
||||
const auto layout = unsupportedLayout.find("interleaved-uv");
|
||||
ASSERT_NE(layout, std::string::npos);
|
||||
unsupportedLayout.replace(layout, std::string("interleaved-uv").size(), "planar-uv");
|
||||
EXPECT_EQ(source.submitHostField(unsupportedLayout, payload),
|
||||
style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
|
||||
|
||||
auto unsupportedEncoding = localMetadata(2);
|
||||
const auto encoding = unsupportedEncoding.find("float32-le");
|
||||
ASSERT_NE(encoding, std::string::npos);
|
||||
unsupportedEncoding.replace(encoding, std::string("float32-le").size(), "float64-le");
|
||||
EXPECT_EQ(source.submitHostField(unsupportedEncoding, payload),
|
||||
style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
|
||||
|
||||
auto invalidDimensions = localMetadata(2);
|
||||
const auto width = invalidDimensions.find("\"width\":16");
|
||||
ASSERT_NE(width, std::string::npos);
|
||||
invalidDimensions.replace(width, std::string("\"width\":16").size(), "\"width\":0");
|
||||
EXPECT_EQ(source.submitHostField(invalidDimensions, payload),
|
||||
style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
|
||||
|
||||
auto shortPayload = payload;
|
||||
shortPayload.pop_back();
|
||||
EXPECT_EQ(source.submitHostField(localMetadata(2), shortPayload),
|
||||
style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
|
||||
|
||||
auto nonMonotonic = localMetadata(2);
|
||||
const auto validTime = nonMonotonic.find("202609040200");
|
||||
ASSERT_NE(validTime, std::string::npos);
|
||||
nonMonotonic.replace(validTime, std::string("202609040200").size(), "202609040050");
|
||||
EXPECT_EQ(source.submitHostField(nonMonotonic, payload),
|
||||
style::RasterArrayHostFieldSubmitResult::RejectedNonMonotonicSnapshot);
|
||||
|
||||
EXPECT_EQ(source.impl().timeline->size(), 2u);
|
||||
EXPECT_EQ(source.impl().timeline->lastEntry().field, lastValid);
|
||||
EXPECT_EQ(source.impl().timeline->frame().state, style::RasterArrayPlaybackState::Ready);
|
||||
|
||||
style::RasterArraySource fixtureSource{"fixture", "uniform-east", "metadata", "field"};
|
||||
EXPECT_EQ(fixtureSource.submitHostField(localMetadata(2), payload),
|
||||
style::RasterArrayHostFieldSubmitResult::RejectedSourceMode);
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayTimelineCandidatesReachReadyThroughFieldAdapter) {
|
||||
SourceTest test;
|
||||
const auto eastMetadata = fixtureBytes("uniform-east", "metadata.json");
|
||||
|
||||
@@ -375,3 +375,12 @@
|
||||
- 新增 local feed 成功、严格 identity/generation/validTime、Timeline READY/PLAY 与非法 generation 拒绝测试;Linux OpenGL 定向 14/14 PASS。Android/Jest 未执行。
|
||||
- Android TIME 入口可加载 production-like resources;MIX 文本滞后仍为 NON-BLOCKING observability,Rotation/Viewport 技术债未扩大处理。
|
||||
- 状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`。
|
||||
|
||||
## 2026-09-04 — STEP-053 Windy-style Canonical Field 与 Host Submission
|
||||
|
||||
- 新增 Host 已持有数据后的低频 ingress:`metadata + contiguous binary UV bytes → RasterArraySource::submitHostField() → LocalParticleFieldFeed → ParticleFieldDataAdapter → ParticleFieldSnapshot`;没有新增第二套 Descriptor、Snapshot、Timeline 或 Runtime Window。
|
||||
- canonical v1 固定 north-west origin、row-major、north-to-south rows、west-to-east columns、`float32-le interleaved-uv`,其中 U=eastward、V=northward;Field payload 不走巨大 JSON/JS Array。
|
||||
- 第一个合法 Field 暂存 T0,第二个复用既有 initial T0/T1 bridge 并建立 Core-owned Timeline,后续 Field 只追加到同一个有界 Timeline;Duplicate/Invalid 均 fail-closed,last valid 保持。
|
||||
- Android `MapLibreMap`/JNI 增加单 Field binary submit;Validation Host 增加 HOST FIELD SUBMIT、HOST DUPLICATE、INVALID HOST,并由 Kotlin 读取本地资源模拟未来 React Native Host。旧 fixture 与 STEP-051 production feed 保留。
|
||||
- Linux OpenGL `mbgl-test-runner`、`mbgl-render`、`mbgl-render-test-runner` 构建通过;新增 Host/canonical 4/4、Source 44/44、RasterParticle property 2/2 PASS。未执行 Android Gradle/APK/ADB/设备或 Jest。
|
||||
- Renderer、GPU simulation/trail、Runtime Window、Playback ownership、WeatherColorLayer 分离边界未改。状态:`IMPLEMENTED / WAITING_ANDROID_VALIDATION`;用户正式数据尚未提供,不能提前关闭。
|
||||
|
||||
Reference in New Issue
Block a user