diff --git a/osu.Framework.Tests/Graphics/TripleBufferTest.cs b/osu.Framework.Tests/Graphics/TripleBufferTest.cs index d14e0faec1..f90fd9fbee 100644 --- a/osu.Framework.Tests/Graphics/TripleBufferTest.cs +++ b/osu.Framework.Tests/Graphics/TripleBufferTest.cs @@ -32,7 +32,7 @@ public void TestReadOnly() var tripleBuffer = new TripleBuffer(); using (var buffer = tripleBuffer.GetForRead()) - Assert.That(buffer, Is.Null); + Assert.That(!buffer.IsValid); } [Test] @@ -51,7 +51,7 @@ public void TestSameBufferIsNotWrittenTwiceInRowNoContestation() } using (var buffer = tripleBuffer.GetForRead()) - Assert.That(buffer!.Object!.ID, Is.EqualTo(lastWrite)); + Assert.That(buffer.Object!.ID, Is.EqualTo(lastWrite)); } } @@ -72,7 +72,7 @@ public void TestSameBufferIsNotWrittenTwiceInRowContestation() { using (var read = tripleBuffer.GetForRead()) { - Assert.That(read!.Object!.ID, Is.Not.EqualTo(lastRead)); + Assert.That(read.Object!.ID, Is.Not.EqualTo(lastRead)); for (int j = 0; j < 3; j++) { @@ -100,11 +100,11 @@ public void TestWriteThenRead() write.Object = obj; using (var buffer = tripleBuffer.GetForRead()) - Assert.That(buffer?.Object, Is.EqualTo(obj)); + Assert.That(buffer.Object, Is.EqualTo(obj)); } using (var buffer = tripleBuffer.GetForRead()) - Assert.That(buffer, Is.Null); + Assert.That(!buffer.IsValid); } [Test] @@ -121,7 +121,7 @@ public void TestReadSaturated() { resetEventSlim.Set(); using (var buffer = tripleBuffer.GetForRead()) - Assert.That(buffer?.Object, Is.EqualTo(obj)); + Assert.That(buffer.Object, Is.EqualTo(obj)); }, TaskCreationOptions.LongRunning); Task.Factory.StartNew(() => diff --git a/osu.Framework.Tests/Visual/Graphics/TestSceneTripleBufferOccupancy.cs b/osu.Framework.Tests/Visual/Graphics/TestSceneTripleBufferOccupancy.cs index 8b119c6de1..eb74385576 100644 --- a/osu.Framework.Tests/Visual/Graphics/TestSceneTripleBufferOccupancy.cs +++ b/osu.Framework.Tests/Visual/Graphics/TestSceneTripleBufferOccupancy.cs @@ -58,7 +58,7 @@ protected override void LoadComplete() { using (var read = tripleBuffer.GetForRead()) { - if (read != null) + if (read.IsValid) reads[read.Index]++; } diff --git a/osu.Framework/Allocation/TripleBuffer.cs b/osu.Framework/Allocation/TripleBuffer.cs index 16a849a2de..a287678d60 100644 --- a/osu.Framework/Allocation/TripleBuffer.cs +++ b/osu.Framework/Allocation/TripleBuffer.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; namespace osu.Framework.Allocation @@ -16,54 +16,46 @@ internal class TripleBuffer where T : class { private const int buffer_count = 3; - private const long read_timeout_milliseconds = 100; - - private readonly Buffer[] buffers = new Buffer[buffer_count]; - - private readonly Stopwatch stopwatch = new Stopwatch(); + private const int read_timeout_milliseconds = 100; + private BufferArray buffers; private int writeIndex; private int flipIndex = 1; private int readIndex = 2; public TripleBuffer() { - for (int i = 0; i < buffer_count; i++) - buffers[i] = new Buffer(i, finishUsage); + buffers[writeIndex] = new Buffer(writeIndex); + buffers[flipIndex] = new Buffer(flipIndex); + buffers[readIndex] = new Buffer(readIndex); } - public Buffer GetForWrite() + public WriteUsage GetForWrite() { - Buffer usage = buffers[writeIndex]; - usage.LastUsage = UsageType.Write; - return usage; + ref Buffer buffer = ref buffers[writeIndex]; + buffer.LastUsage = UsageType.Write; + return new WriteUsage(this, ref buffer); } - public Buffer? GetForRead() + public ReadUsage GetForRead() { - stopwatch.Restart(); + const int estimate_nanoseconds_per_cycle = 5; + const int estimate_cycles_to_timeout = read_timeout_milliseconds * 1000 * 1000 / estimate_nanoseconds_per_cycle; - do + // This should really never happen, but prevents a potential infinite loop if the usage can never be retrieved. + for (int i = 0; i < estimate_cycles_to_timeout; i++) { flip(ref readIndex); - // This should really never happen, but prevents a potential infinite loop if the usage can never be retrieved. - if (stopwatch.ElapsedMilliseconds > read_timeout_milliseconds) - return null; - } while (buffers[readIndex].LastUsage == UsageType.Read); - - Buffer usage = buffers[readIndex]; - - Debug.Assert(usage.LastUsage == UsageType.Write); - usage.LastUsage = UsageType.Read; + ref Buffer buffer = ref buffers[readIndex]; + if (buffer.LastUsage == UsageType.Read) + continue; - return usage; - } + buffer.LastUsage = UsageType.Read; + return new ReadUsage(ref buffer); + } - private void finishUsage(Buffer usage) - { - if (usage.LastUsage == UsageType.Write) - flip(ref writeIndex); + return default; } private void flip(ref int localIndex) @@ -71,32 +63,65 @@ private void flip(ref int localIndex) localIndex = Interlocked.Exchange(ref flipIndex, localIndex); } - public class Buffer : IDisposable + public readonly ref struct WriteUsage { - public T? Object; + private readonly TripleBuffer tripleBuffer; + private readonly ref Buffer buffer; - public volatile UsageType LastUsage; + public WriteUsage(TripleBuffer tripleBuffer, ref Buffer buffer) + { + this.tripleBuffer = tripleBuffer; + this.buffer = ref buffer; + } + + public T? Object + { + get => buffer.Object; + set => buffer.Object = value; + } - public readonly int Index; + public int Index => buffer.Index; - private readonly Action? finish; + public void Dispose() => tripleBuffer.flip(ref tripleBuffer.writeIndex); + } + + public readonly ref struct ReadUsage + { + private readonly ref Buffer buffer; - public Buffer(int index, Action? finish) + public ReadUsage(ref Buffer buffer) { - Index = index; - this.finish = finish; + this.buffer = ref buffer; } + [MemberNotNullWhen(true, nameof(Object))] + public bool IsValid => !Unsafe.IsNullRef(ref buffer); + + public T? Object => buffer.Object; + + public int Index => buffer.Index; + public void Dispose() { - finish?.Invoke(this); } } + public record struct Buffer(int Index) + { + public T? Object; + public volatile UsageType LastUsage; + } + public enum UsageType { Read, Write } + + [InlineArray(buffer_count)] + private struct BufferArray + { + private Buffer buffer; + } } } diff --git a/osu.Framework/Platform/GameHost.cs b/osu.Framework/Platform/GameHost.cs index e5ed5aa6b5..2619331a23 100644 --- a/osu.Framework/Platform/GameHost.cs +++ b/osu.Framework/Platform/GameHost.cs @@ -502,7 +502,7 @@ protected virtual void DrawFrame() Renderer.AllowTearing = windowMode.Value == WindowMode.Fullscreen; - TripleBuffer.Buffer buffer; + TripleBuffer.ReadUsage buffer; using (drawMonitor.BeginCollecting(PerformanceCollectionType.Sleep)) { @@ -516,7 +516,7 @@ protected virtual void DrawFrame() buffer = drawRoots.GetForRead(); } - if (buffer == null) + if (!buffer.IsValid) return; Debug.Assert(buffer.Object != null);