feat(render): draw scene windows as clipped placeholder rectangles
- commit
- 26e0449c02f3b97f63f163e2fa32645052c3b2f5
- author
- brice · · 2026-09-23 15:24 UTC
- parent
- 88e7637
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +178 -0
M internal/adapters/vulkan/renderer.go +133 -0
| @@ -9,6 +9,8 @@ import ( | |||
| 9 | 9 | "strconv" | |
| 10 | 10 | "unsafe" | |
| 11 | 11 | ||
| 12 | + | "github.com/bnema/nefertty/internal/ports" | |
| 13 | + | ||
| 12 | 14 | vk "github.com/bnema/purego-vulkan/vulkan" | |
| 13 | 15 | ) | |
| 14 | 16 | ||
| @@ -24,6 +26,9 @@ type Renderer struct { | |||
| 24 | 26 | imageMemory vk.DeviceMemory | |
| 25 | 27 | buffer vk.Buffer | |
| 26 | 28 | bufferMemory vk.DeviceMemory | |
| 29 | + | staging vk.Buffer | |
| 30 | + | stagingMemory vk.DeviceMemory | |
| 31 | + | stagingMapped unsafe.Pointer | |
| 27 | 32 | mapped unsafe.Pointer | |
| 28 | 33 | pool vk.CommandPool | |
| 29 | 34 | command vk.CommandBuffer | |
| @@ -169,6 +174,27 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 169 | 174 | if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil { | |
| 170 | 175 | return | |
| 171 | 176 | } | |
| 177 | + | // Two frame-sized regions accommodate non-overlapping window bodies and their overlapping borders. | |
| 178 | + | stagingSize := size * 2 | |
| 179 | + | stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive} | |
| 180 | + | if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil { | |
| 181 | + | return | |
| 182 | + | } | |
| 183 | + | r.dd.GetBufferMemoryRequirements(r.device, r.staging, &req) | |
| 184 | + | kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit) | |
| 185 | + | if err != nil { | |
| 186 | + | return | |
| 187 | + | } | |
| 188 | + | alloc = vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind} | |
| 189 | + | if err = checked("vkAllocateMemory(staging)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.stagingMemory)); err != nil { | |
| 190 | + | return | |
| 191 | + | } | |
| 192 | + | if err = checked("vkBindBufferMemory(staging)", r.dd.BindBufferMemory(r.device, r.staging, r.stagingMemory, 0)); err != nil { | |
| 193 | + | return | |
| 194 | + | } | |
| 195 | + | if err = checked("vkMapMemory(staging)", r.dd.MapMemory(r.device, r.stagingMemory, 0, stagingSize, 0, &r.stagingMapped)); err != nil { | |
| 196 | + | return | |
| 197 | + | } | |
| 172 | 198 | pi := vk.CommandPoolCreateInfo{SType: vk.StructureTypeCommandPoolCreateInfo, Flags: vk.CommandPoolCreateResetCommandBufferBit, QueueFamilyIndex: family} | |
| 173 | 199 | if err = checked("vkCreateCommandPool", r.dd.CreateCommandPool(r.device, &pi, nil, &r.pool)); err != nil { | |
| 174 | 200 | return | |
| @@ -194,7 +220,93 @@ func (r *Renderer) findMemoryType(bits uint32, props vk.MemoryPropertyFlags) (ui | |||
| 194 | 220 | return 0, fmt.Errorf("no Vulkan memory type for bits %#x and properties %#x", bits, props) | |
| 195 | 221 | } | |
| 196 | 222 | ||
| 223 | + | func parseColor(s string) [3]uint8 { | |
| 224 | + | var c [3]uint8 | |
| 225 | + | if len(s) != 7 || s[0] != '#' { | |
| 226 | + | return c | |
| 227 | + | } | |
| 228 | + | for i := range c { | |
| 229 | + | v, e := strconv.ParseUint(s[1+i*2:3+i*2], 16, 8) | |
| 230 | + | if e != nil { | |
| 231 | + | return [3]uint8{} | |
| 232 | + | } | |
| 233 | + | c[i] = uint8(v) | |
| 234 | + | } | |
| 235 | + | return c | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | func windowColor(id ports.WindowID) [3]uint8 { | |
| 239 | + | h := math.Mod(float64(id)*0.618034, 1) * 6 | |
| 240 | + | sector := int(h) | |
| 241 | + | f := h - float64(sector) | |
| 242 | + | p, q, t := 0.4, 0.8*(1-0.5*f), 0.8*(1-0.5*(1-f)) | |
| 243 | + | var a, b, c float64 | |
| 244 | + | switch sector { | |
| 245 | + | case 0: | |
| 246 | + | a, b, c = 0.8, t, p | |
| 247 | + | case 1: | |
| 248 | + | a, b, c = q, 0.8, p | |
| 249 | + | case 2: | |
| 250 | + | a, b, c = p, 0.8, t | |
| 251 | + | case 3: | |
| 252 | + | a, b, c = p, q, 0.8 | |
| 253 | + | case 4: | |
| 254 | + | a, b, c = t, p, 0.8 | |
| 255 | + | default: | |
| 256 | + | a, b, c = 0.8, p, q | |
| 257 | + | } | |
| 258 | + | return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)} | |
| 259 | + | } | |
| 260 | + | ||
| 197 | 261 | func (r *Renderer) Clear(rgb [3]uint8) error { | |
| 262 | + | return r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}) | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | func (r *Renderer) Render(s ports.Scene) error { | |
| 266 | + | // Stage before recording: a full buffer is an error, not a partial submission. | |
| 267 | + | type fill struct { | |
| 268 | + | rect image.Rectangle | |
| 269 | + | color [3]uint8 | |
| 270 | + | offset int | |
| 271 | + | } | |
| 272 | + | var fills []fill | |
| 273 | + | used := 0 | |
| 274 | + | add := func(rect image.Rectangle, c [3]uint8) error { | |
| 275 | + | rect = rect.Intersect(image.Rect(0, 0, r.width, r.height)) | |
| 276 | + | if rect.Empty() { | |
| 277 | + | return nil | |
| 278 | + | } | |
| 279 | + | n := rect.Dx() * rect.Dy() * 4 | |
| 280 | + | if n > r.width*r.height*8-used { | |
| 281 | + | return fmt.Errorf("staging buffer full") | |
| 282 | + | } | |
| 283 | + | data := unsafe.Slice((*byte)(r.stagingMapped), r.width*r.height*8)[used : used+n] | |
| 284 | + | for i := 0; i < n; i += 4 { | |
| 285 | + | data[i], data[i+1], data[i+2], data[i+3] = c[2], c[1], c[0], 255 | |
| 286 | + | } | |
| 287 | + | fills = append(fills, fill{rect, c, used}) | |
| 288 | + | used += n | |
| 289 | + | return nil | |
| 290 | + | } | |
| 291 | + | for _, w := range s.Windows { | |
| 292 | + | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |
| 293 | + | continue | |
| 294 | + | } | |
| 295 | + | x, y := w.Rect.X, w.Rect.Y | |
| 296 | + | // Clip before converting coordinates to Vulkan's signed 32-bit offsets. | |
| 297 | + | body := image.Rect(x, y, x+w.Rect.W, y+w.Rect.H) | |
| 298 | + | if err := add(body, windowColor(w.ID)); err != nil { | |
| 299 | + | return err | |
| 300 | + | } | |
| 301 | + | if w.Focused { | |
| 302 | + | for _, strip := range []image.Rectangle{image.Rect(x, y, x+w.Rect.W, y+4), image.Rect(x, y+w.Rect.H-4, x+w.Rect.W, y+w.Rect.H), image.Rect(x, y+4, x+4, y+w.Rect.H-4), image.Rect(x+w.Rect.W-4, y+4, x+w.Rect.W, y+w.Rect.H-4)} { | |
| 303 | + | if err := add(strip, [3]uint8{255, 255, 255}); err != nil { | |
| 304 | + | return err | |
| 305 | + | } | |
| 306 | + | } | |
| 307 | + | } | |
| 308 | + | } | |
| 309 | + | rgb := parseColor(s.Background) | |
| 198 | 310 | d := r.dd | |
| 199 | 311 | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil { | |
| 200 | 312 | return err | |
| @@ -217,6 +329,15 @@ func (r *Renderer) Clear(rgb [3]uint8) error { | |||
| 217 | 329 | color := vk.ClearColorValue{unorm(rgb[0]), unorm(rgb[1]), unorm(rgb[2]), math.Float32bits(1)} | |
| 218 | 330 | d.CmdClearColorImage(r.command, r.image, vk.ImageLayoutTransferDstOptimal, &color, 1, &rangeInfo) | |
| 219 | 331 | barrier.SrcAccessMask = vk.AccessTransferWriteBit | |
| 332 | + | barrier.DstAccessMask = vk.AccessTransferWriteBit | |
| 333 | + | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | |
| 334 | + | barrier.NewLayout = vk.ImageLayoutTransferDstOptimal | |
| 335 | + | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | |
| 336 | + | for _, f := range fills { | |
| 337 | + | rect := f.rect | |
| 338 | + | region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}} | |
| 339 | + | d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, ®ion) | |
| 340 | + | } | |
| 220 | 341 | barrier.DstAccessMask = vk.AccessTransferReadBit | |
| 221 | 342 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | |
| 222 | 343 | barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal | |
| @@ -270,6 +391,18 @@ func (r *Renderer) Close() { | |||
| 270 | 391 | d.DestroyCommandPool(r.device, r.pool, nil) | |
| 271 | 392 | r.pool = 0 | |
| 272 | 393 | } | |
| 394 | + | if r.stagingMapped != nil { | |
| 395 | + | d.UnmapMemory(r.device, r.stagingMemory) | |
| 396 | + | r.stagingMapped = nil | |
| 397 | + | } | |
| 398 | + | if r.staging != 0 { | |
| 399 | + | d.DestroyBuffer(r.device, r.staging, nil) | |
| 400 | + | r.staging = 0 | |
| 401 | + | } | |
| 402 | + | if r.stagingMemory != 0 { | |
| 403 | + | d.FreeMemory(r.device, r.stagingMemory, nil) | |
| 404 | + | r.stagingMemory = 0 | |
| 405 | + | } | |
| 273 | 406 | if r.mapped != nil { | |
| 274 | 407 | d.UnmapMemory(r.device, r.bufferMemory) | |
| 275 | 408 | r.mapped = nil | |
| @@ -9,6 +9,8 @@ import ( | |||||
| 9 | "strconv" | 9 | "strconv" | ||
| 10 | "unsafe" | 10 | "unsafe" | ||
| 11 | 11 | ||||
| 12 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 13 | + | ||||
| 12 | vk "github.com/bnema/purego-vulkan/vulkan" | 14 | vk "github.com/bnema/purego-vulkan/vulkan" | ||
| 13 | ) | 15 | ) | ||
| 14 | 16 | ||||
| @@ -24,6 +26,9 @@ type Renderer struct { | |||||
| 24 | imageMemory vk.DeviceMemory | 26 | imageMemory vk.DeviceMemory | ||
| 25 | buffer vk.Buffer | 27 | buffer vk.Buffer | ||
| 26 | bufferMemory vk.DeviceMemory | 28 | bufferMemory vk.DeviceMemory | ||
| 29 | + | staging vk.Buffer | |||
| 30 | + | stagingMemory vk.DeviceMemory | |||
| 31 | + | stagingMapped unsafe.Pointer | |||
| 27 | mapped unsafe.Pointer | 32 | mapped unsafe.Pointer | ||
| 28 | pool vk.CommandPool | 33 | pool vk.CommandPool | ||
| 29 | command vk.CommandBuffer | 34 | command vk.CommandBuffer | ||
| @@ -169,6 +174,27 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 169 | if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil { | 174 | if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil { | ||
| 170 | return | 175 | return | ||
| 171 | } | 176 | } | ||
| 177 | + | // Two frame-sized regions accommodate non-overlapping window bodies and their overlapping borders. | |||
| 178 | + | stagingSize := size * 2 | |||
| 179 | + | stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive} | |||
| 180 | + | if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil { | |||
| 181 | + | return | |||
| 182 | + | } | |||
| 183 | + | r.dd.GetBufferMemoryRequirements(r.device, r.staging, &req) | |||
| 184 | + | kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit) | |||
| 185 | + | if err != nil { | |||
| 186 | + | return | |||
| 187 | + | } | |||
| 188 | + | alloc = vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind} | |||
| 189 | + | if err = checked("vkAllocateMemory(staging)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.stagingMemory)); err != nil { | |||
| 190 | + | return | |||
| 191 | + | } | |||
| 192 | + | if err = checked("vkBindBufferMemory(staging)", r.dd.BindBufferMemory(r.device, r.staging, r.stagingMemory, 0)); err != nil { | |||
| 193 | + | return | |||
| 194 | + | } | |||
| 195 | + | if err = checked("vkMapMemory(staging)", r.dd.MapMemory(r.device, r.stagingMemory, 0, stagingSize, 0, &r.stagingMapped)); err != nil { | |||
| 196 | + | return | |||
| 197 | + | } | |||
| 172 | pi := vk.CommandPoolCreateInfo{SType: vk.StructureTypeCommandPoolCreateInfo, Flags: vk.CommandPoolCreateResetCommandBufferBit, QueueFamilyIndex: family} | 198 | 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 { | 199 | if err = checked("vkCreateCommandPool", r.dd.CreateCommandPool(r.device, &pi, nil, &r.pool)); err != nil { | ||
| 174 | return | 200 | return | ||
| @@ -194,7 +220,93 @@ func (r *Renderer) findMemoryType(bits uint32, props vk.MemoryPropertyFlags) (ui | |||||
| 194 | return 0, fmt.Errorf("no Vulkan memory type for bits %#x and properties %#x", bits, props) | 220 | return 0, fmt.Errorf("no Vulkan memory type for bits %#x and properties %#x", bits, props) | ||
| 195 | } | 221 | } | ||
| 196 | 222 | ||||
| 223 | + | func parseColor(s string) [3]uint8 { | |||
| 224 | + | var c [3]uint8 | |||
| 225 | + | if len(s) != 7 || s[0] != '#' { | |||
| 226 | + | return c | |||
| 227 | + | } | |||
| 228 | + | for i := range c { | |||
| 229 | + | v, e := strconv.ParseUint(s[1+i*2:3+i*2], 16, 8) | |||
| 230 | + | if e != nil { | |||
| 231 | + | return [3]uint8{} | |||
| 232 | + | } | |||
| 233 | + | c[i] = uint8(v) | |||
| 234 | + | } | |||
| 235 | + | return c | |||
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | func windowColor(id ports.WindowID) [3]uint8 { | |||
| 239 | + | h := math.Mod(float64(id)*0.618034, 1) * 6 | |||
| 240 | + | sector := int(h) | |||
| 241 | + | f := h - float64(sector) | |||
| 242 | + | p, q, t := 0.4, 0.8*(1-0.5*f), 0.8*(1-0.5*(1-f)) | |||
| 243 | + | var a, b, c float64 | |||
| 244 | + | switch sector { | |||
| 245 | + | case 0: | |||
| 246 | + | a, b, c = 0.8, t, p | |||
| 247 | + | case 1: | |||
| 248 | + | a, b, c = q, 0.8, p | |||
| 249 | + | case 2: | |||
| 250 | + | a, b, c = p, 0.8, t | |||
| 251 | + | case 3: | |||
| 252 | + | a, b, c = p, q, 0.8 | |||
| 253 | + | case 4: | |||
| 254 | + | a, b, c = t, p, 0.8 | |||
| 255 | + | default: | |||
| 256 | + | a, b, c = 0.8, p, q | |||
| 257 | + | } | |||
| 258 | + | return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)} | |||
| 259 | + | } | |||
| 260 | + | ||||
| 197 | func (r *Renderer) Clear(rgb [3]uint8) error { | 261 | func (r *Renderer) Clear(rgb [3]uint8) error { | ||
| 262 | + | return r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}) | |||
| 263 | + | } | |||
| 264 | + | ||||
| 265 | + | func (r *Renderer) Render(s ports.Scene) error { | |||
| 266 | + | // Stage before recording: a full buffer is an error, not a partial submission. | |||
| 267 | + | type fill struct { | |||
| 268 | + | rect image.Rectangle | |||
| 269 | + | color [3]uint8 | |||
| 270 | + | offset int | |||
| 271 | + | } | |||
| 272 | + | var fills []fill | |||
| 273 | + | used := 0 | |||
| 274 | + | add := func(rect image.Rectangle, c [3]uint8) error { | |||
| 275 | + | rect = rect.Intersect(image.Rect(0, 0, r.width, r.height)) | |||
| 276 | + | if rect.Empty() { | |||
| 277 | + | return nil | |||
| 278 | + | } | |||
| 279 | + | n := rect.Dx() * rect.Dy() * 4 | |||
| 280 | + | if n > r.width*r.height*8-used { | |||
| 281 | + | return fmt.Errorf("staging buffer full") | |||
| 282 | + | } | |||
| 283 | + | data := unsafe.Slice((*byte)(r.stagingMapped), r.width*r.height*8)[used : used+n] | |||
| 284 | + | for i := 0; i < n; i += 4 { | |||
| 285 | + | data[i], data[i+1], data[i+2], data[i+3] = c[2], c[1], c[0], 255 | |||
| 286 | + | } | |||
| 287 | + | fills = append(fills, fill{rect, c, used}) | |||
| 288 | + | used += n | |||
| 289 | + | return nil | |||
| 290 | + | } | |||
| 291 | + | for _, w := range s.Windows { | |||
| 292 | + | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |||
| 293 | + | continue | |||
| 294 | + | } | |||
| 295 | + | x, y := w.Rect.X, w.Rect.Y | |||
| 296 | + | // Clip before converting coordinates to Vulkan's signed 32-bit offsets. | |||
| 297 | + | body := image.Rect(x, y, x+w.Rect.W, y+w.Rect.H) | |||
| 298 | + | if err := add(body, windowColor(w.ID)); err != nil { | |||
| 299 | + | return err | |||
| 300 | + | } | |||
| 301 | + | if w.Focused { | |||
| 302 | + | for _, strip := range []image.Rectangle{image.Rect(x, y, x+w.Rect.W, y+4), image.Rect(x, y+w.Rect.H-4, x+w.Rect.W, y+w.Rect.H), image.Rect(x, y+4, x+4, y+w.Rect.H-4), image.Rect(x+w.Rect.W-4, y+4, x+w.Rect.W, y+w.Rect.H-4)} { | |||
| 303 | + | if err := add(strip, [3]uint8{255, 255, 255}); err != nil { | |||
| 304 | + | return err | |||
| 305 | + | } | |||
| 306 | + | } | |||
| 307 | + | } | |||
| 308 | + | } | |||
| 309 | + | rgb := parseColor(s.Background) | |||
| 198 | d := r.dd | 310 | d := r.dd | ||
| 199 | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil { | 311 | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil { | ||
| 200 | return err | 312 | return err | ||
| @@ -217,6 +329,15 @@ func (r *Renderer) Clear(rgb [3]uint8) error { | |||||
| 217 | color := vk.ClearColorValue{unorm(rgb[0]), unorm(rgb[1]), unorm(rgb[2]), math.Float32bits(1)} | 329 | 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) | 330 | d.CmdClearColorImage(r.command, r.image, vk.ImageLayoutTransferDstOptimal, &color, 1, &rangeInfo) | ||
| 219 | barrier.SrcAccessMask = vk.AccessTransferWriteBit | 331 | barrier.SrcAccessMask = vk.AccessTransferWriteBit | ||
| 332 | + | barrier.DstAccessMask = vk.AccessTransferWriteBit | |||
| 333 | + | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | |||
| 334 | + | barrier.NewLayout = vk.ImageLayoutTransferDstOptimal | |||
| 335 | + | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | |||
| 336 | + | for _, f := range fills { | |||
| 337 | + | rect := f.rect | |||
| 338 | + | region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}} | |||
| 339 | + | d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, ®ion) | |||
| 340 | + | } | |||
| 220 | barrier.DstAccessMask = vk.AccessTransferReadBit | 341 | barrier.DstAccessMask = vk.AccessTransferReadBit | ||
| 221 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | 342 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | ||
| 222 | barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal | 343 | barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal | ||
| @@ -270,6 +391,18 @@ func (r *Renderer) Close() { | |||||
| 270 | d.DestroyCommandPool(r.device, r.pool, nil) | 391 | d.DestroyCommandPool(r.device, r.pool, nil) | ||
| 271 | r.pool = 0 | 392 | r.pool = 0 | ||
| 272 | } | 393 | } | ||
| 394 | + | if r.stagingMapped != nil { | |||
| 395 | + | d.UnmapMemory(r.device, r.stagingMemory) | |||
| 396 | + | r.stagingMapped = nil | |||
| 397 | + | } | |||
| 398 | + | if r.staging != 0 { | |||
| 399 | + | d.DestroyBuffer(r.device, r.staging, nil) | |||
| 400 | + | r.staging = 0 | |||
| 401 | + | } | |||
| 402 | + | if r.stagingMemory != 0 { | |||
| 403 | + | d.FreeMemory(r.device, r.stagingMemory, nil) | |||
| 404 | + | r.stagingMemory = 0 | |||
| 405 | + | } | |||
| 273 | if r.mapped != nil { | 406 | if r.mapped != nil { | ||
| 274 | d.UnmapMemory(r.device, r.bufferMemory) | 407 | d.UnmapMemory(r.device, r.bufferMemory) | ||
| 275 | r.mapped = nil | 408 | r.mapped = nil | ||
M internal/adapters/vulkan/renderer_test.go +45 -0
| @@ -1,6 +1,8 @@ | |||
| 1 | 1 | package vulkan | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |
| 5 | + | "image" | |
| 4 | 6 | "image/color" | |
| 5 | 7 | "testing" | |
| 6 | 8 | ) | |
| @@ -22,3 +24,46 @@ func TestRendererClear(t *testing.T) { | |||
| 22 | 24 | } | |
| 23 | 25 | } | |
| 24 | 26 | } | |
| 27 | + | ||
| 28 | + | func TestParseColor(t *testing.T) { | |
| 29 | + | for _, tc := range []struct { | |
| 30 | + | s string | |
| 31 | + | want [3]uint8 | |
| 32 | + | }{{"#102030", [3]uint8{16, 32, 48}}, {"#FFFFFF", [3]uint8{255, 255, 255}}, {"bad", [3]uint8{}}, {"#12gg34", [3]uint8{}}, {"102030", [3]uint8{}}} { | |
| 33 | + | if got := parseColor(tc.s); got != tc.want { | |
| 34 | + | t.Errorf("parseColor(%q)=%v want %v", tc.s, got, tc.want) | |
| 35 | + | } | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | func TestRendererRender(t *testing.T) { | |
| 40 | + | r, err := New(64, 48) | |
| 41 | + | if err != nil { | |
| 42 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 43 | + | } | |
| 44 | + | defer r.Close() | |
| 45 | + | bg := color.RGBA{16, 32, 48, 255} | |
| 46 | + | check := func(s ports.Scene, expected map[image.Point]color.RGBA) { | |
| 47 | + | t.Helper() | |
| 48 | + | if err := r.Render(s); err != nil { | |
| 49 | + | t.Fatal(err) | |
| 50 | + | } | |
| 51 | + | for p, want := range expected { | |
| 52 | + | if got := r.Pixels().At(p.X, p.Y); got != want { | |
| 53 | + | t.Errorf("pixel %v=%v want %v", p, got, want) | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | win := ports.SceneWindow{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}} | |
| 58 | + | c := windowColor(1) | |
| 59 | + | wc := color.RGBA{c[0], c[1], c[2], 255} | |
| 60 | + | s := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{win}} | |
| 61 | + | check(s, map[image.Point]color.RGBA{{0, 0}: bg, {16, 16}: wc}) | |
| 62 | + | s.Windows[0].Focused = true | |
| 63 | + | check(s, map[image.Point]color.RGBA{{9, 9}: {255, 255, 255, 255}, {16, 16}: wc}) | |
| 64 | + | s.Windows[0] = ports.SceneWindow{ID: 2, Rect: ports.Rect{X: -10, Y: 0, W: 20, H: 10}} | |
| 65 | + | c = windowColor(2) | |
| 66 | + | check(s, map[image.Point]color.RGBA{{5, 5}: {c[0], c[1], c[2], 255}, {15, 5}: bg}) | |
| 67 | + | s.Windows[0].Hidden = true | |
| 68 | + | check(s, map[image.Point]color.RGBA{{5, 5}: bg}) | |
| 69 | + | } | |
| @@ -1,6 +1,8 @@ | |||||
| 1 | package vulkan | 1 | package vulkan | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 5 | + | "image" | |||
| 4 | "image/color" | 6 | "image/color" | ||
| 5 | "testing" | 7 | "testing" | ||
| 6 | ) | 8 | ) | ||
| @@ -22,3 +24,46 @@ func TestRendererClear(t *testing.T) { | |||||
| 22 | } | 24 | } | ||
| 23 | } | 25 | } | ||
| 24 | } | 26 | } | ||
| 27 | + | ||||
| 28 | + | func TestParseColor(t *testing.T) { | |||
| 29 | + | for _, tc := range []struct { | |||
| 30 | + | s string | |||
| 31 | + | want [3]uint8 | |||
| 32 | + | }{{"#102030", [3]uint8{16, 32, 48}}, {"#FFFFFF", [3]uint8{255, 255, 255}}, {"bad", [3]uint8{}}, {"#12gg34", [3]uint8{}}, {"102030", [3]uint8{}}} { | |||
| 33 | + | if got := parseColor(tc.s); got != tc.want { | |||
| 34 | + | t.Errorf("parseColor(%q)=%v want %v", tc.s, got, tc.want) | |||
| 35 | + | } | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | func TestRendererRender(t *testing.T) { | |||
| 40 | + | r, err := New(64, 48) | |||
| 41 | + | if err != nil { | |||
| 42 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 43 | + | } | |||
| 44 | + | defer r.Close() | |||
| 45 | + | bg := color.RGBA{16, 32, 48, 255} | |||
| 46 | + | check := func(s ports.Scene, expected map[image.Point]color.RGBA) { | |||
| 47 | + | t.Helper() | |||
| 48 | + | if err := r.Render(s); err != nil { | |||
| 49 | + | t.Fatal(err) | |||
| 50 | + | } | |||
| 51 | + | for p, want := range expected { | |||
| 52 | + | if got := r.Pixels().At(p.X, p.Y); got != want { | |||
| 53 | + | t.Errorf("pixel %v=%v want %v", p, got, want) | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | win := ports.SceneWindow{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}} | |||
| 58 | + | c := windowColor(1) | |||
| 59 | + | wc := color.RGBA{c[0], c[1], c[2], 255} | |||
| 60 | + | s := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{win}} | |||
| 61 | + | check(s, map[image.Point]color.RGBA{{0, 0}: bg, {16, 16}: wc}) | |||
| 62 | + | s.Windows[0].Focused = true | |||
| 63 | + | check(s, map[image.Point]color.RGBA{{9, 9}: {255, 255, 255, 255}, {16, 16}: wc}) | |||
| 64 | + | s.Windows[0] = ports.SceneWindow{ID: 2, Rect: ports.Rect{X: -10, Y: 0, W: 20, H: 10}} | |||
| 65 | + | c = windowColor(2) | |||
| 66 | + | check(s, map[image.Point]color.RGBA{{5, 5}: {c[0], c[1], c[2], 255}, {15, 5}: bg}) | |||
| 67 | + | s.Windows[0].Hidden = true | |||
| 68 | + | check(s, map[image.Point]color.RGBA{{5, 5}: bg}) | |||
| 69 | + | } | |||