Compare commits
45 Commits
76b1f9c212
...
project
Author | SHA1 | Date | |
---|---|---|---|
09089c35b9 | |||
bd76fc1b25 | |||
d80cc51aff | |||
ad2f527de5 | |||
b4c3b5ed18 | |||
b9f5a4c56b | |||
50a77d5120 | |||
91672d5760 | |||
546b7feca7 | |||
36a134170a | |||
6e41c2579c | |||
2afbd9defe | |||
6f505f34a9 | |||
f978c49532 | |||
92cf24fe9f | |||
cce6e00960 | |||
1096597161 | |||
d45f7c3b8c | |||
7a81026ca5 | |||
60b85960ff | |||
b003ffbaec | |||
777059489c | |||
16b04ea22a | |||
4b824f3205 | |||
9890026656 | |||
d8b41b0827 | |||
dd3bbf1d5b | |||
38080703ec | |||
fe582c4fba | |||
42b978e8c9 | |||
2c839d8fad | |||
97c2b308f1 | |||
1eb899b240 | |||
7cbb745721 | |||
1e1ed303ad | |||
26eb1da3f0 | |||
e46b43281f | |||
1c64d6fe54 | |||
5d2c350bb8 | |||
8334a24fd1 | |||
e7a4a862be | |||
56cb14441f | |||
cd8beb0337 | |||
4794fbc647 | |||
8169d43a97 |
14
.gitignore
vendored
14
.gitignore
vendored
@ -1,5 +1,3 @@
|
||||
imgui.ini
|
||||
|
||||
#------------------------- Rider -------------------------
|
||||
# Source: https://github.com/JetBrains/resharper-rider-samples/blob/master/.gitignore
|
||||
|
||||
@ -451,3 +449,15 @@ FodyWeavers.xsd
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
#------------------------- Nerfed -------------------------
|
||||
|
||||
Bin/
|
||||
Intermediate/
|
||||
|
||||
imgui.ini
|
||||
|
||||
# include libs
|
||||
!/Native/lib64
|
||||
!/Native/x64
|
||||
|
||||
|
||||
|
16
.gitmodules
vendored
16
.gitmodules
vendored
@ -1,6 +1,18 @@
|
||||
[submodule "Libraries/RefreshCS"]
|
||||
path = Libraries/RefreshCS
|
||||
path = Nerfed.Runtime/Libraries/RefreshCS
|
||||
url = https://github.com/MoonsideGames/RefreshCS.git
|
||||
[submodule "Libraries/SDL2CS"]
|
||||
path = Libraries/SDL2CS
|
||||
path = Nerfed.Runtime/Libraries/SDL2CS
|
||||
url = https://github.com/flibitijibibo/SDL2-CS.git
|
||||
[submodule "Nerfed.Runtime/Libraries/FAudio"]
|
||||
path = Nerfed.Runtime/Libraries/FAudio
|
||||
url = https://github.com/FNA-XNA/FAudio.git
|
||||
[submodule "Nerfed.Runtime/Libraries/WellspringCS"]
|
||||
path = Nerfed.Runtime/Libraries/WellspringCS
|
||||
url = https://github.com/MoonsideGames/WellspringCS.git
|
||||
[submodule "Nerfed.Runtime/Libraries/dav1dfile"]
|
||||
path = Nerfed.Runtime/Libraries/dav1dfile
|
||||
url = https://github.com/MoonsideGames/dav1dfile.git
|
||||
[submodule "Nerfed.Runtime/Libraries/ImGui.NET"]
|
||||
path = Nerfed.Runtime/Libraries/ImGui.NET
|
||||
url = https://github.com/ImGuiNET/ImGui.NET.git
|
||||
|
13
.idea/.idea.Nerfed/.idea/.gitignore
generated
vendored
Normal file
13
.idea/.idea.Nerfed/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Rider ignored files
|
||||
/modules.xml
|
||||
/contentModel.xml
|
||||
/projectSettingsUpdater.xml
|
||||
/.idea.Nerfed.iml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
4
.idea/.idea.Nerfed/.idea/encodings.xml
generated
Normal file
4
.idea/.idea.Nerfed/.idea/encodings.xml
generated
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
8
.idea/.idea.Nerfed/.idea/indexLayout.xml
generated
Normal file
8
.idea/.idea.Nerfed/.idea/indexLayout.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
10
.idea/.idea.Nerfed/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
10
.idea/.idea.Nerfed/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
12
.idea/.idea.Nerfed/.idea/vcs.xml
generated
Normal file
12
.idea/.idea.Nerfed/.idea/vcs.xml
generated
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/Nerfed.Runtime/Libraries/FAudio" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/Nerfed.Runtime/Libraries/ImGui.NET" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/Nerfed.Runtime/Libraries/RefreshCS" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/Nerfed.Runtime/Libraries/SDL2CS" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/Nerfed.Runtime/Libraries/WellspringCS" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/Nerfed.Runtime/Libraries/dav1dfile" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
7
Directory.Build.props
Normal file
7
Directory.Build.props
Normal file
@ -0,0 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<OutDir>../Bin/$(MSBuildProjectName)</OutDir>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">../Intermediate/$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
9
LICENSE
Normal file
9
LICENSE
Normal file
@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Nerfed Engine
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
BIN
Native/lib64/FAudio.so
(Stored with Git LFS)
Executable file
BIN
Native/lib64/FAudio.so
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
Native/lib64/Refresh.so
(Stored with Git LFS)
Executable file
BIN
Native/lib64/Refresh.so
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
Native/lib64/SDL2.so
(Stored with Git LFS)
Executable file
BIN
Native/lib64/SDL2.so
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
Native/lib64/Wellspring.so
(Stored with Git LFS)
Executable file
BIN
Native/lib64/Wellspring.so
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
Native/lib64/cimgui.so
(Stored with Git LFS)
Normal file
BIN
Native/lib64/cimgui.so
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Native/lib64/dav1dfile.so
(Stored with Git LFS)
Normal file
BIN
Native/lib64/dav1dfile.so
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Native/x64/FAudio.dll
(Stored with Git LFS)
Normal file
BIN
Native/x64/FAudio.dll
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Native/x64/Refresh.dll
(Stored with Git LFS)
Normal file
BIN
Native/x64/Refresh.dll
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Native/x64/SDL2.dll
(Stored with Git LFS)
Normal file
BIN
Native/x64/SDL2.dll
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Native/x64/Wellspring.dll
(Stored with Git LFS)
Normal file
BIN
Native/x64/Wellspring.dll
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Native/x64/cimgui.dll
(Stored with Git LFS)
Normal file
BIN
Native/x64/cimgui.dll
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
Native/x64/dav1dfile.dll
(Stored with Git LFS)
Normal file
BIN
Native/x64/dav1dfile.dll
(Stored with Git LFS)
Normal file
Binary file not shown.
129
Nerfed.Builder/ArgsParser.cs
Normal file
129
Nerfed.Builder/ArgsParser.cs
Normal file
@ -0,0 +1,129 @@
|
||||
using System.Collections;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Nerfed.Builder;
|
||||
|
||||
|
||||
public class ArgsParser<TArgs> where TArgs : new()
|
||||
{
|
||||
private enum ArgType
|
||||
{
|
||||
None,
|
||||
Key,
|
||||
Value
|
||||
}
|
||||
|
||||
public TArgs Arguments { get; }
|
||||
|
||||
private readonly string[] args;
|
||||
private readonly Dictionary<string, PropertyInfo> argKeyPropertyMap = new Dictionary<string, PropertyInfo>();
|
||||
|
||||
public ArgsParser(string[] args)
|
||||
{
|
||||
this.args = args;
|
||||
Arguments = new TArgs();
|
||||
PropertyInfo[] properties = Arguments.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
|
||||
foreach (PropertyInfo property in properties)
|
||||
{
|
||||
ArgumentAttribute argAttribute = property.GetCustomAttribute<ArgumentAttribute>();
|
||||
if (argAttribute == null || string.IsNullOrEmpty(argAttribute.ArgKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
argKeyPropertyMap.Add(argAttribute.ArgKey, property);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Parse()
|
||||
{
|
||||
PropertyInfo property = null;
|
||||
ArgType lastArgType = ArgType.None;
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
string arg = args[i];
|
||||
if (arg[0] == '-')
|
||||
{
|
||||
if (!argKeyPropertyMap.TryGetValue(arg, out property))
|
||||
{
|
||||
Console.Error.WriteLine($"Invalid argument: {arg}, no such argument key exists");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Boolean arguments require no value, set to true immidiately.
|
||||
if (property.PropertyType == typeof(bool))
|
||||
{
|
||||
property.SetValue(Arguments, true);
|
||||
lastArgType = ArgType.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastArgType = ArgType.Key;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lastArgType == ArgType.None)
|
||||
{
|
||||
Console.Error.WriteLine($"Invalid argument: {arg}, no argument key was provided");
|
||||
return false;
|
||||
}
|
||||
|
||||
Type propertyType = property.PropertyType;
|
||||
if (propertyType.IsArray)
|
||||
{
|
||||
throw new InvalidOperationException("Arrays are not supported, use List<T> instead");
|
||||
}
|
||||
|
||||
bool propertyTypeIsList = propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>);
|
||||
if (propertyTypeIsList)
|
||||
{
|
||||
propertyType = propertyType.GenericTypeArguments[0];
|
||||
}
|
||||
|
||||
TypeConverter typeConverter = TypeDescriptor.GetConverter(propertyType);
|
||||
object value = typeConverter.ConvertFromString(arg);
|
||||
|
||||
if (value is string stringValue)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stringValue))
|
||||
{
|
||||
if (stringValue[0] == '"')
|
||||
{
|
||||
stringValue = stringValue.Substring(1, stringValue.Length - 1);
|
||||
}
|
||||
|
||||
if (stringValue[^1] == '"')
|
||||
{
|
||||
stringValue = stringValue.Substring(0, stringValue.Length - 1);
|
||||
}
|
||||
|
||||
value = stringValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (propertyTypeIsList)
|
||||
{
|
||||
IList list = (IList)property.GetValue(Arguments);
|
||||
if (list == null)
|
||||
{
|
||||
list = (IList)Activator.CreateInstance(property.PropertyType);
|
||||
property.SetValue(Arguments, list);
|
||||
}
|
||||
|
||||
list.Add(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
property.SetValue(Arguments, value);
|
||||
}
|
||||
|
||||
lastArgType = ArgType.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
11
Nerfed.Builder/ArgumentAttribute.cs
Normal file
11
Nerfed.Builder/ArgumentAttribute.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Nerfed.Builder;
|
||||
|
||||
public class ArgumentAttribute : Attribute
|
||||
{
|
||||
public string ArgKey { get; }
|
||||
|
||||
public ArgumentAttribute(string argKey)
|
||||
{
|
||||
ArgKey = argKey;
|
||||
}
|
||||
}
|
19
Nerfed.Builder/Builder/BuildArgs.cs
Normal file
19
Nerfed.Builder/Builder/BuildArgs.cs
Normal file
@ -0,0 +1,19 @@
|
||||
namespace Nerfed.Builder;
|
||||
|
||||
public class BuildArgs
|
||||
{
|
||||
[Argument("-build")]
|
||||
public bool Build { get; set; }
|
||||
|
||||
[Argument("-resourcePath")]
|
||||
public string ResourcePath { get; set; }
|
||||
|
||||
[Argument("-resourceOutPath")]
|
||||
public string ResourceOutPath { get; set; }
|
||||
|
||||
[Argument("-platform")]
|
||||
public string Platform { get; set; }
|
||||
|
||||
[Argument("-resourceFiles")]
|
||||
public List<string> ResourceFiles { get; set; }
|
||||
}
|
144
Nerfed.Builder/Builder/Builder.cs
Normal file
144
Nerfed.Builder/Builder/Builder.cs
Normal file
@ -0,0 +1,144 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Nerfed.Builder;
|
||||
|
||||
public class Builder : IDisposable
|
||||
{
|
||||
private readonly Dictionary<string, IImporter> importers = new Dictionary<string, IImporter>();
|
||||
private readonly RawFileImporter rawFileImporter;
|
||||
|
||||
public Builder()
|
||||
{
|
||||
rawFileImporter = new RawFileImporter();
|
||||
|
||||
importers.Add(".vert", new ShaderImporter(ShaderStage.Vertex)); // Vertex shader
|
||||
importers.Add(".frag", new ShaderImporter(ShaderStage.Fragment)); // Fragment shader
|
||||
//importers.Add(".tesc", shaderImporter); // Tessellation control shader
|
||||
//importers.Add(".tese", shaderImporter); // Tessellation evaluation shader
|
||||
//importers.Add(".geom", shaderImporter); // Geometry shader
|
||||
//importers.Add(".comp", shaderImporter); // Compute shader
|
||||
}
|
||||
|
||||
public void Run(BuildArgs args)
|
||||
{
|
||||
Stopwatch stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
//CopyLibs(args.ResourcePath);
|
||||
|
||||
List<string> contentFiles = args.ResourceFiles;
|
||||
|
||||
// If no files are provided, build all content.
|
||||
if (args.ResourceFiles == null)
|
||||
{
|
||||
contentFiles = [];
|
||||
CollectAssetFiles(args.ResourcePath, args.ResourcePath, ref contentFiles);
|
||||
}
|
||||
|
||||
if (contentFiles.Count > 0)
|
||||
{
|
||||
ParallelOptions parallelOptions = new ParallelOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = contentFiles.Count
|
||||
};
|
||||
|
||||
Parallel.ForEach(contentFiles, parallelOptions, relativeFile =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string inFile = $"{args.ResourcePath}/{relativeFile}";
|
||||
|
||||
if (!File.Exists(inFile))
|
||||
{
|
||||
Console.Error.WriteLine($"Asset file '{relativeFile}' not found");
|
||||
return;
|
||||
}
|
||||
|
||||
string outFile = $"{args.ResourceOutPath}/{relativeFile}{PathUtil.ImportedFileExtension}";
|
||||
|
||||
FileInfo inFileInfo = new FileInfo(inFile);
|
||||
FileInfo outFileInfo = new FileInfo(outFile);
|
||||
|
||||
if (!FileUtil.IsNewer(inFileInfo, outFileInfo))
|
||||
{
|
||||
// File has not changed since last build, no need to build this one.
|
||||
return;
|
||||
}
|
||||
|
||||
string outDir = Path.GetDirectoryName(outFile);
|
||||
if (!Directory.Exists(outDir))
|
||||
{
|
||||
Directory.CreateDirectory(outDir);
|
||||
}
|
||||
|
||||
string ext = Path.GetExtension(inFile).ToLower();
|
||||
if (importers.TryGetValue(ext, out IImporter importer))
|
||||
{
|
||||
importer.Import(inFile, outFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
rawFileImporter.Import(inFile, outFile);
|
||||
}
|
||||
|
||||
Console.WriteLine(relativeFile);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.Error.WriteLine($"Import error on asset '{relativeFile}': {e.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Console.WriteLine($"Build content completed in {stopwatch.Elapsed.TotalSeconds:F2} seconds");
|
||||
}
|
||||
|
||||
/*private void CopyLibs(string projectPath)
|
||||
{
|
||||
string libDir = $"{AppDomain.CurrentDomain.BaseDirectory}/../../Native/";
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
libDir += "x64";
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
libDir += "lib64";
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
libDir += "osx";
|
||||
}
|
||||
|
||||
libDir = Path.GetFullPath(libDir);
|
||||
foreach (string libFile in Directory.EnumerateFiles(libDir))
|
||||
{
|
||||
FileInfo srcFileInfo = new FileInfo(libFile);
|
||||
FileInfo dstFileInfo = new FileInfo($"{projectPath}/{PathUtil.BuildFolderName}/{Path.GetFileName(libFile)}");
|
||||
if (FileUtil.IsNewer(srcFileInfo, dstFileInfo))
|
||||
{
|
||||
FileUtil.Copy(srcFileInfo, dstFileInfo);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
private void CollectAssetFiles(string assetDir, string dir, ref List<string> files)
|
||||
{
|
||||
foreach (string file in Directory.EnumerateFiles(dir))
|
||||
{
|
||||
string relativeFile = file.Substring(assetDir.Length, file.Length - assetDir.Length);
|
||||
if (relativeFile[0] == Path.DirectorySeparatorChar || relativeFile[0] == Path.AltDirectorySeparatorChar)
|
||||
{
|
||||
relativeFile = relativeFile.Substring(1, relativeFile.Length - 1);
|
||||
}
|
||||
|
||||
files.Add(relativeFile);
|
||||
}
|
||||
|
||||
foreach (string subDir in Directory.EnumerateDirectories(dir))
|
||||
{
|
||||
CollectAssetFiles(assetDir, subDir, ref files);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
49
Nerfed.Builder/Builder/FileUtil.cs
Normal file
49
Nerfed.Builder/Builder/FileUtil.cs
Normal file
@ -0,0 +1,49 @@
|
||||
namespace Nerfed.Builder;
|
||||
|
||||
public static class FileUtil
|
||||
{
|
||||
public static void Copy(FileInfo srcFile, FileInfo dstFile)
|
||||
{
|
||||
Copy(srcFile.FullName, dstFile.FullName);
|
||||
}
|
||||
|
||||
public static void Copy(string srcFile, string dstFile)
|
||||
{
|
||||
string dstDir = Path.GetDirectoryName(dstFile);
|
||||
if (!Directory.Exists(dstDir))
|
||||
{
|
||||
Directory.CreateDirectory(dstDir);
|
||||
}
|
||||
|
||||
File.Copy(srcFile, dstFile, true);
|
||||
UpdateFileTimeAttributes(dstFile);
|
||||
}
|
||||
|
||||
public static void WriteBytes(string dstFile, byte[] bytes)
|
||||
{
|
||||
File.WriteAllBytes(dstFile, bytes);
|
||||
UpdateFileTimeAttributes(dstFile);
|
||||
}
|
||||
|
||||
public static void UpdateFileTimeAttributes(string file)
|
||||
{
|
||||
// Copy over date time attributes so we can check if the file changed.
|
||||
FileInfo dstFileInfo = new FileInfo(file);
|
||||
DateTime now = DateTime.Now;
|
||||
DateTime utcNow = DateTime.UtcNow;
|
||||
dstFileInfo.CreationTime = now;
|
||||
dstFileInfo.CreationTimeUtc = utcNow;
|
||||
dstFileInfo.LastWriteTime = now;
|
||||
dstFileInfo.LastWriteTimeUtc = utcNow;
|
||||
dstFileInfo.LastAccessTime = now;
|
||||
dstFileInfo.LastAccessTimeUtc = utcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the inFileInfo is newer than the outFileInfo.
|
||||
/// </summary>
|
||||
public static bool IsNewer(FileInfo inFileInfo, FileInfo outFileInfo)
|
||||
{
|
||||
return !outFileInfo.Exists || outFileInfo.LastWriteTime <= inFileInfo.LastWriteTime;
|
||||
}
|
||||
}
|
6
Nerfed.Builder/Builder/IImporter.cs
Normal file
6
Nerfed.Builder/Builder/IImporter.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Nerfed.Builder;
|
||||
|
||||
public interface IImporter
|
||||
{
|
||||
void Import(string inFile, string outFile);
|
||||
}
|
9
Nerfed.Builder/Builder/Importers/RawFileImporter.cs
Normal file
9
Nerfed.Builder/Builder/Importers/RawFileImporter.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Nerfed.Builder;
|
||||
|
||||
public class RawFileImporter : IImporter
|
||||
{
|
||||
public void Import(string inFile, string outFile)
|
||||
{
|
||||
FileUtil.Copy(inFile, outFile);
|
||||
}
|
||||
}
|
157
Nerfed.Builder/Builder/Importers/ShaderImporter.cs
Normal file
157
Nerfed.Builder/Builder/Importers/ShaderImporter.cs
Normal file
@ -0,0 +1,157 @@
|
||||
using Vortice.ShaderCompiler;
|
||||
using Vortice.SPIRV.Reflect;
|
||||
|
||||
namespace Nerfed.Builder;
|
||||
|
||||
// Values should match the ShaderStage enum in the runtime.
|
||||
public enum ShaderStage
|
||||
{
|
||||
Vertex,
|
||||
Fragment
|
||||
}
|
||||
|
||||
// Values should match the ShaderFormat enum in the runtime.
|
||||
public enum ShaderFormat
|
||||
{
|
||||
Invalid,
|
||||
SPIRV,
|
||||
HLSL,
|
||||
DXBC,
|
||||
DXIL,
|
||||
MSL,
|
||||
METALLIB,
|
||||
SECRET
|
||||
}
|
||||
|
||||
public class ShaderImporter : IImporter
|
||||
{
|
||||
private readonly ShaderStage shaderStage;
|
||||
|
||||
public ShaderImporter(ShaderStage shaderStage)
|
||||
{
|
||||
this.shaderStage = shaderStage;
|
||||
}
|
||||
|
||||
public unsafe void Import(string inFile, string outFile)
|
||||
{
|
||||
string name = Path.GetFileNameWithoutExtension(inFile);
|
||||
string nameWithExt = Path.GetFileName(inFile);
|
||||
|
||||
// Compile the shader.
|
||||
Result compileResult;
|
||||
using (Compiler compiler = new Compiler())
|
||||
{
|
||||
string shaderSource = File.ReadAllText(inFile);
|
||||
compileResult = compiler.Compile(shaderSource, nameWithExt, ToShaderKind(shaderStage));
|
||||
}
|
||||
|
||||
if (compileResult.Status != CompilationStatus.Success)
|
||||
{
|
||||
Console.Error.WriteLine($"Failed to compile {nameWithExt}\n{compileResult.ErrorMessage}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (compileResult.ErrorsCount > 0 || compileResult.WarningsCount > 0)
|
||||
{
|
||||
Console.Error.WriteLine(compileResult.ErrorMessage);
|
||||
}
|
||||
|
||||
Span<byte> byteCode = compileResult.GetBytecode();
|
||||
|
||||
// Inspect SPIR-V bytecode for information which the runtime requires to create a shader resource.
|
||||
SpvReflectShaderModule module = new SpvReflectShaderModule();
|
||||
if (!CheckReflectResult(SPIRVReflectApi.spvReflectCreateShaderModule(byteCode, &module), name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint descriptorSetCount = 0;
|
||||
if (!CheckReflectResult(SPIRVReflectApi.spvReflectEnumerateDescriptorSets(&module, &descriptorSetCount, null), name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int uniformBufferCount = 0;
|
||||
int storageBufferCount = 0;
|
||||
int storageTextureCount = 0;
|
||||
int samplerCount = 0;
|
||||
|
||||
if (descriptorSetCount > 0)
|
||||
{
|
||||
SpvReflectDescriptorSet* descriptorSets = stackalloc SpvReflectDescriptorSet[(int)descriptorSetCount];
|
||||
if (!CheckReflectResult(
|
||||
SPIRVReflectApi.spvReflectEnumerateDescriptorSets(&module, &descriptorSetCount, &descriptorSets),
|
||||
name
|
||||
))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < descriptorSetCount; i++)
|
||||
{
|
||||
SpvReflectDescriptorSet set = descriptorSets[i];
|
||||
for (int j = 0; j < set.binding_count; j++)
|
||||
{
|
||||
SpvReflectDescriptorBinding binding = *set.bindings[j];
|
||||
if (binding.descriptor_type == SpvReflectDescriptorType.UniformBuffer)
|
||||
{
|
||||
uniformBufferCount++;
|
||||
}
|
||||
else if (binding.descriptor_type == SpvReflectDescriptorType.StorageBuffer)
|
||||
{
|
||||
storageBufferCount++;
|
||||
}
|
||||
else if (binding.descriptor_type == SpvReflectDescriptorType.StorageTexelBuffer)
|
||||
{
|
||||
storageTextureCount++;
|
||||
}
|
||||
else if (binding.descriptor_type == SpvReflectDescriptorType.Sampler ||
|
||||
binding.descriptor_type == SpvReflectDescriptorType.CombinedImageSampler)
|
||||
{
|
||||
samplerCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Convert SPIR-V to other bytecode formats here (DX/Consoles).
|
||||
ShaderFormat format = ShaderFormat.SPIRV;
|
||||
|
||||
// Write shader meta-data and bytecode to the output file.
|
||||
using (FileStream stream = new FileStream(outFile, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
using (BinaryWriter writer = new BinaryWriter(stream))
|
||||
{
|
||||
writer.Write((int)format);
|
||||
writer.Write((int)shaderStage);
|
||||
writer.Write(uniformBufferCount);
|
||||
writer.Write(storageBufferCount);
|
||||
writer.Write(storageTextureCount);
|
||||
writer.Write(samplerCount);
|
||||
writer.Write(byteCode.Length);
|
||||
writer.Write(byteCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckReflectResult(SpvReflectResult result, string name)
|
||||
{
|
||||
if (result != SpvReflectResult.Success)
|
||||
{
|
||||
Console.Error.WriteLine($"SpirV-Reflect failure for '{name}': {result}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private ShaderKind ToShaderKind(ShaderStage shaderStage)
|
||||
{
|
||||
switch (shaderStage)
|
||||
{
|
||||
case ShaderStage.Vertex: return ShaderKind.VertexShader;
|
||||
case ShaderStage.Fragment: return ShaderKind.FragmentShader;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(shaderStage));
|
||||
}
|
||||
}
|
||||
}
|
33
Nerfed.Builder/Nerfed.Builder.csproj
Normal file
33
Nerfed.Builder/Nerfed.Builder.csproj
Normal file
@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<IsPackable>false</IsPackable>
|
||||
<Configurations>Debug;Test;Release</Configurations>
|
||||
<Platforms>x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Test|x64' ">
|
||||
<Optimize>true</Optimize>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
|
||||
<Optimize>true</Optimize>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Vortice.ShaderCompiler" Version="1.7.3" />
|
||||
<PackageReference Include="Vortice.SPIRV.Reflect" Version="1.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
4
Nerfed.Builder/Nerfed.Builder.csproj.DotSettings
Normal file
4
Nerfed.Builder/Nerfed.Builder.csproj.DotSettings
Normal file
@ -0,0 +1,4 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=builder/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=builder_005Cimporters/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=packager/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
6
Nerfed.Builder/Packager/PackageArgs.cs
Normal file
6
Nerfed.Builder/Packager/PackageArgs.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Nerfed.Builder;
|
||||
|
||||
public class PackageArgs
|
||||
{
|
||||
|
||||
}
|
12
Nerfed.Builder/Packager/Packager.cs
Normal file
12
Nerfed.Builder/Packager/Packager.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace Nerfed.Builder;
|
||||
|
||||
public class Packager : IDisposable
|
||||
{
|
||||
public void Run(PackageArgs args)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
6
Nerfed.Builder/PathUtil.cs
Normal file
6
Nerfed.Builder/PathUtil.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Nerfed.Builder;
|
||||
|
||||
public static class PathUtil
|
||||
{
|
||||
public const string ImportedFileExtension = ".bin";
|
||||
}
|
70
Nerfed.Builder/Program.cs
Normal file
70
Nerfed.Builder/Program.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Nerfed.Builder;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
return Run(args);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
return Run(args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.Error.WriteLine(e.Message);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int Run(string[] rawArgs)
|
||||
{
|
||||
if (rawArgs.Length == 0)
|
||||
{
|
||||
Console.Error.WriteLine($"Invalid build type '{rawArgs[0]}' expected '-build' or '-package'");
|
||||
return -1;
|
||||
}
|
||||
|
||||
string buildType = rawArgs[0].ToLower();
|
||||
if (buildType == "-build")
|
||||
{
|
||||
ArgsParser<BuildArgs> parser = new ArgsParser<BuildArgs>(rawArgs);
|
||||
if (!parser.Parse())
|
||||
{
|
||||
Console.Error.Write("Failed to parse build arguments");
|
||||
return -1;
|
||||
}
|
||||
|
||||
using (Builder builder = new Builder())
|
||||
{
|
||||
builder.Run(parser.Arguments);
|
||||
}
|
||||
}
|
||||
else if (buildType == "-package")
|
||||
{
|
||||
ArgsParser<PackageArgs> parser = new ArgsParser<PackageArgs>(rawArgs);
|
||||
if (!parser.Parse())
|
||||
{
|
||||
Console.Error.Write("Failed to parse package arguments");
|
||||
return -1;
|
||||
}
|
||||
|
||||
using (Packager packager = new Packager())
|
||||
{
|
||||
packager.Run(parser.Arguments);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine("Packaging not yet implemented");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
64
Nerfed.Compiler/AssemblyDefinition.cs
Normal file
64
Nerfed.Compiler/AssemblyDefinition.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Nerfed.Compiler;
|
||||
|
||||
public class AssemblyDefinition
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Guid { get; set; }
|
||||
//public bool IsEditor { get; set; }
|
||||
// Add platform stuff here..?
|
||||
// Add dll's here..?
|
||||
// Add dependencies here..?
|
||||
|
||||
public static bool Create(string assemblyDefinitionFilePath, string name, out AssemblyDefinition assemblyDefinition)
|
||||
{
|
||||
assemblyDefinition = null;
|
||||
|
||||
if (File.Exists(assemblyDefinitionFilePath))
|
||||
{
|
||||
Console.WriteLine($"ERROR: File already exists!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create project file.
|
||||
assemblyDefinition = new AssemblyDefinition
|
||||
{
|
||||
Name = name,
|
||||
Guid = System.Guid.NewGuid().ToString("B").ToUpper(),
|
||||
};
|
||||
|
||||
Save(assemblyDefinition, assemblyDefinitionFilePath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool Save(AssemblyDefinition assemblyDefinition, string assemblyDefinitionFilePath)
|
||||
{
|
||||
string jsonString = JsonSerializer.Serialize(assemblyDefinition, AssemblyDefinitionContext.Default.AssemblyDefinition);
|
||||
|
||||
File.WriteAllText(assemblyDefinitionFilePath, jsonString);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool Open(string assemblyDefinitionFilePath, out AssemblyDefinition assemblyDefinition)
|
||||
{
|
||||
string jsonString = File.ReadAllText(assemblyDefinitionFilePath);
|
||||
assemblyDefinition = JsonSerializer.Deserialize(jsonString, AssemblyDefinitionContext.Default.AssemblyDefinition);
|
||||
|
||||
if (assemblyDefinition == null)
|
||||
{
|
||||
Console.WriteLine($"ERROR: Could not open {typeof(AssemblyDefinition)}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(AssemblyDefinition))]
|
||||
public partial class AssemblyDefinitionContext : JsonSerializerContext
|
||||
{
|
||||
}
|
82
Nerfed.Compiler/Compiler.cs
Normal file
82
Nerfed.Compiler/Compiler.cs
Normal file
@ -0,0 +1,82 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nerfed.Compiler;
|
||||
|
||||
public static class Compiler
|
||||
{
|
||||
public static bool Compile(string projectFilePath, string configuration = "Debug")
|
||||
{
|
||||
string projectDirectory = Path.GetDirectoryName(projectFilePath);
|
||||
|
||||
if (!File.Exists(projectFilePath))
|
||||
{
|
||||
Console.WriteLine($"ERROR: Project file not found at {projectDirectory}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Project.Open(projectFilePath, out Project project))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Check project version, to make sure we can compile it or something...
|
||||
|
||||
// Generate solution.
|
||||
Generator.GenerateSolution(projectDirectory, project, out string solutionFilePath);
|
||||
|
||||
// Compile solution.
|
||||
ProcessStartInfo processInfo = new()
|
||||
{
|
||||
WorkingDirectory = Path.GetDirectoryName(solutionFilePath),
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true,
|
||||
};
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
processInfo.FileName = "/bin/bash";
|
||||
processInfo.Arguments = $"-c \"dotnet build '{Path.GetFileName(solutionFilePath)}'\"" + (string.IsNullOrWhiteSpace(configuration) ? $" --configuration {configuration}" : "");
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
processInfo.FileName = "cmd.exe";
|
||||
processInfo.Arguments = $"/c dotnet build \"{Path.GetFileName(solutionFilePath)}\"" + (string.IsNullOrWhiteSpace(configuration) ? $" --configuration {configuration}" : "");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"ERROR: Platform not supported!");
|
||||
return false;
|
||||
}
|
||||
|
||||
Process process = Process.Start(processInfo) ?? throw new Exception();
|
||||
process.OutputDataReceived += (sender, dataArgs) => {
|
||||
string data = dataArgs.Data;
|
||||
|
||||
if (data is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine(data);
|
||||
};
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
process.ErrorDataReceived += (sender, dataArgs) => {
|
||||
if (dataArgs.Data is not null)
|
||||
{
|
||||
Console.WriteLine(dataArgs.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.WaitForExit();
|
||||
|
||||
int exitCode = process.ExitCode;
|
||||
process.Close();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
161
Nerfed.Compiler/Generator.cs
Normal file
161
Nerfed.Compiler/Generator.cs
Normal file
@ -0,0 +1,161 @@
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Nerfed.Compiler;
|
||||
|
||||
public static class Generator
|
||||
{
|
||||
public const string AssemblyDefinitionExtensionName = ".asmdef";
|
||||
public const string CSProjectExtensionName = ".csproj";
|
||||
public const string SolutionExtensionName = ".sln";
|
||||
|
||||
public static void GenerateSolution(string projectDirectory, Project project, out string solutionFilePath)
|
||||
{
|
||||
// Clear files.
|
||||
ClearCSProjectFiles(projectDirectory);
|
||||
|
||||
// Generate projects.
|
||||
string[] assemblyDefinitionFilePaths = Directory.GetFiles(projectDirectory, AssemblyDefinitionExtensionName, SearchOption.AllDirectories);
|
||||
foreach (string assemblyDefinitionFilePath in assemblyDefinitionFilePaths)
|
||||
{
|
||||
GenerateCSProject(assemblyDefinitionFilePath, projectDirectory, out string csProjectFilePath);
|
||||
}
|
||||
|
||||
// Generate solution.
|
||||
string[] csProjectPaths = Directory.GetFiles(projectDirectory, $"*{CSProjectExtensionName}", SearchOption.TopDirectoryOnly);
|
||||
string[] csProjectGuids = new string[csProjectPaths.Length];
|
||||
for (int i = 0; i < csProjectPaths.Length; i++)
|
||||
{
|
||||
csProjectGuids[i] = Guid.NewGuid().ToString("B").ToUpper();
|
||||
}
|
||||
|
||||
StringBuilder content = new StringBuilder();
|
||||
|
||||
// Write the solution file header
|
||||
content.AppendLine("Microsoft Visual Studio Solution File, Format Version 12.00");
|
||||
content.AppendLine("# Visual Studio Version 17");
|
||||
content.AppendLine("VisualStudioVersion = 17.10.35013.160");
|
||||
content.AppendLine("MinimumVisualStudioVersion = 10.0.40219.1");
|
||||
|
||||
// Add each project to the solution file
|
||||
for (int i = 0; i < csProjectPaths.Length; i++)
|
||||
{
|
||||
string csProjectPath = csProjectPaths[i];
|
||||
string csProjectGuid = csProjectGuids[i];
|
||||
string csProjectName = Path.GetFileNameWithoutExtension(csProjectPath);
|
||||
string csProjectRelativePath = Path.GetRelativePath(projectDirectory, csProjectPath);
|
||||
// FAE04EC0-301F-11D3-BF4B-00C04F79EFBC for C# projects.
|
||||
content.AppendLine($"Project(\"{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}\") = \"{csProjectName}\", \"{csProjectRelativePath}\", \"{csProjectGuid}\"");
|
||||
content.AppendLine("EndProject");
|
||||
}
|
||||
|
||||
// Add global sections (these can be extended as needed)
|
||||
content.AppendLine("Global");
|
||||
content.AppendLine(" GlobalSection(SolutionConfigurationPlatforms) = preSolution");
|
||||
content.AppendLine(" Test|x64 = Test|x64");
|
||||
content.AppendLine(" Release|x64 = Release|x64");
|
||||
content.AppendLine(" Debug|x64 = Debug|x64");
|
||||
content.AppendLine(" EndGlobalSection");
|
||||
content.AppendLine(" GlobalSection(ProjectConfigurationPlatforms) = postSolution");
|
||||
|
||||
for (int i = 0; i < csProjectPaths.Length; i++)
|
||||
{
|
||||
string projectGuid = csProjectGuids[i];
|
||||
content.AppendLine($" {projectGuid}.Test|x64.ActiveCfg = Test|x64");
|
||||
content.AppendLine($" {projectGuid}.Test|x64.Build.0 = Test|x64");
|
||||
content.AppendLine($" {projectGuid}.Release|x64.ActiveCfg = Release|x64");
|
||||
content.AppendLine($" {projectGuid}.Release|x64.Build.0 = Release|x64");
|
||||
content.AppendLine($" {projectGuid}.Debug|x64.ActiveCfg = Debug|x64");
|
||||
content.AppendLine($" {projectGuid}.Debug|x64.Build.0 = Debug|x64");
|
||||
}
|
||||
|
||||
content.AppendLine(" EndGlobalSection");
|
||||
content.AppendLine(" GlobalSection(SolutionProperties) = preSolution");
|
||||
content.AppendLine(" HideSolutionNode = FALSE");
|
||||
content.AppendLine(" EndGlobalSection");
|
||||
content.AppendLine("EndGlobal");
|
||||
|
||||
// Write the solution file content to disk
|
||||
string solutionName = project.Name + SolutionExtensionName;
|
||||
solutionFilePath = Path.Combine(projectDirectory, solutionName);
|
||||
File.WriteAllText(solutionFilePath, content.ToString());
|
||||
}
|
||||
|
||||
private static bool GenerateCSProject(string assemblyDefinitionFilePath, string projectPath, out string csProjectFilePath)
|
||||
{
|
||||
if (!File.Exists(assemblyDefinitionFilePath))
|
||||
{
|
||||
csProjectFilePath = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
string jsonString = File.ReadAllText(assemblyDefinitionFilePath);
|
||||
AssemblyDefinition assemblyDefinition = JsonSerializer.Deserialize(jsonString, AssemblyDefinitionContext.Default.AssemblyDefinition);
|
||||
|
||||
Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
Assembly runtimeAssembly = loadedAssemblies.FirstOrDefault(assembly => assembly.GetName().Name == "Nerfed.Runtime") ?? throw new Exception("Failed to find Runtime Assembly!");
|
||||
|
||||
// TODO: get all dependencies.
|
||||
// TODO: properly get assemblies.
|
||||
|
||||
StringBuilder content = new StringBuilder();
|
||||
|
||||
content.AppendLine("<Project Sdk=\"Microsoft.NET.Sdk\">");
|
||||
|
||||
content.AppendLine(" <PropertyGroup>");
|
||||
content.AppendLine(" <TargetFramework>net8.0</TargetFramework>");
|
||||
content.AppendLine(" <ImplicitUsings>enable</ImplicitUsings>");
|
||||
content.AppendLine(" <Nullable>disable</Nullable>");
|
||||
content.AppendLine(" <PublishAot>true</PublishAot>");
|
||||
content.AppendLine(" <InvariantGlobalization>true</InvariantGlobalization>");
|
||||
content.AppendLine(" <AllowUnsafeBlocks>true</AllowUnsafeBlocks>");
|
||||
content.AppendLine(" <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>");
|
||||
content.AppendLine(" <IsPackable>false</IsPackable>");
|
||||
content.AppendLine(" <Configurations>Debug;Test;Release</Configurations>");
|
||||
content.AppendLine(" <Platforms>x64</Platforms>");
|
||||
content.AppendLine(" </PropertyGroup>");
|
||||
|
||||
content.AppendLine(" <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == 'Debug|x64' \">");
|
||||
content.AppendLine(" <DefineConstants>TRACE;LOG_INFO;PROFILING</DefineConstants>");
|
||||
content.AppendLine(" </PropertyGroup>");
|
||||
|
||||
content.AppendLine(" <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == 'Test|x64' \">");
|
||||
content.AppendLine(" <DefineConstants>TRACE;LOG_ERROR;PROFILING</DefineConstants>");
|
||||
content.AppendLine(" <Optimize>true</Optimize>");
|
||||
content.AppendLine(" </PropertyGroup>");
|
||||
|
||||
content.AppendLine(" <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == 'Release|x64' \">");
|
||||
content.AppendLine(" <DefineConstants>TRACE;LOG_ERROR</DefineConstants>");
|
||||
content.AppendLine(" <Optimize>true</Optimize>");
|
||||
content.AppendLine(" </PropertyGroup>");
|
||||
|
||||
content.AppendLine(" <ItemGroup>");
|
||||
content.AppendLine($" <Compile Include=\"{assemblyDefinitionFilePath}/**/*.cs\"/>");
|
||||
content.AppendLine(" </ItemGroup>");
|
||||
|
||||
content.AppendLine(" <ItemGroup>");
|
||||
content.AppendLine(" <Reference Include=\"Nerfed.Runtime\">");
|
||||
content.AppendLine($" <HintPath>{runtimeAssembly.Location}</HintPath>");
|
||||
content.AppendLine(" <Private>false</Private>");
|
||||
content.AppendLine(" </Reference>");
|
||||
content.AppendLine(" </ItemGroup>");
|
||||
|
||||
content.AppendLine("</Project>");
|
||||
|
||||
string csProjectName = assemblyDefinition.Name + CSProjectExtensionName;
|
||||
csProjectFilePath = Path.Combine(projectPath, csProjectName);
|
||||
File.WriteAllText(csProjectFilePath, content.ToString());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ClearCSProjectFiles(string projectPath)
|
||||
{
|
||||
string[] csProjectFiles = Directory.GetFiles(projectPath, $"*{CSProjectExtensionName}", SearchOption.TopDirectoryOnly);
|
||||
foreach (string csProjectFile in csProjectFiles)
|
||||
{
|
||||
File.Delete(csProjectFile);
|
||||
}
|
||||
}
|
||||
}
|
26
Nerfed.Compiler/Nerfed.Compiler.csproj
Normal file
26
Nerfed.Compiler/Nerfed.Compiler.csproj
Normal file
@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<IsPackable>false</IsPackable>
|
||||
<Configurations>Debug;Test;Release</Configurations>
|
||||
<Platforms>x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
|
||||
<Optimize>false</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Test|x64' ">
|
||||
<Optimize>true</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
|
||||
<Optimize>true</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
15
Nerfed.Compiler/Program.cs
Normal file
15
Nerfed.Compiler/Program.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace Nerfed.Compiler;
|
||||
|
||||
public class Program
|
||||
{
|
||||
internal static void Main(string[] args)
|
||||
{
|
||||
if (args.Length != 2)
|
||||
{
|
||||
Console.WriteLine("projectFilePath, configuration (Debug, Test, Release)");
|
||||
return;
|
||||
}
|
||||
|
||||
Compiler.Compile(args[0], args[1]);
|
||||
}
|
||||
}
|
50
Nerfed.Compiler/Project.cs
Normal file
50
Nerfed.Compiler/Project.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Nerfed.Compiler;
|
||||
|
||||
public class Project
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public static bool Create(string path, string name, out Project project)
|
||||
{
|
||||
// Create project file.
|
||||
project = new Project
|
||||
{
|
||||
Name = name,
|
||||
};
|
||||
|
||||
Save(project, path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool Save(Project project, string projectFilePath)
|
||||
{
|
||||
string jsonString = JsonSerializer.Serialize(project, ProjectContext.Default.Project);
|
||||
|
||||
File.WriteAllText(projectFilePath, jsonString);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool Open(string path, out Project project)
|
||||
{
|
||||
string jsonString = File.ReadAllText(path);
|
||||
project = JsonSerializer.Deserialize(jsonString, ProjectContext.Default.Project);
|
||||
|
||||
if (project == null)
|
||||
{
|
||||
Console.WriteLine($"ERROR: Could not open {typeof(Project)}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(Project))]
|
||||
public partial class ProjectContext : JsonSerializerContext
|
||||
{
|
||||
}
|
21
Nerfed.Editor/CopyLibs.targets
Normal file
21
Nerfed.Editor/CopyLibs.targets
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Target Name="Runtime ID" AfterTargets="Build">
|
||||
<Message Text="Runtime ID: $(RuntimeIdentifier)" Importance="high"/>
|
||||
</Target>
|
||||
|
||||
<ItemGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">
|
||||
<Libs Include="..\Native\x64\**\*.*"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">
|
||||
<Libs Include="..\Native\lib64\**\*.*"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))">
|
||||
<Libs Include="..\Native\osx\**\*.*"/>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyLibs" AfterTargets="Build">
|
||||
<Copy SourceFiles="@(Libs)" DestinationFolder="$(OutDir)" SkipUnchangedFiles="true" OverwriteReadOnlyFiles="true"/>
|
||||
</Target>
|
||||
|
||||
</Project>
|
75
Nerfed.Editor/Editor/EditorGui.cs
Normal file
75
Nerfed.Editor/Editor/EditorGui.cs
Normal file
@ -0,0 +1,75 @@
|
||||
using ImGuiNET;
|
||||
using Nerfed.Editor.Project;
|
||||
using Nerfed.Runtime;
|
||||
using Nerfed.Runtime.Graphics;
|
||||
using Nerfed.Runtime.Gui;
|
||||
|
||||
namespace Nerfed.Editor
|
||||
{
|
||||
internal static class EditorGui
|
||||
{
|
||||
private static GuiController guiController;
|
||||
|
||||
internal static void Initialize()
|
||||
{
|
||||
// Create GuiController.
|
||||
guiController = new GuiController(Engine.GraphicsDevice, Engine.MainWindow, Color.DimGray);
|
||||
// Subscribe to GUI update.
|
||||
// GuiController.OnGui call => UpdateDock;
|
||||
// GuiController.OnGui call => UpdateEditorWindows;
|
||||
// GuiController.OnGui call => ...;
|
||||
guiController.OnGui += HandleOnGui;
|
||||
}
|
||||
|
||||
internal static void Update()
|
||||
{
|
||||
// Update GuiController.
|
||||
guiController.Update(Engine.Timestep.TotalSeconds);
|
||||
}
|
||||
|
||||
internal static void Render()
|
||||
{
|
||||
// Reneder GuiController.
|
||||
guiController.Render();
|
||||
}
|
||||
|
||||
internal static void Quit()
|
||||
{
|
||||
guiController.Dispose();
|
||||
}
|
||||
|
||||
private static void UpdateDock()
|
||||
{
|
||||
// Setup default dockspace for the main window.
|
||||
uint id = ImGui.GetID("MainDockSpace");
|
||||
ImGui.DockSpaceOverViewport(id, ImGui.GetMainViewport(), ImGuiDockNodeFlags.None);
|
||||
}
|
||||
|
||||
private static void UpdateMainMenu()
|
||||
{
|
||||
if (ImGui.BeginMainMenuBar())
|
||||
{
|
||||
if (ImGui.BeginMenu("File"))
|
||||
{
|
||||
if (ImGui.MenuItem("Exit"))
|
||||
{
|
||||
Engine.Quit();
|
||||
}
|
||||
ImGui.EndMenu();
|
||||
}
|
||||
|
||||
ImGui.EndMainMenuBar();
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleOnGui()
|
||||
{
|
||||
UpdateMainMenu();
|
||||
UpdateDock();
|
||||
|
||||
ImGui.ShowDemoWindow();
|
||||
|
||||
EditorProjectGui.OnGui();
|
||||
}
|
||||
}
|
||||
}
|
35
Nerfed.Editor/Nerfed.Editor.csproj
Normal file
35
Nerfed.Editor/Nerfed.Editor.csproj
Normal file
@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<IsPackable>false</IsPackable>
|
||||
<Configurations>Debug;Test;Release</Configurations>
|
||||
<Platforms>x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Test|x64' ">
|
||||
<Optimize>true</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
|
||||
<Optimize>true</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Nerfed.Compiler\Nerfed.Compiler.csproj" />
|
||||
<ProjectReference Include="..\Nerfed.Runtime\Nerfed.Runtime.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project=".\CopyLibs.targets" />
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command=""$(ProjectDir)../Bin/Nerfed.Builder/Nerfed.Builder" -build -resourcePath "$(ProjectDir)Resources" -resourceOutPath "$(TargetDir)Resources" " />
|
||||
</Target>
|
||||
</Project>
|
42
Nerfed.Editor/Program.cs
Normal file
42
Nerfed.Editor/Program.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using Nerfed.Runtime;
|
||||
|
||||
namespace Nerfed.Editor;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
Engine.OnInitialize += HandleOnInitialize;
|
||||
Engine.OnUpdate += HandleOnUpdate;
|
||||
Engine.OnRender += HandleOnRender;
|
||||
Engine.OnQuit += HandleOnQuit;
|
||||
|
||||
Engine.Run(args);
|
||||
}
|
||||
|
||||
private static void HandleOnInitialize()
|
||||
{
|
||||
// Open project.
|
||||
// Setip EditorGui.
|
||||
EditorGui.Initialize();
|
||||
}
|
||||
|
||||
private static void HandleOnUpdate()
|
||||
{
|
||||
// Editor Update.
|
||||
EditorGui.Update();
|
||||
|
||||
|
||||
// Try Catch UserCode Update.
|
||||
}
|
||||
|
||||
private static void HandleOnRender()
|
||||
{
|
||||
EditorGui.Render();
|
||||
}
|
||||
|
||||
private static void HandleOnQuit()
|
||||
{
|
||||
EditorGui.Quit();
|
||||
}
|
||||
}
|
8
Nerfed.Editor/Project/EditorAssemblyLoadContext.cs
Normal file
8
Nerfed.Editor/Project/EditorAssemblyLoadContext.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace Nerfed.Editor.Project;
|
||||
|
||||
internal class EditorAssemblyLoadContext : AssemblyLoadContext
|
||||
{
|
||||
public EditorAssemblyLoadContext() : base(isCollectible: true) { }
|
||||
}
|
124
Nerfed.Editor/Project/EditorAssemblyLoader.cs
Normal file
124
Nerfed.Editor/Project/EditorAssemblyLoader.cs
Normal file
@ -0,0 +1,124 @@
|
||||
using Nerfed.Runtime;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Nerfed.Editor.Project;
|
||||
|
||||
// https://github.com/godotengine/godot/blob/master/modules/mono/glue/GodotSharp/GodotPlugins/Main.c
|
||||
// https://gitlab.com/robertk92/assemblyreloadtest/-/blob/main/AppContextTest/Program.cs
|
||||
|
||||
internal static class EditorAssemblyLoader
|
||||
{
|
||||
internal sealed class EditorAssemblyLoadContextWrapper
|
||||
{
|
||||
private EditorAssemblyLoadContext assemblyLoadContext;
|
||||
private readonly WeakReference weakReference;
|
||||
|
||||
private EditorAssemblyLoadContextWrapper(EditorAssemblyLoadContext assemblyLoadContext, WeakReference weakReference)
|
||||
{
|
||||
this.assemblyLoadContext = assemblyLoadContext;
|
||||
this.weakReference = weakReference;
|
||||
}
|
||||
|
||||
public bool IsCollectible
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
// If assemblyLoadContext is null we already started unloading, so it was collectible.
|
||||
get => assemblyLoadContext?.IsCollectible ?? true;
|
||||
}
|
||||
|
||||
public bool IsAlive
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
get => weakReference.IsAlive;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static (Assembly, EditorAssemblyLoadContextWrapper) CreateAndLoad(AssemblyName assemblyName)
|
||||
{
|
||||
EditorAssemblyLoadContext context = new EditorAssemblyLoadContext();
|
||||
WeakReference reference = new WeakReference(context, trackResurrection: true);
|
||||
EditorAssemblyLoadContextWrapper wrapper = new EditorAssemblyLoadContextWrapper(context, reference);
|
||||
Assembly assembly = context.LoadFromAssemblyName(assemblyName);
|
||||
return (assembly, wrapper);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static (Assembly, EditorAssemblyLoadContextWrapper) CreateAndLoad(string assemblyPath)
|
||||
{
|
||||
EditorAssemblyLoadContext context = new EditorAssemblyLoadContext();
|
||||
WeakReference reference = new WeakReference(context, trackResurrection: true);
|
||||
EditorAssemblyLoadContextWrapper wrapper = new EditorAssemblyLoadContextWrapper(context, reference);
|
||||
Assembly assembly = context.LoadFromAssemblyPath(assemblyPath);
|
||||
return (assembly, wrapper);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal void Unload()
|
||||
{
|
||||
assemblyLoadContext?.Unload();
|
||||
assemblyLoadContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static (Assembly, EditorAssemblyLoadContextWrapper) Load(string assemblyFilePath)
|
||||
{
|
||||
string assemblyFileName = Path.GetFileNameWithoutExtension(assemblyFilePath);
|
||||
|
||||
AssemblyName assemblyName = new AssemblyName(assemblyFileName);
|
||||
|
||||
return EditorAssemblyLoadContextWrapper.CreateAndLoad(assemblyName);
|
||||
}
|
||||
|
||||
internal static (Assembly, EditorAssemblyLoadContextWrapper) LoadFromPath(string assemblyFilePath)
|
||||
{
|
||||
return EditorAssemblyLoadContextWrapper.CreateAndLoad(assemblyFilePath);
|
||||
}
|
||||
|
||||
internal static bool Unload(EditorAssemblyLoadContextWrapper assemblyLoadContextWrapper)
|
||||
{
|
||||
if (assemblyLoadContextWrapper == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!assemblyLoadContextWrapper.IsCollectible)
|
||||
{
|
||||
Log.Error($"{assemblyLoadContextWrapper} is not collectable!");
|
||||
return false;
|
||||
}
|
||||
|
||||
assemblyLoadContextWrapper.Unload();
|
||||
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
|
||||
TimeSpan timeout = TimeSpan.FromSeconds(30);
|
||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||
|
||||
while (assemblyLoadContextWrapper.IsAlive)
|
||||
{
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
|
||||
GC.WaitForPendingFinalizers();
|
||||
|
||||
if (!assemblyLoadContextWrapper.IsAlive)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (stopwatch.Elapsed.TotalSeconds % 10 == 0)
|
||||
{
|
||||
Log.Info("Tring to unload assembly...");
|
||||
}
|
||||
|
||||
if (stopwatch.Elapsed >= timeout)
|
||||
{
|
||||
Log.Error("Failed to unload assembly!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
173
Nerfed.Editor/Project/EditorProject.cs
Normal file
173
Nerfed.Editor/Project/EditorProject.cs
Normal file
@ -0,0 +1,173 @@
|
||||
using Nerfed.Runtime;
|
||||
|
||||
namespace Nerfed.Editor.Project;
|
||||
|
||||
internal static class EditorProject
|
||||
{
|
||||
internal static Compiler.Project Project { get; private set; } = null;
|
||||
internal static string ProjectFilePath { get; private set; } = string.Empty;
|
||||
internal static string ProjectSolutionFilePath { get; private set; } = string.Empty;
|
||||
internal static string ProjectDirectory { get; private set; } = string.Empty;
|
||||
internal static string ProjectContentDirectory { get; private set; } = string.Empty;
|
||||
internal static string ProjectTempDirectory { get; private set; } = string.Empty;
|
||||
|
||||
private static readonly List<(string, EditorAssemblyLoader.EditorAssemblyLoadContextWrapper)> editorAssemblyLoadContextWrappers = [];
|
||||
|
||||
internal static bool Create(string projectFilePath, string projectName)
|
||||
{
|
||||
Close();
|
||||
|
||||
if (!Compiler.Project.Create(projectFilePath, projectName, out Compiler.Project project))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Open(projectFilePath);
|
||||
|
||||
Log.Info($"Succesfully created project.");
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool Open(string projectFilePath)
|
||||
{
|
||||
Close();
|
||||
|
||||
if(!Compiler.Project.Open(projectFilePath, out Compiler.Project project))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Project = project;
|
||||
ProjectFilePath = projectFilePath;
|
||||
ProjectDirectory = Path.GetDirectoryName(projectFilePath);
|
||||
|
||||
string projectSolutionFilePath = Path.Combine(ProjectDirectory, Project.Name + Compiler.Generator.SolutionExtensionName);
|
||||
if (File.Exists(projectSolutionFilePath))
|
||||
{
|
||||
ProjectSolutionFilePath = projectSolutionFilePath;
|
||||
}
|
||||
|
||||
SetupDefaultFolders();
|
||||
Compile();
|
||||
|
||||
Log.Info($"Opened project: {project.Name}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void Close()
|
||||
{
|
||||
Project = null;
|
||||
ProjectFilePath = string.Empty;
|
||||
ProjectSolutionFilePath = string.Empty;
|
||||
ProjectDirectory = string.Empty;
|
||||
ProjectContentDirectory = string.Empty;
|
||||
ProjectTempDirectory = string.Empty;
|
||||
}
|
||||
|
||||
internal static bool Save()
|
||||
{
|
||||
if(Project == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Compiler.Project.Save(Project, ProjectFilePath);
|
||||
}
|
||||
|
||||
internal static void Compile()
|
||||
{
|
||||
if(Project == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UnloadAssemblies();
|
||||
|
||||
Compiler.Compiler.Compile(ProjectFilePath, "Debug");
|
||||
|
||||
LoadAssemblies();
|
||||
}
|
||||
|
||||
internal static void GenerateSolution()
|
||||
{
|
||||
if(Project == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Compiler.Generator.GenerateSolution(ProjectDirectory, Project, out string solutionFilePath);
|
||||
ProjectSolutionFilePath = solutionFilePath;
|
||||
}
|
||||
|
||||
private static void SetupDefaultFolders()
|
||||
{
|
||||
if (Project == null || ProjectDirectory == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string contentDirectory = Path.Combine(ProjectDirectory, "Content");
|
||||
if (!Directory.Exists(contentDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(contentDirectory);
|
||||
}
|
||||
ProjectContentDirectory = contentDirectory;
|
||||
string scriptsDirectory = Path.Combine(ProjectContentDirectory, "Scripts");
|
||||
if (!Directory.Exists(scriptsDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(scriptsDirectory);
|
||||
}
|
||||
string scriptsRuntimePath = Path.Combine(scriptsDirectory, "Runtime");
|
||||
if (!Directory.Exists(scriptsRuntimePath))
|
||||
{
|
||||
Directory.CreateDirectory(scriptsRuntimePath);
|
||||
}
|
||||
|
||||
// Test create csproject.
|
||||
string gameplayRuntimeFilePath = Path.Combine(scriptsRuntimePath, Compiler.Generator.AssemblyDefinitionExtensionName);
|
||||
if (!File.Exists(gameplayRuntimeFilePath))
|
||||
{
|
||||
Compiler.AssemblyDefinition.Create(gameplayRuntimeFilePath, "Gameplay", out Compiler.AssemblyDefinition project);
|
||||
}
|
||||
|
||||
string tempDirectory = Path.Combine(ProjectDirectory, "Temp");
|
||||
if (!Directory.Exists(tempDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(tempDirectory);
|
||||
}
|
||||
ProjectTempDirectory = tempDirectory;
|
||||
}
|
||||
|
||||
private static void LoadAssemblies()
|
||||
{
|
||||
string[] assemblies = Directory.GetFiles(Path.Combine(ProjectDirectory, "bin"), "*.dll", SearchOption.AllDirectories);
|
||||
|
||||
foreach (string assembly in assemblies)
|
||||
{
|
||||
(System.Reflection.Assembly, EditorAssemblyLoader.EditorAssemblyLoadContextWrapper) a = EditorAssemblyLoader.LoadFromPath(assembly);
|
||||
string name = a.Item1.GetName().Name;
|
||||
editorAssemblyLoadContextWrappers.Add((name, a.Item2));
|
||||
Log.Info($"loaded {name}");
|
||||
}
|
||||
|
||||
Nerfed.Runtime.Generator.Hook.InvokeHooks();
|
||||
}
|
||||
|
||||
private static void UnloadAssemblies()
|
||||
{
|
||||
for (int i = editorAssemblyLoadContextWrappers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
(string, EditorAssemblyLoader.EditorAssemblyLoadContextWrapper) a = editorAssemblyLoadContextWrappers[i];
|
||||
if (EditorAssemblyLoader.Unload(a.Item2))
|
||||
{
|
||||
Log.Info($"Unloaded {a.Item1}");
|
||||
editorAssemblyLoadContextWrappers.RemoveAt(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"Could not unload {a.Item1}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
67
Nerfed.Editor/Project/EditorProjectGui.cs
Normal file
67
Nerfed.Editor/Project/EditorProjectGui.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Nerfed.Editor.Project;
|
||||
|
||||
internal static class EditorProjectGui
|
||||
{
|
||||
private static string projectDirectory = string.Empty;
|
||||
private static string projectName = string.Empty;
|
||||
private static string projectFilePath = string.Empty;
|
||||
|
||||
internal static void OnGui()
|
||||
{
|
||||
ImGui.Begin("Project");
|
||||
ImGui.BeginGroup();
|
||||
|
||||
ImGui.InputText("Project Directory", ref projectDirectory, 512);
|
||||
ImGui.InputText("Project Name", ref projectName, 512);
|
||||
|
||||
string newProjectFilePath = Path.Combine(projectDirectory, ".project");
|
||||
ImGui.Text(newProjectFilePath);
|
||||
|
||||
if (ImGui.Button("Create Project"))
|
||||
{
|
||||
EditorProject.Create(newProjectFilePath, projectName);
|
||||
}
|
||||
|
||||
ImGui.EndGroup();
|
||||
ImGui.BeginGroup();
|
||||
|
||||
ImGui.InputText("Project File Path", ref projectFilePath, 512);
|
||||
if (ImGui.Button("Open Project"))
|
||||
{
|
||||
EditorProject.Open(projectFilePath);
|
||||
}
|
||||
|
||||
ImGui.Text("Loaded project: ");
|
||||
if(EditorProject.Project != null)
|
||||
{
|
||||
ImGui.Text(EditorProject.Project.Name);
|
||||
ImGui.Text(EditorProject.ProjectFilePath);
|
||||
ImGui.Text(EditorProject.ProjectSolutionFilePath);
|
||||
ImGui.Text(EditorProject.ProjectDirectory);
|
||||
ImGui.Text(EditorProject.ProjectContentDirectory);
|
||||
ImGui.Text(EditorProject.ProjectTempDirectory);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.Text("None");
|
||||
}
|
||||
|
||||
ImGui.EndGroup();
|
||||
ImGui.BeginGroup();
|
||||
|
||||
if (ImGui.Button("Generate Solution"))
|
||||
{
|
||||
EditorProject.GenerateSolution();
|
||||
}
|
||||
|
||||
if (ImGui.Button("Compile"))
|
||||
{
|
||||
EditorProject.Compile();
|
||||
}
|
||||
|
||||
ImGui.EndGroup();
|
||||
ImGui.End();
|
||||
}
|
||||
}
|
9
Nerfed.Editor/Resources/Shaders/Fullscreen.vert
Normal file
9
Nerfed.Editor/Resources/Shaders/Fullscreen.vert
Normal file
@ -0,0 +1,9 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) out vec2 outTexCoord;
|
||||
|
||||
void main()
|
||||
{
|
||||
outTexCoord = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
|
||||
gl_Position = vec4(outTexCoord * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
13
Nerfed.Editor/Resources/Shaders/ImGui.frag
Normal file
13
Nerfed.Editor/Resources/Shaders/ImGui.frag
Normal file
@ -0,0 +1,13 @@
|
||||
#version 450
|
||||
|
||||
layout (location = 0) in vec4 color;
|
||||
layout (location = 1) in vec2 texCoord;
|
||||
|
||||
layout(set = 2, binding = 0) uniform sampler2D Sampler;
|
||||
|
||||
layout (location = 0) out vec4 outputColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
outputColor = color * texture(Sampler, texCoord);
|
||||
}
|
20
Nerfed.Editor/Resources/Shaders/ImGui.vert
Normal file
20
Nerfed.Editor/Resources/Shaders/ImGui.vert
Normal file
@ -0,0 +1,20 @@
|
||||
#version 450
|
||||
|
||||
layout (location = 0) in vec2 in_position;
|
||||
layout (location = 1) in vec2 in_texCoord;
|
||||
layout (location = 2) in vec4 in_color;
|
||||
|
||||
layout (set = 1, binding = 0) uniform ProjectionMatrixBuffer
|
||||
{
|
||||
mat4 projection_matrix;
|
||||
};
|
||||
|
||||
layout (location = 0) out vec4 color;
|
||||
layout (location = 1) out vec2 texCoord;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = projection_matrix * vec4(in_position, 0, 1);
|
||||
color = in_color;
|
||||
texCoord = in_texCoord;
|
||||
}
|
34
Nerfed.Editor/Resources/Shaders/Text.frag
Normal file
34
Nerfed.Editor/Resources/Shaders/Text.frag
Normal file
@ -0,0 +1,34 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 inTexCoord;
|
||||
layout(location = 1) in vec4 inColor;
|
||||
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
layout(set = 2, binding = 0) uniform sampler2D msdf;
|
||||
|
||||
layout(set = 3, binding = 0) uniform UBO
|
||||
{
|
||||
float pxRange;
|
||||
} ubo;
|
||||
|
||||
float median(float r, float g, float b)
|
||||
{
|
||||
return max(min(r, g), min(max(r, g), b));
|
||||
}
|
||||
|
||||
float screenPxRange()
|
||||
{
|
||||
vec2 unitRange = vec2(ubo.pxRange)/vec2(textureSize(msdf, 0));
|
||||
vec2 screenTexSize = vec2(1.0)/fwidth(inTexCoord);
|
||||
return max(0.5*dot(unitRange, screenTexSize), 1.0);
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 msd = texture(msdf, inTexCoord).rgb;
|
||||
float sd = median(msd.r, msd.g, msd.b);
|
||||
float screenPxDistance = screenPxRange() * (sd - 0.5);
|
||||
float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0);
|
||||
outColor = mix(vec4(0.0, 0.0, 0.0, 0.0), inColor, opacity);
|
||||
}
|
20
Nerfed.Editor/Resources/Shaders/Text.vert
Normal file
20
Nerfed.Editor/Resources/Shaders/Text.vert
Normal file
@ -0,0 +1,20 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec3 inPos;
|
||||
layout(location = 1) in vec2 inTexCoord;
|
||||
layout(location = 2) in vec4 inColor;
|
||||
|
||||
layout(location = 0) out vec2 outTexCoord;
|
||||
layout(location = 1) out vec4 outColor;
|
||||
|
||||
layout(set = 1, binding = 0) uniform UBO
|
||||
{
|
||||
mat4 ViewProjection;
|
||||
} ubo;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = ubo.ViewProjection * vec4(inPos, 1.0);
|
||||
outTexCoord = inTexCoord;
|
||||
outColor = inColor;
|
||||
}
|
38
Nerfed.Editor/Resources/Shaders/Video.frag
Normal file
38
Nerfed.Editor/Resources/Shaders/Video.frag
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* This effect is based on the YUV-to-RGBA GLSL shader found in SDL.
|
||||
* Thus, it also released under the zlib license:
|
||||
* http://libsdl.org/license.php
|
||||
*/
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 TexCoord;
|
||||
|
||||
layout(location = 0) out vec4 FragColor;
|
||||
|
||||
layout(set = 2, binding = 0) uniform sampler2D YSampler;
|
||||
layout(set = 2, binding = 1) uniform sampler2D USampler;
|
||||
layout(set = 2, binding = 2) uniform sampler2D VSampler;
|
||||
|
||||
/* More info about colorspace conversion:
|
||||
* http://www.equasys.de/colorconversion.html
|
||||
* http://www.equasys.de/colorformat.html
|
||||
*/
|
||||
|
||||
const vec3 offset = vec3(-0.0625, -0.5, -0.5);
|
||||
const vec3 Rcoeff = vec3(1.164, 0.000, 1.793);
|
||||
const vec3 Gcoeff = vec3(1.164, -0.213, -0.533);
|
||||
const vec3 Bcoeff = vec3(1.164, 2.112, 0.000);
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 yuv;
|
||||
yuv.x = texture(YSampler, TexCoord).r;
|
||||
yuv.y = texture(USampler, TexCoord).r;
|
||||
yuv.z = texture(VSampler, TexCoord).r;
|
||||
yuv += offset;
|
||||
|
||||
FragColor.r = dot(yuv, Rcoeff);
|
||||
FragColor.g = dot(yuv, Gcoeff);
|
||||
FragColor.b = dot(yuv, Bcoeff);
|
||||
FragColor.a = 1.0;
|
||||
}
|
91
Nerfed.Runtime.Generator/HookSourceGenerator.cs
Normal file
91
Nerfed.Runtime.Generator/HookSourceGenerator.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Nerfed.Runtime.Generator
|
||||
{
|
||||
[Generator]
|
||||
public class HookSourceGenerator : ISourceGenerator
|
||||
{
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
{
|
||||
// Ensure the syntax receiver is not null and is of the expected type
|
||||
if (context.SyntaxReceiver is not HookSyntaxReceiver syntaxReceiver)
|
||||
return;
|
||||
|
||||
// Check if we have collected any hook methods
|
||||
List<MethodDeclarationSyntax> hookMethods = syntaxReceiver.HookMethods;
|
||||
if (hookMethods == null || !hookMethods.Any())
|
||||
return;
|
||||
|
||||
StringBuilder codeBuilder = new StringBuilder();
|
||||
|
||||
codeBuilder.AppendLine("using System;");
|
||||
codeBuilder.AppendLine("");
|
||||
codeBuilder.AppendLine("namespace Nerfed.Runtime.Generator;");
|
||||
codeBuilder.AppendLine("");
|
||||
codeBuilder.AppendLine($"// Generated by {typeof(HookSourceGenerator)}");
|
||||
codeBuilder.AppendLine("public static class Hook");
|
||||
codeBuilder.AppendLine("{");
|
||||
codeBuilder.AppendLine(" public static void InvokeHooks()");
|
||||
codeBuilder.AppendLine(" {");
|
||||
|
||||
foreach (MethodDeclarationSyntax method in hookMethods)
|
||||
{
|
||||
SemanticModel model = context.Compilation.GetSemanticModel(method.SyntaxTree);
|
||||
|
||||
if (model.GetDeclaredSymbol(method) is not IMethodSymbol methodSymbol)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (methodSymbol.DeclaredAccessibility != Accessibility.Public || !methodSymbol.IsStatic)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
codeBuilder.AppendLine($" {methodSymbol.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.{methodSymbol.Name}();");
|
||||
}
|
||||
|
||||
codeBuilder.AppendLine(" }");
|
||||
codeBuilder.AppendLine("}");
|
||||
|
||||
// Add the generated code to the compilation
|
||||
context.AddSource("Hook.g.cs", codeBuilder.ToString());
|
||||
}
|
||||
|
||||
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterForSyntaxNotifications(() => new HookSyntaxReceiver());
|
||||
}
|
||||
|
||||
public class HookSyntaxReceiver : ISyntaxReceiver
|
||||
{
|
||||
public List<MethodDeclarationSyntax> HookMethods { get; } = new List<MethodDeclarationSyntax>();
|
||||
|
||||
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
|
||||
{
|
||||
// Check if the node is a method declaration
|
||||
if (syntaxNode is MethodDeclarationSyntax methodDeclaration)
|
||||
{
|
||||
// Ensure the method declaration has attribute lists
|
||||
if (methodDeclaration.AttributeLists.Count == 0)
|
||||
return;
|
||||
|
||||
// Check if the method has the Hook attribute
|
||||
bool hasHookAttribute = methodDeclaration.AttributeLists
|
||||
.SelectMany(attrList => attrList.Attributes)
|
||||
.Any(attr => attr.Name.ToString() == "Hook");
|
||||
|
||||
if (hasHookAttribute)
|
||||
{
|
||||
HookMethods.Add(methodDeclaration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
Nerfed.Runtime.Generator/Nerfed.Runtime.Generator.csproj
Normal file
33
Nerfed.Runtime.Generator/Nerfed.Runtime.Generator.csproj
Normal file
@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<Configurations>Debug;Test;Release</Configurations>
|
||||
<Platforms>x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.11.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
|
||||
<DefineConstants>TRACE;LOG_INFO;PROFILING</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Test|x64' ">
|
||||
<DefineConstants>TRACE;LOG_ERROR;PROFILING</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
|
||||
<DefineConstants>TRACE;LOG_ERROR</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
24
Nerfed.Runtime/Assert.cs
Normal file
24
Nerfed.Runtime/Assert.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Nerfed.Runtime;
|
||||
|
||||
public class AssertionException(string msg) : Exception(msg);
|
||||
|
||||
public static class Assert
|
||||
{
|
||||
[Conditional("DEBUG"), DebuggerHidden]
|
||||
public static void Debug([DoesNotReturnIf(false)] bool cond, [CallerArgumentExpression("cond")] string expression = "") {
|
||||
if (!cond) {
|
||||
throw new AssertionException(expression);
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerHidden]
|
||||
public static void Always([DoesNotReturnIf(false)] bool cond, [CallerArgumentExpression("cond")] string expression = "") {
|
||||
if (!cond) {
|
||||
throw new AssertionException(expression);
|
||||
}
|
||||
}
|
||||
}
|
78
Nerfed.Runtime/Audio/AudioBuffer.cs
Normal file
78
Nerfed.Runtime/Audio/AudioBuffer.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nerfed.Runtime.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);
|
||||
}
|
||||
}
|
146
Nerfed.Runtime/Audio/AudioDataOgg.cs
Normal file
146
Nerfed.Runtime/Audio/AudioDataOgg.cs
Normal file
@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nerfed.Runtime.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;
|
||||
|
||||
IntPtr handle = FAudio.stb_vorbis_open_filename(filePath, out int error, IntPtr.Zero);
|
||||
|
||||
if (error != 0)
|
||||
{
|
||||
throw new InvalidOperationException("Error loading file!");
|
||||
}
|
||||
|
||||
FAudio.stb_vorbis_info info = FAudio.stb_vorbis_get_info(handle);
|
||||
|
||||
Format = new Format
|
||||
{
|
||||
Tag = FormatTag.IEEE_FLOAT,
|
||||
BitsPerSample = 32,
|
||||
Channels = (ushort) info.channels,
|
||||
SampleRate = info.sample_rate
|
||||
};
|
||||
|
||||
FAudio.stb_vorbis_close(handle);
|
||||
}
|
||||
|
||||
public override unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd)
|
||||
{
|
||||
int lengthInFloats = bufferLengthInBytes / sizeof(float);
|
||||
|
||||
/* NOTE: this function returns samples per channel, not total samples */
|
||||
int samples = FAudio.stb_vorbis_get_samples_float_interleaved(
|
||||
VorbisHandle,
|
||||
Format.Channels,
|
||||
(IntPtr) buffer,
|
||||
lengthInFloats
|
||||
);
|
||||
|
||||
int sampleCount = samples * Format.Channels;
|
||||
reachedEnd = sampleCount < lengthInFloats;
|
||||
filledLengthInBytes = sampleCount * sizeof(float);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the Ogg data for streaming.
|
||||
/// </summary>
|
||||
public override unsafe void Load()
|
||||
{
|
||||
if (!Loaded)
|
||||
{
|
||||
FileStream fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
|
||||
FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length);
|
||||
Span<byte> 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);
|
||||
Log.Error("Error opening OGG file!");
|
||||
Log.Error("Error: " + error);
|
||||
throw new InvalidOperationException("Error opening OGG file!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Seek(uint sampleFrame)
|
||||
{
|
||||
FAudio.stb_vorbis_seek(VorbisHandle, sampleFrame);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
IntPtr filePointer = FAudio.stb_vorbis_open_filename(filePath, out int error, IntPtr.Zero);
|
||||
|
||||
if (error != 0)
|
||||
{
|
||||
throw new InvalidOperationException("Error loading file!");
|
||||
}
|
||||
FAudio.stb_vorbis_info info = FAudio.stb_vorbis_get_info(filePointer);
|
||||
long lengthInFloats =
|
||||
FAudio.stb_vorbis_stream_length_in_samples(filePointer) * info.channels;
|
||||
long lengthInBytes = lengthInFloats * Marshal.SizeOf<float>();
|
||||
void* buffer = NativeMemory.Alloc((nuint) lengthInBytes);
|
||||
|
||||
FAudio.stb_vorbis_get_samples_float_interleaved(
|
||||
filePointer,
|
||||
info.channels,
|
||||
(nint) buffer,
|
||||
(int) lengthInFloats
|
||||
);
|
||||
|
||||
FAudio.stb_vorbis_close(filePointer);
|
||||
|
||||
Format format = new Format
|
||||
{
|
||||
Tag = FormatTag.IEEE_FLOAT,
|
||||
BitsPerSample = 32,
|
||||
Channels = (ushort) info.channels,
|
||||
SampleRate = info.sample_rate
|
||||
};
|
||||
|
||||
return new AudioBuffer(
|
||||
device,
|
||||
format,
|
||||
(nint) buffer,
|
||||
(uint) lengthInBytes,
|
||||
true);
|
||||
}
|
||||
}
|
163
Nerfed.Runtime/Audio/AudioDataQoa.cs
Normal file
163
Nerfed.Runtime/Audio/AudioDataQoa.cs
Normal file
@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nerfed.Runtime.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 FileStream stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
|
||||
using BinaryReader reader = new BinaryReader(stream);
|
||||
|
||||
UInt64 fileHeader = ReverseEndianness(reader.ReadUInt64());
|
||||
if ((fileHeader >> 32) != QOA_MAGIC)
|
||||
{
|
||||
throw new InvalidOperationException("Specified file is not a QOA file.");
|
||||
}
|
||||
|
||||
uint totalSamplesPerChannel = (uint) (fileHeader & (0xFFFFFFFF));
|
||||
if (totalSamplesPerChannel == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Specified file is not a valid QOA file.");
|
||||
}
|
||||
|
||||
UInt64 frameHeader = ReverseEndianness(reader.ReadUInt64());
|
||||
uint channels = (uint) ((frameHeader >> 56) & 0x0000FF);
|
||||
uint samplerate = (uint) ((frameHeader >> 32) & 0xFFFFFF);
|
||||
uint samplesPerChannelPerFrame = (uint) ((frameHeader >> 16) & 0x00FFFF);
|
||||
|
||||
Format = new Format
|
||||
{
|
||||
Tag = FormatTag.PCM,
|
||||
BitsPerSample = 16,
|
||||
Channels = (ushort) channels,
|
||||
SampleRate = samplerate
|
||||
};
|
||||
|
||||
decodeBufferSize = channels * samplesPerChannelPerFrame * sizeof(short);
|
||||
}
|
||||
|
||||
public override unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd)
|
||||
{
|
||||
int lengthInShorts = bufferLengthInBytes / sizeof(short);
|
||||
|
||||
// NOTE: this function returns samples per channel!
|
||||
uint samples = FAudio.qoa_decode_next_frame(QoaHandle, (short*) buffer);
|
||||
|
||||
uint sampleCount = samples * Format.Channels;
|
||||
reachedEnd = sampleCount < lengthInShorts;
|
||||
filledLengthInBytes = (int) (sampleCount * sizeof(short));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares qoa data for streaming.
|
||||
/// </summary>
|
||||
public override unsafe void Load()
|
||||
{
|
||||
if (!Loaded)
|
||||
{
|
||||
FileStream fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
|
||||
FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length);
|
||||
Span<byte> 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);
|
||||
Log.Error("Error opening QOA file!");
|
||||
throw new InvalidOperationException("Error opening QOA file!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Seek(uint sampleFrame)
|
||||
{
|
||||
FAudio.qoa_seek_frame(QoaHandle, (int) sampleFrame);
|
||||
}
|
||||
|
||||
/// <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 FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
void* fileDataPtr = NativeMemory.Alloc((nuint) fileStream.Length);
|
||||
Span<byte> fileDataSpan = new Span<byte>(fileDataPtr, (int) fileStream.Length);
|
||||
fileStream.ReadExactly(fileDataSpan);
|
||||
fileStream.Close();
|
||||
|
||||
IntPtr qoaHandle = FAudio.qoa_open_from_memory((char*) fileDataPtr, (uint) fileDataSpan.Length, 0);
|
||||
if (qoaHandle == 0)
|
||||
{
|
||||
NativeMemory.Free(fileDataPtr);
|
||||
Log.Error("Error opening QOA file!");
|
||||
throw new InvalidOperationException("Error opening QOA file!");
|
||||
}
|
||||
|
||||
FAudio.qoa_attributes(qoaHandle, out uint channels, out uint samplerate, out uint samples_per_channel_per_frame, out uint total_samples_per_channel);
|
||||
|
||||
uint bufferLengthInBytes = total_samples_per_channel * channels * sizeof(short);
|
||||
void* buffer = NativeMemory.Alloc(bufferLengthInBytes);
|
||||
FAudio.qoa_decode_entire(qoaHandle, (short*) buffer);
|
||||
|
||||
FAudio.qoa_close(qoaHandle);
|
||||
NativeMemory.Free(fileDataPtr);
|
||||
|
||||
Format format = new Format
|
||||
{
|
||||
Tag = FormatTag.PCM,
|
||||
BitsPerSample = 16,
|
||||
Channels = (ushort) channels,
|
||||
SampleRate = samplerate
|
||||
};
|
||||
|
||||
return new AudioBuffer(device, format, (nint) buffer, bufferLengthInBytes, true);
|
||||
}
|
||||
|
||||
private static unsafe UInt64 ReverseEndianness(UInt64 value)
|
||||
{
|
||||
byte* bytes = (byte*) &value;
|
||||
|
||||
return
|
||||
((UInt64)(bytes[0]) << 56) | ((UInt64)(bytes[1]) << 48) |
|
||||
((UInt64)(bytes[2]) << 40) | ((UInt64)(bytes[3]) << 32) |
|
||||
((UInt64)(bytes[4]) << 24) | ((UInt64)(bytes[5]) << 16) |
|
||||
((UInt64)(bytes[6]) << 8) | ((UInt64)(bytes[7]) << 0);
|
||||
}
|
||||
}
|
48
Nerfed.Runtime/Audio/AudioDataStreamable.cs
Normal file
48
Nerfed.Runtime/Audio/AudioDataStreamable.cs
Normal file
@ -0,0 +1,48 @@
|
||||
namespace Nerfed.Runtime.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);
|
||||
}
|
||||
}
|
99
Nerfed.Runtime/Audio/AudioDataWav.cs
Normal file
99
Nerfed.Runtime/Audio/AudioDataWav.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nerfed.Runtime.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 FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
using BinaryReader reader = new BinaryReader(stream);
|
||||
|
||||
// RIFF Signature
|
||||
string signature = new string(reader.ReadChars(4));
|
||||
if (signature != "RIFF")
|
||||
{
|
||||
throw new NotSupportedException("Specified stream is not a wave file.");
|
||||
}
|
||||
|
||||
reader.ReadUInt32(); // Riff Chunk Size
|
||||
|
||||
string wformat = new string(reader.ReadChars(4));
|
||||
if (wformat != "WAVE")
|
||||
{
|
||||
throw new NotSupportedException("Specified stream is not a wave file.");
|
||||
}
|
||||
|
||||
// WAVE Header
|
||||
string format_signature = new string(reader.ReadChars(4));
|
||||
while (format_signature != "fmt ")
|
||||
{
|
||||
reader.ReadBytes(reader.ReadInt32());
|
||||
format_signature = new string(reader.ReadChars(4));
|
||||
}
|
||||
|
||||
int format_chunk_size = reader.ReadInt32();
|
||||
|
||||
wFormatTag = reader.ReadUInt16();
|
||||
nChannels = reader.ReadUInt16();
|
||||
nSamplesPerSec = reader.ReadUInt32();
|
||||
nAvgBytesPerSec = reader.ReadUInt32();
|
||||
nBlockAlign = reader.ReadUInt16();
|
||||
wBitsPerSample = reader.ReadUInt16();
|
||||
|
||||
// Reads residual bytes
|
||||
if (format_chunk_size > 16)
|
||||
{
|
||||
reader.ReadBytes(format_chunk_size - 16);
|
||||
}
|
||||
|
||||
// data Signature
|
||||
string data_signature = new string(reader.ReadChars(4));
|
||||
while (data_signature.ToLowerInvariant() != "data")
|
||||
{
|
||||
reader.ReadBytes(reader.ReadInt32());
|
||||
data_signature = new string(reader.ReadChars(4));
|
||||
}
|
||||
if (data_signature != "data")
|
||||
{
|
||||
throw new NotSupportedException("Specified wave file is not supported.");
|
||||
}
|
||||
|
||||
int waveDataLength = reader.ReadInt32();
|
||||
void* waveDataBuffer = NativeMemory.Alloc((nuint) waveDataLength);
|
||||
Span<byte> waveDataSpan = new Span<byte>(waveDataBuffer, waveDataLength);
|
||||
stream.ReadExactly(waveDataSpan);
|
||||
|
||||
Format format = new Format
|
||||
{
|
||||
Tag = (FormatTag) wFormatTag,
|
||||
BitsPerSample = wBitsPerSample,
|
||||
Channels = nChannels,
|
||||
SampleRate = nSamplesPerSec
|
||||
};
|
||||
|
||||
return new AudioBuffer(
|
||||
device,
|
||||
format,
|
||||
(nint) waveDataBuffer,
|
||||
(uint) waveDataLength,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
309
Nerfed.Runtime/Audio/AudioDevice.cs
Normal file
309
Nerfed.Runtime/Audio/AudioDevice.cs
Normal file
@ -0,0 +1,309 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nerfed.Runtime.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// AudioDevice manages all audio-related concerns.
|
||||
/// </summary>
|
||||
public class AudioDevice : IDisposable
|
||||
{
|
||||
public IntPtr Handle { get; }
|
||||
public byte[] Handle3D { get; }
|
||||
public FAudio.FAudioDeviceDetails DeviceDetails { get; }
|
||||
|
||||
private IntPtr trueMasteringVoice;
|
||||
|
||||
// this is a fun little trick where we use a submix voice as a "faux" mastering voice
|
||||
// this lets us maintain API consistency for effects like panning and reverb
|
||||
private SubmixVoice fauxMasteringVoice;
|
||||
public SubmixVoice MasteringVoice => fauxMasteringVoice;
|
||||
|
||||
public float CurveDistanceScalar = 1f;
|
||||
public float DopplerScale = 1f;
|
||||
public float SpeedOfSound = 343.5f;
|
||||
|
||||
private readonly HashSet<GCHandle> resourceHandles = new HashSet<GCHandle>();
|
||||
private readonly HashSet<UpdatingSourceVoice> updatingSourceVoices = new HashSet<UpdatingSourceVoice>();
|
||||
|
||||
private SourceVoicePool VoicePool;
|
||||
private List<SourceVoice> VoicesToReturn = new List<SourceVoice>();
|
||||
|
||||
private const int Step = 200;
|
||||
private TimeSpan UpdateInterval;
|
||||
private System.Diagnostics.Stopwatch TickStopwatch = new System.Diagnostics.Stopwatch();
|
||||
private long previousTickTime;
|
||||
private Thread Thread;
|
||||
private AutoResetEvent WakeSignal;
|
||||
internal readonly object StateLock = new object();
|
||||
|
||||
private bool Running;
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
internal unsafe AudioDevice()
|
||||
{
|
||||
UpdateInterval = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / Step);
|
||||
|
||||
FAudio.FAudioCreate(out IntPtr handle, 0, FAudio.FAUDIO_DEFAULT_PROCESSOR);
|
||||
Handle = handle;
|
||||
|
||||
/* Find a suitable device */
|
||||
|
||||
FAudio.FAudio_GetDeviceCount(Handle, out uint devices);
|
||||
|
||||
if (devices == 0)
|
||||
{
|
||||
Log.Error("No audio devices found!");
|
||||
FAudio.FAudio_Release(Handle);
|
||||
Handle = IntPtr.Zero;
|
||||
return;
|
||||
}
|
||||
|
||||
FAudio.FAudioDeviceDetails deviceDetails;
|
||||
|
||||
uint i = 0;
|
||||
for (i = 0; i < devices; i++)
|
||||
{
|
||||
FAudio.FAudio_GetDeviceDetails(
|
||||
Handle,
|
||||
i,
|
||||
out deviceDetails
|
||||
);
|
||||
if ((deviceDetails.Role & FAudio.FAudioDeviceRole.FAudioDefaultGameDevice) == FAudio.FAudioDeviceRole.FAudioDefaultGameDevice)
|
||||
{
|
||||
DeviceDetails = deviceDetails;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i == devices)
|
||||
{
|
||||
i = 0; /* whatever we'll just use the first one I guess */
|
||||
FAudio.FAudio_GetDeviceDetails(
|
||||
Handle,
|
||||
i,
|
||||
out deviceDetails
|
||||
);
|
||||
DeviceDetails = deviceDetails;
|
||||
}
|
||||
|
||||
/* Init Mastering Voice */
|
||||
uint result = FAudio.FAudio_CreateMasteringVoice(
|
||||
Handle,
|
||||
out trueMasteringVoice,
|
||||
FAudio.FAUDIO_DEFAULT_CHANNELS,
|
||||
FAudio.FAUDIO_DEFAULT_SAMPLERATE,
|
||||
0,
|
||||
i,
|
||||
IntPtr.Zero
|
||||
);
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
Log.Error("Failed to create a mastering voice!");
|
||||
Log.Error("Audio device creation failed!");
|
||||
return;
|
||||
}
|
||||
|
||||
fauxMasteringVoice = SubmixVoice.CreateFauxMasteringVoice(this);
|
||||
|
||||
/* Init 3D Audio */
|
||||
|
||||
Handle3D = new byte[FAudio.F3DAUDIO_HANDLE_BYTESIZE];
|
||||
FAudio.F3DAudioInitialize(
|
||||
DeviceDetails.OutputFormat.dwChannelMask,
|
||||
SpeedOfSound,
|
||||
Handle3D
|
||||
);
|
||||
|
||||
VoicePool = new SourceVoicePool(this);
|
||||
|
||||
WakeSignal = new AutoResetEvent(true);
|
||||
|
||||
Thread = new Thread(ThreadMain);
|
||||
Thread.IsBackground = true;
|
||||
Thread.Start();
|
||||
|
||||
Running = true;
|
||||
|
||||
TickStopwatch.Start();
|
||||
previousTickTime = 0;
|
||||
}
|
||||
|
||||
private void ThreadMain()
|
||||
{
|
||||
while (Running)
|
||||
{
|
||||
lock (StateLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
ThreadMainTick();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
WakeSignal.WaitOne(UpdateInterval);
|
||||
}
|
||||
}
|
||||
|
||||
private void ThreadMainTick()
|
||||
{
|
||||
previousTickTime = TickStopwatch.Elapsed.Ticks;
|
||||
foreach (UpdatingSourceVoice voice in updatingSourceVoices)
|
||||
{
|
||||
voice.Update();
|
||||
}
|
||||
|
||||
foreach (SourceVoice voice in VoicesToReturn)
|
||||
{
|
||||
if (voice is UpdatingSourceVoice updatingSourceVoice)
|
||||
{
|
||||
updatingSourceVoices.Remove(updatingSourceVoice);
|
||||
}
|
||||
|
||||
voice.Reset();
|
||||
VoicePool.Return(voice);
|
||||
}
|
||||
|
||||
VoicesToReturn.Clear();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
T 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 WakeThread()
|
||||
{
|
||||
WakeSignal.Set();
|
||||
}
|
||||
|
||||
internal void AddResourceReference(GCHandle resourceReference)
|
||||
{
|
||||
lock (StateLock)
|
||||
{
|
||||
resourceHandles.Add(resourceReference);
|
||||
|
||||
if (resourceReference.Target is UpdatingSourceVoice updatableVoice)
|
||||
{
|
||||
updatingSourceVoices.Add(updatableVoice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveResourceReference(GCHandle resourceReference)
|
||||
{
|
||||
lock (StateLock)
|
||||
{
|
||||
resourceHandles.Remove(resourceReference);
|
||||
|
||||
if (resourceReference.Target is UpdatingSourceVoice updatableVoice)
|
||||
{
|
||||
updatingSourceVoices.Remove(updatableVoice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
Running = false;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
Thread.Join();
|
||||
|
||||
// dispose all source voices first
|
||||
foreach (GCHandle handle in resourceHandles)
|
||||
{
|
||||
if (handle.Target is SourceVoice voice)
|
||||
{
|
||||
voice.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// dispose all submix voices except the faux mastering voice
|
||||
foreach (GCHandle handle in resourceHandles)
|
||||
{
|
||||
if (handle.Target is SubmixVoice voice && voice != fauxMasteringVoice)
|
||||
{
|
||||
voice.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// dispose the faux mastering voice
|
||||
fauxMasteringVoice.Dispose();
|
||||
|
||||
// dispose the true mastering voice
|
||||
FAudio.FAudioVoice_DestroyVoice(trueMasteringVoice);
|
||||
|
||||
// destroy all other audio resources
|
||||
foreach (GCHandle handle in resourceHandles)
|
||||
{
|
||||
if (handle.Target is AudioResource resource)
|
||||
{
|
||||
resource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
resourceHandles.Clear();
|
||||
}
|
||||
|
||||
FAudio.FAudio_Release(Handle);
|
||||
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
~AudioDevice()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
135
Nerfed.Runtime/Audio/AudioEmitter.cs
Normal file
135
Nerfed.Runtime/Audio/AudioEmitter.cs
Normal file
@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nerfed.Runtime.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// An emitter for 3D spatial audio.
|
||||
/// </summary>
|
||||
public class AudioEmitter : AudioResource
|
||||
{
|
||||
internal FAudio.F3DAUDIO_EMITTER emitterData;
|
||||
|
||||
public float DopplerScale
|
||||
{
|
||||
get
|
||||
{
|
||||
return emitterData.DopplerScaler;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value < 0.0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("AudioEmitter.DopplerScale must be greater than or equal to 0.0f");
|
||||
}
|
||||
emitterData.DopplerScaler = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector3 Forward
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Vector3(
|
||||
emitterData.OrientFront.x,
|
||||
emitterData.OrientFront.y,
|
||||
-emitterData.OrientFront.z
|
||||
);
|
||||
}
|
||||
set
|
||||
{
|
||||
emitterData.OrientFront.x = value.X;
|
||||
emitterData.OrientFront.y = value.Y;
|
||||
emitterData.OrientFront.z = -value.Z;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector3 Position
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Vector3(
|
||||
emitterData.Position.x,
|
||||
emitterData.Position.y,
|
||||
-emitterData.Position.z
|
||||
);
|
||||
}
|
||||
set
|
||||
{
|
||||
emitterData.Position.x = value.X;
|
||||
emitterData.Position.y = value.Y;
|
||||
emitterData.Position.z = -value.Z;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Vector3 Up
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Vector3(
|
||||
emitterData.OrientTop.x,
|
||||
emitterData.OrientTop.y,
|
||||
-emitterData.OrientTop.z
|
||||
);
|
||||
}
|
||||
set
|
||||
{
|
||||
emitterData.OrientTop.x = value.X;
|
||||
emitterData.OrientTop.y = value.Y;
|
||||
emitterData.OrientTop.z = -value.Z;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector3 Velocity
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Vector3(
|
||||
emitterData.Velocity.x,
|
||||
emitterData.Velocity.y,
|
||||
-emitterData.Velocity.z
|
||||
);
|
||||
}
|
||||
set
|
||||
{
|
||||
emitterData.Velocity.x = value.X;
|
||||
emitterData.Velocity.y = value.Y;
|
||||
emitterData.Velocity.z = -value.Z;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly float[] stereoAzimuth = new float[]
|
||||
{
|
||||
0.0f, 0.0f
|
||||
};
|
||||
|
||||
private static readonly GCHandle stereoAzimuthHandle = GCHandle.Alloc(
|
||||
stereoAzimuth,
|
||||
GCHandleType.Pinned
|
||||
);
|
||||
|
||||
public AudioEmitter(AudioDevice device) : base(device)
|
||||
{
|
||||
emitterData = new FAudio.F3DAUDIO_EMITTER();
|
||||
|
||||
DopplerScale = 1f;
|
||||
Forward = Vector3.UnitZ;
|
||||
Position = Vector3.Zero;
|
||||
Up = Vector3.UnitY;
|
||||
Velocity = Vector3.Zero;
|
||||
|
||||
/* Unexposed variables, defaults based on XNA behavior */
|
||||
emitterData.pCone = IntPtr.Zero;
|
||||
emitterData.ChannelCount = 1;
|
||||
emitterData.ChannelRadius = 1.0f;
|
||||
emitterData.pChannelAzimuths = stereoAzimuthHandle.AddrOfPinnedObject();
|
||||
emitterData.pVolumeCurve = IntPtr.Zero;
|
||||
emitterData.pLFECurve = IntPtr.Zero;
|
||||
emitterData.pLPFDirectCurve = IntPtr.Zero;
|
||||
emitterData.pLPFReverbCurve = IntPtr.Zero;
|
||||
emitterData.pReverbCurve = IntPtr.Zero;
|
||||
emitterData.CurveDistanceScaler = 1.0f;
|
||||
}
|
||||
}
|
97
Nerfed.Runtime/Audio/AudioListener.cs
Normal file
97
Nerfed.Runtime/Audio/AudioListener.cs
Normal file
@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Nerfed.Runtime.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// A listener for 3D spatial audio. Usually attached to a camera.
|
||||
/// </summary>
|
||||
public class AudioListener : AudioResource
|
||||
{
|
||||
internal FAudio.F3DAUDIO_LISTENER listenerData;
|
||||
|
||||
public Vector3 Forward
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Vector3(
|
||||
listenerData.OrientFront.x,
|
||||
listenerData.OrientFront.y,
|
||||
-listenerData.OrientFront.z
|
||||
);
|
||||
}
|
||||
set
|
||||
{
|
||||
listenerData.OrientFront.x = value.X;
|
||||
listenerData.OrientFront.y = value.Y;
|
||||
listenerData.OrientFront.z = -value.Z;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector3 Position
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Vector3(
|
||||
listenerData.Position.x,
|
||||
listenerData.Position.y,
|
||||
-listenerData.Position.z
|
||||
);
|
||||
}
|
||||
set
|
||||
{
|
||||
listenerData.Position.x = value.X;
|
||||
listenerData.Position.y = value.Y;
|
||||
listenerData.Position.z = -value.Z;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Vector3 Up
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Vector3(
|
||||
listenerData.OrientTop.x,
|
||||
listenerData.OrientTop.y,
|
||||
-listenerData.OrientTop.z
|
||||
);
|
||||
}
|
||||
set
|
||||
{
|
||||
listenerData.OrientTop.x = value.X;
|
||||
listenerData.OrientTop.y = value.Y;
|
||||
listenerData.OrientTop.z = -value.Z;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector3 Velocity
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Vector3(
|
||||
listenerData.Velocity.x,
|
||||
listenerData.Velocity.y,
|
||||
-listenerData.Velocity.z
|
||||
);
|
||||
}
|
||||
set
|
||||
{
|
||||
listenerData.Velocity.x = value.X;
|
||||
listenerData.Velocity.y = value.Y;
|
||||
listenerData.Velocity.z = -value.Z;
|
||||
}
|
||||
}
|
||||
|
||||
public AudioListener(AudioDevice device) : base(device)
|
||||
{
|
||||
listenerData = new FAudio.F3DAUDIO_LISTENER();
|
||||
Forward = Vector3.UnitZ;
|
||||
Position = Vector3.Zero;
|
||||
Up = Vector3.UnitY;
|
||||
Velocity = Vector3.Zero;
|
||||
|
||||
/* Unexposed variables, defaults based on XNA behavior */
|
||||
listenerData.pCone = IntPtr.Zero;
|
||||
}
|
||||
}
|
52
Nerfed.Runtime/Audio/AudioResource.cs
Normal file
52
Nerfed.Runtime/Audio/AudioResource.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nerfed.Runtime.Audio;
|
||||
|
||||
public abstract class AudioResource : IDisposable
|
||||
{
|
||||
public AudioDevice Device { get; }
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
private GCHandle SelfReference;
|
||||
|
||||
protected AudioResource(AudioDevice device)
|
||||
{
|
||||
Device = device;
|
||||
|
||||
SelfReference = GCHandle.Alloc(this, GCHandleType.Weak);
|
||||
Device.AddResourceReference(SelfReference);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Device.RemoveResourceReference(SelfReference);
|
||||
SelfReference.Free();
|
||||
}
|
||||
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
~AudioResource()
|
||||
{
|
||||
#if DEBUG
|
||||
// If you see this log message, you leaked an audio resource without disposing it!
|
||||
// We can't clean it up for you because this can cause catastrophic issues.
|
||||
// You should really fix this when it happens.
|
||||
Log.Warning($"A resource of type {GetType().Name} was not Disposed.");
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
9
Nerfed.Runtime/Audio/FilterType.cs
Normal file
9
Nerfed.Runtime/Audio/FilterType.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Nerfed.Runtime.Audio;
|
||||
|
||||
public enum FilterType
|
||||
{
|
||||
None,
|
||||
LowPass,
|
||||
BandPass,
|
||||
HighPass
|
||||
}
|
35
Nerfed.Runtime/Audio/Format.cs
Normal file
35
Nerfed.Runtime/Audio/Format.cs
Normal file
@ -0,0 +1,35 @@
|
||||
namespace Nerfed.Runtime.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()
|
||||
{
|
||||
ushort blockAlign = (ushort) ((BitsPerSample / 8) * Channels);
|
||||
|
||||
return new FAudio.FAudioWaveFormatEx
|
||||
{
|
||||
wFormatTag = (ushort) Tag,
|
||||
nChannels = Channels,
|
||||
nSamplesPerSec = SampleRate,
|
||||
wBitsPerSample = BitsPerSample,
|
||||
nBlockAlign = blockAlign,
|
||||
nAvgBytesPerSec = blockAlign * SampleRate
|
||||
};
|
||||
}
|
||||
}
|
6
Nerfed.Runtime/Audio/IPoolable.cs
Normal file
6
Nerfed.Runtime/Audio/IPoolable.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Nerfed.Runtime.Audio;
|
||||
|
||||
public interface IPoolable<T>
|
||||
{
|
||||
static abstract T Create(AudioDevice device, Format format);
|
||||
}
|
27
Nerfed.Runtime/Audio/PersistentVoice.cs
Normal file
27
Nerfed.Runtime/Audio/PersistentVoice.cs
Normal file
@ -0,0 +1,27 @@
|
||||
namespace Nerfed.Runtime.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));
|
||||
}
|
||||
}
|
82
Nerfed.Runtime/Audio/ReverbEffect.cs
Normal file
82
Nerfed.Runtime/Audio/ReverbEffect.cs
Normal file
@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nerfed.Runtime.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);
|
||||
|
||||
FAudio.FAudioEffectChain chain = new FAudio.FAudioEffectChain();
|
||||
FAudio.FAudioEffectDescriptor descriptor = new FAudio.FAudioEffectDescriptor
|
||||
{
|
||||
InitialState = 1,
|
||||
OutputChannels = 1,
|
||||
pEffect = reverb
|
||||
};
|
||||
|
||||
chain.EffectCount = 1;
|
||||
chain.pEffectDescriptors = (nint) (&descriptor);
|
||||
|
||||
FAudio.FAudioVoice_SetEffectChain(
|
||||
Handle,
|
||||
ref chain
|
||||
);
|
||||
|
||||
FAudio.FAPOBase_Release(reverb);
|
||||
|
||||
SetParams(DefaultParams);
|
||||
}
|
||||
|
||||
public void SetParams(in FAudio.FAudioFXReverbParameters reverbParams)
|
||||
{
|
||||
Params = reverbParams;
|
||||
|
||||
fixed (FAudio.FAudioFXReverbParameters* reverbParamsPtr = &reverbParams)
|
||||
{
|
||||
FAudio.FAudioVoice_SetEffectParameters(
|
||||
Handle,
|
||||
0,
|
||||
(nint) reverbParamsPtr,
|
||||
(uint) Marshal.SizeOf<FAudio.FAudioFXReverbParameters>(),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
63
Nerfed.Runtime/Audio/SoundSequence.cs
Normal file
63
Nerfed.Runtime/Audio/SoundSequence.cs
Normal file
@ -0,0 +1,63 @@
|
||||
namespace Nerfed.Runtime.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)
|
||||
{
|
||||
int buffersNeeded = NeedSoundThreshold - (int) BuffersQueued;
|
||||
|
||||
for (int i = 0; i < buffersNeeded; i += 1)
|
||||
{
|
||||
if (OnSoundNeeded != null)
|
||||
{
|
||||
OnSoundNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void EnqueueSound(AudioBuffer buffer)
|
||||
{
|
||||
#if DEBUG
|
||||
if (!(buffer.Format == Format))
|
||||
{
|
||||
Log.Warning("Sound sequence audio format mismatch!");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
lock (StateLock)
|
||||
{
|
||||
Submit(buffer.ToFAudioBuffer());
|
||||
}
|
||||
}
|
||||
}
|
8
Nerfed.Runtime/Audio/SoundState.cs
Normal file
8
Nerfed.Runtime/Audio/SoundState.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Nerfed.Runtime.Audio;
|
||||
|
||||
public enum SoundState
|
||||
{
|
||||
Playing,
|
||||
Paused,
|
||||
Stopped
|
||||
}
|
217
Nerfed.Runtime/Audio/SourceVoice.cs
Normal file
217
Nerfed.Runtime/Audio/SourceVoice.cs
Normal file
@ -0,0 +1,217 @@
|
||||
using System;
|
||||
|
||||
namespace Nerfed.Runtime.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 FAudio.FAudioVoiceState state,
|
||||
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
|
||||
);
|
||||
|
||||
return state.BuffersQueued;
|
||||
}
|
||||
}
|
||||
|
||||
private SoundState state = SoundState.Stopped;
|
||||
public SoundState State
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BuffersQueued == 0)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
internal set
|
||||
{
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected object StateLock = new object();
|
||||
|
||||
public SourceVoice(
|
||||
AudioDevice device,
|
||||
Format format
|
||||
) : base(device, format.Channels, device.DeviceDetails.OutputFormat.Format.nChannels)
|
||||
{
|
||||
this.format = format;
|
||||
FAudio.FAudioWaveFormatEx fAudioFormat = format.ToFAudioFormat();
|
||||
|
||||
FAudio.FAudio_CreateSourceVoice(
|
||||
device.Handle,
|
||||
out handle,
|
||||
ref fAudioFormat,
|
||||
FAudio.FAUDIO_VOICE_USEFILTER,
|
||||
FAudio.FAUDIO_DEFAULT_FREQ_RATIO,
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero, // default sends to mastering voice!
|
||||
IntPtr.Zero
|
||||
);
|
||||
|
||||
SetOutputVoice(device.MasteringVoice);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
FAudio.F3DAUDIO_DSP_SETTINGS dspSettings = new FAudio.F3DAUDIO_DSP_SETTINGS
|
||||
{
|
||||
DopplerFactor = DopplerFactor,
|
||||
SrcChannelCount = SourceChannelCount,
|
||||
DstChannelCount = DestinationChannelCount,
|
||||
pMatrixCoefficients = (nint) pMatrixCoefficients
|
||||
};
|
||||
|
||||
FAudio.F3DAudioCalculate(
|
||||
Device.Handle3D,
|
||||
ref listener.listenerData,
|
||||
ref emitter.emitterData,
|
||||
FAudio.F3DAUDIO_CALCULATE_MATRIX | FAudio.F3DAUDIO_CALCULATE_DOPPLER,
|
||||
ref dspSettings
|
||||
);
|
||||
|
||||
UpdatePitch();
|
||||
|
||||
FAudio.FAudioVoice_SetOutputMatrix(
|
||||
Handle,
|
||||
OutputVoice.Handle,
|
||||
SourceChannelCount,
|
||||
DestinationChannelCount,
|
||||
(nint) pMatrixCoefficients,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/// <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();
|
||||
}
|
||||
}
|
38
Nerfed.Runtime/Audio/SourceVoicePool.cs
Normal file
38
Nerfed.Runtime/Audio/SourceVoicePool.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Nerfed.Runtime.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>());
|
||||
}
|
||||
|
||||
Queue<SourceVoice> list = VoiceLists[(typeof(T), format)];
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
list.Enqueue(T.Create(Device, format));
|
||||
}
|
||||
|
||||
return (T) list.Dequeue();
|
||||
}
|
||||
|
||||
public void Return(SourceVoice voice)
|
||||
{
|
||||
Queue<SourceVoice> list = VoiceLists[(voice.GetType(), voice.Format)];
|
||||
list.Enqueue(voice);
|
||||
}
|
||||
}
|
168
Nerfed.Runtime/Audio/StreamingVoice.cs
Normal file
168
Nerfed.Runtime/Audio/StreamingVoice.cs
Normal file
@ -0,0 +1,168 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nerfed.Runtime.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()
|
||||
{
|
||||
IntPtr buffer = buffers[nextBufferIndex];
|
||||
nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT;
|
||||
|
||||
AudioData.Decode(
|
||||
(void*) buffer,
|
||||
(int) BufferSize,
|
||||
out int filledLengthInBytes,
|
||||
out bool reachedEnd
|
||||
);
|
||||
|
||||
if (filledLengthInBytes > 0)
|
||||
{
|
||||
FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer
|
||||
{
|
||||
AudioBytes = (uint) filledLengthInBytes,
|
||||
pAudioData = buffer,
|
||||
PlayLength = (
|
||||
(uint) (filledLengthInBytes /
|
||||
Format.Channels /
|
||||
(uint) (Format.BitsPerSample / 8))
|
||||
)
|
||||
};
|
||||
|
||||
Submit(buf);
|
||||
}
|
||||
|
||||
if (reachedEnd)
|
||||
{
|
||||
/* We have reached the end of the data, what do we do? */
|
||||
if (Loop)
|
||||
{
|
||||
AudioData.Seek(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void InitializeBuffers()
|
||||
{
|
||||
BufferSize = AudioData.DecodeBufferSize;
|
||||
|
||||
for (int i = 0; i < BUFFER_COUNT; i += 1)
|
||||
{
|
||||
if (buffers[i] != IntPtr.Zero)
|
||||
{
|
||||
NativeMemory.Free((void*) buffers[i]);
|
||||
}
|
||||
|
||||
buffers[i] = (IntPtr) NativeMemory.Alloc(BufferSize);
|
||||
}
|
||||
}
|
||||
|
||||
protected override unsafe void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
lock (StateLock)
|
||||
{
|
||||
Stop();
|
||||
|
||||
for (int i = 0; i < BUFFER_COUNT; i += 1)
|
||||
{
|
||||
if (buffers[i] != IntPtr.Zero)
|
||||
{
|
||||
NativeMemory.Free((void*) buffers[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
55
Nerfed.Runtime/Audio/SubmixVoice.cs
Normal file
55
Nerfed.Runtime/Audio/SubmixVoice.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using System;
|
||||
|
||||
namespace Nerfed.Runtime.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);
|
||||
}
|
||||
}
|
28
Nerfed.Runtime/Audio/TransientVoice.cs
Normal file
28
Nerfed.Runtime/Audio/TransientVoice.cs
Normal file
@ -0,0 +1,28 @@
|
||||
namespace Nerfed.Runtime.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
Nerfed.Runtime/Audio/UpdatingSourceVoice.cs
Normal file
10
Nerfed.Runtime/Audio/UpdatingSourceVoice.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Nerfed.Runtime.Audio;
|
||||
|
||||
public abstract class UpdatingSourceVoice : SourceVoice
|
||||
{
|
||||
protected UpdatingSourceVoice(AudioDevice device, Format format) : base(device, format)
|
||||
{
|
||||
}
|
||||
|
||||
public abstract void Update();
|
||||
}
|
433
Nerfed.Runtime/Audio/Voice.cs
Normal file
433
Nerfed.Runtime/Audio/Voice.cs
Normal file
@ -0,0 +1,433 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using EasingFunction = System.Func<float, float>;
|
||||
|
||||
namespace Nerfed.Runtime.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 = MathF.Max(0f, 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.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.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)
|
||||
{
|
||||
Log.Warning("Tried to set reverb value before applying a reverb effect");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public Voice(AudioDevice device, uint sourceChannelCount, uint destinationChannelCount) : base(device)
|
||||
{
|
||||
SourceChannelCount = sourceChannelCount;
|
||||
DestinationChannelCount = destinationChannelCount;
|
||||
nuint memsize = 4 * sourceChannelCount * destinationChannelCount;
|
||||
pMatrixCoefficients = (byte*) NativeMemory.AllocZeroed(memsize);
|
||||
SetPanMatrixCoefficients();
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
FAudio.FAudioVoiceSends 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)
|
||||
{
|
||||
FAudio.FAudioSendDescriptor* sendDesc = stackalloc FAudio.FAudioSendDescriptor[2];
|
||||
sendDesc[0].Flags = 0;
|
||||
sendDesc[0].pOutputVoice = OutputVoice.Handle;
|
||||
sendDesc[1].Flags = 0;
|
||||
sendDesc[1].pOutputVoice = reverbEffect.Handle;
|
||||
|
||||
FAudio.FAudioVoiceSends sends = new FAudio.FAudioVoiceSends();
|
||||
sends.SendCount = 2;
|
||||
sends.pSends = (nint) sendDesc;
|
||||
|
||||
FAudio.FAudioVoice_SetOutputVoices(
|
||||
Handle,
|
||||
ref sends
|
||||
);
|
||||
|
||||
ReverbEffect = reverbEffect;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
265
Nerfed.Runtime/Engine.cs
Normal file
265
Nerfed.Runtime/Engine.cs
Normal file
@ -0,0 +1,265 @@
|
||||
using Nerfed.Runtime.Audio;
|
||||
using Nerfed.Runtime.Graphics;
|
||||
using SDL2;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Nerfed.Runtime;
|
||||
|
||||
public static class Engine
|
||||
{
|
||||
public static event Action OnInitialize;
|
||||
public static event Action OnUpdate;
|
||||
public static event Action OnRender;
|
||||
public static event Action OnQuit;
|
||||
|
||||
public static TimeSpan MaxDeltaTime { get; set; } = TimeSpan.FromMilliseconds(100);
|
||||
public static bool VSync { get; set; }
|
||||
|
||||
public static GraphicsDevice GraphicsDevice { get; private set; }
|
||||
public static AudioDevice AudioDevice { get; private set; }
|
||||
public static Window MainWindow { get; private set; }
|
||||
public static TimeSpan Timestep { get; private set; }
|
||||
|
||||
private static bool quit;
|
||||
private static Stopwatch gameTimer;
|
||||
private static long previousTicks = 0;
|
||||
private static TimeSpan accumulatedUpdateTime = TimeSpan.Zero;
|
||||
|
||||
private static TimeSpan accumulatedDrawTime = TimeSpan.Zero;
|
||||
|
||||
// must be a power of 2 so we can do a bitmask optimization when checking worst case
|
||||
private const int previousSleepTimeCount = 128;
|
||||
private const int sleepTimeMask = previousSleepTimeCount - 1;
|
||||
private static readonly TimeSpan[] previousSleepTimes = new TimeSpan[previousSleepTimeCount];
|
||||
private static int sleepTimeIndex;
|
||||
private static TimeSpan worstCaseSleepPrecision = TimeSpan.FromMilliseconds(1);
|
||||
private static bool framerateCapped;
|
||||
private static TimeSpan framerateCapTimeSpan = TimeSpan.Zero;
|
||||
|
||||
//TODO: These are temp
|
||||
private const int MaxFps = 300;
|
||||
private const int TargetTimestep = 60;
|
||||
private const int WindowWidth = 1280;
|
||||
private const int WindowHeight = 720;
|
||||
private const string WindowTitle = "Nerfed";
|
||||
//..
|
||||
|
||||
public static void Run(string[] args)
|
||||
{
|
||||
Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / TargetTimestep);
|
||||
gameTimer = Stopwatch.StartNew();
|
||||
SetFrameLimiter(new FrameLimiterSettings(FrameLimiterMode.Capped, MaxFps));
|
||||
|
||||
for (int i = 0; i < previousSleepTimes.Length; i += 1)
|
||||
{
|
||||
previousSleepTimes[i] = TimeSpan.FromMilliseconds(1);
|
||||
}
|
||||
|
||||
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_TIMER | SDL.SDL_INIT_GAMECONTROLLER) < 0)
|
||||
{
|
||||
throw new Exception("Failed to init SDL");
|
||||
}
|
||||
|
||||
GraphicsDevice = new GraphicsDevice(BackendFlags.All);
|
||||
GraphicsDevice.LoadDefaultPipelines();
|
||||
|
||||
MainWindow = new Window(GraphicsDevice, new WindowCreateInfo(WindowTitle, WindowWidth, WindowHeight, ScreenMode.Windowed));
|
||||
if (!GraphicsDevice.ClaimWindow(MainWindow, SwapchainComposition.SDR, VSync ? PresentMode.VSync : PresentMode.Mailbox))
|
||||
{
|
||||
throw new Exception("Failed to claim window");
|
||||
}
|
||||
|
||||
AudioDevice = new AudioDevice();
|
||||
|
||||
OnInitialize?.Invoke();
|
||||
Nerfed.Runtime.Generator.Hook.InvokeHooks();
|
||||
|
||||
while (!quit)
|
||||
{
|
||||
Tick();
|
||||
}
|
||||
|
||||
OnQuit?.Invoke();
|
||||
|
||||
GraphicsDevice.UnclaimWindow(MainWindow);
|
||||
MainWindow.Dispose();
|
||||
GraphicsDevice.Dispose();
|
||||
AudioDevice.Dispose();
|
||||
SDL.SDL_Quit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the frame limiter settings.
|
||||
/// </summary>
|
||||
public static void SetFrameLimiter(FrameLimiterSettings settings)
|
||||
{
|
||||
framerateCapped = settings.Mode == FrameLimiterMode.Capped;
|
||||
|
||||
if (framerateCapped)
|
||||
{
|
||||
framerateCapTimeSpan = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / settings.Cap);
|
||||
}
|
||||
else
|
||||
{
|
||||
framerateCapTimeSpan = TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Quit()
|
||||
{
|
||||
quit = true;
|
||||
}
|
||||
|
||||
private static void Tick()
|
||||
{
|
||||
AdvanceElapsedTime();
|
||||
|
||||
if (framerateCapped)
|
||||
{
|
||||
/* We want to wait until the framerate cap,
|
||||
* but we don't want to oversleep. Requesting repeated 1ms sleeps and
|
||||
* seeing how long we actually slept for lets us estimate the worst case
|
||||
* sleep precision so we don't oversleep the next frame.
|
||||
*/
|
||||
while (accumulatedDrawTime + worstCaseSleepPrecision < framerateCapTimeSpan)
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
TimeSpan timeAdvancedSinceSleeping = AdvanceElapsedTime();
|
||||
UpdateEstimatedSleepPrecision(timeAdvancedSinceSleeping);
|
||||
}
|
||||
|
||||
/* Now that we have slept into the sleep precision threshold, we need to wait
|
||||
* for just a little bit longer until the target elapsed time has been reached.
|
||||
* SpinWait(1) works by pausing the thread for very short intervals, so it is
|
||||
* an efficient and time-accurate way to wait out the rest of the time.
|
||||
*/
|
||||
while (accumulatedDrawTime < framerateCapTimeSpan)
|
||||
{
|
||||
Thread.SpinWait(1);
|
||||
AdvanceElapsedTime();
|
||||
}
|
||||
}
|
||||
|
||||
// Do not let any step take longer than our maximum.
|
||||
if (accumulatedUpdateTime > MaxDeltaTime)
|
||||
{
|
||||
accumulatedUpdateTime = MaxDeltaTime;
|
||||
}
|
||||
|
||||
if (!quit)
|
||||
{
|
||||
while (accumulatedUpdateTime >= Timestep)
|
||||
{
|
||||
Keyboard.Update();
|
||||
Mouse.Update();
|
||||
GamePad.Update();
|
||||
|
||||
ProcessSDLEvents();
|
||||
|
||||
// Tick game here...
|
||||
OnUpdate?.Invoke();
|
||||
|
||||
AudioDevice.WakeThread();
|
||||
accumulatedUpdateTime -= Timestep;
|
||||
}
|
||||
|
||||
double alpha = accumulatedUpdateTime / Timestep;
|
||||
|
||||
// Render here..
|
||||
OnRender?.Invoke();
|
||||
|
||||
accumulatedDrawTime -= framerateCapTimeSpan;
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeSpan AdvanceElapsedTime()
|
||||
{
|
||||
long currentTicks = gameTimer.Elapsed.Ticks;
|
||||
TimeSpan timeAdvanced = TimeSpan.FromTicks(currentTicks - previousTicks);
|
||||
accumulatedUpdateTime += timeAdvanced;
|
||||
accumulatedDrawTime += timeAdvanced;
|
||||
previousTicks = currentTicks;
|
||||
return timeAdvanced;
|
||||
}
|
||||
|
||||
private static void ProcessSDLEvents()
|
||||
{
|
||||
while (SDL.SDL_PollEvent(out SDL.SDL_Event ev) == 1)
|
||||
{
|
||||
switch (ev.type)
|
||||
{
|
||||
case SDL.SDL_EventType.SDL_QUIT:
|
||||
Quit();
|
||||
break;
|
||||
case SDL.SDL_EventType.SDL_TEXTINPUT:
|
||||
case SDL.SDL_EventType.SDL_KEYDOWN:
|
||||
case SDL.SDL_EventType.SDL_KEYUP:
|
||||
Keyboard.ProcessEvent(ref ev);
|
||||
break;
|
||||
case SDL.SDL_EventType.SDL_MOUSEBUTTONDOWN:
|
||||
case SDL.SDL_EventType.SDL_MOUSEBUTTONUP:
|
||||
case SDL.SDL_EventType.SDL_MOUSEWHEEL:
|
||||
case SDL.SDL_EventType.SDL_MOUSEMOTION:
|
||||
Mouse.ProcessEvent(ref ev);
|
||||
break;
|
||||
case SDL.SDL_EventType.SDL_CONTROLLERDEVICEADDED:
|
||||
case SDL.SDL_EventType.SDL_CONTROLLERDEVICEREMOVED:
|
||||
case SDL.SDL_EventType.SDL_CONTROLLERBUTTONDOWN:
|
||||
case SDL.SDL_EventType.SDL_CONTROLLERBUTTONUP:
|
||||
case SDL.SDL_EventType.SDL_CONTROLLERAXISMOTION:
|
||||
case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADDOWN:
|
||||
case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADUP:
|
||||
case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADMOTION:
|
||||
case SDL.SDL_EventType.SDL_CONTROLLERSENSORUPDATE:
|
||||
GamePad.ProcessEvent(ref ev);
|
||||
break;
|
||||
case SDL.SDL_EventType.SDL_WINDOWEVENT:
|
||||
Window.ProcessEvent(ref ev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* To calculate the sleep precision of the OS, we take the worst case
|
||||
* time spent sleeping over the results of previous requests to sleep 1ms.
|
||||
*/
|
||||
private static void UpdateEstimatedSleepPrecision(TimeSpan timeSpentSleeping)
|
||||
{
|
||||
/* It is unlikely that the scheduler will actually be more imprecise than
|
||||
* 4ms and we don't want to get wrecked by a single long sleep so we cap this
|
||||
* value at 4ms for sanity.
|
||||
*/
|
||||
TimeSpan upperTimeBound = TimeSpan.FromMilliseconds(4);
|
||||
|
||||
if (timeSpentSleeping > upperTimeBound)
|
||||
{
|
||||
timeSpentSleeping = upperTimeBound;
|
||||
}
|
||||
|
||||
/* We know the previous worst case - it's saved in worstCaseSleepPrecision.
|
||||
* We also know the current index. So the only way the worst case changes
|
||||
* is if we either 1) just got a new worst case, or 2) the worst case was
|
||||
* the oldest entry on the list.
|
||||
*/
|
||||
if (timeSpentSleeping >= worstCaseSleepPrecision)
|
||||
{
|
||||
worstCaseSleepPrecision = timeSpentSleeping;
|
||||
}
|
||||
else if (previousSleepTimes[sleepTimeIndex] == worstCaseSleepPrecision)
|
||||
{
|
||||
TimeSpan maxSleepTime = TimeSpan.MinValue;
|
||||
for (int i = 0; i < previousSleepTimes.Length; i++)
|
||||
{
|
||||
if (previousSleepTimes[i] > maxSleepTime)
|
||||
{
|
||||
maxSleepTime = previousSleepTimes[i];
|
||||
}
|
||||
}
|
||||
|
||||
worstCaseSleepPrecision = maxSleepTime;
|
||||
}
|
||||
|
||||
previousSleepTimes[sleepTimeIndex] = timeSpentSleeping;
|
||||
sleepTimeIndex = (sleepTimeIndex + 1) & sleepTimeMask;
|
||||
}
|
||||
}
|
35
Nerfed.Runtime/FrameLimiterSettings.cs
Normal file
35
Nerfed.Runtime/FrameLimiterSettings.cs
Normal file
@ -0,0 +1,35 @@
|
||||
namespace Nerfed.Runtime;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
1948
Nerfed.Runtime/Graphics/Color.cs
Normal file
1948
Nerfed.Runtime/Graphics/Color.cs
Normal file
File diff suppressed because it is too large
Load Diff
1042
Nerfed.Runtime/Graphics/CommandBuffer.cs
Normal file
1042
Nerfed.Runtime/Graphics/CommandBuffer.cs
Normal file
File diff suppressed because it is too large
Load Diff
33
Nerfed.Runtime/Graphics/CommandBufferPool.cs
Normal file
33
Nerfed.Runtime/Graphics/CommandBufferPool.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Nerfed.Runtime.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 CommandBuffer commandBuffer))
|
||||
{
|
||||
return commandBuffer;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new CommandBuffer(GraphicsDevice);
|
||||
}
|
||||
}
|
||||
|
||||
public void Return(CommandBuffer commandBuffer)
|
||||
{
|
||||
commandBuffer.Handle = IntPtr.Zero;
|
||||
CommandBuffers.Enqueue(commandBuffer);
|
||||
}
|
||||
}
|
171
Nerfed.Runtime/Graphics/ComputePass.cs
Normal file
171
Nerfed.Runtime/Graphics/ComputePass.cs
Normal file
@ -0,0 +1,171 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using RefreshCS;
|
||||
|
||||
namespace Nerfed.Runtime.Graphics;
|
||||
|
||||
public class ComputePass
|
||||
{
|
||||
public nint Handle { get; private set; }
|
||||
|
||||
internal void SetHandle(nint handle)
|
||||
{
|
||||
Handle = handle;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
internal bool active;
|
||||
|
||||
ComputePipeline currentComputePipeline;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Binds a compute pipeline so that compute work may be dispatched.
|
||||
/// </summary>
|
||||
/// <param name="computePipeline">The compute pipeline to bind.</param>
|
||||
public void BindComputePipeline(
|
||||
ComputePipeline computePipeline
|
||||
) {
|
||||
#if DEBUG
|
||||
AssertComputePassActive();
|
||||
|
||||
// TODO: validate formats?
|
||||
#endif
|
||||
|
||||
Refresh.Refresh_BindComputePipeline(
|
||||
Handle,
|
||||
computePipeline.Handle
|
||||
);
|
||||
|
||||
#if DEBUG
|
||||
currentComputePipeline = computePipeline;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds a texture to be used in the compute shader.
|
||||
/// This texture must have been created with the ComputeShaderRead usage flag.
|
||||
/// </summary>
|
||||
public unsafe void BindStorageTexture(
|
||||
in TextureSlice textureSlice,
|
||||
uint slot = 0
|
||||
) {
|
||||
#if DEBUG
|
||||
AssertComputePassActive();
|
||||
AssertComputePipelineBound();
|
||||
AssertTextureNonNull(textureSlice.Texture);
|
||||
AssertTextureHasComputeStorageReadFlag(textureSlice.Texture);
|
||||
#endif
|
||||
|
||||
Refresh.TextureSlice refreshTextureSlice = textureSlice.ToRefresh();
|
||||
|
||||
Refresh.Refresh_BindComputeStorageTextures(
|
||||
Handle,
|
||||
slot,
|
||||
&refreshTextureSlice,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds a buffer to be used in the compute shader.
|
||||
/// This buffer must have been created with the ComputeShaderRead usage flag.
|
||||
/// </summary>
|
||||
public unsafe void BindStorageBuffer(
|
||||
Buffer buffer,
|
||||
uint slot = 0
|
||||
) {
|
||||
#if DEBUG
|
||||
AssertComputePassActive();
|
||||
AssertComputePipelineBound();
|
||||
AssertBufferNonNull(buffer);
|
||||
AssertBufferHasComputeStorageReadFlag(buffer);
|
||||
#endif
|
||||
|
||||
IntPtr bufferHandle = buffer.Handle;
|
||||
|
||||
Refresh.Refresh_BindComputeStorageBuffers(
|
||||
Handle,
|
||||
slot,
|
||||
&bufferHandle,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches compute work.
|
||||
/// </summary>
|
||||
public void Dispatch(
|
||||
uint groupCountX,
|
||||
uint groupCountY,
|
||||
uint groupCountZ
|
||||
) {
|
||||
#if DEBUG
|
||||
AssertComputePassActive();
|
||||
AssertComputePipelineBound();
|
||||
|
||||
if (groupCountX < 1 || groupCountY < 1 || groupCountZ < 1)
|
||||
{
|
||||
throw new System.ArgumentException("All dimensions for the compute work groups must be >= 1!");
|
||||
}
|
||||
#endif
|
||||
|
||||
Refresh.Refresh_DispatchCompute(
|
||||
Handle,
|
||||
groupCountX,
|
||||
groupCountY,
|
||||
groupCountZ
|
||||
);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private void AssertComputePassActive(string message = "Render pass is not active!")
|
||||
{
|
||||
if (!active)
|
||||
{
|
||||
throw new System.InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertComputePipelineBound(string message = "No compute pipeline is bound!")
|
||||
{
|
||||
if (currentComputePipeline == null)
|
||||
{
|
||||
throw new System.InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertTextureNonNull(in TextureSlice textureSlice)
|
||||
{
|
||||
if (textureSlice.Texture == null || textureSlice.Texture.Handle == nint.Zero)
|
||||
{
|
||||
throw new System.NullReferenceException("Texture must not be null!");
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertTextureHasComputeStorageReadFlag(Texture texture)
|
||||
{
|
||||
if ((texture.UsageFlags & TextureUsageFlags.ComputeStorageRead) == 0)
|
||||
{
|
||||
throw new System.ArgumentException("The bound Texture's UsageFlags must include TextureUsageFlags.ComputeStorageRead!");
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertBufferNonNull(Buffer buffer)
|
||||
{
|
||||
if (buffer == null || buffer.Handle == nint.Zero)
|
||||
{
|
||||
throw new System.NullReferenceException("Buffer must not be null!");
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertBufferHasComputeStorageReadFlag(Buffer buffer)
|
||||
{
|
||||
if ((buffer.UsageFlags & BufferUsageFlags.ComputeStorageRead) == 0)
|
||||
{
|
||||
throw new System.ArgumentException("The bound Buffer's UsageFlags must include BufferUsageFlag.ComputeStorageRead!");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
25
Nerfed.Runtime/Graphics/ComputePassPool.cs
Normal file
25
Nerfed.Runtime/Graphics/ComputePassPool.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Nerfed.Runtime.Graphics;
|
||||
|
||||
internal class ComputePassPool
|
||||
{
|
||||
private ConcurrentQueue<ComputePass> ComputePasses = new ConcurrentQueue<ComputePass>();
|
||||
|
||||
public ComputePass Obtain()
|
||||
{
|
||||
if (ComputePasses.TryDequeue(out ComputePass computePass))
|
||||
{
|
||||
return computePass;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ComputePass();
|
||||
}
|
||||
}
|
||||
|
||||
public void Return(ComputePass computePass)
|
||||
{
|
||||
ComputePasses.Enqueue(computePass);
|
||||
}
|
||||
}
|
241
Nerfed.Runtime/Graphics/CopyPass.cs
Normal file
241
Nerfed.Runtime/Graphics/CopyPass.cs
Normal file
@ -0,0 +1,241 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using RefreshCS;
|
||||
|
||||
namespace Nerfed.Runtime.Graphics;
|
||||
|
||||
public class CopyPass
|
||||
{
|
||||
public nint Handle { get; private set; }
|
||||
|
||||
internal void SetHandle(nint handle)
|
||||
{
|
||||
Handle = handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads data from a TransferBuffer to a TextureSlice.
|
||||
/// This copy occurs on the GPU timeline.
|
||||
///
|
||||
/// Overwriting the contents of the TransferBuffer before the command buffer
|
||||
/// has finished execution will cause undefined behavior.
|
||||
///
|
||||
/// You MAY assume that the copy has finished for subsequent commands.
|
||||
/// </summary>
|
||||
/// <param name="cycle">If true, cycles the texture if the given slice is bound.</param>
|
||||
public void UploadToTexture(
|
||||
in TextureTransferInfo source,
|
||||
in TextureRegion destination,
|
||||
bool cycle
|
||||
) {
|
||||
#if DEBUG
|
||||
AssertTransferBufferNotMapped(source.TransferBuffer);
|
||||
#endif
|
||||
|
||||
Refresh.Refresh_UploadToTexture(
|
||||
Handle,
|
||||
source.ToRefresh(),
|
||||
destination.ToRefresh(),
|
||||
Conversions.BoolToInt(cycle)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads the contents of an entire buffer to a 2D texture with no mips.
|
||||
/// </summary>
|
||||
public void UploadToTexture(
|
||||
TransferBuffer source,
|
||||
Texture destination,
|
||||
bool cycle
|
||||
) {
|
||||
UploadToTexture(
|
||||
new TextureTransferInfo(source),
|
||||
new TextureRegion(destination),
|
||||
cycle
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads data from a TransferBuffer to a Buffer.
|
||||
/// This copy occurs on the GPU timeline.
|
||||
///
|
||||
/// Overwriting the contents of the TransferBuffer before the command buffer
|
||||
/// has finished execution will cause undefined behavior.
|
||||
///
|
||||
/// You MAY assume that the copy has finished for subsequent commands.
|
||||
/// </summary>
|
||||
/// <param name="cycle">If true, cycles the buffer if it is bound.</param>
|
||||
public void UploadToBuffer(
|
||||
in TransferBufferLocation source,
|
||||
in BufferRegion destination,
|
||||
bool cycle
|
||||
) {
|
||||
#if DEBUG
|
||||
AssertBufferBoundsCheck(source.TransferBuffer.Size, source.Offset, destination.Size);
|
||||
AssertBufferBoundsCheck(destination.Buffer.Size, destination.Offset, destination.Size);
|
||||
AssertTransferBufferNotMapped(source.TransferBuffer);
|
||||
#endif
|
||||
|
||||
Refresh.Refresh_UploadToBuffer(
|
||||
Handle,
|
||||
source.ToRefresh(),
|
||||
destination.ToRefresh(),
|
||||
Conversions.BoolToInt(cycle)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the entire contents of a TransferBuffer to a Buffer.
|
||||
/// </summary>
|
||||
public void UploadToBuffer(
|
||||
TransferBuffer source,
|
||||
Buffer destination,
|
||||
bool cycle
|
||||
) {
|
||||
UploadToBuffer(
|
||||
new TransferBufferLocation(source),
|
||||
new BufferRegion(destination, 0, destination.Size),
|
||||
cycle
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies data element-wise into from a TransferBuffer to a Buffer.
|
||||
/// </summary>
|
||||
public void UploadToBuffer<T>(
|
||||
TransferBuffer source,
|
||||
Buffer destination,
|
||||
uint sourceStartElement,
|
||||
uint destinationStartElement,
|
||||
uint numElements,
|
||||
bool cycle
|
||||
) where T : unmanaged
|
||||
{
|
||||
int elementSize = Marshal.SizeOf<T>();
|
||||
uint dataLengthInBytes = (uint) (elementSize * numElements);
|
||||
uint srcOffsetInBytes = (uint) (elementSize * sourceStartElement);
|
||||
uint dstOffsetInBytes = (uint) (elementSize * destinationStartElement);
|
||||
|
||||
UploadToBuffer(
|
||||
new TransferBufferLocation(source, srcOffsetInBytes),
|
||||
new BufferRegion(destination, dstOffsetInBytes, dataLengthInBytes),
|
||||
cycle
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the contents of a TextureLocation to another TextureLocation.
|
||||
/// This copy occurs on the GPU timeline.
|
||||
///
|
||||
/// You MAY assume that the copy has finished in subsequent commands.
|
||||
/// </summary>
|
||||
public void CopyTextureToTexture(
|
||||
in TextureLocation source,
|
||||
in TextureLocation destination,
|
||||
uint w,
|
||||
uint h,
|
||||
uint d,
|
||||
bool cycle
|
||||
) {
|
||||
#if DEBUG
|
||||
AssertTextureBoundsCheck(source, w, h, d);
|
||||
AssertTextureBoundsCheck(destination, w, h, d);
|
||||
#endif
|
||||
|
||||
Refresh.Refresh_CopyTextureToTexture(
|
||||
Handle,
|
||||
source.ToRefresh(),
|
||||
destination.ToRefresh(),
|
||||
w,
|
||||
h,
|
||||
d,
|
||||
Conversions.BoolToInt(cycle)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies data from a Buffer to another Buffer.
|
||||
/// This copy occurs on the GPU timeline.
|
||||
///
|
||||
/// You MAY assume that the copy has finished in subsequent commands.
|
||||
/// </summary>
|
||||
public void CopyBufferToBuffer(
|
||||
in BufferLocation source,
|
||||
in BufferLocation destination,
|
||||
uint size,
|
||||
bool cycle
|
||||
) {
|
||||
#if DEBUG
|
||||
AssertBufferBoundsCheck(source.Buffer.Size, source.Offset, size);
|
||||
AssertBufferBoundsCheck(destination.Buffer.Size, destination.Offset, size);
|
||||
#endif
|
||||
|
||||
Refresh.Refresh_CopyBufferToBuffer(
|
||||
Handle,
|
||||
source.ToRefresh(),
|
||||
destination.ToRefresh(),
|
||||
size,
|
||||
Conversions.BoolToInt(cycle)
|
||||
);
|
||||
}
|
||||
|
||||
public void DownloadFromBuffer(
|
||||
in BufferRegion source,
|
||||
in TransferBufferLocation destination
|
||||
) {
|
||||
#if DEBUG
|
||||
AssertBufferBoundsCheck(source.Buffer.Size, source.Offset, source.Size);
|
||||
AssertBufferBoundsCheck(destination.TransferBuffer.Size, destination.Offset, source.Size);
|
||||
AssertTransferBufferNotMapped(destination.TransferBuffer);
|
||||
#endif
|
||||
|
||||
Refresh.Refresh_DownloadFromBuffer(
|
||||
Handle,
|
||||
source.ToRefresh(),
|
||||
destination.ToRefresh()
|
||||
);
|
||||
}
|
||||
|
||||
public void DownloadFromTexture(
|
||||
in TextureRegion source,
|
||||
in TextureTransferInfo destination
|
||||
) {
|
||||
#if DEBUG
|
||||
AssertTransferBufferNotMapped(destination.TransferBuffer);
|
||||
#endif
|
||||
|
||||
Refresh.Refresh_DownloadFromTexture(
|
||||
Handle,
|
||||
source.ToRefresh(),
|
||||
destination.ToRefresh()
|
||||
);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private void AssertBufferBoundsCheck(uint bufferLengthInBytes, uint offsetInBytes, uint copyLengthInBytes)
|
||||
{
|
||||
if (copyLengthInBytes > bufferLengthInBytes + offsetInBytes)
|
||||
{
|
||||
throw new System.InvalidOperationException($"SetBufferData overflow! buffer length {bufferLengthInBytes}, offset {offsetInBytes}, copy length {copyLengthInBytes}");
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertTextureBoundsCheck(in TextureLocation textureLocation, uint w, uint h, uint d)
|
||||
{
|
||||
if (
|
||||
textureLocation.X + w > textureLocation.TextureSlice.Texture.Width ||
|
||||
textureLocation.Y + h > textureLocation.TextureSlice.Texture.Height ||
|
||||
textureLocation.Z + d > textureLocation.TextureSlice.Texture.Depth
|
||||
) {
|
||||
throw new System.InvalidOperationException($"Texture data is out of bounds!");
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertTransferBufferNotMapped(TransferBuffer transferBuffer)
|
||||
{
|
||||
if (transferBuffer.Mapped)
|
||||
{
|
||||
throw new System.InvalidOperationException("Transfer buffer must not be mapped!");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
25
Nerfed.Runtime/Graphics/CopyPassPool.cs
Normal file
25
Nerfed.Runtime/Graphics/CopyPassPool.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Nerfed.Runtime.Graphics;
|
||||
|
||||
internal class CopyPassPool
|
||||
{
|
||||
private ConcurrentQueue<CopyPass> CopyPasses = new ConcurrentQueue<CopyPass>();
|
||||
|
||||
public CopyPass Obtain()
|
||||
{
|
||||
if (CopyPasses.TryDequeue(out CopyPass copyPass))
|
||||
{
|
||||
return copyPass;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new CopyPass();
|
||||
}
|
||||
}
|
||||
|
||||
public void Return(CopyPass copyPass)
|
||||
{
|
||||
CopyPasses.Enqueue(copyPass);
|
||||
}
|
||||
}
|
31
Nerfed.Runtime/Graphics/FencePool.cs
Normal file
31
Nerfed.Runtime/Graphics/FencePool.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Nerfed.Runtime.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 Fence fence))
|
||||
{
|
||||
return fence;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Fence(GraphicsDevice);
|
||||
}
|
||||
}
|
||||
|
||||
public void Return(Fence fence)
|
||||
{
|
||||
Fences.Enqueue(fence);
|
||||
}
|
||||
}
|
16
Nerfed.Runtime/Graphics/Font/Enums.cs
Normal file
16
Nerfed.Runtime/Graphics/Font/Enums.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace Nerfed.Runtime.Graphics;
|
||||
|
||||
public enum HorizontalAlignment
|
||||
{
|
||||
Left,
|
||||
Center,
|
||||
Right
|
||||
}
|
||||
|
||||
public enum VerticalAlignment
|
||||
{
|
||||
Baseline,
|
||||
Top,
|
||||
Middle,
|
||||
Bottom
|
||||
}
|
123
Nerfed.Runtime/Graphics/Font/Font.cs
Normal file
123
Nerfed.Runtime/Graphics/Font/Font.cs
Normal file
@ -0,0 +1,123 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using WellspringCS;
|
||||
|
||||
namespace Nerfed.Runtime.Graphics;
|
||||
|
||||
public unsafe class Font : GraphicsResource
|
||||
{
|
||||
public Texture Texture { get; }
|
||||
public float PixelsPerEm { get; }
|
||||
public float DistanceRange { get; }
|
||||
|
||||
internal IntPtr Handle { get; }
|
||||
|
||||
private byte* StringBytes;
|
||||
private int StringBytesLength;
|
||||
|
||||
/// <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 static Font Load(GraphicsDevice graphicsDevice, CommandBuffer commandBuffer, string fontPath)
|
||||
{
|
||||
FileStream fontFileStream = new FileStream(fontPath, FileMode.Open, FileAccess.Read);
|
||||
void* fontFileByteBuffer = NativeMemory.Alloc((nuint)fontFileStream.Length);
|
||||
Span<byte> fontFileByteSpan = new Span<byte>(fontFileByteBuffer, (int)fontFileStream.Length);
|
||||
fontFileStream.ReadExactly(fontFileByteSpan);
|
||||
fontFileStream.Close();
|
||||
|
||||
FileStream atlasFileStream = new FileStream(Path.ChangeExtension(fontPath, ".json"), FileMode.Open, FileAccess.Read);
|
||||
void* atlasFileByteBuffer = NativeMemory.Alloc((nuint)atlasFileStream.Length);
|
||||
Span<byte> atlasFileByteSpan = new Span<byte>(atlasFileByteBuffer, (int)atlasFileStream.Length);
|
||||
atlasFileStream.ReadExactly(atlasFileByteSpan);
|
||||
atlasFileStream.Close();
|
||||
|
||||
IntPtr handle = Wellspring.Wellspring_CreateFont(
|
||||
(IntPtr)fontFileByteBuffer,
|
||||
(uint)fontFileByteSpan.Length,
|
||||
(IntPtr)atlasFileByteBuffer,
|
||||
(uint)atlasFileByteSpan.Length,
|
||||
out float pixelsPerEm,
|
||||
out float distanceRange
|
||||
);
|
||||
|
||||
string imagePath = Path.ChangeExtension(fontPath, ".png");
|
||||
ImageUtils.ImageInfoFromFile(imagePath, out uint width, out uint height, out uint sizeInBytes);
|
||||
|
||||
ResourceUploader uploader = new ResourceUploader(graphicsDevice);
|
||||
Texture texture = uploader.CreateTexture2DFromCompressed(imagePath);
|
||||
uploader.Upload();
|
||||
uploader.Dispose();
|
||||
|
||||
NativeMemory.Free(fontFileByteBuffer);
|
||||
NativeMemory.Free(atlasFileByteBuffer);
|
||||
|
||||
return new Font(graphicsDevice, handle, texture, pixelsPerEm, distanceRange);
|
||||
}
|
||||
|
||||
private Font(GraphicsDevice device, IntPtr handle, Texture texture, float pixelsPerEm, float distanceRange) : base(device)
|
||||
{
|
||||
Handle = handle;
|
||||
Texture = texture;
|
||||
PixelsPerEm = pixelsPerEm;
|
||||
DistanceRange = distanceRange;
|
||||
|
||||
StringBytesLength = 32;
|
||||
StringBytes = (byte*)NativeMemory.Alloc((nuint)StringBytesLength);
|
||||
}
|
||||
|
||||
public unsafe bool TextBounds(
|
||||
string text,
|
||||
int pixelSize,
|
||||
HorizontalAlignment horizontalAlignment,
|
||||
VerticalAlignment verticalAlignment,
|
||||
out Wellspring.Rectangle rectangle
|
||||
)
|
||||
{
|
||||
int byteCount = System.Text.Encoding.UTF8.GetByteCount(text);
|
||||
|
||||
if (StringBytesLength < byteCount)
|
||||
{
|
||||
StringBytes = (byte*)NativeMemory.Realloc(StringBytes, (nuint)byteCount);
|
||||
}
|
||||
|
||||
fixed (char* chars = text)
|
||||
{
|
||||
System.Text.Encoding.UTF8.GetBytes(chars, text.Length, StringBytes, byteCount);
|
||||
|
||||
byte result = Wellspring.Wellspring_TextBounds(
|
||||
Handle,
|
||||
pixelSize,
|
||||
(Wellspring.HorizontalAlignment)horizontalAlignment,
|
||||
(Wellspring.VerticalAlignment)verticalAlignment,
|
||||
(IntPtr)StringBytes,
|
||||
(uint)byteCount,
|
||||
out rectangle
|
||||
);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
Log.Warning("Could not decode string: " + text);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Texture.Dispose();
|
||||
}
|
||||
|
||||
Wellspring.Wellspring_DestroyFont(Handle);
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
26
Nerfed.Runtime/Graphics/Font/Structs.cs
Normal file
26
Nerfed.Runtime/Graphics/Font/Structs.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nerfed.Runtime.Graphics;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FontVertex : IVertexType
|
||||
{
|
||||
public Vector3 Position;
|
||||
public Vector2 TexCoord;
|
||||
public Color Color;
|
||||
|
||||
public static VertexElementFormat[] Formats { get; } =
|
||||
[
|
||||
VertexElementFormat.Vector3,
|
||||
VertexElementFormat.Vector2,
|
||||
VertexElementFormat.Color
|
||||
];
|
||||
|
||||
public static uint[] Offsets { get; } =
|
||||
[
|
||||
0,
|
||||
12,
|
||||
20
|
||||
];
|
||||
}
|
191
Nerfed.Runtime/Graphics/Font/TextBatch.cs
Normal file
191
Nerfed.Runtime/Graphics/Font/TextBatch.cs
Normal file
@ -0,0 +1,191 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using WellspringCS;
|
||||
|
||||
namespace Nerfed.Runtime.Graphics;
|
||||
|
||||
public unsafe class TextBatch : GraphicsResource
|
||||
{
|
||||
public const int INITIAL_CHAR_COUNT = 64;
|
||||
public const int INITIAL_VERTEX_COUNT = INITIAL_CHAR_COUNT * 4;
|
||||
public const int INITIAL_INDEX_COUNT = INITIAL_CHAR_COUNT * 6;
|
||||
|
||||
public IntPtr Handle { get; }
|
||||
public Buffer VertexBuffer { get; protected set; } = null;
|
||||
public Buffer IndexBuffer { get; protected set; } = null;
|
||||
public uint PrimitiveCount { get; protected set; }
|
||||
public Font CurrentFont { get; private set; }
|
||||
|
||||
private readonly GraphicsDevice graphicsDevice;
|
||||
private readonly int stringBytesLength;
|
||||
private TransferBuffer TransferBuffer;
|
||||
private byte* stringBytes;
|
||||
|
||||
public TextBatch(GraphicsDevice device) : base(device)
|
||||
{
|
||||
graphicsDevice = device;
|
||||
Handle = Wellspring.Wellspring_CreateTextBatch();
|
||||
|
||||
stringBytesLength = 128;
|
||||
stringBytes = (byte*)NativeMemory.Alloc((nuint)stringBytesLength);
|
||||
|
||||
VertexBuffer = Buffer.Create<FontVertex>(graphicsDevice, BufferUsageFlags.Vertex, INITIAL_VERTEX_COUNT);
|
||||
IndexBuffer = Buffer.Create<uint>(graphicsDevice, BufferUsageFlags.Index, INITIAL_INDEX_COUNT);
|
||||
|
||||
TransferBuffer = TransferBuffer.Create<byte>(
|
||||
graphicsDevice,
|
||||
TransferBufferUsage.Upload,
|
||||
VertexBuffer.Size + IndexBuffer.Size
|
||||
);
|
||||
}
|
||||
|
||||
// Call this to initialize or reset the batch.
|
||||
public void Start(Font font)
|
||||
{
|
||||
Wellspring.Wellspring_StartTextBatch(Handle, font.Handle);
|
||||
CurrentFont = font;
|
||||
PrimitiveCount = 0;
|
||||
}
|
||||
|
||||
// Add text with size and color to the batch
|
||||
public unsafe bool Add(
|
||||
string text,
|
||||
int pixelSize,
|
||||
Color color,
|
||||
HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment verticalAlignment = VerticalAlignment.Baseline
|
||||
)
|
||||
{
|
||||
int byteCount = System.Text.Encoding.UTF8.GetByteCount(text);
|
||||
|
||||
if (stringBytesLength < byteCount)
|
||||
{
|
||||
stringBytes = (byte*)NativeMemory.Realloc(stringBytes, (nuint)byteCount);
|
||||
}
|
||||
|
||||
fixed (char* chars = text)
|
||||
{
|
||||
System.Text.Encoding.UTF8.GetBytes(chars, text.Length, stringBytes, byteCount);
|
||||
|
||||
byte result = Wellspring.Wellspring_AddToTextBatch(
|
||||
Handle,
|
||||
pixelSize,
|
||||
new Wellspring.Color { R = color.R, G = color.G, B = color.B, A = color.A },
|
||||
(Wellspring.HorizontalAlignment)horizontalAlignment,
|
||||
(Wellspring.VerticalAlignment)verticalAlignment,
|
||||
(IntPtr)stringBytes,
|
||||
(uint)byteCount
|
||||
);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
Log.Warning("Could not decode string: " + text);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Call this after you have made all the Add calls you want, but before beginning a render pass.
|
||||
public unsafe void UploadBufferData(CommandBuffer commandBuffer)
|
||||
{
|
||||
Wellspring.Wellspring_GetBufferData(
|
||||
Handle,
|
||||
out uint vertexCount,
|
||||
out IntPtr vertexDataPointer,
|
||||
out uint vertexDataLengthInBytes,
|
||||
out IntPtr indexDataPointer,
|
||||
out uint indexDataLengthInBytes
|
||||
);
|
||||
|
||||
Span<byte> vertexSpan = new Span<byte>((void*)vertexDataPointer, (int)vertexDataLengthInBytes);
|
||||
Span<byte> indexSpan = new Span<byte>((void*)indexDataPointer, (int)indexDataLengthInBytes);
|
||||
|
||||
bool newTransferBufferNeeded = false;
|
||||
|
||||
if (VertexBuffer.Size < vertexDataLengthInBytes)
|
||||
{
|
||||
VertexBuffer.Dispose();
|
||||
VertexBuffer = new Buffer(graphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes);
|
||||
newTransferBufferNeeded = true;
|
||||
}
|
||||
|
||||
if (IndexBuffer.Size < indexDataLengthInBytes)
|
||||
{
|
||||
IndexBuffer.Dispose();
|
||||
IndexBuffer = new Buffer(graphicsDevice, BufferUsageFlags.Index, vertexDataLengthInBytes);
|
||||
newTransferBufferNeeded = true;
|
||||
}
|
||||
|
||||
if (newTransferBufferNeeded)
|
||||
{
|
||||
TransferBuffer.Dispose();
|
||||
TransferBuffer = new TransferBuffer(
|
||||
graphicsDevice,
|
||||
TransferBufferUsage.Upload,
|
||||
VertexBuffer.Size + IndexBuffer.Size
|
||||
);
|
||||
}
|
||||
|
||||
if (vertexDataLengthInBytes > 0 && indexDataLengthInBytes > 0)
|
||||
{
|
||||
TransferBuffer.SetData(vertexSpan, true);
|
||||
TransferBuffer.SetData(indexSpan, (uint)vertexSpan.Length, false);
|
||||
|
||||
CopyPass copyPass = commandBuffer.BeginCopyPass();
|
||||
copyPass.UploadToBuffer(
|
||||
new TransferBufferLocation(TransferBuffer),
|
||||
new BufferRegion(VertexBuffer, 0, (uint)vertexSpan.Length),
|
||||
true
|
||||
);
|
||||
copyPass.UploadToBuffer(
|
||||
new TransferBufferLocation(TransferBuffer, (uint)vertexSpan.Length),
|
||||
new BufferRegion(IndexBuffer, 0, (uint)indexSpan.Length),
|
||||
true
|
||||
);
|
||||
commandBuffer.EndCopyPass(copyPass);
|
||||
}
|
||||
|
||||
PrimitiveCount = vertexCount / 2;
|
||||
}
|
||||
|
||||
// Call this AFTER binding your text pipeline!
|
||||
public void Render(CommandBuffer commandBuffer, RenderPass renderPass, Matrix4x4 transformMatrix)
|
||||
{
|
||||
commandBuffer.PushVertexUniformData(transformMatrix);
|
||||
commandBuffer.PushFragmentUniformData(CurrentFont.DistanceRange);
|
||||
|
||||
renderPass.BindFragmentSampler(
|
||||
new TextureSamplerBinding(
|
||||
CurrentFont.Texture,
|
||||
graphicsDevice.LinearSampler
|
||||
)
|
||||
);
|
||||
renderPass.BindVertexBuffer(VertexBuffer);
|
||||
renderPass.BindIndexBuffer(IndexBuffer, IndexElementSize.ThirtyTwo);
|
||||
|
||||
renderPass.DrawIndexedPrimitives(
|
||||
0,
|
||||
0,
|
||||
PrimitiveCount
|
||||
);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
VertexBuffer.Dispose();
|
||||
IndexBuffer.Dispose();
|
||||
}
|
||||
|
||||
NativeMemory.Free(stringBytes);
|
||||
Wellspring.Wellspring_DestroyTextBatch(Handle);
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
400
Nerfed.Runtime/Graphics/GraphicsDevice.cs
Normal file
400
Nerfed.Runtime/Graphics/GraphicsDevice.cs
Normal file
@ -0,0 +1,400 @@
|
||||
using Nerfed.Runtime.Video;
|
||||
using RefreshCS;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nerfed.Runtime.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Manages all graphics-related concerns.
|
||||
/// </summary>
|
||||
public class GraphicsDevice : IDisposable
|
||||
{
|
||||
public IntPtr Handle { get; }
|
||||
public BackendFlags Backend { get; }
|
||||
public bool DebugMode { get; }
|
||||
|
||||
// Built-in shaders
|
||||
public Shader FullscreenVertexShader { get; private set; }
|
||||
public Shader VideoFragmentShader { get; private set; }
|
||||
public Shader TextVertexShader { get; private set; }
|
||||
public Shader TextFragmentShader { get; private set; }
|
||||
|
||||
// Built-in video pipeline
|
||||
internal GraphicsPipeline VideoPipeline { get; private set; }
|
||||
|
||||
// Built-in text shader info
|
||||
public VertexInputState TextVertexInputState { get; }
|
||||
|
||||
// Built-in samplers
|
||||
public Sampler PointSampler { get; }
|
||||
public Sampler LinearSampler { get; }
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
internal readonly RenderPassPool RenderPassPool = new RenderPassPool();
|
||||
internal readonly ComputePassPool ComputePassPool = new ComputePassPool();
|
||||
internal readonly CopyPassPool CopyPassPool = new CopyPassPool();
|
||||
private readonly HashSet<GCHandle> resources = new HashSet<GCHandle>();
|
||||
private readonly CommandBufferPool commandBufferPool;
|
||||
private readonly FencePool fencePool;
|
||||
|
||||
internal GraphicsDevice(BackendFlags preferredBackends)
|
||||
{
|
||||
if (preferredBackends == BackendFlags.Invalid)
|
||||
{
|
||||
throw new System.Exception("Could not set graphics backend!");
|
||||
}
|
||||
|
||||
bool debugMode = false;
|
||||
#if DEBUG
|
||||
debugMode = true;
|
||||
#endif
|
||||
|
||||
Handle = Refresh.Refresh_CreateDevice((Refresh.BackendFlags)preferredBackends, Conversions.BoolToInt(debugMode));
|
||||
|
||||
DebugMode = debugMode;
|
||||
// TODO: check for CreateDevice fail
|
||||
|
||||
Backend = (BackendFlags)Refresh.Refresh_GetBackend(Handle);
|
||||
|
||||
TextVertexInputState = VertexInputState.CreateSingleBinding<FontVertex>();
|
||||
|
||||
PointSampler = new Sampler(this, SamplerCreateInfo.PointClamp);
|
||||
LinearSampler = new Sampler(this, SamplerCreateInfo.LinearClamp);
|
||||
|
||||
fencePool = new FencePool(this);
|
||||
commandBufferPool = new CommandBufferPool(this);
|
||||
}
|
||||
|
||||
internal void LoadDefaultPipelines()
|
||||
{
|
||||
FullscreenVertexShader = ResourceManager.Load<Shader>("Shaders/Fullscreen.vert");
|
||||
VideoFragmentShader = ResourceManager.Load<Shader>("Shaders/Video.frag");
|
||||
TextVertexShader = ResourceManager.Load<Shader>("Shaders/Text.vert");
|
||||
TextFragmentShader = ResourceManager.Load<Shader>("Shaders/Text.frag");
|
||||
|
||||
VideoPipeline = new GraphicsPipeline(
|
||||
this,
|
||||
new GraphicsPipelineCreateInfo
|
||||
{
|
||||
AttachmentInfo = new GraphicsPipelineAttachmentInfo(
|
||||
new ColorAttachmentDescription(
|
||||
TextureFormat.R8G8B8A8,
|
||||
ColorAttachmentBlendState.None
|
||||
)
|
||||
),
|
||||
DepthStencilState = DepthStencilState.Disable,
|
||||
VertexShader = FullscreenVertexShader,
|
||||
FragmentShader = VideoFragmentShader,
|
||||
VertexInputState = VertexInputState.Empty,
|
||||
RasterizerState = RasterizerState.CCW_CullNone,
|
||||
PrimitiveType = PrimitiveType.TriangleList,
|
||||
MultisampleState = MultisampleState.None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares a window so that frames can be presented to it.
|
||||
/// </summary>
|
||||
/// <param name="swapchainComposition">The desired composition of the swapchain. Ignore this unless you are using HDR or tonemapping.</param>
|
||||
/// <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,
|
||||
SwapchainComposition swapchainComposition,
|
||||
PresentMode presentMode
|
||||
)
|
||||
{
|
||||
if (window.Claimed)
|
||||
{
|
||||
Log.Error("Window already claimed!");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = Conversions.IntToBool(
|
||||
Refresh.Refresh_ClaimWindow(
|
||||
Handle,
|
||||
window.Handle,
|
||||
(Refresh.SwapchainComposition)swapchainComposition,
|
||||
(Refresh.PresentMode)presentMode
|
||||
)
|
||||
);
|
||||
|
||||
if (success)
|
||||
{
|
||||
window.Claimed = true;
|
||||
window.SwapchainComposition = swapchainComposition;
|
||||
window.SwapchainFormat = GetSwapchainFormat(window);
|
||||
|
||||
if (window.SwapchainTexture == null)
|
||||
{
|
||||
window.SwapchainTexture = new Texture(this, window.SwapchainFormat);
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public bool SetSwapchainParameters(
|
||||
Window window,
|
||||
SwapchainComposition swapchainComposition,
|
||||
PresentMode presentMode
|
||||
)
|
||||
{
|
||||
if (!window.Claimed)
|
||||
{
|
||||
Log.Error("Cannot set present mode on unclaimed window!");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = Conversions.IntToBool(
|
||||
Refresh.Refresh_SetSwapchainParameters(
|
||||
Handle,
|
||||
window.Handle,
|
||||
(Refresh.SwapchainComposition)swapchainComposition,
|
||||
(Refresh.PresentMode)presentMode
|
||||
)
|
||||
);
|
||||
|
||||
if (success)
|
||||
{
|
||||
window.SwapchainComposition = swapchainComposition;
|
||||
window.SwapchainFormat = GetSwapchainFormat(window);
|
||||
|
||||
if (window.SwapchainTexture != null)
|
||||
{
|
||||
window.SwapchainTexture.Format = window.SwapchainFormat;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Acquires a command buffer.
|
||||
/// This is the start of your rendering process.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CommandBuffer AcquireCommandBuffer()
|
||||
{
|
||||
CommandBuffer 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(
|
||||
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)
|
||||
{
|
||||
IntPtr fenceHandle = Refresh.Refresh_SubmitAndAcquireFence(
|
||||
commandBuffer.Handle
|
||||
);
|
||||
|
||||
Fence fence = fencePool.Obtain();
|
||||
fence.SetHandle(fenceHandle);
|
||||
|
||||
return fence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for the graphics device to become idle.
|
||||
/// </summary>
|
||||
public void Wait()
|
||||
{
|
||||
Refresh.Refresh_Wait(Handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the given fence to become signaled.
|
||||
/// </summary>
|
||||
public unsafe void WaitForFence(Fence fence)
|
||||
{
|
||||
IntPtr fenceHandle = fence.Handle;
|
||||
|
||||
Refresh.Refresh_WaitForFences(
|
||||
Handle,
|
||||
1,
|
||||
&fenceHandle,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
/// <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(Span<Fence> fences, bool waitAll)
|
||||
{
|
||||
IntPtr* handlePtr = stackalloc nint[fences.Length];
|
||||
|
||||
for (int i = 0; i < fences.Length; i += 1)
|
||||
{
|
||||
handlePtr[i] = fences[i].Handle;
|
||||
}
|
||||
|
||||
Refresh.Refresh_WaitForFences(
|
||||
Handle,
|
||||
Conversions.BoolToInt(waitAll),
|
||||
handlePtr,
|
||||
(uint)fences.Length
|
||||
);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
int 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)
|
||||
{
|
||||
if (!window.Claimed)
|
||||
{
|
||||
throw new System.ArgumentException("Cannot get swapchain format of unclaimed window!");
|
||||
}
|
||||
|
||||
return (TextureFormat)Refresh.Refresh_GetSwapchainTextureFormat(Handle, window.Handle);
|
||||
}
|
||||
|
||||
internal void AddResourceReference(GCHandle resourceReference)
|
||||
{
|
||||
lock (resources)
|
||||
{
|
||||
resources.Add(resourceReference);
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveResourceReference(GCHandle resourceReference)
|
||||
{
|
||||
lock (resources)
|
||||
{
|
||||
resources.Remove(resourceReference);
|
||||
}
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
lock (resources)
|
||||
{
|
||||
// Dispose video players first to avoid race condition on threaded decoding
|
||||
foreach (GCHandle resource in resources)
|
||||
{
|
||||
if (resource.Target is VideoPlayer player)
|
||||
{
|
||||
player.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Dispose everything else
|
||||
foreach (GCHandle resource in resources)
|
||||
{
|
||||
if (resource.Target is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
resources.Clear();
|
||||
}
|
||||
|
||||
ResourceManager.Unload(FullscreenVertexShader);
|
||||
ResourceManager.Unload(TextFragmentShader);
|
||||
ResourceManager.Unload(TextVertexShader);
|
||||
ResourceManager.Unload(VideoFragmentShader);
|
||||
}
|
||||
|
||||
Refresh.Refresh_DestroyDevice(Handle);
|
||||
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
~GraphicsDevice()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
53
Nerfed.Runtime/Graphics/GraphicsResource.cs
Normal file
53
Nerfed.Runtime/Graphics/GraphicsResource.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nerfed.Runtime.Graphics;
|
||||
|
||||
public abstract class GraphicsResource : IDisposable
|
||||
{
|
||||
public GraphicsDevice Device { get; }
|
||||
|
||||
private GCHandle SelfReference;
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
protected GraphicsResource(GraphicsDevice device)
|
||||
{
|
||||
Device = device;
|
||||
|
||||
SelfReference = GCHandle.Alloc(this, GCHandleType.Weak);
|
||||
Device.AddResourceReference(SelfReference);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Device.RemoveResourceReference(SelfReference);
|
||||
SelfReference.Free();
|
||||
}
|
||||
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
~GraphicsResource()
|
||||
{
|
||||
#if DEBUG
|
||||
// If you see this log message, you leaked a graphics resource without disposing it!
|
||||
// We'll try to clean it up for you but you really should fix this.
|
||||
Log.Warning($"A resource of type {GetType().Name} was not Disposed.");
|
||||
#endif
|
||||
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
17
Nerfed.Runtime/Graphics/IVertexType.cs
Normal file
17
Nerfed.Runtime/Graphics/IVertexType.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace Nerfed.Runtime.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; }
|
||||
|
||||
/// <summary>
|
||||
/// An ordered list of the offsets in your vertex struct.
|
||||
/// </summary>
|
||||
static abstract uint[] Offsets { get; }
|
||||
}
|
455
Nerfed.Runtime/Graphics/ImageUtils.cs
Normal file
455
Nerfed.Runtime/Graphics/ImageUtils.cs
Normal file
@ -0,0 +1,455 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using RefreshCS;
|
||||
|
||||
namespace Nerfed.Runtime.Graphics;
|
||||
|
||||
public static class ImageUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets pointer to pixel data from compressed image byte data.
|
||||
///
|
||||
/// The returned pointer must be freed by calling FreePixelData.
|
||||
/// </summary>
|
||||
public static unsafe byte* GetPixelDataFromBytes(
|
||||
Span<byte> data,
|
||||
out uint width,
|
||||
out uint height,
|
||||
out uint sizeInBytes
|
||||
) {
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
byte* pixelData =
|
||||
Refresh.Refresh_Image_Load(
|
||||
ptr,
|
||||
data.Length,
|
||||
out int w,
|
||||
out int h,
|
||||
out int len
|
||||
);
|
||||
|
||||
width = (uint) w;
|
||||
height = (uint) h;
|
||||
sizeInBytes = (uint) len;
|
||||
|
||||
return pixelData;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets pointer to pixel data from a compressed image stream.
|
||||
///
|
||||
/// The returned pointer must be freed by calling FreePixelData.
|
||||
/// </summary>
|
||||
public static unsafe byte* GetPixelDataFromStream(
|
||||
Stream stream,
|
||||
out uint width,
|
||||
out uint height,
|
||||
out uint sizeInBytes
|
||||
) {
|
||||
long length = stream.Length;
|
||||
void* buffer = NativeMemory.Alloc((nuint) length);
|
||||
Span<byte> span = new Span<byte>(buffer, (int) length);
|
||||
stream.ReadExactly(span);
|
||||
|
||||
byte* pixelData = GetPixelDataFromBytes(span, out width, out height, out sizeInBytes);
|
||||
|
||||
NativeMemory.Free(buffer);
|
||||
|
||||
return pixelData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets pointer to pixel data from a compressed image file.
|
||||
///
|
||||
/// The returned pointer must be freed by calling FreePixelData.
|
||||
/// </summary>
|
||||
public static unsafe byte* GetPixelDataFromFile(
|
||||
string path,
|
||||
out uint width,
|
||||
out uint height,
|
||||
out uint sizeInBytes
|
||||
) {
|
||||
FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||
return GetPixelDataFromStream(fileStream, out width, out height, out sizeInBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get metadata from compressed image bytes.
|
||||
/// </summary>
|
||||
public static unsafe bool ImageInfoFromBytes(
|
||||
Span<byte> data,
|
||||
out uint width,
|
||||
out uint height,
|
||||
out uint sizeInBytes
|
||||
) {
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
int result =
|
||||
Refresh.Refresh_Image_Info(
|
||||
ptr,
|
||||
data.Length,
|
||||
out int w,
|
||||
out int h,
|
||||
out int len
|
||||
);
|
||||
|
||||
width = (uint) w;
|
||||
height = (uint) h;
|
||||
sizeInBytes = (uint) len;
|
||||
|
||||
return Conversions.IntToBool(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get metadata from a compressed image stream.
|
||||
/// </summary>
|
||||
public static unsafe bool ImageInfoFromStream(
|
||||
Stream stream,
|
||||
out uint width,
|
||||
out uint height,
|
||||
out uint sizeInBytes
|
||||
) {
|
||||
long length = stream.Length;
|
||||
void* buffer = NativeMemory.Alloc((nuint) length);
|
||||
Span<byte> span = new Span<byte>(buffer, (int) length);
|
||||
stream.ReadExactly(span);
|
||||
|
||||
bool result = ImageInfoFromBytes(span, out width, out height, out sizeInBytes);
|
||||
|
||||
NativeMemory.Free(buffer);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get metadata from a compressed image file.
|
||||
/// </summary>
|
||||
public static bool ImageInfoFromFile(
|
||||
string path,
|
||||
out uint width,
|
||||
out uint height,
|
||||
out uint sizeInBytes
|
||||
) {
|
||||
FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||
return ImageInfoFromStream(fileStream, out width, out height, out sizeInBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees pixel data obtained from GetPixelData methods.
|
||||
/// </summary>
|
||||
public unsafe static void FreePixelData(byte* pixels)
|
||||
{
|
||||
Refresh.Refresh_Image_Free(pixels);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves pixel data contained in a TransferBuffer to a PNG file.
|
||||
/// </summary>
|
||||
public static unsafe void SavePNG(
|
||||
string path,
|
||||
TransferBuffer transferBuffer,
|
||||
uint bufferOffsetInBytes,
|
||||
int width,
|
||||
int height,
|
||||
bool bgra
|
||||
) {
|
||||
int sizeInBytes = width * height * 4;
|
||||
|
||||
byte* pixelsPtr = (byte*) NativeMemory.Alloc((nuint) sizeInBytes);
|
||||
Span<byte> pixelsSpan = new Span<byte>(pixelsPtr, sizeInBytes);
|
||||
|
||||
transferBuffer.GetData(pixelsSpan, bufferOffsetInBytes);
|
||||
|
||||
if (bgra)
|
||||
{
|
||||
// if data is bgra, we have to swap the R and B channels
|
||||
byte* rgbaPtr = (byte*) NativeMemory.Alloc((nuint) sizeInBytes);
|
||||
Span<byte> rgbaSpan = new Span<byte>(rgbaPtr, sizeInBytes);
|
||||
|
||||
for (int i = 0; i < sizeInBytes; i += 4)
|
||||
{
|
||||
rgbaSpan[i] = pixelsSpan[i + 2];
|
||||
rgbaSpan[i + 1] = pixelsSpan[i + 1];
|
||||
rgbaSpan[i + 2] = pixelsSpan[i];
|
||||
rgbaSpan[i + 3] = pixelsSpan[i + 3];
|
||||
}
|
||||
|
||||
NativeMemory.Free(pixelsPtr);
|
||||
pixelsPtr = rgbaPtr;
|
||||
}
|
||||
|
||||
Refresh.Refresh_Image_SavePNG(path, pixelsPtr, width, height);
|
||||
NativeMemory.Free(pixelsPtr);
|
||||
}
|
||||
|
||||
// DDS loading extension, based on MojoDDS
|
||||
// Taken from https://github.com/FNA-XNA/FNA/blob/1e49f868f595f62bc6385db45949a03186a7cd7f/src/Graphics/Texture.cs#L194
|
||||
public static void ParseDDS(
|
||||
BinaryReader reader,
|
||||
out TextureFormat format,
|
||||
out int width,
|
||||
out int height,
|
||||
out int levels,
|
||||
out bool isCube
|
||||
) {
|
||||
// A whole bunch of magic numbers, yay DDS!
|
||||
const uint DDS_MAGIC = 0x20534444;
|
||||
const uint DDS_HEADERSIZE = 124;
|
||||
const uint DDS_PIXFMTSIZE = 32;
|
||||
const uint DDSD_HEIGHT = 0x2;
|
||||
const uint DDSD_WIDTH = 0x4;
|
||||
const uint DDSD_PITCH = 0x8;
|
||||
const uint DDSD_LINEARSIZE = 0x80000;
|
||||
const uint DDSD_REQ = (
|
||||
DDSD_HEIGHT | DDSD_WIDTH
|
||||
);
|
||||
const uint DDSCAPS_MIPMAP = 0x400000;
|
||||
const uint DDSCAPS_TEXTURE = 0x1000;
|
||||
const uint DDSCAPS2_CUBEMAP = 0x200;
|
||||
const uint DDPF_FOURCC = 0x4;
|
||||
const uint DDPF_RGB = 0x40;
|
||||
const uint FOURCC_DXT1 = 0x31545844;
|
||||
const uint FOURCC_DXT3 = 0x33545844;
|
||||
const uint FOURCC_DXT5 = 0x35545844;
|
||||
const uint FOURCC_DX10 = 0x30315844;
|
||||
const uint pitchAndLinear = (
|
||||
DDSD_PITCH | DDSD_LINEARSIZE
|
||||
);
|
||||
|
||||
// File should start with 'DDS '
|
||||
if (reader.ReadUInt32() != DDS_MAGIC)
|
||||
{
|
||||
throw new NotSupportedException("Not a DDS!");
|
||||
}
|
||||
|
||||
// Texture info
|
||||
uint size = reader.ReadUInt32();
|
||||
if (size != DDS_HEADERSIZE)
|
||||
{
|
||||
throw new NotSupportedException("Invalid DDS header!");
|
||||
}
|
||||
uint flags = reader.ReadUInt32();
|
||||
if ((flags & DDSD_REQ) != DDSD_REQ)
|
||||
{
|
||||
throw new NotSupportedException("Invalid DDS flags!");
|
||||
}
|
||||
if ((flags & pitchAndLinear) == pitchAndLinear)
|
||||
{
|
||||
throw new NotSupportedException("Invalid DDS flags!");
|
||||
}
|
||||
height = reader.ReadInt32();
|
||||
width = reader.ReadInt32();
|
||||
reader.ReadUInt32(); // dwPitchOrLinearSize, unused
|
||||
reader.ReadUInt32(); // dwDepth, unused
|
||||
levels = reader.ReadInt32();
|
||||
|
||||
// "Reserved"
|
||||
reader.ReadBytes(4 * 11);
|
||||
|
||||
// Format info
|
||||
uint formatSize = reader.ReadUInt32();
|
||||
if (formatSize != DDS_PIXFMTSIZE)
|
||||
{
|
||||
throw new NotSupportedException("Bogus PIXFMTSIZE!");
|
||||
}
|
||||
uint formatFlags = reader.ReadUInt32();
|
||||
uint formatFourCC = reader.ReadUInt32();
|
||||
uint formatRGBBitCount = reader.ReadUInt32();
|
||||
uint formatRBitMask = reader.ReadUInt32();
|
||||
uint formatGBitMask = reader.ReadUInt32();
|
||||
uint formatBBitMask = reader.ReadUInt32();
|
||||
uint formatABitMask = reader.ReadUInt32();
|
||||
|
||||
// dwCaps "stuff"
|
||||
uint caps = reader.ReadUInt32();
|
||||
if ((caps & DDSCAPS_TEXTURE) == 0)
|
||||
{
|
||||
throw new NotSupportedException("Not a texture!");
|
||||
}
|
||||
|
||||
isCube = false;
|
||||
|
||||
uint caps2 = reader.ReadUInt32();
|
||||
if (caps2 != 0)
|
||||
{
|
||||
if ((caps2 & DDSCAPS2_CUBEMAP) == DDSCAPS2_CUBEMAP)
|
||||
{
|
||||
isCube = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Invalid caps2!");
|
||||
}
|
||||
}
|
||||
|
||||
reader.ReadUInt32(); // dwCaps3, unused
|
||||
reader.ReadUInt32(); // dwCaps4, unused
|
||||
|
||||
// "Reserved"
|
||||
reader.ReadUInt32();
|
||||
|
||||
// Mipmap sanity check
|
||||
if ((caps & DDSCAPS_MIPMAP) != DDSCAPS_MIPMAP)
|
||||
{
|
||||
levels = 1;
|
||||
}
|
||||
|
||||
// Determine texture format
|
||||
if ((formatFlags & DDPF_FOURCC) == DDPF_FOURCC)
|
||||
{
|
||||
switch (formatFourCC)
|
||||
{
|
||||
case 0x71: // D3DFMT_A16B16G16R16F
|
||||
format = TextureFormat.R16G16B16A16_SFLOAT;
|
||||
break;
|
||||
case 0x74: // D3DFMT_A32B32G32R32F
|
||||
format = TextureFormat.R32G32B32A32_SFLOAT;
|
||||
break;
|
||||
case FOURCC_DXT1:
|
||||
format = TextureFormat.BC1;
|
||||
break;
|
||||
case FOURCC_DXT3:
|
||||
format = TextureFormat.BC2;
|
||||
break;
|
||||
case FOURCC_DXT5:
|
||||
format = TextureFormat.BC3;
|
||||
break;
|
||||
case FOURCC_DX10:
|
||||
// If the fourCC is DX10, there is an extra header with additional format information.
|
||||
uint dxgiFormat = reader.ReadUInt32();
|
||||
|
||||
// These values are taken from the DXGI_FORMAT enum.
|
||||
switch (dxgiFormat)
|
||||
{
|
||||
case 2:
|
||||
format = TextureFormat.R32G32B32A32_SFLOAT;
|
||||
break;
|
||||
|
||||
case 10:
|
||||
format = TextureFormat.R16G16B16A16_SFLOAT;
|
||||
break;
|
||||
|
||||
case 71:
|
||||
format = TextureFormat.BC1;
|
||||
break;
|
||||
|
||||
case 74:
|
||||
format = TextureFormat.BC2;
|
||||
break;
|
||||
|
||||
case 77:
|
||||
format = TextureFormat.BC3;
|
||||
break;
|
||||
|
||||
case 98:
|
||||
format = TextureFormat.BC7;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException(
|
||||
"Unsupported DDS texture format"
|
||||
);
|
||||
}
|
||||
|
||||
uint resourceDimension = reader.ReadUInt32();
|
||||
|
||||
// These values are taken from the D3D10_RESOURCE_DIMENSION enum.
|
||||
switch (resourceDimension)
|
||||
{
|
||||
case 0: // Unknown
|
||||
case 1: // Buffer
|
||||
throw new NotSupportedException(
|
||||
"Unsupported DDS texture format"
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* This flag seemingly only indicates if the texture is a cube map.
|
||||
* This is already determined above. Cool!
|
||||
*/
|
||||
uint miscFlag = reader.ReadUInt32();
|
||||
|
||||
/*
|
||||
* Indicates the number of elements in the texture array.
|
||||
* We don't support texture arrays so just throw if it's greater than 1.
|
||||
*/
|
||||
uint arraySize = reader.ReadUInt32();
|
||||
|
||||
if (arraySize > 1)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Unsupported DDS texture format"
|
||||
);
|
||||
}
|
||||
|
||||
reader.ReadUInt32(); // reserved
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(
|
||||
"Unsupported DDS texture format"
|
||||
);
|
||||
}
|
||||
}
|
||||
else if ((formatFlags & DDPF_RGB) == DDPF_RGB)
|
||||
{
|
||||
if ( formatRGBBitCount != 32 ||
|
||||
formatRBitMask != 0x00FF0000 ||
|
||||
formatGBitMask != 0x0000FF00 ||
|
||||
formatBBitMask != 0x000000FF ||
|
||||
formatABitMask != 0xFF000000 )
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Unsupported DDS texture format"
|
||||
);
|
||||
}
|
||||
|
||||
format = TextureFormat.B8G8R8A8;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Unsupported DDS texture format"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static int CalculateDDSLevelSize(
|
||||
int width,
|
||||
int height,
|
||||
TextureFormat format
|
||||
) {
|
||||
if (format == TextureFormat.R8G8B8A8)
|
||||
{
|
||||
return (((width * 32) + 7) / 8) * height;
|
||||
}
|
||||
else if (format == TextureFormat.R16G16B16A16_SFLOAT)
|
||||
{
|
||||
return (((width * 64) + 7) / 8) * height;
|
||||
}
|
||||
else if (format == TextureFormat.R32G32B32A32_SFLOAT)
|
||||
{
|
||||
return (((width * 128) + 7) / 8) * height;
|
||||
}
|
||||
else
|
||||
{
|
||||
int blockSize = 16;
|
||||
if (format == TextureFormat.BC1)
|
||||
{
|
||||
blockSize = 8;
|
||||
}
|
||||
width = System.Math.Max(width, 1);
|
||||
height = System.Math.Max(height, 1);
|
||||
return (
|
||||
((width + 3) / 4) *
|
||||
((height + 3) / 4) *
|
||||
blockSize
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
161
Nerfed.Runtime/Graphics/PackedVector/Alpha8.cs
Normal file
161
Nerfed.Runtime/Graphics/PackedVector/Alpha8.cs
Normal file
@ -0,0 +1,161 @@
|
||||
/* MoonWorks - Game Development Framework
|
||||
* Copyright 2021 Evan Hemsley
|
||||
*/
|
||||
|
||||
/* Derived from code by Ethan Lee (Copyright 2009-2021).
|
||||
* Released under the Microsoft Public License.
|
||||
* See fna.LICENSE for details.
|
||||
|
||||
* Derived from code by the Mono.Xna Team (Copyright 2006).
|
||||
* Released under the MIT License. See monoxna.LICENSE for details.
|
||||
*/
|
||||
|
||||
using System.Numerics;
|
||||
|
||||
namespace Nerfed.Runtime.Graphics.PackedVector;
|
||||
|
||||
/// <summary>
|
||||
/// Packed vector type containing unsigned normalized values ranging from 0 to 1.
|
||||
/// The x and z components use 5 bits, and the y component uses 6 bits.
|
||||
/// </summary>
|
||||
public struct Alpha8 : IPackedVector<byte>, IEquatable<Alpha8>, IPackedVector
|
||||
{
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets and sets the packed value.
|
||||
/// </summary>
|
||||
public byte PackedValue
|
||||
{
|
||||
get
|
||||
{
|
||||
return packedValue;
|
||||
}
|
||||
set
|
||||
{
|
||||
packedValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Variables
|
||||
|
||||
private byte packedValue;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Constructor
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of Alpha8.
|
||||
/// </summary>
|
||||
/// <param name="alpha">The alpha component</param>
|
||||
public Alpha8(float alpha)
|
||||
{
|
||||
packedValue = Pack(alpha);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the packed vector in float format.
|
||||
/// </summary>
|
||||
/// <returns>The packed vector in Vector3 format</returns>
|
||||
public float ToAlpha()
|
||||
{
|
||||
return (float) (packedValue / 255.0f);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IPackedVector Methods
|
||||
|
||||
/// <summary>
|
||||
/// Sets the packed vector from a Vector4.
|
||||
/// </summary>
|
||||
/// <param name="vector">Vector containing the components.</param>
|
||||
void IPackedVector.PackFromVector4(Vector4 vector)
|
||||
{
|
||||
packedValue = Pack(vector.W);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the packed vector in Vector4 format.
|
||||
/// </summary>
|
||||
/// <returns>The packed vector in Vector4 format</returns>
|
||||
Vector4 IPackedVector.ToVector4()
|
||||
{
|
||||
return new Vector4(
|
||||
0.0f,
|
||||
0.0f,
|
||||
0.0f,
|
||||
(float) (packedValue / 255.0f)
|
||||
);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Static Operators and Override Methods
|
||||
|
||||
/// <summary>
|
||||
/// Compares an object with the packed vector.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare.</param>
|
||||
/// <returns>True if the object is equal to the packed vector.</returns>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return (obj is Alpha8) && Equals((Alpha8) obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares another Bgra5551 packed vector with the packed vector.
|
||||
/// </summary>
|
||||
/// <param name="other">The Bgra5551 packed vector to compare.</param>
|
||||
/// <returns>True if the packed vectors are equal.</returns>
|
||||
public bool Equals(Alpha8 other)
|
||||
{
|
||||
return packedValue == other.packedValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string representation of the packed vector.
|
||||
/// </summary>
|
||||
/// <returns>A string representation of the packed vector.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return packedValue.ToString("X");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a hash code of the packed vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code for the packed vector.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return packedValue.GetHashCode();
|
||||
}
|
||||
|
||||
public static bool operator ==(Alpha8 lhs, Alpha8 rhs)
|
||||
{
|
||||
return lhs.packedValue == rhs.packedValue;
|
||||
}
|
||||
|
||||
public static bool operator !=(Alpha8 lhs, Alpha8 rhs)
|
||||
{
|
||||
return lhs.packedValue != rhs.packedValue;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Static Pack Method
|
||||
|
||||
private static byte Pack(float alpha)
|
||||
{
|
||||
return (byte) System.Math.Round(Math.Clamp(alpha, 0, 1) * 255.0f);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user