perf: faster game frame path (#59)
* build(deps): use purego-vulkan with global priority bindings * perf(sched): request realtime input and output scheduling * perf(vulkan): request elevated queue priority with fallback * fix(wayland): gate buffer commits on signalled fences * fix(wayland): start buffer waiter without explicit sync * perf(drm): scan out scaled fullscreen buffers and set content type * refactor(perf): remove unused helper and simplify wait error * fix(sched): retire locked realtime threads on exit * fix(wayland): recover from invalid fences and skipped buffers * fix(drm): retest scanout scaling after modeset * test(wayland): use an unowned fd for the poll error case * fix(wayland): retry the implicit fence poll after a signal * fix(render): log the Vulkan queue priority through the render logger * build(deps): use purego-vulkan v0.6.0
- commit
- 92ab9adf8910107eb6365617bcf45197ae78862e
- author
- bnema · · 2026-09-29 05:55 UTC
- committer
- GitHub · · 2026-09-29 05:55 UTC
- parent
- bfed184
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
32 files · +919 -74
M README.md +1 -1
| @@ -61,7 +61,7 @@ These protocols and kernel features cut copies, waits and latency between the ap | |||
| 61 | 61 | - **Explicit sync.** `wp_linux_drm_syncobj_v1` passes GPU fences from the app to KMS. NVIDIA drivers need it. | |
| 62 | 62 | - **Frame pacing.** Apps get real flip times through `wp_presentation`, and can queue frames with `wp_fifo_v1` and `wp_commit_timing_v1`. | |
| 63 | 63 | - **Only what changed.** NeferWL redraws only damaged regions, skips surfaces hidden behind opaque ones, and leaves idle outputs alone. Frame, cursor, overlay and VRR go to the kernel in one atomic commit. | |
| 64 | - | - **Input.** Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. Input at 1 kHz and more never waits on rendering, and the cursor has its own hardware plane. | |
| 64 | + | - **Input.** Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. Input at 1 kHz and more never waits on rendering, and the cursor has its own hardware plane. With CAP_SYS_NICE, input and output threads request real-time scheduling. | |
| 65 | 65 | - **X11 games.** Steam and X11-only games run through xwayland-satellite, started on the first X11 connection. Wine with its Wayland driver does not need it. | |
| 66 | 66 | ||
| 67 | 67 | ## Hardware support | |
| @@ -61,7 +61,7 @@ These protocols and kernel features cut copies, waits and latency between the ap | |||||
| 61 | - **Explicit sync.** `wp_linux_drm_syncobj_v1` passes GPU fences from the app to KMS. NVIDIA drivers need it. | 61 | - **Explicit sync.** `wp_linux_drm_syncobj_v1` passes GPU fences from the app to KMS. NVIDIA drivers need it. | ||
| 62 | - **Frame pacing.** Apps get real flip times through `wp_presentation`, and can queue frames with `wp_fifo_v1` and `wp_commit_timing_v1`. | 62 | - **Frame pacing.** Apps get real flip times through `wp_presentation`, and can queue frames with `wp_fifo_v1` and `wp_commit_timing_v1`. | ||
| 63 | - **Only what changed.** NeferWL redraws only damaged regions, skips surfaces hidden behind opaque ones, and leaves idle outputs alone. Frame, cursor, overlay and VRR go to the kernel in one atomic commit. | 63 | - **Only what changed.** NeferWL redraws only damaged regions, skips surfaces hidden behind opaque ones, and leaves idle outputs alone. Frame, cursor, overlay and VRR go to the kernel in one atomic commit. | ||
| 64 | - | - **Input.** Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. Input at 1 kHz and more never waits on rendering, and the cursor has its own hardware plane. | 64 | + | - **Input.** Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. Input at 1 kHz and more never waits on rendering, and the cursor has its own hardware plane. With CAP_SYS_NICE, input and output threads request real-time scheduling. |
| 65 | - **X11 games.** Steam and X11-only games run through xwayland-satellite, started on the first X11 connection. Wine with its Wayland driver does not need it. | 65 | - **X11 games.** Steam and X11-only games run through xwayland-satellite, started on the first X11 connection. Wine with its Wayland driver does not need it. | ||
| 66 | 66 | ||||
| 67 | ## Hardware support | 67 | ## Hardware support | ||
M docs/config.md +1 -0
| @@ -49,6 +49,7 @@ NeferWL reads `$XDG_CONFIG_HOME/neferwl/config` (or `~/.config/neferwl/config`). | |||
| 49 | 49 | | `output.<name>.sdr-brightness` | `203` | How bright SDR desktop content appears in HDR, in nits (80–1000) | | |
| 50 | 50 | | `render.direct-scanout` | `on` | Fullscreen buffers straight to the display | | |
| 51 | 51 | | `render.tearing` / `render.vrr` | `on` / `on` | Tearing: games in direct scanout. VRR: any fullscreen window covering the output | | |
| 52 | + | | `performance.realtime` | `on` | Request real-time scheduling for output and input threads | | |
| 52 | 53 | | `log.level` / `log.debug` | `info` / empty | Log level / debug components or `all` | | |
| 53 | 54 | | `bind.<keys>` | see below | Action for a key combo; `none` removes a default | | |
| 54 | 55 | ||
| @@ -49,6 +49,7 @@ NeferWL reads `$XDG_CONFIG_HOME/neferwl/config` (or `~/.config/neferwl/config`). | |||||
| 49 | | `output.<name>.sdr-brightness` | `203` | How bright SDR desktop content appears in HDR, in nits (80–1000) | | 49 | | `output.<name>.sdr-brightness` | `203` | How bright SDR desktop content appears in HDR, in nits (80–1000) | | ||
| 50 | | `render.direct-scanout` | `on` | Fullscreen buffers straight to the display | | 50 | | `render.direct-scanout` | `on` | Fullscreen buffers straight to the display | | ||
| 51 | | `render.tearing` / `render.vrr` | `on` / `on` | Tearing: games in direct scanout. VRR: any fullscreen window covering the output | | 51 | | `render.tearing` / `render.vrr` | `on` / `on` | Tearing: games in direct scanout. VRR: any fullscreen window covering the output | | ||
| 52 | + | | `performance.realtime` | `on` | Request real-time scheduling for output and input threads | | |||
| 52 | | `log.level` / `log.debug` | `info` / empty | Log level / debug components or `all` | | 53 | | `log.level` / `log.debug` | `info` / empty | Log level / debug components or `all` | | ||
| 53 | | `bind.<keys>` | see below | Action for a key combo; `none` removes a default | | 54 | | `bind.<keys>` | see below | Action for a key combo; `none` removes a default | | ||
| 54 | 55 | ||||
M docs/performance.md +2 -0
| @@ -9,6 +9,8 @@ How NeferWL keeps its CPU and memory use low, how to measure it, and the referen | |||
| 9 | 9 | - **Cursor rides on game frames under VRR.** While a fullscreen game drives VRR, a cursor move waits for the game's next frame instead of its own commit: a cursor-only commit refreshes the panel at its slowest rate (~21 ms at 48 Hz) and delays the next game frame by as much. If the game stops drawing, the cursor still commits alone at 24 Hz. | |
| 10 | 10 | - **Default GOGC.** The Go live heap is a few MB (client buffers live in shared and GPU memory), so the runtime default `GOGC=100` with a 256 MiB soft limit gives 2 collections on the tiled playback test where `GOGC=50` gave 13. `GOGC` and `GOMEMLIMIT` from the environment win. | |
| 11 | 11 | ||
| 12 | + | Output and input threads request real-time scheduling, and the Vulkan queue requests elevated priority. Both require CAP_SYS_NICE: `sudo setcap cap_sys_nice+ep /usr/bin/neferwl`. Without it NeferWL runs at normal priority. | |
| 13 | + | ||
| 12 | 14 | ## Guards | |
| 13 | 15 | ||
| 14 | 16 | `make perf-check` runs `testing.AllocsPerRun` guards that fail when a hot path starts allocating: tiled SHM scene walk, tiled commit publication, headless tiled playback, steady-state `Render`, DRM report and flip snapshots, frame lifecycle transitions, scanout/overlay frame decision, pacer bookkeeping, output message routing, surface update recycling and input-region traversal. Run it with `make check`. | |
| @@ -9,6 +9,8 @@ How NeferWL keeps its CPU and memory use low, how to measure it, and the referen | |||||
| 9 | - **Cursor rides on game frames under VRR.** While a fullscreen game drives VRR, a cursor move waits for the game's next frame instead of its own commit: a cursor-only commit refreshes the panel at its slowest rate (~21 ms at 48 Hz) and delays the next game frame by as much. If the game stops drawing, the cursor still commits alone at 24 Hz. | 9 | - **Cursor rides on game frames under VRR.** While a fullscreen game drives VRR, a cursor move waits for the game's next frame instead of its own commit: a cursor-only commit refreshes the panel at its slowest rate (~21 ms at 48 Hz) and delays the next game frame by as much. If the game stops drawing, the cursor still commits alone at 24 Hz. | ||
| 10 | - **Default GOGC.** The Go live heap is a few MB (client buffers live in shared and GPU memory), so the runtime default `GOGC=100` with a 256 MiB soft limit gives 2 collections on the tiled playback test where `GOGC=50` gave 13. `GOGC` and `GOMEMLIMIT` from the environment win. | 10 | - **Default GOGC.** The Go live heap is a few MB (client buffers live in shared and GPU memory), so the runtime default `GOGC=100` with a 256 MiB soft limit gives 2 collections on the tiled playback test where `GOGC=50` gave 13. `GOGC` and `GOMEMLIMIT` from the environment win. | ||
| 11 | 11 | ||||
| 12 | + | Output and input threads request real-time scheduling, and the Vulkan queue requests elevated priority. Both require CAP_SYS_NICE: `sudo setcap cap_sys_nice+ep /usr/bin/neferwl`. Without it NeferWL runs at normal priority. | |||
| 13 | + | ||||
| 12 | ## Guards | 14 | ## Guards | ||
| 13 | 15 | ||||
| 14 | `make perf-check` runs `testing.AllocsPerRun` guards that fail when a hot path starts allocating: tiled SHM scene walk, tiled commit publication, headless tiled playback, steady-state `Render`, DRM report and flip snapshots, frame lifecycle transitions, scanout/overlay frame decision, pacer bookkeeping, output message routing, surface update recycling and input-region traversal. Run it with `make check`. | 16 | `make perf-check` runs `testing.AllocsPerRun` guards that fail when a hot path starts allocating: tiled SHM scene walk, tiled commit publication, headless tiled playback, steady-state `Render`, DRM report and flip snapshots, frame lifecycle transitions, scanout/overlay frame decision, pacer bookkeeping, output message routing, surface update recycling and input-region traversal. Run it with `make check`. | ||
M examples/config +3 -0
| @@ -91,6 +91,9 @@ render.direct-scanout = on | |||
| 91 | 91 | render.tearing = on | |
| 92 | 92 | render.vrr = on | |
| 93 | 93 | ||
| 94 | + | # Run output and input threads with SCHED_RR when permitted. | |
| 95 | + | performance.realtime = on | |
| 96 | + | ||
| 94 | 97 | # log.level: debug, info, warn or error. log.debug: components or all. | |
| 95 | 98 | log.level = info | |
| 96 | 99 | log.debug = | |
| @@ -91,6 +91,9 @@ render.direct-scanout = on | |||||
| 91 | render.tearing = on | 91 | render.tearing = on | ||
| 92 | render.vrr = on | 92 | render.vrr = on | ||
| 93 | 93 | ||||
| 94 | + | # Run output and input threads with SCHED_RR when permitted. | |||
| 95 | + | performance.realtime = on | |||
| 96 | + | ||||
| 94 | # log.level: debug, info, warn or error. log.debug: components or all. | 97 | # log.level: debug, info, warn or error. log.debug: components or all. | ||
| 95 | log.level = info | 98 | log.level = info | ||
| 96 | log.debug = | 99 | log.debug = | ||
M go.mod +1 -1
| @@ -5,7 +5,7 @@ go 1.27 | |||
| 5 | 5 | require ( | |
| 6 | 6 | github.com/bnema/purego v0.13.0-bnema.1 | |
| 7 | 7 | github.com/bnema/purego-libwayland v0.5.0 | |
| 8 | - | github.com/bnema/purego-vulkan v0.5.0 | |
| 8 | + | github.com/bnema/purego-vulkan v0.6.0 | |
| 9 | 9 | github.com/bnema/wlturbo v0.2.0 | |
| 10 | 10 | github.com/bnema/zerowrap v1.4.1 | |
| 11 | 11 | github.com/rs/zerolog v1.35.1 | |
| @@ -5,7 +5,7 @@ go 1.27 | |||||
| 5 | require ( | 5 | require ( | ||
| 6 | github.com/bnema/purego v0.13.0-bnema.1 | 6 | github.com/bnema/purego v0.13.0-bnema.1 | ||
| 7 | github.com/bnema/purego-libwayland v0.5.0 | 7 | github.com/bnema/purego-libwayland v0.5.0 | ||
| 8 | - | github.com/bnema/purego-vulkan v0.5.0 | 8 | + | github.com/bnema/purego-vulkan v0.6.0 |
| 9 | github.com/bnema/wlturbo v0.2.0 | 9 | github.com/bnema/wlturbo v0.2.0 | ||
| 10 | github.com/bnema/zerowrap v1.4.1 | 10 | github.com/bnema/zerowrap v1.4.1 | ||
| 11 | github.com/rs/zerolog v1.35.1 | 11 | github.com/rs/zerolog v1.35.1 | ||
M go.sum +2 -2 generated file: collapsed
M internal/adapters/config/config.go +7 -0
| @@ -135,6 +135,7 @@ func Defaults() ports.Config { | |||
| 135 | 135 | c.Render.DirectScanout = true | |
| 136 | 136 | c.Render.Tearing = true | |
| 137 | 137 | c.Render.VRR = true | |
| 138 | + | c.Performance.Realtime = true | |
| 138 | 139 | c.Log.Level = "info" | |
| 139 | 140 | c.Log.Debug = []string{} | |
| 140 | 141 | return c | |
| @@ -576,6 +577,12 @@ func set(c *ports.Config, key, v string) error { | |||
| 576 | 577 | return err | |
| 577 | 578 | } | |
| 578 | 579 | c.Render.VRR = b | |
| 580 | + | case "performance.realtime": | |
| 581 | + | b, err := onOff(v) | |
| 582 | + | if err != nil { | |
| 583 | + | return err | |
| 584 | + | } | |
| 585 | + | c.Performance.Realtime = b | |
| 579 | 586 | case "log.level": | |
| 580 | 587 | if v != "debug" && v != "info" && v != "warn" && v != "error" { | |
| 581 | 588 | return fmt.Errorf("must be debug, info, warn or error") | |
| @@ -135,6 +135,7 @@ func Defaults() ports.Config { | |||||
| 135 | c.Render.DirectScanout = true | 135 | c.Render.DirectScanout = true | ||
| 136 | c.Render.Tearing = true | 136 | c.Render.Tearing = true | ||
| 137 | c.Render.VRR = true | 137 | c.Render.VRR = true | ||
| 138 | + | c.Performance.Realtime = true | |||
| 138 | c.Log.Level = "info" | 139 | c.Log.Level = "info" | ||
| 139 | c.Log.Debug = []string{} | 140 | c.Log.Debug = []string{} | ||
| 140 | return c | 141 | return c | ||
| @@ -576,6 +577,12 @@ func set(c *ports.Config, key, v string) error { | |||||
| 576 | return err | 577 | return err | ||
| 577 | } | 578 | } | ||
| 578 | c.Render.VRR = b | 579 | c.Render.VRR = b | ||
| 580 | + | case "performance.realtime": | |||
| 581 | + | b, err := onOff(v) | |||
| 582 | + | if err != nil { | |||
| 583 | + | return err | |||
| 584 | + | } | |||
| 585 | + | c.Performance.Realtime = b | |||
| 579 | case "log.level": | 586 | case "log.level": | ||
| 580 | if v != "debug" && v != "info" && v != "warn" && v != "error" { | 587 | if v != "debug" && v != "info" && v != "warn" && v != "error" { | ||
| 581 | return fmt.Errorf("must be debug, info, warn or error") | 588 | return fmt.Errorf("must be debug, info, warn or error") | ||
M internal/adapters/config/config_test.go +14 -0
| @@ -48,6 +48,20 @@ func TestDefaultsAndLoad(t *testing.T) { | |||
| 48 | 48 | } | |
| 49 | 49 | } | |
| 50 | 50 | ||
| 51 | + | func TestRealtime(t *testing.T) { | |
| 52 | + | if !Defaults().Performance.Realtime { | |
| 53 | + | t.Fatal("realtime default disabled") | |
| 54 | + | } | |
| 55 | + | c, w := parseString(t, "performance.realtime = off\n") | |
| 56 | + | if c.Performance.Realtime || len(w) != 0 { | |
| 57 | + | t.Fatal(c.Performance, w) | |
| 58 | + | } | |
| 59 | + | c, w = parseString(t, "performance.realtime = invalid\n") | |
| 60 | + | if !c.Performance.Realtime || len(w) != 1 { | |
| 61 | + | t.Fatal(c.Performance, w) | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 51 | 65 | func TestFollowMove(t *testing.T) { | |
| 52 | 66 | c, w := parseString(t, "focus.follow-move = on\n") | |
| 53 | 67 | if !c.Focus.FollowMove || len(w) != 0 { | |
| @@ -48,6 +48,20 @@ func TestDefaultsAndLoad(t *testing.T) { | |||||
| 48 | } | 48 | } | ||
| 49 | } | 49 | } | ||
| 50 | 50 | ||||
| 51 | + | func TestRealtime(t *testing.T) { | |||
| 52 | + | if !Defaults().Performance.Realtime { | |||
| 53 | + | t.Fatal("realtime default disabled") | |||
| 54 | + | } | |||
| 55 | + | c, w := parseString(t, "performance.realtime = off\n") | |||
| 56 | + | if c.Performance.Realtime || len(w) != 0 { | |||
| 57 | + | t.Fatal(c.Performance, w) | |||
| 58 | + | } | |||
| 59 | + | c, w = parseString(t, "performance.realtime = invalid\n") | |||
| 60 | + | if !c.Performance.Realtime || len(w) != 1 { | |||
| 61 | + | t.Fatal(c.Performance, w) | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | ||||
| 51 | func TestFollowMove(t *testing.T) { | 65 | func TestFollowMove(t *testing.T) { | ||
| 52 | c, w := parseString(t, "focus.follow-move = on\n") | 66 | c, w := parseString(t, "focus.follow-move = on\n") | ||
| 53 | if !c.Focus.FollowMove || len(w) != 0 { | 67 | if !c.Focus.FollowMove || len(w) != 0 { | ||
M internal/adapters/drm/frame_submit.go +2 -1
| @@ -26,10 +26,11 @@ func (o *Output) submitFrame(ctx context.Context, r ports.Renderer, scene ports. | |||
| 26 | 26 | // shows is what the frame puts on screen: presentation feedback of | |
| 27 | 27 | // windows it does not draw is discarded, not presented. | |
| 28 | 28 | f := pendingFrame{shows: o.shownBy(scene, seen)} | |
| 29 | + | o.wantContent(scene, surfaces) | |
| 29 | 30 | decision := o.decideFrame(scene, surfaces, len(requests) > 0) | |
| 30 | 31 | if decision.fb != 0 { | |
| 31 | 32 | c := decision.content | |
| 32 | - | if direct, err = o.commitScanout(decision.fb, c, pendingFrame{shows: o.directShownBy(c.ID, seen[c.ID])}); direct { | |
| 33 | + | if direct, err = o.commitScanoutRect(decision.fb, c, pendingFrame{shows: o.directShownBy(c.ID, seen[c.ID])}, decision.rect); direct { | |
| 33 | 34 | return true, err | |
| 34 | 35 | } | |
| 35 | 36 | decision = o.composeFrame(scene, surfaces) | |
| @@ -26,10 +26,11 @@ func (o *Output) submitFrame(ctx context.Context, r ports.Renderer, scene ports. | |||||
| 26 | // shows is what the frame puts on screen: presentation feedback of | 26 | // shows is what the frame puts on screen: presentation feedback of | ||
| 27 | // windows it does not draw is discarded, not presented. | 27 | // windows it does not draw is discarded, not presented. | ||
| 28 | f := pendingFrame{shows: o.shownBy(scene, seen)} | 28 | f := pendingFrame{shows: o.shownBy(scene, seen)} | ||
| 29 | + | o.wantContent(scene, surfaces) | |||
| 29 | decision := o.decideFrame(scene, surfaces, len(requests) > 0) | 30 | decision := o.decideFrame(scene, surfaces, len(requests) > 0) | ||
| 30 | if decision.fb != 0 { | 31 | if decision.fb != 0 { | ||
| 31 | c := decision.content | 32 | c := decision.content | ||
| 32 | - | if direct, err = o.commitScanout(decision.fb, c, pendingFrame{shows: o.directShownBy(c.ID, seen[c.ID])}); direct { | 33 | + | if direct, err = o.commitScanoutRect(decision.fb, c, pendingFrame{shows: o.directShownBy(c.ID, seen[c.ID])}, decision.rect); direct { |
| 33 | return true, err | 34 | return true, err | ||
| 34 | } | 35 | } | ||
| 35 | decision = o.composeFrame(scene, surfaces) | 36 | decision = o.composeFrame(scene, surfaces) | ||
A internal/adapters/drm/game_performance_test.go +103 -0
| @@ -0,0 +1,103 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | "github.com/bnema/zerowrap" | |
| 9 | + | "github.com/stretchr/testify/mock" | |
| 10 | + | "golang.org/x/sys/unix" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | func TestScaledScanoutRectAndCachedRefusal(t *testing.T) { | |
| 14 | + | o, _, commits := testOutput(t, unix.EINVAL) | |
| 15 | + | o.cursor = nil | |
| 16 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 17 | + | o.clientFBs[9] = &clientFB{fbID: 87} | |
| 18 | + | s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}} | |
| 19 | + | c := ports.SurfaceContent{ID: 1, Width: 100, Height: 50, LogicalW: 200, LogicalH: 100, Source: [4]float32{5, 2, 90, 46}, DMABuf: &ports.DMABuf{ID: 9, Format: fourccXRGB}} | |
| 20 | + | var log bytes.Buffer | |
| 21 | + | o.log = zerowrap.New(zerowrap.Config{Output: &log}) | |
| 22 | + | surfaces := map[ports.WindowID]ports.SurfaceContent{1: c} | |
| 23 | + | if fb, _ := o.scanoutFrame(s, surfaces); fb != 0 || o.reason != "scale_refused" { | |
| 24 | + | t.Fatalf("fb %d reason %s", fb, o.reason) | |
| 25 | + | } | |
| 26 | + | if len(*commits) != 1 || (*commits)[0].flags != atomicTestOnly { | |
| 27 | + | t.Fatalf("test commits %+v", *commits) | |
| 28 | + | } | |
| 29 | + | if v, _ := (*commits)[0].req.value(tPrimary, planeProps["SRC_X"]); v != 5<<16 { | |
| 30 | + | t.Fatalf("source x: %d", v) | |
| 31 | + | } | |
| 32 | + | if v, _ := (*commits)[0].req.value(tPrimary, planeProps["SRC_W"]); v != 90<<16 { | |
| 33 | + | t.Fatalf("source width: %d", v) | |
| 34 | + | } | |
| 35 | + | if v, _ := (*commits)[0].req.value(tPrimary, planeProps["CRTC_W"]); v != 200 { | |
| 36 | + | t.Fatalf("destination width: %d", v) | |
| 37 | + | } | |
| 38 | + | if fb, _ := o.scanoutFrame(s, surfaces); fb != 0 || len(*commits) != 1 { | |
| 39 | + | t.Fatal("refused geometry retried") | |
| 40 | + | } | |
| 41 | + | if !bytes.Contains(log.Bytes(), []byte("scale_refused")) { | |
| 42 | + | t.Fatalf("refusal not logged: %s", log.String()) | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func TestModesetInvalidatesScaleDecision(t *testing.T) { | |
| 47 | + | o, k, commits := testOutput(t) | |
| 48 | + | o.cursor = nil | |
| 49 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 50 | + | fb := &clientFB{fbID: 87} | |
| 51 | + | o.clientFBs[9] = fb | |
| 52 | + | s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}} | |
| 53 | + | c := ports.SurfaceContent{ID: 1, Width: 100, Height: 50, LogicalW: 200, LogicalH: 100, DMABuf: &ports.DMABuf{ID: 9, Format: fourccXRGB}} | |
| 54 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: c} | |
| 55 | + | if got, _ := o.scanoutFrame(s, contents); got != 87 || len(*commits) != 1 || !fb.scaleTestedOK { | |
| 56 | + | t.Fatalf("initial scale: %d, commits: %d", got, len(*commits)) | |
| 57 | + | } | |
| 58 | + | k.EXPECT().createBlob(mock.Anything).Return(uint32(77), nil).Once() | |
| 59 | + | if err := o.modeset(); err != nil { | |
| 60 | + | t.Fatal(err) | |
| 61 | + | } | |
| 62 | + | if fb.scaleTestedOK || fb.scaleRefused { | |
| 63 | + | t.Fatal("cached scale survived modeset") | |
| 64 | + | } | |
| 65 | + | if got, _ := o.scanoutFrame(s, contents); got != 87 || len(*commits) != 3 || (*commits)[2].flags != atomicTestOnly { | |
| 66 | + | t.Fatalf("scale not retested: fb %d commits %+v", got, *commits) | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | func TestContentTypeFrameAndReset(t *testing.T) { | |
| 71 | + | o, _, commits := testOutput(t) | |
| 72 | + | o.cursor = nil | |
| 73 | + | o.contentProp = 81 | |
| 74 | + | o.contentValues = [5]uint64{0, 1, 2, 3, 4} | |
| 75 | + | s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}} | |
| 76 | + | o.wantContent(s, map[ports.WindowID]ports.SurfaceContent{1: {ContentType: ports.ContentGame}}) | |
| 77 | + | for i := 0; i < 2; i++ { | |
| 78 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil { | |
| 79 | + | t.Fatal(err) | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | if v, ok := (*commits)[0].req.value(tConn, 81); !ok || v != 4 { | |
| 83 | + | t.Fatalf("game hint: %d %v", v, ok) | |
| 84 | + | } | |
| 85 | + | if _, ok := (*commits)[1].req.value(tConn, 81); ok { | |
| 86 | + | t.Fatal("hint repeated") | |
| 87 | + | } | |
| 88 | + | o.wantContent(ports.Scene{}, nil) | |
| 89 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil { | |
| 90 | + | t.Fatal(err) | |
| 91 | + | } | |
| 92 | + | if v, ok := (*commits)[2].req.value(tConn, 81); !ok || v != 0 { | |
| 93 | + | t.Fatalf("reset hint: %d %v", v, ok) | |
| 94 | + | } | |
| 95 | + | o.contentProp = 0 | |
| 96 | + | o.wantContent(s, map[ports.WindowID]ports.SurfaceContent{1: {ContentType: ports.ContentGame}}) | |
| 97 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil { | |
| 98 | + | t.Fatal(err) | |
| 99 | + | } | |
| 100 | + | if _, ok := (*commits)[3].req.value(tConn, 81); ok { | |
| 101 | + | t.Fatal("absent property set") | |
| 102 | + | } | |
| 103 | + | } | |
| @@ -0,0 +1,103 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | "github.com/bnema/zerowrap" | |||
| 9 | + | "github.com/stretchr/testify/mock" | |||
| 10 | + | "golang.org/x/sys/unix" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | func TestScaledScanoutRectAndCachedRefusal(t *testing.T) { | |||
| 14 | + | o, _, commits := testOutput(t, unix.EINVAL) | |||
| 15 | + | o.cursor = nil | |||
| 16 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 17 | + | o.clientFBs[9] = &clientFB{fbID: 87} | |||
| 18 | + | s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}} | |||
| 19 | + | c := ports.SurfaceContent{ID: 1, Width: 100, Height: 50, LogicalW: 200, LogicalH: 100, Source: [4]float32{5, 2, 90, 46}, DMABuf: &ports.DMABuf{ID: 9, Format: fourccXRGB}} | |||
| 20 | + | var log bytes.Buffer | |||
| 21 | + | o.log = zerowrap.New(zerowrap.Config{Output: &log}) | |||
| 22 | + | surfaces := map[ports.WindowID]ports.SurfaceContent{1: c} | |||
| 23 | + | if fb, _ := o.scanoutFrame(s, surfaces); fb != 0 || o.reason != "scale_refused" { | |||
| 24 | + | t.Fatalf("fb %d reason %s", fb, o.reason) | |||
| 25 | + | } | |||
| 26 | + | if len(*commits) != 1 || (*commits)[0].flags != atomicTestOnly { | |||
| 27 | + | t.Fatalf("test commits %+v", *commits) | |||
| 28 | + | } | |||
| 29 | + | if v, _ := (*commits)[0].req.value(tPrimary, planeProps["SRC_X"]); v != 5<<16 { | |||
| 30 | + | t.Fatalf("source x: %d", v) | |||
| 31 | + | } | |||
| 32 | + | if v, _ := (*commits)[0].req.value(tPrimary, planeProps["SRC_W"]); v != 90<<16 { | |||
| 33 | + | t.Fatalf("source width: %d", v) | |||
| 34 | + | } | |||
| 35 | + | if v, _ := (*commits)[0].req.value(tPrimary, planeProps["CRTC_W"]); v != 200 { | |||
| 36 | + | t.Fatalf("destination width: %d", v) | |||
| 37 | + | } | |||
| 38 | + | if fb, _ := o.scanoutFrame(s, surfaces); fb != 0 || len(*commits) != 1 { | |||
| 39 | + | t.Fatal("refused geometry retried") | |||
| 40 | + | } | |||
| 41 | + | if !bytes.Contains(log.Bytes(), []byte("scale_refused")) { | |||
| 42 | + | t.Fatalf("refusal not logged: %s", log.String()) | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func TestModesetInvalidatesScaleDecision(t *testing.T) { | |||
| 47 | + | o, k, commits := testOutput(t) | |||
| 48 | + | o.cursor = nil | |||
| 49 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 50 | + | fb := &clientFB{fbID: 87} | |||
| 51 | + | o.clientFBs[9] = fb | |||
| 52 | + | s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}} | |||
| 53 | + | c := ports.SurfaceContent{ID: 1, Width: 100, Height: 50, LogicalW: 200, LogicalH: 100, DMABuf: &ports.DMABuf{ID: 9, Format: fourccXRGB}} | |||
| 54 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: c} | |||
| 55 | + | if got, _ := o.scanoutFrame(s, contents); got != 87 || len(*commits) != 1 || !fb.scaleTestedOK { | |||
| 56 | + | t.Fatalf("initial scale: %d, commits: %d", got, len(*commits)) | |||
| 57 | + | } | |||
| 58 | + | k.EXPECT().createBlob(mock.Anything).Return(uint32(77), nil).Once() | |||
| 59 | + | if err := o.modeset(); err != nil { | |||
| 60 | + | t.Fatal(err) | |||
| 61 | + | } | |||
| 62 | + | if fb.scaleTestedOK || fb.scaleRefused { | |||
| 63 | + | t.Fatal("cached scale survived modeset") | |||
| 64 | + | } | |||
| 65 | + | if got, _ := o.scanoutFrame(s, contents); got != 87 || len(*commits) != 3 || (*commits)[2].flags != atomicTestOnly { | |||
| 66 | + | t.Fatalf("scale not retested: fb %d commits %+v", got, *commits) | |||
| 67 | + | } | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | func TestContentTypeFrameAndReset(t *testing.T) { | |||
| 71 | + | o, _, commits := testOutput(t) | |||
| 72 | + | o.cursor = nil | |||
| 73 | + | o.contentProp = 81 | |||
| 74 | + | o.contentValues = [5]uint64{0, 1, 2, 3, 4} | |||
| 75 | + | s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}}} | |||
| 76 | + | o.wantContent(s, map[ports.WindowID]ports.SurfaceContent{1: {ContentType: ports.ContentGame}}) | |||
| 77 | + | for i := 0; i < 2; i++ { | |||
| 78 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil { | |||
| 79 | + | t.Fatal(err) | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | if v, ok := (*commits)[0].req.value(tConn, 81); !ok || v != 4 { | |||
| 83 | + | t.Fatalf("game hint: %d %v", v, ok) | |||
| 84 | + | } | |||
| 85 | + | if _, ok := (*commits)[1].req.value(tConn, 81); ok { | |||
| 86 | + | t.Fatal("hint repeated") | |||
| 87 | + | } | |||
| 88 | + | o.wantContent(ports.Scene{}, nil) | |||
| 89 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil { | |||
| 90 | + | t.Fatal(err) | |||
| 91 | + | } | |||
| 92 | + | if v, ok := (*commits)[2].req.value(tConn, 81); !ok || v != 0 { | |||
| 93 | + | t.Fatalf("reset hint: %d %v", v, ok) | |||
| 94 | + | } | |||
| 95 | + | o.contentProp = 0 | |||
| 96 | + | o.wantContent(s, map[ports.WindowID]ports.SurfaceContent{1: {ContentType: ports.ContentGame}}) | |||
| 97 | + | if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil { | |||
| 98 | + | t.Fatal(err) | |||
| 99 | + | } | |||
| 100 | + | if _, ok := (*commits)[3].req.value(tConn, 81); ok { | |||
| 101 | + | t.Fatal("absent property set") | |||
| 102 | + | } | |||
| 103 | + | } | |||
M internal/adapters/drm/output.go +117 -16
| @@ -75,6 +75,7 @@ type Output struct { | |||
| 75 | 75 | scanout bool | |
| 76 | 76 | clientFBs map[uint64]*clientFB | |
| 77 | 77 | shown, queued uint64 | |
| 78 | + | planeRect planeRect | |
| 78 | 79 | reason string // why the last frame was composed ("" = scanout) | |
| 79 | 80 | unsent []ports.OutputPresented | |
| 80 | 81 | // Last immutable snapshot sent in a report. Never mutate it: wayland may | |
| @@ -96,6 +97,10 @@ type Output struct { | |||
| 96 | 97 | vrrProp uint32 | |
| 97 | 98 | vrrOn bool | |
| 98 | 99 | vrrGame bool | |
| 100 | + | contentProp uint32 | |
| 101 | + | contentValues [5]uint64 | |
| 102 | + | contentValue uint64 | |
| 103 | + | contentWanted uint64 | |
| 99 | 104 | composedSince time.Time | |
| 100 | 105 | // lastFrame is when the last frame was committed. cursorHeld: a cursor | |
| 101 | 106 | // move waits for the next frame (see cursorWaits). | |
| @@ -252,6 +257,9 @@ func (o *Output) readProps(fd, pipe int, taken map[uint32]bool, cursorSide int) | |||
| 252 | 257 | o.connCrtc = uint32(np["CRTC_ID"][0]) | |
| 253 | 258 | // The connector's property metadata is needed for enum/range capability. | |
| 254 | 259 | o.hdrProps = readConnectorHDRProps(fd, np) | |
| 260 | + | o.contentProp, o.contentValues = readContentTypeProp(fd, np) | |
| 261 | + | o.contentValue = np["content type"][1] | |
| 262 | + | o.contentWanted = o.contentValue | |
| 255 | 263 | if o.crtcProps["MODE_ID"] == 0 || o.crtcProps["ACTIVE"] == 0 || o.connCrtc == 0 { | |
| 256 | 264 | return errors.New("crtc or connector lacks atomic properties") | |
| 257 | 265 | } | |
| @@ -336,18 +344,19 @@ func (o *Output) Height() int { return int(o.mode.VDisplay) } | |||
| 336 | 344 | ||
| 337 | 345 | // primaryProps shows fb full screen on the primary plane. | |
| 338 | 346 | func (o *Output) primaryProps(req *atomicReq, fb uint32) { | |
| 347 | + | o.primaryRectProps(req, fb, fullPlaneRect(o.Width(), o.Height())) | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | func (o *Output) primaryRectProps(req *atomicReq, fb uint32, rect planeRect) { | |
| 339 | 351 | p := o.primary | |
| 340 | - | w, h := uint64(o.Width()), uint64(o.Height()) | |
| 341 | 352 | req.set(p.id, p.prop("FB_ID"), uint64(fb)) | |
| 342 | 353 | req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc)) | |
| 343 | - | req.set(p.id, p.prop("SRC_X"), 0) | |
| 344 | - | req.set(p.id, p.prop("SRC_Y"), 0) | |
| 345 | - | req.set(p.id, p.prop("SRC_W"), w<<16) | |
| 346 | - | req.set(p.id, p.prop("SRC_H"), h<<16) | |
| 347 | - | req.set(p.id, p.prop("CRTC_X"), 0) | |
| 348 | - | req.set(p.id, p.prop("CRTC_Y"), 0) | |
| 349 | - | req.set(p.id, p.prop("CRTC_W"), w) | |
| 350 | - | req.set(p.id, p.prop("CRTC_H"), h) | |
| 354 | + | for i, name := range [...]string{"SRC_X", "SRC_Y", "SRC_W", "SRC_H"} { | |
| 355 | + | req.set(p.id, p.prop(name), rect.src[i]) | |
| 356 | + | } | |
| 357 | + | for i, name := range [...]string{"CRTC_X", "CRTC_Y", "CRTC_W", "CRTC_H"} { | |
| 358 | + | req.set(p.id, p.prop(name), rect.crtc[i]) | |
| 359 | + | } | |
| 351 | 360 | } | |
| 352 | 361 | ||
| 353 | 362 | // modeset shows the front image with the output's mode, turning off every | |
| @@ -365,12 +374,18 @@ func (o *Output) modeset() error { | |||
| 365 | 374 | // Only now is nothing of ours pending or on screen: on failure the | |
| 366 | 375 | // previous buffers may still show. | |
| 367 | 376 | o.frame.resetAfterModeset() | |
| 377 | + | // Plane scaling capabilities may change after a modeset or VT resume. | |
| 378 | + | for _, fb := range o.clientFBs { | |
| 379 | + | fb.scaleTestedOK, fb.scaleRefused = false, false | |
| 380 | + | } | |
| 368 | 381 | o.shown, o.queued = 0, 0 | |
| 369 | 382 | if o.modeBlob != 0 { | |
| 370 | 383 | _ = o.k.destroyBlob(o.modeBlob) | |
| 371 | 384 | } | |
| 372 | 385 | o.modeBlob = blob | |
| 373 | 386 | o.vrrOn, o.vrrGame, o.overlayOn, o.off = false, false, 0, o.wantOff | |
| 387 | + | o.contentValue, o.contentWanted = o.contentValues[0], o.contentValues[0] | |
| 388 | + | o.planeRect = fullPlaneRect(o.Width(), o.Height()) | |
| 374 | 389 | if o.cursor != nil { | |
| 375 | 390 | o.cursor.applied = cursorState{} | |
| 376 | 391 | o.cursor.screen, o.cursor.flying = 0, false | |
| @@ -395,10 +410,12 @@ func (o *Output) powerOff() error { | |||
| 395 | 410 | req := &atomicReq{} | |
| 396 | 411 | req.set(o.crtc, o.crtcProps["ACTIVE"], 0) | |
| 397 | 412 | req.set(o.crtc, o.vrrProp, 0) | |
| 413 | + | req.set(o.conn.id, o.contentProp, o.contentValues[0]) | |
| 398 | 414 | if err := o.k.commit(req, atomicAllowModes, 0); err != nil { | |
| 399 | 415 | return fmt.Errorf("power off: %w", err) | |
| 400 | 416 | } | |
| 401 | 417 | o.off, o.vrrOn, o.vrrGame = true, false, false | |
| 418 | + | o.contentValue, o.contentWanted = o.contentValues[0], o.contentValues[0] | |
| 402 | 419 | o.log.Info().Str("connector", o.conn.name).Msg("power off") | |
| 403 | 420 | // An inactive output offers neither HDR nor direct scanout. | |
| 404 | 421 | o.sendFormats() | |
| @@ -481,6 +498,7 @@ func (o *Output) modesetReq(blob uint32, active bool) *atomicReq { | |||
| 481 | 498 | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob)) | |
| 482 | 499 | req.set(o.crtc, o.crtcProps["ACTIVE"], boolValue(active)) | |
| 483 | 500 | req.set(o.crtc, o.vrrProp, 0) | |
| 501 | + | req.set(o.conn.id, o.contentProp, o.contentValues[0]) | |
| 484 | 502 | req.set(o.conn.id, o.connCrtc, uint64(o.crtc)) | |
| 485 | 503 | o.hdrConnectorProps(req, o.hdrOn) | |
| 486 | 504 | o.primaryProps(req, o.fbs[1-o.back]) | |
| @@ -546,21 +564,59 @@ func (o *Output) wantVRR(game bool) bool { | |||
| 546 | 564 | // be a tiled window on the overlay plane. | |
| 547 | 565 | func (o *Output) stateVRR() bool { return o.wantVRR(o.vrrGame) } | |
| 548 | 566 | ||
| 567 | + | // contentProps changes the connector hint only when it differs from the applied value. | |
| 568 | + | func (o *Output) contentProps(req *atomicReq) { | |
| 569 | + | if o.contentProp != 0 && o.contentWanted != o.contentValue { | |
| 570 | + | req.set(o.conn.id, o.contentProp, o.contentWanted) | |
| 571 | + | } | |
| 572 | + | } | |
| 573 | + | ||
| 574 | + | // wantContent chooses a content hint for the only visible fullscreen window. | |
| 575 | + | func (o *Output) wantContent(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) { | |
| 576 | + | o.contentWanted = o.contentValues[0] | |
| 577 | + | var full *ports.SceneWindow | |
| 578 | + | for i := range s.Windows { | |
| 579 | + | w := &s.Windows[i] | |
| 580 | + | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |
| 581 | + | continue | |
| 582 | + | } | |
| 583 | + | if !w.Fullscreen || !covers(&s, w) || full != nil { | |
| 584 | + | return | |
| 585 | + | } | |
| 586 | + | full = w | |
| 587 | + | } | |
| 588 | + | if full == nil { | |
| 589 | + | return | |
| 590 | + | } | |
| 591 | + | switch surfaces[full.ID].ContentType { | |
| 592 | + | case ports.ContentPhoto: | |
| 593 | + | o.contentWanted = o.contentValues[2] | |
| 594 | + | case ports.ContentVideo: | |
| 595 | + | o.contentWanted = o.contentValues[3] | |
| 596 | + | case ports.ContentGame: | |
| 597 | + | o.contentWanted = o.contentValues[4] | |
| 598 | + | } | |
| 599 | + | } | |
| 600 | + | ||
| 549 | 601 | // commitFrame flips fb in one commit with the cursor and VRR. An async | |
| 550 | 602 | // commit carries only the primary plane (kernel rule): a frame that must | |
| 551 | 603 | // also move the cursor or change VRR flips at vblank. fence is the | |
| 552 | 604 | // frame's GPU fence (nil: none); the caller keeps and closes it. | |
| 553 | 605 | func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame) error { | |
| 554 | - | return o.commitWith(fb, fence, async, vrr, f, overlayWin{}) | |
| 606 | + | return o.commitWithRect(fb, fence, async, vrr, f, overlayWin{}, fullPlaneRect(o.Width(), o.Height())) | |
| 555 | 607 | } | |
| 556 | 608 | ||
| 557 | 609 | // commitWith is commitFrame with ov on the overlay plane (zero: off). | |
| 558 | 610 | func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin) error { | |
| 611 | + | return o.commitWithRect(fb, fence, async, vrr, f, ov, fullPlaneRect(o.Width(), o.Height())) | |
| 612 | + | } | |
| 613 | + | ||
| 614 | + | func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect) error { | |
| 559 | 615 | cur := cursorState{} | |
| 560 | 616 | if o.cursor != nil { | |
| 561 | 617 | cur = o.cursor.desired() | |
| 562 | 618 | } | |
| 563 | - | if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn) { | |
| 619 | + | if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn || rect != o.planeRect || o.contentWanted != o.contentValue) { | |
| 564 | 620 | async = false | |
| 565 | 621 | } | |
| 566 | 622 | req := &atomicReq{} | |
| @@ -569,7 +625,7 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||
| 569 | 625 | flags |= flipAsyncFlag | |
| 570 | 626 | req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(fb)) | |
| 571 | 627 | } else { | |
| 572 | - | o.primaryProps(req, fb) | |
| 628 | + | o.primaryRectProps(req, fb, rect) | |
| 573 | 629 | req.set(o.crtc, o.vrrProp, boolValue(vrr)) | |
| 574 | 630 | if o.cursor != nil { | |
| 575 | 631 | o.cursor.props(req, o.crtc, cur) | |
| @@ -578,19 +634,25 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||
| 578 | 634 | o.overlayProps(req, ov) | |
| 579 | 635 | } | |
| 580 | 636 | } | |
| 637 | + | o.contentProps(req) | |
| 581 | 638 | if fence != nil { | |
| 582 | 639 | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |
| 583 | 640 | } | |
| 584 | 641 | // The kernel takes its own reference on the fence. | |
| 585 | 642 | if err := o.k.commit(req, flags, o.frame.userData(userFrame)); err != nil { | |
| 586 | 643 | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | |
| 587 | - | return o.commitWith(fb, fence, false, vrr, f, ov) | |
| 644 | + | return o.commitWithRect(fb, fence, false, vrr, f, ov, rect) | |
| 645 | + | } | |
| 646 | + | if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) && o.contentRefused(fb, fence, vrr, cur, ov, rect) { | |
| 647 | + | o.log.Warn().Str("component", "drm").Err(err).Msg("content type refused; disabled") | |
| 648 | + | o.contentProp = 0 | |
| 649 | + | return o.commitWithRect(fb, fence, false, vrr, f, ov, rect) | |
| 588 | 650 | } | |
| 589 | 651 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | |
| 590 | 652 | // Retry without turning VRR on: if that passes, the driver | |
| 591 | 653 | // refuses VRR on this output. The overlay stays: the composed | |
| 592 | 654 | // image left its window out. | |
| 593 | - | if o.commitWith(fb, fence, false, false, f, ov) == nil { | |
| 655 | + | if o.commitWithRect(fb, fence, false, false, f, ov, rect) == nil { | |
| 594 | 656 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled") | |
| 595 | 657 | o.vrrProp = 0 | |
| 596 | 658 | return nil | |
| @@ -611,6 +673,8 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||
| 611 | 673 | if !async { | |
| 612 | 674 | o.overlayOn = ov.buf | |
| 613 | 675 | } | |
| 676 | + | o.contentValue = o.contentWanted | |
| 677 | + | o.planeRect = rect | |
| 614 | 678 | o.lastFrame, o.cursorHeld = time.Now(), false | |
| 615 | 679 | f.frame, f.async = true, async | |
| 616 | 680 | f.fences = dupFences(fence, ov.acquire) | |
| @@ -619,6 +683,23 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||
| 619 | 683 | return nil | |
| 620 | 684 | } | |
| 621 | 685 | ||
| 686 | + | // contentRefused tests the same frame without the connector hint before disabling it. | |
| 687 | + | func (o *Output) contentRefused(fb uint32, fence *os.File, vrr bool, cur cursorState, ov overlayWin, rect planeRect) bool { | |
| 688 | + | req := &atomicReq{} | |
| 689 | + | o.primaryRectProps(req, fb, rect) | |
| 690 | + | req.set(o.crtc, o.vrrProp, boolValue(vrr)) | |
| 691 | + | if o.cursor != nil { | |
| 692 | + | o.cursor.props(req, o.crtc, cur) | |
| 693 | + | } | |
| 694 | + | if ov.buf != 0 || o.overlayOn != 0 { | |
| 695 | + | o.overlayProps(req, ov) | |
| 696 | + | } | |
| 697 | + | if fence != nil { | |
| 698 | + | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |
| 699 | + | } | |
| 700 | + | return o.k.commit(req, atomicTestOnly, 0) == nil | |
| 701 | + | } | |
| 702 | + | ||
| 622 | 703 | // cursorRefused reports, after a commit carrying the cursor failed with | |
| 623 | 704 | // EINVAL, whether the cursor plane was the cause; then the cursor is off | |
| 624 | 705 | // and the commit can be retried without it. | |
| @@ -647,6 +728,7 @@ func (o *Output) commitState(vrr bool) error { | |||
| 647 | 728 | if vrr != o.vrrOn { | |
| 648 | 729 | req.set(o.crtc, o.vrrProp, boolValue(vrr)) | |
| 649 | 730 | } | |
| 731 | + | o.contentProps(req) | |
| 650 | 732 | if len(req.objs) == 0 { | |
| 651 | 733 | return nil | |
| 652 | 734 | } | |
| @@ -658,6 +740,20 @@ func (o *Output) commitState(vrr bool) error { | |||
| 658 | 740 | if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | |
| 659 | 741 | return o.commitState(vrr) | |
| 660 | 742 | } | |
| 743 | + | if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) { | |
| 744 | + | without := &atomicReq{} | |
| 745 | + | if o.cursor != nil && cur != o.cursor.applied { | |
| 746 | + | o.cursor.props(without, o.crtc, cur) | |
| 747 | + | } | |
| 748 | + | if vrr != o.vrrOn { | |
| 749 | + | without.set(o.crtc, o.vrrProp, boolValue(vrr)) | |
| 750 | + | } | |
| 751 | + | if len(without.objs) == 0 || o.k.commit(without, atomicTestOnly, 0) == nil { | |
| 752 | + | o.contentProp = 0 | |
| 753 | + | o.log.Warn().Str("component", "drm").Err(err).Msg("content type refused; disabled") | |
| 754 | + | return o.commitState(vrr) | |
| 755 | + | } | |
| 756 | + | } | |
| 661 | 757 | return err | |
| 662 | 758 | } | |
| 663 | 759 | if o.cursor != nil { | |
| @@ -667,6 +763,7 @@ func (o *Output) commitState(vrr bool) error { | |||
| 667 | 763 | o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr") | |
| 668 | 764 | } | |
| 669 | 765 | o.vrrOn = vrr | |
| 766 | + | o.contentValue = o.contentWanted | |
| 670 | 767 | o.frame.begin(pendingFrame{}, time.Now()) | |
| 671 | 768 | return nil | |
| 672 | 769 | } | |
| @@ -693,6 +790,10 @@ func boolValue(b bool) uint64 { | |||
| 693 | 790 | // commitScanout flips a client buffer. It reports false when KMS refused | |
| 694 | 791 | // the buffer: the frame must be composed. | |
| 695 | 792 | func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame) (bool, error) { | |
| 793 | + | return o.commitScanoutRect(fb, c, f, fullPlaneRect(o.Width(), o.Height())) | |
| 794 | + | } | |
| 795 | + | ||
| 796 | + | func (o *Output) commitScanoutRect(fb uint32, c ports.SurfaceContent, f pendingFrame, rect planeRect) (bool, error) { | |
| 696 | 797 | cfb := o.clientFBs[c.DMABuf.ID] | |
| 697 | 798 | // Drivers refuse async flips that change the format or modifier: the | |
| 698 | 799 | // flip from the composed image into scanout waits for vblank. | |
| @@ -706,13 +807,13 @@ func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame | |||
| 706 | 807 | if fence != nil { | |
| 707 | 808 | defer fence.Close() | |
| 708 | 809 | } | |
| 709 | - | err := o.commitFrame(fb, fence, async, vrr, f) | |
| 810 | + | err := o.commitWithRect(fb, fence, async, vrr, f, overlayWin{}, rect) | |
| 710 | 811 | if err != nil && async && errors.Is(err, unix.EINVAL) { | |
| 711 | 812 | // Refused (e.g. not a fast update): this buffer flips at vblank | |
| 712 | 813 | // from now on. | |
| 713 | 814 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | |
| 714 | 815 | cfb.noAsync = true | |
| 715 | - | err = o.commitFrame(fb, fence, false, vrr, f) | |
| 816 | + | err = o.commitWithRect(fb, fence, false, vrr, f, overlayWin{}, rect) | |
| 716 | 817 | } | |
| 717 | 818 | if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) { | |
| 718 | 819 | // KMS refuses this buffer on the plane: compose it instead. | |
| @@ -75,6 +75,7 @@ type Output struct { | |||||
| 75 | scanout bool | 75 | scanout bool | ||
| 76 | clientFBs map[uint64]*clientFB | 76 | clientFBs map[uint64]*clientFB | ||
| 77 | shown, queued uint64 | 77 | shown, queued uint64 | ||
| 78 | + | planeRect planeRect | |||
| 78 | reason string // why the last frame was composed ("" = scanout) | 79 | reason string // why the last frame was composed ("" = scanout) | ||
| 79 | unsent []ports.OutputPresented | 80 | unsent []ports.OutputPresented | ||
| 80 | // Last immutable snapshot sent in a report. Never mutate it: wayland may | 81 | // Last immutable snapshot sent in a report. Never mutate it: wayland may | ||
| @@ -96,6 +97,10 @@ type Output struct { | |||||
| 96 | vrrProp uint32 | 97 | vrrProp uint32 | ||
| 97 | vrrOn bool | 98 | vrrOn bool | ||
| 98 | vrrGame bool | 99 | vrrGame bool | ||
| 100 | + | contentProp uint32 | |||
| 101 | + | contentValues [5]uint64 | |||
| 102 | + | contentValue uint64 | |||
| 103 | + | contentWanted uint64 | |||
| 99 | composedSince time.Time | 104 | composedSince time.Time | ||
| 100 | // lastFrame is when the last frame was committed. cursorHeld: a cursor | 105 | // lastFrame is when the last frame was committed. cursorHeld: a cursor | ||
| 101 | // move waits for the next frame (see cursorWaits). | 106 | // move waits for the next frame (see cursorWaits). | ||
| @@ -252,6 +257,9 @@ func (o *Output) readProps(fd, pipe int, taken map[uint32]bool, cursorSide int) | |||||
| 252 | o.connCrtc = uint32(np["CRTC_ID"][0]) | 257 | o.connCrtc = uint32(np["CRTC_ID"][0]) | ||
| 253 | // The connector's property metadata is needed for enum/range capability. | 258 | // The connector's property metadata is needed for enum/range capability. | ||
| 254 | o.hdrProps = readConnectorHDRProps(fd, np) | 259 | o.hdrProps = readConnectorHDRProps(fd, np) | ||
| 260 | + | o.contentProp, o.contentValues = readContentTypeProp(fd, np) | |||
| 261 | + | o.contentValue = np["content type"][1] | |||
| 262 | + | o.contentWanted = o.contentValue | |||
| 255 | if o.crtcProps["MODE_ID"] == 0 || o.crtcProps["ACTIVE"] == 0 || o.connCrtc == 0 { | 263 | if o.crtcProps["MODE_ID"] == 0 || o.crtcProps["ACTIVE"] == 0 || o.connCrtc == 0 { | ||
| 256 | return errors.New("crtc or connector lacks atomic properties") | 264 | return errors.New("crtc or connector lacks atomic properties") | ||
| 257 | } | 265 | } | ||
| @@ -336,18 +344,19 @@ func (o *Output) Height() int { return int(o.mode.VDisplay) } | |||||
| 336 | 344 | ||||
| 337 | // primaryProps shows fb full screen on the primary plane. | 345 | // primaryProps shows fb full screen on the primary plane. | ||
| 338 | func (o *Output) primaryProps(req *atomicReq, fb uint32) { | 346 | func (o *Output) primaryProps(req *atomicReq, fb uint32) { | ||
| 347 | + | o.primaryRectProps(req, fb, fullPlaneRect(o.Width(), o.Height())) | |||
| 348 | + | } | |||
| 349 | + | ||||
| 350 | + | func (o *Output) primaryRectProps(req *atomicReq, fb uint32, rect planeRect) { | |||
| 339 | p := o.primary | 351 | p := o.primary | ||
| 340 | - | w, h := uint64(o.Width()), uint64(o.Height()) | |||
| 341 | req.set(p.id, p.prop("FB_ID"), uint64(fb)) | 352 | req.set(p.id, p.prop("FB_ID"), uint64(fb)) | ||
| 342 | req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc)) | 353 | req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc)) | ||
| 343 | - | req.set(p.id, p.prop("SRC_X"), 0) | 354 | + | for i, name := range [...]string{"SRC_X", "SRC_Y", "SRC_W", "SRC_H"} { |
| 344 | - | req.set(p.id, p.prop("SRC_Y"), 0) | 355 | + | req.set(p.id, p.prop(name), rect.src[i]) |
| 345 | - | req.set(p.id, p.prop("SRC_W"), w<<16) | 356 | + | } |
| 346 | - | req.set(p.id, p.prop("SRC_H"), h<<16) | 357 | + | for i, name := range [...]string{"CRTC_X", "CRTC_Y", "CRTC_W", "CRTC_H"} { |
| 347 | - | req.set(p.id, p.prop("CRTC_X"), 0) | 358 | + | req.set(p.id, p.prop(name), rect.crtc[i]) |
| 348 | - | req.set(p.id, p.prop("CRTC_Y"), 0) | 359 | + | } |
| 349 | - | req.set(p.id, p.prop("CRTC_W"), w) | |||
| 350 | - | req.set(p.id, p.prop("CRTC_H"), h) | |||
| 351 | } | 360 | } | ||
| 352 | 361 | ||||
| 353 | // modeset shows the front image with the output's mode, turning off every | 362 | // modeset shows the front image with the output's mode, turning off every | ||
| @@ -365,12 +374,18 @@ func (o *Output) modeset() error { | |||||
| 365 | // Only now is nothing of ours pending or on screen: on failure the | 374 | // Only now is nothing of ours pending or on screen: on failure the | ||
| 366 | // previous buffers may still show. | 375 | // previous buffers may still show. | ||
| 367 | o.frame.resetAfterModeset() | 376 | o.frame.resetAfterModeset() | ||
| 377 | + | // Plane scaling capabilities may change after a modeset or VT resume. | |||
| 378 | + | for _, fb := range o.clientFBs { | |||
| 379 | + | fb.scaleTestedOK, fb.scaleRefused = false, false | |||
| 380 | + | } | |||
| 368 | o.shown, o.queued = 0, 0 | 381 | o.shown, o.queued = 0, 0 | ||
| 369 | if o.modeBlob != 0 { | 382 | if o.modeBlob != 0 { | ||
| 370 | _ = o.k.destroyBlob(o.modeBlob) | 383 | _ = o.k.destroyBlob(o.modeBlob) | ||
| 371 | } | 384 | } | ||
| 372 | o.modeBlob = blob | 385 | o.modeBlob = blob | ||
| 373 | o.vrrOn, o.vrrGame, o.overlayOn, o.off = false, false, 0, o.wantOff | 386 | o.vrrOn, o.vrrGame, o.overlayOn, o.off = false, false, 0, o.wantOff | ||
| 387 | + | o.contentValue, o.contentWanted = o.contentValues[0], o.contentValues[0] | |||
| 388 | + | o.planeRect = fullPlaneRect(o.Width(), o.Height()) | |||
| 374 | if o.cursor != nil { | 389 | if o.cursor != nil { | ||
| 375 | o.cursor.applied = cursorState{} | 390 | o.cursor.applied = cursorState{} | ||
| 376 | o.cursor.screen, o.cursor.flying = 0, false | 391 | o.cursor.screen, o.cursor.flying = 0, false | ||
| @@ -395,10 +410,12 @@ func (o *Output) powerOff() error { | |||||
| 395 | req := &atomicReq{} | 410 | req := &atomicReq{} | ||
| 396 | req.set(o.crtc, o.crtcProps["ACTIVE"], 0) | 411 | req.set(o.crtc, o.crtcProps["ACTIVE"], 0) | ||
| 397 | req.set(o.crtc, o.vrrProp, 0) | 412 | req.set(o.crtc, o.vrrProp, 0) | ||
| 413 | + | req.set(o.conn.id, o.contentProp, o.contentValues[0]) | |||
| 398 | if err := o.k.commit(req, atomicAllowModes, 0); err != nil { | 414 | if err := o.k.commit(req, atomicAllowModes, 0); err != nil { | ||
| 399 | return fmt.Errorf("power off: %w", err) | 415 | return fmt.Errorf("power off: %w", err) | ||
| 400 | } | 416 | } | ||
| 401 | o.off, o.vrrOn, o.vrrGame = true, false, false | 417 | o.off, o.vrrOn, o.vrrGame = true, false, false | ||
| 418 | + | o.contentValue, o.contentWanted = o.contentValues[0], o.contentValues[0] | |||
| 402 | o.log.Info().Str("connector", o.conn.name).Msg("power off") | 419 | o.log.Info().Str("connector", o.conn.name).Msg("power off") | ||
| 403 | // An inactive output offers neither HDR nor direct scanout. | 420 | // An inactive output offers neither HDR nor direct scanout. | ||
| 404 | o.sendFormats() | 421 | o.sendFormats() | ||
| @@ -481,6 +498,7 @@ func (o *Output) modesetReq(blob uint32, active bool) *atomicReq { | |||||
| 481 | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob)) | 498 | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob)) | ||
| 482 | req.set(o.crtc, o.crtcProps["ACTIVE"], boolValue(active)) | 499 | req.set(o.crtc, o.crtcProps["ACTIVE"], boolValue(active)) | ||
| 483 | req.set(o.crtc, o.vrrProp, 0) | 500 | req.set(o.crtc, o.vrrProp, 0) | ||
| 501 | + | req.set(o.conn.id, o.contentProp, o.contentValues[0]) | |||
| 484 | req.set(o.conn.id, o.connCrtc, uint64(o.crtc)) | 502 | req.set(o.conn.id, o.connCrtc, uint64(o.crtc)) | ||
| 485 | o.hdrConnectorProps(req, o.hdrOn) | 503 | o.hdrConnectorProps(req, o.hdrOn) | ||
| 486 | o.primaryProps(req, o.fbs[1-o.back]) | 504 | o.primaryProps(req, o.fbs[1-o.back]) | ||
| @@ -546,21 +564,59 @@ func (o *Output) wantVRR(game bool) bool { | |||||
| 546 | // be a tiled window on the overlay plane. | 564 | // be a tiled window on the overlay plane. | ||
| 547 | func (o *Output) stateVRR() bool { return o.wantVRR(o.vrrGame) } | 565 | func (o *Output) stateVRR() bool { return o.wantVRR(o.vrrGame) } | ||
| 548 | 566 | ||||
| 567 | + | // contentProps changes the connector hint only when it differs from the applied value. | |||
| 568 | + | func (o *Output) contentProps(req *atomicReq) { | |||
| 569 | + | if o.contentProp != 0 && o.contentWanted != o.contentValue { | |||
| 570 | + | req.set(o.conn.id, o.contentProp, o.contentWanted) | |||
| 571 | + | } | |||
| 572 | + | } | |||
| 573 | + | ||||
| 574 | + | // wantContent chooses a content hint for the only visible fullscreen window. | |||
| 575 | + | func (o *Output) wantContent(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) { | |||
| 576 | + | o.contentWanted = o.contentValues[0] | |||
| 577 | + | var full *ports.SceneWindow | |||
| 578 | + | for i := range s.Windows { | |||
| 579 | + | w := &s.Windows[i] | |||
| 580 | + | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |||
| 581 | + | continue | |||
| 582 | + | } | |||
| 583 | + | if !w.Fullscreen || !covers(&s, w) || full != nil { | |||
| 584 | + | return | |||
| 585 | + | } | |||
| 586 | + | full = w | |||
| 587 | + | } | |||
| 588 | + | if full == nil { | |||
| 589 | + | return | |||
| 590 | + | } | |||
| 591 | + | switch surfaces[full.ID].ContentType { | |||
| 592 | + | case ports.ContentPhoto: | |||
| 593 | + | o.contentWanted = o.contentValues[2] | |||
| 594 | + | case ports.ContentVideo: | |||
| 595 | + | o.contentWanted = o.contentValues[3] | |||
| 596 | + | case ports.ContentGame: | |||
| 597 | + | o.contentWanted = o.contentValues[4] | |||
| 598 | + | } | |||
| 599 | + | } | |||
| 600 | + | ||||
| 549 | // commitFrame flips fb in one commit with the cursor and VRR. An async | 601 | // commitFrame flips fb in one commit with the cursor and VRR. An async | ||
| 550 | // commit carries only the primary plane (kernel rule): a frame that must | 602 | // commit carries only the primary plane (kernel rule): a frame that must | ||
| 551 | // also move the cursor or change VRR flips at vblank. fence is the | 603 | // also move the cursor or change VRR flips at vblank. fence is the | ||
| 552 | // frame's GPU fence (nil: none); the caller keeps and closes it. | 604 | // frame's GPU fence (nil: none); the caller keeps and closes it. | ||
| 553 | func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame) error { | 605 | func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame) error { | ||
| 554 | - | return o.commitWith(fb, fence, async, vrr, f, overlayWin{}) | 606 | + | return o.commitWithRect(fb, fence, async, vrr, f, overlayWin{}, fullPlaneRect(o.Width(), o.Height())) |
| 555 | } | 607 | } | ||
| 556 | 608 | ||||
| 557 | // commitWith is commitFrame with ov on the overlay plane (zero: off). | 609 | // commitWith is commitFrame with ov on the overlay plane (zero: off). | ||
| 558 | func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin) error { | 610 | func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin) error { | ||
| 611 | + | return o.commitWithRect(fb, fence, async, vrr, f, ov, fullPlaneRect(o.Width(), o.Height())) | |||
| 612 | + | } | |||
| 613 | + | ||||
| 614 | + | func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect) error { | |||
| 559 | cur := cursorState{} | 615 | cur := cursorState{} | ||
| 560 | if o.cursor != nil { | 616 | if o.cursor != nil { | ||
| 561 | cur = o.cursor.desired() | 617 | cur = o.cursor.desired() | ||
| 562 | } | 618 | } | ||
| 563 | - | if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn) { | 619 | + | if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn || rect != o.planeRect || o.contentWanted != o.contentValue) { |
| 564 | async = false | 620 | async = false | ||
| 565 | } | 621 | } | ||
| 566 | req := &atomicReq{} | 622 | req := &atomicReq{} | ||
| @@ -569,7 +625,7 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||||
| 569 | flags |= flipAsyncFlag | 625 | flags |= flipAsyncFlag | ||
| 570 | req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(fb)) | 626 | req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(fb)) | ||
| 571 | } else { | 627 | } else { | ||
| 572 | - | o.primaryProps(req, fb) | 628 | + | o.primaryRectProps(req, fb, rect) |
| 573 | req.set(o.crtc, o.vrrProp, boolValue(vrr)) | 629 | req.set(o.crtc, o.vrrProp, boolValue(vrr)) | ||
| 574 | if o.cursor != nil { | 630 | if o.cursor != nil { | ||
| 575 | o.cursor.props(req, o.crtc, cur) | 631 | o.cursor.props(req, o.crtc, cur) | ||
| @@ -578,19 +634,25 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||||
| 578 | o.overlayProps(req, ov) | 634 | o.overlayProps(req, ov) | ||
| 579 | } | 635 | } | ||
| 580 | } | 636 | } | ||
| 637 | + | o.contentProps(req) | |||
| 581 | if fence != nil { | 638 | if fence != nil { | ||
| 582 | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | 639 | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | ||
| 583 | } | 640 | } | ||
| 584 | // The kernel takes its own reference on the fence. | 641 | // The kernel takes its own reference on the fence. | ||
| 585 | if err := o.k.commit(req, flags, o.frame.userData(userFrame)); err != nil { | 642 | if err := o.k.commit(req, flags, o.frame.userData(userFrame)); err != nil { | ||
| 586 | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | 643 | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | ||
| 587 | - | return o.commitWith(fb, fence, false, vrr, f, ov) | 644 | + | return o.commitWithRect(fb, fence, false, vrr, f, ov, rect) |
| 645 | + | } | |||
| 646 | + | if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) && o.contentRefused(fb, fence, vrr, cur, ov, rect) { | |||
| 647 | + | o.log.Warn().Str("component", "drm").Err(err).Msg("content type refused; disabled") | |||
| 648 | + | o.contentProp = 0 | |||
| 649 | + | return o.commitWithRect(fb, fence, false, vrr, f, ov, rect) | |||
| 588 | } | 650 | } | ||
| 589 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | 651 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | ||
| 590 | // Retry without turning VRR on: if that passes, the driver | 652 | // Retry without turning VRR on: if that passes, the driver | ||
| 591 | // refuses VRR on this output. The overlay stays: the composed | 653 | // refuses VRR on this output. The overlay stays: the composed | ||
| 592 | // image left its window out. | 654 | // image left its window out. | ||
| 593 | - | if o.commitWith(fb, fence, false, false, f, ov) == nil { | 655 | + | if o.commitWithRect(fb, fence, false, false, f, ov, rect) == nil { |
| 594 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled") | 656 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled") | ||
| 595 | o.vrrProp = 0 | 657 | o.vrrProp = 0 | ||
| 596 | return nil | 658 | return nil | ||
| @@ -611,6 +673,8 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||||
| 611 | if !async { | 673 | if !async { | ||
| 612 | o.overlayOn = ov.buf | 674 | o.overlayOn = ov.buf | ||
| 613 | } | 675 | } | ||
| 676 | + | o.contentValue = o.contentWanted | |||
| 677 | + | o.planeRect = rect | |||
| 614 | o.lastFrame, o.cursorHeld = time.Now(), false | 678 | o.lastFrame, o.cursorHeld = time.Now(), false | ||
| 615 | f.frame, f.async = true, async | 679 | f.frame, f.async = true, async | ||
| 616 | f.fences = dupFences(fence, ov.acquire) | 680 | f.fences = dupFences(fence, ov.acquire) | ||
| @@ -619,6 +683,23 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||||
| 619 | return nil | 683 | return nil | ||
| 620 | } | 684 | } | ||
| 621 | 685 | ||||
| 686 | + | // contentRefused tests the same frame without the connector hint before disabling it. | |||
| 687 | + | func (o *Output) contentRefused(fb uint32, fence *os.File, vrr bool, cur cursorState, ov overlayWin, rect planeRect) bool { | |||
| 688 | + | req := &atomicReq{} | |||
| 689 | + | o.primaryRectProps(req, fb, rect) | |||
| 690 | + | req.set(o.crtc, o.vrrProp, boolValue(vrr)) | |||
| 691 | + | if o.cursor != nil { | |||
| 692 | + | o.cursor.props(req, o.crtc, cur) | |||
| 693 | + | } | |||
| 694 | + | if ov.buf != 0 || o.overlayOn != 0 { | |||
| 695 | + | o.overlayProps(req, ov) | |||
| 696 | + | } | |||
| 697 | + | if fence != nil { | |||
| 698 | + | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |||
| 699 | + | } | |||
| 700 | + | return o.k.commit(req, atomicTestOnly, 0) == nil | |||
| 701 | + | } | |||
| 702 | + | ||||
| 622 | // cursorRefused reports, after a commit carrying the cursor failed with | 703 | // cursorRefused reports, after a commit carrying the cursor failed with | ||
| 623 | // EINVAL, whether the cursor plane was the cause; then the cursor is off | 704 | // EINVAL, whether the cursor plane was the cause; then the cursor is off | ||
| 624 | // and the commit can be retried without it. | 705 | // and the commit can be retried without it. | ||
| @@ -647,6 +728,7 @@ func (o *Output) commitState(vrr bool) error { | |||||
| 647 | if vrr != o.vrrOn { | 728 | if vrr != o.vrrOn { | ||
| 648 | req.set(o.crtc, o.vrrProp, boolValue(vrr)) | 729 | req.set(o.crtc, o.vrrProp, boolValue(vrr)) | ||
| 649 | } | 730 | } | ||
| 731 | + | o.contentProps(req) | |||
| 650 | if len(req.objs) == 0 { | 732 | if len(req.objs) == 0 { | ||
| 651 | return nil | 733 | return nil | ||
| 652 | } | 734 | } | ||
| @@ -658,6 +740,20 @@ func (o *Output) commitState(vrr bool) error { | |||||
| 658 | if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | 740 | if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | ||
| 659 | return o.commitState(vrr) | 741 | return o.commitState(vrr) | ||
| 660 | } | 742 | } | ||
| 743 | + | if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) { | |||
| 744 | + | without := &atomicReq{} | |||
| 745 | + | if o.cursor != nil && cur != o.cursor.applied { | |||
| 746 | + | o.cursor.props(without, o.crtc, cur) | |||
| 747 | + | } | |||
| 748 | + | if vrr != o.vrrOn { | |||
| 749 | + | without.set(o.crtc, o.vrrProp, boolValue(vrr)) | |||
| 750 | + | } | |||
| 751 | + | if len(without.objs) == 0 || o.k.commit(without, atomicTestOnly, 0) == nil { | |||
| 752 | + | o.contentProp = 0 | |||
| 753 | + | o.log.Warn().Str("component", "drm").Err(err).Msg("content type refused; disabled") | |||
| 754 | + | return o.commitState(vrr) | |||
| 755 | + | } | |||
| 756 | + | } | |||
| 661 | return err | 757 | return err | ||
| 662 | } | 758 | } | ||
| 663 | if o.cursor != nil { | 759 | if o.cursor != nil { | ||
| @@ -667,6 +763,7 @@ func (o *Output) commitState(vrr bool) error { | |||||
| 667 | o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr") | 763 | o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr") | ||
| 668 | } | 764 | } | ||
| 669 | o.vrrOn = vrr | 765 | o.vrrOn = vrr | ||
| 766 | + | o.contentValue = o.contentWanted | |||
| 670 | o.frame.begin(pendingFrame{}, time.Now()) | 767 | o.frame.begin(pendingFrame{}, time.Now()) | ||
| 671 | return nil | 768 | return nil | ||
| 672 | } | 769 | } | ||
| @@ -693,6 +790,10 @@ func boolValue(b bool) uint64 { | |||||
| 693 | // commitScanout flips a client buffer. It reports false when KMS refused | 790 | // commitScanout flips a client buffer. It reports false when KMS refused | ||
| 694 | // the buffer: the frame must be composed. | 791 | // the buffer: the frame must be composed. | ||
| 695 | func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame) (bool, error) { | 792 | func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame) (bool, error) { | ||
| 793 | + | return o.commitScanoutRect(fb, c, f, fullPlaneRect(o.Width(), o.Height())) | |||
| 794 | + | } | |||
| 795 | + | ||||
| 796 | + | func (o *Output) commitScanoutRect(fb uint32, c ports.SurfaceContent, f pendingFrame, rect planeRect) (bool, error) { | |||
| 696 | cfb := o.clientFBs[c.DMABuf.ID] | 797 | cfb := o.clientFBs[c.DMABuf.ID] | ||
| 697 | // Drivers refuse async flips that change the format or modifier: the | 798 | // Drivers refuse async flips that change the format or modifier: the | ||
| 698 | // flip from the composed image into scanout waits for vblank. | 799 | // flip from the composed image into scanout waits for vblank. | ||
| @@ -706,13 +807,13 @@ func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame | |||||
| 706 | if fence != nil { | 807 | if fence != nil { | ||
| 707 | defer fence.Close() | 808 | defer fence.Close() | ||
| 708 | } | 809 | } | ||
| 709 | - | err := o.commitFrame(fb, fence, async, vrr, f) | 810 | + | err := o.commitWithRect(fb, fence, async, vrr, f, overlayWin{}, rect) |
| 710 | if err != nil && async && errors.Is(err, unix.EINVAL) { | 811 | if err != nil && async && errors.Is(err, unix.EINVAL) { | ||
| 711 | // Refused (e.g. not a fast update): this buffer flips at vblank | 812 | // Refused (e.g. not a fast update): this buffer flips at vblank | ||
| 712 | // from now on. | 813 | // from now on. | ||
| 713 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | 814 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | ||
| 714 | cfb.noAsync = true | 815 | cfb.noAsync = true | ||
| 715 | - | err = o.commitFrame(fb, fence, false, vrr, f) | 816 | + | err = o.commitWithRect(fb, fence, false, vrr, f, overlayWin{}, rect) |
| 716 | } | 817 | } | ||
| 717 | if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) { | 818 | if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) { | ||
| 718 | // KMS refuses this buffer on the plane: compose it instead. | 819 | // KMS refuses this buffer on the plane: compose it instead. | ||
M internal/adapters/drm/output_test.go +1 -1
| @@ -67,7 +67,7 @@ func testOutputMu(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec, | |||
| 67 | 67 | cur.fbs = [2]uint32{90, 91} | |
| 68 | 68 | o := &Output{k: k, frame: frameLifecycle{serials: &atomic.Uint64{}}, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(), | |
| 69 | 69 | crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR, | |
| 70 | - | primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true} | |
| 70 | + | primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true, planeRect: fullPlaneRect(200, 100)} | |
| 71 | 71 | return o, k, &commits, commitMu | |
| 72 | 72 | } | |
| 73 | 73 | ||
| @@ -67,7 +67,7 @@ func testOutputMu(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec, | |||||
| 67 | cur.fbs = [2]uint32{90, 91} | 67 | cur.fbs = [2]uint32{90, 91} | ||
| 68 | o := &Output{k: k, frame: frameLifecycle{serials: &atomic.Uint64{}}, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(), | 68 | o := &Output{k: k, frame: frameLifecycle{serials: &atomic.Uint64{}}, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(), | ||
| 69 | crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR, | 69 | crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR, | ||
| 70 | - | primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true} | 70 | + | primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true, planeRect: fullPlaneRect(200, 100)} |
| 71 | return o, k, &commits, commitMu | 71 | return o, k, &commits, commitMu | ||
| 72 | } | 72 | } | ||
| 73 | 73 | ||||
M internal/adapters/drm/props.go +37 -0
| @@ -61,6 +61,43 @@ func objProps(fd int, obj, typ uint32) (map[string][2]uint64, error) { | |||
| 61 | 61 | return out, nil | |
| 62 | 62 | } | |
| 63 | 63 | ||
| 64 | + | // readContentTypeProp records the connector's content type enum values. | |
| 65 | + | func readContentTypeProp(fd int, props map[string][2]uint64) (uint32, [5]uint64) { | |
| 66 | + | id := uint32(props["content type"][0]) | |
| 67 | + | var values [5]uint64 | |
| 68 | + | if id == 0 { | |
| 69 | + | return 0, values | |
| 70 | + | } | |
| 71 | + | p := getProp{propID: id} | |
| 72 | + | if ioctl(fd, ioctlGetProp, unsafe.Pointer(&p)) != nil || p.countEn == 0 { | |
| 73 | + | return 0, values | |
| 74 | + | } | |
| 75 | + | enums := make([]propEnum, p.countEn) | |
| 76 | + | v := make([]uint64, max(p.countValues, 1)) | |
| 77 | + | p.enumBlobs = uint64(uintptr(unsafe.Pointer(&enums[0]))) | |
| 78 | + | p.values = uint64(uintptr(unsafe.Pointer(&v[0]))) | |
| 79 | + | if ioctl(fd, ioctlGetProp, unsafe.Pointer(&p)) != nil { | |
| 80 | + | return 0, values | |
| 81 | + | } | |
| 82 | + | for _, e := range enums[:min(int(p.countEn), len(enums))] { | |
| 83 | + | switch unix.ByteSliceToString(e.name[:]) { | |
| 84 | + | case "No Data": | |
| 85 | + | values[0] = e.value | |
| 86 | + | case "Graphics": | |
| 87 | + | values[1] = e.value | |
| 88 | + | case "Photo": | |
| 89 | + | values[2] = e.value | |
| 90 | + | case "Cinema": | |
| 91 | + | values[3] = e.value | |
| 92 | + | case "Game": | |
| 93 | + | values[4] = e.value | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | runtime.KeepAlive(enums) | |
| 97 | + | runtime.KeepAlive(v) | |
| 98 | + | return id, values | |
| 99 | + | } | |
| 100 | + | ||
| 64 | 101 | // connectorHDRProps retains connector property IDs, enum values and the | |
| 65 | 102 | // original max bpc for SDR modesets and restoration. | |
| 66 | 103 | type connectorHDRProps struct { | |
| @@ -61,6 +61,43 @@ func objProps(fd int, obj, typ uint32) (map[string][2]uint64, error) { | |||||
| 61 | return out, nil | 61 | return out, nil | ||
| 62 | } | 62 | } | ||
| 63 | 63 | ||||
| 64 | + | // readContentTypeProp records the connector's content type enum values. | |||
| 65 | + | func readContentTypeProp(fd int, props map[string][2]uint64) (uint32, [5]uint64) { | |||
| 66 | + | id := uint32(props["content type"][0]) | |||
| 67 | + | var values [5]uint64 | |||
| 68 | + | if id == 0 { | |||
| 69 | + | return 0, values | |||
| 70 | + | } | |||
| 71 | + | p := getProp{propID: id} | |||
| 72 | + | if ioctl(fd, ioctlGetProp, unsafe.Pointer(&p)) != nil || p.countEn == 0 { | |||
| 73 | + | return 0, values | |||
| 74 | + | } | |||
| 75 | + | enums := make([]propEnum, p.countEn) | |||
| 76 | + | v := make([]uint64, max(p.countValues, 1)) | |||
| 77 | + | p.enumBlobs = uint64(uintptr(unsafe.Pointer(&enums[0]))) | |||
| 78 | + | p.values = uint64(uintptr(unsafe.Pointer(&v[0]))) | |||
| 79 | + | if ioctl(fd, ioctlGetProp, unsafe.Pointer(&p)) != nil { | |||
| 80 | + | return 0, values | |||
| 81 | + | } | |||
| 82 | + | for _, e := range enums[:min(int(p.countEn), len(enums))] { | |||
| 83 | + | switch unix.ByteSliceToString(e.name[:]) { | |||
| 84 | + | case "No Data": | |||
| 85 | + | values[0] = e.value | |||
| 86 | + | case "Graphics": | |||
| 87 | + | values[1] = e.value | |||
| 88 | + | case "Photo": | |||
| 89 | + | values[2] = e.value | |||
| 90 | + | case "Cinema": | |||
| 91 | + | values[3] = e.value | |||
| 92 | + | case "Game": | |||
| 93 | + | values[4] = e.value | |||
| 94 | + | } | |||
| 95 | + | } | |||
| 96 | + | runtime.KeepAlive(enums) | |||
| 97 | + | runtime.KeepAlive(v) | |||
| 98 | + | return id, values | |||
| 99 | + | } | |||
| 100 | + | ||||
| 64 | // connectorHDRProps retains connector property IDs, enum values and the | 101 | // connectorHDRProps retains connector property IDs, enum values and the | ||
| 65 | // original max bpc for SDR modesets and restoration. | 102 | // original max bpc for SDR modesets and restoration. | ||
| 66 | type connectorHDRProps struct { | 103 | type connectorHDRProps struct { | ||
M internal/adapters/drm/scanout.go +63 -11
| @@ -2,6 +2,7 @@ package drm | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | + | "math" | |
| 5 | 6 | "os" | |
| 6 | 7 | "slices" | |
| 7 | 8 | "time" | |
| @@ -12,12 +13,11 @@ import ( | |||
| 12 | 13 | ) | |
| 13 | 14 | ||
| 14 | 15 | // Direct scanout (ADR 006): when one fullscreen window covers the output | |
| 15 | - | // with a GPU buffer of the output's size, the output flips to that buffer | |
| 16 | + | // with a GPU buffer whose source can fill the plane, the output flips to that buffer | |
| 16 | 17 | // instead of compositing it. The buffer becomes a KMS framebuffer once | |
| 17 | 18 | // (PRIME import + ADDFB2) and is cached by DMABuf ID while the client | |
| 18 | 19 | // keeps using it. KMS waits on the buffer's implicit fences before showing | |
| 19 | - | // it. Anything else on screen, a scale, or a buffer KMS refuses falls back | |
| 20 | - | // to composition. | |
| 20 | + | // it. Anything else on screen or a buffer KMS refuses falls back to composition. | |
| 21 | 21 | ||
| 22 | 22 | const ( | |
| 23 | 23 | ioctlGemClose = 0x40086409 | |
| @@ -43,6 +43,27 @@ type fbCmd2 struct { | |||
| 43 | 43 | modifiers [4]uint64 | |
| 44 | 44 | } | |
| 45 | 45 | ||
| 46 | + | // planeRect contains source coordinates in 16.16 and destination pixels. | |
| 47 | + | type planeRect struct { | |
| 48 | + | src [4]uint64 | |
| 49 | + | crtc [4]uint64 | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func fullPlaneRect(w, h int) planeRect { | |
| 53 | + | return planeRect{src: [4]uint64{0, 0, uint64(w) << 16, uint64(h) << 16}, crtc: [4]uint64{0, 0, uint64(w), uint64(h)}} | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | func scanoutRect(c ports.SurfaceContent, w, h int) planeRect { | |
| 57 | + | r := fullPlaneRect(w, h) | |
| 58 | + | r.src = [4]uint64{0, 0, uint64(c.Width) << 16, uint64(c.Height) << 16} | |
| 59 | + | if c.Source[2] != 0 { | |
| 60 | + | for i, value := range c.Source { | |
| 61 | + | r.src[i] = uint64(float64(value) * 65536) | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | return r | |
| 65 | + | } | |
| 66 | + | ||
| 46 | 67 | // covers reports whether win fills the output of scene s. | |
| 47 | 68 | func covers(s *ports.Scene, win *ports.SceneWindow) bool { | |
| 48 | 69 | return win.Rect.X == 0 && win.Rect.Y == 0 && win.Rect.W == s.OutputWidth && win.Rect.H == s.OutputHeight | |
| @@ -104,19 +125,28 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 104 | 125 | case c.Transform != 0: | |
| 105 | 126 | // Planes show buffers as they are: rotation is composed. | |
| 106 | 127 | return c, "buffer_transform" | |
| 107 | - | case c.Width != w || c.Height != h: | |
| 108 | - | return c, "size_mismatch" | |
| 109 | - | case cropped(c): | |
| 110 | - | // A viewport crop: the plane would show the whole buffer. | |
| 111 | - | return c, "source_crop" | |
| 112 | 128 | case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H: | |
| 113 | - | return c, "logical_mismatch" | |
| 129 | + | return c, "letterbox" | |
| 130 | + | case c.Width <= 0 || c.Height <= 0 || c.Source[2] < 0 || c.Source[3] < 0 || !finiteSource(c.Source): | |
| 131 | + | return c, "source_crop" | |
| 114 | 132 | case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH): | |
| 115 | 133 | return c, "geometry_crop" | |
| 116 | 134 | } | |
| 135 | + | if c.Source[2] != 0 && (c.Source[0] < 0 || c.Source[1] < 0 || c.Source[2] <= 0 || c.Source[3] <= 0 || c.Source[0]+c.Source[2] > float32(c.Width) || c.Source[1]+c.Source[3] > float32(c.Height)) { | |
| 136 | + | return c, "source_crop" | |
| 137 | + | } | |
| 117 | 138 | return c, "" | |
| 118 | 139 | } | |
| 119 | 140 | ||
| 141 | + | func finiteSource(src [4]float32) bool { | |
| 142 | + | for _, n := range src { | |
| 143 | + | if math.IsNaN(float64(n)) || math.IsInf(float64(n), 0) { | |
| 144 | + | return false | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | return true | |
| 148 | + | } | |
| 149 | + | ||
| 120 | 150 | // cropped reports whether a viewport source shows less than the buffer. | |
| 121 | 151 | func cropped(c ports.SurfaceContent) bool { | |
| 122 | 152 | return c.Source[2] != 0 && c.Source != [4]float32{0, 0, float32(c.Width), float32(c.Height)} | |
| @@ -173,6 +203,25 @@ func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]por | |||
| 173 | 203 | } | |
| 174 | 204 | if reason == "" { | |
| 175 | 205 | fb, reason = o.scanoutFB(c.DMABuf, time.Now()) | |
| 206 | + | if reason == "" { | |
| 207 | + | rect := scanoutRect(c, o.Width(), o.Height()) | |
| 208 | + | cfb := o.clientFBs[c.DMABuf.ID] | |
| 209 | + | if cfb.scaleRefused && cfb.scaleFailed == rect { | |
| 210 | + | reason = "scale_refused" | |
| 211 | + | } else if rect != fullPlaneRect(o.Width(), o.Height()) && (!cfb.scaleTestedOK || cfb.scaleTested != rect) { | |
| 212 | + | req := &atomicReq{} | |
| 213 | + | o.primaryRectProps(req, fb, rect) | |
| 214 | + | if err := o.k.commit(req, atomicTestOnly, 0); err != nil { | |
| 215 | + | cfb.scaleFailed = rect | |
| 216 | + | cfb.scaleRefused = true | |
| 217 | + | reason = "scale_refused" | |
| 218 | + | o.log.Info().Str("component", "render").Err(err).Str("reason", reason).Str("connector", o.conn.name).Msg("scanout scaling refused") | |
| 219 | + | } else { | |
| 220 | + | cfb.scaleTested = rect | |
| 221 | + | cfb.scaleTestedOK = true | |
| 222 | + | } | |
| 223 | + | } | |
| 224 | + | } | |
| 176 | 225 | } | |
| 177 | 226 | o.setScanoutReason(reason) | |
| 178 | 227 | if reason != "" { | |
| @@ -225,6 +274,7 @@ func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.S | |||
| 225 | 274 | type frameDecision struct { | |
| 226 | 275 | fb uint32 | |
| 227 | 276 | content ports.SurfaceContent | |
| 277 | + | rect planeRect | |
| 228 | 278 | overlay overlayWin | |
| 229 | 279 | composed ports.Scene | |
| 230 | 280 | } | |
| @@ -249,7 +299,7 @@ func (o *Output) decideFrame(s ports.Scene, surfaces map[ports.WindowID]ports.Su | |||
| 249 | 299 | } | |
| 250 | 300 | fb, c := o.scanoutFrame(s, surfaces) | |
| 251 | 301 | if fb != 0 { | |
| 252 | - | return frameDecision{fb: fb, content: c} | |
| 302 | + | return frameDecision{fb: fb, content: c, rect: scanoutRect(c, o.Width(), o.Height())} | |
| 253 | 303 | } | |
| 254 | 304 | return o.composeFrame(s, surfaces) | |
| 255 | 305 | } | |
| @@ -277,7 +327,9 @@ type clientFB struct { | |||
| 277 | 327 | // failed is the reason KMS refused the buffer; it is not retried. | |
| 278 | 328 | failed string | |
| 279 | 329 | // noAsync: KMS refused an async flip to it; it flips at vblank. | |
| 280 | - | noAsync bool | |
| 330 | + | noAsync bool | |
| 331 | + | scaleTested, scaleFailed planeRect | |
| 332 | + | scaleRefused, scaleTestedOK bool | |
| 281 | 333 | // overlayFailed is why the overlay plane refused it; kept apart from | |
| 282 | 334 | // failed, so a refusal on one plane does not bar the other. | |
| 283 | 335 | overlayFailed string | |
| @@ -2,6 +2,7 @@ package drm | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "errors" | 4 | "errors" | ||
| 5 | + | "math" | |||
| 5 | "os" | 6 | "os" | ||
| 6 | "slices" | 7 | "slices" | ||
| 7 | "time" | 8 | "time" | ||
| @@ -12,12 +13,11 @@ import ( | |||||
| 12 | ) | 13 | ) | ||
| 13 | 14 | ||||
| 14 | // Direct scanout (ADR 006): when one fullscreen window covers the output | 15 | // Direct scanout (ADR 006): when one fullscreen window covers the output | ||
| 15 | - | // with a GPU buffer of the output's size, the output flips to that buffer | 16 | + | // with a GPU buffer whose source can fill the plane, the output flips to that buffer |
| 16 | // instead of compositing it. The buffer becomes a KMS framebuffer once | 17 | // instead of compositing it. The buffer becomes a KMS framebuffer once | ||
| 17 | // (PRIME import + ADDFB2) and is cached by DMABuf ID while the client | 18 | // (PRIME import + ADDFB2) and is cached by DMABuf ID while the client | ||
| 18 | // keeps using it. KMS waits on the buffer's implicit fences before showing | 19 | // keeps using it. KMS waits on the buffer's implicit fences before showing | ||
| 19 | - | // it. Anything else on screen, a scale, or a buffer KMS refuses falls back | 20 | + | // it. Anything else on screen or a buffer KMS refuses falls back to composition. |
| 20 | - | // to composition. | |||
| 21 | 21 | ||||
| 22 | const ( | 22 | const ( | ||
| 23 | ioctlGemClose = 0x40086409 | 23 | ioctlGemClose = 0x40086409 | ||
| @@ -43,6 +43,27 @@ type fbCmd2 struct { | |||||
| 43 | modifiers [4]uint64 | 43 | modifiers [4]uint64 | ||
| 44 | } | 44 | } | ||
| 45 | 45 | ||||
| 46 | + | // planeRect contains source coordinates in 16.16 and destination pixels. | |||
| 47 | + | type planeRect struct { | |||
| 48 | + | src [4]uint64 | |||
| 49 | + | crtc [4]uint64 | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func fullPlaneRect(w, h int) planeRect { | |||
| 53 | + | return planeRect{src: [4]uint64{0, 0, uint64(w) << 16, uint64(h) << 16}, crtc: [4]uint64{0, 0, uint64(w), uint64(h)}} | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | func scanoutRect(c ports.SurfaceContent, w, h int) planeRect { | |||
| 57 | + | r := fullPlaneRect(w, h) | |||
| 58 | + | r.src = [4]uint64{0, 0, uint64(c.Width) << 16, uint64(c.Height) << 16} | |||
| 59 | + | if c.Source[2] != 0 { | |||
| 60 | + | for i, value := range c.Source { | |||
| 61 | + | r.src[i] = uint64(float64(value) * 65536) | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | return r | |||
| 65 | + | } | |||
| 66 | + | ||||
| 46 | // covers reports whether win fills the output of scene s. | 67 | // covers reports whether win fills the output of scene s. | ||
| 47 | func covers(s *ports.Scene, win *ports.SceneWindow) bool { | 68 | func covers(s *ports.Scene, win *ports.SceneWindow) bool { | ||
| 48 | return win.Rect.X == 0 && win.Rect.Y == 0 && win.Rect.W == s.OutputWidth && win.Rect.H == s.OutputHeight | 69 | return win.Rect.X == 0 && win.Rect.Y == 0 && win.Rect.W == s.OutputWidth && win.Rect.H == s.OutputHeight | ||
| @@ -104,19 +125,28 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 104 | case c.Transform != 0: | 125 | case c.Transform != 0: | ||
| 105 | // Planes show buffers as they are: rotation is composed. | 126 | // Planes show buffers as they are: rotation is composed. | ||
| 106 | return c, "buffer_transform" | 127 | return c, "buffer_transform" | ||
| 107 | - | case c.Width != w || c.Height != h: | |||
| 108 | - | return c, "size_mismatch" | |||
| 109 | - | case cropped(c): | |||
| 110 | - | // A viewport crop: the plane would show the whole buffer. | |||
| 111 | - | return c, "source_crop" | |||
| 112 | case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H: | 128 | case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H: | ||
| 113 | - | return c, "logical_mismatch" | 129 | + | return c, "letterbox" |
| 130 | + | case c.Width <= 0 || c.Height <= 0 || c.Source[2] < 0 || c.Source[3] < 0 || !finiteSource(c.Source): | |||
| 131 | + | return c, "source_crop" | |||
| 114 | case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH): | 132 | case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH): | ||
| 115 | return c, "geometry_crop" | 133 | return c, "geometry_crop" | ||
| 116 | } | 134 | } | ||
| 135 | + | if c.Source[2] != 0 && (c.Source[0] < 0 || c.Source[1] < 0 || c.Source[2] <= 0 || c.Source[3] <= 0 || c.Source[0]+c.Source[2] > float32(c.Width) || c.Source[1]+c.Source[3] > float32(c.Height)) { | |||
| 136 | + | return c, "source_crop" | |||
| 137 | + | } | |||
| 117 | return c, "" | 138 | return c, "" | ||
| 118 | } | 139 | } | ||
| 119 | 140 | ||||
| 141 | + | func finiteSource(src [4]float32) bool { | |||
| 142 | + | for _, n := range src { | |||
| 143 | + | if math.IsNaN(float64(n)) || math.IsInf(float64(n), 0) { | |||
| 144 | + | return false | |||
| 145 | + | } | |||
| 146 | + | } | |||
| 147 | + | return true | |||
| 148 | + | } | |||
| 149 | + | ||||
| 120 | // cropped reports whether a viewport source shows less than the buffer. | 150 | // cropped reports whether a viewport source shows less than the buffer. | ||
| 121 | func cropped(c ports.SurfaceContent) bool { | 151 | func cropped(c ports.SurfaceContent) bool { | ||
| 122 | return c.Source[2] != 0 && c.Source != [4]float32{0, 0, float32(c.Width), float32(c.Height)} | 152 | return c.Source[2] != 0 && c.Source != [4]float32{0, 0, float32(c.Width), float32(c.Height)} | ||
| @@ -173,6 +203,25 @@ func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]por | |||||
| 173 | } | 203 | } | ||
| 174 | if reason == "" { | 204 | if reason == "" { | ||
| 175 | fb, reason = o.scanoutFB(c.DMABuf, time.Now()) | 205 | fb, reason = o.scanoutFB(c.DMABuf, time.Now()) | ||
| 206 | + | if reason == "" { | |||
| 207 | + | rect := scanoutRect(c, o.Width(), o.Height()) | |||
| 208 | + | cfb := o.clientFBs[c.DMABuf.ID] | |||
| 209 | + | if cfb.scaleRefused && cfb.scaleFailed == rect { | |||
| 210 | + | reason = "scale_refused" | |||
| 211 | + | } else if rect != fullPlaneRect(o.Width(), o.Height()) && (!cfb.scaleTestedOK || cfb.scaleTested != rect) { | |||
| 212 | + | req := &atomicReq{} | |||
| 213 | + | o.primaryRectProps(req, fb, rect) | |||
| 214 | + | if err := o.k.commit(req, atomicTestOnly, 0); err != nil { | |||
| 215 | + | cfb.scaleFailed = rect | |||
| 216 | + | cfb.scaleRefused = true | |||
| 217 | + | reason = "scale_refused" | |||
| 218 | + | o.log.Info().Str("component", "render").Err(err).Str("reason", reason).Str("connector", o.conn.name).Msg("scanout scaling refused") | |||
| 219 | + | } else { | |||
| 220 | + | cfb.scaleTested = rect | |||
| 221 | + | cfb.scaleTestedOK = true | |||
| 222 | + | } | |||
| 223 | + | } | |||
| 224 | + | } | |||
| 176 | } | 225 | } | ||
| 177 | o.setScanoutReason(reason) | 226 | o.setScanoutReason(reason) | ||
| 178 | if reason != "" { | 227 | if reason != "" { | ||
| @@ -225,6 +274,7 @@ func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.S | |||||
| 225 | type frameDecision struct { | 274 | type frameDecision struct { | ||
| 226 | fb uint32 | 275 | fb uint32 | ||
| 227 | content ports.SurfaceContent | 276 | content ports.SurfaceContent | ||
| 277 | + | rect planeRect | |||
| 228 | overlay overlayWin | 278 | overlay overlayWin | ||
| 229 | composed ports.Scene | 279 | composed ports.Scene | ||
| 230 | } | 280 | } | ||
| @@ -249,7 +299,7 @@ func (o *Output) decideFrame(s ports.Scene, surfaces map[ports.WindowID]ports.Su | |||||
| 249 | } | 299 | } | ||
| 250 | fb, c := o.scanoutFrame(s, surfaces) | 300 | fb, c := o.scanoutFrame(s, surfaces) | ||
| 251 | if fb != 0 { | 301 | if fb != 0 { | ||
| 252 | - | return frameDecision{fb: fb, content: c} | 302 | + | return frameDecision{fb: fb, content: c, rect: scanoutRect(c, o.Width(), o.Height())} |
| 253 | } | 303 | } | ||
| 254 | return o.composeFrame(s, surfaces) | 304 | return o.composeFrame(s, surfaces) | ||
| 255 | } | 305 | } | ||
| @@ -277,7 +327,9 @@ type clientFB struct { | |||||
| 277 | // failed is the reason KMS refused the buffer; it is not retried. | 327 | // failed is the reason KMS refused the buffer; it is not retried. | ||
| 278 | failed string | 328 | failed string | ||
| 279 | // noAsync: KMS refused an async flip to it; it flips at vblank. | 329 | // noAsync: KMS refused an async flip to it; it flips at vblank. | ||
| 280 | - | noAsync bool | 330 | + | noAsync bool |
| 331 | + | scaleTested, scaleFailed planeRect | |||
| 332 | + | scaleRefused, scaleTestedOK bool | |||
| 281 | // overlayFailed is why the overlay plane refused it; kept apart from | 333 | // overlayFailed is why the overlay plane refused it; kept apart from | ||
| 282 | // failed, so a refusal on one plane does not bar the other. | 334 | // failed, so a refusal on one plane does not bar the other. | ||
| 283 | overlayFailed string | 335 | overlayFailed string | ||
M internal/adapters/drm/scanout_test.go +6 -6
| @@ -43,20 +43,20 @@ func TestScanoutCandidate(t *testing.T) { | |||
| 43 | 43 | {"shm", nil, func(c *ports.SurfaceContent) { c.DMABuf, c.SHM = nil, &ports.SHMBuffer{} }, "not_dmabuf"}, | |
| 44 | 44 | {"subsurface", nil, func(c *ports.SurfaceContent) { c.Children = []ports.Subsurface{{}} }, "subsurfaces"}, | |
| 45 | 45 | {"buffer transform", nil, func(c *ports.SurfaceContent) { c.Transform = 2 }, "buffer_transform"}, | |
| 46 | - | {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, "size_mismatch"}, | |
| 46 | + | {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, ""}, | |
| 47 | 47 | {"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"}, | |
| 48 | - | {"client ignores scale", nil, func(c *ports.SurfaceContent) { c.LogicalW = 200 }, "logical_mismatch"}, | |
| 48 | + | {"client ignores scale", nil, func(c *ports.SurfaceContent) { c.LogicalW = 200 }, "letterbox"}, | |
| 49 | 49 | {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""}, | |
| 50 | 50 | {"wine client subsurface", nil, wineTree(nil), ""}, | |
| 51 | 51 | {"client subsurface translucent", nil, wineTree(func(ch *ports.Subsurface) { ch.Opaque = false }), "subsurface_translucent"}, | |
| 52 | 52 | {"client subsurface shm", nil, wineTree(func(ch *ports.Subsurface) { ch.DMABuf, ch.SHM = nil, &ports.SHMBuffer{} }), "not_dmabuf"}, | |
| 53 | 53 | {"client subsurface offset", nil, wineTree(func(ch *ports.Subsurface) { ch.X = 4 }), "subsurface_offset"}, | |
| 54 | 54 | {"client subsurface below root", nil, wineTree(func(ch *ports.Subsurface) { ch.Below = true }), "subsurface_below"}, | |
| 55 | - | {"client subsurface cropped", nil, wineTree(func(ch *ports.Subsurface) { ch.Source = [4]float32{0, 0, 100, 50} }), "source_crop"}, | |
| 55 | + | {"client subsurface cropped", nil, wineTree(func(ch *ports.Subsurface) { ch.Source = [4]float32{0, 0, 100, 50} }), ""}, | |
| 56 | 56 | {"client subsurface full source", nil, wineTree(func(ch *ports.Subsurface) { ch.Source = [4]float32{0, 0, 200, 100} }), ""}, | |
| 57 | - | {"root cropped", nil, func(c *ports.SurfaceContent) { c.Source = [4]float32{10, 0, 190, 100} }, "source_crop"}, | |
| 58 | - | {"client subsurface scaled", nil, wineTree(func(ch *ports.Subsurface) { ch.Width = 100 }), "size_mismatch"}, | |
| 59 | - | {"client subsurface small", nil, wineTree(func(ch *ports.Subsurface) { ch.LogicalW = 90 }), "logical_mismatch"}, | |
| 57 | + | {"root cropped", nil, func(c *ports.SurfaceContent) { c.Source = [4]float32{10, 0, 190, 100} }, ""}, | |
| 58 | + | {"client subsurface scaled", nil, wineTree(func(ch *ports.Subsurface) { ch.Width = 100 }), ""}, | |
| 59 | + | {"client subsurface small", nil, wineTree(func(ch *ports.Subsurface) { ch.LogicalW = 90 }), "letterbox"}, | |
| 60 | 60 | {"two subsurfaces", nil, func(c *ports.SurfaceContent) { | |
| 61 | 61 | wineTree(nil)(c) | |
| 62 | 62 | c.Children = append(c.Children, c.Children[0]) | |
| @@ -43,20 +43,20 @@ func TestScanoutCandidate(t *testing.T) { | |||||
| 43 | {"shm", nil, func(c *ports.SurfaceContent) { c.DMABuf, c.SHM = nil, &ports.SHMBuffer{} }, "not_dmabuf"}, | 43 | {"shm", nil, func(c *ports.SurfaceContent) { c.DMABuf, c.SHM = nil, &ports.SHMBuffer{} }, "not_dmabuf"}, | ||
| 44 | {"subsurface", nil, func(c *ports.SurfaceContent) { c.Children = []ports.Subsurface{{}} }, "subsurfaces"}, | 44 | {"subsurface", nil, func(c *ports.SurfaceContent) { c.Children = []ports.Subsurface{{}} }, "subsurfaces"}, | ||
| 45 | {"buffer transform", nil, func(c *ports.SurfaceContent) { c.Transform = 2 }, "buffer_transform"}, | 45 | {"buffer transform", nil, func(c *ports.SurfaceContent) { c.Transform = 2 }, "buffer_transform"}, | ||
| 46 | - | {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, "size_mismatch"}, | 46 | + | {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, ""}, |
| 47 | {"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"}, | 47 | {"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"}, | ||
| 48 | - | {"client ignores scale", nil, func(c *ports.SurfaceContent) { c.LogicalW = 200 }, "logical_mismatch"}, | 48 | + | {"client ignores scale", nil, func(c *ports.SurfaceContent) { c.LogicalW = 200 }, "letterbox"}, |
| 49 | {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""}, | 49 | {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""}, | ||
| 50 | {"wine client subsurface", nil, wineTree(nil), ""}, | 50 | {"wine client subsurface", nil, wineTree(nil), ""}, | ||
| 51 | {"client subsurface translucent", nil, wineTree(func(ch *ports.Subsurface) { ch.Opaque = false }), "subsurface_translucent"}, | 51 | {"client subsurface translucent", nil, wineTree(func(ch *ports.Subsurface) { ch.Opaque = false }), "subsurface_translucent"}, | ||
| 52 | {"client subsurface shm", nil, wineTree(func(ch *ports.Subsurface) { ch.DMABuf, ch.SHM = nil, &ports.SHMBuffer{} }), "not_dmabuf"}, | 52 | {"client subsurface shm", nil, wineTree(func(ch *ports.Subsurface) { ch.DMABuf, ch.SHM = nil, &ports.SHMBuffer{} }), "not_dmabuf"}, | ||
| 53 | {"client subsurface offset", nil, wineTree(func(ch *ports.Subsurface) { ch.X = 4 }), "subsurface_offset"}, | 53 | {"client subsurface offset", nil, wineTree(func(ch *ports.Subsurface) { ch.X = 4 }), "subsurface_offset"}, | ||
| 54 | {"client subsurface below root", nil, wineTree(func(ch *ports.Subsurface) { ch.Below = true }), "subsurface_below"}, | 54 | {"client subsurface below root", nil, wineTree(func(ch *ports.Subsurface) { ch.Below = true }), "subsurface_below"}, | ||
| 55 | - | {"client subsurface cropped", nil, wineTree(func(ch *ports.Subsurface) { ch.Source = [4]float32{0, 0, 100, 50} }), "source_crop"}, | 55 | + | {"client subsurface cropped", nil, wineTree(func(ch *ports.Subsurface) { ch.Source = [4]float32{0, 0, 100, 50} }), ""}, |
| 56 | {"client subsurface full source", nil, wineTree(func(ch *ports.Subsurface) { ch.Source = [4]float32{0, 0, 200, 100} }), ""}, | 56 | {"client subsurface full source", nil, wineTree(func(ch *ports.Subsurface) { ch.Source = [4]float32{0, 0, 200, 100} }), ""}, | ||
| 57 | - | {"root cropped", nil, func(c *ports.SurfaceContent) { c.Source = [4]float32{10, 0, 190, 100} }, "source_crop"}, | 57 | + | {"root cropped", nil, func(c *ports.SurfaceContent) { c.Source = [4]float32{10, 0, 190, 100} }, ""}, |
| 58 | - | {"client subsurface scaled", nil, wineTree(func(ch *ports.Subsurface) { ch.Width = 100 }), "size_mismatch"}, | 58 | + | {"client subsurface scaled", nil, wineTree(func(ch *ports.Subsurface) { ch.Width = 100 }), ""}, |
| 59 | - | {"client subsurface small", nil, wineTree(func(ch *ports.Subsurface) { ch.LogicalW = 90 }), "logical_mismatch"}, | 59 | + | {"client subsurface small", nil, wineTree(func(ch *ports.Subsurface) { ch.LogicalW = 90 }), "letterbox"}, |
| 60 | {"two subsurfaces", nil, func(c *ports.SurfaceContent) { | 60 | {"two subsurfaces", nil, func(c *ports.SurfaceContent) { | ||
| 61 | wineTree(nil)(c) | 61 | wineTree(nil)(c) | ||
| 62 | c.Children = append(c.Children, c.Children[0]) | 62 | c.Children = append(c.Children, c.Children[0]) | ||
A internal/adapters/sched/sched.go +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | // Package sched configures latency-sensitive compositor threads. | |
| 2 | + | package sched | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "errors" | |
| 6 | + | "sync" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/zerowrap" | |
| 9 | + | "golang.org/x/sys/unix" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | var unavailable sync.Once | |
| 13 | + | ||
| 14 | + | // Realtime sets SCHED_RR at its minimum priority on the calling locked OS thread. | |
| 15 | + | // Children cannot inherit this scheduling policy. | |
| 16 | + | func Realtime(log zerowrap.Logger) error { | |
| 17 | + | err := unix.SchedSetAttr(0, &unix.SchedAttr{ | |
| 18 | + | Size: unix.SizeofSchedAttr, Policy: unix.SCHED_RR, | |
| 19 | + | Flags: unix.SCHED_FLAG_RESET_ON_FORK, Priority: 1, | |
| 20 | + | }, 0) | |
| 21 | + | if errors.Is(err, unix.EPERM) { | |
| 22 | + | unavailable.Do(func() { | |
| 23 | + | log.Info().Str("component", "sched").Msg("realtime scheduling unavailable, grant CAP_SYS_NICE") | |
| 24 | + | }) | |
| 25 | + | return nil | |
| 26 | + | } | |
| 27 | + | return err | |
| 28 | + | } | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | // Package sched configures latency-sensitive compositor threads. | |||
| 2 | + | package sched | |||
| 3 | + | ||||
| 4 | + | import ( | |||
| 5 | + | "errors" | |||
| 6 | + | "sync" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/zerowrap" | |||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | var unavailable sync.Once | |||
| 13 | + | ||||
| 14 | + | // Realtime sets SCHED_RR at its minimum priority on the calling locked OS thread. | |||
| 15 | + | // Children cannot inherit this scheduling policy. | |||
| 16 | + | func Realtime(log zerowrap.Logger) error { | |||
| 17 | + | err := unix.SchedSetAttr(0, &unix.SchedAttr{ | |||
| 18 | + | Size: unix.SizeofSchedAttr, Policy: unix.SCHED_RR, | |||
| 19 | + | Flags: unix.SCHED_FLAG_RESET_ON_FORK, Priority: 1, | |||
| 20 | + | }, 0) | |||
| 21 | + | if errors.Is(err, unix.EPERM) { | |||
| 22 | + | unavailable.Do(func() { | |||
| 23 | + | log.Info().Str("component", "sched").Msg("realtime scheduling unavailable, grant CAP_SYS_NICE") | |||
| 24 | + | }) | |||
| 25 | + | return nil | |||
| 26 | + | } | |||
| 27 | + | return err | |||
| 28 | + | } | |||
A internal/adapters/sched/sched_test.go +21 -0
| @@ -0,0 +1,21 @@ | |||
| 1 | + | package sched | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "runtime" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/neferwl/internal/logging" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestRealtime(t *testing.T) { | |
| 12 | + | result := make(chan error, 1) | |
| 13 | + | go func() { | |
| 14 | + | runtime.LockOSThread() | |
| 15 | + | defer runtime.UnlockOSThread() | |
| 16 | + | result <- Realtime(logging.For(context.Background(), "sched")) | |
| 17 | + | }() | |
| 18 | + | if err := <-result; err != nil { | |
| 19 | + | t.Fatal(err) | |
| 20 | + | } | |
| 21 | + | } | |
| @@ -0,0 +1,21 @@ | |||||
| 1 | + | package sched | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "runtime" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/neferwl/internal/logging" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func TestRealtime(t *testing.T) { | |||
| 12 | + | result := make(chan error, 1) | |||
| 13 | + | go func() { | |||
| 14 | + | runtime.LockOSThread() | |||
| 15 | + | defer runtime.UnlockOSThread() | |||
| 16 | + | result <- Realtime(logging.For(context.Background(), "sched")) | |||
| 17 | + | }() | |||
| 18 | + | if err := <-result; err != nil { | |||
| 19 | + | t.Fatal(err) | |||
| 20 | + | } | |||
| 21 | + | } | |||
M internal/adapters/vulkan/dmabuf.go +5 -10
| @@ -72,26 +72,21 @@ func vkFormat(f uint32) (vk.Format, bool, bool) { | |||
| 72 | 72 | return 0, false, false | |
| 73 | 73 | } | |
| 74 | 74 | ||
| 75 | - | // hasExtensions reports whether the device offers every dmabuf extension. | |
| 76 | - | func (r *Renderer) hasExtensions(physical vk.PhysicalDevice) bool { | |
| 75 | + | // extensions lists the device extensions supported by the selected GPU. | |
| 76 | + | func (r *Renderer) extensions(physical vk.PhysicalDevice) map[string]bool { | |
| 77 | 77 | var n uint32 | |
| 78 | 78 | if r.id.EnumerateDeviceExtensionProperties(physical, nil, &n, nil) != vk.Success || n == 0 { | |
| 79 | - | return false | |
| 79 | + | return nil | |
| 80 | 80 | } | |
| 81 | 81 | props := make([]vk.ExtensionProperties, n) | |
| 82 | 82 | if r.id.EnumerateDeviceExtensionProperties(physical, nil, &n, &props[0]) != vk.Success { | |
| 83 | - | return false | |
| 83 | + | return nil | |
| 84 | 84 | } | |
| 85 | 85 | have := map[string]bool{} | |
| 86 | 86 | for _, p := range props[:n] { | |
| 87 | 87 | have[cstring(p.ExtensionName[:])] = true | |
| 88 | 88 | } | |
| 89 | - | for _, e := range deviceExtensions { | |
| 90 | - | if !have[e] { | |
| 91 | - | return false | |
| 92 | - | } | |
| 93 | - | } | |
| 94 | - | return true | |
| 89 | + | return have | |
| 95 | 90 | } | |
| 96 | 91 | ||
| 97 | 92 | func cstring(b []byte) string { | |
| @@ -72,26 +72,21 @@ func vkFormat(f uint32) (vk.Format, bool, bool) { | |||||
| 72 | return 0, false, false | 72 | return 0, false, false | ||
| 73 | } | 73 | } | ||
| 74 | 74 | ||||
| 75 | - | // hasExtensions reports whether the device offers every dmabuf extension. | 75 | + | // extensions lists the device extensions supported by the selected GPU. |
| 76 | - | func (r *Renderer) hasExtensions(physical vk.PhysicalDevice) bool { | 76 | + | func (r *Renderer) extensions(physical vk.PhysicalDevice) map[string]bool { |
| 77 | var n uint32 | 77 | var n uint32 | ||
| 78 | if r.id.EnumerateDeviceExtensionProperties(physical, nil, &n, nil) != vk.Success || n == 0 { | 78 | if r.id.EnumerateDeviceExtensionProperties(physical, nil, &n, nil) != vk.Success || n == 0 { | ||
| 79 | - | return false | 79 | + | return nil |
| 80 | } | 80 | } | ||
| 81 | props := make([]vk.ExtensionProperties, n) | 81 | props := make([]vk.ExtensionProperties, n) | ||
| 82 | if r.id.EnumerateDeviceExtensionProperties(physical, nil, &n, &props[0]) != vk.Success { | 82 | if r.id.EnumerateDeviceExtensionProperties(physical, nil, &n, &props[0]) != vk.Success { | ||
| 83 | - | return false | 83 | + | return nil |
| 84 | } | 84 | } | ||
| 85 | have := map[string]bool{} | 85 | have := map[string]bool{} | ||
| 86 | for _, p := range props[:n] { | 86 | for _, p := range props[:n] { | ||
| 87 | have[cstring(p.ExtensionName[:])] = true | 87 | have[cstring(p.ExtensionName[:])] = true | ||
| 88 | } | 88 | } | ||
| 89 | - | for _, e := range deviceExtensions { | 89 | + | return have |
| 90 | - | if !have[e] { | |||
| 91 | - | return false | |||
| 92 | - | } | |||
| 93 | - | } | |||
| 94 | - | return true | |||
| 95 | } | 90 | } | ||
| 96 | 91 | ||||
| 97 | func cstring(b []byte) string { | 92 | func cstring(b []byte) string { | ||
A internal/adapters/vulkan/priority_test.go +17 -0
| @@ -0,0 +1,17 @@ | |||
| 1 | + | package vulkan | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | vk "github.com/bnema/purego-vulkan/vulkan" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestPriorityAttempts(t *testing.T) { | |
| 11 | + | if got := priorityAttempts(true); !slices.Equal(got, []vk.QueueGlobalPriority{vk.QueueGlobalPriorityRealtime, vk.QueueGlobalPriorityHigh, 0}) { | |
| 12 | + | t.Fatal(got) | |
| 13 | + | } | |
| 14 | + | if got := priorityAttempts(false); !slices.Equal(got, []vk.QueueGlobalPriority{0}) { | |
| 15 | + | t.Fatal(got) | |
| 16 | + | } | |
| 17 | + | } | |
| @@ -0,0 +1,17 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | vk "github.com/bnema/purego-vulkan/vulkan" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestPriorityAttempts(t *testing.T) { | |||
| 11 | + | if got := priorityAttempts(true); !slices.Equal(got, []vk.QueueGlobalPriority{vk.QueueGlobalPriorityRealtime, vk.QueueGlobalPriorityHigh, 0}) { | |||
| 12 | + | t.Fatal(got) | |||
| 13 | + | } | |||
| 14 | + | if got := priorityAttempts(false); !slices.Equal(got, []vk.QueueGlobalPriority{0}) { | |||
| 15 | + | t.Fatal(got) | |||
| 16 | + | } | |||
| 17 | + | } | |||
M internal/adapters/vulkan/renderer.go +69 -4
| @@ -17,6 +17,9 @@ import ( | |||
| 17 | 17 | // Renderer is owned by one goroutine; its methods must not be called concurrently. | |
| 18 | 18 | type Renderer struct { | |
| 19 | 19 | width, height int | |
| 20 | + | // queuePriority is the global priority the queue got: realtime, high | |
| 21 | + | // or default. | |
| 22 | + | queuePriority string | |
| 20 | 23 | instance vk.Instance | |
| 21 | 24 | id *vk.InstanceDispatch | |
| 22 | 25 | device vk.Device | |
| @@ -89,6 +92,14 @@ type Renderer struct { | |||
| 89 | 92 | frameStages []vk.PipelineStageFlags | |
| 90 | 93 | } | |
| 91 | 94 | ||
| 95 | + | // priorityAttempts falls back to an ordinary queue when elevated priority is unavailable. | |
| 96 | + | func priorityAttempts(supported bool) []vk.QueueGlobalPriority { | |
| 97 | + | if supported { | |
| 98 | + | return []vk.QueueGlobalPriority{vk.QueueGlobalPriorityRealtime, vk.QueueGlobalPriorityHigh, 0} | |
| 99 | + | } | |
| 100 | + | return []vk.QueueGlobalPriority{0} | |
| 101 | + | } | |
| 102 | + | ||
| 92 | 103 | func checked(name string, result vk.Result) error { | |
| 93 | 104 | if err := vk.Check(result); err != nil { | |
| 94 | 105 | return fmt.Errorf("%s: %w", name, err) | |
| @@ -185,7 +196,14 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 185 | 196 | // dmabuf import needs every extension; without them clients use wl_shm. | |
| 186 | 197 | var extNames [][]byte | |
| 187 | 198 | var extPtrs []*byte | |
| 188 | - | if r.hasExtensions(physical) { | |
| 199 | + | have := r.extensions(physical) | |
| 200 | + | dmabufSupported := true | |
| 201 | + | for _, e := range deviceExtensions { | |
| 202 | + | if !have[e] { | |
| 203 | + | dmabufSupported = false | |
| 204 | + | } | |
| 205 | + | } | |
| 206 | + | if dmabufSupported { | |
| 189 | 207 | for _, e := range deviceExtensions { | |
| 190 | 208 | extNames = append(extNames, append([]byte(e), 0)) | |
| 191 | 209 | extPtrs = append(extPtrs, &extNames[len(extNames)-1][0]) | |
| @@ -193,9 +211,52 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 193 | 211 | di.EnabledExtensionCount = uint32(len(extPtrs)) | |
| 194 | 212 | di.PpEnabledExtensionNames = &extPtrs[0] | |
| 195 | 213 | } | |
| 196 | - | if err = checked("vkCreateDevice", r.id.CreateDevice(physical, &di, nil, &r.device)); err != nil { | |
| 197 | - | return | |
| 214 | + | globalExt := "" | |
| 215 | + | if have[vk.KHRGlobalPriorityExtensionName] { | |
| 216 | + | globalExt = vk.KHRGlobalPriorityExtensionName | |
| 217 | + | } else if have[vk.EXTGlobalPriorityExtensionName] { | |
| 218 | + | globalExt = vk.EXTGlobalPriorityExtensionName | |
| 219 | + | } | |
| 220 | + | var global vk.DeviceQueueGlobalPriorityCreateInfo | |
| 221 | + | chosen := "default" | |
| 222 | + | for _, attempt := range priorityAttempts(globalExt != "") { | |
| 223 | + | qi.Next = nil | |
| 224 | + | count := len(extPtrs) | |
| 225 | + | if attempt != 0 { | |
| 226 | + | global = vk.DeviceQueueGlobalPriorityCreateInfo{SType: vk.StructureTypeDeviceQueueGlobalPriorityCreateInfo, GlobalPriority: attempt} | |
| 227 | + | qi.Next = unsafe.Pointer(&global) | |
| 228 | + | extNames = append(extNames, append([]byte(globalExt), 0)) | |
| 229 | + | extPtrs = append(extPtrs, &extNames[len(extNames)-1][0]) | |
| 230 | + | chosen = "realtime" | |
| 231 | + | if attempt == vk.QueueGlobalPriorityHigh { | |
| 232 | + | chosen = "high" | |
| 233 | + | } | |
| 234 | + | } else { | |
| 235 | + | chosen = "default" | |
| 236 | + | } | |
| 237 | + | di.EnabledExtensionCount = uint32(len(extPtrs)) | |
| 238 | + | if len(extPtrs) > 0 { | |
| 239 | + | di.PpEnabledExtensionNames = &extPtrs[0] | |
| 240 | + | } else { | |
| 241 | + | di.PpEnabledExtensionNames = nil | |
| 242 | + | } | |
| 243 | + | result := r.id.CreateDevice(physical, &di, nil, &r.device) | |
| 244 | + | runtime.KeepAlive(global) | |
| 245 | + | runtime.KeepAlive(qi) | |
| 246 | + | runtime.KeepAlive(extNames) | |
| 247 | + | runtime.KeepAlive(extPtrs) | |
| 248 | + | extPtrs = extPtrs[:count] | |
| 249 | + | extNames = extNames[:count] | |
| 250 | + | if result == vk.Success { | |
| 251 | + | err = nil | |
| 252 | + | break | |
| 253 | + | } | |
| 254 | + | err = checked("vkCreateDevice", result) | |
| 255 | + | if attempt == 0 { | |
| 256 | + | return | |
| 257 | + | } | |
| 198 | 258 | } | |
| 259 | + | r.queuePriority = chosen | |
| 199 | 260 | r.dd, err = vk.LoadDeviceDispatch(r.id, r.device) | |
| 200 | 261 | if err != nil { | |
| 201 | 262 | err = fmt.Errorf("LoadDeviceDispatch: %w", err) | |
| @@ -203,7 +264,7 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 203 | 264 | } | |
| 204 | 265 | runtime.KeepAlive(extNames) | |
| 205 | 266 | runtime.KeepAlive(extPtrs) | |
| 206 | - | if len(extPtrs) > 0 { | |
| 267 | + | if dmabufSupported { | |
| 207 | 268 | // Clients need the render node to allocate on the right GPU. | |
| 208 | 269 | if sup := r.probeDMABuf(physical); sup.Device != 0 { | |
| 209 | 270 | r.dmabuf = sup | |
| @@ -326,6 +387,10 @@ func (r *Renderer) findMemoryType(bits uint32, props vk.MemoryPropertyFlags) (ui | |||
| 326 | 387 | // It is diagnostic only and must be read by the renderer's owner goroutine. | |
| 327 | 388 | func (r *Renderer) CopiedBytes() int { return r.copied } | |
| 328 | 389 | ||
| 390 | + | // QueuePriority is the global priority the queue got: realtime, high or | |
| 391 | + | // default. | |
| 392 | + | func (r *Renderer) QueuePriority() string { return r.queuePriority } | |
| 393 | + | ||
| 329 | 394 | func (r *Renderer) Pixels() *image.RGBA { | |
| 330 | 395 | out := image.NewRGBA(image.Rect(0, 0, r.width, r.height)) | |
| 331 | 396 | if r.mapped == nil || r.readback() != nil { | |
| @@ -17,6 +17,9 @@ import ( | |||||
| 17 | // Renderer is owned by one goroutine; its methods must not be called concurrently. | 17 | // Renderer is owned by one goroutine; its methods must not be called concurrently. | ||
| 18 | type Renderer struct { | 18 | type Renderer struct { | ||
| 19 | width, height int | 19 | width, height int | ||
| 20 | + | // queuePriority is the global priority the queue got: realtime, high | |||
| 21 | + | // or default. | |||
| 22 | + | queuePriority string | |||
| 20 | instance vk.Instance | 23 | instance vk.Instance | ||
| 21 | id *vk.InstanceDispatch | 24 | id *vk.InstanceDispatch | ||
| 22 | device vk.Device | 25 | device vk.Device | ||
| @@ -89,6 +92,14 @@ type Renderer struct { | |||||
| 89 | frameStages []vk.PipelineStageFlags | 92 | frameStages []vk.PipelineStageFlags | ||
| 90 | } | 93 | } | ||
| 91 | 94 | ||||
| 95 | + | // priorityAttempts falls back to an ordinary queue when elevated priority is unavailable. | |||
| 96 | + | func priorityAttempts(supported bool) []vk.QueueGlobalPriority { | |||
| 97 | + | if supported { | |||
| 98 | + | return []vk.QueueGlobalPriority{vk.QueueGlobalPriorityRealtime, vk.QueueGlobalPriorityHigh, 0} | |||
| 99 | + | } | |||
| 100 | + | return []vk.QueueGlobalPriority{0} | |||
| 101 | + | } | |||
| 102 | + | ||||
| 92 | func checked(name string, result vk.Result) error { | 103 | func checked(name string, result vk.Result) error { | ||
| 93 | if err := vk.Check(result); err != nil { | 104 | if err := vk.Check(result); err != nil { | ||
| 94 | return fmt.Errorf("%s: %w", name, err) | 105 | return fmt.Errorf("%s: %w", name, err) | ||
| @@ -185,7 +196,14 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 185 | // dmabuf import needs every extension; without them clients use wl_shm. | 196 | // dmabuf import needs every extension; without them clients use wl_shm. | ||
| 186 | var extNames [][]byte | 197 | var extNames [][]byte | ||
| 187 | var extPtrs []*byte | 198 | var extPtrs []*byte | ||
| 188 | - | if r.hasExtensions(physical) { | 199 | + | have := r.extensions(physical) |
| 200 | + | dmabufSupported := true | |||
| 201 | + | for _, e := range deviceExtensions { | |||
| 202 | + | if !have[e] { | |||
| 203 | + | dmabufSupported = false | |||
| 204 | + | } | |||
| 205 | + | } | |||
| 206 | + | if dmabufSupported { | |||
| 189 | for _, e := range deviceExtensions { | 207 | for _, e := range deviceExtensions { | ||
| 190 | extNames = append(extNames, append([]byte(e), 0)) | 208 | extNames = append(extNames, append([]byte(e), 0)) | ||
| 191 | extPtrs = append(extPtrs, &extNames[len(extNames)-1][0]) | 209 | extPtrs = append(extPtrs, &extNames[len(extNames)-1][0]) | ||
| @@ -193,9 +211,52 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 193 | di.EnabledExtensionCount = uint32(len(extPtrs)) | 211 | di.EnabledExtensionCount = uint32(len(extPtrs)) | ||
| 194 | di.PpEnabledExtensionNames = &extPtrs[0] | 212 | di.PpEnabledExtensionNames = &extPtrs[0] | ||
| 195 | } | 213 | } | ||
| 196 | - | if err = checked("vkCreateDevice", r.id.CreateDevice(physical, &di, nil, &r.device)); err != nil { | 214 | + | globalExt := "" |
| 197 | - | return | 215 | + | if have[vk.KHRGlobalPriorityExtensionName] { |
| 216 | + | globalExt = vk.KHRGlobalPriorityExtensionName | |||
| 217 | + | } else if have[vk.EXTGlobalPriorityExtensionName] { | |||
| 218 | + | globalExt = vk.EXTGlobalPriorityExtensionName | |||
| 219 | + | } | |||
| 220 | + | var global vk.DeviceQueueGlobalPriorityCreateInfo | |||
| 221 | + | chosen := "default" | |||
| 222 | + | for _, attempt := range priorityAttempts(globalExt != "") { | |||
| 223 | + | qi.Next = nil | |||
| 224 | + | count := len(extPtrs) | |||
| 225 | + | if attempt != 0 { | |||
| 226 | + | global = vk.DeviceQueueGlobalPriorityCreateInfo{SType: vk.StructureTypeDeviceQueueGlobalPriorityCreateInfo, GlobalPriority: attempt} | |||
| 227 | + | qi.Next = unsafe.Pointer(&global) | |||
| 228 | + | extNames = append(extNames, append([]byte(globalExt), 0)) | |||
| 229 | + | extPtrs = append(extPtrs, &extNames[len(extNames)-1][0]) | |||
| 230 | + | chosen = "realtime" | |||
| 231 | + | if attempt == vk.QueueGlobalPriorityHigh { | |||
| 232 | + | chosen = "high" | |||
| 233 | + | } | |||
| 234 | + | } else { | |||
| 235 | + | chosen = "default" | |||
| 236 | + | } | |||
| 237 | + | di.EnabledExtensionCount = uint32(len(extPtrs)) | |||
| 238 | + | if len(extPtrs) > 0 { | |||
| 239 | + | di.PpEnabledExtensionNames = &extPtrs[0] | |||
| 240 | + | } else { | |||
| 241 | + | di.PpEnabledExtensionNames = nil | |||
| 242 | + | } | |||
| 243 | + | result := r.id.CreateDevice(physical, &di, nil, &r.device) | |||
| 244 | + | runtime.KeepAlive(global) | |||
| 245 | + | runtime.KeepAlive(qi) | |||
| 246 | + | runtime.KeepAlive(extNames) | |||
| 247 | + | runtime.KeepAlive(extPtrs) | |||
| 248 | + | extPtrs = extPtrs[:count] | |||
| 249 | + | extNames = extNames[:count] | |||
| 250 | + | if result == vk.Success { | |||
| 251 | + | err = nil | |||
| 252 | + | break | |||
| 253 | + | } | |||
| 254 | + | err = checked("vkCreateDevice", result) | |||
| 255 | + | if attempt == 0 { | |||
| 256 | + | return | |||
| 257 | + | } | |||
| 198 | } | 258 | } | ||
| 259 | + | r.queuePriority = chosen | |||
| 199 | r.dd, err = vk.LoadDeviceDispatch(r.id, r.device) | 260 | r.dd, err = vk.LoadDeviceDispatch(r.id, r.device) | ||
| 200 | if err != nil { | 261 | if err != nil { | ||
| 201 | err = fmt.Errorf("LoadDeviceDispatch: %w", err) | 262 | err = fmt.Errorf("LoadDeviceDispatch: %w", err) | ||
| @@ -203,7 +264,7 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 203 | } | 264 | } | ||
| 204 | runtime.KeepAlive(extNames) | 265 | runtime.KeepAlive(extNames) | ||
| 205 | runtime.KeepAlive(extPtrs) | 266 | runtime.KeepAlive(extPtrs) | ||
| 206 | - | if len(extPtrs) > 0 { | 267 | + | if dmabufSupported { |
| 207 | // Clients need the render node to allocate on the right GPU. | 268 | // Clients need the render node to allocate on the right GPU. | ||
| 208 | if sup := r.probeDMABuf(physical); sup.Device != 0 { | 269 | if sup := r.probeDMABuf(physical); sup.Device != 0 { | ||
| 209 | r.dmabuf = sup | 270 | r.dmabuf = sup | ||
| @@ -326,6 +387,10 @@ func (r *Renderer) findMemoryType(bits uint32, props vk.MemoryPropertyFlags) (ui | |||||
| 326 | // It is diagnostic only and must be read by the renderer's owner goroutine. | 387 | // It is diagnostic only and must be read by the renderer's owner goroutine. | ||
| 327 | func (r *Renderer) CopiedBytes() int { return r.copied } | 388 | func (r *Renderer) CopiedBytes() int { return r.copied } | ||
| 328 | 389 | ||||
| 390 | + | // QueuePriority is the global priority the queue got: realtime, high or | |||
| 391 | + | // default. | |||
| 392 | + | func (r *Renderer) QueuePriority() string { return r.queuePriority } | |||
| 393 | + | ||||
| 329 | func (r *Renderer) Pixels() *image.RGBA { | 394 | func (r *Renderer) Pixels() *image.RGBA { | ||
| 330 | out := image.NewRGBA(image.Rect(0, 0, r.width, r.height)) | 395 | out := image.NewRGBA(image.Rect(0, 0, r.width, r.height)) | ||
| 331 | if r.mapped == nil || r.readback() != nil { | 396 | if r.mapped == nil || r.readback() != nil { | ||
M internal/adapters/wayland/fifo.go +37 -1
| @@ -208,6 +208,7 @@ type update struct { | |||
| 208 | 208 | layout []childLayout | |
| 209 | 209 | deps []*update | |
| 210 | 210 | owner *surface | |
| 211 | + | implicit [4]*syncWait | |
| 211 | 212 | prev *update | |
| 212 | 213 | synced, bound bool | |
| 213 | 214 | xdg *xdgSurface | |
| @@ -344,6 +345,15 @@ func (s *surface) queueUpdate() { | |||
| 344 | 345 | *u = s.takePending() | |
| 345 | 346 | u.deps = deps | |
| 346 | 347 | u.owner, u.synced = s, s.effectivelySynced() | |
| 348 | + | if s.server.syncWait != nil && u.attached && u.buffer != nil && u.sync == nil { | |
| 349 | + | if b, ok := s.server.buffers[u.buffer.Resource].(*dmabufBuffer); ok { | |
| 350 | + | err := s.server.syncWait.watchImplicit(b.buf, &u.implicit) | |
| 351 | + | if err != nil { | |
| 352 | + | // Partial registrations were cancelled; do not block this update. | |
| 353 | + | s.server.log.Warn().Str("component", "wayland").Err(err).Msg("implicit buffer wait") | |
| 354 | + | } | |
| 355 | + | } | |
| 356 | + | } | |
| 347 | 357 | for _, ch := range s.sub.children { | |
| 348 | 358 | for i := len(ch.queue) - 1; i >= 0; i-- { | |
| 349 | 359 | candidate := ch.queue[i] | |
| @@ -376,6 +386,10 @@ func (s *surface) applyUpdate(u *update) { | |||
| 376 | 386 | } | |
| 377 | 387 | if u.buffer != nil && !u.buffer.Resource.Alive() { | |
| 378 | 388 | s.commitSkipped = true | |
| 389 | + | // No reader takes this buffer. A release point is safe to signal | |
| 390 | + | // only if the acquire has already signalled; dropSync owns that rule. | |
| 391 | + | s.dropSync(u.sync) | |
| 392 | + | u.sync = nil | |
| 379 | 393 | u.buffer, u.attached = nil, false | |
| 380 | 394 | // Scale, transform and crop describe the same retained buffer. | |
| 381 | 395 | u.scale = s.bufferScale | |
| @@ -407,6 +421,9 @@ func (s *surface) dropQueue() { | |||
| 407 | 421 | discard(fb) | |
| 408 | 422 | } | |
| 409 | 423 | s.dropSync(u.sync) | |
| 424 | + | for _, w := range u.implicit { | |
| 425 | + | s.server.syncWait.cancel(w) | |
| 426 | + | } | |
| 410 | 427 | if u.sync != nil { | |
| 411 | 428 | continue // explicit sync: the release point replaced release | |
| 412 | 429 | } | |
| @@ -582,9 +599,28 @@ func (u *update) graphReady(now time.Time) bool { | |||
| 582 | 599 | if u.prev != nil && !u.prev.graphReady(now) { | |
| 583 | 600 | return false | |
| 584 | 601 | } | |
| 585 | - | if u.wait && s.barrier || !s.syncReady(u.sync) || s.tooEarly(u.at, now) { | |
| 602 | + | if u.wait && s.barrier || s.tooEarly(u.at, now) { | |
| 586 | 603 | return false | |
| 587 | 604 | } | |
| 605 | + | // A dead attachment is skipped by applyUpdate; waiting for its fence | |
| 606 | + | // would leave this update and every later one stuck behind it. | |
| 607 | + | if u.buffer != nil && !u.buffer.Resource.Alive() { | |
| 608 | + | for i, w := range u.implicit { | |
| 609 | + | if w != nil { | |
| 610 | + | s.server.syncWait.cancel(w) | |
| 611 | + | u.implicit[i] = nil | |
| 612 | + | } | |
| 613 | + | } | |
| 614 | + | } else { | |
| 615 | + | if !s.syncReady(u.sync) { | |
| 616 | + | return false | |
| 617 | + | } | |
| 618 | + | for _, w := range u.implicit { | |
| 619 | + | if w != nil && !s.server.syncWait.fired(w) { | |
| 620 | + | return false | |
| 621 | + | } | |
| 622 | + | } | |
| 623 | + | } | |
| 588 | 624 | for _, dep := range u.deps { | |
| 589 | 625 | if !dep.owner.destroyed && !dep.graphReady(now) { | |
| 590 | 626 | return false | |
| @@ -208,6 +208,7 @@ type update struct { | |||||
| 208 | layout []childLayout | 208 | layout []childLayout | ||
| 209 | deps []*update | 209 | deps []*update | ||
| 210 | owner *surface | 210 | owner *surface | ||
| 211 | + | implicit [4]*syncWait | |||
| 211 | prev *update | 212 | prev *update | ||
| 212 | synced, bound bool | 213 | synced, bound bool | ||
| 213 | xdg *xdgSurface | 214 | xdg *xdgSurface | ||
| @@ -344,6 +345,15 @@ func (s *surface) queueUpdate() { | |||||
| 344 | *u = s.takePending() | 345 | *u = s.takePending() | ||
| 345 | u.deps = deps | 346 | u.deps = deps | ||
| 346 | u.owner, u.synced = s, s.effectivelySynced() | 347 | u.owner, u.synced = s, s.effectivelySynced() | ||
| 348 | + | if s.server.syncWait != nil && u.attached && u.buffer != nil && u.sync == nil { | |||
| 349 | + | if b, ok := s.server.buffers[u.buffer.Resource].(*dmabufBuffer); ok { | |||
| 350 | + | err := s.server.syncWait.watchImplicit(b.buf, &u.implicit) | |||
| 351 | + | if err != nil { | |||
| 352 | + | // Partial registrations were cancelled; do not block this update. | |||
| 353 | + | s.server.log.Warn().Str("component", "wayland").Err(err).Msg("implicit buffer wait") | |||
| 354 | + | } | |||
| 355 | + | } | |||
| 356 | + | } | |||
| 347 | for _, ch := range s.sub.children { | 357 | for _, ch := range s.sub.children { | ||
| 348 | for i := len(ch.queue) - 1; i >= 0; i-- { | 358 | for i := len(ch.queue) - 1; i >= 0; i-- { | ||
| 349 | candidate := ch.queue[i] | 359 | candidate := ch.queue[i] | ||
| @@ -376,6 +386,10 @@ func (s *surface) applyUpdate(u *update) { | |||||
| 376 | } | 386 | } | ||
| 377 | if u.buffer != nil && !u.buffer.Resource.Alive() { | 387 | if u.buffer != nil && !u.buffer.Resource.Alive() { | ||
| 378 | s.commitSkipped = true | 388 | s.commitSkipped = true | ||
| 389 | + | // No reader takes this buffer. A release point is safe to signal | |||
| 390 | + | // only if the acquire has already signalled; dropSync owns that rule. | |||
| 391 | + | s.dropSync(u.sync) | |||
| 392 | + | u.sync = nil | |||
| 379 | u.buffer, u.attached = nil, false | 393 | u.buffer, u.attached = nil, false | ||
| 380 | // Scale, transform and crop describe the same retained buffer. | 394 | // Scale, transform and crop describe the same retained buffer. | ||
| 381 | u.scale = s.bufferScale | 395 | u.scale = s.bufferScale | ||
| @@ -407,6 +421,9 @@ func (s *surface) dropQueue() { | |||||
| 407 | discard(fb) | 421 | discard(fb) | ||
| 408 | } | 422 | } | ||
| 409 | s.dropSync(u.sync) | 423 | s.dropSync(u.sync) | ||
| 424 | + | for _, w := range u.implicit { | |||
| 425 | + | s.server.syncWait.cancel(w) | |||
| 426 | + | } | |||
| 410 | if u.sync != nil { | 427 | if u.sync != nil { | ||
| 411 | continue // explicit sync: the release point replaced release | 428 | continue // explicit sync: the release point replaced release | ||
| 412 | } | 429 | } | ||
| @@ -582,9 +599,28 @@ func (u *update) graphReady(now time.Time) bool { | |||||
| 582 | if u.prev != nil && !u.prev.graphReady(now) { | 599 | if u.prev != nil && !u.prev.graphReady(now) { | ||
| 583 | return false | 600 | return false | ||
| 584 | } | 601 | } | ||
| 585 | - | if u.wait && s.barrier || !s.syncReady(u.sync) || s.tooEarly(u.at, now) { | 602 | + | if u.wait && s.barrier || s.tooEarly(u.at, now) { |
| 586 | return false | 603 | return false | ||
| 587 | } | 604 | } | ||
| 605 | + | // A dead attachment is skipped by applyUpdate; waiting for its fence | |||
| 606 | + | // would leave this update and every later one stuck behind it. | |||
| 607 | + | if u.buffer != nil && !u.buffer.Resource.Alive() { | |||
| 608 | + | for i, w := range u.implicit { | |||
| 609 | + | if w != nil { | |||
| 610 | + | s.server.syncWait.cancel(w) | |||
| 611 | + | u.implicit[i] = nil | |||
| 612 | + | } | |||
| 613 | + | } | |||
| 614 | + | } else { | |||
| 615 | + | if !s.syncReady(u.sync) { | |||
| 616 | + | return false | |||
| 617 | + | } | |||
| 618 | + | for _, w := range u.implicit { | |||
| 619 | + | if w != nil && !s.server.syncWait.fired(w) { | |||
| 620 | + | return false | |||
| 621 | + | } | |||
| 622 | + | } | |||
| 623 | + | } | |||
| 588 | for _, dep := range u.deps { | 624 | for _, dep := range u.deps { | ||
| 589 | if !dep.owner.destroyed && !dep.graphReady(now) { | 625 | if !dep.owner.destroyed && !dep.graphReady(now) { | ||
| 590 | return false | 626 | return false | ||
M internal/adapters/wayland/server.go +9 -0
| @@ -249,6 +249,14 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 249 | 249 | s.syncDev = node | |
| 250 | 250 | } | |
| 251 | 251 | } | |
| 252 | + | // Implicit dma-buf fences also need this waiter when explicit sync is off. | |
| 253 | + | if s.syncWait == nil { | |
| 254 | + | if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | |
| 255 | + | cleanup() | |
| 256 | + | d.Close() | |
| 257 | + | return nil, err | |
| 258 | + | } | |
| 259 | + | } | |
| 252 | 260 | s.colorIdentity = map[string]uint64{} | |
| 253 | 261 | s.colorDescriptions = map[*server.Resource]colorDescription{} | |
| 254 | 262 | s.colorOutputs = map[*colorOutput]struct{}{} | |
| @@ -263,6 +271,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 263 | 271 | } | |
| 264 | 272 | s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap | |
| 265 | 273 | } | |
| 274 | + | s.syncWait.log = log | |
| 266 | 275 | s.cleanup = func() { | |
| 267 | 276 | if s.seat.keymapFD >= 0 { | |
| 268 | 277 | unix.Close(s.seat.keymapFD) | |
| @@ -249,6 +249,14 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 249 | s.syncDev = node | 249 | s.syncDev = node | ||
| 250 | } | 250 | } | ||
| 251 | } | 251 | } | ||
| 252 | + | // Implicit dma-buf fences also need this waiter when explicit sync is off. | |||
| 253 | + | if s.syncWait == nil { | |||
| 254 | + | if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | |||
| 255 | + | cleanup() | |||
| 256 | + | d.Close() | |||
| 257 | + | return nil, err | |||
| 258 | + | } | |||
| 259 | + | } | |||
| 252 | s.colorIdentity = map[string]uint64{} | 260 | s.colorIdentity = map[string]uint64{} | ||
| 253 | s.colorDescriptions = map[*server.Resource]colorDescription{} | 261 | s.colorDescriptions = map[*server.Resource]colorDescription{} | ||
| 254 | s.colorOutputs = map[*colorOutput]struct{}{} | 262 | s.colorOutputs = map[*colorOutput]struct{}{} | ||
| @@ -263,6 +271,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 263 | } | 271 | } | ||
| 264 | s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap | 272 | s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap | ||
| 265 | } | 273 | } | ||
| 274 | + | s.syncWait.log = log | |||
| 266 | s.cleanup = func() { | 275 | s.cleanup = func() { | ||
| 267 | if s.seat.keymapFD >= 0 { | 276 | if s.seat.keymapFD >= 0 { | ||
| 268 | unix.Close(s.seat.keymapFD) | 277 | unix.Close(s.seat.keymapFD) | ||
M internal/adapters/wayland/surface.go +2 -1
| @@ -190,6 +190,7 @@ func (s *surface) contentWithColor() ports.SurfaceContent { | |||
| 190 | 190 | func (s *surface) tree(id ports.WindowID) ports.SurfaceContent { | |
| 191 | 191 | c := s.contentWithColor() | |
| 192 | 192 | c.ID = id | |
| 193 | + | c.ContentType = ports.ContentType(s.contentKind) | |
| 193 | 194 | if s.treeDirty || s.cachedTree == nil { | |
| 194 | 195 | count := s.treeCount() | |
| 195 | 196 | children := make([]ports.Subsurface, 0, count) | |
| @@ -364,7 +365,7 @@ func (s *surface) applyCommit(u *update) { | |||
| 364 | 365 | } | |
| 365 | 366 | if s.contentKind != u.kind { | |
| 366 | 367 | s.contentKind = u.kind | |
| 367 | - | s.server.log.Info().Uint64("id", uint64(s.root().windowID())).Uint32("content_type", s.contentKind).Msg("content type") | |
| 368 | + | s.server.log.Info().Str("component", "wayland").Uint64("id", uint64(s.root().windowID())).Uint32("content_type", s.contentKind).Msg("content type") | |
| 368 | 369 | } | |
| 369 | 370 | if u.scale > 0 { | |
| 370 | 371 | s.bufferScale = u.scale | |
| @@ -190,6 +190,7 @@ func (s *surface) contentWithColor() ports.SurfaceContent { | |||||
| 190 | func (s *surface) tree(id ports.WindowID) ports.SurfaceContent { | 190 | func (s *surface) tree(id ports.WindowID) ports.SurfaceContent { | ||
| 191 | c := s.contentWithColor() | 191 | c := s.contentWithColor() | ||
| 192 | c.ID = id | 192 | c.ID = id | ||
| 193 | + | c.ContentType = ports.ContentType(s.contentKind) | |||
| 193 | if s.treeDirty || s.cachedTree == nil { | 194 | if s.treeDirty || s.cachedTree == nil { | ||
| 194 | count := s.treeCount() | 195 | count := s.treeCount() | ||
| 195 | children := make([]ports.Subsurface, 0, count) | 196 | children := make([]ports.Subsurface, 0, count) | ||
| @@ -364,7 +365,7 @@ func (s *surface) applyCommit(u *update) { | |||||
| 364 | } | 365 | } | ||
| 365 | if s.contentKind != u.kind { | 366 | if s.contentKind != u.kind { | ||
| 366 | s.contentKind = u.kind | 367 | s.contentKind = u.kind | ||
| 367 | - | s.server.log.Info().Uint64("id", uint64(s.root().windowID())).Uint32("content_type", s.contentKind).Msg("content type") | 368 | + | s.server.log.Info().Str("component", "wayland").Uint64("id", uint64(s.root().windowID())).Uint32("content_type", s.contentKind).Msg("content type") |
| 368 | } | 369 | } | ||
| 369 | if u.scale > 0 { | 370 | if u.scale > 0 { | ||
| 370 | s.bufferScale = u.scale | 371 | s.bufferScale = u.scale | ||
M internal/adapters/wayland/syncdev.go +25 -3
| @@ -18,7 +18,9 @@ type syncobjDevice interface { | |||
| 18 | 18 | exportSyncFile(h uint32, point uint64) (*os.File, error) | |
| 19 | 19 | // signal signals a timeline point (a release point). | |
| 20 | 20 | signal(h uint32, point uint64) error | |
| 21 | - | // eventfd writes efd once the point has a fence (WAIT_AVAILABLE). | |
| 21 | + | // signalled checks a point without blocking. | |
| 22 | + | signalled(h uint32, point uint64) (bool, error) | |
| 23 | + | // eventfd writes efd once the point is signalled. | |
| 22 | 24 | eventfd(h uint32, point uint64, efd int) error | |
| 23 | 25 | destroy(h uint32) error | |
| 24 | 26 | } | |
| @@ -27,13 +29,13 @@ const ( | |||
| 27 | 29 | ioctlSyncobjDestroy = 0xC00864C0 // DRM_IOWR(0xC0, struct drm_syncobj_destroy) | |
| 28 | 30 | ioctlSyncobjHandleToFD = 0xC01864C1 // DRM_IOWR(0xC1, struct drm_syncobj_handle) | |
| 29 | 31 | ioctlSyncobjFDToHandle = 0xC01864C2 // DRM_IOWR(0xC2, struct drm_syncobj_handle) | |
| 32 | + | ioctlSyncobjTimelineWait = 0xC02864CA // DRM_IOWR(0xCA, struct drm_syncobj_timeline_wait) | |
| 30 | 33 | ioctlSyncobjTimelineSignal = 0xC01864CD // DRM_IOWR(0xCD, struct drm_syncobj_timeline_array) | |
| 31 | 34 | ioctlSyncobjEventfd = 0xC01864CF // DRM_IOWR(0xCF, struct drm_syncobj_eventfd) | |
| 32 | 35 | ioctlGetCap = 0xC010640C // DRM_IOWR(0x0C, struct drm_get_cap) | |
| 33 | 36 | ||
| 34 | 37 | syncobjExportSyncFile = 1 << 0 | |
| 35 | 38 | syncobjTimeline = 1 << 1 | |
| 36 | - | syncobjWaitAvailable = 1 << 2 | |
| 37 | 39 | capSyncobjTimeline = 0x14 | |
| 38 | 40 | ) | |
| 39 | 41 | ||
| @@ -51,6 +53,14 @@ type syncobjTimelineArray struct { | |||
| 51 | 53 | count, flags uint32 | |
| 52 | 54 | } | |
| 53 | 55 | ||
| 56 | + | type syncobjTimelineWait struct { | |
| 57 | + | handles, points uint64 | |
| 58 | + | timeout int64 | |
| 59 | + | count, flags uint32 | |
| 60 | + | first uint32 | |
| 61 | + | pad uint32 | |
| 62 | + | } | |
| 63 | + | ||
| 54 | 64 | type syncobjEventfd struct { | |
| 55 | 65 | handle, flags uint32 | |
| 56 | 66 | point uint64 | |
| @@ -117,8 +127,20 @@ func (n *renderNode) signal(h uint32, point uint64) error { | |||
| 117 | 127 | return err | |
| 118 | 128 | } | |
| 119 | 129 | ||
| 130 | + | func (n *renderNode) signalled(h uint32, point uint64) (bool, error) { | |
| 131 | + | handles, points := [1]uint32{h}, [1]uint64{point} | |
| 132 | + | a := syncobjTimelineWait{handles: uint64(uintptr(unsafe.Pointer(&handles))), points: uint64(uintptr(unsafe.Pointer(&points))), count: 1} | |
| 133 | + | err := ioctl(n.fd, ioctlSyncobjTimelineWait, unsafe.Pointer(&a)) | |
| 134 | + | runtime.KeepAlive(&handles) | |
| 135 | + | runtime.KeepAlive(&points) | |
| 136 | + | if err == unix.ETIME { | |
| 137 | + | return false, nil | |
| 138 | + | } | |
| 139 | + | return err == nil, err | |
| 140 | + | } | |
| 141 | + | ||
| 120 | 142 | func (n *renderNode) eventfd(h uint32, point uint64, efd int) error { | |
| 121 | - | a := syncobjEventfd{handle: h, flags: syncobjWaitAvailable, point: point, fd: int32(efd)} | |
| 143 | + | a := syncobjEventfd{handle: h, point: point, fd: int32(efd)} | |
| 122 | 144 | return ioctl(n.fd, ioctlSyncobjEventfd, unsafe.Pointer(&a)) | |
| 123 | 145 | } | |
| 124 | 146 | ||
| @@ -18,7 +18,9 @@ type syncobjDevice interface { | |||||
| 18 | exportSyncFile(h uint32, point uint64) (*os.File, error) | 18 | exportSyncFile(h uint32, point uint64) (*os.File, error) | ||
| 19 | // signal signals a timeline point (a release point). | 19 | // signal signals a timeline point (a release point). | ||
| 20 | signal(h uint32, point uint64) error | 20 | signal(h uint32, point uint64) error | ||
| 21 | - | // eventfd writes efd once the point has a fence (WAIT_AVAILABLE). | 21 | + | // signalled checks a point without blocking. |
| 22 | + | signalled(h uint32, point uint64) (bool, error) | |||
| 23 | + | // eventfd writes efd once the point is signalled. | |||
| 22 | eventfd(h uint32, point uint64, efd int) error | 24 | eventfd(h uint32, point uint64, efd int) error | ||
| 23 | destroy(h uint32) error | 25 | destroy(h uint32) error | ||
| 24 | } | 26 | } | ||
| @@ -27,13 +29,13 @@ const ( | |||||
| 27 | ioctlSyncobjDestroy = 0xC00864C0 // DRM_IOWR(0xC0, struct drm_syncobj_destroy) | 29 | ioctlSyncobjDestroy = 0xC00864C0 // DRM_IOWR(0xC0, struct drm_syncobj_destroy) | ||
| 28 | ioctlSyncobjHandleToFD = 0xC01864C1 // DRM_IOWR(0xC1, struct drm_syncobj_handle) | 30 | ioctlSyncobjHandleToFD = 0xC01864C1 // DRM_IOWR(0xC1, struct drm_syncobj_handle) | ||
| 29 | ioctlSyncobjFDToHandle = 0xC01864C2 // DRM_IOWR(0xC2, struct drm_syncobj_handle) | 31 | ioctlSyncobjFDToHandle = 0xC01864C2 // DRM_IOWR(0xC2, struct drm_syncobj_handle) | ||
| 32 | + | ioctlSyncobjTimelineWait = 0xC02864CA // DRM_IOWR(0xCA, struct drm_syncobj_timeline_wait) | |||
| 30 | ioctlSyncobjTimelineSignal = 0xC01864CD // DRM_IOWR(0xCD, struct drm_syncobj_timeline_array) | 33 | ioctlSyncobjTimelineSignal = 0xC01864CD // DRM_IOWR(0xCD, struct drm_syncobj_timeline_array) | ||
| 31 | ioctlSyncobjEventfd = 0xC01864CF // DRM_IOWR(0xCF, struct drm_syncobj_eventfd) | 34 | ioctlSyncobjEventfd = 0xC01864CF // DRM_IOWR(0xCF, struct drm_syncobj_eventfd) | ||
| 32 | ioctlGetCap = 0xC010640C // DRM_IOWR(0x0C, struct drm_get_cap) | 35 | ioctlGetCap = 0xC010640C // DRM_IOWR(0x0C, struct drm_get_cap) | ||
| 33 | 36 | ||||
| 34 | syncobjExportSyncFile = 1 << 0 | 37 | syncobjExportSyncFile = 1 << 0 | ||
| 35 | syncobjTimeline = 1 << 1 | 38 | syncobjTimeline = 1 << 1 | ||
| 36 | - | syncobjWaitAvailable = 1 << 2 | |||
| 37 | capSyncobjTimeline = 0x14 | 39 | capSyncobjTimeline = 0x14 | ||
| 38 | ) | 40 | ) | ||
| 39 | 41 | ||||
| @@ -51,6 +53,14 @@ type syncobjTimelineArray struct { | |||||
| 51 | count, flags uint32 | 53 | count, flags uint32 | ||
| 52 | } | 54 | } | ||
| 53 | 55 | ||||
| 56 | + | type syncobjTimelineWait struct { | |||
| 57 | + | handles, points uint64 | |||
| 58 | + | timeout int64 | |||
| 59 | + | count, flags uint32 | |||
| 60 | + | first uint32 | |||
| 61 | + | pad uint32 | |||
| 62 | + | } | |||
| 63 | + | ||||
| 54 | type syncobjEventfd struct { | 64 | type syncobjEventfd struct { | ||
| 55 | handle, flags uint32 | 65 | handle, flags uint32 | ||
| 56 | point uint64 | 66 | point uint64 | ||
| @@ -117,8 +127,20 @@ func (n *renderNode) signal(h uint32, point uint64) error { | |||||
| 117 | return err | 127 | return err | ||
| 118 | } | 128 | } | ||
| 119 | 129 | ||||
| 130 | + | func (n *renderNode) signalled(h uint32, point uint64) (bool, error) { | |||
| 131 | + | handles, points := [1]uint32{h}, [1]uint64{point} | |||
| 132 | + | a := syncobjTimelineWait{handles: uint64(uintptr(unsafe.Pointer(&handles))), points: uint64(uintptr(unsafe.Pointer(&points))), count: 1} | |||
| 133 | + | err := ioctl(n.fd, ioctlSyncobjTimelineWait, unsafe.Pointer(&a)) | |||
| 134 | + | runtime.KeepAlive(&handles) | |||
| 135 | + | runtime.KeepAlive(&points) | |||
| 136 | + | if err == unix.ETIME { | |||
| 137 | + | return false, nil | |||
| 138 | + | } | |||
| 139 | + | return err == nil, err | |||
| 140 | + | } | |||
| 141 | + | ||||
| 120 | func (n *renderNode) eventfd(h uint32, point uint64, efd int) error { | 142 | func (n *renderNode) eventfd(h uint32, point uint64, efd int) error { | ||
| 121 | - | a := syncobjEventfd{handle: h, flags: syncobjWaitAvailable, point: point, fd: int32(efd)} | 143 | + | a := syncobjEventfd{handle: h, point: point, fd: int32(efd)} |
| 122 | return ioctl(n.fd, ioctlSyncobjEventfd, unsafe.Pointer(&a)) | 144 | return ioctl(n.fd, ioctlSyncobjEventfd, unsafe.Pointer(&a)) | ||
| 123 | } | 145 | } | ||
| 124 | 146 | ||||
M internal/adapters/wayland/syncobj.go +79 -14
| @@ -7,9 +7,11 @@ import ( | |||
| 7 | 7 | "sync" | |
| 8 | 8 | "sync/atomic" | |
| 9 | 9 | ||
| 10 | + | "github.com/bnema/neferwl/internal/ports" | |
| 10 | 11 | "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj" | |
| 11 | 12 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 12 | 13 | "github.com/bnema/purego-libwayland/server" | |
| 14 | + | "github.com/bnema/zerowrap" | |
| 13 | 15 | "golang.org/x/sys/unix" | |
| 14 | 16 | ) | |
| 15 | 17 | ||
| @@ -188,15 +190,17 @@ func (s *surface) pendingIsSHM() bool { | |||
| 188 | 190 | } | |
| 189 | 191 | ||
| 190 | 192 | // syncWaiter watches acquire points: each gets an eventfd the kernel | |
| 191 | - | // writes once the point has a fence, and the pacer is woken to apply the | |
| 193 | + | // writes once the point is signalled, and the pacer is woken to apply the | |
| 192 | 194 | // commits that waited for it. | |
| 193 | 195 | type syncWaiter struct { | |
| 194 | - | mu sync.Mutex | |
| 195 | - | ready map[*syncWait]bool | |
| 196 | - | wake func() | |
| 197 | - | pipeR *os.File | |
| 198 | - | pipeW *os.File | |
| 199 | - | added []*syncWait // new waits for run (under mu) | |
| 196 | + | mu sync.Mutex | |
| 197 | + | ready map[*syncWait]bool | |
| 198 | + | wake func() | |
| 199 | + | pipeR *os.File | |
| 200 | + | pipeW *os.File | |
| 201 | + | added []*syncWait // new waits for run (under mu) | |
| 202 | + | log zerowrap.Logger | |
| 203 | + | pollError sync.Once | |
| 200 | 204 | } | |
| 201 | 205 | ||
| 202 | 206 | // syncWait is one acquire point a queued commit waits for. | |
| @@ -211,11 +215,15 @@ func newSyncWaiter(wake func()) (*syncWaiter, error) { | |||
| 211 | 215 | if err != nil { | |
| 212 | 216 | return nil, err | |
| 213 | 217 | } | |
| 214 | - | return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w}, nil | |
| 218 | + | return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, log: zerowrap.Default()}, nil | |
| 215 | 219 | } | |
| 216 | 220 | ||
| 217 | 221 | // watch registers an eventfd for p; the wait is ready once it fires. | |
| 218 | 222 | func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | |
| 223 | + | ready, err := p.tl.dev.signalled(p.tl.handle, p.point) | |
| 224 | + | if err != nil || ready { | |
| 225 | + | return &syncWait{efd: -1, done: ready}, err | |
| 226 | + | } | |
| 219 | 227 | efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK) | |
| 220 | 228 | if err != nil { | |
| 221 | 229 | return nil, err | |
| @@ -224,15 +232,63 @@ func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | |||
| 224 | 232 | unix.Close(efd) | |
| 225 | 233 | return nil, err | |
| 226 | 234 | } | |
| 227 | - | w := &syncWait{efd: efd} | |
| 235 | + | return sw.add(efd), nil | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | // watchImplicit waits for a dma-buf's exclusive fences without blocking dispatch. | |
| 239 | + | func (sw *syncWaiter) watchImplicit(b *ports.DMABuf, waits *[4]*syncWait) error { | |
| 240 | + | var fds [4]unix.PollFd | |
| 241 | + | for i, p := range b.Planes { | |
| 242 | + | if i >= len(fds) { | |
| 243 | + | break | |
| 244 | + | } | |
| 245 | + | fds[i] = unix.PollFd{Fd: int32(p.File.Fd()), Events: unix.POLLIN} | |
| 246 | + | } | |
| 247 | + | planes := fds[:min(len(b.Planes), len(fds))] | |
| 248 | + | _, err := unix.Poll(planes, 0) | |
| 249 | + | for errors.Is(err, unix.EINTR) { // a signal, not a fence error | |
| 250 | + | _, err = unix.Poll(planes, 0) | |
| 251 | + | } | |
| 252 | + | if err != nil { | |
| 253 | + | return err | |
| 254 | + | } | |
| 255 | + | for i, fd := range planes { | |
| 256 | + | if fd.Revents&unix.POLLIN != 0 { | |
| 257 | + | continue | |
| 258 | + | } | |
| 259 | + | if fd.Revents&(unix.POLLERR|unix.POLLNVAL|unix.POLLHUP) != 0 { | |
| 260 | + | sw.warnPollError() | |
| 261 | + | continue | |
| 262 | + | } | |
| 263 | + | dup, err := unix.FcntlInt(uintptr(fd.Fd), unix.F_DUPFD_CLOEXEC, 0) | |
| 264 | + | if err != nil { | |
| 265 | + | for j, w := range waits { | |
| 266 | + | sw.cancel(w) | |
| 267 | + | waits[j] = nil | |
| 268 | + | } | |
| 269 | + | return err | |
| 270 | + | } | |
| 271 | + | waits[i] = sw.add(dup) | |
| 272 | + | } | |
| 273 | + | return nil | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | func (sw *syncWaiter) warnPollError() { | |
| 277 | + | sw.pollError.Do(func() { | |
| 278 | + | sw.log.Warn().Str("component", "wayland").Msg("implicit fence poll error") | |
| 279 | + | }) | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | func (sw *syncWaiter) add(fd int) *syncWait { | |
| 283 | + | w := &syncWait{efd: fd} | |
| 228 | 284 | sw.mu.Lock() | |
| 229 | 285 | sw.added = append(sw.added, w) | |
| 230 | 286 | sw.mu.Unlock() | |
| 231 | 287 | _, _ = sw.pipeW.Write([]byte{0}) | |
| 232 | - | return w, nil | |
| 288 | + | return w | |
| 233 | 289 | } | |
| 234 | 290 | ||
| 235 | - | // fired reports whether w's point has a fence, and forgets w once it has. | |
| 291 | + | // fired reports whether w's point is signalled, and forgets w once it has. | |
| 236 | 292 | func (sw *syncWaiter) fired(w *syncWait) bool { | |
| 237 | 293 | sw.mu.Lock() | |
| 238 | 294 | defer sw.mu.Unlock() | |
| @@ -245,6 +301,9 @@ func (sw *syncWaiter) fired(w *syncWait) bool { | |||
| 245 | 301 | ||
| 246 | 302 | // cancel stops watching w (the commit was dropped). | |
| 247 | 303 | func (sw *syncWaiter) cancel(w *syncWait) { | |
| 304 | + | if w == nil || w.efd < 0 { | |
| 305 | + | return | |
| 306 | + | } | |
| 248 | 307 | w.cancelled.Store(true) | |
| 249 | 308 | sw.mu.Lock() | |
| 250 | 309 | delete(sw.ready, w) | |
| @@ -294,15 +353,21 @@ func (sw *syncWaiter) run(ctx context.Context) { | |||
| 294 | 353 | unix.Close(w.efd) | |
| 295 | 354 | continue | |
| 296 | 355 | } | |
| 297 | - | if fds[i+1].Revents == 0 { | |
| 356 | + | revents := fds[i+1].Revents | |
| 357 | + | if revents&unix.POLLIN == 0 && revents&(unix.POLLERR|unix.POLLNVAL|unix.POLLHUP) == 0 { | |
| 298 | 358 | kept = append(kept, w) | |
| 299 | 359 | continue | |
| 300 | 360 | } | |
| 361 | + | if revents&unix.POLLIN == 0 { | |
| 362 | + | sw.warnPollError() | |
| 363 | + | } | |
| 301 | 364 | unix.Close(w.efd) | |
| 302 | 365 | sw.mu.Lock() | |
| 303 | - | sw.ready[w] = true | |
| 366 | + | if !w.cancelled.Load() { | |
| 367 | + | sw.ready[w] = true | |
| 368 | + | fired = true | |
| 369 | + | } | |
| 304 | 370 | sw.mu.Unlock() | |
| 305 | - | fired = true | |
| 306 | 371 | } | |
| 307 | 372 | waits = kept | |
| 308 | 373 | // New waits join the next poll. | |
| @@ -7,9 +7,11 @@ import ( | |||||
| 7 | "sync" | 7 | "sync" | ||
| 8 | "sync/atomic" | 8 | "sync/atomic" | ||
| 9 | 9 | ||||
| 10 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 10 | "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj" | 11 | "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj" | ||
| 11 | "github.com/bnema/purego-libwayland/protocol/wayland" | 12 | "github.com/bnema/purego-libwayland/protocol/wayland" | ||
| 12 | "github.com/bnema/purego-libwayland/server" | 13 | "github.com/bnema/purego-libwayland/server" | ||
| 14 | + | "github.com/bnema/zerowrap" | |||
| 13 | "golang.org/x/sys/unix" | 15 | "golang.org/x/sys/unix" | ||
| 14 | ) | 16 | ) | ||
| 15 | 17 | ||||
| @@ -188,15 +190,17 @@ func (s *surface) pendingIsSHM() bool { | |||||
| 188 | } | 190 | } | ||
| 189 | 191 | ||||
| 190 | // syncWaiter watches acquire points: each gets an eventfd the kernel | 192 | // syncWaiter watches acquire points: each gets an eventfd the kernel | ||
| 191 | - | // writes once the point has a fence, and the pacer is woken to apply the | 193 | + | // writes once the point is signalled, and the pacer is woken to apply the |
| 192 | // commits that waited for it. | 194 | // commits that waited for it. | ||
| 193 | type syncWaiter struct { | 195 | type syncWaiter struct { | ||
| 194 | - | mu sync.Mutex | 196 | + | mu sync.Mutex |
| 195 | - | ready map[*syncWait]bool | 197 | + | ready map[*syncWait]bool |
| 196 | - | wake func() | 198 | + | wake func() |
| 197 | - | pipeR *os.File | 199 | + | pipeR *os.File |
| 198 | - | pipeW *os.File | 200 | + | pipeW *os.File |
| 199 | - | added []*syncWait // new waits for run (under mu) | 201 | + | added []*syncWait // new waits for run (under mu) |
| 202 | + | log zerowrap.Logger | |||
| 203 | + | pollError sync.Once | |||
| 200 | } | 204 | } | ||
| 201 | 205 | ||||
| 202 | // syncWait is one acquire point a queued commit waits for. | 206 | // syncWait is one acquire point a queued commit waits for. | ||
| @@ -211,11 +215,15 @@ func newSyncWaiter(wake func()) (*syncWaiter, error) { | |||||
| 211 | if err != nil { | 215 | if err != nil { | ||
| 212 | return nil, err | 216 | return nil, err | ||
| 213 | } | 217 | } | ||
| 214 | - | return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w}, nil | 218 | + | return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, log: zerowrap.Default()}, nil |
| 215 | } | 219 | } | ||
| 216 | 220 | ||||
| 217 | // watch registers an eventfd for p; the wait is ready once it fires. | 221 | // watch registers an eventfd for p; the wait is ready once it fires. | ||
| 218 | func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | 222 | func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | ||
| 223 | + | ready, err := p.tl.dev.signalled(p.tl.handle, p.point) | |||
| 224 | + | if err != nil || ready { | |||
| 225 | + | return &syncWait{efd: -1, done: ready}, err | |||
| 226 | + | } | |||
| 219 | efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK) | 227 | efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK) | ||
| 220 | if err != nil { | 228 | if err != nil { | ||
| 221 | return nil, err | 229 | return nil, err | ||
| @@ -224,15 +232,63 @@ func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | |||||
| 224 | unix.Close(efd) | 232 | unix.Close(efd) | ||
| 225 | return nil, err | 233 | return nil, err | ||
| 226 | } | 234 | } | ||
| 227 | - | w := &syncWait{efd: efd} | 235 | + | return sw.add(efd), nil |
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | // watchImplicit waits for a dma-buf's exclusive fences without blocking dispatch. | |||
| 239 | + | func (sw *syncWaiter) watchImplicit(b *ports.DMABuf, waits *[4]*syncWait) error { | |||
| 240 | + | var fds [4]unix.PollFd | |||
| 241 | + | for i, p := range b.Planes { | |||
| 242 | + | if i >= len(fds) { | |||
| 243 | + | break | |||
| 244 | + | } | |||
| 245 | + | fds[i] = unix.PollFd{Fd: int32(p.File.Fd()), Events: unix.POLLIN} | |||
| 246 | + | } | |||
| 247 | + | planes := fds[:min(len(b.Planes), len(fds))] | |||
| 248 | + | _, err := unix.Poll(planes, 0) | |||
| 249 | + | for errors.Is(err, unix.EINTR) { // a signal, not a fence error | |||
| 250 | + | _, err = unix.Poll(planes, 0) | |||
| 251 | + | } | |||
| 252 | + | if err != nil { | |||
| 253 | + | return err | |||
| 254 | + | } | |||
| 255 | + | for i, fd := range planes { | |||
| 256 | + | if fd.Revents&unix.POLLIN != 0 { | |||
| 257 | + | continue | |||
| 258 | + | } | |||
| 259 | + | if fd.Revents&(unix.POLLERR|unix.POLLNVAL|unix.POLLHUP) != 0 { | |||
| 260 | + | sw.warnPollError() | |||
| 261 | + | continue | |||
| 262 | + | } | |||
| 263 | + | dup, err := unix.FcntlInt(uintptr(fd.Fd), unix.F_DUPFD_CLOEXEC, 0) | |||
| 264 | + | if err != nil { | |||
| 265 | + | for j, w := range waits { | |||
| 266 | + | sw.cancel(w) | |||
| 267 | + | waits[j] = nil | |||
| 268 | + | } | |||
| 269 | + | return err | |||
| 270 | + | } | |||
| 271 | + | waits[i] = sw.add(dup) | |||
| 272 | + | } | |||
| 273 | + | return nil | |||
| 274 | + | } | |||
| 275 | + | ||||
| 276 | + | func (sw *syncWaiter) warnPollError() { | |||
| 277 | + | sw.pollError.Do(func() { | |||
| 278 | + | sw.log.Warn().Str("component", "wayland").Msg("implicit fence poll error") | |||
| 279 | + | }) | |||
| 280 | + | } | |||
| 281 | + | ||||
| 282 | + | func (sw *syncWaiter) add(fd int) *syncWait { | |||
| 283 | + | w := &syncWait{efd: fd} | |||
| 228 | sw.mu.Lock() | 284 | sw.mu.Lock() | ||
| 229 | sw.added = append(sw.added, w) | 285 | sw.added = append(sw.added, w) | ||
| 230 | sw.mu.Unlock() | 286 | sw.mu.Unlock() | ||
| 231 | _, _ = sw.pipeW.Write([]byte{0}) | 287 | _, _ = sw.pipeW.Write([]byte{0}) | ||
| 232 | - | return w, nil | 288 | + | return w |
| 233 | } | 289 | } | ||
| 234 | 290 | ||||
| 235 | - | // fired reports whether w's point has a fence, and forgets w once it has. | 291 | + | // fired reports whether w's point is signalled, and forgets w once it has. |
| 236 | func (sw *syncWaiter) fired(w *syncWait) bool { | 292 | func (sw *syncWaiter) fired(w *syncWait) bool { | ||
| 237 | sw.mu.Lock() | 293 | sw.mu.Lock() | ||
| 238 | defer sw.mu.Unlock() | 294 | defer sw.mu.Unlock() | ||
| @@ -245,6 +301,9 @@ func (sw *syncWaiter) fired(w *syncWait) bool { | |||||
| 245 | 301 | ||||
| 246 | // cancel stops watching w (the commit was dropped). | 302 | // cancel stops watching w (the commit was dropped). | ||
| 247 | func (sw *syncWaiter) cancel(w *syncWait) { | 303 | func (sw *syncWaiter) cancel(w *syncWait) { | ||
| 304 | + | if w == nil || w.efd < 0 { | |||
| 305 | + | return | |||
| 306 | + | } | |||
| 248 | w.cancelled.Store(true) | 307 | w.cancelled.Store(true) | ||
| 249 | sw.mu.Lock() | 308 | sw.mu.Lock() | ||
| 250 | delete(sw.ready, w) | 309 | delete(sw.ready, w) | ||
| @@ -294,15 +353,21 @@ func (sw *syncWaiter) run(ctx context.Context) { | |||||
| 294 | unix.Close(w.efd) | 353 | unix.Close(w.efd) | ||
| 295 | continue | 354 | continue | ||
| 296 | } | 355 | } | ||
| 297 | - | if fds[i+1].Revents == 0 { | 356 | + | revents := fds[i+1].Revents |
| 357 | + | if revents&unix.POLLIN == 0 && revents&(unix.POLLERR|unix.POLLNVAL|unix.POLLHUP) == 0 { | |||
| 298 | kept = append(kept, w) | 358 | kept = append(kept, w) | ||
| 299 | continue | 359 | continue | ||
| 300 | } | 360 | } | ||
| 361 | + | if revents&unix.POLLIN == 0 { | |||
| 362 | + | sw.warnPollError() | |||
| 363 | + | } | |||
| 301 | unix.Close(w.efd) | 364 | unix.Close(w.efd) | ||
| 302 | sw.mu.Lock() | 365 | sw.mu.Lock() | ||
| 303 | - | sw.ready[w] = true | 366 | + | if !w.cancelled.Load() { |
| 367 | + | sw.ready[w] = true | |||
| 368 | + | fired = true | |||
| 369 | + | } | |||
| 304 | sw.mu.Unlock() | 370 | sw.mu.Unlock() | ||
| 305 | - | fired = true | |||
| 306 | } | 371 | } | ||
| 307 | waits = kept | 372 | waits = kept | ||
| 308 | // New waits join the next poll. | 373 | // New waits join the next poll. | ||
M internal/adapters/wayland/syncobj_device_mock_test.go +66 -0
| @@ -344,3 +344,69 @@ func (_c *mocksyncobjDevice_signal_Call) RunAndReturn(run func(h uint32, point u | |||
| 344 | 344 | _c.Call.Return(run) | |
| 345 | 345 | return _c | |
| 346 | 346 | } | |
| 347 | + | ||
| 348 | + | // signalled provides a mock function for the type mocksyncobjDevice | |
| 349 | + | func (_mock *mocksyncobjDevice) signalled(h uint32, point uint64) (bool, error) { | |
| 350 | + | ret := _mock.Called(h, point) | |
| 351 | + | ||
| 352 | + | if len(ret) == 0 { | |
| 353 | + | panic("no return value specified for signalled") | |
| 354 | + | } | |
| 355 | + | ||
| 356 | + | var r0 bool | |
| 357 | + | var r1 error | |
| 358 | + | if returnFunc, ok := ret.Get(0).(func(uint32, uint64) (bool, error)); ok { | |
| 359 | + | return returnFunc(h, point) | |
| 360 | + | } | |
| 361 | + | if returnFunc, ok := ret.Get(0).(func(uint32, uint64) bool); ok { | |
| 362 | + | r0 = returnFunc(h, point) | |
| 363 | + | } else { | |
| 364 | + | r0 = ret.Get(0).(bool) | |
| 365 | + | } | |
| 366 | + | if returnFunc, ok := ret.Get(1).(func(uint32, uint64) error); ok { | |
| 367 | + | r1 = returnFunc(h, point) | |
| 368 | + | } else { | |
| 369 | + | r1 = ret.Error(1) | |
| 370 | + | } | |
| 371 | + | return r0, r1 | |
| 372 | + | } | |
| 373 | + | ||
| 374 | + | // mocksyncobjDevice_signalled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'signalled' | |
| 375 | + | type mocksyncobjDevice_signalled_Call struct { | |
| 376 | + | *mock.Call | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | // signalled is a helper method to define mock.On call | |
| 380 | + | // - h uint32 | |
| 381 | + | // - point uint64 | |
| 382 | + | func (_e *mocksyncobjDevice_Expecter) signalled(h any, point any) *mocksyncobjDevice_signalled_Call { | |
| 383 | + | return &mocksyncobjDevice_signalled_Call{Call: _e.mock.On("signalled", h, point)} | |
| 384 | + | } | |
| 385 | + | ||
| 386 | + | func (_c *mocksyncobjDevice_signalled_Call) Run(run func(h uint32, point uint64)) *mocksyncobjDevice_signalled_Call { | |
| 387 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 388 | + | var arg0 uint32 | |
| 389 | + | if args[0] != nil { | |
| 390 | + | arg0 = args[0].(uint32) | |
| 391 | + | } | |
| 392 | + | var arg1 uint64 | |
| 393 | + | if args[1] != nil { | |
| 394 | + | arg1 = args[1].(uint64) | |
| 395 | + | } | |
| 396 | + | run( | |
| 397 | + | arg0, | |
| 398 | + | arg1, | |
| 399 | + | ) | |
| 400 | + | }) | |
| 401 | + | return _c | |
| 402 | + | } | |
| 403 | + | ||
| 404 | + | func (_c *mocksyncobjDevice_signalled_Call) Return(b bool, err error) *mocksyncobjDevice_signalled_Call { | |
| 405 | + | _c.Call.Return(b, err) | |
| 406 | + | return _c | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | func (_c *mocksyncobjDevice_signalled_Call) RunAndReturn(run func(h uint32, point uint64) (bool, error)) *mocksyncobjDevice_signalled_Call { | |
| 410 | + | _c.Call.Return(run) | |
| 411 | + | return _c | |
| 412 | + | } | |
| @@ -344,3 +344,69 @@ func (_c *mocksyncobjDevice_signal_Call) RunAndReturn(run func(h uint32, point u | |||||
| 344 | _c.Call.Return(run) | 344 | _c.Call.Return(run) | ||
| 345 | return _c | 345 | return _c | ||
| 346 | } | 346 | } | ||
| 347 | + | ||||
| 348 | + | // signalled provides a mock function for the type mocksyncobjDevice | |||
| 349 | + | func (_mock *mocksyncobjDevice) signalled(h uint32, point uint64) (bool, error) { | |||
| 350 | + | ret := _mock.Called(h, point) | |||
| 351 | + | ||||
| 352 | + | if len(ret) == 0 { | |||
| 353 | + | panic("no return value specified for signalled") | |||
| 354 | + | } | |||
| 355 | + | ||||
| 356 | + | var r0 bool | |||
| 357 | + | var r1 error | |||
| 358 | + | if returnFunc, ok := ret.Get(0).(func(uint32, uint64) (bool, error)); ok { | |||
| 359 | + | return returnFunc(h, point) | |||
| 360 | + | } | |||
| 361 | + | if returnFunc, ok := ret.Get(0).(func(uint32, uint64) bool); ok { | |||
| 362 | + | r0 = returnFunc(h, point) | |||
| 363 | + | } else { | |||
| 364 | + | r0 = ret.Get(0).(bool) | |||
| 365 | + | } | |||
| 366 | + | if returnFunc, ok := ret.Get(1).(func(uint32, uint64) error); ok { | |||
| 367 | + | r1 = returnFunc(h, point) | |||
| 368 | + | } else { | |||
| 369 | + | r1 = ret.Error(1) | |||
| 370 | + | } | |||
| 371 | + | return r0, r1 | |||
| 372 | + | } | |||
| 373 | + | ||||
| 374 | + | // mocksyncobjDevice_signalled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'signalled' | |||
| 375 | + | type mocksyncobjDevice_signalled_Call struct { | |||
| 376 | + | *mock.Call | |||
| 377 | + | } | |||
| 378 | + | ||||
| 379 | + | // signalled is a helper method to define mock.On call | |||
| 380 | + | // - h uint32 | |||
| 381 | + | // - point uint64 | |||
| 382 | + | func (_e *mocksyncobjDevice_Expecter) signalled(h any, point any) *mocksyncobjDevice_signalled_Call { | |||
| 383 | + | return &mocksyncobjDevice_signalled_Call{Call: _e.mock.On("signalled", h, point)} | |||
| 384 | + | } | |||
| 385 | + | ||||
| 386 | + | func (_c *mocksyncobjDevice_signalled_Call) Run(run func(h uint32, point uint64)) *mocksyncobjDevice_signalled_Call { | |||
| 387 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 388 | + | var arg0 uint32 | |||
| 389 | + | if args[0] != nil { | |||
| 390 | + | arg0 = args[0].(uint32) | |||
| 391 | + | } | |||
| 392 | + | var arg1 uint64 | |||
| 393 | + | if args[1] != nil { | |||
| 394 | + | arg1 = args[1].(uint64) | |||
| 395 | + | } | |||
| 396 | + | run( | |||
| 397 | + | arg0, | |||
| 398 | + | arg1, | |||
| 399 | + | ) | |||
| 400 | + | }) | |||
| 401 | + | return _c | |||
| 402 | + | } | |||
| 403 | + | ||||
| 404 | + | func (_c *mocksyncobjDevice_signalled_Call) Return(b bool, err error) *mocksyncobjDevice_signalled_Call { | |||
| 405 | + | _c.Call.Return(b, err) | |||
| 406 | + | return _c | |||
| 407 | + | } | |||
| 408 | + | ||||
| 409 | + | func (_c *mocksyncobjDevice_signalled_Call) RunAndReturn(run func(h uint32, point uint64) (bool, error)) *mocksyncobjDevice_signalled_Call { | |||
| 410 | + | _c.Call.Return(run) | |||
| 411 | + | return _c | |||
| 412 | + | } | |||
M internal/adapters/wayland/syncobj_test.go +38 -0
| @@ -31,6 +31,7 @@ func TestSyncobjStructSizes(t *testing.T) { | |||
| 31 | 31 | {"handle_to_fd", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjHandleToFD}, | |
| 32 | 32 | {"fd_to_handle", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjFDToHandle}, | |
| 33 | 33 | {"timeline_signal", unsafe.Sizeof(syncobjTimelineArray{}), ioctlSyncobjTimelineSignal}, | |
| 34 | + | {"timeline_wait", unsafe.Sizeof(syncobjTimelineWait{}), ioctlSyncobjTimelineWait}, | |
| 34 | 35 | {"eventfd", unsafe.Sizeof(syncobjEventfd{}), ioctlSyncobjEventfd}, | |
| 35 | 36 | {"get_cap", unsafe.Sizeof(drmCap{}), ioctlGetCap}, | |
| 36 | 37 | } { | |
| @@ -66,6 +67,7 @@ func newSyncHarness(t *testing.T) *syncHarness { | |||
| 66 | 67 | h.dev = newMocksyncobjDevice(t) | |
| 67 | 68 | h.dev.EXPECT().fdToHandle(mock.Anything).Return(7, nil).Maybe() | |
| 68 | 69 | h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe() | |
| 70 | + | h.dev.EXPECT().signalled(uint32(7), mock.Anything).Return(false, nil).Maybe() | |
| 69 | 71 | h.dev.EXPECT().eventfd(uint32(7), mock.Anything, mock.Anything).RunAndReturn(func(_ uint32, point uint64, efd int) error { | |
| 70 | 72 | // Keep a duplicate: the waiter closes its own when it fires. | |
| 71 | 73 | dup, err := unix.Dup(efd) | |
| @@ -355,6 +357,42 @@ func TestQueuedCommitReadinessGatesInOrder(t *testing.T) { | |||
| 355 | 357 | } | |
| 356 | 358 | } | |
| 357 | 359 | ||
| 360 | + | // A destroyed queued buffer must not hold later commits behind its acquire. | |
| 361 | + | // Its release point cannot be advanced before the acquire is ready. | |
| 362 | + | func TestSyncobjDestroyedQueuedAcquire(t *testing.T) { | |
| 363 | + | h := newSyncHarness(t) | |
| 364 | + | h.releases = map[uint32]*syncReleaseProxy{} | |
| 365 | + | first, dead, next := h.dmabuf(), h.dmabuf(), h.dmabuf() | |
| 366 | + | h.commit(first, 1, 2) | |
| 367 | + | h.fire(1) | |
| 368 | + | if _, ok := h.content(2 * time.Second); !ok { | |
| 369 | + | t.Fatal("initial content missing") | |
| 370 | + | } | |
| 371 | + | fm := bindProtocol(t, h.c, "wp_fifo_manager_v1") | |
| 372 | + | f := h.c.AllocateID() | |
| 373 | + | registerProtocol(t, h.c, f) | |
| 374 | + | requestProtocol(t, h.c, fm, fifo.WpFifoManagerV1RequestGetFifo, f, h.surf) | |
| 375 | + | requestProtocol(t, h.c, f, fifo.WpFifoV1RequestSetBarrier) | |
| 376 | + | h.commit(dead, 3, 4) | |
| 377 | + | requestProtocol(t, h.c, dead, wayland.BufferRequestDestroy) | |
| 378 | + | if err := h.c.Roundtrip(); err != nil { | |
| 379 | + | t.Fatal(err) | |
| 380 | + | } | |
| 381 | + | // The dead attachment is skipped; it does not replace the current one. | |
| 382 | + | // A later attachment is queued while the old fence remains unsignalled. | |
| 383 | + | h.commit(next, 5, 6) | |
| 384 | + | h.fire(5) | |
| 385 | + | got, ok := h.content(2 * time.Second) | |
| 386 | + | if !ok || got.DMABuf == nil { | |
| 387 | + | t.Fatalf("later content blocked: %+v", got) | |
| 388 | + | } | |
| 389 | + | for _, p := range h.signalled() { | |
| 390 | + | if p == 4 { | |
| 391 | + | t.Fatal("skipped buffer release signalled before acquire") | |
| 392 | + | } | |
| 393 | + | } | |
| 394 | + | } | |
| 395 | + | ||
| 358 | 396 | func TestSyncobjProtocolErrors(t *testing.T) { | |
| 359 | 397 | for _, tc := range []struct { | |
| 360 | 398 | name string | |
| @@ -31,6 +31,7 @@ func TestSyncobjStructSizes(t *testing.T) { | |||||
| 31 | {"handle_to_fd", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjHandleToFD}, | 31 | {"handle_to_fd", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjHandleToFD}, | ||
| 32 | {"fd_to_handle", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjFDToHandle}, | 32 | {"fd_to_handle", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjFDToHandle}, | ||
| 33 | {"timeline_signal", unsafe.Sizeof(syncobjTimelineArray{}), ioctlSyncobjTimelineSignal}, | 33 | {"timeline_signal", unsafe.Sizeof(syncobjTimelineArray{}), ioctlSyncobjTimelineSignal}, | ||
| 34 | + | {"timeline_wait", unsafe.Sizeof(syncobjTimelineWait{}), ioctlSyncobjTimelineWait}, | |||
| 34 | {"eventfd", unsafe.Sizeof(syncobjEventfd{}), ioctlSyncobjEventfd}, | 35 | {"eventfd", unsafe.Sizeof(syncobjEventfd{}), ioctlSyncobjEventfd}, | ||
| 35 | {"get_cap", unsafe.Sizeof(drmCap{}), ioctlGetCap}, | 36 | {"get_cap", unsafe.Sizeof(drmCap{}), ioctlGetCap}, | ||
| 36 | } { | 37 | } { | ||
| @@ -66,6 +67,7 @@ func newSyncHarness(t *testing.T) *syncHarness { | |||||
| 66 | h.dev = newMocksyncobjDevice(t) | 67 | h.dev = newMocksyncobjDevice(t) | ||
| 67 | h.dev.EXPECT().fdToHandle(mock.Anything).Return(7, nil).Maybe() | 68 | h.dev.EXPECT().fdToHandle(mock.Anything).Return(7, nil).Maybe() | ||
| 68 | h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe() | 69 | h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe() | ||
| 70 | + | h.dev.EXPECT().signalled(uint32(7), mock.Anything).Return(false, nil).Maybe() | |||
| 69 | h.dev.EXPECT().eventfd(uint32(7), mock.Anything, mock.Anything).RunAndReturn(func(_ uint32, point uint64, efd int) error { | 71 | h.dev.EXPECT().eventfd(uint32(7), mock.Anything, mock.Anything).RunAndReturn(func(_ uint32, point uint64, efd int) error { | ||
| 70 | // Keep a duplicate: the waiter closes its own when it fires. | 72 | // Keep a duplicate: the waiter closes its own when it fires. | ||
| 71 | dup, err := unix.Dup(efd) | 73 | dup, err := unix.Dup(efd) | ||
| @@ -355,6 +357,42 @@ func TestQueuedCommitReadinessGatesInOrder(t *testing.T) { | |||||
| 355 | } | 357 | } | ||
| 356 | } | 358 | } | ||
| 357 | 359 | ||||
| 360 | + | // A destroyed queued buffer must not hold later commits behind its acquire. | |||
| 361 | + | // Its release point cannot be advanced before the acquire is ready. | |||
| 362 | + | func TestSyncobjDestroyedQueuedAcquire(t *testing.T) { | |||
| 363 | + | h := newSyncHarness(t) | |||
| 364 | + | h.releases = map[uint32]*syncReleaseProxy{} | |||
| 365 | + | first, dead, next := h.dmabuf(), h.dmabuf(), h.dmabuf() | |||
| 366 | + | h.commit(first, 1, 2) | |||
| 367 | + | h.fire(1) | |||
| 368 | + | if _, ok := h.content(2 * time.Second); !ok { | |||
| 369 | + | t.Fatal("initial content missing") | |||
| 370 | + | } | |||
| 371 | + | fm := bindProtocol(t, h.c, "wp_fifo_manager_v1") | |||
| 372 | + | f := h.c.AllocateID() | |||
| 373 | + | registerProtocol(t, h.c, f) | |||
| 374 | + | requestProtocol(t, h.c, fm, fifo.WpFifoManagerV1RequestGetFifo, f, h.surf) | |||
| 375 | + | requestProtocol(t, h.c, f, fifo.WpFifoV1RequestSetBarrier) | |||
| 376 | + | h.commit(dead, 3, 4) | |||
| 377 | + | requestProtocol(t, h.c, dead, wayland.BufferRequestDestroy) | |||
| 378 | + | if err := h.c.Roundtrip(); err != nil { | |||
| 379 | + | t.Fatal(err) | |||
| 380 | + | } | |||
| 381 | + | // The dead attachment is skipped; it does not replace the current one. | |||
| 382 | + | // A later attachment is queued while the old fence remains unsignalled. | |||
| 383 | + | h.commit(next, 5, 6) | |||
| 384 | + | h.fire(5) | |||
| 385 | + | got, ok := h.content(2 * time.Second) | |||
| 386 | + | if !ok || got.DMABuf == nil { | |||
| 387 | + | t.Fatalf("later content blocked: %+v", got) | |||
| 388 | + | } | |||
| 389 | + | for _, p := range h.signalled() { | |||
| 390 | + | if p == 4 { | |||
| 391 | + | t.Fatal("skipped buffer release signalled before acquire") | |||
| 392 | + | } | |||
| 393 | + | } | |||
| 394 | + | } | |||
| 395 | + | ||||
| 358 | func TestSyncobjProtocolErrors(t *testing.T) { | 396 | func TestSyncobjProtocolErrors(t *testing.T) { | ||
| 359 | for _, tc := range []struct { | 397 | for _, tc := range []struct { | ||
| 360 | name string | 398 | name string | ||
A internal/adapters/wayland/syncwait_test.go +127 -0
| @@ -0,0 +1,127 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "context" | |
| 6 | + | "math" | |
| 7 | + | "os" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/neferwl/internal/ports" | |
| 12 | + | "github.com/bnema/zerowrap" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | func TestSyncWaitAlreadySignalled(t *testing.T) { | |
| 16 | + | dev := newMocksyncobjDevice(t) | |
| 17 | + | dev.EXPECT().signalled(uint32(7), uint64(4)).Return(true, nil).Once() | |
| 18 | + | sw, err := newSyncWaiter(func() {}) | |
| 19 | + | if err != nil { | |
| 20 | + | t.Fatal(err) | |
| 21 | + | } | |
| 22 | + | defer sw.pipeR.Close() | |
| 23 | + | defer sw.close() | |
| 24 | + | w, err := sw.watch(syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 4}) | |
| 25 | + | if err != nil || !sw.fired(w) { | |
| 26 | + | t.Fatalf("wait: %v, ready: %v", err, sw.fired(w)) | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // An invalid poll result is not readiness; it must not hold the surface. | |
| 31 | + | func TestImplicitPollErrorReadyAndWarnOnce(t *testing.T) { | |
| 32 | + | sw, err := newSyncWaiter(func() {}) | |
| 33 | + | if err != nil { | |
| 34 | + | t.Fatal(err) | |
| 35 | + | } | |
| 36 | + | defer sw.pipeR.Close() | |
| 37 | + | defer sw.close() | |
| 38 | + | var output bytes.Buffer | |
| 39 | + | sw.log = zerowrap.New(zerowrap.Config{Output: &output}) | |
| 40 | + | // An fd number no process can own polls as POLLNVAL; its finalizer | |
| 41 | + | // cannot close a reused descriptor. | |
| 42 | + | b := &ports.DMABuf{Planes: []ports.DMABufPlane{{File: os.NewFile(uintptr(math.MaxInt32), "invalid")}}} | |
| 43 | + | var waits [4]*syncWait | |
| 44 | + | for range 2 { | |
| 45 | + | if err := sw.watchImplicit(b, &waits); err != nil || waits[0] != nil { | |
| 46 | + | t.Fatalf("poll error should not register wait: %v %+v", err, waits) | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | if n := bytes.Count(output.Bytes(), []byte("implicit fence poll error")); n != 1 { | |
| 50 | + | t.Fatalf("poll warnings %d: %s", n, output.String()) | |
| 51 | + | } | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // POLLHUP with no POLLIN on a watched fd must wake the surface too. | |
| 55 | + | func TestImplicitPollHangupWakes(t *testing.T) { | |
| 56 | + | sw, err := newSyncWaiter(func() {}) | |
| 57 | + | if err != nil { | |
| 58 | + | t.Fatal(err) | |
| 59 | + | } | |
| 60 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 61 | + | done := make(chan struct{}) | |
| 62 | + | go func() { sw.run(ctx); close(done) }() | |
| 63 | + | defer func() { cancel(); sw.close(); <-done }() | |
| 64 | + | r, wr, err := os.Pipe() | |
| 65 | + | if err != nil { | |
| 66 | + | t.Fatal(err) | |
| 67 | + | } | |
| 68 | + | defer r.Close() | |
| 69 | + | b := &ports.DMABuf{Planes: []ports.DMABufPlane{{File: r}}} | |
| 70 | + | var waits [4]*syncWait | |
| 71 | + | if err := sw.watchImplicit(b, &waits); err != nil || waits[0] == nil { | |
| 72 | + | t.Fatalf("watch: %v", err) | |
| 73 | + | } | |
| 74 | + | wr.Close() | |
| 75 | + | deadline := time.Now().Add(time.Second) | |
| 76 | + | for !sw.fired(waits[0]) && time.Now().Before(deadline) { | |
| 77 | + | time.Sleep(time.Millisecond) | |
| 78 | + | } | |
| 79 | + | if !sw.fired(waits[0]) { | |
| 80 | + | t.Fatal("hangup never woke waiter") | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | func TestImplicitWaitAndDrop(t *testing.T) { | |
| 85 | + | sw, err := newSyncWaiter(func() {}) | |
| 86 | + | if err != nil { | |
| 87 | + | t.Fatal(err) | |
| 88 | + | } | |
| 89 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 90 | + | done := make(chan struct{}) | |
| 91 | + | go func() { sw.run(ctx); close(done) }() | |
| 92 | + | defer func() { cancel(); sw.close(); <-done }() | |
| 93 | + | r, wr, err := os.Pipe() | |
| 94 | + | if err != nil { | |
| 95 | + | t.Fatal(err) | |
| 96 | + | } | |
| 97 | + | defer r.Close() | |
| 98 | + | defer wr.Close() | |
| 99 | + | b := &ports.DMABuf{Planes: []ports.DMABufPlane{{File: r}}} | |
| 100 | + | var waits [4]*syncWait | |
| 101 | + | err = sw.watchImplicit(b, &waits) | |
| 102 | + | if err != nil || waits[0] == nil || sw.fired(waits[0]) { | |
| 103 | + | t.Fatalf("not ready: %v", err) | |
| 104 | + | } | |
| 105 | + | sw.cancel(waits[0]) | |
| 106 | + | waits = [4]*syncWait{} | |
| 107 | + | err = sw.watchImplicit(b, &waits) | |
| 108 | + | if err != nil || waits[0] == nil || sw.fired(waits[0]) { | |
| 109 | + | t.Fatalf("not ready: %v", err) | |
| 110 | + | } | |
| 111 | + | if _, err := wr.Write([]byte{1}); err != nil { | |
| 112 | + | t.Fatal(err) | |
| 113 | + | } | |
| 114 | + | deadline := time.Now().Add(time.Second) | |
| 115 | + | for !sw.fired(waits[0]) && time.Now().Before(deadline) { | |
| 116 | + | time.Sleep(time.Millisecond) | |
| 117 | + | } | |
| 118 | + | if !sw.fired(waits[0]) { | |
| 119 | + | t.Fatal("readable buffer never became ready") | |
| 120 | + | } | |
| 121 | + | // A newly attached readable buffer is ready without a poll goroutine hop. | |
| 122 | + | waits = [4]*syncWait{} | |
| 123 | + | err = sw.watchImplicit(b, &waits) | |
| 124 | + | if err != nil || waits[0] != nil { | |
| 125 | + | t.Fatalf("ready: %v, waits: %+v", err, waits) | |
| 126 | + | } | |
| 127 | + | } | |
| @@ -0,0 +1,127 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "context" | |||
| 6 | + | "math" | |||
| 7 | + | "os" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 12 | + | "github.com/bnema/zerowrap" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | func TestSyncWaitAlreadySignalled(t *testing.T) { | |||
| 16 | + | dev := newMocksyncobjDevice(t) | |||
| 17 | + | dev.EXPECT().signalled(uint32(7), uint64(4)).Return(true, nil).Once() | |||
| 18 | + | sw, err := newSyncWaiter(func() {}) | |||
| 19 | + | if err != nil { | |||
| 20 | + | t.Fatal(err) | |||
| 21 | + | } | |||
| 22 | + | defer sw.pipeR.Close() | |||
| 23 | + | defer sw.close() | |||
| 24 | + | w, err := sw.watch(syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 4}) | |||
| 25 | + | if err != nil || !sw.fired(w) { | |||
| 26 | + | t.Fatalf("wait: %v, ready: %v", err, sw.fired(w)) | |||
| 27 | + | } | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // An invalid poll result is not readiness; it must not hold the surface. | |||
| 31 | + | func TestImplicitPollErrorReadyAndWarnOnce(t *testing.T) { | |||
| 32 | + | sw, err := newSyncWaiter(func() {}) | |||
| 33 | + | if err != nil { | |||
| 34 | + | t.Fatal(err) | |||
| 35 | + | } | |||
| 36 | + | defer sw.pipeR.Close() | |||
| 37 | + | defer sw.close() | |||
| 38 | + | var output bytes.Buffer | |||
| 39 | + | sw.log = zerowrap.New(zerowrap.Config{Output: &output}) | |||
| 40 | + | // An fd number no process can own polls as POLLNVAL; its finalizer | |||
| 41 | + | // cannot close a reused descriptor. | |||
| 42 | + | b := &ports.DMABuf{Planes: []ports.DMABufPlane{{File: os.NewFile(uintptr(math.MaxInt32), "invalid")}}} | |||
| 43 | + | var waits [4]*syncWait | |||
| 44 | + | for range 2 { | |||
| 45 | + | if err := sw.watchImplicit(b, &waits); err != nil || waits[0] != nil { | |||
| 46 | + | t.Fatalf("poll error should not register wait: %v %+v", err, waits) | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | if n := bytes.Count(output.Bytes(), []byte("implicit fence poll error")); n != 1 { | |||
| 50 | + | t.Fatalf("poll warnings %d: %s", n, output.String()) | |||
| 51 | + | } | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // POLLHUP with no POLLIN on a watched fd must wake the surface too. | |||
| 55 | + | func TestImplicitPollHangupWakes(t *testing.T) { | |||
| 56 | + | sw, err := newSyncWaiter(func() {}) | |||
| 57 | + | if err != nil { | |||
| 58 | + | t.Fatal(err) | |||
| 59 | + | } | |||
| 60 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 61 | + | done := make(chan struct{}) | |||
| 62 | + | go func() { sw.run(ctx); close(done) }() | |||
| 63 | + | defer func() { cancel(); sw.close(); <-done }() | |||
| 64 | + | r, wr, err := os.Pipe() | |||
| 65 | + | if err != nil { | |||
| 66 | + | t.Fatal(err) | |||
| 67 | + | } | |||
| 68 | + | defer r.Close() | |||
| 69 | + | b := &ports.DMABuf{Planes: []ports.DMABufPlane{{File: r}}} | |||
| 70 | + | var waits [4]*syncWait | |||
| 71 | + | if err := sw.watchImplicit(b, &waits); err != nil || waits[0] == nil { | |||
| 72 | + | t.Fatalf("watch: %v", err) | |||
| 73 | + | } | |||
| 74 | + | wr.Close() | |||
| 75 | + | deadline := time.Now().Add(time.Second) | |||
| 76 | + | for !sw.fired(waits[0]) && time.Now().Before(deadline) { | |||
| 77 | + | time.Sleep(time.Millisecond) | |||
| 78 | + | } | |||
| 79 | + | if !sw.fired(waits[0]) { | |||
| 80 | + | t.Fatal("hangup never woke waiter") | |||
| 81 | + | } | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | func TestImplicitWaitAndDrop(t *testing.T) { | |||
| 85 | + | sw, err := newSyncWaiter(func() {}) | |||
| 86 | + | if err != nil { | |||
| 87 | + | t.Fatal(err) | |||
| 88 | + | } | |||
| 89 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 90 | + | done := make(chan struct{}) | |||
| 91 | + | go func() { sw.run(ctx); close(done) }() | |||
| 92 | + | defer func() { cancel(); sw.close(); <-done }() | |||
| 93 | + | r, wr, err := os.Pipe() | |||
| 94 | + | if err != nil { | |||
| 95 | + | t.Fatal(err) | |||
| 96 | + | } | |||
| 97 | + | defer r.Close() | |||
| 98 | + | defer wr.Close() | |||
| 99 | + | b := &ports.DMABuf{Planes: []ports.DMABufPlane{{File: r}}} | |||
| 100 | + | var waits [4]*syncWait | |||
| 101 | + | err = sw.watchImplicit(b, &waits) | |||
| 102 | + | if err != nil || waits[0] == nil || sw.fired(waits[0]) { | |||
| 103 | + | t.Fatalf("not ready: %v", err) | |||
| 104 | + | } | |||
| 105 | + | sw.cancel(waits[0]) | |||
| 106 | + | waits = [4]*syncWait{} | |||
| 107 | + | err = sw.watchImplicit(b, &waits) | |||
| 108 | + | if err != nil || waits[0] == nil || sw.fired(waits[0]) { | |||
| 109 | + | t.Fatalf("not ready: %v", err) | |||
| 110 | + | } | |||
| 111 | + | if _, err := wr.Write([]byte{1}); err != nil { | |||
| 112 | + | t.Fatal(err) | |||
| 113 | + | } | |||
| 114 | + | deadline := time.Now().Add(time.Second) | |||
| 115 | + | for !sw.fired(waits[0]) && time.Now().Before(deadline) { | |||
| 116 | + | time.Sleep(time.Millisecond) | |||
| 117 | + | } | |||
| 118 | + | if !sw.fired(waits[0]) { | |||
| 119 | + | t.Fatal("readable buffer never became ready") | |||
| 120 | + | } | |||
| 121 | + | // A newly attached readable buffer is ready without a poll goroutine hop. | |||
| 122 | + | waits = [4]*syncWait{} | |||
| 123 | + | err = sw.watchImplicit(b, &waits) | |||
| 124 | + | if err != nil || waits[0] != nil { | |||
| 125 | + | t.Fatalf("ready: %v, waits: %+v", err, waits) | |||
| 126 | + | } | |||
| 127 | + | } | |||
M internal/app/app.go +11 -0
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | 8 | "fmt" | |
| 9 | 9 | "io" | |
| 10 | 10 | "os" | |
| 11 | + | "runtime" | |
| 11 | 12 | "sync" | |
| 12 | 13 | "time" | |
| 13 | 14 | ||
| @@ -17,6 +18,7 @@ import ( | |||
| 17 | 18 | "github.com/bnema/neferwl/internal/adapters/headlessinput" | |
| 18 | 19 | "github.com/bnema/neferwl/internal/adapters/launcher" | |
| 19 | 20 | "github.com/bnema/neferwl/internal/adapters/libinput" | |
| 21 | + | "github.com/bnema/neferwl/internal/adapters/sched" | |
| 20 | 22 | "github.com/bnema/neferwl/internal/adapters/statefile" | |
| 21 | 23 | "github.com/bnema/neferwl/internal/adapters/vulkan" | |
| 22 | 24 | "github.com/bnema/neferwl/internal/adapters/wayland" | |
| @@ -189,6 +191,13 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 189 | 191 | return | |
| 190 | 192 | } | |
| 191 | 193 | done <- safe("input", func() error { | |
| 194 | + | if opts.Config.Performance.Realtime { | |
| 195 | + | runtime.LockOSThread() | |
| 196 | + | // Let this dedicated thread exit with its scheduling policy. | |
| 197 | + | if err := sched.Realtime(logging.For(ctx, "sched")); err != nil { | |
| 198 | + | log.Warn().Str("component", "sched").Err(err).Msg("input scheduling") | |
| 199 | + | } | |
| 200 | + | } | |
| 192 | 201 | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Touchpad: opts.Config.Touchpad, Touchpads: touchpads, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input"), LogMotion: logging.Enabled(ctx, "input-motion")}, input) | |
| 193 | 202 | }) | |
| 194 | 203 | return | |
| @@ -240,11 +249,13 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 240 | 249 | defer workers.Done() | |
| 241 | 250 | outputIO := outputChannels{events: output, scenes: renderScenes, contents: contents, cursorChanges: cursorChanges, presented: flips, captures: captures, captured: captured, formats: outputFormats, heads: outputHeads, reloads: filtered, configured: configChanges, requests: applyOutput, replies: appliedOutput} | |
| 242 | 251 | apply := newOutputApply(newOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app")) | |
| 252 | + | renderLog := logging.For(ctx, "render") | |
| 243 | 253 | newRenderer := func(w, h int) (ports.Renderer, error) { | |
| 244 | 254 | r, err := vulkan.New(w, h) | |
| 245 | 255 | if err != nil { | |
| 246 | 256 | return nil, err | |
| 247 | 257 | } | |
| 258 | + | renderLog.Info().Str("queue_priority", r.QueuePriority()).Msg("vulkan queue") | |
| 248 | 259 | return r, nil | |
| 249 | 260 | } | |
| 250 | 261 | if hw != nil { | |
| @@ -8,6 +8,7 @@ import ( | |||||
| 8 | "fmt" | 8 | "fmt" | ||
| 9 | "io" | 9 | "io" | ||
| 10 | "os" | 10 | "os" | ||
| 11 | + | "runtime" | |||
| 11 | "sync" | 12 | "sync" | ||
| 12 | "time" | 13 | "time" | ||
| 13 | 14 | ||||
| @@ -17,6 +18,7 @@ import ( | |||||
| 17 | "github.com/bnema/neferwl/internal/adapters/headlessinput" | 18 | "github.com/bnema/neferwl/internal/adapters/headlessinput" | ||
| 18 | "github.com/bnema/neferwl/internal/adapters/launcher" | 19 | "github.com/bnema/neferwl/internal/adapters/launcher" | ||
| 19 | "github.com/bnema/neferwl/internal/adapters/libinput" | 20 | "github.com/bnema/neferwl/internal/adapters/libinput" | ||
| 21 | + | "github.com/bnema/neferwl/internal/adapters/sched" | |||
| 20 | "github.com/bnema/neferwl/internal/adapters/statefile" | 22 | "github.com/bnema/neferwl/internal/adapters/statefile" | ||
| 21 | "github.com/bnema/neferwl/internal/adapters/vulkan" | 23 | "github.com/bnema/neferwl/internal/adapters/vulkan" | ||
| 22 | "github.com/bnema/neferwl/internal/adapters/wayland" | 24 | "github.com/bnema/neferwl/internal/adapters/wayland" | ||
| @@ -189,6 +191,13 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 189 | return | 191 | return | ||
| 190 | } | 192 | } | ||
| 191 | done <- safe("input", func() error { | 193 | done <- safe("input", func() error { | ||
| 194 | + | if opts.Config.Performance.Realtime { | |||
| 195 | + | runtime.LockOSThread() | |||
| 196 | + | // Let this dedicated thread exit with its scheduling policy. | |||
| 197 | + | if err := sched.Realtime(logging.For(ctx, "sched")); err != nil { | |||
| 198 | + | log.Warn().Str("component", "sched").Err(err).Msg("input scheduling") | |||
| 199 | + | } | |||
| 200 | + | } | |||
| 192 | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Touchpad: opts.Config.Touchpad, Touchpads: touchpads, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input"), LogMotion: logging.Enabled(ctx, "input-motion")}, input) | 201 | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Touchpad: opts.Config.Touchpad, Touchpads: touchpads, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input"), LogMotion: logging.Enabled(ctx, "input-motion")}, input) | ||
| 193 | }) | 202 | }) | ||
| 194 | return | 203 | return | ||
| @@ -240,11 +249,13 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 240 | defer workers.Done() | 249 | defer workers.Done() | ||
| 241 | outputIO := outputChannels{events: output, scenes: renderScenes, contents: contents, cursorChanges: cursorChanges, presented: flips, captures: captures, captured: captured, formats: outputFormats, heads: outputHeads, reloads: filtered, configured: configChanges, requests: applyOutput, replies: appliedOutput} | 250 | outputIO := outputChannels{events: output, scenes: renderScenes, contents: contents, cursorChanges: cursorChanges, presented: flips, captures: captures, captured: captured, formats: outputFormats, heads: outputHeads, reloads: filtered, configured: configChanges, requests: applyOutput, replies: appliedOutput} | ||
| 242 | apply := newOutputApply(newOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app")) | 251 | apply := newOutputApply(newOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app")) | ||
| 252 | + | renderLog := logging.For(ctx, "render") | |||
| 243 | newRenderer := func(w, h int) (ports.Renderer, error) { | 253 | newRenderer := func(w, h int) (ports.Renderer, error) { | ||
| 244 | r, err := vulkan.New(w, h) | 254 | r, err := vulkan.New(w, h) | ||
| 245 | if err != nil { | 255 | if err != nil { | ||
| 246 | return nil, err | 256 | return nil, err | ||
| 247 | } | 257 | } | ||
| 258 | + | renderLog.Info().Str("queue_priority", r.QueuePriority()).Msg("vulkan queue") | |||
| 248 | return r, nil | 259 | return r, nil | ||
| 249 | } | 260 | } | ||
| 250 | if hw != nil { | 261 | if hw != nil { | ||
M internal/app/drm.go +11 -0
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | 7 | "path/filepath" | |
| 8 | + | "runtime" | |
| 8 | 9 | "runtime/debug" | |
| 9 | 10 | "sort" | |
| 10 | 11 | "sync" | |
| @@ -13,6 +14,7 @@ import ( | |||
| 13 | 14 | "github.com/bnema/neferwl/internal/adapters/clock" | |
| 14 | 15 | "github.com/bnema/neferwl/internal/adapters/config" | |
| 15 | 16 | "github.com/bnema/neferwl/internal/adapters/drm" | |
| 17 | + | "github.com/bnema/neferwl/internal/adapters/sched" | |
| 16 | 18 | "github.com/bnema/neferwl/internal/adapters/seat" | |
| 17 | 19 | "github.com/bnema/neferwl/internal/logging" | |
| 18 | 20 | "github.com/bnema/neferwl/internal/ports" | |
| @@ -274,10 +276,19 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||
| 274 | 276 | curs.set(name, cur) | |
| 275 | 277 | } | |
| 276 | 278 | active := b.seat.Subscribe() | |
| 279 | + | realtime := currentConfig.Performance.Realtime | |
| 277 | 280 | set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan ports.SurfaceContent, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest) error { | |
| 278 | 281 | defer b.seat.Unsubscribe(active) | |
| 279 | 282 | return safe("output "+name, func() error { | |
| 280 | 283 | defer o.Close() | |
| 284 | + | if realtime { | |
| 285 | + | runtime.LockOSThread() | |
| 286 | + | // Keep the thread locked until this goroutine exits: an RT | |
| 287 | + | // thread must not return to Go's general-purpose pool. | |
| 288 | + | if err := sched.Realtime(log); err != nil { | |
| 289 | + | log.Warn().Str("component", "sched").Err(err).Msg("output scheduling") | |
| 290 | + | } | |
| 291 | + | } | |
| 281 | 292 | return o.Run(octx, newRenderer, loadCursor, active, sc, cc, cu, ch.presented, cap, ch.captured) | |
| 282 | 293 | }) | |
| 283 | 294 | }) | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "path/filepath" | 7 | "path/filepath" | ||
| 8 | + | "runtime" | |||
| 8 | "runtime/debug" | 9 | "runtime/debug" | ||
| 9 | "sort" | 10 | "sort" | ||
| 10 | "sync" | 11 | "sync" | ||
| @@ -13,6 +14,7 @@ import ( | |||||
| 13 | "github.com/bnema/neferwl/internal/adapters/clock" | 14 | "github.com/bnema/neferwl/internal/adapters/clock" | ||
| 14 | "github.com/bnema/neferwl/internal/adapters/config" | 15 | "github.com/bnema/neferwl/internal/adapters/config" | ||
| 15 | "github.com/bnema/neferwl/internal/adapters/drm" | 16 | "github.com/bnema/neferwl/internal/adapters/drm" | ||
| 17 | + | "github.com/bnema/neferwl/internal/adapters/sched" | |||
| 16 | "github.com/bnema/neferwl/internal/adapters/seat" | 18 | "github.com/bnema/neferwl/internal/adapters/seat" | ||
| 17 | "github.com/bnema/neferwl/internal/logging" | 19 | "github.com/bnema/neferwl/internal/logging" | ||
| 18 | "github.com/bnema/neferwl/internal/ports" | 20 | "github.com/bnema/neferwl/internal/ports" | ||
| @@ -274,10 +276,19 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func(ports.Config) drm | |||||
| 274 | curs.set(name, cur) | 276 | curs.set(name, cur) | ||
| 275 | } | 277 | } | ||
| 276 | active := b.seat.Subscribe() | 278 | active := b.seat.Subscribe() | ||
| 279 | + | realtime := currentConfig.Performance.Realtime | |||
| 277 | set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan ports.SurfaceContent, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest) error { | 280 | set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan ports.SurfaceContent, cu <-chan ports.CursorChange, cap <-chan ports.CaptureRequest) error { | ||
| 278 | defer b.seat.Unsubscribe(active) | 281 | defer b.seat.Unsubscribe(active) | ||
| 279 | return safe("output "+name, func() error { | 282 | return safe("output "+name, func() error { | ||
| 280 | defer o.Close() | 283 | defer o.Close() | ||
| 284 | + | if realtime { | |||
| 285 | + | runtime.LockOSThread() | |||
| 286 | + | // Keep the thread locked until this goroutine exits: an RT | |||
| 287 | + | // thread must not return to Go's general-purpose pool. | |||
| 288 | + | if err := sched.Realtime(log); err != nil { | |||
| 289 | + | log.Warn().Str("component", "sched").Err(err).Msg("output scheduling") | |||
| 290 | + | } | |||
| 291 | + | } | |||
| 281 | return o.Run(octx, newRenderer, loadCursor, active, sc, cc, cu, ch.presented, cap, ch.captured) | 292 | return o.Run(octx, newRenderer, loadCursor, active, sc, cc, cu, ch.presented, cap, ch.captured) | ||
| 282 | }) | 293 | }) | ||
| 283 | }) | 294 | }) | ||
M internal/ports/config.go +2 -1
| @@ -114,7 +114,8 @@ type Config struct { | |||
| 114 | 114 | // turns variable refresh on while a buffer is scanned out. | |
| 115 | 115 | Tearing, VRR bool | |
| 116 | 116 | } | |
| 117 | - | Log struct { | |
| 117 | + | Performance struct{ Realtime bool } | |
| 118 | + | Log struct { | |
| 118 | 119 | Level string | |
| 119 | 120 | Debug []string | |
| 120 | 121 | } | |
| @@ -114,7 +114,8 @@ type Config struct { | |||||
| 114 | // turns variable refresh on while a buffer is scanned out. | 114 | // turns variable refresh on while a buffer is scanned out. | ||
| 115 | Tearing, VRR bool | 115 | Tearing, VRR bool | ||
| 116 | } | 116 | } | ||
| 117 | - | Log struct { | 117 | + | Performance struct{ Realtime bool } |
| 118 | + | Log struct { | |||
| 118 | Level string | 119 | Level string | ||
| 119 | Debug []string | 120 | Debug []string | ||
| 120 | } | 121 | } | ||
M internal/ports/messages.go +12 -1
| @@ -845,6 +845,16 @@ func (c SurfaceColor) IsExtendedLinear() bool { | |||
| 845 | 845 | return c.TF == ColorTFExtendedLinear && c.Primaries == ColorPrimariesSRGB | |
| 846 | 846 | } | |
| 847 | 847 | ||
| 848 | + | // ContentType is the committed wp_content_type_v1 hint. | |
| 849 | + | type ContentType uint8 | |
| 850 | + | ||
| 851 | + | const ( | |
| 852 | + | ContentNone ContentType = iota | |
| 853 | + | ContentPhoto | |
| 854 | + | ContentVideo | |
| 855 | + | ContentGame | |
| 856 | + | ) | |
| 857 | + | ||
| 848 | 858 | // SurfaceContent carries wayland → output the latest committed content of a | |
| 849 | 859 | // window: SHM, a client shared-memory buffer, or DMABuf, a GPU buffer. | |
| 850 | 860 | // Renderers read both in place and never modify them. Empty means the window | |
| @@ -853,7 +863,8 @@ func (c SurfaceColor) IsExtendedLinear() bool { | |||
| 853 | 863 | // Geometry is the part of the surface that is the window (xdg window | |
| 854 | 864 | // geometry), the rest being client shadows. | |
| 855 | 865 | type SurfaceContent struct { | |
| 856 | - | ID WindowID | |
| 866 | + | ID WindowID | |
| 867 | + | ContentType ContentType | |
| 857 | 868 | // Surface is a never-reused wl_surface identity; Version identifies its | |
| 858 | 869 | // applied buffer content independently of the window publication Seq. | |
| 859 | 870 | Surface, Version uint64 | |
| @@ -845,6 +845,16 @@ func (c SurfaceColor) IsExtendedLinear() bool { | |||||
| 845 | return c.TF == ColorTFExtendedLinear && c.Primaries == ColorPrimariesSRGB | 845 | return c.TF == ColorTFExtendedLinear && c.Primaries == ColorPrimariesSRGB | ||
| 846 | } | 846 | } | ||
| 847 | 847 | ||||
| 848 | + | // ContentType is the committed wp_content_type_v1 hint. | |||
| 849 | + | type ContentType uint8 | |||
| 850 | + | ||||
| 851 | + | const ( | |||
| 852 | + | ContentNone ContentType = iota | |||
| 853 | + | ContentPhoto | |||
| 854 | + | ContentVideo | |||
| 855 | + | ContentGame | |||
| 856 | + | ) | |||
| 857 | + | ||||
| 848 | // SurfaceContent carries wayland → output the latest committed content of a | 858 | // SurfaceContent carries wayland → output the latest committed content of a | ||
| 849 | // window: SHM, a client shared-memory buffer, or DMABuf, a GPU buffer. | 859 | // window: SHM, a client shared-memory buffer, or DMABuf, a GPU buffer. | ||
| 850 | // Renderers read both in place and never modify them. Empty means the window | 860 | // Renderers read both in place and never modify them. Empty means the window | ||
| @@ -853,7 +863,8 @@ func (c SurfaceColor) IsExtendedLinear() bool { | |||||
| 853 | // Geometry is the part of the surface that is the window (xdg window | 863 | // Geometry is the part of the surface that is the window (xdg window | ||
| 854 | // geometry), the rest being client shadows. | 864 | // geometry), the rest being client shadows. | ||
| 855 | type SurfaceContent struct { | 865 | type SurfaceContent struct { | ||
| 856 | - | ID WindowID | 866 | + | ID WindowID |
| 867 | + | ContentType ContentType | |||
| 857 | // Surface is a never-reused wl_surface identity; Version identifies its | 868 | // Surface is a never-reused wl_surface identity; Version identifies its | ||
| 858 | // applied buffer content independently of the window publication Seq. | 869 | // applied buffer content independently of the window publication Seq. | ||
| 859 | Surface, Version uint64 | 870 | Surface, Version uint64 | ||