Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0132c1326d | ||
|
|
a0f2713fdc | ||
|
|
6ad5aa3f2c | ||
|
|
d9582aecdd | ||
|
|
7853a768de | ||
|
|
83f77d1ebe | ||
|
|
2d4139fb2c | ||
|
|
5eaf3547dc | ||
|
|
d80e1177b9 | ||
|
|
cf6cd080c6 | ||
|
|
87ee6df46f | ||
|
|
57b42d8daa | ||
|
|
2a351f7b9d | ||
|
|
7225d13880 | ||
|
|
567714a52d | ||
|
|
2c84e650d6 | ||
|
|
82fe47f627 | ||
|
|
6be63195f0 | ||
|
|
9387bfa59c | ||
|
|
86b54e1521 | ||
|
|
ba88432e77 | ||
|
|
5cc876fce9 | ||
|
|
91b4f5fafb | ||
|
|
0d14a32726 | ||
|
|
b3adef3a40 | ||
|
|
30deeca452 | ||
|
|
6f505f34a9 | ||
|
|
92cf24fe9f | ||
|
|
cce6e00960 | ||
|
|
1096597161 | ||
|
|
d45f7c3b8c | ||
|
|
7a81026ca5 | ||
|
|
60b85960ff | ||
|
|
b003ffbaec | ||
|
|
777059489c | ||
|
|
16b04ea22a | ||
|
|
4b824f3205 | ||
|
|
9890026656 | ||
|
|
d8b41b0827 | ||
|
|
dd3bbf1d5b | ||
|
|
38080703ec | ||
|
|
fe582c4fba | ||
|
|
42b978e8c9 | ||
|
|
2c839d8fad | ||
|
|
97c2b308f1 | ||
|
|
1eb899b240 | ||
|
|
7cbb745721 | ||
|
|
1e1ed303ad |
+7
-2
@@ -451,8 +451,13 @@ FodyWeavers.xsd
|
|||||||
|
|
||||||
#------------------------- Nerfed -------------------------
|
#------------------------- Nerfed -------------------------
|
||||||
|
|
||||||
|
Bin/
|
||||||
|
Intermediate/
|
||||||
|
|
||||||
imgui.ini
|
imgui.ini
|
||||||
|
|
||||||
# include libs
|
# include libs
|
||||||
!/libs/lib64
|
!/Native/lib64
|
||||||
!/libs/x64
|
!/Native/x64
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,9 @@
|
|||||||
[submodule "Nerfed.Runtime/Libraries/dav1dfile"]
|
[submodule "Nerfed.Runtime/Libraries/dav1dfile"]
|
||||||
path = Nerfed.Runtime/Libraries/dav1dfile
|
path = Nerfed.Runtime/Libraries/dav1dfile
|
||||||
url = https://github.com/MoonsideGames/dav1dfile.git
|
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
|
||||||
|
[submodule "Nerfed.Runtime/Libraries/MoonTools.ECS"]
|
||||||
|
path = Nerfed.Runtime/Libraries/MoonTools.ECS
|
||||||
|
url = https://github.com/MoonsideGames/MoonTools.ECS.git
|
||||||
|
|||||||
Generated
+7
@@ -2,5 +2,12 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<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/MoonTools.ECS" 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>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutDir>../Bin/$(MSBuildProjectName)</OutDir>
|
||||||
|
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">../Intermediate/$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
|
|
||||||
@@ -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
Binary file not shown.
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Nerfed.Builder;
|
||||||
|
|
||||||
|
public class ArgumentAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string ArgKey { get; }
|
||||||
|
|
||||||
|
public ArgumentAttribute(string argKey)
|
||||||
|
{
|
||||||
|
ArgKey = argKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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() { }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Nerfed.Builder;
|
||||||
|
|
||||||
|
public interface IImporter
|
||||||
|
{
|
||||||
|
void Import(string inFile, string outFile);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Nerfed.Builder;
|
||||||
|
|
||||||
|
public class RawFileImporter : IImporter
|
||||||
|
{
|
||||||
|
public void Import(string inFile, string outFile)
|
||||||
|
{
|
||||||
|
FileUtil.Copy(inFile, outFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Nerfed.Builder;
|
||||||
|
|
||||||
|
public class PackageArgs
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Nerfed.Builder;
|
||||||
|
|
||||||
|
public class Packager : IDisposable
|
||||||
|
{
|
||||||
|
public void Run(PackageArgs args)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Nerfed.Builder;
|
||||||
|
|
||||||
|
public static class PathUtil
|
||||||
|
{
|
||||||
|
public const string ImportedFileExtension = ".bin";
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace Nerfed.Editor.Components;
|
||||||
|
|
||||||
|
public readonly record struct SelectedInHierachy;
|
||||||
|
public readonly record struct ClickedInHierachy;
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using ImGuiNET;
|
||||||
|
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();
|
||||||
|
|
||||||
|
foreach (MoonTools.ECS.System system in Program.editorSystems)
|
||||||
|
{
|
||||||
|
using ProfilerScope scope = new(system.GetType().Name);
|
||||||
|
system.Update(Engine.Timestep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
</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.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>
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
using MoonTools.ECS;
|
||||||
|
using Nerfed.Editor.Systems;
|
||||||
|
using Nerfed.Runtime;
|
||||||
|
using Nerfed.Runtime.Components;
|
||||||
|
using Nerfed.Runtime.Scheduling;
|
||||||
|
using Nerfed.Runtime.Systems;
|
||||||
|
using Nerfed.Runtime.Systems.Synthetic;
|
||||||
|
using Nerfed.Runtime.Util;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Nerfed.Editor;
|
||||||
|
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
private static readonly World world = new World();
|
||||||
|
private static List<MoonTools.ECS.System> systems = new List<MoonTools.ECS.System>();
|
||||||
|
private static SystemScheduler scheduler;
|
||||||
|
public static List<MoonTools.ECS.System> editorSystems = new List<MoonTools.ECS.System>();
|
||||||
|
private static bool enableDummySchedulerHarness;
|
||||||
|
private static bool enableParallelSystemExecution;
|
||||||
|
|
||||||
|
private static void Main(string[] args)
|
||||||
|
{
|
||||||
|
enableDummySchedulerHarness = args.Contains("--dummy-scheduler-test", StringComparer.OrdinalIgnoreCase);
|
||||||
|
enableParallelSystemExecution = args.Contains("--parallel-systems", StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
scheduler = new SystemScheduler(new SystemSchedulerOptions
|
||||||
|
{
|
||||||
|
EnableParallelSystemExecution = enableParallelSystemExecution,
|
||||||
|
StrictDependencyValidation = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Engine.OnInitialize += HandleOnInitialize;
|
||||||
|
Engine.OnUpdate += HandleOnUpdate;
|
||||||
|
Engine.OnRender += HandleOnRender;
|
||||||
|
Engine.OnQuit += HandleOnQuit;
|
||||||
|
|
||||||
|
Engine.Run(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleOnInitialize()
|
||||||
|
{
|
||||||
|
//systems.Add(new ParentSystem(world));
|
||||||
|
LocalToWorldSystem localToWorldSystem = new LocalToWorldSystem(world);
|
||||||
|
systems.Add(localToWorldSystem);
|
||||||
|
scheduler.Register(localToWorldSystem, SystemSchedulePhase.Simulation);
|
||||||
|
|
||||||
|
if (enableDummySchedulerHarness)
|
||||||
|
{
|
||||||
|
DummyLongJobSharedState sharedState = new DummyLongJobSharedState();
|
||||||
|
|
||||||
|
// Producer starts long work, middle system burns time on main thread, consumer acts as a barrier.
|
||||||
|
DummyLongJobProducerSystem producer = new DummyLongJobProducerSystem(
|
||||||
|
world,
|
||||||
|
sharedState,
|
||||||
|
JobSystem.Default,
|
||||||
|
TimeSpan.FromMilliseconds(6),
|
||||||
|
DummyWorkloadMode.Compute,
|
||||||
|
JobDeadlineMode.FrameCritical);
|
||||||
|
systems.Add(producer);
|
||||||
|
scheduler.Register(producer, SystemSchedulePhase.Simulation);
|
||||||
|
|
||||||
|
DummyMainThreadWorkSystem mainThreadWork = new DummyMainThreadWorkSystem(
|
||||||
|
world,
|
||||||
|
TimeSpan.FromMilliseconds(2),
|
||||||
|
DummyWorkloadMode.Compute,
|
||||||
|
nameof(DummyMainThreadWorkSystem));
|
||||||
|
systems.Add(mainThreadWork);
|
||||||
|
scheduler.Register(mainThreadWork, SystemSchedulePhase.Simulation);
|
||||||
|
|
||||||
|
DummyLongJobConsumerSystem consumer = new DummyLongJobConsumerSystem(
|
||||||
|
world,
|
||||||
|
sharedState,
|
||||||
|
JobSystem.Default,
|
||||||
|
requireCompletion: true);
|
||||||
|
systems.Add(consumer);
|
||||||
|
scheduler.Register(consumer, SystemSchedulePhase.LateSimulation, nameof(DummyLongJobProducerSystem));
|
||||||
|
}
|
||||||
|
|
||||||
|
editorSystems.Add(new EditorProfilerWindow(world));
|
||||||
|
editorSystems.Add(new EditorHierarchyWindow(world));
|
||||||
|
#if DEBUG
|
||||||
|
editorSystems.Add(new EditorInspectorWindow(world));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Entity ent1 = world.CreateEntity("parent");
|
||||||
|
world.Set(ent1, new Root());
|
||||||
|
world.Set(ent1, new LocalTransform(new Vector3(1, 0, 0), Quaternion.Identity, Vector3.One));
|
||||||
|
|
||||||
|
Entity ent2 = world.CreateEntity("child");
|
||||||
|
world.Set(ent2, new LocalTransform(new Vector3(0, 1, 0), Quaternion.Identity, Vector3.One));
|
||||||
|
Transform.SetParent(world, ent2, ent1);
|
||||||
|
|
||||||
|
Entity ent3 = world.CreateEntity("entity3");
|
||||||
|
world.Set(ent3, new Root());
|
||||||
|
Transform.SetParent(world, ent3, ent2);
|
||||||
|
|
||||||
|
Entity ent4 = world.CreateEntity("entity4");
|
||||||
|
world.Set(ent4, new Root());
|
||||||
|
|
||||||
|
Entity ent5 = world.CreateBaseEntity("entity5");
|
||||||
|
|
||||||
|
for (int i = 0; i < 256; i++)
|
||||||
|
{
|
||||||
|
Entity newEnt = world.CreateBaseEntity();
|
||||||
|
world.Set(newEnt, new LocalTransform(new Vector3(i, i, i), Quaternion.Identity, Vector3.One));
|
||||||
|
|
||||||
|
Entity parent = newEnt;
|
||||||
|
for (int j = 0; j < 2; j++) {
|
||||||
|
Entity newChildEnt = world.CreateEntity();
|
||||||
|
world.Set(newChildEnt, new LocalTransform(new Vector3(i + j * i, i - j * i, j - i * i), Quaternion.Identity, Vector3.One));
|
||||||
|
Transform.SetParent(world, newChildEnt, parent);
|
||||||
|
parent = newChildEnt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open project.
|
||||||
|
// Setip EditorGui.
|
||||||
|
EditorGui.Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleOnUpdate()
|
||||||
|
{
|
||||||
|
using (new ProfilerScope("SystemScheduler.Execute"))
|
||||||
|
{
|
||||||
|
scheduler.Execute(Engine.Timestep);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (new ProfilerScope("EditorGui.Update"))
|
||||||
|
{
|
||||||
|
// Editor Update.
|
||||||
|
EditorGui.Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Catch UserCode Update.
|
||||||
|
|
||||||
|
using (new ProfilerScope("world.FinishUpdate"))
|
||||||
|
{
|
||||||
|
world.FinishUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleOnRender()
|
||||||
|
{
|
||||||
|
using (new ProfilerScope("EditorGui.Render"))
|
||||||
|
{
|
||||||
|
EditorGui.Render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleOnQuit()
|
||||||
|
{
|
||||||
|
EditorGui.Quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
using ImGuiNET;
|
||||||
|
using MoonTools.ECS;
|
||||||
|
using Nerfed.Editor.Components;
|
||||||
|
using Nerfed.Runtime;
|
||||||
|
using Nerfed.Runtime.Components;
|
||||||
|
using Nerfed.Runtime.Util;
|
||||||
|
|
||||||
|
namespace Nerfed.Editor.Systems
|
||||||
|
{
|
||||||
|
// Window that draws entities.
|
||||||
|
internal class EditorHierarchyWindow : MoonTools.ECS.System
|
||||||
|
{
|
||||||
|
private const ImGuiTreeNodeFlags baseFlags = ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.OpenOnDoubleClick | ImGuiTreeNodeFlags.SpanAvailWidth;
|
||||||
|
|
||||||
|
//private readonly Filter rootEntitiesWithTransformFilter;
|
||||||
|
//private readonly Filter rootEntitiesFilterBroken;
|
||||||
|
private readonly Filter rootEntitiesFilter;
|
||||||
|
|
||||||
|
private readonly EditorHierachySelectionSystem hierachySelectionSystem;
|
||||||
|
|
||||||
|
public EditorHierarchyWindow(World world) : base(world)
|
||||||
|
{
|
||||||
|
//rootEntitiesWithTransformFilter = FilterBuilder.Include<LocalTransform>().Exclude<Child>().Build();
|
||||||
|
|
||||||
|
// TODO: this doesn't work.
|
||||||
|
//rootEntitiesFilterBroken = FilterBuilder.Exclude<Child>().Build();
|
||||||
|
|
||||||
|
// Maybe the parent/child functions should add a root component when not being a child.
|
||||||
|
rootEntitiesFilter = FilterBuilder.Include<Root>().Build();
|
||||||
|
|
||||||
|
// Maybe instead of a root, if we need a component that is always on an entity and has some use we could create something like a VersionComponent which only hold an int.
|
||||||
|
// The version would update each time something changes on the entity.
|
||||||
|
// Or a EditorComponent, just a component that always gets added when in editor mode.
|
||||||
|
|
||||||
|
hierachySelectionSystem = new EditorHierachySelectionSystem(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(TimeSpan delta)
|
||||||
|
{
|
||||||
|
ImGui.Begin("Hierarchy");
|
||||||
|
|
||||||
|
ImGuiTreeNodeFlags flags = baseFlags;
|
||||||
|
flags |= ImGuiTreeNodeFlags.DefaultOpen;
|
||||||
|
|
||||||
|
if (ImGui.TreeNodeEx("World", flags))
|
||||||
|
{
|
||||||
|
if (ImGui.BeginDragDropTarget())
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
ImGuiPayloadPtr payload = ImGui.AcceptDragDropPayload($"{nameof(EditorHierarchyWindow)}");
|
||||||
|
if (payload.NativePtr != null)
|
||||||
|
{
|
||||||
|
Entity* data = (Entity*)payload.Data;
|
||||||
|
Entity child = data[0];
|
||||||
|
|
||||||
|
Log.Info($"Dropped {child.ID}");
|
||||||
|
|
||||||
|
Transform.RemoveParent(World, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.EndDragDropTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
//foreach (Entity entity in rootEntitiesWithTransformFilter.Entities)
|
||||||
|
//{
|
||||||
|
// DrawEntityAndChildren(entity);
|
||||||
|
//}
|
||||||
|
|
||||||
|
foreach (Entity entity in rootEntitiesFilter.Entities)
|
||||||
|
{
|
||||||
|
DrawEntityAndChildren(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TreePop();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.End();
|
||||||
|
|
||||||
|
hierachySelectionSystem.Update(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawEntityAndChildren(in Entity entity)
|
||||||
|
{
|
||||||
|
ImGuiTreeNodeFlags flags = baseFlags;
|
||||||
|
|
||||||
|
if (!World.HasInRelation<ChildParentRelation>(entity))
|
||||||
|
{
|
||||||
|
flags |= ImGuiTreeNodeFlags.Leaf;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (World.Has<SelectedInHierachy>(entity))
|
||||||
|
{
|
||||||
|
flags |= ImGuiTreeNodeFlags.Selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.TreeNodeEx($"{entity.ID} | {GetTag(entity)}", flags))
|
||||||
|
{
|
||||||
|
// TODO: fix selection, look at ImGui 1.91, https://github.com/ocornut/imgui/wiki/Multi-Select
|
||||||
|
// Selection.
|
||||||
|
if (ImGui.IsItemClicked() && !ImGui.IsItemToggledOpen())
|
||||||
|
{
|
||||||
|
World.Set(entity, new ClickedInHierachy());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag and drop.
|
||||||
|
if (ImGui.BeginDragDropSource())
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (Entity* payload = &entity)
|
||||||
|
{
|
||||||
|
ImGui.SetDragDropPayload($"{nameof(EditorHierarchyWindow)}", (IntPtr)payload, (uint)sizeof(Entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndDragDropSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.BeginDragDropTarget())
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
ImGuiPayloadPtr payload = ImGui.AcceptDragDropPayload($"{nameof(EditorHierarchyWindow)}");
|
||||||
|
if (payload.NativePtr != null)
|
||||||
|
{
|
||||||
|
Entity ent = *(Entity*)payload.Data;
|
||||||
|
|
||||||
|
Log.Info($"Dropped {ent.ID}");
|
||||||
|
|
||||||
|
Transform.SetParent(World, ent, entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.EndDragDropTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw children.
|
||||||
|
ReverseSpanEnumerator<Entity> childEntities = World.InRelations<ChildParentRelation>(entity);
|
||||||
|
foreach (Entity childEntity in childEntities)
|
||||||
|
{
|
||||||
|
DrawEntityAndChildren(childEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TreePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System for handling the selected entities in the hierachy.
|
||||||
|
private class EditorHierachySelectionSystem : MoonTools.ECS.System
|
||||||
|
{
|
||||||
|
private readonly Filter selectedEntities;
|
||||||
|
private readonly Filter clickedEntities;
|
||||||
|
|
||||||
|
public EditorHierachySelectionSystem(World world) : base(world)
|
||||||
|
{
|
||||||
|
selectedEntities = FilterBuilder.Include<SelectedInHierachy>().Build();
|
||||||
|
clickedEntities = FilterBuilder.Include<ClickedInHierachy>().Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(TimeSpan delta)
|
||||||
|
{
|
||||||
|
ImGuiIOPtr io = ImGui.GetIO();
|
||||||
|
|
||||||
|
if (!clickedEntities.Empty && !io.KeyCtrl)
|
||||||
|
{
|
||||||
|
foreach (Entity entity in selectedEntities.Entities)
|
||||||
|
{
|
||||||
|
Remove<SelectedInHierachy>(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Entity entity in clickedEntities.Entities)
|
||||||
|
{
|
||||||
|
// Unselect.
|
||||||
|
if (Has<SelectedInHierachy>(entity))
|
||||||
|
{
|
||||||
|
Remove<SelectedInHierachy>(entity);
|
||||||
|
}
|
||||||
|
// Select.
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Set(entity, new SelectedInHierachy());
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove<ClickedInHierachy>(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using ImGuiNET;
|
||||||
|
using MoonTools.ECS;
|
||||||
|
using Nerfed.Editor.Components;
|
||||||
|
using Nerfed.Runtime.Serialization;
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
namespace Nerfed.Editor.Systems
|
||||||
|
{
|
||||||
|
// Window that draws entities.
|
||||||
|
internal class EditorInspectorWindow : MoonTools.ECS.DebugSystem
|
||||||
|
{
|
||||||
|
private readonly Filter selectedEntityFilter;
|
||||||
|
|
||||||
|
public EditorInspectorWindow(World world) : base(world)
|
||||||
|
{
|
||||||
|
selectedEntityFilter = FilterBuilder.Include<SelectedInHierachy>().Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(TimeSpan delta)
|
||||||
|
{
|
||||||
|
ImGui.Begin("Inspector");
|
||||||
|
|
||||||
|
foreach (Entity entity in selectedEntityFilter.Entities)
|
||||||
|
{
|
||||||
|
DrawEntityComponents(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.End();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawEntityComponents(Entity entity)
|
||||||
|
{
|
||||||
|
World.ComponentTypeEnumerator componentTypes = World.Debug_GetAllComponentTypes(entity);
|
||||||
|
|
||||||
|
// Add button of all types that we can add. Also filter out types we already have.
|
||||||
|
List<Type> componentTypesToAdd = ComponentHelper.AddComponentByType.Keys.ToList();
|
||||||
|
foreach (Type componentType in componentTypes)
|
||||||
|
{
|
||||||
|
componentTypesToAdd.Remove(componentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const string popupId = "AddComponentPopup";
|
||||||
|
if (ImGui.Button("Add Component"))
|
||||||
|
{
|
||||||
|
ImGui.OpenPopup(popupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.BeginPopup(popupId))
|
||||||
|
{
|
||||||
|
foreach (Type componentType in componentTypesToAdd)
|
||||||
|
{
|
||||||
|
if (ImGui.Selectable(componentType.Name))
|
||||||
|
{
|
||||||
|
if (ComponentHelper.AddComponentByType.TryGetValue(componentType, out Action<World, Entity> componentSetter))
|
||||||
|
{
|
||||||
|
componentSetter.Invoke(World, entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(16, 16));
|
||||||
|
|
||||||
|
ImGui.Text("ComponentInspectorByType");
|
||||||
|
foreach (Type componentType in componentTypes)
|
||||||
|
{
|
||||||
|
if (ComponentHelper.ComponentInspectorByType.TryGetValue(componentType, out Action<World, Entity> componentInspector))
|
||||||
|
{
|
||||||
|
componentInspector(World, entity);
|
||||||
|
}
|
||||||
|
else if (ComponentHelper.GetComponentByType.TryGetValue(componentType, out Func<World, Entity, ValueType> componentGetter))
|
||||||
|
{
|
||||||
|
ValueType component = componentGetter.Invoke(World, entity);
|
||||||
|
ImGui.Text(component.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.Text(componentType.Name);
|
||||||
|
}
|
||||||
|
ImGui.Separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(16, 16));
|
||||||
|
|
||||||
|
// ImGui.Text("Reflection");
|
||||||
|
// foreach (Type component in componentTypes)
|
||||||
|
// {
|
||||||
|
// System.Reflection.MethodInfo getMethodInfo = typeof(World).GetMethod("Get");
|
||||||
|
// System.Reflection.MethodInfo getComponentMethod = getMethodInfo.MakeGenericMethod(component);
|
||||||
|
// object result = getComponentMethod.Invoke(World, [entity]);
|
||||||
|
//
|
||||||
|
// // process here
|
||||||
|
// ImGui.Text(result.ToString());
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
using ImGuiNET;
|
||||||
|
using MoonTools.ECS;
|
||||||
|
using Nerfed.Runtime;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Nerfed.Editor.Systems
|
||||||
|
{
|
||||||
|
internal class EditorProfilerWindow : MoonTools.ECS.System
|
||||||
|
{
|
||||||
|
const ImGuiTableFlags tableFlags = ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.ScrollY | ImGuiTableFlags.ScrollX;
|
||||||
|
const ImGuiTreeNodeFlags treeNodeFlags = ImGuiTreeNodeFlags.SpanAllColumns;
|
||||||
|
const ImGuiTreeNodeFlags treeNodeLeafFlags = ImGuiTreeNodeFlags.SpanAllColumns | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen;
|
||||||
|
|
||||||
|
private int selectedFrame = 0;
|
||||||
|
private int previousSelectedFrame = -1;
|
||||||
|
private IOrderedEnumerable<KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms, long allocBytes, long selfAllocBytes)>> orderedCombinedData = null;
|
||||||
|
private IOrderedEnumerable<KeyValuePair<int, Profiler.RollingThreadMetrics>> orderedThreadRollingData = null;
|
||||||
|
private readonly ProfilerVisualizer.TimelineState timelineState = new ProfilerVisualizer.TimelineState();
|
||||||
|
private readonly List<Profiler.Frame> frameSnapshot = new List<Profiler.Frame>(256);
|
||||||
|
|
||||||
|
public EditorProfilerWindow(World world) : base(world)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(TimeSpan delta)
|
||||||
|
{
|
||||||
|
if (Profiler.Frames.Count <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Profiler.CopyFramesTo(frameSnapshot) <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFrame = Math.Clamp(selectedFrame, 0, frameSnapshot.Count - 1);
|
||||||
|
timelineState.SelectedFrameIndex = Math.Clamp(timelineState.SelectedFrameIndex, -1, frameSnapshot.Count - 1);
|
||||||
|
timelineState.VisibleFrameCount = Math.Clamp(timelineState.VisibleFrameCount, 1, frameSnapshot.Count);
|
||||||
|
|
||||||
|
ImGui.Begin("Profiler");
|
||||||
|
|
||||||
|
ImGui.BeginChild("Toolbar", new Vector2(0, 0), ImGuiChildFlags.AutoResizeY);
|
||||||
|
if (ImGui.RadioButton("Recording", Profiler.IsRecording))
|
||||||
|
{
|
||||||
|
Profiler.SetActive(!Profiler.IsRecording);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.Text("Follow");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.Checkbox("##follow-timeline", ref timelineState.FollowLatest);
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
int visibleFrameCount = timelineState.VisibleFrameCount;
|
||||||
|
ImGui.SetNextItemWidth(130f);
|
||||||
|
if (ImGui.SliderInt("Window", ref visibleFrameCount, 1, frameSnapshot.Count))
|
||||||
|
{
|
||||||
|
timelineState.VisibleFrameCount = visibleFrameCount;
|
||||||
|
timelineState.FollowLatest = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Reset Zoom"))
|
||||||
|
{
|
||||||
|
timelineState.Zoom = 1f;
|
||||||
|
timelineState.PanTicks = 0d;
|
||||||
|
timelineState.FollowLatest = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
int mode = (int)Profiler.Mode;
|
||||||
|
ImGui.SetNextItemWidth(130f);
|
||||||
|
if (ImGui.Combo("Mode", ref mode, "Instrumented\0Sampled\0"))
|
||||||
|
{
|
||||||
|
Profiler.Mode = (Profiler.CaptureMode)mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
int stride = Profiler.SamplingStride;
|
||||||
|
ImGui.SetNextItemWidth(90f);
|
||||||
|
if (ImGui.SliderInt("Stride", ref stride, 1, 64))
|
||||||
|
{
|
||||||
|
Profiler.SamplingStride = stride;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Profiler.IsRecording)
|
||||||
|
{
|
||||||
|
// Select last frame when recording to see latest frame data.
|
||||||
|
selectedFrame = frameSnapshot.Count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sliderFrame = selectedFrame;
|
||||||
|
if (ImGui.SliderInt("Frame", ref sliderFrame, 0, frameSnapshot.Count - 1))
|
||||||
|
{
|
||||||
|
selectedFrame = sliderFrame;
|
||||||
|
timelineState.SelectedFrameIndex = selectedFrame;
|
||||||
|
timelineState.FollowLatest = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Profiler.Frame frame = frameSnapshot[selectedFrame];
|
||||||
|
double ms = frame.ElapsedMilliseconds();
|
||||||
|
double s = 1000;
|
||||||
|
ImGui.Text($"Frame: {frame.FrameCount} ({ms:0.000} ms | {(s / ms):0} fps)");
|
||||||
|
ImGui.Text($"Budget: {frame.BudgetMilliseconds:0.00} ms ({(frame.OverBudget ? "over" : "within")})");
|
||||||
|
ImGui.Text($"Thread Budget: {Profiler.ThreadBudgetMilliseconds:0.00} ms | Capture: {Profiler.Mode}");
|
||||||
|
ImGui.Text($"Alloc: {frame.AllocatedBytesDelta / 1024d:0.0} KB | GC: G0 {frame.Gen0CollectionsDelta}, G1 {frame.Gen1CollectionsDelta}, G2 {frame.Gen2CollectionsDelta}");
|
||||||
|
ImGui.EndChild();
|
||||||
|
|
||||||
|
ProfilerVisualizer.TimelineRenderResult timelineResult = DrawFlameGraph(frameSnapshot, timelineState);
|
||||||
|
if (timelineResult.SelectionChanged)
|
||||||
|
{
|
||||||
|
selectedFrame = timelineResult.SelectedFrameIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFrame = Math.Clamp(selectedFrame, 0, frameSnapshot.Count - 1);
|
||||||
|
frame = frameSnapshot[selectedFrame];
|
||||||
|
|
||||||
|
if (previousSelectedFrame != selectedFrame)
|
||||||
|
{
|
||||||
|
previousSelectedFrame = selectedFrame;
|
||||||
|
orderedCombinedData = CalculateCombinedData(frame);
|
||||||
|
orderedThreadRollingData = CalculateThreadRollingData();
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawThreadRolling(orderedThreadRollingData);
|
||||||
|
|
||||||
|
DrawHierachy(frame);
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
DrawCombined(orderedCombinedData);
|
||||||
|
|
||||||
|
ImGui.End();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawHierachy(Profiler.Frame frame)
|
||||||
|
{
|
||||||
|
if(frame == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.BeginChild("Hierachy", new Vector2(150, 0), ImGuiChildFlags.ResizeX);
|
||||||
|
|
||||||
|
if (ImGui.BeginTable("ProfilerData", 8, tableFlags, new Vector2(0, 0)))
|
||||||
|
{
|
||||||
|
ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthStretch, 0.30f, 0);
|
||||||
|
ImGui.TableSetupColumn("category", ImGuiTableColumnFlags.WidthStretch, 0.12f, 1);
|
||||||
|
ImGui.TableSetupColumn("tags", ImGuiTableColumnFlags.WidthStretch, 0.08f, 2);
|
||||||
|
ImGui.TableSetupColumn("thread", ImGuiTableColumnFlags.WidthStretch, 0.08f, 3);
|
||||||
|
ImGui.TableSetupColumn("ms", ImGuiTableColumnFlags.WidthStretch, 0.10f, 4);
|
||||||
|
ImGui.TableSetupColumn("self", ImGuiTableColumnFlags.WidthStretch, 0.10f, 5);
|
||||||
|
ImGui.TableSetupColumn("alloc(B)", ImGuiTableColumnFlags.WidthStretch, 0.11f, 6);
|
||||||
|
ImGui.TableSetupColumn("self alloc", ImGuiTableColumnFlags.WidthStretch, 0.11f, 7);
|
||||||
|
ImGui.TableSetupScrollFreeze(0, 1); // Make row always visible
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
|
foreach (Profiler.ScopeNode node in frame.RootNodes)
|
||||||
|
{
|
||||||
|
DrawHierachyNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawHierachyNode(Profiler.ScopeNode node)
|
||||||
|
{
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
|
||||||
|
bool isOpen = false;
|
||||||
|
bool isLeaf = node.Children.Count == 0;
|
||||||
|
|
||||||
|
if (isLeaf) {
|
||||||
|
ImGui.TreeNodeEx(node.Label, treeNodeLeafFlags);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
isOpen = ImGui.TreeNodeEx(node.Label, treeNodeFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{node.Category}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"0x{node.TagMask:X}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{node.ManagedThreadId}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{node.ElapsedMilliseconds():0.000}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{node.SelfMilliseconds():0.000}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
if (node.ProfilerSetupBytes > 0)
|
||||||
|
{
|
||||||
|
ImGui.Text($"{node.AllocatedBytes} !");
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"{node.ProfilerSetupBytes} B is profiler warmup overhead");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.Text($"{node.AllocatedBytes}");
|
||||||
|
}
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{node.SelfAllocatedBytes()}");
|
||||||
|
|
||||||
|
if (isOpen)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < node.Children.Count; i++)
|
||||||
|
{
|
||||||
|
DrawHierachyNode(node.Children[i]);
|
||||||
|
}
|
||||||
|
ImGui.TreePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawCombined(in IOrderedEnumerable<KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms, long allocBytes, long selfAllocBytes)>> orderedCombinedData)
|
||||||
|
{
|
||||||
|
if(orderedCombinedData == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.BeginChild("Combined", new Vector2(0, 0));
|
||||||
|
|
||||||
|
if (ImGui.BeginTable("ProfilerCombinedData", 8, tableFlags, new Vector2(0, 0)))
|
||||||
|
{
|
||||||
|
ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthStretch, 0.32f, 0);
|
||||||
|
ImGui.TableSetupColumn("ms", ImGuiTableColumnFlags.WidthStretch, 0.11f, 1);
|
||||||
|
ImGui.TableSetupColumn("self", ImGuiTableColumnFlags.WidthStretch, 0.11f, 2);
|
||||||
|
ImGui.TableSetupColumn("calls", ImGuiTableColumnFlags.WidthStretch, 0.08f, 3);
|
||||||
|
ImGui.TableSetupColumn("avg", ImGuiTableColumnFlags.WidthStretch, 0.09f, 4);
|
||||||
|
ImGui.TableSetupColumn("p95", ImGuiTableColumnFlags.WidthStretch, 0.09f, 5);
|
||||||
|
ImGui.TableSetupColumn("alloc(B)", ImGuiTableColumnFlags.WidthStretch, 0.10f, 6);
|
||||||
|
ImGui.TableSetupColumn("self alloc", ImGuiTableColumnFlags.WidthStretch, 0.10f, 7);
|
||||||
|
ImGui.TableSetupScrollFreeze(0, 1); // Make row always visible
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
|
foreach (KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms, long allocBytes, long selfAllocBytes)> combinedData in orderedCombinedData)
|
||||||
|
{
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{combinedData.Key}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{combinedData.Value.ms:0.000}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{combinedData.Value.selfMs:0.000}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{combinedData.Value.calls}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{combinedData.Value.avgMs:0.000}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{combinedData.Value.p95Ms:0.000}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{combinedData.Value.allocBytes}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{combinedData.Value.selfAllocBytes}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IOrderedEnumerable<KeyValuePair<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms, long allocBytes, long selfAllocBytes)>> CalculateCombinedData(Profiler.Frame frame)
|
||||||
|
{
|
||||||
|
IReadOnlyDictionary<string, Profiler.RollingLabelMetrics> rollingData = Profiler.GetRollingLabelMetricsSnapshot();
|
||||||
|
Dictionary<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms, long allocBytes, long selfAllocBytes)> combinedRecordData = new Dictionary<string, (double ms, double selfMs, uint calls, double avgMs, double p95Ms, long allocBytes, long selfAllocBytes)>(128);
|
||||||
|
foreach (KeyValuePair<string, Profiler.LabelMetrics> metric in frame.LabelMetrics)
|
||||||
|
{
|
||||||
|
Profiler.RollingLabelMetrics rolling = default;
|
||||||
|
if (rollingData.TryGetValue(metric.Key, out Profiler.RollingLabelMetrics found))
|
||||||
|
{
|
||||||
|
rolling = found;
|
||||||
|
}
|
||||||
|
|
||||||
|
combinedRecordData[metric.Key] = (metric.Value.InclusiveMs, metric.Value.SelfMs, metric.Value.Calls, rolling.AverageMs, rolling.P95Ms, metric.Value.AllocatedBytes, metric.Value.SelfAllocatedBytes);
|
||||||
|
}
|
||||||
|
return combinedRecordData.OrderByDescending(x => x.Value.ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IOrderedEnumerable<KeyValuePair<int, Profiler.RollingThreadMetrics>> CalculateThreadRollingData()
|
||||||
|
{
|
||||||
|
IReadOnlyDictionary<int, Profiler.RollingThreadMetrics> rollingData = Profiler.GetRollingThreadMetricsSnapshot();
|
||||||
|
return rollingData.OrderByDescending(x => x.Value.P95Ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawThreadRolling(in IOrderedEnumerable<KeyValuePair<int, Profiler.RollingThreadMetrics>> orderedThreadRollingData)
|
||||||
|
{
|
||||||
|
if (orderedThreadRollingData == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.BeginChild("ThreadRolling", new Vector2(0, 140), ImGuiChildFlags.Border);
|
||||||
|
if (ImGui.BeginTable("ProfilerThreadRollingData", 6, tableFlags, new Vector2(0, 0)))
|
||||||
|
{
|
||||||
|
ImGui.TableSetupColumn("thread", ImGuiTableColumnFlags.WidthStretch, 0.15f, 0);
|
||||||
|
ImGui.TableSetupColumn("avg", ImGuiTableColumnFlags.WidthStretch, 0.20f, 1);
|
||||||
|
ImGui.TableSetupColumn("p95", ImGuiTableColumnFlags.WidthStretch, 0.20f, 2);
|
||||||
|
ImGui.TableSetupColumn("max", ImGuiTableColumnFlags.WidthStretch, 0.20f, 3);
|
||||||
|
ImGui.TableSetupColumn("samples", ImGuiTableColumnFlags.WidthStretch, 0.15f, 4);
|
||||||
|
ImGui.TableSetupColumn("misses", ImGuiTableColumnFlags.WidthStretch, 0.15f, 5);
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
|
foreach (KeyValuePair<int, Profiler.RollingThreadMetrics> metric in orderedThreadRollingData)
|
||||||
|
{
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"T{metric.Key}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{metric.Value.AverageMs:0.000}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{metric.Value.P95Ms:0.000}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{metric.Value.MaxMs:0.000}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{metric.Value.Samples}");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{metric.Value.BudgetMisses}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProfilerVisualizer.TimelineRenderResult DrawFlameGraph(IReadOnlyList<Profiler.Frame> frames, ProfilerVisualizer.TimelineState timelineState)
|
||||||
|
{
|
||||||
|
if (frames == null || frames.Count == 0)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProfilerVisualizer.RenderTimeline(frames, timelineState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Components
|
||||||
|
{
|
||||||
|
public readonly record struct LocalToWorld(Matrix4x4 localToWorldMatrix);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Components
|
||||||
|
{
|
||||||
|
public readonly record struct LocalTransform(Vector3 position, Quaternion rotation, Vector3 scale)
|
||||||
|
{
|
||||||
|
public static readonly LocalTransform Identity = new(Vector3.Zero, Quaternion.Identity, Vector3.One);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using MoonTools.ECS;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Components
|
||||||
|
{
|
||||||
|
public readonly record struct Root;
|
||||||
|
//public readonly record struct Parent;
|
||||||
|
//public readonly record struct PreviousParent;
|
||||||
|
public readonly record struct Child;
|
||||||
|
// Describes a relation from the child to the parent.
|
||||||
|
public readonly record struct ChildParentRelation;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace Nerfed.Runtime.Components
|
||||||
|
{
|
||||||
|
public readonly record struct Test();
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?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)))">
|
|
||||||
<Content Include="..\libs\x64\**\*.*" >
|
|
||||||
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">
|
|
||||||
<Content Include="..\libs\lib64\**\*.*" >
|
|
||||||
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))">
|
|
||||||
<Content Include="..\libs\osx\**\*.*" >
|
|
||||||
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using Nerfed.Runtime.Audio;
|
using Nerfed.Runtime.Audio;
|
||||||
using Nerfed.Runtime.Graphics;
|
using Nerfed.Runtime.Graphics;
|
||||||
using SDL2;
|
using SDL2;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace Nerfed.Runtime;
|
namespace Nerfed.Runtime;
|
||||||
|
|
||||||
public static class Engine
|
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 TimeSpan MaxDeltaTime { get; set; } = TimeSpan.FromMilliseconds(100);
|
||||||
public static bool VSync { get; set; }
|
public static bool VSync { get; set; }
|
||||||
|
|
||||||
@@ -39,7 +44,7 @@ public static class Engine
|
|||||||
private const string WindowTitle = "Nerfed";
|
private const string WindowTitle = "Nerfed";
|
||||||
//..
|
//..
|
||||||
|
|
||||||
internal static void Run(string[] args)
|
public static void Run(string[] args)
|
||||||
{
|
{
|
||||||
Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / TargetTimestep);
|
Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / TargetTimestep);
|
||||||
gameTimer = Stopwatch.StartNew();
|
gameTimer = Stopwatch.StartNew();
|
||||||
@@ -54,8 +59,9 @@ public static class Engine
|
|||||||
{
|
{
|
||||||
throw new Exception("Failed to init SDL");
|
throw new Exception("Failed to init SDL");
|
||||||
}
|
}
|
||||||
|
|
||||||
GraphicsDevice = new GraphicsDevice(BackendFlags.All);
|
GraphicsDevice = new GraphicsDevice(BackendFlags.All);
|
||||||
|
GraphicsDevice.LoadDefaultPipelines();
|
||||||
|
|
||||||
MainWindow = new Window(GraphicsDevice, new WindowCreateInfo(WindowTitle, WindowWidth, WindowHeight, ScreenMode.Windowed));
|
MainWindow = new Window(GraphicsDevice, new WindowCreateInfo(WindowTitle, WindowWidth, WindowHeight, ScreenMode.Windowed));
|
||||||
if (!GraphicsDevice.ClaimWindow(MainWindow, SwapchainComposition.SDR, VSync ? PresentMode.VSync : PresentMode.Mailbox))
|
if (!GraphicsDevice.ClaimWindow(MainWindow, SwapchainComposition.SDR, VSync ? PresentMode.VSync : PresentMode.Mailbox))
|
||||||
@@ -65,15 +71,22 @@ public static class Engine
|
|||||||
|
|
||||||
AudioDevice = new AudioDevice();
|
AudioDevice = new AudioDevice();
|
||||||
|
|
||||||
|
JobSystem.Default.Initialize();
|
||||||
|
|
||||||
|
OnInitialize?.Invoke();
|
||||||
|
|
||||||
while (!quit)
|
while (!quit)
|
||||||
{
|
{
|
||||||
Tick();
|
Tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OnQuit?.Invoke();
|
||||||
|
|
||||||
GraphicsDevice.UnclaimWindow(MainWindow);
|
GraphicsDevice.UnclaimWindow(MainWindow);
|
||||||
MainWindow.Dispose();
|
MainWindow.Dispose();
|
||||||
GraphicsDevice.Dispose();
|
GraphicsDevice.Dispose();
|
||||||
AudioDevice.Dispose();
|
AudioDevice.Dispose();
|
||||||
|
JobSystem.Default.Shutdown();
|
||||||
SDL.SDL_Quit();
|
SDL.SDL_Quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,10 +114,14 @@ public static class Engine
|
|||||||
|
|
||||||
private static void Tick()
|
private static void Tick()
|
||||||
{
|
{
|
||||||
|
Profiler.BeginFrame();
|
||||||
|
|
||||||
AdvanceElapsedTime();
|
AdvanceElapsedTime();
|
||||||
|
|
||||||
if (framerateCapped)
|
if (framerateCapped)
|
||||||
{
|
{
|
||||||
|
Profiler.BeginSample("framerateCapped");
|
||||||
|
|
||||||
/* We want to wait until the framerate cap,
|
/* We want to wait until the framerate cap,
|
||||||
* but we don't want to oversleep. Requesting repeated 1ms sleeps and
|
* 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
|
* seeing how long we actually slept for lets us estimate the worst case
|
||||||
@@ -127,6 +144,8 @@ public static class Engine
|
|||||||
Thread.SpinWait(1);
|
Thread.SpinWait(1);
|
||||||
AdvanceElapsedTime();
|
AdvanceElapsedTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Profiler.EndSample();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not let any step take longer than our maximum.
|
// Do not let any step take longer than our maximum.
|
||||||
@@ -139,6 +158,7 @@ public static class Engine
|
|||||||
{
|
{
|
||||||
while (accumulatedUpdateTime >= Timestep)
|
while (accumulatedUpdateTime >= Timestep)
|
||||||
{
|
{
|
||||||
|
Profiler.BeginSample("Update");
|
||||||
Keyboard.Update();
|
Keyboard.Update();
|
||||||
Mouse.Update();
|
Mouse.Update();
|
||||||
GamePad.Update();
|
GamePad.Update();
|
||||||
@@ -146,17 +166,26 @@ public static class Engine
|
|||||||
ProcessSDLEvents();
|
ProcessSDLEvents();
|
||||||
|
|
||||||
// Tick game here...
|
// Tick game here...
|
||||||
|
Profiler.BeginSample("OnUpdate");
|
||||||
|
OnUpdate?.Invoke();
|
||||||
|
Profiler.EndSample();
|
||||||
|
|
||||||
AudioDevice.WakeThread();
|
AudioDevice.WakeThread();
|
||||||
accumulatedUpdateTime -= Timestep;
|
accumulatedUpdateTime -= Timestep;
|
||||||
|
Profiler.EndSample();
|
||||||
}
|
}
|
||||||
|
|
||||||
double alpha = accumulatedUpdateTime / Timestep;
|
double alpha = accumulatedUpdateTime / Timestep;
|
||||||
|
|
||||||
// Render here..
|
// Render here..
|
||||||
|
Profiler.BeginSample("OnRender");
|
||||||
|
OnRender?.Invoke();
|
||||||
|
Profiler.EndSample();
|
||||||
|
|
||||||
accumulatedDrawTime -= framerateCapTimeSpan;
|
accumulatedDrawTime -= framerateCapTimeSpan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Profiler.EndFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TimeSpan AdvanceElapsedTime()
|
private static TimeSpan AdvanceElapsedTime()
|
||||||
|
|||||||
@@ -1,716 +0,0 @@
|
|||||||
namespace Nerfed.Runtime.Graphics;
|
|
||||||
|
|
||||||
internal class EmbeddedShadersSpirV : IEmbeddedShaders
|
|
||||||
{
|
|
||||||
public ShaderFormat ShaderFormat => ShaderFormat.SPIRV;
|
|
||||||
|
|
||||||
public byte[] FullscreenVert { get; } =
|
|
||||||
[
|
|
||||||
0x3, 0x2, 0x23, 0x7, 0x0, 0x0, 0x1, 0x0, 0xB, 0x0,
|
|
||||||
0x8, 0x0, 0x2E, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x11, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0xB, 0x0,
|
|
||||||
0x6, 0x0, 0x1, 0x0, 0x0, 0x0, 0x47, 0x4C, 0x53, 0x4C,
|
|
||||||
0x2E, 0x73, 0x74, 0x64, 0x2E, 0x34, 0x35, 0x30, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0xE, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0xF, 0x0, 0x8, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0xC, 0x0,
|
|
||||||
0x0, 0x0, 0x1D, 0x0, 0x0, 0x0, 0x3, 0x0, 0x3, 0x0,
|
|
||||||
0x2, 0x0, 0x0, 0x0, 0xC2, 0x1, 0x0, 0x0, 0x5, 0x0,
|
|
||||||
0x4, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x5, 0x0, 0x9, 0x0,
|
|
||||||
0x0, 0x0, 0x6F, 0x75, 0x74, 0x54, 0x65, 0x78, 0x43, 0x6F,
|
|
||||||
0x6F, 0x72, 0x64, 0x0, 0x5, 0x0, 0x6, 0x0, 0xC, 0x0,
|
|
||||||
0x0, 0x0, 0x67, 0x6C, 0x5F, 0x56, 0x65, 0x72, 0x74, 0x65,
|
|
||||||
0x78, 0x49, 0x6E, 0x64, 0x65, 0x78, 0x0, 0x0, 0x5, 0x0,
|
|
||||||
0x6, 0x0, 0x1B, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x50,
|
|
||||||
0x65, 0x72, 0x56, 0x65, 0x72, 0x74, 0x65, 0x78, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x6, 0x0, 0x6, 0x0, 0x1B, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x50, 0x6F, 0x73,
|
|
||||||
0x69, 0x74, 0x69, 0x6F, 0x6E, 0x0, 0x6, 0x0, 0x7, 0x0,
|
|
||||||
0x1B, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x67, 0x6C,
|
|
||||||
0x5F, 0x50, 0x6F, 0x69, 0x6E, 0x74, 0x53, 0x69, 0x7A, 0x65,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x7, 0x0, 0x1B, 0x0,
|
|
||||||
0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x43,
|
|
||||||
0x6C, 0x69, 0x70, 0x44, 0x69, 0x73, 0x74, 0x61, 0x6E, 0x63,
|
|
||||||
0x65, 0x0, 0x6, 0x0, 0x7, 0x0, 0x1B, 0x0, 0x0, 0x0,
|
|
||||||
0x3, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x43, 0x75, 0x6C,
|
|
||||||
0x6C, 0x44, 0x69, 0x73, 0x74, 0x61, 0x6E, 0x63, 0x65, 0x0,
|
|
||||||
0x5, 0x0, 0x3, 0x0, 0x1D, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x9, 0x0, 0x0, 0x0,
|
|
||||||
0x1E, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x47, 0x0,
|
|
||||||
0x4, 0x0, 0xC, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x2A, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0x1B, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0x1B, 0x0,
|
|
||||||
0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0x1B, 0x0,
|
|
||||||
0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x3, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0x1B, 0x0,
|
|
||||||
0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x4, 0x0, 0x0, 0x0, 0x47, 0x0, 0x3, 0x0, 0x1B, 0x0,
|
|
||||||
0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x13, 0x0, 0x2, 0x0,
|
|
||||||
0x2, 0x0, 0x0, 0x0, 0x21, 0x0, 0x3, 0x0, 0x3, 0x0,
|
|
||||||
0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x16, 0x0, 0x3, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x17, 0x0,
|
|
||||||
0x4, 0x0, 0x7, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x2, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x8, 0x0,
|
|
||||||
0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0,
|
|
||||||
0x3B, 0x0, 0x4, 0x0, 0x8, 0x0, 0x0, 0x0, 0x9, 0x0,
|
|
||||||
0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x15, 0x0, 0x4, 0x0,
|
|
||||||
0xA, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x1, 0x0,
|
|
||||||
0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x3B, 0x0,
|
|
||||||
0x4, 0x0, 0xB, 0x0, 0x0, 0x0, 0xC, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0xA, 0x0,
|
|
||||||
0x0, 0x0, 0xE, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0,
|
|
||||||
0x2B, 0x0, 0x4, 0x0, 0xA, 0x0, 0x0, 0x0, 0x10, 0x0,
|
|
||||||
0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0,
|
|
||||||
0x17, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x4, 0x0,
|
|
||||||
0x0, 0x0, 0x15, 0x0, 0x4, 0x0, 0x18, 0x0, 0x0, 0x0,
|
|
||||||
0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2B, 0x0,
|
|
||||||
0x4, 0x0, 0x18, 0x0, 0x0, 0x0, 0x19, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x1C, 0x0, 0x4, 0x0, 0x1A, 0x0,
|
|
||||||
0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x19, 0x0, 0x0, 0x0,
|
|
||||||
0x1E, 0x0, 0x6, 0x0, 0x1B, 0x0, 0x0, 0x0, 0x17, 0x0,
|
|
||||||
0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0,
|
|
||||||
0x1A, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x1C, 0x0,
|
|
||||||
0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1B, 0x0, 0x0, 0x0,
|
|
||||||
0x3B, 0x0, 0x4, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x1D, 0x0,
|
|
||||||
0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0,
|
|
||||||
0xA, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x2B, 0x0,
|
|
||||||
0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0xC0, 0x2C, 0x0, 0x5, 0x0, 0x7, 0x0,
|
|
||||||
0x0, 0x0, 0x22, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0,
|
|
||||||
0x21, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x24, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xBF,
|
|
||||||
0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x25, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x80, 0x3F, 0x2C, 0x0, 0x5, 0x0,
|
|
||||||
0x7, 0x0, 0x0, 0x0, 0x26, 0x0, 0x0, 0x0, 0x24, 0x0,
|
|
||||||
0x0, 0x0, 0x25, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x28, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x2C, 0x0, 0x0, 0x0,
|
|
||||||
0x3, 0x0, 0x0, 0x0, 0x17, 0x0, 0x0, 0x0, 0x36, 0x0,
|
|
||||||
0x5, 0x0, 0x2, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0xF8, 0x0,
|
|
||||||
0x2, 0x0, 0x5, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0,
|
|
||||||
0xA, 0x0, 0x0, 0x0, 0xD, 0x0, 0x0, 0x0, 0xC, 0x0,
|
|
||||||
0x0, 0x0, 0xC4, 0x0, 0x5, 0x0, 0xA, 0x0, 0x0, 0x0,
|
|
||||||
0xF, 0x0, 0x0, 0x0, 0xD, 0x0, 0x0, 0x0, 0xE, 0x0,
|
|
||||||
0x0, 0x0, 0xC7, 0x0, 0x5, 0x0, 0xA, 0x0, 0x0, 0x0,
|
|
||||||
0x11, 0x0, 0x0, 0x0, 0xF, 0x0, 0x0, 0x0, 0x10, 0x0,
|
|
||||||
0x0, 0x0, 0x6F, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x12, 0x0, 0x0, 0x0, 0x11, 0x0, 0x0, 0x0, 0x3D, 0x0,
|
|
||||||
0x4, 0x0, 0xA, 0x0, 0x0, 0x0, 0x13, 0x0, 0x0, 0x0,
|
|
||||||
0xC, 0x0, 0x0, 0x0, 0xC7, 0x0, 0x5, 0x0, 0xA, 0x0,
|
|
||||||
0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x13, 0x0, 0x0, 0x0,
|
|
||||||
0x10, 0x0, 0x0, 0x0, 0x6F, 0x0, 0x4, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x15, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0,
|
|
||||||
0x50, 0x0, 0x5, 0x0, 0x7, 0x0, 0x0, 0x0, 0x16, 0x0,
|
|
||||||
0x0, 0x0, 0x12, 0x0, 0x0, 0x0, 0x15, 0x0, 0x0, 0x0,
|
|
||||||
0x3E, 0x0, 0x3, 0x0, 0x9, 0x0, 0x0, 0x0, 0x16, 0x0,
|
|
||||||
0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x7, 0x0, 0x0, 0x0,
|
|
||||||
0x1F, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x85, 0x0,
|
|
||||||
0x5, 0x0, 0x7, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0,
|
|
||||||
0x1F, 0x0, 0x0, 0x0, 0x22, 0x0, 0x0, 0x0, 0x81, 0x0,
|
|
||||||
0x5, 0x0, 0x7, 0x0, 0x0, 0x0, 0x27, 0x0, 0x0, 0x0,
|
|
||||||
0x23, 0x0, 0x0, 0x0, 0x26, 0x0, 0x0, 0x0, 0x51, 0x0,
|
|
||||||
0x5, 0x0, 0x6, 0x0, 0x0, 0x0, 0x29, 0x0, 0x0, 0x0,
|
|
||||||
0x27, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x51, 0x0,
|
|
||||||
0x5, 0x0, 0x6, 0x0, 0x0, 0x0, 0x2A, 0x0, 0x0, 0x0,
|
|
||||||
0x27, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x50, 0x0,
|
|
||||||
0x7, 0x0, 0x17, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0,
|
|
||||||
0x29, 0x0, 0x0, 0x0, 0x2A, 0x0, 0x0, 0x0, 0x28, 0x0,
|
|
||||||
0x0, 0x0, 0x25, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0,
|
|
||||||
0x2C, 0x0, 0x0, 0x0, 0x2D, 0x0, 0x0, 0x0, 0x1D, 0x0,
|
|
||||||
0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0,
|
|
||||||
0x2D, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, 0xFD, 0x0,
|
|
||||||
0x1, 0x0, 0x38, 0x0, 0x1, 0x0,
|
|
||||||
];
|
|
||||||
|
|
||||||
public byte[] TextMsdfFrag { get; } =
|
|
||||||
[
|
|
||||||
0x3, 0x2, 0x23, 0x7, 0x0, 0x0, 0x1, 0x0, 0xB, 0x0,
|
|
||||||
0x8, 0x0, 0x6C, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x11, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x11, 0x0,
|
|
||||||
0x2, 0x0, 0x32, 0x0, 0x0, 0x0, 0xB, 0x0, 0x6, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x47, 0x4C, 0x53, 0x4C, 0x2E, 0x73,
|
|
||||||
0x74, 0x64, 0x2E, 0x34, 0x35, 0x30, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0xE, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0,
|
|
||||||
0x0, 0x0, 0xF, 0x0, 0x8, 0x0, 0x4, 0x0, 0x0, 0x0,
|
|
||||||
0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x36, 0x0, 0x0, 0x0, 0x64, 0x0, 0x0, 0x0,
|
|
||||||
0x67, 0x0, 0x0, 0x0, 0x10, 0x0, 0x3, 0x0, 0x4, 0x0,
|
|
||||||
0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3, 0x0, 0x3, 0x0,
|
|
||||||
0x2, 0x0, 0x0, 0x0, 0xC2, 0x1, 0x0, 0x0, 0x5, 0x0,
|
|
||||||
0x4, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x7, 0x0, 0xC, 0x0,
|
|
||||||
0x0, 0x0, 0x6D, 0x65, 0x64, 0x69, 0x61, 0x6E, 0x28, 0x66,
|
|
||||||
0x31, 0x3B, 0x66, 0x31, 0x3B, 0x66, 0x31, 0x3B, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x5, 0x0, 0x3, 0x0, 0x9, 0x0, 0x0, 0x0,
|
|
||||||
0x72, 0x0, 0x0, 0x0, 0x5, 0x0, 0x3, 0x0, 0xA, 0x0,
|
|
||||||
0x0, 0x0, 0x67, 0x0, 0x0, 0x0, 0x5, 0x0, 0x3, 0x0,
|
|
||||||
0xB, 0x0, 0x0, 0x0, 0x62, 0x0, 0x0, 0x0, 0x5, 0x0,
|
|
||||||
0x6, 0x0, 0xF, 0x0, 0x0, 0x0, 0x73, 0x63, 0x72, 0x65,
|
|
||||||
0x65, 0x6E, 0x50, 0x78, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x28,
|
|
||||||
0x0, 0x0, 0x5, 0x0, 0x5, 0x0, 0x1E, 0x0, 0x0, 0x0,
|
|
||||||
0x75, 0x6E, 0x69, 0x74, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x0,
|
|
||||||
0x0, 0x0, 0x5, 0x0, 0x3, 0x0, 0x1F, 0x0, 0x0, 0x0,
|
|
||||||
0x55, 0x42, 0x4F, 0x0, 0x6, 0x0, 0x5, 0x0, 0x1F, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x70, 0x78, 0x52, 0x61,
|
|
||||||
0x6E, 0x67, 0x65, 0x0, 0x5, 0x0, 0x3, 0x0, 0x21, 0x0,
|
|
||||||
0x0, 0x0, 0x75, 0x62, 0x6F, 0x0, 0x5, 0x0, 0x4, 0x0,
|
|
||||||
0x2B, 0x0, 0x0, 0x0, 0x6D, 0x73, 0x64, 0x66, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x5, 0x0, 0x6, 0x0, 0x32, 0x0, 0x0, 0x0,
|
|
||||||
0x73, 0x63, 0x72, 0x65, 0x65, 0x6E, 0x54, 0x65, 0x78, 0x53,
|
|
||||||
0x69, 0x7A, 0x65, 0x0, 0x0, 0x0, 0x5, 0x0, 0x5, 0x0,
|
|
||||||
0x36, 0x0, 0x0, 0x0, 0x69, 0x6E, 0x54, 0x65, 0x78, 0x43,
|
|
||||||
0x6F, 0x6F, 0x72, 0x64, 0x0, 0x0, 0x5, 0x0, 0x3, 0x0,
|
|
||||||
0x44, 0x0, 0x0, 0x0, 0x6D, 0x73, 0x64, 0x0, 0x5, 0x0,
|
|
||||||
0x3, 0x0, 0x4A, 0x0, 0x0, 0x0, 0x73, 0x64, 0x0, 0x0,
|
|
||||||
0x5, 0x0, 0x4, 0x0, 0x4B, 0x0, 0x0, 0x0, 0x70, 0x61,
|
|
||||||
0x72, 0x61, 0x6D, 0x0, 0x0, 0x0, 0x5, 0x0, 0x4, 0x0,
|
|
||||||
0x50, 0x0, 0x0, 0x0, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x0,
|
|
||||||
0x0, 0x0, 0x5, 0x0, 0x4, 0x0, 0x54, 0x0, 0x0, 0x0,
|
|
||||||
0x70, 0x61, 0x72, 0x61, 0x6D, 0x0, 0x0, 0x0, 0x5, 0x0,
|
|
||||||
0x7, 0x0, 0x59, 0x0, 0x0, 0x0, 0x73, 0x63, 0x72, 0x65,
|
|
||||||
0x65, 0x6E, 0x50, 0x78, 0x44, 0x69, 0x73, 0x74, 0x61, 0x6E,
|
|
||||||
0x63, 0x65, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x4, 0x0,
|
|
||||||
0x5E, 0x0, 0x0, 0x0, 0x6F, 0x70, 0x61, 0x63, 0x69, 0x74,
|
|
||||||
0x79, 0x0, 0x5, 0x0, 0x5, 0x0, 0x64, 0x0, 0x0, 0x0,
|
|
||||||
0x6F, 0x75, 0x74, 0x43, 0x6F, 0x6C, 0x6F, 0x72, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x5, 0x0, 0x4, 0x0, 0x67, 0x0, 0x0, 0x0,
|
|
||||||
0x69, 0x6E, 0x43, 0x6F, 0x6C, 0x6F, 0x72, 0x0, 0x48, 0x0,
|
|
||||||
0x5, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x23, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x47, 0x0,
|
|
||||||
0x3, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0,
|
|
||||||
0x47, 0x0, 0x4, 0x0, 0x21, 0x0, 0x0, 0x0, 0x22, 0x0,
|
|
||||||
0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0,
|
|
||||||
0x21, 0x0, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x2B, 0x0, 0x0, 0x0,
|
|
||||||
0x22, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x47, 0x0,
|
|
||||||
0x4, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x36, 0x0,
|
|
||||||
0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x47, 0x0, 0x4, 0x0, 0x64, 0x0, 0x0, 0x0, 0x1E, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0,
|
|
||||||
0x67, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x1, 0x0,
|
|
||||||
0x0, 0x0, 0x13, 0x0, 0x2, 0x0, 0x2, 0x0, 0x0, 0x0,
|
|
||||||
0x21, 0x0, 0x3, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x0,
|
|
||||||
0x0, 0x0, 0x16, 0x0, 0x3, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x20, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x7, 0x0,
|
|
||||||
0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x21, 0x0, 0x6, 0x0, 0x8, 0x0, 0x0, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0,
|
|
||||||
0x7, 0x0, 0x0, 0x0, 0x21, 0x0, 0x3, 0x0, 0xE, 0x0,
|
|
||||||
0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0,
|
|
||||||
0x1C, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x2, 0x0,
|
|
||||||
0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x1D, 0x0, 0x0, 0x0,
|
|
||||||
0x7, 0x0, 0x0, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x1E, 0x0,
|
|
||||||
0x3, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x20, 0x0, 0x4, 0x0, 0x20, 0x0, 0x0, 0x0, 0x2, 0x0,
|
|
||||||
0x0, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0,
|
|
||||||
0x20, 0x0, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0, 0x2, 0x0,
|
|
||||||
0x0, 0x0, 0x15, 0x0, 0x4, 0x0, 0x22, 0x0, 0x0, 0x0,
|
|
||||||
0x20, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2B, 0x0,
|
|
||||||
0x4, 0x0, 0x22, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x24, 0x0,
|
|
||||||
0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x19, 0x0, 0x9, 0x0, 0x28, 0x0, 0x0, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1B, 0x0, 0x3, 0x0,
|
|
||||||
0x29, 0x0, 0x0, 0x0, 0x28, 0x0, 0x0, 0x0, 0x20, 0x0,
|
|
||||||
0x4, 0x0, 0x2A, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x29, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x2A, 0x0,
|
|
||||||
0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x17, 0x0, 0x4, 0x0, 0x2E, 0x0, 0x0, 0x0, 0x22, 0x0,
|
|
||||||
0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x80, 0x3F, 0x2C, 0x0, 0x5, 0x0, 0x1C, 0x0, 0x0, 0x0,
|
|
||||||
0x34, 0x0, 0x0, 0x0, 0x33, 0x0, 0x0, 0x0, 0x33, 0x0,
|
|
||||||
0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x35, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x3B, 0x0,
|
|
||||||
0x4, 0x0, 0x35, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x3A, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3F,
|
|
||||||
0x17, 0x0, 0x4, 0x0, 0x42, 0x0, 0x0, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0,
|
|
||||||
0x43, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x42, 0x0,
|
|
||||||
0x0, 0x0, 0x17, 0x0, 0x4, 0x0, 0x47, 0x0, 0x0, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x15, 0x0,
|
|
||||||
0x4, 0x0, 0x4C, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x4C, 0x0,
|
|
||||||
0x0, 0x0, 0x4D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x2B, 0x0, 0x4, 0x0, 0x4C, 0x0, 0x0, 0x0, 0x51, 0x0,
|
|
||||||
0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0,
|
|
||||||
0x4C, 0x0, 0x0, 0x0, 0x55, 0x0, 0x0, 0x0, 0x2, 0x0,
|
|
||||||
0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x61, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0,
|
|
||||||
0x4, 0x0, 0x63, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0,
|
|
||||||
0x47, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x63, 0x0,
|
|
||||||
0x0, 0x0, 0x64, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0,
|
|
||||||
0x2C, 0x0, 0x7, 0x0, 0x47, 0x0, 0x0, 0x0, 0x65, 0x0,
|
|
||||||
0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0,
|
|
||||||
0x61, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x20, 0x0,
|
|
||||||
0x4, 0x0, 0x66, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0,
|
|
||||||
0x47, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x66, 0x0,
|
|
||||||
0x0, 0x0, 0x67, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0,
|
|
||||||
0x36, 0x0, 0x5, 0x0, 0x2, 0x0, 0x0, 0x0, 0x4, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0,
|
|
||||||
0xF8, 0x0, 0x2, 0x0, 0x5, 0x0, 0x0, 0x0, 0x3B, 0x0,
|
|
||||||
0x4, 0x0, 0x43, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0,
|
|
||||||
0x7, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x7, 0x0,
|
|
||||||
0x0, 0x0, 0x4A, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0,
|
|
||||||
0x3B, 0x0, 0x4, 0x0, 0x7, 0x0, 0x0, 0x0, 0x4B, 0x0,
|
|
||||||
0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0,
|
|
||||||
0x7, 0x0, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x7, 0x0,
|
|
||||||
0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x7, 0x0, 0x0, 0x0,
|
|
||||||
0x54, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3B, 0x0,
|
|
||||||
0x4, 0x0, 0x7, 0x0, 0x0, 0x0, 0x59, 0x0, 0x0, 0x0,
|
|
||||||
0x7, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x7, 0x0,
|
|
||||||
0x0, 0x0, 0x5E, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0,
|
|
||||||
0x3D, 0x0, 0x4, 0x0, 0x29, 0x0, 0x0, 0x0, 0x45, 0x0,
|
|
||||||
0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0,
|
|
||||||
0x1C, 0x0, 0x0, 0x0, 0x46, 0x0, 0x0, 0x0, 0x36, 0x0,
|
|
||||||
0x0, 0x0, 0x57, 0x0, 0x5, 0x0, 0x47, 0x0, 0x0, 0x0,
|
|
||||||
0x48, 0x0, 0x0, 0x0, 0x45, 0x0, 0x0, 0x0, 0x46, 0x0,
|
|
||||||
0x0, 0x0, 0x4F, 0x0, 0x8, 0x0, 0x42, 0x0, 0x0, 0x0,
|
|
||||||
0x49, 0x0, 0x0, 0x0, 0x48, 0x0, 0x0, 0x0, 0x48, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0,
|
|
||||||
0x2, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x44, 0x0,
|
|
||||||
0x0, 0x0, 0x49, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0,
|
|
||||||
0x7, 0x0, 0x0, 0x0, 0x4E, 0x0, 0x0, 0x0, 0x44, 0x0,
|
|
||||||
0x0, 0x0, 0x4D, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x4F, 0x0, 0x0, 0x0, 0x4E, 0x0,
|
|
||||||
0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x4B, 0x0, 0x0, 0x0,
|
|
||||||
0x4F, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x7, 0x0,
|
|
||||||
0x0, 0x0, 0x52, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0,
|
|
||||||
0x51, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x53, 0x0, 0x0, 0x0, 0x52, 0x0, 0x0, 0x0,
|
|
||||||
0x3E, 0x0, 0x3, 0x0, 0x50, 0x0, 0x0, 0x0, 0x53, 0x0,
|
|
||||||
0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x7, 0x0, 0x0, 0x0,
|
|
||||||
0x56, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, 0x55, 0x0,
|
|
||||||
0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x57, 0x0, 0x0, 0x0, 0x56, 0x0, 0x0, 0x0, 0x3E, 0x0,
|
|
||||||
0x3, 0x0, 0x54, 0x0, 0x0, 0x0, 0x57, 0x0, 0x0, 0x0,
|
|
||||||
0x39, 0x0, 0x7, 0x0, 0x6, 0x0, 0x0, 0x0, 0x58, 0x0,
|
|
||||||
0x0, 0x0, 0xC, 0x0, 0x0, 0x0, 0x4B, 0x0, 0x0, 0x0,
|
|
||||||
0x50, 0x0, 0x0, 0x0, 0x54, 0x0, 0x0, 0x0, 0x3E, 0x0,
|
|
||||||
0x3, 0x0, 0x4A, 0x0, 0x0, 0x0, 0x58, 0x0, 0x0, 0x0,
|
|
||||||
0x39, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x5A, 0x0,
|
|
||||||
0x0, 0x0, 0xF, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x5B, 0x0, 0x0, 0x0, 0x4A, 0x0,
|
|
||||||
0x0, 0x0, 0x83, 0x0, 0x5, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x5C, 0x0, 0x0, 0x0, 0x5B, 0x0, 0x0, 0x0, 0x3A, 0x0,
|
|
||||||
0x0, 0x0, 0x85, 0x0, 0x5, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x5D, 0x0, 0x0, 0x0, 0x5A, 0x0, 0x0, 0x0, 0x5C, 0x0,
|
|
||||||
0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x59, 0x0, 0x0, 0x0,
|
|
||||||
0x5D, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x5F, 0x0, 0x0, 0x0, 0x59, 0x0, 0x0, 0x0,
|
|
||||||
0x81, 0x0, 0x5, 0x0, 0x6, 0x0, 0x0, 0x0, 0x60, 0x0,
|
|
||||||
0x0, 0x0, 0x5F, 0x0, 0x0, 0x0, 0x3A, 0x0, 0x0, 0x0,
|
|
||||||
0xC, 0x0, 0x8, 0x0, 0x6, 0x0, 0x0, 0x0, 0x62, 0x0,
|
|
||||||
0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0,
|
|
||||||
0x60, 0x0, 0x0, 0x0, 0x61, 0x0, 0x0, 0x0, 0x33, 0x0,
|
|
||||||
0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x5E, 0x0, 0x0, 0x0,
|
|
||||||
0x62, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x47, 0x0,
|
|
||||||
0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x67, 0x0, 0x0, 0x0,
|
|
||||||
0x3D, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x69, 0x0,
|
|
||||||
0x0, 0x0, 0x5E, 0x0, 0x0, 0x0, 0x50, 0x0, 0x7, 0x0,
|
|
||||||
0x47, 0x0, 0x0, 0x0, 0x6A, 0x0, 0x0, 0x0, 0x69, 0x0,
|
|
||||||
0x0, 0x0, 0x69, 0x0, 0x0, 0x0, 0x69, 0x0, 0x0, 0x0,
|
|
||||||
0x69, 0x0, 0x0, 0x0, 0xC, 0x0, 0x8, 0x0, 0x47, 0x0,
|
|
||||||
0x0, 0x0, 0x6B, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0,
|
|
||||||
0x2E, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x0, 0x68, 0x0,
|
|
||||||
0x0, 0x0, 0x6A, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0,
|
|
||||||
0x64, 0x0, 0x0, 0x0, 0x6B, 0x0, 0x0, 0x0, 0xFD, 0x0,
|
|
||||||
0x1, 0x0, 0x38, 0x0, 0x1, 0x0, 0x36, 0x0, 0x5, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x37, 0x0, 0x3, 0x0,
|
|
||||||
0x7, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x37, 0x0,
|
|
||||||
0x3, 0x0, 0x7, 0x0, 0x0, 0x0, 0xA, 0x0, 0x0, 0x0,
|
|
||||||
0x37, 0x0, 0x3, 0x0, 0x7, 0x0, 0x0, 0x0, 0xB, 0x0,
|
|
||||||
0x0, 0x0, 0xF8, 0x0, 0x2, 0x0, 0xD, 0x0, 0x0, 0x0,
|
|
||||||
0x3D, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x11, 0x0,
|
|
||||||
0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x12, 0x0, 0x0, 0x0, 0xA, 0x0,
|
|
||||||
0x0, 0x0, 0xC, 0x0, 0x7, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x13, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x25, 0x0,
|
|
||||||
0x0, 0x0, 0x11, 0x0, 0x0, 0x0, 0x12, 0x0, 0x0, 0x0,
|
|
||||||
0x3D, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x14, 0x0,
|
|
||||||
0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x15, 0x0, 0x0, 0x0, 0xA, 0x0,
|
|
||||||
0x0, 0x0, 0xC, 0x0, 0x7, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x16, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x28, 0x0,
|
|
||||||
0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x15, 0x0, 0x0, 0x0,
|
|
||||||
0x3D, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x17, 0x0,
|
|
||||||
0x0, 0x0, 0xB, 0x0, 0x0, 0x0, 0xC, 0x0, 0x7, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x18, 0x0, 0x0, 0x0, 0x1, 0x0,
|
|
||||||
0x0, 0x0, 0x25, 0x0, 0x0, 0x0, 0x16, 0x0, 0x0, 0x0,
|
|
||||||
0x17, 0x0, 0x0, 0x0, 0xC, 0x0, 0x7, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x19, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0,
|
|
||||||
0x28, 0x0, 0x0, 0x0, 0x13, 0x0, 0x0, 0x0, 0x18, 0x0,
|
|
||||||
0x0, 0x0, 0xFE, 0x0, 0x2, 0x0, 0x19, 0x0, 0x0, 0x0,
|
|
||||||
0x38, 0x0, 0x1, 0x0, 0x36, 0x0, 0x5, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0xE, 0x0, 0x0, 0x0, 0xF8, 0x0, 0x2, 0x0, 0x10, 0x0,
|
|
||||||
0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x1D, 0x0, 0x0, 0x0,
|
|
||||||
0x1E, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3B, 0x0,
|
|
||||||
0x4, 0x0, 0x1D, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0x0,
|
|
||||||
0x7, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x24, 0x0,
|
|
||||||
0x0, 0x0, 0x25, 0x0, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0,
|
|
||||||
0x23, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x26, 0x0, 0x0, 0x0, 0x25, 0x0, 0x0, 0x0,
|
|
||||||
0x50, 0x0, 0x5, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x27, 0x0,
|
|
||||||
0x0, 0x0, 0x26, 0x0, 0x0, 0x0, 0x26, 0x0, 0x0, 0x0,
|
|
||||||
0x3D, 0x0, 0x4, 0x0, 0x29, 0x0, 0x0, 0x0, 0x2C, 0x0,
|
|
||||||
0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x64, 0x0, 0x4, 0x0,
|
|
||||||
0x28, 0x0, 0x0, 0x0, 0x2D, 0x0, 0x0, 0x0, 0x2C, 0x0,
|
|
||||||
0x0, 0x0, 0x67, 0x0, 0x5, 0x0, 0x2E, 0x0, 0x0, 0x0,
|
|
||||||
0x2F, 0x0, 0x0, 0x0, 0x2D, 0x0, 0x0, 0x0, 0x23, 0x0,
|
|
||||||
0x0, 0x0, 0x6F, 0x0, 0x4, 0x0, 0x1C, 0x0, 0x0, 0x0,
|
|
||||||
0x30, 0x0, 0x0, 0x0, 0x2F, 0x0, 0x0, 0x0, 0x88, 0x0,
|
|
||||||
0x5, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x31, 0x0, 0x0, 0x0,
|
|
||||||
0x27, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x3E, 0x0,
|
|
||||||
0x3, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x31, 0x0, 0x0, 0x0,
|
|
||||||
0x3D, 0x0, 0x4, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x37, 0x0,
|
|
||||||
0x0, 0x0, 0x36, 0x0, 0x0, 0x0, 0xD1, 0x0, 0x4, 0x0,
|
|
||||||
0x1C, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x37, 0x0,
|
|
||||||
0x0, 0x0, 0x88, 0x0, 0x5, 0x0, 0x1C, 0x0, 0x0, 0x0,
|
|
||||||
0x39, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x38, 0x0,
|
|
||||||
0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x32, 0x0, 0x0, 0x0,
|
|
||||||
0x39, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x1C, 0x0,
|
|
||||||
0x0, 0x0, 0x3B, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0,
|
|
||||||
0x3D, 0x0, 0x4, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x3C, 0x0,
|
|
||||||
0x0, 0x0, 0x32, 0x0, 0x0, 0x0, 0x94, 0x0, 0x5, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x0, 0x0, 0x3B, 0x0,
|
|
||||||
0x0, 0x0, 0x3C, 0x0, 0x0, 0x0, 0x85, 0x0, 0x5, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x0, 0x0, 0x3A, 0x0,
|
|
||||||
0x0, 0x0, 0x3D, 0x0, 0x0, 0x0, 0xC, 0x0, 0x7, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x3F, 0x0, 0x0, 0x0, 0x1, 0x0,
|
|
||||||
0x0, 0x0, 0x28, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x0, 0x0,
|
|
||||||
0x33, 0x0, 0x0, 0x0, 0xFE, 0x0, 0x2, 0x0, 0x3F, 0x0,
|
|
||||||
0x0, 0x0, 0x38, 0x0, 0x1, 0x0,
|
|
||||||
];
|
|
||||||
|
|
||||||
public byte[] TextTransformVert { get; } =
|
|
||||||
[
|
|
||||||
0x3, 0x2, 0x23, 0x7, 0x0, 0x0, 0x1, 0x0, 0xB, 0x0,
|
|
||||||
0x8, 0x0, 0x2D, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x11, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0xB, 0x0,
|
|
||||||
0x6, 0x0, 0x1, 0x0, 0x0, 0x0, 0x47, 0x4C, 0x53, 0x4C,
|
|
||||||
0x2E, 0x73, 0x74, 0x64, 0x2E, 0x34, 0x35, 0x30, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0xE, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0xF, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0xD, 0x0, 0x0, 0x0, 0x19, 0x0,
|
|
||||||
0x0, 0x0, 0x25, 0x0, 0x0, 0x0, 0x27, 0x0, 0x0, 0x0,
|
|
||||||
0x29, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x3, 0x0,
|
|
||||||
0x3, 0x0, 0x2, 0x0, 0x0, 0x0, 0xC2, 0x1, 0x0, 0x0,
|
|
||||||
0x5, 0x0, 0x4, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61,
|
|
||||||
0x69, 0x6E, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x6, 0x0,
|
|
||||||
0xB, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x50, 0x65, 0x72,
|
|
||||||
0x56, 0x65, 0x72, 0x74, 0x65, 0x78, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x6, 0x0, 0x6, 0x0, 0xB, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x67, 0x6C, 0x5F, 0x50, 0x6F, 0x73, 0x69, 0x74,
|
|
||||||
0x69, 0x6F, 0x6E, 0x0, 0x6, 0x0, 0x7, 0x0, 0xB, 0x0,
|
|
||||||
0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x50,
|
|
||||||
0x6F, 0x69, 0x6E, 0x74, 0x53, 0x69, 0x7A, 0x65, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x6, 0x0, 0x7, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x2, 0x0, 0x0, 0x0, 0x67, 0x6C, 0x5F, 0x43, 0x6C, 0x69,
|
|
||||||
0x70, 0x44, 0x69, 0x73, 0x74, 0x61, 0x6E, 0x63, 0x65, 0x0,
|
|
||||||
0x6, 0x0, 0x7, 0x0, 0xB, 0x0, 0x0, 0x0, 0x3, 0x0,
|
|
||||||
0x0, 0x0, 0x67, 0x6C, 0x5F, 0x43, 0x75, 0x6C, 0x6C, 0x44,
|
|
||||||
0x69, 0x73, 0x74, 0x61, 0x6E, 0x63, 0x65, 0x0, 0x5, 0x0,
|
|
||||||
0x3, 0x0, 0xD, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x5, 0x0, 0x3, 0x0, 0x11, 0x0, 0x0, 0x0, 0x55, 0x42,
|
|
||||||
0x4F, 0x0, 0x6, 0x0, 0x7, 0x0, 0x11, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x56, 0x69, 0x65, 0x77, 0x50, 0x72,
|
|
||||||
0x6F, 0x6A, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x0, 0x0,
|
|
||||||
0x5, 0x0, 0x3, 0x0, 0x13, 0x0, 0x0, 0x0, 0x75, 0x62,
|
|
||||||
0x6F, 0x0, 0x5, 0x0, 0x4, 0x0, 0x19, 0x0, 0x0, 0x0,
|
|
||||||
0x69, 0x6E, 0x50, 0x6F, 0x73, 0x0, 0x0, 0x0, 0x5, 0x0,
|
|
||||||
0x5, 0x0, 0x25, 0x0, 0x0, 0x0, 0x6F, 0x75, 0x74, 0x54,
|
|
||||||
0x65, 0x78, 0x43, 0x6F, 0x6F, 0x72, 0x64, 0x0, 0x5, 0x0,
|
|
||||||
0x5, 0x0, 0x27, 0x0, 0x0, 0x0, 0x69, 0x6E, 0x54, 0x65,
|
|
||||||
0x78, 0x43, 0x6F, 0x6F, 0x72, 0x64, 0x0, 0x0, 0x5, 0x0,
|
|
||||||
0x5, 0x0, 0x29, 0x0, 0x0, 0x0, 0x6F, 0x75, 0x74, 0x43,
|
|
||||||
0x6F, 0x6C, 0x6F, 0x72, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0,
|
|
||||||
0x4, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x69, 0x6E, 0x43, 0x6F,
|
|
||||||
0x6C, 0x6F, 0x72, 0x0, 0x48, 0x0, 0x5, 0x0, 0xB, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0xB, 0x0,
|
|
||||||
0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0xB, 0x0,
|
|
||||||
0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x3, 0x0, 0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0xB, 0x0,
|
|
||||||
0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x4, 0x0, 0x0, 0x0, 0x47, 0x0, 0x3, 0x0, 0xB, 0x0,
|
|
||||||
0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x48, 0x0, 0x4, 0x0,
|
|
||||||
0x11, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0,
|
|
||||||
0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0x11, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x48, 0x0, 0x5, 0x0, 0x11, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x10, 0x0,
|
|
||||||
0x0, 0x0, 0x47, 0x0, 0x3, 0x0, 0x11, 0x0, 0x0, 0x0,
|
|
||||||
0x2, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x13, 0x0,
|
|
||||||
0x0, 0x0, 0x22, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0,
|
|
||||||
0x47, 0x0, 0x4, 0x0, 0x13, 0x0, 0x0, 0x0, 0x21, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0,
|
|
||||||
0x19, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x25, 0x0, 0x0, 0x0,
|
|
||||||
0x1E, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x47, 0x0,
|
|
||||||
0x4, 0x0, 0x27, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x29, 0x0,
|
|
||||||
0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0,
|
|
||||||
0x47, 0x0, 0x4, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x1E, 0x0,
|
|
||||||
0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x13, 0x0, 0x2, 0x0,
|
|
||||||
0x2, 0x0, 0x0, 0x0, 0x21, 0x0, 0x3, 0x0, 0x3, 0x0,
|
|
||||||
0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x16, 0x0, 0x3, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x17, 0x0,
|
|
||||||
0x4, 0x0, 0x7, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x4, 0x0, 0x0, 0x0, 0x15, 0x0, 0x4, 0x0, 0x8, 0x0,
|
|
||||||
0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x2B, 0x0, 0x4, 0x0, 0x8, 0x0, 0x0, 0x0, 0x9, 0x0,
|
|
||||||
0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1C, 0x0, 0x4, 0x0,
|
|
||||||
0xA, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x9, 0x0,
|
|
||||||
0x0, 0x0, 0x1E, 0x0, 0x6, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x7, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0xA, 0x0,
|
|
||||||
0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0,
|
|
||||||
0xC, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0xB, 0x0,
|
|
||||||
0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0xC, 0x0, 0x0, 0x0,
|
|
||||||
0xD, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x15, 0x0,
|
|
||||||
0x4, 0x0, 0xE, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0xE, 0x0,
|
|
||||||
0x0, 0x0, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x18, 0x0, 0x4, 0x0, 0x10, 0x0, 0x0, 0x0, 0x7, 0x0,
|
|
||||||
0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x3, 0x0,
|
|
||||||
0x11, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x20, 0x0,
|
|
||||||
0x4, 0x0, 0x12, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0,
|
|
||||||
0x11, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x12, 0x0,
|
|
||||||
0x0, 0x0, 0x13, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0,
|
|
||||||
0x20, 0x0, 0x4, 0x0, 0x14, 0x0, 0x0, 0x0, 0x2, 0x0,
|
|
||||||
0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0,
|
|
||||||
0x17, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x3, 0x0,
|
|
||||||
0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x18, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x17, 0x0, 0x0, 0x0, 0x3B, 0x0,
|
|
||||||
0x4, 0x0, 0x18, 0x0, 0x0, 0x0, 0x19, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x1B, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3F,
|
|
||||||
0x20, 0x0, 0x4, 0x0, 0x21, 0x0, 0x0, 0x0, 0x3, 0x0,
|
|
||||||
0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0,
|
|
||||||
0x23, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x2, 0x0,
|
|
||||||
0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x24, 0x0, 0x0, 0x0,
|
|
||||||
0x3, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0, 0x3B, 0x0,
|
|
||||||
0x4, 0x0, 0x24, 0x0, 0x0, 0x0, 0x25, 0x0, 0x0, 0x0,
|
|
||||||
0x3, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x26, 0x0,
|
|
||||||
0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0,
|
|
||||||
0x3B, 0x0, 0x4, 0x0, 0x26, 0x0, 0x0, 0x0, 0x27, 0x0,
|
|
||||||
0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0,
|
|
||||||
0x21, 0x0, 0x0, 0x0, 0x29, 0x0, 0x0, 0x0, 0x3, 0x0,
|
|
||||||
0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x2A, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3B, 0x0,
|
|
||||||
0x4, 0x0, 0x2A, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x36, 0x0, 0x5, 0x0, 0x2, 0x0,
|
|
||||||
0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x3, 0x0, 0x0, 0x0, 0xF8, 0x0, 0x2, 0x0, 0x5, 0x0,
|
|
||||||
0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x14, 0x0, 0x0, 0x0,
|
|
||||||
0x15, 0x0, 0x0, 0x0, 0x13, 0x0, 0x0, 0x0, 0xF, 0x0,
|
|
||||||
0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x10, 0x0, 0x0, 0x0,
|
|
||||||
0x16, 0x0, 0x0, 0x0, 0x15, 0x0, 0x0, 0x0, 0x3D, 0x0,
|
|
||||||
0x4, 0x0, 0x17, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0,
|
|
||||||
0x19, 0x0, 0x0, 0x0, 0x51, 0x0, 0x5, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x51, 0x0, 0x5, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x1D, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x51, 0x0, 0x5, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0,
|
|
||||||
0x2, 0x0, 0x0, 0x0, 0x50, 0x0, 0x7, 0x0, 0x7, 0x0,
|
|
||||||
0x0, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x1C, 0x0, 0x0, 0x0,
|
|
||||||
0x1D, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x1B, 0x0,
|
|
||||||
0x0, 0x0, 0x91, 0x0, 0x5, 0x0, 0x7, 0x0, 0x0, 0x0,
|
|
||||||
0x20, 0x0, 0x0, 0x0, 0x16, 0x0, 0x0, 0x0, 0x1F, 0x0,
|
|
||||||
0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x21, 0x0, 0x0, 0x0,
|
|
||||||
0x22, 0x0, 0x0, 0x0, 0xD, 0x0, 0x0, 0x0, 0xF, 0x0,
|
|
||||||
0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x22, 0x0, 0x0, 0x0,
|
|
||||||
0x20, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x23, 0x0,
|
|
||||||
0x0, 0x0, 0x28, 0x0, 0x0, 0x0, 0x27, 0x0, 0x0, 0x0,
|
|
||||||
0x3E, 0x0, 0x3, 0x0, 0x25, 0x0, 0x0, 0x0, 0x28, 0x0,
|
|
||||||
0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x7, 0x0, 0x0, 0x0,
|
|
||||||
0x2C, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x0, 0x0, 0x3E, 0x0,
|
|
||||||
0x3, 0x0, 0x29, 0x0, 0x0, 0x0, 0x2C, 0x0, 0x0, 0x0,
|
|
||||||
0xFD, 0x0, 0x1, 0x0, 0x38, 0x0, 0x1, 0x0,
|
|
||||||
];
|
|
||||||
|
|
||||||
public byte[] VideoYuv2RgbaFrag { get; } =
|
|
||||||
[
|
|
||||||
0x3, 0x2, 0x23, 0x7, 0x0, 0x0, 0x1, 0x0, 0xB, 0x0,
|
|
||||||
0x8, 0x0, 0x45, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x11, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0xB, 0x0,
|
|
||||||
0x6, 0x0, 0x1, 0x0, 0x0, 0x0, 0x47, 0x4C, 0x53, 0x4C,
|
|
||||||
0x2E, 0x73, 0x74, 0x64, 0x2E, 0x34, 0x35, 0x30, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0xE, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0xF, 0x0, 0x7, 0x0, 0x4, 0x0,
|
|
||||||
0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x11, 0x0, 0x0, 0x0, 0x2E, 0x0,
|
|
||||||
0x0, 0x0, 0x10, 0x0, 0x3, 0x0, 0x4, 0x0, 0x0, 0x0,
|
|
||||||
0x7, 0x0, 0x0, 0x0, 0x3, 0x0, 0x3, 0x0, 0x2, 0x0,
|
|
||||||
0x0, 0x0, 0xC2, 0x1, 0x0, 0x0, 0x5, 0x0, 0x4, 0x0,
|
|
||||||
0x4, 0x0, 0x0, 0x0, 0x6D, 0x61, 0x69, 0x6E, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x5, 0x0, 0x3, 0x0, 0x9, 0x0, 0x0, 0x0,
|
|
||||||
0x79, 0x75, 0x76, 0x0, 0x5, 0x0, 0x5, 0x0, 0xD, 0x0,
|
|
||||||
0x0, 0x0, 0x59, 0x53, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x72,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x5, 0x0, 0x11, 0x0,
|
|
||||||
0x0, 0x0, 0x54, 0x65, 0x78, 0x43, 0x6F, 0x6F, 0x72, 0x64,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x5, 0x0, 0x1A, 0x0,
|
|
||||||
0x0, 0x0, 0x55, 0x53, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x72,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x5, 0x0, 0x21, 0x0,
|
|
||||||
0x0, 0x0, 0x56, 0x53, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x72,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x5, 0x0, 0x2E, 0x0,
|
|
||||||
0x0, 0x0, 0x46, 0x72, 0x61, 0x67, 0x43, 0x6F, 0x6C, 0x6F,
|
|
||||||
0x72, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0xD, 0x0,
|
|
||||||
0x0, 0x0, 0x22, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0,
|
|
||||||
0x47, 0x0, 0x4, 0x0, 0xD, 0x0, 0x0, 0x0, 0x21, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0,
|
|
||||||
0x11, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x1A, 0x0, 0x0, 0x0,
|
|
||||||
0x22, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x47, 0x0,
|
|
||||||
0x4, 0x0, 0x1A, 0x0, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0, 0x21, 0x0,
|
|
||||||
0x0, 0x0, 0x22, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0,
|
|
||||||
0x47, 0x0, 0x4, 0x0, 0x21, 0x0, 0x0, 0x0, 0x21, 0x0,
|
|
||||||
0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x47, 0x0, 0x4, 0x0,
|
|
||||||
0x2E, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x13, 0x0, 0x2, 0x0, 0x2, 0x0, 0x0, 0x0,
|
|
||||||
0x21, 0x0, 0x3, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x0,
|
|
||||||
0x0, 0x0, 0x16, 0x0, 0x3, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x20, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0, 0x7, 0x0,
|
|
||||||
0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0,
|
|
||||||
0x20, 0x0, 0x4, 0x0, 0x8, 0x0, 0x0, 0x0, 0x7, 0x0,
|
|
||||||
0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x19, 0x0, 0x9, 0x0,
|
|
||||||
0xA, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x1, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x1B, 0x0, 0x3, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0xA, 0x0, 0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0xC, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xB, 0x0, 0x0, 0x0,
|
|
||||||
0x3B, 0x0, 0x4, 0x0, 0xC, 0x0, 0x0, 0x0, 0xD, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0,
|
|
||||||
0xF, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x2, 0x0,
|
|
||||||
0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x10, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0xF, 0x0, 0x0, 0x0, 0x3B, 0x0,
|
|
||||||
0x4, 0x0, 0x10, 0x0, 0x0, 0x0, 0x11, 0x0, 0x0, 0x0,
|
|
||||||
0x1, 0x0, 0x0, 0x0, 0x17, 0x0, 0x4, 0x0, 0x13, 0x0,
|
|
||||||
0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0,
|
|
||||||
0x15, 0x0, 0x4, 0x0, 0x15, 0x0, 0x0, 0x0, 0x20, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0,
|
|
||||||
0x15, 0x0, 0x0, 0x0, 0x16, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x18, 0x0, 0x0, 0x0,
|
|
||||||
0x7, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x3B, 0x0,
|
|
||||||
0x4, 0x0, 0xC, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x15, 0x0,
|
|
||||||
0x0, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0,
|
|
||||||
0x3B, 0x0, 0x4, 0x0, 0xC, 0x0, 0x0, 0x0, 0x21, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0,
|
|
||||||
0x15, 0x0, 0x0, 0x0, 0x26, 0x0, 0x0, 0x0, 0x2, 0x0,
|
|
||||||
0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x28, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xBD, 0x2B, 0x0,
|
|
||||||
0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x29, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0xBF, 0x2C, 0x0, 0x6, 0x0, 0x7, 0x0,
|
|
||||||
0x0, 0x0, 0x2A, 0x0, 0x0, 0x0, 0x28, 0x0, 0x0, 0x0,
|
|
||||||
0x29, 0x0, 0x0, 0x0, 0x29, 0x0, 0x0, 0x0, 0x20, 0x0,
|
|
||||||
0x4, 0x0, 0x2D, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0,
|
|
||||||
0x13, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x2D, 0x0,
|
|
||||||
0x0, 0x0, 0x2E, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0,
|
|
||||||
0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x30, 0x0,
|
|
||||||
0x0, 0x0, 0xF4, 0xFD, 0x94, 0x3F, 0x2B, 0x0, 0x4, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x31, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x32, 0x0, 0x0, 0x0, 0x6, 0x81, 0xE5, 0x3F, 0x2C, 0x0,
|
|
||||||
0x6, 0x0, 0x7, 0x0, 0x0, 0x0, 0x33, 0x0, 0x0, 0x0,
|
|
||||||
0x30, 0x0, 0x0, 0x0, 0x31, 0x0, 0x0, 0x0, 0x32, 0x0,
|
|
||||||
0x0, 0x0, 0x20, 0x0, 0x4, 0x0, 0x35, 0x0, 0x0, 0x0,
|
|
||||||
0x3, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x2B, 0x0,
|
|
||||||
0x4, 0x0, 0x6, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0,
|
|
||||||
0xAC, 0x1C, 0x5A, 0xBE, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x39, 0x0, 0x0, 0x0, 0xB0, 0x72, 0x8, 0xBF,
|
|
||||||
0x2C, 0x0, 0x6, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3A, 0x0,
|
|
||||||
0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0,
|
|
||||||
0x39, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x3E, 0x0, 0x0, 0x0, 0x2, 0x2B, 0x7, 0x40,
|
|
||||||
0x2C, 0x0, 0x6, 0x0, 0x7, 0x0, 0x0, 0x0, 0x3F, 0x0,
|
|
||||||
0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x0, 0x0,
|
|
||||||
0x31, 0x0, 0x0, 0x0, 0x2B, 0x0, 0x4, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x42, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3F,
|
|
||||||
0x2B, 0x0, 0x4, 0x0, 0x15, 0x0, 0x0, 0x0, 0x43, 0x0,
|
|
||||||
0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x36, 0x0, 0x5, 0x0,
|
|
||||||
0x2, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0xF8, 0x0, 0x2, 0x0,
|
|
||||||
0x5, 0x0, 0x0, 0x0, 0x3B, 0x0, 0x4, 0x0, 0x8, 0x0,
|
|
||||||
0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0,
|
|
||||||
0x3D, 0x0, 0x4, 0x0, 0xB, 0x0, 0x0, 0x0, 0xE, 0x0,
|
|
||||||
0x0, 0x0, 0xD, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0,
|
|
||||||
0xF, 0x0, 0x0, 0x0, 0x12, 0x0, 0x0, 0x0, 0x11, 0x0,
|
|
||||||
0x0, 0x0, 0x57, 0x0, 0x5, 0x0, 0x13, 0x0, 0x0, 0x0,
|
|
||||||
0x14, 0x0, 0x0, 0x0, 0xE, 0x0, 0x0, 0x0, 0x12, 0x0,
|
|
||||||
0x0, 0x0, 0x51, 0x0, 0x5, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x17, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x18, 0x0, 0x0, 0x0,
|
|
||||||
0x19, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x16, 0x0,
|
|
||||||
0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x19, 0x0, 0x0, 0x0,
|
|
||||||
0x17, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0xB, 0x0,
|
|
||||||
0x0, 0x0, 0x1B, 0x0, 0x0, 0x0, 0x1A, 0x0, 0x0, 0x0,
|
|
||||||
0x3D, 0x0, 0x4, 0x0, 0xF, 0x0, 0x0, 0x0, 0x1C, 0x0,
|
|
||||||
0x0, 0x0, 0x11, 0x0, 0x0, 0x0, 0x57, 0x0, 0x5, 0x0,
|
|
||||||
0x13, 0x0, 0x0, 0x0, 0x1D, 0x0, 0x0, 0x0, 0x1B, 0x0,
|
|
||||||
0x0, 0x0, 0x1C, 0x0, 0x0, 0x0, 0x51, 0x0, 0x5, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x1D, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0,
|
|
||||||
0x18, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x9, 0x0,
|
|
||||||
0x0, 0x0, 0x1F, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0,
|
|
||||||
0x20, 0x0, 0x0, 0x0, 0x1E, 0x0, 0x0, 0x0, 0x3D, 0x0,
|
|
||||||
0x4, 0x0, 0xB, 0x0, 0x0, 0x0, 0x22, 0x0, 0x0, 0x0,
|
|
||||||
0x21, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0xF, 0x0,
|
|
||||||
0x0, 0x0, 0x23, 0x0, 0x0, 0x0, 0x11, 0x0, 0x0, 0x0,
|
|
||||||
0x57, 0x0, 0x5, 0x0, 0x13, 0x0, 0x0, 0x0, 0x24, 0x0,
|
|
||||||
0x0, 0x0, 0x22, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0,
|
|
||||||
0x51, 0x0, 0x5, 0x0, 0x6, 0x0, 0x0, 0x0, 0x25, 0x0,
|
|
||||||
0x0, 0x0, 0x24, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x41, 0x0, 0x5, 0x0, 0x18, 0x0, 0x0, 0x0, 0x27, 0x0,
|
|
||||||
0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x26, 0x0, 0x0, 0x0,
|
|
||||||
0x3E, 0x0, 0x3, 0x0, 0x27, 0x0, 0x0, 0x0, 0x25, 0x0,
|
|
||||||
0x0, 0x0, 0x3D, 0x0, 0x4, 0x0, 0x7, 0x0, 0x0, 0x0,
|
|
||||||
0x2B, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x81, 0x0,
|
|
||||||
0x5, 0x0, 0x7, 0x0, 0x0, 0x0, 0x2C, 0x0, 0x0, 0x0,
|
|
||||||
0x2B, 0x0, 0x0, 0x0, 0x2A, 0x0, 0x0, 0x0, 0x3E, 0x0,
|
|
||||||
0x3, 0x0, 0x9, 0x0, 0x0, 0x0, 0x2C, 0x0, 0x0, 0x0,
|
|
||||||
0x3D, 0x0, 0x4, 0x0, 0x7, 0x0, 0x0, 0x0, 0x2F, 0x0,
|
|
||||||
0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x94, 0x0, 0x5, 0x0,
|
|
||||||
0x6, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x2F, 0x0,
|
|
||||||
0x0, 0x0, 0x33, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0,
|
|
||||||
0x35, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x0, 0x2E, 0x0,
|
|
||||||
0x0, 0x0, 0x16, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0,
|
|
||||||
0x36, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x3D, 0x0,
|
|
||||||
0x4, 0x0, 0x7, 0x0, 0x0, 0x0, 0x37, 0x0, 0x0, 0x0,
|
|
||||||
0x9, 0x0, 0x0, 0x0, 0x94, 0x0, 0x5, 0x0, 0x6, 0x0,
|
|
||||||
0x0, 0x0, 0x3B, 0x0, 0x0, 0x0, 0x37, 0x0, 0x0, 0x0,
|
|
||||||
0x3A, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x35, 0x0,
|
|
||||||
0x0, 0x0, 0x3C, 0x0, 0x0, 0x0, 0x2E, 0x0, 0x0, 0x0,
|
|
||||||
0x1F, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x3C, 0x0,
|
|
||||||
0x0, 0x0, 0x3B, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x4, 0x0,
|
|
||||||
0x7, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x0, 0x0, 0x9, 0x0,
|
|
||||||
0x0, 0x0, 0x94, 0x0, 0x5, 0x0, 0x6, 0x0, 0x0, 0x0,
|
|
||||||
0x40, 0x0, 0x0, 0x0, 0x3D, 0x0, 0x0, 0x0, 0x3F, 0x0,
|
|
||||||
0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x35, 0x0, 0x0, 0x0,
|
|
||||||
0x41, 0x0, 0x0, 0x0, 0x2E, 0x0, 0x0, 0x0, 0x26, 0x0,
|
|
||||||
0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x41, 0x0, 0x0, 0x0,
|
|
||||||
0x40, 0x0, 0x0, 0x0, 0x41, 0x0, 0x5, 0x0, 0x35, 0x0,
|
|
||||||
0x0, 0x0, 0x44, 0x0, 0x0, 0x0, 0x2E, 0x0, 0x0, 0x0,
|
|
||||||
0x43, 0x0, 0x0, 0x0, 0x3E, 0x0, 0x3, 0x0, 0x44, 0x0,
|
|
||||||
0x0, 0x0, 0x42, 0x0, 0x0, 0x0, 0xFD, 0x0, 0x1, 0x0,
|
|
||||||
0x38, 0x0, 0x1, 0x0,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Runtime.InteropServices;
|
using Nerfed.Runtime.Video;
|
||||||
using Nerfed.Runtime.Video;
|
|
||||||
using RefreshCS;
|
using RefreshCS;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Nerfed.Runtime.Graphics;
|
namespace Nerfed.Runtime.Graphics;
|
||||||
|
|
||||||
@@ -13,12 +13,16 @@ public class GraphicsDevice : IDisposable
|
|||||||
public BackendFlags Backend { get; }
|
public BackendFlags Backend { get; }
|
||||||
public bool DebugMode { 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
|
// Built-in video pipeline
|
||||||
internal GraphicsPipeline VideoPipeline { get; }
|
internal GraphicsPipeline VideoPipeline { get; private set; }
|
||||||
|
|
||||||
// Built-in text shader info
|
// Built-in text shader info
|
||||||
public Shader TextVertexShader;
|
|
||||||
public Shader TextFragmentShader;
|
|
||||||
public VertexInputState TextVertexInputState { get; }
|
public VertexInputState TextVertexInputState { get; }
|
||||||
|
|
||||||
// Built-in samplers
|
// Built-in samplers
|
||||||
@@ -53,85 +57,21 @@ public class GraphicsDevice : IDisposable
|
|||||||
|
|
||||||
Backend = (BackendFlags)Refresh.Refresh_GetBackend(Handle);
|
Backend = (BackendFlags)Refresh.Refresh_GetBackend(Handle);
|
||||||
|
|
||||||
IEmbeddedShaders embeddedShaders;
|
TextVertexInputState = VertexInputState.CreateSingleBinding<FontVertex>();
|
||||||
switch (Backend)
|
|
||||||
{
|
|
||||||
case BackendFlags.Vulkan:
|
|
||||||
embeddedShaders = new EmbeddedShadersSpirV();
|
|
||||||
break;
|
|
||||||
case BackendFlags.D3D11:
|
|
||||||
throw new NotImplementedException("D3D11 embedded shaders");
|
|
||||||
break;
|
|
||||||
case BackendFlags.Metal:
|
|
||||||
throw new NotImplementedException("Metal embedded shaders");
|
|
||||||
break;
|
|
||||||
default: throw new ArgumentOutOfRangeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
Shader fullscreenVertShader;
|
PointSampler = new Sampler(this, SamplerCreateInfo.PointClamp);
|
||||||
Shader textVertShader;
|
LinearSampler = new Sampler(this, SamplerCreateInfo.LinearClamp);
|
||||||
Shader textFragShader;
|
|
||||||
Shader videoFragShader;
|
|
||||||
|
|
||||||
using (MemoryStream fullscreenVertStream = new MemoryStream(embeddedShaders.FullscreenVert))
|
fencePool = new FencePool(this);
|
||||||
{
|
commandBufferPool = new CommandBufferPool(this);
|
||||||
fullscreenVertShader = new Shader(
|
}
|
||||||
this,
|
|
||||||
fullscreenVertStream,
|
|
||||||
"main",
|
|
||||||
new ShaderCreateInfo
|
|
||||||
{
|
|
||||||
ShaderStage = ShaderStage.Vertex,
|
|
||||||
ShaderFormat = embeddedShaders.ShaderFormat
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
using (MemoryStream videoYuv2RgbaFragStream = new MemoryStream(embeddedShaders.VideoYuv2RgbaFrag))
|
internal void LoadDefaultPipelines()
|
||||||
{
|
{
|
||||||
videoFragShader = new Shader(
|
FullscreenVertexShader = ResourceManager.Load<Shader>("Shaders/Fullscreen.vert");
|
||||||
this,
|
VideoFragmentShader = ResourceManager.Load<Shader>("Shaders/Video.frag");
|
||||||
videoYuv2RgbaFragStream,
|
TextVertexShader = ResourceManager.Load<Shader>("Shaders/Text.vert");
|
||||||
"main",
|
TextFragmentShader = ResourceManager.Load<Shader>("Shaders/Text.frag");
|
||||||
new ShaderCreateInfo
|
|
||||||
{
|
|
||||||
ShaderStage = ShaderStage.Fragment,
|
|
||||||
ShaderFormat = embeddedShaders.ShaderFormat,
|
|
||||||
SamplerCount = 3
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
using (MemoryStream textTransformVertStream = new MemoryStream(embeddedShaders.TextTransformVert))
|
|
||||||
{
|
|
||||||
textVertShader = new Shader(
|
|
||||||
this,
|
|
||||||
textTransformVertStream,
|
|
||||||
"main",
|
|
||||||
new ShaderCreateInfo
|
|
||||||
{
|
|
||||||
ShaderStage = ShaderStage.Vertex,
|
|
||||||
ShaderFormat = embeddedShaders.ShaderFormat,
|
|
||||||
UniformBufferCount = 1
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
using (MemoryStream textMsdfFragStream = new MemoryStream(embeddedShaders.TextMsdfFrag))
|
|
||||||
{
|
|
||||||
textFragShader = new Shader(
|
|
||||||
this,
|
|
||||||
textMsdfFragStream,
|
|
||||||
"main",
|
|
||||||
new ShaderCreateInfo
|
|
||||||
{
|
|
||||||
ShaderStage = ShaderStage.Fragment,
|
|
||||||
ShaderFormat = embeddedShaders.ShaderFormat,
|
|
||||||
SamplerCount = 1,
|
|
||||||
UniformBufferCount = 1
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoPipeline = new GraphicsPipeline(
|
VideoPipeline = new GraphicsPipeline(
|
||||||
this,
|
this,
|
||||||
@@ -144,25 +84,14 @@ public class GraphicsDevice : IDisposable
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
DepthStencilState = DepthStencilState.Disable,
|
DepthStencilState = DepthStencilState.Disable,
|
||||||
VertexShader = fullscreenVertShader,
|
VertexShader = FullscreenVertexShader,
|
||||||
FragmentShader = videoFragShader,
|
FragmentShader = VideoFragmentShader,
|
||||||
VertexInputState = VertexInputState.Empty,
|
VertexInputState = VertexInputState.Empty,
|
||||||
RasterizerState = RasterizerState.CCW_CullNone,
|
RasterizerState = RasterizerState.CCW_CullNone,
|
||||||
PrimitiveType = PrimitiveType.TriangleList,
|
PrimitiveType = PrimitiveType.TriangleList,
|
||||||
MultisampleState = MultisampleState.None
|
MultisampleState = MultisampleState.None
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
TextVertexShader = textVertShader;
|
|
||||||
TextFragmentShader = textFragShader;
|
|
||||||
|
|
||||||
TextVertexInputState = VertexInputState.CreateSingleBinding<FontVertex>();
|
|
||||||
|
|
||||||
PointSampler = new Sampler(this, SamplerCreateInfo.PointClamp);
|
|
||||||
LinearSampler = new Sampler(this, SamplerCreateInfo.LinearClamp);
|
|
||||||
|
|
||||||
fencePool = new FencePool(this);
|
|
||||||
commandBufferPool = new CommandBufferPool(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -443,6 +372,11 @@ public class GraphicsDevice : IDisposable
|
|||||||
|
|
||||||
resources.Clear();
|
resources.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResourceManager.Unload(FullscreenVertexShader);
|
||||||
|
ResourceManager.Unload(TextFragmentShader);
|
||||||
|
ResourceManager.Unload(TextVertexShader);
|
||||||
|
ResourceManager.Unload(VideoFragmentShader);
|
||||||
}
|
}
|
||||||
|
|
||||||
Refresh.Refresh_DestroyDevice(Handle);
|
Refresh.Refresh_DestroyDevice(Handle);
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace Nerfed.Runtime.Graphics;
|
|
||||||
|
|
||||||
internal interface IEmbeddedShaders
|
|
||||||
{
|
|
||||||
ShaderFormat ShaderFormat { get; }
|
|
||||||
byte[] FullscreenVert { get; }
|
|
||||||
byte[] TextMsdfFrag { get; }
|
|
||||||
byte[] TextTransformVert { get; }
|
|
||||||
byte[] VideoYuv2RgbaFrag { get; }
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
using RefreshCS;
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace Nerfed.Runtime.Graphics;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Shaders are used to create graphics pipelines.
|
|
||||||
/// Graphics pipelines take a vertex shader and a fragment shader.
|
|
||||||
/// </summary>
|
|
||||||
public class Shader : RefreshResource
|
|
||||||
{
|
|
||||||
protected override Action<IntPtr, IntPtr> ReleaseFunction => Refresh.Refresh_ReleaseShader;
|
|
||||||
|
|
||||||
public uint SamplerCount { get; }
|
|
||||||
public uint StorageTextureCount { get; }
|
|
||||||
public uint StorageBufferCount { get; }
|
|
||||||
public uint UniformBufferCount { get; }
|
|
||||||
|
|
||||||
public unsafe Shader(
|
|
||||||
GraphicsDevice device,
|
|
||||||
string filePath,
|
|
||||||
string entryPointName,
|
|
||||||
in ShaderCreateInfo shaderCreateInfo
|
|
||||||
) : base(device)
|
|
||||||
{
|
|
||||||
using FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
|
||||||
Handle = CreateFromStream(
|
|
||||||
device,
|
|
||||||
stream,
|
|
||||||
entryPointName,
|
|
||||||
shaderCreateInfo
|
|
||||||
);
|
|
||||||
|
|
||||||
SamplerCount = shaderCreateInfo.SamplerCount;
|
|
||||||
StorageTextureCount = shaderCreateInfo.StorageTextureCount;
|
|
||||||
StorageBufferCount = shaderCreateInfo.StorageBufferCount;
|
|
||||||
UniformBufferCount = shaderCreateInfo.UniformBufferCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe Shader(
|
|
||||||
GraphicsDevice device,
|
|
||||||
Stream stream,
|
|
||||||
string entryPointName,
|
|
||||||
in ShaderCreateInfo shaderCreateInfo
|
|
||||||
) : base(device)
|
|
||||||
{
|
|
||||||
Handle = CreateFromStream(
|
|
||||||
device,
|
|
||||||
stream,
|
|
||||||
entryPointName,
|
|
||||||
shaderCreateInfo
|
|
||||||
);
|
|
||||||
|
|
||||||
SamplerCount = shaderCreateInfo.SamplerCount;
|
|
||||||
StorageTextureCount = shaderCreateInfo.StorageTextureCount;
|
|
||||||
StorageBufferCount = shaderCreateInfo.StorageBufferCount;
|
|
||||||
UniformBufferCount = shaderCreateInfo.UniformBufferCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe IntPtr CreateFromStream(
|
|
||||||
GraphicsDevice device,
|
|
||||||
Stream stream,
|
|
||||||
string entryPointName,
|
|
||||||
in ShaderCreateInfo shaderCreateInfo
|
|
||||||
) {
|
|
||||||
void* bytecodeBuffer = NativeMemory.Alloc((nuint) stream.Length);
|
|
||||||
Span<byte> bytecodeSpan = new Span<byte>(bytecodeBuffer, (int) stream.Length);
|
|
||||||
stream.ReadExactly(bytecodeSpan);
|
|
||||||
|
|
||||||
Refresh.ShaderCreateInfo refreshShaderCreateInfo;
|
|
||||||
refreshShaderCreateInfo.CodeSize = (nuint) stream.Length;
|
|
||||||
refreshShaderCreateInfo.Code = (byte*) bytecodeBuffer;
|
|
||||||
refreshShaderCreateInfo.EntryPointName = entryPointName;
|
|
||||||
refreshShaderCreateInfo.Stage = (Refresh.ShaderStage) shaderCreateInfo.ShaderStage;
|
|
||||||
refreshShaderCreateInfo.Format = (Refresh.ShaderFormat) shaderCreateInfo.ShaderFormat;
|
|
||||||
refreshShaderCreateInfo.SamplerCount = shaderCreateInfo.SamplerCount;
|
|
||||||
refreshShaderCreateInfo.StorageTextureCount = shaderCreateInfo.StorageTextureCount;
|
|
||||||
refreshShaderCreateInfo.StorageBufferCount = shaderCreateInfo.StorageBufferCount;
|
|
||||||
refreshShaderCreateInfo.UniformBufferCount = shaderCreateInfo.UniformBufferCount;
|
|
||||||
|
|
||||||
IntPtr shaderModule = Refresh.Refresh_CreateShader(
|
|
||||||
device.Handle,
|
|
||||||
refreshShaderCreateInfo
|
|
||||||
);
|
|
||||||
|
|
||||||
NativeMemory.Free(bytecodeBuffer);
|
|
||||||
return shaderModule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Gui;
|
||||||
|
|
||||||
|
public static unsafe class GuiClipboard
|
||||||
|
{
|
||||||
|
private static IntPtr clipboard;
|
||||||
|
private static readonly Dictionary<Delegate, IntPtr> pinned = new Dictionary<Delegate, IntPtr>();
|
||||||
|
|
||||||
|
public static readonly IntPtr GetFnPtr = GetPointerTo(Get);
|
||||||
|
public static readonly IntPtr SetFnPtr = GetPointerTo(Set);
|
||||||
|
|
||||||
|
private static unsafe void Set(void* userdata, byte* text)
|
||||||
|
{
|
||||||
|
int len = 0; while (text[len] != 0) len++;
|
||||||
|
string str = Encoding.UTF8.GetString(text, len);
|
||||||
|
SDL2.SDL.SDL_SetClipboardText(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe byte* Get(void* userdata)
|
||||||
|
{
|
||||||
|
if (clipboard != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
NativeMemory.Free((void*) clipboard);
|
||||||
|
clipboard = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
string str = SDL2.SDL.SDL_GetClipboardText();
|
||||||
|
int length = Encoding.UTF8.GetByteCount(str);
|
||||||
|
byte* bytes = (byte*)(clipboard = (nint)NativeMemory.Alloc((nuint)(length + 1)));
|
||||||
|
|
||||||
|
Encoding.UTF8.GetBytes(str, new Span<byte>(bytes, length));
|
||||||
|
bytes[length] = 0;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stops the delegate pointer from being collected
|
||||||
|
private static IntPtr GetPointerTo<T>(T fn) where T : Delegate
|
||||||
|
{
|
||||||
|
if (pinned.TryGetValue(fn, out nint ptr))
|
||||||
|
{
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ptr = Marshal.GetFunctionPointerForDelegate(fn);
|
||||||
|
pinned.Add(fn, ptr);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,639 @@
|
|||||||
|
// ImGuiController with docking and viewport support for MoonWorks/Refresh.
|
||||||
|
// Based on the example im ImGui.NET and MoonWorksDearImGuiScaffold.
|
||||||
|
|
||||||
|
using ImGuiNET;
|
||||||
|
using Nerfed.Runtime.Graphics;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Gui;
|
||||||
|
|
||||||
|
public class GuiController : IDisposable
|
||||||
|
{
|
||||||
|
public event Action OnGui;
|
||||||
|
|
||||||
|
private readonly string shaderContentPath = Path.Combine(System.AppContext.BaseDirectory, "Content", "Shaders");
|
||||||
|
|
||||||
|
private readonly GraphicsDevice graphicsDevice;
|
||||||
|
private readonly Window mainWindow;
|
||||||
|
private readonly Color clearColor;
|
||||||
|
|
||||||
|
private readonly Platform_CreateWindow createWindow;
|
||||||
|
private readonly Platform_DestroyWindow destroyWindow;
|
||||||
|
private readonly Platform_GetWindowPos getWindowPos;
|
||||||
|
private readonly Platform_ShowWindow showWindow;
|
||||||
|
private readonly Platform_SetWindowPos setWindowPos;
|
||||||
|
private readonly Platform_SetWindowSize setWindowSize;
|
||||||
|
private readonly Platform_GetWindowSize getWindowSize;
|
||||||
|
private readonly Platform_SetWindowFocus setWindowFocus;
|
||||||
|
private readonly Platform_GetWindowFocus getWindowFocus;
|
||||||
|
private readonly Platform_GetWindowMinimized getWindowMinimized;
|
||||||
|
private readonly Platform_SetWindowTitle setWindowTitle;
|
||||||
|
|
||||||
|
private readonly ResourceUploader resourceUploader;
|
||||||
|
private readonly GraphicsPipeline imGuiPipeline;
|
||||||
|
private readonly Shader imGuiVertexShader;
|
||||||
|
private readonly Shader imGuiFragmentShader;
|
||||||
|
private readonly Sampler imGuiSampler;
|
||||||
|
private readonly GuiTextureStorage textureStorage = new GuiTextureStorage();
|
||||||
|
private readonly GuiViewportWindow mainViewportWindow;
|
||||||
|
|
||||||
|
private Texture fontTexture = null;
|
||||||
|
private uint vertexCount = 0;
|
||||||
|
private uint indexCount = 0;
|
||||||
|
private Graphics.Buffer imGuiVertexBuffer = null;
|
||||||
|
private Graphics.Buffer imGuiIndexBuffer = null;
|
||||||
|
private bool frameBegun = false;
|
||||||
|
|
||||||
|
public GuiController(GraphicsDevice graphicsDevice, Window mainWindow, Color clearColor, ImGuiConfigFlags configFlags = ImGuiConfigFlags.NavEnableKeyboard | ImGuiConfigFlags.DockingEnable | ImGuiConfigFlags.ViewportsEnable)
|
||||||
|
{
|
||||||
|
this.graphicsDevice = graphicsDevice;
|
||||||
|
this.mainWindow = mainWindow;
|
||||||
|
this.clearColor = clearColor;
|
||||||
|
|
||||||
|
resourceUploader = new ResourceUploader(graphicsDevice);
|
||||||
|
|
||||||
|
ImGui.CreateContext();
|
||||||
|
|
||||||
|
ImGuiIOPtr io = ImGui.GetIO();
|
||||||
|
io.DisplaySize = new Vector2(mainWindow.Width, mainWindow.Height);
|
||||||
|
io.DisplayFramebufferScale = Vector2.One;
|
||||||
|
|
||||||
|
imGuiVertexShader = ResourceManager.Load<Shader>("Shaders/ImGui.vert");
|
||||||
|
imGuiFragmentShader = ResourceManager.Load<Shader>("Shaders/ImGui.frag");
|
||||||
|
|
||||||
|
imGuiSampler = new Sampler(graphicsDevice, SamplerCreateInfo.LinearClamp);
|
||||||
|
|
||||||
|
imGuiPipeline = new GraphicsPipeline(
|
||||||
|
graphicsDevice,
|
||||||
|
new GraphicsPipelineCreateInfo
|
||||||
|
{
|
||||||
|
AttachmentInfo = new GraphicsPipelineAttachmentInfo(
|
||||||
|
new ColorAttachmentDescription(
|
||||||
|
mainWindow.SwapchainFormat,
|
||||||
|
ColorAttachmentBlendState.NonPremultiplied
|
||||||
|
)
|
||||||
|
),
|
||||||
|
DepthStencilState = DepthStencilState.Disable,
|
||||||
|
MultisampleState = MultisampleState.None,
|
||||||
|
PrimitiveType = PrimitiveType.TriangleList,
|
||||||
|
RasterizerState = RasterizerState.CW_CullNone,
|
||||||
|
VertexInputState = VertexInputState.CreateSingleBinding<Position2DTextureColorVertex>(),
|
||||||
|
VertexShader = imGuiVertexShader,
|
||||||
|
FragmentShader = imGuiFragmentShader,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
BuildFontAtlas();
|
||||||
|
|
||||||
|
io.ConfigFlags = configFlags;
|
||||||
|
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
io.SetClipboardTextFn = GuiClipboard.SetFnPtr;
|
||||||
|
io.GetClipboardTextFn = GuiClipboard.GetFnPtr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO();
|
||||||
|
ImGuiViewportPtr mainViewport = platformIO.Viewports[0];
|
||||||
|
mainViewport.PlatformHandle = mainWindow.Handle;
|
||||||
|
mainViewportWindow = new GuiViewportWindow(graphicsDevice, mainViewport, mainWindow);
|
||||||
|
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
createWindow = CreateWindow;
|
||||||
|
destroyWindow = DestroyWindow;
|
||||||
|
getWindowPos = GetWindowPos;
|
||||||
|
showWindow = ShowWindow;
|
||||||
|
setWindowPos = SetWindowPos;
|
||||||
|
setWindowSize = SetWindowSize;
|
||||||
|
getWindowSize = GetWindowSize;
|
||||||
|
setWindowFocus = SetWindowFocus;
|
||||||
|
getWindowFocus = GetWindowFocus;
|
||||||
|
getWindowMinimized = GetWindowMinimized;
|
||||||
|
setWindowTitle = SetWindowTitle;
|
||||||
|
|
||||||
|
platformIO.Platform_CreateWindow = Marshal.GetFunctionPointerForDelegate(createWindow);
|
||||||
|
platformIO.Platform_DestroyWindow = Marshal.GetFunctionPointerForDelegate(destroyWindow);
|
||||||
|
platformIO.Platform_ShowWindow = Marshal.GetFunctionPointerForDelegate(showWindow);
|
||||||
|
platformIO.Platform_SetWindowPos = Marshal.GetFunctionPointerForDelegate(setWindowPos);
|
||||||
|
platformIO.Platform_SetWindowSize = Marshal.GetFunctionPointerForDelegate(setWindowSize);
|
||||||
|
platformIO.Platform_SetWindowFocus = Marshal.GetFunctionPointerForDelegate(setWindowFocus);
|
||||||
|
platformIO.Platform_GetWindowFocus = Marshal.GetFunctionPointerForDelegate(getWindowFocus);
|
||||||
|
platformIO.Platform_GetWindowMinimized = Marshal.GetFunctionPointerForDelegate(getWindowMinimized);
|
||||||
|
platformIO.Platform_SetWindowTitle = Marshal.GetFunctionPointerForDelegate(setWindowTitle);
|
||||||
|
|
||||||
|
ImGuiNative.ImGuiPlatformIO_Set_Platform_GetWindowPos(platformIO.NativePtr, Marshal.GetFunctionPointerForDelegate(getWindowPos));
|
||||||
|
ImGuiNative.ImGuiPlatformIO_Set_Platform_GetWindowSize(platformIO.NativePtr, Marshal.GetFunctionPointerForDelegate(getWindowSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors;
|
||||||
|
io.BackendFlags |= ImGuiBackendFlags.HasSetMousePos;
|
||||||
|
io.BackendFlags |= ImGuiBackendFlags.PlatformHasViewports;
|
||||||
|
io.BackendFlags |= ImGuiBackendFlags.RendererHasViewports;
|
||||||
|
|
||||||
|
UpdatePerFrameImGuiData(1.0 / 60.0);
|
||||||
|
ImGui.NewFrame();
|
||||||
|
frameBegun = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(double deltaTime)
|
||||||
|
{
|
||||||
|
if (frameBegun)
|
||||||
|
{
|
||||||
|
ImGui.Render();
|
||||||
|
ImGui.UpdatePlatformWindows();
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatePerFrameImGuiData(deltaTime);
|
||||||
|
UpdateInput();
|
||||||
|
UpdateCursor();
|
||||||
|
UpdateMonitors();
|
||||||
|
|
||||||
|
frameBegun = true;
|
||||||
|
ImGui.NewFrame();
|
||||||
|
|
||||||
|
OnGui?.Invoke();
|
||||||
|
|
||||||
|
ImGui.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePerFrameImGuiData(double deltaSeconds)
|
||||||
|
{
|
||||||
|
ImGuiIOPtr io = ImGui.GetIO();
|
||||||
|
io.DisplaySize = new Vector2(mainWindow.Width, mainWindow.Height);
|
||||||
|
io.DisplayFramebufferScale = new Vector2(1, 1);
|
||||||
|
io.DeltaTime = (float)deltaSeconds; // DeltaTime is in seconds.
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateInput()
|
||||||
|
{
|
||||||
|
ImGuiIOPtr io = ImGui.GetIO();
|
||||||
|
|
||||||
|
if ((ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
|
||||||
|
{
|
||||||
|
// For viewports we use the global mouse position.
|
||||||
|
_ = SDL2.SDL.SDL_GetGlobalMouseState(out int x, out int y);
|
||||||
|
io.MousePos = new Vector2(x, y);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Without viewports we need to use the relative position.
|
||||||
|
//_ = SDL2.SDL.SDL_GetMouseState(out int x, out int y);
|
||||||
|
io.MousePos = Mouse.Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
io.MouseDown[0] = Mouse.IsButtonDown(MouseButton.Left);
|
||||||
|
io.MouseDown[1] = Mouse.IsButtonDown(MouseButton.Right);
|
||||||
|
io.MouseDown[2] = Mouse.IsButtonDown(MouseButton.Middle);
|
||||||
|
|
||||||
|
io.MouseWheel = Mouse.GetWheel();
|
||||||
|
|
||||||
|
io.AddKeyEvent(ImGuiKey.A, Keyboard.IsKeyDown(Key.A));
|
||||||
|
io.AddKeyEvent(ImGuiKey.Z, Keyboard.IsKeyDown(Key.Z));
|
||||||
|
io.AddKeyEvent(ImGuiKey.Y, Keyboard.IsKeyDown(Key.Y));
|
||||||
|
io.AddKeyEvent(ImGuiKey.X, Keyboard.IsKeyDown(Key.X));
|
||||||
|
io.AddKeyEvent(ImGuiKey.C, Keyboard.IsKeyDown(Key.C));
|
||||||
|
io.AddKeyEvent(ImGuiKey.V, Keyboard.IsKeyDown(Key.V));
|
||||||
|
|
||||||
|
io.AddKeyEvent(ImGuiKey.Tab, Keyboard.IsKeyDown(Key.Tab));
|
||||||
|
io.AddKeyEvent(ImGuiKey.LeftArrow, Keyboard.IsKeyDown(Key.Left));
|
||||||
|
io.AddKeyEvent(ImGuiKey.RightArrow, Keyboard.IsKeyDown(Key.Right));
|
||||||
|
io.AddKeyEvent(ImGuiKey.UpArrow, Keyboard.IsKeyDown(Key.Up));
|
||||||
|
io.AddKeyEvent(ImGuiKey.DownArrow, Keyboard.IsKeyDown(Key.Down));
|
||||||
|
io.AddKeyEvent(ImGuiKey.Enter, Keyboard.IsKeyDown(Key.Enter));
|
||||||
|
io.AddKeyEvent(ImGuiKey.Escape, Keyboard.IsKeyDown(Key.Escape));
|
||||||
|
io.AddKeyEvent(ImGuiKey.Delete, Keyboard.IsKeyDown(Key.Delete));
|
||||||
|
io.AddKeyEvent(ImGuiKey.Backspace, Keyboard.IsKeyDown(Key.Backspace));
|
||||||
|
io.AddKeyEvent(ImGuiKey.Home, Keyboard.IsKeyDown(Key.Home));
|
||||||
|
io.AddKeyEvent(ImGuiKey.End, Keyboard.IsKeyDown(Key.End));
|
||||||
|
io.AddKeyEvent(ImGuiKey.PageDown, Keyboard.IsKeyDown(Key.PageDown));
|
||||||
|
io.AddKeyEvent(ImGuiKey.PageUp, Keyboard.IsKeyDown(Key.PageUp));
|
||||||
|
io.AddKeyEvent(ImGuiKey.Insert, Keyboard.IsKeyDown(Key.Insert));
|
||||||
|
|
||||||
|
io.AddKeyEvent(ImGuiKey.ModCtrl, Keyboard.IsKeyDown(Key.LeftControl) || Keyboard.IsKeyDown(Key.RightControl));
|
||||||
|
io.AddKeyEvent(ImGuiKey.ModShift, Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift));
|
||||||
|
io.AddKeyEvent(ImGuiKey.ModAlt, Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt));
|
||||||
|
io.AddKeyEvent(ImGuiKey.ModSuper, Keyboard.IsKeyDown(Key.LeftSuper) || Keyboard.IsKeyDown(Key.RightSuper));
|
||||||
|
|
||||||
|
ReadOnlySpan<char> input = Keyboard.GetTextInput();
|
||||||
|
if (!input.IsEmpty)
|
||||||
|
{
|
||||||
|
foreach (char c in input)
|
||||||
|
{
|
||||||
|
if (c == '\t')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
io.AddInputCharacter(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCursor()
|
||||||
|
{
|
||||||
|
ImGuiIOPtr io = ImGui.GetIO();
|
||||||
|
|
||||||
|
if ((io.ConfigFlags & ImGuiConfigFlags.NoMouseCursorChange) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiMouseCursor imGuiCursor = ImGui.GetMouseCursor();
|
||||||
|
|
||||||
|
if (imGuiCursor == ImGuiMouseCursor.None || io.MouseDrawCursor)
|
||||||
|
{
|
||||||
|
_ = SDL2.SDL.SDL_ShowCursor(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nint sdlCursor = imGuiCursor switch
|
||||||
|
{
|
||||||
|
ImGuiMouseCursor.Arrow => SDL2.SDL.SDL_CreateSystemCursor(SDL2.SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_ARROW),
|
||||||
|
ImGuiMouseCursor.TextInput => SDL2.SDL.SDL_CreateSystemCursor(SDL2.SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_IBEAM),
|
||||||
|
ImGuiMouseCursor.ResizeAll => SDL2.SDL.SDL_CreateSystemCursor(SDL2.SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZEALL),
|
||||||
|
ImGuiMouseCursor.ResizeNS => SDL2.SDL.SDL_CreateSystemCursor(SDL2.SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENS),
|
||||||
|
ImGuiMouseCursor.ResizeEW => SDL2.SDL.SDL_CreateSystemCursor(SDL2.SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZEWE),
|
||||||
|
ImGuiMouseCursor.ResizeNESW => SDL2.SDL.SDL_CreateSystemCursor(SDL2.SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENESW),
|
||||||
|
ImGuiMouseCursor.ResizeNWSE => SDL2.SDL.SDL_CreateSystemCursor(SDL2.SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENWSE),
|
||||||
|
ImGuiMouseCursor.Hand => SDL2.SDL.SDL_CreateSystemCursor(SDL2.SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_HAND),
|
||||||
|
ImGuiMouseCursor.NotAllowed => SDL2.SDL.SDL_CreateSystemCursor(SDL2.SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_NO),
|
||||||
|
_ => SDL2.SDL.SDL_CreateSystemCursor(SDL2.SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_ARROW),
|
||||||
|
};
|
||||||
|
SDL2.SDL.SDL_SetCursor(sdlCursor);
|
||||||
|
_ = SDL2.SDL.SDL_ShowCursor(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void UpdateMonitors()
|
||||||
|
{
|
||||||
|
ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO();
|
||||||
|
Marshal.FreeHGlobal(platformIO.NativePtr->Monitors.Data);
|
||||||
|
int videoDisplayCount = SDL2.SDL.SDL_GetNumVideoDisplays();
|
||||||
|
IntPtr data = Marshal.AllocHGlobal(Unsafe.SizeOf<ImGuiPlatformMonitor>() * videoDisplayCount);
|
||||||
|
platformIO.NativePtr->Monitors = new ImVector(videoDisplayCount, videoDisplayCount, data);
|
||||||
|
|
||||||
|
for (int i = 0; i < videoDisplayCount; i++)
|
||||||
|
{
|
||||||
|
_ = SDL2.SDL.SDL_GetDisplayUsableBounds(i, out SDL2.SDL.SDL_Rect usableBounds);
|
||||||
|
_ = SDL2.SDL.SDL_GetDisplayBounds(i, out SDL2.SDL.SDL_Rect bounds);
|
||||||
|
_ = SDL2.SDL.SDL_GetDisplayDPI(i, out float ddpi, out float hdpi, out float vdpi);
|
||||||
|
ImGuiPlatformMonitorPtr monitor = platformIO.Monitors[i];
|
||||||
|
float standardDpi = 96f; // Standard DPI typically used
|
||||||
|
monitor.DpiScale = hdpi / standardDpi;
|
||||||
|
monitor.MainPos = new Vector2(bounds.x, bounds.y);
|
||||||
|
monitor.MainSize = new Vector2(bounds.w, bounds.h);
|
||||||
|
monitor.WorkPos = new Vector2(usableBounds.x, usableBounds.y);
|
||||||
|
monitor.WorkSize = new Vector2(usableBounds.w, usableBounds.h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Render()
|
||||||
|
{
|
||||||
|
if (!frameBegun)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
frameBegun = false;
|
||||||
|
|
||||||
|
if ((ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
|
||||||
|
{
|
||||||
|
ImGui.Render();
|
||||||
|
ImGui.UpdatePlatformWindows();
|
||||||
|
ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO();
|
||||||
|
|
||||||
|
for (int i = 0; i < platformIO.Viewports.Size; i++)
|
||||||
|
{
|
||||||
|
ImGuiViewportPtr vp = platformIO.Viewports[i];
|
||||||
|
GuiViewportWindow window = (GuiViewportWindow)GCHandle.FromIntPtr(vp.PlatformUserData).Target;
|
||||||
|
|
||||||
|
if (!window.Window.Claimed)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateImGuiBuffers(vp.DrawData);
|
||||||
|
|
||||||
|
CommandBuffer commandBuffer = graphicsDevice.AcquireCommandBuffer();
|
||||||
|
Texture swapchainTexture = commandBuffer.AcquireSwapchainTexture(window.Window);
|
||||||
|
|
||||||
|
if (swapchainTexture != null)
|
||||||
|
{
|
||||||
|
RenderCommandLists(commandBuffer, swapchainTexture, vp.DrawData);
|
||||||
|
graphicsDevice.Submit(commandBuffer);
|
||||||
|
//graphicsDevice.Wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.Render();
|
||||||
|
|
||||||
|
if (!mainWindow.Claimed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImDrawDataPtr drawDataPtr = ImGui.GetDrawData();
|
||||||
|
UpdateImGuiBuffers(drawDataPtr);
|
||||||
|
|
||||||
|
CommandBuffer commandBuffer = graphicsDevice.AcquireCommandBuffer();
|
||||||
|
Texture swapchainTexture = commandBuffer.AcquireSwapchainTexture(mainWindow);
|
||||||
|
|
||||||
|
if (swapchainTexture != null)
|
||||||
|
{
|
||||||
|
RenderCommandLists(commandBuffer, swapchainTexture, drawDataPtr);
|
||||||
|
graphicsDevice.Submit(commandBuffer);
|
||||||
|
//graphicsDevice.Wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void UpdateImGuiBuffers(ImDrawDataPtr drawDataPtr)
|
||||||
|
{
|
||||||
|
if (drawDataPtr.TotalVtxCount == 0 || drawDataPtr.CmdListsCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawDataPtr.TotalVtxCount > vertexCount)
|
||||||
|
{
|
||||||
|
imGuiVertexBuffer?.Dispose();
|
||||||
|
|
||||||
|
vertexCount = (uint)(drawDataPtr.TotalVtxCount * 1.5f);
|
||||||
|
imGuiVertexBuffer = Graphics.Buffer.Create<Position2DTextureColorVertex>(
|
||||||
|
graphicsDevice,
|
||||||
|
BufferUsageFlags.Vertex,
|
||||||
|
vertexCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawDataPtr.TotalIdxCount > indexCount)
|
||||||
|
{
|
||||||
|
imGuiIndexBuffer?.Dispose();
|
||||||
|
|
||||||
|
indexCount = (uint)(drawDataPtr.TotalIdxCount * 1.5f);
|
||||||
|
imGuiIndexBuffer = Graphics.Buffer.Create<ushort>(
|
||||||
|
graphicsDevice,
|
||||||
|
BufferUsageFlags.Index,
|
||||||
|
indexCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint vertexOffset = 0;
|
||||||
|
uint indexOffset = 0;
|
||||||
|
|
||||||
|
for (int n = 0; n < drawDataPtr.CmdListsCount; n++)
|
||||||
|
{
|
||||||
|
ImDrawListPtr cmdList = drawDataPtr.CmdLists[n];
|
||||||
|
|
||||||
|
resourceUploader.SetBufferData(
|
||||||
|
imGuiVertexBuffer,
|
||||||
|
vertexOffset,
|
||||||
|
new Span<Position2DTextureColorVertex>(cmdList.VtxBuffer.Data.ToPointer(), cmdList.VtxBuffer.Size),
|
||||||
|
n == 0
|
||||||
|
);
|
||||||
|
|
||||||
|
resourceUploader.SetBufferData(
|
||||||
|
imGuiIndexBuffer,
|
||||||
|
indexOffset,
|
||||||
|
new Span<ushort>(cmdList.IdxBuffer.Data.ToPointer(), cmdList.IdxBuffer.Size),
|
||||||
|
n == 0
|
||||||
|
);
|
||||||
|
|
||||||
|
vertexOffset += (uint)cmdList.VtxBuffer.Size;
|
||||||
|
indexOffset += (uint)cmdList.IdxBuffer.Size;
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceUploader.Upload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderCommandLists(CommandBuffer commandBuffer, Texture renderTexture, ImDrawDataPtr drawDataPtr)
|
||||||
|
{
|
||||||
|
Vector2 pos = drawDataPtr.DisplayPos;
|
||||||
|
|
||||||
|
RenderPass renderPass = commandBuffer.BeginRenderPass(
|
||||||
|
new ColorAttachmentInfo(renderTexture, false, clearColor)
|
||||||
|
);
|
||||||
|
|
||||||
|
renderPass.BindGraphicsPipeline(imGuiPipeline);
|
||||||
|
|
||||||
|
// It is possible that the buffers are null (for example nothing is in our main windows viewport, then we exixt early but still clear it).
|
||||||
|
if (imGuiVertexBuffer == null || imGuiIndexBuffer == null)
|
||||||
|
{
|
||||||
|
commandBuffer.EndRenderPass(renderPass);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix4x4 projectionMatrix = Matrix4x4.CreateOrthographicOffCenter(
|
||||||
|
pos.X,
|
||||||
|
pos.X + drawDataPtr.DisplaySize.X,
|
||||||
|
pos.Y + drawDataPtr.DisplaySize.Y,
|
||||||
|
pos.Y,
|
||||||
|
-1.0f,
|
||||||
|
1.0f
|
||||||
|
);
|
||||||
|
TransformVertexUniform vertexUniform = new TransformVertexUniform(projectionMatrix);
|
||||||
|
|
||||||
|
renderPass.BindVertexBuffer(imGuiVertexBuffer);
|
||||||
|
renderPass.BindIndexBuffer(imGuiIndexBuffer, IndexElementSize.Sixteen);
|
||||||
|
|
||||||
|
commandBuffer.PushVertexUniformData(in vertexUniform);
|
||||||
|
|
||||||
|
uint vertexOffset = 0;
|
||||||
|
uint indexOffset = 0;
|
||||||
|
|
||||||
|
for (int n = 0; n < drawDataPtr.CmdListsCount; n++)
|
||||||
|
{
|
||||||
|
ImDrawListPtr cmdList = drawDataPtr.CmdLists[n];
|
||||||
|
|
||||||
|
for (int cmdIndex = 0; cmdIndex < cmdList.CmdBuffer.Size; cmdIndex++)
|
||||||
|
{
|
||||||
|
ImDrawCmdPtr drawCmd = cmdList.CmdBuffer[cmdIndex];
|
||||||
|
|
||||||
|
Texture texture = textureStorage.GetTexture(drawCmd.TextureId);
|
||||||
|
|
||||||
|
if (texture == null)
|
||||||
|
{
|
||||||
|
Log.Error("Texture or drawCmd.TextureId became null. Fit it!");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPass.BindFragmentSampler(new TextureSamplerBinding(texture, imGuiSampler));
|
||||||
|
|
||||||
|
float width = drawCmd.ClipRect.Z - (int)drawCmd.ClipRect.X;
|
||||||
|
float height = drawCmd.ClipRect.W - (int)drawCmd.ClipRect.Y;
|
||||||
|
|
||||||
|
if (width <= 0 || height <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPass.SetScissor(
|
||||||
|
new Rect(
|
||||||
|
(int)drawCmd.ClipRect.X - (int)pos.X,
|
||||||
|
(int)drawCmd.ClipRect.Y - (int)pos.Y,
|
||||||
|
(int)drawCmd.ClipRect.Z - (int)drawCmd.ClipRect.X,
|
||||||
|
(int)drawCmd.ClipRect.W - (int)drawCmd.ClipRect.Y
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
renderPass.DrawIndexedPrimitives(vertexOffset, indexOffset, drawCmd.ElemCount / 3);
|
||||||
|
|
||||||
|
indexOffset += drawCmd.ElemCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
vertexOffset += (uint)cmdList.VtxBuffer.Size;
|
||||||
|
}
|
||||||
|
|
||||||
|
commandBuffer.EndRenderPass(renderPass);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Resources
|
||||||
|
private unsafe void BuildFontAtlas()
|
||||||
|
{
|
||||||
|
ResourceUploader resourceUploader = new ResourceUploader(graphicsDevice);
|
||||||
|
|
||||||
|
ImGuiIOPtr io = ImGui.GetIO();
|
||||||
|
|
||||||
|
io.Fonts.GetTexDataAsRGBA32(
|
||||||
|
out nint pixelData,
|
||||||
|
out int width,
|
||||||
|
out int height,
|
||||||
|
out int bytesPerPixel
|
||||||
|
);
|
||||||
|
|
||||||
|
Texture fontTexture = resourceUploader.CreateTexture2D(
|
||||||
|
new Span<byte>((void*)pixelData, width * height * bytesPerPixel),
|
||||||
|
(uint)width,
|
||||||
|
(uint)height
|
||||||
|
);
|
||||||
|
|
||||||
|
resourceUploader.Upload();
|
||||||
|
resourceUploader.Dispose();
|
||||||
|
|
||||||
|
io.Fonts.SetTexID(fontTexture.Handle);
|
||||||
|
io.Fonts.ClearTexData();
|
||||||
|
|
||||||
|
textureStorage.Add(fontTexture); // <-- The fontTexture seems to get lost after some time (CG?).
|
||||||
|
this.fontTexture = fontTexture; // <-- So we also keep a reference to make sure it doesn't happen.
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Window
|
||||||
|
private void CreateWindow(ImGuiViewportPtr vp)
|
||||||
|
{
|
||||||
|
GuiViewportWindow window = new GuiViewportWindow(graphicsDevice, vp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DestroyWindow(ImGuiViewportPtr vp)
|
||||||
|
{
|
||||||
|
if (vp.PlatformUserData == IntPtr.Zero) return;
|
||||||
|
|
||||||
|
GuiViewportWindow window = (GuiViewportWindow)GCHandle.FromIntPtr(vp.PlatformUserData).Target;
|
||||||
|
window.Dispose();
|
||||||
|
|
||||||
|
vp.PlatformUserData = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowWindow(ImGuiViewportPtr vp)
|
||||||
|
{
|
||||||
|
if (vp.PlatformUserData == IntPtr.Zero) return;
|
||||||
|
|
||||||
|
GuiViewportWindow window = (GuiViewportWindow)GCHandle.FromIntPtr(vp.PlatformUserData).Target;
|
||||||
|
SDL2.SDL.SDL_ShowWindow(window.Window.Handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void GetWindowPos(ImGuiViewportPtr vp, Vector2* outPos)
|
||||||
|
{
|
||||||
|
if (vp.PlatformUserData == IntPtr.Zero) return;
|
||||||
|
|
||||||
|
GuiViewportWindow window = (GuiViewportWindow)GCHandle.FromIntPtr(vp.PlatformUserData).Target;
|
||||||
|
SDL2.SDL.SDL_GetWindowPosition(window.Window.Handle, out int x, out int y);
|
||||||
|
*outPos = new Vector2(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetWindowPos(ImGuiViewportPtr vp, Vector2 pos)
|
||||||
|
{
|
||||||
|
if (vp.PlatformUserData == IntPtr.Zero) return;
|
||||||
|
|
||||||
|
GuiViewportWindow window = (GuiViewportWindow)GCHandle.FromIntPtr(vp.PlatformUserData).Target;
|
||||||
|
SDL2.SDL.SDL_SetWindowPosition(window.Window.Handle, (int)pos.X, (int)pos.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetWindowSize(ImGuiViewportPtr vp, Vector2 size)
|
||||||
|
{
|
||||||
|
if (vp.PlatformUserData == IntPtr.Zero) return;
|
||||||
|
|
||||||
|
GuiViewportWindow window = (GuiViewportWindow)GCHandle.FromIntPtr(vp.PlatformUserData).Target;
|
||||||
|
SDL2.SDL.SDL_SetWindowSize(window.Window.Handle, (int)size.X, (int)size.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void GetWindowSize(ImGuiViewportPtr vp, Vector2* outSize)
|
||||||
|
{
|
||||||
|
if (vp.PlatformUserData == IntPtr.Zero) return;
|
||||||
|
|
||||||
|
GuiViewportWindow window = (GuiViewportWindow)GCHandle.FromIntPtr(vp.PlatformUserData).Target;
|
||||||
|
SDL2.SDL.SDL_GetWindowSize(window.Window.Handle, out int w, out int h);
|
||||||
|
*outSize = new Vector2(w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetWindowFocus(ImGuiViewportPtr vp)
|
||||||
|
{
|
||||||
|
if (vp.PlatformUserData == IntPtr.Zero) return;
|
||||||
|
|
||||||
|
GuiViewportWindow window = (GuiViewportWindow)GCHandle.FromIntPtr(vp.PlatformUserData).Target;
|
||||||
|
//SDL2.SDL.SDL_SetWindowInputFocus(window.Handle);
|
||||||
|
SDL2.SDL.SDL_RaiseWindow(window.Window.Handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte GetWindowFocus(ImGuiViewportPtr vp)
|
||||||
|
{
|
||||||
|
if (vp.PlatformUserData == IntPtr.Zero) return (byte)0;
|
||||||
|
|
||||||
|
GuiViewportWindow window = (GuiViewportWindow)GCHandle.FromIntPtr(vp.PlatformUserData).Target;
|
||||||
|
SDL2.SDL.SDL_WindowFlags flags = (SDL2.SDL.SDL_WindowFlags)SDL2.SDL.SDL_GetWindowFlags(window.Window.Handle);
|
||||||
|
|
||||||
|
return (flags & SDL2.SDL.SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS) != 0 ? (byte)1 : (byte)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte GetWindowMinimized(ImGuiViewportPtr vp)
|
||||||
|
{
|
||||||
|
if (vp.PlatformUserData == IntPtr.Zero) return 0;
|
||||||
|
|
||||||
|
GuiViewportWindow window = (GuiViewportWindow)GCHandle.FromIntPtr(vp.PlatformUserData).Target;
|
||||||
|
SDL2.SDL.SDL_WindowFlags flags = (SDL2.SDL.SDL_WindowFlags)SDL2.SDL.SDL_GetWindowFlags(window.Window.Handle);
|
||||||
|
|
||||||
|
return (flags & SDL2.SDL.SDL_WindowFlags.SDL_WINDOW_MINIMIZED) != 0 ? (byte)1 : (byte)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void SetWindowTitle(ImGuiViewportPtr vp, IntPtr title)
|
||||||
|
{
|
||||||
|
if (vp.PlatformUserData == IntPtr.Zero) return;
|
||||||
|
|
||||||
|
GuiViewportWindow window = (GuiViewportWindow)GCHandle.FromIntPtr(vp.PlatformUserData).Target;
|
||||||
|
byte* titlePtr = (byte*)title;
|
||||||
|
int count = 0;
|
||||||
|
while (titlePtr[count] != 0)
|
||||||
|
{
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
SDL2.SDL.SDL_SetWindowTitle(window.Window.Handle, System.Text.Encoding.ASCII.GetString(titlePtr, count));
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
fontTexture?.Dispose();
|
||||||
|
imGuiVertexBuffer?.Dispose();
|
||||||
|
imGuiIndexBuffer?.Dispose();
|
||||||
|
ResourceManager.Unload(imGuiVertexShader);
|
||||||
|
ResourceManager.Unload(imGuiFragmentShader);
|
||||||
|
imGuiPipeline?.Dispose();
|
||||||
|
imGuiSampler?.Dispose();
|
||||||
|
resourceUploader?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using Nerfed.Runtime.Graphics;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Gui;
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct Position2DTextureColorVertex(Vector2 position, Vector2 texcoord, Color color) : IVertexType
|
||||||
|
{
|
||||||
|
public Vector2 Position = position;
|
||||||
|
public Vector2 TexCoord = texcoord;
|
||||||
|
public Color Color = color;
|
||||||
|
|
||||||
|
public static VertexElementFormat[] Formats { get; } = new VertexElementFormat[3]
|
||||||
|
{
|
||||||
|
VertexElementFormat.Vector2,
|
||||||
|
VertexElementFormat.Vector2,
|
||||||
|
VertexElementFormat.Color
|
||||||
|
};
|
||||||
|
|
||||||
|
public static uint[] Offsets { get; } = new uint[3]
|
||||||
|
{
|
||||||
|
0,
|
||||||
|
8,
|
||||||
|
16
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct TransformVertexUniform(Matrix4x4 projectionMatrix)
|
||||||
|
{
|
||||||
|
public Matrix4x4 ProjectionMatrix = projectionMatrix;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Nerfed.Runtime.Graphics;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Gui;
|
||||||
|
|
||||||
|
public class GuiTextureStorage
|
||||||
|
{
|
||||||
|
private readonly Dictionary<IntPtr, WeakReference<Texture>> pointerToTexture = new Dictionary<IntPtr, WeakReference<Texture>>();
|
||||||
|
|
||||||
|
public IntPtr Add(Texture texture)
|
||||||
|
{
|
||||||
|
if (!pointerToTexture.ContainsKey(texture.Handle))
|
||||||
|
{
|
||||||
|
pointerToTexture.Add(texture.Handle, new WeakReference<Texture>(texture));
|
||||||
|
}
|
||||||
|
return texture.Handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Texture GetTexture(IntPtr pointer)
|
||||||
|
{
|
||||||
|
if (!pointerToTexture.TryGetValue(pointer, out WeakReference<Texture> value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
WeakReference<Texture> result = value;
|
||||||
|
|
||||||
|
if (!result.TryGetTarget(out Texture texture))
|
||||||
|
{
|
||||||
|
pointerToTexture.Remove(pointer);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using ImGuiNET;
|
||||||
|
using Nerfed.Runtime.Graphics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Gui
|
||||||
|
{
|
||||||
|
internal class GuiViewportWindow
|
||||||
|
{
|
||||||
|
public Window Window => window;
|
||||||
|
|
||||||
|
private readonly GCHandle gcHandle;
|
||||||
|
private readonly GraphicsDevice graphicsDevice;
|
||||||
|
private readonly ImGuiViewportPtr vp;
|
||||||
|
private readonly Window window;
|
||||||
|
|
||||||
|
public GuiViewportWindow(GraphicsDevice graphicsDevice, ImGuiViewportPtr vp, Window window)
|
||||||
|
{
|
||||||
|
this.graphicsDevice = graphicsDevice;
|
||||||
|
this.vp = vp;
|
||||||
|
this.window = window;
|
||||||
|
|
||||||
|
gcHandle = GCHandle.Alloc(this);
|
||||||
|
|
||||||
|
if (!window.Claimed)
|
||||||
|
{
|
||||||
|
graphicsDevice.ClaimWindow(window, SwapchainComposition.SDR, PresentMode.Immediate); // What PresentMode do we need?
|
||||||
|
}
|
||||||
|
|
||||||
|
vp.PlatformUserData = (IntPtr)gcHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GuiViewportWindow(GraphicsDevice graphicsDevice, ImGuiViewportPtr vp)
|
||||||
|
{
|
||||||
|
this.graphicsDevice = graphicsDevice;
|
||||||
|
this.vp = vp;
|
||||||
|
|
||||||
|
gcHandle = GCHandle.Alloc(this);
|
||||||
|
|
||||||
|
// TODO: Handle all flags.
|
||||||
|
ScreenMode screenMode = ScreenMode.Windowed;
|
||||||
|
bool systemResizable = true;
|
||||||
|
|
||||||
|
if ((vp.Flags & ImGuiViewportFlags.NoDecoration) == ImGuiViewportFlags.NoDecoration)
|
||||||
|
{
|
||||||
|
screenMode = ScreenMode.WindowedBorderless;
|
||||||
|
systemResizable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowCreateInfo info = new WindowCreateInfo("Window Title", (uint)vp.Pos.X, (uint)vp.Pos.Y, screenMode, systemResizable, false);
|
||||||
|
|
||||||
|
window = new Window(graphicsDevice, info);
|
||||||
|
graphicsDevice.ClaimWindow(window, SwapchainComposition.SDR, PresentMode.Immediate); // What PresentMode do we need?
|
||||||
|
|
||||||
|
window.OnMovedEvent += HandleOnMovedEvent;
|
||||||
|
window.OnResizedEvent += HandleOnResizedEvent;
|
||||||
|
window.OnCloseEvent += HandleOnCloseEvent;
|
||||||
|
|
||||||
|
vp.PlatformUserData = (IntPtr)gcHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
window.OnMovedEvent -= HandleOnMovedEvent;
|
||||||
|
window.OnResizedEvent -= HandleOnResizedEvent;
|
||||||
|
window.OnCloseEvent -= HandleOnCloseEvent;
|
||||||
|
|
||||||
|
graphicsDevice.UnclaimWindow(window);
|
||||||
|
window.Dispose();
|
||||||
|
gcHandle.Free();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleOnMovedEvent(Window window, int x, int y)
|
||||||
|
{
|
||||||
|
vp.PlatformRequestMove = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleOnResizedEvent(Window window, uint w, uint h)
|
||||||
|
{
|
||||||
|
vp.PlatformRequestResize = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleOnCloseEvent(Window window)
|
||||||
|
{
|
||||||
|
vp.PlatformRequestClose = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Numerics;
|
using SDL2;
|
||||||
using SDL2;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace Nerfed.Runtime;
|
namespace Nerfed.Runtime;
|
||||||
|
|
||||||
@@ -65,6 +65,8 @@ internal static class Mouse
|
|||||||
|
|
||||||
internal static void Update()
|
internal static void Update()
|
||||||
{
|
{
|
||||||
|
wheelX = 0;
|
||||||
|
wheelY = 0;
|
||||||
Array.Copy(buttonStates, lastButtonStates, buttonStates.Length);
|
Array.Copy(buttonStates, lastButtonStates, buttonStates.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,8 +107,8 @@ internal static class Mouse
|
|||||||
|
|
||||||
private static void ProcessWheelEvent(ref SDL.SDL_MouseWheelEvent ev)
|
private static void ProcessWheelEvent(ref SDL.SDL_MouseWheelEvent ev)
|
||||||
{
|
{
|
||||||
wheelX += ev.x;
|
wheelX = ev.x;
|
||||||
wheelY += ev.y;
|
wheelY = ev.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ProcessMotionEvent(ref SDL.SDL_MouseMotionEvent ev)
|
private static void ProcessMotionEvent(ref SDL.SDL_MouseMotionEvent ev)
|
||||||
|
|||||||
@@ -0,0 +1,378 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime;
|
||||||
|
|
||||||
|
public enum JobDeadlineMode
|
||||||
|
{
|
||||||
|
FrameCritical,
|
||||||
|
Deferred,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum JobExecutionStatus
|
||||||
|
{
|
||||||
|
Invalid,
|
||||||
|
Queued,
|
||||||
|
Running,
|
||||||
|
Completed,
|
||||||
|
Faulted,
|
||||||
|
Canceled,
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct JobHandle : IEquatable<JobHandle>
|
||||||
|
{
|
||||||
|
internal readonly int id;
|
||||||
|
|
||||||
|
internal JobHandle(int id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsValid => id > 0;
|
||||||
|
|
||||||
|
public bool Equals(JobHandle other)
|
||||||
|
{
|
||||||
|
return id == other.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is JobHandle other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator ==(JobHandle left, JobHandle right)
|
||||||
|
{
|
||||||
|
return left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator !=(JobHandle left, JobHandle right)
|
||||||
|
{
|
||||||
|
return !left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return IsValid ? $"JobHandle({id})" : "JobHandle(Invalid)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class JobSystem : IDisposable
|
||||||
|
{
|
||||||
|
public static readonly JobSystem Default = new JobSystem();
|
||||||
|
|
||||||
|
private Thread[] workers;
|
||||||
|
private SemaphoreSlim startSignal;
|
||||||
|
private CountdownEvent completionEvent;
|
||||||
|
|
||||||
|
private Thread[] asyncWorkers;
|
||||||
|
private SemaphoreSlim asyncSignal;
|
||||||
|
private ConcurrentQueue<int> asyncQueue;
|
||||||
|
private ConcurrentDictionary<int, AsyncJobState> asyncJobStates;
|
||||||
|
private int nextAsyncJobId;
|
||||||
|
|
||||||
|
private volatile bool running;
|
||||||
|
|
||||||
|
// Shared per-dispatch state written by main thread before workers wake.
|
||||||
|
private volatile Action<int> currentAction;
|
||||||
|
private int workCount;
|
||||||
|
private int nextIndex; // grabbed with Interlocked.Increment for work-stealing
|
||||||
|
|
||||||
|
private sealed class AsyncJobState : IDisposable
|
||||||
|
{
|
||||||
|
public readonly Action Action;
|
||||||
|
public readonly JobDeadlineMode Deadline;
|
||||||
|
public readonly string Name;
|
||||||
|
public readonly ManualResetEventSlim Completion = new(false);
|
||||||
|
|
||||||
|
public ExceptionDispatchInfo CapturedException;
|
||||||
|
public volatile JobExecutionStatus Status;
|
||||||
|
public volatile bool CancellationRequested;
|
||||||
|
|
||||||
|
public AsyncJobState(Action action, JobDeadlineMode deadline, string name)
|
||||||
|
{
|
||||||
|
Action = action;
|
||||||
|
Deadline = deadline;
|
||||||
|
Name = name;
|
||||||
|
Status = JobExecutionStatus.Queued;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsTerminal =>
|
||||||
|
Status == JobExecutionStatus.Completed ||
|
||||||
|
Status == JobExecutionStatus.Faulted ||
|
||||||
|
Status == JobExecutionStatus.Canceled;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Completion.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int WorkerCount => workers?.Length ?? 0;
|
||||||
|
public int AsyncWorkerCount => asyncWorkers?.Length ?? 0;
|
||||||
|
public bool IsInitialized => workers != null;
|
||||||
|
|
||||||
|
public void Initialize(int threadCount = -1, int asyncThreadCount = -1)
|
||||||
|
{
|
||||||
|
if (IsInitialized)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("JobSystem is already initialized. Call Shutdown first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
threadCount = threadCount < 0
|
||||||
|
? Math.Max(1, Environment.ProcessorCount - 2)
|
||||||
|
: Math.Max(1, threadCount);
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
startSignal = new SemaphoreSlim(0);
|
||||||
|
completionEvent = new CountdownEvent(1);
|
||||||
|
asyncSignal = new SemaphoreSlim(0);
|
||||||
|
workers = new Thread[threadCount];
|
||||||
|
asyncQueue = new ConcurrentQueue<int>();
|
||||||
|
asyncJobStates = new ConcurrentDictionary<int, AsyncJobState>();
|
||||||
|
|
||||||
|
asyncThreadCount = asyncThreadCount < 0
|
||||||
|
? Math.Max(1, Environment.ProcessorCount >= 8 ? 2 : 1)
|
||||||
|
: Math.Max(0, asyncThreadCount);
|
||||||
|
|
||||||
|
asyncWorkers = new Thread[asyncThreadCount];
|
||||||
|
|
||||||
|
for (int i = 0; i < threadCount; i++)
|
||||||
|
{
|
||||||
|
workers[i] = new Thread(WorkerLoop)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = $"Job-{i}",
|
||||||
|
};
|
||||||
|
workers[i].Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < asyncThreadCount; i++)
|
||||||
|
{
|
||||||
|
asyncWorkers[i] = new Thread(AsyncWorkerLoop)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = $"AsyncJob-{i}",
|
||||||
|
};
|
||||||
|
asyncWorkers[i].Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public JobHandle Submit(Action action, JobDeadlineMode deadline = JobDeadlineMode.Deferred, string name = "")
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
|
|
||||||
|
if (!running)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("JobSystem is not running. Call Initialize before submitting jobs.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int id = Interlocked.Increment(ref nextAsyncJobId);
|
||||||
|
var state = new AsyncJobState(action, deadline, name ?? string.Empty);
|
||||||
|
|
||||||
|
if (!asyncJobStates.TryAdd(id, state))
|
||||||
|
{
|
||||||
|
state.Dispose();
|
||||||
|
throw new InvalidOperationException($"Failed to register async job {id}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
asyncQueue.Enqueue(id);
|
||||||
|
asyncSignal.Release();
|
||||||
|
return new JobHandle(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JobExecutionStatus GetStatus(JobHandle handle)
|
||||||
|
{
|
||||||
|
if (!handle.IsValid)
|
||||||
|
{
|
||||||
|
return JobExecutionStatus.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return asyncJobStates.TryGetValue(handle.id, out AsyncJobState state)
|
||||||
|
? state.Status
|
||||||
|
: JobExecutionStatus.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCompleted(JobHandle handle)
|
||||||
|
{
|
||||||
|
return GetStatus(handle) is JobExecutionStatus.Completed or JobExecutionStatus.Faulted or JobExecutionStatus.Canceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Cancel(JobHandle handle)
|
||||||
|
{
|
||||||
|
if (!handle.IsValid || !asyncJobStates.TryGetValue(handle.id, out AsyncJobState state))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.CancellationRequested = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Wait(JobHandle handle)
|
||||||
|
{
|
||||||
|
if (!handle.IsValid || !asyncJobStates.TryGetValue(handle.id, out AsyncJobState state))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Unknown job handle {handle}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Completion.Wait();
|
||||||
|
|
||||||
|
if (state.Status == JobExecutionStatus.Faulted)
|
||||||
|
{
|
||||||
|
state.CapturedException?.Throw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryForget(JobHandle handle)
|
||||||
|
{
|
||||||
|
if (!handle.IsValid || !asyncJobStates.TryGetValue(handle.id, out AsyncJobState state) || !state.IsTerminal)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asyncJobStates.TryRemove(handle.id, out AsyncJobState removedState))
|
||||||
|
{
|
||||||
|
removedState.Dispose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispatch(int count, Action<int> action)
|
||||||
|
{
|
||||||
|
if (count <= 0) return;
|
||||||
|
|
||||||
|
if (!IsInitialized)
|
||||||
|
{
|
||||||
|
// Safe fallback: run inline if Initialize was never called.
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
action(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAction = action;
|
||||||
|
workCount = count;
|
||||||
|
Volatile.Write(ref nextIndex, 0);
|
||||||
|
|
||||||
|
completionEvent.Reset(workers.Length);
|
||||||
|
startSignal.Release(workers.Length);
|
||||||
|
completionEvent.Wait();
|
||||||
|
|
||||||
|
currentAction = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Shutdown()
|
||||||
|
{
|
||||||
|
if (!IsInitialized) return;
|
||||||
|
|
||||||
|
running = false;
|
||||||
|
startSignal.Release(workers.Length); // wake all workers so they can see running=false and exit
|
||||||
|
|
||||||
|
foreach (Thread t in workers)
|
||||||
|
t.Join();
|
||||||
|
|
||||||
|
for (int i = 0; i < asyncWorkers.Length; i++)
|
||||||
|
{
|
||||||
|
asyncSignal.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Thread t in asyncWorkers)
|
||||||
|
{
|
||||||
|
t.Join();
|
||||||
|
}
|
||||||
|
|
||||||
|
completionEvent.Dispose();
|
||||||
|
startSignal.Dispose();
|
||||||
|
asyncSignal.Dispose();
|
||||||
|
|
||||||
|
foreach (KeyValuePair<int, AsyncJobState> pair in asyncJobStates)
|
||||||
|
{
|
||||||
|
pair.Value.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
asyncJobStates.Clear();
|
||||||
|
|
||||||
|
asyncWorkers = null;
|
||||||
|
asyncQueue = null;
|
||||||
|
asyncJobStates = null;
|
||||||
|
workers = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => Shutdown();
|
||||||
|
|
||||||
|
private void WorkerLoop()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
startSignal.Wait();
|
||||||
|
if (!running) return;
|
||||||
|
|
||||||
|
Action<int> action = currentAction;
|
||||||
|
int total = workCount;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int index = Interlocked.Increment(ref nextIndex) - 1;
|
||||||
|
if (index >= total) break;
|
||||||
|
action(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
completionEvent.Signal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AsyncWorkerLoop()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
asyncSignal.Wait();
|
||||||
|
|
||||||
|
if (!running)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!asyncQueue.TryDequeue(out int jobId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!asyncJobStates.TryGetValue(jobId, out AsyncJobState state))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.CancellationRequested)
|
||||||
|
{
|
||||||
|
state.Status = JobExecutionStatus.Canceled;
|
||||||
|
state.Completion.Set();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Status = JobExecutionStatus.Running;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
state.Action();
|
||||||
|
state.Status = state.CancellationRequested
|
||||||
|
? JobExecutionStatus.Canceled
|
||||||
|
: JobExecutionStatus.Completed;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
state.CapturedException = ExceptionDispatchInfo.Capture(ex);
|
||||||
|
state.Status = JobExecutionStatus.Faulted;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
state.Completion.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Submodule
+1
Submodule Nerfed.Runtime/Libraries/ImGui.NET added at ae493d92a3
Submodule
+1
Submodule Nerfed.Runtime/Libraries/MoonTools.ECS added at 76b18a6ba9
@@ -1,26 +1,44 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<DefaultItemExcludes>$(DefaultItemExcludes);Libraries\**\*</DefaultItemExcludes>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="Libraries\SDL2CS\src\SDL2.cs" />
|
|
||||||
<Compile Include="Libraries\RefreshCS\RefreshCS.cs" />
|
|
||||||
<Compile Include="Libraries\FAudio\csharp\FAudio.cs" />
|
|
||||||
<Compile Include="Libraries\WellspringCS\WellspringCS.cs" />
|
|
||||||
<Compile Include="Libraries\dav1dfile\csharp\dav1dfile.cs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>disable</Nullable>
|
<Nullable>disable</Nullable>
|
||||||
<PublishAot>true</PublishAot>
|
<PublishAot>true</PublishAot>
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<Configurations>Debug;Test;Release</Configurations>
|
||||||
|
<Platforms>x64</Platforms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Import Project=".\CopyLibs.targets" />
|
<PropertyGroup>
|
||||||
|
<DefaultItemExcludes>$(DefaultItemExcludes);Libraries\**\*</DefaultItemExcludes>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Libraries\FAudio\csharp\FAudio.cs" />
|
||||||
|
<Compile Include="Libraries\ImGui.NET\src\ImGui.NET\**\*.cs" />
|
||||||
|
<Compile Include="Libraries\MoonTools.ECS\src\**\*.cs" />
|
||||||
|
<Compile Include="Libraries\RefreshCS\RefreshCS.cs" />
|
||||||
|
<Compile Include="Libraries\SDL2CS\src\SDL2.cs" />
|
||||||
|
<Compile Include="Libraries\WellspringCS\WellspringCS.cs" />
|
||||||
|
<Compile Include="Libraries\dav1dfile\csharp\dav1dfile.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<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/=resource/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||||
+795
-12
@@ -1,29 +1,812 @@
|
|||||||
using System.Diagnostics;
|
using System.Collections.Concurrent;
|
||||||
using System.Reflection;
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace Nerfed.Runtime;
|
namespace Nerfed.Runtime;
|
||||||
|
|
||||||
public struct ProfilerScope : IDisposable
|
public struct ProfilerScope : IDisposable
|
||||||
{
|
{
|
||||||
public ProfilerScope(string label) {
|
public ProfilerScope(string label)
|
||||||
|
{
|
||||||
Profiler.BeginSample(label);
|
Profiler.BeginSample(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public ProfilerScope(string label, string category, ulong tagMask = 0)
|
||||||
|
{
|
||||||
|
Profiler.BeginSample(label, category, tagMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
Profiler.EndSample();
|
Profiler.EndSample();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Profiler
|
public static class Profiler
|
||||||
{
|
{
|
||||||
[Conditional("PROFILER")]
|
public enum CaptureMode
|
||||||
public static void BeginSample(string label) {
|
{
|
||||||
|
Instrumented = 0,
|
||||||
|
SampledInstrumentation = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
[Conditional("PROFILER")]
|
private sealed class ThreadProfilerState
|
||||||
public static void EndSample() {
|
{
|
||||||
|
public readonly Stack<ScopeNode> Scopes = new Stack<ScopeNode>();
|
||||||
|
public readonly Stack<bool> CaptureDecisions = new Stack<bool>();
|
||||||
|
public int SampleCursor;
|
||||||
|
public int ThreadId;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public readonly struct LabelMetrics
|
||||||
|
{
|
||||||
|
public LabelMetrics(double inclusiveMs, double selfMs, uint calls, double minInclusiveMs, double maxInclusiveMs, long allocatedBytes, long selfAllocatedBytes)
|
||||||
|
{
|
||||||
|
InclusiveMs = inclusiveMs;
|
||||||
|
SelfMs = selfMs;
|
||||||
|
Calls = calls;
|
||||||
|
MinInclusiveMs = minInclusiveMs;
|
||||||
|
MaxInclusiveMs = maxInclusiveMs;
|
||||||
|
AllocatedBytes = allocatedBytes;
|
||||||
|
SelfAllocatedBytes = selfAllocatedBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double InclusiveMs { get; }
|
||||||
|
public double SelfMs { get; }
|
||||||
|
public uint Calls { get; }
|
||||||
|
public double MinInclusiveMs { get; }
|
||||||
|
public double MaxInclusiveMs { get; }
|
||||||
|
public long AllocatedBytes { get; }
|
||||||
|
public long SelfAllocatedBytes { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct RollingLabelMetrics
|
||||||
|
{
|
||||||
|
public RollingLabelMetrics(double averageMs, double minMs, double maxMs, double p95Ms, int samples)
|
||||||
|
{
|
||||||
|
AverageMs = averageMs;
|
||||||
|
MinMs = minMs;
|
||||||
|
MaxMs = maxMs;
|
||||||
|
P95Ms = p95Ms;
|
||||||
|
Samples = samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double AverageMs { get; }
|
||||||
|
public double MinMs { get; }
|
||||||
|
public double MaxMs { get; }
|
||||||
|
public double P95Ms { get; }
|
||||||
|
public int Samples { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ThreadMetrics
|
||||||
|
{
|
||||||
|
public ThreadMetrics(double inclusiveMs, double selfMs, uint calls, bool overBudget)
|
||||||
|
{
|
||||||
|
InclusiveMs = inclusiveMs;
|
||||||
|
SelfMs = selfMs;
|
||||||
|
Calls = calls;
|
||||||
|
OverBudget = overBudget;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double InclusiveMs { get; }
|
||||||
|
public double SelfMs { get; }
|
||||||
|
public uint Calls { get; }
|
||||||
|
public bool OverBudget { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct RollingThreadMetrics
|
||||||
|
{
|
||||||
|
public RollingThreadMetrics(double averageMs, double p95Ms, double maxMs, int samples, int budgetMisses)
|
||||||
|
{
|
||||||
|
AverageMs = averageMs;
|
||||||
|
P95Ms = p95Ms;
|
||||||
|
MaxMs = maxMs;
|
||||||
|
Samples = samples;
|
||||||
|
BudgetMisses = budgetMisses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double AverageMs { get; }
|
||||||
|
public double P95Ms { get; }
|
||||||
|
public double MaxMs { get; }
|
||||||
|
public int Samples { get; }
|
||||||
|
public int BudgetMisses { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RollingWindow
|
||||||
|
{
|
||||||
|
private readonly double[] values;
|
||||||
|
private readonly double[] sortBuffer; // pre-allocated; avoids per-snapshot heap allocation
|
||||||
|
private int index;
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
public RollingWindow(int capacity)
|
||||||
|
{
|
||||||
|
int size = Math.Max(8, capacity);
|
||||||
|
values = new double[size];
|
||||||
|
sortBuffer = new double[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(double value)
|
||||||
|
{
|
||||||
|
values[index] = value;
|
||||||
|
index = (index + 1) % values.Length;
|
||||||
|
if (count < values.Length)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RollingLabelMetrics Snapshot()
|
||||||
|
{
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
double sum = 0;
|
||||||
|
double min = double.MaxValue;
|
||||||
|
double max = double.MinValue;
|
||||||
|
|
||||||
|
int start = (index - count + values.Length) % values.Length;
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
double value = values[(start + i) % values.Length];
|
||||||
|
sortBuffer[i] = value;
|
||||||
|
sum += value;
|
||||||
|
min = Math.Min(min, value);
|
||||||
|
max = Math.Max(max, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.Sort(sortBuffer, 0, count);
|
||||||
|
int percentileIndex = (int)Math.Ceiling((count - 1) * 0.95d);
|
||||||
|
double p95 = sortBuffer[Math.Clamp(percentileIndex, 0, count - 1)];
|
||||||
|
return new RollingLabelMetrics(sum / count, min, max, p95, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RollingThreadWindow
|
||||||
|
{
|
||||||
|
private readonly double[] durations;
|
||||||
|
private readonly double[] sortBuffer; // pre-allocated; avoids per-snapshot heap allocation
|
||||||
|
private readonly byte[] misses;
|
||||||
|
private int index;
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
public RollingThreadWindow(int capacity)
|
||||||
|
{
|
||||||
|
int size = Math.Max(8, capacity);
|
||||||
|
durations = new double[size];
|
||||||
|
sortBuffer = new double[size];
|
||||||
|
misses = new byte[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(double durationMs, bool budgetMiss)
|
||||||
|
{
|
||||||
|
durations[index] = durationMs;
|
||||||
|
misses[index] = budgetMiss ? (byte)1 : (byte)0;
|
||||||
|
index = (index + 1) % durations.Length;
|
||||||
|
if (count < durations.Length)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RollingThreadMetrics Snapshot()
|
||||||
|
{
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
double sum = 0;
|
||||||
|
double max = double.MinValue;
|
||||||
|
int budgetMisses = 0;
|
||||||
|
|
||||||
|
int start = (index - count + durations.Length) % durations.Length;
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
int at = (start + i) % durations.Length;
|
||||||
|
double value = durations[at];
|
||||||
|
sum += value;
|
||||||
|
max = Math.Max(max, value);
|
||||||
|
budgetMisses += misses[at];
|
||||||
|
sortBuffer[i] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.Sort(sortBuffer, 0, count);
|
||||||
|
int percentileIndex = (int)Math.Ceiling((count - 1) * 0.95d);
|
||||||
|
double p95 = sortBuffer[Math.Clamp(percentileIndex, 0, count - 1)];
|
||||||
|
return new RollingThreadMetrics(sum / count, p95, max, count, budgetMisses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Frame
|
||||||
|
{
|
||||||
|
private readonly List<ScopeNode> rootNodes = new List<ScopeNode>(8);
|
||||||
|
private readonly object rootNodesLock = new object();
|
||||||
|
private readonly Dictionary<string, LabelMetrics> labelMetrics = new Dictionary<string, LabelMetrics>(128, StringComparer.Ordinal);
|
||||||
|
private readonly Dictionary<string, LabelMetrics> categoryMetrics = new Dictionary<string, LabelMetrics>(32, StringComparer.Ordinal);
|
||||||
|
private readonly Dictionary<int, ThreadMetrics> threadMetrics = new Dictionary<int, ThreadMetrics>(16);
|
||||||
|
private readonly List<int> knownThreadIds = new List<int>(16); // avoids Keys.ToArray() in ApplyThreadBudgetFlags
|
||||||
|
|
||||||
|
public uint FrameCount { get; private set; }
|
||||||
|
public long StartTime { get; private set; }
|
||||||
|
public long EndTime { get; private set; }
|
||||||
|
|
||||||
|
public IReadOnlyList<ScopeNode> RootNodes => rootNodes;
|
||||||
|
public IReadOnlyDictionary<string, LabelMetrics> LabelMetrics => labelMetrics;
|
||||||
|
public IReadOnlyDictionary<string, LabelMetrics> CategoryMetrics => categoryMetrics;
|
||||||
|
public IReadOnlyDictionary<int, ThreadMetrics> ThreadMetrics => threadMetrics;
|
||||||
|
|
||||||
|
// Return concrete types so callers can use the struct enumerator and avoid boxing.
|
||||||
|
internal Dictionary<string, LabelMetrics> LabelMetricsRaw => labelMetrics;
|
||||||
|
internal Dictionary<int, ThreadMetrics> ThreadMetricsRaw => threadMetrics;
|
||||||
|
|
||||||
|
public long AllocatedBytesStart { get; private set; }
|
||||||
|
public long AllocatedBytesEnd { get; private set; }
|
||||||
|
public long AllocatedBytesDelta { get; private set; }
|
||||||
|
public int Gen0CollectionsStart { get; private set; }
|
||||||
|
public int Gen1CollectionsStart { get; private set; }
|
||||||
|
public int Gen2CollectionsStart { get; private set; }
|
||||||
|
public int Gen0CollectionsEnd { get; private set; }
|
||||||
|
public int Gen1CollectionsEnd { get; private set; }
|
||||||
|
public int Gen2CollectionsEnd { get; private set; }
|
||||||
|
public int Gen0CollectionsDelta { get; private set; }
|
||||||
|
public int Gen1CollectionsDelta { get; private set; }
|
||||||
|
public int Gen2CollectionsDelta { get; private set; }
|
||||||
|
public bool OverBudget { get; private set; }
|
||||||
|
public double BudgetMilliseconds { get; private set; }
|
||||||
|
|
||||||
|
internal void Reset(uint frameCount)
|
||||||
|
{
|
||||||
|
FrameCount = frameCount;
|
||||||
|
StartTime = Stopwatch.GetTimestamp();
|
||||||
|
EndTime = 0;
|
||||||
|
OverBudget = false;
|
||||||
|
BudgetMilliseconds = 0;
|
||||||
|
AllocatedBytesStart = GC.GetTotalAllocatedBytes(false);
|
||||||
|
AllocatedBytesEnd = 0;
|
||||||
|
AllocatedBytesDelta = 0;
|
||||||
|
Gen0CollectionsStart = GC.CollectionCount(0);
|
||||||
|
Gen1CollectionsStart = GC.CollectionCount(1);
|
||||||
|
Gen2CollectionsStart = GC.CollectionCount(2);
|
||||||
|
Gen0CollectionsEnd = 0;
|
||||||
|
Gen1CollectionsEnd = 0;
|
||||||
|
Gen2CollectionsEnd = 0;
|
||||||
|
Gen0CollectionsDelta = 0;
|
||||||
|
Gen1CollectionsDelta = 0;
|
||||||
|
Gen2CollectionsDelta = 0;
|
||||||
|
lock (rootNodesLock)
|
||||||
|
{
|
||||||
|
rootNodes.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void AddRootNode(ScopeNode rootNode)
|
||||||
|
{
|
||||||
|
lock (rootNodesLock)
|
||||||
|
{
|
||||||
|
rootNodes.Add(rootNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void End(double budgetMilliseconds)
|
||||||
|
{
|
||||||
|
EndTime = Stopwatch.GetTimestamp();
|
||||||
|
BudgetMilliseconds = budgetMilliseconds;
|
||||||
|
OverBudget = budgetMilliseconds > 0 && ElapsedMilliseconds() > budgetMilliseconds;
|
||||||
|
|
||||||
|
AllocatedBytesEnd = GC.GetTotalAllocatedBytes(false);
|
||||||
|
AllocatedBytesDelta = AllocatedBytesEnd - AllocatedBytesStart;
|
||||||
|
|
||||||
|
Gen0CollectionsEnd = GC.CollectionCount(0);
|
||||||
|
Gen1CollectionsEnd = GC.CollectionCount(1);
|
||||||
|
Gen2CollectionsEnd = GC.CollectionCount(2);
|
||||||
|
Gen0CollectionsDelta = Gen0CollectionsEnd - Gen0CollectionsStart;
|
||||||
|
Gen1CollectionsDelta = Gen1CollectionsEnd - Gen1CollectionsStart;
|
||||||
|
Gen2CollectionsDelta = Gen2CollectionsEnd - Gen2CollectionsStart;
|
||||||
|
|
||||||
|
BuildLabelMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ElapsedMilliseconds()
|
||||||
|
{
|
||||||
|
long elapsedTicks = EndTime - StartTime;
|
||||||
|
return ((double)(elapsedTicks * 1000)) / Stopwatch.Frequency;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildLabelMetrics()
|
||||||
|
{
|
||||||
|
labelMetrics.Clear();
|
||||||
|
categoryMetrics.Clear();
|
||||||
|
threadMetrics.Clear();
|
||||||
|
knownThreadIds.Clear();
|
||||||
|
lock (rootNodesLock)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < rootNodes.Count; i++)
|
||||||
|
{
|
||||||
|
AccumulateLabelMetrics(rootNodes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < rootNodes.Count; i++)
|
||||||
|
{
|
||||||
|
ScopeNode rootNode = rootNodes[i];
|
||||||
|
for (int j = 0; j < rootNode.Children.Count; j++)
|
||||||
|
{
|
||||||
|
AccumulateThreadMetrics(rootNode.ManagedThreadId, rootNode.Children[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyThreadBudgetFlags();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AccumulateLabelMetrics(ScopeNode node)
|
||||||
|
{
|
||||||
|
double inclusiveMs = node.ElapsedMilliseconds();
|
||||||
|
double selfMs = node.SelfMilliseconds();
|
||||||
|
long allocBytes = node.AllocatedBytes;
|
||||||
|
long selfAllocBytes = node.SelfAllocatedBytes();
|
||||||
|
|
||||||
|
if (labelMetrics.TryGetValue(node.Label, out LabelMetrics current))
|
||||||
|
{
|
||||||
|
labelMetrics[node.Label] = new LabelMetrics(
|
||||||
|
current.InclusiveMs + inclusiveMs,
|
||||||
|
current.SelfMs + selfMs,
|
||||||
|
current.Calls + 1,
|
||||||
|
Math.Min(current.MinInclusiveMs, inclusiveMs),
|
||||||
|
Math.Max(current.MaxInclusiveMs, inclusiveMs),
|
||||||
|
current.AllocatedBytes + allocBytes,
|
||||||
|
current.SelfAllocatedBytes + selfAllocBytes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
labelMetrics[node.Label] = new LabelMetrics(inclusiveMs, selfMs, 1, inclusiveMs, inclusiveMs, allocBytes, selfAllocBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryMetrics.TryGetValue(node.Category, out LabelMetrics categoryCurrent))
|
||||||
|
{
|
||||||
|
categoryMetrics[node.Category] = new LabelMetrics(
|
||||||
|
categoryCurrent.InclusiveMs + inclusiveMs,
|
||||||
|
categoryCurrent.SelfMs + selfMs,
|
||||||
|
categoryCurrent.Calls + 1,
|
||||||
|
Math.Min(categoryCurrent.MinInclusiveMs, inclusiveMs),
|
||||||
|
Math.Max(categoryCurrent.MaxInclusiveMs, inclusiveMs),
|
||||||
|
categoryCurrent.AllocatedBytes + allocBytes,
|
||||||
|
categoryCurrent.SelfAllocatedBytes + selfAllocBytes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
categoryMetrics[node.Category] = new LabelMetrics(inclusiveMs, selfMs, 1, inclusiveMs, inclusiveMs, allocBytes, selfAllocBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < node.Children.Count; i++)
|
||||||
|
{
|
||||||
|
AccumulateLabelMetrics(node.Children[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AccumulateThreadMetrics(int threadId, ScopeNode node)
|
||||||
|
{
|
||||||
|
double inclusiveMs = node.ElapsedMilliseconds();
|
||||||
|
double selfMs = node.SelfMilliseconds();
|
||||||
|
|
||||||
|
if (threadMetrics.TryGetValue(threadId, out ThreadMetrics current))
|
||||||
|
{
|
||||||
|
threadMetrics[threadId] = new ThreadMetrics(current.InclusiveMs + inclusiveMs, current.SelfMs + selfMs, current.Calls + 1, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
threadMetrics[threadId] = new ThreadMetrics(inclusiveMs, selfMs, 1, false);
|
||||||
|
knownThreadIds.Add(threadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < node.Children.Count; i++)
|
||||||
|
{
|
||||||
|
AccumulateThreadMetrics(threadId, node.Children[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyThreadBudgetFlags()
|
||||||
|
{
|
||||||
|
double perThreadBudget = Math.Max(0d, ThreadBudgetMilliseconds);
|
||||||
|
if (perThreadBudget <= 0d)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// knownThreadIds avoids Keys.ToArray() allocation
|
||||||
|
for (int i = 0; i < knownThreadIds.Count; i++)
|
||||||
|
{
|
||||||
|
int key = knownThreadIds[i];
|
||||||
|
ThreadMetrics metric = threadMetrics[key];
|
||||||
|
threadMetrics[key] = new ThreadMetrics(metric.InclusiveMs, metric.SelfMs, metric.Calls, metric.InclusiveMs > perThreadBudget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ScopeNode
|
||||||
|
{
|
||||||
|
public string Label { get; private set; } = string.Empty;
|
||||||
|
public string Category { get; private set; } = DefaultCategory;
|
||||||
|
public ulong TagMask { get; private set; }
|
||||||
|
public long StartTime { get; private set; }
|
||||||
|
public long EndTime { get; private set; }
|
||||||
|
public int ManagedThreadId { get; private set; }
|
||||||
|
public List<ScopeNode> Children { get; } = new List<ScopeNode>();
|
||||||
|
public long AllocatedBytes { get; private set; }
|
||||||
|
public long ProfilerSetupBytes { get; private set; }
|
||||||
|
internal ScopeNode Parent { get; private set; }
|
||||||
|
internal long ChildrenDurationTicks { get; private set; }
|
||||||
|
internal long ChildrenAllocatedBytes { get; private set; }
|
||||||
|
private long allocatedBytesAtStart;
|
||||||
|
|
||||||
|
internal void Reset(string label, string category, ulong tagMask, int managedThreadId, ScopeNode parent)
|
||||||
|
{
|
||||||
|
Label = label;
|
||||||
|
Category = string.IsNullOrEmpty(category) ? DefaultCategory : category;
|
||||||
|
TagMask = tagMask;
|
||||||
|
ManagedThreadId = managedThreadId;
|
||||||
|
Parent = parent;
|
||||||
|
StartTime = Stopwatch.GetTimestamp();
|
||||||
|
EndTime = 0;
|
||||||
|
ChildrenDurationTicks = 0;
|
||||||
|
ChildrenAllocatedBytes = 0;
|
||||||
|
AllocatedBytes = 0;
|
||||||
|
ProfilerSetupBytes = 0;
|
||||||
|
Children.Clear();
|
||||||
|
allocatedBytesAtStart = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void End()
|
||||||
|
{
|
||||||
|
if (EndTime != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EndTime = Stopwatch.GetTimestamp();
|
||||||
|
if (Parent != null)
|
||||||
|
{
|
||||||
|
// Root nodes are ended from FinalizeCurrentFrame on the main thread, so their
|
||||||
|
// GC counter would be from the wrong thread. Only track alloc on non-root nodes.
|
||||||
|
AllocatedBytes = Math.Max(0, GC.GetAllocatedBytesForCurrentThread() - allocatedBytesAtStart);
|
||||||
|
Parent.ChildrenDurationTicks += Math.Max(0, EndTime - StartTime);
|
||||||
|
Parent.ChildrenAllocatedBytes += AllocatedBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ElapsedMilliseconds()
|
||||||
|
{
|
||||||
|
return ((double)(Math.Max(0, EndTime - StartTime))) * 1000 / Stopwatch.Frequency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double SelfMilliseconds()
|
||||||
|
{
|
||||||
|
long elapsedTicks = Math.Max(0, EndTime - StartTime);
|
||||||
|
long selfTicks = Math.Max(0, elapsedTicks - ChildrenDurationTicks);
|
||||||
|
return ((double)selfTicks) * 1000 / Stopwatch.Frequency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long SelfAllocatedBytes()
|
||||||
|
{
|
||||||
|
return Math.Max(0, AllocatedBytes - ChildrenAllocatedBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called after all profiler setup (Children.Add + scopes.Push) to measure overhead within this scope's window.
|
||||||
|
internal void NoteSetupOverhead()
|
||||||
|
{
|
||||||
|
ProfilerSetupBytes = Math.Max(0, GC.GetAllocatedBytesForCurrentThread() - allocatedBytesAtStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal ScopeNode AddChild(string label, string category, ulong tagMask)
|
||||||
|
{
|
||||||
|
ScopeNode child = RentNode(label, category, tagMask, ManagedThreadId, this);
|
||||||
|
Children.Add(child);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int maxFrames = 128;
|
||||||
|
private const int rollingWindowSize = 240;
|
||||||
|
private const string DefaultCategory = "General";
|
||||||
|
|
||||||
|
public static bool IsRecording { get; private set; } = true;
|
||||||
|
public static double FrameBudgetMilliseconds { get; set; } = 16.667;
|
||||||
|
public static double ThreadBudgetMilliseconds { get; set; } = 8.333;
|
||||||
|
public static CaptureMode Mode { get; set; } = CaptureMode.Instrumented;
|
||||||
|
public static int SamplingStride { get; set; } = 8;
|
||||||
|
|
||||||
|
public static readonly BoundedQueue<Frame> Frames = new(maxFrames);
|
||||||
|
|
||||||
|
// trackAllValues=false; registeredThreadStates avoids ThreadLocal.Values allocating a ReadOnlyCollection each call
|
||||||
|
private static readonly ThreadLocal<ThreadProfilerState> threadStates =
|
||||||
|
new ThreadLocal<ThreadProfilerState>(() =>
|
||||||
|
{
|
||||||
|
ThreadProfilerState state = new ThreadProfilerState();
|
||||||
|
lock (registeredThreadStatesLock)
|
||||||
|
{
|
||||||
|
registeredThreadStates.Add(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
||||||
|
private static readonly List<ThreadProfilerState> registeredThreadStates = new List<ThreadProfilerState>(8);
|
||||||
|
private static readonly object registeredThreadStatesLock = new object();
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<int, string> threadRootLabelCache = new ConcurrentDictionary<int, string>();
|
||||||
|
private static readonly ConcurrentBag<ScopeNode> nodePool = new ConcurrentBag<ScopeNode>();
|
||||||
|
private static readonly ConcurrentBag<Frame> framePool = new ConcurrentBag<Frame>(); // pooled; avoids per-frame Frame allocation
|
||||||
|
private static readonly Dictionary<string, RollingWindow> rollingWindows = new Dictionary<string, RollingWindow>(256, StringComparer.Ordinal);
|
||||||
|
private static readonly Dictionary<int, RollingThreadWindow> rollingThreadWindows = new Dictionary<int, RollingThreadWindow>(16);
|
||||||
|
private static readonly object rollingWindowsLock = new object();
|
||||||
|
|
||||||
|
private static Frame currentFrame = null;
|
||||||
|
private static uint frameCount = 0;
|
||||||
|
|
||||||
|
public static void SetActive(bool isRecording)
|
||||||
|
{
|
||||||
|
if (IsRecording && !isRecording)
|
||||||
|
{
|
||||||
|
FinalizeCurrentFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
IsRecording = isRecording;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int CopyFramesTo(List<Frame> destination)
|
||||||
|
{
|
||||||
|
return Frames.CopyTo(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<string, RollingLabelMetrics> GetRollingLabelMetricsSnapshot()
|
||||||
|
{
|
||||||
|
lock (rollingWindowsLock)
|
||||||
|
{
|
||||||
|
Dictionary<string, RollingLabelMetrics> snapshot = new Dictionary<string, RollingLabelMetrics>(rollingWindows.Count, StringComparer.Ordinal);
|
||||||
|
foreach (KeyValuePair<string, RollingWindow> pair in rollingWindows)
|
||||||
|
{
|
||||||
|
snapshot[pair.Key] = pair.Value.Snapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<int, RollingThreadMetrics> GetRollingThreadMetricsSnapshot()
|
||||||
|
{
|
||||||
|
lock (rollingWindowsLock)
|
||||||
|
{
|
||||||
|
Dictionary<int, RollingThreadMetrics> snapshot = new Dictionary<int, RollingThreadMetrics>(rollingThreadWindows.Count);
|
||||||
|
foreach (KeyValuePair<int, RollingThreadWindow> pair in rollingThreadWindows)
|
||||||
|
{
|
||||||
|
snapshot[pair.Key] = pair.Value.Snapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("PROFILING")]
|
||||||
|
public static void BeginFrame()
|
||||||
|
{
|
||||||
|
if (!IsRecording)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentFrame != null)
|
||||||
|
{
|
||||||
|
FinalizeCurrentFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentFrame = RentFrame(frameCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("PROFILING")]
|
||||||
|
public static void EndFrame()
|
||||||
|
{
|
||||||
|
if (!IsRecording)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FinalizeCurrentFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("PROFILING")]
|
||||||
|
public static void BeginSample(string label)
|
||||||
|
{
|
||||||
|
BeginSample(label, DefaultCategory, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("PROFILING")]
|
||||||
|
public static void BeginSample(string label, string category, ulong tagMask = 0)
|
||||||
|
{
|
||||||
|
if (!IsRecording || currentFrame == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadProfilerState state = threadStates.Value;
|
||||||
|
state.ThreadId = Environment.CurrentManagedThreadId;
|
||||||
|
|
||||||
|
bool parentCaptured = state.CaptureDecisions.Count > 0 && state.CaptureDecisions.Peek();
|
||||||
|
bool capture = parentCaptured || Mode == CaptureMode.Instrumented || ShouldSample(state);
|
||||||
|
state.CaptureDecisions.Push(capture);
|
||||||
|
|
||||||
|
if (!capture)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stack<ScopeNode> scopes = state.Scopes;
|
||||||
|
Frame frame = currentFrame;
|
||||||
|
if (frame == null)
|
||||||
|
{
|
||||||
|
state.CaptureDecisions.Pop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scopes.Count == 0)
|
||||||
|
{
|
||||||
|
int threadId = state.ThreadId;
|
||||||
|
ScopeNode rootScopeNode = RentNode(GetThreadRootLabel(threadId), DefaultCategory, 0, threadId, null);
|
||||||
|
scopes.Push(rootScopeNode);
|
||||||
|
frame.AddRootNode(rootScopeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopeNode newScope = scopes.Peek().AddChild(label, category, tagMask);
|
||||||
|
scopes.Push(newScope);
|
||||||
|
newScope.NoteSetupOverhead();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("PROFILING")]
|
||||||
|
public static void EndSample()
|
||||||
|
{
|
||||||
|
if (!IsRecording || currentFrame == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadProfilerState state = threadStates.Value;
|
||||||
|
if (state.CaptureDecisions.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool captured = state.CaptureDecisions.Pop();
|
||||||
|
if (!captured)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stack<ScopeNode> scopes = state.Scopes;
|
||||||
|
|
||||||
|
if (scopes.Count > 1)
|
||||||
|
{
|
||||||
|
ScopeNode currentScope = scopes.Pop();
|
||||||
|
currentScope.End();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldSample(ThreadProfilerState state)
|
||||||
|
{
|
||||||
|
int stride = Math.Max(1, SamplingStride);
|
||||||
|
state.SampleCursor++;
|
||||||
|
return state.SampleCursor % stride == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetThreadRootLabel(int threadId)
|
||||||
|
{
|
||||||
|
return threadRootLabelCache.GetOrAdd(threadId, static id => $"Thread-{id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Frame RentFrame(uint count)
|
||||||
|
{
|
||||||
|
if (!framePool.TryTake(out Frame frame))
|
||||||
|
{
|
||||||
|
frame = new Frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.Reset(count);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ScopeNode RentNode(string label, string category, ulong tagMask, int managedThreadId, ScopeNode parent)
|
||||||
|
{
|
||||||
|
if (!nodePool.TryTake(out ScopeNode node))
|
||||||
|
{
|
||||||
|
node = new ScopeNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Reset(label, category, tagMask, managedThreadId, parent);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReturnNodeTree(ScopeNode node)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < node.Children.Count; i++)
|
||||||
|
{
|
||||||
|
ReturnNodeTree(node.Children[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Reset(string.Empty, DefaultCategory, 0, 0, null);
|
||||||
|
nodePool.Add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FinalizeCurrentFrame()
|
||||||
|
{
|
||||||
|
Frame frame = currentFrame;
|
||||||
|
if (frame == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (registeredThreadStatesLock)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < registeredThreadStates.Count; i++)
|
||||||
|
{
|
||||||
|
ThreadProfilerState state = registeredThreadStates[i];
|
||||||
|
Stack<ScopeNode> scopes = state.Scopes;
|
||||||
|
while (scopes.Count > 0)
|
||||||
|
{
|
||||||
|
scopes.Pop().End();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.CaptureDecisions.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.End(FrameBudgetMilliseconds);
|
||||||
|
|
||||||
|
if (Frames.Enqueue(frame, out Frame evictedFrame))
|
||||||
|
{
|
||||||
|
for (int i = 0; i < evictedFrame.RootNodes.Count; i++)
|
||||||
|
{
|
||||||
|
ReturnNodeTree(evictedFrame.RootNodes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
framePool.Add(evictedFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateRollingWindows(frame);
|
||||||
|
UpdateRollingThreadWindows(frame);
|
||||||
|
frameCount++;
|
||||||
|
currentFrame = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateRollingWindows(Frame frame)
|
||||||
|
{
|
||||||
|
lock (rollingWindowsLock)
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<string, LabelMetrics> pair in frame.LabelMetricsRaw)
|
||||||
|
{
|
||||||
|
if (!rollingWindows.TryGetValue(pair.Key, out RollingWindow window))
|
||||||
|
{
|
||||||
|
window = new RollingWindow(rollingWindowSize);
|
||||||
|
rollingWindows.Add(pair.Key, window);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Add(pair.Value.InclusiveMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateRollingThreadWindows(Frame frame)
|
||||||
|
{
|
||||||
|
lock (rollingWindowsLock)
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<int, ThreadMetrics> pair in frame.ThreadMetricsRaw)
|
||||||
|
{
|
||||||
|
if (!rollingThreadWindows.TryGetValue(pair.Key, out RollingThreadWindow window))
|
||||||
|
{
|
||||||
|
window = new RollingThreadWindow(rollingWindowSize);
|
||||||
|
rollingThreadWindows.Add(pair.Key, window);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Add(pair.Value.InclusiveMs, pair.Value.OverBudget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,556 @@
|
|||||||
|
using ImGuiNET;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime;
|
||||||
|
|
||||||
|
public static class ProfilerVisualizer
|
||||||
|
{
|
||||||
|
public sealed class TimelineState
|
||||||
|
{
|
||||||
|
public int SelectedFrameIndex = -1;
|
||||||
|
public int WindowStartIndex = 0;
|
||||||
|
public int VisibleFrameCount = 64;
|
||||||
|
public bool FollowLatest = true;
|
||||||
|
public float Zoom = 1f;
|
||||||
|
public double PanTicks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct TimelineRenderResult
|
||||||
|
{
|
||||||
|
public TimelineRenderResult(int selectedFrameIndex, bool selectionChanged, bool userNavigated)
|
||||||
|
{
|
||||||
|
SelectedFrameIndex = selectedFrameIndex;
|
||||||
|
SelectionChanged = selectionChanged;
|
||||||
|
UserNavigated = userNavigated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SelectedFrameIndex { get; }
|
||||||
|
public bool SelectionChanged { get; }
|
||||||
|
public bool UserNavigated { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct HoverEntry
|
||||||
|
{
|
||||||
|
public HoverEntry(Profiler.ScopeNode node, Profiler.Frame frame, int frameIndex, int depth, double timelineStartTicks)
|
||||||
|
{
|
||||||
|
Node = node;
|
||||||
|
Frame = frame;
|
||||||
|
FrameIndex = frameIndex;
|
||||||
|
Depth = depth;
|
||||||
|
DurationMs = TicksToMilliseconds(node.EndTime - node.StartTime);
|
||||||
|
SelfMs = node.SelfMilliseconds();
|
||||||
|
StartInFrameMs = TicksToMilliseconds(node.StartTime - frame.StartTime);
|
||||||
|
EndInFrameMs = TicksToMilliseconds(node.EndTime - frame.StartTime);
|
||||||
|
StartInTimelineMs = TicksToMilliseconds(node.StartTime - timelineStartTicks);
|
||||||
|
EndInTimelineMs = TicksToMilliseconds(node.EndTime - timelineStartTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Profiler.ScopeNode Node { get; }
|
||||||
|
public Profiler.Frame Frame { get; }
|
||||||
|
public int FrameIndex { get; }
|
||||||
|
public int Depth { get; }
|
||||||
|
public double DurationMs { get; }
|
||||||
|
public double SelfMs { get; }
|
||||||
|
public double StartInFrameMs { get; }
|
||||||
|
public double EndInFrameMs { get; }
|
||||||
|
public double StartInTimelineMs { get; }
|
||||||
|
public double EndInTimelineMs { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private const float BarHeight = 18f;
|
||||||
|
private const float BarPadding = 2f;
|
||||||
|
private const float ThreadGap = 8f;
|
||||||
|
private const float HeaderHeight = 28f;
|
||||||
|
private const float TimelineHeight = 220f;
|
||||||
|
private const float MinTextWidth = 36f;
|
||||||
|
private static readonly double TickToMs = 1000d / System.Diagnostics.Stopwatch.Frequency;
|
||||||
|
|
||||||
|
// Backwards-compatible entry point used by existing call sites.
|
||||||
|
public static void RenderFlameGraph(Profiler.Frame frame)
|
||||||
|
{
|
||||||
|
if (frame == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Profiler.Frame> frames = new List<Profiler.Frame>(1)
|
||||||
|
{
|
||||||
|
frame
|
||||||
|
};
|
||||||
|
|
||||||
|
TimelineState state = new TimelineState
|
||||||
|
{
|
||||||
|
VisibleFrameCount = 1,
|
||||||
|
SelectedFrameIndex = 0,
|
||||||
|
FollowLatest = true
|
||||||
|
};
|
||||||
|
|
||||||
|
RenderTimeline(frames, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TimelineRenderResult RenderTimeline(IReadOnlyList<Profiler.Frame> frames, TimelineState state)
|
||||||
|
{
|
||||||
|
if (frames == null || frames.Count == 0 || state == null)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool selectionChanged = false;
|
||||||
|
bool userNavigated = false;
|
||||||
|
|
||||||
|
int frameCount = frames.Count;
|
||||||
|
state.VisibleFrameCount = Math.Clamp(state.VisibleFrameCount, 1, frameCount);
|
||||||
|
state.Zoom = Math.Clamp(state.Zoom, 1f, 128f);
|
||||||
|
|
||||||
|
int maxStartIndex = Math.Max(0, frameCount - state.VisibleFrameCount);
|
||||||
|
if (state.FollowLatest)
|
||||||
|
{
|
||||||
|
state.WindowStartIndex = maxStartIndex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state.WindowStartIndex = Math.Clamp(state.WindowStartIndex, 0, maxStartIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
int visibleStartIndex = state.WindowStartIndex;
|
||||||
|
int visibleEndIndex = visibleStartIndex + state.VisibleFrameCount - 1;
|
||||||
|
|
||||||
|
if (state.SelectedFrameIndex < 0)
|
||||||
|
{
|
||||||
|
state.SelectedFrameIndex = visibleEndIndex;
|
||||||
|
selectionChanged = true;
|
||||||
|
}
|
||||||
|
state.SelectedFrameIndex = Math.Clamp(state.SelectedFrameIndex, visibleStartIndex, visibleEndIndex);
|
||||||
|
|
||||||
|
Profiler.Frame firstFrame = frames[visibleStartIndex];
|
||||||
|
Profiler.Frame lastFrame = frames[visibleEndIndex];
|
||||||
|
|
||||||
|
double timelineStartTicks = firstFrame.StartTime;
|
||||||
|
double timelineEndTicks = Math.Max(lastFrame.EndTime, firstFrame.StartTime + 1);
|
||||||
|
double timelineDurationTicks = Math.Max(1d, timelineEndTicks - timelineStartTicks);
|
||||||
|
|
||||||
|
double visibleDurationTicks = Math.Max(1d, timelineDurationTicks / state.Zoom);
|
||||||
|
double maxPanTicks = Math.Max(0d, timelineDurationTicks - visibleDurationTicks);
|
||||||
|
if (state.FollowLatest)
|
||||||
|
{
|
||||||
|
state.PanTicks = maxPanTicks;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state.PanTicks = Math.Clamp(state.PanTicks, 0d, maxPanTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
double visibleStartTicks = timelineStartTicks + state.PanTicks;
|
||||||
|
double visibleEndTicks = visibleStartTicks + visibleDurationTicks;
|
||||||
|
|
||||||
|
Dictionary<int, int> threadDepths = BuildThreadDepths(frames, visibleStartIndex, visibleEndIndex);
|
||||||
|
List<int> threadOrder = threadDepths.Keys.OrderBy(x => x).ToList();
|
||||||
|
|
||||||
|
Dictionary<int, float> threadBaseY = new Dictionary<int, float>(threadOrder.Count);
|
||||||
|
float yCursor = HeaderHeight;
|
||||||
|
for (int i = 0; i < threadOrder.Count; i++)
|
||||||
|
{
|
||||||
|
int threadId = threadOrder[i];
|
||||||
|
threadBaseY[threadId] = yCursor;
|
||||||
|
yCursor += ((threadDepths[threadId] + 1) * (BarHeight + BarPadding)) + ThreadGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
float contentHeight = Math.Max(TimelineHeight, yCursor + 6f);
|
||||||
|
|
||||||
|
ImGui.BeginChild("ProfilerTimeline", new Vector2(0, TimelineHeight), ImGuiChildFlags.Border | ImGuiChildFlags.ResizeY, ImGuiWindowFlags.AlwaysVerticalScrollbar);
|
||||||
|
|
||||||
|
ImDrawListPtr drawList = ImGui.GetWindowDrawList();
|
||||||
|
Vector2 origin = ImGui.GetCursorScreenPos();
|
||||||
|
Vector2 viewSize = ImGui.GetContentRegionAvail();
|
||||||
|
float canvasWidth = Math.Max(1f, viewSize.X);
|
||||||
|
|
||||||
|
uint frameBgColor = ImGui.GetColorU32(ImGuiCol.FrameBg);
|
||||||
|
uint frameBgHoveredColor = ImGui.GetColorU32(ImGuiCol.FrameBgHovered);
|
||||||
|
uint headerColor = ImGui.GetColorU32(ImGuiCol.Header);
|
||||||
|
uint headerHoveredColor = ImGui.GetColorU32(ImGuiCol.HeaderHovered);
|
||||||
|
uint textColor = ImGui.GetColorU32(ImGuiCol.Text);
|
||||||
|
uint borderColor = ImGui.GetColorU32(ImGuiCol.Border);
|
||||||
|
|
||||||
|
float clipMinX = origin.X;
|
||||||
|
float clipMaxX = origin.X + canvasWidth;
|
||||||
|
float clipMinY = origin.Y;
|
||||||
|
float clipMaxY = origin.Y + Math.Max(1f, ImGui.GetWindowHeight());
|
||||||
|
|
||||||
|
DrawTimelineHeader(drawList, origin, canvasWidth, timelineStartTicks, visibleStartTicks, visibleDurationTicks, textColor, borderColor);
|
||||||
|
|
||||||
|
HoverEntry? hovered = null;
|
||||||
|
|
||||||
|
for (int frameIndex = visibleStartIndex; frameIndex <= visibleEndIndex; frameIndex++)
|
||||||
|
{
|
||||||
|
Profiler.Frame frame = frames[frameIndex];
|
||||||
|
float frameStartX = ToScreenX(frame.StartTime, visibleStartTicks, visibleDurationTicks, origin.X, canvasWidth);
|
||||||
|
float frameEndX = ToScreenX(frame.EndTime, visibleStartTicks, visibleDurationTicks, origin.X, canvasWidth);
|
||||||
|
|
||||||
|
if (frameEndX < clipMinX || frameStartX > clipMaxX)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSelectedFrame = frameIndex == state.SelectedFrameIndex;
|
||||||
|
uint frameShadeColor = (frameIndex & 1) == 0 ? frameBgColor : frameBgHoveredColor;
|
||||||
|
if (isSelectedFrame)
|
||||||
|
{
|
||||||
|
frameShadeColor = LerpColor(headerColor, headerHoveredColor, 0.45f);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawList.AddRectFilled(new Vector2(frameStartX, origin.Y + HeaderHeight), new Vector2(frameEndX, origin.Y + contentHeight), SetAlpha(frameShadeColor, isSelectedFrame ? 0.20f : 0.08f));
|
||||||
|
drawList.AddLine(new Vector2(frameStartX, origin.Y + HeaderHeight), new Vector2(frameStartX, origin.Y + contentHeight), SetAlpha(borderColor, 0.55f), 1f);
|
||||||
|
|
||||||
|
foreach (Profiler.ScopeNode root in frame.RootNodes)
|
||||||
|
{
|
||||||
|
if (!threadBaseY.TryGetValue(root.ManagedThreadId, out float baseY))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawThreadLabel(drawList, origin.X, origin.Y + baseY, root.ManagedThreadId, textColor);
|
||||||
|
for (int i = 0; i < root.Children.Count; i++)
|
||||||
|
{
|
||||||
|
RenderNode(
|
||||||
|
drawList,
|
||||||
|
root.Children[i],
|
||||||
|
frame,
|
||||||
|
frameIndex,
|
||||||
|
baseY,
|
||||||
|
0,
|
||||||
|
visibleStartTicks,
|
||||||
|
visibleDurationTicks,
|
||||||
|
origin.X,
|
||||||
|
origin.Y,
|
||||||
|
canvasWidth,
|
||||||
|
clipMinX,
|
||||||
|
clipMaxX,
|
||||||
|
clipMaxY,
|
||||||
|
ref hovered,
|
||||||
|
textColor,
|
||||||
|
headerColor,
|
||||||
|
headerHoveredColor,
|
||||||
|
frameIndex == state.SelectedFrameIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawList.AddLine(new Vector2(frameEndX, origin.Y + HeaderHeight), new Vector2(frameEndX, origin.Y + contentHeight), SetAlpha(borderColor, 0.30f), 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(canvasWidth, contentHeight));
|
||||||
|
|
||||||
|
bool windowHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows);
|
||||||
|
if (windowHovered)
|
||||||
|
{
|
||||||
|
HandleZoomAndPan(state, timelineStartTicks, timelineDurationTicks, visibleDurationTicks, origin.X, canvasWidth, ref userNavigated);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windowHovered && ImGui.IsMouseClicked(ImGuiMouseButton.Left))
|
||||||
|
{
|
||||||
|
int clickedFrame = FindFrameIndexByMouseX(frames, visibleStartIndex, visibleEndIndex, visibleStartTicks, visibleDurationTicks, origin.X, canvasWidth, ImGui.GetMousePos().X);
|
||||||
|
if (clickedFrame >= visibleStartIndex && clickedFrame <= visibleEndIndex && clickedFrame != state.SelectedFrameIndex)
|
||||||
|
{
|
||||||
|
state.SelectedFrameIndex = clickedFrame;
|
||||||
|
selectionChanged = true;
|
||||||
|
state.FollowLatest = false;
|
||||||
|
userNavigated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hovered.HasValue)
|
||||||
|
{
|
||||||
|
if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) && hovered.Value.FrameIndex != state.SelectedFrameIndex)
|
||||||
|
{
|
||||||
|
state.SelectedFrameIndex = hovered.Value.FrameIndex;
|
||||||
|
selectionChanged = true;
|
||||||
|
state.FollowLatest = false;
|
||||||
|
userNavigated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawTooltip(hovered.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndChild();
|
||||||
|
|
||||||
|
return new TimelineRenderResult(state.SelectedFrameIndex, selectionChanged, userNavigated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleZoomAndPan(TimelineState state, double timelineStartTicks, double timelineDurationTicks, double visibleDurationTicks, float originX, float canvasWidth, ref bool userNavigated)
|
||||||
|
{
|
||||||
|
ImGuiIOPtr io = ImGui.GetIO();
|
||||||
|
if (Math.Abs(io.MouseWheel) < float.Epsilon)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!io.KeyCtrl && !io.KeyShift)
|
||||||
|
{
|
||||||
|
return; // plain scroll goes to ImGui vertical scrolling
|
||||||
|
}
|
||||||
|
|
||||||
|
float wheel = io.MouseWheel;
|
||||||
|
io.MouseWheel = 0; // consume so the child window doesn't also scroll vertically
|
||||||
|
|
||||||
|
if (io.KeyCtrl)
|
||||||
|
{
|
||||||
|
float previousZoom = state.Zoom;
|
||||||
|
double visibleStartTicksBefore = timelineStartTicks + state.PanTicks;
|
||||||
|
double mouseT = Math.Clamp((ImGui.GetMousePos().X - originX) / Math.Max(1f, canvasWidth), 0f, 1f);
|
||||||
|
double pivotTick = visibleStartTicksBefore + (visibleDurationTicks * mouseT);
|
||||||
|
|
||||||
|
state.Zoom = Math.Clamp(state.Zoom * MathF.Pow(1.12f, wheel), 1f, 128f);
|
||||||
|
if (Math.Abs(previousZoom - state.Zoom) > float.Epsilon)
|
||||||
|
{
|
||||||
|
double newVisibleDurationTicks = Math.Max(1d, timelineDurationTicks / state.Zoom);
|
||||||
|
double newVisibleStartTicks = pivotTick - (newVisibleDurationTicks * mouseT);
|
||||||
|
state.PanTicks = Math.Clamp(newVisibleStartTicks - timelineStartTicks, 0d, Math.Max(0d, timelineDurationTicks - newVisibleDurationTicks));
|
||||||
|
state.FollowLatest = false;
|
||||||
|
userNavigated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Shift + scroll: horizontal pan
|
||||||
|
state.PanTicks -= wheel * (visibleDurationTicks * 0.10d);
|
||||||
|
state.FollowLatest = false;
|
||||||
|
userNavigated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindFrameIndexByMouseX(IReadOnlyList<Profiler.Frame> frames, int visibleStartIndex, int visibleEndIndex, double visibleStartTicks, double visibleDurationTicks, float originX, float width, float mouseX)
|
||||||
|
{
|
||||||
|
double t = Math.Clamp((mouseX - originX) / Math.Max(1f, width), 0f, 1f);
|
||||||
|
double timelineTicks = visibleStartTicks + (visibleDurationTicks * t);
|
||||||
|
|
||||||
|
for (int i = visibleStartIndex; i <= visibleEndIndex; i++)
|
||||||
|
{
|
||||||
|
Profiler.Frame frame = frames[i];
|
||||||
|
if (timelineTicks >= frame.StartTime && timelineTicks <= frame.EndTime)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawTimelineHeader(ImDrawListPtr drawList, Vector2 origin, float width, double timelineStartTicks, double visibleStartTicks, double visibleDurationTicks, uint textColor, uint borderColor)
|
||||||
|
{
|
||||||
|
drawList.AddLine(new Vector2(origin.X, origin.Y + HeaderHeight), new Vector2(origin.X + width, origin.Y + HeaderHeight), SetAlpha(borderColor, 0.65f), 1f);
|
||||||
|
|
||||||
|
int tickCount = Math.Clamp((int)(width / 130f), 4, 20);
|
||||||
|
for (int i = 0; i <= tickCount; i++)
|
||||||
|
{
|
||||||
|
float t = i / (float)tickCount;
|
||||||
|
float x = origin.X + (t * width);
|
||||||
|
drawList.AddLine(new Vector2(x, origin.Y + HeaderHeight - 8f), new Vector2(x, origin.Y + HeaderHeight), SetAlpha(borderColor, 0.8f), 1f);
|
||||||
|
|
||||||
|
double ms = TicksToMilliseconds((visibleStartTicks - timelineStartTicks) + (visibleDurationTicks * t));
|
||||||
|
drawList.AddText(new Vector2(x + 2f, origin.Y + 4f), textColor, $"+{ms:0.0} ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawThreadLabel(ImDrawListPtr drawList, float x, float y, int threadId, uint textColor)
|
||||||
|
{
|
||||||
|
drawList.AddText(new Vector2(x + 4f, y + 2f), SetAlpha(textColor, 0.85f), $"T{threadId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawTooltip(HoverEntry hover)
|
||||||
|
{
|
||||||
|
ImGui.BeginTooltip();
|
||||||
|
ImGui.Text($"{hover.Node.Label}");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Text($"Category: {hover.Node.Category}");
|
||||||
|
ImGui.Text($"Tags: 0x{hover.Node.TagMask:X}");
|
||||||
|
ImGui.Text($"Frame: {hover.Frame.FrameCount} (idx {hover.FrameIndex})");
|
||||||
|
ImGui.Text($"Thread: {hover.Node.ManagedThreadId}");
|
||||||
|
ImGui.Text($"Depth: {hover.Depth}");
|
||||||
|
ImGui.Text($"Duration: {hover.DurationMs:0.000} ms");
|
||||||
|
ImGui.Text($"Self: {hover.SelfMs:0.000} ms");
|
||||||
|
ImGui.Text($"Frame Start: {hover.StartInFrameMs:0.000} ms");
|
||||||
|
ImGui.Text($"Frame End: {hover.EndInFrameMs:0.000} ms");
|
||||||
|
ImGui.Text($"Timeline Start: {hover.StartInTimelineMs:0.000} ms");
|
||||||
|
ImGui.Text($"Timeline End: {hover.EndInTimelineMs:0.000} ms");
|
||||||
|
ImGui.Text($"Children: {hover.Node.Children.Count}");
|
||||||
|
if (hover.Node.ProfilerSetupBytes > 0)
|
||||||
|
{
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.TextColored(new Vector4(1f, 0.75f, 0f, 1f), $"\u26a0 {hover.Node.ProfilerSetupBytes} B of alloc is profiler warmup overhead");
|
||||||
|
}
|
||||||
|
ImGui.EndTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RenderNode(
|
||||||
|
ImDrawListPtr drawList,
|
||||||
|
Profiler.ScopeNode node,
|
||||||
|
Profiler.Frame frame,
|
||||||
|
int frameIndex,
|
||||||
|
float baseY,
|
||||||
|
int depth,
|
||||||
|
double visibleStartTicks,
|
||||||
|
double visibleDurationTicks,
|
||||||
|
float originX,
|
||||||
|
float originY,
|
||||||
|
float width,
|
||||||
|
float clipMinX,
|
||||||
|
float clipMaxX,
|
||||||
|
float clipMaxY,
|
||||||
|
ref HoverEntry? hovered,
|
||||||
|
uint textColor,
|
||||||
|
uint headerColor,
|
||||||
|
uint headerHoveredColor,
|
||||||
|
bool selectedFrame)
|
||||||
|
{
|
||||||
|
long nodeEndTime = Math.Max(node.EndTime, node.StartTime + 1);
|
||||||
|
if (nodeEndTime < visibleStartTicks || node.StartTime > visibleStartTicks + visibleDurationTicks)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float y = originY + baseY + (depth * (BarHeight + BarPadding));
|
||||||
|
if (y > clipMaxY)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float minX = ToScreenX(node.StartTime, visibleStartTicks, visibleDurationTicks, originX, width);
|
||||||
|
float maxX = ToScreenX(nodeEndTime, visibleStartTicks, visibleDurationTicks, originX, width);
|
||||||
|
if (maxX < clipMinX || minX > clipMaxX)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float barWidth = Math.Max(1f, maxX - minX);
|
||||||
|
Vector2 min = new Vector2(minX, y + (BarPadding * 0.5f));
|
||||||
|
Vector2 max = new Vector2(minX + barWidth, y + (BarPadding * 0.5f) + BarHeight);
|
||||||
|
|
||||||
|
uint barColor = BuildBarColor(node.Label, depth, selectedFrame);
|
||||||
|
uint borderColor = LerpColor(headerColor, headerHoveredColor, 0.45f);
|
||||||
|
drawList.AddRectFilled(min, max, barColor, 3f);
|
||||||
|
drawList.AddRect(min, max, SetAlpha(borderColor, 0.55f), 3f, ImDrawFlags.None, 1f);
|
||||||
|
|
||||||
|
Vector2 mousePos = ImGui.GetMousePos();
|
||||||
|
bool isHovered = mousePos.X >= min.X && mousePos.X <= max.X && mousePos.Y >= min.Y && mousePos.Y <= max.Y;
|
||||||
|
if (isHovered)
|
||||||
|
{
|
||||||
|
hovered = new HoverEntry(node, frame, frameIndex, depth, visibleStartTicks);
|
||||||
|
drawList.AddRect(min, max, headerHoveredColor, 3f, ImDrawFlags.None, 1.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (barWidth > MinTextWidth)
|
||||||
|
{
|
||||||
|
string label = node.Label;
|
||||||
|
float textWidth = ImGui.CalcTextSize(label).X;
|
||||||
|
if (textWidth + 8f <= barWidth)
|
||||||
|
{
|
||||||
|
drawList.AddText(new Vector2(min.X + 4f, min.Y + 2f), textColor, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < node.Children.Count; i++)
|
||||||
|
{
|
||||||
|
RenderNode(
|
||||||
|
drawList,
|
||||||
|
node.Children[i],
|
||||||
|
frame,
|
||||||
|
frameIndex,
|
||||||
|
baseY,
|
||||||
|
depth + 1,
|
||||||
|
visibleStartTicks,
|
||||||
|
visibleDurationTicks,
|
||||||
|
originX,
|
||||||
|
originY,
|
||||||
|
width,
|
||||||
|
clipMinX,
|
||||||
|
clipMaxX,
|
||||||
|
clipMaxY,
|
||||||
|
ref hovered,
|
||||||
|
textColor,
|
||||||
|
headerColor,
|
||||||
|
headerHoveredColor,
|
||||||
|
selectedFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<int, int> BuildThreadDepths(IReadOnlyList<Profiler.Frame> frames, int startIndex, int endIndex)
|
||||||
|
{
|
||||||
|
Dictionary<int, int> threadMaxDepths = new Dictionary<int, int>(8);
|
||||||
|
for (int frameIndex = startIndex; frameIndex <= endIndex; frameIndex++)
|
||||||
|
{
|
||||||
|
foreach (Profiler.ScopeNode root in frames[frameIndex].RootNodes)
|
||||||
|
{
|
||||||
|
int maxDepth = 0;
|
||||||
|
for (int i = 0; i < root.Children.Count; i++)
|
||||||
|
{
|
||||||
|
maxDepth = Math.Max(maxDepth, GetMaxDepth(root.Children[i], 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threadMaxDepths.TryGetValue(root.ManagedThreadId, out int currentMax))
|
||||||
|
{
|
||||||
|
if (maxDepth > currentMax)
|
||||||
|
{
|
||||||
|
threadMaxDepths[root.ManagedThreadId] = maxDepth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
threadMaxDepths[root.ManagedThreadId] = maxDepth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return threadMaxDepths;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetMaxDepth(Profiler.ScopeNode node, int depth)
|
||||||
|
{
|
||||||
|
if (node.Children.Count == 0)
|
||||||
|
{
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxDepth = depth;
|
||||||
|
for (int i = 0; i < node.Children.Count; i++)
|
||||||
|
{
|
||||||
|
maxDepth = Math.Max(maxDepth, GetMaxDepth(node.Children[i], depth + 1));
|
||||||
|
}
|
||||||
|
return maxDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint BuildBarColor(string label, int depth, bool selectedFrame)
|
||||||
|
{
|
||||||
|
int hash = label.GetHashCode();
|
||||||
|
float hue = ((hash & 1023) / 1023f + (depth * 0.031f)) % 1f;
|
||||||
|
ImGui.ColorConvertHSVtoRGB(hue, 0.52f, selectedFrame ? 0.82f : 0.68f, out float r, out float g, out float b);
|
||||||
|
|
||||||
|
Vector4 frameBg = ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.FrameBg));
|
||||||
|
Vector4 accent = new Vector4(r, g, b, 1f);
|
||||||
|
Vector4 mixed = Vector4.Lerp(frameBg, accent, 0.72f);
|
||||||
|
return ImGui.ColorConvertFloat4ToU32(mixed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float ToScreenX(double ticks, double visibleStartTicks, double visibleDurationTicks, float startX, float width)
|
||||||
|
{
|
||||||
|
double normalized = (ticks - visibleStartTicks) / visibleDurationTicks;
|
||||||
|
return startX + (float)(normalized * width);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double TicksToMilliseconds(double ticks)
|
||||||
|
{
|
||||||
|
return ticks * TickToMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint SetAlpha(uint color, float alpha)
|
||||||
|
{
|
||||||
|
Vector4 c = ImGui.ColorConvertU32ToFloat4(color);
|
||||||
|
c.W *= alpha;
|
||||||
|
return ImGui.ColorConvertFloat4ToU32(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint LerpColor(uint a, uint b, float t)
|
||||||
|
{
|
||||||
|
Vector4 av = ImGui.ColorConvertU32ToFloat4(a);
|
||||||
|
Vector4 bv = ImGui.ColorConvertU32ToFloat4(b);
|
||||||
|
return ImGui.ColorConvertFloat4ToU32(Vector4.Lerp(av, bv, Math.Clamp(t, 0f, 1f)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Nerfed.Runtime;
|
|
||||||
|
|
||||||
internal class Program
|
|
||||||
{
|
|
||||||
private static void Main(string[] args)
|
|
||||||
{
|
|
||||||
Engine.Run(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Nerfed.Runtime;
|
||||||
|
|
||||||
|
public abstract class Resource
|
||||||
|
{
|
||||||
|
public string Path { get; internal set; }
|
||||||
|
|
||||||
|
internal abstract void Load(Stream stream);
|
||||||
|
internal abstract void Unload();
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
namespace Nerfed.Runtime;
|
||||||
|
|
||||||
|
public static class ResourceManager
|
||||||
|
{
|
||||||
|
private const string rootName = "Resources";
|
||||||
|
private static readonly Dictionary<string, Resource> loadedResources = new Dictionary<string, Resource>();
|
||||||
|
|
||||||
|
public static T Load<T>(string resourcePath) where T : Resource
|
||||||
|
{
|
||||||
|
if (loadedResources.TryGetValue(resourcePath, out Resource resource))
|
||||||
|
{
|
||||||
|
return (T)resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof(T) == typeof(Shader))
|
||||||
|
{
|
||||||
|
resource = new Shader();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to create resource");
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Always(resource != null);
|
||||||
|
resource.Path = resourcePath;
|
||||||
|
resource.Load(StorageContainer.OpenStream(Path.Combine(AppContext.BaseDirectory, rootName, resourcePath) + ".bin"));
|
||||||
|
|
||||||
|
loadedResources.Add(resourcePath, resource);
|
||||||
|
return (T)resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Unload(Resource resource)
|
||||||
|
{
|
||||||
|
if (!loadedResources.ContainsKey(resource.Path))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resource.Unload();
|
||||||
|
resource.Path = string.Empty;
|
||||||
|
loadedResources.Remove(resource.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using RefreshCS;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime;
|
||||||
|
|
||||||
|
public class Shader : Resource
|
||||||
|
{
|
||||||
|
public IntPtr Handle { get; private set; }
|
||||||
|
|
||||||
|
public uint SamplerCount { get; private set; }
|
||||||
|
public uint StorageTextureCount { get; private set; }
|
||||||
|
public uint StorageBufferCount { get; private set; }
|
||||||
|
public uint UniformBufferCount { get; private set; }
|
||||||
|
|
||||||
|
internal Shader() { }
|
||||||
|
|
||||||
|
internal override unsafe void Load(Stream stream)
|
||||||
|
{
|
||||||
|
using (BinaryReader reader = new BinaryReader(stream))
|
||||||
|
{
|
||||||
|
Refresh.ShaderCreateInfo createInfo;
|
||||||
|
createInfo.Format = (Refresh.ShaderFormat)reader.ReadInt32();
|
||||||
|
createInfo.Stage = (Refresh.ShaderStage)reader.ReadInt32();
|
||||||
|
createInfo.UniformBufferCount = (uint)reader.ReadInt32();
|
||||||
|
createInfo.StorageBufferCount = (uint)reader.ReadInt32();
|
||||||
|
createInfo.StorageTextureCount = (uint)reader.ReadInt32();
|
||||||
|
createInfo.SamplerCount = (uint)reader.ReadInt32();
|
||||||
|
|
||||||
|
int byteCodeSize = reader.ReadInt32();
|
||||||
|
void* byteCodeBuffer = NativeMemory.Alloc((nuint)byteCodeSize);
|
||||||
|
Span<byte> byteCodeSpan = new Span<byte>(byteCodeBuffer, byteCodeSize);
|
||||||
|
|
||||||
|
int bytesRead = 0;
|
||||||
|
while (bytesRead < byteCodeSize)
|
||||||
|
{
|
||||||
|
bytesRead += reader.Read(byteCodeSpan.Slice(bytesRead));
|
||||||
|
}
|
||||||
|
|
||||||
|
createInfo.CodeSize = (nuint)byteCodeSize;
|
||||||
|
createInfo.Code = (byte*)byteCodeBuffer;
|
||||||
|
createInfo.EntryPointName = "main";
|
||||||
|
|
||||||
|
Handle = Refresh.Refresh_CreateShader(Engine.GraphicsDevice.Handle, createInfo);
|
||||||
|
NativeMemory.Free(byteCodeBuffer);
|
||||||
|
|
||||||
|
SamplerCount = createInfo.SamplerCount;
|
||||||
|
StorageTextureCount = createInfo.StorageTextureCount;
|
||||||
|
StorageBufferCount = createInfo.StorageBufferCount;
|
||||||
|
UniformBufferCount = createInfo.UniformBufferCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override void Unload()
|
||||||
|
{
|
||||||
|
Refresh.Refresh_ReleaseShader(Engine.GraphicsDevice.Handle, Handle);
|
||||||
|
Handle = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Nerfed.Runtime.Scheduling;
|
||||||
|
|
||||||
|
public interface IParallelSystemMetadata
|
||||||
|
{
|
||||||
|
string ScheduleName { get; }
|
||||||
|
SystemAccessDeclaration AccessDeclaration { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
namespace Nerfed.Runtime.Scheduling;
|
||||||
|
|
||||||
|
public enum SystemAccessMode
|
||||||
|
{
|
||||||
|
Read,
|
||||||
|
WriteExisting,
|
||||||
|
StructuralWrite,
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ComponentAccess
|
||||||
|
{
|
||||||
|
public ComponentAccess(Type componentType, SystemAccessMode mode)
|
||||||
|
{
|
||||||
|
ComponentType = componentType;
|
||||||
|
Mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type ComponentType { get; }
|
||||||
|
public SystemAccessMode Mode { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct SystemAccessDeclaration
|
||||||
|
{
|
||||||
|
private readonly ComponentAccess[] accesses;
|
||||||
|
|
||||||
|
public static readonly SystemAccessDeclaration Empty = new(Array.Empty<ComponentAccess>());
|
||||||
|
|
||||||
|
public SystemAccessDeclaration(params ComponentAccess[] accesses)
|
||||||
|
{
|
||||||
|
this.accesses = accesses ?? Array.Empty<ComponentAccess>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlySpan<ComponentAccess> Accesses => accesses ?? Array.Empty<ComponentAccess>();
|
||||||
|
|
||||||
|
public bool ConflictsWith(in SystemAccessDeclaration other)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<ComponentAccess> left = Accesses;
|
||||||
|
ReadOnlySpan<ComponentAccess> right = other.Accesses;
|
||||||
|
|
||||||
|
for (int i = 0; i < left.Length; i++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < right.Length; j++)
|
||||||
|
{
|
||||||
|
if (left[i].Mode == SystemAccessMode.StructuralWrite || right[j].Mode == SystemAccessMode.StructuralWrite)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left[i].ComponentType != right[j].ComponentType)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left[i].Mode == SystemAccessMode.Read && right[j].Mode == SystemAccessMode.Read)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ComponentAccess Read<T>() where T : unmanaged => new(typeof(T), SystemAccessMode.Read);
|
||||||
|
public static ComponentAccess WriteExisting<T>() where T : unmanaged => new(typeof(T), SystemAccessMode.WriteExisting);
|
||||||
|
public static ComponentAccess StructuralWrite<T>() where T : unmanaged => new(typeof(T), SystemAccessMode.StructuralWrite);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using MoonTools.ECS;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Scheduling;
|
||||||
|
|
||||||
|
public sealed class SystemScheduleEntry
|
||||||
|
{
|
||||||
|
public SystemScheduleEntry(
|
||||||
|
MoonTools.ECS.System system,
|
||||||
|
SystemSchedulePhase phase,
|
||||||
|
string[] dependsOn,
|
||||||
|
int registrationIndex)
|
||||||
|
{
|
||||||
|
System = system;
|
||||||
|
Phase = phase;
|
||||||
|
DependsOn = dependsOn ?? Array.Empty<string>();
|
||||||
|
RegistrationIndex = registrationIndex;
|
||||||
|
|
||||||
|
if (system is IParallelSystemMetadata metadata)
|
||||||
|
{
|
||||||
|
Name = metadata.ScheduleName;
|
||||||
|
Access = metadata.AccessDeclaration;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Name = system.GetType().Name;
|
||||||
|
// Unknown systems are treated as structural to preserve safety until metadata is declared.
|
||||||
|
Access = new SystemAccessDeclaration(new ComponentAccess(typeof(object), SystemAccessMode.StructuralWrite));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MoonTools.ECS.System System { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
public SystemSchedulePhase Phase { get; }
|
||||||
|
public string[] DependsOn { get; }
|
||||||
|
public int RegistrationIndex { get; }
|
||||||
|
public SystemAccessDeclaration Access { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Nerfed.Runtime.Scheduling;
|
||||||
|
|
||||||
|
public enum SystemSchedulePhase
|
||||||
|
{
|
||||||
|
PreUpdate = 0,
|
||||||
|
Simulation = 1,
|
||||||
|
LateSimulation = 2,
|
||||||
|
PreRender = 3,
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
using MoonTools.ECS;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Scheduling;
|
||||||
|
|
||||||
|
public sealed class SystemScheduler
|
||||||
|
{
|
||||||
|
private readonly List<SystemScheduleEntry> entries = new List<SystemScheduleEntry>();
|
||||||
|
private readonly SystemSchedulerOptions options;
|
||||||
|
|
||||||
|
public SystemScheduler(SystemSchedulerOptions options)
|
||||||
|
{
|
||||||
|
this.options = options ?? new SystemSchedulerOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<SystemScheduleEntry> Entries => entries;
|
||||||
|
|
||||||
|
public void Register(MoonTools.ECS.System system, SystemSchedulePhase phase, params string[] dependsOn)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(system);
|
||||||
|
string[] dependencies = dependsOn?.Where(static value => !string.IsNullOrWhiteSpace(value)).ToArray() ?? Array.Empty<string>();
|
||||||
|
entries.Add(new SystemScheduleEntry(system, phase, dependencies, entries.Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(TimeSpan delta)
|
||||||
|
{
|
||||||
|
if (entries.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, SystemScheduleEntry> byName = BuildNameIndex(entries);
|
||||||
|
ValidateDependencies(byName, entries);
|
||||||
|
|
||||||
|
foreach (SystemSchedulePhase phase in Enum.GetValues<SystemSchedulePhase>())
|
||||||
|
{
|
||||||
|
ExecutePhase(phase, delta, byName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, SystemScheduleEntry> BuildNameIndex(List<SystemScheduleEntry> allEntries)
|
||||||
|
{
|
||||||
|
var byName = new Dictionary<string, SystemScheduleEntry>(StringComparer.Ordinal);
|
||||||
|
foreach (SystemScheduleEntry entry in allEntries)
|
||||||
|
{
|
||||||
|
if (byName.ContainsKey(entry.Name))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Duplicate scheduled system name '{entry.Name}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
byName.Add(entry.Name, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return byName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateDependencies(Dictionary<string, SystemScheduleEntry> byName, List<SystemScheduleEntry> allEntries)
|
||||||
|
{
|
||||||
|
foreach (SystemScheduleEntry entry in allEntries)
|
||||||
|
{
|
||||||
|
foreach (string dependency in entry.DependsOn)
|
||||||
|
{
|
||||||
|
if (!byName.TryGetValue(dependency, out SystemScheduleEntry dependencyEntry))
|
||||||
|
{
|
||||||
|
if (options.StrictDependencyValidation)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Scheduled system '{entry.Name}' depends on missing system '{dependency}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dependencyEntry.Phase > entry.Phase)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Scheduled system '{entry.Name}' in phase {entry.Phase} cannot depend on later phase system '{dependency}' in phase {dependencyEntry.Phase}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecutePhase(SystemSchedulePhase phase, TimeSpan delta, Dictionary<string, SystemScheduleEntry> byName)
|
||||||
|
{
|
||||||
|
List<SystemScheduleEntry> phaseEntries = entries
|
||||||
|
.Where(entry => entry.Phase == phase)
|
||||||
|
.OrderBy(entry => entry.RegistrationIndex)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (phaseEntries.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var completed = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
while (completed.Count < phaseEntries.Count)
|
||||||
|
{
|
||||||
|
List<SystemScheduleEntry> ready = phaseEntries
|
||||||
|
.Where(entry => !completed.Contains(entry.Name) && DependenciesSatisfied(entry, completed, byName))
|
||||||
|
.OrderBy(entry => entry.RegistrationIndex)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (ready.Count == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Cyclic or unsatisfied dependencies detected in phase {phase}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SystemScheduleEntry> batch = BuildNonConflictingBatch(ready);
|
||||||
|
ExecuteBatch(batch, delta, phase);
|
||||||
|
|
||||||
|
foreach (SystemScheduleEntry entry in batch)
|
||||||
|
{
|
||||||
|
completed.Add(entry.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DependenciesSatisfied(SystemScheduleEntry entry, HashSet<string> completed, Dictionary<string, SystemScheduleEntry> byName)
|
||||||
|
{
|
||||||
|
foreach (string dependency in entry.DependsOn)
|
||||||
|
{
|
||||||
|
if (!byName.TryGetValue(dependency, out SystemScheduleEntry dependencyEntry))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dependencyEntry.Phase == entry.Phase && !completed.Contains(dependency))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<SystemScheduleEntry> BuildNonConflictingBatch(List<SystemScheduleEntry> ready)
|
||||||
|
{
|
||||||
|
var batch = new List<SystemScheduleEntry>(ready.Count);
|
||||||
|
|
||||||
|
for (int i = 0; i < ready.Count; i++)
|
||||||
|
{
|
||||||
|
SystemScheduleEntry candidate = ready[i];
|
||||||
|
bool conflicts = false;
|
||||||
|
|
||||||
|
for (int j = 0; j < batch.Count; j++)
|
||||||
|
{
|
||||||
|
if (candidate.Access.ConflictsWith(batch[j].Access))
|
||||||
|
{
|
||||||
|
conflicts = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!conflicts)
|
||||||
|
{
|
||||||
|
batch.Add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batch.Count == 0)
|
||||||
|
{
|
||||||
|
batch.Add(ready[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return batch;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteBatch(List<SystemScheduleEntry> batch, TimeSpan delta, SystemSchedulePhase phase)
|
||||||
|
{
|
||||||
|
using ProfilerScope phaseScope = new($"Schedule.{phase}.Batch[{batch.Count}]");
|
||||||
|
|
||||||
|
if (options.EnableParallelSystemExecution && batch.Count > 1)
|
||||||
|
{
|
||||||
|
Parallel.ForEach(batch, entry => ExecuteSystem(entry, delta));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < batch.Count; i++)
|
||||||
|
{
|
||||||
|
ExecuteSystem(batch[i], delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ExecuteSystem(SystemScheduleEntry entry, TimeSpan delta)
|
||||||
|
{
|
||||||
|
using ProfilerScope scope = new(entry.Name);
|
||||||
|
entry.System.Update(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Nerfed.Runtime.Scheduling;
|
||||||
|
|
||||||
|
public sealed class SystemSchedulerOptions
|
||||||
|
{
|
||||||
|
public bool EnableParallelSystemExecution { get; set; }
|
||||||
|
public bool StrictDependencyValidation { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using ImGuiNET;
|
||||||
|
using MoonTools.ECS;
|
||||||
|
using Nerfed.Runtime.Components;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Serialization;
|
||||||
|
|
||||||
|
public static class ComponentHelper
|
||||||
|
{
|
||||||
|
// Auto generate this.
|
||||||
|
public static readonly Dictionary<Type, Func<World, Entity, ValueType>> GetComponentByType = new()
|
||||||
|
{
|
||||||
|
{ typeof(LocalTransform), (world, entity) => world.Get<LocalTransform>(entity) },
|
||||||
|
{ typeof(Root), (world, entity) => world.Get<Root>(entity) },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto generate this.
|
||||||
|
public static readonly Dictionary<Type, Action<World, Entity, ValueType>> SetComponentByType = new()
|
||||||
|
{
|
||||||
|
{ typeof(LocalTransform), (world, entity, component) => world.Set(entity, (LocalTransform)component) },
|
||||||
|
{ typeof(Root), (world, entity, component) => world.Set(entity, (Root)component) },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto generate this, but it should only contain user assignable components (so something like 'root' should be excluded).
|
||||||
|
// Maybe use an attribute for this.
|
||||||
|
public static readonly Dictionary<Type, Action<World, Entity>> AddComponentByType = new()
|
||||||
|
{
|
||||||
|
{ typeof(LocalTransform), (world, entity) => world.Set(entity, LocalTransform.Identity) },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto generate this, but also keep the option for 'custom inspectors'.
|
||||||
|
// Maybe via attribute?
|
||||||
|
public static readonly Dictionary<Type, Action<World, Entity>> ComponentInspectorByType = new()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
typeof(LocalTransform), (world, entity) =>
|
||||||
|
{
|
||||||
|
(Vector3 position, Quaternion rotation, Vector3 scale) = world.Get<LocalTransform>(entity);
|
||||||
|
Vector3 eulerAngles = MathEx.ToEulerAngles(rotation);
|
||||||
|
eulerAngles = new Vector3(float.RadiansToDegrees(eulerAngles.X), float.RadiansToDegrees(eulerAngles.Y), float.RadiansToDegrees(eulerAngles.Z));
|
||||||
|
bool isDirty = false;
|
||||||
|
|
||||||
|
ImGui.BeginGroup();
|
||||||
|
ImGui.Text($"{nameof(LocalTransform)}");
|
||||||
|
isDirty |= ImGui.DragFloat3("Position", ref position, 0.2f, float.MinValue, float.MaxValue /*, "%f0 m" */); // TODO: right format.
|
||||||
|
isDirty |= ImGui.DragFloat3("Rotation", ref eulerAngles);
|
||||||
|
isDirty |= ImGui.DragFloat3("Scale", ref scale);
|
||||||
|
ImGui.EndGroup();
|
||||||
|
|
||||||
|
if (!isDirty)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
eulerAngles = new Vector3(float.DegreesToRadians(eulerAngles.X), float.DegreesToRadians(eulerAngles.Y), float.DegreesToRadians(eulerAngles.Z));
|
||||||
|
world.Set(entity, new LocalTransform(position, MathEx.ToQuaternion(eulerAngles), scale));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(Root), (world, entity) =>
|
||||||
|
{
|
||||||
|
ImGui.BeginGroup();
|
||||||
|
ImGui.Text($"{nameof(Root)}");
|
||||||
|
ImGui.EndGroup();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using MoonTools.ECS;
|
||||||
|
using Nerfed.Runtime.Components;
|
||||||
|
using Nerfed.Runtime.Util;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// Explore if having a WorldTransform and LocalTransfom component each holding position, rotation, scale values and the matricies is useful.
|
||||||
|
// Often you need to either get or set these values.
|
||||||
|
// If so, we probably need a utility funciton to do so. Since changing these values means that we need to update all the related data + children as well.
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// When modifying transform all the children need to be updated as well.
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Systems
|
||||||
|
{
|
||||||
|
public class LocalToWorldSystem : MoonTools.ECS.System
|
||||||
|
{
|
||||||
|
private readonly JobSystem jobs;
|
||||||
|
private readonly Filter rootEntitiesFilter;
|
||||||
|
private readonly Filter entitiesWithoutLocalToWorldFilter;
|
||||||
|
private readonly Action<int> updateWorldTransformByIndex;
|
||||||
|
|
||||||
|
public LocalToWorldSystem(World world, JobSystem jobs = null) : base(world)
|
||||||
|
{
|
||||||
|
this.jobs = jobs ?? JobSystem.Default;
|
||||||
|
rootEntitiesFilter = FilterBuilder.Include<LocalTransform>().Exclude<Child>().Build();
|
||||||
|
entitiesWithoutLocalToWorldFilter = FilterBuilder.Include<LocalTransform>().Exclude<LocalToWorld>().Build();
|
||||||
|
updateWorldTransformByIndex = UpdateWorldTransformByIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(TimeSpan delta)
|
||||||
|
{
|
||||||
|
if (rootEntitiesFilter.Empty)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.jobs.WorkerCount > 0)
|
||||||
|
{
|
||||||
|
Profiler.BeginSample("LocalToWorldCheck");
|
||||||
|
// Structural pre-pass: ensure LocalToWorld exists on all entities before parallel writes.
|
||||||
|
foreach (Entity entity in entitiesWithoutLocalToWorldFilter.Entities)
|
||||||
|
{
|
||||||
|
Set(entity, new LocalToWorld(Matrix4x4.Identity));
|
||||||
|
}
|
||||||
|
Profiler.EndSample();
|
||||||
|
|
||||||
|
Profiler.BeginSample("LocalToWorldUpdate");
|
||||||
|
this.jobs.Dispatch(rootEntitiesFilter.Count, updateWorldTransformByIndex);
|
||||||
|
Profiler.EndSample();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (Entity entity in rootEntitiesFilter.Entities)
|
||||||
|
{
|
||||||
|
Profiler.BeginSample("UpdateWorldTransform");
|
||||||
|
UpdateWorldTransform(entity, Matrix4x4.Identity);
|
||||||
|
Profiler.EndSample();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateWorldTransformByIndex(int entityFilterIndex)
|
||||||
|
{
|
||||||
|
using ProfilerScope scope = new("UpdateWorldTransformByIndex");
|
||||||
|
Entity entity = rootEntitiesFilter.NthEntity(entityFilterIndex);
|
||||||
|
UpdateWorldTransform(entity, Matrix4x4.Identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateWorldTransform(in Entity entity, Matrix4x4 localToWorldMatrix)
|
||||||
|
{
|
||||||
|
if (Has<LocalTransform>(entity))
|
||||||
|
{
|
||||||
|
LocalTransform localTransform = Get<LocalTransform>(entity);
|
||||||
|
localToWorldMatrix = Matrix4x4.Multiply(localToWorldMatrix, localTransform.TRS());
|
||||||
|
LocalToWorld localToWorld = new(localToWorldMatrix);
|
||||||
|
#if DEBUG
|
||||||
|
if (!Has<LocalToWorld>(entity))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Entity {entity} is missing LocalToWorld. Ensure the structural pre-pass runs before parallel dispatch.");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
Set(entity, localToWorld);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReverseSpanEnumerator<Entity> childEntities = World.InRelations<ChildParentRelation>(entity);
|
||||||
|
foreach (Entity childEntity in childEntities)
|
||||||
|
{
|
||||||
|
UpdateWorldTransform(childEntity, localToWorldMatrix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using MoonTools.ECS;
|
||||||
|
using Nerfed.Runtime.Scheduling;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Systems.Synthetic;
|
||||||
|
|
||||||
|
public sealed class DummyLongJobConsumerSystem : MoonTools.ECS.System, IParallelSystemMetadata
|
||||||
|
{
|
||||||
|
private readonly JobSystem jobs;
|
||||||
|
private readonly DummyLongJobSharedState state;
|
||||||
|
private readonly bool requireCompletion;
|
||||||
|
|
||||||
|
public DummyLongJobConsumerSystem(
|
||||||
|
World world,
|
||||||
|
DummyLongJobSharedState state,
|
||||||
|
JobSystem jobs,
|
||||||
|
bool requireCompletion) : base(world)
|
||||||
|
{
|
||||||
|
this.state = state;
|
||||||
|
this.jobs = jobs;
|
||||||
|
this.requireCompletion = requireCompletion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ScheduleName => nameof(DummyLongJobConsumerSystem);
|
||||||
|
|
||||||
|
// Consumer/apply stage is where structural world work should happen in real systems.
|
||||||
|
public SystemAccessDeclaration AccessDeclaration => SystemAccessDeclaration.Empty;
|
||||||
|
|
||||||
|
public override void Update(TimeSpan delta)
|
||||||
|
{
|
||||||
|
JobHandle handle = state.InFlight;
|
||||||
|
if (!handle.IsValid)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requireCompletion && !jobs.IsCompleted(handle))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs.Wait(handle);
|
||||||
|
jobs.TryForget(handle);
|
||||||
|
|
||||||
|
state.InFlight = default;
|
||||||
|
state.ConsumeCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using MoonTools.ECS;
|
||||||
|
using Nerfed.Runtime.Scheduling;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Systems.Synthetic;
|
||||||
|
|
||||||
|
public sealed class DummyLongJobProducerSystem : MoonTools.ECS.System, IParallelSystemMetadata
|
||||||
|
{
|
||||||
|
private readonly JobSystem jobs;
|
||||||
|
private readonly DummyLongJobSharedState state;
|
||||||
|
private readonly TimeSpan duration;
|
||||||
|
private readonly DummyWorkloadMode mode;
|
||||||
|
private readonly JobDeadlineMode deadline;
|
||||||
|
private int seed;
|
||||||
|
|
||||||
|
public DummyLongJobProducerSystem(
|
||||||
|
World world,
|
||||||
|
DummyLongJobSharedState state,
|
||||||
|
JobSystem jobs,
|
||||||
|
TimeSpan duration,
|
||||||
|
DummyWorkloadMode mode,
|
||||||
|
JobDeadlineMode deadline,
|
||||||
|
int initialSeed = 1) : base(world)
|
||||||
|
{
|
||||||
|
this.state = state;
|
||||||
|
this.jobs = jobs;
|
||||||
|
this.duration = duration;
|
||||||
|
this.mode = mode;
|
||||||
|
this.deadline = deadline;
|
||||||
|
seed = initialSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ScheduleName => nameof(DummyLongJobProducerSystem);
|
||||||
|
|
||||||
|
// Producer pattern: reads current world state and emits immutable job payload.
|
||||||
|
public SystemAccessDeclaration AccessDeclaration => SystemAccessDeclaration.Empty;
|
||||||
|
|
||||||
|
public override void Update(TimeSpan delta)
|
||||||
|
{
|
||||||
|
if (state.InFlight.IsValid)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int capturedSeed = seed;
|
||||||
|
state.InFlight = jobs.Submit(
|
||||||
|
() => state.LastResult = DummyWorkloadRunner.Run(duration, mode, capturedSeed),
|
||||||
|
deadline,
|
||||||
|
nameof(DummyLongJobProducerSystem));
|
||||||
|
|
||||||
|
seed = unchecked(capturedSeed + 1);
|
||||||
|
state.SubmitCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Nerfed.Runtime.Systems.Synthetic;
|
||||||
|
|
||||||
|
public sealed class DummyLongJobSharedState
|
||||||
|
{
|
||||||
|
public JobHandle InFlight;
|
||||||
|
public int LastResult;
|
||||||
|
public int SubmitCount;
|
||||||
|
public int ConsumeCount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using MoonTools.ECS;
|
||||||
|
using Nerfed.Runtime.Scheduling;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Systems.Synthetic;
|
||||||
|
|
||||||
|
public sealed class DummyMainThreadWorkSystem : MoonTools.ECS.System, IParallelSystemMetadata
|
||||||
|
{
|
||||||
|
private readonly TimeSpan duration;
|
||||||
|
private readonly DummyWorkloadMode mode;
|
||||||
|
private readonly string scheduleName;
|
||||||
|
private int seed;
|
||||||
|
|
||||||
|
public DummyMainThreadWorkSystem(
|
||||||
|
World world,
|
||||||
|
TimeSpan duration,
|
||||||
|
DummyWorkloadMode mode,
|
||||||
|
string scheduleName,
|
||||||
|
int initialSeed = 1) : base(world)
|
||||||
|
{
|
||||||
|
this.duration = duration;
|
||||||
|
this.mode = mode;
|
||||||
|
this.scheduleName = scheduleName;
|
||||||
|
seed = initialSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ScheduleName => scheduleName;
|
||||||
|
|
||||||
|
public SystemAccessDeclaration AccessDeclaration => SystemAccessDeclaration.Empty;
|
||||||
|
|
||||||
|
public override void Update(TimeSpan delta)
|
||||||
|
{
|
||||||
|
seed = DummyWorkloadRunner.Run(duration, mode, seed);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Nerfed.Runtime.Systems.Synthetic;
|
||||||
|
|
||||||
|
public enum DummyWorkloadMode
|
||||||
|
{
|
||||||
|
Spin,
|
||||||
|
Sleep,
|
||||||
|
Compute,
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Systems.Synthetic;
|
||||||
|
|
||||||
|
internal static class DummyWorkloadRunner
|
||||||
|
{
|
||||||
|
public static int Run(TimeSpan duration, DummyWorkloadMode mode, int seed)
|
||||||
|
{
|
||||||
|
return mode switch
|
||||||
|
{
|
||||||
|
DummyWorkloadMode.Sleep => Sleep(duration, seed),
|
||||||
|
DummyWorkloadMode.Compute => Compute(duration, seed),
|
||||||
|
_ => Spin(duration, seed),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Sleep(TimeSpan duration, int seed)
|
||||||
|
{
|
||||||
|
if (duration > TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
Thread.Sleep(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Spin(TimeSpan duration, int seed)
|
||||||
|
{
|
||||||
|
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||||
|
int value = seed;
|
||||||
|
while (stopwatch.Elapsed < duration)
|
||||||
|
{
|
||||||
|
value = unchecked((value * 1664525) + 1013904223);
|
||||||
|
Thread.SpinWait(128);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Compute(TimeSpan duration, int seed)
|
||||||
|
{
|
||||||
|
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||||
|
int value = seed;
|
||||||
|
while (stopwatch.Elapsed < duration)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 2048; i++)
|
||||||
|
{
|
||||||
|
value = unchecked((value << 5) - value + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime;
|
||||||
|
|
||||||
|
public class BoundedQueue<T> : IEnumerable<T>, ICollection, IReadOnlyCollection<T>
|
||||||
|
{
|
||||||
|
private readonly Queue<T> queue = null;
|
||||||
|
private readonly object syncLock = new object();
|
||||||
|
private readonly int maxSize = 10;
|
||||||
|
private T lastAddedElement;
|
||||||
|
|
||||||
|
public BoundedQueue(int maxSize)
|
||||||
|
{
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
queue = new Queue<T>(maxSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Enqueue(T item)
|
||||||
|
{
|
||||||
|
Enqueue(item, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Enqueue(T item, out T evictedItem)
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
queue.Enqueue(item);
|
||||||
|
if (queue.Count > maxSize)
|
||||||
|
{
|
||||||
|
evictedItem = queue.Dequeue();
|
||||||
|
lastAddedElement = item;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
evictedItem = default;
|
||||||
|
lastAddedElement = item;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Dequeue()
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
return queue.Dequeue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Peek()
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
return queue.Peek();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T LastAddedElement()
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
return lastAddedElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
queue.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(T item)
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
return queue.Contains(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterates the internal Queue<T> directly (struct enumerator, no allocation) under the lock.
|
||||||
|
public int CopyTo(List<T> destination)
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
destination.Clear();
|
||||||
|
foreach (T item in queue)
|
||||||
|
{
|
||||||
|
destination.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<T> GetEnumerator()
|
||||||
|
{
|
||||||
|
T[] snapshot;
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
snapshot = queue.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((IEnumerable<T>)snapshot).GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyTo(Array array, int index)
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
((ICollection)queue).CopyTo(array, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
return queue.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Capacity => maxSize;
|
||||||
|
public bool IsSynchronized => true;
|
||||||
|
public object SyncRoot => syncLock;
|
||||||
|
|
||||||
|
int IReadOnlyCollection<T>.Count
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
return queue.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace Nerfed.Runtime;
|
namespace Nerfed.Runtime;
|
||||||
|
|
||||||
public static class MathEx
|
public static class MathEx
|
||||||
@@ -17,4 +19,51 @@ public static class MathEx
|
|||||||
public static float Remap(float value, float oldMin, float oldMax, float newMin, float newMax) {
|
public static float Remap(float value, float oldMin, float oldMax, float newMin, float newMax) {
|
||||||
return (value - oldMin) / (oldMax - oldMin) * (newMax - newMin) + newMin;
|
return (value - oldMin) / (oldMax - oldMin) * (newMax - newMin) + newMin;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// https://stackoverflow.com/questions/70462758/c-sharp-how-to-convert-quaternions-to-euler-angles-xyz
|
||||||
|
public static Quaternion ToQuaternion(Vector3 v)
|
||||||
|
{
|
||||||
|
float cy = (float)Math.Cos(v.Z * 0.5);
|
||||||
|
float sy = (float)Math.Sin(v.Z * 0.5);
|
||||||
|
float cp = (float)Math.Cos(v.Y * 0.5);
|
||||||
|
float sp = (float)Math.Sin(v.Y * 0.5);
|
||||||
|
float cr = (float)Math.Cos(v.X * 0.5);
|
||||||
|
float sr = (float)Math.Sin(v.X * 0.5);
|
||||||
|
|
||||||
|
return new Quaternion
|
||||||
|
{
|
||||||
|
W = (cr * cp * cy + sr * sp * sy),
|
||||||
|
X = (sr * cp * cy - cr * sp * sy),
|
||||||
|
Y = (cr * sp * cy + sr * cp * sy),
|
||||||
|
Z = (cr * cp * sy - sr * sp * cy),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Vector3 ToEulerAngles(Quaternion q)
|
||||||
|
{
|
||||||
|
Vector3 angles = new();
|
||||||
|
|
||||||
|
// roll / x
|
||||||
|
double sinrCosp = 2 * (q.W * q.X + q.Y * q.Z);
|
||||||
|
double cosrCosp = 1 - 2 * (q.X * q.X + q.Y * q.Y);
|
||||||
|
angles.X = (float)Math.Atan2(sinrCosp, cosrCosp);
|
||||||
|
|
||||||
|
// pitch / y
|
||||||
|
double sinp = 2 * (q.W * q.Y - q.Z * q.X);
|
||||||
|
if (Math.Abs(sinp) >= 1)
|
||||||
|
{
|
||||||
|
angles.Y = (float)Math.CopySign(Math.PI / 2, sinp);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
angles.Y = (float)Math.Asin(sinp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// yaw / z
|
||||||
|
double sinyCosp = 2 * (q.W * q.Z + q.X * q.Y);
|
||||||
|
double cosyCosp = 1 - 2 * (q.Y * q.Y + q.Z * q.Z);
|
||||||
|
angles.Z = (float)Math.Atan2(sinyCosp, cosyCosp);
|
||||||
|
|
||||||
|
return angles;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Util;
|
||||||
|
|
||||||
|
public static class RandomId
|
||||||
|
{
|
||||||
|
public static uint GenerateSecureRandomUInt()
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[4];
|
||||||
|
RandomNumberGenerator.Fill(buffer);
|
||||||
|
return BitConverter.ToUInt32(buffer, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using MoonTools.ECS;
|
||||||
|
using Nerfed.Runtime.Components;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Nerfed.Runtime.Util
|
||||||
|
{
|
||||||
|
// https://github.com/needle-mirror/com.unity.entities/blob/master/Unity.Transforms/TransformHelpers.cs
|
||||||
|
public static class Transform
|
||||||
|
{
|
||||||
|
public static Vector3 Forward(in this Matrix4x4 matrix) => new Vector3(matrix.M31, matrix.M32, matrix.M33);
|
||||||
|
public static Vector3 Back(in this Matrix4x4 matrix) => -matrix.Forward();
|
||||||
|
public static Vector3 Up(in this Matrix4x4 matrix) => new Vector3(matrix.M21, matrix.M22, matrix.M23);
|
||||||
|
public static Vector3 Down(in this Matrix4x4 matrix) => -matrix.Up();
|
||||||
|
public static Vector3 Right(in this Matrix4x4 matrix) => new Vector3(matrix.M11, matrix.M12, matrix.M13);
|
||||||
|
public static Vector3 Left(in this Matrix4x4 matrix) => -matrix.Right();
|
||||||
|
//public static Vector3 Translation(in this Matrix4x4 matrix) => new Vector3();
|
||||||
|
//public static Quaternion Rotation(in this Matrix4x4 matrix) => new Quaternion();
|
||||||
|
|
||||||
|
public static Matrix4x4 TRS(in this LocalTransform localTransform)
|
||||||
|
{
|
||||||
|
return Matrix4x4.CreateScale(localTransform.scale) *
|
||||||
|
Matrix4x4.CreateFromQuaternion(localTransform.rotation) *
|
||||||
|
Matrix4x4.CreateTranslation(localTransform.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the parent child relation and adds a child component.
|
||||||
|
// Relation goes from child to parent.
|
||||||
|
public static void SetParent(in World world, in Entity child, in Entity parent)
|
||||||
|
{
|
||||||
|
RemoveParent(world, child);
|
||||||
|
|
||||||
|
world.Relate(child, parent, new ChildParentRelation());
|
||||||
|
world.Set(child, new Child());
|
||||||
|
world.Remove<Root>(child);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes any parent child relation ship, thus making it a 'root' object.
|
||||||
|
public static void RemoveParent(in World world, in Entity child)
|
||||||
|
{
|
||||||
|
if (!world.HasOutRelation<ChildParentRelation>(child))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity parent = world.OutRelationSingleton<ChildParentRelation>(child);
|
||||||
|
|
||||||
|
// TODO: Check if Unrelate all also unrelates incomming relations..?
|
||||||
|
world.Unrelate<ChildParentRelation>(child, parent);
|
||||||
|
world.Remove<Child>(child);
|
||||||
|
world.Set(child, new Root());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Entity CreateBaseEntity(this World world, string tag = "")
|
||||||
|
{
|
||||||
|
Entity entity = world.CreateEntity(tag);
|
||||||
|
world.Set(entity, new Root());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force update the transform data of an entity (and children).
|
||||||
|
// Useful for when you need precise up to date transform data.
|
||||||
|
public static void ForceUpdateLocalToWorld(in World world, in Entity entity)
|
||||||
|
{
|
||||||
|
Matrix4x4 parentLocalToWorldMatrix = Matrix4x4.Identity;
|
||||||
|
|
||||||
|
if (world.HasOutRelation<ChildParentRelation>(entity)) {
|
||||||
|
Entity parent = world.OutRelationSingleton<ChildParentRelation>(entity);
|
||||||
|
|
||||||
|
if (world.Has<LocalToWorld>(parent))
|
||||||
|
{
|
||||||
|
parentLocalToWorldMatrix = world.Get<LocalToWorld>(parent).localToWorldMatrix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ForceUpdateLocalToWorld(world, entity, parentLocalToWorldMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ForceUpdateLocalToWorld(in World world, in Entity entity, Matrix4x4 localToWorldMatrix)
|
||||||
|
{
|
||||||
|
if (world.Has<LocalTransform>(entity))
|
||||||
|
{
|
||||||
|
LocalTransform localTransform = world.Get<LocalTransform>(entity);
|
||||||
|
localToWorldMatrix = Matrix4x4.Multiply(localToWorldMatrix, localTransform.TRS());
|
||||||
|
LocalToWorld localToWorld = new(localToWorldMatrix);
|
||||||
|
world.Set(entity, localToWorld);
|
||||||
|
|
||||||
|
Log.Info($"Entity {entity} | local position {localTransform.position} | world position {localToWorldMatrix.Translation}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ReverseSpanEnumerator<Entity> childEntities = world.InRelations<ChildParentRelation>(entity);
|
||||||
|
foreach (Entity childEntity in childEntities)
|
||||||
|
{
|
||||||
|
ForceUpdateLocalToWorld(world, childEntity, localToWorldMatrix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ namespace Nerfed.Runtime;
|
|||||||
public enum ScreenMode
|
public enum ScreenMode
|
||||||
{
|
{
|
||||||
Fullscreen,
|
Fullscreen,
|
||||||
BorderlessFullscreen,
|
FullscreenBorderless,
|
||||||
Windowed
|
Windowed,
|
||||||
}
|
WindowedBorderless
|
||||||
|
}
|
||||||
@@ -11,6 +11,10 @@ namespace Nerfed.Runtime;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Window
|
public class Window
|
||||||
{
|
{
|
||||||
|
public event Action<Window, uint, uint> OnResizedEvent;
|
||||||
|
public event Action<Window, int, int> OnMovedEvent;
|
||||||
|
public event Action<Window> OnCloseEvent;
|
||||||
|
|
||||||
internal IntPtr Handle { get; }
|
internal IntPtr Handle { get; }
|
||||||
public ScreenMode ScreenMode { get; private set; }
|
public ScreenMode ScreenMode { get; private set; }
|
||||||
public uint Width { get; private set; }
|
public uint Width { get; private set; }
|
||||||
@@ -33,9 +37,7 @@ public class Window
|
|||||||
public string Title { get; private set;}
|
public string Title { get; private set;}
|
||||||
|
|
||||||
private bool IsDisposed;
|
private bool IsDisposed;
|
||||||
private System.Action<uint, uint> SizeChangeCallback = null;
|
private static readonly Dictionary<uint, Window> windowsById = new Dictionary<uint, Window>();
|
||||||
|
|
||||||
private static readonly Dictionary<uint, Window> windowsById = new Dictionary<uint, Window>();
|
|
||||||
|
|
||||||
public Window(GraphicsDevice graphicsDevice, WindowCreateInfo windowCreateInfo)
|
public Window(GraphicsDevice graphicsDevice, WindowCreateInfo windowCreateInfo)
|
||||||
{
|
{
|
||||||
@@ -54,10 +56,14 @@ public class Window
|
|||||||
{
|
{
|
||||||
flags |= SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN;
|
flags |= SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN;
|
||||||
}
|
}
|
||||||
else if (windowCreateInfo.ScreenMode == ScreenMode.BorderlessFullscreen)
|
else if (windowCreateInfo.ScreenMode == ScreenMode.FullscreenBorderless)
|
||||||
{
|
{
|
||||||
flags |= SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
|
flags |= SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||||
}
|
}
|
||||||
|
else if(windowCreateInfo.ScreenMode == ScreenMode.WindowedBorderless)
|
||||||
|
{
|
||||||
|
flags |= SDL.SDL_WindowFlags.SDL_WINDOW_BORDERLESS;
|
||||||
|
}
|
||||||
|
|
||||||
if (windowCreateInfo.SystemResizable)
|
if (windowCreateInfo.SystemResizable)
|
||||||
{
|
{
|
||||||
@@ -87,14 +93,6 @@ public class Window
|
|||||||
Width = (uint) width;
|
Width = (uint) width;
|
||||||
Height = (uint) height;
|
Height = (uint) height;
|
||||||
|
|
||||||
unsafe
|
|
||||||
{
|
|
||||||
byte* pixels = ImageUtils.GetPixelDataFromFile(Path.Combine(System.AppContext.BaseDirectory, "Assets", "Textures", "NerfedIcon.png"), out uint w, out uint h, out uint size);
|
|
||||||
nint icon = SDL.SDL_CreateRGBSurfaceFrom(new IntPtr(pixels), (int)w, (int)h, 8 * 4, (int)w * 4, 0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000);
|
|
||||||
SDL.SDL_SetWindowIcon(Handle, icon);
|
|
||||||
ImageUtils.FreePixelData(pixels);
|
|
||||||
}
|
|
||||||
|
|
||||||
windowsById.Add(SDL.SDL_GetWindowID(Handle), this);
|
windowsById.Add(SDL.SDL_GetWindowID(Handle), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,27 +110,33 @@ public class Window
|
|||||||
case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED:
|
case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||||
window.ProcessSizeChangedEvent(ref ev.window);
|
window.ProcessSizeChangedEvent(ref ev.window);
|
||||||
break;
|
break;
|
||||||
|
case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_MOVED:
|
||||||
|
window.ProcessMovedChangedEvent(ref ev.window);
|
||||||
|
break;
|
||||||
case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE:
|
case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE:
|
||||||
window.ProcessCloseEvent(ref ev.window);
|
window.ProcessCloseEvent(ref ev.window);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessSizeChangedEvent(ref SDL.SDL_WindowEvent ev)
|
private void ProcessSizeChangedEvent(ref SDL.SDL_WindowEvent ev)
|
||||||
{
|
{
|
||||||
uint newWidth = (uint)ev.data1;
|
uint newWidth = (uint)ev.data1;
|
||||||
uint newHeight = (uint)ev.data2;
|
uint newHeight = (uint)ev.data2;
|
||||||
Width = newWidth;
|
Width = newWidth;
|
||||||
Height = newHeight;
|
Height = newHeight;
|
||||||
|
|
||||||
if (SizeChangeCallback != null)
|
OnResizedEvent?.Invoke(this, Width, Height);
|
||||||
{
|
|
||||||
SizeChangeCallback(newWidth, newHeight);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessCloseEvent(ref SDL.SDL_WindowEvent ev)
|
private void ProcessMovedChangedEvent(ref SDL.SDL_WindowEvent ev)
|
||||||
|
{
|
||||||
|
OnMovedEvent?.Invoke(this, ev.data1, ev.data2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessCloseEvent(ref SDL.SDL_WindowEvent ev)
|
||||||
{
|
{
|
||||||
|
OnCloseEvent?.Invoke(this);
|
||||||
Engine.GraphicsDevice.UnclaimWindow(this);
|
Engine.GraphicsDevice.UnclaimWindow(this);
|
||||||
Dispose();
|
Dispose();
|
||||||
}
|
}
|
||||||
@@ -148,7 +152,7 @@ public class Window
|
|||||||
{
|
{
|
||||||
windowFlag = SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN;
|
windowFlag = SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN;
|
||||||
}
|
}
|
||||||
else if (screenMode == ScreenMode.BorderlessFullscreen)
|
else if (screenMode == ScreenMode.FullscreenBorderless)
|
||||||
{
|
{
|
||||||
windowFlag = SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
|
windowFlag = SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||||
}
|
}
|
||||||
@@ -203,15 +207,7 @@ public class Window
|
|||||||
SDL.SDL_ShowWindow(Handle);
|
SDL.SDL_ShowWindow(Handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected virtual void Dispose(bool disposing)
|
||||||
/// You can specify a method to run when the window size changes.
|
|
||||||
/// </summary>
|
|
||||||
public void RegisterSizeChangeCallback(System.Action<uint, uint> sizeChangeCallback)
|
|
||||||
{
|
|
||||||
SizeChangeCallback = sizeChangeCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
{
|
||||||
if (!IsDisposed)
|
if (!IsDisposed)
|
||||||
{
|
{
|
||||||
|
|||||||
+28
-6
@@ -5,16 +5,38 @@ VisualStudioVersion = 17.10.35013.160
|
|||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nerfed.Runtime", "Nerfed.Runtime\Nerfed.Runtime.csproj", "{98E09BAF-587F-4238-89BD-7693C036C233}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nerfed.Runtime", "Nerfed.Runtime\Nerfed.Runtime.csproj", "{98E09BAF-587F-4238-89BD-7693C036C233}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nerfed.Builder", "Nerfed.Builder\Nerfed.Builder.csproj", "{1B88DE56-2AD8-441E-9B10-073AA43840BF}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nerfed.Editor", "Nerfed.Editor\Nerfed.Editor.csproj", "{FF7D032D-7F0B-4700-A818-0606D66AECF8}"
|
||||||
|
ProjectSection(ProjectDependencies) = postProject
|
||||||
|
{1B88DE56-2AD8-441E-9B10-073AA43840BF} = {1B88DE56-2AD8-441E-9B10-073AA43840BF}
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Test|x64 = Test|x64
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|x64 = Release|x64
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{98E09BAF-587F-4238-89BD-7693C036C233}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{1B88DE56-2AD8-441E-9B10-073AA43840BF}.Test|x64.ActiveCfg = Test|x64
|
||||||
{98E09BAF-587F-4238-89BD-7693C036C233}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{1B88DE56-2AD8-441E-9B10-073AA43840BF}.Test|x64.Build.0 = Test|x64
|
||||||
{98E09BAF-587F-4238-89BD-7693C036C233}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{1B88DE56-2AD8-441E-9B10-073AA43840BF}.Release|x64.ActiveCfg = Release|x64
|
||||||
{98E09BAF-587F-4238-89BD-7693C036C233}.Release|Any CPU.Build.0 = Release|Any CPU
|
{1B88DE56-2AD8-441E-9B10-073AA43840BF}.Release|x64.Build.0 = Release|x64
|
||||||
|
{1B88DE56-2AD8-441E-9B10-073AA43840BF}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{1B88DE56-2AD8-441E-9B10-073AA43840BF}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{FF7D032D-7F0B-4700-A818-0606D66AECF8}.Test|x64.ActiveCfg = Test|x64
|
||||||
|
{FF7D032D-7F0B-4700-A818-0606D66AECF8}.Test|x64.Build.0 = Test|x64
|
||||||
|
{FF7D032D-7F0B-4700-A818-0606D66AECF8}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{FF7D032D-7F0B-4700-A818-0606D66AECF8}.Release|x64.Build.0 = Release|x64
|
||||||
|
{FF7D032D-7F0B-4700-A818-0606D66AECF8}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{FF7D032D-7F0B-4700-A818-0606D66AECF8}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{98E09BAF-587F-4238-89BD-7693C036C233}.Test|x64.ActiveCfg = Test|x64
|
||||||
|
{98E09BAF-587F-4238-89BD-7693C036C233}.Test|x64.Build.0 = Test|x64
|
||||||
|
{98E09BAF-587F-4238-89BD-7693C036C233}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{98E09BAF-587F-4238-89BD-7693C036C233}.Release|x64.Build.0 = Release|x64
|
||||||
|
{98E09BAF-587F-4238-89BD-7693C036C233}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{98E09BAF-587F-4238-89BD-7693C036C233}.Debug|x64.Build.0 = Debug|x64
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
# Nerfed
|
# Nerfed
|
||||||
|
|
||||||
nerfed game engine
|
nerfed game engine
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Third-Party Licenses
|
||||||
|
|
||||||
|
This project includes third-party libraries with their respective licenses, which can be found in the [THIRD_PARTY_LICENSES](THIRD_PARTY_LICENSES) file.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Third-Party Licenses
|
||||||
|
|
||||||
|
## ImGui
|
||||||
|
|
||||||
|
**Name:** Dear ImGui
|
||||||
|
**License:** MIT License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2014-2022 Omar Cornut
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"runtimeTarget": {
|
||||||
|
"name": ".NETCoreApp,Version=v8.0",
|
||||||
|
"signature": ""
|
||||||
|
},
|
||||||
|
"compilationOptions": {},
|
||||||
|
"targets": {
|
||||||
|
".NETCoreApp,Version=v8.0": {
|
||||||
|
"Nerfed.Builder/1.0.0": {
|
||||||
|
"runtime": {
|
||||||
|
"Nerfed.Builder.dll": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libraries": {
|
||||||
|
"Nerfed.Builder/1.0.0": {
|
||||||
|
"type": "project",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user