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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | + | } | |||