Skip to content

Commit f5cd25d

Browse files
pengdevgithub-actions[bot]
authored andcommitted
[maps-sdk] Cache Vulkan resize dimensions when VulkanManager is null (#12554)
GitOrigin-RevId: 209ec74526daf63b2d868cc78a5bff342851dc35
1 parent f7fbc32 commit f5cd25d

3 files changed

Lines changed: 276 additions & 11 deletions

File tree

maps-sdk/src/main/java/com/mapbox/maps/renderer/VulkanMapboxRenderThread.kt

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,67 @@
11
package com.mapbox.maps.renderer
22

3+
import android.annotation.SuppressLint
34
import android.view.Surface
5+
import androidx.annotation.VisibleForTesting
46
import com.mapbox.maps.IVulkanManager
57
import com.mapbox.maps.RenderCallback
68
import com.mapbox.maps.logI
79
import com.mapbox.maps.logW
10+
import java.util.concurrent.locks.Condition
11+
import java.util.concurrent.locks.ReentrantLock
812

913
/**
1014
* Vulkan-based implementation of MapboxRenderThread.
1115
*/
12-
internal class VulkanMapboxRenderThread(
13-
mapboxRenderer: MapboxRenderer,
14-
private val antialiasingSampleCount: Int,
15-
mapName: String,
16-
) :
17-
MapboxRenderThread(
16+
internal class VulkanMapboxRenderThread : MapboxRenderThread {
17+
18+
private val antialiasingSampleCount: Int
19+
private var nativeVulkanManager: IVulkanManager? = null
20+
private val surfaceWrapper: SurfaceWrapper = SurfaceWrapper()
21+
22+
/**
23+
* Latest resize dimensions received before [nativeVulkanManager] was available.
24+
* Drained by [applyCachedResize] once the manager is obtained.
25+
*
26+
* Thread-confined to the render handler thread — both [resize] and
27+
* [prepareRenderer] are dispatched through it, so no synchronization needed.
28+
*/
29+
private var cachedSize: Pair<Int, Int>? = null
30+
31+
constructor(
32+
mapboxRenderer: MapboxRenderer,
33+
antialiasingSampleCount: Int,
34+
mapName: String,
35+
) : super(
1836
mapboxRenderer = mapboxRenderer,
1937
widgetRenderer = null,
2038
mapName = mapName,
2139
rendererName = "Vulkan"
2240
) {
23-
24-
private var nativeVulkanManager: IVulkanManager? = null
25-
private val surfaceWrapper: SurfaceWrapper = SurfaceWrapper()
41+
this.antialiasingSampleCount = antialiasingSampleCount
42+
}
43+
44+
@SuppressLint("VisibleForTests")
45+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
46+
constructor(
47+
mapboxRenderer: MapboxRenderer,
48+
antialiasingSampleCount: Int,
49+
handlerThread: RenderHandlerThread,
50+
fpsManager: FpsManager,
51+
surfaceProcessingLock: ReentrantLock,
52+
createCondition: Condition,
53+
destroyCondition: Condition,
54+
) : super(
55+
mapboxRenderer,
56+
null,
57+
handlerThread,
58+
fpsManager,
59+
surfaceProcessingLock,
60+
createCondition,
61+
destroyCondition
62+
) {
63+
this.antialiasingSampleCount = antialiasingSampleCount
64+
}
2665

2766
init {
2867
logI(TAG, "VulkanMapboxRenderThread created")
@@ -42,6 +81,8 @@ internal class VulkanMapboxRenderThread(
4281
nativeVulkanManager = mapboxRenderer.map?.getVulkanManager()
4382
if (nativeVulkanManager == null) {
4483
logW(TAG, "Failed to obtain VulkanManager - Vulkan rendering will not be available")
84+
} else {
85+
applyCachedResize()
4586
}
4687
}
4788
return nativeVulkanManager != null
@@ -110,11 +151,37 @@ internal class VulkanMapboxRenderThread(
110151
// no-op for now
111152
}
112153

154+
@RenderThread
113155
override fun resize(width: Int, height: Int) {
114-
// TODO cache width/height if NULL
115-
nativeVulkanManager?.resize(width, height)
156+
val mgr = nativeVulkanManager
157+
if (mgr != null) {
158+
mgr.resize(width, height)
159+
} else {
160+
cachedSize = width to height
161+
}
116162
}
117163

164+
/**
165+
* Forwards the cached pre-init resize to the manager and clears the cache.
166+
*
167+
* Apply-once is intentional: once [nativeVulkanManager] is non-null,
168+
* subsequent [resize] calls go directly to the manager and never repopulate
169+
* [cachedSize]. [prepareRenderer] also short-circuits while the manager is
170+
* non-null, so this method does not re-enter. If a manager destroy/re-init
171+
* lifecycle is added, this contract needs revisiting.
172+
*/
173+
@RenderThread
174+
private fun applyCachedResize() {
175+
cachedSize?.let { (w, h) ->
176+
logI(TAG, "Applying cached resize($w, $h)")
177+
nativeVulkanManager?.resize(w, h)
178+
cachedSize = null
179+
}
180+
}
181+
182+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
183+
internal fun prepareRendererForTest(): Boolean = prepareRenderer()
184+
118185
override fun flushCommands() {
119186
// no-op for now
120187
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.mapbox.maps.renderer
2+
3+
import org.robolectric.annotation.Implementation
4+
import org.robolectric.annotation.Implements
5+
6+
@Implements(SurfaceWrapper::class)
7+
public class ShadowSurfaceWrapper {
8+
@Implementation
9+
fun __constructor__() {
10+
// no-op: skip native initialize()
11+
}
12+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package com.mapbox.maps.renderer
2+
3+
import android.util.Log
4+
import com.mapbox.maps.IVulkanManager
5+
import com.mapbox.maps.NativeMapImpl
6+
import com.mapbox.maps.logI
7+
import com.mapbox.maps.logW
8+
import com.mapbox.maps.shadows.ShadowLogThrottler
9+
import com.mapbox.verifyNo
10+
import com.mapbox.verifyOnce
11+
import io.mockk.every
12+
import io.mockk.mockk
13+
import io.mockk.mockkStatic
14+
import io.mockk.unmockkAll
15+
import io.mockk.unmockkStatic
16+
import io.mockk.verify
17+
import io.mockk.verifyOrder
18+
import org.junit.After
19+
import org.junit.Assert.assertFalse
20+
import org.junit.Assert.assertTrue
21+
import org.junit.Test
22+
import org.junit.runner.RunWith
23+
import org.robolectric.RobolectricTestRunner
24+
import org.robolectric.annotation.Config
25+
import org.robolectric.annotation.LooperMode
26+
import org.robolectric.shadows.ShadowLog
27+
import java.util.concurrent.locks.ReentrantLock
28+
29+
@RunWith(RobolectricTestRunner::class)
30+
@Config(
31+
shadows = [
32+
ShadowLogThrottler::class,
33+
ShadowSurfaceWrapper::class
34+
]
35+
)
36+
@LooperMode(LooperMode.Mode.PAUSED)
37+
class VulkanMapboxRenderThreadTest {
38+
39+
private lateinit var mapboxRenderer: MapboxRenderer
40+
private lateinit var vulkanManager: IVulkanManager
41+
private lateinit var nativeMap: NativeMapImpl
42+
private lateinit var renderThread: VulkanMapboxRenderThread
43+
private lateinit var renderHandlerThread: RenderHandlerThread
44+
private lateinit var fpsManager: FpsManager
45+
46+
private fun initRenderThread() {
47+
mapboxRenderer = mockk(relaxUnitFun = true)
48+
vulkanManager = mockk(relaxUnitFun = true)
49+
nativeMap = mockk(relaxUnitFun = true)
50+
51+
renderHandlerThread = RenderHandlerThread(mapName = "")
52+
renderHandlerThread.start()
53+
54+
fpsManager = mockk(relaxUnitFun = true)
55+
every { fpsManager.preRender(any()) } returns true
56+
57+
val surfaceProcessingLock = ReentrantLock()
58+
59+
renderThread = VulkanMapboxRenderThread(
60+
mapboxRenderer = mapboxRenderer,
61+
antialiasingSampleCount = 1,
62+
handlerThread = renderHandlerThread,
63+
fpsManager = fpsManager,
64+
surfaceProcessingLock = surfaceProcessingLock,
65+
createCondition = surfaceProcessingLock.newCondition(),
66+
destroyCondition = surfaceProcessingLock.newCondition(),
67+
)
68+
69+
mockkStatic("com.mapbox.maps.MapboxLogger")
70+
ShadowLog.stream = System.out
71+
every { logI(any(), any()) } answers { Log.i(firstArg(), secondArg()) }
72+
every { logI(any(), any(), any()) } answers { Log.i(firstArg(), secondArg()) }
73+
every { logW(any(), any()) } answers { Log.w(firstArg<String>(), secondArg<String>()) }
74+
}
75+
76+
private fun setupVulkanManagerAvailable() {
77+
every { mapboxRenderer.map } returns nativeMap
78+
every { nativeMap.getVulkanManager() } returns vulkanManager
79+
}
80+
81+
private fun setupVulkanManagerUnavailable() {
82+
every { mapboxRenderer.map } returns nativeMap
83+
every { nativeMap.getVulkanManager() } returns null
84+
}
85+
86+
@After
87+
fun cleanup() {
88+
if (::renderHandlerThread.isInitialized) {
89+
renderHandlerThread.stop()
90+
}
91+
unmockkStatic("com.mapbox.maps.MapboxLogger")
92+
unmockkAll()
93+
}
94+
95+
@Test
96+
fun resizeForwardsToVulkanManagerWhenAvailable() {
97+
initRenderThread()
98+
setupVulkanManagerAvailable()
99+
assertTrue(renderThread.prepareRendererForTest())
100+
101+
renderThread.resize(800, 600)
102+
103+
verifyOnce { vulkanManager.resize(800, 600) }
104+
}
105+
106+
@Test
107+
fun resizeCachesDimensionsWhenVulkanManagerNull() {
108+
initRenderThread()
109+
setupVulkanManagerUnavailable()
110+
assertFalse(renderThread.prepareRendererForTest())
111+
112+
renderThread.resize(800, 600)
113+
114+
verifyNo { vulkanManager.resize(any(), any()) }
115+
}
116+
117+
@Test
118+
fun cachedResizeAppliedWhenVulkanManagerBecomesAvailable() {
119+
initRenderThread()
120+
setupVulkanManagerUnavailable()
121+
renderThread.prepareRendererForTest()
122+
renderThread.resize(800, 600)
123+
124+
setupVulkanManagerAvailable()
125+
renderThread.prepareRendererForTest()
126+
127+
verifyOnce { vulkanManager.resize(800, 600) }
128+
}
129+
130+
@Test
131+
fun lastResizeWinsWhenMultipleCached() {
132+
initRenderThread()
133+
setupVulkanManagerUnavailable()
134+
renderThread.prepareRendererForTest()
135+
136+
renderThread.resize(800, 600)
137+
renderThread.resize(1024, 768)
138+
renderThread.resize(1920, 1080)
139+
140+
setupVulkanManagerAvailable()
141+
renderThread.prepareRendererForTest()
142+
143+
verifyOnce { vulkanManager.resize(1920, 1080) }
144+
verifyNo { vulkanManager.resize(800, 600) }
145+
verifyNo { vulkanManager.resize(1024, 768) }
146+
}
147+
148+
@Test
149+
fun cachedResizeClearedAfterApplying() {
150+
initRenderThread()
151+
setupVulkanManagerUnavailable()
152+
renderThread.prepareRendererForTest()
153+
renderThread.resize(800, 600)
154+
155+
setupVulkanManagerAvailable()
156+
renderThread.prepareRendererForTest()
157+
verifyOnce { vulkanManager.resize(800, 600) }
158+
159+
renderThread.prepareRendererForTest()
160+
verify(exactly = 1) { vulkanManager.resize(800, 600) }
161+
}
162+
163+
@Test
164+
fun noCachedResizeWhenNeverResizedBeforeVulkanManager() {
165+
initRenderThread()
166+
setupVulkanManagerAvailable()
167+
renderThread.prepareRendererForTest()
168+
169+
verifyNo { vulkanManager.resize(any(), any()) }
170+
}
171+
172+
@Test
173+
fun resizeAfterVulkanManagerAvailableGoesDirectly() {
174+
initRenderThread()
175+
setupVulkanManagerAvailable()
176+
renderThread.prepareRendererForTest()
177+
178+
renderThread.resize(800, 600)
179+
renderThread.resize(1024, 768)
180+
181+
verifyOrder {
182+
vulkanManager.resize(800, 600)
183+
vulkanManager.resize(1024, 768)
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)