Skip to content

Instantly share code, notes, and snippets.

@sargunv
Created September 8, 2026 06:29
Show Gist options
  • Select an option

  • Save sargunv/7694acb12497cac7cb834695dd926885 to your computer and use it in GitHub Desktop.

Select an option

Save sargunv/7694acb12497cac7cb834695dd926885 to your computer and use it in GitHub Desktop.
Native synchronous GeoJSON icon disappearance: desktop reproduction harness and results

Apply marker-zoom-reproduction.patch in maplibre/maplibre-compose at d3f2a06356717c2120aec285eed98f1d301b9ee4.

Run mise exec -- ./gradlew :lib:maplibre-compose:jvmTest --tests "*MarkerZoomDiagnosticTest". PNGs and native layer JSON are written to lib/maplibre-compose/build/marker-zoom-diagnostic/. This is an observational harness: test completion does not mean rendering is correct. Initial captures may precede glyph loading; compare the subsequent zoom captures. Glyphs load from the public MapLibre demo font endpoint.

AI assistance: OpenAI GPT-6 through Codex prepared this diagnostic harness and report.

diff --git a/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/mlnffi/ProductionBridgeTestRenderDriver.kt b/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/mlnffi/ProductionBridgeTestRenderDriver.kt
index 231c4c3b5..bdc8a1404 100644
--- a/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/mlnffi/ProductionBridgeTestRenderDriver.kt
+++ b/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/mlnffi/ProductionBridgeTestRenderDriver.kt
@@ -106,6 +106,8 @@ private constructor(
override fun readPixel(x: Int, y: Int): RgbaPixel = environment.readPixel(x, y)
+ fun capturePng(): ByteArray = environment.capturePng()
+
override fun discardPresentedFrame() {
environment.discardPresentedFrame()
}
@@ -184,6 +186,16 @@ private abstract class DesktopTestGpuEnvironment : AutoCloseable {
drew
}
+ fun capturePng(): ByteArray = withContext {
+ Bitmap().use { bitmap ->
+ check(bitmap.allocN32Pixels(destinationWidth, destinationHeight))
+ check(checkNotNull(destination).readPixels(bitmap, 0, 0))
+ org.jetbrains.skia.Image.makeFromBitmap(bitmap).use { image ->
+ checkNotNull(image.encodeToData()).use { it.bytes }
+ }
+ }
+ }
+
fun readPixel(x: Int, y: Int): RgbaPixel = withContext {
val surface = checkNotNull(destination) { "No production bridge frame has been presented" }
require(x in 0 until destinationWidth && y in 0 until destinationHeight) {
diff --git a/lib/maplibre-compose/src/maplibreNativeTest/kotlin/org/maplibre/compose/mlnffi/BridgeMapFixture.kt b/lib/maplibre-compose/src/maplibreNativeTest/kotlin/org/maplibre/compose/mlnffi/BridgeMapFixture.kt
index c89af2d64..3216a7219 100644
--- a/lib/maplibre-compose/src/maplibreNativeTest/kotlin/org/maplibre/compose/mlnffi/BridgeMapFixture.kt
+++ b/lib/maplibre-compose/src/maplibreNativeTest/kotlin/org/maplibre/compose/mlnffi/BridgeMapFixture.kt
@@ -34,7 +34,7 @@ import org.maplibre.compose.testing.RgbaPixel
*/
internal class BridgeMapFixture
private constructor(
- private val driver: FfiTestRenderDriver,
+ internal val driver: FfiTestRenderDriver,
private val cacheFile: Path,
private val initialExtent: MapExtent,
resourceConfig: MapResourceConfig,
diff --git a/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/layers/MarkerZoomDiagnosticTest.kt b/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/layers/MarkerZoomDiagnosticTest.kt
new file mode 100644
index 000000000..a7c24cc8b
--- /dev/null
+++ b/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/layers/MarkerZoomDiagnosticTest.kt
@@ -0,0 +1,101 @@
+package org.maplibre.compose.layers
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import java.io.File
+import javax.imageio.ImageIO
+import kotlin.test.Test
+import kotlinx.serialization.json.JsonPrimitive
+import org.maplibre.compose.camera.CameraPosition
+import org.maplibre.compose.expressions.dsl.const
+import org.maplibre.compose.expressions.dsl.image
+import org.maplibre.compose.expressions.value.SymbolAnchor
+import org.maplibre.compose.mlnffi.ProductionBridgeTestRenderDriver
+import org.maplibre.compose.sources.GeoJsonData
+import org.maplibre.compose.sources.GeoJsonOptions
+import org.maplibre.compose.sources.rememberGeoJsonSource
+import org.maplibre.compose.style.BaseStyle
+import org.maplibre.compose.style.LayerPropertyKind
+import org.maplibre.compose.testing.MlnFfiMapFixture
+import org.maplibre.compose.testing.createMapFixture
+import org.maplibre.compose.testing.declare
+import org.maplibre.compose.testing.runMapTest
+import org.maplibre.spatialk.geojson.Feature
+import org.maplibre.spatialk.geojson.Point
+import org.maplibre.spatialk.geojson.Position
+
+class MarkerZoomDiagnosticTest {
+ @Test
+ fun capture_icons_across_zoom_and_layout_changes() = runMapTest {
+ val output = File("build/marker-zoom-diagnostic").apply { mkdirs() }
+ for (synchronous in listOf(false, true)) for (withText in listOf(false, true)) {
+ (createMapFixture() as MlnFfiMapFixture).use { fixture ->
+ fixture.loadStyle(
+ BaseStyle.Json(
+ """{"version":8,"glyphs":"https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf","sources":{},"layers":[{"id":"bg","type":"background","paint":{"background-color":"#ffffff"}}]}"""
+ )
+ )
+ fixture.state.setCameraPosition(CameraPosition(target = Position(0.0, 0.0), zoom = 14.0))
+ fixture.declare {
+ for (index in 0..2) {
+ val source =
+ rememberGeoJsonSource(
+ GeoJsonData.Features(
+ Feature(geometry = Point(Position((index - 1) * 0.005, 0.0)), properties = null)
+ ),
+ GeoJsonOptions(synchronousUpdate = synchronous),
+ )
+ CircleLayer("shadow-$index", source, radius = const(8.dp), color = const(Color.Black))
+ SymbolLayer(
+ "pin-$index",
+ source,
+ iconImage = image(ColorPainter(Color.Red), size = DpSize(40.dp, 48.dp)),
+ iconAnchor = const(SymbolAnchor.Bottom),
+ iconSize = const(1f),
+ textField = const(if (withText) "New place" else ""),
+ textFont = const(listOf("Noto Sans Regular")),
+ textAnchor = const(SymbolAnchor.Top),
+ iconAllowOverlap = const(true),
+ iconIgnorePlacement = const(true),
+ textAllowOverlap = const(true),
+ textIgnorePlacement = const(true),
+ )
+ }
+ }
+ suspend fun capture(label: String) {
+ fixture.pump(40)
+ val file = File(output, "sync-$synchronous-text-$withText-$label.png")
+ file.writeBytes((fixture.bridge.driver as ProductionBridgeTestRenderDriver).capturePng())
+ val bitmap = ImageIO.read(file)
+ var red = 0
+ for (y in 0 until bitmap.height) for (x in 0 until bitmap.width) {
+ val color = bitmap.getRGB(x, y)
+ if (
+ (color ushr 16 and 255) > 200 && (color ushr 8 and 255) < 50 && (color and 255) < 50
+ )
+ red++
+ }
+ println(
+ "MARKER_DIAGNOSTIC sync=$synchronous text=$withText stage=$label redPixels=$red imagePresent=${fixture.style!!.imageExists("__MAPLIBRE_COMPOSE_image_0")} errors=${fixture.errors}"
+ )
+ File(output, "sync-$synchronous-text-$withText-$label.json")
+ .writeText(checkNotNull(fixture.style).getLayer("pin-0")!!.toJson().toString())
+ }
+ capture("initial")
+ for (zoom in listOf(13.5, 13.0, 12.5, 12.0, 11.0, 10.0, 8.0, 6.0, 14.0)) {
+ fixture.state.setCameraPosition(CameraPosition(target = Position(0.0, 0.0), zoom = zoom))
+ capture("zoom-$zoom")
+ }
+ fixture.style!!.setLayerProperty(
+ "pin-0",
+ "icon-size",
+ JsonPrimitive(1.12),
+ LayerPropertyKind.LAYOUT,
+ )
+ capture("animated")
+ }
+ }
+ }
+}
MARKER_DIAGNOSTIC sync=false text=false stage=initial redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=false stage=zoom-13.5 redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=false stage=zoom-13.0 redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=false stage=zoom-12.5 redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=false stage=zoom-12.0 redPixels=4704 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=false stage=zoom-11.0 redPixels=3360 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=false stage=zoom-10.0 redPixels=2592 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=false stage=zoom-8.0 redPixels=2112 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=false stage=zoom-6.0 redPixels=1920 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=false stage=zoom-14.0 redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=false stage=animated redPixels=6225 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=true stage=initial redPixels=18 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=true stage=zoom-13.5 redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=true stage=zoom-13.0 redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=true stage=zoom-12.5 redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=true stage=zoom-12.0 redPixels=4704 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=true stage=zoom-11.0 redPixels=3360 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=true stage=zoom-10.0 redPixels=2592 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=true stage=zoom-8.0 redPixels=2112 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=true stage=zoom-6.0 redPixels=1920 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=true stage=zoom-14.0 redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=false text=true stage=animated redPixels=6225 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=false stage=initial redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=false stage=zoom-13.5 redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=false stage=zoom-13.0 redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=false stage=zoom-12.5 redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=false stage=zoom-12.0 redPixels=4704 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=false stage=zoom-11.0 redPixels=3360 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=false stage=zoom-10.0 redPixels=2592 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=false stage=zoom-8.0 redPixels=2112 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=false stage=zoom-6.0 redPixels=1920 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=false stage=zoom-14.0 redPixels=5760 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=false stage=animated redPixels=6225 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=true stage=initial redPixels=0 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=true stage=zoom-13.5 redPixels=0 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=true stage=zoom-13.0 redPixels=0 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=true stage=zoom-12.5 redPixels=0 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=true stage=zoom-12.0 redPixels=0 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=true stage=zoom-11.0 redPixels=0 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=true stage=zoom-10.0 redPixels=0 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=true stage=zoom-8.0 redPixels=0 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=true stage=zoom-6.0 redPixels=0 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=true stage=zoom-14.0 redPixels=0 imagePresent=true errors=[]
MARKER_DIAGNOSTIC sync=true text=true stage=animated redPixels=2385 imagePresent=true errors=[]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment