From 41fb0a3d5a081f7424bc1546fbbbc904a8cc105a Mon Sep 17 00:00:00 2001 From: codegirl-007 Date: Fri, 14 Aug 2026 22:32:28 -0700 Subject: [PATCH] Cache clip defs on sprites to avoid per-frame map lookups. Crowd stress (128 sprites) showed Clip_Def string map_get as the top engine cost; cache on set_sprite_clip and use it in update/draw. --- engine/sprite.odin | 49 +++++++++++++++++++++------------------- examples/crowd/main.odin | 37 ++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/engine/sprite.odin b/engine/sprite.odin index 12a9dba..e3883c8 100644 --- a/engine/sprite.odin +++ b/engine/sprite.odin @@ -5,12 +5,14 @@ import sdl "vendor:sdl3" Vec2 :: [2]f32 Sprite :: struct { - data: ^Character_Data, - position: Vec2, - clip: string, - frame: int, - time: f32, - flip_x: bool, + data: ^Character_Data, + position: Vec2, + clip: string, + clip_def: Clip_Def, // shallow cache; frames alias Character_Data (do not mutate clips map after spawn) + has_clip: bool, + frame: int, + time: f32, + flip_x: bool, } spawn_sprite :: proc( @@ -24,16 +26,14 @@ spawn_sprite :: proc( position = position, } set_sprite_clip(&sprite, clip) - if frame != 0 { - c, ok := character_clip(sprite.data, sprite.clip) - if ok { - if frame < 0 { - sprite.frame = 0 - } else if frame >= len(c.frames) { - sprite.frame = len(c.frames) - 1 - } else { - sprite.frame = frame - } + if frame != 0 && sprite.has_clip { + c := sprite.clip_def + if frame < 0 { + sprite.frame = 0 + } else if frame >= len(c.frames) { + sprite.frame = len(c.frames) - 1 + } else { + sprite.frame = frame } } return sprite @@ -42,9 +42,9 @@ spawn_sprite :: proc( update_sprite :: proc(sprite: ^Sprite, dt: f32) { if sprite == nil || sprite.data == nil do return if dt <= 0 do return + if !sprite.has_clip do return - clip, ok := character_clip(sprite.data, sprite.clip) - if !ok do return + clip := sprite.clip_def frame_count := len(clip.frames) if frame_count <= 0 do return @@ -92,12 +92,13 @@ draw_sprite :: proc(app: ^App, sprite: ^Sprite) { if len(app.draw_list) >= MAX_SPRITES { return } - - frame, ok := character_frame(sprite.data, sprite.clip, sprite.frame) - if !ok { + if !sprite.has_clip do return + if sprite.frame < 0 || sprite.frame >= len(sprite.clip_def.frames) { return } + frame := sprite.clip_def.frames[sprite.frame] + src_w := f32(frame.source_size[0]) src_h := f32(frame.source_size[1]) if src_w <= 0 do src_w = f32(frame.rect[2]) @@ -148,12 +149,14 @@ draw_sprite :: proc(app: ^App, sprite: ^Sprite) { set_sprite_clip :: proc(sprite: ^Sprite, clip: string) { if sprite == nil || sprite.data == nil do return - if sprite.clip == clip do return + if sprite.clip == clip && sprite.has_clip do return - _, ok := character_clip(sprite.data, clip) + def, ok := character_clip(sprite.data, clip) if !ok do return sprite.clip = clip + sprite.clip_def = def + sprite.has_clip = true sprite.frame = 0 sprite.time = 0 } diff --git a/examples/crowd/main.odin b/examples/crowd/main.odin index 42e159d..6efa950 100644 --- a/examples/crowd/main.odin +++ b/examples/crowd/main.odin @@ -1,9 +1,12 @@ package main +import "core:fmt" import eng "pkg:engine" -// Many sprites sharing one Character_Data (Flyweight) — good batching demo. -COUNT :: 24 +// Stress the draw budget: one Character_Data, MAX_SPRITES instances. +COUNT :: eng.MAX_SPRITES +COLS :: 16 +FRAME_LOG_EVERY :: 60 main :: proc() { app: eng.App @@ -16,25 +19,45 @@ 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(40 + col * 48), + f32(80 + row * 60), } 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} last := eng.now_seconds() + frame_i := 0 + sum_ms: f64 + peak_ms: f64 for eng.events() { now := eng.now_seconds() dt := f32(now - last) last = now + frame_ms := f64(dt) * 1000.0 + sum_ms += frame_ms + if frame_ms > peak_ms do peak_ms = frame_ms + frame_i += 1 + + if frame_i % FRAME_LOG_EVERY == 0 { + avg := sum_ms / f64(FRAME_LOG_EVERY) + fps := 1000.0 / avg if avg > 0 else 0 + fmt.printfln( + "crowd frame: avg=%.2f ms (%.1f FPS) peak=%.2f ms over %d frames", + avg, + fps, + peak_ms, + FRAME_LOG_EVERY, + ) + sum_ms = 0 + peak_ms = 0 + } for &s in sprites { eng.update_sprite(&s, dt)