fix(rendering): bound portal resource lifetime
Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
3971997689
commit
749e8ceeb1
225 changed files with 29107 additions and 3914 deletions
|
|
@ -22,17 +22,43 @@ namespace AcDream.App.Rendering.Wb {
|
|||
public unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice {
|
||||
private readonly ILogger _log;
|
||||
private readonly DebugRenderSettings _renderSettings;
|
||||
private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue _resourceRetirement;
|
||||
|
||||
public GL GL { get; }
|
||||
public DebugRenderSettings RenderSettings => _renderSettings;
|
||||
|
||||
private readonly ConcurrentQueue<Action<GL>> _glThreadQueue = new();
|
||||
private readonly ConcurrentQueue<Action<GL>> _nextGlThreadQueue = new();
|
||||
|
||||
internal bool HasPendingGLWork =>
|
||||
!_glThreadQueue.IsEmpty || !_nextGlThreadQueue.IsEmpty;
|
||||
|
||||
public void QueueGLAction(Action<GL> action) {
|
||||
_glThreadQueue.Enqueue(action);
|
||||
}
|
||||
|
||||
internal void QueueGLActionForNextPass(Action<GL> action) {
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
_nextGlThreadQueue.Enqueue(action);
|
||||
}
|
||||
|
||||
public void ProcessGLQueue() {
|
||||
// Retry the prior pass before ordinary work (notably sampler
|
||||
// deletion), but process only the captured generation so a
|
||||
// persistent driver failure cannot spin this frame forever.
|
||||
int retryCount = _nextGlThreadQueue.Count;
|
||||
for (int i = 0; i < retryCount && _nextGlThreadQueue.TryDequeue(out Action<GL>? retry); i++) {
|
||||
try {
|
||||
retry(GL);
|
||||
} catch (Exception ex) {
|
||||
_log.LogError(ex, "Error processing retryable GL queue action");
|
||||
}
|
||||
}
|
||||
// Normal actions retain drain-to-empty semantics because teardown
|
||||
// actions intentionally enqueue dependent atlas releases here.
|
||||
// A persistent retryable error must not starve unrelated uploads
|
||||
// and releases forever: the retry generation remains bounded to
|
||||
// one attempt per pass, while ordinary work still makes progress.
|
||||
while (_glThreadQueue.TryDequeue(out var action)) {
|
||||
try {
|
||||
action(GL);
|
||||
|
|
@ -61,6 +87,7 @@ namespace AcDream.App.Rendering.Wb {
|
|||
public uint WrapSampler { get; private set; }
|
||||
/// <summary>OpenGL sampler object with TextureWrapMode.ClampToEdge (for meshes without wrapping UVs).</summary>
|
||||
public uint ClampSampler { get; private set; }
|
||||
internal float MaxSupportedAnisotropy { get; private set; }
|
||||
|
||||
private ManagedGLUniformBuffer? _sceneDataBuffer;
|
||||
/// <summary>Shared SceneData UBO.</summary>
|
||||
|
|
@ -83,12 +110,23 @@ namespace AcDream.App.Rendering.Wb {
|
|||
protected OpenGLGraphicsDevice() : base() {
|
||||
_log = null!;
|
||||
_renderSettings = null!;
|
||||
_resourceRetirement = null!;
|
||||
GL = null!;
|
||||
}
|
||||
|
||||
public OpenGLGraphicsDevice(GL gl, ILogger log, DebugRenderSettings renderSettings, bool allowBindless = true) : base() {
|
||||
public OpenGLGraphicsDevice(GL gl, ILogger log, DebugRenderSettings renderSettings, bool allowBindless = true)
|
||||
: this(gl, log, renderSettings, AcDream.App.Rendering.ImmediateGpuResourceRetirementQueue.Instance, allowBindless) {
|
||||
}
|
||||
|
||||
internal OpenGLGraphicsDevice(
|
||||
GL gl,
|
||||
ILogger log,
|
||||
DebugRenderSettings renderSettings,
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement,
|
||||
bool allowBindless = true) : base() {
|
||||
_log = log;
|
||||
_renderSettings = renderSettings;
|
||||
_resourceRetirement = resourceRetirement ?? throw new ArgumentNullException(nameof(resourceRetirement));
|
||||
|
||||
GL = gl;
|
||||
GLHelpers.Init(this, log);
|
||||
|
|
@ -114,26 +152,30 @@ namespace AcDream.App.Rendering.Wb {
|
|||
GL.GenBuffers(1, out uint instanceVbo);
|
||||
InstanceVBO = instanceVbo;
|
||||
|
||||
// Query this immutable device limit once. Atlas construction can
|
||||
// happen hundreds of times during portal streaming; repeating a
|
||||
// driver GetFloat for every texture serialized the upload burst.
|
||||
if (renderSettings.EnableAnisotropicFiltering) {
|
||||
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
|
||||
MaxSupportedAnisotropy = Math.Max(0f, maxAniso);
|
||||
}
|
||||
|
||||
// Create sampler objects for wrap vs clamp
|
||||
WrapSampler = GL.GenSampler();
|
||||
GL.SamplerParameter(WrapSampler, SamplerParameterI.WrapS, (int)TextureWrapMode.Repeat);
|
||||
GL.SamplerParameter(WrapSampler, SamplerParameterI.WrapT, (int)TextureWrapMode.Repeat);
|
||||
GL.SamplerParameter(WrapSampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.LinearMipmapLinear);
|
||||
GL.SamplerParameter(WrapSampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
|
||||
if (renderSettings.EnableAnisotropicFiltering) {
|
||||
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
|
||||
if (maxAniso > 0) GL.SamplerParameter(WrapSampler, GLEnum.TextureMaxAnisotropy, maxAniso);
|
||||
}
|
||||
if (MaxSupportedAnisotropy > 0)
|
||||
GL.SamplerParameter(WrapSampler, GLEnum.TextureMaxAnisotropy, MaxSupportedAnisotropy);
|
||||
|
||||
ClampSampler = GL.GenSampler();
|
||||
GL.SamplerParameter(ClampSampler, SamplerParameterI.WrapS, (int)TextureWrapMode.ClampToEdge);
|
||||
GL.SamplerParameter(ClampSampler, SamplerParameterI.WrapT, (int)TextureWrapMode.ClampToEdge);
|
||||
GL.SamplerParameter(ClampSampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.LinearMipmapLinear);
|
||||
GL.SamplerParameter(ClampSampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
|
||||
if (renderSettings.EnableAnisotropicFiltering) {
|
||||
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
|
||||
if (maxAniso > 0) GL.SamplerParameter(ClampSampler, GLEnum.TextureMaxAnisotropy, maxAniso);
|
||||
}
|
||||
if (MaxSupportedAnisotropy > 0)
|
||||
GL.SamplerParameter(ClampSampler, GLEnum.TextureMaxAnisotropy, MaxSupportedAnisotropy);
|
||||
|
||||
_sceneDataBuffer = new ManagedGLUniformBuffer(this, BufferUsage.Dynamic, Marshal.SizeOf<SceneData>());
|
||||
|
||||
|
|
@ -145,6 +187,15 @@ namespace AcDream.App.Rendering.Wb {
|
|||
ParticleBatcher = null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retires a GL resource only after every submitted draw that could
|
||||
/// reference it has completed on the GPU.
|
||||
/// </summary>
|
||||
internal void RetireGpuResource(Action release) => _resourceRetirement.Retire(release);
|
||||
|
||||
internal AcDream.App.Rendering.IGpuResourceRetirementQueue ResourceRetirement =>
|
||||
_resourceRetirement;
|
||||
|
||||
private void InitializeSharedDebugResources() {
|
||||
// Unit quad vertices for two triangles (0 to 1 for length, -0.5 to 0.5 for thickness)
|
||||
float[] quadVertices = {
|
||||
|
|
@ -608,14 +659,26 @@ namespace AcDream.App.Rendering.Wb {
|
|||
GpuMemoryTracker.TrackDeallocation(instanceBufferCapacity * instanceBufferStride);
|
||||
}
|
||||
}
|
||||
if (wrapSampler != 0) {
|
||||
gl.DeleteSampler(wrapSampler);
|
||||
}
|
||||
if (clampSampler != 0) {
|
||||
gl.DeleteSampler(clampSampler);
|
||||
}
|
||||
});
|
||||
|
||||
// Bindless texture-array retirements embed these samplers in their
|
||||
// resident handles. Ordinary GL work must keep flowing when one
|
||||
// retry is sick, but sampler deletion itself is dependency-ordered
|
||||
// behind the retry queue. Requeue into the next generation (never
|
||||
// the drain-to-empty ordinary queue) to remain one attempt/frame.
|
||||
Action<GL>? deleteSamplersWhenSafe = null;
|
||||
deleteSamplersWhenSafe = gl => {
|
||||
if (!_nextGlThreadQueue.IsEmpty) {
|
||||
QueueGLActionForNextPass(deleteSamplersWhenSafe!);
|
||||
return;
|
||||
}
|
||||
if (wrapSampler != 0)
|
||||
gl.DeleteSampler(wrapSampler);
|
||||
if (clampSampler != 0)
|
||||
gl.DeleteSampler(clampSampler);
|
||||
};
|
||||
QueueGLActionForNextPass(deleteSamplersWhenSafe);
|
||||
|
||||
InstanceVBO = 0;
|
||||
InstanceVBOPtr = null;
|
||||
WrapSampler = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue