Compare commits

...

111 Commits

Author SHA1 Message Date
cosmonaut bb7e45b9a3 expose Params on ReverbEffect 2024-02-09 13:53:33 -08:00
cosmonaut 4cedf768f7 fix AttackHoldRelease timing 2024-02-03 00:40:29 -08:00
cosmonaut d986b3013f fix TextBatch index buffers being created as vertex buffers 2024-01-27 17:34:26 -08:00
cosmonaut 42e3ac91af Vertex instance input shortcut (#53)
Reviewed-on: MoonsideGames/MoonWorks#53
2024-01-27 03:44:19 +00:00
cosmonaut 0df2944ccf CommandBuffer is now a pooled class + more state validation 2024-01-18 12:27:34 -08:00
cosmonaut e50fb472b1 Debug mode sample count and depth assertions 2024-01-15 23:16:29 -08:00
cosmonaut df3f38a67b Debug mode bounds checks for buffer and texture upload 2024-01-15 22:19:59 -08:00
Evan Hemsley eaa9266521 remove Marshal call from KeyboardButton.CheckPressed 2023-12-28 18:54:21 -08:00
cosmonaut 4dbd5a2cbe MSDF font rendering + improved resource tracking (#52)
This is a major rewrite of the Font system. MoonWorks now uses MSDF font rendering, which allows high quality rendering of fonts at arbitrary sizes.

We now ship default embedded shader binaries for Video and Font. If you replace them with shader binaries of the same name located in your base directory, those will be used instead.

Many improvements have been made to resource tracking to prevent memory corruption, particularly on shutdown.

You must be careful not to leak AudioResource classes in particular, as there isn't much we can automatically do to recover from this without potentially crashing your game.

Reviewed-on: MoonsideGames/MoonWorks#52
2023-12-15 18:46:43 +00:00
cosmonaut 2e890fd696 Remove x64 specification 2023-12-13 11:19:27 -08:00
cosmonaut 385783a846 restructure audio cleanup 2023-12-08 16:33:52 -08:00
cosmonaut 450b08cbd8 Atomically call graphics resource destroy function 2023-12-08 16:07:38 -08:00
cosmonaut 528fb7ac7c fix potential heap corruption on audio shutdown 2023-12-08 15:06:17 -08:00
cosmonaut fcd08fe231 .NET 8 2023-11-21 15:23:52 -08:00
cosmonaut e961a18a83 UpdatingSourceVoice + warn on audio leak 2023-11-20 18:56:22 -08:00
cosmonaut 772a0378bb avoid calling Thread.Join from finalizer 2023-11-20 17:59:26 -08:00
cosmonaut 40fb313d12 change AudioResource WeakRef to GCHandle 2023-11-20 17:52:44 -08:00
cosmonaut a736ed031d clean up graphics resource code 2023-11-20 17:09:22 -08:00
cosmonaut b2a0ca3515 replace WeakReference with weak GCHandle 2023-11-20 15:18:06 -08:00
cosmonaut 36a88afe52 add no-op delegate to gamepad connect events 2023-11-14 11:19:21 -08:00
cosmonaut 6c93350f7f Add OnGamepadConnected and OnGamepadDisconnected events (#51)
Reviewed-on: MoonsideGames/MoonWorks#51
2023-11-13 19:10:29 +00:00
cosmonaut 352bb34f82 update dll config for major versions 2023-10-18 12:15:30 -07:00
Evan Hemsley de7d76c03d update dll.config for macOS 2023-10-16 11:20:55 -07:00
cosmonaut 18d92aeec8 remove Fix64 dependency on System.Random 2023-10-13 13:38:26 -07:00
cosmonaut e616b0fa62 resource management and logging improvements 2023-10-04 14:45:17 -07:00
cosmonaut 1d27a9e4a4 update README with API docs and Discord 2023-09-26 09:44:32 -07:00
cosmonaut 514a0bed29 update FAudio and SDL 2023-09-25 10:03:40 -07:00
cosmonaut 78252d1f6c doxygen generator config 2023-09-19 17:55:17 -07:00
cosmonaut daae1a34b9 fix a few more compile errors 2023-09-19 17:14:48 -07:00
cosmonaut 2e5657789c fix a Conversions issue 2023-09-19 17:11:14 -07:00
cosmonaut 0c76c568a4 document the Game class 2023-09-19 17:04:28 -07:00
cosmonaut abdcac1608 change AudioDevice constructor to internal 2023-09-19 17:04:03 -07:00
cosmonaut d8064862bf move PackedVector classes to MoonWorks.Graphics.PackedVector 2023-09-19 16:50:08 -07:00
cosmonaut b223c31c8b even more frame limiter clarification 2023-09-19 13:48:50 -07:00
cosmonaut dd79090028 update frame limiter docs some more 2023-09-19 13:47:19 -07:00
cosmonaut 653f90c29f correct frame limiter documentation mistake 2023-09-19 13:46:41 -07:00
cosmonaut 402c26131d fix erroneous GetData length warning 2023-09-19 13:40:48 -07:00
cosmonaut b026b9e81f add lots more doc comments 2023-09-19 13:19:41 -07:00
cosmonaut e0f05881b0 new MoonWorks.Graphics.Fence API 2023-09-18 23:18:21 -07:00
cosmonaut 7e18764942 add doc comments for the Input namespace 2023-09-14 11:23:04 -07:00
cosmonaut 1bff459be6 move default reverb params to a static var 2023-09-12 15:24:45 -07:00
cosmonaut be77e8bad1 add exponentiation functions to Fix64 2023-09-07 17:30:35 -07:00
cosmonaut 7f6b6a7bae fix voices not respecting the faux mastering voice 2023-08-10 10:46:19 -07:00
cosmonaut 1de3c73bb7 fix voices being created in the Playing state 2023-08-09 16:10:00 -07:00
cosmonaut e77c87c772 register new SourceVoices as active 2023-08-09 15:58:18 -07:00
cosmonaut 088e7c4b6f add debug check for zero length buffer copy 2023-08-07 10:12:46 -07:00
cosmonaut f298a5ec11 fix StreamingVoice loop behavior 2023-08-04 12:14:14 -07:00
cosmonaut 0cd2c799ee Audio Restructuring (#50)
This is a complete redesign of the MoonWorks Audio API.

Voices are the new major concept. All Voices can be configured with volume, pitch, filters, panning and reverb. SourceVoices take in AudioBuffers and use them to play sound. They contain their own playback state.

There are multiple kinds of SourceVoices:
TransientVoice: Used for short sound effects where the client will not be keeping track of a reference over multiple frames.
PersistentVoice: Used when the client needs to hold on to a Voice reference long-term.
StreamingVoice: Used for playing back AudioDataStreamable objects.
SoundSequence: Used to play back a series of AudioBuffers in sequence. They have a callback so that AudioBuffers can be added dynamically by the client.

SourceVoices are intended to be pooled. You can obtain one from the AudioDevice pool by calling AudioDevice.Obtain<T> where T is the type of SourceVoice you wish to obtain. When you call Return on the voice it will be returned to the pool. TransientVoices are automatically returned to the pool when they have finished playing back their AudioBuffer.

SourceVoices can send audio to SubmixVoices. This is a convenient way to manage categories of audio. For example the client could have a MusicSubmix that all music-related voices send to. Then the volume of all music can be changed at once without the client having to manage all the individual music voices.

By default all voices send audio to AudioDevice.MasteringVoice. This is also a SubmixVoice that can be controlled like any other voice.

AudioDataStreamable is used in conjunction with a StreamingVoice to play back streaming audio from an ogg or qoa file.

AudioDataWav, AudioDataOgg, and AudioDataQoa all have a static CreateBuffer method that can be used to create an AudioBuffer from an audio file.

Reviewed-on: MoonsideGames/MoonWorks#50
2023-08-03 19:54:02 +00:00
cosmonaut 81cd397013 fix AudioDevice crash on shutdown 2023-07-28 15:02:20 -07:00
cosmonaut e73c7ede55 rename SoundQueue to SoundSequence 2023-07-28 13:21:50 -07:00
cosmonaut 83f1cc24db rename OnBufferNeeded to OnSoundNeeded 2023-07-28 13:10:01 -07:00
cosmonaut 1d86d0c210 add SoundQueue 2023-07-28 13:02:50 -07:00
cosmonaut dbbd6540ab fix StaticSoundInstance.Pause not actually pausing 2023-06-30 11:51:20 -07:00
cosmonaut 74ae295036 fix audio thread race condition on StaticAudio.GetInstance 2023-06-29 15:01:22 -07:00
cosmonaut 36ddb03d8f only check video exception on thread fault 2023-06-29 12:14:46 -07:00
cosmonaut bf3ad0c8b0 add Game.ShowRuntimeError method 2023-06-29 11:30:32 -07:00
cosmonaut f761d4f76e re-throw exceptions from video thread 2023-06-29 11:30:16 -07:00
cosmonaut 4c731401ff fix vertical axis on axis buttons being backwards 2023-06-28 18:19:34 -07:00
cosmonaut 071518732e set axis button threshold default to 0.5 2023-06-28 17:17:57 -07:00
cosmonaut 1adb76d5c7 exception handlers for video decoder threads 2023-06-28 13:20:33 -07:00
cosmonaut 5ff7da927a try-catch inside AudioThreadMain 2023-06-28 13:20:18 -07:00
cosmonaut 0fd3365d1d update dll.config 2023-06-23 16:17:44 -07:00
cosmonaut affb592c15 add dav1dfile dependency to README 2023-06-19 16:19:36 -07:00
cosmonaut 7e79e4a11d fix validator warnings on Quit 2023-06-15 17:45:26 -07:00
cosmonaut fc0937b2ff fix crash caused by audio weak references 2023-06-14 18:22:49 -07:00
cosmonaut c83997609f fix some scenarios where video pointers should not be replaced 2023-06-13 19:18:23 -07:00
cosmonaut b65d4e391c public Fix64 RawValue 2023-06-12 09:53:44 -07:00
cosmonaut 56bab545ba add MouseButton.Button lookup 2023-06-09 18:15:15 -07:00
cosmonaut 2ae116c72b fix window dimensions when starting in fullscreen 2023-06-09 16:27:43 -07:00
cosmonaut bd3e70b096 make KeyboardButton.KeyCode public 2023-06-09 16:17:41 -07:00
cosmonaut b1fe7f96b2 recenter window on windowed mode change 2023-06-09 11:42:20 -07:00
cosmonaut 00366cc9d4 fix dav1dfile submodule 2023-06-07 15:35:07 -07:00
cosmonaut 3bc25bc3a1 remove theorafile from dll config 2023-06-07 15:29:45 -07:00
cosmonaut 496eb670ab AV1 Video instead of Theora (#49)
VideoPlayer now takes AV1 video instead of Ogg Theora. This brings a significant decode speed improvement. The decoder now also operates in a threaded manner, which should prevent runtime stalls when fetching video frames.

Reviewed-on: MoonsideGames/MoonWorks#49
2023-06-07 21:18:44 +00:00
cosmonaut 00f4bfdeae optimize StreamingSoundQoa.Create 2023-05-22 19:24:26 -07:00
cosmonaut adeba633e5 csproj tweaks to support app publish 2023-05-22 18:28:13 -07:00
cosmonaut 300ef9f88e fix unnecessary copy in PackFontRanges 2023-05-22 11:41:19 -07:00
cosmonaut 76684eaa33 fix memory leak in StreamingSoundQoa.Create 2023-05-11 19:45:07 -07:00
cosmonaut c037b4cb69 fix StaticSoundInstance race condition and state 2023-05-11 18:59:26 -07:00
cosmonaut 5df08727c1 Sound instancing rework 2023-05-11 17:56:40 -07:00
cosmonaut 537517afb9 remove buffer size log 2023-05-10 15:13:19 -07:00
cosmonaut bd405dfbf0 QOA Support (#48)
Reviewed-on: MoonsideGames/MoonWorks#48
2023-05-05 22:26:32 +00:00
cosmonaut 2d7bb24b5c rename Texture load methods for clarity 2023-04-19 00:50:59 -07:00
cosmonaut 0ea60a376b new image loader API 2023-04-19 00:41:18 -07:00
cosmonaut e3c2f0e119 fix controller hot swapping 2023-04-05 16:52:36 -07:00
cosmonaut 3584e670ee add array overloads to avoid explicit generic parameter 2023-04-05 12:40:34 -07:00
cosmonaut 5a2f7eadb8 change array param to span in Buffer.GetData 2023-04-05 11:32:12 -07:00
cosmonaut 1e3f04235e remove array parameters from API functions 2023-04-05 11:07:16 -07:00
cosmonaut dd06205399 stop static sound instance on Free 2023-04-05 01:18:13 -07:00
cosmonaut 1cf04a7279 assets stream data directly into unmanaged memory 2023-04-05 00:47:02 -07:00
cosmonaut 3bd435746b StaticSound external byte buffer constructor 2023-04-04 17:12:03 -07:00
cosmonaut 8134761e44 load images from memory + QOI support 2023-04-03 17:28:00 -07:00
cosmonaut 8209051a3c remove non-static Normalize + return identity on zero vector 2023-03-29 10:06:37 -07:00
cosmonaut 80f3711f4c add Quit method to Game 2023-03-16 16:48:52 -07:00
cosmonaut 3a6b73e637 add int clamp to MathHelper 2023-03-14 14:42:13 -07:00
cosmonaut 12e7e6b9c1 add Color.FromHSV 2023-03-09 15:14:16 -08:00
cosmonaut 455f4048df MathHelper.Quantize uses round instead of floor 2023-03-09 13:52:45 -08:00
cosmonaut 1f0e3b5040 Audio Improvements (#47)
- Audio is now processed on a background thread instead of the main thread
- Audio tick rate is now ~200Hz
- MoonWorks.Math.Easings class completely rewritten to be easier to understand and use
- SoundInstance properties no longer call into FAudio unless the value actually changed
- SoundInstance property values can now be interpolated over time (tweens)
- SoundInstance tweens can be delayed
- SoundInstance sets a sane filter frequency default when switching filter type
- StreamingSound classes can be designated to update automatically on the audio thread or manually
- StreamingSound buffer consumption should now set Stopped state in a more sane way
- Added ReverbEffect, which creates a submix voice for a reverb effect
- SoundInstance can apply a ReverbEffect, which enables the Reverb property
- Audio resource tracking improvements
- Some tweaks to VideoPlayer to make its behavior more consistent

Reviewed-on: MoonsideGames/MoonWorks#47
2023-03-07 23:28:57 +00:00
cosmonaut f8b14ea94f add ReverbEffect 2023-03-01 17:47:09 -08:00
cosmonaut 472da0edd2 add Hidden property to Mouse 2023-03-01 13:47:25 -08:00
cosmonaut bd825b6c91 allow pushing raw uniform data 2023-02-23 16:59:34 -08:00
cosmonaut 515c2ebbca fix controller double open 2023-02-23 16:59:19 -08:00
cosmonaut 86322e9373 log controller open errors 2023-02-21 16:00:16 -08:00
cosmonaut e9aacb44da Add synchronized audio playback mechanism 2023-02-16 15:12:35 -08:00
cosmonaut 5baa1d7b40 Fix video shader base path lookup 2023-02-08 12:44:36 -08:00
cosmonaut f673803c37 FAudio 23.02 2023-02-07 12:31:37 -08:00
cosmonaut 0f78cd1a0c Refresh 1.11.0 2023-02-07 12:28:52 -08:00
cosmonaut 36ce74b58a tweak video shader filenames 2023-02-03 15:07:32 -08:00
cosmonaut 40d12357c0 Remove MoonWorks.Collision (#46)
After months of tweaking and refactoring I have realized that collision is like rendering - it's so fundamental to the structure of your game that making broad decisions about how it should work from a library level is too restrictive and difficult to optimize. Anyone skilled enough to use MoonWorks should be easily able to roll their own collision detection.

Reviewed-on: MoonsideGames/MoonWorks#46
2023-02-03 19:51:36 +00:00
evan e52fe60657 update RefreshCS 2023-01-31 12:29:17 -08:00
TheSpydog b39526ca90 Textures now have a sample count, not render passes (#45)
Co-authored-by: Caleb Cornett <caleb.cornett@outlook.com>
Reviewed-on: MoonsideGames/MoonWorks#45
Co-authored-by: TheSpydog <thespydog@noreply.example.org>
Co-committed-by: TheSpydog <thespydog@noreply.example.org>
2023-01-31 20:27:26 +00:00
170 changed files with 8361 additions and 5771 deletions

6
.gitmodules vendored
View File

@ -10,6 +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
[submodule "lib/dav1dfile"]
path = lib/dav1dfile
url = https://github.com/MoonsideGames/dav1dfile.git

2862
Doxyfile Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net7.0</TargetFrameworks>
<Platforms>x64</Platforms>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>11</LangVersion>
</PropertyGroup>
@ -15,13 +14,29 @@
<Compile Include="lib\FAudio\csharp\FAudio.cs" />
<Compile Include="lib\RefreshCS\src\Refresh.cs" />
<Compile Include="lib\SDL2-CS\src\SDL2.cs" />
<Compile Include="lib\Theorafile\csharp\Theorafile.cs" />
<Compile Include="lib\WellspringCS\WellspringCS.cs" />
<Compile Include="lib\dav1dfile\csharp\dav1dfile.cs" />
</ItemGroup>
<ItemGroup>
<None Include="MoonWorks.dll.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="src\Graphics\StockShaders\Binary\video_fullscreen.vert.refresh">
<LogicalName>MoonWorks.Graphics.StockShaders.VideoFullscreen.vert.refresh</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="src\Graphics\StockShaders\Binary\video_yuv2rgba.frag.refresh">
<LogicalName>MoonWorks.Graphics.StockShaders.VideoYUV2RGBA.frag.refresh</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="src\Graphics\StockShaders\Binary\text_transform.vert.refresh">
<LogicalName>MoonWorks.Graphics.StockShaders.TextTransform.vert.refresh</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="src\Graphics\StockShaders\Binary\text_msdf.frag.refresh">
<LogicalName>MoonWorks.Graphics.StockShaders.TextMSDF.frag.refresh</LogicalName>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@ -5,18 +5,18 @@
<dllmap dll="SDL2" os="linux,freebsd,netbsd" target="libSDL2-2.0.so.0"/>
<dllmap dll="Refresh" os="windows" target="Refresh.dll"/>
<dllmap dll="Refresh" os="osx" target="libRefresh.0.dylib"/>
<dllmap dll="Refresh" os="linux,freebsd,netbsd" target="libRefresh.so.0"/>
<dllmap dll="Refresh" os="osx" target="libRefresh.1.dylib"/>
<dllmap dll="Refresh" os="linux,freebsd,netbsd" target="libRefresh.so.1"/>
<dllmap dll="FAudio" os="windows" target="FAudio.dll"/>
<dllmap dll="FAudio" os="osx" target="libFAudio.0.dylib"/>
<dllmap dll="FAudio" os="linux,freebsd,netbsd" target="libFAudio.so.0"/>
<dllmap dll="Wellspring" os="windows" target="Wellspring.dll"/>
<dllmap dll="Wellspring" os="osx" target="libWellspring.0.dylib"/>
<dllmap dll="Wellspring" os="linux,freebsd,netbsd" target="libWellspring.so.0"/>
<dllmap dll="Wellspring" os="osx" target="libWellspring.1.dylib"/>
<dllmap dll="Wellspring" os="linux,freebsd,netbsd" target="libWellspring.so.1"/>
<dllmap dll="Theorafile" os="windows" target="libtheorafile.dll"/>
<dllmap dll="Theorafile" os="osx" target="libtheorafile.dylib"/>
<dllmap dll="Theorafile" os="linux,freebsd,netbsd" target="libtheorafile.so"/>
<dllmap dll="dav1dfile" os="windows" target="dav1dfile.dll"/>
<dllmap dll="dav1dfile" os="osx" target="libdav1dfile.1.dylib"/>
<dllmap dll="dav1dfile" os="linux,freebsd,netbsd,openbsd" target="libdav1dfile.so.1"/>
</configuration>

View File

@ -12,9 +12,13 @@ MoonWorks uses strictly Free Open Source Software. It will never have any kind o
## Documentation
API Reference: https://moonside.games/docs/moonworksapi/
High-level documentation is provided here: https://moonside.games/docs/moonworks/
For an actual API reference, the source is documented in doc comments that your preferred IDE can read.
The source is documented in doc comments that your preferred IDE can read.
Join our Discord! https://discord.gg/ujhwdkHmhN
## Dependencies
@ -22,7 +26,7 @@ For an actual API reference, the source is documented in doc comments that your
* [Refresh](https://gitea.moonside.games/MoonsideGames/Refresh) - Graphics
* [FAudio](https://github.com/FNA-XNA/FAudio) - Audio
* [Wellspring](https://gitea.moonside.games/MoonsideGames/Wellspring) - Font Rendering
* [Theorafile](https://github.com/FNA-XNA/Theorafile) - Compressed Video
* [dav1dfile](https://github.com/MoonsideGames/dav1dfile) - Compressed Video
Prebuilt dependencies can be obtained here: https://moonside.games/files/moonlibs.tar.bz2

@ -1 +1 @@
Subproject commit 11ba6b37509a6c2fa2690f2643ee7bf5ce2ab4f2
Subproject commit 60480416bda930bf7544e6abe31b937f0daa0256

@ -1 +1 @@
Subproject commit 1643061386177f62b516ccaad0ea04607cae2333
Subproject commit b5325e6d0329eeb35b074091a569a5f679852d28

@ -1 +1 @@
Subproject commit f8c6fc407fbb22072fdafcda918aec52b2102519
Subproject commit e4afbb848586fca530b6538320f799f81a18b941

@ -1 +0,0 @@
Subproject commit 8f9419ea856480e08294698e1d6be8752df3710b

@ -1 +1 @@
Subproject commit f8872bae59e394b0f8a35224bb39ab8fd041af97
Subproject commit 074f2afc833b221906bb2468735041ce78f2cb89

1
lib/dav1dfile Submodule

@ -0,0 +1 @@
Subproject commit 5065e2cd4662dbe023b77a45ef967f975170dfff

79
src/Audio/AudioBuffer.cs Normal file
View File

@ -0,0 +1,79 @@
using System;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
/// <summary>
/// Contains raw audio data in a specified Format. <br/>
/// Submit this to a SourceVoice to play audio.
/// </summary>
public class AudioBuffer : AudioResource
{
IntPtr BufferDataPtr;
uint BufferDataLength;
private bool OwnsBufferData;
public Format Format { get; }
/// <summary>
/// Create a new AudioBuffer.
/// </summary>
/// <param name="ownsBufferData">If true, the buffer data will be destroyed when this AudioBuffer is destroyed.</param>
public AudioBuffer(
AudioDevice device,
Format format,
IntPtr bufferPtr,
uint bufferLengthInBytes,
bool ownsBufferData) : base(device)
{
Format = format;
BufferDataPtr = bufferPtr;
BufferDataLength = bufferLengthInBytes;
OwnsBufferData = ownsBufferData;
}
/// <summary>
/// Create another AudioBuffer from this audio buffer.
/// It will not own the buffer data.
/// </summary>
/// <param name="offset">Offset in bytes from the top of the original buffer.</param>
/// <param name="length">Length in bytes of the new buffer.</param>
/// <returns></returns>
public AudioBuffer Slice(int offset, uint length)
{
return new AudioBuffer(Device, Format, BufferDataPtr + offset, length, false);
}
/// <summary>
/// Create an FAudioBuffer struct from this AudioBuffer.
/// </summary>
/// <param name="loop">Whether we should set the FAudioBuffer to loop.</param>
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);
}
}
}

147
src/Audio/AudioDataOgg.cs Normal file
View File

@ -0,0 +1,147 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
/// <summary>
/// Streamable audio in Ogg format.
/// </summary>
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;
var handle = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero);
if (error != 0)
{
throw new InvalidOperationException("Error loading file!");
}
var 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)
{
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,
Format.Channels,
(IntPtr) buffer,
lengthInFloats
);
var sampleCount = samples * Format.Channels;
reachedEnd = sampleCount < lengthInFloats;
filledLengthInBytes = sampleCount * sizeof(float);
}
/// <summary>
/// Prepares the Ogg data for streaming.
/// </summary>
public override unsafe void Load()
{
if (!Loaded)
{
var fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length);
var fileDataSpan = new Span<byte>((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);
Logger.LogError("Error opening OGG file!");
Logger.LogError("Error: " + error);
throw new InvalidOperationException("Error opening OGG file!");
}
}
}
public override void Seek(uint sampleFrame)
{
FAudio.stb_vorbis_seek(VorbisHandle, sampleFrame);
}
/// <summary>
/// Unloads the Ogg data, freeing resources.
/// </summary>
public override unsafe void Unload()
{
if (Loaded)
{
FAudio.stb_vorbis_close(VorbisHandle);
NativeMemory.Free((void*) FileDataPtr);
VorbisHandle = IntPtr.Zero;
FileDataPtr = IntPtr.Zero;
}
}
/// <summary>
/// Loads an entire ogg file into an AudioBuffer. Useful for static audio.
/// </summary>
public static unsafe AudioBuffer CreateBuffer(AudioDevice device, string filePath)
{
var filePointer = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero);
if (error != 0)
{
throw new InvalidOperationException("Error loading file!");
}
var info = FAudio.stb_vorbis_get_info(filePointer);
var lengthInFloats =
FAudio.stb_vorbis_stream_length_in_samples(filePointer) * info.channels;
var lengthInBytes = lengthInFloats * Marshal.SizeOf<float>();
var buffer = NativeMemory.Alloc((nuint) lengthInBytes);
FAudio.stb_vorbis_get_samples_float_interleaved(
filePointer,
info.channels,
(nint) buffer,
(int) lengthInFloats
);
FAudio.stb_vorbis_close(filePointer);
var 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);
}
}
}

164
src/Audio/AudioDataQoa.cs Normal file
View File

@ -0,0 +1,164 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
/// <summary>
/// Streamable audio in QOA format.
/// </summary>
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 var stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
using var 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)
{
var lengthInShorts = bufferLengthInBytes / sizeof(short);
// NOTE: this function returns samples per channel!
var samples = FAudio.qoa_decode_next_frame(QoaHandle, (short*) buffer);
var sampleCount = samples * Format.Channels;
reachedEnd = sampleCount < lengthInShorts;
filledLengthInBytes = (int) (sampleCount * sizeof(short));
}
/// <summary>
/// Prepares qoa data for streaming.
/// </summary>
public override unsafe void Load()
{
if (!Loaded)
{
var fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length);
var fileDataSpan = new Span<byte>((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);
Logger.LogError("Error opening QOA file!");
throw new InvalidOperationException("Error opening QOA file!");
}
}
}
public override void Seek(uint sampleFrame)
{
FAudio.qoa_seek_frame(QoaHandle, (int) sampleFrame);
}
/// <summary>
/// Unloads the qoa data, freeing resources.
/// </summary>
public override unsafe void Unload()
{
if (Loaded)
{
FAudio.qoa_close(QoaHandle);
NativeMemory.Free((void*) FileDataPtr);
QoaHandle = IntPtr.Zero;
FileDataPtr = IntPtr.Zero;
}
}
/// <summary>
/// Loads the entire qoa file into an AudioBuffer. Useful for static audio.
/// </summary>
public unsafe static AudioBuffer CreateBuffer(AudioDevice device, string filePath)
{
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
var fileDataPtr = NativeMemory.Alloc((nuint) fileStream.Length);
var fileDataSpan = new Span<byte>(fileDataPtr, (int) fileStream.Length);
fileStream.ReadExactly(fileDataSpan);
fileStream.Close();
var qoaHandle = FAudio.qoa_open_from_memory((char*) fileDataPtr, (uint) fileDataSpan.Length, 0);
if (qoaHandle == 0)
{
NativeMemory.Free(fileDataPtr);
Logger.LogError("Error opening QOA file!");
throw new InvalidOperationException("Error opening QOA file!");
}
FAudio.qoa_attributes(qoaHandle, out var channels, out var samplerate, out var samples_per_channel_per_frame, out var total_samples_per_channel);
var bufferLengthInBytes = total_samples_per_channel * channels * sizeof(short);
var buffer = NativeMemory.Alloc(bufferLengthInBytes);
FAudio.qoa_decode_entire(qoaHandle, (short*) buffer);
FAudio.qoa_close(qoaHandle);
NativeMemory.Free(fileDataPtr);
var 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);
}
}
}

View File

@ -0,0 +1,49 @@
namespace MoonWorks.Audio
{
/// <summary>
/// Use this in conjunction with a StreamingVoice to play back streaming audio data.
/// </summary>
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)
{
}
/// <summary>
/// Loads the raw audio data into memory to prepare it for stream decoding.
/// </summary>
public abstract void Load();
/// <summary>
/// Unloads the raw audio data from memory.
/// </summary>
public abstract void Unload();
/// <summary>
/// Seeks to the given sample frame.
/// </summary>
public abstract void Seek(uint sampleFrame);
/// <summary>
/// Attempts to decodes data of length bufferLengthInBytes into the provided buffer.
/// </summary>
/// <param name="buffer">The buffer that decoded bytes will be placed into.</param>
/// <param name="bufferLengthInBytes">Requested length of decoded audio data.</param>
/// <param name="filledLengthInBytes">How much data was actually filled in by the decode.</param>
/// <param name="reachedEnd">Whether the end of the data was reached on this decode.</param>
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);
}
}
}

100
src/Audio/AudioDataWav.cs Normal file
View File

@ -0,0 +1,100 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
public static class AudioDataWav
{
/// <summary>
/// Create an AudioBuffer containing all the WAV audio data in a file.
/// </summary>
/// <returns></returns>
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 var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using var 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();
var waveDataBuffer = NativeMemory.Alloc((nuint) waveDataLength);
var waveDataSpan = new Span<byte>(waveDataBuffer, waveDataLength);
stream.ReadExactly(waveDataSpan);
var format = new Format
{
Tag = (FormatTag) wFormatTag,
BitsPerSample = wBitsPerSample,
Channels = nChannels,
SampleRate = nSamplesPerSec
};
return new AudioBuffer(
device,
format,
(nint) waveDataBuffer,
(uint) waveDataLength,
true
);
}
}
}

View File

@ -1,41 +1,53 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
namespace MoonWorks.Audio
{
/// <summary>
/// AudioDevice manages all audio-related concerns.
/// </summary>
public class AudioDevice : IDisposable
{
public IntPtr Handle { get; }
public byte[] Handle3D { get; }
public IntPtr MasteringVoice { get; }
public FAudio.FAudioDeviceDetails DeviceDetails { get; }
public IntPtr ReverbVoice { 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 float masteringVolume = 1f;
public float MasteringVolume
private readonly HashSet<GCHandle> resourceHandles = new HashSet<GCHandle>();
private readonly HashSet<UpdatingSourceVoice> updatingSourceVoices = new HashSet<UpdatingSourceVoice>();
private AudioTweenManager AudioTweenManager;
private SourceVoicePool VoicePool;
private List<SourceVoice> VoicesToReturn = new List<SourceVoice>();
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()
{
get => masteringVolume;
set
{
masteringVolume = value;
FAudio.FAudioVoice_SetVolume(MasteringVoice, masteringVolume, 0);
}
}
UpdateInterval = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / Step);
internal FAudio.FAudioVoiceSends ReverbSends;
private readonly List<WeakReference<AudioResource>> resources = new List<WeakReference<AudioResource>>();
private readonly List<WeakReference<StreamingSound>> streamingSounds = new List<WeakReference<StreamingSound>>();
private bool IsDisposed;
public unsafe AudioDevice()
{
FAudio.FAudioCreate(out var handle, 0, FAudio.FAUDIO_DEFAULT_PROCESSOR);
Handle = handle;
@ -80,25 +92,24 @@ namespace MoonWorks.Audio
}
/* Init Mastering Voice */
IntPtr masteringVoice;
if (FAudio.FAudio_CreateMasteringVoice(
var result = FAudio.FAudio_CreateMasteringVoice(
Handle,
out masteringVoice,
out trueMasteringVoice,
FAudio.FAUDIO_DEFAULT_CHANNELS,
FAudio.FAUDIO_DEFAULT_SAMPLERATE,
0,
i,
IntPtr.Zero
) != 0)
);
if (result != 0)
{
Logger.LogError("No mastering voice found!");
Handle = IntPtr.Zero;
FAudio.FAudio_Release(Handle);
Logger.LogError("Failed to create a mastering voice!");
Logger.LogError("Audio device creation failed!");
return;
}
MasteringVoice = masteringVoice;
fauxMasteringVoice = SubmixVoice.CreateFauxMasteringVoice(this);
/* Init 3D Audio */
@ -109,169 +120,224 @@ namespace MoonWorks.Audio
Handle3D
);
/* Init reverb */
AudioTweenManager = new AudioTweenManager();
VoicePool = new SourceVoicePool(this);
IntPtr reverbVoice;
WakeSignal = new AutoResetEvent(true);
IntPtr reverb;
FAudio.FAudioCreateReverb(out reverb, 0);
Thread = new Thread(ThreadMain);
Thread.IsBackground = true;
Thread.Start();
IntPtr chainPtr;
chainPtr = Marshal.AllocHGlobal(
Marshal.SizeOf<FAudio.FAudioEffectChain>()
Running = true;
TickStopwatch.Start();
previousTickTime = 0;
}
private void ThreadMain()
{
while (Running)
{
lock (StateLock)
{
try
{
ThreadMainTick();
}
catch (Exception e)
{
Logger.LogError(e.ToString());
}
}
WakeSignal.WaitOne(UpdateInterval);
}
}
private void ThreadMainTick()
{
long tickDelta = TickStopwatch.Elapsed.Ticks - previousTickTime;
previousTickTime = TickStopwatch.Elapsed.Ticks;
float elapsedSeconds = (float) tickDelta / System.TimeSpan.TicksPerSecond;
AudioTweenManager.Update(elapsedSeconds);
foreach (var voice in updatingSourceVoices)
{
voice.Update();
}
foreach (var voice in VoicesToReturn)
{
if (voice is UpdatingSourceVoice updatingSourceVoice)
{
updatingSourceVoices.Remove(updatingSourceVoice);
}
voice.Reset();
VoicePool.Return(voice);
}
VoicesToReturn.Clear();
}
/// <summary>
/// Triggers all pending operations with the given syncGroup value.
/// </summary>
public void TriggerSyncGroup(uint syncGroup)
{
FAudio.FAudio_CommitChanges(Handle, syncGroup);
}
/// <summary>
/// Obtains an appropriate source voice from the voice pool.
/// </summary>
/// <param name="format">The format that the voice must match.</param>
/// <returns>A source voice with the given format.</returns>
public T Obtain<T>(Format format) where T : SourceVoice, IPoolable<T>
{
lock (StateLock)
{
var voice = VoicePool.Obtain<T>(format);
if (voice is UpdatingSourceVoice updatingSourceVoice)
{
updatingSourceVoices.Add(updatingSourceVoice);
}
return voice;
}
}
/// <summary>
/// Returns the source voice to the voice pool.
/// </summary>
/// <param name="voice"></param>
internal void Return(SourceVoice voice)
{
lock (StateLock)
{
VoicesToReturn.Add(voice);
}
}
internal void CreateTween(
Voice voice,
AudioTweenProperty property,
System.Func<float, float> easingFunction,
float start,
float end,
float duration,
float delayTime
) {
lock (StateLock)
{
AudioTweenManager.CreateTween(
voice,
property,
easingFunction,
start,
end,
duration,
delayTime
);
FAudio.FAudioEffectChain* reverbChain = (FAudio.FAudioEffectChain*) chainPtr;
reverbChain->EffectCount = 1;
reverbChain->pEffectDescriptors = Marshal.AllocHGlobal(
Marshal.SizeOf<FAudio.FAudioEffectDescriptor>()
);
FAudio.FAudioEffectDescriptor* reverbDescriptor =
(FAudio.FAudioEffectDescriptor*) reverbChain->pEffectDescriptors;
reverbDescriptor->InitialState = 1;
reverbDescriptor->OutputChannels = (uint) (
(DeviceDetails.OutputFormat.Format.nChannels == 6) ? 6 : 1
);
reverbDescriptor->pEffect = reverb;
FAudio.FAudio_CreateSubmixVoice(
Handle,
out reverbVoice,
1, /* omnidirectional reverb */
DeviceDetails.OutputFormat.Format.nSamplesPerSec,
0,
0,
IntPtr.Zero,
chainPtr
);
FAudio.FAPOBase_Release(reverb);
Marshal.FreeHGlobal(reverbChain->pEffectDescriptors);
Marshal.FreeHGlobal(chainPtr);
ReverbVoice = reverbVoice;
/* Init reverb params */
// Defaults based on FAUDIOFX_I3DL2_PRESET_GENERIC
IntPtr reverbParamsPtr = Marshal.AllocHGlobal(
Marshal.SizeOf<FAudio.FAudioFXReverbParameters>()
);
FAudio.FAudioFXReverbParameters* reverbParams = (FAudio.FAudioFXReverbParameters*) reverbParamsPtr;
reverbParams->WetDryMix = 100.0f;
reverbParams->ReflectionsDelay = 7;
reverbParams->ReverbDelay = 11;
reverbParams->RearDelay = FAudio.FAUDIOFX_REVERB_DEFAULT_REAR_DELAY;
reverbParams->PositionLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION;
reverbParams->PositionRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION;
reverbParams->PositionMatrixLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX;
reverbParams->PositionMatrixRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX;
reverbParams->EarlyDiffusion = 15;
reverbParams->LateDiffusion = 15;
reverbParams->LowEQGain = 8;
reverbParams->LowEQCutoff = 4;
reverbParams->HighEQGain = 8;
reverbParams->HighEQCutoff = 6;
reverbParams->RoomFilterFreq = 5000f;
reverbParams->RoomFilterMain = -10f;
reverbParams->RoomFilterHF = -1f;
reverbParams->ReflectionsGain = -26.0200005f;
reverbParams->ReverbGain = 10.0f;
reverbParams->DecayTime = 1.49000001f;
reverbParams->Density = 100.0f;
reverbParams->RoomSize = FAudio.FAUDIOFX_REVERB_DEFAULT_ROOM_SIZE;
FAudio.FAudioVoice_SetEffectParameters(
ReverbVoice,
0,
reverbParamsPtr,
(uint) Marshal.SizeOf<FAudio.FAudioFXReverbParameters>(),
0
);
Marshal.FreeHGlobal(reverbParamsPtr);
/* Init reverb sends */
ReverbSends = new FAudio.FAudioVoiceSends
{
SendCount = 2,
pSends = Marshal.AllocHGlobal(
2 * Marshal.SizeOf<FAudio.FAudioSendDescriptor>()
)
};
FAudio.FAudioSendDescriptor* sendDesc = (FAudio.FAudioSendDescriptor*) ReverbSends.pSends;
sendDesc[0].Flags = 0;
sendDesc[0].pOutputVoice = MasteringVoice;
sendDesc[1].Flags = 0;
sendDesc[1].pOutputVoice = ReverbVoice;
}
}
internal void Update()
internal void ClearTweens(
Voice voice,
AudioTweenProperty property
) {
lock (StateLock)
{
for (var i = streamingSounds.Count - 1; i >= 0; i--)
{
var weakReference = streamingSounds[i];
if (weakReference.TryGetTarget(out var streamingSound))
{
streamingSound.Update();
AudioTweenManager.ClearTweens(voice, property);
}
else
}
internal void WakeThread()
{
streamingSounds.RemoveAt(i);
WakeSignal.Set();
}
internal void AddResourceReference(GCHandle resourceReference)
{
lock (StateLock)
{
resourceHandles.Add(resourceReference);
if (resourceReference.Target is UpdatingSourceVoice updatableVoice)
{
updatingSourceVoices.Add(updatableVoice);
}
}
}
internal void AddDynamicSoundInstance(StreamingSound instance)
internal void RemoveResourceReference(GCHandle resourceReference)
{
streamingSounds.Add(new WeakReference<StreamingSound>(instance));
}
lock (StateLock)
{
resourceHandles.Remove(resourceReference);
internal void AddResourceReference(WeakReference<AudioResource> resourceReference)
if (resourceReference.Target is UpdatingSourceVoice updatableVoice)
{
lock (resources)
{
resources.Add(resourceReference);
updatingSourceVoices.Remove(updatableVoice);
}
}
internal void RemoveResourceReference(WeakReference<AudioResource> resourceReference)
{
lock (resources)
{
resources.Remove(resourceReference);
}
}
protected virtual void Dispose(bool disposing)
{
if (!IsDisposed)
{
Running = false;
if (disposing)
{
for (var i = resources.Count - 1; i >= 0; i--)
{
var weakReference = resources[i];
Thread.Join();
if (weakReference.TryGetTarget(out var resource))
// dispose all source voices first
foreach (var handle in resourceHandles)
{
if (handle.Target is SourceVoice voice)
{
voice.Dispose();
}
}
// dispose all submix voices except the faux mastering voice
foreach (var 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 (var handle in resourceHandles)
{
if (handle.Target is AudioResource resource)
{
resource.Dispose();
}
}
resources.Clear();
resourceHandles.Clear();
}
FAudio.FAudioVoice_DestroyVoice(ReverbVoice);
FAudio.FAudioVoice_DestroyVoice(MasteringVoice);
FAudio.FAudio_Release(Handle);
IsDisposed = true;
}
}
// TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
~AudioDevice()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method

View File

@ -4,6 +4,9 @@ using MoonWorks.Math.Float;
namespace MoonWorks.Audio
{
/// <summary>
/// An emitter for 3D spatial audio.
/// </summary>
public class AudioEmitter : AudioResource
{
internal FAudio.F3DAUDIO_EMITTER emitterData;
@ -129,7 +132,5 @@ namespace MoonWorks.Audio
emitterData.pReverbCurve = IntPtr.Zero;
emitterData.CurveDistanceScaler = 1.0f;
}
protected override void Destroy() { }
}
}

View File

@ -3,6 +3,9 @@ using MoonWorks.Math.Float;
namespace MoonWorks.Audio
{
/// <summary>
/// A listener for 3D spatial audio. Usually attached to a camera.
/// </summary>
public class AudioListener : AudioResource
{
internal FAudio.F3DAUDIO_LISTENER listenerData;
@ -91,7 +94,5 @@ namespace MoonWorks.Audio
/* Unexposed variables, defaults based on XNA behavior */
listenerData.pCone = IntPtr.Zero;
}
protected override void Destroy() { }
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
@ -8,28 +9,24 @@ namespace MoonWorks.Audio
public bool IsDisposed { get; private set; }
private WeakReference<AudioResource> selfReference;
private GCHandle SelfReference;
public AudioResource(AudioDevice device)
protected AudioResource(AudioDevice device)
{
Device = device;
selfReference = new WeakReference<AudioResource>(this);
Device.AddResourceReference(selfReference);
SelfReference = GCHandle.Alloc(this, GCHandleType.Weak);
Device.AddResourceReference(SelfReference);
}
protected abstract void Destroy();
protected virtual void Dispose(bool disposing)
{
if (!IsDisposed)
{
Destroy();
if (selfReference != null)
if (disposing)
{
Device.RemoveResourceReference(selfReference);
selfReference = null;
Device.RemoveResourceReference(SelfReference);
SelfReference.Free();
}
IsDisposed = true;
@ -38,8 +35,12 @@ namespace MoonWorks.Audio
~AudioResource()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: false);
#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.
Logger.LogWarn($"A resource of type {GetType().Name} was not Disposed.");
#endif
}
public void Dispose()

58
src/Audio/AudioTween.cs Normal file
View File

@ -0,0 +1,58 @@
using System.Collections.Generic;
using EasingFunction = System.Func<float, float>;
namespace MoonWorks.Audio
{
internal enum AudioTweenProperty
{
Pan,
Pitch,
Volume,
FilterFrequency,
Reverb
}
internal class AudioTween
{
public Voice Voice;
public AudioTweenProperty Property;
public EasingFunction EasingFunction;
public float Time;
public float StartValue;
public float EndValue;
public float DelayTime;
public float Duration;
}
internal class AudioTweenPool
{
private Queue<AudioTween> Tweens = new Queue<AudioTween>(16);
public AudioTweenPool()
{
for (int i = 0; i < 16; i += 1)
{
Tweens.Enqueue(new AudioTween());
}
}
public AudioTween Obtain()
{
if (Tweens.Count > 0)
{
var tween = Tweens.Dequeue();
return tween;
}
else
{
return new AudioTween();
}
}
public void Free(AudioTween tween)
{
tween.Voice = null;
Tweens.Enqueue(tween);
}
}
}

View File

@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
namespace MoonWorks.Audio
{
internal class AudioTweenManager
{
private AudioTweenPool AudioTweenPool = new AudioTweenPool();
private readonly Dictionary<(Voice, AudioTweenProperty), AudioTween> AudioTweens = new Dictionary<(Voice, AudioTweenProperty), AudioTween>();
private readonly List<AudioTween> DelayedAudioTweens = new List<AudioTween>();
public void Update(float elapsedSeconds)
{
for (var i = DelayedAudioTweens.Count - 1; i >= 0; i--)
{
var audioTween = DelayedAudioTweens[i];
var voice = audioTween.Voice;
audioTween.Time += elapsedSeconds;
if (audioTween.Time >= audioTween.DelayTime)
{
// set the tween start value to the current value of the property
switch (audioTween.Property)
{
case AudioTweenProperty.Pan:
audioTween.StartValue = voice.Pan;
break;
case AudioTweenProperty.Pitch:
audioTween.StartValue = voice.Pitch;
break;
case AudioTweenProperty.Volume:
audioTween.StartValue = voice.Volume;
break;
case AudioTweenProperty.FilterFrequency:
audioTween.StartValue = voice.FilterFrequency;
break;
case AudioTweenProperty.Reverb:
audioTween.StartValue = voice.Reverb;
break;
}
audioTween.Time = 0;
DelayedAudioTweens.RemoveAt(i);
AddTween(audioTween);
}
}
foreach (var (key, audioTween) in AudioTweens)
{
bool finished = UpdateAudioTween(audioTween, elapsedSeconds);
if (finished)
{
AudioTweenPool.Free(audioTween);
AudioTweens.Remove(key);
}
}
}
public void CreateTween(
Voice voice,
AudioTweenProperty property,
System.Func<float, float> easingFunction,
float start,
float end,
float duration,
float delayTime
) {
var tween = AudioTweenPool.Obtain();
tween.Voice = voice;
tween.Property = property;
tween.EasingFunction = easingFunction;
tween.StartValue = start;
tween.EndValue = end;
tween.Duration = duration;
tween.Time = 0;
tween.DelayTime = delayTime;
if (delayTime == 0)
{
AddTween(tween);
}
else
{
DelayedAudioTweens.Add(tween);
}
}
public void ClearTweens(Voice voice, AudioTweenProperty property)
{
AudioTweens.Remove((voice, property));
}
private void AddTween(
AudioTween audioTween
) {
// if a tween with the same sound and property already exists, get rid of it
if (AudioTweens.TryGetValue((audioTween.Voice, audioTween.Property), out var currentTween))
{
AudioTweenPool.Free(currentTween);
}
AudioTweens[(audioTween.Voice, audioTween.Property)] = audioTween;
}
private static bool UpdateAudioTween(AudioTween audioTween, float delta)
{
float value;
audioTween.Time += delta;
var finished = audioTween.Time >= audioTween.Duration;
if (finished)
{
value = audioTween.EndValue;
}
else
{
value = MoonWorks.Math.Easing.Interp(
audioTween.StartValue,
audioTween.EndValue,
audioTween.Time,
audioTween.Duration,
audioTween.EasingFunction
);
}
switch (audioTween.Property)
{
case AudioTweenProperty.Pan:
audioTween.Voice.Pan = value;
break;
case AudioTweenProperty.Pitch:
audioTween.Voice.Pitch = value;
break;
case AudioTweenProperty.Volume:
audioTween.Voice.Volume = value;
break;
case AudioTweenProperty.FilterFrequency:
audioTween.Voice.FilterFrequency = value;
break;
case AudioTweenProperty.Reverb:
audioTween.Voice.Reverb = value;
break;
}
return finished;
}
}
}

36
src/Audio/Format.cs Normal file
View File

@ -0,0 +1,36 @@
namespace MoonWorks.Audio
{
public enum FormatTag : ushort
{
Unknown = 0,
PCM = 1,
MSADPCM = 2,
IEEE_FLOAT = 3
}
/// <summary>
/// Describes the format of audio data. Usually specified in an audio file's header information.
/// </summary>
public record struct Format
{
public FormatTag Tag;
public ushort Channels;
public uint SampleRate;
public ushort BitsPerSample;
internal FAudio.FAudioWaveFormatEx ToFAudioFormat()
{
var blockAlign = (ushort) ((BitsPerSample / 8) * Channels);
return new FAudio.FAudioWaveFormatEx
{
wFormatTag = (ushort) Tag,
nChannels = Channels,
nSamplesPerSec = SampleRate,
wBitsPerSample = BitsPerSample,
nBlockAlign = blockAlign,
nAvgBytesPerSec = blockAlign * SampleRate
};
}
}
}

7
src/Audio/IPoolable.cs Normal file
View File

@ -0,0 +1,7 @@
namespace MoonWorks.Audio
{
public interface IPoolable<T>
{
static abstract T Create(AudioDevice device, Format format);
}
}

View File

@ -0,0 +1,28 @@
namespace MoonWorks.Audio
{
/// <summary>
/// PersistentVoice should be used when you need to maintain a long-term reference to a source voice.
/// </summary>
public class PersistentVoice : SourceVoice, IPoolable<PersistentVoice>
{
public PersistentVoice(AudioDevice device, Format format) : base(device, format)
{
}
public static PersistentVoice Create(AudioDevice device, Format format)
{
return new PersistentVoice(device, format);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="buffer">The buffer to submit to the voice.</param>
/// <param name="loop">Whether the voice should loop this buffer.</param>
public void Submit(AudioBuffer buffer, bool loop = false)
{
Submit(buffer.ToFAudioBuffer(loop));
}
}
}

83
src/Audio/ReverbEffect.cs Normal file
View File

@ -0,0 +1,83 @@
using System;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
/// <summary>
/// Use this in conjunction with SourceVoice.SetReverbEffectChain to add reverb to a voice.
/// </summary>
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);
var chain = new FAudio.FAudioEffectChain();
var 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<FAudio.FAudioFXReverbParameters>(),
0
);
}
}
}
}

View File

@ -1,366 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
public abstract class SoundInstance : AudioResource
{
internal IntPtr Handle;
internal FAudio.FAudioWaveFormatEx Format;
protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings;
public bool Is3D { get; protected set; }
public virtual SoundState State { get; protected set; }
private float pan = 0;
public float Pan
{
get => pan;
set
{
pan = value;
if (pan < -1f)
{
pan = -1f;
}
if (pan > 1f)
{
pan = 1f;
}
if (Is3D) { return; }
SetPanMatrixCoefficients();
FAudio.FAudioVoice_SetOutputMatrix(
Handle,
Device.MasteringVoice,
dspSettings.SrcChannelCount,
dspSettings.DstChannelCount,
dspSettings.pMatrixCoefficients,
0
);
}
}
private float pitch = 0;
public float Pitch
{
get => pitch;
set
{
pitch = Math.MathHelper.Clamp(value, -1f, 1f);
UpdatePitch();
}
}
private float volume = 1;
public float Volume
{
get => volume;
set
{
volume = value;
FAudio.FAudioVoice_SetVolume(Handle, volume, 0);
}
}
private float reverb;
public unsafe float Reverb
{
get => reverb;
set
{
reverb = value;
float* outputMatrix = (float*) dspSettings.pMatrixCoefficients;
outputMatrix[0] = reverb;
if (dspSettings.SrcChannelCount == 2)
{
outputMatrix[1] = reverb;
}
FAudio.FAudioVoice_SetOutputMatrix(
Handle,
Device.ReverbVoice,
dspSettings.SrcChannelCount,
1,
dspSettings.pMatrixCoefficients,
0
);
}
}
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
};
private float FilterFrequency
{
get => filterParameters.Frequency;
set
{
value = System.Math.Clamp(value, 0.01f, MAX_FILTER_FREQUENCY);
filterParameters.Frequency = value;
FAudio.FAudioVoice_SetFilterParameters(
Handle,
ref filterParameters,
0
);
}
}
private float FilterOneOverQ
{
get => filterParameters.OneOverQ;
set
{
value = System.Math.Clamp(value, 0.01f, MAX_FILTER_ONEOVERQ);
filterParameters.OneOverQ = value;
FAudio.FAudioVoice_SetFilterParameters(
Handle,
ref filterParameters,
0
);
}
}
private FilterType filterType;
public FilterType FilterType
{
get => filterType;
set
{
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;
break;
case FilterType.BandPass:
filterParameters.Type = FAudio.FAudioFilterType.FAudioBandPassFilter;
break;
case FilterType.HighPass:
filterParameters.Type = FAudio.FAudioFilterType.FAudioHighPassFilter;
break;
}
FAudio.FAudioVoice_SetFilterParameters(
Handle,
ref filterParameters,
0
);
}
}
public SoundInstance(
AudioDevice device,
ushort formatTag,
ushort bitsPerSample,
ushort blockAlign,
ushort channels,
uint samplesPerSecond
) : base(device)
{
var format = new FAudio.FAudioWaveFormatEx
{
wFormatTag = formatTag,
wBitsPerSample = bitsPerSample,
nChannels = channels,
nBlockAlign = blockAlign,
nSamplesPerSec = samplesPerSecond,
nAvgBytesPerSec = blockAlign * samplesPerSecond
};
Format = format;
FAudio.FAudio_CreateSourceVoice(
Device.Handle,
out Handle,
ref Format,
FAudio.FAUDIO_VOICE_USEFILTER,
FAudio.FAUDIO_DEFAULT_FREQ_RATIO,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero
);
if (Handle == IntPtr.Zero)
{
Logger.LogError("SoundInstance failed to initialize!");
return;
}
InitDSPSettings(Format.nChannels);
// FIXME: not everything should be running through reverb...
/*
FAudio.FAudioVoice_SetOutputVoices(
Handle,
ref Device.ReverbSends
);
*/
State = SoundState.Stopped;
}
public void Apply3D(AudioListener listener, AudioEmitter emitter)
{
Is3D = true;
emitter.emitterData.CurveDistanceScaler = Device.CurveDistanceScalar;
emitter.emitterData.ChannelCount = dspSettings.SrcChannelCount;
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,
Device.MasteringVoice,
dspSettings.SrcChannelCount,
dspSettings.DstChannelCount,
dspSettings.pMatrixCoefficients,
0
);
}
public abstract void Play();
public abstract void Pause();
public abstract void Stop();
public abstract void StopImmediate();
private void InitDSPSettings(uint srcChannels)
{
dspSettings = new FAudio.F3DAUDIO_DSP_SETTINGS();
dspSettings.DopplerFactor = 1f;
dspSettings.SrcChannelCount = srcChannels;
dspSettings.DstChannelCount = Device.DeviceDetails.OutputFormat.Format.nChannels;
int memsize = (
4 *
(int) dspSettings.SrcChannelCount *
(int) dspSettings.DstChannelCount
);
dspSettings.pMatrixCoefficients = Marshal.AllocHGlobal(memsize);
unsafe
{
byte* memPtr = (byte*) dspSettings.pMatrixCoefficients;
for (int i = 0; i < memsize; i += 1)
{
memPtr[i] = 0;
}
}
SetPanMatrixCoefficients();
}
private void UpdatePitch()
{
float doppler;
float dopplerScale = Device.DopplerScale;
if (!Is3D || dopplerScale == 0.0f)
{
doppler = 1.0f;
}
else
{
doppler = dspSettings.DopplerFactor * dopplerScale;
}
FAudio.FAudioSourceVoice_SetFrequencyRatio(
Handle,
(float) System.Math.Pow(2.0, pitch) * doppler,
0
);
}
// 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*) dspSettings.pMatrixCoefficients;
if (dspSettings.SrcChannelCount == 1)
{
if (dspSettings.DstChannelCount == 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 (dspSettings.DstChannelCount == 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 override void Destroy()
{
StopImmediate();
FAudio.FAudioVoice_DestroyVoice(Handle);
Marshal.FreeHGlobal(dspSettings.pMatrixCoefficients);
}
}
}

View File

@ -0,0 +1,64 @@
namespace MoonWorks.Audio
{
/// <summary>
/// Plays back a series of AudioBuffers in sequence. Set the OnSoundNeeded callback to add AudioBuffers dynamically.
/// </summary>
public class SoundSequence : UpdatingSourceVoice, IPoolable<SoundSequence>
{
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)
{
var 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))
{
Logger.LogWarn("Sound sequence audio format mismatch!");
return;
}
#endif
lock (StateLock)
{
Submit(buffer.ToFAudioBuffer());
}
}
}
}

218
src/Audio/SourceVoice.cs Normal file
View File

@ -0,0 +1,218 @@
using System;
namespace MoonWorks.Audio
{
/// <summary>
/// Emits audio from submitted audio buffers.
/// </summary>
public abstract class SourceVoice : Voice
{
private Format format;
public Format Format => format;
protected bool PlaybackInitiated;
/// <summary>
/// The number of buffers queued in the voice.
/// This includes the currently playing voice!
/// </summary>
public uint BuffersQueued
{
get
{
FAudio.FAudioSourceVoice_GetState(
Handle,
out var 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;
var 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);
}
/// <summary>
/// Starts consumption and processing of audio by the voice.
/// Delivers the result to any connected submix or mastering voice.
/// </summary>
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
public void Play(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
{
lock (StateLock)
{
FAudio.FAudioSourceVoice_Start(Handle, 0, syncGroup);
State = SoundState.Playing;
}
}
/// <summary>
/// Pauses playback.
/// All source buffers that are queued on the voice and the current cursor position are preserved.
/// </summary>
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
public void Pause(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
{
lock (StateLock)
{
FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup);
State = SoundState.Paused;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
public void ExitLoop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
{
lock (StateLock)
{
FAudio.FAudioSourceVoice_ExitLoop(Handle, syncGroup);
}
}
/// <summary>
/// Stops playback and removes all pending audio buffers from the voice queue.
/// </summary>
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
public void Stop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
{
lock (StateLock)
{
FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup);
FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
State = SoundState.Stopped;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="buffer">The buffer to submit to the voice.</param>
public void Submit(AudioBuffer buffer)
{
Submit(buffer.ToFAudioBuffer());
}
/// <summary>
/// Calculates positional sound. This must be called continuously to update positional sound.
/// </summary>
/// <param name="listener"></param>
/// <param name="emitter"></param>
public unsafe void Apply3D(AudioListener listener, AudioEmitter emitter)
{
Is3D = true;
emitter.emitterData.CurveDistanceScaler = Device.CurveDistanceScalar;
emitter.emitterData.ChannelCount = SourceChannelCount;
var 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
);
}
/// <summary>
/// Specifies that this source voice can be returned to the voice pool.
/// Holding on to the reference after calling this will cause problems!
/// </summary>
public void Return()
{
Stop();
Device.Return(this);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="buffer">The buffer to submit to the voice.</param>
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();
}
}
}

View File

@ -0,0 +1,39 @@
using System.Collections.Generic;
namespace MoonWorks.Audio
{
internal class SourceVoicePool
{
private AudioDevice Device;
Dictionary<(System.Type, Format), Queue<SourceVoice>> VoiceLists = new Dictionary<(System.Type, Format), Queue<SourceVoice>>();
public SourceVoicePool(AudioDevice device)
{
Device = device;
}
public T Obtain<T>(Format format) where T : SourceVoice, IPoolable<T>
{
if (!VoiceLists.ContainsKey((typeof(T), format)))
{
VoiceLists.Add((typeof(T), format), new Queue<SourceVoice>());
}
var list = VoiceLists[(typeof(T), format)];
if (list.Count == 0)
{
list.Enqueue(T.Create(Device, format));
}
return (T) list.Dequeue();
}
public void Return(SourceVoice voice)
{
var list = VoiceLists[(voice.GetType(), voice.Format)];
list.Enqueue(voice);
}
}
}

View File

@ -1,295 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
public class StaticSound : AudioResource
{
internal FAudio.FAudioBuffer Handle;
public ushort FormatTag { get; }
public ushort BitsPerSample { get; }
public ushort Channels { get; }
public uint SamplesPerSecond { get; }
public ushort BlockAlign { get; }
public uint LoopStart { get; set; } = 0;
public uint LoopLength { get; set; } = 0;
private Stack<StaticSoundInstance> Instances = new Stack<StaticSoundInstance>();
public static StaticSound LoadOgg(AudioDevice device, string filePath)
{
var filePointer = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero);
if (error != 0)
{
throw new AudioLoadException("Error loading file!");
}
var info = FAudio.stb_vorbis_get_info(filePointer);
var bufferSize = FAudio.stb_vorbis_stream_length_in_samples(filePointer) * info.channels;
var buffer = new float[bufferSize];
FAudio.stb_vorbis_get_samples_float_interleaved(
filePointer,
info.channels,
buffer,
(int) bufferSize
);
FAudio.stb_vorbis_close(filePointer);
return new StaticSound(
device,
(ushort) info.channels,
info.sample_rate,
buffer,
0,
(uint) buffer.Length
);
}
// mostly borrowed from https://github.com/FNA-XNA/FNA/blob/b71b4a35ae59970ff0070dea6f8620856d8d4fec/src/Audio/SoundEffect.cs#L385
public static StaticSound LoadWav(AudioDevice device, string filePath)
{
// Sample data
byte[] data;
// WaveFormatEx data
ushort wFormatTag;
ushort nChannels;
uint nSamplesPerSec;
uint nAvgBytesPerSec;
ushort nBlockAlign;
ushort wBitsPerSample;
int samplerLoopStart = 0;
int samplerLoopEnd = 0;
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
{
// 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();
data = reader.ReadBytes(waveDataLength);
// Scan for other chunks
while (reader.PeekChar() != -1)
{
char[] chunkIDChars = reader.ReadChars(4);
if (chunkIDChars.Length < 4)
{
break; // EOL!
}
byte[] chunkSizeBytes = reader.ReadBytes(4);
if (chunkSizeBytes.Length < 4)
{
break; // EOL!
}
string chunk_signature = new string(chunkIDChars);
int chunkDataSize = BitConverter.ToInt32(chunkSizeBytes, 0);
if (chunk_signature == "smpl") // "smpl", Sampler Chunk Found
{
reader.ReadUInt32(); // Manufacturer
reader.ReadUInt32(); // Product
reader.ReadUInt32(); // Sample Period
reader.ReadUInt32(); // MIDI Unity Note
reader.ReadUInt32(); // MIDI Pitch Fraction
reader.ReadUInt32(); // SMPTE Format
reader.ReadUInt32(); // SMPTE Offset
uint numSampleLoops = reader.ReadUInt32();
int samplerData = reader.ReadInt32();
for (int i = 0; i < numSampleLoops; i += 1)
{
reader.ReadUInt32(); // Cue Point ID
reader.ReadUInt32(); // Type
int start = reader.ReadInt32();
int end = reader.ReadInt32();
reader.ReadUInt32(); // Fraction
reader.ReadUInt32(); // Play Count
if (i == 0) // Grab loopStart and loopEnd from first sample loop
{
samplerLoopStart = start;
samplerLoopEnd = end;
}
}
if (samplerData != 0) // Read Sampler Data if it exists
{
reader.ReadBytes(samplerData);
}
}
else // Read unwanted chunk data and try again
{
reader.ReadBytes(chunkDataSize);
}
}
// End scan
}
return new StaticSound(
device,
wFormatTag,
wBitsPerSample,
nBlockAlign,
nChannels,
nSamplesPerSec,
data,
0,
(uint) data.Length
);
}
public StaticSound(
AudioDevice device,
ushort formatTag,
ushort bitsPerSample,
ushort blockAlign,
ushort channels,
uint samplesPerSecond,
byte[] buffer,
uint bufferOffset, /* number of bytes */
uint bufferLength /* number of bytes */
) : base(device)
{
FormatTag = formatTag;
BitsPerSample = bitsPerSample;
BlockAlign = blockAlign;
Channels = channels;
SamplesPerSecond = samplesPerSecond;
Handle = new FAudio.FAudioBuffer();
Handle.Flags = FAudio.FAUDIO_END_OF_STREAM;
Handle.pContext = IntPtr.Zero;
Handle.AudioBytes = bufferLength;
Handle.pAudioData = Marshal.AllocHGlobal((int) bufferLength);
Marshal.Copy(buffer, (int) bufferOffset, Handle.pAudioData, (int) bufferLength);
Handle.PlayBegin = 0;
Handle.PlayLength = 0;
if (formatTag == 1)
{
Handle.PlayLength = (uint) (
bufferLength /
channels /
(bitsPerSample / 8)
);
}
else if (formatTag == 2)
{
Handle.PlayLength = (uint) (
bufferLength /
blockAlign *
(((blockAlign / channels) - 6) * 2)
);
}
LoopStart = 0;
LoopLength = 0;
}
public StaticSound(
AudioDevice device,
ushort channels,
uint samplesPerSecond,
float[] buffer,
uint bufferOffset, /* in floats */
uint bufferLength /* in floats */
) : base(device)
{
FormatTag = 3;
BitsPerSample = 32;
BlockAlign = (ushort) (4 * channels);
Channels = channels;
SamplesPerSecond = samplesPerSecond;
var bufferLengthInBytes = (int) (bufferLength * sizeof(float));
Handle = new FAudio.FAudioBuffer();
Handle.Flags = FAudio.FAUDIO_END_OF_STREAM;
Handle.pContext = IntPtr.Zero;
Handle.AudioBytes = (uint) bufferLengthInBytes;
Handle.pAudioData = Marshal.AllocHGlobal(bufferLengthInBytes);
Marshal.Copy(buffer, (int) bufferOffset, Handle.pAudioData, (int) bufferLength);
Handle.PlayBegin = 0;
Handle.PlayLength = 0;
LoopStart = 0;
LoopLength = 0;
}
/// <summary>
/// Gets a sound instance from the pool.
/// NOTE: If you lose track of instances, you will create garbage collection pressure!
/// </summary>
public StaticSoundInstance GetInstance()
{
if (Instances.Count == 0)
{
Instances.Push(new StaticSoundInstance(Device, this));
}
return Instances.Pop();
}
internal void FreeInstance(StaticSoundInstance instance)
{
instance.Reset();
Instances.Push(instance);
}
protected override void Destroy()
{
Marshal.FreeHGlobal(Handle.pAudioData);
}
}
}

View File

@ -1,123 +0,0 @@
using System;
namespace MoonWorks.Audio
{
public class StaticSoundInstance : SoundInstance
{
public StaticSound Parent { get; }
public bool Loop { get; set; }
private SoundState _state = SoundState.Stopped;
public override SoundState State
{
get
{
FAudio.FAudioSourceVoice_GetState(
Handle,
out var state,
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
);
if (state.BuffersQueued == 0)
{
StopImmediate();
}
return _state;
}
protected set
{
_state = value;
}
}
internal StaticSoundInstance(
AudioDevice device,
StaticSound parent
) : base(device, parent.FormatTag, parent.BitsPerSample, parent.BlockAlign, parent.Channels, parent.SamplesPerSecond)
{
Parent = parent;
}
public override void Play()
{
if (State == SoundState.Playing)
{
return;
}
if (Loop)
{
Parent.Handle.LoopCount = 255;
Parent.Handle.LoopBegin = Parent.LoopStart;
Parent.Handle.LoopLength = Parent.LoopLength;
}
else
{
Parent.Handle.LoopCount = 0;
Parent.Handle.LoopBegin = 0;
Parent.Handle.LoopLength = 0;
}
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
Handle,
ref Parent.Handle,
IntPtr.Zero
);
FAudio.FAudioSourceVoice_Start(Handle, 0, 0);
State = SoundState.Playing;
}
public override void Pause()
{
if (State == SoundState.Paused)
{
FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
State = SoundState.Paused;
}
}
public override void Stop()
{
FAudio.FAudioSourceVoice_ExitLoop(Handle, 0);
State = SoundState.Stopped;
}
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)
{
FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
}
Parent.Handle.PlayBegin = sampleFrame;
}
public void Free()
{
Parent.FreeInstance(this);
}
internal void Reset()
{
Pan = 0;
Pitch = 0;
Volume = 1;
Reverb = 0;
Loop = false;
Is3D = false;
FilterType = FilterType.None;
Reverb = 0;
}
}
}

View File

@ -1,165 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
/// <summary>
/// For streaming long playback.
/// Must be extended with a decoder routine called by FillBuffer.
/// See StreamingSoundOgg for an example.
/// </summary>
public abstract class StreamingSound : SoundInstance
{
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 unsafe StreamingSound(
AudioDevice device,
ushort formatTag,
ushort bitsPerSample,
ushort blockAlign,
ushort channels,
uint samplesPerSecond
) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond)
{
device.AddDynamicSoundInstance(this);
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;
}
State = SoundState.Playing;
Update();
FAudio.FAudioSourceVoice_Start(Handle, 0, 0);
}
public override void Pause()
{
if (State == SoundState.Playing)
{
FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
State = SoundState.Paused;
}
}
public override void Stop()
{
State = SoundState.Stopped;
}
public override void StopImmediate()
{
FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
ClearBuffers();
State = SoundState.Stopped;
}
internal unsafe void Update()
{
if (State != SoundState.Playing)
{
return;
}
FAudio.FAudioSourceVoice_GetState(
Handle,
out var state,
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
);
queuedBufferCount = state.BuffersQueued;
QueueBuffers();
}
protected void QueueBuffers()
{
for (int i = 0; i < BUFFER_COUNT - queuedBufferCount; i += 1)
{
AddBuffer();
}
}
protected unsafe void ClearBuffers()
{
nextBufferIndex = 0;
queuedBufferCount = 0;
}
protected unsafe void AddBuffer()
{
var buffer = buffers[nextBufferIndex];
nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT;
FillBuffer(
(void*) buffer,
BUFFER_SIZE,
out int filledLengthInBytes,
out bool reachedEnd
);
FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer
{
AudioBytes = (uint) filledLengthInBytes,
pAudioData = (IntPtr) buffer,
PlayLength = (
(uint) (filledLengthInBytes /
Format.nChannels /
(uint) (Format.wBitsPerSample / 8))
)
};
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
Handle,
ref buf,
IntPtr.Zero
);
queuedBufferCount += 1;
/* We have reached the end of the file, what do we do? */
if (reachedEnd)
{
OnReachedEnd();
}
}
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 unsafe override void Destroy()
{
StopImmediate();
for (int i = 0; i < BUFFER_COUNT; i += 1)
{
NativeMemory.Free((void*) buffers[i]);
}
}
}
}

View File

@ -1,90 +0,0 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
public class StreamingSoundOgg : StreamingSoundSeekable
{
private IntPtr VorbisHandle;
private IntPtr FileDataPtr;
private FAudio.stb_vorbis_info Info;
protected override int BUFFER_SIZE => 32768;
public unsafe static StreamingSoundOgg Load(AudioDevice device, string filePath)
{
var fileData = File.ReadAllBytes(filePath);
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)
{
NativeMemory.Free(fileDataPtr);
Logger.LogError("Error opening OGG file!");
Logger.LogError("Error: " + error);
throw new AudioLoadException("Error opening OGG file!");
}
var info = FAudio.stb_vorbis_get_info(vorbisHandle);
return new StreamingSoundOgg(
device,
(IntPtr) fileDataPtr,
vorbisHandle,
info
);
}
internal StreamingSoundOgg(
AudioDevice device,
IntPtr fileDataPtr, // MUST BE A NATIVE MEMORY HANDLE!!
IntPtr vorbisHandle,
FAudio.stb_vorbis_info info
) : base(
device,
3, /* float type */
32, /* size of float */
(ushort) (4 * info.channels),
(ushort) info.channels,
info.sample_rate
)
{
FileDataPtr = fileDataPtr;
VorbisHandle = vorbisHandle;
Info = info;
}
public override void Seek(uint sampleFrame)
{
FAudio.stb_vorbis_seek(VorbisHandle, sampleFrame);
}
protected unsafe override void FillBuffer(
void* buffer,
int bufferLengthInBytes,
out int filledLengthInBytes,
out bool reachedEnd
)
{
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,
(IntPtr) buffer,
lengthInFloats
);
var sampleCount = samples * Info.channels;
reachedEnd = sampleCount < lengthInFloats;
filledLengthInBytes = sampleCount * sizeof(float);
}
protected unsafe override void Destroy()
{
FAudio.stb_vorbis_close(VorbisHandle);
NativeMemory.Free((void*) FileDataPtr);
}
}
}

View File

@ -1,25 +0,0 @@
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();
}
}
}
}

169
src/Audio/StreamingVoice.cs Normal file
View File

@ -0,0 +1,169 @@
using System;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
/// <summary>
/// Use in conjunction with an AudioDataStreamable object to play back streaming audio data.
/// </summary>
public class StreamingVoice : UpdatingSourceVoice, IPoolable<StreamingVoice>
{
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);
}
/// <summary>
/// Loads and prepares an AudioDataStreamable for streaming playback.
/// This automatically calls Load on the given AudioDataStreamable.
/// </summary>
public void Load(AudioDataStreamable data)
{
lock (StateLock)
{
if (AudioData != null)
{
AudioData.Unload();
}
data.Load();
AudioData = data;
InitializeBuffers();
QueueBuffers();
}
}
/// <summary>
/// Unloads AudioDataStreamable from this voice.
/// This automatically calls Unload on the given AudioDataStreamable.
/// </summary>
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()
{
var buffer = buffers[nextBufferIndex];
nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT;
AudioData.Decode(
(void*) buffer,
(int) BufferSize,
out int filledLengthInBytes,
out bool reachedEnd
);
if (filledLengthInBytes > 0)
{
var 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);
}
}
}

56
src/Audio/SubmixVoice.cs Normal file
View File

@ -0,0 +1,56 @@
using System;
namespace MoonWorks.Audio
{
/// <summary>
/// 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.
/// </summary>
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);
}
}
}

View File

@ -0,0 +1,29 @@
namespace MoonWorks.Audio
{
/// <summary>
/// TransientVoice is intended for playing one-off sound effects that don't have a long term reference. <br/>
/// It will be automatically returned to the AudioDevice SourceVoice pool once it is done playing back.
/// </summary>
public class TransientVoice : UpdatingSourceVoice, IPoolable<TransientVoice>
{
static TransientVoice IPoolable<TransientVoice>.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();
}
}
}
}
}

View File

@ -0,0 +1,11 @@
namespace MoonWorks.Audio
{
public abstract class UpdatingSourceVoice : SourceVoice
{
protected UpdatingSourceVoice(AudioDevice device, Format format) : base(device, format)
{
}
public abstract void Update();
}
}

578
src/Audio/Voice.cs Normal file
View File

@ -0,0 +1,578 @@
using System;
using System.Runtime.InteropServices;
using EasingFunction = System.Func<float, float>;
namespace MoonWorks.Audio
{
/// <summary>
/// Handles audio playback from audio buffer data. Can be configured with a variety of parameters.
/// </summary>
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;
/// <summary>
/// The strength of the doppler effect on this voice.
/// </summary>
public float DopplerFactor
{
get => dopplerFactor;
set
{
if (dopplerFactor != value)
{
dopplerFactor = value;
UpdatePitch();
}
}
}
private float volume = 1;
/// <summary>
/// The overall volume level for the voice.
/// </summary>
public float Volume
{
get => volume;
internal set
{
value = Math.MathHelper.Max(0, value);
if (volume != value)
{
volume = value;
FAudio.FAudioVoice_SetVolume(Handle, volume, 0);
}
}
}
private float pitch = 0;
/// <summary>
/// The pitch of the voice.
/// </summary>
public float Pitch
{
get => pitch;
internal set
{
value = Math.MathHelper.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
};
/// <summary>
/// The frequency cutoff on the voice filter.
/// </summary>
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
);
}
}
}
/// <summary>
/// Reciprocal of Q factor.
/// Controls how quickly frequencies beyond the filter frequency are dampened.
/// </summary>
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;
/// <summary>
/// The frequency filter that is applied to the voice.
/// </summary>
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;
/// <summary>
/// Left-right panning. -1 is hard left pan, 1 is hard right pan.
/// </summary>
public float Pan
{
get => pan;
internal set
{
value = Math.MathHelper.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;
/// <summary>
/// The wet-dry mix of the reverb effect.
/// Has no effect if SetReverbEffectChain has not been called.
/// </summary>
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)
{
Logger.LogWarn("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();
}
/// <summary>
/// Sets the pitch of the voice. Valid input range is -1f to 1f.
/// </summary>
public void SetPitch(float targetValue)
{
Pitch = targetValue;
Device.ClearTweens(this, AudioTweenProperty.Pitch);
}
/// <summary>
/// Sets the pitch of the voice over a time duration in seconds.
/// </summary>
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
public void SetPitch(float targetValue, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pitch, targetValue, duration, 0);
}
/// <summary>
/// Sets the pitch of the voice over a time duration in seconds after a delay in seconds.
/// </summary>
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
public void SetPitch(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pitch, targetValue, duration, delayTime);
}
/// <summary>
/// Sets the volume of the voice. Minimum value is 0f.
/// </summary>
public void SetVolume(float targetValue)
{
Volume = targetValue;
Device.ClearTweens(this, AudioTweenProperty.Volume);
}
/// <summary>
/// Sets the volume of the voice over a time duration in seconds.
/// </summary>
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
public void SetVolume(float targetValue, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, 0);
}
/// <summary>
/// Sets the volume of the voice over a time duration in seconds after a delay in seconds.
/// </summary>
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
public void SetVolume(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, delayTime);
}
/// <summary>
/// Sets the frequency cutoff on the voice filter. Valid range is 0.01f to 1f.
/// </summary>
public void SetFilterFrequency(float targetValue)
{
FilterFrequency = targetValue;
Device.ClearTweens(this, AudioTweenProperty.FilterFrequency);
}
/// <summary>
/// Sets the frequency cutoff on the voice filter over a time duration in seconds.
/// </summary>
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
public void SetFilterFrequency(float targetValue, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, 0);
}
/// <summary>
/// Sets the frequency cutoff on the voice filter over a time duration in seconds after a delay in seconds.
/// </summary>
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
public void SetFilterFrequency(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, delayTime);
}
/// <summary>
/// Sets reciprocal of Q factor on the frequency filter.
/// Controls how quickly frequencies beyond the filter frequency are dampened.
/// </summary>
public void SetFilterOneOverQ(float targetValue)
{
FilterOneOverQ = targetValue;
}
/// <summary>
/// Sets a left-right panning value. -1f is hard left pan, 1f is hard right pan.
/// </summary>
public virtual void SetPan(float targetValue)
{
Pan = targetValue;
Device.ClearTweens(this, AudioTweenProperty.Pan);
}
/// <summary>
/// Sets a left-right panning value over a time duration in seconds.
/// </summary>
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
public virtual void SetPan(float targetValue, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, 0);
}
/// <summary>
/// Sets a left-right panning value over a time duration in seconds after a delay in seconds.
/// </summary>
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
public virtual void SetPan(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, delayTime);
}
/// <summary>
/// Sets the wet-dry mix value of the reverb effect. Minimum value is 0f.
/// </summary>
public virtual void SetReverb(float targetValue)
{
Reverb = targetValue;
Device.ClearTweens(this, AudioTweenProperty.Reverb);
}
/// <summary>
/// Sets the wet-dry mix value of the reverb effect over a time duration in seconds.
/// </summary>
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
public virtual void SetReverb(float targetValue, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, 0);
}
/// <summary>
/// Sets the wet-dry mix value of the reverb effect over a time duration in seconds after a delay in seconds.
/// </summary>
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
public virtual void SetReverb(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
{
Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, delayTime);
}
/// <summary>
/// Sets the output voice for this voice.
/// </summary>
/// <param name="send">Where the output should be sent.</param>
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;
var sends = new FAudio.FAudioVoiceSends();
sends.SendCount = 1;
sends.pSends = (nint) sendDesc;
FAudio.FAudioVoice_SetOutputVoices(
Handle,
ref sends
);
}
}
/// <summary>
/// Applies a reverb effect chain to this voice.
/// </summary>
public unsafe void SetReverbEffectChain(ReverbEffect reverbEffect)
{
var sendDesc = stackalloc FAudio.FAudioSendDescriptor[2];
sendDesc[0].Flags = 0;
sendDesc[0].pOutputVoice = OutputVoice.Handle;
sendDesc[1].Flags = 0;
sendDesc[1].pOutputVoice = reverbEffect.Handle;
var sends = new FAudio.FAudioVoiceSends();
sends.SendCount = 2;
sends.pSends = (nint) sendDesc;
FAudio.FAudioVoice_SetOutputVoices(
Handle,
ref sends
);
ReverbEffect = reverbEffect;
}
/// <summary>
/// Removes the reverb effect chain from this voice.
/// </summary>
public void RemoveReverbEffectChain()
{
if (ReverbEffect != null)
{
ReverbEffect = null;
reverb = 0;
SetOutputVoice(OutputVoice);
}
}
/// <summary>
/// Resets all voice parameters to defaults.
/// </summary>
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);
}
}
}

View File

@ -1,181 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Fixed;
namespace MoonWorks.Collision.Fixed
{
/// <summary>
/// Axis-aligned bounding box.
/// </summary>
public struct AABB2D : System.IEquatable<AABB2D>
{
/// <summary>
/// The top-left position of the AABB.
/// </summary>
/// <value></value>
public Vector2 Min { get; private set; }
/// <summary>
/// The bottom-right position of the AABB.
/// </summary>
/// <value></value>
public Vector2 Max { get; private set; }
public Fix64 Width { get { return Max.X - Min.X; } }
public Fix64 Height { get { return Max.Y - Min.Y; } }
public Fix64 Right { get { return Max.X; } }
public Fix64 Left { get { return Min.X; } }
/// <summary>
/// The top of the AABB. Assumes a downward-aligned Y axis, so this value will be smaller than Bottom.
/// </summary>
/// <value></value>
public Fix64 Top { get { return Min.Y; } }
/// <summary>
/// The bottom of the AABB. Assumes a downward-aligned Y axis, so this value will be larger than Top.
/// </summary>
/// <value></value>
public Fix64 Bottom { get { return Max.Y; } }
public AABB2D(Fix64 minX, Fix64 minY, Fix64 maxX, Fix64 maxY)
{
Min = new Vector2(minX, minY);
Max = new Vector2(maxX, maxY);
}
public AABB2D(int minX, int minY, int maxX, int maxY)
{
Min = new Vector2(minX, minY);
Max = new Vector2(maxX, maxY);
}
public AABB2D(Vector2 min, Vector2 max)
{
Min = min;
Max = max;
}
private static Matrix3x2 AbsoluteMatrix(Matrix3x2 matrix)
{
return new Matrix3x2
(
Fix64.Abs(matrix.M11), Fix64.Abs(matrix.M12),
Fix64.Abs(matrix.M21), Fix64.Abs(matrix.M22),
Fix64.Abs(matrix.M31), Fix64.Abs(matrix.M32)
);
}
/// <summary>
/// Efficiently transforms the AABB by a Transform2D.
/// </summary>
/// <param name="aabb"></param>
/// <param name="transform"></param>
/// <returns></returns>
public static AABB2D Transformed(AABB2D aabb, Transform2D transform)
{
var two = new Fix64(2);
var center = (aabb.Min + aabb.Max) / two;
var extent = (aabb.Max - aabb.Min) / two;
var newCenter = Vector2.Transform(center, transform.TransformMatrix);
var newExtent = Vector2.TransformNormal(extent, AbsoluteMatrix(transform.TransformMatrix));
return new AABB2D(newCenter - newExtent, newCenter + newExtent);
}
public AABB2D Compose(AABB2D aabb)
{
Fix64 left = Left;
Fix64 top = Top;
Fix64 right = Right;
Fix64 bottom = Bottom;
if (aabb.Left < left)
{
left = aabb.Left;
}
if (aabb.Right > right)
{
right = aabb.Right;
}
if (aabb.Top < top)
{
top = aabb.Top;
}
if (aabb.Bottom > bottom)
{
bottom = aabb.Bottom;
}
return new AABB2D(left, top, right, bottom);
}
/// <summary>
/// Creates an AABB for an arbitrary collection of positions.
/// This is less efficient than defining a custom AABB method for most shapes, so avoid using this if possible.
/// </summary>
/// <param name="vertices"></param>
/// <returns></returns>
public static AABB2D FromVertices(IEnumerable<Vector2> vertices)
{
var minX = Fix64.MaxValue;
var minY = Fix64.MaxValue;
var maxX = Fix64.MinValue;
var maxY = Fix64.MinValue;
foreach (var vertex in vertices)
{
if (vertex.X < minX)
{
minX = vertex.X;
}
if (vertex.Y < minY)
{
minY = vertex.Y;
}
if (vertex.X > maxX)
{
maxX = vertex.X;
}
if (vertex.Y > maxY)
{
maxY = vertex.Y;
}
}
return new AABB2D(minX, minY, maxX, maxY);
}
public static bool TestOverlap(AABB2D a, AABB2D b)
{
return a.Left < b.Right && a.Right > b.Left && a.Top < b.Bottom && a.Bottom > b.Top;
}
public override bool Equals(object obj)
{
return obj is AABB2D aabb && Equals(aabb);
}
public bool Equals(AABB2D other)
{
return Min == other.Min &&
Max == other.Max;
}
public override int GetHashCode()
{
return System.HashCode.Combine(Min, Max);
}
public static bool operator ==(AABB2D left, AABB2D right)
{
return left.Equals(right);
}
public static bool operator !=(AABB2D left, AABB2D right)
{
return !(left == right);
}
}
}

View File

@ -1,12 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Fixed;
namespace MoonWorks.Collision.Fixed
{
public interface ICollidable
{
IEnumerable<IShape2D> Shapes { get; }
AABB2D AABB { get; }
AABB2D TransformedAABB(Transform2D transform);
}
}

View File

@ -1,15 +0,0 @@
using MoonWorks.Math.Fixed;
namespace MoonWorks.Collision.Fixed
{
public interface IShape2D : ICollidable, System.IEquatable<IShape2D>
{
/// <summary>
/// A Minkowski support function. Gives the farthest point on the edge of a shape along the given direction.
/// </summary>
/// <param name="direction">A normalized Vector2.</param>
/// <param name="transform">A Transform for transforming the shape vertices.</param>
/// <returns>The farthest point on the edge of the shape along the given direction.</returns>
Vector2 Support(Vector2 direction, Transform2D transform);
}
}

View File

@ -1,57 +0,0 @@
using MoonWorks.Math.Fixed;
namespace MoonWorks.Collision.Fixed
{
/// <summary>
/// A Minkowski difference between two shapes.
/// </summary>
public struct MinkowskiDifference : System.IEquatable<MinkowskiDifference>
{
private IShape2D ShapeA { get; }
private Transform2D TransformA { get; }
private IShape2D ShapeB { get; }
private Transform2D TransformB { get; }
public MinkowskiDifference(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB)
{
ShapeA = shapeA;
TransformA = transformA;
ShapeB = shapeB;
TransformB = transformB;
}
public Vector2 Support(Vector2 direction)
{
return ShapeA.Support(direction, TransformA) - ShapeB.Support(-direction, TransformB);
}
public override bool Equals(object other)
{
return other is MinkowskiDifference minkowskiDifference && Equals(minkowskiDifference);
}
public bool Equals(MinkowskiDifference other)
{
return
ShapeA == other.ShapeA &&
TransformA == other.TransformA &&
ShapeB == other.ShapeB &&
TransformB == other.TransformB;
}
public override int GetHashCode()
{
return System.HashCode.Combine(ShapeA, TransformA, ShapeB, TransformB);
}
public static bool operator ==(MinkowskiDifference a, MinkowskiDifference b)
{
return a.Equals(b);
}
public static bool operator !=(MinkowskiDifference a, MinkowskiDifference b)
{
return !(a == b);
}
}
}

View File

@ -1,333 +0,0 @@
using MoonWorks.Math.Fixed;
namespace MoonWorks.Collision.Fixed
{
public static class NarrowPhase
{
private struct Edge
{
public Fix64 Distance;
public Vector2 Normal;
public int Index;
}
public static bool TestCollision(ICollidable collidableA, Transform2D transformA, ICollidable collidableB, Transform2D transformB)
{
foreach (var shapeA in collidableA.Shapes)
{
foreach (var shapeB in collidableB.Shapes)
{
if (TestCollision(shapeA, transformA, shapeB, transformB))
{
return true;
}
}
}
return false;
}
public static bool TestCollision(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB)
{
// If we can use a fast path check, let's do that!
if (shapeA is Rectangle rectangleA && shapeB is Rectangle rectangleB && transformA.IsAxisAligned && transformB.IsAxisAligned)
{
return TestRectangleOverlap(rectangleA, transformA, rectangleB, transformB);
}
else if (shapeA is Point && shapeB is Rectangle && transformB.IsAxisAligned)
{
return TestPointRectangleOverlap((Point) shapeA, transformA, (Rectangle) shapeB, transformB);
}
else if (shapeA is Rectangle && shapeB is Point && transformA.IsAxisAligned)
{
return TestPointRectangleOverlap((Point) shapeB, transformB, (Rectangle) shapeA, transformA);
}
else if (shapeA is Rectangle && shapeB is Circle && transformA.IsAxisAligned && transformB.IsUniformScale)
{
return TestCircleRectangleOverlap((Circle) shapeB, transformB, (Rectangle) shapeA, transformA);
}
else if (shapeA is Circle && shapeB is Rectangle && transformA.IsUniformScale && transformB.IsAxisAligned)
{
return TestCircleRectangleOverlap((Circle) shapeA, transformA, (Rectangle) shapeB, transformB);
}
else if (shapeA is Circle && shapeB is Point && transformA.IsUniformScale)
{
return TestCirclePointOverlap((Circle) shapeA, transformA, (Point) shapeB, transformB);
}
else if (shapeA is Point && shapeB is Circle && transformB.IsUniformScale)
{
return TestCirclePointOverlap((Circle) shapeB, transformB, (Point) shapeA, transformA);
}
else if (shapeA is Circle circleA && shapeB is Circle circleB && transformA.IsUniformScale && transformB.IsUniformScale)
{
return TestCircleOverlap(circleA, transformA, circleB, transformB);
}
// Sad, we can't do a fast path optimization. Time for a simplex reduction.
return FindCollisionSimplex(shapeA, transformA, shapeB, transformB).Item1;
}
public static bool TestRectangleOverlap(Rectangle rectangleA, Transform2D transformA, Rectangle rectangleB, Transform2D transformB)
{
var firstAABB = rectangleA.TransformedAABB(transformA);
var secondAABB = rectangleB.TransformedAABB(transformB);
return firstAABB.Left < secondAABB.Right && firstAABB.Right > secondAABB.Left && firstAABB.Top < secondAABB.Bottom && firstAABB.Bottom > secondAABB.Top;
}
public static bool TestPointRectangleOverlap(Point point, Transform2D pointTransform, Rectangle rectangle, Transform2D rectangleTransform)
{
var transformedPoint = pointTransform.Position;
var AABB = rectangle.TransformedAABB(rectangleTransform);
return transformedPoint.X > AABB.Left && transformedPoint.X < AABB.Right && transformedPoint.Y < AABB.Bottom && transformedPoint.Y > AABB.Top;
}
public static bool TestCirclePointOverlap(Circle circle, Transform2D circleTransform, Point point, Transform2D pointTransform)
{
var circleCenter = circleTransform.Position;
var circleRadius = circle.Radius * circleTransform.Scale.X;
var distanceX = circleCenter.X - pointTransform.Position.X;
var distanceY = circleCenter.Y - pointTransform.Position.Y;
return (distanceX * distanceX) + (distanceY * distanceY) < (circleRadius * circleRadius);
}
/// <summary>
/// NOTE: The rectangle must be axis aligned, and the scaling of the circle must be uniform.
/// </summary>
public static bool TestCircleRectangleOverlap(Circle circle, Transform2D circleTransform, Rectangle rectangle, Transform2D rectangleTransform)
{
var circleCenter = circleTransform.Position;
var circleRadius = circle.Radius * circleTransform.Scale.X;
var AABB = rectangle.TransformedAABB(rectangleTransform);
var closestX = Fix64.Clamp(circleCenter.X, AABB.Left, AABB.Right);
var closestY = Fix64.Clamp(circleCenter.Y, AABB.Top, AABB.Bottom);
var distanceX = circleCenter.X - closestX;
var distanceY = circleCenter.Y - closestY;
var distanceSquared = (distanceX * distanceX) + (distanceY * distanceY);
return distanceSquared < (circleRadius * circleRadius);
}
public static bool TestCircleOverlap(Circle circleA, Transform2D transformA, Circle circleB, Transform2D transformB)
{
var radiusA = circleA.Radius * transformA.Scale.X;
var radiusB = circleB.Radius * transformB.Scale.Y;
var centerA = transformA.Position;
var centerB = transformB.Position;
var distanceSquared = (centerA - centerB).LengthSquared();
var radiusSumSquared = (radiusA + radiusB) * (radiusA + radiusB);
return distanceSquared < radiusSumSquared;
}
public static (bool, Simplex2D) FindCollisionSimplex(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB)
{
var minkowskiDifference = new MinkowskiDifference(shapeA, transformA, shapeB, transformB);
var c = minkowskiDifference.Support(Vector2.UnitX);
var b = minkowskiDifference.Support(-Vector2.UnitX);
return Check(minkowskiDifference, c, b);
}
public unsafe static Vector2 Intersect(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, Simplex2D simplex)
{
if (shapeA == null) { throw new System.ArgumentNullException(nameof(shapeA)); }
if (shapeB == null) { throw new System.ArgumentNullException(nameof(shapeB)); }
if (!simplex.TwoSimplex) { throw new System.ArgumentException("Simplex must be a 2-Simplex.", nameof(simplex)); }
var epsilon = Fix64.FromFraction(1, 10000);
var a = simplex.A;
var b = simplex.B.Value;
var c = simplex.C.Value;
Vector2 intersection = default;
for (var i = 0; i < 32; i++)
{
var edge = FindClosestEdge(simplex);
var support = CalculateSupport(shapeA, Transform2DA, shapeB, Transform2DB, edge.Normal);
var distance = Vector2.Dot(support, edge.Normal);
intersection = edge.Normal;
intersection *= distance;
if (Fix64.Abs(distance - edge.Distance) <= epsilon)
{
return intersection;
}
else
{
simplex.Insert(support, edge.Index);
}
}
return intersection; // close enough
}
private static unsafe Edge FindClosestEdge(Simplex2D simplex)
{
var closestDistance = Fix64.MaxValue;
var closestNormal = Vector2.Zero;
var closestIndex = 0;
for (var i = 0; i < 4; i += 1)
{
var j = (i + 1 == 3) ? 0 : i + 1;
var a = simplex[i];
var b = simplex[j];
var e = b - a;
var oa = a;
var n = Vector2.Normalize(TripleProduct(e, oa, e));
var d = Vector2.Dot(n, a);
if (d < closestDistance)
{
closestDistance = d;
closestNormal = n;
closestIndex = j;
}
}
return new Edge
{
Distance = closestDistance,
Normal = closestNormal,
Index = closestIndex
};
}
private static Vector2 CalculateSupport(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, Vector2 direction)
{
return shapeA.Support(direction, Transform2DA) - shapeB.Support(-direction, Transform2DB);
}
private static (bool, Simplex2D) Check(MinkowskiDifference minkowskiDifference, Vector2 c, Vector2 b)
{
var cb = c - b;
var c0 = -c;
var d = Direction(cb, c0);
return DoSimplex(minkowskiDifference, new Simplex2D(b, c), d);
}
private static (bool, Simplex2D) DoSimplex(MinkowskiDifference minkowskiDifference, Simplex2D simplex, Vector2 direction)
{
var a = minkowskiDifference.Support(direction);
var notPastOrigin = Vector2.Dot(a, direction) < Fix64.Zero;
var (intersects, newSimplex, newDirection) = EnclosesOrigin(a, simplex);
if (notPastOrigin)
{
return (false, default(Simplex2D));
}
else if (intersects)
{
return (true, new Simplex2D(simplex.A, simplex.B.Value, a));
}
else
{
return DoSimplex(minkowskiDifference, newSimplex, newDirection);
}
}
private static (bool, Simplex2D, Vector2) EnclosesOrigin(Vector2 a, Simplex2D simplex)
{
if (simplex.ZeroSimplex)
{
return HandleZeroSimplex(a, simplex.A);
}
else if (simplex.OneSimplex)
{
return HandleOneSimplex(a, simplex.A, simplex.B.Value);
}
else
{
return (false, simplex, Vector2.Zero);
}
}
private static (bool, Simplex2D, Vector2) HandleZeroSimplex(Vector2 a, Vector2 b)
{
var ab = b - a;
var a0 = -a;
var (newSimplex, newDirection) = SameDirection(ab, a0) ? (new Simplex2D(a, b), Perpendicular(ab, a0)) : (new Simplex2D(a), a0);
return (false, newSimplex, newDirection);
}
private static (bool, Simplex2D, Vector2) HandleOneSimplex(Vector2 a, Vector2 b, Vector2 c)
{
var a0 = -a;
var ab = b - a;
var ac = c - a;
var abp = Perpendicular(ab, -ac);
var acp = Perpendicular(ac, -ab);
if (SameDirection(abp, a0))
{
if (SameDirection(ab, a0))
{
return (false, new Simplex2D(a, b), abp);
}
else
{
return (false, new Simplex2D(a), a0);
}
}
else if (SameDirection(acp, a0))
{
if (SameDirection(ac, a0))
{
return (false, new Simplex2D(a, c), acp);
}
else
{
return (false, new Simplex2D(a), a0);
}
}
else
{
return (true, new Simplex2D(b, c), a0);
}
}
private static Vector2 TripleProduct(Vector2 a, Vector2 b, Vector2 c)
{
var A = new Vector3(a.X, a.Y, Fix64.Zero);
var B = new Vector3(b.X, b.Y, Fix64.Zero);
var C = new Vector3(c.X, c.Y, Fix64.Zero);
var first = Vector3.Cross(A, B);
var second = Vector3.Cross(first, C);
return new Vector2(second.X, second.Y);
}
private static Vector2 Direction(Vector2 a, Vector2 b)
{
var d = TripleProduct(a, b, a);
var collinear = d == Vector2.Zero;
return collinear ? new Vector2(a.Y, -a.X) : d;
}
private static bool SameDirection(Vector2 a, Vector2 b)
{
return Vector2.Dot(a, b) > Fix64.Zero;
}
private static Vector2 Perpendicular(Vector2 a, Vector2 b)
{
return TripleProduct(a, b, a);
}
}
}

View File

@ -1,73 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Fixed;
namespace MoonWorks.Collision.Fixed
{
/// <summary>
/// A Circle is a shape defined by a radius.
/// </summary>
public struct Circle : IShape2D, System.IEquatable<Circle>
{
public Fix64 Radius { get; }
public AABB2D AABB { get; }
public IEnumerable<IShape2D> Shapes
{
get
{
yield return this;
}
}
public Circle(Fix64 radius)
{
Radius = radius;
AABB = new AABB2D(-Radius, -Radius, Radius, Radius);
}
public Circle(int radius)
{
Radius = (Fix64) radius;
AABB = new AABB2D(-Radius, -Radius, Radius, Radius);
}
public Vector2 Support(Vector2 direction, Transform2D transform)
{
return Vector2.Transform(Vector2.Normalize(direction) * Radius, transform.TransformMatrix);
}
public AABB2D TransformedAABB(Transform2D transform2D)
{
return AABB2D.Transformed(AABB, transform2D);
}
public override bool Equals(object obj)
{
return obj is IShape2D other && Equals(other);
}
public bool Equals(IShape2D other)
{
return other is Circle circle && Equals(circle);
}
public bool Equals(Circle other)
{
return Radius == other.Radius;
}
public override int GetHashCode()
{
return System.HashCode.Combine(Radius);
}
public static bool operator ==(Circle a, Circle b)
{
return a.Equals(b);
}
public static bool operator !=(Circle a, Circle b)
{
return !(a == b);
}
}
}

View File

@ -1,83 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Fixed;
namespace MoonWorks.Collision.Fixed
{
/// <summary>
/// A line is a shape defined by exactly two points in space.
/// </summary>
public struct Line : IShape2D, System.IEquatable<Line>
{
public Vector2 Start { get; }
public Vector2 End { get; }
public AABB2D AABB { get; }
public IEnumerable<IShape2D> Shapes
{
get
{
yield return this;
}
}
public Line(Vector2 start, Vector2 end)
{
Start = start;
End = end;
AABB = new AABB2D(
Fix64.Min(Start.X, End.X),
Fix64.Min(Start.Y, End.Y),
Fix64.Max(Start.X, End.X),
Fix64.Max(Start.Y, End.Y)
);
}
public Vector2 Support(Vector2 direction, Transform2D transform)
{
var transformedStart = Vector2.Transform(Start, transform.TransformMatrix);
var transformedEnd = Vector2.Transform(End, transform.TransformMatrix);
return Vector2.Dot(transformedStart, direction) > Vector2.Dot(transformedEnd, direction) ?
transformedStart :
transformedEnd;
}
public AABB2D TransformedAABB(Transform2D transform)
{
return AABB2D.Transformed(AABB, transform);
}
public override bool Equals(object obj)
{
return obj is IShape2D other && Equals(other);
}
public bool Equals(IShape2D other)
{
return other is Line otherLine && Equals(otherLine);
}
public bool Equals(Line other)
{
return
(Start == other.Start && End == other.End) ||
(End == other.Start && Start == other.End);
}
public override int GetHashCode()
{
return System.HashCode.Combine(Start, End);
}
public static bool operator ==(Line a, Line b)
{
return a.Equals(b);
}
public static bool operator !=(Line a, Line b)
{
return !(a == b);
}
}
}

View File

@ -1,61 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Fixed;
namespace MoonWorks.Collision.Fixed
{
/// <summary>
/// A Point is "that which has no part".
/// All points by themselves are identical.
/// </summary>
public struct Point : IShape2D, System.IEquatable<Point>
{
public AABB2D AABB { get; }
public IEnumerable<IShape2D> Shapes
{
get
{
yield return this;
}
}
public AABB2D TransformedAABB(Transform2D transform)
{
return AABB2D.Transformed(AABB, transform);
}
public Vector2 Support(Vector2 direction, Transform2D transform)
{
return Vector2.Transform(Vector2.Zero, transform.TransformMatrix);
}
public override bool Equals(object obj)
{
return obj is IShape2D other && Equals(other);
}
public bool Equals(IShape2D other)
{
return other is Point otherPoint && Equals(otherPoint);
}
public bool Equals(Point other)
{
return true;
}
public override int GetHashCode()
{
return 0;
}
public static bool operator ==(Point a, Point b)
{
return true;
}
public static bool operator !=(Point a, Point b)
{
return false;
}
}
}

View File

@ -1,130 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Fixed;
namespace MoonWorks.Collision.Fixed
{
/// <summary>
/// A rectangle is a shape defined by a width and height. The origin is the center of the rectangle.
/// </summary>
public struct Rectangle : IShape2D, System.IEquatable<Rectangle>
{
public AABB2D AABB { get; }
public Fix64 Width { get; }
public Fix64 Height { get; }
public Fix64 Right { get; }
public Fix64 Left { get; }
public Fix64 Top { get; }
public Fix64 Bottom { get; }
public Vector2 TopLeft { get; }
public Vector2 BottomRight { get; }
public Vector2 Min { get; }
public Vector2 Max { get; }
public IEnumerable<IShape2D> Shapes
{
get
{
yield return this;
}
}
public Rectangle(Fix64 left, Fix64 top, Fix64 width, Fix64 height)
{
Width = width;
Height = height;
Left = left;
Right = left + width;
Top = top;
Bottom = top + height;
AABB = new AABB2D(left, top, Right, Bottom);
TopLeft = new Vector2(Left, Top);
BottomRight = new Vector2(Right, Bottom);
Min = AABB.Min;
Max = AABB.Max;
}
public Rectangle(int left, int top, int width, int height)
{
Width = (Fix64) width;
Height = (Fix64) height;
Left = (Fix64) left;
Right = (Fix64) (left + width);
Top = (Fix64) top;
Bottom = (Fix64) (top + height);
AABB = new AABB2D(Left, Top, Right, Bottom);
TopLeft = new Vector2(Left, Top);
BottomRight = new Vector2(Right, Bottom);
Min = AABB.Min;
Max = AABB.Max;
}
private Vector2 Support(Vector2 direction)
{
if (direction.X >= Fix64.Zero && direction.Y >= Fix64.Zero)
{
return Max;
}
else if (direction.X >= Fix64.Zero && direction.Y < Fix64.Zero)
{
return new Vector2(Max.X, Min.Y);
}
else if (direction.X < Fix64.Zero && direction.Y >= Fix64.Zero)
{
return new Vector2(Min.X, Max.Y);
}
else if (direction.X < Fix64.Zero && direction.Y < Fix64.Zero)
{
return new Vector2(Min.X, Min.Y);
}
else
{
throw new System.ArgumentException("Support vector direction cannot be zero.");
}
}
public Vector2 Support(Vector2 direction, Transform2D transform)
{
Matrix3x2 inverseTransform;
Matrix3x2.Invert(transform.TransformMatrix, out inverseTransform);
var inverseDirection = Vector2.TransformNormal(direction, inverseTransform);
return Vector2.Transform(Support(inverseDirection), transform.TransformMatrix);
}
public AABB2D TransformedAABB(Transform2D transform)
{
return AABB2D.Transformed(AABB, transform);
}
public override bool Equals(object obj)
{
return obj is IShape2D other && Equals(other);
}
public bool Equals(IShape2D other)
{
return (other is Rectangle rectangle && Equals(rectangle));
}
public bool Equals(Rectangle other)
{
return Min == other.Min && Max == other.Max;
}
public override int GetHashCode()
{
return System.HashCode.Combine(Min, Max);
}
public static bool operator ==(Rectangle a, Rectangle b)
{
return a.Equals(b);
}
public static bool operator !=(Rectangle a, Rectangle b)
{
return !(a == b);
}
}
}

View File

@ -1,136 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Fixed;
namespace MoonWorks.Collision.Fixed
{
/// <summary>
/// A simplex is a shape with up to n - 2 vertices in the nth dimension.
/// </summary>
public struct Simplex2D : System.IEquatable<Simplex2D>
{
private Vector2 a;
private Vector2? b;
private Vector2? c;
public Vector2 A => a;
public Vector2? B => b;
public Vector2? C => c;
public bool ZeroSimplex { get { return !b.HasValue && !c.HasValue; } }
public bool OneSimplex { get { return b.HasValue && !c.HasValue; } }
public bool TwoSimplex { get { return b.HasValue && c.HasValue; } }
public int Count => TwoSimplex ? 3 : (OneSimplex ? 2 : 1);
public Simplex2D(Vector2 a)
{
this.a = a;
b = null;
c = null;
}
public Simplex2D(Vector2 a, Vector2 b)
{
this.a = a;
this.b = b;
c = null;
}
public Simplex2D(Vector2 a, Vector2 b, Vector2 c)
{
this.a = a;
this.b = b;
this.c = c;
}
public Vector2 this[int index]
{
get
{
if (index == 0) { return a; }
if (index == 1) { return b.Value; }
if (index == 2) { return c.Value; }
throw new System.IndexOutOfRangeException();
}
}
public IEnumerable<Vector2> Vertices
{
get
{
yield return (Vector2) a;
if (b.HasValue) { yield return (Vector2) b; }
if (c.HasValue) { yield return (Vector2) c; }
}
}
public Vector2 Support(Vector2 direction, Transform2D transform)
{
var maxDotProduct = Fix64.MinValue;
var maxVertex = a;
foreach (var vertex in Vertices)
{
var transformed = Vector2.Transform(vertex, transform.TransformMatrix);
var dot = Vector2.Dot(transformed, direction);
if (dot > maxDotProduct)
{
maxVertex = transformed;
maxDotProduct = dot;
}
}
return maxVertex;
}
public void Insert(Vector2 point, int index)
{
if (index == 0)
{
c = b;
b = a;
a = point;
}
else if (index == 1)
{
c = b;
b = point;
}
else
{
c = point;
}
}
public override bool Equals(object obj)
{
return obj is Simplex2D other && Equals(other);
}
public bool Equals(Simplex2D other)
{
if (Count != other.Count) { return false; }
return
(A == other.A && B == other.B && C == other.C) ||
(A == other.A && B == other.C && C == other.B) ||
(A == other.B && B == other.A && C == other.C) ||
(A == other.B && B == other.C && C == other.A) ||
(A == other.C && B == other.A && C == other.B) ||
(A == other.C && B == other.B && C == other.A);
}
public override int GetHashCode()
{
return System.HashCode.Combine(Vertices);
}
public static bool operator ==(Simplex2D a, Simplex2D b)
{
return a.Equals(b);
}
public static bool operator !=(Simplex2D a, Simplex2D b)
{
return !(a == b);
}
}
}

View File

@ -1,253 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Fixed;
namespace MoonWorks.Collision.Fixed
{
/// <summary>
/// Used to quickly check if two shapes are potentially overlapping.
/// </summary>
/// <typeparam name="T">The type that will be used to uniquely identify shape-transform pairs.</typeparam>
public class SpatialHash2D<T> where T : System.IEquatable<T>
{
private readonly Fix64 cellSize;
private readonly Dictionary<long, HashSet<T>> hashDictionary = new Dictionary<long, HashSet<T>>();
// FIXME: this ICollidable causes boxing which triggers garbage collection
private readonly Dictionary<T, (ICollidable, Transform2D, uint)> IDLookup = new Dictionary<T, (ICollidable, Transform2D, uint)>();
public int MinX { get; private set; } = 0;
public int MaxX { get; private set; } = 0;
public int MinY { get; private set; } = 0;
public int MaxY { get; private set; } = 0;
private Queue<HashSet<T>> hashSetPool = new Queue<HashSet<T>>();
public SpatialHash2D(int cellSize)
{
this.cellSize = new Fix64(cellSize);
}
private (int, int) Hash(Vector2 position)
{
return ((int) (position.X / cellSize), (int) (position.Y / cellSize));
}
/// <summary>
/// Inserts an element into the SpatialHash.
/// </summary>
/// <param name="id">A unique ID for the shape-transform pair.</param>
/// <param name="shape"></param>
/// <param name="transform2D"></param>
/// <param name="collisionGroups">A bitmask value specifying the groups this object belongs to.</param>
public void Insert(T id, ICollidable shape, Transform2D transform2D, uint collisionGroups = uint.MaxValue)
{
var box = shape.TransformedAABB(transform2D);
var minHash = Hash(box.Min);
var maxHash = Hash(box.Max);
foreach (var key in Keys(minHash.Item1, minHash.Item2, maxHash.Item1, maxHash.Item2))
{
if (!hashDictionary.ContainsKey(key))
{
hashDictionary.Add(key, new HashSet<T>());
}
hashDictionary[key].Add(id);
IDLookup[id] = (shape, transform2D, collisionGroups);
}
MinX = System.Math.Min(MinX, minHash.Item1);
MinY = System.Math.Min(MinY, minHash.Item2);
MaxX = System.Math.Max(MaxX, maxHash.Item1);
MaxY = System.Math.Max(MaxY, maxHash.Item2);
}
/// <summary>
/// Retrieves all the potential collisions of a shape-transform pair. Excludes any shape-transforms with the given ID.
/// </summary>
public IEnumerable<(T, ICollidable, Transform2D, uint)> Retrieve(T id, ICollidable shape, Transform2D transform2D, uint collisionMask = uint.MaxValue)
{
var returned = AcquireHashSet();
var box = shape.TransformedAABB(transform2D);
var (minX, minY) = Hash(box.Min);
var (maxX, maxY) = Hash(box.Max);
if (minX < MinX) { minX = MinX; }
if (maxX > MaxX) { maxX = MaxX; }
if (minY < MinY) { minY = MinY; }
if (maxY > MaxY) { maxY = MaxY; }
foreach (var key in Keys(minX, minY, maxX, maxY))
{
if (hashDictionary.ContainsKey(key))
{
foreach (var t in hashDictionary[key])
{
if (!returned.Contains(t))
{
var (otherShape, otherTransform, collisionGroups) = IDLookup[t];
if (!id.Equals(t) && ((collisionGroups & collisionMask) > 0) && AABB2D.TestOverlap(box, otherShape.TransformedAABB(otherTransform)))
{
returned.Add(t);
yield return (t, otherShape, otherTransform, collisionGroups);
}
}
}
}
}
FreeHashSet(returned);
}
/// <summary>
/// Retrieves all the potential collisions of a shape-transform pair.
/// </summary>
public IEnumerable<(T, ICollidable, Transform2D, uint)> Retrieve(ICollidable shape, Transform2D transform2D, uint collisionMask = uint.MaxValue)
{
var returned = AcquireHashSet();
var box = shape.TransformedAABB(transform2D);
var (minX, minY) = Hash(box.Min);
var (maxX, maxY) = Hash(box.Max);
if (minX < MinX) { minX = MinX; }
if (maxX > MaxX) { maxX = MaxX; }
if (minY < MinY) { minY = MinY; }
if (maxY > MaxY) { maxY = MaxY; }
foreach (var key in Keys(minX, minY, maxX, maxY))
{
if (hashDictionary.ContainsKey(key))
{
foreach (var t in hashDictionary[key])
{
if (!returned.Contains(t))
{
var (otherShape, otherTransform, collisionGroups) = IDLookup[t];
if (((collisionGroups & collisionMask) > 0) && AABB2D.TestOverlap(box, otherShape.TransformedAABB(otherTransform)))
{
returned.Add(t);
yield return (t, otherShape, otherTransform, collisionGroups);
}
}
}
}
}
FreeHashSet(returned);
}
/// <summary>
/// Retrieves objects based on a pre-transformed AABB.
/// </summary>
/// <param name="aabb">A transformed AABB.</param>
/// <returns></returns>
public IEnumerable<(T, ICollidable, Transform2D, uint)> Retrieve(AABB2D aabb, uint collisionMask = uint.MaxValue)
{
var returned = AcquireHashSet();
var (minX, minY) = Hash(aabb.Min);
var (maxX, maxY) = Hash(aabb.Max);
if (minX < MinX) { minX = MinX; }
if (maxX > MaxX) { maxX = MaxX; }
if (minY < MinY) { minY = MinY; }
if (maxY > MaxY) { maxY = MaxY; }
foreach (var key in Keys(minX, minY, maxX, maxY))
{
if (hashDictionary.ContainsKey(key))
{
foreach (var t in hashDictionary[key])
{
if (!returned.Contains(t))
{
var (otherShape, otherTransform, collisionGroups) = IDLookup[t];
if (((collisionGroups & collisionMask) > 0) && AABB2D.TestOverlap(aabb, otherShape.TransformedAABB(otherTransform)))
{
yield return (t, otherShape, otherTransform, collisionGroups);
}
}
}
}
}
FreeHashSet(returned);
}
public void Update(T id, ICollidable shape, Transform2D transform2D, uint collisionGroups = uint.MaxValue)
{
Remove(id);
Insert(id, shape, transform2D, collisionGroups);
}
/// <summary>
/// Removes a specific ID from the SpatialHash.
/// </summary>
public void Remove(T id)
{
var (shape, transform, collisionGroups) = IDLookup[id];
var box = shape.TransformedAABB(transform);
var minHash = Hash(box.Min);
var maxHash = Hash(box.Max);
foreach (var key in Keys(minHash.Item1, minHash.Item2, maxHash.Item1, maxHash.Item2))
{
if (hashDictionary.ContainsKey(key))
{
hashDictionary[key].Remove(id);
}
}
IDLookup.Remove(id);
}
/// <summary>
/// Removes everything that has been inserted into the SpatialHash.
/// </summary>
public void Clear()
{
foreach (var hash in hashDictionary.Values)
{
hash.Clear();
}
IDLookup.Clear();
}
private static long MakeLong(int left, int right)
{
return ((long) left << 32) | ((uint) right);
}
private IEnumerable<long> Keys(int minX, int minY, int maxX, int maxY)
{
for (var i = minX; i <= maxX; i++)
{
for (var j = minY; j <= maxY; j++)
{
yield return MakeLong(i, j);
}
}
}
private HashSet<T> AcquireHashSet()
{
if (hashSetPool.Count == 0)
{
hashSetPool.Enqueue(new HashSet<T>());
}
var hashSet = hashSetPool.Dequeue();
hashSet.Clear();
return hashSet;
}
private void FreeHashSet(HashSet<T> hashSet)
{
hashSetPool.Enqueue(hashSet);
}
}
}

View File

@ -1,174 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Float;
namespace MoonWorks.Collision.Float
{
/// <summary>
/// Axis-aligned bounding box.
/// </summary>
public struct AABB2D : System.IEquatable<AABB2D>
{
/// <summary>
/// The top-left position of the AABB.
/// </summary>
/// <value></value>
public Vector2 Min { get; private set; }
/// <summary>
/// The bottom-right position of the AABB.
/// </summary>
/// <value></value>
public Vector2 Max { get; private set; }
public float Width { get { return Max.X - Min.X; } }
public float Height { get { return Max.Y - Min.Y; } }
public float Right { get { return Max.X; } }
public float Left { get { return Min.X; } }
/// <summary>
/// The top of the AABB. Assumes a downward-aligned Y axis, so this value will be smaller than Bottom.
/// </summary>
/// <value></value>
public float Top { get { return Min.Y; } }
/// <summary>
/// The bottom of the AABB. Assumes a downward-aligned Y axis, so this value will be larger than Top.
/// </summary>
/// <value></value>
public float Bottom { get { return Max.Y; } }
public AABB2D(float minX, float minY, float maxX, float maxY)
{
Min = new Vector2(minX, minY);
Max = new Vector2(maxX, maxY);
}
public AABB2D(Vector2 min, Vector2 max)
{
Min = min;
Max = max;
}
private static Matrix3x2 AbsoluteMatrix(Matrix3x2 matrix)
{
return new Matrix3x2
(
System.Math.Abs(matrix.M11), System.Math.Abs(matrix.M12),
System.Math.Abs(matrix.M21), System.Math.Abs(matrix.M22),
System.Math.Abs(matrix.M31), System.Math.Abs(matrix.M32)
);
}
/// <summary>
/// Efficiently transforms the AABB by a Transform2D.
/// </summary>
/// <param name="aabb"></param>
/// <param name="transform"></param>
/// <returns></returns>
public static AABB2D Transformed(AABB2D aabb, Transform2D transform)
{
var center = (aabb.Min + aabb.Max) / 2f;
var extent = (aabb.Max - aabb.Min) / 2f;
var newCenter = Vector2.Transform(center, transform.TransformMatrix);
var newExtent = Vector2.TransformNormal(extent, AbsoluteMatrix(transform.TransformMatrix));
return new AABB2D(newCenter - newExtent, newCenter + newExtent);
}
public AABB2D Compose(AABB2D aabb)
{
float left = Left;
float top = Top;
float right = Right;
float bottom = Bottom;
if (aabb.Left < left)
{
left = aabb.Left;
}
if (aabb.Right > right)
{
right = aabb.Right;
}
if (aabb.Top < top)
{
top = aabb.Top;
}
if (aabb.Bottom > bottom)
{
bottom = aabb.Bottom;
}
return new AABB2D(left, top, right, bottom);
}
/// <summary>
/// Creates an AABB for an arbitrary collection of positions.
/// This is less efficient than defining a custom AABB method for most shapes, so avoid using this if possible.
/// </summary>
/// <param name="vertices"></param>
/// <returns></returns>
public static AABB2D FromVertices(IEnumerable<Vector2> vertices)
{
var minX = float.MaxValue;
var minY = float.MaxValue;
var maxX = float.MinValue;
var maxY = float.MinValue;
foreach (var vertex in vertices)
{
if (vertex.X < minX)
{
minX = vertex.X;
}
if (vertex.Y < minY)
{
minY = vertex.Y;
}
if (vertex.X > maxX)
{
maxX = vertex.X;
}
if (vertex.Y > maxY)
{
maxY = vertex.Y;
}
}
return new AABB2D(minX, minY, maxX, maxY);
}
public static bool TestOverlap(AABB2D a, AABB2D b)
{
return a.Left < b.Right && a.Right > b.Left && a.Top < b.Bottom && a.Bottom > b.Top;
}
public override bool Equals(object obj)
{
return obj is AABB2D aabb && Equals(aabb);
}
public bool Equals(AABB2D other)
{
return Min == other.Min &&
Max == other.Max;
}
public override int GetHashCode()
{
return System.HashCode.Combine(Min, Max);
}
public static bool operator ==(AABB2D left, AABB2D right)
{
return left.Equals(right);
}
public static bool operator !=(AABB2D left, AABB2D right)
{
return !(left == right);
}
}
}

View File

@ -1,12 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Float;
namespace MoonWorks.Collision.Float
{
public interface ICollidable
{
IEnumerable<IShape2D> Shapes { get; }
AABB2D AABB { get; }
AABB2D TransformedAABB(Transform2D transform);
}
}

View File

@ -1,15 +0,0 @@
using MoonWorks.Math.Float;
namespace MoonWorks.Collision.Float
{
public interface IShape2D : ICollidable, System.IEquatable<IShape2D>
{
/// <summary>
/// A Minkowski support function. Gives the farthest point on the edge of a shape along the given direction.
/// </summary>
/// <param name="direction">A normalized Vector2.</param>
/// <param name="transform">A Transform for transforming the shape vertices.</param>
/// <returns>The farthest point on the edge of the shape along the given direction.</returns>
Vector2 Support(Vector2 direction, Transform2D transform);
}
}

View File

@ -1,57 +0,0 @@
using MoonWorks.Math.Float;
namespace MoonWorks.Collision.Float
{
/// <summary>
/// A Minkowski difference between two shapes.
/// </summary>
public struct MinkowskiDifference : System.IEquatable<MinkowskiDifference>
{
private IShape2D ShapeA { get; }
private Transform2D TransformA { get; }
private IShape2D ShapeB { get; }
private Transform2D TransformB { get; }
public MinkowskiDifference(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB)
{
ShapeA = shapeA;
TransformA = transformA;
ShapeB = shapeB;
TransformB = transformB;
}
public Vector2 Support(Vector2 direction)
{
return ShapeA.Support(direction, TransformA) - ShapeB.Support(-direction, TransformB);
}
public override bool Equals(object other)
{
return other is MinkowskiDifference minkowskiDifference && Equals(minkowskiDifference);
}
public bool Equals(MinkowskiDifference other)
{
return
ShapeA == other.ShapeA &&
TransformA == other.TransformA &&
ShapeB == other.ShapeB &&
TransformB == other.TransformB;
}
public override int GetHashCode()
{
return System.HashCode.Combine(ShapeA, TransformA, ShapeB, TransformB);
}
public static bool operator ==(MinkowskiDifference a, MinkowskiDifference b)
{
return a.Equals(b);
}
public static bool operator !=(MinkowskiDifference a, MinkowskiDifference b)
{
return !(a == b);
}
}
}

View File

@ -1,331 +0,0 @@
using MoonWorks.Math.Float;
namespace MoonWorks.Collision.Float
{
public static class NarrowPhase
{
private struct Edge
{
public float Distance;
public Vector2 Normal;
public int Index;
}
public static bool TestCollision(ICollidable collidableA, Transform2D transformA, ICollidable collidableB, Transform2D transformB)
{
foreach (var shapeA in collidableA.Shapes)
{
foreach (var shapeB in collidableB.Shapes)
{
if (TestCollision(shapeA, transformA, shapeB, transformB))
{
return true;
}
}
}
return false;
}
public static bool TestCollision(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB)
{
// If we can use a fast path check, let's do that!
if (shapeA is Rectangle rectangleA && shapeB is Rectangle rectangleB && transformA.IsAxisAligned && transformB.IsAxisAligned)
{
return TestRectangleOverlap(rectangleA, transformA, rectangleB, transformB);
}
else if (shapeA is Point && shapeB is Rectangle && transformB.IsAxisAligned)
{
return TestPointRectangleOverlap((Point) shapeA, transformA, (Rectangle) shapeB, transformB);
}
else if (shapeA is Rectangle && shapeB is Point && transformA.IsAxisAligned)
{
return TestPointRectangleOverlap((Point) shapeB, transformB, (Rectangle) shapeA, transformA);
}
else if (shapeA is Rectangle && shapeB is Circle && transformA.IsAxisAligned && transformB.IsUniformScale)
{
return TestCircleRectangleOverlap((Circle) shapeB, transformB, (Rectangle) shapeA, transformA);
}
else if (shapeA is Circle && shapeB is Rectangle && transformA.IsUniformScale && transformB.IsAxisAligned)
{
return TestCircleRectangleOverlap((Circle) shapeA, transformA, (Rectangle) shapeB, transformB);
}
else if (shapeA is Circle && shapeB is Point && transformA.IsUniformScale)
{
return TestCirclePointOverlap((Circle) shapeA, transformA, (Point) shapeB, transformB);
}
else if (shapeA is Point && shapeB is Circle && transformB.IsUniformScale)
{
return TestCirclePointOverlap((Circle) shapeB, transformB, (Point) shapeA, transformA);
}
else if (shapeA is Circle circleA && shapeB is Circle circleB && transformA.IsUniformScale && transformB.IsUniformScale)
{
return TestCircleOverlap(circleA, transformA, circleB, transformB);
}
// Sad, we can't do a fast path optimization. Time for a simplex reduction.
return FindCollisionSimplex(shapeA, transformA, shapeB, transformB).Item1;
}
public static bool TestRectangleOverlap(Rectangle rectangleA, Transform2D transformA, Rectangle rectangleB, Transform2D transformB)
{
var firstAABB = rectangleA.TransformedAABB(transformA);
var secondAABB = rectangleB.TransformedAABB(transformB);
return firstAABB.Left < secondAABB.Right && firstAABB.Right > secondAABB.Left && firstAABB.Top < secondAABB.Bottom && firstAABB.Bottom > secondAABB.Top;
}
public static bool TestPointRectangleOverlap(Point point, Transform2D pointTransform, Rectangle rectangle, Transform2D rectangleTransform)
{
var transformedPoint = pointTransform.Position;
var AABB = rectangle.TransformedAABB(rectangleTransform);
return transformedPoint.X > AABB.Left && transformedPoint.X < AABB.Right && transformedPoint.Y < AABB.Bottom && transformedPoint.Y > AABB.Top;
}
public static bool TestCirclePointOverlap(Circle circle, Transform2D circleTransform, Point point, Transform2D pointTransform)
{
var circleCenter = circleTransform.Position;
var circleRadius = circle.Radius * circleTransform.Scale.X;
var distanceX = circleCenter.X - pointTransform.Position.X;
var distanceY = circleCenter.Y - pointTransform.Position.Y;
return (distanceX * distanceX) + (distanceY * distanceY) < (circleRadius * circleRadius);
}
/// <summary>
/// NOTE: The rectangle must be axis aligned, and the scaling of the circle must be uniform.
/// </summary>
public static bool TestCircleRectangleOverlap(Circle circle, Transform2D circleTransform, Rectangle rectangle, Transform2D rectangleTransform)
{
var circleCenter = circleTransform.Position;
var circleRadius = circle.Radius * circleTransform.Scale.X;
var AABB = rectangle.TransformedAABB(rectangleTransform);
var closestX = Math.MathHelper.Clamp(circleCenter.X, AABB.Left, AABB.Right);
var closestY = Math.MathHelper.Clamp(circleCenter.Y, AABB.Top, AABB.Bottom);
var distanceX = circleCenter.X - closestX;
var distanceY = circleCenter.Y - closestY;
var distanceSquared = (distanceX * distanceX) + (distanceY * distanceY);
return distanceSquared < (circleRadius * circleRadius);
}
public static bool TestCircleOverlap(Circle circleA, Transform2D transformA, Circle circleB, Transform2D transformB)
{
var radiusA = circleA.Radius * transformA.Scale.X;
var radiusB = circleB.Radius * transformB.Scale.Y;
var centerA = transformA.Position;
var centerB = transformB.Position;
var distanceSquared = (centerA - centerB).LengthSquared();
var radiusSumSquared = (radiusA + radiusB) * (radiusA + radiusB);
return distanceSquared < radiusSumSquared;
}
public static (bool, Simplex2D) FindCollisionSimplex(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB)
{
var minkowskiDifference = new MinkowskiDifference(shapeA, transformA, shapeB, transformB);
var c = minkowskiDifference.Support(Vector2.UnitX);
var b = minkowskiDifference.Support(-Vector2.UnitX);
return Check(minkowskiDifference, c, b);
}
public unsafe static Vector2 Intersect(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, Simplex2D simplex)
{
if (shapeA == null) { throw new System.ArgumentNullException(nameof(shapeA)); }
if (shapeB == null) { throw new System.ArgumentNullException(nameof(shapeB)); }
if (!simplex.TwoSimplex) { throw new System.ArgumentException("Simplex must be a 2-Simplex.", nameof(simplex)); }
var a = simplex.A;
var b = simplex.B.Value;
var c = simplex.C.Value;
Vector2 intersection = default;
for (var i = 0; i < 32; i++)
{
var edge = FindClosestEdge(simplex);
var support = CalculateSupport(shapeA, Transform2DA, shapeB, Transform2DB, edge.Normal);
var distance = Vector2.Dot(support, edge.Normal);
intersection = edge.Normal;
intersection *= distance;
if (System.Math.Abs(distance - edge.Distance) <= 0.00001f)
{
return intersection;
}
else
{
simplex.Insert(support, edge.Index);
}
}
return intersection; // close enough
}
private static unsafe Edge FindClosestEdge(Simplex2D simplex)
{
var closestDistance = float.PositiveInfinity;
var closestNormal = Vector2.Zero;
var closestIndex = 0;
for (var i = 0; i < 4; i += 1)
{
var j = (i + 1 == 3) ? 0 : i + 1;
var a = simplex[i];
var b = simplex[j];
var e = b - a;
var oa = a;
var n = Vector2.Normalize(TripleProduct(e, oa, e));
var d = Vector2.Dot(n, a);
if (d < closestDistance)
{
closestDistance = d;
closestNormal = n;
closestIndex = j;
}
}
return new Edge
{
Distance = closestDistance,
Normal = closestNormal,
Index = closestIndex
};
}
private static Vector2 CalculateSupport(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, Vector2 direction)
{
return shapeA.Support(direction, Transform2DA) - shapeB.Support(-direction, Transform2DB);
}
private static (bool, Simplex2D) Check(MinkowskiDifference minkowskiDifference, Vector2 c, Vector2 b)
{
var cb = c - b;
var c0 = -c;
var d = Direction(cb, c0);
return DoSimplex(minkowskiDifference, new Simplex2D(b, c), d);
}
private static (bool, Simplex2D) DoSimplex(MinkowskiDifference minkowskiDifference, Simplex2D simplex, Vector2 direction)
{
var a = minkowskiDifference.Support(direction);
var notPastOrigin = Vector2.Dot(a, direction) < 0;
var (intersects, newSimplex, newDirection) = EnclosesOrigin(a, simplex);
if (notPastOrigin)
{
return (false, default(Simplex2D));
}
else if (intersects)
{
return (true, new Simplex2D(simplex.A, simplex.B.Value, a));
}
else
{
return DoSimplex(minkowskiDifference, newSimplex, newDirection);
}
}
private static (bool, Simplex2D, Vector2) EnclosesOrigin(Vector2 a, Simplex2D simplex)
{
if (simplex.ZeroSimplex)
{
return HandleZeroSimplex(a, simplex.A);
}
else if (simplex.OneSimplex)
{
return HandleOneSimplex(a, simplex.A, simplex.B.Value);
}
else
{
return (false, simplex, Vector2.Zero);
}
}
private static (bool, Simplex2D, Vector2) HandleZeroSimplex(Vector2 a, Vector2 b)
{
var ab = b - a;
var a0 = -a;
var (newSimplex, newDirection) = SameDirection(ab, a0) ? (new Simplex2D(a, b), Perpendicular(ab, a0)) : (new Simplex2D(a), a0);
return (false, newSimplex, newDirection);
}
private static (bool, Simplex2D, Vector2) HandleOneSimplex(Vector2 a, Vector2 b, Vector2 c)
{
var a0 = -a;
var ab = b - a;
var ac = c - a;
var abp = Perpendicular(ab, -ac);
var acp = Perpendicular(ac, -ab);
if (SameDirection(abp, a0))
{
if (SameDirection(ab, a0))
{
return (false, new Simplex2D(a, b), abp);
}
else
{
return (false, new Simplex2D(a), a0);
}
}
else if (SameDirection(acp, a0))
{
if (SameDirection(ac, a0))
{
return (false, new Simplex2D(a, c), acp);
}
else
{
return (false, new Simplex2D(a), a0);
}
}
else
{
return (true, new Simplex2D(b, c), a0);
}
}
private static Vector2 TripleProduct(Vector2 a, Vector2 b, Vector2 c)
{
var A = new Vector3(a.X, a.Y, 0);
var B = new Vector3(b.X, b.Y, 0);
var C = new Vector3(c.X, c.Y, 0);
var first = Vector3.Cross(A, B);
var second = Vector3.Cross(first, C);
return new Vector2(second.X, second.Y);
}
private static Vector2 Direction(Vector2 a, Vector2 b)
{
var d = TripleProduct(a, b, a);
var collinear = d == Vector2.Zero;
return collinear ? new Vector2(a.Y, -a.X) : d;
}
private static bool SameDirection(Vector2 a, Vector2 b)
{
return Vector2.Dot(a, b) > 0;
}
private static Vector2 Perpendicular(Vector2 a, Vector2 b)
{
return TripleProduct(a, b, a);
}
}
}

View File

@ -1,67 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Float;
namespace MoonWorks.Collision.Float
{
/// <summary>
/// A Circle is a shape defined by a radius.
/// </summary>
public struct Circle : IShape2D, System.IEquatable<Circle>
{
public float Radius { get; }
public AABB2D AABB { get; }
public IEnumerable<IShape2D> Shapes
{
get
{
yield return this;
}
}
public Circle(float radius)
{
Radius = radius;
AABB = new AABB2D(-Radius, -Radius, Radius, Radius);
}
public Vector2 Support(Vector2 direction, Transform2D transform)
{
return Vector2.Transform(Vector2.Normalize(direction) * Radius, transform.TransformMatrix);
}
public AABB2D TransformedAABB(Transform2D transform2D)
{
return AABB2D.Transformed(AABB, transform2D);
}
public override bool Equals(object obj)
{
return obj is IShape2D other && Equals(other);
}
public bool Equals(IShape2D other)
{
return other is Circle circle && Equals(circle);
}
public bool Equals(Circle other)
{
return Radius == other.Radius;
}
public override int GetHashCode()
{
return System.HashCode.Combine(Radius);
}
public static bool operator ==(Circle a, Circle b)
{
return a.Equals(b);
}
public static bool operator !=(Circle a, Circle b)
{
return !(a == b);
}
}
}

View File

@ -1,83 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Float;
namespace MoonWorks.Collision.Float
{
/// <summary>
/// A line is a shape defined by exactly two points in space.
/// </summary>
public struct Line : IShape2D, System.IEquatable<Line>
{
public Vector2 Start { get; }
public Vector2 End { get; }
public AABB2D AABB { get; }
public IEnumerable<IShape2D> Shapes
{
get
{
yield return this;
}
}
public Line(Vector2 start, Vector2 end)
{
Start = start;
End = end;
AABB = new AABB2D(
System.Math.Min(Start.X, End.X),
System.Math.Min(Start.Y, End.Y),
System.Math.Max(Start.X, End.X),
System.Math.Max(Start.Y, End.Y)
);
}
public Vector2 Support(Vector2 direction, Transform2D transform)
{
var transformedStart = Vector2.Transform(Start, transform.TransformMatrix);
var transformedEnd = Vector2.Transform(End, transform.TransformMatrix);
return Vector2.Dot(transformedStart, direction) > Vector2.Dot(transformedEnd, direction) ?
transformedStart :
transformedEnd;
}
public AABB2D TransformedAABB(Transform2D transform)
{
return AABB2D.Transformed(AABB, transform);
}
public override bool Equals(object obj)
{
return obj is IShape2D other && Equals(other);
}
public bool Equals(IShape2D other)
{
return other is Line otherLine && Equals(otherLine);
}
public bool Equals(Line other)
{
return
(Start == other.Start && End == other.End) ||
(End == other.Start && Start == other.End);
}
public override int GetHashCode()
{
return System.HashCode.Combine(Start, End);
}
public static bool operator ==(Line a, Line b)
{
return a.Equals(b);
}
public static bool operator !=(Line a, Line b)
{
return !(a == b);
}
}
}

View File

@ -1,61 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Float;
namespace MoonWorks.Collision.Float
{
/// <summary>
/// A Point is "that which has no part".
/// All points by themselves are identical.
/// </summary>
public struct Point : IShape2D, System.IEquatable<Point>
{
public AABB2D AABB { get; }
public IEnumerable<IShape2D> Shapes
{
get
{
yield return this;
}
}
public AABB2D TransformedAABB(Transform2D transform)
{
return AABB2D.Transformed(AABB, transform);
}
public Vector2 Support(Vector2 direction, Transform2D transform)
{
return Vector2.Transform(Vector2.Zero, transform.TransformMatrix);
}
public override bool Equals(object obj)
{
return obj is IShape2D other && Equals(other);
}
public bool Equals(IShape2D other)
{
return other is Point otherPoint && Equals(otherPoint);
}
public bool Equals(Point other)
{
return true;
}
public override int GetHashCode()
{
return 0;
}
public static bool operator ==(Point a, Point b)
{
return true;
}
public static bool operator !=(Point a, Point b)
{
return false;
}
}
}

View File

@ -1,115 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Float;
namespace MoonWorks.Collision.Float
{
/// <summary>
/// A rectangle is a shape defined by a width and height. The origin is the center of the rectangle.
/// </summary>
public struct Rectangle : IShape2D, System.IEquatable<Rectangle>
{
public AABB2D AABB { get; }
public float Width { get; }
public float Height { get; }
public float Right { get; }
public float Left { get; }
public float Top { get; }
public float Bottom { get; }
public Vector2 TopLeft { get; }
public Vector2 BottomRight { get; }
public Vector2 Min { get; }
public Vector2 Max { get; }
public IEnumerable<IShape2D> Shapes
{
get
{
yield return this;
}
}
public Rectangle(float left, float top, float width, float height)
{
Width = width;
Height = height;
Left = left;
Right = left + width;
Top = top;
Bottom = top + height;
AABB = new AABB2D(left, top, Right, Bottom);
TopLeft = new Vector2(Left, Top);
BottomRight = new Vector2(Right, Bottom);
Min = AABB.Min;
Max = AABB.Max;
}
private Vector2 Support(Vector2 direction)
{
if (direction.X >= 0 && direction.Y >= 0)
{
return Max;
}
else if (direction.X >= 0 && direction.Y < 0)
{
return new Vector2(Max.X, Min.Y);
}
else if (direction.X < 0 && direction.Y >= 0)
{
return new Vector2(Min.X, Max.Y);
}
else if (direction.X < 0 && direction.Y < 0)
{
return new Vector2(Min.X, Min.Y);
}
else
{
throw new System.ArgumentException("Support vector direction cannot be zero.");
}
}
public Vector2 Support(Vector2 direction, Transform2D transform)
{
Matrix3x2 inverseTransform;
Matrix3x2.Invert(transform.TransformMatrix, out inverseTransform);
var inverseDirection = Vector2.TransformNormal(direction, inverseTransform);
return Vector2.Transform(Support(inverseDirection), transform.TransformMatrix);
}
public AABB2D TransformedAABB(Transform2D transform)
{
return AABB2D.Transformed(AABB, transform);
}
public override bool Equals(object obj)
{
return obj is IShape2D other && Equals(other);
}
public bool Equals(IShape2D other)
{
return (other is Rectangle rectangle && Equals(rectangle));
}
public bool Equals(Rectangle other)
{
return Min == other.Min && Max == other.Max;
}
public override int GetHashCode()
{
return System.HashCode.Combine(Min, Max);
}
public static bool operator ==(Rectangle a, Rectangle b)
{
return a.Equals(b);
}
public static bool operator !=(Rectangle a, Rectangle b)
{
return !(a == b);
}
}
}

View File

@ -1,136 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Float;
namespace MoonWorks.Collision.Float
{
/// <summary>
/// A simplex is a shape with up to n - 2 vertices in the nth dimension.
/// </summary>
public struct Simplex2D : System.IEquatable<Simplex2D>
{
private Vector2 a;
private Vector2? b;
private Vector2? c;
public Vector2 A => a;
public Vector2? B => b;
public Vector2? C => c;
public bool ZeroSimplex { get { return !b.HasValue && !c.HasValue; } }
public bool OneSimplex { get { return b.HasValue && !c.HasValue; } }
public bool TwoSimplex { get { return b.HasValue && c.HasValue; } }
public int Count => TwoSimplex ? 3 : (OneSimplex ? 2 : 1);
public Simplex2D(Vector2 a)
{
this.a = a;
b = null;
c = null;
}
public Simplex2D(Vector2 a, Vector2 b)
{
this.a = a;
this.b = b;
c = null;
}
public Simplex2D(Vector2 a, Vector2 b, Vector2 c)
{
this.a = a;
this.b = b;
this.c = c;
}
public Vector2 this[int index]
{
get
{
if (index == 0) { return a; }
if (index == 1) { return b.Value; }
if (index == 2) { return c.Value; }
throw new System.IndexOutOfRangeException();
}
}
public IEnumerable<Vector2> Vertices
{
get
{
yield return (Vector2) a;
if (b.HasValue) { yield return (Vector2) b; }
if (c.HasValue) { yield return (Vector2) c; }
}
}
public Vector2 Support(Vector2 direction, Transform2D transform)
{
var maxDotProduct = float.NegativeInfinity;
var maxVertex = a;
foreach (var vertex in Vertices)
{
var transformed = Vector2.Transform(vertex, transform.TransformMatrix);
var dot = Vector2.Dot(transformed, direction);
if (dot > maxDotProduct)
{
maxVertex = transformed;
maxDotProduct = dot;
}
}
return maxVertex;
}
public void Insert(Vector2 point, int index)
{
if (index == 0)
{
c = b;
b = a;
a = point;
}
else if (index == 1)
{
c = b;
b = point;
}
else
{
c = point;
}
}
public override bool Equals(object obj)
{
return obj is Simplex2D other && Equals(other);
}
public bool Equals(Simplex2D other)
{
if (Count != other.Count) { return false; }
return
(A == other.A && B == other.B && C == other.C) ||
(A == other.A && B == other.C && C == other.B) ||
(A == other.B && B == other.A && C == other.C) ||
(A == other.B && B == other.C && C == other.A) ||
(A == other.C && B == other.A && C == other.B) ||
(A == other.C && B == other.B && C == other.A);
}
public override int GetHashCode()
{
return System.HashCode.Combine(Vertices);
}
public static bool operator ==(Simplex2D a, Simplex2D b)
{
return a.Equals(b);
}
public static bool operator !=(Simplex2D a, Simplex2D b)
{
return !(a == b);
}
}
}

View File

@ -1,253 +0,0 @@
using System.Collections.Generic;
using MoonWorks.Math.Float;
namespace MoonWorks.Collision.Float
{
/// <summary>
/// Used to quickly check if two shapes are potentially overlapping.
/// </summary>
/// <typeparam name="T">The type that will be used to uniquely identify shape-transform pairs.</typeparam>
public class SpatialHash2D<T> where T : System.IEquatable<T>
{
private readonly int cellSize;
private readonly Dictionary<long, HashSet<T>> hashDictionary = new Dictionary<long, HashSet<T>>();
// FIXME: this ICollidable causes boxing which triggers garbage collection
private readonly Dictionary<T, (ICollidable, Transform2D, uint)> IDLookup = new Dictionary<T, (ICollidable, Transform2D, uint)>();
public int MinX { get; private set; } = 0;
public int MaxX { get; private set; } = 0;
public int MinY { get; private set; } = 0;
public int MaxY { get; private set; } = 0;
private Queue<HashSet<T>> hashSetPool = new Queue<HashSet<T>>();
public SpatialHash2D(int cellSize)
{
this.cellSize = cellSize;
}
private (int, int) Hash(Vector2 position)
{
return ((int) System.Math.Floor(position.X / cellSize), (int) System.Math.Floor(position.Y / cellSize));
}
/// <summary>
/// Inserts an element into the SpatialHash.
/// </summary>
/// <param name="id">A unique ID for the shape-transform pair.</param>
/// <param name="shape"></param>
/// <param name="transform2D"></param>
/// <param name="collisionGroups">A bitmask value specifying the groups this object belongs to.</param>
public void Insert(T id, ICollidable shape, Transform2D transform2D, uint collisionGroups = uint.MaxValue)
{
var box = shape.TransformedAABB(transform2D);
var minHash = Hash(box.Min);
var maxHash = Hash(box.Max);
foreach (var key in Keys(minHash.Item1, minHash.Item2, maxHash.Item1, maxHash.Item2))
{
if (!hashDictionary.ContainsKey(key))
{
hashDictionary.Add(key, new HashSet<T>());
}
hashDictionary[key].Add(id);
IDLookup[id] = (shape, transform2D, collisionGroups);
}
MinX = System.Math.Min(MinX, minHash.Item1);
MinY = System.Math.Min(MinY, minHash.Item2);
MaxX = System.Math.Max(MaxX, maxHash.Item1);
MaxY = System.Math.Max(MaxY, maxHash.Item2);
}
/// <summary>
/// Retrieves all the potential collisions of a shape-transform pair. Excludes any shape-transforms with the given ID.
/// </summary>
public IEnumerable<(T, ICollidable, Transform2D, uint)> Retrieve(T id, ICollidable shape, Transform2D transform2D, uint collisionMask = uint.MaxValue)
{
var returned = AcquireHashSet();
var box = shape.TransformedAABB(transform2D);
var (minX, minY) = Hash(box.Min);
var (maxX, maxY) = Hash(box.Max);
if (minX < MinX) { minX = MinX; }
if (maxX > MaxX) { maxX = MaxX; }
if (minY < MinY) { minY = MinY; }
if (maxY > MaxY) { maxY = MaxY; }
foreach (var key in Keys(minX, minY, maxX, maxY))
{
if (hashDictionary.ContainsKey(key))
{
foreach (var t in hashDictionary[key])
{
if (!returned.Contains(t))
{
var (otherShape, otherTransform, collisionGroups) = IDLookup[t];
if (!id.Equals(t) && ((collisionGroups & collisionMask) > 0) && AABB2D.TestOverlap(box, otherShape.TransformedAABB(otherTransform)))
{
returned.Add(t);
yield return (t, otherShape, otherTransform, collisionGroups);
}
}
}
}
}
FreeHashSet(returned);
}
/// <summary>
/// Retrieves all the potential collisions of a shape-transform pair.
/// </summary>
public IEnumerable<(T, ICollidable, Transform2D, uint)> Retrieve(ICollidable shape, Transform2D transform2D, uint collisionMask = uint.MaxValue)
{
var returned = AcquireHashSet();
var box = shape.TransformedAABB(transform2D);
var (minX, minY) = Hash(box.Min);
var (maxX, maxY) = Hash(box.Max);
if (minX < MinX) { minX = MinX; }
if (maxX > MaxX) { maxX = MaxX; }
if (minY < MinY) { minY = MinY; }
if (maxY > MaxY) { maxY = MaxY; }
foreach (var key in Keys(minX, minY, maxX, maxY))
{
if (hashDictionary.ContainsKey(key))
{
foreach (var t in hashDictionary[key])
{
if (!returned.Contains(t))
{
var (otherShape, otherTransform, collisionGroups) = IDLookup[t];
if (((collisionGroups & collisionMask) > 0) && AABB2D.TestOverlap(box, otherShape.TransformedAABB(otherTransform)))
{
returned.Add(t);
yield return (t, otherShape, otherTransform, collisionGroups);
}
}
}
}
}
FreeHashSet(returned);
}
/// <summary>
/// Retrieves objects based on a pre-transformed AABB.
/// </summary>
/// <param name="aabb">A transformed AABB.</param>
/// <returns></returns>
public IEnumerable<(T, ICollidable, Transform2D, uint)> Retrieve(AABB2D aabb, uint collisionMask = uint.MaxValue)
{
var returned = AcquireHashSet();
var (minX, minY) = Hash(aabb.Min);
var (maxX, maxY) = Hash(aabb.Max);
if (minX < MinX) { minX = MinX; }
if (maxX > MaxX) { maxX = MaxX; }
if (minY < MinY) { minY = MinY; }
if (maxY > MaxY) { maxY = MaxY; }
foreach (var key in Keys(minX, minY, maxX, maxY))
{
if (hashDictionary.ContainsKey(key))
{
foreach (var t in hashDictionary[key])
{
if (!returned.Contains(t))
{
var (otherShape, otherTransform, collisionGroups) = IDLookup[t];
if (((collisionGroups & collisionMask) > 0) && AABB2D.TestOverlap(aabb, otherShape.TransformedAABB(otherTransform)))
{
yield return (t, otherShape, otherTransform, collisionGroups);
}
}
}
}
}
FreeHashSet(returned);
}
public void Update(T id, ICollidable shape, Transform2D transform2D, uint collisionGroups = uint.MaxValue)
{
Remove(id);
Insert(id, shape, transform2D, collisionGroups);
}
/// <summary>
/// Removes a specific ID from the SpatialHash.
/// </summary>
public void Remove(T id)
{
var (shape, transform, collisionGroups) = IDLookup[id];
var box = shape.TransformedAABB(transform);
var minHash = Hash(box.Min);
var maxHash = Hash(box.Max);
foreach (var key in Keys(minHash.Item1, minHash.Item2, maxHash.Item1, maxHash.Item2))
{
if (hashDictionary.ContainsKey(key))
{
hashDictionary[key].Remove(id);
}
}
IDLookup.Remove(id);
}
/// <summary>
/// Removes everything that has been inserted into the SpatialHash.
/// </summary>
public void Clear()
{
foreach (var hash in hashDictionary.Values)
{
hash.Clear();
}
IDLookup.Clear();
}
private static long MakeLong(int left, int right)
{
return ((long) left << 32) | ((uint) right);
}
private IEnumerable<long> Keys(int minX, int minY, int maxX, int maxY)
{
for (var i = minX; i <= maxX; i++)
{
for (var j = minY; j <= maxY; j++)
{
yield return MakeLong(i, j);
}
}
}
private HashSet<T> AcquireHashSet()
{
if (hashSetPool.Count == 0)
{
hashSetPool.Enqueue(new HashSet<T>());
}
var hashSet = hashSetPool.Dequeue();
hashSet.Clear();
return hashSet;
}
private void FreeHashSet(HashSet<T> hashSet)
{
hashSetPool.Enqueue(hashSet);
}
}
}

View File

@ -1,9 +1,13 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using MoonWorks.Graphics;
using MoonWorks.Graphics.PackedVector;
namespace MoonWorks
{
/// <summary>
/// Conversion utilities for interop.
/// </summary>
public static class Conversions
{
private readonly static Dictionary<VertexElementFormat, uint> Sizes = new Dictionary<VertexElementFormat, uint>

View File

@ -1,12 +0,0 @@
using System;
namespace MoonWorks
{
public class AudioLoadException : Exception
{
public AudioLoadException(string message) : base(message)
{
}
}
}

View File

@ -2,13 +2,27 @@ namespace MoonWorks
{
public enum FrameLimiterMode
{
/// <summary>
/// The game will render at the maximum possible framerate that the computing resources allow. <br/>
/// Note that this may lead to overheating, resource starvation, etc.
/// </summary>
Uncapped,
/// <summary>
/// The game will render no more than the specified frames per second.
/// </summary>
Capped
}
/// <summary>
/// The Game's frame limiter setting. Specifies uncapped framerate or a maximum rendering frames per second value. <br/>
/// Note that this is separate from the Game's Update timestep and can be a different value.
/// </summary>
public struct FrameLimiterSettings
{
public FrameLimiterMode Mode;
/// <summary>
/// If Mode is set to Capped, this is the maximum frames per second that will be rendered.
/// </summary>
public int Cap;
public FrameLimiterSettings(

View File

@ -1,5 +1,4 @@
using System.Collections.Generic;
using SDL2;
using SDL2;
using MoonWorks.Audio;
using MoonWorks.Graphics;
using MoonWorks.Input;
@ -9,6 +8,12 @@ using System.Diagnostics;
namespace MoonWorks
{
/// <summary>
/// This class is your entry point into controlling your game. <br/>
/// It manages the main game loop and subsystems. <br/>
/// You should inherit this class and implement Update and Draw methods. <br/>
/// Then instantiate your Game subclass from your Program.Main method and call the Run method.
/// </summary>
public abstract class Game
{
public TimeSpan MAX_DELTA_TIME = TimeSpan.FromMilliseconds(100);
@ -33,8 +38,18 @@ namespace MoonWorks
public AudioDevice AudioDevice { get; }
public Inputs Inputs { get; }
/// <summary>
/// This Window is automatically created when your Game is instantiated.
/// </summary>
public Window MainWindow { get; }
/// <summary>
/// Instantiates your Game.
/// </summary>
/// <param name="windowCreateInfo">The parameters that will be used to create the MainWindow.</param>
/// <param name="frameLimiterSettings">The frame limiter settings.</param>
/// <param name="targetTimestep">How often Game.Update will run in terms of ticks per second.</param>
/// <param name="debugMode">If true, enables extra debug checks. Should be turned off for release builds.</param>
public Game(
WindowCreateInfo windowCreateInfo,
FrameLimiterSettings frameLimiterSettings,
@ -42,6 +57,7 @@ namespace MoonWorks
bool debugMode = false
)
{
Logger.LogInfo("Initializing frame limiter...");
Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / targetTimestep);
gameTimer = Stopwatch.StartNew();
@ -52,21 +68,25 @@ namespace MoonWorks
previousSleepTimes[i] = TimeSpan.FromMilliseconds(1);
}
Logger.LogInfo("Initializing SDL...");
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_TIMER | SDL.SDL_INIT_GAMECONTROLLER) < 0)
{
System.Console.WriteLine("Failed to initialize SDL!");
Logger.LogError("Failed to initialize SDL!");
return;
}
Logger.Initialize();
Logger.LogInfo("Initializing input...");
Inputs = new Inputs();
Logger.LogInfo("Initializing graphics device...");
GraphicsDevice = new GraphicsDevice(
Backend.Vulkan,
debugMode
);
Logger.LogInfo("Initializing main window...");
MainWindow = new Window(windowCreateInfo, GraphicsDevice.WindowFlags | SDL.SDL_WindowFlags.SDL_WINDOW_HIDDEN);
if (!GraphicsDevice.ClaimWindow(MainWindow, windowCreateInfo.PresentMode))
@ -74,9 +94,13 @@ namespace MoonWorks
throw new System.SystemException("Could not claim window!");
}
Logger.LogInfo("Initializing audio thread...");
AudioDevice = new AudioDevice();
}
/// <summary>
/// Initiates the main game loop. Call this once from your Program.Main method.
/// </summary>
public void Run()
{
MainWindow.Show();
@ -86,15 +110,29 @@ namespace MoonWorks
Tick();
}
Logger.LogInfo("Starting shutdown sequence...");
Logger.LogInfo("Cleaning up game...");
Destroy();
AudioDevice.Dispose();
Logger.LogInfo("Unclaiming window...");
GraphicsDevice.UnclaimWindow(MainWindow);
Logger.LogInfo("Disposing window...");
MainWindow.Dispose();
Logger.LogInfo("Disposing graphics device...");
GraphicsDevice.Dispose();
Logger.LogInfo("Closing audio thread...");
AudioDevice.Dispose();
SDL.SDL_Quit();
}
/// <summary>
/// Updates the frame limiter settings.
/// </summary>
public void SetFrameLimiter(FrameLimiterSettings settings)
{
FramerateCapped = settings.Mode == FrameLimiterMode.Capped;
@ -109,18 +147,42 @@ namespace MoonWorks
}
}
/// <summary>
/// Starts the game shutdown process.
/// </summary>
public void Quit()
{
quit = true;
}
/// <summary>
/// Will execute at the specified targetTimestep you provided when instantiating your Game class.
/// </summary>
/// <param name="delta"></param>
protected abstract void Update(TimeSpan delta);
/// <summary>
/// If the frame limiter mode is Capped, this will run at most Cap times per second. <br />
/// Otherwise it will run as many times as possible.
/// </summary>
/// <param name="alpha">A value from 0-1 describing how "in-between" update ticks it is called. Useful for interpolation.</param>
protected abstract void Draw(double alpha);
/// <summary>
/// You can optionally override this to perform cleanup tasks before the game quits.
/// </summary>
protected virtual void Destroy() {}
// Called when a file is dropped on the game window.
/// <summary>
/// Called when a file is dropped on the game window.
/// </summary>
protected virtual void DropFile(string filePath) {}
/* Required to distinguish between multiple files dropped at once
* vs multiple files dropped one at a time.
*
* Called once for every multi-file drop.
*/
/// <summary>
/// Required to distinguish between multiple files dropped at once
/// vs multiple files dropped one at a time.
/// Called once for every multi-file drop.
/// </summary>
protected virtual void DropBegin() {}
protected virtual void DropComplete() {}
@ -168,9 +230,8 @@ namespace MoonWorks
while (accumulatedUpdateTime >= Timestep)
{
Inputs.Update();
AudioDevice.Update();
Update(Timestep);
AudioDevice.WakeThread();
accumulatedUpdateTime -= Timestep;
}
@ -282,17 +343,27 @@ namespace MoonWorks
var index = evt.cdevice.which;
if (SDL.SDL_IsGameController(index) == SDL.SDL_bool.SDL_TRUE)
{
System.Console.WriteLine($"New controller detected!");
Logger.LogInfo("New controller detected!");
Inputs.AddGamepad(index);
}
}
private void HandleControllerRemoved(SDL.SDL_Event evt)
{
System.Console.WriteLine($"Controller removal detected!");
Logger.LogInfo("Controller removal detected!");
Inputs.RemoveGamepad(evt.cdevice.which);
}
public static void ShowRuntimeError(string title, string message)
{
SDL.SDL_ShowSimpleMessageBox(
SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
title ?? "",
message ?? "",
IntPtr.Zero
);
}
private TimeSpan AdvanceElapsedTime()
{
long currentTicks = gameTimer.Elapsed.Ticks;

View File

@ -1,5 +1,8 @@
namespace MoonWorks.Graphics
{
/// <summary>
/// A buffer-offset pair to be used when binding vertex buffers.
/// </summary>
public struct BufferBinding
{
public Buffer Buffer;

View File

@ -1,7 +1,8 @@
using System;
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics
{
/// <summary>
/// A texture-sampler pair to be used when binding samplers.
/// </summary>
public struct TextureSamplerBinding
{
public Texture Texture;

View File

@ -20,6 +20,7 @@ using System.Diagnostics;
using System.Text;
using MoonWorks.Math;
using MoonWorks.Math.Float;
using MoonWorks.Graphics.PackedVector;
#endregion
namespace MoonWorks.Graphics
@ -1758,6 +1759,67 @@ namespace MoonWorks.Graphics
);
}
// 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 = MathHelper.Clamp(threeSliceSelector, 0f, 1f); // 1 if NOT the first slice (true for slices 1 and 2)
float IsNotSecondSlice = MathHelper.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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Concurrent;
namespace MoonWorks.Graphics
{
internal class CommandBufferPool
{
private GraphicsDevice GraphicsDevice;
private ConcurrentQueue<CommandBuffer> CommandBuffers = new ConcurrentQueue<CommandBuffer>();
public CommandBufferPool(GraphicsDevice graphicsDevice)
{
GraphicsDevice = graphicsDevice;
}
public CommandBuffer Obtain()
{
if (CommandBuffers.TryDequeue(out var commandBuffer))
{
return commandBuffer;
}
else
{
return new CommandBuffer(GraphicsDevice);
}
}
public void Return(CommandBuffer commandBuffer)
{
commandBuffer.Handle = IntPtr.Zero;
CommandBuffers.Enqueue(commandBuffer);
}
}
}

32
src/Graphics/FencePool.cs Normal file
View File

@ -0,0 +1,32 @@
using System.Collections.Concurrent;
namespace MoonWorks.Graphics
{
internal class FencePool
{
private GraphicsDevice GraphicsDevice;
private ConcurrentQueue<Fence> Fences = new ConcurrentQueue<Fence>();
public FencePool(GraphicsDevice graphicsDevice)
{
GraphicsDevice = graphicsDevice;
}
public Fence Obtain()
{
if (Fences.TryDequeue(out var fence))
{
return fence;
}
else
{
return new Fence(GraphicsDevice);
}
}
public void Return(Fence fence)
{
Fences.Enqueue(fence);
}
}
}

View File

@ -1,44 +1,121 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using WellspringCS;
namespace MoonWorks.Graphics.Font
{
public class Font : IDisposable
public unsafe class Font : GraphicsResource
{
public IntPtr Handle { get; }
public Texture Texture { get; }
public float PixelsPerEm { get; }
public float DistanceRange { get; }
private bool IsDisposed;
internal IntPtr Handle { get; }
public unsafe Font(string path)
private byte* StringBytes;
private int StringBytesLength;
/// <summary>
/// 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.
/// </summary>
/// <returns></returns>
public unsafe static Font Load(
GraphicsDevice graphicsDevice,
CommandBuffer commandBuffer,
string fontPath
) {
var fontFileStream = new FileStream(fontPath, FileMode.Open, FileAccess.Read);
var fontFileByteBuffer = NativeMemory.Alloc((nuint) fontFileStream.Length);
var fontFileByteSpan = new Span<byte>(fontFileByteBuffer, (int) fontFileStream.Length);
fontFileStream.ReadExactly(fontFileByteSpan);
fontFileStream.Close();
var atlasFileStream = new FileStream(Path.ChangeExtension(fontPath, ".json"), FileMode.Open, FileAccess.Read);
var atlasFileByteBuffer = NativeMemory.Alloc((nuint) atlasFileStream.Length);
var atlasFileByteSpan = new Span<byte>(atlasFileByteBuffer, (int) atlasFileStream.Length);
atlasFileStream.ReadExactly(atlasFileByteSpan);
atlasFileStream.Close();
var handle = Wellspring.Wellspring_CreateFont(
(IntPtr) fontFileByteBuffer,
(uint) fontFileByteSpan.Length,
(IntPtr) atlasFileByteBuffer,
(uint) atlasFileByteSpan.Length,
out float pixelsPerEm,
out float distanceRange
);
var texture = Texture.FromImageFile(graphicsDevice, commandBuffer, Path.ChangeExtension(fontPath, ".png"));
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)
{
var bytes = File.ReadAllBytes(path);
fixed (byte* pByte = &bytes[0])
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
) {
var byteCount = System.Text.Encoding.UTF8.GetByteCount(text);
if (StringBytesLength < byteCount)
{
Handle = Wellspring.Wellspring_CreateFont((IntPtr) pByte, (uint) bytes.Length);
StringBytes = (byte*) NativeMemory.Realloc(StringBytes, (nuint) byteCount);
}
fixed (char* chars = text)
{
System.Text.Encoding.UTF8.GetBytes(chars, text.Length, StringBytes, byteCount);
var result = Wellspring.Wellspring_TextBounds(
Handle,
pixelSize,
(Wellspring.HorizontalAlignment) horizontalAlignment,
(Wellspring.VerticalAlignment) verticalAlignment,
(IntPtr) StringBytes,
(uint) byteCount,
out rectangle
);
if (result == 0)
{
Logger.LogWarn("Could not decode string: " + text);
return false;
}
}
protected virtual void Dispose(bool disposing)
return true;
}
protected override void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
Texture.Dispose();
}
Wellspring.Wellspring_DestroyFont(Handle);
IsDisposed = true;
}
}
~Font()
{
// 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);
base.Dispose(disposing);
}
}
}

View File

@ -1,109 +0,0 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using WellspringCS;
namespace MoonWorks.Graphics.Font
{
public class Packer : IDisposable
{
public IntPtr Handle { get; }
public Texture Texture { get; }
public Font Font { get; }
private byte[] StringBytes;
private bool IsDisposed;
public unsafe Packer(GraphicsDevice graphicsDevice, Font font, float fontSize, uint textureWidth, uint textureHeight, uint padding = 1)
{
Font = font;
Handle = Wellspring.Wellspring_CreatePacker(Font.Handle, fontSize, textureWidth, textureHeight, 0, padding);
Texture = Texture.CreateTexture2D(graphicsDevice, textureWidth, textureHeight, TextureFormat.R8, TextureUsageFlags.Sampler);
StringBytes = new byte[128];
}
public unsafe bool PackFontRanges(params FontRange[] fontRanges)
{
fixed (FontRange *pFontRanges = &fontRanges[0])
{
var nativeSize = fontRanges.Length * Marshal.SizeOf<Wellspring.FontRange>();
void* fontRangeMemory = NativeMemory.Alloc((nuint) fontRanges.Length, (nuint) Marshal.SizeOf<Wellspring.FontRange>());
System.Buffer.MemoryCopy(pFontRanges, fontRangeMemory, nativeSize, nativeSize);
var result = Wellspring.Wellspring_PackFontRanges(Handle, (IntPtr) fontRangeMemory, (uint) fontRanges.Length);
NativeMemory.Free(fontRangeMemory);
return result > 0;
}
}
public unsafe void SetTextureData(CommandBuffer commandBuffer)
{
var pixelDataPointer = Wellspring.Wellspring_GetPixelDataPointer(Handle);
commandBuffer.SetTextureData(Texture, pixelDataPointer, Texture.Width * Texture.Height);
}
public unsafe void TextBounds(
string text,
float x,
float y,
HorizontalAlignment horizontalAlignment,
VerticalAlignment verticalAlignment,
out Wellspring.Rectangle rectangle
) {
var byteCount = System.Text.Encoding.UTF8.GetByteCount(text);
if (StringBytes.Length < byteCount)
{
System.Array.Resize(ref StringBytes, byteCount);
}
fixed (char* chars = text)
fixed (byte* bytes = StringBytes)
{
System.Text.Encoding.UTF8.GetBytes(chars, text.Length, bytes, byteCount);
Wellspring.Wellspring_TextBounds(
Handle,
x,
y,
(Wellspring.HorizontalAlignment) horizontalAlignment,
(Wellspring.VerticalAlignment) verticalAlignment,
(IntPtr) bytes,
(uint) byteCount,
out rectangle
);
}
}
protected virtual void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
Texture.Dispose();
}
Wellspring.Wellspring_DestroyPacker(Handle);
IsDisposed = true;
}
}
~Packer()
{
// 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);
}
}
}

View File

@ -4,19 +4,17 @@ using MoonWorks.Math.Float;
namespace MoonWorks.Graphics.Font
{
[StructLayout(LayoutKind.Sequential)]
public struct FontRange
{
public uint FirstCodepoint;
public uint NumChars;
public byte OversampleH;
public byte OversampleV;
}
[StructLayout(LayoutKind.Sequential)]
public struct Vertex
public struct Vertex : IVertexType
{
public Vector3 Position;
public Vector2 TexCoord;
public Color Color;
public static VertexElementFormat[] Formats { get; } = new VertexElementFormat[]
{
VertexElementFormat.Vector3,
VertexElementFormat.Vector2,
VertexElementFormat.Color
};
}
}

View File

@ -1,75 +1,87 @@
using System;
using System.Runtime.InteropServices;
using WellspringCS;
namespace MoonWorks.Graphics.Font
{
public class TextBatch
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;
private GraphicsDevice GraphicsDevice { get; }
public IntPtr Handle { get; }
public Buffer VertexBuffer { get; protected set; } = null;
public Buffer IndexBuffer { get; protected set; } = null;
public Texture Texture { get; protected set; }
public uint PrimitiveCount { get; protected set; }
private byte[] StringBytes;
public Font CurrentFont { get; private set; }
public TextBatch(GraphicsDevice graphicsDevice)
private byte* StringBytes;
private int StringBytesLength;
public TextBatch(GraphicsDevice device) : base(device)
{
GraphicsDevice = graphicsDevice;
GraphicsDevice = device;
Handle = Wellspring.Wellspring_CreateTextBatch();
StringBytes = new byte[128];
StringBytesLength = 128;
StringBytes = (byte*) NativeMemory.Alloc((nuint) StringBytesLength);
VertexBuffer = Buffer.Create<Vertex>(GraphicsDevice, BufferUsageFlags.Vertex, INITIAL_VERTEX_COUNT);
IndexBuffer = Buffer.Create<uint>(GraphicsDevice, BufferUsageFlags.Index, INITIAL_INDEX_COUNT);
}
public void Start(Packer packer)
// Call this to initialize or reset the batch.
public void Start(Font font)
{
Wellspring.Wellspring_StartTextBatch(Handle, packer.Handle);
Texture = packer.Texture;
Wellspring.Wellspring_StartTextBatch(Handle, font.Handle);
CurrentFont = font;
PrimitiveCount = 0;
}
public unsafe void Draw(
// Add text with size and color to the batch
public unsafe bool Add(
string text,
float x,
float y,
float depth,
int pixelSize,
Color color,
HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment verticalAlignment = VerticalAlignment.Baseline
) {
var byteCount = System.Text.Encoding.UTF8.GetByteCount(text);
if (StringBytes.Length < byteCount)
if (StringBytesLength < byteCount)
{
System.Array.Resize(ref StringBytes, byteCount);
StringBytes = (byte*) NativeMemory.Realloc(StringBytes, (nuint) byteCount);
}
fixed (char* chars = text)
fixed (byte* bytes = StringBytes)
{
System.Text.Encoding.UTF8.GetBytes(chars, text.Length, bytes, byteCount);
System.Text.Encoding.UTF8.GetBytes(chars, text.Length, StringBytes, byteCount);
var result = Wellspring.Wellspring_Draw(
var result = Wellspring.Wellspring_AddToTextBatch(
Handle,
x,
y,
depth,
pixelSize,
new Wellspring.Color { R = color.R, G = color.G, B = color.B, A = color.A },
(Wellspring.HorizontalAlignment) horizontalAlignment,
(Wellspring.VerticalAlignment) verticalAlignment,
(IntPtr) bytes,
(IntPtr) StringBytes,
(uint) byteCount
);
if (result == 0)
{
throw new System.ArgumentException("Could not decode string!");
}
Logger.LogWarn("Could not decode string: " + text);
return false;
}
}
// Call this after you have made all the Draw calls you want.
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(
@ -81,24 +93,16 @@ namespace MoonWorks.Graphics.Font
out uint indexDataLengthInBytes
);
if (VertexBuffer == null)
{
VertexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes);
}
else if (VertexBuffer.Size < vertexDataLengthInBytes)
if (VertexBuffer.Size < vertexDataLengthInBytes)
{
VertexBuffer.Dispose();
VertexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes);
}
if (IndexBuffer == null)
{
IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Index, indexDataLengthInBytes);
}
else if (IndexBuffer.Size < indexDataLengthInBytes)
if (IndexBuffer.Size < indexDataLengthInBytes)
{
IndexBuffer.Dispose();
IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Index, indexDataLengthInBytes);
IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Index, vertexDataLengthInBytes);
}
if (vertexDataLengthInBytes > 0 && indexDataLengthInBytes > 0)
@ -107,7 +111,41 @@ namespace MoonWorks.Graphics.Font
commandBuffer.SetBufferData(IndexBuffer, indexDataPointer, 0, indexDataLengthInBytes);
}
PrimitiveCount = vertexCount / 2; // FIXME: is this jank?
PrimitiveCount = vertexCount / 2;
}
// Call this AFTER binding your text pipeline!
public void Render(CommandBuffer commandBuffer, Math.Float.Matrix4x4 transformMatrix)
{
commandBuffer.BindFragmentSamplers(new TextureSamplerBinding(
CurrentFont.Texture,
GraphicsDevice.LinearSampler
));
commandBuffer.BindVertexBuffers(VertexBuffer);
commandBuffer.BindIndexBuffer(IndexBuffer, IndexElementSize.ThirtyTwo);
commandBuffer.DrawIndexedPrimitives(
0,
0,
PrimitiveCount,
commandBuffer.PushVertexShaderUniforms(transformMatrix),
commandBuffer.PushFragmentShaderUniforms(CurrentFont.DistanceRange)
);
}
protected override void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
VertexBuffer.Dispose();
IndexBuffer.Dispose();
}
NativeMemory.Free(StringBytes);
Wellspring.Wellspring_DestroyTextBatch(Handle);
}
base.Dispose(disposing);
}
}
}

View File

@ -1,10 +1,16 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using MoonWorks.Video;
using RefreshCS;
using WellspringCS;
namespace MoonWorks.Graphics
{
/// <summary>
/// GraphicsDevice manages all graphics-related concerns.
/// </summary>
public class GraphicsDevice : IDisposable
{
public IntPtr Handle { get; }
@ -16,11 +22,22 @@ namespace MoonWorks.Graphics
// Built-in video pipeline
internal GraphicsPipeline VideoPipeline { get; }
// Built-in text shader info
public GraphicsShaderInfo TextVertexShaderInfo { get; }
public GraphicsShaderInfo TextFragmentShaderInfo { get; }
public VertexInputState TextVertexInputState { get; }
// Built-in samplers
public Sampler PointSampler { get; }
public Sampler LinearSampler { get; }
public bool IsDisposed { get; private set; }
private readonly List<WeakReference<GraphicsResource>> resources = new List<WeakReference<GraphicsResource>>();
private readonly HashSet<GCHandle> resources = new HashSet<GCHandle>();
private FencePool FencePool;
private CommandBufferPool CommandBufferPool;
public GraphicsDevice(
internal GraphicsDevice(
Backend preferredBackend,
bool debugMode
) {
@ -35,14 +52,56 @@ namespace MoonWorks.Graphics
Conversions.BoolToByte(debugMode)
);
// Check for optional video shaders
string basePath = SDL2.SDL.SDL_GetBasePath();
string videoVertPath = Path.Combine(basePath, "video_fullscreen.refresh");
string videoFragPath = Path.Combine(basePath, "video_yuv2rgba.refresh");
// TODO: check for CreateDevice fail
// Check for replacement stock shaders
string basePath = System.AppContext.BaseDirectory;
string videoVertPath = Path.Combine(basePath, "video_fullscreen.vert.refresh");
string videoFragPath = Path.Combine(basePath, "video_yuv2rgba.frag.refresh");
string textVertPath = Path.Combine(basePath, "text_transform.vert.refresh");
string textFragPath = Path.Combine(basePath, "text_msdf.frag.refresh");
ShaderModule videoVertShader;
ShaderModule videoFragShader;
ShaderModule textVertShader;
ShaderModule textFragShader;
if (File.Exists(videoVertPath) && File.Exists(videoFragPath))
{
ShaderModule videoVertShader = new ShaderModule(this, videoVertPath);
ShaderModule videoFragShader = new ShaderModule(this, videoFragPath);
videoVertShader = new ShaderModule(this, videoVertPath);
videoFragShader = new ShaderModule(this, videoFragPath);
}
else
{
// use defaults
var assembly = typeof(GraphicsDevice).Assembly;
using var vertStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.VideoFullscreen.vert.refresh");
using var fragStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.VideoYUV2RGBA.frag.refresh");
videoVertShader = new ShaderModule(this, vertStream);
videoFragShader = new ShaderModule(this, fragStream);
}
if (File.Exists(textVertPath) && File.Exists(textFragPath))
{
textVertShader = new ShaderModule(this, textVertPath);
textFragShader = new ShaderModule(this, textFragPath);
}
else
{
// use defaults
var assembly = typeof(GraphicsDevice).Assembly;
using var vertStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.TextTransform.vert.refresh");
using var fragStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.TextMSDF.frag.refresh");
textVertShader = new ShaderModule(this, vertStream);
textFragShader = new ShaderModule(this, fragStream);
}
VideoPipeline = new GraphicsPipeline(
this,
@ -71,11 +130,31 @@ namespace MoonWorks.Graphics
MultisampleState = MultisampleState.None
}
);
}
TextVertexShaderInfo = GraphicsShaderInfo.Create<Math.Float.Matrix4x4>(textVertShader, "main", 0);
TextFragmentShaderInfo = GraphicsShaderInfo.Create<float>(textFragShader, "main", 1);
TextVertexInputState = VertexInputState.CreateSingleBinding<Font.Vertex>();
PointSampler = new Sampler(this, SamplerCreateInfo.PointClamp);
LinearSampler = new Sampler(this, SamplerCreateInfo.LinearClamp);
FencePool = new FencePool(this);
CommandBufferPool = new CommandBufferPool(this);
}
/// <summary>
/// Prepares a window so that frames can be presented to it.
/// </summary>
/// <param name="presentMode">The desired presentation mode for the window. Roughly equivalent to V-Sync.</param>
/// <returns>True if successfully claimed.</returns>
public bool ClaimWindow(Window window, PresentMode presentMode)
{
if (window.Claimed)
{
Logger.LogError("Window already claimed!");
return false;
}
var success = Conversions.ByteToBool(
Refresh.Refresh_ClaimWindow(
Handle,
@ -90,24 +169,46 @@ namespace MoonWorks.Graphics
window.SwapchainFormat = GetSwapchainFormat(window);
if (window.SwapchainTexture == null)
{
window.SwapchainTexture = new Texture(this, IntPtr.Zero, window.SwapchainFormat, 0, 0);
window.SwapchainTexture = new Texture(this, window.SwapchainFormat);
}
}
return success;
}
/// <summary>
/// Unclaims a window, making it unavailable for presenting and freeing associated resources.
/// </summary>
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;
}
}
/// <summary>
/// Changes the present mode of a claimed window. Does nothing if the window is not claimed.
/// </summary>
/// <param name="window"></param>
/// <param name="presentMode"></param>
public void SetPresentMode(Window window, PresentMode presentMode)
{
if (!window.Claimed)
{
Logger.LogError("Cannot set present mode on unclaimed window!");
return;
}
Refresh.Refresh_SetSwapchainPresentMode(
Handle,
window.Handle,
@ -115,105 +216,208 @@ namespace MoonWorks.Graphics
);
}
/// <summary>
/// Acquires a command buffer.
/// This is the start of your rendering process.
/// </summary>
/// <returns></returns>
public CommandBuffer AcquireCommandBuffer()
{
return new CommandBuffer(this, Refresh.Refresh_AcquireCommandBuffer(Handle));
var commandBuffer = CommandBufferPool.Obtain();
commandBuffer.SetHandle(Refresh.Refresh_AcquireCommandBuffer(Handle));
#if DEBUG
commandBuffer.ResetStateTracking();
#endif
return commandBuffer;
}
public unsafe void Submit(CommandBuffer commandBuffer)
/// <summary>
/// Submits a command buffer to the GPU for processing.
/// </summary>
public void Submit(CommandBuffer commandBuffer)
{
var commandBufferPtrs = stackalloc IntPtr[1];
commandBufferPtrs[0] = commandBuffer.Handle;
Refresh.Refresh_Submit(
Handle,
1,
(IntPtr) commandBufferPtrs
);
}
public unsafe void Submit(
CommandBuffer commandBufferOne,
CommandBuffer commandBufferTwo
) {
var commandBufferPtrs = stackalloc IntPtr[2];
commandBufferPtrs[0] = commandBufferOne.Handle;
commandBufferPtrs[1] = commandBufferTwo.Handle;
Refresh.Refresh_Submit(
Handle,
2,
(IntPtr) commandBufferPtrs
);
}
public unsafe void Submit(
CommandBuffer commandBufferOne,
CommandBuffer commandBufferTwo,
CommandBuffer commandBufferThree
) {
var commandBufferPtrs = stackalloc IntPtr[3];
commandBufferPtrs[0] = commandBufferOne.Handle;
commandBufferPtrs[1] = commandBufferTwo.Handle;
commandBufferPtrs[2] = commandBufferThree.Handle;
Refresh.Refresh_Submit(
Handle,
3,
(IntPtr) commandBufferPtrs
);
}
public unsafe void Submit(
CommandBuffer commandBufferOne,
CommandBuffer commandBufferTwo,
CommandBuffer commandBufferThree,
CommandBuffer commandBufferFour
) {
var commandBufferPtrs = stackalloc IntPtr[4];
commandBufferPtrs[0] = commandBufferOne.Handle;
commandBufferPtrs[1] = commandBufferTwo.Handle;
commandBufferPtrs[2] = commandBufferThree.Handle;
commandBufferPtrs[3] = commandBufferFour.Handle;
Refresh.Refresh_Submit(
Handle,
4,
(IntPtr) commandBufferPtrs
);
}
public unsafe void Submit(params CommandBuffer[] commandBuffers)
#if DEBUG
if (commandBuffer.Submitted)
{
var commandBufferPtrs = stackalloc IntPtr[commandBuffers.Length];
for (var i = 0; i < commandBuffers.Length; i += 1)
{
commandBufferPtrs[i] = commandBuffers[i].Handle;
throw new System.InvalidOperationException("Command buffer already submitted!");
}
#endif
Refresh.Refresh_Submit(
Handle,
(uint) commandBuffers.Length,
(IntPtr) commandBufferPtrs
commandBuffer.Handle
);
CommandBufferPool.Return(commandBuffer);
#if DEBUG
commandBuffer.Submitted = true;
#endif
}
/// <summary>
/// Submits a command buffer to the GPU for processing and acquires a fence associated with the submission.
/// </summary>
/// <returns></returns>
public Fence SubmitAndAcquireFence(CommandBuffer commandBuffer)
{
var fenceHandle = Refresh.Refresh_SubmitAndAcquireFence(
Handle,
commandBuffer.Handle
);
var fence = FencePool.Obtain();
fence.SetHandle(fenceHandle);
return fence;
}
/// <summary>
/// Wait for the graphics device to become idle.
/// </summary>
public void Wait()
{
Refresh.Refresh_Wait(Handle);
}
/// <summary>
/// Waits for the given fence to become signaled.
/// </summary>
public unsafe void WaitForFences(Fence fence)
{
var handlePtr = stackalloc nint[1];
handlePtr[0] = fence.Handle;
Refresh.Refresh_WaitForFences(
Handle,
1,
1,
(nint) handlePtr
);
}
/// <summary>
/// Wait for one or more fences to become signaled.
/// </summary>
/// <param name="waitAll">If true, will wait for all given fences to be signaled.</param>
public unsafe void WaitForFences(
Fence fenceOne,
Fence fenceTwo,
bool waitAll
) {
var handlePtr = stackalloc nint[2];
handlePtr[0] = fenceOne.Handle;
handlePtr[1] = fenceTwo.Handle;
Refresh.Refresh_WaitForFences(
Handle,
Conversions.BoolToByte(waitAll),
2,
(nint) handlePtr
);
}
/// <summary>
/// Wait for one or more fences to become signaled.
/// </summary>
/// <param name="waitAll">If true, will wait for all given fences to be signaled.</param>
public unsafe void WaitForFences(
Fence fenceOne,
Fence fenceTwo,
Fence fenceThree,
bool waitAll
) {
var handlePtr = stackalloc nint[3];
handlePtr[0] = fenceOne.Handle;
handlePtr[1] = fenceTwo.Handle;
handlePtr[2] = fenceThree.Handle;
Refresh.Refresh_WaitForFences(
Handle,
Conversions.BoolToByte(waitAll),
3,
(nint) handlePtr
);
}
/// <summary>
/// Wait for one or more fences to become signaled.
/// </summary>
/// <param name="waitAll">If true, will wait for all given fences to be signaled.</param>
public unsafe void WaitForFences(
Fence fenceOne,
Fence fenceTwo,
Fence fenceThree,
Fence fenceFour,
bool waitAll
) {
var handlePtr = stackalloc nint[4];
handlePtr[0] = fenceOne.Handle;
handlePtr[1] = fenceTwo.Handle;
handlePtr[2] = fenceThree.Handle;
handlePtr[3] = fenceFour.Handle;
Refresh.Refresh_WaitForFences(
Handle,
Conversions.BoolToByte(waitAll),
4,
(nint) handlePtr
);
}
/// <summary>
/// Wait for one or more fences to become signaled.
/// </summary>
/// <param name="waitAll">If true, will wait for all given fences to be signaled.</param>
public unsafe void WaitForFences(Fence[] fences, bool waitAll)
{
var handlePtr = stackalloc nint[fences.Length];
for (var i = 0; i < fences.Length; i += 1)
{
handlePtr[i] = fences[i].Handle;
}
Refresh.Refresh_WaitForFences(
Handle,
Conversions.BoolToByte(waitAll),
4,
(nint) handlePtr
);
}
/// <summary>
/// Returns true if the fence is signaled, indicating that the associated command buffer has finished processing.
/// </summary>
/// <exception cref="InvalidOperationException">Throws if the fence query indicates that the graphics device has been lost.</exception>
public bool QueryFence(Fence fence)
{
var result = Refresh.Refresh_QueryFence(Handle, fence.Handle);
if (result < 0)
{
throw new InvalidOperationException("The graphics device has been lost.");
}
return result != 0;
}
/// <summary>
/// Release reference to an acquired fence, enabling it to be reused.
/// </summary>
public void ReleaseFence(Fence fence)
{
Refresh.Refresh_ReleaseFence(Handle, fence.Handle);
fence.Handle = IntPtr.Zero;
FencePool.Return(fence);
}
private TextureFormat GetSwapchainFormat(Window window)
{
return (TextureFormat) Refresh.Refresh_GetSwapchainFormat(Handle, window.Handle);
}
internal void AddResourceReference(WeakReference<GraphicsResource> resourceReference)
internal void AddResourceReference(GCHandle resourceReference)
{
lock (resources)
{
@ -221,7 +425,7 @@ namespace MoonWorks.Graphics
}
}
internal void RemoveResourceReference(WeakReference<GraphicsResource> resourceReference)
internal void RemoveResourceReference(GCHandle resourceReference)
{
lock (resources)
{
@ -237,19 +441,28 @@ namespace MoonWorks.Graphics
{
lock (resources)
{
for (var i = resources.Count - 1; i >= 0; i--)
// Dispose video players first to avoid race condition on threaded decoding
foreach (var resource in resources)
{
var resource = resources[i];
if (resource.TryGetTarget(out var target))
if (resource.Target is VideoPlayer player)
{
target.Dispose();
player.Dispose();
}
}
// Dispose everything else
foreach (var resource in resources)
{
if (resource.Target is IDisposable disposable)
{
disposable.Dispose();
}
}
resources.Clear();
}
}
Refresh.Refresh_DestroyDevice(Handle);
}
IsDisposed = true;
}

View File

@ -1,37 +1,33 @@
using System;
using System.Runtime.InteropServices;
namespace MoonWorks.Graphics
{
// TODO: give this a Name property for debugging use
public abstract class GraphicsResource : IDisposable
{
public GraphicsDevice Device { get; }
public IntPtr Handle { get; internal set; }
private GCHandle SelfReference;
public bool IsDisposed { get; private set; }
protected abstract Action<IntPtr, IntPtr> QueueDestroyFunction { get; }
private WeakReference<GraphicsResource> selfReference;
public GraphicsResource(GraphicsDevice device, bool trackResource = true)
protected GraphicsResource(GraphicsDevice device)
{
Device = device;
if (trackResource)
{
selfReference = new WeakReference<GraphicsResource>(this);
Device.AddResourceReference(selfReference);
}
SelfReference = GCHandle.Alloc(this, GCHandleType.Weak);
Device.AddResourceReference(SelfReference);
}
protected virtual void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (selfReference != null)
if (disposing)
{
QueueDestroyFunction(Device.Handle, Handle);
Device.RemoveResourceReference(selfReference);
selfReference = null;
Device.RemoveResourceReference(SelfReference);
SelfReference.Free();
}
IsDisposed = true;
@ -40,8 +36,13 @@ namespace MoonWorks.Graphics
~GraphicsResource()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: false);
#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.
Logger.LogWarn($"A resource of type {GetType().Name} was not Disposed.");
#endif
Dispose(false);
}
public void Dispose()

View File

@ -1,7 +1,13 @@
namespace MoonWorks.Graphics
{
/// <summary>
/// Can be defined on your struct type to enable simplified vertex input state definition.
/// </summary>
public interface IVertexType
{
/// <summary>
/// An ordered list of the types in your vertex struct.
/// </summary>
static abstract VertexElementFormat[] Formats { get; }
}
}

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
/// <summary>
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
/// <summary>
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
/// <summary>
/// Packed vector type containing unsigned normalized values, ranging from 0 to 1, using

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
/// <summary>
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
/// <summary>
/// Packed vector type containing four 8-bit unsigned integer values, ranging from 0 to 255.

View File

@ -19,7 +19,7 @@ using System;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
public struct HalfSingle : IPackedVector<ushort>, IEquatable<HalfSingle>, IPackedVector
{

View File

@ -19,7 +19,7 @@ using System;
using System.Runtime.InteropServices;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
internal static class HalfTypeHelper
{

View File

@ -19,7 +19,7 @@ using System;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
public struct HalfVector2 : IPackedVector<uint>, IPackedVector, IEquatable<HalfVector2>
{

View File

@ -19,7 +19,7 @@ using System;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
/// <summary>
/// Packed vector type containing four 16-bit floating-point values.

View File

@ -16,7 +16,7 @@
using MoonWorks.Math.Float;
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
// http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.graphics.packedvector.ipackedvector.aspx
public interface IPackedVector

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
public struct NormalizedByte2 : IPackedVector<ushort>, IEquatable<NormalizedByte2>
{

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
public struct NormalizedByte4 : IPackedVector<uint>, IEquatable<NormalizedByte4>
{

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
public struct NormalizedShort2 : IPackedVector<uint>, IEquatable<NormalizedShort2>
{

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
public struct NormalizedShort4 : IPackedVector<ulong>, IEquatable<NormalizedShort4>
{

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
/// <summary>
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
/// <summary>
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
/// <summary>
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
public struct Short2 : IPackedVector<uint>, IEquatable<Short2>
{

View File

@ -20,7 +20,7 @@ using MoonWorks.Math;
using MoonWorks.Math.Float;
#endregion
namespace MoonWorks.Graphics
namespace MoonWorks.Graphics.PackedVector
{
/// <summary>
/// Packed vector type containing four 16-bit signed integer values.

View File

@ -2,11 +2,31 @@
namespace MoonWorks
{
/// <summary>
/// Presentation mode for a window.
/// </summary>
public enum PresentMode
{
/// <summary>
/// Does not wait for v-blank to update the window. Can cause visible tearing.
/// </summary>
Immediate,
/// <summary>
/// Waits for v-blank and uses a queue to hold present requests.
/// Allows for low latency while preventing tearing.
/// May not be supported on non-Vulkan non-Linux systems or older hardware.
/// </summary>
Mailbox,
/// <summary>
/// Waits for v-blank and adds present requests to a queue.
/// Will probably cause latency.
/// Required to be supported by all compliant hardware.
/// </summary>
FIFO,
/// <summary>
/// Usually waits for v-blank, but if v-blank has passed since last update will update immediately.
/// May cause visible tearing.
/// </summary>
FIFORelaxed
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
namespace MoonWorks.Graphics;
public abstract class RefreshResource : GraphicsResource
{
public IntPtr Handle { get => handle; internal set => handle = value; }
private IntPtr handle;
protected abstract Action<IntPtr, IntPtr> QueueDestroyFunction { get; }
protected RefreshResource(GraphicsDevice device) : base(device)
{
}
protected override void Dispose(bool disposing)
{
if (!IsDisposed)
{
// Atomically call destroy function in case this is called from the finalizer thread
var toDispose = Interlocked.Exchange(ref handle, IntPtr.Zero);
if (toDispose != IntPtr.Zero)
{
QueueDestroyFunction(Device.Handle, toDispose);
}
}
base.Dispose(disposing);
}
}

View File

@ -127,12 +127,12 @@ namespace MoonWorks.Graphics
public uint Stride;
public VertexInputRate InputRate;
public static VertexBinding Create<T>(uint binding = 0) where T : unmanaged
public static VertexBinding Create<T>(uint binding = 0, VertexInputRate inputRate = VertexInputRate.Vertex) where T : unmanaged
{
return new VertexBinding
{
Binding = binding,
InputRate = VertexInputRate.Vertex,
InputRate = inputRate,
Stride = (uint) Marshal.SizeOf<T>()
};
}
@ -181,7 +181,6 @@ namespace MoonWorks.Graphics
public uint Depth;
public uint Layer;
public uint Level;
public SampleCount SampleCount;
public Color ClearColor;
public LoadOp LoadOp;
public StoreOp StoreOp;
@ -189,15 +188,12 @@ namespace MoonWorks.Graphics
public ColorAttachmentInfo(
Texture texture,
Color clearColor,
SampleCount sampleCount = SampleCount.One,
StoreOp storeOp = StoreOp.Store
)
{
) {
Texture = texture;
Depth = 0;
Layer = 0;
Level = 0;
SampleCount = sampleCount;
ClearColor = clearColor;
LoadOp = LoadOp.Clear;
StoreOp = storeOp;
@ -206,15 +202,12 @@ namespace MoonWorks.Graphics
public ColorAttachmentInfo(
Texture texture,
LoadOp loadOp = LoadOp.DontCare,
SampleCount sampleCount = SampleCount.One,
StoreOp storeOp = StoreOp.Store
)
{
) {
Texture = texture;
Depth = 0;
Layer = 0;
Level = 0;
SampleCount = sampleCount;
ClearColor = Color.White;
LoadOp = loadOp;
StoreOp = storeOp;
@ -228,7 +221,6 @@ namespace MoonWorks.Graphics
depth = Depth,
layer = Layer,
level = Level,
sampleCount = (Refresh.SampleCount) SampleCount,
clearColor = new Refresh.Vec4
{
x = ClearColor.R / 255f,

View File

@ -7,7 +7,7 @@ namespace MoonWorks.Graphics
/// <summary>
/// Buffers are generic data containers that can be used by the GPU.
/// </summary>
public class Buffer : GraphicsResource
public class Buffer : RefreshResource
{
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyBuffer;
@ -58,17 +58,29 @@ namespace MoonWorks.Graphics
}
/// <summary>
/// Reads data out of a buffer and into an array.
/// This operation is only guaranteed to read up-to-date data if GraphicsDevice.Wait is called first.
/// Reads data out of a buffer and into a span.
/// This operation is only guaranteed to read up-to-date data if GraphicsDevice.Wait or GraphicsDevice.WaitForFences is called first.
/// </summary>
/// <param name="data">The array that data will be copied to.</param>
/// <param name="data">The span that data will be copied to.</param>
/// <param name="dataLengthInBytes">The length of the data to read.</param>
public unsafe void GetData<T>(
T[] data,
Span<T> data,
uint dataLengthInBytes
) where T : unmanaged
{
fixed (T* ptr = &data[0])
#if DEBUG
if (dataLengthInBytes > Size)
{
Logger.LogWarn("Requested too many bytes from buffer!");
}
if (dataLengthInBytes > data.Length * Marshal.SizeOf<T>())
{
Logger.LogWarn("Data length is larger than the provided Span!");
}
#endif
fixed (T* ptr = data)
{
Refresh.Refresh_GetBufferData(
Device.Handle,
@ -79,6 +91,48 @@ namespace MoonWorks.Graphics
}
}
/// <summary>
/// Reads data out of a buffer and into an array.
/// This operation is only guaranteed to read up-to-date data if GraphicsDevice.Wait or GraphicsDevice.WaitForFences is called first.
/// </summary>
/// <param name="data">The span that data will be copied to.</param>
/// <param name="dataLengthInBytes">The length of the data to read.</param>
public unsafe void GetData<T>(
T[] data,
uint dataLengthInBytes
) where T : unmanaged
{
GetData(new Span<T>(data), dataLengthInBytes);
}
/// <summary>
/// Reads data out of a buffer and into a span.
/// This operation is only guaranteed to read up-to-date data if GraphicsDevice.Wait or GraphicsDevice.WaitForFences is called first.
/// Fills the span with as much data from the buffer as it can.
/// </summary>
/// <param name="data">The span that data will be copied to.</param>
public unsafe void GetData<T>(
Span<T> data
) where T : unmanaged
{
var lengthInBytes = System.Math.Min(data.Length * Marshal.SizeOf<T>(), Size);
GetData(data, (uint) lengthInBytes);
}
/// <summary>
/// Reads data out of a buffer and into an array.
/// This operation is only guaranteed to read up-to-date data if GraphicsDevice.Wait or GraphicsDevice.WaitForFences is called first.
/// Fills the array with as much data from the buffer as it can.
/// </summary>
/// <param name="data">The span that data will be copied to.</param>
public unsafe void GetData<T>(
T[] data
) where T : unmanaged
{
var lengthInBytes = System.Math.Min(data.Length * Marshal.SizeOf<T>(), Size);
GetData(new Span<T>(data), (uint) lengthInBytes);
}
public static implicit operator BufferBinding(Buffer b)
{
return new BufferBinding(b, 0);

View File

@ -1,10 +1,12 @@
using RefreshCS;
using System;
using System.Runtime.InteropServices;
namespace MoonWorks.Graphics
{
public class ComputePipeline : GraphicsResource
/// <summary>
/// Compute pipelines perform arbitrary parallel processing on input data.
/// </summary>
public class ComputePipeline : RefreshResource
{
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyComputePipeline;

View File

@ -0,0 +1,26 @@
using System;
using RefreshCS;
namespace MoonWorks.Graphics
{
/// <summary>
/// Fences allow you to track the status of a submitted command buffer. <br/>
/// You should only acquire a Fence if you will need to track the command buffer. <br/>
/// You should make sure to call GraphicsDevice.ReleaseFence when done with a Fence to avoid memory growth. <br/>
/// The Fence object itself is basically just a wrapper for the Refresh_Fence. <br/>
/// The internal handle is replaced so that we can pool Fence objects to manage garbage.
/// </summary>
public class Fence : RefreshResource
{
protected override Action<nint, nint> QueueDestroyFunction => Refresh.Refresh_ReleaseFence;
internal Fence(GraphicsDevice device) : base(device)
{
}
internal void SetHandle(nint handle)
{
Handle = handle;
}
}
}

View File

@ -5,10 +5,10 @@ using RefreshCS;
namespace MoonWorks.Graphics
{
/// <summary>
/// Graphics pipelines encapsulate all of the render state in a single object.
/// Graphics pipelines encapsulate all of the render state in a single object. <br/>
/// These pipelines are bound before draw calls are issued.
/// </summary>
public class GraphicsPipeline : GraphicsResource
public class GraphicsPipeline : RefreshResource
{
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyGraphicsPipeline;

View File

@ -6,7 +6,7 @@ namespace MoonWorks.Graphics
/// <summary>
/// A sampler specifies how a texture will be sampled in a shader.
/// </summary>
public class Sampler : GraphicsResource
public class Sampler : RefreshResource
{
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroySampler;

View File

@ -1,42 +1,42 @@
using RefreshCS;
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace MoonWorks.Graphics
{
/// <summary>
/// Shader modules expect input in Refresh bytecode format.
/// </summary>
public class ShaderModule : GraphicsResource
public class ShaderModule : RefreshResource
{
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyShaderModule;
public unsafe ShaderModule(GraphicsDevice device, string filePath) : base(device)
{
using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
Handle = CreateFromStream(device, stream);
}
}
public unsafe ShaderModule(GraphicsDevice device, Stream stream) : base(device)
{
Handle = CreateFromStream(device, stream);
}
private unsafe static IntPtr CreateFromStream(GraphicsDevice device, Stream stream)
private static unsafe IntPtr CreateFromStream(GraphicsDevice device, Stream stream)
{
var bytecode = new byte[stream.Length];
stream.Read(bytecode, 0, (int) stream.Length);
var bytecodeBuffer = NativeMemory.Alloc((nuint) stream.Length);
var bytecodeSpan = new Span<byte>(bytecodeBuffer, (int) stream.Length);
stream.ReadExactly(bytecodeSpan);
fixed (byte* ptr = bytecode)
{
Refresh.ShaderModuleCreateInfo shaderModuleCreateInfo;
shaderModuleCreateInfo.codeSize = (UIntPtr) bytecode.Length;
shaderModuleCreateInfo.byteCode = (IntPtr) ptr;
shaderModuleCreateInfo.codeSize = (nuint) stream.Length;
shaderModuleCreateInfo.byteCode = (nint) bytecodeBuffer;
return Refresh.Refresh_CreateShaderModule(device.Handle, shaderModuleCreateInfo);
}
var shaderModule = Refresh.Refresh_CreateShaderModule(device.Handle, shaderModuleCreateInfo);
NativeMemory.Free(bytecodeBuffer);
return shaderModule;
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using RefreshCS;
namespace MoonWorks.Graphics
@ -7,7 +8,7 @@ namespace MoonWorks.Graphics
/// <summary>
/// A container for pixel data.
/// </summary>
public class Texture : GraphicsResource
public class Texture : RefreshResource
{
public uint Width { get; internal set; }
public uint Height { get; internal set; }
@ -15,67 +16,132 @@ namespace MoonWorks.Graphics
public TextureFormat Format { get; internal set; }
public bool IsCube { get; }
public uint LevelCount { get; }
public SampleCount SampleCount { get; }
public TextureUsageFlags UsageFlags { get; }
public uint Size { get; }
// FIXME: this allocates a delegate instance
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyTexture;
/// <summary>
/// Loads a PNG from a file path.
/// NOTE: You can queue as many of these as you want on to a command buffer but it MUST be submitted!
/// Creates a 2D Texture using PNG or QOI data from raw byte data.
/// </summary>
/// <param name="device"></param>
/// <param name="commandBuffer"></param>
/// <param name="filePath"></param>
/// <returns></returns>
public static Texture LoadPNG(GraphicsDevice device, CommandBuffer commandBuffer, string filePath)
public static unsafe Texture FromImageBytes(
GraphicsDevice device,
CommandBuffer commandBuffer,
Span<byte> data
) {
Texture texture;
fixed (byte *dataPtr = data)
{
var pixels = Refresh.Refresh_Image_Load(
filePath,
out var width,
out var height,
out var channels
);
var pixels = Refresh.Refresh_Image_Load((nint) dataPtr, data.Length, out var width, out var height, out var len);
var byteCount = (uint) (width * height * channels);
TextureCreateInfo textureCreateInfo;
TextureCreateInfo textureCreateInfo = new TextureCreateInfo();
textureCreateInfo.Width = (uint) width;
textureCreateInfo.Height = (uint) height;
textureCreateInfo.Depth = 1;
textureCreateInfo.Format = TextureFormat.R8G8B8A8;
textureCreateInfo.IsCube = false;
textureCreateInfo.LevelCount = 1;
textureCreateInfo.SampleCount = SampleCount.One;
textureCreateInfo.UsageFlags = TextureUsageFlags.Sampler;
var texture = new Texture(device, textureCreateInfo);
commandBuffer.SetTextureData(texture, pixels, byteCount);
texture = new Texture(device, textureCreateInfo);
commandBuffer.SetTextureData(texture, pixels, (uint) len);
Refresh.Refresh_Image_Free(pixels);
}
return texture;
}
/// <summary>
/// Saves RGBA or BGRA pixel data to a file in PNG format.
/// Creates a 2D Texture using PNG or QOI data from a stream.
/// </summary>
public unsafe static void SavePNG(string path, int width, int height, TextureFormat format, byte[] pixels)
{
if (format != TextureFormat.R8G8B8A8 && format != TextureFormat.B8G8R8A8)
{
throw new ArgumentException("Texture format must be RGBA8 or BGRA8!", "format");
public static unsafe Texture FromImageStream(
GraphicsDevice device,
CommandBuffer commandBuffer,
Stream stream
) {
var length = stream.Length;
var buffer = NativeMemory.Alloc((nuint) length);
var span = new Span<byte>(buffer, (int) length);
stream.ReadExactly(span);
var texture = FromImageBytes(device, commandBuffer, span);
NativeMemory.Free((void*) buffer);
return texture;
}
fixed (byte* ptr = &pixels[0])
/// <summary>
/// Creates a 2D Texture using PNG or QOI data from a file.
/// </summary>
public static Texture FromImageFile(
GraphicsDevice device,
CommandBuffer commandBuffer,
string path
) {
var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
return FromImageStream(device, commandBuffer, fileStream);
}
public static unsafe void SetDataFromImageBytes(
CommandBuffer commandBuffer,
TextureSlice textureSlice,
Span<byte> data
) {
fixed (byte* ptr = data)
{
Refresh.Refresh_Image_SavePNG(path, width, height, Conversions.BoolToByte(format == TextureFormat.B8G8R8A8), (IntPtr) ptr);
var pixels = Refresh.Refresh_Image_Load(
(nint) ptr,
(int) data.Length,
out var w,
out var h,
out var len
);
commandBuffer.SetTextureData(textureSlice, pixels, (uint) len);
Refresh.Refresh_Image_Free(pixels);
}
}
public static Texture LoadDDS(GraphicsDevice graphicsDevice, CommandBuffer commandBuffer, System.IO.Stream stream)
{
using (var reader = new BinaryReader(stream))
/// <summary>
/// Sets data for a texture slice using PNG or QOI data from a stream.
/// </summary>
public static unsafe void SetDataFromImageStream(
CommandBuffer commandBuffer,
TextureSlice textureSlice,
Stream stream
) {
var length = stream.Length;
var buffer = NativeMemory.Alloc((nuint) length);
var span = new Span<byte>(buffer, (int) length);
stream.ReadExactly(span);
SetDataFromImageBytes(commandBuffer, textureSlice, span);
NativeMemory.Free((void*) buffer);
}
/// <summary>
/// Sets data for a texture slice using PNG or QOI data from a file.
/// </summary>
public static void SetDataFromImageFile(
CommandBuffer commandBuffer,
TextureSlice textureSlice,
string path
) {
var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
SetDataFromImageStream(commandBuffer, textureSlice, fileStream);
}
public unsafe static Texture LoadDDS(GraphicsDevice graphicsDevice, CommandBuffer commandBuffer, System.IO.Stream stream)
{
using var reader = new BinaryReader(stream);
Texture texture;
int faces;
ParseDDS(reader, out var format, out var width, out var height, out var levels, out var isCube);
@ -98,22 +164,20 @@ namespace MoonWorks.Graphics
var levelWidth = width >> j;
var levelHeight = height >> j;
var pixels = reader.ReadBytes(
Texture.CalculateDDSLevelSize(
levelWidth,
levelHeight,
format
)
);
var levelSize = CalculateDDSLevelSize(levelWidth, levelHeight, format);
var byteBuffer = NativeMemory.Alloc((nuint) levelSize);
var byteSpan = new Span<byte>(byteBuffer, levelSize);
stream.ReadExactly(byteSpan);
var textureSlice = new TextureSlice(texture, new Rect(0, 0, levelWidth, levelHeight), 0, (uint) i, (uint) j);
commandBuffer.SetTextureData(textureSlice, pixels);
commandBuffer.SetTextureData(textureSlice, (nint) byteBuffer, (uint) levelSize);
NativeMemory.Free(byteBuffer);
}
}
return texture;
}
}
/// <summary>
/// Creates a 2D texture.
@ -130,9 +194,9 @@ namespace MoonWorks.Graphics
uint height,
TextureFormat format,
TextureUsageFlags usageFlags,
uint levelCount = 1
)
{
uint levelCount = 1,
SampleCount sampleCount = SampleCount.One
) {
var textureCreateInfo = new TextureCreateInfo
{
Width = width,
@ -140,6 +204,7 @@ namespace MoonWorks.Graphics
Depth = 1,
IsCube = false,
LevelCount = levelCount,
SampleCount = sampleCount,
Format = format,
UsageFlags = usageFlags
};
@ -165,8 +230,7 @@ namespace MoonWorks.Graphics
TextureFormat format,
TextureUsageFlags usageFlags,
uint levelCount = 1
)
{
) {
var textureCreateInfo = new TextureCreateInfo
{
Width = width,
@ -195,8 +259,7 @@ namespace MoonWorks.Graphics
TextureFormat format,
TextureUsageFlags usageFlags,
uint levelCount = 1
)
{
) {
var textureCreateInfo = new TextureCreateInfo
{
Width = size,
@ -232,7 +295,9 @@ namespace MoonWorks.Graphics
Depth = textureCreateInfo.Depth;
IsCube = textureCreateInfo.IsCube;
LevelCount = textureCreateInfo.LevelCount;
SampleCount = textureCreateInfo.SampleCount;
UsageFlags = textureCreateInfo.UsageFlags;
Size = Width * Height * BytesPerPixel(Format) / BlockSizeSquared(Format);
}
public static implicit operator TextureSlice(Texture t) => new TextureSlice(t);
@ -241,21 +306,20 @@ namespace MoonWorks.Graphics
// Should not be tracked, because swapchain textures are managed by Vulkan.
internal Texture(
GraphicsDevice device,
IntPtr handle,
TextureFormat format,
uint width,
uint height
) : base(device, false)
TextureFormat format
) : base(device)
{
Handle = handle;
Handle = IntPtr.Zero;
Format = format;
Width = width;
Height = height;
Width = 0;
Height = 0;
Depth = 1;
IsCube = false;
LevelCount = 1;
SampleCount = SampleCount.One;
UsageFlags = TextureUsageFlags.ColorTarget;
Size = Width * Height * BytesPerPixel(Format) / BlockSizeSquared(Format);
}
// DDS loading extension, based on MojoDDS
@ -525,5 +589,155 @@ namespace MoonWorks.Graphics
);
}
}
/// <summary>
/// Asynchronously saves RGBA or BGRA pixel data to a file in PNG format. <br/>
/// Warning: this is expensive and will block to wait for data download from GPU! <br/>
/// You can avoid blocking by calling this method from a thread.
/// </summary>
public unsafe void SavePNG(string path)
{
#if DEBUG
if (Format != TextureFormat.R8G8B8A8 && Format != TextureFormat.B8G8R8A8)
{
throw new ArgumentException("Texture format must be RGBA or BGRA!", "format");
}
#endif
var buffer = new Buffer(Device, 0, Width * Height * 4); // this creates garbage... oh well
// immediately request the data copy
var commandBuffer = Device.AcquireCommandBuffer();
commandBuffer.CopyTextureToBuffer(this, buffer);
var fence = Device.SubmitAndAcquireFence(commandBuffer);
var byteCount = buffer.Size;
var pixelsPtr = NativeMemory.Alloc((nuint) byteCount);
var pixelsSpan = new Span<byte>(pixelsPtr, (int) byteCount);
Device.WaitForFences(fence); // make sure the data transfer is done...
Device.ReleaseFence(fence); // and then release the fence
buffer.GetData(pixelsSpan);
if (Format == TextureFormat.B8G8R8A8)
{
var rgbaPtr = NativeMemory.Alloc((nuint) byteCount);
var rgbaSpan = new Span<byte>(rgbaPtr, (int) byteCount);
for (var i = 0; i < byteCount; i += 4)
{
rgbaSpan[i] = pixelsSpan[i + 2];
rgbaSpan[i + 1] = pixelsSpan[i + 1];
rgbaSpan[i + 2] = pixelsSpan[i];
rgbaSpan[i + 3] = pixelsSpan[i + 3];
}
Refresh.Refresh_Image_SavePNG(path, (nint) rgbaPtr, (int) Width, (int) Height);
NativeMemory.Free((void*) rgbaPtr);
}
else
{
fixed (byte* ptr = pixelsSpan)
{
Refresh.Refresh_Image_SavePNG(path, (nint) ptr, (int) Width, (int) Height);
}
}
NativeMemory.Free(pixelsPtr);
}
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:
return 2;
case TextureFormat.D16S8:
return 3;
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.D32:
return 4;
case TextureFormat.D32S8:
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:
Logger.LogError("Texture format not recognized!");
return 0;
}
}
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:
case TextureFormat.D32:
case TextureFormat.D16S8:
case TextureFormat.D32S8:
return 1;
default:
Logger.LogError("Texture format not recognized!");
return 0;
}
}
}
}

View File

@ -2,6 +2,9 @@
namespace MoonWorks.Graphics
{
/// <summary>
/// Defines how color blending will be performed in a GraphicsPipeline.
/// </summary>
public struct ColorAttachmentBlendState
{
/// <summary>

View File

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace MoonWorks.Graphics
{
/// <summary>
/// Information that the pipeline needs about a shader.
/// Information that the compute pipeline needs about a compute shader.
/// </summary>
public struct ComputeShaderInfo
{

View File

@ -1,5 +1,8 @@
namespace MoonWorks.Graphics
{
/// <summary>
/// All of the information that is used to create a GraphicsPipeline.
/// </summary>
public struct GraphicsPipelineCreateInfo
{
public DepthStencilState DepthStencilState;

View File

@ -3,7 +3,7 @@
namespace MoonWorks.Graphics
{
/// <summary>
/// Information that the pipeline needs about a shader.
/// Information that the pipeline needs about a graphics shader.
/// </summary>
public struct GraphicsShaderInfo
{

View File

@ -2,21 +2,60 @@
namespace MoonWorks.Graphics
{
/// <summary>
/// All of the information that is used to create a sampler.
/// </summary>
public struct SamplerCreateInfo
{
/// <summary>
/// Minification filter mode. Used when the image is downscaled.
/// </summary>
public Filter MinFilter;
/// <summary>
/// Magnification filter mode. Used when the image is upscaled.
/// </summary>
public Filter MagFilter;
/// <summary>
/// Filter mode applied to mipmap lookups.
/// </summary>
public SamplerMipmapMode MipmapMode;
/// <summary>
/// Horizontal addressing mode.
/// </summary>
public SamplerAddressMode AddressModeU;
/// <summary>
/// Vertical addressing mode.
/// </summary>
public SamplerAddressMode AddressModeV;
/// <summary>
/// Depth addressing mode.
/// </summary>
public SamplerAddressMode AddressModeW;
/// <summary>
/// Bias value added to mipmap level of detail calculation.
/// </summary>
public float MipLodBias;
/// <summary>
/// Enables anisotropic filtering.
/// </summary>
public bool AnisotropyEnable;
/// <summary>
/// Maximum anisotropy value.
/// </summary>
public float MaxAnisotropy;
public bool CompareEnable;
public CompareOp CompareOp;
/// <summary>
/// Clamps the LOD value to a specified minimum.
/// </summary>
public float MinLod;
/// <summary>
/// Clamps the LOD value to a specified maximum.
/// </summary>
public float MaxLod;
/// <summary>
/// If an address mode is set to ClampToBorder, will replace color with this color when samples are outside the [0, 1) range.
/// </summary>
public BorderColor BorderColor;
public static readonly SamplerCreateInfo AnisotropicClamp = new SamplerCreateInfo

View File

@ -2,6 +2,9 @@
namespace MoonWorks.Graphics
{
/// <summary>
/// All of the information that is used to create a texture.
/// </summary>
public struct TextureCreateInfo
{
public uint Width;
@ -9,6 +12,7 @@ namespace MoonWorks.Graphics
public uint Depth;
public bool IsCube;
public uint LevelCount;
public SampleCount SampleCount;
public TextureFormat Format;
public TextureUsageFlags UsageFlags;
@ -21,6 +25,7 @@ namespace MoonWorks.Graphics
depth = Depth,
isCube = Conversions.BoolToByte(IsCube),
levelCount = LevelCount,
sampleCount = (Refresh.SampleCount) SampleCount,
format = (Refresh.TextureFormat) Format,
usageFlags = (Refresh.TextureUsageFlags) UsageFlags
};

View File

@ -1,7 +1,7 @@
namespace MoonWorks.Graphics
{
/// <summary>
/// Specifies how to interpet vertex data in a buffer to be passed to the vertex shader.
/// Specifies how the vertex shader will interpet vertex data in a buffer.
/// </summary>
public struct VertexInputState
{

View File

@ -0,0 +1,34 @@
#version 450
layout(set = 1, binding = 0) uniform sampler2D msdf;
layout(location = 0) in vec2 inTexCoord;
layout(location = 1) in vec4 inColor;
layout(location = 0) out vec4 outColor;
layout(binding = 0, set = 3) 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);
}

View File

@ -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(binding = 0, set = 2) uniform UBO
{
mat4 ViewProjection;
} ubo;
void main()
{
gl_Position = ubo.ViewProjection * vec4(inPos, 1.0);
outTexCoord = inTexCoord;
outColor = inColor;
}

View File

@ -14,6 +14,8 @@ namespace MoonWorks.Graphics
public uint Layer { get; }
public uint Level { get; }
public uint Size => (uint) (Rectangle.W * Rectangle.H * Texture.BytesPerPixel(Texture.Format) / Texture.BlockSizeSquared(Texture.Format));
public TextureSlice(Texture texture)
{
Texture = texture;

View File

@ -1,6 +1,8 @@
namespace MoonWorks.Graphics
{
// This is a convenience structure for pairing a vertex binding with its associated attributes.
/// <summary>
/// A convenience structure for pairing a vertex binding with its associated attributes.
/// </summary>
public struct VertexBindingAndAttributes
{
public VertexBinding VertexBinding { get; }
@ -12,9 +14,9 @@ namespace MoonWorks.Graphics
VertexAttributes = attributes;
}
public static VertexBindingAndAttributes Create<T>(uint bindingIndex) where T : unmanaged, IVertexType
public static VertexBindingAndAttributes Create<T>(uint bindingIndex, uint locationOffset = 0, VertexInputRate inputRate = VertexInputRate.Vertex) where T : unmanaged, IVertexType
{
VertexBinding binding = VertexBinding.Create<T>(bindingIndex);
VertexBinding binding = VertexBinding.Create<T>(bindingIndex, inputRate);
VertexAttribute[] attributes = new VertexAttribute[T.Formats.Length];
uint offset = 0;
@ -25,7 +27,7 @@ namespace MoonWorks.Graphics
attributes[i] = new VertexAttribute
{
Binding = bindingIndex,
Location = i,
Location = locationOffset + i,
Format = format,
Offset = offset
};

View File

@ -3,6 +3,9 @@ using SDL2;
namespace MoonWorks.Input
{
/// <summary>
/// Represents a specific joystick direction on a gamepad.
/// </summary>
public class Axis
{
public Gamepad Parent { get; }

View File

@ -1,5 +1,8 @@
namespace MoonWorks.Input
{
/// <summary>
/// Can be used to access a gamepad axis virtual button without a direct reference to the button object.
/// </summary>
public enum AxisButtonCode
{
LeftX_Left,

View File

@ -1,6 +1,9 @@
namespace MoonWorks.Input
{
// Enum values are equivalent to SDL GameControllerAxis
/// <summary>
/// Can be used to access a gamepad axis without a direct reference to the axis object.
/// Enum values are equivalent to SDL_GameControllerAxis.
/// </summary>
public enum AxisCode
{
LeftX,

View File

@ -1,6 +1,9 @@
namespace MoonWorks.Input
{
// Enum values are equivalent to the SDL GameControllerButton value.
/// <summary>
/// Can be used to access a gamepad button without a direct reference to the button object.
/// Enum values are equivalent to the SDL GameControllerButton value.
/// </summary>
public enum GamepadButtonCode
{
A,

View File

@ -1,14 +1,40 @@
namespace MoonWorks.Input
{
/// <summary>
/// Container for the current state of a binary input.
/// </summary>
public struct ButtonState
{
public ButtonStatus ButtonStatus { get; }
/// <summary>
/// True if the button was pressed this frame.
/// </summary>
public bool IsPressed => ButtonStatus == ButtonStatus.Pressed;
/// <summary>
/// True if the button was pressed this frame and the previous frame.
/// </summary>
public bool IsHeld => ButtonStatus == ButtonStatus.Held;
/// <summary>
/// True if the button was either pressed or continued to be held this frame.
/// </summary>
public bool IsDown => ButtonStatus == ButtonStatus.Pressed || ButtonStatus == ButtonStatus.Held;
/// <summary>
/// True if the button was let go this frame.
/// </summary>
public bool IsReleased => ButtonStatus == ButtonStatus.Released;
/// <summary>
/// True if the button was not pressed this frame or the previous frame.
/// </summary>
public bool IsIdle => ButtonStatus == ButtonStatus.Idle;
/// <summary>
/// True if the button was either idle or released this frame.
/// </summary>
public bool IsUp => ButtonStatus == ButtonStatus.Idle || ButtonStatus == ButtonStatus.Released;
public ButtonState(ButtonStatus buttonStatus)
@ -43,7 +69,7 @@
}
/// <summary>
/// Combines two button states. Useful for alt controls or input buffering.
/// Combines two button states. Useful for alt control sets or input buffering.
/// </summary>
public static ButtonState operator |(ButtonState a, ButtonState b)
{

View File

@ -1,5 +1,8 @@
namespace MoonWorks.Input
{
/// <summary>
/// Represents the current status of a binary input.
/// </summary>
public enum ButtonStatus
{
/// <summary>

View File

@ -1,10 +0,0 @@
namespace MoonWorks.Input
{
public enum DeviceKind
{
None,
Keyboard,
Mouse,
Gamepad,
}
}

View File

@ -5,6 +5,12 @@ using SDL2;
namespace MoonWorks.Input
{
/// <summary>
/// A Gamepad input abstraction that represents input coming from a console controller or other such devices.
/// The button names map to a standard Xbox 360 controller.
/// For different controllers the relative position of the face buttons will determine the button mapping.
/// For example on a DualShock controller the Cross button will map to the A button.
/// </summary>
public class Gamepad
{
internal IntPtr Handle;
@ -51,7 +57,14 @@ namespace MoonWorks.Input
public bool IsDummy => Handle == IntPtr.Zero;
/// <summary>
/// True if any input on the gamepad is active. Useful for input remapping.
/// </summary>
public bool AnyPressed { get; private set; }
/// <summary>
/// Contains a reference to an arbitrary VirtualButton that was pressed on the gamepad this frame. Useful for input remapping.
/// </summary>
public VirtualButton AnyPressedButton { get; private set; }
private Dictionary<SDL.SDL_GameControllerButton, GamepadButton> EnumToButton;
@ -100,13 +113,13 @@ namespace MoonWorks.Input
LeftXLeft = new AxisButton(LeftX, false);
LeftXRight = new AxisButton(LeftX, true);
LeftYUp = new AxisButton(LeftY, false);
LeftYDown = new AxisButton(LeftY, true);
LeftYUp = new AxisButton(LeftY, true);
LeftYDown = new AxisButton(LeftY, false);
RightXLeft = new AxisButton(RightX, false);
RightXRight = new AxisButton(RightX, true);
RightYUp = new AxisButton(RightY, false);
RightYDown = new AxisButton(RightY, true);
RightYUp = new AxisButton(RightY, true);
RightYDown = new AxisButton(RightY, false);
TriggerLeft = new Trigger(this, TriggerCode.Left, SDL.SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERLEFT);
TriggerRight = new Trigger(this, TriggerCode.Right, SDL.SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERRIGHT);
@ -195,6 +208,20 @@ namespace MoonWorks.Input
};
}
internal void Register(IntPtr handle)
{
Handle = handle;
IntPtr joystickHandle = SDL.SDL_GameControllerGetJoystick(Handle);
JoystickInstanceID = SDL.SDL_JoystickInstanceID(joystickHandle);
}
internal void Unregister()
{
Handle = IntPtr.Zero;
JoystickInstanceID = -1;
}
internal void Update()
{
AnyPressed = false;
@ -253,16 +280,25 @@ namespace MoonWorks.Input
) == 0;
}
/// <summary>
/// Obtains a gamepad button object given a button code.
/// </summary>
public GamepadButton Button(GamepadButtonCode buttonCode)
{
return EnumToButton[(SDL.SDL_GameControllerButton) buttonCode];
}
/// <summary>
/// Obtains an axis button object given a button code.
/// </summary>
public AxisButton Button(AxisButtonCode axisButtonCode)
{
return AxisButtonCodeToAxisButton[axisButtonCode];
}
/// <summary>
/// Obtains a trigger button object given a button code.
/// </summary>
public TriggerButton Button(TriggerCode triggerCode)
{
return TriggerCodeToTriggerButton[triggerCode];

View File

@ -3,31 +3,65 @@ using System;
namespace MoonWorks.Input
{
/// <summary>
/// The main container class for all input tracking.
/// Your Game class will automatically have a reference to this class.
/// </summary>
public class Inputs
{
public const int MAX_GAMEPADS = 4;
/// <summary>
/// The reference to the Keyboard input abstraction.
/// </summary>
public Keyboard Keyboard { get; }
/// <summary>
/// The reference to the Mouse input abstraction.
/// </summary>
public Mouse Mouse { get; }
Gamepad[] gamepads;
Gamepad[] Gamepads;
public static event Action<char> TextInput;
/// <summary>
/// True if any input on any input device is active. Useful for input remapping.
/// </summary>
public bool AnyPressed { get; private set; }
/// <summary>
/// Contains a reference to an arbitrary VirtualButton that was pressed this frame. Useful for input remapping.
/// </summary>
public VirtualButton AnyPressedButton { get; private set; }
public delegate void OnGamepadConnectedFunc(int slot);
/// <summary>
/// Called when a gamepad has been connected.
/// </summary>
/// <param name="slot">The slot where the connection occurred.</param>
public OnGamepadConnectedFunc OnGamepadConnected = delegate { };
public delegate void OnGamepadDisconnectedFunc(int slot);
/// <summary>
/// Called when a gamepad has been disconnected.
/// </summary>
/// <param name="slot">The slot where the disconnection occurred.</param>
public OnGamepadDisconnectedFunc OnGamepadDisconnected = delegate { };
internal Inputs()
{
Keyboard = new Keyboard();
Mouse = new Mouse();
gamepads = new Gamepad[MAX_GAMEPADS];
Gamepads = new Gamepad[MAX_GAMEPADS];
// initialize dummy controllers
for (var slot = 0; slot < MAX_GAMEPADS; slot += 1)
{
gamepads[slot] = new Gamepad(IntPtr.Zero, slot);
Gamepads[slot] = new Gamepad(IntPtr.Zero, slot);
}
}
@ -53,7 +87,7 @@ namespace MoonWorks.Input
AnyPressedButton = Mouse.AnyPressedButton;
}
foreach (var gamepad in gamepads)
foreach (var gamepad in Gamepads)
{
gamepad.Update();
@ -65,6 +99,11 @@ namespace MoonWorks.Input
}
}
/// <summary>
/// Returns true if a gamepad is currently connected in the given slot.
/// </summary>
/// <param name="slot">Range: 0-3</param>
/// <returns></returns>
public bool GamepadExists(int slot)
{
if (slot < 0 || slot >= MAX_GAMEPADS)
@ -72,13 +111,19 @@ namespace MoonWorks.Input
return false;
}
return !gamepads[slot].IsDummy;
return !Gamepads[slot].IsDummy;
}
// From 0-4
/// <summary>
/// Gets a gamepad associated with the given slot.
/// The first n slots are guaranteed to occupied with gamepads if they are connected.
/// If a gamepad does not exist for the given slot, a dummy object with all inputs in default state will be returned.
/// You can check if a gamepad is connected in a slot with the GamepadExists function.
/// </summary>
/// <param name="slot">Range: 0-3</param>
public Gamepad GetGamepad(int slot)
{
return gamepads[slot];
return Gamepads[slot];
}
internal void AddGamepad(int index)
@ -87,24 +132,40 @@ namespace MoonWorks.Input
{
if (!GamepadExists(slot))
{
gamepads[slot].Handle = SDL.SDL_GameControllerOpen(index);
System.Console.WriteLine($"Gamepad added to slot {slot}!");
var openResult = SDL.SDL_GameControllerOpen(index);
if (openResult == 0)
{
Logger.LogError("Error opening gamepad!");
Logger.LogError(SDL.SDL_GetError());
}
else
{
Gamepads[slot].Register(openResult);
Logger.LogInfo($"Gamepad added to slot {slot}!");
if (OnGamepadConnected != null)
{
OnGamepadConnected(slot);
}
}
return;
}
}
System.Console.WriteLine("Too many gamepads already!");
Logger.LogInfo("Too many gamepads already!");
}
internal void RemoveGamepad(int joystickInstanceID)
{
for (int slot = 0; slot < MAX_GAMEPADS; slot += 1)
{
if (joystickInstanceID == gamepads[slot].JoystickInstanceID)
if (joystickInstanceID == Gamepads[slot].JoystickInstanceID)
{
SDL.SDL_GameControllerClose(gamepads[slot].Handle);
gamepads[slot].Handle = IntPtr.Zero;
System.Console.WriteLine($"Removing gamepad from slot {slot}!");
SDL.SDL_GameControllerClose(Gamepads[slot].Handle);
Gamepads[slot].Unregister();
Logger.LogInfo($"Removing gamepad from slot {slot}!");
OnGamepadDisconnected(slot);
return;
}
}

View File

@ -1,6 +1,9 @@
namespace MoonWorks.Input
{
// Enum values are equivalent to the SDL Scancode value.
/// <summary>
/// Can be used to determine key state without a direct reference to the virtual button object.
/// Enum values are equivalent to the SDL Scancode value.
/// </summary>
public enum KeyCode : int
{
Unknown = 0,

View File

@ -4,12 +4,22 @@ using SDL2;
namespace MoonWorks.Input
{
/// <summary>
/// The keyboard input device abstraction.
/// </summary>
public class Keyboard
{
/// <summary>
/// True if any button on the keyboard is active. Useful for input remapping.
/// </summary>
public bool AnyPressed { get; private set; }
/// <summary>
/// Contains a reference to an arbitrary KeyboardButton that was pressed this frame. Useful for input remapping.
/// </summary>
public KeyboardButton AnyPressedButton { get; private set; }
public IntPtr State { get; private set; }
internal IntPtr State { get; private set; }
private KeyCode[] KeyCodes;
private KeyboardButton[] Keys { get; }
@ -78,41 +88,65 @@ namespace MoonWorks.Input
}
}
/// <summary>
/// True if the button was pressed this frame.
/// </summary>
public bool IsPressed(KeyCode keycode)
{
return Keys[(int) keycode].IsPressed;
}
/// <summary>
/// True if the button was pressed this frame and the previous frame.
/// </summary>
public bool IsHeld(KeyCode keycode)
{
return Keys[(int) keycode].IsHeld;
}
/// <summary>
/// True if the button was either pressed or continued to be held this frame.
/// </summary>
public bool IsDown(KeyCode keycode)
{
return Keys[(int) keycode].IsDown;
}
/// <summary>
/// True if the button was let go this frame.
/// </summary>
public bool IsReleased(KeyCode keycode)
{
return Keys[(int) keycode].IsReleased;
}
/// <summary>
/// True if the button was not pressed this frame or the previous frame.
/// </summary>
public bool IsIdle(KeyCode keycode)
{
return Keys[(int) keycode].IsIdle;
}
/// <summary>
/// True if the button was either idle or released this frame.
/// </summary>
public bool IsUp(KeyCode keycode)
{
return Keys[(int) keycode].IsUp;
}
/// <summary>
/// Gets a reference to a keyboard button object using a key code.
/// </summary>
public KeyboardButton Button(KeyCode keycode)
{
return Keys[(int) keycode];
}
/// <summary>
/// Gets the state of a keyboard button from a key code.
/// </summary>
public ButtonState ButtonState(KeyCode keycode)
{
return Keys[(int) keycode].State;

View File

@ -3,6 +3,9 @@ using SDL2;
namespace MoonWorks.Input
{
/// <summary>
/// The mouse input device abstraction.
/// </summary>
public class Mouse
{
public MouseButton LeftButton { get; }
@ -21,12 +24,23 @@ namespace MoonWorks.Input
internal int WheelRaw;
private int previousWheelRaw = 0;
/// <summary>
/// True if any button on the keyboard is active. Useful for input remapping.
/// </summary>
public bool AnyPressed { get; private set; }
/// <summary>
/// Contains a reference to an arbitrary MouseButton that was pressed this frame. Useful for input remapping.
/// </summary>
public MouseButton AnyPressedButton { get; private set; }
public uint ButtonMask { get; private set; }
internal uint ButtonMask { get; private set; }
private bool relativeMode;
/// <summary>
/// If set to true, the cursor is hidden, the mouse position is constrained to the window,
/// and relative mouse motion will be reported even if the mouse is at the edge of the window.
/// </summary>
public bool RelativeMode
{
get => relativeMode;
@ -41,9 +55,23 @@ namespace MoonWorks.Input
}
}
private bool hidden;
/// <summary>
/// If set to true, the OS cursor will not be shown in your application window.
/// </summary>
public bool Hidden
{
get => hidden;
set
{
hidden = value;
SDL.SDL_ShowCursor(hidden ? SDL.SDL_DISABLE : SDL.SDL_ENABLE);
}
}
private readonly Dictionary<MouseButtonCode, MouseButton> CodeToButton;
public Mouse()
internal Mouse()
{
LeftButton = new MouseButton(this, MouseButtonCode.Left, SDL.SDL_BUTTON_LMASK);
MiddleButton = new MouseButton(this, MouseButtonCode.Middle, SDL.SDL_BUTTON_MMASK);
@ -88,6 +116,17 @@ namespace MoonWorks.Input
}
}
/// <summary>
/// Gets a button from the mouse given a MouseButtonCode.
/// </summary>
public MouseButton Button(MouseButtonCode buttonCode)
{
return CodeToButton[buttonCode];
}
/// <summary>
/// Gets a button state from a mouse button corresponding to the given MouseButtonCode.
/// </summary>
public ButtonState ButtonState(MouseButtonCode buttonCode)
{
return CodeToButton[buttonCode].State;

View File

@ -1,5 +1,8 @@
namespace MoonWorks.Input
{
/// <summary>
/// Can be used to determine virtual mouse button state without a direct reference to the button object.
/// </summary>
public enum MouseButtonCode
{
Left,

View File

@ -3,6 +3,9 @@ using SDL2;
namespace MoonWorks.Input
{
/// <summary>
/// Represents a trigger input on a gamepad.
/// </summary>
public class Trigger
{
public Gamepad Parent { get; }

View File

@ -1,6 +1,9 @@
namespace MoonWorks.Input
{
// Enum values correspond to SDL GameControllerAxis
/// <summary>
/// Can be used to determine trigger state or trigger virtual button state without direct reference to the trigger object or virtual button object.
/// Enum values correspond to SDL_GameControllerAxis.
/// </summary>
public enum TriggerCode
{
Left = 4,

View File

@ -1,5 +1,8 @@
namespace MoonWorks.Input
{
/// <summary>
/// VirtualButtons map inputs to binary inputs, like a trigger threshold or joystick axis threshold.
/// </summary>
public abstract class VirtualButton
{
public ButtonState State { get; protected set; }

View File

@ -1,11 +1,15 @@
namespace MoonWorks.Input
{
/// <summary>
/// A virtual button corresponding to a direction on a joystick.
/// If the axis value exceeds the threshold, it will be treated as a press.
/// </summary>
public class AxisButton : VirtualButton
{
public Axis Parent { get; }
public AxisButtonCode Code { get; }
private float threshold = 0.9f;
private float threshold = 0.5f;
public float Threshold
{
get => threshold;

View File

@ -1,5 +1,8 @@
namespace MoonWorks.Input
{
/// <summary>
/// A dummy button that can never be pressed. Used for the dummy gamepad.
/// </summary>
public class EmptyButton : VirtualButton
{
internal override bool CheckPressed()

View File

@ -2,6 +2,9 @@ using SDL2;
namespace MoonWorks.Input
{
/// <summary>
/// A virtual button corresponding to a gamepad button.
/// </summary>
public class GamepadButton : VirtualButton
{
public Gamepad Parent { get; }

View File

@ -1,11 +1,12 @@
using System.Runtime.InteropServices;
namespace MoonWorks.Input
{
/// <summary>
/// A virtual button corresponding to a keyboard button.
/// </summary>
public class KeyboardButton : VirtualButton
{
Keyboard Parent;
KeyCode KeyCode;
public KeyCode KeyCode { get; }
internal KeyboardButton(Keyboard parent, KeyCode keyCode)
{
@ -13,9 +14,9 @@ namespace MoonWorks.Input
KeyCode = keyCode;
}
internal override bool CheckPressed()
internal unsafe override bool CheckPressed()
{
return Conversions.ByteToBool(Marshal.ReadByte(Parent.State, (int) KeyCode));
return Conversions.ByteToBool(((byte*) Parent.State)[(int) KeyCode]);
}
}
}

View File

@ -1,5 +1,8 @@
namespace MoonWorks.Input
{
/// <summary>
/// A virtual button corresponding to a mouse button.
/// </summary>
public class MouseButton : VirtualButton
{
Mouse Parent;

View File

@ -1,5 +1,9 @@
namespace MoonWorks.Input
{
/// <summary>
/// A virtual button corresponding to a trigger on a gamepad.
/// If the trigger value exceeds the threshold, it will be treated as a press.
/// </summary>
public class TriggerButton : VirtualButton
{
public Trigger Parent { get; }

View File

@ -5,9 +5,9 @@ namespace MoonWorks
{
public static class Logger
{
public static Action<string> LogInfo;
public static Action<string> LogWarn;
public static Action<string> LogError;
public static Action<string> LogInfo = LogInfoDefault;
public static Action<string> LogWarn = LogWarnDefault;
public static Action<string> LogError = LogErrorDefault;
private static RefreshCS.Refresh.Refresh_LogFunc LogInfoFunc = RefreshLogInfo;
private static RefreshCS.Refresh.Refresh_LogFunc LogWarnFunc = RefreshLogWarn;
@ -15,19 +15,6 @@ namespace MoonWorks
internal static void Initialize()
{
if (Logger.LogInfo == null)
{
Logger.LogInfo = Console.WriteLine;
}
if (Logger.LogWarn == null)
{
Logger.LogWarn = Console.WriteLine;
}
if (Logger.LogError == null)
{
Logger.LogError = Console.WriteLine;
}
Refresh.Refresh_HookLogFunctions(
LogInfoFunc,
LogWarnFunc,
@ -35,6 +22,30 @@ namespace MoonWorks
);
}
private static void LogInfoDefault(string str)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("INFO: ");
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(str);
}
private static void LogWarnDefault(string str)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write("WARN: ");
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(str);
}
private static void LogErrorDefault(string str)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Write("ERROR: ");
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(str);
}
private static void RefreshLogInfo(IntPtr msg)
{
LogInfo(UTF8_ToManaged(msg));

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ namespace MoonWorks.Math.Fixed
{
public struct Fix64 : IEquatable<Fix64>, IComparable<Fix64>
{
private readonly long RawValue;
public long RawValue { get; }
const long MAX_VALUE = long.MaxValue;
const long MIN_VALUE = long.MinValue;
@ -17,6 +17,9 @@ namespace MoonWorks.Math.Fixed
const long PI_TIMES_2 = 0x6487ED511;
const long PI = 0x3243F6A88;
const long PI_OVER_2 = 0x1921FB544;
const long LN2 = 0xB17217F7;
const long LOG2MAX = 0x1F00000000;
const long LOG2MIN = -0x2000000000;
public static readonly Fix64 MaxValue = new Fix64(MAX_VALUE);
public static readonly Fix64 MinValue = new Fix64(MIN_VALUE);
@ -28,6 +31,10 @@ namespace MoonWorks.Math.Fixed
public static readonly Fix64 PiOver4 = PiOver2 / new Fix64(2);
public static readonly Fix64 PiTimes2 = new Fix64(PI_TIMES_2);
static readonly Fix64 Ln2 = new Fix64(LN2);
static readonly Fix64 Log2Max = new Fix64(LOG2MAX);
static readonly Fix64 Log2Min = new Fix64(LOG2MIN);
const int LUT_SIZE = (int)(PI_OVER_2 >> 15);
static readonly Fix64 LutInterval = (Fix64)(LUT_SIZE - 1) / PiOver2;
@ -52,6 +59,11 @@ namespace MoonWorks.Math.Fixed
return new Fix64(numerator) / new Fix64(denominator);
}
public static Fix64 FromRawValue(long value)
{
return new Fix64(value);
}
/// <summary>
/// Gets the fractional component of this Fix64 value.
/// </summary>
@ -60,30 +72,6 @@ namespace MoonWorks.Math.Fixed
return new Fix64(number.RawValue & 0x00000000FFFFFFFF);
}
public static Fix64 Random(System.Random random, int max)
{
return new Fix64(random.NextInt64(new Fix64(max).RawValue));
}
public static Fix64 Random(System.Random random, Fix64 max)
{
return new Fix64(random.NextInt64(max.RawValue));
}
public static Fix64 Random(System.Random random, Fix64 min, Fix64 max)
{
return new Fix64(random.NextInt64(min.RawValue, max.RawValue));
}
// Max should be between 0.0 and 1.0.
public static Fix64 RandomFraction(System.Random random, Fix64 max)
{
long fractionalPart = (max.RawValue & 0x00000000FFFFFFFF);
long fractional = random.NextInt64(fractionalPart);
return new Fix64(fractional);
}
/// <summary>
/// Returns an int indicating the sign of a Fix64 number.
/// </summary>
@ -212,7 +200,145 @@ namespace MoonWorks.Math.Fixed
return Fix64.Floor(value / step) * step;
}
// Trigonometry functions
// Exponentiation functions
/// <summary>
/// Returns 2 raised to the specified power.
/// Provides at least 6 decimals of accuracy.
/// </summary>
internal static Fix64 Pow2(Fix64 x)
{
if (x.RawValue == 0)
{
return One;
}
// Avoid negative arguments by exploiting that exp(-x) = 1/exp(x).
bool neg = x.RawValue < 0;
if (neg)
{
x = -x;
}
if (x == One)
{
return neg ? One / (Fix64)2 : (Fix64)2;
}
if (x >= Log2Max)
{
return neg ? One / MaxValue : MaxValue;
}
if (x <= Log2Min)
{
return neg ? MaxValue : Zero;
}
/* The algorithm is based on the power series for exp(x):
* http://en.wikipedia.org/wiki/Exponential_function#Formal_definition
*
* From term n, we get term n+1 by multiplying with x/n.
* When the sum term drops to zero, we can stop summing.
*/
int integerPart = (int)Floor(x);
// Take fractional part of exponent
x = new Fix64(x.RawValue & 0x00000000FFFFFFFF);
var result = One;
var term = One;
int i = 1;
while (term.RawValue != 0)
{
term = FastMul(FastMul(x, term), Ln2) / (Fix64)i;
result += term;
i++;
}
result = new Fix64(result.RawValue << integerPart);
if (neg)
{
result = One / result;
}
return result;
}
/// <summary>
/// Returns the base-2 logarithm of a specified number.
/// Provides at least 9 decimals of accuracy.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// The argument was non-positive
/// </exception>
internal static Fix64 Log2(Fix64 x)
{
if (x.RawValue <= 0)
{
throw new ArgumentOutOfRangeException("Non-positive value passed to Ln", "x");
}
// This implementation is based on Clay. S. Turner's fast binary logarithm
// algorithm (C. S. Turner, "A Fast Binary Logarithm Algorithm", IEEE Signal
// Processing Mag., pp. 124,140, Sep. 2010.)
long b = 1U << (FRACTIONAL_PLACES - 1);
long y = 0;
long rawX = x.RawValue;
while (rawX < ONE)
{
rawX <<= 1;
y -= ONE;
}
while (rawX >= (ONE << 1))
{
rawX >>= 1;
y += ONE;
}
var z = new Fix64(rawX);
for (int i = 0; i < FRACTIONAL_PLACES; i++)
{
z = FastMul(z, z);
if (z.RawValue >= (ONE << 1))
{
z = new Fix64(z.RawValue >> 1);
y += b;
}
b >>= 1;
}
return new Fix64(y);
}
public static Fix64 Pow(Fix64 b, Fix64 exp)
{
if (b == One)
{
return One;
}
if (exp.RawValue == 0)
{
return One;
}
if (exp.RawValue == ONE)
{
return b;
}
if (b.RawValue == 0)
{
if (exp.RawValue < 0)
{
throw new DivideByZeroException();
}
return Zero;
}
Fix64 log2 = Log2(b);
return Pow2(exp * log2);
}
/// <summary>
/// Returns the square root of the given Fix64 value.

View File

@ -996,8 +996,8 @@ namespace MoonWorks.Math.Fixed
z = Vector3.Normalize(forward);
Vector3.Cross(ref forward, ref up, out x);
Vector3.Cross(ref x, ref forward, out y);
x.Normalize();
y.Normalize();
x = Vector3.Normalize(x);
y = Vector3.Normalize(y);
result = new Matrix4x4();
result.Right = x;

View File

@ -214,23 +214,6 @@ namespace MoonWorks.Math.Fixed
);
}
/// <summary>
/// Scales the quaternion magnitude to unit length.
/// </summary>
public void Normalize()
{
Fix64 num = Fix64.One / (Fix64.Sqrt(
(X * X) +
(Y * Y) +
(Z * Z) +
(W * W)
));
this.X *= num;
this.Y *= num;
this.Z *= num;
this.W *= num;
}
/// <summary>
/// Returns a <see cref="String"/> representation of this <see cref="Quaternion"/> in the format:
/// {X:[<see cref="X"/>] Y:[<see cref="Y"/>] Z:[<see cref="Z"/>] W:[<see cref="W"/>]}
@ -759,12 +742,16 @@ namespace MoonWorks.Math.Fixed
/// <param name="result">The unit length quaternion an output parameter.</param>
public static void Normalize(ref Quaternion quaternion, out Quaternion result)
{
Fix64 num = Fix64.One / (Fix64.Sqrt(
(quaternion.X * quaternion.X) +
(quaternion.Y * quaternion.Y) +
(quaternion.Z * quaternion.Z) +
(quaternion.W * quaternion.W)
));
Fix64 lengthSquared = (quaternion.X * quaternion.X) + (quaternion.Y * quaternion.Y) +
(quaternion.Z * quaternion.Z) + (quaternion.W * quaternion.W);
if (lengthSquared == Fix64.Zero)
{
result = Identity;
return;
}
Fix64 num = Fix64.One / Fix64.Sqrt(lengthSquared);
result.X = quaternion.X * num;
result.Y = quaternion.Y * num;
result.Z = quaternion.Z * num;

View File

@ -1,105 +0,0 @@
namespace MoonWorks.Math.Fixed
{
public struct Transform2D : System.IEquatable<Transform2D>
{
public Vector2 Position { get; }
public Fix64 Rotation { get; }
public Vector2 Scale { get; }
private bool transformMatrixCalculated;
private Matrix3x2 transformMatrix;
public Matrix3x2 TransformMatrix
{
get
{
if (!transformMatrixCalculated)
{
transformMatrix = CreateTransformMatrix(Position, Rotation, Scale);
transformMatrixCalculated = true;
}
return transformMatrix;
}
}
public bool IsAxisAligned => Rotation % Fix64.PiOver2 == Fix64.Zero;
public bool IsUniformScale => Scale.X == Scale.Y;
public static readonly Transform2D Identity = new Transform2D(Vector2.Zero, Fix64.Zero, Vector2.One);
public Transform2D(Vector2 position)
{
Position = position;
Rotation = Fix64.Zero;
Scale = Vector2.One;
transformMatrixCalculated = false;
transformMatrix = Matrix3x2.Identity;
}
public Transform2D(Vector2 position, Fix64 rotation)
{
Position = position;
Rotation = rotation;
Scale = Vector2.One;
transformMatrixCalculated = false;
transformMatrix = Matrix3x2.Identity;
}
public Transform2D(Vector2 position, Fix64 rotation, Vector2 scale)
{
Position = position;
Rotation = rotation;
Scale = scale;
transformMatrixCalculated = false;
transformMatrix = Matrix3x2.Identity;
}
public Transform2D Compose(Transform2D other)
{
return new Transform2D(Position + other.Position, Rotation + other.Rotation, Scale * other.Scale);
}
private static Matrix3x2 CreateTransformMatrix(Vector2 position, Fix64 rotation, Vector2 scale)
{
return
Matrix3x2.CreateScale(scale) *
Matrix3x2.CreateRotation(rotation) *
Matrix3x2.CreateTranslation(position);
}
public bool Equals(Transform2D other)
{
return
Position == other.Position &&
Rotation == other.Rotation &&
Scale == other.Scale;
}
public override bool Equals(System.Object other)
{
if (other is Transform2D otherTransform)
{
return Equals(otherTransform);
}
return false;
}
public override int GetHashCode()
{
return System.HashCode.Combine(Position, Rotation, Scale);
}
public static bool operator ==(Transform2D a, Transform2D b)
{
return a.Equals(b);
}
public static bool operator !=(Transform2D a, Transform2D b)
{
return !a.Equals(b);
}
}
}

View File

@ -200,16 +200,6 @@ namespace MoonWorks.Math.Fixed
return (X * X) + (Y * Y);
}
/// <summary>
/// Turns this <see cref="Vector2"/> to a unit vector with the same direction.
/// </summary>
public void Normalize()
{
Fix64 val = Fix64.One / Fix64.Sqrt((X * X) + (Y * Y));
X *= val;
Y *= val;
}
/// <summary>
/// Turns this <see cref="Vector2"/> to an angle in radians.
/// </summary>
@ -423,7 +413,14 @@ namespace MoonWorks.Math.Fixed
/// <returns>Unit vector.</returns>
public static Vector2 Normalize(Vector2 value)
{
Fix64 val = Fix64.One / Fix64.Sqrt((value.X * value.X) + (value.Y * value.Y));
Fix64 lengthSquared = (value.X * value.X) + (value.Y * value.Y);
if (lengthSquared == Fix64.Zero)
{
return Zero;
}
Fix64 val = Fix64.One / Fix64.Sqrt(lengthSquared);
value.X *= val;
value.Y *= val;
return value;

View File

@ -309,21 +309,6 @@ namespace MoonWorks.Math.Fixed
return (X * X) + (Y * Y) + (Z * Z);
}
/// <summary>
/// Turns this <see cref="Vector3"/> to a unit vector with the same direction.
/// </summary>
public void Normalize()
{
Fix64 factor = Fix64.One / Fix64.Sqrt(
(X * X) +
(Y * Y) +
(Z * Z)
);
X *= factor;
Y *= factor;
Z *= factor;
}
/// <summary>
/// Returns a <see cref="String"/> representation of this <see cref="Vector3"/> in the format:
/// {X:[<see cref="X"/>] Y:[<see cref="Y"/>] Z:[<see cref="Z"/>]}
@ -733,11 +718,14 @@ namespace MoonWorks.Math.Fixed
/// <returns>Unit vector.</returns>
public static Vector3 Normalize(Vector3 value)
{
Fix64 factor = Fix64.One / Fix64.Sqrt(
(value.X * value.X) +
(value.Y * value.Y) +
(value.Z * value.Z)
);
Fix64 lengthSquared = (value.X * value.X) + (value.Y * value.Y) + (value.Z * value.Z);
if (lengthSquared == Fix64.Zero)
{
return Zero;
}
Fix64 factor = Fix64.One / Fix64.Sqrt(lengthSquared);
return new Vector3(
value.X * factor,
value.Y * factor,

View File

@ -611,7 +611,7 @@ namespace MoonWorks.Math.Float
);
}
Vector3.Cross(ref cameraUpVector, ref vector, out vector3);
vector3.Normalize();
vector3 = Vector3.Normalize(vector3);
Vector3.Cross(ref vector, ref vector3, out vector2);
result.M11 = vector3.X;
result.M12 = vector3.Y;
@ -730,16 +730,16 @@ namespace MoonWorks.Math.Float
Vector3.Forward;
}
Vector3.Cross(ref rotateAxis, ref vector, out vector3);
vector3.Normalize();
vector3 = Vector3.Normalize(vector3);
Vector3.Cross(ref vector3, ref rotateAxis, out vector);
vector.Normalize();
vector = Vector3.Normalize(vector);
}
else
{
Vector3.Cross(ref rotateAxis, ref vector2, out vector3);
vector3.Normalize();
vector3 = Vector3.Normalize(vector3);
Vector3.Cross(ref vector3, ref vector4, out vector);
vector.Normalize();
vector = Vector3.Normalize(vector);
}
result.M11 = vector3.X;
@ -1701,8 +1701,8 @@ namespace MoonWorks.Math.Float
Vector3.Normalize(ref forward, out z);
Vector3.Cross(ref forward, ref up, out x);
Vector3.Cross(ref x, ref forward, out y);
x.Normalize();
y.Normalize();
x = Vector3.Normalize(x);
y = Vector3.Normalize(y);
result = new Matrix4x4();
result.Right = x;

View File

@ -1,105 +0,0 @@
namespace MoonWorks.Math.Float
{
public struct Transform2D : System.IEquatable<Transform2D>
{
public Vector2 Position { get; }
public float Rotation { get; }
public Vector2 Scale { get; }
private bool transformMatrixCalculated;
private Matrix3x2 transformMatrix;
public Matrix3x2 TransformMatrix
{
get
{
if (!transformMatrixCalculated)
{
transformMatrix = CreateTransformMatrix(Position, Rotation, Scale);
transformMatrixCalculated = true;
}
return transformMatrix;
}
}
public bool IsAxisAligned => Rotation % MathHelper.PiOver2 == 0;
public bool IsUniformScale => Scale.X == Scale.Y;
public static readonly Transform2D Identity = new Transform2D(Vector2.Zero, 0, Vector2.One);
public Transform2D(Vector2 position)
{
Position = position;
Rotation = 0;
Scale = Vector2.One;
transformMatrixCalculated = false;
transformMatrix = Matrix3x2.Identity;
}
public Transform2D(Vector2 position, float rotation)
{
Position = position;
Rotation = rotation;
Scale = Vector2.One;
transformMatrixCalculated = false;
transformMatrix = Matrix3x2.Identity;
}
public Transform2D(Vector2 position, float rotation, Vector2 scale)
{
Position = position;
Rotation = rotation;
Scale = scale;
transformMatrixCalculated = false;
transformMatrix = Matrix3x2.Identity;
}
public Transform2D Compose(Transform2D other)
{
return new Transform2D(Position + other.Position, Rotation + other.Rotation, Scale * other.Scale);
}
private static Matrix3x2 CreateTransformMatrix(Vector2 position, float rotation, Vector2 scale)
{
return
Matrix3x2.CreateScale(scale) *
Matrix3x2.CreateRotation(rotation) *
Matrix3x2.CreateTranslation(position);
}
public bool Equals(Transform2D other)
{
return
Position == other.Position &&
Rotation == other.Rotation &&
Scale == other.Scale;
}
public override bool Equals(System.Object other)
{
if (other is Transform2D otherTransform)
{
return Equals(otherTransform);
}
return false;
}
public override int GetHashCode()
{
return System.HashCode.Combine(Position, Rotation, Scale);
}
public static bool operator ==(Transform2D a, Transform2D b)
{
return a.Equals(b);
}
public static bool operator !=(Transform2D a, Transform2D b)
{
return !a.Equals(b);
}
}
}

View File

@ -194,16 +194,6 @@ namespace MoonWorks.Math.Float
return (X * X) + (Y * Y);
}
/// <summary>
/// Turns this <see cref="Vector2"/> to a unit vector with the same direction.
/// </summary>
public void Normalize()
{
float val = 1.0f / (float) System.Math.Sqrt((X * X) + (Y * Y));
X *= val;
Y *= val;
}
/// <summary>
/// Turns this <see cref="Vector2"/> to an angle in radians.
/// </summary>
@ -717,7 +707,14 @@ namespace MoonWorks.Math.Float
/// <returns>Unit vector.</returns>
public static Vector2 Normalize(Vector2 value)
{
float val = 1.0f / (float) System.Math.Sqrt((value.X * value.X) + (value.Y * value.Y));
float lengthSquared = (value.X * value.X) + (value.Y * value.Y);
if (lengthSquared == 0)
{
return Zero;
}
float val = 1.0f / System.MathF.Sqrt(lengthSquared);
value.X *= val;
value.Y *= val;
return value;

View File

@ -302,21 +302,6 @@ namespace MoonWorks.Math.Float
return (X * X) + (Y * Y) + (Z * Z);
}
/// <summary>
/// Turns this <see cref="Vector3"/> to a unit vector with the same direction.
/// </summary>
public void Normalize()
{
float factor = 1.0f / (float) System.Math.Sqrt(
(X * X) +
(Y * Y) +
(Z * Z)
);
X *= factor;
Y *= factor;
Z *= factor;
}
/// <summary>
/// Returns a <see cref="String"/> representation of this <see cref="Vector3"/> in the format:
/// {X:[<see cref="X"/>] Y:[<see cref="Y"/>] Z:[<see cref="Z"/>]}
@ -900,11 +885,14 @@ namespace MoonWorks.Math.Float
/// <returns>Unit vector.</returns>
public static Vector3 Normalize(Vector3 value)
{
float factor = 1.0f / (float) System.Math.Sqrt(
(value.X * value.X) +
(value.Y * value.Y) +
(value.Z * value.Z)
);
float lengthSquared = (value.X * value.X) + (value.Y * value.Y) + (value.Z * value.Z);
if (lengthSquared == 0f)
{
return Zero;
}
float factor = 1.0f / System.MathF.Sqrt(lengthSquared);
return new Vector3(
value.X * factor,
value.Y * factor,

View File

@ -267,23 +267,6 @@ namespace MoonWorks.Math.Float
return (X * X) + (Y * Y) + (Z * Z) + (W * W);
}
/// <summary>
/// Turns this <see cref="Vector4"/> to a unit vector with the same direction.
/// </summary>
public void Normalize()
{
float factor = 1.0f / (float) System.Math.Sqrt(
(X * X) +
(Y * Y) +
(Z * Z) +
(W * W)
);
X *= factor;
Y *= factor;
Z *= factor;
W *= factor;
}
public override string ToString()
{
return (
@ -853,12 +836,15 @@ namespace MoonWorks.Math.Float
/// <returns>Unit vector.</returns>
public static Vector4 Normalize(Vector4 vector)
{
float factor = 1.0f / (float) System.Math.Sqrt(
(vector.X * vector.X) +
(vector.Y * vector.Y) +
(vector.Z * vector.Z) +
(vector.W * vector.W)
);
var lengthSquared = (vector.X * vector.X) + (vector.Y * vector.Y) +
(vector.Z * vector.Z) + (vector.W * vector.W);
if (lengthSquared == 0)
{
return Zero;
}
float factor = 1.0f / System.MathF.Sqrt(lengthSquared);
return new Vector4(
vector.X * factor,
vector.Y * factor,
@ -870,16 +856,20 @@ namespace MoonWorks.Math.Float
/// <summary>
/// Creates a new <see cref="Vector4"/> that contains a normalized values from another vector.
/// </summary>
/// <param name="value">Source <see cref="Vector4"/>.</param>
/// <param name="vector">Source <see cref="Vector4"/>.</param>
/// <param name="result">Unit vector as an output parameter.</param>
public static void Normalize(ref Vector4 vector, out Vector4 result)
{
float factor = 1.0f / (float) System.Math.Sqrt(
(vector.X * vector.X) +
(vector.Y * vector.Y) +
(vector.Z * vector.Z) +
(vector.W * vector.W)
);
float lengthSquared = (vector.X * vector.X) + (vector.Y * vector.Y) +
(vector.Z * vector.Z) + (vector.W * vector.W);
if (lengthSquared == 0)
{
result = Zero;
return;
}
float factor = 1.0f / System.MathF.Sqrt(lengthSquared);
result.X = vector.X * factor;
result.Y = vector.Y * factor;
result.Z = vector.Z * factor;

View File

@ -160,6 +160,26 @@ namespace MoonWorks.Math
return value;
}
/// <summary>
/// Restricts a value to be within a specified range.
/// </summary>
/// <param name="value">The value to clamp.</param>
/// <param name="min">
/// The minimum value. If <c>value</c> is less than <c>min</c>, <c>min</c>
/// will be returned.
/// </param>
/// <param name="max">
/// The maximum value. If <c>value</c> is greater than <c>max</c>, <c>max</c>
/// will be returned.
/// </param>
/// <returns>The clamped value.</returns>
public static int Clamp(int value, int min, int max)
{
value = (value > max) ? max : value;
value = (value < min) ? min : value;
return value;
}
/// <summary>
/// Calculates the absolute value of the difference of two values.
/// </summary>
@ -282,7 +302,12 @@ namespace MoonWorks.Math
public static float Quantize(float value, float step)
{
return (float) System.Math.Floor(value / step) * step;
return System.MathF.Round(value / step) * step;
}
public static Fixed.Fix64 Quantize(Fixed.Fix64 value, Fixed.Fix64 step)
{
return Fixed.Fix64.Round(value / step) * step;
}
/// <summary>
@ -395,27 +420,6 @@ namespace MoonWorks.Math
#region Internal Static Methods
// FIXME: This could be an extension! ClampIntEXT? -flibit
/// <summary>
/// Restricts a value to be within a specified range.
/// </summary>
/// <param name="value">The value to clamp.</param>
/// <param name="min">
/// The minimum value. If <c>value</c> is less than <c>min</c>, <c>min</c>
/// will be returned.
/// </param>
/// <param name="max">
/// The maximum value. If <c>value</c> is greater than <c>max</c>, <c>max</c>
/// will be returned.
/// </param>
/// <returns>The clamped value.</returns>
internal static int Clamp(int value, int min, int max)
{
value = (value > max) ? max : value;
value = (value < min) ? min : value;
return value;
}
internal static bool WithinEpsilon(float floatA, float floatB)
{
return System.Math.Abs(floatA - floatB) < MachineEpsilonFloat;

View File

@ -97,16 +97,12 @@ namespace MoonWorks
// Get the path to the assembly
Assembly assembly = Assembly.GetExecutingAssembly();
string assemblyPath = "";
if (assembly.Location != null)
{
assemblyPath = Path.GetDirectoryName(assembly.Location);
}
string assemblyPath = System.AppContext.BaseDirectory;
// Locate the config file
string xmlPath = Path.Combine(
assemblyPath,
assembly.GetName().Name + ".dll.config"
"MoonWorks.dll.config"
);
if (!File.Exists(xmlPath))
{

View File

@ -1,45 +0,0 @@
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;
}
}
}

View File

@ -1,120 +0,0 @@
/* Heavily based on https://github.com/FNA-XNA/FNA/blob/master/src/Media/Xiph/VideoPlayer.cs */
using System;
using System.Runtime.InteropServices;
namespace MoonWorks.Video
{
public enum VideoState
{
Playing,
Paused,
Stopped
}
public unsafe class Video : IDisposable
{
internal IntPtr Handle;
private IntPtr rwData;
private void* videoData;
public double FramesPerSecond => fps;
public int Width => yWidth;
public int Height => yHeight;
public int UVWidth { get; }
public int UVHeight { get; }
private double fps;
private int yWidth;
private int yHeight;
private bool disposed;
public Video(string filename)
{
if (!System.IO.File.Exists(filename))
{
throw new ArgumentException("Video file not found!");
}
var bytes = System.IO.File.ReadAllBytes(filename);
videoData = NativeMemory.Alloc((nuint) bytes.Length);
Marshal.Copy(bytes, 0, (IntPtr) videoData, bytes.Length);
rwData = SDL2.SDL.SDL_RWFromMem((IntPtr) videoData, bytes.Length);
if (Theorafile.tf_open_callbacks(rwData, out Handle, callbacks) < 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 = Width / 2;
UVHeight = Height / 2;
}
else if (format == Theorafile.th_pixel_fmt.TH_PF_422)
{
UVWidth = Width / 2;
UVHeight = Height;
}
else if (format == Theorafile.th_pixel_fmt.TH_PF_444)
{
UVWidth = Width;
UVHeight = Height;
}
else
{
throw new NotSupportedException("Unrecognized YUV format!");
}
}
private static IntPtr Read(IntPtr ptr, IntPtr size, IntPtr nmemb, IntPtr datasource) => (IntPtr) SDL2.SDL.SDL_RWread(datasource, ptr, size, nmemb);
private static int Seek(IntPtr datasource, long offset, Theorafile.SeekWhence whence) => (int) SDL2.SDL.SDL_RWseek(datasource, offset, (int) whence);
private static int Close(IntPtr datasource) => (int) SDL2.SDL.SDL_RWclose(datasource);
private static Theorafile.tf_callbacks callbacks = new Theorafile.tf_callbacks
{
read_func = Read,
seek_func = Seek,
close_func = Close
};
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// dispose managed state (managed objects)
}
// free unmanaged resources (unmanaged objects)
Theorafile.tf_close(ref Handle);
NativeMemory.Free(videoData);
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);
}
}
}

89
src/Video/VideoAV1.cs Normal file
View File

@ -0,0 +1,89 @@
using System;
using System.IO;
using MoonWorks.Graphics;
namespace MoonWorks.Video
{
/// <summary>
/// This class takes in a filename for AV1 data in .obu (open bitstream unit) format
/// </summary>
public unsafe class VideoAV1 : GraphicsResource
{
public string Filename { get; }
// "double buffering" so we can loop without a stutter
internal VideoAV1Stream StreamA { get; }
internal VideoAV1Stream StreamB { 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;
/// <summary>
/// Opens an AV1 file so it can be loaded by VideoPlayer. You must also provide a playback framerate.
/// </summary>
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 var 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;
StreamA = new VideoAV1Stream(device, this);
StreamB = new VideoAV1Stream(device, this);
}
// NOTE: if you call this while a VideoPlayer is playing the stream, your program will explode
protected override void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
StreamA.Dispose();
StreamB.Dispose();
}
}
base.Dispose(disposing);
}
}
}

View File

@ -0,0 +1,82 @@
using System;
using MoonWorks.Graphics;
namespace MoonWorks.Video
{
internal class VideoAV1Stream : GraphicsResource
{
public IntPtr Handle => handle;
IntPtr handle;
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; }
public VideoAV1Stream(GraphicsDevice device, VideoAV1 video) : base(device)
{
if (Dav1dfile.df_fopen(video.Filename, out handle) == 0)
{
throw new Exception("Failed to open video file!");
}
Reset();
}
public void Reset()
{
lock (this)
{
Dav1dfile.df_reset(Handle);
ReadNextFrame();
}
}
public void ReadNextFrame()
{
lock (this)
{
if (!Ended)
{
if (Dav1dfile.df_readvideo(
Handle,
1,
out var yDataHandle,
out var uDataHandle,
out var vDataHandle,
out var yDataLength,
out var uvDataLength,
out var yStride,
out var 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;
}
}
}
}
protected override void Dispose(bool disposing)
{
if (!IsDisposed)
{
Dav1dfile.df_close(Handle);
}
base.Dispose(disposing);
}
}
}

View File

@ -1,30 +1,26 @@
using System;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using MoonWorks.Audio;
using System.Threading.Tasks;
using MoonWorks.Graphics;
namespace MoonWorks.Video
{
public unsafe class VideoPlayer : IDisposable
/// <summary>
/// A structure for continuous decoding of AV1 videos and rendering them into a texture.
/// </summary>
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 Volume {
get => volume;
set
{
volume = value;
if (audioStream != null)
{
audioStream.Volume = value;
}
}
}
public float PlaybackSpeed { get; set; } = 1;
private Video Video = null;
private VideoAV1 Video = null;
private VideoAV1Stream CurrentStream = null;
private Task ReadNextFrameTask;
private Task ResetStreamATask;
private Task ResetStreamBTask;
private GraphicsDevice GraphicsDevice;
private Texture yTexture = null;
@ -32,36 +28,26 @@ namespace MoonWorks.Video
private Texture vTexture = null;
private Sampler LinearSampler;
private void* yuvData = null;
private int yuvDataLength = 0;
private int currentFrame;
private AudioDevice AudioDevice;
private StreamingSoundTheora audioStream = null;
private float volume = 1.0f;
private Stopwatch timer;
private double lastTimestamp;
private double timeElapsed;
private bool disposed;
public VideoPlayer(GraphicsDevice graphicsDevice, AudioDevice audioDevice)
public VideoPlayer(GraphicsDevice device) : base(device)
{
GraphicsDevice = graphicsDevice;
if (GraphicsDevice.VideoPipeline == null)
{
throw new InvalidOperationException("Missing video shaders!");
}
GraphicsDevice = device;
AudioDevice = audioDevice;
LinearSampler = new Sampler(graphicsDevice, SamplerCreateInfo.LinearClamp);
LinearSampler = new Sampler(device, SamplerCreateInfo.LinearClamp);
timer = new Stopwatch();
}
public void Load(Video video)
/// <summary>
/// Prepares a VideoAV1 for decoding and rendering.
/// </summary>
/// <param name="video"></param>
public void Load(VideoAV1 video)
{
if (Video != video)
{
@ -111,25 +97,19 @@ namespace MoonWorks.Video
vTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight);
}
var newDataLength = (
(video.Width * video.Height) +
(video.UVWidth * video.UVHeight * 2)
);
if (newDataLength != yuvDataLength)
{
yuvData = NativeMemory.Realloc(yuvData, (nuint) newDataLength);
yuvDataLength = newDataLength;
}
Video = video;
InitializeTheoraStream();
InitializeDav1dStream();
}
}
/// <summary>
/// Starts playing back and decoding the loaded video.
/// </summary>
public void Play()
{
if (Video == null) { return; }
if (State == VideoState.Playing)
{
return;
@ -137,16 +117,16 @@ namespace MoonWorks.Video
timer.Start();
if (audioStream != null)
{
audioStream.Play();
}
State = VideoState.Playing;
}
/// <summary>
/// Pauses playback and decoding of the currently playing video.
/// </summary>
public void Pause()
{
if (Video == null) { return; }
if (State != VideoState.Playing)
{
return;
@ -154,16 +134,16 @@ namespace MoonWorks.Video
timer.Stop();
if (audioStream != null)
{
audioStream.Pause();
}
State = VideoState.Paused;
}
/// <summary>
/// Stops and resets decoding of the currently playing video.
/// </summary>
public void Stop()
{
if (Video == null) { return; }
if (State == VideoState.Stopped)
{
return;
@ -172,20 +152,28 @@ namespace MoonWorks.Video
timer.Stop();
timer.Reset();
Theorafile.tf_reset(Video.Handle);
lastTimestamp = 0;
timeElapsed = 0;
if (audioStream != null)
{
audioStream.StopImmediate();
audioStream.Dispose();
audioStream = null;
}
InitializeDav1dStream();
State = VideoState.Stopped;
}
/// <summary>
/// Unloads the currently playing video.
/// </summary>
public void Unload()
{
Stop();
ResetStreamATask?.Wait();
ResetStreamBTask?.Wait();
Video = null;
}
/// <summary>
/// Renders the video data into RenderTexture.
/// </summary>
public void Render()
{
if (Video == null || State == VideoState.Stopped)
@ -199,37 +187,39 @@ namespace MoonWorks.Video
int thisFrame = ((int) (timeElapsed / (1000.0 / Video.FramesPerSecond)));
if (thisFrame > currentFrame)
{
if (Theorafile.tf_readvideo(
Video.Handle,
(IntPtr) yuvData,
thisFrame - currentFrame
) == 1 || currentFrame == -1) {
if (CurrentStream.FrameDataUpdated)
{
UpdateRenderTexture();
CurrentStream.FrameDataUpdated = false;
}
currentFrame = thisFrame;
ReadNextFrameTask = Task.Run(CurrentStream.ReadNextFrame);
ReadNextFrameTask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
}
bool ended = Theorafile.tf_eos(Video.Handle) == 1;
if (ended)
if (CurrentStream.Ended)
{
timer.Stop();
timer.Reset();
if (audioStream != null)
{
audioStream.Stop();
audioStream.Dispose();
audioStream = null;
}
var task = Task.Run(CurrentStream.Reset);
task.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
Theorafile.tf_reset(Video.Handle);
if (CurrentStream == Video.StreamA)
{
ResetStreamATask = task;
}
else
{
ResetStreamBTask = task;
}
if (Loop)
{
// Start over!
InitializeTheoraStream();
// Start over on the next stream!
CurrentStream = (CurrentStream == Video.StreamA) ? Video.StreamB : Video.StreamA;
currentFrame = -1;
timer.Start();
}
else
@ -240,6 +230,8 @@ namespace MoonWorks.Video
}
private void UpdateRenderTexture()
{
lock (CurrentStream)
{
var commandBuffer = GraphicsDevice.AcquireCommandBuffer();
@ -247,8 +239,13 @@ namespace MoonWorks.Video
yTexture,
uTexture,
vTexture,
(IntPtr) yuvData,
(uint) yuvDataLength
CurrentStream.yDataHandle,
CurrentStream.uDataHandle,
CurrentStream.vDataHandle,
CurrentStream.yDataLength,
CurrentStream.uvDataLength,
CurrentStream.yStride,
CurrentStream.uvStride
);
commandBuffer.BeginRenderPass(
@ -268,6 +265,7 @@ namespace MoonWorks.Video
GraphicsDevice.Submit(commandBuffer);
}
}
private static Texture CreateRenderTexture(GraphicsDevice graphicsDevice, int width, int height)
{
@ -291,53 +289,42 @@ namespace MoonWorks.Video
);
}
private void InitializeTheoraStream()
private void InitializeDav1dStream()
{
// Grab the first video frame ASAP.
while (Theorafile.tf_readvideo(Video.Handle, (IntPtr) yuvData, 1) == 0);
ReadNextFrameTask?.Wait();
// Grab the first bit of audio. We're trying to start the decoding ASAP.
if (AudioDevice != null && Theorafile.tf_hasaudio(Video.Handle) == 1)
{
int channels, sampleRate;
Theorafile.tf_audioinfo(Video.Handle, out channels, out sampleRate);
audioStream = new StreamingSoundTheora(AudioDevice, Video.Handle, channels, (uint) sampleRate);
}
ResetStreamATask = Task.Run(Video.StreamA.Reset);
ResetStreamATask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
ResetStreamBTask = Task.Run(Video.StreamB.Reset);
ResetStreamBTask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
CurrentStream = Video.StreamA;
currentFrame = -1;
}
protected virtual void Dispose(bool disposing)
private static void HandleTaskException(Task task)
{
if (!disposed)
if (task.Exception.InnerException is not TaskCanceledException)
{
throw task.Exception;
}
}
protected override void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
// dispose managed state (managed objects)
RenderTexture.Dispose();
yTexture.Dispose();
uTexture.Dispose();
vTexture.Dispose();
}
Unload();
// free unmanaged resources (unmanaged objects) and override finalizer
NativeMemory.Free(yuvData);
disposed = true;
RenderTexture?.Dispose();
yTexture?.Dispose();
uTexture?.Dispose();
vTexture?.Dispose();
}
}
~VideoPlayer()
{
// 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);
base.Dispose(disposing);
}
}
}

9
src/Video/VideoState.cs Normal file
View File

@ -0,0 +1,9 @@
namespace MoonWorks.Video
{
public enum VideoState
{
Playing,
Paused,
Stopped
}
}

View File

@ -5,13 +5,18 @@ using SDL2;
namespace MoonWorks
{
/// <summary>
/// Represents a window in the client operating system. <br/>
/// Every Game has a MainWindow automatically. <br/>
/// You can create additional Windows if you desire. They must be Claimed by the GraphicsDevice to be rendered to.
/// </summary>
public class Window : IDisposable
{
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; } = null;
internal Texture SwapchainTexture { get; set; }
public bool Claimed { get; internal set; }
public MoonWorks.Graphics.TextureFormat SwapchainFormat { get; internal set; }
@ -45,21 +50,28 @@ namespace MoonWorks
ScreenMode = windowCreateInfo.ScreenMode;
SDL.SDL_GetDesktopDisplayMode(0, out var displayMode);
Handle = SDL.SDL_CreateWindow(
windowCreateInfo.WindowTitle,
SDL.SDL_WINDOWPOS_UNDEFINED,
SDL.SDL_WINDOWPOS_UNDEFINED,
(int) windowCreateInfo.WindowWidth,
(int) windowCreateInfo.WindowHeight,
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
);
Width = windowCreateInfo.WindowWidth;
Height = windowCreateInfo.WindowHeight;
/* Requested size might be different in fullscreen, so let's just get the area */
SDL.SDL_GetWindowSize(Handle, out var width, out var height);
Width = (uint) width;
Height = (uint) height;
idLookup.Add(SDL.SDL_GetWindowID(Handle), this);
}
/// <summary>
/// Changes the ScreenMode of this window.
/// </summary>
public void SetScreenMode(ScreenMode screenMode)
{
SDL.SDL_WindowFlags windowFlag = 0;
@ -73,13 +85,18 @@ namespace MoonWorks
windowFlag = SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
}
ScreenMode = screenMode;
SDL.SDL_SetWindowFullscreen(Handle, (uint) windowFlag);
if (screenMode == ScreenMode.Windowed)
{
SDL.SDL_SetWindowPosition(Handle, SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED);
}
ScreenMode = screenMode;
}
/// <summary>
/// Resizes the window.
/// Resizes the window. <br/>
/// Note that you are responsible for recreating any graphics resources that need to change as a result of the size change.
/// </summary>
/// <param name="width"></param>
@ -89,6 +106,11 @@ namespace MoonWorks
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);
}
}
internal static Window Lookup(uint windowID)
@ -112,6 +134,9 @@ namespace MoonWorks
}
}
/// <summary>
/// You can specify a method to run when the window size changes.
/// </summary>
public void RegisterSizeChangeCallback(System.Action<uint, uint> sizeChangeCallback)
{
SizeChangeCallback = sizeChangeCallback;

View File

@ -1,13 +1,37 @@
namespace MoonWorks
{
/// <summary>
/// All the information required for window creation.
/// </summary>
public struct WindowCreateInfo
{
/// <summary>
/// The name of the window that will be displayed in the operating system.
/// </summary>
public string WindowTitle;
/// <summary>
/// The width of the window.
/// </summary>
public uint WindowWidth;
/// <summary>
/// The height of the window.
/// </summary>
public uint WindowHeight;
/// <summary>
/// Specifies if the window will be created in windowed mode or a fullscreen mode.
/// </summary>
public ScreenMode ScreenMode;
/// <summary>
/// Specifies the presentation mode for the window. Roughly equivalent to V-Sync.
/// </summary>
public PresentMode PresentMode;
/// <summary>
/// Whether the window can be resized using the operating system's window dragging feature.
/// </summary>
public bool SystemResizable;
/// <summary>
/// Specifies if the window will open at the maximum desktop resolution.
/// </summary>
public bool StartMaximized;
public WindowCreateInfo(