perf(vulkan): compose each pass as batched instanced draws
Draw parameters move from push constants to a frame-slot storage buffer read by gl_InstanceIndex. With nonuniform descriptor indexing, one batch binds a table of client resources; otherwise consecutive draws of one resource share a batch. A pass now costs one bind and one draw per batch instead of three calls per quad. Closes #44
- commit
- 8624a645d564bd9377d6e7d3426e347d55ed0475
- author
- brice · · 2026-10-14 21:04 UTC
- parent
- ea46f39
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
23 files · +730 -134
M docs/performance.md +1 -0
| @@ -3,6 +3,7 @@ | |||
| 3 | 3 | NeferWL uses about 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change. | |
| 4 | 4 | ||
| 5 | 5 | - Only changed areas are redrawn. Windows nobody sees get one frame callback per second. | |
| 6 | + | - Windows are composed as instanced draws, so a frame makes a few Vulkan calls whatever the number of windows. | |
| 6 | 7 | - Fullscreen windows go straight to the display (direct scanout), with tearing for games. See `render.direct-scanout` and `render.tearing` in [Configuration](config.md#keys). | |
| 7 | 8 | - A full frame at 2560×1600 takes about 0.16 ms on an RX 9070 XT and 3 ms on a Radeon 680M. Measure yours with `go test ./internal/adapters/vulkan -bench ComposeFullTarget` (`NEFERWL_VK_DEVICE` picks the GPU). | |
| 8 | 9 | - NeferWL runs with `GOGC=100` and a 256 MiB soft memory limit. `GOGC` and `GOMEMLIMIT` from the environment override them. | |
| @@ -3,6 +3,7 @@ | |||||
| 3 | NeferWL uses about 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change. | 3 | NeferWL uses about 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change. | ||
| 4 | 4 | ||||
| 5 | - Only changed areas are redrawn. Windows nobody sees get one frame callback per second. | 5 | - Only changed areas are redrawn. Windows nobody sees get one frame callback per second. | ||
| 6 | + | - Windows are composed as instanced draws, so a frame makes a few Vulkan calls whatever the number of windows. | |||
| 6 | - Fullscreen windows go straight to the display (direct scanout), with tearing for games. See `render.direct-scanout` and `render.tearing` in [Configuration](config.md#keys). | 7 | - Fullscreen windows go straight to the display (direct scanout), with tearing for games. See `render.direct-scanout` and `render.tearing` in [Configuration](config.md#keys). | ||
| 7 | - A full frame at 2560×1600 takes about 0.16 ms on an RX 9070 XT and 3 ms on a Radeon 680M. Measure yours with `go test ./internal/adapters/vulkan -bench ComposeFullTarget` (`NEFERWL_VK_DEVICE` picks the GPU). | 8 | - A full frame at 2560×1600 takes about 0.16 ms on an RX 9070 XT and 3 ms on a Radeon 680M. Measure yours with `go test ./internal/adapters/vulkan -bench ComposeFullTarget` (`NEFERWL_VK_DEVICE` picks the GPU). | ||
| 8 | - NeferWL runs with `GOGC=100` and a 256 MiB soft memory limit. `GOGC` and `GOMEMLIMIT` from the environment override them. | 9 | - NeferWL runs with `GOGC=100` and a 256 MiB soft memory limit. `GOGC` and `GOMEMLIMIT` from the environment override them. | ||
M internal/adapters/vulkan/capture.go +1 -1
| @@ -673,7 +673,7 @@ func (r *Renderer) createCapturePass() (err error) { | |||
| 673 | 673 | write := vk.WriteDescriptorSet{SType: vk.StructureTypeWriteDescriptorSet, DstSet: p.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img} | |
| 674 | 674 | d.UpdateDescriptorSets(r.device, 1, &write, 0, nil) | |
| 675 | 675 | // The pipeline is last: it marks the pass complete. | |
| 676 | - | return r.createGraphicsPipeline(hdrVert, captureFrag, vk.FormatB8g8r8a8Unorm, p.layout, false, &p.pipeline) | |
| 676 | + | return r.createGraphicsPipeline(hdrVert, captureFrag, vk.FormatB8g8r8a8Unorm, p.layout, false, nil, &p.pipeline) | |
| 677 | 677 | } | |
| 678 | 678 | ||
| 679 | 679 | // destroyCapturePass frees the conversion pipeline (device idle). | |
| @@ -673,7 +673,7 @@ func (r *Renderer) createCapturePass() (err error) { | |||||
| 673 | write := vk.WriteDescriptorSet{SType: vk.StructureTypeWriteDescriptorSet, DstSet: p.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img} | 673 | write := vk.WriteDescriptorSet{SType: vk.StructureTypeWriteDescriptorSet, DstSet: p.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img} | ||
| 674 | d.UpdateDescriptorSets(r.device, 1, &write, 0, nil) | 674 | d.UpdateDescriptorSets(r.device, 1, &write, 0, nil) | ||
| 675 | // The pipeline is last: it marks the pass complete. | 675 | // The pipeline is last: it marks the pass complete. | ||
| 676 | - | return r.createGraphicsPipeline(hdrVert, captureFrag, vk.FormatB8g8r8a8Unorm, p.layout, false, &p.pipeline) | 676 | + | return r.createGraphicsPipeline(hdrVert, captureFrag, vk.FormatB8g8r8a8Unorm, p.layout, false, nil, &p.pipeline) |
| 677 | } | 677 | } | ||
| 678 | 678 | ||||
| 679 | // destroyCapturePass frees the conversion pipeline (device idle). | 679 | // destroyCapturePass frees the conversion pipeline (device idle). | ||
M internal/adapters/vulkan/compose.go +45 -70
| @@ -12,16 +12,16 @@ import ( | |||
| 12 | 12 | vk "github.com/bnema/purego-vulkan/vulkan" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| 15 | - | // Composition is one graphics pipeline drawing every piece of a frame as | |
| 16 | - | // a quad, in paint order, inside one dynamic rendering pass: solid fills, | |
| 17 | - | // client dmabufs sampled as images, and client wl_shm pixels read from a | |
| 18 | - | // GPU buffer (shm.go). Push constants carry each draw; the descriptor set | |
| 19 | - | // holds its image and buffer. Client pixels are premultiplied, hence the | |
| 20 | - | // blend ONE, ONE_MINUS_SRC_ALPHA; opaque draws force alpha to 1. | |
| 15 | + | // Composition draws quads in paint order inside one dynamic rendering | |
| 16 | + | // pass. Each batch binds a frame-slot-owned draw buffer and resource table, | |
| 17 | + | // then draws all its quads as instances. Client pixels are premultiplied, | |
| 18 | + | // hence the blend ONE, ONE_MINUS_SRC_ALPHA; opaque draws force alpha to 1. | |
| 21 | 19 | ||
| 22 | 20 | //go:generate glslc -O --target-env=vulkan1.3 shaders/compose.vert -o shaders/compose.vert.spv | |
| 23 | 21 | //go:generate glslc -O --target-env=vulkan1.3 shaders/compose.frag -o shaders/compose.frag.spv | |
| 24 | 22 | //go:generate glslc -O --target-env=vulkan1.3 shaders/compose_hdr.frag -o shaders/compose_hdr.frag.spv | |
| 23 | + | //go:generate glslc -O --target-env=vulkan1.3 -DSINGLE_RESOURCE shaders/compose.frag -o shaders/compose_single.frag.spv | |
| 24 | + | //go:generate glslc -O --target-env=vulkan1.3 -DSINGLE_RESOURCE shaders/compose_hdr.frag -o shaders/compose_hdr_single.frag.spv | |
| 25 | 25 | //go:generate glslc -O --target-env=vulkan1.3 shaders/hdr.vert -o shaders/hdr.vert.spv | |
| 26 | 26 | //go:generate glslc -O --target-env=vulkan1.3 shaders/hdr.frag -o shaders/hdr.frag.spv | |
| 27 | 27 | ||
| @@ -32,6 +32,10 @@ var ( | |||
| 32 | 32 | composeFrag []byte | |
| 33 | 33 | //go:embed shaders/compose_hdr.frag.spv | |
| 34 | 34 | composeHDRFrag []byte | |
| 35 | + | //go:embed shaders/compose_single.frag.spv | |
| 36 | + | composeSingleFrag []byte | |
| 37 | + | //go:embed shaders/compose_hdr_single.frag.spv | |
| 38 | + | composeHDRSingleFrag []byte | |
| 35 | 39 | ) | |
| 36 | 40 | ||
| 37 | 41 | // Draw modes and flags (shaders: modeSolid, modeImage, flag*). | |
| @@ -51,8 +55,8 @@ const ( | |||
| 51 | 55 | flagSmooth = 64 | |
| 52 | 56 | ) | |
| 53 | 57 | ||
| 54 | - | // pushConstants is struct Draw of the shaders (std430 push constant block). | |
| 55 | - | type pushConstants struct { | |
| 58 | + | // drawParameters is the parameter portion of the shaders' std430 Draw. | |
| 59 | + | type drawParameters struct { | |
| 56 | 60 | rect [4]int32 | |
| 57 | 61 | mapv [4]float32 | |
| 58 | 62 | mapy [4]float32 | |
| @@ -64,8 +68,8 @@ type pushConstants struct { | |||
| 64 | 68 | ||
| 65 | 69 | // draw is one quad of a frame. | |
| 66 | 70 | type draw struct { | |
| 67 | - | pc pushConstants | |
| 68 | - | set vk.DescriptorSet | |
| 71 | + | pc drawParameters | |
| 72 | + | shm *shmCopy | |
| 69 | 73 | im *imported // sampled client dmabuf, for ownership barriers | |
| 70 | 74 | // acquire is the content's explicit-sync fence (nil: implicit). | |
| 71 | 75 | acquire *os.File | |
| @@ -84,23 +88,25 @@ type composer struct { | |||
| 84 | 88 | dummyMemory vk.DeviceMemory | |
| 85 | 89 | dummyView vk.ImageView | |
| 86 | 90 | dummyBuffer gpuBuffer | |
| 87 | - | dummyPool vk.DescriptorPool | |
| 88 | - | dummySet vk.DescriptorSet | |
| 91 | + | // resources is the number of resource slots per batch (one without | |
| 92 | + | // nonuniform descriptor indexing); indexing selects the shader variant. | |
| 93 | + | resources uint32 | |
| 94 | + | indexing bool | |
| 89 | 95 | } | |
| 90 | 96 | ||
| 91 | 97 | func (r *Renderer) createComposer() error { | |
| 92 | 98 | d, c := r.dd, &r.compose | |
| 93 | 99 | bindings := []vk.DescriptorSetLayoutBinding{ | |
| 94 | - | {Binding: 0, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}, | |
| 95 | - | {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}, | |
| 96 | - | {Binding: 2, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}, | |
| 100 | + | {Binding: 0, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: c.resources, StageFlags: vk.ShaderStageFragmentBit}, | |
| 101 | + | {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: c.resources, StageFlags: vk.ShaderStageFragmentBit}, | |
| 102 | + | {Binding: 2, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: c.resources, StageFlags: vk.ShaderStageFragmentBit}, | |
| 103 | + | {Binding: 3, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageVertexBit}, | |
| 97 | 104 | } | |
| 98 | 105 | sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: uint32(len(bindings)), Bindings: &bindings[0]} | |
| 99 | 106 | if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &c.setLayout)); err != nil { | |
| 100 | 107 | return err | |
| 101 | 108 | } | |
| 102 | - | pcr := vk.PushConstantRange{StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit, Size: uint32(unsafe.Sizeof(pushConstants{}))} | |
| 103 | - | pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &c.setLayout, PushConstantRangeCount: 1, PushConstantRanges: &pcr} | |
| 109 | + | pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &c.setLayout} | |
| 104 | 110 | if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &c.layout)); err != nil { | |
| 105 | 111 | return err | |
| 106 | 112 | } | |
| @@ -115,13 +121,24 @@ func (r *Renderer) createComposer() error { | |||
| 115 | 121 | } | |
| 116 | 122 | ||
| 117 | 123 | func (r *Renderer) createPipeline() error { | |
| 118 | - | if err := r.createGraphicsPipeline(composeVert, composeFrag, vk.FormatB8g8r8a8Unorm, r.compose.layout, true, &r.compose.pipeline); err != nil { | |
| 124 | + | fragment := composeFrag | |
| 125 | + | if !r.compose.indexing { | |
| 126 | + | fragment = composeSingleFrag | |
| 127 | + | } | |
| 128 | + | if err := r.createComposePipeline(fragment, vk.FormatB8g8r8a8Unorm, &r.compose.pipeline); err != nil { | |
| 119 | 129 | return err | |
| 120 | 130 | } | |
| 121 | 131 | return nil | |
| 122 | 132 | } | |
| 123 | 133 | ||
| 124 | - | func (r *Renderer) createGraphicsPipeline(vertex, fragment []byte, format vk.Format, layout vk.PipelineLayout, blend bool, pipeline *vk.Pipeline) error { | |
| 134 | + | func (r *Renderer) createComposePipeline(fragment []byte, format vk.Format, pipeline *vk.Pipeline) error { | |
| 135 | + | resourceCount := r.compose.resources | |
| 136 | + | entry := vk.SpecializationMapEntry{ConstantID: 0, Size: 4} | |
| 137 | + | specialization := vk.SpecializationInfo{MapEntryCount: 1, MapEntries: &entry, DataSize: 4, Data: unsafe.Pointer(&resourceCount)} | |
| 138 | + | return r.createGraphicsPipeline(composeVert, fragment, format, r.compose.layout, true, &specialization, pipeline) | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | func (r *Renderer) createGraphicsPipeline(vertex, fragment []byte, format vk.Format, layout vk.PipelineLayout, blend bool, specialization *vk.SpecializationInfo, pipeline *vk.Pipeline) error { | |
| 125 | 142 | d := r.dd | |
| 126 | 143 | vert, err := r.shaderModule(vertex) | |
| 127 | 144 | if err != nil { | |
| @@ -136,7 +153,7 @@ func (r *Renderer) createGraphicsPipeline(vertex, fragment []byte, format vk.For | |||
| 136 | 153 | main := []byte("main\x00") | |
| 137 | 154 | stages := []vk.PipelineShaderStageCreateInfo{ | |
| 138 | 155 | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]}, | |
| 139 | - | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]}, | |
| 156 | + | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0], SpecializationInfo: specialization}, | |
| 140 | 157 | } | |
| 141 | 158 | vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo} | |
| 142 | 159 | assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList} | |
| @@ -167,7 +184,7 @@ func (r *Renderer) createGraphicsPipeline(vertex, fragment []byte, format vk.For | |||
| 167 | 184 | } | |
| 168 | 185 | ||
| 169 | 186 | // createDummies makes the placeholder image (1×1, shader-read layout) and | |
| 170 | - | // buffer, and the set fills bind. | |
| 187 | + | // buffer used by resource-table entries a draw does not sample. | |
| 171 | 188 | func (r *Renderer) createDummies() error { | |
| 172 | 189 | d, c := r.dd, &r.compose | |
| 173 | 190 | ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: 1, Height: 1, Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageSampledBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined} | |
| @@ -193,9 +210,6 @@ func (r *Renderer) createDummies() error { | |||
| 193 | 210 | if err := r.newGPUBuffer(&c.dummyBuffer, 16); err != nil { | |
| 194 | 211 | return err | |
| 195 | 212 | } | |
| 196 | - | if c.dummyPool, c.dummySet, err = r.newSet(c.dummyView, c.dummyBuffer.buffer); err != nil { | |
| 197 | - | return err | |
| 198 | - | } | |
| 199 | 213 | // Move the placeholder image to the layout every draw declares. | |
| 200 | 214 | return r.oneShot(func(cmd vk.CommandBuffer) { | |
| 201 | 215 | b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, DstAccessMask: vk.AccessShaderReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: c.dummyImage, SubresourceRange: colorRange} | |
| @@ -203,42 +217,8 @@ func (r *Renderer) createDummies() error { | |||
| 203 | 217 | }) | |
| 204 | 218 | } | |
| 205 | 219 | ||
| 206 | - | // newSet allocates a descriptor set from its own pool (the bindings lack | |
| 207 | - | // vkFreeDescriptorSets: the pool is destroyed with the object). | |
| 208 | - | func (r *Renderer) newSet(view vk.ImageView, buffer vk.Buffer, chroma ...vk.ImageView) (vk.DescriptorPool, vk.DescriptorSet, error) { | |
| 209 | - | d := r.dd | |
| 210 | - | sizes := []vk.DescriptorPoolSize{{Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 2}, {Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1}} | |
| 211 | - | dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 2, PoolSizes: &sizes[0]} | |
| 212 | - | var pool vk.DescriptorPool | |
| 213 | - | if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &pool)); err != nil { | |
| 214 | - | return 0, 0, err | |
| 215 | - | } | |
| 216 | - | var set vk.DescriptorSet | |
| 217 | - | dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: pool, DescriptorSetCount: 1, SetLayouts: &r.compose.setLayout} | |
| 218 | - | if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &set)); err != nil { | |
| 219 | - | d.DestroyDescriptorPool(r.device, pool, nil) | |
| 220 | - | return 0, 0, err | |
| 221 | - | } | |
| 222 | - | img := vk.DescriptorImageInfo{Sampler: r.compose.sampler, ImageView: view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal} | |
| 223 | - | uv := img | |
| 224 | - | if len(chroma) > 0 { | |
| 225 | - | uv.ImageView = chroma[0] | |
| 226 | - | } | |
| 227 | - | buf := vk.DescriptorBufferInfo{Buffer: buffer, Range: wholeSize} | |
| 228 | - | writes := []vk.WriteDescriptorSet{ | |
| 229 | - | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img}, | |
| 230 | - | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &buf}, | |
| 231 | - | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 2, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &uv}, | |
| 232 | - | } | |
| 233 | - | d.UpdateDescriptorSets(r.device, uint32(len(writes)), &writes[0], 0, nil) | |
| 234 | - | return pool, set, nil | |
| 235 | - | } | |
| 236 | - | ||
| 237 | 220 | func (r *Renderer) destroyComposer() { | |
| 238 | 221 | d, c := r.dd, &r.compose | |
| 239 | - | if c.dummyPool != 0 { | |
| 240 | - | d.DestroyDescriptorPool(r.device, c.dummyPool, nil) | |
| 241 | - | } | |
| 242 | 222 | r.freeGPUBuffer(&c.dummyBuffer) | |
| 243 | 223 | if c.dummyView != 0 { | |
| 244 | 224 | d.DestroyImageView(r.device, c.dummyView, nil) | |
| @@ -295,7 +275,7 @@ func (r *Renderer) createView(t *target) (err error) { | |||
| 295 | 275 | ||
| 296 | 276 | // fillDraw is a solid rect of c. | |
| 297 | 277 | func (r *Renderer) fillDraw(rect image.Rectangle, c [3]uint8) draw { | |
| 298 | - | dr := draw{set: r.compose.dummySet} | |
| 278 | + | dr := draw{} | |
| 299 | 279 | dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)} | |
| 300 | 280 | dr.pc.color = [4]float32{float32(c[0]) / 255, float32(c[1]) / 255, float32(c[2]) / 255, 1} | |
| 301 | 281 | dr.pc.misc = [4]uint32{modeSolid, 0, uint32(r.width), uint32(r.height)} | |
| @@ -403,9 +383,9 @@ func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, source [4]f | |||
| 403 | 383 | return dr | |
| 404 | 384 | } | |
| 405 | 385 | ||
| 406 | - | // recordDraws draws ds in one rendering pass that clears view to bg, or | |
| 386 | + | // recordDraws draws batches in one rendering pass that clears view to bg, or | |
| 407 | 387 | // keeps its contents (keep). | |
| 408 | - | func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]uint8, ds []draw, keep bool) { | |
| 388 | + | func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]uint8, batches []*composeBatch, keep bool) { | |
| 409 | 389 | d, c := r.dd, &r.compose | |
| 410 | 390 | clear := vk.ClearValue{math.Float32bits(float32(bg[0]) / 255), math.Float32bits(float32(bg[1]) / 255), math.Float32bits(float32(bg[2]) / 255), math.Float32bits(1)} | |
| 411 | 391 | if r.hdrNits > 0 { | |
| @@ -425,15 +405,10 @@ func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]ui | |||
| 425 | 405 | pipeline = c.hdrPipeline | |
| 426 | 406 | } | |
| 427 | 407 | d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, pipeline) | |
| 428 | - | var bound vk.DescriptorSet | |
| 429 | - | for i := range ds { | |
| 430 | - | dr := &ds[i] | |
| 431 | - | if dr.set != bound { | |
| 432 | - | d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, c.layout, 0, 1, &dr.set, 0, nil) | |
| 433 | - | bound = dr.set | |
| 434 | - | } | |
| 435 | - | d.CmdPushConstants(cmd, c.layout, vk.ShaderStageVertexBit|vk.ShaderStageFragmentBit, 0, uint32(unsafe.Sizeof(dr.pc)), unsafe.Pointer(&dr.pc)) | |
| 436 | - | d.CmdDraw(cmd, 6, 1, 0, 0) | |
| 408 | + | for i := range batches { | |
| 409 | + | batch := batches[i] | |
| 410 | + | d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, c.layout, 0, 1, &batch.set, 0, nil) | |
| 411 | + | d.CmdDraw(cmd, 6, batch.count, 0, 0) | |
| 437 | 412 | } | |
| 438 | 413 | d.CmdEndRendering(cmd) | |
| 439 | 414 | } | |
| @@ -12,16 +12,16 @@ import ( | |||||
| 12 | vk "github.com/bnema/purego-vulkan/vulkan" | 12 | vk "github.com/bnema/purego-vulkan/vulkan" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
| 15 | - | // Composition is one graphics pipeline drawing every piece of a frame as | 15 | + | // Composition draws quads in paint order inside one dynamic rendering |
| 16 | - | // a quad, in paint order, inside one dynamic rendering pass: solid fills, | 16 | + | // pass. Each batch binds a frame-slot-owned draw buffer and resource table, |
| 17 | - | // client dmabufs sampled as images, and client wl_shm pixels read from a | 17 | + | // then draws all its quads as instances. Client pixels are premultiplied, |
| 18 | - | // GPU buffer (shm.go). Push constants carry each draw; the descriptor set | 18 | + | // hence the blend ONE, ONE_MINUS_SRC_ALPHA; opaque draws force alpha to 1. |
| 19 | - | // holds its image and buffer. Client pixels are premultiplied, hence the | |||
| 20 | - | // blend ONE, ONE_MINUS_SRC_ALPHA; opaque draws force alpha to 1. | |||
| 21 | 19 | ||||
| 22 | //go:generate glslc -O --target-env=vulkan1.3 shaders/compose.vert -o shaders/compose.vert.spv | 20 | //go:generate glslc -O --target-env=vulkan1.3 shaders/compose.vert -o shaders/compose.vert.spv | ||
| 23 | //go:generate glslc -O --target-env=vulkan1.3 shaders/compose.frag -o shaders/compose.frag.spv | 21 | //go:generate glslc -O --target-env=vulkan1.3 shaders/compose.frag -o shaders/compose.frag.spv | ||
| 24 | //go:generate glslc -O --target-env=vulkan1.3 shaders/compose_hdr.frag -o shaders/compose_hdr.frag.spv | 22 | //go:generate glslc -O --target-env=vulkan1.3 shaders/compose_hdr.frag -o shaders/compose_hdr.frag.spv | ||
| 23 | + | //go:generate glslc -O --target-env=vulkan1.3 -DSINGLE_RESOURCE shaders/compose.frag -o shaders/compose_single.frag.spv | |||
| 24 | + | //go:generate glslc -O --target-env=vulkan1.3 -DSINGLE_RESOURCE shaders/compose_hdr.frag -o shaders/compose_hdr_single.frag.spv | |||
| 25 | //go:generate glslc -O --target-env=vulkan1.3 shaders/hdr.vert -o shaders/hdr.vert.spv | 25 | //go:generate glslc -O --target-env=vulkan1.3 shaders/hdr.vert -o shaders/hdr.vert.spv | ||
| 26 | //go:generate glslc -O --target-env=vulkan1.3 shaders/hdr.frag -o shaders/hdr.frag.spv | 26 | //go:generate glslc -O --target-env=vulkan1.3 shaders/hdr.frag -o shaders/hdr.frag.spv | ||
| 27 | 27 | ||||
| @@ -32,6 +32,10 @@ var ( | |||||
| 32 | composeFrag []byte | 32 | composeFrag []byte | ||
| 33 | //go:embed shaders/compose_hdr.frag.spv | 33 | //go:embed shaders/compose_hdr.frag.spv | ||
| 34 | composeHDRFrag []byte | 34 | composeHDRFrag []byte | ||
| 35 | + | //go:embed shaders/compose_single.frag.spv | |||
| 36 | + | composeSingleFrag []byte | |||
| 37 | + | //go:embed shaders/compose_hdr_single.frag.spv | |||
| 38 | + | composeHDRSingleFrag []byte | |||
| 35 | ) | 39 | ) | ||
| 36 | 40 | ||||
| 37 | // Draw modes and flags (shaders: modeSolid, modeImage, flag*). | 41 | // Draw modes and flags (shaders: modeSolid, modeImage, flag*). | ||
| @@ -51,8 +55,8 @@ const ( | |||||
| 51 | flagSmooth = 64 | 55 | flagSmooth = 64 | ||
| 52 | ) | 56 | ) | ||
| 53 | 57 | ||||
| 54 | - | // pushConstants is struct Draw of the shaders (std430 push constant block). | 58 | + | // drawParameters is the parameter portion of the shaders' std430 Draw. |
| 55 | - | type pushConstants struct { | 59 | + | type drawParameters struct { |
| 56 | rect [4]int32 | 60 | rect [4]int32 | ||
| 57 | mapv [4]float32 | 61 | mapv [4]float32 | ||
| 58 | mapy [4]float32 | 62 | mapy [4]float32 | ||
| @@ -64,8 +68,8 @@ type pushConstants struct { | |||||
| 64 | 68 | ||||
| 65 | // draw is one quad of a frame. | 69 | // draw is one quad of a frame. | ||
| 66 | type draw struct { | 70 | type draw struct { | ||
| 67 | - | pc pushConstants | 71 | + | pc drawParameters |
| 68 | - | set vk.DescriptorSet | 72 | + | shm *shmCopy |
| 69 | im *imported // sampled client dmabuf, for ownership barriers | 73 | im *imported // sampled client dmabuf, for ownership barriers | ||
| 70 | // acquire is the content's explicit-sync fence (nil: implicit). | 74 | // acquire is the content's explicit-sync fence (nil: implicit). | ||
| 71 | acquire *os.File | 75 | acquire *os.File | ||
| @@ -84,23 +88,25 @@ type composer struct { | |||||
| 84 | dummyMemory vk.DeviceMemory | 88 | dummyMemory vk.DeviceMemory | ||
| 85 | dummyView vk.ImageView | 89 | dummyView vk.ImageView | ||
| 86 | dummyBuffer gpuBuffer | 90 | dummyBuffer gpuBuffer | ||
| 87 | - | dummyPool vk.DescriptorPool | 91 | + | // resources is the number of resource slots per batch (one without |
| 88 | - | dummySet vk.DescriptorSet | 92 | + | // nonuniform descriptor indexing); indexing selects the shader variant. |
| 93 | + | resources uint32 | |||
| 94 | + | indexing bool | |||
| 89 | } | 95 | } | ||
| 90 | 96 | ||||
| 91 | func (r *Renderer) createComposer() error { | 97 | func (r *Renderer) createComposer() error { | ||
| 92 | d, c := r.dd, &r.compose | 98 | d, c := r.dd, &r.compose | ||
| 93 | bindings := []vk.DescriptorSetLayoutBinding{ | 99 | bindings := []vk.DescriptorSetLayoutBinding{ | ||
| 94 | - | {Binding: 0, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}, | 100 | + | {Binding: 0, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: c.resources, StageFlags: vk.ShaderStageFragmentBit}, |
| 95 | - | {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}, | 101 | + | {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: c.resources, StageFlags: vk.ShaderStageFragmentBit}, |
| 96 | - | {Binding: 2, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}, | 102 | + | {Binding: 2, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: c.resources, StageFlags: vk.ShaderStageFragmentBit}, |
| 103 | + | {Binding: 3, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageVertexBit}, | |||
| 97 | } | 104 | } | ||
| 98 | sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: uint32(len(bindings)), Bindings: &bindings[0]} | 105 | sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: uint32(len(bindings)), Bindings: &bindings[0]} | ||
| 99 | if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &c.setLayout)); err != nil { | 106 | if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &c.setLayout)); err != nil { | ||
| 100 | return err | 107 | return err | ||
| 101 | } | 108 | } | ||
| 102 | - | pcr := vk.PushConstantRange{StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit, Size: uint32(unsafe.Sizeof(pushConstants{}))} | 109 | + | pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &c.setLayout} |
| 103 | - | pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &c.setLayout, PushConstantRangeCount: 1, PushConstantRanges: &pcr} | |||
| 104 | if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &c.layout)); err != nil { | 110 | if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &c.layout)); err != nil { | ||
| 105 | return err | 111 | return err | ||
| 106 | } | 112 | } | ||
| @@ -115,13 +121,24 @@ func (r *Renderer) createComposer() error { | |||||
| 115 | } | 121 | } | ||
| 116 | 122 | ||||
| 117 | func (r *Renderer) createPipeline() error { | 123 | func (r *Renderer) createPipeline() error { | ||
| 118 | - | if err := r.createGraphicsPipeline(composeVert, composeFrag, vk.FormatB8g8r8a8Unorm, r.compose.layout, true, &r.compose.pipeline); err != nil { | 124 | + | fragment := composeFrag |
| 125 | + | if !r.compose.indexing { | |||
| 126 | + | fragment = composeSingleFrag | |||
| 127 | + | } | |||
| 128 | + | if err := r.createComposePipeline(fragment, vk.FormatB8g8r8a8Unorm, &r.compose.pipeline); err != nil { | |||
| 119 | return err | 129 | return err | ||
| 120 | } | 130 | } | ||
| 121 | return nil | 131 | return nil | ||
| 122 | } | 132 | } | ||
| 123 | 133 | ||||
| 124 | - | func (r *Renderer) createGraphicsPipeline(vertex, fragment []byte, format vk.Format, layout vk.PipelineLayout, blend bool, pipeline *vk.Pipeline) error { | 134 | + | func (r *Renderer) createComposePipeline(fragment []byte, format vk.Format, pipeline *vk.Pipeline) error { |
| 135 | + | resourceCount := r.compose.resources | |||
| 136 | + | entry := vk.SpecializationMapEntry{ConstantID: 0, Size: 4} | |||
| 137 | + | specialization := vk.SpecializationInfo{MapEntryCount: 1, MapEntries: &entry, DataSize: 4, Data: unsafe.Pointer(&resourceCount)} | |||
| 138 | + | return r.createGraphicsPipeline(composeVert, fragment, format, r.compose.layout, true, &specialization, pipeline) | |||
| 139 | + | } | |||
| 140 | + | ||||
| 141 | + | func (r *Renderer) createGraphicsPipeline(vertex, fragment []byte, format vk.Format, layout vk.PipelineLayout, blend bool, specialization *vk.SpecializationInfo, pipeline *vk.Pipeline) error { | |||
| 125 | d := r.dd | 142 | d := r.dd | ||
| 126 | vert, err := r.shaderModule(vertex) | 143 | vert, err := r.shaderModule(vertex) | ||
| 127 | if err != nil { | 144 | if err != nil { | ||
| @@ -136,7 +153,7 @@ func (r *Renderer) createGraphicsPipeline(vertex, fragment []byte, format vk.For | |||||
| 136 | main := []byte("main\x00") | 153 | main := []byte("main\x00") | ||
| 137 | stages := []vk.PipelineShaderStageCreateInfo{ | 154 | stages := []vk.PipelineShaderStageCreateInfo{ | ||
| 138 | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]}, | 155 | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]}, | ||
| 139 | - | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]}, | 156 | + | {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0], SpecializationInfo: specialization}, |
| 140 | } | 157 | } | ||
| 141 | vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo} | 158 | vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo} | ||
| 142 | assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList} | 159 | assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList} | ||
| @@ -167,7 +184,7 @@ func (r *Renderer) createGraphicsPipeline(vertex, fragment []byte, format vk.For | |||||
| 167 | } | 184 | } | ||
| 168 | 185 | ||||
| 169 | // createDummies makes the placeholder image (1×1, shader-read layout) and | 186 | // createDummies makes the placeholder image (1×1, shader-read layout) and | ||
| 170 | - | // buffer, and the set fills bind. | 187 | + | // buffer used by resource-table entries a draw does not sample. |
| 171 | func (r *Renderer) createDummies() error { | 188 | func (r *Renderer) createDummies() error { | ||
| 172 | d, c := r.dd, &r.compose | 189 | d, c := r.dd, &r.compose | ||
| 173 | ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: 1, Height: 1, Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageSampledBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined} | 190 | ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: 1, Height: 1, Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageSampledBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined} | ||
| @@ -193,9 +210,6 @@ func (r *Renderer) createDummies() error { | |||||
| 193 | if err := r.newGPUBuffer(&c.dummyBuffer, 16); err != nil { | 210 | if err := r.newGPUBuffer(&c.dummyBuffer, 16); err != nil { | ||
| 194 | return err | 211 | return err | ||
| 195 | } | 212 | } | ||
| 196 | - | if c.dummyPool, c.dummySet, err = r.newSet(c.dummyView, c.dummyBuffer.buffer); err != nil { | |||
| 197 | - | return err | |||
| 198 | - | } | |||
| 199 | // Move the placeholder image to the layout every draw declares. | 213 | // Move the placeholder image to the layout every draw declares. | ||
| 200 | return r.oneShot(func(cmd vk.CommandBuffer) { | 214 | return r.oneShot(func(cmd vk.CommandBuffer) { | ||
| 201 | b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, DstAccessMask: vk.AccessShaderReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: c.dummyImage, SubresourceRange: colorRange} | 215 | b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, DstAccessMask: vk.AccessShaderReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: c.dummyImage, SubresourceRange: colorRange} | ||
| @@ -203,42 +217,8 @@ func (r *Renderer) createDummies() error { | |||||
| 203 | }) | 217 | }) | ||
| 204 | } | 218 | } | ||
| 205 | 219 | ||||
| 206 | - | // newSet allocates a descriptor set from its own pool (the bindings lack | |||
| 207 | - | // vkFreeDescriptorSets: the pool is destroyed with the object). | |||
| 208 | - | func (r *Renderer) newSet(view vk.ImageView, buffer vk.Buffer, chroma ...vk.ImageView) (vk.DescriptorPool, vk.DescriptorSet, error) { | |||
| 209 | - | d := r.dd | |||
| 210 | - | sizes := []vk.DescriptorPoolSize{{Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 2}, {Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1}} | |||
| 211 | - | dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 2, PoolSizes: &sizes[0]} | |||
| 212 | - | var pool vk.DescriptorPool | |||
| 213 | - | if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &pool)); err != nil { | |||
| 214 | - | return 0, 0, err | |||
| 215 | - | } | |||
| 216 | - | var set vk.DescriptorSet | |||
| 217 | - | dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: pool, DescriptorSetCount: 1, SetLayouts: &r.compose.setLayout} | |||
| 218 | - | if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &set)); err != nil { | |||
| 219 | - | d.DestroyDescriptorPool(r.device, pool, nil) | |||
| 220 | - | return 0, 0, err | |||
| 221 | - | } | |||
| 222 | - | img := vk.DescriptorImageInfo{Sampler: r.compose.sampler, ImageView: view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal} | |||
| 223 | - | uv := img | |||
| 224 | - | if len(chroma) > 0 { | |||
| 225 | - | uv.ImageView = chroma[0] | |||
| 226 | - | } | |||
| 227 | - | buf := vk.DescriptorBufferInfo{Buffer: buffer, Range: wholeSize} | |||
| 228 | - | writes := []vk.WriteDescriptorSet{ | |||
| 229 | - | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img}, | |||
| 230 | - | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &buf}, | |||
| 231 | - | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 2, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &uv}, | |||
| 232 | - | } | |||
| 233 | - | d.UpdateDescriptorSets(r.device, uint32(len(writes)), &writes[0], 0, nil) | |||
| 234 | - | return pool, set, nil | |||
| 235 | - | } | |||
| 236 | - | ||||
| 237 | func (r *Renderer) destroyComposer() { | 220 | func (r *Renderer) destroyComposer() { | ||
| 238 | d, c := r.dd, &r.compose | 221 | d, c := r.dd, &r.compose | ||
| 239 | - | if c.dummyPool != 0 { | |||
| 240 | - | d.DestroyDescriptorPool(r.device, c.dummyPool, nil) | |||
| 241 | - | } | |||
| 242 | r.freeGPUBuffer(&c.dummyBuffer) | 222 | r.freeGPUBuffer(&c.dummyBuffer) | ||
| 243 | if c.dummyView != 0 { | 223 | if c.dummyView != 0 { | ||
| 244 | d.DestroyImageView(r.device, c.dummyView, nil) | 224 | d.DestroyImageView(r.device, c.dummyView, nil) | ||
| @@ -295,7 +275,7 @@ func (r *Renderer) createView(t *target) (err error) { | |||||
| 295 | 275 | ||||
| 296 | // fillDraw is a solid rect of c. | 276 | // fillDraw is a solid rect of c. | ||
| 297 | func (r *Renderer) fillDraw(rect image.Rectangle, c [3]uint8) draw { | 277 | func (r *Renderer) fillDraw(rect image.Rectangle, c [3]uint8) draw { | ||
| 298 | - | dr := draw{set: r.compose.dummySet} | 278 | + | dr := draw{} |
| 299 | dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)} | 279 | dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)} | ||
| 300 | dr.pc.color = [4]float32{float32(c[0]) / 255, float32(c[1]) / 255, float32(c[2]) / 255, 1} | 280 | dr.pc.color = [4]float32{float32(c[0]) / 255, float32(c[1]) / 255, float32(c[2]) / 255, 1} | ||
| 301 | dr.pc.misc = [4]uint32{modeSolid, 0, uint32(r.width), uint32(r.height)} | 281 | dr.pc.misc = [4]uint32{modeSolid, 0, uint32(r.width), uint32(r.height)} | ||
| @@ -403,9 +383,9 @@ func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, source [4]f | |||||
| 403 | return dr | 383 | return dr | ||
| 404 | } | 384 | } | ||
| 405 | 385 | ||||
| 406 | - | // recordDraws draws ds in one rendering pass that clears view to bg, or | 386 | + | // recordDraws draws batches in one rendering pass that clears view to bg, or |
| 407 | // keeps its contents (keep). | 387 | // keeps its contents (keep). | ||
| 408 | - | func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]uint8, ds []draw, keep bool) { | 388 | + | func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]uint8, batches []*composeBatch, keep bool) { |
| 409 | d, c := r.dd, &r.compose | 389 | d, c := r.dd, &r.compose | ||
| 410 | clear := vk.ClearValue{math.Float32bits(float32(bg[0]) / 255), math.Float32bits(float32(bg[1]) / 255), math.Float32bits(float32(bg[2]) / 255), math.Float32bits(1)} | 390 | clear := vk.ClearValue{math.Float32bits(float32(bg[0]) / 255), math.Float32bits(float32(bg[1]) / 255), math.Float32bits(float32(bg[2]) / 255), math.Float32bits(1)} | ||
| 411 | if r.hdrNits > 0 { | 391 | if r.hdrNits > 0 { | ||
| @@ -425,15 +405,10 @@ func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]ui | |||||
| 425 | pipeline = c.hdrPipeline | 405 | pipeline = c.hdrPipeline | ||
| 426 | } | 406 | } | ||
| 427 | d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, pipeline) | 407 | d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, pipeline) | ||
| 428 | - | var bound vk.DescriptorSet | 408 | + | for i := range batches { |
| 429 | - | for i := range ds { | 409 | + | batch := batches[i] |
| 430 | - | dr := &ds[i] | 410 | + | d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, c.layout, 0, 1, &batch.set, 0, nil) |
| 431 | - | if dr.set != bound { | 411 | + | d.CmdDraw(cmd, 6, batch.count, 0, 0) |
| 432 | - | d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, c.layout, 0, 1, &dr.set, 0, nil) | |||
| 433 | - | bound = dr.set | |||
| 434 | - | } | |||
| 435 | - | d.CmdPushConstants(cmd, c.layout, vk.ShaderStageVertexBit|vk.ShaderStageFragmentBit, 0, uint32(unsafe.Sizeof(dr.pc)), unsafe.Pointer(&dr.pc)) | |||
| 436 | - | d.CmdDraw(cmd, 6, 1, 0, 0) | |||
| 437 | } | 412 | } | ||
| 438 | d.CmdEndRendering(cmd) | 413 | d.CmdEndRendering(cmd) | ||
| 439 | } | 414 | } | ||
A internal/adapters/vulkan/compose_batch.go +224 -0
| @@ -0,0 +1,224 @@ | |||
| 1 | + | package vulkan | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "runtime" | |
| 6 | + | "unsafe" | |
| 7 | + | ||
| 8 | + | vk "github.com/bnema/purego-vulkan/vulkan" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Bound descriptor-table work and memory even on devices with huge limits. | |
| 12 | + | const maxComposeResources = 64 | |
| 13 | + | ||
| 14 | + | // instanceDraw matches Draw in compose.vert (std430, 128-byte stride). | |
| 15 | + | type instanceDraw struct { | |
| 16 | + | pc drawParameters | |
| 17 | + | resource [4]uint32 | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // resourceKey retains the Go identity as well as the Vulkan handles: a | |
| 21 | + | // destroyed object's recycled handle must not look like a cached descriptor. | |
| 22 | + | type resourceKey struct { | |
| 23 | + | im *imported | |
| 24 | + | shm *shmCopy | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // composeBatch belongs to a frame slot. Neither its mapped memory nor its | |
| 28 | + | // descriptors are changed until the slot's previous submission completed. | |
| 29 | + | type composeBatch struct { | |
| 30 | + | gpu gpuBuffer | |
| 31 | + | pool vk.DescriptorPool | |
| 32 | + | set vk.DescriptorSet | |
| 33 | + | count uint32 | |
| 34 | + | last uint64 // frame that last prepared this batch | |
| 35 | + | instances []instanceDraw | |
| 36 | + | keys [maxComposeResources]resourceKey | |
| 37 | + | cached [maxComposeResources]resourceKey | |
| 38 | + | used int | |
| 39 | + | valid bool | |
| 40 | + | images [maxComposeResources]vk.DescriptorImageInfo | |
| 41 | + | chroma [maxComposeResources]vk.DescriptorImageInfo | |
| 42 | + | buffers [maxComposeResources]vk.DescriptorBufferInfo | |
| 43 | + | drawBuffer vk.DescriptorBufferInfo | |
| 44 | + | writes [4]vk.WriteDescriptorSet | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // add preserves paint order. Solids need no resource and can share either | |
| 48 | + | // neighbouring resource's batch, including on devices without indexing. | |
| 49 | + | func (b *composeBatch) add(dr draw, resources, maxDraws int) bool { | |
| 50 | + | if len(b.instances) == maxDraws { | |
| 51 | + | return false | |
| 52 | + | } | |
| 53 | + | key := resourceKey{im: dr.im, shm: dr.shm} | |
| 54 | + | index := 0 | |
| 55 | + | if key != (resourceKey{}) { | |
| 56 | + | for index < b.used && b.keys[index] != key { | |
| 57 | + | index++ | |
| 58 | + | } | |
| 59 | + | if index == b.used { | |
| 60 | + | if b.used == resources { | |
| 61 | + | return false | |
| 62 | + | } | |
| 63 | + | b.keys[index] = key | |
| 64 | + | b.used++ | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | b.instances = append(b.instances, instanceDraw{pc: dr.pc, resource: [4]uint32{uint32(index)}}) | |
| 68 | + | return true | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | func (r *Renderer) prepareCompose(slot *frameSlot, ds []draw) ([]*composeBatch, error) { | |
| 72 | + | resources := int(r.compose.resources) | |
| 73 | + | maxDraws := r.maxRange / int(unsafe.Sizeof(instanceDraw{})) | |
| 74 | + | if maxDraws < 1 { | |
| 75 | + | return nil, fmt.Errorf("storage buffer range too small for composition") | |
| 76 | + | } | |
| 77 | + | used := 0 | |
| 78 | + | for len(ds) > 0 { | |
| 79 | + | if used == len(slot.batches) { | |
| 80 | + | slot.batches = append(slot.batches, &composeBatch{}) | |
| 81 | + | } | |
| 82 | + | b := slot.batches[used] | |
| 83 | + | clear(b.keys[:]) | |
| 84 | + | b.used = 0 | |
| 85 | + | b.instances = b.instances[:0] | |
| 86 | + | for len(ds) > 0 && b.add(ds[0], resources, maxDraws) { | |
| 87 | + | ds = ds[1:] | |
| 88 | + | } | |
| 89 | + | if err := r.uploadCompose(b); err != nil { | |
| 90 | + | return nil, err | |
| 91 | + | } | |
| 92 | + | b.last = r.frame | |
| 93 | + | used++ | |
| 94 | + | } | |
| 95 | + | // The slot is idle. Drop trailing batches after the same frame TTL | |
| 96 | + | // as client resources, rather than retaining a peak scene forever. | |
| 97 | + | for len(slot.batches) > used { | |
| 98 | + | last := len(slot.batches) - 1 | |
| 99 | + | b := slot.batches[last] | |
| 100 | + | if r.frame-b.last <= importTTL { | |
| 101 | + | break | |
| 102 | + | } | |
| 103 | + | r.freeComposeBatch(b) | |
| 104 | + | slot.batches[last] = nil | |
| 105 | + | slot.batches = slot.batches[:last] | |
| 106 | + | } | |
| 107 | + | return slot.batches[:used], nil | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | func (r *Renderer) uploadCompose(b *composeBatch) error { | |
| 111 | + | count := len(b.instances) | |
| 112 | + | size := count * int(unsafe.Sizeof(instanceDraw{})) | |
| 113 | + | if b.gpu.size < size { | |
| 114 | + | // This slot is idle, so its previous buffer can be replaced now. | |
| 115 | + | capacity := min(r.maxRange, max(size, b.gpu.size*2, 64*int(unsafe.Sizeof(instanceDraw{})))) | |
| 116 | + | b.valid = false | |
| 117 | + | r.freeGPUBuffer(&b.gpu) | |
| 118 | + | if err := r.newGPUBuffer(&b.gpu, capacity); err != nil { | |
| 119 | + | return err | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | copy(unsafe.Slice((*instanceDraw)(b.gpu.mapped), count), b.instances) | |
| 123 | + | b.count = uint32(count) | |
| 124 | + | if b.set == 0 { | |
| 125 | + | if err := r.createComposeSet(b); err != nil { | |
| 126 | + | return err | |
| 127 | + | } | |
| 128 | + | } | |
| 129 | + | if b.valid && b.keys == b.cached { | |
| 130 | + | return nil | |
| 131 | + | } | |
| 132 | + | n := int(r.compose.resources) | |
| 133 | + | for i, key := range b.keys[:n] { | |
| 134 | + | view, chroma, buffer := r.compose.dummyView, r.compose.dummyView, r.compose.dummyBuffer.buffer | |
| 135 | + | if key.im != nil { | |
| 136 | + | view = key.im.view | |
| 137 | + | chroma = view | |
| 138 | + | if key.im.chromaView != 0 { | |
| 139 | + | chroma = key.im.chromaView | |
| 140 | + | } | |
| 141 | + | } | |
| 142 | + | if key.shm != nil { | |
| 143 | + | buffer = key.shm.gpu.buffer | |
| 144 | + | } | |
| 145 | + | b.images[i] = vk.DescriptorImageInfo{Sampler: r.compose.sampler, ImageView: view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal} | |
| 146 | + | b.chroma[i] = vk.DescriptorImageInfo{Sampler: r.compose.sampler, ImageView: chroma, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal} | |
| 147 | + | b.buffers[i] = vk.DescriptorBufferInfo{Buffer: buffer, Range: wholeSize} | |
| 148 | + | } | |
| 149 | + | b.drawBuffer = vk.DescriptorBufferInfo{Buffer: b.gpu.buffer, Range: vk.DeviceSize(b.gpu.size)} | |
| 150 | + | b.writes = [4]vk.WriteDescriptorSet{ | |
| 151 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: b.set, DstBinding: 0, DescriptorCount: uint32(n), DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &b.images[0]}, | |
| 152 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: b.set, DstBinding: 1, DescriptorCount: uint32(n), DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &b.buffers[0]}, | |
| 153 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: b.set, DstBinding: 2, DescriptorCount: uint32(n), DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &b.chroma[0]}, | |
| 154 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: b.set, DstBinding: 3, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &b.drawBuffer}, | |
| 155 | + | } | |
| 156 | + | r.dd.UpdateDescriptorSets(r.device, uint32(len(b.writes)), &b.writes[0], 0, nil) | |
| 157 | + | runtime.KeepAlive(b) | |
| 158 | + | b.cached, b.valid = b.keys, true | |
| 159 | + | return nil | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | func (r *Renderer) createComposeSet(b *composeBatch) error { | |
| 163 | + | n := r.compose.resources | |
| 164 | + | sizes := [2]vk.DescriptorPoolSize{ | |
| 165 | + | {Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 2 * n}, | |
| 166 | + | {Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: n + 1}, | |
| 167 | + | } | |
| 168 | + | info := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: uint32(len(sizes)), PoolSizes: &sizes[0]} | |
| 169 | + | if err := checked("vkCreateDescriptorPool(compose)", r.dd.CreateDescriptorPool(r.device, &info, nil, &b.pool)); err != nil { | |
| 170 | + | return err | |
| 171 | + | } | |
| 172 | + | alloc := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: b.pool, DescriptorSetCount: 1, SetLayouts: &r.compose.setLayout} | |
| 173 | + | if err := checked("vkAllocateDescriptorSets(compose)", r.dd.AllocateDescriptorSets(r.device, &alloc, &b.set)); err != nil { | |
| 174 | + | r.dd.DestroyDescriptorPool(r.device, b.pool, nil) | |
| 175 | + | b.pool = 0 | |
| 176 | + | return err | |
| 177 | + | } | |
| 178 | + | return nil | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | // Released resources may leave unbound descriptors naming dead handles. | |
| 182 | + | // Invalidate their cached identities before destruction: the next bind | |
| 183 | + | // rewrites the entire table after its slot fence. No GPU-visible mutation | |
| 184 | + | // happens here, even if another batch in the slot is still in flight. | |
| 185 | + | func (r *Renderer) invalidateCompose(key resourceKey) { | |
| 186 | + | for i := range r.slots { | |
| 187 | + | for _, b := range r.slots[i].batches { | |
| 188 | + | for j := range b.cached { | |
| 189 | + | if b.cached[j] == key { | |
| 190 | + | b.cached[j], b.keys[j] = resourceKey{}, resourceKey{} | |
| 191 | + | b.valid = false | |
| 192 | + | } | |
| 193 | + | } | |
| 194 | + | } | |
| 195 | + | } | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | // trimCompose runs only after poll, and never frees an in-flight slot. | |
| 199 | + | func (r *Renderer) trimCompose(cutoff uint64) { | |
| 200 | + | for i := range r.slots { | |
| 201 | + | s := &r.slots[i] | |
| 202 | + | if s.busy { | |
| 203 | + | continue | |
| 204 | + | } | |
| 205 | + | for len(s.batches) > 0 { | |
| 206 | + | last := len(s.batches) - 1 | |
| 207 | + | b := s.batches[last] | |
| 208 | + | if b.last > cutoff || b.last >= r.frame { | |
| 209 | + | break | |
| 210 | + | } | |
| 211 | + | r.freeComposeBatch(b) | |
| 212 | + | s.batches[last] = nil | |
| 213 | + | s.batches = s.batches[:last] | |
| 214 | + | } | |
| 215 | + | } | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | func (r *Renderer) freeComposeBatch(b *composeBatch) { | |
| 219 | + | if b.pool != 0 { | |
| 220 | + | r.dd.DestroyDescriptorPool(r.device, b.pool, nil) | |
| 221 | + | } | |
| 222 | + | r.freeGPUBuffer(&b.gpu) | |
| 223 | + | *b = composeBatch{} | |
| 224 | + | } | |
| @@ -0,0 +1,224 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "runtime" | |||
| 6 | + | "unsafe" | |||
| 7 | + | ||||
| 8 | + | vk "github.com/bnema/purego-vulkan/vulkan" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Bound descriptor-table work and memory even on devices with huge limits. | |||
| 12 | + | const maxComposeResources = 64 | |||
| 13 | + | ||||
| 14 | + | // instanceDraw matches Draw in compose.vert (std430, 128-byte stride). | |||
| 15 | + | type instanceDraw struct { | |||
| 16 | + | pc drawParameters | |||
| 17 | + | resource [4]uint32 | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // resourceKey retains the Go identity as well as the Vulkan handles: a | |||
| 21 | + | // destroyed object's recycled handle must not look like a cached descriptor. | |||
| 22 | + | type resourceKey struct { | |||
| 23 | + | im *imported | |||
| 24 | + | shm *shmCopy | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // composeBatch belongs to a frame slot. Neither its mapped memory nor its | |||
| 28 | + | // descriptors are changed until the slot's previous submission completed. | |||
| 29 | + | type composeBatch struct { | |||
| 30 | + | gpu gpuBuffer | |||
| 31 | + | pool vk.DescriptorPool | |||
| 32 | + | set vk.DescriptorSet | |||
| 33 | + | count uint32 | |||
| 34 | + | last uint64 // frame that last prepared this batch | |||
| 35 | + | instances []instanceDraw | |||
| 36 | + | keys [maxComposeResources]resourceKey | |||
| 37 | + | cached [maxComposeResources]resourceKey | |||
| 38 | + | used int | |||
| 39 | + | valid bool | |||
| 40 | + | images [maxComposeResources]vk.DescriptorImageInfo | |||
| 41 | + | chroma [maxComposeResources]vk.DescriptorImageInfo | |||
| 42 | + | buffers [maxComposeResources]vk.DescriptorBufferInfo | |||
| 43 | + | drawBuffer vk.DescriptorBufferInfo | |||
| 44 | + | writes [4]vk.WriteDescriptorSet | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // add preserves paint order. Solids need no resource and can share either | |||
| 48 | + | // neighbouring resource's batch, including on devices without indexing. | |||
| 49 | + | func (b *composeBatch) add(dr draw, resources, maxDraws int) bool { | |||
| 50 | + | if len(b.instances) == maxDraws { | |||
| 51 | + | return false | |||
| 52 | + | } | |||
| 53 | + | key := resourceKey{im: dr.im, shm: dr.shm} | |||
| 54 | + | index := 0 | |||
| 55 | + | if key != (resourceKey{}) { | |||
| 56 | + | for index < b.used && b.keys[index] != key { | |||
| 57 | + | index++ | |||
| 58 | + | } | |||
| 59 | + | if index == b.used { | |||
| 60 | + | if b.used == resources { | |||
| 61 | + | return false | |||
| 62 | + | } | |||
| 63 | + | b.keys[index] = key | |||
| 64 | + | b.used++ | |||
| 65 | + | } | |||
| 66 | + | } | |||
| 67 | + | b.instances = append(b.instances, instanceDraw{pc: dr.pc, resource: [4]uint32{uint32(index)}}) | |||
| 68 | + | return true | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | func (r *Renderer) prepareCompose(slot *frameSlot, ds []draw) ([]*composeBatch, error) { | |||
| 72 | + | resources := int(r.compose.resources) | |||
| 73 | + | maxDraws := r.maxRange / int(unsafe.Sizeof(instanceDraw{})) | |||
| 74 | + | if maxDraws < 1 { | |||
| 75 | + | return nil, fmt.Errorf("storage buffer range too small for composition") | |||
| 76 | + | } | |||
| 77 | + | used := 0 | |||
| 78 | + | for len(ds) > 0 { | |||
| 79 | + | if used == len(slot.batches) { | |||
| 80 | + | slot.batches = append(slot.batches, &composeBatch{}) | |||
| 81 | + | } | |||
| 82 | + | b := slot.batches[used] | |||
| 83 | + | clear(b.keys[:]) | |||
| 84 | + | b.used = 0 | |||
| 85 | + | b.instances = b.instances[:0] | |||
| 86 | + | for len(ds) > 0 && b.add(ds[0], resources, maxDraws) { | |||
| 87 | + | ds = ds[1:] | |||
| 88 | + | } | |||
| 89 | + | if err := r.uploadCompose(b); err != nil { | |||
| 90 | + | return nil, err | |||
| 91 | + | } | |||
| 92 | + | b.last = r.frame | |||
| 93 | + | used++ | |||
| 94 | + | } | |||
| 95 | + | // The slot is idle. Drop trailing batches after the same frame TTL | |||
| 96 | + | // as client resources, rather than retaining a peak scene forever. | |||
| 97 | + | for len(slot.batches) > used { | |||
| 98 | + | last := len(slot.batches) - 1 | |||
| 99 | + | b := slot.batches[last] | |||
| 100 | + | if r.frame-b.last <= importTTL { | |||
| 101 | + | break | |||
| 102 | + | } | |||
| 103 | + | r.freeComposeBatch(b) | |||
| 104 | + | slot.batches[last] = nil | |||
| 105 | + | slot.batches = slot.batches[:last] | |||
| 106 | + | } | |||
| 107 | + | return slot.batches[:used], nil | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | func (r *Renderer) uploadCompose(b *composeBatch) error { | |||
| 111 | + | count := len(b.instances) | |||
| 112 | + | size := count * int(unsafe.Sizeof(instanceDraw{})) | |||
| 113 | + | if b.gpu.size < size { | |||
| 114 | + | // This slot is idle, so its previous buffer can be replaced now. | |||
| 115 | + | capacity := min(r.maxRange, max(size, b.gpu.size*2, 64*int(unsafe.Sizeof(instanceDraw{})))) | |||
| 116 | + | b.valid = false | |||
| 117 | + | r.freeGPUBuffer(&b.gpu) | |||
| 118 | + | if err := r.newGPUBuffer(&b.gpu, capacity); err != nil { | |||
| 119 | + | return err | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | copy(unsafe.Slice((*instanceDraw)(b.gpu.mapped), count), b.instances) | |||
| 123 | + | b.count = uint32(count) | |||
| 124 | + | if b.set == 0 { | |||
| 125 | + | if err := r.createComposeSet(b); err != nil { | |||
| 126 | + | return err | |||
| 127 | + | } | |||
| 128 | + | } | |||
| 129 | + | if b.valid && b.keys == b.cached { | |||
| 130 | + | return nil | |||
| 131 | + | } | |||
| 132 | + | n := int(r.compose.resources) | |||
| 133 | + | for i, key := range b.keys[:n] { | |||
| 134 | + | view, chroma, buffer := r.compose.dummyView, r.compose.dummyView, r.compose.dummyBuffer.buffer | |||
| 135 | + | if key.im != nil { | |||
| 136 | + | view = key.im.view | |||
| 137 | + | chroma = view | |||
| 138 | + | if key.im.chromaView != 0 { | |||
| 139 | + | chroma = key.im.chromaView | |||
| 140 | + | } | |||
| 141 | + | } | |||
| 142 | + | if key.shm != nil { | |||
| 143 | + | buffer = key.shm.gpu.buffer | |||
| 144 | + | } | |||
| 145 | + | b.images[i] = vk.DescriptorImageInfo{Sampler: r.compose.sampler, ImageView: view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal} | |||
| 146 | + | b.chroma[i] = vk.DescriptorImageInfo{Sampler: r.compose.sampler, ImageView: chroma, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal} | |||
| 147 | + | b.buffers[i] = vk.DescriptorBufferInfo{Buffer: buffer, Range: wholeSize} | |||
| 148 | + | } | |||
| 149 | + | b.drawBuffer = vk.DescriptorBufferInfo{Buffer: b.gpu.buffer, Range: vk.DeviceSize(b.gpu.size)} | |||
| 150 | + | b.writes = [4]vk.WriteDescriptorSet{ | |||
| 151 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: b.set, DstBinding: 0, DescriptorCount: uint32(n), DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &b.images[0]}, | |||
| 152 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: b.set, DstBinding: 1, DescriptorCount: uint32(n), DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &b.buffers[0]}, | |||
| 153 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: b.set, DstBinding: 2, DescriptorCount: uint32(n), DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &b.chroma[0]}, | |||
| 154 | + | {SType: vk.StructureTypeWriteDescriptorSet, DstSet: b.set, DstBinding: 3, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &b.drawBuffer}, | |||
| 155 | + | } | |||
| 156 | + | r.dd.UpdateDescriptorSets(r.device, uint32(len(b.writes)), &b.writes[0], 0, nil) | |||
| 157 | + | runtime.KeepAlive(b) | |||
| 158 | + | b.cached, b.valid = b.keys, true | |||
| 159 | + | return nil | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | func (r *Renderer) createComposeSet(b *composeBatch) error { | |||
| 163 | + | n := r.compose.resources | |||
| 164 | + | sizes := [2]vk.DescriptorPoolSize{ | |||
| 165 | + | {Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 2 * n}, | |||
| 166 | + | {Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: n + 1}, | |||
| 167 | + | } | |||
| 168 | + | info := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: uint32(len(sizes)), PoolSizes: &sizes[0]} | |||
| 169 | + | if err := checked("vkCreateDescriptorPool(compose)", r.dd.CreateDescriptorPool(r.device, &info, nil, &b.pool)); err != nil { | |||
| 170 | + | return err | |||
| 171 | + | } | |||
| 172 | + | alloc := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: b.pool, DescriptorSetCount: 1, SetLayouts: &r.compose.setLayout} | |||
| 173 | + | if err := checked("vkAllocateDescriptorSets(compose)", r.dd.AllocateDescriptorSets(r.device, &alloc, &b.set)); err != nil { | |||
| 174 | + | r.dd.DestroyDescriptorPool(r.device, b.pool, nil) | |||
| 175 | + | b.pool = 0 | |||
| 176 | + | return err | |||
| 177 | + | } | |||
| 178 | + | return nil | |||
| 179 | + | } | |||
| 180 | + | ||||
| 181 | + | // Released resources may leave unbound descriptors naming dead handles. | |||
| 182 | + | // Invalidate their cached identities before destruction: the next bind | |||
| 183 | + | // rewrites the entire table after its slot fence. No GPU-visible mutation | |||
| 184 | + | // happens here, even if another batch in the slot is still in flight. | |||
| 185 | + | func (r *Renderer) invalidateCompose(key resourceKey) { | |||
| 186 | + | for i := range r.slots { | |||
| 187 | + | for _, b := range r.slots[i].batches { | |||
| 188 | + | for j := range b.cached { | |||
| 189 | + | if b.cached[j] == key { | |||
| 190 | + | b.cached[j], b.keys[j] = resourceKey{}, resourceKey{} | |||
| 191 | + | b.valid = false | |||
| 192 | + | } | |||
| 193 | + | } | |||
| 194 | + | } | |||
| 195 | + | } | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | // trimCompose runs only after poll, and never frees an in-flight slot. | |||
| 199 | + | func (r *Renderer) trimCompose(cutoff uint64) { | |||
| 200 | + | for i := range r.slots { | |||
| 201 | + | s := &r.slots[i] | |||
| 202 | + | if s.busy { | |||
| 203 | + | continue | |||
| 204 | + | } | |||
| 205 | + | for len(s.batches) > 0 { | |||
| 206 | + | last := len(s.batches) - 1 | |||
| 207 | + | b := s.batches[last] | |||
| 208 | + | if b.last > cutoff || b.last >= r.frame { | |||
| 209 | + | break | |||
| 210 | + | } | |||
| 211 | + | r.freeComposeBatch(b) | |||
| 212 | + | s.batches[last] = nil | |||
| 213 | + | s.batches = s.batches[:last] | |||
| 214 | + | } | |||
| 215 | + | } | |||
| 216 | + | } | |||
| 217 | + | ||||
| 218 | + | func (r *Renderer) freeComposeBatch(b *composeBatch) { | |||
| 219 | + | if b.pool != 0 { | |||
| 220 | + | r.dd.DestroyDescriptorPool(r.device, b.pool, nil) | |||
| 221 | + | } | |||
| 222 | + | r.freeGPUBuffer(&b.gpu) | |||
| 223 | + | *b = composeBatch{} | |||
| 224 | + | } | |||
A internal/adapters/vulkan/compose_batch_test.go +220 -0
| @@ -0,0 +1,220 @@ | |||
| 1 | + | package vulkan | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "image" | |
| 5 | + | "image/color" | |
| 6 | + | "slices" | |
| 7 | + | "testing" | |
| 8 | + | "unsafe" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/neferwl/internal/ports" | |
| 11 | + | vk "github.com/bnema/purego-vulkan/vulkan" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | func TestInstanceDrawLayout(t *testing.T) { | |
| 15 | + | if unsafe.Sizeof(drawParameters{}) != 112 || unsafe.Sizeof(instanceDraw{}) != 128 || unsafe.Offsetof(instanceDraw{}.resource) != 112 { | |
| 16 | + | t.Fatal("draw layout differs from compose.vert std430 layout") | |
| 17 | + | } | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | func TestComposeBatchPaintOrder(t *testing.T) { | |
| 21 | + | a, b := &shmCopy{}, &shmCopy{} | |
| 22 | + | ds := []draw{{}, {shm: a}, {}, {shm: b}, {shm: a}} | |
| 23 | + | for i := range ds { | |
| 24 | + | ds[i].pc.rect[0] = int32(i) | |
| 25 | + | } | |
| 26 | + | var batch composeBatch | |
| 27 | + | for _, dr := range ds { | |
| 28 | + | if !batch.add(dr, 2, 20) { | |
| 29 | + | t.Fatal("unexpected split") | |
| 30 | + | } | |
| 31 | + | } | |
| 32 | + | if batch.used != 2 { | |
| 33 | + | t.Fatalf("resources = %d, want 2", batch.used) | |
| 34 | + | } | |
| 35 | + | for i, want := range []uint32{0, 0, 0, 1, 0} { | |
| 36 | + | if batch.instances[i].resource[0] != want || batch.instances[i].pc.rect[0] != int32(i) { | |
| 37 | + | t.Fatalf("draw %d reordered or assigned wrong resource: %+v", i, batch.instances[i]) | |
| 38 | + | } | |
| 39 | + | } | |
| 40 | + | if batch.add(draw{shm: &shmCopy{}}, 2, 20) { | |
| 41 | + | t.Fatal("accepted resource beyond descriptor capacity") | |
| 42 | + | } | |
| 43 | + | if batch.add(draw{}, 2, len(ds)) { | |
| 44 | + | t.Fatal("accepted draw beyond buffer capacity") | |
| 45 | + | } | |
| 46 | + | batch = composeBatch{} | |
| 47 | + | for _, dr := range ds[:3] { | |
| 48 | + | if !batch.add(dr, 1, 20) { | |
| 49 | + | t.Fatal("solid split a single-resource run") | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | if batch.add(ds[3], 1, 20) { | |
| 53 | + | t.Fatal("single-resource batch accepted another resource") | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | func TestComposeResourceLimit(t *testing.T) { | |
| 58 | + | limits := vk.PhysicalDeviceLimits{ | |
| 59 | + | MaxPerStageDescriptorSamplers: 128, MaxDescriptorSetSamplers: 128, | |
| 60 | + | MaxPerStageDescriptorSampledImages: 128, MaxDescriptorSetSampledImages: 128, | |
| 61 | + | MaxPerStageDescriptorStorageBuffers: 64, MaxDescriptorSetStorageBuffers: 65, | |
| 62 | + | MaxPerStageResources: 193, | |
| 63 | + | } | |
| 64 | + | if got := composeResourceLimit(limits); got != 64 { | |
| 65 | + | t.Fatalf("limit = %d, want 64", got) | |
| 66 | + | } | |
| 67 | + | limits.MaxPerStageResources = 192 | |
| 68 | + | if got := composeResourceLimit(limits); got != 63 { | |
| 69 | + | t.Fatalf("color attachment not counted: %d, want 63", got) | |
| 70 | + | } | |
| 71 | + | limits.MaxDescriptorSetStorageBuffers = 5 | |
| 72 | + | if got := composeResourceLimit(limits); got != 4 { | |
| 73 | + | t.Fatalf("instance buffer not counted: %d, want 4", got) | |
| 74 | + | } | |
| 75 | + | limits.MaxPerStageDescriptorSamplers = 4 | |
| 76 | + | if got := composeResourceLimit(limits); got != 2 { | |
| 77 | + | t.Fatalf("two image planes not counted: %d, want 2", got) | |
| 78 | + | } | |
| 79 | + | limits.MaxDescriptorSetStorageBuffers = 0 | |
| 80 | + | if got := composeResourceLimit(limits); got != 0 { | |
| 81 | + | t.Fatalf("zero limit underflowed: %d", got) | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | // Both shader variants preserve ordered blending across batch boundaries, | |
| 86 | + | // resource changes, and more batches than the slot's initial slice capacity. | |
| 87 | + | func TestComposeInstancedBlendAndBatches(t *testing.T) { | |
| 88 | + | for _, tc := range []struct { | |
| 89 | + | name string | |
| 90 | + | single bool | |
| 91 | + | resources uint32 | |
| 92 | + | }{ | |
| 93 | + | {"indexed", false, 3}, | |
| 94 | + | {"single resource", true, 1}, | |
| 95 | + | } { | |
| 96 | + | t.Run(tc.name, func(t *testing.T) { | |
| 97 | + | r, err := New(16, 16) | |
| 98 | + | if err != nil { | |
| 99 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 100 | + | } | |
| 101 | + | defer r.Close() | |
| 102 | + | if !tc.single && !r.compose.indexing { | |
| 103 | + | t.Skip("nonuniform descriptor indexing unavailable") | |
| 104 | + | } | |
| 105 | + | // Rebuild only the composition pipeline/layout before rendering, | |
| 106 | + | // to exercise bounded tables and the feature-free shader variant. | |
| 107 | + | r.destroyComposer() | |
| 108 | + | r.compose.resources, r.compose.indexing = tc.resources, !tc.single | |
| 109 | + | if err := r.createComposer(); err != nil { | |
| 110 | + | t.Fatal(err) | |
| 111 | + | } | |
| 112 | + | scene := ports.Scene{Background: "#000000"} | |
| 113 | + | contents := make(map[ports.WindowID]ports.SurfaceContent) | |
| 114 | + | var reference [3]float64 | |
| 115 | + | for i := range 7 { | |
| 116 | + | id := ports.WindowID(i + 1) | |
| 117 | + | pixel := [4]byte{byte(12 + i*15), byte(90 - i*9), byte(20 + i*12), 128} | |
| 118 | + | for ch := range 3 { | |
| 119 | + | reference[ch] = float64(pixel[2-ch]) + reference[ch]*(127.0/255) | |
| 120 | + | } | |
| 121 | + | c := *shmContent(t, 16, 16, 64, fill(16, 16, pixel)) | |
| 122 | + | c.ID, c.Seq, c.Version = id, 1, 1 | |
| 123 | + | contents[id] = c | |
| 124 | + | scene.Layers = append(scene.Layers, ports.SceneLayer{ID: id, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 16}}) | |
| 125 | + | } | |
| 126 | + | for range 4 { | |
| 127 | + | if err := render(r, scene, contents); err != nil { | |
| 128 | + | t.Fatal(err) | |
| 129 | + | } | |
| 130 | + | if got := readPixels(t, r).RGBAAt(8, 8); !near(got, color.RGBA{R: uint8(reference[0] + 0.5), G: uint8(reference[1] + 0.5), B: uint8(reference[2] + 0.5), A: 255}, 2) { | |
| 131 | + | t.Fatalf("ordered translucent overlays = %v", got) | |
| 132 | + | } | |
| 133 | + | } | |
| 134 | + | wantBatches := (7 + int(tc.resources) - 1) / int(tc.resources) | |
| 135 | + | for i := range r.slots { | |
| 136 | + | if len(r.slots[i].batches) != wantBatches { | |
| 137 | + | t.Fatalf("slot %d batches = %d, want %d", i, len(r.slots[i].batches), wantBatches) | |
| 138 | + | } | |
| 139 | + | } | |
| 140 | + | // Replace a resource: both slots must refresh their descriptors. | |
| 141 | + | oldKeys := [frameSlots][maxComposeResources]resourceKey{} | |
| 142 | + | for i := range r.slots { | |
| 143 | + | oldKeys[i] = r.slots[i].batches[wantBatches-1].cached | |
| 144 | + | } | |
| 145 | + | replacement := solidContent(t, 8, 8, color.RGBA{B: 255, A: 255}) | |
| 146 | + | replacement.LogicalW, replacement.LogicalH = 16, 16 | |
| 147 | + | replacement.ID, replacement.Seq, replacement.Version = 99, 2, 2 | |
| 148 | + | contents[7] = replacement | |
| 149 | + | for range 4 { | |
| 150 | + | if err := render(r, scene, contents); err != nil { | |
| 151 | + | t.Fatal(err) | |
| 152 | + | } | |
| 153 | + | if got := readPixels(t, r).RGBAAt(8, 8); got != (color.RGBA{B: 255, A: 255}) { | |
| 154 | + | t.Fatalf("replaced resource = %v", got) | |
| 155 | + | } | |
| 156 | + | } | |
| 157 | + | for i := range r.slots { | |
| 158 | + | if r.slots[i].batches[wantBatches-1].cached == oldKeys[i] { | |
| 159 | + | t.Fatalf("slot %d retained old resource descriptors", i) | |
| 160 | + | } | |
| 161 | + | } | |
| 162 | + | }) | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | func TestComposePreparedReuseAndGrowth(t *testing.T) { | |
| 167 | + | r, err := New(16, 16) | |
| 168 | + | if err != nil { | |
| 169 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 170 | + | } | |
| 171 | + | defer r.Close() | |
| 172 | + | slot := &r.slots[0] | |
| 173 | + | ds := []draw{r.fillDraw(image.Rect(0, 0, 16, 16), [3]uint8{255})} | |
| 174 | + | if _, err := r.prepareCompose(slot, ds); err != nil { | |
| 175 | + | t.Fatal(err) | |
| 176 | + | } | |
| 177 | + | batch := slot.batches[0] | |
| 178 | + | initialSize := batch.gpu.size | |
| 179 | + | large := slices.Repeat(ds, 100) | |
| 180 | + | if _, err := r.prepareCompose(slot, large); err != nil { | |
| 181 | + | t.Fatal(err) | |
| 182 | + | } | |
| 183 | + | if batch.gpu.size <= initialSize || batch.drawBuffer.Buffer != batch.gpu.buffer || batch.count != 100 { | |
| 184 | + | t.Fatal("grown draw buffer not rebound") | |
| 185 | + | } | |
| 186 | + | if allocs := testing.AllocsPerRun(50, func() { | |
| 187 | + | if _, err := r.prepareCompose(slot, large); err != nil { | |
| 188 | + | t.Fatal(err) | |
| 189 | + | } | |
| 190 | + | }); allocs != 0 { | |
| 191 | + | t.Fatalf("steady compose upload: %.1f allocations, want zero", allocs) | |
| 192 | + | } | |
| 193 | + | if batches, err := r.prepareCompose(slot, nil); err != nil || len(batches) != 0 { | |
| 194 | + | t.Fatal("empty pass returned batches") | |
| 195 | + | } | |
| 196 | + | // Lower the draw range to force a split independently of descriptors. | |
| 197 | + | r.maxRange = 2 * int(unsafe.Sizeof(instanceDraw{})) | |
| 198 | + | if batches, err := r.prepareCompose(slot, ds); err != nil || len(batches) != 1 { | |
| 199 | + | t.Fatal("single draw did not fit") | |
| 200 | + | } | |
| 201 | + | if batches, err := r.prepareCompose(slot, slices.Repeat(ds, 5)); err != nil || len(batches) != 3 || batches[0].count != 2 || batches[2].count != 1 { | |
| 202 | + | t.Fatal("draw-buffer range did not split into 2,2,1") | |
| 203 | + | } | |
| 204 | + | // Shrink the scene and age its unused batches beyond the frame TTL. | |
| 205 | + | r.frame += importTTL + 1 | |
| 206 | + | if batches, err := r.prepareCompose(slot, ds); err != nil || len(batches) != 1 || len(slot.batches) != 1 { | |
| 207 | + | t.Fatal("unused batches retained beyond frame TTL") | |
| 208 | + | } | |
| 209 | + | key := resourceKey{shm: &shmCopy{}} | |
| 210 | + | slot.batches[0].cached[0] = key | |
| 211 | + | r.invalidateCompose(key) | |
| 212 | + | if slot.batches[0].valid || slot.batches[0].cached[0] != (resourceKey{}) { | |
| 213 | + | t.Fatal("released resource kept a valid descriptor cache") | |
| 214 | + | } | |
| 215 | + | r.frame++ | |
| 216 | + | r.trimCompose(r.frame - 1) | |
| 217 | + | if len(slot.batches) != 0 { | |
| 218 | + | t.Fatal("idle batches retained beyond wall-clock cutoff") | |
| 219 | + | } | |
| 220 | + | } | |
| @@ -0,0 +1,220 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "image" | |||
| 5 | + | "image/color" | |||
| 6 | + | "slices" | |||
| 7 | + | "testing" | |||
| 8 | + | "unsafe" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 11 | + | vk "github.com/bnema/purego-vulkan/vulkan" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | func TestInstanceDrawLayout(t *testing.T) { | |||
| 15 | + | if unsafe.Sizeof(drawParameters{}) != 112 || unsafe.Sizeof(instanceDraw{}) != 128 || unsafe.Offsetof(instanceDraw{}.resource) != 112 { | |||
| 16 | + | t.Fatal("draw layout differs from compose.vert std430 layout") | |||
| 17 | + | } | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | func TestComposeBatchPaintOrder(t *testing.T) { | |||
| 21 | + | a, b := &shmCopy{}, &shmCopy{} | |||
| 22 | + | ds := []draw{{}, {shm: a}, {}, {shm: b}, {shm: a}} | |||
| 23 | + | for i := range ds { | |||
| 24 | + | ds[i].pc.rect[0] = int32(i) | |||
| 25 | + | } | |||
| 26 | + | var batch composeBatch | |||
| 27 | + | for _, dr := range ds { | |||
| 28 | + | if !batch.add(dr, 2, 20) { | |||
| 29 | + | t.Fatal("unexpected split") | |||
| 30 | + | } | |||
| 31 | + | } | |||
| 32 | + | if batch.used != 2 { | |||
| 33 | + | t.Fatalf("resources = %d, want 2", batch.used) | |||
| 34 | + | } | |||
| 35 | + | for i, want := range []uint32{0, 0, 0, 1, 0} { | |||
| 36 | + | if batch.instances[i].resource[0] != want || batch.instances[i].pc.rect[0] != int32(i) { | |||
| 37 | + | t.Fatalf("draw %d reordered or assigned wrong resource: %+v", i, batch.instances[i]) | |||
| 38 | + | } | |||
| 39 | + | } | |||
| 40 | + | if batch.add(draw{shm: &shmCopy{}}, 2, 20) { | |||
| 41 | + | t.Fatal("accepted resource beyond descriptor capacity") | |||
| 42 | + | } | |||
| 43 | + | if batch.add(draw{}, 2, len(ds)) { | |||
| 44 | + | t.Fatal("accepted draw beyond buffer capacity") | |||
| 45 | + | } | |||
| 46 | + | batch = composeBatch{} | |||
| 47 | + | for _, dr := range ds[:3] { | |||
| 48 | + | if !batch.add(dr, 1, 20) { | |||
| 49 | + | t.Fatal("solid split a single-resource run") | |||
| 50 | + | } | |||
| 51 | + | } | |||
| 52 | + | if batch.add(ds[3], 1, 20) { | |||
| 53 | + | t.Fatal("single-resource batch accepted another resource") | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | func TestComposeResourceLimit(t *testing.T) { | |||
| 58 | + | limits := vk.PhysicalDeviceLimits{ | |||
| 59 | + | MaxPerStageDescriptorSamplers: 128, MaxDescriptorSetSamplers: 128, | |||
| 60 | + | MaxPerStageDescriptorSampledImages: 128, MaxDescriptorSetSampledImages: 128, | |||
| 61 | + | MaxPerStageDescriptorStorageBuffers: 64, MaxDescriptorSetStorageBuffers: 65, | |||
| 62 | + | MaxPerStageResources: 193, | |||
| 63 | + | } | |||
| 64 | + | if got := composeResourceLimit(limits); got != 64 { | |||
| 65 | + | t.Fatalf("limit = %d, want 64", got) | |||
| 66 | + | } | |||
| 67 | + | limits.MaxPerStageResources = 192 | |||
| 68 | + | if got := composeResourceLimit(limits); got != 63 { | |||
| 69 | + | t.Fatalf("color attachment not counted: %d, want 63", got) | |||
| 70 | + | } | |||
| 71 | + | limits.MaxDescriptorSetStorageBuffers = 5 | |||
| 72 | + | if got := composeResourceLimit(limits); got != 4 { | |||
| 73 | + | t.Fatalf("instance buffer not counted: %d, want 4", got) | |||
| 74 | + | } | |||
| 75 | + | limits.MaxPerStageDescriptorSamplers = 4 | |||
| 76 | + | if got := composeResourceLimit(limits); got != 2 { | |||
| 77 | + | t.Fatalf("two image planes not counted: %d, want 2", got) | |||
| 78 | + | } | |||
| 79 | + | limits.MaxDescriptorSetStorageBuffers = 0 | |||
| 80 | + | if got := composeResourceLimit(limits); got != 0 { | |||
| 81 | + | t.Fatalf("zero limit underflowed: %d", got) | |||
| 82 | + | } | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | // Both shader variants preserve ordered blending across batch boundaries, | |||
| 86 | + | // resource changes, and more batches than the slot's initial slice capacity. | |||
| 87 | + | func TestComposeInstancedBlendAndBatches(t *testing.T) { | |||
| 88 | + | for _, tc := range []struct { | |||
| 89 | + | name string | |||
| 90 | + | single bool | |||
| 91 | + | resources uint32 | |||
| 92 | + | }{ | |||
| 93 | + | {"indexed", false, 3}, | |||
| 94 | + | {"single resource", true, 1}, | |||
| 95 | + | } { | |||
| 96 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 97 | + | r, err := New(16, 16) | |||
| 98 | + | if err != nil { | |||
| 99 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 100 | + | } | |||
| 101 | + | defer r.Close() | |||
| 102 | + | if !tc.single && !r.compose.indexing { | |||
| 103 | + | t.Skip("nonuniform descriptor indexing unavailable") | |||
| 104 | + | } | |||
| 105 | + | // Rebuild only the composition pipeline/layout before rendering, | |||
| 106 | + | // to exercise bounded tables and the feature-free shader variant. | |||
| 107 | + | r.destroyComposer() | |||
| 108 | + | r.compose.resources, r.compose.indexing = tc.resources, !tc.single | |||
| 109 | + | if err := r.createComposer(); err != nil { | |||
| 110 | + | t.Fatal(err) | |||
| 111 | + | } | |||
| 112 | + | scene := ports.Scene{Background: "#000000"} | |||
| 113 | + | contents := make(map[ports.WindowID]ports.SurfaceContent) | |||
| 114 | + | var reference [3]float64 | |||
| 115 | + | for i := range 7 { | |||
| 116 | + | id := ports.WindowID(i + 1) | |||
| 117 | + | pixel := [4]byte{byte(12 + i*15), byte(90 - i*9), byte(20 + i*12), 128} | |||
| 118 | + | for ch := range 3 { | |||
| 119 | + | reference[ch] = float64(pixel[2-ch]) + reference[ch]*(127.0/255) | |||
| 120 | + | } | |||
| 121 | + | c := *shmContent(t, 16, 16, 64, fill(16, 16, pixel)) | |||
| 122 | + | c.ID, c.Seq, c.Version = id, 1, 1 | |||
| 123 | + | contents[id] = c | |||
| 124 | + | scene.Layers = append(scene.Layers, ports.SceneLayer{ID: id, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 16}}) | |||
| 125 | + | } | |||
| 126 | + | for range 4 { | |||
| 127 | + | if err := render(r, scene, contents); err != nil { | |||
| 128 | + | t.Fatal(err) | |||
| 129 | + | } | |||
| 130 | + | if got := readPixels(t, r).RGBAAt(8, 8); !near(got, color.RGBA{R: uint8(reference[0] + 0.5), G: uint8(reference[1] + 0.5), B: uint8(reference[2] + 0.5), A: 255}, 2) { | |||
| 131 | + | t.Fatalf("ordered translucent overlays = %v", got) | |||
| 132 | + | } | |||
| 133 | + | } | |||
| 134 | + | wantBatches := (7 + int(tc.resources) - 1) / int(tc.resources) | |||
| 135 | + | for i := range r.slots { | |||
| 136 | + | if len(r.slots[i].batches) != wantBatches { | |||
| 137 | + | t.Fatalf("slot %d batches = %d, want %d", i, len(r.slots[i].batches), wantBatches) | |||
| 138 | + | } | |||
| 139 | + | } | |||
| 140 | + | // Replace a resource: both slots must refresh their descriptors. | |||
| 141 | + | oldKeys := [frameSlots][maxComposeResources]resourceKey{} | |||
| 142 | + | for i := range r.slots { | |||
| 143 | + | oldKeys[i] = r.slots[i].batches[wantBatches-1].cached | |||
| 144 | + | } | |||
| 145 | + | replacement := solidContent(t, 8, 8, color.RGBA{B: 255, A: 255}) | |||
| 146 | + | replacement.LogicalW, replacement.LogicalH = 16, 16 | |||
| 147 | + | replacement.ID, replacement.Seq, replacement.Version = 99, 2, 2 | |||
| 148 | + | contents[7] = replacement | |||
| 149 | + | for range 4 { | |||
| 150 | + | if err := render(r, scene, contents); err != nil { | |||
| 151 | + | t.Fatal(err) | |||
| 152 | + | } | |||
| 153 | + | if got := readPixels(t, r).RGBAAt(8, 8); got != (color.RGBA{B: 255, A: 255}) { | |||
| 154 | + | t.Fatalf("replaced resource = %v", got) | |||
| 155 | + | } | |||
| 156 | + | } | |||
| 157 | + | for i := range r.slots { | |||
| 158 | + | if r.slots[i].batches[wantBatches-1].cached == oldKeys[i] { | |||
| 159 | + | t.Fatalf("slot %d retained old resource descriptors", i) | |||
| 160 | + | } | |||
| 161 | + | } | |||
| 162 | + | }) | |||
| 163 | + | } | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | func TestComposePreparedReuseAndGrowth(t *testing.T) { | |||
| 167 | + | r, err := New(16, 16) | |||
| 168 | + | if err != nil { | |||
| 169 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 170 | + | } | |||
| 171 | + | defer r.Close() | |||
| 172 | + | slot := &r.slots[0] | |||
| 173 | + | ds := []draw{r.fillDraw(image.Rect(0, 0, 16, 16), [3]uint8{255})} | |||
| 174 | + | if _, err := r.prepareCompose(slot, ds); err != nil { | |||
| 175 | + | t.Fatal(err) | |||
| 176 | + | } | |||
| 177 | + | batch := slot.batches[0] | |||
| 178 | + | initialSize := batch.gpu.size | |||
| 179 | + | large := slices.Repeat(ds, 100) | |||
| 180 | + | if _, err := r.prepareCompose(slot, large); err != nil { | |||
| 181 | + | t.Fatal(err) | |||
| 182 | + | } | |||
| 183 | + | if batch.gpu.size <= initialSize || batch.drawBuffer.Buffer != batch.gpu.buffer || batch.count != 100 { | |||
| 184 | + | t.Fatal("grown draw buffer not rebound") | |||
| 185 | + | } | |||
| 186 | + | if allocs := testing.AllocsPerRun(50, func() { | |||
| 187 | + | if _, err := r.prepareCompose(slot, large); err != nil { | |||
| 188 | + | t.Fatal(err) | |||
| 189 | + | } | |||
| 190 | + | }); allocs != 0 { | |||
| 191 | + | t.Fatalf("steady compose upload: %.1f allocations, want zero", allocs) | |||
| 192 | + | } | |||
| 193 | + | if batches, err := r.prepareCompose(slot, nil); err != nil || len(batches) != 0 { | |||
| 194 | + | t.Fatal("empty pass returned batches") | |||
| 195 | + | } | |||
| 196 | + | // Lower the draw range to force a split independently of descriptors. | |||
| 197 | + | r.maxRange = 2 * int(unsafe.Sizeof(instanceDraw{})) | |||
| 198 | + | if batches, err := r.prepareCompose(slot, ds); err != nil || len(batches) != 1 { | |||
| 199 | + | t.Fatal("single draw did not fit") | |||
| 200 | + | } | |||
| 201 | + | if batches, err := r.prepareCompose(slot, slices.Repeat(ds, 5)); err != nil || len(batches) != 3 || batches[0].count != 2 || batches[2].count != 1 { | |||
| 202 | + | t.Fatal("draw-buffer range did not split into 2,2,1") | |||
| 203 | + | } | |||
| 204 | + | // Shrink the scene and age its unused batches beyond the frame TTL. | |||
| 205 | + | r.frame += importTTL + 1 | |||
| 206 | + | if batches, err := r.prepareCompose(slot, ds); err != nil || len(batches) != 1 || len(slot.batches) != 1 { | |||
| 207 | + | t.Fatal("unused batches retained beyond frame TTL") | |||
| 208 | + | } | |||
| 209 | + | key := resourceKey{shm: &shmCopy{}} | |||
| 210 | + | slot.batches[0].cached[0] = key | |||
| 211 | + | r.invalidateCompose(key) | |||
| 212 | + | if slot.batches[0].valid || slot.batches[0].cached[0] != (resourceKey{}) { | |||
| 213 | + | t.Fatal("released resource kept a valid descriptor cache") | |||
| 214 | + | } | |||
| 215 | + | r.frame++ | |||
| 216 | + | r.trimCompose(r.frame - 1) | |||
| 217 | + | if len(slot.batches) != 0 { | |||
| 218 | + | t.Fatal("idle batches retained beyond wall-clock cutoff") | |||
| 219 | + | } | |||
| 220 | + | } | |||
A internal/adapters/vulkan/compose_features.go +50 -0
| @@ -0,0 +1,50 @@ | |||
| 1 | + | package vulkan | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "unsafe" | |
| 5 | + | ||
| 6 | + | vk "github.com/bnema/purego-vulkan/vulkan" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // VkPhysicalDeviceDescriptorIndexingFeatures is absent from the bindings. | |
| 10 | + | // Its layout and sType are defined by Vulkan 1.2 (vulkan_core.h). | |
| 11 | + | const descriptorIndexingFeaturesType vk.StructureType = 1000161001 | |
| 12 | + | ||
| 13 | + | type descriptorIndexingFeatures struct { | |
| 14 | + | SType vk.StructureType | |
| 15 | + | Next unsafe.Pointer | |
| 16 | + | ShaderInputAttachmentArrayDynamicIndexing vk.Bool32 | |
| 17 | + | ShaderUniformTexelBufferArrayDynamicIndexing vk.Bool32 | |
| 18 | + | ShaderStorageTexelBufferArrayDynamicIndexing vk.Bool32 | |
| 19 | + | ShaderUniformBufferArrayNonUniformIndexing vk.Bool32 | |
| 20 | + | ShaderSampledImageArrayNonUniformIndexing vk.Bool32 | |
| 21 | + | ShaderStorageBufferArrayNonUniformIndexing vk.Bool32 | |
| 22 | + | ShaderStorageImageArrayNonUniformIndexing vk.Bool32 | |
| 23 | + | ShaderInputAttachmentArrayNonUniformIndexing vk.Bool32 | |
| 24 | + | ShaderUniformTexelBufferArrayNonUniformIndexing vk.Bool32 | |
| 25 | + | ShaderStorageTexelBufferArrayNonUniformIndexing vk.Bool32 | |
| 26 | + | DescriptorBindingUniformBufferUpdateAfterBind vk.Bool32 | |
| 27 | + | DescriptorBindingSampledImageUpdateAfterBind vk.Bool32 | |
| 28 | + | DescriptorBindingStorageImageUpdateAfterBind vk.Bool32 | |
| 29 | + | DescriptorBindingStorageBufferUpdateAfterBind vk.Bool32 | |
| 30 | + | DescriptorBindingUniformTexelBufferUpdateAfterBind vk.Bool32 | |
| 31 | + | DescriptorBindingStorageTexelBufferUpdateAfterBind vk.Bool32 | |
| 32 | + | DescriptorBindingUpdateUnusedWhilePending vk.Bool32 | |
| 33 | + | DescriptorBindingPartiallyBound vk.Bool32 | |
| 34 | + | DescriptorBindingVariableDescriptorCount vk.Bool32 | |
| 35 | + | RuntimeDescriptorArray vk.Bool32 | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // composeResourceLimit counts both image planes and the instance SSBO. | |
| 39 | + | // No update-after-bind/partially-bound features are needed: tables are | |
| 40 | + | // completely initialized and updated only after their frame-slot fence. | |
| 41 | + | func composeResourceLimit(l vk.PhysicalDeviceLimits) uint32 { | |
| 42 | + | if l.MaxDescriptorSetStorageBuffers <= 1 || l.MaxPerStageResources == 0 { | |
| 43 | + | return 0 | |
| 44 | + | } | |
| 45 | + | return min(uint32(maxComposeResources), | |
| 46 | + | l.MaxPerStageDescriptorSamplers/2, l.MaxDescriptorSetSamplers/2, | |
| 47 | + | l.MaxPerStageDescriptorSampledImages/2, l.MaxDescriptorSetSampledImages/2, | |
| 48 | + | l.MaxPerStageDescriptorStorageBuffers, l.MaxDescriptorSetStorageBuffers-1, | |
| 49 | + | (l.MaxPerStageResources-1)/3) | |
| 50 | + | } | |
| @@ -0,0 +1,50 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "unsafe" | |||
| 5 | + | ||||
| 6 | + | vk "github.com/bnema/purego-vulkan/vulkan" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // VkPhysicalDeviceDescriptorIndexingFeatures is absent from the bindings. | |||
| 10 | + | // Its layout and sType are defined by Vulkan 1.2 (vulkan_core.h). | |||
| 11 | + | const descriptorIndexingFeaturesType vk.StructureType = 1000161001 | |||
| 12 | + | ||||
| 13 | + | type descriptorIndexingFeatures struct { | |||
| 14 | + | SType vk.StructureType | |||
| 15 | + | Next unsafe.Pointer | |||
| 16 | + | ShaderInputAttachmentArrayDynamicIndexing vk.Bool32 | |||
| 17 | + | ShaderUniformTexelBufferArrayDynamicIndexing vk.Bool32 | |||
| 18 | + | ShaderStorageTexelBufferArrayDynamicIndexing vk.Bool32 | |||
| 19 | + | ShaderUniformBufferArrayNonUniformIndexing vk.Bool32 | |||
| 20 | + | ShaderSampledImageArrayNonUniformIndexing vk.Bool32 | |||
| 21 | + | ShaderStorageBufferArrayNonUniformIndexing vk.Bool32 | |||
| 22 | + | ShaderStorageImageArrayNonUniformIndexing vk.Bool32 | |||
| 23 | + | ShaderInputAttachmentArrayNonUniformIndexing vk.Bool32 | |||
| 24 | + | ShaderUniformTexelBufferArrayNonUniformIndexing vk.Bool32 | |||
| 25 | + | ShaderStorageTexelBufferArrayNonUniformIndexing vk.Bool32 | |||
| 26 | + | DescriptorBindingUniformBufferUpdateAfterBind vk.Bool32 | |||
| 27 | + | DescriptorBindingSampledImageUpdateAfterBind vk.Bool32 | |||
| 28 | + | DescriptorBindingStorageImageUpdateAfterBind vk.Bool32 | |||
| 29 | + | DescriptorBindingStorageBufferUpdateAfterBind vk.Bool32 | |||
| 30 | + | DescriptorBindingUniformTexelBufferUpdateAfterBind vk.Bool32 | |||
| 31 | + | DescriptorBindingStorageTexelBufferUpdateAfterBind vk.Bool32 | |||
| 32 | + | DescriptorBindingUpdateUnusedWhilePending vk.Bool32 | |||
| 33 | + | DescriptorBindingPartiallyBound vk.Bool32 | |||
| 34 | + | DescriptorBindingVariableDescriptorCount vk.Bool32 | |||
| 35 | + | RuntimeDescriptorArray vk.Bool32 | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // composeResourceLimit counts both image planes and the instance SSBO. | |||
| 39 | + | // No update-after-bind/partially-bound features are needed: tables are | |||
| 40 | + | // completely initialized and updated only after their frame-slot fence. | |||
| 41 | + | func composeResourceLimit(l vk.PhysicalDeviceLimits) uint32 { | |||
| 42 | + | if l.MaxDescriptorSetStorageBuffers <= 1 || l.MaxPerStageResources == 0 { | |||
| 43 | + | return 0 | |||
| 44 | + | } | |||
| 45 | + | return min(uint32(maxComposeResources), | |||
| 46 | + | l.MaxPerStageDescriptorSamplers/2, l.MaxDescriptorSetSamplers/2, | |||
| 47 | + | l.MaxPerStageDescriptorSampledImages/2, l.MaxDescriptorSetSampledImages/2, | |||
| 48 | + | l.MaxPerStageDescriptorStorageBuffers, l.MaxDescriptorSetStorageBuffers-1, | |||
| 49 | + | (l.MaxPerStageResources-1)/3) | |||
| 50 | + | } | |||
A internal/adapters/vulkan/compose_resources_test.go +62 -0
| @@ -0,0 +1,62 @@ | |||
| 1 | + | package vulkan | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "image/color" | |
| 5 | + | "slices" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // A mixed resource table must select SHM pixels, RGB images, and both YUV | |
| 12 | + | // planes independently within the same draw call. | |
| 13 | + | func TestComposeIndexedMixedResources(t *testing.T) { | |
| 14 | + | r, err := New(64, 16) | |
| 15 | + | if err != nil { | |
| 16 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 17 | + | } | |
| 18 | + | defer r.Close() | |
| 19 | + | if !r.compose.indexing || r.compose.resources < 3 { | |
| 20 | + | t.Skip("nonuniform indexing of three resources unavailable") | |
| 21 | + | } | |
| 22 | + | yuv := yuvTestBuffer(t, false, 63, 102, 240) | |
| 23 | + | if !slices.Contains(r.DMABuf().Formats, ports.DMABufFormat{Format: yuv.Format}) { | |
| 24 | + | t.Skip("linear NV12 not importable") | |
| 25 | + | } | |
| 26 | + | rgbFormat := fourcc('A', 'R', '2', '4') | |
| 27 | + | if !slices.Contains(r.DMABuf().Formats, ports.DMABufFormat{Format: rgbFormat}) { | |
| 28 | + | t.Skip("linear ARGB not importable") | |
| 29 | + | } | |
| 30 | + | rgb := &ports.DMABuf{ID: 991, Width: 64, Height: 16, Format: rgbFormat, Planes: []ports.DMABufPlane{{File: udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{255, 0, 0, 255} }), Stride: 256}}} | |
| 31 | + | shm := solidContent(t, 64, 16, color.RGBA{G: 255, A: 255}) | |
| 32 | + | shm.ID, shm.Seq, shm.Version = 2, 1, 1 | |
| 33 | + | scene := ports.Scene{Layers: []ports.SceneLayer{ | |
| 34 | + | {ID: 1, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 64, H: 16}}, | |
| 35 | + | {ID: 2, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 16, W: 32, H: 16}}, | |
| 36 | + | {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 32, W: 16, H: 16}}, | |
| 37 | + | }} | |
| 38 | + | contents := map[ports.WindowID]ports.SurfaceContent{ | |
| 39 | + | 1: {ID: 1, Width: 64, Height: 16, Opaque: true, DMABuf: yuv, Color: ports.SurfaceColor{Coefficients: 2, Range: 2, Chroma: 1}}, | |
| 40 | + | 2: shm, | |
| 41 | + | 3: {ID: 3, Width: 64, Height: 16, Opaque: true, DMABuf: rgb}, | |
| 42 | + | } | |
| 43 | + | for range 4 { | |
| 44 | + | if err := render(r, scene, contents); err != nil { | |
| 45 | + | t.Fatal(err) | |
| 46 | + | } | |
| 47 | + | pixels := readPixels(t, r) | |
| 48 | + | for _, tc := range []struct { | |
| 49 | + | x int | |
| 50 | + | want color.RGBA | |
| 51 | + | }{{8, color.RGBA{R: 255, A: 255}}, {24, color.RGBA{G: 255, A: 255}}, {40, color.RGBA{B: 255, A: 255}}} { | |
| 52 | + | if got := pixels.RGBAAt(tc.x, 8); !near(got, tc.want, 8) { | |
| 53 | + | t.Fatalf("resource at x=%d: %v, want %v", tc.x, got, tc.want) | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | for i := range r.slots { | |
| 58 | + | if len(r.slots[i].batches) != 1 || r.slots[i].batches[0].count != 3 { | |
| 59 | + | t.Fatalf("slot %d did not batch the three resource modes", i) | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | } | |
| @@ -0,0 +1,62 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "image/color" | |||
| 5 | + | "slices" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // A mixed resource table must select SHM pixels, RGB images, and both YUV | |||
| 12 | + | // planes independently within the same draw call. | |||
| 13 | + | func TestComposeIndexedMixedResources(t *testing.T) { | |||
| 14 | + | r, err := New(64, 16) | |||
| 15 | + | if err != nil { | |||
| 16 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 17 | + | } | |||
| 18 | + | defer r.Close() | |||
| 19 | + | if !r.compose.indexing || r.compose.resources < 3 { | |||
| 20 | + | t.Skip("nonuniform indexing of three resources unavailable") | |||
| 21 | + | } | |||
| 22 | + | yuv := yuvTestBuffer(t, false, 63, 102, 240) | |||
| 23 | + | if !slices.Contains(r.DMABuf().Formats, ports.DMABufFormat{Format: yuv.Format}) { | |||
| 24 | + | t.Skip("linear NV12 not importable") | |||
| 25 | + | } | |||
| 26 | + | rgbFormat := fourcc('A', 'R', '2', '4') | |||
| 27 | + | if !slices.Contains(r.DMABuf().Formats, ports.DMABufFormat{Format: rgbFormat}) { | |||
| 28 | + | t.Skip("linear ARGB not importable") | |||
| 29 | + | } | |||
| 30 | + | rgb := &ports.DMABuf{ID: 991, Width: 64, Height: 16, Format: rgbFormat, Planes: []ports.DMABufPlane{{File: udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{255, 0, 0, 255} }), Stride: 256}}} | |||
| 31 | + | shm := solidContent(t, 64, 16, color.RGBA{G: 255, A: 255}) | |||
| 32 | + | shm.ID, shm.Seq, shm.Version = 2, 1, 1 | |||
| 33 | + | scene := ports.Scene{Layers: []ports.SceneLayer{ | |||
| 34 | + | {ID: 1, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 64, H: 16}}, | |||
| 35 | + | {ID: 2, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 16, W: 32, H: 16}}, | |||
| 36 | + | {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 32, W: 16, H: 16}}, | |||
| 37 | + | }} | |||
| 38 | + | contents := map[ports.WindowID]ports.SurfaceContent{ | |||
| 39 | + | 1: {ID: 1, Width: 64, Height: 16, Opaque: true, DMABuf: yuv, Color: ports.SurfaceColor{Coefficients: 2, Range: 2, Chroma: 1}}, | |||
| 40 | + | 2: shm, | |||
| 41 | + | 3: {ID: 3, Width: 64, Height: 16, Opaque: true, DMABuf: rgb}, | |||
| 42 | + | } | |||
| 43 | + | for range 4 { | |||
| 44 | + | if err := render(r, scene, contents); err != nil { | |||
| 45 | + | t.Fatal(err) | |||
| 46 | + | } | |||
| 47 | + | pixels := readPixels(t, r) | |||
| 48 | + | for _, tc := range []struct { | |||
| 49 | + | x int | |||
| 50 | + | want color.RGBA | |||
| 51 | + | }{{8, color.RGBA{R: 255, A: 255}}, {24, color.RGBA{G: 255, A: 255}}, {40, color.RGBA{B: 255, A: 255}}} { | |||
| 52 | + | if got := pixels.RGBAAt(tc.x, 8); !near(got, tc.want, 8) { | |||
| 53 | + | t.Fatalf("resource at x=%d: %v, want %v", tc.x, got, tc.want) | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | for i := range r.slots { | |||
| 58 | + | if len(r.slots[i].batches) != 1 || r.slots[i].batches[0].count != 3 { | |||
| 59 | + | t.Fatalf("slot %d did not batch the three resource modes", i) | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | } | |||
M internal/adapters/vulkan/dmabuf.go +2 -10
| @@ -186,8 +186,7 @@ func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modi | |||
| 186 | 186 | // DMABuf reports the formats this renderer imports. | |
| 187 | 187 | func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf } | |
| 188 | 188 | ||
| 189 | - | // imported is a client dmabuf bound to a VkImage, with the view and set | |
| 190 | - | // the compose shader samples it through. | |
| 189 | + | // imported is a client dmabuf bound to a VkImage and its sampled views. | |
| 191 | 190 | type imported struct { | |
| 192 | 191 | image vk.Image | |
| 193 | 192 | memory vk.DeviceMemory | |
| @@ -199,8 +198,6 @@ type imported struct { | |||
| 199 | 198 | // disjoint is available for this format/modifier; required only when | |
| 200 | 199 | // the two planes refer to different underlying DMA-BUF objects. | |
| 201 | 200 | disjoint bool | |
| 202 | - | pool vk.DescriptorPool | |
| 203 | - | set vk.DescriptorSet | |
| 204 | 201 | fd int // our duplicate of plane 0, for implicit sync | |
| 205 | 202 | // last is the frame that drew it, for eviction. | |
| 206 | 203 | last uint64 | |
| @@ -279,9 +276,6 @@ func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) { | |||
| 279 | 276 | if im.view, err = r.imageView(im.image, format); err != nil { | |
| 280 | 277 | return nil, err | |
| 281 | 278 | } | |
| 282 | - | if im.pool, im.set, err = r.newSet(im.view, r.compose.dummyBuffer.buffer); err != nil { | |
| 283 | - | return nil, err | |
| 284 | - | } | |
| 285 | 279 | ok = true | |
| 286 | 280 | r.imports[b.ID] = im | |
| 287 | 281 | return im, nil | |
| @@ -305,9 +299,7 @@ func dupFile(f *os.File) (int, error) { | |||
| 305 | 299 | } | |
| 306 | 300 | ||
| 307 | 301 | func (r *Renderer) release(im *imported) { | |
| 308 | - | if im.pool != 0 { | |
| 309 | - | r.dd.DestroyDescriptorPool(r.device, im.pool, nil) | |
| 310 | - | } | |
| 302 | + | r.invalidateCompose(resourceKey{im: im}) | |
| 311 | 303 | if im.view != 0 { | |
| 312 | 304 | r.dd.DestroyImageView(r.device, im.view, nil) | |
| 313 | 305 | } | |
| @@ -186,8 +186,7 @@ func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modi | |||||
| 186 | // DMABuf reports the formats this renderer imports. | 186 | // DMABuf reports the formats this renderer imports. | ||
| 187 | func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf } | 187 | func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf } | ||
| 188 | 188 | ||||
| 189 | - | // imported is a client dmabuf bound to a VkImage, with the view and set | 189 | + | // imported is a client dmabuf bound to a VkImage and its sampled views. |
| 190 | - | // the compose shader samples it through. | |||
| 191 | type imported struct { | 190 | type imported struct { | ||
| 192 | image vk.Image | 191 | image vk.Image | ||
| 193 | memory vk.DeviceMemory | 192 | memory vk.DeviceMemory | ||
| @@ -199,8 +198,6 @@ type imported struct { | |||||
| 199 | // disjoint is available for this format/modifier; required only when | 198 | // disjoint is available for this format/modifier; required only when | ||
| 200 | // the two planes refer to different underlying DMA-BUF objects. | 199 | // the two planes refer to different underlying DMA-BUF objects. | ||
| 201 | disjoint bool | 200 | disjoint bool | ||
| 202 | - | pool vk.DescriptorPool | |||
| 203 | - | set vk.DescriptorSet | |||
| 204 | fd int // our duplicate of plane 0, for implicit sync | 201 | fd int // our duplicate of plane 0, for implicit sync | ||
| 205 | // last is the frame that drew it, for eviction. | 202 | // last is the frame that drew it, for eviction. | ||
| 206 | last uint64 | 203 | last uint64 | ||
| @@ -279,9 +276,6 @@ func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) { | |||||
| 279 | if im.view, err = r.imageView(im.image, format); err != nil { | 276 | if im.view, err = r.imageView(im.image, format); err != nil { | ||
| 280 | return nil, err | 277 | return nil, err | ||
| 281 | } | 278 | } | ||
| 282 | - | if im.pool, im.set, err = r.newSet(im.view, r.compose.dummyBuffer.buffer); err != nil { | |||
| 283 | - | return nil, err | |||
| 284 | - | } | |||
| 285 | ok = true | 279 | ok = true | ||
| 286 | r.imports[b.ID] = im | 280 | r.imports[b.ID] = im | ||
| 287 | return im, nil | 281 | return im, nil | ||
| @@ -305,9 +299,7 @@ func dupFile(f *os.File) (int, error) { | |||||
| 305 | } | 299 | } | ||
| 306 | 300 | ||||
| 307 | func (r *Renderer) release(im *imported) { | 301 | func (r *Renderer) release(im *imported) { | ||
| 308 | - | if im.pool != 0 { | 302 | + | r.invalidateCompose(resourceKey{im: im}) |
| 309 | - | r.dd.DestroyDescriptorPool(r.device, im.pool, nil) | |||
| 310 | - | } | |||
| 311 | if im.view != 0 { | 303 | if im.view != 0 { | ||
| 312 | r.dd.DestroyImageView(r.device, im.view, nil) | 304 | r.dd.DestroyImageView(r.device, im.view, nil) | ||
| 313 | } | 305 | } | ||
M internal/adapters/vulkan/frame.go +12 -4
| @@ -27,9 +27,10 @@ type frameSlot struct { | |||
| 27 | 27 | cmd vk.CommandBuffer | |
| 28 | 28 | fence vk.Fence | |
| 29 | 29 | // done is signalled with the frame and exported as its sync file. | |
| 30 | - | done vk.Semaphore | |
| 31 | - | frame uint64 // the frame recorded last, 0 before any | |
| 32 | - | busy bool // submitted, fence not yet seen signalled | |
| 30 | + | done vk.Semaphore | |
| 31 | + | frame uint64 // the frame recorded last, 0 before any | |
| 32 | + | busy bool // submitted, fence not yet seen signalled | |
| 33 | + | batches []*composeBatch | |
| 33 | 34 | } | |
| 34 | 35 | ||
| 35 | 36 | // callScratch holds the structs Render and its record helpers pass to Vulkan | |
| @@ -102,6 +103,9 @@ func (r *Renderer) destroySlots() { | |||
| 102 | 103 | d := r.dd | |
| 103 | 104 | for i := range r.slots { | |
| 104 | 105 | s := &r.slots[i] | |
| 106 | + | for j := range s.batches { | |
| 107 | + | r.freeComposeBatch(s.batches[j]) | |
| 108 | + | } | |
| 105 | 109 | if s.fence != 0 { | |
| 106 | 110 | d.DestroyFence(r.device, s.fence, nil) | |
| 107 | 111 | } | |
| @@ -224,6 +228,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 224 | 228 | r.log.Debug().Int("redrawn", redrawn).Int("draws", len(ds)).Bool("partial", partial).Msg("frame damage") | |
| 225 | 229 | } | |
| 226 | 230 | r.orient(ds, s.Transform) | |
| 231 | + | batches, err := r.prepareCompose(slot, ds) | |
| 232 | + | if err != nil { | |
| 233 | + | return nil, err | |
| 234 | + | } | |
| 227 | 235 | d := r.dd | |
| 228 | 236 | sc := &r.call | |
| 229 | 237 | cmd := slot.cmd | |
| @@ -267,7 +275,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 267 | 275 | } | |
| 268 | 276 | r.frameDMAs = dmas | |
| 269 | 277 | r.ownership(cmd, dmas, true) | |
| 270 | - | r.recordDraws(cmd, tg.view, parseColor(s.Background), ds, partial) | |
| 278 | + | r.recordDraws(cmd, tg.view, parseColor(s.Background), batches, partial) | |
| 271 | 279 | r.ownership(cmd, dmas, false) | |
| 272 | 280 | if hdr { | |
| 273 | 281 | r.recordHDR(cmd, r.targets[r.current]) | |
| @@ -27,9 +27,10 @@ type frameSlot struct { | |||||
| 27 | cmd vk.CommandBuffer | 27 | cmd vk.CommandBuffer | ||
| 28 | fence vk.Fence | 28 | fence vk.Fence | ||
| 29 | // done is signalled with the frame and exported as its sync file. | 29 | // done is signalled with the frame and exported as its sync file. | ||
| 30 | - | done vk.Semaphore | 30 | + | done vk.Semaphore |
| 31 | - | frame uint64 // the frame recorded last, 0 before any | 31 | + | frame uint64 // the frame recorded last, 0 before any |
| 32 | - | busy bool // submitted, fence not yet seen signalled | 32 | + | busy bool // submitted, fence not yet seen signalled |
| 33 | + | batches []*composeBatch | |||
| 33 | } | 34 | } | ||
| 34 | 35 | ||||
| 35 | // callScratch holds the structs Render and its record helpers pass to Vulkan | 36 | // callScratch holds the structs Render and its record helpers pass to Vulkan | ||
| @@ -102,6 +103,9 @@ func (r *Renderer) destroySlots() { | |||||
| 102 | d := r.dd | 103 | d := r.dd | ||
| 103 | for i := range r.slots { | 104 | for i := range r.slots { | ||
| 104 | s := &r.slots[i] | 105 | s := &r.slots[i] | ||
| 106 | + | for j := range s.batches { | |||
| 107 | + | r.freeComposeBatch(s.batches[j]) | |||
| 108 | + | } | |||
| 105 | if s.fence != 0 { | 109 | if s.fence != 0 { | ||
| 106 | d.DestroyFence(r.device, s.fence, nil) | 110 | d.DestroyFence(r.device, s.fence, nil) | ||
| 107 | } | 111 | } | ||
| @@ -224,6 +228,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 224 | r.log.Debug().Int("redrawn", redrawn).Int("draws", len(ds)).Bool("partial", partial).Msg("frame damage") | 228 | r.log.Debug().Int("redrawn", redrawn).Int("draws", len(ds)).Bool("partial", partial).Msg("frame damage") | ||
| 225 | } | 229 | } | ||
| 226 | r.orient(ds, s.Transform) | 230 | r.orient(ds, s.Transform) | ||
| 231 | + | batches, err := r.prepareCompose(slot, ds) | |||
| 232 | + | if err != nil { | |||
| 233 | + | return nil, err | |||
| 234 | + | } | |||
| 227 | d := r.dd | 235 | d := r.dd | ||
| 228 | sc := &r.call | 236 | sc := &r.call | ||
| 229 | cmd := slot.cmd | 237 | cmd := slot.cmd | ||
| @@ -267,7 +275,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 267 | } | 275 | } | ||
| 268 | r.frameDMAs = dmas | 276 | r.frameDMAs = dmas | ||
| 269 | r.ownership(cmd, dmas, true) | 277 | r.ownership(cmd, dmas, true) | ||
| 270 | - | r.recordDraws(cmd, tg.view, parseColor(s.Background), ds, partial) | 278 | + | r.recordDraws(cmd, tg.view, parseColor(s.Background), batches, partial) |
| 271 | r.ownership(cmd, dmas, false) | 279 | r.ownership(cmd, dmas, false) | ||
| 272 | if hdr { | 280 | if hdr { | ||
| 273 | r.recordHDR(cmd, r.targets[r.current]) | 281 | r.recordHDR(cmd, r.targets[r.current]) | ||
M internal/adapters/vulkan/hdr.go +6 -2
| @@ -59,7 +59,11 @@ func (r *Renderer) SetHDR(nits float64) { | |||
| 59 | 59 | } | |
| 60 | 60 | } | |
| 61 | 61 | if r.compose.hdrPipeline == 0 { | |
| 62 | - | r.hdrError = r.createGraphicsPipeline(composeVert, composeHDRFrag, vk.FormatR16g16b16a16Sfloat, r.compose.layout, true, &r.compose.hdrPipeline) | |
| 62 | + | fragment := composeHDRFrag | |
| 63 | + | if !r.compose.indexing { | |
| 64 | + | fragment = composeHDRSingleFrag | |
| 65 | + | } | |
| 66 | + | r.hdrError = r.createComposePipeline(fragment, vk.FormatR16g16b16a16Sfloat, &r.compose.hdrPipeline) | |
| 63 | 67 | if r.hdrError != nil { | |
| 64 | 68 | r.dropHDROwn() | |
| 65 | 69 | return | |
| @@ -124,7 +128,7 @@ func (r *Renderer) createHDR() error { | |||
| 124 | 128 | if err := checked("vkCreatePipelineLayout(HDR)", d.CreatePipelineLayout(r.device, &pli, nil, &h.layout)); err != nil { | |
| 125 | 129 | return err | |
| 126 | 130 | } | |
| 127 | - | if err := r.createGraphicsPipeline(hdrVert, hdrFrag, vk.FormatA2r10g10b10UnormPack32, h.layout, false, &h.pipeline); err != nil { | |
| 131 | + | if err := r.createGraphicsPipeline(hdrVert, hdrFrag, vk.FormatA2r10g10b10UnormPack32, h.layout, false, nil, &h.pipeline); err != nil { | |
| 128 | 132 | return err | |
| 129 | 133 | } | |
| 130 | 134 | si := vk.SamplerCreateInfo{SType: vk.StructureTypeSamplerCreateInfo, MagFilter: vk.FilterNearest, MinFilter: vk.FilterNearest, AddressModeU: vk.SamplerAddressModeClampToEdge, AddressModeV: vk.SamplerAddressModeClampToEdge, AddressModeW: vk.SamplerAddressModeClampToEdge} | |
| @@ -59,7 +59,11 @@ func (r *Renderer) SetHDR(nits float64) { | |||||
| 59 | } | 59 | } | ||
| 60 | } | 60 | } | ||
| 61 | if r.compose.hdrPipeline == 0 { | 61 | if r.compose.hdrPipeline == 0 { | ||
| 62 | - | r.hdrError = r.createGraphicsPipeline(composeVert, composeHDRFrag, vk.FormatR16g16b16a16Sfloat, r.compose.layout, true, &r.compose.hdrPipeline) | 62 | + | fragment := composeHDRFrag |
| 63 | + | if !r.compose.indexing { | |||
| 64 | + | fragment = composeHDRSingleFrag | |||
| 65 | + | } | |||
| 66 | + | r.hdrError = r.createComposePipeline(fragment, vk.FormatR16g16b16a16Sfloat, &r.compose.hdrPipeline) | |||
| 63 | if r.hdrError != nil { | 67 | if r.hdrError != nil { | ||
| 64 | r.dropHDROwn() | 68 | r.dropHDROwn() | ||
| 65 | return | 69 | return | ||
| @@ -124,7 +128,7 @@ func (r *Renderer) createHDR() error { | |||||
| 124 | if err := checked("vkCreatePipelineLayout(HDR)", d.CreatePipelineLayout(r.device, &pli, nil, &h.layout)); err != nil { | 128 | if err := checked("vkCreatePipelineLayout(HDR)", d.CreatePipelineLayout(r.device, &pli, nil, &h.layout)); err != nil { | ||
| 125 | return err | 129 | return err | ||
| 126 | } | 130 | } | ||
| 127 | - | if err := r.createGraphicsPipeline(hdrVert, hdrFrag, vk.FormatA2r10g10b10UnormPack32, h.layout, false, &h.pipeline); err != nil { | 131 | + | if err := r.createGraphicsPipeline(hdrVert, hdrFrag, vk.FormatA2r10g10b10UnormPack32, h.layout, false, nil, &h.pipeline); err != nil { |
| 128 | return err | 132 | return err | ||
| 129 | } | 133 | } | ||
| 130 | si := vk.SamplerCreateInfo{SType: vk.StructureTypeSamplerCreateInfo, MagFilter: vk.FilterNearest, MinFilter: vk.FilterNearest, AddressModeU: vk.SamplerAddressModeClampToEdge, AddressModeV: vk.SamplerAddressModeClampToEdge, AddressModeW: vk.SamplerAddressModeClampToEdge} | 134 | si := vk.SamplerCreateInfo{SType: vk.StructureTypeSamplerCreateInfo, MagFilter: vk.FilterNearest, MinFilter: vk.FilterNearest, AddressModeU: vk.SamplerAddressModeClampToEdge, AddressModeV: vk.SamplerAddressModeClampToEdge, AddressModeW: vk.SamplerAddressModeClampToEdge} | ||
M internal/adapters/vulkan/renderer.go +20 -1
| @@ -214,7 +214,23 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 214 | 214 | priority := float32(1) | |
| 215 | 215 | qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority} | |
| 216 | 216 | dynamicRendering := vk.PhysicalDeviceDynamicRenderingFeatures{SType: vk.StructureTypePhysicalDeviceDynamicRenderingFeatures, DynamicRendering: 1} | |
| 217 | - | di := vk.DeviceCreateInfo{SType: vk.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&dynamicRendering), QueueCreateInfoCount: 1, QueueCreateInfos: &qi} | |
| 217 | + | indexing := descriptorIndexingFeatures{SType: descriptorIndexingFeaturesType} | |
| 218 | + | features := vk.PhysicalDeviceFeatures2{SType: vk.StructureTypePhysicalDeviceFeatures2, Next: unsafe.Pointer(&indexing)} | |
| 219 | + | r.id.GetPhysicalDeviceFeatures2(physical, &features) | |
| 220 | + | r.compose.resources = 1 | |
| 221 | + | coreFeatures := vk.PhysicalDeviceFeatures{} | |
| 222 | + | if features.Features.ShaderSampledImageArrayDynamicIndexing != 0 && features.Features.ShaderStorageBufferArrayDynamicIndexing != 0 && indexing.ShaderSampledImageArrayNonUniformIndexing != 0 && indexing.ShaderStorageBufferArrayNonUniformIndexing != 0 { | |
| 223 | + | limit := composeResourceLimit(properties.Limits) | |
| 224 | + | if limit > 1 { | |
| 225 | + | r.compose.resources, r.compose.indexing = limit, true | |
| 226 | + | coreFeatures.ShaderSampledImageArrayDynamicIndexing = 1 | |
| 227 | + | coreFeatures.ShaderStorageBufferArrayDynamicIndexing = 1 | |
| 228 | + | // Enable only the array indexing features used by these shaders. | |
| 229 | + | indexing = descriptorIndexingFeatures{SType: descriptorIndexingFeaturesType, ShaderSampledImageArrayNonUniformIndexing: 1, ShaderStorageBufferArrayNonUniformIndexing: 1} | |
| 230 | + | dynamicRendering.Next = unsafe.Pointer(&indexing) | |
| 231 | + | } | |
| 232 | + | } | |
| 233 | + | di := vk.DeviceCreateInfo{SType: vk.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&dynamicRendering), QueueCreateInfoCount: 1, QueueCreateInfos: &qi, EnabledFeatures: &coreFeatures} | |
| 218 | 234 | // dmabuf import needs every extension; without them clients use wl_shm. | |
| 219 | 235 | var extNames [][]byte | |
| 220 | 236 | var extPtrs []*byte | |
| @@ -264,6 +280,9 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 264 | 280 | } | |
| 265 | 281 | result := r.id.CreateDevice(physical, &di, nil, &r.device) | |
| 266 | 282 | runtime.KeepAlive(global) | |
| 283 | + | runtime.KeepAlive(coreFeatures) | |
| 284 | + | runtime.KeepAlive(indexing) | |
| 285 | + | runtime.KeepAlive(dynamicRendering) | |
| 267 | 286 | runtime.KeepAlive(qi) | |
| 268 | 287 | runtime.KeepAlive(extNames) | |
| 269 | 288 | runtime.KeepAlive(extPtrs) | |
| @@ -214,7 +214,23 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 214 | priority := float32(1) | 214 | priority := float32(1) | ||
| 215 | qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority} | 215 | qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority} | ||
| 216 | dynamicRendering := vk.PhysicalDeviceDynamicRenderingFeatures{SType: vk.StructureTypePhysicalDeviceDynamicRenderingFeatures, DynamicRendering: 1} | 216 | dynamicRendering := vk.PhysicalDeviceDynamicRenderingFeatures{SType: vk.StructureTypePhysicalDeviceDynamicRenderingFeatures, DynamicRendering: 1} | ||
| 217 | - | di := vk.DeviceCreateInfo{SType: vk.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&dynamicRendering), QueueCreateInfoCount: 1, QueueCreateInfos: &qi} | 217 | + | indexing := descriptorIndexingFeatures{SType: descriptorIndexingFeaturesType} |
| 218 | + | features := vk.PhysicalDeviceFeatures2{SType: vk.StructureTypePhysicalDeviceFeatures2, Next: unsafe.Pointer(&indexing)} | |||
| 219 | + | r.id.GetPhysicalDeviceFeatures2(physical, &features) | |||
| 220 | + | r.compose.resources = 1 | |||
| 221 | + | coreFeatures := vk.PhysicalDeviceFeatures{} | |||
| 222 | + | if features.Features.ShaderSampledImageArrayDynamicIndexing != 0 && features.Features.ShaderStorageBufferArrayDynamicIndexing != 0 && indexing.ShaderSampledImageArrayNonUniformIndexing != 0 && indexing.ShaderStorageBufferArrayNonUniformIndexing != 0 { | |||
| 223 | + | limit := composeResourceLimit(properties.Limits) | |||
| 224 | + | if limit > 1 { | |||
| 225 | + | r.compose.resources, r.compose.indexing = limit, true | |||
| 226 | + | coreFeatures.ShaderSampledImageArrayDynamicIndexing = 1 | |||
| 227 | + | coreFeatures.ShaderStorageBufferArrayDynamicIndexing = 1 | |||
| 228 | + | // Enable only the array indexing features used by these shaders. | |||
| 229 | + | indexing = descriptorIndexingFeatures{SType: descriptorIndexingFeaturesType, ShaderSampledImageArrayNonUniformIndexing: 1, ShaderStorageBufferArrayNonUniformIndexing: 1} | |||
| 230 | + | dynamicRendering.Next = unsafe.Pointer(&indexing) | |||
| 231 | + | } | |||
| 232 | + | } | |||
| 233 | + | di := vk.DeviceCreateInfo{SType: vk.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&dynamicRendering), QueueCreateInfoCount: 1, QueueCreateInfos: &qi, EnabledFeatures: &coreFeatures} | |||
| 218 | // dmabuf import needs every extension; without them clients use wl_shm. | 234 | // dmabuf import needs every extension; without them clients use wl_shm. | ||
| 219 | var extNames [][]byte | 235 | var extNames [][]byte | ||
| 220 | var extPtrs []*byte | 236 | var extPtrs []*byte | ||
| @@ -264,6 +280,9 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 264 | } | 280 | } | ||
| 265 | result := r.id.CreateDevice(physical, &di, nil, &r.device) | 281 | result := r.id.CreateDevice(physical, &di, nil, &r.device) | ||
| 266 | runtime.KeepAlive(global) | 282 | runtime.KeepAlive(global) | ||
| 283 | + | runtime.KeepAlive(coreFeatures) | |||
| 284 | + | runtime.KeepAlive(indexing) | |||
| 285 | + | runtime.KeepAlive(dynamicRendering) | |||
| 267 | runtime.KeepAlive(qi) | 286 | runtime.KeepAlive(qi) | ||
| 268 | runtime.KeepAlive(extNames) | 287 | runtime.KeepAlive(extNames) | ||
| 269 | runtime.KeepAlive(extPtrs) | 288 | runtime.KeepAlive(extPtrs) | ||
M internal/adapters/vulkan/scene.go +2 -2
| @@ -558,7 +558,7 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon | |||
| 558 | 558 | } | |
| 559 | 559 | dr.pc.buf[3] = uint32(coeff) | uint32(ran)<<8 | uint32(content.Color.Chroma)<<16 | |
| 560 | 560 | } | |
| 561 | - | dr.set, dr.im = im.set, im | |
| 561 | + | dr.im = im | |
| 562 | 562 | dr.acquire = content.Acquire | |
| 563 | 563 | w.draws = append(w.draws, dr) | |
| 564 | 564 | return | |
| @@ -591,7 +591,7 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon | |||
| 591 | 591 | r.setContentColor(&dr, content) | |
| 592 | 592 | dr.pc.color[3] *= w.alpha | |
| 593 | 593 | dr.pc.mapy[2] = w.pulse | |
| 594 | - | dr.set = c.set | |
| 594 | + | dr.shm = c | |
| 595 | 595 | dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0} | |
| 596 | 596 | w.draws = append(w.draws, dr) | |
| 597 | 597 | } | |
| @@ -558,7 +558,7 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon | |||||
| 558 | } | 558 | } | ||
| 559 | dr.pc.buf[3] = uint32(coeff) | uint32(ran)<<8 | uint32(content.Color.Chroma)<<16 | 559 | dr.pc.buf[3] = uint32(coeff) | uint32(ran)<<8 | uint32(content.Color.Chroma)<<16 | ||
| 560 | } | 560 | } | ||
| 561 | - | dr.set, dr.im = im.set, im | 561 | + | dr.im = im |
| 562 | dr.acquire = content.Acquire | 562 | dr.acquire = content.Acquire | ||
| 563 | w.draws = append(w.draws, dr) | 563 | w.draws = append(w.draws, dr) | ||
| 564 | return | 564 | return | ||
| @@ -591,7 +591,7 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon | |||||
| 591 | r.setContentColor(&dr, content) | 591 | r.setContentColor(&dr, content) | ||
| 592 | dr.pc.color[3] *= w.alpha | 592 | dr.pc.color[3] *= w.alpha | ||
| 593 | dr.pc.mapy[2] = w.pulse | 593 | dr.pc.mapy[2] = w.pulse | ||
| 594 | - | dr.set = c.set | 594 | + | dr.shm = c |
| 595 | dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0} | 595 | dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0} | ||
| 596 | w.draws = append(w.draws, dr) | 596 | w.draws = append(w.draws, dr) | ||
| 597 | } | 597 | } | ||
M internal/adapters/vulkan/shaders/compose.frag +23 -9
| @@ -1,23 +1,37 @@ | |||
| 1 | 1 | #version 450 | |
| 2 | + | #ifndef SINGLE_RESOURCE | |
| 3 | + | #extension GL_EXT_nonuniform_qualifier : require | |
| 4 | + | #endif | |
| 2 | 5 | // Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm | |
| 3 | 6 | // buffer (B8G8R8A8 words in a storage buffer). Client pixels are | |
| 4 | 7 | // premultiplied, as Wayland buffers are: the pipeline blends ONE, | |
| 5 | 8 | // ONE_MINUS_SRC_ALPHA. Scaled draws filter linearly; 1:1 draws read the | |
| 6 | 9 | // texel under the fragment, pixel exact. | |
| 7 | 10 | ||
| 8 | - | layout(push_constant) uniform Draw { | |
| 9 | - | ivec4 rect; | |
| 10 | - | vec4 map; | |
| 11 | - | vec4 mapy; | |
| 12 | - | vec4 color; | |
| 13 | - | vec4 crop; | |
| 14 | - | uvec4 buf; | |
| 15 | - | uvec4 misc; | |
| 11 | + | layout(location = 0) in Parameters { | |
| 12 | + | flat vec4 map; | |
| 13 | + | flat vec4 mapy; | |
| 14 | + | flat vec4 color; | |
| 15 | + | flat vec4 crop; | |
| 16 | + | flat uvec4 buf; | |
| 17 | + | flat uvec4 misc; | |
| 18 | + | flat uint resourceIndex; | |
| 16 | 19 | } d; | |
| 17 | 20 | ||
| 21 | + | #ifdef SINGLE_RESOURCE | |
| 18 | 22 | layout(set = 0, binding = 0) uniform sampler2D tex; | |
| 19 | 23 | layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; }; | |
| 20 | 24 | layout(set = 0, binding = 2) uniform sampler2D chromaTex; | |
| 25 | + | #define PIXEL(i) pixels[i] | |
| 26 | + | #else | |
| 27 | + | layout(constant_id = 0) const int resourceCount = 1; | |
| 28 | + | layout(set = 0, binding = 0) uniform sampler2D textures[resourceCount]; | |
| 29 | + | layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; } pixelBuffers[resourceCount]; | |
| 30 | + | layout(set = 0, binding = 2) uniform sampler2D chromaTextures[resourceCount]; | |
| 31 | + | #define tex textures[nonuniformEXT(d.resourceIndex)] | |
| 32 | + | #define chromaTex chromaTextures[nonuniformEXT(d.resourceIndex)] | |
| 33 | + | #define PIXEL(i) pixelBuffers[nonuniformEXT(d.resourceIndex)].pixels[i] | |
| 34 | + | #endif | |
| 21 | 35 | ||
| 22 | 36 | layout(location = 0) out vec4 color; | |
| 23 | 37 | ||
| @@ -84,7 +98,7 @@ vec4 sampleYUV(vec2 src) { | |||
| 84 | 98 | vec4 texel(ivec2 p) { | |
| 85 | 99 | p = clamp(p, ivec2(floor(d.crop.xy)), min(ivec2(d.buf.yz) - 1, ivec2(ceil(d.crop.zw)) - 1)); | |
| 86 | 100 | // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A). | |
| 87 | - | return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw; | |
| 101 | + | return unpackUnorm4x8(PIXEL(d.buf.x + uint(p.y) * d.buf.y + uint(p.x))).zyxw; | |
| 88 | 102 | } | |
| 89 | 103 | ||
| 90 | 104 | // sampleBuffer reads the client buffer at src (buffer pixels). | |
| @@ -1,23 +1,37 @@ | |||||
| 1 | #version 450 | 1 | #version 450 | ||
| 2 | + | #ifndef SINGLE_RESOURCE | |||
| 3 | + | #extension GL_EXT_nonuniform_qualifier : require | |||
| 4 | + | #endif | |||
| 2 | // Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm | 5 | // Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm | ||
| 3 | // buffer (B8G8R8A8 words in a storage buffer). Client pixels are | 6 | // buffer (B8G8R8A8 words in a storage buffer). Client pixels are | ||
| 4 | // premultiplied, as Wayland buffers are: the pipeline blends ONE, | 7 | // premultiplied, as Wayland buffers are: the pipeline blends ONE, | ||
| 5 | // ONE_MINUS_SRC_ALPHA. Scaled draws filter linearly; 1:1 draws read the | 8 | // ONE_MINUS_SRC_ALPHA. Scaled draws filter linearly; 1:1 draws read the | ||
| 6 | // texel under the fragment, pixel exact. | 9 | // texel under the fragment, pixel exact. | ||
| 7 | 10 | ||||
| 8 | - | layout(push_constant) uniform Draw { | 11 | + | layout(location = 0) in Parameters { |
| 9 | - | ivec4 rect; | 12 | + | flat vec4 map; |
| 10 | - | vec4 map; | 13 | + | flat vec4 mapy; |
| 11 | - | vec4 mapy; | 14 | + | flat vec4 color; |
| 12 | - | vec4 color; | 15 | + | flat vec4 crop; |
| 13 | - | vec4 crop; | 16 | + | flat uvec4 buf; |
| 14 | - | uvec4 buf; | 17 | + | flat uvec4 misc; |
| 15 | - | uvec4 misc; | 18 | + | flat uint resourceIndex; |
| 16 | } d; | 19 | } d; | ||
| 17 | 20 | ||||
| 21 | + | #ifdef SINGLE_RESOURCE | |||
| 18 | layout(set = 0, binding = 0) uniform sampler2D tex; | 22 | layout(set = 0, binding = 0) uniform sampler2D tex; | ||
| 19 | layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; }; | 23 | layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; }; | ||
| 20 | layout(set = 0, binding = 2) uniform sampler2D chromaTex; | 24 | layout(set = 0, binding = 2) uniform sampler2D chromaTex; | ||
| 25 | + | #define PIXEL(i) pixels[i] | |||
| 26 | + | #else | |||
| 27 | + | layout(constant_id = 0) const int resourceCount = 1; | |||
| 28 | + | layout(set = 0, binding = 0) uniform sampler2D textures[resourceCount]; | |||
| 29 | + | layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; } pixelBuffers[resourceCount]; | |||
| 30 | + | layout(set = 0, binding = 2) uniform sampler2D chromaTextures[resourceCount]; | |||
| 31 | + | #define tex textures[nonuniformEXT(d.resourceIndex)] | |||
| 32 | + | #define chromaTex chromaTextures[nonuniformEXT(d.resourceIndex)] | |||
| 33 | + | #define PIXEL(i) pixelBuffers[nonuniformEXT(d.resourceIndex)].pixels[i] | |||
| 34 | + | #endif | |||
| 21 | 35 | ||||
| 22 | layout(location = 0) out vec4 color; | 36 | layout(location = 0) out vec4 color; | ||
| 23 | 37 | ||||
| @@ -84,7 +98,7 @@ vec4 sampleYUV(vec2 src) { | |||||
| 84 | vec4 texel(ivec2 p) { | 98 | vec4 texel(ivec2 p) { | ||
| 85 | p = clamp(p, ivec2(floor(d.crop.xy)), min(ivec2(d.buf.yz) - 1, ivec2(ceil(d.crop.zw)) - 1)); | 99 | p = clamp(p, ivec2(floor(d.crop.xy)), min(ivec2(d.buf.yz) - 1, ivec2(ceil(d.crop.zw)) - 1)); | ||
| 86 | // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A). | 100 | // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A). | ||
| 87 | - | return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw; | 101 | + | return unpackUnorm4x8(PIXEL(d.buf.x + uint(p.y) * d.buf.y + uint(p.x))).zyxw; |
| 88 | } | 102 | } | ||
| 89 | 103 | ||||
| 90 | // sampleBuffer reads the client buffer at src (buffer pixels). | 104 | // sampleBuffer reads the client buffer at src (buffer pixels). | ||
M internal/adapters/vulkan/shaders/compose.frag.spv +0 -0 binary file: no text diff
binary file: no text diff
M internal/adapters/vulkan/shaders/compose.vert +30 -10
| @@ -1,19 +1,39 @@ | |||
| 1 | 1 | #version 450 | |
| 2 | - | // One quad per draw: two triangles over the visible target rect. | |
| 2 | + | // One quad per instance, in the scene's paint order. Draw parameters are | |
| 3 | + | // fetched in the vertex stage and passed flat, not read per fragment. | |
| 3 | 4 | ||
| 4 | - | layout(push_constant) uniform Draw { | |
| 5 | - | ivec4 rect; // visible x0, y0, x1, y1 in target pixels | |
| 6 | - | vec4 map; // source texel = map.xy + fragment centre.x * map.zw | |
| 7 | - | vec4 mapy; // + fragment centre.y * mapy.xy: any buffer transform; mapy.z focus pulse | |
| 8 | - | vec4 color; // solid fills: premultiplied RGBA | |
| 9 | - | vec4 crop; // source crop bounds in buffer pixels | |
| 10 | - | uvec4 buf; // buffer draws: first word, row width, height, unused | |
| 11 | - | uvec4 misc; // mode, flags, target width, target height | |
| 12 | - | } d; | |
| 5 | + | struct Draw { | |
| 6 | + | ivec4 rect; // visible x0, y0, x1, y1 in target pixels | |
| 7 | + | vec4 map; // source texel = map.xy + fragment centre.x * map.zw | |
| 8 | + | vec4 mapy; // + fragment centre.y * mapy.xy; mapy.z is the focus pulse | |
| 9 | + | vec4 color; // solid premultiplied RGBA; content opacity in alpha | |
| 10 | + | vec4 crop; // source crop bounds in buffer pixels | |
| 11 | + | uvec4 buf; // pixel-buffer first word, width, height; YUV metadata in w | |
| 12 | + | uvec4 misc; // mode, flags, target width, target height | |
| 13 | + | uvec4 resourceIndex; // resource-table slot in x; remaining words padding | |
| 14 | + | }; | |
| 15 | + | layout(std430, set = 0, binding = 3) readonly buffer Draws { Draw draws[]; }; | |
| 16 | + | layout(location = 0) out Parameters { | |
| 17 | + | flat vec4 map; | |
| 18 | + | flat vec4 mapy; | |
| 19 | + | flat vec4 color; | |
| 20 | + | flat vec4 crop; | |
| 21 | + | flat uvec4 buf; | |
| 22 | + | flat uvec4 misc; | |
| 23 | + | flat uint resourceIndex; | |
| 24 | + | } parameters; | |
| 13 | 25 | ||
| 14 | 26 | void main() { | |
| 27 | + | Draw d = draws[gl_InstanceIndex]; | |
| 15 | 28 | const ivec2 corners[6] = ivec2[](ivec2(0, 0), ivec2(1, 0), ivec2(0, 1), ivec2(0, 1), ivec2(1, 0), ivec2(1, 1)); | |
| 16 | 29 | ivec2 c = corners[gl_VertexIndex]; | |
| 17 | 30 | vec2 p = vec2(c.x == 0 ? d.rect.x : d.rect.z, c.y == 0 ? d.rect.y : d.rect.w); | |
| 18 | 31 | gl_Position = vec4(p / vec2(d.misc.zw) * 2.0 - 1.0, 0.0, 1.0); | |
| 32 | + | parameters.map = d.map; | |
| 33 | + | parameters.mapy = d.mapy; | |
| 34 | + | parameters.color = d.color; | |
| 35 | + | parameters.crop = d.crop; | |
| 36 | + | parameters.buf = d.buf; | |
| 37 | + | parameters.misc = d.misc; | |
| 38 | + | parameters.resourceIndex = d.resourceIndex.x; | |
| 19 | 39 | } | |
| @@ -1,19 +1,39 @@ | |||||
| 1 | #version 450 | 1 | #version 450 | ||
| 2 | - | // One quad per draw: two triangles over the visible target rect. | 2 | + | // One quad per instance, in the scene's paint order. Draw parameters are |
| 3 | + | // fetched in the vertex stage and passed flat, not read per fragment. | |||
| 3 | 4 | ||||
| 4 | - | layout(push_constant) uniform Draw { | 5 | + | struct Draw { |
| 5 | - | ivec4 rect; // visible x0, y0, x1, y1 in target pixels | 6 | + | ivec4 rect; // visible x0, y0, x1, y1 in target pixels |
| 6 | - | vec4 map; // source texel = map.xy + fragment centre.x * map.zw | 7 | + | vec4 map; // source texel = map.xy + fragment centre.x * map.zw |
| 7 | - | vec4 mapy; // + fragment centre.y * mapy.xy: any buffer transform; mapy.z focus pulse | 8 | + | vec4 mapy; // + fragment centre.y * mapy.xy; mapy.z is the focus pulse |
| 8 | - | vec4 color; // solid fills: premultiplied RGBA | 9 | + | vec4 color; // solid premultiplied RGBA; content opacity in alpha |
| 9 | - | vec4 crop; // source crop bounds in buffer pixels | 10 | + | vec4 crop; // source crop bounds in buffer pixels |
| 10 | - | uvec4 buf; // buffer draws: first word, row width, height, unused | 11 | + | uvec4 buf; // pixel-buffer first word, width, height; YUV metadata in w |
| 11 | - | uvec4 misc; // mode, flags, target width, target height | 12 | + | uvec4 misc; // mode, flags, target width, target height |
| 12 | - | } d; | 13 | + | uvec4 resourceIndex; // resource-table slot in x; remaining words padding |
| 14 | + | }; | |||
| 15 | + | layout(std430, set = 0, binding = 3) readonly buffer Draws { Draw draws[]; }; | |||
| 16 | + | layout(location = 0) out Parameters { | |||
| 17 | + | flat vec4 map; | |||
| 18 | + | flat vec4 mapy; | |||
| 19 | + | flat vec4 color; | |||
| 20 | + | flat vec4 crop; | |||
| 21 | + | flat uvec4 buf; | |||
| 22 | + | flat uvec4 misc; | |||
| 23 | + | flat uint resourceIndex; | |||
| 24 | + | } parameters; | |||
| 13 | 25 | ||||
| 14 | void main() { | 26 | void main() { | ||
| 27 | + | Draw d = draws[gl_InstanceIndex]; | |||
| 15 | const ivec2 corners[6] = ivec2[](ivec2(0, 0), ivec2(1, 0), ivec2(0, 1), ivec2(0, 1), ivec2(1, 0), ivec2(1, 1)); | 28 | const ivec2 corners[6] = ivec2[](ivec2(0, 0), ivec2(1, 0), ivec2(0, 1), ivec2(0, 1), ivec2(1, 0), ivec2(1, 1)); | ||
| 16 | ivec2 c = corners[gl_VertexIndex]; | 29 | ivec2 c = corners[gl_VertexIndex]; | ||
| 17 | vec2 p = vec2(c.x == 0 ? d.rect.x : d.rect.z, c.y == 0 ? d.rect.y : d.rect.w); | 30 | vec2 p = vec2(c.x == 0 ? d.rect.x : d.rect.z, c.y == 0 ? d.rect.y : d.rect.w); | ||
| 18 | gl_Position = vec4(p / vec2(d.misc.zw) * 2.0 - 1.0, 0.0, 1.0); | 31 | gl_Position = vec4(p / vec2(d.misc.zw) * 2.0 - 1.0, 0.0, 1.0); | ||
| 32 | + | parameters.map = d.map; | |||
| 33 | + | parameters.mapy = d.mapy; | |||
| 34 | + | parameters.color = d.color; | |||
| 35 | + | parameters.crop = d.crop; | |||
| 36 | + | parameters.buf = d.buf; | |||
| 37 | + | parameters.misc = d.misc; | |||
| 38 | + | parameters.resourceIndex = d.resourceIndex.x; | |||
| 19 | } | 39 | } | ||
M internal/adapters/vulkan/shaders/compose.vert.spv +0 -0 binary file: no text diff
binary file: no text diff
M internal/adapters/vulkan/shaders/compose_hdr.frag +23 -9
| @@ -1,23 +1,37 @@ | |||
| 1 | 1 | #version 450 | |
| 2 | + | #ifndef SINGLE_RESOURCE | |
| 3 | + | #extension GL_EXT_nonuniform_qualifier : require | |
| 4 | + | #endif | |
| 2 | 5 | // Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm | |
| 3 | 6 | // buffer (B8G8R8A8 words in a storage buffer). Client pixels are | |
| 4 | 7 | // premultiplied, as Wayland buffers are: the pipeline blends ONE, | |
| 5 | 8 | // ONE_MINUS_SRC_ALPHA. Scaled draws filter linearly; 1:1 draws read the | |
| 6 | 9 | // texel under the fragment, pixel exact. | |
| 7 | 10 | ||
| 8 | - | layout(push_constant) uniform Draw { | |
| 9 | - | ivec4 rect; | |
| 10 | - | vec4 map; | |
| 11 | - | vec4 mapy; | |
| 12 | - | vec4 color; | |
| 13 | - | vec4 crop; | |
| 14 | - | uvec4 buf; | |
| 15 | - | uvec4 misc; | |
| 11 | + | layout(location = 0) in Parameters { | |
| 12 | + | flat vec4 map; | |
| 13 | + | flat vec4 mapy; | |
| 14 | + | flat vec4 color; | |
| 15 | + | flat vec4 crop; | |
| 16 | + | flat uvec4 buf; | |
| 17 | + | flat uvec4 misc; | |
| 18 | + | flat uint resourceIndex; | |
| 16 | 19 | } d; | |
| 17 | 20 | ||
| 21 | + | #ifdef SINGLE_RESOURCE | |
| 18 | 22 | layout(set = 0, binding = 0) uniform sampler2D tex; | |
| 19 | 23 | layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; }; | |
| 20 | 24 | layout(set = 0, binding = 2) uniform sampler2D chromaTex; | |
| 25 | + | #define PIXEL(i) pixels[i] | |
| 26 | + | #else | |
| 27 | + | layout(constant_id = 0) const int resourceCount = 1; | |
| 28 | + | layout(set = 0, binding = 0) uniform sampler2D textures[resourceCount]; | |
| 29 | + | layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; } pixelBuffers[resourceCount]; | |
| 30 | + | layout(set = 0, binding = 2) uniform sampler2D chromaTextures[resourceCount]; | |
| 31 | + | #define tex textures[nonuniformEXT(d.resourceIndex)] | |
| 32 | + | #define chromaTex chromaTextures[nonuniformEXT(d.resourceIndex)] | |
| 33 | + | #define PIXEL(i) pixelBuffers[nonuniformEXT(d.resourceIndex)].pixels[i] | |
| 34 | + | #endif | |
| 21 | 35 | ||
| 22 | 36 | layout(location = 0) out vec4 color; | |
| 23 | 37 | ||
| @@ -83,7 +97,7 @@ vec4 sampleYUV(vec2 src) { | |||
| 83 | 97 | vec4 texel(ivec2 p) { | |
| 84 | 98 | p = clamp(p, ivec2(floor(d.crop.xy)), min(ivec2(d.buf.yz) - 1, ivec2(ceil(d.crop.zw)) - 1)); | |
| 85 | 99 | // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A). | |
| 86 | - | return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw; | |
| 100 | + | return unpackUnorm4x8(PIXEL(d.buf.x + uint(p.y) * d.buf.y + uint(p.x))).zyxw; | |
| 87 | 101 | } | |
| 88 | 102 | ||
| 89 | 103 | // sampleBuffer reads the client buffer at src (buffer pixels). | |
| @@ -1,23 +1,37 @@ | |||||
| 1 | #version 450 | 1 | #version 450 | ||
| 2 | + | #ifndef SINGLE_RESOURCE | |||
| 3 | + | #extension GL_EXT_nonuniform_qualifier : require | |||
| 4 | + | #endif | |||
| 2 | // Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm | 5 | // Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm | ||
| 3 | // buffer (B8G8R8A8 words in a storage buffer). Client pixels are | 6 | // buffer (B8G8R8A8 words in a storage buffer). Client pixels are | ||
| 4 | // premultiplied, as Wayland buffers are: the pipeline blends ONE, | 7 | // premultiplied, as Wayland buffers are: the pipeline blends ONE, | ||
| 5 | // ONE_MINUS_SRC_ALPHA. Scaled draws filter linearly; 1:1 draws read the | 8 | // ONE_MINUS_SRC_ALPHA. Scaled draws filter linearly; 1:1 draws read the | ||
| 6 | // texel under the fragment, pixel exact. | 9 | // texel under the fragment, pixel exact. | ||
| 7 | 10 | ||||
| 8 | - | layout(push_constant) uniform Draw { | 11 | + | layout(location = 0) in Parameters { |
| 9 | - | ivec4 rect; | 12 | + | flat vec4 map; |
| 10 | - | vec4 map; | 13 | + | flat vec4 mapy; |
| 11 | - | vec4 mapy; | 14 | + | flat vec4 color; |
| 12 | - | vec4 color; | 15 | + | flat vec4 crop; |
| 13 | - | vec4 crop; | 16 | + | flat uvec4 buf; |
| 14 | - | uvec4 buf; | 17 | + | flat uvec4 misc; |
| 15 | - | uvec4 misc; | 18 | + | flat uint resourceIndex; |
| 16 | } d; | 19 | } d; | ||
| 17 | 20 | ||||
| 21 | + | #ifdef SINGLE_RESOURCE | |||
| 18 | layout(set = 0, binding = 0) uniform sampler2D tex; | 22 | layout(set = 0, binding = 0) uniform sampler2D tex; | ||
| 19 | layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; }; | 23 | layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; }; | ||
| 20 | layout(set = 0, binding = 2) uniform sampler2D chromaTex; | 24 | layout(set = 0, binding = 2) uniform sampler2D chromaTex; | ||
| 25 | + | #define PIXEL(i) pixels[i] | |||
| 26 | + | #else | |||
| 27 | + | layout(constant_id = 0) const int resourceCount = 1; | |||
| 28 | + | layout(set = 0, binding = 0) uniform sampler2D textures[resourceCount]; | |||
| 29 | + | layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; } pixelBuffers[resourceCount]; | |||
| 30 | + | layout(set = 0, binding = 2) uniform sampler2D chromaTextures[resourceCount]; | |||
| 31 | + | #define tex textures[nonuniformEXT(d.resourceIndex)] | |||
| 32 | + | #define chromaTex chromaTextures[nonuniformEXT(d.resourceIndex)] | |||
| 33 | + | #define PIXEL(i) pixelBuffers[nonuniformEXT(d.resourceIndex)].pixels[i] | |||
| 34 | + | #endif | |||
| 21 | 35 | ||||
| 22 | layout(location = 0) out vec4 color; | 36 | layout(location = 0) out vec4 color; | ||
| 23 | 37 | ||||
| @@ -83,7 +97,7 @@ vec4 sampleYUV(vec2 src) { | |||||
| 83 | vec4 texel(ivec2 p) { | 97 | vec4 texel(ivec2 p) { | ||
| 84 | p = clamp(p, ivec2(floor(d.crop.xy)), min(ivec2(d.buf.yz) - 1, ivec2(ceil(d.crop.zw)) - 1)); | 98 | p = clamp(p, ivec2(floor(d.crop.xy)), min(ivec2(d.buf.yz) - 1, ivec2(ceil(d.crop.zw)) - 1)); | ||
| 85 | // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A). | 99 | // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A). | ||
| 86 | - | return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw; | 100 | + | return unpackUnorm4x8(PIXEL(d.buf.x + uint(p.y) * d.buf.y + uint(p.x))).zyxw; |
| 87 | } | 101 | } | ||
| 88 | 102 | ||||
| 89 | // sampleBuffer reads the client buffer at src (buffer pixels). | 103 | // sampleBuffer reads the client buffer at src (buffer pixels). | ||
M internal/adapters/vulkan/shaders/compose_hdr.frag.spv +0 -0 binary file: no text diff
binary file: no text diff
A internal/adapters/vulkan/shaders/compose_hdr_single.frag.spv +0 -0 binary file: no text diff
binary file: no text diff
A internal/adapters/vulkan/shaders/compose_single.frag.spv +0 -0 binary file: no text diff
binary file: no text diff
M internal/adapters/vulkan/shm.go +2 -12
| @@ -164,11 +164,9 @@ type shmState struct { | |||
| 164 | 164 | windowSeq uint64 // root damage history uses window publications | |
| 165 | 165 | } | |
| 166 | 166 | ||
| 167 | - | // shmCopy is one of a surface's two GPU buffers and the set binding it. | |
| 167 | + | // shmCopy is one of a surface's two GPU buffers. | |
| 168 | 168 | type shmCopy struct { | |
| 169 | 169 | gpu gpuBuffer | |
| 170 | - | pool vk.DescriptorPool | |
| 171 | - | set vk.DescriptorSet | |
| 172 | 170 | holds shmState | |
| 173 | 171 | valid bool | |
| 174 | 172 | } | |
| @@ -228,11 +226,6 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, st | |||
| 228 | 226 | if err := r.newGPUBuffer(&c.gpu, size); err != nil { | |
| 229 | 227 | return nil, err | |
| 230 | 228 | } | |
| 231 | - | var err error | |
| 232 | - | if c.pool, c.set, err = r.newSet(r.compose.dummyView, c.gpu.buffer); err != nil { | |
| 233 | - | r.freeGPUBuffer(&c.gpu) | |
| 234 | - | return nil, err | |
| 235 | - | } | |
| 236 | 229 | s.bufs[i] = c | |
| 237 | 230 | } | |
| 238 | 231 | if err := r.waitFrame(c.gpu.last); err != nil { | |
| @@ -267,10 +260,7 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, st | |||
| 267 | 260 | } | |
| 268 | 261 | ||
| 269 | 262 | func (r *Renderer) freeShmCopy(c *shmCopy) { | |
| 270 | - | if c.pool != 0 { | |
| 271 | - | r.dd.DestroyDescriptorPool(r.device, c.pool, nil) | |
| 272 | - | c.pool = 0 | |
| 273 | - | } | |
| 263 | + | r.invalidateCompose(resourceKey{shm: c}) | |
| 274 | 264 | r.freeGPUBuffer(&c.gpu) | |
| 275 | 265 | } | |
| 276 | 266 | ||
| @@ -164,11 +164,9 @@ type shmState struct { | |||||
| 164 | windowSeq uint64 // root damage history uses window publications | 164 | windowSeq uint64 // root damage history uses window publications | ||
| 165 | } | 165 | } | ||
| 166 | 166 | ||||
| 167 | - | // shmCopy is one of a surface's two GPU buffers and the set binding it. | 167 | + | // shmCopy is one of a surface's two GPU buffers. |
| 168 | type shmCopy struct { | 168 | type shmCopy struct { | ||
| 169 | gpu gpuBuffer | 169 | gpu gpuBuffer | ||
| 170 | - | pool vk.DescriptorPool | |||
| 171 | - | set vk.DescriptorSet | |||
| 172 | holds shmState | 170 | holds shmState | ||
| 173 | valid bool | 171 | valid bool | ||
| 174 | } | 172 | } | ||
| @@ -228,11 +226,6 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, st | |||||
| 228 | if err := r.newGPUBuffer(&c.gpu, size); err != nil { | 226 | if err := r.newGPUBuffer(&c.gpu, size); err != nil { | ||
| 229 | return nil, err | 227 | return nil, err | ||
| 230 | } | 228 | } | ||
| 231 | - | var err error | |||
| 232 | - | if c.pool, c.set, err = r.newSet(r.compose.dummyView, c.gpu.buffer); err != nil { | |||
| 233 | - | r.freeGPUBuffer(&c.gpu) | |||
| 234 | - | return nil, err | |||
| 235 | - | } | |||
| 236 | s.bufs[i] = c | 229 | s.bufs[i] = c | ||
| 237 | } | 230 | } | ||
| 238 | if err := r.waitFrame(c.gpu.last); err != nil { | 231 | if err := r.waitFrame(c.gpu.last); err != nil { | ||
| @@ -267,10 +260,7 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, st | |||||
| 267 | } | 260 | } | ||
| 268 | 261 | ||||
| 269 | func (r *Renderer) freeShmCopy(c *shmCopy) { | 262 | func (r *Renderer) freeShmCopy(c *shmCopy) { | ||
| 270 | - | if c.pool != 0 { | 263 | + | r.invalidateCompose(resourceKey{shm: c}) |
| 271 | - | r.dd.DestroyDescriptorPool(r.device, c.pool, nil) | |||
| 272 | - | c.pool = 0 | |||
| 273 | - | } | |||
| 274 | r.freeGPUBuffer(&c.gpu) | 264 | r.freeGPUBuffer(&c.gpu) | ||
| 275 | } | 265 | } | ||
| 276 | 266 | ||||
M internal/adapters/vulkan/trim.go +7 -1
| @@ -60,7 +60,13 @@ func (r *Renderer) Trim(now time.Time) error { | |||
| 60 | 60 | } | |
| 61 | 61 | } | |
| 62 | 62 | } | |
| 63 | - | return r.poll() | |
| 63 | + | if err := r.poll(); err != nil { | |
| 64 | + | return err | |
| 65 | + | } | |
| 66 | + | if i >= 0 { | |
| 67 | + | r.trimCompose(r.marks[0].frame) | |
| 68 | + | } | |
| 69 | + | return nil | |
| 64 | 70 | } | |
| 65 | 71 | ||
| 66 | 72 | // poll records the frames the GPU finished, without waiting, and frees | |
| @@ -60,7 +60,13 @@ func (r *Renderer) Trim(now time.Time) error { | |||||
| 60 | } | 60 | } | ||
| 61 | } | 61 | } | ||
| 62 | } | 62 | } | ||
| 63 | - | return r.poll() | 63 | + | if err := r.poll(); err != nil { |
| 64 | + | return err | |||
| 65 | + | } | |||
| 66 | + | if i >= 0 { | |||
| 67 | + | r.trimCompose(r.marks[0].frame) | |||
| 68 | + | } | |||
| 69 | + | return nil | |||
| 64 | } | 70 | } | ||
| 65 | 71 | ||||
| 66 | // poll records the frames the GPU finished, without waiting, and frees | 72 | // poll records the frames the GPU finished, without waiting, and frees | ||
M internal/adapters/vulkan/yuv.go +0 -3
| @@ -152,9 +152,6 @@ func (r *Renderer) importYUV(b *ports.DMABuf, format vk.Format) (*imported, erro | |||
| 152 | 152 | if im.chromaView, err = view(aspectPlane1, uvFormat); err != nil { | |
| 153 | 153 | return nil, err | |
| 154 | 154 | } | |
| 155 | - | if im.pool, im.set, err = r.newSet(im.view, r.compose.dummyBuffer.buffer, im.chromaView); err != nil { | |
| 156 | - | return nil, err | |
| 157 | - | } | |
| 158 | 155 | ok = true | |
| 159 | 156 | r.imports[b.ID] = im | |
| 160 | 157 | return im, nil | |
| @@ -152,9 +152,6 @@ func (r *Renderer) importYUV(b *ports.DMABuf, format vk.Format) (*imported, erro | |||||
| 152 | if im.chromaView, err = view(aspectPlane1, uvFormat); err != nil { | 152 | if im.chromaView, err = view(aspectPlane1, uvFormat); err != nil { | ||
| 153 | return nil, err | 153 | return nil, err | ||
| 154 | } | 154 | } | ||
| 155 | - | if im.pool, im.set, err = r.newSet(im.view, r.compose.dummyBuffer.buffer, im.chromaView); err != nil { | |||
| 156 | - | return nil, err | |||
| 157 | - | } | |||
| 158 | ok = true | 155 | ok = true | ||
| 159 | r.imports[b.ID] = im | 156 | r.imports[b.ID] = im | ||
| 160 | return im, nil | 157 | return im, nil | ||