forked from MoonsideGames/MoonWorks
				
			Compare commits
	
		
			178 Commits 
		
	
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | bb7e45b9a3 | |
|  | 4cedf768f7 | |
|  | d986b3013f | |
|  | 42e3ac91af | |
|  | 0df2944ccf | |
|  | e50fb472b1 | |
|  | df3f38a67b | |
|  | eaa9266521 | |
|  | 4dbd5a2cbe | |
|  | 2e890fd696 | |
|  | 385783a846 | |
|  | 450b08cbd8 | |
|  | 528fb7ac7c | |
|  | fcd08fe231 | |
|  | e961a18a83 | |
|  | 772a0378bb | |
|  | 40fb313d12 | |
|  | a736ed031d | |
|  | b2a0ca3515 | |
|  | 36a88afe52 | |
|  | 6c93350f7f | |
|  | 352bb34f82 | |
|  | de7d76c03d | |
|  | 18d92aeec8 | |
|  | e616b0fa62 | |
|  | 1d27a9e4a4 | |
|  | 514a0bed29 | |
|  | 78252d1f6c | |
|  | daae1a34b9 | |
|  | 2e5657789c | |
|  | 0c76c568a4 | |
|  | abdcac1608 | |
|  | d8064862bf | |
|  | b223c31c8b | |
|  | dd79090028 | |
|  | 653f90c29f | |
|  | 402c26131d | |
|  | b026b9e81f | |
|  | e0f05881b0 | |
|  | 7e18764942 | |
|  | 1bff459be6 | |
|  | be77e8bad1 | |
|  | 7f6b6a7bae | |
|  | 1de3c73bb7 | |
|  | e77c87c772 | |
|  | 088e7c4b6f | |
|  | f298a5ec11 | |
|  | 0cd2c799ee | |
|  | 81cd397013 | |
|  | e73c7ede55 | |
|  | 83f1cc24db | |
|  | 1d86d0c210 | |
|  | dbbd6540ab | |
|  | 74ae295036 | |
|  | 36ddb03d8f | |
|  | bf3ad0c8b0 | |
|  | f761d4f76e | |
|  | 4c731401ff | |
|  | 071518732e | |
|  | 1adb76d5c7 | |
|  | 5ff7da927a | |
|  | 0fd3365d1d | |
|  | affb592c15 | |
|  | 7e79e4a11d | |
|  | fc0937b2ff | |
|  | c83997609f | |
|  | b65d4e391c | |
|  | 56bab545ba | |
|  | 2ae116c72b | |
|  | bd3e70b096 | |
|  | b1fe7f96b2 | |
|  | 00366cc9d4 | |
|  | 3bc25bc3a1 | |
|  | 496eb670ab | |
|  | 00f4bfdeae | |
|  | adeba633e5 | |
|  | 300ef9f88e | |
|  | 76684eaa33 | |
|  | c037b4cb69 | |
|  | 5df08727c1 | |
|  | 537517afb9 | |
|  | bd405dfbf0 | |
|  | 2d7bb24b5c | |
|  | 0ea60a376b | |
|  | e3c2f0e119 | |
|  | 3584e670ee | |
|  | 5a2f7eadb8 | |
|  | 1e3f04235e | |
|  | dd06205399 | |
|  | 1cf04a7279 | |
|  | 3bd435746b | |
|  | 8134761e44 | |
|  | 8209051a3c | |
|  | 80f3711f4c | |
|  | 3a6b73e637 | |
|  | 12e7e6b9c1 | |
|  | 455f4048df | |
|  | 1f0e3b5040 | |
|  | f8b14ea94f | |
|  | 472da0edd2 | |
|  | bd825b6c91 | |
|  | 515c2ebbca | |
|  | 86322e9373 | |
|  | e9aacb44da | |
|  | 5baa1d7b40 | |
|  | f673803c37 | |
|  | 0f78cd1a0c | |
|  | 36ce74b58a | |
|  | 40d12357c0 | |
|  | e52fe60657 | |
|  | b39526ca90 | |
|  | 88d9119830 | |
|  | 05de9a4066 | |
|  | b1d30a9e6c | |
|  | 030745361b | |
|  | c43df10c2a | |
|  | 95981f0f03 | |
|  | 703b694bf6 | |
|  | fe561b61ef | |
|  | 230c1b41b4 | |
|  | 1fb3e5adf0 | |
|  | 6f8858c8b7 | |
|  | f96298f991 | |
|  | d76633bdfc | |
|  | debb76f62a | |
|  | ca61e94b13 | |
|  | 36e6c6f332 | |
|  | 916962da6c | |
|  | cfd52b00bd | |
|  | b0e1ad3cf8 | |
|  | 2c4e1b972a | |
|  | 751b8310ce | |
|  | 474b8fe37d | |
|  | bc41d2c079 | |
|  | d5ddd44bd3 | |
|  | de9d13757a | |
|  | ca9fb45536 | |
|  | faf81bceed | |
|  | fa992652b1 | |
|  | c4bae77408 | |
|  | ecf1a8ed55 | |
|  | 970517c3c2 | |
|  | f3c5bbf902 | |
|  | 02b20f79f5 | |
|  | c2ef78d136 | |
|  | 5533eeb2fd | |
|  | 934f3fd623 | |
|  | 02b0c12ad8 | |
|  | 1b38f8606b | |
|  | db38ada410 | |
|  | bd98ae8441 | |
|  | 59190e619d | |
|  | ea86212199 | |
|  | 66c6ceec04 | |
|  | dfddc24d0e | |
|  | b66e077376 | |
|  | 07c0b1b9a2 | |
|  | 3ffdf8a929 | |
|  | bace9f570d | |
|  | 32f80282a0 | |
|  | 23252a149f | |
|  | 80f19e4521 | |
|  | 7ea9a6aea3 | |
|  | d5a7daa524 | |
|  | 5c080a4c42 | |
|  | eb8c350a47 | |
|  | 0933b8e70f | |
|  | 1af16231ce | |
|  | 0c6bb538fb | |
|  | db66fbf115 | |
|  | f93be99545 | |
|  | ebfd4fd457 | |
|  | b380707462 | |
|  | 491eafac76 | |
|  | 0dddf2a0af | |
|  | 15aefc2212 | |
|  | a427b79510 | |
|  | 49f852a822 | 
|  | @ -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 | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
| 
 | ||||
| 	<PropertyGroup> | ||||
| 		<TargetFramework>net6.0</TargetFramework> | ||||
| 		<Platforms>x64</Platforms> | ||||
| 		<TargetFramework>net8.0</TargetFramework> | ||||
| 		<AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||
| 		<LangVersion>11</LangVersion> | ||||
| 	</PropertyGroup> | ||||
| 
 | ||||
| 	<PropertyGroup> | ||||
|  | @ -11,25 +11,32 @@ | |||
| 	</PropertyGroup> | ||||
| 
 | ||||
| 	<ItemGroup> | ||||
| 		<ProjectReference Include=".\lib\SDL2-CS\SDL2-CS.Core.csproj" /> | ||||
| 		<ProjectReference Include=".\lib\RefreshCS\RefreshCS.csproj" /> | ||||
| 		<ProjectReference Include=".\lib\FAudio\csharp\FAudio-CS.Core.csproj" /> | ||||
| 		<ProjectReference Include=".\lib\WellspringCS\WellspringCS.csproj" /> | ||||
| 		<ProjectReference Include=".\lib\Theorafile\csharp\Theorafile-CS.Core.csproj" /> | ||||
| 		<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\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\Video\Shaders\Compiled\FullscreenVert.spv"> | ||||
| 			<LogicalName>MoonWorks.Shaders.FullscreenVert.spv</LogicalName> | ||||
| 		<EmbeddedResource Include="src\Graphics\StockShaders\Binary\video_fullscreen.vert.refresh"> | ||||
| 			<LogicalName>MoonWorks.Graphics.StockShaders.VideoFullscreen.vert.refresh</LogicalName> | ||||
| 		</EmbeddedResource> | ||||
| 		<EmbeddedResource Include="src\Video\Shaders\Compiled\YUV2RGBAFrag.spv"> | ||||
| 			<LogicalName>MoonWorks.Shaders.YUV2RGBAFrag.spv</LogicalName> | ||||
| 		<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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -4,19 +4,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 | |||
| VisualStudioVersion = 16.0.30717.126 | ||||
| MinimumVisualStudioVersion = 15.0.26124.0 | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoonWorks", "MoonWorks.csproj", "{DDC9BA9B-4440-4CB3-BDB4-D5F91DE1686B}" | ||||
| 	ProjectSection(ProjectDependencies) = postProject | ||||
| 		{608AA31D-F163-4096-B4EF-B9C7D21D52BB} = {608AA31D-F163-4096-B4EF-B9C7D21D52BB} | ||||
| 	EndProjectSection | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SDL2-CS.Core", "lib\SDL2-CS\SDL2-CS.Core.csproj", "{0929F2D8-8FE4-4452-AD1E-50760A1A19A5}" | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FAudio-CS.Core", "lib\FAudio\csharp\FAudio-CS.Core.csproj", "{608AA31D-F163-4096-B4EF-B9C7D21D52BB}" | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RefreshCS", "lib\RefreshCS\RefreshCS.csproj", "{AD7C94E4-0AFA-44CA-889C-110142369893}" | ||||
| EndProject | ||||
| Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{69D3788D-6C57-44F7-A912-B201AE6D7C04}" | ||||
| EndProject | ||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WellspringCS", "lib\WellspringCS\WellspringCS.csproj", "{0DD7B866-773C-4A86-8580-F436DAA28989}" | ||||
| EndProject | ||||
| Global | ||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
|  | @ -28,22 +15,6 @@ Global | |||
| 		{DDC9BA9B-4440-4CB3-BDB4-D5F91DE1686B}.Debug|x64.Build.0 = Debug|x64 | ||||
| 		{DDC9BA9B-4440-4CB3-BDB4-D5F91DE1686B}.Release|x64.ActiveCfg = Release|x64 | ||||
| 		{DDC9BA9B-4440-4CB3-BDB4-D5F91DE1686B}.Release|x64.Build.0 = Release|x64 | ||||
| 		{0929F2D8-8FE4-4452-AD1E-50760A1A19A5}.Debug|x64.ActiveCfg = Debug|x64 | ||||
| 		{0929F2D8-8FE4-4452-AD1E-50760A1A19A5}.Debug|x64.Build.0 = Debug|x64 | ||||
| 		{0929F2D8-8FE4-4452-AD1E-50760A1A19A5}.Release|x64.ActiveCfg = Release|x64 | ||||
| 		{0929F2D8-8FE4-4452-AD1E-50760A1A19A5}.Release|x64.Build.0 = Release|x64 | ||||
| 		{608AA31D-F163-4096-B4EF-B9C7D21D52BB}.Debug|x64.ActiveCfg = Debug|x64 | ||||
| 		{608AA31D-F163-4096-B4EF-B9C7D21D52BB}.Debug|x64.Build.0 = Debug|x64 | ||||
| 		{608AA31D-F163-4096-B4EF-B9C7D21D52BB}.Release|x64.ActiveCfg = Release|x64 | ||||
| 		{608AA31D-F163-4096-B4EF-B9C7D21D52BB}.Release|x64.Build.0 = Release|x64 | ||||
| 		{AD7C94E4-0AFA-44CA-889C-110142369893}.Debug|x64.ActiveCfg = Debug|x64 | ||||
| 		{AD7C94E4-0AFA-44CA-889C-110142369893}.Debug|x64.Build.0 = Debug|x64 | ||||
| 		{AD7C94E4-0AFA-44CA-889C-110142369893}.Release|x64.ActiveCfg = Release|x64 | ||||
| 		{AD7C94E4-0AFA-44CA-889C-110142369893}.Release|x64.Build.0 = Release|x64 | ||||
| 		{0DD7B866-773C-4A86-8580-F436DAA28989}.Debug|x64.ActiveCfg = Debug|x64 | ||||
| 		{0DD7B866-773C-4A86-8580-F436DAA28989}.Debug|x64.Build.0 = Debug|x64 | ||||
| 		{0DD7B866-773C-4A86-8580-F436DAA28989}.Release|x64.ActiveCfg = Release|x64 | ||||
| 		{0DD7B866-773C-4A86-8580-F436DAA28989}.Release|x64.Build.0 = Release|x64 | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(SolutionProperties) = preSolution | ||||
| 		HideSolutionNode = FALSE | ||||
|  | @ -51,7 +22,4 @@ Global | |||
| 	GlobalSection(ExtensibilityGlobals) = postSolution | ||||
| 		SolutionGuid = {C3D68FAA-3165-43C7-95B3-D845F0DAA918} | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(NestedProjects) = preSolution | ||||
| 		{0DD7B866-773C-4A86-8580-F436DAA28989} = {69D3788D-6C57-44F7-A912-B201AE6D7C04} | ||||
| 	EndGlobalSection | ||||
| EndGlobal | ||||
|  |  | |||
							
								
								
									
										12
									
								
								README.md
								
								
								
								
							
							
						
						
									
										12
									
								
								README.md
								
								
								
								
							|  | @ -12,9 +12,13 @@ MoonWorks uses strictly Free Open Source Software. It will never have any kind o | |||
| 
 | ||||
| ## Documentation | ||||
| 
 | ||||
| High-level documentation is provided here: http://moonside.games/docs/moonworks/ | ||||
| API Reference: https://moonside.games/docs/moonworksapi/ | ||||
| 
 | ||||
| For an actual API reference, the source is documented in doc comments that your preferred IDE can read. | ||||
| High-level documentation is provided here: https://moonside.games/docs/moonworks/ | ||||
| 
 | ||||
| The source is documented in doc comments that your preferred IDE can read. | ||||
| 
 | ||||
| Join our Discord! https://discord.gg/ujhwdkHmhN | ||||
| 
 | ||||
| ## Dependencies | ||||
| 
 | ||||
|  | @ -22,9 +26,9 @@ 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: http://moonside.games/files/moonlibs.tar.bz2 | ||||
| Prebuilt dependencies can be obtained here: https://moonside.games/files/moonlibs.tar.bz2 | ||||
| 
 | ||||
| ## License | ||||
| 
 | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| Subproject commit 0b6d5dabbf428633482fe3a956fbdb53228fcf35 | ||||
| Subproject commit 60480416bda930bf7544e6abe31b937f0daa0256 | ||||
|  | @ -1 +1 @@ | |||
| Subproject commit 98c590ae77c3b6a64a370bac439f20728959a8b6 | ||||
| Subproject commit b5325e6d0329eeb35b074091a569a5f679852d28 | ||||
|  | @ -1 +1 @@ | |||
| Subproject commit b35aaa494e44d08242788ff0ba2cb7a508f4d8f0 | ||||
| Subproject commit e4afbb848586fca530b6538320f799f81a18b941 | ||||
|  | @ -1 +0,0 @@ | |||
| Subproject commit dd8c7fa69e678b6182cdaa71458ad08dd31c65da | ||||
|  | @ -1 +1 @@ | |||
| Subproject commit f8872bae59e394b0f8a35224bb39ab8fd041af97 | ||||
| Subproject commit 074f2afc833b221906bb2468735041ce78f2cb89 | ||||
|  | @ -0,0 +1 @@ | |||
| Subproject commit 5065e2cd4662dbe023b77a45ef967f975170dfff | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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 | ||||
| 			); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,31 +1,54 @@ | |||
| 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; | ||||
| 
 | ||||
| 		internal FAudio.FAudioVoiceSends ReverbSends; | ||||
| 		private readonly HashSet<GCHandle> resourceHandles = new HashSet<GCHandle>(); | ||||
| 		private readonly HashSet<UpdatingSourceVoice> updatingSourceVoices = new HashSet<UpdatingSourceVoice>(); | ||||
| 
 | ||||
| 		private readonly List<WeakReference<AudioResource>> resources = new List<WeakReference<AudioResource>>(); | ||||
| 		private readonly List<WeakReference<StreamingSound>> streamingSounds = new List<WeakReference<StreamingSound>>(); | ||||
| 		private AudioTweenManager AudioTweenManager; | ||||
| 
 | ||||
| 		private bool IsDisposed; | ||||
| 		private SourceVoicePool VoicePool; | ||||
| 		private List<SourceVoice> VoicesToReturn = new List<SourceVoice>(); | ||||
| 
 | ||||
| 		public unsafe AudioDevice() | ||||
| 		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() | ||||
| 		{ | ||||
| 			FAudio.FAudioCreate(out var handle, 0, 0); | ||||
| 			UpdateInterval = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / Step); | ||||
| 
 | ||||
| 			FAudio.FAudioCreate(out var handle, 0, FAudio.FAUDIO_DEFAULT_PROCESSOR); | ||||
| 			Handle = handle; | ||||
| 
 | ||||
| 			/* Find a suitable device */ | ||||
|  | @ -35,8 +58,8 @@ namespace MoonWorks.Audio | |||
| 			if (devices == 0) | ||||
| 			{ | ||||
| 				Logger.LogError("No audio devices found!"); | ||||
| 				Handle = IntPtr.Zero; | ||||
| 				FAudio.FAudio_Release(Handle); | ||||
| 				Handle = IntPtr.Zero; | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
|  | @ -69,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 */ | ||||
| 
 | ||||
|  | @ -98,144 +120,169 @@ 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( | ||||
| 				sizeof(FAudio.FAudioEffectChain) | ||||
| 			); | ||||
| 			Running = true; | ||||
| 
 | ||||
| 			FAudio.FAudioEffectChain* reverbChain = (FAudio.FAudioEffectChain*) chainPtr; | ||||
| 			reverbChain->EffectCount = 1; | ||||
| 			reverbChain->pEffectDescriptors = Marshal.AllocHGlobal( | ||||
| 				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( | ||||
| 				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) sizeof(FAudio.FAudioFXReverbParameters), | ||||
| 				0 | ||||
| 			); | ||||
| 			Marshal.FreeHGlobal(reverbParamsPtr); | ||||
| 
 | ||||
| 			/* Init reverb sends */ | ||||
| 
 | ||||
| 			ReverbSends = new FAudio.FAudioVoiceSends | ||||
| 			{ | ||||
| 				SendCount = 2, | ||||
| 				pSends = Marshal.AllocHGlobal( | ||||
| 					2 * 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; | ||||
| 			TickStopwatch.Start(); | ||||
| 			previousTickTime = 0; | ||||
| 		} | ||||
| 
 | ||||
| 		public void SetMasteringVolume(float volume) | ||||
| 		private void ThreadMain() | ||||
| 		{ | ||||
| 			FAudio.FAudioVoice_SetVolume(MasteringVoice, volume, 0); | ||||
| 		} | ||||
| 
 | ||||
| 		internal void Update() | ||||
| 		{ | ||||
| 			for (var i = streamingSounds.Count - 1; i >= 0; i--) | ||||
| 			while (Running) | ||||
| 			{ | ||||
| 				var weakReference = streamingSounds[i]; | ||||
| 				if (weakReference.TryGetTarget(out var streamingSound)) | ||||
| 				lock (StateLock) | ||||
| 				{ | ||||
| 					streamingSound.Update(); | ||||
| 					try | ||||
| 					{ | ||||
| 						ThreadMainTick(); | ||||
| 					} | ||||
| 					catch (Exception e) | ||||
| 					{ | ||||
| 						Logger.LogError(e.ToString()); | ||||
| 					} | ||||
| 				} | ||||
| 				else | ||||
| 
 | ||||
| 				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) | ||||
| 				{ | ||||
| 					streamingSounds.RemoveAt(i); | ||||
| 					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 | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		internal void ClearTweens( | ||||
| 			Voice voice, | ||||
| 			AudioTweenProperty property | ||||
| 		) { | ||||
| 			lock (StateLock) | ||||
| 			{ | ||||
| 				AudioTweenManager.ClearTweens(voice, property); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		internal void WakeThread() | ||||
| 		{ | ||||
| 			WakeSignal.Set(); | ||||
| 		} | ||||
| 
 | ||||
| 		internal void AddResourceReference(GCHandle resourceReference) | ||||
| 		{ | ||||
| 			lock (StateLock) | ||||
| 			{ | ||||
| 				resourceHandles.Add(resourceReference); | ||||
| 
 | ||||
| 				if (resourceReference.Target is UpdatingSourceVoice updatableVoice) | ||||
| 				{ | ||||
| 					updatingSourceVoices.Add(updatableVoice); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		internal void AddDynamicSoundInstance(StreamingSound instance) | ||||
| 		internal void RemoveResourceReference(GCHandle resourceReference) | ||||
| 		{ | ||||
| 			streamingSounds.Add(new WeakReference<StreamingSound>(instance)); | ||||
| 		} | ||||
| 
 | ||||
| 		internal void AddResourceReference(WeakReference<AudioResource> resourceReference) | ||||
| 		{ | ||||
| 			lock (resources) | ||||
| 			lock (StateLock) | ||||
| 			{ | ||||
| 				resources.Add(resourceReference); | ||||
| 			} | ||||
| 		} | ||||
| 				resourceHandles.Remove(resourceReference); | ||||
| 
 | ||||
| 		internal void RemoveResourceReference(WeakReference<AudioResource> resourceReference) | ||||
| 		{ | ||||
| 			lock (resources) | ||||
| 			{ | ||||
| 				resources.Remove(resourceReference); | ||||
| 				if (resourceReference.Target is UpdatingSourceVoice updatableVoice) | ||||
| 				{ | ||||
| 					updatingSourceVoices.Remove(updatableVoice); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  | @ -243,29 +290,54 @@ namespace MoonWorks.Audio | |||
| 		{ | ||||
| 			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 | ||||
|  |  | |||
|  | @ -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() { } | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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() { } | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,10 @@ | |||
| namespace MoonWorks.Audio | ||||
| { | ||||
| 	public enum FilterType | ||||
| 	{ | ||||
| 		None, | ||||
| 		LowPass, | ||||
| 		BandPass, | ||||
| 		HighPass | ||||
| 	} | ||||
| } | ||||
|  | @ -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 | ||||
| 			}; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,7 @@ | |||
| namespace MoonWorks.Audio | ||||
| { | ||||
| 	public interface IPoolable<T> | ||||
| 	{ | ||||
| 		static abstract T Create(AudioDevice device, Format format); | ||||
| 	} | ||||
| } | ||||
|  | @ -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)); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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 | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,350 +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 = 1; | ||||
| 		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 float _lowPassFilter; | ||||
| 		public float LowPassFilter | ||||
| 		{ | ||||
| 			get => _lowPassFilter; | ||||
| 			set | ||||
| 			{ | ||||
| 				_lowPassFilter = value; | ||||
| 
 | ||||
| 				FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters | ||||
| 				{ | ||||
| 					Type = FAudio.FAudioFilterType.FAudioLowPassFilter, | ||||
| 					Frequency = _lowPassFilter, | ||||
| 					OneOverQ = 1f | ||||
| 				}; | ||||
| 				FAudio.FAudioVoice_SetFilterParameters( | ||||
| 					Handle, | ||||
| 					ref p, | ||||
| 					0 | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		private float _highPassFilter; | ||||
| 		public float HighPassFilter | ||||
| 		{ | ||||
| 			get => _highPassFilter; | ||||
| 			set | ||||
| 			{ | ||||
| 				_highPassFilter = value; | ||||
| 
 | ||||
| 				FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters | ||||
| 				{ | ||||
| 					Type = FAudio.FAudioFilterType.FAudioHighPassFilter, | ||||
| 					Frequency = _highPassFilter, | ||||
| 					OneOverQ = 1f | ||||
| 				}; | ||||
| 				FAudio.FAudioVoice_SetFilterParameters( | ||||
| 					Handle, | ||||
| 					ref p, | ||||
| 					0 | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		private float _bandPassFilter; | ||||
| 		public float BandPassFilter | ||||
| 		{ | ||||
| 			get => _bandPassFilter; | ||||
| 			set | ||||
| 			{ | ||||
| 				_bandPassFilter = value; | ||||
| 
 | ||||
| 				FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters | ||||
| 				{ | ||||
| 					Type = FAudio.FAudioFilterType.FAudioBandPassFilter, | ||||
| 					Frequency = _bandPassFilter, | ||||
| 					OneOverQ = 1f | ||||
| 				}; | ||||
| 				FAudio.FAudioVoice_SetFilterParameters( | ||||
| 					Handle, | ||||
| 					ref p, | ||||
| 					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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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()); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,294 +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) | ||||
| 		{ | ||||
| 			Instances.Push(instance); | ||||
| 		} | ||||
| 
 | ||||
| 		protected override void Destroy() | ||||
| 		{ | ||||
| 			Marshal.FreeHGlobal(Handle.pAudioData); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,111 +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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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]); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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(); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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(); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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(); | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
|         } | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -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; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,252 +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>>(); | ||||
| 		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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
|         } | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -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; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,252 +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>>(); | ||||
| 		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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,45 @@ | |||
| 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> | ||||
| 		{ | ||||
| 			{ VertexElementFormat.Byte4, (uint) Marshal.SizeOf<Byte4>() }, | ||||
| 			{ VertexElementFormat.Color, (uint) Marshal.SizeOf<Color>() }, | ||||
| 			{ VertexElementFormat.Float, (uint) Marshal.SizeOf<float>() }, | ||||
| 			{ VertexElementFormat.HalfVector2, (uint) Marshal.SizeOf<HalfVector2>() }, | ||||
| 			{ VertexElementFormat.HalfVector4, (uint) Marshal.SizeOf<HalfVector4>() }, | ||||
| 			{ VertexElementFormat.NormalizedShort2, (uint) Marshal.SizeOf<NormalizedShort2>() }, | ||||
| 			{ VertexElementFormat.NormalizedShort4, (uint) Marshal.SizeOf<NormalizedShort4>() }, | ||||
| 			{ VertexElementFormat.Short2, (uint) Marshal.SizeOf<Short2>() }, | ||||
| 			{ VertexElementFormat.Short4, (uint) Marshal.SizeOf<Short4>() }, | ||||
| 			{ VertexElementFormat.UInt, (uint) Marshal.SizeOf<uint>() }, | ||||
| 			{ VertexElementFormat.Vector2, (uint) Marshal.SizeOf<Math.Float.Vector2>() }, | ||||
| 			{ VertexElementFormat.Vector3, (uint) Marshal.SizeOf<Math.Float.Vector3>() }, | ||||
| 			{ VertexElementFormat.Vector4, (uint) Marshal.SizeOf<Math.Float.Vector4>() } | ||||
| 		}; | ||||
| 
 | ||||
| 		public static byte BoolToByte(bool b) | ||||
| 		{ | ||||
| 			return (byte) (b ? 1 : 0); | ||||
| 		} | ||||
| 
 | ||||
| 		public static bool ByteToBool(byte b) | ||||
| 		{ | ||||
| 			return b != 0; | ||||
| 		} | ||||
| 
 | ||||
| 		public static uint VertexElementFormatSize(VertexElementFormat format) | ||||
| 		{ | ||||
| 			return Sizes[format]; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,12 +0,0 @@ | |||
| using System; | ||||
| 
 | ||||
| namespace MoonWorks | ||||
| { | ||||
| 	public class AudioLoadException : Exception | ||||
| 	{ | ||||
| 		public AudioLoadException(string message) : base(message) | ||||
| 		{ | ||||
| 
 | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,36 @@ | |||
| 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( | ||||
| 			FrameLimiterMode mode, | ||||
| 			int cap | ||||
| 		) { | ||||
| 			Mode = mode; | ||||
| 			Cap = cap; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,14 +0,0 @@ | |||
| namespace MoonWorks | ||||
| { | ||||
| 	public enum FramerateMode | ||||
| 	{ | ||||
| 		Uncapped, | ||||
| 		Capped | ||||
| 	} | ||||
| 
 | ||||
| 	public struct FramerateSettings | ||||
| 	{ | ||||
| 		public FramerateMode Mode; | ||||
| 		public int Cap; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										159
									
								
								src/Game.cs
								
								
								
								
							
							
						
						
									
										159
									
								
								src/Game.cs
								
								
								
								
							|  | @ -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); | ||||
|  | @ -29,91 +34,155 @@ namespace MoonWorks | |||
| 		private bool FramerateCapped = false; | ||||
| 		private TimeSpan FramerateCapTimeSpan = TimeSpan.Zero; | ||||
| 
 | ||||
| 		public Window Window { get; } | ||||
| 		public GraphicsDevice GraphicsDevice { get; } | ||||
| 		public AudioDevice AudioDevice { get; } | ||||
| 		public Inputs Inputs { get; } | ||||
| 
 | ||||
| 		private Dictionary<PresentMode, RefreshCS.Refresh.PresentMode> moonWorksToRefreshPresentMode = new Dictionary<PresentMode, RefreshCS.Refresh.PresentMode> | ||||
| 		{ | ||||
| 			{ PresentMode.Immediate, RefreshCS.Refresh.PresentMode.Immediate }, | ||||
| 			{ PresentMode.Mailbox, RefreshCS.Refresh.PresentMode.Mailbox }, | ||||
| 			{ PresentMode.FIFO, RefreshCS.Refresh.PresentMode.FIFO }, | ||||
| 			{ PresentMode.FIFORelaxed, RefreshCS.Refresh.PresentMode.FIFORelaxed } | ||||
| 		}; | ||||
| 		/// <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, | ||||
| 			PresentMode presentMode, | ||||
| 			FramerateSettings framerateSettings, | ||||
| 			FrameLimiterSettings frameLimiterSettings, | ||||
| 			int targetTimestep = 60, | ||||
| 			bool debugMode = false | ||||
| 		) | ||||
| 		{ | ||||
| 			Logger.LogInfo("Initializing frame limiter..."); | ||||
| 			Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / targetTimestep); | ||||
| 			gameTimer = Stopwatch.StartNew(); | ||||
| 
 | ||||
| 			FramerateCapped = framerateSettings.Mode == FramerateMode.Capped; | ||||
| 
 | ||||
| 			if (FramerateCapped) | ||||
| 			{ | ||||
| 				FramerateCapTimeSpan = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / framerateSettings.Cap); | ||||
| 			} | ||||
| 			SetFrameLimiter(frameLimiterSettings); | ||||
| 
 | ||||
| 			for (int i = 0; i < previousSleepTimes.Length; i += 1) | ||||
| 			{ | ||||
| 				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(); | ||||
| 
 | ||||
| 			Window = new Window(windowCreateInfo); | ||||
| 
 | ||||
| 			Logger.LogInfo("Initializing graphics device..."); | ||||
| 			GraphicsDevice = new GraphicsDevice( | ||||
| 				Window.Handle, | ||||
| 				moonWorksToRefreshPresentMode[presentMode], | ||||
| 				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)) | ||||
| 			{ | ||||
| 				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(); | ||||
| 
 | ||||
| 			while (!quit) | ||||
| 			{ | ||||
| 				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(); | ||||
| 			Window.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; | ||||
| 
 | ||||
| 			if (FramerateCapped) | ||||
| 			{ | ||||
| 				FramerateCapTimeSpan = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / settings.Cap); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				FramerateCapTimeSpan = TimeSpan.Zero; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		/// <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() {} | ||||
| 
 | ||||
|  | @ -161,9 +230,8 @@ namespace MoonWorks | |||
| 				while (accumulatedUpdateTime >= Timestep) | ||||
| 				{ | ||||
| 					Inputs.Update(); | ||||
| 					AudioDevice.Update(); | ||||
| 
 | ||||
| 					Update(Timestep); | ||||
| 					AudioDevice.WakeThread(); | ||||
| 
 | ||||
| 					accumulatedUpdateTime -= Timestep; | ||||
| 				} | ||||
|  | @ -190,7 +258,7 @@ namespace MoonWorks | |||
| 						break; | ||||
| 
 | ||||
| 					case SDL.SDL_EventType.SDL_MOUSEWHEEL: | ||||
| 						Inputs.Mouse.Wheel += _event.wheel.y; | ||||
| 						Inputs.Mouse.WheelRaw += _event.wheel.y; | ||||
| 						break; | ||||
| 
 | ||||
| 					case SDL.SDL_EventType.SDL_DROPBEGIN: | ||||
|  | @ -224,7 +292,14 @@ namespace MoonWorks | |||
| 		{ | ||||
| 			if (evt.window.windowEvent == SDL.SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED) | ||||
| 			{ | ||||
| 				Window.SizeChanged((uint) evt.window.data1, (uint) evt.window.data2); | ||||
| 				var window = Window.Lookup(evt.window.windowID); | ||||
| 				window.HandleSizeChange((uint) evt.window.data1, (uint) evt.window.data2); | ||||
| 			} | ||||
| 			else if (evt.window.windowEvent == SDL.SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE) | ||||
| 			{ | ||||
| 				var window = Window.Lookup(evt.window.windowID); | ||||
| 				GraphicsDevice.UnclaimWindow(window); | ||||
| 				window.Dispose(); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  | @ -268,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; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -1,22 +1,17 @@ | |||
| using System; | ||||
| 
 | ||||
| namespace MoonWorks.Graphics | ||||
| namespace MoonWorks.Graphics | ||||
| { | ||||
| 	/// <summary> | ||||
| 	/// A texture-sampler pair to be used when binding samplers. | ||||
| 	/// </summary> | ||||
| 	public struct TextureSamplerBinding | ||||
| 	{ | ||||
| 		public IntPtr TextureHandle; | ||||
| 		public IntPtr SamplerHandle; | ||||
| 		public Texture Texture; | ||||
| 		public Sampler Sampler; | ||||
| 
 | ||||
| 		public TextureSamplerBinding(Texture texture, Sampler sampler) | ||||
| 		{ | ||||
| 			TextureHandle = texture.Handle; | ||||
| 			SamplerHandle = sampler.Handle; | ||||
| 		} | ||||
| 
 | ||||
| 		public TextureSamplerBinding(IntPtr textureHandle, IntPtr samplerHandle) | ||||
| 		{ | ||||
| 			TextureHandle = textureHandle; | ||||
| 			SamplerHandle = samplerHandle; | ||||
| 			Texture = texture; | ||||
| 			Sampler = sampler; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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
											
										
									
								
							|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,44 +1,121 @@ | |||
| using System; | ||||
| using System.IO; | ||||
| using System.Runtime.InteropServices; | ||||
| using WellspringCS; | ||||
| 
 | ||||
| namespace MoonWorks.Graphics.Font | ||||
| { | ||||
|     public class Font : IDisposable | ||||
|     { | ||||
| 		public IntPtr Handle { get; } | ||||
| 	public unsafe class Font : GraphicsResource | ||||
| 	{ | ||||
| 		public Texture Texture { get; } | ||||
| 		public float PixelsPerEm { get; } | ||||
| 		public float DistanceRange { get; } | ||||
| 
 | ||||
| 		private bool IsDisposed; | ||||
| 		internal IntPtr Handle { get; } | ||||
| 
 | ||||
|         public unsafe Font(string path) | ||||
|         { | ||||
|             var bytes = File.ReadAllBytes(path); | ||||
| 			fixed (byte* pByte = &bytes[0]) | ||||
| 		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) | ||||
| 		{ | ||||
| 			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); | ||||
| 			} | ||||
|         } | ||||
| 
 | ||||
| 		protected virtual void Dispose(bool disposing) | ||||
| 			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; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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 * sizeof(Wellspring.FontRange); | ||||
| 				void* fontRangeMemory = NativeMemory.Alloc((nuint) fontRanges.Length, (nuint) 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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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 | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			return true; | ||||
| 		} | ||||
| 
 | ||||
| 		// Call this after you have made all the Draw calls you want. | ||||
| 		// 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,30 +93,59 @@ 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); | ||||
| 			} | ||||
| 
 | ||||
| 			commandBuffer.SetBufferData(VertexBuffer, vertexDataPointer, 0, vertexDataLengthInBytes); | ||||
| 			commandBuffer.SetBufferData(IndexBuffer, indexDataPointer, 0, indexDataLengthInBytes); | ||||
| 			if (vertexDataLengthInBytes > 0 && indexDataLengthInBytes > 0) | ||||
| 			{ | ||||
| 				commandBuffer.SetBufferData(VertexBuffer, vertexDataPointer, 0, vertexDataLengthInBytes); | ||||
| 				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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -1,93 +1,423 @@ | |||
| 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; } | ||||
| 		public Backend Backend { get; } | ||||
| 
 | ||||
| 		private uint windowFlags; | ||||
| 		public SDL2.SDL.SDL_WindowFlags WindowFlags => (SDL2.SDL.SDL_WindowFlags) windowFlags; | ||||
| 
 | ||||
| 		// Built-in video pipeline | ||||
| 		private ShaderModule VideoVertexShader { get; } | ||||
| 		private ShaderModule VideoFragmentShader { get; } | ||||
| 		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( | ||||
| 			IntPtr deviceWindowHandle, | ||||
| 			Refresh.PresentMode presentMode, | ||||
| 		internal GraphicsDevice( | ||||
| 			Backend preferredBackend, | ||||
| 			bool debugMode | ||||
| 		) | ||||
| 		{ | ||||
| 			var presentationParameters = new Refresh.PresentationParameters | ||||
| 		) { | ||||
| 			Backend = (Backend) Refresh.Refresh_SelectBackend((Refresh.Backend) preferredBackend, out windowFlags); | ||||
| 
 | ||||
| 			if (Backend == Backend.Invalid) | ||||
| 			{ | ||||
| 				deviceWindowHandle = deviceWindowHandle, | ||||
| 				presentMode = presentMode | ||||
| 			}; | ||||
| 				throw new System.Exception("Could not set graphics backend!"); | ||||
| 			} | ||||
| 
 | ||||
| 			Handle = Refresh.Refresh_CreateDevice( | ||||
| 				presentationParameters, | ||||
| 				Conversions.BoolToByte(debugMode) | ||||
| 			); | ||||
| 
 | ||||
| 			VideoVertexShader = new ShaderModule(this, GetEmbeddedResource("MoonWorks.Shaders.FullscreenVert.spv")); | ||||
| 			VideoFragmentShader = new ShaderModule(this, GetEmbeddedResource("MoonWorks.Shaders.YUV2RGBAFrag.spv")); | ||||
| 			// 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)) | ||||
| 			{ | ||||
| 				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, | ||||
| 				new GraphicsPipelineCreateInfo | ||||
| 				{ | ||||
| 					AttachmentInfo = new GraphicsPipelineAttachmentInfo( | ||||
| 						new ColorAttachmentDescription(TextureFormat.R8G8B8A8, ColorAttachmentBlendState.None) | ||||
| 						new ColorAttachmentDescription( | ||||
| 							TextureFormat.R8G8B8A8, | ||||
| 							ColorAttachmentBlendState.None | ||||
| 						) | ||||
| 					), | ||||
| 					DepthStencilState = DepthStencilState.Disable, | ||||
| 					VertexShaderInfo = GraphicsShaderInfo.Create(VideoVertexShader, "main", 0), | ||||
| 					FragmentShaderInfo = GraphicsShaderInfo.Create(VideoFragmentShader, "main", 3), | ||||
| 					VertexShaderInfo = GraphicsShaderInfo.Create( | ||||
| 						videoVertShader, | ||||
| 						"main", | ||||
| 						0 | ||||
| 					), | ||||
| 					FragmentShaderInfo = GraphicsShaderInfo.Create( | ||||
| 						videoFragShader, | ||||
| 						"main", | ||||
| 						3 | ||||
| 					), | ||||
| 					VertexInputState = VertexInputState.Empty, | ||||
| 					RasterizerState = RasterizerState.CCW_CullNone, | ||||
| 					PrimitiveType = PrimitiveType.TriangleList, | ||||
| 					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); | ||||
| 		} | ||||
| 
 | ||||
| 		public CommandBuffer AcquireCommandBuffer() | ||||
| 		/// <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) | ||||
| 		{ | ||||
| 			return new CommandBuffer(this, Refresh.Refresh_AcquireCommandBuffer(Handle, 0)); | ||||
| 		} | ||||
| 
 | ||||
| 		public unsafe void Submit(params CommandBuffer[] commandBuffers) | ||||
| 		{ | ||||
| 			var commandBufferPtrs = stackalloc IntPtr[commandBuffers.Length]; | ||||
| 
 | ||||
| 			for (var i = 0; i < commandBuffers.Length; i += 1) | ||||
| 			if (window.Claimed) | ||||
| 			{ | ||||
| 				commandBufferPtrs[i] = commandBuffers[i].Handle; | ||||
| 				Logger.LogError("Window already claimed!"); | ||||
| 				return false; | ||||
| 			} | ||||
| 
 | ||||
| 			Refresh.Refresh_Submit( | ||||
| 			var success = Conversions.ByteToBool( | ||||
| 				Refresh.Refresh_ClaimWindow( | ||||
| 					Handle, | ||||
| 					window.Handle, | ||||
| 					(Refresh.PresentMode) presentMode | ||||
| 				) | ||||
| 			); | ||||
| 
 | ||||
| 			if (success) | ||||
| 			{ | ||||
| 				window.Claimed = true; | ||||
| 				window.SwapchainFormat = GetSwapchainFormat(window); | ||||
| 				if (window.SwapchainTexture == null) | ||||
| 				{ | ||||
| 					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, | ||||
| 				(uint) commandBuffers.Length, | ||||
| 				(IntPtr) commandBufferPtrs | ||||
| 				window.Handle, | ||||
| 				(Refresh.PresentMode) presentMode | ||||
| 			); | ||||
| 		} | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// Acquires a command buffer. | ||||
| 		/// This is the start of your rendering process. | ||||
| 		/// </summary> | ||||
| 		/// <returns></returns> | ||||
| 		public CommandBuffer AcquireCommandBuffer() | ||||
| 		{ | ||||
| 			var commandBuffer = CommandBufferPool.Obtain(); | ||||
| 			commandBuffer.SetHandle(Refresh.Refresh_AcquireCommandBuffer(Handle)); | ||||
| #if DEBUG | ||||
| 			commandBuffer.ResetStateTracking(); | ||||
| #endif | ||||
| 			return commandBuffer; | ||||
| 		} | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// Submits a command buffer to the GPU for processing. | ||||
| 		/// </summary> | ||||
| 		public void Submit(CommandBuffer commandBuffer) | ||||
| 		{ | ||||
| #if DEBUG | ||||
| 			if (commandBuffer.Submitted) | ||||
| 			{ | ||||
| 				throw new System.InvalidOperationException("Command buffer already submitted!"); | ||||
| 			} | ||||
| #endif | ||||
| 
 | ||||
| 			Refresh.Refresh_Submit( | ||||
| 				Handle, | ||||
| 				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); | ||||
| 		} | ||||
| 
 | ||||
| 		public TextureFormat GetSwapchainFormat(Window window) | ||||
| 		/// <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) | ||||
| 			{ | ||||
|  | @ -95,7 +425,7 @@ namespace MoonWorks.Graphics | |||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		internal void RemoveResourceReference(WeakReference<GraphicsResource> resourceReference) | ||||
| 		internal void RemoveResourceReference(GCHandle resourceReference) | ||||
| 		{ | ||||
| 			lock (resources) | ||||
| 			{ | ||||
|  | @ -103,11 +433,6 @@ namespace MoonWorks.Graphics | |||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		private static Stream GetEmbeddedResource(string name) | ||||
| 		{ | ||||
| 			return typeof(GraphicsDevice).Assembly.GetManifestResourceStream(name); | ||||
| 		} | ||||
| 
 | ||||
| 		protected virtual void Dispose(bool disposing) | ||||
| 		{ | ||||
| 			if (!IsDisposed) | ||||
|  | @ -116,20 +441,29 @@ 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); | ||||
| 				} | ||||
| 
 | ||||
| 				Refresh.Refresh_DestroyDevice(Handle); | ||||
| 
 | ||||
| 				IsDisposed = true; | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -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; protected 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() | ||||
|  |  | |||
|  | @ -0,0 +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; } | ||||
| 	} | ||||
| } | ||||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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 | ||||
| 	{ | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ using System; | |||
| using System.Runtime.InteropServices; | ||||
| #endregion | ||||
| 
 | ||||
| namespace MoonWorks.Graphics | ||||
| namespace MoonWorks.Graphics.PackedVector | ||||
| { | ||||
| 	internal static class HalfTypeHelper | ||||
| 	{ | ||||
|  |  | |||
|  | @ -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> | ||||
| 	{ | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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> | ||||
| 	{ | ||||
|  |  | |||
|  | @ -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> | ||||
| 	{ | ||||
|  |  | |||
|  | @ -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> | ||||
| 	{ | ||||
|  |  | |||
|  | @ -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> | ||||
| 	{ | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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> | ||||
| 	{ | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -1,18 +1,41 @@ | |||
| using System; | ||||
| 
 | ||||
| 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 | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| /* Recreate all the enums in here so we don't need to explicitly | ||||
|  * reference the RefreshCS namespace when using MoonWorks.Graphics | ||||
|  */ | ||||
| namespace MoonWorks.Graphics | ||||
| { | ||||
| 	public enum PresentMode | ||||
| 	{ | ||||
| 		Immediate, | ||||
| 		Mailbox, | ||||
| 		FIFO, | ||||
| 		FIFORelaxed | ||||
| 	} | ||||
| 
 | ||||
| 	public enum PrimitiveType | ||||
| 	{ | ||||
| 		PointList, | ||||
|  | @ -64,22 +87,29 @@ namespace MoonWorks.Graphics | |||
| 		R5G6B5, | ||||
| 		A1R5G5B5, | ||||
| 		B4G4R4A4, | ||||
| 		A2R10G10B10, | ||||
| 		R16G16, | ||||
| 		R16G16B16A16, | ||||
| 		R8, | ||||
| 		BC1, | ||||
| 		BC2, | ||||
| 		BC3, | ||||
| 		BC7, | ||||
| 		R8G8_SNORM, | ||||
| 		R8G8B8A8_SNORM, | ||||
| 		A2R10G10B10, | ||||
| 		R16G16, | ||||
| 		R16G16B16A16, | ||||
| 		R8, | ||||
| 		R32_SFLOAT, | ||||
| 		R32G32_SFLOAT, | ||||
| 		R32G32B32A32_SFLOAT, | ||||
| 		R16_SFLOAT, | ||||
| 		R16G16_SFLOAT, | ||||
| 		R16G16B16A16_SFLOAT, | ||||
| 		R32_SFLOAT, | ||||
| 		R32G32_SFLOAT, | ||||
| 		R32G32B32A32_SFLOAT, | ||||
| 
 | ||||
| 		R8_UINT, | ||||
| 		R8G8_UINT, | ||||
| 		R8G8B8A8_UINT, | ||||
| 		R16_UINT, | ||||
| 		R16G16_UINT, | ||||
| 		R16G16B16A16_UINT, | ||||
| 		D16, | ||||
| 		D32, | ||||
| 		D16S8, | ||||
|  | @ -91,7 +121,8 @@ namespace MoonWorks.Graphics | |||
| 	{ | ||||
| 		Sampler = 1, | ||||
| 		ColorTarget = 2, | ||||
| 		DepthStencilTarget = 4 | ||||
| 		DepthStencilTarget = 4, | ||||
| 		Compute = 8 | ||||
| 	} | ||||
| 
 | ||||
| 	public enum SampleCount | ||||
|  | @ -99,10 +130,7 @@ namespace MoonWorks.Graphics | |||
| 		One, | ||||
| 		Two, | ||||
| 		Four, | ||||
| 		Eight, | ||||
| 		Sixteen, | ||||
| 		ThirtyTwo, | ||||
| 		SixtyFour | ||||
| 		Eight | ||||
| 	} | ||||
| 
 | ||||
| 	public enum CubeMapFace : uint | ||||
|  | @ -120,7 +148,8 @@ namespace MoonWorks.Graphics | |||
| 	{ | ||||
| 		Vertex = 1, | ||||
| 		Index = 2, | ||||
| 		Compute = 4 | ||||
| 		Compute = 4, | ||||
| 		Indirect = 8 | ||||
| 	} | ||||
| 
 | ||||
| 	public enum VertexElementFormat | ||||
|  | @ -212,11 +241,7 @@ namespace MoonWorks.Graphics | |||
| 		OneMinusDestinationAlpha, | ||||
| 		ConstantColor, | ||||
| 		OneMinusConstantColor, | ||||
| 		SourceAlphaSaturate, | ||||
| 		SourceOneColor, | ||||
| 		OneMinusSourceOneColor, | ||||
| 		SourceOneAlpha, | ||||
| 		OneMinusSourceOneAlpha | ||||
| 		SourceAlphaSaturate | ||||
| 	} | ||||
| 
 | ||||
| 	[Flags] | ||||
|  | @ -271,4 +296,12 @@ namespace MoonWorks.Graphics | |||
| 		FloatOpaqueWhite, | ||||
| 		IntOpaqueWhite | ||||
| 	} | ||||
| 
 | ||||
| 	public enum Backend | ||||
| 	{ | ||||
| 		DontCare, | ||||
| 		Vulkan, | ||||
| 		PS5, | ||||
| 		Invalid | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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); | ||||
| 	} | ||||
| } | ||||
|  | @ -127,14 +127,13 @@ namespace MoonWorks.Graphics | |||
| 		public uint Stride; | ||||
| 		public VertexInputRate InputRate; | ||||
| 
 | ||||
| 		// Shortcut for the common case of having a single vertex binding. | ||||
| 		public unsafe static VertexBinding Create<T>() where T : unmanaged | ||||
| 		public static VertexBinding Create<T>(uint binding = 0, VertexInputRate inputRate = VertexInputRate.Vertex) where T : unmanaged | ||||
| 		{ | ||||
| 			return new VertexBinding | ||||
| 			{ | ||||
| 				Binding = 0, | ||||
| 				InputRate = VertexInputRate.Vertex, | ||||
| 				Stride = (uint) sizeof(T) | ||||
| 				Binding = binding, | ||||
| 				InputRate = inputRate, | ||||
| 				Stride = (uint) Marshal.SizeOf<T>() | ||||
| 			}; | ||||
| 		} | ||||
| 	} | ||||
|  | @ -146,28 +145,6 @@ namespace MoonWorks.Graphics | |||
| 		public uint Binding; | ||||
| 		public VertexElementFormat Format; | ||||
| 		public uint Offset; | ||||
| 
 | ||||
| 		public static VertexAttribute Create<T>( | ||||
| 			string fieldName, | ||||
| 			uint location, | ||||
| 			uint binding = 0 | ||||
| 		) | ||||
| 		{ | ||||
| 			var fieldInfo = typeof(T).GetField(fieldName); | ||||
| 
 | ||||
| 			if (fieldInfo == null) | ||||
| 			{ | ||||
| 				throw new System.ArgumentException("Field not recognized!"); | ||||
| 			} | ||||
| 
 | ||||
| 			return new VertexAttribute | ||||
| 			{ | ||||
| 				Binding = binding, | ||||
| 				Location = location, | ||||
| 				Format = Conversions.TypeToVertexElementFormat(fieldInfo.FieldType), | ||||
| 				Offset = (uint) Marshal.OffsetOf<T>(fieldName) | ||||
| 			}; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	[StructLayout(LayoutKind.Sequential)] | ||||
|  | @ -204,30 +181,33 @@ namespace MoonWorks.Graphics | |||
| 		public uint Depth; | ||||
| 		public uint Layer; | ||||
| 		public uint Level; | ||||
| 		public SampleCount SampleCount; | ||||
| 		public Color ClearColor; | ||||
| 		public LoadOp LoadOp; | ||||
| 		public StoreOp StoreOp; | ||||
| 
 | ||||
| 		public ColorAttachmentInfo(Texture texture, Color clearColor, StoreOp storeOp = StoreOp.Store) | ||||
| 		{ | ||||
| 		public ColorAttachmentInfo( | ||||
| 			Texture texture, | ||||
| 			Color clearColor, | ||||
| 			StoreOp storeOp = StoreOp.Store | ||||
| 		) { | ||||
| 			Texture = texture; | ||||
| 			Depth = 0; | ||||
| 			Layer = 0; | ||||
| 			Level = 0; | ||||
| 			SampleCount = SampleCount.One; | ||||
| 			ClearColor = clearColor; | ||||
| 			LoadOp = LoadOp.Clear; | ||||
| 			StoreOp = storeOp; | ||||
| 		} | ||||
| 
 | ||||
| 		public ColorAttachmentInfo(Texture texture, LoadOp loadOp = LoadOp.DontCare, StoreOp storeOp = StoreOp.Store) | ||||
| 		{ | ||||
| 		public ColorAttachmentInfo( | ||||
| 			Texture texture, | ||||
| 			LoadOp loadOp = LoadOp.DontCare, | ||||
| 			StoreOp storeOp = StoreOp.Store | ||||
| 		) { | ||||
| 			Texture = texture; | ||||
| 			Depth = 0; | ||||
| 			Layer = 0; | ||||
| 			Level = 0; | ||||
| 			SampleCount = SampleCount.One; | ||||
| 			ClearColor = Color.White; | ||||
| 			LoadOp = loadOp; | ||||
| 			StoreOp = storeOp; | ||||
|  | @ -241,7 +221,6 @@ namespace MoonWorks.Graphics | |||
| 				depth = Depth, | ||||
| 				layer = Layer, | ||||
| 				level = Level, | ||||
| 				sampleCount = (Refresh.SampleCount) SampleCount, | ||||
| 				clearColor = new Refresh.Vec4 | ||||
| 				{ | ||||
| 					x = ClearColor.R / 255f, | ||||
|  | @ -304,6 +283,25 @@ namespace MoonWorks.Graphics | |||
| 			StencilStoreOp = stencilStoreOp; | ||||
| 		} | ||||
| 
 | ||||
| 		public DepthStencilAttachmentInfo( | ||||
| 			Texture texture, | ||||
| 			DepthStencilValue depthStencilValue, | ||||
| 			LoadOp loadOp, | ||||
| 			StoreOp storeOp, | ||||
| 			LoadOp stencilLoadOp, | ||||
| 			StoreOp stencilStoreOp | ||||
| 		) { | ||||
| 			Texture = texture; | ||||
| 			Depth = 0; | ||||
| 			Layer = 0; | ||||
| 			Level = 0; | ||||
| 			DepthStencilClearValue = depthStencilValue; | ||||
| 			LoadOp = loadOp; | ||||
| 			StoreOp = storeOp; | ||||
| 			StencilLoadOp = stencilLoadOp; | ||||
| 			StencilStoreOp = stencilStoreOp; | ||||
| 		} | ||||
| 
 | ||||
| 		public Refresh.DepthStencilAttachmentInfo ToRefresh() | ||||
| 		{ | ||||
| 			return new Refresh.DepthStencilAttachmentInfo | ||||
|  | @ -325,17 +323,35 @@ namespace MoonWorks.Graphics | |||
| 	public struct ColorAttachmentDescription | ||||
| 	{ | ||||
| 		public TextureFormat Format; | ||||
| 		public SampleCount SampleCount; | ||||
| 		public ColorAttachmentBlendState BlendState; | ||||
| 
 | ||||
| 		public ColorAttachmentDescription( | ||||
| 			TextureFormat format, | ||||
| 			ColorAttachmentBlendState blendState, | ||||
| 			SampleCount sampleCount = SampleCount.One | ||||
| 			ColorAttachmentBlendState blendState | ||||
| 		) { | ||||
| 			Format = format; | ||||
| 			SampleCount = sampleCount; | ||||
| 			BlendState = blendState; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	[StructLayout(LayoutKind.Sequential)] | ||||
| 	public struct IndirectDrawCommand | ||||
| 	{ | ||||
| 		public uint VertexCount; | ||||
| 		public uint InstanceCount; | ||||
| 		public uint FirstVertex; | ||||
| 		public uint FirstInstance; | ||||
| 
 | ||||
| 		public IndirectDrawCommand( | ||||
| 			uint vertexCount, | ||||
| 			uint instanceCount, | ||||
| 			uint firstVertex, | ||||
| 			uint firstInstance | ||||
| 		) { | ||||
| 			VertexCount = vertexCount; | ||||
| 			InstanceCount = instanceCount; | ||||
| 			FirstVertex = firstVertex; | ||||
| 			FirstInstance = firstInstance; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  | @ -33,7 +33,7 @@ namespace MoonWorks.Graphics | |||
| 			return new Buffer( | ||||
| 				device, | ||||
| 				usageFlags, | ||||
| 				(uint) sizeof(T) * elementCount | ||||
| 				(uint) Marshal.SizeOf<T>() * elementCount | ||||
| 			); | ||||
| 		} | ||||
| 
 | ||||
|  | @ -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, | ||||
|  | @ -78,5 +90,52 @@ 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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  | @ -28,6 +30,10 @@ namespace MoonWorks.Graphics | |||
| 				device.Handle, | ||||
| 				refreshComputeShaderInfo | ||||
| 			); | ||||
| 			if (Handle == IntPtr.Zero) | ||||
| 			{ | ||||
| 				throw new Exception("Could not create compute pipeline!"); | ||||
| 			} | ||||
| 
 | ||||
| 			ComputeShaderInfo = computeShaderInfo; | ||||
| 		} | ||||
|  |  | |||
|  | @ -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; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -5,15 +5,20 @@ 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; | ||||
| 
 | ||||
| 		public GraphicsShaderInfo VertexShaderInfo { get; } | ||||
| 		public GraphicsShaderInfo FragmentShaderInfo { get; } | ||||
| 		public SampleCount SampleCount { get; } | ||||
| 
 | ||||
| #if DEBUG | ||||
| 		internal GraphicsPipelineAttachmentInfo AttachmentInfo { get; } | ||||
| #endif | ||||
| 
 | ||||
| 		public unsafe GraphicsPipeline( | ||||
| 			GraphicsDevice device, | ||||
|  | @ -46,7 +51,6 @@ namespace MoonWorks.Graphics | |||
| 			for (var i = 0; i < attachmentInfo.ColorAttachmentDescriptions.Length; i += 1) | ||||
| 			{ | ||||
| 				colorAttachmentDescriptions[i].format = (Refresh.TextureFormat) attachmentInfo.ColorAttachmentDescriptions[i].Format; | ||||
| 				colorAttachmentDescriptions[i].sampleCount = (Refresh.SampleCount) attachmentInfo.ColorAttachmentDescriptions[i].SampleCount; | ||||
| 				colorAttachmentDescriptions[i].blendState = attachmentInfo.ColorAttachmentDescriptions[i].BlendState.ToRefresh(); | ||||
| 			} | ||||
| 
 | ||||
|  | @ -85,7 +89,6 @@ namespace MoonWorks.Graphics | |||
| 			refreshGraphicsPipelineCreateInfo.rasterizerState.depthBiasConstantFactor = rasterizerState.DepthBiasConstantFactor; | ||||
| 			refreshGraphicsPipelineCreateInfo.rasterizerState.depthBiasEnable = Conversions.BoolToByte(rasterizerState.DepthBiasEnable); | ||||
| 			refreshGraphicsPipelineCreateInfo.rasterizerState.depthBiasSlopeFactor = rasterizerState.DepthBiasSlopeFactor; | ||||
| 			refreshGraphicsPipelineCreateInfo.rasterizerState.depthClampEnable = Conversions.BoolToByte(rasterizerState.DepthClampEnable); | ||||
| 			refreshGraphicsPipelineCreateInfo.rasterizerState.fillMode = (Refresh.FillMode) rasterizerState.FillMode; | ||||
| 			refreshGraphicsPipelineCreateInfo.rasterizerState.frontFace = (Refresh.FrontFace) rasterizerState.FrontFace; | ||||
| 
 | ||||
|  | @ -102,12 +105,21 @@ namespace MoonWorks.Graphics | |||
| 			refreshGraphicsPipelineCreateInfo.attachmentInfo.hasDepthStencilAttachment = Conversions.BoolToByte(attachmentInfo.HasDepthStencilAttachment); | ||||
| 
 | ||||
| 			Handle = Refresh.Refresh_CreateGraphicsPipeline(device.Handle, refreshGraphicsPipelineCreateInfo); | ||||
| 			if (Handle == IntPtr.Zero) | ||||
| 			{ | ||||
| 				throw new Exception("Could not create graphics pipeline!"); | ||||
| 			} | ||||
| 
 | ||||
| 			vertexAttributesHandle.Free(); | ||||
| 			vertexBindingsHandle.Free(); | ||||
| 
 | ||||
| 			VertexShaderInfo = vertexShaderInfo; | ||||
| 			FragmentShaderInfo = fragmentShaderInfo; | ||||
| 			SampleCount = multisampleState.MultisampleCount; | ||||
| 
 | ||||
| #if DEBUG | ||||
| 			AttachmentInfo = attachmentInfo; | ||||
| #endif | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,22 +1,21 @@ | |||
| using RefreshCS; | ||||
| using System; | ||||
| using System.IO; | ||||
| using System.Runtime.InteropServices; | ||||
| 
 | ||||
| namespace MoonWorks.Graphics | ||||
| { | ||||
| 	/// <summary> | ||||
| 	/// Shader modules expect input in SPIR-V bytecode format. | ||||
| 	/// 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)) | ||||
| 			{ | ||||
| 				Handle = CreateFromStream(device, stream); | ||||
| 			} | ||||
| 			using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); | ||||
| 			Handle = CreateFromStream(device, stream); | ||||
| 		} | ||||
| 
 | ||||
| 		public unsafe ShaderModule(GraphicsDevice device, Stream stream) : base(device) | ||||
|  | @ -24,19 +23,20 @@ namespace MoonWorks.Graphics | |||
| 			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; | ||||
| 			Refresh.ShaderModuleCreateInfo shaderModuleCreateInfo; | ||||
| 			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; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| using System; | ||||
| using System.IO; | ||||
| using System.Runtime.InteropServices; | ||||
| using RefreshCS; | ||||
| 
 | ||||
| namespace MoonWorks.Graphics | ||||
|  | @ -7,113 +8,175 @@ namespace MoonWorks.Graphics | |||
| 	/// <summary> | ||||
| 	/// A container for pixel data. | ||||
| 	/// </summary> | ||||
| 	public class Texture : GraphicsResource | ||||
| 	public class Texture : RefreshResource | ||||
| 	{ | ||||
| 		public uint Width { get; } | ||||
| 		public uint Height { get; } | ||||
| 		public uint Width { get; internal set; } | ||||
| 		public uint Height { get; internal set; } | ||||
| 		public uint Depth { get; } | ||||
| 		public TextureFormat Format { get; } | ||||
| 		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) | ||||
| 		{ | ||||
| 			var pixels = Refresh.Refresh_Image_Load( | ||||
| 				filePath, | ||||
| 				out var width, | ||||
| 				out var height, | ||||
| 				out var channels | ||||
| 			); | ||||
| 		public static unsafe Texture FromImageBytes( | ||||
| 			GraphicsDevice device, | ||||
| 			CommandBuffer commandBuffer, | ||||
| 			Span<byte> data | ||||
| 		) { | ||||
| 			Texture texture; | ||||
| 
 | ||||
| 			var byteCount = (uint) (width * height * channels); | ||||
| 			fixed (byte *dataPtr = data) | ||||
| 			{ | ||||
| 				var pixels = Refresh.Refresh_Image_Load((nint) dataPtr, data.Length, out var width, out var height, out var len); | ||||
| 
 | ||||
| 			TextureCreateInfo 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; | ||||
| 				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); | ||||
| 				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); | ||||
| 
 | ||||
| 			fixed (byte* ptr = &pixels[0]) | ||||
| 			var texture = FromImageBytes(device, commandBuffer, span); | ||||
| 
 | ||||
| 			NativeMemory.Free((void*) buffer); | ||||
| 
 | ||||
| 			return texture; | ||||
| 		} | ||||
| 
 | ||||
| 		/// <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) | ||||
| 		/// <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)) | ||||
| 			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); | ||||
| 
 | ||||
| 			if (isCube) | ||||
| 			{ | ||||
| 				Texture texture; | ||||
| 				int faces; | ||||
| 				ParseDDS(reader, out var format, out var width, out var height, out var levels, out var isCube); | ||||
| 
 | ||||
| 				if (isCube) | ||||
| 				{ | ||||
| 					texture = CreateTextureCube(graphicsDevice, (uint) width, format, TextureUsageFlags.Sampler, SampleCount.One, (uint) levels); | ||||
| 					faces = 6; | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					texture = CreateTexture2D(graphicsDevice, (uint) width, (uint) height, format, TextureUsageFlags.Sampler, SampleCount.One, (uint) levels); | ||||
| 					faces = 1; | ||||
| 				} | ||||
| 
 | ||||
| 				for (int i = 0; i < faces; i += 1) | ||||
| 				{ | ||||
| 					for (int j = 0; j < levels; j += 1) | ||||
| 					{ | ||||
| 						var levelWidth = width >> j; | ||||
| 						var levelHeight = height >> j; | ||||
| 
 | ||||
| 						var pixels = reader.ReadBytes( | ||||
| 							Texture.CalculateDDSLevelSize( | ||||
| 								levelWidth, | ||||
| 								levelHeight, | ||||
| 								format | ||||
| 							) | ||||
| 						); | ||||
| 
 | ||||
| 						var textureSlice = new TextureSlice(texture, new Rect(0, 0, levelWidth, levelHeight), 0, (uint) i, (uint) j); | ||||
| 						commandBuffer.SetTextureData(textureSlice, pixels); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				return texture; | ||||
| 				texture = CreateTextureCube(graphicsDevice, (uint) width, format, TextureUsageFlags.Sampler, (uint) levels); | ||||
| 				faces = 6; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				texture = CreateTexture2D(graphicsDevice, (uint) width, (uint) height, format, TextureUsageFlags.Sampler, (uint) levels); | ||||
| 				faces = 1; | ||||
| 			} | ||||
| 
 | ||||
| 			for (int i = 0; i < faces; i += 1) | ||||
| 			{ | ||||
| 				for (int j = 0; j < levels; j += 1) | ||||
| 				{ | ||||
| 					var levelWidth = width >> j; | ||||
| 					var levelHeight = height >> j; | ||||
| 
 | ||||
| 					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, (nint) byteBuffer, (uint) levelSize); | ||||
| 
 | ||||
| 					NativeMemory.Free(byteBuffer); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			return texture; | ||||
| 		} | ||||
| 
 | ||||
| 		/// <summary> | ||||
|  | @ -124,7 +187,6 @@ namespace MoonWorks.Graphics | |||
| 		/// <param name="height">The height of the texture.</param> | ||||
| 		/// <param name="format">The format of the texture.</param> | ||||
| 		/// <param name="usageFlags">Specifies how the texture will be used.</param> | ||||
| 		/// <param name="sampleCount">Specifies the multisample count.</param> | ||||
| 		/// <param name="levelCount">Specifies the number of mip levels.</param> | ||||
| 		public static Texture CreateTexture2D( | ||||
| 			GraphicsDevice device, | ||||
|  | @ -132,18 +194,17 @@ namespace MoonWorks.Graphics | |||
| 			uint height, | ||||
| 			TextureFormat format, | ||||
| 			TextureUsageFlags usageFlags, | ||||
| 			SampleCount sampleCount = SampleCount.One, | ||||
| 			uint levelCount = 1 | ||||
| 		) | ||||
| 		{ | ||||
| 			uint levelCount = 1, | ||||
| 			SampleCount sampleCount = SampleCount.One | ||||
| 		) { | ||||
| 			var textureCreateInfo = new TextureCreateInfo | ||||
| 			{ | ||||
| 				Width = width, | ||||
| 				Height = height, | ||||
| 				Depth = 1, | ||||
| 				IsCube = false, | ||||
| 				SampleCount = sampleCount, | ||||
| 				LevelCount = levelCount, | ||||
| 				SampleCount = sampleCount, | ||||
| 				Format = format, | ||||
| 				UsageFlags = usageFlags | ||||
| 			}; | ||||
|  | @ -160,7 +221,6 @@ namespace MoonWorks.Graphics | |||
| 		/// <param name="depth">The depth of the texture.</param> | ||||
| 		/// <param name="format">The format of the texture.</param> | ||||
| 		/// <param name="usageFlags">Specifies how the texture will be used.</param> | ||||
| 		/// <param name="sampleCount">Specifies the multisample count.</param> | ||||
| 		/// <param name="levelCount">Specifies the number of mip levels.</param> | ||||
| 		public static Texture CreateTexture3D( | ||||
| 			GraphicsDevice device, | ||||
|  | @ -169,17 +229,14 @@ namespace MoonWorks.Graphics | |||
| 			uint depth, | ||||
| 			TextureFormat format, | ||||
| 			TextureUsageFlags usageFlags, | ||||
| 			SampleCount sampleCount = SampleCount.One, | ||||
| 			uint levelCount = 1 | ||||
| 		) | ||||
| 		{ | ||||
| 		) { | ||||
| 			var textureCreateInfo = new TextureCreateInfo | ||||
| 			{ | ||||
| 				Width = width, | ||||
| 				Height = height, | ||||
| 				Depth = depth, | ||||
| 				IsCube = false, | ||||
| 				SampleCount = sampleCount, | ||||
| 				LevelCount = levelCount, | ||||
| 				Format = format, | ||||
| 				UsageFlags = usageFlags | ||||
|  | @ -195,24 +252,20 @@ namespace MoonWorks.Graphics | |||
| 		/// <param name="size">The length of one side of the cube.</param> | ||||
| 		/// <param name="format">The format of the texture.</param> | ||||
| 		/// <param name="usageFlags">Specifies how the texture will be used.</param> | ||||
| 		/// <param name="sampleCount">Specifies the multisample count.</param> | ||||
| 		/// <param name="levelCount">Specifies the number of mip levels.</param> | ||||
| 		public static Texture CreateTextureCube( | ||||
| 			GraphicsDevice device, | ||||
| 			uint size, | ||||
| 			TextureFormat format, | ||||
| 			TextureUsageFlags usageFlags, | ||||
| 			SampleCount sampleCount = SampleCount.One, | ||||
| 			uint levelCount = 1 | ||||
| 		) | ||||
| 		{ | ||||
| 		) { | ||||
| 			var textureCreateInfo = new TextureCreateInfo | ||||
| 			{ | ||||
| 				Width = size, | ||||
| 				Height = size, | ||||
| 				Depth = 1, | ||||
| 				IsCube = true, | ||||
| 				SampleCount = sampleCount, | ||||
| 				LevelCount = levelCount, | ||||
| 				Format = format, | ||||
| 				UsageFlags = usageFlags | ||||
|  | @ -241,9 +294,10 @@ namespace MoonWorks.Graphics | |||
| 			Height = textureCreateInfo.Height; | ||||
| 			Depth = textureCreateInfo.Depth; | ||||
| 			IsCube = textureCreateInfo.IsCube; | ||||
| 			SampleCount = textureCreateInfo.SampleCount; | ||||
| 			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); | ||||
|  | @ -252,22 +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; | ||||
| 			SampleCount = SampleCount.One; | ||||
| 			LevelCount = 1; | ||||
| 			SampleCount = SampleCount.One; | ||||
| 			UsageFlags = TextureUsageFlags.ColorTarget; | ||||
| 			Size = Width * Height * BytesPerPixel(Format) / BlockSizeSquared(Format); | ||||
| 		} | ||||
| 
 | ||||
| 		// DDS loading extension, based on MojoDDS | ||||
|  | @ -284,14 +336,12 @@ namespace MoonWorks.Graphics | |||
| 			const uint DDS_MAGIC = 0x20534444; | ||||
| 			const uint DDS_HEADERSIZE = 124; | ||||
| 			const uint DDS_PIXFMTSIZE = 32; | ||||
| 			const uint DDSD_CAPS = 0x1; | ||||
| 			const uint DDSD_HEIGHT = 0x2; | ||||
| 			const uint DDSD_WIDTH = 0x4; | ||||
| 			const uint DDSD_PITCH = 0x8; | ||||
| 			const uint DDSD_FMT = 0x1000; | ||||
| 			const uint DDSD_LINEARSIZE = 0x80000; | ||||
| 			const uint DDSD_REQ = ( | ||||
| 				DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_FMT | ||||
| 				DDSD_HEIGHT | DDSD_WIDTH | ||||
| 			); | ||||
| 			const uint DDSCAPS_MIPMAP = 0x400000; | ||||
| 			const uint DDSCAPS_TEXTURE = 0x1000; | ||||
|  | @ -539,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; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -2,6 +2,9 @@ | |||
| 
 | ||||
| namespace MoonWorks.Graphics | ||||
| { | ||||
| 	/// <summary> | ||||
| 	/// Defines how color blending will be performed in a GraphicsPipeline. | ||||
| 	/// </summary> | ||||
| 	public struct ColorAttachmentBlendState | ||||
| 	{ | ||||
| 		/// <summary> | ||||
|  |  | |||
|  | @ -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 | ||||
| 	{ | ||||
|  | @ -24,7 +24,7 @@ namespace MoonWorks.Graphics | |||
| 			{ | ||||
| 				ShaderModule = shaderModule, | ||||
| 				EntryPointName = entryPointName, | ||||
| 				UniformBufferSize = (uint) sizeof(T), | ||||
| 				UniformBufferSize = (uint) Marshal.SizeOf<T>(), | ||||
| 				BufferBindingCount = bufferBindingCount, | ||||
| 				ImageBindingCount = imageBindingCount | ||||
| 			}; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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 | ||||
| 	{ | ||||
|  | @ -22,7 +22,7 @@ namespace MoonWorks.Graphics | |||
| 			{ | ||||
| 				ShaderModule = shaderModule, | ||||
| 				EntryPointName = entryPointName, | ||||
| 				UniformBufferSize = (uint) sizeof(T), | ||||
| 				UniformBufferSize = (uint) Marshal.SizeOf<T>(), | ||||
| 				SamplerBindingCount = samplerBindingCount | ||||
| 			}; | ||||
| 		} | ||||
|  |  | |||
|  | @ -13,5 +13,13 @@ | |||
| 			MultisampleCount = SampleCount.One, | ||||
| 			SampleMask = uint.MaxValue | ||||
| 		}; | ||||
| 
 | ||||
| 		public MultisampleState( | ||||
| 			SampleCount sampleCount, | ||||
| 			uint sampleMask = uint.MaxValue | ||||
| 		) { | ||||
| 			MultisampleCount = sampleCount; | ||||
| 			SampleMask = sampleMask; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -29,7 +29,6 @@ | |||
| 		/// Factor applied to a fragment's slope in depth bias calculations. Only applies if depth biasing is enabled. | ||||
| 		/// </summary> | ||||
| 		public float DepthBiasSlopeFactor; | ||||
| 		public bool DepthClampEnable; | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// Specifies how triangles should be drawn. | ||||
|  | @ -69,7 +68,7 @@ | |||
| 		{ | ||||
| 			CullMode = CullMode.None, | ||||
| 			FrontFace = FrontFace.Clockwise, | ||||
| 			FillMode = FillMode.Fill, | ||||
| 			FillMode = FillMode.Line, | ||||
| 			DepthBiasEnable = false | ||||
| 		}; | ||||
| 
 | ||||
|  | @ -101,7 +100,7 @@ | |||
| 		{ | ||||
| 			CullMode = CullMode.None, | ||||
| 			FrontFace = FrontFace.CounterClockwise, | ||||
| 			FillMode = FillMode.Fill, | ||||
| 			FillMode = FillMode.Line, | ||||
| 			DepthBiasEnable = false | ||||
| 		}; | ||||
| 	} | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -2,14 +2,17 @@ | |||
| 
 | ||||
| namespace MoonWorks.Graphics | ||||
| { | ||||
| 	/// <summary> | ||||
| 	/// All of the information that is used to create a texture. | ||||
| 	/// </summary> | ||||
| 	public struct TextureCreateInfo | ||||
| 	{ | ||||
| 		public uint Width; | ||||
| 		public uint Height; | ||||
| 		public uint Depth; | ||||
| 		public bool IsCube; | ||||
| 		public SampleCount SampleCount; | ||||
| 		public uint LevelCount; | ||||
| 		public SampleCount SampleCount; | ||||
| 		public TextureFormat Format; | ||||
| 		public TextureUsageFlags UsageFlags; | ||||
| 
 | ||||
|  | @ -21,8 +24,8 @@ namespace MoonWorks.Graphics | |||
| 				height = Height, | ||||
| 				depth = Depth, | ||||
| 				isCube = Conversions.BoolToByte(IsCube), | ||||
| 				sampleCount = (Refresh.SampleCount) SampleCount, | ||||
| 				levelCount = LevelCount, | ||||
| 				sampleCount = (Refresh.SampleCount) SampleCount, | ||||
| 				format = (Refresh.TextureFormat) Format, | ||||
| 				usageFlags = (Refresh.TextureUsageFlags) UsageFlags | ||||
| 			}; | ||||
|  |  | |||
|  | @ -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 | ||||
| 	{ | ||||
|  | @ -10,16 +10,61 @@ | |||
| 
 | ||||
| 		public static readonly VertexInputState Empty = new VertexInputState | ||||
| 		{ | ||||
| 			VertexBindings = new VertexBinding[0], | ||||
| 			VertexAttributes = new VertexAttribute[0] | ||||
| 			VertexBindings = System.Array.Empty<VertexBinding>(), | ||||
| 			VertexAttributes = System.Array.Empty<VertexAttribute>() | ||||
| 		}; | ||||
| 
 | ||||
| 		public VertexInputState( | ||||
| 			VertexBinding vertexBinding, | ||||
| 			params VertexAttribute[] vertexAttributes | ||||
| 			VertexAttribute[] vertexAttributes | ||||
| 		) { | ||||
| 			VertexBindings = new VertexBinding[] { vertexBinding }; | ||||
| 			VertexAttributes = vertexAttributes; | ||||
| 		} | ||||
| 
 | ||||
| 		public VertexInputState( | ||||
| 			VertexBinding[] vertexBindings, | ||||
| 			VertexAttribute[] vertexAttributes | ||||
| 		) { | ||||
| 			VertexBindings = vertexBindings; | ||||
| 			VertexAttributes = vertexAttributes; | ||||
| 		} | ||||
| 
 | ||||
| 		public VertexInputState( | ||||
| 			VertexBindingAndAttributes bindingAndAttributes | ||||
| 		) { | ||||
| 			VertexBindings = new VertexBinding[] { bindingAndAttributes.VertexBinding }; | ||||
| 			VertexAttributes = bindingAndAttributes.VertexAttributes; | ||||
| 		} | ||||
| 
 | ||||
| 		public VertexInputState( | ||||
| 			VertexBindingAndAttributes[] bindingAndAttributesArray | ||||
| 		) { | ||||
| 			VertexBindings = new VertexBinding[bindingAndAttributesArray.Length]; | ||||
| 			var attributesLength = 0; | ||||
| 
 | ||||
| 			for (var i = 0; i < bindingAndAttributesArray.Length; i += 1) | ||||
| 			{ | ||||
| 				VertexBindings[i] = bindingAndAttributesArray[i].VertexBinding; | ||||
| 				attributesLength += bindingAndAttributesArray[i].VertexAttributes.Length; | ||||
| 			} | ||||
| 
 | ||||
| 			VertexAttributes = new VertexAttribute[attributesLength]; | ||||
| 
 | ||||
| 			var attributeIndex = 0; | ||||
| 			for (var i = 0; i < bindingAndAttributesArray.Length; i += 1) | ||||
| 			{ | ||||
| 				for (var j = 0; j < bindingAndAttributesArray[i].VertexAttributes.Length; j += 1) | ||||
| 				{ | ||||
| 					VertexAttributes[attributeIndex] = bindingAndAttributesArray[i].VertexAttributes[j]; | ||||
| 					attributeIndex += 1; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static VertexInputState CreateSingleBinding<T>() where T : unmanaged, IVertexType | ||||
| 		{ | ||||
| 			return new VertexInputState(VertexBindingAndAttributes.Create<T>(0)); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -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); | ||||
| } | ||||
|  | @ -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; | ||||
| } | ||||
|  | @ -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; | ||||
|  |  | |||
|  | @ -1,49 +0,0 @@ | |||
| namespace MoonWorks | ||||
| { | ||||
| 	public static class Conversions | ||||
| 	{ | ||||
| 		public static byte BoolToByte(bool b) | ||||
| 		{ | ||||
| 			return (byte) (b ? 1 : 0); | ||||
| 		} | ||||
| 
 | ||||
| 		public static bool ByteToBool(byte b) | ||||
| 		{ | ||||
| 			return b != 0; | ||||
| 		} | ||||
| 
 | ||||
| 		public static Graphics.VertexElementFormat TypeToVertexElementFormat(System.Type type) | ||||
| 		{ | ||||
| 			if (type == typeof(uint)) | ||||
| 			{ | ||||
| 				return Graphics.VertexElementFormat.UInt; | ||||
| 			} | ||||
| 			if (type == typeof(float)) | ||||
| 			{ | ||||
| 				return Graphics.VertexElementFormat.Float; | ||||
| 			} | ||||
| 			else if (type == typeof(Math.Float.Vector2)) | ||||
| 			{ | ||||
| 				return Graphics.VertexElementFormat.Vector2; | ||||
| 			} | ||||
| 			else if (type == typeof(Math.Float.Vector3)) | ||||
| 			{ | ||||
| 				return Graphics.VertexElementFormat.Vector3; | ||||
| 			} | ||||
| 			else if (type == typeof(Math.Float.Vector4)) | ||||
| 			{ | ||||
| 				return Graphics.VertexElementFormat.Vector4; | ||||
| 			} | ||||
| 			else if (type == typeof(Graphics.Color)) | ||||
| 			{ | ||||
| 				return Graphics.VertexElementFormat.Color; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				throw new System.ArgumentException( | ||||
| 					"Cannot automatically convert this type to a VertexElementFormat!" | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,41 @@ | |||
| namespace MoonWorks.Graphics | ||||
| { | ||||
| 	/// <summary> | ||||
| 	/// A convenience structure for pairing a vertex binding with its associated attributes. | ||||
| 	/// </summary> | ||||
| 	public struct VertexBindingAndAttributes | ||||
| 	{ | ||||
| 		public VertexBinding VertexBinding { get; } | ||||
| 		public VertexAttribute[] VertexAttributes { get; } | ||||
| 
 | ||||
| 		public VertexBindingAndAttributes(VertexBinding binding, VertexAttribute[] attributes) | ||||
| 		{ | ||||
| 			VertexBinding = binding; | ||||
| 			VertexAttributes = attributes; | ||||
| 		} | ||||
| 
 | ||||
| 		public static VertexBindingAndAttributes Create<T>(uint bindingIndex, uint locationOffset = 0, VertexInputRate inputRate = VertexInputRate.Vertex) where T : unmanaged, IVertexType | ||||
| 		{ | ||||
| 			VertexBinding binding = VertexBinding.Create<T>(bindingIndex, inputRate); | ||||
| 			VertexAttribute[] attributes = new VertexAttribute[T.Formats.Length]; | ||||
| 			uint offset = 0; | ||||
| 
 | ||||
| 			for (uint i = 0; i < T.Formats.Length; i += 1) | ||||
| 			{ | ||||
| 				var format = T.Formats[i]; | ||||
| 
 | ||||
| 				attributes[i] = new VertexAttribute | ||||
| 				{ | ||||
| 					Binding = bindingIndex, | ||||
| 					Location = locationOffset + i, | ||||
| 					Format = format, | ||||
| 					Offset = offset | ||||
| 				}; | ||||
| 
 | ||||
| 				offset += Conversions.VertexElementFormatSize(format); | ||||
| 			} | ||||
| 
 | ||||
| 			return new VertexBindingAndAttributes(binding, attributes); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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; } | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -1,14 +1,42 @@ | |||
| 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) | ||||
| 		{ | ||||
| 			ButtonStatus = buttonStatus; | ||||
|  | @ -18,26 +46,34 @@ | |||
| 		{ | ||||
| 			if (isPressed) | ||||
| 			{ | ||||
| 				if (ButtonStatus == ButtonStatus.Pressed) | ||||
| 				{ | ||||
| 					return new ButtonState(ButtonStatus.Held); | ||||
| 				} | ||||
| 				else if (ButtonStatus == ButtonStatus.Released) | ||||
| 				if (IsUp) | ||||
| 				{ | ||||
| 					return new ButtonState(ButtonStatus.Pressed); | ||||
| 				} | ||||
| 				else if (ButtonStatus == ButtonStatus.Held) | ||||
| 				else | ||||
| 				{ | ||||
| 					return new ButtonState(ButtonStatus.Held); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			return new ButtonState(ButtonStatus.Released); | ||||
| 			else | ||||
| 			{ | ||||
| 				if (IsDown) | ||||
| 				{ | ||||
| 					return new ButtonState(ButtonStatus.Released); | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					return new ButtonState(ButtonStatus.Idle); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// Combines two button states. Useful for alt control sets or input buffering. | ||||
| 		/// </summary> | ||||
| 		public static ButtonState operator |(ButtonState a, ButtonState b) | ||||
| 		{ | ||||
| 			if (a.ButtonStatus == ButtonStatus.Released) | ||||
| 			if (a.ButtonStatus == ButtonStatus.Idle || a.ButtonStatus == ButtonStatus.Released) | ||||
| 			{ | ||||
| 				return b; | ||||
| 			} | ||||
|  |  | |||
|  | @ -1,17 +1,24 @@ | |||
| namespace MoonWorks.Input | ||||
| { | ||||
| 	/// <summary> | ||||
| 	/// Represents the current status of a binary input. | ||||
| 	/// </summary> | ||||
| 	public enum ButtonStatus | ||||
| 	{ | ||||
| 		/// <summary> | ||||
| 		/// Indicates that the input is not pressed. | ||||
| 		/// Indicates that the button was not pressed last frame and is still not pressed. | ||||
| 		/// </summary> | ||||
| 		Idle, | ||||
| 		/// <summary> | ||||
| 		/// Indicates that the button was released this frame. | ||||
| 		/// </summary> | ||||
| 		Released, | ||||
| 		/// <summary> | ||||
| 		/// Indicates that the input was pressed this frame. | ||||
| 		/// Indicates that the button was pressed this frame. | ||||
| 		/// </summary> | ||||
| 		Pressed, | ||||
| 		/// <summary> | ||||
| 		/// Indicates that the input has been held for multiple frames. | ||||
| 		/// Indicates that the button has been held for multiple frames. | ||||
| 		/// </summary> | ||||
| 		Held | ||||
| 	} | ||||
|  |  | |||
|  | @ -1,10 +0,0 @@ | |||
| namespace MoonWorks.Input | ||||
| { | ||||
|     public enum DeviceKind | ||||
|     { | ||||
| 		None, | ||||
|         Keyboard, | ||||
|         Mouse, | ||||
|         Gamepad, | ||||
|     } | ||||
| } | ||||
|  | @ -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]; | ||||
|  |  | |||
|  | @ -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); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  | @ -37,7 +71,6 @@ namespace MoonWorks.Input | |||
| 			AnyPressed = false; | ||||
| 			AnyPressedButton = default; // DeviceKind.None | ||||
| 
 | ||||
| 			Mouse.Wheel = 0; | ||||
| 			Keyboard.Update(); | ||||
| 
 | ||||
| 			if (Keyboard.AnyPressed) | ||||
|  | @ -54,7 +87,7 @@ namespace MoonWorks.Input | |||
| 				AnyPressedButton = Mouse.AnyPressedButton; | ||||
| 			} | ||||
| 
 | ||||
| 			foreach (var gamepad in gamepads) | ||||
| 			foreach (var gamepad in Gamepads) | ||||
| 			{ | ||||
| 				gamepad.Update(); | ||||
| 
 | ||||
|  | @ -66,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) | ||||
|  | @ -73,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) | ||||
|  | @ -88,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; | ||||
| 				} | ||||
| 			} | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -1,17 +1,27 @@ | |||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Runtime.InteropServices; | ||||
| 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; } | ||||
| 		private int numKeys; | ||||
| 
 | ||||
|  | @ -41,8 +51,10 @@ namespace MoonWorks.Input | |||
| 		{ | ||||
| 			SDL.SDL_GetKeyboardState(out numKeys); | ||||
| 
 | ||||
| 			KeyCodes = Enum.GetValues<KeyCode>(); | ||||
| 			Keys = new KeyboardButton[numKeys]; | ||||
| 			foreach (KeyCode keycode in Enum.GetValues(typeof(KeyCode))) | ||||
| 
 | ||||
| 			foreach (KeyCode keycode in KeyCodes) | ||||
| 			{ | ||||
| 				Keys[(int) keycode] = new KeyboardButton(this, keycode); | ||||
| 			} | ||||
|  | @ -54,18 +66,18 @@ namespace MoonWorks.Input | |||
| 
 | ||||
| 			State = SDL.SDL_GetKeyboardState(out _); | ||||
| 
 | ||||
| 			foreach (int keycode in Enum.GetValues(typeof(KeyCode))) | ||||
| 			foreach (KeyCode keycode in KeyCodes) | ||||
| 			{ | ||||
| 				var button = Keys[keycode]; | ||||
| 				var button = Keys[(int) keycode]; | ||||
| 				button.Update(); | ||||
| 
 | ||||
| 				if (button.IsPressed) | ||||
| 				{ | ||||
| 					if (TextInputBindings.TryGetValue((KeyCode) keycode, out var textIndex)) | ||||
| 					if (TextInputBindings.TryGetValue(keycode, out var textIndex)) | ||||
| 					{ | ||||
| 						Inputs.OnTextInput(TextInputCharacters[(textIndex)]); | ||||
| 					} | ||||
| 					else if (IsDown(KeyCode.LeftControl) && (KeyCode) keycode == KeyCode.V) | ||||
| 					else if (IsDown(KeyCode.LeftControl) && keycode == KeyCode.V) | ||||
| 					{ | ||||
| 						Inputs.OnTextInput(TextInputCharacters[6]); | ||||
| 					} | ||||
|  | @ -76,31 +88,65 @@ namespace MoonWorks.Input | |||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public bool IsDown(KeyCode keycode) | ||||
| 		{ | ||||
| 			return Keys[(int) keycode].IsDown; | ||||
| 		} | ||||
| 
 | ||||
| 		/// <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; | ||||
|  |  | |||
|  | @ -3,25 +3,44 @@ using SDL2; | |||
| 
 | ||||
| namespace MoonWorks.Input | ||||
| { | ||||
| 	/// <summary> | ||||
| 	/// The mouse input device abstraction. | ||||
| 	/// </summary> | ||||
| 	public class Mouse | ||||
| 	{ | ||||
| 		public MouseButton LeftButton { get; } | ||||
| 		public MouseButton MiddleButton { get; } | ||||
| 		public MouseButton RightButton { get; } | ||||
| 		public MouseButton X1Button { get; } | ||||
| 		public MouseButton X2Button { get; } | ||||
| 
 | ||||
| 		public int X { get; private set; } | ||||
| 		public int Y { get; private set; } | ||||
| 		public int DeltaX { get; private set; } | ||||
| 		public int DeltaY { get; private set; } | ||||
| 
 | ||||
| 		public int Wheel { get; internal set; } | ||||
| 		// note that this is a delta value | ||||
| 		public int Wheel { get; private set; } | ||||
| 		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; | ||||
|  | @ -36,29 +55,37 @@ namespace MoonWorks.Input | |||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		private readonly Dictionary<MouseButtonCode, MouseButton> CodeToButton; | ||||
| 
 | ||||
| 		private IEnumerable<MouseButton> Buttons | ||||
| 		private bool hidden; | ||||
| 		/// <summary> | ||||
| 		/// If set to true, the OS cursor will not be shown in your application window. | ||||
| 		/// </summary> | ||||
| 		public bool Hidden | ||||
| 		{ | ||||
| 			get | ||||
| 			get => hidden; | ||||
| 			set | ||||
| 			{ | ||||
| 				yield return LeftButton; | ||||
| 				yield return MiddleButton; | ||||
| 				yield return RightButton; | ||||
| 				hidden = value; | ||||
| 				SDL.SDL_ShowCursor(hidden ? SDL.SDL_DISABLE : SDL.SDL_ENABLE); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public Mouse() | ||||
| 		private readonly Dictionary<MouseButtonCode, MouseButton> CodeToButton; | ||||
| 
 | ||||
| 		internal Mouse() | ||||
| 		{ | ||||
| 			LeftButton = new MouseButton(this, MouseButtonCode.Left, SDL.SDL_BUTTON_LMASK); | ||||
| 			MiddleButton = new MouseButton(this, MouseButtonCode.Middle, SDL.SDL_BUTTON_MMASK); | ||||
| 			RightButton = new MouseButton(this, MouseButtonCode.Right, SDL.SDL_BUTTON_RMASK); | ||||
| 			X1Button = new MouseButton(this, MouseButtonCode.X1, SDL.SDL_BUTTON_X1MASK); | ||||
| 			X2Button = new MouseButton(this, MouseButtonCode.X2, SDL.SDL_BUTTON_X2MASK); | ||||
| 
 | ||||
| 			CodeToButton = new Dictionary<MouseButtonCode, MouseButton> | ||||
| 			{ | ||||
| 				{ MouseButtonCode.Left, LeftButton }, | ||||
| 				{ MouseButtonCode.Right, RightButton }, | ||||
| 				{ MouseButtonCode.Middle, MiddleButton } | ||||
| 				{ MouseButtonCode.Middle, MiddleButton }, | ||||
| 				{ MouseButtonCode.X1, X1Button }, | ||||
| 				{ MouseButtonCode.X2, X2Button } | ||||
| 			}; | ||||
| 		} | ||||
| 
 | ||||
|  | @ -74,11 +101,10 @@ namespace MoonWorks.Input | |||
| 			DeltaX = deltaX; | ||||
| 			DeltaY = deltaY; | ||||
| 
 | ||||
| 			LeftButton.Update(); | ||||
| 			MiddleButton.Update(); | ||||
| 			RightButton.Update(); | ||||
| 			Wheel = WheelRaw - previousWheelRaw; | ||||
| 			previousWheelRaw = WheelRaw; | ||||
| 
 | ||||
| 			foreach (var button in Buttons) | ||||
| 			foreach (var button in CodeToButton.Values) | ||||
| 			{ | ||||
| 				button.Update(); | ||||
| 
 | ||||
|  | @ -90,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; | ||||
|  |  | |||
|  | @ -1,9 +1,14 @@ | |||
| namespace MoonWorks.Input | ||||
| { | ||||
|     public enum MouseButtonCode | ||||
|     { | ||||
|         Left, | ||||
|         Right, | ||||
|         Middle | ||||
|     } | ||||
| 	/// <summary> | ||||
| 	/// Can be used to determine virtual mouse button state without a direct reference to the button object. | ||||
| 	/// </summary> | ||||
| 	public enum MouseButtonCode | ||||
| 	{ | ||||
| 		Left, | ||||
| 		Right, | ||||
| 		Middle, | ||||
| 		X1, | ||||
| 		X2 | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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; } | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -1,29 +1,42 @@ | |||
| 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; } | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// True if the button is pressed or held. | ||||
| 		/// </summary> | ||||
| 		public bool IsDown => State.IsDown; | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// True if the button has been continuously held for more than one frame. | ||||
| 		/// </summary> | ||||
| 		public bool IsHeld => State.IsHeld; | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// True if the button was pressed this exact frame. | ||||
| 		/// </summary> | ||||
| 		public bool IsPressed => State.IsPressed; | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// True if the button is not pressed. | ||||
| 		/// True if the button has been continuously held for more than one frame. | ||||
| 		/// </summary> | ||||
| 		public bool IsHeld => State.IsHeld; | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// True if the button is pressed or held. | ||||
| 		/// </summary> | ||||
| 		public bool IsDown => State.IsDown; | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// True if the button was released this frame. | ||||
| 		/// </summary> | ||||
| 		public bool IsReleased => State.IsReleased; | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// True if the button was not pressed the previous or current frame. | ||||
| 		/// </summary> | ||||
| 		public bool IsIdle => State.IsIdle; | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// True if the button is idle or released. | ||||
| 		/// </summary> | ||||
| 		public bool IsUp => State.IsUp; | ||||
| 
 | ||||
| 		internal virtual void Update() | ||||
| 		{ | ||||
| 			State = State.Update(CheckPressed()); | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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; } | ||||
|  |  | |||
|  | @ -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]); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,8 @@ | |||
| namespace MoonWorks.Input | ||||
| { | ||||
| 	/// <summary> | ||||
| 	/// A virtual button corresponding to a mouse button. | ||||
| 	/// </summary> | ||||
| 	public class MouseButton : VirtualButton | ||||
| 	{ | ||||
| 		Mouse Parent; | ||||
|  |  | |||
|  | @ -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; } | ||||
|  |  | |||
|  | @ -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)); | ||||
|  |  | |||
|  | @ -0,0 +1,886 @@ | |||
| using MoonWorks.Math.Fixed; | ||||
| using System.Collections.Generic; | ||||
| 
 | ||||
| namespace MoonWorks.Math | ||||
| { | ||||
| 	public static class Easing | ||||
| 	{ | ||||
| 		private const float C1 = 1.70158f; | ||||
| 		private const float C2 = C1 * 1.525f; | ||||
| 		private const float C3 = C1 + 1; | ||||
| 		private const float C4 = (2 * System.MathF.PI) / 3; | ||||
| 		private const float C5 = (2 * System.MathF.PI) / 4.5f; | ||||
| 
 | ||||
| 		private static readonly Fix64 HALF = Fix64.FromFraction(1, 2); | ||||
| 		private static readonly Fix64 FIXED_C1 = Fix64.FromFraction(170158, 100000); | ||||
| 		private static readonly Fix64 FIXED_C2 = FIXED_C1 * Fix64.FromFraction(61, 40); | ||||
| 		private static readonly Fix64 FIXED_C3 = FIXED_C1 + Fix64.One; | ||||
| 		private static readonly Fix64 FIXED_C4 = Fix64.PiTimes2 / new Fix64(3); | ||||
| 		private static readonly Fix64 FIXED_C5 = Fix64.PiTimes2 / Fix64.FromFraction(9, 2); | ||||
| 
 | ||||
| 		private static readonly Fix64 FIXED_N1 = Fix64.FromFraction(121, 16); | ||||
| 		private static readonly Fix64 FIXED_D1 = Fix64.FromFraction(11, 4); | ||||
| 
 | ||||
| 		private static float OutIn( | ||||
| 			System.Func<float, float> outFunc, | ||||
| 			System.Func<float, float> inFunc, | ||||
| 			float t | ||||
| 		) { | ||||
| 			if (t < 0.5f) | ||||
| 			{ | ||||
| 				return outFunc(t); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				return inFunc(t); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		private static Fix64 OutIn( | ||||
| 			System.Func<Fix64, Fix64> outFunc, | ||||
| 			System.Func<Fix64, Fix64> inFunc, | ||||
| 			Fix64 t | ||||
| 		) { | ||||
| 			if (t < HALF) | ||||
| 			{ | ||||
| 				return outFunc(t); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				return inFunc(t); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		/* GENERAL-USE FUNCTIONS */ | ||||
| 
 | ||||
| 		public static float AttackHoldRelease( | ||||
| 			float start, | ||||
| 			float hold, | ||||
| 			float end, | ||||
| 			float time, | ||||
| 			float attackDuration, | ||||
| 			Function.Float attackEasingFunction, | ||||
| 			float holdDuration, | ||||
| 			float releaseDuration, | ||||
| 			Function.Float releaseEasingFunction | ||||
| 		) { | ||||
| 			if (time < attackDuration) | ||||
| 			{ | ||||
| 				return Interp(start, hold, time, attackDuration, Function.Get(attackEasingFunction)); | ||||
| 			} | ||||
| 			else if (time >= attackDuration && time < attackDuration + holdDuration) | ||||
| 			{ | ||||
| 				return hold; | ||||
| 			} | ||||
| 			else // time >= attackDuration + holdDuration | ||||
| 			{ | ||||
| 				return Interp(hold, end, time - holdDuration - attackDuration, releaseDuration, Function.Get(releaseEasingFunction)); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 AttackHoldRelease( | ||||
| 			Fix64 start, | ||||
| 			Fix64 hold, | ||||
| 			Fix64 end, | ||||
| 			Fix64 time, | ||||
| 			Fix64 attackDuration, | ||||
| 			Function.Fixed attackEasingFunction, | ||||
| 			Fix64 holdDuration, | ||||
| 			Fix64 releaseDuration, | ||||
| 			Function.Fixed releaseEasingFunction | ||||
| 		) { | ||||
| 			if (time < attackDuration) | ||||
| 			{ | ||||
| 				return Interp(start, hold, time, attackDuration, Function.Get(attackEasingFunction)); | ||||
| 			} | ||||
| 			else if (time >= attackDuration && time < attackDuration + holdDuration) | ||||
| 			{ | ||||
| 				return hold; | ||||
| 			} | ||||
| 			else // time >= attackDuration + holdDuration | ||||
| 			{ | ||||
| 				return Interp(hold, end, time - holdDuration - attackDuration, releaseDuration, Function.Get(releaseEasingFunction)); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float Lerp(float start, float end, float time) | ||||
| 		{ | ||||
| 			return (start + (end - start) * time); | ||||
| 		} | ||||
| 
 | ||||
| 		public static float Interp(float start, float end, float time, float duration, System.Func<float, float> easingFunc) | ||||
| 		{ | ||||
| 			return Lerp(start, end, easingFunc(time / duration)); | ||||
| 		} | ||||
| 
 | ||||
| 		public static float Interp(float start, float end, float time, float duration, MoonWorks.Math.Easing.Function.Float easingFunc) | ||||
| 		{ | ||||
| 			return Interp(start, end, time, duration, Function.Get(easingFunc)); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 Lerp(Fix64 start, Fix64 end, Fix64 time) | ||||
| 		{ | ||||
| 			return (start + (end - start) * time); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 Interp(Fix64 start, Fix64 end, Fix64 time, Fix64 duration, System.Func<Fix64, Fix64> easingFunc) | ||||
| 		{ | ||||
| 			return Lerp(start, end, easingFunc(time / duration)); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 Interp(Fix64 start, Fix64 end, Fix64 time, Fix64 duration, MoonWorks.Math.Easing.Function.Fixed easingFunc) | ||||
| 		{ | ||||
| 			return Interp(start, end, time, duration, Function.Get(easingFunc)); | ||||
| 		} | ||||
| 
 | ||||
| 		/* FLOAT EASING FUNCTIONS */ | ||||
| 
 | ||||
| 		// LINEAR | ||||
| 
 | ||||
| 		public static float Linear(float t) | ||||
| 		{ | ||||
| 			return t; | ||||
| 		} | ||||
| 
 | ||||
| 		// QUADRATIC | ||||
| 
 | ||||
| 		public static float InQuad(float t) | ||||
| 		{ | ||||
| 			return t * t; | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutQuad(float t) | ||||
| 		{ | ||||
| 			return 1 - (1 - t) * (1 - t); | ||||
| 		} | ||||
| 
 | ||||
| 		public static float InOutQuad(float t) | ||||
| 		{ | ||||
| 			if (t < 0.5f) | ||||
| 			{ | ||||
| 				return 2 * t * t; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				var x = (-2 * t + 2); | ||||
| 				return 1 - ((x * x) / 2); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutInQuad(float t) => OutIn(OutQuad, InQuad, t); | ||||
| 
 | ||||
| 		// CUBIC | ||||
| 
 | ||||
| 		public static float InCubic(float t) | ||||
| 		{ | ||||
| 			return t * t * t; | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutCubic(float t) | ||||
| 		{ | ||||
| 			var x = 1 - t; | ||||
| 			return 1 - (x * x * x); | ||||
| 		} | ||||
| 
 | ||||
| 		public static float InOutCubic(float t) | ||||
| 		{ | ||||
| 			if (t < 0.5f) | ||||
| 			{ | ||||
| 				return 4 * t * t * t; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				var x = -2 * t + 2; | ||||
| 				return 1 - ((x * x * x) / 2); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutInCubic(float t) => OutIn(OutCubic, InCubic, t); | ||||
| 
 | ||||
| 		// QUARTIC | ||||
| 
 | ||||
| 		public static float InQuart(float t) | ||||
| 		{ | ||||
| 			return t * t * t * t; | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutQuart(float t) | ||||
| 		{ | ||||
| 			var x = 1 - t; | ||||
| 			return 1 - (x * x * x * x); | ||||
| 		} | ||||
| 
 | ||||
| 		public static float InOutQuart(float t) | ||||
| 		{ | ||||
| 			if (t < 0.5f) | ||||
| 			{ | ||||
| 				return 8 * t * t * t * t; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				var x = -2 * t + 2; | ||||
| 				return 1 - ((x * x * x * x) / 2); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutInQuart(float t) => OutIn(OutQuart, InQuart, t); | ||||
| 
 | ||||
| 		// QUINTIC | ||||
| 
 | ||||
| 		public static float InQuint(float t) | ||||
| 		{ | ||||
| 			return t * t * t * t * t; | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutQuint(float t) | ||||
| 		{ | ||||
| 			var x = 1 - t; | ||||
| 			return 1 - (x * x * x * x * x); | ||||
| 		} | ||||
| 
 | ||||
| 		public static float InOutQuint(float t) | ||||
| 		{ | ||||
| 			if (t < 0.5f) | ||||
| 			{ | ||||
| 				return 16 * t * t * t * t * t; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				var x = -2 * t + 2; | ||||
| 				return 1 - ((x * x * x * x * x) / 2); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutInQuint(float t) => OutIn(OutQuint, InQuint, t); | ||||
| 
 | ||||
| 		// SINE | ||||
| 
 | ||||
| 		public static float InSine(float t) | ||||
| 		{ | ||||
| 			return 1 - System.MathF.Cos((t * System.MathF.PI) / 2); | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutSine(float t) | ||||
| 		{ | ||||
| 			return System.MathF.Sin((t * System.MathF.PI) / 2); | ||||
| 		} | ||||
| 
 | ||||
| 		public static float InOutSine(float t) | ||||
| 		{ | ||||
| 			return -(System.MathF.Cos(System.MathF.PI * t) - 1) / 2; | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutInSine(float t) => OutIn(OutSine, InSine, t); | ||||
| 
 | ||||
| 		// EXPONENTIAL | ||||
| 
 | ||||
| 		public static float InExpo(float t) | ||||
| 		{ | ||||
| 			if (t == 0) | ||||
| 			{ | ||||
| 				return 0; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				return System.MathF.Pow(2, 10 * t - 10); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutExpo(float t) | ||||
| 		{ | ||||
| 			if (t == 1) | ||||
| 			{ | ||||
| 				return 1; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				return 1 - System.MathF.Pow(2, -10 * t); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float InOutExpo(float t) | ||||
| 		{ | ||||
| 			if (t == 0) | ||||
| 			{ | ||||
| 				return 0; | ||||
| 			} | ||||
| 			else if (t == 1) | ||||
| 			{ | ||||
| 				return 1; | ||||
| 			} | ||||
| 			else if (t < 0.5f) | ||||
| 			{ | ||||
| 				return System.MathF.Pow(2, 20 * t - 10) / 2; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				return (2 - System.MathF.Pow(2, -20 * t + 10)) / 2; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutInExpo(float t) => OutIn(OutExpo, InExpo, t); | ||||
| 
 | ||||
| 		// CIRCULAR | ||||
| 
 | ||||
| 		public static float InCirc(float t) | ||||
| 		{ | ||||
| 			return 1 - System.MathF.Sqrt(1 - (t * t)); | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutCirc(float t) | ||||
| 		{ | ||||
| 			return System.MathF.Sqrt(1 - ((t - 1) * (t - 1))); | ||||
| 		} | ||||
| 
 | ||||
| 		public static float InOutCirc(float t) | ||||
| 		{ | ||||
| 			if (t < 0.5f) | ||||
| 			{ | ||||
| 				return (1 - System.MathF.Sqrt(1 - ((2 * t) * (2 * t)))) / 2; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				var x = -2 * t + 2; | ||||
| 				return (System.MathF.Sqrt(1 - (x * x)) + 1) / 2; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutInCirc(float t) => OutIn(OutCirc, InCirc, t); | ||||
| 
 | ||||
| 		// BACK | ||||
| 
 | ||||
| 		public static float InBack(float t) | ||||
| 		{ | ||||
| 			return C3 * t * t * t - C1 * t * t; | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutBack(float t) | ||||
| 		{ | ||||
| 			return 1 + C3 * (t - 1) * (t - 1) * (t - 1) + C1 * (t - 1) * (t - 1); | ||||
| 		} | ||||
| 
 | ||||
| 		public static float InOutBack(float t) | ||||
| 		{ | ||||
| 			if (t < 0.5f) | ||||
| 			{ | ||||
| 				return ((2 * t) * (2 * t) * ((C2 + 1) * 2 * t - C2)) / 2; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				var x = 2 * t - 2; | ||||
| 				return ((t * t) * ((C2 + 1) * (x) + C2) + 2) / 2; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutInBack(float t) => OutIn(OutBack, InBack, t); | ||||
| 
 | ||||
| 		// ELASTIC | ||||
| 
 | ||||
| 		public static float InElastic(float t) | ||||
| 		{ | ||||
| 			if (t == 0) | ||||
| 			{ | ||||
| 				return 0; | ||||
| 			} | ||||
| 			else if (t == 1) | ||||
| 			{ | ||||
| 				return 1; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				return -System.MathF.Pow(2, 10 * t - 10) * System.MathF.Sin((t * 10 - 10.75f) * C4); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutElastic(float t) | ||||
| 		{ | ||||
| 			if (t == 0) | ||||
| 			{ | ||||
| 				return 0; | ||||
| 			} | ||||
| 			else if (t == 1) | ||||
| 			{ | ||||
| 				return 1; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				return System.MathF.Pow(2, -10 * t) * System.MathF.Sin((t * 10 - 0.75f) * C4) + 1; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float InOutElastic(float t) | ||||
| 		{ | ||||
| 			if (t == 0) | ||||
| 			{ | ||||
| 				return 0; | ||||
| 			} | ||||
| 			else if (t == 1) | ||||
| 			{ | ||||
| 				return 1; | ||||
| 			} | ||||
| 			else if (t < 0.5f) | ||||
| 			{ | ||||
| 				return -(System.MathF.Pow(2, 20 * t - 10) * System.MathF.Sin((20 * t - 11.125f) * C5)) / 2; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				return (System.MathF.Pow(2, -20 * t + 10) * System.MathF.Sin((20 * t - 11.125f) * C5)) / 2 + 1; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutInElastic(float t) => OutIn(OutElastic, InElastic, t); | ||||
| 
 | ||||
| 		// BOUNCE | ||||
| 
 | ||||
| 		public static float InBounce(float t) | ||||
| 		{ | ||||
| 			return 1 - OutBounce(1 - t); | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutBounce(float t) | ||||
| 		{ | ||||
| 			const float N1 = 7.5625f; | ||||
| 			const float D1 = 2.75f; | ||||
| 
 | ||||
| 			if (t < 1 / D1) | ||||
| 			{ | ||||
| 				return N1 * t * t; | ||||
| 			} | ||||
| 			else if (t < 2 / D1) { | ||||
| 				return N1 * (t -= 1.5f / D1) * t + 0.75f; | ||||
| 			} | ||||
| 			else if (t < 2.5f / D1) | ||||
| 			{ | ||||
| 				return N1 * (t -= 2.25f / D1) * t + 0.9375f; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				return N1 * (t -= 2.625f / D1) * t + 0.984375f; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float InOutBounce(float t) | ||||
| 		{ | ||||
| 			if (t < 0.5f) | ||||
| 			{ | ||||
| 				return (1 - OutBounce(1 - 2 * t)) / 2; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				return (1 + OutBounce(2 * t - 1)) / 2; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static float OutInBounce(float t) => OutIn(OutBounce, InBounce, t); | ||||
| 
 | ||||
| 		/* FIXED EASING FUNCTIONS */ | ||||
| 
 | ||||
| 		// LINEAR | ||||
| 
 | ||||
| 		public static Fix64 Linear(Fix64 t) | ||||
| 		{ | ||||
| 			return t; | ||||
| 		} | ||||
| 
 | ||||
| 		// QUADRATIC | ||||
| 
 | ||||
| 		public static Fix64 InQuad(Fix64 t) | ||||
| 		{ | ||||
| 			return t * t; | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutQuad(Fix64 t) | ||||
| 		{ | ||||
| 			return 1 - (1 - t) * (1 - t); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 InOutQuad(Fix64 t) | ||||
| 		{ | ||||
| 			if (t < HALF) | ||||
| 			{ | ||||
| 				return 2 * t * t; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				var x = (-2 * t + 2); | ||||
| 				return 1 - ((x * x) / 2); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutInQuad(Fix64 t) => OutIn(OutQuad, InQuad, t); | ||||
| 
 | ||||
| 		// CUBIC | ||||
| 
 | ||||
| 		public static Fix64 InCubic(Fix64 t) | ||||
| 		{ | ||||
| 			return t * t * t; | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutCubic(Fix64 t) | ||||
| 		{ | ||||
| 			var x = 1 - t; | ||||
| 			return 1 - (x * x * x); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 InOutCubic(Fix64 t) | ||||
| 		{ | ||||
| 			if (t < HALF) | ||||
| 			{ | ||||
| 				return 4 * t * t * t; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				var x = -2 * t + 2; | ||||
| 				return 1 - ((x * x * x) / 2); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutInCubic(Fix64 t) => OutIn(OutCubic, InCubic, t); | ||||
| 
 | ||||
| 		// QUARTIC | ||||
| 
 | ||||
| 		public static Fix64 InQuart(Fix64 t) | ||||
| 		{ | ||||
| 			return t * t * t * t; | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutQuart(Fix64 t) | ||||
| 		{ | ||||
| 			var x = 1 - t; | ||||
| 			return 1 - (x * x * x * x); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 InOutQuart(Fix64 t) | ||||
| 		{ | ||||
| 			if (t < HALF) | ||||
| 			{ | ||||
| 				return 8 * t * t * t * t; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				var x = -2 * t + 2; | ||||
| 				return 1 - ((x * x * x * x) / 2); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutInQuart(Fix64 t) => OutIn(OutQuart, InQuart, t); | ||||
| 
 | ||||
| 		// QUINTIC | ||||
| 
 | ||||
| 		public static Fix64 InQuint(Fix64 t) | ||||
| 		{ | ||||
| 			return t * t * t * t * t; | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutQuint(Fix64 t) | ||||
| 		{ | ||||
| 			var x = 1 - t; | ||||
| 			return 1 - (x * x * x * x * x); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 InOutQuint(Fix64 t) | ||||
| 		{ | ||||
| 			if (t < HALF) | ||||
| 			{ | ||||
| 				return 16 * t * t * t * t * t; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				var x = -2 * t + 2; | ||||
| 				return 1 - ((x * x * x * x * x) / 2); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutInQuint(Fix64 t) => OutIn(OutQuint, InQuint, t); | ||||
| 
 | ||||
| 		// SINE | ||||
| 
 | ||||
| 		public static Fix64 InSine(Fix64 t) | ||||
| 		{ | ||||
| 			return 1 - Fix64.Cos((t * Fix64.Pi) / 2); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutSine(Fix64 t) | ||||
| 		{ | ||||
| 			return Fix64.Sin((t * Fix64.Pi) / 2); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 InOutSine(Fix64 t) | ||||
| 		{ | ||||
| 			return -(Fix64.Cos(Fix64.Pi * t) - 1) / 2; | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutInSine(Fix64 t) => OutIn(OutSine, InSine, t); | ||||
| 
 | ||||
| 		// CIRCULAR | ||||
| 
 | ||||
| 		public static Fix64 InCirc(Fix64 t) | ||||
| 		{ | ||||
| 			return 1 - Fix64.Sqrt(1 - (t * t)); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutCirc(Fix64 t) | ||||
| 		{ | ||||
| 			return Fix64.Sqrt(1 - ((t - 1) * (t - 1))); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 InOutCirc(Fix64 t) | ||||
| 		{ | ||||
| 			if (t < HALF) | ||||
| 			{ | ||||
| 				return (1 - Fix64.Sqrt(1 - ((2 * t) * (2 * t)))) / 2; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				var x = -2 * t + 2; | ||||
| 				return (Fix64.Sqrt(1 - (x * x)) + 1) / 2; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutInCirc(Fix64 t) => OutIn(OutCirc, InCirc, t); | ||||
| 
 | ||||
| 		// BACK | ||||
| 
 | ||||
| 		public static Fix64 InBack(Fix64 t) | ||||
| 		{ | ||||
| 			return FIXED_C3 * t * t * t - FIXED_C1 * t * t; | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutBack(Fix64 t) | ||||
| 		{ | ||||
| 			return 1 + FIXED_C3 * (t - 1) * (t - 1) * (t - 1) + FIXED_C1 * (t - 1) * (t - 1); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 InOutBack(Fix64 t) | ||||
| 		{ | ||||
| 			if (t < HALF) | ||||
| 			{ | ||||
| 				return ((2 * t) * (2 * t) * ((FIXED_C2 + 1) * 2 * t - FIXED_C2)) / 2; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				var x = 2 * t - 2; | ||||
| 				return ((t * t) * ((FIXED_C2 + 1) * (x) + FIXED_C2) + 2) / 2; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutInBack(Fix64 t) => OutIn(OutBack, InBack, t); | ||||
| 
 | ||||
| 		// BOUNCE | ||||
| 
 | ||||
| 		public static Fix64 InBounce(Fix64 t) | ||||
| 		{ | ||||
| 			return 1 - OutBounce(1 - t); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutBounce(Fix64 t) | ||||
| 		{ | ||||
| 			if (t < 1 / FIXED_D1) | ||||
| 			{ | ||||
| 				return FIXED_N1 * t * t; | ||||
| 			} | ||||
| 			else if (t < 2 / FIXED_D1) { | ||||
| 				return FIXED_N1 * (t -= Fix64.FromFraction(3, 2) / FIXED_D1) * t + Fix64.FromFraction(3, 4); | ||||
| 			} | ||||
| 			else if (t < Fix64.FromFraction(5, 2) / FIXED_D1) | ||||
| 			{ | ||||
| 				return FIXED_N1 * (t -= Fix64.FromFraction(9, 4) / FIXED_D1) * t + Fix64.FromFraction(15, 16); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				return FIXED_N1 * (t -= Fix64.FromFraction(181, 80) / FIXED_D1) * t + Fix64.FromFraction(63, 64); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 InOutBounce(Fix64 t) | ||||
| 		{ | ||||
| 			if (t < HALF) | ||||
| 			{ | ||||
| 				return (1 - OutBounce(1 - 2 * t)) / 2; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				return (1 + OutBounce(2 * t - 1)) / 2; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 OutInBounce(Fix64 t) => OutIn(OutBounce, InBounce, t); | ||||
| 
 | ||||
| 		public static class Function | ||||
| 		{ | ||||
| 			public enum Float | ||||
| 			{ | ||||
| 				Linear, | ||||
| 				InQuad, | ||||
| 				OutQuad, | ||||
| 				InOutQuad, | ||||
| 				OutInQuad, | ||||
| 				InCubic, | ||||
| 				OutCubic, | ||||
| 				InOutCubic, | ||||
| 				OutInCubic, | ||||
| 				InQuart, | ||||
| 				OutQuart, | ||||
| 				InOutQuart, | ||||
| 				OutInQuart, | ||||
| 				InQuint, | ||||
| 				OutQuint, | ||||
| 				InOutQuint, | ||||
| 				OutInQuint, | ||||
| 				InSine, | ||||
| 				OutSine, | ||||
| 				InOutSine, | ||||
| 				OutInSine, | ||||
| 				InExpo, | ||||
| 				OutExpo, | ||||
| 				InOutExpo, | ||||
| 				OutInExpo, | ||||
| 				InCirc, | ||||
| 				OutCirc, | ||||
| 				InOutCirc, | ||||
| 				OutInCirc, | ||||
| 				InElastic, | ||||
| 				OutElastic, | ||||
| 				InOutElastic, | ||||
| 				OutInElastic, | ||||
| 				InBack, | ||||
| 				OutBack, | ||||
| 				InOutBack, | ||||
| 				OutInBack, | ||||
| 				InBounce, | ||||
| 				OutBounce, | ||||
| 				InOutBounce, | ||||
| 				OutInBounce | ||||
| 			} | ||||
| 
 | ||||
| 			public enum Fixed | ||||
| 			{ | ||||
| 				Linear, | ||||
| 				InQuad, | ||||
| 				OutQuad, | ||||
| 				InOutQuad, | ||||
| 				OutInQuad, | ||||
| 				InCubic, | ||||
| 				OutCubic, | ||||
| 				InOutCubic, | ||||
| 				OutInCubic, | ||||
| 				InQuart, | ||||
| 				OutQuart, | ||||
| 				InOutQuart, | ||||
| 				OutInQuart, | ||||
| 				InQuint, | ||||
| 				OutQuint, | ||||
| 				InOutQuint, | ||||
| 				OutInQuint, | ||||
| 				InSine, | ||||
| 				OutSine, | ||||
| 				InOutSine, | ||||
| 				OutInSine, | ||||
| 				InCirc, | ||||
| 				OutCirc, | ||||
| 				InOutCirc, | ||||
| 				OutInCirc, | ||||
| 				InBack, | ||||
| 				OutBack, | ||||
| 				InOutBack, | ||||
| 				OutInBack, | ||||
| 				InBounce, | ||||
| 				OutBounce, | ||||
| 				InOutBounce, | ||||
| 				OutInBounce | ||||
| 			} | ||||
| 
 | ||||
| 			private static Dictionary<Float, System.Func<float, float>> FloatLookup = new Dictionary<Float, System.Func<float, float>> | ||||
| 			{ | ||||
| 				{ Float.Linear, Linear }, | ||||
| 				{ Float.InQuad, InQuad }, | ||||
| 				{ Float.OutQuad, OutQuad }, | ||||
| 				{ Float.InOutQuad, InOutQuad }, | ||||
| 				{ Float.OutInQuad, OutInQuad }, | ||||
| 				{ Float.InCubic, InCubic }, | ||||
| 				{ Float.OutCubic, OutCubic }, | ||||
| 				{ Float.InOutCubic, InOutCubic }, | ||||
| 				{ Float.OutInCubic, OutInCubic }, | ||||
| 				{ Float.InQuart, InQuart }, | ||||
| 				{ Float.OutQuart, OutQuart }, | ||||
| 				{ Float.InOutQuart, InOutQuart }, | ||||
| 				{ Float.OutInQuart, OutInQuart }, | ||||
| 				{ Float.InQuint, InQuint }, | ||||
| 				{ Float.OutQuint, OutQuint }, | ||||
| 				{ Float.InOutQuint, InOutQuint }, | ||||
| 				{ Float.OutInQuint, OutInQuint }, | ||||
| 				{ Float.InSine, InSine }, | ||||
| 				{ Float.OutSine, OutSine }, | ||||
| 				{ Float.InOutSine, InOutSine }, | ||||
| 				{ Float.OutInSine, OutInSine }, | ||||
| 				{ Float.InExpo, InExpo }, | ||||
| 				{ Float.OutExpo, OutExpo }, | ||||
| 				{ Float.InOutExpo, InOutExpo }, | ||||
| 				{ Float.OutInExpo, OutInExpo }, | ||||
| 				{ Float.InCirc, InCirc }, | ||||
| 				{ Float.OutCirc, OutCirc }, | ||||
| 				{ Float.InOutCirc, InOutCirc }, | ||||
| 				{ Float.OutInCirc, OutInCirc }, | ||||
| 				{ Float.InElastic, InElastic }, | ||||
| 				{ Float.OutElastic, OutElastic }, | ||||
| 				{ Float.InOutElastic, InOutElastic }, | ||||
| 				{ Float.OutInElastic, OutInElastic }, | ||||
| 				{ Float.InBack, InBack }, | ||||
| 				{ Float.OutBack, OutBack }, | ||||
| 				{ Float.InOutBack, InOutBack }, | ||||
| 				{ Float.OutInBack, OutInBack }, | ||||
| 				{ Float.InBounce, InBounce }, | ||||
| 				{ Float.OutBounce, OutBounce }, | ||||
| 				{ Float.InOutBounce, InOutBounce }, | ||||
| 				{ Float.OutInBounce, OutInBounce } | ||||
| 			}; | ||||
| 
 | ||||
| 			private static Dictionary<Fixed, System.Func<Fix64, Fix64>> FixedLookup = new Dictionary<Fixed, System.Func<Fix64, Fix64>> | ||||
| 			{ | ||||
| 				{ Fixed.Linear, Linear }, | ||||
| 				{ Fixed.InQuad, InQuad }, | ||||
| 				{ Fixed.OutQuad, OutQuad }, | ||||
| 				{ Fixed.InOutQuad, InOutQuad }, | ||||
| 				{ Fixed.OutInQuad, OutInQuad }, | ||||
| 				{ Fixed.InCubic, InCubic }, | ||||
| 				{ Fixed.OutCubic, OutCubic }, | ||||
| 				{ Fixed.InOutCubic, InOutCubic }, | ||||
| 				{ Fixed.OutInCubic, OutInCubic }, | ||||
| 				{ Fixed.InQuart, InQuart }, | ||||
| 				{ Fixed.OutQuart, OutQuart }, | ||||
| 				{ Fixed.InOutQuart, InOutQuart }, | ||||
| 				{ Fixed.OutInQuart, OutInQuart }, | ||||
| 				{ Fixed.InQuint, InQuint }, | ||||
| 				{ Fixed.OutQuint, OutQuint }, | ||||
| 				{ Fixed.InOutQuint, InOutQuint }, | ||||
| 				{ Fixed.OutInQuint, OutInQuint }, | ||||
| 				{ Fixed.InSine, InSine }, | ||||
| 				{ Fixed.OutSine, OutSine }, | ||||
| 				{ Fixed.InOutSine, InOutSine }, | ||||
| 				{ Fixed.OutInSine, OutInSine }, | ||||
| 				{ Fixed.InCirc, InCirc }, | ||||
| 				{ Fixed.OutCirc, OutCirc }, | ||||
| 				{ Fixed.InOutCirc, InOutCirc }, | ||||
| 				{ Fixed.OutInCirc, OutInCirc }, | ||||
| 				{ Fixed.InBack, InBack }, | ||||
| 				{ Fixed.OutBack, OutBack }, | ||||
| 				{ Fixed.InOutBack, InOutBack }, | ||||
| 				{ Fixed.OutInBack, OutInBack }, | ||||
| 				{ Fixed.InBounce, InBounce }, | ||||
| 				{ Fixed.OutBounce, OutBounce }, | ||||
| 				{ Fixed.InOutBounce, InOutBounce }, | ||||
| 				{ Fixed.OutInBounce, OutInBounce } | ||||
| 			}; | ||||
| 
 | ||||
| 			public static System.Func<float, float> Get(Float functionEnum) | ||||
| 			{ | ||||
| 				return FloatLookup[functionEnum]; | ||||
| 			} | ||||
| 
 | ||||
| 			public static System.Func<Fix64, Fix64> Get(Fixed functionEnum) | ||||
| 			{ | ||||
| 				return FixedLookup[functionEnum]; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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> | ||||
|  | @ -117,27 +105,27 @@ namespace MoonWorks.Math.Fixed | |||
| 			return new Fix64((value.RawValue + mask) ^ mask); | ||||
| 		} | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Returns the largest integral value less than or equal to the specified number. | ||||
|         /// </summary> | ||||
| 		/// <summary> | ||||
| 		/// Returns the largest integral value less than or equal to the specified number. | ||||
| 		/// </summary> | ||||
| 		public static Fix64 Floor(Fix64 value) | ||||
| 		{ | ||||
| 			// Zero out the fractional part. | ||||
| 			return new Fix64((long)((ulong)value.RawValue & 0xFFFFFFFF00000000)); | ||||
| 		} | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Returns the smallest integral value that is greater than or equal to the specified number. | ||||
|         /// </summary> | ||||
| 		/// <summary> | ||||
| 		/// Returns the smallest integral value that is greater than or equal to the specified number. | ||||
| 		/// </summary> | ||||
| 		public static Fix64 Ceiling(Fix64 value) | ||||
| 		{ | ||||
| 			return value.IsFractional ? Floor(value) + One : value; | ||||
| 		} | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Rounds to the nearest integral value. | ||||
|         /// If the value is halfway between an even and an uneven value, returns the even value. | ||||
|         /// </summary> | ||||
| 		/// <summary> | ||||
| 		/// Rounds to the nearest integral value. | ||||
| 		/// If the value is halfway between an even and an uneven value, returns the even value. | ||||
| 		/// </summary> | ||||
| 		public static Fix64 Round(Fix64 value) | ||||
| 		{ | ||||
| 			var fractionalPart = value.RawValue & 0x00000000FFFFFFFF; | ||||
|  | @ -204,7 +192,153 @@ namespace MoonWorks.Math.Fixed | |||
| 			return ((value - min) * (newMax - newMin)) / (max - min) + newMin; | ||||
| 		} | ||||
| 
 | ||||
| 		// Trigonometry functions | ||||
| 		/// <summary> | ||||
| 		/// Returns Floor(value / step) * step. Nearest multiple of step to a specified value | ||||
| 		/// </summary> | ||||
| 		public static Fix64 Step(Fix64 value, Fix64 step) | ||||
| 		{ | ||||
| 			return Fix64.Floor(value / step) * step; | ||||
| 		} | ||||
| 
 | ||||
| 		// 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. | ||||
|  | @ -531,6 +665,16 @@ namespace MoonWorks.Math.Fixed | |||
| 			return new Fix64(sum); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator +(Fix64 x, int y) | ||||
| 		{ | ||||
| 			return x + new Fix64(y); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator +(int x, Fix64 y) | ||||
| 		{ | ||||
| 			return new Fix64(x) + y; | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator -(Fix64 x, Fix64 y) | ||||
| 		{ | ||||
| 			var xl = x.RawValue; | ||||
|  | @ -544,6 +688,16 @@ namespace MoonWorks.Math.Fixed | |||
| 			return new Fix64(diff); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator -(Fix64 x, int y) | ||||
| 		{ | ||||
| 			return x - new Fix64(y); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator -(int x, Fix64 y) | ||||
| 		{ | ||||
| 			return new Fix64(x) - y; | ||||
| 		} | ||||
| 
 | ||||
| 		[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||||
| 		private static Fix64 FastSub(Fix64 x, Fix64 y) | ||||
| 		{ | ||||
|  | @ -668,6 +822,16 @@ namespace MoonWorks.Math.Fixed | |||
| 			return result; | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator *(Fix64 x, int y) | ||||
| 		{ | ||||
| 			return x * new Fix64(y); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator *(int x, Fix64 y) | ||||
| 		{ | ||||
| 			return new Fix64(x) * y; | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator /(Fix64 x, Fix64 y) | ||||
| 		{ | ||||
| 			var xl = x.RawValue; | ||||
|  | @ -726,6 +890,16 @@ namespace MoonWorks.Math.Fixed | |||
| 			return new Fix64(result); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator /(Fix64 x, int y) | ||||
| 		{ | ||||
| 			return x / new Fix64(y); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator /(int x, Fix64 y) | ||||
| 		{ | ||||
| 			return new Fix64(x) / y; | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator %(Fix64 x, Fix64 y) | ||||
| 		{ | ||||
| 			return new Fix64( | ||||
|  | @ -734,6 +908,16 @@ namespace MoonWorks.Math.Fixed | |||
| 				x.RawValue % y.RawValue); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator %(Fix64 x, int y) | ||||
| 		{ | ||||
| 			return x % new Fix64(y); | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator %(int x, Fix64 y) | ||||
| 		{ | ||||
| 			return new Fix64(x) % y; | ||||
| 		} | ||||
| 
 | ||||
| 		public static Fix64 operator -(Fix64 x) | ||||
| 		{ | ||||
| 			return x.RawValue == MIN_VALUE ? MaxValue : new Fix64(-x.RawValue); | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -84,6 +84,12 @@ namespace MoonWorks | |||
| 		[ModuleInitializer] | ||||
| 		public static void Init() | ||||
| 		{ | ||||
| 			// Ignore NativeAOT platforms since they don't perform dynamic loading. | ||||
| 			if (!RuntimeFeature.IsDynamicCodeSupported) | ||||
| 			{ | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			// Get the platform and architecture | ||||
| 			string os = GetPlatformName(); | ||||
| 			string cpu = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); | ||||
|  | @ -91,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)) | ||||
| 			{ | ||||
|  | @ -192,11 +194,7 @@ namespace MoonWorks | |||
| 			} | ||||
| 
 | ||||
| 			// Set the resolver callback for our native assemblies | ||||
| 			NativeLibrary.SetDllImportResolver(typeof(SDL2.SDL).Assembly, MapAndLoad); | ||||
| 			NativeLibrary.SetDllImportResolver(typeof(RefreshCS.Refresh).Assembly, MapAndLoad); | ||||
| 			NativeLibrary.SetDllImportResolver(typeof(FAudio).Assembly, MapAndLoad); | ||||
| 			NativeLibrary.SetDllImportResolver(typeof(WellspringCS.Wellspring).Assembly, MapAndLoad); | ||||
| 			NativeLibrary.SetDllImportResolver(typeof(Theorafile).Assembly, MapAndLoad); | ||||
| 			NativeLibrary.SetDllImportResolver(assembly, MapAndLoad); | ||||
| 		} | ||||
| 
 | ||||
| 		#endregion | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| 	public enum ScreenMode | ||||
| 	{ | ||||
| 		Fullscreen, | ||||
| 		BorderlessWindow, | ||||
| 		BorderlessFullscreen, | ||||
| 		Windowed | ||||
| 	} | ||||
| } | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -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; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,357 +0,0 @@ | |||
| /* Heavily based on https://github.com/FNA-XNA/FNA/blob/master/src/Media/Xiph/VideoPlayer.cs */ | ||||
| using System; | ||||
| using System.Diagnostics; | ||||
| using System.Runtime.InteropServices; | ||||
| using MoonWorks.Audio; | ||||
| using MoonWorks.Graphics; | ||||
| 
 | ||||
| namespace MoonWorks.Video | ||||
| { | ||||
| 	public enum VideoState | ||||
| 	{ | ||||
| 		Playing, | ||||
| 		Paused, | ||||
| 		Stopped | ||||
| 	} | ||||
| 
 | ||||
| 	public unsafe class Video : IDisposable | ||||
| 	{ | ||||
| 		internal IntPtr Handle; | ||||
| 
 | ||||
| 		public bool Loop { get; private set; } | ||||
| 		public float Volume { | ||||
| 			get => volume; | ||||
| 			set | ||||
| 			{ | ||||
| 				volume = value; | ||||
| 				if (audioStream != null) | ||||
| 				{ | ||||
| 					audioStream.Volume = value; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		public float PlaybackSpeed { get; set; } | ||||
| 		public double FramesPerSecond => fps; | ||||
| 		private VideoState State = VideoState.Stopped; | ||||
| 
 | ||||
| 		private double fps; | ||||
| 		private int yWidth; | ||||
| 		private int yHeight; | ||||
| 		private int uvWidth; | ||||
| 		private int uvHeight; | ||||
| 
 | ||||
| 		private void* yuvData = null; | ||||
| 		private int yuvDataLength; | ||||
| 		private int currentFrame; | ||||
| 
 | ||||
| 		private GraphicsDevice GraphicsDevice; | ||||
| 		private Texture RenderTexture = null; | ||||
| 		private Texture yTexture = null; | ||||
| 		private Texture uTexture = null; | ||||
| 		private Texture vTexture = null; | ||||
| 		private Sampler LinearSampler; | ||||
| 
 | ||||
| 		private AudioDevice AudioDevice = null; | ||||
| 		private StreamingSoundTheora audioStream = null; | ||||
| 		private float volume = 1.0f; | ||||
| 
 | ||||
| 		private Stopwatch timer; | ||||
| 		private double lastTimestamp; | ||||
| 		private double timeElapsed; | ||||
| 
 | ||||
| 		private bool disposed; | ||||
| 
 | ||||
| 		/* TODO: is there some way for us to load the data into memory? */ | ||||
| 		public Video(GraphicsDevice graphicsDevice, AudioDevice audioDevice, string filename) | ||||
| 		{ | ||||
| 			GraphicsDevice = graphicsDevice; | ||||
| 			AudioDevice = audioDevice; | ||||
| 
 | ||||
| 			if (!System.IO.File.Exists(filename)) | ||||
| 			{ | ||||
| 				throw new ArgumentException("Video file not found!"); | ||||
| 			} | ||||
| 
 | ||||
| 			if (Theorafile.tf_fopen(filename, out Handle) < 0) | ||||
| 			{ | ||||
| 				throw new ArgumentException("Invalid video file!"); | ||||
| 			} | ||||
| 
 | ||||
| 			Theorafile.th_pixel_fmt format; | ||||
| 			Theorafile.tf_videoinfo( | ||||
| 				Handle, | ||||
| 				out yWidth, | ||||
| 				out yHeight, | ||||
| 				out fps, | ||||
| 				out format | ||||
| 			); | ||||
| 
 | ||||
| 			if (format == Theorafile.th_pixel_fmt.TH_PF_420) | ||||
| 			{ | ||||
| 				uvWidth = yWidth / 2; | ||||
| 				uvHeight = yHeight / 2; | ||||
| 			} | ||||
| 			else if (format == Theorafile.th_pixel_fmt.TH_PF_422) | ||||
| 			{ | ||||
| 				uvWidth = yWidth / 2; | ||||
| 				uvHeight = yHeight; | ||||
| 			} | ||||
| 			else if (format == Theorafile.th_pixel_fmt.TH_PF_444) | ||||
| 			{ | ||||
| 				uvWidth = yWidth; | ||||
| 				uvHeight = yHeight; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				throw new NotSupportedException("Unrecognized YUV format!"); | ||||
| 			} | ||||
| 
 | ||||
| 			yuvDataLength = ( | ||||
| 				(yWidth * yHeight) + | ||||
| 				(uvWidth * uvHeight * 2) | ||||
| 			); | ||||
| 
 | ||||
| 			yuvData = NativeMemory.Alloc((nuint) yuvDataLength); | ||||
| 
 | ||||
| 			InitializeTheoraStream(); | ||||
| 
 | ||||
| 			if (Theorafile.tf_hasvideo(Handle) == 1) | ||||
| 			{ | ||||
| 				RenderTexture = Texture.CreateTexture2D( | ||||
| 					GraphicsDevice, | ||||
| 					(uint) yWidth, | ||||
| 					(uint) yHeight, | ||||
| 					TextureFormat.R8G8B8A8, | ||||
| 					TextureUsageFlags.ColorTarget | TextureUsageFlags.Sampler | ||||
| 				); | ||||
| 
 | ||||
| 				yTexture = Texture.CreateTexture2D( | ||||
| 					GraphicsDevice, | ||||
| 					(uint) yWidth, | ||||
| 					(uint) yHeight, | ||||
| 					TextureFormat.R8, | ||||
| 					TextureUsageFlags.Sampler | ||||
| 				); | ||||
| 
 | ||||
| 				uTexture = Texture.CreateTexture2D( | ||||
| 					GraphicsDevice, | ||||
| 					(uint) uvWidth, | ||||
| 					(uint) uvHeight, | ||||
| 					TextureFormat.R8, | ||||
| 					TextureUsageFlags.Sampler | ||||
| 				); | ||||
| 
 | ||||
| 				vTexture = Texture.CreateTexture2D( | ||||
| 					GraphicsDevice, | ||||
| 					(uint) uvWidth, | ||||
| 					(uint) uvHeight, | ||||
| 					TextureFormat.R8, | ||||
| 					TextureUsageFlags.Sampler | ||||
| 				); | ||||
| 
 | ||||
| 				LinearSampler = new Sampler(GraphicsDevice, SamplerCreateInfo.LinearClamp); | ||||
| 			} | ||||
| 
 | ||||
| 			timer = new Stopwatch(); | ||||
| 		} | ||||
| 
 | ||||
| 		public void Play(bool loop = false) | ||||
| 		{ | ||||
| 			if (State == VideoState.Playing) | ||||
| 			{ | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			Loop = loop; | ||||
| 			timer.Start(); | ||||
| 
 | ||||
| 			if (audioStream != null) | ||||
| 			{ | ||||
| 				audioStream.Play(); | ||||
| 			} | ||||
| 
 | ||||
| 			State = VideoState.Playing; | ||||
| 		} | ||||
| 
 | ||||
| 		public void Pause() | ||||
| 		{ | ||||
| 			if (State != VideoState.Playing) | ||||
| 			{ | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			timer.Stop(); | ||||
| 
 | ||||
| 			if (audioStream != null) | ||||
| 			{ | ||||
| 				audioStream.Pause(); | ||||
| 			} | ||||
| 
 | ||||
| 			State = VideoState.Paused; | ||||
| 		} | ||||
| 
 | ||||
| 		public void Stop() | ||||
| 		{ | ||||
| 			if (State == VideoState.Stopped) | ||||
| 			{ | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			timer.Stop(); | ||||
| 			timer.Reset(); | ||||
| 
 | ||||
| 			Theorafile.tf_reset(Handle); | ||||
| 			lastTimestamp = 0; | ||||
| 			timeElapsed = 0; | ||||
| 
 | ||||
| 			if (audioStream != null) | ||||
| 			{ | ||||
| 				audioStream.StopImmediate(); | ||||
| 				audioStream.Dispose(); | ||||
| 				audioStream = null; | ||||
| 			} | ||||
| 
 | ||||
| 			State = VideoState.Stopped; | ||||
| 		} | ||||
| 
 | ||||
| 		public Texture GetTexture() | ||||
| 		{ | ||||
| 			if (RenderTexture == null) | ||||
| 			{ | ||||
| 				throw new InvalidOperationException(); | ||||
| 			} | ||||
| 
 | ||||
| 			if (State == VideoState.Stopped) | ||||
| 			{ | ||||
| 				return RenderTexture; | ||||
| 			} | ||||
| 
 | ||||
| 			timeElapsed += (timer.Elapsed.TotalMilliseconds - lastTimestamp) * PlaybackSpeed; | ||||
| 			lastTimestamp = timer.Elapsed.TotalMilliseconds; | ||||
| 
 | ||||
| 			int thisFrame = ((int) (timeElapsed / (1000.0 / FramesPerSecond))); | ||||
| 			if (thisFrame > currentFrame) | ||||
| 			{ | ||||
| 				if (Theorafile.tf_readvideo( | ||||
| 					Handle, | ||||
| 					(IntPtr) yuvData, | ||||
| 					thisFrame - currentFrame | ||||
| 				) == 1 || currentFrame == -1) { | ||||
| 					UpdateTexture(); | ||||
| 				} | ||||
| 
 | ||||
| 				currentFrame = thisFrame; | ||||
| 			} | ||||
| 
 | ||||
| 			bool ended = Theorafile.tf_eos(Handle) == 1; | ||||
| 			if (ended) | ||||
| 			{ | ||||
| 				timer.Stop(); | ||||
| 				timer.Reset(); | ||||
| 
 | ||||
| 				if (audioStream != null) | ||||
| 				{ | ||||
| 					audioStream.Stop(); | ||||
| 					audioStream.Dispose(); | ||||
| 					audioStream = null; | ||||
| 				} | ||||
| 
 | ||||
| 				Theorafile.tf_reset(Handle); | ||||
| 
 | ||||
| 				if (Loop) | ||||
| 				{ | ||||
| 					// Start over! | ||||
| 					InitializeTheoraStream(); | ||||
| 
 | ||||
| 					timer.Start(); | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					State = VideoState.Stopped; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			return RenderTexture; | ||||
| 		} | ||||
| 
 | ||||
| 		private void UpdateTexture() | ||||
| 		{ | ||||
| 			var commandBuffer = GraphicsDevice.AcquireCommandBuffer(); | ||||
| 
 | ||||
| 			commandBuffer.SetTextureDataYUV( | ||||
| 				yTexture, | ||||
| 				uTexture, | ||||
| 				vTexture, | ||||
| 				(IntPtr) yuvData, | ||||
| 				(uint) yuvDataLength | ||||
| 			); | ||||
| 
 | ||||
| 			commandBuffer.BeginRenderPass( | ||||
| 				new ColorAttachmentInfo(RenderTexture, Color.Black) | ||||
| 			); | ||||
| 
 | ||||
| 			commandBuffer.BindGraphicsPipeline(GraphicsDevice.VideoPipeline); | ||||
| 			commandBuffer.BindFragmentSamplers( | ||||
| 				new TextureSamplerBinding(yTexture, LinearSampler), | ||||
| 				new TextureSamplerBinding(uTexture, LinearSampler), | ||||
| 				new TextureSamplerBinding(vTexture, LinearSampler) | ||||
| 			); | ||||
| 
 | ||||
| 			commandBuffer.DrawPrimitives(0, 1, 0, 0); | ||||
| 
 | ||||
| 			commandBuffer.EndRenderPass(); | ||||
| 
 | ||||
| 			GraphicsDevice.Submit(commandBuffer); | ||||
| 		} | ||||
| 
 | ||||
| 		private void InitializeTheoraStream() | ||||
| 		{ | ||||
| 			// Grab the first video frame ASAP. | ||||
| 			while (Theorafile.tf_readvideo(Handle, (IntPtr) yuvData, 1) == 0); | ||||
| 
 | ||||
| 			// Grab the first bit of audio. We're trying to start the decoding ASAP. | ||||
| 			if (AudioDevice != null && Theorafile.tf_hasaudio(Handle) == 1) | ||||
| 			{ | ||||
| 				int channels, sampleRate; | ||||
| 				Theorafile.tf_audioinfo(Handle, out channels, out sampleRate); | ||||
| 				audioStream = new StreamingSoundTheora(AudioDevice, Handle, channels, (uint) sampleRate); | ||||
| 			} | ||||
| 
 | ||||
| 			currentFrame = -1; | ||||
| 		} | ||||
| 
 | ||||
| 		protected virtual void Dispose(bool disposing) | ||||
| 		{ | ||||
| 			if (!disposed) | ||||
| 			{ | ||||
| 				if (disposing) | ||||
| 				{ | ||||
| 					// dispose managed state (managed objects) | ||||
| 					RenderTexture.Dispose(); | ||||
| 					yTexture.Dispose(); | ||||
| 					uTexture.Dispose(); | ||||
| 					vTexture.Dispose(); | ||||
| 				} | ||||
| 
 | ||||
| 				// free unmanaged resources (unmanaged objects) | ||||
| 				Theorafile.tf_close(ref Handle); | ||||
| 				NativeMemory.Free(yuvData); | ||||
| 
 | ||||
| 				disposed = true; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		~Video() | ||||
| 		{ | ||||
| 		    // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method | ||||
| 		    Dispose(disposing: false); | ||||
| 		} | ||||
| 
 | ||||
| 		public void Dispose() | ||||
| 		{ | ||||
| 			// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method | ||||
| 			Dispose(disposing: true); | ||||
| 			GC.SuppressFinalize(this); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,330 @@ | |||
| using System; | ||||
| using System.Diagnostics; | ||||
| using System.Threading.Tasks; | ||||
| using MoonWorks.Graphics; | ||||
| 
 | ||||
| namespace MoonWorks.Video | ||||
| { | ||||
| 	/// <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 PlaybackSpeed { get; set; } = 1; | ||||
| 
 | ||||
| 		private VideoAV1 Video = null; | ||||
| 		private VideoAV1Stream CurrentStream = null; | ||||
| 
 | ||||
| 		private Task ReadNextFrameTask; | ||||
| 		private Task ResetStreamATask; | ||||
| 		private Task ResetStreamBTask; | ||||
| 
 | ||||
| 		private GraphicsDevice GraphicsDevice; | ||||
| 		private Texture yTexture = null; | ||||
| 		private Texture uTexture = null; | ||||
| 		private Texture vTexture = null; | ||||
| 		private Sampler LinearSampler; | ||||
| 
 | ||||
| 		private int currentFrame; | ||||
| 
 | ||||
| 		private Stopwatch timer; | ||||
| 		private double lastTimestamp; | ||||
| 		private double timeElapsed; | ||||
| 
 | ||||
| 		public VideoPlayer(GraphicsDevice device) : base(device) | ||||
| 		{ | ||||
| 			GraphicsDevice = device; | ||||
| 
 | ||||
| 			LinearSampler = new Sampler(device, SamplerCreateInfo.LinearClamp); | ||||
| 
 | ||||
| 			timer = new Stopwatch(); | ||||
| 		} | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// Prepares a VideoAV1 for decoding and rendering. | ||||
| 		/// </summary> | ||||
| 		/// <param name="video"></param> | ||||
| 		public void Load(VideoAV1 video) | ||||
| 		{ | ||||
| 			if (Video != video) | ||||
| 			{ | ||||
| 				Stop(); | ||||
| 
 | ||||
| 				if (RenderTexture == null) | ||||
| 				{ | ||||
| 					RenderTexture = CreateRenderTexture(GraphicsDevice, video.Width, video.Height); | ||||
| 				} | ||||
| 
 | ||||
| 				if (yTexture == null) | ||||
| 				{ | ||||
| 					yTexture = CreateSubTexture(GraphicsDevice, video.Width, video.Height); | ||||
| 				} | ||||
| 
 | ||||
| 				if (uTexture == null) | ||||
| 				{ | ||||
| 					uTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight); | ||||
| 				} | ||||
| 
 | ||||
| 				if (vTexture == null) | ||||
| 				{ | ||||
| 					vTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight); | ||||
| 				} | ||||
| 
 | ||||
| 				if (video.Width != RenderTexture.Width || video.Height != RenderTexture.Height) | ||||
| 				{ | ||||
| 					RenderTexture.Dispose(); | ||||
| 					RenderTexture = CreateRenderTexture(GraphicsDevice, video.Width, video.Height); | ||||
| 				} | ||||
| 
 | ||||
| 				if (video.Width != yTexture.Width || video.Height != yTexture.Height) | ||||
| 				{ | ||||
| 					yTexture.Dispose(); | ||||
| 					yTexture = CreateSubTexture(GraphicsDevice, video.Width, video.Height); | ||||
| 				} | ||||
| 
 | ||||
| 				if (video.UVWidth != uTexture.Width || video.UVHeight != uTexture.Height) | ||||
| 				{ | ||||
| 					uTexture.Dispose(); | ||||
| 					uTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight); | ||||
| 				} | ||||
| 
 | ||||
| 				if (video.UVWidth != vTexture.Width || video.UVHeight != vTexture.Height) | ||||
| 				{ | ||||
| 					vTexture.Dispose(); | ||||
| 					vTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight); | ||||
| 				} | ||||
| 
 | ||||
| 				Video = video; | ||||
| 
 | ||||
| 				InitializeDav1dStream(); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// Starts playing back and decoding the loaded video. | ||||
| 		/// </summary> | ||||
| 		public void Play() | ||||
| 		{ | ||||
| 			if (Video == null) { return; } | ||||
| 
 | ||||
| 			if (State == VideoState.Playing) | ||||
| 			{ | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			timer.Start(); | ||||
| 
 | ||||
| 			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; | ||||
| 			} | ||||
| 
 | ||||
| 			timer.Stop(); | ||||
| 
 | ||||
| 			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; | ||||
| 			} | ||||
| 
 | ||||
| 			timer.Stop(); | ||||
| 			timer.Reset(); | ||||
| 
 | ||||
| 			lastTimestamp = 0; | ||||
| 			timeElapsed = 0; | ||||
| 
 | ||||
| 			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) | ||||
| 			{ | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			timeElapsed += (timer.Elapsed.TotalMilliseconds - lastTimestamp) * PlaybackSpeed; | ||||
| 			lastTimestamp = timer.Elapsed.TotalMilliseconds; | ||||
| 
 | ||||
| 			int thisFrame = ((int) (timeElapsed / (1000.0 / Video.FramesPerSecond))); | ||||
| 			if (thisFrame > currentFrame) | ||||
| 			{ | ||||
| 				if (CurrentStream.FrameDataUpdated) | ||||
| 				{ | ||||
| 					UpdateRenderTexture(); | ||||
| 					CurrentStream.FrameDataUpdated = false; | ||||
| 				} | ||||
| 
 | ||||
| 				currentFrame = thisFrame; | ||||
| 				ReadNextFrameTask = Task.Run(CurrentStream.ReadNextFrame); | ||||
| 				ReadNextFrameTask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted); | ||||
| 			} | ||||
| 
 | ||||
| 			if (CurrentStream.Ended) | ||||
| 			{ | ||||
| 				timer.Stop(); | ||||
| 				timer.Reset(); | ||||
| 
 | ||||
| 				var task = Task.Run(CurrentStream.Reset); | ||||
| 				task.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted); | ||||
| 
 | ||||
| 				if (CurrentStream == Video.StreamA) | ||||
| 				{ | ||||
| 					ResetStreamATask = task; | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					ResetStreamBTask = task; | ||||
| 				} | ||||
| 
 | ||||
| 				if (Loop) | ||||
| 				{ | ||||
| 					// Start over on the next stream! | ||||
| 					CurrentStream = (CurrentStream == Video.StreamA) ? Video.StreamB : Video.StreamA; | ||||
| 					currentFrame = -1; | ||||
| 					timer.Start(); | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					State = VideoState.Stopped; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		private void UpdateRenderTexture() | ||||
| 		{ | ||||
| 			lock (CurrentStream) | ||||
| 			{ | ||||
| 				var commandBuffer = GraphicsDevice.AcquireCommandBuffer(); | ||||
| 
 | ||||
| 				commandBuffer.SetTextureDataYUV( | ||||
| 					yTexture, | ||||
| 					uTexture, | ||||
| 					vTexture, | ||||
| 					CurrentStream.yDataHandle, | ||||
| 					CurrentStream.uDataHandle, | ||||
| 					CurrentStream.vDataHandle, | ||||
| 					CurrentStream.yDataLength, | ||||
| 					CurrentStream.uvDataLength, | ||||
| 					CurrentStream.yStride, | ||||
| 					CurrentStream.uvStride | ||||
| 				); | ||||
| 
 | ||||
| 				commandBuffer.BeginRenderPass( | ||||
| 					new ColorAttachmentInfo(RenderTexture, Color.Black) | ||||
| 				); | ||||
| 
 | ||||
| 				commandBuffer.BindGraphicsPipeline(GraphicsDevice.VideoPipeline); | ||||
| 				commandBuffer.BindFragmentSamplers( | ||||
| 					new TextureSamplerBinding(yTexture, LinearSampler), | ||||
| 					new TextureSamplerBinding(uTexture, LinearSampler), | ||||
| 					new TextureSamplerBinding(vTexture, LinearSampler) | ||||
| 				); | ||||
| 
 | ||||
| 				commandBuffer.DrawPrimitives(0, 1, 0, 0); | ||||
| 
 | ||||
| 				commandBuffer.EndRenderPass(); | ||||
| 
 | ||||
| 				GraphicsDevice.Submit(commandBuffer); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		private static Texture CreateRenderTexture(GraphicsDevice graphicsDevice, int width, int height) | ||||
| 		{ | ||||
| 			return Texture.CreateTexture2D( | ||||
| 				graphicsDevice, | ||||
| 				(uint) width, | ||||
| 				(uint) height, | ||||
| 				TextureFormat.R8G8B8A8, | ||||
| 				TextureUsageFlags.ColorTarget | TextureUsageFlags.Sampler | ||||
| 			); | ||||
| 		} | ||||
| 
 | ||||
| 		private static Texture CreateSubTexture(GraphicsDevice graphicsDevice, int width, int height) | ||||
| 		{ | ||||
| 			return Texture.CreateTexture2D( | ||||
| 				graphicsDevice, | ||||
| 				(uint) width, | ||||
| 				(uint) height, | ||||
| 				TextureFormat.R8, | ||||
| 				TextureUsageFlags.Sampler | ||||
| 			); | ||||
| 		} | ||||
| 
 | ||||
| 		private void InitializeDav1dStream() | ||||
| 		{ | ||||
| 			ReadNextFrameTask?.Wait(); | ||||
| 
 | ||||
| 			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; | ||||
| 		} | ||||
| 
 | ||||
| 		private static void HandleTaskException(Task task) | ||||
| 		{ | ||||
| 			if (task.Exception.InnerException is not TaskCanceledException) | ||||
| 			{ | ||||
| 				throw task.Exception; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		protected override void Dispose(bool disposing) | ||||
| 		{ | ||||
| 			if (!IsDisposed) | ||||
| 			{ | ||||
| 				if (disposing) | ||||
| 				{ | ||||
| 					Unload(); | ||||
| 
 | ||||
| 					RenderTexture?.Dispose(); | ||||
| 					yTexture?.Dispose(); | ||||
| 					uTexture?.Dispose(); | ||||
| 					vTexture?.Dispose(); | ||||
| 				} | ||||
| 			} | ||||
| 			base.Dispose(disposing); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,9 @@ | |||
| namespace MoonWorks.Video | ||||
| { | ||||
| 	public enum VideoState | ||||
| 	{ | ||||
| 		Playing, | ||||
| 		Paused, | ||||
| 		Stopped | ||||
| 	} | ||||
| } | ||||
|  | @ -1,56 +1,78 @@ | |||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using MoonWorks.Graphics; | ||||
| 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; } | ||||
| 
 | ||||
| 		public bool Claimed { get; internal set; } | ||||
| 		public MoonWorks.Graphics.TextureFormat SwapchainFormat { get; internal set; } | ||||
| 
 | ||||
| 		private bool IsDisposed; | ||||
| 
 | ||||
| 		public Window(WindowCreateInfo windowCreateInfo) | ||||
| 		{ | ||||
| 			var windowFlags = SDL.SDL_WindowFlags.SDL_WINDOW_VULKAN; | ||||
| 		private static Dictionary<uint, Window> idLookup = new Dictionary<uint, Window>(); | ||||
| 
 | ||||
| 		private System.Action<uint, uint> SizeChangeCallback = null; | ||||
| 
 | ||||
| 		public Window(WindowCreateInfo windowCreateInfo, SDL.SDL_WindowFlags flags) | ||||
| 		{ | ||||
| 			if (windowCreateInfo.ScreenMode == ScreenMode.Fullscreen) | ||||
| 			{ | ||||
| 				windowFlags |= SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN; | ||||
| 				flags |= SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN; | ||||
| 			} | ||||
| 			else if (windowCreateInfo.ScreenMode == ScreenMode.BorderlessWindow) | ||||
| 			else if (windowCreateInfo.ScreenMode == ScreenMode.BorderlessFullscreen) | ||||
| 			{ | ||||
| 				windowFlags |= SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP; | ||||
| 				flags |= SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP; | ||||
| 			} | ||||
| 
 | ||||
| 			if (windowCreateInfo.SystemResizable) | ||||
| 			{ | ||||
| 				windowFlags |= SDL.SDL_WindowFlags.SDL_WINDOW_RESIZABLE; | ||||
| 				flags |= SDL.SDL_WindowFlags.SDL_WINDOW_RESIZABLE; | ||||
| 			} | ||||
| 
 | ||||
| 			if (windowCreateInfo.StartMaximized) | ||||
| 			{ | ||||
| 				windowFlags |= SDL.SDL_WindowFlags.SDL_WINDOW_MAXIMIZED; | ||||
| 				flags |= SDL.SDL_WindowFlags.SDL_WINDOW_MAXIMIZED; | ||||
| 			} | ||||
| 
 | ||||
| 			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, | ||||
| 				windowFlags | ||||
| 				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); | ||||
| 		} | ||||
| 
 | ||||
| 		public void ChangeScreenMode(ScreenMode screenMode) | ||||
| 		/// <summary> | ||||
| 		/// Changes the ScreenMode of this window. | ||||
| 		/// </summary> | ||||
| 		public void SetScreenMode(ScreenMode screenMode) | ||||
| 		{ | ||||
| 			SDL.SDL_WindowFlags windowFlag = 0; | ||||
| 
 | ||||
|  | @ -58,18 +80,23 @@ namespace MoonWorks | |||
| 			{ | ||||
| 				windowFlag = SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN; | ||||
| 			} | ||||
| 			else if (screenMode == ScreenMode.BorderlessWindow) | ||||
| 			else if (screenMode == ScreenMode.BorderlessFullscreen) | ||||
| 			{ | ||||
| 				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> | ||||
|  | @ -79,12 +106,40 @@ 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 void SizeChanged(uint width, uint height) | ||||
| 		internal static Window Lookup(uint windowID) | ||||
| 		{ | ||||
| 			return idLookup.ContainsKey(windowID) ? idLookup[windowID] : null; | ||||
| 		} | ||||
| 
 | ||||
| 		internal void Show() | ||||
| 		{ | ||||
| 			SDL.SDL_ShowWindow(Handle); | ||||
| 		} | ||||
| 
 | ||||
| 		internal void HandleSizeChange(uint width, uint height) | ||||
| 		{ | ||||
| 			Width = width; | ||||
| 			Height = height; | ||||
| 
 | ||||
| 			if (SizeChangeCallback != null) | ||||
| 			{ | ||||
| 				SizeChangeCallback(width, height); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// You can specify a method to run when the window size changes. | ||||
| 		/// </summary> | ||||
| 		public void RegisterSizeChangeCallback(System.Action<uint, uint> sizeChangeCallback) | ||||
| 		{ | ||||
| 			SizeChangeCallback = sizeChangeCallback; | ||||
| 		} | ||||
| 
 | ||||
| 		protected virtual void Dispose(bool disposing) | ||||
|  | @ -96,6 +151,7 @@ namespace MoonWorks | |||
| 					// dispose managed state (managed objects) | ||||
| 				} | ||||
| 
 | ||||
| 				idLookup.Remove(SDL.SDL_GetWindowID(Handle)); | ||||
| 				SDL.SDL_DestroyWindow(Handle); | ||||
| 
 | ||||
| 				IsDisposed = true; | ||||
|  |  | |||
|  | @ -1,12 +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( | ||||
|  | @ -14,6 +39,7 @@ | |||
| 			uint windowWidth, | ||||
| 			uint windowHeight, | ||||
| 			ScreenMode screenMode, | ||||
| 			PresentMode presentMode, | ||||
| 			bool systemResizable = false, | ||||
| 			bool startMaximized = false | ||||
| 		) { | ||||
|  | @ -21,6 +47,7 @@ | |||
| 			WindowWidth = windowWidth; | ||||
| 			WindowHeight = windowHeight; | ||||
| 			ScreenMode = screenMode; | ||||
| 			PresentMode = presentMode; | ||||
| 			SystemResizable = systemResizable; | ||||
| 			StartMaximized = startMaximized; | ||||
| 		} | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue