fix: reset V17 weather runtime across Android and iOS
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-09-28 21:37:16 +09:00
parent 1e7215e2a0
commit baf9374318
14 changed files with 276 additions and 17 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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