Skip to content

Pipeline Collect ahead of Execute with a lock-free triple buffer - #96

Merged
TareHimself merged 1 commit into
mainfrom
pipeline-collect-execute
Oct 25, 2026
Merged

TareHimself merged 1 commit into
mainfrom
pipeline-collect-execute

Conversation

@TareHimself

Copy link
Copy Markdown
Owner

Summary

  • Replaces strict lockstep Collect/Execute with a pipelined handoff: the main thread's Collect() and the render thread's Execute() used to serialize through a pair of AutoResetEvents, so each was idle while the other ran. Now Collect runs ahead freely; the render thread's AutoResetEvent is purely a wake signal, not a backpressure gate.
  • Adds TripleBuffer<T> (Engine/Rin.Core/Shared/Threading/TripleBuffer.cs): a lock-free single-producer/single-consumer "latest wins" primitive. The writer never blocks, the reader always gets the most recently completed write, and a batch that falls behind before ever being read is reported back to the writer so it can be disposed instead of executed stale and out of order.
  • Fixes a frame-slot aliasing risk in WindowRenderer: frame-slot selection used to be keyed off _framesRendered (incremented only after GPU submission completes), which could let two in-flight collected batches reference the same Frame once Collect runs ahead of Execute. It now selects the slot at collect time via a new _framesCollected counter.
  • Adds opt-in disposal for dropped/executed/shutdown-remaining batches in VulkanGraphicsModule, since a future IRenderData implementation may hold a disposable resource (IRenderData itself isn't IDisposable today).

Test plan

  • dotnet build Engine/Rin.Graphics.Vulkan/Rin.Graphics.Vulkan.csproj -c Debug — builds clean
  • dotnet test Engine/Rin.Core.Tests/Rin.Core.Tests.csproj --filter "FullyQualifiedName~TripleBufferTests" — 6/6 passing
  • Manual smoke test of an example app (SceneTest/AudioPlayer) to confirm no regressions in frame pacing

Collect and Execute used to run strictly lockstep: the main thread would
only gather a frame's render data after the render thread finished the
previous one, and vice versa, serialized through a pair of AutoResetEvents.
That meant the render thread was always idle while Collect ran and vice
versa, with no overlap possible.

Collect() now hands its output to the render thread through a new
TripleBuffer<T>, a lock-free SPSC latest-wins primitive: the writer never
blocks, the reader always gets the most recently completed write, and a
stale batch that falls behind is skipped and disposed rather than executed
out of order. The render thread's AutoResetEvent is now purely a wake
signal, not a backpressure gate. WindowRenderer selects its frame slot at
collect time (_framesCollected) instead of at execute-completion time, so
two in-flight collected batches never alias the same Frame.
@TareHimself
TareHimself merged commit d03df68 into main Oct 25, 2026
11 checks passed
@TareHimself
TareHimself deleted the pipeline-collect-execute branch October 25, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant