detached 8624a64

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
23 files · +730 -134
M docs/performance.md +1 -0
@@ -3,6 +3,7 @@
33 NeferWL uses about 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change.
44
55 - 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.
67 - 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).
78 - 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).
89 - 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) {
673673 write := vk.WriteDescriptorSet{SType: vk.StructureTypeWriteDescriptorSet, DstSet: p.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img}
674674 d.UpdateDescriptorSets(r.device, 1, &write, 0, nil)
675675 // 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)
677677 }
678678
679679 // 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 (
1212 vk "github.com/bnema/purego-vulkan/vulkan"
1313 )
1414
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.
2119
2220 //go:generate glslc -O --target-env=vulkan1.3 shaders/compose.vert -o shaders/compose.vert.spv
2321 //go:generate glslc -O --target-env=vulkan1.3 shaders/compose.frag -o shaders/compose.frag.spv
2422 //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
2525 //go:generate glslc -O --target-env=vulkan1.3 shaders/hdr.vert -o shaders/hdr.vert.spv
2626 //go:generate glslc -O --target-env=vulkan1.3 shaders/hdr.frag -o shaders/hdr.frag.spv
2727
@@ -32,6 +32,10 @@ var (
3232 composeFrag []byte
3333 //go:embed shaders/compose_hdr.frag.spv
3434 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
3539 )
3640
3741 // Draw modes and flags (shaders: modeSolid, modeImage, flag*).
@@ -51,8 +55,8 @@ const (
5155 flagSmooth = 64
5256 )
5357
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 {
5660 rect [4]int32
5761 mapv [4]float32
5862 mapy [4]float32
@@ -64,8 +68,8 @@ type pushConstants struct {
6468
6569 // draw is one quad of a frame.
6670 type draw struct {
67- pc pushConstants
68- set vk.DescriptorSet
71+ pc drawParameters
72+ shm *shmCopy
6973 im *imported // sampled client dmabuf, for ownership barriers
7074 // acquire is the content's explicit-sync fence (nil: implicit).
7175 acquire *os.File
@@ -84,23 +88,25 @@ type composer struct {
8488 dummyMemory vk.DeviceMemory
8589 dummyView vk.ImageView
8690 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
8995 }
9096
9197 func (r *Renderer) createComposer() error {
9298 d, c := r.dd, &r.compose
9399 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},
97104 }
98105 sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: uint32(len(bindings)), Bindings: &bindings[0]}
99106 if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &c.setLayout)); err != nil {
100107 return err
101108 }
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}
104110 if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &c.layout)); err != nil {
105111 return err
106112 }
@@ -115,13 +121,24 @@ func (r *Renderer) createComposer() error {
115121 }
116122
117123 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 {
119129 return err
120130 }
121131 return nil
122132 }
123133
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 {
125142 d := r.dd
126143 vert, err := r.shaderModule(vertex)
127144 if err != nil {
@@ -136,7 +153,7 @@ func (r *Renderer) createGraphicsPipeline(vertex, fragment []byte, format vk.For
136153 main := []byte("main\x00")
137154 stages := []vk.PipelineShaderStageCreateInfo{
138155 {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},
140157 }
141158 vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo}
142159 assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList}
@@ -167,7 +184,7 @@ func (r *Renderer) createGraphicsPipeline(vertex, fragment []byte, format vk.For
167184 }
168185
169186 // 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.
171188 func (r *Renderer) createDummies() error {
172189 d, c := r.dd, &r.compose
173190 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 {
193210 if err := r.newGPUBuffer(&c.dummyBuffer, 16); err != nil {
194211 return err
195212 }
196- if c.dummyPool, c.dummySet, err = r.newSet(c.dummyView, c.dummyBuffer.buffer); err != nil {
197- return err
198- }
199213 // Move the placeholder image to the layout every draw declares.
200214 return r.oneShot(func(cmd vk.CommandBuffer) {
201215 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 {
203217 })
204218 }
205219
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-
237220 func (r *Renderer) destroyComposer() {
238221 d, c := r.dd, &r.compose
239- if c.dummyPool != 0 {
240- d.DestroyDescriptorPool(r.device, c.dummyPool, nil)
241- }
242222 r.freeGPUBuffer(&c.dummyBuffer)
243223 if c.dummyView != 0 {
244224 d.DestroyImageView(r.device, c.dummyView, nil)
@@ -295,7 +275,7 @@ func (r *Renderer) createView(t *target) (err error) {
295275
296276 // fillDraw is a solid rect of c.
297277 func (r *Renderer) fillDraw(rect image.Rectangle, c [3]uint8) draw {
298- dr := draw{set: r.compose.dummySet}
278+ dr := draw{}
299279 dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
300280 dr.pc.color = [4]float32{float32(c[0]) / 255, float32(c[1]) / 255, float32(c[2]) / 255, 1}
301281 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
403383 return dr
404384 }
405385
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
407387 // 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) {
409389 d, c := r.dd, &r.compose
410390 clear := vk.ClearValue{math.Float32bits(float32(bg[0]) / 255), math.Float32bits(float32(bg[1]) / 255), math.Float32bits(float32(bg[2]) / 255), math.Float32bits(1)}
411391 if r.hdrNits > 0 {
@@ -425,15 +405,10 @@ func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]ui
425405 pipeline = c.hdrPipeline
426406 }
427407 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)
437412 }
438413 d.CmdEndRendering(cmd)
439414 }
@@ -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 as15+// 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 a17+// then draws all its quads as instances. Client pixels are premultiplied,
18-// GPU buffer (shm.go). Push constants carry each draw; the descriptor set18+// 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.spv20 //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.spv21 //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.spv22 //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.spv25 //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.spv26 //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 []byte32 composeFrag []byte
33 //go:embed shaders/compose_hdr.frag.spv33 //go:embed shaders/compose_hdr.frag.spv
34 composeHDRFrag []byte34 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 = 6455 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]int3260 rect [4]int32
57 mapv [4]float3261 mapv [4]float32
58 mapy [4]float3262 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 pushConstants71+ pc drawParameters
68- set vk.DescriptorSet72+ shm *shmCopy
69 im *imported // sampled client dmabuf, for ownership barriers73 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.File75 acquire *os.File
@@ -84,23 +88,25 @@ type composer struct {
84 dummyMemory vk.DeviceMemory88 dummyMemory vk.DeviceMemory
85 dummyView vk.ImageView89 dummyView vk.ImageView
86 dummyBuffer gpuBuffer90 dummyBuffer gpuBuffer
87- dummyPool vk.DescriptorPool91+ // resources is the number of resource slots per batch (one without
88- dummySet vk.DescriptorSet92+ // 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.compose98 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 err107 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 err111 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 err129 return err
120 }130 }
121 return nil131 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.dd142 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) and186 // 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.compose189 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 err211 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.compose221 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 dr383 return dr
404 }384 }
405 385
406-// recordDraws draws ds in one rendering pass that clears view to bg, or386+// 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.compose389 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.hdrPipeline405 pipeline = c.hdrPipeline
426 }406 }
427 d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, pipeline)407 d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, pipeline)
428- var bound vk.DescriptorSet408+ 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
186186 // DMABuf reports the formats this renderer imports.
187187 func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf }
188188
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.
191190 type imported struct {
192191 image vk.Image
193192 memory vk.DeviceMemory
@@ -199,8 +198,6 @@ type imported struct {
199198 // disjoint is available for this format/modifier; required only when
200199 // the two planes refer to different underlying DMA-BUF objects.
201200 disjoint bool
202- pool vk.DescriptorPool
203- set vk.DescriptorSet
204201 fd int // our duplicate of plane 0, for implicit sync
205202 // last is the frame that drew it, for eviction.
206203 last uint64
@@ -279,9 +276,6 @@ func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) {
279276 if im.view, err = r.imageView(im.image, format); err != nil {
280277 return nil, err
281278 }
282- if im.pool, im.set, err = r.newSet(im.view, r.compose.dummyBuffer.buffer); err != nil {
283- return nil, err
284- }
285279 ok = true
286280 r.imports[b.ID] = im
287281 return im, nil
@@ -305,9 +299,7 @@ func dupFile(f *os.File) (int, error) {
305299 }
306300
307301 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})
311303 if im.view != 0 {
312304 r.dd.DestroyImageView(r.device, im.view, nil)
313305 }
@@ -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 set189+// 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.Image191 image vk.Image
193 memory vk.DeviceMemory192 memory vk.DeviceMemory
@@ -199,8 +198,6 @@ type imported struct {
199 // disjoint is available for this format/modifier; required only when198 // 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 bool200 disjoint bool
202- pool vk.DescriptorPool
203- set vk.DescriptorSet
204 fd int // our duplicate of plane 0, for implicit sync201 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 uint64203 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, err277 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 = true279 ok = true
286 r.imports[b.ID] = im280 r.imports[b.ID] = im
287 return im, nil281 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 {
2727 cmd vk.CommandBuffer
2828 fence vk.Fence
2929 // 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
3334 }
3435
3536 // callScratch holds the structs Render and its record helpers pass to Vulkan
@@ -102,6 +103,9 @@ func (r *Renderer) destroySlots() {
102103 d := r.dd
103104 for i := range r.slots {
104105 s := &r.slots[i]
106+ for j := range s.batches {
107+ r.freeComposeBatch(s.batches[j])
108+ }
105109 if s.fence != 0 {
106110 d.DestroyFence(r.device, s.fence, nil)
107111 }
@@ -224,6 +228,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
224228 r.log.Debug().Int("redrawn", redrawn).Int("draws", len(ds)).Bool("partial", partial).Msg("frame damage")
225229 }
226230 r.orient(ds, s.Transform)
231+ batches, err := r.prepareCompose(slot, ds)
232+ if err != nil {
233+ return nil, err
234+ }
227235 d := r.dd
228236 sc := &r.call
229237 cmd := slot.cmd
@@ -267,7 +275,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
267275 }
268276 r.frameDMAs = dmas
269277 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)
271279 r.ownership(cmd, dmas, false)
272280 if hdr {
273281 r.recordHDR(cmd, r.targets[r.current])
@@ -27,9 +27,10 @@ type frameSlot struct {
27 cmd vk.CommandBuffer27 cmd vk.CommandBuffer
28 fence vk.Fence28 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.Semaphore30+ done vk.Semaphore
31- frame uint64 // the frame recorded last, 0 before any31+ frame uint64 // the frame recorded last, 0 before any
32- busy bool // submitted, fence not yet seen signalled32+ 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 Vulkan36 // callScratch holds the structs Render and its record helpers pass to Vulkan
@@ -102,6 +103,9 @@ func (r *Renderer) destroySlots() {
102 d := r.dd103 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.dd235 d := r.dd
228 sc := &r.call236 sc := &r.call
229 cmd := slot.cmd237 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 = dmas276 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) {
5959 }
6060 }
6161 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)
6367 if r.hdrError != nil {
6468 r.dropHDROwn()
6569 return
@@ -124,7 +128,7 @@ func (r *Renderer) createHDR() error {
124128 if err := checked("vkCreatePipelineLayout(HDR)", d.CreatePipelineLayout(r.device, &pli, nil, &h.layout)); err != nil {
125129 return err
126130 }
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 {
128132 return err
129133 }
130134 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 return69 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 err129 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 err132 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) {
214214 priority := float32(1)
215215 qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority}
216216 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}
218234 // dmabuf import needs every extension; without them clients use wl_shm.
219235 var extNames [][]byte
220236 var extPtrs []*byte
@@ -264,6 +280,9 @@ func New(width, height int) (r *Renderer, err error) {
264280 }
265281 result := r.id.CreateDevice(physical, &di, nil, &r.device)
266282 runtime.KeepAlive(global)
283+ runtime.KeepAlive(coreFeatures)
284+ runtime.KeepAlive(indexing)
285+ runtime.KeepAlive(dynamicRendering)
267286 runtime.KeepAlive(qi)
268287 runtime.KeepAlive(extNames)
269288 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 [][]byte235 var extNames [][]byte
220 var extPtrs []*byte236 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
558558 }
559559 dr.pc.buf[3] = uint32(coeff) | uint32(ran)<<8 | uint32(content.Color.Chroma)<<16
560560 }
561- dr.set, dr.im = im.set, im
561+ dr.im = im
562562 dr.acquire = content.Acquire
563563 w.draws = append(w.draws, dr)
564564 return
@@ -591,7 +591,7 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon
591591 r.setContentColor(&dr, content)
592592 dr.pc.color[3] *= w.alpha
593593 dr.pc.mapy[2] = w.pulse
594- dr.set = c.set
594+ dr.shm = c
595595 dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
596596 w.draws = append(w.draws, dr)
597597 }
@@ -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)<<16559 dr.pc.buf[3] = uint32(coeff) | uint32(ran)<<8 | uint32(content.Color.Chroma)<<16
560 }560 }
561- dr.set, dr.im = im.set, im561+ dr.im = im
562 dr.acquire = content.Acquire562 dr.acquire = content.Acquire
563 w.draws = append(w.draws, dr)563 w.draws = append(w.draws, dr)
564 return564 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.alpha592 dr.pc.color[3] *= w.alpha
593 dr.pc.mapy[2] = w.pulse593 dr.pc.mapy[2] = w.pulse
594- dr.set = c.set594+ 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 @@
11 #version 450
2+#ifndef SINGLE_RESOURCE
3+#extension GL_EXT_nonuniform_qualifier : require
4+#endif
25 // Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm
36 // buffer (B8G8R8A8 words in a storage buffer). Client pixels are
47 // premultiplied, as Wayland buffers are: the pipeline blends ONE,
58 // ONE_MINUS_SRC_ALPHA. Scaled draws filter linearly; 1:1 draws read the
69 // texel under the fragment, pixel exact.
710
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;
1619 } d;
1720
21+#ifdef SINGLE_RESOURCE
1822 layout(set = 0, binding = 0) uniform sampler2D tex;
1923 layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; };
2024 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
2135
2236 layout(location = 0) out vec4 color;
2337
@@ -84,7 +98,7 @@ vec4 sampleYUV(vec2 src) {
8498 vec4 texel(ivec2 p) {
8599 p = clamp(p, ivec2(floor(d.crop.xy)), min(ivec2(d.buf.yz) - 1, ivec2(ceil(d.crop.zw)) - 1));
86100 // 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;
88102 }
89103
90104 // sampleBuffer reads the client buffer at src (buffer pixels).
@@ -1,23 +1,37 @@
1 #version 4501 #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_shm5 // Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm
3 // buffer (B8G8R8A8 words in a storage buffer). Client pixels are6 // 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 the8 // 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 @@
11 #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.
34
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;
1325
1426 void main() {
27+ Draw d = draws[gl_InstanceIndex];
1528 const ivec2 corners[6] = ivec2[](ivec2(0, 0), ivec2(1, 0), ivec2(0, 1), ivec2(0, 1), ivec2(1, 0), ivec2(1, 1));
1629 ivec2 c = corners[gl_VertexIndex];
1730 vec2 p = vec2(c.x == 0 ? d.rect.x : d.rect.z, c.y == 0 ? d.rect.y : d.rect.w);
1831 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;
1939 }
@@ -1,19 +1,39 @@
1 #version 4501 #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 pixels6+ ivec4 rect; // visible x0, y0, x1, y1 in target pixels
6- vec4 map; // source texel = map.xy + fragment centre.x * map.zw7+ 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 pulse8+ vec4 mapy; // + fragment centre.y * mapy.xy; mapy.z is the focus pulse
8- vec4 color; // solid fills: premultiplied RGBA9+ vec4 color; // solid premultiplied RGBA; content opacity in alpha
9- vec4 crop; // source crop bounds in buffer pixels10+ vec4 crop; // source crop bounds in buffer pixels
10- uvec4 buf; // buffer draws: first word, row width, height, unused11+ uvec4 buf; // pixel-buffer first word, width, height; YUV metadata in w
11- uvec4 misc; // mode, flags, target width, target height12+ 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 @@
11 #version 450
2+#ifndef SINGLE_RESOURCE
3+#extension GL_EXT_nonuniform_qualifier : require
4+#endif
25 // Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm
36 // buffer (B8G8R8A8 words in a storage buffer). Client pixels are
47 // premultiplied, as Wayland buffers are: the pipeline blends ONE,
58 // ONE_MINUS_SRC_ALPHA. Scaled draws filter linearly; 1:1 draws read the
69 // texel under the fragment, pixel exact.
710
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;
1619 } d;
1720
21+#ifdef SINGLE_RESOURCE
1822 layout(set = 0, binding = 0) uniform sampler2D tex;
1923 layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; };
2024 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
2135
2236 layout(location = 0) out vec4 color;
2337
@@ -83,7 +97,7 @@ vec4 sampleYUV(vec2 src) {
8397 vec4 texel(ivec2 p) {
8498 p = clamp(p, ivec2(floor(d.crop.xy)), min(ivec2(d.buf.yz) - 1, ivec2(ceil(d.crop.zw)) - 1));
8599 // 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;
87101 }
88102
89103 // sampleBuffer reads the client buffer at src (buffer pixels).
@@ -1,23 +1,37 @@
1 #version 4501 #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_shm5 // Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm
3 // buffer (B8G8R8A8 words in a storage buffer). Client pixels are6 // 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 the8 // 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 {
164164 windowSeq uint64 // root damage history uses window publications
165165 }
166166
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.
168168 type shmCopy struct {
169169 gpu gpuBuffer
170- pool vk.DescriptorPool
171- set vk.DescriptorSet
172170 holds shmState
173171 valid bool
174172 }
@@ -228,11 +226,6 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, st
228226 if err := r.newGPUBuffer(&c.gpu, size); err != nil {
229227 return nil, err
230228 }
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- }
236229 s.bufs[i] = c
237230 }
238231 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
267260 }
268261
269262 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})
274264 r.freeGPUBuffer(&c.gpu)
275265 }
276266
@@ -164,11 +164,9 @@ type shmState struct {
164 windowSeq uint64 // root damage history uses window publications164 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 gpuBuffer169 gpu gpuBuffer
170- pool vk.DescriptorPool
171- set vk.DescriptorSet
172 holds shmState170 holds shmState
173 valid bool171 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, err227 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] = c229 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 {
6060 }
6161 }
6262 }
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
6470 }
6571
6672 // 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 frees72 // 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
152152 if im.chromaView, err = view(aspectPlane1, uvFormat); err != nil {
153153 return nil, err
154154 }
155- if im.pool, im.set, err = r.newSet(im.view, r.compose.dummyBuffer.buffer, im.chromaView); err != nil {
156- return nil, err
157- }
158155 ok = true
159156 r.imports[b.ID] = im
160157 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, err153 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 = true155 ok = true
159 r.imports[b.ID] = im156 r.imports[b.ID] = im
160 return im, nil157 return im, nil

Keyboard shortcuts

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

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

Branches 25

Tags 10

↑↓ moveenter openesc close