Files
aego/tests/gl/material_phases_test.go
sanspie a6822156cd
ci-platforms / windows amd64 smoke (push) Skipped
ci / release (push) Successful in 8m58s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 4m46s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 13m28s
Finish Stage 12 editor fixes and refreshed platformer
2026-11-05 17:36:42 +03:00

141 lines
5.0 KiB
Go

//go:build aego_gltest
package gl
import (
"encoding/json"
"fmt"
"os"
"testing"
"time"
"aego/asset"
shaderimport "aego/asset/importers/shader"
"aego/asset/pipeline"
"aego/core/ids"
"aego/core/mathx"
"aego/gpu"
"aego/render"
"aego/render/material"
"aego/render/shader"
"aego/render/sprite"
)
// TestMaterialPhaseMeasurements separates import/reference, player decode, driver link, first draw and frame-end publication.
// Timings are observations, not machine-independent CI thresholds; publication, explicit values and GPU resource bounds are gated.
func TestMaterialPhaseMeasurements(t *testing.T) {
r, dev, target := materialRig(t)
const count = 5
type sample struct {
ImportReferenceNS int64 `json:"importReferenceNS"`
DecodeNS int64 `json:"decodeNS"`
DriverLinkNS int64 `json:"driverLinkNS"`
QueueNS int64 `json:"queueNS"`
CommitLatencyNS int64 `json:"commitLatencyNS"`
FrameBoundaryNS int64 `json:"frameBoundaryNS"`
}
var samples [count]sample
var candidates [count]*material.Shader
imp := shaderimport.New()
for i := range candidates {
source := fmt.Sprintf(`#pragma aego abi 1
#pragma material
uniform float Amount; // @default(0.5)
uniform float Scale; // @instance @default(1.0)
#pragma end
void fragment(){
float value=Amount*Scale+float(%d)*0.000001;
aego_out=vec4(value,0.0,0.0,value);
}
`, i)
started := time.Now()
result, err := imp.Import(&pipeline.ImportContext{Path: "assets/phase-measure.aeshader", Source: []byte(source)})
samples[i].ImportReferenceNS = time.Since(started).Nanoseconds()
if err != nil {
t.Fatal(err)
}
started = time.Now()
artifact, err := asset.DecodeShader(result.Artifacts[0].Data)
if err != nil {
t.Fatal(err)
}
if artifact.Validation&shader.ReferenceChecked == 0 {
t.Fatal("phase measurement requires the pinned glslang validator")
}
candidates[i] = artifact.Runtime(ids.AssetID{}, "phase-measure")
samples[i].DecodeNS = time.Since(started).Nanoseconds()
stages := candidates[i].Variants[0].Sources
started = time.Now()
h, err := dev.CreateShader(gpu.ShaderDesc{Label: "phase-link", Sources: []gpu.ShaderSource{
{Stage: gpu.StageVertex, Source: stages[0]},
{Stage: gpu.StageFragment, Source: stages[1]},
}})
samples[i].DriverLinkNS = time.Since(started).Nanoseconds()
if err != nil {
t.Fatal(err)
}
dev.DestroyShader(h)
}
m, ok := material.New("measured", candidates[0], 0, shader.BlendPremultiplied)
if !ok {
t.Fatal("cannot create measured material")
}
m.Set("Amount", [4]float32{0.25})
draw := func(v *render.ViewFrame) {
v.Sprite(sprite.Draw{Region: r.White(), Position: mathx.Vec2{X: 16, Y: 16},
Size: mathx.Vec2{X: 24, Y: 24}, Origin: mathx.Vec2{X: .5, Y: .5}, Tint: mathx.White, Material: m})
}
started := time.Now()
drawMaterialFrame(t, r, dev, target, draw)
prewarmNS := time.Since(started).Nanoseconds()
reporter := dev.(gpu.ResourceReporter)
baseline := reporter.ResourceStats()
for i := range samples {
// Import has finished. This next CPU definition has no dependency on
// the importer or reference validator during driver publication.
next := *candidates[(i+1)%count]
r.Materials().Begin()
var boundaryStart time.Time
started = time.Now()
r.Materials().QueueReload(m.Shader(), &next, []*material.Material{m}, func() {
samples[i].CommitLatencyNS = time.Since(boundaryStart).Nanoseconds()
})
samples[i].QueueNS = time.Since(started).Nanoseconds()
if m.Shader() == &next {
t.Fatal("candidate published before the frame boundary")
}
boundaryStart = time.Now()
r.Materials().End()
samples[i].FrameBoundaryNS = time.Since(boundaryStart).Nanoseconds()
if m.Shader() != &next || m.Failed() || samples[i].CommitLatencyNS == 0 {
t.Fatal("GL33 candidate was not published at the frame boundary")
}
value, found := m.Get("Amount")
if !found || value[0] != .25 {
t.Fatalf("reload lost the explicit material value: %v", value)
}
drawMaterialFrame(t, r, dev, target, draw)
if got := reporter.ResourceStats(); got != baseline {
t.Fatalf("phase %d leaks resources: before=%+v after=%+v", i, baseline, got)
}
}
data, err := json.MarshalIndent(struct {
Method string `json:"method"`
Renderer string `json:"renderer"`
Samples [count]sample `json:"samples"`
PrewarmReadbackNS int64 `json:"prewarmReadbackNS"`
ResourcesBefore gpu.ResourceStats `json:"resourcesBefore"`
ResourcesAfter gpu.ResourceStats `json:"resourcesAfter"`
ReloadGenerations uint64 `json:"reloadGenerations"`
}{"wall time; import includes both reference targets; driver cache is not flushed; prewarm includes draw/readback; frame boundary includes driver/layout/migration/commit", dev.Caps().Renderer, samples, prewarmNS, baseline, reporter.ResourceStats(), r.Materials().Stats().ReloadGenerations}, "", " ")
if err != nil {
t.Fatal(err)
}
t.Log(string(data))
if out := os.Getenv("AEGO_MATERIAL_PHASE_REPORT"); out != "" {
if err := os.WriteFile(out, append(data, '\n'), 0644); err != nil {
t.Fatal(err)
}
}
}