Skip to content

Commit 087b487

Browse files
Optimized rendering pipeline
1 parent 86d8950 commit 087b487

File tree

8 files changed

+204
-94
lines changed

8 files changed

+204
-94
lines changed

composeApp/src/jvmMain/kotlin/ua/valeriishymchuk/lobmapeditor/domain/terrain/Terrain.kt

Lines changed: 177 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ package ua.valeriishymchuk.lobmapeditor.domain.terrain
22

33
import com.google.gson.JsonArray
44
import com.google.gson.JsonObject
5-
import com.google.gson.JsonParser
65
import com.google.gson.JsonPrimitive
6+
import com.jogamp.common.nio.Buffers
7+
import ua.valeriishymchuk.lobmapeditor.domain.terrain.HeightMap.BlobHeightCachedRenderData
8+
import ua.valeriishymchuk.lobmapeditor.domain.terrain.TerrainMap.BlobCachedRenderData
79
import ua.valeriishymchuk.lobmapeditor.shared.GameConstants
810
import ua.valeriishymchuk.lobmapeditor.shared.dimension.ArrayMap2d
11+
import java.nio.IntBuffer
12+
import java.util.concurrent.ConcurrentHashMap
913

10-
data class Terrain (
14+
data class Terrain(
1115
val terrainMap: TerrainMap,
12-
val terrainHeight: ArrayMap2d<Int>
16+
val terrainHeight: HeightMap
1317
) {
1418

1519
init {
@@ -80,7 +84,6 @@ data class Terrain (
8084
val cellsY = heightPixels / GameConstants.TILE_SIZE
8185

8286

83-
8487
// Deserialize terrain types - column-major order
8588
val terrainsArray = json.getAsJsonArray("terrains")
8689
val terrainMap2D = Array(cellsX) { x ->
@@ -114,14 +117,14 @@ data class Terrain (
114117

115118
return Terrain(
116119
terrainMap = TerrainMap(terrainMap2D),
117-
terrainHeight = ArrayMap2d(heightMap2D)
120+
terrainHeight = HeightMap(heightMap2D)
118121
)
119122
}
120123

121124
fun ofPixels(pixelSizeX: Int = 1504, pixelSizeY: Int = 1312): Terrain {
122125
return Terrain(
123126
TerrainMap.ofPixels(pixelSizeX, pixelSizeY),
124-
ArrayMap2d(Array<Array<Int>>(pixelSizeX / GameConstants.TILE_SIZE) {
127+
HeightMap(Array<Array<Int>>(pixelSizeX / GameConstants.TILE_SIZE) {
125128
Array<Int>(pixelSizeY / GameConstants.TILE_SIZE) {
126129
0
127130
}
@@ -136,9 +139,175 @@ data class Terrain (
136139

137140
}
138141

139-
class TerrainMap (
142+
class HeightMap(
143+
map: Array<Array<Int>>
144+
) : ArrayMap2d<Int>(map) {
145+
146+
data class BlobHeightCachedRenderData(
147+
val buffer: IntBuffer
148+
)
149+
150+
private var cachedMaxHeight: Int? = null
151+
private var cachedMinHeight: Int? = null
152+
153+
val maxHeight: Int get() {
154+
var height = cachedMaxHeight
155+
if (height == null) {
156+
height = map.flatMap { it }.distinct().max()
157+
cachedMaxHeight = height
158+
}
159+
return height
160+
}
161+
162+
val minHeight: Int get() {
163+
var height = cachedMinHeight
164+
if (height == null) {
165+
height = map.flatMap { it }.distinct().min()
166+
cachedMinHeight = height
167+
}
168+
return height
169+
}
170+
171+
private val cachedBlobRenderHeights = ConcurrentHashMap<Int, BlobHeightCachedRenderData>()
172+
173+
fun getHeightBlobMap(height: Int): BlobHeightCachedRenderData {
174+
return cachedBlobRenderHeights.computeIfAbsent(height) { _ ->
175+
val buffer = Buffers.newDirectIntBuffer(sizeX * sizeY)
176+
map.flatMap { it }.forEach {
177+
if (height <= it) {
178+
buffer.put(1)
179+
} else buffer.put(0)
180+
}
181+
buffer.flip()
182+
BlobHeightCachedRenderData(buffer)
183+
}
184+
}
185+
186+
override fun set(x: Int, y: Int, value: Int): Int? {
187+
val oldHeight = super.set(x, y, value) ?: return null
188+
cachedBlobRenderHeights.clear()
189+
cachedMaxHeight = null
190+
cachedMinHeight = null
191+
return oldHeight
192+
}
193+
194+
195+
}
196+
197+
class TerrainMap(
140198
map: Array<Array<TerrainType>>
141-
): ArrayMap2d<TerrainType>(map) {
199+
) : ArrayMap2d<TerrainType>(map) {
200+
201+
private val cachedTerrainRenderTypes = ConcurrentHashMap<TerrainType, TerrainCachedRenderData>()
202+
private val cachedBlobRenderTypes = ConcurrentHashMap<TerrainType, BlobCachedRenderData>()
203+
private val cachedOverlayRenderTypes = ConcurrentHashMap<TerrainType, OverlayCachedRenderData>()
204+
205+
data class TerrainCachedRenderData(
206+
val buffer: IntBuffer,
207+
val shouldRender: Boolean
208+
)
209+
210+
data class BlobCachedRenderData(
211+
val buffer: IntBuffer
212+
)
213+
214+
data class OverlayCachedRenderData(
215+
val buffer: IntBuffer
216+
)
217+
218+
fun getOverlayRenderMap(type: TerrainType): OverlayCachedRenderData {
219+
return cachedOverlayRenderTypes.computeIfAbsent(type) { _ ->
220+
val buffer = Buffers.newDirectIntBuffer(sizeX * sizeY)
221+
map.flatMap { it }.forEach {
222+
if (type == it) {
223+
buffer.put(1)
224+
} else buffer.put(0)
225+
}
226+
buffer.flip()
227+
OverlayCachedRenderData(buffer)
228+
}
229+
}
230+
231+
fun getBlobRenderMap(type: TerrainType): BlobCachedRenderData {
232+
return cachedBlobRenderTypes.computeIfAbsent(type) { _ ->
233+
val buffer = Buffers.newDirectIntBuffer(sizeX * sizeY)
234+
235+
map.flatMap { it }.forEach {
236+
if (type == it) {
237+
buffer.put(1)
238+
return@forEach
239+
}
240+
if (it == TerrainType.BRIDGE && (type == TerrainType.ROAD || type == TerrainType.ROAD_WINTER || type == TerrainType.SUNKEN_ROAD)) {
241+
buffer.put(2)
242+
return@forEach
243+
}
244+
if (type == TerrainType.BRIDGE && (it == TerrainType.ROAD || it == TerrainType.ROAD_WINTER || it == TerrainType.SUNKEN_ROAD)) {
245+
buffer.put(2)
246+
return@forEach
247+
}
248+
if (it == TerrainType.SUNKEN_ROAD && type == TerrainType.ROAD || it == TerrainType.ROAD && type == TerrainType.SUNKEN_ROAD ) {
249+
buffer.put(2)
250+
return@forEach
251+
}
252+
buffer.put(0)
253+
}
254+
255+
buffer.flip()
256+
257+
BlobCachedRenderData(buffer)
258+
}
259+
}
260+
261+
fun getRenderMap(type: TerrainType): TerrainCachedRenderData {
262+
return cachedTerrainRenderTypes.computeIfAbsent(type) { _ ->
263+
var hasSomethingToRender = false
264+
265+
val buffer = Buffers.newDirectIntBuffer(sizeX * sizeY)
266+
map.flatMap { it }.forEach {
267+
if (type == it) {
268+
hasSomethingToRender = true
269+
buffer.put(1)
270+
return@forEach
271+
}
272+
if (it.isFarm) {
273+
hasSomethingToRender = true
274+
buffer.put(2)
275+
return@forEach
276+
}
277+
buffer.put(0)
278+
}
279+
buffer.flip()
280+
TerrainCachedRenderData(
281+
buffer,
282+
hasSomethingToRender
283+
)
284+
285+
}
286+
}
287+
288+
override fun set(x: Int, y: Int, value: TerrainType): TerrainType? {
289+
val oldTerrainType = super.set(x, y, value) ?: return null
290+
// val isFarm = value.isFarm || oldTerrainType.isFarm
291+
// cachedTerrainRenderTypes.keys.toSet().forEach { type ->
292+
// if (isFarm && type.isFarm) cachedTerrainRenderTypes.remove(type)
293+
// if (value == type || oldTerrainType == value) cachedTerrainRenderTypes.remove(type)
294+
// }
295+
// val roads = setOf(TerrainType.ROAD, TerrainType.ROAD_WINTER, TerrainType.SUNKEN_ROAD)
296+
// val isRoad = roads.contains(value) || roads.contains(oldTerrainType)
297+
// cachedBlobRenderTypes.keys.toSet().forEach { type ->
298+
// if (isRoad && roads.contains(type)) cachedBlobRenderTypes.remove(type)
299+
// if (value == type || oldTerrainType == value) cachedBlobRenderTypes.remove(type)
300+
// }
301+
// cachedOverlayRenderTypes.keys.forEach { type ->
302+
// if (value == type || oldTerrainType == value) cachedOverlayRenderTypes.remove(type)
303+
// }
304+
305+
cachedTerrainRenderTypes.clear()
306+
cachedBlobRenderTypes.clear()
307+
cachedOverlayRenderTypes.clear()
308+
309+
return oldTerrainType
310+
}
142311

143312
fun serialize(): Array<Array<Int>> {
144313
return _map.map {

composeApp/src/jvmMain/kotlin/ua/valeriishymchuk/lobmapeditor/render/EditorRenderer.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import ua.valeriishymchuk.lobmapeditor.shared.editor.ProjectRef
2424
import java.awt.event.*
2525
import java.lang.Math
2626
import java.lang.System
27+
import kotlin.time.TimeSource
2728

2829

2930
class EditorRenderer(override val di: DI) : GLEventListener, DIAware {
@@ -184,13 +185,23 @@ class EditorRenderer(override val di: DI) : GLEventListener, DIAware {
184185
)
185186

186187

187-
188+
val timeSource = TimeSource.Monotonic
189+
val start = timeSource.markNow()
190+
val marks = mutableListOf<Pair<String, TimeSource.Monotonic.ValueTimeMark>>()
188191
renderStages.forEach { stage ->
189192
if (stage is ColorClosestPointStage && !editorService.enableColorClosestPoint) return@forEach
190193
if (stage is GridStage && !toolService.gridTool.enabled.value) return@forEach
191194
if (stage is ReferenceOverlayStage && !toolService.refenceOverlayTool.enabled.value) return@forEach
192195
stage.draw(renderCtx)
196+
marks.add(stage::class.simpleName!! to timeSource.markNow())
193197
}
198+
val end = timeSource.markNow()
199+
// println("Rendered frame, it took: ${end - start} to render it. Summary for every stage:")
200+
// var lastMark = start
201+
// marks.forEach { (stage, mark) ->
202+
// println("$stage: ${mark - lastMark}")
203+
// lastMark = mark
204+
// }
194205

195206
ctx.glBindVertexArray(0)
196207

composeApp/src/jvmMain/kotlin/ua/valeriishymchuk/lobmapeditor/render/InputListener.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ class InputListener(
610610
val scaleX = newView.m00()
611611
val scaleY = newView.m11()
612612

613-
if (scaleX !in 0.08..14.0) return
613+
if (scaleX !in 0.03..14.0) return
614614

615615
// Apply translation adjustment (inverse because view matrix is inverse of camera)
616616
val translation = newView.getColumn(3, Vector4f())

composeApp/src/jvmMain/kotlin/ua/valeriishymchuk/lobmapeditor/render/program/BlobProcessorProgram.kt

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import org.joml.Matrix4f
77
import org.joml.Vector2f
88
import org.joml.Vector2i
99
import org.joml.Vector4f
10+
import ua.valeriishymchuk.lobmapeditor.domain.terrain.HeightMap
1011
import ua.valeriishymchuk.lobmapeditor.domain.terrain.TerrainMap
1112
import ua.valeriishymchuk.lobmapeditor.domain.terrain.TerrainType
1213
import ua.valeriishymchuk.lobmapeditor.render.helper.*
@@ -47,24 +48,11 @@ class BlobProcessorProgram(
4748
textureID.value
4849
}
4950

50-
fun loadHeight(ctx: GL3, heightMap: ArrayMap2d<Int>, tileHeight: Int) {
51+
fun loadHeight(ctx: GL3, heightMap: HeightMap, tileHeight: Int) {
5152
ctx.glBindTexture(GL3.GL_TEXTURE_2D, tileMapTexture)
5253
val width = heightMap.sizeX
5354
val height = heightMap.sizeY
54-
val buffer = Buffers.newDirectIntBuffer(width * height)
55-
56-
val serializedData = heightMap.map.flatMap { it }.map {
57-
if (tileHeight <= it) 1 else 0
58-
}
59-
60-
serializedData.forEach {
61-
buffer.put(it)
62-
}
63-
// println("Found ${serializedData.filter { it != 0 }.size} of $terrainType")
64-
// Found 4 of SNOW out of thousands, thats fine
65-
// but everything is set to snow
66-
// something fishy is going on with loading the data
67-
buffer.flip()
55+
val buffer = heightMap.getHeightBlobMap(tileHeight).buffer
6856

6957

7058
ctx.glTexImage2D(
@@ -80,34 +68,13 @@ class BlobProcessorProgram(
8068
)
8169
}
8270

83-
// it will be painfully slow, we've got to change the storage format for the sake of performance
8471
fun loadMap(ctx: GL3, terrainMap: TerrainMap, terrainType: TerrainType) {
8572

86-
// also we might want to use Pixel Buffer Objects
8773

8874
ctx.glBindTexture(GL3.GL_TEXTURE_2D, tileMapTexture)
8975
val width = terrainMap.sizeX
9076
val height = terrainMap.sizeY
91-
val buffer = Buffers.newDirectIntBuffer(width * height)
92-
93-
val serializedData = terrainMap.map.flatMap { it }.map {
94-
if (terrainType == it) return@map 1
95-
if (it == TerrainType.BRIDGE && (terrainType == TerrainType.ROAD || terrainType == TerrainType.ROAD_WINTER || terrainType == TerrainType.SUNKEN_ROAD)) return@map 2
96-
if (terrainType == TerrainType.BRIDGE && (it == TerrainType.ROAD || it == TerrainType.ROAD_WINTER || it == TerrainType.SUNKEN_ROAD)) return@map 2
97-
if (it == TerrainType.SUNKEN_ROAD && terrainType == TerrainType.ROAD || it == TerrainType.ROAD && terrainType == TerrainType.SUNKEN_ROAD ) return@map 2
98-
0
99-
}
100-
101-
serializedData.forEach {
102-
buffer.put(it)
103-
}
104-
// println("Found ${serializedData.filter { it != 0 }.size} of $terrainType")
105-
// Found 4 of SNOW out of thousands, thats fine
106-
// but everything is set to snow
107-
// something fishy is going on with loading the data
108-
buffer.flip()
109-
110-
77+
val buffer = terrainMap.getBlobRenderMap(terrainType).buffer
11178
ctx.glTexImage2D(
11279
GL3.GL_TEXTURE_2D,
11380
0,
@@ -117,7 +84,7 @@ class BlobProcessorProgram(
11784
0,
11885
GL3.GL_RED_INTEGER,
11986
GL3.GL_UNSIGNED_INT,
120-
buffer // Pass the buffer directly instead of null
87+
buffer
12188
)
12289

12390

composeApp/src/jvmMain/kotlin/ua/valeriishymchuk/lobmapeditor/render/program/OverlayTileProgram.kt

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,29 +47,13 @@ class OverlayTileProgram(
4747
textureID.value
4848
}
4949

50-
// it will be panfully slow, we've got to change the storage format for the sake of performance
5150
fun loadMap(ctx: GL3, terrainMap: TerrainMap, terrainType: TerrainType) {
5251

53-
// also we might want to use Pixel Buffer Objects
5452

5553
ctx.glBindTexture(GL3.GL_TEXTURE_2D, tileMapTexture)
5654
val width = terrainMap.sizeX
5755
val height = terrainMap.sizeY
58-
val buffer = Buffers.newDirectIntBuffer(width * height)
59-
60-
val serializedData = terrainMap.map.flatMap { it }.map {
61-
if (terrainType == it) return@map 1
62-
0
63-
}
64-
65-
serializedData.forEach {
66-
buffer.put(it)
67-
}
68-
// println("Found ${serializedData.filter { it != 0 }.size} of $terrainType")
69-
// Found 4 of SNOW out of thousands, thats fine
70-
// but everything is set to snow
71-
// something fishy is going on with loading the data
72-
buffer.flip()
56+
val buffer = terrainMap.getOverlayRenderMap(terrainType).buffer
7357

7458

7559
ctx.glTexImage2D(

0 commit comments

Comments
 (0)