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:
77
src/.github/agents/code-executor.agent.md
vendored
Normal file
77
src/.github/agents/code-executor.agent.md
vendored
Normal 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.
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,129 +183,11 @@ 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);
|
||||||
return cachePath;
|
await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
return cachePath;
|
||||||
{
|
|
||||||
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)
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
namespace Ghost.Core.Utilities;
|
|
||||||
|
|
||||||
internal class EnumUtility
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
37
src/Runtime/Ghost.Engine/Shaders/UpdateGPUScene.gcomp
Normal file
37
src/Runtime/Ghost.Engine/Shaders/UpdateGPUScene.gcomp
Normal 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";
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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'">
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -55,13 +55,13 @@ struct MeshData
|
|||||||
BYTE_ADDRESS_BUFFER vertexBuffer;
|
BYTE_ADDRESS_BUFFER vertexBuffer;
|
||||||
float3 worldBoundsMax;
|
float3 worldBoundsMax;
|
||||||
BYTE_ADDRESS_BUFFER indexBuffer;
|
BYTE_ADDRESS_BUFFER indexBuffer;
|
||||||
|
|
||||||
BYTE_ADDRESS_BUFFER meshletBuffer;
|
BYTE_ADDRESS_BUFFER meshletBuffer;
|
||||||
BYTE_ADDRESS_BUFFER meshletVerticesBuffer;
|
BYTE_ADDRESS_BUFFER meshletVerticesBuffer;
|
||||||
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);
|
||||||
|
|||||||
@@ -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++)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user