detached 88e7637

feat(render): add offscreen Vulkan renderer with clear and readback

commit
88e76375b10aa65e821b96e65aef7ac9bdef5cf7
author
brice · · 2026-09-23 15:21 UTC
parent
8fd5205
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +338 -2
M README.md +2 -2
@@ -4,7 +4,7 @@ NeferTTY is a minimal Wayland compositor in Go with Niri-style scrollable column
44
55 Requires Linux and Go 1.27.
66
7-Build: `make build`
7+Build: `make build` (requires sibling checkouts of purego-libwayland and purego-vulkan).
88
99 Run: `nefertty --backend=headless --timeout 5s`
1010
@@ -21,7 +21,7 @@ gaps = 8
2121
2222 See `examples/config.toml` for all defaults.
2323
24-Building requires a sibling checkout of purego-libwayland until it is published.
24+Building requires sibling checkouts of purego-libwayland and purego-vulkan until they are published.
2525
2626 ## Try it (headless)
2727
@@ -4,7 +4,7 @@ NeferTTY is a minimal Wayland compositor in Go with Niri-style scrollable column
4 4
5 Requires Linux and Go 1.27.5 Requires Linux and Go 1.27.
6 6
7-Build: `make build`7+Build: `make build` (requires sibling checkouts of purego-libwayland and purego-vulkan).
8 8
9 Run: `nefertty --backend=headless --timeout 5s`9 Run: `nefertty --backend=headless --timeout 5s`
10 10
@@ -21,7 +21,7 @@ gaps = 8
21 21
22 See `examples/config.toml` for all defaults.22 See `examples/config.toml` for all defaults.
23 23
24-Building requires a sibling checkout of purego-libwayland until it is published.24+Building requires sibling checkouts of purego-libwayland and purego-vulkan until they are published.
25 25
26 ## Try it (headless)26 ## Try it (headless)
27 27
M go.mod +3 -0
@@ -5,6 +5,7 @@ go 1.27
55 require (
66 github.com/BurntSushi/toml v1.5.0
77 github.com/bnema/purego-libwayland v0.0.0
8+ github.com/bnema/purego-vulkan v0.0.0
89 github.com/bnema/wlturbo v0.2.0
910 github.com/bnema/zerowrap v1.4.1
1011 github.com/rs/zerolog v1.35.1
@@ -20,3 +21,5 @@ require (
2021 )
2122
2223 replace github.com/bnema/purego-libwayland => ../purego-libwayland
24+
25+replace github.com/bnema/purego-vulkan => ../purego-vulkan
@@ -5,6 +5,7 @@ go 1.27
5 require (5 require (
6 github.com/BurntSushi/toml v1.5.06 github.com/BurntSushi/toml v1.5.0
7 github.com/bnema/purego-libwayland v0.0.07 github.com/bnema/purego-libwayland v0.0.0
8+ github.com/bnema/purego-vulkan v0.0.0
8 github.com/bnema/wlturbo v0.2.09 github.com/bnema/wlturbo v0.2.0
9 github.com/bnema/zerowrap v1.4.110 github.com/bnema/zerowrap v1.4.1
10 github.com/rs/zerolog v1.35.111 github.com/rs/zerolog v1.35.1
@@ -20,3 +21,5 @@ require (
20 )21 )
21 22
22 replace github.com/bnema/purego-libwayland => ../purego-libwayland23 replace github.com/bnema/purego-libwayland => ../purego-libwayland
24+
25+replace github.com/bnema/purego-vulkan => ../purego-vulkan
A internal/adapters/vulkan/renderer.go +309 -0
@@ -0,0 +1,309 @@
1+package vulkan
2+
3+import (
4+ "fmt"
5+ "image"
6+ "math"
7+ "os"
8+ "runtime"
9+ "strconv"
10+ "unsafe"
11+
12+ vk "github.com/bnema/purego-vulkan/vulkan"
13+)
14+
15+// Renderer is owned by one goroutine; its methods must not be called concurrently.
16+type Renderer struct {
17+ width, height int
18+ instance vk.Instance
19+ id *vk.InstanceDispatch
20+ device vk.Device
21+ dd *vk.DeviceDispatch
22+ queue vk.Queue
23+ image vk.Image
24+ imageMemory vk.DeviceMemory
25+ buffer vk.Buffer
26+ bufferMemory vk.DeviceMemory
27+ mapped unsafe.Pointer
28+ pool vk.CommandPool
29+ command vk.CommandBuffer
30+ fence vk.Fence
31+ layout vk.ImageLayout
32+ memory vk.PhysicalDeviceMemoryProperties
33+}
34+
35+func checked(name string, result vk.Result) error {
36+ if err := vk.Check(result); err != nil {
37+ return fmt.Errorf("%s: %w", name, err)
38+ }
39+ return nil
40+}
41+
42+func New(width, height int) (r *Renderer, err error) {
43+ if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {
44+ return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)
45+ }
46+ r = &Renderer{width: width, height: height, layout: vk.ImageLayoutUndefined}
47+ defer func() {
48+ if err != nil {
49+ r.Close()
50+ r = nil
51+ }
52+ }()
53+ if err = vk.Init(); err != nil {
54+ return nil, fmt.Errorf("vk.Init: %w", err)
55+ }
56+ app := vk.ApplicationInfo{SType: vk.StructureTypeApplicationInfo, ApiVersion: vk.MakeVersion(1, 3, 0)}
57+ info := vk.InstanceCreateInfo{SType: vk.StructureTypeInstanceCreateInfo, ApplicationInfo: &app}
58+ if err = checked("vkCreateInstance", vk.Global().CreateInstance(&info, nil, &r.instance)); err != nil {
59+ return
60+ }
61+ r.id, err = vk.LoadInstanceDispatch(r.instance)
62+ if err != nil {
63+ err = fmt.Errorf("LoadInstanceDispatch: %w", err)
64+ return
65+ }
66+ var count uint32
67+ if err = checked("vkEnumeratePhysicalDevices(count)", r.id.EnumeratePhysicalDevices(r.instance, &count, nil)); err != nil {
68+ return
69+ }
70+ if count == 0 {
71+ err = fmt.Errorf("no Vulkan 1.3 device")
72+ return
73+ }
74+ devices := make([]vk.PhysicalDevice, count)
75+ if err = checked("vkEnumeratePhysicalDevices", r.id.EnumeratePhysicalDevices(r.instance, &count, &devices[0])); err != nil {
76+ return
77+ }
78+ index := -1
79+ if value, ok := os.LookupEnv("NEFERTTY_VK_DEVICE"); ok {
80+ index, err = strconv.Atoi(value)
81+ if err != nil || index < 0 || index >= int(count) {
82+ err = fmt.Errorf("invalid NEFERTTY_VK_DEVICE index %q", value)
83+ return
84+ }
85+ }
86+ var physical vk.PhysicalDevice
87+ var family uint32
88+ for i, device := range devices[:count] {
89+ if index >= 0 && i != index {
90+ continue
91+ }
92+ var properties vk.PhysicalDeviceProperties
93+ r.id.GetPhysicalDeviceProperties(device, &properties)
94+ if properties.ApiVersion < vk.MakeVersion(1, 3, 0) {
95+ continue
96+ }
97+ var n uint32
98+ r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, nil)
99+ if n == 0 {
100+ continue
101+ }
102+ families := make([]vk.QueueFamilyProperties, n)
103+ r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, &families[0])
104+ for j, f := range families[:n] {
105+ if f.QueueCount > 0 && f.QueueFlags&vk.QueueGraphicsBit != 0 {
106+ physical = device
107+ family = uint32(j)
108+ break
109+ }
110+ }
111+ if physical != 0 {
112+ break
113+ }
114+ }
115+ if physical == 0 {
116+ err = fmt.Errorf("no Vulkan 1.3 device with a graphics queue")
117+ return
118+ }
119+ r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory)
120+ priority := float32(1)
121+ qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority}
122+ dynamicRendering := vk.PhysicalDeviceDynamicRenderingFeatures{SType: vk.StructureTypePhysicalDeviceDynamicRenderingFeatures, DynamicRendering: 1}
123+ di := vk.DeviceCreateInfo{SType: vk.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&dynamicRendering), QueueCreateInfoCount: 1, QueueCreateInfos: &qi}
124+ if err = checked("vkCreateDevice", r.id.CreateDevice(physical, &di, nil, &r.device)); err != nil {
125+ return
126+ }
127+ r.dd, err = vk.LoadDeviceDispatch(r.id, r.device)
128+ if err != nil {
129+ err = fmt.Errorf("LoadDeviceDispatch: %w", err)
130+ return
131+ }
132+ r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)
133+ extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}
134+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: extent, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
135+ if err = checked("vkCreateImage", r.dd.CreateImage(r.device, &ii, nil, &r.image)); err != nil {
136+ return
137+ }
138+ var req vk.MemoryRequirements
139+ r.dd.GetImageMemoryRequirements(r.device, r.image, &req)
140+ var kind uint32
141+ kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
142+ if err != nil {
143+ return
144+ }
145+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
146+ if err = checked("vkAllocateMemory(image)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.imageMemory)); err != nil {
147+ return
148+ }
149+ if err = checked("vkBindImageMemory", r.dd.BindImageMemory(r.device, r.image, r.imageMemory, 0)); err != nil {
150+ return
151+ }
152+ size := vk.DeviceSize(width) * vk.DeviceSize(height) * 4
153+ bi := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: size, Usage: vk.BufferUsageTransferDstBit, SharingMode: vk.SharingModeExclusive}
154+ if err = checked("vkCreateBuffer", r.dd.CreateBuffer(r.device, &bi, nil, &r.buffer)); err != nil {
155+ return
156+ }
157+ r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req)
158+ kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
159+ if err != nil {
160+ return
161+ }
162+ alloc = vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
163+ if err = checked("vkAllocateMemory(buffer)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.bufferMemory)); err != nil {
164+ return
165+ }
166+ if err = checked("vkBindBufferMemory", r.dd.BindBufferMemory(r.device, r.buffer, r.bufferMemory, 0)); err != nil {
167+ return
168+ }
169+ if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil {
170+ return
171+ }
172+ pi := vk.CommandPoolCreateInfo{SType: vk.StructureTypeCommandPoolCreateInfo, Flags: vk.CommandPoolCreateResetCommandBufferBit, QueueFamilyIndex: family}
173+ if err = checked("vkCreateCommandPool", r.dd.CreateCommandPool(r.device, &pi, nil, &r.pool)); err != nil {
174+ return
175+ }
176+ ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: 1}
177+ if err = checked("vkAllocateCommandBuffers", r.dd.AllocateCommandBuffers(r.device, &ai, &r.command)); err != nil {
178+ return
179+ }
180+ fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
181+ if err = checked("vkCreateFence", r.dd.CreateFence(r.device, &fi, nil, &r.fence)); err != nil {
182+ return
183+ }
184+ runtime.KeepAlive(priority)
185+ return
186+}
187+
188+func (r *Renderer) findMemoryType(bits uint32, props vk.MemoryPropertyFlags) (uint32, error) {
189+ for i := uint32(0); i < r.memory.MemoryTypeCount; i++ {
190+ if bits&(uint32(1)<<i) != 0 && r.memory.MemoryTypes[i].PropertyFlags&props == props {
191+ return i, nil
192+ }
193+ }
194+ return 0, fmt.Errorf("no Vulkan memory type for bits %#x and properties %#x", bits, props)
195+}
196+
197+func (r *Renderer) Clear(rgb [3]uint8) error {
198+ d := r.dd
199+ if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
200+ return err
201+ }
202+ begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo}
203+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil {
204+ return err
205+ }
206+ rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
207+ srcStage := vk.PipelineStageFlags(vk.PipelineStageTopOfPipeBit)
208+ srcAccess := vk.AccessFlags(0)
209+ if r.layout == vk.ImageLayoutTransferSrcOptimal {
210+ srcStage = vk.PipelineStageTransferBit
211+ srcAccess = vk.AccessTransferReadBit
212+ }
213+ barrier := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: srcAccess, DstAccessMask: vk.AccessTransferWriteBit, OldLayout: r.layout, NewLayout: vk.ImageLayoutTransferDstOptimal, Image: r.image, SubresourceRange: rangeInfo}
214+ d.CmdPipelineBarrier(r.command, srcStage, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
215+ // VkClearColorValue is a union; UNORM formats read it as float32.
216+ unorm := func(v uint8) uint32 { return math.Float32bits(float32(v) / 255) }
217+ color := vk.ClearColorValue{unorm(rgb[0]), unorm(rgb[1]), unorm(rgb[2]), math.Float32bits(1)}
218+ d.CmdClearColorImage(r.command, r.image, vk.ImageLayoutTransferDstOptimal, &color, 1, &rangeInfo)
219+ barrier.SrcAccessMask = vk.AccessTransferWriteBit
220+ barrier.DstAccessMask = vk.AccessTransferReadBit
221+ barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
222+ barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal
223+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
224+ copyRegion := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
225+ d.CmdCopyImageToBuffer(r.command, r.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &copyRegion)
226+ if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
227+ return err
228+ }
229+ submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
230+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
231+ return err
232+ }
233+ r.layout = vk.ImageLayoutTransferSrcOptimal
234+ if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
235+ return err
236+ }
237+ return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
238+}
239+
240+func (r *Renderer) Pixels() *image.RGBA {
241+ out := image.NewRGBA(image.Rect(0, 0, r.width, r.height))
242+ if r.mapped == nil {
243+ return out
244+ }
245+ src := unsafe.Slice((*byte)(r.mapped), len(out.Pix))
246+ for i := 0; i < len(src); i += 4 {
247+ out.Pix[i], out.Pix[i+1], out.Pix[i+2], out.Pix[i+3] = src[i+2], src[i+1], src[i], src[i+3]
248+ }
249+ return out
250+}
251+
252+func (r *Renderer) Close() {
253+ if r == nil {
254+ return
255+ }
256+ if r.dd != nil {
257+ d := r.dd
258+ if r.device != 0 {
259+ _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))
260+ }
261+ if r.fence != 0 {
262+ d.DestroyFence(r.device, r.fence, nil)
263+ r.fence = 0
264+ }
265+ if r.command != 0 {
266+ d.FreeCommandBuffers(r.device, r.pool, 1, &r.command)
267+ r.command = 0
268+ }
269+ if r.pool != 0 {
270+ d.DestroyCommandPool(r.device, r.pool, nil)
271+ r.pool = 0
272+ }
273+ if r.mapped != nil {
274+ d.UnmapMemory(r.device, r.bufferMemory)
275+ r.mapped = nil
276+ }
277+ if r.buffer != 0 {
278+ d.DestroyBuffer(r.device, r.buffer, nil)
279+ r.buffer = 0
280+ }
281+ if r.bufferMemory != 0 {
282+ d.FreeMemory(r.device, r.bufferMemory, nil)
283+ r.bufferMemory = 0
284+ }
285+ if r.image != 0 {
286+ d.DestroyImage(r.device, r.image, nil)
287+ r.image = 0
288+ }
289+ if r.imageMemory != 0 {
290+ d.FreeMemory(r.device, r.imageMemory, nil)
291+ r.imageMemory = 0
292+ }
293+ d.DestroyDevice(r.device, nil)
294+ r.device = 0
295+ } else if r.device != 0 && r.id != nil {
296+ if vk.VkDestroyDevice != nil {
297+ vk.VkDestroyDevice(r.device, nil)
298+ }
299+ r.device = 0
300+ }
301+ if r.instance != 0 {
302+ if r.id != nil {
303+ r.id.DestroyInstance(r.instance, nil)
304+ } else if vk.VkDestroyInstance != nil {
305+ vk.VkDestroyInstance(r.instance, nil)
306+ }
307+ r.instance = 0
308+ }
309+}
@@ -0,0 +1,309 @@
1+package vulkan
2+
3+import (
4+ "fmt"
5+ "image"
6+ "math"
7+ "os"
8+ "runtime"
9+ "strconv"
10+ "unsafe"
11+
12+ vk "github.com/bnema/purego-vulkan/vulkan"
13+)
14+
15+// Renderer is owned by one goroutine; its methods must not be called concurrently.
16+type Renderer struct {
17+ width, height int
18+ instance vk.Instance
19+ id *vk.InstanceDispatch
20+ device vk.Device
21+ dd *vk.DeviceDispatch
22+ queue vk.Queue
23+ image vk.Image
24+ imageMemory vk.DeviceMemory
25+ buffer vk.Buffer
26+ bufferMemory vk.DeviceMemory
27+ mapped unsafe.Pointer
28+ pool vk.CommandPool
29+ command vk.CommandBuffer
30+ fence vk.Fence
31+ layout vk.ImageLayout
32+ memory vk.PhysicalDeviceMemoryProperties
33+}
34+
35+func checked(name string, result vk.Result) error {
36+ if err := vk.Check(result); err != nil {
37+ return fmt.Errorf("%s: %w", name, err)
38+ }
39+ return nil
40+}
41+
42+func New(width, height int) (r *Renderer, err error) {
43+ if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {
44+ return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)
45+ }
46+ r = &Renderer{width: width, height: height, layout: vk.ImageLayoutUndefined}
47+ defer func() {
48+ if err != nil {
49+ r.Close()
50+ r = nil
51+ }
52+ }()
53+ if err = vk.Init(); err != nil {
54+ return nil, fmt.Errorf("vk.Init: %w", err)
55+ }
56+ app := vk.ApplicationInfo{SType: vk.StructureTypeApplicationInfo, ApiVersion: vk.MakeVersion(1, 3, 0)}
57+ info := vk.InstanceCreateInfo{SType: vk.StructureTypeInstanceCreateInfo, ApplicationInfo: &app}
58+ if err = checked("vkCreateInstance", vk.Global().CreateInstance(&info, nil, &r.instance)); err != nil {
59+ return
60+ }
61+ r.id, err = vk.LoadInstanceDispatch(r.instance)
62+ if err != nil {
63+ err = fmt.Errorf("LoadInstanceDispatch: %w", err)
64+ return
65+ }
66+ var count uint32
67+ if err = checked("vkEnumeratePhysicalDevices(count)", r.id.EnumeratePhysicalDevices(r.instance, &count, nil)); err != nil {
68+ return
69+ }
70+ if count == 0 {
71+ err = fmt.Errorf("no Vulkan 1.3 device")
72+ return
73+ }
74+ devices := make([]vk.PhysicalDevice, count)
75+ if err = checked("vkEnumeratePhysicalDevices", r.id.EnumeratePhysicalDevices(r.instance, &count, &devices[0])); err != nil {
76+ return
77+ }
78+ index := -1
79+ if value, ok := os.LookupEnv("NEFERTTY_VK_DEVICE"); ok {
80+ index, err = strconv.Atoi(value)
81+ if err != nil || index < 0 || index >= int(count) {
82+ err = fmt.Errorf("invalid NEFERTTY_VK_DEVICE index %q", value)
83+ return
84+ }
85+ }
86+ var physical vk.PhysicalDevice
87+ var family uint32
88+ for i, device := range devices[:count] {
89+ if index >= 0 && i != index {
90+ continue
91+ }
92+ var properties vk.PhysicalDeviceProperties
93+ r.id.GetPhysicalDeviceProperties(device, &properties)
94+ if properties.ApiVersion < vk.MakeVersion(1, 3, 0) {
95+ continue
96+ }
97+ var n uint32
98+ r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, nil)
99+ if n == 0 {
100+ continue
101+ }
102+ families := make([]vk.QueueFamilyProperties, n)
103+ r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, &families[0])
104+ for j, f := range families[:n] {
105+ if f.QueueCount > 0 && f.QueueFlags&vk.QueueGraphicsBit != 0 {
106+ physical = device
107+ family = uint32(j)
108+ break
109+ }
110+ }
111+ if physical != 0 {
112+ break
113+ }
114+ }
115+ if physical == 0 {
116+ err = fmt.Errorf("no Vulkan 1.3 device with a graphics queue")
117+ return
118+ }
119+ r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory)
120+ priority := float32(1)
121+ qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority}
122+ dynamicRendering := vk.PhysicalDeviceDynamicRenderingFeatures{SType: vk.StructureTypePhysicalDeviceDynamicRenderingFeatures, DynamicRendering: 1}
123+ di := vk.DeviceCreateInfo{SType: vk.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&dynamicRendering), QueueCreateInfoCount: 1, QueueCreateInfos: &qi}
124+ if err = checked("vkCreateDevice", r.id.CreateDevice(physical, &di, nil, &r.device)); err != nil {
125+ return
126+ }
127+ r.dd, err = vk.LoadDeviceDispatch(r.id, r.device)
128+ if err != nil {
129+ err = fmt.Errorf("LoadDeviceDispatch: %w", err)
130+ return
131+ }
132+ r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)
133+ extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}
134+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: extent, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
135+ if err = checked("vkCreateImage", r.dd.CreateImage(r.device, &ii, nil, &r.image)); err != nil {
136+ return
137+ }
138+ var req vk.MemoryRequirements
139+ r.dd.GetImageMemoryRequirements(r.device, r.image, &req)
140+ var kind uint32
141+ kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
142+ if err != nil {
143+ return
144+ }
145+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
146+ if err = checked("vkAllocateMemory(image)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.imageMemory)); err != nil {
147+ return
148+ }
149+ if err = checked("vkBindImageMemory", r.dd.BindImageMemory(r.device, r.image, r.imageMemory, 0)); err != nil {
150+ return
151+ }
152+ size := vk.DeviceSize(width) * vk.DeviceSize(height) * 4
153+ bi := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: size, Usage: vk.BufferUsageTransferDstBit, SharingMode: vk.SharingModeExclusive}
154+ if err = checked("vkCreateBuffer", r.dd.CreateBuffer(r.device, &bi, nil, &r.buffer)); err != nil {
155+ return
156+ }
157+ r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req)
158+ kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
159+ if err != nil {
160+ return
161+ }
162+ alloc = vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
163+ if err = checked("vkAllocateMemory(buffer)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.bufferMemory)); err != nil {
164+ return
165+ }
166+ if err = checked("vkBindBufferMemory", r.dd.BindBufferMemory(r.device, r.buffer, r.bufferMemory, 0)); err != nil {
167+ return
168+ }
169+ if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil {
170+ return
171+ }
172+ pi := vk.CommandPoolCreateInfo{SType: vk.StructureTypeCommandPoolCreateInfo, Flags: vk.CommandPoolCreateResetCommandBufferBit, QueueFamilyIndex: family}
173+ if err = checked("vkCreateCommandPool", r.dd.CreateCommandPool(r.device, &pi, nil, &r.pool)); err != nil {
174+ return
175+ }
176+ ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: 1}
177+ if err = checked("vkAllocateCommandBuffers", r.dd.AllocateCommandBuffers(r.device, &ai, &r.command)); err != nil {
178+ return
179+ }
180+ fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
181+ if err = checked("vkCreateFence", r.dd.CreateFence(r.device, &fi, nil, &r.fence)); err != nil {
182+ return
183+ }
184+ runtime.KeepAlive(priority)
185+ return
186+}
187+
188+func (r *Renderer) findMemoryType(bits uint32, props vk.MemoryPropertyFlags) (uint32, error) {
189+ for i := uint32(0); i < r.memory.MemoryTypeCount; i++ {
190+ if bits&(uint32(1)<<i) != 0 && r.memory.MemoryTypes[i].PropertyFlags&props == props {
191+ return i, nil
192+ }
193+ }
194+ return 0, fmt.Errorf("no Vulkan memory type for bits %#x and properties %#x", bits, props)
195+}
196+
197+func (r *Renderer) Clear(rgb [3]uint8) error {
198+ d := r.dd
199+ if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
200+ return err
201+ }
202+ begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo}
203+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil {
204+ return err
205+ }
206+ rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
207+ srcStage := vk.PipelineStageFlags(vk.PipelineStageTopOfPipeBit)
208+ srcAccess := vk.AccessFlags(0)
209+ if r.layout == vk.ImageLayoutTransferSrcOptimal {
210+ srcStage = vk.PipelineStageTransferBit
211+ srcAccess = vk.AccessTransferReadBit
212+ }
213+ barrier := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: srcAccess, DstAccessMask: vk.AccessTransferWriteBit, OldLayout: r.layout, NewLayout: vk.ImageLayoutTransferDstOptimal, Image: r.image, SubresourceRange: rangeInfo}
214+ d.CmdPipelineBarrier(r.command, srcStage, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
215+ // VkClearColorValue is a union; UNORM formats read it as float32.
216+ unorm := func(v uint8) uint32 { return math.Float32bits(float32(v) / 255) }
217+ color := vk.ClearColorValue{unorm(rgb[0]), unorm(rgb[1]), unorm(rgb[2]), math.Float32bits(1)}
218+ d.CmdClearColorImage(r.command, r.image, vk.ImageLayoutTransferDstOptimal, &color, 1, &rangeInfo)
219+ barrier.SrcAccessMask = vk.AccessTransferWriteBit
220+ barrier.DstAccessMask = vk.AccessTransferReadBit
221+ barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
222+ barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal
223+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
224+ copyRegion := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
225+ d.CmdCopyImageToBuffer(r.command, r.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &copyRegion)
226+ if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
227+ return err
228+ }
229+ submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
230+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
231+ return err
232+ }
233+ r.layout = vk.ImageLayoutTransferSrcOptimal
234+ if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
235+ return err
236+ }
237+ return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
238+}
239+
240+func (r *Renderer) Pixels() *image.RGBA {
241+ out := image.NewRGBA(image.Rect(0, 0, r.width, r.height))
242+ if r.mapped == nil {
243+ return out
244+ }
245+ src := unsafe.Slice((*byte)(r.mapped), len(out.Pix))
246+ for i := 0; i < len(src); i += 4 {
247+ out.Pix[i], out.Pix[i+1], out.Pix[i+2], out.Pix[i+3] = src[i+2], src[i+1], src[i], src[i+3]
248+ }
249+ return out
250+}
251+
252+func (r *Renderer) Close() {
253+ if r == nil {
254+ return
255+ }
256+ if r.dd != nil {
257+ d := r.dd
258+ if r.device != 0 {
259+ _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))
260+ }
261+ if r.fence != 0 {
262+ d.DestroyFence(r.device, r.fence, nil)
263+ r.fence = 0
264+ }
265+ if r.command != 0 {
266+ d.FreeCommandBuffers(r.device, r.pool, 1, &r.command)
267+ r.command = 0
268+ }
269+ if r.pool != 0 {
270+ d.DestroyCommandPool(r.device, r.pool, nil)
271+ r.pool = 0
272+ }
273+ if r.mapped != nil {
274+ d.UnmapMemory(r.device, r.bufferMemory)
275+ r.mapped = nil
276+ }
277+ if r.buffer != 0 {
278+ d.DestroyBuffer(r.device, r.buffer, nil)
279+ r.buffer = 0
280+ }
281+ if r.bufferMemory != 0 {
282+ d.FreeMemory(r.device, r.bufferMemory, nil)
283+ r.bufferMemory = 0
284+ }
285+ if r.image != 0 {
286+ d.DestroyImage(r.device, r.image, nil)
287+ r.image = 0
288+ }
289+ if r.imageMemory != 0 {
290+ d.FreeMemory(r.device, r.imageMemory, nil)
291+ r.imageMemory = 0
292+ }
293+ d.DestroyDevice(r.device, nil)
294+ r.device = 0
295+ } else if r.device != 0 && r.id != nil {
296+ if vk.VkDestroyDevice != nil {
297+ vk.VkDestroyDevice(r.device, nil)
298+ }
299+ r.device = 0
300+ }
301+ if r.instance != 0 {
302+ if r.id != nil {
303+ r.id.DestroyInstance(r.instance, nil)
304+ } else if vk.VkDestroyInstance != nil {
305+ vk.VkDestroyInstance(r.instance, nil)
306+ }
307+ r.instance = 0
308+ }
309+}
A internal/adapters/vulkan/renderer_test.go +24 -0
@@ -0,0 +1,24 @@
1+package vulkan
2+
3+import (
4+ "image/color"
5+ "testing"
6+)
7+
8+func TestRendererClear(t *testing.T) {
9+ r, err := New(64, 48)
10+ if err != nil {
11+ t.Skipf("Vulkan unavailable: %v", err)
12+ }
13+ defer r.Close()
14+ if err := r.Clear([3]uint8{0x10, 0x20, 0x30}); err != nil {
15+ t.Fatal(err)
16+ }
17+ pixels := r.Pixels()
18+ want := color.RGBA{0x10, 0x20, 0x30, 255}
19+ for _, p := range [][2]int{{0, 0}, {63, 47}} {
20+ if got := pixels.At(p[0], p[1]); got != want {
21+ t.Errorf("pixel %v = %v, want %v", p, got, want)
22+ }
23+ }
24+}
@@ -0,0 +1,24 @@
1+package vulkan
2+
3+import (
4+ "image/color"
5+ "testing"
6+)
7+
8+func TestRendererClear(t *testing.T) {
9+ r, err := New(64, 48)
10+ if err != nil {
11+ t.Skipf("Vulkan unavailable: %v", err)
12+ }
13+ defer r.Close()
14+ if err := r.Clear([3]uint8{0x10, 0x20, 0x30}); err != nil {
15+ t.Fatal(err)
16+ }
17+ pixels := r.Pixels()
18+ want := color.RGBA{0x10, 0x20, 0x30, 255}
19+ for _, p := range [][2]int{{0, 0}, {63, 47}} {
20+ if got := pixels.At(p[0], p[1]); got != want {
21+ t.Errorf("pixel %v = %v, want %v", p, got, want)
22+ }
23+ }
24+}

Keyboard shortcuts

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

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

Branches 25

Tags 10

↑↓ moveenter openesc close