detached e28c9e7

feat(vulkan): blend translucent client buffers with a premultiplied-alpha pipeline

commit
e28c9e7c8e320fc7ec0a420c6e86bc0180164255
author
brice · · 2026-09-26 09:19 UTC
parent
cc9b1b3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +347 -10
M go.mod +1 -1
@@ -5,7 +5,7 @@ go 1.27
55 require (
66 github.com/bnema/purego v0.11.0-bnema.4
77 github.com/bnema/purego-libwayland v0.1.1
8- github.com/bnema/purego-vulkan v0.4.0
8+ github.com/bnema/purego-vulkan v0.4.1
99 github.com/bnema/wlturbo v0.2.0
1010 github.com/bnema/zerowrap v1.4.1
1111 github.com/rs/zerolog v1.35.1
@@ -5,7 +5,7 @@ go 1.27
5 require (5 require (
6 github.com/bnema/purego v0.11.0-bnema.46 github.com/bnema/purego v0.11.0-bnema.4
7 github.com/bnema/purego-libwayland v0.1.17 github.com/bnema/purego-libwayland v0.1.1
8- github.com/bnema/purego-vulkan v0.4.08+ github.com/bnema/purego-vulkan v0.4.1
9 github.com/bnema/wlturbo v0.2.09 github.com/bnema/wlturbo v0.2.0
10 github.com/bnema/zerowrap v1.4.110 github.com/bnema/zerowrap v1.4.1
11 github.com/rs/zerolog v1.35.111 github.com/rs/zerolog v1.35.1
M go.sum +2 -0 generated file: collapsed
A internal/adapters/vulkan/blend.go +186 -0
@@ -0,0 +1,186 @@
1+package vulkan
2+
3+import (
4+ _ "embed"
5+ "encoding/binary"
6+ "unsafe"
7+
8+ vk "github.com/bnema/purego-vulkan/vulkan"
9+)
10+
11+// Translucent client pixels are blended over what is below them; opaque
12+// ones keep the plain copy. The blend pipeline draws one quad per upload
13+// and its fragment shader reads the staged pixels straight from the
14+// staging buffer: no texture, no sampler. Wayland buffers are
15+// premultiplied, hence ONE, ONE_MINUS_SRC_ALPHA.
16+
17+//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
19+
20+var (
21+ //go:embed shaders/blend.vert.spv
22+ blendVert []byte
23+ //go:embed shaders/blend.frag.spv
24+ blendFrag []byte
25+)
26+
27+// drawSize is one entry of the draw table (shaders: struct Draw).
28+const drawSize = 32
29+
30+// blendPipeline is the graphics state for blended uploads.
31+type blendPipeline struct {
32+ setLayout vk.DescriptorSetLayout
33+ layout vk.PipelineLayout
34+ pipeline vk.Pipeline
35+ pool vk.DescriptorPool
36+ set vk.DescriptorSet
37+}
38+
39+func (r *Renderer) createBlend() error {
40+ d, b := r.dd, &r.blend
41+ bindings := []vk.DescriptorSetLayoutBinding{
42+ {Binding: 0, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
43+ {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit},
44+ }
45+ sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 2, Bindings: &bindings[0]}
46+ if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &b.setLayout)); err != nil {
47+ return err
48+ }
49+ pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &b.setLayout}
50+ if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &b.layout)); err != nil {
51+ return err
52+ }
53+ size := vk.DescriptorPoolSize{Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 2}
54+ dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &size}
55+ if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &b.pool)); err != nil {
56+ return err
57+ }
58+ dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: b.pool, DescriptorSetCount: 1, SetLayouts: &b.setLayout}
59+ if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &b.set)); err != nil {
60+ return err
61+ }
62+ vert, err := r.shaderModule(blendVert)
63+ if err != nil {
64+ return err
65+ }
66+ defer d.DestroyShaderModule(r.device, vert, nil)
67+ frag, err := r.shaderModule(blendFrag)
68+ if err != nil {
69+ return err
70+ }
71+ defer d.DestroyShaderModule(r.device, frag, nil)
72+ main := []byte("main\x00")
73+ stages := []vk.PipelineShaderStageCreateInfo{
74+ {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]},
75+ {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]},
76+ }
77+ vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo}
78+ assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList}
79+ // The target size is fixed for the renderer's life: static viewport.
80+ viewport := vk.Viewport{Width: float32(r.width), Height: float32(r.height), MaxDepth: 1}
81+ scissor := vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}
82+ viewportState := vk.PipelineViewportStateCreateInfo{SType: vk.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor}
83+ raster := vk.PipelineRasterizationStateCreateInfo{SType: vk.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vk.PolygonModeFill, CullMode: vk.CullModeNone, FrontFace: vk.FrontFaceCounterClockwise, LineWidth: 1}
84+ multisample := vk.PipelineMultisampleStateCreateInfo{SType: vk.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vk.SampleCount1Bit}
85+ attachment := vk.PipelineColorBlendAttachmentState{
86+ BlendEnable: 1,
87+ SrcColorBlendFactor: vk.BlendFactorOne, DstColorBlendFactor: vk.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vk.BlendOpAdd,
88+ SrcAlphaBlendFactor: vk.BlendFactorOne, DstAlphaBlendFactor: vk.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vk.BlendOpAdd,
89+ ColorWriteMask: vk.ColorComponentRBit | vk.ColorComponentGBit | vk.ColorComponentBBit | vk.ColorComponentABit,
90+ }
91+ blendState := vk.PipelineColorBlendStateCreateInfo{SType: vk.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &attachment}
92+ format := vk.Format(vk.FormatB8g8r8a8Unorm)
93+ rendering := vk.PipelineRenderingCreateInfo{SType: vk.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format}
94+ gpi := vk.GraphicsPipelineCreateInfo{
95+ SType: vk.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering),
96+ StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly,
97+ ViewportState: &viewportState, RasterizationState: &raster, MultisampleState: &multisample,
98+ ColorBlendState: &blendState, Layout: b.layout, BasePipelineIndex: -1,
99+ }
100+ return checked("vkCreateGraphicsPipelines", d.CreateGraphicsPipelines(r.device, 0, 1, &gpi, nil, &b.pipeline))
101+}
102+
103+func (r *Renderer) shaderModule(code []byte) (vk.ShaderModule, error) {
104+ // SPIR-V is read as words: copy into a uint32-aligned slice.
105+ words := make([]uint32, len(code)/4)
106+ for i := range words {
107+ words[i] = binary.LittleEndian.Uint32(code[i*4:])
108+ }
109+ info := vk.ShaderModuleCreateInfo{SType: vk.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(words) * 4), Code: &words[0]}
110+ var m vk.ShaderModule
111+ return m, checked("vkCreateShaderModule", r.dd.CreateShaderModule(r.device, &info, nil, &m))
112+}
113+
114+func (r *Renderer) destroyBlend() {
115+ d, b := r.dd, &r.blend
116+ if b.pipeline != 0 {
117+ d.DestroyPipeline(r.device, b.pipeline, nil)
118+ }
119+ if b.pool != 0 {
120+ d.DestroyDescriptorPool(r.device, b.pool, nil)
121+ }
122+ if b.layout != 0 {
123+ d.DestroyPipelineLayout(r.device, b.layout, nil)
124+ }
125+ if b.setLayout != 0 {
126+ d.DestroyDescriptorSetLayout(r.device, b.setLayout, nil)
127+ }
128+ *b = blendPipeline{}
129+}
130+
131+// bindStaging points the set at the staging buffer: pixels from 0, the
132+// draw table from tableOffset.
133+func (r *Renderer) bindStaging(tableOffset int) {
134+ infos := []vk.DescriptorBufferInfo{
135+ {Buffer: r.staging, Range: vk.DeviceSize(tableOffset)},
136+ {Buffer: r.staging, Offset: vk.DeviceSize(tableOffset), Range: wholeSize},
137+ }
138+ writes := []vk.WriteDescriptorSet{
139+ {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[0]},
140+ {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[1]},
141+ }
142+ r.dd.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil)
143+}
144+
145+// putDraw writes draw entry i of the table at data.
146+func (r *Renderer) putDraw(table []byte, i int, u *upload) {
147+ e := table[i*drawSize : (i+1)*drawSize]
148+ for j, v := range []uint32{
149+ uint32(u.rect.Min.X), uint32(u.rect.Min.Y), uint32(u.rect.Max.X), uint32(u.rect.Max.Y),
150+ uint32(u.offset / 4), uint32(u.rect.Dx()), uint32(r.width), uint32(r.height),
151+ } {
152+ binary.LittleEndian.PutUint32(e[j*4:], v)
153+ }
154+}
155+
156+// drawBlended draws uploads[first:last], all blended, into view: one
157+// instanced draw. The image is TRANSFER_DST before and after.
158+func (r *Renderer) drawBlended(image vk.Image, view vk.ImageView, first, last int) {
159+ d := r.dd
160+ rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
161+ b := vk.ImageMemoryBarrier{
162+ SType: vk.StructureTypeImageMemoryBarrier, Image: image, SubresourceRange: rangeInfo,
163+ SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessColorAttachmentReadBit | vk.AccessColorAttachmentWriteBit,
164+ OldLayout: vk.ImageLayoutTransferDstOptimal, NewLayout: vk.ImageLayoutColorAttachmentOptimal,
165+ }
166+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)
167+ attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpLoad, StoreOp: vk.AttachmentStoreOpStore}
168+ info := vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment}
169+ d.CmdBeginRendering(r.command, &info)
170+ d.CmdBindPipeline(r.command, vk.PipelineBindPointGraphics, r.blend.pipeline)
171+ d.CmdBindDescriptorSets(r.command, vk.PipelineBindPointGraphics, r.blend.layout, 0, 1, &r.blend.set, 0, nil)
172+ d.CmdDraw(r.command, 6, uint32(last-first), 0, uint32(first))
173+ d.CmdEndRendering(r.command)
174+ b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, vk.AccessTransferWriteBit|vk.AccessTransferReadBit
175+ b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutTransferDstOptimal
176+ d.CmdPipelineBarrier(r.command, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
177+}
178+
179+// createView makes the color attachment view of a target image.
180+func (r *Renderer) createView(t *target) error {
181+ info := vk.ImageViewCreateInfo{
182+ SType: vk.StructureTypeImageViewCreateInfo, Image: t.image, ViewType: vk.ImageViewType2d, Format: vk.FormatB8g8r8a8Unorm,
183+ SubresourceRange: vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1},
184+ }
185+ return checked("vkCreateImageView", r.dd.CreateImageView(r.device, &info, nil, &t.view))
186+}
@@ -0,0 +1,186 @@
1+package vulkan
2+
3+import (
4+ _ "embed"
5+ "encoding/binary"
6+ "unsafe"
7+
8+ vk "github.com/bnema/purego-vulkan/vulkan"
9+)
10+
11+// Translucent client pixels are blended over what is below them; opaque
12+// ones keep the plain copy. The blend pipeline draws one quad per upload
13+// and its fragment shader reads the staged pixels straight from the
14+// staging buffer: no texture, no sampler. Wayland buffers are
15+// premultiplied, hence ONE, ONE_MINUS_SRC_ALPHA.
16+
17+//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
19+
20+var (
21+ //go:embed shaders/blend.vert.spv
22+ blendVert []byte
23+ //go:embed shaders/blend.frag.spv
24+ blendFrag []byte
25+)
26+
27+// drawSize is one entry of the draw table (shaders: struct Draw).
28+const drawSize = 32
29+
30+// blendPipeline is the graphics state for blended uploads.
31+type blendPipeline struct {
32+ setLayout vk.DescriptorSetLayout
33+ layout vk.PipelineLayout
34+ pipeline vk.Pipeline
35+ pool vk.DescriptorPool
36+ set vk.DescriptorSet
37+}
38+
39+func (r *Renderer) createBlend() error {
40+ d, b := r.dd, &r.blend
41+ bindings := []vk.DescriptorSetLayoutBinding{
42+ {Binding: 0, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
43+ {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit},
44+ }
45+ sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 2, Bindings: &bindings[0]}
46+ if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &b.setLayout)); err != nil {
47+ return err
48+ }
49+ pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &b.setLayout}
50+ if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &b.layout)); err != nil {
51+ return err
52+ }
53+ size := vk.DescriptorPoolSize{Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 2}
54+ dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &size}
55+ if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &b.pool)); err != nil {
56+ return err
57+ }
58+ dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: b.pool, DescriptorSetCount: 1, SetLayouts: &b.setLayout}
59+ if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &b.set)); err != nil {
60+ return err
61+ }
62+ vert, err := r.shaderModule(blendVert)
63+ if err != nil {
64+ return err
65+ }
66+ defer d.DestroyShaderModule(r.device, vert, nil)
67+ frag, err := r.shaderModule(blendFrag)
68+ if err != nil {
69+ return err
70+ }
71+ defer d.DestroyShaderModule(r.device, frag, nil)
72+ main := []byte("main\x00")
73+ stages := []vk.PipelineShaderStageCreateInfo{
74+ {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]},
75+ {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]},
76+ }
77+ vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo}
78+ assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList}
79+ // The target size is fixed for the renderer's life: static viewport.
80+ viewport := vk.Viewport{Width: float32(r.width), Height: float32(r.height), MaxDepth: 1}
81+ scissor := vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}
82+ viewportState := vk.PipelineViewportStateCreateInfo{SType: vk.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor}
83+ raster := vk.PipelineRasterizationStateCreateInfo{SType: vk.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vk.PolygonModeFill, CullMode: vk.CullModeNone, FrontFace: vk.FrontFaceCounterClockwise, LineWidth: 1}
84+ multisample := vk.PipelineMultisampleStateCreateInfo{SType: vk.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vk.SampleCount1Bit}
85+ attachment := vk.PipelineColorBlendAttachmentState{
86+ BlendEnable: 1,
87+ SrcColorBlendFactor: vk.BlendFactorOne, DstColorBlendFactor: vk.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vk.BlendOpAdd,
88+ SrcAlphaBlendFactor: vk.BlendFactorOne, DstAlphaBlendFactor: vk.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vk.BlendOpAdd,
89+ ColorWriteMask: vk.ColorComponentRBit | vk.ColorComponentGBit | vk.ColorComponentBBit | vk.ColorComponentABit,
90+ }
91+ blendState := vk.PipelineColorBlendStateCreateInfo{SType: vk.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &attachment}
92+ format := vk.Format(vk.FormatB8g8r8a8Unorm)
93+ rendering := vk.PipelineRenderingCreateInfo{SType: vk.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format}
94+ gpi := vk.GraphicsPipelineCreateInfo{
95+ SType: vk.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering),
96+ StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly,
97+ ViewportState: &viewportState, RasterizationState: &raster, MultisampleState: &multisample,
98+ ColorBlendState: &blendState, Layout: b.layout, BasePipelineIndex: -1,
99+ }
100+ return checked("vkCreateGraphicsPipelines", d.CreateGraphicsPipelines(r.device, 0, 1, &gpi, nil, &b.pipeline))
101+}
102+
103+func (r *Renderer) shaderModule(code []byte) (vk.ShaderModule, error) {
104+ // SPIR-V is read as words: copy into a uint32-aligned slice.
105+ words := make([]uint32, len(code)/4)
106+ for i := range words {
107+ words[i] = binary.LittleEndian.Uint32(code[i*4:])
108+ }
109+ info := vk.ShaderModuleCreateInfo{SType: vk.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(words) * 4), Code: &words[0]}
110+ var m vk.ShaderModule
111+ return m, checked("vkCreateShaderModule", r.dd.CreateShaderModule(r.device, &info, nil, &m))
112+}
113+
114+func (r *Renderer) destroyBlend() {
115+ d, b := r.dd, &r.blend
116+ if b.pipeline != 0 {
117+ d.DestroyPipeline(r.device, b.pipeline, nil)
118+ }
119+ if b.pool != 0 {
120+ d.DestroyDescriptorPool(r.device, b.pool, nil)
121+ }
122+ if b.layout != 0 {
123+ d.DestroyPipelineLayout(r.device, b.layout, nil)
124+ }
125+ if b.setLayout != 0 {
126+ d.DestroyDescriptorSetLayout(r.device, b.setLayout, nil)
127+ }
128+ *b = blendPipeline{}
129+}
130+
131+// bindStaging points the set at the staging buffer: pixels from 0, the
132+// draw table from tableOffset.
133+func (r *Renderer) bindStaging(tableOffset int) {
134+ infos := []vk.DescriptorBufferInfo{
135+ {Buffer: r.staging, Range: vk.DeviceSize(tableOffset)},
136+ {Buffer: r.staging, Offset: vk.DeviceSize(tableOffset), Range: wholeSize},
137+ }
138+ writes := []vk.WriteDescriptorSet{
139+ {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[0]},
140+ {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[1]},
141+ }
142+ r.dd.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil)
143+}
144+
145+// putDraw writes draw entry i of the table at data.
146+func (r *Renderer) putDraw(table []byte, i int, u *upload) {
147+ e := table[i*drawSize : (i+1)*drawSize]
148+ for j, v := range []uint32{
149+ uint32(u.rect.Min.X), uint32(u.rect.Min.Y), uint32(u.rect.Max.X), uint32(u.rect.Max.Y),
150+ uint32(u.offset / 4), uint32(u.rect.Dx()), uint32(r.width), uint32(r.height),
151+ } {
152+ binary.LittleEndian.PutUint32(e[j*4:], v)
153+ }
154+}
155+
156+// drawBlended draws uploads[first:last], all blended, into view: one
157+// instanced draw. The image is TRANSFER_DST before and after.
158+func (r *Renderer) drawBlended(image vk.Image, view vk.ImageView, first, last int) {
159+ d := r.dd
160+ rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
161+ b := vk.ImageMemoryBarrier{
162+ SType: vk.StructureTypeImageMemoryBarrier, Image: image, SubresourceRange: rangeInfo,
163+ SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessColorAttachmentReadBit | vk.AccessColorAttachmentWriteBit,
164+ OldLayout: vk.ImageLayoutTransferDstOptimal, NewLayout: vk.ImageLayoutColorAttachmentOptimal,
165+ }
166+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)
167+ attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpLoad, StoreOp: vk.AttachmentStoreOpStore}
168+ info := vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment}
169+ d.CmdBeginRendering(r.command, &info)
170+ d.CmdBindPipeline(r.command, vk.PipelineBindPointGraphics, r.blend.pipeline)
171+ d.CmdBindDescriptorSets(r.command, vk.PipelineBindPointGraphics, r.blend.layout, 0, 1, &r.blend.set, 0, nil)
172+ d.CmdDraw(r.command, 6, uint32(last-first), 0, uint32(first))
173+ d.CmdEndRendering(r.command)
174+ b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, vk.AccessTransferWriteBit|vk.AccessTransferReadBit
175+ b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutTransferDstOptimal
176+ d.CmdPipelineBarrier(r.command, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
177+}
178+
179+// createView makes the color attachment view of a target image.
180+func (r *Renderer) createView(t *target) error {
181+ info := vk.ImageViewCreateInfo{
182+ SType: vk.StructureTypeImageViewCreateInfo, Image: t.image, ViewType: vk.ImageViewType2d, Format: vk.FormatB8g8r8a8Unorm,
183+ SubresourceRange: vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1},
184+ }
185+ return checked("vkCreateImageView", r.dd.CreateImageView(r.device, &info, nil, &t.view))
186+}
M internal/adapters/vulkan/renderer.go +55 -7
@@ -55,6 +55,10 @@ type Renderer struct {
5555 frame uint64
5656 // fillRegions is reused by Render for the bands of solid fills.
5757 fillRegions []vk.BufferImageCopy
58+ // blend draws translucent client pixels (blend.go); align is the
59+ // device's storage buffer offset alignment.
60+ blend blendPipeline
61+ align int
5862 }
5963
6064 // fillRows is the height of the strip staged for a solid fill: the CPU
@@ -123,6 +127,7 @@ func New(width, height int) (r *Renderer, err error) {
123127 if properties.ApiVersion < vk.MakeVersion(1, 3, 0) {
124128 continue
125129 }
130+ r.align = int(max(properties.Limits.MinStorageBufferOffsetAlignment, 4))
126131 var n uint32
127132 r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, nil)
128133 if n == 0 {
@@ -181,7 +186,7 @@ func New(width, height int) (r *Renderer, err error) {
181186 }
182187 r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)
183188 extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}
184- ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: extent, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
189+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: extent, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: targetUsage, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
185190 if err = checked("vkCreateImage", r.dd.CreateImage(r.device, &ii, nil, &r.own.image)); err != nil {
186191 return
187192 }
@@ -226,7 +231,7 @@ func New(width, height int) (r *Renderer, err error) {
226231 // Start with two frame-sized regions; later frames grow staging on demand.
227232 stagingSize := size * 2
228233 r.stagingSize = int(stagingSize)
229- stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive}
234+ stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: stagingUsage, SharingMode: vk.SharingModeExclusive}
230235 if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil {
231236 return
232237 }
@@ -253,6 +258,12 @@ func New(width, height int) (r *Renderer, err error) {
253258 if err = checked("vkAllocateCommandBuffers", r.dd.AllocateCommandBuffers(r.device, &ai, &r.command)); err != nil {
254259 return
255260 }
261+ if err = r.createView(&r.own); err != nil {
262+ return
263+ }
264+ if err = r.createBlend(); err != nil {
265+ return
266+ }
256267 fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
257268 if err = checked("vkCreateFence", r.dd.CreateFence(r.device, &fi, nil, &r.fence)); err != nil {
258269 return
@@ -315,7 +326,7 @@ func (r *Renderer) ensureStaging(size int) error {
315326 }
316327 d := r.dd
317328 capacity := size
318- info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive}
329+ info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: stagingUsage, SharingMode: vk.SharingModeExclusive}
319330 var buffer vk.Buffer
320331 if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil {
321332 return err
@@ -370,6 +381,8 @@ type upload struct {
370381 dst, src image.Rectangle
371382 color [3]uint8
372383 opaque bool
384+ // blend draws the staged pixels over the target instead of copying.
385+ blend bool
373386 // dma is a client GPU buffer, blitted instead of copied from staging.
374387 dma *imported
375388 // fill is a solid color: staging holds one strip of rows.
@@ -443,7 +456,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
443456 if err != nil {
444457 return
445458 }
446- uploads = append(uploads, upload{rect: rect, offset: used, pixels: pixels[b.Offset:], stride: b.Stride, opaque: content.Opaque, dst: dst, src: src})
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})
447460 used += rect.Dx() * rect.Dy() * 4
448461 }
449462 // drawSurface draws one surface buffer with its origin at (x, y)
@@ -561,10 +574,31 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
561574 addPopups(false)
562575 addLayers(true)
563576 addPopups(true)
577+ // The draw table of blended uploads follows the pixels.
578+ blends := 0
579+ for _, u := range uploads {
580+ if u.blend {
581+ blends++
582+ }
583+ }
584+ tableOffset := (used + r.align - 1) / r.align * r.align
585+ if blends > 0 {
586+ used = tableOffset + blends*drawSize
587+ }
564588 if err := r.ensureStaging(used); err != nil {
565589 return err
566590 }
567591 data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize)
592+ if blends > 0 {
593+ r.bindStaging(tableOffset)
594+ i := 0
595+ for k := range uploads {
596+ if uploads[k].blend {
597+ r.putDraw(data[tableOffset:], i, &uploads[k])
598+ i++
599+ }
600+ }
601+ }
568602 for _, u := range uploads {
569603 if u.dma != nil {
570604 continue
@@ -581,7 +615,6 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
581615 // then shows garbage for this frame, never a crash.
582616 copyGuarded(func() { u.stage(dst) })
583617 }
584- // B8G8R8A8 pixels are copied unchanged; blending belongs to the compositing pipeline.
585618 rgb := parseColor(s.Background)
586619 d := r.dd
587620 if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
@@ -643,10 +676,24 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
643676 }
644677 }
645678 ownership(true)
646- for i, f := range uploads {
647- if i > 0 {
679+ drawn := 0 // blended uploads drawn so far: their index in the draw table
680+ for i := 0; i < len(uploads); i++ {
681+ f := uploads[i]
682+ if i > 0 && !f.blend {
648683 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
649684 }
685+ if f.blend {
686+ // Consecutive blended uploads share one draw: blending follows
687+ // primitive order within it.
688+ n := 1
689+ for i+n < len(uploads) && uploads[i+n].blend {
690+ n++
691+ }
692+ r.drawBlended(tg.image, tg.view, drawn, drawn+n)
693+ drawn += n
694+ i += n - 1
695+ continue
696+ }
650697 rect := f.rect
651698 if f.dma != nil {
652699 // Map the visible part of dst back onto the buffer.
@@ -860,6 +907,7 @@ func (r *Renderer) Close() {
860907 d.FreeMemory(r.device, r.bufferMemory, nil)
861908 r.bufferMemory = 0
862909 }
910+ r.destroyBlend()
863911 r.freeTarget(&r.own)
864912 d.DestroyDevice(r.device, nil)
865913 r.device = 0
@@ -55,6 +55,10 @@ type Renderer struct {
55 frame uint6455 frame uint64
56 // fillRegions is reused by Render for the bands of solid fills.56 // fillRegions is reused by Render for the bands of solid fills.
57 fillRegions []vk.BufferImageCopy57 fillRegions []vk.BufferImageCopy
58+ // blend draws translucent client pixels (blend.go); align is the
59+ // device's storage buffer offset alignment.
60+ blend blendPipeline
61+ align int
58 }62 }
59 63
60 // fillRows is the height of the strip staged for a solid fill: the CPU64 // fillRows is the height of the strip staged for a solid fill: the CPU
@@ -123,6 +127,7 @@ func New(width, height int) (r *Renderer, err error) {
123 if properties.ApiVersion < vk.MakeVersion(1, 3, 0) {127 if properties.ApiVersion < vk.MakeVersion(1, 3, 0) {
124 continue128 continue
125 }129 }
130+ r.align = int(max(properties.Limits.MinStorageBufferOffsetAlignment, 4))
126 var n uint32131 var n uint32
127 r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, nil)132 r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, nil)
128 if n == 0 {133 if n == 0 {
@@ -181,7 +186,7 @@ func New(width, height int) (r *Renderer, err error) {
181 }186 }
182 r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)187 r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)
183 extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}188 extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}
184- ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: extent, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}189+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: extent, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: targetUsage, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
185 if err = checked("vkCreateImage", r.dd.CreateImage(r.device, &ii, nil, &r.own.image)); err != nil {190 if err = checked("vkCreateImage", r.dd.CreateImage(r.device, &ii, nil, &r.own.image)); err != nil {
186 return191 return
187 }192 }
@@ -226,7 +231,7 @@ func New(width, height int) (r *Renderer, err error) {
226 // Start with two frame-sized regions; later frames grow staging on demand.231 // Start with two frame-sized regions; later frames grow staging on demand.
227 stagingSize := size * 2232 stagingSize := size * 2
228 r.stagingSize = int(stagingSize)233 r.stagingSize = int(stagingSize)
229- stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive}234+ stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: stagingUsage, SharingMode: vk.SharingModeExclusive}
230 if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil {235 if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil {
231 return236 return
232 }237 }
@@ -253,6 +258,12 @@ func New(width, height int) (r *Renderer, err error) {
253 if err = checked("vkAllocateCommandBuffers", r.dd.AllocateCommandBuffers(r.device, &ai, &r.command)); err != nil {258 if err = checked("vkAllocateCommandBuffers", r.dd.AllocateCommandBuffers(r.device, &ai, &r.command)); err != nil {
254 return259 return
255 }260 }
261+ if err = r.createView(&r.own); err != nil {
262+ return
263+ }
264+ if err = r.createBlend(); err != nil {
265+ return
266+ }
256 fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}267 fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
257 if err = checked("vkCreateFence", r.dd.CreateFence(r.device, &fi, nil, &r.fence)); err != nil {268 if err = checked("vkCreateFence", r.dd.CreateFence(r.device, &fi, nil, &r.fence)); err != nil {
258 return269 return
@@ -315,7 +326,7 @@ func (r *Renderer) ensureStaging(size int) error {
315 }326 }
316 d := r.dd327 d := r.dd
317 capacity := size328 capacity := size
318- info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive}329+ info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: stagingUsage, SharingMode: vk.SharingModeExclusive}
319 var buffer vk.Buffer330 var buffer vk.Buffer
320 if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil {331 if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil {
321 return err332 return err
@@ -370,6 +381,8 @@ type upload struct {
370 dst, src image.Rectangle381 dst, src image.Rectangle
371 color [3]uint8382 color [3]uint8
372 opaque bool383 opaque bool
384+ // blend draws the staged pixels over the target instead of copying.
385+ blend bool
373 // dma is a client GPU buffer, blitted instead of copied from staging.386 // dma is a client GPU buffer, blitted instead of copied from staging.
374 dma *imported387 dma *imported
375 // fill is a solid color: staging holds one strip of rows.388 // fill is a solid color: staging holds one strip of rows.
@@ -443,7 +456,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
443 if err != nil {456 if err != nil {
444 return457 return
445 }458 }
446- uploads = append(uploads, upload{rect: rect, offset: used, pixels: pixels[b.Offset:], stride: b.Stride, opaque: content.Opaque, dst: dst, src: src})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})
447 used += rect.Dx() * rect.Dy() * 4460 used += rect.Dx() * rect.Dy() * 4
448 }461 }
449 // drawSurface draws one surface buffer with its origin at (x, y)462 // drawSurface draws one surface buffer with its origin at (x, y)
@@ -561,10 +574,31 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
561 addPopups(false)574 addPopups(false)
562 addLayers(true)575 addLayers(true)
563 addPopups(true)576 addPopups(true)
577+ // The draw table of blended uploads follows the pixels.
578+ blends := 0
579+ for _, u := range uploads {
580+ if u.blend {
581+ blends++
582+ }
583+ }
584+ tableOffset := (used + r.align - 1) / r.align * r.align
585+ if blends > 0 {
586+ used = tableOffset + blends*drawSize
587+ }
564 if err := r.ensureStaging(used); err != nil {588 if err := r.ensureStaging(used); err != nil {
565 return err589 return err
566 }590 }
567 data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize)591 data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize)
592+ if blends > 0 {
593+ r.bindStaging(tableOffset)
594+ i := 0
595+ for k := range uploads {
596+ if uploads[k].blend {
597+ r.putDraw(data[tableOffset:], i, &uploads[k])
598+ i++
599+ }
600+ }
601+ }
568 for _, u := range uploads {602 for _, u := range uploads {
569 if u.dma != nil {603 if u.dma != nil {
570 continue604 continue
@@ -581,7 +615,6 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
581 // then shows garbage for this frame, never a crash.615 // then shows garbage for this frame, never a crash.
582 copyGuarded(func() { u.stage(dst) })616 copyGuarded(func() { u.stage(dst) })
583 }617 }
584- // B8G8R8A8 pixels are copied unchanged; blending belongs to the compositing pipeline.
585 rgb := parseColor(s.Background)618 rgb := parseColor(s.Background)
586 d := r.dd619 d := r.dd
587 if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {620 if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
@@ -643,10 +676,24 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
643 }676 }
644 }677 }
645 ownership(true)678 ownership(true)
646- for i, f := range uploads {679+ drawn := 0 // blended uploads drawn so far: their index in the draw table
647- if i > 0 {680+ for i := 0; i < len(uploads); i++ {
681+ f := uploads[i]
682+ if i > 0 && !f.blend {
648 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)683 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
649 }684 }
685+ if f.blend {
686+ // Consecutive blended uploads share one draw: blending follows
687+ // primitive order within it.
688+ n := 1
689+ for i+n < len(uploads) && uploads[i+n].blend {
690+ n++
691+ }
692+ r.drawBlended(tg.image, tg.view, drawn, drawn+n)
693+ drawn += n
694+ i += n - 1
695+ continue
696+ }
650 rect := f.rect697 rect := f.rect
651 if f.dma != nil {698 if f.dma != nil {
652 // Map the visible part of dst back onto the buffer.699 // Map the visible part of dst back onto the buffer.
@@ -860,6 +907,7 @@ func (r *Renderer) Close() {
860 d.FreeMemory(r.device, r.bufferMemory, nil)907 d.FreeMemory(r.device, r.bufferMemory, nil)
861 r.bufferMemory = 0908 r.bufferMemory = 0
862 }909 }
910+ r.destroyBlend()
863 r.freeTarget(&r.own)911 r.freeTarget(&r.own)
864 d.DestroyDevice(r.device, nil)912 d.DestroyDevice(r.device, nil)
865 r.device = 0913 r.device = 0
M internal/adapters/vulkan/renderer_test.go +46 -0
@@ -364,3 +364,49 @@ func TestRendererTallFill(t *testing.T) {
364364 }
365365 }
366366 }
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.
371+func TestRendererAlphaBlend(t *testing.T) {
372+ r, err := New(64, 48)
373+ if err != nil {
374+ t.Skipf("Vulkan unavailable: %v", err)
375+ }
376+ 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+ }
387+ }
388+ overlay := shmContent(t, 16, 8, 16*4, half)
389+ contents := map[ports.WindowID]ports.SurfaceContent{2: *overlay}
390+ 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
397+ if err := r.Render(scene, contents); err != nil {
398+ t.Fatal(err)
399+ }
400+ 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)
411+ }
412+}
@@ -364,3 +364,49 @@ func TestRendererTallFill(t *testing.T) {
364 }364 }
365 }365 }
366 }366 }
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.
371+func TestRendererAlphaBlend(t *testing.T) {
372+ r, err := New(64, 48)
373+ if err != nil {
374+ t.Skipf("Vulkan unavailable: %v", err)
375+ }
376+ 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+ }
387+ }
388+ overlay := shmContent(t, 16, 8, 16*4, half)
389+ contents := map[ports.WindowID]ports.SurfaceContent{2: *overlay}
390+ 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
397+ if err := r.Render(scene, contents); err != nil {
398+ t.Fatal(err)
399+ }
400+ 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)
411+ }
412+}
A internal/adapters/vulkan/shaders/blend.frag +22 -0
@@ -0,0 +1,22 @@
1+#version 450
2+// Reads the staged B8G8R8A8 pixel under the fragment: premultiplied, as
3+// Wayland ARGB buffers are, so the pipeline blends ONE, ONE_MINUS_SRC_ALPHA.
4+
5+struct Draw {
6+ ivec4 rect;
7+ uvec4 pixels;
8+};
9+
10+layout(std430, set = 0, binding = 0) readonly buffer Pixels { uint pixels[]; };
11+layout(std430, set = 0, binding = 1) readonly buffer Draws { Draw draws[]; };
12+
13+layout(location = 0) flat in uint draw;
14+layout(location = 0) out vec4 color;
15+
16+void main() {
17+ Draw d = draws[draw];
18+ ivec2 p = ivec2(gl_FragCoord.xy) - d.rect.xy;
19+ // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).
20+ vec4 bgra = unpackUnorm4x8(pixels[d.pixels.x + uint(p.y) * d.pixels.y + uint(p.x)]);
21+ color = bgra.zyxw;
22+}
@@ -0,0 +1,22 @@
1+#version 450
2+// Reads the staged B8G8R8A8 pixel under the fragment: premultiplied, as
3+// Wayland ARGB buffers are, so the pipeline blends ONE, ONE_MINUS_SRC_ALPHA.
4+
5+struct Draw {
6+ ivec4 rect;
7+ uvec4 pixels;
8+};
9+
10+layout(std430, set = 0, binding = 0) readonly buffer Pixels { uint pixels[]; };
11+layout(std430, set = 0, binding = 1) readonly buffer Draws { Draw draws[]; };
12+
13+layout(location = 0) flat in uint draw;
14+layout(location = 0) out vec4 color;
15+
16+void main() {
17+ Draw d = draws[draw];
18+ ivec2 p = ivec2(gl_FragCoord.xy) - d.rect.xy;
19+ // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).
20+ vec4 bgra = unpackUnorm4x8(pixels[d.pixels.x + uint(p.y) * d.pixels.y + uint(p.x)]);
21+ color = bgra.zyxw;
22+}
A internal/adapters/vulkan/shaders/blend.frag.spv +0 -0 binary file: no text diff

binary file: no text diff

A internal/adapters/vulkan/shaders/blend.vert +21 -0
@@ -0,0 +1,21 @@
1+#version 450
2+// One quad per instance: the instance indexes the draw table.
3+
4+struct Draw {
5+ ivec4 rect; // x0, y0, x1, y1 in target pixels
6+ uvec4 pixels; // first pixel word in staging, row width, target w, target h
7+};
8+
9+layout(std430, set = 0, binding = 1) readonly buffer Draws { Draw draws[]; };
10+
11+layout(location = 0) flat out uint draw;
12+
13+void main() {
14+ Draw d = draws[gl_InstanceIndex];
15+ // Two triangles: 0,1,2 and 2,1,3 of the corners.
16+ const ivec2 corners[6] = ivec2[](ivec2(0, 0), ivec2(1, 0), ivec2(0, 1), ivec2(0, 1), ivec2(1, 0), ivec2(1, 1));
17+ ivec2 c = corners[gl_VertexIndex];
18+ vec2 p = vec2(c.x == 0 ? d.rect.x : d.rect.z, c.y == 0 ? d.rect.y : d.rect.w);
19+ gl_Position = vec4(p / vec2(d.pixels.zw) * 2.0 - 1.0, 0.0, 1.0);
20+ draw = uint(gl_InstanceIndex);
21+}
@@ -0,0 +1,21 @@
1+#version 450
2+// One quad per instance: the instance indexes the draw table.
3+
4+struct Draw {
5+ ivec4 rect; // x0, y0, x1, y1 in target pixels
6+ uvec4 pixels; // first pixel word in staging, row width, target w, target h
7+};
8+
9+layout(std430, set = 0, binding = 1) readonly buffer Draws { Draw draws[]; };
10+
11+layout(location = 0) flat out uint draw;
12+
13+void main() {
14+ Draw d = draws[gl_InstanceIndex];
15+ // Two triangles: 0,1,2 and 2,1,3 of the corners.
16+ const ivec2 corners[6] = ivec2[](ivec2(0, 0), ivec2(1, 0), ivec2(0, 1), ivec2(0, 1), ivec2(1, 0), ivec2(1, 1));
17+ ivec2 c = corners[gl_VertexIndex];
18+ vec2 p = vec2(c.x == 0 ? d.rect.x : d.rect.z, c.y == 0 ? d.rect.y : d.rect.w);
19+ gl_Position = vec4(p / vec2(d.pixels.zw) * 2.0 - 1.0, 0.0, 1.0);
20+ draw = uint(gl_InstanceIndex);
21+}
A internal/adapters/vulkan/shaders/blend.vert.spv +0 -0 binary file: no text diff

binary file: no text diff

M internal/adapters/vulkan/targets.go +14 -2
@@ -24,6 +24,7 @@ type target struct {
2424 image vk.Image
2525 memory vk.DeviceMemory
2626 layout vk.ImageLayout
27+ view vk.ImageView // color attachment for blended draws
2728 // exported images are shared with the display: ownership goes to the
2829 // foreign queue family after each frame, and KMS reads them.
2930 exported bool
@@ -104,7 +105,7 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
104105 mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
105106 list.DrmFormatModifierProperties = &mods[0]
106107 r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
107- need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit)
108+ need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit)
108109 for _, m := range mods[:list.DrmFormatModifierCount] {
109110 if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) {
110111 r.renderMods = append(r.renderMods, m.DrmFormatModifier)
@@ -131,7 +132,11 @@ const (
131132 formatFeatureTransferDst = 1 << 15
132133 )
133134
134-const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit
135+// Targets are copied into and drawn on (blend.go).
136+const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit | vk.ImageUsageColorAttachmentBit
137+
138+// Staging feeds copies and, as a storage buffer, the blend shaders.
139+const stagingUsage = vk.BufferUsageTransferSrcBit | vk.BufferUsageStorageBufferBit
135140
136141 // exportTarget creates one exported image with a modifier from mods.
137142 func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
@@ -171,6 +176,9 @@ func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
171176 var layout vk.SubresourceLayout
172177 sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}
173178 d.GetImageSubresourceLayout(r.device, t.image, &sub, &layout)
179+ if err := r.createView(t); err != nil {
180+ return nil, ports.DMABuf{}, err
181+ }
174182 var fd int32
175183 get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: t.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
176184 if err := checked("vkGetMemoryFdKHR", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil {
@@ -186,6 +194,10 @@ func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
186194 }
187195
188196 func (r *Renderer) freeTarget(t *target) {
197+ if t.view != 0 {
198+ r.dd.DestroyImageView(r.device, t.view, nil)
199+ t.view = 0
200+ }
189201 if t.image != 0 {
190202 r.dd.DestroyImage(r.device, t.image, nil)
191203 t.image = 0
@@ -24,6 +24,7 @@ type target struct {
24 image vk.Image24 image vk.Image
25 memory vk.DeviceMemory25 memory vk.DeviceMemory
26 layout vk.ImageLayout26 layout vk.ImageLayout
27+ view vk.ImageView // color attachment for blended draws
27 // exported images are shared with the display: ownership goes to the28 // exported images are shared with the display: ownership goes to the
28 // foreign queue family after each frame, and KMS reads them.29 // foreign queue family after each frame, and KMS reads them.
29 exported bool30 exported bool
@@ -104,7 +105,7 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
104 mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)105 mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
105 list.DrmFormatModifierProperties = &mods[0]106 list.DrmFormatModifierProperties = &mods[0]
106 r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)107 r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
107- need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit)108+ need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit)
108 for _, m := range mods[:list.DrmFormatModifierCount] {109 for _, m := range mods[:list.DrmFormatModifierCount] {
109 if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) {110 if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) {
110 r.renderMods = append(r.renderMods, m.DrmFormatModifier)111 r.renderMods = append(r.renderMods, m.DrmFormatModifier)
@@ -131,7 +132,11 @@ const (
131 formatFeatureTransferDst = 1 << 15132 formatFeatureTransferDst = 1 << 15
132 )133 )
133 134
134-const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit135+// Targets are copied into and drawn on (blend.go).
136+const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit | vk.ImageUsageColorAttachmentBit
137+
138+// Staging feeds copies and, as a storage buffer, the blend shaders.
139+const stagingUsage = vk.BufferUsageTransferSrcBit | vk.BufferUsageStorageBufferBit
135 140
136 // exportTarget creates one exported image with a modifier from mods.141 // exportTarget creates one exported image with a modifier from mods.
137 func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {142 func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
@@ -171,6 +176,9 @@ func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
171 var layout vk.SubresourceLayout176 var layout vk.SubresourceLayout
172 sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}177 sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}
173 d.GetImageSubresourceLayout(r.device, t.image, &sub, &layout)178 d.GetImageSubresourceLayout(r.device, t.image, &sub, &layout)
179+ if err := r.createView(t); err != nil {
180+ return nil, ports.DMABuf{}, err
181+ }
174 var fd int32182 var fd int32
175 get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: t.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}183 get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: t.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
176 if err := checked("vkGetMemoryFdKHR", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil {184 if err := checked("vkGetMemoryFdKHR", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil {
@@ -186,6 +194,10 @@ func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
186 }194 }
187 195
188 func (r *Renderer) freeTarget(t *target) {196 func (r *Renderer) freeTarget(t *target) {
197+ if t.view != 0 {
198+ r.dd.DestroyImageView(r.device, t.view, nil)
199+ t.view = 0
200+ }
189 if t.image != 0 {201 if t.image != 0 {
190 r.dd.DestroyImage(r.device, t.image, nil)202 r.dd.DestroyImage(r.device, t.image, nil)
191 t.image = 0203 t.image = 0

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