diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..6e2e244 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "1.3.0", + "commands": [ + "csharpier" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.csharpierignore b/.csharpierignore new file mode 100644 index 0000000..ae5e493 --- /dev/null +++ b/.csharpierignore @@ -0,0 +1,6 @@ +# only format our own code +Cafe-Shader-Studio/ + +# Build artifacts +**/bin/ +**/obj/ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4dce82c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,30 @@ +# autonormalize, and checkout as native +* text=auto + +# these will break with crlf +*.sh text eol=lf +*.nix text eol=lf +*.lock text eol=lf + +# These are binaries +*.dll binary +*.exe binary +*.pdb binary +*.so binary +*.dylib binary +*.zip binary +*.dds binary +*.png binary +*.jpg binary +*.jpeg binary +*.bmp binary +*.tif binary +*.ico binary +*.ttf binary +*.otf binary +*.wav binary +*.ogg binary +*.bin binary +*.bfres binary +*.szs binary +*.sarc binary diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..756130a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +name: Build + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore + run: dotnet restore PlayerViewer/PlayerViewer.csproj + + - name: Publish + run: > + dotnet publish PlayerViewer/PlayerViewer.csproj + -c Release + -r win-x64 + --self-contained true + -p:PublishSingleFile=true + -o publish + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: PlayerViewer-win-x64 + path: publish + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 9e74ff8..db0a0ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,12 @@ bin/ obj/ +# nix +result +result-* +.direnv/ +# dotnet publish output / local nuget cache +publish/ +.nuget/ .vs/ .vscode/ .cursor/ diff --git a/Cafe-Shader-Studio/AGraphicsLibrary/AGraphicsLibrary.csproj b/Cafe-Shader-Studio/AGraphicsLibrary/AGraphicsLibrary.csproj index 3f4682b..b2b0052 100644 --- a/Cafe-Shader-Studio/AGraphicsLibrary/AGraphicsLibrary.csproj +++ b/Cafe-Shader-Studio/AGraphicsLibrary/AGraphicsLibrary.csproj @@ -1,7 +1,7 @@  - netstandard2.1 + net10.0 AnyCPU;x86 @@ -22,13 +22,13 @@ - + - + ..\CafeShaderStudio\Lib\AampLibraryCSharp.dll @@ -36,9 +36,7 @@ ..\BfresEditor\Lib\BfresLibrary.dll - - ..\CafeShaderStudio\Lib\OpenTK.dll - + ..\CafeShaderStudio\Lib\Syroot.BinaryData.dll @@ -49,5 +47,5 @@ ..\CafeShaderStudio\Lib\Toolbox.Core.dll - - \ No newline at end of file + + diff --git a/Cafe-Shader-Studio/BfresEditor/Bfres/Render/SPL3/HoianNXRender.cs b/Cafe-Shader-Studio/BfresEditor/Bfres/Render/SPL3/HoianNXRender.cs index 10decbb..eeb7813 100644 --- a/Cafe-Shader-Studio/BfresEditor/Bfres/Render/SPL3/HoianNXRender.cs +++ b/Cafe-Shader-Studio/BfresEditor/Bfres/Render/SPL3/HoianNXRender.cs @@ -448,7 +448,7 @@ public override void ReloadRenderState(BfresMeshAsset mesh) mesh.Pass = Pass.TRANSPARENT; //Materials that sample the game framebuffer (gsys_enable_color_buffer=1) - //must render in the transparent pass so the pipeline can capture the + //must render in the transparent pass so the pipeline can capture the //framebuffer between passes. bool usesFramebuffer = (mat.ShaderOptions.TryGetValue("gsys_enable_color_buffer", out string ecb) && ecb == "1"); @@ -967,9 +967,9 @@ public static void InitTextures() if (WhiteTexture != null) return; - WhiteTexture = GLTexture2D.FromBitmap(Resources.white); - BlackTexture = GLTexture2D.FromBitmap(Resources.black); - WhiteArrayTexture = GLTexture2DArray.FromBitmap(Resources.white); + WhiteTexture = GLTexture2D.FromRgba(new byte[] { 255, 255, 255, 255 }, 1, 1); + BlackTexture = GLTexture2D.FromRgba(new byte[] { 0, 0, 0, 255 }, 1, 1); + WhiteArrayTexture = GLTexture2DArray.FromRgba(new byte[] { 255, 255, 255, 255 }, 1, 1); CubemapTexture = GLTextureCube.FromDDS(new DDS(new MemoryStream(Resources.CubemapLightmap))); BrdfTexture = GLTexture2D.FromGeneric(new DDS(new MemoryStream(Resources.brdf)), new ImageParameters()); FlipTextureY(BrdfTexture); diff --git a/Cafe-Shader-Studio/BfresEditor/BfresEditor.csproj b/Cafe-Shader-Studio/BfresEditor/BfresEditor.csproj index 2be1392..7e4a653 100644 --- a/Cafe-Shader-Studio/BfresEditor/BfresEditor.csproj +++ b/Cafe-Shader-Studio/BfresEditor/BfresEditor.csproj @@ -1,7 +1,7 @@ - net6.0 + net10.0 AnyCPU;x64;x86 BfresEditor @@ -30,8 +30,11 @@ - + + + + @@ -54,9 +57,7 @@ ..\CafeShaderStudio\Lib\LZ4.Frame.dll - - ..\CafeShaderStudio\Lib\OpenTK.dll - + ..\CafeShaderStudio\Lib\Ryujinx.Common.dll @@ -78,9 +79,6 @@ ..\CafeShaderStudio\Lib\TrackStudioLibrary.dll - - ..\CafeShaderStudio\Lib\ZstdNet.dll - diff --git a/Cafe-Shader-Studio/BfresEditor/Lib/BfresLibrary.pdb b/Cafe-Shader-Studio/BfresEditor/Lib/BfresLibrary.pdb deleted file mode 100644 index 0d2e4dd..0000000 Binary files a/Cafe-Shader-Studio/BfresEditor/Lib/BfresLibrary.pdb and /dev/null differ diff --git a/Cafe-Shader-Studio/BfresEditor/Lib/BfshaLibrary.pdb b/Cafe-Shader-Studio/BfresEditor/Lib/BfshaLibrary.pdb deleted file mode 100644 index a4bcf69..0000000 Binary files a/Cafe-Shader-Studio/BfresEditor/Lib/BfshaLibrary.pdb and /dev/null differ diff --git a/Cafe-Shader-Studio/BfresEditor/Zstb/ZSTB.cs b/Cafe-Shader-Studio/BfresEditor/Zstb/ZSTB.cs index 82068a4..c0aafea 100644 --- a/Cafe-Shader-Studio/BfresEditor/Zstb/ZSTB.cs +++ b/Cafe-Shader-Studio/BfresEditor/Zstb/ZSTB.cs @@ -40,23 +40,23 @@ public Stream Compress(Stream stream) public static byte[] SDecompress(byte[] b) { - using (var decompressor = new ZstdNet.Decompressor()) + using (var decompressor = new ZstdSharp.Decompressor()) { - return decompressor.Unwrap(b); + return decompressor.Unwrap(b).ToArray(); } } public static byte[] SDecompress(byte[] b, int MaxDecompressedSize) { - using (var decompressor = new ZstdNet.Decompressor()) + using (var decompressor = new ZstdSharp.Decompressor()) { - return decompressor.Unwrap(b, MaxDecompressedSize); + return decompressor.Unwrap(b, MaxDecompressedSize).ToArray(); } } public static byte[] SCompress(byte[] b) { - using (var compressor = new ZstdNet.Compressor()) + using (var compressor = new ZstdSharp.Compressor()) { - return compressor.Wrap(b); + return compressor.Wrap(b).ToArray(); } } } diff --git a/Cafe-Shader-Studio/CafeShaderStudio/CafeShaderStudio.csproj b/Cafe-Shader-Studio/CafeShaderStudio/CafeShaderStudio.csproj index 51cb219..320772a 100644 --- a/Cafe-Shader-Studio/CafeShaderStudio/CafeShaderStudio.csproj +++ b/Cafe-Shader-Studio/CafeShaderStudio/CafeShaderStudio.csproj @@ -3,7 +3,7 @@ Exe - net6.0 + net10.0 true AnyCPU;x64;x86 @@ -38,9 +38,7 @@ False Lib\ImGui.NET.dll - - Lib\OpenTK.dll - + Lib\OpenTK.GLControl.dll @@ -56,9 +54,6 @@ Lib\TrackStudioLibrary.dll - - Lib\ZstdNet.dll - @@ -272,6 +267,9 @@ + + @@ -419,12 +417,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - diff --git a/Cafe-Shader-Studio/CafeShaderStudio/Lib/AampLibraryCSharp.pdb b/Cafe-Shader-Studio/CafeShaderStudio/Lib/AampLibraryCSharp.pdb deleted file mode 100644 index 64e6b08..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/Lib/AampLibraryCSharp.pdb and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/Lib/ByamlExt.pdb b/Cafe-Shader-Studio/CafeShaderStudio/Lib/ByamlExt.pdb deleted file mode 100644 index e2e264a..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/Lib/ByamlExt.pdb and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/Lib/ImGui.NET.pdb b/Cafe-Shader-Studio/CafeShaderStudio/Lib/ImGui.NET.pdb deleted file mode 100644 index 115c518..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/Lib/ImGui.NET.pdb and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/Lib/OpenTK.dll b/Cafe-Shader-Studio/CafeShaderStudio/Lib/OpenTK.dll deleted file mode 100644 index 806e370..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/Lib/OpenTK.dll and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/Lib/OpenTK.zip b/Cafe-Shader-Studio/CafeShaderStudio/Lib/OpenTK.zip deleted file mode 100644 index 7b8d9f7..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/Lib/OpenTK.zip and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/Lib/Ryujinx.Common.pdb b/Cafe-Shader-Studio/CafeShaderStudio/Lib/Ryujinx.Common.pdb deleted file mode 100644 index 6846f7e..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/Lib/Ryujinx.Common.pdb and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/Lib/Ryujinx.Graphics.Shader.pdb b/Cafe-Shader-Studio/CafeShaderStudio/Lib/Ryujinx.Graphics.Shader.pdb deleted file mode 100644 index 4a0632f..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/Lib/Ryujinx.Graphics.Shader.pdb and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/Lib/SharpYaml.dll b/Cafe-Shader-Studio/CafeShaderStudio/Lib/SharpYaml.dll deleted file mode 100644 index 8ed6dba..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/Lib/SharpYaml.dll and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/Lib/Toolbox.Core.pdb b/Cafe-Shader-Studio/CafeShaderStudio/Lib/Toolbox.Core.pdb deleted file mode 100644 index e7b4fae..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/Lib/Toolbox.Core.pdb and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/Lib/Toolbox.Core.zip b/Cafe-Shader-Studio/CafeShaderStudio/Lib/Toolbox.Core.zip deleted file mode 100644 index df0e4b1..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/Lib/Toolbox.Core.zip and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/Lib/TrackStudioLibrary.pdb b/Cafe-Shader-Studio/CafeShaderStudio/Lib/TrackStudioLibrary.pdb deleted file mode 100644 index e826439..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/Lib/TrackStudioLibrary.pdb and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/Lib/ZstdNet.dll b/Cafe-Shader-Studio/CafeShaderStudio/Lib/ZstdNet.dll deleted file mode 100644 index fa01c89..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/Lib/ZstdNet.dll and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/cimgui32.dll b/Cafe-Shader-Studio/CafeShaderStudio/cimgui32.dll deleted file mode 100644 index 77c87e0..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/cimgui32.dll and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/debug_test_out.png b/Cafe-Shader-Studio/CafeShaderStudio/debug_test_out.png deleted file mode 100644 index ae4ab4f..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/debug_test_out.png and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/tinyfiledialogs32.dll b/Cafe-Shader-Studio/CafeShaderStudio/tinyfiledialogs32.dll deleted file mode 100644 index 161b042..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/tinyfiledialogs32.dll and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/tinyfiledialogs64.dll b/Cafe-Shader-Studio/CafeShaderStudio/tinyfiledialogs64.dll deleted file mode 100644 index 3c4b6e7..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/tinyfiledialogs64.dll and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/x64/libzstd.dll b/Cafe-Shader-Studio/CafeShaderStudio/x64/libzstd.dll deleted file mode 100644 index a836692..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/x64/libzstd.dll and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/x86/cimgui.win-x86.dll b/Cafe-Shader-Studio/CafeShaderStudio/x86/cimgui.win-x86.dll deleted file mode 100644 index 77c87e0..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/x86/cimgui.win-x86.dll and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeShaderStudio/x86/libzstd.dll b/Cafe-Shader-Studio/CafeShaderStudio/x86/libzstd.dll deleted file mode 100644 index 3ec45c6..0000000 Binary files a/Cafe-Shader-Studio/CafeShaderStudio/x86/libzstd.dll and /dev/null differ diff --git a/Cafe-Shader-Studio/CafeStudio.UI/CafeStudio.UI.csproj b/Cafe-Shader-Studio/CafeStudio.UI/CafeStudio.UI.csproj index 83f350a..e84555b 100644 --- a/Cafe-Shader-Studio/CafeStudio.UI/CafeStudio.UI.csproj +++ b/Cafe-Shader-Studio/CafeStudio.UI/CafeStudio.UI.csproj @@ -1,7 +1,7 @@  - netstandard2.1 + net10.0 AnyCPU;x86 true @@ -23,14 +23,15 @@ - + + - + ..\CafeShaderStudio\Lib\CurveEditorLibrary.dll @@ -38,18 +39,13 @@ ..\CafeShaderStudio\Lib\ImGui.NET.dll - - ..\CafeShaderStudio\Lib\OpenTK.dll - + ..\CafeShaderStudio\Lib\Syroot.BinaryData.dll ..\CafeShaderStudio\Lib\Syroot.Maths.dll - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.4.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - ..\CafeShaderStudio\Lib\Toolbox.Core.dll @@ -57,4 +53,4 @@ ..\CafeShaderStudio\Lib\TrackStudioLibrary.dll - \ No newline at end of file + diff --git a/Cafe-Shader-Studio/GLFrameworkEngine/GLFrameworkEngine.csproj b/Cafe-Shader-Studio/GLFrameworkEngine/GLFrameworkEngine.csproj index 2d66ab8..0a3c075 100644 --- a/Cafe-Shader-Studio/GLFrameworkEngine/GLFrameworkEngine.csproj +++ b/Cafe-Shader-Studio/GLFrameworkEngine/GLFrameworkEngine.csproj @@ -1,7 +1,7 @@  - netstandard2.1 + net10.0 AnyCPU;x64;x86 @@ -22,16 +22,15 @@ - + + - + - - ..\CafeShaderStudio\Lib\OpenTK.dll - + ..\CafeShaderStudio\Lib\Toolbox.Core.dll - - \ No newline at end of file + + diff --git a/Cafe-Shader-Studio/GLFrameworkEngine/Objects/Texture/GLTexture2D.cs b/Cafe-Shader-Studio/GLFrameworkEngine/Objects/Texture/GLTexture2D.cs index fa2eb9c..5ba643d 100644 --- a/Cafe-Shader-Studio/GLFrameworkEngine/Objects/Texture/GLTexture2D.cs +++ b/Cafe-Shader-Studio/GLFrameworkEngine/Objects/Texture/GLTexture2D.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using OpenTK.Graphics.OpenGL; using System.Drawing; using Toolbox.Core; @@ -91,14 +89,25 @@ public static GLTexture2D FromGeneric(STGenericTexture texture, ImageParameters return glTexture; } + //Decodes an encoded image (PNG/JPEG/...) to RGBA and uploads it with ImageSharp. public static GLTexture2D FromBitmap(byte[] imageFile) { - Bitmap image = (Bitmap)Bitmap.FromStream(new System.IO.MemoryStream(imageFile)); + using var image = SixLabors.ImageSharp.Image.Load(imageFile); + byte[] rgba = new byte[image.Width * image.Height * 4]; + image.CopyPixelDataTo(rgba); + return FromRgba(rgba, image.Width, image.Height); + } + public static GLTexture2D FromRgba(byte[] rgba, int width, int height) + { GLTexture2D texture = new GLTexture2D(); texture.Target = TextureTarget.Texture2D; - texture.Width = image.Width; texture.Height = image.Height; - texture.LoadImage(image); + texture.Width = width; texture.Height = height; + texture.Bind(); + GL.TexImage2D(texture.Target, 0, PixelInternalFormat.Rgba, width, height, 0, + PixelFormat.Rgba, PixelType.UnsignedByte, rgba); + GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); + texture.Unbind(); return texture; } diff --git a/Cafe-Shader-Studio/GLFrameworkEngine/Objects/Texture/GLTexture2DArray.cs b/Cafe-Shader-Studio/GLFrameworkEngine/Objects/Texture/GLTexture2DArray.cs index dc8a08c..0d0ba44 100644 --- a/Cafe-Shader-Studio/GLFrameworkEngine/Objects/Texture/GLTexture2DArray.cs +++ b/Cafe-Shader-Studio/GLFrameworkEngine/Objects/Texture/GLTexture2DArray.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using OpenTK.Graphics.OpenGL; using System.Drawing; using Toolbox.Core; @@ -133,30 +131,6 @@ private static byte[] FlipHorizontal(int Width, int Height, byte[] Input) return FlippedOutput; } - private static byte[] FlipVertical(int Width, int Height, byte[] Input) - { - byte[] FlippedOutput = new byte[Width * Height * 4]; - - int Stride = Width * 4; - for (int Y = 0; Y < Height; Y++) - { - int IOffs = Stride * Y; - int OOffs = Stride * (Height - 1 - Y); - - for (int X = 0; X < Width; X++) - { - FlippedOutput[OOffs + 0] = Input[IOffs + 0]; - FlippedOutput[OOffs + 1] = Input[IOffs + 1]; - FlippedOutput[OOffs + 2] = Input[IOffs + 2]; - FlippedOutput[OOffs + 3] = Input[IOffs + 3]; - - IOffs += 4; - OOffs += 4; - } - } - return FlippedOutput; - } - public static GLTexture2DArray FromDDS(DDS[] dds, bool flipY = false) { GLTexture2DArray texture = new GLTexture2DArray(); @@ -217,6 +191,18 @@ public static GLTexture2DArray FromBitmap(Bitmap image) return texture; } + public static GLTexture2DArray FromRgba(byte[] rgba, int width, int height) + { + GLTexture2DArray texture = new GLTexture2DArray(); + texture.Width = width; texture.Height = height; + texture.Bind(); + GL.TexImage3D(texture.Target, 0, PixelInternalFormat.Rgba, width, height, 1, 0, + OpenTK.Graphics.OpenGL.PixelFormat.Rgba, PixelType.UnsignedByte, rgba); + GL.GenerateMipmap(GenerateMipmapTarget.Texture2DArray); + texture.Unbind(); + return texture; + } + public static GLTexture2DArray FromRawData(int width, int height, TexFormat format, byte[] data) { GLTexture2DArray texture = new GLTexture2DArray(); diff --git a/Cafe-Shader-Studio/RedStarLibrary/RedStarLibrary.csproj b/Cafe-Shader-Studio/RedStarLibrary/RedStarLibrary.csproj index 9792fbd..1c92abb 100644 --- a/Cafe-Shader-Studio/RedStarLibrary/RedStarLibrary.csproj +++ b/Cafe-Shader-Studio/RedStarLibrary/RedStarLibrary.csproj @@ -1,7 +1,7 @@ - net6.0 + net10.0 AnyCPU;x86 @@ -25,9 +25,7 @@ ..\CafeShaderStudio\Lib\ImGui.NET.dll - - ..\CafeShaderStudio\Lib\OpenTK.dll - + ..\CafeShaderStudio\Lib\Syroot.BinaryData.dll diff --git a/PlayerViewer/Core/AppConfig.cs b/PlayerViewer/Core/AppConfig.cs index 1561f73..628cdd6 100644 --- a/PlayerViewer/Core/AppConfig.cs +++ b/PlayerViewer/Core/AppConfig.cs @@ -4,7 +4,36 @@ namespace PlayerViewer.Core { - + /// + /// Composited export/viewport background. Part of so it travels + /// with a preset (a preset captures the whole look: gear, colors, and background). + /// + public class BackgroundConfig + { + public int Mode; //0 Transparent, 1 Color, 2 Image + public float[] Color = { 0f, 1f, 0f }; //Color mode; green reproduces the old greenscreen + public string ImagePath = ""; + public int ScaleMode; //0 Fill, 1 Fit, 2 Stretch + public float Zoom = 1f; + public float OffsetX; + public float OffsetY; + public bool Tile; + public int TileX = 1; + public int TileY = 1; + + //Clamp user-supplied (preset/settings) values into valid ranges. + public void Normalize() + { + Mode = System.Math.Clamp(Mode, 0, 2); + ScaleMode = System.Math.Clamp(ScaleMode, 0, 2); + if (Color == null || Color.Length < 3) + Color = new[] { 0f, 1f, 0f }; + ImagePath ??= ""; + TileX = System.Math.Max(1, TileX); + TileY = System.Math.Max(1, TileY); + } + } + public class PlayerConfig { public int PlayerType; @@ -32,11 +61,13 @@ public class PlayerConfig public float[] CustomAlpha = { 0.925f, 0.243f, 0.549f }; public float[] CustomBravo = { 0.196f, 0.855f, 0.302f }; public float[] CustomCharlie = { 0.980f, 0.769f, 0.196f }; - } + //Composited export/viewport background, saved and loaded with the preset. + public BackgroundConfig Background = new(); + } /// - /// Persisted app configuration (romfs path etc). Stored next to the executable. + /// Persisted app configuration (romfs paths etc). Stored in the per-user data folder. /// public class AppConfig { @@ -47,9 +78,37 @@ public class AppConfig public int WindowWidth = 1600; public int WindowHeight = 900; + //--- Export/capture settings (configured in the Settings window) + //Trim fully-transparent deadspace off exported frames. Uses the transparent + //render as an alpha oracle, so it also crops greenscreen MP4s. + public bool TrimDeadspace = false; + + //Extra pixels of transparent margin kept around the content bounding box. + public int TrimMarginPx = 0; + + //WebP encode quality: 100 = lossless (bit-exact), below = lossy (smaller/faster). + public int WebpQuality = 100; + + //Export supersample factor (1-8). Exports render internally at this multiple of the + //capture size; with trim on, the crop keeps that internal resolution so a loosely + //framed subject still exports sharp. VRAM and temp-disk use scale with the square. + public int ExportSupersample = 1; + + //Physics warm-up: plays the animation (/ first animation in the sequence) through + //this many extra times before recording starts without capturing. Physics reset + //whenever an animation loads, so frame 0 has a twitch each time the exported + //WebP/WebM loops. A warm-up lets the sim settle first. 0 = disabled. + public int PrerollLoops = 1; + + //--- Capture-panel selections (persisted so they stick between runs) + public int CaptureResIndex = 2; //index into the resolution dropdown + public int ExportFormat = 0; //0 PNG, 1 MP4, 2 WebP, 3 WebM + public int ExportFps = 60; + public int AnimMode = 0; //0 Single, 1 Sequence + public PlayerConfig Player = new(); - static string FilePath => Path.Combine(AppContext.BaseDirectory, "playerviewer_config.json"); + static string FilePath => Path.Combine(AppPaths.DataDir, "settings.json"); public static AppConfig Load() { @@ -57,10 +116,14 @@ public static AppConfig Load() { if (File.Exists(FilePath)) { - var config = JsonConvert.DeserializeObject(File.ReadAllText(FilePath)) ?? new AppConfig(); + var config = + JsonConvert.DeserializeObject(File.ReadAllText(FilePath)) + ?? new AppConfig(); //Guard against corrupt/zero sizes (e.g. saved while minimized). - if (config.WindowWidth < 200) config.WindowWidth = 1600; - if (config.WindowHeight < 200) config.WindowHeight = 900; + if (config.WindowWidth < 200) + config.WindowWidth = 1600; + if (config.WindowHeight < 200) + config.WindowHeight = 900; return config; } } diff --git a/PlayerViewer/Core/AppPaths.cs b/PlayerViewer/Core/AppPaths.cs new file mode 100644 index 0000000..7fa584a --- /dev/null +++ b/PlayerViewer/Core/AppPaths.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics; +using System.IO; + +namespace PlayerViewer.Core +{ + /// + /// Per-user data directory, created on first access: %APPDATA%\PlayerViewer on Windows, + /// ~/.config/PlayerViewer on Linux/macOS. Holds settings.json and an optional bundled ffmpeg. + /// + public static class AppPaths + { + public static string DataDir { get; } = CreateDataDir(); + + static string CreateDataDir() + { + string dir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "PlayerViewer" + ); + Directory.CreateDirectory(dir); + return dir; + } + + /// Opens the data directory in the OS file browser. + public static void OpenDataDir() + { + try + { + if (OperatingSystem.IsWindows()) + Process.Start( + new ProcessStartInfo { FileName = DataDir, UseShellExecute = true } + ); + else if (OperatingSystem.IsMacOS()) + Process.Start("open", DataDir); + else + Process.Start("xdg-open", DataDir); + } + catch { } + } + } +} diff --git a/PlayerViewer/Core/Formats/Byml.cs b/PlayerViewer/Core/Formats/Byml.cs index f7c4b66..062b471 100644 --- a/PlayerViewer/Core/Formats/Byml.cs +++ b/PlayerViewer/Core/Formats/Byml.cs @@ -36,7 +36,9 @@ public Byml(byte[] data) } uint ReadU32(int offset) => BitConverter.ToUInt32(_data, offset); - uint ReadU24(int offset) => (uint)(_data[offset] | (_data[offset + 1] << 8) | (_data[offset + 2] << 16)); + + uint ReadU24(int offset) => + (uint)(_data[offset] | (_data[offset + 1] << 8) | (_data[offset + 2] << 16)); string[] ReadStringTable(uint offset) { @@ -47,7 +49,8 @@ string[] ReadStringTable(uint offset) { uint strOff = offset + ReadU32((int)(offset + 4 + i * 4)); int end = (int)strOff; - while (_data[end] != 0) end++; + while (_data[end] != 0) + end++; result[i] = Encoding.UTF8.GetString(_data, (int)strOff, end - (int)strOff); } return result; @@ -57,17 +60,28 @@ object ReadNode(byte type, uint valueOrOffset) { switch (type) { - case 0xA0: return _strings[ReadU32((int)valueOrOffset)]; - case 0xC0: return ReadArray(valueOrOffset); - case 0xC1: return ReadHash(valueOrOffset); - case 0xD0: return ReadU32((int)valueOrOffset) != 0; - case 0xD1: return BitConverter.ToInt32(_data, (int)valueOrOffset); - case 0xD2: return BitConverter.ToSingle(_data, (int)valueOrOffset); - case 0xD3: return ReadU32((int)valueOrOffset); - case 0xD4: return BitConverter.ToInt64(_data, (int)ReadU32((int)valueOrOffset)); - case 0xD5: return BitConverter.ToUInt64(_data, (int)ReadU32((int)valueOrOffset)); - case 0xD6: return BitConverter.ToDouble(_data, (int)ReadU32((int)valueOrOffset)); - case 0xFF: return null; + case 0xA0: + return _strings[ReadU32((int)valueOrOffset)]; + case 0xC0: + return ReadArray(valueOrOffset); + case 0xC1: + return ReadHash(valueOrOffset); + case 0xD0: + return ReadU32((int)valueOrOffset) != 0; + case 0xD1: + return BitConverter.ToInt32(_data, (int)valueOrOffset); + case 0xD2: + return BitConverter.ToSingle(_data, (int)valueOrOffset); + case 0xD3: + return ReadU32((int)valueOrOffset); + case 0xD4: + return BitConverter.ToInt64(_data, (int)ReadU32((int)valueOrOffset)); + case 0xD5: + return BitConverter.ToUInt64(_data, (int)ReadU32((int)valueOrOffset)); + case 0xD6: + return BitConverter.ToDouble(_data, (int)ReadU32((int)valueOrOffset)); + case 0xFF: + return null; default: throw new NotSupportedException($"BYML node type 0x{type:X2} not supported."); } @@ -116,26 +130,53 @@ Dictionary ReadHash(uint offset) #region typed access helpers - public static Dictionary AsHash(object node) => node as Dictionary; + public static Dictionary AsHash(object node) => + node as Dictionary; + public static List AsArray(object node) => node as List; - public static string GetString(Dictionary hash, string key, string fallback = "") - => hash != null && hash.TryGetValue(key, out var v) && v is string s ? s : fallback; + public static string GetString( + Dictionary hash, + string key, + string fallback = "" + ) => hash != null && hash.TryGetValue(key, out var v) && v is string s ? s : fallback; public static int GetInt(Dictionary hash, string key, int fallback = 0) { - if (hash == null || !hash.TryGetValue(key, out var v)) return fallback; - return v switch { int i => i, uint u => (int)u, float f => (int)f, _ => fallback }; + if (hash == null || !hash.TryGetValue(key, out var v)) + return fallback; + return v switch + { + int i => i, + uint u => (int)u, + float f => (int)f, + _ => fallback, + }; } - public static float GetFloat(Dictionary hash, string key, float fallback = 0f) + public static float GetFloat( + Dictionary hash, + string key, + float fallback = 0f + ) { - if (hash == null || !hash.TryGetValue(key, out var v)) return fallback; - return v switch { float f => f, int i => i, uint u => u, double d => (float)d, _ => fallback }; + if (hash == null || !hash.TryGetValue(key, out var v)) + return fallback; + return v switch + { + float f => f, + int i => i, + uint u => u, + double d => (float)d, + _ => fallback, + }; } - public static bool GetBool(Dictionary hash, string key, bool fallback = false) - => hash != null && hash.TryGetValue(key, out var v) && v is bool b ? b : fallback; + public static bool GetBool( + Dictionary hash, + string key, + bool fallback = false + ) => hash != null && hash.TryGetValue(key, out var v) && v is bool b ? b : fallback; #endregion } diff --git a/PlayerViewer/Core/Formats/HkTagfile.cs b/PlayerViewer/Core/Formats/HkTagfile.cs index 26c29bc..b52c44e 100644 --- a/PlayerViewer/Core/Formats/HkTagfile.cs +++ b/PlayerViewer/Core/Formats/HkTagfile.cs @@ -14,7 +14,15 @@ public class HkTagfile //--- reflection info class TypeDef { - public uint TypeId, ParentTypeId, Format, SubtypeId, Version, Size, Align, Flags, OptBits; + public uint TypeId, + ParentTypeId, + Format, + SubtypeId, + Version, + Size, + Align, + Flags, + OptBits; public FormatKind Kind; public string Name = ""; public List<(string Name, uint Flags, uint Offset, uint TypeId)> Fields = new(); @@ -22,8 +30,15 @@ class TypeDef enum FormatKind : uint { - Void = 0, Opaque = 1, Bool = 2, String = 3, Int = 4, - Float = 5, Pointer = 6, Record = 7, Array = 8, + Void = 0, + Opaque = 1, + Bool = 2, + String = 3, + Int = 4, + Float = 5, + Pointer = 6, + Record = 7, + Array = 8, } class Item @@ -34,19 +49,26 @@ class Item public uint Count; } - const uint OPT_FORMAT = 1 << 0, OPT_SUBTYPE = 1 << 1, OPT_VERSION = 1 << 2, - OPT_SIZE_ALIGN = 1 << 3, OPT_FLAGS = 1 << 4, OPT_DECLS = 1 << 5, - OPT_INTERFACES = 1 << 6, OPT_ATTRIBUTE_STRING = 1 << 7, OPT_MUTABLE = 1 << 8; + const uint OPT_FORMAT = 1 << 0, + OPT_SUBTYPE = 1 << 1, + OPT_VERSION = 1 << 2, + OPT_SIZE_ALIGN = 1 << 3, + OPT_FLAGS = 1 << 4, + OPT_DECLS = 1 << 5, + OPT_INTERFACES = 1 << 6, + OPT_ATTRIBUTE_STRING = 1 << 7, + OPT_MUTABLE = 1 << 8; const int INT_SIGNED_BIT = 1 << 9; const int INT_NUM_BITS_SHIFT = 10; + //Tuple types (hkVector4f etc.) store the element count in the high bits //of the format word (0x428 = tuple of 4, 0x1028 = tuple of 16). const int TUPLE_COUNT_SHIFT = 8; - byte[] _data; //DATA section payload + byte[] _data; //DATA section payload readonly List _items = new(); - readonly List _types = new(); //indexed by typeId + readonly List _types = new(); //indexed by typeId readonly List _typeStrings = new(); readonly List _fieldStrings = new(); @@ -70,8 +92,8 @@ public static HkTagfile ParseBphcl(byte[] bphcl) { if (bphcl.Length < 0x30 || Encoding.ASCII.GetString(bphcl, 0, 5) != "Phive") throw new InvalidOperationException("Not a Phive file"); - uint tagOffset = BitConverter.ToUInt32(bphcl, 0x0C); //HktOffset - uint tagSize = BitConverter.ToUInt32(bphcl, 0x18); //Section0 size + uint tagOffset = BitConverter.ToUInt32(bphcl, 0x0C); //HktOffset + uint tagSize = BitConverter.ToUInt32(bphcl, 0x18); //Section0 size var tag = new byte[tagSize]; Array.Copy(bphcl, tagOffset, tag, 0, Math.Min(tagSize, bphcl.Length - tagOffset)); return Parse(tag); @@ -87,27 +109,38 @@ void Load(byte[] data) uint tag0Size = ReadBE32(data, 0) & 0x3FFFFFFF; int pos = 8; - int typeStart = -1, typeSize = 0, indxStart = -1, indxSize = 0; + int typeStart = -1, + typeSize = 0, + indxStart = -1, + indxSize = 0; while (pos < tag0Size) { uint secSizeRaw = ReadBE32(data, pos); int secSize = (int)(secSizeRaw & 0x3FFFFFFF); - if (secSize < 8) { pos += 4; continue; } //zero padding + if (secSize < 8) + { + pos += 4; + continue; + } //zero padding string magic = Encoding.ASCII.GetString(data, pos + 4, 4); switch (magic) { case "SDKV": - SdkVersion = Encoding.ASCII.GetString(data, pos + 8, secSize - 8).TrimEnd('\0'); + SdkVersion = Encoding + .ASCII.GetString(data, pos + 8, secSize - 8) + .TrimEnd('\0'); break; case "DATA": _data = new byte[secSize - 8]; Array.Copy(data, pos + 8, _data, 0, secSize - 8); break; case "TYPE": - typeStart = pos; typeSize = secSize; + typeStart = pos; + typeSize = secSize; break; case "INDX": - indxStart = pos + 8; indxSize = secSize - 8; + indxStart = pos + 8; + indxSize = secSize - 8; break; } pos += secSize; @@ -123,21 +156,33 @@ void ParseTypeSection(byte[] data, int start, int size) { int pos = start + 8; int end = start + size; - int tbdyPos = -1, tbdySize = 0; + int tbdyPos = -1, + tbdySize = 0; var tna1 = (pos: -1, size: 0); while (pos < end) { int subSize = (int)(ReadBE32(data, pos) & 0x3FFFFFFF); - if (subSize == 0) break; + if (subSize == 0) + break; string magic = Encoding.ASCII.GetString(data, pos + 4, 4); - int payload = pos + 8, payloadSize = subSize - 8; + int payload = pos + 8, + payloadSize = subSize - 8; switch (magic) { - case "TST1": ReadStringTable(data, payload, payloadSize, _typeStrings); break; - case "FST1": ReadStringTable(data, payload, payloadSize, _fieldStrings); break; - case "TNA1": tna1 = (payload, payloadSize); break; - case "TBDY": tbdyPos = payload; tbdySize = payloadSize; break; + case "TST1": + ReadStringTable(data, payload, payloadSize, _typeStrings); + break; + case "FST1": + ReadStringTable(data, payload, payloadSize, _fieldStrings); + break; + case "TNA1": + tna1 = (payload, payloadSize); + break; + case "TBDY": + tbdyPos = payload; + tbdySize = payloadSize; + break; } pos += subSize; } @@ -154,8 +199,14 @@ void ParseTypeSection(byte[] data, int start, int size) { ulong strIdx = reader.ReadVle(); ulong templateCount = reader.ReadVle(); - typeNames.Add(strIdx < (ulong)_typeStrings.Count ? _typeStrings[(int)strIdx] : ""); - for (ulong t = 0; t < templateCount; t++) { reader.ReadVle(); reader.ReadVle(); } + typeNames.Add( + strIdx < (ulong)_typeStrings.Count ? _typeStrings[(int)strIdx] : "" + ); + for (ulong t = 0; t < templateCount; t++) + { + reader.ReadVle(); + reader.ReadVle(); + } } } @@ -177,7 +228,12 @@ void ParseTypeSection(byte[] data, int start, int size) if (_types[id] == null && typeNames[id].Length > 0) { byName.TryGetValue(typeNames[id] + "f", out uint concrete); - _types[id] = new TypeDef { TypeId = (uint)id, Name = typeNames[id], ParentTypeId = concrete }; + _types[id] = new TypeDef + { + TypeId = (uint)id, + Name = typeNames[id], + ParentTypeId = concrete, + }; } } } @@ -188,7 +244,8 @@ static void ReadStringTable(byte[] data, int pos, int size, List target) while (pos < end) { int len = 0; - while (pos + len < end && data[pos + len] != 0) len++; + while (pos + len < end && data[pos + len] != 0) + len++; target.Add(Encoding.UTF8.GetString(data, pos, len)); pos += len + 1; } @@ -219,14 +276,17 @@ void ParseTbdy(byte[] data, int pos, int size, List typeNames) type.Format = (uint)reader.ReadVle(); type.Kind = (FormatKind)(type.Format & 0x1F); } - if ((type.OptBits & OPT_SUBTYPE) != 0) type.SubtypeId = (uint)reader.ReadVle(); - if ((type.OptBits & OPT_VERSION) != 0) type.Version = (uint)reader.ReadVle(); + if ((type.OptBits & OPT_SUBTYPE) != 0) + type.SubtypeId = (uint)reader.ReadVle(); + if ((type.OptBits & OPT_VERSION) != 0) + type.Version = (uint)reader.ReadVle(); if ((type.OptBits & OPT_SIZE_ALIGN) != 0) { type.Size = (uint)reader.ReadVle(); type.Align = (uint)reader.ReadVle(); } - if ((type.OptBits & OPT_FLAGS) != 0) type.Flags = (uint)reader.ReadVle(); + if ((type.OptBits & OPT_FLAGS) != 0) + type.Flags = (uint)reader.ReadVle(); if ((type.OptBits & OPT_DECLS) != 0) { ulong encoded = reader.ReadVle(); @@ -238,20 +298,28 @@ void ParseTbdy(byte[] data, int pos, int size, List typeNames) //Flag bit 0x80 marks an extra VLE payload after the flags //(seen on e.g. hkPackedVector3::values, hclCollidable::userData). //Not consuming it desyncs the whole remaining type table. - if ((flags & 0x80) != 0) reader.ReadVle(); + if ((flags & 0x80) != 0) + reader.ReadVle(); uint offset = (uint)reader.ReadVle(); uint typeId = (uint)reader.ReadVle(); - string name = nameId < _fieldStrings.Count ? _fieldStrings[(int)nameId] : $"field{i}"; + string name = + nameId < _fieldStrings.Count ? _fieldStrings[(int)nameId] : $"field{i}"; type.Fields.Add((name, flags, offset, typeId)); } } if ((type.OptBits & OPT_INTERFACES) != 0) { uint numIfaces = (uint)reader.ReadVle(); - for (uint i = 0; i < numIfaces; i++) { reader.ReadVle(); reader.ReadVle(); } + for (uint i = 0; i < numIfaces; i++) + { + reader.ReadVle(); + reader.ReadVle(); + } } - if ((type.OptBits & OPT_ATTRIBUTE_STRING) != 0) reader.ReadVle(); - if ((type.OptBits & OPT_MUTABLE) != 0) reader.ReadVle(); + if ((type.OptBits & OPT_ATTRIBUTE_STRING) != 0) + reader.ReadVle(); + if ((type.OptBits & OPT_MUTABLE) != 0) + reader.ReadVle(); type.Name = type.TypeId < typeNames.Count ? typeNames[(int)type.TypeId] : ""; while (_types.Count <= type.TypeId) @@ -269,7 +337,8 @@ void ParseIndx(byte[] data, int pos, int size) while (pos < end) { int subSize = (int)(ReadBE32(data, pos) & 0x3FFFFFFF); - if (subSize == 0) break; + if (subSize == 0) + break; string magic = Encoding.ASCII.GetString(data, pos + 4, 4); if (magic == "ITEM") { @@ -277,13 +346,15 @@ void ParseIndx(byte[] data, int pos, int size) for (int i = 0; i < count; i++) { int e = pos + 8 + i * 12; - _items.Add(new Item - { - TypeIndex = BitConverter.ToUInt16(data, e), - Flags = data[e + 3], - Offset = BitConverter.ToUInt32(data, e + 4), - Count = BitConverter.ToUInt32(data, e + 8), - }); + _items.Add( + new Item + { + TypeIndex = BitConverter.ToUInt16(data, e), + Flags = data[e + 3], + Offset = BitConverter.ToUInt32(data, e + 4), + Count = BitConverter.ToUInt32(data, e + 8), + } + ); } } pos += subSize; @@ -292,30 +363,35 @@ void ParseIndx(byte[] data, int pos, int size) #region type helpers - TypeDef GetType(uint typeId) => - typeId < _types.Count ? _types[(int)typeId] : null; + TypeDef GetType(uint typeId) => typeId < _types.Count ? _types[(int)typeId] : null; uint TypeByteSize(uint typeId) { var t = GetType(typeId); - if (t == null) return 0; - if (t.Size > 0) return t.Size; + if (t == null) + return 0; + if (t.Size > 0) + return t.Size; return t.ParentTypeId != 0 ? TypeByteSize(t.ParentTypeId) : 0; } FormatKind EffectiveKind(uint typeId) { var t = GetType(typeId); - if (t == null) return FormatKind.Void; - if (t.Kind != FormatKind.Void) return t.Kind; + if (t == null) + return FormatKind.Void; + if (t.Kind != FormatKind.Void) + return t.Kind; return t.ParentTypeId != 0 ? EffectiveKind(t.ParentTypeId) : FormatKind.Void; } TypeDef EffectiveType(uint typeId) { var t = GetType(typeId); - if (t == null) return null; - if (t.Kind != FormatKind.Void || t.ParentTypeId == 0) return t; + if (t == null) + return null; + if (t.Kind != FormatKind.Void || t.ParentTypeId == 0) + return t; return EffectiveType(t.ParentTypeId); } @@ -335,8 +411,8 @@ public IEnumerable DumpTypes() foreach (var t in _types.Where(t => t != null)) { string parent = GetType(t.ParentTypeId)?.Name ?? ""; - yield return $"type {t.TypeId}: '{t.Name}' kind={t.Kind} fmt=0x{t.Format:X} size={t.Size} parent='{parent}' " + - $"fields=[{string.Join(",", t.Fields.Select(f => $"{f.Name}@{f.Offset}:{GetType(f.TypeId)?.Name}({f.TypeId})"))}]"; + yield return $"type {t.TypeId}: '{t.Name}' kind={t.Kind} fmt=0x{t.Format:X} size={t.Size} parent='{parent}' " + + $"fields=[{string.Join(",", t.Fields.Select(f => $"{f.Name}@{f.Offset}:{GetType(f.TypeId)?.Name}({f.TypeId})"))}]"; } } @@ -362,12 +438,16 @@ public List> FindItems(string typeName) } /// All decoded record groups of the given type (one list per item). - public IEnumerable<(int index, List> records)> AllItems(string typeName) + public IEnumerable<(int index, List> records)> AllItems( + string typeName + ) { for (int i = 1; i < _items.Count; i++) { - if (ItemTypeName(i) == typeName && - DecodeItem(i) is List> records) + if ( + ItemTypeName(i) == typeName + && DecodeItem(i) is List> records + ) yield return (i, records); } } @@ -392,7 +472,7 @@ public object DecodeItem(int index) if (kind == FormatKind.Record && item.Count > 0 && elemSize > 0) { var records = new List>((int)item.Count); - _decoded[index] = records; //pre-register (cycles via pointers) + _decoded[index] = records; //pre-register (cycles via pointers) var recType = RecordType(item.TypeIndex); for (uint i = 0; i < item.Count; i++) records.Add(DecodeRecord((int)(item.Offset + i * elemSize), recType)); @@ -418,12 +498,17 @@ public object DecodeItem(int index) for (uint i = 0; i < item.Count; i++) { byte c = _data[item.Offset + i]; - if (c != 0 && (c < 0x20 || c > 0x7E)) { isString = false; break; } + if (c != 0 && (c < 0x20 || c > 0x7E)) + { + isString = false; + break; + } } if (isString) { int len = 0; - while (len < item.Count && _data[item.Offset + len] != 0) len++; + while (len < item.Count && _data[item.Offset + len] != 0) + len++; result = Encoding.ASCII.GetString(_data, (int)item.Offset, len); _decoded[index] = result; return result; @@ -435,8 +520,11 @@ public object DecodeItem(int index) _decoded[index] = list; return list; } - if ((kind == FormatKind.Float || kind == FormatKind.Bool || kind == FormatKind.Opaque) - && item.Count > 0 && elemSize > 0) + if ( + (kind == FormatKind.Float || kind == FormatKind.Bool || kind == FormatKind.Opaque) + && item.Count > 0 + && elemSize > 0 + ) { var list = new List((int)item.Count); for (uint i = 0; i < item.Count; i++) @@ -542,7 +630,10 @@ Dictionary DecodeRecord(int offset, TypeDef type) object decoded = itemRef != 0 ? DecodeItem((int)itemRef) : null; if (size > 0) { - if (decoded is List> recs && recs.Count > size) + if ( + decoded is List> recs + && recs.Count > size + ) decoded = recs.Take(size).ToList(); else if (decoded is List vals && vals.Count > size) decoded = vals.Take(size).ToList(); @@ -568,17 +659,28 @@ object DecodeValue(int offset, TypeDef type) { uint numBits = type.Format >> INT_NUM_BITS_SHIFT; bool signed = (type.Format & INT_SIGNED_BIT) != 0; - if (numBits <= 8) return signed ? (sbyte)_data[offset] : (object)_data[offset]; - if (numBits <= 16) return signed ? BitConverter.ToInt16(_data, offset) : (object)BitConverter.ToUInt16(_data, offset); - if (numBits <= 32) return signed ? BitConverter.ToInt32(_data, offset) : (object)BitConverter.ToUInt32(_data, offset); - return signed ? BitConverter.ToInt64(_data, offset) : (object)BitConverter.ToUInt64(_data, offset); + if (numBits <= 8) + return signed ? (sbyte)_data[offset] : (object)_data[offset]; + if (numBits <= 16) + return signed + ? BitConverter.ToInt16(_data, offset) + : (object)BitConverter.ToUInt16(_data, offset); + if (numBits <= 32) + return signed + ? BitConverter.ToInt32(_data, offset) + : (object)BitConverter.ToUInt32(_data, offset); + return signed + ? BitConverter.ToInt64(_data, offset) + : (object)BitConverter.ToUInt64(_data, offset); } case FormatKind.Float: - if (type.Size == 4) return BitConverter.ToSingle(_data, offset); - if (type.Size == 8) return BitConverter.ToDouble(_data, offset); - if (type.Size >= 12 && type.Size % 4 == 0) //hkVector4/matrix vector types + if (type.Size == 4) + return BitConverter.ToSingle(_data, offset); + if (type.Size == 8) + return BitConverter.ToDouble(_data, offset); + if (type.Size >= 12 && type.Size % 4 == 0) //hkVector4/matrix vector types return FloatVector(offset, (int)type.Size / 4); - return (object)BitConverter.ToUInt16(_data, offset); //half, raw bits + return (object)BitConverter.ToUInt16(_data, offset); //half, raw bits case FormatKind.Bool: return _data[offset] != 0; case FormatKind.Pointer: @@ -607,22 +709,71 @@ class VleReader int _pos; readonly int _end; - public VleReader(byte[] d, int pos, int size) { _d = d; _pos = pos; _end = pos + size; } + public VleReader(byte[] d, int pos, int size) + { + _d = d; + _pos = pos; + _end = pos + size; + } + public bool HasMore => _pos < _end; public int Position => _pos; + byte Next() => _pos < _end ? _d[_pos++] : (byte)0; public ulong ReadVle() { byte b0 = Next(); - if ((b0 & 0x80) == 0) return b0; - if ((b0 & 0xC0) == 0x80) return ((ulong)(b0 & 0x3F) << 8) | Next(); - if ((b0 & 0xE0) == 0xC0) { byte b1 = Next(), b2 = Next(); return ((ulong)(b0 & 0x1F) << 16) | ((ulong)b1 << 8) | b2; } - if ((b0 & 0xF8) == 0xE0) { byte b1 = Next(), b2 = Next(), b3 = Next(); return ((ulong)(b0 & 0x0F) << 24) | ((ulong)b1 << 16) | ((ulong)b2 << 8) | b3; } - if ((b0 & 0xF8) == 0xE8) { byte b1 = Next(), b2 = Next(), b3 = Next(), b4 = Next(); return ((ulong)(b0 & 0x07) << 32) | ((ulong)b1 << 24) | ((ulong)b2 << 16) | ((ulong)b3 << 8) | b4; } - if ((b0 & 0xFE) == 0xF0) { ulong v = 0; for (int i = 0; i < 7; i++) v = (v << 8) | Next(); return ((ulong)(b0 & 0x01) << 56) | v; } - if (b0 == 0xF8) { ulong v = 0; for (int i = 0; i < 5; i++) v = (v << 8) | Next(); return v; } - if (b0 == 0xF9) { ulong v = 0; for (int i = 0; i < 8; i++) v = (v << 8) | Next(); return v; } + if ((b0 & 0x80) == 0) + return b0; + if ((b0 & 0xC0) == 0x80) + return ((ulong)(b0 & 0x3F) << 8) | Next(); + if ((b0 & 0xE0) == 0xC0) + { + byte b1 = Next(), + b2 = Next(); + return ((ulong)(b0 & 0x1F) << 16) | ((ulong)b1 << 8) | b2; + } + if ((b0 & 0xF8) == 0xE0) + { + byte b1 = Next(), + b2 = Next(), + b3 = Next(); + return ((ulong)(b0 & 0x0F) << 24) | ((ulong)b1 << 16) | ((ulong)b2 << 8) | b3; + } + if ((b0 & 0xF8) == 0xE8) + { + byte b1 = Next(), + b2 = Next(), + b3 = Next(), + b4 = Next(); + return ((ulong)(b0 & 0x07) << 32) + | ((ulong)b1 << 24) + | ((ulong)b2 << 16) + | ((ulong)b3 << 8) + | b4; + } + if ((b0 & 0xFE) == 0xF0) + { + ulong v = 0; + for (int i = 0; i < 7; i++) + v = (v << 8) | Next(); + return ((ulong)(b0 & 0x01) << 56) | v; + } + if (b0 == 0xF8) + { + ulong v = 0; + for (int i = 0; i < 5; i++) + v = (v << 8) | Next(); + return v; + } + if (b0 == 0xF9) + { + ulong v = 0; + for (int i = 0; i < 8; i++) + v = (v << 8) | Next(); + return v; + } return 0; } } diff --git a/PlayerViewer/Core/Formats/Sarc.cs b/PlayerViewer/Core/Formats/Sarc.cs index f77891c..f6fffc8 100644 --- a/PlayerViewer/Core/Formats/Sarc.cs +++ b/PlayerViewer/Core/Formats/Sarc.cs @@ -9,11 +9,19 @@ namespace PlayerViewer.Core.Formats /// public class Sarc { - public readonly Dictionary> Files = new(StringComparer.OrdinalIgnoreCase); + public readonly Dictionary> Files = new( + StringComparer.OrdinalIgnoreCase + ); public Sarc(byte[] data) { - if (data.Length < 0x20 || data[0] != 'S' || data[1] != 'A' || data[2] != 'R' || data[3] != 'C') + if ( + data.Length < 0x20 + || data[0] != 'S' + || data[1] != 'A' + || data[2] != 'R' + || data[3] != 'C' + ) throw new InvalidOperationException("Not a SARC archive."); uint dataOffset = BitConverter.ToUInt32(data, 0x0C); @@ -39,7 +47,8 @@ public Sarc(byte[] data) { int nameOff = names + (int)((attrs & 0xFFFF) * 4); int strEnd = nameOff; - while (data[strEnd] != 0) strEnd++; + while (data[strEnd] != 0) + strEnd++; name = Encoding.UTF8.GetString(data, nameOff, strEnd - nameOff); } else @@ -47,7 +56,11 @@ public Sarc(byte[] data) name = $"hash_{BitConverter.ToUInt32(data, node):X8}"; } - Files[name] = new ArraySegment(data, (int)(dataOffset + start), (int)(end - start)); + Files[name] = new ArraySegment( + data, + (int)(dataOffset + start), + (int)(end - start) + ); } } diff --git a/PlayerViewer/Core/GameDatabase.cs b/PlayerViewer/Core/GameDatabase.cs index 8cc975c..39ffe4c 100644 --- a/PlayerViewer/Core/GameDatabase.cs +++ b/PlayerViewer/Core/GameDatabase.cs @@ -23,15 +23,16 @@ public enum GearSlot public class GearEntry { public GearSlot Slot; - public string RowId = ""; //RSDB row id, e.g. "Hed_ACC003" or "Blaster_LightLong_00" + public string RowId = ""; //RSDB row id, e.g. "Hed_ACC003" or "Blaster_LightLong_00" public int Id = -1; - public string Label = ""; //Localized-ish label (JP) from RSDB - public int Variation; //Variation index (0 = default) - public int VariationCount; //Total variations - public bool IsCustom; //Loaded from a user file, not the romfs - public string CustomPath; //Source path for custom entries - public string WeaponType = ""; //Weapon rows: Versus/Coop/Mission/... - public string ActorName = ""; //Actor pack name (weapons: from GameActor/SpecActor) + public string Label = ""; //Localized-ish label (JP) from RSDB + public int Variation; //Variation index (0 = default) + public int VariationCount; //Total variations + public bool IsCustom; //Loaded from a user file, not the romfs + public string CustomPath; //Source path for custom entries + public string WeaponType = ""; //Weapon rows: Versus/Coop/Mission/... + public string ActorName = ""; //Actor pack name (weapons: from GameActor/SpecActor) + public string Genre = ""; //Gear rows: Genre0 (Head_Cap, Shoes_Boots, ...) for foldering /// Display name for UI lists. public string DisplayName @@ -39,8 +40,10 @@ public string DisplayName get { string name = RowId; - if (Variation > 0) name += $" (v{Variation})"; - if (IsCustom) name += " [custom]"; + if (Variation > 0) + name += $" (v{Variation})"; + if (IsCustom) + name += " [custom]"; return name; } } @@ -49,7 +52,7 @@ public string DisplayName public class TeamColorSet { public string Name = ""; - public System.Numerics.Vector3 Alpha; //sRGB 0-1 + public System.Numerics.Vector3 Alpha; //sRGB 0-1 public System.Numerics.Vector3 Bravo; public System.Numerics.Vector3 Charlie; public System.Numerics.Vector3 Neutral; @@ -85,19 +88,20 @@ public GameDatabase(Romfs romfs) Load(); } - public List GetList(GearSlot slot) => slot switch - { - GearSlot.Head => Head, - GearSlot.Clothes => Clothes, - GearSlot.Shoes => Shoes, - GearSlot.Hair => Hair, - GearSlot.Eyebrow => Eyebrow, - GearSlot.Bottom => Bottom, - GearSlot.Tank => Tank, - GearSlot.MainWeapon => MainWeapons, - GearSlot.SpecialWeapon => SpecialWeapons, - _ => new List(), - }; + public List GetList(GearSlot slot) => + slot switch + { + GearSlot.Head => Head, + GearSlot.Clothes => Clothes, + GearSlot.Shoes => Shoes, + GearSlot.Hair => Hair, + GearSlot.Eyebrow => Eyebrow, + GearSlot.Bottom => Bottom, + GearSlot.Tank => Tank, + GearSlot.MainWeapon => MainWeapons, + GearSlot.SpecialWeapon => SpecialWeapons, + _ => new List(), + }; void Load() { @@ -135,8 +139,12 @@ List ReadTable(string table) return byml?.Root as List ?? new List(); } - void LoadGearTable(string table, GearSlot slot, List target, - Dictionary> rawRows = null) + void LoadGearTable( + string table, + GearSlot slot, + List target, + Dictionary> rawRows = null + ) { foreach (var row in ReadTable(table).OfType>()) { @@ -149,23 +157,28 @@ void LoadGearTable(string table, GearSlot slot, List target, int varNum = Byml.GetInt(row, "VariationNum", 0); for (int v = 0; v <= varNum; v++) { - target.Add(new GearEntry - { - Slot = slot, - RowId = rowId, - Id = Byml.GetInt(row, "Id", -1), - Label = Byml.GetString(row, "Label"), - Variation = v, - VariationCount = varNum + 1, - }); + target.Add( + new GearEntry + { + Slot = slot, + RowId = rowId, + Id = Byml.GetInt(row, "Id", -1), + Label = Byml.GetString(row, "Label"), + Variation = v, + VariationCount = varNum + 1, + Genre = Byml.GetString(row, "Genre0"), + } + ); } } //List.Sort is unstable; without the variation tiebreak v1 can land above v0. - target.Sort((a, b) => - { - int c = string.Compare(a.RowId, b.RowId, StringComparison.OrdinalIgnoreCase); - return c != 0 ? c : a.Variation.CompareTo(b.Variation); - }); + target.Sort( + (a, b) => + { + int c = string.Compare(a.RowId, b.RowId, StringComparison.OrdinalIgnoreCase); + return c != 0 ? c : a.Variation.CompareTo(b.Variation); + } + ); } void LoadSimpleTable(string table, GearSlot slot, List target) @@ -181,19 +194,24 @@ void LoadSimpleTable(string table, GearSlot slot, List target) string actorRef = Byml.GetString(row, "SpecActor"); string actorName = Path.GetFileName(actorRef ?? ""); int actorDot = actorName.IndexOf('.'); - if (actorDot >= 0) actorName = actorName.Substring(0, actorDot); + if (actorDot >= 0) + actorName = actorName.Substring(0, actorDot); - target.Add(new GearEntry - { - Slot = slot, - RowId = rowId, - Id = Byml.GetInt(row, "Id", -1), - Label = Byml.GetString(row, "Label"), - ActorName = actorName, - }); + target.Add( + new GearEntry + { + Slot = slot, + RowId = rowId, + Id = Byml.GetInt(row, "Id", -1), + Label = Byml.GetString(row, "Label"), + ActorName = actorName, + } + ); } //Hair/eyebrows have an Order field ordering them like the in-game UI. - target.Sort((a, b) => string.Compare(a.RowId, b.RowId, StringComparison.OrdinalIgnoreCase)); + target.Sort( + (a, b) => string.Compare(a.RowId, b.RowId, StringComparison.OrdinalIgnoreCase) + ); } void LoadWeaponTable(string table, GearSlot slot, List target) @@ -210,19 +228,24 @@ void LoadWeaponTable(string table, GearSlot slot, List target) actorRef = Byml.GetString(row, "SpecActor"); string actorName = Path.GetFileName(actorRef ?? ""); int actorDot = actorName.IndexOf('.'); - if (actorDot >= 0) actorName = actorName.Substring(0, actorDot); + if (actorDot >= 0) + actorName = actorName.Substring(0, actorDot); - target.Add(new GearEntry - { - Slot = slot, - RowId = rowId, - Id = Byml.GetInt(row, "Id", -1), - Label = Byml.GetString(row, "Label"), - WeaponType = Byml.GetString(row, "Type"), - ActorName = actorName, - }); + target.Add( + new GearEntry + { + Slot = slot, + RowId = rowId, + Id = Byml.GetInt(row, "Id", -1), + Label = Byml.GetString(row, "Label"), + WeaponType = Byml.GetString(row, "Type"), + ActorName = actorName, + } + ); } - target.Sort((a, b) => string.Compare(a.RowId, b.RowId, StringComparison.OrdinalIgnoreCase)); + target.Sort( + (a, b) => string.Compare(a.RowId, b.RowId, StringComparison.OrdinalIgnoreCase) + ); } void LoadTeamColors() @@ -233,28 +256,38 @@ void LoadTeamColors() //RowId looks like Work/Gyml/.game__gfx__parameter__TeamColorDataSet.gyml string name = rowId; int slash = name.LastIndexOf('/'); - if (slash >= 0) name = name.Substring(slash + 1); + if (slash >= 0) + name = name.Substring(slash + 1); int dot = name.IndexOf('.'); - if (dot >= 0) name = name.Substring(0, dot); + if (dot >= 0) + name = name.Substring(0, dot); System.Numerics.Vector3 ReadColor(string key) { var c = Byml.AsHash(row.GetValueOrDefault(key)); return c == null ? System.Numerics.Vector3.Zero - : new System.Numerics.Vector3(Byml.GetFloat(c, "R"), Byml.GetFloat(c, "G"), Byml.GetFloat(c, "B")); + : new System.Numerics.Vector3( + Byml.GetFloat(c, "R"), + Byml.GetFloat(c, "G"), + Byml.GetFloat(c, "B") + ); } - TeamColors.Add(new TeamColorSet - { - Name = name, - Alpha = ReadColor("AlphaTeamColor"), - Bravo = ReadColor("BravoTeamColor"), - Charlie = ReadColor("CharlieTeamColor"), - Neutral = ReadColor("NeutralColor"), - }); + TeamColors.Add( + new TeamColorSet + { + Name = name, + Alpha = ReadColor("AlphaTeamColor"), + Bravo = ReadColor("BravoTeamColor"), + Charlie = ReadColor("CharlieTeamColor"), + Neutral = ReadColor("NeutralColor"), + } + ); } - TeamColors.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); + TeamColors.Sort( + (a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase) + ); } #region model resolution @@ -313,7 +346,10 @@ public string ResolveModelName(string actorName) /// Left model comes from the MirrorModel component (dualies etc) and usually /// lives as a second model inside the SAME bfres file (Wmn_X + Wmn_X_L). /// - public ((string file, string model) main, (string file, string model) left) ResolveWeaponModels(GearEntry weapon) + public ( + (string file, string model) main, + (string file, string model) left + ) ResolveWeaponModels(GearEntry weapon) { string actorName = !string.IsNullOrEmpty(weapon.ActorName) ? weapon.ActorName @@ -324,7 +360,8 @@ public string ResolveModelName(string actorName) if (pack == null) pack = Romfs.GetActorPack(weapon.RowId); - (string, string) main = (null, null), left = (null, null); + (string, string) main = (null, null), + left = (null, null); //Walk this pack and parent packs for ModelInfo / MirrorModel components. var visited = new HashSet(); @@ -332,7 +369,8 @@ public string ResolveModelName(string actorName) string currentName = actorName; for (int depth = 0; depth < 4 && current != null; depth++) { - if (!visited.Add(currentName)) break; + if (!visited.Add(currentName)) + break; string mirror = current.FindFile(x => x.StartsWith("Component/MirrorModel/")); if (mirror != null && (main.Item1 == null || left.Item1 == null)) @@ -364,18 +402,23 @@ public string ResolveModelName(string actorName) break; //Follow the $parent actor chain. - string actorParam = current.FindFile(x => x.StartsWith("Actor/") && x.Contains(currentName)); + string actorParam = current.FindFile(x => + x.StartsWith("Actor/") && x.Contains(currentName) + ); actorParam ??= current.FindFile(x => x.StartsWith("Actor/")); - if (actorParam == null) break; + if (actorParam == null) + break; var actorByml = new Byml(current.GetFile(actorParam)); string parent = Byml.GetString(Byml.AsHash(actorByml.Root), "$parent"); - if (string.IsNullOrEmpty(parent)) break; + if (string.IsNullOrEmpty(parent)) + break; //Work/Actor/WeaponManeuverDual.engine__actor__ActorParam.gyml -> WeaponManeuverDual string parentName = Path.GetFileName(parent); int dot = parentName.IndexOf('.'); - if (dot >= 0) parentName = parentName.Substring(0, dot); + if (dot >= 0) + parentName = parentName.Substring(0, dot); current = Romfs.GetActorPack(parentName); currentName = parentName; diff --git a/PlayerViewer/Core/Romfs.cs b/PlayerViewer/Core/Romfs.cs index fce69b5..eae4d1c 100644 --- a/PlayerViewer/Core/Romfs.cs +++ b/PlayerViewer/Core/Romfs.cs @@ -26,21 +26,27 @@ public class Romfs readonly Dictionary _packCache = new(StringComparer.OrdinalIgnoreCase); - public Romfs(string root, string layeredRoot = null, bool useLayered = false, - string sdodrRoot = null) + public Romfs( + string root, + string layeredRoot = null, + bool useLayered = false, + string sdodrRoot = null + ) { Root = root; LayeredRoot = layeredRoot; - UseLayered = useLayered && !string.IsNullOrEmpty(layeredRoot) && Directory.Exists(layeredRoot); - SdodrRoot = !string.IsNullOrEmpty(sdodrRoot) && Directory.Exists(sdodrRoot) ? sdodrRoot : null; + UseLayered = + useLayered && !string.IsNullOrEmpty(layeredRoot) && Directory.Exists(layeredRoot); + SdodrRoot = + !string.IsNullOrEmpty(sdodrRoot) && Directory.Exists(sdodrRoot) ? sdodrRoot : null; } public static bool IsValidRoot(string root) { - return !string.IsNullOrEmpty(root) && - Directory.Exists(Path.Combine(root, "Model")) && - Directory.Exists(Path.Combine(root, "RSDB")) && - File.Exists(Path.Combine(root, "Model", "Player00.bfres.zs")); + return !string.IsNullOrEmpty(root) + && Directory.Exists(Path.Combine(root, "Model")) + && Directory.Exists(Path.Combine(root, "RSDB")) + && File.Exists(Path.Combine(root, "Model", "Player00.bfres.zs")); } /// @@ -52,18 +58,24 @@ public string Resolve(string relativePath) if (UseLayered) { string layered = Path.Combine(LayeredRoot, relativePath); - if (File.Exists(layered)) return layered; - if (File.Exists(layered + ".zs")) return layered + ".zs"; + if (File.Exists(layered)) + return layered; + if (File.Exists(layered + ".zs")) + return layered + ".zs"; } if (SdodrRoot != null) { string sdodr = Path.Combine(SdodrRoot, relativePath); - if (File.Exists(sdodr)) return sdodr; - if (File.Exists(sdodr + ".zs")) return sdodr + ".zs"; + if (File.Exists(sdodr)) + return sdodr; + if (File.Exists(sdodr + ".zs")) + return sdodr + ".zs"; } string path = Path.Combine(Root, relativePath); - if (File.Exists(path)) return path; - if (File.Exists(path + ".zs")) return path + ".zs"; + if (File.Exists(path)) + return path; + if (File.Exists(path + ".zs")) + return path + ".zs"; return null; } @@ -94,10 +106,16 @@ public bool IsLayered(string relativePath) public static byte[] Decompress(byte[] data) { //zstd magic - if (data.Length >= 4 && data[0] == 0x28 && data[1] == 0xB5 && data[2] == 0x2F && data[3] == 0xFD) + if ( + data.Length >= 4 + && data[0] == 0x28 + && data[1] == 0xB5 + && data[2] == 0x2F + && data[3] == 0xFD + ) { - using var decompressor = new ZstdNet.Decompressor(); - return decompressor.Unwrap(data); + using var decompressor = new ZstdSharp.Decompressor(); + return decompressor.Unwrap(data).ToArray(); } return data; } diff --git a/PlayerViewer/Player/AlphaMaskSystem.cs b/PlayerViewer/Player/AlphaMaskSystem.cs index 9e93435..cb44ac3 100644 --- a/PlayerViewer/Player/AlphaMaskSystem.cs +++ b/PlayerViewer/Player/AlphaMaskSystem.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; using System.IO; using System.Linq; using BfresEditor; @@ -23,7 +21,10 @@ public class AlphaMaskSystem : IDisposable { public const string CompositeTextureName = "__BodyAlphaMask"; - readonly Dictionary _masks = new(StringComparer.OrdinalIgnoreCase); + //A decoded mask as tightly-packed RGBA8 bytes (the BC4 value lands in R/G/B). + readonly record struct MaskImage(byte[] Rgba, int Width, int Height); + + readonly Dictionary _masks = new(StringComparer.OrdinalIgnoreCase); bool _loaded; RuntimeTexture _composite; @@ -32,7 +33,8 @@ public class AlphaMaskSystem : IDisposable /// Decodes all mask textures from Model/GearAlphaMask.bfres.zs. public void Load(Romfs romfs) { - if (_loaded) return; + if (_loaded) + return; _loaded = true; var data = romfs.ReadModel("GearAlphaMask"); @@ -47,14 +49,20 @@ public void Load(Romfs romfs) return; var bntx = new Syroot.NintenTools.NSW.Bntx.BntxFile( - new MemoryStream(data, bntxOffset, data.Length - bntxOffset)); + new MemoryStream(data, bntxOffset, data.Length - bntxOffset) + ); foreach (var tex in bntx.Textures) { try { //Texture names carry an _Opa suffix; RSDB mask names do not. string name = tex.Name.EndsWith("_Opa") ? tex.Name[..^4] : tex.Name; - _masks[name] = new BntxTexture(bntx, tex).GetBitmap(); + var bt = new BntxTexture(bntx, tex); + _masks[name] = new MaskImage( + bt.GetDecodedSurface(0, 0), + (int)bt.Width, + (int)bt.Height + ); } catch (Exception ex) { @@ -67,7 +75,12 @@ public void Load(Romfs romfs) static int FindBntx(byte[] data) { for (int i = 0; i < data.Length - 4; i++) - if (data[i] == 'B' && data[i + 1] == 'N' && data[i + 2] == 'T' && data[i + 3] == 'X') + if ( + data[i] == 'B' + && data[i + 1] == 'N' + && data[i + 2] == 'T' + && data[i + 3] == 'X' + ) return i; return -1; } @@ -95,64 +108,60 @@ public bool Compose(PartModel human, IEnumerable maskNames) _lastKey = key; //Start from the body's own opacity texture so authored holes survive. - Bitmap result = null; + byte[] result = null; + int rw = 256, + rh = 256; if (human.Render.Textures.TryGetValue("M_Body_Opa", out var bodyOpa)) { - try { result = ((BntxTexture)bodyOpa).GetBitmap(); } + try + { + var bt = (BntxTexture)bodyOpa; + result = bt.GetDecodedSurface(0, 0); + rw = (int)bt.Width; + rh = (int)bt.Height; + } catch { } } - result ??= FilledBitmap(256, 256, 255); + result ??= Filled(rw, rh, 255); foreach (var name in active) - MinBlend(result, _masks[name]); + MinBlend(result, rw, rh, _masks[name]); _composite?.Dispose(); - _composite = new RuntimeTexture(CompositeTextureName, result); + _composite = new RuntimeTexture(CompositeTextureName, result, rw, rh); human.Render.Textures[CompositeTextureName] = _composite; - result.Dispose(); return true; } - static Bitmap FilledBitmap(int w, int h, byte value) + static byte[] Filled(int w, int h, byte value) { - var bmp = new Bitmap(w, h); - using var g = Graphics.FromImage(bmp); - g.Clear(Color.FromArgb(255, value, value, value)); - return bmp; + var buf = new byte[w * h * 4]; + Array.Fill(buf, value); + return buf; } - /// dst = min(dst, mask) per pixel (mask scaled to dst size). - static void MinBlend(Bitmap dst, Bitmap mask) + /// dst = min(dst, mask) per pixel (mask nearest-sampled to dst size). + static void MinBlend(byte[] dst, int dw, int dh, MaskImage mask) { - using Bitmap scaled = mask.Width == dst.Width && mask.Height == dst.Height - ? new Bitmap(mask) - : new Bitmap(mask, dst.Width, dst.Height); - - var rect = new Rectangle(0, 0, dst.Width, dst.Height); - var d = dst.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); - var s = scaled.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); - unsafe + for (int y = 0; y < dh; y++) { - byte* dp = (byte*)d.Scan0; - byte* sp = (byte*)s.Scan0; - int count = dst.Width * dst.Height; - for (int i = 0; i < count; i++, dp += 4, sp += 4) + int my = mask.Height == dh ? y : y * mask.Height / dh; + for (int x = 0; x < dw; x++) { - //BC4 masks decode with the value in the color channels; take red. - byte v = Math.Min(dp[2], sp[2]); - dp[0] = dp[1] = dp[2] = dp[3] = v; + int mx = mask.Width == dw ? x : x * mask.Width / dw; + //BC4 masks decode with the value in the color channels; take red (RGBA index 0). + byte mv = mask.Rgba[(my * mask.Width + mx) * 4]; + int di = (y * dw + x) * 4; + byte v = Math.Min(dst[di], mv); + dst[di] = dst[di + 1] = dst[di + 2] = dst[di + 3] = v; } } - dst.UnlockBits(d); - scaled.UnlockBits(s); } public void Dispose() { _composite?.Dispose(); _composite = null; - foreach (var bmp in _masks.Values) - bmp.Dispose(); _masks.Clear(); _loaded = false; } @@ -164,17 +173,27 @@ public void Dispose() /// class RuntimeTexture : STGenericTexture, IDisposable { - public RuntimeTexture(string name, Bitmap bitmap) + public RuntimeTexture(string name, byte[] rgba, int width, int height) { Name = name; - Width = (uint)bitmap.Width; - Height = (uint)bitmap.Height; + Width = (uint)width; + Height = (uint)height; MipCount = 1; - RenderableTex = GLTexture2D.FromBitmap(bitmap); + RenderableTex = GLTexture2D.FromRgba(rgba, width, height); } - public override void SetImageData(List imageData, uint width, uint height, int arrayLevel = 0) { } - public override byte[] GetImageData(int ArrayLevel = 0, int MipLevel = 0, int DepthLevel = 0) => Array.Empty(); + public override void SetImageData( + List imageData, + uint width, + uint height, + int arrayLevel = 0 + ) { } + + public override byte[] GetImageData( + int ArrayLevel = 0, + int MipLevel = 0, + int DepthLevel = 0 + ) => Array.Empty(); public void Dispose() { diff --git a/PlayerViewer/Player/AnimLibrary.cs b/PlayerViewer/Player/AnimLibrary.cs index 4972e5a..849fe2a 100644 --- a/PlayerViewer/Player/AnimLibrary.cs +++ b/PlayerViewer/Player/AnimLibrary.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using BfresLibrary; using BfresEditor; +using BfresLibrary; using PlayerViewer.Core; namespace PlayerViewer.Player @@ -47,8 +47,14 @@ class Entry /// public void Load(Romfs romfs, string playerModel) { - _skeletal.Clear(); _texPattern.Clear(); _shaderParam.Clear(); _boneVis.Clear(); - _skeletalWrappers.Clear(); _texPatternWrappers.Clear(); _shaderParamWrappers.Clear(); _boneVisWrappers.Clear(); + _skeletal.Clear(); + _texPattern.Clear(); + _shaderParam.Clear(); + _boneVis.Clear(); + _skeletalWrappers.Clear(); + _texPatternWrappers.Clear(); + _shaderParamWrappers.Clear(); + _boneVisWrappers.Clear(); var sources = new List<(string name, int priority)> { ("Player00", 0) }; foreach (var variant in romfs.ListModelFiles("Player00_v-")) @@ -61,16 +67,24 @@ public void Load(Romfs romfs, string playerModel) } //Parse uncached sources in parallel (independent files, no GL work). - var missing = sources.Where(s => !_parsed.ContainsKey(s.name)).Select(s => s.name).Distinct().ToArray(); + var missing = sources + .Where(s => !_parsed.ContainsKey(s.name)) + .Select(s => s.name) + .Distinct() + .ToArray(); var parsed = new ResFile[missing.Length]; - System.Threading.Tasks.Parallel.For(0, missing.Length, i => - { - byte[] data = romfs.ReadModelFile(missing[i]); - if (data != null) - parsed[i] = new ResFile(new MemoryStream(data)); - }); + System.Threading.Tasks.Parallel.For( + 0, + missing.Length, + i => + { + byte[] data = romfs.ReadModelFile(missing[i]); + if (data != null) + parsed[i] = new ResFile(new MemoryStream(data)); + } + ); for (int i = 0; i < missing.Length; i++) - _parsed[missing[i]] = parsed[i]; //null = file absent; cached too + _parsed[missing[i]] = parsed[i]; //null = file absent; cached too foreach (var (name, priority) in sources) { @@ -94,10 +108,21 @@ public void Load(Romfs romfs, string playerModel) AnimNames.Insert(0, "Wait"); } - static void Add(Dictionary> map, string name, T anim, ResFile file, int priority) + static void Add( + Dictionary> map, + string name, + T anim, + ResFile file, + int priority + ) { if (!map.TryGetValue(name, out var existing) || priority >= existing.Priority) - map[name] = new Entry { Anim = anim, File = file, Priority = priority }; + map[name] = new Entry + { + Anim = anim, + File = file, + Priority = priority, + }; } public bool HasAnim(string name) => _skeletal.ContainsKey(name); diff --git a/PlayerViewer/Player/BfresHelpers.cs b/PlayerViewer/Player/BfresHelpers.cs index 2ae9edb..7547119 100644 --- a/PlayerViewer/Player/BfresHelpers.cs +++ b/PlayerViewer/Player/BfresHelpers.cs @@ -24,10 +24,15 @@ public static void ResolveSharedAssets(BFRES bfres, byte[] rawData, Romfs romfs) if (resFile.ExternalFiles.ContainsKey("share_tex_list.txt")) { - string content = System.Text.Encoding.UTF8 - .GetString(resFile.ExternalFiles["share_tex_list.txt"].Data).Trim(); - var refs = content.Split(new[] { '\r', '\n' }, - StringSplitOptions.RemoveEmptyEntries); + string content = System + .Text.Encoding.UTF8.GetString( + resFile.ExternalFiles["share_tex_list.txt"].Data + ) + .Trim(); + var refs = content.Split( + new[] { '\r', '\n' }, + StringSplitOptions.RemoveEmptyEntries + ); foreach (var refModel in refs) MergeSharedAssets(bfres, refModel.Trim(), romfs); } @@ -45,7 +50,11 @@ public static void ResolveSharedAssets(BFRES bfres, byte[] rawData, Romfs romfs) /// the referenced BFRES textures into the render's texture cache. /// Value format: "_rp0/ModelName" or "_re0,ModelName". /// - static void ResolveExternalTextureResources(BFRES bfres, BfresLibrary.ResFile resFile, Romfs romfs) + static void ResolveExternalTextureResources( + BFRES bfres, + BfresLibrary.ResFile resFile, + Romfs romfs + ) { var render = (BfresRender)bfres.Renderer; var loaded = new HashSet(); @@ -56,24 +65,29 @@ static void ResolveExternalTextureResources(BFRES bfres, BfresLibrary.ResFile re if (!mat.UserData.ContainsKey("ExternalTextureResource")) continue; var strings = mat.UserData["ExternalTextureResource"].GetValueStringArray(); - if (strings.Length == 0) continue; + if (strings.Length == 0) + continue; string raw = strings[0]; int sep = Math.Max(raw.LastIndexOf('/'), raw.LastIndexOf(',')); string refName = sep >= 0 ? raw.Substring(sep + 1) : raw; - if (!loaded.Add(refName)) continue; + if (!loaded.Add(refName)) + continue; var refData = romfs.ReadModel(refName); if (refData == null) { - Console.WriteLine($"[Scene] ExternalTextureResource not found: {refName} (from {raw})"); + Console.WriteLine( + $"[Scene] ExternalTextureResource not found: {refName} (from {raw})" + ); continue; } var refRes = new BfresLibrary.ResFile(new MemoryStream(refData)); int added = 0; foreach (var tex in refRes.Textures.Values) { - if (render.Textures.ContainsKey(tex.Name)) continue; + if (render.Textures.ContainsKey(tex.Name)) + continue; if (tex is BfresLibrary.Switch.SwitchTexture st) { render.Textures.Add(tex.Name, new BntxTexture(st.BntxFile, st.Texture)); @@ -81,7 +95,9 @@ static void ResolveExternalTextureResources(BFRES bfres, BfresLibrary.ResFile re } } if (added > 0) - Console.WriteLine($"[Scene] Merged {added} textures from ExternalTextureResource {refName}"); + Console.WriteLine( + $"[Scene] Merged {added} textures from ExternalTextureResource {refName}" + ); } } @@ -131,16 +147,24 @@ static void MergeSharedAssets(BFRES bfres, string baseModelName, Romfs romfs) int total = skelAnims + matAnims + visAnims; if (texAdded > 0 || total > 0) - Console.WriteLine($"[Scene] Merged {texAdded} tex, {total} anims " + - $"(skel={skelAnims} mat={matAnims} vis={visAnims}) from {baseModelName}"); + Console.WriteLine( + $"[Scene] Merged {texAdded} tex, {total} anims " + + $"(skel={skelAnims} mat={matAnims} vis={visAnims}) from {baseModelName}" + ); } catch (Exception ex) { - Console.WriteLine($"[Scene] Shared asset merge failed ({baseModelName}): {ex.Message}"); + Console.WriteLine( + $"[Scene] Shared asset merge failed ({baseModelName}): {ex.Message}" + ); } } - static int MergeMaterialAnims(BFRES bfres, BfresLibrary.ResDict source, string resName) + static int MergeMaterialAnims( + BFRES bfres, + BfresLibrary.ResDict source, + string resName + ) { int added = 0; foreach (var anim in source.Values) diff --git a/PlayerViewer/Player/HairClothData.cs b/PlayerViewer/Player/HairClothData.cs index d4cf700..2866b81 100644 --- a/PlayerViewer/Player/HairClothData.cs +++ b/PlayerViewer/Player/HairClothData.cs @@ -21,38 +21,52 @@ public static HairClothData Load(byte[] bphcl) var data = new HairClothData(); //Collect skeletons by name (cloth_skeleton_Cloth_Hair_R ...). - var skeletons = new Dictionary>(StringComparer.Ordinal); + var skeletons = new Dictionary>( + StringComparer.Ordinal + ); foreach (var (_, records) in tag.AllItems("hkaSkeleton")) - foreach (var skel in records) - if (Str(skel, "name") is string n && !skeletons.ContainsKey(n)) - skeletons[n] = skel; + foreach (var skel in records) + if (Str(skel, "name") is string n && !skeletons.ContainsKey(n)) + skeletons[n] = skel; foreach (var (_, records) in tag.AllItems("hclClothData")) - foreach (var cloth in records) - { - var piece = ParsePiece(cloth, skeletons); - if (piece != null) - data.Pieces.Add(piece); - } + foreach (var cloth in records) + { + var piece = ParsePiece(cloth, skeletons); + if (piece != null) + data.Pieces.Add(piece); + } return data; } - static HairClothPiece ParsePiece(Dictionary cloth, - Dictionary> skeletons) + static HairClothPiece ParsePiece( + Dictionary cloth, + Dictionary> skeletons + ) { var piece = new HairClothPiece { Name = Str(cloth, "name") ?? "" }; - void Fail(string why) => Console.WriteLine($"[HairCloth] piece '{piece.Name}' dropped: {why}"); + void Fail(string why) => + Console.WriteLine($"[HairCloth] piece '{piece.Name}' dropped: {why}"); //--- Skeleton: transform set definition name matches the hkaSkeleton name. var tsd = FirstRecord(cloth, "transformSetDefinitions"); string skelName = tsd != null ? Str(tsd, "name") : null; if (skelName == null || !skeletons.TryGetValue(skelName, out var skel)) - { Fail($"skeleton '{skelName}' not found (have: {string.Join(",", skeletons.Keys)})"); return null; } + { + Fail($"skeleton '{skelName}' not found (have: {string.Join(",", skeletons.Keys)})"); + return null; + } - if (skel.GetValueOrDefault("bones") is not List> bones || - skel.GetValueOrDefault("parentIndices") is not List parents || - skel.GetValueOrDefault("referencePose") is not List> pose) - { Fail("skeleton missing bones/parents/pose"); return null; } + if ( + skel.GetValueOrDefault("bones") is not List> bones + || skel.GetValueOrDefault("parentIndices") is not List parents + || skel.GetValueOrDefault("referencePose") + is not List> pose + ) + { + Fail("skeleton missing bones/parents/pose"); + return null; + } int numBones = bones.Count; piece.BoneNames = bones.Select(b => Str(b, "name") ?? "").ToArray(); @@ -65,30 +79,43 @@ static HairClothPiece ParsePiece(Dictionary cloth, var t = Vec3(pose[i].GetValueOrDefault("translation")); var q = Quat(pose[i].GetValueOrDefault("rotation")); var s = Vec3(pose[i].GetValueOrDefault("scale"), Vector3.One); - local[i] = Matrix4.CreateScale(s) * Matrix4.CreateFromQuaternion(q) * - Matrix4.CreateTranslation(t); + local[i] = + Matrix4.CreateScale(s) + * Matrix4.CreateFromQuaternion(q) + * Matrix4.CreateTranslation(t); } piece.BoneRefPose = new Matrix4[numBones]; for (int i = 0; i < numBones; i++) { int parent = piece.BoneParents[i]; - piece.BoneRefPose[i] = parent >= 0 ? local[i] * piece.BoneRefPose[parent] : local[i]; + piece.BoneRefPose[i] = + parent >= 0 ? local[i] * piece.BoneRefPose[parent] : local[i]; } //--- Sim cloth data var sim = FirstRecord(cloth, "simClothDatas"); if (sim == null) - { Fail("no simClothDatas"); return null; } + { + Fail("no simClothDatas"); + return null; + } - if (sim.GetValueOrDefault("particleDatas") is List> particles) - piece.Particles = particles.Select(p => new HairParticle - { - InvMass = F(p, "invMass"), - Radius = F(p, "radius"), - Friction = F(p, "friction"), - }).ToArray(); + if ( + sim.GetValueOrDefault("particleDatas") is List> particles + ) + piece.Particles = particles + .Select(p => new HairParticle + { + InvMass = F(p, "invMass"), + Radius = F(p, "radius"), + Friction = F(p, "friction"), + }) + .ToArray(); else - { Fail("no particleDatas"); return null; } + { + Fail("no particleDatas"); + return null; + } piece.FixedParticles = IntArray(sim.GetValueOrDefault("fixedParticles")); piece.TriangleIndices = IntArray(sim.GetValueOrDefault("triangleIndices")); @@ -110,7 +137,8 @@ static HairClothPiece ParsePiece(Dictionary cloth, foreach (var setObj in sets) { var set = (setObj as List>)?.FirstOrDefault(); - if (set == null) continue; + if (set == null) + continue; string name = Str(set, "name") ?? ""; string detail = ""; foreach (var kv in set) @@ -125,48 +153,78 @@ static HairClothPiece ParsePiece(Dictionary cloth, //Classify by field shape (names vary), keeping the set order so //the simulate operator's constraintExecution indices resolve. var kind = HairConstraintKind.Unknown; - if (set.GetValueOrDefault("links") is List> links && links.Count > 0) + if ( + set.GetValueOrDefault("links") is List> links + && links.Count > 0 + ) { if (links[0].ContainsKey("restLength")) { - var parsed = links.Select(l => new HairLink + var parsed = links + .Select(l => new HairLink + { + A = Convert.ToInt32(l.GetValueOrDefault("particleA") ?? 0), + B = Convert.ToInt32(l.GetValueOrDefault("particleB") ?? 0), + RestLength = F(l, "restLength"), + Stiffness = F(l, "stiffness"), + }) + .ToList(); + if (name.Contains("Stretch")) + { + piece.StretchLinks = parsed; + kind = HairConstraintKind.Stretch; + } + else { - A = Convert.ToInt32(l.GetValueOrDefault("particleA") ?? 0), - B = Convert.ToInt32(l.GetValueOrDefault("particleB") ?? 0), - RestLength = F(l, "restLength"), - Stiffness = F(l, "stiffness"), - }).ToList(); - if (name.Contains("Stretch")) { piece.StretchLinks = parsed; kind = HairConstraintKind.Stretch; } - else { piece.StandardLinks = parsed; kind = HairConstraintKind.Standard; } + piece.StandardLinks = parsed; + kind = HairConstraintKind.Standard; + } } else if (links[0].ContainsKey("bendMinLength")) { - piece.BendLinks = links.Select(l => new HairLink - { - A = Convert.ToInt32(l.GetValueOrDefault("particleA") ?? 0), - B = Convert.ToInt32(l.GetValueOrDefault("particleB") ?? 0), - MinLength = F(l, "bendMinLength"), - MaxLength = F(l, "stretchMaxLength"), - BendStiffness = F(l, "bendStiffness"), - StretchStiffness = F(l, "stretchStiffness"), - }).ToList(); + piece.BendLinks = links + .Select(l => new HairLink + { + A = Convert.ToInt32(l.GetValueOrDefault("particleA") ?? 0), + B = Convert.ToInt32(l.GetValueOrDefault("particleB") ?? 0), + MinLength = F(l, "bendMinLength"), + MaxLength = F(l, "stretchMaxLength"), + BendStiffness = F(l, "bendStiffness"), + StretchStiffness = F(l, "stretchStiffness"), + }) + .ToList(); kind = HairConstraintKind.Bend; } //else: hclBendStiffnessConstraintSet quads (particleA..D) - not simulated. } - else if (set.GetValueOrDefault("localConstraints") is List> locals || - set.GetValueOrDefault("localStiffnessConstraints") is List> stiffLocals && (locals = stiffLocals) != null) + else if ( + set.GetValueOrDefault("localConstraints") + is List> locals + || set.GetValueOrDefault("localStiffnessConstraints") + is List> stiffLocals + && (locals = stiffLocals) != null + ) { - float setStiffness = set.ContainsKey("stiffness") ? F(set, "stiffness") : 1.0f; - piece.LocalRanges = locals.Select(l => new HairLocalRange - { - Particle = Convert.ToInt32(l.GetValueOrDefault("particleIndex") ?? 0), - ReferenceVertex = Convert.ToInt32(l.GetValueOrDefault("referenceVertex") ?? 0), - Radius = F(l, "shapeRadius"), - MaxNormal = F(l, "maxNormalDistance"), - MinNormal = F(l, "minNormalDistance"), - Stiffness = l.ContainsKey("stiffness") ? F(l, "stiffness") : setStiffness, - }).ToList(); + float setStiffness = set.ContainsKey("stiffness") + ? F(set, "stiffness") + : 1.0f; + piece.LocalRanges = locals + .Select(l => new HairLocalRange + { + Particle = Convert.ToInt32( + l.GetValueOrDefault("particleIndex") ?? 0 + ), + ReferenceVertex = Convert.ToInt32( + l.GetValueOrDefault("referenceVertex") ?? 0 + ), + Radius = F(l, "shapeRadius"), + MaxNormal = F(l, "maxNormalDistance"), + MinNormal = F(l, "minNormalDistance"), + Stiffness = l.ContainsKey("stiffness") + ? F(l, "stiffness") + : setStiffness, + }) + .ToList(); kind = HairConstraintKind.LocalRange; } else if (set.ContainsKey("perParticleData")) @@ -176,29 +234,38 @@ static HairClothPiece ParsePiece(Dictionary cloth, } //--- Collidables (capsules attached to transform-set entries) - if (sim.GetValueOrDefault("collidableTransformMap") is Dictionary ctm && - sim.GetValueOrDefault("perInstanceCollidables") is List collidables) + if ( + sim.GetValueOrDefault("collidableTransformMap") is Dictionary ctm + && sim.GetValueOrDefault("perInstanceCollidables") is List collidables + ) { int[] transformIndices = IntArray(ctm.GetValueOrDefault("transformIndices")); - var offsets = (ctm.GetValueOrDefault("offsets") as List)? - .Select(m => Mat4(m)).ToArray() ?? Array.Empty(); + var offsets = + (ctm.GetValueOrDefault("offsets") as List) + ?.Select(m => Mat4(m)) + .ToArray() + ?? Array.Empty(); for (int i = 0; i < collidables.Count; i++) { - var col = (collidables[i] as List>)?.FirstOrDefault(); + var col = ( + collidables[i] as List> + )?.FirstOrDefault(); var shape = col != null ? FirstRecord(col, "shape") : null; if (shape == null || !shape.ContainsKey("start")) continue; - piece.Collidables.Add(new HairCollidable - { - Name = Str(col, "name") ?? "", - Start = Vec3(shape.GetValueOrDefault("start")), - End = Vec3(shape.GetValueOrDefault("end")), - Radius = F(shape, "radius"), - Transform = Mat4(col.GetValueOrDefault("transform")), - BoneIndex = i < transformIndices.Length ? transformIndices[i] : 0, - BoneOffset = i < offsets.Length ? offsets[i] : Matrix4.Identity, - }); + piece.Collidables.Add( + new HairCollidable + { + Name = Str(col, "name") ?? "", + Start = Vec3(shape.GetValueOrDefault("start")), + End = Vec3(shape.GetValueOrDefault("end")), + Radius = F(shape, "radius"), + Transform = Mat4(col.GetValueOrDefault("transform")), + BoneIndex = i < transformIndices.Length ? transformIndices[i] : 0, + BoneOffset = i < offsets.Length ? offsets[i] : Matrix4.Identity, + } + ); } } @@ -208,48 +275,96 @@ static HairClothPiece ParsePiece(Dictionary cloth, foreach (var opObj in ops) { var op = (opObj as List>)?.FirstOrDefault(); - if (op == null) continue; + if (op == null) + continue; - if (op.ContainsKey("objectSpaceDeformer") || op.ContainsKey("boneSpaceDeformer")) + if ( + op.ContainsKey("objectSpaceDeformer") || op.ContainsKey("boneSpaceDeformer") + ) ParseSkinOperator(op, piece); - else if (op.GetValueOrDefault("simulateOpConfigs") is List> cfgs && - cfgs.FirstOrDefault() is Dictionary cfg) + else if ( + op.GetValueOrDefault("simulateOpConfigs") + is List> cfgs + && cfgs.FirstOrDefault() is Dictionary cfg + ) { - piece.SubSteps = Math.Max(1, Convert.ToInt32(cfg.GetValueOrDefault("subSteps") ?? 1)); - piece.SolveIterations = Math.Max(1, Convert.ToInt32(cfg.GetValueOrDefault("numberOfSolveIterations") ?? 1)); - piece.ConstraintExecution = IntArray(cfg.GetValueOrDefault("constraintExecution")) - .Where(i => i >= 0).ToArray(); + piece.SubSteps = Math.Max( + 1, + Convert.ToInt32(cfg.GetValueOrDefault("subSteps") ?? 1) + ); + piece.SolveIterations = Math.Max( + 1, + Convert.ToInt32(cfg.GetValueOrDefault("numberOfSolveIterations") ?? 1) + ); + piece.ConstraintExecution = IntArray( + cfg.GetValueOrDefault("constraintExecution") + ) + .Where(i => i >= 0) + .ToArray(); } - else if (op.GetValueOrDefault("vertexParticlePairs") is List> vpp) - piece.VertexParticlePairs = vpp.Select(p => ( - Convert.ToInt32(p.GetValueOrDefault("vertexIndex") ?? 0), - Convert.ToInt32(p.GetValueOrDefault("particleIndex") ?? 0))).ToList(); - else if (op.GetValueOrDefault("triangleBonePairs") is List> tbp) + else if ( + op.GetValueOrDefault("vertexParticlePairs") + is List> vpp + ) + piece.VertexParticlePairs = vpp.Select(p => + ( + Convert.ToInt32(p.GetValueOrDefault("vertexIndex") ?? 0), + Convert.ToInt32(p.GetValueOrDefault("particleIndex") ?? 0) + ) + ) + .ToList(); + else if ( + op.GetValueOrDefault("triangleBonePairs") + is List> tbp + ) { - var localTransforms = (op.GetValueOrDefault("localBoneTransforms") as List)? - .Select(m => Mat4(m)).ToArray() ?? Array.Empty(); + var localTransforms = + (op.GetValueOrDefault("localBoneTransforms") as List) + ?.Select(m => Mat4(m)) + .ToArray() + ?? Array.Empty(); piece.BoneAxis = Convert.ToInt32(op.GetValueOrDefault("boneAxis") ?? 0); for (int i = 0; i < tbp.Count; i++) { - piece.BoneDeforms.Add(new HairBoneDeform - { - //boneOffset is a byte offset into the transform set (64 = one matrix). - BoneIndex = Convert.ToInt32(tbp[i].GetValueOrDefault("boneOffset") ?? 0) / 64, - //triangleOffset is a byte offset into triangleIndices (u16). - TriangleStart = Convert.ToInt32(tbp[i].GetValueOrDefault("triangleOffset") ?? 0) / 2, - LocalBoneTransform = i < localTransforms.Length ? localTransforms[i] : Matrix4.Identity, - }); + piece.BoneDeforms.Add( + new HairBoneDeform + { + //boneOffset is a byte offset into the transform set (64 = one matrix). + BoneIndex = + Convert.ToInt32(tbp[i].GetValueOrDefault("boneOffset") ?? 0) + / 64, + //triangleOffset is a byte offset into triangleIndices (u16). + TriangleStart = + Convert.ToInt32( + tbp[i].GetValueOrDefault("triangleOffset") ?? 0 + ) / 2, + LocalBoneTransform = + i < localTransforms.Length + ? localTransforms[i] + : Matrix4.Identity, + } + ); } } } } - if (piece.SkinVertices == null) { Fail("no skin operator (SkinVertices)"); return null; } - if (piece.RestPositions == null) { Fail("no rest positions"); return null; } + if (piece.SkinVertices == null) + { + Fail("no skin operator (SkinVertices)"); + return null; + } + if (piece.RestPositions == null) + { + Fail("no rest positions"); + return null; + } //No simulate config found: execute every parsed set once, authored order. if (piece.ConstraintExecution.Length == 0) - piece.ConstraintExecution = Enumerable.Range(0, piece.ConstraintSetKinds.Count).ToArray(); + piece.ConstraintExecution = Enumerable + .Range(0, piece.ConstraintSetKinds.Count) + .ToArray(); return piece; } @@ -263,13 +378,19 @@ static HairClothPiece ParsePiece(Dictionary cloth, static void ParseSkinOperator(Dictionary op, HairClothPiece piece) { piece.TransformSubset = IntArray(op.GetValueOrDefault("transformSubset")); - piece.BoneFromSkinMesh = (op.GetValueOrDefault("boneFromSkinMeshTransforms") as List)? - .Select(m => Mat4(m)).ToArray() + piece.BoneFromSkinMesh = + (op.GetValueOrDefault("boneFromSkinMeshTransforms") as List) + ?.Select(m => Mat4(m)) + .ToArray() //Bone-space skinning: local positions are authored in bone space. - ?? Enumerable.Repeat(Matrix4.Identity, Math.Max(piece.TransformSubset.Length, 1)).ToArray(); - - bool boneSpace = op.GetValueOrDefault("boneSpaceDeformer") is Dictionary; - var osd = op.GetValueOrDefault("objectSpaceDeformer") as Dictionary + ?? Enumerable + .Repeat(Matrix4.Identity, Math.Max(piece.TransformSubset.Length, 1)) + .ToArray(); + + bool boneSpace = + op.GetValueOrDefault("boneSpaceDeformer") is Dictionary; + var osd = + op.GetValueOrDefault("objectSpaceDeformer") as Dictionary ?? op.GetValueOrDefault("boneSpaceDeformer") as Dictionary; if (osd == null) return; @@ -290,13 +411,21 @@ static void ParseSkinOperator(Dictionary op, HairClothPiece piec continue; foreach (var entry in packed) { - var vals = entry as List - ?? (entry as Dictionary)?.GetValueOrDefault("values") as List; + var vals = + entry as List + ?? (entry as Dictionary)?.GetValueOrDefault("values") + as List; if (vals == null || vals.Count != 4) continue; if (vals[0] is float or double) - localPs.Add(new Vector4(Convert.ToSingle(vals[0]), Convert.ToSingle(vals[1]), - Convert.ToSingle(vals[2]), Convert.ToSingle(vals[3]))); + localPs.Add( + new Vector4( + Convert.ToSingle(vals[0]), + Convert.ToSingle(vals[1]), + Convert.ToSingle(vals[2]), + Convert.ToSingle(vals[3]) + ) + ); else localPs.Add(new Vector4(UnpackVector3(vals), 1)); } @@ -311,9 +440,14 @@ static void ParseSkinOperator(Dictionary op, HairClothPiece piec { string key = n switch { - 1 => "oneBlendEntries", 2 => "twoBlendEntries", 3 => "threeBlendEntries", - 4 => "fourBlendEntries", 5 => "fiveBlendEntries", 6 => "sixBlendEntries", - 7 => "sevenBlendEntries", _ => "eightBlendEntries", + 1 => "oneBlendEntries", + 2 => "twoBlendEntries", + 3 => "threeBlendEntries", + 4 => "fourBlendEntries", + 5 => "fiveBlendEntries", + 6 => "sixBlendEntries", + 7 => "sevenBlendEntries", + _ => "eightBlendEntries", }; if (osd.GetValueOrDefault(key) is List> entries) queues[n] = new Queue>(entries); @@ -324,19 +458,26 @@ static void ParseSkinOperator(Dictionary op, HairClothPiece piec //0=four, 1=three, 2=two, 3=one blend (5..8 blends appended as 4..7). int[] blendCountForControl = { 4, 3, 2, 1, 8, 7, 6, 5 }; int[] controlBytes = IntArray(osd.GetValueOrDefault("controlBytes")) - .Where(b => b >= 0 && b < 8).Select(b => blendCountForControl[b]).ToArray(); + .Where(b => b >= 0 && b < 8) + .Select(b => blendCountForControl[b]) + .ToArray(); if (controlBytes.Length == 0) { //Single-type deformers can omit control bytes. - controlBytes = queues.OrderByDescending(kv => kv.Key) - .SelectMany(kv => Enumerable.Repeat(kv.Key, kv.Value.Count)).ToArray(); + controlBytes = queues + .OrderByDescending(kv => kv.Key) + .SelectMany(kv => Enumerable.Repeat(kv.Key, kv.Value.Count)) + .ToArray(); } int globalBlock = 0; foreach (int bonesPerVertex in controlBytes) { if (!queues.TryGetValue(bonesPerVertex, out var queue) || queue.Count == 0) - { globalBlock++; continue; } + { + globalBlock++; + continue; + } var block = queue.Dequeue(); int[] vertexIndices = IntArray(block.GetValueOrDefault("vertexIndices")); @@ -345,7 +486,8 @@ static void ParseSkinOperator(Dictionary op, HairClothPiece piec for (int v = 0; v < vertexIndices.Length && v < 16; v++) { int vi = vertexIndices[v]; - if (vi > endVertex || verts[vi] != null) continue; + if (vi > endVertex || verts[vi] != null) + continue; var sv = new HairSkinVertex { Bones = new int[bonesPerVertex], @@ -374,13 +516,16 @@ static void ParseSkinOperator(Dictionary op, HairClothPiece piec //weights of one vertex sum to 255. One-blend blocks have no //weight array (implicit 1). int localIdx = globalBlock * 16 + v; - sv.LocalPos = localIdx < localPs.Count ? localPs[localIdx].Xyz : Vector3.Zero; + sv.LocalPos = + localIdx < localPs.Count ? localPs[localIdx].Xyz : Vector3.Zero; for (int b = 0; b < bonesPerVertex; b++) { int idx = v * bonesPerVertex + b; sv.Bones[b] = idx < boneIndices.Length ? boneIndices[idx] : 0; - sv.Weights[b] = idx < weights.Length ? weights[idx] / 255.0f - : (bonesPerVertex == 1 ? 1.0f : 0); + sv.Weights[b] = + idx < weights.Length + ? weights[idx] / 255.0f + : (bonesPerVertex == 1 ? 1.0f : 0); } } verts[vi] = sv; @@ -398,12 +543,15 @@ static void ParseSkinOperator(Dictionary op, HairClothPiece piec /// static Vector3 UnpackVector3(List rawBits) { - short[] v = rawBits.Select(x => unchecked((short)(Convert.ToInt32(x) & 0xFFFF))).ToArray(); + short[] v = rawBits + .Select(x => unchecked((short)(Convert.ToInt32(x) & 0xFFFF))) + .ToArray(); float exp = BitConverter.Int32BitsToSingle(v[3] << 16); return new Vector3( (float)(v[0] << 16) * exp, (float)(v[1] << 16) * exp, - (float)(v[2] << 16) * exp); + (float)(v[2] << 16) * exp + ); } #region graph helpers @@ -422,7 +570,9 @@ static Dictionary FirstRecord(Dictionary d, stri if (v is List> recs) return recs.FirstOrDefault(); if (v is List list) - return (list.FirstOrDefault() as List>)?.FirstOrDefault(); + return ( + list.FirstOrDefault() as List> + )?.FirstOrDefault(); return null; } @@ -436,7 +586,11 @@ static int[] IntArray(object v) static Vector3 Vec3(object v, Vector3? def = null) { if (v is List list && list.Count >= 3) - return new Vector3(Convert.ToSingle(list[0]), Convert.ToSingle(list[1]), Convert.ToSingle(list[2])); + return new Vector3( + Convert.ToSingle(list[0]), + Convert.ToSingle(list[1]), + Convert.ToSingle(list[2]) + ); return def ?? Vector3.Zero; } @@ -446,11 +600,19 @@ static Quaternion Quat(object v) if (v is List list) { if (list.Count == 4 && list[0] is not List) - return new Quaternion(Convert.ToSingle(list[0]), Convert.ToSingle(list[1]), - Convert.ToSingle(list[2]), Convert.ToSingle(list[3])); + return new Quaternion( + Convert.ToSingle(list[0]), + Convert.ToSingle(list[1]), + Convert.ToSingle(list[2]), + Convert.ToSingle(list[3]) + ); if (list.FirstOrDefault() is List inner && inner.Count >= 4) - return new Quaternion(Convert.ToSingle(inner[0]), Convert.ToSingle(inner[1]), - Convert.ToSingle(inner[2]), Convert.ToSingle(inner[3])); + return new Quaternion( + Convert.ToSingle(inner[0]), + Convert.ToSingle(inner[1]), + Convert.ToSingle(inner[2]), + Convert.ToSingle(inner[3]) + ); } return Quaternion.Identity; } @@ -465,10 +627,23 @@ static Matrix4 Mat4(object v) return Matrix4.Identity; float[] f = list.Select(Convert.ToSingle).ToArray(); return new Matrix4( - f[0], f[1], f[2], 0, - f[4], f[5], f[6], 0, - f[8], f[9], f[10], 0, - f[12], f[13], f[14], 1); + f[0], + f[1], + f[2], + 0, + f[4], + f[5], + f[6], + 0, + f[8], + f[9], + f[10], + 0, + f[12], + f[13], + f[14], + 1 + ); } #endregion @@ -481,11 +656,11 @@ public class HairClothPiece //Cloth skeleton (bone 0 = attach root e.g. Head_Root, last often Spine_3). public string[] BoneNames; public int[] BoneParents; - public Matrix4[] BoneRefPose; //model space + public Matrix4[] BoneRefPose; //model space //Skinning (drives fixed particles + local range references) public Matrix4[] BoneFromSkinMesh; - public int[] TransformSubset; //transform-set indices used by the skin + public int[] TransformSubset; //transform-set indices used by the skin public HairSkinVertex[] SkinVertices; //Simulation @@ -517,17 +692,24 @@ public class HairClothPiece public enum HairConstraintKind { - Unknown, Standard, Stretch, Bend, LocalRange, Transition, + Unknown, + Standard, + Stretch, + Bend, + LocalRange, + Transition, } public class HairParticle { - public float InvMass, Radius, Friction; + public float InvMass, + Radius, + Friction; } public class HairSkinVertex { - public int[] Bones; //indices into TransformSubset + public int[] Bones; //indices into TransformSubset public float[] Weights; public Vector3 LocalPos; //skin-mesh space (object-space deformer) public Vector3[] LocalPosPerBone; //bone space, one per blend slot (bone-space deformer) @@ -535,33 +717,41 @@ public class HairSkinVertex public class HairLink { - public int A, B; - public float RestLength, Stiffness; - public float MinLength, MaxLength, BendStiffness, StretchStiffness; + public int A, + B; + public float RestLength, + Stiffness; + public float MinLength, + MaxLength, + BendStiffness, + StretchStiffness; } public class HairLocalRange { - public int Particle, ReferenceVertex; + public int Particle, + ReferenceVertex; public float Radius; - public float MaxNormal, MinNormal; + public float MaxNormal, + MinNormal; public float Stiffness = 1.0f; } public class HairCollidable { public string Name; - public Vector3 Start, End; + public Vector3 Start, + End; public float Radius; - public Matrix4 Transform; //rest/reference transform - public int BoneIndex; //transform-set index the capsule follows - public Matrix4 BoneOffset; //offset from the bone to collidable space + public Matrix4 Transform; //rest/reference transform + public int BoneIndex; //transform-set index the capsule follows + public Matrix4 BoneOffset; //offset from the bone to collidable space } public class HairBoneDeform { - public int BoneIndex; //transform-set index to write - public int TriangleStart; //index into TriangleIndices (start of 3) + public int BoneIndex; //transform-set index to write + public int TriangleStart; //index into TriangleIndices (start of 3) public Matrix4 LocalBoneTransform; } } diff --git a/PlayerViewer/Player/HairPhysics.cs b/PlayerViewer/Player/HairPhysics.cs index cfc7b52..668c3ce 100644 --- a/PlayerViewer/Player/HairPhysics.cs +++ b/PlayerViewer/Player/HairPhysics.cs @@ -12,7 +12,7 @@ namespace PlayerViewer.Player /// animation pose, constrained by the authored link/range sets, collided against /// the authored capsules, and finally written back to the hair bones through the /// triangle-frame bone deform. - /// + /// /// Not necessarily perfectly accurate to Havok, since its not based on a proper decomp of Havok clothes. /// public class HairPhysics @@ -20,14 +20,14 @@ public class HairPhysics public bool Enabled = true; readonly HairClothPiece _piece; - readonly STBone[] _bones; //cloth skeleton index -> scene bone - readonly STBone[] _driven; //bones written by the deform (subset) + readonly STBone[] _bones; //cloth skeleton index -> scene bone + readonly STBone[] _driven; //bones written by the deform (subset) readonly Vector3[] _pos; readonly Vector3[] _prev; - readonly Vector3[] _skinned; //animation-pose skinned vertices - readonly Matrix4[] _boneWorld; //current per-frame transform set - readonly int[] _refVertex; //particle -> reference buffer vertex (-1: none) + readonly Vector3[] _skinned; //animation-pose skinned vertices + readonly Matrix4[] _boneWorld; //current per-frame transform set + readonly int[] _refVertex; //particle -> reference buffer vertex (-1: none) readonly float[][] _capsuleMinDist; //per collidable x particle: rest-aware pushout distance bool _primed; float _accumulator; @@ -54,7 +54,11 @@ public class HairPhysics { _refVertex[p] = p < _skinned.Length ? p : -1; foreach (var range in piece.LocalRanges) - if (range.Particle == p) { _refVertex[p] = range.ReferenceVertex; break; } + if (range.Particle == p) + { + _refVertex[p] = range.ReferenceVertex; + break; + } } foreach (var (vertex, particle) in piece.VertexParticlePairs) if (particle >= 0 && particle < n) @@ -85,8 +89,10 @@ public class HairPhysics static float DistanceToSegment(Vector3 p, Vector3 a, Vector3 b) { Vector3 ab = b - a; - float t = ab.LengthSquared > 1e-9f - ? MathHelper.Clamp(Vector3.Dot(p - a, ab) / ab.LengthSquared, 0, 1) : 0; + float t = + ab.LengthSquared > 1e-9f + ? MathHelper.Clamp(Vector3.Dot(p - a, ab) / ab.LengthSquared, 0, 1) + : 0; return (p - (a + ab * t)).Length; } @@ -95,7 +101,11 @@ static float DistanceToSegment(Vector3 p, Vector3 a, Vector3 b) /// the hair part skeleton first, then the human skeleton (Spine_3 etc. live /// there). Returns null when required bones are missing. /// - public static HairPhysics Create(HairClothPiece piece, STSkeleton hairSkeleton, STSkeleton humanSkeleton) + public static HairPhysics Create( + HairClothPiece piece, + STSkeleton hairSkeleton, + STSkeleton humanSkeleton + ) { if (piece.SkinVertices == null || piece.BoneDeforms.Count == 0) return null; @@ -103,7 +113,8 @@ public static HairPhysics Create(HairClothPiece piece, STSkeleton hairSkeleton, var bones = new STBone[piece.BoneNames.Length]; for (int i = 0; i < bones.Length; i++) { - bones[i] = hairSkeleton.SearchBone(piece.BoneNames[i]) + bones[i] = + hairSkeleton.SearchBone(piece.BoneNames[i]) ?? humanSkeleton?.SearchBone(piece.BoneNames[i]); if (bones[i] == null) return null; @@ -121,22 +132,28 @@ public void DebugDump() for (int p = 0; p < _pos.Length; p++) { Vector3 target = SkinnedForParticle(p); - Console.WriteLine($" p{p}{(IsFixed(p) ? " FIX" : " ")} pos=({_pos[p].X:F3},{_pos[p].Y:F3},{_pos[p].Z:F3}) " + - $"skin=({target.X:F3},{target.Y:F3},{target.Z:F3}) drift={(_pos[p] - target).Length:F3}"); + Console.WriteLine( + $" p{p}{(IsFixed(p) ? " FIX" : " ")} pos=({_pos[p].X:F3},{_pos[p].Y:F3},{_pos[p].Z:F3}) " + + $"skin=({target.X:F3},{target.Y:F3},{target.Z:F3}) drift={(_pos[p] - target).Length:F3}" + ); } foreach (var range in _piece.LocalRanges) { Vector3 c = _skinned[range.ReferenceVertex]; float d = (_pos[range.Particle] - c).Length; - Console.WriteLine($" range p{range.Particle} ref=v{range.ReferenceVertex} r={range.Radius:F3} dist={d:F3}{(d > range.Radius + 0.001f ? " VIOLATED" : "")}"); + Console.WriteLine( + $" range p{range.Particle} ref=v{range.ReferenceVertex} r={range.Radius:F3} dist={d:F3}{(d > range.Radius + 0.001f ? " VIOLATED" : "")}" + ); } foreach (var col in _piece.Collidables) { var world = col.BoneOffset * _boneWorld[col.BoneIndex]; Vector3 a = Vector3.TransformPosition(col.Start, world); Vector3 b = Vector3.TransformPosition(col.End, world); - Console.WriteLine($" capsule '{col.Name}' bone={_piece.BoneNames[col.BoneIndex]} r={col.Radius:F3} " + - $"a=({a.X:F3},{a.Y:F3},{a.Z:F3}) b=({b.X:F3},{b.Y:F3},{b.Z:F3})"); + Console.WriteLine( + $" capsule '{col.Name}' bone={_piece.BoneNames[col.BoneIndex]} r={col.Radius:F3} " + + $"a=({a.X:F3},{a.Y:F3},{a.Z:F3}) b=({b.X:F3},{b.Y:F3},{b.Z:F3})" + ); } } @@ -165,7 +182,10 @@ public void Update(float dt, Dictionary arrange = null //buffer mapping in every asset. var restToWorld = Matrix4.Invert(_piece.BoneRefPose[0]) * _boneWorld[0]; for (int p = 0; p < _pos.Length; p++) - _pos[p] = _prev[p] = Vector3.TransformPosition(_piece.RestPositions[p], restToWorld); + _pos[p] = _prev[p] = Vector3.TransformPosition( + _piece.RestPositions[p], + restToWorld + ); _primed = true; } @@ -198,12 +218,17 @@ void SkinVertices() if (sv.Weights[b] <= 0) continue; int subset = sv.Bones[b]; - int bone = subset < _piece.TransformSubset.Length ? _piece.TransformSubset[subset] : subset; + int bone = + subset < _piece.TransformSubset.Length + ? _piece.TransformSubset[subset] + : subset; //Bone-space deformer: position is authored per blend slot in bone //space. Object-space: shared position through boneFromSkinMesh. Vector3 p = sv.LocalPosPerBone != null ? sv.LocalPosPerBone[b] : sv.LocalPos; - var mat = sv.LocalPosPerBone != null ? _boneWorld[bone] - : _piece.BoneFromSkinMesh[subset] * _boneWorld[bone]; + var mat = + sv.LocalPosPerBone != null + ? _boneWorld[bone] + : _piece.BoneFromSkinMesh[subset] * _boneWorld[bone]; result += sv.Weights[b] * Vector3.TransformPosition(p, mat); } _skinned[vi] = result; @@ -244,8 +269,11 @@ void Step(float dt) //links and 0.4 for soft strand tips). for (int iter = 0; iter < Math.Clamp(piece.SolveIterations, 1, 8); iter++) foreach (int setIndex in piece.ConstraintExecution) - SolveConstraintSet(setIndex < piece.ConstraintSetKinds.Count - ? piece.ConstraintSetKinds[setIndex] : HairConstraintKind.Unknown); + SolveConstraintSet( + setIndex < piece.ConstraintSetKinds.Count + ? piece.ConstraintSetKinds[setIndex] + : HairConstraintKind.Unknown + ); SolveCapsules(); } @@ -259,13 +287,25 @@ void SolveConstraintSet(HairConstraintKind kind) case HairConstraintKind.Standard: //Spring toward rest length. foreach (var link in piece.StandardLinks) - SolveDistance(link.A, link.B, link.RestLength, Math.Min(link.Stiffness, 1), onlyIfLonger: false); + SolveDistance( + link.A, + link.B, + link.RestLength, + Math.Min(link.Stiffness, 1), + onlyIfLonger: false + ); break; case HairConstraintKind.Stretch: //Hard upper bound on length. foreach (var link in piece.StretchLinks) - SolveDistance(link.A, link.B, link.RestLength, Math.Min(link.Stiffness, 1), onlyIfLonger: true); + SolveDistance( + link.A, + link.B, + link.RestLength, + Math.Min(link.Stiffness, 1), + onlyIfLonger: true + ); break; case HairConstraintKind.Bend: @@ -274,9 +314,21 @@ void SolveConstraintSet(HairConstraintKind kind) { float d = (_pos[link.B] - _pos[link.A]).Length; if (d < link.MinLength) - SolveDistance(link.A, link.B, link.MinLength, Math.Clamp(link.BendStiffness, 0, 1), onlyIfLonger: false); + SolveDistance( + link.A, + link.B, + link.MinLength, + Math.Clamp(link.BendStiffness, 0, 1), + onlyIfLonger: false + ); else if (d > link.MaxLength) - SolveDistance(link.A, link.B, link.MaxLength, Math.Clamp(link.StretchStiffness, 0, 1), onlyIfLonger: true); + SolveDistance( + link.A, + link.B, + link.MaxLength, + Math.Clamp(link.StretchStiffness, 0, 1), + onlyIfLonger: true + ); } break; @@ -295,8 +347,11 @@ void SolveConstraintSet(HairConstraintKind kind) if (dist > range.Radius && dist > 1e-7f) { Vector3 clamped = center + delta * (range.Radius / dist); - _pos[range.Particle] = Vector3.Lerp(_pos[range.Particle], clamped, - Math.Clamp(range.Stiffness, 0, 1)); + _pos[range.Particle] = Vector3.Lerp( + _pos[range.Particle], + clamped, + Math.Clamp(range.Stiffness, 0, 1) + ); } } break; @@ -345,8 +400,14 @@ void SolveCapsules() //Closest point on segment ab. Vector3 ab = b - a; - float t = ab.LengthSquared > 1e-9f - ? MathHelper.Clamp(Vector3.Dot(_pos[p] - a, ab) / ab.LengthSquared, 0, 1) : 0; + float t = + ab.LengthSquared > 1e-9f + ? MathHelper.Clamp( + Vector3.Dot(_pos[p] - a, ab) / ab.LengthSquared, + 0, + 1 + ) + : 0; Vector3 closest = a + ab * t; Vector3 delta = _pos[p] - closest; @@ -371,8 +432,11 @@ void WriteBones(Dictionary arrange) continue; //Arrange-collapsed bone: keep the welded (collapsed) transform. - if (arrange != null && arrange.TryGetValue(_driven[i].Name, out var arr) && - arr.Scale.X * arr.Scale.Y * arr.Scale.Z < 0.001f) + if ( + arrange != null + && arrange.TryGetValue(_driven[i].Name, out var arr) + && arr.Scale.X * arr.Scale.Y * arr.Scale.Z < 0.001f + ) continue; Vector3 p0 = _pos[_piece.TriangleIndices[bd.TriangleStart]]; @@ -381,13 +445,27 @@ void WriteBones(Dictionary arrange) Vector3 c = (p0 + p1 + p2) / 3.0f; Vector3 n = Vector3.Cross(p1 - p0, p2 - p0) / 3.0f; - Vector3 r0 = p0 - c, r1 = p1 - c; + Vector3 r0 = p0 - c, + r1 = p1 - c; var frame = new Matrix4( - r0.X, r0.Y, r0.Z, 0, - r1.X, r1.Y, r1.Z, 0, - n.X, n.Y, n.Z, 0, - c.X, c.Y, c.Z, 1); + r0.X, + r0.Y, + r0.Z, + 0, + r1.X, + r1.Y, + r1.Z, + 0, + n.X, + n.Y, + n.Z, + 0, + c.X, + c.Y, + c.Z, + 1 + ); _driven[i].Transform = bd.LocalBoneTransform * frame; } diff --git a/PlayerViewer/Player/PartModel.cs b/PlayerViewer/Player/PartModel.cs index 9759373..9b2eda5 100644 --- a/PlayerViewer/Player/PartModel.cs +++ b/PlayerViewer/Player/PartModel.cs @@ -19,8 +19,8 @@ public enum PartKind ShoeLeft, ShoeRight, Tank, - WeaponMain, //Right hand (Weapon_R) - WeaponLeft, //Left hand (Weapon_L), such as dualies second model + WeaponMain, //Right hand (Weapon_R) + WeaponLeft, //Left hand (Weapon_L), such as dualies second model } /// @@ -103,8 +103,13 @@ public class PoseSrt /// nameMap maps special gear bone names to human bone names (e.g. Head_Root->Head). /// mirrorLR remaps _L/_R suffixes (right shoe reusing the left shoe model). /// - public void ResolveWelds(STSkeleton human, Dictionary nameMap, - bool mirrorLR = false, bool uprightWeld = false, bool mapOnly = false) + public void ResolveWelds( + STSkeleton human, + Dictionary nameMap, + bool mirrorLR = false, + bool uprightWeld = false, + bool mapOnly = false + ) { WeldTargets = new STBone[Skeleton.Bones.Count]; WeldPre = new Matrix4[Skeleton.Bones.Count]; @@ -145,8 +150,10 @@ public void ResolveWelds(STSkeleton human, Dictionary nameMap, var visited = new HashSet(); void Visit(STBone b) { - if (!visited.Add(b)) return; - if (b.Parent != null) Visit(b.Parent); + if (!visited.Add(b)) + return; + if (b.Parent != null) + Visit(b.Parent); ordered.Add(b); } foreach (var bone in Skeleton.Bones) @@ -172,8 +179,10 @@ static Quaternion RestWorldRotation(STBone bone) public static string SwapLR(string name) { - if (name.EndsWith("_L")) return name.Substring(0, name.Length - 2) + "_R"; - if (name.EndsWith("_R")) return name.Substring(0, name.Length - 2) + "_L"; + if (name.EndsWith("_L")) + return name.Substring(0, name.Length - 2) + "_R"; + if (name.EndsWith("_R")) + return name.Substring(0, name.Length - 2) + "_L"; return name; } @@ -188,11 +197,12 @@ public static string SwapLR(string name) //rotation/scale (otherwise one arrange-collapsed bone would flatten the //whole chain below it instead of just its own mesh segment). readonly Dictionary _weldScale = new(); - readonly Dictionary _weldRT = new(); //scale-free world + readonly Dictionary _weldRT = new(); //scale-free world public void ApplyWeld() { - if (WeldTargets == null) return; + if (WeldTargets == null) + return; _weldScale.Clear(); _weldRT.Clear(); @@ -201,8 +211,8 @@ public void ApplyWeld() int i = Skeleton.Bones.IndexOf(bone); var target = WeldTargets[i]; - Matrix4 rt; //world rotation+translation (no scale) - Vector3 scale; //own local scale (applies to skinning + child positions) + Matrix4 rt; //world rotation+translation (no scale) + Vector3 scale; //own local scale (applies to skinning + child positions) if (target != null) { rt = WeldPre[i] * target.Transform; @@ -235,17 +245,23 @@ public void ApplyWeld() } else { - Vector3 parentScale = bone.Parent != null && - _weldScale.TryGetValue(bone.Parent, out var ps) ? ps : Vector3.One; - Matrix4 parentRT = bone.Parent != null && - _weldRT.TryGetValue(bone.Parent, out var prt) ? prt : Matrix4.Identity; + Vector3 parentScale = + bone.Parent != null && _weldScale.TryGetValue(bone.Parent, out var ps) + ? ps + : Vector3.One; + Matrix4 parentRT = + bone.Parent != null && _weldRT.TryGetValue(bone.Parent, out var prt) + ? prt + : Matrix4.Identity; GetLocalPose(bone, out scale, out Quaternion rot, out Vector3 pos); //Child position inherits the parent's own scale (Maya behavior); //rotation/scale do not. pos *= parentScale; - rt = Matrix4.CreateFromQuaternion(rot) * - Matrix4.CreateTranslation(pos) * parentRT; + rt = + Matrix4.CreateFromQuaternion(rot) + * Matrix4.CreateTranslation(pos) + * parentRT; } _weldRT[bone] = rt; @@ -254,15 +270,18 @@ public void ApplyWeld() } } - static Vector3 ClampScale(Vector3 s) => new Vector3( - Math.Max(s.X, 0.01f), Math.Max(s.Y, 0.01f), Math.Max(s.Z, 0.01f)); + static Vector3 ClampScale(Vector3 s) => + new Vector3(Math.Max(s.X, 0.01f), Math.Max(s.Y, 0.01f), Math.Max(s.Z, 0.01f)); static Matrix4 ArrangeRotation(ArrangeBoneParam arr) { - return Matrix4.CreateFromQuaternion(Quaternion.FromEulerAngles( - MathHelper.DegreesToRadians(arr.RotationDeg.X), - MathHelper.DegreesToRadians(arr.RotationDeg.Y), - MathHelper.DegreesToRadians(arr.RotationDeg.Z))); + return Matrix4.CreateFromQuaternion( + Quaternion.FromEulerAngles( + MathHelper.DegreesToRadians(arr.RotationDeg.X), + MathHelper.DegreesToRadians(arr.RotationDeg.Y), + MathHelper.DegreesToRadians(arr.RotationDeg.Z) + ) + ); } void GetLocalPose(STBone bone, out Vector3 scale, out Quaternion rot, out Vector3 pos) @@ -286,20 +305,22 @@ void GetLocalPose(STBone bone, out Vector3 scale, out Quaternion rot, out Vector scale = new Vector3( Math.Max(scale.X * arr.Scale.X, 0.01f), Math.Max(scale.Y * arr.Scale.Y, 0.01f), - Math.Max(scale.Z * arr.Scale.Z, 0.01f)); + Math.Max(scale.Z * arr.Scale.Z, 0.01f) + ); //S2 decomp (hideEarHair_BeforeCalcDraw): finalRot = restRot * arrangeRot //(column conv) = arrange applied in the bone's local frame. In OpenTK //quaternion order that is rest * arrange. Euler XYZ, degrees. var arrRot = Quaternion.FromEulerAngles( MathHelper.DegreesToRadians(arr.RotationDeg.X), MathHelper.DegreesToRadians(arr.RotationDeg.Y), - MathHelper.DegreesToRadians(arr.RotationDeg.Z)); + MathHelper.DegreesToRadians(arr.RotationDeg.Z) + ); rot = rot * arrRot; //The translate is authored in model space (Y = down folds the OCT001 //ponytail); hair bones inherit the head bone's 90° rest twist, so //express it in the parent's bind frame before adding to the local pos. - var parentBind = bone.Parent != null - ? RestWorldRotation(bone.Parent) : Quaternion.Identity; + var parentBind = + bone.Parent != null ? RestWorldRotation(bone.Parent) : Quaternion.Identity; pos += Vector3.Transform(arr.Translate, Quaternion.Invert(parentBind)); } } @@ -313,10 +334,23 @@ void GetLocalPose(STBone bone, out Vector3 scale, out Quaternion rot, out Vector public static Matrix4 NegateMatrix(Matrix4 m) { return new Matrix4( - -m.Row0.X, -m.Row0.Y, -m.Row0.Z, m.Row0.W, - -m.Row1.X, -m.Row1.Y, -m.Row1.Z, m.Row1.W, - -m.Row2.X, -m.Row2.Y, -m.Row2.Z, m.Row2.W, - m.Row3.X, m.Row3.Y, m.Row3.Z, m.Row3.W); + -m.Row0.X, + -m.Row0.Y, + -m.Row0.Z, + m.Row0.W, + -m.Row1.X, + -m.Row1.Y, + -m.Row1.Z, + m.Row1.W, + -m.Row2.X, + -m.Row2.Y, + -m.Row2.Z, + m.Row2.W, + m.Row3.X, + m.Row3.Y, + m.Row3.Z, + m.Row3.W + ); } /// diff --git a/PlayerViewer/Player/PlayerScene.cs b/PlayerViewer/Player/PlayerScene.cs index 1813301..06b6d5d 100644 --- a/PlayerViewer/Player/PlayerScene.cs +++ b/PlayerViewer/Player/PlayerScene.cs @@ -22,16 +22,22 @@ public class PlayerScene : IDisposable, IViewScene public GameDatabase Database; //--- Configuration - public int PlayerType { get; private set; } = 0; //0=Inkling F,1=Inkling M,2=Octoling F,3=Octoling M + public int PlayerType { get; private set; } = 0; //0=Inkling F,1=Inkling M,2=Octoling F,3=Octoling M public bool IsFemale => PlayerType == 0 || PlayerType == 2; public string PlayerModelName => $"Player{PlayerType:00}"; - public GearEntry CurrentHair, CurrentEyebrow, CurrentHead, CurrentClothes, - CurrentShoes, CurrentBottom, CurrentTank, CurrentWeapon; - public int EyeColor = 0; //0..20 (Color_Eye TPA frame) - public int SkinTone = 0; //0..8 (Color_Skin SPA frame) + public GearEntry CurrentHair, + CurrentEyebrow, + CurrentHead, + CurrentClothes, + CurrentShoes, + CurrentBottom, + CurrentTank, + CurrentWeapon; + public int EyeColor = 0; //0..20 (Color_Eye TPA frame) + public int SkinTone = 0; //0..8 (Color_Skin SPA frame) public int TeamColorIndex = 0; - public int TeamIndex = 0; //0=Alpha 1=Bravo 2=Charlie + public int TeamIndex = 0; //0=Alpha 1=Bravo 2=Charlie //--- Runtime state public PartModel Human { get; private set; } @@ -51,7 +57,7 @@ public class PlayerScene : IDisposable, IViewScene int _renderIdCounter = 0; readonly AlphaMaskSystem _alphaMask = new(); - List _bodyMaskMaterials; //M_Body materials with the _o0 override active + List _bodyMaskMaterials; //M_Body materials with the _o0 override active //Hair cloth simulation (bphcl), one sim per cloth piece. public bool HairPhysicsEnabled = true; @@ -76,7 +82,10 @@ public BFRES LoadModelFile(string modelName) Console.WriteLine($"[Scene] Model not found: {modelName}"); return null; } - var bfres = LoadModelData(data, Path.Combine(Romfs.Root, "Model", modelName + ".bfres")); + var bfres = LoadModelData( + data, + Path.Combine(Romfs.Root, "Model", modelName + ".bfres") + ); if (bfres != null) BfresHelpers.ResolveSharedAssets(bfres, data, Romfs); return bfres; @@ -99,7 +108,8 @@ public BFRES LoadModelData(byte[] data, string fakePath) } var render = (BfresRender)bfres.Renderer; - render.ID = (_renderIdCounter++).ToString() + "_" + Path.GetFileNameWithoutExtension(fakePath); + render.ID = + (_renderIdCounter++).ToString() + "_" + Path.GetFileNameWithoutExtension(fakePath); DataCache.ModelCache[render.ID] = render; return bfres; } @@ -111,7 +121,7 @@ public BFRES LoadModelData(byte[] data, string fakePath) //setters anyway. const int PartCacheCapacity = 5; readonly Dictionary _partCache = new(); - readonly List _partCacheLru = new(); //front = oldest + readonly List _partCacheLru = new(); //front = oldest BFRES TakeCachedPart(string modelName) { @@ -156,7 +166,12 @@ void ClearPartCache() /// subModel: name of the model inside the bfres when it contains several /// (dualies: Wmn_X + Wmn_X_L in one file). Other models get hidden. /// - PartModel CreatePart(PartKind kind, string modelName, BFRES loaded = null, string subModel = null) + PartModel CreatePart( + PartKind kind, + string modelName, + BFRES loaded = null, + string subModel = null + ) { if (string.IsNullOrEmpty(modelName)) return null; @@ -171,8 +186,11 @@ PartModel CreatePart(PartKind kind, string modelName, BFRES loaded = null, strin var asset = (BfresModelAsset)render.Models[0]; if (subModel != null && render.Models.Count > 1) { - asset = render.Models.OfType() - .FirstOrDefault(m => m.ModelData.Name == subModel) ?? asset; + asset = + render + .Models.OfType() + .FirstOrDefault(m => m.ModelData.Name == subModel) + ?? asset; } //Hide all but the selected model (each part owns its own BFRES instance). foreach (var m in render.Models.OfType()) @@ -222,7 +240,9 @@ public void SetPlayerType(int type) //Animation parsing is pure CPU work (no GL), so it can run alongside the //human model load instead of after it. - var animTask = System.Threading.Tasks.Task.Run(() => Anims.Load(Romfs, PlayerModelName)); + var animTask = System.Threading.Tasks.Task.Run(() => + Anims.Load(Romfs, PlayerModelName) + ); Human = CreatePart(PartKind.Human, PlayerModelName); if (Human == null) @@ -230,14 +250,16 @@ public void SetPlayerType(int type) animTask.Wait(); - //Defaults: id 0 rows. Weapon defaults to none ("Free" in game terms). - //Head defaults to Hed_INV000 (the game's invisible no-headgear actor, - //which still applies default hair-arrange presets). + //Defaults: id 0 rows. Weapon and Tank default to none so they can stay unequipped. + //Head defaults to Hed_INV000 (no-headgear actor, still applies hair-arrange presets). CurrentHead ??= Database.Head.FirstOrDefault(x => x.RowId == "Hed_INV000"); - CurrentHair ??= Database.Hair.FirstOrDefault(x => x.Id == 0) ?? Database.Hair.FirstOrDefault(); - CurrentEyebrow ??= Database.Eyebrow.FirstOrDefault(x => x.Id == 0) ?? Database.Eyebrow.FirstOrDefault(); - CurrentBottom ??= Database.Bottom.FirstOrDefault(x => x.Id == 0) ?? Database.Bottom.FirstOrDefault(); - CurrentTank ??= Database.Tank.FirstOrDefault(x => x.Id == 0) ?? Database.Tank.FirstOrDefault(); + CurrentHair ??= + Database.Hair.FirstOrDefault(x => x.Id == 0) ?? Database.Hair.FirstOrDefault(); + CurrentEyebrow ??= + Database.Eyebrow.FirstOrDefault(x => x.Id == 0) + ?? Database.Eyebrow.FirstOrDefault(); + CurrentBottom ??= + Database.Bottom.FirstOrDefault(x => x.Id == 0) ?? Database.Bottom.FirstOrDefault(); //Rebuild all parts against the new skeleton. SetHair(CurrentHair); @@ -291,9 +313,15 @@ void ApplyVariationAnim(PartModel part, GearEntry entry) { if (part?.Bfres == null || entry == null) return; - var varAnim = part.Bfres.MaterialAnimations.FirstOrDefault(x => x.Name == part.ModelName); + var varAnim = part.Bfres.MaterialAnimations.FirstOrDefault(x => + x.Name == part.ModelName + ); if (varAnim != null) - ScopedAnimPlayer.ApplyMaterialAnim(varAnim, entry.Variation, new[] { part.ModelAsset }); + ScopedAnimPlayer.ApplyMaterialAnim( + varAnim, + entry.Variation, + new[] { part.ModelAsset } + ); } #endregion @@ -312,16 +340,25 @@ public void SetHair(GearEntry entry) CurrentHair = entry; DestroyPart(PartKind.Hair); _hairPhysics.Clear(); - if (entry == null) { OnPartsChanged?.Invoke(); return; } + if (entry == null) + { + OnPartsChanged?.Invoke(); + return; + } string modelName = entry.IsCustom ? entry.RowId : entry.RowId + GenderSuffix; var part = entry.IsCustom ? CreateCustomPart(PartKind.Hair, entry) : CreatePart(PartKind.Hair, modelName); - if (part == null) { OnPartsChanged?.Invoke(); return; } + if (part == null) + { + OnPartsChanged?.Invoke(); + return; + } part.ResolveWelds(Human.Skeleton, HeadBoneMap); - part.AttachBone = part.Skeleton.SearchBone("Head_Root") ?? part.Skeleton.SearchBone("Root"); + part.AttachBone = + part.Skeleton.SearchBone("Head_Root") ?? part.Skeleton.SearchBone("Root"); Parts[PartKind.Hair] = part; LoadHairPhysics(entry, part); @@ -355,7 +392,9 @@ void LoadHairPhysics(GearEntry entry, PartModel part) if (sim != null) _hairPhysics.Add(sim); } - Console.WriteLine($"[Scene] Hair cloth: {_hairPhysics.Count} piece(s) for {entry.RowId}"); + Console.WriteLine( + $"[Scene] Hair cloth: {_hairPhysics.Count} piece(s) for {entry.RowId}" + ); } catch (Exception ex) { @@ -367,13 +406,21 @@ public void SetEyebrow(GearEntry entry) { CurrentEyebrow = entry; DestroyPart(PartKind.Eyebrow); - if (entry == null) { OnPartsChanged?.Invoke(); return; } + if (entry == null) + { + OnPartsChanged?.Invoke(); + return; + } string modelName = entry.IsCustom ? entry.RowId : entry.RowId + GenderSuffix; var part = entry.IsCustom ? CreateCustomPart(PartKind.Eyebrow, entry) : CreatePart(PartKind.Eyebrow, modelName); - if (part == null) { OnPartsChanged?.Invoke(); return; } + if (part == null) + { + OnPartsChanged?.Invoke(); + return; + } part.ResolveWelds(Human.Skeleton, HeadBoneMap); part.AttachBone = part.Skeleton.SearchBone("Head_Root"); @@ -386,15 +433,31 @@ public void SetGear(GearSlot slot, GearEntry entry) { switch (slot) { - case GearSlot.Head: SetHead(entry); break; - case GearSlot.Clothes: SetClothes(entry); break; - case GearSlot.Shoes: SetShoes(entry); break; - case GearSlot.Hair: SetHair(entry); break; - case GearSlot.Eyebrow: SetEyebrow(entry); break; - case GearSlot.Bottom: SetBottom(entry); break; - case GearSlot.Tank: SetTank(entry); break; + case GearSlot.Head: + SetHead(entry); + break; + case GearSlot.Clothes: + SetClothes(entry); + break; + case GearSlot.Shoes: + SetShoes(entry); + break; + case GearSlot.Hair: + SetHair(entry); + break; + case GearSlot.Eyebrow: + SetEyebrow(entry); + break; + case GearSlot.Bottom: + SetBottom(entry); + break; + case GearSlot.Tank: + SetTank(entry); + break; case GearSlot.MainWeapon: - case GearSlot.SpecialWeapon: SetWeapon(entry); break; + case GearSlot.SpecialWeapon: + SetWeapon(entry); + break; } } @@ -410,7 +473,13 @@ void SetHead(GearEntry entry) DestroyPart(PartKind.Head); //ApplyHeadgearParams also on removal: it resets hair arrange / ear hiding //back to defaults (otherwise no-head keeps the previous cap's arrange). - if (entry == null) { ApplyHeadgearParams(); UpdateAlphaMask(); OnPartsChanged?.Invoke(); return; } + if (entry == null) + { + ApplyHeadgearParams(); + UpdateAlphaMask(); + OnPartsChanged?.Invoke(); + return; + } PartModel part; if (entry.IsCustom) @@ -418,10 +487,20 @@ void SetHead(GearEntry entry) else { string modelName = ResolveGearModelForEntry(entry); - if (modelName == null) { Console.WriteLine($"[Scene] No model for {entry.RowId}"); ApplyHeadgearParams(); return; } + if (modelName == null) + { + Console.WriteLine($"[Scene] No model for {entry.RowId}"); + ApplyHeadgearParams(); + return; + } part = CreatePart(PartKind.Head, modelName); } - if (part == null) { ApplyHeadgearParams(); OnPartsChanged?.Invoke(); return; } + if (part == null) + { + ApplyHeadgearParams(); + OnPartsChanged?.Invoke(); + return; + } ApplyVariationAnim(part, entry); //Some headgear name their root after the model (Hed_EYE002) instead of @@ -431,7 +510,8 @@ void SetHead(GearEntry entry) if (rootBone != null && !headMap.ContainsKey(rootBone.Name)) headMap[rootBone.Name] = "Head"; part.ResolveWelds(Human.Skeleton, headMap, uprightWeld: true); - part.AttachBone = part.Skeleton.SearchBone("Root") + part.AttachBone = + part.Skeleton.SearchBone("Root") ?? part.Skeleton.SearchBone("Root_Model") ?? part.Skeleton.SearchBone("Head_Root") ?? rootBone; @@ -469,7 +549,12 @@ void SetShoes(GearEntry entry) CurrentShoes = entry; DestroyPart(PartKind.ShoeLeft); DestroyPart(PartKind.ShoeRight); - if (entry == null) { UpdateAlphaMask(); OnPartsChanged?.Invoke(); return; } + if (entry == null) + { + UpdateAlphaMask(); + OnPartsChanged?.Invoke(); + return; + } string modelName = entry.IsCustom ? null : ResolveGearModelForEntry(entry); @@ -529,7 +614,9 @@ void SetTank(GearEntry entry) { //TankInfo rows point at their actor via SpecActor (PlayerTank_Jetpack //-> ModelInfo -> Tnk_JetPack.bfres). - string actor = !string.IsNullOrEmpty(entry.ActorName) ? entry.ActorName : entry.RowId; + string actor = !string.IsNullOrEmpty(entry.ActorName) + ? entry.ActorName + : entry.RowId; PartModel part = entry.IsCustom ? CreateCustomPart(PartKind.Tank, entry) : CreatePart(PartKind.Tank, Database.ResolveModelName(actor)); @@ -548,9 +635,14 @@ void SetWeapon(GearEntry entry) CurrentWeapon = entry; DestroyPart(PartKind.WeaponMain); DestroyPart(PartKind.WeaponLeft); - if (entry == null) { OnPartsChanged?.Invoke(); return; } + if (entry == null) + { + OnPartsChanged?.Invoke(); + return; + } - (string file, string model) mainModel, leftModel; + (string file, string model) mainModel, + leftModel; if (entry.IsCustom) { mainModel = (entry.RowId, null); @@ -559,8 +651,9 @@ void SetWeapon(GearEntry entry) else (mainModel, leftModel) = Database.ResolveWeaponModels(entry); - bool isStringer = !entry.IsCustom && - (entry.ActorName.Contains("Stringer") || entry.RowId.Contains("_Strn_")); + bool isStringer = + !entry.IsCustom + && (entry.ActorName.Contains("Stringer") || entry.RowId.Contains("_Strn_")); string mainHandBone = isStringer ? "Weapon_L" : "Weapon_R"; if (mainModel.file != null) @@ -570,7 +663,11 @@ void SetWeapon(GearEntry entry) : CreatePart(PartKind.WeaponMain, mainModel.file, subModel: mainModel.model); if (part != null) { - part.ResolveWelds(Human.Skeleton, new Dictionary { { "Root", mainHandBone } }, mapOnly: true); + part.ResolveWelds( + Human.Skeleton, + new Dictionary { { "Root", mainHandBone } }, + mapOnly: true + ); part.AttachBone = part.Skeleton.SearchBone("Root"); ApplyWeaponCarryPose(part); Parts[PartKind.WeaponMain] = part; @@ -578,14 +675,22 @@ void SetWeapon(GearEntry entry) } if (leftModel.file != null) { - var part = CreatePart(PartKind.WeaponLeft, leftModel.file, subModel: leftModel.model); + var part = CreatePart( + PartKind.WeaponLeft, + leftModel.file, + subModel: leftModel.model + ); if (part != null) { - part.ResolveWelds(Human.Skeleton, new Dictionary { { "Root", "Weapon_L" } }, mapOnly: true); + part.ResolveWelds( + Human.Skeleton, + new Dictionary { { "Root", "Weapon_L" } }, + mapOnly: true + ); part.AttachBone = part.Skeleton.SearchBone("Root"); - bool isMirrorCopy = leftModel.file == mainModel.file && - leftModel.model == mainModel.model; + bool isMirrorCopy = + leftModel.file == mainModel.file && leftModel.model == mainModel.model; if (isMirrorCopy) { //Same model reused for both hands (dualies without a _L mesh). @@ -626,20 +731,23 @@ void ApplyWeaponCarryPose(PartModel part) mesh.Shape.IsVisible = false; } var closeBone = part.Skeleton.SearchBone("Umbrella_Close"); - if (closeBone != null) closeBone.Visible = true; + if (closeBone != null) + closeBone.Visible = true; var openBone = part.Skeleton.SearchBone("Umbrella_Open"); - if (openBone != null) openBone.Visible = false; + if (openBone != null) + openBone.Visible = false; return; } - var anim = part.Bfres.SkeletalAnimations.FirstOrDefault(a => a.Name == "CloseOff") + var anim = + part.Bfres.SkeletalAnimations.FirstOrDefault(a => a.Name == "CloseOff") ?? part.Bfres.SkeletalAnimations.FirstOrDefault(a => a.Name == "Close"); if (anim == null) return; anim.SkeletonOverride = part.Skeleton; anim.SetFrame(anim.FrameCount); - anim.NextFrame(); //writes the pose + runs skeleton.Update() + anim.NextFrame(); //writes the pose + runs skeleton.Update() // The animation's per-bone SSC flags are the ground truth for // whether parent scale should propagate. STSkeleton.GetWorldMatrix @@ -662,17 +770,21 @@ void ApplyWeaponCarryPose(PartModel part) var ctrl = bone.AnimationController; bool inAnim = anim.AnimGroups.Any(g => g.Name == bone.Name); - bool ssc = inAnim ? animSsc.Contains(bone.Name) - : bone.UseSegmentScaleCompensate; + bool ssc = inAnim ? animSsc.Contains(bone.Name) : bone.UseSegmentScaleCompensate; - Vector3 parentAccum = (bone.Parent != null && - accumScale.TryGetValue(bone.Parent.Name, out var pa)) ? pa : Vector3.One; + Vector3 parentAccum = + (bone.Parent != null && accumScale.TryGetValue(bone.Parent.Name, out var pa)) + ? pa + : Vector3.One; Vector3 ownScale = ctrl.Scale; - Vector3 meshScale = ssc ? ownScale - : new Vector3(ownScale.X * parentAccum.X, - ownScale.Y * parentAccum.Y, - ownScale.Z * parentAccum.Z); + Vector3 meshScale = ssc + ? ownScale + : new Vector3( + ownScale.X * parentAccum.X, + ownScale.Y * parentAccum.Y, + ownScale.Z * parentAccum.Z + ); accumScale[bone.Name] = meshScale; part.PoseOverride[bone.Name] = new PartModel.PoseSrt @@ -691,8 +803,11 @@ PartModel CreateCustomPart(PartKind kind, GearEntry entry) var raw = File.ReadAllBytes(entry.CustomPath); raw = Romfs.Decompress(raw); //Fake path inside romfs Model so shader lookup works. - string fakePath = Path.Combine(Romfs.Root, "Model", - Path.GetFileNameWithoutExtension(entry.CustomPath.Replace(".zs", "")) + ".bfres"); + string fakePath = Path.Combine( + Romfs.Root, + "Model", + Path.GetFileNameWithoutExtension(entry.CustomPath.Replace(".zs", "")) + ".bfres" + ); var bfres = LoadModelData(raw, fakePath); return bfres == null ? null : CreatePart(kind, entry.RowId, bfres); } @@ -740,7 +855,8 @@ void ApplyHeadgearParams() if (paramData == null) { string file = pack.FindFile(x => x.Contains("GearHeadParamSet")); - if (file != null) paramData = pack.GetFile(file); + if (file != null) + paramData = pack.GetFile(file); } if (paramData == null) return; @@ -764,7 +880,10 @@ void ApplyHeadgearParams() srt = Byml.AsHash(manualBind.GetValueOrDefault($"V{variation}_{hairKey}")); var variationSrt = Byml.AsHash(root.GetValueOrDefault("VariationSRT")); - var varEntry = variationSrt != null ? Byml.AsHash(variationSrt.GetValueOrDefault($"V{variation}")) : null; + var varEntry = + variationSrt != null + ? Byml.AsHash(variationSrt.GetValueOrDefault($"V{variation}")) + : null; //V0 entries usually only carry HideEar, but some gear (Hed_AMB020) puts a //real bind SRT there too; ReadSrt returns identity when no SRT keys exist. @@ -783,8 +902,12 @@ void ApplyHeadgearParams() if (hair != null && hairKey != null) { var hairArrange = Byml.AsHash(root.GetValueOrDefault("HairArrange")); - var varSet = hairArrange != null ? Byml.AsHash(hairArrange.GetValueOrDefault($"V{variation}")) : null; - var presetMap = varSet != null ? Byml.AsHash(varSet.GetValueOrDefault("PresetMap")) : null; + var varSet = + hairArrange != null + ? Byml.AsHash(hairArrange.GetValueOrDefault($"V{variation}")) + : null; + var presetMap = + varSet != null ? Byml.AsHash(varSet.GetValueOrDefault("PresetMap")) : null; string arrangePath = presetMap != null ? Byml.GetString(presetMap, hairKey) : null; if (!string.IsNullOrEmpty(arrangePath)) @@ -802,7 +925,14 @@ void ApplyHeadgearParams() //Afro-style hairs (Har_OCT003) contain one mesh shell per headgear family, //each hanging off a marker bone; the arrange preset's AfroType picks the //visible shell. Default (no headgear / no preset) = Base. - static readonly string[] AfroShells = { "Base", "Cap", "Fullface", "Headband", "HeadphoneA" }; + static readonly string[] AfroShells = + { + "Base", + "Cap", + "Fullface", + "Headband", + "HeadphoneA", + }; static void ApplyAfroShell(PartModel hair, string afroType) { @@ -825,18 +955,24 @@ string GetHairKey() if (CurrentHair == null || CurrentHair.IsCustom) return null; string id = CurrentHair.RowId; - if (id.StartsWith("Har_")) id = id.Substring(4); + if (id.StartsWith("Har_")) + id = id.Substring(4); int sd = id.IndexOf("_SdodrCstm", StringComparison.Ordinal); - if (sd > 0) id = id.Substring(0, sd); + if (sd > 0) + id = id.Substring(0, sd); return id; } - static Dictionary ParseHairArrange(byte[] data, out string afroType) + static Dictionary ParseHairArrange( + byte[] data, + out string afroType + ) { afroType = null; var result = new Dictionary(); var root = Byml.AsHash(new Byml(data).Root); - if (root == null) return result; + if (root == null) + return result; afroType = Byml.GetString(root, "AfroType"); if (root.GetValueOrDefault("BoneParamArray") is List bones) @@ -863,7 +999,8 @@ static Dictionary ParseHairArrange(byte[] data, out st static Vector3 ReadVec3(Dictionary hash, string key, Vector3 def) { var v = Byml.AsHash(hash.GetValueOrDefault(key)); - if (v == null) return def; + if (v == null) + return def; return new Vector3(Byml.GetFloat(v, "X"), Byml.GetFloat(v, "Y"), Byml.GetFloat(v, "Z")); } @@ -871,21 +1008,40 @@ static Matrix4 ReadSrt(Dictionary srt) { if (srt == null) return Matrix4.Identity; - Vector3 scale = Vector3.One, rotate = Vector3.Zero, translate = Vector3.Zero; + Vector3 scale = Vector3.One, + rotate = Vector3.Zero, + translate = Vector3.Zero; var s = Byml.AsHash(srt.GetValueOrDefault("Scale")); - if (s != null) scale = new Vector3(Byml.GetFloat(s, "X", 1), Byml.GetFloat(s, "Y", 1), Byml.GetFloat(s, "Z", 1)); + if (s != null) + scale = new Vector3( + Byml.GetFloat(s, "X", 1), + Byml.GetFloat(s, "Y", 1), + Byml.GetFloat(s, "Z", 1) + ); //ManualBindSRT uses "Rotate", VariationSRT uses "Rotation". - var r = Byml.AsHash(srt.GetValueOrDefault("Rotate")) ?? Byml.AsHash(srt.GetValueOrDefault("Rotation")); - if (r != null) rotate = new Vector3(Byml.GetFloat(r, "X"), Byml.GetFloat(r, "Y"), Byml.GetFloat(r, "Z")); + var r = + Byml.AsHash(srt.GetValueOrDefault("Rotate")) + ?? Byml.AsHash(srt.GetValueOrDefault("Rotation")); + if (r != null) + rotate = new Vector3( + Byml.GetFloat(r, "X"), + Byml.GetFloat(r, "Y"), + Byml.GetFloat(r, "Z") + ); var t = Byml.AsHash(srt.GetValueOrDefault("Translate")); - if (t != null) translate = new Vector3(Byml.GetFloat(t, "X"), Byml.GetFloat(t, "Y"), Byml.GetFloat(t, "Z")); + if (t != null) + translate = new Vector3( + Byml.GetFloat(t, "X"), + Byml.GetFloat(t, "Y"), + Byml.GetFloat(t, "Z") + ); //Euler XYZ, applied X first (row-vector convention). - return Matrix4.CreateScale(scale) * - Matrix4.CreateRotationX(MathHelper.DegreesToRadians(rotate.X)) * - Matrix4.CreateRotationY(MathHelper.DegreesToRadians(rotate.Y)) * - Matrix4.CreateRotationZ(MathHelper.DegreesToRadians(rotate.Z)) * - Matrix4.CreateTranslation(translate); + return Matrix4.CreateScale(scale) + * Matrix4.CreateRotationX(MathHelper.DegreesToRadians(rotate.X)) + * Matrix4.CreateRotationY(MathHelper.DegreesToRadians(rotate.Y)) + * Matrix4.CreateRotationZ(MathHelper.DegreesToRadians(rotate.Z)) + * Matrix4.CreateTranslation(translate); } #endregion @@ -903,10 +1059,14 @@ void UpdateTankHarness() string type = "S"; bool hide = false; - if (CurrentClothes != null && Database.ClothesRows.TryGetValue(CurrentClothes.RowId, out var row)) + if ( + CurrentClothes != null + && Database.ClothesRows.TryGetValue(CurrentClothes.RowId, out var row) + ) { type = Byml.GetString(row, "HarnessType"); - if (string.IsNullOrEmpty(type)) type = "S"; + if (string.IsNullOrEmpty(type)) + type = "S"; hide = Byml.GetBool(row, "IsHideHarness"); } @@ -928,7 +1088,10 @@ void UpdateTankHarness() /// Mask texture name contributed by a gear entry: AlphaMaskV1 for variation /// rows when present, else the per-gender AlphaMaskF/AlphaMaskM. /// - string GetAlphaMaskName(GearEntry entry, Dictionary> rows) + string GetAlphaMaskName( + GearEntry entry, + Dictionary> rows + ) { if (entry == null || entry.IsCustom || !rows.TryGetValue(entry.RowId, out var row)) return null; @@ -972,10 +1135,11 @@ void UpdateAlphaMask() }; _bodyMaskActive = _alphaMask.Compose(Human, names.Where(x => x != null)); - _bodyMaskMaterials = Human.ModelAsset.Meshes - .Select(m => (FMAT)m.Material) + _bodyMaskMaterials = Human + .ModelAsset.Meshes.Select(m => (FMAT)m.Material) .Where(m => m.Name == "M_Body") - .Distinct().ToList(); + .Distinct() + .ToList(); ApplyBodyMaskSampler(); //New parts start with the authored placeholder skin color. ApplyGearSkinColor(); @@ -1030,7 +1194,8 @@ void ApplyGearSkinColor() { if (Human == null) return; - var body = Human.ModelAsset.Meshes.Select(m => (FMAT)m.Material) + var body = Human + .ModelAsset.Meshes.Select(m => (FMAT)m.Material) .FirstOrDefault(m => m.Name == "M_Body"); if (body == null) return; @@ -1078,11 +1243,18 @@ BfresMaterialAnim FindMaterialAnim(string name) public void ApplyTeamColor(TeamColorSet set, int team) { - if (set == null) return; - var color = team == 0 ? set.Alpha : team == 1 ? set.Bravo : set.Charlie; + if (set == null) + return; + var color = + team == 0 ? set.Alpha + : team == 1 ? set.Bravo + : set.Charlie; //The shader wants linear color (flexlion applies pow 2.2). var linear = new System.Numerics.Vector3( - MathF.Pow(color.X, 2.2f), MathF.Pow(color.Y, 2.2f), MathF.Pow(color.Z, 2.2f)); + MathF.Pow(color.X, 2.2f), + MathF.Pow(color.Y, 2.2f), + MathF.Pow(color.Z, 2.2f) + ); //Force the dumped uniform buffers in first; the lazy load on first draw //resets TeamAlphaColor to the dump's value and would clobber ours. HoianNXRender.LoadResourceData(); @@ -1093,7 +1265,11 @@ public void ApplyTeamColor(TeamColorSet set, int team) #region Animation - public void PlayAnim(string name) + public void PlayAnim(string name) => PlayAnim(name, resetHair: true); + + //resetHair=false keeps the cloth sim running across the switch, so an animation chain + //plays as one continuous take instead of snapping hair back to rest each step. + public void PlayAnim(string name, bool resetHair) { CurrentAnimName = name; AnimFrame = 0; @@ -1123,12 +1299,15 @@ public void PlayAnim(string name) ApplySkinTone(SkinTone); } - //Pose jumps on anim switch; restart the cloth from the new pose. - ResetHairPhysics(); + //Pose jumps on anim switch; restart the cloth from the new pose (unless a chain is + //driving continuous playback). + if (resetHair) + ResetHairPhysics(); } //Mouth01..04 etc default hidden; captured at load time from the bfres bone data. readonly Dictionary _defaultBoneVisibility = new(); + bool IsBoneDefaultVisible(STBone bone) { if (_defaultBoneVisibility.TryGetValue(bone, out bool visible)) @@ -1160,14 +1339,22 @@ public void Update(float deltaSeconds) if (CurrentSkeletal != null) { CurrentSkeletal.SetFrame(AnimFrame); - CurrentSkeletal.NextFrame(); //updates Human.Skeleton (SkeletonOverride) + CurrentSkeletal.NextFrame(); //updates Human.Skeleton (SkeletonOverride) } if (CurrentTexPattern != null) - ScopedAnimPlayer.ApplyMaterialAnim(CurrentTexPattern, AnimFrame, new[] { Human.ModelAsset }); + ScopedAnimPlayer.ApplyMaterialAnim( + CurrentTexPattern, + AnimFrame, + new[] { Human.ModelAsset } + ); //Shader param anims scale the eyeball down behind closed eyelids etc. if (CurrentShaderParam != null) - ScopedAnimPlayer.ApplyMaterialAnim(CurrentShaderParam, AnimFrame, new[] { Human.ModelAsset }); + ScopedAnimPlayer.ApplyMaterialAnim( + CurrentShaderParam, + AnimFrame, + new[] { Human.ModelAsset } + ); if (CurrentBoneVis != null) ScopedAnimPlayer.ApplyBoneVisAnim(CurrentBoneVis, AnimFrame, Human.Skeleton); @@ -1215,14 +1402,23 @@ public void DumpHairPhysics() /// void ApplyEarHide() { - bool hideL, hideR; + bool hideL, + hideR; switch (EarHideMode) { - case "HideAll": hideL = hideR = true; break; - case "HideLeft": hideL = true; hideR = false; break; + case "HideAll": + hideL = hideR = true; + break; + case "HideLeft": + hideL = true; + hideR = false; + break; //"Long" ears = inkling; octoling ears stay visible. - case "HideLong": hideL = hideR = PlayerType <= 1; break; - default: return; + case "HideLong": + hideL = hideR = PlayerType <= 1; + break; + default: + return; } var squash = Matrix4.CreateScale(0.0001f); @@ -1237,6 +1433,13 @@ public void SetAnimFrame(float frame) AnimFrame = frame; } + /// Frame count of a skeletal animation by name (0 if unknown), for the chain timeline. + public int SkeletalFrameCount(string name) + { + var anim = name != null ? Anims.GetSkeletal(name) : null; + return anim != null ? Math.Max((int)Math.Round((float)anim.FrameCount), 1) : 0; + } + #endregion public IEnumerable AllRenders() @@ -1262,10 +1465,14 @@ public void Draw(GLContext control, Pass pass) if (!part.Visible) continue; if (part.Mirror) - OpenTK.Graphics.OpenGL.GL.FrontFace(OpenTK.Graphics.OpenGL.FrontFaceDirection.Cw); + OpenTK.Graphics.OpenGL.GL.FrontFace( + OpenTK.Graphics.OpenGL.FrontFaceDirection.Cw + ); part.Render.DrawModel(control, pass, Vector4.Zero); if (part.Mirror) - OpenTK.Graphics.OpenGL.GL.FrontFace(OpenTK.Graphics.OpenGL.FrontFaceDirection.Ccw); + OpenTK.Graphics.OpenGL.GL.FrontFace( + OpenTK.Graphics.OpenGL.FrontFaceDirection.Ccw + ); } } diff --git a/PlayerViewer/Player/ScopedAnimPlayer.cs b/PlayerViewer/Player/ScopedAnimPlayer.cs index 155ccff..f598c1b 100644 --- a/PlayerViewer/Player/ScopedAnimPlayer.cs +++ b/PlayerViewer/Player/ScopedAnimPlayer.cs @@ -19,7 +19,11 @@ public static class ScopedAnimPlayer /// Applies a material animation (texture pattern and/or shader params) at the /// given frame, but only to materials of the provided models. /// - public static void ApplyMaterialAnim(BfresMaterialAnim anim, float frame, IEnumerable models) + public static void ApplyMaterialAnim( + BfresMaterialAnim anim, + float frame, + IEnumerable models + ) { anim.SetFrame(frame); foreach (BfresMaterialAnim.MaterialAnimGroup group in anim.AnimGroups) @@ -36,7 +40,12 @@ public static void ApplyMaterialAnim(BfresMaterialAnim anim, float frame, IEnume } } - static void ApplyGroup(BfresMaterialAnim anim, STAnimGroup group, FMAT material, float frame) + static void ApplyGroup( + BfresMaterialAnim anim, + STAnimGroup group, + FMAT material, + float frame + ) { foreach (var track in group.GetTracks()) { @@ -50,7 +59,12 @@ static void ApplyGroup(BfresMaterialAnim anim, STAnimGroup group, FMAT material, ApplyGroup(anim, subGroup, material, frame); } - static void ApplySamplerTrack(BfresMaterialAnim anim, FMAT material, BfresMaterialAnim.SamplerTrack track, float frame) + static void ApplySamplerTrack( + BfresMaterialAnim anim, + FMAT material, + BfresMaterialAnim.SamplerTrack track, + float frame + ) { if (anim.TextureList.Count == 0) return; @@ -63,7 +77,12 @@ static void ApplySamplerTrack(BfresMaterialAnim anim, FMAT material, BfresMateri material.AnimatedSamplers.Add(track.Sampler, anim.TextureList[value]); } - static void ApplyParamTrack(FMAT material, STAnimGroup group, BfresMaterialAnim.ParamTrack track, float frame) + static void ApplyParamTrack( + FMAT material, + STAnimGroup group, + BfresMaterialAnim.ParamTrack track, + float frame + ) { if (!material.ShaderParams.ContainsKey(group.Name)) return; @@ -89,26 +108,38 @@ static void ApplyParamTrack(FMAT material, STAnimGroup group, BfresMaterialAnim. var param = material.AnimatedParams[group.Name]; switch (targetParam.Type) { - case ShaderParamType.Float: param.DataValue = value; break; + case ShaderParamType.Float: + param.DataValue = value; + break; case ShaderParamType.Float2: case ShaderParamType.Float3: case ShaderParamType.Float4: var arr = (float[])param.DataValue; - if (index < arr.Length) arr[index] = value; + if (index < arr.Length) + arr[index] = value; + break; + case ShaderParamType.Int: + param.DataValue = (int)value; break; - case ShaderParamType.Int: param.DataValue = (int)value; break; case ShaderParamType.TexSrt: case ShaderParamType.TexSrtEx: { var texSrt = (TexSrt)param.DataValue; - var scaleX = texSrt.Scaling.X; var scaleY = texSrt.Scaling.Y; + var scaleX = texSrt.Scaling.X; + var scaleY = texSrt.Scaling.Y; var rotate = texSrt.Rotation; - var transX = texSrt.Translation.X; var transY = texSrt.Translation.Y; - if (track.ValueOffset == 4) scaleX = value; - if (track.ValueOffset == 8) scaleY = value; - if (track.ValueOffset == 12) rotate = value; - if (track.ValueOffset == 16) transX = value; - if (track.ValueOffset == 20) transY = value; + var transX = texSrt.Translation.X; + var transY = texSrt.Translation.Y; + if (track.ValueOffset == 4) + scaleX = value; + if (track.ValueOffset == 8) + scaleY = value; + if (track.ValueOffset == 12) + rotate = value; + if (track.ValueOffset == 16) + transX = value; + if (track.ValueOffset == 20) + transY = value; param.DataValue = new TexSrt() { Mode = texSrt.Mode, @@ -126,8 +157,12 @@ static void ApplyParamTrack(FMAT material, STAnimGroup group, BfresMaterialAnim. /// Also applies to shape (FSHP) visibility when a track name matches a shape name /// but not a bone name (Splatoon 3 uses both patterns). /// - public static void ApplyBoneVisAnim(BfresVisibilityAnim anim, float frame, - STSkeleton skeleton, IEnumerable models = null) + public static void ApplyBoneVisAnim( + BfresVisibilityAnim anim, + float frame, + STSkeleton skeleton, + IEnumerable models = null + ) { anim.SetFrame(frame); foreach (BfresVisibilityAnim.BoneAnimGroup group in anim.AnimGroups) @@ -139,11 +174,12 @@ public static void ApplyBoneVisAnim(BfresVisibilityAnim anim, float frame, bone.Visible = val; continue; } - if (models == null) continue; + if (models == null) + continue; foreach (var model in models) - foreach (var mesh in model.Meshes) - if (mesh.Name == group.Name) - mesh.Shape.IsVisible = val; + foreach (var mesh in model.Meshes) + if (mesh.Name == group.Name) + mesh.Shape.IsVisible = val; } } diff --git a/PlayerViewer/Player/StandaloneScene.cs b/PlayerViewer/Player/StandaloneScene.cs index 683ac75..0d8b9db 100644 --- a/PlayerViewer/Player/StandaloneScene.cs +++ b/PlayerViewer/Player/StandaloneScene.cs @@ -33,7 +33,7 @@ public class StandaloneScene : IDisposable, IViewScene public List AnimNames { get; } = new(); - public string CurrentAnimName { get; private set; } + public string CurrentAnimName { get; private set; } public BfresSkeletalAnim CurrentSkeletal { get; private set; } public BfresMaterialAnim CurrentTexPattern { get; private set; } public BfresVisibilityAnim CurrentBoneVis { get; private set; } @@ -52,8 +52,10 @@ public static StandaloneScene FromFile(string filePath, Romfs romfs) raw = Romfs.Decompress(raw); string stem = Path.GetFileName(filePath); - if (stem.EndsWith(".zs")) stem = stem[..^3]; - if (stem.EndsWith(".bfres")) stem = stem[..^6]; + if (stem.EndsWith(".zs")) + stem = stem[..^3]; + if (stem.EndsWith(".bfres")) + stem = stem[..^6]; //Fake path inside romfs Model so the shader archive lookup works. string fakePath = Path.Combine(romfs.Root, "Model", stem + ".bfres"); @@ -67,10 +69,22 @@ public static StandaloneScene FromRomfs(string modelName, Romfs romfs) if (data == null) return null; string fakePath = Path.Combine(romfs.Root, "Model", modelName + ".bfres"); - return Load(data, fakePath, modelName, romfs.Resolve($"Model/{modelName}.bfres") ?? "", romfs); + return Load( + data, + fakePath, + modelName, + romfs.Resolve($"Model/{modelName}.bfres") ?? "", + romfs + ); } - static StandaloneScene Load(byte[] data, string fakePath, string name, string sourcePath, Romfs romfs) + static StandaloneScene Load( + byte[] data, + string fakePath, + string name, + string sourcePath, + Romfs romfs + ) { IFileFormat format; using (var ms = new MemoryStream(data)) @@ -103,7 +117,8 @@ static StandaloneScene Load(byte[] data, string fakePath, string name, string so foreach (var bone in model.ModelData.Skeleton.Bones) scene._defaultBoneVisibility[bone] = bone.Visible; foreach (var mesh in model.Meshes) - scene._defaultShapeVisibility[model.ModelData.Name + "/" + mesh.Name] = mesh.Shape.IsVisible; + scene._defaultShapeVisibility[model.ModelData.Name + "/" + mesh.Name] = + mesh.Shape.IsVisible; } return scene; @@ -130,7 +145,12 @@ public void PlayAnim(string name) if (_defaultBoneVisibility.TryGetValue(bone, out bool visible)) bone.Visible = visible; foreach (var mesh in model.Meshes) - if (_defaultShapeVisibility.TryGetValue(model.ModelData.Name + "/" + mesh.Name, out bool vis)) + if ( + _defaultShapeVisibility.TryGetValue( + model.ModelData.Name + "/" + mesh.Name, + out bool vis + ) + ) mesh.Shape.IsVisible = vis; model.ModelData.Skeleton.Reset(); } @@ -138,12 +158,14 @@ public void PlayAnim(string name) CurrentAnimName = name; AnimFrame = 0; - CurrentSkeletal = name != null - ? Bfres.SkeletalAnimations.FirstOrDefault(x => x.Name == name) : null; - CurrentTexPattern = name != null - ? Bfres.MaterialAnimations.FirstOrDefault(x => x.Name == name) : null; - CurrentBoneVis = name != null - ? Bfres.VisibilityAnimations.FirstOrDefault(x => x.Name == name) : null; + CurrentSkeletal = + name != null ? Bfres.SkeletalAnimations.FirstOrDefault(x => x.Name == name) : null; + CurrentTexPattern = + name != null ? Bfres.MaterialAnimations.FirstOrDefault(x => x.Name == name) : null; + CurrentBoneVis = + name != null + ? Bfres.VisibilityAnimations.FirstOrDefault(x => x.Name == name) + : null; //Animate ALL skeletons in the file (multi-model), but scope to only //this render so the hidden player skeleton is not affected. @@ -151,12 +173,21 @@ public void PlayAnim(string name) { CurrentSkeletal.SkeletonOverride = null; CurrentSkeletal.SkeletonOverrides = models - .Select(m => m.ModelData.Skeleton).ToList(); + .Select(m => m.ModelData.Skeleton) + .ToList(); } } public void SetAnimFrame(float frame) => AnimFrame = frame; + /// Frame count of a skeletal animation by name (0 if unknown), for the chain timeline. + public int SkeletalFrameCount(string name) + { + var anim = + name != null ? Bfres.SkeletalAnimations.FirstOrDefault(x => x.Name == name) : null; + return anim != null ? Math.Max((int)Math.Round((float)anim.FrameCount), 1) : 0; + } + public void Update(float deltaSeconds) { if (CurrentSkeletal == null) @@ -180,8 +211,12 @@ public void Update(float deltaSeconds) ScopedAnimPlayer.ApplyMaterialAnim(CurrentTexPattern, AnimFrame, models); if (CurrentBoneVis != null) foreach (var model in models) - ScopedAnimPlayer.ApplyBoneVisAnim(CurrentBoneVis, AnimFrame, - model.ModelData.Skeleton, models); + ScopedAnimPlayer.ApplyBoneVisAnim( + CurrentBoneVis, + AnimFrame, + model.ModelData.Skeleton, + models + ); } public IEnumerable AllRenders() diff --git a/PlayerViewer/PlayerViewer.csproj b/PlayerViewer/PlayerViewer.csproj index 8175104..df42f63 100644 --- a/PlayerViewer/PlayerViewer.csproj +++ b/PlayerViewer/PlayerViewer.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net10.0 true AnyCPU;x64 PlayerViewer @@ -10,6 +10,17 @@ ..\Cafe-Shader-Studio + + + + + + + + + $(CssRoot)\BfresEditor\Lib\BfresLibrary.dll @@ -17,12 +28,7 @@ $(CssRoot)\CafeShaderStudio\Lib\ImGui.NET.dll - - $(CssRoot)\CafeShaderStudio\Lib\OpenTK.dll - - - $(CssRoot)\CafeShaderStudio\Lib\Syroot.BinaryData.dll - + $(CssRoot)\CafeShaderStudio\Lib\Syroot.Maths.dll @@ -32,13 +38,19 @@ $(CssRoot)\CafeShaderStudio\Lib\Toolbox.Core.dll - - $(CssRoot)\CafeShaderStudio\Lib\ZstdNet.dll - + + + + + + + + + @@ -50,18 +62,127 @@ - - + + - - - - + + + + + + + + + + - + + + + + + + + + + + + + + + + + + diff --git a/PlayerViewer/UI/BufferedAnimExporter.cs b/PlayerViewer/UI/BufferedAnimExporter.cs new file mode 100644 index 0000000..3f796b6 --- /dev/null +++ b/PlayerViewer/UI/BufferedAnimExporter.cs @@ -0,0 +1,373 @@ +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO; +using System.Threading; + +namespace PlayerViewer.UI +{ + /// + /// Multiphase animation exporter. Because the crop rectangle (for trim) can't be known + /// until every frame has been seen, frames are buffered to disk rather than streamed + /// straight to ffmpeg. Instead: + /// + /// Pass 1 (capture): each transparent RGBA frame is handed to a worker thread that + /// appends it to a temp file on disk and expands a running content bounding box over + /// non-zero-alpha pixels. Only the bbox is retained in memory; the pixels live on disk. + /// + /// Pass 2 (encode): once capture ends, a second worker computes the final crop rect + /// (bbox + margin, clamped), then streams each frame back from disk, crops it, composites + /// the greenscreen background for MP4 (WebP keeps alpha), and pipes it into ffmpeg. + /// + /// The transparent render doubles as the alpha oracle for MP4, so a single render pass + /// serves all formats. Disk cost is the full raw animation (W*H*4*frames) but transient. + /// + public class BufferedAnimExporter : IDisposable + { + public bool IsCapturing { get; private set; } + public bool IsEncoding { get; private set; } + public int EncodeProgress { get; private set; } + public int EncodeTotal { get; private set; } + public string OutputPath { get; private set; } + public string Error { get; private set; } + + int _width, + _height, + _fps; + string _tempPath; + FileStream _tempWrite; + BlockingCollection _writeQueue; + Thread _writer; + Thread _encoder; + + //Content bounding box in buffer (bottom-up) coordinates; expanded by the writer. + int _minX, + _minY, + _maxX, + _maxY; + bool _trim; + + public bool StartCapture(int width, int height, int fps, string outputPath, bool trim) + { + if (IsCapturing || IsEncoding) + return false; + + _trim = trim; + + _width = width; + _height = height; + _fps = fps; + OutputPath = outputPath; + Error = null; + EncodeProgress = 0; + EncodeTotal = 0; + _minX = width; + _minY = height; + _maxX = -1; + _maxY = -1; + + try + { + string dir = Path.Combine(Path.GetTempPath(), "PlayerViewerExport"); + Directory.CreateDirectory(dir); + _tempPath = Path.Combine(dir, $"anim_{Guid.NewGuid():N}.raw"); + _tempWrite = new FileStream( + _tempPath, + FileMode.Create, + FileAccess.Write, + FileShare.None, + 1 << 20, + FileOptions.SequentialScan + ); + } + catch (Exception ex) + { + Error = ex.Message; + return false; + } + + _writeQueue = new BlockingCollection(8); + _writer = new Thread(WriteLoop) { IsBackground = true, Name = "AnimExportWrite" }; + _writer.Start(); + IsCapturing = true; + return true; + } + + /// Queues one bottom-up transparent RGBA frame (ArrayPool-rented; ownership transfers). + public void PushFrame(byte[] rgba) + { + if (!IsCapturing) + { + ArrayPool.Shared.Return(rgba); + return; + } + try + { + _writeQueue.Add(rgba); + } + catch + { + ArrayPool.Shared.Return(rgba); + } + } + + void WriteLoop() + { + int frameBytes = _width * _height * 4; + try + { + foreach (var buf in _writeQueue.GetConsumingEnumerable()) + { + if (_trim) + ScanBbox(buf); + _tempWrite.Write(buf, 0, frameBytes); + ArrayPool.Shared.Return(buf); + } + } + catch (Exception ex) + { + Error ??= ex.Message; + while (_writeQueue.TryTake(out var leftover)) + ArrayPool.Shared.Return(leftover); + } + } + + //Expands the bounding box to include every pixel with alpha != 0. Bails early once + //the box already spans the whole frame (nothing left to trim). + void ScanBbox(byte[] buf) + { + if (_minX == 0 && _minY == 0 && _maxX == _width - 1 && _maxY == _height - 1) + return; + + int w = _width, + h = _height; + for (int y = 0; y < h; y++) + { + int row = y * w * 4; + for (int x = 0; x < w; x++) + { + if (buf[row + x * 4 + 3] != 0) + { + if (x < _minX) + _minX = x; + if (x > _maxX) + _maxX = x; + if (y < _minY) + _minY = y; + if (y > _maxY) + _maxY = y; + } + } + } + } + + /// + /// Ends capture and kicks off the encode pass on a worker thread. Returns immediately; + /// poll / for progress. + /// + public void FinishCapture( + OutputFormat format, + byte[] background, + int webpQuality, + int marginPx + ) + { + if (!IsCapturing) + return; + IsCapturing = false; + + try + { + _writeQueue.CompleteAdding(); + _writer.Join(15000); + _tempWrite.Flush(); + _tempWrite.Dispose(); + _tempWrite = null; + } + catch (Exception ex) + { + Error ??= ex.Message; + } + + IsEncoding = true; + _encoder = new Thread(() => EncodeLoop(format, background, webpQuality, marginPx)) + { + IsBackground = true, + Name = "AnimExportEncode", + }; + _encoder.Start(); + } + + void EncodeLoop(OutputFormat format, byte[] background, int webpQuality, int marginPx) + { + int frameBytes = _width * _height * 4; + byte[] frame = null; + try + { + long total = new FileInfo(_tempPath).Length / frameBytes; + EncodeTotal = (int)total; + + //Compute crop rect (buffer coords). No trim, or an empty box (nothing drawn), + //keeps the whole frame. + int x0, + y0, + cw, + ch; + if (!_trim || _maxX < 0) + { + x0 = 0; + y0 = 0; + cw = _width; + ch = _height; + } + else + { + x0 = Math.Max(0, _minX - marginPx); + y0 = Math.Max(0, _minY - marginPx); + int x1 = Math.Min(_width - 1, _maxX + marginPx); + int y1 = Math.Min(_height - 1, _maxY + marginPx); + cw = x1 - x0 + 1; + ch = y1 - y0 + 1; + } + //Even dimensions: required by MP4's yuv420p, harmless for WebP. Trimming a + //column/row keeps the crop inside the original frame. + cw &= ~1; + ch &= ~1; + if (cw < 2) + cw = 2; + if (ch < 2) + ch = 2; + + var psi = new ProcessStartInfo + { + FileName = ExportUtil.ResolveFfmpeg(), + Arguments = + ExportUtil.RawInputArgs(cw, ch, _fps) + + ExportUtil.CodecArgs(format, webpQuality, OutputPath), + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + using var proc = Process.Start(psi); + proc.BeginErrorReadLine(); + var stdin = proc.StandardInput.BaseStream; + + //A background buffer (full-frame, same layout as the source) means composite the + //straight-alpha scene over it; null means keep the alpha channel (WebP/WebM). + bool composite = background != null; + + frame = ArrayPool.Shared.Rent(frameBytes); + var outBuf = new byte[cw * ch * 4]; + + using var read = new FileStream( + _tempPath, + FileMode.Open, + FileAccess.Read, + FileShare.None, + 1 << 20, + FileOptions.SequentialScan + ); + + for (long f = 0; f < total; f++) + { + ReadFull(read, frame, frameBytes); + for (int ry = 0; ry < ch; ry++) + { + int srcRow = ((y0 + ry) * _width + x0) * 4; + int dstRow = ry * cw * 4; + if (composite) + { + //Composite straight-alpha source over the background buffer (same + //full-frame layout, so the source index maps directly into it). + for (int rx = 0; rx < cw; rx++) + { + int s = srcRow + rx * 4, + d = dstRow + rx * 4; + byte a = frame[s + 3]; + int ia = 255 - a; + outBuf[d] = (byte)((frame[s] * a + background[s] * ia) / 255); + outBuf[d + 1] = (byte)( + (frame[s + 1] * a + background[s + 1] * ia) / 255 + ); + outBuf[d + 2] = (byte)( + (frame[s + 2] * a + background[s + 2] * ia) / 255 + ); + outBuf[d + 3] = 255; + } + } + else + { + Buffer.BlockCopy(frame, srcRow, outBuf, dstRow, cw * 4); + } + } + stdin.Write(outBuf, 0, outBuf.Length); + EncodeProgress = (int)(f + 1); + } + + stdin.Flush(); + stdin.Close(); + proc.WaitForExit(30000); + } + catch (Exception ex) + { + Error ??= ex.Message; + } + finally + { + if (frame != null) + ArrayPool.Shared.Return(frame); + TryDeleteTemp(); + IsEncoding = false; + } + } + + static void ReadFull(Stream s, byte[] buf, int count) + { + int read = 0; + while (read < count) + { + int n = s.Read(buf, read, count - read); + if (n <= 0) + throw new EndOfStreamException(); + read += n; + } + } + + /// Aborts capture without encoding (cancel button). Safe if idle. + public void Abort() + { + if (IsCapturing) + { + IsCapturing = false; + try + { + _writeQueue.CompleteAdding(); + _writer?.Join(5000); + _tempWrite?.Dispose(); + _tempWrite = null; + } + catch { } + TryDeleteTemp(); + } + } + + void TryDeleteTemp() + { + try + { + if (_tempPath != null && File.Exists(_tempPath)) + File.Delete(_tempPath); + } + catch { } + } + + public void Dispose() + { + Abort(); + _writeQueue?.Dispose(); + _writeQueue = null; + } + } +} diff --git a/PlayerViewer/UI/ExportUtil.cs b/PlayerViewer/UI/ExportUtil.cs new file mode 100644 index 0000000..9fcb0a5 --- /dev/null +++ b/PlayerViewer/UI/ExportUtil.cs @@ -0,0 +1,264 @@ +using System; +using System.Diagnostics; +using System.IO; +using PlayerViewer.Core; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace PlayerViewer.UI +{ + public enum OutputFormat + { + /// H.264 MP4, opaque (yuv420p). Alpha is discarded. + Mp4, + + /// Animated WebP that preserves the RGBA alpha channel. + WebpTransparent, + + /// VP9 WebM that preserves the alpha channel (yuva420p). + WebmTransparent, + } + + /// + /// Shared helpers for the export path: ffmpeg discovery/probe, codec argument + /// building, and filename stamping. Kept static and stateless. + /// + public static class ExportUtil + { + //Prefer an ffmpeg dropped in the per-user data folder; otherwise fall back to PATH. + public static string ResolveFfmpeg() + { + string name = OperatingSystem.IsWindows() ? "ffmpeg.exe" : "ffmpeg"; + string local = Path.Combine(AppPaths.DataDir, name); + return File.Exists(local) ? local : "ffmpeg"; + } + + static bool? _ffmpegAvailable; + public static bool FfmpegAvailable + { + get + { + if (_ffmpegAvailable == null) + { + try + { + using var probe = Process.Start( + new ProcessStartInfo + { + FileName = ResolveFfmpeg(), + Arguments = "-version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + } + ); + probe.WaitForExit(5000); + _ffmpegAvailable = true; + } + catch + { + _ffmpegAvailable = false; + } + } + return _ffmpegAvailable.Value; + } + } + + /// + /// Builds the ffmpeg output-codec arguments for the given format. For WebP the + /// quality knob (0-100) selects lossless vs lossy: 100 keeps the bit-exact + /// lossless encoder (compression_level 4, q:v 75 = entropy-search effort, not + /// quality); below 100 switches to lossy where q:v IS the visual quality. + /// + public static string CodecArgs(OutputFormat format, int quality, string outputPath) + { + if (format == OutputFormat.WebpTransparent) + { + if (quality >= 100) + return $"-c:v libwebp_anim -lossless 1 -compression_level 4 -q:v 75 -loop 0 -an \"{outputPath}\""; + int q = Math.Clamp(quality, 0, 100); + return $"-c:v libwebp_anim -lossless 0 -compression_level 4 -q:v {q} -loop 0 -an \"{outputPath}\""; + } + if (format == OutputFormat.WebmTransparent) + { + //VP9 with an alpha plane (yuva420p). quality 100 = lossless; below maps to a VP9 + //CRF (0 best .. 63 worst). cpu-used/deadline trade encode speed for size. + if (quality >= 100) + return $"-c:v libvpx-vp9 -pix_fmt yuva420p -lossless 1 -cpu-used 4 -deadline good -an \"{outputPath}\""; + int crf = Math.Clamp((100 - quality) * 63 / 100, 0, 63); + return $"-c:v libvpx-vp9 -pix_fmt yuva420p -b:v 0 -crf {crf} -cpu-used 4 -deadline good -an \"{outputPath}\""; + } + return $"-c:v libx264 -preset veryfast -pix_fmt yuv420p -crf 16 \"{outputPath}\""; + } + + /// Raw-RGBA input args for a pipe of the given size and rate. + public static string RawInputArgs(int width, int height, int fps) => + $"-y -f rawvideo -pixel_format rgba -video_size {width}x{height} " + + $"-framerate {fps} -i pipe:0 -vf vflip "; + + /// + /// Builds a full-frame straight-RGBA background buffer for compositing at export time: + /// Color = solid fill, Transparent = opaque black (only used as the MP4 no-alpha + /// fallback; callers pass null when the format keeps alpha), Image = the imported image + /// scaled (Fill/Fit/Stretch + zoom + offset) or tiled. bottomUp flips the vertical sample + /// so a video buffer (composited before ffmpeg's -vf vflip) reads upright. + /// + public static byte[] BuildBackground(int fw, int fh, BackgroundConfig cfg, bool bottomUp) + { + if (fw <= 0 || fh <= 0) + return null; + var buf = new byte[fw * fh * 4]; + + void Fill() + { + byte r = (byte)(Math.Clamp(cfg.Color[0], 0f, 1f) * 255); + byte g = (byte)(Math.Clamp(cfg.Color[1], 0f, 1f) * 255); + byte b = (byte)(Math.Clamp(cfg.Color[2], 0f, 1f) * 255); + for (int i = 0; i < buf.Length; i += 4) + { + buf[i] = r; + buf[i + 1] = g; + buf[i + 2] = b; + buf[i + 3] = 255; + } + } + + if (cfg.Mode == 1) + { + Fill(); + return buf; + } //Color + if (cfg.Mode != 2) //Transparent -> black + { + for (int i = 3; i < buf.Length; i += 4) + buf[i] = 255; + return buf; + } + + //Image mode. A bad/missing path falls back to the solid color so export never crashes. + if (!TryDecodeBackground(cfg.ImagePath, out byte[] src, out int iw, out int ih)) + { + Fill(); + return buf; + } + + float zoom = Math.Max(cfg.Zoom, 1e-4f); + + //Placement/scale is constant across the whole frame; hoist it out of the pixel loops. + int nx = Math.Max(cfg.TileX, 1), + ny = Math.Max(cfg.TileY, 1); + float offXfw = cfg.OffsetX * fw, + offYfh = cfg.OffsetY * fh; + float sx, + sy; + if (cfg.ScaleMode == 2) + { + sx = (float)fw / iw * zoom; + sy = (float)fh / ih * zoom; + } + else + { + float s = + cfg.ScaleMode == 1 + ? MathF.Min((float)fw / iw, (float)fh / ih) //Fit + : MathF.Max((float)fw / iw, (float)fh / ih); //Fill + sx = sy = s * zoom; + } + float dispW = iw * sx, + dispH = ih * sy; + float originX = (fw - dispW) * 0.5f + offXfw; + float originY = (fh - dispH) * 0.5f + offYfh; + + for (int dy = 0; dy < fh; dy++) + { + int dyEff = bottomUp ? fh - 1 - dy : dy; + //v is constant across the row. + float v; + if (cfg.Tile) + { + v = ((dyEff - offYfh) / fh) * ny / zoom; + v -= MathF.Floor(v); + } + else + { + v = (dyEff - originY) / dispH; + } + + for (int dx = 0; dx < fw; dx++) + { + float u; + if (cfg.Tile) + { + u = ((dx - offXfw) / fw) * nx / zoom; + u -= MathF.Floor(u); + } + else + { + u = (dx - originX) / dispW; + if (u < 0 || u >= 1 || v < 0 || v >= 1) + continue; //off-frame / Fit letterbox stays transparent (0,0,0,0) + } + int sxp = Math.Clamp((int)(u * iw), 0, iw - 1); + int syp = Math.Clamp((int)(v * ih), 0, ih - 1); + int si = (syp * iw + sxp) * 4, + d = (dy * fw + dx) * 4; + buf[d] = src[si]; + buf[d + 1] = src[si + 1]; + buf[d + 2] = src[si + 2]; + buf[d + 3] = src[si + 3]; + } + } + return buf; + } + + //Decodes the background image to tightly-packed top-down RGBA, cached by path + write time. + static string _bgPath; + static long _bgStamp; + static byte[] _bgRgba; + static int _bgW, + _bgH; + + static bool TryDecodeBackground(string path, out byte[] rgba, out int w, out int h) + { + rgba = null; + w = h = 0; + try + { + if (string.IsNullOrEmpty(path) || !File.Exists(path)) + return false; + long stamp = File.GetLastWriteTimeUtc(path).Ticks; + if (_bgRgba == null || _bgPath != path || _bgStamp != stamp) + { + using var img = Image.Load(path); + var pixels = new byte[img.Width * img.Height * 4]; + img.CopyPixelDataTo(pixels); + _bgRgba = pixels; + _bgW = img.Width; + _bgH = img.Height; + _bgPath = path; + _bgStamp = stamp; + } + if (_bgW <= 0 || _bgH <= 0) + return false; + rgba = _bgRgba; + w = _bgW; + h = _bgH; + return true; + } + catch + { + return false; + } + } + + /// Appends _<unix seconds> before the extension so saves never collide. + public static string Timestamped(string baseName, string ext) + { + long unix = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + if (!ext.StartsWith('.')) + ext = "." + ext; + return $"{baseName}_{unix}{ext}"; + } + } +} diff --git a/PlayerViewer/UI/NativeFolderPicker.cs b/PlayerViewer/UI/NativeFolderPicker.cs index 9e8029c..0ac1a31 100644 --- a/PlayerViewer/UI/NativeFolderPicker.cs +++ b/PlayerViewer/UI/NativeFolderPicker.cs @@ -1,128 +1,205 @@ using System; +using System.Linq; using System.Runtime.InteropServices; +using System.Text; namespace PlayerViewer.UI { + /// + /// Native file/folder dialogs. On Windows this drives the modern Explorer-style + /// IFileDialog (COM); on Linux/macOS it brokers to tinyfiledialogs (zenity/kdialog + /// or the OS dialog). The COM path is only ever entered on Windows, so nothing here + /// is Windows-bound at runtime elsewhere. All methods return null when cancelled. + /// static class NativeFolderPicker { public static string SelectFolder(string title, string startPath = null) { - try { return SelectFolderCom(title, startPath); } - catch { return null; } + try + { + return OperatingSystem.IsWindows() + ? SelectFolderCom(title, startPath) + : SelectFolderTfd(title, startPath); + } + catch + { + return null; + } } public static string OpenFile(string title, string filterDisplay, string filterExt) { - try { return OpenFileCom(title, filterDisplay, filterExt); } - catch { return null; } + try + { + return OperatingSystem.IsWindows() + ? OpenFileCom(title, filterDisplay, filterExt) + : OpenFileTfd(title, filterDisplay, filterExt); + } + catch + { + return null; + } } - public static string SaveFile(string title, string defaultName, string filterDisplay, string filterExt) + public static string SaveFile( + string title, + string defaultName, + string filterDisplay, + string filterExt + ) { - try { return SaveFileCom(title, defaultName, filterDisplay, filterExt); } - catch { return null; } + try + { + return OperatingSystem.IsWindows() + ? SaveFileCom(title, defaultName, filterDisplay, filterExt) + : SaveFileTfd(title, defaultName, filterDisplay, filterExt); + } + catch + { + return null; + } } - static string SaveFileCom(string title, string defaultName, string filterDisplay, string filterExt) + // ---- Windows: modern IFileDialog (Explorer picker) ---- + + static string OpenFileCom(string title, string filterDisplay, string filterExt) { - int hr = CoCreateInstance(ref CLSID_FileSaveDialog, IntPtr.Zero, 1, - ref IID_IFileSaveDialog, out IntPtr dlg); - if (hr != 0) return null; + int hr = CoCreateInstance( + ref CLSID_FileOpenDialog, + IntPtr.Zero, + 1, + ref IID_IFileOpenDialog, + out IntPtr dlg + ); + if (hr != 0) + return null; try { GetOptions(dlg, out uint opts); - SetOptions(dlg, opts | 0x2 /* FOS_OVERWRITEPROMPT */ | 0x40 /* FOS_FORCEFILESYSTEM */); + SetOptions( + dlg, + opts | 0x40 /* FOS_FORCEFILESYSTEM */ + ); if (!string.IsNullOrEmpty(title)) SetTitle(dlg, title); - if (!string.IsNullOrEmpty(defaultName)) - SetFileName(dlg, defaultName); var filter = new COMDLG_FILTERSPEC { pszName = filterDisplay, pszSpec = filterExt }; SetFileTypes(dlg, 1, ref filter); hr = Show(dlg, GetActiveWindow()); - if (hr != 0) return null; + if (hr != 0) + return null; hr = GetResult(dlg, out IntPtr item); - if (hr != 0 || item == IntPtr.Zero) return null; - + if (hr != 0 || item == IntPtr.Zero) + return null; try { - hr = GetDisplayName(item, 0x80058000, out IntPtr namePtr); - if (hr != 0) return null; - string path = Marshal.PtrToStringUni(namePtr); - Marshal.FreeCoTaskMem(namePtr); - return path; + return DisplayName(item); + } + finally + { + Marshal.Release(item); } - finally { Marshal.Release(item); } } - finally { Marshal.Release(dlg); } - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - struct COMDLG_FILTERSPEC - { - [MarshalAs(UnmanagedType.LPWStr)] public string pszName; - [MarshalAs(UnmanagedType.LPWStr)] public string pszSpec; + finally + { + Marshal.Release(dlg); + } } - static string OpenFileCom(string title, string filterDisplay, string filterExt) + static string SaveFileCom( + string title, + string defaultName, + string filterDisplay, + string filterExt + ) { - int hr = CoCreateInstance(ref CLSID_FileOpenDialog, IntPtr.Zero, 1, - ref IID_IFileOpenDialog, out IntPtr dlg); - if (hr != 0) return null; + int hr = CoCreateInstance( + ref CLSID_FileSaveDialog, + IntPtr.Zero, + 1, + ref IID_IFileSaveDialog, + out IntPtr dlg + ); + if (hr != 0) + return null; try { GetOptions(dlg, out uint opts); - SetOptions(dlg, opts | 0x40 /* FOS_FORCEFILESYSTEM */); + SetOptions( + dlg, + opts + | 0x2 /* FOS_OVERWRITEPROMPT */ + | 0x40 /* FOS_FORCEFILESYSTEM */ + ); if (!string.IsNullOrEmpty(title)) SetTitle(dlg, title); + if (!string.IsNullOrEmpty(defaultName)) + SetFileName(dlg, defaultName); var filter = new COMDLG_FILTERSPEC { pszName = filterDisplay, pszSpec = filterExt }; SetFileTypes(dlg, 1, ref filter); hr = Show(dlg, GetActiveWindow()); - if (hr != 0) return null; + if (hr != 0) + return null; hr = GetResult(dlg, out IntPtr item); - if (hr != 0 || item == IntPtr.Zero) return null; - + if (hr != 0 || item == IntPtr.Zero) + return null; try { - hr = GetDisplayName(item, 0x80058000, out IntPtr namePtr); - if (hr != 0) return null; - string path = Marshal.PtrToStringUni(namePtr); - Marshal.FreeCoTaskMem(namePtr); - return path; + return DisplayName(item); + } + finally + { + Marshal.Release(item); } - finally { Marshal.Release(item); } } - finally { Marshal.Release(dlg); } + finally + { + Marshal.Release(dlg); + } } static string SelectFolderCom(string title, string startPath) { - int hr = CoCreateInstance(ref CLSID_FileOpenDialog, IntPtr.Zero, 1, - ref IID_IFileOpenDialog, out IntPtr dlg); - if (hr != 0) return null; + int hr = CoCreateInstance( + ref CLSID_FileOpenDialog, + IntPtr.Zero, + 1, + ref IID_IFileOpenDialog, + out IntPtr dlg + ); + if (hr != 0) + return null; try { - // FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM GetOptions(dlg, out uint opts); - SetOptions(dlg, opts | 0x20 | 0x40); + SetOptions( + dlg, + opts + | 0x20 /* FOS_PICKFOLDERS */ + | 0x40 /* FOS_FORCEFILESYSTEM */ + ); if (!string.IsNullOrEmpty(title)) SetTitle(dlg, title); if (!string.IsNullOrEmpty(startPath)) { - hr = SHCreateItemFromParsingName(startPath, IntPtr.Zero, - ref IID_IShellItem, out IntPtr folder); + hr = SHCreateItemFromParsingName( + startPath, + IntPtr.Zero, + ref IID_IShellItem, + out IntPtr folder + ); if (hr == 0 && folder != IntPtr.Zero) { SetFolder(dlg, folder); @@ -131,22 +208,50 @@ static string SelectFolderCom(string title, string startPath) } hr = Show(dlg, GetActiveWindow()); - if (hr != 0) return null; + if (hr != 0) + return null; hr = GetResult(dlg, out IntPtr item); - if (hr != 0 || item == IntPtr.Zero) return null; - + if (hr != 0 || item == IntPtr.Zero) + return null; try { - hr = GetDisplayName(item, 0x80058000 /* SIGDN_FILESYSPATH */, out IntPtr namePtr); - if (hr != 0) return null; - string path = Marshal.PtrToStringUni(namePtr); - Marshal.FreeCoTaskMem(namePtr); - return path; + return DisplayName(item); + } + finally + { + Marshal.Release(item); } - finally { Marshal.Release(item); } } - finally { Marshal.Release(dlg); } + finally + { + Marshal.Release(dlg); + } + } + + static string DisplayName(IntPtr item) + { + int hr = GetDisplayName( + item, + 0x80058000 /* SIGDN_FILESYSPATH */ + , + out IntPtr namePtr + ); + if (hr != 0) + return null; + string path = Marshal.PtrToStringUni(namePtr); + Marshal.FreeCoTaskMem(namePtr); + return path; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + struct COMDLG_FILTERSPEC + { + [MarshalAs(UnmanagedType.LPWStr)] + public string pszName; + + [MarshalAs(UnmanagedType.LPWStr)] + public string pszSpec; } static Guid CLSID_FileOpenDialog = new("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7"); @@ -155,20 +260,64 @@ static string SelectFolderCom(string title, string startPath) static Guid IID_IFileSaveDialog = new("84BCCD23-5FDE-4CDB-AEA4-AF64B83D78AB"); static Guid IID_IShellItem = new("43826D1E-E718-42EE-BC55-A1E261C37BFE"); - [DllImport("ole32")] static extern int CoCreateInstance(ref Guid clsid, IntPtr outer, uint ctx, ref Guid iid, out IntPtr obj); - [DllImport("user32")] static extern IntPtr GetActiveWindow(); - [DllImport("shell32", CharSet = CharSet.Unicode)] static extern int SHCreateItemFromParsingName(string path, IntPtr bc, ref Guid iid, out IntPtr item); - - // IFileOpenDialog vtable offsets (IUnknown=3, IModalWindow=1, IFileDialog=15) - static int Show(IntPtr dlg, IntPtr hwnd) => Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 3))(dlg, hwnd); - static void SetOptions(IntPtr dlg, uint opts) => Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 9))(dlg, opts); - static void GetOptions(IntPtr dlg, out uint opts) { opts = 0; Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 10))(dlg, out opts); } - static void SetFileTypes(IntPtr dlg, uint count, ref COMDLG_FILTERSPEC filters) => Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 4))(dlg, count, ref filters); - static void SetFileName(IntPtr dlg, string name) => Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 15))(dlg, name); - static void SetFolder(IntPtr dlg, IntPtr folder) => Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 12))(dlg, folder); - static void SetTitle(IntPtr dlg, string title) => Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 17))(dlg, title); - static int GetResult(IntPtr dlg, out IntPtr item) => Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 20))(dlg, out item); - static int GetDisplayName(IntPtr item, uint sigdn, out IntPtr name) => Marshal.GetDelegateForFunctionPointer(Vtbl(item, 5))(item, sigdn, out name); + [DllImport("ole32")] + static extern int CoCreateInstance( + ref Guid clsid, + IntPtr outer, + uint ctx, + ref Guid iid, + out IntPtr obj + ); + + [DllImport("user32")] + static extern IntPtr GetActiveWindow(); + + [DllImport("shell32", CharSet = CharSet.Unicode)] + static extern int SHCreateItemFromParsingName( + string path, + IntPtr bc, + ref Guid iid, + out IntPtr item + ); + + // IFileDialog vtable offsets (IUnknown=3, IModalWindow=1, IFileDialog=15). + static int Show(IntPtr dlg, IntPtr hwnd) => + Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 3))(dlg, hwnd); + + static void SetOptions(IntPtr dlg, uint opts) => + Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 9))(dlg, opts); + + static void GetOptions(IntPtr dlg, out uint opts) + { + opts = 0; + Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 10))(dlg, out opts); + } + + static void SetFileTypes(IntPtr dlg, uint count, ref COMDLG_FILTERSPEC filters) => + Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 4))( + dlg, + count, + ref filters + ); + + static void SetFileName(IntPtr dlg, string name) => + Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 15))(dlg, name); + + static void SetFolder(IntPtr dlg, IntPtr folder) => + Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 12))(dlg, folder); + + static void SetTitle(IntPtr dlg, string title) => + Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 17))(dlg, title); + + static int GetResult(IntPtr dlg, out IntPtr item) => + Marshal.GetDelegateForFunctionPointer(Vtbl(dlg, 20))(dlg, out item); + + static int GetDisplayName(IntPtr item, uint sigdn, out IntPtr name) => + Marshal.GetDelegateForFunctionPointer(Vtbl(item, 5))( + item, + sigdn, + out name + ); static IntPtr Vtbl(IntPtr obj, int slot) { @@ -176,14 +325,111 @@ static IntPtr Vtbl(IntPtr obj, int slot) return Marshal.ReadIntPtr(vtbl, slot * IntPtr.Size); } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] delegate int ShowDelegate(IntPtr self, IntPtr hwnd); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] delegate int SetOptionsDelegate(IntPtr self, uint opts); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] delegate int GetOptionsDelegate(IntPtr self, out uint opts); - [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] delegate int SetFileTypesDelegate(IntPtr self, uint count, ref COMDLG_FILTERSPEC filters); - [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] delegate int SetFileNameDelegate(IntPtr self, string name); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] delegate int SetFolderDelegate(IntPtr self, IntPtr folder); - [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] delegate int SetTitleDelegate(IntPtr self, string title); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] delegate int GetResultDelegate(IntPtr self, out IntPtr item); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] delegate int GetDisplayNameDelegate(IntPtr self, uint sigdn, out IntPtr name); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate int ShowDelegate(IntPtr self, IntPtr hwnd); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate int SetOptionsDelegate(IntPtr self, uint opts); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate int GetOptionsDelegate(IntPtr self, out uint opts); + + [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] + delegate int SetFileTypesDelegate(IntPtr self, uint count, ref COMDLG_FILTERSPEC filters); + + [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] + delegate int SetFileNameDelegate(IntPtr self, string name); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate int SetFolderDelegate(IntPtr self, IntPtr folder); + + [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] + delegate int SetTitleDelegate(IntPtr self, string title); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate int GetResultDelegate(IntPtr self, out IntPtr item); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate int GetDisplayNameDelegate(IntPtr self, uint sigdn, out IntPtr name); + + // ---- Linux/macOS: tinyfiledialogs ---- + + static string SelectFolderTfd(string title, string startPath) => + Str(tinyfd_selectFolderDialog(title, startPath ?? "")); + + static string OpenFileTfd(string title, string filterDisplay, string filterExt) => + WithPatterns( + filterExt, + (count, patterns) => + tinyfd_openFileDialog(title, "", count, patterns, filterDisplay, 0) + ); + + static string SaveFileTfd( + string title, + string defaultName, + string filterDisplay, + string filterExt + ) => + WithPatterns( + filterExt, + (count, patterns) => + tinyfd_saveFileDialog(title, defaultName ?? "", count, patterns, filterDisplay) + ); + + // tinyfiledialogs wants one glob per array entry ("*.png"), so a combined + // "*.bfres;*.zs" filter is split apart. The UTF-8 pattern buffers are freed after. + static string WithPatterns(string filterExt, Func call) + { + string[] patterns = string.IsNullOrEmpty(filterExt) + ? Array.Empty() + : filterExt.Split(';', StringSplitOptions.RemoveEmptyEntries); + IntPtr[] utf8 = patterns.Select(Utf8).ToArray(); + try + { + return Str(call(utf8.Length, utf8)); + } + finally + { + foreach (var p in utf8) + Marshal.FreeHGlobal(p); + } + } + + static IntPtr Utf8(string s) + { + byte[] bytes = Encoding.UTF8.GetBytes(s + '\0'); + IntPtr p = Marshal.AllocHGlobal(bytes.Length); + Marshal.Copy(bytes, 0, p, bytes.Length); + return p; + } + + static string Str(IntPtr p) => p == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(p); + + const string Tfd = "tinyfiledialogs"; + + [DllImport(Tfd, CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr tinyfd_selectFolderDialog( + [MarshalAs(UnmanagedType.LPUTF8Str)] string title, + [MarshalAs(UnmanagedType.LPUTF8Str)] string defaultPath + ); + + [DllImport(Tfd, CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr tinyfd_openFileDialog( + [MarshalAs(UnmanagedType.LPUTF8Str)] string title, + [MarshalAs(UnmanagedType.LPUTF8Str)] string defaultPathAndFile, + int numOfFilterPatterns, + IntPtr[] filterPatterns, + [MarshalAs(UnmanagedType.LPUTF8Str)] string singleFilterDescription, + int allowMultipleSelects + ); + + [DllImport(Tfd, CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr tinyfd_saveFileDialog( + [MarshalAs(UnmanagedType.LPUTF8Str)] string title, + [MarshalAs(UnmanagedType.LPUTF8Str)] string defaultPathAndFile, + int numOfFilterPatterns, + IntPtr[] filterPatterns, + [MarshalAs(UnmanagedType.LPUTF8Str)] string singleFilterDescription + ); } } diff --git a/PlayerViewer/UI/ScenePipeline.cs b/PlayerViewer/UI/ScenePipeline.cs index 051e03a..b3915a6 100644 --- a/PlayerViewer/UI/ScenePipeline.cs +++ b/PlayerViewer/UI/ScenePipeline.cs @@ -1,9 +1,11 @@ using System; -using System.Drawing; +using GLFrameworkEngine; using OpenTK; using OpenTK.Graphics.OpenGL; -using GLFrameworkEngine; using PlayerViewer.Player; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace PlayerViewer.UI { @@ -26,17 +28,35 @@ public class ScenePipeline : IDisposable /// Main light shines from the camera when enabled. public bool LightFollowsCamera; - float _lightAzimuth = 100, _lightElevation = -55; + float _lightAzimuth = 100, + _lightElevation = -55; bool _lightCustomized; public float LightAzimuth { get => _lightAzimuth; - set { _lightAzimuth = value; _lightCustomized = true; } + set + { + _lightAzimuth = value; + _lightCustomized = true; + } } public float LightElevation { get => _lightElevation; - set { _lightElevation = value; _lightCustomized = true; } + set + { + _lightElevation = value; + _lightCustomized = true; + } + } + + /// Restores the dumped default lighting (follow-cam off, no override). + public void ResetLighting() + { + LightFollowsCamera = false; + _lightAzimuth = 100; + _lightElevation = -55; + _lightCustomized = false; //fall back to the dumped default direction } void UpdateLightOverride() @@ -47,9 +67,12 @@ void UpdateLightOverride() //azimuth/elevation sliders so disabling follow-cam keeps the light //where it last was. var forward = -Camera.InverseRotationMatrix.Row2; - _lightElevation = MathHelper.RadiansToDegrees((float)Math.Asin( - MathHelper.Clamp(forward.Y, -1, 1))); - _lightAzimuth = MathHelper.RadiansToDegrees((float)Math.Atan2(forward.X, forward.Z)); + _lightElevation = MathHelper.RadiansToDegrees( + (float)Math.Asin(MathHelper.Clamp(forward.Y, -1, 1)) + ); + _lightAzimuth = MathHelper.RadiansToDegrees( + (float)Math.Atan2(forward.X, forward.Z) + ); _lightCustomized = true; BfresEditor.HoianNXRender.LightDirOverride = forward; } @@ -60,40 +83,58 @@ void UpdateLightOverride() BfresEditor.HoianNXRender.LightDirOverride = new Vector3( (float)(Math.Cos(el) * Math.Sin(az)), (float)Math.Sin(el), - (float)(Math.Cos(el) * Math.Cos(az))); + (float)(Math.Cos(el) * Math.Cos(az)) + ); } else - BfresEditor.HoianNXRender.LightDirOverride = null; //dumped default + BfresEditor.HoianNXRender.LightDirOverride = null; //dumped default } //The framework's MSAA framebuffer path is broken (the color attachment ends //up non-multisampled and incomplete on strict drivers), so anti-alias by //supersampling: render 2x and downsample with linear filtering in the gamma pass. - Framebuffer _screen; //RGBA16F (linear), supersampled + Framebuffer _screen; //RGBA16F (linear), supersampled DepthTexture _screenDepth; - Framebuffer _final; //RGBA8 (sRGB encoded by the gamma pass) + Framebuffer _final; //RGBA8 (sRGB encoded by the gamma pass) FinalQuad _quad; SelfShadowRenderer _selfShadow; + //Live export-background preview: a fullscreen textured quad drawn behind the scene in + //opaque passes. The pixels come from ExportUtil.BuildBackground so the preview matches + //the exported composite exactly. + readonly BackgroundQuad _bgQuad = new(); + int _bgTex; + //Half-res color copy for refraction (once per frame, between opaque/transparent). int _refractionFbo; GLTexture2D _refractionColor; - int _refractionW, _refractionH; + int _refractionW, + _refractionH; + /// Game-accurate self shadowing (shadow prepass). On by default. public bool EnableSelfShadow = true; const int SuperSample = 2; + //Above this pixel count captures render at 1x (a 4K capture is sharp already). const long SuperSampleBudget = 2560L * 1440L; static int ScaleFor(int width, int height) => (long)width * height > SuperSampleBudget ? 1 : SuperSample; + //When >0, export/capture renders use this supersample scale instead of the auto + //budget above (set from the Settings factor); reset to 0 for interactive sizing. + public int ExportScaleOverride; + int _screenScale = SuperSample; + + int EffectiveScale(int width, int height) => + ExportScaleOverride > 0 ? ExportScaleOverride : ScaleFor(width, height); + public void Init() { Context = new GLContext(); Context.Camera = new Camera(); Context.Camera.ZNear = 0.01f; - Context.Camera.Mode = Camera.CameraMode.Inspect; //instantiates the controller + Context.Camera.Mode = Camera.CameraMode.Inspect; //instantiates the controller Context.UseSRBFrameBuffer = true; //Camera math needs a valid aspect ratio before framing (0x0 -> NaN distance). Context.Width = Width; @@ -102,8 +143,18 @@ public void Init() Context.Camera.Height = Height; FramePlayer(); - _screen = CreateScreenBuffer(Width * SuperSample, Height * SuperSample, out _screenDepth); - _final = new Framebuffer(FramebufferTarget.Framebuffer, Width, Height, PixelInternalFormat.Rgba8, 1); + _screen = CreateScreenBuffer( + Width * SuperSample, + Height * SuperSample, + out _screenDepth + ); + _final = new Framebuffer( + FramebufferTarget.Framebuffer, + Width, + Height, + PixelInternalFormat.Rgba8, + 1 + ); _quad = new FinalQuad(); } @@ -111,8 +162,14 @@ public void Init() //reconstructs world positions from it). static Framebuffer CreateScreenBuffer(int width, int height, out DepthTexture depth) { - var fbo = new Framebuffer(FramebufferTarget.Framebuffer, - width, height, PixelInternalFormat.Rgba16f, 1, useDepth: false); + var fbo = new Framebuffer( + FramebufferTarget.Framebuffer, + width, + height, + PixelInternalFormat.Rgba16f, + 1, + useDepth: false + ); depth = new DepthTexture(width, height, PixelInternalFormat.DepthComponent24); fbo.AddAttachment(FramebufferAttachment.DepthAttachment, depth); return fbo; @@ -125,37 +182,89 @@ static Framebuffer CreateScreenBuffer(int width, int height, out DepthTexture de /// void CaptureRefractionBuffers(Framebuffer screen, DepthTexture depth, int ssW, int ssH) { - int halfW = ssW / 2, halfH = ssH / 2; - if (halfW < 1 || halfH < 1) return; + int halfW = ssW / 2, + halfH = ssH / 2; + if (halfW < 1 || halfH < 1) + return; if (_refractionFbo == 0 || _refractionW != halfW || _refractionH != halfH) { - if (_refractionFbo != 0) GL.DeleteFramebuffer(_refractionFbo); + DisposeRefraction(); //frees the previous-size fbo + color texture _refractionColor = new GLTexture2D(); GL.BindTexture(TextureTarget.Texture2D, _refractionColor.ID); - GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.R11fG11fB10f, - halfW, halfH, 0, PixelFormat.Rgb, PixelType.Float, IntPtr.Zero); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge); + GL.TexImage2D( + TextureTarget.Texture2D, + 0, + PixelInternalFormat.R11fG11fB10f, + halfW, + halfH, + 0, + PixelFormat.Rgb, + PixelType.Float, + IntPtr.Zero + ); + GL.TexParameter( + TextureTarget.Texture2D, + TextureParameterName.TextureMinFilter, + (int)TextureMinFilter.Linear + ); + GL.TexParameter( + TextureTarget.Texture2D, + TextureParameterName.TextureMagFilter, + (int)TextureMagFilter.Linear + ); + GL.TexParameter( + TextureTarget.Texture2D, + TextureParameterName.TextureWrapS, + (int)TextureWrapMode.ClampToEdge + ); + GL.TexParameter( + TextureTarget.Texture2D, + TextureParameterName.TextureWrapT, + (int)TextureWrapMode.ClampToEdge + ); _refractionFbo = GL.GenFramebuffer(); GL.BindFramebuffer(FramebufferTarget.Framebuffer, _refractionFbo); - GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, - FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, _refractionColor.ID, 0); + GL.FramebufferTexture2D( + FramebufferTarget.Framebuffer, + FramebufferAttachment.ColorAttachment0, + TextureTarget.Texture2D, + _refractionColor.ID, + 0 + ); _refractionW = halfW; _refractionH = halfH; } GL.BindFramebuffer(FramebufferTarget.ReadFramebuffer, screen.ID); GL.BindFramebuffer(FramebufferTarget.DrawFramebuffer, _refractionFbo); - GL.BlitFramebuffer(0, 0, ssW, ssH, 0, 0, halfW, halfH, - ClearBufferMask.ColorBufferBit, BlitFramebufferFilter.Linear); + GL.BlitFramebuffer( + 0, + 0, + ssW, + ssH, + 0, + 0, + halfW, + halfH, + ClearBufferMask.ColorBufferBit, + BlitFramebufferFilter.Linear + ); BfresEditor.HoianNXRender.RefractionColorBuffer = _refractionColor; BfresEditor.HoianNXRender.RefractionDepthBuffer = depth; } + void DisposeRefraction() + { + if (_refractionFbo != 0) + GL.DeleteFramebuffer(_refractionFbo); + _refractionFbo = 0; + _refractionColor?.Dispose(); + _refractionColor = null; + _refractionW = _refractionH = 0; + } + //Fallback self-shadow light bounds (player-sized), used when the scene has //no valid render bounds yet. Vector4 _shadowBounds = new Vector4(0, 0.85f, 0, 1.6f); @@ -204,11 +313,12 @@ public void Resize(int width, int height) { width = Math.Max(width, 1); height = Math.Max(height, 1); - if (width == Width && height == Height) + int scale = EffectiveScale(width, height); + if (width == Width && height == Height && scale == _screenScale) return; Width = width; Height = height; - int scale = ScaleFor(width, height); + _screenScale = scale; _screen.Resize(width * scale, height * scale); _final.Resize(width, height); Context.Width = width; @@ -223,26 +333,51 @@ public void Resize(int width, int height) /// Renders the scene into the final displayable texture. public void Render(IViewScene scene) { - RenderInternal(scene, _screen, _final, Width, Height, ScaleFor(Width, Height), - new System.Numerics.Vector4(BackgroundColor.X, BackgroundColor.Y, BackgroundColor.Z, 1), false, - _screenDepth); + RenderInternal( + scene, + _screen, + _final, + Width, + Height, + EffectiveScale(Width, Height), + new System.Numerics.Vector4( + BackgroundColor.X, + BackgroundColor.Y, + BackgroundColor.Z, + 1 + ), + false, + _screenDepth + ); } public static bool DebugTrace; + void Trace(string stage, int w, int h) { - if (!DebugTrace) return; + if (!DebugTrace) + return; var err = GL.GetError(); var status = GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer); float[] px = new float[4]; GL.ReadPixels(w / 2, h / 2, 1, 1, PixelFormat.Rgba, PixelType.Float, px); - Console.WriteLine($"[Pipeline] {stage}: err={err} fbo={status} center=({px[0]:F3},{px[1]:F3},{px[2]:F3},{px[3]:F3}) " + - $"camPos=({Camera.GetViewPostion().X:F2},{Camera.GetViewPostion().Y:F2},{Camera.GetViewPostion().Z:F2}) dist={Camera.TargetDistance:F2}"); + Console.WriteLine( + $"[Pipeline] {stage}: err={err} fbo={status} center=({px[0]:F3},{px[1]:F3},{px[2]:F3},{px[3]:F3}) " + + $"camPos=({Camera.GetViewPostion().X:F2},{Camera.GetViewPostion().Y:F2},{Camera.GetViewPostion().Z:F2}) dist={Camera.TargetDistance:F2}" + ); } - void RenderInternal(IViewScene scene, Framebuffer screen, Framebuffer final, - int width, int height, int scale, System.Numerics.Vector4 background, bool keepAlpha, - DepthTexture screenDepth = null) + void RenderInternal( + IViewScene scene, + Framebuffer screen, + Framebuffer final, + int width, + int height, + int scale, + System.Numerics.Vector4 background, + bool keepAlpha, + DepthTexture screenDepth = null + ) { int ssWidth = width * scale; int ssHeight = height * scale; @@ -252,7 +387,7 @@ void RenderInternal(IViewScene scene, Framebuffer screen, Framebuffer final, //The scene renders at the supersampled size; aspect is unchanged. Context.Width = ssWidth; Context.Height = ssHeight; - Context.ScreenBuffer = screen; //used by XLU/color-pass materials + Context.ScreenBuffer = screen; //used by XLU/color-pass materials Camera.Width = ssWidth; Camera.Height = ssHeight; Camera.UpdateMatrices(); @@ -286,16 +421,31 @@ void DrawScenePass() { screen.Bind(); GL.Viewport(0, 0, ssWidth, ssHeight); - GL.ClearColor(Lin(background.X), Lin(background.Y), Lin(background.Z), background.W); - GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit); + GL.ClearColor( + Lin(background.X), + Lin(background.Y), + Lin(background.Z), + background.W + ); + GL.Clear( + ClearBufferMask.ColorBufferBit + | ClearBufferMask.DepthBufferBit + | ClearBufferMask.StencilBufferBit + ); + + //Opaque (viewport / non-alpha) passes preview the export background behind the + //scene. Transparent capture (keepAlpha) skips it so the alpha oracle is intact. + if (!keepAlpha && _bgTex != 0) + _bgQuad.Draw(Context, _bgTex); + GL.Enable(EnableCap.DepthTest); if (scene != null) { scene.Draw(Context, Pass.OPAQUE); - bool refract = screenDepth != null && - BfresEditor.HoianNXRender.NeedsRefractionBuffers; + bool refract = + screenDepth != null && BfresEditor.HoianNXRender.NeedsRefractionBuffers; if (refract) { CaptureRefractionBuffers(screen, screenDepth, ssWidth, ssHeight); @@ -357,20 +507,56 @@ void DrawScenePass() /// alpha 0 and keeps coverage in the output (background rgb still applies to /// semi-transparent edges). /// - public Bitmap Capture(IViewScene scene, int width, int height, System.Numerics.Vector3 background, bool transparent) + public Image Capture( + IViewScene scene, + int width, + int height, + System.Numerics.Vector3 background, + bool transparent, + int scaleOverride = 0 + ) { - int scale = ScaleFor(width, height); + int scale = scaleOverride > 0 ? scaleOverride : ScaleFor(width, height); var screen = CreateScreenBuffer(width * scale, height * scale, out var screenDepth); - var final = new Framebuffer(FramebufferTarget.Framebuffer, width, height, PixelInternalFormat.Rgba8, 1); + var final = new Framebuffer( + FramebufferTarget.Framebuffer, + width, + height, + PixelInternalFormat.Rgba8, + 1 + ); try { - RenderInternal(scene, screen, final, width, height, scale, - new System.Numerics.Vector4(background.X, background.Y, background.Z, transparent ? 0 : 1), transparent, - screenDepth); + RenderInternal( + scene, + screen, + final, + width, + height, + scale, + new System.Numerics.Vector4( + background.X, + background.Y, + background.Z, + transparent ? 0 : 1 + ), + transparent, + screenDepth + ); final.Bind(); - var bitmap = final.ReadImagePixels(transparent); + byte[] pixels = new byte[width * height * 4]; + GL.ReadBuffer(ReadBufferMode.ColorAttachment0); + GL.ReadPixels( + 0, + 0, + width, + height, + PixelFormat.Rgba, + PixelType.UnsignedByte, + pixels + ); final.Unbind(); - return bitmap; + return ToImage(pixels, width, height, transparent); } finally { @@ -381,107 +567,195 @@ public Bitmap Capture(IViewScene scene, int width, int height, System.Numerics.V } } - /// Reads the current final buffer (viewport-sized), for video frames. - public byte[] ReadFinalPixels() + //Wraps bottom-up RGBA8 bytes (OpenGL row order) into a top-down ImageSharp image. + //When not transparent the alpha channel is forced opaque. + static Image ToImage(byte[] rgba, int width, int height, bool transparent) + { + if (!transparent) + for (int i = 3; i < rgba.Length; i += 4) + rgba[i] = 255; + var image = Image.LoadPixelData(rgba, width, height); + image.Mutate(x => x.Flip(FlipMode.Vertical)); + return image; + } + + /// + /// Renders one frame at the current viewport size into the display buffers and + /// returns raw bottom-up RGBA8 bytes (OpenGL row order, ffmpeg-ready). + /// transparent=true keeps the real alpha channel; otherwise the frame is + /// composited over opaquely. The buffer is rented + /// from ; ownership transfers to the caller. + /// Synchronous, so every frame deterministically maps 1:1. + /// + public byte[] CaptureFrameBytes( + IViewScene scene, + System.Numerics.Vector3 background, + bool transparent, + out int width, + out int height + ) { - var pixels = new byte[Width * Height * 4]; + width = Width; + height = Height; + RenderInternal( + scene, + _screen, + _final, + Width, + Height, + EffectiveScale(Width, Height), + new System.Numerics.Vector4( + background.X, + background.Y, + background.Z, + transparent ? 0 : 1 + ), + transparent, + _screenDepth + ); + + int size = Width * Height * 4; + var buf = System.Buffers.ArrayPool.Shared.Rent(size); _final.Bind(); GL.ReadBuffer(ReadBufferMode.ColorAttachment0); - GL.ReadPixels(0, 0, Width, Height, PixelFormat.Rgba, PixelType.UnsignedByte, pixels); + GL.ReadPixels(0, 0, Width, Height, PixelFormat.Rgba, PixelType.UnsignedByte, buf); _final.Unbind(); - return pixels; + return buf; } - #region PBO async readback (video recording) - - int[] _pbo = new int[2]; - int _pboIndex; - int _pboWidth, _pboHeight; - bool _pboReady; - - void EnsurePBOs(int w, int h) + /// + /// Uploads a full-frame straight-RGBA buffer as the live background preview (drawn behind + /// the scene in opaque passes). Pass null to clear it (Transparent mode). + /// + public void SetBackgroundBuffer(byte[] rgba, int w, int h) { - int size = w * h * 4; - if (_pbo[0] != 0 && _pboWidth == w && _pboHeight == h) - return; - DisposePBOs(); - _pboWidth = w; - _pboHeight = h; - GL.GenBuffers(2, _pbo); - for (int i = 0; i < 2; i++) + if (rgba == null || w <= 0 || h <= 0) { - GL.BindBuffer(BufferTarget.PixelPackBuffer, _pbo[i]); - GL.BufferData(BufferTarget.PixelPackBuffer, size, IntPtr.Zero, BufferUsageHint.StreamRead); + if (_bgTex != 0) + { + GL.DeleteTexture(_bgTex); + _bgTex = 0; + } + return; } - GL.BindBuffer(BufferTarget.PixelPackBuffer, 0); - _pboIndex = 0; - _pboReady = false; + if (_bgTex == 0) + _bgTex = GL.GenTexture(); + GL.BindTexture(TextureTarget.Texture2D, _bgTex); + GL.TexImage2D( + TextureTarget.Texture2D, + 0, + PixelInternalFormat.Rgba, + w, + h, + 0, + PixelFormat.Rgba, + PixelType.UnsignedByte, + rgba + ); + GL.TexParameter( + TextureTarget.Texture2D, + TextureParameterName.TextureMinFilter, + (int)TextureMinFilter.Linear + ); + GL.TexParameter( + TextureTarget.Texture2D, + TextureParameterName.TextureMagFilter, + (int)TextureMagFilter.Linear + ); + GL.TexParameter( + TextureTarget.Texture2D, + TextureParameterName.TextureWrapS, + (int)TextureWrapMode.ClampToEdge + ); + GL.TexParameter( + TextureTarget.Texture2D, + TextureParameterName.TextureWrapT, + (int)TextureWrapMode.ClampToEdge + ); + GL.BindTexture(TextureTarget.Texture2D, 0); } - void DisposePBOs() + public void Dispose() { - if (_pbo[0] != 0) - GL.DeleteBuffers(2, _pbo); - _pbo[0] = _pbo[1] = 0; - _pboReady = false; + DisposeRefraction(); + if (_bgTex != 0) + { + GL.DeleteTexture(_bgTex); + _bgTex = 0; + } + _screen?.Dispoe(); + _final?.Dispoe(); + _selfShadow?.Dispose(); } + } - /// - /// Initiates an async readback of the final buffer into a PBO, and returns - /// the *previous* frame's data (one frame of latency). Returns null on the - /// first call while the pipeline fills. This never stalls the GPU because - /// the readback target is always one frame behind. - /// The returned buffer is rented from ArrayPool; the caller MUST return it - /// via after use. - /// - public byte[] ReadFinalPixelsAsync(out int frameSize) - { - EnsurePBOs(Width, Height); - frameSize = _pboWidth * _pboHeight * 4; + /// Fullscreen quad that draws the background texture (sRGB) into the linear scene + /// buffer, linearizing so the gamma pass restores it; transparent texels are discarded so + /// letterbox/off-frame areas keep the clear color. + class BackgroundQuad + { + ShaderProgram _shader; + VertexBufferObject _vao; - int readPbo = _pboIndex; - int mapPbo = 1 - _pboIndex; + const string Vert = + "#version 330\n" + + "layout (location = 0) in vec2 aPos;\n" + + "layout (location = 1) in vec2 aTexCoords;\n" + + "out vec2 TexCoords;\n" + + "void main(){ gl_Position = vec4(aPos, 0.0, 1.0); TexCoords = aTexCoords; }\n"; + + const string Frag = + "#version 330\n" + + "precision highp float;\n" + + "in vec2 TexCoords;\n" + + "uniform sampler2D uTex;\n" + + "out vec4 FragColor;\n" + + "void main(){\n" + + " vec4 c = texture(uTex, TexCoords);\n" + + " if (c.a < 0.004) discard;\n" + + " FragColor = vec4(pow(c.rgb, vec3(2.2)), 1.0);\n" + + "}\n"; - _final.Bind(); - GL.ReadBuffer(ReadBufferMode.ColorAttachment0); - GL.BindBuffer(BufferTarget.PixelPackBuffer, _pbo[readPbo]); - GL.ReadPixels(0, 0, _pboWidth, _pboHeight, PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero); - GL.BindBuffer(BufferTarget.PixelPackBuffer, 0); - _final.Unbind(); + void Init() + { + if (_shader != null) + return; + _shader = new ShaderProgram(new FragmentShader(Frag), new VertexShader(Vert)); - byte[] result = null; - if (_pboReady) - { - GL.BindBuffer(BufferTarget.PixelPackBuffer, _pbo[mapPbo]); - IntPtr ptr = GL.MapBuffer(BufferTarget.PixelPackBuffer, BufferAccess.ReadOnly); - if (ptr != IntPtr.Zero) - { - result = System.Buffers.ArrayPool.Shared.Rent(frameSize); - System.Runtime.InteropServices.Marshal.Copy(ptr, result, 0, frameSize); - GL.UnmapBuffer(BufferTarget.PixelPackBuffer); - } - GL.BindBuffer(BufferTarget.PixelPackBuffer, 0); - } + int buffer = GL.GenBuffer(); + _vao = new VertexBufferObject(buffer); + _vao.AddAttribute(0, 2, VertexAttribPointerType.Float, false, 16, 0); + _vao.AddAttribute(1, 2, VertexAttribPointerType.Float, false, 16, 8); + _vao.Initialize(); - _pboIndex = mapPbo; - _pboReady = true; - return result; + float[] data = { -1, 1, 0, 1, -1, -1, 0, 0, 1, 1, 1, 1, 1, -1, 1, 0 }; + GL.BufferData( + BufferTarget.ArrayBuffer, + sizeof(float) * data.Length, + data, + BufferUsageHint.StaticDraw + ); } - public static void ReturnFrameBuffer(byte[] buf) + public void Draw(GLContext context, int tex) { - if (buf != null) - System.Buffers.ArrayPool.Shared.Return(buf); - } + Init(); + GL.Disable(EnableCap.Blend); + GL.Disable(EnableCap.CullFace); + GL.Disable(EnableCap.DepthTest); + GL.DepthMask(false); - #endregion + context.CurrentShader = _shader; + GL.ActiveTexture(TextureUnit.Texture0); + GL.BindTexture(TextureTarget.Texture2D, tex); + _shader.SetInt("uTex", 0); - public void Dispose() - { - DisposePBOs(); - _screen?.Dispoe(); - _final?.Dispoe(); - _selfShadow?.Dispose(); + _vao.Enable(_shader); + _vao.Use(); + GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4); + GL.BindTexture(TextureTarget.Texture2D, 0); + GL.UseProgram(0); + GL.DepthMask(true); } } @@ -508,14 +782,13 @@ void Init() _vao.AddAttribute(1, 2, VertexAttribPointerType.Float, false, 16, 8); _vao.Initialize(); - float[] data = - { - -1, 1, 0, 1, - -1, -1, 0, 0, - 1, 1, 1, 1, - 1, -1, 1, 0, - }; - GL.BufferData(BufferTarget.ArrayBuffer, sizeof(float) * data.Length, data, BufferUsageHint.StaticDraw); + float[] data = { -1, 1, 0, 1, -1, -1, 0, 0, 1, 1, 1, 1, 1, -1, 1, 0 }; + GL.BufferData( + BufferTarget.ArrayBuffer, + sizeof(float) * data.Length, + data, + BufferUsageHint.StaticDraw + ); } public void Draw(GLContext context, GLTexture color, bool keepAlpha) @@ -534,8 +807,16 @@ public void Draw(GLContext context, GLTexture color, bool keepAlpha) GL.ActiveTexture(TextureUnit.Texture1); color.Bind(); //Linear filtering does the supersample downsample. - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); + GL.TexParameter( + TextureTarget.Texture2D, + TextureParameterName.TextureMinFilter, + (int)TextureMinFilter.Linear + ); + GL.TexParameter( + TextureTarget.Texture2D, + TextureParameterName.TextureMagFilter, + (int)TextureMagFilter.Linear + ); _shader.SetInt("uColorTex", 1); _vao.Enable(_shader); diff --git a/PlayerViewer/UI/SelfShadowRenderer.cs b/PlayerViewer/UI/SelfShadowRenderer.cs index 25efb8e..8255c1b 100644 --- a/PlayerViewer/UI/SelfShadowRenderer.cs +++ b/PlayerViewer/UI/SelfShadowRenderer.cs @@ -1,7 +1,7 @@ using System; +using GLFrameworkEngine; using OpenTK; using OpenTK.Graphics.OpenGL; -using GLFrameworkEngine; using PlayerViewer.Player; namespace PlayerViewer.UI @@ -39,9 +39,19 @@ void Init() if (_lightFbo != null) return; - _lightFbo = new Framebuffer(FramebufferTarget.Framebuffer, - ShadowMapSize, ShadowMapSize, PixelInternalFormat.Rgba8, 1, useDepth: false); - _lightDepth = new DepthTexture(ShadowMapSize, ShadowMapSize, PixelInternalFormat.DepthComponent24); + _lightFbo = new Framebuffer( + FramebufferTarget.Framebuffer, + ShadowMapSize, + ShadowMapSize, + PixelInternalFormat.Rgba8, + 1, + useDepth: false + ); + _lightDepth = new DepthTexture( + ShadowMapSize, + ShadowMapSize, + PixelInternalFormat.DepthComponent24 + ); _lightFbo.AddAttachment(FramebufferAttachment.DepthAttachment, _lightDepth); //Match the game's cascade-map sampler: linear filtering with LEQUAL @@ -51,14 +61,26 @@ void Init() _lightDepth.MinFilter = TextureMinFilter.Linear; _lightDepth.MagFilter = TextureMagFilter.Linear; _lightDepth.UpdateParameters(); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureCompareMode, - (int)TextureCompareMode.CompareRefToTexture); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureCompareFunc, - (int)All.Lequal); + GL.TexParameter( + TextureTarget.Texture2D, + TextureParameterName.TextureCompareMode, + (int)TextureCompareMode.CompareRefToTexture + ); + GL.TexParameter( + TextureTarget.Texture2D, + TextureParameterName.TextureCompareFunc, + (int)All.Lequal + ); _lightDepth.Unbind(); - _prepassFbo = new Framebuffer(FramebufferTarget.Framebuffer, - 4, 4, PixelInternalFormat.Rgba8, 1, useDepth: false); + _prepassFbo = new Framebuffer( + FramebufferTarget.Framebuffer, + 4, + 4, + PixelInternalFormat.Rgba8, + 1, + useDepth: false + ); string frag = System.IO.File.ReadAllText("Shaders/SelfShadowPrepass.frag"); string vert = System.IO.File.ReadAllText("Shaders/SelfShadowPrepass.vert"); @@ -70,15 +92,14 @@ void Init() _vao.AddAttribute(1, 2, VertexAttribPointerType.Float, false, 16, 8); _vao.Initialize(); - float[] data = - { - -1, 1, 0, 1, - -1, -1, 0, 0, - 1, 1, 1, 1, - 1, -1, 1, 0, - }; + float[] data = { -1, 1, 0, 1, -1, -1, 0, 0, 1, 1, 1, 1, 1, -1, 1, 0 }; GL.BindBuffer(BufferTarget.ArrayBuffer, buffer); - GL.BufferData(BufferTarget.ArrayBuffer, sizeof(float) * data.Length, data, BufferUsageHint.StaticDraw); + GL.BufferData( + BufferTarget.ArrayBuffer, + sizeof(float) * data.Length, + data, + BufferUsageHint.StaticDraw + ); } /// @@ -103,7 +124,8 @@ public void RenderLightDepth(GLContext context, IViewScene scene, Vector4 boundi var camera = context.Camera; var savedView = camera.ViewMatrix; var savedProj = camera.ProjectionMatrix; - int savedWidth = context.Width, savedHeight = context.Height; + int savedWidth = context.Width, + savedHeight = context.Height; camera.SetCustomMatrices(view, proj); context.Width = ShadowMapSize; @@ -134,8 +156,13 @@ public void RenderLightDepth(GLContext context, IViewScene scene, Vector4 boundi /// /// Builds the screen-space prepass from the main camera's depth texture. /// - public void GeneratePrepass(GLContext context, DepthTexture sceneDepth, - Matrix4 camViewProj, int width, int height) + public void GeneratePrepass( + GLContext context, + DepthTexture sceneDepth, + Matrix4 camViewProj, + int width, + int height + ) { Init(); diff --git a/PlayerViewer/UI/Theme.cs b/PlayerViewer/UI/Theme.cs index 9b6fec9..fb72ecd 100644 --- a/PlayerViewer/UI/Theme.cs +++ b/PlayerViewer/UI/Theme.cs @@ -18,6 +18,10 @@ public static class Theme public static readonly Vector4 BgItemActive = new(0.28f, 0.24f, 0.15f, 1.00f); public static readonly Vector4 TextMain = new(0.92f, 0.91f, 0.88f, 1.00f); public static readonly Vector4 TextDim = new(0.55f, 0.54f, 0.52f, 1.00f); + public static readonly Vector4 Error = new(0.90f, 0.35f, 0.30f, 1.00f); + public static readonly Vector4 Success = new(0.40f, 0.85f, 0.40f, 1.00f); + public static readonly Vector4 RedButtonBg = new(0.55f, 0.12f, 0.10f, 1.00f); + public static readonly Vector4 RedButtonHover = new(0.70f, 0.16f, 0.13f, 1.00f); public static void Apply() { diff --git a/PlayerViewer/UI/VideoRecorder.cs b/PlayerViewer/UI/VideoRecorder.cs deleted file mode 100644 index f502f53..0000000 --- a/PlayerViewer/UI/VideoRecorder.cs +++ /dev/null @@ -1,229 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.IO; -using System.Threading; - -namespace PlayerViewer.UI -{ - /// - /// Records viewport frames to a video by piping raw RGBA into ffmpeg. - /// Encoding runs on a worker thread with a bounded frame queue so pipe - /// writes never block the render thread. - /// - public class VideoRecorder : IDisposable - { - const int MaxQueuedFrames = 30; - - readonly record struct CapturedFrame(byte[] Pixels, long Slot); - - Process _ffmpeg; - Stream _stdin; - Thread _worker; - BlockingCollection _queue; - int _width, _height; - int _fps; - int _droppedFrames; - Stopwatch _recClock; - long _lastCapturedSlot; - - public bool IsRecording { get; private set; } - public int FrameCount { get; private set; } - public string OutputPath { get; private set; } - - static string ResolveFfmpeg() - { - string local = Path.Combine(AppContext.BaseDirectory, "ffmpeg.exe"); - return File.Exists(local) ? local : "ffmpeg"; - } - - static bool? _ffmpegAvailable; - public static bool FfmpegAvailable - { - get - { - if (_ffmpegAvailable == null) - { - try - { - using var probe = Process.Start(new ProcessStartInfo - { - FileName = ResolveFfmpeg(), - Arguments = "-version", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }); - probe.WaitForExit(5000); - _ffmpegAvailable = true; - } - catch - { - _ffmpegAvailable = false; - } - } - return _ffmpegAvailable.Value; - } - } - - public bool Start(int width, int height, string outputPath, int fps = 60) - { - if (IsRecording || !FfmpegAvailable) - return false; - - _width = width & ~1; - _height = height & ~1; - OutputPath = outputPath; - FrameCount = 0; - _droppedFrames = 0; - - try - { - var psi = new ProcessStartInfo - { - FileName = ResolveFfmpeg(), - Arguments = $"-y -f rawvideo -pixel_format rgba -video_size {_width}x{_height} " + - $"-framerate {fps} -i pipe:0 -vf vflip -c:v libx264 -preset veryfast " + - $"-pix_fmt yuv420p -crf 16 \"{outputPath}\"", - UseShellExecute = false, - RedirectStandardInput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }; - _ffmpeg = Process.Start(psi); - _ffmpeg.BeginErrorReadLine(); - _stdin = _ffmpeg.StandardInput.BaseStream; - } - catch - { - _ffmpeg = null; - _stdin = null; - _ffmpegAvailable = false; - return false; - } - - _fps = fps; - _queue = new BlockingCollection(MaxQueuedFrames); - _worker = new Thread(EncodeLoop) { IsBackground = true, Name = "VideoEncode" }; - _worker.Start(); - - _recClock = Stopwatch.StartNew(); - _lastCapturedSlot = -1; - IsRecording = true; - return true; - } - - /// - /// Call BEFORE doing the GPU readback. Returns true when the next - /// timeline slot is due, meaning the caller should proceed with - /// and then - /// . When false, skip the readback entirely. - /// - public bool IsCaptureDue() - { - if (!IsRecording) - return false; - - long slot = (long)Math.Floor(_recClock.Elapsed.TotalSeconds * _fps); - return slot > _lastCapturedSlot; - } - - void EncodeLoop() - { - int frameBytes = _width * _height * 4; - byte[] previous = null; - long nextOutputSlot = 0; - try - { - foreach (var frame in _queue.GetConsumingEnumerable()) - { - while (previous != null && nextOutputSlot < frame.Slot) - { - _stdin.Write(previous, 0, frameBytes); - nextOutputSlot++; - } - - _stdin.Write(frame.Pixels, 0, frameBytes); - nextOutputSlot = frame.Slot + 1; - - if (previous != null) - ArrayPool.Shared.Return(previous); - previous = frame.Pixels; - } - - if (previous != null) - ArrayPool.Shared.Return(previous); - } - catch - { - if (previous != null) - ArrayPool.Shared.Return(previous); - while (_queue.TryTake(out var leftover)) - ArrayPool.Shared.Return(leftover.Pixels); - } - } - - /// - /// Queues one bottom-up RGBA frame. The buffer must come from - /// ArrayPool; ownership transfers to the encoder thread. - /// - public void PushFrame(byte[] rgba, int width, int height) - { - if (!IsRecording) - { - ArrayPool.Shared.Return(rgba); - return; - } - if ((width & ~1) != _width || (height & ~1) != _height) - { - ArrayPool.Shared.Return(rgba); - return; - } - - long slot = (long)Math.Floor(_recClock.Elapsed.TotalSeconds * _fps); - if (slot <= _lastCapturedSlot) - { - ArrayPool.Shared.Return(rgba); - return; - } - _lastCapturedSlot = slot; - - if (_queue.TryAdd(new CapturedFrame(rgba, slot))) - FrameCount++; - else - { - ArrayPool.Shared.Return(rgba); - _droppedFrames++; - } - } - - public void Stop() - { - if (!IsRecording) - return; - IsRecording = false; - - try - { - _queue.CompleteAdding(); - _worker.Join(15000); - _stdin?.Flush(); - _stdin?.Close(); - _ffmpeg?.WaitForExit(10000); - } - catch { } - if (_droppedFrames > 0) - Console.WriteLine($"[VideoRecorder] Encoder fell behind; dropped {_droppedFrames} frame(s)."); - _queue?.Dispose(); - _queue = null; - _worker = null; - _ffmpeg?.Dispose(); - _ffmpeg = null; - _stdin = null; - } - - public void Dispose() => Stop(); - } -} diff --git a/PlayerViewer/UI/ViewerWindow.cs b/PlayerViewer/UI/ViewerWindow.cs deleted file mode 100644 index 54ace55..0000000 --- a/PlayerViewer/UI/ViewerWindow.cs +++ /dev/null @@ -1,1213 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime; -using Vector2 = System.Numerics.Vector2; -using Vector4 = System.Numerics.Vector4; -using CafeStudio.UI; -using GLFrameworkEngine; -using ImGuiNET; -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Graphics.OpenGL; -using OpenTK.Input; -using PlayerViewer.Core; -using PlayerViewer.Player; - -namespace PlayerViewer.UI -{ - /// - /// Main interactive window: 3D viewport + player configuration UI. - /// - public class ViewerWindow : GameWindow - { - readonly AppConfig _config; - - ImGuiController _imgui; - ScenePipeline _pipeline; - Romfs _romfs; - GameDatabase _db; - PlayerScene _scene; - readonly VideoRecorder _recorder = new(); - - //--- UI state - string _romfsInput = ""; - string _sdodrInput = ""; - string _layeredInput = ""; - string _romfsError = null; - bool _needsLoad; - bool _preserveStateOnLoad; - string _animSearch = ""; - int _teamColorIndex; - int _teamIndex; - bool _useCustomTeamColor = true; - readonly TeamColorSet _customTeam = new() - { - Name = "Custom", - Alpha = new System.Numerics.Vector3(0.925f, 0.243f, 0.549f), - Bravo = new System.Numerics.Vector3(0.196f, 0.855f, 0.302f), - Charlie = new System.Numerics.Vector3(0.980f, 0.769f, 0.196f), - Neutral = new System.Numerics.Vector3(0.56f, 0.55f, 0.43f), - }; - float _uiFrame; //frame slider mirror - int _captureRes = 2; //index into CaptureSizes - bool _captureTransparent = true; - - static readonly (string Label, int W, int H)[] CaptureSizes = - { - ("1280 x 1280", 1280, 1280), - ("1920 x 1080", 1920, 1080), - ("3840 x 2160 (4K)", 3840, 2160), - ("2160 x 3840 (4K portrait)", 2160, 3840), - }; - - static readonly string[] PlayerTypes = - { - "Inkling Girl (Player00)", - "Inkling Boy (Player01)", - "Octoling Girl (Player02)", - "Octoling Boy (Player03)", - }; - - //--- Standalone model viewing (dropped/browsed files, outside the player) - StandaloneScene _standalone; - string _standaloneError; - - //--- dev self-test: save a window screenshot after N frames and exit - public string AutoScreenshotPath; - public int AutoScreenshotFrame = 40; - public string AutoOpenFile; //opens a standalone model right after load - public string AutoRecordPath; //records N frames of video then exits - public int AutoRecordFrames = 120; - int _frameCounter; - - //--- viewport camera input - bool _viewportHovered; - bool _mouseDown; - - public ViewerWindow(AppConfig config) - : base(config.WindowWidth, config.WindowHeight, - new GraphicsMode(new ColorFormat(32), 24, 8, 4, new ColorFormat(32), 2, false), - "Splatoon 3 Player Viewer", - GameWindowFlags.Default, DisplayDevice.Default, 3, 2, GraphicsContextFlags.Default) - { - _config = config; - _romfsInput = config.RomfsPath ?? ""; - _sdodrInput = config.SdodrRomfsPath ?? ""; - _layeredInput = config.LayeredFsPath ?? ""; - } - - protected override void OnLoad(EventArgs e) - { - base.OnLoad(e); - - _imgui = new ImGuiController(Width, Height); - Theme.Apply(); - ImGui.GetIO().ConfigWindowsMoveFromTitleBarOnly = true; - - RenderTools.Init(); - Toolbox.Core.FileManager.GetFileFormats(); - - //Interactive mode: compile uncached shader programs asynchronously so a - //new gear's shaders never stall the render thread (meshes pop in a few - //frames later instead). - BfresEditor.TegraShaderDecoder.AllowDeferredCompile = true; - - _pipeline = new ScenePipeline(); - _pipeline.Init(); - - if (Romfs.IsValidRoot(_config.RomfsPath)) - _needsLoad = true; - } - - void LoadGame() - { - _needsLoad = false; - var state = _preserveStateOnLoad && _scene != null ? SceneState.Capture(_scene) : null; - _preserveStateOnLoad = false; - try - { - _standalone?.Dispose(); - _standalone = null; - _scene?.Dispose(); - _scene = null; - CompactHeap(); - - _romfs = new Romfs(_config.RomfsPath, _config.LayeredFsPath, _config.UseLayeredFs, - _config.SdodrRomfsPath); - BfresEditor.HoianNXRender.GamePath = _config.RomfsPath; - //Decompress/parse the ~25MB UBER shader archive while the database loads. - BfresEditor.HoianNXRender.PrewarmShaderArchives(); - //Load default/cubemap textures now instead of during the first material render. - BfresEditor.HoianNXRender.InitTextures(); - _db = new GameDatabase(_romfs); - - _scene = new PlayerScene(_romfs, _db); - if (state != null) - { - state.Restore(_scene, _db); - _teamColorIndex = Math.Min(_teamColorIndex, Math.Max(_db.TeamColors.Count - 1, 0)); - } - else if (_config.Player?.Hair != null || _config.Player?.PlayerType != 0) - { - RestorePlayerConfig(); - } - else - { - _scene.SetPlayerType(0); - _pipeline.FramePlayer(); - } - ApplyTeamColor(); - _romfsError = null; - } - catch (Exception ex) - { - _romfsError = ex.Message; - Console.WriteLine($"[UI] Load failed: {ex}"); - } - } - - /// Snapshot of the scene configuration, reapplied after a LayeredFS reload. - class SceneState - { - int _playerType; - int _eye, _skin; - string _anim; - float _frame; - bool _paused; - readonly Dictionary _gear = new(); - - public static SceneState Capture(PlayerScene scene) - { - var s = new SceneState - { - _playerType = scene.PlayerType, - _eye = scene.EyeColor, - _skin = scene.SkinTone, - _anim = scene.CurrentAnimName, - _frame = scene.AnimFrame, - _paused = scene.AnimPaused, - }; - void Add(GearSlot slot, GearEntry e) - { - if (e != null) s._gear[slot] = (e.RowId, e.Variation, e.CustomPath); - else s._gear[slot] = (null, 0, null); - } - Add(GearSlot.Hair, scene.CurrentHair); - Add(GearSlot.Eyebrow, scene.CurrentEyebrow); - Add(GearSlot.Head, scene.CurrentHead); - Add(GearSlot.Clothes, scene.CurrentClothes); - Add(GearSlot.Bottom, scene.CurrentBottom); - Add(GearSlot.Shoes, scene.CurrentShoes); - Add(GearSlot.Tank, scene.CurrentTank); - Add(GearSlot.MainWeapon, scene.CurrentWeapon); - return s; - } - - public void Restore(PlayerScene scene, GameDatabase db) - { - scene.SetPlayerType(_playerType); - foreach (var (slot, gear) in _gear) - { - //Defaults set by SetPlayerType stand in when the row disappeared. - if (gear.RowId == null) - { - if (slot is GearSlot.Head or GearSlot.Clothes or GearSlot.Shoes or GearSlot.Tank or GearSlot.MainWeapon) - scene.SetGear(slot, null); - continue; - } - var entry = db.GetList(slot).FirstOrDefault(x => - x.RowId == gear.RowId && x.Variation == gear.Variation); - if (entry != null) - scene.SetGear(slot, entry); - } - scene.ApplyEyeColor(_eye); - scene.ApplySkinTone(_skin); - if (_anim != null) - { - scene.PlayAnim(_anim); - scene.SetAnimFrame(_frame); - } - scene.AnimPaused = _paused; - } - } - - protected override void OnResize(EventArgs e) - { - base.OnResize(e); - _imgui?.WindowResized(Width, Height); - } - - protected override void OnKeyPress(KeyPressEventArgs e) - { - base.OnKeyPress(e); - _imgui.PressChar(e.KeyChar); - } - - protected override void OnFileDrop(FileDropEventArgs e) - { - base.OnFileDrop(e); - string file = e.FileName; - if (file == null || (!file.EndsWith(".bfres") && !file.EndsWith(".bfres.zs") && !file.EndsWith(".zs"))) - return; - OpenStandalone(file); - } - - /// Opens a loose bfres as a standalone model (no player). - void OpenStandalone(string file) - { - if (_romfs == null) - return; - try - { - bool hadPrevious = _standalone != null; - _standalone?.Dispose(); - _standalone = null; - if (hadPrevious) - CompactHeap(); - - _standalone = StandaloneScene.FromFile(file, _romfs); - _standaloneError = _standalone == null ? "Failed to load model" : null; - if (_standalone != null) - { - _animSearch = ""; - _pipeline.FrameSphere(_standalone.GetBounding()); - } - } - catch (Exception ex) - { - _standaloneError = ex.Message; - Console.WriteLine($"[UI] Standalone load failed: {ex}"); - } - } - - void CloseStandalone() - { - _standalone?.Dispose(); - _standalone = null; - _standaloneError = null; - _animSearch = ""; - CompactHeap(); - _pipeline.FramePlayer(); - } - - static void CompactHeap() - { - GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; - GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true); - GC.WaitForPendingFinalizers(); - GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true); - } - - protected override void OnClosed(EventArgs e) - { - _recorder.Dispose(); - //Width/Height are 0 when closed while minimized; don't persist that. - if (WindowState == WindowState.Normal && Width > 0 && Height > 0) - { - _config.WindowWidth = Width; - _config.WindowHeight = Height; - } - _config.Save(); - base.OnClosed(e); - } - - protected override void OnRenderFrame(FrameEventArgs e) - { - base.OnRenderFrame(e); - GLFrameworkEngine.ShaderProgram.FrameStamp++; - - if (_needsLoad) - { - LoadGame(); - if (AutoOpenFile != null && _scene != null) - { - OpenStandalone(AutoOpenFile); - AutoOpenFile = null; - } - } - - //Advance whichever scene is active - if (_standalone != null) - { - _standalone.Update((float)e.Time); - _uiFrame = _standalone.AnimFrame; - } - else if (_scene != null) - { - _scene.Update((float)e.Time); - _uiFrame = _scene.AnimFrame; - } - - _imgui.Update(this, (float)e.Time); - DrawUI(); - - GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); - GL.Viewport(0, 0, Width, Height); - GL.ClearColor(0.04f, 0.04f, 0.05f, 1); - GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); - _imgui.Render(); - - if (AutoRecordPath != null && _scene != null) - { - if (!_recorder.IsRecording && _frameCounter < AutoRecordFrames) - StartRecording(AutoRecordPath); - else if (_recorder.IsRecording && _recorder.FrameCount >= AutoRecordFrames) - { - StopRecording(); - Console.WriteLine($"[UI] Recorded {AutoRecordPath}"); - AutoRecordPath = null; - if (AutoScreenshotPath == null) - Close(); - } - } - - if (AutoScreenshotPath != null && ++_frameCounter >= AutoScreenshotFrame) - { - if (ActiveScene != null) - { - using var dbg = _pipeline.Capture(ActiveScene, 512, 512, _pipeline.BackgroundColor, _captureTransparent); - dbg.Save(AutoScreenshotPath + ".capture.png"); - } - var pixels = new byte[Width * Height * 4]; - GL.ReadBuffer(ReadBufferMode.Back); - GL.ReadPixels(0, 0, Width, Height, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, pixels); - var bmp = Framebuffer.GetBitmap(Width, Height, pixels); - bmp.RotateFlip(System.Drawing.RotateFlipType.RotateNoneFlipY); - bmp.Save(AutoScreenshotPath); - Console.WriteLine($"[UI] Screenshot saved {AutoScreenshotPath}"); - Close(); - } - - SwapBuffers(); - - //Record after the frame is fully rendered. Check timing BEFORE the - //readback so we skip the GPU transfer entirely for frames we'd drop. - if (_recorder.IsCaptureDue()) - { - var pixels = _pipeline.ReadFinalPixelsAsync(out _); - if (pixels != null) - _recorder.PushFrame(pixels, _pipeline.Width, _pipeline.Height); - } - } - - #region UI layout - - void DrawUI() - { - var viewport = ImGui.GetMainViewport(); - ImGui.SetNextWindowPos(viewport.Pos); - ImGui.SetNextWindowSize(viewport.Size); - ImGui.Begin("##host", - ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | - ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoNavFocus | - ImGuiWindowFlags.MenuBar); - - DrawMenuBar(); - - if (_scene == null) - { - DrawRomfsSetup(); - ImGui.End(); - return; - } - - float leftWidth = 330; - float rightWidth = 300; - - ImGui.BeginChild("##left", new Vector2(leftWidth, 0), true); - if (_standalone != null) - DrawStandalonePanel(); - else - DrawPlayerPanel(); - ImGui.EndChild(); - - ImGui.SameLine(); - ImGui.BeginChild("##center", new Vector2(-rightWidth - 8, 0), false, - ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse); - DrawViewport(); - ImGui.EndChild(); - - ImGui.SameLine(); - ImGui.BeginChild("##right", new Vector2(0, 0), true); - DrawAnimationPanel(); - DrawCapturePanel(); - ImGui.EndChild(); - - ImGui.End(); - } - - void DrawMenuBar() - { - if (!ImGui.BeginMenuBar()) - return; - - ImGui.PushStyleColor(ImGuiCol.Text, Theme.Gold); - ImGui.Text("PLAYER VIEWER"); - ImGui.PopStyleColor(); - ImGui.Separator(); - - if (ImGui.BeginMenu("File")) - { - if (ImGui.MenuItem("Change romfs path...")) - { - _scene?.Dispose(); - _scene = null; - _romfsInput = _config.RomfsPath ?? ""; - _sdodrInput = _config.SdodrRomfsPath ?? ""; - } - if (ImGui.MenuItem("View model file... (or drag && drop)")) - { - string file = NativeFolderPicker.OpenFile("Open Model", "BFRES models (*.bfres;*.zs)", "*.bfres;*.zs"); - if (!string.IsNullOrEmpty(file)) - OpenStandalone(file); - } - if (_standalone != null && ImGui.MenuItem("Back to player")) - CloseStandalone(); - ImGui.Separator(); - if (ImGui.MenuItem("Exit")) - Close(); - ImGui.EndMenu(); - } - - if (_romfs != null) - { - ImGui.SameLine(ImGui.GetWindowWidth() - 320); - ImGui.TextColored(Theme.TextDim, TruncatePath(_config.RomfsPath, 42)); - } - - ImGui.EndMenuBar(); - } - - static string TruncatePath(string path, int max) - { - if (string.IsNullOrEmpty(path) || path.Length <= max) - return path ?? ""; - return "..." + path.Substring(path.Length - max + 3); - } - - void DrawRomfsSetup() - { - var avail = ImGui.GetContentRegionAvail(); - ImGui.SetCursorPos(new Vector2(avail.X / 2 - 260, avail.Y / 2 - 90)); - ImGui.BeginChild("##setup", new Vector2(520, 210), true); - - ImGui.PushStyleColor(ImGuiCol.Text, Theme.Gold); - ImGui.Text("Splatoon 3 romfs path"); - ImGui.PopStyleColor(); - ImGui.Spacing(); - - ImGui.SetNextItemWidth(-90); - ImGui.InputText("##romfs", ref _romfsInput, 512); - ImGui.SameLine(); - if (ImGui.Button("Browse...")) - { - string folder = NativeFolderPicker.SelectFolder("Select romfs folder", _romfsInput); - if (!string.IsNullOrEmpty(folder)) - _romfsInput = folder; - } - - ImGui.Spacing(); - bool valid = Romfs.IsValidRoot(_romfsInput); - if (!valid && !string.IsNullOrEmpty(_romfsInput)) - ImGui.TextColored(new Vector4(0.9f, 0.35f, 0.3f, 1), "Not a valid romfs (needs Model/ + RSDB/)"); - if (_romfsError != null) - ImGui.TextColored(new Vector4(0.9f, 0.35f, 0.3f, 1), _romfsError); - - ImGui.Spacing(); - ImGui.TextColored(new Vector4(0.6f, 0.6f, 0.6f, 1), "Side Order DLC romfs (optional)"); - ImGui.SetNextItemWidth(-90); - ImGui.InputText("##sdodr", ref _sdodrInput, 512); - ImGui.SameLine(); - if (ImGui.Button("Browse...##sdodr")) - { - string folder = NativeFolderPicker.SelectFolder("Select Side Order romfs folder", _sdodrInput); - if (!string.IsNullOrEmpty(folder)) - _sdodrInput = folder; - } - - ImGui.Spacing(); - if (!valid) ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.45f); - if (ImGui.Button("Load", new Vector2(120, 0)) && valid) - { - _config.RomfsPath = _romfsInput; - _config.SdodrRomfsPath = _sdodrInput; - _config.Save(); - _needsLoad = true; - } - if (!valid) ImGui.PopStyleVar(); - - ImGui.EndChild(); - } - - #endregion - - #region Player panel - - void DrawPlayerPanel() - { - Widgets.SectionHeader("Player"); - - if (ImGui.Button("Reset", new Vector2(-1, 0))) - ResetPlayerDefaults(); - - int playerType = _scene.PlayerType; - ImGui.SetNextItemWidth(-1); - if (ImGui.Combo("##playertype", ref playerType, PlayerTypes, PlayerTypes.Length)) - { - _scene.SetPlayerType(playerType); - ApplyTeamColor(); - SavePlayerConfig(); - } - - GearRow("Hair", GearSlot.Hair, _db.Hair, _scene.CurrentHair); - GearRow("Eyebrow", GearSlot.Eyebrow, _db.Eyebrow, _scene.CurrentEyebrow); - - int eye = _scene.EyeColor; - Widgets.LabeledRow("Eyes", () => - { - ImGui.SetNextItemWidth(-1); - if (ImGui.SliderInt("##eye", ref eye, 0, 20)) - { - _scene.ApplyEyeColor(eye); - SavePlayerConfig(); - } - }); - - int skin = _scene.SkinTone; - Widgets.LabeledRow("Skin", () => - { - ImGui.SetNextItemWidth(-1); - if (ImGui.SliderInt("##skin", ref skin, 0, 8)) - { - _scene.ApplySkinTone(skin); - SavePlayerConfig(); - } - }); - - bool hairPhys = _scene.HairPhysicsEnabled; - if (ImGui.Checkbox("Hair physics", ref hairPhys)) - { - _scene.HairPhysicsEnabled = hairPhys; - if (hairPhys) - _scene.ResetHairPhysics(); - } - - DrawTeamColorSection(); - - Widgets.SectionHeader("Gear"); - GearRow("Head", GearSlot.Head, _db.Head, _scene.CurrentHead, allowNone: false); - GearRow("Clothes", GearSlot.Clothes, _db.Clothes, _scene.CurrentClothes); - GearRow("Bottom", GearSlot.Bottom, _db.Bottom, _scene.CurrentBottom); - GearRow("Shoes", GearSlot.Shoes, _db.Shoes, _scene.CurrentShoes); - - Widgets.SectionHeader("Equipment"); - GearRow("Weapon", GearSlot.MainWeapon, _db.MainWeapons, _scene.CurrentWeapon, noneLabel: "Free"); - GearRow("Tank", GearSlot.Tank, _db.Tank, _scene.CurrentTank); - - DrawLightingSection(); - DrawViewSection(); - DrawLayeredFsSection(); - } - - static readonly string[] UniformSetLabels = { "Viewer", "AutoWalk" }; - static readonly string[] UniformSetDirs = { "SPL3", "SPL3_AutoWalk" }; - - void DrawViewSection() - { - Widgets.SectionHeader("View"); - if (ImGui.Button("Reset camera", new Vector2(-1, 0))) - { - if (_standalone != null) - _pipeline.FrameSphere(_standalone.GetBounding()); - else - _pipeline.FramePlayer(); - } - var bg = _pipeline.BackgroundColor; - if (ImGui.ColorEdit3("Background", ref bg, ImGuiColorEditFlags.NoInputs)) - _pipeline.BackgroundColor = bg; - - bool selfShadow = _pipeline.EnableSelfShadow; - if (ImGui.Checkbox("Self shadow", ref selfShadow)) - _pipeline.EnableSelfShadow = selfShadow; - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Game-accurate self shadowing (gsys_shadow_prepass)."); - - int setIdx = Math.Max(Array.IndexOf(UniformSetDirs, BfresEditor.HoianNXRender.UniformSetDir), 0); - Widgets.LabeledRow("Env", () => - { - ImGui.SetNextItemWidth(-1); - if (ImGui.Combo("##uniset", ref setIdx, UniformSetLabels, UniformSetLabels.Length)) - { - BfresEditor.HoianNXRender.SetUniformSet(UniformSetDirs[setIdx]); - ApplyTeamColor(); - } - }); - } - - void DrawLightingSection() - { - Widgets.SectionHeader("Lighting"); - bool followCam = _pipeline.LightFollowsCamera; - if (ImGui.Checkbox("Light follows camera", ref followCam)) - _pipeline.LightFollowsCamera = followCam; - if (!followCam) - { - float az = _pipeline.LightAzimuth, el = _pipeline.LightElevation; - ImGui.SetNextItemWidth(-1); - if (ImGui.SliderFloat("##lightaz", ref az, -180, 180, "Azimuth %.0f\u00b0")) - _pipeline.LightAzimuth = az; - ImGui.SetNextItemWidth(-1); - if (ImGui.SliderFloat("##lightel", ref el, -89, 89, "Elevation %.0f\u00b0")) - _pipeline.LightElevation = el; - } - } - - void DrawTeamColorSection() - { - Widgets.SectionHeader("Team Color"); - var colorSet = _db.TeamColors.ElementAtOrDefault(_teamColorIndex); - ImGui.SetNextItemWidth(-1); - string teamPreview = _useCustomTeamColor ? "Custom" : colorSet?.Name ?? "(default)"; - if (ImGui.BeginCombo("##teamcolor", teamPreview)) - { - //"Custom" first: freely picked colors instead of an RSDB set. - ImGui.ColorButton("##swatchCustA", new Vector4(_customTeam.Alpha.X, _customTeam.Alpha.Y, _customTeam.Alpha.Z, 1), - ImGuiColorEditFlags.NoTooltip, new Vector2(14, 14)); - ImGui.SameLine(); - ImGui.ColorButton("##swatchCustB", new Vector4(_customTeam.Bravo.X, _customTeam.Bravo.Y, _customTeam.Bravo.Z, 1), - ImGuiColorEditFlags.NoTooltip, new Vector2(14, 14)); - ImGui.SameLine(); - if (ImGui.Selectable("Custom", _useCustomTeamColor)) - { - _useCustomTeamColor = true; - ApplyTeamColor(); - SavePlayerConfig(); - } - - for (int i = 0; i < _db.TeamColors.Count; i++) - { - var set = _db.TeamColors[i]; - //Swatch preview - ImGui.ColorButton($"##swatchA{i}", new Vector4(set.Alpha.X, set.Alpha.Y, set.Alpha.Z, 1), - ImGuiColorEditFlags.NoTooltip, new Vector2(14, 14)); - ImGui.SameLine(); - ImGui.ColorButton($"##swatchB{i}", new Vector4(set.Bravo.X, set.Bravo.Y, set.Bravo.Z, 1), - ImGuiColorEditFlags.NoTooltip, new Vector2(14, 14)); - ImGui.SameLine(); - if (ImGui.Selectable(set.Name, !_useCustomTeamColor && i == _teamColorIndex)) - { - _useCustomTeamColor = false; - _teamColorIndex = i; - ApplyTeamColor(); - SavePlayerConfig(); - } - } - ImGui.EndCombo(); - } - if (_useCustomTeamColor) - { - bool custChanged = false; - var a = _customTeam.Alpha; var b = _customTeam.Bravo; var c = _customTeam.Charlie; - custChanged |= ImGui.ColorEdit3("Alpha##cust", ref a, ImGuiColorEditFlags.NoInputs); - ImGui.SameLine(); - custChanged |= ImGui.ColorEdit3("Bravo##cust", ref b, ImGuiColorEditFlags.NoInputs); - ImGui.SameLine(); - custChanged |= ImGui.ColorEdit3("Charlie##cust", ref c, ImGuiColorEditFlags.NoInputs); - if (custChanged) - { - _customTeam.Alpha = a; _customTeam.Bravo = b; _customTeam.Charlie = c; - _customTeam.Neutral = (a + b) * 0.5f; - ApplyTeamColor(); - SavePlayerConfig(); - } - } - if (ImGui.RadioButton("Alpha", _teamIndex == 0)) { _teamIndex = 0; ApplyTeamColor(); SavePlayerConfig(); } - ImGui.SameLine(); - if (ImGui.RadioButton("Bravo", _teamIndex == 1)) { _teamIndex = 1; ApplyTeamColor(); SavePlayerConfig(); } - ImGui.SameLine(); - if (ImGui.RadioButton("Charlie", _teamIndex == 2)) { _teamIndex = 2; ApplyTeamColor(); SavePlayerConfig(); } - } - - void DrawLayeredFsSection() - { - Widgets.SectionHeader("LayeredFS (mods)"); - - ImGui.SetNextItemWidth(-70); - if (ImGui.InputText("##layeredpath", ref _layeredInput, 512)) - { - _config.LayeredFsPath = _layeredInput; - _config.Save(); - } - ImGui.SameLine(); - if (ImGui.Button("...##layeredbrowse", new Vector2(-1, 0))) - { - string folder = NativeFolderPicker.SelectFolder("Select LayeredFS (mod) folder", _layeredInput); - if (!string.IsNullOrEmpty(folder)) - { - _layeredInput = folder; - _config.LayeredFsPath = folder; - _config.Save(); - } - } - - bool useLayered = _config.UseLayeredFs; - if (ImGui.Checkbox("Enable LayeredFS", ref useLayered)) - { - _config.UseLayeredFs = useLayered; - _config.Save(); - _preserveStateOnLoad = true; - _needsLoad = true; - } - - bool dirOk = !string.IsNullOrEmpty(_layeredInput) && Directory.Exists(_layeredInput); - if (!string.IsNullOrEmpty(_layeredInput) && !dirOk) - ImGui.TextColored(new Vector4(0.9f, 0.35f, 0.3f, 1), "folder not found"); - else if (_romfs != null && _romfs.UseLayered) - ImGui.TextColored(new Vector4(0.4f, 0.85f, 0.4f, 1), "active"); - - if (ImGui.Button("Reload", new Vector2(-1, 0))) - { - _preserveStateOnLoad = true; - _needsLoad = true; - } - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Reload everything from the current romfs + LayeredFS.\nKeeps the current player configuration."); - } - - void GearRow(string label, GearSlot slot, List entries, GearEntry current, - bool allowNone = true, string noneLabel = "Blank") - { - Widgets.LabeledRow(label, () => - { - if (Widgets.GearCombo(label, entries, current, out var selected, allowNone, noneLabel)) - { - _scene.SetGear(slot, selected); - SavePlayerConfig(); - } - }); - } - - void ApplyTeamColor() - { - var set = _useCustomTeamColor ? _customTeam : _db?.TeamColors.ElementAtOrDefault(_teamColorIndex); - if (set != null && _scene != null) - _scene.ApplyTeamColor(set, _teamIndex); - } - - void ResetPlayerDefaults() - { - _scene.CurrentHair = null; - _scene.CurrentEyebrow = null; - _scene.CurrentHead = null; - _scene.CurrentClothes = null; - _scene.CurrentBottom = null; - _scene.CurrentShoes = null; - _scene.CurrentTank = null; - _scene.CurrentWeapon = null; - _scene.EyeColor = 0; - _scene.SkinTone = 0; - _scene.SetPlayerType(0); - - _teamColorIndex = 0; - _teamIndex = 0; - _useCustomTeamColor = true; - ApplyTeamColor(); - - _pipeline.FramePlayer(); - SavePlayerConfig(); - } - - void SavePlayerConfig() - { - if (_scene == null) return; - var p = _config.Player; - p.PlayerType = _scene.PlayerType; - p.EyeColor = _scene.EyeColor; - p.SkinTone = _scene.SkinTone; - static void SaveGear(GearEntry e, out string rowId, out int variation) - { - rowId = e?.RowId; - variation = e?.Variation ?? 0; - } - SaveGear(_scene.CurrentHair, out p.Hair, out p.HairVariation); - SaveGear(_scene.CurrentEyebrow, out p.Eyebrow, out p.EyebrowVariation); - SaveGear(_scene.CurrentHead, out p.Head, out p.HeadVariation); - SaveGear(_scene.CurrentClothes, out p.Clothes, out p.ClothesVariation); - SaveGear(_scene.CurrentBottom, out p.Bottom, out p.BottomVariation); - SaveGear(_scene.CurrentShoes, out p.Shoes, out p.ShoesVariation); - SaveGear(_scene.CurrentTank, out p.Tank, out p.TankVariation); - SaveGear(_scene.CurrentWeapon, out p.Weapon, out p.WeaponVariation); - p.TeamColorIndex = _teamColorIndex; - p.TeamIndex = _teamIndex; - p.UseCustomTeamColor = _useCustomTeamColor; - p.CustomAlpha = new[] { _customTeam.Alpha.X, _customTeam.Alpha.Y, _customTeam.Alpha.Z }; - p.CustomBravo = new[] { _customTeam.Bravo.X, _customTeam.Bravo.Y, _customTeam.Bravo.Z }; - p.CustomCharlie = new[] { _customTeam.Charlie.X, _customTeam.Charlie.Y, _customTeam.Charlie.Z }; - _config.Save(); - } - - void RestorePlayerConfig() - { - var p = _config.Player; - if (p == null) return; - - GearEntry FindGear(List list, string rowId, int variation) - { - if (rowId == null) return null; - return list.FirstOrDefault(x => x.RowId == rowId && x.Variation == variation) - ?? list.FirstOrDefault(x => x.RowId == rowId); - } - - _scene.EyeColor = p.EyeColor; - _scene.SkinTone = p.SkinTone; - _scene.CurrentHair = FindGear(_db.Hair, p.Hair, p.HairVariation); - _scene.CurrentEyebrow = FindGear(_db.Eyebrow, p.Eyebrow, p.EyebrowVariation); - _scene.CurrentHead = FindGear(_db.Head, p.Head, p.HeadVariation); - _scene.CurrentClothes = FindGear(_db.Clothes, p.Clothes, p.ClothesVariation); - _scene.CurrentBottom = FindGear(_db.Bottom, p.Bottom, p.BottomVariation); - _scene.CurrentShoes = FindGear(_db.Shoes, p.Shoes, p.ShoesVariation); - _scene.CurrentTank = FindGear(_db.Tank, p.Tank, p.TankVariation); - _scene.CurrentWeapon = FindGear(_db.MainWeapons, p.Weapon, p.WeaponVariation); - _scene.SetPlayerType(p.PlayerType); - - _teamColorIndex = p.TeamColorIndex; - _teamIndex = p.TeamIndex; - _useCustomTeamColor = p.UseCustomTeamColor; - if (p.CustomAlpha is { Length: 3 }) - _customTeam.Alpha = new System.Numerics.Vector3(p.CustomAlpha[0], p.CustomAlpha[1], p.CustomAlpha[2]); - if (p.CustomBravo is { Length: 3 }) - _customTeam.Bravo = new System.Numerics.Vector3(p.CustomBravo[0], p.CustomBravo[1], p.CustomBravo[2]); - if (p.CustomCharlie is { Length: 3 }) - _customTeam.Charlie = new System.Numerics.Vector3(p.CustomCharlie[0], p.CustomCharlie[1], p.CustomCharlie[2]); - _customTeam.Neutral = (_customTeam.Alpha + _customTeam.Bravo) * 0.5f; - ApplyTeamColor(); - - _scene.ApplyEyeColor(p.EyeColor); - _scene.ApplySkinTone(p.SkinTone); - } - - #endregion - - #region Viewport - - Player.IViewScene ActiveScene => _standalone != null ? _standalone : _scene; - - void DrawViewport() - { - var size = ImGui.GetContentRegionAvail(); - if (!_recorder.IsRecording) - _pipeline.Resize((int)size.X, (int)size.Y); - - _pipeline.Render(ActiveScene); - - var pos = ImGui.GetCursorScreenPos(); - ImGui.Image((IntPtr)_pipeline.ViewportTextureId, new Vector2(_pipeline.Width, _pipeline.Height), - new Vector2(0, 1), new Vector2(1, 0)); - - _viewportHovered = ImGui.IsItemHovered(); - UpdateCameraInput(pos); - - //Recording indicator overlay - if (_recorder.IsRecording) - { - var draw = ImGui.GetWindowDrawList(); - draw.AddCircleFilled(new Vector2(pos.X + 18, pos.Y + 18), 7, - ImGui.GetColorU32(new Vector4(0.9f, 0.15f, 0.15f, 1))); - var cursor = ImGui.GetCursorPos(); - ImGui.SetCursorScreenPos(new Vector2(pos.X + 32, pos.Y + 10)); - ImGui.TextColored(new Vector4(1, 1, 1, 1), $"REC {_recorder.FrameCount / 60.0f:F1}s"); - ImGui.SetCursorPos(cursor); - } - } - - void UpdateCameraInput(Vector2 viewportScreenPos) - { - var io = ImGui.GetIO(); - var cam = _pipeline.Camera; - bool changed = false; - - bool leftDown = ImGui.IsMouseDown(ImGuiMouseButton.Left); - bool rightDown = ImGui.IsMouseDown(ImGuiMouseButton.Right); - bool midDown = ImGui.IsMouseDown(ImGuiMouseButton.Middle); - bool anyDown = leftDown || rightDown || midDown; - - //Drags only start inside the viewport, but keep tracking outside it. - if (_viewportHovered && anyDown && !_mouseDown) - _mouseDown = true; - if (!anyDown) - _mouseDown = false; - - if (_mouseDown) - { - var delta = io.MouseDelta; - if (midDown || (leftDown && io.KeyShift)) - { - //Pan, scaled so the model roughly follows the cursor. - float scale = (float)Math.Sin(cam.Fov) * cam.TargetDistance; - float dx = -delta.X / Math.Max(1, cam.Width) * scale; - float dy = delta.Y / Math.Max(1, cam.Height) * scale; - var rot = cam.InverseRotationMatrix; - cam.TargetPosition += rot.Row0 * dx + rot.Row1 * dy; - changed = true; - } - else if (leftDown || rightDown) - { - //Orbit around the target. - cam.RotationY += delta.X * 0.008f; - cam.RotationX += delta.Y * 0.008f; - cam.RotationX = MathHelper.Clamp(cam.RotationX, - -MathHelper.PiOver2 + 0.01f, MathHelper.PiOver2 - 0.01f); - changed = true; - } - } - - if (_viewportHovered && io.MouseWheel != 0) - { - cam.TargetDistance = Math.Max(0.05f, - cam.TargetDistance * (1.0f - io.MouseWheel * 0.12f)); - changed = true; - } - - //WASD pans in camera space (W/S = forward/back, A/D = left/right). - if (!io.WantTextInput && Focused) - { - var kb = Keyboard.GetState(); - float move = cam.TargetDistance * io.DeltaTime; - var dir = OpenTK.Vector3.Zero; - var rot = cam.InverseRotationMatrix; - if (kb.IsKeyDown(Key.W)) dir -= rot.Row2; - if (kb.IsKeyDown(Key.S)) dir += rot.Row2; - if (kb.IsKeyDown(Key.A)) dir -= rot.Row0; - if (kb.IsKeyDown(Key.D)) dir += rot.Row0; - if (kb.IsKeyDown(Key.Space)) dir += rot.Row1; - if (kb.IsKeyDown(Key.ShiftLeft) || kb.IsKeyDown(Key.ShiftRight)) dir -= rot.Row1; - if (dir != OpenTK.Vector3.Zero) - { - cam.TargetPosition += dir * move; - changed = true; - } - } - - if (changed) - cam.UpdateMatrices(); - } - - #endregion - - #region Animation + capture panels - - void DrawAnimationPanel() - { - Widgets.SectionHeader("Animation"); - - //Both scene types expose the same playback surface; bridge through locals. - bool standalone = _standalone != null; - string currentAnim = standalone ? _standalone.CurrentAnimName : _scene.CurrentAnimName; - bool paused = standalone ? _standalone.AnimPaused : _scene.AnimPaused; - float speed = standalone ? _standalone.AnimSpeed : _scene.AnimSpeed; - float rawFrameCount = standalone - ? _standalone.CurrentSkeletal?.FrameCount ?? 1 - : _scene.CurrentSkeletal?.FrameCount ?? 1; - List animNames = standalone ? _standalone.AnimNames : _scene.Anims.AnimNames; - - void SetPaused(bool value) { if (standalone) _standalone.AnimPaused = value; else _scene.AnimPaused = value; } - void SetSpeed(float value) { if (standalone) _standalone.AnimSpeed = value; else _scene.AnimSpeed = value; } - void SetFrame(float value) { if (standalone) _standalone.SetAnimFrame(value); else _scene.SetAnimFrame(value); } - void Play(string name) { if (standalone) _standalone.PlayAnim(name); else _scene.PlayAnim(name); } - - ImGui.TextColored(Theme.GoldBright, currentAnim ?? "(none)"); - - if (ImGui.Button(paused ? " Play " : " Pause ")) - SetPaused(!paused); - ImGui.SameLine(); - ImGui.SetNextItemWidth(-1); - if (ImGui.SliderFloat("##speed", ref speed, 0.1f, 2.0f, "speed %.2fx")) - SetSpeed(speed); - - float frameCount = Math.Max(rawFrameCount - 1, 1); - ImGui.SetNextItemWidth(-1); - if (ImGui.SliderFloat("##frame", ref _uiFrame, 0, frameCount, "frame %.0f")) - { - SetFrame(_uiFrame); - SetPaused(true); - } - - ImGui.Spacing(); - ImGui.AlignTextToFramePadding(); - ImGui.TextColored(Theme.TextDim, "Search"); - ImGui.SameLine(); - ImGui.SetNextItemWidth(-1); - ImGui.InputText("##animsearch", ref _animSearch, 64); - - var avail = ImGui.GetContentRegionAvail(); - ImGui.BeginChild("##animlist", new Vector2(0, Math.Max(avail.Y - 205, 120)), true); - if (animNames.Count == 0) - ImGui.TextColored(Theme.TextDim, "no skeletal animations"); - if (standalone && animNames.Count > 0) - { - if (ImGui.Selectable("", currentAnim == null)) - { - Play(null); - SetPaused(true); - } - } - foreach (var name in animNames) - { - if (!string.IsNullOrEmpty(_animSearch) && - !name.Contains(_animSearch, StringComparison.OrdinalIgnoreCase)) - continue; - bool isCurrent = name == currentAnim; - if (ImGui.Selectable(name, isCurrent)) - { - Play(name); - SetPaused(false); - } - } - ImGui.EndChild(); - } - - void DrawCapturePanel() - { - Widgets.SectionHeader("Capture"); - - ImGui.SetNextItemWidth(-1); - if (ImGui.BeginCombo("##capres", CaptureSizes[_captureRes].Label)) - { - for (int i = 0; i < CaptureSizes.Length; i++) - if (ImGui.Selectable(CaptureSizes[i].Label, i == _captureRes)) - _captureRes = i; - ImGui.EndCombo(); - } - ImGui.Checkbox("Transparent background", ref _captureTransparent); - - if (ImGui.Button("Screenshot (PNG)", new Vector2(-1, 0))) - SaveScreenshot(); - - ImGui.Spacing(); - if (!_recorder.IsRecording) - { - ImGui.Checkbox("Video greenscreen", ref _recordGreenscreen); - bool haveFfmpeg = VideoRecorder.FfmpegAvailable; - if (!haveFfmpeg) ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.45f); - if (ImGui.Button("Record video", new Vector2(-1, 0)) && haveFfmpeg) - StartRecording(); - if (!haveFfmpeg) - { - ImGui.PopStyleVar(); - ImGui.TextColored(Theme.TextDim, "ffmpeg not found (exe folder or PATH)"); - } - } - else - { - ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(0.55f, 0.12f, 0.10f, 1)); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(0.70f, 0.16f, 0.13f, 1)); - //"###" keeps the widget ID stable while the timer in the label changes, - //otherwise the click never registers (ID differs between press/release). - if (ImGui.Button($"Stop recording ({_recorder.FrameCount / 60.0f:F1}s)###stoprec", new Vector2(-1, 0))) - StopRecording(); - ImGui.PopStyleColor(2); - } - } - - void SaveScreenshot() - { - string path = NativeFolderPicker.SaveFile("Save Screenshot", "player.png", "PNG image (*.png)", "*.png"); - if (string.IsNullOrEmpty(path)) - return; - if (!path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) - path += ".png"; - - var (_, w, h) = CaptureSizes[_captureRes]; - using var bmp = _pipeline.Capture(ActiveScene, w, h, _pipeline.BackgroundColor, _captureTransparent); - bmp.Save(path, System.Drawing.Imaging.ImageFormat.Png); - Console.WriteLine($"[UI] Saved {path}"); - } - - bool _recordGreenscreen = true; - System.Numerics.Vector3 _backgroundBeforeRecord; - - void StartRecording() - { - string path = NativeFolderPicker.SaveFile("Save Video", "player.mp4", "MP4 video (*.mp4)", "*.mp4"); - if (string.IsNullOrEmpty(path)) - return; - if (!path.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase)) - path += ".mp4"; - StartRecording(path); - } - - void StartRecording(string path) - { - _backgroundBeforeRecord = _pipeline.BackgroundColor; - if (_recordGreenscreen) - _pipeline.BackgroundColor = new System.Numerics.Vector3(0, 1, 0); - if (!_recorder.Start(_pipeline.Width, _pipeline.Height, path)) - _pipeline.BackgroundColor = _backgroundBeforeRecord; - } - - void StopRecording() - { - //Flush the last PBO frame that the async readback is holding. - var last = _pipeline.ReadFinalPixelsAsync(out _); - if (last != null) - _recorder.PushFrame(last, _pipeline.Width, _pipeline.Height); - _recorder.Stop(); - if (_recordGreenscreen) - _pipeline.BackgroundColor = _backgroundBeforeRecord; - } - - #endregion - - #region Standalone panel - - void DrawStandalonePanel() - { - Widgets.SectionHeader("Standalone Model"); - - ImGui.TextColored(Theme.GoldBright, _standalone.Name); - ImGui.PushTextWrapPos(); - ImGui.TextColored(Theme.TextDim, _standalone.SourcePath); - ImGui.PopTextWrapPos(); - if (_standaloneError != null) - ImGui.TextColored(new Vector4(0.9f, 0.35f, 0.3f, 1), _standaloneError); - - ImGui.Spacing(); - if (ImGui.Button("Back to player", new Vector2(-1, 0))) - { - CloseStandalone(); - return; - } - if (ImGui.Button("Frame model", new Vector2(-1, 0))) - _pipeline.FrameSphere(_standalone.GetBounding()); - - var models = _standalone.Render.Models.OfType().ToList(); - Widgets.SectionHeader("Models"); - for (int mi = 0; mi < models.Count; mi++) - { - var model = models[mi]; - bool visible = model.IsVisible; - if (ImGui.Checkbox($"##{mi}_vis", ref visible)) - model.IsVisible = visible; - ImGui.SameLine(); - if (ImGui.TreeNode($"{model.ModelData.Name}##{mi}")) - { - foreach (var mesh in model.Meshes) - { - bool meshVis = mesh.Shape.IsVisible; - if (ImGui.Checkbox($"{mesh.Name}##{mi}_{mesh.Name}", ref meshVis)) - mesh.Shape.IsVisible = meshVis; - } - ImGui.TreePop(); - } - } - - DrawLightingSection(); - DrawTeamColorSection(); - DrawViewSection(); - } - - #endregion - } -} diff --git a/PlayerViewer/UI/ViewerWindow/ViewerWindow.AnimChain.cs b/PlayerViewer/UI/ViewerWindow/ViewerWindow.AnimChain.cs new file mode 100644 index 0000000..022c18b --- /dev/null +++ b/PlayerViewer/UI/ViewerWindow/ViewerWindow.AnimChain.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ImGuiNET; +using Vector2 = System.Numerics.Vector2; +using Vector4 = System.Numerics.Vector4; + +namespace PlayerViewer.UI +{ + // Animation chaining: play (and export) a sequence of skeletal animations as one continuous + // take. The chain drives a single global frame cursor over the concatenated steps; crossing a + // step boundary rebinds the next animation WITHOUT resetting the cloth sim, so hair flows + // continuously the way it does across a normal loop wrap. Hair is reset only once at the start. + // Preview is pure playback; export reuses the same seek so it is deterministic and frame-exact. + public partial class ViewerWindow + { + readonly List _animChain = new(); + int _animMode; //0 = Single, 1 = Sequence + bool _chainActive; //interactive preview is playing + bool _chainLoop; + int _chainIndex = -1; //step currently bound (shared by preview + export) + int _chainSelected = -1; //timeline segment selected in the editor + float _chainCursor; //global frame cursor for the interactive preview + + float ChainTotalFrames() => + _animChain.Sum(n => (float)Math.Max(PlaybackFrameCountOf(n), 1)); + + //Binds the step containing global frame g (rebinding only on a boundary, without a hair + //reset) and positions its local frame. The caller runs the scene Update afterwards. + void ChainSeek(float g) + { + if (_animChain.Count == 0) + return; + int i = 0; + float acc = 0; + for (; i < _animChain.Count - 1; i++) + { + float fc = Math.Max(PlaybackFrameCountOf(_animChain[i]), 1); + if (g < acc + fc) + break; + acc += fc; + } + if (i != _chainIndex) + { + PlaybackPlay(_animChain[i], resetHair: false); + PlaybackSetPaused(true); + _chainIndex = i; + } + float localEnd = Math.Max(PlaybackFrameCountOf(_animChain[i]) - 1, 0); + PlaybackSetFrame(Math.Min(g - acc, localEnd)); + } + + //Freshly starts a chain run: force a rebind on the first seek, pause the scene's own + //advance (the chain drives frames), and reset the cloth once so the take is reproducible. + void BeginChain() + { + _chainIndex = -1; + PlaybackSetPaused(true); + PlaybackResetHair(); + } + + void StartAnimChainPreview() + { + if (_animChain.Count == 0 || ActiveScene == null) + return; + BeginChain(); + _chainCursor = 0f; + _chainActive = true; + } + + void StopAnimChain() => _chainActive = false; + + //Interactive preview advance; called from OnRenderFrame while a preview is active. + void UpdateAnimChain(float dt) + { + if (_animChain.Count == 0) + { + _chainActive = false; + return; + } + float total = ChainTotalFrames(); + _chainCursor += dt * 60f * PlaybackSpeed; + if (_chainCursor >= total) + { + if (_chainLoop) + _chainCursor %= Math.Max(total, 1f); + else + { + _chainActive = false; + ChainSeek(Math.Max(total - 1, 0)); + PlaybackUpdate(dt); + return; + } + } + ChainSeek(_chainCursor); + PlaybackUpdate(dt); + } + + //--- Sidebar UI (Sequence mode) -------------------------------------------------------- + + void DrawModeTabs() + { + Widgets.SectionHeader("Animation source"); + if (ImGui.RadioButton("Single", _animMode == 0)) + { + _animMode = 0; + SaveCaptureSettings(); + } + ImGui.SameLine(); + if (ImGui.RadioButton("Sequence", _animMode == 1)) + { + _animMode = 1; + SaveCaptureSettings(); + } + } + + void DrawSequencePanel() + { + DrawChainTimeline(); + + string cur = PlaybackCurrentAnim; + Widgets.DisabledButton( + "+ Add current", + !string.IsNullOrEmpty(cur), + () => + { + _animChain.Add(cur); + _chainSelected = _animChain.Count - 1; + } + ); + + bool hasSel = _chainSelected >= 0 && _chainSelected < _animChain.Count; + if (!hasSel) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.45f); + if (ImGui.Button("Move <") && hasSel && _chainSelected > 0) + { + (_animChain[_chainSelected - 1], _animChain[_chainSelected]) = ( + _animChain[_chainSelected], + _animChain[_chainSelected - 1] + ); + _chainSelected--; + } + ImGui.SameLine(); + if (ImGui.Button("Move >") && hasSel && _chainSelected < _animChain.Count - 1) + { + (_animChain[_chainSelected + 1], _animChain[_chainSelected]) = ( + _animChain[_chainSelected], + _animChain[_chainSelected + 1] + ); + _chainSelected++; + } + ImGui.SameLine(); + if (ImGui.Button("Remove") && hasSel) + { + _animChain.RemoveAt(_chainSelected); + if (_chainSelected >= _animChain.Count) + _chainSelected = _animChain.Count - 1; + if (_chainIndex >= _animChain.Count) + _chainIndex = _animChain.Count - 1; + } + if (!hasSel) + ImGui.PopStyleVar(); + ImGui.SameLine(); + if (ImGui.Button("Clear")) + { + _animChain.Clear(); + _chainSelected = -1; + StopAnimChain(); + } + + ImGui.Checkbox("Loop", ref _chainLoop); + ImGui.SameLine(); + if (_chainActive) + Widgets.RedButton("Stop preview", StopAnimChain); + else + Widgets.DisabledButton("Preview", _animChain.Count > 0, StartAnimChainPreview); + } + + //Proportional timeline: segments sized by each step's length (drawn on the window draw + //list), labels overlaid as ImGui text (this ImGui.NET build's draw list has no AddText), + //and a live playhead while previewing or exporting. Click a segment to select it. + void DrawChainTimeline() + { + const float height = 46f; + var origin = ImGui.GetCursorScreenPos(); + float width = ImGui.GetContentRegionAvail().X; + ImGui.InvisibleButton("##chaintimeline", new Vector2(width, height)); + bool clicked = ImGui.IsItemClicked(); + var afterStrip = ImGui.GetCursorScreenPos(); + + var draw = ImGui.GetWindowDrawList(); + draw.AddRectFilled( + origin, + origin + new Vector2(width, height), + ImGui.GetColorU32(new Vector4(0.10f, 0.11f, 0.13f, 1)), + 4f + ); + + var labelCol = new Vector4(0.92f, 0.92f, 0.95f, 1); + void Label(float lx, string text, float maxW) + { + if (maxW <= 24) + return; + ImGui.SetCursorScreenPos(new Vector2(lx, origin.Y + height / 2 - 8)); + ImGui.TextColored(labelCol, FitLabel(text, maxW - 10)); + } + + if (_animChain.Count == 0) + { + Label(origin.X + 5, "empty; preview an animation then + Add current", width); + ImGui.SetCursorScreenPos(afterStrip); + return; + } + + int[] frames = _animChain.Select(n => Math.Max(PlaybackFrameCountOf(n), 1)).ToArray(); + float total = frames.Sum(); + bool running = _chainActive || (_animExporting && _animExportChain); + + uint segA = ImGui.GetColorU32(new Vector4(0.22f, 0.24f, 0.30f, 1)); + uint segB = ImGui.GetColorU32(new Vector4(0.27f, 0.29f, 0.36f, 1)); + uint segActive = ImGui.GetColorU32(new Vector4(0.55f, 0.42f, 0.12f, 1)); + uint outline = ImGui.GetColorU32(new Vector4(1, 1, 1, 0.85f)); + + float x = origin.X; + for (int i = 0; i < _animChain.Count; i++) + { + float w = width * frames[i] / total; + var a = new Vector2(x + 1, origin.Y + 2); + var b = new Vector2(x + w - 1, origin.Y + height - 2); + draw.AddRectFilled( + a, + b, + running && i == _chainIndex ? segActive : (i % 2 == 0 ? segA : segB), + 3f + ); + if (i == _chainSelected) + draw.AddRect(a, b, outline, 3f); + x += w; + } + + float? cursor = _chainActive + ? _chainCursor + : (_animExporting && _animExportChain ? _animExportIndex : null); + if (cursor.HasValue) + { + float px = origin.X + width * Math.Min(cursor.Value, total) / total; + draw.AddLine( + new Vector2(px, origin.Y), + new Vector2(px, origin.Y + height), + outline, + 2f + ); + } + + x = origin.X; + for (int i = 0; i < _animChain.Count; i++) + { + float w = width * frames[i] / total; + Label(x + 5, _animChain[i], w); + x += w; + } + ImGui.SetCursorScreenPos(afterStrip); + + if (clicked) + { + float mx = ImGui.GetMousePos().X - origin.X, + acc = 0; + for (int i = 0; i < _animChain.Count; i++) + { + float w = width * frames[i] / total; + if (mx >= acc && mx < acc + w) + { + _chainSelected = i; + break; + } + acc += w; + } + } + } + + //Truncates a label with ".." so it fits maxW pixels (segment width). + static string FitLabel(string s, float maxW) + { + if (maxW <= 0) + return ""; + if (ImGui.CalcTextSize(s).X <= maxW) + return s; + while (s.Length > 1 && ImGui.CalcTextSize(s + "..").X > maxW) + s = s[..^1]; + return s + ".."; + } + } +} diff --git a/PlayerViewer/UI/ViewerWindow/ViewerWindow.CapturePanel.cs b/PlayerViewer/UI/ViewerWindow/ViewerWindow.CapturePanel.cs new file mode 100644 index 0000000..a00f7e8 --- /dev/null +++ b/PlayerViewer/UI/ViewerWindow/ViewerWindow.CapturePanel.cs @@ -0,0 +1,811 @@ +using System; +using System.Collections.Generic; +using ImGuiNET; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using Vector2 = System.Numerics.Vector2; +using Vector4 = System.Numerics.Vector4; + +namespace PlayerViewer.UI +{ + public partial class ViewerWindow + { + static readonly (string Label, int W, int H)[] CaptureSizes = + { + ("1280 x 1280", 1280, 1280), + ("1920 x 1080", 1920, 1080), + ("3840 x 2160 (4K)", 3840, 2160), + ("2160 x 3840 (4K portrait)", 2160, 3840), + }; + + static readonly string[] ExportFormatLabels = + { + "PNG (current frame)", + "MP4", + "WebP (transparent)", + "WebM (transparent)", + }; + + static readonly string[] BgModeLabels = { "Transparent", "Color", "Image" }; + static readonly string[] BgScaleLabels = { "Fill", "Fit", "Stretch" }; + + void DrawRightSidebar() + { + DrawPlaybackControls(); + + float spacing = ImGui.GetStyle().ItemSpacing.Y; + float avail = ImGui.GetContentRegionAvail().Y; + float listH = Math.Max(avail - _measuredCaptureHeight - spacing, 90); + DrawAnimList(listH); + + float y0 = ImGui.GetCursorPosY(); + DrawModeTabs(); + if (_animMode == 1) + DrawSequencePanel(); + DrawCapturePanel(); + _measuredCaptureHeight = ImGui.GetCursorPosY() - y0; + } + + void DrawPlaybackControls() + { + Widgets.SectionHeader("Animation"); + + //Both scene types expose the same playback surface; bridge through locals. + bool standalone = _standalone != null; + string currentAnim = standalone ? _standalone.CurrentAnimName : _scene.CurrentAnimName; + bool paused = standalone ? _standalone.AnimPaused : _scene.AnimPaused; + float speed = standalone ? _standalone.AnimSpeed : _scene.AnimSpeed; + float rawFrameCount = standalone + ? _standalone.CurrentSkeletal?.FrameCount ?? 1 + : _scene.CurrentSkeletal?.FrameCount ?? 1; + + void SetPaused(bool value) + { + if (standalone) + _standalone.AnimPaused = value; + else + _scene.AnimPaused = value; + } + void SetSpeed(float value) + { + if (standalone) + _standalone.AnimSpeed = value; + else + _scene.AnimSpeed = value; + } + void SetFrame(float value) + { + if (standalone) + _standalone.SetAnimFrame(value); + else + _scene.SetAnimFrame(value); + } + + ImGui.TextColored(Theme.GoldBright, currentAnim ?? "(none)"); + + if (ImGui.Button(paused ? " Play " : " Pause ")) + SetPaused(!paused); + ImGui.SameLine(); + ImGui.SetNextItemWidth(-1); + if (ImGui.SliderFloat("##speed", ref speed, 0.01f, 10.0f, "speed %.2fx")) + SetSpeed(speed); + + float frameCount = Math.Max(rawFrameCount - 1, 1); + ImGui.SetNextItemWidth(-1); + if (ImGui.SliderFloat("##frame", ref _uiFrame, 0, frameCount, "frame %.0f")) + { + SetFrame(_uiFrame); + SetPaused(true); + } + + ImGui.Spacing(); + ImGui.AlignTextToFramePadding(); + Widgets.DimText("Search"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(-1); + ImGui.InputText("##animsearch", ref _animSearch, 64); + } + + void DrawAnimList(float height) + { + bool standalone = _standalone != null; + string currentAnim = standalone ? _standalone.CurrentAnimName : _scene.CurrentAnimName; + List animNames = standalone ? _standalone.AnimNames : _scene.Anims.AnimNames; + + void SetPaused(bool value) + { + if (standalone) + _standalone.AnimPaused = value; + else + _scene.AnimPaused = value; + } + void Play(string name) + { + StopAnimChain(); + if (standalone) + _standalone.PlayAnim(name); + else + _scene.PlayAnim(name); + } + + ImGui.BeginChild("##animlist", new Vector2(0, height), true); + if (animNames.Count == 0) + { + Widgets.DimText("no skeletal animations"); + ImGui.EndChild(); + return; + } + + if (standalone && ImGui.Selectable("", currentAnim == null)) + { + Play(null); + SetPaused(true); + } + foreach (var name in animNames) + { + if ( + !string.IsNullOrEmpty(_animSearch) + && !name.Contains(_animSearch, StringComparison.OrdinalIgnoreCase) + ) + continue; + if (ImGui.Selectable(name, name == currentAnim)) + { + Play(name); + SetPaused(false); + } + } + ImGui.EndChild(); + } + + //Mirrors the capture-panel selections into the config and persists them; called whenever + //one changes so they stick between runs. + void SaveCaptureSettings() + { + _config.CaptureResIndex = _captureRes; + _config.ExportFormat = _exportFormat; + _config.ExportFps = _exportFps; + _config.AnimMode = _animMode; + _config.Save(); + } + + //Background lives on the preset (_config.Player.Background). Persist to settings.json and + //flag the live viewport preview for a rebuild so it matches the exported composite. + void BackgroundChanged() + { + _bgDirty = true; + _config.Save(); + } + + System.Numerics.Vector3 BgColorVec => new(Bg.Color[0], Bg.Color[1], Bg.Color[2]); + + //Keeps the viewport's live background in sync with the settings (rebuilt only when the + //settings or the viewport size change). Uses the same BuildBackground as export, so the + //preview matches the exported composite. Transparent mode clears it (neutral framing). + void UpdateBackgroundPreview() + { + if (Bg.Mode == 0) + { + _pipeline.SetBackgroundBuffer(null, 0, 0); + _bgPreviewW = -1; + return; + } + if (_bgDirty || _bgPreviewW != _pipeline.Width || _bgPreviewH != _pipeline.Height) + { + var buf = ExportUtil.BuildBackground( + _pipeline.Width, + _pipeline.Height, + Bg, + bottomUp: true + ); + _pipeline.SetBackgroundBuffer(buf, _pipeline.Width, _pipeline.Height); + _bgPreviewW = _pipeline.Width; + _bgPreviewH = _pipeline.Height; + _bgDirty = false; + } + } + + //Unified background selector (left panel, part of the preset): Transparent (alpha where + //supported, black on MP4), Color (green = the old greenscreen), or an imported Image with + //scale/tile. Drives both the live viewport preview and the exported composite. + void DrawBackgroundSection() + { + Widgets.SectionHeader("Background"); + + ImGui.SetNextItemWidth(-1); + Widgets.Combo("##bgmode", Bg.Mode, BgModeLabels, v => Bg.Mode = v, BackgroundChanged); + + if (Bg.Mode == 0 && _exportFormat == 1) + Widgets.DimText("MP4 has no alpha; transparent exports as black."); + + if (Bg.Mode == 1) + { + ImGui.SetNextItemWidth(-1); + Widgets.ColorEdit3( + "##bgcolor", + BgColorVec, + v => Bg.Color = new[] { v.X, v.Y, v.Z }, + ImGuiColorEditFlags.None, + BackgroundChanged + ); + } + else if (Bg.Mode == 2) + { + if (ImGui.Button("Browse image...")) + { + string p = NativeFolderPicker.OpenFile( + "Background image", + "Images (*.png;*.jpg;*.jpeg)", + "*.png;*.jpg;*.jpeg" + ); + if (!string.IsNullOrEmpty(p)) + { + Bg.ImagePath = p; + BackgroundChanged(); + } + } + if (!string.IsNullOrEmpty(Bg.ImagePath)) + { + ImGui.SameLine(); + Widgets.DimText(System.IO.Path.GetFileName(Bg.ImagePath)); + } + ImGui.SetNextItemWidth(-1); + Widgets.Combo( + "##bgscale", + Bg.ScaleMode, + BgScaleLabels, + v => Bg.ScaleMode = v, + BackgroundChanged + ); + ImGui.SetNextItemWidth(-1); + Widgets.SliderFloat( + "##bgzoom", + Bg.Zoom, + 0.1f, + 4f, + v => Bg.Zoom = v, + BackgroundChanged, + "zoom %.2f" + ); + var off = new Vector2(Bg.OffsetX, Bg.OffsetY); + ImGui.SetNextItemWidth(-1); + if (ImGui.SliderFloat2("##bgoff", ref off, -1f, 1f, "offset %.2f")) + { + Bg.OffsetX = off.X; + Bg.OffsetY = off.Y; + BackgroundChanged(); + } + Widgets.Checkbox("Tile", Bg.Tile, v => Bg.Tile = v, BackgroundChanged); + if (Bg.Tile) + { + ImGui.SameLine(); + ImGui.SetNextItemWidth(70); + Widgets.InputInt( + "##tilex", + Bg.TileX, + v => Bg.TileX = Math.Max(1, v), + BackgroundChanged + ); + ImGui.SameLine(); + ImGui.SetNextItemWidth(70); + Widgets.InputInt( + "##tiley", + Bg.TileY, + v => Bg.TileY = Math.Max(1, v), + BackgroundChanged + ); + } + } + } + + void DrawCapturePanel() + { + Widgets.SectionHeader("Capture"); + + bool haveFfmpeg = ExportUtil.FfmpegAvailable; + + //--- Busy states: render phase or buffered encode phase, each with its own bar. + if (_animExporting) + { + float progress = + _animExportTotal > 0 ? Math.Min(_animExportIndex / _animExportTotal, 1f) : 0f; + int shown = (int)Math.Min(_animExportIndex + 1, _animExportTotal); + ImGui.ProgressBar( + progress, + new Vector2(-1, 0), + $"Rendering {shown}/{_animExportTotal}" + ); + Widgets.RedButton("Cancel export", AbortAnimExport); + return; + } + if (_bufferedExporter != null) + { + if (_bufferedExporter.IsEncoding) + { + var ex = _bufferedExporter; + float p = + ex.EncodeTotal > 0 + ? Math.Min(ex.EncodeProgress / (float)ex.EncodeTotal, 1f) + : 0f; + ImGui.ProgressBar( + p, + new Vector2(-1, 0), + $"Encoding {ex.EncodeProgress}/{ex.EncodeTotal}" + ); + return; + } + //Encode finished on the worker thread: log and clear. + FinishBufferedExport(); + } + + //--- Idle: resolution + format options, then one Export button. + ImGui.SetNextItemWidth(-1); + if (ImGui.BeginCombo("##capres", CaptureSizes[_captureRes].Label)) + { + for (int i = 0; i < CaptureSizes.Length; i++) + if (ImGui.Selectable(CaptureSizes[i].Label, i == _captureRes)) + { + _captureRes = i; + SaveCaptureSettings(); + } + ImGui.EndCombo(); + } + + ImGui.SetNextItemWidth(-1); + if ( + ImGui.Combo( + "##exportformat", + ref _exportFormat, + ExportFormatLabels, + ExportFormatLabels.Length + ) + ) + SaveCaptureSettings(); + + bool isPng = _exportFormat == 0; + bool isAnim = _exportFormat >= 1 && _exportFormat <= 3; + + if (isAnim) + { + ImGui.AlignTextToFramePadding(); + Widgets.DimText("FPS"); + ImGui.SameLine(); + if (ImGui.RadioButton("30", _exportFps == 30)) + { + _exportFps = 30; + SaveCaptureSettings(); + } + ImGui.SameLine(); + if (ImGui.RadioButton("60", _exportFps == 60)) + { + _exportFps = 60; + SaveCaptureSettings(); + } + } + + //The render is always transparent (alpha oracle), so trim applies whenever it's on. + bool trimApplies = _config.TrimDeadspace; + if (trimApplies) + Widgets.DimText($"Trim deadspace on (+{_config.TrimMarginPx}px)"); + + //In Sequence mode an animation export runs the whole chain instead of the current anim. + bool exportChain = _animMode == 1 && isAnim; + bool animReady = exportChain ? _animChain.Count > 0 : PlaybackHasAnim; + bool needFfmpeg = !isPng; + bool canExport = (!needFfmpeg || haveFfmpeg) && (!isAnim || animReady); + Widgets.DisabledButton(ExportButtonLabel(exportChain), canExport, DoExport); + + if (needFfmpeg && !haveFfmpeg) + Widgets.DimText("ffmpeg not found (data folder or PATH)"); + else if (isAnim && !animReady) + Widgets.DimText( + exportChain ? "add animations to the sequence" : "select an animation to export" + ); + } + + string ExportButtonLabel(bool sequence) => + _exportFormat switch + { + 0 => "Export PNG", + 1 => sequence ? "Export MP4 (sequence)" : "Export MP4", + 2 => sequence ? "Export WebP (sequence)" : "Export WebP", + _ => sequence ? "Export WebM (sequence)" : "Export WebM", + }; + + void DoExport() + { + switch (_exportFormat) + { + case 0: + SaveScreenshot(); + break; + case 1: + StartAnimExport(OutputFormat.Mp4); + break; + case 2: + StartAnimExport(OutputFormat.WebpTransparent); + break; + case 3: + StartAnimExport(OutputFormat.WebmTransparent); + break; + } + } + + void SaveScreenshot() + { + string def = ExportUtil.Timestamped("player", ".png"); + string path = NativeFolderPicker.SaveFile( + "Save Screenshot", + def, + "PNG image (*.png)", + "*.png" + ); + if (string.IsNullOrEmpty(path)) + return; + if (!path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) + path += ".png"; + WriteScreenshot(path); + } + + //Renders and saves a still (no dialog). The scene always renders transparent (alpha + //oracle); Transparent mode saves that directly, while Color/Image composite it over a + //matching crop of the background buffer. With trim on it renders at the internal + //(supersampled) resolution and crops from it so a loosely-framed subject stays sharp. + void WriteScreenshot(string path) + { + var (_, w, h) = CaptureSizes[_captureRes]; + int ss = Math.Clamp(_config.ExportSupersample, 1, 8); + bool trim = _config.TrimDeadspace; + int renderedW = trim ? w * ss : w, + renderedH = trim ? h * ss : h; + using var img = trim + ? _pipeline.Capture( + ActiveScene, + w * ss, + h * ss, + _pipeline.BackgroundColor, + transparent: true, + 1 + ) + : _pipeline.Capture( + ActiveScene, + w, + h, + _pipeline.BackgroundColor, + transparent: true, + ss + ); + + var rect = trim + ? TrimImage(img, _config.TrimMarginPx * ss) + : new Rectangle(0, 0, img.Width, img.Height); + + if (Bg.Mode == 0) + { + img.SaveAsPng(path); //Transparent: keep alpha + } + else + { + //Composite the (already trim-cropped) scene over the same crop of the background, + //built top-down at the rendered resolution so it stays registered under trim. + var bgBytes = ExportUtil.BuildBackground(renderedW, renderedH, Bg, bottomUp: false); + using var bg = Image.LoadPixelData(bgBytes, renderedW, renderedH); + bg.Mutate(c => c.Crop(rect).DrawImage(img, 1f)); + bg.SaveAsPng(path); + } + Console.WriteLine($"[UI] Saved {path}"); + } + + //Crops fully-transparent (alpha==0) deadspace off a captured image in place, keeping a + //margin, and returns the crop rect it applied (full frame if nothing was cropped). + static Rectangle TrimImage(Image img, int margin) + { + int w = img.Width, + h = img.Height; + int minX = w, + minY = h, + maxX = -1, + maxY = -1; + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + if (img[x, y].A != 0) + { + if (x < minX) + minX = x; + if (x > maxX) + maxX = x; + if (y < minY) + minY = y; + if (y > maxY) + maxY = y; + } + + var full = new Rectangle(0, 0, w, h); + if (maxX < 0) + return full; + int x0 = Math.Max(0, minX - margin), + y0 = Math.Max(0, minY - margin); + int x1 = Math.Min(w - 1, maxX + margin), + y1 = Math.Min(h - 1, maxY + margin); + int cw = x1 - x0 + 1, + ch = y1 - y0 + 1; + if (cw >= w && ch >= h) + return full; + var rect = new Rectangle(x0, y0, cw, ch); + img.Mutate(c => c.Crop(rect)); + return rect; + } + + //--- Playback bridge: both scene types expose the same animation surface but + //share no interface for it, so route through the active one. + bool PlaybackHasAnim => + (_standalone != null ? _standalone.CurrentSkeletal : _scene?.CurrentSkeletal) != null; + int PlaybackFrameCount => + (int) + Math.Round( + _standalone != null + ? (_standalone.CurrentSkeletal?.FrameCount ?? 0f) + : (_scene?.CurrentSkeletal?.FrameCount ?? 0f) + ); + float PlaybackAnimFrame => + _standalone != null ? _standalone.AnimFrame : (_scene?.AnimFrame ?? 0f); + bool PlaybackPaused => + _standalone != null ? _standalone.AnimPaused : (_scene?.AnimPaused ?? true); + float PlaybackSpeed => + _standalone != null ? _standalone.AnimSpeed : (_scene?.AnimSpeed ?? 1f); + + void PlaybackSetPaused(bool v) + { + if (_standalone != null) + _standalone.AnimPaused = v; + else if (_scene != null) + _scene.AnimPaused = v; + } + + void PlaybackSetFrame(float f) + { + if (_standalone != null) + _standalone.SetAnimFrame(f); + else + _scene?.SetAnimFrame(f); + } + + void PlaybackUpdate(float dt) + { + if (_standalone != null) + _standalone.Update(dt); + else + _scene?.Update(dt); + } + + void PlaybackPlay(string name, bool resetHair) + { + if (_standalone != null) + _standalone.PlayAnim(name); + else + _scene?.PlayAnim(name, resetHair); + } + + void PlaybackResetHair() + { + if (_standalone == null) + _scene?.ResetHairPhysics(); + } + + string PlaybackCurrentAnim => + _standalone != null ? _standalone.CurrentAnimName : _scene?.CurrentAnimName; + List PlaybackAnimNames => + _standalone != null + ? _standalone.AnimNames + : (_scene?.Anims.AnimNames ?? new List()); + + int PlaybackFrameCountOf(string name) => + _standalone != null + ? _standalone.SkeletalFrameCount(name) + : (_scene?.SkeletalFrameCount(name) ?? 0); + + void StartAnimExport(OutputFormat format) + { + bool chain = _animMode == 1 && _animChain.Count > 0; + if (_animExporting || _bufferedExporter != null) + return; + if (!chain && !PlaybackHasAnim) + return; + StopAnimChain(); //deterministic export drives frames itself; don't let the preview fight it + int total = chain ? (int)Math.Round(ChainTotalFrames()) : PlaybackFrameCount; + if (total < 1) + return; + + (string ext, string filterName, string filterExt) = format switch + { + OutputFormat.WebpTransparent => (".webp", "WebP image (*.webp)", "*.webp"), + OutputFormat.WebmTransparent => (".webm", "WebM video (*.webm)", "*.webm"), + _ => (".mp4", "MP4 video (*.mp4)", "*.mp4"), + }; + string def = ExportUtil.Timestamped("animation", ext); + string path = NativeFolderPicker.SaveFile( + "Export Animation", + def, + filterName, + filterExt + ); + if (string.IsNullOrEmpty(path)) + return; + if (!path.EndsWith(ext, StringComparison.OrdinalIgnoreCase)) + path += ext; + + _animExportPrevPaused = PlaybackPaused; + _animExportPrevFrame = PlaybackAnimFrame; + + //Respect the speed slider by up/downsampling: at 2x we advance the animation + //cursor twice as far per output frame (fewer frames, shorter clip); at 0.5x, + //half as far (more frames). Snapshotted so mid-export slider moves don't matter. + float speed = PlaybackSpeed; + _animExportAdvance = Math.Max(0.0001f, (60f / _exportFps) * speed); + _animExportTotal = total; + _animExportIndex = 0f; + //Keep alpha only where the format supports it AND Transparent mode is selected; + //otherwise the scene is composited over the background buffer. + bool keepAlpha = + Bg.Mode == 0 + && ( + format == OutputFormat.WebpTransparent || format == OutputFormat.WebmTransparent + ); + _animExportFormat = format; + _animExportTrim = _config.TrimDeadspace; + _animExportSupersample = Math.Clamp(_config.ExportSupersample, 1, 8); + _animExportChain = chain; + + //Supersample the render. With trim on, the crop keeps the full internal resolution + //(scale 1, larger frame) so a loosely-framed subject stays sharp; without trim, the + //factor becomes anti-alias supersampling downsampled back to the capture size. Even + //dimensions keep the raw RGBA stride aligned with ffmpeg's -video_size. Resize is + //frozen elsewhere for the duration because an export is in progress. + int ss = _animExportSupersample; + int bw = _pipeline.Width & ~1, + bh = _pipeline.Height & ~1; + int outW, + outH, + scale; + if (_animExportTrim) + { + outW = (bw * ss) & ~1; + outH = (bh * ss) & ~1; + scale = 1; + } + else + { + outW = bw; + outH = bh; + scale = ss; + } + _pipeline.ExportScaleOverride = scale; + _pipeline.Resize(outW, outH); + + //Precompute the full-frame background to composite over (null = keep alpha). Built at + //export resolution and bottom-up to match the OpenGL frames before ffmpeg's vflip. + _animExportBg = keepAlpha + ? null + : ExportUtil.BuildBackground(outW, outH, Bg, bottomUp: true); + + //Buffer raw frames to disk then encode: faster than piping to ffmpeg live (the render + //loop never stalls on the encoder). Always render transparent (alpha oracle for the + //crop); the background is composited over the straight-alpha frames during encode. + _bufferedExporter = new BufferedAnimExporter(); + if (!_bufferedExporter.StartCapture(outW, outH, _exportFps, path, _animExportTrim)) + { + Console.WriteLine($"[UI] Export failed: {_bufferedExporter.Error}"); + _bufferedExporter.Dispose(); + _bufferedExporter = null; + _pipeline.ExportScaleOverride = 0; + return; + } + + _animExporting = true; + PlaybackSetPaused(true); + //Restart cloth from rest so the first exported frame is reproducible; a chain resets + //once here then runs continuously across steps (ChainSeek rebinds without a reset). + if (_animExportChain) + BeginChain(); + else if (_standalone == null) + _scene?.ResetHairPhysics(); + + //Let cloth/hair sims settle before the first captured frame (see PrerollLoops). + RunPhysicsWarmup(); + } + + //Max warm-up loops surfaced in the Settings slider; bounds the synchronous pre-roll. + internal const int PrerollMaxLoops = 5; + + //Play first animation PrerollLoops times WITHOUT capturing, so the verlet sim is steady + //before recording. Runs synchronously (physics without GL) and mirrors 1/fps cloth dt. + void RunPhysicsWarmup() + { + int loops = Math.Clamp(_config.PrerollLoops, 0, PrerollMaxLoops); + if (loops <= 0) + return; + //Warm-up animation = the first step of a chain, or the single anim itself. + float frames = _animExportChain + ? Math.Max(PlaybackFrameCountOf(_animChain[0]), 1) + : PlaybackFrameCount; + if (frames < 1) + return; + + float dt = 1f / _exportFps; + for (int loop = 0; loop < loops; loop++) + for (float idx = 0; idx < frames; idx += _animExportAdvance) + { + //Bind+seek the first step without a hair reset (ChainSeek stays in step 0 for + //idx < frames), or scrub the single anim; then advance pose + cloth one frame. + if (_animExportChain) + ChainSeek(idx); + else + PlaybackSetFrame(idx); + PlaybackUpdate(dt); + } + } + + void CaptureAnimExportFrame() + { + //Always render transparent (alpha oracle for the crop + composite). Matte the edge + //fringe against the solid background color in Color mode (keeps a green key clean), + //otherwise a neutral color; the real background is composited during encode. + var matte = Bg.Mode == 1 ? BgColorVec : _pipeline.BackgroundColor; + var bytes = _pipeline.CaptureFrameBytes( + ActiveScene, + matte, + transparent: true, + out _, + out _ + ); + _bufferedExporter.PushFrame(bytes); + + _animExportIndex += _animExportAdvance; + if (_animExportIndex >= _animExportTotal) + FinishAnimExport(); + } + + //Natural completion: the render/capture phase is done. Kick off the encode pass on a + //worker; the panel polls _bufferedExporter for progress and clears it via + //FinishBufferedExport when encoding completes. + void FinishAnimExport() + { + //Margin is applied at the crop's (internal) resolution, so scale it with the + //supersample factor to keep the visual padding consistent. + _bufferedExporter.FinishCapture( + _animExportFormat, + _animExportBg, + _config.WebpQuality, + _config.TrimMarginPx * _animExportSupersample + ); + _pipeline.ExportScaleOverride = 0; + PlaybackSetPaused(_animExportPrevPaused); + PlaybackSetFrame(_animExportPrevFrame); + _animExporting = false; + } + + //Cancel button: abort without finishing the encode + void AbortAnimExport() + { + _bufferedExporter?.Abort(); + _bufferedExporter?.Dispose(); + _bufferedExporter = null; + _pipeline.ExportScaleOverride = 0; + PlaybackSetPaused(_animExportPrevPaused); + PlaybackSetFrame(_animExportPrevFrame); + _animExporting = false; + } + + void FinishBufferedExport() + { + if (_bufferedExporter == null) + return; + if (_bufferedExporter.Error != null) + Console.WriteLine($"[UI] Export failed: {_bufferedExporter.Error}"); + else + Console.WriteLine($"[UI] Exported {_bufferedExporter.OutputPath}"); + _bufferedExporter.Dispose(); + _bufferedExporter = null; + } + } +} diff --git a/PlayerViewer/UI/ViewerWindow/ViewerWindow.Layout.cs b/PlayerViewer/UI/ViewerWindow/ViewerWindow.Layout.cs new file mode 100644 index 0000000..62827c1 --- /dev/null +++ b/PlayerViewer/UI/ViewerWindow/ViewerWindow.Layout.cs @@ -0,0 +1,182 @@ +using System; +using ImGuiNET; +using PlayerViewer.Core; +using Vector2 = System.Numerics.Vector2; +using Vector4 = System.Numerics.Vector4; + +namespace PlayerViewer.UI +{ + // Top-level window layout: host window, menu bar, and the romfs-setup screen. + public partial class ViewerWindow + { + void DrawUI() + { + var viewport = ImGui.GetMainViewport(); + ImGui.SetNextWindowPos(viewport.Pos); + ImGui.SetNextWindowSize(viewport.Size); + ImGui.Begin( + "##host", + ImGuiWindowFlags.NoTitleBar + | ImGuiWindowFlags.NoResize + | ImGuiWindowFlags.NoMove + | ImGuiWindowFlags.NoCollapse + | ImGuiWindowFlags.NoBringToFrontOnFocus + | ImGuiWindowFlags.NoNavFocus + | ImGuiWindowFlags.MenuBar + ); + + DrawMenuBar(); + + if (_scene == null) + { + DrawRomfsSetup(); + ImGui.End(); + return; + } + + float leftWidth = 330; + float rightWidth = 300; + + ImGui.BeginChild("##left", new Vector2(leftWidth, 0), true); + if (_standalone != null) + DrawStandalonePanel(); + else + DrawPlayerPanel(); + ImGui.EndChild(); + + ImGui.SameLine(); + ImGui.BeginChild( + "##center", + new Vector2(-rightWidth - 8, 0), + false, + ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse + ); + DrawViewport(); + ImGui.EndChild(); + + ImGui.SameLine(); + ImGui.BeginChild("##right", new Vector2(0, 0), true); + DrawRightSidebar(); + ImGui.EndChild(); + + DrawSettingsWindow(); + + ImGui.End(); + } + + void DrawMenuBar() + { + if (!ImGui.BeginMenuBar()) + return; + + ImGui.PushStyleColor(ImGuiCol.Text, Theme.Gold); + ImGui.Text("PLAYER VIEWER"); + ImGui.PopStyleColor(); + ImGui.Separator(); + + if (ImGui.BeginMenu("File")) + { + if (ImGui.MenuItem("Change romfs path...")) + { + _scene?.Dispose(); + _scene = null; + _romfsInput = _config.RomfsPath ?? ""; + _sdodrInput = _config.SdodrRomfsPath ?? ""; + } + if (ImGui.MenuItem("View model file... (or drag && drop)")) + { + string file = NativeFolderPicker.OpenFile( + "Open Model", + "BFRES models (*.bfres;*.zs)", + "*.bfres;*.zs" + ); + if (!string.IsNullOrEmpty(file)) + OpenStandalone(file); + } + if (_standalone != null && ImGui.MenuItem("Back to player")) + CloseStandalone(); + ImGui.Separator(); + if (ImGui.MenuItem("Exit")) + Close(); + ImGui.EndMenu(); + } + + if (ImGui.MenuItem("Settings")) + _showSettings = true; + + if (_romfs != null) + { + ImGui.SameLine(ImGui.GetWindowWidth() - 320); + Widgets.DimText(TruncatePath(_config.RomfsPath, 42)); + } + + ImGui.EndMenuBar(); + } + + static string TruncatePath(string path, int max) + { + if (string.IsNullOrEmpty(path) || path.Length <= max) + return path ?? ""; + return "..." + path.Substring(path.Length - max + 3); + } + + void DrawRomfsSetup() + { + var avail = ImGui.GetContentRegionAvail(); + ImGui.SetCursorPos(new Vector2(avail.X / 2 - 260, avail.Y / 2 - 90)); + ImGui.BeginChild("##setup", new Vector2(520, 210), true); + + ImGui.PushStyleColor(ImGuiCol.Text, Theme.Gold); + ImGui.Text("Splatoon 3 romfs path"); + ImGui.PopStyleColor(); + ImGui.Spacing(); + + ImGui.SetNextItemWidth(-90); + ImGui.InputText("##romfs", ref _romfsInput, 512); + ImGui.SameLine(); + if (ImGui.Button("Browse...")) + { + string folder = NativeFolderPicker.SelectFolder("Select romfs folder", _romfsInput); + if (!string.IsNullOrEmpty(folder)) + _romfsInput = folder; + } + + ImGui.Spacing(); + bool valid = Romfs.IsValidRoot(_romfsInput); + if (!valid && !string.IsNullOrEmpty(_romfsInput)) + Widgets.ErrorText("Not a valid romfs (needs Model/ + RSDB/)"); + if (_romfsError != null) + Widgets.ErrorText(_romfsError); + + ImGui.Spacing(); + ImGui.TextColored(new Vector4(0.6f, 0.6f, 0.6f, 1), "Side Order DLC romfs (optional)"); + ImGui.SetNextItemWidth(-90); + ImGui.InputText("##sdodr", ref _sdodrInput, 512); + ImGui.SameLine(); + if (ImGui.Button("Browse...##sdodr")) + { + string folder = NativeFolderPicker.SelectFolder( + "Select Side Order romfs folder", + _sdodrInput + ); + if (!string.IsNullOrEmpty(folder)) + _sdodrInput = folder; + } + + ImGui.Spacing(); + if (!valid) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.45f); + if (ImGui.Button("Load", new Vector2(120, 0)) && valid) + { + _config.RomfsPath = _romfsInput; + _config.SdodrRomfsPath = _sdodrInput; + _config.Save(); + _needsLoad = true; + } + if (!valid) + ImGui.PopStyleVar(); + + ImGui.EndChild(); + } + } +} diff --git a/PlayerViewer/UI/ViewerWindow/ViewerWindow.PlayerPanel.cs b/PlayerViewer/UI/ViewerWindow/ViewerWindow.PlayerPanel.cs new file mode 100644 index 0000000..14d2552 --- /dev/null +++ b/PlayerViewer/UI/ViewerWindow/ViewerWindow.PlayerPanel.cs @@ -0,0 +1,509 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ImGuiNET; +using PlayerViewer.Core; +using Vector2 = System.Numerics.Vector2; +using Vector4 = System.Numerics.Vector4; + +namespace PlayerViewer.UI +{ + // Left-hand player configuration panel: player type, gear, colors, lighting, view. + public partial class ViewerWindow + { + static readonly string[] PlayerTypes = + { + "Inkling Girl (Player00)", + "Inkling Boy (Player01)", + "Octoling Girl (Player02)", + "Octoling Boy (Player03)", + }; + + static readonly string[] UniformSetLabels = { "Viewer", "AutoWalk" }; + static readonly string[] UniformSetDirs = { "SPL3", "SPL3_AutoWalk" }; + + void DrawPlayerPanel() + { + Widgets.SectionHeader("Player"); + + if (ImGui.Button("Reset", new Vector2(-1, 0))) + ResetPlayerDefaults(); + + float half = (ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) * 0.5f; + if (ImGui.Button("Save preset", new Vector2(half, 0))) + SavePreset(); + ImGui.SameLine(); + if (ImGui.Button("Load preset", new Vector2(half, 0))) + LoadPreset(); + if (!string.IsNullOrEmpty(_presetStatus)) + Widgets.DimText(_presetStatus); + + int playerType = _scene.PlayerType; + ImGui.SetNextItemWidth(-1); + if (ImGui.Combo("##playertype", ref playerType, PlayerTypes, PlayerTypes.Length)) + { + _scene.SetPlayerType(playerType); + ApplyTeamColor(); + SavePlayerConfig(); + } + + GearRow("Hair", GearSlot.Hair, _scene.CurrentHair); + GearRow("Eyebrow", GearSlot.Eyebrow, _scene.CurrentEyebrow); + + Widgets.LabeledRow( + "Eyes", + () => + { + ImGui.SetNextItemWidth(-1); + Widgets.SliderInt( + "##eye", + _scene.EyeColor, + 0, + 20, + v => _scene.ApplyEyeColor(v), + SavePlayerConfig + ); + } + ); + + Widgets.LabeledRow( + "Skin", + () => + { + ImGui.SetNextItemWidth(-1); + Widgets.SliderInt( + "##skin", + _scene.SkinTone, + 0, + 8, + v => _scene.ApplySkinTone(v), + SavePlayerConfig + ); + } + ); + + Widgets.Checkbox( + "Hair physics", + _scene.HairPhysicsEnabled, + v => + { + _scene.HairPhysicsEnabled = v; + if (v) + _scene.ResetHairPhysics(); + } + ); + + DrawTeamColorSection(); + + Widgets.SectionHeader("Gear"); + GearRow("Head", GearSlot.Head, _scene.CurrentHead, allowNone: false); + GearRow("Clothes", GearSlot.Clothes, _scene.CurrentClothes); + GearRow("Bottom", GearSlot.Bottom, _scene.CurrentBottom); + GearRow("Shoes", GearSlot.Shoes, _scene.CurrentShoes); + + Widgets.SectionHeader("Equipment"); + GearRow("Weapon", GearSlot.MainWeapon, _scene.CurrentWeapon, noneLabel: "Free"); + GearRow("Tank", GearSlot.Tank, _scene.CurrentTank); + + DrawLightingSection(); + DrawViewSection(); + DrawLayeredFsSection(); + } + + void DrawViewSection() + { + Widgets.SectionHeader("View"); + if (ImGui.Button("Reset camera", new Vector2(-1, 0))) + { + if (_standalone != null) + _pipeline.FrameSphere(_standalone.GetBounding()); + else + _pipeline.FramePlayer(); + } + + Widgets.Checkbox( + "Self shadow", + _pipeline.EnableSelfShadow, + v => _pipeline.EnableSelfShadow = v + ); + Widgets.ItemTooltip("Game-accurate self shadowing (gsys_shadow_prepass)."); + + int setIdx = Math.Max( + Array.IndexOf(UniformSetDirs, BfresEditor.HoianNXRender.UniformSetDir), + 0 + ); + Widgets.LabeledRow( + "Env", + () => + { + ImGui.SetNextItemWidth(-1); + if ( + ImGui.Combo( + "##uniset", + ref setIdx, + UniformSetLabels, + UniformSetLabels.Length + ) + ) + { + BfresEditor.HoianNXRender.SetUniformSet(UniformSetDirs[setIdx]); + ApplyTeamColor(); + } + } + ); + + //Background (mode/color/image) lives on the preset; folded in here on the left. + DrawBackgroundSection(); + } + + void DrawLightingSection() + { + Widgets.SectionHeader("Lighting"); + if (ImGui.Button("Reset lighting", new Vector2(-1, 0))) + _pipeline.ResetLighting(); + Widgets.Checkbox( + "Light follows camera", + _pipeline.LightFollowsCamera, + v => _pipeline.LightFollowsCamera = v + ); + if (!_pipeline.LightFollowsCamera) + { + ImGui.SetNextItemWidth(-1); + Widgets.SliderFloat( + "##lightaz", + _pipeline.LightAzimuth, + -180, + 180, + v => _pipeline.LightAzimuth = v, + null, + "Azimuth %.0f°" + ); + ImGui.SetNextItemWidth(-1); + Widgets.SliderFloat( + "##lightel", + _pipeline.LightElevation, + -89, + 89, + v => _pipeline.LightElevation = v, + null, + "Elevation %.0f°" + ); + } + } + + void DrawTeamColorSection() + { + Widgets.SectionHeader("Team Color"); + var colorSet = _db.TeamColors.ElementAtOrDefault(_teamColorIndex); + ImGui.SetNextItemWidth(-1); + string teamPreview = _useCustomTeamColor ? "Custom" : colorSet?.Name ?? "(default)"; + if (ImGui.BeginCombo("##teamcolor", teamPreview)) + { + //"Custom" first: freely picked colors instead of an RSDB set. + ImGui.ColorButton( + "##swatchCustA", + new Vector4(_customTeam.Alpha.X, _customTeam.Alpha.Y, _customTeam.Alpha.Z, 1), + ImGuiColorEditFlags.NoTooltip, + new Vector2(14, 14) + ); + ImGui.SameLine(); + ImGui.ColorButton( + "##swatchCustB", + new Vector4(_customTeam.Bravo.X, _customTeam.Bravo.Y, _customTeam.Bravo.Z, 1), + ImGuiColorEditFlags.NoTooltip, + new Vector2(14, 14) + ); + ImGui.SameLine(); + if (ImGui.Selectable("Custom", _useCustomTeamColor)) + { + _useCustomTeamColor = true; + ApplyTeamColor(); + SavePlayerConfig(); + } + + for (int i = 0; i < _db.TeamColors.Count; i++) + { + var set = _db.TeamColors[i]; + //Swatch preview + ImGui.ColorButton( + $"##swatchA{i}", + new Vector4(set.Alpha.X, set.Alpha.Y, set.Alpha.Z, 1), + ImGuiColorEditFlags.NoTooltip, + new Vector2(14, 14) + ); + ImGui.SameLine(); + ImGui.ColorButton( + $"##swatchB{i}", + new Vector4(set.Bravo.X, set.Bravo.Y, set.Bravo.Z, 1), + ImGuiColorEditFlags.NoTooltip, + new Vector2(14, 14) + ); + ImGui.SameLine(); + if (ImGui.Selectable(set.Name, !_useCustomTeamColor && i == _teamColorIndex)) + { + _useCustomTeamColor = false; + _teamColorIndex = i; + ApplyTeamColor(); + SavePlayerConfig(); + } + } + ImGui.EndCombo(); + } + if (_useCustomTeamColor) + { + bool custChanged = false; + var a = _customTeam.Alpha; + var b = _customTeam.Bravo; + var c = _customTeam.Charlie; + custChanged |= ImGui.ColorEdit3("Alpha##cust", ref a, ImGuiColorEditFlags.NoInputs); + ImGui.SameLine(); + custChanged |= ImGui.ColorEdit3("Bravo##cust", ref b, ImGuiColorEditFlags.NoInputs); + ImGui.SameLine(); + custChanged |= ImGui.ColorEdit3( + "Charlie##cust", + ref c, + ImGuiColorEditFlags.NoInputs + ); + if (custChanged) + { + _customTeam.Alpha = a; + _customTeam.Bravo = b; + _customTeam.Charlie = c; + _customTeam.Neutral = (a + b) * 0.5f; + ApplyTeamColor(); + SavePlayerConfig(); + } + } + if (ImGui.RadioButton("Alpha", _teamIndex == 0)) + { + _teamIndex = 0; + ApplyTeamColor(); + SavePlayerConfig(); + } + ImGui.SameLine(); + if (ImGui.RadioButton("Bravo", _teamIndex == 1)) + { + _teamIndex = 1; + ApplyTeamColor(); + SavePlayerConfig(); + } + ImGui.SameLine(); + if (ImGui.RadioButton("Charlie", _teamIndex == 2)) + { + _teamIndex = 2; + ApplyTeamColor(); + SavePlayerConfig(); + } + } + + void DrawLayeredFsSection() + { + Widgets.SectionHeader("LayeredFS (mods)"); + + ImGui.SetNextItemWidth(-70); + if (ImGui.InputText("##layeredpath", ref _layeredInput, 512)) + { + _config.LayeredFsPath = _layeredInput; + _config.Save(); + } + ImGui.SameLine(); + if (ImGui.Button("...##layeredbrowse", new Vector2(-1, 0))) + { + string folder = NativeFolderPicker.SelectFolder( + "Select LayeredFS (mod) folder", + _layeredInput + ); + if (!string.IsNullOrEmpty(folder)) + { + _layeredInput = folder; + _config.LayeredFsPath = folder; + _config.Save(); + } + } + + Widgets.Checkbox( + "Enable LayeredFS", + _config.UseLayeredFs, + v => _config.UseLayeredFs = v, + () => + { + _config.Save(); + _preserveStateOnLoad = true; + _needsLoad = true; + } + ); + + bool dirOk = !string.IsNullOrEmpty(_layeredInput) && Directory.Exists(_layeredInput); + if (!string.IsNullOrEmpty(_layeredInput) && !dirOk) + Widgets.ErrorText("folder not found"); + else if (_romfs != null && _romfs.UseLayered) + Widgets.SuccessText("active"); + + if (ImGui.Button("Reload", new Vector2(-1, 0))) + { + _preserveStateOnLoad = true; + _needsLoad = true; + } + Widgets.ItemTooltip( + "Reload everything from the current romfs + LayeredFS.\nKeeps the current player configuration." + ); + } + + void GearRow( + string label, + GearSlot slot, + GearEntry current, + bool allowNone = true, + string noneLabel = "Blank" + ) + { + Widgets.LabeledRow( + label, + () => + { + if ( + Widgets.GearCombo( + label, + _db.GetList(slot), + current, + out var selected, + allowNone, + noneLabel + ) + ) + { + _scene.SetGear(slot, selected); + SavePlayerConfig(); + } + } + ); + } + + void ApplyTeamColor() + { + var set = _useCustomTeamColor + ? _customTeam + : _db?.TeamColors.ElementAtOrDefault(_teamColorIndex); + if (set != null && _scene != null) + _scene.ApplyTeamColor(set, _teamIndex); + } + + void ResetPlayerDefaults() + { + _scene.CurrentHair = null; + _scene.CurrentEyebrow = null; + _scene.CurrentHead = null; + _scene.CurrentClothes = null; + _scene.CurrentBottom = null; + _scene.CurrentShoes = null; + _scene.CurrentTank = null; + _scene.CurrentWeapon = null; + _scene.EyeColor = 0; + _scene.SkinTone = 0; + _scene.SetPlayerType(0); + + _teamColorIndex = 0; + _teamIndex = 0; + _useCustomTeamColor = true; + ApplyTeamColor(); + + _pipeline.FramePlayer(); + SavePlayerConfig(); + } + + void SavePlayerConfig() + { + if (_scene == null) + return; + var p = _config.Player; + p.PlayerType = _scene.PlayerType; + p.EyeColor = _scene.EyeColor; + p.SkinTone = _scene.SkinTone; + static void SaveGear(GearEntry e, out string rowId, out int variation) + { + rowId = e?.RowId; + variation = e?.Variation ?? 0; + } + SaveGear(_scene.CurrentHair, out p.Hair, out p.HairVariation); + SaveGear(_scene.CurrentEyebrow, out p.Eyebrow, out p.EyebrowVariation); + SaveGear(_scene.CurrentHead, out p.Head, out p.HeadVariation); + SaveGear(_scene.CurrentClothes, out p.Clothes, out p.ClothesVariation); + SaveGear(_scene.CurrentBottom, out p.Bottom, out p.BottomVariation); + SaveGear(_scene.CurrentShoes, out p.Shoes, out p.ShoesVariation); + SaveGear(_scene.CurrentTank, out p.Tank, out p.TankVariation); + SaveGear(_scene.CurrentWeapon, out p.Weapon, out p.WeaponVariation); + p.TeamColorIndex = _teamColorIndex; + p.TeamIndex = _teamIndex; + p.UseCustomTeamColor = _useCustomTeamColor; + p.CustomAlpha = new[] { _customTeam.Alpha.X, _customTeam.Alpha.Y, _customTeam.Alpha.Z }; + p.CustomBravo = new[] { _customTeam.Bravo.X, _customTeam.Bravo.Y, _customTeam.Bravo.Z }; + p.CustomCharlie = new[] + { + _customTeam.Charlie.X, + _customTeam.Charlie.Y, + _customTeam.Charlie.Z, + }; + _config.Save(); + } + + void RestorePlayerConfig() + { + var p = _config.Player; + if (p == null) + return; + + GearEntry FindGear(List list, string rowId, int variation) + { + if (rowId == null) + return null; + return list.FirstOrDefault(x => x.RowId == rowId && x.Variation == variation) + ?? list.FirstOrDefault(x => x.RowId == rowId); + } + + _scene.EyeColor = p.EyeColor; + _scene.SkinTone = p.SkinTone; + _scene.CurrentHair = FindGear(_db.Hair, p.Hair, p.HairVariation); + _scene.CurrentEyebrow = FindGear(_db.Eyebrow, p.Eyebrow, p.EyebrowVariation); + _scene.CurrentHead = FindGear(_db.Head, p.Head, p.HeadVariation); + _scene.CurrentClothes = FindGear(_db.Clothes, p.Clothes, p.ClothesVariation); + _scene.CurrentBottom = FindGear(_db.Bottom, p.Bottom, p.BottomVariation); + _scene.CurrentShoes = FindGear(_db.Shoes, p.Shoes, p.ShoesVariation); + _scene.CurrentTank = FindGear(_db.Tank, p.Tank, p.TankVariation); + _scene.CurrentWeapon = FindGear(_db.MainWeapons, p.Weapon, p.WeaponVariation); + _scene.SetPlayerType(p.PlayerType); + + _teamColorIndex = p.TeamColorIndex; + _teamIndex = p.TeamIndex; + _useCustomTeamColor = p.UseCustomTeamColor; + if (p.CustomAlpha is { Length: 3 }) + _customTeam.Alpha = new System.Numerics.Vector3( + p.CustomAlpha[0], + p.CustomAlpha[1], + p.CustomAlpha[2] + ); + if (p.CustomBravo is { Length: 3 }) + _customTeam.Bravo = new System.Numerics.Vector3( + p.CustomBravo[0], + p.CustomBravo[1], + p.CustomBravo[2] + ); + if (p.CustomCharlie is { Length: 3 }) + _customTeam.Charlie = new System.Numerics.Vector3( + p.CustomCharlie[0], + p.CustomCharlie[1], + p.CustomCharlie[2] + ); + _customTeam.Neutral = (_customTeam.Alpha + _customTeam.Bravo) * 0.5f; + ApplyTeamColor(); + + _scene.ApplyEyeColor(p.EyeColor); + _scene.ApplySkinTone(p.SkinTone); + + //Background travels with the preset; clamp loaded values and rebuild the live preview. + p.Background ??= new Core.BackgroundConfig(); + p.Background.Normalize(); + _bgDirty = true; + } + } +} diff --git a/PlayerViewer/UI/ViewerWindow/ViewerWindow.Presets.cs b/PlayerViewer/UI/ViewerWindow/ViewerWindow.Presets.cs new file mode 100644 index 0000000..2a5d40a --- /dev/null +++ b/PlayerViewer/UI/ViewerWindow/ViewerWindow.Presets.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using Newtonsoft.Json; +using PlayerViewer.Core; + +namespace PlayerViewer.UI +{ + // Save/load the current player loadout (gear, colors, eye/skin) as a standalone preset + // file: a serialized PlayerConfig, reusing the Save/RestorePlayerConfig apply path. + public partial class ViewerWindow + { + string _presetStatus; //transient message shown under the preset buttons + + void SavePreset() + { + if (_scene == null) + return; + string def = ExportUtil.Timestamped("player-preset", ".json"); + string path = NativeFolderPicker.SaveFile( + "Save Preset", + def, + "Player preset (*.json)", + "*.json" + ); + if (string.IsNullOrEmpty(path)) + return; + if (!path.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) + path += ".json"; + + //Mirror the live scene into _config.Player, then serialize that snapshot. + SavePlayerConfig(); + try + { + File.WriteAllText( + path, + JsonConvert.SerializeObject(_config.Player, Formatting.Indented) + ); + _presetStatus = $"Saved {Path.GetFileName(path)}"; + Console.WriteLine($"[UI] Saved preset {path}"); + } + catch (Exception ex) + { + _presetStatus = $"Save failed: {ex.Message}"; + Console.WriteLine($"[UI] Preset save failed: {ex.Message}"); + } + } + + void LoadPreset() + { + if (_scene == null) + return; + string path = NativeFolderPicker.OpenFile( + "Load Preset", + "Player preset (*.json)", + "*.json" + ); + if (string.IsNullOrEmpty(path) || !File.Exists(path)) + return; + + try + { + var preset = JsonConvert.DeserializeObject(File.ReadAllText(path)); + if (preset == null) + { + _presetStatus = "Not a valid preset file"; + return; + } + + //Adopt the preset as the current config and apply it. Gear rows missing from + //the loaded game resolve to blank slots inside RestorePlayerConfig. + _config.Player = preset; + RestorePlayerConfig(); + SavePlayerConfig(); + _presetStatus = $"Loaded {Path.GetFileName(path)}"; + Console.WriteLine($"[UI] Loaded preset {path}"); + } + catch (Exception ex) + { + _presetStatus = $"Load failed: {ex.Message}"; + Console.WriteLine($"[UI] Preset load failed: {ex.Message}"); + } + } + } +} diff --git a/PlayerViewer/UI/ViewerWindow/ViewerWindow.Settings.cs b/PlayerViewer/UI/ViewerWindow/ViewerWindow.Settings.cs new file mode 100644 index 0000000..d4a7c55 --- /dev/null +++ b/PlayerViewer/UI/ViewerWindow/ViewerWindow.Settings.cs @@ -0,0 +1,170 @@ +using System; +using ImGuiNET; +using PlayerViewer.Core; +using Vector2 = System.Numerics.Vector2; +using Vector4 = System.Numerics.Vector4; + +namespace PlayerViewer.UI +{ + // Settings window (opened from the menu bar): export/capture preferences persisted in + // AppConfig: deadspace trimming and WebP encode quality. + public partial class ViewerWindow + { + void DrawSettingsWindow() + { + if (!_showSettings) + return; + + ImGui.SetNextWindowSize(new Vector2(420, 340), ImGuiCond.FirstUseEver); + if ( + !ImGui.Begin( + "Settings", + ref _showSettings, + ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking + ) + ) + { + ImGui.End(); + return; + } + + bool dirty = false; + + Widgets.SectionHeader("Trim deadspace"); + ImGui.TextWrapped( + "Crops fully-transparent space off exported frames, shrinking " + + "file size. Uses the transparent render to find the content, so it crops the " + + "color/image background too. Applies to WebP, WebM, MP4, and PNG." + ); + ImGui.Spacing(); + + Widgets.Checkbox( + "Enable", + _config.TrimDeadspace, + v => _config.TrimDeadspace = v, + () => dirty = true + ); + + ImGui.SetNextItemWidth(160); + Widgets.InputInt( + "Margin (px)", + _config.TrimMarginPx, + v => _config.TrimMarginPx = Math.Max(0, v), + () => dirty = true + ); + ImGui.SameLine(); + Widgets.DimText("transparent padding kept around the content"); + + if (_config.TrimDeadspace) + ImGui.TextColored( + Theme.Gold, + "Note: trimmed animation export buffers every frame " + + "to a temp file on disk first; transiently uses ~width×height×4×frames of space, " + + "times the supersample factor squared (several GB at 4K, tens of GB with high supersample)." + ); + + Widgets.SectionHeader("WebP / WebM quality"); + ImGui.TextWrapped( + "Quality for the transparent WebP and WebM (VP9) exports. 100 = " + + "lossless (largest). Lower = lossy: smaller and faster to encode, with some quality loss." + ); + ImGui.Spacing(); + + ImGui.SetNextItemWidth(-1); + string qLabel = _config.WebpQuality >= 100 ? "Lossless" : "Lossy %d"; + Widgets.SliderInt( + "##webpq", + _config.WebpQuality, + 0, + 100, + v => _config.WebpQuality = Math.Clamp(v, 0, 100), + () => dirty = true, + qLabel + ); + + if (ImGui.Button("Lossless")) + { + _config.WebpQuality = 100; + dirty = true; + } + ImGui.SameLine(); + if (ImGui.Button("Near-lossless")) + { + _config.WebpQuality = 90; + dirty = true; + } + ImGui.SameLine(); + if (ImGui.Button("Lossy")) + { + _config.WebpQuality = 75; + dirty = true; + } + + Widgets.SectionHeader("Supersample (export quality)"); + ImGui.TextWrapped( + "Renders exports at this multiple of the capture size. With trim on, " + + "the crop keeps that full internal resolution, so you only need the camera angle " + + "right; a small or loosely-framed subject still exports sharp." + ); + ImGui.Spacing(); + + ImGui.SetNextItemWidth(-1); + Widgets.SliderInt( + "##supersample", + _config.ExportSupersample, + 1, + 8, + v => _config.ExportSupersample = Math.Clamp(v, 1, 8), + () => dirty = true, + "%dx" + ); + + int ss = _config.ExportSupersample; + Widgets.DimText( + $"A 1080p export renders {1920 * ss}x{1080 * ss} internally ({ss * ss}x the pixels)." + ); + if (ss >= 8) + Widgets.ErrorText("8x is extreme: may exhaust GPU memory at 4K"); + else if (ss > 4) + ImGui.TextColored( + Theme.Gold, + "High supersample: large GPU memory & temp-disk use (grows with the square of the factor)" + ); + + Widgets.SectionHeader("Physics warm-up"); + ImGui.TextWrapped( + "Plays the animation (/ first animation in the sequence) through " + + "this many extra times before recording starts without capturing. Physics reset" + + "whenever an animation loads, so frame 0 has a twitch each time the exported " + + "WebP/WebM loops. A warm-up lets the sim settle first." + ); + ImGui.Spacing(); + + ImGui.SetNextItemWidth(-1); + string plLabel = _config.PrerollLoops <= 0 ? "Off" : "%d loops"; + Widgets.SliderInt( + "##preroll", + _config.PrerollLoops, + 0, + PrerollMaxLoops, + v => _config.PrerollLoops = Math.Clamp(v, 0, PrerollMaxLoops), + () => dirty = true, + plLabel + ); + + Widgets.SectionHeader("Data folder"); + ImGui.TextWrapped( + "settings.json lives here. Drop an ffmpeg binary here to use it " + + "instead of one on PATH." + ); + Widgets.DimText(AppPaths.DataDir); + if (ImGui.Button("Open data folder")) + AppPaths.OpenDataDir(); + + if (dirty) + _config.Save(); + + ImGui.End(); + } + } +} diff --git a/PlayerViewer/UI/ViewerWindow/ViewerWindow.Standalone.cs b/PlayerViewer/UI/ViewerWindow/ViewerWindow.Standalone.cs new file mode 100644 index 0000000..4c2327a --- /dev/null +++ b/PlayerViewer/UI/ViewerWindow/ViewerWindow.Standalone.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using ImGuiNET; +using Vector2 = System.Numerics.Vector2; +using Vector4 = System.Numerics.Vector4; + +namespace PlayerViewer.UI +{ + // Left-hand panel shown when viewing a loose (dropped/browsed) BFRES model. + public partial class ViewerWindow + { + void DrawStandalonePanel() + { + Widgets.SectionHeader("Standalone Model"); + + ImGui.TextColored(Theme.GoldBright, _standalone.Name); + ImGui.PushTextWrapPos(); + Widgets.DimText(_standalone.SourcePath); + ImGui.PopTextWrapPos(); + if (_standaloneError != null) + Widgets.ErrorText(_standaloneError); + + ImGui.Spacing(); + if (ImGui.Button("Back to player", new Vector2(-1, 0))) + { + CloseStandalone(); + return; + } + if (ImGui.Button("Frame model", new Vector2(-1, 0))) + _pipeline.FrameSphere(_standalone.GetBounding()); + + var models = _standalone.Render.Models.OfType().ToList(); + Widgets.SectionHeader("Models"); + + float spacing = ImGui.GetStyle().ItemSpacing.Y; + float avail = ImGui.GetContentRegionAvail().Y; + float listH = Math.Max(avail - _measuredStandaloneTailHeight - spacing, 90); + ImGui.BeginChild("##models", new Vector2(0, listH), true); + for (int mi = 0; mi < models.Count; mi++) + { + var model = models[mi]; + bool visible = model.IsVisible; + if (ImGui.Checkbox($"##{mi}_vis", ref visible)) + model.IsVisible = visible; + ImGui.SameLine(); + if (ImGui.TreeNode($"{model.ModelData.Name}##{mi}")) + { + foreach (var mesh in model.Meshes) + { + bool meshVis = mesh.Shape.IsVisible; + if (ImGui.Checkbox($"{mesh.Name}##{mi}_{mesh.Name}", ref meshVis)) + mesh.Shape.IsVisible = meshVis; + } + ImGui.TreePop(); + } + } + ImGui.EndChild(); + + float tailY0 = ImGui.GetCursorPosY(); + DrawLightingSection(); + DrawTeamColorSection(); + DrawViewSection(); + _measuredStandaloneTailHeight = ImGui.GetCursorPosY() - tailY0; + } + } +} diff --git a/PlayerViewer/UI/ViewerWindow/ViewerWindow.Viewport.cs b/PlayerViewer/UI/ViewerWindow/ViewerWindow.Viewport.cs new file mode 100644 index 0000000..bda93dc --- /dev/null +++ b/PlayerViewer/UI/ViewerWindow/ViewerWindow.Viewport.cs @@ -0,0 +1,140 @@ +using System; +using ImGuiNET; +using OpenTK; +using OpenTK.Input; +using PlayerViewer.Player; +using Vector2 = System.Numerics.Vector2; +using Vector4 = System.Numerics.Vector4; + +namespace PlayerViewer.UI +{ + // Center viewport: renders the active scene and handles orbit/pan/zoom camera input. + public partial class ViewerWindow + { + //--- viewport camera input + bool _viewportHovered; + bool _mouseDown; + + IViewScene ActiveScene => _standalone != null ? _standalone : _scene; + + void DrawViewport() + { + var size = ImGui.GetContentRegionAvail(); + + //Freeze the render size while exporting: the deterministic export (and the trim + //path's temp buffer) assume every frame shares fixed dimensions. + if (!_animExporting) + _pipeline.Resize((int)size.X, (int)size.Y); + + //During an export the capture pass already renders each frame (often supersampled), + // so skip the redundant viewport render and just preview the last captured frame. + if (!_animExporting) + { + UpdateBackgroundPreview(); + _pipeline.Render(ActiveScene); + } + + var pos = ImGui.GetCursorScreenPos(); + //Fit the render texture into the region, preserving aspect. Normally its already + //region-sized (1:1); during a supersampled export its larger, so scale it down + //instead of overflowing the region so it doesn't "zoom" + float fit = Math.Min(size.X / _pipeline.Width, size.Y / _pipeline.Height); + var imgSize = new Vector2(_pipeline.Width * fit, _pipeline.Height * fit); + ImGui.Image( + (IntPtr)_pipeline.ViewportTextureId, + imgSize, + new Vector2(0, 1), + new Vector2(1, 0) + ); + + _viewportHovered = ImGui.IsItemHovered(); + //Freeze the camera during a full-animation export so every frame shares + //the exact same viewpoint. + if (!_animExporting) + UpdateCameraInput(pos); + } + + void UpdateCameraInput(Vector2 viewportScreenPos) + { + var io = ImGui.GetIO(); + var cam = _pipeline.Camera; + bool changed = false; + + bool leftDown = ImGui.IsMouseDown(ImGuiMouseButton.Left); + bool rightDown = ImGui.IsMouseDown(ImGuiMouseButton.Right); + bool midDown = ImGui.IsMouseDown(ImGuiMouseButton.Middle); + bool anyDown = leftDown || rightDown || midDown; + + //Drags only start inside the viewport, but keep tracking outside it. + if (_viewportHovered && anyDown && !_mouseDown) + _mouseDown = true; + if (!anyDown) + _mouseDown = false; + + if (_mouseDown) + { + var delta = io.MouseDelta; + if (midDown || (leftDown && io.KeyShift)) + { + //Pan, scaled so the model roughly follows the cursor. + float scale = (float)Math.Sin(cam.Fov) * cam.TargetDistance; + float dx = -delta.X / Math.Max(1, cam.Width) * scale; + float dy = delta.Y / Math.Max(1, cam.Height) * scale; + var rot = cam.InverseRotationMatrix; + cam.TargetPosition += rot.Row0 * dx + rot.Row1 * dy; + changed = true; + } + else if (leftDown || rightDown) + { + //Orbit around the target. + cam.RotationY += delta.X * 0.008f; + cam.RotationX += delta.Y * 0.008f; + cam.RotationX = MathHelper.Clamp( + cam.RotationX, + -MathHelper.PiOver2 + 0.01f, + MathHelper.PiOver2 - 0.01f + ); + changed = true; + } + } + + if (_viewportHovered && io.MouseWheel != 0) + { + cam.TargetDistance = Math.Max( + 0.05f, + cam.TargetDistance * (1.0f - io.MouseWheel * 0.12f) + ); + changed = true; + } + + //WASD pans in camera space (W/S = forward/back, A/D = left/right). + if (!io.WantTextInput && Focused) + { + var kb = Keyboard.GetState(); + float move = cam.TargetDistance * io.DeltaTime; + var dir = OpenTK.Vector3.Zero; + var rot = cam.InverseRotationMatrix; + if (kb.IsKeyDown(Key.W)) + dir -= rot.Row2; + if (kb.IsKeyDown(Key.S)) + dir += rot.Row2; + if (kb.IsKeyDown(Key.A)) + dir -= rot.Row0; + if (kb.IsKeyDown(Key.D)) + dir += rot.Row0; + if (kb.IsKeyDown(Key.Space)) + dir += rot.Row1; + if (kb.IsKeyDown(Key.ShiftLeft) || kb.IsKeyDown(Key.ShiftRight)) + dir -= rot.Row1; + if (dir != OpenTK.Vector3.Zero) + { + cam.TargetPosition += dir * move; + changed = true; + } + } + + if (changed) + cam.UpdateMatrices(); + } + } +} diff --git a/PlayerViewer/UI/ViewerWindow/ViewerWindow.cs b/PlayerViewer/UI/ViewerWindow/ViewerWindow.cs new file mode 100644 index 0000000..eba4d93 --- /dev/null +++ b/PlayerViewer/UI/ViewerWindow/ViewerWindow.cs @@ -0,0 +1,438 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime; +using CafeStudio.UI; +using GLFrameworkEngine; +using ImGuiNET; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Graphics.OpenGL; +using OpenTK.Input; +using PlayerViewer.Core; +using PlayerViewer.Player; + +namespace PlayerViewer.UI +{ + /// + /// Main interactive window: 3D viewport + player configuration UI. + /// + /// The class is split across several files by concern (all partial class + /// ViewerWindow): this file owns the window lifecycle and scene loading; + /// ViewerWindow.Layout.cs the top-level layout/menu; *.PlayerPanel.cs, + /// *.Viewport.cs, *.CapturePanel.cs and *.Standalone.cs the + /// respective UI sections. + /// + public partial class ViewerWindow : GameWindow + { + readonly AppConfig _config; + + ImGuiController _imgui; + ScenePipeline _pipeline; + Romfs _romfs; + GameDatabase _db; + PlayerScene _scene; + + //--- UI state + string _romfsInput = ""; + string _sdodrInput = ""; + string _layeredInput = ""; + string _romfsError = null; + bool _needsLoad; + bool _preserveStateOnLoad; + string _animSearch = ""; + int _teamColorIndex; + int _teamIndex; + bool _useCustomTeamColor = true; + readonly TeamColorSet _customTeam = new() + { + Name = "Custom", + Alpha = new System.Numerics.Vector3(0.925f, 0.243f, 0.549f), + Bravo = new System.Numerics.Vector3(0.196f, 0.855f, 0.302f), + Charlie = new System.Numerics.Vector3(0.980f, 0.769f, 0.196f), + Neutral = new System.Numerics.Vector3(0.56f, 0.55f, 0.43f), + }; + float _uiFrame; //frame slider mirror + int _captureRes = 2; //index into CaptureSizes + + //--- Standalone model viewing (dropped/browsed files, outside the player) + StandaloneScene _standalone; + string _standaloneError; + + public string AutoOpenFile; //--open : opens a standalone model right after load + + //--- Deterministic full-animation export: drives the timeline frame-by-frame + //(ignoring wall clock) so every animation frame lands exactly once. Reuses the + //current camera and the chosen background (greenscreen for MP4, alpha for WebP). + bool _animExporting; + float _animExportIndex; //current animation-frame position being captured + int _animExportTotal; //frame count of the animation + float _animExportAdvance; //animation frames advanced per output frame ((60/fps) * speed) + int _exportFps = 60; //two-tick control: 30 or 60 + bool _animExportTrim; //snapshot of TrimDeadspace taken at export start + int _animExportSupersample; //snapshot of ExportSupersample taken at export start + bool _animExportChain; //exporting the whole sequence (Sequence mode) vs a single anim + OutputFormat _animExportFormat; + byte[] _animExportBg; //full-frame composite background (null = keep alpha) + bool _animExportPrevPaused; + float _animExportPrevFrame; + BufferedAnimExporter _bufferedExporter; //non-null during the trim (buffered) export + + //--- Unified capture UI + int _exportFormat; //0 PNG, 1 MP4, 2 WebP, 3 WebM + bool _showSettings; + + //--- Background: the data lives on _config.Player.Background (saved with the preset); + //these only track when the live viewport preview buffer needs rebuilding. + Core.BackgroundConfig Bg => _config.Player.Background; + bool _bgDirty = true; //rebuild the live preview buffer on next frame + int _bgPreviewW = -1, + _bgPreviewH = -1; + + //Self-correcting layout: capture controls stay pinned, the animation list absorbs + //slack. We size the list from last frame's measured control height. + float _measuredCaptureHeight = 220; + float _measuredStandaloneTailHeight = 160; + + public ViewerWindow(AppConfig config) + : base( + config.WindowWidth, + config.WindowHeight, + new GraphicsMode(new ColorFormat(32), 24, 8, 4, new ColorFormat(32), 2, false), + "Splatoon 3 Player Viewer", + GameWindowFlags.Default, + DisplayDevice.Default, + 3, + 2, + GraphicsContextFlags.Default + ) + { + _config = config; + _romfsInput = config.RomfsPath ?? ""; + _sdodrInput = config.SdodrRomfsPath ?? ""; + _layeredInput = config.LayeredFsPath ?? ""; + + //Restore persisted capture-panel selections (clamped in case ranges changed). + _captureRes = Math.Clamp(config.CaptureResIndex, 0, CaptureSizes.Length - 1); + _exportFormat = Math.Clamp(config.ExportFormat, 0, 3); + _exportFps = config.ExportFps == 30 ? 30 : 60; + _animMode = config.AnimMode == 1 ? 1 : 0; + + //Background now lives on the player config (travels with presets); clamp on load. + config.Player.Background ??= new Core.BackgroundConfig(); + config.Player.Background.Normalize(); + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + //Anchor Toolbox's Shaders/Plugins/Hashes lookups to the exe directory. Its default + //comes from Assembly.Location, which is empty under single-file publish and would null + //those paths (crashing the plugin scan). AppContext.BaseDirectory is always correct. + Toolbox.Core.Runtime.ExecutableDir = AppContext.BaseDirectory.TrimEnd( + Path.DirectorySeparatorChar, + Path.AltDirectorySeparatorChar + ); + + _imgui = new ImGuiController(Width, Height); + Theme.Apply(); + ImGui.GetIO().ConfigWindowsMoveFromTitleBarOnly = true; + + RenderTools.Init(); + Toolbox.Core.FileManager.GetFileFormats(); + + //Interactive mode: compile uncached shader programs asynchronously so a + //new gear's shaders never stall the render thread (meshes pop in a few + //frames later instead). + BfresEditor.TegraShaderDecoder.AllowDeferredCompile = true; + + _pipeline = new ScenePipeline(); + _pipeline.Init(); + + if (Romfs.IsValidRoot(_config.RomfsPath)) + _needsLoad = true; + } + + void LoadGame() + { + _needsLoad = false; + var state = _preserveStateOnLoad && _scene != null ? SceneState.Capture(_scene) : null; + _preserveStateOnLoad = false; + try + { + _standalone?.Dispose(); + _standalone = null; + _scene?.Dispose(); + _scene = null; + CompactHeap(); + + _romfs = new Romfs( + _config.RomfsPath, + _config.LayeredFsPath, + _config.UseLayeredFs, + _config.SdodrRomfsPath + ); + BfresEditor.HoianNXRender.GamePath = _config.RomfsPath; + //Decompress/parse the ~25MB UBER shader archive while the database loads. + BfresEditor.HoianNXRender.PrewarmShaderArchives(); + //Load default/cubemap textures now instead of during the first material render. + BfresEditor.HoianNXRender.InitTextures(); + _db = new GameDatabase(_romfs); + + _scene = new PlayerScene(_romfs, _db); + if (state != null) + { + state.Restore(_scene, _db); + _teamColorIndex = Math.Min( + _teamColorIndex, + Math.Max(_db.TeamColors.Count - 1, 0) + ); + } + else if (_config.Player?.Hair != null || _config.Player?.PlayerType != 0) + { + RestorePlayerConfig(); + } + else + { + _scene.SetPlayerType(0); + _pipeline.FramePlayer(); + } + ApplyTeamColor(); + _romfsError = null; + } + catch (Exception ex) + { + _romfsError = ex.Message; + Console.WriteLine($"[UI] Load failed: {ex}"); + } + } + + /// Snapshot of the scene configuration, reapplied after a LayeredFS reload. + class SceneState + { + int _playerType; + int _eye, + _skin; + string _anim; + float _frame; + bool _paused; + readonly Dictionary _gear = + new(); + + public static SceneState Capture(PlayerScene scene) + { + var s = new SceneState + { + _playerType = scene.PlayerType, + _eye = scene.EyeColor, + _skin = scene.SkinTone, + _anim = scene.CurrentAnimName, + _frame = scene.AnimFrame, + _paused = scene.AnimPaused, + }; + void Add(GearSlot slot, GearEntry e) + { + if (e != null) + s._gear[slot] = (e.RowId, e.Variation, e.CustomPath); + else + s._gear[slot] = (null, 0, null); + } + Add(GearSlot.Hair, scene.CurrentHair); + Add(GearSlot.Eyebrow, scene.CurrentEyebrow); + Add(GearSlot.Head, scene.CurrentHead); + Add(GearSlot.Clothes, scene.CurrentClothes); + Add(GearSlot.Bottom, scene.CurrentBottom); + Add(GearSlot.Shoes, scene.CurrentShoes); + Add(GearSlot.Tank, scene.CurrentTank); + Add(GearSlot.MainWeapon, scene.CurrentWeapon); + return s; + } + + public void Restore(PlayerScene scene, GameDatabase db) + { + scene.SetPlayerType(_playerType); + foreach (var (slot, gear) in _gear) + { + //Defaults set by SetPlayerType stand in when the row disappeared. + if (gear.RowId == null) + { + if ( + slot + is GearSlot.Head + or GearSlot.Clothes + or GearSlot.Shoes + or GearSlot.Tank + or GearSlot.MainWeapon + ) + scene.SetGear(slot, null); + continue; + } + var entry = db.GetList(slot) + .FirstOrDefault(x => + x.RowId == gear.RowId && x.Variation == gear.Variation + ); + if (entry != null) + scene.SetGear(slot, entry); + } + scene.ApplyEyeColor(_eye); + scene.ApplySkinTone(_skin); + if (_anim != null) + { + scene.PlayAnim(_anim); + scene.SetAnimFrame(_frame); + } + scene.AnimPaused = _paused; + } + } + + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + _imgui?.WindowResized(Width, Height); + } + + protected override void OnKeyPress(KeyPressEventArgs e) + { + base.OnKeyPress(e); + _imgui.PressChar(e.KeyChar); + } + + protected override void OnFileDrop(FileDropEventArgs e) + { + base.OnFileDrop(e); + string file = e.FileName; + if ( + file == null + || ( + !file.EndsWith(".bfres") && !file.EndsWith(".bfres.zs") && !file.EndsWith(".zs") + ) + ) + return; + OpenStandalone(file); + } + + /// Opens a loose bfres as a standalone model (no player). + void OpenStandalone(string file) + { + if (_romfs == null) + return; + try + { + bool hadPrevious = _standalone != null; + _standalone?.Dispose(); + _standalone = null; + if (hadPrevious) + CompactHeap(); + + _standalone = StandaloneScene.FromFile(file, _romfs); + _standaloneError = _standalone == null ? "Failed to load model" : null; + if (_standalone != null) + { + _animSearch = ""; + _pipeline.FrameSphere(_standalone.GetBounding()); + } + } + catch (Exception ex) + { + _standaloneError = ex.Message; + Console.WriteLine($"[UI] Standalone load failed: {ex}"); + } + } + + void CloseStandalone() + { + _standalone?.Dispose(); + _standalone = null; + _standaloneError = null; + _animSearch = ""; + CompactHeap(); + _pipeline.FramePlayer(); + } + + static void CompactHeap() + { + GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true); + GC.WaitForPendingFinalizers(); + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true); + } + + protected override void OnClosed(EventArgs e) + { + //Aborts any in-flight capture/encode and deletes the temp raw buffer. + _bufferedExporter?.Dispose(); + //Width/Height are 0 when closed while minimized; don't persist that. + if (WindowState == WindowState.Normal && Width > 0 && Height > 0) + { + _config.WindowWidth = Width; + _config.WindowHeight = Height; + } + _config.Save(); + base.OnClosed(e); + } + + protected override void OnRenderFrame(FrameEventArgs e) + { + base.OnRenderFrame(e); + GLFrameworkEngine.ShaderProgram.FrameStamp++; + + if (_needsLoad) + { + LoadGame(); + if (AutoOpenFile != null && _scene != null) + { + OpenStandalone(AutoOpenFile); + AutoOpenFile = null; + } + } + + //Advance whichever scene is active. During export we drive the timeline + //deterministically (fixed frame + fixed dt) instead of by real time. + if (_animExporting) + { + //Chain export walks the concatenated sequence; single export scrubs one anim. + if (_animExportChain) + ChainSeek(_animExportIndex); + else + PlaybackSetFrame(_animExportIndex); + //Cloth dt is wall-clock per output frame (1/fps), independent of playback + //speed; matches the viewport, where the sim advances in real time and the + //speed slider only scales how fast the animation cursor moves. + PlaybackUpdate(1f / _exportFps); + _uiFrame = PlaybackAnimFrame; + } + else if (_chainActive) + { + UpdateAnimChain((float)e.Time); + _uiFrame = PlaybackAnimFrame; + } + else if (_standalone != null) + { + _standalone.Update((float)e.Time); + _uiFrame = _standalone.AnimFrame; + } + else if (_scene != null) + { + _scene.Update((float)e.Time); + _uiFrame = _scene.AnimFrame; + } + + _imgui.Update(this, (float)e.Time); + DrawUI(); + + GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); + GL.Viewport(0, 0, Width, Height); + GL.ClearColor(0.04f, 0.04f, 0.05f, 1); + GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); + _imgui.Render(); + + SwapBuffers(); + + //Frame-exact export: capture this frame synchronously, then advance the timeline. + if (_animExporting) + CaptureAnimExportFrame(); + } + } +} diff --git a/PlayerViewer/UI/Widgets.cs b/PlayerViewer/UI/Widgets.cs index 9031db0..401ac48 100644 --- a/PlayerViewer/UI/Widgets.cs +++ b/PlayerViewer/UI/Widgets.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Numerics; using ImGuiNET; using PlayerViewer.Core; @@ -8,7 +7,8 @@ namespace PlayerViewer.UI { /// - /// Reusable themed widgets: searchable gear combos, section headers. + /// Reusable themed widgets: searchable gear combos, section headers, labeled rows, + /// and bound controls. /// public static class Widgets { @@ -24,12 +24,27 @@ public static void SectionHeader(string text) ImGui.Spacing(); } + /// Label + combo on one line with fixed label column. + public static void LabeledRow(string label, Action drawControl) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(Theme.TextDim, label); + ImGui.SameLine(92); + drawControl(); + } + /// /// Searchable combo for a gear list. Returns true when the selection changed /// (selected receives the new entry, null = none). /// - public static bool GearCombo(string label, List entries, GearEntry current, - out GearEntry selected, bool allowNone = true, string noneLabel = "Blank") + public static bool GearCombo( + string label, + List entries, + GearEntry current, + out GearEntry selected, + bool allowNone = true, + string noneLabel = "Blank" + ) { selected = current; bool changed = false; @@ -66,13 +81,21 @@ public static bool GearCombo(string label, List entries, GearEntry cu { if (entry.IsCustom != group) continue; - if (!MatchesSearch(entry.DisplayName, search) && !MatchesSearch(entry.Label ?? "", search)) + if ( + !MatchesSearch(entry.DisplayName, search) + && !MatchesSearch(entry.Label ?? "", search) + ) continue; if (entry.IsCustom) ImGui.PushStyleColor(ImGuiCol.Text, Theme.GoldBright); bool isSelected = entry == current; - if (ImGui.Selectable($"{entry.DisplayName}##{entries.IndexOf(entry)}", isSelected)) + if ( + ImGui.Selectable( + $"{entry.DisplayName}##{entries.IndexOf(entry)}", + isSelected + ) + ) { selected = entry; changed = true; @@ -97,17 +120,146 @@ public static bool GearCombo(string label, List entries, GearEntry cu static bool MatchesSearch(string text, string search) { - return string.IsNullOrEmpty(search) || - text.Contains(search, StringComparison.OrdinalIgnoreCase); + return string.IsNullOrEmpty(search) + || text.Contains(search, StringComparison.OrdinalIgnoreCase); } - /// Label + combo on one line with fixed label column. - public static void LabeledRow(string label, Action drawControl) + /// Full-width button that dims and no-ops when disabled. + public static void DisabledButton(string label, bool enabled, Action onClick) { - ImGui.AlignTextToFramePadding(); - ImGui.TextColored(Theme.TextDim, label); - ImGui.SameLine(92); - drawControl(); + if (!enabled) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.45f); + if (ImGui.Button(label, new Vector2(-1, 0)) && enabled) + onClick(); + if (!enabled) + ImGui.PopStyleVar(); + } + + /// Full-width red (destructive/cancel) button. + public static void RedButton(string label, Action onClick) + { + ImGui.PushStyleColor(ImGuiCol.Button, Theme.RedButtonBg); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Theme.RedButtonHover); + if (ImGui.Button(label, new Vector2(-1, 0))) + onClick(); + ImGui.PopStyleColor(2); + } + + /// Muted caption/status text. + public static void DimText(string text) => ImGui.TextColored(Theme.TextDim, text); + + /// Red error/warning text. + public static void ErrorText(string text) => ImGui.TextColored(Theme.Error, text); + + /// Green success/active text. + public static void SuccessText(string text) => ImGui.TextColored(Theme.Success, text); + + /// Tooltip shown when the last-drawn item is hovered. + public static void ItemTooltip(string text) + { + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(text); + } + + //Read a value, draw the control, and on edit push the new value through then + //run (persist/side effects). Each returns true when the value changed. + + public static bool Checkbox( + string label, + bool value, + Action set, + Action onChanged = null + ) + { + bool v = value; + if (!ImGui.Checkbox(label, ref v)) + return false; + set(v); + onChanged?.Invoke(); + return true; + } + + public static bool SliderInt( + string label, + int value, + int min, + int max, + Action set, + Action onChanged = null, + string format = "%d" + ) + { + int v = value; + if (!ImGui.SliderInt(label, ref v, min, max, format)) + return false; + set(v); + onChanged?.Invoke(); + return true; + } + + public static bool SliderFloat( + string label, + float value, + float min, + float max, + Action set, + Action onChanged = null, + string format = "%.2f" + ) + { + float v = value; + if (!ImGui.SliderFloat(label, ref v, min, max, format)) + return false; + set(v); + onChanged?.Invoke(); + return true; + } + + public static bool InputInt( + string label, + int value, + Action set, + Action onChanged = null + ) + { + int v = value; + if (!ImGui.InputInt(label, ref v)) + return false; + set(v); + onChanged?.Invoke(); + return true; + } + + public static bool Combo( + string label, + int value, + string[] items, + Action set, + Action onChanged = null + ) + { + int v = value; + if (!ImGui.Combo(label, ref v, items, items.Length)) + return false; + set(v); + onChanged?.Invoke(); + return true; + } + + public static bool ColorEdit3( + string label, + Vector3 value, + Action set, + ImGuiColorEditFlags flags = ImGuiColorEditFlags.None, + Action onChanged = null + ) + { + Vector3 v = value; + if (!ImGui.ColorEdit3(label, ref v, flags)) + return false; + set(v); + onChanged?.Invoke(); + return true; } } } diff --git a/ShaderLibrary/BfshaFile.cs b/ShaderLibrary/BfshaFile.cs index c0225e1..727f6c6 100644 --- a/ShaderLibrary/BfshaFile.cs +++ b/ShaderLibrary/BfshaFile.cs @@ -1,12 +1,4 @@ -using BfshaLibrary.WiiU; -using Microsoft.VisualBasic.FileIO; -using ShaderLibrary.Common; -using ShaderLibrary.Helpers; -using ShaderLibrary.IO; -using ShaderLibrary.Switch; -using ShaderLibrary.WiiU; -using Silk.NET.OpenGL; -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -14,6 +6,14 @@ using System.Text; using System.Threading.Tasks; using System.Xml.Linq; +using BfshaLibrary.WiiU; +using Microsoft.VisualBasic.FileIO; +using ShaderLibrary.Common; +using ShaderLibrary.Helpers; +using ShaderLibrary.IO; +using ShaderLibrary.Switch; +using ShaderLibrary.WiiU; +using Silk.NET.OpenGL; using static ShaderLibrary.BnshFile; namespace ShaderLibrary @@ -35,12 +35,14 @@ public class BfshaFile public StringPool StringPool = new StringPool(); - public BfshaFile() { + public BfshaFile() + { BinHeader = new BinaryHeader(); BinHeader.Magic = 2314885531374736198; } public BfshaFile(string filePath) => Read(File.OpenRead(filePath)); + public BfshaFile(Stream stream) => Read(stream); public void Save(string filePath) @@ -83,19 +85,33 @@ public void Read(Stream stream) BfshaLoader.Read(stream, this); } - public void ExportProgram(Stream stream, string name, ShaderModel shader, params BfshaShaderProgram[] programs) + public void ExportProgram( + Stream stream, + string name, + ShaderModel shader, + params BfshaShaderProgram[] programs + ) { var programIndices = programs.Select(x => shader.Programs.IndexOf(x)).ToList(); ExportProgram(stream, name, shader, programIndices.ToArray()); } - public void ExportProgram(Stream stream, string name, ShaderModel shader, params int[] programIndices) + public void ExportProgram( + Stream stream, + string name, + ShaderModel shader, + params int[] programIndices + ) { var bfsha = CreateNewArchive(name, shader, programIndices); bfsha.Save(stream); } - public BfshaFile CreateNewArchive(string name, ShaderModel shader, params int[] programIndices) + public BfshaFile CreateNewArchive( + string name, + ShaderModel shader, + params int[] programIndices + ) { //Export as a separate usable bfsha binary var programs = programIndices.Select(x => shader.Programs[x]).ToList(); @@ -104,7 +120,7 @@ public BfshaFile CreateNewArchive(string name, ShaderModel shader, params int[] { BinHeader = BinHeader, Name = name, - Path = this.Path, + Path = this.Path, IsWiiU = IsWiiU, StringPool = StringPool, Flags = Flags, @@ -127,31 +143,38 @@ public BfshaFile CreateNewArchive(string name, ShaderModel shader, params int[] Header = shader.BnshFile.Header, Variations = new List(), }; - // bnsh.BinHeader.VersionMajor = 2; - // bnsh.BinHeader.VersionMicro = 5; - // bnsh.BinHeader.VersionMinor = 1; + // bnsh.BinHeader.VersionMajor = 2; + // bnsh.BinHeader.VersionMicro = 5; + // bnsh.BinHeader.VersionMinor = 1; List exported_programs = new List(); foreach (var prog in programs) { //Setup program - exported_programs.Add(new BfshaShaderProgram() - { - UsedAttributeFlags = prog.UsedAttributeFlags, - Flags = prog.Flags, - ImageIndices = prog.ImageIndices, - SamplerIndices = prog.SamplerIndices, - UniformBlockIndices = prog.UniformBlockIndices, - StorageBufferIndices = prog.StorageBufferIndices, - VariationIndex = bnsh.Variations.Count, - ParentShader = shader, - }); + exported_programs.Add( + new BfshaShaderProgram() + { + UsedAttributeFlags = prog.UsedAttributeFlags, + Flags = prog.Flags, + ImageIndices = prog.ImageIndices, + SamplerIndices = prog.SamplerIndices, + UniformBlockIndices = prog.UniformBlockIndices, + StorageBufferIndices = prog.StorageBufferIndices, + VariationIndex = bnsh.Variations.Count, + ParentShader = shader, + } + ); //add variation from the bnsh to newly made one - bnsh.Variations.Add(new ShaderVariation() - { - BinaryProgram = shader.BnshFile.Variations[prog.VariationIndex].BinaryProgram, - }); + bnsh.Variations.Add( + new ShaderVariation() + { + BinaryProgram = shader + .BnshFile + .Variations[prog.VariationIndex] + .BinaryProgram, + } + ); } ShaderModel export_shader = new ShaderModel() @@ -173,9 +196,9 @@ public BfshaFile CreateNewArchive(string name, ShaderModel shader, params int[] BlockIndices = shader.BlockIndices, UnknownIndices2 = shader.UnknownIndices2, BnshFile = bnsh, - Programs = exported_programs, + Programs = exported_programs, MaxRingItemSize = shader.MaxRingItemSize, - MaxVSRingItemSize = shader.MaxVSRingItemSize, + MaxVSRingItemSize = shader.MaxVSRingItemSize, }; bfsha.ShaderModels.Add(export_shader.Name, export_shader); @@ -189,67 +212,84 @@ public class ShaderModel : IResData /// Gets or sets the name of the shader model. /// public string Name { get; set; } + /// /// Gets or sets a lookup of static options. /// Used for looking up shader programs. /// public ResDict StaticOptions { get; set; } = new ResDict(); + /// /// Gets or sets a lookup of dynamic options. /// Used for looking up shader programs. /// public ResDict DynamicOptions { get; set; } = new ResDict(); + /// /// Gets or sets a list of shader programs. /// public List Programs { get; set; } = new List(); + /// /// Gets or sets a lookup of storage buffers. /// - public ResDict StorageBuffers { get; set; } = new ResDict(); + public ResDict StorageBuffers { get; set; } = + new ResDict(); + /// /// Gets or sets a lookup of uniform blocks. /// - public ResDict UniformBlocks { get; set; } = new ResDict(); + public ResDict UniformBlocks { get; set; } = + new ResDict(); + /// /// Gets or sets a lookup of image buffers. /// public ResDict Images { get; set; } = new ResDict(); + /// /// Gets or sets a lookup of samplers. /// public ResDict Samplers { get; set; } = new ResDict(); + /// /// Gets or sets a lookup of attributes. /// public ResDict Attributes { get; set; } = new ResDict(); + /// /// Gets or sets a table of symbols. /// This contains strings which typically match the bnsh reflection names. /// - public SymbolData SymbolData { get; set; } + public SymbolData SymbolData { get; set; } + /// /// Gets or sets the bnsh file used to store Switch shader data. /// public BnshFile BnshFile { get; set; } + /// /// The index for what default program to use when option searching fails. /// public int DefaultProgramIndex = -1; + /// /// The amount of int32 keys used for static options. /// public byte StaticKeyLength; + /// /// The amount of int32 keys used for dynamic options. /// public byte DynamicKeyLength; + /// /// A list of int32 keys used for searching shader programs. /// A key is made up of static and dynamic option values set via bits. /// Each program has StaticKeyLength + DynamicKeyLength keys in the order of the table. /// public int[] KeyTable { get; set; } + /// /// An unknown value. /// @@ -266,25 +306,27 @@ public class ShaderModel : IResData public byte[] UnknownIndices2 = new byte[4]; /// - /// + /// /// public int MaxVSRingItemSize = 0; /// - /// + /// /// public int MaxRingItemSize = 0; public BnshFile.ShaderVariation GetVariation(int program_index) { - if (program_index == -1) return null; + if (program_index == -1) + return null; return this.BnshFile.Variations[this.Programs[program_index].VariationIndex]; } public BnshFile.ShaderVariation GetVariation(BfshaShaderProgram program) { - if (program == null) return null; + if (program == null) + return null; return this.BnshFile.Variations[program.VariationIndex]; } @@ -312,9 +354,12 @@ public List GetProgramIndexList(Dictionary options) return indices; } - public void CreateAddNewShaderProgram(BnshFile.ShaderVariation variation, - Dictionary options, - GLSLCompile glslShaderVert, GLSLCompile glslShaderFrag) + public void CreateAddNewShaderProgram( + BnshFile.ShaderVariation variation, + Dictionary options, + GLSLCompile glslShaderVert, + GLSLCompile glslShaderFrag + ) { // Add variation if (!this.BnshFile.Variations.Contains(variation)) @@ -325,36 +370,58 @@ public void CreateAddNewShaderProgram(BnshFile.ShaderVariation variation, // Set locations for (int i = 0; i < this.Samplers.Count; i++) - program.SamplerIndices.Add(new ShaderIndexHeader() - { - VertexLocation = glslShaderVert.GetSamplerLocation(this.Samplers.GetKey(i)), - FragmentLocation = glslShaderFrag.GetSamplerLocation(this.Samplers.GetKey(i)), - }); + program.SamplerIndices.Add( + new ShaderIndexHeader() + { + VertexLocation = glslShaderVert.GetSamplerLocation(this.Samplers.GetKey(i)), + FragmentLocation = glslShaderFrag.GetSamplerLocation( + this.Samplers.GetKey(i) + ), + } + ); for (int i = 0; i < this.UniformBlocks.Count; i++) - program.UniformBlockIndices.Add(new ShaderIndexHeader() - { - VertexLocation = glslShaderVert.GetUniformBlockLocation(this.UniformBlocks.GetKey(i)), - FragmentLocation = glslShaderFrag.GetUniformBlockLocation(this.UniformBlocks.GetKey(i)), - }); + program.UniformBlockIndices.Add( + new ShaderIndexHeader() + { + VertexLocation = glslShaderVert.GetUniformBlockLocation( + this.UniformBlocks.GetKey(i) + ), + FragmentLocation = glslShaderFrag.GetUniformBlockLocation( + this.UniformBlocks.GetKey(i) + ), + } + ); for (int i = 0; i < this.StorageBuffers.Count; i++) - program.StorageBufferIndices.Add(new ShaderIndexHeader() - { - VertexLocation = glslShaderVert.GetStorageBufferLocation(this.StorageBuffers.GetKey(i)), - FragmentLocation = glslShaderFrag.GetStorageBufferLocation(this.StorageBuffers.GetKey(i)), - }); + program.StorageBufferIndices.Add( + new ShaderIndexHeader() + { + VertexLocation = glslShaderVert.GetStorageBufferLocation( + this.StorageBuffers.GetKey(i) + ), + FragmentLocation = glslShaderFrag.GetStorageBufferLocation( + this.StorageBuffers.GetKey(i) + ), + } + ); for (int i = 0; i < this.Attributes.Count; i++) program.SetAttribute(i, glslShaderVert.HasAttribute(this.Attributes.GetKey(i))); for (int i = 0; i < this.UniformBlocks.Count; i++) - Console.WriteLine($"{this.UniformBlocks.GetKey(i)} {program.UniformBlockIndices[i].VertexLocation}"); + Console.WriteLine( + $"{this.UniformBlocks.GetKey(i)} {program.UniformBlockIndices[i].VertexLocation}" + ); for (int i = 0; i < this.UniformBlocks.Count; i++) - Console.WriteLine($"{this.UniformBlocks.GetKey(i)} {program.UniformBlockIndices[i].FragmentLocation}"); + Console.WriteLine( + $"{this.UniformBlocks.GetKey(i)} {program.UniformBlockIndices[i].FragmentLocation}" + ); for (int i = 0; i < this.Samplers.Count; i++) - Console.WriteLine($"{this.Samplers.GetKey(i)} {program.SamplerIndices[i].FragmentLocation}"); + Console.WriteLine( + $"{this.Samplers.GetKey(i)} {program.SamplerIndices[i].FragmentLocation}" + ); for (int i = 0; i < this.Attributes.Count; i++) Console.WriteLine($"{this.Attributes.GetKey(i)} {program.IsAttributeUsed(i)}"); @@ -371,7 +438,10 @@ public void CreateAddNewShaderProgram(BnshFile.ShaderVariation variation, SetProgramOptions(programIndex, options); } - public void CreateAddNewShaderProgramWiiU(BfshaShaderProgram program, Dictionary options) + public void CreateAddNewShaderProgramWiiU( + BfshaShaderProgram program, + Dictionary options + ) { //expand key table int[] program_keys = new int[this.StaticKeyLength + this.DynamicKeyLength]; @@ -393,7 +463,7 @@ public void SetProgramOptions(int programIndex, Dictionary optio if (options.ContainsKey(staticOption.Name)) choice = options[staticOption.Name]; - SetOptionKey(staticOption, choice, programIndex); + SetOptionKey(staticOption, choice, programIndex); } foreach (var dynamicOption in DynamicOptions.Values) { @@ -444,7 +514,9 @@ public void PrintProgramKeys(int programIndex) var option = this.StaticOptions[j]; int choiceIndex = option.GetChoiceIndex(KeyTable[baseIndex + option.Bit32Index]); if (choiceIndex > option.Choices.Count || choiceIndex == -1) - throw new Exception($"Invalid choice index in key table! {option.Name} index {choiceIndex}"); + throw new Exception( + $"Invalid choice index in key table! {option.Name} index {choiceIndex}" + ); Console.WriteLine($"{option.Name} = {option.Choices.GetKey(choiceIndex)}"); } @@ -453,9 +525,13 @@ public void PrintProgramKeys(int programIndex) { var option = this.DynamicOptions[j]; int ind = option.Bit32Index - option.KeyOffset; - int choiceIndex = option.GetChoiceIndex(KeyTable[baseIndex + StaticKeyLength + ind]); + int choiceIndex = option.GetChoiceIndex( + KeyTable[baseIndex + StaticKeyLength + ind] + ); if (choiceIndex > option.Choices.Count || choiceIndex == -1) - throw new Exception($"Invalid choice index in key table! {option.Name} index {choiceIndex}"); + throw new Exception( + $"Invalid choice index in key table! {option.Name} index {choiceIndex}" + ); Console.WriteLine($"{option.Name} = {option.Choices.GetKey(choiceIndex)}"); } @@ -474,9 +550,13 @@ public void DumpProgramChoices(int programIndex, string path) for (int j = 0; j < this.StaticOptions.Count; j++) { var option = this.StaticOptions[j]; - int choiceIndex = option.GetChoiceIndex(KeyTable[baseIndex + option.Bit32Index]); + int choiceIndex = option.GetChoiceIndex( + KeyTable[baseIndex + option.Bit32Index] + ); if (choiceIndex > option.Choices.Count || choiceIndex == -1) - throw new Exception($"Invalid choice index in key table! {option.Name} index {choiceIndex}"); + throw new Exception( + $"Invalid choice index in key table! {option.Name} index {choiceIndex}" + ); writer.WriteLine($"{option.Name} = {option.Choices.GetKey(choiceIndex)}"); } @@ -485,9 +565,13 @@ public void DumpProgramChoices(int programIndex, string path) { var option = this.DynamicOptions[j]; int ind = option.Bit32Index - option.KeyOffset; - int choiceIndex = option.GetChoiceIndex(KeyTable[baseIndex + StaticKeyLength + ind]); + int choiceIndex = option.GetChoiceIndex( + KeyTable[baseIndex + StaticKeyLength + ind] + ); if (choiceIndex > option.Choices.Count || choiceIndex == -1) - throw new Exception($"Invalid choice index in key table! {option.Name} index {choiceIndex}"); + throw new Exception( + $"Invalid choice index in key table! {option.Name} index {choiceIndex}" + ); writer.WriteLine($"{option.Name} = {option.Choices.GetKey(choiceIndex)}"); } @@ -504,7 +588,9 @@ public string GetOptionChoice(int programIndex, string option_name) var option = this.StaticOptions[option_name]; int choiceIndex = option.GetChoiceIndex(KeyTable[baseIndex + option.Bit32Index]); if (choiceIndex > option.Choices.Count || choiceIndex == -1) - throw new Exception($"Invalid choice index in key table! {option.Name} index {choiceIndex}"); + throw new Exception( + $"Invalid choice index in key table! {option.Name} index {choiceIndex}" + ); return option.Choices.GetKey(choiceIndex); } @@ -534,6 +620,7 @@ public override string ToString() } public SymbolEntry() { } + public SymbolEntry(string name, string symbol) { Name1 = name; @@ -607,7 +694,11 @@ public class ResUint32 : IResData public uint Value { get; set; } public ResUint32() { } - public ResUint32(uint value) { this.Value = value; } + + public ResUint32(uint value) + { + this.Value = value; + } public void Read(BinaryDataReader reader) { @@ -660,10 +751,7 @@ public class BfshaUniform : IResData public byte GX2ParamType { get; set; } } - public class BfshaImageBuffer : IResData - { - - } + public class BfshaImageBuffer : IResData { } public class BfshaStorageBuffer : IResData { @@ -736,7 +824,7 @@ public void SetAttribute(int index, bool bind) public void ResetLocations(ShaderModel shadermodel) { UniformBlockIndices = new ShaderIndexHeader[shadermodel.UniformBlocks.Count].ToList(); - SamplerIndices = new ShaderIndexHeader[shadermodel.Samplers.Count].ToList(); + SamplerIndices = new ShaderIndexHeader[shadermodel.Samplers.Count].ToList(); StorageBufferIndices = new ShaderIndexHeader[shadermodel.StorageBuffers.Count].ToList(); ImageIndices = new ShaderIndexHeader[shadermodel.Images.Count].ToList(); diff --git a/ShaderLibrary/BnshFile.cs b/ShaderLibrary/BnshFile.cs index 525d9b0..37f0f15 100644 --- a/ShaderLibrary/BnshFile.cs +++ b/ShaderLibrary/BnshFile.cs @@ -1,6 +1,4 @@ -using ShaderLibrary.IO; -using ShaderLibrary.Switch; -using System; +using System; using System.Collections.Generic; using System.Dynamic; using System.IO; @@ -8,6 +6,8 @@ using System.Runtime; using System.Text; using System.Threading.Tasks; +using ShaderLibrary.IO; +using ShaderLibrary.Switch; namespace ShaderLibrary { @@ -27,6 +27,7 @@ public class BnshFile /// Gets or sets the res file header. /// public BinaryHeader BinHeader; //A header shared between bnsh and other formats + /// /// Gets or sets the shader header. /// @@ -36,7 +37,8 @@ public class BnshFile internal Stream _stream; - public BnshFile() { + public BnshFile() + { Variations = new List(); BinHeader = new BinaryHeader(); Header = new BnshHeader(); @@ -67,7 +69,7 @@ public BnshFile() { /// Saves the binary to a file stream. /// /// - public void Save(string filePath) + public void Save(string filePath) { using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write)) Save(fs); @@ -99,7 +101,7 @@ public void ExportVariation(string filePath, params ShaderVariation[] variation) bnsh.Save(filePath); } - public class ShaderVariation + public class ShaderVariation { private BnshShaderProgram _program; @@ -114,11 +116,13 @@ public BnshShaderProgram BinaryProgram if (_program == null) _program = BnshLoader.ReadBnshShaderProgram(this); return _program; - } set => _program = value; + } + set => _program = value; } // Variation header internal VariationHeader header; + // Stream to read program data internal Stream _stream; @@ -135,28 +139,33 @@ public void Export(string filePath) } } - public class BnshShaderProgram + public class BnshShaderProgram { /// /// Gets or sets vertex shader code. /// public ShaderCode VertexShader { get; set; } + /// /// Gets or sets fragment shader code. /// public ShaderCode FragmentShader { get; set; } + /// /// Gets or sets geometry shader code. /// public ShaderCode GeometryShader { get; set; } + /// /// Gets or sets compute shader code. /// public ShaderCode ComputeShader { get; set; } + /// /// Gets or sets hull shader code. /// public ShaderCode TessellationControlShader { get; set; } + /// /// Gets or sets domain shader code. /// @@ -206,7 +215,8 @@ public BnshShaderProgram() { header = new BnshShaderProgramHeader() { - Flags = 2, ObjectSize = 256, + Flags = 2, + ObjectSize = 256, Reserved8 = 128104, }; } @@ -332,7 +342,7 @@ void Set(ResDict dict, ref int startIdx) public int GetInputLocation(string key) { - var index = this.Inputs.Keys.ToList().IndexOf(key); + var index = this.Inputs.Keys.ToList().IndexOf(key); if (Slots.Length > index) return Slots[index]; return -1; diff --git a/ShaderLibrary/Common/Enums.cs b/ShaderLibrary/Common/Enums.cs index 5a3f581..a65e070 100644 --- a/ShaderLibrary/Common/Enums.cs +++ b/ShaderLibrary/Common/Enums.cs @@ -11,7 +11,7 @@ public enum GX2FetchShaderType TESSELLATION_NONE = 0, TESSELLATION_LINE = 1, TESSELLATION_TRIANGLE = 2, - TESSELLATION_QUAD = 3 + TESSELLATION_QUAD = 3, } public enum GX2ShaderMode @@ -19,7 +19,7 @@ public enum GX2ShaderMode UNIFORM_REGISTER = 0, UNIFORM_BLOCK = 1, GEOMETRY_SHADER = 2, - COMPUTE_SHADER = 3 + COMPUTE_SHADER = 3, } public enum GX2SamplerVarType @@ -74,7 +74,7 @@ public enum GX2ShaderVarType DOUBLE3X4 = 35, DOUBLE4X2 = 36, DOUBLE4X3 = 37, - DOUBLE4X4 = 38 + DOUBLE4X4 = 38, } /// @@ -113,6 +113,6 @@ public enum ShaderParamType : byte Srt2D, Srt3D, TexSrt, - TexSrtEx + TexSrtEx, } } diff --git a/ShaderLibrary/Common/IResData.cs b/ShaderLibrary/Common/IResData.cs index bf4baf5..353710d 100644 --- a/ShaderLibrary/Common/IResData.cs +++ b/ShaderLibrary/Common/IResData.cs @@ -1,13 +1,11 @@ -using ShaderLibrary.IO; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using ShaderLibrary.IO; namespace ShaderLibrary { - public interface IResData - { - } + public interface IResData { } } diff --git a/ShaderLibrary/Common/RelocationTable.cs b/ShaderLibrary/Common/RelocationTable.cs index b2c5e97..7ca324c 100644 --- a/ShaderLibrary/Common/RelocationTable.cs +++ b/ShaderLibrary/Common/RelocationTable.cs @@ -1,9 +1,9 @@ -using ShaderLibrary.IO; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using ShaderLibrary.IO; namespace ShaderLibrary.Common { @@ -72,19 +72,44 @@ internal void SetRelocationSection(int section_idx, uint section_offset, uint se Sections[section_idx].Size = section_size; } - public void SaveEntry(BinaryWriter writer, uint offsetCount, uint structCount, uint paddingCount, uint section_idx, string hint) + public void SaveEntry( + BinaryWriter writer, + uint offsetCount, + uint structCount, + uint paddingCount, + uint section_idx, + string hint + ) { if (section_idx > Sections.Length) section_idx = 0; - Sections[section_idx].Entries.Add(new RelocationEntry((uint)writer.BaseStream.Position, - offsetCount, structCount, paddingCount, hint)); + Sections[section_idx] + .Entries.Add( + new RelocationEntry( + (uint)writer.BaseStream.Position, + offsetCount, + structCount, + paddingCount, + hint + ) + ); } - public void SaveEntry(BinaryWriter writer, long pos, uint offsetCount, uint structCount, uint paddingCount, uint section_idx, string hint) + public void SaveEntry( + BinaryWriter writer, + long pos, + uint offsetCount, + uint structCount, + uint paddingCount, + uint section_idx, + string hint + ) { - Sections[section_idx].Entries.Add(new RelocationEntry((uint)pos, - offsetCount, structCount, paddingCount, hint)); + Sections[section_idx] + .Entries.Add( + new RelocationEntry((uint)pos, offsetCount, structCount, paddingCount, hint) + ); } private class RelocationSection @@ -112,7 +137,13 @@ private class RelocationEntry internal string Hint; internal int SectionIdx; - internal RelocationEntry(uint position, uint offsetCount, uint structCount, uint padingCount, string hint) + internal RelocationEntry( + uint position, + uint offsetCount, + uint structCount, + uint padingCount, + string hint + ) { Position = position; StructCount = structCount; diff --git a/ShaderLibrary/Common/ResString.cs b/ShaderLibrary/Common/ResString.cs index ce97e07..0c3a4b4 100644 --- a/ShaderLibrary/Common/ResString.cs +++ b/ShaderLibrary/Common/ResString.cs @@ -1,18 +1,15 @@ -using ShaderLibrary.IO; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using ShaderLibrary.IO; namespace ShaderLibrary { public class ResString : IResData { - public string String - { - get; set; - } + public string String { get; set; } public static implicit operator ResString(string value) { @@ -24,10 +21,11 @@ public static implicit operator string(ResString value) return value.String; } - public override string ToString() { return String; } - - public void Read(BinaryDataReader reader) + public override string ToString() { + return String; } + + public void Read(BinaryDataReader reader) { } } } diff --git a/ShaderLibrary/Common/ResStringComparer.cs b/ShaderLibrary/Common/ResStringComparer.cs index 2a4f5cb..f2d6ee9 100644 --- a/ShaderLibrary/Common/ResStringComparer.cs +++ b/ShaderLibrary/Common/ResStringComparer.cs @@ -13,9 +13,7 @@ internal class ResStringComparer : StringComparer // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ - private ResStringComparer() - { - } + private ResStringComparer() { } // ---- PROPERTIES --------------------------------------------------------------------------------------------- @@ -35,9 +33,12 @@ internal static ResStringComparer Instance public override int Compare(string x, string y) { - if (ReferenceEquals(x, y)) return 0; - if (String.IsNullOrEmpty(x)) return 1; - if (String.IsNullOrEmpty(y)) return -1; + if (ReferenceEquals(x, y)) + return 0; + if (String.IsNullOrEmpty(x)) + return 1; + if (String.IsNullOrEmpty(y)) + return -1; return String.CompareOrdinal(x, y); } diff --git a/ShaderLibrary/Common/StringTable.cs b/ShaderLibrary/Common/StringTable.cs index 4296791..72bc704 100644 --- a/ShaderLibrary/Common/StringTable.cs +++ b/ShaderLibrary/Common/StringTable.cs @@ -1,15 +1,16 @@ -using ShaderLibrary.IO; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using ShaderLibrary.IO; namespace ShaderLibrary.Common { public class StringTable { - private Dictionary _savedStrings = new Dictionary(); + private Dictionary _savedStrings = + new Dictionary(); private long _ofsStringTable; private long _ofsFileName; @@ -35,10 +36,7 @@ public void AddEntry(long ofs, string str) _savedStrings[str].Positions.Add(ofs); else { - _savedStrings.Add(str, new StringEntry() - { - Positions = new List() { ofs }, - }); + _savedStrings.Add(str, new StringEntry() { Positions = new List() { ofs } }); } } @@ -47,7 +45,9 @@ public void Write(BinaryDataWriter writer) writer.AlignBytes(8); // Sort the strings ordinally. - SortedList sorted = new SortedList(ResStringComparer.Instance); + SortedList sorted = new SortedList( + ResStringComparer.Instance + ); foreach (KeyValuePair entry in _savedStrings) sorted.Add(entry.Key, entry.Value); @@ -57,7 +57,7 @@ public void Write(BinaryDataWriter writer) writer.SaveHeaderBlock(); writer.Write(sorted.Count); - //save file name from binary header + //save file name from binary header if (_ofsFileName != 0) { writer.Write((short)fileName.Length); diff --git a/ShaderLibrary/ControlShader.cs b/ShaderLibrary/ControlShader.cs index c759819..ba98757 100644 --- a/ShaderLibrary/ControlShader.cs +++ b/ShaderLibrary/ControlShader.cs @@ -1,5 +1,4 @@ -using Microsoft.VisualBasic; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -7,6 +6,7 @@ using System.Runtime.InteropServices.ComTypes; using System.Text; using System.Threading.Tasks; +using Microsoft.VisualBasic; namespace ShaderLibrary { @@ -77,7 +77,8 @@ public ControlShader() public ControlShader(byte[] data) { - using (var reader = new BinaryReader(new MemoryStream(data))) { + using (var reader = new BinaryReader(new MemoryStream(data))) + { Read(reader); } } @@ -132,7 +133,7 @@ private void Read(BinaryReader reader) { reader.ReadUInt32(), reader.ReadUInt32(), - reader.ReadUInt32() + reader.ReadUInt32(), }, SharedMemSz = reader.ReadUInt32(), LocalPosMemSz = reader.ReadUInt32(), @@ -164,7 +165,8 @@ public void Save(string filePath) public void Save(Stream stream) { - using (var writer = new BinaryWriter(stream)) { + using (var writer = new BinaryWriter(stream)) + { Write(writer); } } @@ -238,14 +240,14 @@ public byte[] GetConstants(byte[] shader_code) using (var reader = new BinaryReader(new MemoryStream(shader_code))) { reader.BaseStream.Seek(this.ConstBufOffset, SeekOrigin.Begin); - return reader.ReadBytes((int)this.ConstBufSize); + return reader.ReadBytes((int)this.ConstBufSize); } } public void SetConstants(byte[] shader_code, byte[] constants, out byte[] new_shader_code) { this.ConstBufSize = (uint)constants.Length; //constants size - this.ProgramSize = GetBytecodeLength(shader_code); //shader code without the header + this.ProgramSize = GetBytecodeLength(shader_code); //shader code without the header //save to output shader bytecode var mem = new MemoryStream(); @@ -295,7 +297,10 @@ private uint GetBytecodeLength(byte[] code) static void AlignBytes(BinaryWriter wr, int align, byte pad_val = 0) { var startPos = wr.BaseStream.Position; - long position = wr.Seek((int)(-wr.BaseStream.Position % align + align) % align, SeekOrigin.Current); + long position = wr.Seek( + (int)(-wr.BaseStream.Position % align + align) % align, + SeekOrigin.Current + ); wr.Seek((int)startPos, System.IO.SeekOrigin.Begin); while (wr.BaseStream.Position != position) @@ -316,7 +321,7 @@ public enum NVNshaderStage NVN_SHADER_STAGE_GEOMETRY = 2, NVN_SHADER_STAGE_TESS_CONTROL = 3, NVN_SHADER_STAGE_TESS_EVALUATION = 4, - NVN_SHADER_STAGE_COMPUTE = 5 + NVN_SHADER_STAGE_COMPUTE = 5, } } } diff --git a/ShaderLibrary/Dict/ResDict.cs b/ShaderLibrary/Dict/ResDict.cs index 26366cb..d2affde 100644 --- a/ShaderLibrary/Dict/ResDict.cs +++ b/ShaderLibrary/Dict/ResDict.cs @@ -1,4 +1,3 @@ -using ShaderLibrary.IO; using System; using System.Collections; using System.Collections.Generic; @@ -8,10 +7,12 @@ using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; +using ShaderLibrary.IO; namespace ShaderLibrary { - public class ResDict : Dictionary, IResData where T : IResData, new() + public class ResDict : Dictionary, IResData + where T : IResData, new() { internal List _nodes = new List(); @@ -83,13 +84,15 @@ public void Read(BinaryDataReader reader) int i = 0; for (; numNodes >= 0; numNodes--) { - _nodes.Add(new Node() - { - Reference = reader.ReadUInt32(), - IdxLeft = reader.ReadUInt16(), - IdxRight = reader.ReadUInt16(), - Key = reader.LoadString(reader.ReadUInt64()), - }); + _nodes.Add( + new Node() + { + Reference = reader.ReadUInt32(), + IdxLeft = reader.ReadUInt16(), + IdxRight = reader.ReadUInt16(), + Key = reader.LoadString(reader.ReadUInt64()), + } + ); i++; } diff --git a/ShaderLibrary/Dict/ResDictUpdate.cs b/ShaderLibrary/Dict/ResDictUpdate.cs index 02663e7..ff500e5 100644 --- a/ShaderLibrary/Dict/ResDictUpdate.cs +++ b/ShaderLibrary/Dict/ResDictUpdate.cs @@ -1,10 +1,10 @@ using System; -using System.Numerics; -using System.Linq; -using System.Text; using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Text; namespace ShaderLibrary { @@ -14,7 +14,10 @@ public class ResDictUpdate static string ToBinaryString(string text, Encoding encoding) { - return string.Join("", encoding.GetBytes(text).Select(n => Convert.ToString(n, 2).PadLeft(8, '0'))); + return string.Join( + "", + encoding.GetBytes(text).Select(n => Convert.ToString(n, 2).PadLeft(8, '0')) + ); } static int _bit(BigInteger n, int b) @@ -50,7 +53,6 @@ static int bit_mismatch(BigInteger int1, BigInteger int2) return -1; } - static int BitLength(BigInteger bits) { int bitLength = 0; @@ -136,13 +138,11 @@ public void Insert(string Name) else newNode.Child[_bit(data, bitIdx) ^ 1] = root; - current.Child[_bit(data, current.bitInx)] = newNode; insertEntry(data, newNode); } else { - int NewBitIdx = first_1bit(data); if (current.Child[_bit(data, bitIdx)] != root) @@ -156,8 +156,7 @@ public void Insert(string Name) } } - - static internal Node[] UpdateNodes(List keys) + internal static Node[] UpdateNodes(List keys) { Node[] _nodes = new Node[keys.Count + 1]; for (int i = 0; i < keys.Count; i++) @@ -169,7 +168,12 @@ static internal Node[] UpdateNodes(List keys) Tree tree = new Tree(); // Create a new root node with empty key so the length can be retrieved throughout the process. - _nodes[0] = new Node() { Key = String.Empty, bitInx = -1, Parent = _nodes[0] }; + _nodes[0] = new Node() + { + Key = String.Empty, + bitInx = -1, + Parent = _nodes[0], + }; // Update the data-referencing nodes. for (ushort i = 1; i < _nodes.Length; i++) @@ -220,6 +224,7 @@ internal Node() Child.Add(this); Reference = UInt32.MaxValue; } + internal string GetName() { BigInteger data = BitLength(Data) + 7 / 8; @@ -227,21 +232,26 @@ internal string GetName() Array.Reverse(stringBytes, 0, stringBytes.Length); //Convert to big endian return Encoding.UTF8.GetString(stringBytes); //Decode byte[] to string } + internal int GetCompactBitIdx() { int byteIndx = bitInx / 8; return (byteIndx << 3) | bitInx - 8 * byteIndx; } - internal Node(BigInteger data, int bitidx, Node parent) : this() + + internal Node(BigInteger data, int bitidx, Node parent) + : this() { Data = data; bitInx = bitidx; Parent = parent; } - internal Node(string key) : this() + + internal Node(string key) + : this() { Key = key; } } } -} \ No newline at end of file +} diff --git a/ShaderLibrary/Dict/ResDictUpdateWiiU.cs b/ShaderLibrary/Dict/ResDictUpdateWiiU.cs index a0601d9..382146d 100644 --- a/ShaderLibrary/Dict/ResDictUpdateWiiU.cs +++ b/ShaderLibrary/Dict/ResDictUpdateWiiU.cs @@ -1,16 +1,16 @@ using System; -using System.Numerics; -using System.Linq; -using System.Text; using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Text; namespace ShaderLibrary { public class ResDictUpdateWiiU { - static internal Node[] UpdateNodes(List keys) + internal static Node[] UpdateNodes(List keys) { List _nodes = new Node[keys.Count + 1].ToList(); for (ushort i = 1; i < keys.Count + 1; i++) @@ -31,13 +31,17 @@ static internal Node[] UpdateNodes(List keys) while (parent.Reference > child.Reference) { parent = child; - child = GetDirection(curKey, child.Reference) == 1 ? _nodes[child.IdxRight] : _nodes[child.IdxLeft]; + child = + GetDirection(curKey, child.Reference) == 1 + ? _nodes[child.IdxRight] + : _nodes[child.IdxLeft]; } uint reference = (uint)Math.Max(curKey.Length, child.Key.Length) * 8; // Check for duplicate keys. while (GetDirection(child.Key, reference) == GetDirection(curKey, reference)) { - if (reference == 0) throw new Exception($"Duplicate key \"{curKey}\"."); + if (reference == 0) + throw new Exception($"Duplicate key \"{curKey}\"."); reference--; } current.Reference = reference; @@ -49,7 +53,10 @@ static internal Node[] UpdateNodes(List keys) while (parent.Reference > child.Reference && child.Reference > reference) { parent = child; - child = GetDirection(curKey, child.Reference) == 1 ? _nodes[child.IdxRight] : _nodes[child.IdxLeft]; + child = + GetDirection(curKey, child.Reference) == 1 + ? _nodes[child.IdxRight] + : _nodes[child.IdxLeft]; } // Attach left or right depending on the resulting direction bit. if (GetDirection(curKey, current.Reference) == 1) @@ -101,4 +108,4 @@ internal Node() } } } -} \ No newline at end of file +} diff --git a/ShaderLibrary/GLSLParser/GLSLCompile.cs b/ShaderLibrary/GLSLParser/GLSLCompile.cs index fb0991f..d2042c8 100644 --- a/ShaderLibrary/GLSLParser/GLSLCompile.cs +++ b/ShaderLibrary/GLSLParser/GLSLCompile.cs @@ -1,12 +1,12 @@ -using Silk.NET.Core.Native; -using Silk.NET.OpenGL; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using System.Xml.Linq; +using Silk.NET.Core.Native; +using Silk.NET.OpenGL; namespace ShaderLibrary { @@ -52,28 +52,32 @@ public bool HasAttribute(string name) public int GetSamplerLocation(string name) { name = SamplerSymbols.ContainsKey(name) ? SamplerSymbols[name] : name; - return this.Samplers.ContainsKey(name) ? this.Samplers[name] : -1; + return this.Samplers.ContainsKey(name) ? this.Samplers[name] : -1; } - + public int GetUniformBlockLocation(string name) { name = UniformBlockSymbols.ContainsKey(name) ? UniformBlockSymbols[name] : name; return this.UniformBlocks.ContainsKey(name) ? this.UniformBlocks[name] : -1; } - public int GetStorageBufferLocation(string name) - => this.StorageBuffers.ContainsKey(name) ? this.StorageBuffers[name] : -1; - public int GetOutputLocation(string name) - => this.Outputs.ContainsKey(name) ? this.Outputs[name] : -1; + public int GetStorageBufferLocation(string name) => + this.StorageBuffers.ContainsKey(name) ? this.StorageBuffers[name] : -1; + + public int GetOutputLocation(string name) => + this.Outputs.ContainsKey(name) ? this.Outputs[name] : -1; public unsafe void CompileVert(string vertexShaderSource) { - const string dummyFragShader = "#version 330 core\r\n\r\nout vec4 FragColor;\r\n\r\nvoid main()\r\n{\r\n FragColor = vec4(1.0); // Output solid white\r\n}"; + const string dummyFragShader = + "#version 330 core\r\n\r\nout vec4 FragColor;\r\n\r\nvoid main()\r\n{\r\n FragColor = vec4(1.0); // Output solid white\r\n}"; Compile(vertexShaderSource, dummyFragShader); } + public unsafe void CompileFrag(string fragmentShaderSource) { - const string dummyVertexShader = "#version 330 core\nvoid main() { gl_Position = vec4(0.0); }"; + const string dummyVertexShader = + "#version 330 core\nvoid main() { gl_Position = vec4(0.0); }"; Compile(dummyVertexShader, fragmentShaderSource); } @@ -124,7 +128,12 @@ public unsafe void Compile(string vertexShaderSource, string fragmentShaderSourc _gl.GetProgram(ShaderProgram, GLEnum.ActiveUniforms, out int numUniforms); for (int i = 0; i < numUniforms; i++) { - string name = _gl.GetActiveUniform(ShaderProgram, (uint)i, out _, out UniformType type); + string name = _gl.GetActiveUniform( + ShaderProgram, + (uint)i, + out _, + out UniformType type + ); int location = _gl.GetUniformLocation(ShaderProgram, name); if (type.ToString().Contains("Sampler")) @@ -145,39 +154,72 @@ public unsafe void Compile(string vertexShaderSource, string fragmentShaderSourc } // Query uniform blocks - _gl.GetProgram(ShaderProgram, ProgramPropertyARB.ActiveUniformBlocks, out int numBlocks); + _gl.GetProgram( + ShaderProgram, + ProgramPropertyARB.ActiveUniformBlocks, + out int numBlocks + ); for (int i = 0; i < numBlocks; i++) { Span nameBuffer = stackalloc byte[256]; unsafe { - _gl.GetActiveUniformBlock(ShaderProgram, (uint)i, - GLEnum.UniformBlockReferencedByVertexShader, out int isVertexUsed); - _gl.GetActiveUniformBlock(ShaderProgram, (uint)i, - GLEnum.UniformBlockReferencedByFragmentShader, out int isFragmentUsed); - - _gl.GetActiveUniformBlockName(ShaderProgram, (uint)i, - (uint)nameBuffer.Length, null, out nameBuffer[0]); + _gl.GetActiveUniformBlock( + ShaderProgram, + (uint)i, + GLEnum.UniformBlockReferencedByVertexShader, + out int isVertexUsed + ); + _gl.GetActiveUniformBlock( + ShaderProgram, + (uint)i, + GLEnum.UniformBlockReferencedByFragmentShader, + out int isFragmentUsed + ); + + _gl.GetActiveUniformBlockName( + ShaderProgram, + (uint)i, + (uint)nameBuffer.Length, + null, + out nameBuffer[0] + ); if (isVertexUsed == 0 && isFragmentUsed == 0) continue; // Not used, skip } - _gl.GetActiveUniformBlock(ShaderProgram, (uint)i, GLEnum.UniformBlockBinding, out int binding); + _gl.GetActiveUniformBlock( + ShaderProgram, + (uint)i, + GLEnum.UniformBlockBinding, + out int binding + ); string name = SilkMarshal.PtrToString((nint)Unsafe.AsPointer(ref nameBuffer[0]))!; UniformBlocks[name] = binding; } // Query shader storage blocks - _gl.GetProgramInterface(ShaderProgram, GLEnum.ShaderStorageBlock, GLEnum.ActiveResources, out int numSSBOs); + _gl.GetProgramInterface( + ShaderProgram, + GLEnum.ShaderStorageBlock, + GLEnum.ActiveResources, + out int numSSBOs + ); for (int i = 0; i < numSSBOs; i++) { Span nameBuffer = stackalloc byte[256]; unsafe { - _gl.GetProgramResourceName(ShaderProgram, ProgramInterface.ShaderStorageBlock, (uint)i, - (uint)nameBuffer.Length, out uint len, out nameBuffer[0]); + _gl.GetProgramResourceName( + ShaderProgram, + ProgramInterface.ShaderStorageBlock, + (uint)i, + (uint)nameBuffer.Length, + out uint len, + out nameBuffer[0] + ); } string name = SilkMarshal.PtrToString((nint)Unsafe.AsPointer(ref nameBuffer[0]))!; StorageBuffers[name] = i; diff --git a/ShaderLibrary/GLSLParser/GLSLLoad.cs b/ShaderLibrary/GLSLParser/GLSLLoad.cs index b05c983..841e596 100644 --- a/ShaderLibrary/GLSLParser/GLSLLoad.cs +++ b/ShaderLibrary/GLSLParser/GLSLLoad.cs @@ -11,7 +11,10 @@ public class GLSLShaderLoader { public static string LoadShader(string filePath) { - return GlslUtility.ProcessIncludes(File.ReadAllText(filePath), Path.GetDirectoryName(filePath)); + return GlslUtility.ProcessIncludes( + File.ReadAllText(filePath), + Path.GetDirectoryName(filePath) + ); } } } diff --git a/ShaderLibrary/GLSLParser/GLSLParser.cs b/ShaderLibrary/GLSLParser/GLSLParser.cs index 607f0ea..92962c2 100644 --- a/ShaderLibrary/GLSLParser/GLSLParser.cs +++ b/ShaderLibrary/GLSLParser/GLSLParser.cs @@ -1,5 +1,4 @@ -using Microsoft.VisualBasic.FileIO; -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; @@ -10,26 +9,36 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml.Linq; +using Microsoft.VisualBasic.FileIO; using static ShaderLibrary.IntermediateShader; namespace ShaderLibrary { public class GLSLParser { - public Dictionary StaticOptions { get; set; } = new(); - public Dictionary DynamicOptions { get; set; } = new(); - public Dictionary UniformBlocks { get; set; } = new(); - public Dictionary StorageBlocks { get; set; } = new(); + public Dictionary StaticOptions { get; set; } = + new(); + public Dictionary DynamicOptions { get; set; } = + new(); + public Dictionary UniformBlocks { get; set; } = + new(); + public Dictionary StorageBlocks { get; set; } = + new(); public Dictionary Samplers { get; set; } = new(); - public Dictionary InputAttributes { get; set; } = new(); - public Dictionary OutputAttributes { get; set; } = new(); + public Dictionary InputAttributes { get; set; } = + new(); + public Dictionary OutputAttributes { get; set; } = + new(); public Dictionary RenderInfos { get; set; } = new(); public string Source; public GLSLParser() { } - public GLSLParser(string glsl) { ParseGLSL(glsl); } + public GLSLParser(string glsl) + { + ParseGLSL(glsl); + } public void ParseGLSL(string glslSource) { @@ -41,7 +50,7 @@ public void ParseGLSL(string glslSource) ParseOptions(glslSource); ParseRenderInfo(glslSource); ParseStorageBuffers(glslSource); - // File.WriteAllText("", jsocon.SerializeObject(Shader)); + // File.WriteAllText("", jsocon.SerializeObject(Shader)); } public BnshFile.ShaderReflectionData GetBnshReflection() @@ -63,40 +72,49 @@ public BnshFile.ShaderReflectionData GetBnshReflection() } private static readonly Regex AttributeUniformRegex = new Regex( - @"(?:layout\s*\(.*?location\s*=\s*(\d+).*?\)\s*)?(in|out|attribute)\s+([\w\d_]+)\s+([\w\d_]+)\s*;\s*(?://@.*)?", - RegexOptions.Compiled); + @"(?:layout\s*\(.*?location\s*=\s*(\d+).*?\)\s*)?(in|out|attribute)\s+([\w\d_]+)\s+([\w\d_]+)\s*;\s*(?://@.*)?", + RegexOptions.Compiled + ); private static readonly Regex UniformBlockRegex = new Regex( - @"(layout\s*\(.*\)\s*uniform\s+\w+\s+\w+\s*{\s*(.*?)\s*};)\s*//\s*@\s*(.*)", - RegexOptions.Compiled | RegexOptions.Singleline); + @"(layout\s*\(.*\)\s*uniform\s+\w+\s+\w+\s*{\s*(.*?)\s*};)\s*//\s*@\s*(.*)", + RegexOptions.Compiled | RegexOptions.Singleline + ); private static readonly Regex PropertyCommentRegex = new Regex( @"\/\/@\s*(\w+)\s*=\s*""([^""]+)""", - RegexOptions.Compiled); + RegexOptions.Compiled + ); private static readonly Regex UniformSamplerRegex = new Regex( - @"(?:layout\s*\(\s*(?:location|binding)\s*=\s*(?\d+)\s*\)\s*)?uniform\s+(?sampler\w+)\s+(?\w+)\s*;\s*//\s*@\s*(?.*)", - RegexOptions.Compiled); + @"(?:layout\s*\(\s*(?:location|binding)\s*=\s*(?\d+)\s*\)\s*)?uniform\s+(?sampler\w+)\s+(?\w+)\s*;\s*//\s*@\s*(?.*)", + RegexOptions.Compiled + ); private static readonly Regex MacrosRegex = new Regex( - @"#define\s+(?\w+)\s+(?\w+)\s*\/\/@\s*(?(\w+\s*=\s*""[^""]+""\s*)*)(?:\s*flags\s*=\s*""(?[^""]+)""\s*)?", - RegexOptions.Compiled); + @"#define\s+(?\w+)\s+(?\w+)\s*\/\/@\s*(?(\w+\s*=\s*""[^""]+""\s*)*)(?:\s*flags\s*=\s*""(?[^""]+)""\s*)?", + RegexOptions.Compiled + ); private static readonly Regex StorageBufferRegex = new Regex( @"layout\s*\(\s*(?[^\)]+)\)\s*buffer\s+(?\w+)\s*(\/\/@\s*(?(\w+\s*=\s*""[^""]+""\s*)+))?\s*\{(?(.|\n)*?)\}\s*\w*\s*;", - RegexOptions.Compiled); + RegexOptions.Compiled + ); private static readonly Regex BindingRegex = new Regex( @"binding\s*=\s*(\d+)", - RegexOptions.Compiled); + RegexOptions.Compiled + ); private static readonly Regex RenderInfoRegex = new Regex( @"^\/\/@\s*render_info=.*$", - RegexOptions.Compiled | RegexOptions.Multiline); + RegexOptions.Compiled | RegexOptions.Multiline + ); private static readonly Regex PropertyCommentRegex2 = new Regex( - @"(\w+)\s*=\s*""([^""]*)""", - RegexOptions.Compiled); + @"(\w+)\s*=\s*""([^""]*)""", + RegexOptions.Compiled + ); public void ParseAttributes(string glslSource) { @@ -124,36 +142,43 @@ public void ParseAttributes(string glslSource) switch (propertyName) { - case "id": name = propertyValue; break; + case "id": + name = propertyValue; + break; } } if (qualifier == "in") { if (!this.InputAttributes.ContainsKey(name)) - this.InputAttributes.Add(name, new IntermediateShader.Attribute() - { - ID = name, - Symbol = symbol, - Location = int.Parse(location), - Type = value_type, - ArrayCount = (uint)array_count, - }); + this.InputAttributes.Add( + name, + new IntermediateShader.Attribute() + { + ID = name, + Symbol = symbol, + Location = int.Parse(location), + Type = value_type, + ArrayCount = (uint)array_count, + } + ); } else { if (!this.OutputAttributes.ContainsKey(name)) - this.OutputAttributes.Add(name, new IntermediateShader.Attribute() - { - ID = name, - Symbol = symbol, - Location = int.Parse(location), - Type = value_type, - ArrayCount = (uint)array_count, - }); + this.OutputAttributes.Add( + name, + new IntermediateShader.Attribute() + { + ID = name, + Symbol = symbol, + Location = int.Parse(location), + Type = value_type, + ArrayCount = (uint)array_count, + } + ); } } - } private void ParseRenderInfo(string glslSource) @@ -163,10 +188,14 @@ static RenderInfo.RenderInfoType ParseType(string typeStr) switch (typeStr.ToLowerInvariant()) { case "int": - case "int32": return RenderInfo.RenderInfoType.Int32; - case "float": return RenderInfo.RenderInfoType.Float; - case "string": return RenderInfo.RenderInfoType.String; - default: return RenderInfo.RenderInfoType.String; + case "int32": + return RenderInfo.RenderInfoType.Int32; + case "float": + return RenderInfo.RenderInfoType.Float; + case "string": + return RenderInfo.RenderInfoType.String; + default: + return RenderInfo.RenderInfoType.String; } } @@ -181,13 +210,25 @@ static RenderInfo.RenderInfoType ParseType(string typeStr) switch (propertyName) { - case "render_info": info.Name = propertyValue; break; - case "type": info.Type = ParseType(propertyValue); break; - case "default": info.DefaultChoice = propertyValue; break; + case "render_info": + info.Name = propertyValue; + break; + case "type": + info.Type = ParseType(propertyValue); + break; + case "default": + info.DefaultChoice = propertyValue; + break; // ui data - case "group": info.Group = propertyValue; break; - case "order": int.TryParse(propertyValue, out order); break; - case "label": info.Label = propertyValue; break; + case "group": + info.Group = propertyValue; + break; + case "order": + int.TryParse(propertyValue, out order); + break; + case "label": + info.Label = propertyValue; + break; } } RenderInfos.TryAdd(info.Name, info); @@ -200,14 +241,14 @@ public void ParseSamplers(string glslSource) foreach (Match match in UniformSamplerRegex.Matches(glslSource)) { string samplerType = match.Groups["samplerType"].Value; - string samplerName = match.Groups["samplerName"].Value; + string samplerName = match.Groups["samplerName"].Value; string symbol = samplerName; string properties = match.Groups["properties"].Value; string group = ""; string label = samplerName; int order = -1; - int location = -1; + int location = -1; if (match.Groups["binding"].Success) { location = int.Parse(match.Groups["binding"].Value); @@ -220,10 +261,18 @@ public void ParseSamplers(string glslSource) switch (propertyName) { - case "id": samplerName = propertyValue; break; - case "group": group = propertyValue; break; - case "order": int.TryParse(propertyValue, out order); break; - case "label": label = propertyValue; break; + case "id": + samplerName = propertyValue; + break; + case "group": + group = propertyValue; + break; + case "order": + int.TryParse(propertyValue, out order); + break; + case "label": + label = propertyValue; + break; } } @@ -236,16 +285,19 @@ public void ParseSamplers(string glslSource) case "samplerCube": case "samplerCubeArray": if (!this.Samplers.ContainsKey(samplerName)) - this.Samplers.Add(samplerName, new IntermediateShader.Sampler() - { - ID = samplerName, - Location = location, - Symbol = symbol, - Type = GetType(samplerType), - Group = group, - Label = label, - Order = order - }); + this.Samplers.Add( + samplerName, + new IntermediateShader.Sampler() + { + ID = samplerName, + Location = location, + Symbol = symbol, + Type = GetType(samplerType), + Group = group, + Label = label, + Order = order, + } + ); break; } } @@ -267,8 +319,10 @@ public void ParseOptions(string glslSource) //options must be in int form, so ensure booleans are converted string GetChoiceValue(string v) { - if (v == "false") return "0"; - else if (v == "true") return "1"; + if (v == "false") + return "0"; + else if (v == "true") + return "1"; return v; } @@ -290,7 +344,10 @@ string GetChoiceValue(string v) string desc = ""; if (!string.IsNullOrEmpty(macroProperties)) { - var macroPropertyMatches = Regex.Matches(macroProperties, @"(?\w+)\s*=\s*""(?[^""]+)"""); + var macroPropertyMatches = Regex.Matches( + macroProperties, + @"(?\w+)\s*=\s*""(?[^""]+)""" + ); foreach (Match propertyMatch in macroPropertyMatches) { string property = propertyMatch.Groups["property"].Value; @@ -298,19 +355,41 @@ string GetChoiceValue(string v) switch (property) { - case "branch": is_static = valueProp != "dynamic"; break; - case "desc": desc = valueProp; break; - case "flags": compile_all = valueProp.Contains("compile_all_coices"); break; - case "id": name = valueProp; break; - case "order": int.TryParse(valueProp, out order); break; - case "group": group = valueProp; break; - case "label": label = valueProp; break; - case "is_skin_count": is_skin_count = true; break; - case "type": - type = valueProp; break; + case "branch": + is_static = valueProp != "dynamic"; + break; + case "desc": + desc = valueProp; + break; + case "flags": + compile_all = valueProp.Contains("compile_all_coices"); + break; + case "id": + name = valueProp; + break; + case "order": + int.TryParse(valueProp, out order); + break; + case "group": + group = valueProp; + break; + case "label": + label = valueProp; + break; + case "is_skin_count": + is_skin_count = true; + break; + case "type": + type = valueProp; + break; case "choices": choices.Clear(); - foreach (var v in valueProp.Split(new[] { ",", " " }, StringSplitOptions.RemoveEmptyEntries)) + foreach ( + var v in valueProp.Split( + new[] { ",", " " }, + StringSplitOptions.RemoveEmptyEntries + ) + ) choices.Add(GetChoiceValue(v)); //slight hack atm @@ -357,7 +436,10 @@ public void ParseStorageBuffers(string glslSource) uint blockSize = 0; // Parse and display block properties - var blockPropertyMatches = Regex.Matches(blockProperties, @"(?\w+)\s*=\s*""(?[^""]+)"""); + var blockPropertyMatches = Regex.Matches( + blockProperties, + @"(?\w+)\s*=\s*""(?[^""]+)""" + ); foreach (Match propertyMatch in blockPropertyMatches) { string property = propertyMatch.Groups["property"].Value; @@ -365,9 +447,15 @@ public void ParseStorageBuffers(string glslSource) switch (property) { - case "id": blockName = value; break; - case "type": type = value; break; - case "size": uint.TryParse(value, out blockSize); break; + case "id": + blockName = value; + break; + case "type": + type = value; + break; + case "size": + uint.TryParse(value, out blockSize); + break; } } @@ -389,10 +477,12 @@ public void ParseStorageBuffers(string glslSource) public void ParseUniformBlocks(string glslSource) { // Pattern to match uniform blocks - string blockPattern = @"layout\s*\(\s*(?[^\)]+)\)\s*uniform\s+(?\w+)\s*(\/\/@\s*(?(\w+\s*=\s*""[^""]+""\s*)+))?\s*\{(?(.|\n)*?)\}\s*\w*\s*;"; + string blockPattern = + @"layout\s*\(\s*(?[^\)]+)\)\s*uniform\s+(?\w+)\s*(\/\/@\s*(?(\w+\s*=\s*""[^""]+""\s*)+))?\s*\{(?(.|\n)*?)\}\s*\w*\s*;"; // Pattern to match uniforms inside blocks - string uniformPattern = @"(?\w+)\s+(?\w+)\s*(\[(?[^\]]+)\])?\s*;\s*(\/\/@\s*(?(\w+\s*=\s*""[^""]+""\s*)+))?"; + string uniformPattern = + @"(?\w+)\s+(?\w+)\s*(\[(?[^\]]+)\])?\s*;\s*(\/\/@\s*(?(\w+\s*=\s*""[^""]+""\s*)+))?"; // Match and parse uniform blocks var blockMatches = Regex.Matches(glslSource, blockPattern); @@ -408,7 +498,10 @@ public void ParseUniformBlocks(string glslSource) uint blockSize = 0; // Parse and display block properties - var blockPropertyMatches = Regex.Matches(blockProperties, @"(?\w+)\s*=\s*""(?[^""]+)"""); + var blockPropertyMatches = Regex.Matches( + blockProperties, + @"(?\w+)\s*=\s*""(?[^""]+)""" + ); foreach (Match propertyMatch in blockPropertyMatches) { string property = propertyMatch.Groups["property"].Value; @@ -416,9 +509,15 @@ public void ParseUniformBlocks(string glslSource) switch (property) { - case "id": blockName = value; break; - case "type": type = value; break; - case "size": uint.TryParse(value, out blockSize); break; + case "id": + blockName = value; + break; + case "type": + type = value; + break; + case "size": + uint.TryParse(value, out blockSize); + break; } } @@ -443,11 +542,21 @@ public void ParseUniformBlocks(string glslSource) switch (type) { - case "material": block.Type = IntermediateShader.BlockType.Material; break; - case "shape": block.Type = IntermediateShader.BlockType.Shape; break; - case "option": block.Type = IntermediateShader.BlockType.Option; break; - case "skeleton": block.Type = IntermediateShader.BlockType.Skeleton; break; - case "resmaterial": block.Type = IntermediateShader.BlockType.ResMaterial; break; + case "material": + block.Type = IntermediateShader.BlockType.Material; + break; + case "shape": + block.Type = IntermediateShader.BlockType.Shape; + break; + case "option": + block.Type = IntermediateShader.BlockType.Option; + break; + case "skeleton": + block.Type = IntermediateShader.BlockType.Skeleton; + break; + case "resmaterial": + block.Type = IntermediateShader.BlockType.ResMaterial; + break; } if (type == "material" || type == "scene") @@ -466,12 +575,17 @@ public void ParseUniformBlocks(string glslSource) int array_count = 1; // int.TryParse(arraySize, out array_count); - IntermediateShader.ValueType value_type = IntermediateShader.ValueType.FLOAT; + IntermediateShader.ValueType value_type = IntermediateShader + .ValueType + .FLOAT; (value_type, array_count) = GetValueType(datatype, array_count); if (!string.IsNullOrEmpty(properties)) { - var propertyMatches = Regex.Matches(properties, @"(?\w+)\s*=\s*""(?[^""]+)"""); + var propertyMatches = Regex.Matches( + properties, + @"(?\w+)\s*=\s*""(?[^""]+)""" + ); foreach (Match propertyMatch in propertyMatches) { string property = propertyMatch.Groups["property"].Value; @@ -479,11 +593,15 @@ public void ParseUniformBlocks(string glslSource) switch (property) { - case "id": name = value; break; + case "id": + name = value; + break; case "default_value": defaultValue = value; break; - case "group": group = value; break; + case "group": + group = value; + break; } } } @@ -491,38 +609,52 @@ public void ParseUniformBlocks(string glslSource) if (block.Uniforms.Any(x => x.ID == name)) continue; - block.Uniforms.Add(new IntermediateShader.Uniform() - { - ID = name, - Type = value_type, - ArrayCount = (uint)array_count, - DefaultValue = defaultValue, - Group = group, - }); + block.Uniforms.Add( + new IntermediateShader.Uniform() + { + ID = name, + Type = value_type, + ArrayCount = (uint)array_count, + DefaultValue = defaultValue, + Group = group, + } + ); } } } } - private (IntermediateShader.ValueType, int) GetValueType(string type, int count) { switch (type) { //represent matrices as vec4[] as done generally by game shaders - case "mat4": return (IntermediateShader.ValueType.FLOAT4, count * 4); - case "mat2x4": return (IntermediateShader.ValueType.FLOAT4, count * 2); - case "mat4x2": return (IntermediateShader.ValueType.FLOAT4, count * 2); - case "mat3x4": return (IntermediateShader.ValueType.FLOAT3, count * 3); - case "bool": return (IntermediateShader.ValueType.BOOL, count); - case "float": return (IntermediateShader.ValueType.FLOAT, count); - case "vec2": return (IntermediateShader.ValueType.FLOAT2, count); - case "vec3": return (IntermediateShader.ValueType.FLOAT3, count); - case "vec4": return (IntermediateShader.ValueType.FLOAT4, count); - case "int": return (IntermediateShader.ValueType.INT, count); - case "ivec2": return (IntermediateShader.ValueType.INT2, count); - case "ivec3": return (IntermediateShader.ValueType.INT3, count); - case "ivec4": return (IntermediateShader.ValueType.INT4, count); + case "mat4": + return (IntermediateShader.ValueType.FLOAT4, count * 4); + case "mat2x4": + return (IntermediateShader.ValueType.FLOAT4, count * 2); + case "mat4x2": + return (IntermediateShader.ValueType.FLOAT4, count * 2); + case "mat3x4": + return (IntermediateShader.ValueType.FLOAT3, count * 3); + case "bool": + return (IntermediateShader.ValueType.BOOL, count); + case "float": + return (IntermediateShader.ValueType.FLOAT, count); + case "vec2": + return (IntermediateShader.ValueType.FLOAT2, count); + case "vec3": + return (IntermediateShader.ValueType.FLOAT3, count); + case "vec4": + return (IntermediateShader.ValueType.FLOAT4, count); + case "int": + return (IntermediateShader.ValueType.INT, count); + case "ivec2": + return (IntermediateShader.ValueType.INT2, count); + case "ivec3": + return (IntermediateShader.ValueType.INT3, count); + case "ivec4": + return (IntermediateShader.ValueType.INT4, count); } throw new Exception($"value type not supported {type}!"); } @@ -531,12 +663,18 @@ private IntermediateShader.Sampler.SamplerType GetType(string type) { switch (type) { - case "sampler1D": return IntermediateShader.Sampler.SamplerType.Sampler1D; - case "sampler2D": return IntermediateShader.Sampler.SamplerType.Sampler2D; - case "sampler3D": return IntermediateShader.Sampler.SamplerType.Sampler3D; - case "sampler2DArray": return IntermediateShader.Sampler.SamplerType.Sampler2DArray; - case "samplerCube": return IntermediateShader.Sampler.SamplerType.SamplerCube; - case "samplerCubeArray": return IntermediateShader.Sampler.SamplerType.SamplerCubeArray; + case "sampler1D": + return IntermediateShader.Sampler.SamplerType.Sampler1D; + case "sampler2D": + return IntermediateShader.Sampler.SamplerType.Sampler2D; + case "sampler3D": + return IntermediateShader.Sampler.SamplerType.Sampler3D; + case "sampler2DArray": + return IntermediateShader.Sampler.SamplerType.Sampler2DArray; + case "samplerCube": + return IntermediateShader.Sampler.SamplerType.SamplerCube; + case "samplerCubeArray": + return IntermediateShader.Sampler.SamplerType.SamplerCubeArray; } throw new Exception($"sampler type not supported {type}!"); } diff --git a/ShaderLibrary/GLSLParser/GlslUtility.cs b/ShaderLibrary/GLSLParser/GlslUtility.cs index ad66dd7..f7d280b 100644 --- a/ShaderLibrary/GLSLParser/GlslUtility.cs +++ b/ShaderLibrary/GLSLParser/GlslUtility.cs @@ -9,11 +9,14 @@ namespace ShaderLibrary { public class GlslUtility { - private static readonly Regex IncludeRegex = new Regex(@"#include\s+""(.+?)""", RegexOptions.Compiled); + private static readonly Regex IncludeRegex = new Regex( + @"#include\s+""(.+?)""", + RegexOptions.Compiled + ); /// /// Gets other shader sources when paths are marked as #include. - /// + /// /// /// /// @@ -22,7 +25,9 @@ public static string ProcessIncludes(string shaderSource, string directory) { StringBuilder processedShader = new StringBuilder(); - foreach (string line in shaderSource.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)) + foreach ( + string line in shaderSource.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None) + ) { Match match = IncludeRegex.Match(line); if (match.Success) @@ -94,8 +99,10 @@ public static string ApplyMacros(Dictionary macros, string shade if (isBool) // Set as true or false if necessary { - if (macroNew == "1") macroNew = "true"; - if (macroNew == "0") macroNew = "false"; + if (macroNew == "1") + macroNew = "true"; + if (macroNew == "0") + macroNew = "false"; } Console.WriteLine($"{macroValue} -> {macroNew}"); diff --git a/ShaderLibrary/GLSLParser/IntermediateShader.cs b/ShaderLibrary/GLSLParser/IntermediateShader.cs index 7e04b7e..385466c 100644 --- a/ShaderLibrary/GLSLParser/IntermediateShader.cs +++ b/ShaderLibrary/GLSLParser/IntermediateShader.cs @@ -1,7 +1,4 @@ -using ShaderLibrary.Common; -using ShaderLibrary.Helpers; -using ShaderLibrary.WiiU; -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -11,6 +8,9 @@ using System.Threading.Tasks; using System.Xml.Linq; using System.Xml.Serialization; +using ShaderLibrary.Common; +using ShaderLibrary.Helpers; +using ShaderLibrary.WiiU; using static ShaderLibrary.IntermediateShader; namespace ShaderLibrary @@ -82,7 +82,11 @@ void LoadShaderModel(string modelFolder) public static IntermediateShader LoadFromXml(string path) { - using var reader = new StreamReader(path, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); + using var reader = new StreamReader( + path, + Encoding.UTF8, + detectEncodingFromByteOrderMarks: true + ); var serializer = new XmlSerializer(typeof(IntermediateShader)); return (IntermediateShader)serializer.Deserialize(reader); @@ -159,7 +163,8 @@ public BfshaFile CreateBfshaFileSwitch() bfsha.BinHeader.VersionMinor = 0; bfsha.Flags = 0; - foreach (var model in ShaderModels) { + foreach (var model in ShaderModels) + { ShaderModel shaderModel = CreateShaderModel(model); bfsha.ShaderModels.Add(shaderModel.Name, shaderModel); } @@ -181,7 +186,8 @@ public BfshaFile CreateBfshaFileWiiU() bfsha.BinHeader.VersionMinor = 2; bfsha.Flags = 4; - foreach (var model in ShaderModels) { + foreach (var model in ShaderModels) + { ShaderModel shaderModel = CreateShaderModel(model); bfsha.ShaderModels.Add(shaderModel.Name, shaderModel); } @@ -223,22 +229,28 @@ ShaderOption ConvertOption(string key, OptionMacro op) ShaderOptionCreator.SetupOptionKeyFlags(shaderModel); foreach (var samp in model.Samplers.OrderBy(x => x.ID)) - shaderModel.Samplers.Add(samp.ID, new BfshaSampler() - { - Index = (byte)shaderModel.Samplers.Count, - Annotation = "", - GX2Type = (byte)GetGX2SamplerType(samp.Type), - GX2Count = 1, - }); + shaderModel.Samplers.Add( + samp.ID, + new BfshaSampler() + { + Index = (byte)shaderModel.Samplers.Count, + Annotation = "", + GX2Type = (byte)GetGX2SamplerType(samp.Type), + GX2Count = 1, + } + ); foreach (var attr in model.Attributes) - shaderModel.Attributes.Add(attr.ID, new BfshaAttribute() - { - Index = (byte)shaderModel.Attributes.Count, - Location = (sbyte)attr.Location, - GX2Count = (byte)attr.ArrayCount, - GX2Type = (byte)GetValueType(attr.Type), - }); + shaderModel.Attributes.Add( + attr.ID, + new BfshaAttribute() + { + Index = (byte)shaderModel.Attributes.Count, + Location = (sbyte)attr.Location, + GX2Count = (byte)attr.ArrayCount, + GX2Type = (byte)GetValueType(attr.Type), + } + ); //unk shaderModel.UnknownIndices2[0] = 255; @@ -263,36 +275,57 @@ ShaderOption ConvertOption(string key, OptionMacro op) //material, shape, skeleton, option block indices switch (b.Type) { - case BlockType.Material: shaderModel.BlockIndices[0] = block.header.Index; break; - case BlockType.Shape: shaderModel.BlockIndices[1] = block.header.Index; break; - case BlockType.Skeleton: shaderModel.BlockIndices[2] = block.header.Index; break; - case BlockType.Option: shaderModel.BlockIndices[3] = block.header.Index; break; + case BlockType.Material: + shaderModel.BlockIndices[0] = block.header.Index; + break; + case BlockType.Shape: + shaderModel.BlockIndices[1] = block.header.Index; + break; + case BlockType.Skeleton: + shaderModel.BlockIndices[2] = block.header.Index; + break; + case BlockType.Option: + shaderModel.BlockIndices[3] = block.header.Index; + break; } //block types switch (b.Type) { - case BlockType.Material: block.header.Type = 1; break; - case BlockType.Shape: block.header.Type = 2; break; - case BlockType.Skeleton: block.header.Type = 3; break; - case BlockType.Option: block.header.Type = 4; break; - default: block.header.Type = 0; break; + case BlockType.Material: + block.header.Type = 1; + break; + case BlockType.Shape: + block.header.Type = 2; + break; + case BlockType.Skeleton: + block.header.Type = 3; + break; + case BlockType.Option: + block.header.Type = 4; + break; + default: + block.header.Type = 0; + break; } if (block.DefaultBuffer?.Length > 0) { foreach (var u in b.Uniforms) { - block.Uniforms.Add(u.ID, new BfshaUniform() - { - BlockIndex = block.Index, - DataOffset = (ushort)u.Offset, - Index = block.Uniforms.Count, - Name = u.ID, - GX2Count = (ushort)u.ArrayCount, - GX2Type = (byte)GetValueType(u.Type), - GX2ParamType = (byte)GetParamType(u.Type, u.ArrayCount) - }); + block.Uniforms.Add( + u.ID, + new BfshaUniform() + { + BlockIndex = block.Index, + DataOffset = (ushort)u.Offset, + Index = block.Uniforms.Count, + Name = u.ID, + GX2Count = (ushort)u.ArrayCount, + GX2Type = (byte)GetValueType(u.Type), + GX2ParamType = (byte)GetParamType(u.Type, u.ArrayCount), + } + ); } } @@ -306,19 +339,27 @@ static ShaderParamType GetParamType(ValueType type, uint arrayCount) { switch (type) { - case ValueType.FLOAT: return ShaderParamType.Float; - case ValueType.FLOAT2: return ShaderParamType.Float2; - case ValueType.FLOAT3: return ShaderParamType.Float3; + case ValueType.FLOAT: + return ShaderParamType.Float; + case ValueType.FLOAT2: + return ShaderParamType.Float2; + case ValueType.FLOAT3: + return ShaderParamType.Float3; case ValueType.FLOAT4: if (arrayCount == 2) //TEXSRT return ShaderParamType.TexSrt; return ShaderParamType.Float4; - case ValueType.INT: return ShaderParamType.Int; - case ValueType.INT2: return ShaderParamType.Int2; - case ValueType.INT3: return ShaderParamType.Int3; - case ValueType.INT4: return ShaderParamType.Int4; - case ValueType.BOOL: return ShaderParamType.Bool; + case ValueType.INT: + return ShaderParamType.Int; + case ValueType.INT2: + return ShaderParamType.Int2; + case ValueType.INT3: + return ShaderParamType.Int3; + case ValueType.INT4: + return ShaderParamType.Int4; + case ValueType.BOOL: + return ShaderParamType.Bool; } throw new NotImplementedException($"{type} {arrayCount}"); } @@ -327,16 +368,26 @@ static GX2ShaderVarType GetValueType(ValueType type) { switch (type) { - case ValueType.FLOAT: return GX2ShaderVarType.FLOAT; - case ValueType.FLOAT2: return GX2ShaderVarType.FLOAT2; - case ValueType.FLOAT3: return GX2ShaderVarType.FLOAT3; - case ValueType.FLOAT4: return GX2ShaderVarType.FLOAT4; - case ValueType.INT: return GX2ShaderVarType.INT; - case ValueType.INT2: return GX2ShaderVarType.INT2; - case ValueType.INT3: return GX2ShaderVarType.UINT3; - case ValueType.INT4: return GX2ShaderVarType.INT4; - case ValueType.BOOL: return GX2ShaderVarType.BOOL; - case ValueType.MAT2x4: return GX2ShaderVarType.FLOAT2X4; + case ValueType.FLOAT: + return GX2ShaderVarType.FLOAT; + case ValueType.FLOAT2: + return GX2ShaderVarType.FLOAT2; + case ValueType.FLOAT3: + return GX2ShaderVarType.FLOAT3; + case ValueType.FLOAT4: + return GX2ShaderVarType.FLOAT4; + case ValueType.INT: + return GX2ShaderVarType.INT; + case ValueType.INT2: + return GX2ShaderVarType.INT2; + case ValueType.INT3: + return GX2ShaderVarType.UINT3; + case ValueType.INT4: + return GX2ShaderVarType.INT4; + case ValueType.BOOL: + return GX2ShaderVarType.BOOL; + case ValueType.MAT2x4: + return GX2ShaderVarType.FLOAT2X4; } throw new NotImplementedException($"{type}"); } @@ -345,12 +396,18 @@ static GX2SamplerVarType GetGX2SamplerType(Sampler.SamplerType type) { switch (type) { - case Sampler.SamplerType.Sampler2D: return GX2SamplerVarType.SAMPLER_2D; - case Sampler.SamplerType.Sampler3D: return GX2SamplerVarType.SAMPLER_3D; - case Sampler.SamplerType.SamplerCube: return GX2SamplerVarType.SAMPLER_CUBE; - case Sampler.SamplerType.SamplerCubeArray: return GX2SamplerVarType.SAMPLER_CUBE_ARRAY; - case Sampler.SamplerType.Sampler2DArray: return GX2SamplerVarType.SAMPLER_2D_ARRAY; - case Sampler.SamplerType.Sampler1D: return GX2SamplerVarType.SAMPLER_1D; + case Sampler.SamplerType.Sampler2D: + return GX2SamplerVarType.SAMPLER_2D; + case Sampler.SamplerType.Sampler3D: + return GX2SamplerVarType.SAMPLER_3D; + case Sampler.SamplerType.SamplerCube: + return GX2SamplerVarType.SAMPLER_CUBE; + case Sampler.SamplerType.SamplerCubeArray: + return GX2SamplerVarType.SAMPLER_CUBE_ARRAY; + case Sampler.SamplerType.Sampler2DArray: + return GX2SamplerVarType.SAMPLER_2D_ARRAY; + case Sampler.SamplerType.Sampler1D: + return GX2SamplerVarType.SAMPLER_1D; } throw new NotImplementedException($"{type}"); } @@ -385,20 +442,32 @@ public void BuildLookupData() storageBlockLookup = StorageBlocks.ToDictionary(s => s.ID, s => s.Symbol); } - private static string Lookup(Dictionary dict, string key) - => dict.TryGetValue(key, out var value) ? value : key; - private static string LookupReverse(Dictionary dict, string value) - => dict.FirstOrDefault(x => x.Value == value).Key; + private static string Lookup(Dictionary dict, string key) => + dict.TryGetValue(key, out var value) ? value : key; + + private static string LookupReverse(Dictionary dict, string value) => + dict.FirstOrDefault(x => x.Value == value).Key; public string GetSamplerSymbolName(string name) => Lookup(samplerLookup, name); + public string GetAttributeSymbolName(string name) => Lookup(attributeLookup, name); - public string GetUniformBlockSymbolName(string name) => Lookup(uniformBlockLookup, name); - public string GetStorageBlockSymbolName(string name) => Lookup(storageBlockLookup, name); + + public string GetUniformBlockSymbolName(string name) => + Lookup(uniformBlockLookup, name); + + public string GetStorageBlockSymbolName(string name) => + Lookup(storageBlockLookup, name); public string GetSamplerBfshaName(string name) => LookupReverse(samplerLookup, name); - public string GetAttributeBfshaName(string name) => LookupReverse(attributeLookup, name); - public string GetUniformBlockBfshaName(string name) => LookupReverse(uniformBlockLookup, name); - public string GetStorageBlockBfshaName(string name) => LookupReverse(storageBlockLookup, name); + + public string GetAttributeBfshaName(string name) => + LookupReverse(attributeLookup, name); + + public string GetUniformBlockBfshaName(string name) => + LookupReverse(uniformBlockLookup, name); + + public string GetStorageBlockBfshaName(string name) => + LookupReverse(storageBlockLookup, name); } public class RenderInfo : UIElement @@ -407,28 +476,33 @@ public class RenderInfo : UIElement /// Render info name. /// [XmlAttribute("name")] - public string Name { get; set; -} /// + public string Name { get; set; } + + /// /// Render info type. /// [XmlAttribute("type")] public RenderInfoType Type { get; set; } + /// /// Render info default choice/value. /// [XmlAttribute("default")] public string DefaultChoice { get; set; } + /// /// Render info choices. /// [XmlAttribute("choices")] public List Choices { get; set; } = new List(); + /// /// Determines if the render info entry is necessary for materials. /// If set, the render info will be set with a default value in the material if not present. /// [XmlAttribute("optional")] public bool Optional { get; set; } = true; + /// /// The type if render info. The data present in a material. /// @@ -444,15 +518,19 @@ public class OptionMacro : UIElement { [XmlAttribute("name")] public string ID { get; set; } + [XmlAttribute("symbol")] public string Symbol; public List Choices { get; set; } = new List(); + [XmlAttribute("default")] [DefaultValue(null)] public string DefaultChoice { get; set; } + [XmlAttribute("branch")] [DefaultValue(false)] public bool Branch = false; + [XmlAttribute("type")] public string Type { get; set; } @@ -479,6 +557,7 @@ public string GetMacroChoice(string value) } public string GetOptionChoice() => GetOptionChoice(DefaultChoice); + public string GetOptionChoice(string choice) { return choice.Split(":").FirstOrDefault(); @@ -489,10 +568,13 @@ public class StorageBlock { [XmlAttribute("name")] public string ID { get; set; } + [XmlAttribute("symbol")] public string Symbol { get; set; } + [XmlAttribute("size")] public uint Size { get; set; } + [XmlAttribute("index")] public int Location { get; set; } } @@ -501,12 +583,16 @@ public class UniformBlock { [XmlAttribute("name")] public string ID { get; set; } + [XmlAttribute("symbol")] public string Symbol { get; set; } + [XmlAttribute("type")] public BlockType Type { get; set; } + [XmlAttribute("index")] public int Location { get; set; } + [XmlElement("uniforms")] public List Uniforms { get; set; } = new(); @@ -527,7 +613,9 @@ public byte[] GetBuffer() string[] data_values = uniform.DefaultValue.Split(" "); for (int i = 0; i < data_values.Length; i++) - writer.Write(float.Parse(data_values[i], CultureInfo.InvariantCulture)); + writer.Write( + float.Parse(data_values[i], CultureInfo.InvariantCulture) + ); } } } @@ -539,12 +627,16 @@ public class Uniform : UIElement { [XmlAttribute("name")] public string ID { get; set; } + [XmlIgnore()] public uint Offset { get; set; } + [XmlAttribute("type")] public ValueType Type { get; set; } + [XmlAttribute("count")] public uint ArrayCount { get; set; } = 1; + [XmlAttribute("default")] public string DefaultValue { get; set; } } @@ -553,10 +645,13 @@ public class Sampler : UIElement { [XmlAttribute("name")] public string ID { get; set; } + [XmlAttribute("symbol")] public string Symbol { get; set; } + [XmlAttribute("index")] public int Location { get; set; } + [XmlAttribute("type")] public SamplerType Type { get; set; } = SamplerType.Sampler2D; @@ -575,12 +670,16 @@ public class Attribute : UIElement { [XmlAttribute("name")] public string ID { get; set; } + [XmlAttribute("symbol")] public string Symbol { get; set; } + [XmlAttribute("index")] public int Location { get; set; } + [XmlAttribute("type")] public ValueType Type { get; set; } + [XmlAttribute("count")] public uint ArrayCount { get; set; } = 1; } @@ -590,12 +689,15 @@ public class UIElement [XmlAttribute("ui_label")] [DefaultValue(null)] public string Label { get; set; } + [XmlAttribute("ui_group")] [DefaultValue(null)] public string Group { get; set; } + [XmlAttribute("info")] [DefaultValue(null)] public string Description { get; set; } + [XmlAttribute("ui_order")] [DefaultValue(-1)] public int Order { get; set; } = -1; diff --git a/ShaderLibrary/Helpers/ShaderOptionCreator.cs b/ShaderLibrary/Helpers/ShaderOptionCreator.cs index 8e979d9..20062d8 100644 --- a/ShaderLibrary/Helpers/ShaderOptionCreator.cs +++ b/ShaderLibrary/Helpers/ShaderOptionCreator.cs @@ -46,12 +46,12 @@ private static void SetupOptionKeyFlags(List bitfield, List o // Calculate the bit mask for the choices //total bits to take up, shift by bit pos - uint bitMask = (uint)((1 << bitsPerChoice) - 1) ; + uint bitMask = (uint)((1 << bitsPerChoice) - 1); bitMask <<= bitPos; uint bitKeyIndex = (uint)bitfield.Count - 1; - // Console.WriteLine($"{bitKeyIndex} {bitPos} {bitMask} og {option.Bit32Index} {option.Bit32Shift} {option.Bit32Mask} bitsPerChoice {bitsPerChoice} {option.Choices.Count}"); + // Console.WriteLine($"{bitKeyIndex} {bitPos} {bitMask} og {option.Bit32Index} {option.Bit32Shift} {option.Bit32Mask} bitsPerChoice {bitsPerChoice} {option.Choices.Count}"); option.Bit32Mask = (uint)bitMask; option.Bit32Shift = (byte)bitPos; diff --git a/ShaderLibrary/Helpers/ShaderOptionSearcher.cs b/ShaderLibrary/Helpers/ShaderOptionSearcher.cs index 06e9181..6c326b3 100644 --- a/ShaderLibrary/Helpers/ShaderOptionSearcher.cs +++ b/ShaderLibrary/Helpers/ShaderOptionSearcher.cs @@ -13,7 +13,10 @@ public class ShaderOptionSearcher //Program key table indexed by hashed key vector, built once per shader model. //Without this every lookup scans all programs, and the lenient fallback scan //does string work per program per option, which takes seconds per material. - static readonly System.Runtime.CompilerServices.ConditionalWeakTable> _programLookups = new(); + static readonly System.Runtime.CompilerServices.ConditionalWeakTable< + ShaderModel, + Dictionary + > _programLookups = new(); readonly struct KeyVector : IEquatable { @@ -47,28 +50,33 @@ public bool Equals(KeyVector other) } public override bool Equals(object obj) => obj is KeyVector other && Equals(other); + public override int GetHashCode() => _hash; } static Dictionary GetProgramLookup(ShaderModel shader) { - return _programLookups.GetValue(shader, s => - { - int stride = s.StaticKeyLength + s.DynamicKeyLength; - var lookup = new Dictionary(s.Programs.Count); - for (int i = 0; i < s.Programs.Count; i++) + return _programLookups.GetValue( + shader, + s => { - var key = new KeyVector(s.KeyTable, stride * i, stride); - if (!lookup.ContainsKey(key)) - lookup.Add(key, i); + int stride = s.StaticKeyLength + s.DynamicKeyLength; + var lookup = new Dictionary(s.Programs.Count); + for (int i = 0; i < s.Programs.Count; i++) + { + var key = new KeyVector(s.KeyTable, stride * i, stride); + if (!lookup.ContainsKey(key)) + lookup.Add(key, i); + } + return lookup; } - return lookup; - }); + ); } //Profiling: exact hashed hits vs full lenient scans (the expensive path). public static readonly Stopwatch SearchTime = new Stopwatch(); - public static int ExactHits, LenientScans; + public static int ExactHits, + LenientScans; public static int GetProgramIndex(ShaderModel shader, Dictionary options) { @@ -77,7 +85,10 @@ public static int GetProgramIndex(ShaderModel shader, Dictionary { //Generate keys of the shader options and look them up in the hashed key table. int[] key_lookup = WriteOptionKeys(shader, options); - if (GetProgramLookup(shader).TryGetValue(new KeyVector(key_lookup, 0, key_lookup.Length), out int index)) + if ( + GetProgramLookup(shader) + .TryGetValue(new KeyVector(key_lookup, 0, key_lookup.Length), out int index) + ) { ExactHits++; return index; @@ -91,7 +102,10 @@ public static int GetProgramIndex(ShaderModel shader, Dictionary LenientScans++; return LenientSearch(shader, options, key_lookup); } - finally { SearchTime.Stop(); } + finally + { + SearchTime.Stop(); + } } /// @@ -101,7 +115,11 @@ public static int GetProgramIndex(ShaderModel shader, Dictionary /// The query is precompiled to (key index, mask, shift, choice index) so the /// per-program work is integer-only. /// - static int LenientSearch(ShaderModel shader, Dictionary options, int[] expectedKeys) + static int LenientSearch( + ShaderModel shader, + Dictionary options, + int[] expectedKeys + ) { int stride = shader.StaticKeyLength + shader.DynamicKeyLength; @@ -121,7 +139,9 @@ static int LenientSearch(ShaderModel shader, Dictionary options, } else { - int expectedIdx = (int)(((uint)expectedKeys[keyIndex] & option.Bit32Mask) >> option.Bit32Shift); + int expectedIdx = (int)( + ((uint)expectedKeys[keyIndex] & option.Bit32Mask) >> option.Bit32Shift + ); scored.Add((keyIndex, option.Bit32Mask, option.Bit32Shift, expectedIdx)); } } @@ -139,13 +159,16 @@ static int LenientSearch(ShaderModel shader, Dictionary options, } else { - int expectedIdx = (int)(((uint)expectedKeys[keyIndex] & option.Bit32Mask) >> option.Bit32Shift); + int expectedIdx = (int)( + ((uint)expectedKeys[keyIndex] & option.Bit32Mask) >> option.Bit32Shift + ); scored.Add((keyIndex, option.Bit32Mask, option.Bit32Shift, expectedIdx)); } } var table = shader.KeyTable; - int best = -1, bestScore = -1; + int best = -1, + bestScore = -1; for (int i = 0; i < shader.Programs.Count; i++) { int baseIndex = stride * i; @@ -209,7 +232,13 @@ public static int[] WriteOptionKeys(ShaderModel shader, Dictionary options) + public static bool IsValidProgram( + ShaderModel shader, + int programIndex, + Dictionary options + ) { //The amount of keys used per program int numKeysPerProgram = shader.StaticKeyLength + shader.DynamicKeyLength; @@ -274,9 +313,13 @@ public static bool IsValidProgram(ShaderModel shader, int programIndex, Dictiona continue; //Get key in table - int choiceIndex = option.GetChoiceIndex(shader.KeyTable[baseIndex + option.Bit32Index]); + int choiceIndex = option.GetChoiceIndex( + shader.KeyTable[baseIndex + option.Bit32Index] + ); if (choiceIndex > option.Choices.Count) - throw new Exception($"Invalid choice index in key table! Option {option.Name} choice {options[option.Name]}"); + throw new Exception( + $"Invalid choice index in key table! Option {option.Name} choice {options[option.Name]}" + ); //If the choice is not in the program, then skip the current program var choice = option.Choices.GetKey(choiceIndex); @@ -291,7 +334,9 @@ public static bool IsValidProgram(ShaderModel shader, int programIndex, Dictiona continue; int ind = option.Bit32Index - option.KeyOffset; - int choiceIndex = option.GetChoiceIndex(shader.KeyTable[baseIndex + shader.StaticKeyLength + ind]); + int choiceIndex = option.GetChoiceIndex( + shader.KeyTable[baseIndex + shader.StaticKeyLength + ind] + ); if (choiceIndex > option.Choices.Count) throw new Exception($"Invalid choice index in key table!"); @@ -303,7 +348,10 @@ public static bool IsValidProgram(ShaderModel shader, int programIndex, Dictiona } //Checks if the shader option list is missing any shader option choices required for a full key search - public static void CheckMissingShaderOptions(ShaderModel shader, Dictionary options) + public static void CheckMissingShaderOptions( + ShaderModel shader, + Dictionary options + ) { int num_keys_per_program = shader.StaticKeyLength + shader.DynamicKeyLength; for (int i = 0; i < shader.Programs.Count; i++) @@ -313,7 +361,11 @@ public static void CheckMissingShaderOptions(ShaderModel shader, Dictionary options) + static void CheckChoices( + ShaderModel shader, + int programIndex, + Dictionary options + ) { Debug.WriteLine($"checking program {programIndex}"); @@ -324,30 +376,41 @@ static void CheckChoices(ShaderModel shader, int programIndex, Dictionary option.Choices.Count || choiceIndex == -1) - throw new Exception($"Invalid choice index in key table! {option.Name} index {choiceIndex}"); + throw new Exception( + $"Invalid choice index in key table! {option.Name} index {choiceIndex}" + ); string choice = option.Choices.GetKey(choiceIndex); //A shader option choice not set in the lookup and not a default choice //This must be set for a valid lookup if (!options.ContainsKey(option.Name) && choice != option.DefaultChoice) - Debug.WriteLine($"Unexpected choice value {option.Name} should be {choice}, not default {option.DefaultChoice}"); + Debug.WriteLine( + $"Unexpected choice value {option.Name} should be {choice}, not default {option.DefaultChoice}" + ); } for (int j = 0; j < shader.DynamicOptions.Count; j++) { var option = shader.DynamicOptions[j]; int ind = option.Bit32Index - option.KeyOffset; - int choiceIndex = option.GetChoiceIndex(shader.KeyTable[baseIndex + shader.StaticKeyLength + ind]); + int choiceIndex = option.GetChoiceIndex( + shader.KeyTable[baseIndex + shader.StaticKeyLength + ind] + ); if (choiceIndex > option.Choices.Count || choiceIndex == -1) - throw new Exception($"Invalid choice index in key table! {option.Name} index {choiceIndex}"); - + throw new Exception( + $"Invalid choice index in key table! {option.Name} index {choiceIndex}" + ); string choice = option.Choices.GetKey(choiceIndex); if (!options.ContainsKey(option.Name) && choice != option.DefaultChoice) - Debug.WriteLine($"Unexpected choice value {option.Name} should be {choice}, not default {option.DefaultChoice}"); + Debug.WriteLine( + $"Unexpected choice value {option.Name} should be {choice}, not default {option.DefaultChoice}" + ); } } } diff --git a/ShaderLibrary/IO/BinaryDataReader.cs b/ShaderLibrary/IO/BinaryDataReader.cs index 363052c..a6a4a1d 100644 --- a/ShaderLibrary/IO/BinaryDataReader.cs +++ b/ShaderLibrary/IO/BinaryDataReader.cs @@ -16,7 +16,8 @@ public class BinaryDataReader : BinaryReader private bool IsBigEndian = false; private bool IsWiiU = false; - public BinaryDataReader(Stream input, bool is_big_endian = false, bool leaveOpen = false) : base(input, Encoding.UTF8, leaveOpen) + public BinaryDataReader(Stream input, bool is_big_endian = false, bool leaveOpen = false) + : base(input, Encoding.UTF8, leaveOpen) { IsBigEndian = is_big_endian; IsWiiU |= is_big_endian; @@ -25,7 +26,8 @@ public BinaryDataReader(Stream input, bool is_big_endian = false, bool leaveOpen public override uint ReadUInt32() { var bytes = base.ReadBytes(4); - if (IsBigEndian) Array.Reverse(bytes); + if (IsBigEndian) + Array.Reverse(bytes); return BitConverter.ToUInt32(bytes); } @@ -33,7 +35,8 @@ public override uint ReadUInt32() public override int ReadInt32() { var bytes = base.ReadBytes(4); - if (IsBigEndian) Array.Reverse(bytes); + if (IsBigEndian) + Array.Reverse(bytes); return BitConverter.ToInt32(bytes); } @@ -41,7 +44,8 @@ public override int ReadInt32() public override short ReadInt16() { var bytes = base.ReadBytes(2); - if (IsBigEndian) Array.Reverse(bytes); + if (IsBigEndian) + Array.Reverse(bytes); return BitConverter.ToInt16(bytes); } @@ -49,7 +53,8 @@ public override short ReadInt16() public override ushort ReadUInt16() { var bytes = base.ReadBytes(2); - if (IsBigEndian) Array.Reverse(bytes); + if (IsBigEndian) + Array.Reverse(bytes); return BitConverter.ToUInt16(bytes); } @@ -69,14 +74,15 @@ public long ReadOffset() public T ReadCustom(Func value, ulong offset) { - if (offset == 0) return default(T); + if (offset == 0) + return default(T); - using (this.BaseStream.TemporarySeek((long)offset, SeekOrigin.Begin)) { + using (this.BaseStream.TemporarySeek((long)offset, SeekOrigin.Begin)) + { return value.Invoke(); } } - public sbyte[] ReadSbytes(int count) { sbyte[] values = new sbyte[count]; @@ -148,11 +154,13 @@ public string LoadString() public string LoadString(ulong offset) { - if (offset == 0) return ""; + if (offset == 0) + return ""; var shift = IsWiiU ? 0 : 2; //switch shifts by 2 due to string length - using (this.BaseStream.TemporarySeek((long)offset + shift, SeekOrigin.Begin)) { + using (this.BaseStream.TemporarySeek((long)offset + shift, SeekOrigin.Begin)) + { return ReadZeroTerminatedString(); } } @@ -167,8 +175,11 @@ public void SeekBegin(ulong offset) this.BaseStream.Seek((long)offset, SeekOrigin.Begin); } - public T ReadStruct() => Utils.BytesToStruct(ReadBytes(Marshal.SizeOf()), IsBigEndian); - public List ReadMultipleStructs(int count) => Enumerable.Range(0, count).Select(_ => ReadStruct()).ToList(); + public T ReadStruct() => + Utils.BytesToStruct(ReadBytes(Marshal.SizeOf()), IsBigEndian); + + public List ReadMultipleStructs(int count) => + Enumerable.Range(0, count).Select(_ => ReadStruct()).ToList(); public string ReadZeroTerminatedString(int maxLength = int.MaxValue) { @@ -190,7 +201,10 @@ public string ReadZeroTerminatedString(int maxLength = int.MaxValue) public void Align(int align) { var startPos = this.BaseStream.Position; - this.BaseStream.Seek((int)(-this.BaseStream.Position % align + align) % align, SeekOrigin.Current); + this.BaseStream.Seek( + (int)(-this.BaseStream.Position % align + align) % align, + SeekOrigin.Current + ); } } } diff --git a/ShaderLibrary/IO/BinaryDataWriter.cs b/ShaderLibrary/IO/BinaryDataWriter.cs index 8d1392d..6bc6b40 100644 --- a/ShaderLibrary/IO/BinaryDataWriter.cs +++ b/ShaderLibrary/IO/BinaryDataWriter.cs @@ -1,10 +1,10 @@ -using ShaderLibrary.Common; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +using ShaderLibrary.Common; namespace ShaderLibrary.IO { @@ -24,7 +24,8 @@ public class BinaryDataWriter : BinaryWriter public bool IsBigEndian = false; public bool IsWiiU = false; - public BinaryDataWriter(Stream input, bool bigEndian = false) : base(input, Encoding.UTF8, true) + public BinaryDataWriter(Stream input, bool bigEndian = false) + : base(input, Encoding.UTF8, true) { IsBigEndian = bigEndian; IsWiiU = IsBigEndian; @@ -103,9 +104,11 @@ public void WriteSignature(string magic) this.Write(Encoding.ASCII.GetBytes(magic)); } - internal void WriteDictionary(ResDict dict, long offset_pos) where T : IResData, new() + internal void WriteDictionary(ResDict dict, long offset_pos) + where T : IResData, new() { - if (dict.Count == 0) return; + if (dict.Count == 0) + return; this.AlignBytes(8); this.WriteOffset(offset_pos); @@ -127,20 +130,23 @@ internal void WriteOffset(long offset, long target) { var target_relative = target - offset; //Seek to where to write the offset itself and use relative position - using (this.BaseStream.TemporarySeek(offset, SeekOrigin.Begin)) { + using (this.BaseStream.TemporarySeek(offset, SeekOrigin.Begin)) + { Write(((uint)target_relative)); } } else { - //Seek to where to write the offset itself - using (this.BaseStream.TemporarySeek((uint)offset, SeekOrigin.Begin)) { + //Seek to where to write the offset itself + using (this.BaseStream.TemporarySeek((uint)offset, SeekOrigin.Begin)) + { Write(((uint)target)); } } } - private void WriteDictionary(ResDict resDict) where T : IResData, new() + private void WriteDictionary(ResDict resDict) + where T : IResData, new() { resDict.GenerateTree(); var nodes = resDict.GetNodes(); @@ -194,7 +200,9 @@ internal virtual void WriteHeaderBlocks() { if (i < _savedHeaderBlockPositions.Count - 1) { - uint blockSize = (uint)(_savedHeaderBlockPositions[i + 1] - _savedHeaderBlockPositions[i]); + uint blockSize = (uint)( + _savedHeaderBlockPositions[i + 1] - _savedHeaderBlockPositions[i] + ); WriteHeaderBlock(blockSize, blockSize); } } @@ -216,7 +224,10 @@ private void WriteHeaderBlock(uint size, ulong offset) public void AlignBytes(int align, byte pad_val = 0) { var startPos = this.BaseStream.Position; - long position = this.Seek((int)(-this.BaseStream.Position % align + align) % align, SeekOrigin.Current); + long position = this.Seek( + (int)(-this.BaseStream.Position % align + align) % align, + SeekOrigin.Current + ); this.Seek((int)startPos, System.IO.SeekOrigin.Begin); while (this.BaseStream.Position != position) diff --git a/ShaderLibrary/ShaderLibrary.csproj b/ShaderLibrary/ShaderLibrary.csproj index 05a17a7..82f4f4a 100644 --- a/ShaderLibrary/ShaderLibrary.csproj +++ b/ShaderLibrary/ShaderLibrary.csproj @@ -1,7 +1,6 @@ - - net6.0 + net10.0 enable enable True @@ -10,5 +9,4 @@ - diff --git a/ShaderLibrary/Structs.cs b/ShaderLibrary/Structs.cs index 8613dad..63e3a30 100644 --- a/ShaderLibrary/Structs.cs +++ b/ShaderLibrary/Structs.cs @@ -1,10 +1,10 @@ -using Microsoft.VisualBasic; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +using Microsoft.VisualBasic; namespace ShaderLibrary { @@ -41,7 +41,7 @@ public struct BufferMemoryPool [StructLayout(LayoutKind.Sequential, Size = 0x10)] public struct BfshaHeader { - public ulong ShaderArchiveOffset; + public ulong ShaderArchiveOffset; public ulong StringPoolOffset; public uint StringPoolSize; public uint Padding; @@ -76,6 +76,7 @@ public struct ShaderModelHeader // public ulong ImageArrayOffset; public ulong ImageDictionaryOffset; + // public ulong UniformBlockArrayOffset; @@ -87,6 +88,7 @@ public struct ShaderModelHeader public ulong StorageBlockArrayOffset; public ulong StorageBlockDictionaryOffset; public ulong Unknown0; + // public ulong ShaderProgramArrayOffset; @@ -105,6 +107,7 @@ public struct ShaderModelHeader // public ulong Unknown5; public ulong Unknown6; + // public uint NumUniforms; @@ -323,9 +326,9 @@ public struct BnshShaderReflectionHeader public ulong StorageBufferDictionaryOffset; public int OutputIdx; //id in slot list - public int SamplerIdx;//id in slot list - public int UniformBufferIdx;//id in slot list - public int StorageBufferIdx;//id in slot list + public int SamplerIdx; //id in slot list + public int UniformBufferIdx; //id in slot list + public int StorageBufferIdx; //id in slot list public int SlotOffset; diff --git a/ShaderLibrary/Switch/BfshaLoader.cs b/ShaderLibrary/Switch/BfshaLoader.cs index 0071919..27b800d 100644 --- a/ShaderLibrary/Switch/BfshaLoader.cs +++ b/ShaderLibrary/Switch/BfshaLoader.cs @@ -1,5 +1,4 @@ -using ShaderLibrary.IO; -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Drawing; @@ -8,6 +7,7 @@ using System.Text; using System.Threading.Tasks; using System.Xml.Linq; +using ShaderLibrary.IO; namespace ShaderLibrary.Switch { @@ -87,7 +87,7 @@ private static ShaderModel ReadShaderModel(BinaryDataReader reader) ushort dynamicOptionCount = reader.ReadUInt16(); ushort programCount = reader.ReadUInt16(); if (reader.Header.VersionMajor < 7) - reader.ReadUInt16(); //unk + reader.ReadUInt16(); //unk shaderModel.StaticKeyLength = reader.ReadByte(); shaderModel.DynamicKeyLength = reader.ReadByte(); @@ -118,19 +118,26 @@ private static ShaderModel ReadShaderModel(BinaryDataReader reader) var bnshFileStream = new SubStream(reader.BaseStream, shaderFileOffset, bnshSize); shaderModel.BnshFile = new BnshFile(bnshFileStream); - shaderModel.Programs = ReadArray(reader, - (ulong)shaderProgramArrayOffset, - programCount, ReadBfshaShaderProgram); + shaderModel.Programs = ReadArray( + reader, + (ulong)shaderProgramArrayOffset, + programCount, + ReadBfshaShaderProgram + ); foreach (var program in shaderModel.Programs) program.ParentShader = shaderModel; - shaderModel.KeyTable = reader.ReadCustom(() => - { - int numKeysPerProgram = shaderModel.StaticKeyLength + shaderModel.DynamicKeyLength; + shaderModel.KeyTable = reader.ReadCustom( + () => + { + int numKeysPerProgram = + shaderModel.StaticKeyLength + shaderModel.DynamicKeyLength; - return reader.ReadInt32s(numKeysPerProgram * shaderModel.Programs.Count); - }, (ulong)keyTableOffset); + return reader.ReadInt32s(numKeysPerProgram * shaderModel.Programs.Count); + }, + (ulong)keyTableOffset + ); if (symbolInfoOffset != 0) { @@ -195,14 +202,18 @@ static ShaderOption ReadShaderOption(BinaryDataReader reader) uint padding = reader.ReadUInt32(); } - option.ChoiceValues = reader.ReadCustom(() => - { - return reader.ReadUInt32s(choiceCount); - }, choiceValuesOffset); + option.ChoiceValues = reader.ReadCustom( + () => + { + return reader.ReadUInt32s(choiceCount); + }, + choiceValuesOffset + ); return option; } - static internal ResUint32 ReadResUint(BinaryDataReader reader) => new ResUint32(reader.ReadUInt32()); + internal static ResUint32 ReadResUint(BinaryDataReader reader) => + new ResUint32(reader.ReadUInt32()); static BfshaStorageBuffer ReadStorageBuffer(BinaryDataReader reader) { @@ -223,14 +234,18 @@ static BfshaUniformBlock ReadBfshaUniformBlock(BinaryDataReader reader) long pos = reader.BaseStream.Position; - block.Uniforms = LoadDictionary(reader, - block.header.UniformDictionaryOffset, - block.header.UniformArrayOffset, - ReadBfshaUniform); + block.Uniforms = LoadDictionary( + reader, + block.header.UniformDictionaryOffset, + block.header.UniformArrayOffset, + ReadBfshaUniform + ); // Read default buffer - block.DefaultBuffer = reader.ReadCustom(() => - reader.ReadBytes(block.header.Size), (uint)block.header.DefaultOffset); + block.DefaultBuffer = reader.ReadCustom( + () => reader.ReadBytes(block.header.Size), + (uint)block.header.DefaultOffset + ); reader.SeekBegin(pos); return block; @@ -261,10 +276,30 @@ static BfshaShaderProgram ReadBfshaShaderProgram(BinaryDataReader reader) prog.UsedAttributeFlags = header.UsedAttributeFlags; prog.Flags = header.Flags; - prog.UniformBlockIndices = ReadArray(reader, header.UniformIndexTableBlockOffset, header.NumBlocks, ReadShaderLocations); - prog.SamplerIndices = ReadArray(reader, header.SamplerIndexTableOffset, header.NumSamplers, ReadShaderLocations); - prog.ImageIndices = ReadArray(reader, header.ImageIndexTableOffset, header.NumImages, ReadShaderLocations); - prog.StorageBufferIndices = ReadArray(reader, header.StorageBufferIndexTableOffset, header.NumStorageBuffers, ReadShaderLocations); + prog.UniformBlockIndices = ReadArray( + reader, + header.UniformIndexTableBlockOffset, + header.NumBlocks, + ReadShaderLocations + ); + prog.SamplerIndices = ReadArray( + reader, + header.SamplerIndexTableOffset, + header.NumSamplers, + ReadShaderLocations + ); + prog.ImageIndices = ReadArray( + reader, + header.ImageIndexTableOffset, + header.NumImages, + ReadShaderLocations + ); + prog.StorageBufferIndices = ReadArray( + reader, + header.StorageBufferIndexTableOffset, + header.NumStorageBuffers, + ReadShaderLocations + ); } else if (reader.Header.VersionMajor >= 7) { @@ -275,9 +310,24 @@ static BfshaShaderProgram ReadBfshaShaderProgram(BinaryDataReader reader) prog.UsedAttributeFlags = header.UsedAttributeFlags; prog.Flags = header.Flags; - prog.UniformBlockIndices = ReadArray(reader, header.UniformIndexTableBlockOffset, header.NumBlocks, ReadShaderLocations); - prog.SamplerIndices = ReadArray(reader, header.SamplerIndexTableOffset, header.NumSamplers, ReadShaderLocations); - prog.StorageBufferIndices = ReadArray(reader, header.StorageBufferIndexTableOffset, header.NumStorageBuffers, ReadShaderLocations); + prog.UniformBlockIndices = ReadArray( + reader, + header.UniformIndexTableBlockOffset, + header.NumBlocks, + ReadShaderLocations + ); + prog.SamplerIndices = ReadArray( + reader, + header.SamplerIndexTableOffset, + header.NumSamplers, + ReadShaderLocations + ); + prog.StorageBufferIndices = ReadArray( + reader, + header.StorageBufferIndexTableOffset, + header.NumStorageBuffers, + ReadShaderLocations + ); } else if (reader.Header.VersionMajor >= 5) { @@ -287,8 +337,18 @@ static BfshaShaderProgram ReadBfshaShaderProgram(BinaryDataReader reader) prog.VariationOffset = header.VariationOffset; prog.UsedAttributeFlags = header.UsedAttributeFlags; prog.Flags = header.Flags; - prog.UniformBlockIndices = ReadArray(reader, header.UniformIndexTableBlockOffset, header.NumBlocks, ReadShaderLocations); - prog.SamplerIndices = ReadArray(reader, header.SamplerIndexTableOffset, header.NumSamplers, ReadShaderLocations); + prog.UniformBlockIndices = ReadArray( + reader, + header.UniformIndexTableBlockOffset, + header.NumBlocks, + ReadShaderLocations + ); + prog.SamplerIndices = ReadArray( + reader, + header.SamplerIndexTableOffset, + header.NumSamplers, + ReadShaderLocations + ); } else { @@ -299,8 +359,18 @@ static BfshaShaderProgram ReadBfshaShaderProgram(BinaryDataReader reader) prog.UsedAttributeFlags = header.UsedAttributeFlags; prog.Flags = header.Flags; - prog.UniformBlockIndices = ReadArray(reader, header.UniformIndexTableBlockOffset, header.NumBlocks, ReadShaderLocations); - prog.SamplerIndices = ReadArray(reader, header.SamplerIndexTableOffset, header.NumSamplers, ReadShaderLocations); + prog.UniformBlockIndices = ReadArray( + reader, + header.UniformIndexTableBlockOffset, + header.NumBlocks, + ReadShaderLocations + ); + prog.SamplerIndices = ReadArray( + reader, + header.SamplerIndexTableOffset, + header.NumSamplers, + ReadShaderLocations + ); } return prog; } @@ -335,22 +405,67 @@ static SymbolData ReadSymbolTable(BinaryDataReader reader, ShaderModel shaderMod if (reader.Header.VersionMajor >= 8) { - symbolTable.Samplers = ReadArray(reader, reader.ReadUInt64(), shaderModel.Samplers.Count, ReadSymbol); - symbolTable.Images = ReadArray(reader, reader.ReadUInt64(), shaderModel.Images.Count, ReadSymbol); - symbolTable.UniformBlocks = ReadArray(reader, reader.ReadUInt64(), shaderModel.UniformBlocks.Count, ReadSymbol); - symbolTable.StorageBuffers = ReadArray(reader, reader.ReadUInt64(), shaderModel.StorageBuffers.Count, ReadSymbol); + symbolTable.Samplers = ReadArray( + reader, + reader.ReadUInt64(), + shaderModel.Samplers.Count, + ReadSymbol + ); + symbolTable.Images = ReadArray( + reader, + reader.ReadUInt64(), + shaderModel.Images.Count, + ReadSymbol + ); + symbolTable.UniformBlocks = ReadArray( + reader, + reader.ReadUInt64(), + shaderModel.UniformBlocks.Count, + ReadSymbol + ); + symbolTable.StorageBuffers = ReadArray( + reader, + reader.ReadUInt64(), + shaderModel.StorageBuffers.Count, + ReadSymbol + ); } else if (reader.Header.VersionMajor >= 7) { - symbolTable.Samplers = ReadArray(reader, reader.ReadUInt64(), shaderModel.Samplers.Count, ReadSymbol); - symbolTable.UniformBlocks = ReadArray(reader, reader.ReadUInt64(), shaderModel.UniformBlocks.Count, ReadSymbol); - symbolTable.StorageBuffers = ReadArray(reader, reader.ReadUInt64(), shaderModel.StorageBuffers.Count, ReadSymbol); + symbolTable.Samplers = ReadArray( + reader, + reader.ReadUInt64(), + shaderModel.Samplers.Count, + ReadSymbol + ); + symbolTable.UniformBlocks = ReadArray( + reader, + reader.ReadUInt64(), + shaderModel.UniformBlocks.Count, + ReadSymbol + ); + symbolTable.StorageBuffers = ReadArray( + reader, + reader.ReadUInt64(), + shaderModel.StorageBuffers.Count, + ReadSymbol + ); reader.ReadUInt64(); } else { - symbolTable.Samplers = ReadArray(reader, reader.ReadUInt64(), shaderModel.Samplers.Count, ReadSymbol); - symbolTable.UniformBlocks = ReadArray(reader, reader.ReadUInt64(), shaderModel.UniformBlocks.Count, ReadSymbol); + symbolTable.Samplers = ReadArray( + reader, + reader.ReadUInt64(), + shaderModel.Samplers.Count, + ReadSymbol + ); + symbolTable.UniformBlocks = ReadArray( + reader, + reader.ReadUInt64(), + shaderModel.UniformBlocks.Count, + ReadSymbol + ); reader.ReadUInt64(); reader.ReadUInt64(); } @@ -379,8 +494,13 @@ static SymbolData.SymbolEntry ReadSymbol(BinaryDataReader reader) return symbol; } - static List ReadArray(BinaryDataReader reader, ulong offset, int count, - Func load_section) where T : class, new() + static List ReadArray( + BinaryDataReader reader, + ulong offset, + int count, + Func load_section + ) + where T : class, new() { var start = reader.Position; reader.SeekBegin((long)offset); @@ -393,15 +513,24 @@ static List ReadArray(BinaryDataReader reader, ulong offset, int count, return values.ToList(); } - static internal ResDict LoadDictionary(BinaryDataReader reader, Func load_section) where T : class, IResData, new() + internal static ResDict LoadDictionary( + BinaryDataReader reader, + Func load_section + ) + where T : class, IResData, new() { var valuesOffset = reader.ReadUInt64(); var dictOffset = reader.ReadUInt64(); return LoadDictionary(reader, dictOffset, valuesOffset, load_section); } - static internal ResDict LoadDictionary(BinaryDataReader reader, - ulong dictOffset, ulong valuesOffset, Func load_section) where T : class, IResData, new() + internal static ResDict LoadDictionary( + BinaryDataReader reader, + ulong dictOffset, + ulong valuesOffset, + Func load_section + ) + where T : class, IResData, new() { ResDict dict = new ResDict(); if (dictOffset == 0) @@ -419,13 +548,15 @@ static internal ResDict LoadDictionary(BinaryDataReader reader, var idxRight = reader.ReadUInt16(); var key = reader.LoadString(reader.ReadUInt64()); - dict._nodes.Add(new ResDict.Node() - { - Reference = refe, - IdxLeft = idxLeft, - IdxRight = idxRight, - Key = key, - }); + dict._nodes.Add( + new ResDict.Node() + { + Reference = refe, + IdxLeft = idxLeft, + IdxRight = idxRight, + Key = key, + } + ); if (i == 0) continue; diff --git a/ShaderLibrary/Switch/BnshLoader.cs b/ShaderLibrary/Switch/BnshLoader.cs index 1fd1ff1..8d296de 100644 --- a/ShaderLibrary/Switch/BnshLoader.cs +++ b/ShaderLibrary/Switch/BnshLoader.cs @@ -1,10 +1,10 @@ -using ShaderLibrary.IO; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using ShaderLibrary.IO; namespace ShaderLibrary.Switch { @@ -40,7 +40,9 @@ private static BnshFile.ShaderVariation ReadShaderVariation(BinaryDataReader rea return variation; } - internal static BnshFile.BnshShaderProgram ReadBnshShaderProgram(BnshFile.ShaderVariation variation) + internal static BnshFile.BnshShaderProgram ReadBnshShaderProgram( + BnshFile.ShaderVariation variation + ) { BnshFile.BnshShaderProgram program = new(); @@ -54,8 +56,14 @@ internal static BnshFile.BnshShaderProgram ReadBnshShaderProgram(BnshFile.Shader program.FragmentShader = ReadShaderCode(reader, program.header.FragmentShaderOffset); program.GeometryShader = ReadShaderCode(reader, program.header.GeometryShaderOffset); program.ComputeShader = ReadShaderCode(reader, program.header.ComputeShaderOffset); - program.TessellationControlShader = ReadShaderCode(reader, program.header.TessellationControlShaderOffset); - program.TessellationEvalShader = ReadShaderCode(reader, program.header.TessellationEvalShaderOffset); + program.TessellationControlShader = ReadShaderCode( + reader, + program.header.TessellationControlShaderOffset + ); + program.TessellationEvalShader = ReadShaderCode( + reader, + program.header.TessellationEvalShaderOffset + ); reader.SeekBegin(program.header.ObjectOffset); program.MemoryData = reader.ReadBytes((int)program.header.ObjectSize); @@ -66,7 +74,10 @@ internal static BnshFile.BnshShaderProgram ReadBnshShaderProgram(BnshFile.Shader //offsets var offsets = reader.ReadUInt64s(6); program.VertexShaderReflection = ReadReflectionData(reader, offsets[0]); - program.TessellationControlShaderReflection = ReadReflectionData(reader, offsets[1]); + program.TessellationControlShaderReflection = ReadReflectionData( + reader, + offsets[1] + ); program.TessellationEvalShaderReflection = ReadReflectionData(reader, offsets[2]); program.GeometryShaderReflection = ReadReflectionData(reader, offsets[3]); program.FragmentShaderReflection = ReadReflectionData(reader, offsets[4]); @@ -80,7 +91,8 @@ internal static BnshFile.BnshShaderProgram ReadBnshShaderProgram(BnshFile.Shader private static BnshFile.ShaderCode ReadShaderCode(BinaryDataReader reader, ulong offset) { - if (offset == 0) return null; + if (offset == 0) + return null; reader.SeekBegin(offset); BnshFile.ShaderCode code = new(); @@ -91,22 +103,32 @@ private static BnshFile.ShaderCode ReadShaderCode(BinaryDataReader reader, ulong uint controlCodeSize = reader.ReadUInt32(); code.Reserved = reader.ReadBytes(32); //padding - code.ControlCode = reader.ReadCustom(() => - { - return reader.ReadBytes((int)controlCodeSize); - }, controlCodeOffset); - - code.ByteCode = reader.ReadCustom(() => - { - return reader.ReadBytes((int)byteCodeSize); - }, byteCodeOffset); + code.ControlCode = reader.ReadCustom( + () => + { + return reader.ReadBytes((int)controlCodeSize); + }, + controlCodeOffset + ); + + code.ByteCode = reader.ReadCustom( + () => + { + return reader.ReadBytes((int)byteCodeSize); + }, + byteCodeOffset + ); return code; } - private static BnshFile.ShaderReflectionData ReadReflectionData(BinaryDataReader reader, ulong offset) + private static BnshFile.ShaderReflectionData ReadReflectionData( + BinaryDataReader reader, + ulong offset + ) { - if (offset == 0) return null; + if (offset == 0) + return null; reader.SeekBegin(offset); BnshFile.ShaderReflectionData reflect = new(); @@ -114,21 +136,47 @@ private static BnshFile.ShaderReflectionData ReadReflectionData(BinaryDataReader reader.BaseStream.Read(Utils.AsSpan(ref reflect.header)); var pos = reader.BaseStream.Position; - reflect.Inputs = BfshaLoader.LoadDictionary(reader, reflect.header.InputDictionaryOffset, 0, BfshaLoader.ReadResUint); - reflect.Outputs = BfshaLoader.LoadDictionary(reader, reflect.header.OutputDictionaryOffset, 0, BfshaLoader.ReadResUint); - reflect.Samplers = BfshaLoader.LoadDictionary(reader, reflect.header.SamplerDictionaryOffset, 0, BfshaLoader.ReadResUint); - reflect.UniformBuffers = BfshaLoader.LoadDictionary(reader, reflect.header.UniformBufferDictionaryOffset, 0, BfshaLoader.ReadResUint); - reflect.StorageBuffers = BfshaLoader.LoadDictionary(reader, reflect.header.StorageBufferDictionaryOffset, 0, BfshaLoader.ReadResUint); + reflect.Inputs = BfshaLoader.LoadDictionary( + reader, + reflect.header.InputDictionaryOffset, + 0, + BfshaLoader.ReadResUint + ); + reflect.Outputs = BfshaLoader.LoadDictionary( + reader, + reflect.header.OutputDictionaryOffset, + 0, + BfshaLoader.ReadResUint + ); + reflect.Samplers = BfshaLoader.LoadDictionary( + reader, + reflect.header.SamplerDictionaryOffset, + 0, + BfshaLoader.ReadResUint + ); + reflect.UniformBuffers = BfshaLoader.LoadDictionary( + reader, + reflect.header.UniformBufferDictionaryOffset, + 0, + BfshaLoader.ReadResUint + ); + reflect.StorageBuffers = BfshaLoader.LoadDictionary( + reader, + reflect.header.StorageBufferDictionaryOffset, + 0, + BfshaLoader.ReadResUint + ); if (reflect.header.SlotCount > 0) { - reflect.Slots = reader.ReadCustom(() => reader.ReadInt32s((int)reflect.header.SlotCount), (uint)reflect.header.SlotOffset); + reflect.Slots = reader.ReadCustom( + () => reader.ReadInt32s((int)reflect.header.SlotCount), + (uint)reflect.header.SlotOffset + ); reflect.AssignSlots(reflect.Slots); } reader.SeekBegin(pos); return reflect; } - - } } diff --git a/ShaderLibrary/Switch/BnshSaver.cs b/ShaderLibrary/Switch/BnshSaver.cs index 9a295f8..74507c8 100644 --- a/ShaderLibrary/Switch/BnshSaver.cs +++ b/ShaderLibrary/Switch/BnshSaver.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; +using System.Reflection.PortableExecutable; using System.Text; using System.Xml.Linq; -using System.IO; -using System.Reflection.PortableExecutable; using ShaderLibrary.Common; -using System.Diagnostics; using ShaderLibrary.IO; namespace ShaderLibrary @@ -19,7 +19,6 @@ public class BnshSaver public RelocationTable RelocationTable = new RelocationTable(); public StringTable StringTable = new StringTable(); - private long _fileSizePos; public void Save(BnshFile bnsh, BinaryDataWriter writer) @@ -75,8 +74,15 @@ public void Save(BnshFile bnsh, BinaryDataWriter writer) long shaderVarPos = writer.BaseStream.Position; writer.WriteOffset(shaderVariationArrayOffset); - RelocationTable.SaveEntry(writer, - (uint)writer.BaseStream.Position + 16, 2, (uint)bnsh.Variations.Count, 6, 0, "variation headers"); + RelocationTable.SaveEntry( + writer, + (uint)writer.BaseStream.Position + 16, + 2, + (uint)bnsh.Variations.Count, + 6, + 0, + "variation headers" + ); //Shader variations long variationPos = writer.BaseStream.Position; @@ -96,7 +102,9 @@ public void Save(BnshFile bnsh, BinaryDataWriter writer) long[] objOffsets = new long[bnsh.Variations.Count]; - Dictionary data_blocks = new Dictionary(new ByteArrayComparer()); + Dictionary data_blocks = new Dictionary( + new ByteArrayComparer() + ); int num_dict = 0; @@ -141,13 +149,14 @@ public void Save(BnshFile bnsh, BinaryDataWriter writer) //shader codes void WriteShaderHeaders(BnshFile.ShaderCode code, int idx, ref long byteCodeOffset) { - if (code == null || code.ByteCode == null) return; + if (code == null || code.ByteCode == null) + return; writer.WriteOffset(stageOffsets[idx]); writer.Write(0L); //0 - controlCodeOffsets[idx] = writer.SaveOffset(); //Control code offset - byteCodeOffset = writer.SaveOffset(); //Byte code offset + controlCodeOffsets[idx] = writer.SaveOffset(); //Control code offset + byteCodeOffset = writer.SaveOffset(); //Byte code offset writer.Write((uint)code.ByteCode.Length); writer.Write((uint)code.ControlCode.Length); @@ -163,16 +172,34 @@ void WriteShaderHeaders(BnshFile.ShaderCode code, int idx, ref long byteCodeOffs var num_headers = (uint)(writer.BaseStream.Position - spos) / 64; - RelocationTable.SaveEntry(writer, (uint)spos + 8, 1, num_headers, 7, 0, "control code offsets"); - RelocationTable.SaveEntry(writer, (uint)spos + 16, 1, num_headers, 7, 4, "byte code offsets"); + RelocationTable.SaveEntry( + writer, + (uint)spos + 8, + 1, + num_headers, + 7, + 0, + "control code offsets" + ); + RelocationTable.SaveEntry( + writer, + (uint)spos + 16, + 1, + num_headers, + 7, + 4, + "byte code offsets" + ); long[] stageReflectOffsets = new long[6]; //reflection - if (prog.VertexShaderReflection != null || - prog.FragmentShaderReflection != null || - prog.GeometryShaderReflection != null || - prog.ComputeShaderReflection != null) + if ( + prog.VertexShaderReflection != null + || prog.FragmentShaderReflection != null + || prog.GeometryShaderReflection != null + || prog.ComputeShaderReflection != null + ) { writer.WriteOffset(reflectionOffset); @@ -192,7 +219,12 @@ void SaveControlCode(BnshFile.ShaderCode code, int idx) if (data_blocks.ContainsKey(data)) { - using (writer.BaseStream.TemporarySeek(controlCodeOffsets[idx], SeekOrigin.Begin)) + using ( + writer.BaseStream.TemporarySeek( + controlCodeOffsets[idx], + SeekOrigin.Begin + ) + ) { writer.Write(data_blocks[data]); } @@ -216,7 +248,9 @@ void SaveControlCode(BnshFile.ShaderCode code, int idx) SaveControlCode(prog.ComputeShader, 5); //reflection data - BnshFile.ShaderReflectionData[] reflectionDatas = new BnshFile.ShaderReflectionData[6]; + BnshFile.ShaderReflectionData[] reflectionDatas = new BnshFile.ShaderReflectionData[ + 6 + ]; reflectionDatas[0] = prog.VertexShaderReflection; reflectionDatas[1] = prog.TessellationControlShaderReflection; reflectionDatas[2] = prog.TessellationEvalShaderReflection; @@ -242,8 +276,10 @@ void SaveControlCode(BnshFile.ShaderCode code, int idx) reflectionDatas[j].header.InputDictionaryOffset = (ulong)writer.SaveOffset(); reflectionDatas[j].header.OutputDictionaryOffset = (ulong)writer.SaveOffset(); reflectionDatas[j].header.SamplerDictionaryOffset = (ulong)writer.SaveOffset(); - reflectionDatas[j].header.UniformBufferDictionaryOffset = (ulong)writer.SaveOffset(); - reflectionDatas[j].header.StorageBufferDictionaryOffset = (ulong)writer.SaveOffset(); + reflectionDatas[j].header.UniformBufferDictionaryOffset = (ulong) + writer.SaveOffset(); + reflectionDatas[j].header.StorageBufferDictionaryOffset = (ulong) + writer.SaveOffset(); writer.Write(reflectionDatas[j].header.OutputIdx); writer.Write(reflectionDatas[j].header.SamplerIdx); @@ -274,9 +310,18 @@ void SaveControlCode(BnshFile.ShaderCode code, int idx) writer.WriteDictionary(data.Inputs, (long)data.header.InputDictionaryOffset); writer.WriteDictionary(data.Outputs, (long)data.header.OutputDictionaryOffset); - writer.WriteDictionary(data.Samplers, (long)data.header.SamplerDictionaryOffset); - writer.WriteDictionary(data.UniformBuffers, (long)data.header.UniformBufferDictionaryOffset); - writer.WriteDictionary(data.StorageBuffers, (long)data.header.StorageBufferDictionaryOffset); + writer.WriteDictionary( + data.Samplers, + (long)data.header.SamplerDictionaryOffset + ); + writer.WriteDictionary( + data.UniformBuffers, + (long)data.header.UniformBufferDictionaryOffset + ); + writer.WriteDictionary( + data.StorageBuffers, + (long)data.header.StorageBufferDictionaryOffset + ); if (data.Slots.Length > 0) { @@ -284,7 +329,12 @@ void SaveControlCode(BnshFile.ShaderCode code, int idx) //Uint32 offset var pos = writer.BaseStream.Position; - using (writer.BaseStream.TemporarySeek(data.header.SlotOffset, System.IO.SeekOrigin.Begin)) + using ( + writer.BaseStream.TemporarySeek( + data.header.SlotOffset, + System.IO.SeekOrigin.Begin + ) + ) { writer.Write((uint)pos); } @@ -331,7 +381,11 @@ void SaveControlCode(BnshFile.ShaderCode code, int idx) long obj_size = writer.BaseStream.Position - obj_start; //relocation table section 1 start/end - RelocationTable.SetRelocationSection(1, (uint)writer.BaseStream.Position, (uint)obj_size); + RelocationTable.SetRelocationSection( + 1, + (uint)writer.BaseStream.Position, + (uint)obj_size + ); //relocation table section 2 start/end RelocationTable.SetRelocationSection(2, (uint)writer.BaseStream.Position, 0u); //relocation table section 3 start/end @@ -344,7 +398,9 @@ void SaveControlCode(BnshFile.ShaderCode code, int idx) writer.WriteOffset(binary_buffer_offset); - Dictionary code_blocks = new Dictionary(new ByteArrayComparer()); + Dictionary code_blocks = new Dictionary( + new ByteArrayComparer() + ); for (int i = 0; i < bnsh.Variations.Count; i++) { @@ -385,7 +441,8 @@ void SaveShaderCode(BnshFile.ShaderCode code, long offset_save) long data_end = writer.BaseStream.Position; long data_size = data_end - data_start; - using (writer.BaseStream.TemporarySeek(binary_buffer_size, SeekOrigin.Begin)) { + using (writer.BaseStream.TemporarySeek(binary_buffer_size, SeekOrigin.Begin)) + { writer.Write((uint)data_size); } @@ -408,7 +465,11 @@ void SaveShaderCode(BnshFile.ShaderCode code, long offset_save) StringTable.Write(writer); //relocation table section 5 start/end - RelocationTable.SetRelocationSection(5, (uint)stringPoolOffset, (uint)writer.BaseStream.Position - stringPoolOffset); + RelocationTable.SetRelocationSection( + 5, + (uint)stringPoolOffset, + (uint)writer.BaseStream.Position - stringPoolOffset + ); RelocationTable.Write(writer); @@ -440,6 +501,7 @@ public bool Equals(byte[] left, byte[] right) } return left.SequenceEqual(right); } + public int GetHashCode(byte[] key) { if (key == null) diff --git a/ShaderLibrary/Switch/BshaSaver.cs b/ShaderLibrary/Switch/BshaSaver.cs index f09b9ed..544ee07 100644 --- a/ShaderLibrary/Switch/BshaSaver.cs +++ b/ShaderLibrary/Switch/BshaSaver.cs @@ -1,6 +1,4 @@ -using ShaderLibrary.Common; -using ShaderLibrary.IO; -using System; +using System; using System.Collections.Generic; using System.Drawing; using System.Linq; @@ -8,6 +6,8 @@ using System.Text; using System.Threading.Tasks; using System.Xml.Linq; +using ShaderLibrary.Common; +using ShaderLibrary.IO; namespace ShaderLibrary { @@ -163,8 +163,14 @@ public void Save(BfshaFile bfsha, BinaryDataWriter writer) var model = bfsha.ShaderModels[i]; var ofs_list = saved_models[i]; - RelocationTable.SaveEntry(writer, 3, - (uint)model.StaticOptions.Count, 2, 0, "Static Options"); + RelocationTable.SaveEntry( + writer, + 3, + (uint)model.StaticOptions.Count, + 2, + 0, + "Static Options" + ); writer.AlignBytes(8); writer.WriteOffset(ofs_list.static_option_offset); @@ -211,8 +217,14 @@ void SaveOption(ShaderOption op) writer.AlignBytes(8); writer.WriteOffset(ofs_list.dynamic_option_offset); - RelocationTable.SaveEntry(writer, 3, - (uint)model.DynamicOptions.Count, 2, 0, "Dynamic Options"); + RelocationTable.SaveEntry( + writer, + 3, + (uint)model.DynamicOptions.Count, + 2, + 0, + "Dynamic Options" + ); foreach (var op in model.DynamicOptions.Values) SaveOption(op); @@ -229,8 +241,7 @@ void SaveOption(ShaderOption op) writer.AlignBytes(8); writer.WriteOffset(ofs_list.sampler_offset); - RelocationTable.SaveEntry(writer, 1, - (uint)model.Samplers.Count, 1, 0, "Samplers"); + RelocationTable.SaveEntry(writer, 1, (uint)model.Samplers.Count, 1, 0, "Samplers"); foreach (var sampler in model.Samplers.Values) { @@ -242,8 +253,14 @@ void SaveOption(ShaderOption op) writer.AlignBytes(8); writer.WriteOffset(ofs_list.uniform_block_offset); - RelocationTable.SaveEntry(writer, 3, - (uint)model.UniformBlocks.Count, 1, 0, "Uniform Blocks"); + RelocationTable.SaveEntry( + writer, + 3, + (uint)model.UniformBlocks.Count, + 1, + 0, + "Uniform Blocks" + ); foreach (var uniformBlocks in model.UniformBlocks.Values) { @@ -269,12 +286,10 @@ void SaveOption(ShaderOption op) } } - writer.AlignBytes(8); writer.WriteOffset(ofs_list.uniform_offset); - var num_uniforms = - (uint)model.UniformBlocks.Values.Sum(x => x.Uniforms.Count); + var num_uniforms = (uint)model.UniformBlocks.Values.Sum(x => x.Uniforms.Count); RelocationTable.SaveEntry(writer, 1, num_uniforms, 1, 0, "Uniform Vars"); @@ -308,14 +323,34 @@ void SaveOption(ShaderOption op) else if (bfsha.BinHeader.VersionMajor == 5) num_padding = 6; - RelocationTable.SaveEntry(writer, num_offsets, - (uint)model.Programs.Count, 4, 0, "Shader Programs"); - - RelocationTable.SaveEntry(writer, (uint)writer.Position + (num_offsets * 8), 1, - (uint)model.Programs.Count, num_padding, 1, "Shader Variations"); - - RelocationTable.SaveEntry(writer, (uint)writer.Position + (num_offsets * 8) + 8, 1, - (uint)model.Programs.Count, num_padding, 0, "Shader Program model pos"); + RelocationTable.SaveEntry( + writer, + num_offsets, + (uint)model.Programs.Count, + 4, + 0, + "Shader Programs" + ); + + RelocationTable.SaveEntry( + writer, + (uint)writer.Position + (num_offsets * 8), + 1, + (uint)model.Programs.Count, + num_padding, + 1, + "Shader Variations" + ); + + RelocationTable.SaveEntry( + writer, + (uint)writer.Position + (num_offsets * 8) + 8, + 1, + (uint)model.Programs.Count, + num_padding, + 0, + "Shader Program model pos" + ); foreach (var prog in model.Programs) { @@ -361,8 +396,16 @@ void SaveOption(ShaderOption op) writer.Write((ushort)prog.UniformBlockIndices.Count); writer.Write(new byte[6]); - RelocationTable.SaveEntry(writer, (uint)writer.Position, 1, 1, 0, 0, "Shader Program v5 unk offset"); - writer.Write(new byte[8]); //version 5 is weird, write this unused pointer + RelocationTable.SaveEntry( + writer, + (uint)writer.Position, + 1, + 1, + 0, + 0, + "Shader Program v5 unk offset" + ); + writer.Write(new byte[8]); //version 5 is weird, write this unused pointer } else { @@ -425,7 +468,7 @@ void SaveOption(ShaderOption op) writer.Write(0UL); writer.Write(0UL); } - + uint num_symbol_offsets = 4; if (bfsha.BinHeader.VersionMajor > 8) num_symbol_offsets = 1; @@ -434,7 +477,7 @@ void SaveOption(ShaderOption op) void SaveSymbol(SymbolData.SymbolEntry entry) { - writer.SaveString(entry.Name1); + writer.SaveString(entry.Name1); if (bfsha.BinHeader.VersionMajor <= 8) { writer.SaveString(entry.Value1); @@ -456,8 +499,15 @@ void SaveSymbol(SymbolData.SymbolEntry entry) { writer.WriteOffset(samplerSymbolsOfsPos); - RelocationTable.SaveEntry(writer, (uint)writer.Position, num_symbol_offsets, - (uint)model.SymbolData.Samplers.Count, 0, 0, "Samplers Symbols"); + RelocationTable.SaveEntry( + writer, + (uint)writer.Position, + num_symbol_offsets, + (uint)model.SymbolData.Samplers.Count, + 0, + 0, + "Samplers Symbols" + ); foreach (var symbol in model.SymbolData.Samplers) SaveSymbol(symbol); @@ -467,19 +517,36 @@ void SaveSymbol(SymbolData.SymbolEntry entry) { writer.WriteOffset(uniformBlockSymbolsOfsPos); - RelocationTable.SaveEntry(writer, (uint)writer.Position, num_symbol_offsets, - (uint)model.SymbolData.UniformBlocks.Count, 0, 0, "UniformBlocks Symbols"); + RelocationTable.SaveEntry( + writer, + (uint)writer.Position, + num_symbol_offsets, + (uint)model.SymbolData.UniformBlocks.Count, + 0, + 0, + "UniformBlocks Symbols" + ); foreach (var symbol in model.SymbolData.UniformBlocks) SaveSymbol(symbol); } - if (bfsha.BinHeader.VersionMajor >= 7 && model.SymbolData.StorageBuffers.Count > 0) + if ( + bfsha.BinHeader.VersionMajor >= 7 + && model.SymbolData.StorageBuffers.Count > 0 + ) { writer.WriteOffset(storageBlockSymbolsOfsPos); - RelocationTable.SaveEntry(writer, (uint)writer.Position, num_symbol_offsets, - (uint)model.SymbolData.StorageBuffers.Count, 0, 0, "StorageBuffer Symbols"); + RelocationTable.SaveEntry( + writer, + (uint)writer.Position, + num_symbol_offsets, + (uint)model.SymbolData.StorageBuffers.Count, + 0, + 0, + "StorageBuffer Symbols" + ); foreach (var symbol in model.SymbolData.StorageBuffers) SaveSymbol(symbol); @@ -493,7 +560,10 @@ void SaveSymbol(SymbolData.SymbolEntry entry) writer.WriteDictionary(model.UniformBlocks, ofs_list.uniform_block_dict_offset); if (bfsha.BinHeader.VersionMajor >= 7) - writer.WriteDictionary(model.StorageBuffers, ofs_list.storage_block_dict_offset); + writer.WriteDictionary( + model.StorageBuffers, + ofs_list.storage_block_dict_offset + ); foreach (var block in model.UniformBlocks.Values) writer.WriteDictionary(block.Uniforms, block._uniformVarDictOfsPos); @@ -552,7 +622,9 @@ void SaveLocations(List locations, long ofs) var size = writer.Position - pos; - using (writer.BaseStream.TemporarySeek(string_pool_offset + 8, System.IO.SeekOrigin.Begin)) + using ( + writer.BaseStream.TemporarySeek(string_pool_offset + 8, System.IO.SeekOrigin.Begin) + ) { writer.Write((uint)size); } @@ -590,7 +662,12 @@ void SaveLocations(List locations, long ofs) for (int j = 0; j < model.Programs.Count; j++) { var program = model.Programs[j]; - using (writer.BaseStream.TemporarySeek(shader_pos + variationStartOffset + (program.VariationIndex * 64), System.IO.SeekOrigin.Begin)) + using ( + writer.BaseStream.TemporarySeek( + shader_pos + variationStartOffset + (program.VariationIndex * 64), + System.IO.SeekOrigin.Begin + ) + ) { writer.WriteOffset(program._shaderVariationOfsPos); } @@ -599,7 +676,11 @@ void SaveLocations(List locations, long ofs) writer.AlignBytes(1024); - RelocationTable.SetRelocationSection(1, (uint)pos2, (uint)(writer.BaseStream.Position - pos2)); + RelocationTable.SetRelocationSection( + 1, + (uint)pos2, + (uint)(writer.BaseStream.Position - pos2) + ); RelocationTable.Write(writer); writer.WriteHeaderBlocks(); @@ -619,7 +700,6 @@ private void SaveFileNameString(BinaryWriter writer, string name) StringTable.AddFileNameEntry(pos, name); } - class ShaderModelOffset { public long Position; diff --git a/ShaderLibrary/Util/ShaderUniformUtil.cs b/ShaderLibrary/Util/ShaderUniformUtil.cs index ea9b29b..de54baa 100644 --- a/ShaderLibrary/Util/ShaderUniformUtil.cs +++ b/ShaderLibrary/Util/ShaderUniformUtil.cs @@ -9,19 +9,31 @@ namespace ShaderLibrary { public class ShaderUniformUtil { - public static List FindUniformsVertex(string shaderCode, BfshaShaderProgram program, BfshaUniformBlock block) + public static List FindUniformsVertex( + string shaderCode, + BfshaShaderProgram program, + BfshaUniformBlock block + ) { var locationInfo = program.UniformBlockIndices[block.Index]; return FindUniforms(shaderCode, block, $"vp_c{locationInfo.FragmentLocation + 3}.data"); } - public static List FindUniformsFragment(string shaderCode, BfshaShaderProgram program, BfshaUniformBlock block) + public static List FindUniformsFragment( + string shaderCode, + BfshaShaderProgram program, + BfshaUniformBlock block + ) { var locationInfo = program.UniformBlockIndices[block.Index]; return FindUniforms(shaderCode, block, $"fp_c{locationInfo.FragmentLocation + 3}.data"); } - public static List FindUniforms(string shaderCode, BfshaUniformBlock block, string blockName) + public static List FindUniforms( + string shaderCode, + BfshaUniformBlock block, + string blockName + ) { string swizzle = "x"; @@ -64,9 +76,12 @@ public static List FindUniforms(string shaderCode, BfshaUniformBlock blo static string SwizzleShift(string swizzle) { - if (swizzle == "x") return "y"; - if (swizzle == "y") return "z"; - if (swizzle == "z") return "w"; + if (swizzle == "x") + return "y"; + if (swizzle == "y") + return "z"; + if (swizzle == "z") + return "w"; return "x"; } } diff --git a/ShaderLibrary/Util/SubStream.cs b/ShaderLibrary/Util/SubStream.cs index 7ac67ee..f3b4319 100644 --- a/ShaderLibrary/Util/SubStream.cs +++ b/ShaderLibrary/Util/SubStream.cs @@ -15,13 +15,19 @@ public class SubStream : Stream Stream baseStream; readonly long length; readonly long baseOffset; + public SubStream(Stream baseStream, long offset, long length) { - if (baseStream == null) throw new ArgumentNullException("baseStream"); - if (!baseStream.CanRead) throw new ArgumentException("baseStream.CanRead is false"); - if (!baseStream.CanSeek) throw new ArgumentException("baseStream.CanSeek is false"); - if (offset < 0) throw new ArgumentOutOfRangeException("offset"); - if (offset + length > baseStream.Length) throw new ArgumentOutOfRangeException("length"); + if (baseStream == null) + throw new ArgumentNullException("baseStream"); + if (!baseStream.CanRead) + throw new ArgumentException("baseStream.CanRead is false"); + if (!baseStream.CanSeek) + throw new ArgumentException("baseStream.CanSeek is false"); + if (offset < 0) + throw new ArgumentOutOfRangeException("offset"); + if (offset + length > baseStream.Length) + throw new ArgumentOutOfRangeException("length"); this.baseStream = baseStream; this.length = length; @@ -32,11 +38,16 @@ public SubStream(Stream baseStream, long offset) { long length = baseStream.Length - offset; - if (baseStream == null) throw new ArgumentNullException("baseStream"); - if (!baseStream.CanRead) throw new ArgumentException("baseStream.CanRead is false"); - if (!baseStream.CanSeek) throw new ArgumentException("baseStream.CanSeek is false"); - if (offset < 0) throw new ArgumentOutOfRangeException("offset"); - if (offset + length > baseStream.Length) throw new ArgumentOutOfRangeException("length"); + if (baseStream == null) + throw new ArgumentNullException("baseStream"); + if (!baseStream.CanRead) + throw new ArgumentException("baseStream.CanRead is false"); + if (!baseStream.CanSeek) + throw new ArgumentException("baseStream.CanSeek is false"); + if (offset < 0) + throw new ArgumentOutOfRangeException("offset"); + if (offset + length > baseStream.Length) + throw new ArgumentOutOfRangeException("length"); this.baseStream = baseStream; this.length = length; @@ -56,15 +67,19 @@ public override int Read(byte[] buffer, int offset, int count) public override bool CanWrite => false; public override bool CanSeek => true; public override long Position { get; set; } + public override void Flush() => baseStream.Flush(); public override long Seek(long offset, SeekOrigin origin) { switch (origin) { - case SeekOrigin.Begin: return Position = offset; - case SeekOrigin.Current: return Position += offset; - case SeekOrigin.End: return Position = length + offset; + case SeekOrigin.Begin: + return Position = offset; + case SeekOrigin.Current: + return Position += offset; + case SeekOrigin.End: + return Position = length + offset; } throw new ArgumentException("origin is invalid"); } @@ -73,9 +88,10 @@ public override void SetLength(long value) { throw new NotSupportedException(); } + public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } } -} \ No newline at end of file +} diff --git a/ShaderLibrary/Util/Utils.cs b/ShaderLibrary/Util/Utils.cs index 42382d8..56e60dd 100644 --- a/ShaderLibrary/Util/Utils.cs +++ b/ShaderLibrary/Util/Utils.cs @@ -1,10 +1,10 @@ -using ShaderLibrary.IO; -using System; +using System; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Xml.Linq; +using ShaderLibrary.IO; namespace ShaderLibrary { @@ -27,13 +27,18 @@ public readonly void Dispose() internal static class Utils { - public static Span AsSpan(ref T val) where T : unmanaged + public static Span AsSpan(ref T val) + where T : unmanaged { Span valSpan = MemoryMarshal.CreateSpan(ref val, 1); return MemoryMarshal.Cast(valSpan); } - public static unsafe T BytesToStruct(this byte[] buffer, bool isBigEndian = false, int offset = 0) + public static unsafe T BytesToStruct( + this byte[] buffer, + bool isBigEndian = false, + int offset = 0 + ) { AdjustBigEndianByteOrder(typeof(T), buffer, isBigEndian); @@ -54,17 +59,28 @@ public static unsafe byte[] StructToBytes(this T item, bool isBigEndian) } //Adjust byte order for big endian - private static void AdjustBigEndianByteOrder(Type type, byte[] buffer, bool isBigEndian, int startOffset = 0) + private static void AdjustBigEndianByteOrder( + Type type, + byte[] buffer, + bool isBigEndian, + int startOffset = 0 + ) { if (!isBigEndian) return; if (type.IsPrimitive) { - if (type == typeof(short) || type == typeof(ushort) || - type == typeof(int) || type == typeof(uint) || - type == typeof(long) || type == typeof(ulong) || - type == typeof(double) || type == typeof(float)) + if ( + type == typeof(short) + || type == typeof(ushort) + || type == typeof(int) + || type == typeof(uint) + || type == typeof(long) + || type == typeof(ulong) + || type == typeof(double) + || type == typeof(float) + ) { Array.Reverse(buffer); return; @@ -76,7 +92,8 @@ private static void AdjustBigEndianByteOrder(Type type, byte[] buffer, bool isBi var fieldType = field.FieldType; // Ignore static fields - if (field.IsStatic) continue; + if (field.IsStatic) + continue; if (fieldType.BaseType == typeof(Enum)) fieldType = fieldType.GetFields()[0].FieldType; @@ -87,17 +104,25 @@ private static void AdjustBigEndianByteOrder(Type type, byte[] buffer, bool isBi fieldType = Enum.GetUnderlyingType(fieldType); // Check for sub-fields to recurse if necessary - var subFields = fieldType.GetFields().Where(subField => subField.IsStatic == false).ToArray(); + var subFields = fieldType + .GetFields() + .Where(subField => subField.IsStatic == false) + .ToArray(); var effectiveOffset = startOffset + offset; - if (fieldType == typeof(short) || fieldType == typeof(ushort) || - fieldType == typeof(int) || fieldType == typeof(uint) || - fieldType == typeof(long) || fieldType == typeof(ulong) || - fieldType == typeof(double) || fieldType == typeof(float)) + if ( + fieldType == typeof(short) + || fieldType == typeof(ushort) + || fieldType == typeof(int) + || fieldType == typeof(uint) + || fieldType == typeof(long) + || fieldType == typeof(ulong) + || fieldType == typeof(double) + || fieldType == typeof(float) + ) { if (subFields.Length == 0) Array.Reverse(buffer, effectiveOffset, Marshal.SizeOf(fieldType)); - } if (subFields.Length > 0) @@ -105,7 +130,11 @@ private static void AdjustBigEndianByteOrder(Type type, byte[] buffer, bool isBi } } - public static TemporarySeekHandle TemporarySeek(this Stream stream, long offset, SeekOrigin origin) + public static TemporarySeekHandle TemporarySeek( + this Stream stream, + long offset, + SeekOrigin origin + ) { long ret = stream.Position; stream.Seek(offset, origin); diff --git a/ShaderLibrary/WiiU/BfshaGX2Shader.cs b/ShaderLibrary/WiiU/BfshaGX2Shader.cs index 4a20034..7870997 100644 --- a/ShaderLibrary/WiiU/BfshaGX2Shader.cs +++ b/ShaderLibrary/WiiU/BfshaGX2Shader.cs @@ -1,9 +1,9 @@ -using ShaderLibrary; -using ShaderLibrary.IO; -using ShaderLibrary.WiiU; -using System; +using System; using System.Collections.Generic; using System.IO; +using ShaderLibrary; +using ShaderLibrary.IO; +using ShaderLibrary.WiiU; namespace BfshaLibrary.WiiU { @@ -39,6 +39,7 @@ public byte[] ToArray() } return mem.ToArray(); } + public BfshaGX2VertexHeader() { } public BfshaGX2VertexHeader(BinaryDataReader reader) @@ -69,6 +70,7 @@ public BfshaGX2VertexHeader(BinaryDataReader reader) } internal long _ofs_pos; + public void Write(BinaryDataWriter writer) { UnusedHeader[7] = (uint)this.Loops.Count; @@ -162,6 +164,7 @@ public BfshaGX2GeometryHeader(BinaryDataReader reader) } private long _ofs_pos; + public void Write(BinaryDataWriter writer) { UnusedHeader[7] = (uint)this.Loops.Count; diff --git a/ShaderLibrary/WiiU/BfshaGX2ShaderImporter.cs b/ShaderLibrary/WiiU/BfshaGX2ShaderImporter.cs index 35bafa9..c0825e2 100644 --- a/ShaderLibrary/WiiU/BfshaGX2ShaderImporter.cs +++ b/ShaderLibrary/WiiU/BfshaGX2ShaderImporter.cs @@ -8,8 +8,12 @@ namespace ShaderLibrary.WiiU { public class BfshaGX2ShaderImporter { - public static void Import(ShaderModel shadermodel, BfshaShaderProgram program, - GSHFile.GX2Shader shader, IntermediateShader.ShaderModelInfo intermediate) + public static void Import( + ShaderModel shadermodel, + BfshaShaderProgram program, + GSHFile.GX2Shader shader, + IntermediateShader.ShaderModelInfo intermediate + ) { program.GX2VertexData = new BfshaLibrary.WiiU.BfshaGX2VertexHeader(); program.GX2PixelData = new BfshaLibrary.WiiU.BfshaGX2PixelHeader(); @@ -29,17 +33,21 @@ public static void Import(ShaderModel shadermodel, BfshaShaderProgram program, program.GX2PixelData.Loops = shader.PixelHeader.Loops; SetLocations(shadermodel, program, shader.PixelHeader, intermediate); -/* - program.GX2GeometryData.Data = shader.GeometryShData; - program.GX2GeometryData.Regs = shader.GeometryHeader.GetRegs(); - program.GX2GeometryData.Mode = shader.GeometryHeader.Mode; - program.GX2GeometryData.Loops = shader.GeometryHeader.Loops; - - SetLocations(shadermodel, program, shader.GeometryHeader, intermediate);*/ + /* + program.GX2GeometryData.Data = shader.GeometryShData; + program.GX2GeometryData.Regs = shader.GeometryHeader.GetRegs(); + program.GX2GeometryData.Mode = shader.GeometryHeader.Mode; + program.GX2GeometryData.Loops = shader.GeometryHeader.Loops; + + SetLocations(shadermodel, program, shader.GeometryHeader, intermediate);*/ } - static void SetLocations(ShaderModel shadermodel, BfshaShaderProgram program, - GSHFile.GX2VertexHeader shader, IntermediateShader.ShaderModelInfo intermediate) + static void SetLocations( + ShaderModel shadermodel, + BfshaShaderProgram program, + GSHFile.GX2VertexHeader shader, + IntermediateShader.ShaderModelInfo intermediate + ) { foreach (var uniformBlock in shader.UniformBlocks) { @@ -47,7 +55,9 @@ static void SetLocations(ShaderModel shadermodel, BfshaShaderProgram program, if (!shadermodel.UniformBlocks.ContainsKey(target)) return; - var bind_info = program.UniformBlockIndices[shadermodel.UniformBlocks[target].Index]; + var bind_info = program.UniformBlockIndices[ + shadermodel.UniformBlocks[target].Index + ]; bind_info.VertexLocation = (int)uniformBlock.Offset; } foreach (var samplerVar in shader.Samplers) @@ -70,8 +80,12 @@ static void SetLocations(ShaderModel shadermodel, BfshaShaderProgram program, } } - static void SetLocations(ShaderModel shadermodel, BfshaShaderProgram program, - GSHFile.GX2PixelHeader shader, IntermediateShader.ShaderModelInfo intermediate) + static void SetLocations( + ShaderModel shadermodel, + BfshaShaderProgram program, + GSHFile.GX2PixelHeader shader, + IntermediateShader.ShaderModelInfo intermediate + ) { foreach (var uniformBlock in shader.UniformBlocks) { @@ -79,7 +93,9 @@ static void SetLocations(ShaderModel shadermodel, BfshaShaderProgram program, if (!shadermodel.UniformBlocks.ContainsKey(target)) return; - var bind_info = program.UniformBlockIndices[shadermodel.UniformBlocks[target].Index]; + var bind_info = program.UniformBlockIndices[ + shadermodel.UniformBlocks[target].Index + ]; bind_info.FragmentLocation = (int)uniformBlock.Offset; } foreach (var samplerVar in shader.Samplers) @@ -92,8 +108,13 @@ static void SetLocations(ShaderModel shadermodel, BfshaShaderProgram program, bind_info.FragmentLocation = (int)samplerVar.Location; } } - static void SetLocations(ShaderModel shadermodel, BfshaShaderProgram program, - GSHFile.GX2GeometryShaderHeader shader, IntermediateShader.ShaderModelInfo intermediate) + + static void SetLocations( + ShaderModel shadermodel, + BfshaShaderProgram program, + GSHFile.GX2GeometryShaderHeader shader, + IntermediateShader.ShaderModelInfo intermediate + ) { foreach (var uniformBlock in shader.UniformBlocks) { @@ -101,7 +122,9 @@ static void SetLocations(ShaderModel shadermodel, BfshaShaderProgram program, if (!shadermodel.UniformBlocks.ContainsKey(target)) return; - var bind_info = program.UniformBlockIndices[shadermodel.UniformBlocks[target].Index]; + var bind_info = program.UniformBlockIndices[ + shadermodel.UniformBlocks[target].Index + ]; bind_info.GeoemetryLocation = (int)uniformBlock.Offset; } foreach (var samplerVar in shader.Samplers) @@ -115,4 +138,4 @@ static void SetLocations(ShaderModel shadermodel, BfshaShaderProgram program, } } } -} \ No newline at end of file +} diff --git a/ShaderLibrary/WiiU/BfshaLoaderWiiU.cs b/ShaderLibrary/WiiU/BfshaLoaderWiiU.cs index cdea980..eca0af3 100644 --- a/ShaderLibrary/WiiU/BfshaLoaderWiiU.cs +++ b/ShaderLibrary/WiiU/BfshaLoaderWiiU.cs @@ -1,6 +1,4 @@ -using Microsoft.VisualBasic.FileIO; -using ShaderLibrary.IO; -using System; +using System; using System.Collections.Generic; using System.Drawing; using System.IO; @@ -9,6 +7,8 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +using Microsoft.VisualBasic.FileIO; +using ShaderLibrary.IO; namespace ShaderLibrary.WiiU { @@ -107,7 +107,7 @@ static ShaderModel ReadShaderModel(BinaryDataReader reader) shaderModel.Name = reader.LoadString(nameOffset); - if (staticOptionsDictOffset != 0) + if (staticOptionsDictOffset != 0) { reader.SeekBegin(staticOptionsDictOffset); shaderModel.StaticOptions = ReadDictionary(reader, ReadShaderOption); @@ -135,7 +135,9 @@ static ShaderModel ReadShaderModel(BinaryDataReader reader) if (keyTableOffset != 0) { reader.SeekBegin(keyTableOffset); - shaderModel.KeyTable = reader.ReadInt32s((shaderModel.StaticKeyLength + shaderModel.DynamicKeyLength) * programCount); + shaderModel.KeyTable = reader.ReadInt32s( + (shaderModel.StaticKeyLength + shaderModel.DynamicKeyLength) * programCount + ); } reader.SeekBegin(shaderProgramsOffset); @@ -174,10 +176,7 @@ static ShaderOption ReadShaderOption(BinaryDataReader reader) static ResUint32 ReadOptionValue(BinaryReader reader) { - return new ResUint32() - { - Value = reader.ReadUInt32(), - }; + return new ResUint32() { Value = reader.ReadUInt32() }; } static BfshaSampler ReadShaderSampler(BinaryDataReader reader) @@ -188,7 +187,7 @@ static BfshaSampler ReadShaderSampler(BinaryDataReader reader) sampler.GX2Count = reader.ReadByte(); reader.ReadSByte(); //padding reader.ReadUInt32(); - // sampler.Annotation = reader.LoadString((uint)reader.ReadOffset()); + // sampler.Annotation = reader.LoadString((uint)reader.ReadOffset()); Console.WriteLine($"BfshaSampler {sampler.Index}"); @@ -222,14 +221,17 @@ static BfshaUniformBlock ReadShaderUniformBlock(BinaryDataReader reader) if (uniformOffset != 0) { - using (reader.BaseStream.TemporarySeek(uniformOffset, SeekOrigin.Begin)) { + using (reader.BaseStream.TemporarySeek(uniformOffset, SeekOrigin.Begin)) + { block.Uniforms = ReadDictionary(reader, ReadShaderUniform); } } if (dataOffset != 0) { - block.DefaultBuffer = reader.ReadCustom(() => - reader.ReadBytes(block.Size), (uint)dataOffset); + block.DefaultBuffer = reader.ReadCustom( + () => reader.ReadBytes(block.Size), + (uint)dataOffset + ); } Console.WriteLine($"BfshaUniformBlock {block.Index}"); @@ -283,10 +285,10 @@ static BfshaShaderProgram ReadShaderProgram(BinaryDataReader reader) //Rather than GX2 shader headers, this version has unsupported headers that work differently //Unsure how to support these atm - // throw new Exception($"Version {reader.Header.VersionMajor} not supported!"); + // throw new Exception($"Version {reader.Header.VersionMajor} not supported!"); - // program.SamplerIndices = ReadLocationList(reader, samplerLocationsOffset, samplerCount); - // program.UniformBlockIndices = ReadLocationList(reader, uniformBlockLocationsOffset, blockCount); + // program.SamplerIndices = ReadLocationList(reader, samplerLocationsOffset, samplerCount); + // program.UniformBlockIndices = ReadLocationList(reader, uniformBlockLocationsOffset, blockCount); } else { @@ -302,47 +304,75 @@ static BfshaShaderProgram ReadShaderProgram(BinaryDataReader reader) var parentModelOffset = reader.ReadOffset(); - program.GX2VertexData = reader.ReadCustom(() => - { - return new BfshaLibrary.WiiU.BfshaGX2VertexHeader(reader); - }, (uint)GX2VertexDataOffset); - - program.GX2GeometryData = reader.ReadCustom(() => - { - return new BfshaLibrary.WiiU.BfshaGX2GeometryHeader(reader); - }, (uint)GX2GeometryDataOffset); + program.GX2VertexData = reader.ReadCustom( + () => + { + return new BfshaLibrary.WiiU.BfshaGX2VertexHeader(reader); + }, + (uint)GX2VertexDataOffset + ); - program.GX2PixelData = reader.ReadCustom(() => - { - return new BfshaLibrary.WiiU.BfshaGX2PixelHeader(reader); - }, (uint)GX2FragmentDataOffset); + program.GX2GeometryData = reader.ReadCustom( + () => + { + return new BfshaLibrary.WiiU.BfshaGX2GeometryHeader(reader); + }, + (uint)GX2GeometryDataOffset + ); - program.SamplerIndices = ReadLocationList(reader, samplerLocationsOffset, samplerCount); - program.UniformBlockIndices = ReadLocationList(reader, uniformBlockLocationsOffset, blockCount); + program.GX2PixelData = reader.ReadCustom( + () => + { + return new BfshaLibrary.WiiU.BfshaGX2PixelHeader(reader); + }, + (uint)GX2FragmentDataOffset + ); + + program.SamplerIndices = ReadLocationList( + reader, + samplerLocationsOffset, + samplerCount + ); + program.UniformBlockIndices = ReadLocationList( + reader, + uniformBlockLocationsOffset, + blockCount + ); } return program; } - static List ReadLocationList(BinaryDataReader reader, long offset, byte count) + static List ReadLocationList( + BinaryDataReader reader, + long offset, + byte count + ) { List values = new List(); using (reader.BaseStream.TemporarySeek(offset, SeekOrigin.Begin)) { for (int i = 0; i < count; i++) { - values.Add(new ShaderIndexHeader() - { - VertexLocation = reader.ReadSByte(), - GeoemetryLocation = reader.ReadSByte(), - FragmentLocation = reader.ReadSByte(), - }); + values.Add( + new ShaderIndexHeader() + { + VertexLocation = reader.ReadSByte(), + GeoemetryLocation = reader.ReadSByte(), + FragmentLocation = reader.ReadSByte(), + } + ); if (reader.Header.VersionMajor >= 4) //extra shader values[i].ComputeLocation = reader.ReadSByte(); } - }return values; + } + return values; } - static ResDict ReadDictionary(BinaryDataReader reader, Func load_section) where T : class, IResData, new() + static ResDict ReadDictionary( + BinaryDataReader reader, + Func load_section + ) + where T : class, IResData, new() { ResDict dict = new ResDict(); @@ -356,13 +386,15 @@ static List ReadLocationList(BinaryDataReader reader, long of var Key = reader.LoadString((uint)reader.ReadOffset()); var ValueOffset = reader.ReadOffset(); - dict._nodes.Add(new ResDict.Node() - { - Reference = refe, - IdxLeft = IdxLeft, - IdxRight = IdxRight, - Key = Key, - }); + dict._nodes.Add( + new ResDict.Node() + { + Reference = refe, + IdxLeft = IdxLeft, + IdxRight = IdxRight, + Key = Key, + } + ); if (i == 0) continue; @@ -380,7 +412,6 @@ static List ReadLocationList(BinaryDataReader reader, long of return dict; } - [StructLayout(LayoutKind.Sequential, Size = 0x10)] public struct BfshaHeader { @@ -410,8 +441,6 @@ public struct BfshaHeader public uint Unknown2; } - - [StructLayout(LayoutKind.Sequential, Size = 0x10)] public struct ShaderModelHeader { diff --git a/ShaderLibrary/WiiU/BfshaSaverWiiU.cs b/ShaderLibrary/WiiU/BfshaSaverWiiU.cs index 099ba3a..b25fe9a 100644 --- a/ShaderLibrary/WiiU/BfshaSaverWiiU.cs +++ b/ShaderLibrary/WiiU/BfshaSaverWiiU.cs @@ -1,12 +1,12 @@ -using BfshaLibrary.WiiU; -using Microsoft.VisualBasic.FileIO; -using ShaderLibrary.IO; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection.PortableExecutable; using System.Text; using System.Threading.Tasks; +using BfshaLibrary.WiiU; +using Microsoft.VisualBasic.FileIO; +using ShaderLibrary.IO; namespace ShaderLibrary.WiiU { @@ -70,9 +70,13 @@ public void Save(BfshaFile bfsha, BinaryDataWriter writer) foreach (var op in shaderModel.DynamicOptions.Values) WriteShaderOption(writer, op); - foreach (var c in shaderModel.StaticOptions.Values.SelectMany(x => x.Choices.Values)) + foreach ( + var c in shaderModel.StaticOptions.Values.SelectMany(x => x.Choices.Values) + ) WriteOptionValue(writer, c); - foreach (var c in shaderModel.DynamicOptions.Values.SelectMany(x => x.Choices.Values)) + foreach ( + var c in shaderModel.DynamicOptions.Values.SelectMany(x => x.Choices.Values) + ) WriteOptionValue(writer, c); foreach (var attr in shaderModel.Attributes.Values) @@ -81,12 +85,16 @@ public void Save(BfshaFile bfsha, BinaryDataWriter writer) WriteShaderSampler(writer, sampler); foreach (var block in shaderModel.UniformBlocks.Values) WriteShaderUniformBlock(writer, block); - foreach (var uniform in shaderModel.UniformBlocks.Values.SelectMany(x => x.Uniforms.Values)) + foreach ( + var uniform in shaderModel.UniformBlocks.Values.SelectMany(x => + x.Uniforms.Values + ) + ) WriteShaderUniform(writer, uniform); SaveReference(writer, shaderModel.Programs); foreach (var prog in shaderModel.Programs) - WriteShaderProgram(writer, shaderModel, prog); + WriteShaderProgram(writer, shaderModel, prog); SaveReference(writer, shaderModel.KeyTable); for (int i = 0; i < shaderModel.KeyTable.Length; i++) @@ -94,7 +102,7 @@ public void Save(BfshaFile bfsha, BinaryDataWriter writer) foreach (var block in shaderModel.UniformBlocks.Values) { - if (block.DefaultBuffer != null) + if (block.DefaultBuffer != null) { SaveReference(writer, block.DefaultBuffer); writer.Write(block.DefaultBuffer); @@ -107,7 +115,9 @@ public void Save(BfshaFile bfsha, BinaryDataWriter writer) WriteDictionary(writer, shaderModel.Attributes); WriteDictionary(writer, shaderModel.Samplers); WriteDictionary(writer, shaderModel.UniformBlocks); - foreach (var block in shaderModel.UniformBlocks.Values.Where(x => x.Uniforms.Count > 0)) + foreach ( + var block in shaderModel.UniformBlocks.Values.Where(x => x.Uniforms.Count > 0) + ) WriteDictionary(writer, block.Uniforms); foreach (var op in shaderModel.StaticOptions.Values) @@ -115,11 +125,13 @@ public void Save(BfshaFile bfsha, BinaryDataWriter writer) foreach (var op in shaderModel.DynamicOptions.Values) WriteDictionary(writer, op.Choices); - Dictionary gx2HeaderLoopOffsets = new Dictionary(); + Dictionary gx2HeaderLoopOffsets = + new Dictionary(); void WriteShaderHeader(BfshaGX2Header header) { - if (header == null) return; + if (header == null) + return; writer.AlignBytes(4); var pos = writer.BaseStream.Position; @@ -180,7 +192,8 @@ void WriteShaderHeader(BfshaGX2Header header) writer.AlignBytes((int)bfsha.DataAlignment); //file size - using (writer.BaseStream.TemporarySeek(_fileSizePos, SeekOrigin.Begin)) { + using (writer.BaseStream.TemporarySeek(_fileSizePos, SeekOrigin.Begin)) + { writer.Write((uint)writer.BaseStream.Length); } } @@ -232,7 +245,8 @@ private void WriteShaderModel(BinaryDataWriter writer, ShaderModel shaderModel) writer.Write(0); writer.Write(0); - void WriteDictValueOffsets(ResDict value) where T : IResData, new() + void WriteDictValueOffsets(ResDict value) + where T : IResData, new() { SaveDataPointer(writer, value.Values.FirstOrDefault()); //value offset SaveDataPointer(writer, value); //dict offset @@ -244,8 +258,12 @@ private void WriteShaderModel(BinaryDataWriter writer, ShaderModel shaderModel) WriteDictValueOffsets(shaderModel.Samplers); WriteDictValueOffsets(shaderModel.UniformBlocks); - SaveDataPointer(writer, shaderModel.UniformBlocks.Values.FirstOrDefault(x - => x.Uniforms?.Count > 0).Uniforms.Values.FirstOrDefault()); //uniform start offset + SaveDataPointer( + writer, + shaderModel + .UniformBlocks.Values.FirstOrDefault(x => x.Uniforms?.Count > 0) + .Uniforms.Values.FirstOrDefault() + ); //uniform start offset SaveDataPointer(writer, shaderModel.Programs); //Program offset SaveDataPointer(writer, shaderModel.KeyTable); //KeyTable offset @@ -333,7 +351,11 @@ private void WriteShaderAttribute(BinaryDataWriter writer, BfshaAttribute attr) writer.Write((byte)attr.Location); } - private void WriteShaderProgram(BinaryDataWriter writer, ShaderModel shaderModel, BfshaShaderProgram program) + private void WriteShaderProgram( + BinaryDataWriter writer, + ShaderModel shaderModel, + BfshaShaderProgram program + ) { if (writer.Header.VersionMajor >= 4) { @@ -361,7 +383,8 @@ private void WriteShaderProgram(BinaryDataWriter writer, ShaderModel shaderModel } } - private void WriteDictionary(BinaryDataWriter writer, ResDict dict) where T : IResData, new() + private void WriteDictionary(BinaryDataWriter writer, ResDict dict) + where T : IResData, new() { if (dict.Count == 0) return; @@ -377,7 +400,7 @@ private void WriteShaderProgram(BinaryDataWriter writer, ShaderModel shaderModel writer.Write(nodes.Count * 16 + 8); //size writer.Write(dict.Count); //count - for (int i = 0; i < nodes.Count; i++) + for (int i = 0; i < nodes.Count; i++) { writer.Write(nodes[i].Reference); writer.Write(nodes[i].IdxLeft); @@ -402,7 +425,7 @@ private void SaveStringTable(BinaryDataWriter writer) long table_start = writer.Position; writer.WriteOffset(_stringPoolOfsPos + 4); - // var ordered = _savedStrings.OrderBy(x => x.Key).ToList(); + // var ordered = _savedStrings.OrderBy(x => x.Key).ToList(); foreach (var str in _savedStrings) { @@ -418,12 +441,14 @@ private void SaveStringTable(BinaryDataWriter writer) long table_size = writer.Position - table_start; - using (writer.BaseStream.TemporarySeek(_stringPoolOfsPos, SeekOrigin.Begin)) { + using (writer.BaseStream.TemporarySeek(_stringPoolOfsPos, SeekOrigin.Begin)) + { writer.Write((uint)table_size); } } - private void SaveDataPointer(BinaryDataWriter writer, ResDict value) where T : IResData, new() + private void SaveDataPointer(BinaryDataWriter writer, ResDict value) + where T : IResData, new() { var pos = writer.Position; writer.Write(0U); @@ -474,7 +499,7 @@ private void SaveReference(BinaryDataWriter writer, object value) _savedRefs.Add(value, writer.Position); } - private void SaveString( BinaryDataWriter writer, string value) + private void SaveString(BinaryDataWriter writer, string value) { var pos = writer.Position; writer.Write(0U); @@ -497,6 +522,7 @@ public bool Equals(byte[] left, byte[] right) } return left.SequenceEqual(right); } + public int GetHashCode(byte[] key) { if (key == null) diff --git a/ShaderLibrary/WiiU/GSHCompile.cs b/ShaderLibrary/WiiU/GSHCompile.cs index 50360ef..f8a8380 100644 --- a/ShaderLibrary/WiiU/GSHCompile.cs +++ b/ShaderLibrary/WiiU/GSHCompile.cs @@ -12,23 +12,21 @@ public class GSHCompile public static string GSH_PATH = "gshCompile.exe"; public static string OUTPUT_PATH = "temp.gsh"; - public GSHCompile() - { - - } + public GSHCompile() { } public static bool IsValid() { return File.Exists(GSH_PATH); } - public static byte[] CompileStages(string vertex, string fragment, string geometry) + public static byte[] CompileStages(string vertex, string fragment, string geometry) { string vsh_path = "temp.vert"; string fsh_path = "temp.frag"; string gsh_path = "temp.geom"; - if (File.Exists(OUTPUT_PATH)) File.Delete(OUTPUT_PATH); + if (File.Exists(OUTPUT_PATH)) + File.Delete(OUTPUT_PATH); //save shader File.WriteAllText(vsh_path, vertex); @@ -38,12 +36,18 @@ public static byte[] CompileStages(string vertex, string fragment, string geomet string cmd = $""; // Shader stages - if (!string.IsNullOrEmpty(vertex)) cmd += $" -v {vsh_path}"; - if (!string.IsNullOrEmpty(fragment)) cmd += $" -p {fsh_path}"; - if (!string.IsNullOrEmpty(geometry)) cmd += $" -g {gsh_path}"; + if (!string.IsNullOrEmpty(vertex)) + cmd += $" -v {vsh_path}"; + if (!string.IsNullOrEmpty(fragment)) + cmd += $" -p {fsh_path}"; + if (!string.IsNullOrEmpty(geometry)) + cmd += $" -g {gsh_path}"; // Exec(GSH_PATH, $"-v {vsh_path} -p {fsh_path} -o {OUTPUT_PATH} -force_uniformblock -no_limit_array_syms -nospark -O"); - Exec(GSH_PATH, $"{cmd} -o {OUTPUT_PATH} -force_uniformblock -no_limit_array_syms -nospark -O"); + Exec( + GSH_PATH, + $"{cmd} -o {OUTPUT_PATH} -force_uniformblock -no_limit_array_syms -nospark -O" + ); if (File.Exists(OUTPUT_PATH)) { @@ -56,10 +60,14 @@ static string GetTypeArg(GSHShaderType type) { switch (type) { - case GSHShaderType.Vertex: return "-v"; - case GSHShaderType.Pixel: return "-p"; - case GSHShaderType.Geometry: return "-g"; - case GSHShaderType.Compute: return "-c"; + case GSHShaderType.Vertex: + return "-v"; + case GSHShaderType.Pixel: + return "-p"; + case GSHShaderType.Geometry: + return "-g"; + case GSHShaderType.Compute: + return "-c"; default: throw new ArgumentException($"Invalid type argument {type}!"); } @@ -84,7 +92,7 @@ private static bool Exec(string exec, string args) RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, - CreateNoWindow = true + CreateNoWindow = true, }; Process cmd = new Process(); @@ -155,12 +163,15 @@ public static string CompileMacros(Dictionary macros, string src var macro_values = line.Split(); var macroValue = macro_values[2]; - bool isBool = macroValue.Contains("true") || macroValue.Contains("false"); + bool isBool = + macroValue.Contains("true") || macroValue.Contains("false"); if (isBool) { - if (macros[macroName] == "1") macros[macroName] = "true"; - if (macros[macroName] == "0") macros[macroName] = "false"; + if (macros[macroName] == "1") + macros[macroName] = "true"; + if (macros[macroName] == "0") + macros[macroName] = "false"; } value = value.Replace(macroValue, macros[macroName]); diff --git a/ShaderLibrary/WiiU/GSHFile.cs b/ShaderLibrary/WiiU/GSHFile.cs index 619d406..2c2ec37 100644 --- a/ShaderLibrary/WiiU/GSHFile.cs +++ b/ShaderLibrary/WiiU/GSHFile.cs @@ -1,13 +1,13 @@ -using BfshaLibrary.WiiU; -using ShaderLibrary.Common; -using ShaderLibrary.IO; -using Silk.NET.OpenGL; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +using BfshaLibrary.WiiU; +using ShaderLibrary.Common; +using ShaderLibrary.IO; +using Silk.NET.OpenGL; namespace ShaderLibrary.WiiU { @@ -38,11 +38,13 @@ public struct DataBlockHeader public List Blocks = new List(); public List Shaders = new List(); - public GSHFile(string path) { + public GSHFile(string path) + { Read(new BinaryDataReader(File.OpenRead(path), true)); } - public GSHFile(Stream stream) { + public GSHFile(Stream stream) + { Read(new BinaryDataReader(stream, true)); } @@ -79,7 +81,9 @@ private void Read(BinaryDataReader reader) if (block.BlockType == BlockType.PixelShaderHeader) currentShader.PixelHeader = new GX2PixelHeader(new MemoryStream(data)); if (block.BlockType == BlockType.GeometryShaderHeader) - currentShader.GeometryHeader = new GX2GeometryShaderHeader(new MemoryStream(data)); + currentShader.GeometryHeader = new GX2GeometryShaderHeader( + new MemoryStream(data) + ); if (block.BlockType == BlockType.VertexShaderProgram) currentShader.VertexData = data; @@ -88,11 +92,7 @@ private void Read(BinaryDataReader reader) if (block.BlockType == BlockType.GeometryShaderProgram) currentShader.GeometryShData = data; - Blocks.Add(new GX2Block() - { - Header = block, - Data = data, - }); + Blocks.Add(new GX2Block() { Header = block, Data = data }); } } @@ -134,7 +134,8 @@ public class GX2VertexHeader public byte[] GetRegs() { var mem = new MemoryStream(); - using (var writer = new BinaryDataWriter(mem, true)) { + using (var writer = new BinaryDataWriter(mem, true)) + { writer.WriteStruct(ShaderRegsHeader); } return mem.ToArray(); @@ -204,7 +205,7 @@ public void Write(BinaryDataWriter writer) writer.Write(0); //offset for later writer.Write(0); - writer.Write(0); + writer.Write(0); writer.Write(Loops.Count); writer.Write(0); //offset for later @@ -312,7 +313,7 @@ public void Write(BinaryDataWriter writer) writer.Write(0); //offset for later writer.Write(0); - writer.Write(0); + writer.Write(0); writer.Write(Loops.Count); writer.Write(0); //offset for later @@ -342,7 +343,8 @@ public class GX2PixelHeader public byte[] GetRegs() { var mem = new MemoryStream(); - using (var writer = new BinaryDataWriter(mem, true)) { + using (var writer = new BinaryDataWriter(mem, true)) + { writer.WriteStruct(ShaderRegsHeader); } return mem.ToArray(); @@ -413,12 +415,14 @@ class GX2VertexShaderStuct public uint vgt_primitiveid_en; public uint spi_vs_out_config; public uint num_spi_vs_out_id; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] public uint[] spi_vs_out_id; public uint pa_cl_vs_out_cntl; public uint sq_vtx_semantic_clear; public uint num_sq_vtx_semantic; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] public uint[] sq_vtx_semantic; @@ -435,6 +439,7 @@ class GX2PixelShaderStuct public uint spi_ps_in_control_0; public uint spi_ps_in_control_1; public uint num_spi_ps_input_cntl; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] public uint[] spi_ps_input_cntls; @@ -452,6 +457,7 @@ class GX2GeometryShaderStuct public uint spi_ps_in_control_0; public uint spi_ps_in_control_1; public uint num_spi_ps_input_cntl; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] public uint[] spi_ps_input_cntls; diff --git a/ShaderLibrary/Xml/XmlConverter.cs b/ShaderLibrary/Xml/XmlConverter.cs index 21629cd..7b294b7 100644 --- a/ShaderLibrary/Xml/XmlConverter.cs +++ b/ShaderLibrary/Xml/XmlConverter.cs @@ -22,43 +22,51 @@ public static string ToXml(BfshaFile bfsha) foreach (var op in shaderModel.StaticOptions.Values) { - xml_shader_model.static_options.Add(new shader_option() - { - Name = op.Name, - DefaultChoice = op.DefaultChoice, - Choices = op.Choices.Keys.ToList(), - }); + xml_shader_model.static_options.Add( + new shader_option() + { + Name = op.Name, + DefaultChoice = op.DefaultChoice, + Choices = op.Choices.Keys.ToList(), + } + ); } foreach (var op in shaderModel.DynamicOptions.Values) { - xml_shader_model.dynamic_options.Add(new shader_option() - { - Name = op.Name, - DefaultChoice = op.DefaultChoice, - Choices = op.Choices.Keys.ToList(), - }); + xml_shader_model.dynamic_options.Add( + new shader_option() + { + Name = op.Name, + DefaultChoice = op.DefaultChoice, + Choices = op.Choices.Keys.ToList(), + } + ); } foreach (var samp in shaderModel.Samplers) { - xml_shader_model.samplers.Add(new sampler() - { - Name = samp.Key, - alt = samp.Value.Annotation, - Index = samp.Value.Index, - Gx2Count = samp.Value.GX2Count, - Gx2Type = samp.Value.GX2Type, - }); + xml_shader_model.samplers.Add( + new sampler() + { + Name = samp.Key, + alt = samp.Value.Annotation, + Index = samp.Value.Index, + Gx2Count = samp.Value.GX2Count, + Gx2Type = samp.Value.GX2Type, + } + ); } foreach (var attr in shaderModel.Attributes) { - xml_shader_model.attributes.Add(new attribute() - { - Name = attr.Key, - Location = attr.Value.Location, - Index = attr.Value.Index, - Gx2Count = attr.Value.GX2Count, - Gx2Type = attr.Value.GX2Type, - }); + xml_shader_model.attributes.Add( + new attribute() + { + Name = attr.Key, + Location = attr.Value.Location, + Index = attr.Value.Index, + Gx2Count = attr.Value.GX2Count, + Gx2Type = attr.Value.GX2Type, + } + ); } foreach (var block in shaderModel.UniformBlocks) { @@ -66,25 +74,29 @@ public static string ToXml(BfshaFile bfsha) foreach (var un in block.Value.Uniforms) { - uniforms.Add(new uniform() - { - Index = un.Value.Index, - BlockIndex = un.Value.BlockIndex, - Name = un.Key, - Offset = un.Value.DataOffset, - Gx2Count = un.Value.GX2Count, - Gx2Type = un.Value.GX2Type, - }); + uniforms.Add( + new uniform() + { + Index = un.Value.Index, + BlockIndex = un.Value.BlockIndex, + Name = un.Key, + Offset = un.Value.DataOffset, + Gx2Count = un.Value.GX2Count, + Gx2Type = un.Value.GX2Type, + } + ); } - xml_shader_model.uniform_blocks.Add(new uniform_block() - { - Name = block.Key, - Type = (int)block.Value.Type, - Size = (int)block.Value.Size, - Index = block.Value.Index, - uniforms = uniforms, - }); + xml_shader_model.uniform_blocks.Add( + new uniform_block() + { + Name = block.Key, + Type = (int)block.Value.Type, + Size = (int)block.Value.Size, + Index = block.Value.Index, + uniforms = uniforms, + } + ); } foreach (var p in shaderModel.Programs) { @@ -92,22 +104,26 @@ public static string ToXml(BfshaFile bfsha) for (int i = 0; i < shaderModel.Samplers.Count; i++) { - xml_program.sampler_locations.Add(new bind_info() - { - Name = shaderModel.Samplers.GetKey(i), - VertexLocation = p.SamplerIndices[i].VertexLocation, - FragmentLocation = p.SamplerIndices[i].FragmentLocation, - }); + xml_program.sampler_locations.Add( + new bind_info() + { + Name = shaderModel.Samplers.GetKey(i), + VertexLocation = p.SamplerIndices[i].VertexLocation, + FragmentLocation = p.SamplerIndices[i].FragmentLocation, + } + ); } for (int i = 0; i < shaderModel.UniformBlocks.Count; i++) { - xml_program.block_locations.Add(new bind_info() - { - Name = shaderModel.UniformBlocks.GetKey(i), - VertexLocation = p.UniformBlockIndices[i].VertexLocation, - FragmentLocation = p.UniformBlockIndices[i].FragmentLocation, - }); + xml_program.block_locations.Add( + new bind_info() + { + Name = shaderModel.UniformBlocks.GetKey(i), + VertexLocation = p.UniformBlockIndices[i].VertexLocation, + FragmentLocation = p.UniformBlockIndices[i].FragmentLocation, + } + ); } for (int i = 0; i < shaderModel.Attributes.Count; i++) @@ -159,8 +175,10 @@ public class bind_info { [XmlAttribute] public string Name; + [XmlAttribute] public int VertexLocation; + [XmlAttribute] public int FragmentLocation; } @@ -181,10 +199,13 @@ public class uniform_block { [XmlAttribute] public string Name; + [XmlAttribute] public int Size; + [XmlAttribute] public int Type; + [XmlAttribute] public int Index; @@ -195,15 +216,19 @@ public class uniform { [XmlAttribute] public string Name; + [XmlAttribute] public int Index; + [XmlAttribute] public int BlockIndex; + [XmlAttribute] public int Offset; [XmlAttribute] public int Gx2Type; + [XmlAttribute] public int Gx2Count; } @@ -212,13 +237,16 @@ public class attribute { [XmlAttribute] public string Name; + [XmlAttribute] public int Index; + [XmlAttribute] public int Location; [XmlAttribute] public int Gx2Type; + [XmlAttribute] public int Gx2Count; } @@ -227,13 +255,16 @@ public class sampler { [XmlAttribute] public string Name; + [XmlAttribute] public int Index; + [XmlAttribute] public string alt; [XmlAttribute] public int Gx2Type; + [XmlAttribute] public int Gx2Count; } diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..fe90317 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1783522502, + "narHash": "sha256-iffAls3iaNTyJC2faYcUXSI+Gp02cDjYl+MygxKl2GI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0bb7ec54c8483066ec9d7720e780a5caa71f8612", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..75e10f2 --- /dev/null +++ b/flake.nix @@ -0,0 +1,80 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = + { self, nixpkgs }: + let + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f (pkgsFor system)); + pkgsFor = + system: + import nixpkgs { + inherit system; + config.allowUnfree = true; # prebuilt .net is unfree + }; + + sdkFor = pkgs: pkgs.dotnetCorePackages.sdk_10_0; + in + { + devShells = forAllSystems ( + pkgs: + let + dotnet = sdkFor pkgs; + runtimeLibs = with pkgs; [ + libglvnd + libGL + libx11 + libxi + libxrandr + libxcursor + libxext + libxinerama + libxkbcommon + icu + fontconfig + freetype + zlib + openssl + stdenv.cc.cc.lib + ]; + in + { + default = pkgs.mkShell { + name = "hoianviewer-dotnet10"; + + # ffmpeg for export, zenity for tinyfiledialogs' file pickers + packages = [ + dotnet + pkgs.ffmpeg + pkgs.zenity + ]; + + env = { + DOTNET_ROOT = "${dotnet}"; + DOTNET_CLI_TELEMETRY_OPTOUT = "1"; + DOTNET_NOLOGO = "1"; + }; + + shellHook = '' + export NUGET_PACKAGES="$PWD/.nuget/packages" + export LD_LIBRARY_PATH="/run/opengl-driver/lib:${pkgs.lib.makeLibraryPath runtimeLibs}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + launch() { + dotnet run --project PlayerViewer -c "''${CONFIG:-Debug}" "$@" + } + launch_tracy() { + DOTNET_PerfMapEnabled=1 DOTNET_EnableWriteXorExecute=0 \ + dotnet run --project PlayerViewer -c "''${CONFIG:-Release}" --property:Tracy=true "$@" + } + ''; + }; + } + ); + + formatter = forAllSystems (pkgs: pkgs.nixpkgs-fmt); + }; +} diff --git a/tools/TracyWeave.cs b/tools/TracyWeave.cs new file mode 100644 index 0000000..3e4b022 --- /dev/null +++ b/tools/TracyWeave.cs @@ -0,0 +1,80 @@ +using System; +using System.Threading; +using bottlenoselabs.C2CS.Runtime; +using Tracy; + +// Runtime target of the woven instrumentation. Compiled directly into PlayerViewer under +// -p:Tracy=true (see PlayerViewer.csproj) and called by the IL the weaver injects. Marked +// [NoProfile] so the weaver — which also processes PlayerViewer.dll — never wraps these methods +// (that would recurse: a zone around Begin calls Begin...). +namespace TracyWeaver.Runtime +{ + /// + /// Each eligible method gets ctx = TracyWeave.Begin("Type.Method") at entry and + /// TracyWeave.End(ctx) in an injected finally; the configured frame method + /// also gets . Threads name themselves on their first zone. + /// + [NoProfile] + public static class TracyWeave + { + /// Opaque zone handle passed from to . + public readonly struct Ctx + { + internal readonly PInvoke.TracyCZoneCtx C; + + internal Ctx(PInvoke.TracyCZoneCtx c) + { + C = c; + } + } + + [ThreadStatic] + static bool _named; + + static void NameThread() + { + var t = Thread.CurrentThread; + string name = string.IsNullOrEmpty(t.Name) ? "Thread " + t.ManagedThreadId : t.Name; + //Tracy keeps this pointer for the process lifetime, so it is intentionally not freed. + PInvoke.TracySetThreadName(CString.FromString(name)); + } + + /// Opens a zone named and returns its handle. + public static Ctx Begin(string zone) + { + if (!_named) + { + _named = true; + NameThread(); + } + //AllocSrcloc copies the strings, so the temporary can be freed right after. + var s = CString.FromString(zone); + ulong srcloc = PInvoke.TracyAllocSrcloc( + 0, + s, + (ulong)zone.Length, + s, + (ulong)zone.Length, + 0 + ); + var ctx = PInvoke.TracyEmitZoneBeginAlloc(srcloc, 1); + s.Dispose(); + return new Ctx(ctx); + } + + /// Closes a zone opened by . + public static void End(Ctx ctx) => PInvoke.TracyEmitZoneEnd(ctx.C); + + /// Marks a frame boundary (Tracy's default frame timeline). + public static void FrameMark() => PInvoke.TracyEmitFrameMark(default); + } + + /// Apply to a method or type to exclude it (and its members) from weaving. + [AttributeUsage( + AttributeTargets.Method + | AttributeTargets.Constructor + | AttributeTargets.Class + | AttributeTargets.Struct + )] + public sealed class NoProfileAttribute : Attribute { } +} diff --git a/tools/TracyWeaver.cs b/tools/TracyWeaver.cs new file mode 100644 index 0000000..5691e48 --- /dev/null +++ b/tools/TracyWeaver.cs @@ -0,0 +1,347 @@ +#:package Mono.Cecil@0.11.6 +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; + +// TracyWeaver — .NET 10 file-based build tool. Run straight from source (no project): +// +// dotnet run TracyWeaver.cs -- --dir [--frame-mark Type::Method] Asm1.dll Asm2.dll ... +// +// For each eligible method it injects, at entry: ctx = TracyWeave.Begin("Type.Method"); +// and wraps the original body in a try/finally whose finally calls TracyWeave.End(ctx) +// (plus TracyWeave.FrameMark() in the configured frame method). The TracyWeave shim itself is +// compiled into one of the target assemblies (PlayerViewer.dll) and found by reflection here. +// +// Idempotent: a woven module is stamped with an AssemblyMetadata("TracyWoven") marker and +// skipped on re-runs. + +const int MinInstructions = 10; // skip trivial/inlinable leaf methods + +string dir = null, + frameMark = null; +var targets = new List(); +for (int i = 0; i < args.Length; i++) +{ + switch (args[i]) + { + case "--dir": + dir = args[++i]; + break; + case "--frame-mark": + frameMark = args[++i]; + break; + default: + targets.Add(args[i]); + break; + } +} +if (dir == null || targets.Count == 0) +{ + Console.Error.WriteLine( + "usage: TracyWeaver.cs -- --dir [--frame-mark Type::Method] Asm.dll ..." + ); + return 2; +} + +// Open every target module up front (the shim host is among them). +var resolver = MakeResolver(dir); +var modules = new Dictionary(); +foreach (var name in targets) +{ + string path = Path.Combine(dir, name); + if (!File.Exists(path)) + { + Console.Error.WriteLine($"[tracy-weave] missing {path}, skipping"); + continue; + } + modules[name] = ModuleDefinition.ReadModule( + path, + new ReaderParameters + { + ReadWrite = true, + ReadSymbols = false, + AssemblyResolver = resolver, + } + ); +} + +var shim = modules + .Values.Select(m => m.GetType("TracyWeaver.Runtime.TracyWeave")) + .FirstOrDefault(t => t != null); +if (shim == null) +{ + Console.Error.WriteLine( + "[tracy-weave] TracyWeaver.Runtime.TracyWeave not found in any target assembly" + ); + return 3; +} +var ctxDef = shim.NestedTypes.Single(t => t.Name == "Ctx"); +var beginDef = shim.Methods.Single(m => m.Name == "Begin"); +var endDef = shim.Methods.Single(m => m.Name == "End"); +var frameMarkDef = shim.Methods.Single(m => m.Name == "FrameMark"); + +int totalWoven = 0; +var toWrite = new List(); +foreach (var (name, module) in modules) +{ + if (IsWoven(module)) + { + Console.WriteLine($"[tracy-weave] {name}: already woven, skipping"); + continue; + } + + var ctxType = module.ImportReference(ctxDef); // same-module for the shim host, cross-module otherwise + var beginRef = module.ImportReference(beginDef); + var endRef = module.ImportReference(endDef); + var frameMarkRef = module.ImportReference(frameMarkDef); + + int woven = 0; + foreach (var type in AllTypes(module.Types)) + { + if (HasNoProfile(type)) + continue; // excludes the TracyWeave shim itself + foreach (var method in type.Methods.ToList()) + { + if (!Eligible(method)) + continue; + bool markFrame = + frameMark != null + && (method.DeclaringType.FullName + "::" + method.Name) == frameMark; + try + { + Weave(method, ctxType, beginRef, endRef, markFrame ? frameMarkRef : null); + woven++; + } + catch (Exception ex) + { + Console.Error.WriteLine($"[tracy-weave] skipped {method.FullName}: {ex.Message}"); + } + } + } + + StampWoven(module); + toWrite.Add(module); + Console.WriteLine($"[tracy-weave] {name}: wove {woven} methods"); + totalWoven += woven; +} + +// Write after all imports are done (cross-module refs into the shim host stay valid). +foreach (var module in toWrite) + module.Write(); +foreach (var module in modules.Values) + module.Dispose(); + +Console.WriteLine($"[tracy-weave] done ({totalWoven} methods across {modules.Count} assemblies)"); +return 0; + +// ---- helpers ---- + +// Resolver that falls back to the NuGet cache for refs absent from the output dir (e.g. the +// Windows-only System.Drawing.Common, which Cecil must still resolve to write the module). +static DefaultAssemblyResolver MakeResolver(string dir) +{ + var resolver = new DefaultAssemblyResolver(); + resolver.AddSearchDirectory(dir); + string nuget = Environment.GetEnvironmentVariable("NUGET_PACKAGES"); + if (string.IsNullOrEmpty(nuget)) + nuget = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".nuget", + "packages" + ); + resolver.ResolveFailure += (sender, name) => + { + string pkgDir = Path.Combine(nuget, name.Name.ToLowerInvariant()); + if (!Directory.Exists(pkgDir)) + return null; + var hit = Directory + .EnumerateFiles(pkgDir, name.Name + ".dll", SearchOption.AllDirectories) + .OrderByDescending(TfmRank) + .FirstOrDefault(); + if (hit == null) + return null; + try + { + return AssemblyDefinition.ReadAssembly( + hit, + new ReaderParameters { AssemblyResolver = (IAssemblyResolver)sender } + ); + } + catch + { + return null; + } + }; + return resolver; +} + +// Prefer a modern managed TFM's copy of the assembly. +static int TfmRank(string path) +{ + string[] order = + { + "net10.0", + "net9.0", + "net8.0", + "net7.0", + "net6.0", + "netstandard2.1", + "netstandard2.0", + }; + for (int i = 0; i < order.Length; i++) + if (path.Contains(Path.DirectorySeparatorChar + order[i] + Path.DirectorySeparatorChar)) + return order.Length - i; + return 0; +} + +static IEnumerable AllTypes(IEnumerable types) +{ + foreach (var t in types) + { + yield return t; + foreach (var n in AllTypes(t.NestedTypes)) + yield return n; + } +} + +static bool Eligible(MethodDefinition m) +{ + if (!m.HasBody || m.IsAbstract || m.IsPInvokeImpl || m.IsInternalCall || m.IsRuntime) + return false; + if (m.IsConstructor) + return false; // avoid ctor/base-call verification pitfalls + if (m.IsGetter || m.IsSetter || m.IsAddOn || m.IsRemoveOn) + return false; + if (m.Name.StartsWith("op_", StringComparison.Ordinal)) + return false; + if (m.ReturnType.IsByReference) + return false; // ref-returns complicate value capture + if (m.DeclaringType.IsInterface) + return false; + if (IsGenerated(m.Name) || IsGenerated(m.DeclaringType.Name)) + return false; + if (HasNoProfile(m) || HasCompilerGenerated(m) || HasCompilerGenerated(m.DeclaringType)) + return false; + if (m.Body.Instructions.Count < MinInstructions) + return false; + return true; +} + +// Lambdas, local functions, iterator/async state machines, etc. carry '<' in their names. +static bool IsGenerated(string name) => name.IndexOf('<') >= 0; + +static bool HasNoProfile(ICustomAttributeProvider p) => + p.HasCustomAttributes + && p.CustomAttributes.Any(a => a.AttributeType.Name == "NoProfileAttribute"); + +static bool HasCompilerGenerated(ICustomAttributeProvider p) => + p.HasCustomAttributes + && p.CustomAttributes.Any(a => a.AttributeType.Name == "CompilerGeneratedAttribute"); + +static void Weave( + MethodDefinition m, + TypeReference ctxType, + MethodReference beginRef, + MethodReference endRef, + MethodReference frameMarkRef +) +{ + var body = m.Body; + body.InitLocals = true; + //Expand short-form branches to long form so inserting instructions can't push a target out + //of a short branch's ±127 range; OptimizeMacros re-shortens what still fits after weaving. + body.SimplifyMacros(); + var il = body.GetILProcessor(); + + var ctxVar = new VariableDefinition(ctxType); + body.Variables.Add(ctxVar); + + var first = body.Instructions[0]; + string zone = m.DeclaringType.Name + "." + m.Name; + + // Prologue (outside the try): ctx = Begin("Type.Method"); + il.InsertBefore(first, il.Create(OpCodes.Ldstr, zone)); + il.InsertBefore(first, il.Create(OpCodes.Call, beginRef)); + il.InsertBefore(first, il.Create(OpCodes.Stloc, ctxVar)); + + // Return-value capture local (for non-void methods). + VariableDefinition retVar = null; + if (m.ReturnType.MetadataType != MetadataType.Void) + { + retVar = new VariableDefinition(m.ReturnType); + body.Variables.Add(retVar); + } + + // Epilogue (after the finally): [ldloc retVar]; ret + var retInstr = Instruction.Create(OpCodes.Ret); + var loadRet = retVar != null ? Instruction.Create(OpCodes.Ldloc, retVar) : null; + var leaveTarget = loadRet ?? retInstr; + + // Finally handler: ldloc ctx; End(ctx); [FrameMark();] endfinally + var handlerStart = Instruction.Create(OpCodes.Ldloc, ctxVar); + var endfinally = Instruction.Create(OpCodes.Endfinally); + + // Redirect every existing ret to flow through the finally: capture value (if any), then leave. + foreach (var ins in body.Instructions.Where(i => i.OpCode == OpCodes.Ret).ToList()) + { + if (retVar != null) + { + ins.OpCode = OpCodes.Stloc; + ins.Operand = retVar; // reuse object so branch targets stay valid + il.InsertAfter(ins, Instruction.Create(OpCodes.Leave, leaveTarget)); + } + else + { + ins.OpCode = OpCodes.Leave; + ins.Operand = leaveTarget; + } + } + + // Append handler + epilogue at the end of the method. + il.Append(handlerStart); + il.Append(Instruction.Create(OpCodes.Call, endRef)); + if (frameMarkRef != null) + il.Append(Instruction.Create(OpCodes.Call, frameMarkRef)); + il.Append(endfinally); + if (loadRet != null) + il.Append(loadRet); + il.Append(retInstr); + + body.ExceptionHandlers.Add( + new ExceptionHandler(ExceptionHandlerType.Finally) + { + TryStart = first, + TryEnd = handlerStart, // exclusive + HandlerStart = handlerStart, + HandlerEnd = leaveTarget, // exclusive + } + ); + + body.OptimizeMacros(); // re-shorten branches/locals that fit +} + +static bool IsWoven(ModuleDefinition module) => + module.Assembly.CustomAttributes.Any(a => + a.AttributeType.Name == "AssemblyMetadataAttribute" + && a.ConstructorArguments.Count == 2 + && (a.ConstructorArguments[0].Value as string) == "TracyWoven" + ); + +static void StampWoven(ModuleDefinition module) +{ + var ctor = module.ImportReference( + typeof(System.Reflection.AssemblyMetadataAttribute).GetConstructor( + new[] { typeof(string), typeof(string) } + ) + ); + var attr = new CustomAttribute(ctor); + attr.ConstructorArguments.Add( + new CustomAttributeArgument(module.TypeSystem.String, "TracyWoven") + ); + attr.ConstructorArguments.Add(new CustomAttributeArgument(module.TypeSystem.String, "1")); + module.Assembly.CustomAttributes.Add(attr); +}