detached 4121259

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +91 -48
M internal/adapters/vulkan/blend.go +8 -4
@@ -13,6 +13,10 @@ import (
1313 // and its fragment shader reads the staged pixels straight from the
1414 // staging buffer: no texture, no sampler. Wayland buffers are
1515 // 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.
1620
1721 //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.vert -o shaders/blend.vert.spv
1822 //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.frag -o shaders/blend.frag.spv
@@ -128,11 +132,11 @@ func (r *Renderer) destroyBlend() {
128132 *b = blendPipeline{}
129133 }
130134
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.
133137 func (r *Renderer) bindStaging(tableOffset int) {
134138 infos := []vk.DescriptorBufferInfo{
135- {Buffer: r.staging, Range: vk.DeviceSize(tableOffset)},
139+ {Buffer: r.staging, Range: vk.DeviceSize(max(tableOffset, 4))},
136140 {Buffer: r.staging, Offset: vk.DeviceSize(tableOffset), Range: wholeSize},
137141 }
138142 writes := []vk.WriteDescriptorSet{
@@ -142,7 +146,7 @@ func (r *Renderer) bindStaging(tableOffset int) {
142146 r.dd.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil)
143147 }
144148
145-// putDraw writes draw entry i of the table at data.
149+// putDraw writes draw entry i of table.
146150 func (r *Renderer) putDraw(table []byte, i int, u *upload) {
147151 e := table[i*drawSize : (i+1)*drawSize]
148152 for j, v := range []uint32{
@@ -13,6 +13,10 @@ import (
13 // and its fragment shader reads the staged pixels straight from the13 // and its fragment shader reads the staged pixels straight from the
14 // staging buffer: no texture, no sampler. Wayland buffers are14 // 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.spv21 //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.spv22 //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, the135+// 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 {
5757 fillRegions []vk.BufferImageCopy
5858 // blend draws translucent client pixels (blend.go); align is the
5959 // 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
6263 }
6364
6465 // 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) {
127128 if properties.ApiVersion < vk.MakeVersion(1, 3, 0) {
128129 continue
129130 }
130- r.align = int(max(properties.Limits.MinStorageBufferOffsetAlignment, 4))
131131 var n uint32
132132 r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, nil)
133133 if n == 0 {
@@ -151,6 +151,10 @@ func New(width, height int) (r *Renderer, err error) {
151151 return
152152 }
153153 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)
154158 r.family = family
155159 priority := float32(1)
156160 qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority}
@@ -389,6 +393,14 @@ type upload struct {
389393 fill bool
390394 }
391395
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+
392404 // stage copies the visible pixels of a client buffer into dst, resampling
393405 // when the buffer is not drawn at its size.
394406 func (u *upload) stage(dst []byte) {
@@ -424,7 +436,6 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
424436 phys := func(v int) int { return int(math.Round(float64(v) * scale)) }
425437 physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) }
426438 var uploads []upload
427- used := 0
428439 bounds := image.Rect(0, 0, r.width, r.height)
429440 add := func(rect image.Rectangle, c [3]uint8) {
430441 rect = rect.Intersect(bounds)
@@ -433,8 +444,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
433444 }
434445 // A fill stages a strip of up to fillRows rows; the GPU copies the
435446 // 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})
438448 }
439449 // addContent maps src (buffer pixels) onto dst (physical pixels).
440450 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
456466 if err != nil {
457467 return
458468 }
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})
461470 }
462471 // drawSurface draws one surface buffer with its origin at (x, y)
463472 // logical, clipped to clip (logical).
@@ -574,16 +583,28 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
574583 addPopups(false)
575584 addLayers(true)
576585 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+ }
580595 if u.blend {
596+ u.offset = used
597+ used += u.size()
581598 blends++
582599 }
583600 }
584601 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+ }
587608 }
588609 if err := r.ensureStaging(used); err != nil {
589610 return err
@@ -57,8 +57,9 @@ type Renderer struct {
57 fillRegions []vk.BufferImageCopy57 fillRegions []vk.BufferImageCopy
58 // blend draws translucent client pixels (blend.go); align is the58 // 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 blendPipeline60+ blend blendPipeline
61- align int61+ 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 CPU65 // 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 continue129 continue
129 }130 }
130- r.align = int(max(properties.Limits.MinStorageBufferOffsetAlignment, 4))
131 var n uint32131 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 return151 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 = family158 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 bool393 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, resampling404 // 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 []upload438 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 the445 // 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 return467 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 := 0587+ // 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.align601 tableOffset := (used + r.align - 1) / r.align * r.align
585- if blends > 0 {602+ used = tableOffset + blends*drawSize
586- used = tableOffset + blends*drawSize603+ 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 err610 return err
M internal/adapters/vulkan/renderer_test.go +47 -31
@@ -365,48 +365,64 @@ func TestRendererTallFill(t *testing.T) {
365365 }
366366 }
367367
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.
371380 func TestRendererAlphaBlend(t *testing.T) {
372381 r, err := New(64, 48)
373382 if err != nil {
374383 t.Skipf("Vulkan unavailable: %v", err)
375384 }
376385 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)),
387398 }
388- overlay := shmContent(t, 16, 8, 16*4, half)
389- contents := map[ports.WindowID]ports.SurfaceContent{2: *overlay}
390399 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+ }}
397406 if err := r.Render(scene, contents); err != nil {
398407 t.Fatal(err)
399408 }
400409 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+ }
411427 }
412428 }
@@ -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 (premultiplied368+// fill returns a w×h premultiplied B8G8R8A8 buffer of one pixel.
369-// ARGB); a fully transparent one leaves it untouched. A copy between two369+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 = true404+ {ID: 4, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 8, W: 16, H: 8}},
396- contents[1] = *win405+ }}
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) {
106106 list.DrmFormatModifierProperties = &mods[0]
107107 r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
108108 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.
109111 for _, m := range mods[:list.DrmFormatModifierCount] {
110112 if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) {
111113 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)

Keyboard shortcuts

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

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

Branches 25

Tags 10

↑↓ moveenter openesc close