From 0c06d75e86c6e6c85079707e1078001bfe26dabd Mon Sep 17 00:00:00 2001 From: codegirl-007 Date: Sun, 9 Aug 2026 21:12:57 -0700 Subject: [PATCH] Cursor: Apply local changes for cloud agent --- engine/app.odin | 18 ++- engine/fps_overlay.odin | 279 +++++++++++++++++++++++++++++++++++++++ engine/sprite.odin | 7 +- examples/crowd/main.odin | 21 ++- 4 files changed, 317 insertions(+), 8 deletions(-) create mode 100644 engine/fps_overlay.odin diff --git a/engine/app.odin b/engine/app.odin index 7bc961a..0e73a3f 100644 --- a/engine/app.odin +++ b/engine/app.odin @@ -11,7 +11,7 @@ Vertex :: struct { } SPRITE_VERT_COUNT :: 6 -MAX_SPRITES :: 128 +MAX_SPRITES :: 1012 // 1000 game sprites + FPS overlay glyphs SPRITE_VERTS_SIZE :: SPRITE_VERT_COUNT * size_of(Vertex) VERTEX_BUFFER_SIZE :: MAX_SPRITES * SPRITE_VERTS_SIZE @@ -36,6 +36,12 @@ App :: struct { draw_list: [dynamic]Queued_Sprite, clear_color: sdl.FColor, camera: Camera, + show_fps: bool, + fps_texture: ^sdl.GPUTexture, + fps_smooth: f32, + fps_display: int, + fps_last_time: f64, + fps_update_accum: f32, } Shader_Backend :: enum { @@ -140,6 +146,10 @@ init :: proc(app: ^App, title: cstring, width, height: i32) -> bool { app.camera = camera_default() + if !fps_overlay_init(app) { + fmt.eprintfln("fps overlay init failed; continuing without on-screen FPS") + } + return true } @@ -152,6 +162,8 @@ shutdown :: proc(app: ^App) { fmt.eprintfln("WaitForGPUIdle failed") } + fps_overlay_shutdown(app) + if app.transfer_buffer != nil { sdl.ReleaseGPUTransferBuffer(app.device, app.transfer_buffer) } @@ -195,6 +207,8 @@ events :: proc() -> bool { } begin_frame :: proc(app: ^App, clear_color: sdl.FColor = {0.12, 0.12, 0.16, 1}) { + fps_overlay_begin_frame(app) + clear(&app.draw_list) app.clear_color = clear_color app.render_pass = nil @@ -241,6 +255,8 @@ end_frame :: proc(app: ^App) { return } + fps_overlay_queue(app) + n := len(app.draw_list) if n > 0 { map_ptr := sdl.MapGPUTransferBuffer(app.device, app.transfer_buffer, false) diff --git a/engine/fps_overlay.odin b/engine/fps_overlay.odin new file mode 100644 index 0000000..3d0e81f --- /dev/null +++ b/engine/fps_overlay.odin @@ -0,0 +1,279 @@ +package engine + +import "core:fmt" +import "core:mem" +import sdl "vendor:sdl3" + +// Enough for an 8-digit rate plus " FPS". +FPS_OVERLAY_MAX_GLYPHS :: 12 +FPS_GLYPH_W :: 5 +FPS_GLYPH_H :: 7 +FPS_CELL_W :: 6 +FPS_CELL_H :: 8 +FPS_ATLAS_COLS :: 16 +FPS_SCALE :: 2 +FPS_MARGIN :: 8 +FPS_UPDATE_INTERVAL :: 0.25 + +// Glyph indices in the debug atlas. +FPS_GLYPH_DIGIT_0 :: 0 +FPS_GLYPH_F :: 10 +FPS_GLYPH_P :: 11 +FPS_GLYPH_S :: 12 +FPS_GLYPH_SPACE :: 13 + +// 5x7 bitmaps (MSB = left). Digits 0-9, then F/P/S/space. +@(private) +fps_glyph_rows := [14][7]u8 { + {0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E}, // 0 + {0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E}, // 1 + {0x0E, 0x11, 0x01, 0x06, 0x08, 0x10, 0x1F}, // 2 + {0x0E, 0x11, 0x01, 0x06, 0x01, 0x11, 0x0E}, // 3 + {0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02}, // 4 + {0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E}, // 5 + {0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E}, // 6 + {0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08}, // 7 + {0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E}, // 8 + {0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C}, // 9 + {0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10}, // F + {0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10}, // P + {0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E}, // S + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // space +} + +set_show_fps :: proc(app: ^App, enabled: bool) { + if app == nil do return + app.show_fps = enabled +} + +@(private) +fps_overlay_init :: proc(app: ^App) -> bool { + app.show_fps = true + app.fps_smooth = 0 + app.fps_display = 0 + app.fps_last_time = 0 + app.fps_update_accum = 0 + app.fps_texture = nil + + atlas_w := FPS_ATLAS_COLS * FPS_CELL_W + atlas_h := FPS_CELL_H + pixels := make([]u8, atlas_w * atlas_h * 4) + defer delete(pixels) + + for gi in 0 ..< len(fps_glyph_rows) { + gx := gi * FPS_CELL_W + rows := fps_glyph_rows[gi] + for row in 0 ..< FPS_GLYPH_H { + bits := rows[row] + for col in 0 ..< FPS_GLYPH_W { + on := (bits >> u8(FPS_GLYPH_W - 1 - col)) & 1 == 1 + px := gx + col + py := row + i := (py * atlas_w + px) * 4 + if on { + pixels[i + 0] = 255 + pixels[i + 1] = 255 + pixels[i + 2] = 255 + pixels[i + 3] = 255 + } + } + } + } + + app.fps_texture = sdl.CreateGPUTexture( + app.device, + { + type = .D2, + format = .R8G8B8A8_UNORM, + usage = {.SAMPLER}, + width = u32(atlas_w), + height = u32(atlas_h), + layer_count_or_depth = 1, + num_levels = 1, + sample_count = ._1, + }, + ) + if app.fps_texture == nil { + fmt.eprintfln("FPS overlay CreateGPUTexture failed: %s", sdl.GetError()) + return false + } + + upload_size := atlas_w * atlas_h * 4 + tbuf := sdl.CreateGPUTransferBuffer(app.device, {usage = .UPLOAD, size = u32(upload_size)}) + if tbuf == nil { + fmt.eprintfln("FPS overlay CreateGPUTransferBuffer failed: %s", sdl.GetError()) + sdl.ReleaseGPUTexture(app.device, app.fps_texture) + app.fps_texture = nil + return false + } + defer sdl.ReleaseGPUTransferBuffer(app.device, tbuf) + + map_ptr := sdl.MapGPUTransferBuffer(app.device, tbuf, false) + if map_ptr == nil { + fmt.eprintfln("FPS overlay MapGPUTransferBuffer failed: %s", sdl.GetError()) + sdl.ReleaseGPUTexture(app.device, app.fps_texture) + app.fps_texture = nil + return false + } + mem.copy(map_ptr, raw_data(pixels), upload_size) + sdl.UnmapGPUTransferBuffer(app.device, tbuf) + + cmd := sdl.AcquireGPUCommandBuffer(app.device) + if cmd == nil { + fmt.eprintfln("FPS overlay AcquireGPUCommandBuffer failed: %s", sdl.GetError()) + sdl.ReleaseGPUTexture(app.device, app.fps_texture) + app.fps_texture = nil + return false + } + + copy_pass := sdl.BeginGPUCopyPass(cmd) + transfer := sdl.GPUTextureTransferInfo { + transfer_buffer = tbuf, + offset = 0, + pixels_per_row = u32(atlas_w), + rows_per_layer = u32(atlas_h), + } + region := sdl.GPUTextureRegion { + texture = app.fps_texture, + w = u32(atlas_w), + h = u32(atlas_h), + d = 1, + } + sdl.UploadToGPUTexture(copy_pass, transfer, region, false) + sdl.EndGPUCopyPass(copy_pass) + + fence := sdl.SubmitGPUCommandBufferAndAcquireFence(cmd) + if fence == nil { + fmt.eprintfln("FPS overlay texture upload submit failed: %s", sdl.GetError()) + sdl.ReleaseGPUTexture(app.device, app.fps_texture) + app.fps_texture = nil + return false + } + defer sdl.ReleaseGPUFence(app.device, fence) + + if !sdl.WaitForGPUFences(app.device, true, &fence, 1) { + fmt.eprintfln("FPS overlay WaitForGPUFences failed: %s", sdl.GetError()) + sdl.ReleaseGPUTexture(app.device, app.fps_texture) + app.fps_texture = nil + return false + } + + return true +} + +@(private) +fps_overlay_shutdown :: proc(app: ^App) { + if app.device != nil && app.fps_texture != nil { + sdl.ReleaseGPUTexture(app.device, app.fps_texture) + } + app.fps_texture = nil +} + +@(private) +fps_overlay_begin_frame :: proc(app: ^App) { + now := now_seconds() + if app.fps_last_time > 0 { + dt := now - app.fps_last_time + if dt > 0 { + instant := f32(1.0 / dt) + if app.fps_smooth <= 0 { + app.fps_smooth = instant + } else { + app.fps_smooth = app.fps_smooth * 0.9 + instant * 0.1 + } + app.fps_update_accum += f32(dt) + if app.fps_display == 0 || app.fps_update_accum >= FPS_UPDATE_INTERVAL { + app.fps_display = int(app.fps_smooth + 0.5) + app.fps_update_accum = 0 + } + } + } + app.fps_last_time = now +} + +@(private) +fps_overlay_queue :: proc(app: ^App) { + if !app.show_fps || app.fps_texture == nil do return + if app.cmd == nil || app.swapchain_texture == nil do return + if app.swapchain_w == 0 || app.swapchain_h == 0 do return + + fps := app.fps_display + if fps < 0 do fps = 0 + + // Build "N… FPS" with the full measured rate (no artificial cap). + glyphs: [FPS_OVERLAY_MAX_GLYPHS]int + count := 0 + + digits: [8]int + dcount := 0 + if fps == 0 { + digits[0] = 0 + dcount = 1 + } else { + v := fps + for v > 0 && dcount < len(digits) { + digits[dcount] = v % 10 + dcount += 1 + v /= 10 + } + for i in 0 ..< dcount / 2 { + digits[i], digits[dcount - 1 - i] = digits[dcount - 1 - i], digits[i] + } + } + + for i in 0 ..< dcount { + if count >= FPS_OVERLAY_MAX_GLYPHS do break + glyphs[count] = FPS_GLYPH_DIGIT_0 + digits[i] + count += 1 + } + if count + 4 <= FPS_OVERLAY_MAX_GLYPHS { + glyphs[count] = FPS_GLYPH_SPACE + count += 1 + glyphs[count] = FPS_GLYPH_F + count += 1 + glyphs[count] = FPS_GLYPH_P + count += 1 + glyphs[count] = FPS_GLYPH_S + count += 1 + } + + sw := f32(app.swapchain_w) + sh := f32(app.swapchain_h) + atlas_w := f32(FPS_ATLAS_COLS * FPS_CELL_W) + atlas_h := f32(FPS_CELL_H) + gw := f32(FPS_GLYPH_W * FPS_SCALE) + gh := f32(FPS_GLYPH_H * FPS_SCALE) + advance := f32(FPS_CELL_W * FPS_SCALE) + x := f32(FPS_MARGIN) + y := f32(FPS_MARGIN) + + for i in 0 ..< count { + if len(app.draw_list) >= MAX_SPRITES do break + + gi := glyphs[i] + u0 := f32(gi * FPS_CELL_W) / atlas_w + v0 := f32(0) + u1 := f32(gi * FPS_CELL_W + FPS_GLYPH_W) / atlas_w + v1 := f32(FPS_GLYPH_H) / atlas_h + + x0 := x + f32(i) * advance + y0 := y + x1 := x0 + gw + y1 := y0 + gh + + p0 := to_clip(x0, y0, sw, sh) + p1 := to_clip(x1, y0, sw, sh) + p2 := to_clip(x1, y1, sw, sh) + p3 := to_clip(x0, y1, sw, sh) + + verts := [SPRITE_VERT_COUNT]Vertex { + {pos = p0, uv = {u0, v0}}, + {pos = p1, uv = {u1, v0}}, + {pos = p2, uv = {u1, v1}}, + {pos = p0, uv = {u0, v0}}, + {pos = p2, uv = {u1, v1}}, + {pos = p3, uv = {u0, v1}}, + } + append(&app.draw_list, Queued_Sprite{texture = app.fps_texture, verts = verts}) + } +} diff --git a/engine/sprite.odin b/engine/sprite.odin index 12a9dba..0babe90 100644 --- a/engine/sprite.odin +++ b/engine/sprite.odin @@ -89,7 +89,12 @@ draw_sprite :: proc(app: ^App, sprite: ^Sprite) { if sprite == nil || sprite.data == nil || sprite.data.texture == nil { return } - if len(app.draw_list) >= MAX_SPRITES { + // Reserve room for the engine FPS overlay glyphs when enabled. + max_game := MAX_SPRITES + if app.show_fps && app.fps_texture != nil { + max_game -= FPS_OVERLAY_MAX_GLYPHS + } + if len(app.draw_list) >= max_game { return } diff --git a/examples/crowd/main.odin b/examples/crowd/main.odin index 42e159d..20dd960 100644 --- a/examples/crowd/main.odin +++ b/examples/crowd/main.odin @@ -3,7 +3,12 @@ package main import eng "pkg:engine" // Many sprites sharing one Character_Data (Flyweight) — good batching demo. -COUNT :: 24 +COUNT :: 1000 +COLS :: 40 +CELL_W :: 20 +CELL_H :: 24 +ORIGIN_X :: 20 +ORIGIN_Y :: 40 main :: proc() { app: eng.App @@ -16,18 +21,22 @@ main :: proc() { sprites: [COUNT]eng.Sprite for i in 0 ..< COUNT { - col := i % 8 - row := i / 8 + col := i % COLS + row := i / COLS pos := eng.Vec2 { - f32(120 + col * 80), - f32(280 + row * 120), + f32(ORIGIN_X + col * CELL_W), + f32(ORIGIN_Y + row * CELL_H), } clip := "idle" if (i % 2) == 0 else "walk" sprites[i] = eng.spawn_sprite(&data, pos, clip, i % 5) } // Look at the middle of the grid - app.camera.position = {400, 400} + rows := (COUNT + COLS - 1) / COLS + app.camera.position = { + f32(ORIGIN_X + (COLS - 1) * CELL_W / 2), + f32(ORIGIN_Y + (rows - 1) * CELL_H / 2), + } last := eng.now_seconds()