feat(render): draw client SHM contents in window rects
- commit
- 7dfb0ed4617be2237d525c9f8f17399008b9c993
- author
- brice · · 2026-09-23 15:26 UTC
- parent
- f1e2d50
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +141 -28
M internal/adapters/vulkan/renderer.go +97 -27
| @@ -29,6 +29,7 @@ type Renderer struct { | |||
| 29 | 29 | staging vk.Buffer | |
| 30 | 30 | stagingMemory vk.DeviceMemory | |
| 31 | 31 | stagingMapped unsafe.Pointer | |
| 32 | + | stagingSize int | |
| 32 | 33 | mapped unsafe.Pointer | |
| 33 | 34 | pool vk.CommandPool | |
| 34 | 35 | command vk.CommandBuffer | |
| @@ -174,8 +175,9 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 174 | 175 | if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil { | |
| 175 | 176 | return | |
| 176 | 177 | } | |
| 177 | - | // Two frame-sized regions accommodate non-overlapping window bodies and their overlapping borders. | |
| 178 | + | // Start with two frame-sized regions; later frames grow staging on demand. | |
| 178 | 179 | stagingSize := size * 2 | |
| 180 | + | r.stagingSize = int(stagingSize) | |
| 179 | 181 | stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive} | |
| 180 | 182 | if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil { | |
| 181 | 183 | return | |
| @@ -258,54 +260,122 @@ func windowColor(id ports.WindowID) [3]uint8 { | |||
| 258 | 260 | return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)} | |
| 259 | 261 | } | |
| 260 | 262 | ||
| 263 | + | // ensureStaging grows the mapped transfer buffer before recording any commands. | |
| 264 | + | func (r *Renderer) ensureStaging(size int) error { | |
| 265 | + | if size <= r.stagingSize { | |
| 266 | + | return nil | |
| 267 | + | } | |
| 268 | + | d := r.dd | |
| 269 | + | capacity := size | |
| 270 | + | info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive} | |
| 271 | + | var buffer vk.Buffer | |
| 272 | + | if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil { | |
| 273 | + | return err | |
| 274 | + | } | |
| 275 | + | var memory vk.DeviceMemory | |
| 276 | + | defer func() { | |
| 277 | + | if buffer != 0 { | |
| 278 | + | d.DestroyBuffer(r.device, buffer, nil) | |
| 279 | + | } | |
| 280 | + | if memory != 0 { | |
| 281 | + | d.FreeMemory(r.device, memory, nil) | |
| 282 | + | } | |
| 283 | + | }() | |
| 284 | + | var req vk.MemoryRequirements | |
| 285 | + | d.GetBufferMemoryRequirements(r.device, buffer, &req) | |
| 286 | + | kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit) | |
| 287 | + | if err != nil { | |
| 288 | + | return err | |
| 289 | + | } | |
| 290 | + | alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind} | |
| 291 | + | if err := checked("vkAllocateMemory(staging)", d.AllocateMemory(r.device, &alloc, nil, &memory)); err != nil { | |
| 292 | + | return err | |
| 293 | + | } | |
| 294 | + | if err := checked("vkBindBufferMemory(staging)", d.BindBufferMemory(r.device, buffer, memory, 0)); err != nil { | |
| 295 | + | return err | |
| 296 | + | } | |
| 297 | + | var mapped unsafe.Pointer | |
| 298 | + | if err := checked("vkMapMemory(staging)", d.MapMemory(r.device, memory, 0, vk.DeviceSize(capacity), 0, &mapped)); err != nil { | |
| 299 | + | return err | |
| 300 | + | } | |
| 301 | + | d.UnmapMemory(r.device, r.stagingMemory) | |
| 302 | + | d.DestroyBuffer(r.device, r.staging, nil) | |
| 303 | + | d.FreeMemory(r.device, r.stagingMemory, nil) | |
| 304 | + | r.staging, r.stagingMemory, r.stagingMapped, r.stagingSize = buffer, memory, mapped, capacity | |
| 305 | + | buffer, memory = 0, 0 | |
| 306 | + | return nil | |
| 307 | + | } | |
| 308 | + | ||
| 261 | 309 | 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])}) | |
| 310 | + | return r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil) | |
| 263 | 311 | } | |
| 264 | 312 | ||
| 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 | |
| 313 | + | func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error { | |
| 314 | + | type upload struct { | |
| 315 | + | rect image.Rectangle | |
| 316 | + | offset int | |
| 317 | + | pixels []byte | |
| 318 | + | stride, sourceX, sourceY int | |
| 319 | + | color [3]uint8 | |
| 320 | + | } | |
| 321 | + | var uploads []upload | |
| 273 | 322 | used := 0 | |
| 274 | - | add := func(rect image.Rectangle, c [3]uint8) error { | |
| 275 | - | rect = rect.Intersect(image.Rect(0, 0, r.width, r.height)) | |
| 323 | + | bounds := image.Rect(0, 0, r.width, r.height) | |
| 324 | + | add := func(rect image.Rectangle, c [3]uint8, content *ports.SurfaceContent, origin image.Point) { | |
| 325 | + | rect = rect.Intersect(bounds) | |
| 276 | 326 | if rect.Empty() { | |
| 277 | - | return nil | |
| 327 | + | return | |
| 278 | 328 | } | |
| 279 | 329 | n := rect.Dx() * rect.Dy() * 4 | |
| 280 | - | if n > r.width*r.height*8-used { | |
| 281 | - | return fmt.Errorf("staging buffer full") | |
| 330 | + | u := upload{rect: rect, offset: used, color: c} | |
| 331 | + | if content != nil { | |
| 332 | + | u.pixels, u.stride = content.Pixels, content.Stride | |
| 333 | + | u.sourceX, u.sourceY = rect.Min.X-origin.X, rect.Min.Y-origin.Y | |
| 282 | 334 | } | |
| 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}) | |
| 335 | + | uploads = append(uploads, u) | |
| 288 | 336 | used += n | |
| 289 | - | return nil | |
| 290 | 337 | } | |
| 291 | 338 | for _, w := range s.Windows { | |
| 292 | 339 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |
| 293 | 340 | continue | |
| 294 | 341 | } | |
| 295 | 342 | x, y := w.Rect.X, w.Rect.Y | |
| 296 | - | // Clip before converting coordinates to Vulkan's signed 32-bit offsets. | |
| 297 | 343 | 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 | |
| 344 | + | content := contents[w.ID] | |
| 345 | + | if content.Pixels == nil { | |
| 346 | + | add(body, windowColor(w.ID), nil, image.Point{}) | |
| 347 | + | } else { | |
| 348 | + | add(body, parseColor(s.Background), nil, image.Point{}) | |
| 349 | + | width, height := min(content.Width, w.Rect.W), min(content.Height, w.Rect.H) | |
| 350 | + | if width > 0 && height > 0 && content.Stride >= width*4 && len(content.Pixels) >= (height-1)*content.Stride+width*4 { | |
| 351 | + | add(image.Rect(x, y, x+width, y+height), [3]uint8{}, &content, image.Pt(x, y)) | |
| 352 | + | } | |
| 300 | 353 | } | |
| 301 | 354 | if w.Focused { | |
| 302 | 355 | 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 | |
| 356 | + | add(strip, [3]uint8{255, 255, 255}, nil, image.Point{}) | |
| 357 | + | } | |
| 358 | + | } | |
| 359 | + | } | |
| 360 | + | if err := r.ensureStaging(used); err != nil { | |
| 361 | + | return err | |
| 362 | + | } | |
| 363 | + | data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize) | |
| 364 | + | for _, u := range uploads { | |
| 365 | + | dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4] | |
| 366 | + | for row := 0; row < u.rect.Dy(); row++ { | |
| 367 | + | line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4] | |
| 368 | + | if u.pixels != nil { | |
| 369 | + | start := (u.sourceY+row)*u.stride + u.sourceX*4 | |
| 370 | + | copy(line, u.pixels[start:start+len(line)]) | |
| 371 | + | } else { | |
| 372 | + | for i := 0; i < len(line); i += 4 { | |
| 373 | + | line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255 | |
| 305 | 374 | } | |
| 306 | 375 | } | |
| 307 | 376 | } | |
| 308 | 377 | } | |
| 378 | + | // B8G8R8A8 pixels are copied unchanged; blending belongs to the compositing pipeline. | |
| 309 | 379 | rgb := parseColor(s.Background) | |
| 310 | 380 | d := r.dd | |
| 311 | 381 | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil { | |
| @@ -333,7 +403,7 @@ func (r *Renderer) Render(s ports.Scene) error { | |||
| 333 | 403 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | |
| 334 | 404 | barrier.NewLayout = vk.ImageLayoutTransferDstOptimal | |
| 335 | 405 | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | |
| 336 | - | for _, f := range fills { | |
| 406 | + | for _, f := range uploads { | |
| 337 | 407 | rect := f.rect | |
| 338 | 408 | 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 | 409 | d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, ®ion) | |
| @@ -29,6 +29,7 @@ type Renderer struct { | |||||
| 29 | staging vk.Buffer | 29 | staging vk.Buffer | ||
| 30 | stagingMemory vk.DeviceMemory | 30 | stagingMemory vk.DeviceMemory | ||
| 31 | stagingMapped unsafe.Pointer | 31 | stagingMapped unsafe.Pointer | ||
| 32 | + | stagingSize int | |||
| 32 | mapped unsafe.Pointer | 33 | mapped unsafe.Pointer | ||
| 33 | pool vk.CommandPool | 34 | pool vk.CommandPool | ||
| 34 | command vk.CommandBuffer | 35 | command vk.CommandBuffer | ||
| @@ -174,8 +175,9 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 174 | if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil { | 175 | if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil { | ||
| 175 | return | 176 | return | ||
| 176 | } | 177 | } | ||
| 177 | - | // Two frame-sized regions accommodate non-overlapping window bodies and their overlapping borders. | 178 | + | // Start with two frame-sized regions; later frames grow staging on demand. |
| 178 | stagingSize := size * 2 | 179 | stagingSize := size * 2 | ||
| 180 | + | r.stagingSize = int(stagingSize) | |||
| 179 | stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive} | 181 | 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 { | 182 | if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil { | ||
| 181 | return | 183 | return | ||
| @@ -258,54 +260,122 @@ func windowColor(id ports.WindowID) [3]uint8 { | |||||
| 258 | return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)} | 260 | return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)} | ||
| 259 | } | 261 | } | ||
| 260 | 262 | ||||
| 263 | + | // ensureStaging grows the mapped transfer buffer before recording any commands. | |||
| 264 | + | func (r *Renderer) ensureStaging(size int) error { | |||
| 265 | + | if size <= r.stagingSize { | |||
| 266 | + | return nil | |||
| 267 | + | } | |||
| 268 | + | d := r.dd | |||
| 269 | + | capacity := size | |||
| 270 | + | info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive} | |||
| 271 | + | var buffer vk.Buffer | |||
| 272 | + | if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil { | |||
| 273 | + | return err | |||
| 274 | + | } | |||
| 275 | + | var memory vk.DeviceMemory | |||
| 276 | + | defer func() { | |||
| 277 | + | if buffer != 0 { | |||
| 278 | + | d.DestroyBuffer(r.device, buffer, nil) | |||
| 279 | + | } | |||
| 280 | + | if memory != 0 { | |||
| 281 | + | d.FreeMemory(r.device, memory, nil) | |||
| 282 | + | } | |||
| 283 | + | }() | |||
| 284 | + | var req vk.MemoryRequirements | |||
| 285 | + | d.GetBufferMemoryRequirements(r.device, buffer, &req) | |||
| 286 | + | kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit) | |||
| 287 | + | if err != nil { | |||
| 288 | + | return err | |||
| 289 | + | } | |||
| 290 | + | alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind} | |||
| 291 | + | if err := checked("vkAllocateMemory(staging)", d.AllocateMemory(r.device, &alloc, nil, &memory)); err != nil { | |||
| 292 | + | return err | |||
| 293 | + | } | |||
| 294 | + | if err := checked("vkBindBufferMemory(staging)", d.BindBufferMemory(r.device, buffer, memory, 0)); err != nil { | |||
| 295 | + | return err | |||
| 296 | + | } | |||
| 297 | + | var mapped unsafe.Pointer | |||
| 298 | + | if err := checked("vkMapMemory(staging)", d.MapMemory(r.device, memory, 0, vk.DeviceSize(capacity), 0, &mapped)); err != nil { | |||
| 299 | + | return err | |||
| 300 | + | } | |||
| 301 | + | d.UnmapMemory(r.device, r.stagingMemory) | |||
| 302 | + | d.DestroyBuffer(r.device, r.staging, nil) | |||
| 303 | + | d.FreeMemory(r.device, r.stagingMemory, nil) | |||
| 304 | + | r.staging, r.stagingMemory, r.stagingMapped, r.stagingSize = buffer, memory, mapped, capacity | |||
| 305 | + | buffer, memory = 0, 0 | |||
| 306 | + | return nil | |||
| 307 | + | } | |||
| 308 | + | ||||
| 261 | func (r *Renderer) Clear(rgb [3]uint8) error { | 309 | 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])}) | 310 | + | return r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil) |
| 263 | } | 311 | } | ||
| 264 | 312 | ||||
| 265 | - | func (r *Renderer) Render(s ports.Scene) error { | 313 | + | func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error { |
| 266 | - | // Stage before recording: a full buffer is an error, not a partial submission. | 314 | + | type upload struct { |
| 267 | - | type fill struct { | 315 | + | rect image.Rectangle |
| 268 | - | rect image.Rectangle | 316 | + | offset int |
| 269 | - | color [3]uint8 | 317 | + | pixels []byte |
| 270 | - | offset int | 318 | + | stride, sourceX, sourceY int |
| 271 | - | } | 319 | + | color [3]uint8 |
| 272 | - | var fills []fill | 320 | + | } |
| 321 | + | var uploads []upload | |||
| 273 | used := 0 | 322 | used := 0 | ||
| 274 | - | add := func(rect image.Rectangle, c [3]uint8) error { | 323 | + | bounds := image.Rect(0, 0, r.width, r.height) |
| 275 | - | rect = rect.Intersect(image.Rect(0, 0, r.width, r.height)) | 324 | + | add := func(rect image.Rectangle, c [3]uint8, content *ports.SurfaceContent, origin image.Point) { |
| 325 | + | rect = rect.Intersect(bounds) | |||
| 276 | if rect.Empty() { | 326 | if rect.Empty() { | ||
| 277 | - | return nil | 327 | + | return |
| 278 | } | 328 | } | ||
| 279 | n := rect.Dx() * rect.Dy() * 4 | 329 | n := rect.Dx() * rect.Dy() * 4 | ||
| 280 | - | if n > r.width*r.height*8-used { | 330 | + | u := upload{rect: rect, offset: used, color: c} |
| 281 | - | return fmt.Errorf("staging buffer full") | 331 | + | if content != nil { |
| 332 | + | u.pixels, u.stride = content.Pixels, content.Stride | |||
| 333 | + | u.sourceX, u.sourceY = rect.Min.X-origin.X, rect.Min.Y-origin.Y | |||
| 282 | } | 334 | } | ||
| 283 | - | data := unsafe.Slice((*byte)(r.stagingMapped), r.width*r.height*8)[used : used+n] | 335 | + | uploads = append(uploads, u) |
| 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 | 336 | used += n | ||
| 289 | - | return nil | |||
| 290 | } | 337 | } | ||
| 291 | for _, w := range s.Windows { | 338 | for _, w := range s.Windows { | ||
| 292 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | 339 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | ||
| 293 | continue | 340 | continue | ||
| 294 | } | 341 | } | ||
| 295 | x, y := w.Rect.X, w.Rect.Y | 342 | 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) | 343 | body := image.Rect(x, y, x+w.Rect.W, y+w.Rect.H) | ||
| 298 | - | if err := add(body, windowColor(w.ID)); err != nil { | 344 | + | content := contents[w.ID] |
| 299 | - | return err | 345 | + | if content.Pixels == nil { |
| 346 | + | add(body, windowColor(w.ID), nil, image.Point{}) | |||
| 347 | + | } else { | |||
| 348 | + | add(body, parseColor(s.Background), nil, image.Point{}) | |||
| 349 | + | width, height := min(content.Width, w.Rect.W), min(content.Height, w.Rect.H) | |||
| 350 | + | if width > 0 && height > 0 && content.Stride >= width*4 && len(content.Pixels) >= (height-1)*content.Stride+width*4 { | |||
| 351 | + | add(image.Rect(x, y, x+width, y+height), [3]uint8{}, &content, image.Pt(x, y)) | |||
| 352 | + | } | |||
| 300 | } | 353 | } | ||
| 301 | if w.Focused { | 354 | 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)} { | 355 | 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 { | 356 | + | add(strip, [3]uint8{255, 255, 255}, nil, image.Point{}) |
| 304 | - | return err | 357 | + | } |
| 358 | + | } | |||
| 359 | + | } | |||
| 360 | + | if err := r.ensureStaging(used); err != nil { | |||
| 361 | + | return err | |||
| 362 | + | } | |||
| 363 | + | data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize) | |||
| 364 | + | for _, u := range uploads { | |||
| 365 | + | dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4] | |||
| 366 | + | for row := 0; row < u.rect.Dy(); row++ { | |||
| 367 | + | line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4] | |||
| 368 | + | if u.pixels != nil { | |||
| 369 | + | start := (u.sourceY+row)*u.stride + u.sourceX*4 | |||
| 370 | + | copy(line, u.pixels[start:start+len(line)]) | |||
| 371 | + | } else { | |||
| 372 | + | for i := 0; i < len(line); i += 4 { | |||
| 373 | + | line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255 | |||
| 305 | } | 374 | } | ||
| 306 | } | 375 | } | ||
| 307 | } | 376 | } | ||
| 308 | } | 377 | } | ||
| 378 | + | // B8G8R8A8 pixels are copied unchanged; blending belongs to the compositing pipeline. | |||
| 309 | rgb := parseColor(s.Background) | 379 | rgb := parseColor(s.Background) | ||
| 310 | d := r.dd | 380 | d := r.dd | ||
| 311 | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil { | 381 | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil { | ||
| @@ -333,7 +403,7 @@ func (r *Renderer) Render(s ports.Scene) error { | |||||
| 333 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | 403 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | ||
| 334 | barrier.NewLayout = vk.ImageLayoutTransferDstOptimal | 404 | barrier.NewLayout = vk.ImageLayoutTransferDstOptimal | ||
| 335 | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | 405 | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | ||
| 336 | - | for _, f := range fills { | 406 | + | for _, f := range uploads { |
| 337 | rect := f.rect | 407 | 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}} | 408 | 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) | 409 | d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, ®ion) | ||
M internal/adapters/vulkan/renderer_test.go +44 -1
| @@ -45,7 +45,7 @@ func TestRendererRender(t *testing.T) { | |||
| 45 | 45 | bg := color.RGBA{16, 32, 48, 255} | |
| 46 | 46 | check := func(s ports.Scene, expected map[image.Point]color.RGBA) { | |
| 47 | 47 | t.Helper() | |
| 48 | - | if err := r.Render(s); err != nil { | |
| 48 | + | if err := r.Render(s, nil); err != nil { | |
| 49 | 49 | t.Fatal(err) | |
| 50 | 50 | } | |
| 51 | 51 | for p, want := range expected { | |
| @@ -67,3 +67,46 @@ func TestRendererRender(t *testing.T) { | |||
| 67 | 67 | s.Windows[0].Hidden = true | |
| 68 | 68 | check(s, map[image.Point]color.RGBA{{5, 5}: bg}) | |
| 69 | 69 | } | |
| 70 | + | ||
| 71 | + | func TestRendererContents(t *testing.T) { | |
| 72 | + | r, err := New(64, 48) | |
| 73 | + | if err != nil { | |
| 74 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 75 | + | } | |
| 76 | + | defer r.Close() | |
| 77 | + | bg := color.RGBA{16, 32, 48, 255} | |
| 78 | + | scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 20, H: 10}}}} | |
| 79 | + | pixels := make([]byte, 4*2*4) | |
| 80 | + | for i := 0; i < len(pixels); i += 4 { | |
| 81 | + | copy(pixels[i:i+4], []byte{0, 0, 255, 255}) | |
| 82 | + | } | |
| 83 | + | render := func(c *ports.SurfaceContent) { | |
| 84 | + | t.Helper() | |
| 85 | + | var contents map[ports.WindowID]ports.SurfaceContent | |
| 86 | + | if c != nil { | |
| 87 | + | contents = map[ports.WindowID]ports.SurfaceContent{1: *c} | |
| 88 | + | } | |
| 89 | + | if err := r.Render(scene, contents); err != nil { | |
| 90 | + | t.Fatal(err) | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | check := func(x, y int, want color.RGBA) { | |
| 94 | + | t.Helper() | |
| 95 | + | if got := r.Pixels().At(x, y); got != want { | |
| 96 | + | t.Errorf("At(%d,%d)=%v want %v", x, y, got, want) | |
| 97 | + | } | |
| 98 | + | } | |
| 99 | + | render(&ports.SurfaceContent{Width: 4, Height: 2, Stride: 16, Pixels: pixels}) | |
| 100 | + | check(8, 8, color.RGBA{255, 0, 0, 255}) | |
| 101 | + | check(11, 9, color.RGBA{255, 0, 0, 255}) | |
| 102 | + | check(12, 8, bg) | |
| 103 | + | check(0, 0, bg) | |
| 104 | + | scene.Windows[0].Rect = ports.Rect{X: -2, Y: 0, W: 10, H: 10} | |
| 105 | + | padded := make([]byte, 4*20) | |
| 106 | + | copy(padded[8:12], []byte{7, 11, 23, 255}) | |
| 107 | + | render(&ports.SurfaceContent{Width: 4, Height: 4, Stride: 20, Pixels: padded}) | |
| 108 | + | check(0, 0, color.RGBA{23, 11, 7, 255}) | |
| 109 | + | render(nil) | |
| 110 | + | c := windowColor(1) | |
| 111 | + | check(0, 0, color.RGBA{c[0], c[1], c[2], 255}) | |
| 112 | + | } | |
| @@ -45,7 +45,7 @@ func TestRendererRender(t *testing.T) { | |||||
| 45 | bg := color.RGBA{16, 32, 48, 255} | 45 | bg := color.RGBA{16, 32, 48, 255} | ||
| 46 | check := func(s ports.Scene, expected map[image.Point]color.RGBA) { | 46 | check := func(s ports.Scene, expected map[image.Point]color.RGBA) { | ||
| 47 | t.Helper() | 47 | t.Helper() | ||
| 48 | - | if err := r.Render(s); err != nil { | 48 | + | if err := r.Render(s, nil); err != nil { |
| 49 | t.Fatal(err) | 49 | t.Fatal(err) | ||
| 50 | } | 50 | } | ||
| 51 | for p, want := range expected { | 51 | for p, want := range expected { | ||
| @@ -67,3 +67,46 @@ func TestRendererRender(t *testing.T) { | |||||
| 67 | s.Windows[0].Hidden = true | 67 | s.Windows[0].Hidden = true | ||
| 68 | check(s, map[image.Point]color.RGBA{{5, 5}: bg}) | 68 | check(s, map[image.Point]color.RGBA{{5, 5}: bg}) | ||
| 69 | } | 69 | } | ||
| 70 | + | ||||
| 71 | + | func TestRendererContents(t *testing.T) { | |||
| 72 | + | r, err := New(64, 48) | |||
| 73 | + | if err != nil { | |||
| 74 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 75 | + | } | |||
| 76 | + | defer r.Close() | |||
| 77 | + | bg := color.RGBA{16, 32, 48, 255} | |||
| 78 | + | scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 20, H: 10}}}} | |||
| 79 | + | pixels := make([]byte, 4*2*4) | |||
| 80 | + | for i := 0; i < len(pixels); i += 4 { | |||
| 81 | + | copy(pixels[i:i+4], []byte{0, 0, 255, 255}) | |||
| 82 | + | } | |||
| 83 | + | render := func(c *ports.SurfaceContent) { | |||
| 84 | + | t.Helper() | |||
| 85 | + | var contents map[ports.WindowID]ports.SurfaceContent | |||
| 86 | + | if c != nil { | |||
| 87 | + | contents = map[ports.WindowID]ports.SurfaceContent{1: *c} | |||
| 88 | + | } | |||
| 89 | + | if err := r.Render(scene, contents); err != nil { | |||
| 90 | + | t.Fatal(err) | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | check := func(x, y int, want color.RGBA) { | |||
| 94 | + | t.Helper() | |||
| 95 | + | if got := r.Pixels().At(x, y); got != want { | |||
| 96 | + | t.Errorf("At(%d,%d)=%v want %v", x, y, got, want) | |||
| 97 | + | } | |||
| 98 | + | } | |||
| 99 | + | render(&ports.SurfaceContent{Width: 4, Height: 2, Stride: 16, Pixels: pixels}) | |||
| 100 | + | check(8, 8, color.RGBA{255, 0, 0, 255}) | |||
| 101 | + | check(11, 9, color.RGBA{255, 0, 0, 255}) | |||
| 102 | + | check(12, 8, bg) | |||
| 103 | + | check(0, 0, bg) | |||
| 104 | + | scene.Windows[0].Rect = ports.Rect{X: -2, Y: 0, W: 10, H: 10} | |||
| 105 | + | padded := make([]byte, 4*20) | |||
| 106 | + | copy(padded[8:12], []byte{7, 11, 23, 255}) | |||
| 107 | + | render(&ports.SurfaceContent{Width: 4, Height: 4, Stride: 20, Pixels: padded}) | |||
| 108 | + | check(0, 0, color.RGBA{23, 11, 7, 255}) | |||
| 109 | + | render(nil) | |||
| 110 | + | c := windowColor(1) | |||
| 111 | + | check(0, 0, color.RGBA{c[0], c[1], c[2], 255}) | |||
| 112 | + | } | |||