Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 235 additions & 6 deletions examples/new-api/systems/render-overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@
"time"
)

const (
fpsAvgSamples = 100
fontSize = 20
fpsGraphWidth = 160
fpsGraphHeight = 60
msGraphMaxValue = 33.33 // Max ms to show on graph (30 FPS)
)

func NewRenderOverlaySystem() RenderOverlaySystem {
return RenderOverlaySystem{}
}
Expand All @@ -50,6 +58,17 @@
monitorHeight int
debugLvl int
debug bool
lastFPSTime time.Time
frameCount int
currentFPS int
fpsSamples []int
fpsSampleSum int
fpsSampleIdx int
avgFPS float64
percentileFPS int
lastFrameDuration time.Duration
msHistory []float64 // ms per frame, ring buffer
msHistoryIdx int
}

func (s *RenderOverlaySystem) Init() {
Expand All @@ -65,6 +84,9 @@
Tint: rl.White,
Dst: rl.Rectangle{Width: float32(s.monitorWidth), Height: float32(s.monitorHeight)},
})

// Initialize ms history buffer for graph
s.msHistory = make([]float64, fpsGraphWidth)
}

func (s *RenderOverlaySystem) Run(dt time.Duration) bool {
Expand All @@ -88,6 +110,48 @@
}
}

// FPS calculation (custom)
now := time.Now()
if s.lastFPSTime.IsZero() {
s.lastFPSTime = now
s.fpsSamples = make([]int, fpsAvgSamples)
s.msHistory = make([]float64, fpsGraphWidth)
}
s.frameCount++
s.lastFrameDuration = dt

// Store current frame FPS in samples
frameFPS := 0
if dt > 0 {
// Correct calculation: convert duration to frames per second
frameFPS = int(time.Second / dt)
}
s.fpsSampleSum -= s.fpsSamples[s.fpsSampleIdx]
s.fpsSamples[s.fpsSampleIdx] = frameFPS
s.fpsSampleSum += frameFPS
s.fpsSampleIdx = (s.fpsSampleIdx + 1) % len(s.fpsSamples)

// Calculate average FPS over samples
s.avgFPS = float64(s.fpsSampleSum) / float64(len(s.fpsSamples))

// Calculate 1% FPS (lowest 1% frame in the sample window)
s.percentileFPS = s.calcPercentileFPS(0.01)

// Update frame time history (ms) on every frame
// Use average of last two frames for smoother graph
var ms float64
if s.lastFrameDuration > 0 {
ms = float64(s.lastFrameDuration.Microseconds()) / 1000.0
s.msHistory[s.msHistoryIdx] = ms
s.msHistoryIdx = (s.msHistoryIdx + 1) % len(s.msHistory)
}

if now.Sub(s.lastFPSTime) >= time.Second {
s.currentFPS = s.frameCount
s.frameCount = 0
s.lastFPSTime = now
}

s.Cameras.EachEntity()(func(entity ecs.Entity) bool {
camera := s.Cameras.GetUnsafe(entity)
frame := s.FrameBuffer2D.GetUnsafe(entity)
Expand Down Expand Up @@ -233,14 +297,18 @@
}

// Print stats
rl.DrawRectangleRec(rl.Rectangle{Height: 120, Width: 200}, rl.Black)
rl.DrawFPS(10, 10)
rl.DrawText(fmt.Sprintf("%d entities", s.EntityManager.Size()), 10, 70, 20, rl.RayWhite)
rl.DrawText(fmt.Sprintf("%d debugLvl", s.debugLvl), 10, 90, 20, rl.RayWhite)
const x = 10
const y = 10
statsPanelWidth := float32(200 + fpsGraphWidth)
statsPanelHeight := float32(y + fontSize*8)
rl.DrawRectangleRec(rl.Rectangle{Height: statsPanelHeight, Width: statsPanelWidth}, rl.Black)
s.drawCustomFPS(x, y)
rl.DrawText(fmt.Sprintf("%d entities", s.EntityManager.Size()), x, y+fontSize*6, fontSize, rl.RayWhite)
rl.DrawText(fmt.Sprintf("%d debugLvl", s.debugLvl), x, y+fontSize*7, 20, rl.RayWhite)
// Game over
s.SceneManager.EachComponent()(func(a *components.AsteroidSceneManager) bool {
rl.DrawText(fmt.Sprintf("Player HP: %d", a.PlayerHp), 10, 30, 20, rl.RayWhite)
rl.DrawText(fmt.Sprintf("Score: %d", a.PlayerScore), 10, 50, 20, rl.RayWhite)
rl.DrawText(fmt.Sprintf("Player HP: %d", a.PlayerHp), x, y+fontSize*4, 20, rl.RayWhite)
rl.DrawText(fmt.Sprintf("Score: %d", a.PlayerScore), x, y+fontSize*5, 20, rl.RayWhite)
if a.PlayerHp <= 0 {
text := "Game Over"
textSize := rl.MeasureTextEx(rl.GetFontDefault(), text, 96, 0)
Expand All @@ -264,6 +332,167 @@
return true
}

// Draws FPS stats: 1% low, current frame, average, and current FPS
func (s *RenderOverlaySystem) drawCustomFPS(x, y int32) {
fps := int32(s.currentFPS)

Check failure

Code scanning / gosec

integer overflow conversion uint64 -> uint32 Error

integer overflow conversion int -> int32

// Frame time in milliseconds
frameTimeMs := 0.0
if s.lastFrameDuration > 0 {
frameTimeMs = float64(s.lastFrameDuration.Microseconds()) / 1000.0
}

avgFPS := int32(s.avgFPS)
percentileFPS := int32(s.percentileFPS)

Check failure

Code scanning / gosec

integer overflow conversion uint64 -> int Error

integer overflow conversion int -> int32

// Colors
fontColor := rl.Lime
if fps < 30 {
fontColor = rl.Red
} else if fps < 60 {
fontColor = rl.Yellow
}

// Frame time color (lower is better)
frameTimeColor := rl.Lime
if frameTimeMs > 33.33 { // 30 FPS threshold (33.33ms)
frameTimeColor = rl.Red
} else if frameTimeMs > 16.67 { // 60 FPS threshold (16.67ms)
frameTimeColor = rl.Yellow
}

// Draw all stats
rl.DrawText(fmt.Sprintf("FPS: %d", fps), x, y, fontSize, fontColor)
rl.DrawText(fmt.Sprintf("Frame: %.2f ms", frameTimeMs), x, y+fontSize, fontSize, frameTimeColor)
rl.DrawText(fmt.Sprintf("Avg %d: %d", fpsAvgSamples, avgFPS), x, y+fontSize*2, fontSize, fontColor)
rl.DrawText(fmt.Sprintf("1%% Low: %d", percentileFPS), x, y+fontSize*3, fontSize, fontColor)

// Draw ms graph
s.drawMsGraph(x+180, y)
}

// Draw a graph of historical frame times in milliseconds
func (s *RenderOverlaySystem) drawMsGraph(x, y int32) {
// Draw graph border
rl.DrawRectangleLinesEx(rl.Rectangle{
X: float32(x),
Y: float32(y),
Width: float32(fpsGraphWidth),
Height: float32(fpsGraphHeight),
}, 1, rl.Gray)

// Draw graph background
rl.DrawRectangle(x+1, y+1, fpsGraphWidth-2, fpsGraphHeight-2, rl.Black)

// Draw horizontal reference lines (33.33ms, 16.67ms, 8.33ms)
// These correspond to 30 FPS, 60 FPS, and 120 FPS
refLines := []struct {
ms float32
color rl.Color
label string
}{
{33.33, rl.Red, "33.33 (30 FPS)"},
{16.67, rl.Yellow, "16.67 (60 FPS)"},
{8.33, rl.Green, "8.33 (120 FPS)"},
}
for _, ref := range refLines {
refY := y + int32(float32(fpsGraphHeight)*(ref.ms/msGraphMaxValue))
rl.DrawLineEx(
rl.NewVector2(float32(x), float32(refY)),
rl.NewVector2(float32(x+int32(fpsGraphWidth)), float32(refY)),
1.0,
ref.color,
)
rl.DrawText(ref.label, x+int32(fpsGraphWidth)+2, refY-8, 10, rl.Fade(ref.color, 0.8))
}

// Start from the oldest sample and move forward
startIdx := s.msHistoryIdx % len(s.msHistory)

// Draw frame time data points and connect with lines
for i := 0; i < len(s.msHistory)-1; i++ {
// Calculate indices in a way that we're drawing from left to right,
// with the newest data on the right
idx := (startIdx + i) % len(s.msHistory)
nextIdx := (startIdx + i + 1) % len(s.msHistory)

ms1 := float32(s.msHistory[idx])
ms2 := float32(s.msHistory[nextIdx])

// Clamp values to max
if ms1 > msGraphMaxValue {
ms1 = msGraphMaxValue
}
if ms2 > msGraphMaxValue {
ms2 = msGraphMaxValue
}

// Calculate positions (note: for ms, higher value = worse performance, so we scale directly)
x1 := x + int32(i)

Check failure

Code scanning / gosec

integer overflow conversion uint64 -> uint32 Error

integer overflow conversion int -> int32
y1 := y + int32(float32(fpsGraphHeight)*(ms1/msGraphMaxValue))
x2 := x + int32(i+1)

Check failure

Code scanning / gosec

integer overflow conversion uint64 -> uint32 Error

integer overflow conversion int -> int32
y2 := y + int32(float32(fpsGraphHeight)*(ms2/msGraphMaxValue))

// Choose color based on frame time
lineColor := rl.Green
if ms2 > 16.67 { // 60 FPS threshold
lineColor = rl.Yellow
}
if ms2 > 33.33 { // 30 FPS threshold
lineColor = rl.Red
}

// Skip drawing if either value is zero (not yet initialized)
if ms1 > 0 && ms2 > 0 {
rl.DrawLineEx(
rl.NewVector2(float32(x1), float32(y1)),
rl.NewVector2(float32(x2), float32(y2)),
2.0,
lineColor,
)
}
}

// Draw a vertical line indicating the current position in the buffer
currentX := x + int32(len(s.msHistory)-1)

Check failure

Code scanning / gosec

integer overflow conversion uint64 -> uint32 Error

integer overflow conversion int -> int32
rl.DrawLineEx(
rl.NewVector2(float32(currentX), float32(y)),
rl.NewVector2(float32(currentX), float32(y+int32(fpsGraphHeight))),
1.0,
rl.White,
)
}

// Calculates the given percentile FPS (e.g., 0.01 for 1% low)
func (s *RenderOverlaySystem) calcPercentileFPS(percentile float64) int {
n := len(s.fpsSamples)
if n == 0 {
return 0
}
// Copy and sort samples
sorted := make([]int, n)
copy(sorted, s.fpsSamples)
for i := 1; i < n; i++ {
key := sorted[i]
j := i - 1
for j >= 0 && sorted[j] > key {
sorted[j+1] = sorted[j]
j--
}
sorted[j+1] = key
}

// For 1% low, we want the 1st percentile (lowest values)
idx := int(float64(n) * percentile)
if idx < 0 {
idx = 0
}
if idx >= n {
idx = n - 1
}
return sorted[idx]
}

func (s *RenderOverlaySystem) intersects(rect1, rect2 vectors.Rectangle) bool {
return rect1.X < rect2.X+rect2.Width &&
rect1.X+rect1.Width > rect2.X &&
Expand Down
11 changes: 6 additions & 5 deletions pkg/ecs/component-bit-table.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ import (
)

const (
uintShift = 7 - 64/bits.UintSize
pageSizeMask = pageSize - 1
uintShift = 7 - 64/bits.UintSize
)

func NewComponentBitTable(maxComponentsLen int) ComponentBitTable {
bitsetSize := ((maxComponentsLen - 1) / bits.UintSize) + 1
return ComponentBitTable{
bitsetsBook: make([][]uint, 0, initialBookSize),
entitiesBook: make([][]Entity, 0, initialBookSize),
entitiesBook: make([]*entityArray, 0, initialBookSize),
lookup: NewPagedMap[Entity, int](),
bitsetSize: bitsetSize,
pageSize: bitsetSize * pageSize,
Expand All @@ -37,13 +36,15 @@ func NewComponentBitTable(maxComponentsLen int) ComponentBitTable {

type ComponentBitTable struct {
bitsetsBook [][]uint
entitiesBook [][]Entity
entitiesBook []*entityArray
lookup PagedMap[Entity, int]
length int
bitsetSize int
pageSize int
}

type entityArray [pageSize]Entity

func (b *ComponentBitTable) Create(entity Entity) {
assert.False(b.lookup.Has(entity), "entity already exists")

Expand Down Expand Up @@ -140,7 +141,7 @@ func (b *ComponentBitTable) extend() {
lastChunkId, lastEntityId := b.getPageIDAndEntityIndex(b.length)
if lastChunkId == len(b.bitsetsBook) && lastEntityId == 0 {
b.bitsetsBook = append(b.bitsetsBook, make([]uint, b.pageSize))
b.entitiesBook = append(b.entitiesBook, make([]Entity, pageSize))
b.entitiesBook = append(b.entitiesBook, &entityArray{})
}
}

Expand Down
Loading
Loading