detached 14a7b2d

feat(gpu): render a cell grid into DMA-BUF images with raw Vulkan and DRM syncobj sync

commit
14a7b2da83af7f01e78f87fd7ddb945d65745e1f
author
brice · · 2026-10-05 14:08 UTC
parent
992a427
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
13 files · +1512 -0
A internal/glyph/glyph.go +135 -0
@@ -0,0 +1,135 @@
1+// Package glyph rasterizes single glyphs of a monospace font into fixed-size
2+// cell bitmaps (one byte of coverage per pixel).
3+package glyph
4+
5+import (
6+ "fmt"
7+ "image"
8+ "math"
9+ "os"
10+ "os/exec"
11+ "strings"
12+
13+ "golang.org/x/image/font"
14+ "golang.org/x/image/font/opentype"
15+ "golang.org/x/image/math/fixed"
16+)
17+
18+// Face rasterizes runes of one font at one pixel size. All glyphs are clipped
19+// to a CellW x CellH box. It is used from one goroutine.
20+type Face struct {
21+ regular, bold font.Face
22+ // CellW and CellH are the cell size in physical pixels.
23+ CellW, CellH int
24+ ascent fixed.Int26_6
25+}
26+
27+// FindFont asks fontconfig for the file of a family and weight.
28+func FindFont(family string, bold bool) (string, error) {
29+ pattern := family
30+ if bold {
31+ pattern += ":bold"
32+ }
33+ out, err := exec.Command("fc-match", "-f", "%{file}", pattern).Output()
34+ if err != nil {
35+ return "", fmt.Errorf("glyph: fc-match %q: %w", pattern, err)
36+ }
37+ path := strings.TrimSpace(string(out))
38+ if path == "" {
39+ return "", fmt.Errorf("glyph: no font file for %q", pattern)
40+ }
41+ return path, nil
42+}
43+
44+func loadFace(path string, px float64) (font.Face, error) {
45+ data, err := os.ReadFile(path)
46+ if err != nil {
47+ return nil, fmt.Errorf("glyph: %w", err)
48+ }
49+ f, err := opentype.Parse(data)
50+ if err != nil {
51+ return nil, fmt.Errorf("glyph: parse %s: %w", path, err)
52+ }
53+ return opentype.NewFace(f, &opentype.FaceOptions{Size: px, DPI: 72, Hinting: font.HintingFull})
54+}
55+
56+// Load opens a regular and an optional bold font file at px pixels. The cell
57+// size comes from the regular face: the advance of "M" and ascent plus descent.
58+func Load(regularPath, boldPath string, px float64) (*Face, error) {
59+ if px < 4 || px > 512 {
60+ return nil, fmt.Errorf("glyph: unsupported font size %.1f px", px)
61+ }
62+ f := &Face{}
63+ var err error
64+ if f.regular, err = loadFace(regularPath, px); err != nil {
65+ return nil, err
66+ }
67+ f.bold = f.regular
68+ if boldPath != "" && boldPath != regularPath {
69+ if f.bold, err = loadFace(boldPath, px); err != nil {
70+ f.regular.Close()
71+ return nil, err
72+ }
73+ }
74+ m := f.regular.Metrics()
75+ adv, ok := f.regular.GlyphAdvance('M')
76+ if !ok || adv <= 0 {
77+ f.Close()
78+ return nil, fmt.Errorf("glyph: font has no usable advance for 'M'")
79+ }
80+ f.CellW = int(math.Round(float64(adv) / 64))
81+ f.CellH = int(math.Ceil(float64(m.Ascent+m.Descent) / 64))
82+ f.ascent = m.Ascent
83+ if f.CellW < 1 || f.CellH < 1 {
84+ f.Close()
85+ return nil, fmt.Errorf("glyph: degenerate cell %dx%d", f.CellW, f.CellH)
86+ }
87+ return f, nil
88+}
89+
90+// Close releases the font faces.
91+func (f *Face) Close() {
92+ if f == nil {
93+ return
94+ }
95+ if f.bold != nil && f.bold != f.regular {
96+ f.bold.Close()
97+ }
98+ if f.regular != nil {
99+ f.regular.Close()
100+ }
101+ f.regular, f.bold = nil, nil
102+}
103+
104+// Rasterize draws r into dst, which must hold CellW*CellH bytes (stride
105+// CellW). Pixels outside the cell are clipped; a missing glyph leaves dst
106+// empty.
107+func (f *Face) Rasterize(r rune, bold bool, dst []byte) {
108+ clear(dst[:f.CellW*f.CellH])
109+ face := f.regular
110+ if bold {
111+ face = f.bold
112+ }
113+ dr, mask, maskp, _, ok := face.Glyph(fixed.Point26_6{X: 0, Y: f.ascent}, r)
114+ if !ok {
115+ return
116+ }
117+ alpha, isAlpha := mask.(*image.Alpha)
118+ for y := dr.Min.Y; y < dr.Max.Y; y++ {
119+ if y < 0 || y >= f.CellH {
120+ continue
121+ }
122+ for x := dr.Min.X; x < dr.Max.X; x++ {
123+ if x < 0 || x >= f.CellW {
124+ continue
125+ }
126+ mx, my := maskp.X+x-dr.Min.X, maskp.Y+y-dr.Min.Y
127+ if isAlpha {
128+ dst[y*f.CellW+x] = alpha.Pix[alpha.PixOffset(mx, my)]
129+ continue
130+ }
131+ _, _, _, a := mask.At(mx, my).RGBA()
132+ dst[y*f.CellW+x] = uint8(a >> 8)
133+ }
134+ }
135+}
@@ -0,0 +1,135 @@
1+// Package glyph rasterizes single glyphs of a monospace font into fixed-size
2+// cell bitmaps (one byte of coverage per pixel).
3+package glyph
4+
5+import (
6+ "fmt"
7+ "image"
8+ "math"
9+ "os"
10+ "os/exec"
11+ "strings"
12+
13+ "golang.org/x/image/font"
14+ "golang.org/x/image/font/opentype"
15+ "golang.org/x/image/math/fixed"
16+)
17+
18+// Face rasterizes runes of one font at one pixel size. All glyphs are clipped
19+// to a CellW x CellH box. It is used from one goroutine.
20+type Face struct {
21+ regular, bold font.Face
22+ // CellW and CellH are the cell size in physical pixels.
23+ CellW, CellH int
24+ ascent fixed.Int26_6
25+}
26+
27+// FindFont asks fontconfig for the file of a family and weight.
28+func FindFont(family string, bold bool) (string, error) {
29+ pattern := family
30+ if bold {
31+ pattern += ":bold"
32+ }
33+ out, err := exec.Command("fc-match", "-f", "%{file}", pattern).Output()
34+ if err != nil {
35+ return "", fmt.Errorf("glyph: fc-match %q: %w", pattern, err)
36+ }
37+ path := strings.TrimSpace(string(out))
38+ if path == "" {
39+ return "", fmt.Errorf("glyph: no font file for %q", pattern)
40+ }
41+ return path, nil
42+}
43+
44+func loadFace(path string, px float64) (font.Face, error) {
45+ data, err := os.ReadFile(path)
46+ if err != nil {
47+ return nil, fmt.Errorf("glyph: %w", err)
48+ }
49+ f, err := opentype.Parse(data)
50+ if err != nil {
51+ return nil, fmt.Errorf("glyph: parse %s: %w", path, err)
52+ }
53+ return opentype.NewFace(f, &opentype.FaceOptions{Size: px, DPI: 72, Hinting: font.HintingFull})
54+}
55+
56+// Load opens a regular and an optional bold font file at px pixels. The cell
57+// size comes from the regular face: the advance of "M" and ascent plus descent.
58+func Load(regularPath, boldPath string, px float64) (*Face, error) {
59+ if px < 4 || px > 512 {
60+ return nil, fmt.Errorf("glyph: unsupported font size %.1f px", px)
61+ }
62+ f := &Face{}
63+ var err error
64+ if f.regular, err = loadFace(regularPath, px); err != nil {
65+ return nil, err
66+ }
67+ f.bold = f.regular
68+ if boldPath != "" && boldPath != regularPath {
69+ if f.bold, err = loadFace(boldPath, px); err != nil {
70+ f.regular.Close()
71+ return nil, err
72+ }
73+ }
74+ m := f.regular.Metrics()
75+ adv, ok := f.regular.GlyphAdvance('M')
76+ if !ok || adv <= 0 {
77+ f.Close()
78+ return nil, fmt.Errorf("glyph: font has no usable advance for 'M'")
79+ }
80+ f.CellW = int(math.Round(float64(adv) / 64))
81+ f.CellH = int(math.Ceil(float64(m.Ascent+m.Descent) / 64))
82+ f.ascent = m.Ascent
83+ if f.CellW < 1 || f.CellH < 1 {
84+ f.Close()
85+ return nil, fmt.Errorf("glyph: degenerate cell %dx%d", f.CellW, f.CellH)
86+ }
87+ return f, nil
88+}
89+
90+// Close releases the font faces.
91+func (f *Face) Close() {
92+ if f == nil {
93+ return
94+ }
95+ if f.bold != nil && f.bold != f.regular {
96+ f.bold.Close()
97+ }
98+ if f.regular != nil {
99+ f.regular.Close()
100+ }
101+ f.regular, f.bold = nil, nil
102+}
103+
104+// Rasterize draws r into dst, which must hold CellW*CellH bytes (stride
105+// CellW). Pixels outside the cell are clipped; a missing glyph leaves dst
106+// empty.
107+func (f *Face) Rasterize(r rune, bold bool, dst []byte) {
108+ clear(dst[:f.CellW*f.CellH])
109+ face := f.regular
110+ if bold {
111+ face = f.bold
112+ }
113+ dr, mask, maskp, _, ok := face.Glyph(fixed.Point26_6{X: 0, Y: f.ascent}, r)
114+ if !ok {
115+ return
116+ }
117+ alpha, isAlpha := mask.(*image.Alpha)
118+ for y := dr.Min.Y; y < dr.Max.Y; y++ {
119+ if y < 0 || y >= f.CellH {
120+ continue
121+ }
122+ for x := dr.Min.X; x < dr.Max.X; x++ {
123+ if x < 0 || x >= f.CellW {
124+ continue
125+ }
126+ mx, my := maskp.X+x-dr.Min.X, maskp.Y+y-dr.Min.Y
127+ if isAlpha {
128+ dst[y*f.CellW+x] = alpha.Pix[alpha.PixOffset(mx, my)]
129+ continue
130+ }
131+ _, _, _, a := mask.At(mx, my).RGBA()
132+ dst[y*f.CellW+x] = uint8(a >> 8)
133+ }
134+ }
135+}
A internal/gpu/alloc_test.go +93 -0
@@ -0,0 +1,93 @@
1+package gpu
2+
3+import (
4+ "os"
5+ "syscall"
6+ "testing"
7+
8+ "git.bnema.dev/bnema/neferbar/internal/glyph"
9+ "git.bnema.dev/bnema/neferbar/internal/racecheck"
10+ "git.bnema.dev/bnema/neferbar/internal/syncobj"
11+)
12+
13+// newTestRenderer needs a Vulkan device with DMA-BUF export; it skips without one.
14+func newTestRenderer(t *testing.T) (*Renderer, *Device, *syncobj.Node) {
15+ t.Helper()
16+ var st syscall.Stat_t
17+ if err := syscall.Stat("/dev/dri/renderD128", &st); err != nil {
18+ t.Skip("no render node")
19+ }
20+ dev, err := Open(uint64(st.Rdev))
21+ if err != nil {
22+ t.Skipf("no matching Vulkan device: %v", err)
23+ }
24+ t.Cleanup(dev.Close)
25+ node, err := syncobj.Open("/dev/dri/renderD128")
26+ if err != nil {
27+ t.Skip(err)
28+ }
29+ t.Cleanup(func() { node.Close() })
30+ mods, err := dev.ExportableModifiers()
31+ if err != nil || len(mods) == 0 {
32+ t.Skip("no exportable modifier")
33+ }
34+ var mod Modifier
35+ for m := range mods {
36+ mod = Modifier(m)
37+ break
38+ }
39+ path, err := glyph.FindFont("monospace", false)
40+ if err != nil {
41+ t.Skip(err)
42+ }
43+ if _, err := os.Stat(path); err != nil {
44+ t.Skip(err)
45+ }
46+ face, err := glyph.Load(path, "", 16)
47+ if err != nil {
48+ t.Skip(err)
49+ }
50+ t.Cleanup(face.Close)
51+ r, err := NewRenderer(dev, node, mod, face, 800, int32(face.CellH))
52+ if err != nil {
53+ t.Fatal(err)
54+ }
55+ t.Cleanup(r.Close)
56+ return r, dev, node
57+}
58+
59+// TestDrawSteadyStateAllocs reports the allocations of one Draw once the
60+// atlas is warm: the budget is what the Vulkan bindings force per call.
61+func TestDrawSteadyStateAllocs(t *testing.T) {
62+ if racecheck.Enabled {
63+ t.Skip("the race detector allocates")
64+ }
65+ r, _, node := newTestRenderer(t)
66+ cells := make([]Cell, r.Cols())
67+ for i := range cells {
68+ cells[i] = Cell{Rune: rune('a' + i%26), FG: [3]uint8{255, 255, 255}, BG: [3]uint8{0, 0, 40}}
69+ }
70+ var f Frame
71+ step := func() {
72+ ok, err := r.Draw(cells, [3]uint8{}, &f)
73+ if err != nil || !ok {
74+ t.Fatalf("draw ok=%v err=%v", ok, err)
75+ }
76+ // Play the compositor: signal the release point, then hand it back.
77+ s := &r.slots[f.Slot]
78+ if err := node.Signal(s.releaseHandle, s.releasePoint); err != nil {
79+ t.Fatal(err)
80+ }
81+ if err := r.Released(f.Slot); err != nil {
82+ t.Fatal(err)
83+ }
84+ }
85+ for i := 0; i < 8; i++ { // warm: atlas upload, lazily created state
86+ step()
87+ }
88+ got := testing.AllocsPerRun(200, step)
89+ t.Logf("allocations per Draw+Released: %.1f", got)
90+ if got > 0 {
91+ t.Errorf("steady-state Draw allocates %.1f objects; want 0", got)
92+ }
93+}
@@ -0,0 +1,93 @@
1+package gpu
2+
3+import (
4+ "os"
5+ "syscall"
6+ "testing"
7+
8+ "git.bnema.dev/bnema/neferbar/internal/glyph"
9+ "git.bnema.dev/bnema/neferbar/internal/racecheck"
10+ "git.bnema.dev/bnema/neferbar/internal/syncobj"
11+)
12+
13+// newTestRenderer needs a Vulkan device with DMA-BUF export; it skips without one.
14+func newTestRenderer(t *testing.T) (*Renderer, *Device, *syncobj.Node) {
15+ t.Helper()
16+ var st syscall.Stat_t
17+ if err := syscall.Stat("/dev/dri/renderD128", &st); err != nil {
18+ t.Skip("no render node")
19+ }
20+ dev, err := Open(uint64(st.Rdev))
21+ if err != nil {
22+ t.Skipf("no matching Vulkan device: %v", err)
23+ }
24+ t.Cleanup(dev.Close)
25+ node, err := syncobj.Open("/dev/dri/renderD128")
26+ if err != nil {
27+ t.Skip(err)
28+ }
29+ t.Cleanup(func() { node.Close() })
30+ mods, err := dev.ExportableModifiers()
31+ if err != nil || len(mods) == 0 {
32+ t.Skip("no exportable modifier")
33+ }
34+ var mod Modifier
35+ for m := range mods {
36+ mod = Modifier(m)
37+ break
38+ }
39+ path, err := glyph.FindFont("monospace", false)
40+ if err != nil {
41+ t.Skip(err)
42+ }
43+ if _, err := os.Stat(path); err != nil {
44+ t.Skip(err)
45+ }
46+ face, err := glyph.Load(path, "", 16)
47+ if err != nil {
48+ t.Skip(err)
49+ }
50+ t.Cleanup(face.Close)
51+ r, err := NewRenderer(dev, node, mod, face, 800, int32(face.CellH))
52+ if err != nil {
53+ t.Fatal(err)
54+ }
55+ t.Cleanup(r.Close)
56+ return r, dev, node
57+}
58+
59+// TestDrawSteadyStateAllocs reports the allocations of one Draw once the
60+// atlas is warm: the budget is what the Vulkan bindings force per call.
61+func TestDrawSteadyStateAllocs(t *testing.T) {
62+ if racecheck.Enabled {
63+ t.Skip("the race detector allocates")
64+ }
65+ r, _, node := newTestRenderer(t)
66+ cells := make([]Cell, r.Cols())
67+ for i := range cells {
68+ cells[i] = Cell{Rune: rune('a' + i%26), FG: [3]uint8{255, 255, 255}, BG: [3]uint8{0, 0, 40}}
69+ }
70+ var f Frame
71+ step := func() {
72+ ok, err := r.Draw(cells, [3]uint8{}, &f)
73+ if err != nil || !ok {
74+ t.Fatalf("draw ok=%v err=%v", ok, err)
75+ }
76+ // Play the compositor: signal the release point, then hand it back.
77+ s := &r.slots[f.Slot]
78+ if err := node.Signal(s.releaseHandle, s.releasePoint); err != nil {
79+ t.Fatal(err)
80+ }
81+ if err := r.Released(f.Slot); err != nil {
82+ t.Fatal(err)
83+ }
84+ }
85+ for i := 0; i < 8; i++ { // warm: atlas upload, lazily created state
86+ step()
87+ }
88+ got := testing.AllocsPerRun(200, step)
89+ t.Logf("allocations per Draw+Released: %.1f", got)
90+ if got > 0 {
91+ t.Errorf("steady-state Draw allocates %.1f objects; want 0", got)
92+ }
93+}
A internal/gpu/device.go +265 -0
@@ -0,0 +1,265 @@
1+// Package gpu renders the cell grid with raw Vulkan into DMA-BUF images that
2+// the Wayland layer can hand to the compositor.
3+package gpu
4+
5+import (
6+ "bytes"
7+ "encoding/binary"
8+ "fmt"
9+ "math"
10+ "unsafe"
11+
12+ "github.com/bnema/purego-vulkan/vulkan"
13+)
14+
15+// requiredExtensions are mandatory on the device that matches the compositor's
16+// linux-dmabuf main device.
17+var requiredExtensions = []string{
18+ "VK_KHR_external_memory_fd",
19+ "VK_EXT_external_memory_dma_buf",
20+ "VK_EXT_image_drm_format_modifier",
21+}
22+
23+// Device is the Vulkan device chosen by the compositor's main_device.
24+type Device struct {
25+ instance vulkan.Instance
26+ idisp *vulkan.InstanceDispatch
27+ phys vulkan.PhysicalDevice
28+ logical vulkan.Device
29+ disp *vulkan.DeviceDispatch
30+ queue vulkan.Queue
31+ family uint32
32+ // RenderMinor is the minor of the matching /dev/dri/renderD node.
33+ RenderMinor uint32
34+
35+ memProps vulkan.PhysicalDeviceMemoryProperties
36+}
37+
38+func devMajor(d uint64) uint32 { return uint32((d >> 8) & 0xfff) }
39+func devMinor(d uint64) uint32 { return uint32((d & 0xff) | ((d >> 12) & 0xffffff00)) }
40+
41+// Open selects the Vulkan device whose primary or render node matches
42+// mainDevice (a dev_t). There is no cross-GPU fallback.
43+func Open(mainDevice uint64) (_ *Device, err error) {
44+ if err = vulkan.Init(); err != nil {
45+ return nil, err
46+ }
47+ name := []byte("neferbar\x00")
48+ ai := vulkan.ApplicationInfo{SType: vulkan.StructureTypeApplicationInfo, ApplicationName: &name[0], ApiVersion: 1<<22 | 3<<12}
49+ ci := vulkan.InstanceCreateInfo{SType: vulkan.StructureTypeInstanceCreateInfo, ApplicationInfo: &ai}
50+ d := &Device{}
51+ if err = vulkan.Check(vulkan.Global().CreateInstance(&ci, nil, &d.instance)); err != nil {
52+ return nil, fmt.Errorf("gpu: create Vulkan 1.3 instance: %w", err)
53+ }
54+ if d.idisp, err = vulkan.LoadInstanceDispatch(d.instance); err != nil {
55+ vulkan.VkDestroyInstance(d.instance, nil)
56+ return nil, err
57+ }
58+ defer func() {
59+ if err != nil {
60+ d.Close()
61+ }
62+ }()
63+ if err = d.pickPhysical(mainDevice); err != nil {
64+ return nil, err
65+ }
66+ if err = d.createLogical(); err != nil {
67+ return nil, err
68+ }
69+ d.idisp.GetPhysicalDeviceMemoryProperties(d.phys, &d.memProps)
70+ return d, nil
71+}
72+
73+func (d *Device) pickPhysical(main uint64) error {
74+ var count uint32
75+ if err := vulkan.Check(d.idisp.EnumeratePhysicalDevices(d.instance, &count, nil)); err != nil {
76+ return err
77+ }
78+ if count == 0 {
79+ return fmt.Errorf("gpu: no Vulkan physical device")
80+ }
81+ all := make([]vulkan.PhysicalDevice, count)
82+ if err := vulkan.Check(d.idisp.EnumeratePhysicalDevices(d.instance, &count, &all[0])); err != nil {
83+ return err
84+ }
85+ mj, mn := devMajor(main), devMinor(main)
86+ for _, cand := range all[:count] {
87+ exts, err := d.deviceExtensions(cand)
88+ if err != nil {
89+ return err
90+ }
91+ if !exts["VK_EXT_physical_device_drm"] {
92+ continue
93+ }
94+ props := vulkan.PhysicalDeviceProperties2{SType: vulkan.StructureTypePhysicalDeviceProperties2}
95+ drm := vulkan.PhysicalDeviceDrmPropertiesEXT{SType: vulkan.StructureTypePhysicalDeviceDRMPropertiesEXT}
96+ props.Next = unsafe.Pointer(&drm)
97+ d.idisp.GetPhysicalDeviceProperties2(cand, &props)
98+ primary := drm.HasPrimary != 0 && uint32(drm.PrimaryMajor) == mj && uint32(drm.PrimaryMinor) == mn
99+ render := drm.HasRender != 0 && uint32(drm.RenderMajor) == mj && uint32(drm.RenderMinor) == mn
100+ if !primary && !render {
101+ continue
102+ }
103+ if drm.HasRender == 0 {
104+ return fmt.Errorf("gpu: device matching main_device %d:%d has no render node", mj, mn)
105+ }
106+ for _, need := range requiredExtensions {
107+ if !exts[need] {
108+ return fmt.Errorf("gpu: matching device lacks %s", need)
109+ }
110+ }
111+ d.phys, d.RenderMinor = cand, uint32(drm.RenderMinor)
112+ return nil
113+ }
114+ return fmt.Errorf("gpu: no Vulkan device matches linux-dmabuf main_device %d:%d", mj, mn)
115+}
116+
117+func (d *Device) deviceExtensions(p vulkan.PhysicalDevice) (map[string]bool, error) {
118+ var count uint32
119+ if err := vulkan.Check(d.idisp.EnumerateDeviceExtensionProperties(p, nil, &count, nil)); err != nil {
120+ return nil, err
121+ }
122+ out := map[string]bool{}
123+ if count == 0 {
124+ return out, nil
125+ }
126+ props := make([]vulkan.ExtensionProperties, count)
127+ if err := vulkan.Check(d.idisp.EnumerateDeviceExtensionProperties(p, nil, &count, &props[0])); err != nil {
128+ return nil, err
129+ }
130+ for _, e := range props[:count] {
131+ out[string(bytes.TrimRight(e.ExtensionName[:], "\x00"))] = true
132+ }
133+ return out, nil
134+}
135+
136+func (d *Device) createLogical() error {
137+ var families uint32
138+ d.idisp.GetPhysicalDeviceQueueFamilyProperties(d.phys, &families, nil)
139+ if families == 0 {
140+ return fmt.Errorf("gpu: no queue family")
141+ }
142+ qs := make([]vulkan.QueueFamilyProperties, families)
143+ d.idisp.GetPhysicalDeviceQueueFamilyProperties(d.phys, &families, &qs[0])
144+ found := false
145+ for i, q := range qs[:families] {
146+ if q.QueueCount != 0 && q.QueueFlags&vulkan.QueueGraphicsBit != 0 {
147+ d.family, found = uint32(i), true
148+ break
149+ }
150+ }
151+ if !found {
152+ return fmt.Errorf("gpu: no graphics queue")
153+ }
154+ sync2 := vulkan.PhysicalDeviceSynchronization2Features{SType: vulkan.StructureTypePhysicalDeviceSynchronization2Features, Synchronization2: 1}
155+ dyn := vulkan.PhysicalDeviceDynamicRenderingFeatures{SType: vulkan.StructureTypePhysicalDeviceDynamicRenderingFeatures, Next: unsafe.Pointer(&sync2), DynamicRendering: 1}
156+ prio := float32(1)
157+ qci := vulkan.DeviceQueueCreateInfo{SType: vulkan.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: d.family, QueueCount: 1, QueuePriorities: &prio}
158+ names := make([][]byte, len(requiredExtensions))
159+ ptrs := make([]*byte, len(requiredExtensions))
160+ for i, n := range requiredExtensions {
161+ names[i] = append([]byte(n), 0)
162+ ptrs[i] = &names[i][0]
163+ }
164+ dci := vulkan.DeviceCreateInfo{SType: vulkan.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&dyn), QueueCreateInfoCount: 1, QueueCreateInfos: &qci, EnabledExtensionCount: uint32(len(ptrs)), PpEnabledExtensionNames: &ptrs[0]}
165+ if err := vulkan.Check(d.idisp.CreateDevice(d.phys, &dci, nil, &d.logical)); err != nil {
166+ return fmt.Errorf("gpu: create device: %w", err)
167+ }
168+ var err error
169+ if d.disp, err = vulkan.LoadDeviceDispatch(d.idisp, d.logical); err != nil {
170+ return err
171+ }
172+ d.disp.GetDeviceQueue(d.logical, d.family, 0, &d.queue)
173+ return nil
174+}
175+
176+// memoryType finds a memory type index allowed by bits with all want flags.
177+func (d *Device) memoryType(bits uint32, want vulkan.MemoryPropertyFlags) (uint32, error) {
178+ for i := uint32(0); i < d.memProps.MemoryTypeCount; i++ {
179+ if bits&(1<<i) != 0 && d.memProps.MemoryTypes[i].PropertyFlags&want == want {
180+ return i, nil
181+ }
182+ }
183+ return math.MaxUint32, fmt.Errorf("gpu: no memory type for flags %#x", want)
184+}
185+
186+// WaitIdle blocks until the queue has finished all submitted work.
187+func (d *Device) WaitIdle() error { return vulkan.Check(d.disp.DeviceWaitIdle(d.logical)) }
188+
189+// Close destroys the device and the instance.
190+func (d *Device) Close() {
191+ if d == nil {
192+ return
193+ }
194+ if d.logical != 0 {
195+ _ = d.WaitIdle()
196+ d.disp.DestroyDevice(d.logical, nil)
197+ d.logical = 0
198+ }
199+ if d.instance != 0 {
200+ d.idisp.DestroyInstance(d.instance, nil)
201+ d.instance = 0
202+ }
203+}
204+
205+// Modifier is a DRM format modifier both the GPU and the compositor accept.
206+type Modifier uint64
207+
208+// ExportableModifiers lists single-plane modifiers of B8G8R8A8 the GPU can
209+// render to and export as DMA-BUF.
210+func (d *Device) ExportableModifiers() (map[uint64]bool, error) {
211+ props := vulkan.FormatProperties2{SType: vulkan.StructureTypeFormatProperties2}
212+ list := vulkan.DrmFormatModifierPropertiesList2EXT{SType: vulkan.StructureTypeDRMFormatModifierPropertiesList2EXT}
213+ props.Next = unsafe.Pointer(&list)
214+ d.idisp.GetPhysicalDeviceFormatProperties2(d.phys, vulkan.FormatB8g8r8a8Unorm, &props)
215+ out := map[uint64]bool{}
216+ if list.DrmFormatModifierCount == 0 {
217+ return out, nil
218+ }
219+ mods := make([]vulkan.DrmFormatModifierProperties2EXT, list.DrmFormatModifierCount)
220+ list.DrmFormatModifierProperties = &mods[0]
221+ d.idisp.GetPhysicalDeviceFormatProperties2(d.phys, vulkan.FormatB8g8r8a8Unorm, &props)
222+ for _, m := range mods[:list.DrmFormatModifierCount] {
223+ if m.DrmFormatModifierPlaneCount != 1 || m.DrmFormatModifierTilingFeatures&vulkan.FormatFeature2ColorAttachmentBit == 0 {
224+ continue
225+ }
226+ mi := vulkan.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vulkan.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: m.DrmFormatModifier, SharingMode: vulkan.SharingModeExclusive}
227+ ei := vulkan.PhysicalDeviceExternalImageFormatInfo{SType: vulkan.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mi), HandleType: vulkan.ExternalMemoryHandleTypeDMABUFBitEXT}
228+ info := vulkan.PhysicalDeviceImageFormatInfo2{SType: vulkan.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ei), Format: vulkan.FormatB8g8r8a8Unorm, Type: vulkan.ImageType2d, Tiling: vulkan.ImageTilingDRMFormatModifierEXT, Usage: vulkan.ImageUsageColorAttachmentBit}
229+ ext := vulkan.ExternalImageFormatProperties{SType: vulkan.StructureTypeExternalImageFormatProperties}
230+ ip := vulkan.ImageFormatProperties2{SType: vulkan.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&ext)}
231+ r := d.idisp.GetPhysicalDeviceImageFormatProperties2(d.phys, &info, &ip)
232+ if r == vulkan.ErrorFormatNotSupported {
233+ continue
234+ }
235+ if err := vulkan.Check(r); err != nil {
236+ return nil, fmt.Errorf("gpu: query modifier %#x: %w", m.DrmFormatModifier, err)
237+ }
238+ if ext.ExternalMemoryProperties.ExternalMemoryFeatures&vulkan.ExternalMemoryFeatureExportableBit != 0 {
239+ out[m.DrmFormatModifier] = true
240+ }
241+ }
242+ return out, nil
243+}
244+
245+// DRMFormatXRGB8888 is the fourcc of XRGB8888, which maps to B8G8R8A8.
246+const DRMFormatXRGB8888 uint32 = 0x34325258
247+
248+// FormatPair is one DRM format and modifier the compositor accepts.
249+type FormatPair struct {
250+ FourCC uint32
251+ Modifier uint64
252+}
253+
254+// ChooseModifier returns the first compositor-preferred XRGB8888 modifier the
255+// GPU can export.
256+func ChooseModifier(formats []FormatPair, supported map[uint64]bool) (Modifier, error) {
257+ for _, f := range formats {
258+ if f.FourCC == DRMFormatXRGB8888 && supported[f.Modifier] {
259+ return Modifier(f.Modifier), nil
260+ }
261+ }
262+ return 0, fmt.Errorf("gpu: no XRGB8888 modifier shared by the GPU and the compositor")
263+}
264+
265+func le32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) }
@@ -0,0 +1,265 @@
1+// Package gpu renders the cell grid with raw Vulkan into DMA-BUF images that
2+// the Wayland layer can hand to the compositor.
3+package gpu
4+
5+import (
6+ "bytes"
7+ "encoding/binary"
8+ "fmt"
9+ "math"
10+ "unsafe"
11+
12+ "github.com/bnema/purego-vulkan/vulkan"
13+)
14+
15+// requiredExtensions are mandatory on the device that matches the compositor's
16+// linux-dmabuf main device.
17+var requiredExtensions = []string{
18+ "VK_KHR_external_memory_fd",
19+ "VK_EXT_external_memory_dma_buf",
20+ "VK_EXT_image_drm_format_modifier",
21+}
22+
23+// Device is the Vulkan device chosen by the compositor's main_device.
24+type Device struct {
25+ instance vulkan.Instance
26+ idisp *vulkan.InstanceDispatch
27+ phys vulkan.PhysicalDevice
28+ logical vulkan.Device
29+ disp *vulkan.DeviceDispatch
30+ queue vulkan.Queue
31+ family uint32
32+ // RenderMinor is the minor of the matching /dev/dri/renderD node.
33+ RenderMinor uint32
34+
35+ memProps vulkan.PhysicalDeviceMemoryProperties
36+}
37+
38+func devMajor(d uint64) uint32 { return uint32((d >> 8) & 0xfff) }
39+func devMinor(d uint64) uint32 { return uint32((d & 0xff) | ((d >> 12) & 0xffffff00)) }
40+
41+// Open selects the Vulkan device whose primary or render node matches
42+// mainDevice (a dev_t). There is no cross-GPU fallback.
43+func Open(mainDevice uint64) (_ *Device, err error) {
44+ if err = vulkan.Init(); err != nil {
45+ return nil, err
46+ }
47+ name := []byte("neferbar\x00")
48+ ai := vulkan.ApplicationInfo{SType: vulkan.StructureTypeApplicationInfo, ApplicationName: &name[0], ApiVersion: 1<<22 | 3<<12}
49+ ci := vulkan.InstanceCreateInfo{SType: vulkan.StructureTypeInstanceCreateInfo, ApplicationInfo: &ai}
50+ d := &Device{}
51+ if err = vulkan.Check(vulkan.Global().CreateInstance(&ci, nil, &d.instance)); err != nil {
52+ return nil, fmt.Errorf("gpu: create Vulkan 1.3 instance: %w", err)
53+ }
54+ if d.idisp, err = vulkan.LoadInstanceDispatch(d.instance); err != nil {
55+ vulkan.VkDestroyInstance(d.instance, nil)
56+ return nil, err
57+ }
58+ defer func() {
59+ if err != nil {
60+ d.Close()
61+ }
62+ }()
63+ if err = d.pickPhysical(mainDevice); err != nil {
64+ return nil, err
65+ }
66+ if err = d.createLogical(); err != nil {
67+ return nil, err
68+ }
69+ d.idisp.GetPhysicalDeviceMemoryProperties(d.phys, &d.memProps)
70+ return d, nil
71+}
72+
73+func (d *Device) pickPhysical(main uint64) error {
74+ var count uint32
75+ if err := vulkan.Check(d.idisp.EnumeratePhysicalDevices(d.instance, &count, nil)); err != nil {
76+ return err
77+ }
78+ if count == 0 {
79+ return fmt.Errorf("gpu: no Vulkan physical device")
80+ }
81+ all := make([]vulkan.PhysicalDevice, count)
82+ if err := vulkan.Check(d.idisp.EnumeratePhysicalDevices(d.instance, &count, &all[0])); err != nil {
83+ return err
84+ }
85+ mj, mn := devMajor(main), devMinor(main)
86+ for _, cand := range all[:count] {
87+ exts, err := d.deviceExtensions(cand)
88+ if err != nil {
89+ return err
90+ }
91+ if !exts["VK_EXT_physical_device_drm"] {
92+ continue
93+ }
94+ props := vulkan.PhysicalDeviceProperties2{SType: vulkan.StructureTypePhysicalDeviceProperties2}
95+ drm := vulkan.PhysicalDeviceDrmPropertiesEXT{SType: vulkan.StructureTypePhysicalDeviceDRMPropertiesEXT}
96+ props.Next = unsafe.Pointer(&drm)
97+ d.idisp.GetPhysicalDeviceProperties2(cand, &props)
98+ primary := drm.HasPrimary != 0 && uint32(drm.PrimaryMajor) == mj && uint32(drm.PrimaryMinor) == mn
99+ render := drm.HasRender != 0 && uint32(drm.RenderMajor) == mj && uint32(drm.RenderMinor) == mn
100+ if !primary && !render {
101+ continue
102+ }
103+ if drm.HasRender == 0 {
104+ return fmt.Errorf("gpu: device matching main_device %d:%d has no render node", mj, mn)
105+ }
106+ for _, need := range requiredExtensions {
107+ if !exts[need] {
108+ return fmt.Errorf("gpu: matching device lacks %s", need)
109+ }
110+ }
111+ d.phys, d.RenderMinor = cand, uint32(drm.RenderMinor)
112+ return nil
113+ }
114+ return fmt.Errorf("gpu: no Vulkan device matches linux-dmabuf main_device %d:%d", mj, mn)
115+}
116+
117+func (d *Device) deviceExtensions(p vulkan.PhysicalDevice) (map[string]bool, error) {
118+ var count uint32
119+ if err := vulkan.Check(d.idisp.EnumerateDeviceExtensionProperties(p, nil, &count, nil)); err != nil {
120+ return nil, err
121+ }
122+ out := map[string]bool{}
123+ if count == 0 {
124+ return out, nil
125+ }
126+ props := make([]vulkan.ExtensionProperties, count)
127+ if err := vulkan.Check(d.idisp.EnumerateDeviceExtensionProperties(p, nil, &count, &props[0])); err != nil {
128+ return nil, err
129+ }
130+ for _, e := range props[:count] {
131+ out[string(bytes.TrimRight(e.ExtensionName[:], "\x00"))] = true
132+ }
133+ return out, nil
134+}
135+
136+func (d *Device) createLogical() error {
137+ var families uint32
138+ d.idisp.GetPhysicalDeviceQueueFamilyProperties(d.phys, &families, nil)
139+ if families == 0 {
140+ return fmt.Errorf("gpu: no queue family")
141+ }
142+ qs := make([]vulkan.QueueFamilyProperties, families)
143+ d.idisp.GetPhysicalDeviceQueueFamilyProperties(d.phys, &families, &qs[0])
144+ found := false
145+ for i, q := range qs[:families] {
146+ if q.QueueCount != 0 && q.QueueFlags&vulkan.QueueGraphicsBit != 0 {
147+ d.family, found = uint32(i), true
148+ break
149+ }
150+ }
151+ if !found {
152+ return fmt.Errorf("gpu: no graphics queue")
153+ }
154+ sync2 := vulkan.PhysicalDeviceSynchronization2Features{SType: vulkan.StructureTypePhysicalDeviceSynchronization2Features, Synchronization2: 1}
155+ dyn := vulkan.PhysicalDeviceDynamicRenderingFeatures{SType: vulkan.StructureTypePhysicalDeviceDynamicRenderingFeatures, Next: unsafe.Pointer(&sync2), DynamicRendering: 1}
156+ prio := float32(1)
157+ qci := vulkan.DeviceQueueCreateInfo{SType: vulkan.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: d.family, QueueCount: 1, QueuePriorities: &prio}
158+ names := make([][]byte, len(requiredExtensions))
159+ ptrs := make([]*byte, len(requiredExtensions))
160+ for i, n := range requiredExtensions {
161+ names[i] = append([]byte(n), 0)
162+ ptrs[i] = &names[i][0]
163+ }
164+ dci := vulkan.DeviceCreateInfo{SType: vulkan.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&dyn), QueueCreateInfoCount: 1, QueueCreateInfos: &qci, EnabledExtensionCount: uint32(len(ptrs)), PpEnabledExtensionNames: &ptrs[0]}
165+ if err := vulkan.Check(d.idisp.CreateDevice(d.phys, &dci, nil, &d.logical)); err != nil {
166+ return fmt.Errorf("gpu: create device: %w", err)
167+ }
168+ var err error
169+ if d.disp, err = vulkan.LoadDeviceDispatch(d.idisp, d.logical); err != nil {
170+ return err
171+ }
172+ d.disp.GetDeviceQueue(d.logical, d.family, 0, &d.queue)
173+ return nil
174+}
175+
176+// memoryType finds a memory type index allowed by bits with all want flags.
177+func (d *Device) memoryType(bits uint32, want vulkan.MemoryPropertyFlags) (uint32, error) {
178+ for i := uint32(0); i < d.memProps.MemoryTypeCount; i++ {
179+ if bits&(1<<i) != 0 && d.memProps.MemoryTypes[i].PropertyFlags&want == want {
180+ return i, nil
181+ }
182+ }
183+ return math.MaxUint32, fmt.Errorf("gpu: no memory type for flags %#x", want)
184+}
185+
186+// WaitIdle blocks until the queue has finished all submitted work.
187+func (d *Device) WaitIdle() error { return vulkan.Check(d.disp.DeviceWaitIdle(d.logical)) }
188+
189+// Close destroys the device and the instance.
190+func (d *Device) Close() {
191+ if d == nil {
192+ return
193+ }
194+ if d.logical != 0 {
195+ _ = d.WaitIdle()
196+ d.disp.DestroyDevice(d.logical, nil)
197+ d.logical = 0
198+ }
199+ if d.instance != 0 {
200+ d.idisp.DestroyInstance(d.instance, nil)
201+ d.instance = 0
202+ }
203+}
204+
205+// Modifier is a DRM format modifier both the GPU and the compositor accept.
206+type Modifier uint64
207+
208+// ExportableModifiers lists single-plane modifiers of B8G8R8A8 the GPU can
209+// render to and export as DMA-BUF.
210+func (d *Device) ExportableModifiers() (map[uint64]bool, error) {
211+ props := vulkan.FormatProperties2{SType: vulkan.StructureTypeFormatProperties2}
212+ list := vulkan.DrmFormatModifierPropertiesList2EXT{SType: vulkan.StructureTypeDRMFormatModifierPropertiesList2EXT}
213+ props.Next = unsafe.Pointer(&list)
214+ d.idisp.GetPhysicalDeviceFormatProperties2(d.phys, vulkan.FormatB8g8r8a8Unorm, &props)
215+ out := map[uint64]bool{}
216+ if list.DrmFormatModifierCount == 0 {
217+ return out, nil
218+ }
219+ mods := make([]vulkan.DrmFormatModifierProperties2EXT, list.DrmFormatModifierCount)
220+ list.DrmFormatModifierProperties = &mods[0]
221+ d.idisp.GetPhysicalDeviceFormatProperties2(d.phys, vulkan.FormatB8g8r8a8Unorm, &props)
222+ for _, m := range mods[:list.DrmFormatModifierCount] {
223+ if m.DrmFormatModifierPlaneCount != 1 || m.DrmFormatModifierTilingFeatures&vulkan.FormatFeature2ColorAttachmentBit == 0 {
224+ continue
225+ }
226+ mi := vulkan.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vulkan.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: m.DrmFormatModifier, SharingMode: vulkan.SharingModeExclusive}
227+ ei := vulkan.PhysicalDeviceExternalImageFormatInfo{SType: vulkan.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mi), HandleType: vulkan.ExternalMemoryHandleTypeDMABUFBitEXT}
228+ info := vulkan.PhysicalDeviceImageFormatInfo2{SType: vulkan.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ei), Format: vulkan.FormatB8g8r8a8Unorm, Type: vulkan.ImageType2d, Tiling: vulkan.ImageTilingDRMFormatModifierEXT, Usage: vulkan.ImageUsageColorAttachmentBit}
229+ ext := vulkan.ExternalImageFormatProperties{SType: vulkan.StructureTypeExternalImageFormatProperties}
230+ ip := vulkan.ImageFormatProperties2{SType: vulkan.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&ext)}
231+ r := d.idisp.GetPhysicalDeviceImageFormatProperties2(d.phys, &info, &ip)
232+ if r == vulkan.ErrorFormatNotSupported {
233+ continue
234+ }
235+ if err := vulkan.Check(r); err != nil {
236+ return nil, fmt.Errorf("gpu: query modifier %#x: %w", m.DrmFormatModifier, err)
237+ }
238+ if ext.ExternalMemoryProperties.ExternalMemoryFeatures&vulkan.ExternalMemoryFeatureExportableBit != 0 {
239+ out[m.DrmFormatModifier] = true
240+ }
241+ }
242+ return out, nil
243+}
244+
245+// DRMFormatXRGB8888 is the fourcc of XRGB8888, which maps to B8G8R8A8.
246+const DRMFormatXRGB8888 uint32 = 0x34325258
247+
248+// FormatPair is one DRM format and modifier the compositor accepts.
249+type FormatPair struct {
250+ FourCC uint32
251+ Modifier uint64
252+}
253+
254+// ChooseModifier returns the first compositor-preferred XRGB8888 modifier the
255+// GPU can export.
256+func ChooseModifier(formats []FormatPair, supported map[uint64]bool) (Modifier, error) {
257+ for _, f := range formats {
258+ if f.FourCC == DRMFormatXRGB8888 && supported[f.Modifier] {
259+ return Modifier(f.Modifier), nil
260+ }
261+ }
262+ return 0, fmt.Errorf("gpu: no XRGB8888 modifier shared by the GPU and the compositor")
263+}
264+
265+func le32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) }
A internal/gpu/image.go +129 -0
@@ -0,0 +1,129 @@
1+package gpu
2+
3+import (
4+ "fmt"
5+ "math"
6+ "unsafe"
7+
8+ "github.com/bnema/purego-vulkan/vulkan"
9+ "golang.org/x/sys/unix"
10+)
11+
12+// Image is a render target backed by exportable DMA-BUF memory. FD stays owned
13+// by the Image; the Wayland layer duplicates it on import.
14+type Image struct {
15+ dev *Device
16+ image vulkan.Image
17+ memory vulkan.DeviceMemory
18+ view vulkan.ImageView
19+
20+ Width, Height int32
21+ FD int
22+ Modifier uint64
23+ Offset, Stride uint32
24+
25+ // used is false until the first render: the first transition starts from
26+ // an undefined layout.
27+ used bool
28+}
29+
30+// NewImage allocates a Width x Height B8G8R8A8 image with the given modifier.
31+func (d *Device) NewImage(width, height int32, mod Modifier) (img *Image, err error) {
32+ if width <= 0 || height <= 0 {
33+ return nil, fmt.Errorf("gpu: invalid image size %dx%d", width, height)
34+ }
35+ img = &Image{dev: d, Width: width, Height: height, FD: -1}
36+ defer func() {
37+ if err != nil {
38+ img.Close()
39+ img = nil
40+ }
41+ }()
42+ modifier := uint64(mod)
43+ list := vulkan.ImageDrmFormatModifierListCreateInfoEXT{SType: vulkan.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: 1, DrmFormatModifiers: &modifier}
44+ ext := vulkan.ExternalMemoryImageCreateInfo{SType: vulkan.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vulkan.ExternalMemoryHandleTypeDMABUFBitEXT}
45+ ci := vulkan.ImageCreateInfo{
46+ SType: vulkan.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&ext),
47+ ImageType: vulkan.ImageType2d, Format: vulkan.FormatB8g8r8a8Unorm,
48+ Extent: vulkan.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1},
49+ MipLevels: 1, ArrayLayers: 1, Samples: vulkan.SampleCount1Bit,
50+ Tiling: vulkan.ImageTilingDRMFormatModifierEXT, Usage: vulkan.ImageUsageColorAttachmentBit,
51+ SharingMode: vulkan.SharingModeExclusive, InitialLayout: vulkan.ImageLayoutUndefined,
52+ }
53+ if err = vulkan.Check(d.disp.CreateImage(d.logical, &ci, nil, &img.image)); err != nil {
54+ return nil, fmt.Errorf("gpu: create image: %w", err)
55+ }
56+ props := vulkan.ImageDrmFormatModifierPropertiesEXT{SType: vulkan.StructureTypeImageDRMFormatModifierPropertiesEXT}
57+ if err = vulkan.Check(d.disp.GetImageDrmFormatModifierPropertiesEXT(d.logical, img.image, &props)); err != nil {
58+ return nil, err
59+ }
60+ if props.DrmFormatModifier != modifier {
61+ return nil, fmt.Errorf("gpu: asked for modifier %#x, driver chose %#x", modifier, props.DrmFormatModifier)
62+ }
63+ img.Modifier = modifier
64+ sub := vulkan.ImageSubresource{AspectMask: vulkan.ImageAspectMemoryPlane0BitEXT}
65+ var layout vulkan.SubresourceLayout
66+ d.disp.GetImageSubresourceLayout(d.logical, img.image, &sub, &layout)
67+ if layout.RowPitch == 0 || layout.RowPitch > math.MaxUint32 || layout.Offset > math.MaxUint32 {
68+ return nil, fmt.Errorf("gpu: unusable plane layout offset %d stride %d", layout.Offset, layout.RowPitch)
69+ }
70+ img.Offset, img.Stride = uint32(layout.Offset), uint32(layout.RowPitch)
71+
72+ reqInfo := vulkan.ImageMemoryRequirementsInfo2{SType: vulkan.StructureTypeImageMemoryRequirementsInfo2, Image: img.image}
73+ reqs := vulkan.MemoryRequirements2{SType: vulkan.StructureTypeMemoryRequirements2}
74+ d.disp.GetImageMemoryRequirements2(d.logical, &reqInfo, &reqs)
75+ idx, err := d.memoryType(reqs.MemoryRequirements.MemoryTypeBits, vulkan.MemoryPropertyDeviceLocalBit)
76+ if err != nil {
77+ return nil, err
78+ }
79+ ded := vulkan.MemoryDedicatedAllocateInfo{SType: vulkan.StructureTypeMemoryDedicatedAllocateInfo, Image: img.image}
80+ exp := vulkan.ExportMemoryAllocateInfo{SType: vulkan.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&ded), HandleTypes: vulkan.ExternalMemoryHandleTypeDMABUFBitEXT}
81+ ai := vulkan.MemoryAllocateInfo{SType: vulkan.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&exp), AllocationSize: reqs.MemoryRequirements.Size, MemoryTypeIndex: idx}
82+ if err = vulkan.Check(d.disp.AllocateMemory(d.logical, &ai, nil, &img.memory)); err != nil {
83+ return nil, fmt.Errorf("gpu: allocate image memory: %w", err)
84+ }
85+ bind := vulkan.BindImageMemoryInfo{SType: vulkan.StructureTypeBindImageMemoryInfo, Image: img.image, Memory: img.memory}
86+ if err = vulkan.Check(d.disp.BindImageMemory2(d.logical, 1, &bind)); err != nil {
87+ return nil, fmt.Errorf("gpu: bind image memory: %w", err)
88+ }
89+ fdi := vulkan.MemoryGetFdInfoKHR{SType: vulkan.StructureTypeMemoryGetFDInfoKHR, Memory: img.memory, HandleType: vulkan.ExternalMemoryHandleTypeDMABUFBitEXT}
90+ fd := int32(-1)
91+ if err = vulkan.Check(d.disp.GetMemoryFdKHR(d.logical, &fdi, &fd)); err != nil {
92+ return nil, fmt.Errorf("gpu: export DMA-BUF: %w", err)
93+ }
94+ img.FD = int(fd)
95+ vi := vulkan.ImageViewCreateInfo{
96+ SType: vulkan.StructureTypeImageViewCreateInfo, Image: img.image, ViewType: vulkan.ImageViewType2d,
97+ Format: vulkan.FormatB8g8r8a8Unorm,
98+ SubresourceRange: vulkan.ImageSubresourceRange{AspectMask: vulkan.ImageAspectColorBit, LevelCount: 1, LayerCount: 1},
99+ }
100+ if err = vulkan.Check(d.disp.CreateImageView(d.logical, &vi, nil, &img.view)); err != nil {
101+ return nil, err
102+ }
103+ return img, nil
104+}
105+
106+// Close frees the image. The GPU must be done with it; the compositor's import
107+// holds its own kernel reference.
108+func (i *Image) Close() {
109+ if i == nil || i.dev == nil {
110+ return
111+ }
112+ d := i.dev
113+ if i.FD >= 0 {
114+ _ = unix.Close(i.FD)
115+ i.FD = -1
116+ }
117+ if i.view != 0 {
118+ d.disp.DestroyImageView(d.logical, i.view, nil)
119+ i.view = 0
120+ }
121+ if i.image != 0 {
122+ d.disp.DestroyImage(d.logical, i.image, nil)
123+ i.image = 0
124+ }
125+ if i.memory != 0 {
126+ d.disp.FreeMemory(d.logical, i.memory, nil)
127+ i.memory = 0
128+ }
129+}
@@ -0,0 +1,129 @@
1+package gpu
2+
3+import (
4+ "fmt"
5+ "math"
6+ "unsafe"
7+
8+ "github.com/bnema/purego-vulkan/vulkan"
9+ "golang.org/x/sys/unix"
10+)
11+
12+// Image is a render target backed by exportable DMA-BUF memory. FD stays owned
13+// by the Image; the Wayland layer duplicates it on import.
14+type Image struct {
15+ dev *Device
16+ image vulkan.Image
17+ memory vulkan.DeviceMemory
18+ view vulkan.ImageView
19+
20+ Width, Height int32
21+ FD int
22+ Modifier uint64
23+ Offset, Stride uint32
24+
25+ // used is false until the first render: the first transition starts from
26+ // an undefined layout.
27+ used bool
28+}
29+
30+// NewImage allocates a Width x Height B8G8R8A8 image with the given modifier.
31+func (d *Device) NewImage(width, height int32, mod Modifier) (img *Image, err error) {
32+ if width <= 0 || height <= 0 {
33+ return nil, fmt.Errorf("gpu: invalid image size %dx%d", width, height)
34+ }
35+ img = &Image{dev: d, Width: width, Height: height, FD: -1}
36+ defer func() {
37+ if err != nil {
38+ img.Close()
39+ img = nil
40+ }
41+ }()
42+ modifier := uint64(mod)
43+ list := vulkan.ImageDrmFormatModifierListCreateInfoEXT{SType: vulkan.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: 1, DrmFormatModifiers: &modifier}
44+ ext := vulkan.ExternalMemoryImageCreateInfo{SType: vulkan.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vulkan.ExternalMemoryHandleTypeDMABUFBitEXT}
45+ ci := vulkan.ImageCreateInfo{
46+ SType: vulkan.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&ext),
47+ ImageType: vulkan.ImageType2d, Format: vulkan.FormatB8g8r8a8Unorm,
48+ Extent: vulkan.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1},
49+ MipLevels: 1, ArrayLayers: 1, Samples: vulkan.SampleCount1Bit,
50+ Tiling: vulkan.ImageTilingDRMFormatModifierEXT, Usage: vulkan.ImageUsageColorAttachmentBit,
51+ SharingMode: vulkan.SharingModeExclusive, InitialLayout: vulkan.ImageLayoutUndefined,
52+ }
53+ if err = vulkan.Check(d.disp.CreateImage(d.logical, &ci, nil, &img.image)); err != nil {
54+ return nil, fmt.Errorf("gpu: create image: %w", err)
55+ }
56+ props := vulkan.ImageDrmFormatModifierPropertiesEXT{SType: vulkan.StructureTypeImageDRMFormatModifierPropertiesEXT}
57+ if err = vulkan.Check(d.disp.GetImageDrmFormatModifierPropertiesEXT(d.logical, img.image, &props)); err != nil {
58+ return nil, err
59+ }
60+ if props.DrmFormatModifier != modifier {
61+ return nil, fmt.Errorf("gpu: asked for modifier %#x, driver chose %#x", modifier, props.DrmFormatModifier)
62+ }
63+ img.Modifier = modifier
64+ sub := vulkan.ImageSubresource{AspectMask: vulkan.ImageAspectMemoryPlane0BitEXT}
65+ var layout vulkan.SubresourceLayout
66+ d.disp.GetImageSubresourceLayout(d.logical, img.image, &sub, &layout)
67+ if layout.RowPitch == 0 || layout.RowPitch > math.MaxUint32 || layout.Offset > math.MaxUint32 {
68+ return nil, fmt.Errorf("gpu: unusable plane layout offset %d stride %d", layout.Offset, layout.RowPitch)
69+ }
70+ img.Offset, img.Stride = uint32(layout.Offset), uint32(layout.RowPitch)
71+
72+ reqInfo := vulkan.ImageMemoryRequirementsInfo2{SType: vulkan.StructureTypeImageMemoryRequirementsInfo2, Image: img.image}
73+ reqs := vulkan.MemoryRequirements2{SType: vulkan.StructureTypeMemoryRequirements2}
74+ d.disp.GetImageMemoryRequirements2(d.logical, &reqInfo, &reqs)
75+ idx, err := d.memoryType(reqs.MemoryRequirements.MemoryTypeBits, vulkan.MemoryPropertyDeviceLocalBit)
76+ if err != nil {
77+ return nil, err
78+ }
79+ ded := vulkan.MemoryDedicatedAllocateInfo{SType: vulkan.StructureTypeMemoryDedicatedAllocateInfo, Image: img.image}
80+ exp := vulkan.ExportMemoryAllocateInfo{SType: vulkan.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&ded), HandleTypes: vulkan.ExternalMemoryHandleTypeDMABUFBitEXT}
81+ ai := vulkan.MemoryAllocateInfo{SType: vulkan.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&exp), AllocationSize: reqs.MemoryRequirements.Size, MemoryTypeIndex: idx}
82+ if err = vulkan.Check(d.disp.AllocateMemory(d.logical, &ai, nil, &img.memory)); err != nil {
83+ return nil, fmt.Errorf("gpu: allocate image memory: %w", err)
84+ }
85+ bind := vulkan.BindImageMemoryInfo{SType: vulkan.StructureTypeBindImageMemoryInfo, Image: img.image, Memory: img.memory}
86+ if err = vulkan.Check(d.disp.BindImageMemory2(d.logical, 1, &bind)); err != nil {
87+ return nil, fmt.Errorf("gpu: bind image memory: %w", err)
88+ }
89+ fdi := vulkan.MemoryGetFdInfoKHR{SType: vulkan.StructureTypeMemoryGetFDInfoKHR, Memory: img.memory, HandleType: vulkan.ExternalMemoryHandleTypeDMABUFBitEXT}
90+ fd := int32(-1)
91+ if err = vulkan.Check(d.disp.GetMemoryFdKHR(d.logical, &fdi, &fd)); err != nil {
92+ return nil, fmt.Errorf("gpu: export DMA-BUF: %w", err)
93+ }
94+ img.FD = int(fd)
95+ vi := vulkan.ImageViewCreateInfo{
96+ SType: vulkan.StructureTypeImageViewCreateInfo, Image: img.image, ViewType: vulkan.ImageViewType2d,
97+ Format: vulkan.FormatB8g8r8a8Unorm,
98+ SubresourceRange: vulkan.ImageSubresourceRange{AspectMask: vulkan.ImageAspectColorBit, LevelCount: 1, LayerCount: 1},
99+ }
100+ if err = vulkan.Check(d.disp.CreateImageView(d.logical, &vi, nil, &img.view)); err != nil {
101+ return nil, err
102+ }
103+ return img, nil
104+}
105+
106+// Close frees the image. The GPU must be done with it; the compositor's import
107+// holds its own kernel reference.
108+func (i *Image) Close() {
109+ if i == nil || i.dev == nil {
110+ return
111+ }
112+ d := i.dev
113+ if i.FD >= 0 {
114+ _ = unix.Close(i.FD)
115+ i.FD = -1
116+ }
117+ if i.view != 0 {
118+ d.disp.DestroyImageView(d.logical, i.view, nil)
119+ i.view = 0
120+ }
121+ if i.image != 0 {
122+ d.disp.DestroyImage(d.logical, i.image, nil)
123+ i.image = 0
124+ }
125+ if i.memory != 0 {
126+ d.disp.FreeMemory(d.logical, i.memory, nil)
127+ i.memory = 0
128+ }
129+}
A internal/gpu/renderer.go +689 -0
@@ -0,0 +1,689 @@
1+package gpu
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "fmt"
7+ "math"
8+ "unsafe"
9+
10+ "github.com/bnema/purego-vulkan/vulkan"
11+ "golang.org/x/sys/unix"
12+
13+ "git.bnema.dev/bnema/neferbar/internal/glyph"
14+ "git.bnema.dev/bnema/neferbar/internal/gpu/shaders"
15+ "git.bnema.dev/bnema/neferbar/internal/syncobj"
16+)
17+
18+const (
19+ atlasSize = 2048
20+ // SlotCount is the number of DMA-BUF images: one on screen, one being
21+ // shown or queued, one being drawn.
22+ SlotCount = 3
23+ // maxUploads bounds the glyphs rasterized by one Draw.
24+ maxUploads = 256
25+)
26+
27+// Cell is one grid cell as the renderer consumes it.
28+type Cell struct {
29+ Rune rune
30+ FG, BG [3]uint8
31+ Bold bool
32+}
33+
34+// instance is the per-cell vertex data (64 bytes, four vec4).
35+type instance struct {
36+ Rect [4]float32 // x, y, w, h in pixels
37+ UV [4]float32 // atlas u0, v0, u1, v1
38+ FG [4]float32
39+ BG [4]float32
40+}
41+
42+type glyphKey struct {
43+ r rune
44+ bold bool
45+}
46+
47+type slotState uint8
48+
49+const (
50+ slotFree slotState = iota
51+ slotShown
52+)
53+
54+// slot is one DMA-BUF image with everything needed to draw and present it.
55+type slot struct {
56+ img *Image
57+ pool vulkan.CommandPool
58+ cmd vulkan.CommandBuffer
59+ fence vulkan.Fence
60+
61+ instBuf, stageBuf vulkan.Buffer
62+ instMem, stageMem vulkan.DeviceMemory
63+ inst []instance // mapped
64+ stage []byte // mapped
65+
66+ releaseHandle uint32
67+ releaseFD int // syncobj fd, exported once
68+ eventFD int // readable when releasePoint signals
69+ releasePoint uint64
70+ presented bool
71+ state slotState
72+
73+ // Scratch argument structs: the bindings heap-allocate pointer arguments,
74+ // so a call built from locals allocates on every frame.
75+ begin vulkan.CommandBufferBeginInfo
76+ barrier [1]vulkan.ImageMemoryBarrier2
77+ dep vulkan.DependencyInfo
78+ color vulkan.RenderingAttachmentInfo
79+ render vulkan.RenderingInfo
80+ cmdInfo vulkan.CommandBufferSubmitInfo
81+ submit vulkan.SubmitInfo2
82+ offset vulkan.DeviceSize
83+ push [2]float32
84+ regions []vulkan.BufferImageCopy
85+ sets [1]vulkan.DescriptorSet
86+ atlasSub vulkan.ImageSubresourceRange
87+}
88+
89+// Frame describes one drawn slot for the Wayland layer. Fields stay valid
90+// until the next Draw.
91+type Frame struct {
92+ Slot int
93+ NewBuffer bool // import Image as a wl_buffer
94+ Image *Image
95+ AcquirePoint uint64
96+ ReleasePoint uint64
97+ ReleaseEventFD int // readable when ReleasePoint signals; stable per slot
98+ ReleaseTimeline int // syncobj fd of the slot's release timeline; valid with NewBuffer
99+}
100+
101+// Renderer draws a row of cells into a pool of DMA-BUF images.
102+type Renderer struct {
103+ dev *Device
104+ node *syncobj.Node
105+ face *glyph.Face
106+
107+ W, H int32
108+ cols, maxCols int
109+ yOff float32
110+
111+ slots [SlotCount]slot
112+
113+ acquireHandle uint32
114+ acquireFD int
115+ acquirePoint uint64
116+
117+ // atlas
118+ atlasImg vulkan.Image
119+ atlasMem vulkan.DeviceMemory
120+ atlasView vulkan.ImageView
121+ sampler vulkan.Sampler
122+ setLayout vulkan.DescriptorSetLayout
123+ descPool vulkan.DescriptorPool
124+ descSet vulkan.DescriptorSet
125+ pipeLayout vulkan.PipelineLayout
126+ pipeline vulkan.Pipeline
127+ atlasCols int
128+ atlasRows int
129+ glyphs map[glyphKey]uint32
130+ nextGlyph uint32
131+ atlasInited bool
132+
133+ uploadKeys []glyphKey // glyphs added during the current Draw
134+ uploadIdx []uint32
135+}
136+
137+// NewRenderer builds the pipeline, the atlas and the image pool for a w x h
138+// target. node must be the render node of dev.
139+func NewRenderer(dev *Device, node *syncobj.Node, mod Modifier, face *glyph.Face, w, h int32) (r *Renderer, err error) {
140+ if face.CellH > int(h) {
141+ return nil, fmt.Errorf("gpu: cell height %d exceeds bar height %d", face.CellH, h)
142+ }
143+ r = &Renderer{dev: dev, node: node, face: face, W: w, H: h, acquireFD: -1,
144+ glyphs: make(map[glyphKey]uint32, 256), yOff: float32((int(h) - face.CellH) / 2)}
145+ r.cols = (int(w) + face.CellW - 1) / face.CellW
146+ r.maxCols = r.cols
147+ for i := range r.slots {
148+ r.slots[i].releaseFD, r.slots[i].eventFD = -1, -1
149+ }
150+ defer func() {
151+ if err != nil {
152+ r.Close()
153+ r = nil
154+ }
155+ }()
156+ if err = r.newAtlas(); err != nil {
157+ return nil, err
158+ }
159+ if err = r.newPipeline(); err != nil {
160+ return nil, err
161+ }
162+ if r.acquireHandle, err = node.Create(); err != nil {
163+ return nil, err
164+ }
165+ if r.acquireFD, err = node.Export(r.acquireHandle); err != nil {
166+ return nil, err
167+ }
168+ for i := range r.slots {
169+ if err = r.newSlot(i, mod); err != nil {
170+ return nil, err
171+ }
172+ }
173+ // Glyph 0 is the blank cell. Printable ASCII is assigned now and uploaded
174+ // with the first frame.
175+ r.glyphs[glyphKey{' ', false}] = 0
176+ r.nextGlyph = 1
177+ for c := rune(0x21); c < 0x7f; c++ {
178+ r.glyphs[glyphKey{c, false}] = r.nextGlyph
179+ r.nextGlyph++
180+ }
181+ r.uploadKeys = make([]glyphKey, 0, maxUploads)
182+ r.uploadIdx = make([]uint32, 0, maxUploads)
183+ return r, nil
184+}
185+
186+func (r *Renderer) buffer(size vulkan.DeviceSize, usage vulkan.BufferUsageFlags) (vulkan.Buffer, vulkan.DeviceMemory, unsafe.Pointer, error) {
187+ d := r.dev
188+ var buf vulkan.Buffer
189+ ci := vulkan.BufferCreateInfo{SType: vulkan.StructureTypeBufferCreateInfo, Size: size, Usage: usage, SharingMode: vulkan.SharingModeExclusive}
190+ if err := vulkan.Check(d.disp.CreateBuffer(d.logical, &ci, nil, &buf)); err != nil {
191+ return 0, 0, nil, err
192+ }
193+ var req vulkan.MemoryRequirements
194+ d.disp.GetBufferMemoryRequirements(d.logical, buf, &req)
195+ idx, err := d.memoryType(req.MemoryTypeBits, vulkan.MemoryPropertyHostVisibleBit|vulkan.MemoryPropertyHostCoherentBit)
196+ if err != nil {
197+ d.disp.DestroyBuffer(d.logical, buf, nil)
198+ return 0, 0, nil, err
199+ }
200+ var mem vulkan.DeviceMemory
201+ ai := vulkan.MemoryAllocateInfo{SType: vulkan.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: idx}
202+ if err = vulkan.Check(d.disp.AllocateMemory(d.logical, &ai, nil, &mem)); err != nil {
203+ d.disp.DestroyBuffer(d.logical, buf, nil)
204+ return 0, 0, nil, err
205+ }
206+ fail := func(err error) (vulkan.Buffer, vulkan.DeviceMemory, unsafe.Pointer, error) {
207+ d.disp.FreeMemory(d.logical, mem, nil)
208+ d.disp.DestroyBuffer(d.logical, buf, nil)
209+ return 0, 0, nil, err
210+ }
211+ if err = vulkan.Check(d.disp.BindBufferMemory(d.logical, buf, mem, 0)); err != nil {
212+ return fail(err)
213+ }
214+ var ptr unsafe.Pointer
215+ if err = vulkan.Check(d.disp.MapMemory(d.logical, mem, 0, size, 0, &ptr)); err != nil {
216+ return fail(err)
217+ }
218+ return buf, mem, ptr, nil
219+}
220+
221+func (r *Renderer) newSlot(i int, mod Modifier) (err error) {
222+ d := r.dev
223+ s := &r.slots[i]
224+ if s.img, err = d.NewImage(r.W, r.H, mod); err != nil {
225+ return err
226+ }
227+ pi := vulkan.CommandPoolCreateInfo{SType: vulkan.StructureTypeCommandPoolCreateInfo, QueueFamilyIndex: d.family, Flags: vulkan.CommandPoolCreateResetCommandBufferBit}
228+ if err = vulkan.Check(d.disp.CreateCommandPool(d.logical, &pi, nil, &s.pool)); err != nil {
229+ return err
230+ }
231+ ai := vulkan.CommandBufferAllocateInfo{SType: vulkan.StructureTypeCommandBufferAllocateInfo, CommandPool: s.pool, Level: vulkan.CommandBufferLevelPrimary, CommandBufferCount: 1}
232+ if err = vulkan.Check(d.disp.AllocateCommandBuffers(d.logical, &ai, &s.cmd)); err != nil {
233+ return err
234+ }
235+ fi := vulkan.FenceCreateInfo{SType: vulkan.StructureTypeFenceCreateInfo}
236+ if err = vulkan.Check(d.disp.CreateFence(d.logical, &fi, nil, &s.fence)); err != nil {
237+ return err
238+ }
239+ instBytes := vulkan.DeviceSize(r.maxCols) * vulkan.DeviceSize(unsafe.Sizeof(instance{}))
240+ var p unsafe.Pointer
241+ if s.instBuf, s.instMem, p, err = r.buffer(instBytes, vulkan.BufferUsageVertexBufferBit); err != nil {
242+ return err
243+ }
244+ s.inst = unsafe.Slice((*instance)(p), r.maxCols)
245+ stageBytes := maxUploads * r.face.CellW * r.face.CellH
246+ if s.stageBuf, s.stageMem, p, err = r.buffer(vulkan.DeviceSize(stageBytes), vulkan.BufferUsageTransferSrcBit); err != nil {
247+ return err
248+ }
249+ s.stage = unsafe.Slice((*byte)(p), stageBytes)
250+ s.regions = make([]vulkan.BufferImageCopy, 0, maxUploads)
251+ s.atlasSub = vulkan.ImageSubresourceRange{AspectMask: vulkan.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
252+ if s.releaseHandle, err = r.node.Create(); err != nil {
253+ return err
254+ }
255+ if s.releaseFD, err = r.node.Export(s.releaseHandle); err != nil {
256+ return err
257+ }
258+ if s.eventFD, err = unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK); err != nil {
259+ return err
260+ }
261+ return nil
262+}
263+
264+func (r *Renderer) newAtlas() error {
265+ d := r.dev
266+ r.atlasCols = atlasSize / r.face.CellW
267+ r.atlasRows = atlasSize / r.face.CellH
268+ if r.atlasCols < 1 || r.atlasRows < 1 {
269+ return fmt.Errorf("gpu: cell %dx%d does not fit the atlas", r.face.CellW, r.face.CellH)
270+ }
271+ ci := vulkan.ImageCreateInfo{
272+ SType: vulkan.StructureTypeImageCreateInfo, ImageType: vulkan.ImageType2d, Format: vulkan.FormatR8Unorm,
273+ Extent: vulkan.Extent3D{Width: atlasSize, Height: atlasSize, Depth: 1},
274+ MipLevels: 1, ArrayLayers: 1, Samples: vulkan.SampleCount1Bit, Tiling: vulkan.ImageTilingOptimal,
275+ Usage: vulkan.ImageUsageSampledBit | vulkan.ImageUsageTransferDstBit,
276+ SharingMode: vulkan.SharingModeExclusive, InitialLayout: vulkan.ImageLayoutUndefined,
277+ }
278+ if err := vulkan.Check(d.disp.CreateImage(d.logical, &ci, nil, &r.atlasImg)); err != nil {
279+ return fmt.Errorf("gpu: create atlas: %w", err)
280+ }
281+ var req vulkan.MemoryRequirements
282+ d.disp.GetImageMemoryRequirements(d.logical, r.atlasImg, &req)
283+ idx, err := d.memoryType(req.MemoryTypeBits, vulkan.MemoryPropertyDeviceLocalBit)
284+ if err != nil {
285+ return err
286+ }
287+ ai := vulkan.MemoryAllocateInfo{SType: vulkan.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: idx}
288+ if err = vulkan.Check(d.disp.AllocateMemory(d.logical, &ai, nil, &r.atlasMem)); err != nil {
289+ return err
290+ }
291+ if err = vulkan.Check(d.disp.BindImageMemory(d.logical, r.atlasImg, r.atlasMem, 0)); err != nil {
292+ return err
293+ }
294+ vi := vulkan.ImageViewCreateInfo{
295+ SType: vulkan.StructureTypeImageViewCreateInfo, Image: r.atlasImg, ViewType: vulkan.ImageViewType2d, Format: vulkan.FormatR8Unorm,
296+ SubresourceRange: vulkan.ImageSubresourceRange{AspectMask: vulkan.ImageAspectColorBit, LevelCount: 1, LayerCount: 1},
297+ }
298+ if err = vulkan.Check(d.disp.CreateImageView(d.logical, &vi, nil, &r.atlasView)); err != nil {
299+ return err
300+ }
301+ si := vulkan.SamplerCreateInfo{
302+ SType: vulkan.StructureTypeSamplerCreateInfo, MagFilter: vulkan.FilterNearest, MinFilter: vulkan.FilterNearest,
303+ MipmapMode: vulkan.SamplerMipmapModeNearest,
304+ AddressModeU: vulkan.SamplerAddressModeClampToEdge, AddressModeV: vulkan.SamplerAddressModeClampToEdge, AddressModeW: vulkan.SamplerAddressModeClampToEdge,
305+ }
306+ if err = vulkan.Check(d.disp.CreateSampler(d.logical, &si, nil, &r.sampler)); err != nil {
307+ return err
308+ }
309+ b := vulkan.DescriptorSetLayoutBinding{DescriptorType: vulkan.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vulkan.ShaderStageFragmentBit}
310+ li := vulkan.DescriptorSetLayoutCreateInfo{SType: vulkan.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 1, Bindings: &b}
311+ if err = vulkan.Check(d.disp.CreateDescriptorSetLayout(d.logical, &li, nil, &r.setLayout)); err != nil {
312+ return err
313+ }
314+ ps := vulkan.DescriptorPoolSize{Type: vulkan.DescriptorTypeCombinedImageSampler, DescriptorCount: 1}
315+ pi := vulkan.DescriptorPoolCreateInfo{SType: vulkan.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &ps}
316+ if err = vulkan.Check(d.disp.CreateDescriptorPool(d.logical, &pi, nil, &r.descPool)); err != nil {
317+ return err
318+ }
319+ ds := vulkan.DescriptorSetAllocateInfo{SType: vulkan.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: r.descPool, DescriptorSetCount: 1, SetLayouts: &r.setLayout}
320+ if err = vulkan.Check(d.disp.AllocateDescriptorSets(d.logical, &ds, &r.descSet)); err != nil {
321+ return err
322+ }
323+ info := vulkan.DescriptorImageInfo{Sampler: r.sampler, ImageView: r.atlasView, ImageLayout: vulkan.ImageLayoutShaderReadOnlyOptimal}
324+ w := vulkan.WriteDescriptorSet{SType: vulkan.StructureTypeWriteDescriptorSet, DstSet: r.descSet, DescriptorCount: 1, DescriptorType: vulkan.DescriptorTypeCombinedImageSampler, ImageInfo: &info}
325+ d.disp.UpdateDescriptorSets(d.logical, 1, &w, 0, nil)
326+ return nil
327+}
328+
329+func (r *Renderer) shader(code []byte) (vulkan.ShaderModule, error) {
330+ if len(code) < 20 || len(code)%4 != 0 || binary.LittleEndian.Uint32(code) != 0x07230203 {
331+ return 0, errors.New("gpu: invalid embedded SPIR-V")
332+ }
333+ words := make([]uint32, len(code)/4)
334+ for i := range words {
335+ words[i] = le32(code[i*4:])
336+ }
337+ ci := vulkan.ShaderModuleCreateInfo{SType: vulkan.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(code)), Code: &words[0]}
338+ var m vulkan.ShaderModule
339+ if err := vulkan.Check(r.dev.disp.CreateShaderModule(r.dev.logical, &ci, nil, &m)); err != nil {
340+ return 0, err
341+ }
342+ return m, nil
343+}
344+
345+func (r *Renderer) newPipeline() error {
346+ d := r.dev
347+ pc := vulkan.PushConstantRange{StageFlags: vulkan.ShaderStageVertexBit, Size: 8}
348+ li := vulkan.PipelineLayoutCreateInfo{SType: vulkan.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &r.setLayout, PushConstantRangeCount: 1, PushConstantRanges: &pc}
349+ if err := vulkan.Check(d.disp.CreatePipelineLayout(d.logical, &li, nil, &r.pipeLayout)); err != nil {
350+ return err
351+ }
352+ vs, err := r.shader(shaders.CellVertex)
353+ if err != nil {
354+ return err
355+ }
356+ defer d.disp.DestroyShaderModule(d.logical, vs, nil)
357+ fs, err := r.shader(shaders.CellFragment)
358+ if err != nil {
359+ return err
360+ }
361+ defer d.disp.DestroyShaderModule(d.logical, fs, nil)
362+ entry := []byte("main\x00")
363+ stages := [2]vulkan.PipelineShaderStageCreateInfo{
364+ {SType: vulkan.StructureTypePipelineShaderStageCreateInfo, Stage: vulkan.ShaderStageVertexBit, Module: vs, Name: &entry[0]},
365+ {SType: vulkan.StructureTypePipelineShaderStageCreateInfo, Stage: vulkan.ShaderStageFragmentBit, Module: fs, Name: &entry[0]},
366+ }
367+ var attrs [4]vulkan.VertexInputAttributeDescription
368+ for i := range attrs {
369+ attrs[i] = vulkan.VertexInputAttributeDescription{Location: uint32(i), Format: vulkan.FormatR32g32b32a32Sfloat, Offset: uint32(i) * 16}
370+ }
371+ bind := vulkan.VertexInputBindingDescription{Stride: uint32(unsafe.Sizeof(instance{})), InputRate: vulkan.VertexInputRateInstance}
372+ vin := vulkan.PipelineVertexInputStateCreateInfo{SType: vulkan.StructureTypePipelineVertexInputStateCreateInfo, VertexBindingDescriptionCount: 1, VertexBindingDescriptions: &bind, VertexAttributeDescriptionCount: 4, VertexAttributeDescriptions: &attrs[0]}
373+ asm := vulkan.PipelineInputAssemblyStateCreateInfo{SType: vulkan.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vulkan.PrimitiveTopologyTriangleList}
374+ vp := vulkan.Viewport{Width: float32(r.W), Height: float32(r.H), MaxDepth: 1}
375+ sc := vulkan.Rect2D{Extent: vulkan.Extent2D{Width: uint32(r.W), Height: uint32(r.H)}}
376+ vps := vulkan.PipelineViewportStateCreateInfo{SType: vulkan.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &vp, ScissorCount: 1, Scissors: &sc}
377+ ras := vulkan.PipelineRasterizationStateCreateInfo{SType: vulkan.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vulkan.PolygonModeFill, CullMode: vulkan.CullModeNone, FrontFace: vulkan.FrontFaceCounterClockwise, LineWidth: 1}
378+ ms := vulkan.PipelineMultisampleStateCreateInfo{SType: vulkan.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vulkan.SampleCount1Bit}
379+ att := vulkan.PipelineColorBlendAttachmentState{ColorWriteMask: vulkan.ColorComponentRBit | vulkan.ColorComponentGBit | vulkan.ColorComponentBBit | vulkan.ColorComponentABit}
380+ blend := vulkan.PipelineColorBlendStateCreateInfo{SType: vulkan.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &att}
381+ format := vulkan.Format(vulkan.FormatB8g8r8a8Unorm)
382+ rend := vulkan.PipelineRenderingCreateInfo{SType: vulkan.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format}
383+ gi := vulkan.GraphicsPipelineCreateInfo{
384+ SType: vulkan.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rend), StageCount: 2, Stages: &stages[0],
385+ VertexInputState: &vin, InputAssemblyState: &asm, ViewportState: &vps, RasterizationState: &ras,
386+ MultisampleState: &ms, ColorBlendState: &blend, Layout: r.pipeLayout,
387+ }
388+ if err = vulkan.Check(d.disp.CreateGraphicsPipelines(d.logical, 0, 1, &gi, nil, &r.pipeline)); err != nil {
389+ return fmt.Errorf("gpu: create pipeline: %w", err)
390+ }
391+ return nil
392+}
393+
394+// queueGlyph schedules k for upload to atlas index idx in the current Draw.
395+func (r *Renderer) queueGlyph(k glyphKey, idx uint32) {
396+ r.uploadKeys = append(r.uploadKeys, k)
397+ r.uploadIdx = append(r.uploadIdx, idx)
398+}
399+
400+// glyph returns the atlas index of k, assigning one (and queueing its upload)
401+// on first use. An exhausted atlas maps to the blank glyph.
402+func (r *Renderer) glyph(k glyphKey) uint32 {
403+ if idx, ok := r.glyphs[k]; ok {
404+ return idx
405+ }
406+ if int(r.nextGlyph) >= r.atlasCols*r.atlasRows || len(r.uploadKeys) >= maxUploads {
407+ return 0
408+ }
409+ idx := r.nextGlyph
410+ r.nextGlyph++
411+ r.glyphs[k] = idx
412+ r.queueGlyph(k, idx)
413+ return idx
414+}
415+
416+// pickSlot returns a free slot index, or -1.
417+func (r *Renderer) pickSlot() int {
418+ for i := range r.slots {
419+ if r.slots[i].state == slotFree {
420+ return i
421+ }
422+ }
423+ return -1
424+}
425+
426+// Draw renders cells (len(cells) <= cols) into a free slot, waits for the GPU
427+// and signals the acquire point. It reports false when every image is still
428+// held by the compositor. clear is the background for pixels outside cells.
429+func (r *Renderer) Draw(cells []Cell, clear [3]uint8, out *Frame) (bool, error) {
430+ n := r.pickSlot()
431+ if n < 0 {
432+ return false, nil
433+ }
434+ s := &r.slots[n]
435+ if len(cells) > r.maxCols {
436+ cells = cells[:r.maxCols]
437+ }
438+ r.uploadKeys, r.uploadIdx = r.uploadKeys[:0], r.uploadIdx[:0]
439+ if !r.atlasInited {
440+ r.queueGlyph(glyphKey{' ', false}, 0)
441+ for c := rune(0x21); c < 0x7f; c++ {
442+ r.queueGlyph(glyphKey{c, false}, r.glyphs[glyphKey{c, false}])
443+ }
444+ }
445+ cw, ch := float32(r.face.CellW), float32(r.face.CellH)
446+ inv := float32(1) / atlasSize
447+ for i := range cells {
448+ c := &cells[i]
449+ gi := r.glyph(glyphKey{c.Rune, c.Bold})
450+ gx, gy := float32(int(gi)%r.atlasCols)*cw, float32(int(gi)/r.atlasCols)*ch
451+ in := &s.inst[i]
452+ in.Rect = [4]float32{float32(i) * cw, r.yOff, cw, ch}
453+ in.UV = [4]float32{gx * inv, gy * inv, (gx + cw) * inv, (gy + ch) * inv}
454+ in.FG = [4]float32{float32(c.FG[0]) / 255, float32(c.FG[1]) / 255, float32(c.FG[2]) / 255, 1}
455+ in.BG = [4]float32{float32(c.BG[0]) / 255, float32(c.BG[1]) / 255, float32(c.BG[2]) / 255, 1}
456+ }
457+ cellBytes := r.face.CellW * r.face.CellH
458+ s.regions = s.regions[:0]
459+ for i, k := range r.uploadKeys {
460+ off := i * cellBytes
461+ r.face.Rasterize(k.r, k.bold, s.stage[off:off+cellBytes])
462+ gi := int(r.uploadIdx[i])
463+ s.regions = append(s.regions, vulkan.BufferImageCopy{
464+ BufferOffset: vulkan.DeviceSize(off),
465+ BufferRowLength: uint32(r.face.CellW),
466+ ImageSubresource: vulkan.ImageSubresourceLayers{AspectMask: vulkan.ImageAspectColorBit, LayerCount: 1},
467+ ImageOffset: vulkan.Offset3D{X: int32((gi % r.atlasCols) * r.face.CellW), Y: int32((gi / r.atlasCols) * r.face.CellH)},
468+ ImageExtent: vulkan.Extent3D{Width: uint32(r.face.CellW), Height: uint32(r.face.CellH), Depth: 1},
469+ })
470+ }
471+ if err := r.record(s, len(cells), clear); err != nil {
472+ return false, err
473+ }
474+ d := r.dev
475+ s.cmdInfo = vulkan.CommandBufferSubmitInfo{SType: vulkan.StructureTypeCommandBufferSubmitInfo, CommandBuffer: s.cmd}
476+ s.submit = vulkan.SubmitInfo2{SType: vulkan.StructureTypeSubmitInfo2, CommandBufferInfoCount: 1, CommandBufferInfos: &s.cmdInfo}
477+ if err := vulkan.Check(d.disp.QueueSubmit2(d.queue, 1, &s.submit, s.fence)); err != nil {
478+ return false, fmt.Errorf("gpu: submit: %w", err)
479+ }
480+ // The draw is tiny: wait for it here, then signal the acquire point from
481+ // the CPU. No sync-file import is needed.
482+ if err := vulkan.Check(d.disp.WaitForFences(d.logical, 1, &s.fence, 1, math.MaxUint64)); err != nil {
483+ return false, fmt.Errorf("gpu: wait fence: %w", err)
484+ }
485+ if err := vulkan.Check(d.disp.ResetFences(d.logical, 1, &s.fence)); err != nil {
486+ return false, err
487+ }
488+ r.atlasInited = true
489+ r.acquirePoint++
490+ if err := r.node.Signal(r.acquireHandle, r.acquirePoint); err != nil {
491+ return false, err
492+ }
493+ s.releasePoint++
494+ if err := r.node.EventFD(s.releaseHandle, s.releasePoint, s.eventFD); err != nil {
495+ return false, err
496+ }
497+ s.state = slotShown
498+ *out = Frame{
499+ Slot: n, NewBuffer: !s.presented, Image: s.img,
500+ AcquirePoint: r.acquirePoint, ReleasePoint: s.releasePoint,
501+ ReleaseEventFD: s.eventFD, ReleaseTimeline: s.releaseFD,
502+ }
503+ s.presented = true
504+ return true, nil
505+}
506+
507+func (r *Renderer) record(s *slot, count int, clearRGB [3]uint8) error {
508+ d := r.dev
509+ disp := d.disp
510+ if err := vulkan.Check(disp.ResetCommandBuffer(s.cmd, 0)); err != nil {
511+ return err
512+ }
513+ s.begin = vulkan.CommandBufferBeginInfo{SType: vulkan.StructureTypeCommandBufferBeginInfo, Flags: vulkan.CommandBufferUsageOneTimeSubmitBit}
514+ if err := vulkan.Check(disp.BeginCommandBuffer(s.cmd, &s.begin)); err != nil {
515+ return err
516+ }
517+ s.dep = vulkan.DependencyInfo{SType: vulkan.StructureTypeDependencyInfo, ImageMemoryBarrierCount: 1, ImageMemoryBarriers: &s.barrier[0]}
518+ barrier := &s.barrier[0]
519+ if len(s.regions) > 0 || !r.atlasInited {
520+ old := vulkan.ImageLayout(vulkan.ImageLayoutShaderReadOnlyOptimal)
521+ srcStage, srcAccess := vulkan.PipelineStageFlags2(vulkan.PipelineStage2FragmentShaderBit), vulkan.AccessFlags2(vulkan.Access2ShaderSampledReadBit)
522+ if !r.atlasInited {
523+ old, srcStage, srcAccess = vulkan.ImageLayoutUndefined, vulkan.PipelineStage2None, 0
524+ }
525+ *barrier = vulkan.ImageMemoryBarrier2{
526+ SType: vulkan.StructureTypeImageMemoryBarrier2, SrcStageMask: srcStage, SrcAccessMask: srcAccess,
527+ DstStageMask: vulkan.PipelineStage2AllTransferBit, DstAccessMask: vulkan.Access2TransferWriteBit,
528+ OldLayout: old, NewLayout: vulkan.ImageLayoutTransferDstOptimal,
529+ SrcQueueFamilyIndex: ^uint32(0), DstQueueFamilyIndex: ^uint32(0), Image: r.atlasImg, SubresourceRange: s.atlasSub,
530+ }
531+ disp.CmdPipelineBarrier2(s.cmd, &s.dep)
532+ if !r.atlasInited {
533+ var zero vulkan.ClearColorValue
534+ disp.CmdClearColorImage(s.cmd, r.atlasImg, vulkan.ImageLayoutTransferDstOptimal, &zero, 1, &s.atlasSub)
535+ barrier.SrcStageMask, barrier.SrcAccessMask = vulkan.PipelineStage2AllTransferBit, vulkan.Access2TransferWriteBit
536+ barrier.OldLayout = vulkan.ImageLayoutTransferDstOptimal
537+ disp.CmdPipelineBarrier2(s.cmd, &s.dep)
538+ }
539+ if len(s.regions) > 0 {
540+ disp.CmdCopyBufferToImage(s.cmd, s.stageBuf, r.atlasImg, vulkan.ImageLayoutTransferDstOptimal, uint32(len(s.regions)), &s.regions[0])
541+ }
542+ barrier.SrcStageMask, barrier.SrcAccessMask = vulkan.PipelineStage2AllTransferBit, vulkan.Access2TransferWriteBit
543+ barrier.DstStageMask, barrier.DstAccessMask = vulkan.PipelineStage2FragmentShaderBit, vulkan.Access2ShaderSampledReadBit
544+ barrier.OldLayout, barrier.NewLayout = vulkan.ImageLayoutTransferDstOptimal, vulkan.ImageLayoutShaderReadOnlyOptimal
545+ disp.CmdPipelineBarrier2(s.cmd, &s.dep)
546+ }
547+ old := vulkan.ImageLayout(vulkan.ImageLayoutGeneral)
548+ if !s.img.used {
549+ old = vulkan.ImageLayoutUndefined
550+ }
551+ *barrier = vulkan.ImageMemoryBarrier2{
552+ SType: vulkan.StructureTypeImageMemoryBarrier2, SrcStageMask: vulkan.PipelineStage2None,
553+ DstStageMask: vulkan.PipelineStage2ColorAttachmentOutputBit, DstAccessMask: vulkan.Access2ColorAttachmentWriteBit,
554+ OldLayout: old, NewLayout: vulkan.ImageLayoutColorAttachmentOptimal,
555+ SrcQueueFamilyIndex: ^uint32(0), DstQueueFamilyIndex: ^uint32(0), Image: s.img.image, SubresourceRange: s.atlasSub,
556+ }
557+ disp.CmdPipelineBarrier2(s.cmd, &s.dep)
558+ var cv vulkan.ClearValue
559+ cv[0], cv[1], cv[2], cv[3] = math.Float32bits(float32(clearRGB[0])/255), math.Float32bits(float32(clearRGB[1])/255), math.Float32bits(float32(clearRGB[2])/255), math.Float32bits(1)
560+ s.color = vulkan.RenderingAttachmentInfo{
561+ SType: vulkan.StructureTypeRenderingAttachmentInfo, ImageView: s.img.view, ImageLayout: vulkan.ImageLayoutColorAttachmentOptimal,
562+ LoadOp: vulkan.AttachmentLoadOpClear, StoreOp: vulkan.AttachmentStoreOpStore, ClearValue: cv,
563+ }
564+ s.render = vulkan.RenderingInfo{
565+ SType: vulkan.StructureTypeRenderingInfo,
566+ RenderArea: vulkan.Rect2D{Extent: vulkan.Extent2D{Width: uint32(r.W), Height: uint32(r.H)}},
567+ LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &s.color,
568+ }
569+ disp.CmdBeginRendering(s.cmd, &s.render)
570+ disp.CmdBindPipeline(s.cmd, vulkan.PipelineBindPointGraphics, r.pipeline)
571+ s.sets[0] = r.descSet
572+ disp.CmdBindDescriptorSets(s.cmd, vulkan.PipelineBindPointGraphics, r.pipeLayout, 0, 1, &s.sets[0], 0, nil)
573+ s.push = [2]float32{float32(r.W), float32(r.H)}
574+ disp.CmdPushConstants(s.cmd, r.pipeLayout, vulkan.ShaderStageVertexBit, 0, 8, unsafe.Pointer(&s.push))
575+ s.offset = 0
576+ disp.CmdBindVertexBuffers(s.cmd, 0, 1, &s.instBuf, &s.offset)
577+ if count > 0 {
578+ disp.CmdDraw(s.cmd, 6, uint32(count), 0, 0)
579+ }
580+ disp.CmdEndRendering(s.cmd)
581+ // Leave the image in GENERAL for the compositor.
582+ barrier.SrcStageMask, barrier.SrcAccessMask = vulkan.PipelineStage2ColorAttachmentOutputBit, vulkan.Access2ColorAttachmentWriteBit
583+ barrier.DstStageMask, barrier.DstAccessMask = vulkan.PipelineStage2AllCommandsBit, vulkan.Access2MemoryReadBit
584+ barrier.OldLayout, barrier.NewLayout = vulkan.ImageLayoutColorAttachmentOptimal, vulkan.ImageLayoutGeneral
585+ disp.CmdPipelineBarrier2(s.cmd, &s.dep)
586+ s.img.used = true
587+ return vulkan.Check(disp.EndCommandBuffer(s.cmd))
588+}
589+
590+// Released handles a readable release eventfd of slot: when the compositor's
591+// release point has signaled, the image may be drawn again.
592+func (r *Renderer) Released(slotIdx int) error {
593+ if slotIdx < 0 || slotIdx >= SlotCount {
594+ return nil
595+ }
596+ s := &r.slots[slotIdx]
597+ var counter [8]byte
598+ if _, err := unix.Read(s.eventFD, counter[:]); err != nil && !errors.Is(err, unix.EAGAIN) {
599+ return fmt.Errorf("gpu: read release eventfd: %w", err)
600+ }
601+ if s.state != slotShown {
602+ return nil
603+ }
604+ point, err := r.node.Query(s.releaseHandle)
605+ if err != nil {
606+ return err
607+ }
608+ if point >= s.releasePoint {
609+ s.state = slotFree
610+ }
611+ return nil
612+}
613+
614+// AcquireTimelineFD is the syncobj fd of the shared acquire timeline.
615+func (r *Renderer) AcquireTimelineFD() int { return r.acquireFD }
616+
617+// Cols is the number of cells that fit the width, rounding up.
618+func (r *Renderer) Cols() int { return r.cols }
619+
620+// Close waits for the GPU and frees everything. The compositor's imports keep
621+// their own kernel references.
622+func (r *Renderer) Close() {
623+ if r == nil {
624+ return
625+ }
626+ d := r.dev
627+ _ = d.WaitIdle()
628+ for i := range r.slots {
629+ s := &r.slots[i]
630+ if s.eventFD >= 0 {
631+ _ = unix.Close(s.eventFD)
632+ }
633+ if s.releaseFD >= 0 {
634+ _ = unix.Close(s.releaseFD)
635+ }
636+ if s.releaseHandle != 0 {
637+ _ = r.node.Destroy(s.releaseHandle)
638+ }
639+ for _, b := range [...]struct {
640+ buf vulkan.Buffer
641+ mem vulkan.DeviceMemory
642+ }{{s.instBuf, s.instMem}, {s.stageBuf, s.stageMem}} {
643+ if b.mem != 0 {
644+ d.disp.UnmapMemory(d.logical, b.mem)
645+ d.disp.FreeMemory(d.logical, b.mem, nil)
646+ }
647+ if b.buf != 0 {
648+ d.disp.DestroyBuffer(d.logical, b.buf, nil)
649+ }
650+ }
651+ if s.fence != 0 {
652+ d.disp.DestroyFence(d.logical, s.fence, nil)
653+ }
654+ if s.pool != 0 {
655+ d.disp.DestroyCommandPool(d.logical, s.pool, nil)
656+ }
657+ s.img.Close()
658+ }
659+ if r.acquireFD >= 0 {
660+ _ = unix.Close(r.acquireFD)
661+ }
662+ if r.acquireHandle != 0 {
663+ _ = r.node.Destroy(r.acquireHandle)
664+ }
665+ if r.pipeline != 0 {
666+ d.disp.DestroyPipeline(d.logical, r.pipeline, nil)
667+ }
668+ if r.pipeLayout != 0 {
669+ d.disp.DestroyPipelineLayout(d.logical, r.pipeLayout, nil)
670+ }
671+ if r.descPool != 0 {
672+ d.disp.DestroyDescriptorPool(d.logical, r.descPool, nil)
673+ }
674+ if r.setLayout != 0 {
675+ d.disp.DestroyDescriptorSetLayout(d.logical, r.setLayout, nil)
676+ }
677+ if r.sampler != 0 {
678+ d.disp.DestroySampler(d.logical, r.sampler, nil)
679+ }
680+ if r.atlasView != 0 {
681+ d.disp.DestroyImageView(d.logical, r.atlasView, nil)
682+ }
683+ if r.atlasImg != 0 {
684+ d.disp.DestroyImage(d.logical, r.atlasImg, nil)
685+ }
686+ if r.atlasMem != 0 {
687+ d.disp.FreeMemory(d.logical, r.atlasMem, nil)
688+ }
689+}
@@ -0,0 +1,689 @@
1+package gpu
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "fmt"
7+ "math"
8+ "unsafe"
9+
10+ "github.com/bnema/purego-vulkan/vulkan"
11+ "golang.org/x/sys/unix"
12+
13+ "git.bnema.dev/bnema/neferbar/internal/glyph"
14+ "git.bnema.dev/bnema/neferbar/internal/gpu/shaders"
15+ "git.bnema.dev/bnema/neferbar/internal/syncobj"
16+)
17+
18+const (
19+ atlasSize = 2048
20+ // SlotCount is the number of DMA-BUF images: one on screen, one being
21+ // shown or queued, one being drawn.
22+ SlotCount = 3
23+ // maxUploads bounds the glyphs rasterized by one Draw.
24+ maxUploads = 256
25+)
26+
27+// Cell is one grid cell as the renderer consumes it.
28+type Cell struct {
29+ Rune rune
30+ FG, BG [3]uint8
31+ Bold bool
32+}
33+
34+// instance is the per-cell vertex data (64 bytes, four vec4).
35+type instance struct {
36+ Rect [4]float32 // x, y, w, h in pixels
37+ UV [4]float32 // atlas u0, v0, u1, v1
38+ FG [4]float32
39+ BG [4]float32
40+}
41+
42+type glyphKey struct {
43+ r rune
44+ bold bool
45+}
46+
47+type slotState uint8
48+
49+const (
50+ slotFree slotState = iota
51+ slotShown
52+)
53+
54+// slot is one DMA-BUF image with everything needed to draw and present it.
55+type slot struct {
56+ img *Image
57+ pool vulkan.CommandPool
58+ cmd vulkan.CommandBuffer
59+ fence vulkan.Fence
60+
61+ instBuf, stageBuf vulkan.Buffer
62+ instMem, stageMem vulkan.DeviceMemory
63+ inst []instance // mapped
64+ stage []byte // mapped
65+
66+ releaseHandle uint32
67+ releaseFD int // syncobj fd, exported once
68+ eventFD int // readable when releasePoint signals
69+ releasePoint uint64
70+ presented bool
71+ state slotState
72+
73+ // Scratch argument structs: the bindings heap-allocate pointer arguments,
74+ // so a call built from locals allocates on every frame.
75+ begin vulkan.CommandBufferBeginInfo
76+ barrier [1]vulkan.ImageMemoryBarrier2
77+ dep vulkan.DependencyInfo
78+ color vulkan.RenderingAttachmentInfo
79+ render vulkan.RenderingInfo
80+ cmdInfo vulkan.CommandBufferSubmitInfo
81+ submit vulkan.SubmitInfo2
82+ offset vulkan.DeviceSize
83+ push [2]float32
84+ regions []vulkan.BufferImageCopy
85+ sets [1]vulkan.DescriptorSet
86+ atlasSub vulkan.ImageSubresourceRange
87+}
88+
89+// Frame describes one drawn slot for the Wayland layer. Fields stay valid
90+// until the next Draw.
91+type Frame struct {
92+ Slot int
93+ NewBuffer bool // import Image as a wl_buffer
94+ Image *Image
95+ AcquirePoint uint64
96+ ReleasePoint uint64
97+ ReleaseEventFD int // readable when ReleasePoint signals; stable per slot
98+ ReleaseTimeline int // syncobj fd of the slot's release timeline; valid with NewBuffer
99+}
100+
101+// Renderer draws a row of cells into a pool of DMA-BUF images.
102+type Renderer struct {
103+ dev *Device
104+ node *syncobj.Node
105+ face *glyph.Face
106+
107+ W, H int32
108+ cols, maxCols int
109+ yOff float32
110+
111+ slots [SlotCount]slot
112+
113+ acquireHandle uint32
114+ acquireFD int
115+ acquirePoint uint64
116+
117+ // atlas
118+ atlasImg vulkan.Image
119+ atlasMem vulkan.DeviceMemory
120+ atlasView vulkan.ImageView
121+ sampler vulkan.Sampler
122+ setLayout vulkan.DescriptorSetLayout
123+ descPool vulkan.DescriptorPool
124+ descSet vulkan.DescriptorSet
125+ pipeLayout vulkan.PipelineLayout
126+ pipeline vulkan.Pipeline
127+ atlasCols int
128+ atlasRows int
129+ glyphs map[glyphKey]uint32
130+ nextGlyph uint32
131+ atlasInited bool
132+
133+ uploadKeys []glyphKey // glyphs added during the current Draw
134+ uploadIdx []uint32
135+}
136+
137+// NewRenderer builds the pipeline, the atlas and the image pool for a w x h
138+// target. node must be the render node of dev.
139+func NewRenderer(dev *Device, node *syncobj.Node, mod Modifier, face *glyph.Face, w, h int32) (r *Renderer, err error) {
140+ if face.CellH > int(h) {
141+ return nil, fmt.Errorf("gpu: cell height %d exceeds bar height %d", face.CellH, h)
142+ }
143+ r = &Renderer{dev: dev, node: node, face: face, W: w, H: h, acquireFD: -1,
144+ glyphs: make(map[glyphKey]uint32, 256), yOff: float32((int(h) - face.CellH) / 2)}
145+ r.cols = (int(w) + face.CellW - 1) / face.CellW
146+ r.maxCols = r.cols
147+ for i := range r.slots {
148+ r.slots[i].releaseFD, r.slots[i].eventFD = -1, -1
149+ }
150+ defer func() {
151+ if err != nil {
152+ r.Close()
153+ r = nil
154+ }
155+ }()
156+ if err = r.newAtlas(); err != nil {
157+ return nil, err
158+ }
159+ if err = r.newPipeline(); err != nil {
160+ return nil, err
161+ }
162+ if r.acquireHandle, err = node.Create(); err != nil {
163+ return nil, err
164+ }
165+ if r.acquireFD, err = node.Export(r.acquireHandle); err != nil {
166+ return nil, err
167+ }
168+ for i := range r.slots {
169+ if err = r.newSlot(i, mod); err != nil {
170+ return nil, err
171+ }
172+ }
173+ // Glyph 0 is the blank cell. Printable ASCII is assigned now and uploaded
174+ // with the first frame.
175+ r.glyphs[glyphKey{' ', false}] = 0
176+ r.nextGlyph = 1
177+ for c := rune(0x21); c < 0x7f; c++ {
178+ r.glyphs[glyphKey{c, false}] = r.nextGlyph
179+ r.nextGlyph++
180+ }
181+ r.uploadKeys = make([]glyphKey, 0, maxUploads)
182+ r.uploadIdx = make([]uint32, 0, maxUploads)
183+ return r, nil
184+}
185+
186+func (r *Renderer) buffer(size vulkan.DeviceSize, usage vulkan.BufferUsageFlags) (vulkan.Buffer, vulkan.DeviceMemory, unsafe.Pointer, error) {
187+ d := r.dev
188+ var buf vulkan.Buffer
189+ ci := vulkan.BufferCreateInfo{SType: vulkan.StructureTypeBufferCreateInfo, Size: size, Usage: usage, SharingMode: vulkan.SharingModeExclusive}
190+ if err := vulkan.Check(d.disp.CreateBuffer(d.logical, &ci, nil, &buf)); err != nil {
191+ return 0, 0, nil, err
192+ }
193+ var req vulkan.MemoryRequirements
194+ d.disp.GetBufferMemoryRequirements(d.logical, buf, &req)
195+ idx, err := d.memoryType(req.MemoryTypeBits, vulkan.MemoryPropertyHostVisibleBit|vulkan.MemoryPropertyHostCoherentBit)
196+ if err != nil {
197+ d.disp.DestroyBuffer(d.logical, buf, nil)
198+ return 0, 0, nil, err
199+ }
200+ var mem vulkan.DeviceMemory
201+ ai := vulkan.MemoryAllocateInfo{SType: vulkan.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: idx}
202+ if err = vulkan.Check(d.disp.AllocateMemory(d.logical, &ai, nil, &mem)); err != nil {
203+ d.disp.DestroyBuffer(d.logical, buf, nil)
204+ return 0, 0, nil, err
205+ }
206+ fail := func(err error) (vulkan.Buffer, vulkan.DeviceMemory, unsafe.Pointer, error) {
207+ d.disp.FreeMemory(d.logical, mem, nil)
208+ d.disp.DestroyBuffer(d.logical, buf, nil)
209+ return 0, 0, nil, err
210+ }
211+ if err = vulkan.Check(d.disp.BindBufferMemory(d.logical, buf, mem, 0)); err != nil {
212+ return fail(err)
213+ }
214+ var ptr unsafe.Pointer
215+ if err = vulkan.Check(d.disp.MapMemory(d.logical, mem, 0, size, 0, &ptr)); err != nil {
216+ return fail(err)
217+ }
218+ return buf, mem, ptr, nil
219+}
220+
221+func (r *Renderer) newSlot(i int, mod Modifier) (err error) {
222+ d := r.dev
223+ s := &r.slots[i]
224+ if s.img, err = d.NewImage(r.W, r.H, mod); err != nil {
225+ return err
226+ }
227+ pi := vulkan.CommandPoolCreateInfo{SType: vulkan.StructureTypeCommandPoolCreateInfo, QueueFamilyIndex: d.family, Flags: vulkan.CommandPoolCreateResetCommandBufferBit}
228+ if err = vulkan.Check(d.disp.CreateCommandPool(d.logical, &pi, nil, &s.pool)); err != nil {
229+ return err
230+ }
231+ ai := vulkan.CommandBufferAllocateInfo{SType: vulkan.StructureTypeCommandBufferAllocateInfo, CommandPool: s.pool, Level: vulkan.CommandBufferLevelPrimary, CommandBufferCount: 1}
232+ if err = vulkan.Check(d.disp.AllocateCommandBuffers(d.logical, &ai, &s.cmd)); err != nil {
233+ return err
234+ }
235+ fi := vulkan.FenceCreateInfo{SType: vulkan.StructureTypeFenceCreateInfo}
236+ if err = vulkan.Check(d.disp.CreateFence(d.logical, &fi, nil, &s.fence)); err != nil {
237+ return err
238+ }
239+ instBytes := vulkan.DeviceSize(r.maxCols) * vulkan.DeviceSize(unsafe.Sizeof(instance{}))
240+ var p unsafe.Pointer
241+ if s.instBuf, s.instMem, p, err = r.buffer(instBytes, vulkan.BufferUsageVertexBufferBit); err != nil {
242+ return err
243+ }
244+ s.inst = unsafe.Slice((*instance)(p), r.maxCols)
245+ stageBytes := maxUploads * r.face.CellW * r.face.CellH
246+ if s.stageBuf, s.stageMem, p, err = r.buffer(vulkan.DeviceSize(stageBytes), vulkan.BufferUsageTransferSrcBit); err != nil {
247+ return err
248+ }
249+ s.stage = unsafe.Slice((*byte)(p), stageBytes)
250+ s.regions = make([]vulkan.BufferImageCopy, 0, maxUploads)
251+ s.atlasSub = vulkan.ImageSubresourceRange{AspectMask: vulkan.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
252+ if s.releaseHandle, err = r.node.Create(); err != nil {
253+ return err
254+ }
255+ if s.releaseFD, err = r.node.Export(s.releaseHandle); err != nil {
256+ return err
257+ }
258+ if s.eventFD, err = unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK); err != nil {
259+ return err
260+ }
261+ return nil
262+}
263+
264+func (r *Renderer) newAtlas() error {
265+ d := r.dev
266+ r.atlasCols = atlasSize / r.face.CellW
267+ r.atlasRows = atlasSize / r.face.CellH
268+ if r.atlasCols < 1 || r.atlasRows < 1 {
269+ return fmt.Errorf("gpu: cell %dx%d does not fit the atlas", r.face.CellW, r.face.CellH)
270+ }
271+ ci := vulkan.ImageCreateInfo{
272+ SType: vulkan.StructureTypeImageCreateInfo, ImageType: vulkan.ImageType2d, Format: vulkan.FormatR8Unorm,
273+ Extent: vulkan.Extent3D{Width: atlasSize, Height: atlasSize, Depth: 1},
274+ MipLevels: 1, ArrayLayers: 1, Samples: vulkan.SampleCount1Bit, Tiling: vulkan.ImageTilingOptimal,
275+ Usage: vulkan.ImageUsageSampledBit | vulkan.ImageUsageTransferDstBit,
276+ SharingMode: vulkan.SharingModeExclusive, InitialLayout: vulkan.ImageLayoutUndefined,
277+ }
278+ if err := vulkan.Check(d.disp.CreateImage(d.logical, &ci, nil, &r.atlasImg)); err != nil {
279+ return fmt.Errorf("gpu: create atlas: %w", err)
280+ }
281+ var req vulkan.MemoryRequirements
282+ d.disp.GetImageMemoryRequirements(d.logical, r.atlasImg, &req)
283+ idx, err := d.memoryType(req.MemoryTypeBits, vulkan.MemoryPropertyDeviceLocalBit)
284+ if err != nil {
285+ return err
286+ }
287+ ai := vulkan.MemoryAllocateInfo{SType: vulkan.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: idx}
288+ if err = vulkan.Check(d.disp.AllocateMemory(d.logical, &ai, nil, &r.atlasMem)); err != nil {
289+ return err
290+ }
291+ if err = vulkan.Check(d.disp.BindImageMemory(d.logical, r.atlasImg, r.atlasMem, 0)); err != nil {
292+ return err
293+ }
294+ vi := vulkan.ImageViewCreateInfo{
295+ SType: vulkan.StructureTypeImageViewCreateInfo, Image: r.atlasImg, ViewType: vulkan.ImageViewType2d, Format: vulkan.FormatR8Unorm,
296+ SubresourceRange: vulkan.ImageSubresourceRange{AspectMask: vulkan.ImageAspectColorBit, LevelCount: 1, LayerCount: 1},
297+ }
298+ if err = vulkan.Check(d.disp.CreateImageView(d.logical, &vi, nil, &r.atlasView)); err != nil {
299+ return err
300+ }
301+ si := vulkan.SamplerCreateInfo{
302+ SType: vulkan.StructureTypeSamplerCreateInfo, MagFilter: vulkan.FilterNearest, MinFilter: vulkan.FilterNearest,
303+ MipmapMode: vulkan.SamplerMipmapModeNearest,
304+ AddressModeU: vulkan.SamplerAddressModeClampToEdge, AddressModeV: vulkan.SamplerAddressModeClampToEdge, AddressModeW: vulkan.SamplerAddressModeClampToEdge,
305+ }
306+ if err = vulkan.Check(d.disp.CreateSampler(d.logical, &si, nil, &r.sampler)); err != nil {
307+ return err
308+ }
309+ b := vulkan.DescriptorSetLayoutBinding{DescriptorType: vulkan.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vulkan.ShaderStageFragmentBit}
310+ li := vulkan.DescriptorSetLayoutCreateInfo{SType: vulkan.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 1, Bindings: &b}
311+ if err = vulkan.Check(d.disp.CreateDescriptorSetLayout(d.logical, &li, nil, &r.setLayout)); err != nil {
312+ return err
313+ }
314+ ps := vulkan.DescriptorPoolSize{Type: vulkan.DescriptorTypeCombinedImageSampler, DescriptorCount: 1}
315+ pi := vulkan.DescriptorPoolCreateInfo{SType: vulkan.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &ps}
316+ if err = vulkan.Check(d.disp.CreateDescriptorPool(d.logical, &pi, nil, &r.descPool)); err != nil {
317+ return err
318+ }
319+ ds := vulkan.DescriptorSetAllocateInfo{SType: vulkan.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: r.descPool, DescriptorSetCount: 1, SetLayouts: &r.setLayout}
320+ if err = vulkan.Check(d.disp.AllocateDescriptorSets(d.logical, &ds, &r.descSet)); err != nil {
321+ return err
322+ }
323+ info := vulkan.DescriptorImageInfo{Sampler: r.sampler, ImageView: r.atlasView, ImageLayout: vulkan.ImageLayoutShaderReadOnlyOptimal}
324+ w := vulkan.WriteDescriptorSet{SType: vulkan.StructureTypeWriteDescriptorSet, DstSet: r.descSet, DescriptorCount: 1, DescriptorType: vulkan.DescriptorTypeCombinedImageSampler, ImageInfo: &info}
325+ d.disp.UpdateDescriptorSets(d.logical, 1, &w, 0, nil)
326+ return nil
327+}
328+
329+func (r *Renderer) shader(code []byte) (vulkan.ShaderModule, error) {
330+ if len(code) < 20 || len(code)%4 != 0 || binary.LittleEndian.Uint32(code) != 0x07230203 {
331+ return 0, errors.New("gpu: invalid embedded SPIR-V")
332+ }
333+ words := make([]uint32, len(code)/4)
334+ for i := range words {
335+ words[i] = le32(code[i*4:])
336+ }
337+ ci := vulkan.ShaderModuleCreateInfo{SType: vulkan.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(code)), Code: &words[0]}
338+ var m vulkan.ShaderModule
339+ if err := vulkan.Check(r.dev.disp.CreateShaderModule(r.dev.logical, &ci, nil, &m)); err != nil {
340+ return 0, err
341+ }
342+ return m, nil
343+}
344+
345+func (r *Renderer) newPipeline() error {
346+ d := r.dev
347+ pc := vulkan.PushConstantRange{StageFlags: vulkan.ShaderStageVertexBit, Size: 8}
348+ li := vulkan.PipelineLayoutCreateInfo{SType: vulkan.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &r.setLayout, PushConstantRangeCount: 1, PushConstantRanges: &pc}
349+ if err := vulkan.Check(d.disp.CreatePipelineLayout(d.logical, &li, nil, &r.pipeLayout)); err != nil {
350+ return err
351+ }
352+ vs, err := r.shader(shaders.CellVertex)
353+ if err != nil {
354+ return err
355+ }
356+ defer d.disp.DestroyShaderModule(d.logical, vs, nil)
357+ fs, err := r.shader(shaders.CellFragment)
358+ if err != nil {
359+ return err
360+ }
361+ defer d.disp.DestroyShaderModule(d.logical, fs, nil)
362+ entry := []byte("main\x00")
363+ stages := [2]vulkan.PipelineShaderStageCreateInfo{
364+ {SType: vulkan.StructureTypePipelineShaderStageCreateInfo, Stage: vulkan.ShaderStageVertexBit, Module: vs, Name: &entry[0]},
365+ {SType: vulkan.StructureTypePipelineShaderStageCreateInfo, Stage: vulkan.ShaderStageFragmentBit, Module: fs, Name: &entry[0]},
366+ }
367+ var attrs [4]vulkan.VertexInputAttributeDescription
368+ for i := range attrs {
369+ attrs[i] = vulkan.VertexInputAttributeDescription{Location: uint32(i), Format: vulkan.FormatR32g32b32a32Sfloat, Offset: uint32(i) * 16}
370+ }
371+ bind := vulkan.VertexInputBindingDescription{Stride: uint32(unsafe.Sizeof(instance{})), InputRate: vulkan.VertexInputRateInstance}
372+ vin := vulkan.PipelineVertexInputStateCreateInfo{SType: vulkan.StructureTypePipelineVertexInputStateCreateInfo, VertexBindingDescriptionCount: 1, VertexBindingDescriptions: &bind, VertexAttributeDescriptionCount: 4, VertexAttributeDescriptions: &attrs[0]}
373+ asm := vulkan.PipelineInputAssemblyStateCreateInfo{SType: vulkan.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vulkan.PrimitiveTopologyTriangleList}
374+ vp := vulkan.Viewport{Width: float32(r.W), Height: float32(r.H), MaxDepth: 1}
375+ sc := vulkan.Rect2D{Extent: vulkan.Extent2D{Width: uint32(r.W), Height: uint32(r.H)}}
376+ vps := vulkan.PipelineViewportStateCreateInfo{SType: vulkan.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &vp, ScissorCount: 1, Scissors: &sc}
377+ ras := vulkan.PipelineRasterizationStateCreateInfo{SType: vulkan.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vulkan.PolygonModeFill, CullMode: vulkan.CullModeNone, FrontFace: vulkan.FrontFaceCounterClockwise, LineWidth: 1}
378+ ms := vulkan.PipelineMultisampleStateCreateInfo{SType: vulkan.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vulkan.SampleCount1Bit}
379+ att := vulkan.PipelineColorBlendAttachmentState{ColorWriteMask: vulkan.ColorComponentRBit | vulkan.ColorComponentGBit | vulkan.ColorComponentBBit | vulkan.ColorComponentABit}
380+ blend := vulkan.PipelineColorBlendStateCreateInfo{SType: vulkan.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &att}
381+ format := vulkan.Format(vulkan.FormatB8g8r8a8Unorm)
382+ rend := vulkan.PipelineRenderingCreateInfo{SType: vulkan.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format}
383+ gi := vulkan.GraphicsPipelineCreateInfo{
384+ SType: vulkan.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rend), StageCount: 2, Stages: &stages[0],
385+ VertexInputState: &vin, InputAssemblyState: &asm, ViewportState: &vps, RasterizationState: &ras,
386+ MultisampleState: &ms, ColorBlendState: &blend, Layout: r.pipeLayout,
387+ }
388+ if err = vulkan.Check(d.disp.CreateGraphicsPipelines(d.logical, 0, 1, &gi, nil, &r.pipeline)); err != nil {
389+ return fmt.Errorf("gpu: create pipeline: %w", err)
390+ }
391+ return nil
392+}
393+
394+// queueGlyph schedules k for upload to atlas index idx in the current Draw.
395+func (r *Renderer) queueGlyph(k glyphKey, idx uint32) {
396+ r.uploadKeys = append(r.uploadKeys, k)
397+ r.uploadIdx = append(r.uploadIdx, idx)
398+}
399+
400+// glyph returns the atlas index of k, assigning one (and queueing its upload)
401+// on first use. An exhausted atlas maps to the blank glyph.
402+func (r *Renderer) glyph(k glyphKey) uint32 {
403+ if idx, ok := r.glyphs[k]; ok {
404+ return idx
405+ }
406+ if int(r.nextGlyph) >= r.atlasCols*r.atlasRows || len(r.uploadKeys) >= maxUploads {
407+ return 0
408+ }
409+ idx := r.nextGlyph
410+ r.nextGlyph++
411+ r.glyphs[k] = idx
412+ r.queueGlyph(k, idx)
413+ return idx
414+}
415+
416+// pickSlot returns a free slot index, or -1.
417+func (r *Renderer) pickSlot() int {
418+ for i := range r.slots {
419+ if r.slots[i].state == slotFree {
420+ return i
421+ }
422+ }
423+ return -1
424+}
425+
426+// Draw renders cells (len(cells) <= cols) into a free slot, waits for the GPU
427+// and signals the acquire point. It reports false when every image is still
428+// held by the compositor. clear is the background for pixels outside cells.
429+func (r *Renderer) Draw(cells []Cell, clear [3]uint8, out *Frame) (bool, error) {
430+ n := r.pickSlot()
431+ if n < 0 {
432+ return false, nil
433+ }
434+ s := &r.slots[n]
435+ if len(cells) > r.maxCols {
436+ cells = cells[:r.maxCols]
437+ }
438+ r.uploadKeys, r.uploadIdx = r.uploadKeys[:0], r.uploadIdx[:0]
439+ if !r.atlasInited {
440+ r.queueGlyph(glyphKey{' ', false}, 0)
441+ for c := rune(0x21); c < 0x7f; c++ {
442+ r.queueGlyph(glyphKey{c, false}, r.glyphs[glyphKey{c, false}])
443+ }
444+ }
445+ cw, ch := float32(r.face.CellW), float32(r.face.CellH)
446+ inv := float32(1) / atlasSize
447+ for i := range cells {
448+ c := &cells[i]
449+ gi := r.glyph(glyphKey{c.Rune, c.Bold})
450+ gx, gy := float32(int(gi)%r.atlasCols)*cw, float32(int(gi)/r.atlasCols)*ch
451+ in := &s.inst[i]
452+ in.Rect = [4]float32{float32(i) * cw, r.yOff, cw, ch}
453+ in.UV = [4]float32{gx * inv, gy * inv, (gx + cw) * inv, (gy + ch) * inv}
454+ in.FG = [4]float32{float32(c.FG[0]) / 255, float32(c.FG[1]) / 255, float32(c.FG[2]) / 255, 1}
455+ in.BG = [4]float32{float32(c.BG[0]) / 255, float32(c.BG[1]) / 255, float32(c.BG[2]) / 255, 1}
456+ }
457+ cellBytes := r.face.CellW * r.face.CellH
458+ s.regions = s.regions[:0]
459+ for i, k := range r.uploadKeys {
460+ off := i * cellBytes
461+ r.face.Rasterize(k.r, k.bold, s.stage[off:off+cellBytes])
462+ gi := int(r.uploadIdx[i])
463+ s.regions = append(s.regions, vulkan.BufferImageCopy{
464+ BufferOffset: vulkan.DeviceSize(off),
465+ BufferRowLength: uint32(r.face.CellW),
466+ ImageSubresource: vulkan.ImageSubresourceLayers{AspectMask: vulkan.ImageAspectColorBit, LayerCount: 1},
467+ ImageOffset: vulkan.Offset3D{X: int32((gi % r.atlasCols) * r.face.CellW), Y: int32((gi / r.atlasCols) * r.face.CellH)},
468+ ImageExtent: vulkan.Extent3D{Width: uint32(r.face.CellW), Height: uint32(r.face.CellH), Depth: 1},
469+ })
470+ }
471+ if err := r.record(s, len(cells), clear); err != nil {
472+ return false, err
473+ }
474+ d := r.dev
475+ s.cmdInfo = vulkan.CommandBufferSubmitInfo{SType: vulkan.StructureTypeCommandBufferSubmitInfo, CommandBuffer: s.cmd}
476+ s.submit = vulkan.SubmitInfo2{SType: vulkan.StructureTypeSubmitInfo2, CommandBufferInfoCount: 1, CommandBufferInfos: &s.cmdInfo}
477+ if err := vulkan.Check(d.disp.QueueSubmit2(d.queue, 1, &s.submit, s.fence)); err != nil {
478+ return false, fmt.Errorf("gpu: submit: %w", err)
479+ }
480+ // The draw is tiny: wait for it here, then signal the acquire point from
481+ // the CPU. No sync-file import is needed.
482+ if err := vulkan.Check(d.disp.WaitForFences(d.logical, 1, &s.fence, 1, math.MaxUint64)); err != nil {
483+ return false, fmt.Errorf("gpu: wait fence: %w", err)
484+ }
485+ if err := vulkan.Check(d.disp.ResetFences(d.logical, 1, &s.fence)); err != nil {
486+ return false, err
487+ }
488+ r.atlasInited = true
489+ r.acquirePoint++
490+ if err := r.node.Signal(r.acquireHandle, r.acquirePoint); err != nil {
491+ return false, err
492+ }
493+ s.releasePoint++
494+ if err := r.node.EventFD(s.releaseHandle, s.releasePoint, s.eventFD); err != nil {
495+ return false, err
496+ }
497+ s.state = slotShown
498+ *out = Frame{
499+ Slot: n, NewBuffer: !s.presented, Image: s.img,
500+ AcquirePoint: r.acquirePoint, ReleasePoint: s.releasePoint,
501+ ReleaseEventFD: s.eventFD, ReleaseTimeline: s.releaseFD,
502+ }
503+ s.presented = true
504+ return true, nil
505+}
506+
507+func (r *Renderer) record(s *slot, count int, clearRGB [3]uint8) error {
508+ d := r.dev
509+ disp := d.disp
510+ if err := vulkan.Check(disp.ResetCommandBuffer(s.cmd, 0)); err != nil {
511+ return err
512+ }
513+ s.begin = vulkan.CommandBufferBeginInfo{SType: vulkan.StructureTypeCommandBufferBeginInfo, Flags: vulkan.CommandBufferUsageOneTimeSubmitBit}
514+ if err := vulkan.Check(disp.BeginCommandBuffer(s.cmd, &s.begin)); err != nil {
515+ return err
516+ }
517+ s.dep = vulkan.DependencyInfo{SType: vulkan.StructureTypeDependencyInfo, ImageMemoryBarrierCount: 1, ImageMemoryBarriers: &s.barrier[0]}
518+ barrier := &s.barrier[0]
519+ if len(s.regions) > 0 || !r.atlasInited {
520+ old := vulkan.ImageLayout(vulkan.ImageLayoutShaderReadOnlyOptimal)
521+ srcStage, srcAccess := vulkan.PipelineStageFlags2(vulkan.PipelineStage2FragmentShaderBit), vulkan.AccessFlags2(vulkan.Access2ShaderSampledReadBit)
522+ if !r.atlasInited {
523+ old, srcStage, srcAccess = vulkan.ImageLayoutUndefined, vulkan.PipelineStage2None, 0
524+ }
525+ *barrier = vulkan.ImageMemoryBarrier2{
526+ SType: vulkan.StructureTypeImageMemoryBarrier2, SrcStageMask: srcStage, SrcAccessMask: srcAccess,
527+ DstStageMask: vulkan.PipelineStage2AllTransferBit, DstAccessMask: vulkan.Access2TransferWriteBit,
528+ OldLayout: old, NewLayout: vulkan.ImageLayoutTransferDstOptimal,
529+ SrcQueueFamilyIndex: ^uint32(0), DstQueueFamilyIndex: ^uint32(0), Image: r.atlasImg, SubresourceRange: s.atlasSub,
530+ }
531+ disp.CmdPipelineBarrier2(s.cmd, &s.dep)
532+ if !r.atlasInited {
533+ var zero vulkan.ClearColorValue
534+ disp.CmdClearColorImage(s.cmd, r.atlasImg, vulkan.ImageLayoutTransferDstOptimal, &zero, 1, &s.atlasSub)
535+ barrier.SrcStageMask, barrier.SrcAccessMask = vulkan.PipelineStage2AllTransferBit, vulkan.Access2TransferWriteBit
536+ barrier.OldLayout = vulkan.ImageLayoutTransferDstOptimal
537+ disp.CmdPipelineBarrier2(s.cmd, &s.dep)
538+ }
539+ if len(s.regions) > 0 {
540+ disp.CmdCopyBufferToImage(s.cmd, s.stageBuf, r.atlasImg, vulkan.ImageLayoutTransferDstOptimal, uint32(len(s.regions)), &s.regions[0])
541+ }
542+ barrier.SrcStageMask, barrier.SrcAccessMask = vulkan.PipelineStage2AllTransferBit, vulkan.Access2TransferWriteBit
543+ barrier.DstStageMask, barrier.DstAccessMask = vulkan.PipelineStage2FragmentShaderBit, vulkan.Access2ShaderSampledReadBit
544+ barrier.OldLayout, barrier.NewLayout = vulkan.ImageLayoutTransferDstOptimal, vulkan.ImageLayoutShaderReadOnlyOptimal
545+ disp.CmdPipelineBarrier2(s.cmd, &s.dep)
546+ }
547+ old := vulkan.ImageLayout(vulkan.ImageLayoutGeneral)
548+ if !s.img.used {
549+ old = vulkan.ImageLayoutUndefined
550+ }
551+ *barrier = vulkan.ImageMemoryBarrier2{
552+ SType: vulkan.StructureTypeImageMemoryBarrier2, SrcStageMask: vulkan.PipelineStage2None,
553+ DstStageMask: vulkan.PipelineStage2ColorAttachmentOutputBit, DstAccessMask: vulkan.Access2ColorAttachmentWriteBit,
554+ OldLayout: old, NewLayout: vulkan.ImageLayoutColorAttachmentOptimal,
555+ SrcQueueFamilyIndex: ^uint32(0), DstQueueFamilyIndex: ^uint32(0), Image: s.img.image, SubresourceRange: s.atlasSub,
556+ }
557+ disp.CmdPipelineBarrier2(s.cmd, &s.dep)
558+ var cv vulkan.ClearValue
559+ cv[0], cv[1], cv[2], cv[3] = math.Float32bits(float32(clearRGB[0])/255), math.Float32bits(float32(clearRGB[1])/255), math.Float32bits(float32(clearRGB[2])/255), math.Float32bits(1)
560+ s.color = vulkan.RenderingAttachmentInfo{
561+ SType: vulkan.StructureTypeRenderingAttachmentInfo, ImageView: s.img.view, ImageLayout: vulkan.ImageLayoutColorAttachmentOptimal,
562+ LoadOp: vulkan.AttachmentLoadOpClear, StoreOp: vulkan.AttachmentStoreOpStore, ClearValue: cv,
563+ }
564+ s.render = vulkan.RenderingInfo{
565+ SType: vulkan.StructureTypeRenderingInfo,
566+ RenderArea: vulkan.Rect2D{Extent: vulkan.Extent2D{Width: uint32(r.W), Height: uint32(r.H)}},
567+ LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &s.color,
568+ }
569+ disp.CmdBeginRendering(s.cmd, &s.render)
570+ disp.CmdBindPipeline(s.cmd, vulkan.PipelineBindPointGraphics, r.pipeline)
571+ s.sets[0] = r.descSet
572+ disp.CmdBindDescriptorSets(s.cmd, vulkan.PipelineBindPointGraphics, r.pipeLayout, 0, 1, &s.sets[0], 0, nil)
573+ s.push = [2]float32{float32(r.W), float32(r.H)}
574+ disp.CmdPushConstants(s.cmd, r.pipeLayout, vulkan.ShaderStageVertexBit, 0, 8, unsafe.Pointer(&s.push))
575+ s.offset = 0
576+ disp.CmdBindVertexBuffers(s.cmd, 0, 1, &s.instBuf, &s.offset)
577+ if count > 0 {
578+ disp.CmdDraw(s.cmd, 6, uint32(count), 0, 0)
579+ }
580+ disp.CmdEndRendering(s.cmd)
581+ // Leave the image in GENERAL for the compositor.
582+ barrier.SrcStageMask, barrier.SrcAccessMask = vulkan.PipelineStage2ColorAttachmentOutputBit, vulkan.Access2ColorAttachmentWriteBit
583+ barrier.DstStageMask, barrier.DstAccessMask = vulkan.PipelineStage2AllCommandsBit, vulkan.Access2MemoryReadBit
584+ barrier.OldLayout, barrier.NewLayout = vulkan.ImageLayoutColorAttachmentOptimal, vulkan.ImageLayoutGeneral
585+ disp.CmdPipelineBarrier2(s.cmd, &s.dep)
586+ s.img.used = true
587+ return vulkan.Check(disp.EndCommandBuffer(s.cmd))
588+}
589+
590+// Released handles a readable release eventfd of slot: when the compositor's
591+// release point has signaled, the image may be drawn again.
592+func (r *Renderer) Released(slotIdx int) error {
593+ if slotIdx < 0 || slotIdx >= SlotCount {
594+ return nil
595+ }
596+ s := &r.slots[slotIdx]
597+ var counter [8]byte
598+ if _, err := unix.Read(s.eventFD, counter[:]); err != nil && !errors.Is(err, unix.EAGAIN) {
599+ return fmt.Errorf("gpu: read release eventfd: %w", err)
600+ }
601+ if s.state != slotShown {
602+ return nil
603+ }
604+ point, err := r.node.Query(s.releaseHandle)
605+ if err != nil {
606+ return err
607+ }
608+ if point >= s.releasePoint {
609+ s.state = slotFree
610+ }
611+ return nil
612+}
613+
614+// AcquireTimelineFD is the syncobj fd of the shared acquire timeline.
615+func (r *Renderer) AcquireTimelineFD() int { return r.acquireFD }
616+
617+// Cols is the number of cells that fit the width, rounding up.
618+func (r *Renderer) Cols() int { return r.cols }
619+
620+// Close waits for the GPU and frees everything. The compositor's imports keep
621+// their own kernel references.
622+func (r *Renderer) Close() {
623+ if r == nil {
624+ return
625+ }
626+ d := r.dev
627+ _ = d.WaitIdle()
628+ for i := range r.slots {
629+ s := &r.slots[i]
630+ if s.eventFD >= 0 {
631+ _ = unix.Close(s.eventFD)
632+ }
633+ if s.releaseFD >= 0 {
634+ _ = unix.Close(s.releaseFD)
635+ }
636+ if s.releaseHandle != 0 {
637+ _ = r.node.Destroy(s.releaseHandle)
638+ }
639+ for _, b := range [...]struct {
640+ buf vulkan.Buffer
641+ mem vulkan.DeviceMemory
642+ }{{s.instBuf, s.instMem}, {s.stageBuf, s.stageMem}} {
643+ if b.mem != 0 {
644+ d.disp.UnmapMemory(d.logical, b.mem)
645+ d.disp.FreeMemory(d.logical, b.mem, nil)
646+ }
647+ if b.buf != 0 {
648+ d.disp.DestroyBuffer(d.logical, b.buf, nil)
649+ }
650+ }
651+ if s.fence != 0 {
652+ d.disp.DestroyFence(d.logical, s.fence, nil)
653+ }
654+ if s.pool != 0 {
655+ d.disp.DestroyCommandPool(d.logical, s.pool, nil)
656+ }
657+ s.img.Close()
658+ }
659+ if r.acquireFD >= 0 {
660+ _ = unix.Close(r.acquireFD)
661+ }
662+ if r.acquireHandle != 0 {
663+ _ = r.node.Destroy(r.acquireHandle)
664+ }
665+ if r.pipeline != 0 {
666+ d.disp.DestroyPipeline(d.logical, r.pipeline, nil)
667+ }
668+ if r.pipeLayout != 0 {
669+ d.disp.DestroyPipelineLayout(d.logical, r.pipeLayout, nil)
670+ }
671+ if r.descPool != 0 {
672+ d.disp.DestroyDescriptorPool(d.logical, r.descPool, nil)
673+ }
674+ if r.setLayout != 0 {
675+ d.disp.DestroyDescriptorSetLayout(d.logical, r.setLayout, nil)
676+ }
677+ if r.sampler != 0 {
678+ d.disp.DestroySampler(d.logical, r.sampler, nil)
679+ }
680+ if r.atlasView != 0 {
681+ d.disp.DestroyImageView(d.logical, r.atlasView, nil)
682+ }
683+ if r.atlasImg != 0 {
684+ d.disp.DestroyImage(d.logical, r.atlasImg, nil)
685+ }
686+ if r.atlasMem != 0 {
687+ d.disp.FreeMemory(d.logical, r.atlasMem, nil)
688+ }
689+}
A internal/gpu/shaders/cell.frag +13 -0
@@ -0,0 +1,13 @@
1+#version 450
2+
3+layout(set = 0, binding = 0) uniform sampler2D atlas;
4+
5+layout(location = 0) in vec2 vUV;
6+layout(location = 1) flat in vec4 vFG;
7+layout(location = 2) flat in vec4 vBG;
8+layout(location = 0) out vec4 outColor;
9+
10+void main() {
11+ float coverage = texture(atlas, vUV).r;
12+ outColor = vec4(mix(vBG.rgb, vFG.rgb, coverage), 1.0);
13+}
@@ -0,0 +1,13 @@
1+#version 450
2+
3+layout(set = 0, binding = 0) uniform sampler2D atlas;
4+
5+layout(location = 0) in vec2 vUV;
6+layout(location = 1) flat in vec4 vFG;
7+layout(location = 2) flat in vec4 vBG;
8+layout(location = 0) out vec4 outColor;
9+
10+void main() {
11+ float coverage = texture(atlas, vUV).r;
12+ outColor = vec4(mix(vBG.rgb, vFG.rgb, coverage), 1.0);
13+}
A internal/gpu/shaders/cell.frag.spv +0 -0 binary file: no text diff

binary file: no text diff

A internal/gpu/shaders/cell.vert +25 -0
@@ -0,0 +1,25 @@
1+#version 450
2+
3+// One instance per terminal cell (or per double-width cell pair).
4+layout(location = 0) in vec4 cell; // x, y, w, h in pixels
5+layout(location = 1) in vec4 uv; // atlas u0, v0, u1, v1
6+layout(location = 2) in vec4 fg;
7+layout(location = 3) in vec4 bg;
8+
9+layout(push_constant) uniform Screen { vec2 size; } screen;
10+
11+layout(location = 0) out vec2 vUV;
12+layout(location = 1) flat out vec4 vFG;
13+layout(location = 2) flat out vec4 vBG;
14+
15+void main() {
16+ vec2 corner[6] = vec2[6](
17+ vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),
18+ vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0));
19+ vec2 q = corner[gl_VertexIndex];
20+ vec2 xy = cell.xy + q * cell.zw;
21+ gl_Position = vec4(xy * 2.0 / screen.size - 1.0, 0.0, 1.0);
22+ vUV = mix(uv.xy, uv.zw, q);
23+ vFG = fg;
24+ vBG = bg;
25+}
@@ -0,0 +1,25 @@
1+#version 450
2+
3+// One instance per terminal cell (or per double-width cell pair).
4+layout(location = 0) in vec4 cell; // x, y, w, h in pixels
5+layout(location = 1) in vec4 uv; // atlas u0, v0, u1, v1
6+layout(location = 2) in vec4 fg;
7+layout(location = 3) in vec4 bg;
8+
9+layout(push_constant) uniform Screen { vec2 size; } screen;
10+
11+layout(location = 0) out vec2 vUV;
12+layout(location = 1) flat out vec4 vFG;
13+layout(location = 2) flat out vec4 vBG;
14+
15+void main() {
16+ vec2 corner[6] = vec2[6](
17+ vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),
18+ vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0));
19+ vec2 q = corner[gl_VertexIndex];
20+ vec2 xy = cell.xy + q * cell.zw;
21+ gl_Position = vec4(xy * 2.0 / screen.size - 1.0, 0.0, 1.0);
22+ vUV = mix(uv.xy, uv.zw, q);
23+ vFG = fg;
24+ vBG = bg;
25+}
A internal/gpu/shaders/cell.vert.spv +0 -0 binary file: no text diff

binary file: no text diff

A internal/gpu/shaders/shaders.go +14 -0
@@ -0,0 +1,14 @@
1+// Package shaders holds the committed SPIR-V compiled from the sibling GLSL
2+// sources. Consumers do not need glslc; maintainers run go generate.
3+package shaders
4+
5+import _ "embed"
6+
7+//go:generate glslc -O -fshader-stage=vertex -o cell.vert.spv cell.vert
8+//go:generate glslc -O -fshader-stage=fragment -o cell.frag.spv cell.frag
9+
10+//go:embed cell.vert.spv
11+var CellVertex []byte
12+
13+//go:embed cell.frag.spv
14+var CellFragment []byte
@@ -0,0 +1,14 @@
1+// Package shaders holds the committed SPIR-V compiled from the sibling GLSL
2+// sources. Consumers do not need glslc; maintainers run go generate.
3+package shaders
4+
5+import _ "embed"
6+
7+//go:generate glslc -O -fshader-stage=vertex -o cell.vert.spv cell.vert
8+//go:generate glslc -O -fshader-stage=fragment -o cell.frag.spv cell.frag
9+
10+//go:embed cell.vert.spv
11+var CellVertex []byte
12+
13+//go:embed cell.frag.spv
14+var CellFragment []byte
A internal/racecheck/race_off.go +8 -0
@@ -0,0 +1,8 @@
1+//go:build !race
2+
3+// Package racecheck tells tests whether the race detector is on. The detector
4+// adds allocations of its own, so zero-allocation assertions must skip.
5+package racecheck
6+
7+// Enabled is true when built with -race.
8+const Enabled = false
@@ -0,0 +1,8 @@
1+//go:build !race
2+
3+// Package racecheck tells tests whether the race detector is on. The detector
4+// adds allocations of its own, so zero-allocation assertions must skip.
5+package racecheck
6+
7+// Enabled is true when built with -race.
8+const Enabled = false
A internal/racecheck/race_on.go +8 -0
@@ -0,0 +1,8 @@
1+//go:build race
2+
3+// Package racecheck tells tests whether the race detector is on. The detector
4+// adds allocations of its own, so zero-allocation assertions must skip.
5+package racecheck
6+
7+// Enabled is true when built with -race.
8+const Enabled = true
@@ -0,0 +1,8 @@
1+//go:build race
2+
3+// Package racecheck tells tests whether the race detector is on. The detector
4+// adds allocations of its own, so zero-allocation assertions must skip.
5+package racecheck
6+
7+// Enabled is true when built with -race.
8+const Enabled = true
A internal/syncobj/syncobj.go +133 -0
@@ -0,0 +1,133 @@
1+// Package syncobj wraps the few DRM syncobj ioctls the bar needs: one acquire
2+// timeline signaled from the CPU, and one release timeline per buffer whose
3+// points the compositor signals and an eventfd reports.
4+package syncobj
5+
6+import (
7+ "fmt"
8+ "unsafe"
9+
10+ "golang.org/x/sys/unix"
11+)
12+
13+// ioctl numbers of DRM_IOCTL_SYNCOBJ_* (Linux drm.h, type 'd').
14+const (
15+ ioctlCreate = uintptr(0xc00864bf)
16+ ioctlDestroy = uintptr(0xc00864c0)
17+ ioctlHandleToFD = uintptr(0xc01864c1)
18+ ioctlQuery = uintptr(0xc01864cb)
19+ ioctlTimelineSig = uintptr(0xc01864cd)
20+ ioctlEventFD = uintptr(0xc01864cf)
21+)
22+
23+type createArg struct{ Handle, Flags uint32 }
24+type destroyArg struct{ Handle, Pad uint32 }
25+type handleArg struct {
26+ Handle, Flags uint32
27+ FD int32
28+ Pad uint32
29+ Point uint64
30+}
31+type arrayArg struct { // drm_syncobj_timeline_array
32+ Handles, Points uint64
33+ Count, Flags uint32
34+}
35+type eventFDArg struct {
36+ Handle, Flags uint32
37+ Point uint64
38+ FD int32
39+ Pad uint32
40+}
41+
42+// Node owns a DRM render node fd. It is used from one goroutine; its scratch
43+// fields live in the heap object so the addresses embedded in ioctl arguments
44+// stay valid and no call allocates.
45+type Node struct {
46+ fd int
47+
48+ handle uint32
49+ point uint64
50+ array arrayArg
51+}
52+
53+// Open opens a render node such as /dev/dri/renderD128.
54+func Open(path string) (*Node, error) {
55+ fd, err := unix.Open(path, unix.O_RDWR|unix.O_CLOEXEC, 0)
56+ if err != nil {
57+ return nil, fmt.Errorf("syncobj: open %s: %w", path, err)
58+ }
59+ return &Node{fd: fd}, nil
60+}
61+
62+// Close closes the render node.
63+func (n *Node) Close() error {
64+ if n.fd < 0 {
65+ return nil
66+ }
67+ err := unix.Close(n.fd)
68+ n.fd = -1
69+ return err
70+}
71+
72+func (n *Node) ioctl(request uintptr, arg unsafe.Pointer) error {
73+ if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(n.fd), request, uintptr(arg)); errno != 0 {
74+ return fmt.Errorf("syncobj: ioctl %#x: %w", request, errno)
75+ }
76+ return nil
77+}
78+
79+// Create returns a new syncobj handle.
80+func (n *Node) Create() (uint32, error) {
81+ var a createArg
82+ if err := n.ioctl(ioctlCreate, unsafe.Pointer(&a)); err != nil {
83+ return 0, err
84+ }
85+ return a.Handle, nil
86+}
87+
88+// Destroy releases a handle.
89+func (n *Node) Destroy(handle uint32) error {
90+ a := destroyArg{Handle: handle}
91+ return n.ioctl(ioctlDestroy, unsafe.Pointer(&a))
92+}
93+
94+// Export returns a new owned syncobj fd for handle; the caller closes it.
95+func (n *Node) Export(handle uint32) (int, error) {
96+ a := handleArg{Handle: handle, FD: -1}
97+ if err := n.ioctl(ioctlHandleToFD, unsafe.Pointer(&a)); err != nil {
98+ return -1, err
99+ }
100+ return int(a.FD), nil
101+}
102+
103+// Signal signals one timeline point from the CPU.
104+func (n *Node) Signal(handle uint32, point uint64) error {
105+ n.handle, n.point = handle, point
106+ n.array = arrayArg{
107+ Handles: uint64(uintptr(unsafe.Pointer(&n.handle))),
108+ Points: uint64(uintptr(unsafe.Pointer(&n.point))),
109+ Count: 1,
110+ }
111+ return n.ioctl(ioctlTimelineSig, unsafe.Pointer(&n.array))
112+}
113+
114+// Query returns the last signaled point of a timeline.
115+func (n *Node) Query(handle uint32) (uint64, error) {
116+ n.handle, n.point = handle, 0
117+ n.array = arrayArg{
118+ Handles: uint64(uintptr(unsafe.Pointer(&n.handle))),
119+ Points: uint64(uintptr(unsafe.Pointer(&n.point))),
120+ Count: 1,
121+ }
122+ if err := n.ioctl(ioctlQuery, unsafe.Pointer(&n.array)); err != nil {
123+ return 0, err
124+ }
125+ return n.point, nil
126+}
127+
128+// EventFD arms efd to become readable once point is signaled. The registration
129+// is one-shot and returns at once; the point need not be submitted yet.
130+func (n *Node) EventFD(handle uint32, point uint64, efd int) error {
131+ a := eventFDArg{Handle: handle, Point: point, FD: int32(efd)}
132+ return n.ioctl(ioctlEventFD, unsafe.Pointer(&a))
133+}
@@ -0,0 +1,133 @@
1+// Package syncobj wraps the few DRM syncobj ioctls the bar needs: one acquire
2+// timeline signaled from the CPU, and one release timeline per buffer whose
3+// points the compositor signals and an eventfd reports.
4+package syncobj
5+
6+import (
7+ "fmt"
8+ "unsafe"
9+
10+ "golang.org/x/sys/unix"
11+)
12+
13+// ioctl numbers of DRM_IOCTL_SYNCOBJ_* (Linux drm.h, type 'd').
14+const (
15+ ioctlCreate = uintptr(0xc00864bf)
16+ ioctlDestroy = uintptr(0xc00864c0)
17+ ioctlHandleToFD = uintptr(0xc01864c1)
18+ ioctlQuery = uintptr(0xc01864cb)
19+ ioctlTimelineSig = uintptr(0xc01864cd)
20+ ioctlEventFD = uintptr(0xc01864cf)
21+)
22+
23+type createArg struct{ Handle, Flags uint32 }
24+type destroyArg struct{ Handle, Pad uint32 }
25+type handleArg struct {
26+ Handle, Flags uint32
27+ FD int32
28+ Pad uint32
29+ Point uint64
30+}
31+type arrayArg struct { // drm_syncobj_timeline_array
32+ Handles, Points uint64
33+ Count, Flags uint32
34+}
35+type eventFDArg struct {
36+ Handle, Flags uint32
37+ Point uint64
38+ FD int32
39+ Pad uint32
40+}
41+
42+// Node owns a DRM render node fd. It is used from one goroutine; its scratch
43+// fields live in the heap object so the addresses embedded in ioctl arguments
44+// stay valid and no call allocates.
45+type Node struct {
46+ fd int
47+
48+ handle uint32
49+ point uint64
50+ array arrayArg
51+}
52+
53+// Open opens a render node such as /dev/dri/renderD128.
54+func Open(path string) (*Node, error) {
55+ fd, err := unix.Open(path, unix.O_RDWR|unix.O_CLOEXEC, 0)
56+ if err != nil {
57+ return nil, fmt.Errorf("syncobj: open %s: %w", path, err)
58+ }
59+ return &Node{fd: fd}, nil
60+}
61+
62+// Close closes the render node.
63+func (n *Node) Close() error {
64+ if n.fd < 0 {
65+ return nil
66+ }
67+ err := unix.Close(n.fd)
68+ n.fd = -1
69+ return err
70+}
71+
72+func (n *Node) ioctl(request uintptr, arg unsafe.Pointer) error {
73+ if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(n.fd), request, uintptr(arg)); errno != 0 {
74+ return fmt.Errorf("syncobj: ioctl %#x: %w", request, errno)
75+ }
76+ return nil
77+}
78+
79+// Create returns a new syncobj handle.
80+func (n *Node) Create() (uint32, error) {
81+ var a createArg
82+ if err := n.ioctl(ioctlCreate, unsafe.Pointer(&a)); err != nil {
83+ return 0, err
84+ }
85+ return a.Handle, nil
86+}
87+
88+// Destroy releases a handle.
89+func (n *Node) Destroy(handle uint32) error {
90+ a := destroyArg{Handle: handle}
91+ return n.ioctl(ioctlDestroy, unsafe.Pointer(&a))
92+}
93+
94+// Export returns a new owned syncobj fd for handle; the caller closes it.
95+func (n *Node) Export(handle uint32) (int, error) {
96+ a := handleArg{Handle: handle, FD: -1}
97+ if err := n.ioctl(ioctlHandleToFD, unsafe.Pointer(&a)); err != nil {
98+ return -1, err
99+ }
100+ return int(a.FD), nil
101+}
102+
103+// Signal signals one timeline point from the CPU.
104+func (n *Node) Signal(handle uint32, point uint64) error {
105+ n.handle, n.point = handle, point
106+ n.array = arrayArg{
107+ Handles: uint64(uintptr(unsafe.Pointer(&n.handle))),
108+ Points: uint64(uintptr(unsafe.Pointer(&n.point))),
109+ Count: 1,
110+ }
111+ return n.ioctl(ioctlTimelineSig, unsafe.Pointer(&n.array))
112+}
113+
114+// Query returns the last signaled point of a timeline.
115+func (n *Node) Query(handle uint32) (uint64, error) {
116+ n.handle, n.point = handle, 0
117+ n.array = arrayArg{
118+ Handles: uint64(uintptr(unsafe.Pointer(&n.handle))),
119+ Points: uint64(uintptr(unsafe.Pointer(&n.point))),
120+ Count: 1,
121+ }
122+ if err := n.ioctl(ioctlQuery, unsafe.Pointer(&n.array)); err != nil {
123+ return 0, err
124+ }
125+ return n.point, nil
126+}
127+
128+// EventFD arms efd to become readable once point is signaled. The registration
129+// is one-shot and returns at once; the point need not be submitted yet.
130+func (n *Node) EventFD(handle uint32, point uint64, efd int) error {
131+ a := eventFDArg{Handle: handle, Point: point, FD: int32(efd)}
132+ return n.ioctl(ioctlEventFD, unsafe.Pointer(&a))
133+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 5

Tags 4

↑↓ moveenter openesc close