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;
using Ghost.Core.Graphics; using Ghost.Core.Graphics;
using Ghost.Core.Utilities;
using Ghost.DSL.ShaderParser; using Ghost.DSL.ShaderParser;
using Misaki.HighPerformance.Utilities; using Misaki.HighPerformance.Utilities;
using System.IO.Hashing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
namespace Ghost.DSL.ShaderCompiler; namespace Ghost.DSL.ShaderCompiler;
@@ -24,17 +20,6 @@ public struct DSLShaderError
internal static class DSLShaderCompiler 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) private static PipelineState MeragePipeline(PipelineSemantic? semantic, PipelineState parent)
{ {
if (semantic == null) if (semantic == null)
@@ -163,7 +148,7 @@ internal static class DSLShaderCompiler
passes = passes 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; descriptor.passes[i].shader = descriptor;
} }
@@ -283,7 +268,7 @@ internal static class DSLShaderCompiler
} }
var shaderCodes = new ShaderCode[semantics.entryPoints.Count]; 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); var result = BuildFinalShaderCode(semantics.entryPoints[i].shaderPath, semantics.includes.AsSpan(), semantics.hlsl, propertyInfo.code);
if (result.IsFailure) if (result.IsFailure)

View File

@@ -1,7 +1,7 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Contracts;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Image; using ImageMagick;
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -218,6 +218,14 @@ internal class TextureAssetHandler : IImportableAssetHandler
{ {
private const int _CURRENT_VERSION = 1; 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) 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>(); 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) public async ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, CancellationToken token = default)
{ {
var info = ImageInfo.FromStream(sourceStream); using var image = new MagickImage(sourceStream);
if (info.BitsPerChannel <= 0) 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; // Content layout (all little-endian):
var width = info.Width; // uint32 width
var height = info.Height; // uint32 height
var colorComponents = info.ColorComponents; // uint32 depth
// uint32 colorComponents
// byte[] pixelBytes
byte[] pixelBytes; header.SettingsSize = sizeResult.Value;
header.ContentOffset = header.SettingsOffset + sizeResult.Value;
if (isFloat) unsafe
{ {
using var image = ImageResultFloat.FromStream(sourceStream, colorComponents); header.ContentSize = sizeof(ImageContentHeader) + image.Width * image.Height * (image.Depth / 8) * image.ChannelCount;
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);
} }
try // Write raw image content
targetStream.Seek(header.ContentOffset, SeekOrigin.Begin);
var contentHeader = new ImageContentHeader
{ {
var settings = new TextureAssetSettings(); width = image.Width,
await Task.Run(() => height = image.Height,
TextureProcessor.CompressToCache( depth = image.Depth,
EditorApplication.CachesFolderPath, colorComponents = image.ChannelCount
id, };
pixelBytes,
width,
height,
isFloat,
colorComponents,
settings),
token).ConfigureAwait(false);
var header = new AssetMetadata(id, TextureAsset.s_typeGuid) targetStream.Write(MemoryMarshal.AsBytes(new Span<ImageContentHeader>(ref contentHeader)));
{
HandlerVersion = _CURRENT_VERSION,
SettingsOffset = AssetMetadata.SIZE,
};
targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin); await targetStream.WriteAsync(bytes, token).ConfigureAwait(false);
var sizeResult = await WriteSettingsToStreamAsync(settings, targetStream, token).ConfigureAwait(false); await targetStream.FlushAsync(token).ConfigureAwait(false);
if (sizeResult.IsFailure)
{
return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}");
}
// Content layout (all little-endian): // Patch header now that all sizes are known
// int32 width targetStream.Seek(0, SeekOrigin.Begin);
// int32 height AssetMetadata.WriteToStream(targetStream, ref header);
// 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
header.SettingsSize = sizeResult.Value; return Result.Success();
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);
}
} }
public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default) public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default)

View File

@@ -1,5 +1,5 @@
using Ghost.Nvtt; using Ghost.Nvtt;
using Misaki.HighPerformance.Image; using ImageMagick;
using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel;
using System.IO.Hashing; using System.IO.Hashing;
using System.Runtime.CompilerServices; 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. /// The caller owns opening/closing all streams; this class only takes spans and paths.
/// </summary> /// </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"; private const string _TEXTURE_CACHE_SUBFOLDER = "TextureCache";
/// <summary> public static async ValueTask<string> CompressToCacheAsync(string cachesFolderPath, Guid assetId, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken)
/// 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)
{ {
var cacheDir = Path.Combine(cachesFolderPath, _TEXTURE_CACHE_SUBFOLDER); var cacheDir = Path.Combine(cachesFolderPath, _TEXTURE_CACHE_SUBFOLDER);
Directory.CreateDirectory(cacheDir); Directory.CreateDirectory(cacheDir);
@@ -57,131 +183,13 @@ internal static unsafe class TextureProcessor
File.Delete(stale); 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; 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) private static NvttFormat SelectFormat(TextureAssetSettings settings)
=> settings.Basic.TextureType switch => settings.Basic.TextureType switch
{ {
@@ -208,7 +216,7 @@ internal static unsafe class TextureProcessor
_ => NvttMipmapFilter.NVTT_MipmapFilter_Kaiser, _ => NvttMipmapFilter.NVTT_MipmapFilter_Kaiser,
}; };
private static ulong ComputeSettingsHash(TextureAssetSettings s) private static ulong ComputeSettingsHash(TextureAssetSettings settings)
{ {
var basicSize = Unsafe.SizeOf<TextureAssetSettings.BasicSettings>(); var basicSize = Unsafe.SizeOf<TextureAssetSettings.BasicSettings>();
var advancedSize = Unsafe.SizeOf<TextureAssetSettings.AdvancedSettings>(); var advancedSize = Unsafe.SizeOf<TextureAssetSettings.AdvancedSettings>();
@@ -216,9 +224,9 @@ internal static unsafe class TextureProcessor
var total = basicSize + advancedSize + samplerSize; var total = basicSize + advancedSize + samplerSize;
Span<byte> buf = stackalloc byte[total]; Span<byte> buf = stackalloc byte[total];
var basic = s.Basic; var basic = settings.Basic;
var advanced = s.Advanced; var advanced = settings.Advanced;
var sampler = s.Sampler; var sampler = settings.Sampler;
MemoryMarshal.Write(buf, in basic); MemoryMarshal.Write(buf, in basic);
MemoryMarshal.Write(buf.Slice(basicSize), in advanced); MemoryMarshal.Write(buf.Slice(basicSize), in advanced);
MemoryMarshal.Write(buf.Slice(basicSize + advancedSize), in sampler); 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 ASSETS_FOLDER_NAME = "Assets";
public const string SOURCES_FOLDER_NAME = "Sources"; public const string SOURCES_FOLDER_NAME = "Sources";
public const string PACKAGES_FOLDER_NAME = "Packages"; 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"; public const string CONFIG_FOLDER_NAME = "Config";
private static IServiceProvider? s_serviceProvider; 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 AssetsFolderPath => Path.Combine(ProjectPath, ASSETS_FOLDER_NAME);
public static string SourcesFolderPath => Path.Combine(ProjectPath, SOURCES_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 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 string ConfigFolderPath => Path.Combine(ProjectPath, CONFIG_FOLDER_NAME);
public static DispatcherQueue DispatcherQueue public static DispatcherQueue DispatcherQueue

View File

@@ -3,7 +3,8 @@
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework> <TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion> <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Ghost.Editor.Core</RootNamespace> <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> <UseWinUI>true</UseWinUI>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion> <SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
@@ -13,8 +14,9 @@
<langversion>preview</langversion> <langversion>preview</langversion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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.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="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" /> <PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />

View File

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

View File

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

View File

@@ -219,7 +219,11 @@ public static class Logger
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Error(object? message) 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] [StackTraceHidden]
@@ -227,13 +231,21 @@ public static class Logger
public static void Error(string message) public static void Error(string message)
{ {
s_logger.Log(message, LogLevel.Error); s_logger.Log(message, LogLevel.Error);
#if DEBUG
System.Diagnostics.Debug.Fail(message);
#endif
} }
[StackTraceHidden] [StackTraceHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Error(string format, params object?[] args) 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] [StackTraceHidden]
@@ -241,6 +253,10 @@ public static class Logger
public static void Error(Exception ex) public static void Error(Exception ex)
{ {
s_logger.Log(ex); s_logger.Log(ex);
#if DEBUG
System.Diagnostics.Debug.Fail(ex.Message);
#endif
} }
[StackTraceHidden] [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.Engine.RenderPipeline;
using Ghost.Entities;
using Ghost.Graphics; using Ghost.Graphics;
using Misaki.HighPerformance.Jobs; using Misaki.HighPerformance.Jobs;
namespace Ghost.Engine; namespace Ghost.Engine;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal class EngineEntryAttribute : Attribute
{
}
[EngineEntry]
public sealed partial class EngineCore : IDisposable public sealed partial class EngineCore : IDisposable
{ {
private readonly JobScheduler _jobScheduler; private readonly JobScheduler _jobScheduler;
@@ -27,12 +20,11 @@ public sealed partial class EngineCore : IDisposable
{ {
FrameBufferCount = 2, FrameBufferCount = 2,
GraphicsAPI = GraphicsAPI.Direct3D12, 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); _renderSystem = new RenderSystem(renderingConfig);
ComponentRegistry.GetOrRegisterComponentID<ManagedEntityRef>();
} }
public void Dispose() public void Dispose()

View File

@@ -9,6 +9,7 @@ internal unsafe class GPUScene : IDisposable
private readonly IResourceDatabase _resourceDatabase; private readonly IResourceDatabase _resourceDatabase;
private Handle<GPUBuffer> _sceneBuffer; private Handle<GPUBuffer> _sceneBuffer;
private Handle<GPUBuffer> _instanceCounterBuffer;
private uint _instanceCount; private uint _instanceCount;
private uint _capacity; private uint _capacity;
@@ -16,6 +17,8 @@ internal unsafe class GPUScene : IDisposable
private bool _disposed; private bool _disposed;
public Handle<GPUBuffer> SceneBuffer => _sceneBuffer; 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) internal GPUScene(IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, uint initialCount)
{ {
@@ -30,9 +33,20 @@ internal unsafe class GPUScene : IDisposable
HeapType = HeapType.Default, 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"); _sceneBuffer = _resourceAllocator.CreateBuffer(in bufferDesc, "SceneBuffer");
Logger.DebugAssert(_sceneBuffer.IsValid, "Failed to create GPUScene buffer."); 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; _capacity = initialCount;
} }
@@ -55,7 +69,7 @@ internal unsafe class GPUScene : IDisposable
{ {
Size = newCapacity * (ulong)sizeof(InstanceData), Size = newCapacity * (ulong)sizeof(InstanceData),
Stride = (uint)sizeof(InstanceData), Stride = (uint)sizeof(InstanceData),
Usage = BufferUsage.Structured | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource, Usage = BufferUsage.Raw | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource,
HeapType = HeapType.Default, HeapType = HeapType.Default,
}; };
@@ -88,7 +102,7 @@ internal unsafe class GPUScene : IDisposable
{ {
if (index < 0 || index >= _capacity) 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. // 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(_sceneBuffer.AsResource());
_resourceDatabase.ReleaseResource(_instanceCounterBuffer.AsResource());
_disposed = true; _disposed = true;
GC.SuppressFinalize(this); GC.SuppressFinalize(this);

View File

@@ -2,8 +2,11 @@ using Ghost.Core;
using Ghost.Core.Graphics; using Ghost.Core.Graphics;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
namespace Ghost.Engine.RenderPipeline; namespace Ghost.Engine.RenderPipeline;
[GenerateShaderProperty("Internal/UpdateGPUScene")] [GenerateShaderProperty("Internal/UpdateGPUScene")]
@@ -18,8 +21,96 @@ public partial struct UpdateGPUSceneShaderProperty
internal partial class GhostRenderPipeline 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) if (addCount <= 0 && removeCount <= 0)
{ {
Logger.DebugAssert(addBuffer.IsInvalid && removeBuffer.IsInvalid, "Buffers should be invalid when there are no updates."); 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. // 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(); var keywords = new LocalKeywordSet();
ctx.DispatchCompute(shader, 0, in keywords, in property, new uint3()); 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;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RenderGraphModule; using Ghost.Graphics.RenderGraphModule;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
using System.Diagnostics;
namespace Ghost.Engine.RenderPipeline; namespace Ghost.Engine.RenderPipeline;
@@ -15,7 +11,7 @@ internal partial class GhostRenderPipeline : IRenderPipeline
private struct AddInstanceData private struct AddInstanceData
{ {
public float4x4 localToWorld; public float4x4 localToWorld;
public uint instanceId; public uint instanceID;
public uint meshBuffer; public uint meshBuffer;
public uint materialPalette; public uint materialPalette;
public uint renderingLayerMask; public uint renderingLayerMask;
@@ -24,8 +20,8 @@ internal partial class GhostRenderPipeline : IRenderPipeline
private struct RemoveInstanceData private struct RemoveInstanceData
{ {
public uint instanceId; public uint instanceID;
public uint swapWithInstanceId; public uint swapWithInstanceID;
} }
private readonly RenderSystem _renderSystem; 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 _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) public void Render(RenderContext ctx, int frameIndex, IRenderPayload payload)
{ {
var ghostPayload = (GhostRenderPayload)payload; var ghostPayload = (GhostRenderPayload)payload;
var resourceManager = _renderSystem.ResourceManager;
var resourceDatabase = _renderSystem.GraphicsEngine.ResourceDatabase;
foreach (ref readonly var request in ghostPayload.RenderRequests) foreach (ref readonly var request in ghostPayload.RenderRequests)
{ {
try 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); RenderPipelineUtility.GetVPMatrices(in request, viewData.ScreenSize, out var view, out var projection);
var addBuffer = CreateAddInstanceBuffer(ghostPayload, resourceManager, resourceDatabase, out var addCount); UpdateGPUScene(ctx, ghostPayload);
var removeBuffer = CreateRemoveInstanceBuffer(ghostPayload, resourceManager, resourceDatabase, out var removeCount);
UpdateGPUScene(ctx, addBuffer, addCount, removeBuffer, removeCount);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -30,10 +30,13 @@ internal sealed class GhostRenderPayload : IRenderPayload
private readonly ConcurrentQueue<AddInstanceRequest> _addRequest; private readonly ConcurrentQueue<AddInstanceRequest> _addRequest;
private readonly ConcurrentQueue<RemoveInstanceRequest> _removeRequest; private readonly ConcurrentQueue<RemoveInstanceRequest> _removeRequest;
private uint _instanceCount;
public ReadOnlySpan<RenderRequest> RenderRequests => _renderRequests; public ReadOnlySpan<RenderRequest> RenderRequests => _renderRequests;
public ConcurrentQueue<AddInstanceRequest> AddRequest => _addRequest; public ConcurrentQueue<AddInstanceRequest> AddRequest => _addRequest;
public ConcurrentQueue<RemoveInstanceRequest> RemoveRequest => _removeRequest; public ConcurrentQueue<RemoveInstanceRequest> RemoveRequest => _removeRequest;
public uint InstanceCount => _instanceCount;
public GhostRenderPayload(GhostRenderPipeline renderPipeline) public GhostRenderPayload(GhostRenderPipeline renderPipeline)
{ {
@@ -53,6 +56,7 @@ internal sealed class GhostRenderPayload : IRenderPayload
public uint AddInstance(float4x4 ltw, ref readonly MeshInstance meshInstance) public uint AddInstance(float4x4 ltw, ref readonly MeshInstance meshInstance)
{ {
var index = _renderPipeline.GPUScene.AddInstance(); var index = _renderPipeline.GPUScene.AddInstance();
_addRequest.Enqueue(new AddInstanceRequest { instanceId = index, localToWorld = ltw, meshInstance = meshInstance }); _addRequest.Enqueue(new AddInstanceRequest { instanceId = index, localToWorld = ltw, meshInstance = meshInstance });
return index; return index;
} }
@@ -60,12 +64,18 @@ internal sealed class GhostRenderPayload : IRenderPayload
public void RemoveInstance(uint instanceId) public void RemoveInstance(uint instanceId)
{ {
var swapWithInstanceId = _renderPipeline.GPUScene.RemoveInstance(instanceId); var swapWithInstanceId = _renderPipeline.GPUScene.RemoveInstance(instanceId);
if (swapWithInstanceId != ~0u) if (swapWithInstanceId != uint.MaxValue)
{ {
_removeRequest.Enqueue(new RemoveInstanceRequest { instanceId = instanceId, swapWithInstanceId = swapWithInstanceId }); _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() public void Reset()
{ {
_renderRequests.Clear(); _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; namespace Ghost.Engine.Systems;
[UpdateAfter<RemoveGPUInstanceSystem>]
[RenderPipelineSystem<GhostRenderPipelineSettings>] [RenderPipelineSystem<GhostRenderPipelineSettings>]
internal class AddGPUInstanceSystem : SystemBase internal class AddGPUInstanceSystem : SystemBase
{ {
@@ -48,5 +49,7 @@ internal class AddGPUInstanceSystem : SystemBase
systemAPI.World.EntityCommandBuffer.AddComponent(entity, new GPUInstanceRef { gpuSceneIndex = index }); 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(); internal static readonly Dictionary<int, Type> s_runtimeIDToType = new();
#endif #endif
static ComponentRegistry()
{
GetOrRegisterComponentID<ManagedEntityRef>();
}
public static unsafe Identifier<IComponent> GetOrRegisterComponentID<T>() public static unsafe Identifier<IComponent> GetOrRegisterComponentID<T>()
where T : unmanaged, IComponent where T : unmanaged, IComponent
{ {

View File

@@ -424,7 +424,7 @@ public unsafe partial class EntityManager : IDisposable
// Remove Managed Entities first // Remove Managed Entities first
// RemoveManagedEntity(rowIndicesCache.AsSpan(), in prevArchetype, prevChunkIndex); // 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 // Execute the hole-filling/swap logic
prevArchetype.RemoveEntities(prevChunkIndex, rowIndicesCache.AsSpan()); prevArchetype.RemoveEntities(prevChunkIndex, rowIndicesCache.AsSpan());

View File

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

View File

@@ -1,5 +1,6 @@
using Ghost.Core; using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using System.Runtime.InteropServices;
namespace Ghost.Entities; namespace Ghost.Entities;
@@ -201,6 +202,7 @@ public abstract class SystemGroup : ISystem
// _systems = SystemGroupRegistry.GetSystemsForGroup(GetType()); // _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) private static List<ISystem> Sort(List<ISystem> systems)
{ {
// 1. Build the Graph // 1. Build the Graph
@@ -211,10 +213,10 @@ public abstract class SystemGroup : ISystem
foreach (var sys in systems) foreach (var sys in systems)
{ {
var type = sys.GetType(); 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 = []; value = new HashSet<Type>();
dependencies[type] = value;
} }
// Handle [UpdateAfter(typeof(Other))] -> Other comes before This // 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)) foreach (var attr in type.GetCustomAttributes(typeof(UpdateBeforeAttribute), true))
{ {
var targetType = ((UpdateBeforeAttribute)attr).SystemType; var targetType = ((UpdateBeforeAttribute)attr).SystemType;
if (!dependencies.ContainsKey(targetType)) dependencies[targetType] = []; ref var targetDeps = ref CollectionsMarshal.GetValueRefOrAddDefault(dependencies, targetType, out exists);
dependencies[targetType].Add(type); 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) foreach (var sys in systems)
{ {
var type = sys.GetType(); 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 // Check if all dependencies for this system are already visited/sorted
var canRun = true; var canRun = true;
@@ -297,7 +307,7 @@ public abstract class SystemGroup : ISystem
if (_systems.Count == 0) if (_systems.Count == 0)
{ {
_sortedSystems = []; _sortedSystems = new List<ISystem>();
_sortedVersion = _version; _sortedVersion = _version;
return; 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() private readonly void ThrowIfNotCreated()
{ {
if (!IsCreated) if (!IsCreated)

View File

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

View File

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

View File

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

View File

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