Files
aego/tests/integration/platformer_lighting_gl_test.go
sanspie a46347e2c6
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m19s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 23m57s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
editor: add art studio and stage 12.5 workflows
2026-11-08 18:47:57 +03:00

173 lines
6.0 KiB
Go

//go:build aego_gltest
package integration
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"image"
"image/png"
"os"
"path/filepath"
"runtime"
"slices"
"testing"
"aego/app/run"
"aego/core/log"
"aego/modules/gameplay"
"aego/project"
"aego/render"
"aego/render/lighting"
)
// The demonstration route is opt-in because it draws thousands of real frames; the regular suite still verifies the complete gameplay replay.
func TestPlatformerLightingGLRoute(t *testing.T) {
dir := os.Getenv("AEGO_PLATFORMER_SHOTS")
if dir == "" {
t.Skip("set AEGO_PLATFORMER_SHOTS to capture the canonical native lighting route")
}
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(lightingRouteFile)
if err != nil {
t.Fatal(err)
}
type capture struct {
Frame int `json:"frame"`
Tick uint64 `json:"tick"`
Lighting bool `json:"lighting"`
File string `json:"file"`
GameplayHash string `json:"gameplayHash"`
Stats render.Stats `json:"render"`
}
report := struct {
Project string `json:"project"`
RouteSHA256 string `json:"routeSHA256"`
Renderer string `json:"renderer"`
Seed uint64 `json:"seed"`
Captures []capture `json:"captures"`
InputFrames int `json:"inputFrames"`
NativeWidth int `json:"width"`
NativeHeight int `json:"height"`
}{Project: "examples/platformer", Seed: 42, InputFrames: 1840}
hash := sha256.Sum256(data)
report.RouteSHA256 = hex.EncodeToString(hash[:])
frames := []int{400, 1039, 1216, 1840}
before := map[int]capture{}
beforePixels := map[int][]byte{}
var beforeHits []checkpointReached
for _, enabled := range []bool{true, false} {
name := "off"
if enabled {
name = "on"
}
t.Run(name, func(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
root := copyExample(t, "platformer")
p, err := project.Load(root)
if err != nil {
t.Fatal(err)
}
var logs bytes.Buffer
a, err := run.Open(p, run.Config{Hidden: true, FrameTime: 1.0 / 60, Seed: 42, Logger: log.New(log.NewTextSink(&logs)), NoProfile: true}, combatModule{})
if err != nil {
t.Fatal(err)
}
defer a.Close()
// The recording starts with a zero-delta frame. Enable lockstep
// after that frame so its input samples land on the recorded ticks.
report.Renderer = a.Engine.Caps().Renderer
pf := &platformer{app: a, root: root, player: a.Scene.Find(a.Scene.Root(), "Player")}
pf.pid, _ = a.Scripts.InstanceOf(pf.player, playerType)
var hits []checkpointReached
stop := observePlayerCheckpoints(t, pf, &hits)
defer stop()
a.Runner.SetLightingView(enabled, lighting.DebugLighting)
if _, err := actionsOf(t, pf).Replay(data); err != nil {
t.Fatal(err)
}
driver := a.Driver()
for frame := 1; frame <= report.InputFrames; frame++ {
var shot *image.RGBA
var captureErr error
captureNow := slices.Contains(frames, frame)
if captureNow {
a.Engine.CaptureFrame(func(img *image.RGBA, err error) { shot, captureErr = img, err })
}
if !a.Engine.Step(driver) {
t.Fatalf("route stopped at %d: %v\n%s", frame, a.Engine.Err(), logs.String())
}
if tick := a.Engine.MainWorld().Time.Tick(); tick != uint64(frame-1) {
t.Fatalf("recorded replay clock mismatch at frame %d: tick=%d, want %d", frame, tick, frame-1)
}
if frame == 1 {
a.Engine.SetLockstep(true)
}
if !captureNow {
continue
}
if captureErr != nil || shot == nil {
t.Fatalf("capture %d failed: %v", frame, captureErr)
}
report.NativeWidth, report.NativeHeight = shot.Rect.Dx(), shot.Rect.Dy()
filename := fmt.Sprintf("%s-%04d.png", name, frame)
f, err := os.Create(filepath.Join(dir, filename))
if err != nil {
t.Fatal(err)
}
err = png.Encode(f, shot)
closeErr := f.Close()
if err != nil || closeErr != nil {
t.Fatalf("save capture: %v %v", err, closeErr)
}
c := capture{Frame: frame, Tick: a.Engine.MainWorld().Time.Tick(), Lighting: enabled, File: filename, GameplayHash: fmt.Sprintf("%016x", gameplayDigest(pf)), Stats: a.RenderStats()}
if c.Stats.MaterialCompileFailures != 0 || c.Stats.LightingFailures != 0 {
t.Fatalf("GPU preparation failed: %+v\n%s", c.Stats, logs.String())
}
if enabled {
before[frame], beforePixels[frame] = c, slices.Clone(shot.Pix)
} else {
if c.GameplayHash != before[frame].GameplayHash {
t.Fatalf("lighting view changed gameplay at %d", frame)
}
if bytes.Equal(shot.Pix, beforePixels[frame]) {
t.Fatalf("lighting on/off produced the same pixels at %d", frame)
}
}
report.Captures = append(report.Captures, c)
t.Logf("%s frame=%d player=%v drawCalls=%d lights=%d shadowMasks=%d outputBytes=%d", name, frame, a.Scene.WorldPosition(pf.player), c.Stats.DrawCalls, c.Stats.SelectedLights, c.Stats.DirtyShadowMasks, c.Stats.LightingTargetBytes)
}
pf.noFaults(t)
if at := a.Scene.WorldPosition(pf.player); at.X < 66.5 || !pf.motion().Grounded {
t.Fatalf("native GPU replay missed Finish: %v", at)
}
finish := a.Scene.Find(a.Scene.Root(), "Finish")
if len(hits) < 2 || hits[len(hits)-1].point != a.Scene.ID(finish) || hits[len(hits)-1].by != a.Scene.ID(pf.player) || hits[len(hits)-1].tick != lightingFinishTick {
t.Fatalf("native GPU replay did not activate intermediate checkpoints and Finish at tick %d: %+v", lightingFinishTick, hits)
}
if stats := gameplay.Of(a.Engine.MainWorld()).Stats(); stats.Respawns != 0 {
t.Fatalf("native GPU replay respawned: %+v", stats)
}
if enabled {
beforeHits = slices.Clone(hits)
} else if !slices.Equal(beforeHits, hits) {
t.Fatalf("lighting changed actual checkpoint activation: %v != %v", beforeHits, hits)
}
t.Logf("native checkpoints=%+v", hits)
})
}
encoded, err := json.MarshalIndent(report, "", " ")
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "results.json"), encoded, 0o644); err != nil {
t.Fatal(err)
}
}