Skip to content

Commit 0f8c086

Browse files
committed
feat: separate gl thread state from the surface's composition state (#21)
1 parent 32c6335 commit 0f8c086

File tree

2 files changed

+129
-54
lines changed

2 files changed

+129
-54
lines changed

src/main/java/dev/silenium/compose/gl/surface/GLSurfaceView.kt renamed to src/main/java/dev/silenium/compose/gl/surface/GLSurface.kt

Lines changed: 89 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -47,39 +47,99 @@ data class FBOSizeOverride(
4747

4848
/**
4949
* A composable that displays OpenGL content.
50-
* @param state The state of the GLSurfaceView.
51-
* @param modifier The modifier to apply to the GLSurfaceView.
50+
* @param state The state of the [GLSurface].
51+
* @param modifier The modifier to apply to the [GLSurface].
5252
* @param paint The paint to draw the contents on the compose scene.
5353
* @param glContextProvider The provider of the OpenGL context (default: [GLContextProviderFactory.detected]).
54-
* @param presentMode The present mode of the GLSurfaceView (default: [GLSurfaceView.PresentMode.FIFO]).
55-
*
54+
* @param presentMode The present mode of the GLSurfaceView (default: [GLSurface.PresentMode.FIFO]).
55+
* @param swapChainSize The size of the swap chain (default: 10).
56+
* @param fboSizeOverride The size override of the FBO (default: null).
57+
* @param cleanup The cleanup block to run when the [GLSurface] is destroyed (default: {}).
58+
* @param draw The draw block to render the OpenGL content.
59+
* @see [GLDrawScope]
5660
*/
5761
@Composable
5862
fun GLSurfaceView(
5963
state: GLSurfaceState = rememberGLSurfaceState(),
6064
modifier: Modifier = Modifier,
6165
paint: Paint = Paint(),
6266
glContextProvider: GLContextProvider<*> = GLContextProviderFactory.detected,
63-
presentMode: GLSurfaceView.PresentMode = GLSurfaceView.PresentMode.FIFO,
67+
presentMode: GLSurface.PresentMode = GLSurface.PresentMode.FIFO,
6468
swapChainSize: Int = 10,
6569
fboSizeOverride: FBOSizeOverride? = null,
6670
cleanup: () -> Unit = {},
6771
draw: GLDrawScope.() -> Unit,
6872
) {
69-
var invalidations by remember { mutableStateOf(0) }
70-
val surfaceView = remember {
73+
val surfaceView = rememberGLSurface(
74+
state = state,
75+
glContextProvider = glContextProvider,
76+
presentMode = presentMode,
77+
swapChainSize = swapChainSize,
78+
fboSizeOverride = fboSizeOverride,
79+
cleanup = cleanup,
80+
draw = draw,
81+
)
82+
GLSurfaceView(surfaceView, modifier, paint)
83+
}
84+
85+
/**
86+
* A composable that remembers a [GLSurface].
87+
* @param state The state of the [GLSurface].
88+
* @param glContextProvider The provider of the OpenGL context (default: [GLContextProviderFactory.detected]).
89+
* @param presentMode The present mode of the GLSurfaceView (default: [GLSurface.PresentMode.FIFO]).
90+
* @param swapChainSize The size of the swap chain (default: 10).
91+
* @param fboSizeOverride The size override of the FBO (default: null).
92+
* @param cleanup The cleanup block to run when the [GLSurface] is destroyed (default: {}).
93+
* @param draw The draw block to render the OpenGL content.
94+
*/
95+
@Composable
96+
fun rememberGLSurface(
97+
state: GLSurfaceState = rememberGLSurfaceState(),
98+
glContextProvider: GLContextProvider<*> = GLContextProviderFactory.detected,
99+
presentMode: GLSurface.PresentMode = GLSurface.PresentMode.FIFO,
100+
swapChainSize: Int = 10,
101+
fboSizeOverride: FBOSizeOverride? = null,
102+
cleanup: () -> Unit = {},
103+
draw: GLDrawScope.() -> Unit,
104+
): GLSurface {
105+
val surfaceView = remember(state, glContextProvider, presentMode, swapChainSize, cleanup, draw) {
71106
val currentContext = glContextProvider.fromCurrent() ?: error("No current EGL context")
72-
GLSurfaceView(
107+
GLSurface(
73108
state = state,
74109
parentContext = currentContext,
75-
invalidate = { invalidations++ },
76-
paint = paint,
77110
presentMode = presentMode,
78111
swapChainSize = swapChainSize,
79112
cleanupBlock = cleanup,
80113
drawBlock = draw,
114+
fboSizeOverride = fboSizeOverride,
81115
)
82116
}
117+
DisposableEffect(surfaceView) {
118+
surfaceView.launch()
119+
onDispose {
120+
surfaceView.interrupt()
121+
}
122+
}
123+
LaunchedEffect(fboSizeOverride) {
124+
surfaceView.fboSizeOverride = fboSizeOverride
125+
fboSizeOverride?.size?.let(surfaceView::resize)
126+
}
127+
128+
return surfaceView
129+
}
130+
131+
/**
132+
* A composable that displays OpenGL content.
133+
* @param surface The [GLSurface] to display.
134+
* @param modifier The modifier to apply to the [GLSurface].
135+
* @param paint The paint to draw the contents on the compose scene.
136+
*/
137+
@Composable
138+
fun GLSurfaceView(
139+
surface: GLSurface,
140+
modifier: Modifier = Modifier,
141+
paint: Paint = Paint(),
142+
) {
83143
val window = LocalWindow.current
84144
var directContext by remember { mutableStateOf<DirectContext?>(null) }
85145
LaunchedEffect(window) {
@@ -96,19 +156,20 @@ fun GLSurfaceView(
96156
Canvas(
97157
modifier = Modifier
98158
.onSizeChanged {
99-
if (fboSizeOverride == null) {
100-
surfaceView.resize(it)
159+
if (surface.fboSizeOverride == null) {
160+
surface.resize(it)
101161
}
102162
}.let {
103-
if (fboSizeOverride != null) {
163+
val override = surface.fboSizeOverride
164+
if (override != null) {
104165
it.matchParentSize()
105166
.drawWithContent {
106-
val xScale = size.width / fboSizeOverride.width
107-
val yScale = size.height / fboSizeOverride.height
167+
val xScale = size.width / override.width
168+
val yScale = size.height / override.height
108169
val scale = minOf(xScale, yScale)
109170
translate(
110-
(size.width - fboSizeOverride.width * scale) * fboSizeOverride.transformOrigin.pivotFractionX,
111-
(size.height - fboSizeOverride.height * scale) * fboSizeOverride.transformOrigin.pivotFractionY,
171+
(size.width - override.width * scale) * override.transformOrigin.pivotFractionX,
172+
(size.height - override.height * scale) * override.transformOrigin.pivotFractionY,
112173
) {
113174
scale(scale, Offset.Zero) {
114175
this@drawWithContent.drawContent()
@@ -120,34 +181,23 @@ fun GLSurfaceView(
120181
}
121182
}
122183
) {
123-
invalidations.let {
184+
surface.invalidations.let {
124185
directContext?.let { directContext ->
125-
surfaceView.display(drawContext.canvas.nativeCanvas, directContext)
186+
surface.display(drawContext.canvas.nativeCanvas, directContext, paint)
126187
}
127188
}
128189
}
129190
}
130-
DisposableEffect(surfaceView) {
131-
surfaceView.launch()
132-
onDispose {
133-
surfaceView.interrupt()
134-
// surfaceView.join()
135-
}
136-
}
137-
LaunchedEffect(fboSizeOverride) {
138-
fboSizeOverride?.size?.let(surfaceView::resize)
139-
}
140191
}
141192

142-
class GLSurfaceView internal constructor(
193+
class GLSurface internal constructor(
143194
private val state: GLSurfaceState,
144195
private val parentContext: GLContext<*>,
145196
private val drawBlock: GLDrawScope.() -> Unit,
146197
private val cleanupBlock: () -> Unit = {},
147-
private val invalidate: () -> Unit = {},
148-
private val paint: Paint = Paint(),
149198
private val presentMode: PresentMode = PresentMode.MAILBOX,
150199
private val swapChainSize: Int = 10,
200+
internal var fboSizeOverride: FBOSizeOverride? = null,
151201
) : Thread("GLSurfaceView-${index.getAndIncrement()}") {
152202
enum class PresentMode(internal val impl: (Int, (IntSize) -> FBO) -> FBOSwapChain) {
153203
/**
@@ -168,6 +218,7 @@ class GLSurfaceView internal constructor(
168218
private var renderContext: GLContext<*>? = null
169219
private var size: IntSize = IntSize.Zero
170220
private var fboPool: FBOPool? = null
221+
internal var invalidations by mutableStateOf(0L)
171222

172223
internal fun launch() {
173224
GL.createCapabilities()
@@ -186,17 +237,18 @@ class GLSurfaceView internal constructor(
186237
state.requestUpdate()
187238
}
188239

189-
internal fun display(canvas: Canvas, displayContext: DirectContext) {
240+
internal fun display(canvas: Canvas, displayContext: DirectContext, paint: Paint) {
190241
val t1 = System.nanoTime()
191-
fboPool?.display { displayImpl(canvas, displayContext) }
192-
invalidate()
242+
fboPool?.display { displayImpl(canvas, displayContext, paint) }
243+
invalidations = t1
193244
val t2 = System.nanoTime()
194245
state.onDisplay(t2, (t2 - t1).nanoseconds)
195246
}
196247

197248
private fun GLDisplayScope.displayImpl(
198249
canvas: Canvas,
199250
displayContext: DirectContext,
251+
paint: Paint,
200252
) {
201253
val rt = BackendRenderTarget.makeGL(
202254
fbo.size.width,
@@ -252,8 +304,8 @@ class GLSurfaceView internal constructor(
252304
break
253305
}
254306
val waitTime = renderResult.getOrNull()
255-
invalidate()
256307
val renderEnd = System.nanoTime()
308+
invalidations = renderEnd
257309
state.onRender(renderEnd, (renderEnd - renderStart).nanoseconds)
258310
lastFrame = renderStart
259311
try {
@@ -280,7 +332,7 @@ class GLSurfaceView internal constructor(
280332
}
281333

282334
companion object {
283-
private val logger = LoggerFactory.getLogger(GLSurfaceView::class.java)
335+
private val logger = LoggerFactory.getLogger(GLSurface::class.java)
284336
private val index = AtomicLong(0L)
285337
}
286338
}

src/test/kotlin/Main.kt

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,30 @@ import androidx.compose.animation.core.tween
44
import androidx.compose.desktop.ui.tooling.preview.Preview
55
import androidx.compose.foundation.background
66
import androidx.compose.foundation.layout.*
7-
import androidx.compose.material.Button
8-
import androidx.compose.material.MaterialTheme
9-
import androidx.compose.material.Surface
10-
import androidx.compose.material.Text
7+
import androidx.compose.material.*
118
import androidx.compose.runtime.*
129
import androidx.compose.ui.Alignment
1310
import androidx.compose.ui.Modifier
1411
import androidx.compose.ui.graphics.Color
12+
import androidx.compose.ui.graphics.TransformOrigin
1513
import androidx.compose.ui.unit.dp
1614
import androidx.compose.ui.window.ApplicationScope
1715
import androidx.compose.ui.window.Window
1816
import androidx.compose.ui.window.awaitApplication
19-
import dev.silenium.compose.gl.surface.GLSurfaceView
20-
import dev.silenium.compose.gl.surface.Stats
21-
import dev.silenium.compose.gl.surface.rememberGLSurfaceState
17+
import dev.silenium.compose.gl.surface.*
2218
import kotlinx.coroutines.delay
2319
import me.saket.telephoto.zoomable.ZoomSpec
2420
import me.saket.telephoto.zoomable.rememberZoomableState
2521
import me.saket.telephoto.zoomable.zoomable
22+
import org.jetbrains.skia.Paint
2623
import org.lwjgl.opengl.GL30.*
2724
import kotlin.time.Duration.Companion.milliseconds
25+
import kotlin.time.Duration.Companion.seconds
2826

2927
@Composable
3028
@Preview
3129
fun ApplicationScope.App() {
32-
MaterialTheme {
30+
MaterialTheme(lightColors()) {
3331
Box(contentAlignment = Alignment.TopStart, modifier = Modifier.fillMaxSize().background(Color.White)) {
3432
val state = rememberGLSurfaceState()
3533
var targetHue by remember { mutableStateOf(0f) }
@@ -43,28 +41,33 @@ fun ApplicationScope.App() {
4341
delay(200)
4442
}
4543
}
44+
var visible by remember { mutableStateOf(true) }
45+
LaunchedEffect(Unit) {
46+
while (true) {
47+
delay(2.seconds)
48+
visible = !visible
49+
}
50+
}
4651
LaunchedEffect(Unit) {
4752
while (true) {
4853
delay(300.milliseconds)
49-
state.requestUpdate()
54+
if (visible) {
55+
state.requestUpdate()
56+
}
5057
}
5158
}
52-
GLSurfaceView(
59+
val surfaceView = rememberGLSurface(
5360
state = state,
54-
modifier = Modifier
55-
.aspectRatio(1f)
56-
.zoomable(rememberZoomableState(ZoomSpec(6f)))
57-
.align(Alignment.Center),
58-
presentMode = GLSurfaceView.PresentMode.MAILBOX,
59-
// fboSizeOverride = FBOSizeOverride(4096, 4096, TransformOrigin.Center),
61+
presentMode = GLSurface.PresentMode.MAILBOX,
62+
fboSizeOverride = FBOSizeOverride(4096, 4096, TransformOrigin.Center),
6063
) {
6164
glClearColor(color.red, color.green, color.blue, color.alpha)
6265
glClear(GL_COLOR_BUFFER_BIT)
6366
try {
6467
Thread.sleep(33, 333)
6568
} catch (e: InterruptedException) {
6669
terminate()
67-
return@GLSurfaceView
70+
return@rememberGLSurface
6871
}
6972
glBegin(GL_QUADS)
7073
glColor3f(1f, 0f, 0f)
@@ -80,6 +83,26 @@ fun ApplicationScope.App() {
8083
val wait = (1000.0 / 60).milliseconds
8184
redrawAfter(null)
8285
}
86+
val modifier = Modifier
87+
.aspectRatio(1f)
88+
.zoomable(rememberZoomableState(ZoomSpec(6f)))
89+
.align(Alignment.Center)
90+
if (visible) {
91+
GLSurfaceView(
92+
surfaceView,
93+
modifier = modifier,
94+
paint = Paint().apply {
95+
alpha = 128
96+
}
97+
)
98+
} else {
99+
Box(
100+
modifier = modifier,
101+
contentAlignment = Alignment.Center,
102+
) {
103+
Text("Surface is not visible", style = MaterialTheme.typography.h6, color = MaterialTheme.colors.onBackground, modifier = Modifier.align(Alignment.Center))
104+
}
105+
}
83106
Surface(modifier = Modifier.align(Alignment.TopStart).padding(4.dp)) {
84107
Column(modifier = Modifier.padding(4.dp).width(400.dp)) {
85108
val display by state.displayStatistics.collectAsState()

0 commit comments

Comments
 (0)