Files
aego/render/bloom_bounds_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

174 lines
5.8 KiB
Go

package render
import (
"math"
"testing"
"aego/core/mathx"
"aego/gpu"
"aego/render/lighting"
"aego/render/material"
"aego/render/postfx"
"aego/render/shader"
"aego/render/sprite"
"aego/render/view"
)
type bloomCoverageDevice struct {
gpu.Device
opaque bool
format gpu.TextureFormat
}
func (d *bloomCoverageDevice) OpaqueRegion(gpu.TextureHandle, float32, float32, float32, float32) bool {
return d.opaque
}
func (d *bloomCoverageDevice) TextureInfo(gpu.TextureHandle) (gpu.TextureDesc, bool) {
return gpu.TextureDesc{Format: d.format}, true
}
func TestBloomOpaqueReceiverRequiresProofOfDarkness(t *testing.T) {
dev := &bloomCoverageDevice{opaque: true, format: gpu.TexSRGBA8}
env := lighting.DefaultEnvironment()
env.AmbientIntensity = .4
input := &lighting.Input{HasEnvironment: true, Environment: env, Lights: []lighting.Light{{Type: lighting.Directional, Color: mathx.White, Intensity: .5}}}
f := &ViewFrame{r: &Renderer{dev: dev}, brightThreshold: .75, lighting: input, sets: []lighting.Set{{Count: 1}}}
sh := &material.Shader{StockLit: true, Variants: []material.Variant{{}}, Schema: material.NewSchema([]shader.Param{{Name: "EmissionIntensity", Kind: shader.KindFloat, Instance: true}}, 0, 1)}
m, ok := material.New("receiver", sh, 0, shader.BlendPremultiplied)
if !ok {
t.Fatal("material")
}
d := sprite.Draw{Lighting: true, LightSet: 1, Material: m, Tint: mathx.Color{R: .25, G: .25, B: .25, A: 1}}
check := func(want bool) {
t.Helper()
if f.belowBloomThreshold(&d) != want {
t.Fatalf("below threshold should be %t", want)
}
}
check(true)
input.Environment.Ambient.R = -.25
check(false)
input.Environment.Ambient.R = 1
input.Lights[0].Direction.X = 100
check(false)
input.Lights[0].Direction.X = 1
d.HasLinear, d.Linear = true, [4]float32{1, 1, 1, 1.00001}
check(false)
d.HasLinear = false
input.Lights[0].Type = lighting.Point
input.Lights[0].Range, input.Lights[0].Falloff = .00001, 1
check(false)
input.Lights[0].Type = lighting.Directional
input.Lights[0].Intensity = 4
check(false)
input.Lights[0].Intensity = .5
f.brightThreshold = .226
check(false) // no proof inside the storage-rounding margin
f.brightThreshold = .75
sh.StockLit = false
sh.OpaqueWithOpaqueAlbedo = true
check(false)
sh.StockLit = true
m.Set("EmissionIntensity", [4]float32{2})
check(false)
d.Lanes = &[16]float32{}
check(true) // draw-time override, exactly as uploaded to the instance stream
d.Lanes[0] = 1
check(false)
d.Lanes[0] = 0
dev.format = gpu.TexRGBA16F
check(false)
dev.format = gpu.TexRGBA8
d.Lanes[4] = float32(math.Inf(1))
check(false)
d.Lanes[4] = 0
dev.opaque = false
check(false)
dev.opaque = true
input.Debug = lighting.DebugNormals
check(false)
input.Debug = lighting.DebugLighting
d.Tint.A = .9
check(false)
d.Tint.A = 1
d.Material, _ = material.New("additive", sh, 0, shader.BlendAdditive)
check(false)
}
func TestBloomAuxiliaryAndFinalCoverageAreIndependent(t *testing.T) {
r, _ := newTestRenderer(t, 4)
v := view.ScreenView(400, 200, 1)
v.DoClear, v.Clear.A = true, 1
full := v.Viewport
reg := postfx.NewRegistry()
e, _ := reg.NewInstance(postfx.BloomID, "bloom")
var plan postfx.Plan
if err := reg.Build(&plan, true, []postfx.Instance{e}, postfx.Profile{}); err != nil {
t.Fatal(err)
}
o := &lightingOutput{info: OutputInfo{Width: 400, Height: 200}, fxRect: full, clears: []mathx.Rect{full}}
f := &ViewFrame{r: r, output: o, view: v, trackResolveBounds: true, hasResolveBounds: true,
resolveBounds: mathx.Rect{X: 15, Y: 15, W: 200, H: 110}.ToAABB(),
hasBrightBounds: true, brightBounds: mathx.Rect{X: 205, Y: 60, W: 10, H: 9}.ToAABB()}
f.PostProcess(&plan)
pass := &plan.Passes[0]
scene := r.auxiliaryScissor(f, pass)
bright := r.auxiliaryBounds(f, pass, true)
if !(scene.W > bright.W && bright.W > 0 && scene.H > bright.H && bright.H > 0) {
t.Fatalf("scene %v, bright %v", scene, bright)
}
f.submission++
f.PostProcess(&plan) // earlier submissions were not classified for a new stack
if r.auxiliaryBounds(f, pass, true) != scene {
t.Fatal("late stack change reused incomplete brightness information")
}
}
func TestSceneGroupsCoverAllSpritesAfterCoalescing(t *testing.T) {
var groups sceneGroups
var rects []mathx.AABB2
for i := 0; i < 40; i++ {
rect := mathx.Rect{X: float32((i*29)%87 - 20), Y: float32((i * 13) % 47), W: float32(1 + i%13), H: float32(1 + i%8)}.ToAABB()
rects = append(rects, rect)
groups.add(rect)
}
if groups.count != len(groups.box) {
t.Fatal(groups)
}
for _, rect := range rects {
covered := false
for _, group := range groups.box[:groups.count] {
covered = covered || group.Contains(rect.Min) && group.Contains(rect.Max)
}
if !covered {
t.Fatalf("lost %v in %v", rect, groups)
}
}
}
// Pool pixels may be stale: clamped linear-filter taps must land in the write footprint, and clamping may move only centers whose corresponding channel was already black.
func TestBloomSampleWindowNeverReadsStalePoolPixels(t *testing.T) {
for _, width := range []int{63, 64, 129} {
half := (width + 1) / 2
for lo := 0; lo < half; lo++ {
for hi := lo + 1; hi <= half; hi++ {
window := mathx.Rect{X: float32(lo), W: float32(hi - lo), H: 1}
dst := mathx.Rect{W: float32(half), H: 1}
guard, writes := auxSampleWindow(window, dst)
for i := 0; i < width; i++ {
x := mathx.Clamp((float32(i)+0.5)*float32(half)/float32(width), 0.5, float32(half)-0.5)
y := mathx.Clamp(x, guard.X, guard.X+guard.W)
a, b := int(math.Floor(float64(y-.5))), int(math.Ceil(float64(y-.5)))
if a < int(writes.X) || b >= int(writes.X+writes.W) {
t.Fatalf("sample %f needs [%d,%d] outside %v", y, a, b, writes)
}
if y != x && !((x+.5 <= float32(lo) && y+.5 <= float32(lo)) || (x-.5 >= float32(hi-1) && y-.5 >= float32(hi-1))) {
t.Fatalf("clamp %f -> %f can touch [%d,%d)", x, y, lo, hi)
}
}
}
}
}
}