feat(engine)!: refactor graphics, ECS, and logging APIs

Major refactor of graphics and ECS infrastructure:
- Removed IResourceManager, IRenderSystem, IFenceSynchronizer interfaces; ResourceManager and RenderSystem are now concrete classes.
- Updated all render graph, pipeline, and context code to use concrete ResourceManager.
- Refactored camera/frustum math and render extraction for clarity and correctness; frustum now uses inline arrays.
- RenderingLayerMask is now an immutable struct with bitwise operators.
- Meshlet and meshlet group data structures improved; meshlet build callback signature updated.
- Logging system overhauled: LogMessage is now a class, LogCollection supports change events, and Logger is used directly in the debug console.
- ECS query API: ChunkView.Count renamed to EntityCount; query builder/iterators use VirtualStack.Scope.
- Updated render pipeline and passes for new resource manager and render list APIs.
- Cleaned up obsolete files, improved code style, and updated documentation.
- HLSL meshlet shader updated for new struct layout.
- Debug console now uses new logger and log collection.

BREAKING CHANGE: Public APIs for resource management, rendering, ECS queries, and logging have changed. Interfaces removed; use new concrete types and updated method signatures.
This commit is contained in:
2026-03-21 22:10:28 +09:00
parent 793df1af4f
commit 37f4795b4f
45 changed files with 1007 additions and 840 deletions

212
AGENT.md
View File

@@ -1,212 +0,0 @@
# GhostEngine — Agent Guidelines
## Repository Overview
GhostEngine is a C# game engine targeting .NET 10 / Windows, built around:
- **ECS runtime** (`Ghost.Entities`, `Ghost.Core`) — high-performance, AOT-compatible
- **Graphics** (`Ghost.Graphics`, `Ghost.Graphics.RHI`, `Ghost.Graphics.D3D12`) — D3D12 RHI
- **Editor** (`Ghost.Editor`, `Ghost.Editor.Core`, `Ghost.DSL`, `Ghost.Data`) — WinUI 3 (WindowsAppSDK)
- **Third-party bindings** (`Ghost.FMOD`, `Ghost.MeshOptimizer`, `Ghost.Nvtt`, `Ghost.Ufbx`)
- **Tools** (`Ghost.NativeWrapperGen`)
Solution file: `src/GhostEngine.slnx`
All commands below should be run from the `src/` directory unless noted.
---
## Build Commands
```shell
# Build entire solution (x64, Debug)
dotnet build GhostEngine.slnx -c Debug -p:Platform=x64
# Build entire solution (Release)
dotnet build GhostEngine.slnx -c Release -p:Platform=x64
# Build a single project
dotnet build Runtime/Ghost.Entities/Ghost.Entities.csproj
# Clean
dotnet clean GhostEngine.slnx
```
> **Note:** Editor projects (`Ghost.Editor`, `Ghost.Editor.Core`) require
> `net10.0-windows10.0.22621.0` and the Windows App SDK. They will only build
> on a Windows machine with the correct SDK installed. Platform-agnostic runtime
> and test projects target plain `net10.0`.
---
## Test Commands
There are two test frameworks in use:
### MSTest — `Ghost.UnitTest`
Standard `dotnet test` runner. Tests are parallelized at method level by default.
Currently the integration test file is `#if false`-guarded until the asset
service is fully wired.
```shell
# Run all MSTest tests
dotnet test Test/Ghost.UnitTest/Ghost.UnitTest.csproj -c Debug -p:Platform=x64
# Run a single test method by name
dotnet test Test/Ghost.UnitTest/Ghost.UnitTest.csproj \
--filter "FullyQualifiedName~TestAutoMetaGeneration_WhenFileCreated"
# Run a single test class
dotnet test Test/Ghost.UnitTest/Ghost.UnitTest.csproj \
--filter "ClassName~AssetDatabaseIntegrationTest"
```
### Custom TestRunner — `Ghost.MicroTest` / `Ghost.Entities.Test`
These are console executables driven by `Ghost.Test.Core.TestRunner`. There is
no `dotnet test` integration; run them directly:
```shell
# Micro tests (native binding smoke tests)
dotnet run --project Test/Ghost.MicroTest/Ghost.MicroTest.csproj
# ECS benchmarks / manual tests
dotnet run --project Test/Ghost.Entities.Test/Ghost.Entities.Test.csproj -c Release
```
To run a specific `ITest` implementation, edit `Program.cs` in the respective
project and call `TestRunner.Run<YourTestClass>()`.
---
## Code Style
### EditorConfig (enforced — `src/.editorconfig`)
- Max line length: **200**
- Opening braces always on a **new line** for all C# constructs
- Single-line statements and blocks are **preserved** (not force-expanded)
- **No** primary constructors (`csharp_style_prefer_primary_constructors = false`)
- `System.*` using directives are **not** sorted first
- Import directive groups are **not** separated by blank lines
- Collection expressions and collection initializer syntax are **disabled**
(`dotnet_style_prefer_collection_expression = false`)
### Language
- C# `latest` (runtime/test projects) or `preview` (editor projects, for `field`
keyword support in .NET 10)
- Nullable reference types: **enabled** everywhere (`<Nullable>enable</Nullable>`)
- Implicit usings: **enabled** (`<ImplicitUsings>enable</ImplicitUsings>`)
- Unsafe blocks: **enabled** where needed (ECS, graphics, native bindings)
### Namespaces & File Layout
- One type per file; file name matches type name exactly
- Namespace matches folder structure: `Ghost.<Module>[.<SubFolder>]`
- `partial` classes are split across files named `TypeName.Purpose.cs`
(e.g. `EntityManager.cs`, `EntityManager.Managed.cs`)
- `AssemblyInfo.cs` holds `[assembly: InternalsVisibleTo(...)]` and assembly
attributes; do not scatter these across regular source files
### Naming Conventions
| Symbol | Convention | Example |
|--------|-----------|---------|
| Private fields | `_camelCase` | `_jobScheduler` |
| Private static fields | `s_camelCase` | `s_worlds`, `s_logger` |
| Constants (public/private) | `UPPER_SNAKE_CASE` | `ASSET_EXTENSION`, `ASSETS_FOLDER_NAME` |
| Properties & public members | `PascalCase` | `EntityManager`, `IsSuccess` |
| Local variables / params | `camelCase` | `entityCapacity`, `signatureHash` |
| Interfaces | `I` prefix | `IComponent`, `ISystem`, `ITest` |
| Generic type parameters | `T`, `TKey`, `TValue`, `E` | |
| Type-tagged structs (handles) | Generic param encodes context | `Handle<T>`, `Identifier<T>`, `Key64<T>` |
### Types & Structs
- Prefer `readonly struct` for value types that are logically immutable.
- Prefer `ref struct` / `readonly ref struct` for stack-only types
(`RefResult<T,E>`, `SystemAPI`, `ChunkView`).
- Use `partial class` to split large classes by concern.
- Avoid primary constructors (disabled by editorconfig).
- Use the `field` keyword (preview feature) for auto-property backing fields
where it simplifies code — only in editor projects that opt in via
`<langversion>preview</langversion>`.
### Imports
- `using` directives at the top of each file, before the `namespace` declaration.
- No blank line between `using` groups (enforced by editorconfig).
- `System.*` namespaces may appear in any order alongside project namespaces.
- Prefer specific `using` imports over global usings for clarity in low-level
performance-critical files.
### Error Handling
- **Return `Result` / `Result<T>` instead of throwing** for expected failures
(file-not-found, invalid args, etc.).
`Result.Success()` / `Result.Failure(message)` or `Result.Failure(Error.XXX)`.
- Use the typed `Error` enum (`Ghost.Core.Error`) for structured error codes.
- Use `result.ThrowIfFailed()` / `result.GetValueOrThrow()` extension methods at
call sites that want throw-on-failure semantics.
- **Throw exceptions** only for programming errors / invariant violations
(corrupt state, null-ref on internal APIs).
- In performance-critical paths, guard validation behind `#if DEBUG || GHOST_EDITOR`
to eliminate overhead in release builds.
- `Logger.LogError(...)` / `Logger.LogWarning(...)` for non-fatal operational
issues; do not use `Console.WriteLine` in production library code.
### Performance Patterns
- Annotate hot paths with `[MethodImpl(MethodImplOptions.AggressiveInlining)]`.
- Annotate log/assert helpers with `[StackTraceHidden]`.
- Prefer `stackalloc` + `Span<T>` over heap allocation for small temporary arrays.
- Use the `Misaki.HighPerformance.*` allocation APIs (`AllocationManager`,
`UnsafeList<T>`, `UnsafeHashMap<T,V>`, etc.) for long-lived unmanaged buffers.
- All runtime/ECS types must be AOT-compatible and trimmable (set
`<IsAotCompatible>True</IsAotCompatible>` and `<IsTrimmable>True</IsTrimmable>`
in Release config).
- Avoid LINQ in hot paths; use `for` loops or `foreach` over `Span<T>`.
### Attributes & Extensibility
- Custom attributes for editor extension points inherit from
`DiscoverableAttributeBase` (discovered at startup via `TypeCache`).
- Use `[UpdateAfter(typeof(X))]` / `[UpdateBefore(typeof(X))]` to declare
`ISystem` ordering dependencies; `SystemGroup.SortSystems()` topologically sorts
them at startup.
- Use `[EditorInjection(ServiceLifetime.Singleton)]` to register editor services
via DI without manual wiring.
### XML Documentation
- All public API surface should have `<summary>` doc-comments.
- Use `<remarks>` for non-obvious behavior or threading constraints.
- Document thread-safety expectations explicitly (see `EntityCommandBuffer` /
`AssetRegistry` as reference).
### Preprocessor Defines
| Define | Meaning |
|--------|---------|
| `DEBUG` | Standard debug build |
| `GHOST_EDITOR` | Editor build (extra validation, reflection helpers) |
| `PLATEFORME_WIN64` | Windows 64-bit platform target |
---
## Project Structure
```
src/
GhostEngine.slnx # Solution
.editorconfig # Formatting rules
Runtime/
Ghost.Core/ # Core types: Result, Handle, Logger, math helpers
Ghost.Engine/ # Engine entry point & loop
Ghost.Entities/ # ECS: World, Entity, Component, System
Ghost.Generator/ # Source generators
Ghost.Graphics/ # High-level graphics API
Ghost.Graphics.RHI/ # Render hardware interface abstractions
Ghost.Graphics.D3D12/ # D3D12 backend
Editor/
Ghost.Editor/ # WinUI 3 shell
Ghost.Editor.Core/ # Editor services, asset registry, inspector
Ghost.DSL/ # Shader DSL compiler
Ghost.Data/ # Serialization / project data models
ThridParty/ # Native binding wrappers (FMOD, MeshOptimizer, Nvtt, Ufbx)
Test/
Ghost.Test.Core/ # Shared ITest / TestRunner infrastructure
Ghost.UnitTest/ # MSTest integration tests
Ghost.MicroTest/ # Native binding smoke tests (console app)
Ghost.Entities.Test/ # ECS benchmarks (BenchmarkDotNet, console app)
Ghost.Shader.Test/ # Shader DSL manual tests (console app)
Tools/
Ghost.NativeWrapperGen/ # Code-gen tool for native wrappers
```

View File

@@ -1,20 +0,0 @@
# Julian's Workspace: GhostEngine
Misaki has set up my environment to work from my own fork of "GhostEngine." Here's how I'll manage my workflow:
## What I'll Do
1. Write, commit, and push code changes directly to my local fork.
2. Use the "tea" CLI tool when I'm ready to create PRs back to the original repository.
## Tools & Resources
- **Gitea:** The repository is hosted here. I can authenticate directly.
- **Tea CLI:** Details for creating PRs can be found in the `gitea-pr` and `gitea` skill documentation.
## Collaboration Rules
We'll refine separately; I'll merge when...
...the functionality is complete and tested.
**PR Description Tip:** write everything in the description, do not include concise changelog markdown.
Also, I should not include AGENT.md and README_julian.md in the PR.

View File

@@ -1,6 +1,7 @@
using Ghost.Editor.Core.Utilities;
using Ghost.Editor.Models;
using Ghost.Engine;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Reflection;
namespace Ghost.Editor;
@@ -53,10 +54,19 @@ internal static class ActivationHandler
public static async Task HandleAsync(LaunchArguments args)
{
var opts = new AllocationManagerInitOpts
{
ArenaCapacity = 1024 * 1024 * 1024, // 1 GB. Arena using virtual memory, so this is just a reservation and won't actually consume physical memory until used.
StackCapacity = 1024 * 1024 * 32, // 32 MB. Stack using virtual memory, so this is just a reservation and won't actually consume physical memory until used.
FreeListConcurrencyLevel = Environment.ProcessorCount
};
AllocationManager.Initialize(opts);
await Task.Run(() =>
{
TypeCache.Init();
((EngineCore)App.GetService<IEngineContext>()).Init();
App.GetService<EngineCore>();
});
// await ((Core.AssetHandle.AssetService)App.GetService<IAssetService>()).Init();

View File

@@ -57,7 +57,7 @@ public partial class App : Application
UseContentRoot(AppContext.BaseDirectory).
ConfigureServices((context, services) =>
{
services.AddSingleton<IEngineContext, EngineCore>();
services.AddSingleton<EngineCore>();
services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<IProgressService, ProgressService>();

View File

@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
namespace Ghost.Core;
@@ -7,10 +8,11 @@ public enum LogLevel
{
Info,
Warning,
Error
Error,
Debug
}
public readonly struct LogMessage
public class LogMessage
{
public LogLevel Level
{
@@ -51,17 +53,38 @@ public readonly struct LogMessage
}
}
public sealed class LogCollection : ReadOnlyObservableCollection<LogMessage>
{
public LogCollection(ObservableCollection<LogMessage> list)
: base(list)
{
}
public event NotifyCollectionChangedEventHandler? LogChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
base.OnCollectionChanged(args);
LogChanged?.Invoke(this, args);
}
}
public interface ILogger
{
ReadOnlyObservableCollection<LogMessage> Logs
LogCollection Logs
{
get;
}
public bool CaptureStackTrace
{
get; set;
}
void Log(string message, LogLevel level);
void Log(Exception exception);
void Assert(bool condition, string message);
void Clear();
void Clear(bool includeFile = false);
}
public static class Logger
@@ -70,14 +93,19 @@ public static class Logger
private class LoggerImpl : ILogger
{
private readonly ObservableCollection<LogMessage> _logs = new();
private readonly ReadOnlyObservableCollection<LogMessage> _readOnly;
private readonly LogCollection _readOnly;
private readonly Lock _lock = new();
public ReadOnlyObservableCollection<LogMessage> Logs => _readOnly;
public LogCollection Logs => _readOnly;
public bool CaptureStackTrace
{
get; set;
} = true;
public LoggerImpl()
{
_readOnly = new ReadOnlyObservableCollection<LogMessage>(_logs);
_readOnly = new LogCollection(_logs);
}
[StackTraceHidden]
@@ -85,7 +113,8 @@ public static class Logger
{
lock (_lock)
{
_logs.Add(new LogMessage(level, message));
var stackTrace = CaptureStackTrace ? new StackTrace(true).ToString() : null;
_logs.Add(new LogMessage(level, message, stackTrace));
}
}
@@ -100,17 +129,14 @@ public static class Logger
[StackTraceHidden]
public void Assert(bool condition, string message)
{
lock (_lock)
{
if (!condition)
{
Log(message, LogLevel.Error);
}
}
}
public void Clear()
public void Clear(bool includeFile = false)
{
lock (_lock)
{
@@ -119,9 +145,10 @@ public static class Logger
}
}
private static readonly ILogger s_logger = new LoggerImpl();
private static readonly LoggerImpl s_logger = new LoggerImpl();
public static ReadOnlyObservableCollection<LogMessage> Logs => s_logger.Logs;
public static ILogger Impl => s_logger;
public static LogCollection Logs => s_logger.Logs;
[StackTraceHidden]
public static void Log(LogLevel level, object? message)
@@ -207,8 +234,27 @@ public static class Logger
s_logger.Assert(condition, message);
}
public static void Clear()
[StackTraceHidden]
[Conditional("DEBUG")]
[Conditional("GHOST_EDITOR")]
public static void Debug(object? message)
{
s_logger.Clear();
s_logger.Log(message?.ToString() ?? "null", LogLevel.Debug);
}
[StackTraceHidden]
[Conditional("DEBUG")]
[Conditional("GHOST_EDITOR")]
public static void Debug(string message)
{
s_logger.Log(message, LogLevel.Debug);
}
[StackTraceHidden]
[Conditional("DEBUG")]
[Conditional("GHOST_EDITOR")]
public static void Debug(string format, params object?[] args)
{
s_logger.Log(string.Format(format, args), LogLevel.Debug);
}
}

View File

@@ -11,13 +11,13 @@ public unsafe struct Camera : IComponent
public float nearClipPlane;
public float farClipPlane;
public float2 sensorSize;
public float2 sensorSize; // mm
public GateFit gateFit;
public float iso;
public float shutterSpeed;
public float aperture;
public float focalLength;
public float focusDistance;
public float focalLength; // mm
public float focusDistance; // m
public RenderingLayerMask renderingLayerMask;
@@ -29,5 +29,5 @@ public unsafe struct Camera : IComponent
// TODO: Add more render targets like motion vector, etc.
// Custim render function. If it's not null, the render system will call this function instead of the default render pipeline.
public delegate*<ref readonly RenderingContext, ref readonly RenderRequest, void> renderFunc;
public delegate*<ref readonly RenderContext, ref readonly RenderRequest, void> renderFunc;
}

View File

@@ -8,7 +8,7 @@ public struct MeshInstance : IComponent
{
public Handle<Mesh> mesh;
public Identifier<MaterialPalette> materialPalette;
public ShadowCastingMode shadowCastingMode;
public RenderingLayerMask renderingLayerMask;
public ShadowCastingMode shadowCastingMode;
public bool staticShadowCaster;
}

View File

@@ -107,7 +107,7 @@ public static class SceneManager
var entities = chunk.GetEntities();
var sceneIDs = chunk.GetComponentData<Components.SceneID>();
for (var i = 0; i < chunk.Count; i++)
for (var i = 0; i < chunk.EntityCount; i++)
{
if (sceneIDs[i].scene.ID == scene.ID)
{
@@ -140,7 +140,7 @@ public static class SceneManager
var chunkEntities = chunk.GetEntities();
var sceneIDs = chunk.GetComponentData<Components.SceneID>();
for (var i = 0; i < chunk.Count; i++)
for (var i = 0; i < chunk.EntityCount; i++)
{
if (sceneIDs[i].scene.ID == scene.ID)
{

View File

@@ -4,31 +4,24 @@ using Misaki.HighPerformance.Jobs;
namespace Ghost.Engine;
public interface IEngineContext : IDisposable
{
IJobScheduler JobScheduler { get; }
IRenderSystem RenderSystem { get; }
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal class EngineEntryAttribute : Attribute
{
}
[EngineEntry]
internal sealed partial class EngineCore : IEngineContext
public sealed partial class EngineCore : IDisposable
{
private readonly JobScheduler _jobScheduler;
private readonly RenderSystem _renderSystem;
public IJobScheduler JobScheduler => _jobScheduler;
public IRenderSystem RenderSystem => _renderSystem;
public JobScheduler JobScheduler => _jobScheduler;
public RenderSystem RenderSystem => _renderSystem;
public EngineCore()
internal EngineCore()
{
_jobScheduler = new JobScheduler(Environment.ProcessorCount - 2); // We -2 here, one for main thread, one for render thread
// TODO: Remove the windows dependency from RenderSystem.
var renderingConfig = new RenderSystemDesc
{
FrameBufferCount = 2,
@@ -40,10 +33,6 @@ internal sealed partial class EngineCore : IEngineContext
ComponentRegistry.GetOrRegisterComponentID<ManagedEntityRef>();
}
public void Init()
{
}
public void Dispose()
{
_jobScheduler.Dispose();

View File

@@ -2,55 +2,241 @@ using Ghost.Core;
using Ghost.Engine.Components;
using Ghost.Entities;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Mathematics.Geometry;
namespace Ghost.Engine.Systems;
public class RenderExtractionSystem : ISystem
{
private Identifier<EntityQuery> _queryID;
private IGraphicsEngine _graphicsEngine = null!;
private Identifier<EntityQuery> _cameraQueryID;
private Identifier<EntityQuery> _meshQueryID;
public void Initialize(ref readonly SystemAPI systemAPI)
{
_queryID = new QueryBuilder()
_graphicsEngine = systemAPI.World.GetResource<IGraphicsEngine>();
var builder = new QueryBuilder();
_cameraQueryID = builder
.WithAll<Camera, LocalToWorld>()
.Build(systemAPI.World, false);
_meshQueryID = builder
.WithAll<MeshInstance, LocalToWorld>()
.Build(systemAPI.World);
.Build(systemAPI.World, true);
}
public void Update(ref readonly SystemAPI systemAPI)
private static float3 IntersectFrustumPlanes(float4 p0, float4 p1, float4 p2)
{
if (_queryID.IsInvalid)
float3 n0 = p0.xyz;
float3 n1 = p1.xyz;
float3 n2 = p2.xyz;
float det = math.dot(math.cross(n0, n1), n2);
return (math.cross(n2, n1) * p0.w + math.cross(n0, n2) * p1.w - math.cross(n0, n1) * p2.w) * (1.0f / det);
}
private static Frustum CreateFrustum(Camera camRef, float4x4 vp, float3 viewDir, float3 viewPos)
{
var frustum = new Frustum();
Frustum.CalculateFrustumPlanes(vp, ref frustum.planes);
// We need to recalculate the near and far planes otherwise it does not work for oblique projection matrices used for reflection.
var nearPlane = Plane.CreateFromUnitNormalAndPointInPlane(viewDir, viewPos);
nearPlane.Distance -= camRef.nearClipPlane;
var farPlane = Plane.CreateFromUnitNormalAndPointInPlane(-viewDir, viewPos);
farPlane.Distance += camRef.farClipPlane;
frustum.planes[4] = nearPlane;
frustum.planes[5] = farPlane;
frustum.corners[0] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[3], frustum.planes[4]);
frustum.corners[1] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[3], frustum.planes[4]);
frustum.corners[2] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[2], frustum.planes[4]);
frustum.corners[3] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[2], frustum.planes[4]);
frustum.corners[4] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[3], frustum.planes[5]);
frustum.corners[5] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[3], frustum.planes[5]);
frustum.corners[6] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[2], frustum.planes[5]);
frustum.corners[7] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[2], frustum.planes[5]);
return frustum;
}
public unsafe void Update(ref readonly SystemAPI systemAPI)
{
if (_meshQueryID.IsInvalid)
{
return;
}
ref var query = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_queryID);
var renderList = new RenderList(1, 64, Allocator.Temp);
ref var cameraQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_cameraQueryID);
ref var meshQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_meshQueryID);
// TODO: We should extract the render record for each camera because different cameras may have different culling results.
foreach (var (cam, camLtw) in cameraQuery.GetComponentIterator<Camera, LocalToWorld>())
{
ref readonly var camRef = ref cam.Get();
ref readonly var camLtwRef = ref camLtw.Get();
var rtResult = _graphicsEngine.ResourceDatabase.GetResourceDescription(camRef.colorTarget.AsResource());
if (rtResult.IsFailure)
{
continue;
}
var rtSize = new uint2(rtResult.Value.TextureDescription.Width, rtResult.Value.TextureDescription.Height);
var aspectScreen = (float)rtSize.x / rtSize.y;
var renderList = new RenderList(1, 64, Allocator.Temp);
var shadowCasterRenderList = new RenderList(1, 64, Allocator.Temp);
// TODO: This chould be done in parallel jobs.
foreach (var chunk in query.GetChunkIterator())
foreach (var chunk in meshQuery.GetChunkIterator())
{
var meshInstances = chunk.GetComponentData<MeshInstance>();
var localToWorlds = chunk.GetComponentData<LocalToWorld>();
for (var i = 0; i < chunk.Count; i++)
for (var i = 0; i < chunk.EntityCount; i++)
{
ref readonly var meshInstance = ref meshInstances[i];
ref readonly var localToWorld = ref localToWorlds[i];
if ((meshInstance.renderingLayerMask & camRef.renderingLayerMask) == 0u)
{
continue;
}
ref readonly var meshLtw = ref localToWorlds[i];
var meshPosition = meshLtw.matrix.c3.xyz;
var camPosition = camLtwRef.matrix.c3.xyz;
var distance = math.distance(meshPosition, camPosition);
if (distance < camRef.nearClipPlane || distance > camRef.farClipPlane)
{
continue;
}
if (meshInstance.shadowCastingMode != ShadowCastingMode.ShadowsOnly)
{
renderList.Add(new RenderRecord
{
localToWorld = localToWorld.matrix,
localToWorld = meshLtw.matrix,
mesh = meshInstance.mesh,
materialPalette = meshInstance.materialPalette,
renderingLayerMask = meshInstance.renderingLayerMask,
}, 0);
}
if (meshInstance.shadowCastingMode != ShadowCastingMode.Off)
{
shadowCasterRenderList.Add(new RenderRecord
{
localToWorld = meshLtw.matrix,
mesh = meshInstance.mesh,
materialPalette = meshInstance.materialPalette,
renderingLayerMask = meshInstance.renderingLayerMask,
}, 0);
}
}
}
// TODO: Send render list to render pipeline.
// NOTE: We assume camera's scale is always (1, 1, 1). Otherwise fastinverse will fail and we need to use regular inverse which is more expensive.
var viewMatrix = math.fastinverse(camLtwRef.matrix);
var vfov = 2.0f * math.atan(camRef.sensorSize.y / 2.0f * camRef.focalLength);
var hfov = 2.0f * math.atan(camRef.sensorSize.x / 2.0f * camRef.focalLength);
var aspectSensor = camRef.sensorSize.x / camRef.sensorSize.y;
float vfovF;
switch (camRef.gateFit)
{
case GateFit.Vertical:
vfovF = vfov;
break;
case GateFit.Horizontal:
// Adjust VFOV so that the sensor width fits the screen width
var horizontalAspectBuffer = math.tan(hfov * 0.5f);
vfovF = 2.0f * math.atan(horizontalAspectBuffer / aspectScreen);
break;
case GateFit.Fill:
if (aspectSensor > aspectScreen)
{
goto case GateFit.Vertical;
}
else
{
goto case GateFit.Horizontal;
}
case GateFit.Overscan:
if (aspectSensor > aspectScreen)
{
goto case GateFit.Horizontal;
}
else
{
goto case GateFit.Vertical;
}
default:
vfovF = vfov;
break;
}
var m_00 = 1.0f / aspectScreen * math.tan(vfovF * 0.5f);
var m_11 = 1.0f / math.tan(vfovF * 0.5f);
var m_22 = -(camRef.farClipPlane + camRef.nearClipPlane) / (camRef.farClipPlane - camRef.nearClipPlane);
var m_23 = -(2.0f * camRef.farClipPlane * camRef.nearClipPlane) / (camRef.farClipPlane - camRef.nearClipPlane);
var projectionMatrix = new float4x4
(
m_00, 0, 0, 0,
0, m_11, 0, 0,
0, 0, m_22, m_23,
0, 0, -1, 0
);
var vp = math.mul(projectionMatrix, viewMatrix);
var viewDir = math.normalize(camLtwRef.matrix.c2.xyz);
var viewPos = camLtwRef.matrix.c3.xyz;
var frustum = CreateFrustum(camRef, vp, viewDir, viewPos);
// TODO: Send this to render pipeline.
var request = new RenderRequest
{
colorTarget = camRef.colorTarget,
depthTarget = camRef.depthTarget,
opaqueRenderList = renderList,
shadowCasterRenderList = shadowCasterRenderList,
transparentRenderList = default,
renderFunc = camRef.renderFunc,
view = new RenderView
{
viewMatrix = viewMatrix,
projectionMatrix = projectionMatrix,
position = camLtwRef.matrix.c3.xyz,
frustum = frustum,
nearClipPlane = camRef.nearClipPlane,
farClipPlane = camRef.farClipPlane,
sensorSize = camRef.sensorSize,
gateFit = camRef.gateFit,
iso = camRef.iso,
shutterSpeed = camRef.shutterSpeed,
aperture = camRef.aperture,
focalLength = camRef.focalLength,
focusDistance = camRef.focusDistance,
renderingLayerMask = camRef.renderingLayerMask,
},
};
}
}
public void Cleanup(ref readonly SystemAPI systemAPI)

View File

@@ -96,7 +96,7 @@ public readonly unsafe ref struct ChunkView
private readonly int _structuralVersion;
private readonly int _currentVersion;
public readonly int Count => _entityCount;
public readonly int EntityCount => _entityCount;
internal ChunkView(ref readonly Archetype archetype, ref readonly Chunk chunk)
{
@@ -478,7 +478,7 @@ public unsafe partial struct EntityQuery : IDisposable
public ref partial struct QueryBuilder : IDisposable
{
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<Identifier<IComponent>> _all;
private UnsafeList<Identifier<IComponent>> _any;
@@ -666,6 +666,10 @@ public ref partial struct QueryBuilder : IDisposable
{
Dispose();
}
else
{
Clear();
}
return queryID;
}

View File

@@ -338,9 +338,9 @@ public abstract class SystemGroup : ISystem
}
}
public class DefaultSystemGroup : SystemGroup;
public sealed class DefaultSystemGroup : SystemGroup;
public class SystemManager
public sealed class SystemManager
{
private readonly World _world;

View File

@@ -21,7 +21,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -216,7 +216,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -423,7 +423,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -640,7 +640,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -867,7 +867,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -1104,7 +1104,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -1351,7 +1351,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -1608,7 +1608,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;

View File

@@ -56,7 +56,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;

View File

@@ -41,7 +41,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -245,7 +245,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -459,7 +459,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -683,7 +683,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -917,7 +917,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -1161,7 +1161,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -1415,7 +1415,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;
@@ -1679,7 +1679,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;

View File

@@ -60,7 +60,7 @@ public unsafe partial struct EntityQuery
private readonly EntityQueryMask _mask;
private readonly World _world;
private readonly Stack.Scope _scope;
private readonly VirtualStack.Scope _scope;
private UnsafeList<int> _changedComponentIDs;
private ref Archetype _currentArchetype;

View File

@@ -1,6 +1,7 @@
using Ghost.Core;
using Misaki.HighPerformance.Jobs;
using System.Runtime.CompilerServices;
using TerraFX.Interop.Windows;
namespace Ghost.Entities;
@@ -85,6 +86,8 @@ public partial class World : IDisposable, IEquatable<World>
private readonly ComponentManager _componentManager;
private readonly SystemManager _systemManager;
private readonly Dictionary<Type, object> _globalResource;
private int _version;
private bool _disposed = false;
@@ -137,6 +140,8 @@ public partial class World : IDisposable, IEquatable<World>
_componentManager = new ComponentManager(this);
_systemManager = new SystemManager(this);
_globalResource = new Dictionary<Type, object>();
if (jobScheduler != null)
{
_threadLocalECBs = new EntityCommandBuffer[jobScheduler.WorkerCount];
@@ -186,6 +191,41 @@ public partial class World : IDisposable, IEquatable<World>
return _threadLocalECBs[threadIndex];
}
/// <summary>
/// Registers or overwrites a global resource in the world.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetResource<T>(T resource)
where T : class
{
_globalResource[typeof(T)] = resource;
}
/// <summary>
/// Retrieves a global resource from the world.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T GetResource<T>()
where T : class
{
if (_globalResource.TryGetValue(typeof(T), out var resource))
{
return (T)resource;
}
throw new InvalidOperationException($"Resource of type {typeof(T).FullName} has not been registered in the World.");
}
/// <summary>
/// Checks if a global resource exists.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HasResource<T>()
where T : class
{
return _globalResource.ContainsKey(typeof(T));
}
public bool Equals(World? other)
{
return other is not null && _id == other._id;

View File

@@ -1,28 +0,0 @@
namespace Ghost.Graphics.RHI;
public interface IFenceSynchronizer
{
uint CPUFenceValue
{
get;
}
uint GPUFenceValue
{
get;
}
uint FrameIndex
{
get;
}
uint MaxFrameLatency
{
get;
}
bool WaitForGPUReady(int timeOut = -1);
void SignalCPUReady();
void WaitIdle();
}

View File

@@ -8,5 +8,5 @@ public interface IRenderPass
{
void Initialize(ref readonly RenderingContext ctx);
void Build(RenderGraph graph, Identifier<RGTexture> backbuffer);
void Cleanup(IResourceManager resourceManager, IResourceDatabase resourceDatabase);
void Cleanup(ResourceManager resourceManager, IResourceDatabase resourceDatabase);
}

View File

@@ -65,7 +65,7 @@ public struct Material : IResourceReleasable
get; set;
}
public Error SetShader(Identifier<Shader> shaderId, IResourceManager resourceManager, IResourceDatabase resourceDatabase, IResourceAllocator resourceAllocator)
public Error SetShader(Identifier<Shader> shaderId, ResourceManager resourceManager, IResourceDatabase resourceDatabase, IResourceAllocator resourceAllocator)
{
if (!shaderId.IsValid)
{
@@ -198,7 +198,7 @@ public struct Material : IResourceReleasable
_isDirty = true;
}
public Error SetKeyword(IResourceManager manager, int keywordId, bool enabled)
public Error SetKeyword(ResourceManager manager, int keywordId, bool enabled)
{
var r = manager.GetShaderReference(_shader);
if (r.IsFailure)
@@ -219,7 +219,7 @@ public struct Material : IResourceReleasable
return Error.None;
}
public readonly bool IsKeywordEnabled(IResourceManager manager, int keywordId)
public readonly bool IsKeywordEnabled(ResourceManager manager, int keywordId)
{
var r = manager.GetShaderReference(_shader);
if (r.IsFailure)

View File

@@ -6,11 +6,13 @@ using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Mathematics.Geometry;
using System.Runtime.InteropServices;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Graphics.Core;
[StructLayout(LayoutKind.Sequential)]
public struct Meshlet
{
public SphereBounds boundingSphere; // 16 bytes
@@ -25,6 +27,7 @@ public struct Meshlet
public byte lodLevel; // this meshlet's LOD level
}
[StructLayout(LayoutKind.Sequential)]
public struct MeshletGroup
{
public SphereBounds boundingSphere; // 16 bytes
@@ -35,6 +38,7 @@ public struct MeshletGroup
public uint lodLevel; // group LOD level
}
[StructLayout(LayoutKind.Sequential)]
public struct MeshletHierarchyNode
{
public SphereBounds boundingSphere; // 16 bytes
@@ -43,6 +47,7 @@ public struct MeshletHierarchyNode
public uint nodeData; // packed leaf/internal metadata
}
[StructLayout(LayoutKind.Sequential)]
public struct MeshletMeshData : IDisposable
{
public UnsafeList<Meshlet> meshlets;
@@ -63,14 +68,14 @@ public struct MeshletMeshData : IDisposable
}
}
// TODO: Support and meshlets.
public struct Mesh : IResourceReleasable
{
private UnsafeList<Vertex> _vertices;
private UnsafeList<uint> _indices;
private MeshletMeshData _meshletData;
public MeshletMeshData MeshletData => _meshletData;
[UnscopedRef]
public readonly ref readonly MeshletMeshData MeshletData => ref _meshletData;
internal bool IsMeshDataDirty
{
@@ -219,7 +224,7 @@ public struct Mesh : IResourceReleasable
};
// 2. Map Mesh to ClodMesh
ClodMesh clodMesh = new ClodMesh
var clodMesh = new ClodMesh
{
vertexPositions = (float*)_vertices.GetUnsafePtr(),
vertexCount = (nuint)_vertices.Count,
@@ -233,9 +238,9 @@ public struct Mesh : IResourceReleasable
MeshletUtility.Build(config, clodMesh, Unsafe.AsPointer(ref this), MeshletOutputCallback);
}
private static unsafe int MeshletOutputCallback(void* context, ClodGroup group, ClodCluster* clusters, nuint clusterCount)
private static unsafe int MeshletOutputCallback(void* context, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster>clusters)
{
Mesh* mesh = (Mesh*)context;
var mesh = (Mesh*)context;
ref var data = ref mesh->_meshletData;
// Ensure lists are initialized
@@ -247,12 +252,12 @@ public struct Mesh : IResourceReleasable
var meshletGroup = new MeshletGroup
{
meshletStartIndex = (uint)data.meshlets.Count,
meshletCount = (uint)clusterCount,
meshletCount = (uint)clusters.Count,
lodLevel = (uint)group.depth
};
data.groups.Add(meshletGroup);
for (nuint i = 0; i < clusterCount; i++)
for (var i = 0; i < clusters.Count; i++)
{
var cluster = clusters[i];

View File

@@ -17,28 +17,28 @@ public struct RenderList : IDisposable
{
public unsafe ref struct Enumerator
{
private readonly UnsafeList<RenderRecord>* pList;
private readonly int length;
private readonly UnsafeList<RenderRecord>* _pList;
private readonly int _length;
private int _listIndex;
private int _itemIndex;
internal Enumerator(RenderList List)
{
pList = (UnsafeList<RenderRecord>*)List._threadLocalRecords.GetUnsafePtr();
length = List._threadLocalRecords.Length;
_pList = (UnsafeList<RenderRecord>*)List._threadLocalRecords.GetUnsafePtr();
_length = List._threadLocalRecords.Length;
_listIndex = 0;
_itemIndex = -1;
}
public RenderRecord Current => pList[_listIndex][_itemIndex];
public RenderRecord Current => _pList[_listIndex][_itemIndex];
public bool MoveNext()
{
while (_listIndex < length)
while (_listIndex < _length)
{
if (_itemIndex < pList[_listIndex].Count)
if (_itemIndex < _pList[_listIndex].Count)
{
_itemIndex++;
return true;

View File

@@ -1,6 +1,7 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Mathematics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Graphics.Core;
@@ -11,35 +12,145 @@ public enum GateFit : uint
Horizontal,
Fill,
Overscan,
None
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct Frustum
{
// The data of the 6 planes of the frustum
public float3 normal0;
public float dist0;
public float3 normal1;
public float dist1;
public float3 normal2;
public float dist2;
public float3 normal3;
public float dist3;
public float3 normal4;
public float dist4;
public float3 normal5;
public float dist5;
[InlineArray(6)]
public struct plane_array
{
private float4 plane;
}
// The data of the 8 corners of the frustum
public float3 corner0;
public float3 corner1;
public float3 corner2;
public float3 corner3;
public float3 corner4;
public float3 corner5;
public float3 corner6;
public float3 corner7;
[InlineArray(8)]
public struct corner_array
{
private float3 corner;
}
public plane_array planes;
public corner_array corners;
public static void CalculateFrustumPlanes(float4x4 finalMatrix, ref plane_array outPlanes)
{
const int kPlaneFrustumLeft = 0;
const int kPlaneFrustumRight = 1;
const int kPlaneFrustumBottom = 2;
const int kPlaneFrustumTop = 3;
const int kPlaneFrustumNear = 4;
const int kPlaneFrustumFar = 5;
float4 tmpVec = default;
float4 otherVec = default;
tmpVec[0] = finalMatrix[0][3];
tmpVec[1] = finalMatrix[1][3];
tmpVec[2] = finalMatrix[2][3];
tmpVec[3] = finalMatrix[3][3];
otherVec[0] = finalMatrix[0][0];
otherVec[1] = finalMatrix[1][0];
otherVec[2] = finalMatrix[2][0];
otherVec[3] = finalMatrix[3][0];
// left & right
var leftNormalX = otherVec[0] + tmpVec[0];
var leftNormalY = otherVec[1] + tmpVec[1];
var leftNormalZ = otherVec[2] + tmpVec[2];
var leftDistance = otherVec[3] + tmpVec[3];
var leftDot = leftNormalX * leftNormalX + leftNormalY * leftNormalY + leftNormalZ * leftNormalZ;
var leftMagnitude = math.sqrt(leftDot);
var leftInvMagnitude = 1.0f / leftMagnitude;
leftNormalX *= leftInvMagnitude;
leftNormalY *= leftInvMagnitude;
leftNormalZ *= leftInvMagnitude;
leftDistance *= leftInvMagnitude;
outPlanes[kPlaneFrustumLeft].xyz = new float3(leftNormalX, leftNormalY, leftNormalZ);
outPlanes[kPlaneFrustumLeft].w = leftDistance;
var rightNormalX = -otherVec[0] + tmpVec[0];
var rightNormalY = -otherVec[1] + tmpVec[1];
var rightNormalZ = -otherVec[2] + tmpVec[2];
var rightDistance = -otherVec[3] + tmpVec[3];
var rightDot = rightNormalX * rightNormalX + rightNormalY * rightNormalY + rightNormalZ * rightNormalZ;
var rightMagnitude = math.sqrt(rightDot);
var rightInvMagnitude = 1.0f / rightMagnitude;
rightNormalX *= rightInvMagnitude;
rightNormalY *= rightInvMagnitude;
rightNormalZ *= rightInvMagnitude;
rightDistance *= rightInvMagnitude;
outPlanes[kPlaneFrustumRight].xyz = new float3(rightNormalX, rightNormalY, rightNormalZ);
outPlanes[kPlaneFrustumRight].w = rightDistance;
// bottom & top
otherVec[0] = finalMatrix[0][1];
otherVec[1] = finalMatrix[1][1];
otherVec[2] = finalMatrix[2][1];
otherVec[3] = finalMatrix[3][1];
var bottomNormalX = otherVec[0] + tmpVec[0];
var bottomNormalY = otherVec[1] + tmpVec[1];
var bottomNormalZ = otherVec[2] + tmpVec[2];
var bottomDistance = otherVec[3] + tmpVec[3];
var bottomDot = bottomNormalX * bottomNormalX + bottomNormalY * bottomNormalY + bottomNormalZ * bottomNormalZ;
var bottomMagnitude = math.sqrt(bottomDot);
var bottomInvMagnitude = 1.0f / bottomMagnitude;
bottomNormalX *= bottomInvMagnitude;
bottomNormalY *= bottomInvMagnitude;
bottomNormalZ *= bottomInvMagnitude;
bottomDistance *= bottomInvMagnitude;
outPlanes[kPlaneFrustumBottom].xyz = new float3(bottomNormalX, bottomNormalY, bottomNormalZ);
outPlanes[kPlaneFrustumBottom].w = bottomDistance;
var topNormalX = -otherVec[0] + tmpVec[0];
var topNormalY = -otherVec[1] + tmpVec[1];
var topNormalZ = -otherVec[2] + tmpVec[2];
var topDistance = -otherVec[3] + tmpVec[3];
var topDot = topNormalX * topNormalX + topNormalY * topNormalY + topNormalZ * topNormalZ;
var topMagnitude = math.sqrt(topDot);
var topInvMagnitude = 1.0f / topMagnitude;
topNormalX *= topInvMagnitude;
topNormalY *= topInvMagnitude;
topNormalZ *= topInvMagnitude;
topDistance *= topInvMagnitude;
outPlanes[kPlaneFrustumTop].xyz = new float3(topNormalX, topNormalY, topNormalZ);
outPlanes[kPlaneFrustumTop].w = topDistance;
// near & far
otherVec[0] = finalMatrix[0][2];
otherVec[1] = finalMatrix[1][2];
otherVec[2] = finalMatrix[2][2];
otherVec[3] = finalMatrix[3][2];
var nearNormalX = otherVec[0] + tmpVec[0];
var nearNormalY = otherVec[1] + tmpVec[1];
var nearNormalZ = otherVec[2] + tmpVec[2];
var nearDistance = otherVec[3] + tmpVec[3];
var nearDot = nearNormalX * nearNormalX + nearNormalY * nearNormalY + nearNormalZ * nearNormalZ;
var nearMagnitude = math.sqrt(nearDot);
var nearInvMagnitude = 1.0f / nearMagnitude;
nearNormalX *= nearInvMagnitude;
nearNormalY *= nearInvMagnitude;
nearNormalZ *= nearInvMagnitude;
nearDistance *= nearInvMagnitude;
outPlanes[kPlaneFrustumNear].xyz = new float3(nearNormalX, nearNormalY, nearNormalZ);
outPlanes[kPlaneFrustumNear].w = nearDistance;
var farNormalX = -otherVec[0] + tmpVec[0];
var farNormalY = -otherVec[1] + tmpVec[1];
var farNormalZ = -otherVec[2] + tmpVec[2];
var farDistance = -otherVec[3] + tmpVec[3];
var farDot = farNormalX * farNormalX + farNormalY * farNormalY + farNormalZ * farNormalZ;
var farMagnitude = math.sqrt(farDot);
var farInvMagnitude = 1.0f / farMagnitude;
farNormalX *= farInvMagnitude;
farNormalY *= farInvMagnitude;
farNormalZ *= farInvMagnitude;
farDistance *= farInvMagnitude;
outPlanes[kPlaneFrustumFar].xyz = new float3(farNormalX, farNormalY, farNormalZ);
outPlanes[kPlaneFrustumFar].w = farDistance;
}
}
// Since we are using ByteAddressBuffer in hlsl, we don't need to care about the 16 bytes alignment of the data like in CBuffer.
@@ -54,6 +165,7 @@ public struct RenderView
public float nearClipPlane;
public float farClipPlane;
// Maybe use fov directly?
public float2 sensorSize;
public GateFit gateFit;
public float iso;

View File

@@ -10,18 +10,18 @@ namespace Ghost.Graphics.Core;
public readonly unsafe ref struct RenderingContext
{
private readonly IGraphicsEngine _engine;
private readonly IResourceManager _resourceManager;
private readonly ResourceManager _resourceManager;
private readonly ICommandBuffer _directCmd;
public ICommandBuffer DirectCommandBuffer => _directCmd;
public IShaderCompiler ShaderCompiler => _engine.ShaderCompiler;
public IResourceManager ResourceManager => _resourceManager;
public ResourceManager ResourceManager => _resourceManager;
public IResourceAllocator ResourceAllocator => _engine.ResourceAllocator;
public IResourceDatabase ResourceDatabase => _engine.ResourceDatabase;
public IPipelineLibrary PipelineLibrary => _engine.PipelineLibrary;
internal RenderingContext(IGraphicsEngine engine, IResourceManager resourceManager, ICommandBuffer directCmd)
internal RenderingContext(IGraphicsEngine engine, ResourceManager resourceManager, ICommandBuffer directCmd)
{
_engine = engine;
_resourceManager = resourceManager;
@@ -163,7 +163,7 @@ public readonly unsafe ref struct RenderingContext
if (r.IsFailure) return;
ref var meshRef = ref r.Value;
var meshletData = meshRef.MeshletData;
ref readonly var meshletData = ref meshRef.MeshletData;
if (!meshletData.meshlets.IsCreated || meshletData.meshlets.Count == 0) return;

View File

@@ -2,35 +2,40 @@ using System.Diagnostics;
namespace Ghost.Graphics.Core;
public struct RenderingLayerMask : IEquatable<RenderingLayerMask>
public readonly struct RenderingLayerMask : IEquatable<RenderingLayerMask>
{
private static readonly Dictionary<string, uint> _layerNameToBit = new(32);
private static readonly Dictionary<uint, string> _bitToLayerName = new(32);
private static readonly Dictionary<string, uint> s_layerNameToBit = new(32);
private static readonly Dictionary<uint, string> s_bitToLayerName = new(32);
internal static void SetLayerName(int layerIndex, string name)
{
Debug.Assert(layerIndex >= 0 && layerIndex < 32, "Layer index must be between 0 and 31.");
var bit = 1u << layerIndex;
_layerNameToBit[name] = bit;
_bitToLayerName[bit] = name;
s_layerNameToBit[name] = bit;
s_bitToLayerName[bit] = name;
}
public static uint GetLayerBit(string name)
{
if (_layerNameToBit.TryGetValue(name, out var bit))
if (s_layerNameToBit.TryGetValue(name, out var bit))
{
return bit;
}
return ~0u;
return 0u;
}
public uint value;
private readonly uint _value;
public RenderingLayerMask(uint value)
{
_value = value;
}
public readonly bool Equals(RenderingLayerMask other)
{
return value == other.value;
return _value == other._value;
}
public override readonly bool Equals(object? obj)
@@ -53,13 +58,43 @@ public struct RenderingLayerMask : IEquatable<RenderingLayerMask>
return !(left == right);
}
public static RenderingLayerMask operator |(RenderingLayerMask left, RenderingLayerMask right)
{
return new RenderingLayerMask(left._value | right._value);
}
public static RenderingLayerMask operator &(RenderingLayerMask left, RenderingLayerMask right)
{
return new RenderingLayerMask(left._value & right._value);
}
public static RenderingLayerMask operator ~(RenderingLayerMask mask)
{
return new RenderingLayerMask(~mask._value);
}
public static RenderingLayerMask operator ^(RenderingLayerMask left, RenderingLayerMask right)
{
return new RenderingLayerMask(left._value ^ right._value);
}
public static RenderingLayerMask operator <<(RenderingLayerMask mask, int shift)
{
return new RenderingLayerMask(mask._value << shift);
}
public static RenderingLayerMask operator >>(RenderingLayerMask mask, int shift)
{
return new RenderingLayerMask(mask._value >> shift);
}
public static implicit operator uint(RenderingLayerMask mask)
{
return mask.value;
return mask._value;
}
public static implicit operator RenderingLayerMask(uint value)
{
return new RenderingLayerMask { value = value };
return new RenderingLayerMask(value);
}
}

View File

@@ -8,7 +8,7 @@ namespace Ghost.Graphics.RenderGraphModule;
/// </summary>
public sealed class RenderGraph : IDisposable
{
private readonly IResourceManager _resourceManager;
private readonly ResourceManager _resourceManager;
private readonly IResourceAllocator _resourceAllocator;
private readonly IResourceDatabase _resourceDatabase;
@@ -37,7 +37,7 @@ public sealed class RenderGraph : IDisposable
public RenderGraphBlackboard Blackboard => _blackboard;
public RenderGraph(IResourceManager resourceManager, IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, IPipelineLibrary pipelineLibrary, IShaderCompiler shaderCompiler)
public RenderGraph(ResourceManager resourceManager, IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, IPipelineLibrary pipelineLibrary, IShaderCompiler shaderCompiler)
{
_resourceManager = resourceManager;
_resourceAllocator = resourceAllocator;

View File

@@ -9,7 +9,7 @@ namespace Ghost.Graphics.RenderGraphModule;
/// </summary>
internal sealed class RenderGraphCompiler
{
private readonly IResourceManager _resourceManager;
private readonly ResourceManager _resourceManager;
private readonly IResourceDatabase _resourceDatabase;
private readonly IResourceAllocator _resourceAllocator;
private readonly RenderGraphResourceRegistry _resources;
@@ -20,7 +20,7 @@ internal sealed class RenderGraphCompiler
private Handle<GPUResource> _resourceHeap;
public RenderGraphCompiler(
IResourceManager resourceManager,
ResourceManager resourceManager,
IResourceDatabase resourceDatabase,
IResourceAllocator resourceAllocator,
RenderGraphResourceRegistry resources,

View File

@@ -7,7 +7,7 @@ namespace Ghost.Graphics.RenderGraphModule;
public interface IRenderGraphContext
{
IResourceManager ResourceManager { get; }
ResourceManager ResourceManager { get; }
IResourceDatabase ResourceDatabase { get; }
Handle<GPUResource> GetActualResource(Identifier<RGResource> resource);
@@ -41,7 +41,7 @@ public interface IUnsafeRenderContext : IRasterRenderContext, IRenderGraphContex
internal sealed class RenderGraphContext : IRasterRenderContext, IComputeRenderContext, IUnsafeRenderContext
{
private readonly IResourceManager _resourceManager;
private readonly ResourceManager _resourceManager;
private readonly IResourceDatabase _resourceDatabase;
private readonly IPipelineLibrary _pipelineLibrary;
private readonly IShaderCompiler _shaderCompiler;
@@ -58,14 +58,14 @@ internal sealed class RenderGraphContext : IRasterRenderContext, IComputeRenderC
private Handle<GraphicsBuffer> _activePerMeshData;
private int _activeMeshIndexCount;
public IResourceManager ResourceManager => _resourceManager;
public ResourceManager ResourceManager => _resourceManager;
public IResourceDatabase ResourceDatabase => _resourceDatabase;
public int ActiveMeshIndexCount => _activeMeshIndexCount;
public ICommandBuffer CommandBuffer => _commandBuffer;
internal RenderGraphContext(IResourceManager resourceManager, IResourceDatabase resourceDatabase, IPipelineLibrary pipelineLibrary, IShaderCompiler shaderCompiler, RenderGraphResourceRegistry resources)
internal RenderGraphContext(ResourceManager resourceManager, IResourceDatabase resourceDatabase, IPipelineLibrary pipelineLibrary, IShaderCompiler shaderCompiler, RenderGraphResourceRegistry resources)
{
_resourceManager = resourceManager;
_resourceDatabase = resourceDatabase;

View File

@@ -5,7 +5,7 @@ namespace Ghost.Graphics.RenderGraphModule;
internal sealed class RenderGraphExecutor
{
private readonly IResourceManager _resourceManager;
private readonly ResourceManager _resourceManager;
private readonly IResourceDatabase _resourceDatabase;
private readonly RenderGraphResourceRegistry _resources;
private readonly RenderGraphContext _context;
@@ -13,7 +13,7 @@ internal sealed class RenderGraphExecutor
private uint _frameIndex;
public RenderGraphExecutor(
IResourceManager resourceManager,
ResourceManager resourceManager,
IResourceDatabase resourceDatabase,
RenderGraphResourceRegistry resources,
RenderGraphContext context)

View File

@@ -0,0 +1,110 @@
using Ghost.Core;
using Ghost.Graphics.Core;
using Ghost.Graphics.RenderGraphModule;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Mathematics;
using System.Runtime.InteropServices;
namespace Ghost.Graphics.RenderPipeline;
public partial class GhostRenderPipeline
{
private class MeshRenderPassData
{
public RenderList renderList;
public Identifier<RGTexture> renderTarget;
}
private class BlitPassData
{
public Identifier<RGTexture> source;
public Identifier<RGTexture> destination;
public Handle<Material> blitMaterial;
public Identifier<Sampler> sampler;
}
[StructLayout(LayoutKind.Sequential)]
private struct ShaderProperties_MyShader_Standard
{
public float4 color;
public uint texture1;
public uint texture2;
public uint texture3;
public uint texture4;
public uint tex_sampler;
private readonly uint _padding1;
private readonly uint _padding2;
private readonly uint _padding3;
}
[StructLayout(LayoutKind.Sequential)]
private struct ShaderProperties_Hidden_Blit
{
public uint mainTex;
public uint sampler_mainTex;
private readonly uint _padding1;
private readonly uint _padding2;
}
private void RenderTest(RenderGraph graph, Identifier<RGTexture> backbuffer)
{
Identifier<RGTexture> renderTarget;
using (var builder = graph.AddRasterRenderPass<MeshRenderPassData>("Mesh Render Pass", out var passData))
{
passData.mesh = _mesh;
passData.material = _material;
passData.renderTarget = builder.CreateTexture(RGTextureDesc.Relative(1.0f, TextureFormat.R8G8B8A8_UNorm), "Render Target");
builder.SetColorAttachment(passData.renderTarget, 0);
renderTarget = passData.renderTarget;
builder.SetRenderFunc<MeshRenderPassData>(static (data, ctx) =>
{
ctx.SetActiveMaterial(data.material);
ctx.SetActiveMesh(data.mesh);
var threadGroupCountX = ((uint)ctx.ActiveMeshIndexCount + 2u) / 3u;
ctx.DispatchMesh(new uint3(threadGroupCountX, 1u, 1u));
});
}
using (var builder = graph.AddUnsafeRenderPass<BlitPassData>("Blit Pass", out var passData))
{
passData.source = renderTarget;
passData.destination = backbuffer;
passData.blitMaterial = _blitMaterial;
passData.sampler = _sampler;
builder.UseTexture(passData.source, AccessFlags.Read);
builder.UseTexture(passData.destination, AccessFlags.WriteAll);
builder.SetRenderFunc<BlitPassData>(static (data, ctx) =>
{
var r = ctx.ResourceManager.GetMaterialReference(data.blitMaterial);
if (r.IsFailure)
{
return;
}
ref var matRef = ref r.Value;
var blitProps = new ShaderProperties_Hidden_Blit
{
mainTex = ctx.ResourceDatabase.GetBindlessIndex(ctx.GetActualResource(data.source.AsResource())),
sampler_mainTex = (uint)data.sampler.Value,
};
matRef.SetPropertyCache(in blitProps).ThrowIfFailed();
matRef.UploadData(ctx.CommandBuffer, ctx.ResourceDatabase);
ctx.CommandBuffer.SetRenderTargets([ctx.GetActualTexture(data.destination)], Handle<Texture>.Invalid);
ctx.SetActiveMaterial(data.blitMaterial);
ctx.SetActiveMesh(Handle<Mesh>.Invalid); // Generate a full-screen triangle dynamically in mesh shader.
ctx.DispatchMesh(new uint3(1, 1, 1));
});
}
}
}

View File

@@ -7,7 +7,7 @@ namespace Ghost.Graphics.RenderPipeline;
public sealed class GhostRenderPipelineSettings : IRenderPipelineSettings
{
public static IRenderPipeline CreatePipeline(IRenderSystem renderSystem)
public static IRenderPipeline CreatePipeline(RenderSystem renderSystem)
{
return new GhostRenderPipeline(renderSystem);
}
@@ -30,7 +30,7 @@ public unsafe partial class GhostRenderPipeline : IRenderPipeline
ObjectDisposedException.ThrowIf(_disposed, this);
}
internal GhostRenderPipeline(IRenderSystem renderSystem)
internal GhostRenderPipeline(RenderSystem renderSystem)
{
_renderGraph = new RenderGraph(renderSystem.ResourceManager,
renderSystem.GraphicsEngine.ResourceAllocator,

View File

@@ -5,7 +5,7 @@ namespace Ghost.Graphics.RenderPipeline;
public interface IRenderPipelineSettings
{
static abstract IRenderPipeline CreatePipeline(IRenderSystem renderSystem);
static abstract IRenderPipeline CreatePipeline(RenderSystem renderSystem);
}
public interface IRenderPipeline : IDisposable

View File

@@ -21,7 +21,7 @@ struct Meshlet
};
[numthreads(64, 1, 1)] // 64 threads for max 64 vertices and up to 124 triangles
[OUTPUT_TRIANGLE_TOPOLOGY]
[outputtopology("triangle")]
void MSMain(
uint3 groupThreadID : SV_GroupThreadID,
uint groupID : SV_GroupID,

View File

@@ -6,34 +6,12 @@ using System.Collections.Concurrent;
namespace Ghost.Graphics;
public interface IRenderSystem : IFenceSynchronizer, IDisposable
{
IGraphicsEngine GraphicsEngine
{
get;
}
IResourceManager ResourceManager
{
get;
}
bool IsRunning
{
get;
}
void Start();
void Stop();
void RequestSwapChainResize(ISwapChain swapChain, uint2 newSize);
}
public enum GraphicsAPI
internal enum GraphicsAPI
{
Direct3D12
}
public struct RenderSystemDesc
internal struct RenderSystemDesc
{
public GraphicsAPI GraphicsAPI
{
@@ -50,9 +28,8 @@ public struct RenderSystemDesc
/// Application-level render system that orchestrates multiple renderers
/// and handles frame synchronization
/// </summary>
internal class RenderSystem : IRenderSystem
public class RenderSystem : IDisposable
{
// TODO: Thread local command buffers.
private struct FrameResource : IDisposable
{
public required AutoResetEvent CpuReadyEvent
@@ -85,7 +62,7 @@ internal class RenderSystem : IRenderSystem
private readonly RenderSystemDesc _config;
private readonly IGraphicsEngine _graphicsEngine;
private readonly IResourceManager _resourceManager;
private readonly ResourceManager _resourceManager;
private readonly FrameResource[] _frameResources;
private readonly Thread _renderThread;
@@ -100,7 +77,7 @@ internal class RenderSystem : IRenderSystem
private bool _disposed;
public IGraphicsEngine GraphicsEngine => _graphicsEngine;
public IResourceManager ResourceManager => _resourceManager;
public ResourceManager ResourceManager => _resourceManager;
public bool IsRunning => _isRunning;
public uint CPUFenceValue => _cpuFenceValue;
@@ -108,7 +85,7 @@ internal class RenderSystem : IRenderSystem
public uint FrameIndex => _frameIndex;
public uint MaxFrameLatency => _config.FrameBufferCount;
public RenderSystem(RenderSystemDesc desc)
internal RenderSystem(RenderSystemDesc desc)
{
_config = desc;
@@ -169,67 +146,6 @@ internal class RenderSystem : IRenderSystem
Dispose();
}
public void Start()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_isRunning)
{
return;
}
_isRunning = true;
_renderThread.Start();
}
public void Stop()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!_isRunning)
{
return;
}
_isRunning = false;
_shutdownEvent.Set();
_renderThread.Join();
}
public void RequestSwapChainResize(ISwapChain swapChain, uint2 newSize)
{
ObjectDisposedException.ThrowIf(_disposed, this);
_resizeRequest.AddOrUpdate(swapChain, newSize, (_, _) => newSize);
}
public bool WaitForGPUReady(int timeOut = -1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
return _frameResources[eventIndex].GpuReadyEvent.WaitOne(timeOut);
}
public void SignalCPUReady()
{
ObjectDisposedException.ThrowIf(_disposed, this);
var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
_frameResources[eventIndex].CpuReadyEvent.Set();
_cpuFenceValue++;
}
public void WaitIdle()
{
foreach (var frameResource in _frameResources)
{
if (frameResource.FenceValue > 0)
{
_graphicsEngine.Device.GraphicsQueue.WaitForValue(frameResource.FenceValue);
}
}
}
private void RenderLoop()
{
var waitHandles = new WaitHandle[] { null!, _shutdownEvent };
@@ -272,14 +188,16 @@ internal class RenderSystem : IRenderSystem
resource.CommandAllocator.Reset();
}
foreach (var kvp in _resizeRequest)
var keys = _resizeRequest.Keys.ToArray();
foreach (var swapChain in keys)
{
if (_resizeRequest.TryRemove(swapChain, out var newSize))
{
var swapChain = kvp.Key;
var newSize = kvp.Value;
swapChain.Resize(newSize.x, newSize.y);
}
}
_resizeRequest.Clear();
frameResource.GpuReadyEvent.Set();
continue; // Skip rendering this frame since we just resized and may have invalid render targets
}
@@ -304,6 +222,67 @@ internal class RenderSystem : IRenderSystem
}
}
internal void Start()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_isRunning)
{
return;
}
_isRunning = true;
_renderThread.Start();
}
internal void Stop()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!_isRunning)
{
return;
}
_isRunning = false;
_shutdownEvent.Set();
_renderThread.Join();
}
internal void SignalCPUReady()
{
ObjectDisposedException.ThrowIf(_disposed, this);
var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
_frameResources[eventIndex].CpuReadyEvent.Set();
_cpuFenceValue++;
}
internal void RequestSwapChainResize(ISwapChain swapChain, uint2 newSize)
{
ObjectDisposedException.ThrowIf(_disposed, this);
_resizeRequest.AddOrUpdate(swapChain, newSize, (_, _) => newSize);
}
public bool WaitForGPUReady(int timeOut = -1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
return _frameResources[eventIndex].GpuReadyEvent.WaitOne(timeOut);
}
public void WaitIdle()
{
foreach (var frameResource in _frameResources)
{
if (frameResource.FenceValue > 0)
{
_graphicsEngine.Device.GraphicsQueue.WaitForValue(frameResource.FenceValue);
}
}
}
public void Dispose()
{
if (_disposed)

View File

@@ -7,124 +7,7 @@ using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics;
public interface IResourceManager
{
/// <summary>
/// Creates a new mesh from the specified vertex and index data.
/// </summary>
/// <param name="vertices">A UnsafeList containing the vertices that define the geometry of the mesh. Must contain at least one vertex.</param>
/// <param name="indices">A UnsafeList containing the indices that specify how vertices are connected to form primitives. Must contain at least one index.</param>
/// <returns>An <see cref="Identifier{Mesh}"/> representing the newly created mesh.</returns>
Handle<Mesh> CreateMesh(UnsafeList<Vertex> vertices, UnsafeList<uint> indices);
/// <summary>
/// Creates a new material instance using the specified shader.
/// </summary>
/// <param name="shader">The identifier of the shader to associate with the new material.</param>
/// <returns>An <see cref="Identifier{Material}"/> representing the newly created material.</returns>
Handle<Material> CreateMaterial(Identifier<Shader> shader);
/// <summary>
/// Creates a new shader and returns its unique identifier.
/// </summary>
/// <returns>An <see cref="Identifier{Shader}"/> representing the newly created shader.</returns>
/// <param name="descriptor">The viewGroup containing the shader's properties and passes.</param>
Identifier<Shader> CreateGraphicsShader(ShaderDescriptor descriptor);
/// <summary>
/// Determines whether a mesh with the specified Handle exists.
/// </summary>
/// <param name="handle">The handle of the mesh to check for existence. Cannot be null.</param>
/// <returns>true if a mesh with the specified Handle exists; otherwise, false.</returns>
bool HasMesh(Handle<Mesh> handle);
/// <summary>
/// Returns a reference to the mesh associated with the specified handle.
/// </summary>
/// <param name="handle">The handle of the mesh to retrieve. Must refer to a valid mesh; otherwise, the behavior is undefined.</param>
/// <returns>A result containing a reference to the mesh corresponding to the specified handle, or an error status if the handle is invalid.</returns>
RefResult<Mesh, Error> GetMeshReference(Handle<Mesh> handle);
/// <summary>
/// Releases the mesh resource associated with the specified handle, freeing any resources held by it. Includes both CPU and GPU resources.
/// </summary>
/// <param name="handle">The handle of the mesh to release. Must refer to a mesh that was previously created and not already released.</param>
void ReleaseMesh(Handle<Mesh> handle);
/// <summary>
/// Determines whether a material with the specified handle exists in the collection.
/// </summary>
/// <param name="handle">The handle of the material to check for existence.</param>
/// <returns>true if a material with the specified handle exists; otherwise, false.</returns>
bool HasMaterial(Handle<Material> handle);
/// <summary>
/// Gets a reference to the material associated with the specified handle.
/// </summary>
/// <param name="handle">The handle of the material to retrieve. Must refer to a valid material.</param>
/// <returns>A result containing a reference to the material corresponding to the specified handle, or an error status if the handle is invalid.</returns>
RefResult<Material, Error> GetMaterialReference(Handle<Material> handle);
/// <summary>
/// Releases the material associated with the specified handle, making it available for reuse or disposal.
/// </summary>
/// <param name="handle">The handle of the material to release. Must refer to a material that has been previously acquired.</param>
void ReleaseMaterial(Handle<Material> handle);
/// <summary>
/// Returns an existing material palette index for the specified material sequence or creates a new one.
/// </summary>
/// <param name="materials">The ordered material list for the palette.</param>
/// <returns>The palette index. Index 0 represents an empty palette.</returns>
int GetOrCreateMaterialPalette(ReadOnlySpan<Handle<Material>> materials);
/// <summary>
/// Determines whether the specified material palette index is valid.
/// </summary>
/// <param name="paletteID">The palette index to validate.</param>
bool HasMaterialPalette(Identifier<MaterialPalette> paletteID);
/// <summary>
/// Gets metadata for a material palette entry.
/// </summary>
/// <param name="paletteID">The palette index to query.</param>
MaterialPalette GetMaterialPaletteInfo(Identifier<MaterialPalette> paletteID);
/// <summary>
/// Gets a material handle from a palette entry by local material index.
/// </summary>
/// <param name="paletteID">The palette index to query.</param>
/// <param name="localMaterialIndex">The material slot inside the palette.</param>
Handle<Material> GetMaterialPaletteMaterial(Identifier<MaterialPalette> paletteID, int localMaterialIndex);
/// <summary>
/// Releases a material palette reference previously returned by <see cref="GetOrCreateMaterialPalette(ReadOnlySpan{Handle{Material}})"/>.
/// </summary>
/// <param name="paletteID">The palette index to release.</param>
void ReleaseMaterialPalette(Identifier<MaterialPalette> paletteID);
/// <summary>
/// Determines whether a shader with the specified identifier exists in the collection.
/// </summary>
/// <param name="id">The identifier of the shader to check for existence.</param>
/// <returns>true if a shader with the specified identifier exists; otherwise, false.</returns>
bool HasShader(Identifier<Shader> id);
/// <summary>
/// Returns a reference to the shader associated with the specified identifier.
/// </summary>
/// <param name="id">The identifier of the shader to retrieve. Must refer to a valid shader.</param>
/// <returns>A result containing a reference to the shader corresponding to the specified identifier, or an error status if the identifier is invalid.</returns>
RefResult<Shader, Error> GetShaderReference(Identifier<Shader> id);
/// <summary>
/// Releases the shader associated with the specified identifier, freeing any resources allocated to it.
/// </summary>
/// <param name="id">The identifier of the shader to release. Must refer to a valid, previously created shader.</param>
void ReleaseShader(Identifier<Shader> id);
}
internal sealed class ResourceManager : IResourceManager, IDisposable
public sealed class ResourceManager : IDisposable
{
private readonly IResourceAllocator _resourceAllocator;
private readonly IResourceDatabase _resourceDatabase;
@@ -153,6 +36,12 @@ internal sealed class ResourceManager : IResourceManager, IDisposable
Dispose();
}
/// <summary>
/// Creates a new mesh from the specified vertex and index data.
/// </summary>
/// <param name="vertices">A UnsafeList containing the vertices that define the geometry of the mesh. Must contain at least one vertex.</param>
/// <param name="indices">A UnsafeList containing the indices that specify how vertices are connected to form primitives. Must contain at least one index.</param>
/// <returns>An <see cref="Identifier{Mesh}"/> representing the newly created mesh.</returns>
public unsafe Handle<Mesh> CreateMesh(UnsafeList<Vertex> vertices, UnsafeList<uint> indices)
{
ObjectDisposedException.ThrowIf(_disposed, this);
@@ -198,6 +87,11 @@ internal sealed class ResourceManager : IResourceManager, IDisposable
return new Handle<Mesh>(id, generation);
}
/// <summary>
/// Creates a new material instance using the specified shader.
/// </summary>
/// <param name="shader">The identifier of the shader to associate with the new material.</param>
/// <returns>An <see cref="Identifier{Material}"/> representing the newly created material.</returns>
public Handle<Material> CreateMaterial(Identifier<Shader> shader)
{
ObjectDisposedException.ThrowIf(_disposed, this);
@@ -212,6 +106,11 @@ internal sealed class ResourceManager : IResourceManager, IDisposable
return new Handle<Material>(id, generation);
}
/// <summary>
/// Creates a new shader and returns its unique identifier.
/// </summary>
/// <returns>An <see cref="Identifier{Shader}"/> representing the newly created shader.</returns>
/// <param name="descriptor">The viewGroup containing the shader's properties and passes.</param>
public Identifier<Shader> CreateGraphicsShader(ShaderDescriptor descriptor)
{
ObjectDisposedException.ThrowIf(_disposed, this);
@@ -223,12 +122,22 @@ internal sealed class ResourceManager : IResourceManager, IDisposable
return new Identifier<Shader>(id);
}
/// <summary>
/// Determines whether a mesh with the specified Handle exists.
/// </summary>
/// <param name="handle">The handle of the mesh to check for existence. Cannot be null.</param>
/// <returns>true if a mesh with the specified Handle exists; otherwise, false.</returns>
public bool HasMesh(Handle<Mesh> handle)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return _meshes.Contains(handle.ID, handle.Generation);
}
/// <summary>
/// Returns a reference to the mesh associated with the specified handle.
/// </summary>
/// <param name="handle">The handle of the mesh to retrieve. Must refer to a valid mesh; otherwise, the behavior is undefined.</param>
/// <returns>A result containing a reference to the mesh corresponding to the specified handle, or an error status if the handle is invalid.</returns>
public RefResult<Mesh, Error> GetMeshReference(Handle<Mesh> handle)
{
ref var mesh = ref _meshes.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
@@ -240,6 +149,10 @@ internal sealed class ResourceManager : IResourceManager, IDisposable
return RefResult<Mesh, Error>.Success(ref mesh);
}
/// <summary>
/// Releases the mesh resource associated with the specified handle, freeing any resources held by it. Includes both CPU and GPU resources.
/// </summary>
/// <param name="handle">The handle of the mesh to release. Must refer to a mesh that was previously created and not already released.</param>
public void ReleaseMesh(Handle<Mesh> handle)
{
ObjectDisposedException.ThrowIf(_disposed, this);
@@ -253,12 +166,22 @@ internal sealed class ResourceManager : IResourceManager, IDisposable
mesh.ReleaseResource(_resourceDatabase);
}
/// <summary>
/// Determines whether a material with the specified handle exists in the collection.
/// </summary>
/// <param name="handle">The handle of the material to check for existence.</param>
/// <returns>true if a material with the specified handle exists; otherwise, false.</returns>
public bool HasMaterial(Handle<Material> handle)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return _materials.Contains(handle.ID, handle.Generation);
}
/// <summary>
/// Gets a reference to the material associated with the specified handle.
/// </summary>
/// <param name="handle">The handle of the material to retrieve. Must refer to a valid material.</param>
/// <returns>A result containing a reference to the material corresponding to the specified handle, or an error status if the handle is invalid.</returns>
public RefResult<Material, Error> GetMaterialReference(Handle<Material> handle)
{
ref var material = ref _materials.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
@@ -270,6 +193,10 @@ internal sealed class ResourceManager : IResourceManager, IDisposable
return RefResult<Material, Error>.Success(ref material);
}
/// <summary>
/// Releases the material associated with the specified handle, making it available for reuse or disposal.
/// </summary>
/// <param name="handle">The handle of the material to release. Must refer to a material that has been previously acquired.</param>
public void ReleaseMaterial(Handle<Material> handle)
{
ObjectDisposedException.ThrowIf(_disposed, this);
@@ -284,6 +211,11 @@ internal sealed class ResourceManager : IResourceManager, IDisposable
material.ReleaseResource(_resourceDatabase);
}
/// <summary>
/// Returns an existing material palette index for the specified material sequence or creates a new one.
/// </summary>
/// <param name="materials">The ordered material list for the palette.</param>
/// <returns>The palette index. Index 0 represents an empty palette.</returns>
public int GetOrCreateMaterialPalette(ReadOnlySpan<Handle<Material>> materials)
{
ObjectDisposedException.ThrowIf(_disposed, this);
@@ -299,36 +231,63 @@ internal sealed class ResourceManager : IResourceManager, IDisposable
return _materialPalettes.InsertOrGet(materials);
}
/// <summary>
/// Determines whether the specified material palette index is valid.
/// </summary>
/// <param name="paletteID">The palette index to validate.</param>
public bool HasMaterialPalette(Identifier<MaterialPalette> paletteID)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return _materialPalettes.IsValid(paletteID);
}
/// <summary>
/// Gets metadata for a material palette entry.
/// </summary>
/// <param name="paletteID">The palette index to query.</param>
public MaterialPalette GetMaterialPaletteInfo(Identifier<MaterialPalette> paletteID)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return _materialPalettes.GetInfo(paletteID);
}
/// <summary>
/// Gets a material handle from a palette entry by local material index.
/// </summary>
/// <param name="paletteID">The palette index to query.</param>
/// <param name="localMaterialIndex">The material slot inside the palette.</param>
public Handle<Material> GetMaterialPaletteMaterial(Identifier<MaterialPalette> paletteID, int localMaterialIndex)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return _materialPalettes.GetMaterial(paletteID, localMaterialIndex);
}
/// <summary>
/// Releases a material palette reference previously returned by <see cref="GetOrCreateMaterialPalette(ReadOnlySpan{Handle{Material}})"/>.
/// </summary>
/// <param name="paletteID">The palette index to release.</param>
public void ReleaseMaterialPalette(Identifier<MaterialPalette> paletteID)
{
ObjectDisposedException.ThrowIf(_disposed, this);
_materialPalettes.Release(paletteID);
}
/// <summary>
/// Determines whether a shader with the specified identifier exists in the collection.
/// </summary>
/// <param name="id">The identifier of the shader to check for existence.</param>
/// <returns>true if a shader with the specified identifier exists; otherwise, false.</returns>
public bool HasShader(Identifier<Shader> id)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return id.Value >= 0 && id.Value < _shaders.Count;
}
/// <summary>
/// Returns a reference to the shader associated with the specified identifier.
/// </summary>
/// <param name="id">The identifier of the shader to retrieve. Must refer to a valid shader.</param>
/// <returns>A result containing a reference to the shader corresponding to the specified identifier, or an error status if the identifier is invalid.</returns>
public RefResult<Shader, Error> GetShaderReference(Identifier<Shader> id)
{
if (!HasShader(id))
@@ -339,6 +298,10 @@ internal sealed class ResourceManager : IResourceManager, IDisposable
return RefResult<Shader, Error>.Success(ref _shaders[id.Value]);
}
/// <summary>
/// Releases the shader associated with the specified identifier, freeing any resources allocated to it.
/// </summary>
/// <param name="id">The identifier of the shader to release. Must refer to a valid, previously created shader.</param>
public void ReleaseShader(Identifier<Shader> id)
{
ObjectDisposedException.ThrowIf(_disposed, this);

View File

@@ -49,10 +49,6 @@ struct Vertex
#define SAMPLE_TEXTURE2D_ARRAY(texId, sampId, uvw) SampleTextureArray(texId, sampId, uvw)
#define OUTPUT_TRIANGLE_TOPOLOGY outputtopology("triangle")
#define OUTPUT_LINE_TOPOLOGY outputtopology("line")
#define ZERO_INIT(T) (T)0

View File

@@ -143,7 +143,7 @@ public unsafe struct ClodCluster
/// <summary>
/// Delegate type for processing generated LOD groups.
/// </summary>
public unsafe delegate int ClodOutputDelegate(void* context, ClodGroup group, ClodCluster* clusters, nuint clusterCount);
public unsafe delegate int ClodOutputDelegate(void* context, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster> clusters);
// FIX: UnsafeList and UnsafeArray are not same as std::vector.
@@ -383,7 +383,7 @@ public static unsafe class MeshletUtility
var clodGroup = new ClodGroup { depth = depth, simplified = simplified };
var result = outputCallback != null
? outputCallback(outputContext, clodGroup, (ClodCluster*)groupClusters.GetUnsafePtr(), (nuint)groupClusters.Count)
? outputCallback(outputContext, clodGroup, groupClusters.AsReadOnly())
: -1;
return result;

View File

@@ -21,7 +21,7 @@ shader "MyShader/Standard"
color_mask = all;
}
mesh "F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/RenderPasses/ShaderCode.hlsl" : "MSMain";
pixel "F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/RenderPasses/ShaderCode.hlsl" : "PSMain";
mesh "F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/RenderPipeline/ShaderCode.hlsl" : "MSMain";
pixel "F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/RenderPipeline/ShaderCode.hlsl" : "PSMain";
}
}

View File

@@ -20,7 +20,7 @@ internal struct TestChunkQueryJob : IJobChunk
var random = new random((uint)ctx.ThreadIndex + 1u);
var transforms = view.GetComponentDataRW<Transform>();
for (var i = 0; i < view.Count; i++)
for (var i = 0; i < view.EntityCount; i++)
{
transforms[i].position += random.NextFloat3();
}
@@ -76,8 +76,8 @@ public partial class EntityQueryTest : ITest
// var bits = chunk.GetEnableBits<Transform>();
// var it = bits.GetIterator();
// while (it.Next(out var index) && index < chunk.Count)
for (var index = 0; index < chunk.Count; index++)
// while (it.Next(out var index) && index < chunk.EntityCount)
for (var index = 0; index < chunk.EntityCount; index++)
{
Console.WriteLine($"Entity {chunkEntities[index]} Updated Position: {transforms[index].position}");
}

View File

@@ -1,29 +1,25 @@
using Ghost.Graphics.Test.Models;
using Ghost.Graphics.Test.Services;
using Ghost.Core;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
namespace Ghost.Graphics.Test.Controls;
public sealed partial class DebugConsole : UserControl
{
private readonly ObservableCollection<LogItem> _filteredLogs = [];
private readonly LoggingService _loggingService;
private readonly ObservableCollection<LogMessage> _filteredLogs = [];
public DebugConsole()
{
InitializeComponent();
_loggingService = LoggingService.Instance;
LogItemsRepeater.ItemsSource = _filteredLogs;
// Subscribe to logging events
_loggingService.LogAdded += OnLogAdded;
_loggingService.LogsCleared += OnLogsCleared;
Logger.Logs.LogChanged += OnLogChange;
// Subscribe to filter changes
ShowInfoCheckBox.Checked += OnFilterChanged;
@@ -39,27 +35,47 @@ public sealed partial class DebugConsole : UserControl
RefreshLogs();
}
private void OnLogAdded(LogItem logItem)
private void OnLogChange(object? sender, NotifyCollectionChangedEventArgs e)
{
DispatcherQueue.TryEnqueue(() =>
{
if (ShouldShowLogItem(logItem))
switch (e.Action)
{
_filteredLogs.Add(logItem);
case NotifyCollectionChangedAction.Add:
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
if (item is LogMessage logMessage && ShouldShowLogItem(logMessage))
{
_filteredLogs.Add(logMessage);
if (AutoScrollCheckBox.IsChecked == true)
{
LogScrollViewer.ScrollToVerticalOffset(LogScrollViewer.ScrollableHeight);
}
}
});
}
}
break;
private void OnLogsCleared()
case NotifyCollectionChangedAction.Remove:
if (e.OldItems != null)
{
DispatcherQueue.TryEnqueue(() =>
foreach (var item in e.OldItems)
{
_filteredLogs.Clear();
if (item is LogMessage logMessage)
{
_filteredLogs.Remove(logMessage);
}
}
}
break;
case NotifyCollectionChangedAction.Reset:
RefreshLogs();
break;
default:
break;
}
});
}
@@ -68,9 +84,9 @@ public sealed partial class DebugConsole : UserControl
RefreshLogs();
}
private bool ShouldShowLogItem(LogItem logItem)
private bool ShouldShowLogItem(LogMessage message)
{
return logItem.Level switch
return message.Level switch
{
LogLevel.Info => ShowInfoCheckBox.IsChecked == true,
LogLevel.Warning => ShowWarningCheckBox.IsChecked == true,
@@ -84,7 +100,7 @@ public sealed partial class DebugConsole : UserControl
{
_filteredLogs.Clear();
foreach (var log in _loggingService.Logs)
foreach (var log in Logger.Logs)
{
if (ShouldShowLogItem(log))
{
@@ -100,17 +116,17 @@ public sealed partial class DebugConsole : UserControl
private void ClearButton_Click(object sender, RoutedEventArgs e)
{
_loggingService.Clear();
Logger.Impl.Clear();
}
private void ShowStackTraceCheckBox_Checked(object sender, RoutedEventArgs e)
{
_loggingService.CaptureStackTrace = true;
Logger.Impl.CaptureStackTrace = true;
}
private void ShowStackTraceCheckBox_Unchecked(object sender, RoutedEventArgs e)
{
_loggingService.CaptureStackTrace = false;
Logger.Impl.CaptureStackTrace = false;
}
}

View File

@@ -1,52 +0,0 @@
namespace Ghost.Graphics.Test.Models;
public enum LogLevel
{
Info,
Warning,
Error,
Debug
}
internal struct LogItem
{
public LogLevel Level
{
get; init;
}
public string Message
{
get; init;
}
public DateTime Timestamp
{
get; init;
}
public string? StackTrace
{
get; init;
}
public LogItem(LogLevel level, string message, string? stackTrace = null)
{
Level = level;
Message = message;
StackTrace = stackTrace;
Timestamp = DateTime.Now;
}
public override readonly string ToString()
{
return $"{Timestamp:HH:mm:ss.fff} [{Level}] {Message}";
}
public readonly string ToStringWithStackTrace()
{
if (string.IsNullOrEmpty(StackTrace))
{
return ToString();
}
return $"{ToString()}\n{StackTrace}";
}
}

View File

@@ -317,7 +317,7 @@ internal class MeshRenderPass : IRenderPass
}
}
public void Cleanup(IResourceManager resourceManager, IResourceDatabase resourceDatabase)
public void Cleanup(ResourceManager resourceManager, IResourceDatabase resourceDatabase)
{
resourceManager.ReleaseMaterial(_blitMaterial);

View File

@@ -0,0 +1,99 @@
#include "F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/Shaders/Includes/Properties.hlsl"
#include "F:/csharp/GhostEngine/src/Runtime//Ghost.Graphics/Shaders/Includes/Common.hlsl"
struct PixelInput
{
float4 position : SV_POSITION;
float4 color : COLOR;
float4 uv : TEXCOORD0;
};
struct Meshlet
{
float4 boundingSphere;
float3 boundingBoxMin;
float3 boundingBoxMax;
uint vertexOffset;
uint triangleOffset;
uint groupIndex;
float parentError;
uint packedCounts; // byte vertexCount, byte triangleCount, byte localMaterialIndex, byte lodLevel
};
[numthreads(64, 1, 1)] // 64 threads for max 64 vertices and up to 124 triangles
[outputtopology("triangle")]
void MSMain(
uint3 groupThreadID : SV_GroupThreadID,
uint groupID : SV_GroupID,
out vertices PixelInput outVerts[64],
out indices uint3 outTris[124])
{
PerObjectData perObjectData = LoadData<PerObjectData>(g_PushConstantData.perObjectBuffer, 0);
ByteAddressBuffer meshletBuffer = GET_BUFFER(perObjectData.meshletBuffer);
Meshlet m = meshletBuffer.Load<Meshlet>(groupID.x * sizeof(Meshlet));
uint vertexCount = m.packedCounts & 0xFF;
uint triangleCount = (m.packedCounts >> 8) & 0xFF;
SetMeshOutputCounts(vertexCount, triangleCount);
ByteAddressBuffer meshletVerticesBuffer = GET_BUFFER(perObjectData.meshletVerticesBuffer);
ByteAddressBuffer meshletTrianglesBuffer = GET_BUFFER(perObjectData.meshletTrianglesBuffer);
// Write vertex output
if (groupThreadID.x < vertexCount)
{
uint vertexIndex = meshletVerticesBuffer.Load((m.vertexOffset + groupThreadID.x) * 4);
ByteAddressBuffer vertices = GET_BUFFER(perObjectData.vertexBuffer);
Vertex v = vertices.Load<Vertex>(vertexIndex * sizeof(Vertex));
// Basic MVP transform not needed if already in world space, but usually we need localToWorld and ViewProj
PerViewData perViewData = LoadData<PerViewData>(g_PushConstantData.perViewBuffer, 0);
float4 worldPos = mul(perObjectData.localToWorld, float4(v.position.xyz, 1.0f));
outVerts[groupThreadID.x].position = mul(perViewData.viewMatrix, worldPos);
outVerts[groupThreadID.x].position = mul(perViewData.projectionMatrix, outVerts[groupThreadID.x].position);
outVerts[groupThreadID.x].color = v.color;
outVerts[groupThreadID.x].uv = v.uv;
}
// Write triangle output (1 thread processes 1 triangle)
// We could pack 3 indices in a uint or just use byte offset
// In our CPU code, we packed it as individual bytes, so 3 bytes per triangle.
// For 124 triangles, we have 372 bytes.
if (groupThreadID.x < triangleCount)
{
uint triangleIndex = groupThreadID.x;
uint baseOffset = m.triangleOffset + triangleIndex * 3;
// Load 4 bytes to get the 3 index bytes
// Needs byte-aligned loading
uint wordOffset = baseOffset & ~3;
uint shift = (baseOffset & 3) * 8;
uint packedIndices1 = meshletTrianglesBuffer.Load(wordOffset);
uint packedIndices2 = meshletTrianglesBuffer.Load(wordOffset + 4);
uint64_t combined = ((uint64_t)packedIndices2 << 32) | packedIndices1;
uint packedIndices = (uint)(combined >> shift);
uint i0 = packedIndices & 0xFF;
uint i1 = (packedIndices >> 8) & 0xFF;
uint i2 = (packedIndices >> 16) & 0xFF;
outTris[triangleIndex] = uint3(i0, i1, i2);
}
}
float4 PSMain(PixelInput input) : SV_TARGET
{
PerMaterialData perMaterialData = LoadData<PerMaterialData>(g_PushConstantData.perMaterialBuffer, 0);
float4 color1 = SAMPLE_TEXTURE2D(perMaterialData.texture1, perMaterialData.tex_sampler, input.uv.xy);
float4 color2 = SAMPLE_TEXTURE2D(perMaterialData.texture2, perMaterialData.tex_sampler, input.uv.xy);
float4 color3 = SAMPLE_TEXTURE2D(perMaterialData.texture3, perMaterialData.tex_sampler, input.uv.xy);
float4 color4 = SAMPLE_TEXTURE2D(perMaterialData.texture4, perMaterialData.tex_sampler, input.uv.xy);
float4 blendedColor = (color1 + color2 + color3 + color4) * 0.25f;
return perMaterialData.color * blendedColor + input.color;
}

View File

@@ -1,111 +0,0 @@
using Ghost.Graphics.Test.Models;
using System.Diagnostics;
namespace Ghost.Graphics.Test.Services;
internal class LoggingService
{
private const int MAX_LOGS = 4096;
private static readonly Lazy<LoggingService> _instance = new(() => new LoggingService());
private readonly List<LogItem> _logs = [];
private readonly object _lockObject = new();
public static LoggingService Instance => _instance.Value;
public IReadOnlyList<LogItem> Logs
{
get
{
lock (_lockObject)
{
return _logs.AsReadOnly();
}
}
}
public bool CaptureStackTrace { get; set; } = false;
public event Action<LogItem>? LogAdded;
public event Action? LogsCleared;
private LoggingService()
{
}
private void AddLog(LogItem logItem)
{
lock (_lockObject)
{
if (_logs.Count >= MAX_LOGS)
{
_logs.RemoveAt(0);
}
_logs.Add(logItem);
}
// Invoke event outside of lock to prevent deadlock
LogAdded?.Invoke(logItem);
}
private string? CaptureCurrentStackTrace()
{
if (!CaptureStackTrace)
return null;
var stackTrace = new StackTrace(skipFrames: 2, fNeedFileInfo: true);
return stackTrace.ToString();
}
public void Log(LogLevel level, object? message)
{
var stackTrace = CaptureCurrentStackTrace();
var logItem = new LogItem(level, message?.ToString() ?? string.Empty, stackTrace);
AddLog(logItem);
}
public void LogInfo(object? message)
{
Log(LogLevel.Info, message);
}
public void LogWarning(object? message)
{
Log(LogLevel.Warning, message);
}
public void LogError(object? message)
{
Log(LogLevel.Error, message);
}
public void LogError(Exception exception)
{
var logItem = new LogItem(LogLevel.Error, exception.Message, exception.StackTrace);
AddLog(logItem);
}
public void LogDebug(object? message)
{
Log(LogLevel.Debug, message);
}
public void Clear()
{
lock (_lockObject)
{
_logs.Clear();
}
LogsCleared?.Invoke();
}
// Static methods for easier usage throughout the test project
public static void Info(object? message) => Instance.LogInfo(message);
public static void Warning(object? message) => Instance.LogWarning(message);
public static void Error(object? message) => Instance.LogError(message);
public static void Error(Exception exception) => Instance.LogError(exception);
public static void Debug(object? message) => Instance.LogDebug(message);
}

View File

@@ -9,7 +9,7 @@ namespace Ghost.Graphics.Test.Windows;
public sealed partial class GraphicsTestWindow : Window
{
private IRenderSystem? _renderSystem;
private RenderSystem? _renderSystem;
private IRenderer? _renderer;
private ISwapChain? _swapChain;