Files
aego/render/lighting_output.go
T
sanspie 5eb1832907
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 15m28s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 7m47s
Complete Stage 12 for v0.12.0
2026-11-05 11:46:53 +03:00

481 lines
15 KiB
Go

package render
import (
"fmt"
"math"
"time"
"aego/core/ids"
"aego/core/mathx"
"aego/gpu"
"aego/render/lighting"
"aego/render/rt"
"aego/render/shader"
"aego/render/view"
)
type OutputInfo struct {
Requested, Active lighting.ColorMode
Format gpu.TextureFormat
Width, Height int
Bytes uint64
Reason string
}
type backgroundCheck struct {
color, linear mathx.Color
exposure float32
generation uint64
tested, match bool
}
type lightingOutput struct {
fxRect mathx.Rect
fxCoverage view.OutputCoverage
fxOpaque bool
fxRegions []mathx.Rect
fxCuts [2][]mathx.Rect
fxPool *rt.Pool
background backgroundCheck
fxBackground backgroundCheck
info OutputInfo
color, depth gpu.TextureHandle
lastFrame uint64
destination gpu.TextureHandle
subresource gpu.TextureSubresource
additive bool
opaque bool
pending bool
clears []mathx.Rect
environment lighting.Environment
hdrFailed bool
hdrGeneration uint64
hdrReason string
}
func (r *Renderer) LightingOutput(id view.ID) (OutputInfo, bool) {
o := r.lightState.outputs[id]
if o == nil {
return OutputInfo{}, false
}
return o.info, true
}
func (r *Renderer) prepareLightingOutput(f *ViewFrame, env lighting.Environment) bool {
if r.frame == nil {
return true
}
describe, ok := r.dev.(gpu.TextureDescriber)
var target gpu.TextureDesc
known := false
if ok && !f.view.Target.IsZero() {
target, known = describe.TextureInfo(f.view.Target)
}
if f.view.OutputPolicy == view.OutputLinear {
if !known || target.Format != gpu.TexRGBA16F {
r.lightingFailure(fmt.Errorf("LGT008: a linear lighting output requires an explicit RGBA16F target"))
return false
}
if f.postProcess == nil {
// Draw directly; coverage and RGB are preserved until a later composition.
return true
}
env.ColorMode = lighting.HDR
}
s := &r.lightState
if s.outputs == nil {
s.outputs = make(map[view.ID]*lightingOutput)
}
id := f.view.OutputID
if id == (view.ID{}) {
id = view.ID{Scope: ^uint64(0), Node: ids.NodeID{Lo: uint64(r.stats.Views)}}
}
o := s.outputs[id]
if o == nil {
if len(s.outputs) >= maxLightingViews {
r.lightingFailure(fmt.Errorf("LGT008: more than 16 active lighting outputs"))
return false
}
o = &lightingOutput{}
s.outputs[id] = o
}
first := o.lastFrame != s.frame || !o.pending
if !first && (o.destination != f.view.Target || o.subresource != f.view.TargetSubresource) {
r.lightingFailure(fmt.Errorf("LGT008: cameras sharing an output must use the same target subresource"))
return false
}
if !first && o.environment != env {
r.lightingFailure(fmt.Errorf("LGT003: cameras sharing a lighting output have different environments; use one environment or separate logical outputs"))
return false
}
o.lastFrame = s.frame
f.outputOriginal = f.view
f.output = o
format := gpu.TexRGBA8
reason := ""
if env.ColorMode != lighting.LDR {
if r.dev.Caps().RGBA16FRenderable {
format = gpu.TexRGBA16F
} else {
reason = "RGBA16F render/draw/readback capability is unavailable; using LDR"
}
}
w, h := f.view.TargetW, f.view.TargetH
if known {
w = max(1, target.Width>>f.view.TargetSubresource.Mip)
h = max(1, target.Height>>f.view.TargetSubresource.Mip)
}
if f.view.OutputPolicy == view.OutputLinear {
format = gpu.TexRGBA16F
}
if f.view.OutputPolicy != view.OutputLinear && format == gpu.TexRGBA16F && o.hdrFailed && o.hdrGeneration == s.generation && o.info.Width == w && o.info.Height == h {
format, reason = gpu.TexRGBA8, o.hdrReason
}
needsDepth := f.view.Is3D()
if o.color.IsZero() || o.info.Width != w || o.info.Height != h || o.info.Format != format || needsDepth && o.depth.IsZero() || o.destination != f.view.Target || o.subresource != f.view.TargetSubresource {
if err := r.resizeLightingOutput(o, w, h, format, needsDepth); err != nil {
if format == gpu.TexRGBA16F && f.view.OutputPolicy != view.OutputLinear {
reason = "RGBA16F framebuffer failed: " + err.Error() + "; using LDR"
o.hdrFailed, o.hdrGeneration, o.hdrReason = true, s.generation, reason
format = gpu.TexRGBA8
err = r.resizeLightingOutput(o, w, h, format, needsDepth)
}
if err != nil {
r.lightingFailure(err)
return false
}
}
}
if format == gpu.TexRGBA16F {
o.hdrFailed, o.hdrReason = false, ""
}
o.destination, o.subresource = f.view.Target, f.view.TargetSubresource
o.info.Requested = env.ColorMode
o.info.Reason = reason
o.info.Active = lighting.LDR
if format == gpu.TexRGBA16F {
o.info.Active = lighting.HDR
}
f.outputEnvironment = env
f.view.TargetW, f.view.TargetH = w, h
f.trackResolveBounds = f.trackResolveBounds && first
if first {
o.environment = env
o.additive, o.opaque, o.pending = false, false, true
o.clears = o.clears[:0]
o.fxOpaque = true
o.fxCoverage.Reset()
o.fxRect = mathx.Rect{}
o.fxRegions = o.fxRegions[:0]
if !r.caps.ESSL || !f.view.DoClear || !coversTarget(f.view.ClearRegion(), w, h) {
p := r.frame.BeginPass(gpu.PassDesc{Target: o.color, Color: gpu.LoadClear, ClearRect: mathx.Rect{W: float32(w), H: float32(h)}, Label: "lighting.output-begin"})
p.End()
}
}
if f.postProcess != nil {
o.includePostProcessRect(f.view.Viewport, w, h)
if first && !f.view.DeferResolve {
o.fxOpaque = f.view.DoClear && f.view.Clear.A >= 1
} else {
o.fxCoverage.Add(f.view, 0) // Views arrive in actual submission order.
if !f.view.DeferResolve {
o.fxOpaque = o.fxCoverage.Opaque()
}
}
}
o.additive = o.additive || f.hasAdditive
if f.view.DoClear {
rect := f.view.ClearRegion()
o.clears = append(o.clears, rect)
if f.view.Clear.A < 1 {
o.opaque = false
} else if coversTarget(rect, w, h) {
o.opaque = true
}
}
f.view.Target, f.view.DepthTarget = o.color, o.depth
f.view.TargetSubresource, f.view.DepthSubresource = gpu.TextureSubresource{}, gpu.TextureSubresource{}
return true
}
func (r *Renderer) resizeLightingOutput(o *lightingOutput, w, h int, format gpu.TextureFormat, depth bool) error {
color, err := r.dev.CreateTexture(gpu.TextureDesc{Width: w, Height: h, Format: format, Label: "lighting.scene"})
if err != nil {
return err
}
var z gpu.TextureHandle
if depth {
z, err = r.dev.CreateTexture(gpu.TextureDesc{Width: w, Height: h, Format: gpu.TexDepth24Stencil8, Label: "lighting.scene-depth"})
if err != nil {
r.dev.DestroyTexture(color)
return err
}
}
desc := gpu.PassDesc{Target: color, DepthTarget: z, Color: gpu.LoadClear, ClearRect: mathx.Rect{W: float32(w), H: float32(h)}, Label: "lighting.output-initialize"}
if validator, ok := r.dev.(gpu.RenderTargetValidator); ok {
err = validator.ValidateRenderTarget(desc)
}
if err != nil {
r.dev.DestroyTexture(color)
if !z.IsZero() {
r.dev.DestroyTexture(z)
}
return err
}
r.dropLightingOutput(o)
o.background = backgroundCheck{}
o.color, o.depth = color, z
o.info = OutputInfo{Format: format, Width: w, Height: h, Bytes: uint64(w * h * format.StorageBytesPerPixel())}
if depth {
o.info.Bytes += uint64(w * h * 4)
}
p := r.frame.BeginPass(desc)
p.End()
return nil
}
func (r *Renderer) dropLightingOutput(o *lightingOutput) {
if o == nil {
return
}
o.closePostProcessPool()
if !o.color.IsZero() {
r.dev.DestroyTexture(o.color)
}
if !o.depth.IsZero() {
r.dev.DestroyTexture(o.depth)
}
}
func (r *Renderer) resolveLighting(f *ViewFrame) {
o := f.output
if o == nil {
return
}
f.view = f.outputOriginal
if f.view.DeferResolve {
return
}
start := time.Now()
defer func() { r.stats.LightingResolveNS += uint64(time.Since(start)) }()
o.pending = false
// A driver can lose its context inside the preceding sprite/material pass.
// A fresh frame will produce this logical output after restoration.
if r.contextLost() {
return
}
if f.postProcess != nil && (o.fxRect.W <= 0 || o.fxRect.H <= 0) {
r.clearPostProcessDestination(f)
return
}
if f.view.OutputPolicy == view.OutputLinear {
if err := r.resolvePostProcess(f); err != nil {
if r.contextLost() {
return
}
r.postProcessFailure(err)
f.postProcess = &linearCopyPlan
if err := r.resolvePostProcess(f); err != nil && !r.contextLost() {
r.postProcessFailure(err)
}
}
return
}
// A display RGBA image cannot represent background-independent emitted
// energy with zero coverage. Keep it linear until its final opaque composite.
if o.additive && !o.opaque && !(f.postProcess != nil && o.fxOpaque) {
r.lightingFailure(fmt.Errorf("LGT008: transparent display output with Additive needs a linear RGBA16F intermediate or an opaque background before resolve"))
return
}
if f.postProcess != nil {
if err := r.resolvePostProcess(f); err == nil || r.contextLost() {
return
} else {
r.postProcessFailure(err)
}
}
s := &r.lightState
if s.resolveShader.IsZero() {
h, err := r.loadShader("lighting.resolve", "resolve.vert", "resolve.frag")
if err != nil {
r.lightingFailure(err)
return
}
s.resolveShader = h
p, err := r.dev.CreatePipeline(gpu.PipelineDesc{Shader: h, Blend: gpu.BlendPremultiplied(), Primitive: gpu.PrimTriangleStrip, Label: "lighting.resolve"})
if err != nil {
r.dev.DestroyShader(h)
s.resolveShader = gpu.ShaderHandle{}
r.lightingFailure(err)
return
}
s.resolvePipe = p
ub, err := r.dev.CreateBuffer(gpu.BufferDesc{Usage: gpu.BufferUniform, Size: 16, Dynamic: true, Label: "lighting.resolve"})
if err != nil {
r.dev.DestroyPipeline(s.resolvePipe)
r.dev.DestroyShader(s.resolveShader)
s.resolvePipe, s.resolveShader = gpu.PipelineHandle{}, gpu.ShaderHandle{}
r.lightingFailure(err)
return
}
s.resolveUB = ub
s.resolveData = make([]byte, 16)
}
exposure, tone, flip, encode := f.outputEnvironment.Exposure, float32(0), float32(-1), float32(0)
if o.info.Active == lighting.HDR {
tone = 1
}
if f.lighting != nil && f.lighting.Debug != lighting.DebugLighting {
exposure, tone = 1, 0
}
if f.view.Flipped(r.caps) {
flip = 1
}
if describer, ok := r.dev.(gpu.TextureDescriber); ok && !f.view.Target.IsZero() {
if desc, known := describer.TextureInfo(f.view.Target); known && desc.Format == gpu.TexRGBA8 {
encode = 1
}
}
shader.PutLightingVec4(s.resolveData[:], 0, [4]float32{exposure, tone, flip, encode})
r.dev.UpdateBuffer(s.resolveUB, 0, s.resolveData[:])
viewport := mathx.Rect{W: float32(o.info.Width), H: float32(o.info.Height)}
replace := false
for _, rect := range o.clears {
if r.caps.ESSL && (f.view.Target.IsZero() && r.fbW == o.info.Width && r.fbH == o.info.Height || !f.view.Target.IsZero()) && coversTarget(rect, o.info.Width, o.info.Height) {
replace = true
break
}
}
pipe := s.resolvePipe
if replace {
if s.resolveReplace.IsZero() {
h, err := r.dev.CreatePipeline(gpu.PipelineDesc{Shader: s.resolveShader, Primitive: gpu.PrimTriangleStrip, Label: "lighting.resolve-replace"})
if err != nil {
r.lightingFailure(err)
return
}
s.resolveReplace = h
}
pipe = s.resolveReplace
} else {
for _, rect := range o.clears {
p := r.frame.BeginPass(gpu.PassDesc{Target: f.view.Target, TargetSubresource: f.view.TargetSubresource, Color: gpu.LoadClear, ClearRect: rect, Label: "lighting.output-clear"})
p.End()
}
}
desc := gpu.PassDesc{Target: f.view.Target, TargetSubresource: f.view.TargetSubresource, Color: gpu.LoadKeep, Label: "lighting.resolve"}
var cut, witness mathx.Rect
var candidate mathx.Color
check := false
clipped := false
probe, canProbe := r.dev.(interface {
MatchWindowClear(int, int, mathx.Color) bool
})
if canProbe && f.trackResolveBounds && f.hasResolveBounds && replace && tone == 1 && encode == 0 && f.view.Target.IsZero() && !f.view.Is3D() && viewport.W == float32(r.fbW) && viewport.H == float32(r.fbH) {
world, over, screen := r.dbg.Counts()
if world+over+screen == 0 {
if rect, ok := f.resolveRect(viewport); ok {
cached := &o.background
same := cached.tested && cached.generation == s.generation
if same && cached.match && cached.color == f.view.Clear && cached.exposure == exposure {
desc.Color, desc.ClearColor, desc.ClearRect = gpu.LoadClear, cached.linear, viewport
cut, clipped = rect, true
} else if !same {
if c, ok := resolveBackground(f.view.Clear, exposure); ok {
candidate = c
witness, check = backgroundWitness(rect, viewport)
}
}
}
}
}
draw := func(desc gpu.PassDesc, rect mathx.Rect, scissor bool) {
p := r.frame.BeginPass(desc)
p.SetViewport(viewport)
if scissor {
p.SetScissor(rect, true)
}
p.SetPipeline(pipe)
p.SetUniformBuffer(0, s.resolveUB)
p.SetTexture(0, o.color, r.defSampler)
p.Draw(0, 4)
p.End()
}
draw(desc, cut, clipped)
if check {
matched := probe.MatchWindowClear(int(witness.X), int(witness.Y), candidate)
o.background = backgroundCheck{color: f.view.Clear, exposure: exposure, generation: s.generation, tested: true, match: matched, linear: candidate}
if !matched {
draw(desc, witness, true)
}
}
r.stats.ResolvePasses++
}
func coversTarget(rect mathx.Rect, w, h int) bool {
return rect.X <= 0 && rect.Y <= 0 && rect.X+rect.W >= float32(w) && rect.Y+rect.H >= float32(h)
}
func (f *ViewFrame) resolveRect(target mathx.Rect) (mathx.Rect, bool) {
return f.projectBounds(f.resolveBounds, target)
}
func (f *ViewFrame) projectBounds(b mathx.AABB2, target mathx.Rect) (mathx.Rect, bool) {
v := f.view
m := v.ViewMatrix()
extent := max(mathx.Abs(b.Min.X), mathx.Abs(b.Min.Y), mathx.Abs(b.Max.X), mathx.Abs(b.Max.Y))
if extent*(mathx.Abs(m[0])+mathx.Abs(m[1])+mathx.Abs(m[3])+mathx.Abs(m[4]))+mathx.Abs(m[6])+mathx.Abs(m[7]) > 1<<20 {
return mathx.Rect{}, false
}
p0, p1 := v.WorldToScreen(b.Min), v.WorldToScreen(b.Max)
p2, p3 := v.WorldToScreen(mathx.Vec2{X: b.Min.X, Y: b.Max.Y}), v.WorldToScreen(mathx.Vec2{X: b.Max.X, Y: b.Min.Y})
x0 := max(0, float32(math.Floor(float64(min(p0.X, p1.X, p2.X, p3.X))))-2)
y0 := max(0, float32(math.Floor(float64(min(p0.Y, p1.Y, p2.Y, p3.Y))))-2)
x1 := min(target.W, float32(math.Ceil(float64(max(p0.X, p1.X, p2.X, p3.X))))+2)
y1 := min(target.H, float32(math.Ceil(float64(max(p0.Y, p1.Y, p2.Y, p3.Y))))+2)
r := mathx.Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
return r, r.W > 0 && r.H > 0 && r.W*r.H < target.W*target.H*0.95
}
// Only clear directly if HDR storage rounding cannot change the displayed byte.
func resolveBackground(c mathx.Color, exposure float32) (mathx.Color, bool) {
if c.A != 1 || !(exposure > 0 && exposure <= 65504) {
return mathx.Color{}, false
}
result := mathx.Color{A: 1}
for i, x := range [3]float32{c.R, c.G, c.B} {
if !(x >= 0 && x <= 65504) {
return mathx.Color{}, false
}
e := x/1024 + 1.0/(1<<24)
encode := func(v float32) int {
v *= exposure
return int(mathx.Clamp(mathx.LinearToSRGB(v/(1+v)), 0, 1)*255 + .5)
}
lo, hi := encode(max(0, x-e)), encode(min(65504, x+e))
if lo != hi {
return mathx.Color{}, false
}
val := mathx.SRGBToLinear(float32(lo) / 255)
switch i {
case 0:
result.R = val
case 1:
result.G = val
case 2:
result.B = val
}
}
return result, true
}
func backgroundWitness(cut, target mathx.Rect) (mathx.Rect, bool) {
for _, p := range [4]mathx.Vec2{{}, {X: target.W - 8}, {Y: target.H - 8}, {X: target.W - 8, Y: target.H - 8}} {
r := mathx.Rect{X: p.X, Y: p.Y, W: 8, H: 8}
if p.X >= 0 && p.Y >= 0 && target.W >= 8 && target.H >= 8 && !r.ToAABB().Intersects(cut.ToAABB()) {
return r, true
}
}
return mathx.Rect{}, false
}