detached 717ee76

feat(vulkan): compose every frame on the GPU with one pipeline and return its fence as a sync file

One graphics pipeline draws fills, sampled dmabufs and wl_shm pixels as quads with push constants in a single rendering pass. Scaled content filters linearly, 1:1 content stays pixel exact. wl_shm surfaces are copied once per content into persistently mapped, double-buffered GPU buffers the shader reads directly; the staging buffer, CPU resample and fill strips are gone. Frames go through a ring of two command buffer slots and Render exports the frame semaphore as a sync file, so no CPU wait remains before the flip; objects a frame read are freed only after it completed. Dmabuf formats are advertised on sampled-image support.
commit
717ee76b8fbc355795fda2ff8c2503fd1836cc2c
author
brice · · 2026-09-26 11:33 UTC
parent
d129f9d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
19 files · +1247 -732
M internal/adapters/drm/commit_test.go +94 -0
@@ -314,3 +314,97 @@ func TestRunReportsSeenAfterCursorCommit(t *testing.T) {
314314 t.Fatal(err)
315315 }
316316 }
317+
318+// Frame fences are closed on every path: flipped, EBUSY (re-rendered
319+// after the event) and lost master (re-rendered after the modeset). The
320+// fd count is unchanged after many frames.
321+func TestRunClosesFrameFences(t *testing.T) {
322+ fds := func() int {
323+ ents, err := os.ReadDir("/proc/self/fd")
324+ if err != nil {
325+ t.Skip(err)
326+ }
327+ return len(ents)
328+ }
329+ const rounds = 30
330+ // Commit results: modeset test and modeset, then per round
331+ // EBUSY + flip, or lost master + modeset + flip, or a plain flip.
332+ errs := []error{nil, nil}
333+ for i := range rounds {
334+ switch i % 3 {
335+ case 0:
336+ errs = append(errs, unix.EBUSY, nil)
337+ case 1:
338+ errs = append(errs, unix.EACCES, nil, nil)
339+ default:
340+ errs = append(errs, nil)
341+ }
342+ }
343+ o, k, commits, commitMu := testOutputMu(t, errs...)
344+ o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
345+ flips := make(chan flipEvent, 1)
346+ o.flipped = flips
347+ r := portsmocks.NewMockRenderer(t)
348+ buf := func() ports.DMABuf {
349+ f, w, _ := os.Pipe()
350+ w.Close()
351+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
352+ }
353+ r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
354+ r.EXPECT().UseTarget(mock.Anything).Return()
355+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
356+ f, w, _ := os.Pipe()
357+ _, _ = w.Write([]byte{1})
358+ w.Close()
359+ return f, nil
360+ })
361+ r.EXPECT().Close().Return().Once()
362+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
363+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
364+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
365+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
366+ before := fds()
367+ active := make(chan bool, 1)
368+ scenes := make(chan ports.Scene, 1)
369+ presented := make(chan ports.OutputPresented, 4*rounds)
370+ ctx, cancel := context.WithCancel(context.Background())
371+ done := make(chan error, 1)
372+ go func() {
373+ done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, presented)
374+ }()
375+ count := func() int {
376+ commitMu.Lock()
377+ defer commitMu.Unlock()
378+ return len(*commits)
379+ }
380+ last := func() commitRec {
381+ commitMu.Lock()
382+ defer commitMu.Unlock()
383+ return (*commits)[len(*commits)-1]
384+ }
385+ waitFor(t, func() bool { return count() == 2 })
386+ for i := range rounds {
387+ n := count()
388+ scenes <- ports.Scene{Seq: uint64(i)}
389+ switch i % 3 {
390+ case 0:
391+ waitFor(t, func() bool { return count() == n+1 })
392+ flips <- flipEvent{crtc: tCrtc} // ends the EBUSY wait
393+ waitFor(t, func() bool { return count() == n+2 })
394+ case 1:
395+ waitFor(t, func() bool { return count() == n+1 })
396+ active <- true
397+ waitFor(t, func() bool { return count() == n+3 })
398+ default:
399+ waitFor(t, func() bool { return count() == n+1 })
400+ }
401+ flips <- eventOf(last())
402+ }
403+ cancel()
404+ if err := <-done; err != nil {
405+ t.Fatal(err)
406+ }
407+ if after := fds(); after > before {
408+ t.Fatalf("fds %d -> %d", before, after)
409+ }
410+}
@@ -314,3 +314,97 @@ func TestRunReportsSeenAfterCursorCommit(t *testing.T) {
314 t.Fatal(err)314 t.Fatal(err)
315 }315 }
316 }316 }
317+
318+// Frame fences are closed on every path: flipped, EBUSY (re-rendered
319+// after the event) and lost master (re-rendered after the modeset). The
320+// fd count is unchanged after many frames.
321+func TestRunClosesFrameFences(t *testing.T) {
322+ fds := func() int {
323+ ents, err := os.ReadDir("/proc/self/fd")
324+ if err != nil {
325+ t.Skip(err)
326+ }
327+ return len(ents)
328+ }
329+ const rounds = 30
330+ // Commit results: modeset test and modeset, then per round
331+ // EBUSY + flip, or lost master + modeset + flip, or a plain flip.
332+ errs := []error{nil, nil}
333+ for i := range rounds {
334+ switch i % 3 {
335+ case 0:
336+ errs = append(errs, unix.EBUSY, nil)
337+ case 1:
338+ errs = append(errs, unix.EACCES, nil, nil)
339+ default:
340+ errs = append(errs, nil)
341+ }
342+ }
343+ o, k, commits, commitMu := testOutputMu(t, errs...)
344+ o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
345+ flips := make(chan flipEvent, 1)
346+ o.flipped = flips
347+ r := portsmocks.NewMockRenderer(t)
348+ buf := func() ports.DMABuf {
349+ f, w, _ := os.Pipe()
350+ w.Close()
351+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
352+ }
353+ r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
354+ r.EXPECT().UseTarget(mock.Anything).Return()
355+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
356+ f, w, _ := os.Pipe()
357+ _, _ = w.Write([]byte{1})
358+ w.Close()
359+ return f, nil
360+ })
361+ r.EXPECT().Close().Return().Once()
362+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
363+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
364+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
365+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
366+ before := fds()
367+ active := make(chan bool, 1)
368+ scenes := make(chan ports.Scene, 1)
369+ presented := make(chan ports.OutputPresented, 4*rounds)
370+ ctx, cancel := context.WithCancel(context.Background())
371+ done := make(chan error, 1)
372+ go func() {
373+ done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, presented)
374+ }()
375+ count := func() int {
376+ commitMu.Lock()
377+ defer commitMu.Unlock()
378+ return len(*commits)
379+ }
380+ last := func() commitRec {
381+ commitMu.Lock()
382+ defer commitMu.Unlock()
383+ return (*commits)[len(*commits)-1]
384+ }
385+ waitFor(t, func() bool { return count() == 2 })
386+ for i := range rounds {
387+ n := count()
388+ scenes <- ports.Scene{Seq: uint64(i)}
389+ switch i % 3 {
390+ case 0:
391+ waitFor(t, func() bool { return count() == n+1 })
392+ flips <- flipEvent{crtc: tCrtc} // ends the EBUSY wait
393+ waitFor(t, func() bool { return count() == n+2 })
394+ case 1:
395+ waitFor(t, func() bool { return count() == n+1 })
396+ active <- true
397+ waitFor(t, func() bool { return count() == n+3 })
398+ default:
399+ waitFor(t, func() bool { return count() == n+1 })
400+ }
401+ flips <- eventOf(last())
402+ }
403+ cancel()
404+ if err := <-done; err != nil {
405+ t.Fatal(err)
406+ }
407+ if after := fds(); after > before {
408+ t.Fatalf("fds %d -> %d", before, after)
409+ }
410+}
D internal/adapters/vulkan/blend.go +0 -190
@@ -1,190 +0,0 @@
1-package vulkan
2-
3-import (
4- _ "embed"
5- "encoding/binary"
6- "unsafe"
7-
8- vk "github.com/bnema/purego-vulkan/vulkan"
9-)
10-
11-// Translucent client pixels are blended over what is below them; opaque
12-// ones keep the plain copy. The blend pipeline draws one quad per upload
13-// and its fragment shader reads the staged pixels straight from the
14-// staging buffer: no texture, no sampler. Wayland buffers are
15-// premultiplied, hence ONE, ONE_MINUS_SRC_ALPHA.
16-//
17-// Limits: dmabuf buffers are still blitted, alpha ignored (blending them
18-// needs a sampled-image variant). The opaque region is not honoured, so
19-// an ARGB buffer blends even where it is opaque.
20-
21-//go:generate glslc -O --target-env=vulkan1.3 shaders/blend.vert -o shaders/blend.vert.spv
22-//go:generate glslc -O --target-env=vulkan1.3 shaders/blend.frag -o shaders/blend.frag.spv
23-
24-var (
25- //go:embed shaders/blend.vert.spv
26- blendVert []byte
27- //go:embed shaders/blend.frag.spv
28- blendFrag []byte
29-)
30-
31-// drawSize is one entry of the draw table (shaders: struct Draw).
32-const drawSize = 32
33-
34-// blendPipeline is the graphics state for blended uploads.
35-type blendPipeline struct {
36- setLayout vk.DescriptorSetLayout
37- layout vk.PipelineLayout
38- pipeline vk.Pipeline
39- pool vk.DescriptorPool
40- set vk.DescriptorSet
41-}
42-
43-func (r *Renderer) createBlend() error {
44- d, b := r.dd, &r.blend
45- bindings := []vk.DescriptorSetLayoutBinding{
46- {Binding: 0, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
47- {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit},
48- }
49- sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 2, Bindings: &bindings[0]}
50- if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &b.setLayout)); err != nil {
51- return err
52- }
53- pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &b.setLayout}
54- if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &b.layout)); err != nil {
55- return err
56- }
57- size := vk.DescriptorPoolSize{Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 2}
58- dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &size}
59- if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &b.pool)); err != nil {
60- return err
61- }
62- dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: b.pool, DescriptorSetCount: 1, SetLayouts: &b.setLayout}
63- if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &b.set)); err != nil {
64- return err
65- }
66- vert, err := r.shaderModule(blendVert)
67- if err != nil {
68- return err
69- }
70- defer d.DestroyShaderModule(r.device, vert, nil)
71- frag, err := r.shaderModule(blendFrag)
72- if err != nil {
73- return err
74- }
75- defer d.DestroyShaderModule(r.device, frag, nil)
76- main := []byte("main\x00")
77- stages := []vk.PipelineShaderStageCreateInfo{
78- {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]},
79- {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]},
80- }
81- vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo}
82- assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList}
83- // The target size is fixed for the renderer's life: static viewport.
84- viewport := vk.Viewport{Width: float32(r.width), Height: float32(r.height), MaxDepth: 1}
85- scissor := vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}
86- viewportState := vk.PipelineViewportStateCreateInfo{SType: vk.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor}
87- raster := vk.PipelineRasterizationStateCreateInfo{SType: vk.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vk.PolygonModeFill, CullMode: vk.CullModeNone, FrontFace: vk.FrontFaceCounterClockwise, LineWidth: 1}
88- multisample := vk.PipelineMultisampleStateCreateInfo{SType: vk.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vk.SampleCount1Bit}
89- attachment := vk.PipelineColorBlendAttachmentState{
90- BlendEnable: 1,
91- SrcColorBlendFactor: vk.BlendFactorOne, DstColorBlendFactor: vk.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vk.BlendOpAdd,
92- SrcAlphaBlendFactor: vk.BlendFactorOne, DstAlphaBlendFactor: vk.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vk.BlendOpAdd,
93- ColorWriteMask: vk.ColorComponentRBit | vk.ColorComponentGBit | vk.ColorComponentBBit | vk.ColorComponentABit,
94- }
95- blendState := vk.PipelineColorBlendStateCreateInfo{SType: vk.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &attachment}
96- format := vk.Format(vk.FormatB8g8r8a8Unorm)
97- rendering := vk.PipelineRenderingCreateInfo{SType: vk.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format}
98- gpi := vk.GraphicsPipelineCreateInfo{
99- SType: vk.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering),
100- StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly,
101- ViewportState: &viewportState, RasterizationState: &raster, MultisampleState: &multisample,
102- ColorBlendState: &blendState, Layout: b.layout, BasePipelineIndex: -1,
103- }
104- return checked("vkCreateGraphicsPipelines", d.CreateGraphicsPipelines(r.device, 0, 1, &gpi, nil, &b.pipeline))
105-}
106-
107-func (r *Renderer) shaderModule(code []byte) (vk.ShaderModule, error) {
108- // SPIR-V is read as words: copy into a uint32-aligned slice.
109- words := make([]uint32, len(code)/4)
110- for i := range words {
111- words[i] = binary.LittleEndian.Uint32(code[i*4:])
112- }
113- info := vk.ShaderModuleCreateInfo{SType: vk.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(words) * 4), Code: &words[0]}
114- var m vk.ShaderModule
115- return m, checked("vkCreateShaderModule", r.dd.CreateShaderModule(r.device, &info, nil, &m))
116-}
117-
118-func (r *Renderer) destroyBlend() {
119- d, b := r.dd, &r.blend
120- if b.pipeline != 0 {
121- d.DestroyPipeline(r.device, b.pipeline, nil)
122- }
123- if b.pool != 0 {
124- d.DestroyDescriptorPool(r.device, b.pool, nil)
125- }
126- if b.layout != 0 {
127- d.DestroyPipelineLayout(r.device, b.layout, nil)
128- }
129- if b.setLayout != 0 {
130- d.DestroyDescriptorSetLayout(r.device, b.setLayout, nil)
131- }
132- *b = blendPipeline{}
133-}
134-
135-// bindStaging points the set at the staging buffer: pixels blended
136-// pixel bytes from 0, and the draw table of n entries at tableOffset.
137-func (r *Renderer) bindStaging(pixels, tableOffset, n int) {
138- infos := []vk.DescriptorBufferInfo{
139- {Buffer: r.staging, Range: vk.DeviceSize(max(pixels, 4))},
140- {Buffer: r.staging, Offset: vk.DeviceSize(tableOffset), Range: vk.DeviceSize(n * drawSize)},
141- }
142- writes := []vk.WriteDescriptorSet{
143- {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[0]},
144- {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[1]},
145- }
146- r.dd.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil)
147-}
148-
149-// putDraw writes draw entry i of table.
150-func (r *Renderer) putDraw(table []byte, i int, u *upload) {
151- e := table[i*drawSize : (i+1)*drawSize]
152- for j, v := range []uint32{
153- uint32(u.rect.Min.X), uint32(u.rect.Min.Y), uint32(u.rect.Max.X), uint32(u.rect.Max.Y),
154- uint32(u.offset / 4), uint32(u.rect.Dx()), uint32(r.width), uint32(r.height),
155- } {
156- binary.LittleEndian.PutUint32(e[j*4:], v)
157- }
158-}
159-
160-// drawBlended draws uploads[first:last], all blended, into view: one
161-// instanced draw. The image is TRANSFER_DST before and after.
162-func (r *Renderer) drawBlended(image vk.Image, view vk.ImageView, first, last int) {
163- d := r.dd
164- rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
165- b := vk.ImageMemoryBarrier{
166- SType: vk.StructureTypeImageMemoryBarrier, Image: image, SubresourceRange: rangeInfo,
167- SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessColorAttachmentReadBit | vk.AccessColorAttachmentWriteBit,
168- OldLayout: vk.ImageLayoutTransferDstOptimal, NewLayout: vk.ImageLayoutColorAttachmentOptimal,
169- }
170- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)
171- attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpLoad, StoreOp: vk.AttachmentStoreOpStore}
172- info := vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment}
173- d.CmdBeginRendering(r.command, &info)
174- d.CmdBindPipeline(r.command, vk.PipelineBindPointGraphics, r.blend.pipeline)
175- d.CmdBindDescriptorSets(r.command, vk.PipelineBindPointGraphics, r.blend.layout, 0, 1, &r.blend.set, 0, nil)
176- d.CmdDraw(r.command, 6, uint32(last-first), 0, uint32(first))
177- d.CmdEndRendering(r.command)
178- b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, vk.AccessTransferWriteBit|vk.AccessTransferReadBit
179- b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutTransferDstOptimal
180- d.CmdPipelineBarrier(r.command, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
181-}
182-
183-// createView makes the color attachment view of a target image.
184-func (r *Renderer) createView(t *target) error {
185- info := vk.ImageViewCreateInfo{
186- SType: vk.StructureTypeImageViewCreateInfo, Image: t.image, ViewType: vk.ImageViewType2d, Format: vk.FormatB8g8r8a8Unorm,
187- SubresourceRange: vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1},
188- }
189- return checked("vkCreateImageView", r.dd.CreateImageView(r.device, &info, nil, &t.view))
190-}
@@ -1,190 +0,0 @@
1-package vulkan
2-
3-import (
4- _ "embed"
5- "encoding/binary"
6- "unsafe"
7-
8- vk "github.com/bnema/purego-vulkan/vulkan"
9-)
10-
11-// Translucent client pixels are blended over what is below them; opaque
12-// ones keep the plain copy. The blend pipeline draws one quad per upload
13-// and its fragment shader reads the staged pixels straight from the
14-// staging buffer: no texture, no sampler. Wayland buffers are
15-// premultiplied, hence ONE, ONE_MINUS_SRC_ALPHA.
16-//
17-// Limits: dmabuf buffers are still blitted, alpha ignored (blending them
18-// needs a sampled-image variant). The opaque region is not honoured, so
19-// an ARGB buffer blends even where it is opaque.
20-
21-//go:generate glslc -O --target-env=vulkan1.3 shaders/blend.vert -o shaders/blend.vert.spv
22-//go:generate glslc -O --target-env=vulkan1.3 shaders/blend.frag -o shaders/blend.frag.spv
23-
24-var (
25- //go:embed shaders/blend.vert.spv
26- blendVert []byte
27- //go:embed shaders/blend.frag.spv
28- blendFrag []byte
29-)
30-
31-// drawSize is one entry of the draw table (shaders: struct Draw).
32-const drawSize = 32
33-
34-// blendPipeline is the graphics state for blended uploads.
35-type blendPipeline struct {
36- setLayout vk.DescriptorSetLayout
37- layout vk.PipelineLayout
38- pipeline vk.Pipeline
39- pool vk.DescriptorPool
40- set vk.DescriptorSet
41-}
42-
43-func (r *Renderer) createBlend() error {
44- d, b := r.dd, &r.blend
45- bindings := []vk.DescriptorSetLayoutBinding{
46- {Binding: 0, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
47- {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit},
48- }
49- sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 2, Bindings: &bindings[0]}
50- if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &b.setLayout)); err != nil {
51- return err
52- }
53- pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &b.setLayout}
54- if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &b.layout)); err != nil {
55- return err
56- }
57- size := vk.DescriptorPoolSize{Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 2}
58- dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &size}
59- if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &b.pool)); err != nil {
60- return err
61- }
62- dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: b.pool, DescriptorSetCount: 1, SetLayouts: &b.setLayout}
63- if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &b.set)); err != nil {
64- return err
65- }
66- vert, err := r.shaderModule(blendVert)
67- if err != nil {
68- return err
69- }
70- defer d.DestroyShaderModule(r.device, vert, nil)
71- frag, err := r.shaderModule(blendFrag)
72- if err != nil {
73- return err
74- }
75- defer d.DestroyShaderModule(r.device, frag, nil)
76- main := []byte("main\x00")
77- stages := []vk.PipelineShaderStageCreateInfo{
78- {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]},
79- {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]},
80- }
81- vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo}
82- assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList}
83- // The target size is fixed for the renderer's life: static viewport.
84- viewport := vk.Viewport{Width: float32(r.width), Height: float32(r.height), MaxDepth: 1}
85- scissor := vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}
86- viewportState := vk.PipelineViewportStateCreateInfo{SType: vk.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor}
87- raster := vk.PipelineRasterizationStateCreateInfo{SType: vk.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vk.PolygonModeFill, CullMode: vk.CullModeNone, FrontFace: vk.FrontFaceCounterClockwise, LineWidth: 1}
88- multisample := vk.PipelineMultisampleStateCreateInfo{SType: vk.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vk.SampleCount1Bit}
89- attachment := vk.PipelineColorBlendAttachmentState{
90- BlendEnable: 1,
91- SrcColorBlendFactor: vk.BlendFactorOne, DstColorBlendFactor: vk.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vk.BlendOpAdd,
92- SrcAlphaBlendFactor: vk.BlendFactorOne, DstAlphaBlendFactor: vk.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vk.BlendOpAdd,
93- ColorWriteMask: vk.ColorComponentRBit | vk.ColorComponentGBit | vk.ColorComponentBBit | vk.ColorComponentABit,
94- }
95- blendState := vk.PipelineColorBlendStateCreateInfo{SType: vk.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &attachment}
96- format := vk.Format(vk.FormatB8g8r8a8Unorm)
97- rendering := vk.PipelineRenderingCreateInfo{SType: vk.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format}
98- gpi := vk.GraphicsPipelineCreateInfo{
99- SType: vk.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering),
100- StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly,
101- ViewportState: &viewportState, RasterizationState: &raster, MultisampleState: &multisample,
102- ColorBlendState: &blendState, Layout: b.layout, BasePipelineIndex: -1,
103- }
104- return checked("vkCreateGraphicsPipelines", d.CreateGraphicsPipelines(r.device, 0, 1, &gpi, nil, &b.pipeline))
105-}
106-
107-func (r *Renderer) shaderModule(code []byte) (vk.ShaderModule, error) {
108- // SPIR-V is read as words: copy into a uint32-aligned slice.
109- words := make([]uint32, len(code)/4)
110- for i := range words {
111- words[i] = binary.LittleEndian.Uint32(code[i*4:])
112- }
113- info := vk.ShaderModuleCreateInfo{SType: vk.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(words) * 4), Code: &words[0]}
114- var m vk.ShaderModule
115- return m, checked("vkCreateShaderModule", r.dd.CreateShaderModule(r.device, &info, nil, &m))
116-}
117-
118-func (r *Renderer) destroyBlend() {
119- d, b := r.dd, &r.blend
120- if b.pipeline != 0 {
121- d.DestroyPipeline(r.device, b.pipeline, nil)
122- }
123- if b.pool != 0 {
124- d.DestroyDescriptorPool(r.device, b.pool, nil)
125- }
126- if b.layout != 0 {
127- d.DestroyPipelineLayout(r.device, b.layout, nil)
128- }
129- if b.setLayout != 0 {
130- d.DestroyDescriptorSetLayout(r.device, b.setLayout, nil)
131- }
132- *b = blendPipeline{}
133-}
134-
135-// bindStaging points the set at the staging buffer: pixels blended
136-// pixel bytes from 0, and the draw table of n entries at tableOffset.
137-func (r *Renderer) bindStaging(pixels, tableOffset, n int) {
138- infos := []vk.DescriptorBufferInfo{
139- {Buffer: r.staging, Range: vk.DeviceSize(max(pixels, 4))},
140- {Buffer: r.staging, Offset: vk.DeviceSize(tableOffset), Range: vk.DeviceSize(n * drawSize)},
141- }
142- writes := []vk.WriteDescriptorSet{
143- {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[0]},
144- {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[1]},
145- }
146- r.dd.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil)
147-}
148-
149-// putDraw writes draw entry i of table.
150-func (r *Renderer) putDraw(table []byte, i int, u *upload) {
151- e := table[i*drawSize : (i+1)*drawSize]
152- for j, v := range []uint32{
153- uint32(u.rect.Min.X), uint32(u.rect.Min.Y), uint32(u.rect.Max.X), uint32(u.rect.Max.Y),
154- uint32(u.offset / 4), uint32(u.rect.Dx()), uint32(r.width), uint32(r.height),
155- } {
156- binary.LittleEndian.PutUint32(e[j*4:], v)
157- }
158-}
159-
160-// drawBlended draws uploads[first:last], all blended, into view: one
161-// instanced draw. The image is TRANSFER_DST before and after.
162-func (r *Renderer) drawBlended(image vk.Image, view vk.ImageView, first, last int) {
163- d := r.dd
164- rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
165- b := vk.ImageMemoryBarrier{
166- SType: vk.StructureTypeImageMemoryBarrier, Image: image, SubresourceRange: rangeInfo,
167- SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessColorAttachmentReadBit | vk.AccessColorAttachmentWriteBit,
168- OldLayout: vk.ImageLayoutTransferDstOptimal, NewLayout: vk.ImageLayoutColorAttachmentOptimal,
169- }
170- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)
171- attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpLoad, StoreOp: vk.AttachmentStoreOpStore}
172- info := vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment}
173- d.CmdBeginRendering(r.command, &info)
174- d.CmdBindPipeline(r.command, vk.PipelineBindPointGraphics, r.blend.pipeline)
175- d.CmdBindDescriptorSets(r.command, vk.PipelineBindPointGraphics, r.blend.layout, 0, 1, &r.blend.set, 0, nil)
176- d.CmdDraw(r.command, 6, uint32(last-first), 0, uint32(first))
177- d.CmdEndRendering(r.command)
178- b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, vk.AccessTransferWriteBit|vk.AccessTransferReadBit
179- b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutTransferDstOptimal
180- d.CmdPipelineBarrier(r.command, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
181-}
182-
183-// createView makes the color attachment view of a target image.
184-func (r *Renderer) createView(t *target) error {
185- info := vk.ImageViewCreateInfo{
186- SType: vk.StructureTypeImageViewCreateInfo, Image: t.image, ViewType: vk.ImageViewType2d, Format: vk.FormatB8g8r8a8Unorm,
187- SubresourceRange: vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1},
188- }
189- return checked("vkCreateImageView", r.dd.CreateImageView(r.device, &info, nil, &t.view))
190-}
A internal/adapters/vulkan/compose.go +305 -0
@@ -0,0 +1,305 @@
1+package vulkan
2+
3+import (
4+ _ "embed"
5+ "encoding/binary"
6+ "image"
7+ "math"
8+ "unsafe"
9+
10+ vk "github.com/bnema/purego-vulkan/vulkan"
11+)
12+
13+// Composition is one graphics pipeline drawing every piece of a frame as
14+// a quad, in paint order, inside one dynamic rendering pass: solid fills,
15+// client dmabufs sampled as images, and client wl_shm pixels read from a
16+// GPU buffer (shm.go). Push constants carry each draw; the descriptor set
17+// holds its image and buffer. Client pixels are premultiplied, hence the
18+// blend ONE, ONE_MINUS_SRC_ALPHA; opaque draws force alpha to 1.
19+
20+//go:generate glslc -O --target-env=vulkan1.3 shaders/compose.vert -o shaders/compose.vert.spv
21+//go:generate glslc -O --target-env=vulkan1.3 shaders/compose.frag -o shaders/compose.frag.spv
22+
23+var (
24+ //go:embed shaders/compose.vert.spv
25+ composeVert []byte
26+ //go:embed shaders/compose.frag.spv
27+ composeFrag []byte
28+)
29+
30+// Draw modes and flags (shaders: modeSolid, modeImage, flag*).
31+const (
32+ modeSolid = 0
33+ modeImage = 1
34+ modeBuffer = 2
35+
36+ flagOpaque = 1
37+ flagExact = 2
38+)
39+
40+// pushConstants is struct Draw of the shaders (std430 push constant block).
41+type pushConstants struct {
42+ rect [4]int32
43+ mapv [4]float32
44+ color [4]float32
45+ buf [4]uint32
46+ misc [4]uint32
47+}
48+
49+// draw is one quad of a frame.
50+type draw struct {
51+ pc pushConstants
52+ set vk.DescriptorSet
53+ im *imported // sampled client dmabuf, for ownership barriers
54+}
55+
56+// composer is the pipeline state and the objects every draw can bind.
57+type composer struct {
58+ setLayout vk.DescriptorSetLayout
59+ layout vk.PipelineLayout
60+ pipeline vk.Pipeline
61+ sampler vk.Sampler
62+ // Placeholders for the binding a draw does not use: every binding
63+ // the shader declares must be valid.
64+ dummyImage vk.Image
65+ dummyMemory vk.DeviceMemory
66+ dummyView vk.ImageView
67+ dummyBuffer gpuBuffer
68+ dummyPool vk.DescriptorPool
69+ dummySet vk.DescriptorSet
70+}
71+
72+func (r *Renderer) createComposer() error {
73+ d, c := r.dd, &r.compose
74+ bindings := []vk.DescriptorSetLayoutBinding{
75+ {Binding: 0, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
76+ {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
77+ }
78+ sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 2, Bindings: &bindings[0]}
79+ if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &c.setLayout)); err != nil {
80+ return err
81+ }
82+ pcr := vk.PushConstantRange{StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit, Size: uint32(unsafe.Sizeof(pushConstants{}))}
83+ pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &c.setLayout, PushConstantRangeCount: 1, PushConstantRanges: &pcr}
84+ if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &c.layout)); err != nil {
85+ return err
86+ }
87+ si := vk.SamplerCreateInfo{SType: vk.StructureTypeSamplerCreateInfo, MagFilter: vk.FilterLinear, MinFilter: vk.FilterLinear, AddressModeU: vk.SamplerAddressModeClampToEdge, AddressModeV: vk.SamplerAddressModeClampToEdge, AddressModeW: vk.SamplerAddressModeClampToEdge}
88+ if err := checked("vkCreateSampler", d.CreateSampler(r.device, &si, nil, &c.sampler)); err != nil {
89+ return err
90+ }
91+ if err := r.createPipeline(); err != nil {
92+ return err
93+ }
94+ return r.createDummies()
95+}
96+
97+func (r *Renderer) createPipeline() error {
98+ d, c := r.dd, &r.compose
99+ vert, err := r.shaderModule(composeVert)
100+ if err != nil {
101+ return err
102+ }
103+ defer d.DestroyShaderModule(r.device, vert, nil)
104+ frag, err := r.shaderModule(composeFrag)
105+ if err != nil {
106+ return err
107+ }
108+ defer d.DestroyShaderModule(r.device, frag, nil)
109+ main := []byte("main\x00")
110+ stages := []vk.PipelineShaderStageCreateInfo{
111+ {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]},
112+ {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]},
113+ }
114+ vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo}
115+ assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList}
116+ // The target size is fixed for the renderer's life: static viewport.
117+ viewport := vk.Viewport{Width: float32(r.width), Height: float32(r.height), MaxDepth: 1}
118+ scissor := vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}
119+ viewportState := vk.PipelineViewportStateCreateInfo{SType: vk.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor}
120+ raster := vk.PipelineRasterizationStateCreateInfo{SType: vk.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vk.PolygonModeFill, CullMode: vk.CullModeNone, FrontFace: vk.FrontFaceCounterClockwise, LineWidth: 1}
121+ multisample := vk.PipelineMultisampleStateCreateInfo{SType: vk.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vk.SampleCount1Bit}
122+ attachment := vk.PipelineColorBlendAttachmentState{
123+ BlendEnable: 1,
124+ SrcColorBlendFactor: vk.BlendFactorOne, DstColorBlendFactor: vk.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vk.BlendOpAdd,
125+ SrcAlphaBlendFactor: vk.BlendFactorOne, DstAlphaBlendFactor: vk.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vk.BlendOpAdd,
126+ ColorWriteMask: vk.ColorComponentRBit | vk.ColorComponentGBit | vk.ColorComponentBBit | vk.ColorComponentABit,
127+ }
128+ blendState := vk.PipelineColorBlendStateCreateInfo{SType: vk.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &attachment}
129+ format := vk.Format(vk.FormatB8g8r8a8Unorm)
130+ rendering := vk.PipelineRenderingCreateInfo{SType: vk.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format}
131+ gpi := vk.GraphicsPipelineCreateInfo{
132+ SType: vk.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering),
133+ StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly,
134+ ViewportState: &viewportState, RasterizationState: &raster, MultisampleState: &multisample,
135+ ColorBlendState: &blendState, Layout: c.layout, BasePipelineIndex: -1,
136+ }
137+ return checked("vkCreateGraphicsPipelines", d.CreateGraphicsPipelines(r.device, 0, 1, &gpi, nil, &c.pipeline))
138+}
139+
140+// createDummies makes the placeholder image (1×1, shader-read layout) and
141+// buffer, and the set fills bind.
142+func (r *Renderer) createDummies() error {
143+ d, c := r.dd, &r.compose
144+ 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}
145+ if err := checked("vkCreateImage(dummy)", d.CreateImage(r.device, &ii, nil, &c.dummyImage)); err != nil {
146+ return err
147+ }
148+ var req vk.MemoryRequirements
149+ d.GetImageMemoryRequirements(r.device, c.dummyImage, &req)
150+ kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
151+ if err != nil {
152+ return err
153+ }
154+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
155+ if err := checked("vkAllocateMemory(dummy)", d.AllocateMemory(r.device, &alloc, nil, &c.dummyMemory)); err != nil {
156+ return err
157+ }
158+ if err := checked("vkBindImageMemory(dummy)", d.BindImageMemory(r.device, c.dummyImage, c.dummyMemory, 0)); err != nil {
159+ return err
160+ }
161+ if c.dummyView, err = r.imageView(c.dummyImage, vk.FormatB8g8r8a8Unorm); err != nil {
162+ return err
163+ }
164+ if err := r.newGPUBuffer(&c.dummyBuffer, 16); err != nil {
165+ return err
166+ }
167+ if c.dummyPool, c.dummySet, err = r.newSet(c.dummyView, c.dummyBuffer.buffer); err != nil {
168+ return err
169+ }
170+ // Move the placeholder image to the layout every draw declares.
171+ return r.oneShot(func(cmd vk.CommandBuffer) {
172+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, DstAccessMask: vk.AccessShaderReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: c.dummyImage, SubresourceRange: colorRange}
173+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)
174+ })
175+}
176+
177+// newSet allocates a descriptor set from its own pool (the bindings lack
178+// vkFreeDescriptorSets: the pool is destroyed with the object).
179+func (r *Renderer) newSet(view vk.ImageView, buffer vk.Buffer) (vk.DescriptorPool, vk.DescriptorSet, error) {
180+ d := r.dd
181+ sizes := []vk.DescriptorPoolSize{{Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1}, {Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1}}
182+ dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 2, PoolSizes: &sizes[0]}
183+ var pool vk.DescriptorPool
184+ if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &pool)); err != nil {
185+ return 0, 0, err
186+ }
187+ var set vk.DescriptorSet
188+ dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: pool, DescriptorSetCount: 1, SetLayouts: &r.compose.setLayout}
189+ if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &set)); err != nil {
190+ d.DestroyDescriptorPool(r.device, pool, nil)
191+ return 0, 0, err
192+ }
193+ img := vk.DescriptorImageInfo{Sampler: r.compose.sampler, ImageView: view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal}
194+ buf := vk.DescriptorBufferInfo{Buffer: buffer, Range: wholeSize}
195+ writes := []vk.WriteDescriptorSet{
196+ {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img},
197+ {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &buf},
198+ }
199+ d.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil)
200+ return pool, set, nil
201+}
202+
203+func (r *Renderer) destroyComposer() {
204+ d, c := r.dd, &r.compose
205+ if c.dummyPool != 0 {
206+ d.DestroyDescriptorPool(r.device, c.dummyPool, nil)
207+ }
208+ r.freeGPUBuffer(&c.dummyBuffer)
209+ if c.dummyView != 0 {
210+ d.DestroyImageView(r.device, c.dummyView, nil)
211+ }
212+ if c.dummyImage != 0 {
213+ d.DestroyImage(r.device, c.dummyImage, nil)
214+ }
215+ if c.dummyMemory != 0 {
216+ d.FreeMemory(r.device, c.dummyMemory, nil)
217+ }
218+ if c.sampler != 0 {
219+ d.DestroySampler(r.device, c.sampler, nil)
220+ }
221+ if c.pipeline != 0 {
222+ d.DestroyPipeline(r.device, c.pipeline, nil)
223+ }
224+ if c.layout != 0 {
225+ d.DestroyPipelineLayout(r.device, c.layout, nil)
226+ }
227+ if c.setLayout != 0 {
228+ d.DestroyDescriptorSetLayout(r.device, c.setLayout, nil)
229+ }
230+ *c = composer{}
231+}
232+
233+func (r *Renderer) shaderModule(code []byte) (vk.ShaderModule, error) {
234+ // SPIR-V is read as words: copy into a uint32-aligned slice.
235+ words := make([]uint32, len(code)/4)
236+ for i := range words {
237+ words[i] = binary.LittleEndian.Uint32(code[i*4:])
238+ }
239+ info := vk.ShaderModuleCreateInfo{SType: vk.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(words) * 4), Code: &words[0]}
240+ var m vk.ShaderModule
241+ return m, checked("vkCreateShaderModule", r.dd.CreateShaderModule(r.device, &info, nil, &m))
242+}
243+
244+var colorRange = vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
245+
246+// imageView makes a 2D color view of an image.
247+func (r *Renderer) imageView(img vk.Image, format vk.Format) (vk.ImageView, error) {
248+ info := vk.ImageViewCreateInfo{SType: vk.StructureTypeImageViewCreateInfo, Image: img, ViewType: vk.ImageViewType2d, Format: format, SubresourceRange: colorRange}
249+ var v vk.ImageView
250+ return v, checked("vkCreateImageView", r.dd.CreateImageView(r.device, &info, nil, &v))
251+}
252+
253+// createView makes the color attachment view of a target image.
254+func (r *Renderer) createView(t *target) (err error) {
255+ t.view, err = r.imageView(t.image, vk.FormatB8g8r8a8Unorm)
256+ return err
257+}
258+
259+// fillDraw is a solid rect of c.
260+func (r *Renderer) fillDraw(rect image.Rectangle, c [3]uint8) draw {
261+ dr := draw{set: r.compose.dummySet}
262+ dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
263+ dr.pc.color = [4]float32{float32(c[0]) / 255, float32(c[1]) / 255, float32(c[2]) / 255, 1}
264+ dr.pc.misc = [4]uint32{modeSolid, 0, uint32(r.width), uint32(r.height)}
265+ return dr
266+}
267+
268+// contentDraw draws the visible rect of a w×h buffer mapped onto full
269+// (target pixels). exact: the buffer is drawn at its size.
270+func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, mode uint32, opaque bool) draw {
271+ var dr draw
272+ sx, sy := float32(w)/float32(full.Dx()), float32(h)/float32(full.Dy())
273+ dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
274+ dr.pc.mapv = [4]float32{-float32(full.Min.X) * sx, -float32(full.Min.Y) * sy, sx, sy}
275+ flags := uint32(0)
276+ if opaque {
277+ flags |= flagOpaque
278+ }
279+ if full.Dx() == w && full.Dy() == h {
280+ flags |= flagExact
281+ }
282+ dr.pc.misc = [4]uint32{mode, flags, uint32(r.width), uint32(r.height)}
283+ return dr
284+}
285+
286+// recordDraws draws ds in one rendering pass that clears view to bg.
287+func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]uint8, ds []draw) {
288+ d, c := r.dd, &r.compose
289+ clear := vk.ClearValue{math.Float32bits(float32(bg[0]) / 255), math.Float32bits(float32(bg[1]) / 255), math.Float32bits(float32(bg[2]) / 255), math.Float32bits(1)}
290+ attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpClear, StoreOp: vk.AttachmentStoreOpStore, ClearValue: clear}
291+ info := vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment}
292+ d.CmdBeginRendering(cmd, &info)
293+ d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, c.pipeline)
294+ var bound vk.DescriptorSet
295+ for i := range ds {
296+ dr := &ds[i]
297+ if dr.set != bound {
298+ d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, c.layout, 0, 1, &dr.set, 0, nil)
299+ bound = dr.set
300+ }
301+ d.CmdPushConstants(cmd, c.layout, vk.ShaderStageVertexBit|vk.ShaderStageFragmentBit, 0, uint32(unsafe.Sizeof(dr.pc)), unsafe.Pointer(&dr.pc))
302+ d.CmdDraw(cmd, 6, 1, 0, 0)
303+ }
304+ d.CmdEndRendering(cmd)
305+}
@@ -0,0 +1,305 @@
1+package vulkan
2+
3+import (
4+ _ "embed"
5+ "encoding/binary"
6+ "image"
7+ "math"
8+ "unsafe"
9+
10+ vk "github.com/bnema/purego-vulkan/vulkan"
11+)
12+
13+// Composition is one graphics pipeline drawing every piece of a frame as
14+// a quad, in paint order, inside one dynamic rendering pass: solid fills,
15+// client dmabufs sampled as images, and client wl_shm pixels read from a
16+// GPU buffer (shm.go). Push constants carry each draw; the descriptor set
17+// holds its image and buffer. Client pixels are premultiplied, hence the
18+// blend ONE, ONE_MINUS_SRC_ALPHA; opaque draws force alpha to 1.
19+
20+//go:generate glslc -O --target-env=vulkan1.3 shaders/compose.vert -o shaders/compose.vert.spv
21+//go:generate glslc -O --target-env=vulkan1.3 shaders/compose.frag -o shaders/compose.frag.spv
22+
23+var (
24+ //go:embed shaders/compose.vert.spv
25+ composeVert []byte
26+ //go:embed shaders/compose.frag.spv
27+ composeFrag []byte
28+)
29+
30+// Draw modes and flags (shaders: modeSolid, modeImage, flag*).
31+const (
32+ modeSolid = 0
33+ modeImage = 1
34+ modeBuffer = 2
35+
36+ flagOpaque = 1
37+ flagExact = 2
38+)
39+
40+// pushConstants is struct Draw of the shaders (std430 push constant block).
41+type pushConstants struct {
42+ rect [4]int32
43+ mapv [4]float32
44+ color [4]float32
45+ buf [4]uint32
46+ misc [4]uint32
47+}
48+
49+// draw is one quad of a frame.
50+type draw struct {
51+ pc pushConstants
52+ set vk.DescriptorSet
53+ im *imported // sampled client dmabuf, for ownership barriers
54+}
55+
56+// composer is the pipeline state and the objects every draw can bind.
57+type composer struct {
58+ setLayout vk.DescriptorSetLayout
59+ layout vk.PipelineLayout
60+ pipeline vk.Pipeline
61+ sampler vk.Sampler
62+ // Placeholders for the binding a draw does not use: every binding
63+ // the shader declares must be valid.
64+ dummyImage vk.Image
65+ dummyMemory vk.DeviceMemory
66+ dummyView vk.ImageView
67+ dummyBuffer gpuBuffer
68+ dummyPool vk.DescriptorPool
69+ dummySet vk.DescriptorSet
70+}
71+
72+func (r *Renderer) createComposer() error {
73+ d, c := r.dd, &r.compose
74+ bindings := []vk.DescriptorSetLayoutBinding{
75+ {Binding: 0, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
76+ {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
77+ }
78+ sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 2, Bindings: &bindings[0]}
79+ if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &c.setLayout)); err != nil {
80+ return err
81+ }
82+ pcr := vk.PushConstantRange{StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit, Size: uint32(unsafe.Sizeof(pushConstants{}))}
83+ pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &c.setLayout, PushConstantRangeCount: 1, PushConstantRanges: &pcr}
84+ if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &c.layout)); err != nil {
85+ return err
86+ }
87+ si := vk.SamplerCreateInfo{SType: vk.StructureTypeSamplerCreateInfo, MagFilter: vk.FilterLinear, MinFilter: vk.FilterLinear, AddressModeU: vk.SamplerAddressModeClampToEdge, AddressModeV: vk.SamplerAddressModeClampToEdge, AddressModeW: vk.SamplerAddressModeClampToEdge}
88+ if err := checked("vkCreateSampler", d.CreateSampler(r.device, &si, nil, &c.sampler)); err != nil {
89+ return err
90+ }
91+ if err := r.createPipeline(); err != nil {
92+ return err
93+ }
94+ return r.createDummies()
95+}
96+
97+func (r *Renderer) createPipeline() error {
98+ d, c := r.dd, &r.compose
99+ vert, err := r.shaderModule(composeVert)
100+ if err != nil {
101+ return err
102+ }
103+ defer d.DestroyShaderModule(r.device, vert, nil)
104+ frag, err := r.shaderModule(composeFrag)
105+ if err != nil {
106+ return err
107+ }
108+ defer d.DestroyShaderModule(r.device, frag, nil)
109+ main := []byte("main\x00")
110+ stages := []vk.PipelineShaderStageCreateInfo{
111+ {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]},
112+ {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]},
113+ }
114+ vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo}
115+ assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList}
116+ // The target size is fixed for the renderer's life: static viewport.
117+ viewport := vk.Viewport{Width: float32(r.width), Height: float32(r.height), MaxDepth: 1}
118+ scissor := vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}
119+ viewportState := vk.PipelineViewportStateCreateInfo{SType: vk.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor}
120+ raster := vk.PipelineRasterizationStateCreateInfo{SType: vk.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vk.PolygonModeFill, CullMode: vk.CullModeNone, FrontFace: vk.FrontFaceCounterClockwise, LineWidth: 1}
121+ multisample := vk.PipelineMultisampleStateCreateInfo{SType: vk.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vk.SampleCount1Bit}
122+ attachment := vk.PipelineColorBlendAttachmentState{
123+ BlendEnable: 1,
124+ SrcColorBlendFactor: vk.BlendFactorOne, DstColorBlendFactor: vk.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vk.BlendOpAdd,
125+ SrcAlphaBlendFactor: vk.BlendFactorOne, DstAlphaBlendFactor: vk.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vk.BlendOpAdd,
126+ ColorWriteMask: vk.ColorComponentRBit | vk.ColorComponentGBit | vk.ColorComponentBBit | vk.ColorComponentABit,
127+ }
128+ blendState := vk.PipelineColorBlendStateCreateInfo{SType: vk.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &attachment}
129+ format := vk.Format(vk.FormatB8g8r8a8Unorm)
130+ rendering := vk.PipelineRenderingCreateInfo{SType: vk.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format}
131+ gpi := vk.GraphicsPipelineCreateInfo{
132+ SType: vk.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering),
133+ StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly,
134+ ViewportState: &viewportState, RasterizationState: &raster, MultisampleState: &multisample,
135+ ColorBlendState: &blendState, Layout: c.layout, BasePipelineIndex: -1,
136+ }
137+ return checked("vkCreateGraphicsPipelines", d.CreateGraphicsPipelines(r.device, 0, 1, &gpi, nil, &c.pipeline))
138+}
139+
140+// createDummies makes the placeholder image (1×1, shader-read layout) and
141+// buffer, and the set fills bind.
142+func (r *Renderer) createDummies() error {
143+ d, c := r.dd, &r.compose
144+ 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}
145+ if err := checked("vkCreateImage(dummy)", d.CreateImage(r.device, &ii, nil, &c.dummyImage)); err != nil {
146+ return err
147+ }
148+ var req vk.MemoryRequirements
149+ d.GetImageMemoryRequirements(r.device, c.dummyImage, &req)
150+ kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
151+ if err != nil {
152+ return err
153+ }
154+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
155+ if err := checked("vkAllocateMemory(dummy)", d.AllocateMemory(r.device, &alloc, nil, &c.dummyMemory)); err != nil {
156+ return err
157+ }
158+ if err := checked("vkBindImageMemory(dummy)", d.BindImageMemory(r.device, c.dummyImage, c.dummyMemory, 0)); err != nil {
159+ return err
160+ }
161+ if c.dummyView, err = r.imageView(c.dummyImage, vk.FormatB8g8r8a8Unorm); err != nil {
162+ return err
163+ }
164+ if err := r.newGPUBuffer(&c.dummyBuffer, 16); err != nil {
165+ return err
166+ }
167+ if c.dummyPool, c.dummySet, err = r.newSet(c.dummyView, c.dummyBuffer.buffer); err != nil {
168+ return err
169+ }
170+ // Move the placeholder image to the layout every draw declares.
171+ return r.oneShot(func(cmd vk.CommandBuffer) {
172+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, DstAccessMask: vk.AccessShaderReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: c.dummyImage, SubresourceRange: colorRange}
173+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)
174+ })
175+}
176+
177+// newSet allocates a descriptor set from its own pool (the bindings lack
178+// vkFreeDescriptorSets: the pool is destroyed with the object).
179+func (r *Renderer) newSet(view vk.ImageView, buffer vk.Buffer) (vk.DescriptorPool, vk.DescriptorSet, error) {
180+ d := r.dd
181+ sizes := []vk.DescriptorPoolSize{{Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1}, {Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1}}
182+ dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 2, PoolSizes: &sizes[0]}
183+ var pool vk.DescriptorPool
184+ if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &pool)); err != nil {
185+ return 0, 0, err
186+ }
187+ var set vk.DescriptorSet
188+ dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: pool, DescriptorSetCount: 1, SetLayouts: &r.compose.setLayout}
189+ if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &set)); err != nil {
190+ d.DestroyDescriptorPool(r.device, pool, nil)
191+ return 0, 0, err
192+ }
193+ img := vk.DescriptorImageInfo{Sampler: r.compose.sampler, ImageView: view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal}
194+ buf := vk.DescriptorBufferInfo{Buffer: buffer, Range: wholeSize}
195+ writes := []vk.WriteDescriptorSet{
196+ {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img},
197+ {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &buf},
198+ }
199+ d.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil)
200+ return pool, set, nil
201+}
202+
203+func (r *Renderer) destroyComposer() {
204+ d, c := r.dd, &r.compose
205+ if c.dummyPool != 0 {
206+ d.DestroyDescriptorPool(r.device, c.dummyPool, nil)
207+ }
208+ r.freeGPUBuffer(&c.dummyBuffer)
209+ if c.dummyView != 0 {
210+ d.DestroyImageView(r.device, c.dummyView, nil)
211+ }
212+ if c.dummyImage != 0 {
213+ d.DestroyImage(r.device, c.dummyImage, nil)
214+ }
215+ if c.dummyMemory != 0 {
216+ d.FreeMemory(r.device, c.dummyMemory, nil)
217+ }
218+ if c.sampler != 0 {
219+ d.DestroySampler(r.device, c.sampler, nil)
220+ }
221+ if c.pipeline != 0 {
222+ d.DestroyPipeline(r.device, c.pipeline, nil)
223+ }
224+ if c.layout != 0 {
225+ d.DestroyPipelineLayout(r.device, c.layout, nil)
226+ }
227+ if c.setLayout != 0 {
228+ d.DestroyDescriptorSetLayout(r.device, c.setLayout, nil)
229+ }
230+ *c = composer{}
231+}
232+
233+func (r *Renderer) shaderModule(code []byte) (vk.ShaderModule, error) {
234+ // SPIR-V is read as words: copy into a uint32-aligned slice.
235+ words := make([]uint32, len(code)/4)
236+ for i := range words {
237+ words[i] = binary.LittleEndian.Uint32(code[i*4:])
238+ }
239+ info := vk.ShaderModuleCreateInfo{SType: vk.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(words) * 4), Code: &words[0]}
240+ var m vk.ShaderModule
241+ return m, checked("vkCreateShaderModule", r.dd.CreateShaderModule(r.device, &info, nil, &m))
242+}
243+
244+var colorRange = vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
245+
246+// imageView makes a 2D color view of an image.
247+func (r *Renderer) imageView(img vk.Image, format vk.Format) (vk.ImageView, error) {
248+ info := vk.ImageViewCreateInfo{SType: vk.StructureTypeImageViewCreateInfo, Image: img, ViewType: vk.ImageViewType2d, Format: format, SubresourceRange: colorRange}
249+ var v vk.ImageView
250+ return v, checked("vkCreateImageView", r.dd.CreateImageView(r.device, &info, nil, &v))
251+}
252+
253+// createView makes the color attachment view of a target image.
254+func (r *Renderer) createView(t *target) (err error) {
255+ t.view, err = r.imageView(t.image, vk.FormatB8g8r8a8Unorm)
256+ return err
257+}
258+
259+// fillDraw is a solid rect of c.
260+func (r *Renderer) fillDraw(rect image.Rectangle, c [3]uint8) draw {
261+ dr := draw{set: r.compose.dummySet}
262+ dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
263+ dr.pc.color = [4]float32{float32(c[0]) / 255, float32(c[1]) / 255, float32(c[2]) / 255, 1}
264+ dr.pc.misc = [4]uint32{modeSolid, 0, uint32(r.width), uint32(r.height)}
265+ return dr
266+}
267+
268+// contentDraw draws the visible rect of a w×h buffer mapped onto full
269+// (target pixels). exact: the buffer is drawn at its size.
270+func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, mode uint32, opaque bool) draw {
271+ var dr draw
272+ sx, sy := float32(w)/float32(full.Dx()), float32(h)/float32(full.Dy())
273+ dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
274+ dr.pc.mapv = [4]float32{-float32(full.Min.X) * sx, -float32(full.Min.Y) * sy, sx, sy}
275+ flags := uint32(0)
276+ if opaque {
277+ flags |= flagOpaque
278+ }
279+ if full.Dx() == w && full.Dy() == h {
280+ flags |= flagExact
281+ }
282+ dr.pc.misc = [4]uint32{mode, flags, uint32(r.width), uint32(r.height)}
283+ return dr
284+}
285+
286+// recordDraws draws ds in one rendering pass that clears view to bg.
287+func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]uint8, ds []draw) {
288+ d, c := r.dd, &r.compose
289+ clear := vk.ClearValue{math.Float32bits(float32(bg[0]) / 255), math.Float32bits(float32(bg[1]) / 255), math.Float32bits(float32(bg[2]) / 255), math.Float32bits(1)}
290+ attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpClear, StoreOp: vk.AttachmentStoreOpStore, ClearValue: clear}
291+ info := vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment}
292+ d.CmdBeginRendering(cmd, &info)
293+ d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, c.pipeline)
294+ var bound vk.DescriptorSet
295+ for i := range ds {
296+ dr := &ds[i]
297+ if dr.set != bound {
298+ d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, c.layout, 0, 1, &dr.set, 0, nil)
299+ bound = dr.set
300+ }
301+ d.CmdPushConstants(cmd, c.layout, vk.ShaderStageVertexBit|vk.ShaderStageFragmentBit, 0, uint32(unsafe.Sizeof(dr.pc)), unsafe.Pointer(&dr.pc))
302+ d.CmdDraw(cmd, 6, 1, 0, 0)
303+ }
304+ d.CmdEndRendering(cmd)
305+}
A internal/adapters/vulkan/compose_test.go +138 -0
@@ -0,0 +1,138 @@
1+package vulkan
2+
3+import (
4+ "context"
5+ "image/color"
6+ "slices"
7+ "testing"
8+ "time"
9+
10+ "github.com/bnema/nefertty/internal/adapters/syncfile"
11+ "github.com/bnema/nefertty/internal/ports"
12+)
13+
14+// near reports whether two colors are within tol per channel.
15+func near(a, b color.RGBA, tol int) bool {
16+ d := func(x, y uint8) bool { v := int(x) - int(y); return v >= -tol && v <= tol }
17+ return d(a.R, b.R) && d(a.G, b.G) && d(a.B, b.B)
18+}
19+
20+// Scaled wl_shm pixels filter linearly: halfway between a black and a
21+// white texel is grey, not either one (nearest neighbour).
22+func TestRendererScaledSHMFiltersLinearly(t *testing.T) {
23+ r, err := New(64, 16)
24+ if err != nil {
25+ t.Skipf("Vulkan unavailable: %v", err)
26+ }
27+ defer r.Close()
28+ // 2×1 buffer: black, white; drawn 64×16, texel centres at x=16, x=48.
29+ px := []byte{0, 0, 0, 255, 255, 255, 255, 255}
30+ c := shmContent(t, 2, 1, 8, px)
31+ c.LogicalW, c.LogicalH = 64, 16
32+ scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}, Borderless: true}}}
33+ if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
34+ t.Fatal(err)
35+ }
36+ got := r.Pixels().RGBAAt(32, 8)
37+ if !near(got, color.RGBA{128, 128, 128, 255}, 8) {
38+ t.Fatalf("midpoint %v, want grey", got)
39+ }
40+ if got := r.Pixels().RGBAAt(2, 8); !near(got, color.RGBA{0, 0, 0, 255}, 2) {
41+ t.Fatalf("left edge %v, want black (clamped)", got)
42+ }
43+}
44+
45+// Scaled dmabufs are sampled with the linear sampler.
46+func TestRendererScaledDMABufFiltersLinearly(t *testing.T) {
47+ r, err := New(128, 4)
48+ if err != nil {
49+ t.Skipf("Vulkan unavailable: %v", err)
50+ }
51+ defer r.Close()
52+ linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
53+ if !slices.Contains(r.DMABuf().Formats, linear) {
54+ t.Skipf("linear ARGB8888 not importable")
55+ }
56+ // 64×1: left half black, right half white, drawn twice as wide.
57+ f := udmabuf(t, 64, 1, func(x, _ int) [4]byte {
58+ if x < 32 {
59+ return [4]byte{0, 0, 0, 255}
60+ }
61+ return [4]byte{255, 255, 255, 255}
62+ })
63+ buf := &ports.DMABuf{ID: 3, Width: 64, Height: 1, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
64+ scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 4}, Borderless: true}}}
65+ c := ports.SurfaceContent{ID: 1, Width: 64, Height: 1, LogicalW: 128, LogicalH: 4, DMABuf: buf}
66+ if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
67+ t.Fatal(err)
68+ }
69+ // Target x=64 (centre 64.5) maps to buffer 32.25: between texel 31
70+ // (black, centre 31.5) and 32 (white, centre 32.5), 75% white.
71+ if got := r.Pixels().RGBAAt(64, 2); !near(got, color.RGBA{191, 191, 191, 255}, 10) {
72+ t.Fatalf("boundary %v, want a linear mix", got)
73+ }
74+}
75+
76+// Render returns a sync file that becomes readable when the GPU is done.
77+func TestRendererReturnsDoneFence(t *testing.T) {
78+ r, err := New(32, 32)
79+ if err != nil {
80+ t.Skipf("Vulkan unavailable: %v", err)
81+ }
82+ defer r.Close()
83+ if !r.syncFD {
84+ t.Skip("device cannot export sync files")
85+ }
86+ done, err := r.Render(ports.Scene{Background: "#102030"}, nil)
87+ if err != nil {
88+ t.Fatal(err)
89+ }
90+ if done == nil {
91+ t.Fatal("no done fence")
92+ }
93+ defer done.Close()
94+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
95+ defer cancel()
96+ if err := syncfile.Wait(ctx, done); err != nil {
97+ t.Fatal(err)
98+ }
99+}
100+
101+// An unchanged wl_shm window is not copied again; a new content is.
102+func TestRendererSHMCopiedOncePerContent(t *testing.T) {
103+ r, err := New(32, 32)
104+ if err != nil {
105+ t.Skipf("Vulkan unavailable: %v", err)
106+ }
107+ defer r.Close()
108+ c := solidContent(t, 16, 16, color.RGBA{1, 2, 3, 255})
109+ c.ID, c.Seq = 1, 1
110+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}, Borderless: true}}}
111+ contents := map[ports.WindowID]ports.SurfaceContent{1: c}
112+ render := func() {
113+ t.Helper()
114+ done, err := r.Render(scene, contents)
115+ if done != nil {
116+ done.Close()
117+ }
118+ if err != nil {
119+ t.Fatal(err)
120+ }
121+ }
122+ render()
123+ if r.copied != 16*16*4 {
124+ t.Fatalf("first frame copied %d", r.copied)
125+ }
126+ for range 3 {
127+ render()
128+ }
129+ if r.copied != 16*16*4 {
130+ t.Fatalf("unchanged content copied again: %d", r.copied)
131+ }
132+ c.Seq = 2
133+ contents[1] = c
134+ render()
135+ if r.copied != 2*16*16*4 {
136+ t.Fatalf("new content copied %d", r.copied)
137+ }
138+}
@@ -0,0 +1,138 @@
1+package vulkan
2+
3+import (
4+ "context"
5+ "image/color"
6+ "slices"
7+ "testing"
8+ "time"
9+
10+ "github.com/bnema/nefertty/internal/adapters/syncfile"
11+ "github.com/bnema/nefertty/internal/ports"
12+)
13+
14+// near reports whether two colors are within tol per channel.
15+func near(a, b color.RGBA, tol int) bool {
16+ d := func(x, y uint8) bool { v := int(x) - int(y); return v >= -tol && v <= tol }
17+ return d(a.R, b.R) && d(a.G, b.G) && d(a.B, b.B)
18+}
19+
20+// Scaled wl_shm pixels filter linearly: halfway between a black and a
21+// white texel is grey, not either one (nearest neighbour).
22+func TestRendererScaledSHMFiltersLinearly(t *testing.T) {
23+ r, err := New(64, 16)
24+ if err != nil {
25+ t.Skipf("Vulkan unavailable: %v", err)
26+ }
27+ defer r.Close()
28+ // 2×1 buffer: black, white; drawn 64×16, texel centres at x=16, x=48.
29+ px := []byte{0, 0, 0, 255, 255, 255, 255, 255}
30+ c := shmContent(t, 2, 1, 8, px)
31+ c.LogicalW, c.LogicalH = 64, 16
32+ scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}, Borderless: true}}}
33+ if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
34+ t.Fatal(err)
35+ }
36+ got := r.Pixels().RGBAAt(32, 8)
37+ if !near(got, color.RGBA{128, 128, 128, 255}, 8) {
38+ t.Fatalf("midpoint %v, want grey", got)
39+ }
40+ if got := r.Pixels().RGBAAt(2, 8); !near(got, color.RGBA{0, 0, 0, 255}, 2) {
41+ t.Fatalf("left edge %v, want black (clamped)", got)
42+ }
43+}
44+
45+// Scaled dmabufs are sampled with the linear sampler.
46+func TestRendererScaledDMABufFiltersLinearly(t *testing.T) {
47+ r, err := New(128, 4)
48+ if err != nil {
49+ t.Skipf("Vulkan unavailable: %v", err)
50+ }
51+ defer r.Close()
52+ linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
53+ if !slices.Contains(r.DMABuf().Formats, linear) {
54+ t.Skipf("linear ARGB8888 not importable")
55+ }
56+ // 64×1: left half black, right half white, drawn twice as wide.
57+ f := udmabuf(t, 64, 1, func(x, _ int) [4]byte {
58+ if x < 32 {
59+ return [4]byte{0, 0, 0, 255}
60+ }
61+ return [4]byte{255, 255, 255, 255}
62+ })
63+ buf := &ports.DMABuf{ID: 3, Width: 64, Height: 1, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
64+ scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 4}, Borderless: true}}}
65+ c := ports.SurfaceContent{ID: 1, Width: 64, Height: 1, LogicalW: 128, LogicalH: 4, DMABuf: buf}
66+ if _, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
67+ t.Fatal(err)
68+ }
69+ // Target x=64 (centre 64.5) maps to buffer 32.25: between texel 31
70+ // (black, centre 31.5) and 32 (white, centre 32.5), 75% white.
71+ if got := r.Pixels().RGBAAt(64, 2); !near(got, color.RGBA{191, 191, 191, 255}, 10) {
72+ t.Fatalf("boundary %v, want a linear mix", got)
73+ }
74+}
75+
76+// Render returns a sync file that becomes readable when the GPU is done.
77+func TestRendererReturnsDoneFence(t *testing.T) {
78+ r, err := New(32, 32)
79+ if err != nil {
80+ t.Skipf("Vulkan unavailable: %v", err)
81+ }
82+ defer r.Close()
83+ if !r.syncFD {
84+ t.Skip("device cannot export sync files")
85+ }
86+ done, err := r.Render(ports.Scene{Background: "#102030"}, nil)
87+ if err != nil {
88+ t.Fatal(err)
89+ }
90+ if done == nil {
91+ t.Fatal("no done fence")
92+ }
93+ defer done.Close()
94+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
95+ defer cancel()
96+ if err := syncfile.Wait(ctx, done); err != nil {
97+ t.Fatal(err)
98+ }
99+}
100+
101+// An unchanged wl_shm window is not copied again; a new content is.
102+func TestRendererSHMCopiedOncePerContent(t *testing.T) {
103+ r, err := New(32, 32)
104+ if err != nil {
105+ t.Skipf("Vulkan unavailable: %v", err)
106+ }
107+ defer r.Close()
108+ c := solidContent(t, 16, 16, color.RGBA{1, 2, 3, 255})
109+ c.ID, c.Seq = 1, 1
110+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}, Borderless: true}}}
111+ contents := map[ports.WindowID]ports.SurfaceContent{1: c}
112+ render := func() {
113+ t.Helper()
114+ done, err := r.Render(scene, contents)
115+ if done != nil {
116+ done.Close()
117+ }
118+ if err != nil {
119+ t.Fatal(err)
120+ }
121+ }
122+ render()
123+ if r.copied != 16*16*4 {
124+ t.Fatalf("first frame copied %d", r.copied)
125+ }
126+ for range 3 {
127+ render()
128+ }
129+ if r.copied != 16*16*4 {
130+ t.Fatalf("unchanged content copied again: %d", r.copied)
131+ }
132+ c.Seq = 2
133+ contents[1] = c
134+ render()
135+ if r.copied != 2*16*16*4 {
136+ t.Fatalf("new content copied %d", r.copied)
137+ }
138+}
M internal/adapters/vulkan/cursor.go +8 -4
@@ -29,7 +29,7 @@ const fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
2929 func (r *Renderer) CursorBuffers(size int) ([2]ports.DMABuf, error) {
3030 var out [2]ports.DMABuf
3131 r.dropCursors()
32- if r.dd.GetMemoryFdKHR == nil || size <= 0 || size > 512 {
32+ if r.dd.GetMemoryFdKHR == nil || len(r.dmabuf.Formats) == 0 || size <= 0 || size > 512 {
3333 return out, errors.New("device cannot export cursor images")
3434 }
3535 for i := range r.cursors {
@@ -55,8 +55,12 @@ func (r *Renderer) cursorImage(size int) (*cursorImage, ports.DMABuf, error) {
5555 r.freeCursor(c)
5656 }
5757 }()
58- external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
59- ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: uint32(size), Height: uint32(size), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingLinear, Usage: vk.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutPreinitialized}
58+ // Exported images take an explicit DRM modifier: linear, as cursor
59+ // planes scan out. The GPU never touches them (the CPU writes, KMS reads).
60+ linear := uint64(0)
61+ list := vk.ImageDrmFormatModifierListCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: 1, DrmFormatModifiers: &linear}
62+ external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
63+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: uint32(size), Height: uint32(size), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
6064 if err := checked("vkCreateImage(cursor)", d.CreateImage(r.device, &ii, nil, &c.image)); err != nil {
6165 return nil, ports.DMABuf{}, err
6266 }
@@ -76,7 +80,7 @@ func (r *Renderer) cursorImage(size int) (*cursorImage, ports.DMABuf, error) {
7680 return nil, ports.DMABuf{}, err
7781 }
7882 var layout vk.SubresourceLayout
79- sub := vk.ImageSubresource{AspectMask: vk.ImageAspectColorBit}
83+ sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}
8084 d.GetImageSubresourceLayout(r.device, c.image, &sub, &layout)
8185 c.pitch = int(layout.RowPitch)
8286 // Cursor planes scan out packed rows from the start of the buffer.
@@ -29,7 +29,7 @@ const fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
29 func (r *Renderer) CursorBuffers(size int) ([2]ports.DMABuf, error) {29 func (r *Renderer) CursorBuffers(size int) ([2]ports.DMABuf, error) {
30 var out [2]ports.DMABuf30 var out [2]ports.DMABuf
31 r.dropCursors()31 r.dropCursors()
32- if r.dd.GetMemoryFdKHR == nil || size <= 0 || size > 512 {32+ if r.dd.GetMemoryFdKHR == nil || len(r.dmabuf.Formats) == 0 || size <= 0 || size > 512 {
33 return out, errors.New("device cannot export cursor images")33 return out, errors.New("device cannot export cursor images")
34 }34 }
35 for i := range r.cursors {35 for i := range r.cursors {
@@ -55,8 +55,12 @@ func (r *Renderer) cursorImage(size int) (*cursorImage, ports.DMABuf, error) {
55 r.freeCursor(c)55 r.freeCursor(c)
56 }56 }
57 }()57 }()
58- external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}58+ // Exported images take an explicit DRM modifier: linear, as cursor
59- ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: uint32(size), Height: uint32(size), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingLinear, Usage: vk.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutPreinitialized}59+ // planes scan out. The GPU never touches them (the CPU writes, KMS reads).
60+ linear := uint64(0)
61+ list := vk.ImageDrmFormatModifierListCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: 1, DrmFormatModifiers: &linear}
62+ external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
63+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: uint32(size), Height: uint32(size), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
60 if err := checked("vkCreateImage(cursor)", d.CreateImage(r.device, &ii, nil, &c.image)); err != nil {64 if err := checked("vkCreateImage(cursor)", d.CreateImage(r.device, &ii, nil, &c.image)); err != nil {
61 return nil, ports.DMABuf{}, err65 return nil, ports.DMABuf{}, err
62 }66 }
@@ -76,7 +80,7 @@ func (r *Renderer) cursorImage(size int) (*cursorImage, ports.DMABuf, error) {
76 return nil, ports.DMABuf{}, err80 return nil, ports.DMABuf{}, err
77 }81 }
78 var layout vk.SubresourceLayout82 var layout vk.SubresourceLayout
79- sub := vk.ImageSubresource{AspectMask: vk.ImageAspectColorBit}83+ sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}
80 d.GetImageSubresourceLayout(r.device, c.image, &sub, &layout)84 d.GetImageSubresourceLayout(r.device, c.image, &sub, &layout)
81 c.pitch = int(layout.RowPitch)85 c.pitch = int(layout.RowPitch)
82 // Cursor planes scan out packed rows from the start of the buffer.86 // Cursor planes scan out packed rows from the start of the buffer.
M internal/adapters/vulkan/dmabuf.go +28 -11
@@ -13,7 +13,7 @@ import (
1313
1414 // dmabuf import (linux-dmabuf clients): each client buffer becomes a
1515 // VkImage bound to its imported memory, cached by buffer ID while the
16-// client keeps attaching it, and blitted into the output image. Sync is
16+// client keeps attaching it, and sampled by the compose shader. Sync is
1717 // implicit: before sampling, the renderer waits on the fences the client's
1818 // driver attached to the buffer (DMA_BUF_IOCTL_EXPORT_SYNC_FILE), imported
1919 // as a semaphore.
@@ -86,7 +86,7 @@ func cstring(b []byte) string {
8686 }
8787
8888 // probeDMABuf lists the fourcc/modifier pairs the device imports as
89-// single-plane blit sources, and the render node.
89+// single-plane sampled images with linear filtering, and the render node.
9090 func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport {
9191 var sup ports.DMABufSupport
9292 drm := vk.PhysicalDeviceDrmPropertiesEXT{SType: vk.StructureTypePhysicalDeviceDRMPropertiesEXT}
@@ -107,8 +107,9 @@ func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport {
107107 r.id.GetPhysicalDeviceFormatProperties2(physical, f.format, &fp)
108108 for _, m := range mods[:list.DrmFormatModifierCount] {
109109 // Multi-plane modifiers (compression metadata) are left to a
110- // later step; the blit needs a source.
111- if m.DrmFormatModifierPlaneCount != 1 || m.DrmFormatModifierTilingFeatures&vk.FormatFeatureBlitSrcBit == 0 {
110+ // later step.
111+ need := vk.FormatFeatureFlags(vk.FormatFeatureSampledImageBit | vk.FormatFeatureSampledImageFilterLinearBit)
112+ if m.DrmFormatModifierPlaneCount != 1 || m.DrmFormatModifierTilingFeatures&need != need {
112113 continue
113114 }
114115 if r.importable(physical, f.format, m.DrmFormatModifier) {
@@ -124,7 +125,7 @@ func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport {
124125 func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {
125126 mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
126127 ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
127- info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit}
128+ info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageSampledBit}
128129 extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
129130 out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
130131 if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
@@ -136,10 +137,14 @@ func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modi
136137 // DMABuf reports the formats this renderer imports.
137138 func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf }
138139
139-// imported is a client dmabuf bound to a VkImage.
140+// imported is a client dmabuf bound to a VkImage, with the view and set
141+// the compose shader samples it through.
140142 type imported struct {
141143 image vk.Image
142144 memory vk.DeviceMemory
145+ view vk.ImageView
146+ pool vk.DescriptorPool
147+ set vk.DescriptorSet
143148 fd int // our duplicate of plane 0, for implicit sync
144149 // last is the frame that drew it, for eviction.
145150 last uint64
@@ -177,7 +182,7 @@ func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) {
177182 layout := vk.SubresourceLayout{Offset: vk.DeviceSize(b.Planes[0].Offset), RowPitch: vk.DeviceSize(b.Planes[0].Stride)}
178183 explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: 1, PlaneLayouts: &layout}
179184 external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
180- ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(b.Width), Height: uint32(b.Height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
185+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(b.Width), Height: uint32(b.Height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageSampledBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
181186 if err := checked("vkCreateImage(dmabuf)", d.CreateImage(r.device, &ii, nil, &im.image)); err != nil {
182187 return nil, err
183188 }
@@ -206,6 +211,12 @@ func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) {
206211 if err := checked("vkBindImageMemory(dmabuf)", d.BindImageMemory(r.device, im.image, im.memory, 0)); err != nil {
207212 return nil, err
208213 }
214+ if im.view, err = r.imageView(im.image, format); err != nil {
215+ return nil, err
216+ }
217+ if im.pool, im.set, err = r.newSet(im.view, r.compose.dummyBuffer.buffer); err != nil {
218+ return nil, err
219+ }
209220 ok = true
210221 r.imports[b.ID] = im
211222 return im, nil
@@ -229,6 +240,12 @@ func dupFile(f *os.File) (int, error) {
229240 }
230241
231242 func (r *Renderer) release(im *imported) {
243+ if im.pool != 0 {
244+ r.dd.DestroyDescriptorPool(r.device, im.pool, nil)
245+ }
246+ if im.view != 0 {
247+ r.dd.DestroyImageView(r.device, im.view, nil)
248+ }
232249 if im.image != 0 {
233250 r.dd.DestroyImage(r.device, im.image, nil)
234251 }
@@ -240,13 +257,13 @@ func (r *Renderer) release(im *imported) {
240257 }
241258 }
242259
243-// dropUnused frees imports not drawn for importTTL frames: the client moved
244-// on to other buffers or went away. Called after a frame completes.
260+// dropUnused frees imports not drawn for importTTL frames (the client
261+// moved on to other buffers or went away), once the last frame that drew
262+// them completed.
245263 func (r *Renderer) dropUnused() {
246- r.frame++
247264 for id, im := range r.imports {
248265 if r.frame-im.last > importTTL {
249- r.release(im)
266+ r.retire(im.last, func() { r.release(im) })
250267 delete(r.imports, id)
251268 }
252269 }
@@ -13,7 +13,7 @@ import (
13 13
14 // dmabuf import (linux-dmabuf clients): each client buffer becomes a14 // dmabuf import (linux-dmabuf clients): each client buffer becomes a
15 // VkImage bound to its imported memory, cached by buffer ID while the15 // VkImage bound to its imported memory, cached by buffer ID while the
16-// client keeps attaching it, and blitted into the output image. Sync is16+// client keeps attaching it, and sampled by the compose shader. Sync is
17 // implicit: before sampling, the renderer waits on the fences the client's17 // implicit: before sampling, the renderer waits on the fences the client's
18 // driver attached to the buffer (DMA_BUF_IOCTL_EXPORT_SYNC_FILE), imported18 // driver attached to the buffer (DMA_BUF_IOCTL_EXPORT_SYNC_FILE), imported
19 // as a semaphore.19 // as a semaphore.
@@ -86,7 +86,7 @@ func cstring(b []byte) string {
86 }86 }
87 87
88 // probeDMABuf lists the fourcc/modifier pairs the device imports as88 // probeDMABuf lists the fourcc/modifier pairs the device imports as
89-// single-plane blit sources, and the render node.89+// single-plane sampled images with linear filtering, and the render node.
90 func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport {90 func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport {
91 var sup ports.DMABufSupport91 var sup ports.DMABufSupport
92 drm := vk.PhysicalDeviceDrmPropertiesEXT{SType: vk.StructureTypePhysicalDeviceDRMPropertiesEXT}92 drm := vk.PhysicalDeviceDrmPropertiesEXT{SType: vk.StructureTypePhysicalDeviceDRMPropertiesEXT}
@@ -107,8 +107,9 @@ func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport {
107 r.id.GetPhysicalDeviceFormatProperties2(physical, f.format, &fp)107 r.id.GetPhysicalDeviceFormatProperties2(physical, f.format, &fp)
108 for _, m := range mods[:list.DrmFormatModifierCount] {108 for _, m := range mods[:list.DrmFormatModifierCount] {
109 // Multi-plane modifiers (compression metadata) are left to a109 // Multi-plane modifiers (compression metadata) are left to a
110- // later step; the blit needs a source.110+ // later step.
111- if m.DrmFormatModifierPlaneCount != 1 || m.DrmFormatModifierTilingFeatures&vk.FormatFeatureBlitSrcBit == 0 {111+ need := vk.FormatFeatureFlags(vk.FormatFeatureSampledImageBit | vk.FormatFeatureSampledImageFilterLinearBit)
112+ if m.DrmFormatModifierPlaneCount != 1 || m.DrmFormatModifierTilingFeatures&need != need {
112 continue113 continue
113 }114 }
114 if r.importable(physical, f.format, m.DrmFormatModifier) {115 if r.importable(physical, f.format, m.DrmFormatModifier) {
@@ -124,7 +125,7 @@ func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport {
124 func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {125 func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {
125 mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}126 mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
126 ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}127 ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
127- info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit}128+ info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageSampledBit}
128 extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}129 extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
129 out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}130 out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
130 if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {131 if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
@@ -136,10 +137,14 @@ func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modi
136 // DMABuf reports the formats this renderer imports.137 // DMABuf reports the formats this renderer imports.
137 func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf }138 func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf }
138 139
139-// imported is a client dmabuf bound to a VkImage.140+// imported is a client dmabuf bound to a VkImage, with the view and set
141+// the compose shader samples it through.
140 type imported struct {142 type imported struct {
141 image vk.Image143 image vk.Image
142 memory vk.DeviceMemory144 memory vk.DeviceMemory
145+ view vk.ImageView
146+ pool vk.DescriptorPool
147+ set vk.DescriptorSet
143 fd int // our duplicate of plane 0, for implicit sync148 fd int // our duplicate of plane 0, for implicit sync
144 // last is the frame that drew it, for eviction.149 // last is the frame that drew it, for eviction.
145 last uint64150 last uint64
@@ -177,7 +182,7 @@ func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) {
177 layout := vk.SubresourceLayout{Offset: vk.DeviceSize(b.Planes[0].Offset), RowPitch: vk.DeviceSize(b.Planes[0].Stride)}182 layout := vk.SubresourceLayout{Offset: vk.DeviceSize(b.Planes[0].Offset), RowPitch: vk.DeviceSize(b.Planes[0].Stride)}
178 explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: 1, PlaneLayouts: &layout}183 explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: 1, PlaneLayouts: &layout}
179 external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}184 external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
180- ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(b.Width), Height: uint32(b.Height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}185+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(b.Width), Height: uint32(b.Height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageSampledBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
181 if err := checked("vkCreateImage(dmabuf)", d.CreateImage(r.device, &ii, nil, &im.image)); err != nil {186 if err := checked("vkCreateImage(dmabuf)", d.CreateImage(r.device, &ii, nil, &im.image)); err != nil {
182 return nil, err187 return nil, err
183 }188 }
@@ -206,6 +211,12 @@ func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) {
206 if err := checked("vkBindImageMemory(dmabuf)", d.BindImageMemory(r.device, im.image, im.memory, 0)); err != nil {211 if err := checked("vkBindImageMemory(dmabuf)", d.BindImageMemory(r.device, im.image, im.memory, 0)); err != nil {
207 return nil, err212 return nil, err
208 }213 }
214+ if im.view, err = r.imageView(im.image, format); err != nil {
215+ return nil, err
216+ }
217+ if im.pool, im.set, err = r.newSet(im.view, r.compose.dummyBuffer.buffer); err != nil {
218+ return nil, err
219+ }
209 ok = true220 ok = true
210 r.imports[b.ID] = im221 r.imports[b.ID] = im
211 return im, nil222 return im, nil
@@ -229,6 +240,12 @@ func dupFile(f *os.File) (int, error) {
229 }240 }
230 241
231 func (r *Renderer) release(im *imported) {242 func (r *Renderer) release(im *imported) {
243+ if im.pool != 0 {
244+ r.dd.DestroyDescriptorPool(r.device, im.pool, nil)
245+ }
246+ if im.view != 0 {
247+ r.dd.DestroyImageView(r.device, im.view, nil)
248+ }
232 if im.image != 0 {249 if im.image != 0 {
233 r.dd.DestroyImage(r.device, im.image, nil)250 r.dd.DestroyImage(r.device, im.image, nil)
234 }251 }
@@ -240,13 +257,13 @@ func (r *Renderer) release(im *imported) {
240 }257 }
241 }258 }
242 259
243-// dropUnused frees imports not drawn for importTTL frames: the client moved260+// dropUnused frees imports not drawn for importTTL frames (the client
244-// on to other buffers or went away. Called after a frame completes.261+// moved on to other buffers or went away), once the last frame that drew
262+// them completed.
245 func (r *Renderer) dropUnused() {263 func (r *Renderer) dropUnused() {
246- r.frame++
247 for id, im := range r.imports {264 for id, im := range r.imports {
248 if r.frame-im.last > importTTL {265 if r.frame-im.last > importTTL {
249- r.release(im)266+ r.retire(im.last, func() { r.release(im) })
250 delete(r.imports, id)267 delete(r.imports, id)
251 }268 }
252 }269 }
A internal/adapters/vulkan/frame.go +313 -0
@@ -0,0 +1,313 @@
1+package vulkan
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "math"
7+ "os"
8+ "runtime"
9+ "slices"
10+ "unsafe"
11+
12+ "github.com/bnema/nefertty/internal/ports"
13+ vk "github.com/bnema/purego-vulkan/vulkan"
14+)
15+
16+// Frames are recorded into a ring of slots, one per frame in flight. The
17+// CPU never waits for a frame it just submitted: Render returns a sync
18+// file the display waits on (KMS IN_FENCE_FD). A slot's fence is waited
19+// only when the slot is reused, and objects a frame read (imports, GPU
20+// buffers, client semaphores) are freed only once that frame completed.
21+
22+// frameSlots is how many frames may be in flight: one per output target.
23+const frameSlots = 2
24+
25+// frameSlot is the command buffer and sync objects of one frame in flight.
26+type frameSlot struct {
27+ cmd vk.CommandBuffer
28+ fence vk.Fence
29+ // done is signalled with the frame and exported as its sync file.
30+ done vk.Semaphore
31+ frame uint64 // the frame recorded last, 0 before any
32+ busy bool // submitted, fence not yet seen signalled
33+}
34+
35+// deferredFree runs once frame completed.
36+type deferredFree struct {
37+ frame uint64
38+ free func()
39+}
40+
41+func (r *Renderer) createSlots() error {
42+ d := r.dd
43+ cmds := make([]vk.CommandBuffer, frameSlots)
44+ ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: frameSlots}
45+ if err := checked("vkAllocateCommandBuffers", d.AllocateCommandBuffers(r.device, &ai, &cmds[0])); err != nil {
46+ return err
47+ }
48+ for i := range r.slots {
49+ s := &r.slots[i]
50+ s.cmd = cmds[i]
51+ fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
52+ if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &s.fence)); err != nil {
53+ return err
54+ }
55+ si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
56+ export := vk.ExportSemaphoreCreateInfo{SType: vk.StructureTypeExportSemaphoreCreateInfo, HandleTypes: vk.ExternalSemaphoreHandleTypeSyncFDBit}
57+ if r.syncFD {
58+ si.Next = unsafe.Pointer(&export)
59+ }
60+ if err := checked("vkCreateSemaphore", d.CreateSemaphore(r.device, &si, nil, &s.done)); err != nil {
61+ return err
62+ }
63+ }
64+ return nil
65+}
66+
67+func (r *Renderer) destroySlots() {
68+ d := r.dd
69+ for i := range r.slots {
70+ s := &r.slots[i]
71+ if s.fence != 0 {
72+ d.DestroyFence(r.device, s.fence, nil)
73+ }
74+ if s.done != 0 {
75+ d.DestroySemaphore(r.device, s.done, nil)
76+ }
77+ if s.cmd != 0 {
78+ d.FreeCommandBuffers(r.device, r.pool, 1, &s.cmd)
79+ }
80+ *s = frameSlot{}
81+ }
82+}
83+
84+// waitFrame blocks until frame n completed. Frames not submitted yet and
85+// frames known done return at once. Every slot holding a frame up to n
86+// is waited and reset, so its fence is unsignalled before reuse.
87+func (r *Renderer) waitFrame(n uint64) error {
88+ n = min(n, r.submitted)
89+ if n <= r.completed {
90+ return nil
91+ }
92+ for i := range r.slots {
93+ s := &r.slots[i]
94+ if !s.busy || s.frame > n {
95+ continue
96+ }
97+ if err := checked("vkWaitForFences", r.dd.WaitForFences(r.device, 1, &s.fence, 1, math.MaxUint64)); err != nil {
98+ return err
99+ }
100+ if err := checked("vkResetFences", r.dd.ResetFences(r.device, 1, &s.fence)); err != nil {
101+ return err
102+ }
103+ s.busy = false
104+ }
105+ r.completed = n
106+ r.runDeferred()
107+ return nil
108+}
109+
110+// retire frees an object once the frame that last used it completed.
111+func (r *Renderer) retire(frame uint64, free func()) {
112+ if frame <= r.completed {
113+ free()
114+ return
115+ }
116+ r.deferred = append(r.deferred, deferredFree{frame: frame, free: free})
117+}
118+
119+func (r *Renderer) runDeferred() {
120+ r.deferred = slices.DeleteFunc(r.deferred, func(f deferredFree) bool {
121+ if f.frame <= r.completed {
122+ f.free()
123+ return true
124+ }
125+ return false
126+ })
127+}
128+
129+// idle records that the device finished every submitted frame.
130+func (r *Renderer) idle() {
131+ for i := range r.slots {
132+ s := &r.slots[i]
133+ if s.busy {
134+ _ = r.dd.ResetFences(r.device, 1, &s.fence)
135+ s.busy = false
136+ }
137+ }
138+ // Frames recorded but never submitted have no GPU work either.
139+ r.completed = r.frame
140+ r.runDeferred()
141+}
142+
143+// Render draws a frame into the current target. done is signalled when
144+// the GPU finished it; nil when the device cannot export a sync file,
145+// then the frame is finished on return.
146+func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) (done *os.File, err error) {
147+ r.frame++
148+ slot := &r.slots[r.frame%frameSlots]
149+ // The slot's previous frame must be done before its command buffer
150+ // is recorded again.
151+ if slot.busy {
152+ if err := r.waitFrame(slot.frame); err != nil {
153+ return nil, err
154+ }
155+ }
156+ ds := r.draws(s, contents)
157+ r.dropPools()
158+ d := r.dd
159+ cmd := slot.cmd
160+ if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(cmd, 0)); err != nil {
161+ return nil, err
162+ }
163+ begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
164+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil {
165+ return nil, err
166+ }
167+ tg := r.target()
168+ // The whole target is cleared by the pass: its old contents never
169+ // matter (UNDEFINED).
170+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: tg.image, SubresourceRange: colorRange}
171+ if tg.exported {
172+ // Acquire from the display (foreign queue family).
173+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
174+ }
175+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit|vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)
176+ // Client buffers come from the foreign queue family (their driver)
177+ // and go back to it after the frame, so the next frame acquires them.
178+ var dmas []*imported
179+ for _, dr := range ds {
180+ if dr.im != nil && !slices.Contains(dmas, dr.im) {
181+ dmas = append(dmas, dr.im)
182+ dr.im.last = r.frame
183+ }
184+ }
185+ r.ownership(cmd, dmas, true)
186+ r.recordDraws(cmd, tg.view, parseColor(s.Background), ds)
187+ r.ownership(cmd, dmas, false)
188+ b.SrcAccessMask, b.OldLayout = vk.AccessColorAttachmentWriteBit, vk.ImageLayoutColorAttachmentOptimal
189+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = queueFamilyIgnored, queueFamilyIgnored
190+ if tg.exported {
191+ // Release to the display: KMS scans it out after the fence.
192+ b.NewLayout, b.DstAccessMask = vk.ImageLayoutGeneral, 0
193+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
194+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
195+ } else {
196+ b.NewLayout, b.DstAccessMask = vk.ImageLayoutTransferSrcOptimal, vk.AccessTransferReadBit
197+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
198+ }
199+ if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
200+ return nil, err
201+ }
202+ // Implicit sync: wait for the client's GPU writes to each buffer.
203+ var waits []vk.Semaphore
204+ for _, im := range dmas {
205+ if sem := r.readFence(im); sem != 0 {
206+ waits = append(waits, sem)
207+ }
208+ }
209+ frame := r.frame
210+ for _, sem := range waits {
211+ r.retire(frame, func() { d.DestroySemaphore(r.device, sem, nil) })
212+ }
213+ submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &cmd}
214+ stages := make([]vk.PipelineStageFlags, len(waits))
215+ if len(waits) > 0 {
216+ for i := range stages {
217+ stages[i] = vk.PipelineStageFragmentShaderBit
218+ }
219+ submit.WaitSemaphoreCount, submit.WaitSemaphores, submit.WaitDstStageMask = uint32(len(waits)), &waits[0], &stages[0]
220+ }
221+ if r.syncFD {
222+ submit.SignalSemaphoreCount, submit.SignalSemaphores = 1, &slot.done
223+ }
224+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, slot.fence)); err != nil {
225+ return nil, err
226+ }
227+ runtime.KeepAlive(stages)
228+ runtime.KeepAlive(waits)
229+ slot.frame, slot.busy, r.submitted = frame, true, frame
230+ tg.layout = b.NewLayout
231+ r.last, r.readBack = tg, false
232+ r.dropUnused()
233+ r.dropShm()
234+ if !r.syncFD {
235+ return nil, r.waitFrame(frame)
236+ }
237+ var fd int32
238+ get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit}
239+ if err := checked("vkGetSemaphoreFdKHR", d.GetSemaphoreFdKHR(r.device, &get, &fd)); err != nil {
240+ return nil, errors.Join(err, r.waitFrame(frame))
241+ }
242+ if fd < 0 {
243+ // Already signalled: the driver may return -1.
244+ return nil, r.waitFrame(frame)
245+ }
246+ f := os.NewFile(uintptr(fd), "nefertty-frame")
247+ if f == nil {
248+ return nil, fmt.Errorf("invalid frame sync file %d", fd)
249+ }
250+ return f, nil
251+}
252+
253+// ownership moves client images between their driver (foreign queue
254+// family, GENERAL: their contents survive) and the fragment shader.
255+func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire bool) {
256+ for _, im := range dmas {
257+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: colorRange}
258+ if acquire {
259+ b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutShaderReadOnlyOptimal
260+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
261+ b.DstAccessMask = vk.AccessShaderReadBit
262+ r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)
263+ } else {
264+ b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral
265+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
266+ b.SrcAccessMask = vk.AccessShaderReadBit
267+ r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
268+ }
269+ }
270+}
271+
272+// oneShot records and runs commands outside the frame ring and waits for
273+// them (setup and screenshots only, never per frame).
274+func (r *Renderer) oneShot(record func(vk.CommandBuffer)) error {
275+ d := r.dd
276+ var cmd vk.CommandBuffer
277+ ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: 1}
278+ if err := checked("vkAllocateCommandBuffers", d.AllocateCommandBuffers(r.device, &ai, &cmd)); err != nil {
279+ return err
280+ }
281+ defer d.FreeCommandBuffers(r.device, r.pool, 1, &cmd)
282+ var fence vk.Fence
283+ fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
284+ if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &fence)); err != nil {
285+ return err
286+ }
287+ defer d.DestroyFence(r.device, fence, nil)
288+ begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
289+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil {
290+ return err
291+ }
292+ record(cmd)
293+ if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
294+ return err
295+ }
296+ submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &cmd}
297+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, fence)); err != nil {
298+ return err
299+ }
300+ return checked("vkWaitForFences", d.WaitForFences(r.device, 1, &fence, 1, math.MaxUint64))
301+}
302+
303+// Clear draws a background-only frame and waits for it.
304+func (r *Renderer) Clear(rgb [3]uint8) error {
305+ done, err := r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil)
306+ if done != nil {
307+ done.Close()
308+ }
309+ if err != nil {
310+ return err
311+ }
312+ return r.waitFrame(r.frame)
313+}
@@ -0,0 +1,313 @@
1+package vulkan
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "math"
7+ "os"
8+ "runtime"
9+ "slices"
10+ "unsafe"
11+
12+ "github.com/bnema/nefertty/internal/ports"
13+ vk "github.com/bnema/purego-vulkan/vulkan"
14+)
15+
16+// Frames are recorded into a ring of slots, one per frame in flight. The
17+// CPU never waits for a frame it just submitted: Render returns a sync
18+// file the display waits on (KMS IN_FENCE_FD). A slot's fence is waited
19+// only when the slot is reused, and objects a frame read (imports, GPU
20+// buffers, client semaphores) are freed only once that frame completed.
21+
22+// frameSlots is how many frames may be in flight: one per output target.
23+const frameSlots = 2
24+
25+// frameSlot is the command buffer and sync objects of one frame in flight.
26+type frameSlot struct {
27+ cmd vk.CommandBuffer
28+ fence vk.Fence
29+ // done is signalled with the frame and exported as its sync file.
30+ done vk.Semaphore
31+ frame uint64 // the frame recorded last, 0 before any
32+ busy bool // submitted, fence not yet seen signalled
33+}
34+
35+// deferredFree runs once frame completed.
36+type deferredFree struct {
37+ frame uint64
38+ free func()
39+}
40+
41+func (r *Renderer) createSlots() error {
42+ d := r.dd
43+ cmds := make([]vk.CommandBuffer, frameSlots)
44+ ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: frameSlots}
45+ if err := checked("vkAllocateCommandBuffers", d.AllocateCommandBuffers(r.device, &ai, &cmds[0])); err != nil {
46+ return err
47+ }
48+ for i := range r.slots {
49+ s := &r.slots[i]
50+ s.cmd = cmds[i]
51+ fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
52+ if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &s.fence)); err != nil {
53+ return err
54+ }
55+ si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
56+ export := vk.ExportSemaphoreCreateInfo{SType: vk.StructureTypeExportSemaphoreCreateInfo, HandleTypes: vk.ExternalSemaphoreHandleTypeSyncFDBit}
57+ if r.syncFD {
58+ si.Next = unsafe.Pointer(&export)
59+ }
60+ if err := checked("vkCreateSemaphore", d.CreateSemaphore(r.device, &si, nil, &s.done)); err != nil {
61+ return err
62+ }
63+ }
64+ return nil
65+}
66+
67+func (r *Renderer) destroySlots() {
68+ d := r.dd
69+ for i := range r.slots {
70+ s := &r.slots[i]
71+ if s.fence != 0 {
72+ d.DestroyFence(r.device, s.fence, nil)
73+ }
74+ if s.done != 0 {
75+ d.DestroySemaphore(r.device, s.done, nil)
76+ }
77+ if s.cmd != 0 {
78+ d.FreeCommandBuffers(r.device, r.pool, 1, &s.cmd)
79+ }
80+ *s = frameSlot{}
81+ }
82+}
83+
84+// waitFrame blocks until frame n completed. Frames not submitted yet and
85+// frames known done return at once. Every slot holding a frame up to n
86+// is waited and reset, so its fence is unsignalled before reuse.
87+func (r *Renderer) waitFrame(n uint64) error {
88+ n = min(n, r.submitted)
89+ if n <= r.completed {
90+ return nil
91+ }
92+ for i := range r.slots {
93+ s := &r.slots[i]
94+ if !s.busy || s.frame > n {
95+ continue
96+ }
97+ if err := checked("vkWaitForFences", r.dd.WaitForFences(r.device, 1, &s.fence, 1, math.MaxUint64)); err != nil {
98+ return err
99+ }
100+ if err := checked("vkResetFences", r.dd.ResetFences(r.device, 1, &s.fence)); err != nil {
101+ return err
102+ }
103+ s.busy = false
104+ }
105+ r.completed = n
106+ r.runDeferred()
107+ return nil
108+}
109+
110+// retire frees an object once the frame that last used it completed.
111+func (r *Renderer) retire(frame uint64, free func()) {
112+ if frame <= r.completed {
113+ free()
114+ return
115+ }
116+ r.deferred = append(r.deferred, deferredFree{frame: frame, free: free})
117+}
118+
119+func (r *Renderer) runDeferred() {
120+ r.deferred = slices.DeleteFunc(r.deferred, func(f deferredFree) bool {
121+ if f.frame <= r.completed {
122+ f.free()
123+ return true
124+ }
125+ return false
126+ })
127+}
128+
129+// idle records that the device finished every submitted frame.
130+func (r *Renderer) idle() {
131+ for i := range r.slots {
132+ s := &r.slots[i]
133+ if s.busy {
134+ _ = r.dd.ResetFences(r.device, 1, &s.fence)
135+ s.busy = false
136+ }
137+ }
138+ // Frames recorded but never submitted have no GPU work either.
139+ r.completed = r.frame
140+ r.runDeferred()
141+}
142+
143+// Render draws a frame into the current target. done is signalled when
144+// the GPU finished it; nil when the device cannot export a sync file,
145+// then the frame is finished on return.
146+func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) (done *os.File, err error) {
147+ r.frame++
148+ slot := &r.slots[r.frame%frameSlots]
149+ // The slot's previous frame must be done before its command buffer
150+ // is recorded again.
151+ if slot.busy {
152+ if err := r.waitFrame(slot.frame); err != nil {
153+ return nil, err
154+ }
155+ }
156+ ds := r.draws(s, contents)
157+ r.dropPools()
158+ d := r.dd
159+ cmd := slot.cmd
160+ if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(cmd, 0)); err != nil {
161+ return nil, err
162+ }
163+ begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
164+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil {
165+ return nil, err
166+ }
167+ tg := r.target()
168+ // The whole target is cleared by the pass: its old contents never
169+ // matter (UNDEFINED).
170+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: tg.image, SubresourceRange: colorRange}
171+ if tg.exported {
172+ // Acquire from the display (foreign queue family).
173+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
174+ }
175+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit|vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)
176+ // Client buffers come from the foreign queue family (their driver)
177+ // and go back to it after the frame, so the next frame acquires them.
178+ var dmas []*imported
179+ for _, dr := range ds {
180+ if dr.im != nil && !slices.Contains(dmas, dr.im) {
181+ dmas = append(dmas, dr.im)
182+ dr.im.last = r.frame
183+ }
184+ }
185+ r.ownership(cmd, dmas, true)
186+ r.recordDraws(cmd, tg.view, parseColor(s.Background), ds)
187+ r.ownership(cmd, dmas, false)
188+ b.SrcAccessMask, b.OldLayout = vk.AccessColorAttachmentWriteBit, vk.ImageLayoutColorAttachmentOptimal
189+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = queueFamilyIgnored, queueFamilyIgnored
190+ if tg.exported {
191+ // Release to the display: KMS scans it out after the fence.
192+ b.NewLayout, b.DstAccessMask = vk.ImageLayoutGeneral, 0
193+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
194+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
195+ } else {
196+ b.NewLayout, b.DstAccessMask = vk.ImageLayoutTransferSrcOptimal, vk.AccessTransferReadBit
197+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
198+ }
199+ if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
200+ return nil, err
201+ }
202+ // Implicit sync: wait for the client's GPU writes to each buffer.
203+ var waits []vk.Semaphore
204+ for _, im := range dmas {
205+ if sem := r.readFence(im); sem != 0 {
206+ waits = append(waits, sem)
207+ }
208+ }
209+ frame := r.frame
210+ for _, sem := range waits {
211+ r.retire(frame, func() { d.DestroySemaphore(r.device, sem, nil) })
212+ }
213+ submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &cmd}
214+ stages := make([]vk.PipelineStageFlags, len(waits))
215+ if len(waits) > 0 {
216+ for i := range stages {
217+ stages[i] = vk.PipelineStageFragmentShaderBit
218+ }
219+ submit.WaitSemaphoreCount, submit.WaitSemaphores, submit.WaitDstStageMask = uint32(len(waits)), &waits[0], &stages[0]
220+ }
221+ if r.syncFD {
222+ submit.SignalSemaphoreCount, submit.SignalSemaphores = 1, &slot.done
223+ }
224+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, slot.fence)); err != nil {
225+ return nil, err
226+ }
227+ runtime.KeepAlive(stages)
228+ runtime.KeepAlive(waits)
229+ slot.frame, slot.busy, r.submitted = frame, true, frame
230+ tg.layout = b.NewLayout
231+ r.last, r.readBack = tg, false
232+ r.dropUnused()
233+ r.dropShm()
234+ if !r.syncFD {
235+ return nil, r.waitFrame(frame)
236+ }
237+ var fd int32
238+ get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit}
239+ if err := checked("vkGetSemaphoreFdKHR", d.GetSemaphoreFdKHR(r.device, &get, &fd)); err != nil {
240+ return nil, errors.Join(err, r.waitFrame(frame))
241+ }
242+ if fd < 0 {
243+ // Already signalled: the driver may return -1.
244+ return nil, r.waitFrame(frame)
245+ }
246+ f := os.NewFile(uintptr(fd), "nefertty-frame")
247+ if f == nil {
248+ return nil, fmt.Errorf("invalid frame sync file %d", fd)
249+ }
250+ return f, nil
251+}
252+
253+// ownership moves client images between their driver (foreign queue
254+// family, GENERAL: their contents survive) and the fragment shader.
255+func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire bool) {
256+ for _, im := range dmas {
257+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: colorRange}
258+ if acquire {
259+ b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutShaderReadOnlyOptimal
260+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
261+ b.DstAccessMask = vk.AccessShaderReadBit
262+ r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)
263+ } else {
264+ b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral
265+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
266+ b.SrcAccessMask = vk.AccessShaderReadBit
267+ r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
268+ }
269+ }
270+}
271+
272+// oneShot records and runs commands outside the frame ring and waits for
273+// them (setup and screenshots only, never per frame).
274+func (r *Renderer) oneShot(record func(vk.CommandBuffer)) error {
275+ d := r.dd
276+ var cmd vk.CommandBuffer
277+ ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: 1}
278+ if err := checked("vkAllocateCommandBuffers", d.AllocateCommandBuffers(r.device, &ai, &cmd)); err != nil {
279+ return err
280+ }
281+ defer d.FreeCommandBuffers(r.device, r.pool, 1, &cmd)
282+ var fence vk.Fence
283+ fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
284+ if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &fence)); err != nil {
285+ return err
286+ }
287+ defer d.DestroyFence(r.device, fence, nil)
288+ begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
289+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil {
290+ return err
291+ }
292+ record(cmd)
293+ if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
294+ return err
295+ }
296+ submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &cmd}
297+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, fence)); err != nil {
298+ return err
299+ }
300+ return checked("vkWaitForFences", d.WaitForFences(r.device, 1, &fence, 1, math.MaxUint64))
301+}
302+
303+// Clear draws a background-only frame and waits for it.
304+func (r *Renderer) Clear(rgb [3]uint8) error {
305+ done, err := r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil)
306+ if done != nil {
307+ done.Close()
308+ }
309+ if err != nil {
310+ return err
311+ }
312+ return r.waitFrame(r.frame)
313+}
M internal/adapters/vulkan/renderer.go +96 -460
@@ -6,7 +6,6 @@ import (
66 "math"
77 "os"
88 "runtime"
9- "slices"
109 "strconv"
1110 "unsafe"
1211
@@ -31,43 +30,41 @@ type Renderer struct {
3130 renderMods []uint64
3231 // last is the target of the last frame; readback copies it into
3332 // buffer only when Pixels asks (stale until then).
34- last *target
35- readBack bool
36- buffer vk.Buffer
37- bufferMemory vk.DeviceMemory
38- staging vk.Buffer
39- stagingMemory vk.DeviceMemory
40- stagingMapped unsafe.Pointer
41- stagingSize int
42- mapped unsafe.Pointer
43- pool vk.CommandPool
44- command vk.CommandBuffer
45- fence vk.Fence
46- memory vk.PhysicalDeviceMemoryProperties
47- family uint32
33+ last *target
34+ readBack bool
35+ buffer vk.Buffer
36+ bufferMemory vk.DeviceMemory
37+ mapped unsafe.Pointer
38+ pool vk.CommandPool
39+ memory vk.PhysicalDeviceMemoryProperties
40+ family uint32
41+ // Frames in flight (frame.go): frame is the last frame recorded,
42+ // submitted the last submitted, completed the last known finished.
43+ slots [frameSlots]frameSlot
44+ frame, submitted, completed uint64
45+ deferred []deferredFree
46+ // syncFD: frames export a sync file (else Render waits for them).
47+ syncFD bool
4848 // dmabuf is what clients may send; imports are their buffers by ID.
4949 dmabuf ports.DMABufSupport
5050 imports map[uint64]*imported
5151 // pools are client wl_shm pools mapped by pool ID.
5252 pools map[uint64]*mapping
53- // retired are pool mappings replaced this frame, unmapped after staging.
53+ // retired are pool mappings replaced this frame, unmapped once the
54+ // frame's copies are done.
5455 retired [][]byte
55- frame uint64
56- // fillRegions is reused by Render for the bands of solid fills.
57- fillRegions []vk.BufferImageCopy
56+ // shm are the GPU copies of wl_shm surfaces (shm.go); copied counts
57+ // the bytes copied into them, for tests.
58+ shm map[shmKey]*shmSurface
59+ copied int
5860 // cursors are the exported cursor images (CursorBuffers).
5961 cursors [2]*cursorImage
60- // blend draws translucent client pixels (blend.go); align is the
61- // device's storage buffer offset alignment.
62- blend blendPipeline
63- align int
64- maxRange int // bytes of blended pixels one frame may bind
62+ // compose draws every frame (compose.go); maxRange bounds one
63+ // storage buffer binding.
64+ compose composer
65+ maxRange int
6566 }
6667
67-// fillRows is the height of the strip staged for a solid fill: the CPU
68-// writes it once, the GPU copies it down the rect.
69-const fillRows = 64
70-
7168 func checked(name string, result vk.Result) error {
7269 if err := vk.Check(result); err != nil {
7370 return fmt.Errorf("%s: %w", name, err)
@@ -79,7 +76,7 @@ func New(width, height int) (r *Renderer, err error) {
7976 if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {
8077 return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)
8178 }
82- r = &Renderer{width: width, height: height, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}}
79+ r = &Renderer{width: width, height: height, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}, shm: map[shmKey]*shmSurface{}}
8380 defer func() {
8481 if err != nil {
8582 r.Close()
@@ -155,7 +152,6 @@ func New(width, height int) (r *Renderer, err error) {
155152 r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory)
156153 var properties vk.PhysicalDeviceProperties
157154 r.id.GetPhysicalDeviceProperties(physical, &properties)
158- r.align = int(max(properties.Limits.MinStorageBufferOffsetAlignment, 4))
159155 r.maxRange = int(properties.Limits.MaxStorageBufferRange)
160156 r.family = family
161157 priority := float32(1)
@@ -189,6 +185,9 @@ func New(width, height int) (r *Renderer, err error) {
189185 r.dmabuf = sup
190186 r.probeRenderModifiers(physical)
191187 }
188+ // VK_KHR_external_semaphore_fd (enabled above): frames export
189+ // their fence as a sync file.
190+ r.syncFD = r.dd.GetSemaphoreFdKHR != nil
192191 }
193192 r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)
194193 extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}
@@ -216,7 +215,7 @@ func New(width, height int) (r *Renderer, err error) {
216215 return
217216 }
218217 r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req)
219- // Readback (screenshots, fallback) reads this buffer: uncached memory is ~100x slower.
218+ // Readback (screenshots) reads this buffer: uncached memory is ~100x slower.
220219 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit|vk.MemoryPropertyHostCachedBit)
221220 if err != nil {
222221 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
@@ -234,44 +233,17 @@ func New(width, height int) (r *Renderer, err error) {
234233 if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil {
235234 return
236235 }
237- // Start with two frame-sized regions; later frames grow staging on demand.
238- stagingSize := size * 2
239- r.stagingSize = int(stagingSize)
240- stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: stagingUsage, SharingMode: vk.SharingModeExclusive}
241- if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil {
242- return
243- }
244- r.dd.GetBufferMemoryRequirements(r.device, r.staging, &req)
245- kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
246- if err != nil {
247- return
248- }
249- alloc = vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
250- if err = checked("vkAllocateMemory(staging)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.stagingMemory)); err != nil {
251- return
252- }
253- if err = checked("vkBindBufferMemory(staging)", r.dd.BindBufferMemory(r.device, r.staging, r.stagingMemory, 0)); err != nil {
254- return
255- }
256- if err = checked("vkMapMemory(staging)", r.dd.MapMemory(r.device, r.stagingMemory, 0, stagingSize, 0, &r.stagingMapped)); err != nil {
257- return
258- }
259236 pi := vk.CommandPoolCreateInfo{SType: vk.StructureTypeCommandPoolCreateInfo, Flags: vk.CommandPoolCreateResetCommandBufferBit, QueueFamilyIndex: family}
260237 if err = checked("vkCreateCommandPool", r.dd.CreateCommandPool(r.device, &pi, nil, &r.pool)); err != nil {
261238 return
262239 }
263- ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: 1}
264- if err = checked("vkAllocateCommandBuffers", r.dd.AllocateCommandBuffers(r.device, &ai, &r.command)); err != nil {
240+ if err = r.createSlots(); err != nil {
265241 return
266242 }
267243 if err = r.createView(&r.own); err != nil {
268244 return
269245 }
270- if err = r.createBlend(); err != nil {
271- return
272- }
273- fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
274- if err = checked("vkCreateFence", r.dd.CreateFence(r.device, &fi, nil, &r.fence)); err != nil {
246+ if err = r.createComposer(); err != nil {
275247 return
276248 }
277249 runtime.KeepAlive(priority)
@@ -325,116 +297,9 @@ func windowColor(id ports.WindowID) [3]uint8 {
325297 return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}
326298 }
327299
328-// ensureStaging grows the mapped transfer buffer before recording any commands.
329-func (r *Renderer) ensureStaging(size int) error {
330- if size <= r.stagingSize {
331- return nil
332- }
333- d := r.dd
334- capacity := size
335- info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: stagingUsage, SharingMode: vk.SharingModeExclusive}
336- var buffer vk.Buffer
337- if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil {
338- return err
339- }
340- var memory vk.DeviceMemory
341- defer func() {
342- if buffer != 0 {
343- d.DestroyBuffer(r.device, buffer, nil)
344- }
345- if memory != 0 {
346- d.FreeMemory(r.device, memory, nil)
347- }
348- }()
349- var req vk.MemoryRequirements
350- d.GetBufferMemoryRequirements(r.device, buffer, &req)
351- kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
352- if err != nil {
353- return err
354- }
355- alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
356- if err := checked("vkAllocateMemory(staging)", d.AllocateMemory(r.device, &alloc, nil, &memory)); err != nil {
357- return err
358- }
359- if err := checked("vkBindBufferMemory(staging)", d.BindBufferMemory(r.device, buffer, memory, 0)); err != nil {
360- return err
361- }
362- var mapped unsafe.Pointer
363- if err := checked("vkMapMemory(staging)", d.MapMemory(r.device, memory, 0, vk.DeviceSize(capacity), 0, &mapped)); err != nil {
364- return err
365- }
366- d.UnmapMemory(r.device, r.stagingMemory)
367- d.DestroyBuffer(r.device, r.staging, nil)
368- d.FreeMemory(r.device, r.stagingMemory, nil)
369- r.staging, r.stagingMemory, r.stagingMapped, r.stagingSize = buffer, memory, mapped, capacity
370- buffer, memory = 0, 0
371- return nil
372-}
373-
374-func (r *Renderer) Clear(rgb [3]uint8) error {
375- return r.render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil)
376-}
377-
378-// upload is one piece of a frame: a solid fill, client pixels staged from
379-// a wl_shm pool, or a dmabuf blitted in place.
380-type upload struct {
381- rect image.Rectangle
382- offset int
383- pixels []byte
384- stride int
385- // dst covers src; they differ in size when the buffer is not drawn at
386- // the physical size (integer-scale clients, rounding).
387- dst, src image.Rectangle
388- color [3]uint8
389- opaque bool
390- // blend draws the staged pixels over the target instead of copying.
391- blend bool
392- // dma is a client GPU buffer, blitted instead of copied from staging.
393- dma *imported
394- // fill is a solid color: staging holds one strip of rows.
395- fill bool
396-}
397-
398-// size is the staging bytes of an upload: a strip for a fill.
399-func (u *upload) size() int {
400- if u.fill {
401- return u.rect.Dx() * min(u.rect.Dy(), fillRows) * 4
402- }
403- return u.rect.Dx() * u.rect.Dy() * 4
404-}
405-
406-// stage copies the visible pixels of a client buffer into dst, resampling
407-// when the buffer is not drawn at its size.
408-func (u *upload) stage(dst []byte) {
409- for row := 0; row < u.rect.Dy(); row++ {
410- line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]
411- y := u.rect.Min.Y + row
412- if u.dst.Size() == u.src.Size() {
413- start := (u.src.Min.Y+y-u.dst.Min.Y)*u.stride + (u.src.Min.X+u.rect.Min.X-u.dst.Min.X)*4
414- copy(line, u.pixels[start:start+len(line)])
415- } else {
416- // Nearest-neighbour resample, sampling pixel centres.
417- sy := u.src.Min.Y + ((y-u.dst.Min.Y)*2+1)*u.src.Dy()/(u.dst.Dy()*2)
418- src := u.pixels[sy*u.stride:]
419- for i := 0; i < u.rect.Dx(); i++ {
420- sx := u.src.Min.X + ((u.rect.Min.X+i-u.dst.Min.X)*2+1)*u.src.Dx()/(u.dst.Dx()*2)
421- copy(line[i*4:i*4+4], src[sx*4:sx*4+4])
422- }
423- }
424- if u.opaque {
425- for i := 3; i < len(line); i += 4 {
426- line[i] = 255
427- }
428- }
429- }
430-}
431-
432-// Render draws a frame. It still waits for the GPU; done is nil.
433-func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) (done *os.File, err error) {
434- return nil, r.render(s, contents)
435-}
436-
437-func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
300+// draws walks the scene into quads in paint order, copying new wl_shm
301+// content into GPU buffers on the way.
302+func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) []draw {
438303 // Scene rects are logical; everything below works in physical pixels.
439304 scale := s.Scale
440305 if scale <= 0 {
@@ -442,21 +307,20 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
442307 }
443308 phys := func(v int) int { return int(math.Round(float64(v) * scale)) }
444309 physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) }
445- var uploads []upload
310+ var draws []draw
446311 bounds := image.Rect(0, 0, r.width, r.height)
447312 add := func(rect image.Rectangle, c [3]uint8) {
448313 rect = rect.Intersect(bounds)
449314 if rect.Empty() {
450315 return
451316 }
452- // A fill stages a strip of up to fillRows rows; the GPU copies the
453- // strip down the rect.
454- uploads = append(uploads, upload{rect: rect, color: c, fill: true})
317+ draws = append(draws, r.fillDraw(rect, c))
455318 }
456- // addContent maps src (buffer pixels) onto dst (physical pixels).
457- addContent := func(dst, src image.Rectangle, content *ports.SurfaceContent) {
319+ // addContent draws the part of a buffer mapped onto full (physical
320+ // pixels) that dst shows. key names the surface, seq its content.
321+ addContent := func(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64) {
458322 rect := dst.Intersect(bounds)
459- if rect.Empty() || src.Empty() {
323+ if rect.Empty() {
460324 return
461325 }
462326 if content.DMABuf != nil {
@@ -465,7 +329,9 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
465329 // The window shows its background until a buffer imports.
466330 return
467331 }
468- uploads = append(uploads, upload{rect: rect, dst: dst, src: src, dma: im})
332+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)
333+ dr.set, dr.im = im.set, im
334+ draws = append(draws, dr)
469335 return
470336 }
471337 b := content.SHM
@@ -473,11 +339,19 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
473339 if err != nil {
474340 return
475341 }
476- uploads = append(uploads, upload{rect: rect, pixels: pixels[b.Offset:], stride: b.Stride, opaque: content.Opaque, blend: !content.Opaque, dst: dst, src: src})
342+ st := shmState{pool: b.Pool, offset: b.Offset, stride: b.Stride, w: content.Width, h: content.Height, seq: seq}
343+ c, err := r.shmCopyFor(key, st, pixels)
344+ if err != nil {
345+ return
346+ }
347+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeBuffer, content.Opaque)
348+ dr.set = c.set
349+ dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
350+ draws = append(draws, dr)
477351 }
478352 // drawSurface draws one surface buffer with its origin at (x, y)
479353 // logical, clipped to clip (logical).
480- drawSurface := func(content *ports.SurfaceContent, x, y int, clip image.Rectangle) {
354+ drawSurface := func(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64) {
481355 lw, lh := content.LogicalW, content.LogicalH
482356 if lw <= 0 || lh <= 0 {
483357 lw, lh = content.Width, content.Height
@@ -499,26 +373,24 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
499373 if dst.Empty() {
500374 return
501375 }
502- // The visible part of dst in buffer pixels.
503- bx := func(v int) int { return (v - full.Min.X) * content.Width / full.Dx() }
504- by := func(v int) int { return (v - full.Min.Y) * content.Height / full.Dy() }
505- src := image.Rect(bx(dst.Min.X), by(dst.Min.Y), bx(dst.Max.X), by(dst.Max.Y))
506- addContent(dst, src, content)
376+ addContent(dst, full, content, key, seq)
507377 }
508378 // place draws a surface tree with its window geometry at (x, y)
509379 // logical, clipped to w×h logical: client shadows fall outside.
510- place := func(content *ports.SurfaceContent, x, y, w, h int) {
380+ // Every surface of the tree is keyed by its window and place in it;
381+ // the tree's Seq changes with any of them.
382+ place := func(id ports.WindowID, content *ports.SurfaceContent, x, y, w, h int) {
511383 clip := image.Rect(x, y, x+w, y+h)
512384 ox, oy := x-content.Geometry.X, y-content.Geometry.Y
513385 for i := range content.Children {
514386 if ch := &content.Children[i]; ch.Below {
515- drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)
387+ drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq)
516388 }
517389 }
518- drawSurface(content, ox, oy, clip)
390+ drawSurface(content, ox, oy, clip, shmKey{id, 0}, content.Seq)
519391 for i := range content.Children {
520392 if ch := &content.Children[i]; !ch.Below {
521- drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)
393+ drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq)
522394 }
523395 }
524396 }
@@ -534,7 +406,7 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
534406 continue
535407 }
536408 content := contents[layer.ID]
537- place(&content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)
409+ place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)
538410 }
539411 }
540412 addLayers(false)
@@ -558,7 +430,7 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
558430 add(body, windowColor(w.ID))
559431 } else {
560432 add(body, parseColor(s.Background))
561- place(&content, cx, cy, cw, ch)
433+ place(w.ID, &content, cx, cy, cw, ch)
562434 }
563435 borderColor := s.Border.Inactive
564436 if w.Focused {
@@ -583,225 +455,14 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
583455 for _, w := range s.Windows {
584456 if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 {
585457 content := contents[w.ID]
586- place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)
458+ place(w.ID, &content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)
587459 }
588460 }
589461 }
590462 addPopups(false)
591463 addLayers(true)
592464 addPopups(true)
593- // Blended pixels are staged first, so the shaders bind only them,
594- // within the device's storage buffer range; past it they are copied.
595- // The draw table follows them, then the other uploads.
596- blends, used := 0, 0
597- for k := range uploads {
598- u := &uploads[k]
599- if u.blend && used+u.size() > r.maxRange {
600- u.blend = false
601- }
602- if u.blend {
603- u.offset = used
604- used += u.size()
605- blends++
606- }
607- }
608- pixelBytes := used
609- tableOffset := (used + r.align - 1) / r.align * r.align
610- used = tableOffset + blends*drawSize
611- for k := range uploads {
612- if u := &uploads[k]; !u.blend && u.dma == nil {
613- u.offset = used
614- used += u.size()
615- }
616- }
617- if err := r.ensureStaging(used); err != nil {
618- return err
619- }
620- data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize)
621- if blends > 0 {
622- r.bindStaging(pixelBytes, tableOffset, blends)
623- i := 0
624- for k := range uploads {
625- if uploads[k].blend {
626- r.putDraw(data[tableOffset:], i, &uploads[k])
627- i++
628- }
629- }
630- }
631- for _, u := range uploads {
632- if u.dma != nil {
633- continue
634- }
635- if u.fill {
636- line := data[u.offset : u.offset+u.rect.Dx()*min(u.rect.Dy(), fillRows)*4]
637- for i := 0; i < len(line); i += 4 {
638- line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255
639- }
640- continue
641- }
642- dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]
643- // The client may shrink its pool under the mapping: its window
644- // then shows garbage for this frame, never a crash.
645- copyGuarded(func() { u.stage(dst) })
646- }
647- rgb := parseColor(s.Background)
648- d := r.dd
649- if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
650- return err
651- }
652- begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo}
653- if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil {
654- return err
655- }
656- rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
657- tg := r.target()
658- srcStage := vk.PipelineStageFlags(vk.PipelineStageTopOfPipeBit)
659- srcAccess := vk.AccessFlags(0)
660- if tg.layout == vk.ImageLayoutTransferSrcOptimal {
661- srcStage = vk.PipelineStageTransferBit
662- srcAccess = vk.AccessTransferReadBit
663- }
664- // The whole target is cleared: its old contents never matter.
665- barrier := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: srcAccess, DstAccessMask: vk.AccessTransferWriteBit, OldLayout: tg.layout, NewLayout: vk.ImageLayoutTransferDstOptimal, Image: tg.image, SubresourceRange: rangeInfo}
666- if tg.exported {
667- // Acquire from the display (foreign queue family).
668- barrier.OldLayout = vk.ImageLayoutUndefined
669- barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
670- srcStage, barrier.SrcAccessMask = vk.PipelineStageTopOfPipeBit, 0
671- }
672- d.CmdPipelineBarrier(r.command, srcStage, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
673- barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = 0, 0
674- // VkClearColorValue is a union; UNORM formats read it as float32.
675- unorm := func(v uint8) uint32 { return math.Float32bits(float32(v) / 255) }
676- color := vk.ClearColorValue{unorm(rgb[0]), unorm(rgb[1]), unorm(rgb[2]), math.Float32bits(1)}
677- d.CmdClearColorImage(r.command, tg.image, vk.ImageLayoutTransferDstOptimal, &color, 1, &rangeInfo)
678- barrier.SrcAccessMask = vk.AccessTransferWriteBit
679- barrier.DstAccessMask = vk.AccessTransferWriteBit
680- barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
681- barrier.NewLayout = vk.ImageLayoutTransferDstOptimal
682- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
683- // Client buffers come from the foreign queue family (their driver) and
684- // go back to it after the frame, so the next frame acquires them again.
685- var dmas []*imported
686- for _, u := range uploads {
687- if u.dma != nil && !slices.Contains(dmas, u.dma) {
688- dmas = append(dmas, u.dma)
689- }
690- }
691- ownership := func(acquire bool) {
692- for _, im := range dmas {
693- b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: rangeInfo}
694- if acquire {
695- // GENERAL, never UNDEFINED: the client's contents must survive.
696- b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutTransferSrcOptimal
697- b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
698- b.DstAccessMask = vk.AccessTransferReadBit
699- } else {
700- b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
701- b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
702- b.SrcAccessMask = vk.AccessTransferReadBit
703- }
704- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
705- }
706- }
707- ownership(true)
708- drawn := 0 // blended uploads drawn so far: their index in the draw table
709- for i := 0; i < len(uploads); i++ {
710- f := uploads[i]
711- if i > 0 && !f.blend {
712- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
713- }
714- if f.blend {
715- // Consecutive blended uploads share one draw: blending follows
716- // primitive order within it.
717- n := 1
718- for i+n < len(uploads) && uploads[i+n].blend {
719- n++
720- }
721- r.drawBlended(tg.image, tg.view, drawn, drawn+n)
722- drawn += n
723- i += n - 1
724- continue
725- }
726- rect := f.rect
727- if f.dma != nil {
728- // Map the visible part of dst back onto the buffer.
729- sx := func(x int) int32 { return int32(f.src.Min.X + (x-f.dst.Min.X)*f.src.Dx()/f.dst.Dx()) }
730- sy := func(y int) int32 { return int32(f.src.Min.Y + (y-f.dst.Min.Y)*f.src.Dy()/f.dst.Dy()) }
731- layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}
732- blit := vk.ImageBlit{
733- SrcSubresource: layers,
734- SrcOffsets: [2]vk.Offset3D{{X: sx(rect.Min.X), Y: sy(rect.Min.Y)}, {X: sx(rect.Max.X), Y: sy(rect.Max.Y), Z: 1}},
735- DstSubresource: layers,
736- DstOffsets: [2]vk.Offset3D{{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, {X: int32(rect.Max.X), Y: int32(rect.Max.Y), Z: 1}},
737- }
738- d.CmdBlitImage(r.command, f.dma.image, vk.ImageLayoutTransferSrcOptimal, tg.image, vk.ImageLayoutTransferDstOptimal, 1, &blit, vk.FilterNearest)
739- continue
740- }
741- layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}
742- if f.fill {
743- // Every band reads the same staged strip.
744- r.fillRegions = r.fillRegions[:0]
745- for y := rect.Min.Y; y < rect.Max.Y; y += fillRows {
746- h := min(fillRows, rect.Max.Y-y)
747- r.fillRegions = append(r.fillRegions, vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(h), Depth: 1}})
748- }
749- d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, uint32(len(r.fillRegions)), &r.fillRegions[0])
750- continue
751- }
752- region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}
753- d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, 1, &region)
754- }
755- ownership(false)
756- barrier.SrcAccessMask = vk.AccessTransferWriteBit
757- barrier.DstAccessMask = vk.AccessTransferReadBit
758- barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
759- barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal
760- if tg.exported {
761- // Release to the display: KMS scans it out after the fence.
762- barrier.NewLayout = vk.ImageLayoutGeneral
763- barrier.DstAccessMask = 0
764- barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
765- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &barrier)
766- } else {
767- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
768- }
769- if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
770- return err
771- }
772- // Implicit sync: wait for the client's GPU writes to each buffer.
773- var waits []vk.Semaphore
774- for _, im := range dmas {
775- if sem := r.readFence(im); sem != 0 {
776- waits = append(waits, sem)
777- }
778- }
779- defer func() {
780- for _, sem := range waits {
781- d.DestroySemaphore(r.device, sem, nil)
782- }
783- }()
784- submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
785- stages := make([]vk.PipelineStageFlags, len(waits))
786- if len(waits) > 0 {
787- for i := range stages {
788- stages[i] = vk.PipelineStageTransferBit
789- }
790- submit.WaitSemaphoreCount, submit.WaitSemaphores, submit.WaitDstStageMask = uint32(len(waits)), &waits[0], &stages[0]
791- }
792- if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
793- return err
794- }
795- runtime.KeepAlive(stages)
796- tg.layout = barrier.NewLayout
797- r.last, r.readBack = tg, false
798- // The fence wait is the GPU completion ADR 014 requires before a flip.
799- if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
800- return err
801- }
802- r.dropUnused()
803- r.dropPools()
804- return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
465+ return draws
805466 }
806467
807468 func (r *Renderer) Pixels() *image.RGBA {
@@ -823,52 +484,44 @@ const (
823484 wholeSize = ^vk.DeviceSize(0)
824485 )
825486
826-// readback copies the last frame into the host buffer, once per frame.
487+// readback copies the last frame into the host buffer, once per frame
488+// (screenshots only).
827489 func (r *Renderer) readback() error {
828490 if r.readBack || r.last == nil || r.last.image == 0 {
829491 return nil
830492 }
831- d := r.dd
832- if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
833- return err
834- }
835- begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo}
836- if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil {
493+ // The copy reads the frame: wait for it, and keep the ring ordered.
494+ if err := r.waitFrame(r.submitted); err != nil {
837495 return err
838496 }
839497 t := r.last
840- rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
841- b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessTransferReadBit, OldLayout: t.layout, NewLayout: vk.ImageLayoutTransferSrcOptimal, Image: t.image, SubresourceRange: rangeInfo}
842- if t.exported {
843- b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
844- b.SrcAccessMask = 0
845- }
846- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
847- region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
848- d.CmdCopyImageToBuffer(r.command, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &region)
849- // Make the copy visible to host reads of the mapped buffer.
850- hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: r.buffer, Size: wholeSize}
851- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil)
852- if t.exported {
853- b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
854- b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0
855- b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
856- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
857- } else {
858- t.layout = vk.ImageLayoutTransferSrcOptimal
859- }
860- if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
861- return err
862- }
863- submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
864- if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
498+ err := r.oneShot(func(cmd vk.CommandBuffer) {
499+ d := r.dd
500+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessTransferReadBit, OldLayout: t.layout, NewLayout: vk.ImageLayoutTransferSrcOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: t.image, SubresourceRange: colorRange}
501+ if t.exported {
502+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
503+ }
504+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
505+ region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
506+ d.CmdCopyImageToBuffer(cmd, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &region)
507+ // Make the copy visible to host reads of the mapped buffer.
508+ hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: r.buffer, Size: wholeSize}
509+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil)
510+ if t.exported {
511+ b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
512+ b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0
513+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
514+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
515+ }
516+ })
517+ if err != nil {
865518 return err
866519 }
867- if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
868- return err
520+ if !t.exported {
521+ t.layout = vk.ImageLayoutTransferSrcOptimal
869522 }
870523 r.readBack = true
871- return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
524+ return nil
872525 }
873526
874527 func (r *Renderer) Close() {
@@ -879,6 +532,7 @@ func (r *Renderer) Close() {
879532 d := r.dd
880533 if r.device != 0 {
881534 _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))
535+ r.idle()
882536 }
883537 r.dropTargets()
884538 r.dropCursors()
@@ -888,30 +542,12 @@ func (r *Renderer) Close() {
888542 }
889543 r.frame += importTTL + 1
890544 r.dropPools()
891- if r.fence != 0 {
892- d.DestroyFence(r.device, r.fence, nil)
893- r.fence = 0
894- }
895- if r.command != 0 {
896- d.FreeCommandBuffers(r.device, r.pool, 1, &r.command)
897- r.command = 0
898- }
545+ r.dropShm()
546+ r.destroySlots()
899547 if r.pool != 0 {
900548 d.DestroyCommandPool(r.device, r.pool, nil)
901549 r.pool = 0
902550 }
903- if r.stagingMapped != nil {
904- d.UnmapMemory(r.device, r.stagingMemory)
905- r.stagingMapped = nil
906- }
907- if r.staging != 0 {
908- d.DestroyBuffer(r.device, r.staging, nil)
909- r.staging = 0
910- }
911- if r.stagingMemory != 0 {
912- d.FreeMemory(r.device, r.stagingMemory, nil)
913- r.stagingMemory = 0
914- }
915551 if r.mapped != nil {
916552 d.UnmapMemory(r.device, r.bufferMemory)
917553 r.mapped = nil
@@ -924,7 +560,7 @@ func (r *Renderer) Close() {
924560 d.FreeMemory(r.device, r.bufferMemory, nil)
925561 r.bufferMemory = 0
926562 }
927- r.destroyBlend()
563+ r.destroyComposer()
928564 r.freeTarget(&r.own)
929565 d.DestroyDevice(r.device, nil)
930566 r.device = 0
@@ -6,7 +6,6 @@ import (
6 "math"6 "math"
7 "os"7 "os"
8 "runtime"8 "runtime"
9- "slices"
10 "strconv"9 "strconv"
11 "unsafe"10 "unsafe"
12 11
@@ -31,43 +30,41 @@ type Renderer struct {
31 renderMods []uint6430 renderMods []uint64
32 // last is the target of the last frame; readback copies it into31 // last is the target of the last frame; readback copies it into
33 // buffer only when Pixels asks (stale until then).32 // buffer only when Pixels asks (stale until then).
34- last *target33+ last *target
35- readBack bool34+ readBack bool
36- buffer vk.Buffer35+ buffer vk.Buffer
37- bufferMemory vk.DeviceMemory36+ bufferMemory vk.DeviceMemory
38- staging vk.Buffer37+ mapped unsafe.Pointer
39- stagingMemory vk.DeviceMemory38+ pool vk.CommandPool
40- stagingMapped unsafe.Pointer39+ memory vk.PhysicalDeviceMemoryProperties
41- stagingSize int40+ family uint32
42- mapped unsafe.Pointer41+ // Frames in flight (frame.go): frame is the last frame recorded,
43- pool vk.CommandPool42+ // submitted the last submitted, completed the last known finished.
44- command vk.CommandBuffer43+ slots [frameSlots]frameSlot
45- fence vk.Fence44+ frame, submitted, completed uint64
46- memory vk.PhysicalDeviceMemoryProperties45+ deferred []deferredFree
47- family uint3246+ // syncFD: frames export a sync file (else Render waits for them).
47+ syncFD bool
48 // dmabuf is what clients may send; imports are their buffers by ID.48 // dmabuf is what clients may send; imports are their buffers by ID.
49 dmabuf ports.DMABufSupport49 dmabuf ports.DMABufSupport
50 imports map[uint64]*imported50 imports map[uint64]*imported
51 // pools are client wl_shm pools mapped by pool ID.51 // pools are client wl_shm pools mapped by pool ID.
52 pools map[uint64]*mapping52 pools map[uint64]*mapping
53- // retired are pool mappings replaced this frame, unmapped after staging.53+ // retired are pool mappings replaced this frame, unmapped once the
54+ // frame's copies are done.
54 retired [][]byte55 retired [][]byte
55- frame uint6456+ // shm are the GPU copies of wl_shm surfaces (shm.go); copied counts
56- // fillRegions is reused by Render for the bands of solid fills.57+ // the bytes copied into them, for tests.
57- fillRegions []vk.BufferImageCopy58+ shm map[shmKey]*shmSurface
59+ copied int
58 // cursors are the exported cursor images (CursorBuffers).60 // cursors are the exported cursor images (CursorBuffers).
59 cursors [2]*cursorImage61 cursors [2]*cursorImage
60- // blend draws translucent client pixels (blend.go); align is the62+ // compose draws every frame (compose.go); maxRange bounds one
61- // device's storage buffer offset alignment.63+ // storage buffer binding.
62- blend blendPipeline64+ compose composer
63- align int65+ maxRange int
64- maxRange int // bytes of blended pixels one frame may bind
65 }66 }
66 67
67-// fillRows is the height of the strip staged for a solid fill: the CPU
68-// writes it once, the GPU copies it down the rect.
69-const fillRows = 64
70-
71 func checked(name string, result vk.Result) error {68 func checked(name string, result vk.Result) error {
72 if err := vk.Check(result); err != nil {69 if err := vk.Check(result); err != nil {
73 return fmt.Errorf("%s: %w", name, err)70 return fmt.Errorf("%s: %w", name, err)
@@ -79,7 +76,7 @@ func New(width, height int) (r *Renderer, err error) {
79 if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {76 if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {
80 return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)77 return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)
81 }78 }
82- r = &Renderer{width: width, height: height, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}}79+ r = &Renderer{width: width, height: height, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}, shm: map[shmKey]*shmSurface{}}
83 defer func() {80 defer func() {
84 if err != nil {81 if err != nil {
85 r.Close()82 r.Close()
@@ -155,7 +152,6 @@ func New(width, height int) (r *Renderer, err error) {
155 r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory)152 r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory)
156 var properties vk.PhysicalDeviceProperties153 var properties vk.PhysicalDeviceProperties
157 r.id.GetPhysicalDeviceProperties(physical, &properties)154 r.id.GetPhysicalDeviceProperties(physical, &properties)
158- r.align = int(max(properties.Limits.MinStorageBufferOffsetAlignment, 4))
159 r.maxRange = int(properties.Limits.MaxStorageBufferRange)155 r.maxRange = int(properties.Limits.MaxStorageBufferRange)
160 r.family = family156 r.family = family
161 priority := float32(1)157 priority := float32(1)
@@ -189,6 +185,9 @@ func New(width, height int) (r *Renderer, err error) {
189 r.dmabuf = sup185 r.dmabuf = sup
190 r.probeRenderModifiers(physical)186 r.probeRenderModifiers(physical)
191 }187 }
188+ // VK_KHR_external_semaphore_fd (enabled above): frames export
189+ // their fence as a sync file.
190+ r.syncFD = r.dd.GetSemaphoreFdKHR != nil
192 }191 }
193 r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)192 r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)
194 extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}193 extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}
@@ -216,7 +215,7 @@ func New(width, height int) (r *Renderer, err error) {
216 return215 return
217 }216 }
218 r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req)217 r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req)
219- // Readback (screenshots, fallback) reads this buffer: uncached memory is ~100x slower.218+ // Readback (screenshots) reads this buffer: uncached memory is ~100x slower.
220 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit|vk.MemoryPropertyHostCachedBit)219 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit|vk.MemoryPropertyHostCachedBit)
221 if err != nil {220 if err != nil {
222 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)221 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
@@ -234,44 +233,17 @@ func New(width, height int) (r *Renderer, err error) {
234 if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil {233 if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil {
235 return234 return
236 }235 }
237- // Start with two frame-sized regions; later frames grow staging on demand.
238- stagingSize := size * 2
239- r.stagingSize = int(stagingSize)
240- stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: stagingUsage, SharingMode: vk.SharingModeExclusive}
241- if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil {
242- return
243- }
244- r.dd.GetBufferMemoryRequirements(r.device, r.staging, &req)
245- kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
246- if err != nil {
247- return
248- }
249- alloc = vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
250- if err = checked("vkAllocateMemory(staging)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.stagingMemory)); err != nil {
251- return
252- }
253- if err = checked("vkBindBufferMemory(staging)", r.dd.BindBufferMemory(r.device, r.staging, r.stagingMemory, 0)); err != nil {
254- return
255- }
256- if err = checked("vkMapMemory(staging)", r.dd.MapMemory(r.device, r.stagingMemory, 0, stagingSize, 0, &r.stagingMapped)); err != nil {
257- return
258- }
259 pi := vk.CommandPoolCreateInfo{SType: vk.StructureTypeCommandPoolCreateInfo, Flags: vk.CommandPoolCreateResetCommandBufferBit, QueueFamilyIndex: family}236 pi := vk.CommandPoolCreateInfo{SType: vk.StructureTypeCommandPoolCreateInfo, Flags: vk.CommandPoolCreateResetCommandBufferBit, QueueFamilyIndex: family}
260 if err = checked("vkCreateCommandPool", r.dd.CreateCommandPool(r.device, &pi, nil, &r.pool)); err != nil {237 if err = checked("vkCreateCommandPool", r.dd.CreateCommandPool(r.device, &pi, nil, &r.pool)); err != nil {
261 return238 return
262 }239 }
263- ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: 1}240+ if err = r.createSlots(); err != nil {
264- if err = checked("vkAllocateCommandBuffers", r.dd.AllocateCommandBuffers(r.device, &ai, &r.command)); err != nil {
265 return241 return
266 }242 }
267 if err = r.createView(&r.own); err != nil {243 if err = r.createView(&r.own); err != nil {
268 return244 return
269 }245 }
270- if err = r.createBlend(); err != nil {246+ if err = r.createComposer(); err != nil {
271- return
272- }
273- fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
274- if err = checked("vkCreateFence", r.dd.CreateFence(r.device, &fi, nil, &r.fence)); err != nil {
275 return247 return
276 }248 }
277 runtime.KeepAlive(priority)249 runtime.KeepAlive(priority)
@@ -325,116 +297,9 @@ func windowColor(id ports.WindowID) [3]uint8 {
325 return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}297 return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}
326 }298 }
327 299
328-// ensureStaging grows the mapped transfer buffer before recording any commands.300+// draws walks the scene into quads in paint order, copying new wl_shm
329-func (r *Renderer) ensureStaging(size int) error {301+// content into GPU buffers on the way.
330- if size <= r.stagingSize {302+func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) []draw {
331- return nil
332- }
333- d := r.dd
334- capacity := size
335- info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: stagingUsage, SharingMode: vk.SharingModeExclusive}
336- var buffer vk.Buffer
337- if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil {
338- return err
339- }
340- var memory vk.DeviceMemory
341- defer func() {
342- if buffer != 0 {
343- d.DestroyBuffer(r.device, buffer, nil)
344- }
345- if memory != 0 {
346- d.FreeMemory(r.device, memory, nil)
347- }
348- }()
349- var req vk.MemoryRequirements
350- d.GetBufferMemoryRequirements(r.device, buffer, &req)
351- kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
352- if err != nil {
353- return err
354- }
355- alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
356- if err := checked("vkAllocateMemory(staging)", d.AllocateMemory(r.device, &alloc, nil, &memory)); err != nil {
357- return err
358- }
359- if err := checked("vkBindBufferMemory(staging)", d.BindBufferMemory(r.device, buffer, memory, 0)); err != nil {
360- return err
361- }
362- var mapped unsafe.Pointer
363- if err := checked("vkMapMemory(staging)", d.MapMemory(r.device, memory, 0, vk.DeviceSize(capacity), 0, &mapped)); err != nil {
364- return err
365- }
366- d.UnmapMemory(r.device, r.stagingMemory)
367- d.DestroyBuffer(r.device, r.staging, nil)
368- d.FreeMemory(r.device, r.stagingMemory, nil)
369- r.staging, r.stagingMemory, r.stagingMapped, r.stagingSize = buffer, memory, mapped, capacity
370- buffer, memory = 0, 0
371- return nil
372-}
373-
374-func (r *Renderer) Clear(rgb [3]uint8) error {
375- return r.render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil)
376-}
377-
378-// upload is one piece of a frame: a solid fill, client pixels staged from
379-// a wl_shm pool, or a dmabuf blitted in place.
380-type upload struct {
381- rect image.Rectangle
382- offset int
383- pixels []byte
384- stride int
385- // dst covers src; they differ in size when the buffer is not drawn at
386- // the physical size (integer-scale clients, rounding).
387- dst, src image.Rectangle
388- color [3]uint8
389- opaque bool
390- // blend draws the staged pixels over the target instead of copying.
391- blend bool
392- // dma is a client GPU buffer, blitted instead of copied from staging.
393- dma *imported
394- // fill is a solid color: staging holds one strip of rows.
395- fill bool
396-}
397-
398-// size is the staging bytes of an upload: a strip for a fill.
399-func (u *upload) size() int {
400- if u.fill {
401- return u.rect.Dx() * min(u.rect.Dy(), fillRows) * 4
402- }
403- return u.rect.Dx() * u.rect.Dy() * 4
404-}
405-
406-// stage copies the visible pixels of a client buffer into dst, resampling
407-// when the buffer is not drawn at its size.
408-func (u *upload) stage(dst []byte) {
409- for row := 0; row < u.rect.Dy(); row++ {
410- line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]
411- y := u.rect.Min.Y + row
412- if u.dst.Size() == u.src.Size() {
413- start := (u.src.Min.Y+y-u.dst.Min.Y)*u.stride + (u.src.Min.X+u.rect.Min.X-u.dst.Min.X)*4
414- copy(line, u.pixels[start:start+len(line)])
415- } else {
416- // Nearest-neighbour resample, sampling pixel centres.
417- sy := u.src.Min.Y + ((y-u.dst.Min.Y)*2+1)*u.src.Dy()/(u.dst.Dy()*2)
418- src := u.pixels[sy*u.stride:]
419- for i := 0; i < u.rect.Dx(); i++ {
420- sx := u.src.Min.X + ((u.rect.Min.X+i-u.dst.Min.X)*2+1)*u.src.Dx()/(u.dst.Dx()*2)
421- copy(line[i*4:i*4+4], src[sx*4:sx*4+4])
422- }
423- }
424- if u.opaque {
425- for i := 3; i < len(line); i += 4 {
426- line[i] = 255
427- }
428- }
429- }
430-}
431-
432-// Render draws a frame. It still waits for the GPU; done is nil.
433-func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) (done *os.File, err error) {
434- return nil, r.render(s, contents)
435-}
436-
437-func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
438 // Scene rects are logical; everything below works in physical pixels.303 // Scene rects are logical; everything below works in physical pixels.
439 scale := s.Scale304 scale := s.Scale
440 if scale <= 0 {305 if scale <= 0 {
@@ -442,21 +307,20 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
442 }307 }
443 phys := func(v int) int { return int(math.Round(float64(v) * scale)) }308 phys := func(v int) int { return int(math.Round(float64(v) * scale)) }
444 physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) }309 physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) }
445- var uploads []upload310+ var draws []draw
446 bounds := image.Rect(0, 0, r.width, r.height)311 bounds := image.Rect(0, 0, r.width, r.height)
447 add := func(rect image.Rectangle, c [3]uint8) {312 add := func(rect image.Rectangle, c [3]uint8) {
448 rect = rect.Intersect(bounds)313 rect = rect.Intersect(bounds)
449 if rect.Empty() {314 if rect.Empty() {
450 return315 return
451 }316 }
452- // A fill stages a strip of up to fillRows rows; the GPU copies the317+ draws = append(draws, r.fillDraw(rect, c))
453- // strip down the rect.
454- uploads = append(uploads, upload{rect: rect, color: c, fill: true})
455 }318 }
456- // addContent maps src (buffer pixels) onto dst (physical pixels).319+ // addContent draws the part of a buffer mapped onto full (physical
457- addContent := func(dst, src image.Rectangle, content *ports.SurfaceContent) {320+ // pixels) that dst shows. key names the surface, seq its content.
321+ addContent := func(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64) {
458 rect := dst.Intersect(bounds)322 rect := dst.Intersect(bounds)
459- if rect.Empty() || src.Empty() {323+ if rect.Empty() {
460 return324 return
461 }325 }
462 if content.DMABuf != nil {326 if content.DMABuf != nil {
@@ -465,7 +329,9 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
465 // The window shows its background until a buffer imports.329 // The window shows its background until a buffer imports.
466 return330 return
467 }331 }
468- uploads = append(uploads, upload{rect: rect, dst: dst, src: src, dma: im})332+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)
333+ dr.set, dr.im = im.set, im
334+ draws = append(draws, dr)
469 return335 return
470 }336 }
471 b := content.SHM337 b := content.SHM
@@ -473,11 +339,19 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
473 if err != nil {339 if err != nil {
474 return340 return
475 }341 }
476- uploads = append(uploads, upload{rect: rect, pixels: pixels[b.Offset:], stride: b.Stride, opaque: content.Opaque, blend: !content.Opaque, dst: dst, src: src})342+ st := shmState{pool: b.Pool, offset: b.Offset, stride: b.Stride, w: content.Width, h: content.Height, seq: seq}
343+ c, err := r.shmCopyFor(key, st, pixels)
344+ if err != nil {
345+ return
346+ }
347+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeBuffer, content.Opaque)
348+ dr.set = c.set
349+ dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
350+ draws = append(draws, dr)
477 }351 }
478 // drawSurface draws one surface buffer with its origin at (x, y)352 // drawSurface draws one surface buffer with its origin at (x, y)
479 // logical, clipped to clip (logical).353 // logical, clipped to clip (logical).
480- drawSurface := func(content *ports.SurfaceContent, x, y int, clip image.Rectangle) {354+ drawSurface := func(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64) {
481 lw, lh := content.LogicalW, content.LogicalH355 lw, lh := content.LogicalW, content.LogicalH
482 if lw <= 0 || lh <= 0 {356 if lw <= 0 || lh <= 0 {
483 lw, lh = content.Width, content.Height357 lw, lh = content.Width, content.Height
@@ -499,26 +373,24 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
499 if dst.Empty() {373 if dst.Empty() {
500 return374 return
501 }375 }
502- // The visible part of dst in buffer pixels.376+ addContent(dst, full, content, key, seq)
503- bx := func(v int) int { return (v - full.Min.X) * content.Width / full.Dx() }
504- by := func(v int) int { return (v - full.Min.Y) * content.Height / full.Dy() }
505- src := image.Rect(bx(dst.Min.X), by(dst.Min.Y), bx(dst.Max.X), by(dst.Max.Y))
506- addContent(dst, src, content)
507 }377 }
508 // place draws a surface tree with its window geometry at (x, y)378 // place draws a surface tree with its window geometry at (x, y)
509 // logical, clipped to w×h logical: client shadows fall outside.379 // logical, clipped to w×h logical: client shadows fall outside.
510- place := func(content *ports.SurfaceContent, x, y, w, h int) {380+ // Every surface of the tree is keyed by its window and place in it;
381+ // the tree's Seq changes with any of them.
382+ place := func(id ports.WindowID, content *ports.SurfaceContent, x, y, w, h int) {
511 clip := image.Rect(x, y, x+w, y+h)383 clip := image.Rect(x, y, x+w, y+h)
512 ox, oy := x-content.Geometry.X, y-content.Geometry.Y384 ox, oy := x-content.Geometry.X, y-content.Geometry.Y
513 for i := range content.Children {385 for i := range content.Children {
514 if ch := &content.Children[i]; ch.Below {386 if ch := &content.Children[i]; ch.Below {
515- drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)387+ drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq)
516 }388 }
517 }389 }
518- drawSurface(content, ox, oy, clip)390+ drawSurface(content, ox, oy, clip, shmKey{id, 0}, content.Seq)
519 for i := range content.Children {391 for i := range content.Children {
520 if ch := &content.Children[i]; !ch.Below {392 if ch := &content.Children[i]; !ch.Below {
521- drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)393+ drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq)
522 }394 }
523 }395 }
524 }396 }
@@ -534,7 +406,7 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
534 continue406 continue
535 }407 }
536 content := contents[layer.ID]408 content := contents[layer.ID]
537- place(&content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)409+ place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)
538 }410 }
539 }411 }
540 addLayers(false)412 addLayers(false)
@@ -558,7 +430,7 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
558 add(body, windowColor(w.ID))430 add(body, windowColor(w.ID))
559 } else {431 } else {
560 add(body, parseColor(s.Background))432 add(body, parseColor(s.Background))
561- place(&content, cx, cy, cw, ch)433+ place(w.ID, &content, cx, cy, cw, ch)
562 }434 }
563 borderColor := s.Border.Inactive435 borderColor := s.Border.Inactive
564 if w.Focused {436 if w.Focused {
@@ -583,225 +455,14 @@ func (r *Renderer) render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
583 for _, w := range s.Windows {455 for _, w := range s.Windows {
584 if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 {456 if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 {
585 content := contents[w.ID]457 content := contents[w.ID]
586- place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)458+ place(w.ID, &content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)
587 }459 }
588 }460 }
589 }461 }
590 addPopups(false)462 addPopups(false)
591 addLayers(true)463 addLayers(true)
592 addPopups(true)464 addPopups(true)
593- // Blended pixels are staged first, so the shaders bind only them,465+ return draws
594- // within the device's storage buffer range; past it they are copied.
595- // The draw table follows them, then the other uploads.
596- blends, used := 0, 0
597- for k := range uploads {
598- u := &uploads[k]
599- if u.blend && used+u.size() > r.maxRange {
600- u.blend = false
601- }
602- if u.blend {
603- u.offset = used
604- used += u.size()
605- blends++
606- }
607- }
608- pixelBytes := used
609- tableOffset := (used + r.align - 1) / r.align * r.align
610- used = tableOffset + blends*drawSize
611- for k := range uploads {
612- if u := &uploads[k]; !u.blend && u.dma == nil {
613- u.offset = used
614- used += u.size()
615- }
616- }
617- if err := r.ensureStaging(used); err != nil {
618- return err
619- }
620- data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize)
621- if blends > 0 {
622- r.bindStaging(pixelBytes, tableOffset, blends)
623- i := 0
624- for k := range uploads {
625- if uploads[k].blend {
626- r.putDraw(data[tableOffset:], i, &uploads[k])
627- i++
628- }
629- }
630- }
631- for _, u := range uploads {
632- if u.dma != nil {
633- continue
634- }
635- if u.fill {
636- line := data[u.offset : u.offset+u.rect.Dx()*min(u.rect.Dy(), fillRows)*4]
637- for i := 0; i < len(line); i += 4 {
638- line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255
639- }
640- continue
641- }
642- dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]
643- // The client may shrink its pool under the mapping: its window
644- // then shows garbage for this frame, never a crash.
645- copyGuarded(func() { u.stage(dst) })
646- }
647- rgb := parseColor(s.Background)
648- d := r.dd
649- if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
650- return err
651- }
652- begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo}
653- if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil {
654- return err
655- }
656- rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
657- tg := r.target()
658- srcStage := vk.PipelineStageFlags(vk.PipelineStageTopOfPipeBit)
659- srcAccess := vk.AccessFlags(0)
660- if tg.layout == vk.ImageLayoutTransferSrcOptimal {
661- srcStage = vk.PipelineStageTransferBit
662- srcAccess = vk.AccessTransferReadBit
663- }
664- // The whole target is cleared: its old contents never matter.
665- barrier := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: srcAccess, DstAccessMask: vk.AccessTransferWriteBit, OldLayout: tg.layout, NewLayout: vk.ImageLayoutTransferDstOptimal, Image: tg.image, SubresourceRange: rangeInfo}
666- if tg.exported {
667- // Acquire from the display (foreign queue family).
668- barrier.OldLayout = vk.ImageLayoutUndefined
669- barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
670- srcStage, barrier.SrcAccessMask = vk.PipelineStageTopOfPipeBit, 0
671- }
672- d.CmdPipelineBarrier(r.command, srcStage, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
673- barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = 0, 0
674- // VkClearColorValue is a union; UNORM formats read it as float32.
675- unorm := func(v uint8) uint32 { return math.Float32bits(float32(v) / 255) }
676- color := vk.ClearColorValue{unorm(rgb[0]), unorm(rgb[1]), unorm(rgb[2]), math.Float32bits(1)}
677- d.CmdClearColorImage(r.command, tg.image, vk.ImageLayoutTransferDstOptimal, &color, 1, &rangeInfo)
678- barrier.SrcAccessMask = vk.AccessTransferWriteBit
679- barrier.DstAccessMask = vk.AccessTransferWriteBit
680- barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
681- barrier.NewLayout = vk.ImageLayoutTransferDstOptimal
682- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
683- // Client buffers come from the foreign queue family (their driver) and
684- // go back to it after the frame, so the next frame acquires them again.
685- var dmas []*imported
686- for _, u := range uploads {
687- if u.dma != nil && !slices.Contains(dmas, u.dma) {
688- dmas = append(dmas, u.dma)
689- }
690- }
691- ownership := func(acquire bool) {
692- for _, im := range dmas {
693- b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: rangeInfo}
694- if acquire {
695- // GENERAL, never UNDEFINED: the client's contents must survive.
696- b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutTransferSrcOptimal
697- b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
698- b.DstAccessMask = vk.AccessTransferReadBit
699- } else {
700- b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
701- b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
702- b.SrcAccessMask = vk.AccessTransferReadBit
703- }
704- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
705- }
706- }
707- ownership(true)
708- drawn := 0 // blended uploads drawn so far: their index in the draw table
709- for i := 0; i < len(uploads); i++ {
710- f := uploads[i]
711- if i > 0 && !f.blend {
712- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
713- }
714- if f.blend {
715- // Consecutive blended uploads share one draw: blending follows
716- // primitive order within it.
717- n := 1
718- for i+n < len(uploads) && uploads[i+n].blend {
719- n++
720- }
721- r.drawBlended(tg.image, tg.view, drawn, drawn+n)
722- drawn += n
723- i += n - 1
724- continue
725- }
726- rect := f.rect
727- if f.dma != nil {
728- // Map the visible part of dst back onto the buffer.
729- sx := func(x int) int32 { return int32(f.src.Min.X + (x-f.dst.Min.X)*f.src.Dx()/f.dst.Dx()) }
730- sy := func(y int) int32 { return int32(f.src.Min.Y + (y-f.dst.Min.Y)*f.src.Dy()/f.dst.Dy()) }
731- layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}
732- blit := vk.ImageBlit{
733- SrcSubresource: layers,
734- SrcOffsets: [2]vk.Offset3D{{X: sx(rect.Min.X), Y: sy(rect.Min.Y)}, {X: sx(rect.Max.X), Y: sy(rect.Max.Y), Z: 1}},
735- DstSubresource: layers,
736- DstOffsets: [2]vk.Offset3D{{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, {X: int32(rect.Max.X), Y: int32(rect.Max.Y), Z: 1}},
737- }
738- d.CmdBlitImage(r.command, f.dma.image, vk.ImageLayoutTransferSrcOptimal, tg.image, vk.ImageLayoutTransferDstOptimal, 1, &blit, vk.FilterNearest)
739- continue
740- }
741- layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}
742- if f.fill {
743- // Every band reads the same staged strip.
744- r.fillRegions = r.fillRegions[:0]
745- for y := rect.Min.Y; y < rect.Max.Y; y += fillRows {
746- h := min(fillRows, rect.Max.Y-y)
747- r.fillRegions = append(r.fillRegions, vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(h), Depth: 1}})
748- }
749- d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, uint32(len(r.fillRegions)), &r.fillRegions[0])
750- continue
751- }
752- region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}
753- d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, 1, &region)
754- }
755- ownership(false)
756- barrier.SrcAccessMask = vk.AccessTransferWriteBit
757- barrier.DstAccessMask = vk.AccessTransferReadBit
758- barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
759- barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal
760- if tg.exported {
761- // Release to the display: KMS scans it out after the fence.
762- barrier.NewLayout = vk.ImageLayoutGeneral
763- barrier.DstAccessMask = 0
764- barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
765- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &barrier)
766- } else {
767- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
768- }
769- if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
770- return err
771- }
772- // Implicit sync: wait for the client's GPU writes to each buffer.
773- var waits []vk.Semaphore
774- for _, im := range dmas {
775- if sem := r.readFence(im); sem != 0 {
776- waits = append(waits, sem)
777- }
778- }
779- defer func() {
780- for _, sem := range waits {
781- d.DestroySemaphore(r.device, sem, nil)
782- }
783- }()
784- submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
785- stages := make([]vk.PipelineStageFlags, len(waits))
786- if len(waits) > 0 {
787- for i := range stages {
788- stages[i] = vk.PipelineStageTransferBit
789- }
790- submit.WaitSemaphoreCount, submit.WaitSemaphores, submit.WaitDstStageMask = uint32(len(waits)), &waits[0], &stages[0]
791- }
792- if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
793- return err
794- }
795- runtime.KeepAlive(stages)
796- tg.layout = barrier.NewLayout
797- r.last, r.readBack = tg, false
798- // The fence wait is the GPU completion ADR 014 requires before a flip.
799- if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
800- return err
801- }
802- r.dropUnused()
803- r.dropPools()
804- return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
805 }466 }
806 467
807 func (r *Renderer) Pixels() *image.RGBA {468 func (r *Renderer) Pixels() *image.RGBA {
@@ -823,52 +484,44 @@ const (
823 wholeSize = ^vk.DeviceSize(0)484 wholeSize = ^vk.DeviceSize(0)
824 )485 )
825 486
826-// readback copies the last frame into the host buffer, once per frame.487+// readback copies the last frame into the host buffer, once per frame
488+// (screenshots only).
827 func (r *Renderer) readback() error {489 func (r *Renderer) readback() error {
828 if r.readBack || r.last == nil || r.last.image == 0 {490 if r.readBack || r.last == nil || r.last.image == 0 {
829 return nil491 return nil
830 }492 }
831- d := r.dd493+ // The copy reads the frame: wait for it, and keep the ring ordered.
832- if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {494+ if err := r.waitFrame(r.submitted); err != nil {
833- return err
834- }
835- begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo}
836- if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil {
837 return err495 return err
838 }496 }
839 t := r.last497 t := r.last
840- rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}498+ err := r.oneShot(func(cmd vk.CommandBuffer) {
841- b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessTransferReadBit, OldLayout: t.layout, NewLayout: vk.ImageLayoutTransferSrcOptimal, Image: t.image, SubresourceRange: rangeInfo}499+ d := r.dd
842- if t.exported {500+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessTransferReadBit, OldLayout: t.layout, NewLayout: vk.ImageLayoutTransferSrcOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: t.image, SubresourceRange: colorRange}
843- b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family501+ if t.exported {
844- b.SrcAccessMask = 0502+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
845- }503+ }
846- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)504+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
847- region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}505+ region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
848- d.CmdCopyImageToBuffer(r.command, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &region)506+ d.CmdCopyImageToBuffer(cmd, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &region)
849- // Make the copy visible to host reads of the mapped buffer.507+ // Make the copy visible to host reads of the mapped buffer.
850- hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: r.buffer, Size: wholeSize}508+ hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: r.buffer, Size: wholeSize}
851- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil)509+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil)
852- if t.exported {510+ if t.exported {
853- b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral511+ b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
854- b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0512+ b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0
855- b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT513+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
856- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)514+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
857- } else {515+ }
858- t.layout = vk.ImageLayoutTransferSrcOptimal516+ })
859- }517+ if err != nil {
860- if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
861- return err
862- }
863- submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
864- if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
865 return err518 return err
866 }519 }
867- if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {520+ if !t.exported {
868- return err521+ t.layout = vk.ImageLayoutTransferSrcOptimal
869 }522 }
870 r.readBack = true523 r.readBack = true
871- return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))524+ return nil
872 }525 }
873 526
874 func (r *Renderer) Close() {527 func (r *Renderer) Close() {
@@ -879,6 +532,7 @@ func (r *Renderer) Close() {
879 d := r.dd532 d := r.dd
880 if r.device != 0 {533 if r.device != 0 {
881 _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))534 _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))
535+ r.idle()
882 }536 }
883 r.dropTargets()537 r.dropTargets()
884 r.dropCursors()538 r.dropCursors()
@@ -888,30 +542,12 @@ func (r *Renderer) Close() {
888 }542 }
889 r.frame += importTTL + 1543 r.frame += importTTL + 1
890 r.dropPools()544 r.dropPools()
891- if r.fence != 0 {545+ r.dropShm()
892- d.DestroyFence(r.device, r.fence, nil)546+ r.destroySlots()
893- r.fence = 0
894- }
895- if r.command != 0 {
896- d.FreeCommandBuffers(r.device, r.pool, 1, &r.command)
897- r.command = 0
898- }
899 if r.pool != 0 {547 if r.pool != 0 {
900 d.DestroyCommandPool(r.device, r.pool, nil)548 d.DestroyCommandPool(r.device, r.pool, nil)
901 r.pool = 0549 r.pool = 0
902 }550 }
903- if r.stagingMapped != nil {
904- d.UnmapMemory(r.device, r.stagingMemory)
905- r.stagingMapped = nil
906- }
907- if r.staging != 0 {
908- d.DestroyBuffer(r.device, r.staging, nil)
909- r.staging = 0
910- }
911- if r.stagingMemory != 0 {
912- d.FreeMemory(r.device, r.stagingMemory, nil)
913- r.stagingMemory = 0
914- }
915 if r.mapped != nil {551 if r.mapped != nil {
916 d.UnmapMemory(r.device, r.bufferMemory)552 d.UnmapMemory(r.device, r.bufferMemory)
917 r.mapped = nil553 r.mapped = nil
@@ -924,7 +560,7 @@ func (r *Renderer) Close() {
924 d.FreeMemory(r.device, r.bufferMemory, nil)560 d.FreeMemory(r.device, r.bufferMemory, nil)
925 r.bufferMemory = 0561 r.bufferMemory = 0
926 }562 }
927- r.destroyBlend()563+ r.destroyComposer()
928 r.freeTarget(&r.own)564 r.freeTarget(&r.own)
929 d.DestroyDevice(r.device, nil)565 d.DestroyDevice(r.device, nil)
930 r.device = 0566 r.device = 0
M internal/adapters/vulkan/renderer_test.go +2 -3
@@ -335,10 +335,9 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
335335 }
336336 }
337337
338-// A fill taller than one staged strip is copied down in bands: every row
339-// through the last one gets the color.
338+// A tall fill covers every row through the last one.
340339 func TestRendererTallFill(t *testing.T) {
341- h := fillRows*2 + 7
340+ h := 135
342341 r, err := New(8, h)
343342 if err != nil {
344343 t.Skipf("Vulkan unavailable: %v", err)
@@ -335,10 +335,9 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
335 }335 }
336 }336 }
337 337
338-// A fill taller than one staged strip is copied down in bands: every row338+// A tall fill covers every row through the last one.
339-// through the last one gets the color.
340 func TestRendererTallFill(t *testing.T) {339 func TestRendererTallFill(t *testing.T) {
341- h := fillRows*2 + 7340+ h := 135
342 r, err := New(8, h)341 r, err := New(8, h)
343 if err != nil {342 if err != nil {
344 t.Skipf("Vulkan unavailable: %v", err)343 t.Skipf("Vulkan unavailable: %v", err)
D internal/adapters/vulkan/shaders/blend.frag +0 -22
@@ -1,22 +0,0 @@
1-#version 450
2-// Reads the staged B8G8R8A8 pixel under the fragment: premultiplied, as
3-// Wayland ARGB buffers are, so the pipeline blends ONE, ONE_MINUS_SRC_ALPHA.
4-
5-struct Draw {
6- ivec4 rect;
7- uvec4 pixels;
8-};
9-
10-layout(std430, set = 0, binding = 0) readonly buffer Pixels { uint pixels[]; };
11-layout(std430, set = 0, binding = 1) readonly buffer Draws { Draw draws[]; };
12-
13-layout(location = 0) flat in uint draw;
14-layout(location = 0) out vec4 color;
15-
16-void main() {
17- Draw d = draws[draw];
18- ivec2 p = ivec2(gl_FragCoord.xy) - d.rect.xy;
19- // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).
20- vec4 bgra = unpackUnorm4x8(pixels[d.pixels.x + uint(p.y) * d.pixels.y + uint(p.x)]);
21- color = bgra.zyxw;
22-}
@@ -1,22 +0,0 @@
1-#version 450
2-// Reads the staged B8G8R8A8 pixel under the fragment: premultiplied, as
3-// Wayland ARGB buffers are, so the pipeline blends ONE, ONE_MINUS_SRC_ALPHA.
4-
5-struct Draw {
6- ivec4 rect;
7- uvec4 pixels;
8-};
9-
10-layout(std430, set = 0, binding = 0) readonly buffer Pixels { uint pixels[]; };
11-layout(std430, set = 0, binding = 1) readonly buffer Draws { Draw draws[]; };
12-
13-layout(location = 0) flat in uint draw;
14-layout(location = 0) out vec4 color;
15-
16-void main() {
17- Draw d = draws[draw];
18- ivec2 p = ivec2(gl_FragCoord.xy) - d.rect.xy;
19- // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).
20- vec4 bgra = unpackUnorm4x8(pixels[d.pixels.x + uint(p.y) * d.pixels.y + uint(p.x)]);
21- color = bgra.zyxw;
22-}
D internal/adapters/vulkan/shaders/blend.frag.spv +0 -0 binary file: no text diff

binary file: no text diff

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

binary file: no text diff

A internal/adapters/vulkan/shaders/compose.frag +55 -0
@@ -0,0 +1,55 @@
1+#version 450
2+// Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm
3+// buffer (B8G8R8A8 words in a storage buffer). Client pixels are
4+// premultiplied, as Wayland buffers are: the pipeline blends ONE,
5+// ONE_MINUS_SRC_ALPHA. Scaled draws filter linearly; 1:1 draws read the
6+// texel under the fragment, pixel exact.
7+
8+layout(push_constant) uniform Draw {
9+ ivec4 rect;
10+ vec4 map;
11+ vec4 color;
12+ uvec4 buf;
13+ uvec4 misc;
14+} d;
15+
16+layout(set = 0, binding = 0) uniform sampler2D tex;
17+layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; };
18+
19+layout(location = 0) out vec4 color;
20+
21+const uint modeSolid = 0u;
22+const uint modeImage = 1u;
23+const uint flagOpaque = 1u;
24+const uint flagExact = 2u;
25+
26+// texel reads buffer pixel p, clamped to the buffer (clamp to edge).
27+vec4 texel(ivec2 p) {
28+ p = clamp(p, ivec2(0), ivec2(d.buf.yz) - 1);
29+ // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).
30+ return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw;
31+}
32+
33+void main() {
34+ vec2 src = d.map.xy + gl_FragCoord.xy * d.map.zw;
35+ bool exact = (d.misc.y & flagExact) != 0u;
36+ vec4 c;
37+ if (d.misc.x == modeSolid) {
38+ c = d.color;
39+ } else if (d.misc.x == modeImage) {
40+ ivec2 size = textureSize(tex, 0);
41+ c = exact ? texelFetch(tex, clamp(ivec2(floor(src)), ivec2(0), size - 1), 0) : texture(tex, src / vec2(size));
42+ } else if (exact) {
43+ c = texel(ivec2(floor(src)));
44+ } else {
45+ // Bilinear: four texel reads around the sample point.
46+ vec2 p = src - 0.5;
47+ ivec2 i = ivec2(floor(p));
48+ vec2 f = p - vec2(i);
49+ c = mix(mix(texel(i), texel(i + ivec2(1, 0)), f.x), mix(texel(i + ivec2(0, 1)), texel(i + ivec2(1, 1)), f.x), f.y);
50+ }
51+ if ((d.misc.y & flagOpaque) != 0u) {
52+ c.a = 1.0;
53+ }
54+ color = c;
55+}
@@ -0,0 +1,55 @@
1+#version 450
2+// Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm
3+// buffer (B8G8R8A8 words in a storage buffer). Client pixels are
4+// premultiplied, as Wayland buffers are: the pipeline blends ONE,
5+// ONE_MINUS_SRC_ALPHA. Scaled draws filter linearly; 1:1 draws read the
6+// texel under the fragment, pixel exact.
7+
8+layout(push_constant) uniform Draw {
9+ ivec4 rect;
10+ vec4 map;
11+ vec4 color;
12+ uvec4 buf;
13+ uvec4 misc;
14+} d;
15+
16+layout(set = 0, binding = 0) uniform sampler2D tex;
17+layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; };
18+
19+layout(location = 0) out vec4 color;
20+
21+const uint modeSolid = 0u;
22+const uint modeImage = 1u;
23+const uint flagOpaque = 1u;
24+const uint flagExact = 2u;
25+
26+// texel reads buffer pixel p, clamped to the buffer (clamp to edge).
27+vec4 texel(ivec2 p) {
28+ p = clamp(p, ivec2(0), ivec2(d.buf.yz) - 1);
29+ // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).
30+ return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw;
31+}
32+
33+void main() {
34+ vec2 src = d.map.xy + gl_FragCoord.xy * d.map.zw;
35+ bool exact = (d.misc.y & flagExact) != 0u;
36+ vec4 c;
37+ if (d.misc.x == modeSolid) {
38+ c = d.color;
39+ } else if (d.misc.x == modeImage) {
40+ ivec2 size = textureSize(tex, 0);
41+ c = exact ? texelFetch(tex, clamp(ivec2(floor(src)), ivec2(0), size - 1), 0) : texture(tex, src / vec2(size));
42+ } else if (exact) {
43+ c = texel(ivec2(floor(src)));
44+ } else {
45+ // Bilinear: four texel reads around the sample point.
46+ vec2 p = src - 0.5;
47+ ivec2 i = ivec2(floor(p));
48+ vec2 f = p - vec2(i);
49+ c = mix(mix(texel(i), texel(i + ivec2(1, 0)), f.x), mix(texel(i + ivec2(0, 1)), texel(i + ivec2(1, 1)), f.x), f.y);
50+ }
51+ if ((d.misc.y & flagOpaque) != 0u) {
52+ c.a = 1.0;
53+ }
54+ color = c;
55+}
A internal/adapters/vulkan/shaders/compose.frag.spv +0 -0 binary file: no text diff

binary file: no text diff

A internal/adapters/vulkan/shaders/compose.vert +17 -0
@@ -0,0 +1,17 @@
1+#version 450
2+// One quad per draw: two triangles over the visible target rect.
3+
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 * map.zw
7+ vec4 color; // solid fills: premultiplied RGBA
8+ uvec4 buf; // buffer draws: first word, row width, height, unused
9+ uvec4 misc; // mode, flags, target width, target height
10+} d;
11+
12+void main() {
13+ const ivec2 corners[6] = ivec2[](ivec2(0, 0), ivec2(1, 0), ivec2(0, 1), ivec2(0, 1), ivec2(1, 0), ivec2(1, 1));
14+ ivec2 c = corners[gl_VertexIndex];
15+ vec2 p = vec2(c.x == 0 ? d.rect.x : d.rect.z, c.y == 0 ? d.rect.y : d.rect.w);
16+ gl_Position = vec4(p / vec2(d.misc.zw) * 2.0 - 1.0, 0.0, 1.0);
17+}
@@ -0,0 +1,17 @@
1+#version 450
2+// One quad per draw: two triangles over the visible target rect.
3+
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 * map.zw
7+ vec4 color; // solid fills: premultiplied RGBA
8+ uvec4 buf; // buffer draws: first word, row width, height, unused
9+ uvec4 misc; // mode, flags, target width, target height
10+} d;
11+
12+void main() {
13+ const ivec2 corners[6] = ivec2[](ivec2(0, 0), ivec2(1, 0), ivec2(0, 1), ivec2(0, 1), ivec2(1, 0), ivec2(1, 1));
14+ ivec2 c = corners[gl_VertexIndex];
15+ vec2 p = vec2(c.x == 0 ? d.rect.x : d.rect.z, c.y == 0 ? d.rect.y : d.rect.w);
16+ gl_Position = vec4(p / vec2(d.misc.zw) * 2.0 - 1.0, 0.0, 1.0);
17+}
A internal/adapters/vulkan/shaders/compose.vert.spv +0 -0 binary file: no text diff

binary file: no text diff

M internal/adapters/vulkan/shm.go +183 -7
@@ -3,15 +3,20 @@ package vulkan
33 import (
44 "fmt"
55 "runtime/debug"
6+ "unsafe"
67
78 "github.com/bnema/nefertty/internal/ports"
9+ vk "github.com/bnema/purego-vulkan/vulkan"
810 "golang.org/x/sys/unix"
911 )
1012
11-// wl_shm buffers are read in place: each client pool is mapped once, read
12-// only, and the visible pixels are copied from it into staging when a frame
13-// is drawn. Mappings follow the dmabuf import rules: kept while drawn,
14-// dropped after importTTL frames without use.
13+// wl_shm buffers: each client pool is mapped once, read only. A surface's
14+// pixels are copied once per new content into a persistently mapped,
15+// host-visible GPU buffer (device-local when the GPU exposes it, ReBAR)
16+// that the fragment shader reads directly: one memcpy per row, no staging
17+// copy, no resample, no alpha fix-up on the CPU. Each surface has two GPU
18+// buffers used in turn, so the CPU never writes one a frame in flight
19+// reads. Mappings and buffers are dropped after importTTL frames unused.
1520
1621 // mapping is a client pool mapped read-only.
1722 type mapping struct {
@@ -27,8 +32,8 @@ func (r *Renderer) shmPixels(b *ports.SHMBuffer, end int) ([]byte, error) {
2732 m.last = r.frame
2833 return m.data, nil
2934 }
30- // Uploads collected earlier this frame may still slice the old
31- // mapping: it is unmapped once staging is done.
35+ // Draws collected earlier this frame may still read the old
36+ // mapping: it is unmapped once the frame is recorded.
3237 r.retired = append(r.retired, m.data)
3338 delete(r.pools, b.Pool)
3439 }
@@ -53,7 +58,8 @@ func (r *Renderer) shmPixels(b *ports.SHMBuffer, end int) ([]byte, error) {
5358 }
5459
5560 // dropPools unmaps replaced mappings and pools not drawn for importTTL
56-// frames.
61+// frames. The GPU never reads a mapping: this runs once the CPU copies of
62+// a frame are done.
5763 func (r *Renderer) dropPools() {
5864 for _, data := range r.retired {
5965 _ = unix.Munmap(data)
@@ -80,3 +86,173 @@ func copyGuarded(copy func()) (ok bool) {
8086 copy()
8187 return true
8288 }
89+
90+// gpuBuffer is a host-visible storage buffer, mapped for its whole life.
91+type gpuBuffer struct {
92+ buffer vk.Buffer
93+ memory vk.DeviceMemory
94+ mapped unsafe.Pointer
95+ size int
96+ // last is the frame that last read it: it is written or freed only
97+ // once that frame completed.
98+ last uint64
99+}
100+
101+func (r *Renderer) newGPUBuffer(b *gpuBuffer, size int) error {
102+ d := r.dd
103+ ok := false
104+ defer func() {
105+ if !ok {
106+ r.freeGPUBuffer(b)
107+ }
108+ }()
109+ info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(size), Usage: vk.BufferUsageStorageBufferBit, SharingMode: vk.SharingModeExclusive}
110+ if err := checked("vkCreateBuffer(shm)", d.CreateBuffer(r.device, &info, nil, &b.buffer)); err != nil {
111+ return err
112+ }
113+ var req vk.MemoryRequirements
114+ d.GetBufferMemoryRequirements(r.device, b.buffer, &req)
115+ host := vk.MemoryPropertyFlags(vk.MemoryPropertyHostVisibleBit | vk.MemoryPropertyHostCoherentBit)
116+ kind, err := r.findMemoryType(req.MemoryTypeBits, host|vk.MemoryPropertyDeviceLocalBit)
117+ if err != nil {
118+ kind, err = r.findMemoryType(req.MemoryTypeBits, host)
119+ }
120+ if err != nil {
121+ return err
122+ }
123+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
124+ if err := checked("vkAllocateMemory(shm)", d.AllocateMemory(r.device, &alloc, nil, &b.memory)); err != nil {
125+ return err
126+ }
127+ if err := checked("vkBindBufferMemory(shm)", d.BindBufferMemory(r.device, b.buffer, b.memory, 0)); err != nil {
128+ return err
129+ }
130+ if err := checked("vkMapMemory(shm)", d.MapMemory(r.device, b.memory, 0, vk.DeviceSize(size), 0, &b.mapped)); err != nil {
131+ return err
132+ }
133+ b.size, ok = size, true
134+ return nil
135+}
136+
137+func (r *Renderer) freeGPUBuffer(b *gpuBuffer) {
138+ d := r.dd
139+ if b.mapped != nil {
140+ d.UnmapMemory(r.device, b.memory)
141+ }
142+ if b.buffer != 0 {
143+ d.DestroyBuffer(r.device, b.buffer, nil)
144+ }
145+ if b.memory != 0 {
146+ d.FreeMemory(r.device, b.memory, nil)
147+ }
148+ *b = gpuBuffer{}
149+}
150+
151+// shmKey is a surface of a window: index 0 is the root, i+1 its child i.
152+type shmKey struct {
153+ win ports.WindowID
154+ index int
155+}
156+
157+// shmState is what a GPU buffer holds: a client buffer at a content Seq.
158+type shmState struct {
159+ pool uint64
160+ offset, stride int
161+ w, h int
162+ seq uint64
163+}
164+
165+// shmCopy is one of a surface's two GPU buffers and the set binding it.
166+type shmCopy struct {
167+ gpu gpuBuffer
168+ pool vk.DescriptorPool
169+ set vk.DescriptorSet
170+ holds shmState
171+ valid bool
172+}
173+
174+// shmSurface is the GPU copy of one surface's wl_shm content.
175+type shmSurface struct {
176+ bufs [2]*shmCopy
177+ last uint64 // frame that drew it
178+}
179+
180+// shmCopyFor returns the GPU buffer holding a surface's content st, copying
181+// the client pixels into one when no buffer holds them yet.
182+func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte) (*shmCopy, error) {
183+ s := r.shm[key]
184+ if s == nil {
185+ s = &shmSurface{}
186+ r.shm[key] = s
187+ }
188+ s.last = r.frame
189+ for _, c := range s.bufs {
190+ if c != nil && c.valid && c.holds == st {
191+ c.gpu.last = r.frame
192+ return c, nil
193+ }
194+ }
195+ // The older buffer takes the new content.
196+ i := 0
197+ if s.bufs[0] != nil && (s.bufs[1] == nil || s.bufs[1].gpu.last < s.bufs[0].gpu.last) {
198+ i = 1
199+ }
200+ size := st.w * st.h * 4
201+ if size > r.maxRange {
202+ return nil, fmt.Errorf("shm buffer of %d bytes past the storage buffer range", size)
203+ }
204+ c := s.bufs[i]
205+ if c != nil && c.gpu.size != size {
206+ r.retire(c.gpu.last, func() { r.freeShmCopy(c) })
207+ s.bufs[i], c = nil, nil
208+ }
209+ if c == nil {
210+ c = &shmCopy{}
211+ if err := r.newGPUBuffer(&c.gpu, size); err != nil {
212+ return nil, err
213+ }
214+ var err error
215+ if c.pool, c.set, err = r.newSet(r.compose.dummyView, c.gpu.buffer); err != nil {
216+ r.freeGPUBuffer(&c.gpu)
217+ return nil, err
218+ }
219+ s.bufs[i] = c
220+ }
221+ if err := r.waitFrame(c.gpu.last); err != nil {
222+ return nil, err
223+ }
224+ dst := unsafe.Slice((*byte)(c.gpu.mapped), size)
225+ row := st.w * 4
226+ // The client may shrink its pool under the mapping: its window then
227+ // shows garbage for this frame, never a crash, and is copied again.
228+ c.valid = copyGuarded(func() {
229+ for y := range st.h {
230+ copy(dst[y*row:(y+1)*row], pixels[st.offset+y*st.stride:])
231+ }
232+ })
233+ r.copied += size
234+ c.holds, c.gpu.last = st, r.frame
235+ return c, nil
236+}
237+
238+func (r *Renderer) freeShmCopy(c *shmCopy) {
239+ if c.pool != 0 {
240+ r.dd.DestroyDescriptorPool(r.device, c.pool, nil)
241+ c.pool = 0
242+ }
243+ r.freeGPUBuffer(&c.gpu)
244+}
245+
246+// dropShm frees the GPU buffers of surfaces not drawn for importTTL frames.
247+func (r *Renderer) dropShm() {
248+ for key, s := range r.shm {
249+ if r.frame-s.last > importTTL {
250+ for _, c := range s.bufs {
251+ if c != nil {
252+ r.retire(c.gpu.last, func() { r.freeShmCopy(c) })
253+ }
254+ }
255+ delete(r.shm, key)
256+ }
257+ }
258+}
@@ -3,15 +3,20 @@ package vulkan
3 import (3 import (
4 "fmt"4 "fmt"
5 "runtime/debug"5 "runtime/debug"
6+ "unsafe"
6 7
7 "github.com/bnema/nefertty/internal/ports"8 "github.com/bnema/nefertty/internal/ports"
9+ vk "github.com/bnema/purego-vulkan/vulkan"
8 "golang.org/x/sys/unix"10 "golang.org/x/sys/unix"
9 )11 )
10 12
11-// wl_shm buffers are read in place: each client pool is mapped once, read13+// wl_shm buffers: each client pool is mapped once, read only. A surface's
12-// only, and the visible pixels are copied from it into staging when a frame14+// pixels are copied once per new content into a persistently mapped,
13-// is drawn. Mappings follow the dmabuf import rules: kept while drawn,15+// host-visible GPU buffer (device-local when the GPU exposes it, ReBAR)
14-// dropped after importTTL frames without use.16+// that the fragment shader reads directly: one memcpy per row, no staging
17+// copy, no resample, no alpha fix-up on the CPU. Each surface has two GPU
18+// buffers used in turn, so the CPU never writes one a frame in flight
19+// reads. Mappings and buffers are dropped after importTTL frames unused.
15 20
16 // mapping is a client pool mapped read-only.21 // mapping is a client pool mapped read-only.
17 type mapping struct {22 type mapping struct {
@@ -27,8 +32,8 @@ func (r *Renderer) shmPixels(b *ports.SHMBuffer, end int) ([]byte, error) {
27 m.last = r.frame32 m.last = r.frame
28 return m.data, nil33 return m.data, nil
29 }34 }
30- // Uploads collected earlier this frame may still slice the old35+ // Draws collected earlier this frame may still read the old
31- // mapping: it is unmapped once staging is done.36+ // mapping: it is unmapped once the frame is recorded.
32 r.retired = append(r.retired, m.data)37 r.retired = append(r.retired, m.data)
33 delete(r.pools, b.Pool)38 delete(r.pools, b.Pool)
34 }39 }
@@ -53,7 +58,8 @@ func (r *Renderer) shmPixels(b *ports.SHMBuffer, end int) ([]byte, error) {
53 }58 }
54 59
55 // dropPools unmaps replaced mappings and pools not drawn for importTTL60 // dropPools unmaps replaced mappings and pools not drawn for importTTL
56-// frames.61+// frames. The GPU never reads a mapping: this runs once the CPU copies of
62+// a frame are done.
57 func (r *Renderer) dropPools() {63 func (r *Renderer) dropPools() {
58 for _, data := range r.retired {64 for _, data := range r.retired {
59 _ = unix.Munmap(data)65 _ = unix.Munmap(data)
@@ -80,3 +86,173 @@ func copyGuarded(copy func()) (ok bool) {
80 copy()86 copy()
81 return true87 return true
82 }88 }
89+
90+// gpuBuffer is a host-visible storage buffer, mapped for its whole life.
91+type gpuBuffer struct {
92+ buffer vk.Buffer
93+ memory vk.DeviceMemory
94+ mapped unsafe.Pointer
95+ size int
96+ // last is the frame that last read it: it is written or freed only
97+ // once that frame completed.
98+ last uint64
99+}
100+
101+func (r *Renderer) newGPUBuffer(b *gpuBuffer, size int) error {
102+ d := r.dd
103+ ok := false
104+ defer func() {
105+ if !ok {
106+ r.freeGPUBuffer(b)
107+ }
108+ }()
109+ info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(size), Usage: vk.BufferUsageStorageBufferBit, SharingMode: vk.SharingModeExclusive}
110+ if err := checked("vkCreateBuffer(shm)", d.CreateBuffer(r.device, &info, nil, &b.buffer)); err != nil {
111+ return err
112+ }
113+ var req vk.MemoryRequirements
114+ d.GetBufferMemoryRequirements(r.device, b.buffer, &req)
115+ host := vk.MemoryPropertyFlags(vk.MemoryPropertyHostVisibleBit | vk.MemoryPropertyHostCoherentBit)
116+ kind, err := r.findMemoryType(req.MemoryTypeBits, host|vk.MemoryPropertyDeviceLocalBit)
117+ if err != nil {
118+ kind, err = r.findMemoryType(req.MemoryTypeBits, host)
119+ }
120+ if err != nil {
121+ return err
122+ }
123+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
124+ if err := checked("vkAllocateMemory(shm)", d.AllocateMemory(r.device, &alloc, nil, &b.memory)); err != nil {
125+ return err
126+ }
127+ if err := checked("vkBindBufferMemory(shm)", d.BindBufferMemory(r.device, b.buffer, b.memory, 0)); err != nil {
128+ return err
129+ }
130+ if err := checked("vkMapMemory(shm)", d.MapMemory(r.device, b.memory, 0, vk.DeviceSize(size), 0, &b.mapped)); err != nil {
131+ return err
132+ }
133+ b.size, ok = size, true
134+ return nil
135+}
136+
137+func (r *Renderer) freeGPUBuffer(b *gpuBuffer) {
138+ d := r.dd
139+ if b.mapped != nil {
140+ d.UnmapMemory(r.device, b.memory)
141+ }
142+ if b.buffer != 0 {
143+ d.DestroyBuffer(r.device, b.buffer, nil)
144+ }
145+ if b.memory != 0 {
146+ d.FreeMemory(r.device, b.memory, nil)
147+ }
148+ *b = gpuBuffer{}
149+}
150+
151+// shmKey is a surface of a window: index 0 is the root, i+1 its child i.
152+type shmKey struct {
153+ win ports.WindowID
154+ index int
155+}
156+
157+// shmState is what a GPU buffer holds: a client buffer at a content Seq.
158+type shmState struct {
159+ pool uint64
160+ offset, stride int
161+ w, h int
162+ seq uint64
163+}
164+
165+// shmCopy is one of a surface's two GPU buffers and the set binding it.
166+type shmCopy struct {
167+ gpu gpuBuffer
168+ pool vk.DescriptorPool
169+ set vk.DescriptorSet
170+ holds shmState
171+ valid bool
172+}
173+
174+// shmSurface is the GPU copy of one surface's wl_shm content.
175+type shmSurface struct {
176+ bufs [2]*shmCopy
177+ last uint64 // frame that drew it
178+}
179+
180+// shmCopyFor returns the GPU buffer holding a surface's content st, copying
181+// the client pixels into one when no buffer holds them yet.
182+func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte) (*shmCopy, error) {
183+ s := r.shm[key]
184+ if s == nil {
185+ s = &shmSurface{}
186+ r.shm[key] = s
187+ }
188+ s.last = r.frame
189+ for _, c := range s.bufs {
190+ if c != nil && c.valid && c.holds == st {
191+ c.gpu.last = r.frame
192+ return c, nil
193+ }
194+ }
195+ // The older buffer takes the new content.
196+ i := 0
197+ if s.bufs[0] != nil && (s.bufs[1] == nil || s.bufs[1].gpu.last < s.bufs[0].gpu.last) {
198+ i = 1
199+ }
200+ size := st.w * st.h * 4
201+ if size > r.maxRange {
202+ return nil, fmt.Errorf("shm buffer of %d bytes past the storage buffer range", size)
203+ }
204+ c := s.bufs[i]
205+ if c != nil && c.gpu.size != size {
206+ r.retire(c.gpu.last, func() { r.freeShmCopy(c) })
207+ s.bufs[i], c = nil, nil
208+ }
209+ if c == nil {
210+ c = &shmCopy{}
211+ if err := r.newGPUBuffer(&c.gpu, size); err != nil {
212+ return nil, err
213+ }
214+ var err error
215+ if c.pool, c.set, err = r.newSet(r.compose.dummyView, c.gpu.buffer); err != nil {
216+ r.freeGPUBuffer(&c.gpu)
217+ return nil, err
218+ }
219+ s.bufs[i] = c
220+ }
221+ if err := r.waitFrame(c.gpu.last); err != nil {
222+ return nil, err
223+ }
224+ dst := unsafe.Slice((*byte)(c.gpu.mapped), size)
225+ row := st.w * 4
226+ // The client may shrink its pool under the mapping: its window then
227+ // shows garbage for this frame, never a crash, and is copied again.
228+ c.valid = copyGuarded(func() {
229+ for y := range st.h {
230+ copy(dst[y*row:(y+1)*row], pixels[st.offset+y*st.stride:])
231+ }
232+ })
233+ r.copied += size
234+ c.holds, c.gpu.last = st, r.frame
235+ return c, nil
236+}
237+
238+func (r *Renderer) freeShmCopy(c *shmCopy) {
239+ if c.pool != 0 {
240+ r.dd.DestroyDescriptorPool(r.device, c.pool, nil)
241+ c.pool = 0
242+ }
243+ r.freeGPUBuffer(&c.gpu)
244+}
245+
246+// dropShm frees the GPU buffers of surfaces not drawn for importTTL frames.
247+func (r *Renderer) dropShm() {
248+ for key, s := range r.shm {
249+ if r.frame-s.last > importTTL {
250+ for _, c := range s.bufs {
251+ if c != nil {
252+ r.retire(c.gpu.last, func() { r.freeShmCopy(c) })
253+ }
254+ }
255+ delete(r.shm, key)
256+ }
257+ }
258+}
M internal/adapters/vulkan/targets.go +8 -14
@@ -105,9 +105,8 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
105105 mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
106106 list.DrmFormatModifierProperties = &mods[0]
107107 r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
108- need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit)
109- // Targets are drawn on too (blend.go): a transfer-only modifier is
110- // not a target, and without any the output cannot start.
108+ need := vk.FormatFeatureFlags(formatFeatureTransferSrc | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit)
109+ // Targets are drawn on (compose.go) and read back for screenshots.
111110 for _, m := range mods[:list.DrmFormatModifierCount] {
112111 if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) {
113112 r.renderMods = append(r.renderMods, m.DrmFormatModifier)
@@ -127,18 +126,12 @@ func (r *Renderer) exportable(physical vk.PhysicalDevice, modifier uint64) bool
127126 return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0
128127 }
129128
130-// VK_FORMAT_FEATURE_TRANSFER_SRC_BIT / _DST_BIT (Vulkan 1.1), missing from
131-// the generated bindings.
132-const (
133- formatFeatureTransferSrc = 1 << 14
134- formatFeatureTransferDst = 1 << 15
135-)
136-
137-// Targets are copied into and drawn on (blend.go).
138-const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit | vk.ImageUsageColorAttachmentBit
129+// VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the
130+// generated bindings.
131+const formatFeatureTransferSrc = 1 << 14
139132
140-// Staging feeds copies and, as a storage buffer, the blend shaders.
141-const stagingUsage = vk.BufferUsageTransferSrcBit | vk.BufferUsageStorageBufferBit
133+// Targets are drawn on (compose.go) and copied from for screenshots.
134+const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageColorAttachmentBit
142135
143136 // exportTarget creates one exported image with a modifier from mods.
144137 func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
@@ -213,6 +206,7 @@ func (r *Renderer) freeTarget(t *target) {
213206 func (r *Renderer) dropTargets() {
214207 if len(r.targets) > 0 && r.device != 0 {
215208 _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
209+ r.idle()
216210 }
217211 for _, t := range r.targets {
218212 if r.last == t {
@@ -105,9 +105,8 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
105 mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)105 mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
106 list.DrmFormatModifierProperties = &mods[0]106 list.DrmFormatModifierProperties = &mods[0]
107 r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)107 r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
108- need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit)108+ need := vk.FormatFeatureFlags(formatFeatureTransferSrc | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit)
109- // Targets are drawn on too (blend.go): a transfer-only modifier is109+ // Targets are drawn on (compose.go) and read back for screenshots.
110- // not a target, and without any the output cannot start.
111 for _, m := range mods[:list.DrmFormatModifierCount] {110 for _, m := range mods[:list.DrmFormatModifierCount] {
112 if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) {111 if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) {
113 r.renderMods = append(r.renderMods, m.DrmFormatModifier)112 r.renderMods = append(r.renderMods, m.DrmFormatModifier)
@@ -127,18 +126,12 @@ func (r *Renderer) exportable(physical vk.PhysicalDevice, modifier uint64) bool
127 return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0126 return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0
128 }127 }
129 128
130-// VK_FORMAT_FEATURE_TRANSFER_SRC_BIT / _DST_BIT (Vulkan 1.1), missing from129+// VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the
131-// the generated bindings.130+// generated bindings.
132-const (131+const formatFeatureTransferSrc = 1 << 14
133- formatFeatureTransferSrc = 1 << 14
134- formatFeatureTransferDst = 1 << 15
135-)
136-
137-// Targets are copied into and drawn on (blend.go).
138-const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit | vk.ImageUsageColorAttachmentBit
139 132
140-// Staging feeds copies and, as a storage buffer, the blend shaders.133+// Targets are drawn on (compose.go) and copied from for screenshots.
141-const stagingUsage = vk.BufferUsageTransferSrcBit | vk.BufferUsageStorageBufferBit134+const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageColorAttachmentBit
142 135
143 // exportTarget creates one exported image with a modifier from mods.136 // exportTarget creates one exported image with a modifier from mods.
144 func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {137 func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
@@ -213,6 +206,7 @@ func (r *Renderer) freeTarget(t *target) {
213 func (r *Renderer) dropTargets() {206 func (r *Renderer) dropTargets() {
214 if len(r.targets) > 0 && r.device != 0 {207 if len(r.targets) > 0 && r.device != 0 {
215 _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))208 _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
209+ r.idle()
216 }210 }
217 for _, t := range r.targets {211 for _, t := range r.targets {
218 if r.last == t {212 if r.last == t {

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 23

Tags 10

↑↓ moveenter openesc close