fix: reset V17 weather runtime across Android and iOS
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:
@@ -245,6 +245,10 @@ public:
|
||||
particle::ParticleProjectionResult resolveParticleProjection() const;
|
||||
bool clearEncodedField(const std::string& fieldKey);
|
||||
void clearAllEncodedFields();
|
||||
// Explicit terminal cleanup for an attached WeatherRuntime. Unlike an ordinary
|
||||
// V4 window clear, this also drops bindings, forecast clock and V3 host ingress.
|
||||
// Source and Style layers remain installed for the next runtime generation.
|
||||
bool resetWeatherRuntimeState();
|
||||
// Producer-facing timestamp seek. Core alone selects T0/T1 and MIX.
|
||||
bool seekRuntimeTimelineToTimestamp(uint64_t timestamp);
|
||||
// Low-frequency JNI commands for the Source-owned validation timeline.
|
||||
|
||||
@@ -46,7 +46,11 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
|
||||
),
|
||||
)
|
||||
}
|
||||
val runtime = map.replaceWeatherRuntime(WeatherRuntime.Config(WeatherIds.SOURCE, WeatherIds.COLOR_LAYER, WeatherIds.PARTICLE_LAYER))
|
||||
val runtime = try {
|
||||
map.replaceWeatherRuntime(WeatherRuntime.Config(WeatherIds.SOURCE, WeatherIds.COLOR_LAYER, WeatherIds.PARTICLE_LAYER))
|
||||
} catch (error: IllegalStateException) {
|
||||
return@withMapView reject(promise, "RUNTIME_CLEANUP_FAILED", error.message ?: "WeatherRuntime replacement cleanup failed")
|
||||
}
|
||||
if (!attachmentOwner(view).attach(map)) {
|
||||
map.disposeWeatherRuntime()
|
||||
return@withMapView reject(promise, "RUNTIME_DISPOSED", "The MapView attachment owner has been destroyed")
|
||||
@@ -57,11 +61,18 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
|
||||
@ReactMethod
|
||||
override fun detach(reactTag: Double, promise: Promise) = withMapView(reactTag, promise) { view, map ->
|
||||
val owner = view.getTag(R.id.weather_runtime_attachment_owner) as? WeatherRuntimeAttachmentOwner<*>
|
||||
val hasRuntime = map.weatherRuntime != null
|
||||
val detachedByOwner = (owner as? WeatherRuntimeAttachmentOwner<MapLibreMap>)?.detach(map) ?: false
|
||||
if (!detachedByOwner) map.disposeWeatherRuntime()
|
||||
if (!hasRuntime) reject(promise, "RUNTIME_DISPOSED", "No attached WeatherRuntime exists for this map")
|
||||
else promise.resolve(null)
|
||||
// Duplicate detach is idempotent; Core failures must not be reported as success.
|
||||
try {
|
||||
val detachedByOwner = (owner as? WeatherRuntimeAttachmentOwner<MapLibreMap>)?.detach(map) ?: false
|
||||
if (!detachedByOwner) map.disposeWeatherRuntime()
|
||||
if (map.weatherRuntime != null) {
|
||||
reject(promise, "RUNTIME_CLEANUP_FAILED", "WeatherRuntime remains attached after detach")
|
||||
} else {
|
||||
promise.resolve(null)
|
||||
}
|
||||
} catch (error: IllegalStateException) {
|
||||
reject(promise, "RUNTIME_CLEANUP_FAILED", error.message ?: "WeatherRuntime detach cleanup failed")
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod override fun getState(reactTag: Double, promise: Promise) = withRuntime(reactTag, promise) { runtime -> promise.resolve(snapshot(runtime)) }
|
||||
|
||||
@@ -155,7 +155,16 @@ RCT_EXPORT_MODULE(WeatherRuntimeBridge)
|
||||
}];
|
||||
}
|
||||
- (void)detach:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
[self withMapForTag:reactTag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { [map disposeParticleFieldRuntime]; resolve(nil); }];
|
||||
[self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) {
|
||||
// Duplicate detach is a no-op, but an actual Source cleanup failure is not.
|
||||
if (!runtime) { resolve(nil); return; }
|
||||
NSError *error = nil;
|
||||
if (![map disposeParticleFieldRuntimeWithError:&error]) {
|
||||
reject(@"RUNTIME_CLEANUP_FAILED", error.localizedDescription ?: @"WeatherRuntime detach cleanup failed", error);
|
||||
return;
|
||||
}
|
||||
resolve(nil);
|
||||
}];
|
||||
}
|
||||
- (void)getState:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self withMapForTag:reactTag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { resolve(snapshot(runtime)); }]; }
|
||||
- (void)setRetainedUnderlayEnabled:(double)reactTag enabled:(BOOL)enabled resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
|
||||
@@ -1472,6 +1472,16 @@ jni::jboolean NativeMapView::clearRasterArraySourceHostFields(JNIEnv& env, const
|
||||
return cleared;
|
||||
}
|
||||
|
||||
jni::jboolean NativeMapView::resetRasterArraySourceWeatherRuntime(JNIEnv& env, const jni::String& sourceId) {
|
||||
const auto id = jni::Make<std::string>(env, sourceId);
|
||||
auto* source = dynamic_cast<mbgl::style::RasterArraySource*>(map->getStyle().getSource(id));
|
||||
if (!source) return false;
|
||||
const bool reset = source->resetWeatherRuntimeState();
|
||||
mbgl::Log::Info(mbgl::Event::JNI, "WeatherRuntime: event=TERMINAL_RESET sourceId=" + id +
|
||||
" result=" + (reset ? "SUCCESS" : "REJECTED"));
|
||||
return reset;
|
||||
}
|
||||
|
||||
jni::jboolean NativeMapView::seekRasterArraySourceTimelineToTimestamp(JNIEnv& env,
|
||||
const jni::String& sourceId,
|
||||
const jni::jlong timestamp) {
|
||||
@@ -1920,6 +1930,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
|
||||
METHOD(&NativeMapView::submitRasterArraySourceHostField, "nativeSubmitRasterArraySourceHostField"),
|
||||
METHOD(&NativeMapView::submitRasterArraySourceHostFieldV2, "nativeSubmitRasterArraySourceHostFieldV2"),
|
||||
METHOD(&NativeMapView::clearRasterArraySourceHostFields, "nativeClearRasterArraySourceHostFields"),
|
||||
METHOD(&NativeMapView::resetRasterArraySourceWeatherRuntime, "nativeResetRasterArraySourceWeatherRuntime"),
|
||||
METHOD(&NativeMapView::seekRasterArraySourceTimelineToTimestamp, "nativeSeekRasterArraySourceTimelineToTimestamp"),
|
||||
METHOD(&NativeMapView::controlRasterArraySourceTimeline, "nativeControlRasterArraySourceTimeline"),
|
||||
METHOD(&NativeMapView::submitRasterArraySourceEncodedField, "nativeSubmitRasterArraySourceEncodedField"),
|
||||
|
||||
@@ -318,6 +318,7 @@ public:
|
||||
jni::jint,
|
||||
jni::jint);
|
||||
jni::jboolean clearRasterArraySourceHostFields(JNIEnv&, const jni::String&);
|
||||
jni::jboolean resetRasterArraySourceWeatherRuntime(JNIEnv&, const jni::String&);
|
||||
jni::jboolean seekRasterArraySourceTimelineToTimestamp(JNIEnv&, const jni::String&, jni::jlong);
|
||||
jni::jboolean controlRasterArraySourceTimeline(JNIEnv&, const jni::String&, jni::jint, jni::jfloat);
|
||||
jni::jint submitRasterArraySourceEncodedField(JNIEnv&, const jni::String&, const jni::String&, const jni::String&, jni::jlong, jni::jlong, const jni::String&, const jni::Array<jbyte>&);
|
||||
|
||||
@@ -491,6 +491,11 @@ public final class MapLibreMap {
|
||||
return nativeMapView.clearRasterArraySourceHostFields(sourceId);
|
||||
}
|
||||
|
||||
/** Internal terminal reset for V3+V4 state on the same existing Style source. */
|
||||
boolean resetRasterArraySourceWeatherRuntime(String sourceId) {
|
||||
return nativeMapView.resetRasterArraySourceWeatherRuntime(sourceId);
|
||||
}
|
||||
|
||||
/** Requests a Core-owned T0/T1/MIX selection for a producer timestamp in milliseconds. */
|
||||
public boolean seekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp) {
|
||||
return nativeMapView.seekRasterArraySourceTimelineToTimestamp(sourceId, timestamp);
|
||||
|
||||
@@ -172,6 +172,7 @@ interface NativeMap {
|
||||
boolean vectorPresent, int vectorLength, int validityLength);
|
||||
|
||||
boolean clearRasterArraySourceHostFields(String sourceId);
|
||||
boolean resetRasterArraySourceWeatherRuntime(String sourceId);
|
||||
|
||||
boolean seekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp);
|
||||
|
||||
|
||||
@@ -1070,6 +1070,12 @@ final class NativeMapView implements NativeMap {
|
||||
&& nativeClearRasterArraySourceHostFields(sourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean resetRasterArraySourceWeatherRuntime(String sourceId) {
|
||||
return !checkState("resetRasterArraySourceWeatherRuntime")
|
||||
&& nativeResetRasterArraySourceWeatherRuntime(sourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean seekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp) {
|
||||
return !checkState("seekRasterArraySourceTimelineToTimestamp")
|
||||
@@ -1876,6 +1882,9 @@ final class NativeMapView implements NativeMap {
|
||||
@Keep
|
||||
private native boolean nativeClearRasterArraySourceHostFields(String sourceId);
|
||||
|
||||
@Keep
|
||||
private native boolean nativeResetRasterArraySourceWeatherRuntime(String sourceId);
|
||||
|
||||
@Keep
|
||||
private native boolean nativeSeekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp);
|
||||
|
||||
|
||||
@@ -81,7 +81,25 @@ public final class WeatherRuntime {
|
||||
public Config getConfig() { return config; }
|
||||
public boolean isDisposed() { return disposed; }
|
||||
/** Ends this map/style-scoped facade. It is safe to call repeatedly. */
|
||||
public void dispose() { if (disposed) return; disposed = true; cancelCadenceWake(); }
|
||||
public void dispose() {
|
||||
if (disposed) return;
|
||||
cancelCadenceWake();
|
||||
// If Style has already dropped the source, there is no Source-owned V4 store
|
||||
// left to reset; otherwise disposal must not claim success before Core reset.
|
||||
if (map.hasStyleSource(config.sourceId)) {
|
||||
map.setStyleLayerVisibility(config.weatherColorLayerId, false);
|
||||
map.setStyleLayerVisibility(config.particleLayerId, false);
|
||||
if (!map.resetRasterArraySourceWeatherRuntime(config.sourceId)) {
|
||||
throw new IllegalStateException("WeatherRuntime terminal Core reset failed");
|
||||
}
|
||||
}
|
||||
submittedEncodedFieldKeys.clear();
|
||||
v4ParticleBindingFieldKey = null;
|
||||
v4ForecastPlaying = false;
|
||||
playbackState = PlaybackState.PAUSE;
|
||||
state = State.DISABLED;
|
||||
disposed = true;
|
||||
}
|
||||
/** Core-owned capacity; Host never sees the shader's UBO layout. */
|
||||
public int getWeatherColorStopCapacity() { return weatherColorStopCapacity; }
|
||||
|
||||
@@ -154,8 +172,14 @@ public final class WeatherRuntime {
|
||||
map.controlRasterArraySourceTimeline(config.sourceId, 1, 0f);
|
||||
boolean color = map.setStyleLayerVisibility(config.weatherColorLayerId, false);
|
||||
boolean particle = map.setStyleLayerVisibility(config.particleLayerId, false);
|
||||
boolean clear = map.clearRasterArraySourceHostFields(config.sourceId);
|
||||
boolean clear = map.resetRasterArraySourceWeatherRuntime(config.sourceId);
|
||||
state = stateAfterDisable(color && particle && clear);
|
||||
if (clear) {
|
||||
submittedEncodedFieldKeys.clear();
|
||||
v4ParticleBindingFieldKey = null;
|
||||
v4ForecastPlaying = false;
|
||||
playbackState = PlaybackState.PAUSE;
|
||||
}
|
||||
if (state != State.READY) cancelCadenceWake();
|
||||
return result(color && particle && clear);
|
||||
}
|
||||
|
||||
@@ -103,6 +103,40 @@ public class WeatherRuntimeStateTest {
|
||||
Mockito.verify(map, Mockito.never()).submitRasterArraySourceHostField(Mockito.anyString(), Mockito.anyString(), Mockito.any(byte[].class));
|
||||
}
|
||||
|
||||
@Test public void disableResetsV3AndV4ViaOneCoreCommandAndCanEnableAgain() {
|
||||
MapLibreMap map = Mockito.mock(MapLibreMap.class);
|
||||
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
|
||||
Mockito.when(map.setStyleLayerVisibility(Mockito.anyString(), Mockito.anyBoolean())).thenReturn(true);
|
||||
Mockito.when(map.resetRasterArraySourceWeatherRuntime("source")).thenReturn(true);
|
||||
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
|
||||
assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setWeatherEnabled(false));
|
||||
assertEquals(WeatherRuntime.State.DISABLED, runtime.getState());
|
||||
Mockito.verify(map).resetRasterArraySourceWeatherRuntime("source");
|
||||
Mockito.verify(map, Mockito.never()).clearRasterArraySourceHostFields(Mockito.anyString());
|
||||
assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setWeatherEnabled(true));
|
||||
assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, runtime.getState());
|
||||
}
|
||||
|
||||
@Test public void terminalDisposeResetsSourceAndFailureDoesNotClaimDisposed() {
|
||||
MapLibreMap map = Mockito.mock(MapLibreMap.class);
|
||||
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
|
||||
Mockito.when(map.hasStyleSource("source")).thenReturn(true);
|
||||
Mockito.when(map.resetRasterArraySourceWeatherRuntime("source")).thenReturn(false).thenReturn(true);
|
||||
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
|
||||
try {
|
||||
runtime.dispose();
|
||||
org.junit.Assert.fail("Core reset failure must be reported");
|
||||
} catch (IllegalStateException expected) {
|
||||
assertEquals(false, runtime.isDisposed());
|
||||
}
|
||||
runtime.dispose();
|
||||
assertEquals(true, runtime.isDisposed());
|
||||
Mockito.verify(map, Mockito.times(2)).resetRasterArraySourceWeatherRuntime("source");
|
||||
Mockito.clearInvocations(map);
|
||||
runtime.dispose();
|
||||
Mockito.verifyNoInteractions(map);
|
||||
}
|
||||
|
||||
@Test public void disposeIsIdempotentAndRejectsFurtherControls() {
|
||||
MapLibreMap map = Mockito.mock(MapLibreMap.class);
|
||||
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
|
||||
|
||||
@@ -131,7 +131,8 @@ MLN_EXPORT
|
||||
@property (nonatomic, readonly) MLNParticleFieldRuntimeTargetFps targetFps;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (void)dispose;
|
||||
- (void)dispose; // Existing API; use the error-returning form for Bridge operations.
|
||||
- (BOOL)disposeWithError:(NSError * _Nullable * _Nullable)error;
|
||||
|
||||
- (MLNParticleHostFieldSubmitResult)submitWindFieldWithMetadataJSON:(NSString *)metadataJSON payload:(NSData *)payload error:(NSError * _Nullable * _Nullable)error;
|
||||
- (MLNParticleHostFieldSubmitResult)submitFieldWithMetadataJSON:(NSString *)metadataJSON scalar:(nullable NSData *)scalar vector:(nullable NSData *)vector validity:(NSData *)validity error:(NSError * _Nullable * _Nullable)error;
|
||||
@@ -196,7 +197,11 @@ MLN_EXPORT
|
||||
- (nullable MLNParticleFieldRuntime *)replaceParticleFieldRuntimeWithConfiguration:(MLNParticleFieldRuntimeConfiguration *)configuration
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
- (nullable MLNParticleFieldRuntime *)particleFieldRuntime;
|
||||
// Legacy public selector retained for existing Native consumers.
|
||||
- (void)disposeParticleFieldRuntime;
|
||||
// Completes V3+V4 Source cleanup before detaching; a failure leaves the runtime
|
||||
// associated so the caller may retry and must not be reported as success.
|
||||
- (BOOL)disposeParticleFieldRuntimeWithError:(NSError * _Nullable * _Nullable)error;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -138,11 +138,36 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
- (MLNParticleFieldRuntimePlaybackState)playbackState { return _playbackState; }
|
||||
- (MLNParticleFieldRuntimeTargetFps)targetFps { return _targetFps; }
|
||||
|
||||
- (void)dispose {
|
||||
- (BOOL)disposeWithError:(NSError **)error {
|
||||
if (_disposed) return YES;
|
||||
// A terminal dispose must clear the Source while the map and Style are valid.
|
||||
// Otherwise the Source outlives this ObjC runtime and retains V4 field windows.
|
||||
[_v4CadenceTimer invalidate];
|
||||
_v4CadenceTimer = nil;
|
||||
_v4ForecastPlaying = NO;
|
||||
const BOOL completed = [self withCurrentSource:error action:^bool(
|
||||
mbgl::style::RasterArraySource &source,
|
||||
mbgl::style::WeatherColorLayer &weather,
|
||||
mbgl::style::RasterParticleLayer &particle) {
|
||||
weather.setVisibility(mbgl::style::VisibilityType::None);
|
||||
particle.setVisibility(mbgl::style::VisibilityType::None);
|
||||
return source.resetWeatherRuntimeState();
|
||||
}];
|
||||
if (!completed) return NO;
|
||||
[_submittedEncodedFieldKeys removeAllObjects];
|
||||
_v4ParticleBindingFieldKey = nil;
|
||||
_playbackState = MLNParticleFieldRuntimePlaybackStatePause;
|
||||
_state = MLNParticleFieldRuntimeStateDisabled;
|
||||
_disposed = YES;
|
||||
_mapView = nil;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)dispose {
|
||||
NSError *error = nil;
|
||||
if (![self disposeWithError:&error]) {
|
||||
NSLog(@"WeatherRuntime: legacy dispose cleanup failed: %@", error.localizedDescription ?: @"unknown");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)refreshV4CadenceWake {
|
||||
@@ -355,11 +380,34 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
if (_disposed) return MLNParticleFieldRuntimeCommandResultRejected;
|
||||
if (enabled && _state == MLNParticleFieldRuntimeStateError) return MLNParticleFieldRuntimeCommandResultRejected;
|
||||
if (enabled && _state != MLNParticleFieldRuntimeStateDisabled) return MLNParticleFieldRuntimeCommandResultAccepted;
|
||||
__block bool accepted = false; BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &weather, mbgl::style::RasterParticleLayer &particle) {
|
||||
if (!enabled) { source.controlRuntimeTimeline(1); weather.setVisibility(mbgl::style::VisibilityType::None); particle.setVisibility(mbgl::style::VisibilityType::None); accepted = source.clearHostFields(); }
|
||||
else { weather.setVisibility(_weatherColorVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None); particle.setVisibility(_particleVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None); accepted = true; } return true;
|
||||
if (!enabled && _state == MLNParticleFieldRuntimeStateDisabled) return MLNParticleFieldRuntimeCommandResultAccepted;
|
||||
__block bool accepted = false;
|
||||
BOOL completed = [self withCurrentSource:error action:^bool(
|
||||
mbgl::style::RasterArraySource &source,
|
||||
mbgl::style::WeatherColorLayer &weather,
|
||||
mbgl::style::RasterParticleLayer &particle) {
|
||||
if (!enabled) {
|
||||
weather.setVisibility(mbgl::style::VisibilityType::None);
|
||||
particle.setVisibility(mbgl::style::VisibilityType::None);
|
||||
accepted = source.resetWeatherRuntimeState();
|
||||
} else {
|
||||
weather.setVisibility(_weatherColorVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None);
|
||||
particle.setVisibility(_particleVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None);
|
||||
accepted = true;
|
||||
}
|
||||
return accepted;
|
||||
}];
|
||||
if (completed && accepted) _state = enabled ? MLNParticleFieldRuntimeStateWaitingForFields : MLNParticleFieldRuntimeStateDisabled; else if (completed) _state = MLNParticleFieldRuntimeStateError;
|
||||
if (completed && accepted) {
|
||||
_state = enabled ? MLNParticleFieldRuntimeStateWaitingForFields : MLNParticleFieldRuntimeStateDisabled;
|
||||
if (!enabled) {
|
||||
[_submittedEncodedFieldKeys removeAllObjects];
|
||||
_v4ParticleBindingFieldKey = nil;
|
||||
_v4ForecastPlaying = NO;
|
||||
_playbackState = MLNParticleFieldRuntimePlaybackStatePause;
|
||||
}
|
||||
} else if (completed) {
|
||||
_state = MLNParticleFieldRuntimeStateError;
|
||||
}
|
||||
[self refreshV4CadenceWake];
|
||||
return commandResult(completed && accepted);
|
||||
}
|
||||
@@ -400,7 +448,7 @@ namespace { const void *ParticleFieldRuntimeAssociationKey = &ParticleFieldRunti
|
||||
@implementation MLNMapView (ParticleFieldRuntime)
|
||||
|
||||
- (MLNParticleFieldRuntime *)replaceParticleFieldRuntimeWithConfiguration:(MLNParticleFieldRuntimeConfiguration *)configuration error:(NSError **)error {
|
||||
[self disposeParticleFieldRuntime];
|
||||
if (![self disposeParticleFieldRuntimeWithError:error]) return nil;
|
||||
if (!configuration) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"A particle field runtime configuration is required."); return nil; }
|
||||
MLNParticleFieldRuntime *runtime = [[MLNParticleFieldRuntime alloc] initWithMapView:self configuration:configuration error:error];
|
||||
if (!runtime) return nil;
|
||||
@@ -413,9 +461,18 @@ namespace { const void *ParticleFieldRuntimeAssociationKey = &ParticleFieldRunti
|
||||
}
|
||||
|
||||
- (void)disposeParticleFieldRuntime {
|
||||
NSError *error = nil;
|
||||
if (![self disposeParticleFieldRuntimeWithError:&error]) {
|
||||
NSLog(@"WeatherRuntime: legacy map dispose cleanup failed: %@", error.localizedDescription ?: @"unknown");
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)disposeParticleFieldRuntimeWithError:(NSError **)error {
|
||||
MLNParticleFieldRuntime *runtime = self.particleFieldRuntime;
|
||||
[runtime dispose];
|
||||
if (!runtime) return YES;
|
||||
if (![runtime disposeWithError:error]) return NO;
|
||||
objc_setAssociatedObject(self, ParticleFieldRuntimeAssociationKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -535,6 +535,37 @@ void RasterArraySource::clearAllEncodedFields() {
|
||||
refreshV4RendererState();
|
||||
}
|
||||
|
||||
bool RasterArraySource::resetWeatherRuntimeState() {
|
||||
// V3 Host ingress and V4 QBIN can coexist on the same raster-array Source.
|
||||
// Clearing the V3 window alone would leave the independent V4 store/projection.
|
||||
// Pause first so Source notifications during cleanup cannot advance the clock.
|
||||
forecastClock.pause();
|
||||
lastForecastTick = {};
|
||||
if (impl().hostFieldSubmission && !clearHostFields()) return false;
|
||||
|
||||
const auto oldFieldCount = encodedFieldStore.fieldCount();
|
||||
const auto oldDecodedBytes = encodedFieldStore.decodedByteSize();
|
||||
const auto hadColorProjection = static_cast<bool>(impl().v4RendererState.color);
|
||||
const auto hadParticleProjection = static_cast<bool>(impl().v4RendererState.particle);
|
||||
encodedFieldStore.clearAll();
|
||||
colorBinding.reset();
|
||||
particleBinding.reset();
|
||||
forecastClock = particle::ForecastClock{};
|
||||
lastForecastTick = {};
|
||||
refreshV4RendererState();
|
||||
Log::Info(Event::Render,
|
||||
"WeatherRuntime: event=V4_RUNTIME_RESET sourceId=" + getID() +
|
||||
" oldFieldCount=" + std::to_string(oldFieldCount) +
|
||||
" oldDecodedBytes=" + std::to_string(oldDecodedBytes) +
|
||||
" oldColorProjection=" + std::to_string(hadColorProjection) +
|
||||
" oldParticleProjection=" + std::to_string(hadParticleProjection) +
|
||||
" remainingFieldCount=" + std::to_string(encodedFieldStore.fieldCount()) +
|
||||
" remainingDecodedBytes=" + std::to_string(encodedFieldStore.decodedByteSize()) +
|
||||
" remainingColorProjection=" + std::to_string(static_cast<bool>(impl().v4RendererState.color)) +
|
||||
" remainingParticleProjection=" + std::to_string(static_cast<bool>(impl().v4RendererState.particle)));
|
||||
return true;
|
||||
}
|
||||
|
||||
void RasterArraySource::refreshV4RendererState() {
|
||||
Impl::V4RendererState next;
|
||||
const auto& current = impl().v4RendererState;
|
||||
|
||||
@@ -912,3 +912,60 @@ TEST(Source, RasterArrayV4MaterializerDecodesDirectionToUInt16LittleEndian) {
|
||||
EXPECT_NEAR(materialized->t0->vector[7], 0.0f, 1e-5f);
|
||||
EXPECT_EQ(materialized->t0->vectorSemantics, particle::VectorSemantics::Direction);
|
||||
}
|
||||
|
||||
|
||||
// The RN weather source uses Host V3 ingress and V4 QBIN on one Style source.
|
||||
// A terminal detach must clear both, drop old bindings, and permit a new owner.
|
||||
TEST(Source, RasterArrayV4TerminalResetClearsBothIngressesAndAllowsNewGeneration) {
|
||||
style::RasterArraySource source{"v4-detach-reset"};
|
||||
auto v3T0 = particle::OfflineFixtureParticleAdapter::load("uniform-east", 71);
|
||||
auto v3T1 = particle::OfflineFixtureParticleAdapter::load("uniform-north", 72);
|
||||
v3T1.validTime = v3T0.validTime + Hour;
|
||||
ASSERT_EQ(source.submitInitialRuntimeFieldWindow(v3T0, v3T1),
|
||||
style::RasterArrayRuntimeFieldUpdateResult::Applied);
|
||||
ASSERT_TRUE(source.impl().field);
|
||||
ASSERT_TRUE(source.impl().nextField);
|
||||
|
||||
submit(source, "wind", "wind", Base, 1);
|
||||
submit(source, "wind", "wind", Base + Hour, 2);
|
||||
source.setColorBinding({"wind", {1, 1, particle::FieldComponent::UEast},
|
||||
particle::FieldValueClass::VelocityComponent, 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));
|
||||
ASSERT_TRUE(source.impl().v4RendererState.color);
|
||||
ASSERT_TRUE(source.impl().v4RendererState.particle);
|
||||
ASSERT_EQ(source.impl().v4RendererState.color->sampleState.mix, 0.5f);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle->sampleState.mix, 0.5f);
|
||||
source.setForecastPlaybackState(particle::ForecastPlaybackState::Playing);
|
||||
ASSERT_EQ(source.impl().hostFieldSubmission, true);
|
||||
|
||||
ASSERT_TRUE(source.resetWeatherRuntimeState());
|
||||
EXPECT_FALSE(source.impl().field);
|
||||
EXPECT_FALSE(source.impl().nextField);
|
||||
EXPECT_FALSE(source.impl().timeline);
|
||||
EXPECT_EQ(source.impl().v4RendererState.color, nullptr);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle, nullptr);
|
||||
EXPECT_FALSE(source.impl().v4RendererState.colorInput);
|
||||
EXPECT_FALSE(source.impl().v4RendererState.particleInput);
|
||||
EXPECT_EQ(source.resolveColorProjection().error, particle::BindingProjectionError::InvalidBinding);
|
||||
EXPECT_EQ(source.resolveParticleProjection().error, particle::BindingProjectionError::InvalidBinding);
|
||||
EXPECT_EQ(source.impl().v4RendererState.color, nullptr);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle, nullptr);
|
||||
EXPECT_FALSE(source.tickForecastClock(std::chrono::steady_clock::now()));
|
||||
EXPECT_TRUE(source.resetWeatherRuntimeState()); // Idempotent.
|
||||
|
||||
submit(source, "wind", "wind", Base, 3);
|
||||
submit(source, "wind", "wind", Base + Hour, 4);
|
||||
source.setColorBinding({"wind", {1, 1, particle::FieldComponent::UEast},
|
||||
particle::FieldValueClass::VelocityComponent, 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));
|
||||
EXPECT_TRUE(source.resolveColorProjection().accepted());
|
||||
EXPECT_TRUE(source.resolveParticleProjection().accepted());
|
||||
EXPECT_TRUE(source.impl().v4RendererState.color);
|
||||
EXPECT_TRUE(source.impl().v4RendererState.particle);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user