diff --git a/.gitmodules b/.gitmodules
index 481755f..e323a49 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -10,3 +10,6 @@
[submodule "lib/WellspringCS"]
path = lib/WellspringCS
url = https://gitea.moonside.games/MoonsideGames/WellspringCS.git
+[submodule "lib/Theorafile"]
+ path = lib/Theorafile
+ url = https://github.com/FNA-XNA/Theorafile.git
diff --git a/MoonWorks.csproj b/MoonWorks.csproj
index 88105d4..31c1fce 100644
--- a/MoonWorks.csproj
+++ b/MoonWorks.csproj
@@ -15,6 +15,7 @@
+
@@ -22,4 +23,13 @@
PreserveNewest
+
+
+
+ MoonWorks.Shaders.FullscreenVert.spv
+
+
+ MoonWorks.Shaders.YUV2RGBAFrag.spv
+
+
diff --git a/MoonWorks.dll.config b/MoonWorks.dll.config
index d384162..c076485 100644
--- a/MoonWorks.dll.config
+++ b/MoonWorks.dll.config
@@ -15,4 +15,8 @@
+
+
+
+
diff --git a/lib/Theorafile b/lib/Theorafile
new file mode 160000
index 0000000..dd8c7fa
--- /dev/null
+++ b/lib/Theorafile
@@ -0,0 +1 @@
+Subproject commit dd8c7fa69e678b6182cdaa71458ad08dd31c65da
diff --git a/src/Audio/SoundInstance.cs b/src/Audio/SoundInstance.cs
index 03a09ec..caba229 100644
--- a/src/Audio/SoundInstance.cs
+++ b/src/Audio/SoundInstance.cs
@@ -1,6 +1,5 @@
using System;
using System.Runtime.InteropServices;
-using MoonWorks.Math.Float;
namespace MoonWorks.Audio
{
@@ -8,13 +7,12 @@ namespace MoonWorks.Audio
{
internal IntPtr Handle;
internal FAudio.FAudioWaveFormatEx Format;
- public bool Loop { get; protected set; } = false;
protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings;
public bool Is3D { get; protected set; }
- public abstract SoundState State { get; protected set; }
+ public virtual SoundState State { get; protected set; }
private float _pan = 0;
public float Pan
@@ -238,11 +236,10 @@ namespace MoonWorks.Audio
);
}
- public abstract void Play(bool loop);
+ public abstract void Play();
public abstract void Pause();
- public abstract void Stop(bool immediate);
- public abstract void Seek(float seconds);
- public abstract void Seek(uint sampleFrame);
+ public abstract void Stop();
+ public abstract void StopImmediate();
private void InitDSPSettings(uint srcChannels)
{
@@ -345,8 +342,7 @@ namespace MoonWorks.Audio
protected override void Destroy()
{
- Stop(true);
-
+ StopImmediate();
FAudio.FAudioVoice_DestroyVoice(Handle);
Marshal.FreeHGlobal(dspSettings.pMatrixCoefficients);
}
diff --git a/src/Audio/StaticSoundInstance.cs b/src/Audio/StaticSoundInstance.cs
index 1afbb0d..7f52a97 100644
--- a/src/Audio/StaticSoundInstance.cs
+++ b/src/Audio/StaticSoundInstance.cs
@@ -6,6 +6,8 @@ namespace MoonWorks.Audio
{
public StaticSound Parent { get; }
+ public bool Loop { get; set; }
+
private SoundState _state = SoundState.Stopped;
public override SoundState State
{
@@ -18,7 +20,7 @@ namespace MoonWorks.Audio
);
if (state.BuffersQueued == 0)
{
- Stop(true);
+ StopImmediate();
}
return _state;
@@ -38,15 +40,13 @@ namespace MoonWorks.Audio
Parent = parent;
}
- public override void Play(bool loop = false)
+ public override void Play()
{
if (State == SoundState.Playing)
{
return;
}
- Loop = loop;
-
if (Loop)
{
Parent.Handle.LoopCount = 255;
@@ -79,21 +79,20 @@ namespace MoonWorks.Audio
}
}
- public override void Stop(bool immediate = true)
+ public override void Stop()
{
- if (immediate)
- {
- FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
- FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
- State = SoundState.Stopped;
- }
- else
- {
- FAudio.FAudioSourceVoice_ExitLoop(Handle, 0);
- }
+ FAudio.FAudioSourceVoice_ExitLoop(Handle, 0);
+ State = SoundState.Stopped;
}
- private void PerformSeek(uint sampleFrame)
+ public override void StopImmediate()
+ {
+ FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
+ FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
+ State = SoundState.Stopped;
+ }
+
+ public void Seek(uint sampleFrame)
{
if (State == SoundState.Playing)
{
@@ -102,20 +101,6 @@ namespace MoonWorks.Audio
}
Parent.Handle.PlayBegin = sampleFrame;
- Play();
- }
-
- public override void Seek(float seconds)
- {
- uint sampleFrame =
- (uint) (Parent.SamplesPerSecond * seconds);
-
- PerformSeek(sampleFrame);
- }
-
- public override void Seek(uint sampleFrame)
- {
- PerformSeek(sampleFrame);
}
public void Free()
diff --git a/src/Audio/StreamingSound.cs b/src/Audio/StreamingSound.cs
index 18a5a33..4982544 100644
--- a/src/Audio/StreamingSound.cs
+++ b/src/Audio/StreamingSound.cs
@@ -1,38 +1,46 @@
using System;
-using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
///
/// For streaming long playback.
- /// Can be extended to support custom decoders.
+ /// Must be extended with a decoder routine called by FillBuffer.
+ /// See StreamingSoundOgg for an example.
///
public abstract class StreamingSound : SoundInstance
{
- private readonly List queuedBuffers = new List();
- private readonly List queuedSizes = new List();
- private const int MINIMUM_BUFFER_CHECK = 3;
+ private const int BUFFER_COUNT = 3;
+ private readonly IntPtr[] buffers;
+ private int nextBufferIndex = 0;
+ private uint queuedBufferCount = 0;
+ protected abstract int BUFFER_SIZE { get; }
- public int PendingBufferCount => queuedBuffers.Count;
-
- public StreamingSound(
+ public unsafe StreamingSound(
AudioDevice device,
ushort formatTag,
ushort bitsPerSample,
ushort blockAlign,
ushort channels,
uint samplesPerSecond
- ) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond) { }
+ ) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond)
+ {
+ device.AddDynamicSoundInstance(this);
- public override void Play(bool loop = false)
+ buffers = new IntPtr[BUFFER_COUNT];
+ for (int i = 0; i < BUFFER_COUNT; i += 1)
+ {
+ buffers[i] = (IntPtr) NativeMemory.Alloc((nuint) BUFFER_SIZE);
+ }
+ }
+
+ public override void Play()
{
if (State == SoundState.Playing)
{
return;
}
- Loop = loop;
State = SoundState.Playing;
Update();
@@ -48,19 +56,21 @@ namespace MoonWorks.Audio
}
}
- public override void Stop(bool immediate = true)
+ public override void Stop()
{
- if (immediate)
- {
- FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
- FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
- ClearBuffers();
- }
+ State = SoundState.Stopped;
+ }
+
+ public override void StopImmediate()
+ {
+ FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
+ FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
+ ClearBuffers();
State = SoundState.Stopped;
}
- internal void Update()
+ internal unsafe void Update()
{
if (State != SoundState.Playing)
{
@@ -73,109 +83,83 @@ namespace MoonWorks.Audio
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
);
- while (PendingBufferCount > state.BuffersQueued)
- lock (queuedBuffers)
- {
- Marshal.FreeHGlobal(queuedBuffers[0]);
- queuedBuffers.RemoveAt(0);
- }
+ queuedBufferCount = state.BuffersQueued;
QueueBuffers();
}
protected void QueueBuffers()
{
- for (
- int i = MINIMUM_BUFFER_CHECK - PendingBufferCount;
- i > 0;
- i -= 1
- )
+ for (int i = 0; i < BUFFER_COUNT - queuedBufferCount; i += 1)
{
AddBuffer();
}
}
- protected void ClearBuffers()
+ protected unsafe void ClearBuffers()
{
- lock (queuedBuffers)
- {
- foreach (IntPtr buf in queuedBuffers)
- {
- Marshal.FreeHGlobal(buf);
- }
- queuedBuffers.Clear();
- queuedSizes.Clear();
- }
+ nextBufferIndex = 0;
+ queuedBufferCount = 0;
}
- protected void AddBuffer()
+ protected unsafe void AddBuffer()
{
- AddBuffer(
- out var buffer,
- out var bufferOffset,
- out var bufferLength,
- out var reachedEnd
+ var buffer = buffers[nextBufferIndex];
+ nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT;
+
+ FillBuffer(
+ (void*) buffer,
+ BUFFER_SIZE,
+ out int filledLengthInBytes,
+ out bool reachedEnd
);
- var lengthInBytes = bufferLength * sizeof(float);
-
- IntPtr next = Marshal.AllocHGlobal((int) lengthInBytes);
- Marshal.Copy(buffer, (int) bufferOffset, next, (int) bufferLength);
-
- lock (queuedBuffers)
+ FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer
{
- queuedBuffers.Add(next);
- if (State != SoundState.Stopped)
- {
- FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer
- {
- AudioBytes = lengthInBytes,
- pAudioData = next,
- PlayLength = (
- lengthInBytes /
- Format.nChannels /
- (uint) (Format.wBitsPerSample / 8)
- )
- };
+ AudioBytes = (uint) filledLengthInBytes,
+ pAudioData = (IntPtr) buffer,
+ PlayLength = (
+ (uint) (filledLengthInBytes /
+ Format.nChannels /
+ (uint) (Format.wBitsPerSample / 8))
+ )
+ };
- FAudio.FAudioSourceVoice_SubmitSourceBuffer(
- Handle,
- ref buf,
- IntPtr.Zero
- );
- }
- else
- {
- queuedSizes.Add(lengthInBytes);
- }
- }
+ FAudio.FAudioSourceVoice_SubmitSourceBuffer(
+ Handle,
+ ref buf,
+ IntPtr.Zero
+ );
+
+ queuedBufferCount += 1;
/* We have reached the end of the file, what do we do? */
if (reachedEnd)
{
- if (Loop)
- {
- SeekStart();
- }
- else
- {
- Stop(false);
- }
+ OnReachedEnd();
}
}
- protected abstract void AddBuffer(
- out float[] buffer,
- out uint bufferOffset, /* in floats */
- out uint bufferLength, /* in floats */
+ protected virtual void OnReachedEnd()
+ {
+ Stop();
+ }
+
+ protected unsafe abstract void FillBuffer(
+ void* buffer,
+ int bufferLengthInBytes, /* in bytes */
+ out int filledLengthInBytes, /* in bytes */
out bool reachedEnd
);
- protected abstract void SeekStart();
-
- protected override void Destroy()
+ protected unsafe override void Destroy()
{
- Stop(true);
+ StopImmediate();
+
+ for (int i = 0; i < BUFFER_COUNT; i += 1)
+ {
+ NativeMemory.Free((void*) buffers[i]);
+ }
}
}
}
diff --git a/src/Audio/StreamingSoundOgg.cs b/src/Audio/StreamingSoundOgg.cs
index 8aa4721..1b44e55 100644
--- a/src/Audio/StreamingSoundOgg.cs
+++ b/src/Audio/StreamingSoundOgg.cs
@@ -4,28 +4,23 @@ using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
- public class StreamingSoundOgg : StreamingSound
+ public class StreamingSoundOgg : StreamingSoundSeekable
{
- // FIXME: what should this value be?
- public const int BUFFER_SIZE = 1024 * 128;
-
private IntPtr VorbisHandle;
private IntPtr FileDataPtr;
private FAudio.stb_vorbis_info Info;
- private readonly float[] buffer; // currently decoded bytes
+ protected override int BUFFER_SIZE => 32768;
- public override SoundState State { get; protected set; }
-
- public static StreamingSoundOgg Load(AudioDevice device, string filePath)
+ public unsafe static StreamingSoundOgg Load(AudioDevice device, string filePath)
{
var fileData = File.ReadAllBytes(filePath);
- var fileDataPtr = Marshal.AllocHGlobal(fileData.Length);
- Marshal.Copy(fileData, 0, fileDataPtr, fileData.Length);
- var vorbisHandle = FAudio.stb_vorbis_open_memory(fileDataPtr, fileData.Length, out int error, IntPtr.Zero);
+ var fileDataPtr = NativeMemory.Alloc((nuint) fileData.Length);
+ Marshal.Copy(fileData, 0, (IntPtr) fileDataPtr, fileData.Length);
+ var vorbisHandle = FAudio.stb_vorbis_open_memory((IntPtr) fileDataPtr, fileData.Length, out int error, IntPtr.Zero);
if (error != 0)
{
- ((GCHandle) fileDataPtr).Free();
+ NativeMemory.Free(fileDataPtr);
Logger.LogError("Error opening OGG file!");
Logger.LogError("Error: " + error);
throw new AudioLoadException("Error opening OGG file!");
@@ -34,7 +29,7 @@ namespace MoonWorks.Audio
return new StreamingSoundOgg(
device,
- fileDataPtr,
+ (IntPtr) fileDataPtr,
vorbisHandle,
info
);
@@ -42,7 +37,7 @@ namespace MoonWorks.Audio
internal StreamingSoundOgg(
AudioDevice device,
- IntPtr fileDataPtr, // MUST BE AN ALLOCHGLOBAL HANDLE!!
+ IntPtr fileDataPtr, // MUST BE A NATIVE MEMORY HANDLE!!
IntPtr vorbisHandle,
FAudio.stb_vorbis_info info
) : base(
@@ -57,12 +52,9 @@ namespace MoonWorks.Audio
FileDataPtr = fileDataPtr;
VorbisHandle = vorbisHandle;
Info = info;
- buffer = new float[BUFFER_SIZE];
-
- device.AddDynamicSoundInstance(this);
}
- private void PerformSeek(uint sampleFrame)
+ public override void Seek(uint sampleFrame)
{
if (State == SoundState.Playing)
{
@@ -80,49 +72,32 @@ namespace MoonWorks.Audio
}
}
- public override void Seek(float seconds)
- {
- uint sampleFrame = (uint) (Info.sample_rate * seconds);
- PerformSeek(sampleFrame);
- }
-
- public override void Seek(uint sampleFrame)
- {
- PerformSeek(sampleFrame);
- }
-
- protected override void AddBuffer(
- out float[] buffer,
- out uint bufferOffset,
- out uint bufferLength,
+ protected unsafe override void FillBuffer(
+ void* buffer,
+ int bufferLengthInBytes,
+ out int filledLengthInBytes,
out bool reachedEnd
)
{
- buffer = this.buffer;
+ var lengthInFloats = bufferLengthInBytes / sizeof(float);
/* NOTE: this function returns samples per channel, not total samples */
var samples = FAudio.stb_vorbis_get_samples_float_interleaved(
VorbisHandle,
Info.channels,
- buffer,
- buffer.Length
+ (IntPtr) buffer,
+ lengthInFloats
);
var sampleCount = samples * Info.channels;
- bufferOffset = 0;
- bufferLength = (uint) sampleCount;
- reachedEnd = sampleCount < buffer.Length;
+ reachedEnd = sampleCount < lengthInFloats;
+ filledLengthInBytes = sampleCount * sizeof(float);
}
- protected override void SeekStart()
- {
- FAudio.stb_vorbis_seek_start(VorbisHandle);
- }
-
- protected override void Destroy()
+ protected unsafe override void Destroy()
{
FAudio.stb_vorbis_close(VorbisHandle);
- Marshal.FreeHGlobal(FileDataPtr);
+ NativeMemory.Free((void*) FileDataPtr);
}
}
}
diff --git a/src/Audio/StreamingSoundSeekable.cs b/src/Audio/StreamingSoundSeekable.cs
new file mode 100644
index 0000000..7ca563b
--- /dev/null
+++ b/src/Audio/StreamingSoundSeekable.cs
@@ -0,0 +1,25 @@
+namespace MoonWorks.Audio
+{
+ public abstract class StreamingSoundSeekable : StreamingSound
+ {
+ public bool Loop { get; set; }
+
+ protected StreamingSoundSeekable(AudioDevice device, ushort formatTag, ushort bitsPerSample, ushort blockAlign, ushort channels, uint samplesPerSecond) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond)
+ {
+ }
+
+ public abstract void Seek(uint sampleFrame);
+
+ protected override void OnReachedEnd()
+ {
+ if (Loop)
+ {
+ Seek(0);
+ }
+ else
+ {
+ Stop();
+ }
+ }
+ }
+}
diff --git a/src/Graphics/CommandBuffer.cs b/src/Graphics/CommandBuffer.cs
index bcd698a..598d50b 100644
--- a/src/Graphics/CommandBuffer.cs
+++ b/src/Graphics/CommandBuffer.cs
@@ -1,6 +1,4 @@
using System;
-using System.Runtime.InteropServices;
-using MoonWorks.Math;
using RefreshCS;
namespace MoonWorks.Graphics
@@ -835,6 +833,26 @@ namespace MoonWorks.Graphics
SetTextureData(new TextureSlice(texture), dataPtr, dataLengthInBytes);
}
+ ///
+ /// Asynchronously copies YUV data into three textures. Use with compressed video.
+ ///
+ public void SetTextureDataYUV(Texture yTexture, Texture uTexture, Texture vTexture, IntPtr dataPtr, uint dataLengthInBytes)
+ {
+ Refresh.Refresh_SetTextureDataYUV(
+ Device.Handle,
+ Handle,
+ yTexture.Handle,
+ uTexture.Handle,
+ vTexture.Handle,
+ yTexture.Width,
+ yTexture.Height,
+ uTexture.Width,
+ uTexture.Height,
+ dataPtr,
+ dataLengthInBytes
+ );
+ }
+
///
/// Performs an asynchronous texture-to-texture copy on the GPU.
///
diff --git a/src/Graphics/GraphicsDevice.cs b/src/Graphics/GraphicsDevice.cs
index 99915b4..c111c9e 100644
--- a/src/Graphics/GraphicsDevice.cs
+++ b/src/Graphics/GraphicsDevice.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.IO;
using RefreshCS;
namespace MoonWorks.Graphics
@@ -8,6 +9,11 @@ namespace MoonWorks.Graphics
{
public IntPtr Handle { get; }
+ // Built-in video pipeline
+ private ShaderModule VideoVertexShader { get; }
+ private ShaderModule VideoFragmentShader { get; }
+ internal GraphicsPipeline VideoPipeline { get; }
+
public bool IsDisposed { get; private set; }
private readonly List> resources = new List>();
@@ -28,6 +34,26 @@ namespace MoonWorks.Graphics
presentationParameters,
Conversions.BoolToByte(debugMode)
);
+
+ VideoVertexShader = new ShaderModule(this, GetEmbeddedResource("MoonWorks.Shaders.FullscreenVert.spv"));
+ VideoFragmentShader = new ShaderModule(this, GetEmbeddedResource("MoonWorks.Shaders.YUV2RGBAFrag.spv"));
+
+ VideoPipeline = new GraphicsPipeline(
+ this,
+ new GraphicsPipelineCreateInfo
+ {
+ AttachmentInfo = new GraphicsPipelineAttachmentInfo(
+ new ColorAttachmentDescription(TextureFormat.R8G8B8A8, ColorAttachmentBlendState.None)
+ ),
+ DepthStencilState = DepthStencilState.Disable,
+ VertexShaderInfo = GraphicsShaderInfo.Create(VideoVertexShader, "main", 0),
+ FragmentShaderInfo = GraphicsShaderInfo.Create(VideoFragmentShader, "main", 3),
+ VertexInputState = VertexInputState.Empty,
+ RasterizerState = RasterizerState.CCW_CullNone,
+ PrimitiveType = PrimitiveType.TriangleList,
+ MultisampleState = MultisampleState.None
+ }
+ );
}
public CommandBuffer AcquireCommandBuffer()
@@ -77,6 +103,11 @@ namespace MoonWorks.Graphics
}
}
+ private static Stream GetEmbeddedResource(string name)
+ {
+ return typeof(GraphicsDevice).Assembly.GetManifestResourceStream(name);
+ }
+
protected virtual void Dispose(bool disposing)
{
if (!IsDisposed)
diff --git a/src/Input/Gamepad.cs b/src/Input/Gamepad.cs
index e6d15d9..7b53b3f 100644
--- a/src/Input/Gamepad.cs
+++ b/src/Input/Gamepad.cs
@@ -159,6 +159,12 @@ namespace MoonWorks.Input
{ AxisButtonCode.RightY_Down, RightYDown }
};
+ TriggerCodeToTriggerButton = new Dictionary
+ {
+ { TriggerCode.Left, TriggerLeftButton },
+ { TriggerCode.Right, TriggerRightButton }
+ };
+
VirtualButtons = new VirtualButton[]
{
A,
diff --git a/src/MoonWorksDllMap.cs b/src/MoonWorksDllMap.cs
index 5164838..fd9b0f3 100644
--- a/src/MoonWorksDllMap.cs
+++ b/src/MoonWorksDllMap.cs
@@ -196,6 +196,7 @@ namespace MoonWorks
NativeLibrary.SetDllImportResolver(typeof(RefreshCS.Refresh).Assembly, MapAndLoad);
NativeLibrary.SetDllImportResolver(typeof(FAudio).Assembly, MapAndLoad);
NativeLibrary.SetDllImportResolver(typeof(WellspringCS.Wellspring).Assembly, MapAndLoad);
+ NativeLibrary.SetDllImportResolver(typeof(Theorafile).Assembly, MapAndLoad);
}
#endregion
diff --git a/src/Video/Shaders/Compiled/FullscreenVert.spv b/src/Video/Shaders/Compiled/FullscreenVert.spv
new file mode 100644
index 0000000..ffc57de
Binary files /dev/null and b/src/Video/Shaders/Compiled/FullscreenVert.spv differ
diff --git a/src/Video/Shaders/Compiled/YUV2RGBAFrag.spv b/src/Video/Shaders/Compiled/YUV2RGBAFrag.spv
new file mode 100644
index 0000000..c9fbf32
Binary files /dev/null and b/src/Video/Shaders/Compiled/YUV2RGBAFrag.spv differ
diff --git a/src/Video/Shaders/Source/Fullscreen.vert b/src/Video/Shaders/Source/Fullscreen.vert
new file mode 100644
index 0000000..2f3c315
--- /dev/null
+++ b/src/Video/Shaders/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 * 2.0 - 1.0, 0.0, 1.0);
+}
diff --git a/src/Video/Shaders/Source/YUV2RGBA.frag b/src/Video/Shaders/Source/YUV2RGBA.frag
new file mode 100644
index 0000000..fe2b5a1
--- /dev/null
+++ b/src/Video/Shaders/Source/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(binding = 0, set = 1) uniform sampler2D YSampler;
+layout(binding = 1, set = 1) uniform sampler2D USampler;
+layout(binding = 2, set = 1) 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/src/Video/StreamingSoundTheora.cs b/src/Video/StreamingSoundTheora.cs
new file mode 100644
index 0000000..a49c939
--- /dev/null
+++ b/src/Video/StreamingSoundTheora.cs
@@ -0,0 +1,45 @@
+using System;
+using MoonWorks.Audio;
+
+namespace MoonWorks.Video
+{
+ public unsafe class StreamingSoundTheora : StreamingSound
+ {
+ private IntPtr VideoHandle;
+ protected override int BUFFER_SIZE => 8192;
+
+ internal StreamingSoundTheora(
+ AudioDevice device,
+ IntPtr videoHandle,
+ int channels,
+ uint sampleRate
+ ) : base(
+ device,
+ 3, /* float type */
+ 32, /* size of float */
+ (ushort) (4 * channels),
+ (ushort) channels,
+ sampleRate
+ ) {
+ VideoHandle = videoHandle;
+ }
+
+ protected override unsafe void FillBuffer(
+ void* buffer,
+ int bufferLengthInBytes,
+ out int filledLengthInBytes,
+ out bool reachedEnd
+ ) {
+ var lengthInFloats = bufferLengthInBytes / sizeof(float);
+
+ int samples = Theorafile.tf_readaudio(
+ VideoHandle,
+ (IntPtr) buffer,
+ lengthInFloats
+ );
+
+ filledLengthInBytes = samples * sizeof(float);
+ reachedEnd = Theorafile.tf_eos(VideoHandle) == 1;
+ }
+ }
+}
diff --git a/src/Video/Video.cs b/src/Video/Video.cs
new file mode 100644
index 0000000..01dba5b
--- /dev/null
+++ b/src/Video/Video.cs
@@ -0,0 +1,357 @@
+/* Heavily based on https://github.com/FNA-XNA/FNA/blob/master/src/Media/Xiph/VideoPlayer.cs */
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using MoonWorks.Audio;
+using MoonWorks.Graphics;
+
+namespace MoonWorks.Video
+{
+ public enum VideoState
+ {
+ Playing,
+ Paused,
+ Stopped
+ }
+
+ public unsafe class Video : IDisposable
+ {
+ internal IntPtr Handle;
+
+ public bool Loop { get; private set; }
+ public float Volume {
+ get => volume;
+ set
+ {
+ volume = value;
+ if (audioStream != null)
+ {
+ audioStream.Volume = value;
+ }
+ }
+ }
+ public float PlaybackSpeed { get; set; }
+ public double FramesPerSecond => fps;
+ private VideoState State = VideoState.Stopped;
+
+ private double fps;
+ private int yWidth;
+ private int yHeight;
+ private int uvWidth;
+ private int uvHeight;
+
+ private void* yuvData = null;
+ private int yuvDataLength;
+ private int currentFrame;
+
+ private GraphicsDevice GraphicsDevice;
+ private Texture RenderTexture = null;
+ private Texture yTexture = null;
+ private Texture uTexture = null;
+ private Texture vTexture = null;
+ private Sampler LinearSampler;
+
+ private AudioDevice AudioDevice = null;
+ private StreamingSoundTheora audioStream = null;
+ private float volume = 1.0f;
+
+ private Stopwatch timer;
+ private double lastTimestamp;
+ private double timeElapsed;
+
+ private bool disposed;
+
+ /* TODO: is there some way for us to load the data into memory? */
+ public Video(GraphicsDevice graphicsDevice, AudioDevice audioDevice, string filename)
+ {
+ GraphicsDevice = graphicsDevice;
+ AudioDevice = audioDevice;
+
+ if (!System.IO.File.Exists(filename))
+ {
+ throw new ArgumentException("Video file not found!");
+ }
+
+ if (Theorafile.tf_fopen(filename, out Handle) < 0)
+ {
+ throw new ArgumentException("Invalid video file!");
+ }
+
+ Theorafile.th_pixel_fmt format;
+ Theorafile.tf_videoinfo(
+ Handle,
+ out yWidth,
+ out yHeight,
+ out fps,
+ out format
+ );
+
+ if (format == Theorafile.th_pixel_fmt.TH_PF_420)
+ {
+ uvWidth = yWidth / 2;
+ uvHeight = yHeight / 2;
+ }
+ else if (format == Theorafile.th_pixel_fmt.TH_PF_422)
+ {
+ uvWidth = yWidth / 2;
+ uvHeight = yHeight;
+ }
+ else if (format == Theorafile.th_pixel_fmt.TH_PF_444)
+ {
+ uvWidth = yWidth;
+ uvHeight = yHeight;
+ }
+ else
+ {
+ throw new NotSupportedException("Unrecognized YUV format!");
+ }
+
+ yuvDataLength = (
+ (yWidth * yHeight) +
+ (uvWidth * uvHeight * 2)
+ );
+
+ yuvData = NativeMemory.Alloc((nuint) yuvDataLength);
+
+ InitializeTheoraStream();
+
+ if (Theorafile.tf_hasvideo(Handle) == 1)
+ {
+ RenderTexture = Texture.CreateTexture2D(
+ GraphicsDevice,
+ (uint) yWidth,
+ (uint) yHeight,
+ TextureFormat.R8G8B8A8,
+ TextureUsageFlags.ColorTarget | TextureUsageFlags.Sampler
+ );
+
+ yTexture = Texture.CreateTexture2D(
+ GraphicsDevice,
+ (uint) yWidth,
+ (uint) yHeight,
+ TextureFormat.R8,
+ TextureUsageFlags.Sampler
+ );
+
+ uTexture = Texture.CreateTexture2D(
+ GraphicsDevice,
+ (uint) uvWidth,
+ (uint) uvHeight,
+ TextureFormat.R8,
+ TextureUsageFlags.Sampler
+ );
+
+ vTexture = Texture.CreateTexture2D(
+ GraphicsDevice,
+ (uint) uvWidth,
+ (uint) uvHeight,
+ TextureFormat.R8,
+ TextureUsageFlags.Sampler
+ );
+
+ LinearSampler = new Sampler(GraphicsDevice, SamplerCreateInfo.LinearClamp);
+ }
+
+ timer = new Stopwatch();
+ }
+
+ public void Play(bool loop = false)
+ {
+ if (State == VideoState.Playing)
+ {
+ return;
+ }
+
+ Loop = loop;
+ timer.Start();
+
+ if (audioStream != null)
+ {
+ audioStream.Play();
+ }
+
+ State = VideoState.Playing;
+ }
+
+ public void Pause()
+ {
+ if (State != VideoState.Playing)
+ {
+ return;
+ }
+
+ timer.Stop();
+
+ if (audioStream != null)
+ {
+ audioStream.Pause();
+ }
+
+ State = VideoState.Paused;
+ }
+
+ public void Stop()
+ {
+ if (State == VideoState.Stopped)
+ {
+ return;
+ }
+
+ timer.Stop();
+ timer.Reset();
+
+ Theorafile.tf_reset(Handle);
+ lastTimestamp = 0;
+ timeElapsed = 0;
+
+ if (audioStream != null)
+ {
+ audioStream.StopImmediate();
+ audioStream.Dispose();
+ audioStream = null;
+ }
+
+ State = VideoState.Stopped;
+ }
+
+ public Texture GetTexture()
+ {
+ if (RenderTexture == null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ if (State == VideoState.Stopped)
+ {
+ return RenderTexture;
+ }
+
+ timeElapsed += (timer.Elapsed.TotalMilliseconds - lastTimestamp) * PlaybackSpeed;
+ lastTimestamp = timer.Elapsed.TotalMilliseconds;
+
+ int thisFrame = ((int) (timeElapsed / (1000.0 / FramesPerSecond)));
+ if (thisFrame > currentFrame)
+ {
+ if (Theorafile.tf_readvideo(
+ Handle,
+ (IntPtr) yuvData,
+ thisFrame - currentFrame
+ ) == 1 || currentFrame == -1) {
+ UpdateTexture();
+ }
+
+ currentFrame = thisFrame;
+ }
+
+ bool ended = Theorafile.tf_eos(Handle) == 1;
+ if (ended)
+ {
+ timer.Stop();
+ timer.Reset();
+
+ if (audioStream != null)
+ {
+ audioStream.Stop();
+ audioStream.Dispose();
+ audioStream = null;
+ }
+
+ Theorafile.tf_reset(Handle);
+
+ if (Loop)
+ {
+ // Start over!
+ InitializeTheoraStream();
+
+ timer.Start();
+ }
+ else
+ {
+ State = VideoState.Stopped;
+ }
+ }
+
+ return RenderTexture;
+ }
+
+ private void UpdateTexture()
+ {
+ var commandBuffer = GraphicsDevice.AcquireCommandBuffer();
+
+ commandBuffer.SetTextureDataYUV(
+ yTexture,
+ uTexture,
+ vTexture,
+ (IntPtr) yuvData,
+ (uint) yuvDataLength
+ );
+
+ commandBuffer.BeginRenderPass(
+ new ColorAttachmentInfo(RenderTexture, Color.Black)
+ );
+
+ commandBuffer.BindGraphicsPipeline(GraphicsDevice.VideoPipeline);
+ commandBuffer.BindFragmentSamplers(
+ new TextureSamplerBinding(yTexture, LinearSampler),
+ new TextureSamplerBinding(uTexture, LinearSampler),
+ new TextureSamplerBinding(vTexture, LinearSampler)
+ );
+
+ commandBuffer.DrawPrimitives(0, 1, 0, 0);
+
+ commandBuffer.EndRenderPass();
+
+ GraphicsDevice.Submit(commandBuffer);
+ }
+
+ private void InitializeTheoraStream()
+ {
+ // Grab the first video frame ASAP.
+ while (Theorafile.tf_readvideo(Handle, (IntPtr) yuvData, 1) == 0);
+
+ // Grab the first bit of audio. We're trying to start the decoding ASAP.
+ if (AudioDevice != null && Theorafile.tf_hasaudio(Handle) == 1)
+ {
+ int channels, sampleRate;
+ Theorafile.tf_audioinfo(Handle, out channels, out sampleRate);
+ audioStream = new StreamingSoundTheora(AudioDevice, Handle, channels, (uint) sampleRate);
+ }
+
+ currentFrame = -1;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposed)
+ {
+ if (disposing)
+ {
+ // dispose managed state (managed objects)
+ RenderTexture.Dispose();
+ yTexture.Dispose();
+ uTexture.Dispose();
+ vTexture.Dispose();
+ }
+
+ // free unmanaged resources (unmanaged objects)
+ Theorafile.tf_close(ref Handle);
+ NativeMemory.Free(yuvData);
+
+ disposed = true;
+ }
+ }
+
+ ~Video()
+ {
+ // 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);
+ }
+ }
+}