merge: alpha-blend (premultiplied blending of translucent SHM buffers)
10 files · +395 -14
M go.mod +1 -1
| @@ -5,7 +5,7 @@ go 1.27 | |||
| 5 | 5 | require ( | |
| 6 | 6 | github.com/bnema/purego v0.11.0-bnema.4 | |
| 7 | 7 | 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 | |
| 9 | 9 | github.com/bnema/wlturbo v0.2.0 | |
| 10 | 10 | github.com/bnema/zerowrap v1.4.1 | |
| 11 | 11 | 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.4 | 6 | github.com/bnema/purego v0.11.0-bnema.4 | ||
| 7 | github.com/bnema/purego-libwayland v0.1.1 | 7 | 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 |
| 9 | github.com/bnema/wlturbo v0.2.0 | 9 | github.com/bnema/wlturbo v0.2.0 | ||
| 10 | github.com/bnema/zerowrap v1.4.1 | 10 | github.com/bnema/zerowrap v1.4.1 | ||
| 11 | github.com/rs/zerolog v1.35.1 | 11 | github.com/rs/zerolog v1.35.1 | ||
M go.sum +2 -0 generated file: collapsed
A internal/adapters/vulkan/blend.go +190 -0
| @@ -0,0 +1,190 @@ | |||
| 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 | + | // 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. | |
| 20 | + | ||
| 21 | + | //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.vert -o shaders/blend.vert.spv | |
| 22 | + | //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.frag -o shaders/blend.frag.spv | |
| 23 | + | ||
| 24 | + | var ( | |
| 25 | + | //go:embed shaders/blend.vert.spv | |
| 26 | + | blendVert []byte | |
| 27 | + | //go:embed shaders/blend.frag.spv | |
| 28 | + | blendFrag []byte | |
| 29 | + | ) | |
| 30 | + | ||
| 31 | + | // drawSize is one entry of the draw table (shaders: struct Draw). | |
| 32 | + | const drawSize = 32 | |
| 33 | + | ||
| 34 | + | // blendPipeline is the graphics state for blended uploads. | |
| 35 | + | type blendPipeline struct { | |
| 36 | + | setLayout vk.DescriptorSetLayout | |
| 37 | + | layout vk.PipelineLayout | |
| 38 | + | pipeline vk.Pipeline | |
| 39 | + | pool vk.DescriptorPool | |
| 40 | + | set vk.DescriptorSet | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func (r *Renderer) createBlend() error { | |
| 44 | + | d, b := r.dd, &r.blend | |
| 45 | + | bindings := []vk.DescriptorSetLayoutBinding{ | |
| 46 | + | {Binding: 0, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}, | |
| 47 | + | {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit}, | |
| 48 | + | } | |
| 49 | + | sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 2, Bindings: &bindings[0]} | |
| 50 | + | if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &b.setLayout)); err != nil { | |
| 51 | + | return err | |
| 52 | + | } | |
| 53 | + | pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &b.setLayout} | |
| 54 | + | if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &b.layout)); err != nil { | |
| 55 | + | return err | |
| 56 | + | } | |
| 57 | + | size := vk.DescriptorPoolSize{Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 2} | |
| 58 | + | dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &size} | |
| 59 | + | if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &b.pool)); err != nil { | |
| 60 | + | return err | |
| 61 | + | } | |
| 62 | + | dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: b.pool, DescriptorSetCount: 1, SetLayouts: &b.setLayout} | |
| 63 | + | if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &b.set)); err != nil { | |
| 64 | + | return err | |
| 65 | + | } | |
| 66 | + | vert, err := r.shaderModule(blendVert) | |
| 67 | + | if err != nil { | |
| 68 | + | return err | |
| 69 | + | } | |
| 70 | + | defer d.DestroyShaderModule(r.device, vert, nil) | |
| 71 | + | frag, err := r.shaderModule(blendFrag) | |
| 72 | + | if err != nil { | |
| 73 | + | return err | |
| 74 | + | } | |
| 75 | + | defer d.DestroyShaderModule(r.device, frag, nil) | |
| 76 | + | main := []byte("main\x00") | |
| 77 | + | stages := []vk.PipelineShaderStageCreateInfo{ | |
| 78 | + | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]}, | |
| 79 | + | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]}, | |
| 80 | + | } | |
| 81 | + | vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo} | |
| 82 | + | assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList} | |
| 83 | + | // The target size is fixed for the renderer's life: static viewport. | |
| 84 | + | viewport := vk.Viewport{Width: float32(r.width), Height: float32(r.height), MaxDepth: 1} | |
| 85 | + | scissor := vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}} | |
| 86 | + | viewportState := vk.PipelineViewportStateCreateInfo{SType: vk.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor} | |
| 87 | + | raster := vk.PipelineRasterizationStateCreateInfo{SType: vk.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vk.PolygonModeFill, CullMode: vk.CullModeNone, FrontFace: vk.FrontFaceCounterClockwise, LineWidth: 1} | |
| 88 | + | multisample := vk.PipelineMultisampleStateCreateInfo{SType: vk.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vk.SampleCount1Bit} | |
| 89 | + | attachment := vk.PipelineColorBlendAttachmentState{ | |
| 90 | + | BlendEnable: 1, | |
| 91 | + | SrcColorBlendFactor: vk.BlendFactorOne, DstColorBlendFactor: vk.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vk.BlendOpAdd, | |
| 92 | + | SrcAlphaBlendFactor: vk.BlendFactorOne, DstAlphaBlendFactor: vk.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vk.BlendOpAdd, | |
| 93 | + | ColorWriteMask: vk.ColorComponentRBit | vk.ColorComponentGBit | vk.ColorComponentBBit | vk.ColorComponentABit, | |
| 94 | + | } | |
| 95 | + | blendState := vk.PipelineColorBlendStateCreateInfo{SType: vk.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &attachment} | |
| 96 | + | format := vk.Format(vk.FormatB8g8r8a8Unorm) | |
| 97 | + | rendering := vk.PipelineRenderingCreateInfo{SType: vk.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format} | |
| 98 | + | gpi := vk.GraphicsPipelineCreateInfo{ | |
| 99 | + | SType: vk.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering), | |
| 100 | + | StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly, | |
| 101 | + | ViewportState: &viewportState, RasterizationState: &raster, MultisampleState: &multisample, | |
| 102 | + | ColorBlendState: &blendState, Layout: b.layout, BasePipelineIndex: -1, | |
| 103 | + | } | |
| 104 | + | return checked("vkCreateGraphicsPipelines", d.CreateGraphicsPipelines(r.device, 0, 1, &gpi, nil, &b.pipeline)) | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | func (r *Renderer) shaderModule(code []byte) (vk.ShaderModule, error) { | |
| 108 | + | // SPIR-V is read as words: copy into a uint32-aligned slice. | |
| 109 | + | words := make([]uint32, len(code)/4) | |
| 110 | + | for i := range words { | |
| 111 | + | words[i] = binary.LittleEndian.Uint32(code[i*4:]) | |
| 112 | + | } | |
| 113 | + | info := vk.ShaderModuleCreateInfo{SType: vk.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(words) * 4), Code: &words[0]} | |
| 114 | + | var m vk.ShaderModule | |
| 115 | + | return m, checked("vkCreateShaderModule", r.dd.CreateShaderModule(r.device, &info, nil, &m)) | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | func (r *Renderer) destroyBlend() { | |
| 119 | + | d, b := r.dd, &r.blend | |
| 120 | + | if b.pipeline != 0 { | |
| 121 | + | d.DestroyPipeline(r.device, b.pipeline, nil) | |
| 122 | + | } | |
| 123 | + | if b.pool != 0 { | |
| 124 | + | d.DestroyDescriptorPool(r.device, b.pool, nil) | |
| 125 | + | } | |
| 126 | + | if b.layout != 0 { | |
| 127 | + | d.DestroyPipelineLayout(r.device, b.layout, nil) | |
| 128 | + | } | |
| 129 | + | if b.setLayout != 0 { | |
| 130 | + | d.DestroyDescriptorSetLayout(r.device, b.setLayout, nil) | |
| 131 | + | } | |
| 132 | + | *b = blendPipeline{} | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | // bindStaging points the set at the staging buffer: pixels blended | |
| 136 | + | // pixel bytes from 0, and the draw table of n entries at tableOffset. | |
| 137 | + | func (r *Renderer) bindStaging(pixels, tableOffset, n int) { | |
| 138 | + | infos := []vk.DescriptorBufferInfo{ | |
| 139 | + | {Buffer: r.staging, Range: vk.DeviceSize(max(pixels, 4))}, | |
| 140 | + | {Buffer: r.staging, Offset: vk.DeviceSize(tableOffset), Range: vk.DeviceSize(n * drawSize)}, | |
| 141 | + | } | |
| 142 | + | writes := []vk.WriteDescriptorSet{ | |
| 143 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[0]}, | |
| 144 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[1]}, | |
| 145 | + | } | |
| 146 | + | r.dd.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil) | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | // putDraw writes draw entry i of table. | |
| 150 | + | func (r *Renderer) putDraw(table []byte, i int, u *upload) { | |
| 151 | + | e := table[i*drawSize : (i+1)*drawSize] | |
| 152 | + | for j, v := range []uint32{ | |
| 153 | + | uint32(u.rect.Min.X), uint32(u.rect.Min.Y), uint32(u.rect.Max.X), uint32(u.rect.Max.Y), | |
| 154 | + | uint32(u.offset / 4), uint32(u.rect.Dx()), uint32(r.width), uint32(r.height), | |
| 155 | + | } { | |
| 156 | + | binary.LittleEndian.PutUint32(e[j*4:], v) | |
| 157 | + | } | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | // drawBlended draws uploads[first:last], all blended, into view: one | |
| 161 | + | // instanced draw. The image is TRANSFER_DST before and after. | |
| 162 | + | func (r *Renderer) drawBlended(image vk.Image, view vk.ImageView, first, last int) { | |
| 163 | + | d := r.dd | |
| 164 | + | rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1} | |
| 165 | + | b := vk.ImageMemoryBarrier{ | |
| 166 | + | SType: vk.StructureTypeImageMemoryBarrier, Image: image, SubresourceRange: rangeInfo, | |
| 167 | + | SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessColorAttachmentReadBit | vk.AccessColorAttachmentWriteBit, | |
| 168 | + | OldLayout: vk.ImageLayoutTransferDstOptimal, NewLayout: vk.ImageLayoutColorAttachmentOptimal, | |
| 169 | + | } | |
| 170 | + | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b) | |
| 171 | + | attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpLoad, StoreOp: vk.AttachmentStoreOpStore} | |
| 172 | + | 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} | |
| 173 | + | d.CmdBeginRendering(r.command, &info) | |
| 174 | + | d.CmdBindPipeline(r.command, vk.PipelineBindPointGraphics, r.blend.pipeline) | |
| 175 | + | d.CmdBindDescriptorSets(r.command, vk.PipelineBindPointGraphics, r.blend.layout, 0, 1, &r.blend.set, 0, nil) | |
| 176 | + | d.CmdDraw(r.command, 6, uint32(last-first), 0, uint32(first)) | |
| 177 | + | d.CmdEndRendering(r.command) | |
| 178 | + | b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, vk.AccessTransferWriteBit|vk.AccessTransferReadBit | |
| 179 | + | b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutTransferDstOptimal | |
| 180 | + | d.CmdPipelineBarrier(r.command, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b) | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | // createView makes the color attachment view of a target image. | |
| 184 | + | func (r *Renderer) createView(t *target) error { | |
| 185 | + | info := vk.ImageViewCreateInfo{ | |
| 186 | + | SType: vk.StructureTypeImageViewCreateInfo, Image: t.image, ViewType: vk.ImageViewType2d, Format: vk.FormatB8g8r8a8Unorm, | |
| 187 | + | SubresourceRange: vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}, | |
| 188 | + | } | |
| 189 | + | return checked("vkCreateImageView", r.dd.CreateImageView(r.device, &info, nil, &t.view)) | |
| 190 | + | } | |
| @@ -0,0 +1,190 @@ | |||||
| 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 | + | // 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. | |||
| 20 | + | ||||
| 21 | + | //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.vert -o shaders/blend.vert.spv | |||
| 22 | + | //go:generate glslc -O --target-env=vulkan1.3 shaders/blend.frag -o shaders/blend.frag.spv | |||
| 23 | + | ||||
| 24 | + | var ( | |||
| 25 | + | //go:embed shaders/blend.vert.spv | |||
| 26 | + | blendVert []byte | |||
| 27 | + | //go:embed shaders/blend.frag.spv | |||
| 28 | + | blendFrag []byte | |||
| 29 | + | ) | |||
| 30 | + | ||||
| 31 | + | // drawSize is one entry of the draw table (shaders: struct Draw). | |||
| 32 | + | const drawSize = 32 | |||
| 33 | + | ||||
| 34 | + | // blendPipeline is the graphics state for blended uploads. | |||
| 35 | + | type blendPipeline struct { | |||
| 36 | + | setLayout vk.DescriptorSetLayout | |||
| 37 | + | layout vk.PipelineLayout | |||
| 38 | + | pipeline vk.Pipeline | |||
| 39 | + | pool vk.DescriptorPool | |||
| 40 | + | set vk.DescriptorSet | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func (r *Renderer) createBlend() error { | |||
| 44 | + | d, b := r.dd, &r.blend | |||
| 45 | + | bindings := []vk.DescriptorSetLayoutBinding{ | |||
| 46 | + | {Binding: 0, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}, | |||
| 47 | + | {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit}, | |||
| 48 | + | } | |||
| 49 | + | sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 2, Bindings: &bindings[0]} | |||
| 50 | + | if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &b.setLayout)); err != nil { | |||
| 51 | + | return err | |||
| 52 | + | } | |||
| 53 | + | pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &b.setLayout} | |||
| 54 | + | if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &b.layout)); err != nil { | |||
| 55 | + | return err | |||
| 56 | + | } | |||
| 57 | + | size := vk.DescriptorPoolSize{Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 2} | |||
| 58 | + | dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &size} | |||
| 59 | + | if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &b.pool)); err != nil { | |||
| 60 | + | return err | |||
| 61 | + | } | |||
| 62 | + | dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: b.pool, DescriptorSetCount: 1, SetLayouts: &b.setLayout} | |||
| 63 | + | if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &b.set)); err != nil { | |||
| 64 | + | return err | |||
| 65 | + | } | |||
| 66 | + | vert, err := r.shaderModule(blendVert) | |||
| 67 | + | if err != nil { | |||
| 68 | + | return err | |||
| 69 | + | } | |||
| 70 | + | defer d.DestroyShaderModule(r.device, vert, nil) | |||
| 71 | + | frag, err := r.shaderModule(blendFrag) | |||
| 72 | + | if err != nil { | |||
| 73 | + | return err | |||
| 74 | + | } | |||
| 75 | + | defer d.DestroyShaderModule(r.device, frag, nil) | |||
| 76 | + | main := []byte("main\x00") | |||
| 77 | + | stages := []vk.PipelineShaderStageCreateInfo{ | |||
| 78 | + | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]}, | |||
| 79 | + | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]}, | |||
| 80 | + | } | |||
| 81 | + | vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo} | |||
| 82 | + | assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList} | |||
| 83 | + | // The target size is fixed for the renderer's life: static viewport. | |||
| 84 | + | viewport := vk.Viewport{Width: float32(r.width), Height: float32(r.height), MaxDepth: 1} | |||
| 85 | + | scissor := vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}} | |||
| 86 | + | viewportState := vk.PipelineViewportStateCreateInfo{SType: vk.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor} | |||
| 87 | + | raster := vk.PipelineRasterizationStateCreateInfo{SType: vk.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vk.PolygonModeFill, CullMode: vk.CullModeNone, FrontFace: vk.FrontFaceCounterClockwise, LineWidth: 1} | |||
| 88 | + | multisample := vk.PipelineMultisampleStateCreateInfo{SType: vk.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vk.SampleCount1Bit} | |||
| 89 | + | attachment := vk.PipelineColorBlendAttachmentState{ | |||
| 90 | + | BlendEnable: 1, | |||
| 91 | + | SrcColorBlendFactor: vk.BlendFactorOne, DstColorBlendFactor: vk.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vk.BlendOpAdd, | |||
| 92 | + | SrcAlphaBlendFactor: vk.BlendFactorOne, DstAlphaBlendFactor: vk.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vk.BlendOpAdd, | |||
| 93 | + | ColorWriteMask: vk.ColorComponentRBit | vk.ColorComponentGBit | vk.ColorComponentBBit | vk.ColorComponentABit, | |||
| 94 | + | } | |||
| 95 | + | blendState := vk.PipelineColorBlendStateCreateInfo{SType: vk.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &attachment} | |||
| 96 | + | format := vk.Format(vk.FormatB8g8r8a8Unorm) | |||
| 97 | + | rendering := vk.PipelineRenderingCreateInfo{SType: vk.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format} | |||
| 98 | + | gpi := vk.GraphicsPipelineCreateInfo{ | |||
| 99 | + | SType: vk.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering), | |||
| 100 | + | StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly, | |||
| 101 | + | ViewportState: &viewportState, RasterizationState: &raster, MultisampleState: &multisample, | |||
| 102 | + | ColorBlendState: &blendState, Layout: b.layout, BasePipelineIndex: -1, | |||
| 103 | + | } | |||
| 104 | + | return checked("vkCreateGraphicsPipelines", d.CreateGraphicsPipelines(r.device, 0, 1, &gpi, nil, &b.pipeline)) | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | func (r *Renderer) shaderModule(code []byte) (vk.ShaderModule, error) { | |||
| 108 | + | // SPIR-V is read as words: copy into a uint32-aligned slice. | |||
| 109 | + | words := make([]uint32, len(code)/4) | |||
| 110 | + | for i := range words { | |||
| 111 | + | words[i] = binary.LittleEndian.Uint32(code[i*4:]) | |||
| 112 | + | } | |||
| 113 | + | info := vk.ShaderModuleCreateInfo{SType: vk.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(words) * 4), Code: &words[0]} | |||
| 114 | + | var m vk.ShaderModule | |||
| 115 | + | return m, checked("vkCreateShaderModule", r.dd.CreateShaderModule(r.device, &info, nil, &m)) | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | func (r *Renderer) destroyBlend() { | |||
| 119 | + | d, b := r.dd, &r.blend | |||
| 120 | + | if b.pipeline != 0 { | |||
| 121 | + | d.DestroyPipeline(r.device, b.pipeline, nil) | |||
| 122 | + | } | |||
| 123 | + | if b.pool != 0 { | |||
| 124 | + | d.DestroyDescriptorPool(r.device, b.pool, nil) | |||
| 125 | + | } | |||
| 126 | + | if b.layout != 0 { | |||
| 127 | + | d.DestroyPipelineLayout(r.device, b.layout, nil) | |||
| 128 | + | } | |||
| 129 | + | if b.setLayout != 0 { | |||
| 130 | + | d.DestroyDescriptorSetLayout(r.device, b.setLayout, nil) | |||
| 131 | + | } | |||
| 132 | + | *b = blendPipeline{} | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | // bindStaging points the set at the staging buffer: pixels blended | |||
| 136 | + | // pixel bytes from 0, and the draw table of n entries at tableOffset. | |||
| 137 | + | func (r *Renderer) bindStaging(pixels, tableOffset, n int) { | |||
| 138 | + | infos := []vk.DescriptorBufferInfo{ | |||
| 139 | + | {Buffer: r.staging, Range: vk.DeviceSize(max(pixels, 4))}, | |||
| 140 | + | {Buffer: r.staging, Offset: vk.DeviceSize(tableOffset), Range: vk.DeviceSize(n * drawSize)}, | |||
| 141 | + | } | |||
| 142 | + | writes := []vk.WriteDescriptorSet{ | |||
| 143 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[0]}, | |||
| 144 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[1]}, | |||
| 145 | + | } | |||
| 146 | + | r.dd.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil) | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | // putDraw writes draw entry i of table. | |||
| 150 | + | func (r *Renderer) putDraw(table []byte, i int, u *upload) { | |||
| 151 | + | e := table[i*drawSize : (i+1)*drawSize] | |||
| 152 | + | for j, v := range []uint32{ | |||
| 153 | + | uint32(u.rect.Min.X), uint32(u.rect.Min.Y), uint32(u.rect.Max.X), uint32(u.rect.Max.Y), | |||
| 154 | + | uint32(u.offset / 4), uint32(u.rect.Dx()), uint32(r.width), uint32(r.height), | |||
| 155 | + | } { | |||
| 156 | + | binary.LittleEndian.PutUint32(e[j*4:], v) | |||
| 157 | + | } | |||
| 158 | + | } | |||
| 159 | + | ||||
| 160 | + | // drawBlended draws uploads[first:last], all blended, into view: one | |||
| 161 | + | // instanced draw. The image is TRANSFER_DST before and after. | |||
| 162 | + | func (r *Renderer) drawBlended(image vk.Image, view vk.ImageView, first, last int) { | |||
| 163 | + | d := r.dd | |||
| 164 | + | rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1} | |||
| 165 | + | b := vk.ImageMemoryBarrier{ | |||
| 166 | + | SType: vk.StructureTypeImageMemoryBarrier, Image: image, SubresourceRange: rangeInfo, | |||
| 167 | + | SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessColorAttachmentReadBit | vk.AccessColorAttachmentWriteBit, | |||
| 168 | + | OldLayout: vk.ImageLayoutTransferDstOptimal, NewLayout: vk.ImageLayoutColorAttachmentOptimal, | |||
| 169 | + | } | |||
| 170 | + | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b) | |||
| 171 | + | attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpLoad, StoreOp: vk.AttachmentStoreOpStore} | |||
| 172 | + | 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} | |||
| 173 | + | d.CmdBeginRendering(r.command, &info) | |||
| 174 | + | d.CmdBindPipeline(r.command, vk.PipelineBindPointGraphics, r.blend.pipeline) | |||
| 175 | + | d.CmdBindDescriptorSets(r.command, vk.PipelineBindPointGraphics, r.blend.layout, 0, 1, &r.blend.set, 0, nil) | |||
| 176 | + | d.CmdDraw(r.command, 6, uint32(last-first), 0, uint32(first)) | |||
| 177 | + | d.CmdEndRendering(r.command) | |||
| 178 | + | b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, vk.AccessTransferWriteBit|vk.AccessTransferReadBit | |||
| 179 | + | b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutTransferDstOptimal | |||
| 180 | + | d.CmdPipelineBarrier(r.command, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b) | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | // createView makes the color attachment view of a target image. | |||
| 184 | + | func (r *Renderer) createView(t *target) error { | |||
| 185 | + | info := vk.ImageViewCreateInfo{ | |||
| 186 | + | SType: vk.StructureTypeImageViewCreateInfo, Image: t.image, ViewType: vk.ImageViewType2d, Format: vk.FormatB8g8r8a8Unorm, | |||
| 187 | + | SubresourceRange: vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}, | |||
| 188 | + | } | |||
| 189 | + | return checked("vkCreateImageView", r.dd.CreateImageView(r.device, &info, nil, &t.view)) | |||
| 190 | + | } | |||
M internal/adapters/vulkan/renderer.go +81 -11
| @@ -55,6 +55,11 @@ type Renderer struct { | |||
| 55 | 55 | frame uint64 | |
| 56 | 56 | // fillRegions is reused by Render for the bands of solid fills. | |
| 57 | 57 | 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 | |
| 62 | + | maxRange int // bytes of blended pixels one frame may bind | |
| 58 | 63 | } | |
| 59 | 64 | ||
| 60 | 65 | // fillRows is the height of the strip staged for a solid fill: the CPU | |
| @@ -146,6 +151,10 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 146 | 151 | return | |
| 147 | 152 | } | |
| 148 | 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) | |
| 149 | 158 | r.family = family | |
| 150 | 159 | priority := float32(1) | |
| 151 | 160 | qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority} | |
| @@ -181,7 +190,7 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 181 | 190 | } | |
| 182 | 191 | r.dd.GetDeviceQueue(r.device, family, 0, &r.queue) | |
| 183 | 192 | 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} | |
| 193 | + | 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 | 194 | if err = checked("vkCreateImage", r.dd.CreateImage(r.device, &ii, nil, &r.own.image)); err != nil { | |
| 186 | 195 | return | |
| 187 | 196 | } | |
| @@ -226,7 +235,7 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 226 | 235 | // Start with two frame-sized regions; later frames grow staging on demand. | |
| 227 | 236 | stagingSize := size * 2 | |
| 228 | 237 | r.stagingSize = int(stagingSize) | |
| 229 | - | stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive} | |
| 238 | + | stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: stagingUsage, SharingMode: vk.SharingModeExclusive} | |
| 230 | 239 | if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil { | |
| 231 | 240 | return | |
| 232 | 241 | } | |
| @@ -253,6 +262,12 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 253 | 262 | if err = checked("vkAllocateCommandBuffers", r.dd.AllocateCommandBuffers(r.device, &ai, &r.command)); err != nil { | |
| 254 | 263 | return | |
| 255 | 264 | } | |
| 265 | + | if err = r.createView(&r.own); err != nil { | |
| 266 | + | return | |
| 267 | + | } | |
| 268 | + | if err = r.createBlend(); err != nil { | |
| 269 | + | return | |
| 270 | + | } | |
| 256 | 271 | fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo} | |
| 257 | 272 | if err = checked("vkCreateFence", r.dd.CreateFence(r.device, &fi, nil, &r.fence)); err != nil { | |
| 258 | 273 | return | |
| @@ -315,7 +330,7 @@ func (r *Renderer) ensureStaging(size int) error { | |||
| 315 | 330 | } | |
| 316 | 331 | d := r.dd | |
| 317 | 332 | capacity := size | |
| 318 | - | info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive} | |
| 333 | + | info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: stagingUsage, SharingMode: vk.SharingModeExclusive} | |
| 319 | 334 | var buffer vk.Buffer | |
| 320 | 335 | if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil { | |
| 321 | 336 | return err | |
| @@ -370,12 +385,22 @@ type upload struct { | |||
| 370 | 385 | dst, src image.Rectangle | |
| 371 | 386 | color [3]uint8 | |
| 372 | 387 | opaque bool | |
| 388 | + | // blend draws the staged pixels over the target instead of copying. | |
| 389 | + | blend bool | |
| 373 | 390 | // dma is a client GPU buffer, blitted instead of copied from staging. | |
| 374 | 391 | dma *imported | |
| 375 | 392 | // fill is a solid color: staging holds one strip of rows. | |
| 376 | 393 | fill bool | |
| 377 | 394 | } | |
| 378 | 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 | + | ||
| 379 | 404 | // stage copies the visible pixels of a client buffer into dst, resampling | |
| 380 | 405 | // when the buffer is not drawn at its size. | |
| 381 | 406 | func (u *upload) stage(dst []byte) { | |
| @@ -411,7 +436,6 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 411 | 436 | phys := func(v int) int { return int(math.Round(float64(v) * scale)) } | |
| 412 | 437 | physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) } | |
| 413 | 438 | var uploads []upload | |
| 414 | - | used := 0 | |
| 415 | 439 | bounds := image.Rect(0, 0, r.width, r.height) | |
| 416 | 440 | add := func(rect image.Rectangle, c [3]uint8) { | |
| 417 | 441 | rect = rect.Intersect(bounds) | |
| @@ -420,8 +444,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 420 | 444 | } | |
| 421 | 445 | // A fill stages a strip of up to fillRows rows; the GPU copies the | |
| 422 | 446 | // strip down the rect. | |
| 423 | - | uploads = append(uploads, upload{rect: rect, offset: used, color: c, fill: true}) | |
| 424 | - | used += rect.Dx() * min(rect.Dy(), fillRows) * 4 | |
| 447 | + | uploads = append(uploads, upload{rect: rect, color: c, fill: true}) | |
| 425 | 448 | } | |
| 426 | 449 | // addContent maps src (buffer pixels) onto dst (physical pixels). | |
| 427 | 450 | addContent := func(dst, src image.Rectangle, content *ports.SurfaceContent) { | |
| @@ -443,8 +466,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 443 | 466 | if err != nil { | |
| 444 | 467 | return | |
| 445 | 468 | } | |
| 446 | - | uploads = append(uploads, upload{rect: rect, offset: used, pixels: pixels[b.Offset:], stride: b.Stride, opaque: content.Opaque, dst: dst, src: src}) | |
| 447 | - | 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}) | |
| 448 | 470 | } | |
| 449 | 471 | // drawSurface draws one surface buffer with its origin at (x, y) | |
| 450 | 472 | // logical, clipped to clip (logical). | |
| @@ -561,10 +583,44 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 561 | 583 | addPopups(false) | |
| 562 | 584 | addLayers(true) | |
| 563 | 585 | addPopups(true) | |
| 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 | + | } | |
| 595 | + | if u.blend { | |
| 596 | + | u.offset = used | |
| 597 | + | used += u.size() | |
| 598 | + | blends++ | |
| 599 | + | } | |
| 600 | + | } | |
| 601 | + | pixelBytes := used | |
| 602 | + | tableOffset := (used + r.align - 1) / r.align * r.align | |
| 603 | + | used = tableOffset + blends*drawSize | |
| 604 | + | for k := range uploads { | |
| 605 | + | if u := &uploads[k]; !u.blend && u.dma == nil { | |
| 606 | + | u.offset = used | |
| 607 | + | used += u.size() | |
| 608 | + | } | |
| 609 | + | } | |
| 564 | 610 | if err := r.ensureStaging(used); err != nil { | |
| 565 | 611 | return err | |
| 566 | 612 | } | |
| 567 | 613 | data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize) | |
| 614 | + | if blends > 0 { | |
| 615 | + | r.bindStaging(pixelBytes, tableOffset, blends) | |
| 616 | + | i := 0 | |
| 617 | + | for k := range uploads { | |
| 618 | + | if uploads[k].blend { | |
| 619 | + | r.putDraw(data[tableOffset:], i, &uploads[k]) | |
| 620 | + | i++ | |
| 621 | + | } | |
| 622 | + | } | |
| 623 | + | } | |
| 568 | 624 | for _, u := range uploads { | |
| 569 | 625 | if u.dma != nil { | |
| 570 | 626 | continue | |
| @@ -581,7 +637,6 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 581 | 637 | // then shows garbage for this frame, never a crash. | |
| 582 | 638 | copyGuarded(func() { u.stage(dst) }) | |
| 583 | 639 | } | |
| 584 | - | // B8G8R8A8 pixels are copied unchanged; blending belongs to the compositing pipeline. | |
| 585 | 640 | rgb := parseColor(s.Background) | |
| 586 | 641 | d := r.dd | |
| 587 | 642 | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil { | |
| @@ -643,10 +698,24 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 643 | 698 | } | |
| 644 | 699 | } | |
| 645 | 700 | ownership(true) | |
| 646 | - | for i, f := range uploads { | |
| 647 | - | if i > 0 { | |
| 701 | + | drawn := 0 // blended uploads drawn so far: their index in the draw table | |
| 702 | + | for i := 0; i < len(uploads); i++ { | |
| 703 | + | f := uploads[i] | |
| 704 | + | if i > 0 && !f.blend { | |
| 648 | 705 | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | |
| 649 | 706 | } | |
| 707 | + | if f.blend { | |
| 708 | + | // Consecutive blended uploads share one draw: blending follows | |
| 709 | + | // primitive order within it. | |
| 710 | + | n := 1 | |
| 711 | + | for i+n < len(uploads) && uploads[i+n].blend { | |
| 712 | + | n++ | |
| 713 | + | } | |
| 714 | + | r.drawBlended(tg.image, tg.view, drawn, drawn+n) | |
| 715 | + | drawn += n | |
| 716 | + | i += n - 1 | |
| 717 | + | continue | |
| 718 | + | } | |
| 650 | 719 | rect := f.rect | |
| 651 | 720 | if f.dma != nil { | |
| 652 | 721 | // Map the visible part of dst back onto the buffer. | |
| @@ -860,6 +929,7 @@ func (r *Renderer) Close() { | |||
| 860 | 929 | d.FreeMemory(r.device, r.bufferMemory, nil) | |
| 861 | 930 | r.bufferMemory = 0 | |
| 862 | 931 | } | |
| 932 | + | r.destroyBlend() | |
| 863 | 933 | r.freeTarget(&r.own) | |
| 864 | 934 | d.DestroyDevice(r.device, nil) | |
| 865 | 935 | r.device = 0 | |
| @@ -55,6 +55,11 @@ type Renderer struct { | |||||
| 55 | frame uint64 | 55 | 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.BufferImageCopy | 57 | 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 | |||
| 62 | + | maxRange int // bytes of blended pixels one frame may bind | |||
| 58 | } | 63 | } | ||
| 59 | 64 | ||||
| 60 | // 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 | ||
| @@ -146,6 +151,10 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 146 | return | 151 | return | ||
| 147 | } | 152 | } | ||
| 148 | 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) | |||
| 149 | r.family = family | 158 | r.family = family | ||
| 150 | priority := float32(1) | 159 | priority := float32(1) | ||
| 151 | 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} | ||
| @@ -181,7 +190,7 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 181 | } | 190 | } | ||
| 182 | r.dd.GetDeviceQueue(r.device, family, 0, &r.queue) | 191 | r.dd.GetDeviceQueue(r.device, family, 0, &r.queue) | ||
| 183 | extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1} | 192 | 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} | 193 | + | 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 { | 194 | if err = checked("vkCreateImage", r.dd.CreateImage(r.device, &ii, nil, &r.own.image)); err != nil { | ||
| 186 | return | 195 | return | ||
| 187 | } | 196 | } | ||
| @@ -226,7 +235,7 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 226 | // Start with two frame-sized regions; later frames grow staging on demand. | 235 | // Start with two frame-sized regions; later frames grow staging on demand. | ||
| 227 | stagingSize := size * 2 | 236 | stagingSize := size * 2 | ||
| 228 | r.stagingSize = int(stagingSize) | 237 | r.stagingSize = int(stagingSize) | ||
| 229 | - | stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive} | 238 | + | 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 { | 239 | if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil { | ||
| 231 | return | 240 | return | ||
| 232 | } | 241 | } | ||
| @@ -253,6 +262,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 { | 262 | if err = checked("vkAllocateCommandBuffers", r.dd.AllocateCommandBuffers(r.device, &ai, &r.command)); err != nil { | ||
| 254 | return | 263 | return | ||
| 255 | } | 264 | } | ||
| 265 | + | if err = r.createView(&r.own); err != nil { | |||
| 266 | + | return | |||
| 267 | + | } | |||
| 268 | + | if err = r.createBlend(); err != nil { | |||
| 269 | + | return | |||
| 270 | + | } | |||
| 256 | fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo} | 271 | fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo} | ||
| 257 | if err = checked("vkCreateFence", r.dd.CreateFence(r.device, &fi, nil, &r.fence)); err != nil { | 272 | if err = checked("vkCreateFence", r.dd.CreateFence(r.device, &fi, nil, &r.fence)); err != nil { | ||
| 258 | return | 273 | return | ||
| @@ -315,7 +330,7 @@ func (r *Renderer) ensureStaging(size int) error { | |||||
| 315 | } | 330 | } | ||
| 316 | d := r.dd | 331 | d := r.dd | ||
| 317 | capacity := size | 332 | capacity := size | ||
| 318 | - | info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive} | 333 | + | info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: stagingUsage, SharingMode: vk.SharingModeExclusive} |
| 319 | var buffer vk.Buffer | 334 | var buffer vk.Buffer | ||
| 320 | if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil { | 335 | if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil { | ||
| 321 | return err | 336 | return err | ||
| @@ -370,12 +385,22 @@ type upload struct { | |||||
| 370 | dst, src image.Rectangle | 385 | dst, src image.Rectangle | ||
| 371 | color [3]uint8 | 386 | color [3]uint8 | ||
| 372 | opaque bool | 387 | opaque bool | ||
| 388 | + | // blend draws the staged pixels over the target instead of copying. | |||
| 389 | + | blend bool | |||
| 373 | // dma is a client GPU buffer, blitted instead of copied from staging. | 390 | // dma is a client GPU buffer, blitted instead of copied from staging. | ||
| 374 | dma *imported | 391 | dma *imported | ||
| 375 | // fill is a solid color: staging holds one strip of rows. | 392 | // fill is a solid color: staging holds one strip of rows. | ||
| 376 | fill bool | 393 | fill bool | ||
| 377 | } | 394 | } | ||
| 378 | 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 | + | ||||
| 379 | // 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 | ||
| 380 | // when the buffer is not drawn at its size. | 405 | // when the buffer is not drawn at its size. | ||
| 381 | func (u *upload) stage(dst []byte) { | 406 | func (u *upload) stage(dst []byte) { | ||
| @@ -411,7 +436,6 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 411 | 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)) } | ||
| 412 | 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)) } | ||
| 413 | var uploads []upload | 438 | var uploads []upload | ||
| 414 | - | used := 0 | |||
| 415 | bounds := image.Rect(0, 0, r.width, r.height) | 439 | bounds := image.Rect(0, 0, r.width, r.height) | ||
| 416 | add := func(rect image.Rectangle, c [3]uint8) { | 440 | add := func(rect image.Rectangle, c [3]uint8) { | ||
| 417 | rect = rect.Intersect(bounds) | 441 | rect = rect.Intersect(bounds) | ||
| @@ -420,8 +444,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 420 | } | 444 | } | ||
| 421 | // 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 | ||
| 422 | // strip down the rect. | 446 | // strip down the rect. | ||
| 423 | - | uploads = append(uploads, upload{rect: rect, offset: used, color: c, fill: true}) | 447 | + | uploads = append(uploads, upload{rect: rect, color: c, fill: true}) |
| 424 | - | used += rect.Dx() * min(rect.Dy(), fillRows) * 4 | |||
| 425 | } | 448 | } | ||
| 426 | // addContent maps src (buffer pixels) onto dst (physical pixels). | 449 | // addContent maps src (buffer pixels) onto dst (physical pixels). | ||
| 427 | addContent := func(dst, src image.Rectangle, content *ports.SurfaceContent) { | 450 | addContent := func(dst, src image.Rectangle, content *ports.SurfaceContent) { | ||
| @@ -443,8 +466,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 443 | if err != nil { | 466 | if err != nil { | ||
| 444 | return | 467 | return | ||
| 445 | } | 468 | } | ||
| 446 | - | uploads = append(uploads, upload{rect: rect, offset: used, pixels: pixels[b.Offset:], stride: b.Stride, opaque: 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}) |
| 447 | - | used += rect.Dx() * rect.Dy() * 4 | |||
| 448 | } | 470 | } | ||
| 449 | // drawSurface draws one surface buffer with its origin at (x, y) | 471 | // drawSurface draws one surface buffer with its origin at (x, y) | ||
| 450 | // logical, clipped to clip (logical). | 472 | // logical, clipped to clip (logical). | ||
| @@ -561,10 +583,44 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 561 | addPopups(false) | 583 | addPopups(false) | ||
| 562 | addLayers(true) | 584 | addLayers(true) | ||
| 563 | addPopups(true) | 585 | addPopups(true) | ||
| 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 | + | } | |||
| 595 | + | if u.blend { | |||
| 596 | + | u.offset = used | |||
| 597 | + | used += u.size() | |||
| 598 | + | blends++ | |||
| 599 | + | } | |||
| 600 | + | } | |||
| 601 | + | pixelBytes := used | |||
| 602 | + | tableOffset := (used + r.align - 1) / r.align * r.align | |||
| 603 | + | used = tableOffset + blends*drawSize | |||
| 604 | + | for k := range uploads { | |||
| 605 | + | if u := &uploads[k]; !u.blend && u.dma == nil { | |||
| 606 | + | u.offset = used | |||
| 607 | + | used += u.size() | |||
| 608 | + | } | |||
| 609 | + | } | |||
| 564 | if err := r.ensureStaging(used); err != nil { | 610 | if err := r.ensureStaging(used); err != nil { | ||
| 565 | return err | 611 | return err | ||
| 566 | } | 612 | } | ||
| 567 | data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize) | 613 | data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize) | ||
| 614 | + | if blends > 0 { | |||
| 615 | + | r.bindStaging(pixelBytes, tableOffset, blends) | |||
| 616 | + | i := 0 | |||
| 617 | + | for k := range uploads { | |||
| 618 | + | if uploads[k].blend { | |||
| 619 | + | r.putDraw(data[tableOffset:], i, &uploads[k]) | |||
| 620 | + | i++ | |||
| 621 | + | } | |||
| 622 | + | } | |||
| 623 | + | } | |||
| 568 | for _, u := range uploads { | 624 | for _, u := range uploads { | ||
| 569 | if u.dma != nil { | 625 | if u.dma != nil { | ||
| 570 | continue | 626 | continue | ||
| @@ -581,7 +637,6 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 581 | // then shows garbage for this frame, never a crash. | 637 | // then shows garbage for this frame, never a crash. | ||
| 582 | copyGuarded(func() { u.stage(dst) }) | 638 | copyGuarded(func() { u.stage(dst) }) | ||
| 583 | } | 639 | } | ||
| 584 | - | // B8G8R8A8 pixels are copied unchanged; blending belongs to the compositing pipeline. | |||
| 585 | rgb := parseColor(s.Background) | 640 | rgb := parseColor(s.Background) | ||
| 586 | d := r.dd | 641 | d := r.dd | ||
| 587 | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil { | 642 | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil { | ||
| @@ -643,10 +698,24 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 643 | } | 698 | } | ||
| 644 | } | 699 | } | ||
| 645 | ownership(true) | 700 | ownership(true) | ||
| 646 | - | for i, f := range uploads { | 701 | + | drawn := 0 // blended uploads drawn so far: their index in the draw table |
| 647 | - | if i > 0 { | 702 | + | for i := 0; i < len(uploads); i++ { |
| 703 | + | f := uploads[i] | |||
| 704 | + | if i > 0 && !f.blend { | |||
| 648 | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | 705 | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | ||
| 649 | } | 706 | } | ||
| 707 | + | if f.blend { | |||
| 708 | + | // Consecutive blended uploads share one draw: blending follows | |||
| 709 | + | // primitive order within it. | |||
| 710 | + | n := 1 | |||
| 711 | + | for i+n < len(uploads) && uploads[i+n].blend { | |||
| 712 | + | n++ | |||
| 713 | + | } | |||
| 714 | + | r.drawBlended(tg.image, tg.view, drawn, drawn+n) | |||
| 715 | + | drawn += n | |||
| 716 | + | i += n - 1 | |||
| 717 | + | continue | |||
| 718 | + | } | |||
| 650 | rect := f.rect | 719 | rect := f.rect | ||
| 651 | if f.dma != nil { | 720 | if f.dma != nil { | ||
| 652 | // Map the visible part of dst back onto the buffer. | 721 | // Map the visible part of dst back onto the buffer. | ||
| @@ -860,6 +929,7 @@ func (r *Renderer) Close() { | |||||
| 860 | d.FreeMemory(r.device, r.bufferMemory, nil) | 929 | d.FreeMemory(r.device, r.bufferMemory, nil) | ||
| 861 | r.bufferMemory = 0 | 930 | r.bufferMemory = 0 | ||
| 862 | } | 931 | } | ||
| 932 | + | r.destroyBlend() | |||
| 863 | r.freeTarget(&r.own) | 933 | r.freeTarget(&r.own) | ||
| 864 | d.DestroyDevice(r.device, nil) | 934 | d.DestroyDevice(r.device, nil) | ||
| 865 | r.device = 0 | 935 | r.device = 0 | ||
M internal/adapters/vulkan/renderer_test.go +62 -0
| @@ -364,3 +364,65 @@ func TestRendererTallFill(t *testing.T) { | |||
| 364 | 364 | } | |
| 365 | 365 | } | |
| 366 | 366 | } | |
| 367 | + | ||
| 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. | |
| 380 | + | func TestRendererAlphaBlend(t *testing.T) { | |
| 381 | + | r, err := New(64, 48) | |
| 382 | + | if err != nil { | |
| 383 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 384 | + | } | |
| 385 | + | defer r.Close() | |
| 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)), | |
| 398 | + | } | |
| 399 | + | scene := ports.Scene{Background: "#000000", | |
| 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 | + | }} | |
| 406 | + | if err := r.Render(scene, contents); err != nil { | |
| 407 | + | t.Fatal(err) | |
| 408 | + | } | |
| 409 | + | px := r.Pixels() | |
| 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 | + | } | |
| 427 | + | } | |
| 428 | + | } | |
| @@ -364,3 +364,65 @@ func TestRendererTallFill(t *testing.T) { | |||||
| 364 | } | 364 | } | ||
| 365 | } | 365 | } | ||
| 366 | } | 366 | } | ||
| 367 | + | ||||
| 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. | |||
| 380 | + | func TestRendererAlphaBlend(t *testing.T) { | |||
| 381 | + | r, err := New(64, 48) | |||
| 382 | + | if err != nil { | |||
| 383 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 384 | + | } | |||
| 385 | + | defer r.Close() | |||
| 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)), | |||
| 398 | + | } | |||
| 399 | + | scene := ports.Scene{Background: "#000000", | |||
| 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 | + | }} | |||
| 406 | + | if err := r.Render(scene, contents); err != nil { | |||
| 407 | + | t.Fatal(err) | |||
| 408 | + | } | |||
| 409 | + | px := r.Pixels() | |||
| 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 | + | } | |||
| 427 | + | } | |||
| 428 | + | } | |||
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 +16 -2
| @@ -24,6 +24,7 @@ type target struct { | |||
| 24 | 24 | image vk.Image | |
| 25 | 25 | memory vk.DeviceMemory | |
| 26 | 26 | layout vk.ImageLayout | |
| 27 | + | view vk.ImageView // color attachment for blended draws | |
| 27 | 28 | // exported images are shared with the display: ownership goes to the | |
| 28 | 29 | // foreign queue family after each frame, and KMS reads them. | |
| 29 | 30 | exported bool | |
| @@ -104,7 +105,9 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) { | |||
| 104 | 105 | mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount) | |
| 105 | 106 | list.DrmFormatModifierProperties = &mods[0] | |
| 106 | 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) | |
| 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. | |
| 108 | 111 | for _, m := range mods[:list.DrmFormatModifierCount] { | |
| 109 | 112 | if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) { | |
| 110 | 113 | r.renderMods = append(r.renderMods, m.DrmFormatModifier) | |
| @@ -131,7 +134,11 @@ const ( | |||
| 131 | 134 | formatFeatureTransferDst = 1 << 15 | |
| 132 | 135 | ) | |
| 133 | 136 | ||
| 134 | - | const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit | |
| 137 | + | // Targets are copied into and drawn on (blend.go). | |
| 138 | + | const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit | vk.ImageUsageColorAttachmentBit | |
| 139 | + | ||
| 140 | + | // Staging feeds copies and, as a storage buffer, the blend shaders. | |
| 141 | + | const stagingUsage = vk.BufferUsageTransferSrcBit | vk.BufferUsageStorageBufferBit | |
| 135 | 142 | ||
| 136 | 143 | // exportTarget creates one exported image with a modifier from mods. | |
| 137 | 144 | func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) { | |
| @@ -171,6 +178,9 @@ func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) { | |||
| 171 | 178 | var layout vk.SubresourceLayout | |
| 172 | 179 | sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT} | |
| 173 | 180 | d.GetImageSubresourceLayout(r.device, t.image, &sub, &layout) | |
| 181 | + | if err := r.createView(t); err != nil { | |
| 182 | + | return nil, ports.DMABuf{}, err | |
| 183 | + | } | |
| 174 | 184 | var fd int32 | |
| 175 | 185 | get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: t.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | |
| 176 | 186 | if err := checked("vkGetMemoryFdKHR", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil { | |
| @@ -186,6 +196,10 @@ func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) { | |||
| 186 | 196 | } | |
| 187 | 197 | ||
| 188 | 198 | func (r *Renderer) freeTarget(t *target) { | |
| 199 | + | if t.view != 0 { | |
| 200 | + | r.dd.DestroyImageView(r.device, t.view, nil) | |
| 201 | + | t.view = 0 | |
| 202 | + | } | |
| 189 | 203 | if t.image != 0 { | |
| 190 | 204 | r.dd.DestroyImage(r.device, t.image, nil) | |
| 191 | 205 | t.image = 0 | |
| @@ -24,6 +24,7 @@ type target struct { | |||||
| 24 | image vk.Image | 24 | image vk.Image | ||
| 25 | memory vk.DeviceMemory | 25 | memory vk.DeviceMemory | ||
| 26 | layout vk.ImageLayout | 26 | layout vk.ImageLayout | ||
| 27 | + | view vk.ImageView // color attachment for blended draws | |||
| 27 | // exported images are shared with the display: ownership goes to the | 28 | // 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 bool | 30 | exported bool | ||
| @@ -104,7 +105,9 @@ 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) |
| 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. | |||
| 108 | for _, m := range mods[:list.DrmFormatModifierCount] { | 111 | for _, m := range mods[:list.DrmFormatModifierCount] { | ||
| 109 | 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) { | ||
| 110 | r.renderMods = append(r.renderMods, m.DrmFormatModifier) | 113 | r.renderMods = append(r.renderMods, m.DrmFormatModifier) | ||
| @@ -131,7 +134,11 @@ const ( | |||||
| 131 | formatFeatureTransferDst = 1 << 15 | 134 | formatFeatureTransferDst = 1 << 15 | ||
| 132 | ) | 135 | ) | ||
| 133 | 136 | ||||
| 134 | - | const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit | 137 | + | // Targets are copied into and drawn on (blend.go). |
| 138 | + | const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit | vk.ImageUsageColorAttachmentBit | |||
| 139 | + | ||||
| 140 | + | // Staging feeds copies and, as a storage buffer, the blend shaders. | |||
| 141 | + | const stagingUsage = vk.BufferUsageTransferSrcBit | vk.BufferUsageStorageBufferBit | |||
| 135 | 142 | ||||
| 136 | // exportTarget creates one exported image with a modifier from mods. | 143 | // exportTarget creates one exported image with a modifier from mods. | ||
| 137 | func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) { | 144 | func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) { | ||
| @@ -171,6 +178,9 @@ func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) { | |||||
| 171 | var layout vk.SubresourceLayout | 178 | var layout vk.SubresourceLayout | ||
| 172 | sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT} | 179 | sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT} | ||
| 173 | d.GetImageSubresourceLayout(r.device, t.image, &sub, &layout) | 180 | d.GetImageSubresourceLayout(r.device, t.image, &sub, &layout) | ||
| 181 | + | if err := r.createView(t); err != nil { | |||
| 182 | + | return nil, ports.DMABuf{}, err | |||
| 183 | + | } | |||
| 174 | var fd int32 | 184 | var fd int32 | ||
| 175 | get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: t.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT} | 185 | 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 { | 186 | if err := checked("vkGetMemoryFdKHR", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil { | ||
| @@ -186,6 +196,10 @@ func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) { | |||||
| 186 | } | 196 | } | ||
| 187 | 197 | ||||
| 188 | func (r *Renderer) freeTarget(t *target) { | 198 | func (r *Renderer) freeTarget(t *target) { | ||
| 199 | + | if t.view != 0 { | |||
| 200 | + | r.dd.DestroyImageView(r.device, t.view, nil) | |||
| 201 | + | t.view = 0 | |||
| 202 | + | } | |||
| 189 | if t.image != 0 { | 203 | if t.image != 0 { | ||
| 190 | r.dd.DestroyImage(r.device, t.image, nil) | 204 | r.dd.DestroyImage(r.device, t.image, nil) | ||
| 191 | t.image = 0 | 205 | t.image = 0 | ||