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 0000000..6f7d258
Binary files /dev/null and b/Nerfed.Runtime/Graphics/StockShaders/Binary/fullscreen.vert.spv differ
diff --git a/Nerfed.Runtime/Graphics/StockShaders/Binary/text_msdf.frag.spv b/Nerfed.Runtime/Graphics/StockShaders/Binary/text_msdf.frag.spv
new file mode 100644
index 0000000..91aef3b
Binary files /dev/null and b/Nerfed.Runtime/Graphics/StockShaders/Binary/text_msdf.frag.spv differ
diff --git a/Nerfed.Runtime/Graphics/StockShaders/Binary/text_transform.vert.spv b/Nerfed.Runtime/Graphics/StockShaders/Binary/text_transform.vert.spv
new file mode 100644
index 0000000..6e1063d
Binary files /dev/null and b/Nerfed.Runtime/Graphics/StockShaders/Binary/text_transform.vert.spv differ
diff --git a/Nerfed.Runtime/Graphics/StockShaders/Binary/video_yuv2rgba.frag.spv b/Nerfed.Runtime/Graphics/StockShaders/Binary/video_yuv2rgba.frag.spv
new file mode 100644
index 0000000..68346e2
Binary files /dev/null and b/Nerfed.Runtime/Graphics/StockShaders/Binary/video_yuv2rgba.frag.spv differ
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;
+ }
+}