detached 470e764

perf(vulkan): keep per-frame barrier structs off the heap

commit
470e7640a3af6778e600462617bc21b668f21ff6
author
brice · · 2026-10-13 18:12 UTC
committer
brice · · 2026-10-14 04:34 UTC
parent
4375727
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +112 -31
M Makefile +1 -1
@@ -69,7 +69,7 @@ adapter-imports:
6969 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
7070 # Fast, mandatory guards for tiled content publications and SHM copies.
7171 perf-check:
72- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=1
72+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=1
7373 check: vet test arch fakes-check log-check perf-check
7474
7575 # On-demand SDR/HDR colour check with examples/testpattern on a headless
@@ -69,7 +69,7 @@ adapter-imports:
69 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }69 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
70 # Fast, mandatory guards for tiled content publications and SHM copies.70 # Fast, mandatory guards for tiled content publications and SHM copies.
71 perf-check:71 perf-check:
72- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=172+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=1
73 check: vet test arch fakes-check log-check perf-check73 check: vet test arch fakes-check log-check perf-check
74 74
75 # On-demand SDR/HDR colour check with examples/testpattern on a headless75 # On-demand SDR/HDR colour check with examples/testpattern on a headless
M internal/adapters/vulkan/compose.go +4 -3
@@ -413,12 +413,13 @@ func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]ui
413413 clear[i] = math.Float32bits(float32(srgbToLinear(float64(bg[i]) / 255)))
414414 }
415415 }
416- attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpClear, StoreOp: vk.AttachmentStoreOpStore, ClearValue: clear}
416+ attachment, info := &r.call.attach, &r.call.render
417+ *attachment = vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpClear, StoreOp: vk.AttachmentStoreOpStore, ClearValue: clear}
417418 if keep {
418419 attachment.LoadOp = vk.AttachmentLoadOpLoad
419420 }
420- 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}
421- d.CmdBeginRendering(cmd, &info)
421+ *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}
422+ d.CmdBeginRendering(cmd, info)
422423 pipeline := c.pipeline
423424 if r.hdrNits > 0 {
424425 pipeline = c.hdrPipeline
@@ -413,12 +413,13 @@ func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]ui
413 clear[i] = math.Float32bits(float32(srgbToLinear(float64(bg[i]) / 255)))413 clear[i] = math.Float32bits(float32(srgbToLinear(float64(bg[i]) / 255)))
414 }414 }
415 }415 }
416- attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpClear, StoreOp: vk.AttachmentStoreOpStore, ClearValue: clear}416+ attachment, info := &r.call.attach, &r.call.render
417+ *attachment = vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpClear, StoreOp: vk.AttachmentStoreOpStore, ClearValue: clear}
417 if keep {418 if keep {
418 attachment.LoadOp = vk.AttachmentLoadOpLoad419 attachment.LoadOp = vk.AttachmentLoadOpLoad
419 }420 }
420- 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}421+ *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}
421- d.CmdBeginRendering(cmd, &info)422+ d.CmdBeginRendering(cmd, info)
422 pipeline := c.pipeline423 pipeline := c.pipeline
423 if r.hdrNits > 0 {424 if r.hdrNits > 0 {
424 pipeline = c.hdrPipeline425 pipeline = c.hdrPipeline
M internal/adapters/vulkan/frame.go +41 -14
@@ -32,6 +32,27 @@ type frameSlot struct {
3232 busy bool // submitted, fence not yet seen signalled
3333 }
3434
35+// callScratch holds the structs Render and its record helpers pass to Vulkan
36+// by pointer. The purego call makes a pointer to a local escape, so a local
37+// would be a heap allocation per frame; these live in the Renderer instead
38+// (owned by one goroutine, and Vulkan reads each only during its call). Every
39+// field is set in full before use; two uses that overlap in time never share a
40+// field.
41+type callScratch struct {
42+ cmd vk.CommandBuffer
43+ begin vk.CommandBufferBeginInfo
44+ submit vk.SubmitInfo
45+ target vk.ImageMemoryBarrier // the render target's, kept to the frame's end
46+ client vk.ImageMemoryBarrier // a client image's (ownership)
47+ attach vk.RenderingAttachmentInfo
48+ render vk.RenderingInfo
49+ getFD vk.SemaphoreGetFdInfoKHR
50+ fd int32
51+ hdrIn vk.ImageMemoryBarrier // recordHDR
52+ hdrOut vk.ImageMemoryBarrier
53+ hdrNits float32
54+}
55+
3556 // deferredFree runs once frame completed.
3657 type deferredFree struct {
3758 frame uint64
@@ -204,17 +225,20 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
204225 }
205226 r.orient(ds, s.Transform)
206227 d := r.dd
228+ sc := &r.call
207229 cmd := slot.cmd
230+ sc.cmd = cmd
208231 if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(cmd, 0)); err != nil {
209232 return nil, err
210233 }
211- begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
212- if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil {
234+ sc.begin = vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
235+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &sc.begin)); err != nil {
213236 return nil, err
214237 }
215238 // A full redraw clears the target: its old contents never matter
216239 // (UNDEFINED). A partial one keeps them.
217- b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: tg.image, SubresourceRange: colorRange}
240+ b := &sc.target
241+ *b = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: tg.image, SubresourceRange: colorRange}
218242 if partial {
219243 b.OldLayout = tg.layout
220244 b.DstAccessMask |= vk.AccessColorAttachmentReadBit
@@ -223,7 +247,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
223247 // Acquire from the display (foreign queue family).
224248 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
225249 }
226- d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit|vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)
250+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit|vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, b)
227251 // Client buffers come from the foreign queue family (their driver)
228252 // and go back to it after the frame, so the next frame acquires them.
229253 dmas := r.frameDMAs[:0]
@@ -255,10 +279,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
255279 // Release to the display: KMS scans it out after the fence.
256280 b.NewLayout, b.DstAccessMask = vk.ImageLayoutGeneral, 0
257281 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
258- d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
282+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, b)
259283 } else {
260284 b.NewLayout, b.DstAccessMask = vk.ImageLayoutTransferSrcOptimal, vk.AccessTransferReadBit
261- d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
285+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, b)
262286 }
263287 }
264288 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
@@ -282,7 +306,8 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
282306 sem := sem
283307 r.retire(frame, func() { d.DestroySemaphore(r.device, sem, nil) })
284308 }
285- submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &cmd}
309+ sc.submit = vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &sc.cmd}
310+ submit := &sc.submit
286311 if cap(r.frameStages) < len(waits) {
287312 r.frameStages = make([]vk.PipelineStageFlags, len(waits))
288313 }
@@ -296,7 +321,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
296321 if r.syncFD {
297322 submit.SignalSemaphoreCount, submit.SignalSemaphores = 1, &slot.done
298323 }
299- if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, slot.fence)); err != nil {
324+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, submit, slot.fence)); err != nil {
300325 return nil, err
301326 }
302327 runtime.KeepAlive(stages)
@@ -321,9 +346,9 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
321346 if !r.syncFD {
322347 return nil, r.waitFrame(frame)
323348 }
324- var fd int32
325- get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit}
326- if d.GetSemaphoreFdKHR(r.device, &get, &fd) != vk.Success {
349+ sc.fd = 0
350+ sc.getFD = vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit}
351+ if d.GetSemaphoreFdKHR(r.device, &sc.getFD, &sc.fd) != vk.Success {
327352 // The frame is drawn: finish it on the CPU. The semaphore stays
328353 // signalled: replace it, so the slot's next submit signals a
329354 // fresh one.
@@ -334,6 +359,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
334359 slot.done = 0
335360 return nil, r.newDone(slot)
336361 }
362+ fd := sc.fd
337363 if fd < 0 {
338364 // Already signalled: the driver may return -1.
339365 return nil, r.waitFrame(frame)
@@ -349,7 +375,8 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
349375 // family, GENERAL: their contents survive) and the fragment shader.
350376 func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire bool) {
351377 for _, im := range dmas {
352- b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: colorRange}
378+ b := &r.call.client
379+ *b = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: colorRange}
353380 if im.yuv {
354381 b.SubresourceRange.AspectMask = aspectPlane0 | aspectPlane1
355382 }
@@ -359,12 +386,12 @@ func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire boo
359386 b.DstAccessMask = vk.AccessShaderReadBit
360387 // The frame waits on the client's fence at the fragment
361388 // stage: the transfer must come after that wait.
362- r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)
389+ r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, b)
363390 } else {
364391 b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral
365392 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
366393 b.SrcAccessMask = vk.AccessShaderReadBit
367- r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
394+ r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, b)
368395 }
369396 }
370397 }
@@ -32,6 +32,27 @@ type frameSlot struct {
32 busy bool // submitted, fence not yet seen signalled32 busy bool // submitted, fence not yet seen signalled
33 }33 }
34 34
35+// callScratch holds the structs Render and its record helpers pass to Vulkan
36+// by pointer. The purego call makes a pointer to a local escape, so a local
37+// would be a heap allocation per frame; these live in the Renderer instead
38+// (owned by one goroutine, and Vulkan reads each only during its call). Every
39+// field is set in full before use; two uses that overlap in time never share a
40+// field.
41+type callScratch struct {
42+ cmd vk.CommandBuffer
43+ begin vk.CommandBufferBeginInfo
44+ submit vk.SubmitInfo
45+ target vk.ImageMemoryBarrier // the render target's, kept to the frame's end
46+ client vk.ImageMemoryBarrier // a client image's (ownership)
47+ attach vk.RenderingAttachmentInfo
48+ render vk.RenderingInfo
49+ getFD vk.SemaphoreGetFdInfoKHR
50+ fd int32
51+ hdrIn vk.ImageMemoryBarrier // recordHDR
52+ hdrOut vk.ImageMemoryBarrier
53+ hdrNits float32
54+}
55+
35 // deferredFree runs once frame completed.56 // deferredFree runs once frame completed.
36 type deferredFree struct {57 type deferredFree struct {
37 frame uint6458 frame uint64
@@ -204,17 +225,20 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
204 }225 }
205 r.orient(ds, s.Transform)226 r.orient(ds, s.Transform)
206 d := r.dd227 d := r.dd
228+ sc := &r.call
207 cmd := slot.cmd229 cmd := slot.cmd
230+ sc.cmd = cmd
208 if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(cmd, 0)); err != nil {231 if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(cmd, 0)); err != nil {
209 return nil, err232 return nil, err
210 }233 }
211- begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}234+ sc.begin = vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
212- if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil {235+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &sc.begin)); err != nil {
213 return nil, err236 return nil, err
214 }237 }
215 // A full redraw clears the target: its old contents never matter238 // A full redraw clears the target: its old contents never matter
216 // (UNDEFINED). A partial one keeps them.239 // (UNDEFINED). A partial one keeps them.
217- b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: tg.image, SubresourceRange: colorRange}240+ b := &sc.target
241+ *b = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: tg.image, SubresourceRange: colorRange}
218 if partial {242 if partial {
219 b.OldLayout = tg.layout243 b.OldLayout = tg.layout
220 b.DstAccessMask |= vk.AccessColorAttachmentReadBit244 b.DstAccessMask |= vk.AccessColorAttachmentReadBit
@@ -223,7 +247,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
223 // Acquire from the display (foreign queue family).247 // Acquire from the display (foreign queue family).
224 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family248 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
225 }249 }
226- d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit|vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)250+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit|vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, b)
227 // Client buffers come from the foreign queue family (their driver)251 // Client buffers come from the foreign queue family (their driver)
228 // and go back to it after the frame, so the next frame acquires them.252 // and go back to it after the frame, so the next frame acquires them.
229 dmas := r.frameDMAs[:0]253 dmas := r.frameDMAs[:0]
@@ -255,10 +279,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
255 // Release to the display: KMS scans it out after the fence.279 // Release to the display: KMS scans it out after the fence.
256 b.NewLayout, b.DstAccessMask = vk.ImageLayoutGeneral, 0280 b.NewLayout, b.DstAccessMask = vk.ImageLayoutGeneral, 0
257 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT281 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
258- d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)282+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, b)
259 } else {283 } else {
260 b.NewLayout, b.DstAccessMask = vk.ImageLayoutTransferSrcOptimal, vk.AccessTransferReadBit284 b.NewLayout, b.DstAccessMask = vk.ImageLayoutTransferSrcOptimal, vk.AccessTransferReadBit
261- d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)285+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, b)
262 }286 }
263 }287 }
264 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {288 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
@@ -282,7 +306,8 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
282 sem := sem306 sem := sem
283 r.retire(frame, func() { d.DestroySemaphore(r.device, sem, nil) })307 r.retire(frame, func() { d.DestroySemaphore(r.device, sem, nil) })
284 }308 }
285- submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &cmd}309+ sc.submit = vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &sc.cmd}
310+ submit := &sc.submit
286 if cap(r.frameStages) < len(waits) {311 if cap(r.frameStages) < len(waits) {
287 r.frameStages = make([]vk.PipelineStageFlags, len(waits))312 r.frameStages = make([]vk.PipelineStageFlags, len(waits))
288 }313 }
@@ -296,7 +321,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
296 if r.syncFD {321 if r.syncFD {
297 submit.SignalSemaphoreCount, submit.SignalSemaphores = 1, &slot.done322 submit.SignalSemaphoreCount, submit.SignalSemaphores = 1, &slot.done
298 }323 }
299- if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, slot.fence)); err != nil {324+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, submit, slot.fence)); err != nil {
300 return nil, err325 return nil, err
301 }326 }
302 runtime.KeepAlive(stages)327 runtime.KeepAlive(stages)
@@ -321,9 +346,9 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
321 if !r.syncFD {346 if !r.syncFD {
322 return nil, r.waitFrame(frame)347 return nil, r.waitFrame(frame)
323 }348 }
324- var fd int32349+ sc.fd = 0
325- get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit}350+ sc.getFD = vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit}
326- if d.GetSemaphoreFdKHR(r.device, &get, &fd) != vk.Success {351+ if d.GetSemaphoreFdKHR(r.device, &sc.getFD, &sc.fd) != vk.Success {
327 // The frame is drawn: finish it on the CPU. The semaphore stays352 // The frame is drawn: finish it on the CPU. The semaphore stays
328 // signalled: replace it, so the slot's next submit signals a353 // signalled: replace it, so the slot's next submit signals a
329 // fresh one.354 // fresh one.
@@ -334,6 +359,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
334 slot.done = 0359 slot.done = 0
335 return nil, r.newDone(slot)360 return nil, r.newDone(slot)
336 }361 }
362+ fd := sc.fd
337 if fd < 0 {363 if fd < 0 {
338 // Already signalled: the driver may return -1.364 // Already signalled: the driver may return -1.
339 return nil, r.waitFrame(frame)365 return nil, r.waitFrame(frame)
@@ -349,7 +375,8 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
349 // family, GENERAL: their contents survive) and the fragment shader.375 // family, GENERAL: their contents survive) and the fragment shader.
350 func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire bool) {376 func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire bool) {
351 for _, im := range dmas {377 for _, im := range dmas {
352- b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: colorRange}378+ b := &r.call.client
379+ *b = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: colorRange}
353 if im.yuv {380 if im.yuv {
354 b.SubresourceRange.AspectMask = aspectPlane0 | aspectPlane1381 b.SubresourceRange.AspectMask = aspectPlane0 | aspectPlane1
355 }382 }
@@ -359,12 +386,12 @@ func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire boo
359 b.DstAccessMask = vk.AccessShaderReadBit386 b.DstAccessMask = vk.AccessShaderReadBit
360 // The frame waits on the client's fence at the fragment387 // The frame waits on the client's fence at the fragment
361 // stage: the transfer must come after that wait.388 // stage: the transfer must come after that wait.
362- r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)389+ r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, b)
363 } else {390 } else {
364 b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral391 b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral
365 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT392 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
366 b.SrcAccessMask = vk.AccessShaderReadBit393 b.SrcAccessMask = vk.AccessShaderReadBit
367- r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)394+ r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, b)
368 }395 }
369 }396 }
370 }397 }
M internal/adapters/vulkan/hdr.go +13 -11
@@ -172,26 +172,28 @@ func (r *Renderer) destroyHDR() {
172172 // recordHDR converts the fully composed linear HDR image in the same command buffer.
173173 func (r *Renderer) recordHDR(cmd vk.CommandBuffer, out *target) {
174174 d, h := r.dd, &r.hdr
175- in := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessColorAttachmentWriteBit, DstAccessMask: vk.AccessShaderReadBit, OldLayout: vk.ImageLayoutColorAttachmentOptimal, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: r.hdrOwn.image, SubresourceRange: colorRange}
176- d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &in)
177- b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: vk.QueueFamilyForeignEXT, DstQueueFamilyIndex: r.family, Image: out.image, SubresourceRange: colorRange}
178- d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)
179- attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: out.view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpDontCare, StoreOp: vk.AttachmentStoreOpStore}
180- 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}
181- d.CmdBeginRendering(cmd, &info)
175+ in, b := &r.call.hdrIn, &r.call.hdrOut
176+ attachment, info := &r.call.attach, &r.call.render
177+ *in = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessColorAttachmentWriteBit, DstAccessMask: vk.AccessShaderReadBit, OldLayout: vk.ImageLayoutColorAttachmentOptimal, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: r.hdrOwn.image, SubresourceRange: colorRange}
178+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, in)
179+ *b = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: vk.QueueFamilyForeignEXT, DstQueueFamilyIndex: r.family, Image: out.image, SubresourceRange: colorRange}
180+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, b)
181+ *attachment = vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: out.view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpDontCare, StoreOp: vk.AttachmentStoreOpStore}
182+ *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}
183+ d.CmdBeginRendering(cmd, info)
182184 d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, h.pipeline)
183185 d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, h.layout, 0, 1, &h.set, 0, nil)
184- nits := float32(r.hdrNits)
185- d.CmdPushConstants(cmd, h.layout, vk.ShaderStageFragmentBit, 0, 4, unsafe.Pointer(&nits))
186+ r.call.hdrNits = float32(r.hdrNits)
187+ d.CmdPushConstants(cmd, h.layout, vk.ShaderStageFragmentBit, 0, 4, unsafe.Pointer(&r.call.hdrNits))
186188 d.CmdDraw(cmd, 3, 1, 0, 0)
187189 d.CmdEndRendering(cmd)
188190 b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, 0
189191 b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutGeneral
190192 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
191- d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
193+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, b)
192194 in.SrcAccessMask, in.DstAccessMask = vk.AccessShaderReadBit, vk.AccessTransferReadBit
193195 in.OldLayout, in.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutTransferSrcOptimal
194- d.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &in)
196+ d.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, in)
195197 }
196198
197199 // dropHDROwn frees the HDR composition image with the capture pass that
@@ -172,26 +172,28 @@ func (r *Renderer) destroyHDR() {
172 // recordHDR converts the fully composed linear HDR image in the same command buffer.172 // recordHDR converts the fully composed linear HDR image in the same command buffer.
173 func (r *Renderer) recordHDR(cmd vk.CommandBuffer, out *target) {173 func (r *Renderer) recordHDR(cmd vk.CommandBuffer, out *target) {
174 d, h := r.dd, &r.hdr174 d, h := r.dd, &r.hdr
175- in := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessColorAttachmentWriteBit, DstAccessMask: vk.AccessShaderReadBit, OldLayout: vk.ImageLayoutColorAttachmentOptimal, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: r.hdrOwn.image, SubresourceRange: colorRange}175+ in, b := &r.call.hdrIn, &r.call.hdrOut
176- d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &in)176+ attachment, info := &r.call.attach, &r.call.render
177- b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: vk.QueueFamilyForeignEXT, DstQueueFamilyIndex: r.family, Image: out.image, SubresourceRange: colorRange}177+ *in = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessColorAttachmentWriteBit, DstAccessMask: vk.AccessShaderReadBit, OldLayout: vk.ImageLayoutColorAttachmentOptimal, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: r.hdrOwn.image, SubresourceRange: colorRange}
178- d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)178+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, in)
179- attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: out.view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpDontCare, StoreOp: vk.AttachmentStoreOpStore}179+ *b = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: vk.QueueFamilyForeignEXT, DstQueueFamilyIndex: r.family, Image: out.image, SubresourceRange: colorRange}
180- 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}180+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, b)
181- d.CmdBeginRendering(cmd, &info)181+ *attachment = vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: out.view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpDontCare, StoreOp: vk.AttachmentStoreOpStore}
182+ *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}
183+ d.CmdBeginRendering(cmd, info)
182 d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, h.pipeline)184 d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, h.pipeline)
183 d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, h.layout, 0, 1, &h.set, 0, nil)185 d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, h.layout, 0, 1, &h.set, 0, nil)
184- nits := float32(r.hdrNits)186+ r.call.hdrNits = float32(r.hdrNits)
185- d.CmdPushConstants(cmd, h.layout, vk.ShaderStageFragmentBit, 0, 4, unsafe.Pointer(&nits))187+ d.CmdPushConstants(cmd, h.layout, vk.ShaderStageFragmentBit, 0, 4, unsafe.Pointer(&r.call.hdrNits))
186 d.CmdDraw(cmd, 3, 1, 0, 0)188 d.CmdDraw(cmd, 3, 1, 0, 0)
187 d.CmdEndRendering(cmd)189 d.CmdEndRendering(cmd)
188 b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, 0190 b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, 0
189 b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutGeneral191 b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutGeneral
190 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT192 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
191- d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)193+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, b)
192 in.SrcAccessMask, in.DstAccessMask = vk.AccessShaderReadBit, vk.AccessTransferReadBit194 in.SrcAccessMask, in.DstAccessMask = vk.AccessShaderReadBit, vk.AccessTransferReadBit
193 in.OldLayout, in.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutTransferSrcOptimal195 in.OldLayout, in.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutTransferSrcOptimal
194- d.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &in)196+ d.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, in)
195 }197 }
196 198
197 // dropHDROwn frees the HDR composition image with the capture pass that199 // dropHDROwn frees the HDR composition image with the capture pass that
M internal/adapters/vulkan/renderer.go +2 -0
@@ -110,6 +110,8 @@ type Renderer struct {
110110 frameAcquires map[*imported]*os.File
111111 frameWaits []vk.Semaphore
112112 frameStages []vk.PipelineStageFlags
113+ // call holds the structs a per-frame Vulkan call is handed by pointer.
114+ call callScratch
113115 }
114116
115117 // priorityAttempts falls back to an ordinary queue when elevated priority is unavailable.
@@ -110,6 +110,8 @@ type Renderer struct {
110 frameAcquires map[*imported]*os.File110 frameAcquires map[*imported]*os.File
111 frameWaits []vk.Semaphore111 frameWaits []vk.Semaphore
112 frameStages []vk.PipelineStageFlags112 frameStages []vk.PipelineStageFlags
113+ // call holds the structs a per-frame Vulkan call is handed by pointer.
114+ call callScratch
113 }115 }
114 116
115 // priorityAttempts falls back to an ordinary queue when elevated priority is unavailable.117 // priorityAttempts falls back to an ordinary queue when elevated priority is unavailable.
M internal/adapters/vulkan/scene_perf_test.go +51 -2
@@ -3,6 +3,7 @@ package vulkan
33 import (
44 "image"
55 "image/color"
6+ "slices"
67 "testing"
78
89 "github.com/bnema/neferwl/internal/ports"
@@ -40,8 +41,8 @@ func TestRenderSteadyStateAllocations(t *testing.T) {
4041 }
4142 }
4243 frame()
43- if allocs := testing.AllocsPerRun(20, frame); allocs > 13 {
44- t.Errorf("small steady-state Render: %.1f allocs/frame, want <=13", allocs)
44+ if allocs := testing.AllocsPerRun(20, frame); allocs > 3 {
45+ t.Errorf("small steady-state Render: %.1f allocs/frame, want <=3", allocs)
4546 } else {
4647 t.Logf("small steady-state Render: %.1f allocs/frame", allocs)
4748 }
@@ -136,6 +137,54 @@ func TestRenderSteadyStateAllocations(t *testing.T) {
136137 }
137138 }
138139
140+// A steady frame that draws a client dmabuf also records the ownership
141+// barriers (acquire and release) of its image: no allocation per frame beyond
142+// the SHM scene's. Skipped without a GPU or /dev/udmabuf.
143+func TestRenderSteadyStateDMABufAllocations(t *testing.T) {
144+ r, err := New(80, 32)
145+ if err != nil {
146+ t.Skipf("Vulkan unavailable: %v", err)
147+ }
148+ defer r.Close()
149+ linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
150+ if !slices.Contains(r.DMABuf().Formats, linear) {
151+ t.Skipf("linear ARGB8888 not importable")
152+ }
153+ f := udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{0, 0, 255, 255} })
154+ buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
155+ scene := ports.Scene{Seq: 1, Background: "#000000", Scale: 1, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}}
156+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Surface: 1, Seq: 1, Version: 1, Width: 64, Height: 16, DMABuf: buf}}
157+ frame := func() {
158+ if err := render(r, scene, contents); err != nil {
159+ t.Fatal(err)
160+ }
161+ }
162+ frame()
163+ if len(r.imports) != 1 {
164+ t.Fatalf("imports %d: the frame does not take the dmabuf path", len(r.imports))
165+ }
166+ // Each new scene Seq draws the dmabuf again, with its barriers.
167+ var moving [4]ports.Scene
168+ for i := range moving {
169+ moving[i] = scene
170+ moving[i].Seq = uint64(10 + i)
171+ moving[i].Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4 + i, Y: 4, W: 64, H: 16}}}
172+ }
173+ n := 0
174+ moved := func() {
175+ n++
176+ if err := render(r, moving[n%len(moving)], contents); err != nil {
177+ t.Fatal(err)
178+ }
179+ }
180+ moved()
181+ allocs := testing.AllocsPerRun(20, moved)
182+ t.Logf("dmabuf Render: %.1f allocs/frame", allocs)
183+ if allocs > 7 {
184+ t.Errorf("dmabuf steady-state Render: %.1f allocs/frame, want <=7", allocs)
185+ }
186+}
187+
139188 // TestSceneWalkUnchangedTiledSHM guards the GPU copy budget of an unchanged
140189 // subsurface tree. Each child has its own real wl_shm pool.
141190 func TestSceneWalkUnchangedTiledSHM(t *testing.T) {
@@ -3,6 +3,7 @@ package vulkan
3 import (3 import (
4 "image"4 "image"
5 "image/color"5 "image/color"
6+ "slices"
6 "testing"7 "testing"
7 8
8 "github.com/bnema/neferwl/internal/ports"9 "github.com/bnema/neferwl/internal/ports"
@@ -40,8 +41,8 @@ func TestRenderSteadyStateAllocations(t *testing.T) {
40 }41 }
41 }42 }
42 frame()43 frame()
43- if allocs := testing.AllocsPerRun(20, frame); allocs > 13 {44+ if allocs := testing.AllocsPerRun(20, frame); allocs > 3 {
44- t.Errorf("small steady-state Render: %.1f allocs/frame, want <=13", allocs)45+ t.Errorf("small steady-state Render: %.1f allocs/frame, want <=3", allocs)
45 } else {46 } else {
46 t.Logf("small steady-state Render: %.1f allocs/frame", allocs)47 t.Logf("small steady-state Render: %.1f allocs/frame", allocs)
47 }48 }
@@ -136,6 +137,54 @@ func TestRenderSteadyStateAllocations(t *testing.T) {
136 }137 }
137 }138 }
138 139
140+// A steady frame that draws a client dmabuf also records the ownership
141+// barriers (acquire and release) of its image: no allocation per frame beyond
142+// the SHM scene's. Skipped without a GPU or /dev/udmabuf.
143+func TestRenderSteadyStateDMABufAllocations(t *testing.T) {
144+ r, err := New(80, 32)
145+ if err != nil {
146+ t.Skipf("Vulkan unavailable: %v", err)
147+ }
148+ defer r.Close()
149+ linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
150+ if !slices.Contains(r.DMABuf().Formats, linear) {
151+ t.Skipf("linear ARGB8888 not importable")
152+ }
153+ f := udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{0, 0, 255, 255} })
154+ buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
155+ scene := ports.Scene{Seq: 1, Background: "#000000", Scale: 1, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}}
156+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Surface: 1, Seq: 1, Version: 1, Width: 64, Height: 16, DMABuf: buf}}
157+ frame := func() {
158+ if err := render(r, scene, contents); err != nil {
159+ t.Fatal(err)
160+ }
161+ }
162+ frame()
163+ if len(r.imports) != 1 {
164+ t.Fatalf("imports %d: the frame does not take the dmabuf path", len(r.imports))
165+ }
166+ // Each new scene Seq draws the dmabuf again, with its barriers.
167+ var moving [4]ports.Scene
168+ for i := range moving {
169+ moving[i] = scene
170+ moving[i].Seq = uint64(10 + i)
171+ moving[i].Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4 + i, Y: 4, W: 64, H: 16}}}
172+ }
173+ n := 0
174+ moved := func() {
175+ n++
176+ if err := render(r, moving[n%len(moving)], contents); err != nil {
177+ t.Fatal(err)
178+ }
179+ }
180+ moved()
181+ allocs := testing.AllocsPerRun(20, moved)
182+ t.Logf("dmabuf Render: %.1f allocs/frame", allocs)
183+ if allocs > 7 {
184+ t.Errorf("dmabuf steady-state Render: %.1f allocs/frame, want <=7", allocs)
185+ }
186+}
187+
139 // TestSceneWalkUnchangedTiledSHM guards the GPU copy budget of an unchanged188 // TestSceneWalkUnchangedTiledSHM guards the GPU copy budget of an unchanged
140 // subsurface tree. Each child has its own real wl_shm pool.189 // subsurface tree. Each child has its own real wl_shm pool.
141 func TestSceneWalkUnchangedTiledSHM(t *testing.T) {190 func TestSceneWalkUnchangedTiledSHM(t *testing.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 25

Tags 10

↑↓ moveenter openesc close