Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 1 addition & 19 deletions app/window.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,6 @@ func (w *Window) validateAndProcess(size image.Point, sync bool, frame *op.Ops,
w.gpu = gpu
}
if w.gpu != nil {
if ager, ok := w.ctx.(interface{ BufferAge() int }); ok {
if g, ok := w.gpu.(interface{ SetBufferAge(int) }); ok {
g.SetBufferAge(ager.BufferAge())
}
}
if err := w.frame(frame, size); err != nil {
w.ctx.Unlock()
if errors.Is(err, errOutOfDate) {
Expand All @@ -209,7 +204,7 @@ func (w *Window) validateAndProcess(size image.Point, sync bool, frame *op.Ops,
signal()
var err error
if w.gpu != nil {
err = w.present(size)
err = w.ctx.Present()
w.ctx.Unlock()
if errors.Is(err, gpu.ErrDeviceLost) {
// The frame is already released to the client and can't be
Expand Down Expand Up @@ -238,19 +233,6 @@ func (w *Window) frame(frame *op.Ops, viewport image.Point) error {
return w.gpu.Frame(frame, target, viewport)
}

// present shows the frame, as damage to the part the GPU drew when the
// context takes a damage rectangle.
func (w *Window) present(size image.Point) error {
p, ok := w.ctx.(interface {
PresentDamage([]image.Rectangle, image.Point) error
})
g, okg := w.gpu.(interface{ Damage() []image.Rectangle })
if !ok || !okg {
return w.ctx.Present()
}
return p.PresentDamage(g.Damage(), size)
}

func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) {
w.coalesced.framePending = false
wrapper := &w.decorations.Ops
Expand Down
103 changes: 36 additions & 67 deletions gpu/damage.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,27 @@ package gpu

import (
"image"
"os"

"gioui.org/gpu/internal/driver"
"gioui.org/internal/f32"
"gioui.org/internal/f32color"
)

// damageTracker finds what a frame changed against the one before it, so
// that only that part of the window is drawn again. It sees a frame as a
// multiset of draw operations: an operation that was not in the last frame,
// or one of the last frame that is gone, dirties its rectangle. The region
// drawn is that, plus what the frames the back buffer is older than
// changed, which the window learns from EGL_EXT_buffer_age.
// that only that part of the retained framebuffer is drawn again. It sees a
// frame as a multiset of draw operations: an operation that was not in the
// last frame, or one of the last frame that is gone, dirties its rectangle.
type damageTracker struct {
prev, cur map[opSig]int
// history holds the dirty rectangles of the latest frames, newest first.
history [8][]image.Rectangle
age int
viewport image.Point
viewport image.Point
// region is what the frame being drawn covers; full says it is the
// whole viewport, and no culling applies.
region []image.Rectangle
full bool
frames int
// clearColor is the last frame's, when its first op was a fill of the
// whole viewport that the renderer turned into the clear: a frame drawn in
// part paints it under its rectangles, and one of another color is whole.
// held says the frame just tracked was drawn into the retained
// framebuffer, which then holds it for the next one.
held bool
clearColor f32color.RGBA
cleared bool
}

// opSig is what identifies a draw operation across frames.
Expand Down Expand Up @@ -82,77 +74,54 @@ func sigOf(img *imageOp) opSig {
return s
}

// setAge tells the tracker how many frames old the back buffer the next
// frame draws into is: 1 is the frame just shown, 0 is unknown, and
// unknown means the whole viewport is drawn.
func (t *damageTracker) setAge(age int) { t.age = age }

// frame reads the frame's operations and settles the region to draw.
func (t *damageTracker) frame(ops []imageOp, layers []opacityLayer, viewport image.Point, cleared bool, clearColor f32color.RGBA) {
if !damageOn {
// Every frame is whole, so hashing its operations would find
// what nothing reads.
t.full = true
t.region = append(t.region[:0], image.Rectangle{Max: viewport})
// frame reads the frame's operations and settles the region to draw. A
// frame that does not go into the retained framebuffer is whole, and hashing
// its operations would find what nothing reads.
func (t *damageTracker) frame(ops []imageOp, layers []opacityLayer, viewport image.Point, retained bool, clearColor f32color.RGBA) {
whole := image.Rectangle{Max: viewport}
if !retained {
t.held, t.full = false, true
t.region = append(t.region[:0], whole)
return
}
if t.cur == nil {
t.cur = map[opSig]int{}
}
clear(t.cur)
// A layer's opacity lives on the layer, not on the operations it
// composites, so each layer counts as an operation of its own.
all := append(ops[:len(ops):len(ops)], make([]imageOp, 0, len(layers))...)
for _, l := range layers {
all = append(all, imageOp{clip: l.clip, layerOps: -1 - l.depth, material: material{opacity: l.opacity}})
}
for i := range all {
t.cur[sigOf(&all[i])]++
}
var dirty []image.Rectangle
for i := range all {
s := sigOf(&all[i])
dirty := t.region[:0]
mark := func(img *imageOp) {
s := sigOf(img)
t.cur[s]++
if n := t.prev[s]; n > 0 {
t.prev[s] = n - 1
continue
return
}
dirty = addRect(dirty, all[i].clip)
dirty = addRect(dirty, img.clip.Intersect(whole))
}
for i := range ops {
mark(&ops[i])
}
// A layer's opacity lives on the layer, not on the operations it
// composites, so each layer counts as an operation of its own.
for _, l := range layers {
mark(&imageOp{clip: l.clip, layerOps: -1 - l.depth, material: material{opacity: l.opacity}})
}
for s, n := range t.prev {
if n > 0 {
dirty = addRect(dirty, s.clip)
dirty = addRect(dirty, s.clip.Intersect(whole))
}
}
t.prev, t.cur = t.cur, t.prev
whole := image.Rectangle{Max: viewport}
for i := range dirty {
dirty[i] = dirty[i].Intersect(whole)
}
first := t.prev == nil || len(t.prev) == 0 && t.frames == 0
t.full = !damageOn || first || t.age <= 0 || t.age > len(t.history)+1 || viewport != t.viewport || cleared != t.cleared || clearColor != t.clearColor
t.cleared, t.clearColor = cleared, clearColor
t.viewport = viewport
t.region = append(t.region[:0], dirty...)
for i := 0; !t.full && i < t.age-1; i++ {
for _, r := range t.history[i] {
t.region = addRect(t.region, r)
}
}
copy(t.history[1:], t.history[:])
t.history[0] = dirty
// A translucent ground blends over what the framebuffer kept instead
// of replacing it, so only a whole frame gets a translucent clear right.
t.full = !t.held || viewport != t.viewport || clearColor != t.clearColor || clearColor.A != 1
t.held, t.viewport, t.clearColor = true, viewport, clearColor
if t.full {
t.region = append(t.region[:0], whole)
dirty = append(dirty[:0], whole)
}
t.frames++
t.age = 0
t.region = dirty
}

// damageOn draws a frame in part where the buffer age allows: GIO_DAMAGE=on
// in the environment. Off, every frame is drawn whole, the way Gio does
// upstream. It is opt-in because a driver can report an age its buffer does
// not hold, and the frame then shows stale pixels: NVIDIA 595 on COSMIC does.
var damageOn = os.Getenv("GIO_DAMAGE") == "on"

// damageRects bounds how many rectangles a frame is drawn in: past it,
// the rectangles fold into their bounding box.
const damageRects = 16
Expand Down
97 changes: 38 additions & 59 deletions gpu/damage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,11 @@ package gpu

import (
"image"
"os"
"testing"

"gioui.org/internal/f32color"
)

// TestMain draws in part, as GIO_DAMAGE=on does: the tracker's diff is what
// these tests are about.
func TestMain(m *testing.M) {
damageOn = true
os.Exit(m.Run())
}

func colorOp(r image.Rectangle, c f32color.RGBA) imageOp {
return imageOp{clip: r, material: material{material: materialColor, color: c, opaque: true, opacity: 1}}
}
Expand All @@ -27,68 +19,48 @@ var (
view = image.Pt(100, 100)
)

// freshTracker draws into a buffer one frame old, as a double-buffered
// surface reports it.
func freshTracker() *damageTracker { return &damageTracker{} }

func frameAt(t *damageTracker, age int, ops []imageOp, view image.Point, clear f32color.RGBA) {
t.setAge(age)
func frameAt(t *damageTracker, ops []imageOp, view image.Point, clear f32color.RGBA) {
t.frame(ops, nil, view, true, clear)
}

func TestFirstFrameIsWhole(t *testing.T) {
tr := freshTracker()
frameAt(tr, 1, []imageOp{colorOp(image.Rect(0, 0, 10, 10), red)}, view, blue)
tr := &damageTracker{}
frameAt(tr, []imageOp{colorOp(image.Rect(0, 0, 10, 10), red)}, view, blue)
if !tr.full {
t.Fatal("a first frame, with nothing before it, must be drawn whole")
}
}

func TestUnchangedOpsDirtyNothing(t *testing.T) {
tr := freshTracker()
tr := &damageTracker{}
ops := []imageOp{colorOp(image.Rect(0, 0, 10, 10), red), colorOp(image.Rect(50, 50, 60, 60), blue)}
frameAt(tr, 1, ops, view, blue)
frameAt(tr, 1, ops, view, blue)
frameAt(tr, ops, view, blue)
frameAt(tr, ops, view, blue)
if tr.full || len(tr.region) != 0 {
t.Fatalf("the same ops again dirtied %v (full %v), want nothing", tr.region, tr.full)
}
}

func TestNewAndGoneOpsDirtyTheirRectangles(t *testing.T) {
tr := freshTracker()
frameAt(tr, 1, []imageOp{colorOp(image.Rect(0, 0, 10, 10), red), colorOp(image.Rect(50, 50, 60, 60), blue)}, view, blue)
frameAt(tr, 1, []imageOp{colorOp(image.Rect(0, 0, 10, 10), red), colorOp(image.Rect(80, 80, 90, 90), blue)}, view, blue)
tr := &damageTracker{}
frameAt(tr, []imageOp{colorOp(image.Rect(0, 0, 10, 10), red), colorOp(image.Rect(50, 50, 60, 60), blue)}, view, blue)
frameAt(tr, []imageOp{colorOp(image.Rect(0, 0, 10, 10), red), colorOp(image.Rect(80, 80, 90, 90), blue)}, view, blue)
want := []image.Rectangle{image.Rect(80, 80, 90, 90), image.Rect(50, 50, 60, 60)}
if tr.full || len(tr.region) != 2 || tr.region[0] != want[0] || tr.region[1] != want[1] {
t.Fatalf("region %v (full %v), want the op that came and the op that went: %v", tr.region, tr.full, want)
}
}

func TestOlderBuffersGatherTheFramesBetween(t *testing.T) {
tr := &damageTracker{}
a, b, c := image.Rect(0, 0, 10, 10), image.Rect(20, 20, 30, 30), image.Rect(40, 40, 50, 50)
frameAt(tr, 1, []imageOp{colorOp(a, red)}, view, blue)
frameAt(tr, 1, []imageOp{colorOp(b, red)}, view, blue)
frameAt(tr, 3, []imageOp{colorOp(c, red)}, view, blue)
if tr.full || len(tr.region) != 3 {
t.Fatalf("drawing into a buffer three frames old covered %v (full %v), want this frame's change and the two before it", tr.region, tr.full)
}
frameAt(tr, 0, []imageOp{colorOp(c, red)}, view, blue)
if !tr.full {
t.Fatal("a buffer of unknown age must be drawn whole")
}
}

func TestAChangedViewportOrClearIsWhole(t *testing.T) {
tr := freshTracker()
tr := &damageTracker{}
ops := []imageOp{colorOp(image.Rect(0, 0, 10, 10), red)}
frameAt(tr, 1, ops, view, blue)
frameAt(tr, 1, ops, image.Pt(200, 100), blue)
frameAt(tr, ops, view, blue)
frameAt(tr, ops, image.Pt(200, 100), blue)
if !tr.full {
t.Fatal("a resized viewport must be drawn whole")
}
frameAt(tr, 1, ops, image.Pt(200, 100), blue)
frameAt(tr, 1, ops, image.Pt(200, 100), red)
frameAt(tr, ops, image.Pt(200, 100), blue)
frameAt(tr, ops, image.Pt(200, 100), red)
if !tr.full {
t.Fatal("a frame whose clear color changed must be drawn whole")
}
Expand All @@ -108,36 +80,43 @@ func TestManyRectanglesFoldIntoTheirBox(t *testing.T) {
}

func TestAFadingLayerDirtiesItsRectangle(t *testing.T) {
tr := freshTracker()
tr := &damageTracker{}
ops := []imageOp{colorOp(image.Rect(0, 0, 10, 10), red)}
layer := image.Rect(20, 20, 60, 60)
tr.setAge(1)
tr.frame(ops, []opacityLayer{{opacity: 1, clip: layer}}, view, true, blue)
tr.setAge(1)
tr.frame(ops, []opacityLayer{{opacity: 0.5, clip: layer}}, view, true, blue)
if tr.full || len(tr.region) != 1 || tr.region[0] != layer {
t.Fatalf("a layer fading dirtied %v (full %v), want its rectangle %v", tr.region, tr.full, layer)
}
}

func TestWithoutGIODamageEveryFrameIsWhole(t *testing.T) {
damageOn = false
t.Cleanup(func() { damageOn = true })
tr := freshTracker()
func TestAFrameOutsideTheRetainedFramebufferIsWholeAndUntracked(t *testing.T) {
tr := &damageTracker{}
ops := []imageOp{colorOp(image.Rect(0, 0, 10, 10), red)}
frameAt(tr, 1, ops, view, blue)
frameAt(tr, 1, ops, view, blue)
tr.frame(ops, nil, view, false, blue)
if !tr.full || tr.held || tr.cur != nil || len(tr.region) != 1 || tr.region[0] != (image.Rectangle{Max: view}) {
t.Fatalf("full %v, held %v, cur %v, region %v: want a whole, untracked frame", tr.full, tr.held, tr.cur, tr.region)
}
}

func TestReturningToTheRetainedFramebufferIsWhole(t *testing.T) {
tr := &damageTracker{}
ops := []imageOp{colorOp(image.Rect(0, 0, 10, 10), red)}
frameAt(tr, ops, view, blue)
tr.frame(ops, nil, view, false, blue)
frameAt(tr, ops, view, blue)
if !tr.full {
t.Fatal("without GIO_DAMAGE=on a frame must be drawn whole, whatever the buffer age")
t.Fatal("the retained framebuffer missed a frame, so the next one must be whole")
}
}

func TestWithoutGIODamageTheRegionIsTheViewport(t *testing.T) {
damageOn = false
t.Cleanup(func() { damageOn = true })
tr := freshTracker()
frameAt(tr, 1, []imageOp{colorOp(image.Rect(0, 0, 10, 10), red)}, view, blue)
if len(tr.region) != 1 || tr.region[0] != (image.Rectangle{Max: view}) {
t.Fatalf("region %v, want the whole viewport %v", tr.region, view)
func TestATranslucentClearIsWhole(t *testing.T) {
tr := &damageTracker{}
clearColor := f32color.RGBA{R: 0.1, A: 0.3}
ops := []imageOp{colorOp(image.Rect(0, 0, 10, 10), red)}
frameAt(tr, ops, view, clearColor)
frameAt(tr, ops, view, clearColor)
if !tr.full {
t.Fatal("a translucent ground would blend over the kept frame, so its frame must be whole")
}
}
Loading