feat(rendering): add GPU scene updates and optimizations

Added a new `code-executor` agent with strict TDD and performance focus. Refactored `TextureProcessor` and `TextureAssetHandler` to use `Magick.NET` for image processing. Enhanced `GPUScene` with `InstanceCounterBuffer` and improved instance management. Introduced a compute shader for GPU scene updates. Updated `GhostRenderPipeline` to handle add/remove instance buffers.

BREAKING CHANGE: Removed `x86` platform support and replaced `CachesFolderPath` with `LibraryFolderPath`. Updated project dependencies and removed unused utility classes.
This commit is contained in:
2026-04-14 17:56:23 +09:00
parent 817b32b8d9
commit d9bfa43663
28 changed files with 517 additions and 459 deletions

View File

@@ -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.

View File

@@ -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)

View File

@@ -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<Result<long>> WriteSettingsToStreamAsync(TextureAssetSettings settings, Stream stream, CancellationToken token = default)
{
var size = Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>() + Unsafe.SizeOf<SamplerSettings>();
@@ -284,105 +292,60 @@ internal class TextureAssetHandler : IImportableAssetHandler
public async ValueTask<Result> 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<byte>.Shared.Rent(span.Length);
span.CopyTo(pixelBytes);
}
else
{
using var image = ImageResult.FromStream(sourceStream, colorComponents);
var span = image.AsSpan();
pixelBytes = ArrayPool<byte>.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<ImageContentHeader>(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<byte>.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<byte>.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<byte>.Shared.Return(pixelBytes);
}
return Result.Success();
}
public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default)

View File

@@ -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.
/// </summary>
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>(NvttSurface.Create());
using var pCompOpts = new DisposablePtr<NvttCompressionOptions>(NvttCompressionOptions.Create());
using var pOutOpts = new DisposablePtr<NvttOutputOptions>(NvttOutputOptions.Create());
using var pCtx = new DisposablePtr<NvttContext>(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<NvttSurface>(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";
/// <summary>
/// Compresses <paramref name="pixelData"/> according to <paramref name="settings"/>
/// 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.
/// </summary>
public static string CompressToCache(
string cachesFolderPath,
Guid assetId,
ReadOnlySpan<byte> pixelData,
int width,
int height,
bool isFloat,
ColorComponents colorComponents,
TextureAssetSettings settings)
public static async ValueTask<string> 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,131 +183,13 @@ internal static unsafe class TextureProcessor
File.Delete(stale);
}
RunNvttPipeline(cachePath, pixelData, width, height, isFloat, colorComponents, settings);
var workItem = new NvttPipelineTask(cachePath, image, width, height, depth, settings);
ThreadPool.UnsafeQueueUserWorkItem(workItem, true);
await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
return cachePath;
}
private static void RunNvttPipeline(
string outputPath,
ReadOnlySpan<byte> pixelData,
int width,
int height,
bool isFloat,
ColorComponents colorComponents,
TextureAssetSettings settings)
{
using var pSurface = new DisposablePtr<NvttSurface>(NvttSurface.Create());
using var pCompOpts = new DisposablePtr<NvttCompressionOptions>(NvttCompressionOptions.Create());
using var pOutOpts = new DisposablePtr<NvttOutputOptions>(NvttOutputOptions.Create());
using var pCtx = new DisposablePtr<NvttContext>(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);
}
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<NvttSurface>(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);
}
}
}
private static NvttFormat SelectFormat(TextureAssetSettings settings)
=> settings.Basic.TextureType switch
{
@@ -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<TextureAssetSettings.BasicSettings>();
var advancedSize = Unsafe.SizeOf<TextureAssetSettings.AdvancedSettings>();
@@ -216,9 +224,9 @@ internal static unsafe class TextureProcessor
var total = basicSize + advancedSize + samplerSize;
Span<byte> 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);

View File

@@ -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

View File

@@ -3,7 +3,8 @@
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Ghost.Editor.Core</RootNamespace>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<Platforms>x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<ImplicitUsings>enable</ImplicitUsings>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
@@ -13,8 +14,9 @@
<langversion>preview</langversion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Magick.NET-Q16-HDRI-OpenMP-x64" Version="14.12.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7705" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />

View File

@@ -3,8 +3,8 @@
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<Platforms>x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
@@ -41,7 +41,7 @@
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7705" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
<PackageReference Include="WinUIEx" Version="2.9.0" />
</ItemGroup>

View File

@@ -6,7 +6,9 @@
</Configurations>
<Folder Name="/Editor/">
<Project Path="Editor/Ghost.DSL/Ghost.DSL.csproj" />
<Project Path="Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj" />
<Project Path="Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj">
<Platform Solution="Debug|x64" Project="x64" />
</Project>
<Project Path="Editor/Ghost.Editor/Ghost.Editor.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />

View File

@@ -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]

View File

@@ -1,5 +0,0 @@
namespace Ghost.Core.Utilities;
internal class EnumUtility
{
}

View File

@@ -1,12 +0,0 @@
using Ghost.Core.Contracts;
namespace Ghost.Core.Utilities;
internal static class InternalResource
{
public static void Release<T>(ref T? resource)
where T : IReleasable
{
resource?.InternalRelease();
}
}

View File

@@ -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<T> Move<T>(ref this ComPtr<T> comPtr)
where T : unmanaged, IUnknown.Interface
{
var copy = default(ComPtr<T>);
comPtr.Swap(ref copy);
return copy;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool HasFlag<T>(this uint flags, T flag)
where T : Enum
{
return (flags & Unsafe.As<T, uint>(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}");
}
}
}
}

View File

@@ -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<ManagedEntityRef>();
}
public void Dispose()

View File

@@ -9,6 +9,7 @@ internal unsafe class GPUScene : IDisposable
private readonly IResourceDatabase _resourceDatabase;
private Handle<GPUBuffer> _sceneBuffer;
private Handle<GPUBuffer> _instanceCounterBuffer;
private uint _instanceCount;
private uint _capacity;
@@ -16,6 +17,8 @@ internal unsafe class GPUScene : IDisposable
private bool _disposed;
public Handle<GPUBuffer> SceneBuffer => _sceneBuffer;
public Handle<GPUBuffer> 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);

View File

@@ -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<GPUBuffer> addBuffer, int addCount, Handle<GPUBuffer> removeBuffer, int removeCount)
private static unsafe Handle<GPUBuffer> 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<AddInstanceData>(),
Stride = (uint)MemoryUtility.SizeOf<AddInstanceData>(),
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<GPUBuffer> 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<RemoveInstanceData>(),
Stride = (uint)MemoryUtility.SizeOf<RemoveInstanceData>(),
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<ComputeShader>);
var shader = Handle<ComputeShader>.Invalid;
var keywords = new LocalKeywordSet();
ctx.DispatchCompute(shader, 0, in keywords, in property, new uint3());

View File

@@ -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<GPUBuffer> 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<AddInstanceData>(),
Stride = (uint)MemoryUtility.SizeOf<AddInstanceData>(),
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<GPUBuffer> 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<RemoveInstanceData>(),
Stride = (uint)MemoryUtility.SizeOf<RemoveInstanceData>(),
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)
{

View File

@@ -30,10 +30,13 @@ internal sealed class GhostRenderPayload : IRenderPayload
private readonly ConcurrentQueue<AddInstanceRequest> _addRequest;
private readonly ConcurrentQueue<RemoveInstanceRequest> _removeRequest;
private uint _instanceCount;
public ReadOnlySpan<RenderRequest> RenderRequests => _renderRequests;
public ConcurrentQueue<AddInstanceRequest> AddRequest => _addRequest;
public ConcurrentQueue<RemoveInstanceRequest> 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();

View File

@@ -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<UpdateGPUSceneShaderProperty>(g_PushConstantData.propertiesBuffer, 0);
RWStructuredBuffer<InstanceData> gpuSceneBuffer = GET_BUFFER(properties.gpuSceneBuffer);
if (properties.addCount > 0)
{
StructuredBuffer<AddInstanceData> 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<RemoveInstanceData> removeBuffer = GET_BUFFER(properties.removeBuffer);
RemoveInstanceData removeData = removeBuffer[dispatchThreadID.x];
gpuSceneBuffer[removeData.instanceID] = gpuSceneBuffer[removeData.swapWithInstanceID];
}
}
}
cs "hlsl_block" : "CSMain";
}

View File

@@ -7,6 +7,7 @@ using Misaki.HighPerformance.Utilities;
namespace Ghost.Engine.Systems;
[UpdateAfter<RemoveGPUInstanceSystem>]
[RenderPipelineSystem<GhostRenderPipelineSettings>]
internal class AddGPUInstanceSystem : SystemBase
{
@@ -48,5 +49,7 @@ internal class AddGPUInstanceSystem : SystemBase
systemAPI.World.EntityCommandBuffer.AddComponent(entity, new GPUInstanceRef { gpuSceneIndex = index });
}
}
payload.EndRecord();
}
}

View File

@@ -48,6 +48,11 @@ internal static class ComponentRegistry
internal static readonly Dictionary<int, Type> s_runtimeIDToType = new();
#endif
static ComponentRegistry()
{
GetOrRegisterComponentID<ManagedEntityRef>();
}
public static unsafe Identifier<IComponent> GetOrRegisterComponentID<T>()
where T : unmanaged, IComponent
{

View File

@@ -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());

View File

@@ -8,8 +8,8 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>False</IsAotCompatible>
<IsTrimmable>False</IsTrimmable>
<IsAotCompatible>True</IsAotCompatible>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">

View File

@@ -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<ISystem> Sort(List<ISystem> 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<Type>();
}
// 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<Type>();
}
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<ISystem>();
_sortedVersion = _version;
return;
}

View File

@@ -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)

View File

@@ -22,7 +22,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Misaki.HighPerformance.Image" Version="1.1.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -61,7 +61,7 @@ struct MeshData
BYTE_ADDRESS_BUFFER meshletTrianglesBuffer;
};
#if define(__GRAPHICS__)
#if defined(__GRAPHICS__)
GraphicsPushConstantData g_PushConstantData : register(b0);
#elif defined(__COMPUTE__)
ComputePushConstantData g_PushConstantData : register(b0);

View File

@@ -10,12 +10,12 @@ public static unsafe class MeshBuilder
/// <summary>
/// Creates a unit cube centered at the origin with size 1.
/// </summary>
public static void CreateCube(float size, Color128 color, Allocator allocator, out UnsafeList<Vertex> vertices, out UnsafeList<uint> indices)
public static void CreateCube(float size, Color128 color, AllocationHandle allocationHandle, out UnsafeList<Vertex> vertices, out UnsafeList<uint> indices)
{
var half = size * 0.5f;
vertices = new UnsafeList<Vertex>(24, allocator);
indices = new UnsafeList<uint>(36, allocator);
vertices = new UnsafeList<Vertex>(24, allocationHandle);
indices = new UnsafeList<uint>(36, allocationHandle);
var corners = new float3[]
{
@@ -71,13 +71,13 @@ public static unsafe class MeshBuilder
/// <summary>
/// Creates a plane on the XZ axis centered at the origin.
/// </summary>
public static void CreatePlane(float width, float depth, Color128 color, Allocator allocator, out UnsafeList<Vertex> vertices, out UnsafeList<uint> indices)
public static void CreatePlane(float width, float depth, Color128 color, AllocationHandle allocationHandle, out UnsafeList<Vertex> vertices, out UnsafeList<uint> indices)
{
var hw = width * 0.5f;
var hd = depth * 0.5f;
vertices = new UnsafeList<Vertex>(4, allocator);
indices = new UnsafeList<uint>(6, allocator);
vertices = new UnsafeList<Vertex>(4, allocationHandle);
indices = new UnsafeList<uint>(6, allocationHandle);
vertices.Add(new Vertex()
{
@@ -129,10 +129,10 @@ public static unsafe class MeshBuilder
/// <summary>
/// Creates a UV sphere centered at the origin.
/// </summary>
public static void CreateSphere(int latitudeSegments, int longitudeSegments, float radius, Color128 color, Allocator allocator, out UnsafeList<Vertex> vertices, out UnsafeList<uint> indices)
public static void CreateSphere(int latitudeSegments, int longitudeSegments, float radius, Color128 color, AllocationHandle allocationHandle, out UnsafeList<Vertex> vertices, out UnsafeList<uint> indices)
{
vertices = new UnsafeList<Vertex>((latitudeSegments + 1) * (longitudeSegments + 1), allocator);
indices = new UnsafeList<uint>(latitudeSegments * longitudeSegments * 6, allocator);
vertices = new UnsafeList<Vertex>((latitudeSegments + 1) * (longitudeSegments + 1), allocationHandle);
indices = new UnsafeList<uint>(latitudeSegments * longitudeSegments * 6, allocationHandle);
// Vertices
for (var lat = 0; lat <= latitudeSegments; lat++)

View File

@@ -1,4 +1,3 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static Ghost.DXC.Api;