feat(render): add offscreen Vulkan renderer with clear and readback
- commit
- 88e76375b10aa65e821b96e65aef7ac9bdef5cf7
- author
- brice · · 2026-09-23 15:21 UTC
- parent
- 8fd5205
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 4 | 4 | ||
| 5 | 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 | 9 | Run: `nefertty --backend=headless --timeout 5s` | |
| 10 | 10 | ||
| @@ -21,7 +21,7 @@ gaps = 8 | |||
| 21 | 21 | ||
| 22 | 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 | 26 | ## Try it (headless) | |
| 27 | 27 | ||
| @@ -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 | |||
| 5 | 5 | require ( | |
| 6 | 6 | github.com/BurntSushi/toml v1.5.0 | |
| 7 | 7 | github.com/bnema/purego-libwayland v0.0.0 | |
| 8 | + | github.com/bnema/purego-vulkan v0.0.0 | |
| 8 | 9 | github.com/bnema/wlturbo v0.2.0 | |
| 9 | 10 | github.com/bnema/zerowrap v1.4.1 | |
| 10 | 11 | github.com/rs/zerolog v1.35.1 | |
| @@ -20,3 +21,5 @@ require ( | |||
| 20 | 21 | ) | |
| 21 | 22 | ||
| 22 | 23 | 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.0 | 6 | github.com/BurntSushi/toml v1.5.0 | ||
| 7 | github.com/bnema/purego-libwayland v0.0.0 | 7 | github.com/bnema/purego-libwayland v0.0.0 | ||
| 8 | + | github.com/bnema/purego-vulkan v0.0.0 | |||
| 8 | github.com/bnema/wlturbo v0.2.0 | 9 | github.com/bnema/wlturbo v0.2.0 | ||
| 9 | github.com/bnema/zerowrap v1.4.1 | 10 | github.com/bnema/zerowrap v1.4.1 | ||
| 10 | github.com/rs/zerolog v1.35.1 | 11 | github.com/rs/zerolog v1.35.1 | ||
| @@ -20,3 +21,5 @@ require ( | |||||
| 20 | ) | 21 | ) | ||
| 21 | 22 | ||||
| 22 | replace github.com/bnema/purego-libwayland => ../purego-libwayland | 23 | 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, ©Region) | |
| 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, ©Region) | |||
| 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 | + | } | |||