From 8334a24fd190cf34febf367c220d0789f17054a7 Mon Sep 17 00:00:00 2001 From: robert Date: Fri, 5 Jul 2024 14:32:58 +0200 Subject: [PATCH] Setup entry point + integrated moonworks stuff --- .idea/.idea.Nerfed/.idea/.gitignore | 13 + .idea/.idea.Nerfed/.idea/encodings.xml | 4 + .idea/.idea.Nerfed/.idea/indexLayout.xml | 8 + .../inspectionProfiles/Project_Default.xml | 10 + .idea/.idea.Nerfed/.idea/vcs.xml | 6 + Libraries/RefreshCS | 1 + Libraries/SDL2CS | 1 + Nerfed.Runtime/Assert.cs | 24 + Nerfed.Runtime/Audio/AudioBuffer.cs | 78 + Nerfed.Runtime/Audio/AudioDataOgg.cs | 146 ++ Nerfed.Runtime/Audio/AudioDataQoa.cs | 163 ++ Nerfed.Runtime/Audio/AudioDataStreamable.cs | 48 + Nerfed.Runtime/Audio/AudioDataWav.cs | 99 + Nerfed.Runtime/Audio/AudioDevice.cs | 309 +++ Nerfed.Runtime/Audio/AudioEmitter.cs | 135 ++ Nerfed.Runtime/Audio/AudioListener.cs | 97 + Nerfed.Runtime/Audio/AudioResource.cs | 52 + Nerfed.Runtime/Audio/FilterType.cs | 9 + Nerfed.Runtime/Audio/Format.cs | 35 + Nerfed.Runtime/Audio/IPoolable.cs | 6 + Nerfed.Runtime/Audio/PersistentVoice.cs | 27 + Nerfed.Runtime/Audio/ReverbEffect.cs | 82 + Nerfed.Runtime/Audio/SoundSequence.cs | 63 + Nerfed.Runtime/Audio/SoundState.cs | 8 + Nerfed.Runtime/Audio/SourceVoice.cs | 217 ++ Nerfed.Runtime/Audio/SourceVoicePool.cs | 38 + Nerfed.Runtime/Audio/StreamingVoice.cs | 168 ++ Nerfed.Runtime/Audio/SubmixVoice.cs | 55 + Nerfed.Runtime/Audio/TransientVoice.cs | 28 + Nerfed.Runtime/Audio/UpdatingSourceVoice.cs | 10 + Nerfed.Runtime/Audio/Voice.cs | 433 ++++ Nerfed.Runtime/Engine.cs | 252 +++ Nerfed.Runtime/FrameLimiterSettings.cs | 35 + Nerfed.Runtime/Graphics/Color.cs | 1948 +++++++++++++++++ Nerfed.Runtime/Graphics/CommandBuffer.cs | 1042 +++++++++ Nerfed.Runtime/Graphics/CommandBufferPool.cs | 33 + Nerfed.Runtime/Graphics/ComputePass.cs | 171 ++ Nerfed.Runtime/Graphics/ComputePassPool.cs | 25 + Nerfed.Runtime/Graphics/CopyPass.cs | 241 ++ Nerfed.Runtime/Graphics/CopyPassPool.cs | 25 + .../Graphics/EmbeddedShadersSpirV.cs | 716 ++++++ Nerfed.Runtime/Graphics/FencePool.cs | 31 + Nerfed.Runtime/Graphics/Font/Enums.cs | 16 + Nerfed.Runtime/Graphics/Font/Font.cs | 123 ++ Nerfed.Runtime/Graphics/Font/Structs.cs | 26 + Nerfed.Runtime/Graphics/Font/TextBatch.cs | 191 ++ Nerfed.Runtime/Graphics/GraphicsDevice.cs | 466 ++++ Nerfed.Runtime/Graphics/GraphicsResource.cs | 53 + Nerfed.Runtime/Graphics/IEmbeddedShaders.cs | 10 + Nerfed.Runtime/Graphics/IVertexType.cs | 17 + Nerfed.Runtime/Graphics/ImageUtils.cs | 455 ++++ .../Graphics/PackedVector/Alpha8.cs | 161 ++ .../Graphics/PackedVector/Bgr565.cs | 184 ++ .../Graphics/PackedVector/Bgra4444.cs | 178 ++ .../Graphics/PackedVector/Bgra5551.cs | 178 ++ Nerfed.Runtime/Graphics/PackedVector/Byte4.cs | 204 ++ .../Graphics/PackedVector/HalfSingle.cs | 113 + .../Graphics/PackedVector/HalfTypeHelper.cs | 138 ++ .../Graphics/PackedVector/HalfVector2.cs | 133 ++ .../Graphics/PackedVector/HalfVector4.cs | 204 ++ .../Graphics/PackedVector/IPackedVector.cs | 38 + .../Graphics/PackedVector/NormalizedByte2.cs | 141 ++ .../Graphics/PackedVector/NormalizedByte4.cs | 146 ++ .../Graphics/PackedVector/NormalizedShort2.cs | 150 ++ .../Graphics/PackedVector/NormalizedShort4.cs | 161 ++ Nerfed.Runtime/Graphics/PackedVector/Rg32.cs | 182 ++ .../Graphics/PackedVector/Rgba1010102.cs | 178 ++ .../Graphics/PackedVector/Rgba64.cs | 178 ++ .../Graphics/PackedVector/Short2.cs | 134 ++ .../Graphics/PackedVector/Short4.cs | 204 ++ Nerfed.Runtime/Graphics/RefreshResource.cs | 30 + Nerfed.Runtime/Graphics/RefreshTypes.cs | 1723 +++++++++++++++ Nerfed.Runtime/Graphics/RenderPass.cs | 495 +++++ Nerfed.Runtime/Graphics/RenderPassPool.cs | 25 + Nerfed.Runtime/Graphics/ResourceUploader.cs | 376 ++++ Nerfed.Runtime/Graphics/Resources/Buffer.cs | 88 + .../Graphics/Resources/ComputePipeline.cs | 91 + Nerfed.Runtime/Graphics/Resources/Fence.cs | 25 + .../Graphics/Resources/GraphicsPipeline.cs | 99 + Nerfed.Runtime/Graphics/Resources/Sampler.cs | 23 + Nerfed.Runtime/Graphics/Resources/Shader.cs | 91 + Nerfed.Runtime/Graphics/Resources/Texture.cs | 326 +++ .../Graphics/Resources/TransferBuffer.cs | 209 ++ .../StockShaders/Binary/fullscreen.vert.spv | Bin 0 -> 1216 bytes .../StockShaders/Binary/text_msdf.frag.spv | Bin 0 -> 2616 bytes .../Binary/text_transform.vert.spv | Bin 0 -> 1448 bytes .../Binary/video_yuv2rgba.frag.spv | Bin 0 -> 1644 bytes .../StockShaders/Source/fullscreen.vert | 9 + .../StockShaders/Source/text_msdf.frag | 34 + .../StockShaders/Source/text_transform.vert | 20 + .../StockShaders/Source/video_yuv2rgba.frag | 38 + Nerfed.Runtime/Input/ButtonState.cs | 7 + Nerfed.Runtime/Input/Devices/GamePad.cs | 224 ++ Nerfed.Runtime/Input/Devices/Keyboard.cs | 295 +++ Nerfed.Runtime/Input/Devices/Mouse.cs | 132 ++ Nerfed.Runtime/Input/GamePadAxis.cs | 11 + Nerfed.Runtime/Input/GamePadButton.cs | 20 + Nerfed.Runtime/Input/Key.cs | 127 ++ Nerfed.Runtime/Input/KeyState.cs | 7 + Nerfed.Runtime/Input/MouseButton.cs | 10 + Nerfed.Runtime/Input/MouseState.cs | 14 + Nerfed.Runtime/Interop/Conversions.cs | 55 + Nerfed.Runtime/Log.cs | 54 + Nerfed.Runtime/Nerfed.Runtime.csproj | 2 +- Nerfed.Runtime/Profiler.cs | 29 + Nerfed.Runtime/Program.cs | 4 +- Nerfed.Runtime/Storage/StorageContainer.cs | 9 + Nerfed.Runtime/Util/MathEx.cs | 20 + Nerfed.Runtime/Util/SpanExtensions.cs | 147 ++ Nerfed.Runtime/Video/VideoAV1.cs | 67 + Nerfed.Runtime/Video/VideoAV1Stream.cs | 156 ++ Nerfed.Runtime/Video/VideoPlayer.cs | 347 +++ Nerfed.Runtime/Video/VideoState.cs | 8 + Nerfed.Runtime/Window/ScreenMode.cs | 8 + Nerfed.Runtime/Window/Window.cs | 234 ++ Nerfed.Runtime/Window/WindowCreateInfo.cs | 48 + 116 files changed, 16988 insertions(+), 3 deletions(-) create mode 100644 .idea/.idea.Nerfed/.idea/.gitignore create mode 100644 .idea/.idea.Nerfed/.idea/encodings.xml create mode 100644 .idea/.idea.Nerfed/.idea/indexLayout.xml create mode 100644 .idea/.idea.Nerfed/.idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/.idea.Nerfed/.idea/vcs.xml create mode 160000 Libraries/RefreshCS create mode 160000 Libraries/SDL2CS create mode 100644 Nerfed.Runtime/Assert.cs create mode 100644 Nerfed.Runtime/Audio/AudioBuffer.cs create mode 100644 Nerfed.Runtime/Audio/AudioDataOgg.cs create mode 100644 Nerfed.Runtime/Audio/AudioDataQoa.cs create mode 100644 Nerfed.Runtime/Audio/AudioDataStreamable.cs create mode 100644 Nerfed.Runtime/Audio/AudioDataWav.cs create mode 100644 Nerfed.Runtime/Audio/AudioDevice.cs create mode 100644 Nerfed.Runtime/Audio/AudioEmitter.cs create mode 100644 Nerfed.Runtime/Audio/AudioListener.cs create mode 100644 Nerfed.Runtime/Audio/AudioResource.cs create mode 100644 Nerfed.Runtime/Audio/FilterType.cs create mode 100644 Nerfed.Runtime/Audio/Format.cs create mode 100644 Nerfed.Runtime/Audio/IPoolable.cs create mode 100644 Nerfed.Runtime/Audio/PersistentVoice.cs create mode 100644 Nerfed.Runtime/Audio/ReverbEffect.cs create mode 100644 Nerfed.Runtime/Audio/SoundSequence.cs create mode 100644 Nerfed.Runtime/Audio/SoundState.cs create mode 100644 Nerfed.Runtime/Audio/SourceVoice.cs create mode 100644 Nerfed.Runtime/Audio/SourceVoicePool.cs create mode 100644 Nerfed.Runtime/Audio/StreamingVoice.cs create mode 100644 Nerfed.Runtime/Audio/SubmixVoice.cs create mode 100644 Nerfed.Runtime/Audio/TransientVoice.cs create mode 100644 Nerfed.Runtime/Audio/UpdatingSourceVoice.cs create mode 100644 Nerfed.Runtime/Audio/Voice.cs create mode 100644 Nerfed.Runtime/Engine.cs create mode 100644 Nerfed.Runtime/FrameLimiterSettings.cs create mode 100644 Nerfed.Runtime/Graphics/Color.cs create mode 100644 Nerfed.Runtime/Graphics/CommandBuffer.cs create mode 100644 Nerfed.Runtime/Graphics/CommandBufferPool.cs create mode 100644 Nerfed.Runtime/Graphics/ComputePass.cs create mode 100644 Nerfed.Runtime/Graphics/ComputePassPool.cs create mode 100644 Nerfed.Runtime/Graphics/CopyPass.cs create mode 100644 Nerfed.Runtime/Graphics/CopyPassPool.cs create mode 100644 Nerfed.Runtime/Graphics/EmbeddedShadersSpirV.cs create mode 100644 Nerfed.Runtime/Graphics/FencePool.cs create mode 100644 Nerfed.Runtime/Graphics/Font/Enums.cs create mode 100644 Nerfed.Runtime/Graphics/Font/Font.cs create mode 100644 Nerfed.Runtime/Graphics/Font/Structs.cs create mode 100644 Nerfed.Runtime/Graphics/Font/TextBatch.cs create mode 100644 Nerfed.Runtime/Graphics/GraphicsDevice.cs create mode 100644 Nerfed.Runtime/Graphics/GraphicsResource.cs create mode 100644 Nerfed.Runtime/Graphics/IEmbeddedShaders.cs create mode 100644 Nerfed.Runtime/Graphics/IVertexType.cs create mode 100644 Nerfed.Runtime/Graphics/ImageUtils.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/Alpha8.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/Bgr565.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/Bgra4444.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/Bgra5551.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/Byte4.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/HalfSingle.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/HalfTypeHelper.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/HalfVector2.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/HalfVector4.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/IPackedVector.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/NormalizedByte2.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/NormalizedByte4.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/NormalizedShort2.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/NormalizedShort4.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/Rg32.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/Rgba1010102.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/Rgba64.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/Short2.cs create mode 100644 Nerfed.Runtime/Graphics/PackedVector/Short4.cs create mode 100644 Nerfed.Runtime/Graphics/RefreshResource.cs create mode 100644 Nerfed.Runtime/Graphics/RefreshTypes.cs create mode 100644 Nerfed.Runtime/Graphics/RenderPass.cs create mode 100644 Nerfed.Runtime/Graphics/RenderPassPool.cs create mode 100644 Nerfed.Runtime/Graphics/ResourceUploader.cs create mode 100644 Nerfed.Runtime/Graphics/Resources/Buffer.cs create mode 100644 Nerfed.Runtime/Graphics/Resources/ComputePipeline.cs create mode 100644 Nerfed.Runtime/Graphics/Resources/Fence.cs create mode 100644 Nerfed.Runtime/Graphics/Resources/GraphicsPipeline.cs create mode 100644 Nerfed.Runtime/Graphics/Resources/Sampler.cs create mode 100644 Nerfed.Runtime/Graphics/Resources/Shader.cs create mode 100644 Nerfed.Runtime/Graphics/Resources/Texture.cs create mode 100644 Nerfed.Runtime/Graphics/Resources/TransferBuffer.cs create mode 100644 Nerfed.Runtime/Graphics/StockShaders/Binary/fullscreen.vert.spv create mode 100644 Nerfed.Runtime/Graphics/StockShaders/Binary/text_msdf.frag.spv create mode 100644 Nerfed.Runtime/Graphics/StockShaders/Binary/text_transform.vert.spv create mode 100644 Nerfed.Runtime/Graphics/StockShaders/Binary/video_yuv2rgba.frag.spv create mode 100644 Nerfed.Runtime/Graphics/StockShaders/Source/fullscreen.vert create mode 100644 Nerfed.Runtime/Graphics/StockShaders/Source/text_msdf.frag create mode 100644 Nerfed.Runtime/Graphics/StockShaders/Source/text_transform.vert create mode 100644 Nerfed.Runtime/Graphics/StockShaders/Source/video_yuv2rgba.frag create mode 100644 Nerfed.Runtime/Input/ButtonState.cs create mode 100644 Nerfed.Runtime/Input/Devices/GamePad.cs create mode 100644 Nerfed.Runtime/Input/Devices/Keyboard.cs create mode 100644 Nerfed.Runtime/Input/Devices/Mouse.cs create mode 100644 Nerfed.Runtime/Input/GamePadAxis.cs create mode 100644 Nerfed.Runtime/Input/GamePadButton.cs create mode 100644 Nerfed.Runtime/Input/Key.cs create mode 100644 Nerfed.Runtime/Input/KeyState.cs create mode 100644 Nerfed.Runtime/Input/MouseButton.cs create mode 100644 Nerfed.Runtime/Input/MouseState.cs create mode 100644 Nerfed.Runtime/Interop/Conversions.cs create mode 100644 Nerfed.Runtime/Log.cs create mode 100644 Nerfed.Runtime/Profiler.cs create mode 100644 Nerfed.Runtime/Storage/StorageContainer.cs create mode 100644 Nerfed.Runtime/Util/MathEx.cs create mode 100644 Nerfed.Runtime/Util/SpanExtensions.cs create mode 100644 Nerfed.Runtime/Video/VideoAV1.cs create mode 100644 Nerfed.Runtime/Video/VideoAV1Stream.cs create mode 100644 Nerfed.Runtime/Video/VideoPlayer.cs create mode 100644 Nerfed.Runtime/Video/VideoState.cs create mode 100644 Nerfed.Runtime/Window/ScreenMode.cs create mode 100644 Nerfed.Runtime/Window/Window.cs create mode 100644 Nerfed.Runtime/Window/WindowCreateInfo.cs diff --git a/.idea/.idea.Nerfed/.idea/.gitignore b/.idea/.idea.Nerfed/.idea/.gitignore new file mode 100644 index 0000000..580efc9 --- /dev/null +++ b/.idea/.idea.Nerfed/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/contentModel.xml +/projectSettingsUpdater.xml +/.idea.Nerfed.iml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.Nerfed/.idea/encodings.xml b/.idea/.idea.Nerfed/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.Nerfed/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.Nerfed/.idea/indexLayout.xml b/.idea/.idea.Nerfed/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.Nerfed/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Nerfed/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.Nerfed/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..146ab09 --- /dev/null +++ b/.idea/.idea.Nerfed/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.Nerfed/.idea/vcs.xml b/.idea/.idea.Nerfed/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/.idea.Nerfed/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Libraries/RefreshCS b/Libraries/RefreshCS new file mode 160000 index 0000000..07b3484 --- /dev/null +++ b/Libraries/RefreshCS @@ -0,0 +1 @@ +Subproject commit 07b3484b26b4ff0e09ea0a00a8e320659dda7479 diff --git a/Libraries/SDL2CS b/Libraries/SDL2CS new file mode 160000 index 0000000..1eb20e5 --- /dev/null +++ b/Libraries/SDL2CS @@ -0,0 +1 @@ +Subproject commit 1eb20e5c690aee9a5188ba9cf06207295c51d935 diff --git a/Nerfed.Runtime/Assert.cs b/Nerfed.Runtime/Assert.cs new file mode 100644 index 0000000..0a454b4 --- /dev/null +++ b/Nerfed.Runtime/Assert.cs @@ -0,0 +1,24 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Nerfed.Runtime; + +public class AssertionException(string msg) : Exception(msg); + +public static class Assert +{ + [Conditional("DEBUG"), DebuggerHidden] + public static void Debug([DoesNotReturnIf(false)] bool cond, [CallerArgumentExpression("cond")] string expression = "") { + if (!cond) { + throw new AssertionException(expression); + } + } + + [DebuggerHidden] + public static void Always([DoesNotReturnIf(false)] bool cond, [CallerArgumentExpression("cond")] string expression = "") { + if (!cond) { + throw new AssertionException(expression); + } + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/AudioBuffer.cs b/Nerfed.Runtime/Audio/AudioBuffer.cs new file mode 100644 index 0000000..b099787 --- /dev/null +++ b/Nerfed.Runtime/Audio/AudioBuffer.cs @@ -0,0 +1,78 @@ +using System; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Audio; + +/// +/// Contains raw audio data in a specified Format.
+/// Submit this to a SourceVoice to play audio. +///
+public class AudioBuffer : AudioResource +{ + IntPtr BufferDataPtr; + uint BufferDataLength; + private bool OwnsBufferData; + + public Format Format { get; } + + /// + /// Create a new AudioBuffer. + /// + /// If true, the buffer data will be destroyed when this AudioBuffer is destroyed. + public AudioBuffer( + AudioDevice device, + Format format, + IntPtr bufferPtr, + uint bufferLengthInBytes, + bool ownsBufferData) : base(device) + { + Format = format; + BufferDataPtr = bufferPtr; + BufferDataLength = bufferLengthInBytes; + OwnsBufferData = ownsBufferData; + } + + /// + /// Create another AudioBuffer from this audio buffer. + /// It will not own the buffer data. + /// + /// Offset in bytes from the top of the original buffer. + /// Length in bytes of the new buffer. + /// + public AudioBuffer Slice(int offset, uint length) + { + return new AudioBuffer(Device, Format, BufferDataPtr + offset, length, false); + } + + /// + /// Create an FAudioBuffer struct from this AudioBuffer. + /// + /// Whether we should set the FAudioBuffer to loop. + public FAudio.FAudioBuffer ToFAudioBuffer(bool loop = false) + { + return new FAudio.FAudioBuffer + { + Flags = FAudio.FAUDIO_END_OF_STREAM, + pContext = IntPtr.Zero, + pAudioData = BufferDataPtr, + AudioBytes = BufferDataLength, + PlayBegin = 0, + PlayLength = 0, + LoopBegin = 0, + LoopLength = 0, + LoopCount = loop ? FAudio.FAUDIO_LOOP_INFINITE : 0 + }; + } + + protected override unsafe void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (OwnsBufferData) + { + NativeMemory.Free((void*) BufferDataPtr); + } + } + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/AudioDataOgg.cs b/Nerfed.Runtime/Audio/AudioDataOgg.cs new file mode 100644 index 0000000..3443a0d --- /dev/null +++ b/Nerfed.Runtime/Audio/AudioDataOgg.cs @@ -0,0 +1,146 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Audio; + +/// +/// Streamable audio in Ogg format. +/// +public class AudioDataOgg : AudioDataStreamable +{ + private IntPtr FileDataPtr = IntPtr.Zero; + private IntPtr VorbisHandle = IntPtr.Zero; + + private string FilePath; + + public override bool Loaded => VorbisHandle != IntPtr.Zero; + public override uint DecodeBufferSize => 32768; + + public AudioDataOgg(AudioDevice device, string filePath) : base(device) + { + FilePath = filePath; + + IntPtr handle = FAudio.stb_vorbis_open_filename(filePath, out int error, IntPtr.Zero); + + if (error != 0) + { + throw new InvalidOperationException("Error loading file!"); + } + + FAudio.stb_vorbis_info info = FAudio.stb_vorbis_get_info(handle); + + Format = new Format + { + Tag = FormatTag.IEEE_FLOAT, + BitsPerSample = 32, + Channels = (ushort) info.channels, + SampleRate = info.sample_rate + }; + + FAudio.stb_vorbis_close(handle); + } + + public override unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd) + { + int lengthInFloats = bufferLengthInBytes / sizeof(float); + + /* NOTE: this function returns samples per channel, not total samples */ + int samples = FAudio.stb_vorbis_get_samples_float_interleaved( + VorbisHandle, + Format.Channels, + (IntPtr) buffer, + lengthInFloats + ); + + int sampleCount = samples * Format.Channels; + reachedEnd = sampleCount < lengthInFloats; + filledLengthInBytes = sampleCount * sizeof(float); + } + + /// + /// Prepares the Ogg data for streaming. + /// + public override unsafe void Load() + { + if (!Loaded) + { + FileStream fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read); + FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length); + Span fileDataSpan = new Span((void*) FileDataPtr, (int) fileStream.Length); + fileStream.ReadExactly(fileDataSpan); + fileStream.Close(); + + VorbisHandle = FAudio.stb_vorbis_open_memory(FileDataPtr, fileDataSpan.Length, out int error, IntPtr.Zero); + if (error != 0) + { + NativeMemory.Free((void*) FileDataPtr); + Log.Error("Error opening OGG file!"); + Log.Error("Error: " + error); + throw new InvalidOperationException("Error opening OGG file!"); + } + } + } + + public override void Seek(uint sampleFrame) + { + FAudio.stb_vorbis_seek(VorbisHandle, sampleFrame); + } + + /// + /// Unloads the Ogg data, freeing resources. + /// + public override unsafe void Unload() + { + if (Loaded) + { + FAudio.stb_vorbis_close(VorbisHandle); + NativeMemory.Free((void*) FileDataPtr); + + VorbisHandle = IntPtr.Zero; + FileDataPtr = IntPtr.Zero; + } + } + + /// + /// Loads an entire ogg file into an AudioBuffer. Useful for static audio. + /// + public static unsafe AudioBuffer CreateBuffer(AudioDevice device, string filePath) + { + IntPtr filePointer = FAudio.stb_vorbis_open_filename(filePath, out int error, IntPtr.Zero); + + if (error != 0) + { + throw new InvalidOperationException("Error loading file!"); + } + FAudio.stb_vorbis_info info = FAudio.stb_vorbis_get_info(filePointer); + long lengthInFloats = + FAudio.stb_vorbis_stream_length_in_samples(filePointer) * info.channels; + long lengthInBytes = lengthInFloats * Marshal.SizeOf(); + void* buffer = NativeMemory.Alloc((nuint) lengthInBytes); + + FAudio.stb_vorbis_get_samples_float_interleaved( + filePointer, + info.channels, + (nint) buffer, + (int) lengthInFloats + ); + + FAudio.stb_vorbis_close(filePointer); + + Format format = new Format + { + Tag = FormatTag.IEEE_FLOAT, + BitsPerSample = 32, + Channels = (ushort) info.channels, + SampleRate = info.sample_rate + }; + + return new AudioBuffer( + device, + format, + (nint) buffer, + (uint) lengthInBytes, + true); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/AudioDataQoa.cs b/Nerfed.Runtime/Audio/AudioDataQoa.cs new file mode 100644 index 0000000..42d97e9 --- /dev/null +++ b/Nerfed.Runtime/Audio/AudioDataQoa.cs @@ -0,0 +1,163 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Audio; + +/// +/// Streamable audio in QOA format. +/// +public class AudioDataQoa : AudioDataStreamable +{ + private IntPtr QoaHandle = IntPtr.Zero; + private IntPtr FileDataPtr = IntPtr.Zero; + + private string FilePath; + + private const uint QOA_MAGIC = 0x716f6166; /* 'qoaf' */ + + public override bool Loaded => QoaHandle != IntPtr.Zero; + + private uint decodeBufferSize; + public override uint DecodeBufferSize => decodeBufferSize; + + public AudioDataQoa(AudioDevice device, string filePath) : base(device) + { + FilePath = filePath; + + using FileStream stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read); + using BinaryReader reader = new BinaryReader(stream); + + UInt64 fileHeader = ReverseEndianness(reader.ReadUInt64()); + if ((fileHeader >> 32) != QOA_MAGIC) + { + throw new InvalidOperationException("Specified file is not a QOA file."); + } + + uint totalSamplesPerChannel = (uint) (fileHeader & (0xFFFFFFFF)); + if (totalSamplesPerChannel == 0) + { + throw new InvalidOperationException("Specified file is not a valid QOA file."); + } + + UInt64 frameHeader = ReverseEndianness(reader.ReadUInt64()); + uint channels = (uint) ((frameHeader >> 56) & 0x0000FF); + uint samplerate = (uint) ((frameHeader >> 32) & 0xFFFFFF); + uint samplesPerChannelPerFrame = (uint) ((frameHeader >> 16) & 0x00FFFF); + + Format = new Format + { + Tag = FormatTag.PCM, + BitsPerSample = 16, + Channels = (ushort) channels, + SampleRate = samplerate + }; + + decodeBufferSize = channels * samplesPerChannelPerFrame * sizeof(short); + } + + public override unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd) + { + int lengthInShorts = bufferLengthInBytes / sizeof(short); + + // NOTE: this function returns samples per channel! + uint samples = FAudio.qoa_decode_next_frame(QoaHandle, (short*) buffer); + + uint sampleCount = samples * Format.Channels; + reachedEnd = sampleCount < lengthInShorts; + filledLengthInBytes = (int) (sampleCount * sizeof(short)); + } + + /// + /// Prepares qoa data for streaming. + /// + public override unsafe void Load() + { + if (!Loaded) + { + FileStream fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read); + FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length); + Span fileDataSpan = new Span((void*) FileDataPtr, (int) fileStream.Length); + fileStream.ReadExactly(fileDataSpan); + fileStream.Close(); + + QoaHandle = FAudio.qoa_open_from_memory((char*) FileDataPtr, (uint) fileDataSpan.Length, 0); + if (QoaHandle == IntPtr.Zero) + { + NativeMemory.Free((void*) FileDataPtr); + Log.Error("Error opening QOA file!"); + throw new InvalidOperationException("Error opening QOA file!"); + } + } + } + + public override void Seek(uint sampleFrame) + { + FAudio.qoa_seek_frame(QoaHandle, (int) sampleFrame); + } + + /// + /// Unloads the qoa data, freeing resources. + /// + public override unsafe void Unload() + { + if (Loaded) + { + FAudio.qoa_close(QoaHandle); + NativeMemory.Free((void*) FileDataPtr); + + QoaHandle = IntPtr.Zero; + FileDataPtr = IntPtr.Zero; + } + } + + /// + /// Loads the entire qoa file into an AudioBuffer. Useful for static audio. + /// + public unsafe static AudioBuffer CreateBuffer(AudioDevice device, string filePath) + { + using FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + void* fileDataPtr = NativeMemory.Alloc((nuint) fileStream.Length); + Span fileDataSpan = new Span(fileDataPtr, (int) fileStream.Length); + fileStream.ReadExactly(fileDataSpan); + fileStream.Close(); + + IntPtr qoaHandle = FAudio.qoa_open_from_memory((char*) fileDataPtr, (uint) fileDataSpan.Length, 0); + if (qoaHandle == 0) + { + NativeMemory.Free(fileDataPtr); + Log.Error("Error opening QOA file!"); + throw new InvalidOperationException("Error opening QOA file!"); + } + + FAudio.qoa_attributes(qoaHandle, out uint channels, out uint samplerate, out uint samples_per_channel_per_frame, out uint total_samples_per_channel); + + uint bufferLengthInBytes = total_samples_per_channel * channels * sizeof(short); + void* buffer = NativeMemory.Alloc(bufferLengthInBytes); + FAudio.qoa_decode_entire(qoaHandle, (short*) buffer); + + FAudio.qoa_close(qoaHandle); + NativeMemory.Free(fileDataPtr); + + Format format = new Format + { + Tag = FormatTag.PCM, + BitsPerSample = 16, + Channels = (ushort) channels, + SampleRate = samplerate + }; + + return new AudioBuffer(device, format, (nint) buffer, bufferLengthInBytes, true); + } + + private static unsafe UInt64 ReverseEndianness(UInt64 value) + { + byte* bytes = (byte*) &value; + + return + ((UInt64)(bytes[0]) << 56) | ((UInt64)(bytes[1]) << 48) | + ((UInt64)(bytes[2]) << 40) | ((UInt64)(bytes[3]) << 32) | + ((UInt64)(bytes[4]) << 24) | ((UInt64)(bytes[5]) << 16) | + ((UInt64)(bytes[6]) << 8) | ((UInt64)(bytes[7]) << 0); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/AudioDataStreamable.cs b/Nerfed.Runtime/Audio/AudioDataStreamable.cs new file mode 100644 index 0000000..c12c104 --- /dev/null +++ b/Nerfed.Runtime/Audio/AudioDataStreamable.cs @@ -0,0 +1,48 @@ +namespace Nerfed.Runtime.Audio; + +/// +/// Use this in conjunction with a StreamingVoice to play back streaming audio data. +/// +public abstract class AudioDataStreamable : AudioResource +{ + public Format Format { get; protected set; } + public abstract bool Loaded { get; } + public abstract uint DecodeBufferSize { get; } + + protected AudioDataStreamable(AudioDevice device) : base(device) + { + } + + /// + /// Loads the raw audio data into memory to prepare it for stream decoding. + /// + public abstract void Load(); + + /// + /// Unloads the raw audio data from memory. + /// + public abstract void Unload(); + + /// + /// Seeks to the given sample frame. + /// + public abstract void Seek(uint sampleFrame); + + /// + /// Attempts to decodes data of length bufferLengthInBytes into the provided buffer. + /// + /// The buffer that decoded bytes will be placed into. + /// Requested length of decoded audio data. + /// How much data was actually filled in by the decode. + /// Whether the end of the data was reached on this decode. + public abstract unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd); + + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + Unload(); + } + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/AudioDataWav.cs b/Nerfed.Runtime/Audio/AudioDataWav.cs new file mode 100644 index 0000000..9463113 --- /dev/null +++ b/Nerfed.Runtime/Audio/AudioDataWav.cs @@ -0,0 +1,99 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Audio; + +public static class AudioDataWav +{ + /// + /// Create an AudioBuffer containing all the WAV audio data in a file. + /// + /// + public unsafe static AudioBuffer CreateBuffer(AudioDevice device, string filePath) + { + // mostly borrowed from https://github.com/FNA-XNA/FNA/blob/b71b4a35ae59970ff0070dea6f8620856d8d4fec/src/Audio/SoundEffect.cs#L385 + + // WaveFormatEx data + ushort wFormatTag; + ushort nChannels; + uint nSamplesPerSec; + uint nAvgBytesPerSec; + ushort nBlockAlign; + ushort wBitsPerSample; + + using FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + using BinaryReader reader = new BinaryReader(stream); + + // RIFF Signature + string signature = new string(reader.ReadChars(4)); + if (signature != "RIFF") + { + throw new NotSupportedException("Specified stream is not a wave file."); + } + + reader.ReadUInt32(); // Riff Chunk Size + + string wformat = new string(reader.ReadChars(4)); + if (wformat != "WAVE") + { + throw new NotSupportedException("Specified stream is not a wave file."); + } + + // WAVE Header + string format_signature = new string(reader.ReadChars(4)); + while (format_signature != "fmt ") + { + reader.ReadBytes(reader.ReadInt32()); + format_signature = new string(reader.ReadChars(4)); + } + + int format_chunk_size = reader.ReadInt32(); + + wFormatTag = reader.ReadUInt16(); + nChannels = reader.ReadUInt16(); + nSamplesPerSec = reader.ReadUInt32(); + nAvgBytesPerSec = reader.ReadUInt32(); + nBlockAlign = reader.ReadUInt16(); + wBitsPerSample = reader.ReadUInt16(); + + // Reads residual bytes + if (format_chunk_size > 16) + { + reader.ReadBytes(format_chunk_size - 16); + } + + // data Signature + string data_signature = new string(reader.ReadChars(4)); + while (data_signature.ToLowerInvariant() != "data") + { + reader.ReadBytes(reader.ReadInt32()); + data_signature = new string(reader.ReadChars(4)); + } + if (data_signature != "data") + { + throw new NotSupportedException("Specified wave file is not supported."); + } + + int waveDataLength = reader.ReadInt32(); + void* waveDataBuffer = NativeMemory.Alloc((nuint) waveDataLength); + Span waveDataSpan = new Span(waveDataBuffer, waveDataLength); + stream.ReadExactly(waveDataSpan); + + Format format = new Format + { + Tag = (FormatTag) wFormatTag, + BitsPerSample = wBitsPerSample, + Channels = nChannels, + SampleRate = nSamplesPerSec + }; + + return new AudioBuffer( + device, + format, + (nint) waveDataBuffer, + (uint) waveDataLength, + true + ); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/AudioDevice.cs b/Nerfed.Runtime/Audio/AudioDevice.cs new file mode 100644 index 0000000..71e464c --- /dev/null +++ b/Nerfed.Runtime/Audio/AudioDevice.cs @@ -0,0 +1,309 @@ +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Audio; + +/// +/// AudioDevice manages all audio-related concerns. +/// +public class AudioDevice : IDisposable +{ + public IntPtr Handle { get; } + public byte[] Handle3D { get; } + public FAudio.FAudioDeviceDetails DeviceDetails { get; } + + private IntPtr trueMasteringVoice; + + // this is a fun little trick where we use a submix voice as a "faux" mastering voice + // this lets us maintain API consistency for effects like panning and reverb + private SubmixVoice fauxMasteringVoice; + public SubmixVoice MasteringVoice => fauxMasteringVoice; + + public float CurveDistanceScalar = 1f; + public float DopplerScale = 1f; + public float SpeedOfSound = 343.5f; + + private readonly HashSet resourceHandles = new HashSet(); + private readonly HashSet updatingSourceVoices = new HashSet(); + + private SourceVoicePool VoicePool; + private List VoicesToReturn = new List(); + + private const int Step = 200; + private TimeSpan UpdateInterval; + private System.Diagnostics.Stopwatch TickStopwatch = new System.Diagnostics.Stopwatch(); + private long previousTickTime; + private Thread Thread; + private AutoResetEvent WakeSignal; + internal readonly object StateLock = new object(); + + private bool Running; + public bool IsDisposed { get; private set; } + + internal unsafe AudioDevice() + { + UpdateInterval = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / Step); + + FAudio.FAudioCreate(out IntPtr handle, 0, FAudio.FAUDIO_DEFAULT_PROCESSOR); + Handle = handle; + + /* Find a suitable device */ + + FAudio.FAudio_GetDeviceCount(Handle, out uint devices); + + if (devices == 0) + { + Log.Error("No audio devices found!"); + FAudio.FAudio_Release(Handle); + Handle = IntPtr.Zero; + return; + } + + FAudio.FAudioDeviceDetails deviceDetails; + + uint i = 0; + for (i = 0; i < devices; i++) + { + FAudio.FAudio_GetDeviceDetails( + Handle, + i, + out deviceDetails + ); + if ((deviceDetails.Role & FAudio.FAudioDeviceRole.FAudioDefaultGameDevice) == FAudio.FAudioDeviceRole.FAudioDefaultGameDevice) + { + DeviceDetails = deviceDetails; + break; + } + } + + if (i == devices) + { + i = 0; /* whatever we'll just use the first one I guess */ + FAudio.FAudio_GetDeviceDetails( + Handle, + i, + out deviceDetails + ); + DeviceDetails = deviceDetails; + } + + /* Init Mastering Voice */ + uint result = FAudio.FAudio_CreateMasteringVoice( + Handle, + out trueMasteringVoice, + FAudio.FAUDIO_DEFAULT_CHANNELS, + FAudio.FAUDIO_DEFAULT_SAMPLERATE, + 0, + i, + IntPtr.Zero + ); + + if (result != 0) + { + Log.Error("Failed to create a mastering voice!"); + Log.Error("Audio device creation failed!"); + return; + } + + fauxMasteringVoice = SubmixVoice.CreateFauxMasteringVoice(this); + + /* Init 3D Audio */ + + Handle3D = new byte[FAudio.F3DAUDIO_HANDLE_BYTESIZE]; + FAudio.F3DAudioInitialize( + DeviceDetails.OutputFormat.dwChannelMask, + SpeedOfSound, + Handle3D + ); + + VoicePool = new SourceVoicePool(this); + + WakeSignal = new AutoResetEvent(true); + + Thread = new Thread(ThreadMain); + Thread.IsBackground = true; + Thread.Start(); + + Running = true; + + TickStopwatch.Start(); + previousTickTime = 0; + } + + private void ThreadMain() + { + while (Running) + { + lock (StateLock) + { + try + { + ThreadMainTick(); + } + catch (Exception e) + { + Log.Error(e.ToString()); + } + } + + WakeSignal.WaitOne(UpdateInterval); + } + } + + private void ThreadMainTick() + { + previousTickTime = TickStopwatch.Elapsed.Ticks; + foreach (UpdatingSourceVoice voice in updatingSourceVoices) + { + voice.Update(); + } + + foreach (SourceVoice voice in VoicesToReturn) + { + if (voice is UpdatingSourceVoice updatingSourceVoice) + { + updatingSourceVoices.Remove(updatingSourceVoice); + } + + voice.Reset(); + VoicePool.Return(voice); + } + + VoicesToReturn.Clear(); + } + + /// + /// Triggers all pending operations with the given syncGroup value. + /// + public void TriggerSyncGroup(uint syncGroup) + { + FAudio.FAudio_CommitChanges(Handle, syncGroup); + } + + /// + /// Obtains an appropriate source voice from the voice pool. + /// + /// The format that the voice must match. + /// A source voice with the given format. + public T Obtain(Format format) where T : SourceVoice, IPoolable + { + lock (StateLock) + { + T voice = VoicePool.Obtain(format); + + if (voice is UpdatingSourceVoice updatingSourceVoice) + { + updatingSourceVoices.Add(updatingSourceVoice); + } + + return voice; + } + } + + /// + /// Returns the source voice to the voice pool. + /// + /// + internal void Return(SourceVoice voice) + { + lock (StateLock) + { + VoicesToReturn.Add(voice); + } + } + + internal void WakeThread() + { + WakeSignal.Set(); + } + + internal void AddResourceReference(GCHandle resourceReference) + { + lock (StateLock) + { + resourceHandles.Add(resourceReference); + + if (resourceReference.Target is UpdatingSourceVoice updatableVoice) + { + updatingSourceVoices.Add(updatableVoice); + } + } + } + + internal void RemoveResourceReference(GCHandle resourceReference) + { + lock (StateLock) + { + resourceHandles.Remove(resourceReference); + + if (resourceReference.Target is UpdatingSourceVoice updatableVoice) + { + updatingSourceVoices.Remove(updatableVoice); + } + } + } + + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + Running = false; + + if (disposing) + { + Thread.Join(); + + // dispose all source voices first + foreach (GCHandle handle in resourceHandles) + { + if (handle.Target is SourceVoice voice) + { + voice.Dispose(); + } + } + + // dispose all submix voices except the faux mastering voice + foreach (GCHandle handle in resourceHandles) + { + if (handle.Target is SubmixVoice voice && voice != fauxMasteringVoice) + { + voice.Dispose(); + } + } + + // dispose the faux mastering voice + fauxMasteringVoice.Dispose(); + + // dispose the true mastering voice + FAudio.FAudioVoice_DestroyVoice(trueMasteringVoice); + + // destroy all other audio resources + foreach (GCHandle handle in resourceHandles) + { + if (handle.Target is AudioResource resource) + { + resource.Dispose(); + } + } + + resourceHandles.Clear(); + } + + FAudio.FAudio_Release(Handle); + + IsDisposed = true; + } + } + + ~AudioDevice() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/AudioEmitter.cs b/Nerfed.Runtime/Audio/AudioEmitter.cs new file mode 100644 index 0000000..99f60f3 --- /dev/null +++ b/Nerfed.Runtime/Audio/AudioEmitter.cs @@ -0,0 +1,135 @@ +using System; +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Audio; + +/// +/// An emitter for 3D spatial audio. +/// +public class AudioEmitter : AudioResource +{ + internal FAudio.F3DAUDIO_EMITTER emitterData; + + public float DopplerScale + { + get + { + return emitterData.DopplerScaler; + } + set + { + if (value < 0.0f) + { + throw new ArgumentOutOfRangeException("AudioEmitter.DopplerScale must be greater than or equal to 0.0f"); + } + emitterData.DopplerScaler = value; + } + } + + public Vector3 Forward + { + get + { + return new Vector3( + emitterData.OrientFront.x, + emitterData.OrientFront.y, + -emitterData.OrientFront.z + ); + } + set + { + emitterData.OrientFront.x = value.X; + emitterData.OrientFront.y = value.Y; + emitterData.OrientFront.z = -value.Z; + } + } + + public Vector3 Position + { + get + { + return new Vector3( + emitterData.Position.x, + emitterData.Position.y, + -emitterData.Position.z + ); + } + set + { + emitterData.Position.x = value.X; + emitterData.Position.y = value.Y; + emitterData.Position.z = -value.Z; + } + } + + + public Vector3 Up + { + get + { + return new Vector3( + emitterData.OrientTop.x, + emitterData.OrientTop.y, + -emitterData.OrientTop.z + ); + } + set + { + emitterData.OrientTop.x = value.X; + emitterData.OrientTop.y = value.Y; + emitterData.OrientTop.z = -value.Z; + } + } + + public Vector3 Velocity + { + get + { + return new Vector3( + emitterData.Velocity.x, + emitterData.Velocity.y, + -emitterData.Velocity.z + ); + } + set + { + emitterData.Velocity.x = value.X; + emitterData.Velocity.y = value.Y; + emitterData.Velocity.z = -value.Z; + } + } + + private static readonly float[] stereoAzimuth = new float[] + { + 0.0f, 0.0f + }; + + private static readonly GCHandle stereoAzimuthHandle = GCHandle.Alloc( + stereoAzimuth, + GCHandleType.Pinned + ); + + public AudioEmitter(AudioDevice device) : base(device) + { + emitterData = new FAudio.F3DAUDIO_EMITTER(); + + DopplerScale = 1f; + Forward = Vector3.UnitZ; + Position = Vector3.Zero; + Up = Vector3.UnitY; + Velocity = Vector3.Zero; + + /* Unexposed variables, defaults based on XNA behavior */ + emitterData.pCone = IntPtr.Zero; + emitterData.ChannelCount = 1; + emitterData.ChannelRadius = 1.0f; + emitterData.pChannelAzimuths = stereoAzimuthHandle.AddrOfPinnedObject(); + emitterData.pVolumeCurve = IntPtr.Zero; + emitterData.pLFECurve = IntPtr.Zero; + emitterData.pLPFDirectCurve = IntPtr.Zero; + emitterData.pLPFReverbCurve = IntPtr.Zero; + emitterData.pReverbCurve = IntPtr.Zero; + emitterData.CurveDistanceScaler = 1.0f; + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/AudioListener.cs b/Nerfed.Runtime/Audio/AudioListener.cs new file mode 100644 index 0000000..992c4b0 --- /dev/null +++ b/Nerfed.Runtime/Audio/AudioListener.cs @@ -0,0 +1,97 @@ +using System; +using System.Numerics; + +namespace Nerfed.Runtime.Audio; + +/// +/// A listener for 3D spatial audio. Usually attached to a camera. +/// +public class AudioListener : AudioResource +{ + internal FAudio.F3DAUDIO_LISTENER listenerData; + + public Vector3 Forward + { + get + { + return new Vector3( + listenerData.OrientFront.x, + listenerData.OrientFront.y, + -listenerData.OrientFront.z + ); + } + set + { + listenerData.OrientFront.x = value.X; + listenerData.OrientFront.y = value.Y; + listenerData.OrientFront.z = -value.Z; + } + } + + public Vector3 Position + { + get + { + return new Vector3( + listenerData.Position.x, + listenerData.Position.y, + -listenerData.Position.z + ); + } + set + { + listenerData.Position.x = value.X; + listenerData.Position.y = value.Y; + listenerData.Position.z = -value.Z; + } + } + + + public Vector3 Up + { + get + { + return new Vector3( + listenerData.OrientTop.x, + listenerData.OrientTop.y, + -listenerData.OrientTop.z + ); + } + set + { + listenerData.OrientTop.x = value.X; + listenerData.OrientTop.y = value.Y; + listenerData.OrientTop.z = -value.Z; + } + } + + public Vector3 Velocity + { + get + { + return new Vector3( + listenerData.Velocity.x, + listenerData.Velocity.y, + -listenerData.Velocity.z + ); + } + set + { + listenerData.Velocity.x = value.X; + listenerData.Velocity.y = value.Y; + listenerData.Velocity.z = -value.Z; + } + } + + public AudioListener(AudioDevice device) : base(device) + { + listenerData = new FAudio.F3DAUDIO_LISTENER(); + Forward = Vector3.UnitZ; + Position = Vector3.Zero; + Up = Vector3.UnitY; + Velocity = Vector3.Zero; + + /* Unexposed variables, defaults based on XNA behavior */ + listenerData.pCone = IntPtr.Zero; + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/AudioResource.cs b/Nerfed.Runtime/Audio/AudioResource.cs new file mode 100644 index 0000000..e6a9d51 --- /dev/null +++ b/Nerfed.Runtime/Audio/AudioResource.cs @@ -0,0 +1,52 @@ +using System; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Audio; + +public abstract class AudioResource : IDisposable +{ + public AudioDevice Device { get; } + + public bool IsDisposed { get; private set; } + + private GCHandle SelfReference; + + protected AudioResource(AudioDevice device) + { + Device = device; + + SelfReference = GCHandle.Alloc(this, GCHandleType.Weak); + Device.AddResourceReference(SelfReference); + } + + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + Device.RemoveResourceReference(SelfReference); + SelfReference.Free(); + } + + IsDisposed = true; + } + } + + ~AudioResource() + { +#if DEBUG + // If you see this log message, you leaked an audio resource without disposing it! + // We can't clean it up for you because this can cause catastrophic issues. + // You should really fix this when it happens. + Log.Warning($"A resource of type {GetType().Name} was not Disposed."); +#endif + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/FilterType.cs b/Nerfed.Runtime/Audio/FilterType.cs new file mode 100644 index 0000000..48e7ef2 --- /dev/null +++ b/Nerfed.Runtime/Audio/FilterType.cs @@ -0,0 +1,9 @@ +namespace Nerfed.Runtime.Audio; + +public enum FilterType +{ + None, + LowPass, + BandPass, + HighPass +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/Format.cs b/Nerfed.Runtime/Audio/Format.cs new file mode 100644 index 0000000..69a2801 --- /dev/null +++ b/Nerfed.Runtime/Audio/Format.cs @@ -0,0 +1,35 @@ +namespace Nerfed.Runtime.Audio; + +public enum FormatTag : ushort +{ + Unknown = 0, + PCM = 1, + MSADPCM = 2, + IEEE_FLOAT = 3 +} + +/// +/// Describes the format of audio data. Usually specified in an audio file's header information. +/// +public record struct Format +{ + public FormatTag Tag; + public ushort Channels; + public uint SampleRate; + public ushort BitsPerSample; + + internal FAudio.FAudioWaveFormatEx ToFAudioFormat() + { + ushort blockAlign = (ushort) ((BitsPerSample / 8) * Channels); + + return new FAudio.FAudioWaveFormatEx + { + wFormatTag = (ushort) Tag, + nChannels = Channels, + nSamplesPerSec = SampleRate, + wBitsPerSample = BitsPerSample, + nBlockAlign = blockAlign, + nAvgBytesPerSec = blockAlign * SampleRate + }; + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/IPoolable.cs b/Nerfed.Runtime/Audio/IPoolable.cs new file mode 100644 index 0000000..1f10855 --- /dev/null +++ b/Nerfed.Runtime/Audio/IPoolable.cs @@ -0,0 +1,6 @@ +namespace Nerfed.Runtime.Audio; + +public interface IPoolable +{ + static abstract T Create(AudioDevice device, Format format); +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/PersistentVoice.cs b/Nerfed.Runtime/Audio/PersistentVoice.cs new file mode 100644 index 0000000..a4f5dac --- /dev/null +++ b/Nerfed.Runtime/Audio/PersistentVoice.cs @@ -0,0 +1,27 @@ +namespace Nerfed.Runtime.Audio; + +/// +/// PersistentVoice should be used when you need to maintain a long-term reference to a source voice. +/// +public class PersistentVoice : SourceVoice, IPoolable +{ + public PersistentVoice(AudioDevice device, Format format) : base(device, format) + { + } + + public static PersistentVoice Create(AudioDevice device, Format format) + { + return new PersistentVoice(device, format); + } + + /// + /// Adds an AudioBuffer to the voice queue. + /// The voice processes and plays back the buffers in its queue in the order that they were submitted. + /// + /// The buffer to submit to the voice. + /// Whether the voice should loop this buffer. + public void Submit(AudioBuffer buffer, bool loop = false) + { + Submit(buffer.ToFAudioBuffer(loop)); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/ReverbEffect.cs b/Nerfed.Runtime/Audio/ReverbEffect.cs new file mode 100644 index 0000000..69ccf28 --- /dev/null +++ b/Nerfed.Runtime/Audio/ReverbEffect.cs @@ -0,0 +1,82 @@ +using System; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Audio; + +/// +/// Use this in conjunction with SourceVoice.SetReverbEffectChain to add reverb to a voice. +/// +public unsafe class ReverbEffect : SubmixVoice +{ + // Defaults based on FAUDIOFX_I3DL2_PRESET_GENERIC + public static FAudio.FAudioFXReverbParameters DefaultParams = new FAudio.FAudioFXReverbParameters + { + WetDryMix = 100.0f, + ReflectionsDelay = 7, + ReverbDelay = 11, + RearDelay = FAudio.FAUDIOFX_REVERB_DEFAULT_REAR_DELAY, + PositionLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION, + PositionRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION, + PositionMatrixLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX, + PositionMatrixRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX, + EarlyDiffusion = 15, + LateDiffusion = 15, + LowEQGain = 8, + LowEQCutoff = 4, + HighEQGain = 8, + HighEQCutoff = 6, + RoomFilterFreq = 5000f, + RoomFilterMain = -10f, + RoomFilterHF = -1f, + ReflectionsGain = -26.0200005f, + ReverbGain = 10.0f, + DecayTime = 1.49000001f, + Density = 100.0f, + RoomSize = FAudio.FAUDIOFX_REVERB_DEFAULT_ROOM_SIZE + }; + + public FAudio.FAudioFXReverbParameters Params { get; private set; } + + public ReverbEffect(AudioDevice audioDevice, uint processingStage) : base(audioDevice, 1, audioDevice.DeviceDetails.OutputFormat.Format.nSamplesPerSec, processingStage) + { + /* Init reverb */ + IntPtr reverb; + FAudio.FAudioCreateReverb(out reverb, 0); + + FAudio.FAudioEffectChain chain = new FAudio.FAudioEffectChain(); + FAudio.FAudioEffectDescriptor descriptor = new FAudio.FAudioEffectDescriptor + { + InitialState = 1, + OutputChannels = 1, + pEffect = reverb + }; + + chain.EffectCount = 1; + chain.pEffectDescriptors = (nint) (&descriptor); + + FAudio.FAudioVoice_SetEffectChain( + Handle, + ref chain + ); + + FAudio.FAPOBase_Release(reverb); + + SetParams(DefaultParams); + } + + public void SetParams(in FAudio.FAudioFXReverbParameters reverbParams) + { + Params = reverbParams; + + fixed (FAudio.FAudioFXReverbParameters* reverbParamsPtr = &reverbParams) + { + FAudio.FAudioVoice_SetEffectParameters( + Handle, + 0, + (nint) reverbParamsPtr, + (uint) Marshal.SizeOf(), + 0 + ); + } + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/SoundSequence.cs b/Nerfed.Runtime/Audio/SoundSequence.cs new file mode 100644 index 0000000..bfed46f --- /dev/null +++ b/Nerfed.Runtime/Audio/SoundSequence.cs @@ -0,0 +1,63 @@ +namespace Nerfed.Runtime.Audio; + +/// +/// Plays back a series of AudioBuffers in sequence. Set the OnSoundNeeded callback to add AudioBuffers dynamically. +/// +public class SoundSequence : UpdatingSourceVoice, IPoolable +{ + public int NeedSoundThreshold = 0; + public delegate void OnSoundNeededFunc(); + public OnSoundNeededFunc OnSoundNeeded; + + public SoundSequence(AudioDevice device, Format format) : base(device, format) + { + + } + + public SoundSequence(AudioDevice device, AudioBuffer templateSound) : base(device, templateSound.Format) + { + + } + + public static SoundSequence Create(AudioDevice device, Format format) + { + return new SoundSequence(device, format); + } + + public override void Update() + { + lock (StateLock) + { + if (State != SoundState.Playing) { return; } + + if (NeedSoundThreshold > 0) + { + int buffersNeeded = NeedSoundThreshold - (int) BuffersQueued; + + for (int i = 0; i < buffersNeeded; i += 1) + { + if (OnSoundNeeded != null) + { + OnSoundNeeded(); + } + } + } + } + } + + public void EnqueueSound(AudioBuffer buffer) + { +#if DEBUG + if (!(buffer.Format == Format)) + { + Log.Warning("Sound sequence audio format mismatch!"); + return; + } +#endif + + lock (StateLock) + { + Submit(buffer.ToFAudioBuffer()); + } + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/SoundState.cs b/Nerfed.Runtime/Audio/SoundState.cs new file mode 100644 index 0000000..fb1722e --- /dev/null +++ b/Nerfed.Runtime/Audio/SoundState.cs @@ -0,0 +1,8 @@ +namespace Nerfed.Runtime.Audio; + +public enum SoundState +{ + Playing, + Paused, + Stopped +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/SourceVoice.cs b/Nerfed.Runtime/Audio/SourceVoice.cs new file mode 100644 index 0000000..367c971 --- /dev/null +++ b/Nerfed.Runtime/Audio/SourceVoice.cs @@ -0,0 +1,217 @@ +using System; + +namespace Nerfed.Runtime.Audio; + +/// +/// Emits audio from submitted audio buffers. +/// +public abstract class SourceVoice : Voice +{ + private Format format; + public Format Format => format; + + protected bool PlaybackInitiated; + + /// + /// The number of buffers queued in the voice. + /// This includes the currently playing voice! + /// + public uint BuffersQueued + { + get + { + FAudio.FAudioSourceVoice_GetState( + Handle, + out FAudio.FAudioVoiceState state, + FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED + ); + + return state.BuffersQueued; + } + } + + private SoundState state = SoundState.Stopped; + public SoundState State + { + get + { + if (BuffersQueued == 0) + { + Stop(); + } + + return state; + } + + internal set + { + state = value; + } + } + + protected object StateLock = new object(); + + public SourceVoice( + AudioDevice device, + Format format + ) : base(device, format.Channels, device.DeviceDetails.OutputFormat.Format.nChannels) + { + this.format = format; + FAudio.FAudioWaveFormatEx fAudioFormat = format.ToFAudioFormat(); + + FAudio.FAudio_CreateSourceVoice( + device.Handle, + out handle, + ref fAudioFormat, + FAudio.FAUDIO_VOICE_USEFILTER, + FAudio.FAUDIO_DEFAULT_FREQ_RATIO, + IntPtr.Zero, + IntPtr.Zero, // default sends to mastering voice! + IntPtr.Zero + ); + + SetOutputVoice(device.MasteringVoice); + } + + /// + /// Starts consumption and processing of audio by the voice. + /// Delivers the result to any connected submix or mastering voice. + /// + /// Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called. + public void Play(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_Start(Handle, 0, syncGroup); + + State = SoundState.Playing; + } + } + + /// + /// Pauses playback. + /// All source buffers that are queued on the voice and the current cursor position are preserved. + /// + /// Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called. + public void Pause(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup); + + State = SoundState.Paused; + } + } + + /// + /// Stops looping the voice when it reaches the end of the current loop region. + /// If the cursor for the voice is not in a loop region, ExitLoop does nothing. + /// + /// Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called. + public void ExitLoop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_ExitLoop(Handle, syncGroup); + } + } + + /// + /// Stops playback and removes all pending audio buffers from the voice queue. + /// + /// Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called. + public void Stop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup); + FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle); + + State = SoundState.Stopped; + } + } + + /// + /// Adds an AudioBuffer to the voice queue. + /// The voice processes and plays back the buffers in its queue in the order that they were submitted. + /// + /// The buffer to submit to the voice. + public void Submit(AudioBuffer buffer) + { + Submit(buffer.ToFAudioBuffer()); + } + + /// + /// Calculates positional sound. This must be called continuously to update positional sound. + /// + /// + /// + public unsafe void Apply3D(AudioListener listener, AudioEmitter emitter) + { + Is3D = true; + + emitter.emitterData.CurveDistanceScaler = Device.CurveDistanceScalar; + emitter.emitterData.ChannelCount = SourceChannelCount; + + FAudio.F3DAUDIO_DSP_SETTINGS dspSettings = new FAudio.F3DAUDIO_DSP_SETTINGS + { + DopplerFactor = DopplerFactor, + SrcChannelCount = SourceChannelCount, + DstChannelCount = DestinationChannelCount, + pMatrixCoefficients = (nint) pMatrixCoefficients + }; + + FAudio.F3DAudioCalculate( + Device.Handle3D, + ref listener.listenerData, + ref emitter.emitterData, + FAudio.F3DAUDIO_CALCULATE_MATRIX | FAudio.F3DAUDIO_CALCULATE_DOPPLER, + ref dspSettings + ); + + UpdatePitch(); + + FAudio.FAudioVoice_SetOutputMatrix( + Handle, + OutputVoice.Handle, + SourceChannelCount, + DestinationChannelCount, + (nint) pMatrixCoefficients, + 0 + ); + } + + /// + /// Specifies that this source voice can be returned to the voice pool. + /// Holding on to the reference after calling this will cause problems! + /// + public void Return() + { + Stop(); + Device.Return(this); + } + + /// + /// Adds an FAudio buffer to the voice queue. + /// The voice processes and plays back the buffers in its queue in the order that they were submitted. + /// + /// The buffer to submit to the voice. + protected void Submit(FAudio.FAudioBuffer buffer) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_SubmitSourceBuffer( + Handle, + ref buffer, + IntPtr.Zero + ); + } + } + + public override void Reset() + { + Stop(); + PlaybackInitiated = false; + base.Reset(); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/SourceVoicePool.cs b/Nerfed.Runtime/Audio/SourceVoicePool.cs new file mode 100644 index 0000000..74308a1 --- /dev/null +++ b/Nerfed.Runtime/Audio/SourceVoicePool.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace Nerfed.Runtime.Audio; + +internal class SourceVoicePool +{ + private AudioDevice Device; + + Dictionary<(System.Type, Format), Queue> VoiceLists = new Dictionary<(System.Type, Format), Queue>(); + + public SourceVoicePool(AudioDevice device) + { + Device = device; + } + + public T Obtain(Format format) where T : SourceVoice, IPoolable + { + if (!VoiceLists.ContainsKey((typeof(T), format))) + { + VoiceLists.Add((typeof(T), format), new Queue()); + } + + Queue list = VoiceLists[(typeof(T), format)]; + + if (list.Count == 0) + { + list.Enqueue(T.Create(Device, format)); + } + + return (T) list.Dequeue(); + } + + public void Return(SourceVoice voice) + { + Queue list = VoiceLists[(voice.GetType(), voice.Format)]; + list.Enqueue(voice); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/StreamingVoice.cs b/Nerfed.Runtime/Audio/StreamingVoice.cs new file mode 100644 index 0000000..2208c5e --- /dev/null +++ b/Nerfed.Runtime/Audio/StreamingVoice.cs @@ -0,0 +1,168 @@ +using System; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Audio; + +/// +/// Use in conjunction with an AudioDataStreamable object to play back streaming audio data. +/// +public class StreamingVoice : UpdatingSourceVoice, IPoolable +{ + private const int BUFFER_COUNT = 3; + private readonly IntPtr[] buffers; + private int nextBufferIndex = 0; + private uint BufferSize; + + public bool Loop { get; set; } + + public AudioDataStreamable AudioData { get; protected set; } + + public unsafe StreamingVoice(AudioDevice device, Format format) : base(device, format) + { + buffers = new IntPtr[BUFFER_COUNT]; + } + + public static StreamingVoice Create(AudioDevice device, Format format) + { + return new StreamingVoice(device, format); + } + + /// + /// Loads and prepares an AudioDataStreamable for streaming playback. + /// This automatically calls Load on the given AudioDataStreamable. + /// + public void Load(AudioDataStreamable data) + { + lock (StateLock) + { + if (AudioData != null) + { + AudioData.Unload(); + } + + data.Load(); + AudioData = data; + + InitializeBuffers(); + QueueBuffers(); + } + } + + /// + /// Unloads AudioDataStreamable from this voice. + /// This automatically calls Unload on the given AudioDataStreamable. + /// + public void Unload() + { + lock (StateLock) + { + if (AudioData != null) + { + Stop(); + AudioData.Unload(); + AudioData = null; + } + } + } + + public override void Reset() + { + Unload(); + base.Reset(); + } + + public override void Update() + { + lock (StateLock) + { + if (AudioData == null || State != SoundState.Playing) + { + return; + } + + QueueBuffers(); + } + } + + private void QueueBuffers() + { + int buffersNeeded = BUFFER_COUNT - (int) BuffersQueued; // don't get got by uint underflow! + for (int i = 0; i < buffersNeeded; i += 1) + { + AddBuffer(); + } + } + + private unsafe void AddBuffer() + { + IntPtr buffer = buffers[nextBufferIndex]; + nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT; + + AudioData.Decode( + (void*) buffer, + (int) BufferSize, + out int filledLengthInBytes, + out bool reachedEnd + ); + + if (filledLengthInBytes > 0) + { + FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer + { + AudioBytes = (uint) filledLengthInBytes, + pAudioData = buffer, + PlayLength = ( + (uint) (filledLengthInBytes / + Format.Channels / + (uint) (Format.BitsPerSample / 8)) + ) + }; + + Submit(buf); + } + + if (reachedEnd) + { + /* We have reached the end of the data, what do we do? */ + if (Loop) + { + AudioData.Seek(0); + } + } + } + + private unsafe void InitializeBuffers() + { + BufferSize = AudioData.DecodeBufferSize; + + for (int i = 0; i < BUFFER_COUNT; i += 1) + { + if (buffers[i] != IntPtr.Zero) + { + NativeMemory.Free((void*) buffers[i]); + } + + buffers[i] = (IntPtr) NativeMemory.Alloc(BufferSize); + } + } + + protected override unsafe void Dispose(bool disposing) + { + if (!IsDisposed) + { + lock (StateLock) + { + Stop(); + + for (int i = 0; i < BUFFER_COUNT; i += 1) + { + if (buffers[i] != IntPtr.Zero) + { + NativeMemory.Free((void*) buffers[i]); + } + } + } + } + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/SubmixVoice.cs b/Nerfed.Runtime/Audio/SubmixVoice.cs new file mode 100644 index 0000000..56707cd --- /dev/null +++ b/Nerfed.Runtime/Audio/SubmixVoice.cs @@ -0,0 +1,55 @@ +using System; + +namespace Nerfed.Runtime.Audio; + +/// +/// SourceVoices can send audio to a SubmixVoice for convenient effects processing. +/// Submixes process in order of processingStage, from lowest to highest. +/// Therefore submixes early in a chain should have a low processingStage, and later in the chain they should have a higher one. +/// +public class SubmixVoice : Voice +{ + public SubmixVoice( + AudioDevice device, + uint sourceChannelCount, + uint sampleRate, + uint processingStage + ) : base(device, sourceChannelCount, device.DeviceDetails.OutputFormat.Format.nChannels) + { + FAudio.FAudio_CreateSubmixVoice( + device.Handle, + out handle, + sourceChannelCount, + sampleRate, + FAudio.FAUDIO_VOICE_USEFILTER, + processingStage, + IntPtr.Zero, + IntPtr.Zero + ); + + SetOutputVoice(device.MasteringVoice); + } + + private SubmixVoice( + AudioDevice device + ) : base(device, device.DeviceDetails.OutputFormat.Format.nChannels, device.DeviceDetails.OutputFormat.Format.nChannels) + { + FAudio.FAudio_CreateSubmixVoice( + device.Handle, + out handle, + device.DeviceDetails.OutputFormat.Format.nChannels, + device.DeviceDetails.OutputFormat.Format.nSamplesPerSec, + FAudio.FAUDIO_VOICE_USEFILTER, + int.MaxValue, + IntPtr.Zero, // default sends to mastering voice + IntPtr.Zero + ); + + OutputVoice = null; + } + + internal static SubmixVoice CreateFauxMasteringVoice(AudioDevice device) + { + return new SubmixVoice(device); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/TransientVoice.cs b/Nerfed.Runtime/Audio/TransientVoice.cs new file mode 100644 index 0000000..f202387 --- /dev/null +++ b/Nerfed.Runtime/Audio/TransientVoice.cs @@ -0,0 +1,28 @@ +namespace Nerfed.Runtime.Audio; + +/// +/// TransientVoice is intended for playing one-off sound effects that don't have a long term reference.
+/// It will be automatically returned to the AudioDevice SourceVoice pool once it is done playing back. +///
+public class TransientVoice : UpdatingSourceVoice, IPoolable +{ + static TransientVoice IPoolable.Create(AudioDevice device, Format format) + { + return new TransientVoice(device, format); + } + + public TransientVoice(AudioDevice device, Format format) : base(device, format) + { + } + + public override void Update() + { + lock (StateLock) + { + if (PlaybackInitiated && BuffersQueued == 0) + { + Return(); + } + } + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/UpdatingSourceVoice.cs b/Nerfed.Runtime/Audio/UpdatingSourceVoice.cs new file mode 100644 index 0000000..77ccf28 --- /dev/null +++ b/Nerfed.Runtime/Audio/UpdatingSourceVoice.cs @@ -0,0 +1,10 @@ +namespace Nerfed.Runtime.Audio; + +public abstract class UpdatingSourceVoice : SourceVoice +{ + protected UpdatingSourceVoice(AudioDevice device, Format format) : base(device, format) + { + } + + public abstract void Update(); +} \ No newline at end of file diff --git a/Nerfed.Runtime/Audio/Voice.cs b/Nerfed.Runtime/Audio/Voice.cs new file mode 100644 index 0000000..b046b86 --- /dev/null +++ b/Nerfed.Runtime/Audio/Voice.cs @@ -0,0 +1,433 @@ +using System; +using System.Runtime.InteropServices; +using EasingFunction = System.Func; + +namespace Nerfed.Runtime.Audio; + +/// +/// Handles audio playback from audio buffer data. Can be configured with a variety of parameters. +/// +public abstract unsafe class Voice : AudioResource +{ + protected IntPtr handle; + public IntPtr Handle => handle; + + public uint SourceChannelCount { get; } + public uint DestinationChannelCount { get; } + + protected SubmixVoice OutputVoice; + private ReverbEffect ReverbEffect; + + protected byte* pMatrixCoefficients; + + public bool Is3D { get; protected set; } + + private float dopplerFactor; + /// + /// The strength of the doppler effect on this voice. + /// + public float DopplerFactor + { + get => dopplerFactor; + set + { + if (dopplerFactor != value) + { + dopplerFactor = value; + UpdatePitch(); + } + } + } + + private float volume = 1; + /// + /// The overall volume level for the voice. + /// + public float Volume + { + get => volume; + internal set + { + value = MathF.Max(0f, value); + if (volume != value) + { + volume = value; + FAudio.FAudioVoice_SetVolume(Handle, volume, 0); + } + } + } + + private float pitch = 0; + /// + /// The pitch of the voice. + /// + public float Pitch + { + get => pitch; + internal set + { + value = Math.Clamp(value, -1f, 1f); + if (pitch != value) + { + pitch = value; + UpdatePitch(); + } + } + } + + private const float MAX_FILTER_FREQUENCY = 1f; + private const float MAX_FILTER_ONEOVERQ = 1.5f; + + private FAudio.FAudioFilterParameters filterParameters = new FAudio.FAudioFilterParameters + { + Type = FAudio.FAudioFilterType.FAudioLowPassFilter, + Frequency = 1f, + OneOverQ = 1f + }; + + /// + /// The frequency cutoff on the voice filter. + /// + public float FilterFrequency + { + get => filterParameters.Frequency; + internal set + { + value = System.Math.Clamp(value, 0.01f, MAX_FILTER_FREQUENCY); + if (filterParameters.Frequency != value) + { + filterParameters.Frequency = value; + + FAudio.FAudioVoice_SetFilterParameters( + Handle, + ref filterParameters, + 0 + ); + } + } + } + + /// + /// Reciprocal of Q factor. + /// Controls how quickly frequencies beyond the filter frequency are dampened. + /// + public float FilterOneOverQ + { + get => filterParameters.OneOverQ; + internal set + { + value = System.Math.Clamp(value, 0.01f, MAX_FILTER_ONEOVERQ); + if (filterParameters.OneOverQ != value) + { + filterParameters.OneOverQ = value; + + FAudio.FAudioVoice_SetFilterParameters( + Handle, + ref filterParameters, + 0 + ); + } + } + } + + private FilterType filterType; + /// + /// The frequency filter that is applied to the voice. + /// + public FilterType FilterType + { + get => filterType; + set + { + if (filterType != value) + { + filterType = value; + + switch (filterType) + { + case FilterType.None: + filterParameters = new FAudio.FAudioFilterParameters + { + Type = FAudio.FAudioFilterType.FAudioLowPassFilter, + Frequency = 1f, + OneOverQ = 1f + }; + break; + + case FilterType.LowPass: + filterParameters.Type = FAudio.FAudioFilterType.FAudioLowPassFilter; + filterParameters.Frequency = 1f; + break; + + case FilterType.BandPass: + filterParameters.Type = FAudio.FAudioFilterType.FAudioBandPassFilter; + break; + + case FilterType.HighPass: + filterParameters.Type = FAudio.FAudioFilterType.FAudioHighPassFilter; + filterParameters.Frequency = 0f; + break; + } + + FAudio.FAudioVoice_SetFilterParameters( + Handle, + ref filterParameters, + 0 + ); + } + } + } + + protected float pan = 0; + /// + /// Left-right panning. -1 is hard left pan, 1 is hard right pan. + /// + public float Pan + { + get => pan; + internal set + { + value = Math.Clamp(value, -1f, 1f); + if (pan != value) + { + pan = value; + + if (pan < -1f) + { + pan = -1f; + } + if (pan > 1f) + { + pan = 1f; + } + + if (Is3D) { return; } + + SetPanMatrixCoefficients(); + FAudio.FAudioVoice_SetOutputMatrix( + Handle, + OutputVoice.Handle, + SourceChannelCount, + DestinationChannelCount, + (nint) pMatrixCoefficients, + 0 + ); + } + } + } + + private float reverb; + /// + /// The wet-dry mix of the reverb effect. + /// Has no effect if SetReverbEffectChain has not been called. + /// + public unsafe float Reverb + { + get => reverb; + internal set + { + if (ReverbEffect != null) + { + value = MathF.Max(0, value); + if (reverb != value) + { + reverb = value; + + float* outputMatrix = (float*) pMatrixCoefficients; + outputMatrix[0] = reverb; + if (SourceChannelCount == 2) + { + outputMatrix[1] = reverb; + } + + FAudio.FAudioVoice_SetOutputMatrix( + Handle, + ReverbEffect.Handle, + SourceChannelCount, + 1, + (nint) pMatrixCoefficients, + 0 + ); + } + } + +#if DEBUG + if (ReverbEffect == null) + { + Log.Warning("Tried to set reverb value before applying a reverb effect"); + } +#endif + } + } + + public Voice(AudioDevice device, uint sourceChannelCount, uint destinationChannelCount) : base(device) + { + SourceChannelCount = sourceChannelCount; + DestinationChannelCount = destinationChannelCount; + nuint memsize = 4 * sourceChannelCount * destinationChannelCount; + pMatrixCoefficients = (byte*) NativeMemory.AllocZeroed(memsize); + SetPanMatrixCoefficients(); + } + + /// + /// Sets the output voice for this voice. + /// + /// Where the output should be sent. + public unsafe void SetOutputVoice(SubmixVoice send) + { + OutputVoice = send; + + if (ReverbEffect != null) + { + SetReverbEffectChain(ReverbEffect); + } + else + { + FAudio.FAudioSendDescriptor* sendDesc = stackalloc FAudio.FAudioSendDescriptor[1]; + sendDesc[0].Flags = 0; + sendDesc[0].pOutputVoice = send.Handle; + + FAudio.FAudioVoiceSends sends = new FAudio.FAudioVoiceSends(); + sends.SendCount = 1; + sends.pSends = (nint) sendDesc; + + FAudio.FAudioVoice_SetOutputVoices( + Handle, + ref sends + ); + } + } + + /// + /// Applies a reverb effect chain to this voice. + /// + public unsafe void SetReverbEffectChain(ReverbEffect reverbEffect) + { + FAudio.FAudioSendDescriptor* sendDesc = stackalloc FAudio.FAudioSendDescriptor[2]; + sendDesc[0].Flags = 0; + sendDesc[0].pOutputVoice = OutputVoice.Handle; + sendDesc[1].Flags = 0; + sendDesc[1].pOutputVoice = reverbEffect.Handle; + + FAudio.FAudioVoiceSends sends = new FAudio.FAudioVoiceSends(); + sends.SendCount = 2; + sends.pSends = (nint) sendDesc; + + FAudio.FAudioVoice_SetOutputVoices( + Handle, + ref sends + ); + + ReverbEffect = reverbEffect; + } + + /// + /// Removes the reverb effect chain from this voice. + /// + public void RemoveReverbEffectChain() + { + if (ReverbEffect != null) + { + ReverbEffect = null; + reverb = 0; + SetOutputVoice(OutputVoice); + } + } + + /// + /// Resets all voice parameters to defaults. + /// + public virtual void Reset() + { + RemoveReverbEffectChain(); + Volume = 1; + Pan = 0; + Pitch = 0; + FilterType = FilterType.None; + SetOutputVoice(Device.MasteringVoice); + } + + // Taken from https://github.com/FNA-XNA/FNA/blob/master/src/Audio/SoundEffectInstance.cs + private unsafe void SetPanMatrixCoefficients() + { + /* Two major things to notice: + * 1. The spec assumes any speaker count >= 2 has Front Left/Right. + * 2. Stereo panning is WAY more complicated than you think. + * The main thing is that hard panning does NOT eliminate an + * entire channel; the two channels are blended on each side. + * -flibit + */ + float* outputMatrix = (float*) pMatrixCoefficients; + if (SourceChannelCount == 1) + { + if (DestinationChannelCount == 1) + { + outputMatrix[0] = 1.0f; + } + else + { + outputMatrix[0] = (pan > 0.0f) ? (1.0f - pan) : 1.0f; + outputMatrix[1] = (pan < 0.0f) ? (1.0f + pan) : 1.0f; + } + } + else + { + if (DestinationChannelCount == 1) + { + outputMatrix[0] = 1.0f; + outputMatrix[1] = 1.0f; + } + else + { + if (pan <= 0.0f) + { + // Left speaker blends left/right channels + outputMatrix[0] = 0.5f * pan + 1.0f; + outputMatrix[1] = 0.5f * -pan; + // Right speaker gets less of the right channel + outputMatrix[2] = 0.0f; + outputMatrix[3] = pan + 1.0f; + } + else + { + // Left speaker gets less of the left channel + outputMatrix[0] = -pan + 1.0f; + outputMatrix[1] = 0.0f; + // Right speaker blends right/left channels + outputMatrix[2] = 0.5f * pan; + outputMatrix[3] = 0.5f * -pan + 1.0f; + } + } + } + } + + protected void UpdatePitch() + { + float doppler; + float dopplerScale = Device.DopplerScale; + if (!Is3D || dopplerScale == 0.0f) + { + doppler = 1.0f; + } + else + { + doppler = DopplerFactor * dopplerScale; + } + + FAudio.FAudioSourceVoice_SetFrequencyRatio( + Handle, + (float) System.Math.Pow(2.0, pitch) * doppler, + 0 + ); + } + + protected override unsafe void Dispose(bool disposing) + { + if (!IsDisposed) + { + NativeMemory.Free(pMatrixCoefficients); + FAudio.FAudioVoice_DestroyVoice(Handle); + } + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Engine.cs b/Nerfed.Runtime/Engine.cs new file mode 100644 index 0000000..b714d2c --- /dev/null +++ b/Nerfed.Runtime/Engine.cs @@ -0,0 +1,252 @@ +using System.Diagnostics; +using Nerfed.Runtime.Audio; +using Nerfed.Runtime.Graphics; +using SDL2; + +namespace Nerfed.Runtime; + +public static class Engine +{ + public static TimeSpan MaxDeltaTime { get; set; } = TimeSpan.FromMilliseconds(100); + public static bool VSync { get; set; } + + public static GraphicsDevice GraphicsDevice { get; private set; } + public static AudioDevice AudioDevice { get; private set; } + public static Window MainWindow { get; private set; } + public static TimeSpan Timestep { get; private set; } + + private static bool quit; + private static Stopwatch gameTimer; + private static long previousTicks = 0; + private static TimeSpan accumulatedUpdateTime = TimeSpan.Zero; + + private static TimeSpan accumulatedDrawTime = TimeSpan.Zero; + + // must be a power of 2 so we can do a bitmask optimization when checking worst case + private const int previousSleepTimeCount = 128; + private const int sleepTimeMask = previousSleepTimeCount - 1; + private static readonly TimeSpan[] previousSleepTimes = new TimeSpan[previousSleepTimeCount]; + private static int sleepTimeIndex; + private static TimeSpan worstCaseSleepPrecision = TimeSpan.FromMilliseconds(1); + private static bool framerateCapped; + private static TimeSpan framerateCapTimeSpan = TimeSpan.Zero; + + //TODO: These are temp + private const int MaxFps = 300; + private const int TargetTimestep = 60; + private const int WindowWidth = 1280; + private const int WindowHeight = 720; + private const string WindowTitle = "Nerfed"; + //.. + + internal static void Run(string[] args) + { + Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / TargetTimestep); + gameTimer = Stopwatch.StartNew(); + SetFrameLimiter(new FrameLimiterSettings(FrameLimiterMode.Capped, MaxFps)); + + for (int i = 0; i < previousSleepTimes.Length; i += 1) + { + previousSleepTimes[i] = TimeSpan.FromMilliseconds(1); + } + + if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_TIMER | SDL.SDL_INIT_GAMECONTROLLER) < 0) + { + throw new Exception("Failed to init SDL"); + } + + GraphicsDevice = new GraphicsDevice(BackendFlags.All); + + MainWindow = new Window(GraphicsDevice, new WindowCreateInfo(WindowTitle, WindowWidth, WindowHeight, ScreenMode.Windowed)); + if (!GraphicsDevice.ClaimWindow(MainWindow, SwapchainComposition.SDR, VSync ? PresentMode.VSync : PresentMode.Mailbox)) + { + throw new Exception("Failed to claim window"); + } + + AudioDevice = new AudioDevice(); + + while (!quit) + { + Tick(); + } + + GraphicsDevice.UnclaimWindow(MainWindow); + MainWindow.Dispose(); + GraphicsDevice.Dispose(); + AudioDevice.Dispose(); + SDL.SDL_Quit(); + } + + /// + /// Updates the frame limiter settings. + /// + public static void SetFrameLimiter(FrameLimiterSettings settings) + { + framerateCapped = settings.Mode == FrameLimiterMode.Capped; + + if (framerateCapped) + { + framerateCapTimeSpan = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / settings.Cap); + } + else + { + framerateCapTimeSpan = TimeSpan.Zero; + } + } + + public static void Quit() + { + quit = true; + } + + private static void Tick() + { + AdvanceElapsedTime(); + + if (framerateCapped) + { + /* We want to wait until the framerate cap, + * but we don't want to oversleep. Requesting repeated 1ms sleeps and + * seeing how long we actually slept for lets us estimate the worst case + * sleep precision so we don't oversleep the next frame. + */ + while (accumulatedDrawTime + worstCaseSleepPrecision < framerateCapTimeSpan) + { + Thread.Sleep(1); + TimeSpan timeAdvancedSinceSleeping = AdvanceElapsedTime(); + UpdateEstimatedSleepPrecision(timeAdvancedSinceSleeping); + } + + /* Now that we have slept into the sleep precision threshold, we need to wait + * for just a little bit longer until the target elapsed time has been reached. + * SpinWait(1) works by pausing the thread for very short intervals, so it is + * an efficient and time-accurate way to wait out the rest of the time. + */ + while (accumulatedDrawTime < framerateCapTimeSpan) + { + Thread.SpinWait(1); + AdvanceElapsedTime(); + } + } + + // Do not let any step take longer than our maximum. + if (accumulatedUpdateTime > MaxDeltaTime) + { + accumulatedUpdateTime = MaxDeltaTime; + } + + if (!quit) + { + while (accumulatedUpdateTime >= Timestep) + { + Keyboard.Update(); + Mouse.Update(); + GamePad.Update(); + + ProcessSDLEvents(); + + // Tick game here... + + AudioDevice.WakeThread(); + accumulatedUpdateTime -= Timestep; + } + + double alpha = accumulatedUpdateTime / Timestep; + + // Render here.. + + accumulatedDrawTime -= framerateCapTimeSpan; + } + } + + private static TimeSpan AdvanceElapsedTime() + { + long currentTicks = gameTimer.Elapsed.Ticks; + TimeSpan timeAdvanced = TimeSpan.FromTicks(currentTicks - previousTicks); + accumulatedUpdateTime += timeAdvanced; + accumulatedDrawTime += timeAdvanced; + previousTicks = currentTicks; + return timeAdvanced; + } + + private static void ProcessSDLEvents() + { + while (SDL.SDL_PollEvent(out SDL.SDL_Event ev) == 1) + { + switch (ev.type) + { + case SDL.SDL_EventType.SDL_QUIT: + Quit(); + break; + case SDL.SDL_EventType.SDL_TEXTINPUT: + case SDL.SDL_EventType.SDL_KEYDOWN: + case SDL.SDL_EventType.SDL_KEYUP: + Keyboard.ProcessEvent(ref ev); + break; + case SDL.SDL_EventType.SDL_MOUSEBUTTONDOWN: + case SDL.SDL_EventType.SDL_MOUSEBUTTONUP: + case SDL.SDL_EventType.SDL_MOUSEWHEEL: + case SDL.SDL_EventType.SDL_MOUSEMOTION: + Mouse.ProcessEvent(ref ev); + break; + case SDL.SDL_EventType.SDL_CONTROLLERDEVICEADDED: + case SDL.SDL_EventType.SDL_CONTROLLERDEVICEREMOVED: + case SDL.SDL_EventType.SDL_CONTROLLERBUTTONDOWN: + case SDL.SDL_EventType.SDL_CONTROLLERBUTTONUP: + case SDL.SDL_EventType.SDL_CONTROLLERAXISMOTION: + case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADDOWN: + case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADUP: + case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADMOTION: + case SDL.SDL_EventType.SDL_CONTROLLERSENSORUPDATE: + GamePad.ProcessEvent(ref ev); + break; + case SDL.SDL_EventType.SDL_WINDOWEVENT: + Window.ProcessEvent(ref ev); + break; + } + } + } + + /* To calculate the sleep precision of the OS, we take the worst case + * time spent sleeping over the results of previous requests to sleep 1ms. + */ + private static void UpdateEstimatedSleepPrecision(TimeSpan timeSpentSleeping) + { + /* It is unlikely that the scheduler will actually be more imprecise than + * 4ms and we don't want to get wrecked by a single long sleep so we cap this + * value at 4ms for sanity. + */ + TimeSpan upperTimeBound = TimeSpan.FromMilliseconds(4); + + if (timeSpentSleeping > upperTimeBound) + { + timeSpentSleeping = upperTimeBound; + } + + /* We know the previous worst case - it's saved in worstCaseSleepPrecision. + * We also know the current index. So the only way the worst case changes + * is if we either 1) just got a new worst case, or 2) the worst case was + * the oldest entry on the list. + */ + if (timeSpentSleeping >= worstCaseSleepPrecision) + { + worstCaseSleepPrecision = timeSpentSleeping; + } + else if (previousSleepTimes[sleepTimeIndex] == worstCaseSleepPrecision) + { + TimeSpan maxSleepTime = TimeSpan.MinValue; + for (int i = 0; i < previousSleepTimes.Length; i++) + { + if (previousSleepTimes[i] > maxSleepTime) + { + maxSleepTime = previousSleepTimes[i]; + } + } + + worstCaseSleepPrecision = maxSleepTime; + } + + previousSleepTimes[sleepTimeIndex] = timeSpentSleeping; + sleepTimeIndex = (sleepTimeIndex + 1) & sleepTimeMask; + } +} diff --git a/Nerfed.Runtime/FrameLimiterSettings.cs b/Nerfed.Runtime/FrameLimiterSettings.cs new file mode 100644 index 0000000..2dd9083 --- /dev/null +++ b/Nerfed.Runtime/FrameLimiterSettings.cs @@ -0,0 +1,35 @@ +namespace Nerfed.Runtime; + +public enum FrameLimiterMode +{ + /// + /// The game will render at the maximum possible framerate that the computing resources allow.
+ /// Note that this may lead to overheating, resource starvation, etc. + ///
+ Uncapped, + /// + /// The game will render no more than the specified frames per second. + /// + Capped +} + +/// +/// The Game's frame limiter setting. Specifies uncapped framerate or a maximum rendering frames per second value.
+/// Note that this is separate from the Game's Update timestep and can be a different value. +///
+public struct FrameLimiterSettings +{ + public FrameLimiterMode Mode; + /// + /// If Mode is set to Capped, this is the maximum frames per second that will be rendered. + /// + public int Cap; + + public FrameLimiterSettings( + FrameLimiterMode mode, + int cap + ) { + Mode = mode; + Cap = cap; + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/Color.cs b/Nerfed.Runtime/Graphics/Color.cs new file mode 100644 index 0000000..c676c43 --- /dev/null +++ b/Nerfed.Runtime/Graphics/Color.cs @@ -0,0 +1,1948 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Diagnostics; +using System.Numerics; +using System.Text; +using Nerfed.Runtime.Graphics.PackedVector; + +#endregion + +namespace Nerfed.Runtime.Graphics; + +/// +/// Describes a 32-bit packed color. +/// +[Serializable] +[DebuggerDisplay("{DebugDisplayString,nq}")] +public struct Color : IEquatable, IPackedVector, IPackedVector +{ + #region Public Properties + + /// + /// Gets or sets the red component. + /// + public byte R + { + get + { + unchecked + { + return (byte) (this.packedValue); + } + } + set + { + this.packedValue = (this.packedValue & 0xffffff00) | value; + } + } + + /// + /// Gets or sets the green component. + /// + public byte G + { + get + { + unchecked + { + return (byte) (this.packedValue >> 8); + } + } + set + { + this.packedValue = (this.packedValue & 0xffff00ff) | ((uint) value << 8); + } + } + + /// + /// Gets or sets the blue component. + /// + public byte B + { + get + { + unchecked + { + return (byte) (this.packedValue >> 16); + } + } + set + { + this.packedValue = (this.packedValue & 0xff00ffff) | ((uint) value << 16); + } + } + + /// + /// Gets or sets the alpha component. + /// + public byte A + { + get + { + unchecked + { + return (byte) (this.packedValue >> 24); + } + } + set + { + this.packedValue = (this.packedValue & 0x00ffffff) | ((uint) value << 24); + } + } + + /// + /// Gets or sets packed value of this . + /// + public UInt32 PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Public Static Color Properties + + /// + /// Transparent color (R:0,G:0,B:0,A:0). + /// + public static Color Transparent + { + get; + private set; + } + + /// + /// AliceBlue color (R:240,G:248,B:255,A:255). + /// + public static Color AliceBlue + { + get; + private set; + } + + /// + /// AntiqueWhite color (R:250,G:235,B:215,A:255). + /// + public static Color AntiqueWhite + { + get; + private set; + } + + /// + /// Aqua color (R:0,G:255,B:255,A:255). + /// + public static Color Aqua + { + get; + private set; + } + + /// + /// Aquamarine color (R:127,G:255,B:212,A:255). + /// + public static Color Aquamarine + { + get; + private set; + } + + /// + /// Azure color (R:240,G:255,B:255,A:255). + /// + public static Color Azure + { + get; + private set; + } + + /// + /// Beige color (R:245,G:245,B:220,A:255). + /// + public static Color Beige + { + get; + private set; + } + + /// + /// Bisque color (R:255,G:228,B:196,A:255). + /// + public static Color Bisque + { + get; + private set; + } + + /// + /// Black color (R:0,G:0,B:0,A:255). + /// + public static Color Black + { + get; + private set; + } + + /// + /// BlanchedAlmond color (R:255,G:235,B:205,A:255). + /// + public static Color BlanchedAlmond + { + get; + private set; + } + + /// + /// Blue color (R:0,G:0,B:255,A:255). + /// + public static Color Blue + { + get; + private set; + } + + /// + /// BlueViolet color (R:138,G:43,B:226,A:255). + /// + public static Color BlueViolet + { + get; + private set; + } + + /// + /// Brown color (R:165,G:42,B:42,A:255). + /// + public static Color Brown + { + get; + private set; + } + + /// + /// BurlyWood color (R:222,G:184,B:135,A:255). + /// + public static Color BurlyWood + { + get; + private set; + } + + /// + /// CadetBlue color (R:95,G:158,B:160,A:255). + /// + public static Color CadetBlue + { + get; + private set; + } + + /// + /// Chartreuse color (R:127,G:255,B:0,A:255). + /// + public static Color Chartreuse + { + get; + private set; + } + + /// + /// Chocolate color (R:210,G:105,B:30,A:255). + /// + public static Color Chocolate + { + get; + private set; + } + + /// + /// Coral color (R:255,G:127,B:80,A:255). + /// + public static Color Coral + { + get; + private set; + } + + /// + /// CornflowerBlue color (R:100,G:149,B:237,A:255). + /// + public static Color CornflowerBlue + { + get; + private set; + } + + /// + /// Cornsilk color (R:255,G:248,B:220,A:255). + /// + public static Color Cornsilk + { + get; + private set; + } + + /// + /// Crimson color (R:220,G:20,B:60,A:255). + /// + public static Color Crimson + { + get; + private set; + } + + /// + /// Cyan color (R:0,G:255,B:255,A:255). + /// + public static Color Cyan + { + get; + private set; + } + + /// + /// DarkBlue color (R:0,G:0,B:139,A:255). + /// + public static Color DarkBlue + { + get; + private set; + } + + /// + /// DarkCyan color (R:0,G:139,B:139,A:255). + /// + public static Color DarkCyan + { + get; + private set; + } + + /// + /// DarkGoldenrod color (R:184,G:134,B:11,A:255). + /// + public static Color DarkGoldenrod + { + get; + private set; + } + + /// + /// DarkGray color (R:169,G:169,B:169,A:255). + /// + public static Color DarkGray + { + get; + private set; + } + + /// + /// DarkGreen color (R:0,G:100,B:0,A:255). + /// + public static Color DarkGreen + { + get; + private set; + } + + /// + /// DarkKhaki color (R:189,G:183,B:107,A:255). + /// + public static Color DarkKhaki + { + get; + private set; + } + + /// + /// DarkMagenta color (R:139,G:0,B:139,A:255). + /// + public static Color DarkMagenta + { + get; + private set; + } + + /// + /// DarkOliveGreen color (R:85,G:107,B:47,A:255). + /// + public static Color DarkOliveGreen + { + get; + private set; + } + + /// + /// DarkOrange color (R:255,G:140,B:0,A:255). + /// + public static Color DarkOrange + { + get; + private set; + } + + /// + /// DarkOrchid color (R:153,G:50,B:204,A:255). + /// + public static Color DarkOrchid + { + get; + private set; + } + + /// + /// DarkRed color (R:139,G:0,B:0,A:255). + /// + public static Color DarkRed + { + get; + private set; + } + + /// + /// DarkSalmon color (R:233,G:150,B:122,A:255). + /// + public static Color DarkSalmon + { + get; + private set; + } + + /// + /// DarkSeaGreen color (R:143,G:188,B:139,A:255). + /// + public static Color DarkSeaGreen + { + get; + private set; + } + + /// + /// DarkSlateBlue color (R:72,G:61,B:139,A:255). + /// + public static Color DarkSlateBlue + { + get; + private set; + } + + /// + /// DarkSlateGray color (R:47,G:79,B:79,A:255). + /// + public static Color DarkSlateGray + { + get; + private set; + } + + /// + /// DarkTurquoise color (R:0,G:206,B:209,A:255). + /// + public static Color DarkTurquoise + { + get; + private set; + } + + /// + /// DarkViolet color (R:148,G:0,B:211,A:255). + /// + public static Color DarkViolet + { + get; + private set; + } + + /// + /// DeepPink color (R:255,G:20,B:147,A:255). + /// + public static Color DeepPink + { + get; + private set; + } + + /// + /// DeepSkyBlue color (R:0,G:191,B:255,A:255). + /// + public static Color DeepSkyBlue + { + get; + private set; + } + + /// + /// DimGray color (R:105,G:105,B:105,A:255). + /// + public static Color DimGray + { + get; + private set; + } + + /// + /// DodgerBlue color (R:30,G:144,B:255,A:255). + /// + public static Color DodgerBlue + { + get; + private set; + } + + /// + /// Firebrick color (R:178,G:34,B:34,A:255). + /// + public static Color Firebrick + { + get; + private set; + } + + /// + /// FloralWhite color (R:255,G:250,B:240,A:255). + /// + public static Color FloralWhite + { + get; + private set; + } + + /// + /// ForestGreen color (R:34,G:139,B:34,A:255). + /// + public static Color ForestGreen + { + get; + private set; + } + + /// + /// Fuchsia color (R:255,G:0,B:255,A:255). + /// + public static Color Fuchsia + { + get; + private set; + } + + /// + /// Gainsboro color (R:220,G:220,B:220,A:255). + /// + public static Color Gainsboro + { + get; + private set; + } + + /// + /// GhostWhite color (R:248,G:248,B:255,A:255). + /// + public static Color GhostWhite + { + get; + private set; + } + /// + /// Gold color (R:255,G:215,B:0,A:255). + /// + public static Color Gold + { + get; + private set; + } + + /// + /// Goldenrod color (R:218,G:165,B:32,A:255). + /// + public static Color Goldenrod + { + get; + private set; + } + + /// + /// Gray color (R:128,G:128,B:128,A:255). + /// + public static Color Gray + { + get; + private set; + } + + /// + /// Green color (R:0,G:128,B:0,A:255). + /// + public static Color Green + { + get; + private set; + } + + /// + /// GreenYellow color (R:173,G:255,B:47,A:255). + /// + public static Color GreenYellow + { + get; + private set; + } + + /// + /// Honeydew color (R:240,G:255,B:240,A:255). + /// + public static Color Honeydew + { + get; + private set; + } + + /// + /// HotPink color (R:255,G:105,B:180,A:255). + /// + public static Color HotPink + { + get; + private set; + } + + /// + /// IndianRed color (R:205,G:92,B:92,A:255). + /// + public static Color IndianRed + { + get; + private set; + } + + /// + /// Indigo color (R:75,G:0,B:130,A:255). + /// + public static Color Indigo + { + get; + private set; + } + + /// + /// Ivory color (R:255,G:255,B:240,A:255). + /// + public static Color Ivory + { + get; + private set; + } + + /// + /// Khaki color (R:240,G:230,B:140,A:255). + /// + public static Color Khaki + { + get; + private set; + } + + /// + /// Lavender color (R:230,G:230,B:250,A:255). + /// + public static Color Lavender + { + get; + private set; + } + + /// + /// LavenderBlush color (R:255,G:240,B:245,A:255). + /// + public static Color LavenderBlush + { + get; + private set; + } + + /// + /// LawnGreen color (R:124,G:252,B:0,A:255). + /// + public static Color LawnGreen + { + get; + private set; + } + + /// + /// LemonChiffon color (R:255,G:250,B:205,A:255). + /// + public static Color LemonChiffon + { + get; + private set; + } + + /// + /// LightBlue color (R:173,G:216,B:230,A:255). + /// + public static Color LightBlue + { + get; + private set; + } + + /// + /// LightCoral color (R:240,G:128,B:128,A:255). + /// + public static Color LightCoral + { + get; + private set; + } + + /// + /// LightCyan color (R:224,G:255,B:255,A:255). + /// + public static Color LightCyan + { + get; + private set; + } + + /// + /// LightGoldenrodYellow color (R:250,G:250,B:210,A:255). + /// + public static Color LightGoldenrodYellow + { + get; + private set; + } + + /// + /// LightGray color (R:211,G:211,B:211,A:255). + /// + public static Color LightGray + { + get; + private set; + } + + /// + /// LightGreen color (R:144,G:238,B:144,A:255). + /// + public static Color LightGreen + { + get; + private set; + } + + /// + /// LightPink color (R:255,G:182,B:193,A:255). + /// + public static Color LightPink + { + get; + private set; + } + + /// + /// LightSalmon color (R:255,G:160,B:122,A:255). + /// + public static Color LightSalmon + { + get; + private set; + } + + /// + /// LightSeaGreen color (R:32,G:178,B:170,A:255). + /// + public static Color LightSeaGreen + { + get; + private set; + } + + /// + /// LightSkyBlue color (R:135,G:206,B:250,A:255). + /// + public static Color LightSkyBlue + { + get; + private set; + } + + /// + /// LightSlateGray color (R:119,G:136,B:153,A:255). + /// + public static Color LightSlateGray + { + get; + private set; + } + + /// + /// LightSteelBlue color (R:176,G:196,B:222,A:255). + /// + public static Color LightSteelBlue + { + get; + private set; + } + + /// + /// LightYellow color (R:255,G:255,B:224,A:255). + /// + public static Color LightYellow + { + get; + private set; + } + + /// + /// Lime color (R:0,G:255,B:0,A:255). + /// + public static Color Lime + { + get; + private set; + } + + /// + /// LimeGreen color (R:50,G:205,B:50,A:255). + /// + public static Color LimeGreen + { + get; + private set; + } + + /// + /// Linen color (R:250,G:240,B:230,A:255). + /// + public static Color Linen + { + get; + private set; + } + + /// + /// Magenta color (R:255,G:0,B:255,A:255). + /// + public static Color Magenta + { + get; + private set; + } + + /// + /// Maroon color (R:128,G:0,B:0,A:255). + /// + public static Color Maroon + { + get; + private set; + } + + /// + /// MediumAquamarine color (R:102,G:205,B:170,A:255). + /// + public static Color MediumAquamarine + { + get; + private set; + } + + /// + /// MediumBlue color (R:0,G:0,B:205,A:255). + /// + public static Color MediumBlue + { + get; + private set; + } + + /// + /// MediumOrchid color (R:186,G:85,B:211,A:255). + /// + public static Color MediumOrchid + { + get; + private set; + } + + /// + /// MediumPurple color (R:147,G:112,B:219,A:255). + /// + public static Color MediumPurple + { + get; + private set; + } + + /// + /// MediumSeaGreen color (R:60,G:179,B:113,A:255). + /// + public static Color MediumSeaGreen + { + get; + private set; + } + + /// + /// MediumSlateBlue color (R:123,G:104,B:238,A:255). + /// + public static Color MediumSlateBlue + { + get; + private set; + } + + /// + /// MediumSpringGreen color (R:0,G:250,B:154,A:255). + /// + public static Color MediumSpringGreen + { + get; + private set; + } + + /// + /// MediumTurquoise color (R:72,G:209,B:204,A:255). + /// + public static Color MediumTurquoise + { + get; + private set; + } + + /// + /// MediumVioletRed color (R:199,G:21,B:133,A:255). + /// + public static Color MediumVioletRed + { + get; + private set; + } + + /// + /// MidnightBlue color (R:25,G:25,B:112,A:255). + /// + public static Color MidnightBlue + { + get; + private set; + } + + /// + /// MintCream color (R:245,G:255,B:250,A:255). + /// + public static Color MintCream + { + get; + private set; + } + + /// + /// MistyRose color (R:255,G:228,B:225,A:255). + /// + public static Color MistyRose + { + get; + private set; + } + + /// + /// Moccasin color (R:255,G:228,B:181,A:255). + /// + public static Color Moccasin + { + get; + private set; + } + + /// + /// NavajoWhite color (R:255,G:222,B:173,A:255). + /// + public static Color NavajoWhite + { + get; + private set; + } + + /// + /// Navy color (R:0,G:0,B:128,A:255). + /// + public static Color Navy + { + get; + private set; + } + + /// + /// OldLace color (R:253,G:245,B:230,A:255). + /// + public static Color OldLace + { + get; + private set; + } + + /// + /// Olive color (R:128,G:128,B:0,A:255). + /// + public static Color Olive + { + get; + private set; + } + + /// + /// OliveDrab color (R:107,G:142,B:35,A:255). + /// + public static Color OliveDrab + { + get; + private set; + } + + /// + /// Orange color (R:255,G:165,B:0,A:255). + /// + public static Color Orange + { + get; + private set; + } + + /// + /// OrangeRed color (R:255,G:69,B:0,A:255). + /// + public static Color OrangeRed + { + get; + private set; + } + + /// + /// Orchid color (R:218,G:112,B:214,A:255). + /// + public static Color Orchid + { + get; + private set; + } + + /// + /// PaleGoldenrod color (R:238,G:232,B:170,A:255). + /// + public static Color PaleGoldenrod + { + get; + private set; + } + + /// + /// PaleGreen color (R:152,G:251,B:152,A:255). + /// + public static Color PaleGreen + { + get; + private set; + } + + /// + /// PaleTurquoise color (R:175,G:238,B:238,A:255). + /// + public static Color PaleTurquoise + { + get; + private set; + } + /// + /// PaleVioletRed color (R:219,G:112,B:147,A:255). + /// + public static Color PaleVioletRed + { + get; + private set; + } + + /// + /// PapayaWhip color (R:255,G:239,B:213,A:255). + /// + public static Color PapayaWhip + { + get; + private set; + } + + /// + /// PeachPuff color (R:255,G:218,B:185,A:255). + /// + public static Color PeachPuff + { + get; + private set; + } + + /// + /// Peru color (R:205,G:133,B:63,A:255). + /// + public static Color Peru + { + get; + private set; + } + + /// + /// Pink color (R:255,G:192,B:203,A:255). + /// + public static Color Pink + { + get; + private set; + } + + /// + /// Plum color (R:221,G:160,B:221,A:255). + /// + public static Color Plum + { + get; + private set; + } + + /// + /// PowderBlue color (R:176,G:224,B:230,A:255). + /// + public static Color PowderBlue + { + get; + private set; + } + + /// + /// Purple color (R:128,G:0,B:128,A:255). + /// + public static Color Purple + { + get; + private set; + } + + /// + /// Red color (R:255,G:0,B:0,A:255). + /// + public static Color Red + { + get; + private set; + } + + /// + /// RosyBrown color (R:188,G:143,B:143,A:255). + /// + public static Color RosyBrown + { + get; + private set; + } + + /// + /// RoyalBlue color (R:65,G:105,B:225,A:255). + /// + public static Color RoyalBlue + { + get; + private set; + } + + /// + /// SaddleBrown color (R:139,G:69,B:19,A:255). + /// + public static Color SaddleBrown + { + get; + private set; + } + + /// + /// Salmon color (R:250,G:128,B:114,A:255). + /// + public static Color Salmon + { + get; + private set; + } + + /// + /// SandyBrown color (R:244,G:164,B:96,A:255). + /// + public static Color SandyBrown + { + get; + private set; + } + + /// + /// SeaGreen color (R:46,G:139,B:87,A:255). + /// + public static Color SeaGreen + { + get; + private set; + } + + /// + /// SeaShell color (R:255,G:245,B:238,A:255). + /// + public static Color SeaShell + { + get; + private set; + } + + /// + /// Sienna color (R:160,G:82,B:45,A:255). + /// + public static Color Sienna + { + get; + private set; + } + + /// + /// Silver color (R:192,G:192,B:192,A:255). + /// + public static Color Silver + { + get; + private set; + } + + /// + /// SkyBlue color (R:135,G:206,B:235,A:255). + /// + public static Color SkyBlue + { + get; + private set; + } + + /// + /// SlateBlue color (R:106,G:90,B:205,A:255). + /// + public static Color SlateBlue + { + get; + private set; + } + + /// + /// SlateGray color (R:112,G:128,B:144,A:255). + /// + public static Color SlateGray + { + get; + private set; + } + + /// + /// Snow color (R:255,G:250,B:250,A:255). + /// + public static Color Snow + { + get; + private set; + } + + /// + /// SpringGreen color (R:0,G:255,B:127,A:255). + /// + public static Color SpringGreen + { + get; + private set; + } + + /// + /// SteelBlue color (R:70,G:130,B:180,A:255). + /// + public static Color SteelBlue + { + get; + private set; + } + + /// + /// Tan color (R:210,G:180,B:140,A:255). + /// + public static Color Tan + { + get; + private set; + } + + /// + /// Teal color (R:0,G:128,B:128,A:255). + /// + public static Color Teal + { + get; + private set; + } + + /// + /// Thistle color (R:216,G:191,B:216,A:255). + /// + public static Color Thistle + { + get; + private set; + } + + /// + /// Tomato color (R:255,G:99,B:71,A:255). + /// + public static Color Tomato + { + get; + private set; + } + + /// + /// Turquoise color (R:64,G:224,B:208,A:255). + /// + public static Color Turquoise + { + get; + private set; + } + + /// + /// Violet color (R:238,G:130,B:238,A:255). + /// + public static Color Violet + { + get; + private set; + } + + /// + /// Wheat color (R:245,G:222,B:179,A:255). + /// + public static Color Wheat + { + get; + private set; + } + + /// + /// White color (R:255,G:255,B:255,A:255). + /// + public static Color White + { + get; + private set; + } + + /// + /// WhiteSmoke color (R:245,G:245,B:245,A:255). + /// + public static Color WhiteSmoke + { + get; + private set; + } + + /// + /// Yellow color (R:255,G:255,B:0,A:255). + /// + public static Color Yellow + { + get; + private set; + } + + /// + /// YellowGreen color (R:154,G:205,B:50,A:255). + /// + public static Color YellowGreen + { + get; + private set; + } + + #endregion + + #region Internal Properties + + internal string DebugDisplayString + { + get + { + return string.Concat( + R.ToString(), " ", + G.ToString(), " ", + B.ToString(), " ", + A.ToString() + ); + } + } + + #endregion + + #region Private Variables + + // ARGB. Keep this name as it is used by XNA games in reflection! + private uint packedValue; + + #endregion + + #region Private Static Constructors + + static Color() + { + Transparent = new Color(0); + AliceBlue = new Color(0xfffff8f0); + AntiqueWhite = new Color(0xffd7ebfa); + Aqua = new Color(0xffffff00); + Aquamarine = new Color(0xffd4ff7f); + Azure = new Color(0xfffffff0); + Beige = new Color(0xffdcf5f5); + Bisque = new Color(0xffc4e4ff); + Black = new Color(0xff000000); + BlanchedAlmond = new Color(0xffcdebff); + Blue = new Color(0xffff0000); + BlueViolet = new Color(0xffe22b8a); + Brown = new Color(0xff2a2aa5); + BurlyWood = new Color(0xff87b8de); + CadetBlue = new Color(0xffa09e5f); + Chartreuse = new Color(0xff00ff7f); + Chocolate = new Color(0xff1e69d2); + Coral = new Color(0xff507fff); + CornflowerBlue = new Color(0xffed9564); + Cornsilk = new Color(0xffdcf8ff); + Crimson = new Color(0xff3c14dc); + Cyan = new Color(0xffffff00); + DarkBlue = new Color(0xff8b0000); + DarkCyan = new Color(0xff8b8b00); + DarkGoldenrod = new Color(0xff0b86b8); + DarkGray = new Color(0xffa9a9a9); + DarkGreen = new Color(0xff006400); + DarkKhaki = new Color(0xff6bb7bd); + DarkMagenta = new Color(0xff8b008b); + DarkOliveGreen = new Color(0xff2f6b55); + DarkOrange = new Color(0xff008cff); + DarkOrchid = new Color(0xffcc3299); + DarkRed = new Color(0xff00008b); + DarkSalmon = new Color(0xff7a96e9); + DarkSeaGreen = new Color(0xff8bbc8f); + DarkSlateBlue = new Color(0xff8b3d48); + DarkSlateGray = new Color(0xff4f4f2f); + DarkTurquoise = new Color(0xffd1ce00); + DarkViolet = new Color(0xffd30094); + DeepPink = new Color(0xff9314ff); + DeepSkyBlue = new Color(0xffffbf00); + DimGray = new Color(0xff696969); + DodgerBlue = new Color(0xffff901e); + Firebrick = new Color(0xff2222b2); + FloralWhite = new Color(0xfff0faff); + ForestGreen = new Color(0xff228b22); + Fuchsia = new Color(0xffff00ff); + Gainsboro = new Color(0xffdcdcdc); + GhostWhite = new Color(0xfffff8f8); + Gold = new Color(0xff00d7ff); + Goldenrod = new Color(0xff20a5da); + Gray = new Color(0xff808080); + Green = new Color(0xff008000); + GreenYellow = new Color(0xff2fffad); + Honeydew = new Color(0xfff0fff0); + HotPink = new Color(0xffb469ff); + IndianRed = new Color(0xff5c5ccd); + Indigo = new Color(0xff82004b); + Ivory = new Color(0xfff0ffff); + Khaki = new Color(0xff8ce6f0); + Lavender = new Color(0xfffae6e6); + LavenderBlush = new Color(0xfff5f0ff); + LawnGreen = new Color(0xff00fc7c); + LemonChiffon = new Color(0xffcdfaff); + LightBlue = new Color(0xffe6d8ad); + LightCoral = new Color(0xff8080f0); + LightCyan = new Color(0xffffffe0); + LightGoldenrodYellow = new Color(0xffd2fafa); + LightGray = new Color(0xffd3d3d3); + LightGreen = new Color(0xff90ee90); + LightPink = new Color(0xffc1b6ff); + LightSalmon = new Color(0xff7aa0ff); + LightSeaGreen = new Color(0xffaab220); + LightSkyBlue = new Color(0xffface87); + LightSlateGray = new Color(0xff998877); + LightSteelBlue = new Color(0xffdec4b0); + LightYellow = new Color(0xffe0ffff); + Lime = new Color(0xff00ff00); + LimeGreen = new Color(0xff32cd32); + Linen = new Color(0xffe6f0fa); + Magenta = new Color(0xffff00ff); + Maroon = new Color(0xff000080); + MediumAquamarine = new Color(0xffaacd66); + MediumBlue = new Color(0xffcd0000); + MediumOrchid = new Color(0xffd355ba); + MediumPurple = new Color(0xffdb7093); + MediumSeaGreen = new Color(0xff71b33c); + MediumSlateBlue = new Color(0xffee687b); + MediumSpringGreen = new Color(0xff9afa00); + MediumTurquoise = new Color(0xffccd148); + MediumVioletRed = new Color(0xff8515c7); + MidnightBlue = new Color(0xff701919); + MintCream = new Color(0xfffafff5); + MistyRose = new Color(0xffe1e4ff); + Moccasin = new Color(0xffb5e4ff); + NavajoWhite = new Color(0xffaddeff); + Navy = new Color(0xff800000); + OldLace = new Color(0xffe6f5fd); + Olive = new Color(0xff008080); + OliveDrab = new Color(0xff238e6b); + Orange = new Color(0xff00a5ff); + OrangeRed = new Color(0xff0045ff); + Orchid = new Color(0xffd670da); + PaleGoldenrod = new Color(0xffaae8ee); + PaleGreen = new Color(0xff98fb98); + PaleTurquoise = new Color(0xffeeeeaf); + PaleVioletRed = new Color(0xff9370db); + PapayaWhip = new Color(0xffd5efff); + PeachPuff = new Color(0xffb9daff); + Peru = new Color(0xff3f85cd); + Pink = new Color(0xffcbc0ff); + Plum = new Color(0xffdda0dd); + PowderBlue = new Color(0xffe6e0b0); + Purple = new Color(0xff800080); + Red = new Color(0xff0000ff); + RosyBrown = new Color(0xff8f8fbc); + RoyalBlue = new Color(0xffe16941); + SaddleBrown = new Color(0xff13458b); + Salmon = new Color(0xff7280fa); + SandyBrown = new Color(0xff60a4f4); + SeaGreen = new Color(0xff578b2e); + SeaShell = new Color(0xffeef5ff); + Sienna = new Color(0xff2d52a0); + Silver = new Color(0xffc0c0c0); + SkyBlue = new Color(0xffebce87); + SlateBlue = new Color(0xffcd5a6a); + SlateGray = new Color(0xff908070); + Snow = new Color(0xfffafaff); + SpringGreen = new Color(0xff7fff00); + SteelBlue = new Color(0xffb48246); + Tan = new Color(0xff8cb4d2); + Teal = new Color(0xff808000); + Thistle = new Color(0xffd8bfd8); + Tomato = new Color(0xff4763ff); + Turquoise = new Color(0xffd0e040); + Violet = new Color(0xffee82ee); + Wheat = new Color(0xffb3def5); + White = new Color(uint.MaxValue); + WhiteSmoke = new Color(0xfff5f5f5); + Yellow = new Color(0xff00ffff); + YellowGreen = new Color(0xff32cd9a); + } + + #endregion + + #region Public Constructors + + /// + /// Creates a new instance of struct. + /// + /// A representing a color. + public Color(Vector4 color) + { + packedValue = 0; + + R = (byte) Math.Clamp(color.X * 255, Byte.MinValue, Byte.MaxValue); + G = (byte) Math.Clamp(color.Y * 255, Byte.MinValue, Byte.MaxValue); + B = (byte) Math.Clamp(color.Z * 255, Byte.MinValue, Byte.MaxValue); + A = (byte) Math.Clamp(color.W * 255, Byte.MinValue, Byte.MaxValue); + } + + /// + /// Constructs an RGBA color from the XYZW unit length components of a vector. + /// + /// A representing a color. + public Color(Vector3 color) + { + packedValue = 0; + + R = (byte) Math.Clamp(color.X * 255, Byte.MinValue, Byte.MaxValue); + G = (byte) Math.Clamp(color.Y * 255, Byte.MinValue, Byte.MaxValue); + B = (byte) Math.Clamp(color.Z * 255, Byte.MinValue, Byte.MaxValue); + A = 255; + } + + /// + /// Constructs an RGBA color from scalars which representing red, green and blue values. Alpha value will be opaque. + /// + /// Red component value from 0.0f to 1.0f. + /// Green component value from 0.0f to 1.0f. + /// Blue component value from 0.0f to 1.0f. + public Color(float r, float g, float b) + { + packedValue = 0; + + R = (byte) Math.Clamp(r * 255, Byte.MinValue, Byte.MaxValue); + G = (byte) Math.Clamp(g * 255, Byte.MinValue, Byte.MaxValue); + B = (byte) Math.Clamp(b * 255, Byte.MinValue, Byte.MaxValue); + A = 255; + } + + /// + /// Constructs an RGBA color from scalars which representing red, green and blue values. Alpha value will be opaque. + /// + /// Red component value from 0 to 255. + /// Green component value from 0 to 255. + /// Blue component value from 0 to 255. + public Color(int r, int g, int b) + { + packedValue = 0; + R = (byte) Math.Clamp(r, Byte.MinValue, Byte.MaxValue); + G = (byte) Math.Clamp(g, Byte.MinValue, Byte.MaxValue); + B = (byte) Math.Clamp(b, Byte.MinValue, Byte.MaxValue); + A = (byte) 255; + } + + /// + /// Constructs an RGBA color from scalars which representing red, green, blue and alpha values. + /// + /// Red component value from 0 to 255. + /// Green component value from 0 to 255. + /// Blue component value from 0 to 255. + /// Alpha component value from 0 to 255. + public Color(int r, int g, int b, int alpha) + { + packedValue = 0; + R = (byte) Math.Clamp(r, Byte.MinValue, Byte.MaxValue); + G = (byte) Math.Clamp(g, Byte.MinValue, Byte.MaxValue); + B = (byte) Math.Clamp(b, Byte.MinValue, Byte.MaxValue); + A = (byte) Math.Clamp(alpha, Byte.MinValue, Byte.MaxValue); + } + + /// + /// Constructs an RGBA color from scalars which representing red, green, blue and alpha values. + /// + /// Red component value from 0.0f to 1.0f. + /// Green component value from 0.0f to 1.0f. + /// Blue component value from 0.0f to 1.0f. + /// Alpha component value from 0.0f to 1.0f. + public Color(float r, float g, float b, float alpha) + { + packedValue = 0; + + R = (byte) Math.Clamp(r * 255, Byte.MinValue, Byte.MaxValue); + G = (byte) Math.Clamp(g * 255, Byte.MinValue, Byte.MaxValue); + B = (byte) Math.Clamp(b * 255, Byte.MinValue, Byte.MaxValue); + A = (byte) Math.Clamp(alpha * 255, Byte.MinValue, Byte.MaxValue); + } + + #endregion + + #region Private Constructors + + private Color(uint packedValue) + { + this.packedValue = packedValue; + } + + #endregion + + #region Public Methods + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public bool Equals(Color other) + { + return this.PackedValue == other.PackedValue; + } + + /// + /// Gets a representation for this object. + /// + /// A representation for this object. + public Vector3 ToVector3() + { + return new Vector3(R / 255.0f, G / 255.0f, B / 255.0f); + } + + /// + /// Gets a representation for this object. + /// + /// A representation for this object. + public Vector4 ToVector4() + { + return new Vector4(R / 255.0f, G / 255.0f, B / 255.0f, A / 255.0f); + } + + #endregion + + #region Public Static Methods + + /// + /// Performs linear interpolation of . + /// + /// Source . + /// Destination . + /// Interpolation factor. + /// Interpolated . + public static Color Lerp(Color value1, Color value2, float amount) + { + amount = Math.Clamp(amount, 0.0f, 1.0f); + return new Color( + (int) float.Lerp(value1.R, value2.R, amount), + (int) float.Lerp(value1.G, value2.G, amount), + (int) float.Lerp(value1.B, value2.B, amount), + (int) float.Lerp(value1.A, value2.A, amount) + ); + } + + /// + /// Translate a non-premultipled alpha to a + /// that contains premultiplied alpha. + /// + /// A representing color. + /// A which contains premultiplied alpha data. + public static Color FromNonPremultiplied(Vector4 vector) + { + return new Color( + vector.X * vector.W, + vector.Y * vector.W, + vector.Z * vector.W, + vector.W + ); + } + + /// + /// Translate a non-premultipled alpha to a + /// that contains premultiplied alpha. + /// + /// Red component value. + /// Green component value. + /// Blue component value. + /// Alpha component value. + /// A which contains premultiplied alpha data. + public static Color FromNonPremultiplied(int r, int g, int b, int a) + { + return new Color( + (r * a / 255), + (g * a / 255), + (b * a / 255), + a + ); + } + + // Modified from one of the responses here: + // https://stackoverflow.com/questions/3018313/algorithm-to-convert-rgb-to-hsv-and-hsv-to-rgb-in-range-0-255-for-both/6930407#6930407 + public static Color FromHSV(float r, float g, float b) + { + r = (100 + r) % 1f; + + float hueSlice = 6 * r; // [0, 6) + float hueSliceInteger = MathF.Floor(hueSlice); + + // In [0,1) for each hue slice + float hueSliceInterpolant = hueSlice - hueSliceInteger; + + Vector3 tempRGB = new Vector3( + b * (1f - g), + b * (1f - g * hueSliceInterpolant), + b * (1f - g * (1f - hueSliceInterpolant)) + ); + + // The idea here to avoid conditions is to notice that the conversion code can be rewritten: + // if ( var_i == 0 ) { R = V ; G = TempRGB.z ; B = TempRGB.x } + // else if ( var_i == 2 ) { R = TempRGB.x ; G = V ; B = TempRGB.z } + // else if ( var_i == 4 ) { R = TempRGB.z ; G = TempRGB.x ; B = V } + // + // else if ( var_i == 1 ) { R = TempRGB.y ; G = V ; B = TempRGB.x } + // else if ( var_i == 3 ) { R = TempRGB.x ; G = TempRGB.y ; B = V } + // else if ( var_i == 5 ) { R = V ; G = TempRGB.x ; B = TempRGB.y } + // + // This shows several things: + // . A separation between even and odd slices + // . If slices (0,2,4) and (1,3,5) can be rewritten as basically being slices (0,1,2) then + // the operation simply amounts to performing a "rotate right" on the RGB components + // . The base value to rotate is either (V, B, R) for even slices or (G, V, R) for odd slices + // + float isOddSlice = hueSliceInteger % 2f; // 0 if even (slices 0, 2, 4), 1 if odd (slices 1, 3, 5) + float threeSliceSelector = 0.5f * (hueSliceInteger - isOddSlice); // (0, 1, 2) corresponding to slices (0, 2, 4) and (1, 3, 5) + + Vector3 scrollingRGBForEvenSlices = new Vector3(b, tempRGB.Z, tempRGB.X); // (V, Temp Blue, Temp Red) for even slices (0, 2, 4) + Vector3 scrollingRGBForOddSlices = new Vector3(tempRGB.Y, b, tempRGB.X); // (Temp Green, V, Temp Red) for odd slices (1, 3, 5) + Vector3 scrollingRGB = Vector3.Lerp(scrollingRGBForEvenSlices, scrollingRGBForOddSlices, isOddSlice); + + float IsNotFirstSlice = Math.Clamp(threeSliceSelector, 0f, 1f); // 1 if NOT the first slice (true for slices 1 and 2) + float IsNotSecondSlice = Math.Clamp(threeSliceSelector - 1f, 0f, 1f); // 1 if NOT the first or second slice (true only for slice 2) + + Vector3 color = Vector3.Lerp( + scrollingRGB, + Vector3.Lerp( + new Vector3(scrollingRGB.Z, scrollingRGB.X, scrollingRGB.Y), + new Vector3(scrollingRGB.Y, scrollingRGB.Z, scrollingRGB.X), + IsNotSecondSlice + ), + IsNotFirstSlice + ); + + return new Color(color); + } + + public static Color FromHSV(int r, int g, int b) + { + return Color.FromHSV(r / 255f, g / 255f, b / 255f); + } + + #endregion + + #region Public Static Operators and Override Methods + + /// + /// Compares whether two instances are equal. + /// + /// instance on the left of the equal sign. + /// instance on the right of the equal sign. + /// True if the instances are equal; false otherwise. + public static bool operator ==(Color a, Color b) + { + return (a.A == b.A && + a.R == b.R && + a.G == b.G && + a.B == b.B); + } + + /// + /// Compares whether two instances are not equal. + /// + /// + /// instance on the left of the not equal sign. + /// + /// + /// instance on the right of the not equal sign. + /// + /// + /// True if the instances are not equal; false otherwise. + /// + public static bool operator !=(Color a, Color b) + { + return !(a == b); + } + + /// + /// Gets the hash code of this . + /// + /// Hash code of this . + public override int GetHashCode() + { + return this.packedValue.GetHashCode(); + } + + /// + /// Compares whether current instance is equal to specified object. + /// + /// The to compare. + /// True if the instances are equal; false otherwise. + public override bool Equals(object obj) + { + return ((obj is Color) && this.Equals((Color) obj)); + } + + /// + /// Multiply by value. + /// + /// Source . + /// Multiplicator. + /// Multiplication result. + public static Color Multiply(Color value, float scale) + { + return new Color( + (int) (value.R * scale), + (int) (value.G * scale), + (int) (value.B * scale), + (int) (value.A * scale) + ); + } + + /// + /// Multiply by value. + /// + /// Source . + /// Multiplicator. + /// Multiplication result. + public static Color operator *(Color value, float scale) + { + return new Color( + (int) (value.R * scale), + (int) (value.G * scale), + (int) (value.B * scale), + (int) (value.A * scale) + ); + } + + /// + /// Returns a representation of this in the format: + /// {R:[red] G:[green] B:[blue] A:[alpha]} + /// + /// representation of this . + public override string ToString() + { + StringBuilder sb = new StringBuilder(25); + sb.Append("{R:"); + sb.Append(R); + sb.Append(" G:"); + sb.Append(G); + sb.Append(" B:"); + sb.Append(B); + sb.Append(" A:"); + sb.Append(A); + sb.Append("}"); + return sb.ToString(); + } + + #endregion + + #region IPackedVector Member + + /// + /// Pack a four-component color from a vector format into the format of a color object. + /// + /// A four-component color. + void IPackedVector.PackFromVector4(Vector4 vector) + { + // Should we round here? + R = (byte) (vector.X * 255.0f); + G = (byte) (vector.Y * 255.0f); + B = (byte) (vector.Z * 255.0f); + A = (byte) (vector.W * 255.0f); + } + + #endregion + +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/CommandBuffer.cs b/Nerfed.Runtime/Graphics/CommandBuffer.cs new file mode 100644 index 0000000..b84a584 --- /dev/null +++ b/Nerfed.Runtime/Graphics/CommandBuffer.cs @@ -0,0 +1,1042 @@ +using System; +using System.Runtime.InteropServices; +using RefreshCS; + +namespace Nerfed.Runtime.Graphics; + +/// +/// A structure that buffers commands to be submitted to the GPU. +/// These commands include operations like copying data, draw calls, and compute dispatches. +/// Commands only begin executing once the command buffer is submitted. +/// If you need to know when the command buffer is done, use GraphicsDevice.SubmitAndAcquireFence +/// +public class CommandBuffer +{ + public GraphicsDevice Device { get; } + public IntPtr Handle { get; internal set; } + +#if DEBUG + bool swapchainTextureAcquired; + + ComputePipeline currentComputePipeline; + + bool renderPassActive; + bool copyPassActive; + bool computePassActive; + + internal bool Submitted; +#endif + + // called from CommandBufferPool + internal CommandBuffer(GraphicsDevice device) + { + Device = device; + Handle = IntPtr.Zero; + +#if DEBUG + ResetStateTracking(); +#endif + } + + internal void SetHandle(nint handle) + { + Handle = handle; + } + +#if DEBUG + internal void ResetStateTracking() + { + swapchainTextureAcquired = false; + + currentComputePipeline = null; + + renderPassActive = false; + copyPassActive = false; + computePassActive = false; + + Submitted = false; + } +#endif + + /// + /// Acquires a swapchain texture. + /// This texture will be presented to the given window when the command buffer is submitted. + /// Can return null if the swapchain is unavailable. The user should ALWAYS handle the case where this occurs. + /// If null is returned, presentation will not occur. + /// It is an error to acquire two swapchain textures from the same window in one command buffer. + /// It is an error to dispose the swapchain texture. If you do this your game WILL crash. DO NOT DO THIS. + /// + public Texture AcquireSwapchainTexture( + Window window + ) { +#if DEBUG + AssertNotSubmitted(); + + if (!window.Claimed) + { + throw new System.InvalidOperationException("Cannot acquire swapchain texture, window has not been claimed!"); + } + + if (swapchainTextureAcquired) + { + throw new System.InvalidOperationException("Cannot acquire two swapchain textures on the same command buffer!"); + } +#endif + + IntPtr texturePtr = Refresh.Refresh_AcquireSwapchainTexture( + Handle, + window.Handle, + out uint width, + out uint height + ); + + if (texturePtr == IntPtr.Zero) + { + return null; + } + + // Override the texture properties to avoid allocating a new texture instance! + window.SwapchainTexture.Handle = texturePtr; + window.SwapchainTexture.Width = width; + window.SwapchainTexture.Height = height; + window.SwapchainTexture.Format = window.SwapchainFormat; + +#if DEBUG + swapchainTextureAcquired = true; +#endif + + return window.SwapchainTexture; + } + + /// + /// Pushes data to a vertex uniform slot on the command buffer. + /// Subsequent draw calls will use this uniform data. + /// It is legal to push uniforms during a render or compute pass. + /// + public unsafe void PushVertexUniformData( + void* uniformsPtr, + uint size, + uint slot = 0 + ) { + Refresh.Refresh_PushVertexUniformData( + Handle, + slot, + (nint) uniformsPtr, + size + ); + } + + /// + /// Pushes data to a vertex uniform slot on the command buffer. + /// Subsequent draw calls will use this uniform data. + /// It is legal to push uniforms during a render or compute pass. + /// + public unsafe void PushVertexUniformData( + in T uniforms, + uint slot = 0 + ) where T : unmanaged + { + fixed (T* uniformsPtr = &uniforms) + { + PushVertexUniformData(uniformsPtr, (uint) Marshal.SizeOf(), slot); + } + } + + /// + /// Pushes data to a fragment uniform slot on the command buffer. + /// Subsequent draw calls will use this uniform data. + /// It is legal to push uniforms during a pass. + /// + public unsafe void PushFragmentUniformData( + void* uniformsPtr, + uint size, + uint slot = 0 + ) { + Refresh.Refresh_PushFragmentUniformData( + Handle, + slot, + (nint) uniformsPtr, + size + ); + } + + /// + /// Pushes data to a fragment uniform slot on the command buffer. + /// Subsequent draw calls will use this uniform data. + /// It is legal to push uniforms during a pass. + /// + public unsafe void PushFragmentUniformData( + in T uniforms, + uint slot = 0 + ) where T : unmanaged + { + fixed (T* uniformsPtr = &uniforms) + { + PushFragmentUniformData(uniformsPtr, (uint) Marshal.SizeOf(), slot); + } + } + + /// + /// Pushes data to a compute uniform slot on the command buffer. + /// Subsequent draw calls will use this uniform data. + /// It is legal to push uniforms during a pass. + /// + public unsafe void PushComputeUniformData( + void* uniformsPtr, + uint size, + uint slot = 0 + ) { + Refresh.Refresh_PushComputeUniformData( + Handle, + slot, + (nint) uniformsPtr, + size + ); + } + + /// + /// Pushes data to a compute uniform slot on the command buffer. + /// Subsequent draw calls will use this uniform data. + /// It is legal to push uniforms during a pass. + /// + public unsafe void PushComputeUniformData( + in T uniforms, + uint slot = 0 + ) where T : unmanaged + { + fixed (T* uniformsPtr = &uniforms) + { + PushComputeUniformData(uniformsPtr, (uint) Marshal.SizeOf(), slot); + } + } + + /// + /// Begins a render pass. + /// All render state, resource binding, and draw commands must be made within a render pass. + /// It is an error to call this during any kind of pass. + /// + /// The color attachment to use in the render pass. + public unsafe RenderPass BeginRenderPass( + in ColorAttachmentInfo colorAttachmentInfo + ) { +#if DEBUG + AssertNotSubmitted(); + AssertNotInPass("Cannot begin a render pass inside another pass!"); + AssertTextureNotNull(colorAttachmentInfo); + AssertColorTarget(colorAttachmentInfo); +#endif + + Refresh.ColorAttachmentInfo* refreshColorAttachmentInfos = stackalloc Refresh.ColorAttachmentInfo[1]; + refreshColorAttachmentInfos[0] = colorAttachmentInfo.ToRefresh(); + + IntPtr renderPassHandle = Refresh.Refresh_BeginRenderPass( + Handle, + refreshColorAttachmentInfos, + 1, + (Refresh.DepthStencilAttachmentInfo*) nint.Zero + ); + + RenderPass renderPass = Device.RenderPassPool.Obtain(); + renderPass.SetHandle(renderPassHandle); + +#if DEBUG + renderPassActive = true; + renderPass.colorAttachmentCount = 1; + renderPass.colorAttachmentSampleCount = colorAttachmentInfo.TextureSlice.Texture.SampleCount; + renderPass.colorFormatOne = colorAttachmentInfo.TextureSlice.Texture.Format; + renderPass.hasDepthStencilAttachment = false; +#endif + + return renderPass; + } + + /// + /// Begins a render pass. + /// All render state, resource binding, and draw commands must be made within a render pass. + /// It is an error to call this after calling BeginRenderPass but before calling EndRenderPass. + /// + /// The first color attachment to use in the render pass. + /// The second color attachment to use in the render pass. + public unsafe RenderPass BeginRenderPass( + in ColorAttachmentInfo colorAttachmentInfoOne, + in ColorAttachmentInfo colorAttachmentInfoTwo + ) { +#if DEBUG + AssertNotSubmitted(); + AssertNotInPass("Cannot begin a render pass inside another pass!"); + + AssertTextureNotNull(colorAttachmentInfoOne); + AssertColorTarget(colorAttachmentInfoOne); + + AssertTextureNotNull(colorAttachmentInfoTwo); + AssertColorTarget(colorAttachmentInfoTwo); + + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, colorAttachmentInfoTwo.TextureSlice.Texture); +#endif + + Refresh.ColorAttachmentInfo* refreshColorAttachmentInfos = stackalloc Refresh.ColorAttachmentInfo[2]; + refreshColorAttachmentInfos[0] = colorAttachmentInfoOne.ToRefresh(); + refreshColorAttachmentInfos[1] = colorAttachmentInfoTwo.ToRefresh(); + + IntPtr renderPassHandle = Refresh.Refresh_BeginRenderPass( + Handle, + refreshColorAttachmentInfos, + 2, + (Refresh.DepthStencilAttachmentInfo*) nint.Zero + ); + + RenderPass renderPass = Device.RenderPassPool.Obtain(); + renderPass.SetHandle(renderPassHandle); + +#if DEBUG + renderPassActive = true; + renderPass.colorAttachmentCount = 2; + renderPass.colorAttachmentSampleCount = colorAttachmentInfoOne.TextureSlice.Texture.SampleCount; + renderPass.colorFormatOne = colorAttachmentInfoOne.TextureSlice.Texture.Format; + renderPass.colorFormatTwo = colorAttachmentInfoTwo.TextureSlice.Texture.Format; + renderPass.hasDepthStencilAttachment = false; +#endif + + return renderPass; + } + + /// + /// Begins a render pass. + /// All render state, resource binding, and draw commands must be made within a render pass. + /// It is an error to call this after calling BeginRenderPass but before calling EndRenderPass. + /// + /// The first color attachment to use in the render pass. + /// The second color attachment to use in the render pass. + /// The third color attachment to use in the render pass. + public unsafe RenderPass BeginRenderPass( + in ColorAttachmentInfo colorAttachmentInfoOne, + in ColorAttachmentInfo colorAttachmentInfoTwo, + in ColorAttachmentInfo colorAttachmentInfoThree + ) { +#if DEBUG + AssertNotSubmitted(); + AssertNotInPass("Cannot begin a render pass inside another pass!"); + + AssertTextureNotNull(colorAttachmentInfoOne); + AssertColorTarget(colorAttachmentInfoOne); + + AssertTextureNotNull(colorAttachmentInfoTwo); + AssertColorTarget(colorAttachmentInfoTwo); + + AssertTextureNotNull(colorAttachmentInfoThree); + AssertColorTarget(colorAttachmentInfoThree); + + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, colorAttachmentInfoTwo.TextureSlice.Texture); + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, colorAttachmentInfoThree.TextureSlice.Texture); +#endif + + Refresh.ColorAttachmentInfo* refreshColorAttachmentInfos = stackalloc Refresh.ColorAttachmentInfo[3]; + refreshColorAttachmentInfos[0] = colorAttachmentInfoOne.ToRefresh(); + refreshColorAttachmentInfos[1] = colorAttachmentInfoTwo.ToRefresh(); + refreshColorAttachmentInfos[2] = colorAttachmentInfoThree.ToRefresh(); + + IntPtr renderPassHandle = Refresh.Refresh_BeginRenderPass( + Handle, + refreshColorAttachmentInfos, + 3, + (Refresh.DepthStencilAttachmentInfo*) nint.Zero + ); + + RenderPass renderPass = Device.RenderPassPool.Obtain(); + renderPass.SetHandle(renderPassHandle); + +#if DEBUG + renderPassActive = true; + renderPass.colorAttachmentCount = 3; + renderPass.colorAttachmentSampleCount = colorAttachmentInfoOne.TextureSlice.Texture.SampleCount; + renderPass.colorFormatOne = colorAttachmentInfoOne.TextureSlice.Texture.Format; + renderPass.colorFormatTwo = colorAttachmentInfoTwo.TextureSlice.Texture.Format; + renderPass.colorFormatThree = colorAttachmentInfoThree.TextureSlice.Texture.Format; + renderPass.hasDepthStencilAttachment = false; +#endif + + return renderPass; + } + + /// + /// Begins a render pass. + /// All render state, resource binding, and draw commands must be made within a render pass. + /// It is an error to call this after calling BeginRenderPass but before calling EndRenderPass. + /// + /// The first color attachment to use in the render pass. + /// The second color attachment to use in the render pass. + /// The third color attachment to use in the render pass. + /// The four color attachment to use in the render pass. + public unsafe RenderPass BeginRenderPass( + in ColorAttachmentInfo colorAttachmentInfoOne, + in ColorAttachmentInfo colorAttachmentInfoTwo, + in ColorAttachmentInfo colorAttachmentInfoThree, + in ColorAttachmentInfo colorAttachmentInfoFour + ) { +#if DEBUG + AssertNotSubmitted(); + + AssertTextureNotNull(colorAttachmentInfoOne); + AssertColorTarget(colorAttachmentInfoOne); + + AssertTextureNotNull(colorAttachmentInfoTwo); + AssertColorTarget(colorAttachmentInfoTwo); + + AssertTextureNotNull(colorAttachmentInfoThree); + AssertColorTarget(colorAttachmentInfoThree); + + AssertTextureNotNull(colorAttachmentInfoFour); + AssertColorTarget(colorAttachmentInfoFour); + + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, colorAttachmentInfoTwo.TextureSlice.Texture); + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, colorAttachmentInfoThree.TextureSlice.Texture); + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, colorAttachmentInfoFour.TextureSlice.Texture); +#endif + + Refresh.ColorAttachmentInfo* refreshColorAttachmentInfos = stackalloc Refresh.ColorAttachmentInfo[4]; + refreshColorAttachmentInfos[0] = colorAttachmentInfoOne.ToRefresh(); + refreshColorAttachmentInfos[1] = colorAttachmentInfoTwo.ToRefresh(); + refreshColorAttachmentInfos[2] = colorAttachmentInfoThree.ToRefresh(); + refreshColorAttachmentInfos[3] = colorAttachmentInfoFour.ToRefresh(); + + IntPtr renderPassHandle = Refresh.Refresh_BeginRenderPass( + Handle, + refreshColorAttachmentInfos, + 4, + (Refresh.DepthStencilAttachmentInfo*) nint.Zero + ); + + RenderPass renderPass = Device.RenderPassPool.Obtain(); + renderPass.SetHandle(renderPassHandle); + +#if DEBUG + renderPassActive = true; + renderPass.colorAttachmentCount = 3; + renderPass.colorAttachmentSampleCount = colorAttachmentInfoOne.TextureSlice.Texture.SampleCount; + renderPass.colorFormatOne = colorAttachmentInfoOne.TextureSlice.Texture.Format; + renderPass.colorFormatTwo = colorAttachmentInfoTwo.TextureSlice.Texture.Format; + renderPass.colorFormatThree = colorAttachmentInfoThree.TextureSlice.Texture.Format; + renderPass.colorFormatFour = colorAttachmentInfoFour.TextureSlice.Texture.Format; + renderPass.hasDepthStencilAttachment = false; +#endif + + return renderPass; + } + + /// + /// Begins a render pass. + /// All render state, resource binding, and draw commands must be made within a render pass. + /// It is an error to call this after calling BeginRenderPass but before calling EndRenderPass. + /// + /// The depth stencil attachment to use in the render pass. + public unsafe RenderPass BeginRenderPass( + in DepthStencilAttachmentInfo depthStencilAttachmentInfo + ) { +#if DEBUG + AssertNotSubmitted(); + AssertValidDepthAttachment(depthStencilAttachmentInfo); +#endif + + Refresh.DepthStencilAttachmentInfo refreshDepthStencilAttachmentInfo = depthStencilAttachmentInfo.ToRefresh(); + + IntPtr renderPassHandle = Refresh.Refresh_BeginRenderPass( + Handle, + (Refresh.ColorAttachmentInfo*) nint.Zero, + 0, + &refreshDepthStencilAttachmentInfo + ); + + RenderPass renderPass = Device.RenderPassPool.Obtain(); + renderPass.SetHandle(renderPassHandle); + +#if DEBUG + renderPassActive = true; + renderPass.hasDepthStencilAttachment = true; + renderPass.depthStencilAttachmentSampleCount = depthStencilAttachmentInfo.TextureSlice.Texture.SampleCount; + renderPass.depthStencilFormat = depthStencilAttachmentInfo.TextureSlice.Texture.Format; +#endif + + return renderPass; + } + + /// + /// Begins a render pass. + /// All render state, resource binding, and draw commands must be made within a render pass. + /// It is an error to call this after calling BeginRenderPass but before calling EndRenderPass. + /// + /// The depth stencil attachment to use in the render pass. + /// The color attachment to use in the render pass. + public unsafe RenderPass BeginRenderPass( + in DepthStencilAttachmentInfo depthStencilAttachmentInfo, + in ColorAttachmentInfo colorAttachmentInfo + ) { +#if DEBUG + AssertNotSubmitted(); + AssertValidDepthAttachment(depthStencilAttachmentInfo); + + AssertTextureNotNull(colorAttachmentInfo); + AssertColorTarget(colorAttachmentInfo); + AssertSameSampleCount(colorAttachmentInfo.TextureSlice.Texture, depthStencilAttachmentInfo.TextureSlice.Texture); +#endif + + Refresh.ColorAttachmentInfo* refreshColorAttachmentInfos = stackalloc Refresh.ColorAttachmentInfo[1]; + refreshColorAttachmentInfos[0] = colorAttachmentInfo.ToRefresh(); + + Refresh.DepthStencilAttachmentInfo refreshDepthStencilAttachmentInfo = depthStencilAttachmentInfo.ToRefresh(); + + IntPtr renderPassHandle = Refresh.Refresh_BeginRenderPass( + Handle, + refreshColorAttachmentInfos, + 1, + &refreshDepthStencilAttachmentInfo + ); + + RenderPass renderPass = Device.RenderPassPool.Obtain(); + renderPass.SetHandle(renderPassHandle); + +#if DEBUG + renderPassActive = true; + renderPass.hasDepthStencilAttachment = true; + renderPass.colorAttachmentCount = 1; + renderPass.colorAttachmentSampleCount = colorAttachmentInfo.TextureSlice.Texture.SampleCount; + renderPass.colorFormatOne = colorAttachmentInfo.TextureSlice.Texture.Format; + renderPass.depthStencilAttachmentSampleCount = depthStencilAttachmentInfo.TextureSlice.Texture.SampleCount; + renderPass.depthStencilFormat = depthStencilAttachmentInfo.TextureSlice.Texture.Format; +#endif + + return renderPass; + } + + /// + /// Begins a render pass. + /// All render state, resource binding, and draw commands must be made within a render pass. + /// It is an error to call this after calling BeginRenderPass but before calling EndRenderPass. + /// + /// The depth stencil attachment to use in the render pass. + /// The first color attachment to use in the render pass. + /// The second color attachment to use in the render pass. + public unsafe RenderPass BeginRenderPass( + in DepthStencilAttachmentInfo depthStencilAttachmentInfo, + in ColorAttachmentInfo colorAttachmentInfoOne, + in ColorAttachmentInfo colorAttachmentInfoTwo + ) { +#if DEBUG + AssertNotSubmitted(); + AssertValidDepthAttachment(depthStencilAttachmentInfo); + + AssertTextureNotNull(colorAttachmentInfoOne); + AssertColorTarget(colorAttachmentInfoOne); + + AssertTextureNotNull(colorAttachmentInfoTwo); + AssertColorTarget(colorAttachmentInfoTwo); + + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, colorAttachmentInfoTwo.TextureSlice.Texture); + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, depthStencilAttachmentInfo.TextureSlice.Texture); +#endif + + Refresh.ColorAttachmentInfo* refreshColorAttachmentInfos = stackalloc Refresh.ColorAttachmentInfo[2]; + refreshColorAttachmentInfos[0] = colorAttachmentInfoOne.ToRefresh(); + refreshColorAttachmentInfos[1] = colorAttachmentInfoTwo.ToRefresh(); + + Refresh.DepthStencilAttachmentInfo refreshDepthStencilAttachmentInfo = depthStencilAttachmentInfo.ToRefresh(); + + IntPtr renderPassHandle = Refresh.Refresh_BeginRenderPass( + Handle, + refreshColorAttachmentInfos, + 2, + &refreshDepthStencilAttachmentInfo + ); + + RenderPass renderPass = Device.RenderPassPool.Obtain(); + renderPass.SetHandle(renderPassHandle); + +#if DEBUG + renderPassActive = true; + renderPass.hasDepthStencilAttachment = true; + renderPass.colorAttachmentCount = 2; + renderPass.colorFormatOne = colorAttachmentInfoOne.TextureSlice.Texture.Format; + renderPass.colorFormatTwo = colorAttachmentInfoTwo.TextureSlice.Texture.Format; + renderPass.colorAttachmentSampleCount = colorAttachmentInfoOne.TextureSlice.Texture.SampleCount; + renderPass.depthStencilAttachmentSampleCount = depthStencilAttachmentInfo.TextureSlice.Texture.SampleCount; + renderPass.depthStencilFormat = depthStencilAttachmentInfo.TextureSlice.Texture.Format; +#endif + + return renderPass; + } + + /// + /// Begins a render pass. + /// All render state, resource binding, and draw commands must be made within a render pass. + /// It is an error to call this after calling BeginRenderPass but before calling EndRenderPass. + /// + /// The depth stencil attachment to use in the render pass. + /// The first color attachment to use in the render pass. + /// The second color attachment to use in the render pass. + /// The third color attachment to use in the render pass. + public unsafe RenderPass BeginRenderPass( + in DepthStencilAttachmentInfo depthStencilAttachmentInfo, + in ColorAttachmentInfo colorAttachmentInfoOne, + in ColorAttachmentInfo colorAttachmentInfoTwo, + in ColorAttachmentInfo colorAttachmentInfoThree + ) { +#if DEBUG + AssertNotSubmitted(); + AssertValidDepthAttachment(depthStencilAttachmentInfo); + + AssertTextureNotNull(colorAttachmentInfoOne); + AssertColorTarget(colorAttachmentInfoOne); + + AssertTextureNotNull(colorAttachmentInfoTwo); + AssertColorTarget(colorAttachmentInfoTwo); + + AssertTextureNotNull(colorAttachmentInfoThree); + AssertColorTarget(colorAttachmentInfoThree); + + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, colorAttachmentInfoTwo.TextureSlice.Texture); + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, colorAttachmentInfoThree.TextureSlice.Texture); + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, depthStencilAttachmentInfo.TextureSlice.Texture); +#endif + + Refresh.ColorAttachmentInfo* refreshColorAttachmentInfos = stackalloc Refresh.ColorAttachmentInfo[3]; + refreshColorAttachmentInfos[0] = colorAttachmentInfoOne.ToRefresh(); + refreshColorAttachmentInfos[1] = colorAttachmentInfoTwo.ToRefresh(); + refreshColorAttachmentInfos[2] = colorAttachmentInfoThree.ToRefresh(); + + Refresh.DepthStencilAttachmentInfo refreshDepthStencilAttachmentInfo = depthStencilAttachmentInfo.ToRefresh(); + + IntPtr renderPassHandle = Refresh.Refresh_BeginRenderPass( + Handle, + refreshColorAttachmentInfos, + 3, + &refreshDepthStencilAttachmentInfo + ); + + RenderPass renderPass = Device.RenderPassPool.Obtain(); + renderPass.SetHandle(renderPassHandle); + +#if DEBUG + renderPassActive = true; + renderPass.hasDepthStencilAttachment = true; + renderPass.colorAttachmentCount = 3; + renderPass.colorAttachmentSampleCount = colorAttachmentInfoOne.TextureSlice.Texture.SampleCount; + renderPass.colorFormatOne = colorAttachmentInfoOne.TextureSlice.Texture.Format; + renderPass.colorFormatTwo = colorAttachmentInfoTwo.TextureSlice.Texture.Format; + renderPass.colorFormatThree = colorAttachmentInfoThree.TextureSlice.Texture.Format; + renderPass.depthStencilAttachmentSampleCount = depthStencilAttachmentInfo.TextureSlice.Texture.SampleCount; + renderPass.depthStencilFormat = depthStencilAttachmentInfo.TextureSlice.Texture.Format; +#endif + + return renderPass; + } + + /// + /// Begins a render pass. + /// All render state, resource binding, and draw commands must be made within a render pass. + /// It is an error to call this after calling BeginRenderPass but before calling EndRenderPass. + /// + /// The depth stencil attachment to use in the render pass. + /// The first color attachment to use in the render pass. + /// The second color attachment to use in the render pass. + /// The third color attachment to use in the render pass. + /// The four color attachment to use in the render pass. + public unsafe RenderPass BeginRenderPass( + in DepthStencilAttachmentInfo depthStencilAttachmentInfo, + in ColorAttachmentInfo colorAttachmentInfoOne, + in ColorAttachmentInfo colorAttachmentInfoTwo, + in ColorAttachmentInfo colorAttachmentInfoThree, + in ColorAttachmentInfo colorAttachmentInfoFour + ) { +#if DEBUG + AssertNotSubmitted(); + AssertValidDepthAttachment(depthStencilAttachmentInfo); + + AssertTextureNotNull(colorAttachmentInfoOne); + AssertColorTarget(colorAttachmentInfoOne); + + AssertTextureNotNull(colorAttachmentInfoTwo); + AssertColorTarget(colorAttachmentInfoTwo); + + AssertTextureNotNull(colorAttachmentInfoThree); + AssertColorTarget(colorAttachmentInfoThree); + + AssertTextureNotNull(colorAttachmentInfoFour); + AssertColorTarget(colorAttachmentInfoFour); + + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, colorAttachmentInfoTwo.TextureSlice.Texture); + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, colorAttachmentInfoThree.TextureSlice.Texture); + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, colorAttachmentInfoFour.TextureSlice.Texture); + AssertSameSampleCount(colorAttachmentInfoOne.TextureSlice.Texture, depthStencilAttachmentInfo.TextureSlice.Texture); +#endif + + Refresh.ColorAttachmentInfo* refreshColorAttachmentInfos = stackalloc Refresh.ColorAttachmentInfo[4]; + refreshColorAttachmentInfos[0] = colorAttachmentInfoOne.ToRefresh(); + refreshColorAttachmentInfos[1] = colorAttachmentInfoTwo.ToRefresh(); + refreshColorAttachmentInfos[2] = colorAttachmentInfoThree.ToRefresh(); + refreshColorAttachmentInfos[3] = colorAttachmentInfoFour.ToRefresh(); + + Refresh.DepthStencilAttachmentInfo refreshDepthStencilAttachmentInfo = depthStencilAttachmentInfo.ToRefresh(); + + IntPtr renderPassHandle = Refresh.Refresh_BeginRenderPass( + Handle, + refreshColorAttachmentInfos, + 4, + &refreshDepthStencilAttachmentInfo + ); + + RenderPass renderPass = Device.RenderPassPool.Obtain(); + renderPass.SetHandle(renderPassHandle); + +#if DEBUG + renderPassActive = true; + renderPass.hasDepthStencilAttachment = true; + renderPass.colorAttachmentCount = 4; + renderPass.colorAttachmentSampleCount = colorAttachmentInfoOne.TextureSlice.Texture.SampleCount; + renderPass.colorFormatOne = colorAttachmentInfoOne.TextureSlice.Texture.Format; + renderPass.colorFormatTwo = colorAttachmentInfoTwo.TextureSlice.Texture.Format; + renderPass.colorFormatThree = colorAttachmentInfoThree.TextureSlice.Texture.Format; + renderPass.colorFormatFour = colorAttachmentInfoFour.TextureSlice.Texture.Format; + renderPass.depthStencilAttachmentSampleCount = depthStencilAttachmentInfo.TextureSlice.Texture.SampleCount; + renderPass.depthStencilFormat = depthStencilAttachmentInfo.TextureSlice.Texture.Format; +#endif + + return renderPass; + } + + /// + /// Ends the current render pass. + /// This must be called before beginning another render pass or submitting the command buffer. + /// + public void EndRenderPass(RenderPass renderPass) + { +#if DEBUG + AssertNotSubmitted(); + AssertRenderPassActive(); + + renderPassActive = false; +#endif + + Refresh.Refresh_EndRenderPass( + renderPass.Handle + ); + + renderPass.SetHandle(nint.Zero); + Device.RenderPassPool.Return(renderPass); + } + + /// + /// Blits a texture to another texture with the specified filter. + /// + /// This operation cannot be performed inside any pass. + /// + /// If true, the destination texture will cycle if bound. + public void Blit( + in TextureRegion source, + in TextureRegion destination, + Filter filter, + bool cycle + ) { + Refresh.Refresh_Blit( + Handle, + source.ToRefresh(), + destination.ToRefresh(), + (Refresh.Filter) filter, + Conversions.BoolToInt(cycle) + ); + } + + public unsafe ComputePass BeginComputePass( + in StorageTextureReadWriteBinding readWriteTextureBinding + ) { +#if DEBUG + AssertNotSubmitted(); + AssertNotInPass("Cannot begin compute pass while in another pass!"); + computePassActive = true; +#endif + + Refresh.StorageTextureReadWriteBinding refreshTextureBinding = readWriteTextureBinding.ToRefresh(); + + IntPtr computePassHandle = Refresh.Refresh_BeginComputePass( + Handle, + &refreshTextureBinding, + 1, + (Refresh.StorageBufferReadWriteBinding*) nint.Size, + 0 + ); + + ComputePass computePass = Device.ComputePassPool.Obtain(); + computePass.SetHandle(computePassHandle); + +#if DEBUG + computePass.active = true; +#endif + + return computePass; + } + + public unsafe ComputePass BeginComputePass( + in StorageBufferReadWriteBinding readWriteBufferBinding + ) { +#if DEBUG + AssertNotSubmitted(); + AssertNotInPass("Cannot begin compute pass while in another pass!"); + computePassActive = true; +#endif + + Refresh.StorageBufferReadWriteBinding refreshBufferBinding = readWriteBufferBinding.ToRefresh(); + + IntPtr computePassHandle = Refresh.Refresh_BeginComputePass( + Handle, + (Refresh.StorageTextureReadWriteBinding*) nint.Zero, + 0, + &refreshBufferBinding, + 1 + ); + + ComputePass computePass = Device.ComputePassPool.Obtain(); + computePass.SetHandle(computePassHandle); + +#if DEBUG + computePass.active = true; +#endif + + return computePass; + } + + public unsafe ComputePass BeginComputePass( + in StorageTextureReadWriteBinding readWriteTextureBinding, + in StorageBufferReadWriteBinding readWriteBufferBinding + ) { +#if DEBUG + AssertNotSubmitted(); + AssertNotInPass("Cannot begin compute pass while in another pass!"); + computePassActive = true; +#endif + + Refresh.StorageTextureReadWriteBinding refreshTextureBinding = readWriteTextureBinding.ToRefresh(); + Refresh.StorageBufferReadWriteBinding refreshBufferBinding = readWriteBufferBinding.ToRefresh(); + + IntPtr computePassHandle = Refresh.Refresh_BeginComputePass( + Handle, + &refreshTextureBinding, + 1, + &refreshBufferBinding, + 1 + ); + + ComputePass computePass = Device.ComputePassPool.Obtain(); + computePass.SetHandle(computePassHandle); + +#if DEBUG + computePass.active = true; +#endif + + return computePass; + } + + public unsafe ComputePass BeginComputePass( + Span readWriteTextureBindings, + Span readWriteBufferBindings + ) { +#if DEBUG + AssertNotSubmitted(); + AssertNotInPass("Cannot begin compute pass while in another pass!"); + computePassActive = true; +#endif + + void* refreshTextureBindings = NativeMemory.Alloc( + (nuint) (readWriteTextureBindings.Length * Marshal.SizeOf()) + ); + + void* refreshBufferBindings = NativeMemory.Alloc( + (nuint) (readWriteBufferBindings.Length * Marshal.SizeOf()) + ); + + IntPtr computePassHandle = Refresh.Refresh_BeginComputePass( + Handle, + (Refresh.StorageTextureReadWriteBinding*) refreshTextureBindings, + (uint) readWriteTextureBindings.Length, + (Refresh.StorageBufferReadWriteBinding*) refreshBufferBindings, + (uint) readWriteBufferBindings.Length + ); + + ComputePass computePass = Device.ComputePassPool.Obtain(); + computePass.SetHandle(computePassHandle); + +#if DEBUG + computePass.active = true; +#endif + + NativeMemory.Free(refreshTextureBindings); + NativeMemory.Free(refreshBufferBindings); + + return computePass; + } + + public void EndComputePass(ComputePass computePass) + { +#if DEBUG + AssertNotSubmitted(); + AssertInComputePass("Cannot end compute pass while not in a compute pass!"); + computePassActive = false; + computePass.active = false; +#endif + + Refresh.Refresh_EndComputePass( + computePass.Handle + ); + + computePass.SetHandle(nint.Zero); + Device.ComputePassPool.Return(computePass); + } + + // Copy Pass + + /// + /// Begins a copy pass. + /// All copy commands must be made within a copy pass. + /// It is an error to call this during any kind of pass. + /// + public CopyPass BeginCopyPass() + { +#if DEBUG + AssertNotSubmitted(); + AssertNotInPass("Cannot begin copy pass while in another pass!"); + copyPassActive = true; +#endif + + IntPtr copyPassHandle = Refresh.Refresh_BeginCopyPass(Handle); + + CopyPass copyPass = Device.CopyPassPool.Obtain(); + copyPass.SetHandle(copyPassHandle); + + return copyPass; + } + + public void EndCopyPass(CopyPass copyPass) + { +#if DEBUG + AssertNotSubmitted(); + AssertInCopyPass("Cannot end copy pass while not in a copy pass!"); + copyPassActive = false; +#endif + + Refresh.Refresh_EndCopyPass( + copyPass.Handle + ); + + copyPass.SetHandle(nint.Zero); + Device.CopyPassPool.Return(copyPass); + } + +#if DEBUG + private void AssertRenderPassActive(string message = "No active render pass!") + { + if (!renderPassActive) + { + throw new System.InvalidOperationException(message); + } + } + + private void AssertComputePipelineBound(string message = "No compute pipeline is bound!") + { + if (currentComputePipeline == null) + { + throw new System.InvalidOperationException(message); + } + } + + private void AssertTextureNotNull(ColorAttachmentInfo colorAttachmentInfo) + { + if (colorAttachmentInfo.TextureSlice.Texture == null || colorAttachmentInfo.TextureSlice.Texture.Handle == IntPtr.Zero) + { + throw new System.ArgumentException("Render pass color attachment Texture cannot be null!"); + } + } + + private void AssertColorTarget(ColorAttachmentInfo colorAttachmentInfo) + { + if ((colorAttachmentInfo.TextureSlice.Texture.UsageFlags & TextureUsageFlags.ColorTarget) == 0) + { + throw new System.ArgumentException("Render pass color attachment UsageFlags must include TextureUsageFlags.ColorTarget!"); + } + } + + private void AssertSameSampleCount(Texture a, Texture b) + { + if (a.SampleCount != b.SampleCount) + { + throw new System.ArgumentException("All attachments in a render pass must have the same SampleCount!"); + } + } + + private void AssertValidDepthAttachment(DepthStencilAttachmentInfo depthStencilAttachmentInfo) + { + if (depthStencilAttachmentInfo.TextureSlice.Texture == null || + depthStencilAttachmentInfo.TextureSlice.Texture.Handle == IntPtr.Zero) + { + throw new System.ArgumentException("Render pass depth stencil attachment Texture cannot be null!"); + } + + if ((depthStencilAttachmentInfo.TextureSlice.Texture.UsageFlags & TextureUsageFlags.DepthStencil) == 0) + { + throw new System.ArgumentException("Render pass depth stencil attachment UsageFlags must include TextureUsageFlags.DepthStencilTarget!"); + } + } + + private void AssertNonEmptyCopy(uint dataLengthInBytes) + { + if (dataLengthInBytes == 0) + { + throw new System.InvalidOperationException("SetBufferData must have a length greater than 0 bytes!"); + } + } + + private void AssertBufferBoundsCheck(uint bufferLengthInBytes, uint offsetInBytes, uint copyLengthInBytes) + { + if (copyLengthInBytes > bufferLengthInBytes + offsetInBytes) + { + throw new System.InvalidOperationException($"SetBufferData overflow! buffer length {bufferLengthInBytes}, offset {offsetInBytes}, copy length {copyLengthInBytes}"); + } + } + + private void AssertTextureBoundsCheck(uint textureSizeInBytes, uint dataLengthInBytes) + { + if (dataLengthInBytes > textureSizeInBytes) + { + throw new System.InvalidOperationException($"SetTextureData overflow! texture size {textureSizeInBytes}, data size {dataLengthInBytes}"); + } + } + + private void AssertNotInPass(string message) + { + if (renderPassActive || copyPassActive || computePassActive) + { + throw new System.InvalidOperationException(message); + } + } + + private void AssertInCopyPass(string message) + { + if (!copyPassActive) + { + throw new System.InvalidOperationException(message); + } + } + + private void AssertInComputePass(string message) + { + if (!computePassActive) + { + throw new System.InvalidOperationException(message); + } + } + + private void AssertNotSubmitted() + { + if (Submitted) + { + throw new System.InvalidOperationException("Cannot add commands to a submitted command buffer!"); + } + } +#endif +} diff --git a/Nerfed.Runtime/Graphics/CommandBufferPool.cs b/Nerfed.Runtime/Graphics/CommandBufferPool.cs new file mode 100644 index 0000000..58eff6b --- /dev/null +++ b/Nerfed.Runtime/Graphics/CommandBufferPool.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Concurrent; + +namespace Nerfed.Runtime.Graphics; + +internal class CommandBufferPool +{ + private GraphicsDevice GraphicsDevice; + private ConcurrentQueue CommandBuffers = new ConcurrentQueue(); + + public CommandBufferPool(GraphicsDevice graphicsDevice) + { + GraphicsDevice = graphicsDevice; + } + + public CommandBuffer Obtain() + { + if (CommandBuffers.TryDequeue(out CommandBuffer commandBuffer)) + { + return commandBuffer; + } + else + { + return new CommandBuffer(GraphicsDevice); + } + } + + public void Return(CommandBuffer commandBuffer) + { + commandBuffer.Handle = IntPtr.Zero; + CommandBuffers.Enqueue(commandBuffer); + } +} diff --git a/Nerfed.Runtime/Graphics/ComputePass.cs b/Nerfed.Runtime/Graphics/ComputePass.cs new file mode 100644 index 0000000..f73f38b --- /dev/null +++ b/Nerfed.Runtime/Graphics/ComputePass.cs @@ -0,0 +1,171 @@ +using System.Runtime.InteropServices; +using RefreshCS; + +namespace Nerfed.Runtime.Graphics; + +public class ComputePass +{ + public nint Handle { get; private set; } + + internal void SetHandle(nint handle) + { + Handle = handle; + } + +#if DEBUG + internal bool active; + + ComputePipeline currentComputePipeline; +#endif + + /// + /// Binds a compute pipeline so that compute work may be dispatched. + /// + /// The compute pipeline to bind. + public void BindComputePipeline( + ComputePipeline computePipeline + ) { +#if DEBUG + AssertComputePassActive(); + + // TODO: validate formats? +#endif + + Refresh.Refresh_BindComputePipeline( + Handle, + computePipeline.Handle + ); + +#if DEBUG + currentComputePipeline = computePipeline; +#endif + } + + /// + /// Binds a texture to be used in the compute shader. + /// This texture must have been created with the ComputeShaderRead usage flag. + /// + public unsafe void BindStorageTexture( + in TextureSlice textureSlice, + uint slot = 0 + ) { +#if DEBUG + AssertComputePassActive(); + AssertComputePipelineBound(); + AssertTextureNonNull(textureSlice.Texture); + AssertTextureHasComputeStorageReadFlag(textureSlice.Texture); +#endif + + Refresh.TextureSlice refreshTextureSlice = textureSlice.ToRefresh(); + + Refresh.Refresh_BindComputeStorageTextures( + Handle, + slot, + &refreshTextureSlice, + 1 + ); + } + + /// + /// Binds a buffer to be used in the compute shader. + /// This buffer must have been created with the ComputeShaderRead usage flag. + /// + public unsafe void BindStorageBuffer( + Buffer buffer, + uint slot = 0 + ) { +#if DEBUG + AssertComputePassActive(); + AssertComputePipelineBound(); + AssertBufferNonNull(buffer); + AssertBufferHasComputeStorageReadFlag(buffer); +#endif + + IntPtr bufferHandle = buffer.Handle; + + Refresh.Refresh_BindComputeStorageBuffers( + Handle, + slot, + &bufferHandle, + 1 + ); + } + + + + /// + /// Dispatches compute work. + /// + public void Dispatch( + uint groupCountX, + uint groupCountY, + uint groupCountZ + ) { +#if DEBUG + AssertComputePassActive(); + AssertComputePipelineBound(); + + if (groupCountX < 1 || groupCountY < 1 || groupCountZ < 1) + { + throw new System.ArgumentException("All dimensions for the compute work groups must be >= 1!"); + } +#endif + + Refresh.Refresh_DispatchCompute( + Handle, + groupCountX, + groupCountY, + groupCountZ + ); + } + +#if DEBUG + private void AssertComputePassActive(string message = "Render pass is not active!") + { + if (!active) + { + throw new System.InvalidOperationException(message); + } + } + + private void AssertComputePipelineBound(string message = "No compute pipeline is bound!") + { + if (currentComputePipeline == null) + { + throw new System.InvalidOperationException(message); + } + } + + private void AssertTextureNonNull(in TextureSlice textureSlice) + { + if (textureSlice.Texture == null || textureSlice.Texture.Handle == nint.Zero) + { + throw new System.NullReferenceException("Texture must not be null!"); + } + } + + private void AssertTextureHasComputeStorageReadFlag(Texture texture) + { + if ((texture.UsageFlags & TextureUsageFlags.ComputeStorageRead) == 0) + { + throw new System.ArgumentException("The bound Texture's UsageFlags must include TextureUsageFlags.ComputeStorageRead!"); + } + } + + private void AssertBufferNonNull(Buffer buffer) + { + if (buffer == null || buffer.Handle == nint.Zero) + { + throw new System.NullReferenceException("Buffer must not be null!"); + } + } + + private void AssertBufferHasComputeStorageReadFlag(Buffer buffer) + { + if ((buffer.UsageFlags & BufferUsageFlags.ComputeStorageRead) == 0) + { + throw new System.ArgumentException("The bound Buffer's UsageFlags must include BufferUsageFlag.ComputeStorageRead!"); + } + } +#endif +} diff --git a/Nerfed.Runtime/Graphics/ComputePassPool.cs b/Nerfed.Runtime/Graphics/ComputePassPool.cs new file mode 100644 index 0000000..5747702 --- /dev/null +++ b/Nerfed.Runtime/Graphics/ComputePassPool.cs @@ -0,0 +1,25 @@ +using System.Collections.Concurrent; + +namespace Nerfed.Runtime.Graphics; + +internal class ComputePassPool +{ + private ConcurrentQueue ComputePasses = new ConcurrentQueue(); + + public ComputePass Obtain() + { + if (ComputePasses.TryDequeue(out ComputePass computePass)) + { + return computePass; + } + else + { + return new ComputePass(); + } + } + + public void Return(ComputePass computePass) + { + ComputePasses.Enqueue(computePass); + } +} diff --git a/Nerfed.Runtime/Graphics/CopyPass.cs b/Nerfed.Runtime/Graphics/CopyPass.cs new file mode 100644 index 0000000..6c90000 --- /dev/null +++ b/Nerfed.Runtime/Graphics/CopyPass.cs @@ -0,0 +1,241 @@ +using System.Runtime.InteropServices; +using RefreshCS; + +namespace Nerfed.Runtime.Graphics; + +public class CopyPass +{ + public nint Handle { get; private set; } + + internal void SetHandle(nint handle) + { + Handle = handle; + } + + /// + /// Uploads data from a TransferBuffer to a TextureSlice. + /// This copy occurs on the GPU timeline. + /// + /// Overwriting the contents of the TransferBuffer before the command buffer + /// has finished execution will cause undefined behavior. + /// + /// You MAY assume that the copy has finished for subsequent commands. + /// + /// If true, cycles the texture if the given slice is bound. + public void UploadToTexture( + in TextureTransferInfo source, + in TextureRegion destination, + bool cycle + ) { +#if DEBUG + AssertTransferBufferNotMapped(source.TransferBuffer); +#endif + + Refresh.Refresh_UploadToTexture( + Handle, + source.ToRefresh(), + destination.ToRefresh(), + Conversions.BoolToInt(cycle) + ); + } + + /// + /// Uploads the contents of an entire buffer to a 2D texture with no mips. + /// + public void UploadToTexture( + TransferBuffer source, + Texture destination, + bool cycle + ) { + UploadToTexture( + new TextureTransferInfo(source), + new TextureRegion(destination), + cycle + ); + } + + /// + /// Uploads data from a TransferBuffer to a Buffer. + /// This copy occurs on the GPU timeline. + /// + /// Overwriting the contents of the TransferBuffer before the command buffer + /// has finished execution will cause undefined behavior. + /// + /// You MAY assume that the copy has finished for subsequent commands. + /// + /// If true, cycles the buffer if it is bound. + public void UploadToBuffer( + in TransferBufferLocation source, + in BufferRegion destination, + bool cycle + ) { +#if DEBUG + AssertBufferBoundsCheck(source.TransferBuffer.Size, source.Offset, destination.Size); + AssertBufferBoundsCheck(destination.Buffer.Size, destination.Offset, destination.Size); + AssertTransferBufferNotMapped(source.TransferBuffer); +#endif + + Refresh.Refresh_UploadToBuffer( + Handle, + source.ToRefresh(), + destination.ToRefresh(), + Conversions.BoolToInt(cycle) + ); + } + + /// + /// Copies the entire contents of a TransferBuffer to a Buffer. + /// + public void UploadToBuffer( + TransferBuffer source, + Buffer destination, + bool cycle + ) { + UploadToBuffer( + new TransferBufferLocation(source), + new BufferRegion(destination, 0, destination.Size), + cycle + ); + } + + /// + /// Copies data element-wise into from a TransferBuffer to a Buffer. + /// + public void UploadToBuffer( + TransferBuffer source, + Buffer destination, + uint sourceStartElement, + uint destinationStartElement, + uint numElements, + bool cycle + ) where T : unmanaged + { + int elementSize = Marshal.SizeOf(); + uint dataLengthInBytes = (uint) (elementSize * numElements); + uint srcOffsetInBytes = (uint) (elementSize * sourceStartElement); + uint dstOffsetInBytes = (uint) (elementSize * destinationStartElement); + + UploadToBuffer( + new TransferBufferLocation(source, srcOffsetInBytes), + new BufferRegion(destination, dstOffsetInBytes, dataLengthInBytes), + cycle + ); + } + + /// + /// Copies the contents of a TextureLocation to another TextureLocation. + /// This copy occurs on the GPU timeline. + /// + /// You MAY assume that the copy has finished in subsequent commands. + /// + public void CopyTextureToTexture( + in TextureLocation source, + in TextureLocation destination, + uint w, + uint h, + uint d, + bool cycle + ) { +#if DEBUG + AssertTextureBoundsCheck(source, w, h, d); + AssertTextureBoundsCheck(destination, w, h, d); +#endif + + Refresh.Refresh_CopyTextureToTexture( + Handle, + source.ToRefresh(), + destination.ToRefresh(), + w, + h, + d, + Conversions.BoolToInt(cycle) + ); + } + + /// + /// Copies data from a Buffer to another Buffer. + /// This copy occurs on the GPU timeline. + /// + /// You MAY assume that the copy has finished in subsequent commands. + /// + public void CopyBufferToBuffer( + in BufferLocation source, + in BufferLocation destination, + uint size, + bool cycle + ) { +#if DEBUG + AssertBufferBoundsCheck(source.Buffer.Size, source.Offset, size); + AssertBufferBoundsCheck(destination.Buffer.Size, destination.Offset, size); +#endif + + Refresh.Refresh_CopyBufferToBuffer( + Handle, + source.ToRefresh(), + destination.ToRefresh(), + size, + Conversions.BoolToInt(cycle) + ); + } + + public void DownloadFromBuffer( + in BufferRegion source, + in TransferBufferLocation destination + ) { +#if DEBUG + AssertBufferBoundsCheck(source.Buffer.Size, source.Offset, source.Size); + AssertBufferBoundsCheck(destination.TransferBuffer.Size, destination.Offset, source.Size); + AssertTransferBufferNotMapped(destination.TransferBuffer); +#endif + + Refresh.Refresh_DownloadFromBuffer( + Handle, + source.ToRefresh(), + destination.ToRefresh() + ); + } + + public void DownloadFromTexture( + in TextureRegion source, + in TextureTransferInfo destination + ) { +#if DEBUG + AssertTransferBufferNotMapped(destination.TransferBuffer); +#endif + + Refresh.Refresh_DownloadFromTexture( + Handle, + source.ToRefresh(), + destination.ToRefresh() + ); + } + +#if DEBUG + private void AssertBufferBoundsCheck(uint bufferLengthInBytes, uint offsetInBytes, uint copyLengthInBytes) + { + if (copyLengthInBytes > bufferLengthInBytes + offsetInBytes) + { + throw new System.InvalidOperationException($"SetBufferData overflow! buffer length {bufferLengthInBytes}, offset {offsetInBytes}, copy length {copyLengthInBytes}"); + } + } + + private void AssertTextureBoundsCheck(in TextureLocation textureLocation, uint w, uint h, uint d) + { + if ( + textureLocation.X + w > textureLocation.TextureSlice.Texture.Width || + textureLocation.Y + h > textureLocation.TextureSlice.Texture.Height || + textureLocation.Z + d > textureLocation.TextureSlice.Texture.Depth + ) { + throw new System.InvalidOperationException($"Texture data is out of bounds!"); + } + } + + private void AssertTransferBufferNotMapped(TransferBuffer transferBuffer) + { + if (transferBuffer.Mapped) + { + throw new System.InvalidOperationException("Transfer buffer must not be mapped!"); + } + } +#endif +} diff --git a/Nerfed.Runtime/Graphics/CopyPassPool.cs b/Nerfed.Runtime/Graphics/CopyPassPool.cs new file mode 100644 index 0000000..2fbdc52 --- /dev/null +++ b/Nerfed.Runtime/Graphics/CopyPassPool.cs @@ -0,0 +1,25 @@ +using System.Collections.Concurrent; + +namespace Nerfed.Runtime.Graphics; + +internal class CopyPassPool +{ + private ConcurrentQueue CopyPasses = new ConcurrentQueue(); + + public CopyPass Obtain() + { + if (CopyPasses.TryDequeue(out CopyPass copyPass)) + { + return copyPass; + } + else + { + return new CopyPass(); + } + } + + public void Return(CopyPass copyPass) + { + CopyPasses.Enqueue(copyPass); + } +} diff --git a/Nerfed.Runtime/Graphics/EmbeddedShadersSpirV.cs b/Nerfed.Runtime/Graphics/EmbeddedShadersSpirV.cs new file mode 100644 index 0000000..567fe09 --- /dev/null +++ b/Nerfed.Runtime/Graphics/EmbeddedShadersSpirV.cs @@ -0,0 +1,716 @@ +namespace Nerfed.Runtime.Graphics; + +internal class EmbeddedShadersSpirV : IEmbeddedShaders +{ + public ShaderFormat ShaderFormat => ShaderFormat.SPIRV; + + public byte[] FullscreenVert { get; } = + [ + 0x3, 0x2, 0x23, 0x7, 0x0, 0x0, 0x1, 0x0, 0xB, 0x0, + 0x8, 0x0, 0x2E, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x11, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0xB, 0x0, + 0x6, 0x0, 0x1, 0x0, 0x0, 0x0, 0x47, 0x4C, 0x53, 0x4C, + 0x2E, 0x73, 0x74, 0x64, 0x2E, 0x34, 0x35, 0x30, 0x0, 0x0, + 0x0, 0x0, 0xE, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0xF, 0x0, 0x8, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E, + 0x0, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0xC, 0x0, + 0x0, 0x0, 0x1D, 0x0, 0x0, 0x0, 0x3, 0x0, 0x3, 0x0, + 0x2, 0x0, 0x0, 0x0, 0xC2, 0x1, 0x0, 0x0, 0x5, 0x0, + 0x4, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E, + 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x5, 0x0, 0x9, 0x0, + 0x0, 0x0, 0x6F, 0x75, 0x74, 0x54, 0x65, 0x78, 0x43, 0x6F, + 0x6F, 0x72, 0x64, 0x0, 0x5, 0x0, 0x6, 0x0, 0xC, 0x0, + 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x56, 0x65, 0x72, 0x74, 0x65, + 0x78, 0x49, 0x6E, 0x64, 0x65, 0x78, 0x0, 0x0, 0x5, 0x0, + 0x6, 0x0, 0x1B, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x50, + 0x65, 0x72, 0x56, 0x65, 0x72, 0x74, 0x65, 0x78, 0x0, 0x0, + 0x0, 0x0, 0x6, 0x0, 0x6, 0x0, 0x1B, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x50, 0x6F, 0x73, + 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x0, 0x6, 0x0, 0x7, 0x0, + 0x1B, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x67, 0x6C, + 0x5F, 0x50, 0x6F, 0x69, 0x6E, 0x74, 0x53, 0x69, 0x7A, 0x65, + 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x7, 0x0, 0x1B, 0x0, + 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x43, + 0x6C, 0x69, 0x70, 0x44, 0x69, 0x73, 0x74, 0x61, 0x6E, 0x63, + 0x65, 0x0, 0x6, 0x0, 0x7, 0x0, 0x1B, 0x0, 0x0, 0x0, + 0x3, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x43, 0x75, 0x6C, + 0x6C, 0x44, 0x69, 0x73, 0x74, 0x61, 0x6E, 0x63, 0x65, 0x0, + 0x5, 0x0, 0x3, 0x0, 0x1D, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x9, 0x0, 0x0, 0x0, + 0x1E, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x47, 0x0, + 0x4, 0x0, 0xC, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x2A, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0x1B, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0x1B, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0x1B, 0x0, + 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x3, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0x1B, 0x0, + 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x4, 0x0, 0x0, 0x0, 0x47, 0x0, 0x3, 0x0, 0x1B, 0x0, + 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x13, 0x0, 0x2, 0x0, + 0x2, 0x0, 0x0, 0x0, 0x21, 0x0, 0x3, 0x0, 0x3, 0x0, + 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x16, 0x0, 0x3, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x17, 0x0, + 0x4, 0x0, 0x7, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x2, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x8, 0x0, + 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, + 0x3B, 0x0, 0x4, 0x0, 0x8, 0x0, 0x0, 0x0, 0x9, 0x0, + 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x15, 0x0, 0x4, 0x0, + 0xA, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x1, 0x0, + 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x3B, 0x0, + 0x4, 0x0, 0xB, 0x0, 0x0, 0x0, 0xC, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0xA, 0x0, + 0x0, 0x0, 0xE, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x2B, 0x0, 0x4, 0x0, 0xA, 0x0, 0x0, 0x0, 0x10, 0x0, + 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0, + 0x17, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x4, 0x0, + 0x0, 0x0, 0x15, 0x0, 0x4, 0x0, 0x18, 0x0, 0x0, 0x0, + 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2B, 0x0, + 0x4, 0x0, 0x18, 0x0, 0x0, 0x0, 0x19, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x1C, 0x0, 0x4, 0x0, 0x1A, 0x0, + 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x19, 0x0, 0x0, 0x0, + 0x1E, 0x0, 0x6, 0x0, 0x1B, 0x0, 0x0, 0x0, 0x17, 0x0, + 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0, + 0x1A, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x1C, 0x0, + 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1B, 0x0, 0x0, 0x0, + 0x3B, 0x0, 0x4, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x1D, 0x0, + 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, + 0xA, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x2B, 0x0, + 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0xC0, 0x2C, 0x0, 0x5, 0x0, 0x7, 0x0, + 0x0, 0x0, 0x22, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, + 0x21, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x24, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xBF, + 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x25, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x80, 0x3F, 0x2C, 0x0, 0x5, 0x0, + 0x7, 0x0, 0x0, 0x0, 0x26, 0x0, 0x0, 0x0, 0x24, 0x0, + 0x0, 0x0, 0x25, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x28, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x2C, 0x0, 0x0, 0x0, + 0x3, 0x0, 0x0, 0x0, 0x17, 0x0, 0x0, 0x0, 0x36, 0x0, + 0x5, 0x0, 0x2, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0xF8, 0x0, + 0x2, 0x0, 0x5, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, + 0xA, 0x0, 0x0, 0x0, 0xD, 0x0, 0x0, 0x0, 0xC, 0x0, + 0x0, 0x0, 0xC4, 0x0, 0x5, 0x0, 0xA, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0xD, 0x0, 0x0, 0x0, 0xE, 0x0, + 0x0, 0x0, 0xC7, 0x0, 0x5, 0x0, 0xA, 0x0, 0x0, 0x0, + 0x11, 0x0, 0x0, 0x0, 0xF, 0x0, 0x0, 0x0, 0x10, 0x0, + 0x0, 0x0, 0x6F, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x12, 0x0, 0x0, 0x0, 0x11, 0x0, 0x0, 0x0, 0x3D, 0x0, + 0x4, 0x0, 0xA, 0x0, 0x0, 0x0, 0x13, 0x0, 0x0, 0x0, + 0xC, 0x0, 0x0, 0x0, 0xC7, 0x0, 0x5, 0x0, 0xA, 0x0, + 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x13, 0x0, 0x0, 0x0, + 0x10, 0x0, 0x0, 0x0, 0x6F, 0x0, 0x4, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x15, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, + 0x50, 0x0, 0x5, 0x0, 0x7, 0x0, 0x0, 0x0, 0x16, 0x0, + 0x0, 0x0, 0x12, 0x0, 0x0, 0x0, 0x15, 0x0, 0x0, 0x0, + 0x3E, 0x0, 0x3, 0x0, 0x9, 0x0, 0x0, 0x0, 0x16, 0x0, + 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x7, 0x0, 0x0, 0x0, + 0x1F, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x85, 0x0, + 0x5, 0x0, 0x7, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0, + 0x1F, 0x0, 0x0, 0x0, 0x22, 0x0, 0x0, 0x0, 0x81, 0x0, + 0x5, 0x0, 0x7, 0x0, 0x0, 0x0, 0x27, 0x0, 0x0, 0x0, + 0x23, 0x0, 0x0, 0x0, 0x26, 0x0, 0x0, 0x0, 0x51, 0x0, + 0x5, 0x0, 0x6, 0x0, 0x0, 0x0, 0x29, 0x0, 0x0, 0x0, + 0x27, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x51, 0x0, + 0x5, 0x0, 0x6, 0x0, 0x0, 0x0, 0x2A, 0x0, 0x0, 0x0, + 0x27, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x50, 0x0, + 0x7, 0x0, 0x17, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, + 0x29, 0x0, 0x0, 0x0, 0x2A, 0x0, 0x0, 0x0, 0x28, 0x0, + 0x0, 0x0, 0x25, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, + 0x2C, 0x0, 0x0, 0x0, 0x2D, 0x0, 0x0, 0x0, 0x1D, 0x0, + 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, + 0x2D, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, 0xFD, 0x0, + 0x1, 0x0, 0x38, 0x0, 0x1, 0x0, + ]; + + public byte[] TextMsdfFrag { get; } = + [ + 0x3, 0x2, 0x23, 0x7, 0x0, 0x0, 0x1, 0x0, 0xB, 0x0, + 0x8, 0x0, 0x6C, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x11, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x11, 0x0, + 0x2, 0x0, 0x32, 0x0, 0x0, 0x0, 0xB, 0x0, 0x6, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x47, 0x4C, 0x53, 0x4C, 0x2E, 0x73, + 0x74, 0x64, 0x2E, 0x34, 0x35, 0x30, 0x0, 0x0, 0x0, 0x0, + 0xE, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, + 0x0, 0x0, 0xF, 0x0, 0x8, 0x0, 0x4, 0x0, 0x0, 0x0, + 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E, 0x0, 0x0, + 0x0, 0x0, 0x36, 0x0, 0x0, 0x0, 0x64, 0x0, 0x0, 0x0, + 0x67, 0x0, 0x0, 0x0, 0x10, 0x0, 0x3, 0x0, 0x4, 0x0, + 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3, 0x0, 0x3, 0x0, + 0x2, 0x0, 0x0, 0x0, 0xC2, 0x1, 0x0, 0x0, 0x5, 0x0, + 0x4, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E, + 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x7, 0x0, 0xC, 0x0, + 0x0, 0x0, 0x6D, 0x65, 0x64, 0x69, 0x61, 0x6E, 0x28, 0x66, + 0x31, 0x3B, 0x66, 0x31, 0x3B, 0x66, 0x31, 0x3B, 0x0, 0x0, + 0x0, 0x0, 0x5, 0x0, 0x3, 0x0, 0x9, 0x0, 0x0, 0x0, + 0x72, 0x0, 0x0, 0x0, 0x5, 0x0, 0x3, 0x0, 0xA, 0x0, + 0x0, 0x0, 0x67, 0x0, 0x0, 0x0, 0x5, 0x0, 0x3, 0x0, + 0xB, 0x0, 0x0, 0x0, 0x62, 0x0, 0x0, 0x0, 0x5, 0x0, + 0x6, 0x0, 0xF, 0x0, 0x0, 0x0, 0x73, 0x63, 0x72, 0x65, + 0x65, 0x6E, 0x50, 0x78, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x28, + 0x0, 0x0, 0x5, 0x0, 0x5, 0x0, 0x1E, 0x0, 0x0, 0x0, + 0x75, 0x6E, 0x69, 0x74, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x0, + 0x0, 0x0, 0x5, 0x0, 0x3, 0x0, 0x1F, 0x0, 0x0, 0x0, + 0x55, 0x42, 0x4F, 0x0, 0x6, 0x0, 0x5, 0x0, 0x1F, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x70, 0x78, 0x52, 0x61, + 0x6E, 0x67, 0x65, 0x0, 0x5, 0x0, 0x3, 0x0, 0x21, 0x0, + 0x0, 0x0, 0x75, 0x62, 0x6F, 0x0, 0x5, 0x0, 0x4, 0x0, + 0x2B, 0x0, 0x0, 0x0, 0x6D, 0x73, 0x64, 0x66, 0x0, 0x0, + 0x0, 0x0, 0x5, 0x0, 0x6, 0x0, 0x32, 0x0, 0x0, 0x0, + 0x73, 0x63, 0x72, 0x65, 0x65, 0x6E, 0x54, 0x65, 0x78, 0x53, + 0x69, 0x7A, 0x65, 0x0, 0x0, 0x0, 0x5, 0x0, 0x5, 0x0, + 0x36, 0x0, 0x0, 0x0, 0x69, 0x6E, 0x54, 0x65, 0x78, 0x43, + 0x6F, 0x6F, 0x72, 0x64, 0x0, 0x0, 0x5, 0x0, 0x3, 0x0, + 0x44, 0x0, 0x0, 0x0, 0x6D, 0x73, 0x64, 0x0, 0x5, 0x0, + 0x3, 0x0, 0x4A, 0x0, 0x0, 0x0, 0x73, 0x64, 0x0, 0x0, + 0x5, 0x0, 0x4, 0x0, 0x4B, 0x0, 0x0, 0x0, 0x70, 0x61, + 0x72, 0x61, 0x6D, 0x0, 0x0, 0x0, 0x5, 0x0, 0x4, 0x0, + 0x50, 0x0, 0x0, 0x0, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x0, + 0x0, 0x0, 0x5, 0x0, 0x4, 0x0, 0x54, 0x0, 0x0, 0x0, + 0x70, 0x61, 0x72, 0x61, 0x6D, 0x0, 0x0, 0x0, 0x5, 0x0, + 0x7, 0x0, 0x59, 0x0, 0x0, 0x0, 0x73, 0x63, 0x72, 0x65, + 0x65, 0x6E, 0x50, 0x78, 0x44, 0x69, 0x73, 0x74, 0x61, 0x6E, + 0x63, 0x65, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x4, 0x0, + 0x5E, 0x0, 0x0, 0x0, 0x6F, 0x70, 0x61, 0x63, 0x69, 0x74, + 0x79, 0x0, 0x5, 0x0, 0x5, 0x0, 0x64, 0x0, 0x0, 0x0, + 0x6F, 0x75, 0x74, 0x43, 0x6F, 0x6C, 0x6F, 0x72, 0x0, 0x0, + 0x0, 0x0, 0x5, 0x0, 0x4, 0x0, 0x67, 0x0, 0x0, 0x0, + 0x69, 0x6E, 0x43, 0x6F, 0x6C, 0x6F, 0x72, 0x0, 0x48, 0x0, + 0x5, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x23, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x47, 0x0, + 0x3, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, + 0x47, 0x0, 0x4, 0x0, 0x21, 0x0, 0x0, 0x0, 0x22, 0x0, + 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, + 0x21, 0x0, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x2B, 0x0, 0x0, 0x0, + 0x22, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x47, 0x0, + 0x4, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x36, 0x0, + 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x47, 0x0, 0x4, 0x0, 0x64, 0x0, 0x0, 0x0, 0x1E, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, + 0x67, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x1, 0x0, + 0x0, 0x0, 0x13, 0x0, 0x2, 0x0, 0x2, 0x0, 0x0, 0x0, + 0x21, 0x0, 0x3, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x0, + 0x0, 0x0, 0x16, 0x0, 0x3, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x20, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x7, 0x0, + 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x21, 0x0, 0x6, 0x0, 0x8, 0x0, 0x0, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, + 0x7, 0x0, 0x0, 0x0, 0x21, 0x0, 0x3, 0x0, 0xE, 0x0, + 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0, + 0x1C, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x2, 0x0, + 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x1D, 0x0, 0x0, 0x0, + 0x7, 0x0, 0x0, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x1E, 0x0, + 0x3, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x20, 0x0, 0x4, 0x0, 0x20, 0x0, 0x0, 0x0, 0x2, 0x0, + 0x0, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, + 0x20, 0x0, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0, 0x2, 0x0, + 0x0, 0x0, 0x15, 0x0, 0x4, 0x0, 0x22, 0x0, 0x0, 0x0, + 0x20, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2B, 0x0, + 0x4, 0x0, 0x22, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x24, 0x0, + 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x19, 0x0, 0x9, 0x0, 0x28, 0x0, 0x0, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1B, 0x0, 0x3, 0x0, + 0x29, 0x0, 0x0, 0x0, 0x28, 0x0, 0x0, 0x0, 0x20, 0x0, + 0x4, 0x0, 0x2A, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x29, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x2A, 0x0, + 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x17, 0x0, 0x4, 0x0, 0x2E, 0x0, 0x0, 0x0, 0x22, 0x0, + 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x3F, 0x2C, 0x0, 0x5, 0x0, 0x1C, 0x0, 0x0, 0x0, + 0x34, 0x0, 0x0, 0x0, 0x33, 0x0, 0x0, 0x0, 0x33, 0x0, + 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x35, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x3B, 0x0, + 0x4, 0x0, 0x35, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x3A, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3F, + 0x17, 0x0, 0x4, 0x0, 0x42, 0x0, 0x0, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, + 0x43, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x42, 0x0, + 0x0, 0x0, 0x17, 0x0, 0x4, 0x0, 0x47, 0x0, 0x0, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x15, 0x0, + 0x4, 0x0, 0x4C, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x4C, 0x0, + 0x0, 0x0, 0x4D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x2B, 0x0, 0x4, 0x0, 0x4C, 0x0, 0x0, 0x0, 0x51, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, + 0x4C, 0x0, 0x0, 0x0, 0x55, 0x0, 0x0, 0x0, 0x2, 0x0, + 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x61, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, + 0x4, 0x0, 0x63, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, + 0x47, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x63, 0x0, + 0x0, 0x0, 0x64, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, + 0x2C, 0x0, 0x7, 0x0, 0x47, 0x0, 0x0, 0x0, 0x65, 0x0, + 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, + 0x61, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x20, 0x0, + 0x4, 0x0, 0x66, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x47, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x66, 0x0, + 0x0, 0x0, 0x67, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x36, 0x0, 0x5, 0x0, 0x2, 0x0, 0x0, 0x0, 0x4, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, + 0xF8, 0x0, 0x2, 0x0, 0x5, 0x0, 0x0, 0x0, 0x3B, 0x0, + 0x4, 0x0, 0x43, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, + 0x7, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x7, 0x0, + 0x0, 0x0, 0x4A, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, + 0x3B, 0x0, 0x4, 0x0, 0x7, 0x0, 0x0, 0x0, 0x4B, 0x0, + 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, + 0x7, 0x0, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x7, 0x0, + 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x7, 0x0, 0x0, 0x0, + 0x54, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3B, 0x0, + 0x4, 0x0, 0x7, 0x0, 0x0, 0x0, 0x59, 0x0, 0x0, 0x0, + 0x7, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x7, 0x0, + 0x0, 0x0, 0x5E, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, + 0x3D, 0x0, 0x4, 0x0, 0x29, 0x0, 0x0, 0x0, 0x45, 0x0, + 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, + 0x1C, 0x0, 0x0, 0x0, 0x46, 0x0, 0x0, 0x0, 0x36, 0x0, + 0x0, 0x0, 0x57, 0x0, 0x5, 0x0, 0x47, 0x0, 0x0, 0x0, + 0x48, 0x0, 0x0, 0x0, 0x45, 0x0, 0x0, 0x0, 0x46, 0x0, + 0x0, 0x0, 0x4F, 0x0, 0x8, 0x0, 0x42, 0x0, 0x0, 0x0, + 0x49, 0x0, 0x0, 0x0, 0x48, 0x0, 0x0, 0x0, 0x48, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x2, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x44, 0x0, + 0x0, 0x0, 0x49, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, + 0x7, 0x0, 0x0, 0x0, 0x4E, 0x0, 0x0, 0x0, 0x44, 0x0, + 0x0, 0x0, 0x4D, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x4F, 0x0, 0x0, 0x0, 0x4E, 0x0, + 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x4B, 0x0, 0x0, 0x0, + 0x4F, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x7, 0x0, + 0x0, 0x0, 0x52, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, + 0x51, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x53, 0x0, 0x0, 0x0, 0x52, 0x0, 0x0, 0x0, + 0x3E, 0x0, 0x3, 0x0, 0x50, 0x0, 0x0, 0x0, 0x53, 0x0, + 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x7, 0x0, 0x0, 0x0, + 0x56, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, 0x55, 0x0, + 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x57, 0x0, 0x0, 0x0, 0x56, 0x0, 0x0, 0x0, 0x3E, 0x0, + 0x3, 0x0, 0x54, 0x0, 0x0, 0x0, 0x57, 0x0, 0x0, 0x0, + 0x39, 0x0, 0x7, 0x0, 0x6, 0x0, 0x0, 0x0, 0x58, 0x0, + 0x0, 0x0, 0xC, 0x0, 0x0, 0x0, 0x4B, 0x0, 0x0, 0x0, + 0x50, 0x0, 0x0, 0x0, 0x54, 0x0, 0x0, 0x0, 0x3E, 0x0, + 0x3, 0x0, 0x4A, 0x0, 0x0, 0x0, 0x58, 0x0, 0x0, 0x0, + 0x39, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x5A, 0x0, + 0x0, 0x0, 0xF, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x5B, 0x0, 0x0, 0x0, 0x4A, 0x0, + 0x0, 0x0, 0x83, 0x0, 0x5, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x5C, 0x0, 0x0, 0x0, 0x5B, 0x0, 0x0, 0x0, 0x3A, 0x0, + 0x0, 0x0, 0x85, 0x0, 0x5, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x5D, 0x0, 0x0, 0x0, 0x5A, 0x0, 0x0, 0x0, 0x5C, 0x0, + 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x59, 0x0, 0x0, 0x0, + 0x5D, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x5F, 0x0, 0x0, 0x0, 0x59, 0x0, 0x0, 0x0, + 0x81, 0x0, 0x5, 0x0, 0x6, 0x0, 0x0, 0x0, 0x60, 0x0, + 0x0, 0x0, 0x5F, 0x0, 0x0, 0x0, 0x3A, 0x0, 0x0, 0x0, + 0xC, 0x0, 0x8, 0x0, 0x6, 0x0, 0x0, 0x0, 0x62, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, + 0x60, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x33, 0x0, + 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x5E, 0x0, 0x0, 0x0, + 0x62, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x47, 0x0, + 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x67, 0x0, 0x0, 0x0, + 0x3D, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x69, 0x0, + 0x0, 0x0, 0x5E, 0x0, 0x0, 0x0, 0x50, 0x0, 0x7, 0x0, + 0x47, 0x0, 0x0, 0x0, 0x6A, 0x0, 0x0, 0x0, 0x69, 0x0, + 0x0, 0x0, 0x69, 0x0, 0x0, 0x0, 0x69, 0x0, 0x0, 0x0, + 0x69, 0x0, 0x0, 0x0, 0xC, 0x0, 0x8, 0x0, 0x47, 0x0, + 0x0, 0x0, 0x6B, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x2E, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x0, 0x68, 0x0, + 0x0, 0x0, 0x6A, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, + 0x64, 0x0, 0x0, 0x0, 0x6B, 0x0, 0x0, 0x0, 0xFD, 0x0, + 0x1, 0x0, 0x38, 0x0, 0x1, 0x0, 0x36, 0x0, 0x5, 0x0, + 0x6, 0x0, 0x0, 0x0, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x37, 0x0, 0x3, 0x0, + 0x7, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x37, 0x0, + 0x3, 0x0, 0x7, 0x0, 0x0, 0x0, 0xA, 0x0, 0x0, 0x0, + 0x37, 0x0, 0x3, 0x0, 0x7, 0x0, 0x0, 0x0, 0xB, 0x0, + 0x0, 0x0, 0xF8, 0x0, 0x2, 0x0, 0xD, 0x0, 0x0, 0x0, + 0x3D, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x11, 0x0, + 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x12, 0x0, 0x0, 0x0, 0xA, 0x0, + 0x0, 0x0, 0xC, 0x0, 0x7, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x13, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x25, 0x0, + 0x0, 0x0, 0x11, 0x0, 0x0, 0x0, 0x12, 0x0, 0x0, 0x0, + 0x3D, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x14, 0x0, + 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x15, 0x0, 0x0, 0x0, 0xA, 0x0, + 0x0, 0x0, 0xC, 0x0, 0x7, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x16, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x28, 0x0, + 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x15, 0x0, 0x0, 0x0, + 0x3D, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x17, 0x0, + 0x0, 0x0, 0xB, 0x0, 0x0, 0x0, 0xC, 0x0, 0x7, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x18, 0x0, 0x0, 0x0, 0x1, 0x0, + 0x0, 0x0, 0x25, 0x0, 0x0, 0x0, 0x16, 0x0, 0x0, 0x0, + 0x17, 0x0, 0x0, 0x0, 0xC, 0x0, 0x7, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x19, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x28, 0x0, 0x0, 0x0, 0x13, 0x0, 0x0, 0x0, 0x18, 0x0, + 0x0, 0x0, 0xFE, 0x0, 0x2, 0x0, 0x19, 0x0, 0x0, 0x0, + 0x38, 0x0, 0x1, 0x0, 0x36, 0x0, 0x5, 0x0, 0x6, 0x0, + 0x0, 0x0, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xE, 0x0, 0x0, 0x0, 0xF8, 0x0, 0x2, 0x0, 0x10, 0x0, + 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x1D, 0x0, 0x0, 0x0, + 0x1E, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3B, 0x0, + 0x4, 0x0, 0x1D, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0x0, + 0x7, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x24, 0x0, + 0x0, 0x0, 0x25, 0x0, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0, + 0x23, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x26, 0x0, 0x0, 0x0, 0x25, 0x0, 0x0, 0x0, + 0x50, 0x0, 0x5, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x27, 0x0, + 0x0, 0x0, 0x26, 0x0, 0x0, 0x0, 0x26, 0x0, 0x0, 0x0, + 0x3D, 0x0, 0x4, 0x0, 0x29, 0x0, 0x0, 0x0, 0x2C, 0x0, + 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x64, 0x0, 0x4, 0x0, + 0x28, 0x0, 0x0, 0x0, 0x2D, 0x0, 0x0, 0x0, 0x2C, 0x0, + 0x0, 0x0, 0x67, 0x0, 0x5, 0x0, 0x2E, 0x0, 0x0, 0x0, + 0x2F, 0x0, 0x0, 0x0, 0x2D, 0x0, 0x0, 0x0, 0x23, 0x0, + 0x0, 0x0, 0x6F, 0x0, 0x4, 0x0, 0x1C, 0x0, 0x0, 0x0, + 0x30, 0x0, 0x0, 0x0, 0x2F, 0x0, 0x0, 0x0, 0x88, 0x0, + 0x5, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x31, 0x0, 0x0, 0x0, + 0x27, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x3E, 0x0, + 0x3, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x31, 0x0, 0x0, 0x0, + 0x3D, 0x0, 0x4, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x37, 0x0, + 0x0, 0x0, 0x36, 0x0, 0x0, 0x0, 0xD1, 0x0, 0x4, 0x0, + 0x1C, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x37, 0x0, + 0x0, 0x0, 0x88, 0x0, 0x5, 0x0, 0x1C, 0x0, 0x0, 0x0, + 0x39, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x38, 0x0, + 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x32, 0x0, 0x0, 0x0, + 0x39, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x1C, 0x0, + 0x0, 0x0, 0x3B, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, + 0x3D, 0x0, 0x4, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x3C, 0x0, + 0x0, 0x0, 0x32, 0x0, 0x0, 0x0, 0x94, 0x0, 0x5, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x0, 0x0, 0x3B, 0x0, + 0x0, 0x0, 0x3C, 0x0, 0x0, 0x0, 0x85, 0x0, 0x5, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x0, 0x0, 0x3A, 0x0, + 0x0, 0x0, 0x3D, 0x0, 0x0, 0x0, 0xC, 0x0, 0x7, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x3F, 0x0, 0x0, 0x0, 0x1, 0x0, + 0x0, 0x0, 0x28, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x0, 0x0, + 0x33, 0x0, 0x0, 0x0, 0xFE, 0x0, 0x2, 0x0, 0x3F, 0x0, + 0x0, 0x0, 0x38, 0x0, 0x1, 0x0, + ]; + + public byte[] TextTransformVert { get; } = + [ + 0x3, 0x2, 0x23, 0x7, 0x0, 0x0, 0x1, 0x0, 0xB, 0x0, + 0x8, 0x0, 0x2D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x11, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0xB, 0x0, + 0x6, 0x0, 0x1, 0x0, 0x0, 0x0, 0x47, 0x4C, 0x53, 0x4C, + 0x2E, 0x73, 0x74, 0x64, 0x2E, 0x34, 0x35, 0x30, 0x0, 0x0, + 0x0, 0x0, 0xE, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0xF, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E, + 0x0, 0x0, 0x0, 0x0, 0xD, 0x0, 0x0, 0x0, 0x19, 0x0, + 0x0, 0x0, 0x25, 0x0, 0x0, 0x0, 0x27, 0x0, 0x0, 0x0, + 0x29, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x3, 0x0, + 0x3, 0x0, 0x2, 0x0, 0x0, 0x0, 0xC2, 0x1, 0x0, 0x0, + 0x5, 0x0, 0x4, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, + 0x69, 0x6E, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x6, 0x0, + 0xB, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x50, 0x65, 0x72, + 0x56, 0x65, 0x72, 0x74, 0x65, 0x78, 0x0, 0x0, 0x0, 0x0, + 0x6, 0x0, 0x6, 0x0, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x50, 0x6F, 0x73, 0x69, 0x74, + 0x69, 0x6F, 0x6E, 0x0, 0x6, 0x0, 0x7, 0x0, 0xB, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x50, + 0x6F, 0x69, 0x6E, 0x74, 0x53, 0x69, 0x7A, 0x65, 0x0, 0x0, + 0x0, 0x0, 0x6, 0x0, 0x7, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x2, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x43, 0x6C, 0x69, + 0x70, 0x44, 0x69, 0x73, 0x74, 0x61, 0x6E, 0x63, 0x65, 0x0, + 0x6, 0x0, 0x7, 0x0, 0xB, 0x0, 0x0, 0x0, 0x3, 0x0, + 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x43, 0x75, 0x6C, 0x6C, 0x44, + 0x69, 0x73, 0x74, 0x61, 0x6E, 0x63, 0x65, 0x0, 0x5, 0x0, + 0x3, 0x0, 0xD, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x5, 0x0, 0x3, 0x0, 0x11, 0x0, 0x0, 0x0, 0x55, 0x42, + 0x4F, 0x0, 0x6, 0x0, 0x7, 0x0, 0x11, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x56, 0x69, 0x65, 0x77, 0x50, 0x72, + 0x6F, 0x6A, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x0, 0x0, + 0x5, 0x0, 0x3, 0x0, 0x13, 0x0, 0x0, 0x0, 0x75, 0x62, + 0x6F, 0x0, 0x5, 0x0, 0x4, 0x0, 0x19, 0x0, 0x0, 0x0, + 0x69, 0x6E, 0x50, 0x6F, 0x73, 0x0, 0x0, 0x0, 0x5, 0x0, + 0x5, 0x0, 0x25, 0x0, 0x0, 0x0, 0x6F, 0x75, 0x74, 0x54, + 0x65, 0x78, 0x43, 0x6F, 0x6F, 0x72, 0x64, 0x0, 0x5, 0x0, + 0x5, 0x0, 0x27, 0x0, 0x0, 0x0, 0x69, 0x6E, 0x54, 0x65, + 0x78, 0x43, 0x6F, 0x6F, 0x72, 0x64, 0x0, 0x0, 0x5, 0x0, + 0x5, 0x0, 0x29, 0x0, 0x0, 0x0, 0x6F, 0x75, 0x74, 0x43, + 0x6F, 0x6C, 0x6F, 0x72, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, + 0x4, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x69, 0x6E, 0x43, 0x6F, + 0x6C, 0x6F, 0x72, 0x0, 0x48, 0x0, 0x5, 0x0, 0xB, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0xB, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0xB, 0x0, + 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x3, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0xB, 0x0, + 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x4, 0x0, 0x0, 0x0, 0x47, 0x0, 0x3, 0x0, 0xB, 0x0, + 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x48, 0x0, 0x4, 0x0, + 0x11, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, + 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0x11, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0x11, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x10, 0x0, + 0x0, 0x0, 0x47, 0x0, 0x3, 0x0, 0x11, 0x0, 0x0, 0x0, + 0x2, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x13, 0x0, + 0x0, 0x0, 0x22, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x47, 0x0, 0x4, 0x0, 0x13, 0x0, 0x0, 0x0, 0x21, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, + 0x19, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x25, 0x0, 0x0, 0x0, + 0x1E, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x47, 0x0, + 0x4, 0x0, 0x27, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x29, 0x0, + 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x47, 0x0, 0x4, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x1E, 0x0, + 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x13, 0x0, 0x2, 0x0, + 0x2, 0x0, 0x0, 0x0, 0x21, 0x0, 0x3, 0x0, 0x3, 0x0, + 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x16, 0x0, 0x3, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x17, 0x0, + 0x4, 0x0, 0x7, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x4, 0x0, 0x0, 0x0, 0x15, 0x0, 0x4, 0x0, 0x8, 0x0, + 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x2B, 0x0, 0x4, 0x0, 0x8, 0x0, 0x0, 0x0, 0x9, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1C, 0x0, 0x4, 0x0, + 0xA, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x9, 0x0, + 0x0, 0x0, 0x1E, 0x0, 0x6, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x7, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0xA, 0x0, + 0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, + 0xC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0xB, 0x0, + 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0xC, 0x0, 0x0, 0x0, + 0xD, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x15, 0x0, + 0x4, 0x0, 0xE, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0xE, 0x0, + 0x0, 0x0, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x18, 0x0, 0x4, 0x0, 0x10, 0x0, 0x0, 0x0, 0x7, 0x0, + 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x3, 0x0, + 0x11, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x20, 0x0, + 0x4, 0x0, 0x12, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, + 0x11, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x12, 0x0, + 0x0, 0x0, 0x13, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, + 0x20, 0x0, 0x4, 0x0, 0x14, 0x0, 0x0, 0x0, 0x2, 0x0, + 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0, + 0x17, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x3, 0x0, + 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x18, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x17, 0x0, 0x0, 0x0, 0x3B, 0x0, + 0x4, 0x0, 0x18, 0x0, 0x0, 0x0, 0x19, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x1B, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3F, + 0x20, 0x0, 0x4, 0x0, 0x21, 0x0, 0x0, 0x0, 0x3, 0x0, + 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0, + 0x23, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x2, 0x0, + 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x24, 0x0, 0x0, 0x0, + 0x3, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0, 0x3B, 0x0, + 0x4, 0x0, 0x24, 0x0, 0x0, 0x0, 0x25, 0x0, 0x0, 0x0, + 0x3, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x26, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0, + 0x3B, 0x0, 0x4, 0x0, 0x26, 0x0, 0x0, 0x0, 0x27, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, + 0x21, 0x0, 0x0, 0x0, 0x29, 0x0, 0x0, 0x0, 0x3, 0x0, + 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x2A, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3B, 0x0, + 0x4, 0x0, 0x2A, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x36, 0x0, 0x5, 0x0, 0x2, 0x0, + 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x3, 0x0, 0x0, 0x0, 0xF8, 0x0, 0x2, 0x0, 0x5, 0x0, + 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x14, 0x0, 0x0, 0x0, + 0x15, 0x0, 0x0, 0x0, 0x13, 0x0, 0x0, 0x0, 0xF, 0x0, + 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x10, 0x0, 0x0, 0x0, + 0x16, 0x0, 0x0, 0x0, 0x15, 0x0, 0x0, 0x0, 0x3D, 0x0, + 0x4, 0x0, 0x17, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0, + 0x19, 0x0, 0x0, 0x0, 0x51, 0x0, 0x5, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x51, 0x0, 0x5, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x1D, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x51, 0x0, 0x5, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0, + 0x2, 0x0, 0x0, 0x0, 0x50, 0x0, 0x7, 0x0, 0x7, 0x0, + 0x0, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x1C, 0x0, 0x0, 0x0, + 0x1D, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x1B, 0x0, + 0x0, 0x0, 0x91, 0x0, 0x5, 0x0, 0x7, 0x0, 0x0, 0x0, + 0x20, 0x0, 0x0, 0x0, 0x16, 0x0, 0x0, 0x0, 0x1F, 0x0, + 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x21, 0x0, 0x0, 0x0, + 0x22, 0x0, 0x0, 0x0, 0xD, 0x0, 0x0, 0x0, 0xF, 0x0, + 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x22, 0x0, 0x0, 0x0, + 0x20, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x23, 0x0, + 0x0, 0x0, 0x28, 0x0, 0x0, 0x0, 0x27, 0x0, 0x0, 0x0, + 0x3E, 0x0, 0x3, 0x0, 0x25, 0x0, 0x0, 0x0, 0x28, 0x0, + 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x7, 0x0, 0x0, 0x0, + 0x2C, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x3E, 0x0, + 0x3, 0x0, 0x29, 0x0, 0x0, 0x0, 0x2C, 0x0, 0x0, 0x0, + 0xFD, 0x0, 0x1, 0x0, 0x38, 0x0, 0x1, 0x0, + ]; + + public byte[] VideoYuv2RgbaFrag { get; } = + [ + 0x3, 0x2, 0x23, 0x7, 0x0, 0x0, 0x1, 0x0, 0xB, 0x0, + 0x8, 0x0, 0x45, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x11, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0xB, 0x0, + 0x6, 0x0, 0x1, 0x0, 0x0, 0x0, 0x47, 0x4C, 0x53, 0x4C, + 0x2E, 0x73, 0x74, 0x64, 0x2E, 0x34, 0x35, 0x30, 0x0, 0x0, + 0x0, 0x0, 0xE, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0xF, 0x0, 0x7, 0x0, 0x4, 0x0, + 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E, + 0x0, 0x0, 0x0, 0x0, 0x11, 0x0, 0x0, 0x0, 0x2E, 0x0, + 0x0, 0x0, 0x10, 0x0, 0x3, 0x0, 0x4, 0x0, 0x0, 0x0, + 0x7, 0x0, 0x0, 0x0, 0x3, 0x0, 0x3, 0x0, 0x2, 0x0, + 0x0, 0x0, 0xC2, 0x1, 0x0, 0x0, 0x5, 0x0, 0x4, 0x0, + 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E, 0x0, 0x0, + 0x0, 0x0, 0x5, 0x0, 0x3, 0x0, 0x9, 0x0, 0x0, 0x0, + 0x79, 0x75, 0x76, 0x0, 0x5, 0x0, 0x5, 0x0, 0xD, 0x0, + 0x0, 0x0, 0x59, 0x53, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x72, + 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x5, 0x0, 0x11, 0x0, + 0x0, 0x0, 0x54, 0x65, 0x78, 0x43, 0x6F, 0x6F, 0x72, 0x64, + 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x5, 0x0, 0x1A, 0x0, + 0x0, 0x0, 0x55, 0x53, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x72, + 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x5, 0x0, 0x21, 0x0, + 0x0, 0x0, 0x56, 0x53, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x72, + 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x5, 0x0, 0x2E, 0x0, + 0x0, 0x0, 0x46, 0x72, 0x61, 0x67, 0x43, 0x6F, 0x6C, 0x6F, + 0x72, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0xD, 0x0, + 0x0, 0x0, 0x22, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, + 0x47, 0x0, 0x4, 0x0, 0xD, 0x0, 0x0, 0x0, 0x21, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, + 0x11, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x1A, 0x0, 0x0, 0x0, + 0x22, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x47, 0x0, + 0x4, 0x0, 0x1A, 0x0, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x21, 0x0, + 0x0, 0x0, 0x22, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, + 0x47, 0x0, 0x4, 0x0, 0x21, 0x0, 0x0, 0x0, 0x21, 0x0, + 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, + 0x2E, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x13, 0x0, 0x2, 0x0, 0x2, 0x0, 0x0, 0x0, + 0x21, 0x0, 0x3, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x0, + 0x0, 0x0, 0x16, 0x0, 0x3, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x20, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0, 0x7, 0x0, + 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, + 0x20, 0x0, 0x4, 0x0, 0x8, 0x0, 0x0, 0x0, 0x7, 0x0, + 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x19, 0x0, 0x9, 0x0, + 0xA, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x1, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x1B, 0x0, 0x3, 0x0, 0xB, 0x0, 0x0, 0x0, + 0xA, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0xC, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0, + 0x3B, 0x0, 0x4, 0x0, 0xC, 0x0, 0x0, 0x0, 0xD, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x2, 0x0, + 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x10, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0xF, 0x0, 0x0, 0x0, 0x3B, 0x0, + 0x4, 0x0, 0x10, 0x0, 0x0, 0x0, 0x11, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0, 0x13, 0x0, + 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, + 0x15, 0x0, 0x4, 0x0, 0x15, 0x0, 0x0, 0x0, 0x20, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, + 0x15, 0x0, 0x0, 0x0, 0x16, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x18, 0x0, 0x0, 0x0, + 0x7, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x3B, 0x0, + 0x4, 0x0, 0xC, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x15, 0x0, + 0x0, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x3B, 0x0, 0x4, 0x0, 0xC, 0x0, 0x0, 0x0, 0x21, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, + 0x15, 0x0, 0x0, 0x0, 0x26, 0x0, 0x0, 0x0, 0x2, 0x0, + 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x28, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xBD, 0x2B, 0x0, + 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x29, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0xBF, 0x2C, 0x0, 0x6, 0x0, 0x7, 0x0, + 0x0, 0x0, 0x2A, 0x0, 0x0, 0x0, 0x28, 0x0, 0x0, 0x0, + 0x29, 0x0, 0x0, 0x0, 0x29, 0x0, 0x0, 0x0, 0x20, 0x0, + 0x4, 0x0, 0x2D, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, + 0x13, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x2D, 0x0, + 0x0, 0x0, 0x2E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, + 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x30, 0x0, + 0x0, 0x0, 0xF4, 0xFD, 0x94, 0x3F, 0x2B, 0x0, 0x4, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x31, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x32, 0x0, 0x0, 0x0, 0x6, 0x81, 0xE5, 0x3F, 0x2C, 0x0, + 0x6, 0x0, 0x7, 0x0, 0x0, 0x0, 0x33, 0x0, 0x0, 0x0, + 0x30, 0x0, 0x0, 0x0, 0x31, 0x0, 0x0, 0x0, 0x32, 0x0, + 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x35, 0x0, 0x0, 0x0, + 0x3, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x2B, 0x0, + 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, + 0xAC, 0x1C, 0x5A, 0xBE, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x39, 0x0, 0x0, 0x0, 0xB0, 0x72, 0x8, 0xBF, + 0x2C, 0x0, 0x6, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3A, 0x0, + 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, + 0x39, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x3E, 0x0, 0x0, 0x0, 0x2, 0x2B, 0x7, 0x40, + 0x2C, 0x0, 0x6, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3F, 0x0, + 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x0, 0x0, + 0x31, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x42, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3F, + 0x2B, 0x0, 0x4, 0x0, 0x15, 0x0, 0x0, 0x0, 0x43, 0x0, + 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x36, 0x0, 0x5, 0x0, + 0x2, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0xF8, 0x0, 0x2, 0x0, + 0x5, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x8, 0x0, + 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, + 0x3D, 0x0, 0x4, 0x0, 0xB, 0x0, 0x0, 0x0, 0xE, 0x0, + 0x0, 0x0, 0xD, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x12, 0x0, 0x0, 0x0, 0x11, 0x0, + 0x0, 0x0, 0x57, 0x0, 0x5, 0x0, 0x13, 0x0, 0x0, 0x0, + 0x14, 0x0, 0x0, 0x0, 0xE, 0x0, 0x0, 0x0, 0x12, 0x0, + 0x0, 0x0, 0x51, 0x0, 0x5, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x17, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x18, 0x0, 0x0, 0x0, + 0x19, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x16, 0x0, + 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x19, 0x0, 0x0, 0x0, + 0x17, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0xB, 0x0, + 0x0, 0x0, 0x1B, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0, + 0x3D, 0x0, 0x4, 0x0, 0xF, 0x0, 0x0, 0x0, 0x1C, 0x0, + 0x0, 0x0, 0x11, 0x0, 0x0, 0x0, 0x57, 0x0, 0x5, 0x0, + 0x13, 0x0, 0x0, 0x0, 0x1D, 0x0, 0x0, 0x0, 0x1B, 0x0, + 0x0, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x51, 0x0, 0x5, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x1D, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, + 0x18, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x9, 0x0, + 0x0, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, + 0x20, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x3D, 0x0, + 0x4, 0x0, 0xB, 0x0, 0x0, 0x0, 0x22, 0x0, 0x0, 0x0, + 0x21, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0xF, 0x0, + 0x0, 0x0, 0x23, 0x0, 0x0, 0x0, 0x11, 0x0, 0x0, 0x0, + 0x57, 0x0, 0x5, 0x0, 0x13, 0x0, 0x0, 0x0, 0x24, 0x0, + 0x0, 0x0, 0x22, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0, + 0x51, 0x0, 0x5, 0x0, 0x6, 0x0, 0x0, 0x0, 0x25, 0x0, + 0x0, 0x0, 0x24, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x41, 0x0, 0x5, 0x0, 0x18, 0x0, 0x0, 0x0, 0x27, 0x0, + 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x26, 0x0, 0x0, 0x0, + 0x3E, 0x0, 0x3, 0x0, 0x27, 0x0, 0x0, 0x0, 0x25, 0x0, + 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x7, 0x0, 0x0, 0x0, + 0x2B, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x81, 0x0, + 0x5, 0x0, 0x7, 0x0, 0x0, 0x0, 0x2C, 0x0, 0x0, 0x0, + 0x2B, 0x0, 0x0, 0x0, 0x2A, 0x0, 0x0, 0x0, 0x3E, 0x0, + 0x3, 0x0, 0x9, 0x0, 0x0, 0x0, 0x2C, 0x0, 0x0, 0x0, + 0x3D, 0x0, 0x4, 0x0, 0x7, 0x0, 0x0, 0x0, 0x2F, 0x0, + 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x94, 0x0, 0x5, 0x0, + 0x6, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x2F, 0x0, + 0x0, 0x0, 0x33, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, + 0x35, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x0, 0x2E, 0x0, + 0x0, 0x0, 0x16, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, + 0x36, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x3D, 0x0, + 0x4, 0x0, 0x7, 0x0, 0x0, 0x0, 0x37, 0x0, 0x0, 0x0, + 0x9, 0x0, 0x0, 0x0, 0x94, 0x0, 0x5, 0x0, 0x6, 0x0, + 0x0, 0x0, 0x3B, 0x0, 0x0, 0x0, 0x37, 0x0, 0x0, 0x0, + 0x3A, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x35, 0x0, + 0x0, 0x0, 0x3C, 0x0, 0x0, 0x0, 0x2E, 0x0, 0x0, 0x0, + 0x1F, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x3C, 0x0, + 0x0, 0x0, 0x3B, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, + 0x7, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x0, 0x0, 0x9, 0x0, + 0x0, 0x0, 0x94, 0x0, 0x5, 0x0, 0x6, 0x0, 0x0, 0x0, + 0x40, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x0, 0x0, 0x3F, 0x0, + 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x35, 0x0, 0x0, 0x0, + 0x41, 0x0, 0x0, 0x0, 0x2E, 0x0, 0x0, 0x0, 0x26, 0x0, + 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x41, 0x0, 0x0, 0x0, + 0x40, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x35, 0x0, + 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, 0x2E, 0x0, 0x0, 0x0, + 0x43, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x44, 0x0, + 0x0, 0x0, 0x42, 0x0, 0x0, 0x0, 0xFD, 0x0, 0x1, 0x0, + 0x38, 0x0, 0x1, 0x0, + ]; +} diff --git a/Nerfed.Runtime/Graphics/FencePool.cs b/Nerfed.Runtime/Graphics/FencePool.cs new file mode 100644 index 0000000..76d5068 --- /dev/null +++ b/Nerfed.Runtime/Graphics/FencePool.cs @@ -0,0 +1,31 @@ +using System.Collections.Concurrent; + +namespace Nerfed.Runtime.Graphics; + +internal class FencePool +{ + private GraphicsDevice GraphicsDevice; + private ConcurrentQueue Fences = new ConcurrentQueue(); + + public FencePool(GraphicsDevice graphicsDevice) + { + GraphicsDevice = graphicsDevice; + } + + public Fence Obtain() + { + if (Fences.TryDequeue(out Fence fence)) + { + return fence; + } + else + { + return new Fence(GraphicsDevice); + } + } + + public void Return(Fence fence) + { + Fences.Enqueue(fence); + } +} diff --git a/Nerfed.Runtime/Graphics/Font/Enums.cs b/Nerfed.Runtime/Graphics/Font/Enums.cs new file mode 100644 index 0000000..55f8a49 --- /dev/null +++ b/Nerfed.Runtime/Graphics/Font/Enums.cs @@ -0,0 +1,16 @@ +namespace Nerfed.Runtime.Graphics; + +public enum HorizontalAlignment +{ + Left, + Center, + Right +} + +public enum VerticalAlignment +{ + Baseline, + Top, + Middle, + Bottom +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/Font/Font.cs b/Nerfed.Runtime/Graphics/Font/Font.cs new file mode 100644 index 0000000..1533312 --- /dev/null +++ b/Nerfed.Runtime/Graphics/Font/Font.cs @@ -0,0 +1,123 @@ +using System.Runtime.InteropServices; +using WellspringCS; + +namespace Nerfed.Runtime.Graphics; + +public unsafe class Font : GraphicsResource +{ + public Texture Texture { get; } + public float PixelsPerEm { get; } + public float DistanceRange { get; } + + internal IntPtr Handle { get; } + + private byte* StringBytes; + private int StringBytesLength; + + /// + /// Loads a TTF or OTF font from a path for use in MSDF rendering. + /// Note that there must be an msdf-atlas-gen JSON and image file alongside. + /// + /// + public static Font Load(GraphicsDevice graphicsDevice, CommandBuffer commandBuffer, string fontPath) + { + FileStream fontFileStream = new FileStream(fontPath, FileMode.Open, FileAccess.Read); + void* fontFileByteBuffer = NativeMemory.Alloc((nuint)fontFileStream.Length); + Span fontFileByteSpan = new Span(fontFileByteBuffer, (int)fontFileStream.Length); + fontFileStream.ReadExactly(fontFileByteSpan); + fontFileStream.Close(); + + FileStream atlasFileStream = new FileStream(Path.ChangeExtension(fontPath, ".json"), FileMode.Open, FileAccess.Read); + void* atlasFileByteBuffer = NativeMemory.Alloc((nuint)atlasFileStream.Length); + Span atlasFileByteSpan = new Span(atlasFileByteBuffer, (int)atlasFileStream.Length); + atlasFileStream.ReadExactly(atlasFileByteSpan); + atlasFileStream.Close(); + + IntPtr handle = Wellspring.Wellspring_CreateFont( + (IntPtr)fontFileByteBuffer, + (uint)fontFileByteSpan.Length, + (IntPtr)atlasFileByteBuffer, + (uint)atlasFileByteSpan.Length, + out float pixelsPerEm, + out float distanceRange + ); + + string imagePath = Path.ChangeExtension(fontPath, ".png"); + ImageUtils.ImageInfoFromFile(imagePath, out uint width, out uint height, out uint sizeInBytes); + + ResourceUploader uploader = new ResourceUploader(graphicsDevice); + Texture texture = uploader.CreateTexture2DFromCompressed(imagePath); + uploader.Upload(); + uploader.Dispose(); + + NativeMemory.Free(fontFileByteBuffer); + NativeMemory.Free(atlasFileByteBuffer); + + return new Font(graphicsDevice, handle, texture, pixelsPerEm, distanceRange); + } + + private Font(GraphicsDevice device, IntPtr handle, Texture texture, float pixelsPerEm, float distanceRange) : base(device) + { + Handle = handle; + Texture = texture; + PixelsPerEm = pixelsPerEm; + DistanceRange = distanceRange; + + StringBytesLength = 32; + StringBytes = (byte*)NativeMemory.Alloc((nuint)StringBytesLength); + } + + public unsafe bool TextBounds( + string text, + int pixelSize, + HorizontalAlignment horizontalAlignment, + VerticalAlignment verticalAlignment, + out Wellspring.Rectangle rectangle + ) + { + int byteCount = System.Text.Encoding.UTF8.GetByteCount(text); + + if (StringBytesLength < byteCount) + { + StringBytes = (byte*)NativeMemory.Realloc(StringBytes, (nuint)byteCount); + } + + fixed (char* chars = text) + { + System.Text.Encoding.UTF8.GetBytes(chars, text.Length, StringBytes, byteCount); + + byte result = Wellspring.Wellspring_TextBounds( + Handle, + pixelSize, + (Wellspring.HorizontalAlignment)horizontalAlignment, + (Wellspring.VerticalAlignment)verticalAlignment, + (IntPtr)StringBytes, + (uint)byteCount, + out rectangle + ); + + if (result == 0) + { + Log.Warning("Could not decode string: " + text); + return false; + } + } + + return true; + } + + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + Texture.Dispose(); + } + + Wellspring.Wellspring_DestroyFont(Handle); + } + + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/Font/Structs.cs b/Nerfed.Runtime/Graphics/Font/Structs.cs new file mode 100644 index 0000000..cd353b6 --- /dev/null +++ b/Nerfed.Runtime/Graphics/Font/Structs.cs @@ -0,0 +1,26 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Graphics; + +[StructLayout(LayoutKind.Sequential)] +public struct FontVertex : IVertexType +{ + public Vector3 Position; + public Vector2 TexCoord; + public Color Color; + + public static VertexElementFormat[] Formats { get; } = + [ + VertexElementFormat.Vector3, + VertexElementFormat.Vector2, + VertexElementFormat.Color + ]; + + public static uint[] Offsets { get; } = + [ + 0, + 12, + 20 + ]; +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/Font/TextBatch.cs b/Nerfed.Runtime/Graphics/Font/TextBatch.cs new file mode 100644 index 0000000..0b3b08f --- /dev/null +++ b/Nerfed.Runtime/Graphics/Font/TextBatch.cs @@ -0,0 +1,191 @@ +using System.Numerics; +using System.Runtime.InteropServices; +using WellspringCS; + +namespace Nerfed.Runtime.Graphics; + +public unsafe class TextBatch : GraphicsResource +{ + public const int INITIAL_CHAR_COUNT = 64; + public const int INITIAL_VERTEX_COUNT = INITIAL_CHAR_COUNT * 4; + public const int INITIAL_INDEX_COUNT = INITIAL_CHAR_COUNT * 6; + + public IntPtr Handle { get; } + public Buffer VertexBuffer { get; protected set; } = null; + public Buffer IndexBuffer { get; protected set; } = null; + public uint PrimitiveCount { get; protected set; } + public Font CurrentFont { get; private set; } + + private readonly GraphicsDevice graphicsDevice; + private readonly int stringBytesLength; + private TransferBuffer TransferBuffer; + private byte* stringBytes; + + public TextBatch(GraphicsDevice device) : base(device) + { + graphicsDevice = device; + Handle = Wellspring.Wellspring_CreateTextBatch(); + + stringBytesLength = 128; + stringBytes = (byte*)NativeMemory.Alloc((nuint)stringBytesLength); + + VertexBuffer = Buffer.Create(graphicsDevice, BufferUsageFlags.Vertex, INITIAL_VERTEX_COUNT); + IndexBuffer = Buffer.Create(graphicsDevice, BufferUsageFlags.Index, INITIAL_INDEX_COUNT); + + TransferBuffer = TransferBuffer.Create( + graphicsDevice, + TransferBufferUsage.Upload, + VertexBuffer.Size + IndexBuffer.Size + ); + } + + // Call this to initialize or reset the batch. + public void Start(Font font) + { + Wellspring.Wellspring_StartTextBatch(Handle, font.Handle); + CurrentFont = font; + PrimitiveCount = 0; + } + + // Add text with size and color to the batch + public unsafe bool Add( + string text, + int pixelSize, + Color color, + HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment verticalAlignment = VerticalAlignment.Baseline + ) + { + int byteCount = System.Text.Encoding.UTF8.GetByteCount(text); + + if (stringBytesLength < byteCount) + { + stringBytes = (byte*)NativeMemory.Realloc(stringBytes, (nuint)byteCount); + } + + fixed (char* chars = text) + { + System.Text.Encoding.UTF8.GetBytes(chars, text.Length, stringBytes, byteCount); + + byte result = Wellspring.Wellspring_AddToTextBatch( + Handle, + pixelSize, + new Wellspring.Color { R = color.R, G = color.G, B = color.B, A = color.A }, + (Wellspring.HorizontalAlignment)horizontalAlignment, + (Wellspring.VerticalAlignment)verticalAlignment, + (IntPtr)stringBytes, + (uint)byteCount + ); + + if (result == 0) + { + Log.Warning("Could not decode string: " + text); + return false; + } + } + + return true; + } + + // Call this after you have made all the Add calls you want, but before beginning a render pass. + public unsafe void UploadBufferData(CommandBuffer commandBuffer) + { + Wellspring.Wellspring_GetBufferData( + Handle, + out uint vertexCount, + out IntPtr vertexDataPointer, + out uint vertexDataLengthInBytes, + out IntPtr indexDataPointer, + out uint indexDataLengthInBytes + ); + + Span vertexSpan = new Span((void*)vertexDataPointer, (int)vertexDataLengthInBytes); + Span indexSpan = new Span((void*)indexDataPointer, (int)indexDataLengthInBytes); + + bool newTransferBufferNeeded = false; + + if (VertexBuffer.Size < vertexDataLengthInBytes) + { + VertexBuffer.Dispose(); + VertexBuffer = new Buffer(graphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes); + newTransferBufferNeeded = true; + } + + if (IndexBuffer.Size < indexDataLengthInBytes) + { + IndexBuffer.Dispose(); + IndexBuffer = new Buffer(graphicsDevice, BufferUsageFlags.Index, vertexDataLengthInBytes); + newTransferBufferNeeded = true; + } + + if (newTransferBufferNeeded) + { + TransferBuffer.Dispose(); + TransferBuffer = new TransferBuffer( + graphicsDevice, + TransferBufferUsage.Upload, + VertexBuffer.Size + IndexBuffer.Size + ); + } + + if (vertexDataLengthInBytes > 0 && indexDataLengthInBytes > 0) + { + TransferBuffer.SetData(vertexSpan, true); + TransferBuffer.SetData(indexSpan, (uint)vertexSpan.Length, false); + + CopyPass copyPass = commandBuffer.BeginCopyPass(); + copyPass.UploadToBuffer( + new TransferBufferLocation(TransferBuffer), + new BufferRegion(VertexBuffer, 0, (uint)vertexSpan.Length), + true + ); + copyPass.UploadToBuffer( + new TransferBufferLocation(TransferBuffer, (uint)vertexSpan.Length), + new BufferRegion(IndexBuffer, 0, (uint)indexSpan.Length), + true + ); + commandBuffer.EndCopyPass(copyPass); + } + + PrimitiveCount = vertexCount / 2; + } + + // Call this AFTER binding your text pipeline! + public void Render(CommandBuffer commandBuffer, RenderPass renderPass, Matrix4x4 transformMatrix) + { + commandBuffer.PushVertexUniformData(transformMatrix); + commandBuffer.PushFragmentUniformData(CurrentFont.DistanceRange); + + renderPass.BindFragmentSampler( + new TextureSamplerBinding( + CurrentFont.Texture, + graphicsDevice.LinearSampler + ) + ); + renderPass.BindVertexBuffer(VertexBuffer); + renderPass.BindIndexBuffer(IndexBuffer, IndexElementSize.ThirtyTwo); + + renderPass.DrawIndexedPrimitives( + 0, + 0, + PrimitiveCount + ); + } + + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + VertexBuffer.Dispose(); + IndexBuffer.Dispose(); + } + + NativeMemory.Free(stringBytes); + Wellspring.Wellspring_DestroyTextBatch(Handle); + } + + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/GraphicsDevice.cs b/Nerfed.Runtime/Graphics/GraphicsDevice.cs new file mode 100644 index 0000000..4bdfde2 --- /dev/null +++ b/Nerfed.Runtime/Graphics/GraphicsDevice.cs @@ -0,0 +1,466 @@ +using System.Runtime.InteropServices; +using Nerfed.Runtime.Video; +using RefreshCS; + +namespace Nerfed.Runtime.Graphics; + +/// +/// Manages all graphics-related concerns. +/// +public class GraphicsDevice : IDisposable +{ + public IntPtr Handle { get; } + public BackendFlags Backend { get; } + public bool DebugMode { get; } + + // Built-in video pipeline + internal GraphicsPipeline VideoPipeline { get; } + + // Built-in text shader info + public Shader TextVertexShader; + public Shader TextFragmentShader; + public VertexInputState TextVertexInputState { get; } + + // Built-in samplers + public Sampler PointSampler { get; } + public Sampler LinearSampler { get; } + + public bool IsDisposed { get; private set; } + + internal readonly RenderPassPool RenderPassPool = new RenderPassPool(); + internal readonly ComputePassPool ComputePassPool = new ComputePassPool(); + internal readonly CopyPassPool CopyPassPool = new CopyPassPool(); + private readonly HashSet resources = new HashSet(); + private readonly CommandBufferPool commandBufferPool; + private readonly FencePool fencePool; + + internal GraphicsDevice(BackendFlags preferredBackends) + { + if (preferredBackends == BackendFlags.Invalid) + { + throw new System.Exception("Could not set graphics backend!"); + } + + bool debugMode = false; +#if DEBUG + debugMode = true; +#endif + + Handle = Refresh.Refresh_CreateDevice((Refresh.BackendFlags)preferredBackends, Conversions.BoolToInt(debugMode)); + + DebugMode = debugMode; + // TODO: check for CreateDevice fail + + Backend = (BackendFlags)Refresh.Refresh_GetBackend(Handle); + + IEmbeddedShaders embeddedShaders; + switch (Backend) + { + case BackendFlags.Vulkan: + embeddedShaders = new EmbeddedShadersSpirV(); + break; + case BackendFlags.D3D11: + throw new NotImplementedException("D3D11 embedded shaders"); + break; + case BackendFlags.Metal: + throw new NotImplementedException("Metal embedded shaders"); + break; + default: throw new ArgumentOutOfRangeException(); + } + + Shader fullscreenVertShader; + Shader textVertShader; + Shader textFragShader; + Shader videoFragShader; + + using (MemoryStream fullscreenVertStream = new MemoryStream(embeddedShaders.FullscreenVert)) + { + fullscreenVertShader = new Shader( + this, + fullscreenVertStream, + "main", + new ShaderCreateInfo + { + ShaderStage = ShaderStage.Vertex, + ShaderFormat = embeddedShaders.ShaderFormat + } + ); + } + + using (MemoryStream videoYuv2RgbaFragStream = new MemoryStream(embeddedShaders.VideoYuv2RgbaFrag)) + { + videoFragShader = new Shader( + this, + videoYuv2RgbaFragStream, + "main", + new ShaderCreateInfo + { + ShaderStage = ShaderStage.Fragment, + ShaderFormat = embeddedShaders.ShaderFormat, + SamplerCount = 3 + } + ); + } + + using (MemoryStream textTransformVertStream = new MemoryStream(embeddedShaders.TextTransformVert)) + { + textVertShader = new Shader( + this, + textTransformVertStream, + "main", + new ShaderCreateInfo + { + ShaderStage = ShaderStage.Vertex, + ShaderFormat = embeddedShaders.ShaderFormat, + UniformBufferCount = 1 + } + ); + } + + using (MemoryStream textMsdfFragStream = new MemoryStream(embeddedShaders.TextMsdfFrag)) + { + textFragShader = new Shader( + this, + textMsdfFragStream, + "main", + new ShaderCreateInfo + { + ShaderStage = ShaderStage.Fragment, + ShaderFormat = embeddedShaders.ShaderFormat, + SamplerCount = 1, + UniformBufferCount = 1 + } + ); + } + + VideoPipeline = new GraphicsPipeline( + this, + new GraphicsPipelineCreateInfo + { + AttachmentInfo = new GraphicsPipelineAttachmentInfo( + new ColorAttachmentDescription( + TextureFormat.R8G8B8A8, + ColorAttachmentBlendState.None + ) + ), + DepthStencilState = DepthStencilState.Disable, + VertexShader = fullscreenVertShader, + FragmentShader = videoFragShader, + VertexInputState = VertexInputState.Empty, + RasterizerState = RasterizerState.CCW_CullNone, + PrimitiveType = PrimitiveType.TriangleList, + MultisampleState = MultisampleState.None + } + ); + + TextVertexShader = textVertShader; + TextFragmentShader = textFragShader; + + TextVertexInputState = VertexInputState.CreateSingleBinding(); + + PointSampler = new Sampler(this, SamplerCreateInfo.PointClamp); + LinearSampler = new Sampler(this, SamplerCreateInfo.LinearClamp); + + fencePool = new FencePool(this); + commandBufferPool = new CommandBufferPool(this); + } + + /// + /// Prepares a window so that frames can be presented to it. + /// + /// The desired composition of the swapchain. Ignore this unless you are using HDR or tonemapping. + /// The desired presentation mode for the window. Roughly equivalent to V-Sync. + /// True if successfully claimed. + public bool ClaimWindow( + Window window, + SwapchainComposition swapchainComposition, + PresentMode presentMode + ) + { + if (window.Claimed) + { + Log.Error("Window already claimed!"); + return false; + } + + bool success = Conversions.IntToBool( + Refresh.Refresh_ClaimWindow( + Handle, + window.Handle, + (Refresh.SwapchainComposition)swapchainComposition, + (Refresh.PresentMode)presentMode + ) + ); + + if (success) + { + window.Claimed = true; + window.SwapchainComposition = swapchainComposition; + window.SwapchainFormat = GetSwapchainFormat(window); + + if (window.SwapchainTexture == null) + { + window.SwapchainTexture = new Texture(this, window.SwapchainFormat); + } + } + + return success; + } + + /// + /// Unclaims a window, making it unavailable for presenting and freeing associated resources. + /// + public void UnclaimWindow(Window window) + { + if (window.Claimed) + { + Refresh.Refresh_UnclaimWindow( + Handle, + window.Handle + ); + window.Claimed = false; + + // The swapchain texture doesn't actually have a permanent texture reference, so we zero the handle before disposing. + window.SwapchainTexture.Handle = IntPtr.Zero; + window.SwapchainTexture.Dispose(); + window.SwapchainTexture = null; + } + } + + /// + /// Changes the present mode of a claimed window. Does nothing if the window is not claimed. + /// + public bool SetSwapchainParameters( + Window window, + SwapchainComposition swapchainComposition, + PresentMode presentMode + ) + { + if (!window.Claimed) + { + Log.Error("Cannot set present mode on unclaimed window!"); + return false; + } + + bool success = Conversions.IntToBool( + Refresh.Refresh_SetSwapchainParameters( + Handle, + window.Handle, + (Refresh.SwapchainComposition)swapchainComposition, + (Refresh.PresentMode)presentMode + ) + ); + + if (success) + { + window.SwapchainComposition = swapchainComposition; + window.SwapchainFormat = GetSwapchainFormat(window); + + if (window.SwapchainTexture != null) + { + window.SwapchainTexture.Format = window.SwapchainFormat; + } + } + + return success; + } + + /// + /// Acquires a command buffer. + /// This is the start of your rendering process. + /// + /// + public CommandBuffer AcquireCommandBuffer() + { + CommandBuffer commandBuffer = commandBufferPool.Obtain(); + commandBuffer.SetHandle(Refresh.Refresh_AcquireCommandBuffer(Handle)); +#if DEBUG + commandBuffer.ResetStateTracking(); +#endif + return commandBuffer; + } + + /// + /// Submits a command buffer to the GPU for processing. + /// + public void Submit(CommandBuffer commandBuffer) + { +#if DEBUG + if (commandBuffer.Submitted) + { + throw new System.InvalidOperationException("Command buffer already submitted!"); + } +#endif + + Refresh.Refresh_Submit( + commandBuffer.Handle + ); + + commandBufferPool.Return(commandBuffer); + +#if DEBUG + commandBuffer.Submitted = true; +#endif + } + + /// + /// Submits a command buffer to the GPU for processing and acquires a fence associated with the submission. + /// + /// + public Fence SubmitAndAcquireFence(CommandBuffer commandBuffer) + { + IntPtr fenceHandle = Refresh.Refresh_SubmitAndAcquireFence( + commandBuffer.Handle + ); + + Fence fence = fencePool.Obtain(); + fence.SetHandle(fenceHandle); + + return fence; + } + + /// + /// Wait for the graphics device to become idle. + /// + public void Wait() + { + Refresh.Refresh_Wait(Handle); + } + + /// + /// Waits for the given fence to become signaled. + /// + public unsafe void WaitForFence(Fence fence) + { + IntPtr fenceHandle = fence.Handle; + + Refresh.Refresh_WaitForFences( + Handle, + 1, + &fenceHandle, + 1 + ); + } + + /// + /// Wait for one or more fences to become signaled. + /// + /// If true, will wait for all given fences to be signaled. + public unsafe void WaitForFences(Span fences, bool waitAll) + { + IntPtr* handlePtr = stackalloc nint[fences.Length]; + + for (int i = 0; i < fences.Length; i += 1) + { + handlePtr[i] = fences[i].Handle; + } + + Refresh.Refresh_WaitForFences( + Handle, + Conversions.BoolToInt(waitAll), + handlePtr, + (uint)fences.Length + ); + } + + /// + /// Returns true if the fence is signaled, indicating that the associated command buffer has finished processing. + /// + /// Throws if the fence query indicates that the graphics device has been lost. + public bool QueryFence(Fence fence) + { + int result = Refresh.Refresh_QueryFence(Handle, fence.Handle); + + if (result < 0) + { + throw new InvalidOperationException("The graphics device has been lost."); + } + + return result != 0; + } + + /// + /// Release reference to an acquired fence, enabling it to be reused. + /// + public void ReleaseFence(Fence fence) + { + Refresh.Refresh_ReleaseFence(Handle, fence.Handle); + fence.Handle = IntPtr.Zero; + fencePool.Return(fence); + } + + private TextureFormat GetSwapchainFormat(Window window) + { + if (!window.Claimed) + { + throw new System.ArgumentException("Cannot get swapchain format of unclaimed window!"); + } + + return (TextureFormat)Refresh.Refresh_GetSwapchainTextureFormat(Handle, window.Handle); + } + + internal void AddResourceReference(GCHandle resourceReference) + { + lock (resources) + { + resources.Add(resourceReference); + } + } + + internal void RemoveResourceReference(GCHandle resourceReference) + { + lock (resources) + { + resources.Remove(resourceReference); + } + } + + private void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + lock (resources) + { + // Dispose video players first to avoid race condition on threaded decoding + foreach (GCHandle resource in resources) + { + if (resource.Target is VideoPlayer player) + { + player.Dispose(); + } + } + + // Dispose everything else + foreach (GCHandle resource in resources) + { + if (resource.Target is IDisposable disposable) + { + disposable.Dispose(); + } + } + + resources.Clear(); + } + } + + Refresh.Refresh_DestroyDevice(Handle); + + IsDisposed = true; + } + } + + ~GraphicsDevice() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/Nerfed.Runtime/Graphics/GraphicsResource.cs b/Nerfed.Runtime/Graphics/GraphicsResource.cs new file mode 100644 index 0000000..4dd47ed --- /dev/null +++ b/Nerfed.Runtime/Graphics/GraphicsResource.cs @@ -0,0 +1,53 @@ +using System; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Graphics; + +public abstract class GraphicsResource : IDisposable +{ + public GraphicsDevice Device { get; } + + private GCHandle SelfReference; + + public bool IsDisposed { get; private set; } + + protected GraphicsResource(GraphicsDevice device) + { + Device = device; + + SelfReference = GCHandle.Alloc(this, GCHandleType.Weak); + Device.AddResourceReference(SelfReference); + } + + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + Device.RemoveResourceReference(SelfReference); + SelfReference.Free(); + } + + IsDisposed = true; + } + } + + ~GraphicsResource() + { +#if DEBUG + // If you see this log message, you leaked a graphics resource without disposing it! + // We'll try to clean it up for you but you really should fix this. + Log.Warning($"A resource of type {GetType().Name} was not Disposed."); +#endif + + Dispose(false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/Nerfed.Runtime/Graphics/IEmbeddedShaders.cs b/Nerfed.Runtime/Graphics/IEmbeddedShaders.cs new file mode 100644 index 0000000..d533ca3 --- /dev/null +++ b/Nerfed.Runtime/Graphics/IEmbeddedShaders.cs @@ -0,0 +1,10 @@ +namespace Nerfed.Runtime.Graphics; + +internal interface IEmbeddedShaders +{ + ShaderFormat ShaderFormat { get; } + byte[] FullscreenVert { get; } + byte[] TextMsdfFrag { get; } + byte[] TextTransformVert { get; } + byte[] VideoYuv2RgbaFrag { get; } +} diff --git a/Nerfed.Runtime/Graphics/IVertexType.cs b/Nerfed.Runtime/Graphics/IVertexType.cs new file mode 100644 index 0000000..c0353cd --- /dev/null +++ b/Nerfed.Runtime/Graphics/IVertexType.cs @@ -0,0 +1,17 @@ +namespace Nerfed.Runtime.Graphics; + +/// +/// Can be defined on your struct type to enable simplified vertex input state definition. +/// +public interface IVertexType +{ + /// + /// An ordered list of the types in your vertex struct. + /// + static abstract VertexElementFormat[] Formats { get; } + + /// + /// An ordered list of the offsets in your vertex struct. + /// + static abstract uint[] Offsets { get; } +} diff --git a/Nerfed.Runtime/Graphics/ImageUtils.cs b/Nerfed.Runtime/Graphics/ImageUtils.cs new file mode 100644 index 0000000..13db681 --- /dev/null +++ b/Nerfed.Runtime/Graphics/ImageUtils.cs @@ -0,0 +1,455 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using RefreshCS; + +namespace Nerfed.Runtime.Graphics; + +public static class ImageUtils +{ + /// + /// Gets pointer to pixel data from compressed image byte data. + /// + /// The returned pointer must be freed by calling FreePixelData. + /// + public static unsafe byte* GetPixelDataFromBytes( + Span data, + out uint width, + out uint height, + out uint sizeInBytes + ) { + fixed (byte* ptr = data) + { + byte* pixelData = + Refresh.Refresh_Image_Load( + ptr, + data.Length, + out int w, + out int h, + out int len + ); + + width = (uint) w; + height = (uint) h; + sizeInBytes = (uint) len; + + return pixelData; + } + } + + /// + /// Gets pointer to pixel data from a compressed image stream. + /// + /// The returned pointer must be freed by calling FreePixelData. + /// + public static unsafe byte* GetPixelDataFromStream( + Stream stream, + out uint width, + out uint height, + out uint sizeInBytes + ) { + long length = stream.Length; + void* buffer = NativeMemory.Alloc((nuint) length); + Span span = new Span(buffer, (int) length); + stream.ReadExactly(span); + + byte* pixelData = GetPixelDataFromBytes(span, out width, out height, out sizeInBytes); + + NativeMemory.Free(buffer); + + return pixelData; + } + + /// + /// Gets pointer to pixel data from a compressed image file. + /// + /// The returned pointer must be freed by calling FreePixelData. + /// + public static unsafe byte* GetPixelDataFromFile( + string path, + out uint width, + out uint height, + out uint sizeInBytes + ) { + FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read); + return GetPixelDataFromStream(fileStream, out width, out height, out sizeInBytes); + } + + /// + /// Get metadata from compressed image bytes. + /// + public static unsafe bool ImageInfoFromBytes( + Span data, + out uint width, + out uint height, + out uint sizeInBytes + ) { + fixed (byte* ptr = data) + { + int result = + Refresh.Refresh_Image_Info( + ptr, + data.Length, + out int w, + out int h, + out int len + ); + + width = (uint) w; + height = (uint) h; + sizeInBytes = (uint) len; + + return Conversions.IntToBool(result); + } + } + + /// + /// Get metadata from a compressed image stream. + /// + public static unsafe bool ImageInfoFromStream( + Stream stream, + out uint width, + out uint height, + out uint sizeInBytes + ) { + long length = stream.Length; + void* buffer = NativeMemory.Alloc((nuint) length); + Span span = new Span(buffer, (int) length); + stream.ReadExactly(span); + + bool result = ImageInfoFromBytes(span, out width, out height, out sizeInBytes); + + NativeMemory.Free(buffer); + + return result; + } + + /// + /// Get metadata from a compressed image file. + /// + public static bool ImageInfoFromFile( + string path, + out uint width, + out uint height, + out uint sizeInBytes + ) { + FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read); + return ImageInfoFromStream(fileStream, out width, out height, out sizeInBytes); + } + + /// + /// Frees pixel data obtained from GetPixelData methods. + /// + public unsafe static void FreePixelData(byte* pixels) + { + Refresh.Refresh_Image_Free(pixels); + } + + /// + /// Saves pixel data contained in a TransferBuffer to a PNG file. + /// + public static unsafe void SavePNG( + string path, + TransferBuffer transferBuffer, + uint bufferOffsetInBytes, + int width, + int height, + bool bgra + ) { + int sizeInBytes = width * height * 4; + + byte* pixelsPtr = (byte*) NativeMemory.Alloc((nuint) sizeInBytes); + Span pixelsSpan = new Span(pixelsPtr, sizeInBytes); + + transferBuffer.GetData(pixelsSpan, bufferOffsetInBytes); + + if (bgra) + { + // if data is bgra, we have to swap the R and B channels + byte* rgbaPtr = (byte*) NativeMemory.Alloc((nuint) sizeInBytes); + Span rgbaSpan = new Span(rgbaPtr, sizeInBytes); + + for (int i = 0; i < sizeInBytes; i += 4) + { + rgbaSpan[i] = pixelsSpan[i + 2]; + rgbaSpan[i + 1] = pixelsSpan[i + 1]; + rgbaSpan[i + 2] = pixelsSpan[i]; + rgbaSpan[i + 3] = pixelsSpan[i + 3]; + } + + NativeMemory.Free(pixelsPtr); + pixelsPtr = rgbaPtr; + } + + Refresh.Refresh_Image_SavePNG(path, pixelsPtr, width, height); + NativeMemory.Free(pixelsPtr); + } + + // DDS loading extension, based on MojoDDS + // Taken from https://github.com/FNA-XNA/FNA/blob/1e49f868f595f62bc6385db45949a03186a7cd7f/src/Graphics/Texture.cs#L194 + public static void ParseDDS( + BinaryReader reader, + out TextureFormat format, + out int width, + out int height, + out int levels, + out bool isCube + ) { + // A whole bunch of magic numbers, yay DDS! + const uint DDS_MAGIC = 0x20534444; + const uint DDS_HEADERSIZE = 124; + const uint DDS_PIXFMTSIZE = 32; + const uint DDSD_HEIGHT = 0x2; + const uint DDSD_WIDTH = 0x4; + const uint DDSD_PITCH = 0x8; + const uint DDSD_LINEARSIZE = 0x80000; + const uint DDSD_REQ = ( + DDSD_HEIGHT | DDSD_WIDTH + ); + const uint DDSCAPS_MIPMAP = 0x400000; + const uint DDSCAPS_TEXTURE = 0x1000; + const uint DDSCAPS2_CUBEMAP = 0x200; + const uint DDPF_FOURCC = 0x4; + const uint DDPF_RGB = 0x40; + const uint FOURCC_DXT1 = 0x31545844; + const uint FOURCC_DXT3 = 0x33545844; + const uint FOURCC_DXT5 = 0x35545844; + const uint FOURCC_DX10 = 0x30315844; + const uint pitchAndLinear = ( + DDSD_PITCH | DDSD_LINEARSIZE + ); + + // File should start with 'DDS ' + if (reader.ReadUInt32() != DDS_MAGIC) + { + throw new NotSupportedException("Not a DDS!"); + } + + // Texture info + uint size = reader.ReadUInt32(); + if (size != DDS_HEADERSIZE) + { + throw new NotSupportedException("Invalid DDS header!"); + } + uint flags = reader.ReadUInt32(); + if ((flags & DDSD_REQ) != DDSD_REQ) + { + throw new NotSupportedException("Invalid DDS flags!"); + } + if ((flags & pitchAndLinear) == pitchAndLinear) + { + throw new NotSupportedException("Invalid DDS flags!"); + } + height = reader.ReadInt32(); + width = reader.ReadInt32(); + reader.ReadUInt32(); // dwPitchOrLinearSize, unused + reader.ReadUInt32(); // dwDepth, unused + levels = reader.ReadInt32(); + + // "Reserved" + reader.ReadBytes(4 * 11); + + // Format info + uint formatSize = reader.ReadUInt32(); + if (formatSize != DDS_PIXFMTSIZE) + { + throw new NotSupportedException("Bogus PIXFMTSIZE!"); + } + uint formatFlags = reader.ReadUInt32(); + uint formatFourCC = reader.ReadUInt32(); + uint formatRGBBitCount = reader.ReadUInt32(); + uint formatRBitMask = reader.ReadUInt32(); + uint formatGBitMask = reader.ReadUInt32(); + uint formatBBitMask = reader.ReadUInt32(); + uint formatABitMask = reader.ReadUInt32(); + + // dwCaps "stuff" + uint caps = reader.ReadUInt32(); + if ((caps & DDSCAPS_TEXTURE) == 0) + { + throw new NotSupportedException("Not a texture!"); + } + + isCube = false; + + uint caps2 = reader.ReadUInt32(); + if (caps2 != 0) + { + if ((caps2 & DDSCAPS2_CUBEMAP) == DDSCAPS2_CUBEMAP) + { + isCube = true; + } + else + { + throw new NotSupportedException("Invalid caps2!"); + } + } + + reader.ReadUInt32(); // dwCaps3, unused + reader.ReadUInt32(); // dwCaps4, unused + + // "Reserved" + reader.ReadUInt32(); + + // Mipmap sanity check + if ((caps & DDSCAPS_MIPMAP) != DDSCAPS_MIPMAP) + { + levels = 1; + } + + // Determine texture format + if ((formatFlags & DDPF_FOURCC) == DDPF_FOURCC) + { + switch (formatFourCC) + { + case 0x71: // D3DFMT_A16B16G16R16F + format = TextureFormat.R16G16B16A16_SFLOAT; + break; + case 0x74: // D3DFMT_A32B32G32R32F + format = TextureFormat.R32G32B32A32_SFLOAT; + break; + case FOURCC_DXT1: + format = TextureFormat.BC1; + break; + case FOURCC_DXT3: + format = TextureFormat.BC2; + break; + case FOURCC_DXT5: + format = TextureFormat.BC3; + break; + case FOURCC_DX10: + // If the fourCC is DX10, there is an extra header with additional format information. + uint dxgiFormat = reader.ReadUInt32(); + + // These values are taken from the DXGI_FORMAT enum. + switch (dxgiFormat) + { + case 2: + format = TextureFormat.R32G32B32A32_SFLOAT; + break; + + case 10: + format = TextureFormat.R16G16B16A16_SFLOAT; + break; + + case 71: + format = TextureFormat.BC1; + break; + + case 74: + format = TextureFormat.BC2; + break; + + case 77: + format = TextureFormat.BC3; + break; + + case 98: + format = TextureFormat.BC7; + break; + + default: + throw new NotSupportedException( + "Unsupported DDS texture format" + ); + } + + uint resourceDimension = reader.ReadUInt32(); + + // These values are taken from the D3D10_RESOURCE_DIMENSION enum. + switch (resourceDimension) + { + case 0: // Unknown + case 1: // Buffer + throw new NotSupportedException( + "Unsupported DDS texture format" + ); + default: + break; + } + + /* + * This flag seemingly only indicates if the texture is a cube map. + * This is already determined above. Cool! + */ + uint miscFlag = reader.ReadUInt32(); + + /* + * Indicates the number of elements in the texture array. + * We don't support texture arrays so just throw if it's greater than 1. + */ + uint arraySize = reader.ReadUInt32(); + + if (arraySize > 1) + { + throw new NotSupportedException( + "Unsupported DDS texture format" + ); + } + + reader.ReadUInt32(); // reserved + + break; + default: + throw new NotSupportedException( + "Unsupported DDS texture format" + ); + } + } + else if ((formatFlags & DDPF_RGB) == DDPF_RGB) + { + if ( formatRGBBitCount != 32 || + formatRBitMask != 0x00FF0000 || + formatGBitMask != 0x0000FF00 || + formatBBitMask != 0x000000FF || + formatABitMask != 0xFF000000 ) + { + throw new NotSupportedException( + "Unsupported DDS texture format" + ); + } + + format = TextureFormat.B8G8R8A8; + } + else + { + throw new NotSupportedException( + "Unsupported DDS texture format" + ); + } + } + + public static int CalculateDDSLevelSize( + int width, + int height, + TextureFormat format + ) { + if (format == TextureFormat.R8G8B8A8) + { + return (((width * 32) + 7) / 8) * height; + } + else if (format == TextureFormat.R16G16B16A16_SFLOAT) + { + return (((width * 64) + 7) / 8) * height; + } + else if (format == TextureFormat.R32G32B32A32_SFLOAT) + { + return (((width * 128) + 7) / 8) * height; + } + else + { + int blockSize = 16; + if (format == TextureFormat.BC1) + { + blockSize = 8; + } + width = System.Math.Max(width, 1); + height = System.Math.Max(height, 1); + return ( + ((width + 3) / 4) * + ((height + 3) / 4) * + blockSize + ); + } + } +} diff --git a/Nerfed.Runtime/Graphics/PackedVector/Alpha8.cs b/Nerfed.Runtime/Graphics/PackedVector/Alpha8.cs new file mode 100644 index 0000000..7e6e71d --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/Alpha8.cs @@ -0,0 +1,161 @@ +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +using System.Numerics; + +namespace Nerfed.Runtime.Graphics.PackedVector; + +/// +/// Packed vector type containing unsigned normalized values ranging from 0 to 1. +/// The x and z components use 5 bits, and the y component uses 6 bits. +/// +public struct Alpha8 : IPackedVector, IEquatable, IPackedVector +{ + #region Public Properties + + /// + /// Gets and sets the packed value. + /// + public byte PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private byte packedValue; + + #endregion + + #region Public Constructor + + /// + /// Creates a new instance of Alpha8. + /// + /// The alpha component + public Alpha8(float alpha) + { + packedValue = Pack(alpha); + } + + #endregion + + #region Public Methods + + /// + /// Gets the packed vector in float format. + /// + /// The packed vector in Vector3 format + public float ToAlpha() + { + return (float) (packedValue / 255.0f); + } + + #endregion + + #region IPackedVector Methods + + /// + /// Sets the packed vector from a Vector4. + /// + /// Vector containing the components. + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.W); + } + + /// + /// Gets the packed vector in Vector4 format. + /// + /// The packed vector in Vector4 format + Vector4 IPackedVector.ToVector4() + { + return new Vector4( + 0.0f, + 0.0f, + 0.0f, + (float) (packedValue / 255.0f) + ); + } + + #endregion + + #region Public Static Operators and Override Methods + + /// + /// Compares an object with the packed vector. + /// + /// The object to compare. + /// True if the object is equal to the packed vector. + public override bool Equals(object obj) + { + return (obj is Alpha8) && Equals((Alpha8) obj); + } + + /// + /// Compares another Bgra5551 packed vector with the packed vector. + /// + /// The Bgra5551 packed vector to compare. + /// True if the packed vectors are equal. + public bool Equals(Alpha8 other) + { + return packedValue == other.packedValue; + } + + /// + /// Gets a string representation of the packed vector. + /// + /// A string representation of the packed vector. + public override string ToString() + { + return packedValue.ToString("X"); + } + + /// + /// Gets a hash code of the packed vector. + /// + /// The hash code for the packed vector. + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public static bool operator ==(Alpha8 lhs, Alpha8 rhs) + { + return lhs.packedValue == rhs.packedValue; + } + + public static bool operator !=(Alpha8 lhs, Alpha8 rhs) + { + return lhs.packedValue != rhs.packedValue; + } + + #endregion + + #region Private Static Pack Method + + private static byte Pack(float alpha) + { + return (byte) System.Math.Round(Math.Clamp(alpha, 0, 1) * 255.0f); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/Bgr565.cs b/Nerfed.Runtime/Graphics/PackedVector/Bgr565.cs new file mode 100644 index 0000000..1dccc21 --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/Bgr565.cs @@ -0,0 +1,184 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +/// +/// Packed vector type containing unsigned normalized values ranging from 0 to 1. +/// The x and z components use 5 bits, and the y component uses 6 bits. +/// +public struct Bgr565 : IPackedVector, IEquatable, IPackedVector +{ + #region Public Properties + + /// + /// Gets and sets the packed value. + /// + public ushort PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private ushort packedValue; + + #endregion + + #region Public Constructors + + /// + /// Creates a new instance of Bgr565. + /// + /// The x component + /// The y component + /// The z component + public Bgr565(float x, float y, float z) + { + packedValue = Pack(x, y, z); + } + + /// + /// Creates a new instance of Bgr565. + /// + /// + /// Vector containing the components for the packed vector. + /// + public Bgr565(Vector3 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z); + } + + #endregion + + #region Public Methods + + /// + /// Gets the packed vector in Vector3 format. + /// + /// The packed vector in Vector3 format + public Vector3 ToVector3() + { + return new Vector3( + (packedValue >> 11) / 31.0f, + ((packedValue >> 5) & 0x3F) / 63.0f, + (packedValue & 0x1F) / 31.0f + ); + } + + #endregion + + #region IPackedVector Methods + + /// + /// Sets the packed vector from a Vector4. + /// + /// Vector containing the components. + void IPackedVector.PackFromVector4(Vector4 vector) + { + Pack(vector.X, vector.Y, vector.Z); + } + + /// + /// Gets the packed vector in Vector4 format. + /// + /// The packed vector in Vector4 format + Vector4 IPackedVector.ToVector4() + { + return new Vector4(ToVector3(), 1.0f); + } + + #endregion + + #region Public Static Operators and Override Methods + + /// + /// Compares an object with the packed vector. + /// + /// The object to compare. + /// True if the object is equal to the packed vector. + public override bool Equals(object obj) + { + return (obj is Bgr565) && Equals((Bgr565) obj); + } + + /// + /// Compares another Bgr565 packed vector with the packed vector. + /// + /// The Bgr565 packed vector to compare. + /// True if the packed vectors are equal. + public bool Equals(Bgr565 other) + { + return packedValue == other.packedValue; + } + + /// + /// Gets a string representation of the packed vector. + /// + /// A string representation of the packed vector. + public override string ToString() + { + return packedValue.ToString("X"); + } + + /// + /// Gets a hash code of the packed vector. + /// + /// The hash code for the packed vector. + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public static bool operator ==(Bgr565 lhs, Bgr565 rhs) + { + return lhs.packedValue == rhs.packedValue; + } + + public static bool operator !=(Bgr565 lhs, Bgr565 rhs) + { + return lhs.packedValue != rhs.packedValue; + } + + #endregion + + #region Private Static Pack Method + + private static ushort Pack(float x, float y, float z) + { + return (ushort) ( + (((ushort) System.Math.Round(Math.Clamp(x, 0, 1) * 31.0f)) << 11) | + (((ushort) System.Math.Round(Math.Clamp(y, 0, 1) * 63.0f)) << 5) | + ((ushort) System.Math.Round(Math.Clamp(z, 0, 1) * 31.0f)) + ); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/Bgra4444.cs b/Nerfed.Runtime/Graphics/PackedVector/Bgra4444.cs new file mode 100644 index 0000000..8963331 --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/Bgra4444.cs @@ -0,0 +1,178 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +/// +/// Packed vector type containing unsigned normalized values, ranging from 0 to 1, using +/// 4 bits each for x, y, z, and w. +/// +public struct Bgra4444 : IPackedVector, IEquatable +{ + #region Public Properties + + /// + /// Gets and sets the packed value. + /// + public ushort PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private ushort packedValue; + + #endregion + + #region Public Constructors + + /// + /// Creates a new instance of Bgra4444. + /// + /// The x component + /// The y component + /// The z component + /// The w component + public Bgra4444(float x, float y, float z, float w) + { + packedValue = Pack(x, y, z, w); + } + + /// + /// Creates a new instance of Bgra4444. + /// + /// + /// Vector containing the components for the packed vector. + /// + public Bgra4444(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Methods + + /// + /// Gets the packed vector in Vector4 format. + /// + /// The packed vector in Vector4 format + public Vector4 ToVector4() + { + return new Vector4( + ((packedValue >> 8) & 0x0F) / 15.0f, + ((packedValue >> 4) & 0x0F) / 15.0f, + (packedValue & 0x0F) / 15.0f, + (packedValue >> 12) / 15.0f + ); + } + + #endregion + + #region IPackedVector Methods + + /// + /// Sets the packed vector from a Vector4. + /// + /// Vector containing the components. + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Static Operators and Override Methods + + /// + /// Compares an object with the packed vector. + /// + /// The object to compare. + /// True if the object is equal to the packed vector. + public override bool Equals(object obj) + { + return (obj is Bgra4444) && Equals((Bgra4444) obj); + } + + /// + /// Compares another Bgra4444 packed vector with the packed vector. + /// + /// The Bgra4444 packed vector to compare. + /// True if the packed vectors are equal. + public bool Equals(Bgra4444 other) + { + return packedValue == other.packedValue; + } + + /// + /// Gets a string representation of the packed vector. + /// + /// A string representation of the packed vector. + public override string ToString() + { + return packedValue.ToString("X"); + } + + /// + /// Gets a hash code of the packed vector. + /// + /// The hash code for the packed vector. + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public static bool operator ==(Bgra4444 lhs, Bgra4444 rhs) + { + return lhs.packedValue == rhs.packedValue; + } + + public static bool operator !=(Bgra4444 lhs, Bgra4444 rhs) + { + return lhs.packedValue != rhs.packedValue; + } + + #endregion + + #region Private Static Pack Method + + private static ushort Pack(float x, float y, float z, float w) + { + return (ushort) ( + (((ushort) System.Math.Round(Math.Clamp(x, 0, 1) * 15.0f)) << 8) | + (((ushort) System.Math.Round(Math.Clamp(y, 0, 1) * 15.0f)) << 4) | + ((ushort) System.Math.Round(Math.Clamp(z, 0, 1) * 15.0f)) | + (((ushort) System.Math.Round(Math.Clamp(w, 0, 1) * 15.0f)) << 12) + ); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/Bgra5551.cs b/Nerfed.Runtime/Graphics/PackedVector/Bgra5551.cs new file mode 100644 index 0000000..0ee1358 --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/Bgra5551.cs @@ -0,0 +1,178 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +/// +/// Packed vector type containing unsigned normalized values ranging from 0 to 1. +/// The x and z components use 5 bits, and the y component uses 6 bits. +/// +public struct Bgra5551 : IPackedVector, IEquatable, IPackedVector +{ + #region Public Properties + + /// + /// Gets and sets the packed value. + /// + public ushort PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private ushort packedValue; + + #endregion + + #region Public Constructors + + /// + /// Creates a new instance of Bgra5551. + /// + /// The x component + /// The y component + /// The z component + /// The w component + public Bgra5551(float x, float y, float z, float w) + { + packedValue = Pack(x, y, z, w); + } + + /// + /// Creates a new instance of Bgra5551. + /// + /// + /// Vector containing the components for the packed vector. + /// + public Bgra5551(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Methods + + /// + /// Gets the packed vector in Vector4 format. + /// + /// The packed vector in Vector4 format + public Vector4 ToVector4() + { + return new Vector4( + ((packedValue >> 10) & 0x1F) / 31.0f, + ((packedValue >> 5) & 0x1F) / 31.0f, + (packedValue & 0x1F) / 31.0f, + (float) (packedValue >> 15) + ); + } + + #endregion + + #region IPackedVector Methods + + /// + /// Sets the packed vector from a Vector4. + /// + /// Vector containing the components. + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Static Operators and Override Methods + + /// + /// Compares an object with the packed vector. + /// + /// The object to compare. + /// True if the object is equal to the packed vector. + public override bool Equals(object obj) + { + return (obj is Bgra5551) && Equals((Bgra5551) obj); + } + + /// + /// Compares another Bgra5551 packed vector with the packed vector. + /// + /// The Bgra5551 packed vector to compare. + /// True if the packed vectors are equal. + public bool Equals(Bgra5551 other) + { + return packedValue == other.packedValue; + } + + /// + /// Gets a string representation of the packed vector. + /// + /// A string representation of the packed vector. + public override string ToString() + { + return packedValue.ToString("X"); + } + + /// + /// Gets a hash code of the packed vector. + /// + /// The hash code for the packed vector. + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public static bool operator ==(Bgra5551 lhs, Bgra5551 rhs) + { + return lhs.packedValue == rhs.packedValue; + } + + public static bool operator !=(Bgra5551 lhs, Bgra5551 rhs) + { + return lhs.packedValue != rhs.packedValue; + } + + #endregion + + #region Private Static Pack Method + + private static ushort Pack(float x, float y, float z, float w) + { + return (ushort) ( + (((ushort) System.Math.Round(Math.Clamp(x, 0, 1) * 31.0f)) << 10) | + (((ushort) System.Math.Round(Math.Clamp(y, 0, 1) * 31.0f)) << 5) | + ((ushort) System.Math.Round(Math.Clamp(z, 0, 1) * 31.0f)) | + ((ushort) System.Math.Round(Math.Clamp(w, 0, 1)) << 15) + ); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/Byte4.cs b/Nerfed.Runtime/Graphics/PackedVector/Byte4.cs new file mode 100644 index 0000000..3e660a8 --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/Byte4.cs @@ -0,0 +1,204 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; + +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +/// +/// Packed vector type containing four 8-bit unsigned integer values, ranging from 0 to 255. +/// +public struct Byte4 : IPackedVector, IEquatable, IPackedVector +{ + #region Public Properties + + /// + /// Directly gets or sets the packed representation of the value. + /// + /// The packed representation of the value. + public uint PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private uint packedValue; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the Byte4 class. + /// + /// + /// A vector containing the initial values for the components of the Byte4 structure. + /// + public Byte4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + /// + /// Initializes a new instance of the Byte4 class. + /// + /// Initial value for the x component. + /// Initial value for the y component. + /// Initial value for the z component. + /// Initial value for the w component. + public Byte4(float x, float y, float z, float w) + { + packedValue = Pack(x, y, z, w); + } + + #endregion + + #region Public Methods + + /// + /// Expands the packed representation into a Vector4. + /// + /// The expanded vector. + public Vector4 ToVector4() + { + return new Vector4( + (packedValue & 0xFF), + ((packedValue >> 8) & 0xFF), + ((packedValue >> 16) & 0xFF), + (packedValue >> 24) + ); + } + + #endregion + + #region IPackedVector Methods + + /// + /// Sets the packed representation from a Vector4. + /// + /// The vector to create the packed representation from. + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Static Operators and Override Methods + + /// + /// Compares the current instance of a class to another instance to determine + /// whether they are different. + /// + /// The object to the left of the equality operator. + /// The object to the right of the equality operator. + /// True if the objects are different; false otherwise. + public static bool operator !=(Byte4 a, Byte4 b) + { + return a.packedValue != b.packedValue; + } + + /// + /// Compares the current instance of a class to another instance to determine + /// whether they are the same. + /// + /// The object to the left of the equality operator. + /// The object to the right of the equality operator. + /// True if the objects are the same; false otherwise. + public static bool operator ==(Byte4 a, Byte4 b) + { + return a.packedValue == b.packedValue; + } + + /// + /// Returns a value that indicates whether the current instance is equal to a + /// specified object. + /// + /// The object with which to make the comparison. + /// + /// True if the current instance is equal to the specified object; false otherwise. + /// + public override bool Equals(object obj) + { + return (obj is Byte4) && Equals((Byte4) obj); + } + + /// + /// Returns a value that indicates whether the current instance is equal to a + /// specified object. + /// + /// The object with which to make the comparison. + /// + /// True if the current instance is equal to the specified object; false otherwise. + /// + public bool Equals(Byte4 other) + { + return this == other; + } + + /// + /// Gets the hash code for the current instance. + /// + /// Hash code for the instance. + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + /// + /// Returns a string representation of the current instance. + /// + /// String that represents the object. + public override string ToString() + { + return packedValue.ToString("X"); + } + + #endregion + + #region Private Static Pack Method + + /// + /// Packs a vector into a uint. + /// + /// The vector containing the values to pack. + /// The ulong containing the packed values. + static uint Pack(float x, float y, float z, float w) + { + return (uint) ( + ((uint) System.Math.Round(Math.Clamp(x, 0, 255))) | + ((uint) System.Math.Round(Math.Clamp(y, 0, 255)) << 8) | + ((uint) System.Math.Round(Math.Clamp(z, 0, 255)) << 16) | + ((uint) System.Math.Round(Math.Clamp(w, 0, 255)) << 24) + ); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/HalfSingle.cs b/Nerfed.Runtime/Graphics/PackedVector/HalfSingle.cs new file mode 100644 index 0000000..f99cca4 --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/HalfSingle.cs @@ -0,0 +1,113 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +public struct HalfSingle : IPackedVector, IEquatable, IPackedVector +{ + #region Public Properties + + public ushort PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private ushort packedValue; + + #endregion + + #region Public Constructors + + public HalfSingle(float single) + { + packedValue = HalfTypeHelper.Convert(single); + } + + #endregion + + #region Public Methods + + public float ToSingle() + { + return HalfTypeHelper.Convert(packedValue); + } + + #endregion + + #region IPackedVector Methods + + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = HalfTypeHelper.Convert(vector.X); + } + + Vector4 IPackedVector.ToVector4() + { + return new Vector4(ToSingle(), 0.0f, 0.0f, 1.0f); + } + + #endregion + + #region Public Static Operators and Override Methods + + public override bool Equals(object obj) + { + return (obj is HalfSingle) && Equals((HalfSingle) obj); + } + + public bool Equals(HalfSingle other) + { + return packedValue == other.packedValue; + } + + public override string ToString() + { + return packedValue.ToString("X"); + } + + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public static bool operator ==(HalfSingle lhs, HalfSingle rhs) + { + return lhs.packedValue == rhs.packedValue; + } + + public static bool operator !=(HalfSingle lhs, HalfSingle rhs) + { + return lhs.packedValue != rhs.packedValue; + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/HalfTypeHelper.cs b/Nerfed.Runtime/Graphics/PackedVector/HalfTypeHelper.cs new file mode 100644 index 0000000..e2305ad --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/HalfTypeHelper.cs @@ -0,0 +1,138 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Runtime.InteropServices; +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +internal static class HalfTypeHelper +{ + #region Private Struct uif + + [StructLayout(LayoutKind.Explicit)] + private struct uif + { + [FieldOffset(0)] + public float f; + [FieldOffset(0)] + public int i; + [FieldOffset(0)] + public uint u; + } + + #endregion + + #region Internal Static Methods + + internal static ushort Convert(float f) + { + uif uif = new uif(); + uif.f = f; + return Convert(uif.i); + } + + internal static ushort Convert(int i) + { + int s = (i >> 16) & 0x00008000; + int e = ((i >> 23) & 0x000000ff) - (127 - 15); + int m = i & 0x007fffff; + + if (e <= 0) + { + if (e < -10) + { + return (ushort) s; + } + + m = m | 0x00800000; + + int t = 14 - e; + int a = (1 << (t - 1)) - 1; + int b = (m >> t) & 1; + + m = (m + a + b) >> t; + + return (ushort) (s | m); + } + else if (e == 0xff - (127 - 15)) + { + if (m == 0) + { + return (ushort) (s | 0x7c00); + } + else + { + m >>= 13; + return (ushort) (s | 0x7c00 | m | ((m == 0) ? 1 : 0)); + } + } + else + { + m = m + 0x00000fff + ((m >> 13) & 1); + + if ((m & 0x00800000) != 0) + { + m = 0; + e += 1; + } + + if (e > 30) + { + return (ushort) (s | 0x7c00); + } + + return (ushort) (s | (e << 10) | (m >> 13)); + } + } + + internal static float Convert(ushort value) + { + uint rst; + uint mantissa = (uint) (value & 1023); + uint exp = 0xfffffff2; + + if ((value & -33792) == 0) + { + if (mantissa != 0) + { + while ((mantissa & 1024) == 0) + { + exp--; + mantissa = mantissa << 1; + } + mantissa &= 0xfffffbff; + rst = ((uint) ((((uint) value & 0x8000) << 16) | ((exp + 127) << 23))) | (mantissa << 13); + } + else + { + rst = (uint) ((value & 0x8000) << 16); + } + } + else + { + rst = (uint) (((((uint) value & 0x8000) << 16) | ((((((uint) value >> 10) & 0x1f) - 15) + 127) << 23)) | (mantissa << 13)); + } + + uif uif = new uif(); + uif.u = rst; + return uif.f; + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/HalfVector2.cs b/Nerfed.Runtime/Graphics/PackedVector/HalfVector2.cs new file mode 100644 index 0000000..44b9fce --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/HalfVector2.cs @@ -0,0 +1,133 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +public struct HalfVector2 : IPackedVector, IPackedVector, IEquatable +{ + #region Public Properties + + public uint PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private uint packedValue; + + #endregion + + #region Public Constructors + + public HalfVector2(float x, float y) + { + packedValue = PackHelper(x, y); + } + + public HalfVector2(Vector2 vector) + { + packedValue = PackHelper(vector.X, vector.Y); + } + + #endregion + + #region Public Methods + + public Vector2 ToVector2() + { + Vector2 vector; + vector.X = HalfTypeHelper.Convert((ushort) packedValue); + vector.Y = HalfTypeHelper.Convert((ushort) (packedValue >> 0x10)); + return vector; + } + + #endregion + + #region IPackedVector Methods + + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = PackHelper(vector.X, vector.Y); + } + + Vector4 IPackedVector.ToVector4() + { + return new Vector4(ToVector2(), 0.0f, 1.0f); + } + + #endregion + + #region Public Static Operators and Override Methods + + public override string ToString() + { + return packedValue.ToString("X"); + } + + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public override bool Equals(object obj) + { + return ((obj is HalfVector2) && Equals((HalfVector2) obj)); + } + + public bool Equals(HalfVector2 other) + { + return packedValue.Equals(other.packedValue); + } + + public static bool operator ==(HalfVector2 a, HalfVector2 b) + { + return a.Equals(b); + } + + public static bool operator !=(HalfVector2 a, HalfVector2 b) + { + return !a.Equals(b); + } + + #endregion + + #region Private Static Pack Method + + private static uint PackHelper(float vectorX, float vectorY) + { + return (uint) ( + HalfTypeHelper.Convert(vectorX) | + ((uint) (HalfTypeHelper.Convert(vectorY) << 0x10)) + ); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/HalfVector4.cs b/Nerfed.Runtime/Graphics/PackedVector/HalfVector4.cs new file mode 100644 index 0000000..0d9e2cc --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/HalfVector4.cs @@ -0,0 +1,204 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +/// +/// Packed vector type containing four 16-bit floating-point values. +/// +public struct HalfVector4 : IPackedVector, IPackedVector, IEquatable +{ + #region Public Properties + + /// + /// Directly gets or sets the packed representation of the value. + /// + /// The packed representation of the value. + public ulong PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private ulong packedValue; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the HalfVector4 structure. + /// + /// Initial value for the x component. + /// Initial value for the y component. + /// Initial value for the z component. + /// Initial value for the q component. + public HalfVector4(float x, float y, float z, float w) + { + packedValue = Pack(x, y, z, w); + } + + /// + /// Initializes a new instance of the HalfVector4 structure. + /// + /// + /// A vector containing the initial values for the components of the HalfVector4 + /// structure. + /// + public HalfVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Methods + + /// + /// Expands the packed representation into a Vector4. + /// + /// The expanded vector. + public Vector4 ToVector4() + { + return new Vector4( + HalfTypeHelper.Convert((ushort) packedValue), + HalfTypeHelper.Convert((ushort) (packedValue >> 0x10)), + HalfTypeHelper.Convert((ushort) (packedValue >> 0x20)), + HalfTypeHelper.Convert((ushort) (packedValue >> 0x30)) + ); + } + + #endregion + + #region IPackedVector Methods + + /// + /// Sets the packed representation from a Vector4. + /// + /// The vector to create the packed representation from. + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Static Operators and Override Methods + + /// + /// Returns a string representation of the current instance. + /// + /// String that represents the object. + public override string ToString() + { + return packedValue.ToString("X"); + } + + /// + /// Gets the hash code for the current instance. + /// + /// Hash code for the instance. + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + /// + /// Returns a value that indicates whether the current instance is equal to a + /// specified object. + /// + /// The object with which to make the comparison. + /// + /// True if the current instance is equal to the specified object; false otherwise. + /// + public override bool Equals(object obj) + { + return ((obj is HalfVector4) && Equals((HalfVector4) obj)); + } + + /// + /// Returns a value that indicates whether the current instance is equal to a + /// specified object. + /// + /// The object with which to make the comparison. + /// + /// True if the current instance is equal to the specified object; false otherwise. + /// + public bool Equals(HalfVector4 other) + { + return packedValue.Equals(other.packedValue); + } + + /// + /// Compares the current instance of a class to another instance to determine + /// whether they are the same. + /// + /// The object to the left of the equality operator. + /// The object to the right of the equality operator. + /// True if the objects are the same; false otherwise. + public static bool operator ==(HalfVector4 a, HalfVector4 b) + { + return a.Equals(b); + } + + /// + /// Compares the current instance of a class to another instance to determine + /// whether they are different. + /// + /// The object to the left of the equality operator. + /// The object to the right of the equality operator. + /// True if the objects are different; false otherwise. + public static bool operator !=(HalfVector4 a, HalfVector4 b) + { + return !a.Equals(b); + } + + #endregion + + #region Private Static Pack Method + + /// + /// Packs a vector into a ulong. + /// + /// The vector containing the values to pack. + /// The ulong containing the packed values. + private static ulong Pack(float x, float y, float z, float w) + { + return (ulong) ( + ((ulong) HalfTypeHelper.Convert(x)) | + (((ulong) HalfTypeHelper.Convert(y) << 0x10)) | + (((ulong) HalfTypeHelper.Convert(z) << 0x20)) | + (((ulong) HalfTypeHelper.Convert(w) << 0x30)) + ); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/IPackedVector.cs b/Nerfed.Runtime/Graphics/PackedVector/IPackedVector.cs new file mode 100644 index 0000000..27c426a --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/IPackedVector.cs @@ -0,0 +1,38 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +using System.Numerics; + +namespace Nerfed.Runtime.Graphics.PackedVector; + +// http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.graphics.packedvector.ipackedvector.aspx +public interface IPackedVector +{ + void PackFromVector4(Vector4 vector); + + Vector4 ToVector4(); +} + +// PackedVector Generic interface +// http://msdn.microsoft.com/en-us/library/bb197661.aspx +public interface IPackedVector : IPackedVector +{ + TPacked PackedValue + { + get; + set; + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/NormalizedByte2.cs b/Nerfed.Runtime/Graphics/PackedVector/NormalizedByte2.cs new file mode 100644 index 0000000..1dbaad2 --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/NormalizedByte2.cs @@ -0,0 +1,141 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +public struct NormalizedByte2 : IPackedVector, IEquatable +{ + #region Public Properties + + public ushort PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private ushort packedValue; + + #endregion + + #region Public Constructors + + public NormalizedByte2(Vector2 vector) + { + packedValue = Pack(vector.X, vector.Y); + } + + public NormalizedByte2(float x, float y) + { + packedValue = Pack(x, y); + } + + #endregion + + #region Public Methods + + public Vector2 ToVector2() + { + return new Vector2( + ((sbyte) (packedValue & 0xFF)) / 127.0f, + ((sbyte) ((packedValue >> 8) & 0xFF)) / 127.0f + ); + } + + #endregion + + #region IPackedVector Methods + + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y); + } + + Vector4 IPackedVector.ToVector4() + { + return new Vector4(ToVector2(), 0.0f, 1.0f); + } + + #endregion + + #region Public Static Operators and Override Methods + + public static bool operator !=(NormalizedByte2 a, NormalizedByte2 b) + { + return a.packedValue != b.packedValue; + } + + public static bool operator ==(NormalizedByte2 a, NormalizedByte2 b) + { + return a.packedValue == b.packedValue; + } + + public override bool Equals(object obj) + { + return (obj is NormalizedByte2) && Equals((NormalizedByte2) obj); + } + + public bool Equals(NormalizedByte2 other) + { + return packedValue == other.packedValue; + } + + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public override string ToString() + { + return packedValue.ToString("X"); + } + + #endregion + + #region Private Static Pack Method + + private static ushort Pack(float x, float y) + { + int byte2 = ( + ((ushort) + System.Math.Round(Math.Clamp(x, -1.0f, 1.0f) * 127.0f) + ) + ) & 0x00FF; + int byte1 = ( + ((ushort) + System.Math.Round(Math.Clamp(y, -1.0f, 1.0f) * 127.0f) + ) << 8 + ) & 0xFF00; + + return (ushort) (byte2 | byte1); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/NormalizedByte4.cs b/Nerfed.Runtime/Graphics/PackedVector/NormalizedByte4.cs new file mode 100644 index 0000000..a688d58 --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/NormalizedByte4.cs @@ -0,0 +1,146 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +public struct NormalizedByte4 : IPackedVector, IEquatable +{ + #region Public Properties + + public uint PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private uint packedValue; + + #endregion + + #region Public Constructors + + public NormalizedByte4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + public NormalizedByte4(float x, float y, float z, float w) + { + packedValue = Pack(x, y, z, w); + } + + #endregion + + #region Public Methods + + public Vector4 ToVector4() + { + return new Vector4( + ((sbyte) (packedValue & 0xFF)) / 127.0f, + ((sbyte) ((packedValue >> 8) & 0xFF)) / 127.0f, + ((sbyte) ((packedValue >> 16) & 0xFF)) / 127.0f, + ((sbyte) ((packedValue >> 24) & 0xFF)) / 127.0f + ); + } + + #endregion + + #region IPackedVector Methods + + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Static Operators and Override Methods + + public static bool operator !=(NormalizedByte4 a, NormalizedByte4 b) + { + return a.packedValue != b.packedValue; + } + + public static bool operator ==(NormalizedByte4 a, NormalizedByte4 b) + { + return a.packedValue == b.packedValue; + } + + public override bool Equals(object obj) + { + return (obj is NormalizedByte4) && Equals((NormalizedByte4) obj); + } + + public bool Equals(NormalizedByte4 other) + { + return packedValue == other.packedValue; + } + + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public override string ToString() + { + return packedValue.ToString("X"); + } + + #endregion + + #region Private Static Pack Method + + private static uint Pack(float x, float y, float z, float w) + { + uint byte4 = ( + (uint) System.Math.Round(Math.Clamp(x, -1.0f, 1.0f) * 127.0f) + ) & 0x000000FF; + uint byte3 = ( + ( + (uint) System.Math.Round(Math.Clamp(y, -1.0f, 1.0f) * 127.0f) + ) << 8 + ) & 0x0000FF00; + uint byte2 = ( + ( + (uint) System.Math.Round(Math.Clamp(z, -1.0f, 1.0f) * 127.0f) + ) << 16 + ) & 0x00FF0000; + uint byte1 = ( + ( + (uint) System.Math.Round(Math.Clamp(w, -1.0f, 1.0f) * 127.0f) + ) << 24 + ) & 0xFF000000; + + return byte4 | byte3 | byte2 | byte1; + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/NormalizedShort2.cs b/Nerfed.Runtime/Graphics/PackedVector/NormalizedShort2.cs new file mode 100644 index 0000000..b466993 --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/NormalizedShort2.cs @@ -0,0 +1,150 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +public struct NormalizedShort2 : IPackedVector, IEquatable +{ + #region Public Properties + + public uint PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private uint packedValue; + + #endregion + + #region Public Constructors + + public NormalizedShort2(Vector2 vector) + { + packedValue = Pack(vector.X, vector.Y); + } + + public NormalizedShort2(float x, float y) + { + packedValue = Pack(x, y); + } + + #endregion + + #region Public Methods + + public Vector2 ToVector2() + { + const float maxVal = 0x7FFF; + + return new Vector2( + (short) (packedValue & 0xFFFF) / maxVal, + (short) (packedValue >> 0x10) / maxVal + ); + } + + #endregion + + #region IPackedVector Methods + + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y); + } + + Vector4 IPackedVector.ToVector4() + { + return new Vector4(ToVector2(), 0.0f, 1.0f); + } + + #endregion + + #region Public Static Operators and Override Methods + + public static bool operator !=(NormalizedShort2 a, NormalizedShort2 b) + { + return !a.Equals(b); + } + + public static bool operator ==(NormalizedShort2 a, NormalizedShort2 b) + { + return a.Equals(b); + } + + public override bool Equals(object obj) + { + return (obj is NormalizedShort2) && Equals((NormalizedShort2) obj); + } + + public bool Equals(NormalizedShort2 other) + { + return packedValue.Equals(other.packedValue); + } + + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public override string ToString() + { + return packedValue.ToString("X"); + } + + #endregion + + #region Private Static Pack Method + + private static uint Pack(float x, float y) + { + const float max = 0x7FFF; + const float min = -max; + + uint word2 = (uint) ( + (int) Math.Clamp( + (float) System.Math.Round(x * max), + min, + max + ) & 0xFFFF + ); + uint word1 = (uint) (( + (int) Math.Clamp( + (float) System.Math.Round(y * max), + min, + max + ) & 0xFFFF + ) << 0x10); + + return (word2 | word1); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/NormalizedShort4.cs b/Nerfed.Runtime/Graphics/PackedVector/NormalizedShort4.cs new file mode 100644 index 0000000..84c10e9 --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/NormalizedShort4.cs @@ -0,0 +1,161 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +public struct NormalizedShort4 : IPackedVector, IEquatable +{ + #region Public Properties + + public ulong PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private ulong packedValue; + + #endregion + + #region Public Constructors + + public NormalizedShort4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + public NormalizedShort4(float x, float y, float z, float w) + { + packedValue = Pack(x, y, z, w); + } + + #endregion + + #region Public Methods + + public Vector4 ToVector4() + { + const float maxVal = 0x7FFF; + + return new Vector4( + ((short) (packedValue & 0xFFFF)) / maxVal, + ((short) ((packedValue >> 0x10) & 0xFFFF)) / maxVal, + ((short) ((packedValue >> 0x20) & 0xFFFF)) / maxVal, + ((short) ((packedValue >> 0x30) & 0xFFFF)) / maxVal + ); + } + + #endregion + + #region IPackedVector Methods + + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Static Operators and Override Methods + + public static bool operator !=(NormalizedShort4 a, NormalizedShort4 b) + { + return !a.Equals(b); + } + + public static bool operator ==(NormalizedShort4 a, NormalizedShort4 b) + { + return a.Equals(b); + } + + public override bool Equals(object obj) + { + return (obj is NormalizedShort4) && Equals((NormalizedShort4) obj); + } + + public bool Equals(NormalizedShort4 other) + { + return packedValue.Equals(other.packedValue); + } + + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public override string ToString() + { + return packedValue.ToString("X"); + } + + #endregion + + #region Private Static Pack Method + + private static ulong Pack(float x, float y, float z, float w) + { + const float max = 0x7FFF; + const float min = -max; + + ulong word4 = ( + (ulong) Math.Clamp( + (float) System.Math.Round(x * max), + min, + max + ) & 0xFFFF + ); + ulong word3 = ( + (ulong) Math.Clamp( + (float) System.Math.Round(y * max), + min, + max + ) & 0xFFFF + ) << 0x10; + ulong word2 = ( + (ulong) Math.Clamp( + (float) System.Math.Round(z * max), + min, + max + ) & 0xFFFF + ) << 0x20; + ulong word1 = ( + (ulong) Math.Clamp( + (float) System.Math.Round(w * max), + min, + max + ) & 0xFFFF + ) << 0x30; + + return (word4 | word3 | word2 | word1); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/Rg32.cs b/Nerfed.Runtime/Graphics/PackedVector/Rg32.cs new file mode 100644 index 0000000..8271b08 --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/Rg32.cs @@ -0,0 +1,182 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; + +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +/// +/// Packed vector type containing unsigned normalized values ranging from 0 to 1. +/// The x and z components use 5 bits, and the y component uses 6 bits. +/// +public struct Rg32 : IPackedVector, IEquatable, IPackedVector +{ + #region Public Properties + + /// + /// Gets and sets the packed value. + /// + public uint PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private uint packedValue; + + #endregion + + #region Public Constructors + + /// + /// Creates a new instance of Rg32. + /// + /// The x component + /// The y component + public Rg32(float x, float y) + { + packedValue = Pack(x, y); + } + + /// + /// Creates a new instance of Rg32. + /// + /// + /// Vector containing the components for the packed vector. + /// + public Rg32(Vector2 vector) + { + packedValue = Pack(vector.X, vector.Y); + } + + #endregion + + #region Public Methods + + /// + /// Gets the packed vector in Vector2 format. + /// + /// The packed vector in Vector2 format + public Vector2 ToVector2() + { + return new Vector2( + (packedValue & 0xFFFF) / 65535.0f, + (packedValue >> 16) / 65535.0f + ); + } + + #endregion + + #region IPackedVector Methods + + /// + /// Sets the packed vector from a Vector4. + /// + /// Vector containing the components. + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y); + } + + /// + /// Gets the packed vector in Vector4 format. + /// + /// The packed vector in Vector4 format + Vector4 IPackedVector.ToVector4() + { + return new Vector4(ToVector2(), 0.0f, 1.0f); + } + + #endregion + + #region Public Static Operators and Override Methods + + /// + /// Compares an object with the packed vector. + /// + /// The object to compare. + /// True if the object is equal to the packed vector. + public override bool Equals(object obj) + { + return (obj is Rg32) && Equals((Rg32) obj); + } + + /// + /// Compares another Rg32 packed vector with the packed vector. + /// + /// The Rg32 packed vector to compare. + /// True if the packed vectors are equal. + public bool Equals(Rg32 other) + { + return packedValue == other.packedValue; + } + + /// + /// Gets a string representation of the packed vector. + /// + /// A string representation of the packed vector. + public override string ToString() + { + return packedValue.ToString("X"); + } + + /// + /// Gets a hash code of the packed vector. + /// + /// The hash code for the packed vector. + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public static bool operator ==(Rg32 lhs, Rg32 rhs) + { + return lhs.packedValue == rhs.packedValue; + } + + public static bool operator !=(Rg32 lhs, Rg32 rhs) + { + return lhs.packedValue != rhs.packedValue; + } + + #endregion + + #region Private Static Pack Method + + private static uint Pack(float x, float y) + { + return (uint) ( + ((uint) System.Math.Round(Math.Clamp(x, 0, 1) * 65535.0f)) | + (((uint) System.Math.Round(Math.Clamp(y, 0, 1) * 65535.0f)) << 16) + ); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/Rgba1010102.cs b/Nerfed.Runtime/Graphics/PackedVector/Rgba1010102.cs new file mode 100644 index 0000000..eddc344 --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/Rgba1010102.cs @@ -0,0 +1,178 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +/// +/// Packed vector type containing unsigned normalized values ranging from 0 to 1. +/// The x and z components use 5 bits, and the y component uses 6 bits. +/// +public struct Rgba1010102 : IPackedVector, IEquatable, IPackedVector +{ + #region Public Properties + + /// + /// Gets and sets the packed value. + /// + public uint PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private uint packedValue; + + #endregion + + #region Public Constructors + + /// + /// Creates a new instance of Rgba1010102. + /// + /// The x component + /// The y component + /// The z component + /// The w component + public Rgba1010102(float x, float y, float z, float w) + { + packedValue = Pack(x, y, z, w); + } + + /// + /// Creates a new instance of Rgba1010102. + /// + /// + /// Vector containing the components for the packed vector. + /// + public Rgba1010102(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Methods + + /// + /// Gets the packed vector in Vector4 format. + /// + /// The packed vector in Vector4 format + public Vector4 ToVector4() + { + return new Vector4( + (packedValue & 0x03FF) / 1023.0f, + ((packedValue >> 10) & 0x03FF) / 1023.0f, + ((packedValue >> 20) & 0x03FF) / 1023.0f, + (packedValue >> 30) / 3.0f + ); + } + + #endregion + + #region IPackedVector Methods + + /// + /// Sets the packed vector from a Vector4. + /// + /// Vector containing the components. + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Static Operators and Override Methods + + /// + /// Compares an object with the packed vector. + /// + /// The object to compare. + /// True if the object is equal to the packed vector. + public override bool Equals(object obj) + { + return (obj is Rgba1010102) && Equals((Rgba1010102) obj); + } + + /// + /// Compares another Rgba1010102 packed vector with the packed vector. + /// + /// The Rgba1010102 packed vector to compare. + /// True if the packed vectors are equal. + public bool Equals(Rgba1010102 other) + { + return packedValue == other.packedValue; + } + + /// + /// Gets a string representation of the packed vector. + /// + /// A string representation of the packed vector. + public override string ToString() + { + return packedValue.ToString("X"); + } + + /// + /// Gets a hash code of the packed vector. + /// + /// The hash code for the packed vector. + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public static bool operator ==(Rgba1010102 lhs, Rgba1010102 rhs) + { + return lhs.packedValue == rhs.packedValue; + } + + public static bool operator !=(Rgba1010102 lhs, Rgba1010102 rhs) + { + return lhs.packedValue != rhs.packedValue; + } + + #endregion + + #region Private Static Pack Method + + private static uint Pack(float x, float y, float z, float w) + { + return (uint) ( + ((uint) System.Math.Round(Math.Clamp(x, 0, 1) * 1023.0f)) | + ((uint) System.Math.Round(Math.Clamp(y, 0, 1) * 1023.0f) << 10) | + ((uint) System.Math.Round(Math.Clamp(z, 0, 1) * 1023.0f) << 20) | + ((uint) System.Math.Round(Math.Clamp(w, 0, 1) * 3.0f) << 30) + ); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/Rgba64.cs b/Nerfed.Runtime/Graphics/PackedVector/Rgba64.cs new file mode 100644 index 0000000..807a1e1 --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/Rgba64.cs @@ -0,0 +1,178 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +/// +/// Packed vector type containing unsigned normalized values ranging from 0 to 1. +/// The x and z components use 5 bits, and the y component uses 6 bits. +/// +public struct Rgba64 : IPackedVector, IEquatable, IPackedVector +{ + #region Public Properties + + /// + /// Gets and sets the packed value. + /// + public ulong PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private ulong packedValue; + + #endregion + + #region Public Constructors + + /// + /// Creates a new instance of Rgba64. + /// + /// The x component + /// The y component + /// The z component + /// The w component + public Rgba64(float x, float y, float z, float w) + { + packedValue = Pack(x, y, z, w); + } + + /// + /// Creates a new instance of Rgba64. + /// + /// + /// Vector containing the components for the packed vector. + /// + public Rgba64(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Methods + + /// + /// Gets the packed vector in Vector4 format. + /// + /// The packed vector in Vector4 format + public Vector4 ToVector4() + { + return new Vector4( + (packedValue & 0xFFFF) / 65535.0f, + ((packedValue >> 16) & 0xFFFF) / 65535.0f, + ((packedValue >> 32) & 0xFFFF) / 65535.0f, + (packedValue >> 48) / 65535.0f + ); + } + + #endregion + + #region IPackedVector Methods + + /// + /// Sets the packed vector from a Vector4. + /// + /// Vector containing the components. + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Static Operators and Override Methods + + /// + /// Compares an object with the packed vector. + /// + /// The object to compare. + /// True if the object is equal to the packed vector. + public override bool Equals(object obj) + { + return (obj is Rgba64) && Equals((Rgba64) obj); + } + + /// + /// Compares another Rgba64 packed vector with the packed vector. + /// + /// The Rgba64 packed vector to compare. + /// True if the packed vectors are equal. + public bool Equals(Rgba64 other) + { + return packedValue == other.packedValue; + } + + /// + /// Gets a string representation of the packed vector. + /// + /// A string representation of the packed vector. + public override string ToString() + { + return packedValue.ToString("X"); + } + + /// + /// Gets a hash code of the packed vector. + /// + /// The hash code for the packed vector. + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public static bool operator ==(Rgba64 lhs, Rgba64 rhs) + { + return lhs.packedValue == rhs.packedValue; + } + + public static bool operator !=(Rgba64 lhs, Rgba64 rhs) + { + return lhs.packedValue != rhs.packedValue; + } + + #endregion + + #region Private Static Pack Method + + private static ulong Pack(float x, float y, float z, float w) + { + return (ulong) ( + ((ulong) System.Math.Round(Math.Clamp(x, 0, 1) * 65535.0f)) | + (((ulong) System.Math.Round(Math.Clamp(y, 0, 1) * 65535.0f)) << 16) | + (((ulong) System.Math.Round(Math.Clamp(z, 0, 1) * 65535.0f)) << 32) | + (((ulong) System.Math.Round(Math.Clamp(w, 0, 1) * 65535.0f)) << 48) + ); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/Short2.cs b/Nerfed.Runtime/Graphics/PackedVector/Short2.cs new file mode 100644 index 0000000..506780f --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/Short2.cs @@ -0,0 +1,134 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; + +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +public struct Short2 : IPackedVector, IEquatable +{ + #region Public Properties + + public uint PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private uint packedValue; + + #endregion + + #region Public Constructors + + public Short2(Vector2 vector) + { + packedValue = Pack(vector.X, vector.Y); + } + + public Short2(float x, float y) + { + packedValue = Pack(x, y); + } + + #endregion + + #region Public Methods + + public Vector2 ToVector2() + { + return new Vector2( + (short) (packedValue & 0xFFFF), + (short) (packedValue >> 16) + ); + } + + #endregion + + #region IPackedVector Methods + + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y); + } + + Vector4 IPackedVector.ToVector4() + { + return new Vector4(ToVector2(), 0.0f, 1.0f); + } + + #endregion + + #region Public Static Operators and Override Methods + + public static bool operator !=(Short2 a, Short2 b) + { + return a.packedValue != b.packedValue; + } + + public static bool operator ==(Short2 a, Short2 b) + { + return a.packedValue == b.packedValue; + } + + public override bool Equals(object obj) + { + return (obj is Short2) && Equals((Short2) obj); + } + + public bool Equals(Short2 other) + { + return this == other; + } + + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + public override string ToString() + { + return packedValue.ToString("X"); + } + + #endregion + + #region Private Static Pack Method + + private static uint Pack(float x, float y) + { + return (uint) ( + ((int) System.Math.Round(Math.Clamp(x, -32768, 32767)) & 0x0000FFFF) | + (((int) System.Math.Round(Math.Clamp(y, -32768, 32767))) << 16) + ); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/PackedVector/Short4.cs b/Nerfed.Runtime/Graphics/PackedVector/Short4.cs new file mode 100644 index 0000000..c10ff7f --- /dev/null +++ b/Nerfed.Runtime/Graphics/PackedVector/Short4.cs @@ -0,0 +1,204 @@ +#region License + +/* MoonWorks - Game Development Framework + * Copyright 2021 Evan Hemsley + */ + +/* Derived from code by Ethan Lee (Copyright 2009-2021). + * Released under the Microsoft Public License. + * See fna.LICENSE for details. + + * Derived from code by the Mono.Xna Team (Copyright 2006). + * Released under the MIT License. See monoxna.LICENSE for details. + */ + +#endregion + +#region Using Statements +using System; +using System.Numerics; + +#endregion + +namespace Nerfed.Runtime.Graphics.PackedVector; + +/// +/// Packed vector type containing four 16-bit signed integer values. +/// +public struct Short4 : IPackedVector, IEquatable +{ + #region Public Properties + + /// + /// Directly gets or sets the packed representation of the value. + /// + /// The packed representation of the value. + public ulong PackedValue + { + get + { + return packedValue; + } + set + { + packedValue = value; + } + } + + #endregion + + #region Private Variables + + private ulong packedValue; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the Short4 class. + /// + /// + /// A vector containing the initial values for the components of the Short4 structure. + /// + public Short4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + /// + /// Initializes a new instance of the Short4 class. + /// + /// Initial value for the x component. + /// Initial value for the y component. + /// Initial value for the z component. + /// Initial value for the w component. + public Short4(float x, float y, float z, float w) + { + packedValue = Pack(x, y, z, w); + } + + #endregion + + #region Public Methods + + /// + /// Expands the packed representation into a Vector4. + /// + /// The expanded vector. + public Vector4 ToVector4() + { + return new Vector4( + (short) (packedValue & 0xFFFF), + (short) ((packedValue >> 16) & 0xFFFF), + (short) ((packedValue >> 32) & 0xFFFF), + (short) (packedValue >> 48) + ); + } + + #endregion + + #region IPackedVector Methods + + /// + /// Sets the packed representation from a Vector4. + /// + /// The vector to create the packed representation from. + void IPackedVector.PackFromVector4(Vector4 vector) + { + packedValue = Pack(vector.X, vector.Y, vector.Z, vector.W); + } + + #endregion + + #region Public Static Operators and Override Methods + + /// + /// Compares the current instance of a class to another instance to determine + /// whether they are different. + /// + /// The object to the left of the equality operator. + /// The object to the right of the equality operator. + /// True if the objects are different; false otherwise. + public static bool operator !=(Short4 a, Short4 b) + { + return a.PackedValue != b.PackedValue; + } + + /// + /// Compares the current instance of a class to another instance to determine + /// whether they are the same. + /// + /// The object to the left of the equality operator. + /// The object to the right of the equality operator. + /// True if the objects are the same; false otherwise. + public static bool operator ==(Short4 a, Short4 b) + { + return a.PackedValue == b.PackedValue; + } + + /// + /// Returns a value that indicates whether the current instance is equal to a + /// specified object. + /// + /// The object with which to make the comparison. + /// + /// True if the current instance is equal to the specified object; false otherwise. + /// + public override bool Equals(object obj) + { + return (obj is Short4) && Equals((Short4) obj); + } + + /// + /// Returns a value that indicates whether the current instance is equal to a + /// specified object. + /// + /// The object with which to make the comparison. + /// + /// True if the current instance is equal to the specified object; false otherwise. + /// + public bool Equals(Short4 other) + { + return this == other; + } + + /// + /// Gets the hash code for the current instance. + /// + /// Hash code for the instance. + public override int GetHashCode() + { + return packedValue.GetHashCode(); + } + + /// + /// Returns a string representation of the current instance. + /// + /// String that represents the object. + public override string ToString() + { + return packedValue.ToString("X"); + } + + #endregion + + #region Private Static Pack Method + + /// + /// Packs a vector into a ulong. + /// + /// The vector containing the values to pack. + /// The ulong containing the packed values. + static ulong Pack(float x, float y, float z, float w) + { + return (ulong) ( + ((long) System.Math.Round(Math.Clamp(x, -32768, 32767)) & 0xFFFF) | + (((long) System.Math.Round(Math.Clamp(y, -32768, 32767)) << 16) & 0xFFFF0000) | + (((long) System.Math.Round(Math.Clamp(z, -32768, 32767)) << 32) & 0xFFFF00000000) | + ((long) System.Math.Round(Math.Clamp(w, -32768, 32767)) << 48) + ); + } + + #endregion +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/RefreshResource.cs b/Nerfed.Runtime/Graphics/RefreshResource.cs new file mode 100644 index 0000000..1c8e06a --- /dev/null +++ b/Nerfed.Runtime/Graphics/RefreshResource.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading; + +namespace Nerfed.Runtime.Graphics; + +public abstract class RefreshResource : GraphicsResource +{ + public IntPtr Handle { get => handle; internal set => handle = value; } + private IntPtr handle; + + protected abstract Action ReleaseFunction { get; } + + protected RefreshResource(GraphicsDevice device) : base(device) + { + } + + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + // Atomically call release function in case this is called from the finalizer thread + IntPtr toDispose = Interlocked.Exchange(ref handle, IntPtr.Zero); + if (toDispose != IntPtr.Zero) + { + ReleaseFunction(Device.Handle, toDispose); + } + } + base.Dispose(disposing); + } +} diff --git a/Nerfed.Runtime/Graphics/RefreshTypes.cs b/Nerfed.Runtime/Graphics/RefreshTypes.cs new file mode 100644 index 0000000..d95f7cc --- /dev/null +++ b/Nerfed.Runtime/Graphics/RefreshTypes.cs @@ -0,0 +1,1723 @@ +using System; +using System.Runtime.InteropServices; +using RefreshCS; + +namespace Nerfed.Runtime.Graphics; + +// Recreate certain types in here so we can csharp-ify them and hide the Refresh namespace + +public enum PrimitiveType +{ + PointList, + LineList, + LineStrip, + TriangleList, + TriangleStrip +} + +public enum LoadOp +{ + Load, + Clear, + DontCare +} + +public enum StoreOp +{ + Store, + DontCare +} + +public enum IndexElementSize +{ + Sixteen, + ThirtyTwo +} + +public enum TextureFormat +{ + /* Unsigned Normalized Float Color Formats */ + R8G8B8A8, + B8G8R8A8, + R5G6B5, + A1R5G5B5, + B4G4R4A4, + A2R10G10B10, + A2B10G10R10, + R16G16, + R16G16B16A16, + R8, + A8, + /* Compressed Unsigned Normalized Float Color Formats */ + BC1, + BC2, + BC3, + BC7, + /* Signed Normalized Float Color Formats */ + R8G8_SNORM, + R8G8B8A8_SNORM, + /* Signed Float Color Formats */ + R16_SFLOAT, + R16G16_SFLOAT, + R16G16B16A16_SFLOAT, + R32_SFLOAT, + R32G32_SFLOAT, + R32G32B32A32_SFLOAT, + /* Unsigned Integer Color Formats */ + R8_UINT, + R8G8_UINT, + R8G8B8A8_UINT, + R16_UINT, + R16G16_UINT, + R16G16B16A16_UINT, + /* SRGB Color Formats */ + R8G8B8A8_SRGB, + B8G8R8A8_SRGB, + /* Compressed SRGB Color Formats */ + BC3_SRGB, + BC7_SRGB, + /* Depth Formats */ + D16_UNORM, + D24_UNORM, + D32_SFLOAT, + D24_UNORM_S8_UINT, + D32_SFLOAT_S8_UINT +} + +[Flags] +public enum TextureUsageFlags +{ + Sampler = 0x1, + ColorTarget = 0x2, + DepthStencil = 0x4, + GraphicsStorage = 0x8, + ComputeStorageRead = 0x20, + ComputeStorageWrite = 0x40 +} + +public enum TextureType +{ + TwoD, + ThreeD, + Cube +} + +public enum SampleCount +{ + One, + Two, + Four, + Eight +} + +public enum CubeMapFace +{ + PositiveX, + NegativeX, + PositiveY, + NegativeY, + PositiveZ, + NegativeZ +} + +[Flags] +public enum BufferUsageFlags +{ + Vertex = 0x1, + Index = 0x2, + Indirect = 0x4, + GraphicsStorage = 0x8, + ComputeStorageRead = 0x20, + ComputeStorageWrite = 0x40 +} + +public enum TransferBufferUsage +{ + Upload, + Download +} + +public enum ShaderStage +{ + Vertex, + Fragment +} + +public enum ShaderFormat +{ + Invalid, + SPIRV, + HLSL, + DXBC, + DXIL, + MSL, + METALLIB, + SECRET +} + +public enum VertexElementFormat +{ + Uint, + Float, + Vector2, + Vector3, + Vector4, + Color, + Byte4, + Short2, + Short4, + NormalizedShort2, + NormalizedShort4, + HalfVector2, + HalfVector4 +} + +public enum VertexInputRate +{ + Vertex, + Instance +} + +public enum FillMode +{ + Fill, + Line +} + +public enum CullMode +{ + None, + Front, + Back +} + +public enum FrontFace +{ + CounterClockwise, + Clockwise +} + +public enum CompareOp +{ + Never, + Less, + Equal, + LessOrEqual, + Greater, + NotEqual, + GreaterOrEqual, + Always +} + +public enum StencilOp +{ + Keep, + Zero, + Replace, + IncrementAndClamp, + DecrementAndClamp, + Invert, + IncrementAndWrap, + DecrementAndWrap +} + +public enum BlendOp +{ + Add, + Subtract, + ReverseSubtract, + Min, + Max +} + +public enum BlendFactor +{ + Zero, + One, + SourceColor, + OneMinusSourceColor, + DestinationColor, + OneMinusDestinationColor, + SourceAlpha, + OneMinusSourceAlpha, + DestinationAlpha, + OneMinusDestinationAlpha, + ConstantColor, + OneMinusConstantColor, + SourceAlphaSaturate +} + +[Flags] +public enum ColorComponentFlags +{ + None = 0x0, + R = 0x1, + G = 0x2, + B = 0x4, + A = 0x8, + RGB = R | G | B, + RGBA = R | G| B | A +} + +public enum Filter +{ + Nearest, + Linear +} + +public enum SamplerMipmapMode +{ + Nearest, + Linear +} + +public enum SamplerAddressMode +{ + Repeat, + MirroredRepeat, + ClampToEdge +} + +public enum PresentMode +{ + VSync, + Immediate, + Mailbox +} + +public enum SwapchainComposition +{ + SDR, + SDRLinear, + HDRExtendedLinear, + HDR10_ST2084 +} + +[Flags] +public enum BackendFlags +{ + Invalid = 0x0, + Vulkan = 0x1, + D3D11 = 0x2, + Metal = 0x4, + All = Vulkan | D3D11 | Metal +} + +[StructLayout(LayoutKind.Sequential)] +public struct DepthStencilValue +{ + public float Depth; + public uint Stencil; + + public DepthStencilValue(float depth, uint stencil) + { + Depth = depth; + Stencil = stencil; + } + + // FIXME: can we do an unsafe cast somehow? + public Refresh.DepthStencilValue ToRefresh() + { + return new Refresh.DepthStencilValue + { + Depth = Depth, + Stencil = Stencil + }; + } +} + +[StructLayout(LayoutKind.Sequential)] +public struct Rect +{ + public int X; + public int Y; + public int W; + public int H; + + public Rect(int x, int y, int w, int h) + { + X = x; + Y = y; + W = w; + H = h; + } + + public Rect(int w, int h) + { + X = 0; + Y = 0; + W = w; + H = h; + } + + // FIXME: can we do an unsafe cast somehow? + public Refresh.Rect ToRefresh() + { + return new Refresh.Rect + { + X = X, + Y = Y, + W = W, + H = H + }; + } +} + +[StructLayout(LayoutKind.Sequential)] +public struct Viewport +{ + public float X; + public float Y; + public float W; + public float H; + public float MinDepth; + public float MaxDepth; + + public Viewport(float w, float h) + { + X = 0; + Y = 0; + W = w; + H = h; + MinDepth = 0; + MaxDepth = 1; + } + + public Viewport(float x, float y, float w, float h) + { + X = x; + Y = y; + W = w; + H = h; + MinDepth = 0; + MaxDepth = 1; + } + + public Viewport(float x, float y, float w, float h, float minDepth, float maxDepth) + { + X = x; + Y = y; + W = w; + H = h; + MinDepth = minDepth; + MaxDepth = maxDepth; + } + + public Refresh.Viewport ToRefresh() + { + return new Refresh.Viewport + { + X = X, + Y = Y, + W = W, + H = H, + MinDepth = MinDepth, + MaxDepth = MaxDepth + }; + } +} + +[StructLayout(LayoutKind.Sequential)] +public struct VertexBinding +{ + public uint Binding; + public uint Stride; + public VertexInputRate InputRate; + public uint StepRate; + + public static VertexBinding Create( + uint binding = 0, + VertexInputRate inputRate = VertexInputRate.Vertex, + uint stepRate = 1 + ) where T : unmanaged + { + return new VertexBinding + { + Binding = binding, + InputRate = inputRate, + Stride = (uint) Marshal.SizeOf(), + StepRate = stepRate + }; + } + + public Refresh.VertexBinding ToRefresh() + { + return new Refresh.VertexBinding + { + Binding = Binding, + Stride = Stride, + InputRate = (Refresh.VertexInputRate) InputRate, + StepRate = StepRate + }; + } +} + +[StructLayout(LayoutKind.Sequential)] +public struct VertexAttribute +{ + public uint Location; + public uint Binding; + public VertexElementFormat Format; + public uint Offset; + + public Refresh.VertexAttribute ToRefresh() + { + return new Refresh.VertexAttribute + { + Location = Location, + Binding = Binding, + Format = (Refresh.VertexElementFormat) Format, + Offset = Offset + }; + } +} + +[StructLayout(LayoutKind.Sequential)] +public struct StencilOpState +{ + public StencilOp FailOp; + public StencilOp PassOp; + public StencilOp DepthFailOp; + public CompareOp CompareOp; + + public Refresh.StencilOpState ToRefresh() + { + return new Refresh.StencilOpState + { + FailOp = (Refresh.StencilOp) FailOp, + PassOp = (Refresh.StencilOp) PassOp, + DepthFailOp = (Refresh.StencilOp) DepthFailOp, + CompareOp = (Refresh.CompareOp) CompareOp + }; + } +} + +/// +/// Determines how a color texture will be read/written in a render pass. +/// +public struct ColorAttachmentInfo +{ + public TextureSlice TextureSlice; + + /// + /// If LoadOp is set to Clear, the texture slice will be cleared to this color. + /// + public Color ClearColor; + + /// + /// Determines what is done with the texture slice memory + /// at the beginning of the render pass.
+ /// + /// Load: + /// Loads the data currently in the texture slice.
+ /// + /// Clear: + /// Clears the texture slice to a single color.
+ /// + /// DontCare: + /// The driver will do whatever it wants with the texture slice data. + /// This is a good option if you know that every single pixel will be written in the render pass. + ///
+ public LoadOp LoadOp; + + /// + /// Determines what is done with the texture slice memory + /// at the end of the render pass.
+ /// + /// Store: + /// Stores the results of the render pass in the texture slice memory.
+ /// + /// DontCare: + /// The driver will do whatever it wants with the texture slice memory. + ///
+ public StoreOp StoreOp; + + /// + /// If true, cycles the texture if it is bound. + /// + public bool Cycle; + + public ColorAttachmentInfo( + TextureSlice textureSlice, + bool cycle, + Color clearColor, + StoreOp storeOp = StoreOp.Store + ) { + TextureSlice = textureSlice; + ClearColor = clearColor; + LoadOp = LoadOp.Clear; + StoreOp = storeOp; + Cycle = cycle; + } + + public ColorAttachmentInfo( + TextureSlice textureSlice, + bool cycle, + LoadOp loadOp = LoadOp.DontCare, + StoreOp storeOp = StoreOp.Store + ) { + TextureSlice = textureSlice; + ClearColor = Color.White; + LoadOp = loadOp; + StoreOp = storeOp; + Cycle = cycle; + } + + public Refresh.ColorAttachmentInfo ToRefresh() + { + return new Refresh.ColorAttachmentInfo + { + TextureSlice = TextureSlice.ToRefresh(), + ClearColor = new Refresh.Color + { + R = ClearColor.R / 255f, + G = ClearColor.G / 255f, + B = ClearColor.B / 255f, + A = ClearColor.A / 255f + }, + LoadOp = (Refresh.LoadOp) LoadOp, + StoreOp = (Refresh.StoreOp) StoreOp, + Cycle = Conversions.BoolToInt(Cycle) + }; + } +} + +/// +/// Determines how a depth/stencil texture will be read/written in a render pass. +/// +public struct DepthStencilAttachmentInfo +{ + public TextureSlice TextureSlice; + + /// + /// If LoadOp is set to Clear, the texture slice depth will be cleared to this depth value.
+ /// If StencilLoadOp is set to Clear, the texture slice stencil value will be cleared to this stencil value. + ///
+ public DepthStencilValue DepthStencilClearValue; + + /// + /// Determines what is done with the texture slice depth values + /// at the beginning of the render pass.
+ /// + /// Load: + /// Loads the data currently in the texture slice.
+ /// + /// Clear: + /// Clears the texture slice to a single depth value.
+ /// + /// DontCare: + /// The driver will do whatever it wants with the texture slice data. + /// This is a good option if you know that every single pixel will be written in the render pass. + ///
+ public LoadOp LoadOp; + + /// + /// Determines what is done with the texture slice depth values + /// at the end of the render pass.
+ /// + /// Store: + /// Stores the results of the render pass in the texture slice memory.
+ /// + /// DontCare: + /// The driver will do whatever it wants with the texture slice memory. + /// This is usually a good option for depth textures that don't need to be reused. + ///
+ public StoreOp StoreOp; + + /// + /// Determines what is done with the texture slice stencil values + /// at the beginning of the render pass.
+ /// + /// Load: + /// Loads the data currently in the texture slice.
+ /// + /// Clear: + /// Clears the texture slice to a single stencil value.
+ /// + /// DontCare: + /// The driver will do whatever it wants with the texture slice data. + /// This is a good option if you know that every single pixel will be written in the render pass. + ///
+ public LoadOp StencilLoadOp; + + /// + /// Determines what is done with the texture slice stencil values + /// at the end of the render pass.
+ /// + /// Store: + /// Stores the results of the render pass in the texture slice memory.
+ /// + /// DontCare: + /// The driver will do whatever it wants with the texture slice memory. + /// This is usually a good option for stencil textures that don't need to be reused. + ///
+ public StoreOp StencilStoreOp; + + /// + /// If true, cycles the texture if it is bound. + /// + public bool Cycle; + + public DepthStencilAttachmentInfo( + TextureSlice textureSlice, + bool cycle, + DepthStencilValue clearValue, + StoreOp depthStoreOp = StoreOp.DontCare, + StoreOp stencilStoreOp = StoreOp.DontCare + ){ + TextureSlice = textureSlice; + DepthStencilClearValue = clearValue; + LoadOp = LoadOp.Clear; + StoreOp = depthStoreOp; + StencilLoadOp = LoadOp.Clear; + StencilStoreOp = stencilStoreOp; + Cycle = cycle; + } + + public DepthStencilAttachmentInfo( + TextureSlice textureSlice, + bool cycle, + LoadOp loadOp = LoadOp.DontCare, + StoreOp storeOp = StoreOp.DontCare, + LoadOp stencilLoadOp = LoadOp.DontCare, + StoreOp stencilStoreOp = StoreOp.DontCare + ) { + TextureSlice = textureSlice; + DepthStencilClearValue = new DepthStencilValue(); + LoadOp = loadOp; + StoreOp = storeOp; + StencilLoadOp = stencilLoadOp; + StencilStoreOp = stencilStoreOp; + Cycle = cycle; + } + + public DepthStencilAttachmentInfo( + TextureSlice textureSlice, + bool cycle, + DepthStencilValue clearValue, + LoadOp loadOp, + StoreOp storeOp, + LoadOp stencilLoadOp, + StoreOp stencilStoreOp + ) { + TextureSlice = textureSlice; + DepthStencilClearValue = clearValue; + LoadOp = loadOp; + StoreOp = storeOp; + StencilLoadOp = stencilLoadOp; + StencilStoreOp = stencilStoreOp; + Cycle = cycle; + } + + public Refresh.DepthStencilAttachmentInfo ToRefresh() + { + return new Refresh.DepthStencilAttachmentInfo + { + TextureSlice = TextureSlice.ToRefresh(), + DepthStencilClearValue = DepthStencilClearValue.ToRefresh(), + LoadOp = (Refresh.LoadOp) LoadOp, + StoreOp = (Refresh.StoreOp) StoreOp, + StencilLoadOp = (Refresh.LoadOp) StencilLoadOp, + StencilStoreOp = (Refresh.StoreOp) StencilStoreOp, + Cycle = Conversions.BoolToInt(Cycle) + }; + } +} + +/// +/// Defines how color blending will be performed in a GraphicsPipeline. +/// +public struct ColorAttachmentBlendState +{ + /// + /// If disabled, no blending will occur. + /// + public bool BlendEnable; + + /// + /// Selects which blend operation to use with alpha values. + /// + public BlendOp AlphaBlendOp; + /// + /// Selects which blend operation to use with color values. + /// + public BlendOp ColorBlendOp; + + /// + /// Specifies which of the RGBA components are enabled for writing. + /// + public ColorComponentFlags ColorWriteMask; + + /// + /// Selects which blend factor is used to determine the alpha destination factor. + /// + public BlendFactor DestinationAlphaBlendFactor; + + /// + /// Selects which blend factor is used to determine the color destination factor. + /// + public BlendFactor DestinationColorBlendFactor; + + /// + /// Selects which blend factor is used to determine the alpha source factor. + /// + public BlendFactor SourceAlphaBlendFactor; + + /// + /// Selects which blend factor is used to determine the color source factor. + /// + public BlendFactor SourceColorBlendFactor; + + public static readonly ColorAttachmentBlendState Additive = new ColorAttachmentBlendState + { + BlendEnable = true, + AlphaBlendOp = BlendOp.Add, + ColorBlendOp = BlendOp.Add, + ColorWriteMask = ColorComponentFlags.RGBA, + SourceColorBlendFactor = BlendFactor.SourceAlpha, + SourceAlphaBlendFactor = BlendFactor.SourceAlpha, + DestinationColorBlendFactor = BlendFactor.One, + DestinationAlphaBlendFactor = BlendFactor.One + }; + + public static readonly ColorAttachmentBlendState AlphaBlend = new ColorAttachmentBlendState + { + BlendEnable = true, + AlphaBlendOp = BlendOp.Add, + ColorBlendOp = BlendOp.Add, + ColorWriteMask = ColorComponentFlags.RGBA, + SourceColorBlendFactor = BlendFactor.One, + SourceAlphaBlendFactor = BlendFactor.One, + DestinationColorBlendFactor = BlendFactor.OneMinusSourceAlpha, + DestinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha + }; + + public static readonly ColorAttachmentBlendState NonPremultiplied = new ColorAttachmentBlendState + { + BlendEnable = true, + AlphaBlendOp = BlendOp.Add, + ColorBlendOp = BlendOp.Add, + ColorWriteMask = ColorComponentFlags.RGBA, + SourceColorBlendFactor = BlendFactor.SourceAlpha, + SourceAlphaBlendFactor = BlendFactor.SourceAlpha, + DestinationColorBlendFactor = BlendFactor.OneMinusSourceAlpha, + DestinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha + }; + + public static readonly ColorAttachmentBlendState Opaque = new ColorAttachmentBlendState + { + BlendEnable = true, + AlphaBlendOp = BlendOp.Add, + ColorBlendOp = BlendOp.Add, + ColorWriteMask = ColorComponentFlags.RGBA, + SourceColorBlendFactor = BlendFactor.One, + SourceAlphaBlendFactor = BlendFactor.One, + DestinationColorBlendFactor = BlendFactor.Zero, + DestinationAlphaBlendFactor = BlendFactor.Zero + }; + + public static readonly ColorAttachmentBlendState None = new ColorAttachmentBlendState + { + BlendEnable = false, + ColorWriteMask = ColorComponentFlags.RGBA + }; + + public static readonly ColorAttachmentBlendState Disable = new ColorAttachmentBlendState + { + BlendEnable = false, + ColorWriteMask = ColorComponentFlags.None + }; + + public Refresh.ColorAttachmentBlendState ToRefresh() + { + return new Refresh.ColorAttachmentBlendState + { + BlendEnable = Conversions.BoolToInt(BlendEnable), + AlphaBlendOp = (Refresh.BlendOp) AlphaBlendOp, + ColorBlendOp = (Refresh.BlendOp) ColorBlendOp, + ColorWriteMask = (Refresh.ColorComponentFlags) ColorWriteMask, + DestinationAlphaBlendFactor = (Refresh.BlendFactor) DestinationAlphaBlendFactor, + DestinationColorBlendFactor = (Refresh.BlendFactor) DestinationColorBlendFactor, + SourceAlphaBlendFactor = (Refresh.BlendFactor) SourceAlphaBlendFactor, + SourceColorBlendFactor = (Refresh.BlendFactor) SourceColorBlendFactor + }; + } +} + +[StructLayout(LayoutKind.Sequential)] +public struct ColorAttachmentDescription +{ + public TextureFormat Format; + public ColorAttachmentBlendState BlendState; + + public ColorAttachmentDescription( + TextureFormat format, + ColorAttachmentBlendState blendState + ) { + Format = format; + BlendState = blendState; + } +} + +[StructLayout(LayoutKind.Sequential)] +public struct IndirectDrawCommand +{ + public uint VertexCount; + public uint InstanceCount; + public uint FirstVertex; + public uint FirstInstance; + + public IndirectDrawCommand( + uint vertexCount, + uint instanceCount, + uint firstVertex, + uint firstInstance + ) { + VertexCount = vertexCount; + InstanceCount = instanceCount; + FirstVertex = firstVertex; + FirstInstance = firstInstance; + } +} + +/// +/// A buffer location used when transferring data. +/// +public record struct BufferLocation( + Buffer Buffer, + uint Offset +) { + public Refresh.BufferLocation ToRefresh() + { + return new Refresh.BufferLocation + { + Buffer = Buffer.Handle, + Offset = Offset + }; + } +} + +/// +/// A buffer region used when transferring data. +/// +public record struct BufferRegion( + Buffer Buffer, + uint Offset, + uint Size +) { + public Refresh.BufferRegion ToRefresh() + { + return new Refresh.BufferRegion + { + Buffer = Buffer.Handle, + Offset = Offset, + Size = Size + }; + } +} + +/// +/// A buffer-offset pair to be used when binding buffers. +/// +public record struct BufferBinding( + Buffer Buffer, + uint Offset +) { + public Refresh.BufferBinding ToRefresh() + { + return new Refresh.BufferBinding + { + Buffer = Buffer.Handle, + Offset = Offset + }; + } +} + +/// +/// A texture-sampler pair to be used when binding samplers. +/// +public record struct TextureSamplerBinding( + Texture Texture, + Sampler Sampler +) { + public Refresh.TextureSamplerBinding ToRefresh() + { + return new Refresh.TextureSamplerBinding + { + Texture = Texture.Handle, + Sampler = Sampler.Handle + }; + } +} + +public record struct StorageBufferReadWriteBinding( + Buffer Buffer, + bool Cycle +) { + public Refresh.StorageBufferReadWriteBinding ToRefresh() + { + return new Refresh.StorageBufferReadWriteBinding + { + Buffer = Buffer.Handle, + Cycle = Conversions.BoolToInt(Cycle) + }; + } +} + +public record struct StorageTextureReadWriteBinding( + in TextureSlice TextureSlice, + bool Cycle +) { + public Refresh.StorageTextureReadWriteBinding ToRefresh() + { + return new Refresh.StorageTextureReadWriteBinding + { + TextureSlice = TextureSlice.ToRefresh(), + Cycle = Conversions.BoolToInt(Cycle) + }; + } +} + +/// +/// A convenience structure for pairing a vertex binding with its associated attributes. +/// +public struct VertexBindingAndAttributes +{ + public VertexBinding VertexBinding { get; } + public VertexAttribute[] VertexAttributes { get; } + + public VertexBindingAndAttributes(VertexBinding binding, VertexAttribute[] attributes) + { + VertexBinding = binding; + VertexAttributes = attributes; + } + + public static VertexBindingAndAttributes Create(uint bindingIndex, uint locationOffset = 0, VertexInputRate inputRate = VertexInputRate.Vertex) where T : unmanaged, IVertexType + { + VertexBinding binding = VertexBinding.Create(bindingIndex, inputRate); + VertexAttribute[] attributes = new VertexAttribute[T.Formats.Length]; + + for (uint i = 0; i < T.Formats.Length; i += 1) + { + VertexElementFormat format = T.Formats[i]; + uint offset = T.Offsets[i]; + + attributes[i] = new VertexAttribute + { + Binding = bindingIndex, + Location = locationOffset + i, + Format = format, + Offset = offset + }; + } + + return new VertexBindingAndAttributes(binding, attributes); + } +} + +/// +/// Specifies how the vertex shader will interpet vertex data in a buffer. +/// +public struct VertexInputState +{ + public VertexBinding[] VertexBindings; + public VertexAttribute[] VertexAttributes; + + public static readonly VertexInputState Empty = new VertexInputState + { + VertexBindings = System.Array.Empty(), + VertexAttributes = System.Array.Empty() + }; + + public VertexInputState( + VertexBinding vertexBinding, + VertexAttribute[] vertexAttributes + ) { + VertexBindings = new VertexBinding[] { vertexBinding }; + VertexAttributes = vertexAttributes; + } + + public VertexInputState( + VertexBinding[] vertexBindings, + VertexAttribute[] vertexAttributes + ) { + VertexBindings = vertexBindings; + VertexAttributes = vertexAttributes; + } + + public VertexInputState( + VertexBindingAndAttributes bindingAndAttributes + ) { + VertexBindings = new VertexBinding[] { bindingAndAttributes.VertexBinding }; + VertexAttributes = bindingAndAttributes.VertexAttributes; + } + + public VertexInputState( + VertexBindingAndAttributes[] bindingAndAttributesArray + ) { + VertexBindings = new VertexBinding[bindingAndAttributesArray.Length]; + int attributesLength = 0; + + for (int i = 0; i < bindingAndAttributesArray.Length; i += 1) + { + VertexBindings[i] = bindingAndAttributesArray[i].VertexBinding; + attributesLength += bindingAndAttributesArray[i].VertexAttributes.Length; + } + + VertexAttributes = new VertexAttribute[attributesLength]; + + int attributeIndex = 0; + for (int i = 0; i < bindingAndAttributesArray.Length; i += 1) + { + for (int j = 0; j < bindingAndAttributesArray[i].VertexAttributes.Length; j += 1) + { + VertexAttributes[attributeIndex] = bindingAndAttributesArray[i].VertexAttributes[j]; + attributeIndex += 1; + } + } + } + + public static VertexInputState CreateSingleBinding() where T : unmanaged, IVertexType + { + return new VertexInputState(VertexBindingAndAttributes.Create(0)); + } +} + + +/// +/// Specifies how the rasterizer should be configured for a graphics pipeline. +/// +public struct RasterizerState +{ + /// + /// Specifies whether front faces, back faces, none, or both should be culled. + /// + public CullMode CullMode; + + /// + /// Specifies maximum depth bias of a fragment. Only applies if depth biasing is enabled. + /// + public float DepthBiasClamp; + + /// + /// The constant depth value added to each fragment. Only applies if depth biasing is enabled. + /// + public float DepthBiasConstantFactor; + + /// + /// Specifies whether depth biasing is enabled. Only applies if depth biasing is enabled. + /// + public bool DepthBiasEnable; + + /// + /// Factor applied to a fragment's slope in depth bias calculations. Only applies if depth biasing is enabled. + /// + public float DepthBiasSlopeFactor; + + /// + /// Specifies how triangles should be drawn. + /// + public FillMode FillMode; + + /// + /// Specifies which triangle winding order is designated as front-facing. + /// + public FrontFace FrontFace; + + public static readonly RasterizerState CW_CullFront = new RasterizerState + { + CullMode = CullMode.Front, + FrontFace = FrontFace.Clockwise, + FillMode = FillMode.Fill, + DepthBiasEnable = false + }; + + public static readonly RasterizerState CW_CullBack = new RasterizerState + { + CullMode = CullMode.Back, + FrontFace = FrontFace.Clockwise, + FillMode = FillMode.Fill, + DepthBiasEnable = false + }; + + public static readonly RasterizerState CW_CullNone = new RasterizerState + { + CullMode = CullMode.None, + FrontFace = FrontFace.Clockwise, + FillMode = FillMode.Fill, + DepthBiasEnable = false + }; + + public static readonly RasterizerState CW_Wireframe = new RasterizerState + { + CullMode = CullMode.None, + FrontFace = FrontFace.Clockwise, + FillMode = FillMode.Line, + DepthBiasEnable = false + }; + + public static readonly RasterizerState CCW_CullFront = new RasterizerState + { + CullMode = CullMode.Front, + FrontFace = FrontFace.CounterClockwise, + FillMode = FillMode.Fill, + DepthBiasEnable = false + }; + + public static readonly RasterizerState CCW_CullBack = new RasterizerState + { + CullMode = CullMode.Back, + FrontFace = FrontFace.CounterClockwise, + FillMode = FillMode.Fill, + DepthBiasEnable = false + }; + + public static readonly RasterizerState CCW_CullNone = new RasterizerState + { + CullMode = CullMode.None, + FrontFace = FrontFace.CounterClockwise, + FillMode = FillMode.Fill, + DepthBiasEnable = false + }; + + public static readonly RasterizerState CCW_Wireframe = new RasterizerState + { + CullMode = CullMode.None, + FrontFace = FrontFace.CounterClockwise, + FillMode = FillMode.Line, + DepthBiasEnable = false + }; + + public Refresh.RasterizerState ToRefresh() + { + return new Refresh.RasterizerState + { + CullMode = (Refresh.CullMode) CullMode, + DepthBiasClamp = DepthBiasClamp, + DepthBiasConstantFactor = DepthBiasConstantFactor, + DepthBiasEnable = Conversions.BoolToInt(DepthBiasEnable), + DepthBiasSlopeFactor = DepthBiasSlopeFactor, + FillMode = (Refresh.FillMode) FillMode, + FrontFace = (Refresh.FrontFace) FrontFace + }; + } +} + +/// +/// Specifies how many samples should be used in rasterization. +/// +public struct MultisampleState +{ + public SampleCount MultisampleCount; + public uint SampleMask; + + public static readonly MultisampleState None = new MultisampleState + { + MultisampleCount = SampleCount.One, + SampleMask = uint.MaxValue + }; + + public MultisampleState( + SampleCount sampleCount, + uint sampleMask = uint.MaxValue + ) { + MultisampleCount = sampleCount; + SampleMask = sampleMask; + } + + public Refresh.MultisampleState ToRefresh() + { + return new Refresh.MultisampleState + { + MultisampleCount = (Refresh.SampleCount) MultisampleCount, + SampleMask = SampleMask + }; + } +} + +/// +/// Determines how data is written to and read from the depth/stencil buffer. +/// +public struct DepthStencilState +{ + /// + /// If disabled, no depth culling will occur. + /// + public bool DepthTestEnable; + + /// + /// Describes the back-face stencil operation. + /// + public StencilOpState BackStencilState; + + /// + /// Describes the front-face stencil operation. + /// + public StencilOpState FrontStencilState; + + /// + /// The compare mask for stencil ops. + /// + public uint CompareMask; + + /// + /// The write mask for stencil ops. + /// + public uint WriteMask; + + /// + /// The stencil reference value. + /// + public uint Reference; + + /// + /// The comparison operator used in the depth test. + /// + public CompareOp CompareOp; + + /// + /// Specifies whether depth values will be written to the buffer during rendering. + /// + public bool DepthWriteEnable; + + /// + /// If disabled, no stencil culling will occur. + /// + public bool StencilTestEnable; + + public static readonly DepthStencilState DepthReadWrite = new DepthStencilState + { + DepthTestEnable = true, + DepthWriteEnable = true, + StencilTestEnable = false, + CompareOp = CompareOp.LessOrEqual + }; + + public static readonly DepthStencilState DepthRead = new DepthStencilState + { + DepthTestEnable = true, + DepthWriteEnable = false, + StencilTestEnable = false, + CompareOp = CompareOp.LessOrEqual + }; + + public static readonly DepthStencilState Disable = new DepthStencilState + { + DepthTestEnable = false, + DepthWriteEnable = false, + StencilTestEnable = false + }; + + public Refresh.DepthStencilState ToRefresh() + { + return new Refresh.DepthStencilState + { + DepthTestEnable = Conversions.BoolToInt(DepthTestEnable), + BackStencilState = BackStencilState.ToRefresh(), + FrontStencilState = FrontStencilState.ToRefresh(), + CompareMask = CompareMask, + WriteMask = WriteMask, + Reference = Reference, + CompareOp = (Refresh.CompareOp) CompareOp, + DepthWriteEnable = Conversions.BoolToInt(DepthWriteEnable), + StencilTestEnable = Conversions.BoolToInt(StencilTestEnable) + }; + } +} + +/// +/// Describes the kind of attachments that will be used with this pipeline. +/// +public struct GraphicsPipelineAttachmentInfo +{ + public ColorAttachmentDescription[] ColorAttachmentDescriptions; + public bool HasDepthStencilAttachment; + public TextureFormat DepthStencilFormat; + + public GraphicsPipelineAttachmentInfo( + params ColorAttachmentDescription[] colorAttachmentDescriptions + ) { + ColorAttachmentDescriptions = colorAttachmentDescriptions; + HasDepthStencilAttachment = false; + DepthStencilFormat = TextureFormat.D16_UNORM; + } + + public GraphicsPipelineAttachmentInfo( + TextureFormat depthStencilFormat, + params ColorAttachmentDescription[] colorAttachmentDescriptions + ) { + ColorAttachmentDescriptions = colorAttachmentDescriptions; + HasDepthStencilAttachment = true; + DepthStencilFormat = depthStencilFormat; + } +} + +public struct BlendConstants +{ + public float R; + public float G; + public float B; + public float A; +} + +/// +/// All of the information that is used to create a GraphicsPipeline. +/// +public struct GraphicsPipelineCreateInfo +{ + public Shader VertexShader; + public Shader FragmentShader; + public VertexInputState VertexInputState; + public PrimitiveType PrimitiveType; + public RasterizerState RasterizerState; + public MultisampleState MultisampleState; + public DepthStencilState DepthStencilState; + public GraphicsPipelineAttachmentInfo AttachmentInfo; + public BlendConstants BlendConstants; +} + +public struct ComputePipelineCreateInfo +{ + public ShaderFormat ShaderFormat; + public uint ReadOnlyStorageTextureCount; + public uint ReadOnlyStorageBufferCount; + public uint ReadWriteStorageTextureCount; + public uint ReadWriteStorageBufferCount; + public uint UniformBufferCount; + public uint ThreadCountX; + public uint ThreadCountY; + public uint ThreadCountZ; +} + +public struct ShaderCreateInfo +{ + public ShaderStage ShaderStage; + public ShaderFormat ShaderFormat; + public uint SamplerCount; + public uint StorageTextureCount; + public uint StorageBufferCount; + public uint UniformBufferCount; + + public static ShaderCreateInfo None = new ShaderCreateInfo(); +} + +/// +/// All of the information that is used to create a texture. +/// +public struct TextureCreateInfo +{ + public uint Width; + public uint Height; + public uint Depth; + public bool IsCube; + public uint LayerCount; + public uint LevelCount; + public SampleCount SampleCount; + public TextureFormat Format; + public TextureUsageFlags UsageFlags; + + public Refresh.TextureCreateInfo ToRefresh() + { + return new Refresh.TextureCreateInfo + { + Width = Width, + Height = Height, + Depth = Depth, + IsCube = Conversions.BoolToInt(IsCube), + LayerCount = LayerCount, + LevelCount = LevelCount, + SampleCount = (Refresh.SampleCount) SampleCount, + Format = (Refresh.TextureFormat) Format, + UsageFlags = (Refresh.TextureUsageFlags) UsageFlags + }; + } +} + + +/// +/// All of the information that is used to create a sampler. +/// +public struct SamplerCreateInfo +{ + /// + /// Minification filter mode. Used when the image is downscaled. + /// + public Filter MinFilter; + /// + /// Magnification filter mode. Used when the image is upscaled. + /// + public Filter MagFilter; + /// + /// Filter mode applied to mipmap lookups. + /// + public SamplerMipmapMode MipmapMode; + /// + /// Horizontal addressing mode. + /// + public SamplerAddressMode AddressModeU; + /// + /// Vertical addressing mode. + /// + public SamplerAddressMode AddressModeV; + /// + /// Depth addressing mode. + /// + public SamplerAddressMode AddressModeW; + /// + /// Bias value added to mipmap level of detail calculation. + /// + public float MipLodBias; + /// + /// Enables anisotropic filtering. + /// + public bool AnisotropyEnable; + /// + /// Maximum anisotropy value. + /// + public float MaxAnisotropy; + public bool CompareEnable; + public CompareOp CompareOp; + /// + /// Clamps the LOD value to a specified minimum. + /// + public float MinLod; + /// + /// Clamps the LOD value to a specified maximum. + /// + public float MaxLod; + + public static readonly SamplerCreateInfo AnisotropicClamp = new SamplerCreateInfo + { + MinFilter = Filter.Linear, + MagFilter = Filter.Linear, + MipmapMode = SamplerMipmapMode.Linear, + AddressModeU = SamplerAddressMode.ClampToEdge, + AddressModeV = SamplerAddressMode.ClampToEdge, + AddressModeW = SamplerAddressMode.ClampToEdge, + CompareEnable = false, + AnisotropyEnable = true, + MaxAnisotropy = 4, + MipLodBias = 0f, + MinLod = 0, + MaxLod = 1000 /* VK_LOD_CLAMP_NONE */ + }; + + public static readonly SamplerCreateInfo AnisotropicWrap = new SamplerCreateInfo + { + MinFilter = Filter.Linear, + MagFilter = Filter.Linear, + MipmapMode = SamplerMipmapMode.Linear, + AddressModeU = SamplerAddressMode.Repeat, + AddressModeV = SamplerAddressMode.Repeat, + AddressModeW = SamplerAddressMode.Repeat, + CompareEnable = false, + AnisotropyEnable = true, + MaxAnisotropy = 4, + MipLodBias = 0f, + MinLod = 0, + MaxLod = 1000 /* VK_LOD_CLAMP_NONE */ + }; + + public static readonly SamplerCreateInfo LinearClamp = new SamplerCreateInfo + { + MinFilter = Filter.Linear, + MagFilter = Filter.Linear, + MipmapMode = SamplerMipmapMode.Linear, + AddressModeU = SamplerAddressMode.ClampToEdge, + AddressModeV = SamplerAddressMode.ClampToEdge, + AddressModeW = SamplerAddressMode.ClampToEdge, + CompareEnable = false, + AnisotropyEnable = false, + MipLodBias = 0f, + MinLod = 0, + MaxLod = 1000 + }; + + public static readonly SamplerCreateInfo LinearWrap = new SamplerCreateInfo + { + MinFilter = Filter.Linear, + MagFilter = Filter.Linear, + MipmapMode = SamplerMipmapMode.Linear, + AddressModeU = SamplerAddressMode.Repeat, + AddressModeV = SamplerAddressMode.Repeat, + AddressModeW = SamplerAddressMode.Repeat, + CompareEnable = false, + AnisotropyEnable = false, + MipLodBias = 0f, + MinLod = 0, + MaxLod = 1000 + }; + + public static readonly SamplerCreateInfo PointClamp = new SamplerCreateInfo + { + MinFilter = Filter.Nearest, + MagFilter = Filter.Nearest, + MipmapMode = SamplerMipmapMode.Nearest, + AddressModeU = SamplerAddressMode.ClampToEdge, + AddressModeV = SamplerAddressMode.ClampToEdge, + AddressModeW = SamplerAddressMode.ClampToEdge, + CompareEnable = false, + AnisotropyEnable = false, + MipLodBias = 0f, + MinLod = 0, + MaxLod = 1000 + }; + + public static readonly SamplerCreateInfo PointWrap = new SamplerCreateInfo + { + MinFilter = Filter.Nearest, + MagFilter = Filter.Nearest, + MipmapMode = SamplerMipmapMode.Nearest, + AddressModeU = SamplerAddressMode.Repeat, + AddressModeV = SamplerAddressMode.Repeat, + AddressModeW = SamplerAddressMode.Repeat, + CompareEnable = false, + AnisotropyEnable = false, + MipLodBias = 0f, + MinLod = 0, + MaxLod = 1000 + }; + + public Refresh.SamplerCreateInfo ToRefresh() + { + return new Refresh.SamplerCreateInfo + { + MinFilter = (Refresh.Filter) MinFilter, + MagFilter = (Refresh.Filter) MagFilter, + MipmapMode = (Refresh.SamplerMipmapMode) MipmapMode, + AddressModeU = (Refresh.SamplerAddressMode) AddressModeU, + AddressModeV = (Refresh.SamplerAddressMode) AddressModeV, + AddressModeW = (Refresh.SamplerAddressMode) AddressModeW, + MipLodBias = MipLodBias, + AnisotropyEnable = Conversions.BoolToInt(AnisotropyEnable), + MaxAnisotropy = MaxAnisotropy, + CompareEnable = Conversions.BoolToInt(CompareEnable), + CompareOp = (Refresh.CompareOp) CompareOp, + MinLod = MinLod, + MaxLod = MaxLod + }; + } +} + +/// +/// Contains data pertaining to a texture upload or download. +/// +public record struct TextureTransferInfo( + TransferBuffer TransferBuffer, + uint Offset = 0, + uint ImagePitch = 0, + uint ImageHeight = 0 +) { + public Refresh.TextureTransferInfo ToRefresh() + { + return new Refresh.TextureTransferInfo + { + TransferBuffer = TransferBuffer.Handle, + Offset = Offset, + ImagePitch = ImagePitch, + ImageHeight = ImageHeight + }; + } +} + +public record struct TransferBufferLocation( + TransferBuffer TransferBuffer, + uint Offset = 0 +) { + public Refresh.TransferBufferLocation ToRefresh() + { + return new Refresh.TransferBufferLocation + { + TransferBuffer = TransferBuffer.Handle, + Offset = Offset + }; + } +} + +public record struct TransferBufferRegion( + TransferBuffer TransferBuffer, + uint Offset, + uint Size +) { + public Refresh.TransferBufferRegion ToRefresh() + { + return new Refresh.TransferBufferRegion + { + TransferBuffer = TransferBuffer.Handle, + Offset = Offset, + Size = Size + }; + } +} + +/// +/// A texture slice specifies a subresource of a texture. +/// +public record struct TextureSlice( + Texture Texture, + uint MipLevel = 0, + uint Layer = 0 +) { + public uint Size => (Texture.Width * Texture.Height * Texture.Depth * Texture.BytesPerPixel(Texture.Format) / Texture.BlockSizeSquared(Texture.Format)) >> (int) MipLevel; + + public Refresh.TextureSlice ToRefresh() + { + return new Refresh.TextureSlice + { + Texture = Texture.Handle, + MipLevel = MipLevel, + Layer = Layer + }; + } +} + +public record struct TextureLocation +( + TextureSlice TextureSlice, + uint X = 0, + uint Y = 0, + uint Z = 0 +) { + public Refresh.TextureLocation ToRefresh() + { + return new Refresh.TextureLocation + { + TextureSlice = TextureSlice.ToRefresh(), + X = X, + Y = Y, + Z = Z + }; + } +} + +/// +/// A texture region specifies a subregion of a texture. +/// These are used by copy commands. +/// +public record struct TextureRegion( + TextureSlice TextureSlice, + uint X, + uint Y, + uint Z, + uint Width, + uint Height, + uint Depth +) { + public uint Size => (Width * Height * Depth * Texture.BytesPerPixel(TextureSlice.Texture.Format) / Texture.BlockSizeSquared(TextureSlice.Texture.Format)) >> (int) TextureSlice.MipLevel; + + public TextureRegion(Texture texture) : this(texture, 0, 0, 0, texture.Width, texture.Height, texture.Depth) { } + + public Refresh.TextureRegion ToRefresh() + { + return new Refresh.TextureRegion + { + TextureSlice = TextureSlice.ToRefresh(), + X = X, + Y = Y, + Z = Z, + W = Width, + H = Height, + D = Depth + }; + } +} diff --git a/Nerfed.Runtime/Graphics/RenderPass.cs b/Nerfed.Runtime/Graphics/RenderPass.cs new file mode 100644 index 0000000..d020a8b --- /dev/null +++ b/Nerfed.Runtime/Graphics/RenderPass.cs @@ -0,0 +1,495 @@ +using System; +using System.Runtime.InteropServices; +using RefreshCS; + +namespace Nerfed.Runtime.Graphics; + +/// +/// Render passes are begun in command buffers and are used to apply render state and issue draw calls. +/// Render passes are pooled and should not be referenced after calling EndRenderPass. +/// +public class RenderPass +{ + public nint Handle { get; private set; } + +#if DEBUG + internal uint colorAttachmentCount; + internal SampleCount colorAttachmentSampleCount; + internal TextureFormat colorFormatOne; + internal TextureFormat colorFormatTwo; + internal TextureFormat colorFormatThree; + internal TextureFormat colorFormatFour; + internal bool hasDepthStencilAttachment; + internal SampleCount depthStencilAttachmentSampleCount; + internal TextureFormat depthStencilFormat; + + GraphicsPipeline currentGraphicsPipeline; +#endif + + internal void SetHandle(nint handle) + { + Handle = handle; + } + + /// + /// Binds a graphics pipeline so that rendering work may be performed. + /// + /// The graphics pipeline to bind. + public void BindGraphicsPipeline(GraphicsPipeline graphicsPipeline) + { +#if DEBUG + AssertRenderPassPipelineFormatMatch(graphicsPipeline); + + if (colorAttachmentCount > 0) + { + if (graphicsPipeline.SampleCount != colorAttachmentSampleCount) + { + throw new System.InvalidOperationException( + $"Sample count mismatch! Graphics pipeline sample count: {graphicsPipeline.SampleCount}, Color attachment sample count: {colorAttachmentSampleCount}" + ); + } + } + + if (hasDepthStencilAttachment) + { + if (graphicsPipeline.SampleCount != depthStencilAttachmentSampleCount) + { + throw new System.InvalidOperationException( + $"Sample count mismatch! Graphics pipeline sample count: {graphicsPipeline.SampleCount}, Depth stencil attachment sample count: {depthStencilAttachmentSampleCount}" + ); + } + } +#endif + + Refresh.Refresh_BindGraphicsPipeline( + Handle, + graphicsPipeline.Handle + ); + +#if DEBUG + currentGraphicsPipeline = graphicsPipeline; +#endif + } + + /// + /// Sets the viewport. + /// + public void SetViewport(in Viewport viewport) + { + Refresh.Refresh_SetViewport( + Handle, + viewport.ToRefresh() + ); + } + + /// + /// Sets the scissor area. + /// + public void SetScissor(in Rect scissor) + { +#if DEBUG + if (scissor.X < 0 || scissor.Y < 0 || scissor.W <= 0 || scissor.H <= 0) + { + throw new System.ArgumentOutOfRangeException("Scissor position cannot be negative and dimensions must be positive!"); + } +#endif + + Refresh.Refresh_SetScissor( + Handle, + scissor.ToRefresh() + ); + } + + /// + /// Binds vertex buffers to be used by subsequent draw calls. + /// + /// Buffer to bind and associated offset. + /// The index of the first vertex input binding whose state is updated by the command. + public unsafe void BindVertexBuffer( + in BufferBinding bufferBinding, + uint firstBinding = 0 + ) + { +#if DEBUG + AssertGraphicsPipelineBound(); +#endif + + Refresh.BufferBinding refreshBufferBinding = bufferBinding.ToRefresh(); + + Refresh.Refresh_BindVertexBuffers( + Handle, + firstBinding, + &refreshBufferBinding, + 1 + ); + } + + /// + /// Binds an index buffer to be used by subsequent draw calls. + /// + /// The index buffer to bind. + /// The size in bytes of the index buffer elements. + /// The offset index for the buffer. + public void BindIndexBuffer( + BufferBinding bufferBinding, + IndexElementSize indexElementSize + ) + { +#if DEBUG + AssertGraphicsPipelineBound(); +#endif + + Refresh.Refresh_BindIndexBuffer( + Handle, + bufferBinding.ToRefresh(), + (Refresh.IndexElementSize)indexElementSize + ); + } + + /// + /// Binds samplers to be used by the vertex shader. + /// + /// The texture-sampler to bind. + public unsafe void BindVertexSampler( + in TextureSamplerBinding textureSamplerBinding, + uint slot = 0 + ) + { +#if DEBUG + AssertGraphicsPipelineBound(); + AssertTextureSamplerBindingNonNull(textureSamplerBinding); + AssertTextureHasSamplerFlag(textureSamplerBinding.Texture); +#endif + + Refresh.TextureSamplerBinding refreshTextureSamplerBinding = textureSamplerBinding.ToRefresh(); + + Refresh.Refresh_BindVertexSamplers( + Handle, + slot, + &refreshTextureSamplerBinding, + 1 + ); + } + + public unsafe void BindVertexStorageTexture( + in TextureSlice textureSlice, + uint slot = 0 + ) + { +#if DEBUG + AssertGraphicsPipelineBound(); + AssertTextureNonNull(textureSlice.Texture); + AssertTextureHasGraphicsStorageFlag(textureSlice.Texture); +#endif + + Refresh.TextureSlice refreshTextureSlice = textureSlice.ToRefresh(); + + Refresh.Refresh_BindVertexStorageTextures( + Handle, + slot, + &refreshTextureSlice, + 1 + ); + } + + public unsafe void BindVertexStorageBuffer( + Buffer buffer, + uint slot = 0 + ) + { +#if DEBUG + AssertGraphicsPipelineBound(); + AssertBufferNonNull(buffer); + AssertBufferHasGraphicsStorageFlag(buffer); +#endif + + IntPtr bufferHandle = buffer.Handle; + + Refresh.Refresh_BindVertexStorageBuffers( + Handle, + slot, + &bufferHandle, + 1 + ); + } + + public unsafe void BindFragmentSampler( + in TextureSamplerBinding textureSamplerBinding, + uint slot = 0 + ) + { +#if DEBUG + AssertGraphicsPipelineBound(); + AssertTextureSamplerBindingNonNull(textureSamplerBinding); + AssertTextureHasSamplerFlag(textureSamplerBinding.Texture); +#endif + + Refresh.TextureSamplerBinding refreshTextureSamplerBinding = textureSamplerBinding.ToRefresh(); + + Refresh.Refresh_BindFragmentSamplers( + Handle, + slot, + &refreshTextureSamplerBinding, + 1 + ); + } + + public unsafe void BindFragmentStorageTexture( + in TextureSlice textureSlice, + uint slot = 0 + ) + { +#if DEBUG + AssertGraphicsPipelineBound(); + AssertTextureNonNull(textureSlice.Texture); + AssertTextureHasGraphicsStorageFlag(textureSlice.Texture); +#endif + + Refresh.TextureSlice refreshTextureSlice = textureSlice.ToRefresh(); + + Refresh.Refresh_BindFragmentStorageTextures( + Handle, + slot, + &refreshTextureSlice, + 1 + ); + } + + public unsafe void BindFragmentStorageBuffer( + Buffer buffer, + uint slot = 0 + ) + { +#if DEBUG + AssertGraphicsPipelineBound(); + AssertBufferNonNull(buffer); + AssertBufferHasGraphicsStorageFlag(buffer); +#endif + + IntPtr bufferHandle = buffer.Handle; + + Refresh.Refresh_BindFragmentStorageBuffers( + Handle, + slot, + &bufferHandle, + 1 + ); + } + + /// + /// Draws using a vertex buffer and an index buffer, and an optional instance count. + /// + /// The starting index offset for the vertex buffer. + /// The starting index offset for the index buffer. + /// The number of primitives to draw. + /// The number of instances to draw. + public void DrawIndexedPrimitives( + uint baseVertex, + uint startIndex, + uint primitiveCount, + uint instanceCount = 1 + ) + { +#if DEBUG + AssertGraphicsPipelineBound(); +#endif + + Refresh.Refresh_DrawIndexedPrimitives( + Handle, + baseVertex, + startIndex, + primitiveCount, + instanceCount + ); + } + + /// + /// Draws using a vertex buffer and an index buffer. + /// + /// The starting index offset for the vertex buffer. + /// The starting index offset for the index buffer. + /// The number of primitives to draw. + public void DrawPrimitives( + uint vertexStart, + uint primitiveCount + ) + { +#if DEBUG + AssertGraphicsPipelineBound(); +#endif + + Refresh.Refresh_DrawPrimitives( + Handle, + vertexStart, + primitiveCount + ); + } + + /// + /// Similar to DrawPrimitives, but parameters are set from a buffer. + /// The buffer must have the Indirect usage flag set. + /// + /// The draw parameters buffer. + /// The offset to start reading from the draw parameters buffer. + /// The number of draw parameter sets that should be read from the buffer. + /// The byte stride between sets of draw parameters. + public void DrawPrimitivesIndirect( + Buffer buffer, + uint offsetInBytes, + uint drawCount, + uint stride + ) + { +#if DEBUG + AssertGraphicsPipelineBound(); +#endif + + Refresh.Refresh_DrawPrimitivesIndirect( + Handle, + buffer.Handle, + offsetInBytes, + drawCount, + stride + ); + } + + /// + /// Similar to DrawIndexedPrimitives, but parameters are set from a buffer. + /// The buffer must have the Indirect usage flag set. + /// + /// The draw parameters buffer. + /// The offset to start reading from the draw parameters buffer. + /// The number of draw parameter sets that should be read from the buffer. + /// The byte stride between sets of draw parameters. + public void DrawIndexedPrimitivesIndirect( + Buffer buffer, + uint offsetInBytes, + uint drawCount, + uint stride + ) + { +#if DEBUG + AssertGraphicsPipelineBound(); +#endif + + Refresh.Refresh_DrawIndexedPrimitivesIndirect( + Handle, + buffer.Handle, + offsetInBytes, + drawCount, + stride + ); + } + +#if DEBUG + private void AssertRenderPassPipelineFormatMatch(GraphicsPipeline graphicsPipeline) + { + for (int i = 0; i < graphicsPipeline.AttachmentInfo.ColorAttachmentDescriptions.Length; i += 1) + { + TextureFormat format; + if (i == 0) + { + format = colorFormatOne; + } + else if (i == 1) + { + format = colorFormatTwo; + } + else if (i == 2) + { + format = colorFormatThree; + } + else + { + format = colorFormatFour; + } + + TextureFormat pipelineFormat = graphicsPipeline.AttachmentInfo.ColorAttachmentDescriptions[i].Format; + if (pipelineFormat != format) + { + throw new System.InvalidOperationException( + $"Color texture format mismatch! Pipeline expects {pipelineFormat}, render pass attachment is {format}" + ); + } + } + + if (graphicsPipeline.AttachmentInfo.HasDepthStencilAttachment) + { + TextureFormat pipelineDepthFormat = graphicsPipeline.AttachmentInfo.DepthStencilFormat; + + if (!hasDepthStencilAttachment) + { + throw new System.InvalidOperationException("Pipeline expects depth attachment!"); + } + + if (pipelineDepthFormat != depthStencilFormat) + { + throw new System.InvalidOperationException( + $"Depth texture format mismatch! Pipeline expects {pipelineDepthFormat}, render pass attachment is {depthStencilFormat}" + ); + } + } + } + + private void AssertGraphicsPipelineBound(string message = "No graphics pipeline is bound!") + { + if (currentGraphicsPipeline == null) + { + throw new System.InvalidOperationException(message); + } + } + + private void AssertTextureNonNull(in TextureSlice textureSlice) + { + if (textureSlice.Texture == null) + { + throw new NullReferenceException("Texture must not be null!"); + } + } + + private void AssertTextureSamplerBindingNonNull(in TextureSamplerBinding binding) + { + if (binding.Texture == null || binding.Texture.Handle == IntPtr.Zero) + { + throw new NullReferenceException("Texture binding must not be null!"); + } + + if (binding.Sampler == null || binding.Sampler.Handle == IntPtr.Zero) + { + throw new NullReferenceException("Sampler binding must not be null!"); + } + } + + private void AssertTextureHasSamplerFlag(Texture texture) + { + if ((texture.UsageFlags & TextureUsageFlags.Sampler) == 0) + { + throw new System.ArgumentException("The bound Texture's UsageFlags must include TextureUsageFlags.Sampler!"); + } + } + + private void AssertTextureHasGraphicsStorageFlag(Texture texture) + { + if ((texture.UsageFlags & TextureUsageFlags.GraphicsStorage) == 0) + { + throw new System.ArgumentException("The bound Texture's UsageFlags must include TextureUsageFlags.GraphicsStorage!"); + } + } + + private void AssertBufferNonNull(Buffer buffer) + { + if (buffer == null || buffer.Handle == IntPtr.Zero) + { + throw new System.NullReferenceException("Buffer must not be null!"); + } + } + + private void AssertBufferHasGraphicsStorageFlag(Buffer buffer) + { + if ((buffer.UsageFlags & BufferUsageFlags.GraphicsStorage) == 0) + { + throw new System.ArgumentException("The bound Buffer's UsageFlags must include BufferUsageFlag.GraphicsStorage!"); + } + } +#endif +} diff --git a/Nerfed.Runtime/Graphics/RenderPassPool.cs b/Nerfed.Runtime/Graphics/RenderPassPool.cs new file mode 100644 index 0000000..62734a6 --- /dev/null +++ b/Nerfed.Runtime/Graphics/RenderPassPool.cs @@ -0,0 +1,25 @@ +using System.Collections.Concurrent; + +namespace Nerfed.Runtime.Graphics; + +internal class RenderPassPool +{ + private ConcurrentQueue RenderPasses = new ConcurrentQueue(); + + public RenderPass Obtain() + { + if (RenderPasses.TryDequeue(out RenderPass renderPass)) + { + return renderPass; + } + else + { + return new RenderPass(); + } + } + + public void Return(RenderPass renderPass) + { + RenderPasses.Enqueue(renderPass); + } +} diff --git a/Nerfed.Runtime/Graphics/ResourceUploader.cs b/Nerfed.Runtime/Graphics/ResourceUploader.cs new file mode 100644 index 0000000..aece047 --- /dev/null +++ b/Nerfed.Runtime/Graphics/ResourceUploader.cs @@ -0,0 +1,376 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Graphics; + +// FIXME: can we map the transfer buffer instead of maintaining a local pointer + +/// +/// A convenience structure for creating resources and uploading data to them. +/// +/// Note that Upload or UploadAndWait must be called after the Create methods for the data to actually be uploaded. +/// +/// Note that this structure does not magically keep memory usage down - +/// you may want to stagger uploads over multiple submissions to minimize memory usage. +/// +public unsafe class ResourceUploader : GraphicsResource +{ + TransferBuffer BufferTransferBuffer; + TransferBuffer TextureTransferBuffer; + + byte* bufferData; + uint bufferDataOffset = 0; + uint bufferDataSize = 1024; + + byte* textureData; + uint textureDataOffset = 0; + uint textureDataSize = 1024; + + List<(uint, BufferRegion, bool)> BufferUploads = new List<(uint, BufferRegion, bool)>(); + List<(uint, TextureRegion, bool)> TextureUploads = new List<(uint, TextureRegion, bool)>(); + + public ResourceUploader(GraphicsDevice device) : base(device) + { + bufferData = (byte*) NativeMemory.Alloc(bufferDataSize); + textureData = (byte*) NativeMemory.Alloc(textureDataSize); + } + + // Buffers + + /// + /// Creates a Buffer with data to be uploaded. + /// + public Buffer CreateBuffer(Span data, BufferUsageFlags usageFlags) where T : unmanaged + { + uint lengthInBytes = (uint) (Marshal.SizeOf() * data.Length); + Buffer buffer = new Buffer(Device, usageFlags, lengthInBytes); + + SetBufferData(buffer, 0, data, false); + + return buffer; + } + + /// + /// Prepares upload of data into a Buffer. + /// + public void SetBufferData(Buffer buffer, uint bufferOffsetInElements, Span data, bool cycle) where T : unmanaged + { + uint elementSize = (uint) Marshal.SizeOf(); + uint offsetInBytes = elementSize * bufferOffsetInElements; + uint lengthInBytes = (uint) (elementSize * data.Length); + + uint resourceOffset; + fixed (void* spanPtr = data) + { + resourceOffset = CopyBufferData(spanPtr, lengthInBytes); + } + + BufferRegion bufferRegion = new BufferRegion(buffer, offsetInBytes, lengthInBytes); + BufferUploads.Add((resourceOffset, bufferRegion, cycle)); + } + + // Textures + + public Texture CreateTexture2D(Span pixelData, uint width, uint height) where T : unmanaged + { + Texture texture = Texture.CreateTexture2D(Device, width, height, TextureFormat.R8G8B8A8, TextureUsageFlags.Sampler); + SetTextureData(texture, pixelData, false); + return texture; + } + + /// + /// Creates a 2D Texture from compressed image data to be uploaded. + /// + public Texture CreateTexture2DFromCompressed(Span compressedImageData) + { + ImageUtils.ImageInfoFromBytes(compressedImageData, out uint width, out uint height, out uint _); + Texture texture = Texture.CreateTexture2D(Device, width, height, TextureFormat.R8G8B8A8, TextureUsageFlags.Sampler); + SetTextureDataFromCompressed(texture, compressedImageData); + return texture; + } + + /// + /// Creates a 2D Texture from a compressed image stream to be uploaded. + /// + public Texture CreateTexture2DFromCompressed(Stream compressedImageStream) + { + long length = compressedImageStream.Length; + void* buffer = NativeMemory.Alloc((nuint) length); + Span span = new Span(buffer, (int) length); + compressedImageStream.ReadExactly(span); + + Texture texture = CreateTexture2DFromCompressed(span); + + NativeMemory.Free(buffer); + + return texture; + } + + /// + /// Creates a 2D Texture from a compressed image file to be uploaded. + /// + public Texture CreateTexture2DFromCompressed(string compressedImageFilePath) + { + FileStream fileStream = new FileStream(compressedImageFilePath, FileMode.Open, FileAccess.Read); + return CreateTexture2DFromCompressed(fileStream); + } + + /// + /// Creates a texture from a DDS stream. + /// + public Texture CreateTextureFromDDS(Stream stream) + { + using BinaryReader reader = new BinaryReader(stream); + Texture texture; + int faces; + ImageUtils.ParseDDS(reader, out TextureFormat format, out int width, out int height, out int levels, out bool isCube); + + if (isCube) + { + texture = Texture.CreateTextureCube(Device, (uint) width, format, TextureUsageFlags.Sampler, (uint) levels); + faces = 6; + } + else + { + texture = Texture.CreateTexture2D(Device, (uint) width, (uint) height, format, TextureUsageFlags.Sampler, (uint) levels); + faces = 1; + } + + for (int face = 0; face < faces; face += 1) + { + for (int level = 0; level < levels; level += 1) + { + int levelWidth = width >> level; + int levelHeight = height >> level; + + int levelSize = ImageUtils.CalculateDDSLevelSize(levelWidth, levelHeight, format); + void* byteBuffer = NativeMemory.Alloc((nuint) levelSize); + Span byteSpan = new Span(byteBuffer, levelSize); + stream.ReadExactly(byteSpan); + + TextureRegion textureRegion = new TextureRegion + { + TextureSlice = new TextureSlice + { + Texture = texture, + Layer = (uint) face, + MipLevel = (uint) level + }, + X = 0, + Y = 0, + Z = 0, + Width = (uint) levelWidth, + Height = (uint) levelHeight, + Depth = 1 + }; + + SetTextureData(textureRegion, byteSpan, false); + + NativeMemory.Free(byteBuffer); + } + } + + return texture; + } + + /// + /// Creates a texture from a DDS file. + /// + public Texture CreateTextureFromDDS(string path) + { + FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read); + return CreateTextureFromDDS(stream); + } + + public void SetTextureDataFromCompressed(TextureRegion textureRegion, Span compressedImageData) + { + byte* pixelData = ImageUtils.GetPixelDataFromBytes(compressedImageData, out uint _, out uint _, out uint sizeInBytes); + Span pixelSpan = new Span((void*) pixelData, (int) sizeInBytes); + + SetTextureData(textureRegion, pixelSpan, false); + + ImageUtils.FreePixelData(pixelData); + } + + public void SetTextureDataFromCompressed(TextureRegion textureRegion, Stream compressedImageStream) + { + long length = compressedImageStream.Length; + void* buffer = NativeMemory.Alloc((nuint) length); + Span span = new Span(buffer, (int) length); + compressedImageStream.ReadExactly(span); + SetTextureDataFromCompressed(textureRegion, span); + NativeMemory.Free(buffer); + } + + public void SetTextureDataFromCompressed(TextureRegion textureRegion, string compressedImageFilePath) + { + FileStream fileStream = new FileStream(compressedImageFilePath, FileMode.Open, FileAccess.Read); + SetTextureDataFromCompressed(textureRegion, fileStream); + } + + /// + /// Prepares upload of pixel data into a TextureSlice. + /// + public void SetTextureData(TextureRegion textureRegion, Span data, bool cycle) where T : unmanaged + { + int elementSize = Marshal.SizeOf(); + uint dataLengthInBytes = (uint) (elementSize * data.Length); + + uint resourceOffset; + fixed (T* dataPtr = data) + { + resourceOffset = CopyTextureData(dataPtr, dataLengthInBytes, Texture.BytesPerPixel(textureRegion.TextureSlice.Texture.Format)); + } + + TextureUploads.Add((resourceOffset, textureRegion, cycle)); + } + + // Upload + + /// + /// Uploads all the data corresponding to the created resources. + /// + public void Upload() + { + CopyToTransferBuffer(); + + CommandBuffer commandBuffer = Device.AcquireCommandBuffer(); + RecordUploadCommands(commandBuffer); + Device.Submit(commandBuffer); + } + + /// + /// Uploads and then blocks until the upload is finished. + /// This is useful for keeping memory usage down during threaded upload. + /// + public void UploadAndWait() + { + CopyToTransferBuffer(); + + CommandBuffer commandBuffer = Device.AcquireCommandBuffer(); + RecordUploadCommands(commandBuffer); + Fence fence = Device.SubmitAndAcquireFence(commandBuffer); + Device.WaitForFence(fence); + Device.ReleaseFence(fence); + } + + // Helper methods + + private void CopyToTransferBuffer() + { + if (BufferUploads.Count > 0) + { + if (BufferTransferBuffer == null || BufferTransferBuffer.Size < bufferDataSize) + { + BufferTransferBuffer?.Dispose(); + BufferTransferBuffer = new TransferBuffer(Device, TransferBufferUsage.Upload, bufferDataSize); + } + + Span dataSpan = new Span(bufferData, (int) bufferDataSize); + BufferTransferBuffer.SetData(dataSpan, true); + } + + + if (TextureUploads.Count > 0) + { + if (TextureTransferBuffer == null || TextureTransferBuffer.Size < textureDataSize) + { + TextureTransferBuffer?.Dispose(); + TextureTransferBuffer = new TransferBuffer(Device, TransferBufferUsage.Upload, textureDataSize); + } + + Span dataSpan = new Span(textureData, (int) textureDataSize); + TextureTransferBuffer.SetData(dataSpan, true); + } + } + + private void RecordUploadCommands(CommandBuffer commandBuffer) + { + CopyPass copyPass = commandBuffer.BeginCopyPass(); + + foreach ((uint transferOffset, BufferRegion bufferRegion, bool option) in BufferUploads) + { + copyPass.UploadToBuffer( + new TransferBufferLocation(BufferTransferBuffer, transferOffset), + bufferRegion, + option + ); + } + + foreach ((uint transferOffset, TextureRegion textureRegion, bool option) in TextureUploads) + { + copyPass.UploadToTexture( + new TextureTransferInfo(TextureTransferBuffer, transferOffset), + textureRegion, + option + ); + } + + commandBuffer.EndCopyPass(copyPass); + + BufferUploads.Clear(); + TextureUploads.Clear(); + bufferDataOffset = 0; + } + + private uint CopyBufferData(void* ptr, uint lengthInBytes) + { + if (bufferDataOffset + lengthInBytes >= bufferDataSize) + { + bufferDataSize = bufferDataOffset + lengthInBytes; + bufferData = (byte*) NativeMemory.Realloc(bufferData, bufferDataSize); + } + + uint resourceOffset = bufferDataOffset; + + NativeMemory.Copy(ptr, bufferData + bufferDataOffset, lengthInBytes); + bufferDataOffset += lengthInBytes; + + return resourceOffset; + } + + private uint CopyTextureData(void* ptr, uint lengthInBytes, uint alignment) + { + textureDataOffset = RoundToAlignment(textureDataOffset, alignment); + + if (textureDataOffset + lengthInBytes >= textureDataSize) + { + textureDataSize = textureDataOffset + lengthInBytes; + textureData = (byte*) NativeMemory.Realloc(textureData, textureDataSize); + } + + uint resourceOffset = textureDataOffset; + + NativeMemory.Copy(ptr, textureData + textureDataOffset, lengthInBytes); + textureDataOffset += lengthInBytes; + + return resourceOffset; + } + + private uint RoundToAlignment(uint value, uint alignment) + { + return alignment * ((value + alignment - 1) / alignment); + } + + // Dispose + + /// + /// It is valid to immediately call Dispose after calling Upload. + /// + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + BufferTransferBuffer?.Dispose(); + TextureTransferBuffer?.Dispose(); + } + + NativeMemory.Free(bufferData); + } + base.Dispose(disposing); + } +} diff --git a/Nerfed.Runtime/Graphics/Resources/Buffer.cs b/Nerfed.Runtime/Graphics/Resources/Buffer.cs new file mode 100644 index 0000000..2a51adf --- /dev/null +++ b/Nerfed.Runtime/Graphics/Resources/Buffer.cs @@ -0,0 +1,88 @@ +using System; +using System.Runtime.InteropServices; +using RefreshCS; + +namespace Nerfed.Runtime.Graphics; + +/// +/// A data container that can be efficiently used by the GPU. +/// +public class Buffer : RefreshResource +{ + protected override Action ReleaseFunction => Refresh.Refresh_ReleaseBuffer; + + public BufferUsageFlags UsageFlags { get; } + + /// + /// Size in bytes. + /// + public uint Size { get; } + + private string name; + public string Name + { + get => name; + + set + { + if (Device.DebugMode) + { + Refresh.Refresh_SetBufferName( + Device.Handle, + Handle, + value + ); + } + + name = value; + } + } + + /// + /// Creates a buffer of appropriate size given a type and element count. + /// + /// The type that the buffer will contain. + /// The GraphicsDevice. + /// Specifies how the buffer will be used. + /// How many elements of type T the buffer will contain. + /// + public unsafe static Buffer Create( + GraphicsDevice device, + BufferUsageFlags usageFlags, + uint elementCount + ) where T : unmanaged + { + return new Buffer( + device, + usageFlags, + (uint) Marshal.SizeOf() * elementCount + ); + } + + /// + /// Creates a buffer. + /// + /// An initialized GraphicsDevice. + /// Specifies how the buffer will be used. + /// The length of the array. Cannot be resized. + public Buffer( + GraphicsDevice device, + BufferUsageFlags usageFlags, + uint sizeInBytes + ) : base(device) + { + Handle = Refresh.Refresh_CreateBuffer( + device.Handle, + (Refresh.BufferUsageFlags) usageFlags, + sizeInBytes + ); + UsageFlags = usageFlags; + Size = sizeInBytes; + name = ""; + } + + public static implicit operator BufferBinding(Buffer b) + { + return new BufferBinding(b, 0); + } +} diff --git a/Nerfed.Runtime/Graphics/Resources/ComputePipeline.cs b/Nerfed.Runtime/Graphics/Resources/ComputePipeline.cs new file mode 100644 index 0000000..5b2ff74 --- /dev/null +++ b/Nerfed.Runtime/Graphics/Resources/ComputePipeline.cs @@ -0,0 +1,91 @@ +using RefreshCS; +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Graphics; + +/// +/// Compute pipelines perform arbitrary parallel processing on input data. +/// +public class ComputePipeline : RefreshResource +{ + protected override Action ReleaseFunction => Refresh.Refresh_ReleaseComputePipeline; + + public uint ReadOnlyStorageTextureCount { get; } + public uint ReadOnlyStorageBufferCount { get; } + public uint ReadWriteStorageTextureCount { get; } + public uint ReadWriteStorageBufferCount { get; } + public uint UniformBufferCount { get; } + + public ComputePipeline( + GraphicsDevice device, + string filePath, + string entryPointName, + in ComputePipelineCreateInfo computePipelineCreateInfo + ) : base(device) + { + using FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + Handle = CreateFromStream(device, stream, entryPointName, computePipelineCreateInfo); + + ReadOnlyStorageTextureCount = computePipelineCreateInfo.ReadOnlyStorageTextureCount; + ReadOnlyStorageBufferCount = computePipelineCreateInfo.ReadOnlyStorageBufferCount; + ReadWriteStorageTextureCount = computePipelineCreateInfo.ReadWriteStorageTextureCount; + ReadWriteStorageBufferCount = computePipelineCreateInfo.ReadWriteStorageBufferCount; + UniformBufferCount = computePipelineCreateInfo.UniformBufferCount; + } + + public ComputePipeline( + GraphicsDevice device, + Stream stream, + string entryPointName, + in ComputePipelineCreateInfo computePipelineCreateInfo + ) : base(device) + { + Handle = CreateFromStream(device, stream, entryPointName, computePipelineCreateInfo); + + ReadOnlyStorageTextureCount = computePipelineCreateInfo.ReadOnlyStorageTextureCount; + ReadOnlyStorageBufferCount = computePipelineCreateInfo.ReadOnlyStorageBufferCount; + ReadWriteStorageTextureCount = computePipelineCreateInfo.ReadWriteStorageTextureCount; + ReadWriteStorageBufferCount = computePipelineCreateInfo.ReadWriteStorageBufferCount; + UniformBufferCount = computePipelineCreateInfo.UniformBufferCount; + } + + private static unsafe nint CreateFromStream( + GraphicsDevice device, + Stream stream, + string entryPointName, + in ComputePipelineCreateInfo computePipelineCreateInfo + ) { + byte* bytecodeBuffer = (byte*) NativeMemory.Alloc((nuint) stream.Length); + Span bytecodeSpan = new Span(bytecodeBuffer, (int) stream.Length); + stream.ReadExactly(bytecodeSpan); + + Refresh.ComputePipelineCreateInfo refreshPipelineCreateInfo; + refreshPipelineCreateInfo.Code = bytecodeBuffer; + refreshPipelineCreateInfo.CodeSize = (nuint) stream.Length; + refreshPipelineCreateInfo.EntryPointName = entryPointName; + refreshPipelineCreateInfo.Format = (Refresh.ShaderFormat) computePipelineCreateInfo.ShaderFormat; + refreshPipelineCreateInfo.ReadOnlyStorageTextureCount = computePipelineCreateInfo.ReadOnlyStorageTextureCount; + refreshPipelineCreateInfo.ReadOnlyStorageBufferCount = computePipelineCreateInfo.ReadOnlyStorageBufferCount; + refreshPipelineCreateInfo.ReadWriteStorageTextureCount = computePipelineCreateInfo.ReadWriteStorageTextureCount; + refreshPipelineCreateInfo.ReadWriteStorageBufferCount = computePipelineCreateInfo.ReadWriteStorageBufferCount; + refreshPipelineCreateInfo.UniformBufferCount = computePipelineCreateInfo.UniformBufferCount; + refreshPipelineCreateInfo.ThreadCountX = computePipelineCreateInfo.ThreadCountX; + refreshPipelineCreateInfo.ThreadCountY = computePipelineCreateInfo.ThreadCountY; + refreshPipelineCreateInfo.ThreadCountZ = computePipelineCreateInfo.ThreadCountZ; + + IntPtr computePipelineHandle = Refresh.Refresh_CreateComputePipeline( + device.Handle, + refreshPipelineCreateInfo + ); + + if (computePipelineHandle == nint.Zero) + { + throw new Exception("Could not create compute pipeline!"); + } + + NativeMemory.Free(bytecodeBuffer); + return computePipelineHandle; + } +} diff --git a/Nerfed.Runtime/Graphics/Resources/Fence.cs b/Nerfed.Runtime/Graphics/Resources/Fence.cs new file mode 100644 index 0000000..2b30795 --- /dev/null +++ b/Nerfed.Runtime/Graphics/Resources/Fence.cs @@ -0,0 +1,25 @@ +using System; +using RefreshCS; + +namespace Nerfed.Runtime.Graphics; + +/// +/// Fences allow you to track the status of a submitted command buffer.
+/// You should only acquire a Fence if you will need to track the command buffer.
+/// You should make sure to call GraphicsDevice.ReleaseFence when done with a Fence to avoid memory growth.
+/// The Fence object itself is basically just a wrapper for the Refresh_Fence.
+/// The internal handle is replaced so that we can pool Fence objects to manage garbage. +///
+public class Fence : RefreshResource +{ + protected override Action ReleaseFunction => Refresh.Refresh_ReleaseFence; + + internal Fence(GraphicsDevice device) : base(device) + { + } + + internal void SetHandle(nint handle) + { + Handle = handle; + } +} diff --git a/Nerfed.Runtime/Graphics/Resources/GraphicsPipeline.cs b/Nerfed.Runtime/Graphics/Resources/GraphicsPipeline.cs new file mode 100644 index 0000000..d576e52 --- /dev/null +++ b/Nerfed.Runtime/Graphics/Resources/GraphicsPipeline.cs @@ -0,0 +1,99 @@ +using System; +using System.Runtime.InteropServices; +using RefreshCS; + +namespace Nerfed.Runtime.Graphics; + +/// +/// Graphics pipelines encapsulate all of the render state in a single object.
+/// These pipelines are bound before draw calls are issued. +///
+public class GraphicsPipeline : RefreshResource +{ + protected override Action ReleaseFunction => Refresh.Refresh_ReleaseGraphicsPipeline; + + public Shader VertexShader; + public Shader FragmentShader; + public SampleCount SampleCount { get; } + +#if DEBUG + internal GraphicsPipelineAttachmentInfo AttachmentInfo { get; } +#endif + + public unsafe GraphicsPipeline( + GraphicsDevice device, + in GraphicsPipelineCreateInfo graphicsPipelineCreateInfo + ) : base(device) + { + Refresh.GraphicsPipelineCreateInfo refreshGraphicsPipelineCreateInfo; + + Refresh.VertexAttribute* vertexAttributes = (Refresh.VertexAttribute*) NativeMemory.Alloc( + (nuint) (graphicsPipelineCreateInfo.VertexInputState.VertexAttributes.Length * Marshal.SizeOf()) + ); + + for (int i = 0; i < graphicsPipelineCreateInfo.VertexInputState.VertexAttributes.Length; i += 1) + { + vertexAttributes[i] = graphicsPipelineCreateInfo.VertexInputState.VertexAttributes[i].ToRefresh(); + } + + Refresh.VertexBinding* vertexBindings = (Refresh.VertexBinding*) NativeMemory.Alloc( + (nuint) (graphicsPipelineCreateInfo.VertexInputState.VertexBindings.Length * Marshal.SizeOf()) + ); + + for (int i = 0; i < graphicsPipelineCreateInfo.VertexInputState.VertexBindings.Length; i += 1) + { + vertexBindings[i] = graphicsPipelineCreateInfo.VertexInputState.VertexBindings[i].ToRefresh(); + } + + Refresh.ColorAttachmentDescription* colorAttachmentDescriptions = stackalloc Refresh.ColorAttachmentDescription[ + graphicsPipelineCreateInfo.AttachmentInfo.ColorAttachmentDescriptions.Length + ]; + + for (int i = 0; i < graphicsPipelineCreateInfo.AttachmentInfo.ColorAttachmentDescriptions.Length; i += 1) + { + colorAttachmentDescriptions[i].Format = (Refresh.TextureFormat) graphicsPipelineCreateInfo.AttachmentInfo.ColorAttachmentDescriptions[i].Format; + colorAttachmentDescriptions[i].BlendState = graphicsPipelineCreateInfo.AttachmentInfo.ColorAttachmentDescriptions[i].BlendState.ToRefresh(); + } + + refreshGraphicsPipelineCreateInfo.VertexShader = graphicsPipelineCreateInfo.VertexShader.Handle; + refreshGraphicsPipelineCreateInfo.FragmentShader = graphicsPipelineCreateInfo.FragmentShader.Handle; + + refreshGraphicsPipelineCreateInfo.VertexInputState.VertexAttributes = vertexAttributes; + refreshGraphicsPipelineCreateInfo.VertexInputState.VertexAttributeCount = (uint) graphicsPipelineCreateInfo.VertexInputState.VertexAttributes.Length; + refreshGraphicsPipelineCreateInfo.VertexInputState.VertexBindings = vertexBindings; + refreshGraphicsPipelineCreateInfo.VertexInputState.VertexBindingCount = (uint) graphicsPipelineCreateInfo.VertexInputState.VertexBindings.Length; + + refreshGraphicsPipelineCreateInfo.PrimitiveType = (Refresh.PrimitiveType) graphicsPipelineCreateInfo.PrimitiveType; + + refreshGraphicsPipelineCreateInfo.RasterizerState = graphicsPipelineCreateInfo.RasterizerState.ToRefresh(); + refreshGraphicsPipelineCreateInfo.MultisampleState = graphicsPipelineCreateInfo.MultisampleState.ToRefresh(); + refreshGraphicsPipelineCreateInfo.DepthStencilState = graphicsPipelineCreateInfo.DepthStencilState.ToRefresh(); + + refreshGraphicsPipelineCreateInfo.AttachmentInfo.ColorAttachmentCount = (uint) graphicsPipelineCreateInfo.AttachmentInfo.ColorAttachmentDescriptions.Length; + refreshGraphicsPipelineCreateInfo.AttachmentInfo.ColorAttachmentDescriptions = colorAttachmentDescriptions; + refreshGraphicsPipelineCreateInfo.AttachmentInfo.DepthStencilFormat = (Refresh.TextureFormat) graphicsPipelineCreateInfo.AttachmentInfo.DepthStencilFormat; + refreshGraphicsPipelineCreateInfo.AttachmentInfo.HasDepthStencilAttachment = Conversions.BoolToInt(graphicsPipelineCreateInfo.AttachmentInfo.HasDepthStencilAttachment); + + refreshGraphicsPipelineCreateInfo.BlendConstants[0] = graphicsPipelineCreateInfo.BlendConstants.R; + refreshGraphicsPipelineCreateInfo.BlendConstants[1] = graphicsPipelineCreateInfo.BlendConstants.G; + refreshGraphicsPipelineCreateInfo.BlendConstants[2] = graphicsPipelineCreateInfo.BlendConstants.B; + refreshGraphicsPipelineCreateInfo.BlendConstants[3] = graphicsPipelineCreateInfo.BlendConstants.A; + + Handle = Refresh.Refresh_CreateGraphicsPipeline(device.Handle, refreshGraphicsPipelineCreateInfo); + if (Handle == IntPtr.Zero) + { + throw new Exception("Could not create graphics pipeline!"); + } + + NativeMemory.Free(vertexAttributes); + NativeMemory.Free(vertexBindings); + + VertexShader = graphicsPipelineCreateInfo.VertexShader; + FragmentShader = graphicsPipelineCreateInfo.FragmentShader; + SampleCount = graphicsPipelineCreateInfo.MultisampleState.MultisampleCount; + +#if DEBUG + AttachmentInfo = graphicsPipelineCreateInfo.AttachmentInfo; +#endif + } +} diff --git a/Nerfed.Runtime/Graphics/Resources/Sampler.cs b/Nerfed.Runtime/Graphics/Resources/Sampler.cs new file mode 100644 index 0000000..ba3fd73 --- /dev/null +++ b/Nerfed.Runtime/Graphics/Resources/Sampler.cs @@ -0,0 +1,23 @@ +using System; +using RefreshCS; + +namespace Nerfed.Runtime.Graphics; + +/// +/// Specifies how a texture will be sampled in a shader. +/// +public class Sampler : RefreshResource +{ + protected override Action ReleaseFunction => Refresh.Refresh_ReleaseSampler; + + public Sampler( + GraphicsDevice device, + in SamplerCreateInfo samplerCreateInfo + ) : base(device) + { + Handle = Refresh.Refresh_CreateSampler( + device.Handle, + samplerCreateInfo.ToRefresh() + ); + } +} diff --git a/Nerfed.Runtime/Graphics/Resources/Shader.cs b/Nerfed.Runtime/Graphics/Resources/Shader.cs new file mode 100644 index 0000000..0453a6b --- /dev/null +++ b/Nerfed.Runtime/Graphics/Resources/Shader.cs @@ -0,0 +1,91 @@ +using RefreshCS; +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Graphics; + +/// +/// Shaders are used to create graphics pipelines. +/// Graphics pipelines take a vertex shader and a fragment shader. +/// +public class Shader : RefreshResource +{ + protected override Action ReleaseFunction => Refresh.Refresh_ReleaseShader; + + public uint SamplerCount { get; } + public uint StorageTextureCount { get; } + public uint StorageBufferCount { get; } + public uint UniformBufferCount { get; } + + public unsafe Shader( + GraphicsDevice device, + string filePath, + string entryPointName, + in ShaderCreateInfo shaderCreateInfo + ) : base(device) + { + using FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + Handle = CreateFromStream( + device, + stream, + entryPointName, + shaderCreateInfo + ); + + SamplerCount = shaderCreateInfo.SamplerCount; + StorageTextureCount = shaderCreateInfo.StorageTextureCount; + StorageBufferCount = shaderCreateInfo.StorageBufferCount; + UniformBufferCount = shaderCreateInfo.UniformBufferCount; + } + + public unsafe Shader( + GraphicsDevice device, + Stream stream, + string entryPointName, + in ShaderCreateInfo shaderCreateInfo + ) : base(device) + { + Handle = CreateFromStream( + device, + stream, + entryPointName, + shaderCreateInfo + ); + + SamplerCount = shaderCreateInfo.SamplerCount; + StorageTextureCount = shaderCreateInfo.StorageTextureCount; + StorageBufferCount = shaderCreateInfo.StorageBufferCount; + UniformBufferCount = shaderCreateInfo.UniformBufferCount; + } + + private static unsafe IntPtr CreateFromStream( + GraphicsDevice device, + Stream stream, + string entryPointName, + in ShaderCreateInfo shaderCreateInfo + ) { + void* bytecodeBuffer = NativeMemory.Alloc((nuint) stream.Length); + Span bytecodeSpan = new Span(bytecodeBuffer, (int) stream.Length); + stream.ReadExactly(bytecodeSpan); + + Refresh.ShaderCreateInfo refreshShaderCreateInfo; + refreshShaderCreateInfo.CodeSize = (nuint) stream.Length; + refreshShaderCreateInfo.Code = (byte*) bytecodeBuffer; + refreshShaderCreateInfo.EntryPointName = entryPointName; + refreshShaderCreateInfo.Stage = (Refresh.ShaderStage) shaderCreateInfo.ShaderStage; + refreshShaderCreateInfo.Format = (Refresh.ShaderFormat) shaderCreateInfo.ShaderFormat; + refreshShaderCreateInfo.SamplerCount = shaderCreateInfo.SamplerCount; + refreshShaderCreateInfo.StorageTextureCount = shaderCreateInfo.StorageTextureCount; + refreshShaderCreateInfo.StorageBufferCount = shaderCreateInfo.StorageBufferCount; + refreshShaderCreateInfo.UniformBufferCount = shaderCreateInfo.UniformBufferCount; + + IntPtr shaderModule = Refresh.Refresh_CreateShader( + device.Handle, + refreshShaderCreateInfo + ); + + NativeMemory.Free(bytecodeBuffer); + return shaderModule; + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/Resources/Texture.cs b/Nerfed.Runtime/Graphics/Resources/Texture.cs new file mode 100644 index 0000000..a72378b --- /dev/null +++ b/Nerfed.Runtime/Graphics/Resources/Texture.cs @@ -0,0 +1,326 @@ +using System; +using RefreshCS; + +namespace Nerfed.Runtime.Graphics; + +/// +/// A multi-dimensional data container that can be efficiently used by the GPU. +/// +public class Texture : RefreshResource +{ + public uint Width { get; internal set; } + public uint Height { get; internal set; } + public uint Depth { get; } + public TextureFormat Format { get; internal set; } + public bool IsCube { get; } + public uint LayerCount { get; } + public uint LevelCount { get; } + public SampleCount SampleCount { get; } + public TextureUsageFlags UsageFlags { get; } + public uint Size { get; } + + private string name; + public string Name + { + get => name; + + set + { + if (Device.DebugMode) + { + Refresh.Refresh_SetTextureName( + Device.Handle, + Handle, + value + ); + } + + name = value; + } + } + + // FIXME: this allocates a delegate instance + protected override Action ReleaseFunction => Refresh.Refresh_ReleaseTexture; + + /// + /// Creates a 2D texture. + /// + /// An initialized GraphicsDevice. + /// The width of the texture. + /// The height of the texture. + /// The format of the texture. + /// Specifies how the texture will be used. + /// Specifies the number of mip levels. + public static Texture CreateTexture2D( + GraphicsDevice device, + uint width, + uint height, + TextureFormat format, + TextureUsageFlags usageFlags, + uint levelCount = 1, + SampleCount sampleCount = SampleCount.One + ) { + TextureCreateInfo textureCreateInfo = new TextureCreateInfo + { + Width = width, + Height = height, + Depth = 1, + IsCube = false, + LayerCount = 1, + LevelCount = levelCount, + SampleCount = sampleCount, + Format = format, + UsageFlags = usageFlags + }; + + return new Texture(device, textureCreateInfo); + } + + /// + /// Creates a 2D texture array. + /// + /// An initialized GraphicsDevice. + /// The width of the texture. + /// The height of the texture. + /// The layer count of the texture. + /// The format of the texture. + /// Specifies how the texture will be used. + /// Specifies the number of mip levels. + public static Texture CreateTexture2DArray( + GraphicsDevice device, + uint width, + uint height, + uint layerCount, + TextureFormat format, + TextureUsageFlags usageFlags, + uint levelCount = 1 + ) { + TextureCreateInfo textureCreateInfo = new TextureCreateInfo + { + Width = width, + Height = height, + Depth = 1, + IsCube = false, + LayerCount = layerCount, + LevelCount = levelCount, + Format = format, + UsageFlags = usageFlags + }; + + return new Texture(device, textureCreateInfo); + } + + /// + /// Creates a 3D texture. + /// Note that the width, height and depth all form one slice and cannot be subdivided in a texture slice. + /// + public static Texture CreateTexture3D( + GraphicsDevice device, + uint width, + uint height, + uint depth, + TextureFormat format, + TextureUsageFlags usageFlags, + uint levelCount = 1 + ) { + TextureCreateInfo textureCreateInfo = new TextureCreateInfo + { + Width = width, + Height = height, + Depth = depth, + IsCube = false, + LayerCount = 1, + LevelCount = levelCount, + Format = format, + UsageFlags = usageFlags + }; + + return new Texture(device, textureCreateInfo); + } + + /// + /// Creates a cube texture. + /// + /// An initialized GraphicsDevice. + /// The length of one side of the cube. + /// The format of the texture. + /// Specifies how the texture will be used. + /// Specifies the number of mip levels. + public static Texture CreateTextureCube( + GraphicsDevice device, + uint size, + TextureFormat format, + TextureUsageFlags usageFlags, + uint levelCount = 1 + ) { + TextureCreateInfo textureCreateInfo = new TextureCreateInfo + { + Width = size, + Height = size, + Depth = 1, + IsCube = true, + LayerCount = 6, + LevelCount = levelCount, + Format = format, + UsageFlags = usageFlags + }; + + return new Texture(device, textureCreateInfo); + } + + /// + /// Creates a new texture using a TextureCreateInfo struct. + /// + /// An initialized GraphicsDevice. + /// The parameters to use when creating the texture. + public Texture( + GraphicsDevice device, + in TextureCreateInfo textureCreateInfo + ) : base(device) + { + Handle = Refresh.Refresh_CreateTexture( + device.Handle, + textureCreateInfo.ToRefresh() + ); + + Format = textureCreateInfo.Format; + Width = textureCreateInfo.Width; + Height = textureCreateInfo.Height; + Depth = textureCreateInfo.Depth; + IsCube = textureCreateInfo.IsCube; + LayerCount = textureCreateInfo.LayerCount; + LevelCount = textureCreateInfo.LevelCount; + SampleCount = textureCreateInfo.SampleCount; + UsageFlags = textureCreateInfo.UsageFlags; + Size = Width * Height * BytesPerPixel(Format) / BlockSizeSquared(Format); + name = ""; + } + + // Used by Window. Swapchain texture handles are managed by the driver backend. + internal Texture( + GraphicsDevice device, + TextureFormat format + ) : base(device) + { + Handle = IntPtr.Zero; + + Format = format; + Width = 0; + Height = 0; + Depth = 1; + IsCube = false; + LevelCount = 1; + SampleCount = SampleCount.One; + UsageFlags = TextureUsageFlags.ColorTarget; + Size = Width * Height * BytesPerPixel(Format) / BlockSizeSquared(Format); + } + + public static uint BytesPerPixel(TextureFormat format) + { + switch (format) + { + case TextureFormat.R8: + case TextureFormat.R8_UINT: + return 1; + case TextureFormat.R5G6B5: + case TextureFormat.B4G4R4A4: + case TextureFormat.A1R5G5B5: + case TextureFormat.R16_SFLOAT: + case TextureFormat.R8G8_SNORM: + case TextureFormat.R8G8_UINT: + case TextureFormat.R16_UINT: + case TextureFormat.D16_UNORM: + return 2; + case TextureFormat.R8G8B8A8: + case TextureFormat.B8G8R8A8: + case TextureFormat.R32_SFLOAT: + case TextureFormat.R16G16: + case TextureFormat.R16G16_SFLOAT: + case TextureFormat.R8G8B8A8_SNORM: + case TextureFormat.A2R10G10B10: + case TextureFormat.R8G8B8A8_UINT: + case TextureFormat.R16G16_UINT: + case TextureFormat.D24_UNORM_S8_UINT: + case TextureFormat.D32_SFLOAT: + return 4; + case TextureFormat.D32_SFLOAT_S8_UINT: + return 5; + case TextureFormat.R16G16B16A16_SFLOAT: + case TextureFormat.R16G16B16A16: + case TextureFormat.R32G32_SFLOAT: + case TextureFormat.R16G16B16A16_UINT: + case TextureFormat.BC1: + return 8; + case TextureFormat.R32G32B32A32_SFLOAT: + case TextureFormat.BC2: + case TextureFormat.BC3: + case TextureFormat.BC7: + return 16; + default: + Log.Error("Texture format not recognized!"); + return 0; + } + } + + public static uint TexelSize(TextureFormat format) + { + switch (format) + { + case TextureFormat.BC2: + case TextureFormat.BC3: + case TextureFormat.BC7: + return 16; + case TextureFormat.BC1: + return 8; + default: + return 1; + } + } + + public static uint BlockSizeSquared(TextureFormat format) + { + switch (format) + { + case TextureFormat.BC1: + case TextureFormat.BC2: + case TextureFormat.BC3: + case TextureFormat.BC7: + return 16; + case TextureFormat.R8G8B8A8: + case TextureFormat.B8G8R8A8: + case TextureFormat.R5G6B5: + case TextureFormat.A1R5G5B5: + case TextureFormat.B4G4R4A4: + case TextureFormat.A2R10G10B10: + case TextureFormat.R16G16: + case TextureFormat.R16G16B16A16: + case TextureFormat.R8: + case TextureFormat.R8G8_SNORM: + case TextureFormat.R8G8B8A8_SNORM: + case TextureFormat.R16_SFLOAT: + case TextureFormat.R16G16_SFLOAT: + case TextureFormat.R16G16B16A16_SFLOAT: + case TextureFormat.R32_SFLOAT: + case TextureFormat.R32G32_SFLOAT: + case TextureFormat.R32G32B32A32_SFLOAT: + case TextureFormat.R8_UINT: + case TextureFormat.R8G8_UINT: + case TextureFormat.R8G8B8A8_UINT: + case TextureFormat.R16_UINT: + case TextureFormat.R16G16_UINT: + case TextureFormat.R16G16B16A16_UINT: + case TextureFormat.D16_UNORM: + case TextureFormat.D24_UNORM: + case TextureFormat.D24_UNORM_S8_UINT: + case TextureFormat.D32_SFLOAT: + case TextureFormat.D32_SFLOAT_S8_UINT: + return 1; + default: + Log.Error("Texture format not recognized!"); + return 0; + } + } + + public static implicit operator TextureSlice(Texture t) => new TextureSlice(t); + public static implicit operator TextureRegion(Texture t) => new TextureRegion(t); +} \ No newline at end of file diff --git a/Nerfed.Runtime/Graphics/Resources/TransferBuffer.cs b/Nerfed.Runtime/Graphics/Resources/TransferBuffer.cs new file mode 100644 index 0000000..a4bf66d --- /dev/null +++ b/Nerfed.Runtime/Graphics/Resources/TransferBuffer.cs @@ -0,0 +1,209 @@ +using System; +using System.Runtime.InteropServices; +using RefreshCS; + +namespace Nerfed.Runtime.Graphics; + +/// +/// A data container that can efficiently transfer data to and from the GPU. +/// +public unsafe class TransferBuffer : RefreshResource +{ + protected override Action ReleaseFunction => Refresh.Refresh_ReleaseTransferBuffer; + + /// + /// Size in bytes. + /// + public uint Size { get; } + +#if DEBUG + public bool Mapped { get; private set; } +#endif + + /// + /// Creates a buffer of requested size given a type and element count. + /// + /// The type that the buffer will contain. + /// The GraphicsDevice. + /// How many elements of type T the buffer will contain. + /// + public unsafe static TransferBuffer Create( + GraphicsDevice device, + TransferBufferUsage usage, + uint elementCount + ) where T : unmanaged + { + return new TransferBuffer( + device, + usage, + (uint) Marshal.SizeOf() * elementCount + ); + } + + /// + /// Creates a TransferBuffer. + /// + /// An initialized GraphicsDevice. + /// Whether this will be used to upload buffers or textures. + /// The length of the buffer. Cannot be resized. + public TransferBuffer( + GraphicsDevice device, + TransferBufferUsage usage, + uint sizeInBytes + ) : base(device) + { + Handle = Refresh.Refresh_CreateTransferBuffer( + device.Handle, + (Refresh.TransferBufferUsage) usage, + sizeInBytes + ); + Size = sizeInBytes; + } + + /// + /// Immediately copies data from a Span to the TransferBuffer. + /// Returns the length of the copy in bytes. + /// + /// If cycle is set to true and this TransferBuffer was used in an Upload command, + /// that command will still use the correct data at the cost of increased memory usage. + /// + /// If cycle is set to false, the data will be overwritten immediately, + /// which could cause a data race. + /// + public unsafe uint SetData( + Span source, + uint bufferOffsetInBytes, + bool cycle + ) where T : unmanaged + { + int elementSize = Marshal.SizeOf(); + uint dataLengthInBytes = (uint) (elementSize * source.Length); + +#if DEBUG + AssertBufferBoundsCheck(Size, bufferOffsetInBytes, dataLengthInBytes); + AssertNotMapped(); +#endif + + fixed (T* dataPtr = source) + { + Refresh.Refresh_SetTransferData( + Device.Handle, + (nint) dataPtr, + new Refresh.TransferBufferRegion + { + TransferBuffer = Handle, + Offset = bufferOffsetInBytes, + Size = dataLengthInBytes + }, + Conversions.BoolToInt(cycle) + ); + } + + return dataLengthInBytes; + } + + /// + /// Immediately copies data from a Span to the TransferBuffer. + /// Returns the length of the copy in bytes. + /// + /// If cycle is set to true and this TransferBuffer was used in an Upload command, + /// that command will still use the corret data at the cost of increased memory usage. + /// + /// If cycle is set to false, the data will be overwritten immediately, + /// which could cause a data race. + /// + public unsafe uint SetData( + Span source, + bool cycle + ) where T : unmanaged + { + return SetData(source, 0, cycle); + } + + /// + /// Immediately copies data from the TransferBuffer into a Span. + /// + public unsafe void GetData( + Span destination, + uint bufferOffsetInBytes = 0 + ) where T : unmanaged + { + int elementSize = Marshal.SizeOf(); + uint dataLengthInBytes = (uint) (elementSize * destination.Length); + +#if DEBUG + AssertBufferBoundsCheck(Size, bufferOffsetInBytes, dataLengthInBytes); + AssertNotMapped(); +#endif + + fixed (T* dataPtr = destination) + { + Refresh.Refresh_GetTransferData( + Device.Handle, + new Refresh.TransferBufferRegion + { + TransferBuffer = Handle, + Offset = bufferOffsetInBytes, + Size = dataLengthInBytes + }, + (nint) dataPtr + ); + } + } + + /// + /// Maps the transfer buffer into application address space. + /// You must call Unmap before encoding transfer commands. + /// + public unsafe void Map(bool cycle, out byte* data) + { +#if DEBUG + AssertNotMapped(); +#endif + + Refresh.Refresh_MapTransferBuffer( + Device.Handle, + Handle, + Conversions.BoolToInt(cycle), + out data + ); + +#if DEBUG + Mapped = true; +#endif + } + + /// + /// Unmaps the transfer buffer. + /// The pointer given by Map is no longer valid. + /// + public void Unmap() + { + Refresh.Refresh_UnmapTransferBuffer( + Device.Handle, + Handle + ); + +#if DEBUG + Mapped = false; +#endif + } + +#if DEBUG + private void AssertBufferBoundsCheck(uint bufferLengthInBytes, uint offsetInBytes, uint copyLengthInBytes) + { + if (copyLengthInBytes > bufferLengthInBytes + offsetInBytes) + { + throw new InvalidOperationException($"Data overflow! Transfer buffer length {bufferLengthInBytes}, offset {offsetInBytes}, copy length {copyLengthInBytes}"); + } + } + + private void AssertNotMapped() + { + if (Mapped) + { + throw new InvalidOperationException("Transfer buffer must not be mapped!"); + } + } +#endif +} diff --git a/Nerfed.Runtime/Graphics/StockShaders/Binary/fullscreen.vert.spv b/Nerfed.Runtime/Graphics/StockShaders/Binary/fullscreen.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..6f7d258b624fde0d417c460d452774a8b36c924d GIT binary patch literal 1216 zcmZ9KTWb?h6opR{ld9F$THAW5b!xq~HTa++h&L)kp%0OYuTsn)12H4X6oOCs;O|mI z@Gtoy_X;49(x_*SbFO3H<6bs2J^@aF%b>|$gZ(Gw7Otbd7K+|{zj%{Qw)1?{M`@R5KMdZz zO-DsKc{S{(6VJB8CA{dpbX29FsFiMPE+1z_mJd;u%4d15+Id5oge=s=6wYZCSa&@_hy|0$6;@)`%Y=D<^Y`-owSJbN6I`u=%UiA6h z=rdMxW?eGY)3?BX_cb&<#!EzP_5>_*f=4a6xdiSj=~aJJbNg3Ygr8(jfIaE=yTM`7?C4~JkvP+mcfh2dz| zS2tE?_xsJ+xvQ6qoCqaF^c^L?j4pN>ov!>mXo5C4Myx)Xp_Ql{!V{WQV2e2wBF6FU zw3?kpcWV3c;@}??v88YrxtH`KNNdZFAvdEw9F8LQxAt1C?)u}qjc&U&WlSZUg+J(a z`jLyVrSK~9{;f53Scy7j*d1iWx;5;B%^rF=Or!7YH@A(ckm1=3`rKHmaXiD%zC=r8vk^*pi|<9T(u1L`)6`}dn1h)`#(a?YntP4|BeIO%N5 z`DuG5`g6Q?(eEAb-2IGa+t1jpNBG4gBNc4zP*kQaXL z*L~e&N3LK0d*s{a4v=#m_83O40eR!yk9Ll~i*CI0%JCcM^2WPg?HvCZy7A64$3H-q zH{SEmUSKu${v7!GZeMHZH_mg`w~*HOCAz=kx#m~s#{9;CBzxABW_n@6@b34G8b@rH$L0X zqnk6HW^)4FHN6Im)t_TuKriN=#4hHZ!Zvpb7^^?$oW!*2q(&tpj6u)pp8f8yC>`%1FCv**#>Ukyxxyv7TOUnj0-iub4l3dQlgPKDywJXcTtn_4o5+1+3z^_Ij{d{N1~*3UH0WieFWpa_taq682H6=d zXoYJVEBpB%FALD=nC}b`&x^r9{xdT##q33_-Oazh%lm_);y4?|+`!mrw>ykAn9tcg z+Vkj-Z};OJZO>tz{pj?{ui0@NBjzoDpMEamPVSu-jEQdWlY{bf@F6>Emu2rHVserf zLnvZ$L|(h>mOV_c$;eq=RM0L+#kB8IorG0yE9t^m9&q_g&f>UHj0)|Xc?<5z3je0N+%JF~rwub{^e zaqsKPyO^_>djkpK*K6}zSl)FW>usRTN&RkP8yB~pzV9aH{kv#ur?~!RjhSzK-!bJs zsAKjsuJ5~A;~C$WedWdDoxdP{lW+KU_eDfL$~@L#GT)B$6Z!y3^l}V z)|h=$p5K~xouLCG&Ug#)zWK)G?l*OfH4%H8<9zPAj`%G&-zzrIZz9enNB0o38q9 zre|hvsOV1ZnNUqn>P$!2josSoHx~McX`&aMJO*U#bWQ0O4;5ka`XaTX)INXsNUfMo zkiXch?Y`aFZjff2J)dlUs@Cg`t>Dg(JUyfvhkJI2^DK`VwO7^pPThvk{jCE<;VMS6(kx&+BA*^L$-wM1Ri^{rX|$$Fr$4HG||GaAjf$e`h9k(q(XoUCtP0 zTF2O}w-Najc!VdrD5&%K<1LZgLJK zCR~*k9&Ey_-)ZuAr|@@%uF#K>+dt~#7FkihNbdQ>dS50|`tm*Sl?~yo>6s$%Xzpx!ithhBM@H-YGr3M{|UCG-fDP7d`Hf z=X)%G<-AYP<371PVm@MZ(PNQ3-(w}KrQ}_U9;@W`h<6vS5K(_XZoNwQuHrS~fPeKo F@ei?}RjB{~ literal 0 HcmV?d00001 diff --git a/Nerfed.Runtime/Graphics/StockShaders/Source/fullscreen.vert b/Nerfed.Runtime/Graphics/StockShaders/Source/fullscreen.vert new file mode 100644 index 0000000..447e258 --- /dev/null +++ b/Nerfed.Runtime/Graphics/StockShaders/Source/fullscreen.vert @@ -0,0 +1,9 @@ +#version 450 + +layout(location = 0) out vec2 outTexCoord; + +void main() +{ + outTexCoord = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + gl_Position = vec4(outTexCoord * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0); +} diff --git a/Nerfed.Runtime/Graphics/StockShaders/Source/text_msdf.frag b/Nerfed.Runtime/Graphics/StockShaders/Source/text_msdf.frag new file mode 100644 index 0000000..e3a5f55 --- /dev/null +++ b/Nerfed.Runtime/Graphics/StockShaders/Source/text_msdf.frag @@ -0,0 +1,34 @@ +#version 450 + +layout(location = 0) in vec2 inTexCoord; +layout(location = 1) in vec4 inColor; + +layout(location = 0) out vec4 outColor; + +layout(set = 2, binding = 0) uniform sampler2D msdf; + +layout(set = 3, binding = 0) uniform UBO +{ + float pxRange; +} ubo; + +float median(float r, float g, float b) +{ + return max(min(r, g), min(max(r, g), b)); +} + +float screenPxRange() +{ + vec2 unitRange = vec2(ubo.pxRange)/vec2(textureSize(msdf, 0)); + vec2 screenTexSize = vec2(1.0)/fwidth(inTexCoord); + return max(0.5*dot(unitRange, screenTexSize), 1.0); +} + +void main() +{ + vec3 msd = texture(msdf, inTexCoord).rgb; + float sd = median(msd.r, msd.g, msd.b); + float screenPxDistance = screenPxRange() * (sd - 0.5); + float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0); + outColor = mix(vec4(0.0, 0.0, 0.0, 0.0), inColor, opacity); +} diff --git a/Nerfed.Runtime/Graphics/StockShaders/Source/text_transform.vert b/Nerfed.Runtime/Graphics/StockShaders/Source/text_transform.vert new file mode 100644 index 0000000..42f7827 --- /dev/null +++ b/Nerfed.Runtime/Graphics/StockShaders/Source/text_transform.vert @@ -0,0 +1,20 @@ +#version 450 + +layout(location = 0) in vec3 inPos; +layout(location = 1) in vec2 inTexCoord; +layout(location = 2) in vec4 inColor; + +layout(location = 0) out vec2 outTexCoord; +layout(location = 1) out vec4 outColor; + +layout(set = 1, binding = 0) uniform UBO +{ + mat4 ViewProjection; +} ubo; + +void main() +{ + gl_Position = ubo.ViewProjection * vec4(inPos, 1.0); + outTexCoord = inTexCoord; + outColor = inColor; +} diff --git a/Nerfed.Runtime/Graphics/StockShaders/Source/video_yuv2rgba.frag b/Nerfed.Runtime/Graphics/StockShaders/Source/video_yuv2rgba.frag new file mode 100644 index 0000000..d0d5510 --- /dev/null +++ b/Nerfed.Runtime/Graphics/StockShaders/Source/video_yuv2rgba.frag @@ -0,0 +1,38 @@ +/* + * This effect is based on the YUV-to-RGBA GLSL shader found in SDL. + * Thus, it also released under the zlib license: + * http://libsdl.org/license.php + */ +#version 450 + +layout(location = 0) in vec2 TexCoord; + +layout(location = 0) out vec4 FragColor; + +layout(set = 2, binding = 0) uniform sampler2D YSampler; +layout(set = 2, binding = 1) uniform sampler2D USampler; +layout(set = 2, binding = 2) uniform sampler2D VSampler; + +/* More info about colorspace conversion: + * http://www.equasys.de/colorconversion.html + * http://www.equasys.de/colorformat.html + */ + +const vec3 offset = vec3(-0.0625, -0.5, -0.5); +const vec3 Rcoeff = vec3(1.164, 0.000, 1.793); +const vec3 Gcoeff = vec3(1.164, -0.213, -0.533); +const vec3 Bcoeff = vec3(1.164, 2.112, 0.000); + +void main() +{ + vec3 yuv; + yuv.x = texture(YSampler, TexCoord).r; + yuv.y = texture(USampler, TexCoord).r; + yuv.z = texture(VSampler, TexCoord).r; + yuv += offset; + + FragColor.r = dot(yuv, Rcoeff); + FragColor.g = dot(yuv, Gcoeff); + FragColor.b = dot(yuv, Bcoeff); + FragColor.a = 1.0; +} diff --git a/Nerfed.Runtime/Input/ButtonState.cs b/Nerfed.Runtime/Input/ButtonState.cs new file mode 100644 index 0000000..2b1b421 --- /dev/null +++ b/Nerfed.Runtime/Input/ButtonState.cs @@ -0,0 +1,7 @@ +namespace Nerfed.Runtime; + +public enum ButtonState +{ + Released, + Pressed +} \ No newline at end of file diff --git a/Nerfed.Runtime/Input/Devices/GamePad.cs b/Nerfed.Runtime/Input/Devices/GamePad.cs new file mode 100644 index 0000000..376d021 --- /dev/null +++ b/Nerfed.Runtime/Input/Devices/GamePad.cs @@ -0,0 +1,224 @@ +using SDL2; + +namespace Nerfed.Runtime; + +public class GamePad +{ + private const int maxGamePads = 4; + private static readonly GamePad[] gamePads = new GamePad[maxGamePads]; + private static readonly GamePad dummy = new GamePad(); + + public IntPtr Handle { get; private set; } + public bool IsConnected => Handle != IntPtr.Zero; + public int JoystickInstanceId { get; private set; } = -1; + + private readonly ButtonState[] buttonStates; + private readonly ButtonState[] lastButtonStates; + + static GamePad() + { + for (int i = 0; i < maxGamePads; i++) + { + gamePads[i] = new GamePad(); + } + } + + private GamePad() + { + int gamePadButtonCount = Enum.GetValues().Length; + buttonStates = new ButtonState[gamePadButtonCount]; + lastButtonStates = new ButtonState[gamePadButtonCount]; + } + + public bool IsButtonDown(GamePadButton button) + { + return buttonStates[(int)button] == ButtonState.Pressed; + } + + private void Register(IntPtr handle) + { + Handle = handle; + IntPtr joystickHandle = SDL.SDL_GameControllerGetJoystick(handle); + JoystickInstanceId = SDL.SDL_JoystickInstanceID(joystickHandle); + Log.Info("Controller connected"); + } + + private void UnRegister() + { + Handle = IntPtr.Zero; + JoystickInstanceId = -1; + Log.Info("Controller disconnected"); + } + + public static GamePad Get(int slot) + { + return slot >= 0 && slot < maxGamePads ? gamePads[slot] : dummy; + } + + internal static void Update() + { + for (int i = 0; i < maxGamePads; i++) + { + GamePad gamePad = gamePads[i]; + if (gamePad.IsConnected) + { + Array.Copy(gamePad.buttonStates, gamePad.lastButtonStates, gamePad.buttonStates.Length); + } + } + } + + internal static void ProcessEvent(ref SDL.SDL_Event ev) + { + switch (ev.type) + { + case SDL.SDL_EventType.SDL_CONTROLLERDEVICEADDED: + ProcessDeviceAdded(ref ev.cdevice); + break; + case SDL.SDL_EventType.SDL_CONTROLLERDEVICEREMOVED: + ProcessDeviceRemoved(ref ev.cdevice); + break; + case SDL.SDL_EventType.SDL_CONTROLLERBUTTONDOWN: + ProcessButtonDown(ref ev.cbutton); + break; + case SDL.SDL_EventType.SDL_CONTROLLERBUTTONUP: + ProcessButtonUp(ref ev.cbutton); + break; + case SDL.SDL_EventType.SDL_CONTROLLERAXISMOTION: + ProcessAxisMotion(ref ev.caxis); + break; + case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADDOWN: + ProcessTouchPadDown(ref ev.ctouchpad); + break; + case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADUP: + ProcessTouchPadUp(ref ev.ctouchpad); + break; + case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADMOTION: + ProcessTouchPadMotion(ref ev.ctouchpad); + break; + case SDL.SDL_EventType.SDL_CONTROLLERSENSORUPDATE: + ProcessSensorUpdate(ref ev.sensor); + break; + } + } + + private static void ProcessDeviceAdded(ref SDL.SDL_ControllerDeviceEvent ev) + { + int index = ev.which; + if (SDL.SDL_IsGameController(index) == SDL.SDL_bool.SDL_TRUE) + { + int slot = -1; + for (int i = 0; i < maxGamePads; i++) + { + if (!gamePads[i].IsConnected) + { + slot = i; + break; + } + } + + if (slot == -1) + { + Log.Warning("Too many gamepads connected"); + return; + } + + IntPtr handle = SDL.SDL_GameControllerOpen(index); + if (handle == IntPtr.Zero) + { + Log.Error($"Failed to open gamepad: {SDL.SDL_GetError()}"); + return; + } + + gamePads[slot].Register(handle); + } + } + + private static GamePad GetByJoystickId(int joystickId) + { + for (int slot = 0; slot < maxGamePads; slot++) + { + GamePad gamePad = gamePads[slot]; + if (gamePad.IsConnected && gamePad.JoystickInstanceId == joystickId) + { + return gamePad; + } + } + + return null; + } + + private static void ProcessDeviceRemoved(ref SDL.SDL_ControllerDeviceEvent ev) + { + GamePad gamePad = GetByJoystickId(ev.which); + gamePad?.UnRegister(); + } + + private static void ProcessButtonDown(ref SDL.SDL_ControllerButtonEvent ev) + { + if (TryConvertButton(ev.button, out GamePadButton button)) + { + GamePad gamePad = GetByJoystickId(ev.which); + gamePad.buttonStates[(int)button] = ButtonState.Pressed; + } + } + + private static void ProcessButtonUp(ref SDL.SDL_ControllerButtonEvent ev) + { + if (TryConvertButton(ev.button, out GamePadButton button)) + { + GamePad gamePad = GetByJoystickId(ev.which); + gamePad.buttonStates[(int)button] = ButtonState.Released; + } + } + + private static void ProcessAxisMotion(ref SDL.SDL_ControllerAxisEvent ev) + { + + } + + private static void ProcessTouchPadDown(ref SDL.SDL_ControllerTouchpadEvent ev) + { + + } + + private static void ProcessTouchPadUp(ref SDL.SDL_ControllerTouchpadEvent ev) + { + + } + + private static void ProcessTouchPadMotion(ref SDL.SDL_ControllerTouchpadEvent ev) + { + + } + + private static void ProcessSensorUpdate(ref SDL.SDL_SensorEvent ev) + { + + } + + private static bool TryConvertButton(byte rawButton, out GamePadButton button) + { + GamePadButton? result = rawButton switch + { + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_A => GamePadButton.A, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_B => GamePadButton.B, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_X => GamePadButton.X, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_Y => GamePadButton.Y, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_BACK => GamePadButton.Back, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_START => GamePadButton.Start, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSTICK => GamePadButton.LeftStick, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSTICK => GamePadButton.RightStick, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSHOULDER => GamePadButton.LeftShoulder, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER => GamePadButton.RightShoulder, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_UP => GamePadButton.DPadUp, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_DOWN => GamePadButton.DPadDown, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_LEFT => GamePadButton.DPadLeft, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_RIGHT => GamePadButton.DPadRight, + (byte)SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_TOUCHPAD => GamePadButton.BigButton, + _ => null + }; + + button = result.GetValueOrDefault(); + return result.HasValue; + } +} diff --git a/Nerfed.Runtime/Input/Devices/Keyboard.cs b/Nerfed.Runtime/Input/Devices/Keyboard.cs new file mode 100644 index 0000000..ca6b3c6 --- /dev/null +++ b/Nerfed.Runtime/Input/Devices/Keyboard.cs @@ -0,0 +1,295 @@ +using System.Runtime.InteropServices; +using System.Text; +using SDL2; + +namespace Nerfed.Runtime; + +public static class Keyboard +{ + private static readonly List textInput = new List(); + + private const int maxPressedKeys = 8; + private static readonly List keys = new List(maxPressedKeys); + private static readonly List lastKeys = new List(maxPressedKeys); + + /// + /// True if the key was pressed or continued to be held down this frame. + /// + /// + /// + public static bool IsKeyDown(Key key) + { + return HasKey(keys, key); + } + + /// + /// True if the key was pressed this frame. + /// + public static bool IsKeyPressed(Key key) + { + return HasKey(keys, key) && !HasKey(lastKeys, key); + } + + /// + /// True if the key was released or continued to be released this frame. + /// + public static bool IsKeyUp(Key key) + { + return !HasKey(keys, key); + } + + /// + /// True if the key was released this frame. + /// + public static bool IsKeyReleased(Key key) + { + return !HasKey(keys, key) && HasKey(lastKeys, key); + } + + public static ReadOnlySpan GetPressedKeys() + { + return CollectionsMarshal.AsSpan(keys); + } + + public static ReadOnlySpan GetTextInput() + { + return CollectionsMarshal.AsSpan(textInput); + } + + internal static void Update() + { + textInput.Clear(); + + lastKeys.Clear(); + if (keys.Count > 0) + { + lastKeys.AddRange(keys); + } + } + + private static bool HasKey(List list, Key key) + { + for (int i = 0; i < list.Count; i++) + { + if (keys[i] == key) + { + return true; + } + } + + return false; + } + + internal static void ProcessEvent(ref SDL.SDL_Event ev) + { + switch (ev.type) + { + case SDL.SDL_EventType.SDL_TEXTINPUT: + ProcessTextInputEvent(ref ev.text); + break; + case SDL.SDL_EventType.SDL_KEYDOWN: + ProcessKeyDownEvent(ref ev.key); + break; + case SDL.SDL_EventType.SDL_KEYUP: + ProcessKeyUpEvent(ref ev.key); + break; + } + } + + private static unsafe void ProcessTextInputEvent(ref SDL.SDL_TextInputEvent evt) + { + // Based on the SDL2# LPUtf8StrMarshaler + int MeasureStringLength(byte* ptr) + { + int bytes; + for (bytes = 0; *ptr != 0; ptr += 1, bytes += 1) ; + return bytes; + } + + fixed (byte* textPtr = evt.text) + { + int bytes = MeasureStringLength(textPtr); + if (bytes > 0) + { + /* UTF8 will never encode more characters + * than bytes in a string, so bytes is a + * suitable upper estimate of size needed + */ + char* charsBuffer = stackalloc char[bytes]; + int chars = Encoding.UTF8.GetChars( + textPtr, + bytes, + charsBuffer, + bytes + ); + + if (chars > 0) + { + textInput.AddRange(new ReadOnlySpan(charsBuffer, chars)); + } + } + } + } + + private static void ProcessKeyDownEvent(ref SDL.SDL_KeyboardEvent ev) + { + Key key = ConvertScancode(ev.keysym.scancode); + SetKeyState(key, KeyState.Down); + } + + private static void ProcessKeyUpEvent(ref SDL.SDL_KeyboardEvent ev) + { + Key key = ConvertScancode(ev.keysym.scancode); + SetKeyState(key, KeyState.Up); + } + + public static void SetKeyState(Key key, KeyState state) + { + if (keys.Count >= maxPressedKeys) + { + return; + } + + if (state == KeyState.Down) + { + if (!keys.Contains(key)) + { + keys.Add(key); + } + } + else if (state == KeyState.Up) + { + keys.Remove(key); + } + } + + private static Key ConvertScancode(SDL.SDL_Scancode scancode) + { + switch (scancode) + { + case SDL.SDL_Scancode.SDL_SCANCODE_A: return Key.A; + case SDL.SDL_Scancode.SDL_SCANCODE_B: return Key.B; + case SDL.SDL_Scancode.SDL_SCANCODE_C: return Key.C; + case SDL.SDL_Scancode.SDL_SCANCODE_D: return Key.D; + case SDL.SDL_Scancode.SDL_SCANCODE_E: return Key.E; + case SDL.SDL_Scancode.SDL_SCANCODE_F: return Key.F; + case SDL.SDL_Scancode.SDL_SCANCODE_G: return Key.G; + case SDL.SDL_Scancode.SDL_SCANCODE_H: return Key.H; + case SDL.SDL_Scancode.SDL_SCANCODE_I: return Key.I; + case SDL.SDL_Scancode.SDL_SCANCODE_J: return Key.J; + case SDL.SDL_Scancode.SDL_SCANCODE_K: return Key.K; + case SDL.SDL_Scancode.SDL_SCANCODE_L: return Key.L; + case SDL.SDL_Scancode.SDL_SCANCODE_M: return Key.M; + case SDL.SDL_Scancode.SDL_SCANCODE_N: return Key.N; + case SDL.SDL_Scancode.SDL_SCANCODE_O: return Key.O; + case SDL.SDL_Scancode.SDL_SCANCODE_P: return Key.P; + case SDL.SDL_Scancode.SDL_SCANCODE_Q: return Key.Q; + case SDL.SDL_Scancode.SDL_SCANCODE_R: return Key.R; + case SDL.SDL_Scancode.SDL_SCANCODE_S: return Key.S; + case SDL.SDL_Scancode.SDL_SCANCODE_T: return Key.T; + case SDL.SDL_Scancode.SDL_SCANCODE_U: return Key.U; + case SDL.SDL_Scancode.SDL_SCANCODE_V: return Key.V; + case SDL.SDL_Scancode.SDL_SCANCODE_W: return Key.W; + case SDL.SDL_Scancode.SDL_SCANCODE_X: return Key.X; + case SDL.SDL_Scancode.SDL_SCANCODE_Y: return Key.Y; + case SDL.SDL_Scancode.SDL_SCANCODE_Z: return Key.Z; + case SDL.SDL_Scancode.SDL_SCANCODE_1: return Key.D1; + case SDL.SDL_Scancode.SDL_SCANCODE_2: return Key.D2; + case SDL.SDL_Scancode.SDL_SCANCODE_3: return Key.D3; + case SDL.SDL_Scancode.SDL_SCANCODE_4: return Key.D4; + case SDL.SDL_Scancode.SDL_SCANCODE_5: return Key.D5; + case SDL.SDL_Scancode.SDL_SCANCODE_6: return Key.D6; + case SDL.SDL_Scancode.SDL_SCANCODE_7: return Key.D7; + case SDL.SDL_Scancode.SDL_SCANCODE_8: return Key.D8; + case SDL.SDL_Scancode.SDL_SCANCODE_9: return Key.D9; + case SDL.SDL_Scancode.SDL_SCANCODE_0: return Key.D0; + case SDL.SDL_Scancode.SDL_SCANCODE_RETURN: return Key.Enter; + case SDL.SDL_Scancode.SDL_SCANCODE_ESCAPE: return Key.Escape; + case SDL.SDL_Scancode.SDL_SCANCODE_BACKSPACE: return Key.Backspace; + case SDL.SDL_Scancode.SDL_SCANCODE_TAB: return Key.Tab; + case SDL.SDL_Scancode.SDL_SCANCODE_SPACE: return Key.Space; + case SDL.SDL_Scancode.SDL_SCANCODE_MINUS: return Key.Minus; + case SDL.SDL_Scancode.SDL_SCANCODE_EQUALS: return Key.Equals; + case SDL.SDL_Scancode.SDL_SCANCODE_LEFTBRACKET: return Key.LeftBracket; + case SDL.SDL_Scancode.SDL_SCANCODE_RIGHTBRACKET: return Key.RightBracket; + case SDL.SDL_Scancode.SDL_SCANCODE_BACKSLASH: return Key.Backslash; + case SDL.SDL_Scancode.SDL_SCANCODE_SEMICOLON: return Key.Semicolon; + case SDL.SDL_Scancode.SDL_SCANCODE_APOSTROPHE: return Key.Apostrophe; + case SDL.SDL_Scancode.SDL_SCANCODE_GRAVE: return Key.Tilde; + case SDL.SDL_Scancode.SDL_SCANCODE_COMMA: return Key.Comma; + case SDL.SDL_Scancode.SDL_SCANCODE_PERIOD: return Key.Period; + case SDL.SDL_Scancode.SDL_SCANCODE_SLASH: return Key.Slash; + case SDL.SDL_Scancode.SDL_SCANCODE_CAPSLOCK: return Key.CapsLock; + case SDL.SDL_Scancode.SDL_SCANCODE_F1: return Key.F1; + case SDL.SDL_Scancode.SDL_SCANCODE_F2: return Key.F2; + case SDL.SDL_Scancode.SDL_SCANCODE_F3: return Key.F3; + case SDL.SDL_Scancode.SDL_SCANCODE_F4: return Key.F4; + case SDL.SDL_Scancode.SDL_SCANCODE_F5: return Key.F5; + case SDL.SDL_Scancode.SDL_SCANCODE_F6: return Key.F6; + case SDL.SDL_Scancode.SDL_SCANCODE_F7: return Key.F7; + case SDL.SDL_Scancode.SDL_SCANCODE_F8: return Key.F8; + case SDL.SDL_Scancode.SDL_SCANCODE_F9: return Key.F9; + case SDL.SDL_Scancode.SDL_SCANCODE_F10: return Key.F10; + case SDL.SDL_Scancode.SDL_SCANCODE_F11: return Key.F11; + case SDL.SDL_Scancode.SDL_SCANCODE_F12: return Key.F12; + case SDL.SDL_Scancode.SDL_SCANCODE_PRINTSCREEN: return Key.PrintScreen; + case SDL.SDL_Scancode.SDL_SCANCODE_SCROLLLOCK: return Key.Scroll; + case SDL.SDL_Scancode.SDL_SCANCODE_PAUSE: return Key.Pause; + case SDL.SDL_Scancode.SDL_SCANCODE_INSERT: return Key.Insert; + case SDL.SDL_Scancode.SDL_SCANCODE_HOME: return Key.Home; + case SDL.SDL_Scancode.SDL_SCANCODE_PAGEUP: return Key.PageUp; + case SDL.SDL_Scancode.SDL_SCANCODE_DELETE: return Key.Delete; + case SDL.SDL_Scancode.SDL_SCANCODE_END: return Key.End; + case SDL.SDL_Scancode.SDL_SCANCODE_PAGEDOWN: return Key.PageDown; + case SDL.SDL_Scancode.SDL_SCANCODE_RIGHT: return Key.Right; + case SDL.SDL_Scancode.SDL_SCANCODE_LEFT: return Key.Left; + case SDL.SDL_Scancode.SDL_SCANCODE_DOWN: return Key.Down; + case SDL.SDL_Scancode.SDL_SCANCODE_UP: return Key.Up; + case SDL.SDL_Scancode.SDL_SCANCODE_NUMLOCKCLEAR: return Key.NumLock; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_DIVIDE: return Key.Divide; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_MULTIPLY: return Key.Multiply; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_MINUS: return Key.Subtract; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_PLUS: return Key.Add; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_ENTER: return Key.Enter; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_1: return Key.NumPad1; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_2: return Key.NumPad2; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_3: return Key.NumPad3; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_4: return Key.NumPad4; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_5: return Key.NumPad5; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_6: return Key.NumPad6; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_7: return Key.NumPad7; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_8: return Key.NumPad8; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_9: return Key.NumPad9; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_0: return Key.NumPad0; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_PERIOD: return Key.Period; + case SDL.SDL_Scancode.SDL_SCANCODE_APPLICATION: return Key.Apps; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_EQUALS: return Key.Equals; + case SDL.SDL_Scancode.SDL_SCANCODE_F13: return Key.F13; + case SDL.SDL_Scancode.SDL_SCANCODE_F14: return Key.F14; + case SDL.SDL_Scancode.SDL_SCANCODE_F15: return Key.F15; + case SDL.SDL_Scancode.SDL_SCANCODE_F16: return Key.F16; + case SDL.SDL_Scancode.SDL_SCANCODE_F17: return Key.F17; + case SDL.SDL_Scancode.SDL_SCANCODE_F18: return Key.F18; + case SDL.SDL_Scancode.SDL_SCANCODE_F19: return Key.F19; + case SDL.SDL_Scancode.SDL_SCANCODE_F20: return Key.F20; + case SDL.SDL_Scancode.SDL_SCANCODE_F21: return Key.F21; + case SDL.SDL_Scancode.SDL_SCANCODE_F22: return Key.F22; + case SDL.SDL_Scancode.SDL_SCANCODE_F23: return Key.F23; + case SDL.SDL_Scancode.SDL_SCANCODE_F24: return Key.F24; + case SDL.SDL_Scancode.SDL_SCANCODE_EXECUTE: return Key.Execute; + case SDL.SDL_Scancode.SDL_SCANCODE_HELP: return Key.Help; + case SDL.SDL_Scancode.SDL_SCANCODE_MENU: return Key.Apps; + case SDL.SDL_Scancode.SDL_SCANCODE_SELECT: return Key.Select; + case SDL.SDL_Scancode.SDL_SCANCODE_KP_COMMA: return Key.Comma; + case SDL.SDL_Scancode.SDL_SCANCODE_LCTRL: return Key.LeftControl; + case SDL.SDL_Scancode.SDL_SCANCODE_LSHIFT: return Key.LeftShift; + case SDL.SDL_Scancode.SDL_SCANCODE_LALT: return Key.LeftAlt; + case SDL.SDL_Scancode.SDL_SCANCODE_LGUI: return Key.LeftSuper; + case SDL.SDL_Scancode.SDL_SCANCODE_RCTRL: return Key.RightControl; + case SDL.SDL_Scancode.SDL_SCANCODE_RSHIFT: return Key.RightShift; + case SDL.SDL_Scancode.SDL_SCANCODE_RALT: return Key.RightAlt; + case SDL.SDL_Scancode.SDL_SCANCODE_RGUI: return Key.RightSuper; + default: return Key.None; + } + } +} diff --git a/Nerfed.Runtime/Input/Devices/Mouse.cs b/Nerfed.Runtime/Input/Devices/Mouse.cs new file mode 100644 index 0000000..6386fcd --- /dev/null +++ b/Nerfed.Runtime/Input/Devices/Mouse.cs @@ -0,0 +1,132 @@ +using System.Numerics; +using SDL2; + +namespace Nerfed.Runtime; + +internal static class Mouse +{ + public static Vector2 Position { get; private set; } + + private static readonly ButtonState[] buttonStates; + private static readonly ButtonState[] lastButtonStates; + private static int wheelX; + private static int wheelY; + + static Mouse() + { + int numButtons = Enum.GetValues().Length; + buttonStates = new ButtonState[numButtons]; + lastButtonStates = new ButtonState[numButtons]; + } + + /// /// + /// True if the button is pressed or continued to be held this frame. + /// + public static bool IsButtonDown(MouseButton button) + { + return buttonStates[(int)button] == ButtonState.Pressed; + } + + /// + /// True if the button is pressed this frame. + /// + public static bool IsButtonPressed(MouseButton button) + { + return buttonStates[(int)button] == ButtonState.Pressed && lastButtonStates[(int)button] == ButtonState.Released; + } + + /// + /// True if the button is released or continued to be released this frame. + /// + /// + /// + public static bool IsButtonUp(MouseButton button) + { + return buttonStates[(int)button] == ButtonState.Released; + } + + /// + /// True if the button is released this frame. + /// + public static bool IsButtonReleased(MouseButton button) + { + return buttonStates[(int)button] == ButtonState.Released && lastButtonStates[(int)button] == ButtonState.Pressed; + } + + public static int GetWheel() + { + return wheelY; + } + + public static int GetWheelHorizontal() + { + return wheelX; + } + + internal static void Update() + { + Array.Copy(buttonStates, lastButtonStates, buttonStates.Length); + } + + internal static void ProcessEvent(ref SDL.SDL_Event ev) + { + switch (ev.type) + { + case SDL.SDL_EventType.SDL_MOUSEBUTTONDOWN: + ProcessButtonDownEvent(ref ev.button); + break; + case SDL.SDL_EventType.SDL_MOUSEBUTTONUP: + ProcessButtonUpEvent(ref ev.button); + break; + case SDL.SDL_EventType.SDL_MOUSEWHEEL: + ProcessWheelEvent(ref ev.wheel); + break; + case SDL.SDL_EventType.SDL_MOUSEMOTION: + ProcessMotionEvent(ref ev.motion); + break; + } + } + + private static void ProcessButtonDownEvent(ref SDL.SDL_MouseButtonEvent ev) + { + if (TryConvertButton(ev.button, out MouseButton button)) + { + buttonStates[(int)button] = ButtonState.Pressed; + } + } + + private static void ProcessButtonUpEvent(ref SDL.SDL_MouseButtonEvent ev) + { + if (TryConvertButton(ev.button, out MouseButton button)) + { + buttonStates[(int)button] = ButtonState.Released; + } + } + + private static void ProcessWheelEvent(ref SDL.SDL_MouseWheelEvent ev) + { + wheelX += ev.x; + wheelY += ev.y; + } + + private static void ProcessMotionEvent(ref SDL.SDL_MouseMotionEvent ev) + { + Position = new Vector2(ev.x, ev.y); + } + + private static bool TryConvertButton(byte rawButton, out MouseButton button) + { + MouseButton? result = rawButton switch + { + (byte)SDL.SDL_BUTTON_LEFT => MouseButton.Left, + (byte)SDL.SDL_BUTTON_MIDDLE => MouseButton.Middle, + (byte)SDL.SDL_BUTTON_RIGHT => MouseButton.Right, + (byte)SDL.SDL_BUTTON_X1 => MouseButton.XButton1, + (byte)SDL.SDL_BUTTON_X2 => MouseButton.XButton2, + _ => null + }; + + button = result.GetValueOrDefault(); + return result.HasValue; + } +} diff --git a/Nerfed.Runtime/Input/GamePadAxis.cs b/Nerfed.Runtime/Input/GamePadAxis.cs new file mode 100644 index 0000000..1ab98af --- /dev/null +++ b/Nerfed.Runtime/Input/GamePadAxis.cs @@ -0,0 +1,11 @@ +namespace Nerfed.Runtime; + +public enum GamePadAxis +{ + LeftTrigger, + RightTrigger, + LeftThumbStickX, + LeftThumbStickY, + RightThumbStickX, + RightThumbStickY +} \ No newline at end of file diff --git a/Nerfed.Runtime/Input/GamePadButton.cs b/Nerfed.Runtime/Input/GamePadButton.cs new file mode 100644 index 0000000..b588149 --- /dev/null +++ b/Nerfed.Runtime/Input/GamePadButton.cs @@ -0,0 +1,20 @@ +namespace Nerfed.Runtime; + +public enum GamePadButton +{ + Start, + A, + B, + X, + Y, + DPadLeft, + DPadRight, + DPadUp, + DPadDown, + RightShoulder, + LeftShoulder, + LeftStick, + RightStick, + Back, + BigButton +} \ No newline at end of file diff --git a/Nerfed.Runtime/Input/Key.cs b/Nerfed.Runtime/Input/Key.cs new file mode 100644 index 0000000..7520721 --- /dev/null +++ b/Nerfed.Runtime/Input/Key.cs @@ -0,0 +1,127 @@ +namespace Nerfed.Runtime; + +public enum Key : int // must be int32 (see KeyboardState) +{ + None, + Backspace, + Tab, + Enter, + CapsLock, + Escape, + Space, + PageUp, + PageDown, + End, + Home, + Left, + Up, + Right, + Down, + Select, + Execute, + PrintScreen, + Insert, + Delete, + Help, + D0, + D1, + D2, + D3, + D4, + D5, + D6, + D7, + D8, + D9, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + LeftSuper, + RightSuper, + Apps, + NumPad0, + NumPad1, + NumPad2, + NumPad3, + NumPad4, + NumPad5, + NumPad6, + NumPad7, + NumPad8, + NumPad9, + Multiply, + Add, + Separator, + Subtract, + Decimal, + Divide, + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + F13, + F14, + F15, + F16, + F17, + F18, + F19, + F20, + F21, + F22, + F23, + F24, + NumLock, + Scroll, + LeftShift, + RightShift, + LeftControl, + RightControl, + LeftAlt, + RightAlt, + Semicolon, + Equals, + Comma, + Minus, + Period, + Slash, + Tilde, + LeftBracket, + Backslash, + RightBracket, + Apostrophe, + Pause, + + Count +} \ No newline at end of file diff --git a/Nerfed.Runtime/Input/KeyState.cs b/Nerfed.Runtime/Input/KeyState.cs new file mode 100644 index 0000000..cfda331 --- /dev/null +++ b/Nerfed.Runtime/Input/KeyState.cs @@ -0,0 +1,7 @@ +namespace Nerfed.Runtime; + +public enum KeyState +{ + Up, + Down +} \ No newline at end of file diff --git a/Nerfed.Runtime/Input/MouseButton.cs b/Nerfed.Runtime/Input/MouseButton.cs new file mode 100644 index 0000000..ff850f0 --- /dev/null +++ b/Nerfed.Runtime/Input/MouseButton.cs @@ -0,0 +1,10 @@ +namespace Nerfed.Runtime; + +public enum MouseButton +{ + Left, + Middle, + Right, + XButton1, + XButton2 +} \ No newline at end of file diff --git a/Nerfed.Runtime/Input/MouseState.cs b/Nerfed.Runtime/Input/MouseState.cs new file mode 100644 index 0000000..cc495b4 --- /dev/null +++ b/Nerfed.Runtime/Input/MouseState.cs @@ -0,0 +1,14 @@ +using System.Numerics; + +namespace Nerfed.Runtime; + +public struct MouseState +{ + public Vector2 position; + public ButtonState left; + public ButtonState middle; + public ButtonState right; + public ButtonState x1; + public ButtonState x2; + public int wheel; +} \ No newline at end of file diff --git a/Nerfed.Runtime/Interop/Conversions.cs b/Nerfed.Runtime/Interop/Conversions.cs new file mode 100644 index 0000000..2cac9fb --- /dev/null +++ b/Nerfed.Runtime/Interop/Conversions.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.InteropServices; +using Nerfed.Runtime.Graphics; +using Nerfed.Runtime.Graphics.PackedVector; + +namespace Nerfed.Runtime; + +/// +/// Conversion utilities for interop. +/// +public static class Conversions +{ + private readonly static Dictionary Sizes = new Dictionary + { + { VertexElementFormat.Byte4, (uint) Marshal.SizeOf() }, + { VertexElementFormat.Color, (uint) Marshal.SizeOf() }, + { VertexElementFormat.Float, (uint) Marshal.SizeOf() }, + { VertexElementFormat.HalfVector2, (uint) Marshal.SizeOf() }, + { VertexElementFormat.HalfVector4, (uint) Marshal.SizeOf() }, + { VertexElementFormat.NormalizedShort2, (uint) Marshal.SizeOf() }, + { VertexElementFormat.NormalizedShort4, (uint) Marshal.SizeOf() }, + { VertexElementFormat.Short2, (uint) Marshal.SizeOf() }, + { VertexElementFormat.Short4, (uint) Marshal.SizeOf() }, + { VertexElementFormat.Uint, (uint) Marshal.SizeOf() }, + { VertexElementFormat.Vector2, (uint) Marshal.SizeOf() }, + { VertexElementFormat.Vector3, (uint) Marshal.SizeOf() }, + { VertexElementFormat.Vector4, (uint) Marshal.SizeOf() } + }; + + public static byte BoolToByte(bool b) + { + return (byte) (b ? 1 : 0); + } + + public static bool ByteToBool(byte b) + { + return b != 0; + } + + public static int BoolToInt(bool b) + { + return b ? 1 : 0; + } + + public static bool IntToBool(int b) + { + return b != 0; + } + + public static uint VertexElementFormatSize(VertexElementFormat format) + { + return Sizes[format]; + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Log.cs b/Nerfed.Runtime/Log.cs new file mode 100644 index 0000000..f5027e6 --- /dev/null +++ b/Nerfed.Runtime/Log.cs @@ -0,0 +1,54 @@ +namespace Nerfed.Runtime; + +public static class Log +{ + public enum Type + { + Info, + Warning, + Error + } + + public static void Info(object message) + { + LogInternal(Type.Info, message); + } + + public static void Warning(object message) + { + LogInternal(Type.Warning, message); + } + + public static void Error(object message) + { + LogInternal(Type.Error, message); + } + + public static void Force(Type logType, object message) + { + LogInternal(logType, message); + } + + private static void LogInternal(Type logType, object message) + { + TextWriter writer = logType <= Type.Warning ? Console.Out : Console.Error; + + switch (logType) + { + case Type.Info: + writer.WriteLine(); + break; + case Type.Warning: + writer.WriteLine("[Warning] "); + break; + case Type.Error: + writer.WriteLine("[Error] "); + break; + default: + throw new ArgumentOutOfRangeException(nameof(logType), logType, null); + } + + writer.Write(message); + writer.Flush(); + } +} diff --git a/Nerfed.Runtime/Nerfed.Runtime.csproj b/Nerfed.Runtime/Nerfed.Runtime.csproj index 5f9679e..5de0b0c 100644 --- a/Nerfed.Runtime/Nerfed.Runtime.csproj +++ b/Nerfed.Runtime/Nerfed.Runtime.csproj @@ -16,7 +16,7 @@ Exe net8.0 enable - enable + disable true true true diff --git a/Nerfed.Runtime/Profiler.cs b/Nerfed.Runtime/Profiler.cs new file mode 100644 index 0000000..e8bb4fb --- /dev/null +++ b/Nerfed.Runtime/Profiler.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Nerfed.Runtime; + +public struct ProfilerScope : IDisposable +{ + public ProfilerScope(string label) { + Profiler.BeginSample(label); + } + + public void Dispose() { + Profiler.EndSample(); + } +} + +public static class Profiler +{ + [Conditional("PROFILER")] + public static void BeginSample(string label) { + + } + + [Conditional("PROFILER")] + public static void EndSample() { + + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Program.cs b/Nerfed.Runtime/Program.cs index f0b4543..985f334 100644 --- a/Nerfed.Runtime/Program.cs +++ b/Nerfed.Runtime/Program.cs @@ -2,8 +2,8 @@ internal class Program { - static void Main(string[] args) + private static void Main(string[] args) { - Console.WriteLine("Hello, World!"); + Engine.Run(args); } } \ No newline at end of file diff --git a/Nerfed.Runtime/Storage/StorageContainer.cs b/Nerfed.Runtime/Storage/StorageContainer.cs new file mode 100644 index 0000000..3e2bbda --- /dev/null +++ b/Nerfed.Runtime/Storage/StorageContainer.cs @@ -0,0 +1,9 @@ +namespace Nerfed.Runtime; + +public static class StorageContainer +{ + public static Stream OpenStream(string file) + { + return File.Open(file, FileMode.Open); + } +} diff --git a/Nerfed.Runtime/Util/MathEx.cs b/Nerfed.Runtime/Util/MathEx.cs new file mode 100644 index 0000000..fd411c5 --- /dev/null +++ b/Nerfed.Runtime/Util/MathEx.cs @@ -0,0 +1,20 @@ +namespace Nerfed.Runtime; + +public static class MathEx +{ + public static float ToNearestMultiple(float value, float multiple) { + return MathF.Round(value / multiple, MidpointRounding.AwayFromZero) * multiple; + } + + public static float Normalize(float value, float oldMin, float oldMax) { + return Remap(value, oldMin, oldMax, 0f, 1f); + } + + public static float Denormalize(float value, float newMin, float newMax) { + return Remap(value, 0f, 1f, newMin, newMax); + } + + public static float Remap(float value, float oldMin, float oldMax, float newMin, float newMax) { + return (value - oldMin) / (oldMax - oldMin) * (newMax - newMin) + newMin; + } +} diff --git a/Nerfed.Runtime/Util/SpanExtensions.cs b/Nerfed.Runtime/Util/SpanExtensions.cs new file mode 100644 index 0000000..57b62b1 --- /dev/null +++ b/Nerfed.Runtime/Util/SpanExtensions.cs @@ -0,0 +1,147 @@ +namespace Nerfed.Runtime; + +public static class SpanExtensions +{ + public static void QuickSort(this Span span, Comparison comparison) { + QuickSort(span, 0, span.Length - 1, comparison); + } + + private static void QuickSort(this Span span, int leftIndex, int rightIndex, Comparison comparison) { + for(;;) { + int i = leftIndex; + int j = rightIndex; + T pivot = span[leftIndex]; + + while(i <= j) { + while(comparison(span[i], pivot) < 0) { + i++; + } + + while(comparison(span[j], pivot) > 0) { + j--; + } + + if(i <= j) { + (span[i], span[j]) = (span[j], span[i]); + i++; + j--; + } + } + + if(leftIndex < j) { + QuickSort(span, leftIndex, j, comparison); + } + + if(i < rightIndex) { + leftIndex = i; + continue; + } + + break; + } + } + + public static void HeapSort(this Span span, Comparison comparison) { + if(span.Length <= 1) { + return; + } + + for(int i = span.Length / 2 - 1; i >= 0; i--) { + Heapify(span, span.Length, i, comparison); + } + + for(int i = span.Length - 1; i >= 0; i--) { + (span[0], span[i]) = (span[i], span[0]); + Heapify(span, i, 0, comparison); + } + } + + private static void Heapify(Span span, int size, int index, Comparison comparison) { + for(;;) { + int largestIndex = index; + int leftChild = 2 * index + 1; + int rightChild = 2 * index + 2; + + if(leftChild < size && comparison(span[leftChild], span[largestIndex]) > 0) { + largestIndex = leftChild; + } + + if(rightChild < size && comparison(span[rightChild], span[largestIndex]) > 0) { + largestIndex = rightChild; + } + + if(largestIndex != index) { + (span[index], span[largestIndex]) = (span[largestIndex], span[index]); + index = largestIndex; + continue; + } + + break; + } + } + + public static void MergeSort(this Span span, T[] leftArray, T[] rightArray, Comparison comparison) { + MergeSort(span, leftArray, rightArray, 0, span.Length - 1, comparison); + } + + private static void MergeSort(this Span span, T[] leftArray, T[] rightArray, int left, int right, Comparison comparison) { + if(left < right) { + int middle = left + (right - left) / 2; + MergeSort(span, leftArray, rightArray, left, middle, comparison); + MergeSort(span, leftArray, rightArray, middle + 1, right, comparison); + MergeArray(span, leftArray, rightArray, left, middle, right, comparison); + } + } + + private static void MergeArray(Span span, T[] leftArray, T[] rightArray, int left, int middle, int right, Comparison comparison) { + int leftArrayLength = middle - left + 1; + int rightArrayLength = right - middle; + int i, j; + + for(i = 0; i < leftArrayLength; ++i) { + leftArray[i] = span[left + i]; + } + + for(j = 0; j < rightArrayLength; ++j) { + rightArray[j] = span[middle + 1 + j]; + } + + i = 0; + j = 0; + int k = left; + + while(i < leftArrayLength && j < rightArrayLength) { + if(comparison(leftArray[i], rightArray[j]) <= 0) { + span[k++] = leftArray[i++]; + } + else { + span[k++] = rightArray[j++]; + } + } + + while(i < leftArrayLength) { + span[k++] = leftArray[i++]; + } + + while(j < rightArrayLength) { + span[k++] = rightArray[j++]; + } + } + + public static void InsertionSort(this Span span, Comparison comparison) { + for(int i = 1; i < span.Length; i++) { + T key = span[i]; + int flag = 0; + for(int j = i - 1; j >= 0 && flag != 1;) { + if(comparison(key, span[j]) < 0) { + span[j + 1] = span[j]; + j--; + span[j + 1] = key; + } + else { + flag = 1; + } + } + } + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Video/VideoAV1.cs b/Nerfed.Runtime/Video/VideoAV1.cs new file mode 100644 index 0000000..668afad --- /dev/null +++ b/Nerfed.Runtime/Video/VideoAV1.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; +using Nerfed.Runtime.Graphics; + +namespace Nerfed.Runtime.Video; + +/// +/// This class takes in a filename for AV1 data in .obu (open bitstream unit) format +/// +public unsafe class VideoAV1 : GraphicsResource +{ + public string Filename { get; } + + public int Width => width; + public int Height => height; + public double FramesPerSecond { get; set; } + public Dav1dfile.PixelLayout PixelLayout => pixelLayout; + public int UVWidth { get; } + public int UVHeight { get; } + + private int width; + private int height; + private Dav1dfile.PixelLayout pixelLayout; + + /// + /// Opens an AV1 file so it can be loaded by VideoPlayer. You must also provide a playback framerate. + /// + public VideoAV1(GraphicsDevice device, string filename, double framesPerSecond) : base(device) + { + if (!File.Exists(filename)) + { + throw new ArgumentException("Video file not found!"); + } + + if (Dav1dfile.df_fopen(filename, out IntPtr handle) == 0) + { + throw new Exception("Failed to open video file!"); + } + + Dav1dfile.df_videoinfo(handle, out width, out height, out pixelLayout); + Dav1dfile.df_close(handle); + + if (pixelLayout == Dav1dfile.PixelLayout.I420) + { + UVWidth = Width / 2; + UVHeight = Height / 2; + } + else if (pixelLayout == Dav1dfile.PixelLayout.I422) + { + UVWidth = Width / 2; + UVHeight = Height; + } + else if (pixelLayout == Dav1dfile.PixelLayout.I444) + { + UVWidth = width; + UVHeight = height; + } + else + { + throw new NotSupportedException("Unrecognized YUV format!"); + } + + FramesPerSecond = framesPerSecond; + + Filename = filename; + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Video/VideoAV1Stream.cs b/Nerfed.Runtime/Video/VideoAV1Stream.cs new file mode 100644 index 0000000..a59fa16 --- /dev/null +++ b/Nerfed.Runtime/Video/VideoAV1Stream.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using Nerfed.Runtime.Graphics; + +namespace Nerfed.Runtime.Video; + +// Note that all public methods are async. +internal class VideoAV1Stream : GraphicsResource +{ + public IntPtr Handle => handle; + IntPtr handle; + + public bool Loaded => handle != IntPtr.Zero; + public bool Ended => Dav1dfile.df_eos(Handle) == 1; + + public IntPtr yDataHandle; + public IntPtr uDataHandle; + public IntPtr vDataHandle; + public uint yDataLength; + public uint uvDataLength; + public uint yStride; + public uint uvStride; + + public bool FrameDataUpdated { get; set; } + + private BlockingCollection Actions = new BlockingCollection(); + + private bool Running = false; + + Thread Thread; + + public VideoAV1Stream(GraphicsDevice device) : base(device) + { + handle = IntPtr.Zero; + + Running = true; + + Thread = new Thread(ThreadMain); + Thread.Start(); + } + + private void ThreadMain() + { + while (Running) + { + // block until we can take an action, then run it + Action action = Actions.Take(); + action.Invoke(); + } + + // shutting down... + while (Actions.TryTake(out Action action)) + { + action.Invoke(); + } + } + + public void Load(string filename) + { + Actions.Add(() => LoadHelper(filename)); + } + + public void Reset() + { + Actions.Add(ResetHelper); + } + + public void ReadNextFrame() + { + Actions.Add(ReadNextFrameHelper); + } + + public void Unload() + { + Actions.Add(UnloadHelper); + } + + private void LoadHelper(string filename) + { + if (!Loaded) + { + if (Dav1dfile.df_fopen(filename, out handle) == 0) + { + Log.Error("Failed to load video file: " + filename); + throw new Exception("Failed to load video file!"); + } + + Reset(); + } + } + + private void ResetHelper() + { + if (Loaded) + { + Dav1dfile.df_reset(handle); + ReadNextFrame(); + } + } + + private void ReadNextFrameHelper() + { + if (Loaded && !Ended) + { + lock (this) + { + if (Dav1dfile.df_readvideo( + handle, + 1, + out IntPtr yDataHandle, + out IntPtr uDataHandle, + out IntPtr vDataHandle, + out uint yDataLength, + out uint uvDataLength, + out uint yStride, + out uint uvStride) == 1 + ) { + this.yDataHandle = yDataHandle; + this.uDataHandle = uDataHandle; + this.vDataHandle = vDataHandle; + this.yDataLength = yDataLength; + this.uvDataLength = uvDataLength; + this.yStride = yStride; + this.uvStride = uvStride; + + FrameDataUpdated = true; + } + } + } + } + + private void UnloadHelper() + { + if (Loaded) + { + Dav1dfile.df_close(handle); + handle = IntPtr.Zero; + } + } + + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + Unload(); + Running = false; + + if (disposing) + { + Thread.Join(); + } + } + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Video/VideoPlayer.cs b/Nerfed.Runtime/Video/VideoPlayer.cs new file mode 100644 index 0000000..8b225ac --- /dev/null +++ b/Nerfed.Runtime/Video/VideoPlayer.cs @@ -0,0 +1,347 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Nerfed.Runtime.Graphics; + +namespace Nerfed.Runtime.Video; + +/// +/// A structure for continuous decoding of AV1 videos and rendering them into a texture. +/// +public unsafe class VideoPlayer : GraphicsResource +{ + public Texture RenderTexture { get; private set; } = null; + public VideoState State { get; private set; } = VideoState.Stopped; + public bool Loop { get; set; } + public float PlaybackSpeed { get; set; } = 1; + + private VideoAV1 Video = null; + private VideoAV1Stream Stream { get; } + + private Texture yTexture = null; + private Texture uTexture = null; + private Texture vTexture = null; + private Sampler LinearSampler; + + private TransferBuffer TransferBuffer; + + private int currentFrame; + + private Stopwatch timer; + private double lastTimestamp; + private double timeElapsed; + + public VideoPlayer(GraphicsDevice device) : base(device) + { + Stream = new VideoAV1Stream(device); + + LinearSampler = new Sampler(device, SamplerCreateInfo.LinearClamp); + + timer = new Stopwatch(); + } + + /// + /// Prepares a VideoAV1 for decoding and rendering. + /// + /// + public void Load(VideoAV1 video) + { + if (Video != video) + { + Unload(); + + if (RenderTexture == null) + { + RenderTexture = CreateRenderTexture(Device, video.Width, video.Height); + } + + if (yTexture == null) + { + yTexture = CreateSubTexture(Device, video.Width, video.Height); + } + + if (uTexture == null) + { + uTexture = CreateSubTexture(Device, video.UVWidth, video.UVHeight); + } + + if (vTexture == null) + { + vTexture = CreateSubTexture(Device, video.UVWidth, video.UVHeight); + } + + if (video.Width != RenderTexture.Width || video.Height != RenderTexture.Height) + { + RenderTexture.Dispose(); + RenderTexture = CreateRenderTexture(Device, video.Width, video.Height); + } + + if (video.Width != yTexture.Width || video.Height != yTexture.Height) + { + yTexture.Dispose(); + yTexture = CreateSubTexture(Device, video.Width, video.Height); + } + + if (video.UVWidth != uTexture.Width || video.UVHeight != uTexture.Height) + { + uTexture.Dispose(); + uTexture = CreateSubTexture(Device, video.UVWidth, video.UVHeight); + } + + if (video.UVWidth != vTexture.Width || video.UVHeight != vTexture.Height) + { + vTexture.Dispose(); + vTexture = CreateSubTexture(Device, video.UVWidth, video.UVHeight); + } + + Video = video; + + InitializeDav1dStream(); + } + } + + /// + /// Starts playing back and decoding the loaded video. + /// + public void Play() + { + if (Video == null) { return; } + + if (State == VideoState.Playing) + { + return; + } + + timer.Start(); + + State = VideoState.Playing; + } + + /// + /// Pauses playback and decoding of the currently playing video. + /// + public void Pause() + { + if (Video == null) { return; } + + if (State != VideoState.Playing) + { + return; + } + + timer.Stop(); + + State = VideoState.Paused; + } + + /// + /// Stops and resets decoding of the currently playing video. + /// + public void Stop() + { + if (Video == null) { return; } + + if (State == VideoState.Stopped) + { + return; + } + + timer.Stop(); + timer.Reset(); + + lastTimestamp = 0; + timeElapsed = 0; + + ResetDav1dStreams(); + + State = VideoState.Stopped; + } + + /// + /// Unloads the currently playing video. + /// + public void Unload() + { + if (Video == null) + { + return; + } + + timer.Stop(); + timer.Reset(); + + lastTimestamp = 0; + timeElapsed = 0; + + State = VideoState.Stopped; + + Stream.Unload(); + + Video = null; + } + + /// + /// Renders the video data into RenderTexture. + /// + public void Render() + { + if (Video == null || State == VideoState.Stopped) + { + return; + } + + timeElapsed += (timer.Elapsed.TotalMilliseconds - lastTimestamp) * PlaybackSpeed; + lastTimestamp = timer.Elapsed.TotalMilliseconds; + + int thisFrame = ((int) (timeElapsed / (1000.0 / Video.FramesPerSecond))); + if (thisFrame > currentFrame) + { + if (Stream.FrameDataUpdated) + { + UpdateRenderTexture(); + Stream.FrameDataUpdated = false; + } + + currentFrame = thisFrame; + Stream.ReadNextFrame(); + } + + if (Stream.Ended) + { + timer.Stop(); + timer.Reset(); + + Stream.Reset(); + + if (Loop) + { + // Start over! + currentFrame = -1; + timer.Start(); + } + else + { + State = VideoState.Stopped; + } + } + } + + private void UpdateRenderTexture() + { + uint uOffset; + uint vOffset; + uint yStride; + uint uvStride; + + lock (Stream) + { + Span ySpan = new Span((void*) Stream.yDataHandle, (int) Stream.yDataLength); + Span uSpan = new Span((void*) Stream.uDataHandle, (int) Stream.uvDataLength); + Span vSpan = new Span((void*) Stream.vDataHandle, (int) Stream.uvDataLength); + + if (TransferBuffer == null || TransferBuffer.Size < ySpan.Length + uSpan.Length + vSpan.Length) + { + TransferBuffer?.Dispose(); + TransferBuffer = new TransferBuffer(Device, TransferBufferUsage.Upload, (uint) (ySpan.Length + uSpan.Length + vSpan.Length)); + } + TransferBuffer.SetData(ySpan, 0, true); + TransferBuffer.SetData(uSpan, (uint) ySpan.Length, false); + TransferBuffer.SetData(vSpan, (uint) (ySpan.Length + uSpan.Length), false); + + uOffset = (uint) ySpan.Length; + vOffset = (uint) (ySpan.Length + vSpan.Length); + + yStride = Stream.yStride; + uvStride = Stream.uvStride; + } + + CommandBuffer commandBuffer = Device.AcquireCommandBuffer(); + + CopyPass copyPass = commandBuffer.BeginCopyPass(); + + copyPass.UploadToTexture( + new TextureTransferInfo(TransferBuffer, 0, yStride, yTexture.Height), + yTexture, + true + ); + + copyPass.UploadToTexture( + new TextureTransferInfo(TransferBuffer, uOffset, uvStride, uTexture.Height), + uTexture, + true + ); + + copyPass.UploadToTexture( + new TextureTransferInfo(TransferBuffer, vOffset, uvStride, vTexture.Height), + vTexture, + true + ); + + commandBuffer.EndCopyPass(copyPass); + + RenderPass renderPass = commandBuffer.BeginRenderPass( + new ColorAttachmentInfo(RenderTexture, true, Color.Black) + ); + + renderPass.BindGraphicsPipeline(Device.VideoPipeline); + renderPass.BindFragmentSampler(new TextureSamplerBinding(yTexture, LinearSampler), 0); + renderPass.BindFragmentSampler(new TextureSamplerBinding(uTexture, LinearSampler), 1); + renderPass.BindFragmentSampler(new TextureSamplerBinding(vTexture, LinearSampler), 2); + renderPass.DrawPrimitives(0, 1); + + commandBuffer.EndRenderPass(renderPass); + + Device.Submit(commandBuffer); + } + + private static Texture CreateRenderTexture(GraphicsDevice graphicsDevice, int width, int height) + { + return Texture.CreateTexture2D( + graphicsDevice, + (uint) width, + (uint) height, + TextureFormat.R8G8B8A8, + TextureUsageFlags.ColorTarget | TextureUsageFlags.Sampler + ); + } + + private static Texture CreateSubTexture(GraphicsDevice graphicsDevice, int width, int height) + { + return Texture.CreateTexture2D( + graphicsDevice, + (uint) width, + (uint) height, + TextureFormat.R8, + TextureUsageFlags.Sampler + ); + } + + private void InitializeDav1dStream() + { + Stream.Load(Video.Filename); + currentFrame = -1; + } + + private void ResetDav1dStreams() + { + Stream.Reset(); + currentFrame = -1; + } + + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + Unload(); + + RenderTexture?.Dispose(); + yTexture?.Dispose(); + uTexture?.Dispose(); + vTexture?.Dispose(); + } + } + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Video/VideoState.cs b/Nerfed.Runtime/Video/VideoState.cs new file mode 100644 index 0000000..bcff21f --- /dev/null +++ b/Nerfed.Runtime/Video/VideoState.cs @@ -0,0 +1,8 @@ +namespace Nerfed.Runtime.Video; + +public enum VideoState +{ + Playing, + Paused, + Stopped +} \ No newline at end of file diff --git a/Nerfed.Runtime/Window/ScreenMode.cs b/Nerfed.Runtime/Window/ScreenMode.cs new file mode 100644 index 0000000..e9a34ff --- /dev/null +++ b/Nerfed.Runtime/Window/ScreenMode.cs @@ -0,0 +1,8 @@ +namespace Nerfed.Runtime; + +public enum ScreenMode +{ + Fullscreen, + BorderlessFullscreen, + Windowed +} diff --git a/Nerfed.Runtime/Window/Window.cs b/Nerfed.Runtime/Window/Window.cs new file mode 100644 index 0000000..42b9066 --- /dev/null +++ b/Nerfed.Runtime/Window/Window.cs @@ -0,0 +1,234 @@ +using Nerfed.Runtime.Graphics; +using SDL2; + +namespace Nerfed.Runtime; + + +/// +/// Represents a window in the client operating system.
+/// Every Game has a MainWindow automatically.
+/// You can create additional Windows if you desire. They must be Claimed by the GraphicsDevice to be rendered to. +///
+public class Window +{ + internal IntPtr Handle { get; } + public ScreenMode ScreenMode { get; private set; } + public uint Width { get; private set; } + public uint Height { get; private set; } + internal Texture SwapchainTexture { get; set; } + + public bool Claimed { get; internal set; } + public SwapchainComposition SwapchainComposition { get; internal set; } + public TextureFormat SwapchainFormat { get; internal set; } + + public (int, int) Position + { + get + { + SDL.SDL_GetWindowPosition(Handle, out int x, out int y); + return (x, y); + } + } + + public string Title { get; private set;} + + private bool IsDisposed; + private System.Action SizeChangeCallback = null; + + private static readonly Dictionary windowsById = new Dictionary(); + + public Window(GraphicsDevice graphicsDevice, WindowCreateInfo windowCreateInfo) + { + SDL.SDL_WindowFlags flags = SDL.SDL_WindowFlags.SDL_WINDOW_SHOWN; + + if (graphicsDevice.Backend == BackendFlags.Vulkan) + { + flags |= SDL.SDL_WindowFlags.SDL_WINDOW_VULKAN; + } + else if (graphicsDevice.Backend == BackendFlags.Metal) + { + flags |= SDL.SDL_WindowFlags.SDL_WINDOW_METAL; + } + + if (windowCreateInfo.ScreenMode == ScreenMode.Fullscreen) + { + flags |= SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN; + } + else if (windowCreateInfo.ScreenMode == ScreenMode.BorderlessFullscreen) + { + flags |= SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP; + } + + if (windowCreateInfo.SystemResizable) + { + flags |= SDL.SDL_WindowFlags.SDL_WINDOW_RESIZABLE; + } + + if (windowCreateInfo.StartMaximized) + { + flags |= SDL.SDL_WindowFlags.SDL_WINDOW_MAXIMIZED; + } + + ScreenMode = windowCreateInfo.ScreenMode; + + SDL.SDL_GetDesktopDisplayMode(0, out SDL.SDL_DisplayMode displayMode); + + Handle = SDL.SDL_CreateWindow( + windowCreateInfo.WindowTitle, + SDL.SDL_WINDOWPOS_CENTERED, + SDL.SDL_WINDOWPOS_CENTERED, + windowCreateInfo.ScreenMode == ScreenMode.Windowed ? (int) windowCreateInfo.WindowWidth : displayMode.w, + windowCreateInfo.ScreenMode == ScreenMode.Windowed ? (int) windowCreateInfo.WindowHeight : displayMode.h, + flags + ); + + /* Requested size might be different in fullscreen, so let's just get the area */ + SDL.SDL_GetWindowSize(Handle, out int width, out int height); + Width = (uint) width; + Height = (uint) height; + + windowsById.Add(SDL.SDL_GetWindowID(Handle), this); + } + + internal static void ProcessEvent(ref SDL.SDL_Event ev) + { + uint windowId = ev.window.windowID; + if (!windowsById.TryGetValue(windowId, out Window window)) + { + Log.Error($"Received window event for unknown window id {windowsById}"); + return; + } + + switch (ev.window.windowEvent) + { + case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED: + window.ProcessSizeChangedEvent(ref ev.window); + break; + case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE: + window.ProcessCloseEvent(ref ev.window); + break; + } + } + + private void ProcessSizeChangedEvent(ref SDL.SDL_WindowEvent ev) + { + uint newWidth = (uint)ev.data1; + uint newHeight = (uint)ev.data2; + Width = newWidth; + Height = newHeight; + + if (SizeChangeCallback != null) + { + SizeChangeCallback(newWidth, newHeight); + } + } + + private void ProcessCloseEvent(ref SDL.SDL_WindowEvent ev) + { + Engine.GraphicsDevice.UnclaimWindow(this); + Dispose(); + } + + /// + /// Changes the ScreenMode of this window. + /// + public void SetScreenMode(ScreenMode screenMode) + { + SDL.SDL_WindowFlags windowFlag = 0; + + if (screenMode == ScreenMode.Fullscreen) + { + windowFlag = SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN; + } + else if (screenMode == ScreenMode.BorderlessFullscreen) + { + windowFlag = SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP; + } + + SDL.SDL_SetWindowFullscreen(Handle, (uint) windowFlag); + + if (screenMode == ScreenMode.Windowed) + { + SDL.SDL_SetWindowPosition(Handle, SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED); + } + + ScreenMode = screenMode; + } + + /// + /// Resizes the window.
+ /// Note that you are responsible for recreating any graphics resources that need to change as a result of the size change. + ///
+ /// + /// + public void SetSize(uint width, uint height) + { + SDL.SDL_SetWindowSize(Handle, (int) width, (int) height); + Width = width; + Height = height; + + if (ScreenMode == ScreenMode.Windowed) + { + SDL.SDL_SetWindowPosition(Handle, SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED); + } + } + + /// + /// Sets the window position. + /// + public void SetPosition(int x, int y) + { + SDL.SDL_SetWindowPosition(Handle, x, y); + } + + /// + /// Sets the window title. + /// + public void SetTitle(string title) + { + SDL.SDL_SetWindowTitle(Handle, title); + Title = title; + } + + internal void Show() + { + SDL.SDL_ShowWindow(Handle); + } + + /// + /// You can specify a method to run when the window size changes. + /// + public void RegisterSizeChangeCallback(System.Action sizeChangeCallback) + { + SizeChangeCallback = sizeChangeCallback; + } + + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + // dispose managed state (managed objects) + } + + windowsById.Remove(SDL.SDL_GetWindowID(Handle)); + SDL.SDL_DestroyWindow(Handle); + + IsDisposed = true; + } + } + + ~Window() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/Nerfed.Runtime/Window/WindowCreateInfo.cs b/Nerfed.Runtime/Window/WindowCreateInfo.cs new file mode 100644 index 0000000..e6c7ae7 --- /dev/null +++ b/Nerfed.Runtime/Window/WindowCreateInfo.cs @@ -0,0 +1,48 @@ +namespace Nerfed.Runtime; + +/// +/// All the information required for window creation. +/// +public struct WindowCreateInfo +{ + /// + /// The name of the window that will be displayed in the operating system. + /// + public string WindowTitle; + /// + /// The width of the window. + /// + public uint WindowWidth; + /// + /// The height of the window. + /// + public uint WindowHeight; + /// + /// Specifies if the window will be created in windowed mode or a fullscreen mode. + /// + public ScreenMode ScreenMode; + /// + /// Whether the window can be resized using the operating system's window dragging feature. + /// + public bool SystemResizable; + /// + /// Specifies if the window will open at the maximum desktop resolution. + /// + public bool StartMaximized; + + public WindowCreateInfo( + string windowTitle, + uint windowWidth, + uint windowHeight, + ScreenMode screenMode, + bool systemResizable = false, + bool startMaximized = false + ) { + WindowTitle = windowTitle; + WindowWidth = windowWidth; + WindowHeight = windowHeight; + ScreenMode = screenMode; + SystemResizable = systemResizable; + StartMaximized = startMaximized; + } +}