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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +112 -31
M Makefile +1 -1
| @@ -69,7 +69,7 @@ adapter-imports: | |||
| 69 | 69 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | |
| 70 | 70 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 71 | 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=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 | |
| 73 | 73 | check: vet test arch fakes-check log-check perf-check | |
| 74 | 74 | ||
| 75 | 75 | # 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=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 |
| 73 | check: vet test arch fakes-check log-check perf-check | 73 | check: vet test arch fakes-check log-check perf-check | ||
| 74 | 74 | ||||
| 75 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | 75 | # 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 | |||
| 413 | 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 | 418 | if keep { | |
| 418 | 419 | 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 | - | 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) | |
| 422 | 423 | pipeline := c.pipeline | |
| 423 | 424 | if r.hdrNits > 0 { | |
| 424 | 425 | 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.AttachmentLoadOpLoad | 419 | 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.pipeline | 423 | pipeline := c.pipeline | ||
| 423 | if r.hdrNits > 0 { | 424 | if r.hdrNits > 0 { | ||
| 424 | pipeline = c.hdrPipeline | 425 | pipeline = c.hdrPipeline | ||
M internal/adapters/vulkan/frame.go +41 -14
| @@ -32,6 +32,27 @@ type frameSlot struct { | |||
| 32 | 32 | 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 | 56 | // deferredFree runs once frame completed. | |
| 36 | 57 | type deferredFree struct { | |
| 37 | 58 | frame uint64 | |
| @@ -204,17 +225,20 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 204 | 225 | } | |
| 205 | 226 | r.orient(ds, s.Transform) | |
| 206 | 227 | d := r.dd | |
| 228 | + | sc := &r.call | |
| 207 | 229 | cmd := slot.cmd | |
| 230 | + | sc.cmd = cmd | |
| 208 | 231 | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(cmd, 0)); err != nil { | |
| 209 | 232 | return nil, err | |
| 210 | 233 | } | |
| 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 { | |
| 213 | 236 | return nil, err | |
| 214 | 237 | } | |
| 215 | 238 | // A full redraw clears the target: its old contents never matter | |
| 216 | 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 | 242 | if partial { | |
| 219 | 243 | b.OldLayout = tg.layout | |
| 220 | 244 | b.DstAccessMask |= vk.AccessColorAttachmentReadBit | |
| @@ -223,7 +247,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 223 | 247 | // Acquire from the display (foreign queue family). | |
| 224 | 248 | 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 | 251 | // Client buffers come from the foreign queue family (their driver) | |
| 228 | 252 | // and go back to it after the frame, so the next frame acquires them. | |
| 229 | 253 | dmas := r.frameDMAs[:0] | |
| @@ -255,10 +279,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 255 | 279 | // Release to the display: KMS scans it out after the fence. | |
| 256 | 280 | b.NewLayout, b.DstAccessMask = vk.ImageLayoutGeneral, 0 | |
| 257 | 281 | 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 | 283 | } else { | |
| 260 | 284 | 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 | 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 | 306 | sem := sem | |
| 283 | 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 | 311 | if cap(r.frameStages) < len(waits) { | |
| 287 | 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 | 321 | if r.syncFD { | |
| 297 | 322 | 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 | 325 | return nil, err | |
| 301 | 326 | } | |
| 302 | 327 | runtime.KeepAlive(stages) | |
| @@ -321,9 +346,9 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 321 | 346 | if !r.syncFD { | |
| 322 | 347 | return nil, r.waitFrame(frame) | |
| 323 | 348 | } | |
| 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 { | |
| 327 | 352 | // The frame is drawn: finish it on the CPU. The semaphore stays | |
| 328 | 353 | // signalled: replace it, so the slot's next submit signals a | |
| 329 | 354 | // fresh one. | |
| @@ -334,6 +359,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 334 | 359 | slot.done = 0 | |
| 335 | 360 | return nil, r.newDone(slot) | |
| 336 | 361 | } | |
| 362 | + | fd := sc.fd | |
| 337 | 363 | if fd < 0 { | |
| 338 | 364 | // Already signalled: the driver may return -1. | |
| 339 | 365 | return nil, r.waitFrame(frame) | |
| @@ -349,7 +375,8 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 349 | 375 | // family, GENERAL: their contents survive) and the fragment shader. | |
| 350 | 376 | func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire bool) { | |
| 351 | 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 | 380 | if im.yuv { | |
| 354 | 381 | b.SubresourceRange.AspectMask = aspectPlane0 | aspectPlane1 | |
| 355 | 382 | } | |
| @@ -359,12 +386,12 @@ func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire boo | |||
| 359 | 386 | b.DstAccessMask = vk.AccessShaderReadBit | |
| 360 | 387 | // The frame waits on the client's fence at the fragment | |
| 361 | 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 | 390 | } else { | |
| 364 | 391 | b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral | |
| 365 | 392 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT | |
| 366 | 393 | 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 | } | |
| @@ -32,6 +32,27 @@ type frameSlot struct { | |||||
| 32 | busy bool // submitted, fence not yet seen signalled | 32 | 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 uint64 | 58 | 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.dd | 227 | d := r.dd | ||
| 228 | + | sc := &r.call | |||
| 207 | cmd := slot.cmd | 229 | 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, err | 232 | 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, err | 236 | return nil, err | ||
| 214 | } | 237 | } | ||
| 215 | // A full redraw clears the target: its old contents never matter | 238 | // 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.layout | 243 | b.OldLayout = tg.layout | ||
| 220 | b.DstAccessMask |= vk.AccessColorAttachmentReadBit | 244 | 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.family | 248 | 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, 0 | 280 | b.NewLayout, b.DstAccessMask = vk.ImageLayoutGeneral, 0 | ||
| 257 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT | 281 | 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.AccessTransferReadBit | 284 | 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 := sem | 306 | 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.done | 322 | 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, err | 325 | 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 int32 | 349 | + | 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 stays | 352 | // The frame is drawn: finish it on the CPU. The semaphore stays | ||
| 328 | // signalled: replace it, so the slot's next submit signals a | 353 | // 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 = 0 | 359 | 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 | aspectPlane1 | 381 | 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.AccessShaderReadBit | 386 | b.DstAccessMask = vk.AccessShaderReadBit | ||
| 360 | // The frame waits on the client's fence at the fragment | 387 | // 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.ImageLayoutGeneral | 391 | b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral | ||
| 365 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT | 392 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT | ||
| 366 | b.SrcAccessMask = vk.AccessShaderReadBit | 393 | 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() { | |||
| 172 | 172 | // recordHDR converts the fully composed linear HDR image in the same command buffer. | |
| 173 | 173 | func (r *Renderer) recordHDR(cmd vk.CommandBuffer, out *target) { | |
| 174 | 174 | 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) | |
| 182 | 184 | d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, h.pipeline) | |
| 183 | 185 | 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)) | |
| 186 | 188 | d.CmdDraw(cmd, 3, 1, 0, 0) | |
| 187 | 189 | d.CmdEndRendering(cmd) | |
| 188 | 190 | b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, 0 | |
| 189 | 191 | b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutGeneral | |
| 190 | 192 | 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 | 194 | in.SrcAccessMask, in.DstAccessMask = vk.AccessShaderReadBit, vk.AccessTransferReadBit | |
| 193 | 195 | 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 | 199 | // 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.hdr | 174 | 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, 0 | 190 | b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, 0 | ||
| 189 | b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutGeneral | 191 | b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutGeneral | ||
| 190 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT | 192 | 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.AccessTransferReadBit | 194 | in.SrcAccessMask, in.DstAccessMask = vk.AccessShaderReadBit, vk.AccessTransferReadBit | ||
| 193 | in.OldLayout, in.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutTransferSrcOptimal | 195 | 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 that | 199 | // 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 { | |||
| 110 | 110 | frameAcquires map[*imported]*os.File | |
| 111 | 111 | frameWaits []vk.Semaphore | |
| 112 | 112 | 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 | 117 | // priorityAttempts falls back to an ordinary queue when elevated priority is unavailable. | |
| @@ -110,6 +110,8 @@ type Renderer struct { | |||||
| 110 | frameAcquires map[*imported]*os.File | 110 | frameAcquires map[*imported]*os.File | ||
| 111 | frameWaits []vk.Semaphore | 111 | frameWaits []vk.Semaphore | ||
| 112 | frameStages []vk.PipelineStageFlags | 112 | 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 | |||
| 3 | 3 | import ( | |
| 4 | 4 | "image" | |
| 5 | 5 | "image/color" | |
| 6 | + | "slices" | |
| 6 | 7 | "testing" | |
| 7 | 8 | ||
| 8 | 9 | "github.com/bnema/neferwl/internal/ports" | |
| @@ -40,8 +41,8 @@ func TestRenderSteadyStateAllocations(t *testing.T) { | |||
| 40 | 41 | } | |
| 41 | 42 | } | |
| 42 | 43 | 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) | |
| 45 | 46 | } else { | |
| 46 | 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 | 188 | // TestSceneWalkUnchangedTiledSHM guards the GPU copy budget of an unchanged | |
| 140 | 189 | // subsurface tree. Each child has its own real wl_shm pool. | |
| 141 | 190 | 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 unchanged | 188 | // 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) { | ||