Skip to content

Commit 29387f1

Browse files
committed
feat: add gl canvas state to allow update requests, also fix some random crash with unclean gl states
1 parent 99725d8 commit 29387f1

File tree

5 files changed

+215
-24
lines changed

5 files changed

+215
-24
lines changed

src/main/java/dev/silenium/compose/gl/canvas/CanvasDriver.kt

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ import androidx.compose.ui.graphics.drawscope.DrawScope
44
import androidx.compose.ui.unit.IntSize
55
import dev.silenium.compose.gl.fbo.FBO
66
import org.jetbrains.skia.DirectContext
7-
import org.lwjgl.opengl.GL11.glFlush
7+
import org.lwjgl.opengl.GL46.*
88
import java.awt.Window
9+
import kotlin.time.Duration
910

1011
interface CanvasDriver {
1112
fun setup(directContext: DirectContext)
1213
fun render(
1314
scope: DrawScope,
14-
userResizeHandler: GLDrawScope.(old: IntSize?, new: IntSize) -> Unit = { _, _ -> },
15-
block: GLDrawScope.() -> Unit,
15+
userResizeHandler: FBOScope.(old: IntSize?, new: IntSize) -> Unit = { _, _ -> },
16+
block: FBOScope.() -> Unit,
1617
)
1718

1819
fun display(scope: DrawScope)
@@ -23,16 +24,69 @@ fun interface CanvasDriverFactory<T : CanvasDriver> {
2324
fun create(window: Window): T
2425
}
2526

26-
data class GLDrawScope(
27-
val fbo: FBO,
28-
)
27+
interface FBOScope {
28+
val fbo: FBO
29+
}
30+
31+
interface GLDrawScope : FBOScope {
32+
val deltaTime: Duration
33+
}
34+
35+
data class GLDrawScopeImpl(
36+
val fboScope: FBOScope,
37+
override val deltaTime: Duration,
38+
) : FBOScope by fboScope, GLDrawScope
2939

30-
internal inline fun <T> GLDrawScope.drawGL(block: () -> T): T {
40+
data class FBOScopeImpl(override val fbo: FBO) : FBOScope
41+
42+
internal inline fun <T> FBOScope.drawGL(block: () -> T): T {
3143
fbo.bind()
44+
resetGLFeatures()
3245
try {
3346
return block()
3447
} finally {
3548
fbo.unbind()
3649
glFlush()
3750
}
3851
}
52+
53+
fun resetGLFeatures() {
54+
listOf(
55+
GL_BLEND,
56+
GL_CLIP_DISTANCE0,
57+
GL_CLIP_DISTANCE1,
58+
GL_CLIP_DISTANCE2,
59+
GL_CLIP_DISTANCE3,
60+
GL_CLIP_DISTANCE4,
61+
GL_CLIP_DISTANCE5,
62+
GL_CLIP_DISTANCE6,
63+
GL_CLIP_DISTANCE7,
64+
GL_COLOR_LOGIC_OP,
65+
GL_CULL_FACE,
66+
GL_DEBUG_OUTPUT,
67+
GL_DEBUG_OUTPUT_SYNCHRONOUS,
68+
GL_DEPTH_CLAMP,
69+
GL_DEPTH_TEST,
70+
GL_DITHER,
71+
GL_FRAMEBUFFER_SRGB,
72+
GL_LINE_SMOOTH,
73+
GL_MULTISAMPLE,
74+
GL_POLYGON_OFFSET_FILL,
75+
GL_POLYGON_OFFSET_LINE,
76+
GL_POLYGON_OFFSET_POINT,
77+
GL_POLYGON_SMOOTH,
78+
GL_PRIMITIVE_RESTART,
79+
GL_PRIMITIVE_RESTART_FIXED_INDEX,
80+
GL_RASTERIZER_DISCARD,
81+
GL_SAMPLE_ALPHA_TO_COVERAGE,
82+
GL_SAMPLE_ALPHA_TO_ONE,
83+
GL_SAMPLE_COVERAGE,
84+
GL_SAMPLE_SHADING,
85+
GL_SAMPLE_MASK,
86+
GL_SCISSOR_TEST,
87+
GL_STENCIL_TEST,
88+
GL_TEXTURE_CUBE_MAP_SEAMLESS,
89+
GL_PROGRAM_POINT_SIZE,
90+
)
91+
glDisable(GL_BLEND)
92+
}

src/main/java/dev/silenium/compose/gl/canvas/D3DCanvasDriver.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ class D3DCanvasDriver(private val window: Window) : CanvasDriver {
6161

6262
override fun render(
6363
scope: DrawScope,
64-
userResizeHandler: GLDrawScope.(old: IntSize?, new: IntSize) -> Unit,
65-
block: GLDrawScope.() -> Unit
64+
userResizeHandler: FBOScope.(old: IntSize?, new: IntSize) -> Unit,
65+
block: FBOScope.() -> Unit
6666
) {
6767
val d3dCtx = d3dDirectContext ?: return
6868
ensureInitialized()
@@ -71,12 +71,11 @@ class D3DCanvasDriver(private val window: Window) : CanvasDriver {
7171
val oldSize = fbo?.size
7272
val newSize = scope.size.toIntSize()
7373
ensureFBO(scope.size.toIntSize(), d3dCtx)
74-
7574
if (oldSize != newSize) {
76-
GLDrawScope(fbo!!).userResizeHandler(oldSize, newSize)
75+
FBOScopeImpl(fbo!!).userResizeHandler(oldSize, newSize)
7776
}
7877

79-
GLDrawScope(fbo!!).block()
78+
FBOScopeImpl(fbo!!).block()
8079
glFlush()
8180
GLFW.glfwMakeContextCurrent(MemoryUtil.NULL)
8281
}

src/main/java/dev/silenium/compose/gl/canvas/GLCanvas.kt

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@ import androidx.compose.runtime.DisposableEffect
66
import androidx.compose.runtime.LaunchedEffect
77
import androidx.compose.runtime.remember
88
import androidx.compose.ui.Modifier
9-
import androidx.compose.ui.graphics.nativeCanvas
109
import androidx.compose.ui.unit.IntSize
1110
import dev.silenium.compose.gl.LocalWindow
1211
import dev.silenium.compose.gl.directContext
1312
import dev.silenium.compose.gl.findSkiaLayer
1413
import kotlinx.coroutines.Dispatchers
1514
import kotlinx.coroutines.isActive
1615
import kotlinx.coroutines.withContext
16+
import kotlin.time.Duration
17+
import kotlin.time.Duration.Companion.nanoseconds
18+
import kotlin.time.ExperimentalTime
1719

20+
@OptIn(ExperimentalTime::class)
1821
@Composable
1922
fun GLCanvas(
23+
state: GLCanvasState = rememberGLCanvasState(),
2024
wrapperFactory: CanvasDriverFactory<*> = DefaultCanvasDriverFactory,
2125
modifier: Modifier = Modifier,
2226
onDispose: () -> Unit = {},
@@ -41,10 +45,20 @@ fun GLCanvas(
4145
}
4246
}
4347
Canvas(modifier) {
44-
drawContext.canvas.nativeCanvas
45-
wrapper.render(this, onResize) {
46-
drawGL { block() }
48+
state.invalidations.let {
49+
val t1 = System.nanoTime()
50+
val delta = state.lastFrame?.let { (it - t1).nanoseconds } ?: Duration.ZERO
51+
wrapper.render(this, userResizeHandler = { old, new ->
52+
GLDrawScopeImpl(this, delta).onResize(old, new)
53+
}) {
54+
drawGL { GLDrawScopeImpl(this, delta).block() }
55+
}
56+
val t2 = System.nanoTime()
57+
state.onRender(t2, (t2 - t1).nanoseconds)
58+
val t3 = System.nanoTime()
59+
wrapper.display(this)
60+
val t4 = System.nanoTime()
61+
state.onDisplay(t4, (t4 - t3).nanoseconds)
4762
}
48-
wrapper.display(this)
4963
}
5064
}

src/main/java/dev/silenium/compose/gl/canvas/GLCanvasDriver.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import dev.silenium.compose.gl.objects.Texture
1313
import org.jetbrains.skia.*
1414
import org.lwjgl.opengl.GL
1515
import org.lwjgl.opengl.GL11
16-
import org.lwjgl.opengl.GL11.glFlush
16+
import org.lwjgl.opengl.GL11.glFinish
1717
import org.lwjgl.opengl.GL33
1818
import org.lwjgl.opengl.GLCapabilities
1919
import org.slf4j.LoggerFactory
@@ -47,8 +47,8 @@ class GLCanvasDriver : CanvasDriver {
4747

4848
override fun render(
4949
scope: DrawScope,
50-
userResizeHandler: GLDrawScope.(old: IntSize?, new: IntSize) -> Unit,
51-
block: GLDrawScope.() -> Unit
50+
userResizeHandler: FBOScope.(old: IntSize?, new: IntSize) -> Unit,
51+
block: FBOScope.() -> Unit
5252
) {
5353
val ctx = directContext ?: return
5454
ctx.submit(syncCpu = true)
@@ -58,12 +58,12 @@ class GLCanvasDriver : CanvasDriver {
5858
val newSize = scope.size.toIntSize()
5959
ensureFBO(newSize, ctx)
6060
if (oldSize != newSize) {
61-
GLDrawScope(fbo!!).userResizeHandler(oldSize, newSize)
61+
FBOScopeImpl(fbo!!).userResizeHandler(oldSize, newSize)
6262
}
6363

64-
GLDrawScope(fbo!!).block()
65-
glFlush()
66-
ctx.resetGLAll()
64+
FBOScopeImpl(fbo!!).block()
65+
glFinish()
66+
ctx.resetAll()
6767
}
6868

6969
override fun display(scope: DrawScope) {
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package dev.silenium.compose.gl.canvas
2+
3+
import androidx.compose.runtime.*
4+
import kotlinx.coroutines.flow.MutableStateFlow
5+
import kotlinx.coroutines.flow.StateFlow
6+
import kotlinx.coroutines.flow.asStateFlow
7+
import kotlin.time.Duration
8+
import kotlin.time.Duration.Companion.seconds
9+
import kotlin.time.ExperimentalTime
10+
11+
interface Stats<T : Comparable<T>> {
12+
val values: List<T>
13+
val sum: T
14+
val average: T
15+
val min: T
16+
val max: T
17+
val median: T
18+
fun percentile(percentile: Double, direction: Percentile = Percentile.UP): T
19+
20+
enum class Percentile {
21+
UP, LOWEST
22+
}
23+
}
24+
25+
data class DurationStats(override val values: List<Duration>) : Stats<Duration> {
26+
override val sum by lazy { values.fold(Duration.ZERO) { a, it -> a + it } }
27+
override val average by lazy { sum / values.size }
28+
override val min by lazy { values.minOrNull() ?: Duration.ZERO }
29+
override val max by lazy { values.maxOrNull() ?: Duration.ZERO }
30+
override val median by lazy {
31+
if (values.isEmpty()) return@lazy Duration.ZERO
32+
if (values.size == 1) return@lazy values.first()
33+
val sorted = values.sorted()
34+
val middle = sorted.size / 2
35+
if (sorted.size % 2 == 0) {
36+
(sorted[middle - 1] + sorted[middle]) / 2
37+
} else {
38+
sorted[middle]
39+
}
40+
}
41+
42+
override fun percentile(percentile: Double, direction: Stats.Percentile): Duration {
43+
if (values.isEmpty()) return Duration.ZERO
44+
val sorted = values.sorted()
45+
val index = when (direction) {
46+
Stats.Percentile.UP -> (percentile * sorted.size).toInt()
47+
Stats.Percentile.LOWEST -> sorted.size - (percentile * sorted.size).toInt() - 1
48+
}
49+
return sorted[index]
50+
}
51+
}
52+
53+
data class DoubleStats(override val values: List<Double>) : Stats<Double> {
54+
override val sum by lazy { values.fold(0.0) { a, it -> a + it } }
55+
override val average by lazy { sum / values.size }
56+
override val min by lazy { values.minOrNull() ?: 0.0 }
57+
override val max by lazy { values.maxOrNull() ?: 0.0 }
58+
override val median by lazy {
59+
if (values.isEmpty()) return@lazy 0.0
60+
if (values.size == 1) return@lazy values.first()
61+
val sorted = values.sorted()
62+
val middle = sorted.size / 2
63+
if (sorted.size % 2 == 0) {
64+
(sorted[middle - 1] + sorted[middle]) / 2
65+
} else {
66+
sorted[middle]
67+
}
68+
}
69+
70+
override fun percentile(percentile: Double, direction: Stats.Percentile): Double {
71+
if (values.isEmpty()) return 0.0
72+
val sorted = values.sorted()
73+
val index = when (direction) {
74+
Stats.Percentile.UP -> (percentile * sorted.size).toInt()
75+
Stats.Percentile.LOWEST -> sorted.size - (percentile * sorted.size).toInt() - 1
76+
}
77+
return sorted[index]
78+
}
79+
}
80+
81+
data class RollingWindowStatistics(
82+
val windowSize: Duration = 5.seconds,
83+
val values: Map<Long, Duration> = emptyMap(),
84+
) {
85+
val frameTimes by lazy { DurationStats(values.values.toList()) }
86+
val fps by lazy {
87+
DoubleStats(
88+
if (values.size < 2) emptyList()
89+
else values.keys.sorted().zipWithNext().map { (a, b) -> 1_000_000_000.0 / (b - a) }
90+
)
91+
}
92+
93+
fun add(nanos: Long, time: Duration): RollingWindowStatistics {
94+
val newValues = values.toMutableMap()
95+
newValues[nanos] = time
96+
return copy(values = newValues.filter { it.key >= nanos - windowSize.inWholeNanoseconds })
97+
}
98+
}
99+
100+
@OptIn(ExperimentalTime::class)
101+
class GLCanvasState {
102+
private val renderStatisticsMutable = MutableStateFlow(RollingWindowStatistics())
103+
private val displayStatisticsMutable = MutableStateFlow(RollingWindowStatistics())
104+
internal var invalidations by mutableStateOf(0L)
105+
internal var lastFrame: Long? = null
106+
107+
val renderStatistics: StateFlow<RollingWindowStatistics> get() = renderStatisticsMutable.asStateFlow()
108+
val displayStatistics: StateFlow<RollingWindowStatistics> get() = displayStatisticsMutable.asStateFlow()
109+
110+
fun requestUpdate() {
111+
invalidations = System.nanoTime()
112+
}
113+
114+
internal fun onDisplay(nanos: Long, frameTime: Duration) {
115+
displayStatisticsMutable.tryEmit(displayStatisticsMutable.value.add(nanos, frameTime))
116+
}
117+
118+
internal fun onRender(nanos: Long, frameTime: Duration) {
119+
renderStatisticsMutable.tryEmit(renderStatisticsMutable.value.add(nanos, frameTime))
120+
}
121+
}
122+
123+
@Composable
124+
fun rememberGLCanvasState() = remember { GLCanvasState() }

0 commit comments

Comments
 (0)