feat(logging): add wayland-damage and render-damage debug categories
- commit
- 5c50d2832ea9b57824f4859b1524cac98352ddb4
- author
- brice · · 2026-10-13 15:31 UTC
- committer
- brice · · 2026-10-14 04:34 UTC
- parent
- 5280113
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
12 files · +186 -11
M docs/performance.md +1 -1
| @@ -40,7 +40,7 @@ GODEBUG=gctrace=1 neferwl --backend=drm | |||
| 40 | 40 | ||
| 41 | 41 | `make tty` serves pprof on `localhost:6060` by default (`make tty PPROF=` turns it off). | |
| 42 | 42 | ||
| 43 | - | Logs are in `~/.local/state/neferwl/runs/<backend>/`, with `latest.log` pointing at the newest run. `--debug=all` enables every category except the noisy `drm-flip`, `input-motion` and `input-keys`; name them to turn them on. | |
| 43 | + | Logs are in `~/.local/state/neferwl/runs/<backend>/`, with `latest.log` pointing at the newest run. `--debug=all` enables every category except the noisy `drm-flip`, `input-motion`, `input-keys`, `wayland-damage` (one `client damage` entry per client commit) and `render-damage` (one `frame damage` entry per rendered frame); name them to turn them on. | |
| 44 | 44 | ||
| 45 | 45 | ## Frame stats | |
| 46 | 46 | ||
| @@ -40,7 +40,7 @@ GODEBUG=gctrace=1 neferwl --backend=drm | |||||
| 40 | 40 | ||||
| 41 | `make tty` serves pprof on `localhost:6060` by default (`make tty PPROF=` turns it off). | 41 | `make tty` serves pprof on `localhost:6060` by default (`make tty PPROF=` turns it off). | ||
| 42 | 42 | ||||
| 43 | - | Logs are in `~/.local/state/neferwl/runs/<backend>/`, with `latest.log` pointing at the newest run. `--debug=all` enables every category except the noisy `drm-flip`, `input-motion` and `input-keys`; name them to turn them on. | 43 | + | Logs are in `~/.local/state/neferwl/runs/<backend>/`, with `latest.log` pointing at the newest run. `--debug=all` enables every category except the noisy `drm-flip`, `input-motion`, `input-keys`, `wayland-damage` (one `client damage` entry per client commit) and `render-damage` (one `frame damage` entry per rendered frame); name them to turn them on. |
| 44 | 44 | ||||
| 45 | ## Frame stats | 45 | ## Frame stats | ||
| 46 | 46 | ||||
M internal/adapters/logging/logging.go +7 -2
| @@ -28,8 +28,13 @@ var components = map[string]bool{ | |||
| 28 | 28 | // categories are debug switches narrower than a component, only on when | |
| 29 | 29 | // named: "all" leaves them off. input-motion logs every pointer motion, | |
| 30 | 30 | // input-keys every key (typed text), drm-flip every frame and commit | |
| 31 | - | // completion with its timing. Naming one turns on debug for its component. | |
| 32 | - | var categories = map[string]string{"input-motion": "input", "input-keys": "input", "drm-flip": "drm"} | |
| 31 | + | // completion with its timing, wayland-damage every fresh client commit with | |
| 32 | + | // its damage rects, render-damage every rendered frame with its redrawn | |
| 33 | + | // area. Naming one turns on debug for its component. | |
| 34 | + | var categories = map[string]string{ | |
| 35 | + | "input-motion": "input", "input-keys": "input", "drm-flip": "drm", | |
| 36 | + | "wayland-damage": "wayland", "render-damage": "render", | |
| 37 | + | } | |
| 33 | 38 | ||
| 34 | 39 | type debugKey struct{} | |
| 35 | 40 | type levelKey struct{} | |
| @@ -28,8 +28,13 @@ var components = map[string]bool{ | |||||
| 28 | // categories are debug switches narrower than a component, only on when | 28 | // categories are debug switches narrower than a component, only on when | ||
| 29 | // named: "all" leaves them off. input-motion logs every pointer motion, | 29 | // named: "all" leaves them off. input-motion logs every pointer motion, | ||
| 30 | // input-keys every key (typed text), drm-flip every frame and commit | 30 | // input-keys every key (typed text), drm-flip every frame and commit | ||
| 31 | - | // completion with its timing. Naming one turns on debug for its component. | 31 | + | // completion with its timing, wayland-damage every fresh client commit with |
| 32 | - | var categories = map[string]string{"input-motion": "input", "input-keys": "input", "drm-flip": "drm"} | 32 | + | // its damage rects, render-damage every rendered frame with its redrawn |
| 33 | + | // area. Naming one turns on debug for its component. | |||
| 34 | + | var categories = map[string]string{ | |||
| 35 | + | "input-motion": "input", "input-keys": "input", "drm-flip": "drm", | |||
| 36 | + | "wayland-damage": "wayland", "render-damage": "render", | |||
| 37 | + | } | |||
| 33 | 38 | ||||
| 34 | type debugKey struct{} | 39 | type debugKey struct{} | ||
| 35 | type levelKey struct{} | 40 | type levelKey struct{} | ||
M internal/adapters/logging/logging_test.go +4 -1
| @@ -168,8 +168,11 @@ func TestLogWriteNeverBlocksOnSlowDisk(t *testing.T) { | |||
| 168 | 168 | ||
| 169 | 169 | // Narrow categories are on only when named, never through "all". | |
| 170 | 170 | func TestEnabledCategories(t *testing.T) { | |
| 171 | - | for _, category := range []string{"input-motion", "input-keys", "drm-flip"} { | |
| 171 | + | for _, category := range []string{"input-motion", "input-keys", "drm-flip", "wayland-damage", "render-damage"} { | |
| 172 | 172 | component := categories[category] | |
| 173 | + | if category == "wayland-damage" && component != "wayland" || category == "render-damage" && component != "render" { | |
| 174 | + | t.Errorf("%s belongs to %q", category, component) | |
| 175 | + | } | |
| 173 | 176 | for _, tc := range []struct { | |
| 174 | 177 | debug string | |
| 175 | 178 | want bool | |
| @@ -168,8 +168,11 @@ func TestLogWriteNeverBlocksOnSlowDisk(t *testing.T) { | |||||
| 168 | 168 | ||||
| 169 | // Narrow categories are on only when named, never through "all". | 169 | // Narrow categories are on only when named, never through "all". | ||
| 170 | func TestEnabledCategories(t *testing.T) { | 170 | func TestEnabledCategories(t *testing.T) { | ||
| 171 | - | for _, category := range []string{"input-motion", "input-keys", "drm-flip"} { | 171 | + | for _, category := range []string{"input-motion", "input-keys", "drm-flip", "wayland-damage", "render-damage"} { |
| 172 | component := categories[category] | 172 | component := categories[category] | ||
| 173 | + | if category == "wayland-damage" && component != "wayland" || category == "render-damage" && component != "render" { | |||
| 174 | + | t.Errorf("%s belongs to %q", category, component) | |||
| 175 | + | } | |||
| 173 | for _, tc := range []struct { | 176 | for _, tc := range []struct { | ||
| 174 | debug string | 177 | debug string | ||
| 175 | want bool | 178 | want bool | ||
M internal/adapters/vulkan/frame.go +7 -0
| @@ -199,6 +199,13 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 199 | 199 | } else { | |
| 200 | 200 | r.redrawn += sw * sh | |
| 201 | 201 | } | |
| 202 | + | if r.traceDamage { | |
| 203 | + | redrawn := sw * sh | |
| 204 | + | if partial { | |
| 205 | + | redrawn = dmg.area.Dx() * dmg.area.Dy() | |
| 206 | + | } | |
| 207 | + | r.log.Debug().Int("redrawn", redrawn).Int("draws", len(ds)).Bool("partial", partial).Msg("frame damage") | |
| 208 | + | } | |
| 202 | 209 | r.orient(ds, s.Transform) | |
| 203 | 210 | d := r.dd | |
| 204 | 211 | cmd := slot.cmd | |
| @@ -199,6 +199,13 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 199 | } else { | 199 | } else { | ||
| 200 | r.redrawn += sw * sh | 200 | r.redrawn += sw * sh | ||
| 201 | } | 201 | } | ||
| 202 | + | if r.traceDamage { | |||
| 203 | + | redrawn := sw * sh | |||
| 204 | + | if partial { | |||
| 205 | + | redrawn = dmg.area.Dx() * dmg.area.Dy() | |||
| 206 | + | } | |||
| 207 | + | r.log.Debug().Int("redrawn", redrawn).Int("draws", len(ds)).Bool("partial", partial).Msg("frame damage") | |||
| 208 | + | } | |||
| 202 | r.orient(ds, s.Transform) | 209 | r.orient(ds, s.Transform) | ||
| 203 | d := r.dd | 210 | d := r.dd | ||
| 204 | cmd := slot.cmd | 211 | cmd := slot.cmd | ||
M internal/adapters/vulkan/renderer.go +6 -0
| @@ -41,6 +41,8 @@ type Renderer struct { | |||
| 41 | 41 | modPlanes map[uint64]uint32 | |
| 42 | 42 | // log reports the chosen target modifier; the zero value discards. | |
| 43 | 43 | log zerowrap.Logger | |
| 44 | + | // traceDamage logs every frame's redrawn area (--debug=render-damage). | |
| 45 | + | traceDamage bool | |
| 44 | 46 | // hdrReadback makes exported HDR targets transfer sources, for GPU tests | |
| 45 | 47 | // and headless --screenshot-raw (SetHDRReadback); never DRM. | |
| 46 | 48 | hdrReadback bool | |
| @@ -359,6 +361,10 @@ func (r *Renderer) TakeRedrawn() int { | |||
| 359 | 361 | // events. Not on the port; the zero logger discards. | |
| 360 | 362 | func (r *Renderer) SetLogger(l zerowrap.Logger) { r.log = l } | |
| 361 | 363 | ||
| 364 | + | // SetTraceDamage logs the redrawn pixels, draw count and partial flag of | |
| 365 | + | // every frame at debug level (--debug=render-damage). Off by default. | |
| 366 | + | func (r *Renderer) SetTraceDamage(on bool) { r.traceDamage = on } | |
| 367 | + | ||
| 362 | 368 | // QueuePriority is the global priority the queue got: realtime, high or | |
| 363 | 369 | // default. | |
| 364 | 370 | func (r *Renderer) QueuePriority() string { return r.queuePriority } | |
| @@ -41,6 +41,8 @@ type Renderer struct { | |||||
| 41 | modPlanes map[uint64]uint32 | 41 | modPlanes map[uint64]uint32 | ||
| 42 | // log reports the chosen target modifier; the zero value discards. | 42 | // log reports the chosen target modifier; the zero value discards. | ||
| 43 | log zerowrap.Logger | 43 | log zerowrap.Logger | ||
| 44 | + | // traceDamage logs every frame's redrawn area (--debug=render-damage). | |||
| 45 | + | traceDamage bool | |||
| 44 | // hdrReadback makes exported HDR targets transfer sources, for GPU tests | 46 | // hdrReadback makes exported HDR targets transfer sources, for GPU tests | ||
| 45 | // and headless --screenshot-raw (SetHDRReadback); never DRM. | 47 | // and headless --screenshot-raw (SetHDRReadback); never DRM. | ||
| 46 | hdrReadback bool | 48 | hdrReadback bool | ||
| @@ -359,6 +361,10 @@ func (r *Renderer) TakeRedrawn() int { | |||||
| 359 | // events. Not on the port; the zero logger discards. | 361 | // events. Not on the port; the zero logger discards. | ||
| 360 | func (r *Renderer) SetLogger(l zerowrap.Logger) { r.log = l } | 362 | func (r *Renderer) SetLogger(l zerowrap.Logger) { r.log = l } | ||
| 361 | 363 | ||||
| 364 | + | // SetTraceDamage logs the redrawn pixels, draw count and partial flag of | |||
| 365 | + | // every frame at debug level (--debug=render-damage). Off by default. | |||
| 366 | + | func (r *Renderer) SetTraceDamage(on bool) { r.traceDamage = on } | |||
| 367 | + | ||||
| 362 | // QueuePriority is the global priority the queue got: realtime, high or | 368 | // QueuePriority is the global priority the queue got: realtime, high or | ||
| 363 | // default. | 369 | // default. | ||
| 364 | func (r *Renderer) QueuePriority() string { return r.queuePriority } | 370 | func (r *Renderer) QueuePriority() string { return r.queuePriority } | ||
A internal/adapters/wayland/damage_log_test.go +105 -0
| @@ -0,0 +1,105 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "strings" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 9 | + | "github.com/bnema/go-wayland-bindings/server/xdgshell" | |
| 10 | + | "github.com/bnema/zerowrap" | |
| 11 | + | "golang.org/x/sys/unix" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // --debug=wayland-damage logs one "client damage" entry per fresh commit | |
| 15 | + | // with the damage in buffer pixels; without it nothing is logged. | |
| 16 | + | func TestClientDamageLog(t *testing.T) { | |
| 17 | + | for _, on := range []bool{true, false} { | |
| 18 | + | var out lockedBuffer | |
| 19 | + | log := zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &out}).WithField("component", "wayland") | |
| 20 | + | s, events, _, contents, dir := contentServerWith(t, Options{LogDamage: on}, log) | |
| 21 | + | c := protocolClient(t, s, dir) | |
| 22 | + | comp := bindVersion(t, c, "wl_compositor", 4) | |
| 23 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |
| 24 | + | shm := bindProtocol(t, c, "wl_shm") | |
| 25 | + | registerProtocol(t, c, shm) | |
| 26 | + | surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID() | |
| 27 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |
| 28 | + | registerProtocol(t, c, surf) | |
| 29 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |
| 30 | + | serials := make(chan uint32, 8) | |
| 31 | + | xp := &configureProxy{serial: serials} | |
| 32 | + | xp.SetID(xdg) | |
| 33 | + | registerWireProxy(c, xp) | |
| 34 | + | registerProtocol(t, c, top) | |
| 35 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |
| 36 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 37 | + | if err := c.Roundtrip(); err != nil { | |
| 38 | + | t.Fatal(err) | |
| 39 | + | } | |
| 40 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials) | |
| 41 | + | fd, err := unix.MemfdCreate("damagelog", 0) | |
| 42 | + | if err != nil { | |
| 43 | + | t.Fatal(err) | |
| 44 | + | } | |
| 45 | + | defer unix.Close(fd) | |
| 46 | + | if err := unix.Ftruncate(fd, 16*16*4); err != nil { | |
| 47 | + | t.Fatal(err) | |
| 48 | + | } | |
| 49 | + | pool, buf := c.AllocateID(), c.AllocateID() | |
| 50 | + | registerProtocol(t, c, pool) | |
| 51 | + | if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(16*16*4)); err != nil { | |
| 52 | + | t.Fatal(err) | |
| 53 | + | } | |
| 54 | + | registerProtocol(t, c, buf) | |
| 55 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(16), int32(16), int32(64), uint32(0)) | |
| 56 | + | commit := func(rects ...[4]int32) { | |
| 57 | + | t.Helper() | |
| 58 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 59 | + | for _, r := range rects { | |
| 60 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, r[0], r[1], r[2], r[3]) | |
| 61 | + | } | |
| 62 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 63 | + | if err := c.Roundtrip(); err != nil { | |
| 64 | + | t.Fatal(err) | |
| 65 | + | } | |
| 66 | + | select { | |
| 67 | + | case <-contents: | |
| 68 | + | case <-time.After(2 * time.Second): | |
| 69 | + | t.Fatal("no content") | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | // First buffer: a new size, full. Then a small rect, then one | |
| 73 | + | // covering the whole buffer. | |
| 74 | + | commit([4]int32{0, 0, 4, 4}) | |
| 75 | + | mapped(t, events, 2*time.Second) | |
| 76 | + | commit([4]int32{2, 3, 4, 5}) | |
| 77 | + | commit([4]int32{0, 0, 16, 16}) | |
| 78 | + | lines := strings.Split(strings.TrimSpace(out.String()), "\n") | |
| 79 | + | var damage []string | |
| 80 | + | for _, l := range lines { | |
| 81 | + | if strings.Contains(l, `"client damage"`) { | |
| 82 | + | damage = append(damage, l) | |
| 83 | + | } | |
| 84 | + | } | |
| 85 | + | if !on { | |
| 86 | + | if len(damage) != 0 { | |
| 87 | + | t.Fatalf("logged with LogDamage off: %q", damage) | |
| 88 | + | } | |
| 89 | + | continue | |
| 90 | + | } | |
| 91 | + | if len(damage) != 3 { | |
| 92 | + | t.Fatalf("%d client damage entries, want 3: %q", len(damage), damage) | |
| 93 | + | } | |
| 94 | + | for i, want := range []string{`"full_buffer":true`, `"full_buffer":false`, `"full_buffer":true`} { | |
| 95 | + | if !strings.Contains(damage[i], want) { | |
| 96 | + | t.Errorf("entry %d lacks %s: %s", i, want, damage[i]) | |
| 97 | + | } | |
| 98 | + | } | |
| 99 | + | for _, want := range []string{`"component":"wayland"`, `"bw":16`, `"bh":16`, `"rects":1`, `"window":`, `"first_rects":[{"x":2,"y":3,"w":4,"h":5}]`} { | |
| 100 | + | if !strings.Contains(damage[1], want) { | |
| 101 | + | t.Errorf("entry 1 lacks %s: %s", want, damage[1]) | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | } | |
| @@ -0,0 +1,105 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "strings" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 9 | + | "github.com/bnema/go-wayland-bindings/server/xdgshell" | |||
| 10 | + | "github.com/bnema/zerowrap" | |||
| 11 | + | "golang.org/x/sys/unix" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // --debug=wayland-damage logs one "client damage" entry per fresh commit | |||
| 15 | + | // with the damage in buffer pixels; without it nothing is logged. | |||
| 16 | + | func TestClientDamageLog(t *testing.T) { | |||
| 17 | + | for _, on := range []bool{true, false} { | |||
| 18 | + | var out lockedBuffer | |||
| 19 | + | log := zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &out}).WithField("component", "wayland") | |||
| 20 | + | s, events, _, contents, dir := contentServerWith(t, Options{LogDamage: on}, log) | |||
| 21 | + | c := protocolClient(t, s, dir) | |||
| 22 | + | comp := bindVersion(t, c, "wl_compositor", 4) | |||
| 23 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |||
| 24 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 25 | + | registerProtocol(t, c, shm) | |||
| 26 | + | surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID() | |||
| 27 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 28 | + | registerProtocol(t, c, surf) | |||
| 29 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |||
| 30 | + | serials := make(chan uint32, 8) | |||
| 31 | + | xp := &configureProxy{serial: serials} | |||
| 32 | + | xp.SetID(xdg) | |||
| 33 | + | registerWireProxy(c, xp) | |||
| 34 | + | registerProtocol(t, c, top) | |||
| 35 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |||
| 36 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 37 | + | if err := c.Roundtrip(); err != nil { | |||
| 38 | + | t.Fatal(err) | |||
| 39 | + | } | |||
| 40 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials) | |||
| 41 | + | fd, err := unix.MemfdCreate("damagelog", 0) | |||
| 42 | + | if err != nil { | |||
| 43 | + | t.Fatal(err) | |||
| 44 | + | } | |||
| 45 | + | defer unix.Close(fd) | |||
| 46 | + | if err := unix.Ftruncate(fd, 16*16*4); err != nil { | |||
| 47 | + | t.Fatal(err) | |||
| 48 | + | } | |||
| 49 | + | pool, buf := c.AllocateID(), c.AllocateID() | |||
| 50 | + | registerProtocol(t, c, pool) | |||
| 51 | + | if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(16*16*4)); err != nil { | |||
| 52 | + | t.Fatal(err) | |||
| 53 | + | } | |||
| 54 | + | registerProtocol(t, c, buf) | |||
| 55 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(16), int32(16), int32(64), uint32(0)) | |||
| 56 | + | commit := func(rects ...[4]int32) { | |||
| 57 | + | t.Helper() | |||
| 58 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 59 | + | for _, r := range rects { | |||
| 60 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, r[0], r[1], r[2], r[3]) | |||
| 61 | + | } | |||
| 62 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 63 | + | if err := c.Roundtrip(); err != nil { | |||
| 64 | + | t.Fatal(err) | |||
| 65 | + | } | |||
| 66 | + | select { | |||
| 67 | + | case <-contents: | |||
| 68 | + | case <-time.After(2 * time.Second): | |||
| 69 | + | t.Fatal("no content") | |||
| 70 | + | } | |||
| 71 | + | } | |||
| 72 | + | // First buffer: a new size, full. Then a small rect, then one | |||
| 73 | + | // covering the whole buffer. | |||
| 74 | + | commit([4]int32{0, 0, 4, 4}) | |||
| 75 | + | mapped(t, events, 2*time.Second) | |||
| 76 | + | commit([4]int32{2, 3, 4, 5}) | |||
| 77 | + | commit([4]int32{0, 0, 16, 16}) | |||
| 78 | + | lines := strings.Split(strings.TrimSpace(out.String()), "\n") | |||
| 79 | + | var damage []string | |||
| 80 | + | for _, l := range lines { | |||
| 81 | + | if strings.Contains(l, `"client damage"`) { | |||
| 82 | + | damage = append(damage, l) | |||
| 83 | + | } | |||
| 84 | + | } | |||
| 85 | + | if !on { | |||
| 86 | + | if len(damage) != 0 { | |||
| 87 | + | t.Fatalf("logged with LogDamage off: %q", damage) | |||
| 88 | + | } | |||
| 89 | + | continue | |||
| 90 | + | } | |||
| 91 | + | if len(damage) != 3 { | |||
| 92 | + | t.Fatalf("%d client damage entries, want 3: %q", len(damage), damage) | |||
| 93 | + | } | |||
| 94 | + | for i, want := range []string{`"full_buffer":true`, `"full_buffer":false`, `"full_buffer":true`} { | |||
| 95 | + | if !strings.Contains(damage[i], want) { | |||
| 96 | + | t.Errorf("entry %d lacks %s: %s", i, want, damage[i]) | |||
| 97 | + | } | |||
| 98 | + | } | |||
| 99 | + | for _, want := range []string{`"component":"wayland"`, `"bw":16`, `"bh":16`, `"rects":1`, `"window":`, `"first_rects":[{"x":2,"y":3,"w":4,"h":5}]`} { | |||
| 100 | + | if !strings.Contains(damage[1], want) { | |||
| 101 | + | t.Errorf("entry 1 lacks %s: %s", want, damage[1]) | |||
| 102 | + | } | |||
| 103 | + | } | |||
| 104 | + | } | |||
| 105 | + | } | |||
M internal/adapters/wayland/scale_test.go +9 -1
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "github.com/bnema/neferwl/internal/adapters/logging" | |
| 14 | 14 | "github.com/bnema/neferwl/internal/ports" | |
| 15 | 15 | "github.com/bnema/wlturbo" | |
| 16 | + | "github.com/bnema/zerowrap" | |
| 16 | 17 | "golang.org/x/sys/unix" | |
| 17 | 18 | ) | |
| 18 | 19 | ||
| @@ -52,12 +53,19 @@ func (p *surfaceTransformProxy) Dispatch(e *wlturbo.Event) { | |||
| 52 | 53 | } | |
| 53 | 54 | ||
| 54 | 55 | func contentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, string) { | |
| 56 | + | t.Helper() | |
| 57 | + | return contentServerWith(t, Options{}, logging.For(context.Background(), "wayland")) | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | // contentServerWith is contentServer with extra options and a logger. | |
| 61 | + | func contentServerWith(t *testing.T, opts Options, log zerowrap.Logger) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, string) { | |
| 55 | 62 | t.Helper() | |
| 56 | 63 | dir := t.TempDir() | |
| 57 | 64 | events := make(chan ports.ClientEvent, 16) | |
| 58 | 65 | commands := make(chan ports.ClientCommand, 16) | |
| 59 | 66 | contents := make(chan ports.SurfaceContent, 16) | |
| 60 | - | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Contents: contents}, logging.For(context.Background(), "wayland")) | |
| 67 | + | opts.RuntimeDir, opts.Outputs = dir, testOutputs | |
| 68 | + | s, err := New(opts, Channels{Events: events, Commands: commands, Contents: contents}, log) | |
| 61 | 69 | if err != nil { | |
| 62 | 70 | t.Fatal(err) | |
| 63 | 71 | } | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "github.com/bnema/neferwl/internal/adapters/logging" | 13 | "github.com/bnema/neferwl/internal/adapters/logging" | ||
| 14 | "github.com/bnema/neferwl/internal/ports" | 14 | "github.com/bnema/neferwl/internal/ports" | ||
| 15 | "github.com/bnema/wlturbo" | 15 | "github.com/bnema/wlturbo" | ||
| 16 | + | "github.com/bnema/zerowrap" | |||
| 16 | "golang.org/x/sys/unix" | 17 | "golang.org/x/sys/unix" | ||
| 17 | ) | 18 | ) | ||
| 18 | 19 | ||||
| @@ -52,12 +53,19 @@ func (p *surfaceTransformProxy) Dispatch(e *wlturbo.Event) { | |||||
| 52 | } | 53 | } | ||
| 53 | 54 | ||||
| 54 | func contentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, string) { | 55 | func contentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, string) { | ||
| 56 | + | t.Helper() | |||
| 57 | + | return contentServerWith(t, Options{}, logging.For(context.Background(), "wayland")) | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | // contentServerWith is contentServer with extra options and a logger. | |||
| 61 | + | func contentServerWith(t *testing.T, opts Options, log zerowrap.Logger) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, string) { | |||
| 55 | t.Helper() | 62 | t.Helper() | ||
| 56 | dir := t.TempDir() | 63 | dir := t.TempDir() | ||
| 57 | events := make(chan ports.ClientEvent, 16) | 64 | events := make(chan ports.ClientEvent, 16) | ||
| 58 | commands := make(chan ports.ClientCommand, 16) | 65 | commands := make(chan ports.ClientCommand, 16) | ||
| 59 | contents := make(chan ports.SurfaceContent, 16) | 66 | contents := make(chan ports.SurfaceContent, 16) | ||
| 60 | - | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Contents: contents}, logging.For(context.Background(), "wayland")) | 67 | + | opts.RuntimeDir, opts.Outputs = dir, testOutputs |
| 68 | + | s, err := New(opts, Channels{Events: events, Commands: commands, Contents: contents}, log) | |||
| 61 | if err != nil { | 69 | if err != nil { | ||
| 62 | t.Fatal(err) | 70 | t.Fatal(err) | ||
| 63 | } | 71 | } | ||
M internal/adapters/wayland/server.go +5 -0
| @@ -48,6 +48,9 @@ type Options struct { | |||
| 48 | 48 | // nil lets every client capture, unless the session is protected. The | |
| 49 | 49 | // caller keeps it current (captureallow.Store.Run). | |
| 50 | 50 | CaptureAllow *captureallow.Store | |
| 51 | + | // LogDamage logs the damage of every fresh client commit at debug level | |
| 52 | + | // (--debug=wayland-damage). | |
| 53 | + | LogDamage bool | |
| 51 | 54 | // syncDev replaces the render node's syncobj interface (tests). | |
| 52 | 55 | syncDev syncobjDevice | |
| 53 | 56 | // peer replaces the peer executable lookup (tests). | |
| @@ -144,6 +147,7 @@ type Server struct { | |||
| 144 | 147 | name string | |
| 145 | 148 | cleanup func() | |
| 146 | 149 | log zerowrap.Logger | |
| 150 | + | logDamage bool | |
| 147 | 151 | channels Channels | |
| 148 | 152 | // awaiting holds frame callbacks by output name, due at its next | |
| 149 | 153 | // frame (frameDue, or its page flip). | |
| @@ -321,6 +325,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 321 | 325 | name: name, | |
| 322 | 326 | cleanup: cleanup, | |
| 323 | 327 | log: log, | |
| 328 | + | logDamage: opts.LogDamage, | |
| 324 | 329 | channels: ch, | |
| 325 | 330 | surfaces: make(map[*server.Resource]*surface), | |
| 326 | 331 | buffers: make(map[*server.Resource]clientBuffer), | |
| @@ -48,6 +48,9 @@ type Options struct { | |||||
| 48 | // nil lets every client capture, unless the session is protected. The | 48 | // nil lets every client capture, unless the session is protected. The | ||
| 49 | // caller keeps it current (captureallow.Store.Run). | 49 | // caller keeps it current (captureallow.Store.Run). | ||
| 50 | CaptureAllow *captureallow.Store | 50 | CaptureAllow *captureallow.Store | ||
| 51 | + | // LogDamage logs the damage of every fresh client commit at debug level | |||
| 52 | + | // (--debug=wayland-damage). | |||
| 53 | + | LogDamage bool | |||
| 51 | // syncDev replaces the render node's syncobj interface (tests). | 54 | // syncDev replaces the render node's syncobj interface (tests). | ||
| 52 | syncDev syncobjDevice | 55 | syncDev syncobjDevice | ||
| 53 | // peer replaces the peer executable lookup (tests). | 56 | // peer replaces the peer executable lookup (tests). | ||
| @@ -144,6 +147,7 @@ type Server struct { | |||||
| 144 | name string | 147 | name string | ||
| 145 | cleanup func() | 148 | cleanup func() | ||
| 146 | log zerowrap.Logger | 149 | log zerowrap.Logger | ||
| 150 | + | logDamage bool | |||
| 147 | channels Channels | 151 | channels Channels | ||
| 148 | // awaiting holds frame callbacks by output name, due at its next | 152 | // awaiting holds frame callbacks by output name, due at its next | ||
| 149 | // frame (frameDue, or its page flip). | 153 | // frame (frameDue, or its page flip). | ||
| @@ -321,6 +325,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 321 | name: name, | 325 | name: name, | ||
| 322 | cleanup: cleanup, | 326 | cleanup: cleanup, | ||
| 323 | log: log, | 327 | log: log, | ||
| 328 | + | logDamage: opts.LogDamage, | |||
| 324 | channels: ch, | 329 | channels: ch, | ||
| 325 | surfaces: make(map[*server.Resource]*surface), | 330 | surfaces: make(map[*server.Resource]*surface), | ||
| 326 | buffers: make(map[*server.Resource]clientBuffer), | 331 | buffers: make(map[*server.Resource]clientBuffer), | ||
M internal/adapters/wayland/surface.go +32 -0
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "github.com/bnema/go-wayland-bindings/server/wlrlayershell" | |
| 14 | 14 | "github.com/bnema/go-wayland-bindings/server/xdgshell" | |
| 15 | 15 | "github.com/bnema/neferwl/internal/ports" | |
| 16 | + | "github.com/rs/zerolog" | |
| 16 | 17 | ) | |
| 17 | 18 | ||
| 18 | 19 | type roleKind uint8 | |
| @@ -588,6 +589,37 @@ func (s *surface) DamageBuffer(_ *wayland.Surface, x, y, w, h int32) { | |||
| 588 | 589 | // A new size, too many rects or none at all with a new buffer (clients | |
| 589 | 590 | // that do not report damage) is a full change. | |
| 590 | 591 | func (s *surface) commitDamage(u *update, fresh, resized bool, bw, bh int) { | |
| 592 | + | s.mapDamage(u, fresh, resized, bw, bh) | |
| 593 | + | if fresh && s.server.logDamage { | |
| 594 | + | s.traceDamage(bw, bh) | |
| 595 | + | } | |
| 596 | + | } | |
| 597 | + | ||
| 598 | + | // traceDamage logs the damage commitDamage just computed | |
| 599 | + | // (--debug=wayland-damage): full_buffer is set when the commit is a full | |
| 600 | + | // change or one rect covers the whole buffer. Rects are already clipped to | |
| 601 | + | // the buffer, so covering means equal to it. | |
| 602 | + | func (s *surface) traceDamage(bw, bh int) { | |
| 603 | + | d := s.committed | |
| 604 | + | full := d.full | |
| 605 | + | for _, r := range d.rects { | |
| 606 | + | if r.X <= 0 && r.Y <= 0 && r.X+r.W >= bw && r.Y+r.H >= bh { | |
| 607 | + | full = true | |
| 608 | + | } | |
| 609 | + | } | |
| 610 | + | arr := zerolog.Arr() | |
| 611 | + | for _, r := range d.rects[:min(len(d.rects), maxLoggedDamageRects)] { | |
| 612 | + | arr.Dict(zerolog.Dict().Int("x", r.X).Int("y", r.Y).Int("w", r.W).Int("h", r.H)) | |
| 613 | + | } | |
| 614 | + | s.server.log.Debug().Uint64("window", uint64(s.root().windowID())).Int("bw", bw).Int("bh", bh). | |
| 615 | + | Int("rects", len(d.rects)).Bool("full_buffer", full).Array("first_rects", arr).Msg("client damage") | |
| 616 | + | } | |
| 617 | + | ||
| 618 | + | // maxLoggedDamageRects bounds the rects one "client damage" entry lists. | |
| 619 | + | const maxLoggedDamageRects = 4 | |
| 620 | + | ||
| 621 | + | // mapDamage sets s.committed from the commit's requested damage. | |
| 622 | + | func (s *surface) mapDamage(u *update, fresh, resized bool, bw, bh int) { | |
| 591 | 623 | surf, buf := u.damage, u.bufDamage | |
| 592 | 624 | if !fresh { | |
| 593 | 625 | s.committed = damage{} | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "github.com/bnema/go-wayland-bindings/server/wlrlayershell" | 13 | "github.com/bnema/go-wayland-bindings/server/wlrlayershell" | ||
| 14 | "github.com/bnema/go-wayland-bindings/server/xdgshell" | 14 | "github.com/bnema/go-wayland-bindings/server/xdgshell" | ||
| 15 | "github.com/bnema/neferwl/internal/ports" | 15 | "github.com/bnema/neferwl/internal/ports" | ||
| 16 | + | "github.com/rs/zerolog" | |||
| 16 | ) | 17 | ) | ||
| 17 | 18 | ||||
| 18 | type roleKind uint8 | 19 | type roleKind uint8 | ||
| @@ -588,6 +589,37 @@ func (s *surface) DamageBuffer(_ *wayland.Surface, x, y, w, h int32) { | |||||
| 588 | // A new size, too many rects or none at all with a new buffer (clients | 589 | // A new size, too many rects or none at all with a new buffer (clients | ||
| 589 | // that do not report damage) is a full change. | 590 | // that do not report damage) is a full change. | ||
| 590 | func (s *surface) commitDamage(u *update, fresh, resized bool, bw, bh int) { | 591 | func (s *surface) commitDamage(u *update, fresh, resized bool, bw, bh int) { | ||
| 592 | + | s.mapDamage(u, fresh, resized, bw, bh) | |||
| 593 | + | if fresh && s.server.logDamage { | |||
| 594 | + | s.traceDamage(bw, bh) | |||
| 595 | + | } | |||
| 596 | + | } | |||
| 597 | + | ||||
| 598 | + | // traceDamage logs the damage commitDamage just computed | |||
| 599 | + | // (--debug=wayland-damage): full_buffer is set when the commit is a full | |||
| 600 | + | // change or one rect covers the whole buffer. Rects are already clipped to | |||
| 601 | + | // the buffer, so covering means equal to it. | |||
| 602 | + | func (s *surface) traceDamage(bw, bh int) { | |||
| 603 | + | d := s.committed | |||
| 604 | + | full := d.full | |||
| 605 | + | for _, r := range d.rects { | |||
| 606 | + | if r.X <= 0 && r.Y <= 0 && r.X+r.W >= bw && r.Y+r.H >= bh { | |||
| 607 | + | full = true | |||
| 608 | + | } | |||
| 609 | + | } | |||
| 610 | + | arr := zerolog.Arr() | |||
| 611 | + | for _, r := range d.rects[:min(len(d.rects), maxLoggedDamageRects)] { | |||
| 612 | + | arr.Dict(zerolog.Dict().Int("x", r.X).Int("y", r.Y).Int("w", r.W).Int("h", r.H)) | |||
| 613 | + | } | |||
| 614 | + | s.server.log.Debug().Uint64("window", uint64(s.root().windowID())).Int("bw", bw).Int("bh", bh). | |||
| 615 | + | Int("rects", len(d.rects)).Bool("full_buffer", full).Array("first_rects", arr).Msg("client damage") | |||
| 616 | + | } | |||
| 617 | + | ||||
| 618 | + | // maxLoggedDamageRects bounds the rects one "client damage" entry lists. | |||
| 619 | + | const maxLoggedDamageRects = 4 | |||
| 620 | + | ||||
| 621 | + | // mapDamage sets s.committed from the commit's requested damage. | |||
| 622 | + | func (s *surface) mapDamage(u *update, fresh, resized bool, bw, bh int) { | |||
| 591 | surf, buf := u.damage, u.bufDamage | 623 | surf, buf := u.damage, u.bufDamage | ||
| 592 | if !fresh { | 624 | if !fresh { | ||
| 593 | s.committed = damage{} | 625 | s.committed = damage{} | ||
M internal/app/headless_test.go +1 -1
| @@ -15,7 +15,7 @@ import ( | |||
| 15 | 15 | // and a headless output falls back to SDR. Skips without Vulkan or without a | |
| 16 | 16 | // device that exports dmabufs (lavapipe in CI). | |
| 17 | 17 | func TestVirtualRendererExportsHDRTargets(t *testing.T) { | |
| 18 | - | r, err := newVulkanRenderer(logging.For(t.Context(), "render"), true, false)(64, 16) | |
| 18 | + | r, err := newVulkanRenderer(logging.For(t.Context(), "render"), true, false, false)(64, 16) | |
| 19 | 19 | if err != nil { | |
| 20 | 20 | t.Skipf("Vulkan unavailable: %v", err) | |
| 21 | 21 | } | |
| @@ -15,7 +15,7 @@ import ( | |||||
| 15 | // and a headless output falls back to SDR. Skips without Vulkan or without a | 15 | // and a headless output falls back to SDR. Skips without Vulkan or without a | ||
| 16 | // device that exports dmabufs (lavapipe in CI). | 16 | // device that exports dmabufs (lavapipe in CI). | ||
| 17 | func TestVirtualRendererExportsHDRTargets(t *testing.T) { | 17 | func TestVirtualRendererExportsHDRTargets(t *testing.T) { | ||
| 18 | - | r, err := newVulkanRenderer(logging.For(t.Context(), "render"), true, false)(64, 16) | 18 | + | r, err := newVulkanRenderer(logging.For(t.Context(), "render"), true, false, false)(64, 16) |
| 19 | if err != nil { | 19 | if err != nil { | ||
| 20 | t.Skipf("Vulkan unavailable: %v", err) | 20 | t.Skipf("Vulkan unavailable: %v", err) | ||
| 21 | } | 21 | } | ||
M internal/app/session.go +8 -4
| @@ -123,6 +123,7 @@ func assembleSession(ctx context.Context, opts Options) (s *session, err error) | |||
| 123 | 123 | Keymap: s.km.String(), | |
| 124 | 124 | RepeatRate: opts.Config.Keyboard.RepeatRate, | |
| 125 | 125 | RepeatDelay: opts.Config.Keyboard.RepeatDelay, | |
| 126 | + | LogDamage: logging.Enabled(ctx, "wayland-damage"), | |
| 126 | 127 | }, s.p.wayland(), logging.For(ctx, "wayland")); err != nil { | |
| 127 | 128 | return s, err | |
| 128 | 129 | } | |
| @@ -145,10 +146,11 @@ func assembleSession(ctx context.Context, opts Options) (s *session, err error) | |||
| 145 | 146 | s.path = config.DefaultPath() | |
| 146 | 147 | } | |
| 147 | 148 | renderLog := logging.For(ctx, "render") | |
| 149 | + | traceDamage := logging.Enabled(ctx, "render-damage") | |
| 148 | 150 | if s.hw != nil { | |
| 149 | - | s.newRenderer = newVulkanRenderer(renderLog, false, false) | |
| 151 | + | s.newRenderer = newVulkanRenderer(renderLog, false, false, traceDamage) | |
| 150 | 152 | } else { | |
| 151 | - | s.newRenderer = newVulkanRenderer(renderLog, true, opts.ScreenshotRaw) | |
| 153 | + | s.newRenderer = newVulkanRenderer(renderLog, true, opts.ScreenshotRaw, traceDamage) | |
| 152 | 154 | } | |
| 153 | 155 | return s, nil | |
| 154 | 156 | } | |
| @@ -182,14 +184,16 @@ func (s *session) close() { | |||
| 182 | 184 | // outputs have no display to list modifiers, so a headless renderer is told | |
| 183 | 185 | // it drives one: without it a real GPU refuses the HDR targets and the | |
| 184 | 186 | // output falls back to SDR. readback makes the HDR targets readable | |
| 185 | - | // (HDRPixels). These settings are Vulkan-only and not on ports.Renderer. | |
| 186 | - | func newVulkanRenderer(log zerowrap.Logger, virtual, readback bool) func(w, h int) (ports.Renderer, error) { | |
| 187 | + | // (HDRPixels). traceDamage logs every frame's redrawn area. These settings | |
| 188 | + | // are Vulkan-only and not on ports.Renderer. | |
| 189 | + | func newVulkanRenderer(log zerowrap.Logger, virtual, readback, traceDamage bool) func(w, h int) (ports.Renderer, error) { | |
| 187 | 190 | return func(w, h int) (ports.Renderer, error) { | |
| 188 | 191 | r, err := vulkan.New(w, h) | |
| 189 | 192 | if err != nil { | |
| 190 | 193 | return nil, err | |
| 191 | 194 | } | |
| 192 | 195 | r.SetLogger(log) | |
| 196 | + | r.SetTraceDamage(traceDamage) | |
| 193 | 197 | log.Info().Str("queue_priority", r.QueuePriority()).Msg("vulkan queue") | |
| 194 | 198 | r.SetVirtualOutput(virtual) | |
| 195 | 199 | if readback { | |
| @@ -123,6 +123,7 @@ func assembleSession(ctx context.Context, opts Options) (s *session, err error) | |||||
| 123 | Keymap: s.km.String(), | 123 | Keymap: s.km.String(), | ||
| 124 | RepeatRate: opts.Config.Keyboard.RepeatRate, | 124 | RepeatRate: opts.Config.Keyboard.RepeatRate, | ||
| 125 | RepeatDelay: opts.Config.Keyboard.RepeatDelay, | 125 | RepeatDelay: opts.Config.Keyboard.RepeatDelay, | ||
| 126 | + | LogDamage: logging.Enabled(ctx, "wayland-damage"), | |||
| 126 | }, s.p.wayland(), logging.For(ctx, "wayland")); err != nil { | 127 | }, s.p.wayland(), logging.For(ctx, "wayland")); err != nil { | ||
| 127 | return s, err | 128 | return s, err | ||
| 128 | } | 129 | } | ||
| @@ -145,10 +146,11 @@ func assembleSession(ctx context.Context, opts Options) (s *session, err error) | |||||
| 145 | s.path = config.DefaultPath() | 146 | s.path = config.DefaultPath() | ||
| 146 | } | 147 | } | ||
| 147 | renderLog := logging.For(ctx, "render") | 148 | renderLog := logging.For(ctx, "render") | ||
| 149 | + | traceDamage := logging.Enabled(ctx, "render-damage") | |||
| 148 | if s.hw != nil { | 150 | if s.hw != nil { | ||
| 149 | - | s.newRenderer = newVulkanRenderer(renderLog, false, false) | 151 | + | s.newRenderer = newVulkanRenderer(renderLog, false, false, traceDamage) |
| 150 | } else { | 152 | } else { | ||
| 151 | - | s.newRenderer = newVulkanRenderer(renderLog, true, opts.ScreenshotRaw) | 153 | + | s.newRenderer = newVulkanRenderer(renderLog, true, opts.ScreenshotRaw, traceDamage) |
| 152 | } | 154 | } | ||
| 153 | return s, nil | 155 | return s, nil | ||
| 154 | } | 156 | } | ||
| @@ -182,14 +184,16 @@ func (s *session) close() { | |||||
| 182 | // outputs have no display to list modifiers, so a headless renderer is told | 184 | // outputs have no display to list modifiers, so a headless renderer is told | ||
| 183 | // it drives one: without it a real GPU refuses the HDR targets and the | 185 | // it drives one: without it a real GPU refuses the HDR targets and the | ||
| 184 | // output falls back to SDR. readback makes the HDR targets readable | 186 | // output falls back to SDR. readback makes the HDR targets readable | ||
| 185 | - | // (HDRPixels). These settings are Vulkan-only and not on ports.Renderer. | 187 | + | // (HDRPixels). traceDamage logs every frame's redrawn area. These settings |
| 186 | - | func newVulkanRenderer(log zerowrap.Logger, virtual, readback bool) func(w, h int) (ports.Renderer, error) { | 188 | + | // are Vulkan-only and not on ports.Renderer. |
| 189 | + | func newVulkanRenderer(log zerowrap.Logger, virtual, readback, traceDamage bool) func(w, h int) (ports.Renderer, error) { | |||
| 187 | return func(w, h int) (ports.Renderer, error) { | 190 | return func(w, h int) (ports.Renderer, error) { | ||
| 188 | r, err := vulkan.New(w, h) | 191 | r, err := vulkan.New(w, h) | ||
| 189 | if err != nil { | 192 | if err != nil { | ||
| 190 | return nil, err | 193 | return nil, err | ||
| 191 | } | 194 | } | ||
| 192 | r.SetLogger(log) | 195 | r.SetLogger(log) | ||
| 196 | + | r.SetTraceDamage(traceDamage) | |||
| 193 | log.Info().Str("queue_priority", r.QueuePriority()).Msg("vulkan queue") | 197 | log.Info().Str("queue_priority", r.QueuePriority()).Msg("vulkan queue") | ||
| 194 | r.SetVirtualOutput(virtual) | 198 | r.SetVirtualOutput(virtual) | ||
| 195 | if readback { | 199 | if readback { | ||
M internal/cli/run.go +1 -1
| @@ -39,7 +39,7 @@ func (f *runFlags) register(fs *flag.FlagSet) { | |||
| 39 | 39 | fs.BoolVar(&f.headlessHDR, "headless-hdr", false, "test-only: report HDR on virtual outputs; fall back if Vulkan HDR unavailable") | |
| 40 | 40 | fs.BoolVar(&f.screenshotRaw, "screenshot-raw", false, "test-only: also write latest-pq.png, the raw PQ codes of an HDR output") | |
| 41 | 41 | fs.DurationVar(&f.timeout, "timeout", 0, "duration before exit (0 disables timeout)") | |
| 42 | - | fs.StringVar(&f.debug, "debug", "", "debug components (comma-separated or all; all leaves out drm-flip, input-motion and input-keys, which log every flip, pointer motion or key)") | |
| 42 | + | fs.StringVar(&f.debug, "debug", "", "debug components (comma-separated or all; all leaves out drm-flip, input-motion, input-keys, wayland-damage and render-damage, which log every flip, pointer motion, key, client commit or frame)") | |
| 43 | 43 | fs.StringVar(&f.config, "config", "", "config path (empty uses XDG default)") | |
| 44 | 44 | fs.BoolVar(&f.session, "session", false, "run as the login session: share WAYLAND_DISPLAY and DISPLAY with systemd and D-Bus user services; notify systemd when ready") | |
| 45 | 45 | fs.StringVar(&f.pprof, "pprof", "", "serve Go runtime profiles (heap, CPU, trace) on this address, e.g. localhost:6060") | |
| @@ -39,7 +39,7 @@ func (f *runFlags) register(fs *flag.FlagSet) { | |||||
| 39 | fs.BoolVar(&f.headlessHDR, "headless-hdr", false, "test-only: report HDR on virtual outputs; fall back if Vulkan HDR unavailable") | 39 | fs.BoolVar(&f.headlessHDR, "headless-hdr", false, "test-only: report HDR on virtual outputs; fall back if Vulkan HDR unavailable") | ||
| 40 | fs.BoolVar(&f.screenshotRaw, "screenshot-raw", false, "test-only: also write latest-pq.png, the raw PQ codes of an HDR output") | 40 | fs.BoolVar(&f.screenshotRaw, "screenshot-raw", false, "test-only: also write latest-pq.png, the raw PQ codes of an HDR output") | ||
| 41 | fs.DurationVar(&f.timeout, "timeout", 0, "duration before exit (0 disables timeout)") | 41 | fs.DurationVar(&f.timeout, "timeout", 0, "duration before exit (0 disables timeout)") | ||
| 42 | - | fs.StringVar(&f.debug, "debug", "", "debug components (comma-separated or all; all leaves out drm-flip, input-motion and input-keys, which log every flip, pointer motion or key)") | 42 | + | fs.StringVar(&f.debug, "debug", "", "debug components (comma-separated or all; all leaves out drm-flip, input-motion, input-keys, wayland-damage and render-damage, which log every flip, pointer motion, key, client commit or frame)") |
| 43 | fs.StringVar(&f.config, "config", "", "config path (empty uses XDG default)") | 43 | fs.StringVar(&f.config, "config", "", "config path (empty uses XDG default)") | ||
| 44 | fs.BoolVar(&f.session, "session", false, "run as the login session: share WAYLAND_DISPLAY and DISPLAY with systemd and D-Bus user services; notify systemd when ready") | 44 | fs.BoolVar(&f.session, "session", false, "run as the login session: share WAYLAND_DISPLAY and DISPLAY with systemd and D-Bus user services; notify systemd when ready") | ||
| 45 | fs.StringVar(&f.pprof, "pprof", "", "serve Go runtime profiles (heap, CPU, trace) on this address, e.g. localhost:6060") | 45 | fs.StringVar(&f.pprof, "pprof", "", "serve Go runtime profiles (heap, CPU, trace) on this address, e.g. localhost:6060") | ||