feat(render): improve frame sync and CPU write tracking

Refactored frame submission and synchronization logic for more accurate GPU/CPU coordination. Introduced CpuWriteOpen property to enforce correct CPU frame access patterns. Updated ResourceManager to track _submittedFrame and improved event waiting logic. Added debug assertions and enhanced logging for frame start events. Documented known command submission issue in GraphicsTestWindow.
This commit is contained in:
2026-04-03 22:17:54 +09:00
parent 2dc97f3149
commit 92970f85ef
4 changed files with 52 additions and 18 deletions

View File

@@ -65,6 +65,11 @@ public class RenderSystem : IDisposable
get; set; get; set;
} }
public bool CpuWriteOpen
{
get; set;
}
[UnscopedRef] [UnscopedRef]
public ref UnsafeList<RenderRequest> RenderRequests => ref _renderRequests; public ref UnsafeList<RenderRequest> RenderRequests => ref _renderRequests;
@@ -262,11 +267,13 @@ public class RenderSystem : IDisposable
_submittedFenceValue = _completedFenceValue; _submittedFenceValue = _completedFenceValue;
} }
var nextFenceValue = _submittedFenceValue + 1;
// Begin rendering for this frame // Begin rendering for this frame
frameResource.CommandAllocator.Reset(); frameResource.CommandAllocator.Reset();
_resourceManager.BeginFrame(_submittedFenceValue + 1); _resourceManager.BeginFrame(nextFenceValue);
var r = _graphicsEngine.BeginFrame(_submittedFenceValue + 1); var r = _graphicsEngine.BeginFrame(nextFenceValue);
if (r.IsFailure) if (r.IsFailure)
{ {
@@ -275,7 +282,7 @@ public class RenderSystem : IDisposable
} }
// Start recording commands // Start recording commands
Debug.WriteLine($"GPU: Frame started.");
// TODO: How can we support async compute and async copy? // TODO: How can we support async compute and async copy?
var cmd = _graphicsEngine.GetPooledCommandBuffer(CommandBufferType.Graphics); var cmd = _graphicsEngine.GetPooledCommandBuffer(CommandBufferType.Graphics);
ref var renderRequests = ref frameResource.RenderRequests; ref var renderRequests = ref frameResource.RenderRequests;
@@ -286,7 +293,6 @@ public class RenderSystem : IDisposable
var renderCtx = new RenderContext(_graphicsEngine, _resourceManager, cmd); var renderCtx = new RenderContext(_graphicsEngine, _resourceManager, cmd);
//Debug.WriteLine($"GPU: Frame started.");
_renderPipeline.Render(renderCtx, renderRequests.AsSpan()); _renderPipeline.Render(renderCtx, renderRequests.AsSpan());
_swapChainManager.TransitionToPresent(cmd); _swapChainManager.TransitionToPresent(cmd);
@@ -313,7 +319,13 @@ public class RenderSystem : IDisposable
renderRequests.Clear(); renderRequests.Clear();
} }
// End the frame and retire resources based on actual GPU progress. _submittedFenceValue = nextFenceValue;
frameResource.FenceValue = _graphicsEngine.Device.GraphicsQueue.Signal(nextFenceValue);
frameResource.GpuReadyEvent.Set();
_completedFenceValue = _graphicsEngine.Device.GraphicsQueue.GetCompletedValue();
// End the frame and retire resources based on the freshest observed GPU progress.
_resourceManager.EndFrame(_completedFenceValue); _resourceManager.EndFrame(_completedFenceValue);
r = _graphicsEngine.EndFrame(_completedFenceValue); r = _graphicsEngine.EndFrame(_completedFenceValue);
@@ -323,9 +335,6 @@ public class RenderSystem : IDisposable
break; break;
} }
_submittedFenceValue++;
frameResource.GpuReadyEvent.Set();
frameResource.FenceValue = _graphicsEngine.Device.GraphicsQueue.Signal(_submittedFenceValue);
} }
} }
@@ -361,7 +370,12 @@ public class RenderSystem : IDisposable
Debug.Assert(!_disposed, "Cannot signal CPU ready on a disposed RenderSystem."); Debug.Assert(!_disposed, "Cannot signal CPU ready on a disposed RenderSystem.");
var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount); var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
_frameResources[eventIndex].CpuReadyEvent.Set(); ref var frameResource = ref _frameResources[eventIndex];
Debug.Assert(frameResource.CpuWriteOpen, "SignalCPUReady called without a matching successful TryAcquireCPUFrame.");
frameResource.CpuWriteOpen = false;
frameResource.CpuReadyEvent.Set();
_cpuFenceValue++; _cpuFenceValue++;
} }
@@ -381,7 +395,12 @@ public class RenderSystem : IDisposable
} }
var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount); var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
_frameResources[eventIndex].RenderRequests.Clear(); ref var frameResource = ref _frameResources[eventIndex];
Debug.Assert(!frameResource.CpuWriteOpen, "TryAcquireCPUFrame called while the previous CPU frame is still open. Call SignalCPUReady first.");
frameResource.CpuWriteOpen = true;
frameResource.RenderRequests.Clear();
return true; return true;
} }
@@ -391,14 +410,23 @@ public class RenderSystem : IDisposable
Debug.Assert(!_disposed, "Cannot add render request to a disposed RenderSystem."); Debug.Assert(!_disposed, "Cannot add render request to a disposed RenderSystem.");
var frameIndex = (int)(_cpuFenceValue % _config.FrameBufferCount); var frameIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
_frameResources[frameIndex].RenderRequests.Add(request); ref var frameResource = ref _frameResources[frameIndex];
Debug.Assert(frameResource.CpuWriteOpen, "AddRenderRequest requires a successful TryAcquireCPUFrame and must happen before SignalCPUReady.");
frameResource.RenderRequests.Add(request);
} }
public bool WaitForGPUReady(int timeOut = -1) public bool WaitForGPUReady(int timeOut = -1)
{ {
Debug.Assert(!_disposed, "Cannot wait for GPU ready on a disposed RenderSystem."); Debug.Assert(!_disposed, "Cannot wait for GPU ready on a disposed RenderSystem.");
var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount); var submittedFenceValue = Volatile.Read(ref _submittedFenceValue);
if (submittedFenceValue == 0)
{
return true;
}
var eventIndex = (int)((submittedFenceValue - 1) % _config.FrameBufferCount);
return _frameResources[eventIndex].GpuReadyEvent.WaitOne(timeOut); return _frameResources[eventIndex].GpuReadyEvent.WaitOne(timeOut);
} }

View File

@@ -259,7 +259,7 @@ public partial class ResourceManager
_retiringPages.Enqueue(new RetiringPage _retiringPages.Enqueue(new RetiringPage
{ {
page = page, page = page,
retireFrame = _cpuFrame retireFrame = _submittedFrame
}); });
} }

View File

@@ -32,7 +32,7 @@ public sealed partial class ResourceManager : IDisposable
private readonly MaterialPaletteStore _materialPalettes; private readonly MaterialPaletteStore _materialPalettes;
private ulong _cpuFrame; private ulong _submittedFrame;
private bool _disposed; private bool _disposed;
@@ -59,13 +59,16 @@ public sealed partial class ResourceManager : IDisposable
internal void BeginFrame(ulong cpuFrame) internal void BeginFrame(ulong cpuFrame)
{ {
Debug.Assert(!_disposed); Debug.Assert(!_disposed);
_cpuFrame = cpuFrame; _submittedFrame = cpuFrame;
} }
internal void EndFrame(ulong gpuFrame) internal void EndFrame(ulong completedFrame)
{ {
Debug.Assert(!_disposed); Debug.Assert(!_disposed);
EndFramePool(gpuFrame);
//_submittedFrame = submittedFrame;
EndFramePool(completedFrame);
} }
/// <summary> /// <summary>

View File

@@ -125,6 +125,9 @@ public sealed partial class GraphicsTestWindow : Window
ctx.UpdateObjectData(_meshHandle); ctx.UpdateObjectData(_meshHandle);
directCmd.End().ThrowIfFailed(); directCmd.End().ThrowIfFailed();
// FIX: This will bump the complete value of the queue and cause the render thread render the first frame twice, which is not expected. We should have a better way to handle this.
// Maybe a async upload support in the future?
_renderSystem.GraphicsEngine.Device.GraphicsQueue.Submit(directCmd); _renderSystem.GraphicsEngine.Device.GraphicsQueue.Submit(directCmd);
_renderSystem.GraphicsEngine.Device.GraphicsQueue.WaitIdle(); _renderSystem.GraphicsEngine.Device.GraphicsQueue.WaitIdle();
@@ -205,7 +208,7 @@ public sealed partial class GraphicsTestWindow : Window
if (_renderSystem.TryAcquireCPUFrame()) if (_renderSystem.TryAcquireCPUFrame())
{ {
//Debug.WriteLine($"CPU: Frame started."); Debug.WriteLine($"CPU: Frame started.");
_world.SystemManager.UpdateAll(default); _world.SystemManager.UpdateAll(default);
_renderSystem.SignalCPUReady(); _renderSystem.SignalCPUReady();
} }