From cb4092179f9501b60871716cf2b4650ea4a921ba Mon Sep 17 00:00:00 2001 From: Misaki Date: Tue, 21 Apr 2026 23:20:29 +0900 Subject: [PATCH] refactor(core): asset pipeline overhaul & dock removal - Introduced IAsset interface and refactored asset loading/saving. - Migrated TextureContentHeader to Ghost.Engine; updated usage. - Rewrote AssetRegistry, AssetCatalog, ImportCoordinator for new asset flow. - Added thread-safe ConcurrentHashSet utility. - Improved EditorApplication folder management/init. - Updated TextureAssetHandler/TextureProcessor for new import/export. - Added EditorContentProvider for asset access. - Updated AssetManager to use new AssetType enum; removed GCHandle. - Removed all custom docking controls and templates. - Deleted obsolete ViewModels/Pages (Console, Hierarchy, Inspector, Project). - Renamed ProjectBrowser to ContentBrowser; updated references. - Updated NuGet packages, Result conversions, and commit instructions. - General cleanup: namespaces, dead code, structure. --- .github/commit-instructions.md | 7 + src/.github/agents/code-executor.agent.md | 77 ---- .../AssetHandler/AssetHandler.cs | 27 +- .../AssetHandler/AssetHandlerRegistry.cs | 2 + .../AssetHandler/AssetMeta.cs | 15 +- .../AssetHandler/AssetProcesser.cs | 38 -- .../AssetHandler/TextureAssetHandler.cs | 151 +++++++- .../AssetHandler/TextureProcessor.cs | 13 +- .../Contracts/IAssetRegistry.cs | 13 +- .../Ghost.Editor.Core/EditorApplication.cs | 43 ++- .../Services/AssetCatalog.cs | 77 +--- .../Services/AssetRegistry.cs | 173 ++++++--- .../Services/EditorContentProvider.cs | 43 +++ .../Services/ImportCoordinator.cs | 37 +- src/Editor/Ghost.Editor/App.xaml | 1 - src/Editor/Ghost.Editor/App.xaml.cs | 22 +- src/Editor/Ghost.Editor/Ghost.Editor.csproj | 2 + .../Pages/EngineEditor/ConsoleViewModel.cs | 53 --- .../Pages/EngineEditor/HierarchyViewModel.cs | 36 -- .../Pages/EngineEditor/InspectorViewModel.cs | 31 -- .../Pages/EngineEditor/ProjectViewModel.cs | 143 -------- ...Browser.Menu.cs => ContentBrowser.Menu.cs} | 2 +- ...rojectBrowser.xaml => ContentBrowser.xaml} | 2 +- ...Browser.xaml.cs => ContentBrowser.xaml.cs} | 6 +- .../Views/Controls/Docking/DockContainer.cs | 211 ----------- .../Views/Controls/Docking/DockDocument.cs | 42 --- .../Views/Controls/Docking/DockDocument.xaml | 22 -- .../Views/Controls/Docking/DockGroup.cs | 182 --------- .../Views/Controls/Docking/DockGroup.xaml | 23 -- .../Views/Controls/Docking/DockModule.cs | 47 --- .../Views/Controls/Docking/DockPanel.cs | 231 ------------ .../Views/Controls/Docking/DockPanel.xaml | 15 - .../Controls/Docking/DockRegionHighlight.cs | 15 - .../Controls/Docking/DockRegionHighlight.xaml | 20 - .../Views/Controls/Docking/DockingLayout.cs | 344 ------------------ .../Views/Controls/Docking/DockingLayout.xaml | 20 - .../Views/Controls/Docking/Enums.cs | 10 - .../Views/Controls/Docking/FloatingWindow.cs | 42 --- .../Views/Pages/EngineEditor/ConsolePage.xaml | 81 ----- .../Pages/EngineEditor/ConsolePage.xaml.cs | 19 - .../Pages/EngineEditor/HierarchyPage.xaml | 44 --- .../Pages/EngineEditor/HierarchyPage.xaml.cs | 61 ---- .../Pages/EngineEditor/InspectorPage.xaml | 45 --- .../Pages/EngineEditor/InspectorPage.xaml.cs | 29 -- .../Views/Pages/EngineEditor/ProjectPage.xaml | 140 ------- .../Pages/EngineEditor/ProjectPage.xaml.cs | 25 -- .../Views/Pages/EngineEditor/ScenePage.xaml | 18 - .../Pages/EngineEditor/ScenePage.xaml.cs | 45 --- .../Views/Windows/EngineEditorWindowOld.xaml | 246 ------------- .../Windows/EngineEditorWindowOld.xaml.cs | 88 ----- src/Runtime/Ghost.Core/Common.cs | 13 - src/Runtime/Ghost.Core/Ghost.Core.csproj | 2 +- src/Runtime/Ghost.Core/Result.cs | 3 +- .../Ghost.Core/Utilities/ConcurrentHashSet.cs | 162 +++++++++ ...ativeMemoryManager.cs => MemoryManager.cs} | 46 +++ src/Runtime/Ghost.Engine/AssetLoader/Asset.cs | 108 ------ .../Ghost.Engine/AssetManager.Texture.cs | 14 +- src/Runtime/Ghost.Engine/AssetManager.cs | 52 ++- .../Ghost.Entities/EntityQuery.JobChunk.cs | 1 - 59 files changed, 700 insertions(+), 2780 deletions(-) create mode 100644 .github/commit-instructions.md delete mode 100644 src/.github/agents/code-executor.agent.md delete mode 100644 src/Editor/Ghost.Editor.Core/AssetHandler/AssetProcesser.cs create mode 100644 src/Editor/Ghost.Editor.Core/Services/EditorContentProvider.cs delete mode 100644 src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/ConsoleViewModel.cs delete mode 100644 src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/HierarchyViewModel.cs delete mode 100644 src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/InspectorViewModel.cs delete mode 100644 src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs rename src/Editor/Ghost.Editor/Views/Controls/{ProjectBrowser.Menu.cs => ContentBrowser.Menu.cs} (97%) rename src/Editor/Ghost.Editor/Views/Controls/{ProjectBrowser.xaml => ContentBrowser.xaml} (99%) rename src/Editor/Ghost.Editor/Views/Controls/{ProjectBrowser.xaml.cs => ContentBrowser.xaml.cs} (97%) delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/DockContainer.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/DockDocument.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/DockDocument.xaml delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/DockGroup.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/DockGroup.xaml delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/DockModule.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/DockPanel.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/DockPanel.xaml delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/DockRegionHighlight.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/DockRegionHighlight.xaml delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/DockingLayout.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/DockingLayout.xaml delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/Enums.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Controls/Docking/FloatingWindow.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ConsolePage.xaml delete mode 100644 src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ConsolePage.xaml.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Pages/EngineEditor/HierarchyPage.xaml delete mode 100644 src/Editor/Ghost.Editor/Views/Pages/EngineEditor/HierarchyPage.xaml.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Pages/EngineEditor/InspectorPage.xaml delete mode 100644 src/Editor/Ghost.Editor/Views/Pages/EngineEditor/InspectorPage.xaml.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ProjectPage.xaml delete mode 100644 src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ProjectPage.xaml.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ScenePage.xaml delete mode 100644 src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ScenePage.xaml.cs delete mode 100644 src/Editor/Ghost.Editor/Views/Windows/EngineEditorWindowOld.xaml delete mode 100644 src/Editor/Ghost.Editor/Views/Windows/EngineEditorWindowOld.xaml.cs create mode 100644 src/Runtime/Ghost.Core/Utilities/ConcurrentHashSet.cs rename src/Runtime/Ghost.Core/Utilities/{NativeMemoryManager.cs => MemoryManager.cs} (57%) diff --git a/.github/commit-instructions.md b/.github/commit-instructions.md new file mode 100644 index 0000000..cba523b --- /dev/null +++ b/.github/commit-instructions.md @@ -0,0 +1,7 @@ +Use this instructions when writing a git commit message + +The first line should be a single line with no more than 50 characters that summary the changes. The second line should be blank. Start at the third line for actual changes. + +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + +Commits MUST be prefixed with a type, which consists of a noun, feat, fix, etc., followed by the OPTIONAL scope, OPTIONAL !, and REQUIRED terminal colon and space. The type feat MUST be used when a commit adds a new feature to your application or library. The type fix MUST be used when a commit represents a bug fix for your application. A scope MAY be provided after a type. A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis, e.g., fix(parser) A description MUST immediately follow the colon and space after the typescope prefix. The description is a short summary of the code changes, e.g., fix array parsing issue when multiple spaces were contained in string. A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes. The body MUST begin one blank line after the description. A commit body is free-form and MAY consist of any number of newline separated paragraphs. One or more footers MAY be provided one blank line after the body. Each footer MUST consist of a word token, followed by either a or # separator, followed by a string value (this is inspired by the git trailer convention). A footer’s token MUST use - in place of whitespace characters, e.g., Acked-by (this helps differentiate the footer section from a multi-paragraph body). An exception is made for BREAKING CHANGE, which MAY also be used as a token. A footer’s value MAY contain spaces and newlines, and parsing MUST terminate when the next valid footer tokenseparator pair is observed. Breaking changes MUST be indicated in the typescope prefix of a commit, or as an entry in the footer. If included as a footer, a breaking change MUST consist of the uppercase text BREAKING CHANGE, followed by a colon, space, and description, e.g., BREAKING CHANGE environment variables now take precedence over config files. If included in the typescope prefix, breaking changes MUST be indicated by a ! immediately before the . If ! is used, BREAKING CHANGE MAY be omitted from the footer section, and the commit description SHALL be used to describe the breaking change. Types other than feat and fix MAY be used in your commit messages, e.g., docs update ref docs. The units of information that make up Conventional Commits MUST NOT be treated as case-sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase. BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer. \ No newline at end of file diff --git a/src/.github/agents/code-executor.agent.md b/src/.github/agents/code-executor.agent.md deleted file mode 100644 index 3778bf6..0000000 --- a/src/.github/agents/code-executor.agent.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -name: code-executor -description: plan-executing coding agent. It heavily emphasizes strict adherence to the provided plan, rigorous Test-Driven Development (TDD) practices, and high-performance output. ---- - -# code-executor - -## 1. Agent Identity & Core Objective -**Role:** Senior Plan Executor & Performance-Oriented Developer -**Objective:** To meticulously execute predefined architectural and feature plans using a strict Test-Driven Development (TDD) workflow, ensuring all deliverables are highly optimized, performant, and perfectly aligned with the provided specifications. - -You do not invent new features. You do not alter the architectural vision. You execute the plan with precision, speed, and uncompromising quality. - ---- - -## 2. Core Directives - -### I. Strict Plan Adherence -* **Zero Deviation:** Implement strictly what is detailed in the provided plan. Do not add "nice-to-have" features, scope creep, or unauthorized structural changes. -* **Clarification over Assumption:** If a step in the plan is ambiguous, incomplete, or technically unfeasible, halt execution and request clarification. Do not guess. -* **Traceability:** Every piece of code written must directly map back to a specific requirement or step in the provided plan. - -### II. Absolute TDD Workflow (Red-Green-Refactor) -* You must follow strict TDD principles for every implementation. No production code is written without a failing test existing first. -* **Red:** Write comprehensive, edge-case-aware unit and integration tests based *only* on the plan's requirements. -* **Green:** Write the minimal necessary production code to make the tests pass. -* **Refactor:** Optimize the code for readability, maintainability, and performance without changing its behavior (tests must remain green). - -### III. Performance & Optimization Focus -* **Algorithmic Efficiency:** Prioritize optimal Time (Big O) and Space complexity. -* **Resource Management:** Ensure proper memory management, garbage collection awareness, and prevent memory leaks. -* **Concurrency & Asynchrony:** Utilize non-blocking operations and efficient concurrency models where appropriate to maximize throughput. - ---- - -## 3. Execution Protocol - -When provided with a plan, you will execute the following phases sequentially: - -### Phase 1: Plan Ingestion & Test Strategy -1. **Analyze:** Read the provided plan thoroughly. -2. **Deconstruct:** Break the plan down into testable units (functions, classes, API endpoints). -3. **Report:** Output a brief summary acknowledging the exact scope of what will be built and the testing strategy. - -### Phase 2: Test Generation (RED) -1. Write the tests for the current step of the plan. -2. Ensure tests cover standard use cases, boundary conditions, invalid inputs, and error handling. -3. *Output the test files and confirm they will currently fail.* - -### Phase 3: Implementation (GREEN) -1. Write the exact production code required to satisfy the generated tests. -2. Focus strictly on passing the tests. Do not prematurely optimize in this phase. -3. *Output the production code and confirm tests are now passing.* - -### Phase 4: Performance Refactoring (REFACTOR) -1. Review the passing code for bottlenecks, redundant logic, or memory inefficiencies. -2. Refactor the code applying performance best practices. -3. Rerun tests to ensure functional equivalence. -4. *Output the refactored code alongside a brief explanation of the performance improvements made.* - ---- - -## 4. Constraints & Anti-Patterns - -* **DO NOT** skip the testing phase under any circumstances, even for "simple" scripts. -* **DO NOT** mock core logic that is meant to be implemented in the current step; only mock external dependencies (databases, network calls, file systems). -* **DO NOT** modify the testing framework or configuration unless explicitly stated in the plan. -* **DO NOT** return code with placeholder comments (e.g., `// TODO: Implement this`). Write complete, working code for the current step. - ---- - -## 5. Output Format -When delivering responses, structure your output clearly: -1. **Context:** Which step of the plan is currently being addressed. -2. **Tests:** Code blocks containing the test specifications. -3. **Implementation:** Code blocks containing the production code. -4. **Notes:** Any necessary instructions for running the code or specific performance characteristics achieved. \ No newline at end of file diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs index 07dc99b..1812d40 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs @@ -1,22 +1,45 @@ using Ghost.Core; +using Ghost.Engine; namespace Ghost.Editor.Core.AssetHandler; [AttributeUsage(AttributeTargets.Class)] public sealed class CustomAssetHandlerAttribute : Attribute { - public CustomAssetHandlerAttribute(string id, string[] supportedExtensions, int version = 1) + public CustomAssetHandlerAttribute(string TypeID, string[] supportedExtensions, int version = 1) { } } +public interface IAsset : IDisposable +{ + public Guid ID + { + get; + } + + public Guid TypeID + { + get; + } + + public IAssetSettings Settings + { + get; + } +} + public interface IAssetExportOptions; public interface IAssetHandler { bool CanExport => false; + AssetType TargetAssetType { get; } IAssetSettings? CreateDefaultSettings(); + + ValueTask> LoadAssetAsync(Stream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default); + ValueTask SaveAssetAsync(Stream targetStream, IAsset asset, CancellationToken token = default); ValueTask ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default); ValueTask ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default); -} \ No newline at end of file +} diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandlerRegistry.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandlerRegistry.cs index 0f41285..47d93db 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandlerRegistry.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandlerRegistry.cs @@ -1,3 +1,5 @@ +using Ghost.Engine; + namespace Ghost.Editor.Core.AssetHandler; /// diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetMeta.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetMeta.cs index ae2f003..6ee9d4f 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetMeta.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetMeta.cs @@ -1,3 +1,4 @@ +using Ghost.Engine; using System.Text.Json; using System.Text.Json.Serialization; @@ -67,7 +68,8 @@ public sealed class AssetMeta internal static class AssetMetaIO { - private const string _META_EXTENSION = ".gmeta"; + public const string META_EXTENSION_NAME = "gmeta"; + public const string META_EXTENSION = ".gmeta"; private static readonly JsonSerializerOptions s_options = new() { @@ -107,10 +109,17 @@ internal static class AssetMetaIO { File.Delete(metaPath); } + File.Move(tempPath, metaPath); } - public static string GetMetaPath(string sourceFilePath) => sourceFilePath + _META_EXTENSION; + public static string GetMetaPath(string sourceFilePath) + { + return sourceFilePath + META_EXTENSION; + } - public static string GetSourcePath(string metaPath) => metaPath[..^_META_EXTENSION.Length]; + public static string GetSourcePath(string metaPath) + { + return metaPath[..^META_EXTENSION.Length]; + } } diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetProcesser.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetProcesser.cs deleted file mode 100644 index 5d1dc1c..0000000 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetProcesser.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Ghost.Editor.Core.Contracts; -using Ghost.Engine.AssetLoader; - -namespace Ghost.Editor.Core.AssetHandler; - -[AttributeUsage(AttributeTargets.Class)] -public sealed class CustomAssetProcesserAttribute : Attribute -{ - public Type Type => typeof(T); -} - -public readonly struct AssetProcesserContext -{ - public IAssetRegistry Registry - { - get; init; - } - - public string AssetPath - { - get; init; - } - - public Asset Asset - { - get; init; - } - - public IAssetHandler Handler - { - get; init; - } -} - -public interface IAssetProcesser -{ - ValueTask ProcessAsync(AssetProcesserContext ctx); -} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAssetHandler.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAssetHandler.cs index 73cef30..738788b 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAssetHandler.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAssetHandler.cs @@ -1,7 +1,10 @@ using Ghost.Core; +using Ghost.Engine; using Ghost.Engine.AssetLoader; using Ghost.Graphics.RHI; using ImageMagick; +using Misaki.HighPerformance.LowLevel; +using Misaki.HighPerformance.LowLevel.Buffer; using System.Runtime.InteropServices; namespace Ghost.Editor.Core.AssetHandler; @@ -46,6 +49,59 @@ public enum MipmapFilter : uint MitchellNetravali } +[Guid(GUID)] +public class TextureAsset : IAsset +{ + public const string GUID = "27965FFF-860C-40EF-9123-1874D7DE9CDC"; + + private static readonly Guid s_typeID = Guid.Parse(GUID); + + private readonly Guid _id; + private readonly IAssetSettings _settings; + + private readonly MagickImage _textureData; + private readonly uint _width; + private readonly uint _height; + private readonly uint _depth; + private readonly uint _colorComponents; + private readonly uint _dimension; + + public Guid ID => _id; + public Guid TypeID => s_typeID; + public IAssetSettings Settings => _settings; + + public MagickImage TextureData => _textureData; + public uint Width => _width; + public uint Height => _height; + public uint Depth => _depth; + public uint Dimension => _dimension; + public uint ColorComponents => _colorComponents; + + internal TextureAsset([OwnershipTransfer] MagickImage data, TextureContentHeader header, Guid id, IAssetSettings settings) + { + _id = id; + _settings = settings; + + _textureData = data; + _width = header.width; + _height = header.height; + _depth = header.depth; + _dimension = header.dimension; + _colorComponents = header.colorComponents; + } + + ~TextureAsset() + { + Dispose(); + } + + public void Dispose() + { + _textureData.Dispose(); + GC.SuppressFinalize(this); + } +} + public class TextureAssetSettings : IAssetSettings { public struct BasicSettings() @@ -70,6 +126,11 @@ public class TextureAssetSettings : IAssetSettings get; set; } = 1; + public int Depth + { + get; set; + } = 1; + public bool IsSRGB { get; set; @@ -193,26 +254,99 @@ public class TextureAssetSettings : IAssetSettings } = new SamplerSettings(); } -[CustomAssetHandler(_GUID, [".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr"], 1)] -[Guid(_GUID)] +[CustomAssetHandler(TextureAsset.GUID, [".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr"], 1)] internal class TextureAssetHandler : IAssetHandler { - private const string _GUID = "27965FFF-860C-40EF-9123-1874D7DE9CDC"; + public AssetType TargetAssetType => AssetType.Texture; public IAssetSettings? CreateDefaultSettings() { return new TextureAssetSettings(); } + private static TextureDimension GetTextureDimension(TextureAssetSettings settings) + { + if (settings.Basic.Columns > 1 && settings.Basic.Rows > 1) + { + if (settings.Basic.Depth > 1) + { + return TextureDimension.Texture3D; + } + + return TextureDimension.Texture2DArray; + } + + if (settings.Basic.Columns == 1 && settings.Basic.Rows == 1) + { + if (settings.Basic.Depth == 6) + { + return TextureDimension.TextureCube; + } + else if (settings.Basic.Depth > 6 && settings.Basic.Depth % 6 == 0) + { + return TextureDimension.TextureCubeArray; + } + } + + // If none of the above conditions are met, we will treat it as a regular 2D texture. + return TextureDimension.Texture2D; + } + + public ValueTask> LoadAssetAsync(Stream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) + { + try + { + var image = new MagickImage(assetStream); + + var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings(); + var contentHeader = new TextureContentHeader + { + width = image.Width, + height = image.Height, + depth = image.Depth, + colorComponents = image.ChannelCount, + dimension = (uint)GetTextureDimension(textureSettings) + }; + + return ValueTask.FromResult(Result.Success(new TextureAsset(image, contentHeader, id, textureSettings))); + } + catch (Exception ex) + { + return ValueTask.FromResult(Result.Failure(ex.Message)); + } + } + + public async ValueTask SaveAssetAsync(Stream targetStream, IAsset asset, CancellationToken token = default) + { + if (asset is not TextureAsset textureAsset) + { + return Result.Failure("Asset type is not TextureAsset"); + } + + try + { + await textureAsset.TextureData.WriteAsync(targetStream, token); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Failure(ex.Message); + } + } + public async ValueTask ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) { try { - var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings(); using var image = new MagickImage(sourceStream); - var bytes = image.ToByteArray(); + var pixels = image.GetPixels().GetValues(); + if (pixels == null) + { + return Result.Failure("Failed to retrieve pixel data from the source image."); + } - var (path, mip) = await TextureProcessor.CompressToCacheAsync(EditorApplication.ImportsFolderPath, id, bytes, image.Width, image.Height, image.Depth, textureSettings, token) + var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings(); + var (path, mip) = await TextureProcessor.CompressToCacheAsync(EditorApplication.CacheFolderPath, id, pixels, image.Width, image.Height, image.Depth, textureSettings, token) .ConfigureAwait(false); targetStream.Seek(0, SeekOrigin.Begin); @@ -224,12 +358,13 @@ internal class TextureAssetHandler : IAssetHandler depth = image.Depth, colorComponents = image.ChannelCount, mipLevels = (uint)mip, - dimension = (int)TextureDimension.Texture2D // TODO: Implement dimension calculation + dimension = (uint)GetTextureDimension(textureSettings) }; targetStream.Write(MemoryMarshal.AsBytes(new Span(ref contentHeader))); - await targetStream.WriteAsync(bytes, token).ConfigureAwait(false); + await using var ddsStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + await ddsStream.CopyToAsync(targetStream, token).ConfigureAwait(false); await targetStream.FlushAsync(token).ConfigureAwait(false); return Result.Success(); diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs index 8f03454..482eb33 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs @@ -1,3 +1,4 @@ +using Ghost.Graphics.RHI; using Ghost.Nvtt; using ImageMagick; using Misaki.HighPerformance.LowLevel; @@ -25,7 +26,7 @@ internal static class TextureProcessor { private readonly string _outputPath; - private readonly byte[] _image; + private readonly float[] _image; private readonly uint _depth; private readonly uint _width; private readonly uint _height; @@ -37,7 +38,7 @@ internal static class TextureProcessor public Task Task => _completionSource.Task; - public NvttPipelineTask(string outputPath, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings) + public NvttPipelineTask(string outputPath, float[] image, uint width, uint height, uint depth, TextureAssetSettings settings) { _outputPath = outputPath; _image = image; @@ -163,11 +164,15 @@ internal static class TextureProcessor } } - public static async ValueTask<(string cachePath, int mipmapCount)> CompressToCacheAsync(string cachesFolderPath, Guid assetId, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken) + public static async ValueTask<(string cachePath, int mipmapCount)> CompressToCacheAsync(string cachesFolderPath, Guid assetId, float[] image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken) { var settingsHash = ComputeSettingsHash(settings); var cacheFileName = $"texturecache_{assetId:N}_{settingsHash:X16}.dds"; - var cachePath = Path.Combine(cachesFolderPath, cacheFileName); + + var textureCachePath = Path.Combine(cachesFolderPath, "TextureCache"); + var cachePath = Path.Combine(textureCachePath, cacheFileName); + + Directory.CreateDirectory(textureCachePath); if (File.Exists(cachePath)) { diff --git a/src/Editor/Ghost.Editor.Core/Contracts/IAssetRegistry.cs b/src/Editor/Ghost.Editor.Core/Contracts/IAssetRegistry.cs index 5348bf9..bd8a492 100644 --- a/src/Editor/Ghost.Editor.Core/Contracts/IAssetRegistry.cs +++ b/src/Editor/Ghost.Editor.Core/Contracts/IAssetRegistry.cs @@ -1,5 +1,5 @@ using Ghost.Core; -using Ghost.Editor.Core.Services; +using Ghost.Editor.Core.AssetHandler; using Ghost.Engine.AssetLoader; namespace Ghost.Editor.Core.Contracts; @@ -38,7 +38,6 @@ public sealed class AssetChangedEventArgs : EventArgs } } -[EditorInjection(EditorInjectionAttribute.ServiceLifetime.Singleton, typeof(AssetRegistry))] public interface IAssetRegistry : IDisposable { string? GetAssetPath(Guid id); @@ -46,6 +45,12 @@ public interface IAssetRegistry : IDisposable ValueTask> ImportAssetAsync(string sourceFilePath, string targetAssetPath, CancellationToken token = default); ValueTask ReimportAssetAsync(Guid assetId, string sourceFilePath, CancellationToken token = default); - ValueTask> LoadAssetAsync(Guid id, CancellationToken token = default); - ValueTask SaveAssetAsync(Asset asset, CancellationToken token = default); + ValueTask> LoadAssetAsync(Guid id, CancellationToken token = default); + ValueTask SaveAssetAsync(IAsset asset, CancellationToken token = default); + ValueTask SaveAssetAsync(Guid id, CancellationToken token = default); + + void SetAssetDirty(Guid id); + ValueTask SaveAssetIfDirtyAsync(IAsset asset, CancellationToken token = default); + ValueTask SaveAssetIfDirtyAsync(Guid id, CancellationToken token = default); + ValueTask SaveDirtyAssetsAsync(); } diff --git a/src/Editor/Ghost.Editor.Core/EditorApplication.cs b/src/Editor/Ghost.Editor.Core/EditorApplication.cs index b88cc97..53dfe95 100644 --- a/src/Editor/Ghost.Editor.Core/EditorApplication.cs +++ b/src/Editor/Ghost.Editor.Core/EditorApplication.cs @@ -1,4 +1,3 @@ -using Ghost.Editor.Core.Utilities; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; @@ -7,17 +6,26 @@ namespace Ghost.Editor.Core; public static class EditorApplication { public const string ASSETS_FOLDER_NAME = "Assets"; - public const string SOURCES_FOLDER_NAME = "Sources"; public const string PACKAGES_FOLDER_NAME = "Packages"; public const string LIBRARY_FOLDER_NAME = "Library"; + public const string CACHE_FOLDER_NAME = "Cache"; public const string CONFIG_FOLDER_NAME = "Config"; public const string IMPORTS_FOLDER_NAME = "Imports"; private static IServiceProvider? s_serviceProvider; + private static string s_currentProjectPath = string.Empty; private static string s_currentProjectName = string.Empty; + private static string s_assetsFolderPath = string.Empty; + private static string s_packagesFolderPath = string.Empty; + private static string s_libraryFolderPath = string.Empty; + private static string s_cacheFolderPath = string.Empty; + private static string s_configFolderPath = string.Empty; + + private static string s_libraryImportsFolderPath = string.Empty; + private static DispatcherQueue? s_dispatcherQueue; internal static Application CurrentApplication => Application.Current; @@ -25,13 +33,12 @@ public static class EditorApplication public static string ProjectPath => s_currentProjectPath; public static string ProjectName => s_currentProjectName; - public static readonly string AssetsFolderPath = Path.Combine(ProjectPath, ASSETS_FOLDER_NAME); - public static readonly string SourcesFolderPath = Path.Combine(ProjectPath, SOURCES_FOLDER_NAME); - public static readonly string PackagesFolderPath = Path.Combine(ProjectPath, PACKAGES_FOLDER_NAME); - public static readonly string LibraryFolderPath = Path.Combine(ProjectPath, LIBRARY_FOLDER_NAME); - public static readonly string ConfigFolderPath = Path.Combine(ProjectPath, CONFIG_FOLDER_NAME); - - public static readonly string ImportsFolderPath = Path.Combine(LibraryFolderPath, IMPORTS_FOLDER_NAME); + public static string AssetsFolderPath => s_assetsFolderPath; + public static string PackagesFolderPath => s_packagesFolderPath; + public static string LibraryFolderPath => s_libraryFolderPath; + public static string ConfigFolderPath => s_configFolderPath; + public static string CacheFolderPath => s_cacheFolderPath; + public static string LibraryImportsFolderPath => s_libraryImportsFolderPath; public static DispatcherQueue DispatcherQueue { @@ -51,6 +58,22 @@ public static class EditorApplication s_serviceProvider = serviceProvider; s_currentProjectPath = projectPath; s_currentProjectName = projectName; + + s_assetsFolderPath = Path.Combine(projectPath, ASSETS_FOLDER_NAME); + s_packagesFolderPath = Path.Combine(projectPath, PACKAGES_FOLDER_NAME); + s_libraryFolderPath = Path.Combine(projectPath, LIBRARY_FOLDER_NAME); + s_configFolderPath = Path.Combine(projectPath, CONFIG_FOLDER_NAME); + s_cacheFolderPath = Path.Combine(projectPath, CACHE_FOLDER_NAME); + + s_libraryImportsFolderPath = Path.Combine(s_libraryFolderPath, IMPORTS_FOLDER_NAME); + + Directory.CreateDirectory(s_assetsFolderPath); + Directory.CreateDirectory(s_packagesFolderPath); + Directory.CreateDirectory(s_libraryFolderPath); + Directory.CreateDirectory(s_configFolderPath); + Directory.CreateDirectory(s_cacheFolderPath); + + Directory.CreateDirectory(s_libraryImportsFolderPath); } internal static void SetDispatcherQueue(DispatcherQueue dispatcherQueue) @@ -72,4 +95,4 @@ public static class EditorApplication internal static void Shutdown() { } -} \ No newline at end of file +} diff --git a/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs b/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs index 88b0db3..6d93efb 100644 --- a/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs +++ b/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs @@ -1,4 +1,5 @@ using Ghost.Editor.Core.AssetHandler; +using Ghost.Engine; using Microsoft.Data.Sqlite; namespace Ghost.Editor.Core.Services; @@ -10,21 +11,18 @@ namespace Ghost.Editor.Core.Services; internal sealed class AssetCatalog : IDisposable { private readonly SqliteConnection _connection; - private readonly object _writeLock = new(); + private readonly Lock _writeLock = new(); // Prepared statements private readonly SqliteCommand _cmdGetGuid; private readonly SqliteCommand _cmdGetPath; private readonly SqliteCommand _cmdUpsert; private readonly SqliteCommand _cmdDelete; - private readonly SqliteCommand _cmdMarkDirty; - private readonly SqliteCommand _cmdMarkImported; - private readonly SqliteCommand _cmdMarkFailed; + private readonly SqliteCommand _cmdGetHandlerTypeId; private readonly SqliteCommand _cmdGetReferencers; private readonly SqliteCommand _cmdGetDependencies; private readonly SqliteCommand _cmdInsertDep; private readonly SqliteCommand _cmdClearDeps; - private readonly SqliteCommand _cmdGetDirty; private readonly SqliteCommand _cmdEnumerate; public AssetCatalog(string dbPath) @@ -50,6 +48,7 @@ internal sealed class AssetCatalog : IDisposable _cmdGetGuid = CreateCommand("SELECT guid FROM assets WHERE source_path = @path"); _cmdGetPath = CreateCommand("SELECT source_path FROM assets WHERE guid = @guid"); + _cmdGetHandlerTypeId = CreateCommand("SELECT handler_type_id FROM assets WHERE guid = @guid"); _cmdUpsert = CreateCommand(@" INSERT INTO assets (guid, source_path, handler_type_id, handler_version, state) VALUES (@guid, @path, @handler_id, @version, 0) @@ -59,21 +58,10 @@ internal sealed class AssetCatalog : IDisposable handler_version = excluded.handler_version, state = 0;"); _cmdDelete = CreateCommand("DELETE FROM assets WHERE guid = @guid"); - _cmdMarkDirty = CreateCommand("UPDATE assets SET state = 0 WHERE guid = @guid"); - _cmdMarkImported = CreateCommand(@" - UPDATE assets SET - content_hash = @content_hash, - settings_hash = @settings_hash, - imported_at_ms = @time, - state = 1, - error_message = NULL - WHERE guid = @guid"); - _cmdMarkFailed = CreateCommand("UPDATE assets SET state = 2, error_message = @msg WHERE guid = @guid"); _cmdGetReferencers = CreateCommand("SELECT from_guid FROM dependencies WHERE to_guid = @guid"); _cmdGetDependencies = CreateCommand("SELECT to_guid FROM dependencies WHERE from_guid = @guid"); _cmdInsertDep = CreateCommand("INSERT INTO dependencies (from_guid, to_guid) VALUES (@from, @to)"); _cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid"); - _cmdGetDirty = CreateCommand("SELECT guid, source_path FROM assets WHERE state = 0"); _cmdEnumerate = CreateCommand("SELECT guid, source_path FROM assets"); } @@ -96,8 +84,6 @@ internal sealed class AssetCatalog : IDisposable content_hash TEXT, settings_hash TEXT, imported_at_ms INTEGER, - state INTEGER NOT NULL DEFAULT 0, - error_message TEXT ); CREATE UNIQUE INDEX IF NOT EXISTS idx_assets_path ON assets(source_path); @@ -155,38 +141,12 @@ internal sealed class AssetCatalog : IDisposable } } - public void MarkDirty(Guid guid) + public Guid GetHandlerTypeId(Guid guid) { - lock (_writeLock) - { - _cmdMarkDirty.Parameters.Clear(); - _cmdMarkDirty.Parameters.AddWithValue("@guid", guid.ToByteArray()); - _cmdMarkDirty.ExecuteNonQuery(); - } - } - - public void MarkImported(Guid guid, string contentHash, string settingsHash) - { - lock (_writeLock) - { - _cmdMarkImported.Parameters.Clear(); - _cmdMarkImported.Parameters.AddWithValue("@guid", guid.ToByteArray()); - _cmdMarkImported.Parameters.AddWithValue("@content_hash", contentHash); - _cmdMarkImported.Parameters.AddWithValue("@settings_hash", settingsHash); - _cmdMarkImported.Parameters.AddWithValue("@time", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - _cmdMarkImported.ExecuteNonQuery(); - } - } - - public void MarkFailed(Guid guid, string error) - { - lock (_writeLock) - { - _cmdMarkFailed.Parameters.Clear(); - _cmdMarkFailed.Parameters.AddWithValue("@guid", guid.ToByteArray()); - _cmdMarkFailed.Parameters.AddWithValue("@msg", error); - _cmdMarkFailed.ExecuteNonQuery(); - } + _cmdGetHandlerTypeId.Parameters.Clear(); + _cmdGetHandlerTypeId.Parameters.AddWithValue("@guid", guid.ToByteArray()); + var result = _cmdGetHandlerTypeId.ExecuteScalar(); + return result is byte[] bytes ? new Guid(bytes) : Guid.Empty; } public void SetDependencies(Guid assetId, ReadOnlySpan dependencies) @@ -207,6 +167,7 @@ internal sealed class AssetCatalog : IDisposable _cmdInsertDep.Parameters.AddWithValue("@to", dep.ToByteArray()); _cmdInsertDep.ExecuteNonQuery(); } + tx.Commit(); } } @@ -215,12 +176,14 @@ internal sealed class AssetCatalog : IDisposable { _cmdGetReferencers.Parameters.Clear(); _cmdGetReferencers.Parameters.AddWithValue("@guid", guid.ToByteArray()); + using var reader = _cmdGetReferencers.ExecuteReader(); var list = new List(); while (reader.Read()) { list.Add(new Guid((byte[])reader[0])); } + return list; } @@ -228,23 +191,14 @@ internal sealed class AssetCatalog : IDisposable { _cmdGetDependencies.Parameters.Clear(); _cmdGetDependencies.Parameters.AddWithValue("@guid", guid.ToByteArray()); + using var reader = _cmdGetDependencies.ExecuteReader(); var list = new List(); while (reader.Read()) { list.Add(new Guid((byte[])reader[0])); } - return list; - } - public List<(Guid guid, string sourcePath)> GetDirtyAssets() - { - using var reader = _cmdGetDirty.ExecuteReader(); - var list = new List<(Guid guid, string sourcePath)>(); - while (reader.Read()) - { - list.Add((new Guid((byte[])reader[0]), reader.GetString(1))); - } return list; } @@ -263,14 +217,11 @@ internal sealed class AssetCatalog : IDisposable _cmdGetPath.Dispose(); _cmdUpsert.Dispose(); _cmdDelete.Dispose(); - _cmdMarkDirty.Dispose(); - _cmdMarkImported.Dispose(); - _cmdMarkFailed.Dispose(); + _cmdGetHandlerTypeId.Dispose(); _cmdGetReferencers.Dispose(); _cmdGetDependencies.Dispose(); _cmdInsertDep.Dispose(); _cmdClearDeps.Dispose(); - _cmdGetDirty.Dispose(); _cmdEnumerate.Dispose(); _connection.Dispose(); } diff --git a/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs b/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs index 20d38ad..f7b9eec 100644 --- a/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs +++ b/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs @@ -1,7 +1,7 @@ using Ghost.Core; +using Ghost.Core.Utilities; using Ghost.Editor.Core.AssetHandler; using Ghost.Editor.Core.Contracts; -using Ghost.Engine.AssetLoader; using System.Collections.Concurrent; namespace Ghost.Editor.Core.Services; @@ -11,37 +11,34 @@ namespace Ghost.Editor.Core.Services; /// internal sealed class AssetRegistry : IAssetRegistry, IDisposable { - private readonly string _assetsRoot; - private readonly string _libraryRoot; private readonly AssetCatalog _catalog; private readonly ImportCoordinator _importCoordinator; private readonly FileSystemWatcher _watcher; - private readonly ConcurrentDictionary> _loadedAssets; - private readonly SemaphoreSlim _loadLock = new(1, 1); - private readonly ConcurrentDictionary _ignoreMetaWrites = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary> _loadedAssets; + private readonly SemaphoreSlim _loadLock; + + private readonly ConcurrentDictionary _ignoreMetaWrites; + private readonly ConcurrentHashSet _dirtyAssets; public event EventHandler? OnAssetChanged; - public AssetRegistry(string assetsRoot) + public AssetRegistry() { - _assetsRoot = Path.GetFullPath(assetsRoot); - _libraryRoot = Path.Combine(Path.GetDirectoryName(_assetsRoot)!, EditorApplication.LIBRARY_FOLDER_NAME); - - // TODO: This should be handled by EditorApplication. - Directory.CreateDirectory(_assetsRoot); - Directory.CreateDirectory(_libraryRoot); - - var dbPath = Path.Combine(_libraryRoot, "AssetDB.sqlite"); + var dbPath = Path.Combine(EditorApplication.LibraryFolderPath, "AssetDB.sqlite"); _catalog = new AssetCatalog(dbPath); - _importCoordinator = new ImportCoordinator(_catalog, _assetsRoot, _libraryRoot); + _importCoordinator = new ImportCoordinator(_catalog); - _loadedAssets = new ConcurrentDictionary>(); + _loadedAssets = new ConcurrentDictionary>(); + _loadLock = new SemaphoreSlim(1, 1); + + _ignoreMetaWrites = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + _dirtyAssets = new ConcurrentHashSet(); SyncCatalogWithDisk(); - _watcher = new FileSystemWatcher(_assetsRoot) + _watcher = new FileSystemWatcher(EditorApplication.AssetsFolderPath) { IncludeSubdirectories = true, EnableRaisingEvents = true, @@ -52,18 +49,16 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable _watcher.Deleted += OnFileSystemEvent; _watcher.Changed += OnFileSystemEvent; _watcher.Renamed += OnFileSystemRenameEvent; - - _importCoordinator.EnqueueDirtyAssetsAsync().AsTask().Wait(); } private void SyncCatalogWithDisk() { - if (!Directory.Exists(_assetsRoot)) + if (!Directory.Exists(EditorApplication.AssetsFolderPath)) { return; } - var metaFiles = Directory.EnumerateFiles(_assetsRoot, "*.gmeta", SearchOption.AllDirectories); + var metaFiles = Directory.EnumerateFiles(EditorApplication.AssetsFolderPath, "*.gmeta", SearchOption.AllDirectories); var foundGuids = new HashSet(); foreach (var metaPath in metaFiles) @@ -71,8 +66,8 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable var meta = AssetMetaIO.ReadAsync(metaPath).AsTask().Result; if (meta != null) { - var sourceRelative = AssetMetaIO.GetSourcePath(Path.GetRelativePath(_assetsRoot, metaPath)); - _catalog.Upsert(meta, sourceRelative.Replace('\\', '/')); + var sourceRelative = AssetMetaIO.GetSourcePath(Path.GetRelativePath(EditorApplication.AssetsFolderPath, metaPath)); + _catalog.Upsert(meta, sourceRelative.Replace(Path.DirectorySeparatorChar, '/')); foundGuids.Add(meta.Guid); } } @@ -89,7 +84,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable private async void OnFileSystemEvent(object sender, FileSystemEventArgs e) { var ext = Path.GetExtension(e.FullPath); - var relativePath = Path.GetRelativePath(_assetsRoot, e.FullPath).Replace('\\', '/'); + var relativePath = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath).Replace(Path.DirectorySeparatorChar, '/'); if (_ignoreMetaWrites.TryRemove(e.FullPath, out _)) { @@ -101,7 +96,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable return; } - if (ext == ".gmeta") + if (ext == AssetMetaIO.META_EXTENSION) { if (e.ChangeType == WatcherChangeTypes.Changed || e.ChangeType == WatcherChangeTypes.Created) { @@ -131,8 +126,8 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable private void OnFileSystemRenameEvent(object sender, RenamedEventArgs e) { - var oldRelative = Path.GetRelativePath(_assetsRoot, e.OldFullPath).Replace('\\', '/'); - var newRelative = Path.GetRelativePath(_assetsRoot, e.FullPath).Replace('\\', '/'); + var oldRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.OldFullPath).Replace(Path.DirectorySeparatorChar, '/'); + var newRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath).Replace(Path.DirectorySeparatorChar, '/'); var guid = _catalog.GetGuid(oldRelative); if (guid != Guid.Empty) @@ -200,7 +195,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable var ext = Path.GetExtension(sourceFilePath); var relativePath = targetAssetPath.Replace(Path.DirectorySeparatorChar, '/'); - var fullPath = Path.Combine(_assetsRoot, relativePath); + var fullPath = Path.Combine(EditorApplication.AssetsFolderPath, relativePath); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); File.Copy(sourceFilePath, fullPath, true); @@ -220,18 +215,18 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable return Result.Failure("Asset not found"); } - var fullPath = Path.Combine(_assetsRoot, path); + var fullPath = Path.Combine(EditorApplication.AssetsFolderPath, path); var metaPath = AssetMetaIO.GetMetaPath(fullPath); await _importCoordinator.EnqueueAsync(new ImportJob(assetId, path, metaPath, ImportReason.ManualReimport), token); return Result.Success(); } - public async ValueTask> LoadAssetAsync(Guid id, CancellationToken token = default) + public async ValueTask> LoadAssetAsync(Guid id, CancellationToken token = default) { if (_loadedAssets.TryGetValue(id, out var weakRef) && weakRef.TryGetTarget(out var asset)) { - return asset; + return Result.Success(asset); } await _loadLock.WaitAsync(token); @@ -239,24 +234,33 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable { if (_loadedAssets.TryGetValue(id, out weakRef) && weakRef.TryGetTarget(out asset)) { - return asset; + return Result.Success(asset); } - var importedPath = Path.Combine(_libraryRoot, "Imports", $"{id:N}.imported"); - if (!File.Exists(importedPath)) + var path = GetAssetPath(id); + if (!File.Exists(path)) { - return Result.Failure("Asset not imported"); + return Result.Failure("Asset does not exist."); } - // For now, we use a basic LoadAsync implementation. - // In a better design, we'd read the handler ID from the header. - // Here we we assume the catalog is correct (it's synced with gmeta). + var handler = AssetHandlerRegistry.GetByExtension(Path.GetExtension(path)); + if (handler is null) + { + return Result.Failure("No Available handler type."); + } - // Looking up TypeId from catalog isn't implemented in AssetCatalog yet. - // We should add it or use the header. - // The existing Asset class might still be tied to the old binary format. + var meta = await AssetMetaIO.ReadAsync(AssetMetaIO.GetMetaPath(path), token); + if (meta is null) + { + return Result.Failure("Meta file does not exist."); + } - return Result.Failure("Full asset loading would require updating all assets to the new format first."); + await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + return await handler.LoadAssetAsync(stream, id, meta.Settings, token); + } + catch (Exception ex) + { + return Result.Failure(ex.Message); } finally { @@ -264,9 +268,88 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable } } - public ValueTask SaveAssetAsync(Asset asset, CancellationToken token = default) + public async ValueTask SaveAssetAsync(IAsset asset, CancellationToken token = default) { - throw new NotImplementedException(); + try + { + var path = GetAssetPath(asset.ID); + if (!File.Exists(path)) + { + return Result.Failure("Asset does not exist."); + } + + var handler = AssetHandlerRegistry.GetByTypeId(asset.TypeID); + if (handler is null) + { + return Result.Failure("No Avaliable handler type."); + } + + await using var stream = new FileStream(path, FileMode.Open, FileAccess.Write, FileShare.None); + return await handler.SaveAssetAsync(stream, asset, token); + } + catch (Exception ex) + { + return Result.Failure(ex.Message); + } + } + + public async ValueTask SaveAssetAsync(Guid id, CancellationToken token = default) + { + var result = await LoadAssetAsync(id, token); + if (result.IsFailure) + { + return result; + } + + return await SaveAssetAsync(result.Value, token); + } + + public void SetAssetDirty(Guid id) + { + _dirtyAssets.Add(id); + } + + public async ValueTask SaveAssetIfDirtyAsync(IAsset asset, CancellationToken token = default) + { + if (_dirtyAssets.Contains(asset.ID)) + { + var result = await SaveAssetAsync(asset, token); + _dirtyAssets.Remove(asset.ID); + + return result; + } + + return Result.Success(); + } + + public async ValueTask SaveAssetIfDirtyAsync(Guid id, CancellationToken token = default) + { + var result = await LoadAssetAsync(id, token); + if (result.IsFailure) + { + return result; + } + + return await SaveAssetIfDirtyAsync(result.Value, token); + } + + public async ValueTask SaveDirtyAssetsAsync() + { + if (_dirtyAssets.IsEmpty) + { + return Array.Empty(); + } + + var tasks = new Task[_dirtyAssets.Count]; + + var i = 0; + foreach (var id in _dirtyAssets) + { + tasks[i++] = SaveAssetIfDirtyAsync(id).AsTask(); + } + + _dirtyAssets.Clear(); + return await Task.WhenAll(tasks); } public void Dispose() diff --git a/src/Editor/Ghost.Editor.Core/Services/EditorContentProvider.cs b/src/Editor/Ghost.Editor.Core/Services/EditorContentProvider.cs new file mode 100644 index 0000000..f4dddbc --- /dev/null +++ b/src/Editor/Ghost.Editor.Core/Services/EditorContentProvider.cs @@ -0,0 +1,43 @@ +using Ghost.Core; +using Ghost.Editor.Core.AssetHandler; +using Ghost.Engine; + +namespace Ghost.Editor.Core.Services; + +internal class EditorContentProvider : IContentProvider +{ + private readonly AssetCatalog _catalog; + + public EditorContentProvider(AssetCatalog catalog) + { + _catalog = catalog; + } + + public bool HasAsset(Guid guid) + { + return _catalog.GetSourcePath(guid) != null; + } + + public Result OpenRead(Guid guid, CancellationToken token = default) + { + var importedPath = Path.Combine(EditorApplication.LibraryImportsFolderPath, $"{guid:N}{ImportCoordinator.IMPORTED_EXTENSION}"); + if (!File.Exists(importedPath)) + { + return Result.Failure($"Imported asset not found for GUID: {guid}"); + } + + return new FileStream(importedPath, FileMode.Open, FileAccess.Read, FileShare.Read); + } + + public Guid[] GetDependencies(Guid guid) + { + return _catalog.GetDependencies(guid).ToArray(); + } + + public AssetType GetAssetType(Guid guid) + { + var handlerID = _catalog.GetHandlerTypeId(guid); + var handler = AssetHandlerRegistry.GetByTypeId(handlerID); + return handler?.TargetAssetType ?? AssetType.Unknown; + } +} diff --git a/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs b/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs index e79fcb2..ec7515e 100644 --- a/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs +++ b/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs @@ -25,10 +25,11 @@ internal readonly record struct ImportJob( internal sealed class ImportCoordinator : IDisposable { + public const string IMPORTED_EXTENSION_NAME = "Imported"; + public const string IMPORTED_EXTENSION = ".imported"; + private readonly Channel _importChannel; private readonly AssetCatalog _catalog; - private readonly string _assetsRoot; - private readonly string _libraryRoot; private readonly CancellationTokenSource _cts; private readonly Task[] _workers; @@ -36,11 +37,9 @@ internal sealed class ImportCoordinator : IDisposable // For now we just focus on the core logic // public event EventHandler? OnAssetChanged; - public ImportCoordinator(AssetCatalog catalog, string assetsRoot, string libraryRoot, int workerCount = 2) + public ImportCoordinator(AssetCatalog catalog, int workerCount = 2) { _catalog = catalog; - _assetsRoot = assetsRoot; - _libraryRoot = libraryRoot; _cts = new CancellationTokenSource(); _importChannel = Channel.CreateBounded(new BoundedChannelOptions(256) @@ -61,15 +60,6 @@ internal sealed class ImportCoordinator : IDisposable return _importChannel.Writer.WriteAsync(job, token); } - public async ValueTask EnqueueDirtyAssetsAsync(CancellationToken token = default) - { - foreach (var (guid, sourcePath) in _catalog.GetDirtyAssets()) - { - var metaPath = AssetMetaIO.GetMetaPath(Path.Combine(_assetsRoot, sourcePath)); - await EnqueueAsync(new ImportJob(guid, sourcePath, metaPath, ImportReason.Startup), token); - } - } - private async Task WorkerLoop(CancellationToken token) { await foreach (var job in _importChannel.Reader.ReadAllAsync(token)) @@ -80,18 +70,18 @@ internal sealed class ImportCoordinator : IDisposable } catch (Exception ex) { - _catalog.MarkFailed(job.AssetGuid, ex.Message); + Logger.Error(ex); } } } private async ValueTask ProcessImportAsync(ImportJob job, CancellationToken token) { - var fullSourcePath = Path.Combine(_assetsRoot, job.SourcePath); + var fullSourcePath = Path.Combine(EditorApplication.AssetsFolderPath, job.SourcePath); var meta = await AssetMetaIO.ReadAsync(job.MetaPath, token); if (meta is null) { - _catalog.MarkFailed(job.AssetGuid, "Missing .gmeta file"); + Logger.Error("Missing .gmeta file"); return; } @@ -108,17 +98,13 @@ internal sealed class ImportCoordinator : IDisposable meta.SettingsHash == settingsHash && meta.HandlerVersion == AssetHandlerRegistry.GetVersionByTypeId(meta.HandlerTypeId ?? Guid.Empty)) { - _catalog.MarkImported(job.AssetGuid, contentHash, settingsHash); return; } var importResult = Result.Success(); if (handler is IAssetHandler importable) { - // TODO: This should be handled by EditorApplication. - var importsDir = Path.Combine(_libraryRoot, "Imports"); - Directory.CreateDirectory(importsDir); - var targetPath = Path.Combine(importsDir, $"{job.AssetGuid:N}.imported"); + var targetPath = Path.Combine(EditorApplication.LibraryImportsFolderPath, $"{job.AssetGuid:N}{IMPORTED_EXTENSION}"); await using var sourceStream = new FileStream(fullSourcePath, FileMode.Open, FileAccess.Read, FileShare.Read); await using var targetStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None); @@ -134,11 +120,10 @@ internal sealed class ImportCoordinator : IDisposable meta.LastImportedUtc = DateTime.UtcNow; await AssetMetaIO.WriteAsync(job.MetaPath, meta, token); - _catalog.MarkImported(job.AssetGuid, contentHash, settingsHash); } else { - _catalog.MarkFailed(job.AssetGuid, importResult.Message ?? "Unknown import error"); + Logger.Error(importResult.Message ?? "Unknown import error"); } } @@ -146,7 +131,7 @@ internal sealed class ImportCoordinator : IDisposable { if (!File.Exists(filePath)) { - return ""; + return string.Empty; } using var sha = SHA256.Create(); @@ -159,7 +144,7 @@ internal sealed class ImportCoordinator : IDisposable { if (settings is null) { - return ""; + return string.Empty; } var json = JsonSerializer.SerializeToUtf8Bytes(settings); diff --git a/src/Editor/Ghost.Editor/App.xaml b/src/Editor/Ghost.Editor/App.xaml index d5c7231..a329b04 100644 --- a/src/Editor/Ghost.Editor/App.xaml +++ b/src/Editor/Ghost.Editor/App.xaml @@ -10,7 +10,6 @@ - diff --git a/src/Editor/Ghost.Editor/App.xaml.cs b/src/Editor/Ghost.Editor/App.xaml.cs index 7df4fad..3057b51 100644 --- a/src/Editor/Ghost.Editor/App.xaml.cs +++ b/src/Editor/Ghost.Editor/App.xaml.cs @@ -3,11 +3,9 @@ using Ghost.Editor.Core; using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Services; using Ghost.Editor.Core.Utilities; -using Ghost.Editor.Views.Pages.EngineEditor; -using Ghost.Editor.Views.Windows; using Ghost.Editor.ViewModels.Controls; -using Ghost.Editor.ViewModels.Pages.EngineEditor; using Ghost.Editor.ViewModels.Windows; +using Ghost.Editor.Views.Windows; using Ghost.Engine; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -66,7 +64,7 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - // services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); @@ -97,22 +95,6 @@ public partial class App : Application break; } } - - #region Should be deleted - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - #endregion }) .Build(); diff --git a/src/Editor/Ghost.Editor/Ghost.Editor.csproj b/src/Editor/Ghost.Editor/Ghost.Editor.csproj index 66c1f17..7f1dbc3 100644 --- a/src/Editor/Ghost.Editor/Ghost.Editor.csproj +++ b/src/Editor/Ghost.Editor/Ghost.Editor.csproj @@ -198,6 +198,8 @@ + + diff --git a/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/ConsoleViewModel.cs b/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/ConsoleViewModel.cs deleted file mode 100644 index 95c657e..0000000 --- a/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/ConsoleViewModel.cs +++ /dev/null @@ -1,53 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Ghost.Core; -using System.Collections.ObjectModel; - -namespace Ghost.Editor.ViewModels.Pages.EngineEditor; - -internal partial class ConsoleViewModel : ObservableObject -{ - public ReadOnlyObservableCollection Logs; - - [ObservableProperty] - public partial bool ShowInfo - { - get; set; - } = true; - - [ObservableProperty] - public partial bool ShowWarning - { - get; set; - } = true; - - [ObservableProperty] - public partial bool ShowError - { - get; set; - } = true; - - [ObservableProperty] - public partial bool ShowStackTrace - { - get; set; - } = false; - - [ObservableProperty] - public partial LogMessage? SelectedLog - { - get; set; - } - - partial void OnShowStackTraceChanged(bool value) - { - //Logger.HasStackTrace = value; - //Logger.LogInfo($"Stack trace visibility set to {value}."); - } - - [RelayCommand] - private void ClearLogs() - { - //Logger.Clear(); - } -} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/HierarchyViewModel.cs b/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/HierarchyViewModel.cs deleted file mode 100644 index d05145c..0000000 --- a/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/HierarchyViewModel.cs +++ /dev/null @@ -1,36 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using Ghost.Editor.Core.Contracts; - -namespace Ghost.Editor.ViewModels.Pages.EngineEditor; - -internal partial class HierarchyViewModel : ObservableObject, INavigationAware -{ - //[ObservableProperty] - //public partial ObservableCollection SceneList - //{ - // get; - // private set; - //} = new(EditorSceneManager.LoadedWorlds); - - //private void OnWorldLoaded(SceneNode node) - //{ - // SceneList.Add(node); - //} - - //private void OnWorldUnloaded(SceneNode node) - //{ - // SceneList.Remove(node); - //} - - public void OnNavigatedTo(object? parameter) - { - //EditorSceneManager.OnWorldLoaded += OnWorldLoaded; - //EditorSceneManager.OnWorldUnloaded += OnWorldUnloaded; - } - - public void OnNavigatedFrom() - { - //EditorSceneManager.OnWorldLoaded -= OnWorldLoaded; - //EditorSceneManager.OnWorldUnloaded -= OnWorldUnloaded; - } -} diff --git a/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/InspectorViewModel.cs b/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/InspectorViewModel.cs deleted file mode 100644 index a13ab8f..0000000 --- a/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/InspectorViewModel.cs +++ /dev/null @@ -1,31 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using Ghost.Editor.Core.Contracts; - -namespace Ghost.Editor.ViewModels.Pages.EngineEditor; - -internal partial class InspectorViewModel(IInspectorService inspectorService) : ObservableObject, INavigationAware -{ - [ObservableProperty] - public partial IInspectable? Inspectable - { - get; - set; - } - - public void OnNavigatedTo(object? parameter) - { - inspectorService.OnSelectionChanged += OnSelectionChanged; - Inspectable = inspectorService.Selected; - } - - public void OnNavigatedFrom() - { - inspectorService.OnSelectionChanged -= OnSelectionChanged; - Inspectable = null; - } - - private void OnSelectionChanged(object? sender, EventArgs e) - { - Inspectable = inspectorService.Selected; - } -} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs b/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs deleted file mode 100644 index 16da255..0000000 --- a/src/Editor/Ghost.Editor/ViewModels/Pages/EngineEditor/ProjectViewModel.cs +++ /dev/null @@ -1,143 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using Ghost.Editor.Models; -using System.Collections.ObjectModel; - -namespace Ghost.Editor.ViewModels.Pages.EngineEditor; - -internal partial class ProjectViewModel : ObservableObject -{ - // private readonly IAssetService _assetService; - - public ObservableCollection SubDirectories - { - get; - } = new(); - - [ObservableProperty] - public partial ObservableCollection DirectoryAssets - { - get; - set; - } = new(); - - [ObservableProperty] - public partial ExplorerItem? SelectedDirectory - { - get; - set; - } - - [ObservableProperty] - public partial ExplorerItem? SelectedAsset - { - get; - set; - } - - // public ProjectViewModel(IAssetService assetService) - // { - // _assetService = assetService; - - // var assetsRootItem = new ExplorerItem("Assets", Path.Combine(EditorApplication.ProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true); - // LoadSubFolderRecursive(ref assetsRootItem); - - // SubDirectories.Add(assetsRootItem); - // } - - private static void LoadSubFolderRecursive(ref ExplorerItem parentItem) - { - foreach (var directory in Directory.EnumerateDirectories(parentItem.FullName)) - { - var item = new ExplorerItem(Path.GetFileName(directory), directory, true); - LoadSubFolderRecursive(ref item); - - parentItem.Children ??= new(); - parentItem.Children.Add(item); - } - } - - public static Task FindNodeIterative(ExplorerItem root, Func predicate) - { - var stack = new Stack(); - stack.Push(root); - - return Task.Run(() => - { - while (stack.Count > 0) - { - var node = stack.Pop(); - if (predicate(node)) - { - return node; - } - - if (node.Children == null || node.Children.Count == 0) - { - continue; - } - - for (var i = node.Children.Count - 1; i >= 0; i--) - { - stack.Push(node.Children[i]); - } - } - - return null; - }); - } - - private void NavigateToDirectory(string? path) - { - App.Window?.DispatcherQueue.TryEnqueue(async () => - { - DirectoryAssets.Clear(); - - if (!Directory.Exists(path)) - { - return; - } - - foreach (var directory in Directory.EnumerateDirectories(path)) - { - var directoryItem = new ExplorerItem(Path.GetFileName(directory), directory, true); - DirectoryAssets.Add(directoryItem); - } - - foreach (var file in Directory.EnumerateFiles(path)) - { - var fileItem = new ExplorerItem(Path.GetFileName(file), file, false); - DirectoryAssets.Add(fileItem); - } - - SelectedDirectory = await FindNodeIterative(SubDirectories[0], x => x.FullName == path); - }); - } - - public void OpenSelected() - { - if (SelectedAsset == null) - { - return; - } - - if (SelectedAsset.IsDirectory) - { - NavigateToDirectory(SelectedAsset.FullName); - } - else - { - // _assetService.OpenAsset(SelectedAsset.FullName); - } - } - - partial void OnSelectedDirectoryChanged(ExplorerItem? value) - { - if (value == null) - { - return; - } - - DirectoryAssets.Clear(); - NavigateToDirectory(value.FullName); - } -} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor/Views/Controls/ProjectBrowser.Menu.cs b/src/Editor/Ghost.Editor/Views/Controls/ContentBrowser.Menu.cs similarity index 97% rename from src/Editor/Ghost.Editor/Views/Controls/ProjectBrowser.Menu.cs rename to src/Editor/Ghost.Editor/Views/Controls/ContentBrowser.Menu.cs index 333e8bf..0414064 100644 --- a/src/Editor/Ghost.Editor/Views/Controls/ProjectBrowser.Menu.cs +++ b/src/Editor/Ghost.Editor/Views/Controls/ContentBrowser.Menu.cs @@ -2,7 +2,7 @@ using Ghost.Editor.Core; namespace Ghost.Editor.Views.Controls; -internal partial class ProjectBrowser +internal partial class ContentBrowser { [ContextMenuItem("project-browser", "Show in Explorer")] private static void ShowInExplorer() diff --git a/src/Editor/Ghost.Editor/Views/Controls/ProjectBrowser.xaml b/src/Editor/Ghost.Editor/Views/Controls/ContentBrowser.xaml similarity index 99% rename from src/Editor/Ghost.Editor/Views/Controls/ProjectBrowser.xaml rename to src/Editor/Ghost.Editor/Views/Controls/ContentBrowser.xaml index 490118a..6e3f050f 100644 --- a/src/Editor/Ghost.Editor/Views/Controls/ProjectBrowser.xaml +++ b/src/Editor/Ghost.Editor/Views/Controls/ContentBrowser.xaml @@ -1,6 +1,6 @@ (); ViewModel = App.GetService(); diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockContainer.cs b/src/Editor/Ghost.Editor/Views/Controls/Docking/DockContainer.cs deleted file mode 100644 index 35a431b..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockContainer.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.Collections.ObjectModel; - -namespace Ghost.Editor.Views.Controls.Docking; - -/// -/// Base class for containers that can hold other dock modules. -/// -public abstract class DockContainer : DockModule -{ - private readonly ObservableCollection _children = new(); - private bool _isCleaningUp; - /// - /// Gets the collection of child modules. - /// - public ReadOnlyObservableCollection Children { get; } - - protected DockContainer() - { - Children = new ReadOnlyObservableCollection(_children); - _children.CollectionChanged += OnChildrenChanged; - } - - private void OnChildrenChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) - { - OnChildrenUpdated(); - } - - /// - /// Adds a child module to the end of the container. - /// - /// The module to add. - public virtual void AddChild(DockModule module) - { - InsertChild(_children.Count, module); - } - - /// - /// Inserts a child module at the specified index. - /// - /// - /// This method does not support reordering existing children within the same container. - /// Cross-layout moves are intentionally allowed and supported (e.g., for dragging tabs between floating windows and the main window). - /// - /// The zero-based index at which the module should be inserted. - /// The module to insert. - public virtual void InsertChild(int index, DockModule module) - { - ValidateChild(module); - - if (module.Owner == null && module.Root != null && module.Root != this.Root) - throw new InvalidOperationException("Cannot insert a module that is the root of another layout. Detach it first."); - - if (index < 0 || index > _children.Count) - throw new ArgumentOutOfRangeException(nameof(index)); - - if (_children.Contains(module)) - return; - - module.Owner?.RemoveChild(module); - - module.Owner = this; - module.Root = Root; - _children.Insert(index, module); - } - - /// - /// Removes a child module from the container. - /// - /// The module to remove. - public virtual void RemoveChild(DockModule module) - { - RemoveChildInternal(module, true); - } - - internal void RemoveChildInternal(DockModule module, bool triggerCleanup) - { - ArgumentNullException.ThrowIfNull(module); - - if (_children.Remove(module)) - { - module.Owner = null; - module.Root = null; - if (!_isCleaningUp && triggerCleanup) - { - CheckCleanup(); - } - } - } - - /// - /// Replaces an existing child module with a new one. - /// - /// - /// Cross-layout moves are intentionally allowed and supported (e.g., for dragging tabs between floating windows and the main window). - /// - /// The child module to be replaced. - /// The new child module to insert. - public virtual void ReplaceChild(DockModule oldChild, DockModule newChild) - { - ArgumentNullException.ThrowIfNull(oldChild); - ValidateChild(newChild); - - if (newChild.Owner == null && newChild.Root != null && newChild.Root != this.Root) - throw new InvalidOperationException("Cannot insert a module that is the root of another layout. Detach it first."); - - if (oldChild == newChild) return; - - int index = _children.IndexOf(oldChild); - if (index < 0) throw new ArgumentException("oldChild not found in this container", nameof(oldChild)); - - // Detach newChild from its current owner if any - if (newChild.Owner == this) - { - throw new ArgumentException("newChild is already in this container", nameof(newChild)); - } - - var oldOwner = newChild.Owner; - newChild.Owner?.RemoveChildInternal(newChild, false); - - // Remove oldChild without triggering cleanup - _isCleaningUp = true; - try - { - _children.RemoveAt(index); - oldChild.Owner = null; - oldChild.Root = null; - - newChild.Owner = this; - newChild.Root = Root; - _children.Insert(index, newChild); - } - finally - { - _isCleaningUp = false; - } - - CheckCleanup(); - oldOwner?.CheckCleanup(); - } - - /// - /// Checks if the container is empty and removes it from its owner if necessary. - /// - internal virtual void CheckCleanup() - { - if (Children.Count == 0) - { - if (Owner != null) - { - Owner.RemoveChildInternal(this, true); - } - else if (Root != null && Root.RootModule == this) - { - var root = Root; - root.RootModule = null; - root.NotifyLayoutEmpty(); - } - } - } - - /// - /// Validates if a module can be added as a child to this container. - /// - /// The module to validate. - protected virtual void ValidateChild(DockModule module) - { - ArgumentNullException.ThrowIfNull(module); - - if (module == this) - throw new ArgumentException("Cannot add a container to itself.", nameof(module)); - - if (module is DockContainer container) - { - var current = Owner; - while (current != null) - { - if (current == container) - throw new ArgumentException("Cannot add a container that is an ancestor of this container.", nameof(module)); - current = current.Owner; - } - } - } - - /// - /// Removes all child modules from the container. - /// - public void Clear() - { - foreach (var child in _children) - { - child.Owner = null; - child.Root = null; - } - _children.Clear(); - if (!_isCleaningUp) - { - CheckCleanup(); - } - } - - protected override void OnRootChanged() - { - foreach (var child in _children) - { - child.Root = Root; - } - } - - protected virtual void OnChildrenUpdated() { } -} diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockDocument.cs b/src/Editor/Ghost.Editor/Views/Controls/Docking/DockDocument.cs deleted file mode 100644 index a3cf987..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockDocument.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.Views.Controls.Docking; - -/// -/// Represents a document module in the docking system. -/// -public partial class DockDocument : DockModule -{ - public static readonly DependencyProperty TitleProperty = DependencyProperty.Register( - nameof(Title), typeof(string), typeof(DockDocument), new PropertyMetadata(string.Empty)); - - public static readonly DependencyProperty ContentProperty = DependencyProperty.Register( - nameof(Content), typeof(object), typeof(DockDocument), new PropertyMetadata(null)); - - /// - /// Gets or sets the title of the document. - /// - public string? Title - { - get => (string?)GetValue(TitleProperty); - set => SetValue(TitleProperty, value); - } - - /// - /// Gets or sets the content of the document. - /// - public object? Content - { - get => GetValue(ContentProperty); - set => SetValue(ContentProperty, value); - } - - /// - /// Initializes a new instance of the class. - /// - public DockDocument() - { - DefaultStyleKey = typeof(DockDocument); - } -} diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockDocument.xaml b/src/Editor/Ghost.Editor/Views/Controls/Docking/DockDocument.xaml deleted file mode 100644 index d77cf47..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockDocument.xaml +++ /dev/null @@ -1,22 +0,0 @@ - - - - diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockGroup.cs b/src/Editor/Ghost.Editor/Views/Controls/Docking/DockGroup.cs deleted file mode 100644 index 0b07835..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockGroup.cs +++ /dev/null @@ -1,182 +0,0 @@ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Data; - -namespace Ghost.Editor.Views.Controls.Docking; - -/// -/// A container that displays its children (documents) as tabs. -/// -[TemplatePart(Name = PART_TAB_VIEW, Type = typeof(TabView))] -public partial class DockGroup : DockContainer -{ - private const string PART_TAB_VIEW = "PART_TabView"; - private const string DRAG_DOCUMENT_KEY = "DockDocument"; - private TabView? _tabView; - - public DockGroup() - { - DefaultStyleKey = typeof(DockGroup); - } - - protected override void ValidateChild(DockModule module) - { - base.ValidateChild(module); - - if (module is not DockDocument) - { - throw new ArgumentException($"{nameof(DockGroup)} only accepts {nameof(DockDocument)} children.", nameof(module)); - } - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - if (_tabView != null) - { - _tabView.TabDragStarting -= OnTabDragStarting; - _tabView.TabDroppedOutside -= OnTabDroppedOutside; - _tabView.DragOver -= OnDragOver; - _tabView.Drop -= OnDrop; - _tabView.DragLeave -= OnDragLeave; - _tabView.TabCloseRequested -= OnTabCloseRequested; - } - - _tabView = GetTemplateChild(PART_TAB_VIEW) as TabView; - - if (_tabView != null) - { - _tabView.TabDragStarting += OnTabDragStarting; - _tabView.TabDroppedOutside += OnTabDroppedOutside; - _tabView.DragOver += OnDragOver; - _tabView.Drop += OnDrop; - _tabView.DragLeave += OnDragLeave; - _tabView.TabCloseRequested += OnTabCloseRequested; - } - - UpdateTabs(); - } - - private void OnTabDragStarting(TabView sender, TabViewTabDragStartingEventArgs args) - { - if (args.Tab.Tag is DockDocument doc) - { - args.Data.Properties.Add(DRAG_DOCUMENT_KEY, doc); - } - } - - private void OnTabDroppedOutside(TabView sender, TabViewTabDroppedOutsideEventArgs args) - { - if (args.Tab.Tag is DockDocument doc) - { - Root?.CreateFloatingWindow(doc); - } - } - - private void OnDragOver(object sender, DragEventArgs e) - { - if (e.DataView.Properties.ContainsKey(DRAG_DOCUMENT_KEY)) - { - e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; - Root?.ShowHighlight(this, e.GetPosition(this)); - } - } - - private void OnDrop(object sender, DragEventArgs e) - { - if (e.DataView.Properties.TryGetValue(DRAG_DOCUMENT_KEY, out var obj) && obj is DockDocument doc) - { - Root?.HandleDrop(doc, this, e.GetPosition(this)); - } - } - - private void OnDragLeave(object sender, DragEventArgs e) - { - Root?.HideHighlight(); - } - - private void OnTabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args) - { - if (args.Tab.Tag is DockDocument doc) - { - RemoveChild(doc); - } - } - - protected override void OnChildrenUpdated() - { - UpdateTabs(); - } - - private void UpdateTabs() - { - if (_tabView == null || Root == null) return; - - var selectedDoc = _tabView.SelectedItem is TabViewItem selectedItem ? selectedItem.Tag as DockDocument : null; - - // Remove tabs that are no longer in Children - for (int i = _tabView.TabItems.Count - 1; i >= 0; i--) - { - if (_tabView.TabItems[i] is TabViewItem tabItem && tabItem.Tag is DockDocument doc) - { - if (!Children.Contains(doc)) - { - tabItem.Content = null; - _tabView.TabItems.RemoveAt(i); - } - } - } - - // Add new tabs that aren't in TabItems yet, and ensure correct order - for (int i = 0; i < Children.Count; i++) - { - if (Children[i] is DockDocument doc) - { - TabViewItem? existingTab = null; - for (int j = 0; j < _tabView.TabItems.Count; j++) - { - if (_tabView.TabItems[j] is TabViewItem tabItem && tabItem.Tag == doc) - { - existingTab = tabItem; - // Fix order if necessary - if (j != i) - { - _tabView.TabItems.RemoveAt(j); - _tabView.TabItems.Insert(i, existingTab); - } - break; - } - } - - if (existingTab == null) - { - existingTab = new TabViewItem - { - Tag = doc, - Content = doc - }; - - existingTab.SetBinding(TabViewItem.HeaderProperty, new Binding - { - Source = doc, - Path = new PropertyPath(nameof(DockDocument.Title)), - Mode = Microsoft.UI.Xaml.Data.BindingMode.OneWay - }); - - _tabView.TabItems.Insert(i, existingTab); - } - } - } - - // Restore selection - if (selectedDoc != null && _tabView.TabItems.FirstOrDefault(t => t is TabViewItem item && item.Tag == selectedDoc) is TabViewItem newSelected) - { - _tabView.SelectedItem = newSelected; - } - else if (_tabView.TabItems.Count > 0) - { - _tabView.SelectedItem = _tabView.TabItems[0]; - } - } -} diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockGroup.xaml b/src/Editor/Ghost.Editor/Views/Controls/Docking/DockGroup.xaml deleted file mode 100644 index ff8b79a..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockGroup.xaml +++ /dev/null @@ -1,23 +0,0 @@ - - - - diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockModule.cs b/src/Editor/Ghost.Editor/Views/Controls/Docking/DockModule.cs deleted file mode 100644 index 92568a1..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockModule.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.Views.Controls.Docking; - -/// -/// Base class for all dockable modules in the docking system. -/// -public abstract class DockModule : Control -{ - /// - /// Gets the container that owns this module. - /// - public DockContainer? Owner { get; internal set; } - - /// - /// Gets or sets the proportional length (star weight) of this module within its parent panel. - /// - public double DockLength { get; set; } = 1.0; - - private DockingLayout? _root; - - /// - /// Gets or sets the root docking layout this module belongs to. - /// - public virtual DockingLayout? Root - { - get => _root; - internal set - { - if (_root != value) - { - _root = value; - OnRootChanged(); - } - } - } - - protected virtual void OnRootChanged() { } - - /// - /// Detaches this module from its current owner. - /// - public void Detach() - { - Owner?.RemoveChild(this); - } -} diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockPanel.cs b/src/Editor/Ghost.Editor/Views/Controls/Docking/DockPanel.cs deleted file mode 100644 index f0f270f..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockPanel.cs +++ /dev/null @@ -1,231 +0,0 @@ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using CommunityToolkit.WinUI.Controls; -using Windows.Foundation; - -namespace Ghost.Editor.Views.Controls.Docking; - -/// -/// A container that can host multiple dock modules with splitters. -/// -[TemplatePart(Name = PART_GRID, Type = typeof(Grid))] -public partial class DockPanel : DockContainer -{ - private const string PART_GRID = "PART_Grid"; - private const double SPLITTER_THICKNESS = 4; - - public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( - nameof(Orientation), typeof(Orientation), typeof(DockPanel), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged)); - - /// - /// Gets or sets the orientation of the panel. - /// - public Orientation Orientation - { - get => (Orientation)GetValue(OrientationProperty); - set => SetValue(OrientationProperty, value); - } - - private Grid? _grid; - - public DockPanel() - { - DefaultStyleKey = typeof(DockPanel); - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - _grid = GetTemplateChild(PART_GRID) as Grid; - UpdateLayoutStructure(); - } - - protected override void OnChildrenUpdated() - { - UpdateLayoutStructure(); - } - - internal void SyncLengths() - { - if (_grid == null) return; - if (Orientation == Orientation.Horizontal) - { - for (int i = 0; i < Children.Count; i++) - { - int col = i * 2; - if (col < _grid.ColumnDefinitions.Count) - { - Children[i].DockLength = _grid.ColumnDefinitions[col].Width.Value; - } - } - } - else - { - for (int i = 0; i < Children.Count; i++) - { - int row = i * 2; - if (row < _grid.RowDefinitions.Count) - { - Children[i].DockLength = _grid.RowDefinitions[row].Height.Value; - } - } - } - } - - internal override void CheckCleanup() - { - base.CheckCleanup(); - - if (Children.Count == 1) - { - var child = Children[0]; - var owner = Owner; - - if (owner != null) - { - owner.ReplaceChild(this, child); - } - else if (Root != null && Root.RootModule == this) - { - RemoveChildInternal(child, false); - Root.RootModule = child; - } - } - } - - private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - ((DockPanel)d).UpdateLayoutStructure(); - } - - private void UpdateLayoutStructure() - { - if (_grid == null) return; - - // Remove splitters and children that are no longer in the collection - for (int i = _grid.Children.Count - 1; i >= 0; i--) - { - var child = _grid.Children[i]; - if (child is GridSplitter) - { - _grid.Children.RemoveAt(i); - } - else if (child is DockModule module && !Children.Contains(module)) - { - _grid.Children.RemoveAt(i); - } - } - - if (Children.Count == 0) - { - _grid.RowDefinitions.Clear(); - _grid.ColumnDefinitions.Clear(); - return; - } - - if (Orientation == Orientation.Horizontal) - { - _grid.RowDefinitions.Clear(); - - int requiredColumns = (Children.Count * 2) - 1; - while (_grid.ColumnDefinitions.Count > requiredColumns) - { - _grid.ColumnDefinitions.RemoveAt(_grid.ColumnDefinitions.Count - 1); - } - - for (var i = 0; i < Children.Count; i++) - { - int columnIndex = i * 2; - if (columnIndex >= _grid.ColumnDefinitions.Count) - { - _grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(Children[i].DockLength, GridUnitType.Star), MinWidth = 250 }); - } - else - { - _grid.ColumnDefinitions[columnIndex].Width = new GridLength(Children[i].DockLength, GridUnitType.Star); - _grid.ColumnDefinitions[columnIndex].MinWidth = 250; - } - - var child = Children[i]; - if (!_grid.Children.Contains(child)) - { - _grid.Children.Add(child); - } - - Grid.SetColumn(child, columnIndex); - Grid.SetRow(child, 0); - - if (i < Children.Count - 1) - { - int splitterIndex = columnIndex + 1; - if (splitterIndex >= _grid.ColumnDefinitions.Count) - { - _grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - } - else - { - _grid.ColumnDefinitions[splitterIndex].Width = GridLength.Auto; - } - - var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Columns, Width = SPLITTER_THICKNESS }; - Grid.SetColumn(splitter, splitterIndex); - Grid.SetRow(splitter, 0); - _grid.Children.Add(splitter); - } - } - } - else - { - _grid.ColumnDefinitions.Clear(); - - int requiredRows = (Children.Count * 2) - 1; - while (_grid.RowDefinitions.Count > requiredRows) - { - _grid.RowDefinitions.RemoveAt(_grid.RowDefinitions.Count - 1); - } - - for (var i = 0; i < Children.Count; i++) - { - int rowIndex = i * 2; - if (rowIndex >= _grid.RowDefinitions.Count) - { - _grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(Children[i].DockLength, GridUnitType.Star), MinHeight = 250 }); - } - else - { - _grid.RowDefinitions[rowIndex].Height = new GridLength(Children[i].DockLength, GridUnitType.Star); - _grid.RowDefinitions[rowIndex].MinHeight = 250; - } - - var child = Children[i]; - if (!_grid.Children.Contains(child)) - { - _grid.Children.Add(child); - } - - Grid.SetRow(child, rowIndex); - Grid.SetColumn(child, 0); - - if (i < Children.Count - 1) - { - int splitterIndex = rowIndex + 1; - if (splitterIndex >= _grid.RowDefinitions.Count) - { - _grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); - } - else - { - _grid.RowDefinitions[splitterIndex].Height = GridLength.Auto; - } - - var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Rows, Height = SPLITTER_THICKNESS }; - Grid.SetRow(splitter, splitterIndex); - Grid.SetColumn(splitter, 0); - _grid.Children.Add(splitter); - } - } - } - - UpdateLayout(); - } -} diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockPanel.xaml b/src/Editor/Ghost.Editor/Views/Controls/Docking/DockPanel.xaml deleted file mode 100644 index 5cf7f98..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockPanel.xaml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockRegionHighlight.cs b/src/Editor/Ghost.Editor/Views/Controls/Docking/DockRegionHighlight.cs deleted file mode 100644 index 6e2446d..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockRegionHighlight.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.Views.Controls.Docking; - -/// -/// Represents a visual highlight for a docking region. -/// -public partial class DockRegionHighlight : Control -{ - public DockRegionHighlight() - { - DefaultStyleKey = typeof(DockRegionHighlight); - IsHitTestVisible = false; - } -} diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockRegionHighlight.xaml b/src/Editor/Ghost.Editor/Views/Controls/Docking/DockRegionHighlight.xaml deleted file mode 100644 index f82ad5c..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockRegionHighlight.xaml +++ /dev/null @@ -1,20 +0,0 @@ - - - - diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockingLayout.cs b/src/Editor/Ghost.Editor/Views/Controls/Docking/DockingLayout.cs deleted file mode 100644 index 6d5d830..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockingLayout.cs +++ /dev/null @@ -1,344 +0,0 @@ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.Views.Controls.Docking; - -/// -/// The root control for the docking system layout. -/// -[TemplatePart(Name = PART_OVERLAY_CANVAS, Type = typeof(Canvas))] -[TemplatePart(Name = PART_HIGHLIGHT, Type = typeof(DockRegionHighlight))] -public partial class DockingLayout : Control -{ - private const string PART_OVERLAY_CANVAS = "PART_OverlayCanvas"; - private const string PART_HIGHLIGHT = "PART_Highlight"; - - /// - /// Gets or sets the root module of the docking layout. - /// - public static readonly DependencyProperty RootModuleProperty = DependencyProperty.Register( - nameof(RootModule), typeof(DockModule), typeof(DockingLayout), new PropertyMetadata(null, OnRootModuleChanged)); - - /// - /// Gets or sets the root module of the docking layout. - /// - public DockModule? RootModule - { - get => (DockModule?)GetValue(RootModuleProperty); - set => SetValue(RootModuleProperty, value); - } - - /// - /// Occurs when the layout becomes empty. - /// - public event EventHandler? LayoutEmpty; - - internal void NotifyLayoutEmpty() => LayoutEmpty?.Invoke(this, EventArgs.Empty); - - private Canvas? _overlayCanvas; - private DockRegionHighlight? _highlight; - private readonly List _floatingWindows = new(); - - /// - /// Initializes a new instance of the class. - /// - public DockingLayout() - { - DefaultStyleKey = typeof(DockingLayout); - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - _overlayCanvas = GetTemplateChild(PART_OVERLAY_CANVAS) as Canvas; - _highlight = GetTemplateChild(PART_HIGHLIGHT) as DockRegionHighlight; - } - - private static void OnRootModuleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is DockingLayout layout) - { - if (e.OldValue is DockModule oldModule) - { - oldModule.Root = null; - } - - if (e.NewValue is DockModule newModule) - { - if (newModule.Root != null && newModule.Root != layout) - { - throw new InvalidOperationException("Module is already owned by another DockingLayout"); - } - - if (newModule.Owner != null) - { - newModule.Owner.RemoveChild(newModule); - } - - newModule.Root = layout; - } - } - } - - /// - /// Adds a document to the docking layout. - /// - /// The document to add. - /// The docking target position. - /// The target group to add the document to. If null, a suitable group will be found or created. - public void AddDocument(DockDocument document, DockTarget target, DockGroup? targetGroup = null) - { - ArgumentNullException.ThrowIfNull(document); - - if (targetGroup != null && targetGroup.Root != this) - { - throw new ArgumentException("targetGroup does not belong to this DockingLayout", nameof(targetGroup)); - } - - if (targetGroup == null) - { - if (RootModule != null) - { - targetGroup = FindFirstDockGroup(RootModule as DockContainer); - if (targetGroup == null) - { - // Root is not a container, or contains no pGroups. Wrap it. - var newGroup = new DockGroup(); - newGroup.AddChild(document); - - if (RootModule is DockDocument existingDoc) - { - RootModule = null; - newGroup.AddChild(existingDoc); - RootModule = newGroup; - } - else - { - var oldRoot = RootModule; - RootModule = null; - var panel = new DockPanel(); - panel.AddChild(oldRoot); - panel.AddChild(newGroup); - RootModule = panel; - } - targetGroup = newGroup; - } - } - else - { - targetGroup = new DockGroup(); - RootModule = targetGroup; - } - } - - if (target == DockTarget.Center || targetGroup.Children.Count == 0) - { - targetGroup.AddChild(document); - } - else - { - SplitGroup(targetGroup, document, target); - } - } - - private void SplitGroup(DockGroup targetGroup, DockDocument doc, DockTarget target) - { - var parentPanel = targetGroup.Owner as DockPanel; - parentPanel?.SyncLengths(); // Sync before modifying! - - var newGroup = new DockGroup(); - newGroup.AddChild(doc); - - var orientation = (target == DockTarget.Left || target == DockTarget.Right) ? Orientation.Horizontal : Orientation.Vertical; - - if (parentPanel == null) - { - // targetGroup is the RootModule - var newPanel = new DockPanel { Orientation = orientation }; - RootModule = newPanel; - - targetGroup.DockLength = 1.0; - newGroup.DockLength = 1.0; - - if (target == DockTarget.Left || target == DockTarget.Top) - { - newPanel.AddChild(newGroup); - newPanel.AddChild(targetGroup); - } - else - { - newPanel.AddChild(targetGroup); - newPanel.AddChild(newGroup); - } - return; - } - - int index = parentPanel.Children.IndexOf(targetGroup); - - if (index < 0) - { - throw new InvalidOperationException("targetGroup not found in parentPanel"); - } - - if (parentPanel.Orientation == orientation) - { - // Splitting in the same orientation. Share the length. - double halfLength = targetGroup.DockLength / 2.0; - targetGroup.DockLength = halfLength; - newGroup.DockLength = halfLength; - - if (target == DockTarget.Left || target == DockTarget.Top) - { - parentPanel.InsertChild(index, newGroup); - } - else - { - parentPanel.InsertChild(index + 1, newGroup); - } - } - else - { - // Splitting in opposite orientation. New panel takes the full length. - var newPanel = new DockPanel { Orientation = orientation }; - newPanel.DockLength = targetGroup.DockLength; - - targetGroup.DockLength = 1.0; - newGroup.DockLength = 1.0; - - parentPanel.ReplaceChild(targetGroup, newPanel); - - if (target == DockTarget.Left || target == DockTarget.Top) - { - newPanel.AddChild(newGroup); - newPanel.AddChild(targetGroup); - } - else - { - newPanel.AddChild(targetGroup); - newPanel.AddChild(newGroup); - } - } - } - - private static DockGroup? FindFirstDockGroup(DockContainer? container) - { - if (container == null) return null; - - if (container is DockGroup group) - { - return group; - } - - foreach (var child in container.Children) - { - if (child is DockContainer childContainer) - { - var result = FindFirstDockGroup(childContainer); - if (result != null) - { - return result; - } - } - } - - return null; - } - - internal void ShowHighlight(DockGroup targetGroup, global::Windows.Foundation.Point position) - { - if (_highlight == null || _overlayCanvas == null) return; - - _highlight.Visibility = Visibility.Visible; - var target = CalculateDockTarget(targetGroup, position); - - // Calculate rect based on target - double width = targetGroup.ActualWidth; - double height = targetGroup.ActualHeight; - double x = 0, y = 0; - - switch (target) - { - case DockTarget.Left: width /= 2; break; - case DockTarget.Right: width /= 2; x = width; break; - case DockTarget.Top: height /= 2; break; - case DockTarget.Bottom: height /= 2; y = height; break; - case DockTarget.Center: break; - } - - var transform = targetGroup.TransformToVisual(_overlayCanvas); - var point = transform.TransformPoint(new global::Windows.Foundation.Point(x, y)); - - Microsoft.UI.Xaml.Controls.Canvas.SetLeft(_highlight, point.X); - Microsoft.UI.Xaml.Controls.Canvas.SetTop(_highlight, point.Y); - _highlight.Width = width; - _highlight.Height = height; - } - - internal void HideHighlight() - { - if (_highlight != null) _highlight.Visibility = Visibility.Collapsed; - } - - internal void HandleDrop(DockDocument doc, DockGroup targetGroup, global::Windows.Foundation.Point position) - { - HideHighlight(); - var target = CalculateDockTarget(targetGroup, position); - - var oldOwner = doc.Owner as DockContainer; - oldOwner?.RemoveChildInternal(doc, false); - - if (target == DockTarget.Center) - { - if (doc.Owner == targetGroup) return; - targetGroup.AddChild(doc); - } - else - { - if (doc.Owner == targetGroup && targetGroup.Children.Count == 1) return; - SplitGroup(targetGroup, doc, target); - } - - if (oldOwner != null) - { - DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () => - { - oldOwner.CheckCleanup(); - }); - } - } - - private DockTarget CalculateDockTarget(DockGroup group, global::Windows.Foundation.Point position) - { - double w = group.ActualWidth; - double h = group.ActualHeight; - double x = position.X; - double y = position.Y; - - if (x < w * 0.25) return DockTarget.Left; - if (x > w * 0.75) return DockTarget.Right; - if (y < h * 0.25) return DockTarget.Top; - if (y > h * 0.75) return DockTarget.Bottom; - return DockTarget.Center; - } - - internal void CreateFloatingWindow(DockDocument doc) - { - ArgumentNullException.ThrowIfNull(doc); - - var oldOwner = doc.Owner as DockContainer; - oldOwner?.RemoveChildInternal(doc, false); - - var window = new FloatingWindow(doc); - _floatingWindows.Add(window); - window.Closed += (s, e) => _floatingWindows.Remove(window); - window.Activate(); - - if (oldOwner != null) - { - DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () => - { - oldOwner.CheckCleanup(); - }); - } - } -} diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockingLayout.xaml b/src/Editor/Ghost.Editor/Views/Controls/Docking/DockingLayout.xaml deleted file mode 100644 index cf8a0e1..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/DockingLayout.xaml +++ /dev/null @@ -1,20 +0,0 @@ - - - - diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/Enums.cs b/src/Editor/Ghost.Editor/Views/Controls/Docking/Enums.cs deleted file mode 100644 index 58eba45..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/Enums.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Ghost.Editor.Views.Controls.Docking; - -public enum DockTarget -{ - Center, - Left, - Right, - Top, - Bottom -} diff --git a/src/Editor/Ghost.Editor/Views/Controls/Docking/FloatingWindow.cs b/src/Editor/Ghost.Editor/Views/Controls/Docking/FloatingWindow.cs deleted file mode 100644 index a5e00cb..0000000 --- a/src/Editor/Ghost.Editor/Views/Controls/Docking/FloatingWindow.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.UI.Xaml; - -namespace Ghost.Editor.Views.Controls.Docking; - -/// -/// A floating window that contains a docking layout. -/// -public class FloatingWindow : Window -{ - private readonly DockingLayout _layout; - - /// - /// Initializes a new instance of the class with the specified document. - /// - /// The document to display in the floating window. - public FloatingWindow(DockDocument document) - { - ArgumentNullException.ThrowIfNull(document); - - _layout = new DockingLayout(); - var group = new DockGroup(); - group.AddChild(document); - - _layout.RootModule = group; - _layout.LayoutEmpty += (s, e) => Close(); - - Content = _layout; - - // Basic window setup - AppWindow.Resize(new global::Windows.Graphics.SizeInt32(800, 600)); - - // When the user manually closes the window, ensure we clean up the documents inside - this.Closed += FloatingWindow_Closed; - } - - private void FloatingWindow_Closed(object sender, WindowEventArgs args) - { - // Force cleanup of the visual tree so we don't leak anything from this window - _layout.RootModule = null; - Content = null; - } -} diff --git a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ConsolePage.xaml b/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ConsolePage.xaml deleted file mode 100644 index 73127e6..0000000 --- a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ConsolePage.xaml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ConsolePage.xaml.cs b/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ConsolePage.xaml.cs deleted file mode 100644 index f11c9cf..0000000 --- a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ConsolePage.xaml.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Ghost.Editor.ViewModels.Pages.EngineEditor; -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.Views.Pages.EngineEditor; - -internal sealed partial class ConsolePage : Page -{ - public ConsoleViewModel ViewModel - { - get; - } - - public ConsolePage() - { - ViewModel = App.GetService(); - - InitializeComponent(); - } -} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/HierarchyPage.xaml b/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/HierarchyPage.xaml deleted file mode 100644 index 1ffeac0..0000000 --- a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/HierarchyPage.xaml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/HierarchyPage.xaml.cs b/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/HierarchyPage.xaml.cs deleted file mode 100644 index ac4ca51..0000000 --- a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/HierarchyPage.xaml.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Ghost.Editor.Controls; -using Ghost.Editor.Core.Contracts; -using Ghost.Editor.Core.SceneGraph; -using Ghost.Editor.ViewModels.Pages.EngineEditor; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.Views.Pages.EngineEditor; - -internal sealed partial class HierarchyPage : NavigationTabPage -{ - private readonly IInspectorService _inspectorService; - - public HierarchyViewModel ViewModel - { - get; - } - - public HierarchyPage() - { - _inspectorService = App.GetService(); - ViewModel = App.GetService(); - - InitializeComponent(); - } - - public override void OnNavigatedTo(object? parameter) - { - ViewModel.OnNavigatedTo(parameter); - } - - public override void OnNavigatedFrom() - { - ViewModel.OnNavigatedFrom(); - } - - private void TreeView_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args) - { - if (args.AddedItems.Count > 0 && args.AddedItems[0] is IInspectable inspectable) - { - _inspectorService.SetSelected(inspectable, ViewModel); - } - else - { - _inspectorService.SetSelected(null, ViewModel); - } - } -} - -internal partial class HierarchyTemplateSector : DataTemplateSelector -{ - protected override DataTemplate SelectTemplateCore(object item) - { - if (item is not SceneGraphNode node) - { - return base.SelectTemplateCore(item); - } - - return node.GetSceneHierarchyTemplate(); - } -} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/InspectorPage.xaml b/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/InspectorPage.xaml deleted file mode 100644 index 80b05b2..0000000 --- a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/InspectorPage.xaml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/InspectorPage.xaml.cs b/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/InspectorPage.xaml.cs deleted file mode 100644 index 7334ba0..0000000 --- a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/InspectorPage.xaml.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Ghost.Editor.Controls; -using Ghost.Editor.ViewModels.Pages.EngineEditor; - -namespace Ghost.Editor.Views.Pages.EngineEditor; - -internal sealed partial class InspectorPage : NavigationTabPage -{ - public InspectorViewModel ViewModel - { - get; - } - - public InspectorPage() - { - ViewModel = App.GetService(); - - InitializeComponent(); - } - - public override void OnNavigatedTo(object? parameter) - { - ViewModel.OnNavigatedTo(parameter); - } - - public override void OnNavigatedFrom() - { - ViewModel.OnNavigatedFrom(); - } -} diff --git a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ProjectPage.xaml b/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ProjectPage.xaml deleted file mode 100644 index 0bc591f..0000000 --- a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ProjectPage.xaml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ProjectPage.xaml.cs b/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ProjectPage.xaml.cs deleted file mode 100644 index 942cc39..0000000 --- a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ProjectPage.xaml.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Ghost.Editor.ViewModels.Pages.EngineEditor; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Input; - -namespace Ghost.Editor.Views.Pages.EngineEditor; - -internal sealed partial class ProjectPage : Page -{ - public ProjectViewModel ViewModel - { - get; - } - - public ProjectPage() - { - ViewModel = App.GetService(); - - InitializeComponent(); - } - - private void GridViewItem_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e) - { - ViewModel.OpenSelected(); - } -} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ScenePage.xaml b/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ScenePage.xaml deleted file mode 100644 index c1a7e0d..0000000 --- a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ScenePage.xaml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ScenePage.xaml.cs b/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ScenePage.xaml.cs deleted file mode 100644 index b43daf0..0000000 --- a/src/Editor/Ghost.Editor/Views/Pages/EngineEditor/ScenePage.xaml.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Ghost.Editor.Controls; -//using Ghost.Graphics.Contracts; -//using Microsoft.UI.Xaml; -//using Microsoft.UI.Xaml.Controls; -//using WinRT; - -namespace Ghost.Editor.Views.Pages.EngineEditor; - -internal sealed partial class ScenePage : NavigationTabPage -{ - //private Renderer? _renderView; - //private ISwapChainPanelNative _swapChainPanelNative; - - public ScenePage() - { - InitializeComponent(); - - //SwapChainPanel.Loaded += SwapChainPanel_Loaded; - //SwapChainPanel.Unloaded += SwapChainPanel_Unloaded; - //SwapChainPanel.SizeChanged += SwapChainPanel_SizeChanged; - } - - //private void SwapChainPanel_Loaded(object sender, RoutedEventArgs e) - //{ - // var guid = typeof(ISwapChainPanelNative.Interface).GUID; - // ((IWinRTObject)SwapChainPanel).NativeObject.TryAs(guid, out var swapChainPanelNativeHandle); - // _swapChainPanelNative = new ISwapChainPanelNative(swapChainPanelNativeHandle); - - // _renderView = GraphicsPipeline.GraphicsDevice.CreateRenderer(new(_swapChainPanelNative, (uint)SwapChainPanel.ActualWidth, (uint)SwapChainPanel.ActualHeight)); - //} - - //private void SwapChainPanel_Unloaded(object sender, RoutedEventArgs e) - //{ - // _swapChainPanelNative.Dispose(); - // _renderView?.Dispose(); - //} - - //private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e) - //{ - // if (e.NewSize.ActualWidth > 8.0 && e.NewSize.ActualHeight > 8.0) - // { - // _renderView?.RequestResize((uint)e.NewSize.ActualWidth, (uint)e.NewSize.ActualHeight); - // } - //} -} \ No newline at end of file diff --git a/src/Editor/Ghost.Editor/Views/Windows/EngineEditorWindowOld.xaml b/src/Editor/Ghost.Editor/Views/Windows/EngineEditorWindowOld.xaml deleted file mode 100644 index fdcf948..0000000 --- a/src/Editor/Ghost.Editor/Views/Windows/EngineEditorWindowOld.xaml +++ /dev/null @@ -1,246 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Editor/Ghost.Editor/Views/Windows/EngineEditorWindowOld.xaml.cs b/src/Editor/Ghost.Editor/Views/Windows/EngineEditorWindowOld.xaml.cs deleted file mode 100644 index 3e31ecc..0000000 --- a/src/Editor/Ghost.Editor/Views/Windows/EngineEditorWindowOld.xaml.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Ghost.Editor.Core; -using Ghost.Editor.Core.Contracts; -using Ghost.Editor.Core.Services; -using Ghost.Editor.ViewModels.Windows; -using Windows.ApplicationModel; -using WinUIEx; - -namespace Ghost.Editor.Views.Windows; -/// -/// An empty window that can be used on its own or navigated to within a Frame. -/// -internal sealed partial class EngineEditorWindowOld : WindowEx -{ - private readonly NotificationService _notificationService; - private readonly ProgressService _progressService; - - public EngineEditorViewModel ViewModel - { - get; - } - - public EngineEditorWindowOld() - { - ViewModel = App.GetService(); - - _notificationService = (NotificationService)App.GetService(); - _progressService = (ProgressService)App.GetService(); - - InitializeComponent(); - - AppWindow.SetIcon(Path.Combine(AppContext.BaseDirectory, "Assets/icon.ico")); - Title = "Ghost Engine"; - ExtendsContentIntoTitleBar = true; - - SetTitleBar(PART_TitleBar); - this.CenterOnScreen(); - } - - private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) - { - PART_TitleBar.Title = EditorApplication.ProjectName; - PART_TitleBar.Subtitle = $"Ghost Engine {Package.Current.Id.Version.Major}.{Package.Current.Id.Version.Minor}.{Package.Current.Id.Version.Build}"; - - _notificationService.SetReference(InfoBar, NotificationQueue); - _progressService.SetReference(ProgressBarContainer); - - InitializeDockingLayout(); - } - - private void InitializeDockingLayout() - { - var sceneDoc = new Controls.Docking.DockDocument { Title = "Scene", Content = new Pages.EngineEditor.ScenePage() }; - var hierarchyDoc = new Controls.Docking.DockDocument { Title = "Hierarchy", Content = new Controls.Hierarchy() }; - var inspectorDoc = new Controls.Docking.DockDocument { Title = "Inspector", Content = new Pages.EngineEditor.InspectorPage() }; - var projectDoc = new Controls.Docking.DockDocument { Title = "Project", Content = new Controls.ProjectBrowser() }; - var consoleDoc = new Controls.Docking.DockDocument { Title = "Console", Content = new Pages.EngineEditor.ConsolePage() }; - - var leftGroup = new Controls.Docking.DockGroup(); - leftGroup.AddChild(hierarchyDoc); - - var centerGroup = new Controls.Docking.DockGroup(); - centerGroup.AddChild(sceneDoc); - - var rightGroup = new Controls.Docking.DockGroup(); - rightGroup.AddChild(inspectorDoc); - - var bottomGroup = new Controls.Docking.DockGroup(); - bottomGroup.AddChild(projectDoc); - bottomGroup.AddChild(consoleDoc); - - var topPanel = new Controls.Docking.DockPanel { Orientation = Microsoft.UI.Xaml.Controls.Orientation.Horizontal }; - topPanel.AddChild(leftGroup); - topPanel.AddChild(centerGroup); - topPanel.AddChild(rightGroup); - - var rootPanel = new Controls.Docking.DockPanel { Orientation = Microsoft.UI.Xaml.Controls.Orientation.Vertical }; - rootPanel.AddChild(topPanel); - rootPanel.AddChild(bottomGroup); - - MainDockingLayout.RootModule = rootPanel; - } - - private void MainGrid_Unloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) - { - _notificationService.ClearReference(); - _progressService.ClearReference(); - } -} \ No newline at end of file diff --git a/src/Runtime/Ghost.Core/Common.cs b/src/Runtime/Ghost.Core/Common.cs index 037c5a5..734f64b 100644 --- a/src/Runtime/Ghost.Core/Common.cs +++ b/src/Runtime/Ghost.Core/Common.cs @@ -1,15 +1,2 @@ namespace Ghost.Core; -public enum AssetType : byte -{ - Texture = 0, - Mesh = 1, - Material = 2, - Shaders = 3, - Audio = 4, - Scene = 5, - Video = 6, - Json = 7, - - Unknown = 64, -} diff --git a/src/Runtime/Ghost.Core/Ghost.Core.csproj b/src/Runtime/Ghost.Core/Ghost.Core.csproj index 0eab3f7..47ee480 100644 --- a/src/Runtime/Ghost.Core/Ghost.Core.csproj +++ b/src/Runtime/Ghost.Core/Ghost.Core.csproj @@ -20,7 +20,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Runtime/Ghost.Core/Result.cs b/src/Runtime/Ghost.Core/Result.cs index 38c91de..aa05003 100644 --- a/src/Runtime/Ghost.Core/Result.cs +++ b/src/Runtime/Ghost.Core/Result.cs @@ -81,7 +81,7 @@ public readonly struct Result sb.AppendLine(result.Message); } } - + if (sb.Length == 0) { return Success(); @@ -158,6 +158,7 @@ public readonly struct Result public static implicit operator Result(T? data) => data is not null ? Success(data) : Failure(null); public static implicit operator Result(Result result) => result.IsSuccess ? Success(default!) : Failure(result.Message); + public static implicit operator Result(Result result) => result.IsSuccess ? Result.Success() : Result.Failure(result.Message); public static implicit operator bool(Result result) => result.IsSuccess; } diff --git a/src/Runtime/Ghost.Core/Utilities/ConcurrentHashSet.cs b/src/Runtime/Ghost.Core/Utilities/ConcurrentHashSet.cs new file mode 100644 index 0000000..3c329a5 --- /dev/null +++ b/src/Runtime/Ghost.Core/Utilities/ConcurrentHashSet.cs @@ -0,0 +1,162 @@ +using System.Collections; + +namespace Ghost.Core.Utilities; + +public class ConcurrentHashSet : IDisposable +{ + public struct Enumerator : IEnumerator + { + private readonly ConcurrentHashSet _set; + private readonly HashSet.Enumerator _enumerator; + + public Enumerator(ConcurrentHashSet set) + { + _set = set; + _set._lock.EnterReadLock(); + _enumerator = _set._hashSet.GetEnumerator(); + } + + public readonly T Current => _enumerator.Current; + readonly object? IEnumerator.Current => Current; + + public void Dispose() + { + if (_set._lock.IsReadLockHeld) + { + _set._lock.ExitReadLock(); + } + + _enumerator.Dispose(); + } + + public bool MoveNext() + { + return _enumerator.MoveNext(); + } + + public void Reset() + { + } + } + + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + private readonly HashSet _hashSet = new HashSet(); + + public int Count + { + get + { + _lock.EnterReadLock(); + try + { + return _hashSet.Count; + } + finally + { + if (_lock.IsReadLockHeld) + { + _lock.ExitReadLock(); + } + } + } + } + + public bool IsEmpty + { + get + { + _lock.EnterReadLock(); + try + { + return _hashSet.Count == 0; + } + finally + { + if (_lock.IsReadLockHeld) + { + _lock.ExitReadLock(); + } + } + } + } + + ~ConcurrentHashSet() + { + Dispose(); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public bool Add(T item) + { + _lock.EnterWriteLock(); + try + { + return _hashSet.Add(item); + } + finally + { + if (_lock.IsWriteLockHeld) + { + _lock.ExitWriteLock(); + } + } + } + + public bool Contains(T item) + { + _lock.EnterReadLock(); + try + { + return _hashSet.Contains(item); + } + finally + { + if (_lock.IsReadLockHeld) + { + _lock.ExitReadLock(); + } + } + } + + public bool Remove(T item) + { + _lock.EnterWriteLock(); + try + { + return _hashSet.Remove(item); + } + finally + { + if (_lock.IsWriteLockHeld) + { + _lock.ExitWriteLock(); + } + } + } + + public void Clear() + { + _lock.EnterWriteLock(); + try + { + _hashSet.Clear(); + } + finally + { + if (_lock.IsWriteLockHeld) + { + _lock.ExitWriteLock(); + } + } + } + + public void Dispose() + { + _lock.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/Runtime/Ghost.Core/Utilities/NativeMemoryManager.cs b/src/Runtime/Ghost.Core/Utilities/MemoryManager.cs similarity index 57% rename from src/Runtime/Ghost.Core/Utilities/NativeMemoryManager.cs rename to src/Runtime/Ghost.Core/Utilities/MemoryManager.cs index 6973483..97e99e7 100644 --- a/src/Runtime/Ghost.Core/Utilities/NativeMemoryManager.cs +++ b/src/Runtime/Ghost.Core/Utilities/MemoryManager.cs @@ -1,6 +1,8 @@ using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections.Contracts; using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using MemoryHandle = System.Buffers.MemoryHandle; namespace Ghost.Core.Utilities; @@ -51,3 +53,47 @@ public unsafe class NativeMemoryManager : MemoryManager { } } + +public sealed class CastMemoryManager : MemoryManager + where TFrom : struct + where TTo : struct +{ + private readonly Memory _from; + private MemoryHandle _innerHandle; + + public CastMemoryManager(Memory from) + { + _from = from; + } + + public override Span GetSpan() + { + return MemoryMarshal.Cast(_from.Span); + } + + public override MemoryHandle Pin(int elementIndex = 0) + { + _innerHandle = _from.Pin(); + + unsafe + { + int byteOffset = elementIndex * Unsafe.SizeOf(); + void* pointer = (byte*)_innerHandle.Pointer + byteOffset; + + return new MemoryHandle(pointer, default, this); + } + } + + public override void Unpin() + { + _innerHandle.Dispose(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _innerHandle.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Runtime/Ghost.Engine/AssetLoader/Asset.cs b/src/Runtime/Ghost.Engine/AssetLoader/Asset.cs index 9a2e31e..9438aa9 100644 --- a/src/Runtime/Ghost.Engine/AssetLoader/Asset.cs +++ b/src/Runtime/Ghost.Engine/AssetLoader/Asset.cs @@ -1,47 +1,7 @@ -using Ghost.Core; -using Ghost.Graphics.RHI; -using Misaki.HighPerformance.LowLevel; -using Misaki.HighPerformance.LowLevel.Buffer; using System.Runtime.InteropServices; namespace Ghost.Engine.AssetLoader; -public abstract class Asset : IResourceReleasable -{ - private bool _disposed; - - public Guid ID - { - get; - } - - public abstract AssetType Type - { - get; - } - - protected Asset(Guid id) - { - ID = id; - } - - protected virtual void Release(IResourceDatabase resourceDatabase) - { - } - - public void ReleaseResource(IResourceDatabase database) - { - if (_disposed) - { - return; - } - - Release(database); - - _disposed = true; - } -} - public readonly struct AssetReference : IEquatable { private readonly int _value; @@ -84,71 +44,3 @@ public readonly struct AssetReference : IEquatable return !(left == right); } } - -[StructLayout(LayoutKind.Sequential, Size = 64)] // Leave extra space for future expansion without breaking compatibility -public struct TextureContentHeader -{ - public uint width; - public uint height; - public uint depth; - public uint mipLevels; - public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D - public uint colorComponents; -} - -public class TextureAsset : Asset -{ - private MemoryBlock _textureData; - private readonly uint _width; - private readonly uint _height; - private readonly uint _depth; - private readonly uint _colorComponents; - private readonly uint _mipLevels; - private readonly uint _dimension; - - private Handle _textureHandle; - - public override AssetType Type => AssetType.Texture; - - public uint Width => _width; - public uint Height => _height; - public uint Depth => _depth; - public uint MipLevels => _mipLevels; - public uint Dimension => _dimension; - public uint ColorComponents => _colorComponents; - - public Handle TextureHandle => _textureHandle; - - internal TextureAsset([OwnershipTransfer] ref MemoryBlock data, TextureContentHeader header, Guid id) - : base(id) - { - _textureData = data; - _width = header.width; - _height = header.height; - _depth = header.depth; - _mipLevels = header.mipLevels; - _dimension = header.dimension; - _colorComponents = header.colorComponents; - } - - internal void SetTextureHandle(Handle handle, bool disposeCPUData = true) - { - _textureHandle = handle; - if (disposeCPUData) - { - _textureData.Dispose(); - } - } - - public ReadOnlySpan GeData() - where T : unmanaged - { - return _textureData.AsSpan(); - } - - protected override void Release(IResourceDatabase resourceDatabase) - { - _textureData.Dispose(); - resourceDatabase.ReleaseResource(_textureHandle.AsResource()); - } -} \ No newline at end of file diff --git a/src/Runtime/Ghost.Engine/AssetManager.Texture.cs b/src/Runtime/Ghost.Engine/AssetManager.Texture.cs index 5b1c245..f0358e2 100644 --- a/src/Runtime/Ghost.Engine/AssetManager.Texture.cs +++ b/src/Runtime/Ghost.Engine/AssetManager.Texture.cs @@ -1,13 +1,23 @@ using Ghost.Core; using Ghost.Core.Utilities; -using Ghost.Engine.AssetLoader; using Ghost.Graphics; using Ghost.Graphics.RHI; using Ghost.Graphics.Utilities; -using TerraFX.Interop.Windows; +using System.Runtime.InteropServices; namespace Ghost.Engine; +[StructLayout(LayoutKind.Sequential, Size = 64)] // Leave extra space for future expansion without breaking compatibility +public struct TextureContentHeader +{ + public uint width; + public uint height; + public uint depth; + public uint mipLevels; + public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D + public uint colorComponents; +} + internal partial class AssetEntry { private unsafe class TextureData diff --git a/src/Runtime/Ghost.Engine/AssetManager.cs b/src/Runtime/Ghost.Engine/AssetManager.cs index d937a8f..ff04912 100644 --- a/src/Runtime/Ghost.Engine/AssetManager.cs +++ b/src/Runtime/Ghost.Engine/AssetManager.cs @@ -11,11 +11,24 @@ using Misaki.HighPerformance.LowLevel.Utilities; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace Ghost.Engine; -public enum AssetState : byte +public enum AssetType +{ + Texture = 0, + Mesh = 1, + Material = 2, + Shaders = 3, + Scene = 4, + Audio = 5, + Video = 6, + Json = 7, + + Unknown = 32, // We are unlikely to have more than 32 asset types. +} + +public enum AssetState { Unloaded = 0, Scheduled = 1, @@ -192,6 +205,11 @@ internal unsafe partial class AssetEntry public void OnReleaseResource() { s_onReleaseResource[(int)_assetType]?.Invoke(this); + + if (_rawData.IsCreated) + { + _rawData.Dispose(); + } } } @@ -199,7 +217,7 @@ internal struct LoadAssetJob : IJob { public Guid assetID; public AssetType assetType; - public GCHandle assetManagerHandle; + public AssetManager assetManager; private static Result LoadRawData(IContentProvider contentProvider, AssetEntry entry) { @@ -232,8 +250,6 @@ internal struct LoadAssetJob : IJob public readonly void Execute(ref readonly JobExecutionContext ctx) { - var assetManager = assetManagerHandle.Target as AssetManager; - Logger.DebugAssert(assetManager is not null); if (!assetManager.TryGetEntry(assetID, out var entry)) @@ -279,8 +295,6 @@ internal partial class AssetManager : IDisposable private readonly ConcurrentDictionary _entries; - private GCHandle _selfHandle; - // TODO private Handle _fallbackTexture; private Handle _fallbackNormalMap; @@ -300,7 +314,6 @@ internal partial class AssetManager : IDisposable _jobScheduler = jobScheduler; _entries = new ConcurrentDictionary(); - _selfHandle = GCHandle.Alloc(this, GCHandleType.Normal); } internal bool TryGetEntry(Guid guid, [NotNullWhen(true)] out AssetEntry? entry) @@ -378,10 +391,10 @@ internal partial class AssetManager : IDisposable { assetID = entry.AssetId, assetType = entry.AssetType, - assetManagerHandle = _selfHandle, + assetManager = this, }; - entry.SetLoadJobHandle(_jobScheduler.Schedule(ref job, dependency, JobPriority.Low)); // Use low priority to avoid blocking main thread critical tasks like rendering and physics. + entry.SetLoadJobHandle(_jobScheduler.Schedule(ref job, JobPriority.Low, dependency)); // Use low priority to avoid blocking main thread critical tasks like rendering and physics. } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -409,18 +422,18 @@ internal partial class AssetManager : IDisposable return; } - if (entry.State is AssetState.Loading or AssetState.Loaded or AssetState.Uploading) + if (Interlocked.CompareExchange(ref entry.StateValue, (int)AssetState.Scheduled, (int)AssetState.Ready) == (int)AssetState.Ready) + { + // Entry is in Ready state — the old texture is valid and will remain visible. + // Go directly to Scheduled → Loading → Loaded → Uploading → Ready again. + // The swap cycle in RecordTextureUpload/OnTextureUploadComplete handles the + // v1 → v2 transition exactly like the fallback → v1 transition. + EnsureScheduled(entry); + } + else { entry.SetPendingReimport(); - return; } - - // Entry is in Ready state — the old texture is valid and will remain visible. - // Go directly to Scheduled → Loading → Loaded → Uploading → Ready again. - // The swap cycle in RecordTextureUpload/OnTextureUploadComplete handles the - // v1 → v2 transition exactly like the fallback → v1 transition. - entry.State = AssetState.Scheduled; - EnsureScheduled(entry); } public void Dispose() @@ -431,6 +444,5 @@ internal partial class AssetManager : IDisposable } _entries.Clear(); - _selfHandle.Free(); } } diff --git a/src/Runtime/Ghost.Entities/EntityQuery.JobChunk.cs b/src/Runtime/Ghost.Entities/EntityQuery.JobChunk.cs index 333e5a7..628d71c 100644 --- a/src/Runtime/Ghost.Entities/EntityQuery.JobChunk.cs +++ b/src/Runtime/Ghost.Entities/EntityQuery.JobChunk.cs @@ -19,7 +19,6 @@ internal unsafe struct ChunkInfo internal unsafe struct JobChunkBatch : IJobParallelFor where TJob : unmanaged, IJobChunk { - public TJob userJob; public ReadOnlyUnsafeCollection chunkInfos;