Skip to content

Commit 3606ff7

Browse files
committed
feat: async chunk loading, minimap & mushroom lights off-thread, PointLight3D Intensity/Falloff
- Both samples: async chunk generation via Cmd.ofAsync, ConcurrentDictionary - Both samples: minimap block collection + image gen on thread pool, texture upload on main thread - ThreeDSample: mushroom light collection async (throttled to every 6 frames) - Fix render order: point lights added to buffer before instanced blocks - PointLight3D: add Intensity and Falloff fields, wire through shader + pipeline - Both samples: new ChunkCreated, MinimapReady msgs; ThreeDSample also MushroomLightsReady
1 parent 33f692a commit 3606ff7

16 files changed

Lines changed: 433 additions & 228 deletions

File tree

samples/PlatformerSample/MinimapView.fs

Lines changed: 74 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module PlatformerSample.Minimap
22

33
#nowarn "9"
44

5+
open System.Collections.Concurrent
56
open System.Collections.Generic
67
open System.Numerics
78
open FSharp.NativeInterop
@@ -32,7 +33,11 @@ let private texSize = 200
3233

3334
// ── Helpers ──
3435

35-
let private tileColor (skyColor: Color) (biome: Biome) (tile: TileType) : Color =
36+
let private tileColor
37+
(skyColor: Color)
38+
(biome: Biome)
39+
(tile: TileType)
40+
: Color =
3641
match tile with
3742
| Ground ->
3843
match biome with
@@ -48,88 +53,81 @@ let private tileColor (skyColor: Color) (biome: Biome) (tile: TileType) : Color
4853

4954
// ── System ──
5055

51-
let system
52-
(chunks: Dictionary<struct (int * int), Chunk>)
56+
let generateMinimapImage
57+
(chunks: ConcurrentDictionary<struct (int * int), Chunk>)
5358
(timeOfDay: float32)
5459
(playerPos: Vector2)
55-
(model: inref<MinimapModel>)
56-
: unit =
60+
: Image =
5761
let scale = minimapSize / (minimapWorldRadius * 2.0f)
5862

59-
let posDelta = playerPos - model.LastPlayerPos
60-
61-
let needsUpdate =
62-
model.FrameCounter % updateInterval = 0 || posDelta.LengthSquared() > 4.0f
63-
64-
if needsUpdate then
65-
model.LastPlayerPos <- playerPos
66-
model.Blocks.Clear()
67-
68-
let halfWorld = minimapWorldRadius
69-
70-
for KeyValue(struct (_cx, _cy), chunk) in chunks do
71-
if
72-
chunk.Bounds.X + chunk.Bounds.Width >= playerPos.X - halfWorld
73-
&& chunk.Bounds.X <= playerPos.X + halfWorld
74-
&& chunk.Bounds.Y + chunk.Bounds.Height >= playerPos.Y - halfWorld
75-
&& chunk.Bounds.Y <= playerPos.Y + halfWorld
76-
then
77-
let cellW = chunk.Grid.CellSize.X
78-
let cellH = chunk.Grid.CellSize.Y
79-
let chunkBiome = chunk.Biome
80-
81-
for y in 0 .. chunk.Grid.Height - 1 do
82-
for x in 0 .. chunk.Grid.Width - 1 do
83-
match CellGrid2D.get x y chunk.Grid with
84-
| ValueSome tile when tile <> Empty ->
85-
let wx = chunk.Grid.Origin.X + float32 x * cellW
86-
let wy = chunk.Grid.Origin.Y + float32 y * cellH
87-
let qx = int wx
88-
let qz = int wy
89-
let key = struct (qx, qz)
90-
91-
if not(model.Blocks.ContainsKey key) then
92-
model.Blocks[key] <- struct (wy, tile, chunkBiome)
93-
| _ -> ()
94-
95-
// Bake texture
96-
let skyTop, _skyBot = DayNight.getSkyColors timeOfDay
97-
let halfMinimap = minimapSize * 0.5f
98-
let pixelSize = tileSize * scale + 1.0f
99-
let pixelSizeI = max 1 (int pixelSize)
100-
101-
let mutable img = Raylib.GenImageColor(texSize, texSize, skyTop)
102-
use imgPin = fixed &img
103-
104-
for KeyValue(struct (wx, wz), struct (_, tile, biome)) in model.Blocks do
105-
let relX = (float32 wx - playerPos.X) * scale
106-
let relZ = (float32 wz - playerPos.Y) * scale
107-
let pixelX = int(halfMinimap + relX)
108-
let pixelZ = int(halfMinimap + relZ)
109-
let color = tileColor skyTop biome tile
110-
111-
if color.A > 0uy then
112-
Raylib.ImageDrawRectangle(
113-
imgPin,
114-
pixelX,
115-
pixelZ,
116-
pixelSizeI,
117-
pixelSizeI,
118-
color
119-
)
120-
121-
if model.TexReady then
122-
Raylib.UpdateTexture(
123-
model.Texture,
124-
NativePtr.toVoidPtr(NativePtr.ofVoidPtr<byte> img.Data)
63+
let blocks =
64+
Dictionary<struct (int * int), struct (float32 * TileType * Biome)>()
65+
66+
let halfWorld = minimapWorldRadius
67+
68+
for KeyValue(struct (_cx, _cy), chunk) in chunks do
69+
if
70+
chunk.Bounds.X + chunk.Bounds.Width >= playerPos.X - halfWorld
71+
&& chunk.Bounds.X <= playerPos.X + halfWorld
72+
&& chunk.Bounds.Y + chunk.Bounds.Height >= playerPos.Y - halfWorld
73+
&& chunk.Bounds.Y <= playerPos.Y + halfWorld
74+
then
75+
let cellW = chunk.Grid.CellSize.X
76+
let cellH = chunk.Grid.CellSize.Y
77+
let chunkBiome = chunk.Biome
78+
79+
for y in 0 .. chunk.Grid.Height - 1 do
80+
for x in 0 .. chunk.Grid.Width - 1 do
81+
match CellGrid2D.get x y chunk.Grid with
82+
| ValueSome tile when tile <> Empty ->
83+
let wx = chunk.Grid.Origin.X + float32 x * cellW
84+
let wy = chunk.Grid.Origin.Y + float32 y * cellH
85+
let qx = int wx
86+
let qz = int wy
87+
let key = struct (qx, qz)
88+
89+
if not(blocks.ContainsKey key) then
90+
blocks[key] <- struct (wy, tile, chunkBiome)
91+
| _ -> ()
92+
93+
let skyTop, _skyBot = DayNight.getSkyColors timeOfDay
94+
let halfMinimap = minimapSize * 0.5f
95+
let pixelSize = tileSize * scale + 1.0f
96+
let pixelSizeI = max 1 (int pixelSize)
97+
98+
let mutable img = Raylib.GenImageColor(texSize, texSize, skyTop)
99+
use imgPin = fixed &img
100+
101+
for KeyValue(struct (wx, wz), struct (_, tile, biome)) in blocks do
102+
let relX = (float32 wx - playerPos.X) * scale
103+
let relZ = (float32 wz - playerPos.Y) * scale
104+
let pixelX = int(halfMinimap + relX)
105+
let pixelZ = int(halfMinimap + relZ)
106+
let color = tileColor skyTop biome tile
107+
108+
if color.A > 0uy then
109+
Raylib.ImageDrawRectangle(
110+
imgPin,
111+
pixelX,
112+
pixelZ,
113+
pixelSizeI,
114+
pixelSizeI,
115+
color
125116
)
126-
else
127-
model.Texture <- Raylib.LoadTextureFromImage(img)
128-
model.TexReady <- true
129117

130-
Raylib.UnloadImage(img)
118+
img
119+
120+
let uploadTexture (image: Image) (model: inref<MinimapModel>) : unit =
121+
if model.TexReady then
122+
Raylib.UpdateTexture(
123+
model.Texture,
124+
NativePtr.toVoidPtr(NativePtr.ofVoidPtr<byte> image.Data)
125+
)
126+
else
127+
model.Texture <- Raylib.LoadTextureFromImage(image)
128+
model.TexReady <- true
131129

132-
model.FrameCounter <- model.FrameCounter + 1
130+
Raylib.UnloadImage(image)
133131

134132
// ── View ──
135133

samples/PlatformerSample/Program.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ let main _ =
160160
Width = 1280
161161
Height = 720
162162
Title = "Mibo Raylib Platformer"
163-
TargetFPS = 60
163+
TargetFPS = 120
164164
})
165165
|> Program.withInput
166166
|> Program.withSubscription subscribe

samples/PlatformerSample/Systems.fs

Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ open Mibo.Layout
2121
let nearbyPlatforms = ResizeArray<Rectangle>(256)
2222
let nearbySpikes = ResizeArray<Rectangle>(64)
2323
let nearbyCoins = ResizeArray<Rectangle>(64)
24+
let collectedCoins = ResizeArray<Rectangle>(16)
2425
let keysToRemove = ResizeArray<struct (int * int)>(32)
2526

2627
let confettiColors = [|
@@ -98,11 +99,7 @@ let physicsSystem (dt: float32) (model: Model) : struct (Model * Cmd<Msg>) =
9899
if jumpStarted && canJump then
99100
spawnConfetti model
100101
velocityY <- jumpSpeed
101-
elif
102-
not canJump
103-
&& not jumpHeld
104-
&& velocityY < 0.0f
105-
then
102+
elif not canJump && not jumpHeld && velocityY < 0.0f then
106103
velocityY <- velocityY * jumpCutMultiplier
107104

108105
let velocity = Vector2(model.PlayerVelocity.X, velocityY)
@@ -141,16 +138,22 @@ let physicsSystem (dt: float32) (model: Model) : struct (Model * Cmd<Msg>) =
141138
isGrounded <- true
142139

143140
// Coin collection → increment score
144-
let mutable collectedCoins = ResizeArray<Rectangle>()
141+
collectedCoins.Clear()
145142

146143
for i = 0 to nearbyCoins.Count - 1 do
147144
if checkCollision playerRect nearbyCoins[i] then
148145
model.Score <- model.Score + 1
149146
collectedCoins.Add nearbyCoins[i]
150147

151148
// Remove collected coins from chunks (mark as Empty in grid)
152-
for coinRect in collectedCoins do
153-
for KeyValue(_key, chunk) in model.Chunks do
149+
for i = 0 to collectedCoins.Count - 1 do
150+
let coinRect = collectedCoins[i]
151+
let coinCx = int(Math.Floor(float coinRect.X / float chunkWorldSize))
152+
let coinCy = int(Math.Floor(float coinRect.Y / float chunkWorldSize))
153+
let key = struct (coinCx, coinCy)
154+
155+
match model.Chunks.TryGetValue key with
156+
| true, chunk ->
154157
let cellX =
155158
int((coinRect.X - chunk.Grid.Origin.X) / chunk.Grid.CellSize.X)
156159

@@ -163,9 +166,8 @@ let physicsSystem (dt: float32) (model: Model) : struct (Model * Cmd<Msg>) =
163166
&& cellY >= 0
164167
&& cellY < chunk.Grid.Height
165168
then
166-
match CellGrid2D.get cellX cellY chunk.Grid with
167-
| ValueSome Coin -> CellGrid2D.set cellX cellY Empty chunk.Grid
168-
| _ -> ()
169+
CellGrid2D.set cellX cellY Empty chunk.Grid
170+
| _ -> ()
169171

170172
// Respawn if fallen too far
171173
if finalPos.Y > groundLevel + 500.0f then
@@ -214,17 +216,41 @@ let physicsSystem (dt: float32) (model: Model) : struct (Model * Cmd<Msg>) =
214216
// System: Chunk Management (only runs when player changes chunk)
215217
// -------------------------------------------------------------
216218

219+
let private generateChunkAsync (cx: int) (cy: int) (seed: int) : Cmd<Msg> =
220+
Cmd.ofAsync
221+
(async { return generateChunk cx cy seed })
222+
(fun chunk -> ChunkCreated(struct (cx, cy), chunk))
223+
(fun _ex -> ChunkCreated(struct (cx, cy), generateChunk cx cy seed))
224+
217225
let chunkSystem (dt: float32) (model: Model) : struct (Model * Cmd<Msg>) =
218226
let pos = model.PlayerPosition
219227
let pcx = int(Math.Floor(float pos.X / float chunkWorldSize))
220228
let pcy = int(Math.Floor(float pos.Y / float chunkWorldSize))
221-
let currentChunk = struct (pcx, pcy)
229+
let keysToGenerate = ResizeArray<struct (int * int)>()
222230

223-
if currentChunk <> model.PlayerChunk then
224-
loadChunks pos model.Chunks model.Seed
225-
evictDistantChunks pos model.Chunks keysToRemove
231+
for x in pcx - chunkLoadRadius .. pcx + chunkLoadRadius do
232+
for y in pcy - chunkLoadRadius .. pcy + chunkLoadRadius do
233+
let key = struct (x, y)
226234

227-
model, Cmd.none
235+
if
236+
not(model.Chunks.ContainsKey(key))
237+
&& not(model.PendingChunks.Contains(key))
238+
then
239+
model.PendingChunks.Add(key) |> ignore
240+
keysToGenerate.Add(key)
241+
242+
evictDistantChunks pos model.Chunks keysToRemove
243+
244+
if keysToGenerate.Count = 0 then
245+
struct (model, Cmd.none)
246+
else
247+
let cmd =
248+
Cmd.batch [|
249+
for struct (x, y) in keysToGenerate do
250+
generateChunkAsync x y model.Seed
251+
|]
252+
253+
struct (model, cmd)
228254

229255
// -------------------------------------------------------------
230256
// System: Animation
@@ -295,14 +321,32 @@ let dayNightSystem (dt: float32) (model: Model) : struct (Model * Cmd<Msg>) =
295321

296322
let minimapSystem (dt: float32) (model: Model) : struct (Model * Cmd<Msg>) =
297323
let minimap = model.Minimap
298-
299-
Minimap.system
300-
model.Chunks
301-
model.DayNightTimeOfDay
302-
model.PlayerPosition
303-
&minimap
304-
305-
model, Cmd.none
324+
let posDelta = model.PlayerPosition - minimap.LastPlayerPos
325+
326+
let needsUpdate =
327+
minimap.FrameCounter % Minimap.updateInterval = 0
328+
|| posDelta.LengthSquared() > 4.0f
329+
330+
minimap.FrameCounter <- minimap.FrameCounter + 1
331+
332+
if needsUpdate then
333+
minimap.LastPlayerPos <- model.PlayerPosition
334+
335+
let cmd =
336+
Cmd.ofAsync
337+
(async {
338+
return
339+
Minimap.generateMinimapImage
340+
model.Chunks
341+
model.DayNightTimeOfDay
342+
model.PlayerPosition
343+
})
344+
(fun img -> MinimapReady img)
345+
(fun _ex -> MinimapReady(Raylib.GenImageColor(1, 1, Color.Black)))
346+
347+
model, cmd
348+
else
349+
model, Cmd.none
306350

307351
let diagnosticSystem (dt: float32) (model: Model) : struct (Model * Cmd<Msg>) =
308352
model.Diagnostics.Fps <- Raylib.GetFPS()
@@ -319,6 +363,17 @@ let update (msg: Msg) (model: Model) : struct (Model * Cmd<Msg>) =
319363
model.Actions <- actions
320364
model, Cmd.none
321365

366+
| ChunkCreated(key, chunk) ->
367+
model.Chunks[key] <- chunk
368+
model.PendingChunks.Remove(key) |> ignore
369+
model, Cmd.none
370+
371+
| MinimapReady image ->
372+
let mutable minimap = model.Minimap
373+
Minimap.uploadTexture image &minimap
374+
model.Minimap <- minimap
375+
model, Cmd.none
376+
322377
| Tick gt ->
323378
let dt = float32 gt.ElapsedGameTime.TotalSeconds
324379

samples/PlatformerSample/Types.fs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module PlatformerSample.Types
22

33
open System
4+
open System.Collections.Concurrent
45
open System.Collections.Generic
56
open System.Numerics
67
open Raylib_cs
@@ -79,7 +80,9 @@ type SpriteAssets = {
7980
// -------------------------------------------------------------
8081

8182
type MinimapModel() =
82-
member val Blocks = Dictionary<struct (int * int), struct (float32 * TileType * Biome)>() with get, set
83+
member val Blocks =
84+
Dictionary<struct (int * int), struct (float32 * TileType * Biome)>() with get, set
85+
8386
member val Texture = Unchecked.defaultof<Texture2D> with get, set
8487
member val TexReady = false with get, set
8588
member val FrameCounter = 0 with get, set
@@ -108,7 +111,10 @@ type Model() as self =
108111
member val PlayerSprite: AnimatedSprite = Unchecked.defaultof<_> with get, set
109112
member val TorchSprite: AnimatedSprite = Unchecked.defaultof<_> with get, set
110113
member val PlayerChunk = struct (0, 0) with get, set
111-
member val Chunks = Dictionary<struct (int * int), Chunk>() with get, set
114+
115+
member val Chunks =
116+
ConcurrentDictionary<struct (int * int), Chunk>() with get, set
117+
112118
member val Seed = 0 with get, set
113119
member val DayNightTimeOfDay = 12.0f with get, set
114120
member val DayNightDuration = 60.0f with get, set
@@ -117,6 +123,7 @@ type Model() as self =
117123
member val ParticleVelocities: Vector2[] = Array.zeroCreate 512 with get, set
118124
member val ParticleCount = 0 with get, set
119125
member val Score = 0 with get, set
126+
member val PendingChunks = HashSet<struct (int * int)>() with get, set
120127

121128
member val Diagnostics = Diagnostics() with get, set
122129
member val Minimap = MinimapModel() with get, set
@@ -134,3 +141,5 @@ type Model() as self =
134141
type Msg =
135142
| Tick of tick: GameTime
136143
| InputMapped of inputs: ActionState<GameAction>
144+
| ChunkCreated of key: struct (int * int) * chunk: Chunk
145+
| MinimapReady of image: Raylib_cs.Image

0 commit comments

Comments
 (0)