Files
aego/engine/loop.go
T
2026-10-19 01:47:39 +03:00

251 lines
5.9 KiB
Go

package engine
import (
"runtime/metrics"
"time"
"aego/core/clock"
"aego/core/errs"
"aego/core/rng"
"aego/platform"
)
const idleSleep = 16 * time.Millisecond
func (e *Engine) Step(app App) bool {
if e.closed || e.err != nil {
return false
}
if !e.appInit {
e.app = app
if err := app.Init(e); err != nil {
e.err = errs.Wrap(err, errs.EngineInit, "app initialization failed")
return false
}
e.appInit = true
e.lastTime = e.plat.Now()
}
e.frame()
return !e.quit
}
func gated(p Phase) bool {
return p >= PhaseFixedBegin && p <= PhaseAnimation
}
func (e *Engine) exec(ctx *Context, p Phase, mask SystemScope, paused bool) {
list := e.reg.systems.phases[p]
if len(list) == 0 {
return
}
var before uint64
if e.probe {
before = e.objects()
}
gate := gated(p)
ps := e.tracer.Begin(e.phaseZones[p])
for i := range list {
s := &list[i]
if s.scope&mask == 0 || (gate && !s.pause.runs(paused)) {
continue
}
z := e.tracer.Begin(s.zone)
s.sys.Run(ctx)
z.End()
}
ps.End()
if e.probe {
e.allocs[p] += e.objects() - before
}
}
func (e *Engine) SetPhaseAllocs(on bool) {
if on && len(e.probeSample) == 0 {
e.probeSample = []metrics.Sample{{Name: "/gc/heap/allocs:objects"}}
}
e.probe = on
}
func (e *Engine) objects() uint64 {
metrics.Read(e.probeSample)
if v := e.probeSample[0].Value; v.Kind() == metrics.KindUint64 {
return v.Uint64()
}
return 0
}
func (e *Engine) PhaseAllocs(dst []uint64) []uint64 {
return append(dst, e.lastAllocs[:]...)
}
func (e *Engine) random(w *World) *rng.Source {
if w != nil && w.Random != nil {
return w.Random
}
return e.ctx.Random
}
func (e *Engine) begin(ctx *Context, w *World, real float64) clock.Plan {
t := w.Time
plan := t.Begin(real)
rnd := e.random(w)
*ctx = Context{
Engine: e,
World: w,
Time: t,
Dt: t.Span(clock.Game).Delta,
Unscaled: t.Span(clock.Real).Delta,
Alpha: t.Alpha(),
Frame: t.Frame(),
Tick: t.Tick(),
Paused: plan.Paused,
Random: rnd,
}
if w.Recorder != nil {
w.Recorder.Stamp(uint64(w.ID()), ctx.Frame, ctx.Tick)
}
return plan
}
func (e *Engine) mark(w *World, steps int) {
t := w.Time
var first, last uint64
if steps > 0 {
first, last = t.Tick()+1, t.Tick()+uint64(steps)
}
e.tracer.Mark(uint64(w.ID()), t.Frame(), first, last)
}
func (e *Engine) simulate(ctx *Context, mask SystemScope, plan clock.Plan) {
t, w := ctx.Time, ctx.World
fixed := t.FixedDelta()
paused := plan.Paused
for range plan.Steps {
ctx.Tick = t.BeginTick()
if w.Recorder != nil {
w.Recorder.Stamp(uint64(w.ID()), ctx.Frame, ctx.Tick)
}
ctx.Dt, ctx.Alpha = fixed, 0
for p := PhaseFixedBegin; p <= PhaseFixedEnd; p++ {
e.exec(ctx, p, mask, false)
}
if w.Break != nil && w.Break() {
t.SetPaused(true)
paused = true
break
}
}
ctx.Paused = paused
ctx.Dt, ctx.Alpha = t.Span(clock.Game).Delta, t.Alpha()
if paused {
ctx.Dt = 0
}
for p := PhasePreUpdate; p <= PhaseAnimation; p++ {
e.exec(ctx, p, mask, paused)
}
if !paused && w.Break != nil && w.Break() {
t.SetPaused(true)
}
}
func (e *Engine) TickWorld(w *World, scope SystemScope, real float64) clock.Plan {
if w == nil || w.Time == nil {
return clock.Plan{}
}
saved := e.wctx
plan := e.begin(&e.wctx, w, real)
e.mark(w, plan.Steps)
e.exec(&e.wctx, PhaseFrameBegin, scope, plan.Paused)
e.exec(&e.wctx, PhaseInput, scope, plan.Paused)
e.simulate(&e.wctx, scope, plan)
e.wctx = saved
return plan
}
func (e *Engine) RunPhase(w *World, p Phase, scope SystemScope, dt, alpha float64) {
if w == nil || int(p) >= phaseCount {
return
}
saved := e.wctx
e.wctx = Context{Engine: e, World: w, Dt: dt, Alpha: alpha, Frame: e.frameNo, Random: e.random(w)}
if t := w.Time; t != nil {
e.wctx.Time, e.wctx.Frame, e.wctx.Tick = t, t.Frame(), t.Tick()
e.wctx.Unscaled = t.Span(clock.Real).Delta
}
e.exec(&e.wctx, p, scope, false)
e.wctx = saved
}
func (e *Engine) RunFixed(w *World, scope SystemScope, dt float64) {
for p := PhaseFixedBegin; p <= PhaseFixedEnd; p++ {
e.RunPhase(w, p, scope, dt, 0)
}
}
func (e *Engine) RunVariable(w *World, scope SystemScope, dt, alpha float64) {
for p := PhasePreUpdate; p <= PhaseAnimation; p++ {
e.RunPhase(w, p, scope, dt, alpha)
}
}
func (e *Engine) frame() {
e.lastAllocs, e.allocs = e.allocs, [phaseCount]uint64{}
e.tracer.BeginFrame()
now := e.plat.Now()
delta := 0.0
if e.frameNo > 0 {
delta = now - e.lastTime
}
if delta < 0 {
delta = 0
}
e.lastTime = now
e.frameNo++
mask := ScopeFrame | e.cfg.LoopScope
plan := e.begin(&e.ctx, e.main, delta)
e.mark(e.main, plan.Steps)
e.jobs.drain()
e.exec(&e.ctx, PhaseFrameBegin, mask, plan.Paused)
e.input.BeginFrame()
pz := e.tracer.Begin(e.zPoll)
e.plat.PollEvents(e.onEvent)
pz.End()
e.exec(&e.ctx, PhasePollEvents, mask, plan.Paused)
e.exec(&e.ctx, PhaseInput, mask, plan.Paused)
updateStart := e.plat.Now()
e.simulate(&e.ctx, mask, plan)
renderStart := e.plat.Now()
e.stats.UpdateTime = renderStart - updateStart
e.stats.ResetGPU()
if !e.minimized && e.metrics.PixelW > 0 && e.metrics.PixelH > 0 {
e.exec(&e.ctx, PhaseRenderPrepare, mask, plan.Paused)
e.curFrame = e.dev.BeginFrame(e.metrics.PixelW, e.metrics.PixelH)
e.exec(&e.ctx, PhaseRender, mask, plan.Paused)
e.exec(&e.ctx, PhaseRenderUI, mask, plan.Paused)
gs := e.curFrame.Stats()
e.dev.EndFrame()
e.curFrame = nil
sz := e.tracer.Begin(e.zSwap)
e.win.SwapBuffers()
sz.End()
e.stats.DrawCalls = gs.DrawCalls
e.stats.Vertices = gs.Vertices
e.stats.Triangles = gs.Triangles
e.stats.TexSwitches = gs.TexSwitches
} else if e.minimized {
time.Sleep(idleSleep)
}
e.stats.RenderTime = e.plat.Now() - renderStart
if lim := e.frameLimit; lim > 0 && e.vsync == platform.VSyncOff && !e.cfg.Headless {
if wait := 1/lim - (e.plat.Now() - now); wait > 0 {
time.Sleep(time.Duration(wait * float64(time.Second)))
}
}
e.exec(&e.ctx, PhaseFrameEnd, mask, plan.Paused)
e.stats.FrameTime = delta
e.counter.Sample(&e.stats)
e.tracer.EndFrame()
}