diff --git a/src/.github/agents/code-executor.agent.md b/src/.github/agents/code-executor.agent.md new file mode 100644 index 0000000..3778bf6 --- /dev/null +++ b/src/.github/agents/code-executor.agent.md @@ -0,0 +1,77 @@ +--- +name: code-executor +description: plan-executing coding agent. It heavily emphasizes strict adherence to the provided plan, rigorous Test-Driven Development (TDD) practices, and high-performance output. +--- + +# code-executor + +## 1. Agent Identity & Core Objective +**Role:** Senior Plan Executor & Performance-Oriented Developer +**Objective:** To meticulously execute predefined architectural and feature plans using a strict Test-Driven Development (TDD) workflow, ensuring all deliverables are highly optimized, performant, and perfectly aligned with the provided specifications. + +You do not invent new features. You do not alter the architectural vision. You execute the plan with precision, speed, and uncompromising quality. + +--- + +## 2. Core Directives + +### I. Strict Plan Adherence +* **Zero Deviation:** Implement strictly what is detailed in the provided plan. Do not add "nice-to-have" features, scope creep, or unauthorized structural changes. +* **Clarification over Assumption:** If a step in the plan is ambiguous, incomplete, or technically unfeasible, halt execution and request clarification. Do not guess. +* **Traceability:** Every piece of code written must directly map back to a specific requirement or step in the provided plan. + +### II. Absolute TDD Workflow (Red-Green-Refactor) +* You must follow strict TDD principles for every implementation. No production code is written without a failing test existing first. +* **Red:** Write comprehensive, edge-case-aware unit and integration tests based *only* on the plan's requirements. +* **Green:** Write the minimal necessary production code to make the tests pass. +* **Refactor:** Optimize the code for readability, maintainability, and performance without changing its behavior (tests must remain green). + +### III. Performance & Optimization Focus +* **Algorithmic Efficiency:** Prioritize optimal Time (Big O) and Space complexity. +* **Resource Management:** Ensure proper memory management, garbage collection awareness, and prevent memory leaks. +* **Concurrency & Asynchrony:** Utilize non-blocking operations and efficient concurrency models where appropriate to maximize throughput. + +--- + +## 3. Execution Protocol + +When provided with a plan, you will execute the following phases sequentially: + +### Phase 1: Plan Ingestion & Test Strategy +1. **Analyze:** Read the provided plan thoroughly. +2. **Deconstruct:** Break the plan down into testable units (functions, classes, API endpoints). +3. **Report:** Output a brief summary acknowledging the exact scope of what will be built and the testing strategy. + +### Phase 2: Test Generation (RED) +1. Write the tests for the current step of the plan. +2. Ensure tests cover standard use cases, boundary conditions, invalid inputs, and error handling. +3. *Output the test files and confirm they will currently fail.* + +### Phase 3: Implementation (GREEN) +1. Write the exact production code required to satisfy the generated tests. +2. Focus strictly on passing the tests. Do not prematurely optimize in this phase. +3. *Output the production code and confirm tests are now passing.* + +### Phase 4: Performance Refactoring (REFACTOR) +1. Review the passing code for bottlenecks, redundant logic, or memory inefficiencies. +2. Refactor the code applying performance best practices. +3. Rerun tests to ensure functional equivalence. +4. *Output the refactored code alongside a brief explanation of the performance improvements made.* + +--- + +## 4. Constraints & Anti-Patterns + +* **DO NOT** skip the testing phase under any circumstances, even for "simple" scripts. +* **DO NOT** mock core logic that is meant to be implemented in the current step; only mock external dependencies (databases, network calls, file systems). +* **DO NOT** modify the testing framework or configuration unless explicitly stated in the plan. +* **DO NOT** return code with placeholder comments (e.g., `// TODO: Implement this`). Write complete, working code for the current step. + +--- + +## 5. Output Format +When delivering responses, structure your output clearly: +1. **Context:** Which step of the plan is currently being addressed. +2. **Tests:** Code blocks containing the test specifications. +3. **Implementation:** Code blocks containing the production code. +4. **Notes:** Any necessary instructions for running the code or specific performance characteristics achieved. \ No newline at end of file diff --git a/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs b/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs index ed2d720..bfb9851 100644 --- a/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs +++ b/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs @@ -1,11 +1,7 @@ using Ghost.Core; using Ghost.Core.Graphics; -using Ghost.Core.Utilities; using Ghost.DSL.ShaderParser; using Misaki.HighPerformance.Utilities; -using System.IO.Hashing; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text; namespace Ghost.DSL.ShaderCompiler; @@ -24,17 +20,6 @@ public struct DSLShaderError internal static class DSLShaderCompiler { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong GetUniqueId(string code) - { - if (string.IsNullOrEmpty(code)) - { - return 0; - } - - return XxHash64.HashToUInt64(MemoryMarshal.AsBytes(code.AsSpan())); - } - private static PipelineState MeragePipeline(PipelineSemantic? semantic, PipelineState parent) { if (semantic == null) @@ -163,7 +148,7 @@ internal static class DSLShaderCompiler passes = passes }; - for (int i = 0; i < descriptor.passes.Length; i++) + for (var i = 0; i < descriptor.passes.Length; i++) { descriptor.passes[i].shader = descriptor; } @@ -283,7 +268,7 @@ internal static class DSLShaderCompiler } var shaderCodes = new ShaderCode[semantics.entryPoints.Count]; - for (int i = 0; i < shaderCodes.Length; i++) + for (var i = 0; i < shaderCodes.Length; i++) { var result = BuildFinalShaderCode(semantics.entryPoints[i].shaderPath, semantics.includes.AsSpan(), semantics.hlsl, propertyInfo.code); if (result.IsFailure) diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAsset.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAsset.cs index ee5ccf7..6aae3af 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAsset.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAsset.cs @@ -1,7 +1,7 @@ using Ghost.Core; using Ghost.Editor.Core.Contracts; using Ghost.Graphics.RHI; -using Misaki.HighPerformance.Image; +using ImageMagick; using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -218,6 +218,14 @@ internal class TextureAssetHandler : IImportableAssetHandler { private const int _CURRENT_VERSION = 1; + private struct ImageContentHeader + { + public uint width; + public uint height; + public uint depth; + public uint colorComponents; + } + private static async ValueTask> WriteSettingsToStreamAsync(TextureAssetSettings settings, Stream stream, CancellationToken token = default) { var size = Unsafe.SizeOf() + Unsafe.SizeOf() + Unsafe.SizeOf(); @@ -284,105 +292,60 @@ internal class TextureAssetHandler : IImportableAssetHandler public async ValueTask ImportAsync(Stream sourceStream, Stream targetStream, Guid id, CancellationToken token = default) { - var info = ImageInfo.FromStream(sourceStream); - if (info.BitsPerChannel <= 0) + using var image = new MagickImage(sourceStream); + var bytes = image.ToByteArray(); + + var settings = new TextureAssetSettings(); + await TextureProcessor.CompressToCacheAsync(EditorApplication.LibraryFolderPath, id, bytes, image.Width, image.Height, image.Depth, settings, token).ConfigureAwait(false); + + var header = new AssetMetadata(id, TextureAsset.s_typeGuid) { - return Result.Failure($"Unsupported image format with {info.BitsPerChannel} bits per channel."); + HandlerVersion = _CURRENT_VERSION, + SettingsOffset = AssetMetadata.SIZE, + }; + + targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin); + var sizeResult = await WriteSettingsToStreamAsync(settings, targetStream, token).ConfigureAwait(false); + if (sizeResult.IsFailure) + { + return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}"); } - var isFloat = info.BitsPerChannel > 8; - var width = info.Width; - var height = info.Height; - var colorComponents = info.ColorComponents; + // Content layout (all little-endian): + // uint32 width + // uint32 height + // uint32 depth + // uint32 colorComponents + // byte[] pixelBytes - byte[] pixelBytes; - - if (isFloat) + header.SettingsSize = sizeResult.Value; + header.ContentOffset = header.SettingsOffset + sizeResult.Value; + unsafe { - using var image = ImageResultFloat.FromStream(sourceStream, colorComponents); - var span = MemoryMarshal.AsBytes(image.AsSpan()); - pixelBytes = ArrayPool.Shared.Rent(span.Length); - span.CopyTo(pixelBytes); - } - else - { - using var image = ImageResult.FromStream(sourceStream, colorComponents); - var span = image.AsSpan(); - pixelBytes = ArrayPool.Shared.Rent(span.Length); - span.CopyTo(pixelBytes); + header.ContentSize = sizeof(ImageContentHeader) + image.Width * image.Height * (image.Depth / 8) * image.ChannelCount; } - try + // Write raw image content + targetStream.Seek(header.ContentOffset, SeekOrigin.Begin); + + var contentHeader = new ImageContentHeader { - var settings = new TextureAssetSettings(); - await Task.Run(() => - TextureProcessor.CompressToCache( - EditorApplication.CachesFolderPath, - id, - pixelBytes, - width, - height, - isFloat, - colorComponents, - settings), - token).ConfigureAwait(false); + width = image.Width, + height = image.Height, + depth = image.Depth, + colorComponents = image.ChannelCount + }; - var header = new AssetMetadata(id, TextureAsset.s_typeGuid) - { - HandlerVersion = _CURRENT_VERSION, - SettingsOffset = AssetMetadata.SIZE, - }; + targetStream.Write(MemoryMarshal.AsBytes(new Span(ref contentHeader))); - targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin); - var sizeResult = await WriteSettingsToStreamAsync(settings, targetStream, token).ConfigureAwait(false); - if (sizeResult.IsFailure) - { - return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}"); - } + await targetStream.WriteAsync(bytes, token).ConfigureAwait(false); + await targetStream.FlushAsync(token).ConfigureAwait(false); - // Content layout (all little-endian): - // int32 width - // int32 height - // byte isFloat (0 = byte, 1 = float) - // int32 colorComponents (cast of ColorComponents enum) - // byte[] pixelBytes - const int _CONTENT_HEADER_SIZE = 4 + 4 + 1 + 4; // 13 bytes + // Patch header now that all sizes are known + targetStream.Seek(0, SeekOrigin.Begin); + AssetMetadata.WriteToStream(targetStream, ref header); - header.SettingsSize = sizeResult.Value; - header.ContentOffset = header.SettingsOffset + sizeResult.Value; - header.ContentSize = _CONTENT_HEADER_SIZE + pixelBytes.Length; - - // Write raw image content - targetStream.Seek(header.ContentOffset, SeekOrigin.Begin); - - var contentHeader = ArrayPool.Shared.Rent(_CONTENT_HEADER_SIZE); - try - { - BitConverter.TryWriteBytes(contentHeader.AsSpan(0, 4), width); - BitConverter.TryWriteBytes(contentHeader.AsSpan(4, 4), height); - contentHeader[8] = isFloat ? (byte)1 : (byte)0; - BitConverter.TryWriteBytes(contentHeader.AsSpan(9, 4), (int)colorComponents); - - await targetStream.WriteAsync(contentHeader.AsMemory(0, _CONTENT_HEADER_SIZE), token).ConfigureAwait(false); - } - finally - { - ArrayPool.Shared.Return(contentHeader); - } - - await targetStream.WriteAsync(pixelBytes, token).ConfigureAwait(false); - await targetStream.FlushAsync(token).ConfigureAwait(false); - - // Patch header now that all sizes are known - targetStream.Seek(0, SeekOrigin.Begin); - AssetMetadata.WriteToStream(targetStream, ref header); - - return Result.Success(); - } - finally - { - ArrayPool.Shared.Return(pixelBytes); - } + return Result.Success(); } public ValueTask> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default) diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs index f14995a..721be18 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs @@ -1,5 +1,5 @@ using Ghost.Nvtt; -using Misaki.HighPerformance.Image; +using ImageMagick; using Misaki.HighPerformance.LowLevel; using System.IO.Hashing; using System.Runtime.CompilerServices; @@ -19,26 +19,152 @@ namespace Ghost.Editor.Core.AssetHandler; /// /// The caller owns opening/closing all streams; this class only takes spans and paths. /// -internal static unsafe class TextureProcessor +internal static class TextureProcessor { + private class NvttPipelineTask : IThreadPoolWorkItem + { + private readonly string _outputPath; + + private readonly byte[] _image; + private readonly uint _depth; + private readonly uint _width; + private readonly uint _height; + + private readonly TextureAssetSettings _settings; + private readonly TaskCompletionSource _completionSource; + + public Task Task => _completionSource.Task; + + public NvttPipelineTask(string outputPath, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings) + { + _outputPath = outputPath; + _image = image; + _width = width; + _height = height; + _depth = depth; + _settings = settings; + _completionSource = new TaskCompletionSource(); + } + + public unsafe void Execute() + { + using var pSurface = new DisposablePtr(NvttSurface.Create()); + using var pCompOpts = new DisposablePtr(NvttCompressionOptions.Create()); + using var pOutOpts = new DisposablePtr(NvttOutputOptions.Create()); + using var pCtx = new DisposablePtr(NvttContext.Create()); + + var inputFormat = _depth > 8 + ? NvttInputFormat.NVTT_InputFormat_RGBA_32F + : NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below + + fixed (void* pData = _image) + { + pSurface.Get()->SetImageData(inputFormat, (int)_width, (int)_height, 1, pData, NvttBoolean.NVTT_True, null); + } + + // stb gives us RGBA byte order; NVTT BGRA_8UB reads it as BGRA, + // so channels R and B are swapped — fix with swizzle(2,1,0,3). + if (_depth <= 8) + { + pSurface.Get()->Swizzle(2, 1, 0, 3, null); + } + + var maxExtent = (int)_settings.Sampler.MaxSize; + if (_settings.Advanced.StretchToPowerOfTwo) + { + pSurface.Get()->ResizeMakeSquare(maxExtent, + NvttRoundMode.NVTT_RoundMode_ToNearestPowerOfTwo, + NvttResizeFilter.NVTT_ResizeFilter_Box, null); + } + else if (pSurface.Get()->Width() > maxExtent || pSurface.Get()->Height() > maxExtent) + { + pSurface.Get()->ResizeMax(maxExtent, + NvttRoundMode.NVTT_RoundMode_None, + NvttResizeFilter.NVTT_ResizeFilter_Box, null); + } + + if (_settings.Advanced.UseBorderColor) + { + var c = _settings.Advanced.BorderColor; + pSurface.Get()->SetBorder(c.r, c.g, c.b, c.a, null); + } + else if (_settings.Advanced.ZeroAlphaBorder) + { + pSurface.Get()->SetBorder(0f, 0f, 0f, 0f, null); + } + + if (_settings.Basic.IsSRGB && _settings.Advanced.GammaCorrection) + { + pSurface.Get()->ToLinearFromSrgb(null); + } + + if (_settings.Advanced.PremultiplyAlpha) + { + pSurface.Get()->PremultiplyAlpha(null); + } + + pCompOpts.Get()->SetFormat(SelectFormat(_settings)); + pCompOpts.Get()->SetQuality(SelectQuality(_settings.Advanced.CompressionLevel)); + + if (_settings.Advanced.CutoutAlpha) + { + pCompOpts.Get()->SetQuantization(false, false, true, + _settings.Advanced.CutoutAlphaThreshold); + } + + pOutOpts.Get()->SetOutputHeader(true); + pOutOpts.Get()->SetSrgbFlag(_settings.Basic.IsSRGB); + pOutOpts.Get()->SetContainer(NvttContainer.NVTT_Container_DDS10); + pOutOpts.Get()->SetFileName(Encoding.UTF8.GetBytes(_outputPath)); + + var nvttFilter = SelectMipmapFilter(_settings.Advanced.MipmapFilter); + + int mipmapCount; + if (!_settings.Advanced.GenerateMipmaps) + { + mipmapCount = 1; + } + else if (_settings.Advanced.MipmapLevelCount == 0) + { + mipmapCount = pSurface.Get()->CountMipmaps(1); + } + else + { + mipmapCount = (int)_settings.Advanced.MipmapLevelCount; + } + + pCtx.Get()->SetCudaAcceleration(NvttApi.IsCudaSupported()); + + pCtx.Get()->OutputHeader(pSurface.Get(), mipmapCount, pCompOpts.Get(), pOutOpts.Get()); + + using var pMip = new DisposablePtr(pSurface.Get()->Clone()); + + for (var level = 0; level < mipmapCount; level++) + { + // Scale alpha for coverage on each pMip (if requested) + if (_settings.Advanced.ScaleAlphaForMipCoverage && level > 0) + { + var refCoverage = pMip.Get()->AlphaTestCoverage( + _settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3); + pMip.Get()->ScaleAlphaToCoverage(refCoverage, + _settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3, null); + } + + pCtx.Get()->Compress(pMip.Get(), 0, level, pCompOpts.Get(), pOutOpts.Get()); + + if (level + 1 < mipmapCount) + { + pMip.Get()->BuildNextMipmapDefaults(nvttFilter, 1, null); + } + } + + _completionSource.SetResult(); + } + } + private const string _TEXTURE_CACHE_SUBFOLDER = "TextureCache"; - /// - /// Compresses according to - /// and writes the result to the texture cache. - /// - /// Returns the absolute path of the cache file on success. - /// The cache file is skipped if it already exists with a matching content hash. - /// - public static string CompressToCache( - string cachesFolderPath, - Guid assetId, - ReadOnlySpan pixelData, - int width, - int height, - bool isFloat, - ColorComponents colorComponents, - TextureAssetSettings settings) + public static async ValueTask CompressToCacheAsync(string cachesFolderPath, Guid assetId, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken) { var cacheDir = Path.Combine(cachesFolderPath, _TEXTURE_CACHE_SUBFOLDER); Directory.CreateDirectory(cacheDir); @@ -57,129 +183,11 @@ internal static unsafe class TextureProcessor File.Delete(stale); } - RunNvttPipeline(cachePath, pixelData, width, height, isFloat, colorComponents, settings); - - return cachePath; - } - - private static void RunNvttPipeline( - string outputPath, - ReadOnlySpan pixelData, - int width, - int height, - bool isFloat, - ColorComponents colorComponents, - TextureAssetSettings settings) - { - using var pSurface = new DisposablePtr(NvttSurface.Create()); - using var pCompOpts = new DisposablePtr(NvttCompressionOptions.Create()); - using var pOutOpts = new DisposablePtr(NvttOutputOptions.Create()); - using var pCtx = new DisposablePtr(NvttContext.Create()); - - var inputFormat = isFloat - ? NvttInputFormat.NVTT_InputFormat_RGBA_32F - : NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below - - fixed (void* pData = pixelData) - { - pSurface.Get()->SetImageData(inputFormat, width, height, 1, pData, NvttBoolean.NVTT_True, null); - } - - // stb gives us RGBA byte order; NVTT BGRA_8UB reads it as BGRA, - // so channels R and B are swapped — fix with swizzle(2,1,0,3). - if (!isFloat) - { - pSurface.Get()->Swizzle(2, 1, 0, 3, null); - } - - var maxExtent = (int)settings.Sampler.MaxSize; - if (settings.Advanced.StretchToPowerOfTwo) - { - pSurface.Get()->ResizeMakeSquare(maxExtent, - NvttRoundMode.NVTT_RoundMode_ToNearestPowerOfTwo, - NvttResizeFilter.NVTT_ResizeFilter_Box, null); - } - else if (pSurface.Get()->Width() > maxExtent || pSurface.Get()->Height() > maxExtent) - { - pSurface.Get()->ResizeMax(maxExtent, - NvttRoundMode.NVTT_RoundMode_None, - NvttResizeFilter.NVTT_ResizeFilter_Box, null); - } - - if (settings.Advanced.UseBorderColor) - { - var c = settings.Advanced.BorderColor; - pSurface.Get()->SetBorder(c.r, c.g, c.b, c.a, null); - } - else if (settings.Advanced.ZeroAlphaBorder) - { - pSurface.Get()->SetBorder(0f, 0f, 0f, 0f, null); - } + var workItem = new NvttPipelineTask(cachePath, image, width, height, depth, settings); + ThreadPool.UnsafeQueueUserWorkItem(workItem, true); + await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - if (settings.Basic.IsSRGB && settings.Advanced.GammaCorrection) - { - pSurface.Get()->ToLinearFromSrgb(null); - } - - if (settings.Advanced.PremultiplyAlpha) - { - pSurface.Get()->PremultiplyAlpha(null); - } - - pCompOpts.Get()->SetFormat(SelectFormat(settings)); - pCompOpts.Get()->SetQuality(SelectQuality(settings.Advanced.CompressionLevel)); - - if (settings.Advanced.CutoutAlpha) - { - pCompOpts.Get()->SetQuantization(false, false, true, - settings.Advanced.CutoutAlphaThreshold); - } - - pOutOpts.Get()->SetOutputHeader(true); - pOutOpts.Get()->SetSrgbFlag(settings.Basic.IsSRGB); - pOutOpts.Get()->SetContainer(NvttContainer.NVTT_Container_DDS10); - pOutOpts.Get()->SetFileName(Encoding.UTF8.GetBytes(outputPath)); - - var nvttFilter = SelectMipmapFilter(settings.Advanced.MipmapFilter); - - int mipmapCount; - if (!settings.Advanced.GenerateMipmaps) - { - mipmapCount = 1; - } - else if (settings.Advanced.MipmapLevelCount == 0) - { - mipmapCount = pSurface.Get()->CountMipmaps(1); - } - else - { - mipmapCount = (int)settings.Advanced.MipmapLevelCount; - } - - pCtx.Get()->SetCudaAcceleration(NvttApi.IsCudaSupported()); - - pCtx.Get()->OutputHeader(pSurface.Get(), mipmapCount, pCompOpts.Get(), pOutOpts.Get()); - - using var pMip = new DisposablePtr(pSurface.Get()->Clone()); - - for (var level = 0; level < mipmapCount; level++) - { - // Scale alpha for coverage on each pMip (if requested) - if (settings.Advanced.ScaleAlphaForMipCoverage && level > 0) - { - var refCoverage = pMip.Get()->AlphaTestCoverage( - settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3); - pMip.Get()->ScaleAlphaToCoverage(refCoverage, - settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3, null); - } - - pCtx.Get()->Compress(pMip.Get(), 0, level, pCompOpts.Get(), pOutOpts.Get()); - - if (level + 1 < mipmapCount) - { - pMip.Get()->BuildNextMipmapDefaults(nvttFilter, 1, null); - } - } + return cachePath; } private static NvttFormat SelectFormat(TextureAssetSettings settings) @@ -208,7 +216,7 @@ internal static unsafe class TextureProcessor _ => NvttMipmapFilter.NVTT_MipmapFilter_Kaiser, }; - private static ulong ComputeSettingsHash(TextureAssetSettings s) + private static ulong ComputeSettingsHash(TextureAssetSettings settings) { var basicSize = Unsafe.SizeOf(); var advancedSize = Unsafe.SizeOf(); @@ -216,9 +224,9 @@ internal static unsafe class TextureProcessor var total = basicSize + advancedSize + samplerSize; Span buf = stackalloc byte[total]; - var basic = s.Basic; - var advanced = s.Advanced; - var sampler = s.Sampler; + var basic = settings.Basic; + var advanced = settings.Advanced; + var sampler = settings.Sampler; MemoryMarshal.Write(buf, in basic); MemoryMarshal.Write(buf.Slice(basicSize), in advanced); MemoryMarshal.Write(buf.Slice(basicSize + advancedSize), in sampler); diff --git a/src/Editor/Ghost.Editor.Core/EditorApplication.cs b/src/Editor/Ghost.Editor.Core/EditorApplication.cs index 2b72005..1a042ee 100644 --- a/src/Editor/Ghost.Editor.Core/EditorApplication.cs +++ b/src/Editor/Ghost.Editor.Core/EditorApplication.cs @@ -8,7 +8,7 @@ public static class EditorApplication public const string ASSETS_FOLDER_NAME = "Assets"; public const string SOURCES_FOLDER_NAME = "Sources"; public const string PACKAGES_FOLDER_NAME = "Packages"; - public const string CACHES_FOLDER_NAME = "Caches"; + public const string LIBRARY_FOLDER_NAME = "Library"; public const string CONFIG_FOLDER_NAME = "Config"; private static IServiceProvider? s_serviceProvider; @@ -25,7 +25,7 @@ public static class EditorApplication public static string AssetsFolderPath => Path.Combine(ProjectPath, ASSETS_FOLDER_NAME); public static string SourcesFolderPath => Path.Combine(ProjectPath, SOURCES_FOLDER_NAME); public static string PackagesFolderPath => Path.Combine(ProjectPath, PACKAGES_FOLDER_NAME); - public static string CachesFolderPath => Path.Combine(ProjectPath, CACHES_FOLDER_NAME); + public static string LibraryFolderPath => Path.Combine(ProjectPath, LIBRARY_FOLDER_NAME); public static string ConfigFolderPath => Path.Combine(ProjectPath, CONFIG_FOLDER_NAME); public static DispatcherQueue DispatcherQueue diff --git a/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj b/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj index a016b48..9183680 100644 --- a/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj +++ b/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj @@ -3,7 +3,8 @@ net10.0-windows10.0.22621.0 10.0.17763.0 Ghost.Editor.Core - win-x86;win-x64;win-arm64 + x64;ARM64 + win-x64;win-arm64 true enable 10.0.20348.0 @@ -13,8 +14,9 @@ preview + - + diff --git a/src/Editor/Ghost.Editor/Ghost.Editor.csproj b/src/Editor/Ghost.Editor/Ghost.Editor.csproj index ac58f70..5b50c39 100644 --- a/src/Editor/Ghost.Editor/Ghost.Editor.csproj +++ b/src/Editor/Ghost.Editor/Ghost.Editor.csproj @@ -3,8 +3,8 @@ WinExe net10.0-windows10.0.22621.0 10.0.17763.0 - x86;x64;ARM64 - win-x86;win-x64;win-arm64 + x64;ARM64 + win-x64;win-arm64 win-$(Platform).pubxml true true @@ -41,7 +41,7 @@ - + diff --git a/src/GhostEngine.slnx b/src/GhostEngine.slnx index 99b099f..52246a9 100644 --- a/src/GhostEngine.slnx +++ b/src/GhostEngine.slnx @@ -6,7 +6,9 @@ - + + + diff --git a/src/Runtime/Ghost.Core/Logging.cs b/src/Runtime/Ghost.Core/Logging.cs index 0564891..eeaac1d 100644 --- a/src/Runtime/Ghost.Core/Logging.cs +++ b/src/Runtime/Ghost.Core/Logging.cs @@ -219,7 +219,11 @@ public static class Logger [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Error(object? message) { - s_logger.Log(message?.ToString() ?? "null", LogLevel.Error); + var messageStr = message?.ToString() ?? "null"; + s_logger.Log(messageStr, LogLevel.Error); +#if DEBUG + System.Diagnostics.Debug.Fail(messageStr); +#endif } [StackTraceHidden] @@ -227,13 +231,21 @@ public static class Logger public static void Error(string message) { s_logger.Log(message, LogLevel.Error); +#if DEBUG + System.Diagnostics.Debug.Fail(message); +#endif } [StackTraceHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Error(string format, params object?[] args) { - s_logger.Log(string.Format(format, args), LogLevel.Error); + var message = string.Format(format, args); + s_logger.Log(message, LogLevel.Error); +#if DEBUG + System.Diagnostics.Debug.Fail(message); +#endif + } [StackTraceHidden] @@ -241,6 +253,10 @@ public static class Logger public static void Error(Exception ex) { s_logger.Log(ex); +#if DEBUG + System.Diagnostics.Debug.Fail(ex.Message); +#endif + } [StackTraceHidden] diff --git a/src/Runtime/Ghost.Core/Utilities/EnumUtility.cs b/src/Runtime/Ghost.Core/Utilities/EnumUtility.cs deleted file mode 100644 index d354bc2..0000000 --- a/src/Runtime/Ghost.Core/Utilities/EnumUtility.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Ghost.Core.Utilities; - -internal class EnumUtility -{ -} diff --git a/src/Runtime/Ghost.Core/Utilities/InternalResource.cs b/src/Runtime/Ghost.Core/Utilities/InternalResource.cs deleted file mode 100644 index 60b338e..0000000 --- a/src/Runtime/Ghost.Core/Utilities/InternalResource.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Ghost.Core.Contracts; - -namespace Ghost.Core.Utilities; - -internal static class InternalResource -{ - public static void Release(ref T? resource) - where T : IReleasable - { - resource?.InternalRelease(); - } -} \ No newline at end of file diff --git a/src/Runtime/Ghost.Core/Utilities/Win32Utility.cs b/src/Runtime/Ghost.Core/Utilities/Win32Utility.cs deleted file mode 100644 index d629eae..0000000 --- a/src/Runtime/Ghost.Core/Utilities/Win32Utility.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Misaki.HighPerformance.LowLevel; -using System.Runtime.CompilerServices; -using System.Runtime.Versioning; -using TerraFX.Interop.Windows; - -namespace Ghost.Core.Utilities; - -[SupportedOSPlatform("windows10.0.19041.0")] -internal static unsafe partial class Win32Utility -{ - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ComPtr Move(ref this ComPtr comPtr) - where T : unmanaged, IUnknown.Interface - { - var copy = default(ComPtr); - comPtr.Swap(ref copy); - return copy; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool HasFlag(this uint flags, T flag) - where T : Enum - { - return (flags & Unsafe.As(ref flag)) != 0; - } - - extension(MemoryLeakException) - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfRefCountNonZero(uint count) - { - if (count != 0) - { - throw new MemoryLeakException($"Reference count is not zero: {count}"); - } - } - } -} \ No newline at end of file diff --git a/src/Runtime/Ghost.Engine/EngineCore.cs b/src/Runtime/Ghost.Engine/EngineCore.cs index 9021139..39dde61 100644 --- a/src/Runtime/Ghost.Engine/EngineCore.cs +++ b/src/Runtime/Ghost.Engine/EngineCore.cs @@ -1,16 +1,9 @@ -using Ghost.Core.Graphics; -using Ghost.Entities; +using Ghost.Engine.RenderPipeline; using Ghost.Graphics; using Misaki.HighPerformance.Jobs; namespace Ghost.Engine; -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -internal class EngineEntryAttribute : Attribute -{ -} - -[EngineEntry] public sealed partial class EngineCore : IDisposable { private readonly JobScheduler _jobScheduler; @@ -27,12 +20,11 @@ public sealed partial class EngineCore : IDisposable { FrameBufferCount = 2, GraphicsAPI = GraphicsAPI.Direct3D12, - InitialRenderPipelineSettings = null! // TODO: We should allow user to specify the initial render pipeline settings. + InitialRenderPipelineSettings = new GhostRenderPipelineSettings(), + ShaderCacheDirectory = "ShaderCache", }; _renderSystem = new RenderSystem(renderingConfig); - - ComponentRegistry.GetOrRegisterComponentID(); } public void Dispose() diff --git a/src/Runtime/Ghost.Engine/RenderPipeline/GPUScene.cs b/src/Runtime/Ghost.Engine/RenderPipeline/GPUScene.cs index 7edab20..230f20e 100644 --- a/src/Runtime/Ghost.Engine/RenderPipeline/GPUScene.cs +++ b/src/Runtime/Ghost.Engine/RenderPipeline/GPUScene.cs @@ -9,6 +9,7 @@ internal unsafe class GPUScene : IDisposable private readonly IResourceDatabase _resourceDatabase; private Handle _sceneBuffer; + private Handle _instanceCounterBuffer; private uint _instanceCount; private uint _capacity; @@ -16,6 +17,8 @@ internal unsafe class GPUScene : IDisposable private bool _disposed; public Handle SceneBuffer => _sceneBuffer; + public Handle InstanceCounterBuffer => _instanceCounterBuffer; + public uint InstanceCount => Volatile.Read(ref _instanceCount); internal GPUScene(IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, uint initialCount) { @@ -30,9 +33,20 @@ internal unsafe class GPUScene : IDisposable HeapType = HeapType.Default, }; + var counterBufferDesc = new BufferDesc + { + Size = sizeof(uint), + Stride = sizeof(uint), + Usage = BufferUsage.Structured | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource, + HeapType = HeapType.Default, + }; + _sceneBuffer = _resourceAllocator.CreateBuffer(in bufferDesc, "SceneBuffer"); Logger.DebugAssert(_sceneBuffer.IsValid, "Failed to create GPUScene buffer."); + _instanceCounterBuffer = _resourceAllocator.CreateBuffer(in counterBufferDesc, "SceneInstanceCounterBuffer"); + Logger.DebugAssert(_instanceCounterBuffer.IsValid, "Failed to create GPUScene instance counter buffer."); + _capacity = initialCount; } @@ -55,7 +69,7 @@ internal unsafe class GPUScene : IDisposable { Size = newCapacity * (ulong)sizeof(InstanceData), Stride = (uint)sizeof(InstanceData), - Usage = BufferUsage.Structured | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource, + Usage = BufferUsage.Raw | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource, HeapType = HeapType.Default, }; @@ -88,7 +102,7 @@ internal unsafe class GPUScene : IDisposable { if (index < 0 || index >= _capacity) { - return ~0u; + return uint.MaxValue; } // Return the last index. We will swap the last instance data with the removed index on gpu to keep the buffer compact. @@ -104,6 +118,7 @@ internal unsafe class GPUScene : IDisposable } _resourceDatabase.ReleaseResource(_sceneBuffer.AsResource()); + _resourceDatabase.ReleaseResource(_instanceCounterBuffer.AsResource()); _disposed = true; GC.SuppressFinalize(this); diff --git a/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.UpdateGPUScene.cs b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.UpdateGPUScene.cs index 98359f1..1f95cfd 100644 --- a/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.UpdateGPUScene.cs +++ b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.UpdateGPUScene.cs @@ -2,8 +2,11 @@ using Ghost.Core; using Ghost.Core.Graphics; using Ghost.Graphics.Core; using Ghost.Graphics.RHI; +using Ghost.Graphics.Services; +using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.Mathematics; + namespace Ghost.Engine.RenderPipeline; [GenerateShaderProperty("Internal/UpdateGPUScene")] @@ -18,8 +21,96 @@ public partial struct UpdateGPUSceneShaderProperty internal partial class GhostRenderPipeline { - public void UpdateGPUScene(RenderContext ctx, Handle addBuffer, int addCount, Handle removeBuffer, int removeCount) + private static unsafe Handle CreateAddInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase, out int count) { + if (!ghostPayload.AddRequest.IsEmpty) + { + var addDesc = new BufferDesc + { + Size = (nuint)ghostPayload.AddRequest.Count * MemoryUtility.SizeOf(), + Stride = (uint)MemoryUtility.SizeOf(), + Usage = BufferUsage.Structured | BufferUsage.ShaderResource, + HeapType = HeapType.Upload + }; + + var addBuffer = resourceManager.CreateTransientBuffer(in addDesc, "Add Instance Buffer"); + var pAddData = (AddInstanceData*)resourceDatabase.MapResource(addBuffer.AsResource(), 0, null); + + var i = 0; + while (ghostPayload.AddRequest.TryDequeue(out var addRequest)) + { + var (mesh, error) = resourceManager.GetMeshReference(addRequest.meshInstance.mesh); + if (error.IsFailure) + { + Logger.Error($"Failed to get mesh reference for mesh instance with ID {addRequest.instanceId}"); + continue; + } + + pAddData[i] = new AddInstanceData + { + localToWorld = addRequest.localToWorld, + instanceID = addRequest.instanceId, + meshBuffer = resourceDatabase.GetBindlessIndex(mesh.Get().MeshDataBuffer.AsResource()), + materialPalette = (uint)addRequest.meshInstance.materialPalette.Value, + renderingLayerMask = addRequest.meshInstance.renderingLayerMask, + shadowCastingMode = (uint)addRequest.meshInstance.shadowCastingMode + }; + + i++; + } + + resourceDatabase.UnmapResource(addBuffer.AsResource(), 0, null); + + count = i; + return addBuffer; + } + + count = 0; + return default; + } + + private static unsafe Handle CreateRemoveInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase, out int count) + { + if (!ghostPayload.RemoveRequest.IsEmpty) + { + var addDesc = new BufferDesc + { + Size = (nuint)ghostPayload.AddRequest.Count * MemoryUtility.SizeOf(), + Stride = (uint)MemoryUtility.SizeOf(), + Usage = BufferUsage.Structured | BufferUsage.ShaderResource, + HeapType = HeapType.Upload + }; + + var removeBuffer = resourceManager.CreateTransientBuffer(in addDesc, "Remove Instance Buffer"); + var pRemoveData = (RemoveInstanceData*)resourceDatabase.MapResource(removeBuffer.AsResource(), 0, null); + + var i = 0; + while (ghostPayload.RemoveRequest.TryDequeue(out var removeRequest)) + { + pRemoveData[i] = new RemoveInstanceData + { + instanceID = removeRequest.instanceId, + swapWithInstanceID = removeRequest.swapWithInstanceId + }; + + i++; + } + + resourceDatabase.UnmapResource(removeBuffer.AsResource(), 0, null); + + count = i; + return removeBuffer; + } + + count = 0; + return default; + } + + public void UpdateGPUScene(RenderContext ctx, GhostRenderPayload payload) + { + var addBuffer = CreateAddInstanceBuffer(payload, ctx.ResourceManager, ctx.ResourceDatabase, out var addCount); + var removeBuffer = CreateRemoveInstanceBuffer(payload, ctx.ResourceManager, ctx.ResourceDatabase, out var removeCount); + if (addCount <= 0 && removeCount <= 0) { Logger.DebugAssert(addBuffer.IsInvalid && removeBuffer.IsInvalid, "Buffers should be invalid when there are no updates."); @@ -42,7 +133,7 @@ internal partial class GhostRenderPipeline }; // TODO: Write and load the shader. This is just a placeholder for now. - var shader = default(Handle); + var shader = Handle.Invalid; var keywords = new LocalKeywordSet(); ctx.DispatchCompute(shader, 0, in keywords, in property, new uint3()); diff --git a/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.cs b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.cs index 8429b3b..9d814b0 100644 --- a/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.cs +++ b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.cs @@ -2,11 +2,7 @@ using Ghost.Core; using Ghost.Graphics; using Ghost.Graphics.Core; using Ghost.Graphics.RenderGraphModule; -using Ghost.Graphics.RHI; -using Ghost.Graphics.Services; -using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.Mathematics; -using System.Diagnostics; namespace Ghost.Engine.RenderPipeline; @@ -15,7 +11,7 @@ internal partial class GhostRenderPipeline : IRenderPipeline private struct AddInstanceData { public float4x4 localToWorld; - public uint instanceId; + public uint instanceID; public uint meshBuffer; public uint materialPalette; public uint renderingLayerMask; @@ -24,8 +20,8 @@ internal partial class GhostRenderPipeline : IRenderPipeline private struct RemoveInstanceData { - public uint instanceId; - public uint swapWithInstanceId; + public uint instanceID; + public uint swapWithInstanceID; } private readonly RenderSystem _renderSystem; @@ -43,109 +39,18 @@ internal partial class GhostRenderPipeline : IRenderPipeline _gpuScene = new GPUScene(renderSystem.GraphicsEngine.ResourceAllocator, renderSystem.GraphicsEngine.ResourceDatabase, 102_400u); // 102.4k objects should be enough for now } - private static unsafe Handle CreateAddInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase, out int count) - { - if (!ghostPayload.AddRequest.IsEmpty) - { - var addDesc = new BufferDesc - { - Size = (nuint)ghostPayload.AddRequest.Count * MemoryUtility.SizeOf(), - Stride = (uint)MemoryUtility.SizeOf(), - Usage = BufferUsage.Structured | BufferUsage.ShaderResource, - HeapType = HeapType.Upload - }; - - var addBuffer = resourceManager.CreateTransientBuffer(in addDesc, "Add Instance Buffer"); - var pAddData = (AddInstanceData*)resourceDatabase.MapResource(addBuffer.AsResource(), 0, null); - - var i = 0; - while (ghostPayload.AddRequest.TryDequeue(out var addRequest)) - { - var (mesh, error) = resourceManager.GetMeshReference(addRequest.meshInstance.mesh); - if (error.IsFailure) - { - Debug.Fail($"Failed to get mesh reference for mesh instance with ID {addRequest.instanceId}"); - continue; - } - - pAddData[i] = new AddInstanceData - { - localToWorld = addRequest.localToWorld, - instanceId = addRequest.instanceId, - meshBuffer = resourceDatabase.GetBindlessIndex(mesh.Get().MeshDataBuffer.AsResource()), - materialPalette = (uint)addRequest.meshInstance.materialPalette.Value, - renderingLayerMask = addRequest.meshInstance.renderingLayerMask, - shadowCastingMode = (uint)addRequest.meshInstance.shadowCastingMode - }; - - i++; - } - - resourceDatabase.UnmapResource(addBuffer.AsResource(), 0, null); - - count = i; - return addBuffer; - } - - count = 0; - return default; - } - - private static unsafe Handle CreateRemoveInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase, out int count) - { - if (!ghostPayload.RemoveRequest.IsEmpty) - { - var addDesc = new BufferDesc - { - Size = (nuint)ghostPayload.AddRequest.Count * MemoryUtility.SizeOf(), - Stride = (uint)MemoryUtility.SizeOf(), - Usage = BufferUsage.Structured | BufferUsage.ShaderResource, - HeapType = HeapType.Upload - }; - - var removeBuffer = resourceManager.CreateTransientBuffer(in addDesc, "Remove Instance Buffer"); - var pRemoveData = (RemoveInstanceData*)resourceDatabase.MapResource(removeBuffer.AsResource(), 0, null); - - var i = 0; - while (ghostPayload.RemoveRequest.TryDequeue(out var removeRequest)) - { - pRemoveData[i] = new RemoveInstanceData - { - instanceId = removeRequest.instanceId, - swapWithInstanceId = removeRequest.swapWithInstanceId - }; - - i++; - } - - resourceDatabase.UnmapResource(removeBuffer.AsResource(), 0, null); - - count = i; - return removeBuffer; - } - - count = 0; - return default; - } - public void Render(RenderContext ctx, int frameIndex, IRenderPayload payload) { var ghostPayload = (GhostRenderPayload)payload; - var resourceManager = _renderSystem.ResourceManager; - var resourceDatabase = _renderSystem.GraphicsEngine.ResourceDatabase; - foreach (ref readonly var request in ghostPayload.RenderRequests) { try { - using var viewData = new RenderViewData(_renderSystem.SwapChainManager, resourceDatabase, in request); + using var viewData = new RenderViewData(_renderSystem.SwapChainManager, ctx.ResourceDatabase, in request); RenderPipelineUtility.GetVPMatrices(in request, viewData.ScreenSize, out var view, out var projection); - var addBuffer = CreateAddInstanceBuffer(ghostPayload, resourceManager, resourceDatabase, out var addCount); - var removeBuffer = CreateRemoveInstanceBuffer(ghostPayload, resourceManager, resourceDatabase, out var removeCount); - - UpdateGPUScene(ctx, addBuffer, addCount, removeBuffer, removeCount); + UpdateGPUScene(ctx, ghostPayload); } catch (Exception ex) { diff --git a/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipelineSettings.cs b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipelineSettings.cs index 84d6801..6053c55 100644 --- a/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipelineSettings.cs +++ b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipelineSettings.cs @@ -30,10 +30,13 @@ internal sealed class GhostRenderPayload : IRenderPayload private readonly ConcurrentQueue _addRequest; private readonly ConcurrentQueue _removeRequest; + private uint _instanceCount; + public ReadOnlySpan RenderRequests => _renderRequests; public ConcurrentQueue AddRequest => _addRequest; public ConcurrentQueue RemoveRequest => _removeRequest; + public uint InstanceCount => _instanceCount; public GhostRenderPayload(GhostRenderPipeline renderPipeline) { @@ -53,6 +56,7 @@ internal sealed class GhostRenderPayload : IRenderPayload public uint AddInstance(float4x4 ltw, ref readonly MeshInstance meshInstance) { var index = _renderPipeline.GPUScene.AddInstance(); + _addRequest.Enqueue(new AddInstanceRequest { instanceId = index, localToWorld = ltw, meshInstance = meshInstance }); return index; } @@ -60,12 +64,18 @@ internal sealed class GhostRenderPayload : IRenderPayload public void RemoveInstance(uint instanceId) { var swapWithInstanceId = _renderPipeline.GPUScene.RemoveInstance(instanceId); - if (swapWithInstanceId != ~0u) + if (swapWithInstanceId != uint.MaxValue) { _removeRequest.Enqueue(new RemoveInstanceRequest { instanceId = instanceId, swapWithInstanceId = swapWithInstanceId }); } } + public void EndRecord() + { + // We capture the count here to prevent that main thread continues to add more requests for next frame while the render thread is still processing current frame's requests. + _instanceCount = _renderPipeline.GPUScene.InstanceCount; + } + public void Reset() { _renderRequests.Clear(); diff --git a/src/Runtime/Ghost.Engine/Shaders/UpdateGPUScene.gcomp b/src/Runtime/Ghost.Engine/Shaders/UpdateGPUScene.gcomp new file mode 100644 index 0000000..d807e9a --- /dev/null +++ b/src/Runtime/Ghost.Engine/Shaders/UpdateGPUScene.gcomp @@ -0,0 +1,37 @@ +compute "Internal/UpdateGPUScene" +{ + includes + { + "F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Includes/Properties.hlsl"; + } + + hlsl + { + [numthreads(64, 1, 1)] + void CSMain(uint3 dispatchThreadID : SV_DispatchThreadID) + { + UpdateGPUSceneShaderProperty properties = LoadData(g_PushConstantData.propertiesBuffer, 0); + RWStructuredBuffer gpuSceneBuffer = GET_BUFFER(properties.gpuSceneBuffer); + + if (properties.addCount > 0) + { + StructuredBuffer addBuffer = GET_BUFFER(properties.addBuffer); + AddInstanceData addData = addBuffer[dispatchThreadID.x]; + + gpuSceneBuffer[addData.instanceID].localToWorld = addData.localToWorld; + gpuSceneBuffer[addData.instanceID].meshBuffer = addData.meshBuffer; + gpuSceneBuffer[addData.instanceID].materialBuffer = addData.materialPalette; + } + + if (properties.removeCount > 0) + { + StructuredBuffer removeBuffer = GET_BUFFER(properties.removeBuffer); + RemoveInstanceData removeData = removeBuffer[dispatchThreadID.x]; + + gpuSceneBuffer[removeData.instanceID] = gpuSceneBuffer[removeData.swapWithInstanceID]; + } + } + } + + cs "hlsl_block" : "CSMain"; +} diff --git a/src/Runtime/Ghost.Engine/Systems/AddGPUInstanceSystem.cs b/src/Runtime/Ghost.Engine/Systems/AddGPUInstanceSystem.cs index 7280e2f..a548d35 100644 --- a/src/Runtime/Ghost.Engine/Systems/AddGPUInstanceSystem.cs +++ b/src/Runtime/Ghost.Engine/Systems/AddGPUInstanceSystem.cs @@ -7,6 +7,7 @@ using Misaki.HighPerformance.Utilities; namespace Ghost.Engine.Systems; +[UpdateAfter] [RenderPipelineSystem] internal class AddGPUInstanceSystem : SystemBase { @@ -48,5 +49,7 @@ internal class AddGPUInstanceSystem : SystemBase systemAPI.World.EntityCommandBuffer.AddComponent(entity, new GPUInstanceRef { gpuSceneIndex = index }); } } + + payload.EndRecord(); } } diff --git a/src/Runtime/Ghost.Entities/Component.cs b/src/Runtime/Ghost.Entities/Component.cs index 85e2222..6b5dac9 100644 --- a/src/Runtime/Ghost.Entities/Component.cs +++ b/src/Runtime/Ghost.Entities/Component.cs @@ -48,6 +48,11 @@ internal static class ComponentRegistry internal static readonly Dictionary s_runtimeIDToType = new(); #endif + static ComponentRegistry() + { + GetOrRegisterComponentID(); + } + public static unsafe Identifier GetOrRegisterComponentID() where T : unmanaged, IComponent { diff --git a/src/Runtime/Ghost.Entities/EntityManager.cs b/src/Runtime/Ghost.Entities/EntityManager.cs index d6f19ee..9cef3a2 100644 --- a/src/Runtime/Ghost.Entities/EntityManager.cs +++ b/src/Runtime/Ghost.Entities/EntityManager.cs @@ -424,7 +424,7 @@ public unsafe partial class EntityManager : IDisposable // Remove Managed Entities first // RemoveManagedEntity(rowIndicesCache.AsSpan(), in prevArchetype, prevChunkIndex); - // TODO: Handle ICleanupComponent here before we remove the entities from the archetype. + // FIX: Handle ICleanupComponent here before we remove the entities from the archetype. // Execute the hole-filling/swap logic prevArchetype.RemoveEntities(prevChunkIndex, rowIndicesCache.AsSpan()); diff --git a/src/Runtime/Ghost.Entities/Ghost.Entities.csproj b/src/Runtime/Ghost.Entities/Ghost.Entities.csproj index 5091892..84098a5 100644 --- a/src/Runtime/Ghost.Entities/Ghost.Entities.csproj +++ b/src/Runtime/Ghost.Entities/Ghost.Entities.csproj @@ -8,8 +8,8 @@ - False - False + True + True diff --git a/src/Runtime/Ghost.Entities/System.cs b/src/Runtime/Ghost.Entities/System.cs index 09408a7..d1bfcb5 100644 --- a/src/Runtime/Ghost.Entities/System.cs +++ b/src/Runtime/Ghost.Entities/System.cs @@ -1,5 +1,6 @@ using Ghost.Core; using Misaki.HighPerformance.LowLevel.Collections; +using System.Runtime.InteropServices; namespace Ghost.Entities; @@ -201,6 +202,7 @@ public abstract class SystemGroup : ISystem // _systems = SystemGroupRegistry.GetSystemsForGroup(GetType()); // } + // TODO: Use Source Generators to generate group registrations at compile time, and remove the need for this public constructor. private static List Sort(List systems) { // 1. Build the Graph @@ -211,10 +213,10 @@ public abstract class SystemGroup : ISystem foreach (var sys in systems) { var type = sys.GetType(); - if (!dependencies.TryGetValue(type, out var value)) + ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(dependencies, type, out var exists); + if (!exists || value == null) { - value = []; - dependencies[type] = value; + value = new HashSet(); } // Handle [UpdateAfter(typeof(Other))] -> Other comes before This @@ -229,8 +231,13 @@ public abstract class SystemGroup : ISystem foreach (var attr in type.GetCustomAttributes(typeof(UpdateBeforeAttribute), true)) { var targetType = ((UpdateBeforeAttribute)attr).SystemType; - if (!dependencies.ContainsKey(targetType)) dependencies[targetType] = []; - dependencies[targetType].Add(type); + ref var targetDeps = ref CollectionsMarshal.GetValueRefOrAddDefault(dependencies, targetType, out exists); + if (!exists || targetDeps == null) + { + targetDeps = new HashSet(); + } + + targetDeps.Add(type); } } @@ -246,7 +253,10 @@ public abstract class SystemGroup : ISystem foreach (var sys in systems) { var type = sys.GetType(); - if (visited.Contains(type)) continue; + if (visited.Contains(type)) + { + continue; + } // Check if all dependencies for this system are already visited/sorted var canRun = true; @@ -297,7 +307,7 @@ public abstract class SystemGroup : ISystem if (_systems.Count == 0) { - _sortedSystems = []; + _sortedSystems = new List(); _sortedVersion = _version; return; } diff --git a/src/Runtime/Ghost.Graphics/Core/RenderList.cs b/src/Runtime/Ghost.Graphics/Core/RenderList.cs index c34ea32..4cf2b9a 100644 --- a/src/Runtime/Ghost.Graphics/Core/RenderList.cs +++ b/src/Runtime/Ghost.Graphics/Core/RenderList.cs @@ -93,11 +93,6 @@ public struct RenderList : IDisposable } } - public RenderList(int maxLevelOfConcurrency, int capacity, Allocator allocator) - : this(maxLevelOfConcurrency, capacity, AllocationManager.GetAllocationHandle(allocator)) - { - } - private readonly void ThrowIfNotCreated() { if (!IsCreated) diff --git a/src/Runtime/Ghost.Graphics/Ghost.Graphics.csproj b/src/Runtime/Ghost.Graphics/Ghost.Graphics.csproj index 4002b0b..0bd2085 100644 --- a/src/Runtime/Ghost.Graphics/Ghost.Graphics.csproj +++ b/src/Runtime/Ghost.Graphics/Ghost.Graphics.csproj @@ -22,7 +22,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/Runtime/Ghost.Graphics/Shaders/Includes/Properties.hlsl b/src/Runtime/Ghost.Graphics/Shaders/Includes/Properties.hlsl index 4175875..0b5d387 100644 --- a/src/Runtime/Ghost.Graphics/Shaders/Includes/Properties.hlsl +++ b/src/Runtime/Ghost.Graphics/Shaders/Includes/Properties.hlsl @@ -55,13 +55,13 @@ struct MeshData BYTE_ADDRESS_BUFFER vertexBuffer; float3 worldBoundsMax; BYTE_ADDRESS_BUFFER indexBuffer; - + BYTE_ADDRESS_BUFFER meshletBuffer; BYTE_ADDRESS_BUFFER meshletVerticesBuffer; BYTE_ADDRESS_BUFFER meshletTrianglesBuffer; }; -#if define(__GRAPHICS__) +#if defined(__GRAPHICS__) GraphicsPushConstantData g_PushConstantData : register(b0); #elif defined(__COMPUTE__) ComputePushConstantData g_PushConstantData : register(b0); diff --git a/src/Runtime/Ghost.Graphics/Utilities/MeshBuilder.cs b/src/Runtime/Ghost.Graphics/Utilities/MeshBuilder.cs index b080836..98a9c52 100644 --- a/src/Runtime/Ghost.Graphics/Utilities/MeshBuilder.cs +++ b/src/Runtime/Ghost.Graphics/Utilities/MeshBuilder.cs @@ -10,12 +10,12 @@ public static unsafe class MeshBuilder /// /// Creates a unit cube centered at the origin with size 1. /// - public static void CreateCube(float size, Color128 color, Allocator allocator, out UnsafeList vertices, out UnsafeList indices) + public static void CreateCube(float size, Color128 color, AllocationHandle allocationHandle, out UnsafeList vertices, out UnsafeList indices) { var half = size * 0.5f; - vertices = new UnsafeList(24, allocator); - indices = new UnsafeList(36, allocator); + vertices = new UnsafeList(24, allocationHandle); + indices = new UnsafeList(36, allocationHandle); var corners = new float3[] { @@ -71,13 +71,13 @@ public static unsafe class MeshBuilder /// /// Creates a plane on the XZ axis centered at the origin. /// - public static void CreatePlane(float width, float depth, Color128 color, Allocator allocator, out UnsafeList vertices, out UnsafeList indices) + public static void CreatePlane(float width, float depth, Color128 color, AllocationHandle allocationHandle, out UnsafeList vertices, out UnsafeList indices) { var hw = width * 0.5f; var hd = depth * 0.5f; - vertices = new UnsafeList(4, allocator); - indices = new UnsafeList(6, allocator); + vertices = new UnsafeList(4, allocationHandle); + indices = new UnsafeList(6, allocationHandle); vertices.Add(new Vertex() { @@ -129,10 +129,10 @@ public static unsafe class MeshBuilder /// /// Creates a UV sphere centered at the origin. /// - public static void CreateSphere(int latitudeSegments, int longitudeSegments, float radius, Color128 color, Allocator allocator, out UnsafeList vertices, out UnsafeList indices) + public static void CreateSphere(int latitudeSegments, int longitudeSegments, float radius, Color128 color, AllocationHandle allocationHandle, out UnsafeList vertices, out UnsafeList indices) { - vertices = new UnsafeList((latitudeSegments + 1) * (longitudeSegments + 1), allocator); - indices = new UnsafeList(latitudeSegments * longitudeSegments * 6, allocator); + vertices = new UnsafeList((latitudeSegments + 1) * (longitudeSegments + 1), allocationHandle); + indices = new UnsafeList(latitudeSegments * longitudeSegments * 6, allocationHandle); // Vertices for (var lat = 0; lat <= latitudeSegments; lat++) diff --git a/src/ThridParty/Ghost.DXC/Generated/IDxcUtils.cs b/src/ThridParty/Ghost.DXC/Generated/IDxcUtils.cs index 1ec4930..8332426 100644 --- a/src/ThridParty/Ghost.DXC/Generated/IDxcUtils.cs +++ b/src/ThridParty/Ghost.DXC/Generated/IDxcUtils.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using static Ghost.DXC.Api;