Files
aego/tests/gl/depth_test.go
sanspie 17cb5e4291 Stage 2-5: assets, scene/ECS, editor, 3D, and Aego Script
Stage 2: render pipeline and UI toolkit over gpu/platform.
Stage 3: schema registry, asset pipeline + runtime loader, scene graph and ECS.
Stage 4: editor — docking, inspector, scene/game views, play mode, asset browser, console, profiler; 4.5 adds the 3D foundation (mixed 2D/3D spaces, meshes, depth).
Stage 5: Aego Script — compiler (lexer/parser/types/resolver/checker/asm/compiler), bytecode with verifier and register VM, instance runtime, stdlib, native bindings, and editor/CLI integration (aego run/edit/check/test/dump).
2026-10-08 13:17:28 +03:00

213 lines
5.5 KiB
Go

//go:build aego_gltest
package gl
import (
"testing"
"aego/core/mathx"
"aego/gpu"
)
const depthSize = 32
const colorVert = `#version 330 core
layout(location = 0) in vec3 pos;
layout(location = 1) in vec4 col;
out vec4 v_col;
void main() {
v_col = col;
gl_Position = vec4(pos, 1.0);
}
`
const colorFrag = `#version 330 core
in vec4 v_col;
out vec4 frag;
void main() {
frag = v_col;
}
`
type rig struct {
t *testing.T
dev gpu.Device
shader gpu.ShaderHandle
}
func quad(z, r, g, b float32) []float32 {
out := make([]float32, 0, 42)
for _, p := range [6][2]float32{{-1, -1}, {1, -1}, {1, 1}, {-1, -1}, {1, 1}, {-1, 1}} {
out = append(out, p[0], p[1], z, r, g, b, 1)
}
return out
}
func newRig(t *testing.T, dev gpu.Device) *rig {
t.Helper()
sh, err := dev.CreateShader(gpu.ShaderDesc{
Label: "colored",
Sources: []gpu.ShaderSource{
{Stage: gpu.StageVertex, Source: colorVert},
{Stage: gpu.StageFragment, Source: colorFrag},
},
})
if err != nil {
t.Fatalf("shader: %v", err)
}
return &rig{t: t, dev: dev, shader: sh}
}
func (r *rig) pipeline(depth gpu.DepthState, cull gpu.CullMode) gpu.PipelineHandle {
r.t.Helper()
p, err := r.dev.CreatePipeline(gpu.PipelineDesc{
Shader: r.shader,
Layout: gpu.VertexLayout{
Attrs: []gpu.VertexAttr{
{Location: 0, Slot: 0, Format: gpu.VFFloat3, Offset: 0},
{Location: 1, Slot: 0, Format: gpu.VFFloat4, Offset: 12},
},
Strides: [gpu.MaxVertexSlots]uint16{28},
},
Depth: depth,
Cull: cull,
Primitive: gpu.PrimTriangles,
Label: "colored",
})
if err != nil {
r.t.Fatalf("pipeline: %v", err)
}
return p
}
func (r *rig) buffer(data []float32) gpu.BufferHandle {
r.t.Helper()
b, err := r.dev.CreateBuffer(gpu.BufferDesc{Usage: gpu.BufferVertex, Data: f32bytes(data...), Label: "quad"})
if err != nil {
r.t.Fatalf("buffer: %v", err)
}
return b
}
func (r *rig) texture(format gpu.TextureFormat, size int) gpu.TextureHandle {
r.t.Helper()
tex, err := r.dev.CreateTexture(gpu.TextureDesc{Width: size, Height: size, Format: format})
if err != nil {
r.t.Fatalf("texture: %v", err)
}
return tex
}
func (r *rig) draw(desc gpu.PassDesc, pipe gpu.PipelineHandle, bufs ...gpu.BufferHandle) {
fr := r.dev.BeginFrame(depthSize, depthSize)
p := fr.BeginPass(desc)
p.SetViewport(mathx.Rect{W: depthSize, H: depthSize})
p.SetPipeline(pipe)
for _, b := range bufs {
p.SetVertexBuffer(0, b)
p.Draw(0, 6)
}
p.End()
r.dev.EndFrame()
}
func (r *rig) center(tex gpu.TextureHandle) [4]byte {
r.t.Helper()
var out [4]byte
if err := r.dev.ReadPixels(tex, depthSize/2, depthSize/2, 1, 1, out[:]); err != nil {
r.t.Fatalf("read pixels: %v", err)
}
return out
}
func isGreen(px [4]byte) bool {
return px[1] > 200 && px[0] < 50 && px[2] < 50
}
func isRed(px [4]byte) bool {
return px[0] > 200 && px[1] < 50 && px[2] < 50
}
func isBlue(px [4]byte) bool {
return px[2] > 200 && px[0] < 50 && px[1] < 50
}
func TestDepthTargetRejectsFartherFragments(t *testing.T) {
e := newGLEngine(t)
defer e.Close()
r := newRig(t, e.GPU())
pipe := r.pipeline(gpu.DepthState{Test: true, Write: true, Compare: gpu.CompareLess}, gpu.CullNone)
near := r.buffer(quad(-0.5, 0, 1, 0))
far := r.buffer(quad(0.5, 1, 0, 0))
color := r.texture(gpu.TexRGBA8, depthSize)
depth := r.texture(gpu.TexDepth24Stencil8, depthSize)
r.draw(gpu.PassDesc{
Target: color,
DepthTarget: depth,
Color: gpu.LoadClear,
ClearColor: mathx.Color{B: 1, A: 1},
Depth: gpu.LoadClear,
ClearDepth: 1,
}, pipe, near, far)
if px := r.center(color); !isGreen(px) {
t.Fatalf("depth test did not keep the nearer quad: %v", px)
}
plain := r.texture(gpu.TexRGBA8, depthSize)
r.draw(gpu.PassDesc{
Target: plain,
Color: gpu.LoadClear,
ClearColor: mathx.Color{B: 1, A: 1},
Depth: gpu.LoadClear,
ClearDepth: 1,
}, pipe, near, far)
if px := r.center(plain); !isRed(px) {
t.Fatalf("an offscreen pass without a depth target must not depth test: %v", px)
}
}
func TestDepthTargetSizeMismatchIsIgnored(t *testing.T) {
e := newGLEngine(t)
defer e.Close()
r := newRig(t, e.GPU())
pipe := r.pipeline(gpu.DepthState{Test: true, Write: true, Compare: gpu.CompareLess}, gpu.CullNone)
near := r.buffer(quad(-0.5, 0, 1, 0))
far := r.buffer(quad(0.5, 1, 0, 0))
color := r.texture(gpu.TexRGBA8, depthSize)
depth := r.texture(gpu.TexDepth24Stencil8, depthSize*2)
r.draw(gpu.PassDesc{
Target: color,
DepthTarget: depth,
Color: gpu.LoadClear,
Depth: gpu.LoadClear,
ClearDepth: 1,
}, pipe, near, far)
if px := r.center(color); !isRed(px) {
t.Fatalf("a mismatched depth target must be dropped, not used: %v", px)
}
}
func TestFlipWindingCullsMirroredFaces(t *testing.T) {
e := newGLEngine(t)
defer e.Close()
r := newRig(t, e.GPU())
pipe := r.pipeline(gpu.DepthState{}, gpu.CullBack)
front := r.buffer(quad(0, 0, 1, 0))
color := r.texture(gpu.TexRGBA8, depthSize)
clear := mathx.Color{B: 1, A: 1}
r.draw(gpu.PassDesc{Target: color, Color: gpu.LoadClear, ClearColor: clear}, pipe, front)
if px := r.center(color); !isGreen(px) {
t.Fatalf("a CCW quad must survive back-face culling: %v", px)
}
r.draw(gpu.PassDesc{Target: color, Color: gpu.LoadClear, ClearColor: clear, FlipWinding: true}, pipe, front)
if px := r.center(color); !isBlue(px) {
t.Fatalf("FlipWinding must turn the CCW quad into a back face: %v", px)
}
r.draw(gpu.PassDesc{Target: color, Color: gpu.LoadClear, ClearColor: clear}, pipe, front)
if px := r.center(color); !isGreen(px) {
t.Fatalf("front face state leaked into the next pass: %v", px)
}
}