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;
}
public bool CpuWriteOpen
{
get; set;
}
[UnscopedRef]
public ref UnsafeList<RenderRequest> RenderRequests => ref _renderRequests;
@@ -262,11 +267,13 @@ public class RenderSystem : IDisposable
_submittedFenceValue = _completedFenceValue;
}
var nextFenceValue = _submittedFenceValue + 1;
// Begin rendering for this frame
frameResource.CommandAllocator.Reset();
_resourceManager.BeginFrame(_submittedFenceValue + 1);
var r = _graphicsEngine.BeginFrame(_submittedFenceValue + 1);
_resourceManager.BeginFrame(nextFenceValue);
var r = _graphicsEngine.BeginFrame(nextFenceValue);
if (r.IsFailure)
{
@@ -275,7 +282,7 @@ public class RenderSystem : IDisposable
}
// Start recording commands
Debug.WriteLine($"GPU: Frame started.");
// TODO: How can we support async compute and async copy?
var cmd = _graphicsEngine.GetPooledCommandBuffer(CommandBufferType.Graphics);
ref var renderRequests = ref frameResource.RenderRequests;
@@ -286,7 +293,6 @@ public class RenderSystem : IDisposable
var renderCtx = new RenderContext(_graphicsEngine, _resourceManager, cmd);
//Debug.WriteLine($"GPU: Frame started.");
_renderPipeline.Render(renderCtx, renderRequests.AsSpan());
_swapChainManager.TransitionToPresent(cmd);
@@ -313,7 +319,13 @@ public class RenderSystem : IDisposable
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);
r = _graphicsEngine.EndFrame(_completedFenceValue);
@@ -323,9 +335,6 @@ public class RenderSystem : IDisposable
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.");
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++;
}
@@ -381,7 +395,12 @@ public class RenderSystem : IDisposable
}
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;
}
@@ -391,14 +410,23 @@ public class RenderSystem : IDisposable
Debug.Assert(!_disposed, "Cannot add render request to a disposed RenderSystem.");
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)
{
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);
}

View File

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

View File

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

View File

@@ -125,6 +125,9 @@ public sealed partial class GraphicsTestWindow : Window
ctx.UpdateObjectData(_meshHandle);
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.WaitIdle();
@@ -205,7 +208,7 @@ public sealed partial class GraphicsTestWindow : Window
if (_renderSystem.TryAcquireCPUFrame())
{
//Debug.WriteLine($"CPU: Frame started.");
Debug.WriteLine($"CPU: Frame started.");
_world.SystemManager.UpdateAll(default);
_renderSystem.SignalCPUReady();
}