fix(vulkan): bind only blended pixels within the storage buffer range and test blend paint order
- commit
- 412125902ba2f37289ca7be2061ed534aa31fd5c
- author
- brice · · 2026-09-26 09:26 UTC
- parent
- e28c9e7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +91 -48
M internal/adapters/vulkan/blend.go +8 -4
| @@ -13,6 +13,10 @@ import ( | |||
| 13 | 13 | // and its fragment shader reads the staged pixels straight from the | |
| 14 | 14 | // staging buffer: no texture, no sampler. Wayland buffers are | |
| 15 | 15 | // premultiplied, hence ONE, ONE_MINUS_SRC_ALPHA. | |
| 16 | + | // | |
| 17 | + | // Limits: dmabuf buffers are still blitted, alpha ignored (blending them | |
| 18 | + | // needs a sampled-image variant). The opaque region is not honoured, so | |
| 19 | + | // an ARGB buffer blends even where it is opaque. | |
| 16 | 20 | ||
| 17 | 21 | //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.vert -o shaders/blend.vert.spv | |
| 18 | 22 | //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.frag -o shaders/blend.frag.spv | |
| @@ -128,11 +132,11 @@ func (r *Renderer) destroyBlend() { | |||
| 128 | 132 | *b = blendPipeline{} | |
| 129 | 133 | } | |
| 130 | 134 | ||
| 131 | - | // bindStaging points the set at the staging buffer: pixels from 0, the | |
| 132 | - | // draw table from tableOffset. | |
| 135 | + | // bindStaging points the set at the staging buffer: blended pixels in | |
| 136 | + | // [0, tableOffset), the draw table from tableOffset. | |
| 133 | 137 | func (r *Renderer) bindStaging(tableOffset int) { | |
| 134 | 138 | infos := []vk.DescriptorBufferInfo{ | |
| 135 | - | {Buffer: r.staging, Range: vk.DeviceSize(tableOffset)}, | |
| 139 | + | {Buffer: r.staging, Range: vk.DeviceSize(max(tableOffset, 4))}, | |
| 136 | 140 | {Buffer: r.staging, Offset: vk.DeviceSize(tableOffset), Range: wholeSize}, | |
| 137 | 141 | } | |
| 138 | 142 | writes := []vk.WriteDescriptorSet{ | |
| @@ -142,7 +146,7 @@ func (r *Renderer) bindStaging(tableOffset int) { | |||
| 142 | 146 | r.dd.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil) | |
| 143 | 147 | } | |
| 144 | 148 | ||
| 145 | - | // putDraw writes draw entry i of the table at data. | |
| 149 | + | // putDraw writes draw entry i of table. | |
| 146 | 150 | func (r *Renderer) putDraw(table []byte, i int, u *upload) { | |
| 147 | 151 | e := table[i*drawSize : (i+1)*drawSize] | |
| 148 | 152 | for j, v := range []uint32{ | |
| @@ -13,6 +13,10 @@ import ( | |||||
| 13 | // and its fragment shader reads the staged pixels straight from the | 13 | // and its fragment shader reads the staged pixels straight from the | ||
| 14 | // staging buffer: no texture, no sampler. Wayland buffers are | 14 | // staging buffer: no texture, no sampler. Wayland buffers are | ||
| 15 | // premultiplied, hence ONE, ONE_MINUS_SRC_ALPHA. | 15 | // premultiplied, hence ONE, ONE_MINUS_SRC_ALPHA. | ||
| 16 | + | // | |||
| 17 | + | // Limits: dmabuf buffers are still blitted, alpha ignored (blending them | |||
| 18 | + | // needs a sampled-image variant). The opaque region is not honoured, so | |||
| 19 | + | // an ARGB buffer blends even where it is opaque. | |||
| 16 | 20 | ||||
| 17 | //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.vert -o shaders/blend.vert.spv | 21 | //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.vert -o shaders/blend.vert.spv | ||
| 18 | //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.frag -o shaders/blend.frag.spv | 22 | //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.frag -o shaders/blend.frag.spv | ||
| @@ -128,11 +132,11 @@ func (r *Renderer) destroyBlend() { | |||||
| 128 | *b = blendPipeline{} | 132 | *b = blendPipeline{} | ||
| 129 | } | 133 | } | ||
| 130 | 134 | ||||
| 131 | - | // bindStaging points the set at the staging buffer: pixels from 0, the | 135 | + | // bindStaging points the set at the staging buffer: blended pixels in |
| 132 | - | // draw table from tableOffset. | 136 | + | // [0, tableOffset), the draw table from tableOffset. |
| 133 | func (r *Renderer) bindStaging(tableOffset int) { | 137 | func (r *Renderer) bindStaging(tableOffset int) { | ||
| 134 | infos := []vk.DescriptorBufferInfo{ | 138 | infos := []vk.DescriptorBufferInfo{ | ||
| 135 | - | {Buffer: r.staging, Range: vk.DeviceSize(tableOffset)}, | 139 | + | {Buffer: r.staging, Range: vk.DeviceSize(max(tableOffset, 4))}, |
| 136 | {Buffer: r.staging, Offset: vk.DeviceSize(tableOffset), Range: wholeSize}, | 140 | {Buffer: r.staging, Offset: vk.DeviceSize(tableOffset), Range: wholeSize}, | ||
| 137 | } | 141 | } | ||
| 138 | writes := []vk.WriteDescriptorSet{ | 142 | writes := []vk.WriteDescriptorSet{ | ||
| @@ -142,7 +146,7 @@ func (r *Renderer) bindStaging(tableOffset int) { | |||||
| 142 | r.dd.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil) | 146 | r.dd.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil) | ||
| 143 | } | 147 | } | ||
| 144 | 148 | ||||
| 145 | - | // putDraw writes draw entry i of the table at data. | 149 | + | // putDraw writes draw entry i of table. |
| 146 | func (r *Renderer) putDraw(table []byte, i int, u *upload) { | 150 | func (r *Renderer) putDraw(table []byte, i int, u *upload) { | ||
| 147 | e := table[i*drawSize : (i+1)*drawSize] | 151 | e := table[i*drawSize : (i+1)*drawSize] | ||
| 148 | for j, v := range []uint32{ | 152 | for j, v := range []uint32{ | ||
M internal/adapters/vulkan/renderer.go +34 -13
| @@ -57,8 +57,9 @@ type Renderer struct { | |||
| 57 | 57 | fillRegions []vk.BufferImageCopy | |
| 58 | 58 | // blend draws translucent client pixels (blend.go); align is the | |
| 59 | 59 | // device's storage buffer offset alignment. | |
| 60 | - | blend blendPipeline | |
| 61 | - | align int | |
| 60 | + | blend blendPipeline | |
| 61 | + | align int | |
| 62 | + | maxRange int // bytes of blended pixels one frame may bind | |
| 62 | 63 | } | |
| 63 | 64 | ||
| 64 | 65 | // fillRows is the height of the strip staged for a solid fill: the CPU | |
| @@ -127,7 +128,6 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 127 | 128 | if properties.ApiVersion < vk.MakeVersion(1, 3, 0) { | |
| 128 | 129 | continue | |
| 129 | 130 | } | |
| 130 | - | r.align = int(max(properties.Limits.MinStorageBufferOffsetAlignment, 4)) | |
| 131 | 131 | var n uint32 | |
| 132 | 132 | r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, nil) | |
| 133 | 133 | if n == 0 { | |
| @@ -151,6 +151,10 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 151 | 151 | return | |
| 152 | 152 | } | |
| 153 | 153 | r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory) | |
| 154 | + | var properties vk.PhysicalDeviceProperties | |
| 155 | + | r.id.GetPhysicalDeviceProperties(physical, &properties) | |
| 156 | + | r.align = int(max(properties.Limits.MinStorageBufferOffsetAlignment, 4)) | |
| 157 | + | r.maxRange = int(properties.Limits.MaxStorageBufferRange) | |
| 154 | 158 | r.family = family | |
| 155 | 159 | priority := float32(1) | |
| 156 | 160 | qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority} | |
| @@ -389,6 +393,14 @@ type upload struct { | |||
| 389 | 393 | fill bool | |
| 390 | 394 | } | |
| 391 | 395 | ||
| 396 | + | // size is the staging bytes of an upload: a strip for a fill. | |
| 397 | + | func (u *upload) size() int { | |
| 398 | + | if u.fill { | |
| 399 | + | return u.rect.Dx() * min(u.rect.Dy(), fillRows) * 4 | |
| 400 | + | } | |
| 401 | + | return u.rect.Dx() * u.rect.Dy() * 4 | |
| 402 | + | } | |
| 403 | + | ||
| 392 | 404 | // stage copies the visible pixels of a client buffer into dst, resampling | |
| 393 | 405 | // when the buffer is not drawn at its size. | |
| 394 | 406 | func (u *upload) stage(dst []byte) { | |
| @@ -424,7 +436,6 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 424 | 436 | phys := func(v int) int { return int(math.Round(float64(v) * scale)) } | |
| 425 | 437 | physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) } | |
| 426 | 438 | var uploads []upload | |
| 427 | - | used := 0 | |
| 428 | 439 | bounds := image.Rect(0, 0, r.width, r.height) | |
| 429 | 440 | add := func(rect image.Rectangle, c [3]uint8) { | |
| 430 | 441 | rect = rect.Intersect(bounds) | |
| @@ -433,8 +444,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 433 | 444 | } | |
| 434 | 445 | // A fill stages a strip of up to fillRows rows; the GPU copies the | |
| 435 | 446 | // strip down the rect. | |
| 436 | - | uploads = append(uploads, upload{rect: rect, offset: used, color: c, fill: true}) | |
| 437 | - | used += rect.Dx() * min(rect.Dy(), fillRows) * 4 | |
| 447 | + | uploads = append(uploads, upload{rect: rect, color: c, fill: true}) | |
| 438 | 448 | } | |
| 439 | 449 | // addContent maps src (buffer pixels) onto dst (physical pixels). | |
| 440 | 450 | addContent := func(dst, src image.Rectangle, content *ports.SurfaceContent) { | |
| @@ -456,8 +466,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 456 | 466 | if err != nil { | |
| 457 | 467 | return | |
| 458 | 468 | } | |
| 459 | - | uploads = append(uploads, upload{rect: rect, offset: used, pixels: pixels[b.Offset:], stride: b.Stride, opaque: content.Opaque, blend: !content.Opaque, dst: dst, src: src}) | |
| 460 | - | used += rect.Dx() * rect.Dy() * 4 | |
| 469 | + | uploads = append(uploads, upload{rect: rect, pixels: pixels[b.Offset:], stride: b.Stride, opaque: content.Opaque, blend: !content.Opaque, dst: dst, src: src}) | |
| 461 | 470 | } | |
| 462 | 471 | // drawSurface draws one surface buffer with its origin at (x, y) | |
| 463 | 472 | // logical, clipped to clip (logical). | |
| @@ -574,16 +583,28 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 574 | 583 | addPopups(false) | |
| 575 | 584 | addLayers(true) | |
| 576 | 585 | addPopups(true) | |
| 577 | - | // The draw table of blended uploads follows the pixels. | |
| 578 | - | blends := 0 | |
| 579 | - | for _, u := range uploads { | |
| 586 | + | // Blended pixels are staged first, so the shaders bind only them, | |
| 587 | + | // within the device's storage buffer range; past it they are copied. | |
| 588 | + | // The draw table follows them, then the other uploads. | |
| 589 | + | blends, used := 0, 0 | |
| 590 | + | for k := range uploads { | |
| 591 | + | u := &uploads[k] | |
| 592 | + | if u.blend && used+u.size() > r.maxRange { | |
| 593 | + | u.blend = false | |
| 594 | + | } | |
| 580 | 595 | if u.blend { | |
| 596 | + | u.offset = used | |
| 597 | + | used += u.size() | |
| 581 | 598 | blends++ | |
| 582 | 599 | } | |
| 583 | 600 | } | |
| 584 | 601 | tableOffset := (used + r.align - 1) / r.align * r.align | |
| 585 | - | if blends > 0 { | |
| 586 | - | used = tableOffset + blends*drawSize | |
| 602 | + | used = tableOffset + blends*drawSize | |
| 603 | + | for k := range uploads { | |
| 604 | + | if u := &uploads[k]; !u.blend && u.dma == nil { | |
| 605 | + | u.offset = used | |
| 606 | + | used += u.size() | |
| 607 | + | } | |
| 587 | 608 | } | |
| 588 | 609 | if err := r.ensureStaging(used); err != nil { | |
| 589 | 610 | return err | |
| @@ -57,8 +57,9 @@ type Renderer struct { | |||||
| 57 | fillRegions []vk.BufferImageCopy | 57 | fillRegions []vk.BufferImageCopy | ||
| 58 | // blend draws translucent client pixels (blend.go); align is the | 58 | // blend draws translucent client pixels (blend.go); align is the | ||
| 59 | // device's storage buffer offset alignment. | 59 | // device's storage buffer offset alignment. | ||
| 60 | - | blend blendPipeline | 60 | + | blend blendPipeline |
| 61 | - | align int | 61 | + | align int |
| 62 | + | maxRange int // bytes of blended pixels one frame may bind | |||
| 62 | } | 63 | } | ||
| 63 | 64 | ||||
| 64 | // fillRows is the height of the strip staged for a solid fill: the CPU | 65 | // fillRows is the height of the strip staged for a solid fill: the CPU | ||
| @@ -127,7 +128,6 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 127 | if properties.ApiVersion < vk.MakeVersion(1, 3, 0) { | 128 | if properties.ApiVersion < vk.MakeVersion(1, 3, 0) { | ||
| 128 | continue | 129 | continue | ||
| 129 | } | 130 | } | ||
| 130 | - | r.align = int(max(properties.Limits.MinStorageBufferOffsetAlignment, 4)) | |||
| 131 | var n uint32 | 131 | var n uint32 | ||
| 132 | r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, nil) | 132 | r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, nil) | ||
| 133 | if n == 0 { | 133 | if n == 0 { | ||
| @@ -151,6 +151,10 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 151 | return | 151 | return | ||
| 152 | } | 152 | } | ||
| 153 | r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory) | 153 | r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory) | ||
| 154 | + | var properties vk.PhysicalDeviceProperties | |||
| 155 | + | r.id.GetPhysicalDeviceProperties(physical, &properties) | |||
| 156 | + | r.align = int(max(properties.Limits.MinStorageBufferOffsetAlignment, 4)) | |||
| 157 | + | r.maxRange = int(properties.Limits.MaxStorageBufferRange) | |||
| 154 | r.family = family | 158 | r.family = family | ||
| 155 | priority := float32(1) | 159 | priority := float32(1) | ||
| 156 | qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority} | 160 | qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority} | ||
| @@ -389,6 +393,14 @@ type upload struct { | |||||
| 389 | fill bool | 393 | fill bool | ||
| 390 | } | 394 | } | ||
| 391 | 395 | ||||
| 396 | + | // size is the staging bytes of an upload: a strip for a fill. | |||
| 397 | + | func (u *upload) size() int { | |||
| 398 | + | if u.fill { | |||
| 399 | + | return u.rect.Dx() * min(u.rect.Dy(), fillRows) * 4 | |||
| 400 | + | } | |||
| 401 | + | return u.rect.Dx() * u.rect.Dy() * 4 | |||
| 402 | + | } | |||
| 403 | + | ||||
| 392 | // stage copies the visible pixels of a client buffer into dst, resampling | 404 | // stage copies the visible pixels of a client buffer into dst, resampling | ||
| 393 | // when the buffer is not drawn at its size. | 405 | // when the buffer is not drawn at its size. | ||
| 394 | func (u *upload) stage(dst []byte) { | 406 | func (u *upload) stage(dst []byte) { | ||
| @@ -424,7 +436,6 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 424 | phys := func(v int) int { return int(math.Round(float64(v) * scale)) } | 436 | phys := func(v int) int { return int(math.Round(float64(v) * scale)) } | ||
| 425 | physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) } | 437 | physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) } | ||
| 426 | var uploads []upload | 438 | var uploads []upload | ||
| 427 | - | used := 0 | |||
| 428 | bounds := image.Rect(0, 0, r.width, r.height) | 439 | bounds := image.Rect(0, 0, r.width, r.height) | ||
| 429 | add := func(rect image.Rectangle, c [3]uint8) { | 440 | add := func(rect image.Rectangle, c [3]uint8) { | ||
| 430 | rect = rect.Intersect(bounds) | 441 | rect = rect.Intersect(bounds) | ||
| @@ -433,8 +444,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 433 | } | 444 | } | ||
| 434 | // A fill stages a strip of up to fillRows rows; the GPU copies the | 445 | // A fill stages a strip of up to fillRows rows; the GPU copies the | ||
| 435 | // strip down the rect. | 446 | // strip down the rect. | ||
| 436 | - | uploads = append(uploads, upload{rect: rect, offset: used, color: c, fill: true}) | 447 | + | uploads = append(uploads, upload{rect: rect, color: c, fill: true}) |
| 437 | - | used += rect.Dx() * min(rect.Dy(), fillRows) * 4 | |||
| 438 | } | 448 | } | ||
| 439 | // addContent maps src (buffer pixels) onto dst (physical pixels). | 449 | // addContent maps src (buffer pixels) onto dst (physical pixels). | ||
| 440 | addContent := func(dst, src image.Rectangle, content *ports.SurfaceContent) { | 450 | addContent := func(dst, src image.Rectangle, content *ports.SurfaceContent) { | ||
| @@ -456,8 +466,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 456 | if err != nil { | 466 | if err != nil { | ||
| 457 | return | 467 | return | ||
| 458 | } | 468 | } | ||
| 459 | - | uploads = append(uploads, upload{rect: rect, offset: used, pixels: pixels[b.Offset:], stride: b.Stride, opaque: content.Opaque, blend: !content.Opaque, dst: dst, src: src}) | 469 | + | uploads = append(uploads, upload{rect: rect, pixels: pixels[b.Offset:], stride: b.Stride, opaque: content.Opaque, blend: !content.Opaque, dst: dst, src: src}) |
| 460 | - | used += rect.Dx() * rect.Dy() * 4 | |||
| 461 | } | 470 | } | ||
| 462 | // drawSurface draws one surface buffer with its origin at (x, y) | 471 | // drawSurface draws one surface buffer with its origin at (x, y) | ||
| 463 | // logical, clipped to clip (logical). | 472 | // logical, clipped to clip (logical). | ||
| @@ -574,16 +583,28 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 574 | addPopups(false) | 583 | addPopups(false) | ||
| 575 | addLayers(true) | 584 | addLayers(true) | ||
| 576 | addPopups(true) | 585 | addPopups(true) | ||
| 577 | - | // The draw table of blended uploads follows the pixels. | 586 | + | // Blended pixels are staged first, so the shaders bind only them, |
| 578 | - | blends := 0 | 587 | + | // within the device's storage buffer range; past it they are copied. |
| 579 | - | for _, u := range uploads { | 588 | + | // The draw table follows them, then the other uploads. |
| 589 | + | blends, used := 0, 0 | |||
| 590 | + | for k := range uploads { | |||
| 591 | + | u := &uploads[k] | |||
| 592 | + | if u.blend && used+u.size() > r.maxRange { | |||
| 593 | + | u.blend = false | |||
| 594 | + | } | |||
| 580 | if u.blend { | 595 | if u.blend { | ||
| 596 | + | u.offset = used | |||
| 597 | + | used += u.size() | |||
| 581 | blends++ | 598 | blends++ | ||
| 582 | } | 599 | } | ||
| 583 | } | 600 | } | ||
| 584 | tableOffset := (used + r.align - 1) / r.align * r.align | 601 | tableOffset := (used + r.align - 1) / r.align * r.align | ||
| 585 | - | if blends > 0 { | 602 | + | used = tableOffset + blends*drawSize |
| 586 | - | used = tableOffset + blends*drawSize | 603 | + | for k := range uploads { |
| 604 | + | if u := &uploads[k]; !u.blend && u.dma == nil { | |||
| 605 | + | u.offset = used | |||
| 606 | + | used += u.size() | |||
| 607 | + | } | |||
| 587 | } | 608 | } | ||
| 588 | if err := r.ensureStaging(used); err != nil { | 609 | if err := r.ensureStaging(used); err != nil { | ||
| 589 | return err | 610 | return err | ||
M internal/adapters/vulkan/renderer_test.go +47 -31
| @@ -365,48 +365,64 @@ func TestRendererTallFill(t *testing.T) { | |||
| 365 | 365 | } | |
| 366 | 366 | } | |
| 367 | 367 | ||
| 368 | - | // A translucent overlay layer blends over the window below it (premultiplied | |
| 369 | - | // ARGB); a fully transparent one leaves it untouched. A copy between two | |
| 370 | - | // blended draws keeps the paint order. | |
| 368 | + | // fill returns a w×h premultiplied B8G8R8A8 buffer of one pixel. | |
| 369 | + | func fill(w, h int, bgra [4]byte) []byte { | |
| 370 | + | p := make([]byte, w*h*4) | |
| 371 | + | for i := 0; i < len(p); i += 4 { | |
| 372 | + | copy(p[i:i+4], bgra[:]) | |
| 373 | + | } | |
| 374 | + | return p | |
| 375 | + | } | |
| 376 | + | ||
| 377 | + | // Translucent buffers blend over what is below them, in paint order: a | |
| 378 | + | // bottom layer (one draw), an opaque window copied over it, then two | |
| 379 | + | // overlapping overlays sharing one draw. A transparent pixel changes nothing. | |
| 371 | 380 | func TestRendererAlphaBlend(t *testing.T) { | |
| 372 | 381 | r, err := New(64, 48) | |
| 373 | 382 | if err != nil { | |
| 374 | 383 | t.Skipf("Vulkan unavailable: %v", err) | |
| 375 | 384 | } | |
| 376 | 385 | defer r.Close() | |
| 377 | - | // Half-transparent red, premultiplied: B,G,R,A = 0,0,128,128. | |
| 378 | - | half := make([]byte, 16*8*4) | |
| 379 | - | for i := 0; i < 8*16*4; i += 4 { | |
| 380 | - | copy(half[i:i+4], []byte{0, 0, 128, 128}) | |
| 381 | - | } | |
| 382 | - | // The right half of the buffer is fully transparent. | |
| 383 | - | for y := 0; y < 8; y++ { | |
| 384 | - | for x := 8; x < 16; x++ { | |
| 385 | - | copy(half[(y*16+x)*4:], []byte{0, 0, 0, 0}) | |
| 386 | - | } | |
| 386 | + | halfRed, halfBlue := [4]byte{0, 0, 128, 128}, [4]byte{128, 0, 0, 128} | |
| 387 | + | clear := fill(16, 8, halfRed) | |
| 388 | + | for y := range 8 { | |
| 389 | + | copy(clear[y*64+48:y*64+64], make([]byte, 16)) // last 4 pixels: transparent | |
| 390 | + | } | |
| 391 | + | white := shmContent(t, 32, 48, 32*4, solid(32, 48, [3]uint8{255, 255, 255})) | |
| 392 | + | white.Opaque = true | |
| 393 | + | contents := map[ports.WindowID]ports.SurfaceContent{ | |
| 394 | + | 1: *white, | |
| 395 | + | 2: *shmContent(t, 64, 48, 64*4, fill(64, 48, halfRed)), | |
| 396 | + | 3: *shmContent(t, 16, 8, 16*4, clear), | |
| 397 | + | 4: *shmContent(t, 16, 8, 16*4, fill(16, 8, halfBlue)), | |
| 387 | 398 | } | |
| 388 | - | overlay := shmContent(t, 16, 8, 16*4, half) | |
| 389 | - | contents := map[ports.WindowID]ports.SurfaceContent{2: *overlay} | |
| 390 | 399 | scene := ports.Scene{Background: "#000000", | |
| 391 | - | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}, Borderless: true}}, | |
| 392 | - | Layers: []ports.SceneLayer{{ID: 2, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 4, Y: 4, W: 16, H: 8}}}} | |
| 393 | - | white := solid(64, 48, [3]uint8{255, 255, 255}) | |
| 394 | - | win := shmContent(t, 64, 48, 64*4, white) | |
| 395 | - | win.Opaque = true | |
| 396 | - | contents[1] = *win | |
| 400 | + | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 32, H: 48}, Borderless: true}}, | |
| 401 | + | Layers: []ports.SceneLayer{ | |
| 402 | + | {ID: 2, Layer: ports.LayerBottom, Rect: ports.Rect{W: 64, H: 48}}, | |
| 403 | + | {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}}, | |
| 404 | + | {ID: 4, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 8, W: 16, H: 8}}, | |
| 405 | + | }} | |
| 397 | 406 | if err := r.Render(scene, contents); err != nil { | |
| 398 | 407 | t.Fatal(err) | |
| 399 | 408 | } | |
| 400 | 409 | px := r.Pixels() | |
| 401 | - | // White under half red: R = 128 + 255*127/255 = 255, G = B = 127. | |
| 402 | - | near := func(a, b uint8) bool { return a-b <= 1 || b-a <= 1 } | |
| 403 | - | if got := px.RGBAAt(6, 6); !near(got.R, 255) || !near(got.G, 127) || !near(got.B, 127) { | |
| 404 | - | t.Errorf("blended pixel = %v, want ~{255 127 127}", got) | |
| 405 | - | } | |
| 406 | - | if got := px.RGBAAt(14, 6); got != (color.RGBA{255, 255, 255, 255}) { | |
| 407 | - | t.Errorf("transparent pixel = %v, want white", got) | |
| 408 | - | } | |
| 409 | - | if got := px.RGBAAt(40, 30); got != (color.RGBA{255, 255, 255, 255}) { | |
| 410 | - | t.Errorf("uncovered pixel = %v, want white", got) | |
| 410 | + | near := func(a, b uint8) bool { d := int(a) - int(b); return d >= -1 && d <= 1 } | |
| 411 | + | for _, tc := range []struct { | |
| 412 | + | name string | |
| 413 | + | x, y int | |
| 414 | + | r, g, b uint8 | |
| 415 | + | }{ | |
| 416 | + | {"bottom layer over background", 40, 20, 128, 0, 0}, | |
| 417 | + | {"opaque window over bottom layer", 20, 20, 255, 255, 255}, | |
| 418 | + | {"red overlay over window", 4, 4, 255, 127, 127}, | |
| 419 | + | {"blue over red over window", 10, 4, 127, 63, 191}, | |
| 420 | + | {"blue over window", 20, 4, 127, 127, 255}, | |
| 421 | + | {"transparent red pixels: blue over window only", 14, 4, 127, 127, 255}, | |
| 422 | + | } { | |
| 423 | + | got := px.RGBAAt(tc.x, tc.y) | |
| 424 | + | if !near(got.R, tc.r) || !near(got.G, tc.g) || !near(got.B, tc.b) { | |
| 425 | + | t.Errorf("%s: pixel (%d,%d) = %v, want ~{%d %d %d}", tc.name, tc.x, tc.y, got, tc.r, tc.g, tc.b) | |
| 426 | + | } | |
| 411 | 427 | } | |
| 412 | 428 | } | |
| @@ -365,48 +365,64 @@ func TestRendererTallFill(t *testing.T) { | |||||
| 365 | } | 365 | } | ||
| 366 | } | 366 | } | ||
| 367 | 367 | ||||
| 368 | - | // A translucent overlay layer blends over the window below it (premultiplied | 368 | + | // fill returns a w×h premultiplied B8G8R8A8 buffer of one pixel. |
| 369 | - | // ARGB); a fully transparent one leaves it untouched. A copy between two | 369 | + | func fill(w, h int, bgra [4]byte) []byte { |
| 370 | - | // blended draws keeps the paint order. | 370 | + | p := make([]byte, w*h*4) |
| 371 | + | for i := 0; i < len(p); i += 4 { | |||
| 372 | + | copy(p[i:i+4], bgra[:]) | |||
| 373 | + | } | |||
| 374 | + | return p | |||
| 375 | + | } | |||
| 376 | + | ||||
| 377 | + | // Translucent buffers blend over what is below them, in paint order: a | |||
| 378 | + | // bottom layer (one draw), an opaque window copied over it, then two | |||
| 379 | + | // overlapping overlays sharing one draw. A transparent pixel changes nothing. | |||
| 371 | func TestRendererAlphaBlend(t *testing.T) { | 380 | func TestRendererAlphaBlend(t *testing.T) { | ||
| 372 | r, err := New(64, 48) | 381 | r, err := New(64, 48) | ||
| 373 | if err != nil { | 382 | if err != nil { | ||
| 374 | t.Skipf("Vulkan unavailable: %v", err) | 383 | t.Skipf("Vulkan unavailable: %v", err) | ||
| 375 | } | 384 | } | ||
| 376 | defer r.Close() | 385 | defer r.Close() | ||
| 377 | - | // Half-transparent red, premultiplied: B,G,R,A = 0,0,128,128. | 386 | + | halfRed, halfBlue := [4]byte{0, 0, 128, 128}, [4]byte{128, 0, 0, 128} |
| 378 | - | half := make([]byte, 16*8*4) | 387 | + | clear := fill(16, 8, halfRed) |
| 379 | - | for i := 0; i < 8*16*4; i += 4 { | 388 | + | for y := range 8 { |
| 380 | - | copy(half[i:i+4], []byte{0, 0, 128, 128}) | 389 | + | copy(clear[y*64+48:y*64+64], make([]byte, 16)) // last 4 pixels: transparent |
| 381 | - | } | 390 | + | } |
| 382 | - | // The right half of the buffer is fully transparent. | 391 | + | white := shmContent(t, 32, 48, 32*4, solid(32, 48, [3]uint8{255, 255, 255})) |
| 383 | - | for y := 0; y < 8; y++ { | 392 | + | white.Opaque = true |
| 384 | - | for x := 8; x < 16; x++ { | 393 | + | contents := map[ports.WindowID]ports.SurfaceContent{ |
| 385 | - | copy(half[(y*16+x)*4:], []byte{0, 0, 0, 0}) | 394 | + | 1: *white, |
| 386 | - | } | 395 | + | 2: *shmContent(t, 64, 48, 64*4, fill(64, 48, halfRed)), |
| 396 | + | 3: *shmContent(t, 16, 8, 16*4, clear), | |||
| 397 | + | 4: *shmContent(t, 16, 8, 16*4, fill(16, 8, halfBlue)), | |||
| 387 | } | 398 | } | ||
| 388 | - | overlay := shmContent(t, 16, 8, 16*4, half) | |||
| 389 | - | contents := map[ports.WindowID]ports.SurfaceContent{2: *overlay} | |||
| 390 | scene := ports.Scene{Background: "#000000", | 399 | scene := ports.Scene{Background: "#000000", | ||
| 391 | - | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}, Borderless: true}}, | 400 | + | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 32, H: 48}, Borderless: true}}, |
| 392 | - | Layers: []ports.SceneLayer{{ID: 2, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 4, Y: 4, W: 16, H: 8}}}} | 401 | + | Layers: []ports.SceneLayer{ |
| 393 | - | white := solid(64, 48, [3]uint8{255, 255, 255}) | 402 | + | {ID: 2, Layer: ports.LayerBottom, Rect: ports.Rect{W: 64, H: 48}}, |
| 394 | - | win := shmContent(t, 64, 48, 64*4, white) | 403 | + | {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}}, |
| 395 | - | win.Opaque = true | 404 | + | {ID: 4, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 8, W: 16, H: 8}}, |
| 396 | - | contents[1] = *win | 405 | + | }} |
| 397 | if err := r.Render(scene, contents); err != nil { | 406 | if err := r.Render(scene, contents); err != nil { | ||
| 398 | t.Fatal(err) | 407 | t.Fatal(err) | ||
| 399 | } | 408 | } | ||
| 400 | px := r.Pixels() | 409 | px := r.Pixels() | ||
| 401 | - | // White under half red: R = 128 + 255*127/255 = 255, G = B = 127. | 410 | + | near := func(a, b uint8) bool { d := int(a) - int(b); return d >= -1 && d <= 1 } |
| 402 | - | near := func(a, b uint8) bool { return a-b <= 1 || b-a <= 1 } | 411 | + | for _, tc := range []struct { |
| 403 | - | if got := px.RGBAAt(6, 6); !near(got.R, 255) || !near(got.G, 127) || !near(got.B, 127) { | 412 | + | name string |
| 404 | - | t.Errorf("blended pixel = %v, want ~{255 127 127}", got) | 413 | + | x, y int |
| 405 | - | } | 414 | + | r, g, b uint8 |
| 406 | - | if got := px.RGBAAt(14, 6); got != (color.RGBA{255, 255, 255, 255}) { | 415 | + | }{ |
| 407 | - | t.Errorf("transparent pixel = %v, want white", got) | 416 | + | {"bottom layer over background", 40, 20, 128, 0, 0}, |
| 408 | - | } | 417 | + | {"opaque window over bottom layer", 20, 20, 255, 255, 255}, |
| 409 | - | if got := px.RGBAAt(40, 30); got != (color.RGBA{255, 255, 255, 255}) { | 418 | + | {"red overlay over window", 4, 4, 255, 127, 127}, |
| 410 | - | t.Errorf("uncovered pixel = %v, want white", got) | 419 | + | {"blue over red over window", 10, 4, 127, 63, 191}, |
| 420 | + | {"blue over window", 20, 4, 127, 127, 255}, | |||
| 421 | + | {"transparent red pixels: blue over window only", 14, 4, 127, 127, 255}, | |||
| 422 | + | } { | |||
| 423 | + | got := px.RGBAAt(tc.x, tc.y) | |||
| 424 | + | if !near(got.R, tc.r) || !near(got.G, tc.g) || !near(got.B, tc.b) { | |||
| 425 | + | t.Errorf("%s: pixel (%d,%d) = %v, want ~{%d %d %d}", tc.name, tc.x, tc.y, got, tc.r, tc.g, tc.b) | |||
| 426 | + | } | |||
| 411 | } | 427 | } | ||
| 412 | } | 428 | } | ||
M internal/adapters/vulkan/targets.go +2 -0
| @@ -106,6 +106,8 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) { | |||
| 106 | 106 | list.DrmFormatModifierProperties = &mods[0] | |
| 107 | 107 | r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp) | |
| 108 | 108 | need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit) | |
| 109 | + | // Targets are drawn on too (blend.go): a transfer-only modifier is | |
| 110 | + | // not a target, and without any the display falls back to readback. | |
| 109 | 111 | for _, m := range mods[:list.DrmFormatModifierCount] { | |
| 110 | 112 | if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) { | |
| 111 | 113 | r.renderMods = append(r.renderMods, m.DrmFormatModifier) | |
| @@ -106,6 +106,8 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) { | |||||
| 106 | list.DrmFormatModifierProperties = &mods[0] | 106 | list.DrmFormatModifierProperties = &mods[0] | ||
| 107 | r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp) | 107 | r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp) | ||
| 108 | need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit) | 108 | need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit) | ||
| 109 | + | // Targets are drawn on too (blend.go): a transfer-only modifier is | |||
| 110 | + | // not a target, and without any the display falls back to readback. | |||
| 109 | for _, m := range mods[:list.DrmFormatModifierCount] { | 111 | for _, m := range mods[:list.DrmFormatModifierCount] { | ||
| 110 | if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) { | 112 | if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) { | ||
| 111 | r.renderMods = append(r.renderMods, m.DrmFormatModifier) | 113 | r.renderMods = append(r.renderMods, m.DrmFormatModifier) | ||