From 7c2999dea987e86f8f2f1bad79ea68ebd96044a2 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 16 Apr 2026 11:55:04 +0200 Subject: [PATCH 1/4] Code --- .vscode/settings.json | 5 + AssetEditor/DependencyInjectionContainer.cs | 2 + AssetEditor/Language_Cn.json | 2 + AssetEditor/Language_En.json | 2 + .../GraphicsResourceExceptionInfoProvider.cs | 61 ++++ .../PrintTrackedGraphicsResourcesCommand.cs | 74 +++++ AssetEditor/ViewModels/MenuBarViewModel.cs | 3 + AssetEditor/Views/MenuBarView.xaml | 9 + .../ViewModels/TextureBuilder.cs | 6 +- .../Editor/Rendering/TwuiPreviewBuilder.cs | 14 +- .../Editor/Rendering/TwuiRenderComponent.cs | 13 +- GameWorld/View3D/Components/Gizmo/Gizmo.cs | 17 +- .../View3D/Components/Gizmo/GizmoComponent.cs | 6 +- .../GraphicsResourceStatsComponent.cs | 49 +++ .../Components/Navigation/ViewportGizmo.cs | 10 +- .../Components/Rendering/RasterStateHelper.cs | 17 +- .../Rendering/RenderEngineComponent.cs | 31 +- .../Rendering/RenderTargetHelper.cs | 11 +- .../Selection/SelectionComponent.cs | 9 +- .../Components/Selection/SelectionManager.cs | 10 +- .../View3D/DependencyInjectionContainer.cs | 9 +- GameWorld/View3D/Rendering/BloomFilter.cs | 28 +- .../Geometry/IGraphicsCardGeometry.cs | 35 +-- .../Materials/Shaders/BasicShader.cs | 16 +- .../Rendering/TextureToTextureRenderer.cs | 10 +- .../View3D/Rendering/VertexInstanceMesh.cs | 16 +- .../Services/GraphicsResourceCreator.cs | 280 ++++++++++++++++++ GameWorld/View3D/Services/ResourceLibary.cs | 8 +- .../View3D/Services/ScopedResourceLibrary.cs | 10 +- .../GraphicsResourceExceptionInfoProvider.cs | 61 ++++ GameWorld/View3D/Utility/ImageLoader.cs | 20 +- GameWorld/View3D/WpfWindow/D3D11Host.cs | 21 +- GameWorld/View3D/WpfWindow/WpfGame.cs | 2 +- .../DependencyInjection/IScopeOwnerAware.cs | 7 + .../DependencyInjection/ScopeRepository.cs | 7 +- .../Exceptions/ExceptionInformation.cs | 6 + .../CustomExceptionWindow.xaml.cs | 12 + 37 files changed, 765 insertions(+), 134 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 AssetEditor/Services/GraphicsResourceExceptionInfoProvider.cs create mode 100644 AssetEditor/UiCommands/PrintTrackedGraphicsResourcesCommand.cs create mode 100644 GameWorld/View3D/Components/GraphicsResourceStatsComponent.cs create mode 100644 GameWorld/View3D/Services/GraphicsResourceCreator.cs create mode 100644 GameWorld/View3D/Utility/GraphicsResourceExceptionInfoProvider.cs create mode 100644 Shared/SharedCore/Shared.Core/DependencyInjection/IScopeOwnerAware.cs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..9a85d1da1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "chat.tools.terminal.autoApprove": { + "ForEach-Object": true + } +} \ No newline at end of file diff --git a/AssetEditor/DependencyInjectionContainer.cs b/AssetEditor/DependencyInjectionContainer.cs index 9633f055f..bfcff145b 100644 --- a/AssetEditor/DependencyInjectionContainer.cs +++ b/AssetEditor/DependencyInjectionContainer.cs @@ -28,6 +28,7 @@ public override void Register(IServiceCollection serviceCollection) serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); @@ -43,6 +44,7 @@ public override void Register(IServiceCollection serviceCollection) serviceCollection.AddScoped(); + RegisterAllAsInterface(serviceCollection, ServiceLifetime.Transient); } } diff --git a/AssetEditor/Language_Cn.json b/AssetEditor/Language_Cn.json index 4bd6e633e..4daaeed54 100644 --- a/AssetEditor/Language_Cn.json +++ b/AssetEditor/Language_Cn.json @@ -133,6 +133,8 @@ "MenuBar.Reports.TouchedFiles.StopRecorder": "停止记录", "MenuBar.Reports.DebugClearConsole": "调试 - 清空控制台", "MenuBar.Reports.DebugPrintScopes": "调试 - 打印作用域", + "MenuBar.Debug": "调试", + "MenuBar.Debug.PrintTrackedGraphicsResources": "打印已跟踪图形资源", "MenuBar.Help": "帮助", "MenuBar.Help.AssetEditorHelp": "AssetEditor 帮助", "MenuBar.Help.ModdingWiki": "Mod 百科", diff --git a/AssetEditor/Language_En.json b/AssetEditor/Language_En.json index e39749eaf..bcff2d230 100644 --- a/AssetEditor/Language_En.json +++ b/AssetEditor/Language_En.json @@ -133,6 +133,8 @@ "MenuBar.Reports.TouchedFiles.StopRecorder": "Stop Recorder", "MenuBar.Reports.DebugClearConsole": "Debug - Clear Console", "MenuBar.Reports.DebugPrintScopes": "Debug - Print Scopes", + "MenuBar.Debug": "Debug", + "MenuBar.Debug.PrintTrackedGraphicsResources": "Print Tracked Graphics Resources", "MenuBar.Help": "Help", "MenuBar.Help.AssetEditorHelp": "AssetEditor Help", "MenuBar.Help.ModdingWiki": "Modding Wiki", diff --git a/AssetEditor/Services/GraphicsResourceExceptionInfoProvider.cs b/AssetEditor/Services/GraphicsResourceExceptionInfoProvider.cs new file mode 100644 index 000000000..3cdeac5d0 --- /dev/null +++ b/AssetEditor/Services/GraphicsResourceExceptionInfoProvider.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GameWorld.Core.Services; +using Shared.Core.DependencyInjection; +using Shared.Core.ErrorHandling.Exceptions; +using Shared.Core.ToolCreation; + +namespace AssetEditor.Services +{ + internal class GraphicsResourceExceptionInfoProvider : IExceptionInformationProvider + { + private readonly IEditorManager _editorManager; + private readonly IScopeRepository _scopeRepository; + + public GraphicsResourceExceptionInfoProvider(IEditorManager editorManager, IScopeRepository scopeRepository) + { + _editorManager = editorManager; + _scopeRepository = scopeRepository; + } + + public void HydrateExcetion(ExceptionInformation exceptionInformation) + { + try + { + var allEditors = _editorManager.GetAllEditors(); + var currentEditorIndex = _editorManager.GetCurrentEditor(); + var hasCurrentEditor = currentEditorIndex >= 0 && currentEditorIndex < allEditors.Count; + var currentEditor = hasCurrentEditor ? allEditors[currentEditorIndex] : null; + + var editorHandles = _scopeRepository.GetEditorHandles(); + + foreach (var editor in editorHandles) + { + var isCurrentScope = currentEditor != null && ReferenceEquals(editor, currentEditor); + + try + { + var creator = _scopeRepository.GetRequiredService(editor); + var records = creator.Records; + var resourceLines = records + .Select(x => $"id={x.ResourceId} | {x.ResourceType} | owner={x.ScopeOwner} | source={x.SourceFile}:{x.SourceLine}::{x.SourceMember}") + .ToList(); + + exceptionInformation.GraphicsResourceScopes.Add( + new GraphicsResourceScopeInfo(creator.ScopeOwner, isCurrentScope, records.Count, resourceLines)); + } + catch + { + exceptionInformation.GraphicsResourceScopes.Add( + new GraphicsResourceScopeInfo($"{editor.DisplayName} ({editor.GetType().Name})", isCurrentScope, 0, new List())); + } + } + } + catch (Exception e) + { + exceptionInformation.CurrentEditorGraphicsResourceInfoError = e.Message; + } + } + } +} diff --git a/AssetEditor/UiCommands/PrintTrackedGraphicsResourcesCommand.cs b/AssetEditor/UiCommands/PrintTrackedGraphicsResourcesCommand.cs new file mode 100644 index 000000000..32a278456 --- /dev/null +++ b/AssetEditor/UiCommands/PrintTrackedGraphicsResourcesCommand.cs @@ -0,0 +1,74 @@ +using System; +using System.Text; +using GameWorld.Core.Services; +using Serilog; +using Shared.Core.DependencyInjection; +using Shared.Core.ErrorHandling; +using Shared.Core.Events; +using Shared.Core.ToolCreation; + +namespace AssetEditor.UiCommands +{ + public class PrintTrackedGraphicsResourcesCommand : IUiCommand + { + private readonly ILogger _logger = Logging.Create(); + private readonly IScopeRepository _scopeRepository; + private readonly IEditorManager _editorManager; + + public PrintTrackedGraphicsResourcesCommand(IScopeRepository scopeRepository, IEditorManager editorManager) + { + _scopeRepository = scopeRepository; + _editorManager = editorManager; + } + + public void Execute() + { + var builder = new StringBuilder(); + var allEditors = _editorManager.GetAllEditors(); + var currentEditorIndex = _editorManager.GetCurrentEditor(); + var hasCurrentEditor = currentEditorIndex >= 0 && currentEditorIndex < allEditors.Count; + var currentEditor = hasCurrentEditor ? allEditors[currentEditorIndex] : null; + + var editorHandles = _scopeRepository.GetEditorHandles(); + + builder.AppendLine(); + builder.AppendLine("=== Graphics Resource Tracker Dump ==="); + builder.AppendLine($"Scopes: {editorHandles.Count}"); + + var totalTrackedResources = 0; + foreach (var editor in editorHandles) + { + var isCurrentScope = currentEditor != null && ReferenceEquals(editor, currentEditor); + var marker = isCurrentScope ? " [CURRENT]" : string.Empty; + builder.AppendLine($"Scope: {editor.DisplayName} ({editor.GetType().Name}){marker}"); + + try + { + var creator = _scopeRepository.GetRequiredService(editor); + var records = creator.Records; + builder.AppendLine($" Items in scope: {records.Count}"); + if (records.Count == 0) + { + builder.AppendLine(" (no tracked graphics resources)"); + continue; + } + + foreach (var record in records) + { + totalTrackedResources++; + builder.AppendLine($" - id={record.ResourceId} | {record.ResourceType} | owner={record.ScopeOwner} | source={record.SourceFile}:{record.SourceLine}::{record.SourceMember}"); + } + } + catch + { + builder.AppendLine(" Items in scope: 0"); + builder.AppendLine(" (no graphics resource tracker in this scope)"); + } + } + + builder.AppendLine($"Total tracked items across all scopes: {totalTrackedResources}"); + builder.AppendLine("=== End Graphics Resource Tracker Dump ==="); + _logger.Here().Information(builder.ToString()); + } + } +} diff --git a/AssetEditor/ViewModels/MenuBarViewModel.cs b/AssetEditor/ViewModels/MenuBarViewModel.cs index 81e6854a6..f9fc92325 100644 --- a/AssetEditor/ViewModels/MenuBarViewModel.cs +++ b/AssetEditor/ViewModels/MenuBarViewModel.cs @@ -102,8 +102,11 @@ [RelayCommand] private void CreateNewPackFile() [RelayCommand] private void TouchedFileRecorderExtract() => _touchedFilesRecorder.ExtractFilesToPack(@"c:\temp\extractedPack.pack"); [RelayCommand] private void TouchedFileRecorderStop() => _touchedFilesRecorder.Stop(); + public bool IsDebuggerAttached => Debugger.IsAttached; + [RelayCommand] private void ClearConsole() => Console.Clear(); [RelayCommand] private void PrintScope() => _uiCommandFactory.Create().Execute(); + [RelayCommand] private void PrintTrackedGraphicsResources() => _uiCommandFactory.Create().Execute(); [RelayCommand] private void Search() => _uiCommandFactory.Create().Execute(); [RelayCommand] private void OpenAttilaPacks() => _uiCommandFactory.Create().Execute(GameTypeEnum.Attila); [RelayCommand] private void OpenRomeRemasteredPacks() => _uiCommandFactory.Create().Execute(GameTypeEnum.RomeRemastered); diff --git a/AssetEditor/Views/MenuBarView.xaml b/AssetEditor/Views/MenuBarView.xaml index 883b81eed..fecc0adff 100644 --- a/AssetEditor/Views/MenuBarView.xaml +++ b/AssetEditor/Views/MenuBarView.xaml @@ -9,6 +9,10 @@ mc:Ignorable="d" d:DesignHeight="30" d:DesignWidth="800"> + + + + + + + + diff --git a/Editors/TextureEditor/ViewModels/TextureBuilder.cs b/Editors/TextureEditor/ViewModels/TextureBuilder.cs index 78ee9afcb..61c0af518 100644 --- a/Editors/TextureEditor/ViewModels/TextureBuilder.cs +++ b/Editors/TextureEditor/ViewModels/TextureBuilder.cs @@ -17,14 +17,16 @@ public class TextureBuilder private readonly IScopedResourceLibrary _resourceLib; private readonly TextureToTextureRenderer _textureRenderer; private readonly IWpfGame _wpfGame; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; - public TextureBuilder(IScopedResourceLibrary resourceLibrary, IWpfGame wpfGame) + public TextureBuilder(IScopedResourceLibrary resourceLibrary, IWpfGame wpfGame, IGraphicsResourceCreator graphicsResourceCreator) { _resourceLib = resourceLibrary; _wpfGame = wpfGame; + _graphicsResourceCreator = graphicsResourceCreator; _wpfGame.ForceEnsureCreated(); - _textureRenderer = new TextureToTextureRenderer(_wpfGame.GraphicsDevice, new SpriteBatch(_wpfGame.GraphicsDevice), _resourceLib); + _textureRenderer = new TextureToTextureRenderer(_wpfGame.GraphicsDevice, _graphicsResourceCreator.CreateSpriteBatch(), _resourceLib, _graphicsResourceCreator); } public void Build(TexturePreviewViewModel viewModel, string imagePath) diff --git a/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiPreviewBuilder.cs b/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiPreviewBuilder.cs index 989844da0..e224e507d 100644 --- a/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiPreviewBuilder.cs +++ b/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiPreviewBuilder.cs @@ -15,22 +15,24 @@ public class TwuiPreviewBuilder { private readonly IWpfGame _wpfGame; private readonly IScopedResourceLibrary _resourceLibrary; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; private readonly float _invMaxLayerDepth = 1f / 999999f; private RenderTarget2D _renderTarget; private SpriteBatch _spriteBatch; private Texture2D _whiteSquareTexture; - public TwuiPreviewBuilder(IWpfGame wpfGame, IScopedResourceLibrary resourceLibrary) + public TwuiPreviewBuilder(IWpfGame wpfGame, IScopedResourceLibrary resourceLibrary, IGraphicsResourceCreator graphicsResourceCreator) { _wpfGame = wpfGame; _resourceLibrary = resourceLibrary; + _graphicsResourceCreator = graphicsResourceCreator; } public void Initialize() { - _spriteBatch = new SpriteBatch(_wpfGame.GraphicsDevice); - _whiteSquareTexture = new Texture2D(_wpfGame.GraphicsDevice, 1, 1); + _spriteBatch = _graphicsResourceCreator.CreateSpriteBatch(); + _whiteSquareTexture = _graphicsResourceCreator.CreateTexture2D(1, 1); _whiteSquareTexture.SetData([Color.White]); } @@ -42,8 +44,8 @@ RenderTarget2D GetRenderTarget(int width, int height) if (reCreateRenderTarget) { - _renderTarget?.Dispose(); - _renderTarget = new RenderTarget2D(_wpfGame.GraphicsDevice, width, height, false, SurfaceFormat.Color, DepthFormat.Depth24); + _renderTarget = _graphicsResourceCreator.DisposeTracked(_renderTarget); + _renderTarget = _graphicsResourceCreator.CreateRenderTarget2D(width, height, false, SurfaceFormat.Color, DepthFormat.Depth24); } return _renderTarget!; @@ -59,7 +61,7 @@ public RenderTarget2D UpdateTexture(TwuiContext twuiContext) var device = _wpfGame.GraphicsDevice; device.SetRenderTarget(renderTarget); device.Clear(Color.Transparent); - device.DepthStencilState = new DepthStencilState() { DepthBufferEnable = true }; + device.DepthStencilState = DepthStencilState.Default; //_spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend);//, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); _spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend); diff --git a/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiRenderComponent.cs b/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiRenderComponent.cs index 1da031a68..5a558f3ea 100644 --- a/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiRenderComponent.cs +++ b/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiRenderComponent.cs @@ -1,4 +1,5 @@ using GameWorld.Core.Components; +using GameWorld.Core.Services; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Shared.Core.Events; @@ -12,15 +13,17 @@ public class TwuiRenderComponent : BaseComponent, IDisposable { private readonly IWpfGame _wpfGame; private readonly TwuiPreviewBuilder _twuiPreviewBuilder; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; Texture2D _twuiPreview; SpriteBatch _spriteBatch; Texture2D _whiteSquareTexture; - public TwuiRenderComponent(IWpfGame wpfGame, TwuiPreviewBuilder twuiPreviewBuilder, IEventHub eventHub) + public TwuiRenderComponent(IWpfGame wpfGame, TwuiPreviewBuilder twuiPreviewBuilder, IEventHub eventHub, IGraphicsResourceCreator graphicsResourceCreator) { _wpfGame = wpfGame; _twuiPreviewBuilder = twuiPreviewBuilder; + _graphicsResourceCreator = graphicsResourceCreator; } TwuiContext? _temp_twuiFile; @@ -32,8 +35,8 @@ public void SetFile(TwuiContext twuiContext) public override void Initialize() { - _spriteBatch = new SpriteBatch(_wpfGame.GraphicsDevice); - _whiteSquareTexture = new Texture2D(_wpfGame.GraphicsDevice, 1, 1); + _spriteBatch = _graphicsResourceCreator.CreateSpriteBatch(); + _whiteSquareTexture = _graphicsResourceCreator.CreateTexture2D(1, 1); _whiteSquareTexture.SetData([Color.White]); _twuiPreviewBuilder.Initialize(); @@ -101,8 +104,8 @@ void DrawCheckerboardBackground(int squareSize) public void Dispose() { - _whiteSquareTexture?.Dispose(); - _spriteBatch?.Dispose(); + _whiteSquareTexture = _graphicsResourceCreator.DisposeTracked(_whiteSquareTexture); + _spriteBatch = _graphicsResourceCreator.DisposeTracked(_spriteBatch); } diff --git a/GameWorld/View3D/Components/Gizmo/Gizmo.cs b/GameWorld/View3D/Components/Gizmo/Gizmo.cs index 3e8136dbf..eaa7f837f 100644 --- a/GameWorld/View3D/Components/Gizmo/Gizmo.cs +++ b/GameWorld/View3D/Components/Gizmo/Gizmo.cs @@ -1,5 +1,6 @@ using GameWorld.Core.Components.Input; using GameWorld.Core.Components.Rendering; +using GameWorld.Core.Services; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; @@ -39,6 +40,7 @@ public class Gizmo : IDisposable private readonly GraphicsDevice _graphics; private readonly RenderEngineComponent _renderEngineComponent; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; private readonly BasicEffect _lineEffect; private readonly BasicEffect _meshEffect; @@ -142,19 +144,24 @@ private BoundingSphere ZSphere private readonly IMouseComponent _mouse; - public Gizmo(ArcBallCamera camera, IMouseComponent mouse, GraphicsDevice graphics, RenderEngineComponent renderEngineComponent) + public Gizmo(ArcBallCamera camera, IMouseComponent mouse, GraphicsDevice graphics, RenderEngineComponent renderEngineComponent, IGraphicsResourceCreator graphicsResourceCreator) { SceneWorld = Matrix.Identity; _graphics = graphics; _renderEngineComponent = renderEngineComponent; + _graphicsResourceCreator = graphicsResourceCreator; _camera = camera; _mouse = mouse; Enabled = true; - _lineEffect = new BasicEffect(graphics) { VertexColorEnabled = true, AmbientLightColor = Vector3.One, EmissiveColor = Vector3.One }; - _meshEffect = new BasicEffect(graphics); + _lineEffect = _graphicsResourceCreator.CreateBasicEffect(); + _lineEffect.VertexColorEnabled = true; + _lineEffect.AmbientLightColor = Vector3.One; + _lineEffect.EmissiveColor = Vector3.One; + + _meshEffect = _graphicsResourceCreator.CreateBasicEffect(); Initialize(); } @@ -730,8 +737,8 @@ public void ToggleActiveSpace() public void Dispose() { - _lineEffect.Dispose(); - _meshEffect.Dispose(); + _graphicsResourceCreator.DisposeTracked(_lineEffect); + _graphicsResourceCreator.DisposeTracked(_meshEffect); } diff --git a/GameWorld/View3D/Components/Gizmo/GizmoComponent.cs b/GameWorld/View3D/Components/Gizmo/GizmoComponent.cs index 4b5aa9747..016bea31a 100644 --- a/GameWorld/View3D/Components/Gizmo/GizmoComponent.cs +++ b/GameWorld/View3D/Components/Gizmo/GizmoComponent.cs @@ -22,6 +22,7 @@ public class GizmoComponent : BaseComponent, IDisposable private readonly RenderEngineComponent _resourceLibary; private readonly IDeviceResolver _deviceResolverComponent; private readonly CommandFactory _commandFactory; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; Gizmo _gizmo; bool _isEnabled = false; TransformGizmoWrapper _activeTransformation; @@ -31,7 +32,7 @@ public class GizmoComponent : BaseComponent, IDisposable public GizmoComponent(IEventHub eventHub, IKeyboardComponent keyboardComponent, IMouseComponent mouseComponent, ArcBallCamera camera, CommandExecutor commandExecutor, RenderEngineComponent resourceLibary, IDeviceResolver deviceResolverComponent, CommandFactory commandFactory, - SelectionManager selectionManager) + SelectionManager selectionManager, IGraphicsResourceCreator graphicsResourceCreator) { UpdateOrder = (int)ComponentUpdateOrderEnum.Gizmo; DrawOrder = (int)ComponentDrawOrderEnum.Gizmo; @@ -44,13 +45,14 @@ public GizmoComponent(IEventHub eventHub, _deviceResolverComponent = deviceResolverComponent; _commandFactory = commandFactory; _selectionManager = selectionManager; + _graphicsResourceCreator = graphicsResourceCreator; _eventHub.Register(this, Handle); } public override void Initialize() { - _gizmo = new Gizmo(_camera, _mouse, _deviceResolverComponent.Device, _resourceLibary); + _gizmo = new Gizmo(_camera, _mouse, _deviceResolverComponent.Device, _resourceLibary, _graphicsResourceCreator); _gizmo.ActivePivot = PivotType.ObjectCenter; _gizmo.TranslateEvent += GizmoTranslateEvent; _gizmo.RotateEvent += GizmoRotateEvent; diff --git a/GameWorld/View3D/Components/GraphicsResourceStatsComponent.cs b/GameWorld/View3D/Components/GraphicsResourceStatsComponent.cs new file mode 100644 index 000000000..3018ed6b3 --- /dev/null +++ b/GameWorld/View3D/Components/GraphicsResourceStatsComponent.cs @@ -0,0 +1,49 @@ +using System.Text; +using GameWorld.Core.Components.Rendering; +using GameWorld.Core.Rendering.RenderItems; +using GameWorld.Core.Services; +using GameWorld.Core.Utility; +using Microsoft.Xna.Framework; + +namespace GameWorld.Core.Components +{ + public class GraphicsResourceStatsComponent : BaseComponent + { + private readonly IDeviceResolver _deviceResolver; + private readonly RenderEngineComponent _renderEngineComponent; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; + + public GraphicsResourceStatsComponent(IDeviceResolver deviceResolver, RenderEngineComponent renderEngineComponent, IGraphicsResourceCreator graphicsResourceCreator) + { + _deviceResolver = deviceResolver; + _renderEngineComponent = renderEngineComponent; + _graphicsResourceCreator = graphicsResourceCreator; + } + + + public override void Draw(GameTime gameTime) + { + var records = _graphicsResourceCreator.Records; + var groupedByType = records + .GroupBy(x => x.ResourceType) + .OrderBy(x => x.Key) + .ToList(); + + var builder = new StringBuilder(); + builder.AppendLine($"GR Total: {records.Count}"); + + foreach (var group in groupedByType) + builder.AppendLine($"{group.Key}: {group.Count()}"); + + var displayText = builder.ToString().TrimEnd(); + var textSize = _renderEngineComponent.DefaultFont.MeasureString(displayText); + var viewport = _deviceResolver.Device.Viewport; + var position = new Vector2( + viewport.Width - textSize.X - 5, + viewport.Height - textSize.Y - 5); + + var renderItem = new FontRenderItem(_renderEngineComponent, displayText, position, Color.LightGreen); + _renderEngineComponent.AddRenderItem(RenderBuckedId.Font, renderItem); + } + } +} diff --git a/GameWorld/View3D/Components/Navigation/ViewportGizmo.cs b/GameWorld/View3D/Components/Navigation/ViewportGizmo.cs index 8526ece96..30d40b227 100644 --- a/GameWorld/View3D/Components/Navigation/ViewportGizmo.cs +++ b/GameWorld/View3D/Components/Navigation/ViewportGizmo.cs @@ -1,6 +1,7 @@ using GameWorld.Core.Components.Input; using GameWorld.Core.Components.Rendering; using GameWorld.Core.Rendering.RenderItems; +using GameWorld.Core.Services; using GameWorld.Core.Utility; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -15,6 +16,7 @@ public class ViewportGizmo : BaseComponent, IDisposable private readonly IMouseComponent _mouse; private readonly RenderEngineComponent _renderEngine; private readonly IEventHub _eventHub; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; // Gizmo size and position private const float GIZMO_SIZE = 70f; // Display size (pixels) @@ -45,18 +47,20 @@ public ViewportGizmo(IDeviceResolver deviceResolver, ArcBallCamera camera, IMouseComponent mouse, RenderEngineComponent renderEngine, - IEventHub eventHub) + IEventHub eventHub, + IGraphicsResourceCreator graphicsResourceCreator) { _deviceResolver = deviceResolver; _camera = camera; _mouse = mouse; _renderEngine = renderEngine; _eventHub = eventHub; + _graphicsResourceCreator = graphicsResourceCreator; } public override void Initialize() { - _whiteTexture = new Texture2D(_deviceResolver.Device, 1, 1); + _whiteTexture = _graphicsResourceCreator.CreateTexture2D(1, 1); _whiteTexture.SetData([Color.White]); base.Initialize(); @@ -377,7 +381,7 @@ private void DrawCircleOutline(Vector2 center, float radius, Color color, float public void Dispose() { - _whiteTexture?.Dispose(); + _whiteTexture = _graphicsResourceCreator.DisposeTracked(_whiteTexture); } } } diff --git a/GameWorld/View3D/Components/Rendering/RasterStateHelper.cs b/GameWorld/View3D/Components/Rendering/RasterStateHelper.cs index 81484c892..df87f12db 100644 --- a/GameWorld/View3D/Components/Rendering/RasterStateHelper.cs +++ b/GameWorld/View3D/Components/Rendering/RasterStateHelper.cs @@ -1,14 +1,15 @@ using System.Collections.Generic; +using GameWorld.Core.Services; using Microsoft.Xna.Framework.Graphics; namespace GameWorld.Core.Components.Rendering { internal static class RasterStateHelper { - static public void Rebuild(Dictionary renderState, bool cullingEnabled, bool bigSceneDepthBias ) + static public void Rebuild(Dictionary renderState, bool cullingEnabled, bool bigSceneDepthBias, IGraphicsResourceCreator graphicsResourceCreator) { foreach (var item in renderState.Values) - item.Dispose(); + graphicsResourceCreator.DisposeTracked(item); renderState.Clear(); @@ -16,32 +17,32 @@ static public void Rebuild(Dictionary rend var bias = bigSceneDepthBias ? 0 : 0; var depthOffsetBias = 0.00005f; - renderState[RasterizerStateEnum.Normal] = new RasterizerState + renderState[RasterizerStateEnum.Normal] = graphicsResourceCreator.CreateRasterizerState(new RasterizerState { FillMode = FillMode.Solid, CullMode = cullMode, DepthBias = bias, DepthClipEnable = true, MultiSampleAntiAlias = true - }; + }); - renderState[RasterizerStateEnum.Wireframe] = new RasterizerState + renderState[RasterizerStateEnum.Wireframe] = graphicsResourceCreator.CreateRasterizerState(new RasterizerState { FillMode = FillMode.WireFrame, CullMode = cullMode, DepthBias = bias - depthOffsetBias, DepthClipEnable = true, MultiSampleAntiAlias = true - }; + }); - renderState[RasterizerStateEnum.SelectedFaces] = new RasterizerState + renderState[RasterizerStateEnum.SelectedFaces] = graphicsResourceCreator.CreateRasterizerState(new RasterizerState { FillMode = FillMode.Solid, CullMode = CullMode.None, DepthBias = bias - depthOffsetBias, DepthClipEnable = true, MultiSampleAntiAlias = true - }; + }); } } } diff --git a/GameWorld/View3D/Components/Rendering/RenderEngineComponent.cs b/GameWorld/View3D/Components/Rendering/RenderEngineComponent.cs index 32fdb3646..08027f50d 100644 --- a/GameWorld/View3D/Components/Rendering/RenderEngineComponent.cs +++ b/GameWorld/View3D/Components/Rendering/RenderEngineComponent.cs @@ -31,6 +31,7 @@ public class RenderEngineComponent : BaseComponent, IDisposable private readonly ApplicationSettingsService _applicationSettingsService; private readonly SceneRenderParametersStore _sceneLightParameters; private readonly IEventHub _eventHub; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; bool _cullingEnabled = false; bool _bigSceneDepthBiasMode = false; @@ -47,7 +48,7 @@ public class RenderEngineComponent : BaseComponent, IDisposable public SpriteBatch CommonSpriteBatch { get; private set; } public SpriteFont DefaultFont { get; private set; } - public RenderEngineComponent(IWpfGame wpfGame, ResourceLibrary resourceLibrary, ArcBallCamera camera, IDeviceResolver deviceResolverComponent, ApplicationSettingsService applicationSettingsService, SceneRenderParametersStore sceneLightParametersStore, IEventHub eventHub) + public RenderEngineComponent(IWpfGame wpfGame, ResourceLibrary resourceLibrary, ArcBallCamera camera, IDeviceResolver deviceResolverComponent, ApplicationSettingsService applicationSettingsService, SceneRenderParametersStore sceneLightParametersStore, IEventHub eventHub, IGraphicsResourceCreator graphicsResourceCreator) { UpdateOrder = (int)ComponentUpdateOrderEnum.RenderEngine; DrawOrder = (int)ComponentDrawOrderEnum.RenderEngine; @@ -60,6 +61,7 @@ public RenderEngineComponent(IWpfGame wpfGame, ResourceLibrary resourceLibrary, _applicationSettingsService = applicationSettingsService; _sceneLightParameters = sceneLightParametersStore; _eventHub = eventHub; + _graphicsResourceCreator = graphicsResourceCreator; foreach (RenderBuckedId value in Enum.GetValues(typeof(RenderBuckedId))) _renderItems.Add(value, new List(100)); @@ -90,13 +92,13 @@ public override void Initialize() var device = _deviceResolverComponent.Device; _bloomFilter = new BloomFilter(); - _bloomFilter.Load(device, _resourceLibrary, device.Viewport.Width, device.Viewport.Height); + _bloomFilter.Load(device, _resourceLibrary, _graphicsResourceCreator, device.Viewport.Width, device.Viewport.Height); _bloomFilter.BloomPreset = BloomFilter.BloomPresets.SuperWide; - _whiteTexture = new Texture2D(_deviceResolverComponent.Device, 1, 1); + _whiteTexture = _graphicsResourceCreator.CreateTexture2D(1, 1); _whiteTexture.SetData(new[] { Color.White }); - CommonSpriteBatch = new SpriteBatch(device); + CommonSpriteBatch = _graphicsResourceCreator.CreateSpriteBatch(); DefaultFont = _wpfGame.Content.Load("Fonts//DefaultFont"); } @@ -107,7 +109,7 @@ void RebuildRasterStates(bool cullingEnabled, bool bigSceneDepthBias) // Set renderState to something we dont use, so we can rebuild the ones we care about _deviceResolverComponent.Device.RasterizerState = RasterizerState.CullNone; - RasterStateHelper.Rebuild(_rasterStates, _cullingEnabled, _bigSceneDepthBiasMode); + RasterStateHelper.Rebuild(_rasterStates, _cullingEnabled, _bigSceneDepthBiasMode, _graphicsResourceCreator); } public bool BackfaceCulling { get => _cullingEnabled; set => RebuildRasterStates(value, _bigSceneDepthBiasMode); } @@ -151,9 +153,9 @@ public override void Draw(GameTime gameTime) var commonShaderParameters = CommonShaderParameterBuilder.Build(_camera, _sceneLightParameters); var backgroundColour = ApplicationSettingsHelper.GetEnumAsColour(_applicationSettingsService.CurrentSettings.RenderEngineBackgroundColour); - _normalRenderTarget = RenderTargetHelper.GetRenderTarget(device, _normalRenderTarget, imageUpScale); - _emissiveRenderTarget = RenderTargetHelper.GetRenderTarget(device, _emissiveRenderTarget, imageUpScale); - _screenRenderTarget = RenderTargetHelper.GetRenderTarget(device, _screenRenderTarget, imageUpScale); + _normalRenderTarget = RenderTargetHelper.GetRenderTarget(device, _normalRenderTarget, imageUpScale, _graphicsResourceCreator); + _emissiveRenderTarget = RenderTargetHelper.GetRenderTarget(device, _emissiveRenderTarget, imageUpScale, _graphicsResourceCreator); + _screenRenderTarget = RenderTargetHelper.GetRenderTarget(device, _screenRenderTarget, imageUpScale, _graphicsResourceCreator); // Configure render targets var backBufferRenderTarget = device.GetRenderTargets()[0].RenderTarget as RenderTarget2D; @@ -278,20 +280,19 @@ public void Dispose() { _eventHub.UnRegister(this); - CommonSpriteBatch?.Dispose(); - CommonSpriteBatch = null; + CommonSpriteBatch = _graphicsResourceCreator.DisposeTracked(CommonSpriteBatch); _bloomFilter.Dispose(); - _normalRenderTarget.Dispose(); - _emissiveRenderTarget.Dispose(); - _screenRenderTarget.Dispose(); - _whiteTexture.Dispose(); + _normalRenderTarget = _graphicsResourceCreator.DisposeTracked(_normalRenderTarget); + _emissiveRenderTarget = _graphicsResourceCreator.DisposeTracked(_emissiveRenderTarget); + _screenRenderTarget = _graphicsResourceCreator.DisposeTracked(_screenRenderTarget); + _whiteTexture = _graphicsResourceCreator.DisposeTracked(_whiteTexture); _renderLines.Clear(); _renderItems.Clear(); foreach (var item in _rasterStates.Values) - item.Dispose(); + _graphicsResourceCreator.DisposeTracked(item); _rasterStates.Clear(); } } diff --git a/GameWorld/View3D/Components/Rendering/RenderTargetHelper.cs b/GameWorld/View3D/Components/Rendering/RenderTargetHelper.cs index 0e2f681d3..1cd596485 100644 --- a/GameWorld/View3D/Components/Rendering/RenderTargetHelper.cs +++ b/GameWorld/View3D/Components/Rendering/RenderTargetHelper.cs @@ -1,24 +1,25 @@ -using Microsoft.Xna.Framework.Graphics; +using GameWorld.Core.Services; +using Microsoft.Xna.Framework.Graphics; namespace GameWorld.Core.Components.Rendering { internal class RenderTargetHelper { - public static RenderTarget2D GetRenderTarget(GraphicsDevice device, RenderTarget2D existingRenderTarget, float imageUpScale) + public static RenderTarget2D GetRenderTarget(GraphicsDevice device, RenderTarget2D existingRenderTarget, float imageUpScale, IGraphicsResourceCreator graphicsResourceCreator) { var width = (int)(device.Viewport.Width * imageUpScale); var height = (int)(device.Viewport.Height * imageUpScale); if (existingRenderTarget == null) { - return new RenderTarget2D(device, width, height, false, SurfaceFormat.Color, DepthFormat.Depth24, 8, RenderTargetUsage.PreserveContents); + return graphicsResourceCreator.CreateRenderTarget2D(width, height, false, SurfaceFormat.Color, DepthFormat.Depth24, 8, RenderTargetUsage.PreserveContents); } if (existingRenderTarget.Width == width && existingRenderTarget.Height == height) return existingRenderTarget; - existingRenderTarget.Dispose(); - return new RenderTarget2D(device, width, height, false, SurfaceFormat.Color, DepthFormat.Depth24, 8, RenderTargetUsage.PreserveContents); + graphicsResourceCreator.DisposeTracked(existingRenderTarget); + return graphicsResourceCreator.CreateRenderTarget2D(width, height, false, SurfaceFormat.Color, DepthFormat.Depth24, 8, RenderTargetUsage.PreserveContents); } } } diff --git a/GameWorld/View3D/Components/Selection/SelectionComponent.cs b/GameWorld/View3D/Components/Selection/SelectionComponent.cs index 70afe0719..b5f41c68c 100644 --- a/GameWorld/View3D/Components/Selection/SelectionComponent.cs +++ b/GameWorld/View3D/Components/Selection/SelectionComponent.cs @@ -7,6 +7,7 @@ using GameWorld.Core.Components.Input; using GameWorld.Core.Components.Rendering; using GameWorld.Core.SceneNodes; +using GameWorld.Core.Services; using GameWorld.Core.Utility; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -31,12 +32,13 @@ public class SelectionComponent : BaseComponent, IDisposable private readonly CommandFactory _commandFactory; private readonly SceneManager _sceneManger; private readonly RenderEngineComponent _resourceLibrary; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; public SelectionComponent( IMouseComponent mouseComponent, IKeyboardComponent keyboardComponent, ArcBallCamera camera, SelectionManager selectionManager, IDeviceResolver deviceResolverComponent, CommandFactory commandFactory, - SceneManager sceneManager, RenderEngineComponent resourceLibrary) + SceneManager sceneManager, RenderEngineComponent resourceLibrary, IGraphicsResourceCreator graphicsResourceCreator) { _mouseComponent = mouseComponent; _keyboardComponent = keyboardComponent; @@ -46,6 +48,7 @@ public SelectionComponent( _commandFactory = commandFactory; _sceneManger = sceneManager; _resourceLibrary = resourceLibrary; + _graphicsResourceCreator = graphicsResourceCreator; } public override void Initialize() @@ -54,7 +57,7 @@ public override void Initialize() DrawOrder = (int)ComponentDrawOrderEnum.SelectionComponent; //_spriteBatch = new SpriteBatch(_deviceResolverComponent.Device); - _textTexture = new Texture2D(_deviceResolverComponent.Device, 1, 1); + _textTexture = _graphicsResourceCreator.CreateTexture2D(1, 1); _textTexture.SetData(new Color[1 * 1] { Color.White }); base.Initialize(); @@ -340,7 +343,7 @@ bool IsSelectionRectangle(Rectangle rect) public void Dispose() { - _textTexture.Dispose(); + _textTexture = _graphicsResourceCreator.DisposeTracked(_textTexture); } } } diff --git a/GameWorld/View3D/Components/Selection/SelectionManager.cs b/GameWorld/View3D/Components/Selection/SelectionManager.cs index 52da05c59..a74394de8 100644 --- a/GameWorld/View3D/Components/Selection/SelectionManager.cs +++ b/GameWorld/View3D/Components/Selection/SelectionManager.cs @@ -28,25 +28,27 @@ public class SelectionManager : BaseComponent, IDisposable float _vertexSelectionFalloff = 0; private readonly IScopedResourceLibrary _resourceLib; private readonly IDeviceResolver _deviceResolverComponent; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; - public SelectionManager(IEventHub eventHub, RenderEngineComponent renderEngine, IScopedResourceLibrary resourceLib, IDeviceResolver deviceResolverComponent) + public SelectionManager(IEventHub eventHub, RenderEngineComponent renderEngine, IScopedResourceLibrary resourceLib, IDeviceResolver deviceResolverComponent, IGraphicsResourceCreator graphicsResourceCreator) { _eventHub = eventHub; _renderEngine = renderEngine; _resourceLib = resourceLib; _deviceResolverComponent = deviceResolverComponent; + _graphicsResourceCreator = graphicsResourceCreator; } public override void Initialize() { CreateSelectionSate(GeometrySelectionMode.Object, null, false); - _vertexRenderer = new VertexInstanceMesh(_deviceResolverComponent, _resourceLib); + _vertexRenderer = new VertexInstanceMesh(_deviceResolverComponent, _resourceLib, _graphicsResourceCreator); - _wireframeEffect = new BasicShader(_deviceResolverComponent.Device); + _wireframeEffect = new BasicShader(_deviceResolverComponent.Device, _graphicsResourceCreator); _wireframeEffect.DiffuseColour = Vector3.Zero; - _selectedFacesEffect = new BasicShader(_deviceResolverComponent.Device); + _selectedFacesEffect = new BasicShader(_deviceResolverComponent.Device, _graphicsResourceCreator); _selectedFacesEffect.DiffuseColour = new Vector3(1, 0, 0); _selectedFacesEffect.SpecularColour = new Vector3(1, 0, 0); _selectedFacesEffect.EnableDefaultLighting(); diff --git a/GameWorld/View3D/DependencyInjectionContainer.cs b/GameWorld/View3D/DependencyInjectionContainer.cs index c6ecf19a8..8bde8cdc2 100644 --- a/GameWorld/View3D/DependencyInjectionContainer.cs +++ b/GameWorld/View3D/DependencyInjectionContainer.cs @@ -26,6 +26,7 @@ using GameWorld.Core.WpfWindow; using Microsoft.Extensions.DependencyInjection; using Shared.Core.DependencyInjection; +using Shared.Core.ErrorHandling.Exceptions; using Shared.Core.Services; namespace GameWorld.Core @@ -38,7 +39,11 @@ public override void Register(IServiceCollection serviceCollection) serviceCollection.AddScoped(); serviceCollection.AddScoped(); serviceCollection.AddScoped(); + serviceCollection.AddScoped(x => new GraphicsResourceCreator(() => x.GetRequiredService().GraphicsDevice)); + serviceCollection.AddScoped(x => x.GetRequiredService() as IScopeOwnerAware); + + serviceCollection.AddSingleton(); // Settings @@ -53,7 +58,8 @@ public override void Register(IServiceCollection serviceCollection) serviceCollection.AddScoped(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); - + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); serviceCollection.AddScoped(); serviceCollection.AddScoped(); @@ -101,6 +107,7 @@ void RegisterComponents(IServiceCollection serviceCollection) RegisterGameComponent(serviceCollection); RegisterGameComponent(serviceCollection); + RegisterGameComponent(serviceCollection); RegisterGameComponent(serviceCollection); RegisterGameComponent(serviceCollection); RegisterGameComponent(serviceCollection); diff --git a/GameWorld/View3D/Rendering/BloomFilter.cs b/GameWorld/View3D/Rendering/BloomFilter.cs index 3f219ac8a..ada933e8b 100644 --- a/GameWorld/View3D/Rendering/BloomFilter.cs +++ b/GameWorld/View3D/Rendering/BloomFilter.cs @@ -64,6 +64,7 @@ public class BloomFilter : IDisposable //Objects private GraphicsDevice _graphicsDevice; private QuadRenderer _quadRenderer; + private IGraphicsResourceCreator _graphicsResourceCreator; //Shader + variables private Effect _bloomEffect; @@ -226,9 +227,10 @@ public float BloomThreshold /// initial value for creating the rendertargets /// The intended format for the rendertargets. For normal, non-hdr, applications color or rgba1010102 are fine NOTE: For OpenGL, SurfaceFormat.Color is recommended for non-HDR applications. /// if you already have quadRenderer you may reuse it here - public void Load(GraphicsDevice graphicsDevice, ResourceLibrary content, int width, int height, SurfaceFormat renderTargetFormat = SurfaceFormat.Color, QuadRenderer quadRenderer = null) + public void Load(GraphicsDevice graphicsDevice, ResourceLibrary content, IGraphicsResourceCreator graphicsResourceCreator, int width, int height, SurfaceFormat renderTargetFormat = SurfaceFormat.Color, QuadRenderer quadRenderer = null) { _graphicsDevice = graphicsDevice; + _graphicsResourceCreator = graphicsResourceCreator; UpdateResolution(width, height); //if quadRenderer == null -> new, otherwise not @@ -592,22 +594,22 @@ public void UpdateResolution(int width, int height) Dispose(); } - _bloomRenderTarget2DMip0 = new RenderTarget2D(_graphicsDevice, + _bloomRenderTarget2DMip0 = _graphicsResourceCreator.CreateRenderTarget2D( Math.Max(1,width), Math.Max(1, height), false, _renderTargetFormat, DepthFormat.None, 0, RenderTargetUsage.DiscardContents); - _bloomRenderTarget2DMip1 = new RenderTarget2D(_graphicsDevice, + _bloomRenderTarget2DMip1 = _graphicsResourceCreator.CreateRenderTarget2D( Math.Max(1,width / 2), Math.Max(1, height / 2), false, _renderTargetFormat, DepthFormat.None, 0, RenderTargetUsage.PreserveContents); - _bloomRenderTarget2DMip2 = new RenderTarget2D(_graphicsDevice, + _bloomRenderTarget2DMip2 = _graphicsResourceCreator.CreateRenderTarget2D( Math.Max(1,width / 4), Math.Max(1, height / 4), false, _renderTargetFormat, DepthFormat.None, 0, RenderTargetUsage.PreserveContents); - _bloomRenderTarget2DMip3 = new RenderTarget2D(_graphicsDevice, + _bloomRenderTarget2DMip3 = _graphicsResourceCreator.CreateRenderTarget2D( Math.Max(1,width / 8), Math.Max(1, height / 8), false, _renderTargetFormat, DepthFormat.None, 0, RenderTargetUsage.PreserveContents); - _bloomRenderTarget2DMip4 = new RenderTarget2D(_graphicsDevice, + _bloomRenderTarget2DMip4 = _graphicsResourceCreator.CreateRenderTarget2D( Math.Max(1,width / 16), Math.Max(1, height / 16), false, _renderTargetFormat, DepthFormat.None, 0, RenderTargetUsage.PreserveContents); - _bloomRenderTarget2DMip5 = new RenderTarget2D(_graphicsDevice, + _bloomRenderTarget2DMip5 = _graphicsResourceCreator.CreateRenderTarget2D( Math.Max(1, width / 32), Math.Max(1, height / 32), false, _renderTargetFormat, DepthFormat.None, 0, RenderTargetUsage.PreserveContents); } @@ -617,12 +619,12 @@ public void UpdateResolution(int width, int height) /// public void Dispose() { - _bloomRenderTarget2DMip0.Dispose(); - _bloomRenderTarget2DMip1.Dispose(); - _bloomRenderTarget2DMip2.Dispose(); - _bloomRenderTarget2DMip3.Dispose(); - _bloomRenderTarget2DMip4.Dispose(); - _bloomRenderTarget2DMip5.Dispose(); + _bloomRenderTarget2DMip0 = _graphicsResourceCreator?.DisposeTracked(_bloomRenderTarget2DMip0); + _bloomRenderTarget2DMip1 = _graphicsResourceCreator?.DisposeTracked(_bloomRenderTarget2DMip1); + _bloomRenderTarget2DMip2 = _graphicsResourceCreator?.DisposeTracked(_bloomRenderTarget2DMip2); + _bloomRenderTarget2DMip3 = _graphicsResourceCreator?.DisposeTracked(_bloomRenderTarget2DMip3); + _bloomRenderTarget2DMip4 = _graphicsResourceCreator?.DisposeTracked(_bloomRenderTarget2DMip4); + _bloomRenderTarget2DMip5 = _graphicsResourceCreator?.DisposeTracked(_bloomRenderTarget2DMip5); } } } diff --git a/GameWorld/View3D/Rendering/Geometry/IGraphicsCardGeometry.cs b/GameWorld/View3D/Rendering/Geometry/IGraphicsCardGeometry.cs index e0c35ee5e..94d26c9d9 100644 --- a/GameWorld/View3D/Rendering/Geometry/IGraphicsCardGeometry.cs +++ b/GameWorld/View3D/Rendering/Geometry/IGraphicsCardGeometry.cs @@ -1,4 +1,5 @@ using GameWorld.Core.Utility; +using GameWorld.Core.Services; using Microsoft.Xna.Framework.Graphics; namespace GameWorld.Core.Rendering.Geometry @@ -18,55 +19,47 @@ public interface IGraphicsCardGeometry public class GraphicsCardGeometry : IGraphicsCardGeometry { private readonly GraphicsDevice Device; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; public VertexBuffer VertexBuffer { get; private set; } public IndexBuffer IndexBuffer { get; private set; } - public GraphicsCardGeometry(GraphicsDevice device) + public GraphicsCardGeometry(GraphicsDevice device, IGraphicsResourceCreator graphicsResourceCreator) { Device = device; + _graphicsResourceCreator = graphicsResourceCreator; } public void RebuildIndexBuffer(ushort[] indexList) { - if (IndexBuffer != null) - { - IndexBuffer.Dispose(); - IndexBuffer = null; - } + IndexBuffer = _graphicsResourceCreator.DisposeTracked(IndexBuffer); if (indexList.Length != 0) { - IndexBuffer = new IndexBuffer(Device, typeof(ushort), indexList.Length, BufferUsage.None); + IndexBuffer = _graphicsResourceCreator.CreateIndexBuffer(typeof(ushort), indexList.Length, BufferUsage.None); IndexBuffer.SetData(indexList); } } public virtual void RebuildVertexBuffer(VertexPositionNormalTextureCustom[] vertArray, VertexDeclaration vertexDeclaration) { - if (VertexBuffer != null) - { - VertexBuffer.Dispose(); - VertexBuffer = null; - } + VertexBuffer = _graphicsResourceCreator.DisposeTracked(VertexBuffer); if (vertArray.Length != 0) { - VertexBuffer = new VertexBuffer(Device, vertexDeclaration, vertArray.Length, BufferUsage.None); + VertexBuffer = _graphicsResourceCreator.CreateVertexBuffer(vertexDeclaration, vertArray.Length, BufferUsage.None); VertexBuffer.SetData(vertArray); } } public IGraphicsCardGeometry Clone() { - return new GraphicsCardGeometry(Device); + return new GraphicsCardGeometry(Device, _graphicsResourceCreator); } public void Dispose() { - if (IndexBuffer != null) - IndexBuffer.Dispose(); - if (VertexBuffer != null) - VertexBuffer.Dispose(); + IndexBuffer = _graphicsResourceCreator.DisposeTracked(IndexBuffer); + VertexBuffer = _graphicsResourceCreator.DisposeTracked(VertexBuffer); } } @@ -77,15 +70,17 @@ public interface IGeometryGraphicsContextFactory public class GeometryGraphicsContextFactory : IGeometryGraphicsContextFactory { private readonly IDeviceResolver _deviceResolverComponent; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; - public GeometryGraphicsContextFactory(IDeviceResolver deviceResolverComponent) + public GeometryGraphicsContextFactory(IDeviceResolver deviceResolverComponent, IGraphicsResourceCreator graphicsResourceCreator) { _deviceResolverComponent = deviceResolverComponent; + _graphicsResourceCreator = graphicsResourceCreator; } public IGraphicsCardGeometry Create() { - return new GraphicsCardGeometry(_deviceResolverComponent.Device); + return new GraphicsCardGeometry(_deviceResolverComponent.Device, _graphicsResourceCreator); } } } diff --git a/GameWorld/View3D/Rendering/Materials/Shaders/BasicShader.cs b/GameWorld/View3D/Rendering/Materials/Shaders/BasicShader.cs index c0bfc6e1a..1f58f2612 100644 --- a/GameWorld/View3D/Rendering/Materials/Shaders/BasicShader.cs +++ b/GameWorld/View3D/Rendering/Materials/Shaders/BasicShader.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using GameWorld.Core.Components.Rendering; +using GameWorld.Core.Services; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -9,6 +10,7 @@ namespace GameWorld.Core.Rendering.Materials.Shaders public class BasicShader : IShader, IDisposable { private readonly BasicEffect _effect; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; bool _enableDefaultLighting = false; @@ -16,20 +18,22 @@ public class BasicShader : IShader, IDisposable public Vector3 SpecularColour { get; set; } public void EnableDefaultLighting() { _enableDefaultLighting = true; } - public BasicShader(GraphicsDevice device) + public BasicShader(GraphicsDevice device, IGraphicsResourceCreator graphicsResourceCreator) { - _effect = new BasicEffect(device); + _graphicsResourceCreator = graphicsResourceCreator; + _effect = graphicsResourceCreator.CreateBasicEffect(); } - protected BasicShader(BasicEffect effect) + protected BasicShader(BasicEffect effect, IGraphicsResourceCreator graphicsResourceCreator) { - _effect = effect; + _graphicsResourceCreator = graphicsResourceCreator; + _effect = graphicsResourceCreator.Track(effect); } public BasicShader Clone() { var clonedEffect = _effect.Clone() as BasicEffect; - return new BasicShader(clonedEffect!) + return new BasicShader(clonedEffect!, _graphicsResourceCreator) { DiffuseColour = DiffuseColour, SpecularColour = SpecularColour, @@ -54,7 +58,7 @@ public void Apply(CommonShaderParameters commonShaderParameters, Matrix modelMat public void Dispose() { - _effect.Dispose(); + _graphicsResourceCreator.DisposeTracked(_effect); } public void SetTechnique(RenderingTechnique technique) diff --git a/GameWorld/View3D/Rendering/TextureToTextureRenderer.cs b/GameWorld/View3D/Rendering/TextureToTextureRenderer.cs index cb2e5c1ef..b590e9eec 100644 --- a/GameWorld/View3D/Rendering/TextureToTextureRenderer.cs +++ b/GameWorld/View3D/Rendering/TextureToTextureRenderer.cs @@ -12,6 +12,7 @@ public class TextureToTextureRenderer : IDisposable public readonly GraphicsDevice _device; public readonly SpriteBatch _spriteBatch; public readonly IScopedResourceLibrary _resourceLibary; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; public class DrawSettings { @@ -21,16 +22,17 @@ public class DrawSettings public bool OnlyAlpha { get; set; } = false; } - public TextureToTextureRenderer(GraphicsDevice device, SpriteBatch spriteBatch, IScopedResourceLibrary resourceLibary) + public TextureToTextureRenderer(GraphicsDevice device, SpriteBatch spriteBatch, IScopedResourceLibrary resourceLibary, IGraphicsResourceCreator graphicsResourceCreator) { _device = device; _spriteBatch = spriteBatch; _resourceLibary = resourceLibary; + _graphicsResourceCreator = graphicsResourceCreator; } public Texture2D RenderToTexture(Texture2D texture, int width, int height, DrawSettings settings, string outputPath = null) { - var renderTarget = new RenderTarget2D(_device, width, height, false, SurfaceFormat.Color, DepthFormat.Depth24); + var renderTarget = _graphicsResourceCreator.CreateRenderTarget2D(width, height, false, SurfaceFormat.Color, DepthFormat.Depth24); DrawTextureToTarget(renderTarget, texture, settings); if (outputPath != null) SaveTexture(renderTarget, outputPath); @@ -41,7 +43,7 @@ void DrawTextureToTarget(RenderTarget2D renderTarget, Texture2D texture, DrawSet { _device.SetRenderTarget(renderTarget); _device.Clear(Color.Transparent); - _device.DepthStencilState = new DepthStencilState() { DepthBufferEnable = true }; + _device.DepthStencilState = DepthStencilState.Default; var previewShader = _resourceLibary.GetStaticEffect(ShaderTypes.TexturePreview); @@ -68,7 +70,7 @@ public void SaveTexture(Texture2D texture, string path) public void Dispose() { - _spriteBatch.Dispose(); + _graphicsResourceCreator.DisposeTracked(_spriteBatch); } } } diff --git a/GameWorld/View3D/Rendering/VertexInstanceMesh.cs b/GameWorld/View3D/Rendering/VertexInstanceMesh.cs index 9546706b6..734ee4875 100644 --- a/GameWorld/View3D/Rendering/VertexInstanceMesh.cs +++ b/GameWorld/View3D/Rendering/VertexInstanceMesh.cs @@ -49,6 +49,7 @@ struct VertexMeshInstanceInfo public class VertexInstanceMesh : IDisposable { + private readonly IGraphicsResourceCreator _graphicsResourceCreator; Effect _effect; VertexDeclaration _instanceVertexDeclaration; @@ -65,8 +66,9 @@ public class VertexInstanceMesh : IDisposable Vector3 _selectedColour = new(1, 0, 0); Vector3 _deselectedColour = new (1, 1, 1); - public VertexInstanceMesh(IDeviceResolver deviceResolverComponent, IScopedResourceLibrary resourceLibrary) + public VertexInstanceMesh(IDeviceResolver deviceResolverComponent, IScopedResourceLibrary resourceLibrary, IGraphicsResourceCreator graphicsResourceCreator) { + _graphicsResourceCreator = graphicsResourceCreator; Initialize(deviceResolverComponent.Device, resourceLibrary); } @@ -76,7 +78,7 @@ void Initialize(GraphicsDevice device, IScopedResourceLibrary resourceLib) _instanceVertexDeclaration = InstanceDataOrientation.VertexDeclaration; GenerateGeometry(device); - _instanceBuffer = new DynamicVertexBuffer(device, _instanceVertexDeclaration, _maxInstanceCount, BufferUsage.WriteOnly); + _instanceBuffer = _graphicsResourceCreator.CreateDynamicVertexBuffer(_instanceVertexDeclaration, _maxInstanceCount, BufferUsage.WriteOnly); _instanceTransform = new VertexMeshInstanceInfo[_maxInstanceCount]; GenerateInstanceInformation(_maxInstanceCount); @@ -118,7 +120,7 @@ void GenerateGeometry(GraphicsDevice device) vertices[22].Position = new Vector3(1, -1, -1); vertices[23].Position = new Vector3(-1, -1, -1); - _geometryBuffer = new VertexBuffer(device, VertexPosition.VertexDeclaration, 24, BufferUsage.WriteOnly); + _geometryBuffer = _graphicsResourceCreator.CreateVertexBuffer(VertexPosition.VertexDeclaration, 24, BufferUsage.WriteOnly); _geometryBuffer.SetData(vertices); var indices = new int[36]; @@ -140,7 +142,7 @@ void GenerateGeometry(GraphicsDevice device) indices[30] = 20; indices[31] = 21; indices[32] = 22; indices[33] = 21; indices[34] = 23; indices[35] = 22; - _indexBuffer = new IndexBuffer(device, typeof(int), 36, BufferUsage.WriteOnly); + _indexBuffer = _graphicsResourceCreator.CreateIndexBuffer(typeof(int), 36, BufferUsage.WriteOnly); _indexBuffer.SetData(indices); } @@ -198,8 +200,10 @@ public void Draw(Matrix view, Matrix projection, GraphicsDevice device, Vector3 public void Dispose() { - _instanceVertexDeclaration.Dispose(); - _instanceBuffer.Dispose(); + _instanceVertexDeclaration = null; + _instanceBuffer = _graphicsResourceCreator.DisposeTracked(_instanceBuffer); + _geometryBuffer = _graphicsResourceCreator.DisposeTracked(_geometryBuffer); + _indexBuffer = _graphicsResourceCreator.DisposeTracked(_indexBuffer); } } } diff --git a/GameWorld/View3D/Services/GraphicsResourceCreator.cs b/GameWorld/View3D/Services/GraphicsResourceCreator.cs new file mode 100644 index 000000000..5b26fe0df --- /dev/null +++ b/GameWorld/View3D/Services/GraphicsResourceCreator.cs @@ -0,0 +1,280 @@ +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.Xna.Framework.Graphics; +using Serilog; +using Shared.Core.DependencyInjection; +using Shared.Core.ErrorHandling; + +namespace GameWorld.Core.Services +{ + public record GraphicsResourceRecord(int ResourceId, string ScopeOwner, string ResourceType, string SourceMember, string SourceFile, int SourceLine); + + public interface IGraphicsResourceCreator + { + string ScopeOwner { get; } + IReadOnlyList Records { get; } + + void RemoveTracking(object resource); + T? DisposeTracked(T? resource) where T : class, IDisposable; + + T Track(T resource, + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0) where T : class; + + Texture2D CreateTexture2D(int width, int height, + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0); + + Texture2D CreateTexture2D(int width, int height, bool mipMap, SurfaceFormat format, + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0); + + Texture2D CreateTextureFromStream(Stream stream, + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0); + + RenderTarget2D CreateRenderTarget2D(int width, int height, bool mipMap, SurfaceFormat preferredFormat, DepthFormat preferredDepthFormat, + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0); + + RenderTarget2D CreateRenderTarget2D(int width, int height, bool mipMap, SurfaceFormat preferredFormat, DepthFormat preferredDepthFormat, int preferredMultiSampleCount, RenderTargetUsage usage, + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0); + + RenderTarget2D CreateRenderTarget2D(int width, int height, bool mipMap, SurfaceFormat preferredFormat, DepthFormat preferredDepthFormat, int preferredMultiSampleCount, RenderTargetUsage usage, bool shared, + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0); + + SpriteBatch CreateSpriteBatch( + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0); + + DepthStencilState CreateDepthStencilState(DepthStencilState state, + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0); + + RasterizerState CreateRasterizerState(RasterizerState state, + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0); + + BasicEffect CreateBasicEffect( + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0); + + DynamicVertexBuffer CreateDynamicVertexBuffer(VertexDeclaration vertexDeclaration, int vertexCount, BufferUsage usage, + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0); + + VertexBuffer CreateVertexBuffer(VertexDeclaration vertexDeclaration, int vertexCount, BufferUsage usage, + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0); + + IndexBuffer CreateIndexBuffer(Type indexElementType, int indexCount, BufferUsage usage, + [CallerMemberName] string sourceMember = "", + [CallerFilePath] string sourceFile = "", + [CallerLineNumber] int sourceLine = 0); + } + + public class GraphicsResourceCreator : IGraphicsResourceCreator, IScopeOwnerAware, IDisposable + { + private record TrackedResource(object Resource, GraphicsResourceRecord Record); + + private readonly Func _graphicsDeviceFactory; + private readonly ILogger _logger = Logging.Create(); + private readonly List _trackedResources = []; + private bool _isDisposed; + + public string ScopeOwner { get; private set; } = "UnknownScopeOwner"; + public IReadOnlyList Records => _trackedResources.Select(x => x.Record).ToList(); + + private GraphicsDevice GraphicsDevice => _graphicsDeviceFactory() ?? throw new InvalidOperationException("GraphicsDevice is not available for the current scope."); + + public GraphicsResourceCreator(Func graphicsDeviceFactory) + { + _graphicsDeviceFactory = graphicsDeviceFactory; + } + + public void SetScopeOwner(Type ownerType) + { + ScopeOwner = ownerType.Name; + } + + public T Track(T resource, string sourceMember = "", string sourceFile = "", int sourceLine = 0) where T : class + { + if (resource == null) + return null; + + var resourceId = RuntimeHelpers.GetHashCode(resource); + + var record = new GraphicsResourceRecord( + resourceId, + ScopeOwner, + resource.GetType().Name, + sourceMember, + Path.GetFileName(sourceFile), + sourceLine); + + _trackedResources.Add(new TrackedResource( + resource, + record)); + + _logger.Here().Information( + "Graphics resource created: Id={ResourceId}, Type={ResourceType}, ScopeOwner={ScopeOwner}, Source={SourceFile}:{SourceLine}::{SourceMember}", + resourceId, + record.ResourceType, + record.ScopeOwner, + record.SourceFile, + record.SourceLine, + record.SourceMember); + + return resource; + } + + public void RemoveTracking(object resource) + { + if (resource == null) + return; + + var trackedMatches = _trackedResources.Where(x => ReferenceEquals(x.Resource, resource)).ToList(); + if (trackedMatches.Count == 0) + return; + + foreach (var match in trackedMatches) + { + _logger.Here().Information( + "Graphics resource deleted: Id={ResourceId}, Type={ResourceType}, ScopeOwner={ScopeOwner}, Source={SourceFile}:{SourceLine}::{SourceMember}", + match.Record.ResourceId, + match.Record.ResourceType, + match.Record.ScopeOwner, + match.Record.SourceFile, + match.Record.SourceLine, + match.Record.SourceMember); + } + + _trackedResources.RemoveAll(x => ReferenceEquals(x.Resource, resource)); + } + + public T? DisposeTracked(T? resource) where T : class, IDisposable + { + if (resource == null) + return null; + + try + { + resource.Dispose(); + } + catch + { + // Best effort cleanup: some resources may have already been disposed elsewhere. + } + finally + { + RemoveTracking(resource); + } + + return null; + } + + public Texture2D CreateTexture2D(int width, int height, string sourceMember = "", string sourceFile = "", int sourceLine = 0) + => Track(new Texture2D(GraphicsDevice, width, height), sourceMember, sourceFile, sourceLine); + + public Texture2D CreateTexture2D(int width, int height, bool mipMap, SurfaceFormat format, string sourceMember = "", string sourceFile = "", int sourceLine = 0) + => Track(new Texture2D(GraphicsDevice, width, height, mipMap, format), sourceMember, sourceFile, sourceLine); + + public Texture2D CreateTextureFromStream(Stream stream, string sourceMember = "", string sourceFile = "", int sourceLine = 0) + => Track(Texture2D.FromStream(GraphicsDevice, stream), sourceMember, sourceFile, sourceLine); + + public RenderTarget2D CreateRenderTarget2D(int width, int height, bool mipMap, SurfaceFormat preferredFormat, DepthFormat preferredDepthFormat, string sourceMember = "", string sourceFile = "", int sourceLine = 0) + => Track(new RenderTarget2D(GraphicsDevice, width, height, mipMap, preferredFormat, preferredDepthFormat), sourceMember, sourceFile, sourceLine); + + public RenderTarget2D CreateRenderTarget2D(int width, int height, bool mipMap, SurfaceFormat preferredFormat, DepthFormat preferredDepthFormat, int preferredMultiSampleCount, RenderTargetUsage usage, string sourceMember = "", string sourceFile = "", int sourceLine = 0) + => Track(new RenderTarget2D(GraphicsDevice, width, height, mipMap, preferredFormat, preferredDepthFormat, preferredMultiSampleCount, usage), sourceMember, sourceFile, sourceLine); + + public RenderTarget2D CreateRenderTarget2D(int width, int height, bool mipMap, SurfaceFormat preferredFormat, DepthFormat preferredDepthFormat, int preferredMultiSampleCount, RenderTargetUsage usage, bool shared, string sourceMember = "", string sourceFile = "", int sourceLine = 0) + => Track(new RenderTarget2D(GraphicsDevice, width, height, mipMap, preferredFormat, preferredDepthFormat, preferredMultiSampleCount, usage, shared), sourceMember, sourceFile, sourceLine); + + public SpriteBatch CreateSpriteBatch(string sourceMember = "", string sourceFile = "", int sourceLine = 0) + => Track(new SpriteBatch(GraphicsDevice), sourceMember, sourceFile, sourceLine); + + public DepthStencilState CreateDepthStencilState(DepthStencilState state, string sourceMember = "", string sourceFile = "", int sourceLine = 0) + => Track(state, sourceMember, sourceFile, sourceLine); + + public RasterizerState CreateRasterizerState(RasterizerState state, string sourceMember = "", string sourceFile = "", int sourceLine = 0) + => Track(state, sourceMember, sourceFile, sourceLine); + + public BasicEffect CreateBasicEffect(string sourceMember = "", string sourceFile = "", int sourceLine = 0) + => Track(new BasicEffect(GraphicsDevice), sourceMember, sourceFile, sourceLine); + + public DynamicVertexBuffer CreateDynamicVertexBuffer(VertexDeclaration vertexDeclaration, int vertexCount, BufferUsage usage, string sourceMember = "", string sourceFile = "", int sourceLine = 0) + => Track(new DynamicVertexBuffer(GraphicsDevice, vertexDeclaration, vertexCount, usage), sourceMember, sourceFile, sourceLine); + + public VertexBuffer CreateVertexBuffer(VertexDeclaration vertexDeclaration, int vertexCount, BufferUsage usage, string sourceMember = "", string sourceFile = "", int sourceLine = 0) + => Track(new VertexBuffer(GraphicsDevice, vertexDeclaration, vertexCount, usage), sourceMember, sourceFile, sourceLine); + + public IndexBuffer CreateIndexBuffer(Type indexElementType, int indexCount, BufferUsage usage, string sourceMember = "", string sourceFile = "", int sourceLine = 0) + => Track(new IndexBuffer(GraphicsDevice, indexElementType, indexCount, usage), sourceMember, sourceFile, sourceLine); + + public void Dispose() + { + if (_isDisposed) + return; + _isDisposed = true; + +#if DEBUG + var leakedResources = _trackedResources.Select(x => x.Record).ToList(); +#endif + + var trackedDisposables = new List(); + var uniqueDisposables = new HashSet(ReferenceEqualityComparer.Instance); + foreach (var item in _trackedResources) + { + if (item.Resource is IDisposable disposable && uniqueDisposables.Add(disposable)) + { + trackedDisposables.Add(disposable); + } + } + + for (var i = trackedDisposables.Count - 1; i >= 0; i--) + DisposeTracked(trackedDisposables[i]); + + _trackedResources.Clear(); + +#if DEBUG + //if (leakedResources.Count > 0) + //{ + // var leakedResourceMessage = BuildLeakedResourceMessage(leakedResources); + // _logger.Here().Error(leakedResourceMessage); + // throw new InvalidOperationException(leakedResourceMessage); + //} +#endif + } + +#if DEBUG + private string BuildLeakedResourceMessage(IReadOnlyList leakedResources) + { + var builder = new StringBuilder(); + builder.AppendLine($"Graphics resources were still alive when scope '{ScopeOwner}' was destroyed."); + builder.AppendLine($"Tracked resources still alive: {leakedResources.Count}"); + + foreach (var record in leakedResources) + builder.AppendLine($"- id={record.ResourceId} | {record.ResourceType} | source={record.SourceFile}:{record.SourceLine}::{record.SourceMember}"); + + return builder.ToString().TrimEnd(); + } +#endif + } +} diff --git a/GameWorld/View3D/Services/ResourceLibary.cs b/GameWorld/View3D/Services/ResourceLibary.cs index 04726bc73..787072520 100644 --- a/GameWorld/View3D/Services/ResourceLibary.cs +++ b/GameWorld/View3D/Services/ResourceLibary.cs @@ -99,14 +99,14 @@ public void Reset() _isInitialized = false; } - public Texture2D ForceLoadImage(string imagePath, out ImageInformation imageInformation) + public Texture2D ForceLoadImage(string imagePath, out ImageInformation imageInformation, IGraphicsResourceCreator graphicsResourceCreator = null) { - return ImageLoader.ForceLoadImage(imagePath, _pfs, _graphicsDevice, out imageInformation); + return ImageLoader.ForceLoadImage(imagePath, _pfs, _graphicsDevice, out imageInformation, false, graphicsResourceCreator); } - public Texture2D LoadTexture(string fileName, bool forceRefreshTexture = false, bool fromFile = false) + public Texture2D LoadTexture(string fileName, bool forceRefreshTexture = false, bool fromFile = false, IGraphicsResourceCreator graphicsResourceCreator = null) { - var texture = ImageLoader.LoadTextureAsTexture2d(fileName, _pfs, _graphicsDevice, out var _, fromFile); + var texture = ImageLoader.LoadTextureAsTexture2d(fileName, _pfs, _graphicsDevice, out var _, fromFile, graphicsResourceCreator); return texture; } diff --git a/GameWorld/View3D/Services/ScopedResourceLibrary.cs b/GameWorld/View3D/Services/ScopedResourceLibrary.cs index aa0a437e0..f8d90ae21 100644 --- a/GameWorld/View3D/Services/ScopedResourceLibrary.cs +++ b/GameWorld/View3D/Services/ScopedResourceLibrary.cs @@ -19,20 +19,22 @@ public class ScopedResourceLibrary : IScopedResourceLibrary, IDisposable private readonly ResourceLibrary _resourceLibrary; private readonly IEventHub _eventHub; private readonly IStandardDialogs _standardDialogs; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; private readonly Dictionary _cachedTextures = []; private bool _isDisposed = false; - public ScopedResourceLibrary(ResourceLibrary resourceLibrary, IEventHub eventHub, IStandardDialogs standardDialogs) + public ScopedResourceLibrary(ResourceLibrary resourceLibrary, IEventHub eventHub, IStandardDialogs standardDialogs, IGraphicsResourceCreator graphicsResourceCreator) { _resourceLibrary = resourceLibrary; _eventHub = eventHub; _standardDialogs = standardDialogs; + _graphicsResourceCreator = graphicsResourceCreator; _eventHub.Register(this, x=> ClearTextureCache()); _eventHub.Register(this, x => ClearTextureCache()); } - public Texture2D? ForceLoadImage(string imagePath, out ImageInformation imageInformation) => _resourceLibrary.ForceLoadImage(imagePath, out imageInformation); + public Texture2D? ForceLoadImage(string imagePath, out ImageInformation imageInformation) => _resourceLibrary.ForceLoadImage(imagePath, out imageInformation, _graphicsResourceCreator); public Texture2D? LoadTexture(string fileName, bool forceRefreshTexture = false, bool fromFile = false) { @@ -42,7 +44,7 @@ public ScopedResourceLibrary(ResourceLibrary resourceLibrary, IEventHub eventHub Texture2D? textureLoadResult = null; try { - textureLoadResult = _resourceLibrary.LoadTexture(fileName, forceRefreshTexture, fromFile); + textureLoadResult = _resourceLibrary.LoadTexture(fileName, forceRefreshTexture, fromFile, _graphicsResourceCreator); } catch (Exception ex) { @@ -70,7 +72,7 @@ public void Dispose() void ClearTextureCache() { foreach (var item in _cachedTextures) - item.Value?.Dispose(); + _graphicsResourceCreator.DisposeTracked(item.Value); _cachedTextures.Clear(); } } diff --git a/GameWorld/View3D/Utility/GraphicsResourceExceptionInfoProvider.cs b/GameWorld/View3D/Utility/GraphicsResourceExceptionInfoProvider.cs new file mode 100644 index 000000000..d0db998ac --- /dev/null +++ b/GameWorld/View3D/Utility/GraphicsResourceExceptionInfoProvider.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GameWorld.Core.Services; +using Shared.Core.DependencyInjection; +using Shared.Core.ErrorHandling.Exceptions; +using Shared.Core.ToolCreation; + +namespace GameWorld.Core.Utility +{ + internal class GraphicsResourceExceptionInfoProvider : IExceptionInformationProvider + { + private readonly IEditorManager _editorManager; + private readonly IScopeRepository _scopeRepository; + + public GraphicsResourceExceptionInfoProvider(IEditorManager editorManager, IScopeRepository scopeRepository) + { + _editorManager = editorManager; + _scopeRepository = scopeRepository; + } + + public void HydrateExcetion(ExceptionInformation exceptionInformation) + { + try + { + var allEditors = _editorManager.GetAllEditors(); + var currentEditorIndex = _editorManager.GetCurrentEditor(); + var hasCurrentEditor = currentEditorIndex >= 0 && currentEditorIndex < allEditors.Count; + var currentEditor = hasCurrentEditor ? allEditors[currentEditorIndex] : null; + + var editorHandles = _scopeRepository.GetEditorHandles(); + + foreach (var editor in editorHandles) + { + var isCurrentScope = currentEditor != null && ReferenceEquals(editor, currentEditor); + + try + { + var creator = _scopeRepository.GetRequiredService(editor); + var records = creator.Records; + var resourceLines = records + .Select(x => $"id={x.ResourceId} | {x.ResourceType} | owner={x.ScopeOwner} | source={x.SourceFile}:{x.SourceLine}::{x.SourceMember}") + .ToList(); + + exceptionInformation.GraphicsResourceScopes.Add( + new GraphicsResourceScopeInfo(creator.ScopeOwner, isCurrentScope, records.Count, resourceLines)); + } + catch + { + exceptionInformation.GraphicsResourceScopes.Add( + new GraphicsResourceScopeInfo($"{editor.DisplayName} ({editor.GetType().Name})", isCurrentScope, 0, new List())); + } + } + } + catch (Exception e) + { + exceptionInformation.CurrentEditorGraphicsResourceInfoError = e.Message; + } + } + } +} diff --git a/GameWorld/View3D/Utility/ImageLoader.cs b/GameWorld/View3D/Utility/ImageLoader.cs index 3647f426c..d93107d12 100644 --- a/GameWorld/View3D/Utility/ImageLoader.cs +++ b/GameWorld/View3D/Utility/ImageLoader.cs @@ -1,4 +1,5 @@ using System.IO; +using GameWorld.Core.Services; using Microsoft.Xna.Framework.Graphics; using Pfim; using Serilog; @@ -11,9 +12,9 @@ public static class ImageLoader { private static readonly ILogger _logger = Logging.CreateStatic(typeof(ImageLoader)); - public static Texture2D ForceLoadImage(string fileName, IPackFileService packFileService, GraphicsDevice graphicsDevice, out ImageInformation imageInfo, bool fromFile = false) + public static Texture2D ForceLoadImage(string fileName, IPackFileService packFileService, GraphicsDevice graphicsDevice, out ImageInformation imageInfo, bool fromFile = false, IGraphicsResourceCreator graphicsResourceCreator = null) { - return LoadTextureAsTexture2d(fileName, packFileService, graphicsDevice, out imageInfo, fromFile); + return LoadTextureAsTexture2d(fileName, packFileService, graphicsDevice, out imageInfo, fromFile, graphicsResourceCreator); } public static void SaveTexture(Texture2D texture, string path) @@ -56,14 +57,17 @@ public static IImage LoadImageFromBytes(byte[] imageContent, out ImageInformatio return image; } - private static Texture2D ConvertTexture2D(string fileName, IImage image, GraphicsDevice device) + private static Texture2D ConvertTexture2D(string fileName, IImage image, GraphicsDevice device, IGraphicsResourceCreator graphicsResourceCreator) { Texture2D texture = null; if (image.Format == ImageFormat.Rgba32) { try { - texture = new Texture2D(device, image.Width, image.Height, true, SurfaceFormat.Bgra32); + if (graphicsResourceCreator == null) + texture = new Texture2D(device, image.Width, image.Height, true, SurfaceFormat.Bgra32); + else + texture = graphicsResourceCreator.CreateTexture2D(image.Width, image.Height, true, SurfaceFormat.Bgra32); texture.SetData(0, null, image.Data, 0, image.DataLen); } catch @@ -96,7 +100,7 @@ private static Texture2D ConvertTexture2D(string fileName, IImage image, Graphic return texture; } - public static Texture2D LoadTextureAsTexture2d(string fileName, IPackFileService pfs, GraphicsDevice device, out ImageInformation out_imageInfo, bool fromFile) + public static Texture2D LoadTextureAsTexture2d(string fileName, IPackFileService pfs, GraphicsDevice device, out ImageInformation out_imageInfo, bool fromFile, IGraphicsResourceCreator graphicsResourceCreator = null) { out_imageInfo = null; var imageContent = GetFileBytes(pfs, fileName, fromFile); @@ -106,14 +110,16 @@ public static Texture2D LoadTextureAsTexture2d(string fileName, IPackFileService if (Path.GetExtension(fileName).ToLower() == ".png") { using var stream = new MemoryStream(imageContent); - return Texture2D.FromStream(device, stream); + if (graphicsResourceCreator == null) + return Texture2D.FromStream(device, stream); + return graphicsResourceCreator.CreateTextureFromStream(stream); } var image = LoadImageFromBytes(imageContent, out out_imageInfo); if (image == null) return null; - return ConvertTexture2D(fileName, image, device); + return ConvertTexture2D(fileName, image, device, graphicsResourceCreator); } } } diff --git a/GameWorld/View3D/WpfWindow/D3D11Host.cs b/GameWorld/View3D/WpfWindow/D3D11Host.cs index 167280f84..e0b903619 100644 --- a/GameWorld/View3D/WpfWindow/D3D11Host.cs +++ b/GameWorld/View3D/WpfWindow/D3D11Host.cs @@ -1,4 +1,5 @@ using GameWorld.Core.WpfWindow.Internals; +using GameWorld.Core.Services; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -49,12 +50,14 @@ public abstract class D3D11Host : Image, IDisposable private double _dpiScalingFactor = 1; private static bool _useASingleSharedGraphicsDevice = true; private List _toBeDisposedNextFrame = new List(); + private readonly IGraphicsResourceCreator _graphicsResourceCreator; /// /// Initializes a new instance of the class. /// - protected D3D11Host() + protected D3D11Host(IGraphicsResourceCreator graphicsResourceCreator) { + _graphicsResourceCreator = graphicsResourceCreator; // defaulting to fill as that's what's needed in most cases Stretch = Stretch.Fill; @@ -212,8 +215,7 @@ public void Dispose() Deactivated = null; if (_spriteBatch != null) { - _spriteBatch.Dispose(); - _spriteBatch = null; + _spriteBatch = _graphicsResourceCreator.DisposeTracked(_spriteBatch); } Dispose(true); } @@ -369,7 +371,7 @@ private void CreateGraphicsDeviceDependentResources(PresentationParameters pp) var height = pp.BackBufferHeight; var ms = pp.MultiSampleCount; - _sharedRenderTarget = new RenderTarget2D(GraphicsDevice, width, height, false, SurfaceFormat.Bgr32, + _sharedRenderTarget = _graphicsResourceCreator.CreateRenderTarget2D(width, height, false, SurfaceFormat.Bgr32, DepthFormat.Depth24Stencil8, 0, RenderTargetUsage.DiscardContents, true); _sharedRenderTarget.Name = "sharedRenderTarget"; _d3D11Image.SetBackBuffer(_sharedRenderTarget); @@ -377,7 +379,7 @@ private void CreateGraphicsDeviceDependentResources(PresentationParameters pp) // internal rendertarget; all user draws render into this before we draw it to the actual back buffer // that way flickering of screen will be prevented when under heavy system load (such as when using rendertargets on intel graphics: https://gitlab.com/MarcStan/MonoGame.Framework.WpfInterop/issues/12) // -> always preserve its contents so worst case user gets to see the old screen again - _cachedRenderTarget = new RenderTarget2D(GraphicsDevice, width, height, false, SurfaceFormat.Bgr32, + _cachedRenderTarget = _graphicsResourceCreator.CreateRenderTarget2D(width, height, false, SurfaceFormat.Bgr32, DepthFormat.Depth24Stencil8, ms, RenderTargetUsage.PreserveContents, false); _cachedRenderTarget.Name = "cachedRenderTarget"; } @@ -388,7 +390,7 @@ private void InitializeImageSource() _d3D11Image.IsFrontBufferAvailableChanged += OnIsFrontBufferAvailableChanged; CreateBackBuffer(); Source = _d3D11Image; - _spriteBatch = new SpriteBatch(GraphicsDevice); + _spriteBatch = _graphicsResourceCreator.CreateSpriteBatch(); } private void OnIsFrontBufferAvailableChanged(object sender, DependencyPropertyChangedEventArgs eventArgs) @@ -594,7 +596,7 @@ private void DisposeRenderTargetsFromPreviousFrames() { for (var i = 0; i < _toBeDisposedNextFrame.Count; i++) { - _toBeDisposedNextFrame[i]?.Dispose(); + _graphicsResourceCreator.DisposeTracked(_toBeDisposedNextFrame[i]); _toBeDisposedNextFrame.RemoveAt(i--); } } @@ -632,7 +634,7 @@ private void UnitializeImageSource() } if (_sharedRenderTarget != null) { - _sharedRenderTarget.Dispose(); + _graphicsResourceCreator.DisposeTracked(_sharedRenderTarget); _sharedRenderTarget = null; } if (_cachedRenderTarget != null) @@ -642,10 +644,11 @@ private void UnitializeImageSource() // TODO: this is a memoryleak, the code is intentional because Dispose actually crashes Monogame when using a shared graphicsdevice and disposing MSAA enabled rendertargets // at the very latest this will happen on window close, for SPA this is fine as the process will shut down // but if your editor has multiple windows (or tabs) that can be created/closed multiple times this will slowly increase memory usage.. + _graphicsResourceCreator.RemoveTracking(_cachedRenderTarget); } else { - _cachedRenderTarget.Dispose(); + _graphicsResourceCreator.DisposeTracked(_cachedRenderTarget); } _cachedRenderTarget = null; } diff --git a/GameWorld/View3D/WpfWindow/WpfGame.cs b/GameWorld/View3D/WpfWindow/WpfGame.cs index fd1a899ee..3903e6d35 100644 --- a/GameWorld/View3D/WpfWindow/WpfGame.cs +++ b/GameWorld/View3D/WpfWindow/WpfGame.cs @@ -31,7 +31,7 @@ public class WpfGame : D3D11Host, IWpfGame /// /// Creates a new instance of a game host panel. /// - public WpfGame(ResourceLibrary resourceLibrary, IStandardDialogs exceptionService, IEventHub eventHub, string contentDir = "Content") + public WpfGame(ResourceLibrary resourceLibrary, IStandardDialogs exceptionService, IEventHub eventHub, IGraphicsResourceCreator graphicsResourceCreator, string contentDir = "Content") : base(graphicsResourceCreator) { if (string.IsNullOrEmpty(contentDir)) throw new ArgumentNullException(nameof(contentDir)); diff --git a/Shared/SharedCore/Shared.Core/DependencyInjection/IScopeOwnerAware.cs b/Shared/SharedCore/Shared.Core/DependencyInjection/IScopeOwnerAware.cs new file mode 100644 index 000000000..52bf3cc55 --- /dev/null +++ b/Shared/SharedCore/Shared.Core/DependencyInjection/IScopeOwnerAware.cs @@ -0,0 +1,7 @@ +namespace Shared.Core.DependencyInjection +{ + public interface IScopeOwnerAware + { + void SetScopeOwner(Type ownerType); + } +} diff --git a/Shared/SharedCore/Shared.Core/DependencyInjection/ScopeRepository.cs b/Shared/SharedCore/Shared.Core/DependencyInjection/ScopeRepository.cs index 7092c8707..8e409ffe0 100644 --- a/Shared/SharedCore/Shared.Core/DependencyInjection/ScopeRepository.cs +++ b/Shared/SharedCore/Shared.Core/DependencyInjection/ScopeRepository.cs @@ -1,7 +1,6 @@ using System.Collections; using System.Reflection; using System.Text; -using System.Windows.Forms.VisualStyles; using Microsoft.Extensions.DependencyInjection; using Shared.Core.ErrorHandling; using Shared.Core.ToolCreation; @@ -45,6 +44,9 @@ public ScopeRepository(IServiceProvider rootProvider) public IServiceScope CreateScope(IEditorInterface owner) { var scope = _rootProvider.CreateScope(); + foreach (var ownerAware in scope.ServiceProvider.GetServices()) + ownerAware.SetScopeOwner(owner.GetType()); + Add(owner, scope); return scope; } @@ -52,6 +54,9 @@ public IServiceScope CreateScope(IEditorInterface owner) public IEditorInterface CreateScope(Type editorType) { var scope = _rootProvider.CreateScope(); + foreach (var ownerAware in scope.ServiceProvider.GetServices()) + ownerAware.SetScopeOwner(editorType); + var instance = scope.ServiceProvider.GetRequiredService(editorType) as IEditorInterface; if (instance == null) throw new Exception($"Type '{editorType}' is not a IEditorViewModel"); diff --git a/Shared/SharedCore/Shared.Core/ErrorHandling/Exceptions/ExceptionInformation.cs b/Shared/SharedCore/Shared.Core/ErrorHandling/Exceptions/ExceptionInformation.cs index f4cf8a53e..d9e2c0e58 100644 --- a/Shared/SharedCore/Shared.Core/ErrorHandling/Exceptions/ExceptionInformation.cs +++ b/Shared/SharedCore/Shared.Core/ErrorHandling/Exceptions/ExceptionInformation.cs @@ -4,6 +4,7 @@ namespace Shared.Core.ErrorHandling.Exceptions { public record ExceptionPackFileContainerInfo(bool IsMainEditable, bool IsCa, string Name, string SystemPath); public record ExceptionInstance(string Message, string[] StackTrace); + public record GraphicsResourceScopeInfo(string ScopeOwner, bool IsCurrentScope, int ResourceCount, List Resources); public class ExceptionInformation { @@ -27,6 +28,11 @@ public class ExceptionInformation public string EditorInputFile { get; set; } = "Not set"; public string EditorInputFileFull { get; set; } = "Not set"; public string EditorInputFilePack { get; set; } = "Not set"; + public string CurrentEditorGraphicsResourceScopeOwner { get; set; } = "Not set"; + public int CurrentEditorGraphicsResourceCount { get; set; } + public List CurrentEditorGraphicsResources { get; set; } = []; + public string CurrentEditorGraphicsResourceInfoError { get; set; } = ""; + public List GraphicsResourceScopes { get; set; } = []; public List LogHistory { get; set; } = []; public string UserMessage { get; set; } = ""; diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/StandardDialog/ExceptionHandling/CustomExceptionWindow.xaml.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/StandardDialog/ExceptionHandling/CustomExceptionWindow.xaml.cs index 906e80019..544601984 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/StandardDialog/ExceptionHandling/CustomExceptionWindow.xaml.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/StandardDialog/ExceptionHandling/CustomExceptionWindow.xaml.cs @@ -64,6 +64,18 @@ public CustomExceptionWindow(ExceptionInformation extendedExceptionInformation, extraInfo.AppendLine($"Culture: {extendedExceptionInformation.Culture}"); extraInfo.AppendLine($"Open editors: {extendedExceptionInformation.NumberOfOpenEditors}"); extraInfo.AppendLine($"Total Created editors: {extendedExceptionInformation.NumberOfOpenedEditors}"); + if (extendedExceptionInformation.GraphicsResourceScopes.Count > 0) + { + var totalResources = extendedExceptionInformation.GraphicsResourceScopes.Sum(x => x.ResourceCount); + extraInfo.AppendLine($"Graphics resources tracked: {totalResources} across {extendedExceptionInformation.GraphicsResourceScopes.Count} scope(s)"); + foreach (var scope in extendedExceptionInformation.GraphicsResourceScopes) + { + var marker = scope.IsCurrentScope ? " [CURRENT]" : string.Empty; + extraInfo.AppendLine($" {scope.ScopeOwner}{marker}: {scope.ResourceCount} resource(s)"); + } + } + if (string.IsNullOrWhiteSpace(extendedExceptionInformation.CurrentEditorGraphicsResourceInfoError) == false) + extraInfo.AppendLine($"Graphics resource info error: {extendedExceptionInformation.CurrentEditorGraphicsResourceInfoError}"); ExtraInfoHandle.Text = extraInfo.ToString(); } From c1c18dac48019a7188a63d9e0eeb98676c04618e Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 16 Apr 2026 14:14:13 +0200 Subject: [PATCH 2/4] Code --- .github/agents/theasseteditor.agent.md | 86 +++++++++++++++++++ .../ViewModels/TextureBuilder.cs | 12 ++- .../ViewModels/TextureEditorViewModel.cs | 11 ++- .../Editor/Rendering/TwuiPreviewBuilder.cs | 14 ++- .../Editor/Rendering/TwuiRenderComponent.cs | 2 + 5 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 .github/agents/theasseteditor.agent.md diff --git a/.github/agents/theasseteditor.agent.md b/.github/agents/theasseteditor.agent.md new file mode 100644 index 000000000..b6a4f3c75 --- /dev/null +++ b/.github/agents/theasseteditor.agent.md @@ -0,0 +1,86 @@ +--- +name: TheAssetEditor Coding Agent +description: "Use when working on TheAssetEditor C#/.NET/WPF codebase: AssetEditor app, Editors modules, Shared libraries, GameWorld, tests, dependency injection, XAML, and build/test validation." +tools: [read, search, edit, execute, todo] +user-invocable: true +--- +You are a specialized coding agent for TheAssetEditor. + +Your goal is to produce safe, minimal, verifiable changes that align with existing architecture and conventions. + +## Repository Context +- This is a large multi-project .NET solution centered on `AssetEditor.sln`. +- Main app is WPF (`AssetEditor/AssetEditor.csproj`) targeting `net10.0-windows`, with `LangVersion=preview` and nullable enabled. +- Architecture is modular: + - `AssetEditor/` for shell app and composition. + - `Editors/` for feature editors. + - `Shared/` for shared core, UI, formats, and utilities. + - `GameWorld/` for rendering/3D systems. + - `Testing/` for test projects. +- Dependency injection is heavily used (`Microsoft.Extensions.DependencyInjection`) with scoped/transient/singleton lifetimes. + +## Operating Principles +1. Preserve behavior unless the task explicitly asks for functional changes. +2. Prefer small, localized edits over broad refactors. +3. Match existing naming, style, and file organization. +4. Do not introduce new frameworks or patterns unless clearly justified by existing usage. +5. Keep WPF/XAML changes consistent with existing localization and binding patterns. +6. Prefer the UiCommand pattern when linking UI actions to functionality. +7. Optimize code for unit testing and avoid designs that require implementing fake classes. +8. Any change in `Shared.Core` must include corresponding unit tests in the same work item. +9. Any change in `Shared.GameFormats` requires explicit user permission before implementation. +10. Do not modify `RenderEngineComponent.cs` unless the user explicitly requests it. +11. For 3D world rendering or draw-loop integrations, add `RenderItems` instead of modifying the core render engine loop. + +## C# and Style Rules +- Follow `.editorconfig` as source of truth. +- Use spaces, 4-space indentation in C# files. +- Keep `nullable` expectations intact; avoid broad nullable annotation churn. +- Naming conventions to preserve: + - Instance fields: `_camelCase` + - Static fields: `s_camelCase` + - Members/types: `PascalCase` +- Keep `using` directives sorted with `System.*` first. +- Prefer minimal comments; add comments only for non-obvious logic. + +## WPF/XAML Rules +- Keep existing MVVM/data-binding patterns. +- Prefer UiCommand and IUiCommandFactory over direct UI event-handler logic when wiring functionality from UI interactions. +- Reuse Shared UI localization conventions already in repo. +- For cross-project localization namespaces, follow existing `Shared.Ui` assembly-qualified `xmlns` patterns. +- Avoid visual or resource-key churn outside the task scope. + +## Dependency Injection Rules +- Register services in existing composition roots and preserve lifetimes unless a change requires otherwise. +- Prefer existing abstractions/interfaces when adding dependencies. +- Do not duplicate registrations unless the pattern already intentionally does so. + +## Testing and Validation +Always validate with the smallest relevant scope first: +1. Build changed project(s): + - `dotnet build .\AssetEditor\AssetEditor.csproj -nologo` +2. Run targeted tests for affected area (project/file-level where possible). +3. If needed for confidence, run broader validation: + - `dotnet test .\AssetEditor.sln --configuration Release --no-restore --verbosity normal` + +When tests are mixed-framework, preserve existing framework choice (NUnit and MSTest both exist in this repository). + +## Testability Design Rules +- Design for unit testing by favoring small, focused classes with explicit dependencies and deterministic behavior. +- Avoid coupling business logic to UI framework types where possible; keep logic behind testable seams. +- Prefer existing abstractions and dependency injection instead of creating new layers purely for mocking. +- Avoid designs that require implementing fake classes to test core behavior. + +## Safety Checklist Before Finalizing +- Change is limited to requested scope. +- No unrelated formatting or file movement. +- Build/test command results are checked. +- Public APIs are unchanged unless requested. +- XAML namespace/localization conventions remain valid. + +## Response Format +When reporting back: +1. Briefly state what changed. +2. List exact files touched. +3. Summarize validation performed (build/tests and outcome). +4. Call out any risks, assumptions, or follow-up options. diff --git a/Editors/TextureEditor/ViewModels/TextureBuilder.cs b/Editors/TextureEditor/ViewModels/TextureBuilder.cs index 61c0af518..35171743d 100644 --- a/Editors/TextureEditor/ViewModels/TextureBuilder.cs +++ b/Editors/TextureEditor/ViewModels/TextureBuilder.cs @@ -12,12 +12,13 @@ namespace Editors.TextureEditor.ViewModels { - public class TextureBuilder + public class TextureBuilder : IDisposable { private readonly IScopedResourceLibrary _resourceLib; private readonly TextureToTextureRenderer _textureRenderer; private readonly IWpfGame _wpfGame; private readonly IGraphicsResourceCreator _graphicsResourceCreator; + private bool _isDisposed; public TextureBuilder(IScopedResourceLibrary resourceLibrary, IWpfGame wpfGame, IGraphicsResourceCreator graphicsResourceCreator) { @@ -106,5 +107,14 @@ public static BitmapImage BitmapToImageSource(Image bitmap) return bitmapImage; } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + _textureRenderer.Dispose(); + } } } diff --git a/Editors/TextureEditor/ViewModels/TextureEditorViewModel.cs b/Editors/TextureEditor/ViewModels/TextureEditorViewModel.cs index 4a9a72324..7400f1f37 100644 --- a/Editors/TextureEditor/ViewModels/TextureEditorViewModel.cs +++ b/Editors/TextureEditor/ViewModels/TextureEditorViewModel.cs @@ -1,11 +1,12 @@ -using Shared.Core.Misc; +using System; +using Shared.Core.Misc; using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; using Shared.Core.ToolCreation; namespace Editors.TextureEditor.ViewModels { - public class TextureEditorViewModel : NotifyPropertyChangedImpl, IEditorInterface, IFileEditor + public class TextureEditorViewModel : NotifyPropertyChangedImpl, IEditorInterface, IFileEditor, IDisposable { private readonly IPackFileService _pfs; private readonly TextureBuilder _textureBuilder; @@ -45,9 +46,11 @@ public void LoadFile(PackFile packFile) public void ShowTextureDetailsInfo() => ViewModel.ShowTextureDetailsInfo(); - public void Close() - { + public void Close() => Dispose(); + public void Dispose() + { + _textureBuilder.Dispose(); } } } diff --git a/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiPreviewBuilder.cs b/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiPreviewBuilder.cs index e224e507d..4e22e0ad9 100644 --- a/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiPreviewBuilder.cs +++ b/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiPreviewBuilder.cs @@ -11,7 +11,7 @@ namespace Editors.Twui.Editor.Rendering { - public class TwuiPreviewBuilder + public class TwuiPreviewBuilder : IDisposable { private readonly IWpfGame _wpfGame; private readonly IScopedResourceLibrary _resourceLibrary; @@ -21,6 +21,7 @@ public class TwuiPreviewBuilder private RenderTarget2D _renderTarget; private SpriteBatch _spriteBatch; private Texture2D _whiteSquareTexture; + private bool _isDisposed; public TwuiPreviewBuilder(IWpfGame wpfGame, IScopedResourceLibrary resourceLibrary, IGraphicsResourceCreator graphicsResourceCreator) { @@ -134,5 +135,16 @@ Rectangle DrawComponent(Rectangle localSpace, TwuiComponent component, int depth } record DebugData(Rectangle renderRect, Color color); + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + _renderTarget = _graphicsResourceCreator.DisposeTracked(_renderTarget); + _whiteSquareTexture = _graphicsResourceCreator.DisposeTracked(_whiteSquareTexture); + _spriteBatch = _graphicsResourceCreator.DisposeTracked(_spriteBatch); + } } } diff --git a/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiRenderComponent.cs b/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiRenderComponent.cs index 5a558f3ea..d9e72478f 100644 --- a/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiRenderComponent.cs +++ b/Editors/TwuiEditor/Editor.Twui/Editor/Rendering/TwuiRenderComponent.cs @@ -104,6 +104,8 @@ void DrawCheckerboardBackground(int squareSize) public void Dispose() { + _twuiPreviewBuilder.Dispose(); + _twuiPreview = null; _whiteSquareTexture = _graphicsResourceCreator.DisposeTracked(_whiteSquareTexture); _spriteBatch = _graphicsResourceCreator.DisposeTracked(_spriteBatch); } From 4e73372e0cbc0f07a846b72801524c6da5ae771b Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 16 Apr 2026 14:24:31 +0200 Subject: [PATCH 3/4] Code --- .../Core/MenuBarViews/MenuBarView.xaml | 2 +- .../Core/MenuBarViews/MenuBarView.xaml.cs | 29 ++++++++++++------- .../BaseDialogs/MathViews/Vector2View.xaml | 2 +- .../BaseDialogs/MathViews/Vector2ViewModel.cs | 2 ++ 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/Editors/Kitbashing/KitbasherEditor/Core/MenuBarViews/MenuBarView.xaml b/Editors/Kitbashing/KitbasherEditor/Core/MenuBarViews/MenuBarView.xaml index ae9cdc800..d96b8bf08 100644 --- a/Editors/Kitbashing/KitbasherEditor/Core/MenuBarViews/MenuBarView.xaml +++ b/Editors/Kitbashing/KitbasherEditor/Core/MenuBarViews/MenuBarView.xaml @@ -5,7 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:viewmodel="clr-namespace:KitbasherEditor.ViewModels.MenuBarViews" xmlns:menusystem="clr-namespace:Shared.Ui.Common.MenuSystem;assembly=Shared.Ui" - mc:Ignorable="d" Loaded="UserControl_Loaded" > + mc:Ignorable="d" Loaded="UserControl_Loaded" Unloaded="UserControl_Unloaded" >