Added new RHI abstraction layer;

Added new console debug page to UnitTest;
This commit is contained in:
2025-08-25 10:48:59 +09:00
parent eafbfb2fa1
commit 5385141f14
44 changed files with 3473 additions and 357 deletions

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Ghost.UnitTest.Controls.DebugConsole"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.UnitTest.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<UserControl.Resources>
<local:LogLevelToColorConverter x:Key="LogLevelToColorConverter" />
<local:LogLevelToSymbolConverter x:Key="LogLevelToSymbolConverter" />
<DataTemplate x:Key="LogItemTemplate">
<Border Padding="8,4" Background="Transparent">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="0,0,8,0"
VerticalAlignment="Center"
FontFamily="Segoe UI Symbol"
Foreground="{Binding Level, Converter={StaticResource LogLevelToColorConverter}}"
Text="{Binding Level, Converter={StaticResource LogLevelToSymbolConverter}}" />
<TextBlock
Grid.Column="1"
Margin="0,0,8,0"
VerticalAlignment="Center"
FontFamily="Consolas"
Foreground="Gray"
Text="{Binding Timestamp}" />
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
Text="{Binding Message}"
TextWrapping="Wrap" />
</Grid>
</Border>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Toolbar -->
<Border
Grid.Row="0"
Background="{ThemeResource SystemControlBackgroundAltMediumBrush}"
BorderBrush="{ThemeResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1">
<StackPanel Margin="8,4" Orientation="Horizontal">
<Button
x:Name="ClearButton"
Margin="0,0,8,0"
Click="ClearButton_Click"
Content="Clear" />
<CheckBox
x:Name="AutoScrollCheckBox"
Margin="0,0,8,0"
Content="Auto Scroll"
IsChecked="True" />
<CheckBox
x:Name="ShowStackTraceCheckBox"
Margin="0,0,8,0"
Checked="ShowStackTraceCheckBox_Checked"
Content="Stack Trace"
Unchecked="ShowStackTraceCheckBox_Unchecked" />
<!-- Log level filters -->
<TextBlock
Margin="16,0,8,0"
VerticalAlignment="Center"
Text="Show:" />
<CheckBox
x:Name="ShowInfoCheckBox"
Margin="0,0,4,0"
Content="Info"
IsChecked="True" />
<CheckBox
x:Name="ShowWarningCheckBox"
Margin="0,0,4,0"
Content="Warning"
IsChecked="True" />
<CheckBox
x:Name="ShowErrorCheckBox"
Margin="0,0,4,0"
Content="Error"
IsChecked="True" />
<CheckBox
x:Name="ShowDebugCheckBox"
Margin="0,0,4,0"
Content="Debug"
IsChecked="True" />
</StackPanel>
</Border>
<!-- Log display -->
<ScrollViewer
x:Name="LogScrollViewer"
Grid.Row="1"
HorizontalScrollBarVisibility="Auto"
HorizontalScrollMode="Auto"
VerticalScrollBarVisibility="Auto"
VerticalScrollMode="Auto"
ZoomMode="Disabled">
<ItemsRepeater x:Name="LogItemsRepeater" ItemTemplate="{StaticResource LogItemTemplate}" />
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -0,0 +1,165 @@
using Ghost.UnitTest.Models;
using Ghost.UnitTest.Services;
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;
namespace Ghost.UnitTest.Controls;
public sealed partial class DebugConsole : UserControl
{
private readonly ObservableCollection<LogItem> _filteredLogs = [];
private readonly LoggingService _loggingService;
public DebugConsole()
{
InitializeComponent();
_loggingService = LoggingService.Instance;
LogItemsRepeater.ItemsSource = _filteredLogs;
// Subscribe to logging events
_loggingService.LogAdded += OnLogAdded;
_loggingService.LogsCleared += OnLogsCleared;
// Subscribe to filter changes
ShowInfoCheckBox.Checked += OnFilterChanged;
ShowInfoCheckBox.Unchecked += OnFilterChanged;
ShowWarningCheckBox.Checked += OnFilterChanged;
ShowWarningCheckBox.Unchecked += OnFilterChanged;
ShowErrorCheckBox.Checked += OnFilterChanged;
ShowErrorCheckBox.Unchecked += OnFilterChanged;
ShowDebugCheckBox.Checked += OnFilterChanged;
ShowDebugCheckBox.Unchecked += OnFilterChanged;
// Load existing logs
RefreshLogs();
}
private void OnLogAdded(LogItem logItem)
{
DispatcherQueue.TryEnqueue(() =>
{
if (ShouldShowLogItem(logItem))
{
_filteredLogs.Add(logItem);
if (AutoScrollCheckBox.IsChecked == true)
{
LogScrollViewer.ScrollToVerticalOffset(LogScrollViewer.ScrollableHeight);
}
}
});
}
private void OnLogsCleared()
{
DispatcherQueue.TryEnqueue(() =>
{
_filteredLogs.Clear();
});
}
private void OnFilterChanged(object sender, RoutedEventArgs e)
{
RefreshLogs();
}
private bool ShouldShowLogItem(LogItem logItem)
{
return logItem.Level switch
{
LogLevel.Info => ShowInfoCheckBox.IsChecked == true,
LogLevel.Warning => ShowWarningCheckBox.IsChecked == true,
LogLevel.Error => ShowErrorCheckBox.IsChecked == true,
LogLevel.Debug => ShowDebugCheckBox.IsChecked == true,
_ => true
};
}
private void RefreshLogs()
{
_filteredLogs.Clear();
foreach (var log in _loggingService.Logs)
{
if (ShouldShowLogItem(log))
{
_filteredLogs.Add(log);
}
}
if (AutoScrollCheckBox.IsChecked == true)
{
LogScrollViewer.ScrollToVerticalOffset(LogScrollViewer.ScrollableHeight);
}
}
private void ClearButton_Click(object sender, RoutedEventArgs e)
{
_loggingService.Clear();
}
private void ShowStackTraceCheckBox_Checked(object sender, RoutedEventArgs e)
{
_loggingService.CaptureStackTrace = true;
}
private void ShowStackTraceCheckBox_Unchecked(object sender, RoutedEventArgs e)
{
_loggingService.CaptureStackTrace = false;
}
}
// Converter for log level to color
public class LogLevelToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is LogLevel level)
{
return level switch
{
LogLevel.Info => new SolidColorBrush(Colors.DodgerBlue),
LogLevel.Warning => new SolidColorBrush(Colors.Orange),
LogLevel.Error => new SolidColorBrush(Colors.Red),
LogLevel.Debug => new SolidColorBrush(Colors.Gray),
_ => new SolidColorBrush(Colors.Black)
};
}
return new SolidColorBrush(Colors.Black);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
// Converter for log level to symbol
public class LogLevelToSymbolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is LogLevel level)
{
return level switch
{
LogLevel.Info => "",
LogLevel.Warning => "⚠",
LogLevel.Error => "✖",
LogLevel.Debug => "🐛",
_ => "•"
};
}
return "•";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -12,6 +12,10 @@
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="Controls\DebugConsole.xaml" />
<None Remove="Windows\DebugOutputWindow.xaml" />
</ItemGroup>
<ItemGroup>
<Page Remove="UnitTestApp.xaml" />
@@ -59,6 +63,18 @@
<HintPath>..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.LowLevel\bin\Release\net9.0\Misaki.HighPerformance.LowLevel.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Page Update="Windows\DebugOutputWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\DebugConsole.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
@@ -76,5 +92,6 @@
<PublishTrimmed>False</PublishTrimmed>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,42 @@
using System;
namespace Ghost.UnitTest.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

@@ -2,7 +2,7 @@
"profiles": {
"Ghost.UnitTest (Package)": {
"commandName": "MsixPackage",
"nativeDebugging": true
"nativeDebugging": false
},
"Ghost.UnitTest (Unpackaged)": {
"commandName": "Project"

View File

@@ -0,0 +1,111 @@
using Ghost.UnitTest.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading;
namespace Ghost.UnitTest.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

@@ -1,10 +1,11 @@
using Ghost.Entities;
using Ghost.Entities.Components;
using Ghost.Entities.Systems;
using Ghost.UnitTest.Services;
using Ghost.UnitTest.TestFramework;
using System.Numerics;
namespace Ghost.UnitTest;
namespace Ghost.UnitTest.Test;
public partial class EntityTest : ITest
{
@@ -65,7 +66,7 @@ public class TestSystem : ISystem
{
foreach (var (entity, transform) in state.World.Query<Transform>())
{
Console.WriteLine($"Entity {entity.ID}: Transform Position = {transform.ValueRO.position}");
LoggingService.Info($"Entity {entity.ID}: Transform Position = {transform.ValueRO.position}");
}
}
@@ -85,7 +86,7 @@ public class TestSystem2 : ISystem
{
foreach (var (entity, mesh) in state.World.Query<Mesh>())
{
Console.WriteLine($"Entity {entity.ID}: Mesh Index = {mesh.ValueRO.index}");
LoggingService.Info($"Entity {entity.ID}: Mesh Index = {mesh.ValueRO.index}");
}
}
@@ -112,17 +113,17 @@ public class UserScript : ScriptComponent
public override void Start()
{
Console.WriteLine("UserScript started for entity: " + Owner.ID);
LoggingService.Info("UserScript started for entity: " + Owner.ID);
}
public override void Update()
{
Console.WriteLine("UserScript updating for entity: " + Owner.ID);
LoggingService.Info("UserScript updating for entity: " + Owner.ID);
}
public override void OnDestroy()
{
Console.WriteLine("UserScript destroyed for entity: " + Owner.ID);
LoggingService.Info("UserScript destroyed for entity: " + Owner.ID);
}
}
@@ -130,17 +131,17 @@ public class UIManager : ScriptComponent
{
public override void Start()
{
Console.WriteLine("UIManager started for entity: " + Owner.ID);
LoggingService.Info("UIManager started for entity: " + Owner.ID);
}
public override void Update()
{
Console.WriteLine("UIManager updating for entity: " + Owner.ID);
LoggingService.Info("UIManager updating for entity: " + Owner.ID);
}
public override void OnDestroy()
{
Console.WriteLine("UIManager destroyed for entity: " + Owner.ID);
LoggingService.Info("UIManager destroyed for entity: " + Owner.ID);
}
}
@@ -148,16 +149,16 @@ public class EventManager : ScriptComponent
{
public override void Start()
{
Console.WriteLine("EventManager started for entity: " + Owner.ID);
LoggingService.Info("EventManager started for entity: " + Owner.ID);
}
public override void Update()
{
Console.WriteLine("EventManager updating for entity: " + Owner.ID);
LoggingService.Info("EventManager updating for entity: " + Owner.ID);
}
public override void OnDestroy()
{
Console.WriteLine("EventManager destroyed for entity: " + Owner.ID);
LoggingService.Info("EventManager destroyed for entity: " + Owner.ID);
}
}

View File

@@ -1,4 +1,7 @@
using Microsoft.UI.Xaml;
using Ghost.UnitTest.Test;
using Ghost.UnitTest.TestFramework;
using Ghost.UnitTest.Windows;
using Microsoft.UI.Xaml;
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
// To learn more about WinUI, the WinUI project structure,
@@ -29,11 +32,12 @@ public partial class UnitTestApp : Application
{
Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.CreateDefaultUI();
_window = new UnitTestAppWindow();
_window = new DebugOutputWindow();
_window.Activate();
UITestMethodAttribute.DispatcherQueue = _window.DispatcherQueue;
Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.Run(Environment.CommandLine);
TestRunner.Run<EntityTest>();
}
}

View File

@@ -1,22 +0,0 @@
using Microsoft.UI.Xaml.Controls;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
namespace Ghost.UnitTest;
[TestClass]
public partial class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
Assert.AreEqual(0, 0);
}
// Use the UITestMethod attribute for tests that need to run on the UI thread.
[UITestMethod]
public void TestMethod2()
{
var grid = new Grid();
Assert.AreEqual(0, grid.MinWidth);
}
}

View File

@@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="Ghost.UnitTest.UnitTestAppWindow"
x:Class="Ghost.UnitTest.Windows.DebugOutputWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Ghost.UnitTest.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.UnitTest"
xmlns:local="using:Ghost.UnitTest.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="Ghost.UnitTest"
Title="DebugOutputWindow"
mc:Ignorable="d">
<Window.SystemBackdrop>
@@ -14,9 +15,6 @@
</Window.SystemBackdrop>
<Grid>
<SwapChainPanel
x:Name="Panel"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<controls:DebugConsole HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
</Grid>
</Window>

View File

@@ -0,0 +1,11 @@
using Microsoft.UI.Xaml;
namespace Ghost.UnitTest.Windows;
internal sealed partial class DebugOutputWindow : Window
{
public DebugOutputWindow()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="Ghost.UnitTest.Windows.GraphicsTestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Ghost.UnitTest.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.UnitTest.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="GraphicsTestWindow"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="300" MinHeight="150" />
</Grid.RowDefinitions>
<!-- Main test content area -->
<SwapChainPanel
x:Name="Panel"
Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<!-- Splitter -->
<Border
Grid.Row="1"
Height="4"
HorizontalAlignment="Stretch"
Background="{ThemeResource SystemControlBackgroundBaseLowBrush}" />
<!-- Debug Console -->
<Border
Grid.Row="2"
Background="{ThemeResource SystemControlBackgroundAltHighBrush}"
BorderBrush="{ThemeResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,1,0,0">
<controls:DebugConsole x:Name="DebugConsole" />
</Border>
</Grid>
</Window>

View File

@@ -6,14 +6,14 @@ using Microsoft.UI.Xaml.Media;
using Misaki.HighPerformance.LowLevel.Buffer;
using WinRT;
namespace Ghost.UnitTest;
namespace Ghost.UnitTest.Windows;
public sealed partial class UnitTestAppWindow : Window
public sealed partial class GraphicsTestWindow : Window
{
private Renderer? _renderer;
private ISwapChainPanelNative _swapChainPanelNative;
public UnitTestAppWindow()
public GraphicsTestWindow()
{
InitializeComponent();
@@ -35,7 +35,7 @@ public sealed partial class UnitTestAppWindow : Window
((IWinRTObject)Panel).NativeObject.TryAs(guid, out var swapChainPanelNativeHandle);
_swapChainPanelNative = new ISwapChainPanelNative(swapChainPanelNativeHandle);
_renderer = GraphicsPipeline.GraphicsDevice.CreateRenderer(new(_swapChainPanelNative, (uint)AppWindow.Size.Width, (uint)AppWindow.Size.Height));
//_renderer = GraphicsPipeline.GraphicsDevice.CreateRenderer(new(_swapChainPanelNative, (uint)AppWindow.Size.Width, (uint)AppWindow.Size.Height));
CompositionTarget.Rendering += OnRendering;
}
@@ -61,12 +61,12 @@ public sealed partial class UnitTestAppWindow : Window
private void OnRendering(object? sender, object e)
{
if (GraphicsPipeline.CPUFenceValue < GraphicsPipeline.GPUFenceValue + GraphicsPipeline._FRAME_COUNT)
{
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.High, () =>
{
GraphicsPipeline.SignalCPUReady();
});
}
//if (GraphicsPipeline.CPUFenceValue < GraphicsPipeline.GPUFenceValue + GraphicsPipeline._FRAME_COUNT)
//{
// DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.High, () =>
// {
// GraphicsPipeline.SignalCPUReady();
// });
//}
}
}