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

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,38 +35,58 @@ 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;
if (AutoScrollCheckBox.IsChecked == true)
{
LogScrollViewer.ScrollToVerticalOffset(LogScrollViewer.ScrollableHeight);
}
case NotifyCollectionChangedAction.Remove:
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
if (item is LogMessage logMessage)
{
_filteredLogs.Remove(logMessage);
}
}
}
break;
case NotifyCollectionChangedAction.Reset:
RefreshLogs();
break;
default:
break;
}
});
}
private void OnLogsCleared()
{
DispatcherQueue.TryEnqueue(() =>
{
_filteredLogs.Clear();
});
}
private void OnFilterChanged(object sender, RoutedEventArgs e)
{
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

@@ -0,0 +1,337 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.DSL.ShaderCompiler;
using Ghost.Graphics.Core;
using Ghost.Graphics.Core.Contracts;
using Ghost.Graphics.RenderGraphModule;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Utilities;
using Misaki.HighPerformance.Image;
using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Utilities;
using System.Runtime.InteropServices;
namespace Ghost.Graphics.RenderPasses;
internal class MeshRenderPassData
{
public Handle<Mesh> mesh;
public Handle<Material> material;
public Identifier<RGTexture> renderTarget;
}
internal class BlitPassData
{
public Identifier<RGTexture> source;
public Identifier<RGTexture> destination;
public Handle<Material> blitMaterial;
public Identifier<Sampler> sampler;
}
/// <summary>
/// Simplified bindless mesh render pass using high-level bindless APIs with fully bindless vertex/index buffer access
/// </summary>
internal class MeshRenderPass : IRenderPass
{
[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 Handle<Mesh> _mesh;
private Identifier<Shader> _shader;
private Handle<Material> _material;
private Handle<Texture>[]? _textures;
private Identifier<Sampler> _sampler;
private Identifier<Shader> _blitShader;
private Handle<Material> _blitMaterial;
// Texture file paths for this demo
private readonly string[] _textureFiles = [
"C:/Users/Misaki/Downloads/Im/Icon.png",
"C:/Users/Misaki/Downloads/Im/Backdrop.jpg",
"C:/Users/Misaki/Downloads/Im/101167591_p0.png",
"C:/Users/Misaki/Downloads/Im/yande.re 1134666 blue_archive nakamasa_ichika sugarhigh.jpg"
];
private static IEnumerable<ReadOnlyMemory<string>> GetAllVariantCombination(KeywordsGroup[] keywordsGroups)
{
if (keywordsGroups.Length == 0)
{
yield return ReadOnlyMemory<string>.Empty;
yield break;
}
var firstGroup = keywordsGroups[0];
var remainingGroups = keywordsGroups[1..];
foreach (var combination in GetAllVariantCombination(remainingGroups))
{
yield return combination;
}
foreach (var keyword in firstGroup.keywords)
{
foreach (var combination in GetAllVariantCombination(remainingGroups))
{
var array = new string[combination.Length + 1];
array[0] = keyword;
combination.Span.CopyTo(array.AsSpan(1));
yield return array;
}
}
}
private void CompileBlitShader(ref readonly RenderingContext ctx)
{
var shaderDescriptor = DSLShaderCompiler.CompileShader("F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/Shaders/Blit.gshdr", "C:/Users/Misaki/Downloads/Archive").GetValueOrThrow();
_blitShader = ctx.ResourceManager.CreateGraphicsShader(shaderDescriptor);
_blitMaterial = ctx.ResourceManager.CreateMaterial(_blitShader);
var config = new ShaderCompilationConfig
{
optimizeLevel = CompilerOptimizeLevel.O3,
options = CompilerOption.KeepReflections,
tier = CompilerTier.Tier2
};
var pass = shaderDescriptor.passes[0];
var emptyKeywords = new LocalKeywordSet();
var variantKey = RHIUtility.CreateShaderVariantKey(
RHIUtility.CreateShaderPassKey(pass.identifier),
in emptyKeywords);
ctx.ShaderCompiler.CompilePass(in pass, in config, variantKey).GetValueOrThrow();
}
public void Initialize(ref readonly RenderingContext ctx)
{
CompileBlitShader(in ctx);
var shaderDescriptor = DSLShaderCompiler.CompileShader("F:/csharp/GhostEngine/src/Runtime/Ghost.Graphics/test.gshdr", "C:/Users/Misaki/Downloads/Archive").GetValueOrThrow();
_shader = ctx.ResourceManager.CreateGraphicsShader(shaderDescriptor);
_material = ctx.ResourceManager.CreateMaterial(_shader);
for (var i = 0; i < shaderDescriptor.passes.Length; i++)
{
ref var pass = ref shaderDescriptor.passes[i];
var config = new ShaderCompilationConfig
{
optimizeLevel = CompilerOptimizeLevel.O3,
options = CompilerOption.KeepReflections,
tier = CompilerTier.Tier2
};
// TODO: Ideally, in editor mode, we compile a single variant when it's needed during rendering. Before the compilation is done, we fallback to a special "compilation in progress" shader.
// During the build process, we can precompile all the variants and store them in the cache for fast loading in runtime.
// After the compilation, we should store the compiled result in the disk cache even in editor mode. This allows us to avoid recompiling the same variant, same code hash and same version) multiple times.
if (pass.keywords.Length == 0)
{
var emptyKeywords = new LocalKeywordSet();
var variantKey = RHIUtility.CreateShaderVariantKey(
RHIUtility.CreateShaderPassKey(pass.identifier),
in emptyKeywords);
ctx.ShaderCompiler.CompilePass(in pass, in config, variantKey).GetValueOrThrow();
}
else
{
var shaderResult = ctx.ResourceManager.GetShaderReference(_shader);
if (shaderResult.IsFailure)
{
throw new InvalidOperationException("Failed to get shader reference.");
}
ref readonly var shaderRef = ref shaderResult.Value;
foreach (var keyGroup in GetAllVariantCombination(pass.keywords))
{
config.defines = keyGroup.Span;
var keywordsSet = new LocalKeywordSet();
foreach (var key in keyGroup.Span)
{
var localIndex = shaderRef.GetLocalKeywordIndex(Shader.GetKeywordID(key));
if (localIndex == -1)
{
continue;
}
keywordsSet.SetKeyword(localIndex, true);
}
var variantKey = RHIUtility.CreateShaderVariantKey(
RHIUtility.CreateShaderPassKey(pass.identifier),
in keywordsSet);
ctx.ShaderCompiler.CompilePass(in pass, in config, variantKey).GetValueOrThrow();
}
}
}
MeshBuilder.CreateCube(0.75f, default, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent, out var vertices, out var indices);
_mesh = ctx.CreateMesh(vertices, indices, true);
// Cook meshlets for the mesh
var meshRef = ctx.ResourceManager.GetMeshReference(_mesh);
if (meshRef.IsSuccess)
{
meshRef.Value.CookMeshlets();
}
ctx.UploadMeshlets(_mesh);
ctx.UpdateObjectData(_mesh, float4x4.identity);
_textures = new Handle<Texture>[_textureFiles.Length];
for (var i = 0; i < _textureFiles.Length; i++)
{
using var stream = File.OpenRead(_textureFiles[i]);
using var imageData = ImageResult.FromStream(stream, ColorComponents.RGBA);
var desc = new TextureDesc
{
Width = imageData.Width,
Height = imageData.Height,
Dimension = TextureDimension.Texture2D,
Format = TextureFormat.R8G8B8A8_UNorm,
MipLevels = 1,
Slice = 1,
Usage = TextureUsage.ShaderResource,
};
_textures[i] = ctx.CreateTexture<byte>(in desc, imageData.AsSpan(), $"Texture_{i}");
}
var samplerDesc = new SamplerDesc
{
AddressU = TextureAddressMode.Repeat,
AddressV = TextureAddressMode.Repeat,
AddressW = TextureAddressMode.Repeat,
FilterMode = TextureFilterMode.Bilinear,
MaxAnisotropy = 16,
};
_sampler = ctx.ResourceAllocator.CreateSampler(in samplerDesc);
var meshResult = ctx.ResourceManager.GetMaterialReference(_material);
if (meshResult.IsFailure)
{
throw new InvalidOperationException("Failed to get material reference.");
}
ref var matRef = ref meshResult.Value;
var matProps = new ShaderProperties_MyShader_Standard
{
color = new float4(1.0f, 1.0f, 1.0f, 1.0f),
texture1 = ctx.ResourceDatabase.GetBindlessIndex(_textures[0].AsResource()),
texture2 = ctx.ResourceDatabase.GetBindlessIndex(_textures[1].AsResource()),
texture3 = ctx.ResourceDatabase.GetBindlessIndex(_textures[2].AsResource()),
texture4 = ctx.ResourceDatabase.GetBindlessIndex(_textures[3].AsResource()),
tex_sampler = (uint)_sampler.Value,
};
matRef.SetPropertyCache(in matProps).ThrowIfFailed();
matRef.UploadData(ctx.DirectCommandBuffer, ctx.ResourceDatabase);
}
public void Build(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));
});
}
}
public void Cleanup(ResourceManager resourceManager, IResourceDatabase resourceDatabase)
{
resourceManager.ReleaseMaterial(_blitMaterial);
resourceManager.ReleaseMaterial(_material);
resourceManager.ReleaseShader(_shader);
resourceManager.ReleaseMesh(_mesh);
resourceDatabase.ReleaseSampler(_sampler);
if (_textures != null)
{
foreach (var texture in _textures)
{
resourceDatabase.ReleaseResource(texture.AsResource());
}
}
}
}

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;