117 Commits

Author SHA1 Message Date
9fcf06dbe4 Update icon assets 2026-02-01 01:54:04 +09:00
6505099667 Fixed the issue where the test does not cleanup the temp folder 2026-02-01 01:53:19 +09:00
d263f0c7e1 Imporving AssetDatabase 2026-01-30 21:20:18 +09:00
9f05944d81 Improve AssetDatabase performance. 2026-01-29 20:37:45 +09:00
e71851550b Update asset database 2026-01-29 14:03:24 +09:00
8a5795069f Update AssetDatabase 2026-01-27 14:39:00 +09:00
b505c7c1c0 backup 2026-01-26 22:55:14 +09:00
8d82c0a750 Update plan 2026-01-26 15:58:19 +09:00
8df0b46960 Update scene graph 2026-01-26 13:59:33 +09:00
06a150b899 Fix error 2026-01-26 11:47:00 +09:00
49f54c6b43 Add scene graph draft 2026-01-25 22:06:58 +09:00
fdf831630b feat: implement complete scene graph system with hierarchical editor support
- Add SceneNode and EntityNode classes for editor-only metadata storage
- Implement SceneGraph view-model with O(1) entity lookup via internal caching
- Create IdRemapTable for file-local to global entity ID remapping on load
- Implement SceneSerializationContext for load/save operation tracking
- Add JSON-serializable SceneAssetData, EntityData, and ComponentData models
- Implement SceneSerializer for save/load with validation and reference remapping
- Add comprehensive documentation: README.md, IMPLEMENTATION_GUIDE.md, SYSTEM_SUMMARY.md
- Update Ghost.Editor.Core.csproj to reference Ghost.Entities assembly
- Support parent-child relationships via Hierarchy component
- Enforce no cross-scene entity references
- Keep runtime minimal: only SceneID, Hierarchy, LocalToWorld components
- All editor metadata (names, UI state) stored in editor-only SceneNode/EntityNode classes

This implements the architecture from SceneGraph Plan.md with clean separation of concerns,
minimal runtime footprint, and AOT compatibility.
2026-01-25 21:42:03 +09:00
ba5dc2159e Remove old SceneGraph 2026-01-25 21:18:16 +09:00
0201f0fc33 Add simple scene graph 2026-01-25 18:37:45 +09:00
364fbf9208 Refactor error handling: use Error enum, update APIs
Replaces ErrorStatus with Error across all systems for consistency.
Renames ResourceBarrierData fields to camelCase.
Adds BindlessAccess enum and updates GetBindlessIndex API.
Updates method signatures, result types, and error checks.
Modernizes HLSL mesh shader syntax and fixes naming.
Improves code style and updates comments for clarity.
2026-01-25 16:34:28 +09:00
e11a9ebb52 Refactor render graph: modular compilation & execution
Major overhaul of render graph system for modularity and performance:
- Split compilation and execution logic into dedicated classes (Compiler, Executor, NativePassBuilder, Barriers)
- Overhauled barrier system: now uses CompiledBarrier with target state only, querying before state at execution
- Resource size/alignment now queried from D3D12 device for accurate heap allocation
- ResourceDesc now includes Type field and asserts correct union access
- Centralized D3D12 interop logic in D3D12Utility extensions
- Added RenderGraphHasher for structural graph hashing and cache invalidation
- RenderGraph class simplified to orchestrate specialized components
- ResourceAliasingManager now uses allocator for size queries
- Compilation cache now stores compiled barriers, reducing memory usage
- Improved comments, debug assertions, and removed redundant code

Result: more maintainable, efficient, and robust render graph pipeline.
2026-01-23 18:12:52 +09:00
4173ff2432 Refactor RenderGraph barrier/state tracking system
Major overhaul of resource barrier and state tracking in RenderGraph:
- Introduce ResourceBarrierData for explicit (layout, access, sync) tracking.
- Separate aliasing and transition barriers; explicit aliasing support.
- Remove BufferHint; infer buffer usage from BufferUsage flags.
- Update TextureAccess/BufferAccess to include usage requirements.
- Improve enums (BarrierSync, BarrierAccess, BarrierLayout) for D3D12 alignment.
- Update D3D12CommandBuffer to use new barrier data and error handling.
- Make D3D12DescriptorHeap a class; add ReleaseSampler to IResourceDatabase.
- Reset resource pools and aliasing managers each frame.
- Batch and flush barriers efficiently per pass.
- Update HLSL mesh shader macros to [NumThreads].
- Remove obsolete code and improve documentation.
This refactor improves correctness, extensibility, and prepares for advanced features.
2026-01-22 20:51:58 +09:00
139312d73b Enhanced barrier 2026-01-22 12:33:23 +09:00
92b966fe0d Render graph integration and resource management refactor
Introduces a full-featured render graph system with pass culling, resource aliasing, and automatic barrier generation. Refactors resource and barrier APIs, improves error handling, and unifies result types. Renderer and render passes now use the new graph-based workflow. Updates shader includes, adds a blit shader, and improves HLSL parsing. Removes dynamic descriptor heaps in favor of persistent ones. Project file now includes the render graph module. Lays the foundation for advanced rendering features and improved memory efficiency.
2026-01-21 18:32:03 +09:00
1c155f962c Render graph: native pass merging & heap-based aliasing
Major architecture upgrade:
- Add native render pass merging (hardware pass grouping, load/store op inference)
- Implement heap-based aliasing for textures & buffers (D3D12-style)
- Unify resource model: buffers and textures in one registry
- Extend builder API for buffer creation/usage, access flags, hints
- Improve barrier/state tracking (buffer hints, indirect argument state)
- Update caching, hashing, and debug output for new model
- Add enums/structs: AttachmentLoadOp, StoreOp, BufferHint, etc.
- D3D12 backend: support named resources, temp upload buffers, correct heap usage
- Update docs, benchmarks, and project files for new features

Brings render graph closer to AAA engine standards, enabling efficient memory usage, lower driver overhead, and a more flexible API.
2026-01-16 01:59:33 +09:00
ac36bbf8c7 small backup 2026-01-13 14:50:55 +09:00
02df8d7732 Update render graph 2026-01-13 13:46:50 +09:00
954e3756aa Refactor Render Graph: unified resources, benchmarking
Major overhaul of Render Graph system:
- Replaced texture handles with generic Identifier<T> for unified, type-safe resource management (textures, buffers, etc.)
- Refactored resource registry and pooling for performance and extensibility
- Added AccessFlags and TextureAccess for precise resource usage tracking
- Split passes into Raster and Compute types; introduced builder interfaces for safer pass construction
- Modernized pass setup API (SetColorAttachment, UseTexture, etc.)
- Updated command buffer and context structs to use new resource system
- Refactored barrier and aliasing logic for improved correctness
- Integrated BenchmarkDotNet for performance/memory benchmarking
- Improved blackboard type safety and removed obsolete code/extensions
- Added BenchmarkDotNet NuGet package

These changes make the Render Graph more extensible, efficient, and ready for future resource types and advanced features.
2026-01-12 23:48:56 +09:00
1fc9df1812 GhostEngine Render Graph: major refactor & Unity RG ref
- Major architectural refactor for performance, extensibility, and feature completeness: resource pooling, pass culling, aliasing, and compilation caching.
- Introduces type-safe builder and context APIs, blackboard pattern, and unified resource management.
- Adds detailed documentation and cleans up obsolete files and APIs.
- Includes (commented) Unity Render Graph source for reference; not compiled, for parity and future extension.
2026-01-11 23:43:17 +09:00
87e315a588 Refactor render graph & DSL; remove material system
- Major optimization of Ghost.RenderGraph.Concept: pooled resources, zero-allocation hot paths, explicit queue types, and batch barrier APIs.
- Migrated Ghost.DSL shader compiler to ANTLR4-based parser; removed hand-written parser, added grammar files and semantic model conversion.
- Added CollectionPool/ListPool for pooled list management.
- Updated documentation for new architecture and performance.
- Removed Ghost.Shader.Concept (material/material system) from repo and solution.
- README.md replaced with a brief project statement.
2026-01-11 13:28:17 +09:00
d71bdb3fc9 Refactor shader system: arrays, keywords, property syntax
Major refactor of shader compiler and related systems:
- Switch ShaderDescriptor/PassDescriptor to arrays; remove IPassDescriptor
- Rewrite keywords block parser/semantic analysis for flexible syntax
- Change property initializers to brace syntax `{ ... }`
- Simplify TokenStream API (remove ref index params)
- Make GetBindlessIndex return uint (~0u for not found)
- Update shader compilation and variant logic for new descriptors
- Update test shader syntax to match new property/keyword formats
- Add AGENTS.md agent development guide
- Add Antlr4 dependency to Ghost.DSL
- Miscellaneous code style and error handling improvements
2026-01-10 18:36:18 +09:00
6a041f75ba Refactor: variant-aware shader/material pipeline overhaul
Major architectural update to graphics/material/shader system:
- Introduced strongly-typed key structs (Key64/Key128) for passes, variants, and pipelines; removed legacy key types.
- Implemented robust hashing and key generation utilities for efficient variant and pipeline lookup/caching.
- Shader compiler now compiles/caches all keyword variants using new key system; includes handled as lists.
- Switched to push constant root signature for per-draw data; updated HLSL and C# codegen accordingly.
- Refactored Material, Shader, and Pass data structures for cache efficiency and variant support.
- Pipeline library and PSO management now use 128-bit keys and variant-specific caching.
- Replaced WorldNode with SceneNode in editor/scene graph; introduced ComponentManager for archetype/query management.
- Migrated math utilities to Misaki.HighPerformance.Mathematics; updated editor controls.
- Updated all HLSL and codegen for new buffer/push constant layouts and macros.
- Misc: project reference cleanup, D3D12 Work Graph support, doc updates, and code modernization.
2026-01-09 22:25:37 +09:00
c9be05fc60 Added additional config to CompilePass in IShaderCompiler 2025-12-27 15:14:06 +09:00
f988c34b3d Add high-performance material/shader system (Ghost.Shader.Concept)
Introduces a new Ghost.Shader.Concept project implementing a modern, data-oriented material and shader system with:
- Global/local keyword bitsets (fast O(1) ops, 64 bytes)
- Multi-pass shader program and per-pass render state overrides
- Thread-safe, 16-byte aligned material property blocks
- Material pooling to reduce GC pressure
- Batch renderer for efficient PSO grouping and async variant warmup
- Full demo (Program.cs) and extensive documentation (ARCHITECTURE.md, README.md, PROJECT_SUMMARY.md)
- Minor integration: new enums, doc updates, and keyword handling in existing code

No breaking changes to the existing engine; all new code is isolated. This serves as a reference implementation for high-performance, extensible material/shader architectures.
2025-12-26 19:19:30 +09:00
a89719bfc9 Refactor pipeline state and render output abstractions
- Replace old pipeline enums/structs with new strongly-typed PipelineState and enums (ZTest, ZWrite, Cull, Blend, ColorWriteMask)
- Redesign pipeline keying: introduce 128-bit GraphicsPipelineKey, MaterialPipelineKey, and PassPipelineKey for robust PSO caching
- Replace IRenderTargetStrategy with IRenderOutput; add SwapChainRenderOutput and TextureRenderOutput
- Update renderer and window code to use new render output abstraction and handle viewport/scissor updates
- Make ShaderPass a readonly struct and Shader a struct; use ID-based pass lookup for efficiency
- Materials now support per-pass pipeline overrides with new keying
- Add defensive checks in D3D12CommandBuffer; update D3D12PipelineLibrary for new keying/state
- Move test shader to test.gsdef and update for new pipeline state syntax
- Remove obsolete files/interfaces and perform general code cleanups
- Update all usages and parsing logic for new pipeline state system
2025-12-24 19:06:34 +09:00
b8ce824292 Updated Handle and Identifier that default is invalid 2025-12-23 14:22:44 +09:00
aa3d9c749b Refactor: add command allocator & render target strategies
Major refactor of graphics infrastructure:
- Introduce ICommandAllocator and D3D12CommandAllocator for explicit command buffer management.
- Change ICommandBuffer.Begin to require an allocator.
- Add IRenderTargetStrategy abstraction with swap chain and texture implementations.
- Update IRenderer to use RenderTargetStrategy instead of direct handle.
- Add DPI scaling support to swap chains (ScaleX/ScaleY, SetScale).
- RenderSystem now supports thread-safe swap chain resize requests.
- Remove persistent copy command buffer; use per-frame allocators.
- Make Logger public/static and clean up API visibility.
- Update .editorconfig and debug layer enablement.
These changes improve modularity, DPI-awareness, and future extensibility.
2025-12-23 00:35:34 +09:00
d23e701f0a refactor IRenderer 2025-12-22 15:59:02 +09:00
2881fda112 Refactor component registration, update deps, improve JSON
- Updated Misaki.HighPerformance package versions in Core and Graphics projects.
- Added IsTrimmable to Ghost.Engine.csproj for trimming support.
- Renamed GetOrRegisterComponent to GetOrRegisterComponentID and updated all usages.
- Component registration codegen now uses a static class with [ModuleInitializer], no longer requires [EngineEntry].
- Improved JSON serialization: added string support, introduced Utf8JsonObjectScope/ArrayScope, and new extension methods for cleaner JSON writing.
- Removed [SkipLocalsInit] from Hierarchy and LocalToWorld.
- Fixed Entity.Invalid to use INVALID_ID for both fields.
- Minor cleanup: clarified comments, reorganized Ghost.Generator in solution, and disabled component serialization generator.
2025-12-21 22:18:25 +09:00
840cf7dd5a Add entities SerializationTest 2025-12-21 13:07:59 +09:00
00b4e82ded ECS refactor: new ComponentSet, serialization, generators
Major ECS API overhaul: added ComponentSet, refactored ComponentRegistry, and updated all entity/component creation methods. Introduced robust custom serialization infrastructure and per-component source generators for registration and (de)serialization. Updated editor, engine, and test code to use new APIs. Improved code quality, naming, and performance throughout. Removed obsolete code and updated dependencies.
2025-12-20 20:41:40 +09:00
3118021272 Merge pull request 'feature/archetype-ecs' (#1) from feature/archetype-ecs into develop
Reviewed-on: #1
2025-12-17 08:27:31 +00:00
756727dc06 Added SystemBase 2025-12-16 15:05:42 +09:00
7613b5087e Add new test and structural change version to chunk. 2025-12-16 11:03:11 +09:00
70cdd981aa Added new method to remove entities efficently. 2025-12-12 21:12:46 +09:00
05843fd665 Updated debug view for chunk 2025-12-12 21:11:01 +09:00
7db4be1e6e Add test 2025-12-12 17:22:41 +09:00
a3863c1263 Update version support 2025-12-11 21:25:32 +09:00
856fa4f07d Per-component versioning and change tracking for ECS
Introduce per-component versioning in chunks and world for efficient change detection.
- Add version arrays to chunks and global version to world.
- Update queries and ForEach to mark written components as changed.
- Extend QueryBuilder with WithAllRW/WithPresentRW for write access.
- Expose change tracking API in ChunkView.
- Improve thread safety and debug code.
- Update tests and examples to demonstrate new features.
2025-12-10 19:01:25 +09:00
21e85e0c02 Add managed entity and script component.
Added ManagedEntity and related methods in EntityManager;
Added ScriptComponent to write game play logic in oop;
2025-12-10 16:12:56 +09:00
99c1a1980e Improve the usability of Result<T, E> and add new job schedule method to EntityQuery.
Added implicate conversion to Result<T, E> and RefResult<T, E>;
Added new ScheduleChunkParallel in EntityQuery;
Remove Ghost.SparseEntity from solution file. It's now completlty replaced by Ghost.Entities;
2025-12-09 21:43:12 +09:00
97d1118caa Remove old project and continue improving ecs.
Updated packages version;
Removed Ghost.SparseEntities;
Added new EntityQuery.EntityComponentIterator;
Added new thread local command buffer in World;
Changed commands in EntityCommandBuffer from UnsafeList<Command> to UnsafeList<byte> for better performance;
Changed the name of IJobEntityParallel to IJobEntity;
2025-12-09 15:10:10 +09:00
5e276b289d Removed Ghost.ArcEntities project, it's replaced by Ghost.Entities
Added Playback to EntityCommandBuffer
Added JobSchedular to world
Added ISystem and SystemGroup
Updated packages
2025-12-08 20:44:56 +09:00
f44208b502 change document formatting
merge conflits
2025-12-07 11:51:08 +09:00
02084c1e47 Added ScheduleEntityParallel and IJobEntityParallel for parallel querying 2025-12-07 11:45:25 +09:00
30c1d99959 Support enableable components and query enhancements
- Upgraded `Misaki.HighPerformance.LowLevel` to v1.2.8.
- Added `IEquatable` to `Handle<T>` and `Identifier<T>`.
- Improved `Result` extensions with `[CallerArgumentExpression]`.
- Introduced `SetEnabled` in `EntityManager` to toggle components.
- Refactored `Chunk` and `Archetype` for enableable components.
- Added `EntityQueryMask` for filtering enabled/disabled components.
- Enhanced `QueryBuilder` with new filtering methods (`WithAll`, etc.).
- Improved `EntityQuery.ForEach` with entity validation.
2025-12-05 22:38:11 +09:00
224b2b2dd5 Refactor ECS framework and improve performance
Refactored `ArcEntityTest` to use updated `Transform` and `Mesh` components, improving query logic with `GetChunkIterator` and introducing `ForEach` methods for better modularity.

Enhanced `Chunk` and `Archetype` structs with `readonly` properties and memory optimizations. Fixed bugs in memory copy logic and entity relocation.

Improved `EntityManager` with proper disposal handling, added a destructor, and fixed pointer usage in `AddComponent` and `SetComponentData`.

Refactored `EntityQuery` to use `ChunkIterator` and `ChunkView` for better abstraction. Simplified `EntityQueryMask` logic for performance.

Introduced templated `ForEach` methods and `ForEachWithEntity` methods, dynamically generated using T4 templates for scalability.

Added disposal logic for archetypes and queries in `World`. Updated `Program.cs` to include memory debugging setup.

Integrated T4 templates for dynamic code generation and added helper functions for template generation. Updated project file to include templates and generated outputs.

General improvements include enforcing immutability, optimizing memory management, and adding debugging/logging for better traceability.
2025-12-05 00:29:12 +09:00
f9db047a5f Updated entity query 2025-12-04 20:37:52 +09:00
93bc8e55a3 Changed project name 2025-12-04 16:55:26 +09:00
3bbf485fce Update EntityManager and Archetype 2025-12-04 15:03:01 +09:00
948fae4401 Continue improve archetype ecs
Updated Archetype to support add and remove entity
Added EntityManager
Added EntityCommandBuffer
2025-12-03 20:40:19 +09:00
63a70f1a74 Updated Archetype 2025-12-02 17:41:48 +09:00
95cb9af16f Update archtype ecs 2025-12-02 16:40:23 +09:00
9d991bf316 Merge branch 'develop' into feature/archetype-ecs 2025-12-02 11:04:02 +09:00
e3be5d0087 Migrated sln to slnx 2025-12-02 11:03:05 +09:00
1fc4ff3f39 Add ArcEntities project 2025-12-02 11:02:31 +09:00
3af1d8c3bd Updated alias algorithm 2025-12-02 10:33:21 +09:00
676f8bb74c Add render graph proof of concept and refactor graphics
Implemented a transient render graph system as a proof of concept, including resource aliasing, pass culling, and typed pass data. Added new project `Ghost.RenderGraph.Concept` targeting `.NET 10.0`.

Refactored graphics-related components:
- Simplified resource state transitions in `RenderingContext`.
- Improved resize handling in `GraphicsTestWindow`.
- Updated `D3D12GraphicsEngine` to streamline frame rendering.
- Enhanced `D3D12ResourceDatabase` and `D3D12SwapChain` for better resource management.

Added detailed documentation:
- `ALIASING.md` explains resource aliasing techniques.
- `API_DESIGN.md` outlines the render graph API design.

Updated solution to include the new render graph project.
2025-12-01 22:31:17 +09:00
85280c746d Refactor error handling and improve type safety
Refactored error handling across the codebase by replacing exceptions with `Result`-based error handling for better robustness and consistency.

- Updated `ResultExtensions` to use `EqualityComparer` for comparisons.
- Enhanced `RenderingContext` with `GetValueOrThrow` for resource validation and added type constraints for texture methods.
- Introduced `CommandError` and `RecordError` in `D3D12CommandBuffer` for improved error tracking.
- Refactored `D3D12ResourceDatabase` to use `Result` objects for resource queries.
- Updated `ICommandBuffer` and `IResourceDatabase` interfaces to return `Result` objects.
- Improved type safety by replacing `int` with `uint` where appropriate.
- Simplified texture handling in `MeshRenderPass` with new `CreateTexture` logic.
- Cleaned up project files by removing unused and redundant entries.

These changes enhance code maintainability, improve error reporting, and ensure type safety throughout the project.
2025-11-30 19:06:31 +09:00
0ec318a9ab Add sampler support and refactor resource handling
Enhanced shader and resource systems with `Sampler` support, including updates to `ShaderPropertyType`, HLSL code, and resource management. Refactored `Result` structs for better type safety and added new enums for texture and comparison settings. Improved `MeshRenderPass` to dynamically load textures and samplers. Updated SDL compiler and token lexicon for `Sampler` handling. Embedded debug info in project files and streamlined resource state tracking.
2025-11-29 18:27:47 +09:00
bd97d233cb Refactor and optimize rendering pipeline
- Added `<IsTrimmable>` property in project files for trimming.
- Replaced bindless texture types with non-bindless equivalents.
- Refactored `ShaderDescriptor` and `ShaderPass` for better modularity.
- Introduced `ShaderDescriptorExtensions` for property size calculations.
- Simplified constant buffer handling in `Material.cs`.
- Improved resource management in `D3D12` components.
- Added support for static meshes and optimized resource barriers.
- Refactored shader code generation and property merging in `SDLCompiler`.
- Removed unused or redundant code (e.g., `IncludesBlock` parser).
- Updated comments, documentation, and error handling for clarity.
2025-11-28 18:58:50 +09:00
0720444c2c Refactor and enhance resource management and rendering
Updated multiple components to improve encapsulation, maintainability, and performance. Key changes include:

- Upgraded package dependencies in project files.
- Refactored `Mesh` and `RenderingContext` to use properties and added support for per-object constant buffers.
- Improved resource management in `D3D12CommandBuffer`, `D3D12CommandQueue`, and `D3D12ResourceAllocator` with better encapsulation and disposal handling.
- Added validation for constant buffer sizes in `D3D12PipelineLibrary`.
- Simplified `MeshBuilder` methods to accept allocators and removed hardcoded values.
- Enhanced debugging with `GPUResourceLeakException` and resource tracking updates.
- Updated shaders and rendering logic for testing, including hardcoded triangle rendering.
- Removed redundant base classes and interfaces for cleaner code structure.
2025-11-26 01:48:24 +09:00
dfe786a2aa Refactor core systems and improve resource management
- Updated dependencies, including `Misaki.HighPerformance` and `TerraFX.Interop`.
- Refactored `Result` struct for better error handling and chaining.
- Removed `Ptr<T>` struct as it was no longer necessary.
- Enhanced `Win32Utility` with `Attach` and `Dispose` methods.
- Improved `ProjectService` and `AppStateMachine` with `Result` integration.
- Refactored `IShaderCompiler` to support SPIR-V cross-compilation and pass-level compilation.
- Standardized Direct3D12 resource management with `UniquePtr` and added `D3D12Object` base class.
- Improved shader reflection validation and pipeline creation in `D3D12PipelineLibrary`.
- Updated `SDLCompiler` for better error handling during shader generation.
- Enhanced logging, debugging, and code readability across the codebase.
- Performed general code cleanup, including unused namespace removal and naming consistency.
2025-11-23 15:02:37 +09:00
5c4e1a3350 Added IShaderCompiler 2025-11-16 19:50:24 +09:00
d91d6f6e57 Refactor shader pipeline and improve modularity
- Added `generatedCodePath` to `FullPassDescriptor` for better shader code organization.
- Removed redundant `IID_PPV_ARGS` method and unused `Misaki.HighPerformance.Unsafe` reference.
- Refactored `Material` and `MaterialAccessor` to use `CBuffer` and updated buffer size handling.
- Renamed command buffer variables in `RenderingContext` for consistency.
- Updated `D3D12PipelineLibrary` to cache compiled shader results and added `ShaderPassKey`.
- Refactored `D3D12GraphicsEngine` to integrate `_copyCommandBuffer` lifecycle.
- Enhanced `D3D12ResourceAllocator` with shader pass creation using constant buffer info.
- Simplified `D3D12ShaderCompiler` with `GENERATED_CODE_PATH` support and improved reflection handling.
- Introduced `CBufferPropertyInfo` and `CBufferInfo` structs for better encapsulation.
- Updated HLSL shaders to use `g_PerMaterialData` and dynamic includes.
- Improved error handling in `SDLCompiler` with try-catch blocks and better messages.
- Refactored `test.gshader` to use dynamically generated includes.
- Fixed typos, improved code readability, and removed unused code.
2025-11-14 19:41:36 +09:00
708b8cd065 Fixed bugs in rendering. 2025-11-12 20:31:37 +09:00
6cf2e35a9b Updated solution to .Net 10 2025-11-12 10:15:58 +09:00
6f786a0698 Refactor namespaces and improve resource handling
- Updated namespaces from `Ghost.UnitTest` to `Ghost.Graphics.Test` across multiple files.
- Refactored `GraphicsTestWindow` to use a new `RenderSystem` configuration.
- Removed deprecated `Logger` and `SerializationTest` classes.
- Improved memory management in D3D12 components, including resource allocation and cleanup.
- Added `[SupportedOSPlatform]` attributes to specify Windows version compatibility.
- Updated `.editorconfig` settings and project references for consistency.
- Enabled `nativeDebugging` in `launchSettings.json`.
2025-11-11 21:30:47 +09:00
fb003da26a Updated D3D12Renderer for testing. 2025-11-11 16:10:17 +09:00
56f73e774b Refactoring rendering system.
Added new IRenderSystem and IFenceSynchronizer

Changed IRenderer managment from RenderSystem to IGraphicsEngine
2025-11-07 16:46:21 +09:00
15aca9aefb Update RenderingContext and D3D12Renderer to use new API. 2025-11-06 04:13:20 +00:00
b3eeb8d366 Add mesh shader support to rendering context and fix some bugs. 2025-11-05 09:37:54 +00:00
3bcf0ad539 Update package dependency using nuget instead of dll 2025-11-04 21:00:35 +09:00
ad36250979 Remove .net 10 only extension block; 2025-11-04 14:42:13 +09:00
4dc98d6ed8 Change .net version from 10 to 9; 2025-11-04 14:20:22 +09:00
2612e19d35 Re-encoding all files with utf-8; 2025-11-04 13:31:21 +09:00
017153aa02 Refactor resource management and enforce code formatting
Refactored `D3D12ResourceAllocator` to improve maintainability,
introducing new descriptor creation methods, utility functions,
and enhanced resource handling. Added thread safety and proper
disposal logic. Updated `.editorconfig` to enforce consistent
`using` directive sorting and increased max line length.

Revised `BufferUsage` enum in `Common.cs` to include new flags
and reorganized existing ones. Refactored `RenderTargetDesc`
conversion to an instance method. Adjusted `MeshRenderPass`
for consistency and added a parameter to `Execute`.

Minor formatting updates in `ShaderCode.hlsl` and cleanup of
unused directives in `D3D12Utility.cs`. Overall, these changes
enhance readability, maintainability, and functionality.
2025-11-03 22:11:31 +09:00
a8d7cd8828 Refactor and enhance rendering pipeline
- Added new C# formatting rules in .editorconfig.
- Introduced `IKeyType`, `Key<T>`, and `Ptr<T>` structs.
- Updated `Result` and `Result<T>` for implicit conversions.
- Added AOT compatibility to project files.
- Introduced a `Camera` class and refactored namespaces.
- Enhanced rendering with bindless support and pipeline state management.
- Refactored `D3D12CommandBuffer` for new rendering features.
- Improved `D3D12PipelineLibrary` with disk caching methods.
- Added support for UAVs and raw buffers in `D3D12ResourceAllocator`.
- Improved shader compilation and reflection in `D3D12ShaderCompiler`.
- Refactored descriptor heap and swap chain initialization.
- Added enums and structs for rendering configurations.
- Expanded `ICommandBuffer` and `IPipelineLibrary` interfaces.
- Updated `MeshRenderPass` to align with the new pipeline.
- Consolidated namespaces and improved code maintainability.
2025-11-01 22:30:08 +09:00
9dc4f63e40 Update namespace 2025-10-23 15:13:10 +09:00
28c386b0bb Refactor D3D12 Resource Management
Refactored and renamed components related to D3D12 graphics programming, replacing "descriptor" with "viewGroup" to improve resource grouping and management. Updated `D3D12CommandBuffer`, `D3D12DescriptorAllocator`, and `D3D12PipelineLibrary` to reflect these changes. Simplified material and shader creation in `D3D12ResourceAllocator`. Enhanced `D3D12ResourceDatabase` with resource naming for debugging and improved management. Refactored `Shader` and `ShaderPass` to use modern C# features and `IResourceReleasable` interface. Introduced `D3D12Utility` for centralized utility methods. Updated `Material` class for efficient buffer creation. Renamed `ShaderCompiler` to `SDLCompiler` with improved error handling. Updated `MeshRenderPass` to use new shader compilation process. Various improvements in error handling, code readability, and utility methods.
2025-10-23 14:42:53 +09:00
d2d9f5feb7 Refactor and enhance codebase for maintainability
Refactored and reorganized the codebase to improve readability, performance, and maintainability. Introduced new interfaces and structs for better resource management, updated project configuration files, and refactored shader and graphics pipeline management. Improved error handling, code formatting, and removed unused code and namespaces. Updated DLL references and method signatures for consistency and maintainability.
2025-10-22 18:46:39 +09:00
6d1b510ac1 Improve ecs query performance; 2025-10-12 19:49:05 +09:00
682200cbf1 Refactor and enhance graphics and audio systems
Updated target frameworks to .NET 10.0 across multiple projects for compatibility with the latest features. Refactored namespaces and introduced new classes for shader descriptors, FMOD integration, and DirectX 12 utilities using TerraFX. Replaced `Win32` bindings with TerraFX equivalents for DirectX 12. Added a C# wrapper for FMOD Studio API, including DSP and error handling. Enhanced entity queries, component storage, and query filters for better performance and type safety. Introduced new test projects and updated the solution structure. Added `meshoptimizer` bindings and integrated `meshoptimizer_native.dll`. Improved code readability, maintainability, and performance.
2025-10-09 05:16:28 +09:00
01a850ff94 Refactoring Rendering backend 2025-10-05 16:26:37 +09:00
a39f377533 Refactor GPU resource management and rendering pipeline
- Introduced `Handle<T>` and `Identifier<T>` for lightweight, strongly-typed resource identifiers.
- Replaced `BitSet` with `UnsafeBitSet` for improved performance and memory safety.
- Refactored `Mesh` and `Material` into `MeshClass` and `MaterialClass` for better GPU resource handling.
- Added `D3D12ResourceDatabase` to centralize GPU resource tracking and lifecycle management.
- Updated `D3D12ShaderCompiler` to load shaders from disk and dynamically populate constant buffers and textures.
- Enhanced `ICommandBuffer` with new upload operations for buffers and textures.
- Refactored `Vertex` struct for simplified memory layout and better performance.
- Updated `MeshBuilder` and rendering logic to align with new resource and shader structures.
- Added `BindlessDescriptor` support to `TextureHandle` and `BufferHandle`.
- Removed unused classes and performed general cleanup.
- Updated unit tests and demos to reflect the new architecture.
2025-09-19 23:20:15 +09:00
6a504cefc8 Migrate rendering from oop to dod 2025-09-16 20:55:20 +09:00
74bb2ccda5 Refactor descriptor handling and shader compilation
Refactored descriptor allocation and release logic by introducing `IDescriptorAllocator` and replacing `DescriptorHeapAllocator` with `D3D12DescriptorHeap`. Updated descriptor structs to include validation properties and improved memory management with `ReadOnlySpan`.

Enhanced shader compilation by introducing `ShaderStage` and `CompilerVersion` enums, enabling more flexible and maintainable shader handling.

Refactored `Mesh` to use `IBuffer` for vertex and index buffers, added bindless descriptor support, and improved resource cleanup.

Updated `RenderSystem` and other components for better initialization, error handling, and disposal logic. General improvements to code readability and maintainability.
2025-09-13 20:07:29 +09:00
1dfed83e38 Continue working on RHI 2025-09-12 21:44:32 +09:00
1b0ef03728 Remove unused Test folder. 2025-09-02 19:44:52 +09:00
78cc64b1d2 feat: Implement D3D12 resource factory and improve swap chain management
- Added D3D12ResourceFactory for creating render targets, textures, and buffers.
- Enhanced D3D12SwapChain to manage back buffer render targets and provide access to them.
- Updated D3D12Texture to utilize resource handles for better resource management.
- Removed legacy ResourceAllocator and integrated improvements for resource handling.
- Introduced new interfaces for resource factory and swap chain to streamline resource creation.
- Added support for mip levels and texture dimensions in render target and texture descriptions.
- Created new markdown files to document allocator and swap chain improvements.
2025-09-02 19:39:34 +09:00
5385141f14 Added new RHI abstraction layer;
Added new console debug page to UnitTest;
2025-08-25 10:48:59 +09:00
eafbfb2fa1 Update rendering architecture and resource management
Added a new `Ref<T>` struct for reference semantics.
Added the `RenderGraph` system for managing rendering passes.
Added the `RenderTexture` class for encapsulating GPU resources.
Added `GraphicsBuffer` class for effective GPU resource management.
Changed `CommandList` methods from public to internal for visibility control.
Changed `IRenderPass` interface from internal to public for accessibility.
Changed `GetData<T>()` in `ComponentObject.cs` to return `CompRef<T>`.
Changed `GetComponent<T>()` in `EntityManager.cs` to return `CompRef<T>`.
Changed `GetSingleton<T>()` in `World.cs` to use `CompRef<T>`.
Changed `IQueryTypeParameter` to use `CompRef<T>` for consistency.
Changed `QueryItem<T0>` and related structs to use `CompRef<T>`.
Changed `Material` class to support bindless textures.
Changed `Shader` class to support bindless rendering.
Changed `Mesh` class to support bindless vertex and index buffer access.
Updated documentation to reflect the new bindless rendering architecture.
2025-08-01 21:34:48 +09:00
1284bb17de Refactor graphics architecture and resource management
Added DescriptorAllocator.cs to manage descriptor allocations for Direct3D 12.
Added Texture2D.cs to handle 2D textures and GPU resource creation.
Added DescriptorAllocatorExample.cs to demonstrate the new descriptor allocator interface.

Changed project files to reference Misaki.HighPerformance.LowLevel instead of Misaki.HighPerformance.Unsafe.
Changed _renderView type from IRenderer? to Renderer? in ScenePage.xaml.cs.
Changed EngineCore.cs to remove explicit graphics API specification during initialization.
Changed Logger.cs to enhance the Assert method with a DoesNotReturnIf attribute.
Changed resource types in Mesh.cs from IResource to GraphicsResource.

Removed multiple interfaces including ICommandBuffer, IDebugLayer, IGraphicsDevice, IPipelineResource, IRenderPass, IRenderer, IResource, and IResourceAllocator to simplify the graphics architecture.
Removed D3D12DebugLayer class from DebugLayer.cs to streamline the debug layer implementation.

Updated CommandList.cs and D3D12CommandBuffer.cs to implement a new command list structure for Direct3D 12.
Updated Material.cs to improve handling of constant buffers and textures.
Updated Shader.cs to include new structures for texture and property information.
Updated GraphicsPipeline.cs to support the new graphics device and resource management system.
Updated UnitTestAppWindow.xaml.cs to reflect changes in the renderer type and ensure proper resource management.
Updated BindlessMeshRenderPass.cs and MeshRenderPass.cs to implement modern rendering techniques, including bindless textures and improved shader management.
Updated CBufferCache.cs to align with the new resource management system and improve memory handling.
2025-07-12 14:25:20 +09:00
eed1b9d3d0 Refactor graphics engine dependencies and structure
Removed references to `Misaki.HighPerformance.Unsafe` and replaced them with `Misaki.HighPerformance.LowLevel` in multiple files.
Removed calls to `AllocationManager.Initialize()` and `AllocationManager.Dispose()` in `EngineCore.cs`.
Added new methods to the `ICommandBuffer` interface for enhanced rendering capabilities.
Updated the `D3D12CommandBuffer` class to implement new graphics command handling methods.
Added the `Material` class to manage shader properties and caching for improved performance.
Encapsulated shader compilation and reflection processes within the `Shader` class for better organization.
Added the `CBufferCache` struct to optimize GPU resource management for constant buffer data.
Updated the `MeshRenderPass` class to utilize the new `Material` class for dynamic mesh rendering.
Updated various project files to reflect the restructuring of dependencies.
Modified XAML files and code-behind for improved readability and maintainability.
2025-07-07 22:59:47 +09:00
261afa4133 Update rendering and resource management
Changed the `EditorState` class to use a timeout in the `WaitForGPUReady` method for improved responsiveness.
Changed the `nativeDebugging` setting in `launchSettings.json` to `false` for the "Ghost.Editor (Package)" profile.
Changed the `D3D12Renderer` class to set the swap chain only for the composition target type and replaced back buffer reset with dispose.
Changed the mapping of resources in `D3D12Resource` to use a pointer for improved safety and clarity.
Changed the `Mesh` class's upload buffer creation to not use the `true` flag for better memory management.
Added a new `Vertex` struct with a `StructLayout` attribute for improved interoperability with unmanaged code.
Refactored the `GraphicsPipeline` class to replace `IsGpuReady` with `WaitForGPUReady`, including a timeout parameter.
Added a constant buffer to the HLSL source code in `MeshRenderPass` for passing transformation matrices to the vertex shader.
Expanded the `UnitTestAppWindow` class to include event handlers for window activation and size changes for better resource management.
Updated the XAML for `UnitTestAppWindow` to include a `SwapChainPanel` and corrected the XML declaration for formatting consistency.
2025-07-03 23:23:46 +09:00
5ae4128baf Enhance graphics engine and code organization
Added `InternalsVisibleTo` attribute for "Ghost.Graphics" and "Ghost.Editor" in `AssemblyInfo.cs`.
Added a new `EngineAssemblyAttribute` in `EngineAssemblyAttribute.cs`.
Added a reference to `Misaki.HighPerformance.Unsafe` in `Ghost.Core.csproj`.
Added a new `Bounds` struct to represent axis-aligned bounding boxes in `Bounds.cs`.
Added new `Color32` and `Color128` structs for color representation in `Color.cs`.

Changed the namespace from `Ghost.Editor.Controls` to `Ghost.Editor.Core.Controls` in multiple files.
Changed the implicit conversion operator in `ConstPtr<T>` to use a more descriptive parameter name in `ConstPtr.cs`.
Changed the `Mesh` class to use `Color128` instead of `Color32` for color representation.

Enhanced the `TypeCache` class to load types from assemblies marked with `EngineAssemblyAttribute`.
Enhanced the `ProjectService` class to improve the `GetAllProjectAsync` method by filtering out bad projects.
Enhanced the `GraphicsPipeline` class to support both DX12 and D3D12 graphics APIs.
Enhanced the `Shader` class to include methods for compiling HLSL shaders and managing root signatures.
Enhanced the `MeshRenderPass` class to utilize the new shader compilation methods.

Refactored the `AppStateMachine` class to use private fields instead of static fields for state management.
Refactored the `ComponentDataView` class to use the new namespace and improve organization.
Refactored project references in `Ghost.Graphics.csproj` to include new dependencies and remove outdated ones.

Made various adjustments to ensure consistency and improve code quality across multiple files.
2025-07-02 21:30:10 +09:00
300ae7251b Add new interfaces and refactor rendering logic
Added a new `ConstPtr<T>` struct for type-safe pointers.
Added a new `ICommandBuffer` interface for resource copying.
Added a new `IRenderPass` interface to define render passes.
Added a new `IResource` interface for GPU resources.
Added a new `IResourceAllocator` interface for resource management.
Added a new `ISwapChainPanelNative` struct for native interactions.
Added a new `D3D12Utility` class for Direct3D 12 utilities.
Added a new package reference for `Vortice.Win32.Graphics.D3D12MemoryAllocator`.

Changed project file to allow unsafe code blocks.
Changed `Result` struct methods to improve clarity.
Changed error handling in `ProjectService` and `AssetDatabase` to use `Result.Failure()`.
Changed `launchSettings.json` to enable native debugging.
Changed rendering logic in `ScenePage.xaml.cs` to use `IRenderer`.
Changed `IGraphicsDevice` interface to include renderer properties.
Changed `IRenderView` to `IRenderer` and updated its methods.
Changed `Mesh` class to use the new `IResource` interface for buffers.
Changed `GraphicsAPI` enum to include a `None` value.
Changed various aspects of the `GraphicsPipeline` class for new architecture.

Removed the old `DX12RenderView` class and replaced it with `DX12Renderer`.
Removed unnecessary code in the `ResourceView` class.
2025-06-30 13:50:06 +09:00
8fd1222780 Refactor AppState and rendering pipeline components
Changed the `AppStateMachine` to implement `IDisposable` and `IAsyncDisposable` for better resource management.
Changed the `IAppState` interface to include asynchronous methods for state transitions.
Changed the `App` class to start the host asynchronously and added an `OnClosed` method for proper shutdown.
Changed the `EditorState` class to ensure the window closes correctly when exiting the state.
Changed the `LandingState` class to improve window activation and deactivation management.
Changed the `HostHelper` class to register `LandingWindow` and `EngineEditorWindow` as singletons for better performance.
Changed the `ScenePage` class to utilize a new interface for swap chain management.
Changed the `OpenProjectPage` and `CreateProjectPage` classes to enhance navigation handling.
Changed the `ConsoleViewModel` to improve log update handling with a new context structure.
Changed the `OpenProjectViewModel` to clear project lists when navigating away.
Changed the `EngineCore` class to start the graphics pipeline asynchronously.
Changed the `Logger` class to use a new context structure for log changes.
Added the `ICommandBuffer`, `IGraphicsDevice`, and `IRenderView` interfaces to enhance the rendering pipeline.
Changed the `DX12CommandBuffer`, `DX12GraphicsDevice`, and `DX12RenderView` classes for improved resource management and rendering efficiency.
Refactored the `Mesh` class to use a new `Vertex` structure for simplified vertex management.
Added the `TextureUtility` class for texture management utilities, including mip count calculation.
Changed the `launchSettings.json` to include a new profile for the graphics project with native debugging enabled.
Changed the `MeshBuilder` class to utilize the new `Vertex` structure for vertex creation.
2025-06-29 11:38:29 +09:00
4110c166cf Refactor Vector3Field and update project structure
Changed Vector3Field.cs to derive from ValueControl<Vector3> and use NumberBox controls for better UI handling.
Changed EditorControls.xaml to update resource paths for new controls.
Changed InternalControls.xaml to simplify the resource dictionary by removing unnecessary references.
Changed IComponentEditor.cs to reflect updates in the component editor's lifecycle methods.
Changed project files for Ghost.Editor and Ghost.Core to include new dependencies and project references.
Changed FileExtensions.cs and IInspectorService.cs to align with the new namespace structure.
Changed Result.cs to enhance error handling and success checking methods.
Changed TypeHandle.cs to improve type handling compatibility.
Changed AssemblyInfo.cs files to include new assembly visibility attributes for better encapsulation.
Added new graphics-related classes and interfaces in the Ghost.Engine project, including IGraphicsDevice and DX12GraphicsDevice.
Added a new Mesh class to handle 3D mesh data and provide methods for creating geometric shapes.
Added GraphicsPipeline.cs to manage the graphics rendering loop and device initialization.
Added ScenePage.xaml and ScenePage.xaml.cs to create a new page for rendering scenes.
Updated HierarchyPage.xaml.cs and InspectorPage.xaml.cs to use the new service locator pattern for service retrieval.
Updated LandingWindow.xaml.cs and EngineEditorWindow.xaml.cs to utilize the new service locator pattern for better service access.
Updated Logger.cs to enhance logging capabilities with optional stack traces and assertion logging.
Updated QueryFilter.cs and QueryEnumerable.cs to use the new TypeHandle structure for improved efficiency.
Updated WorldNode.cs and WorldNodeSerializer.cs to enhance serialization and management of world nodes.
Updated AssetDatabase and related classes to improve asset management and metadata generation.
Updated Ghost.UnitTest.csproj to include new project references and package dependencies for unit tests.
2025-06-27 20:02:02 +09:00
1724072f7e Add component editors and UI controls
Added the `HierarchyEditor` and `LocalToWorldEditor` classes to implement custom component editing functionality.
Added the `Vector3Field` control for 3D vector manipulation and its corresponding XAML definition.
Added the `ComponentDataView` and `ComponentObject` classes to manage component data display and access.
Added the `CustomEditorAttribute` to mark classes as custom editors for specific components.

Changed the `IInspectable` interface to use properties for `Icon`, `HeaderContent`, and `InspectorContent`.
Changed the `PropertyField` class to enhance UI control binding capabilities.
Changed the `EditorWorldManager` to improve world data loading and deserialization processes.
Changed the `EntityNode` and `WorldNode` classes to update entity construction and component querying.
Changed the `StaticResource` class to include new binding flags for component properties.
Changed the `InspectorService` to remove old contract references and adopt new interfaces.
Changed the `QueryEnumerable` and related files to update generic constraints for improved type safety.
Changed the `QueryItem` class to reflect new generic constraints and enhance deconstruction.
Changed the `World.Query` methods to utilize the updated generic constraints.

Updated the `SerializationTest` to align with new entity creation and management practices.
2025-06-20 20:19:14 +09:00
fc44c73ca8 Refactor project from Ghost.App to Ghost.Editor
Changed the project structure to reflect a shift from `Ghost.App` to `Ghost.Editor`, updating namespaces and class names throughout.

Changed the application class in `App.xaml` and `App.xaml.cs` from `GhostApplication` to `EditorApplication`.

Changed several service interfaces to reside under `Ghost.Editor.Services.Contracts`, including `IInspectorService`, `INotificationService`, and `IProgressService`.

Added `InspectorView` and `InspectorViewModel` classes to manage inspector functionality.

Added `NavigationTabView` and `NavigationTabPage` classes to facilitate navigation within the editor.

Enhanced `WorldNode` and `EntityNode` classes to support scene graph functionality, including serialization and entity management.

Updated the project file `Ghost.Editor.csproj` to reflect the new structure and removed old references.

Modified the solution file `GhostEngine.sln` to remove references to `Ghost.App` and include `Ghost.Editor`.

Updated unit tests to align with the new namespaces and project structure.
2025-06-17 19:37:30 +09:00
ff14c0f49a Refactor application structure and add unit tests
Added:
- New `ProgressService` class for managing progress indicators.
- New `AssetDatabase`, `AssetOpenHandlerAttribute`, and `AsyncAssetOpenHandlerAttribute` classes for asset handling.
- `Ghost.UnitTest` project for unit testing with associated files and configurations.

Changed:
- `ActivationHandler` class to ensure correct handling of `LaunchActivatedEventArgs`.
- `App.xaml.cs` to register `INotificationService` and `IProgressService`, replacing `StackedNotificationService`.
- `OnLaunched` method in `App.xaml.cs` to correctly call `ActivationHandler.Handle(args)` and start the host.
- `INavigationAware` interface from internal to public for broader access.
- `EditorState.cs` to activate `EditorApplication` with the current service provider.
- Property names in `AssetItem` and `ExplorerItem` structs to `Name` and `FullName`.
- `NotificationService` class to implement `INotificationService` and refactor notification handling.
- `AssetPathToGlyphConverter` to handle file extensions consistently.
- Bindings in `ProjectPage.xaml` and `ProjectPage.xaml.cs` to use `FullName` instead of `Path`.
- `EngineEditorWindow` and `LandingWindow` classes to utilize new notification and progress services.
- `Logger` class to include a new method for logging errors with exceptions.

Updated:
- Manifest files and project files to reflect new structure and dependencies.
- Solution file `GhostEngine.sln` to include the new unit test project.
- Added several new test classes and methods in `UnitTests.cs`.
2025-06-10 16:32:32 +09:00
40d333b004 Refactor project structure and enhance functionality
Changed the project namespace from `Ghost.Editor` to `Ghost.App` across multiple files.
Changed the `InternalsVisibleTo` attribute in `AssemblyInfo.cs` to include `Ghost.App`.
Changed the `ProjectRepository` class to add new asynchronous methods for retrieving projects by ID, name, and metadata path.
Changed the `ProjectService` class to utilize the new asynchronous project loading methods.
Changed the `SceneGraph` classes to improve node management and serialization.
Changed the `EntityManager` class to enhance entity management with new component handling methods.
Added new test classes, `EntityTest` and `SerializationTest`, to ensure reliability in entity and serialization systems.
Added the `Ghost.App` project file to establish a modular project structure.
Added the `Ghost.Generator` project for automated component serialization code generation.
Updated UI components to reflect the new namespace for proper functionality.
2025-06-07 20:54:07 +09:00
bab3be2508 Refactor project structure and improve performance
Changed the `ProjectRepository` class to be static for easier usage.
Changed `ProjectService` constants to public properties for accessibility.
Changed `App.xaml` to consolidate theme resources into `Override.xaml`.
Changed `App.xaml.cs` to implement an `AppStateMachine` for better state management.
Changed `ConsolePage` and `HierarchyPage` to utilize the new ViewModel structure.
Changed `ProjectPage` to use the `ExplorerItem` model for asset display.
Changed `Entity` and `EntityManager` to enhance component management with a new `IComponentData` interface.
Changed the `Logger` class to introduce structured logging functionality.
Changed the system architecture to support dependency management for better organization.
Changed the `QueryEnumerable` class to allow for more flexible entity queries.
Changed the `TypeHandle` class to improve efficiency in retrieving type handles.
Changed the `World` class to support robust world management and multiple worlds.
Updated the `Test` class to demonstrate the new entity and component management system.
2025-06-05 21:45:50 +09:00
61bbb1bc68 Refactor project structure and enhance functionality
Added `InternalsVisibleTo` attribute for "Ghost.Editor" in `AssemblyInfo.cs`.
Added a binary file `Empty.zip` to the project.
Added a new `ProjectMetadata` class in `ProjectMetadata.cs`.
Added new states and interfaces for managing application states in `EditorState.cs`, `LandingState.cs`, and `IAppState.cs`.
Added a notification service in `INotificationService.cs` and `StackedNotificationService.cs`.
Added new XAML files for UI components, including `InspectorView.xaml` and `InternalControls.xaml`.

Changed the `ProjectInfo` class in `ProjectInfo.cs` to include a `MetadataPath` property instead of `Path` and `EngineVersion`.
Changed the `TemplateInfo` class in `TemplateInfo.cs` to use a struct instead of a class for `TemplateData`.
Changed the `ProjectService` class to use the new `ProjectRepository` for managing project data.

Removed several using directives and the entire `ProjectRepository` class from `ProjectRepository.cs`, replacing it with a new implementation.
Removed old methods and properties in `EntityManager` and `World` classes to improve entity management and component handling.

Updated the `Ghost.Data.csproj` file to include the new `Empty.zip` file as a content item.
Updated the `ProjectRepository` class to manage project data using SQLite.
Updated various XAML files to include new styles and controls, improving the overall UI design.
Updated the `CreateProjectViewModel` to include a notification service and handle project creation logic.
Updated the test project to include references to the new `Ghost.Graphics` project and modified test cases to align with the new structure.
2025-05-31 01:45:34 +09:00
67b6040b5e Refactor entity-component system and related classes
Changed the `Component` class to an interface `IComponentData` to support a data-oriented design.
Changed the `Transform` class from a class to a struct, implementing `IComponentData` and updating properties.
Changed the `GameObject` class to use a dictionary for components and added properties for state management.
Changed the `PlayerLoopService` class to `GameLoopService` and updated methods to integrate with the new `SceneManager`.
Changed the `World` class to manage multiple worlds and enhance entity management with new querying methods.

Added the `Scene` class to manage root game objects and their lifecycle.
Added new utility classes like `ComponentMask`, `Box<T>`, and `TypeHandle<T>` for better component management.
Added the `ScriptComponent` class to allow for modular scriptable components attached to entities.
Added the `QueryEnumerable` class to facilitate flexible querying of entities with specific components.

Updated the `Test` class in `Program.cs` to demonstrate the new entity and component management system.
Updated project files to include new references and settings supporting the changes made in the codebase.
2025-05-28 15:21:43 +09:00
0cf3104a6a Implement core entity management features
Added `Archetype` struct with chunk management and disposal.
Added `BitSet` class for managing collections of bits.
Added `Class1.cs` as a placeholder for graphics functionality.
Added `ComponentData` struct and `ComponentPool` class for component management.
Added `ComponentRegistry` for efficient component registration.
Added `EntityChangeQueue` as a placeholder for future changes.
Added `Helpers.ttinclude` and `QueryRefComponent.tt` for code generation.
Added `Signature` struct for managing component signatures.
Added `World` struct to manage the game world and entities.
Added `QueryRefComponent` delegates for querying entities.

Changed `Archetype.cs` to implement `IDisposable`.
Changed `AssemblyInfo.cs` to update global using directives.
Changed `Chunk.cs` to introduce `ChunkCollection` for chunk management.
Changed `Component.cs` to refine component management methods.
Changed `Entity.cs` to improve properties and methods.
Changed `Ghost.Entities.csproj` to update project properties.
Changed `Program.cs` to demonstrate entity creation and querying.
Changed `World.Query.cs` to facilitate querying with components.
2025-05-21 11:46:48 +09:00
56a21bab2b Removed InternalsVisibleTo and project configuration
Removed the `InternalsVisibleTo` attribute from `AssemblyInfo.cs`, restricting visibility of internal types to "Ghost.Engine". Removed the SDK declaration and property group from `Ghost.Game.csproj`, indicating a significant change in project structure and configuration.
2025-04-05 16:09:26 +09:00
7cd881b7d4 Refactor project management and enhance architecture
Added a new static class `AssetsPath` for asset management.
Added a new icon file (`icon-256.ico`) for UI representation.
Added new package references to enhance functionality.
Added internals visibility attributes for better testing.
Added a new `EngineEditorViewModel` class for MVVM support.
Added a new `GameObject` class for component management.
Added a new `BitSet` class for efficient bit manipulation.
Added various utility classes to support the new entity system.

Changed the `ID` property in `ProjectInfo` to internal.
Changed the `AddProjectAsync` method to return the created `ProjectInfo`.
Changed the connection string retrieval method to use the new `Command` constant.
Changed the `DataPath` class to use `readonly` fields for folder paths.
Changed the `ActivationHandler` class to use new `DataPath` constants.
Changed the `OpenProjectPage` layout and interaction for better UI.

Updated the target framework to a newer version for compatibility.
Updated the `ProjectService` to use new constants from `DataPath`.
Updated the `World` class to improve entity management.

Refactored the `ProjectRepository` class to encapsulate SQL commands.
Refactored the `Transform` class to use properties for better encapsulation.
2025-04-05 16:07:53 +09:00
62fe30ff2b Refactor activation handling and introduce entity system
Added new `ActivationHandler` class for folder initialization.
Added `ProjectService` class for project-related operations.
Added `Ghost.Entities` project with entity management classes.
Added `EngineEditorWindow` for enhanced user interface.

Changed project files to restructure dependencies and remove unused references.
Changed `ProjectRepository` to use asynchronous methods for improved performance.
Changed data binding in `CreateProjectPage.xaml` and `OpenProjectPage.xaml` to use new data models.
Changed `App.xaml.cs` to utilize the new `ActivationHandler` and include additional services.

Removed `IActivationHandler` interface and integrated its functionality into `ActivationHandler`.
Removed `EditorActivationHandler` as its functionality was merged into `ActivationHandler`.

Updated `AssemblyInfo.cs` to include global using directives for entity types.
Updated image assets to reflect visual resource changes.
2025-03-27 00:52:07 +09:00
02b3edcd7a Refactor project structure and enhance UI components
Changed connection string format in ProjectRepository.cs and updated application data path retrieval.

Added project reference to Ghost.Engine in Ghost.Database.csproj.

Added new property Packages in TemplateInfo.cs.

Changed XamlControlsResources source in App.xaml for clarity.

Changed App.xaml.cs to include new Host property and updated OnLaunched method.

Removed unused image files related to branding.

Updated Ghost.Editor.csproj to change target framework and organize content files.

Changed display name in Package.appxmanifest from "Ghost.Editor" to "GhostEngine".

Enhanced UI layout and data binding in CreateProjectPage.xaml and its code-behind.

Changed LandingWindow to use WindowEx for improved functionality.

Updated CreateProjectViewModel to implement INavigationAware for better navigation handling.

Updated AssemblyInfo.cs for internal visibility to Ghost.Database.

Added new files for activation handling and game object management in Ghost.Core and Ghost.Engine.

Introduced SystemUtilities for folder picker dialog functionality.

Created PropertyField control with corresponding XAML for UI consistency.

Added TemplateInfoWarper for managing template information.

Introduced HostHelper class to set up application services.

Overall, these changes reflect a significant restructuring of the project, enhancing architecture, improving UI components, and establishing clearer separation of concerns.
2025-03-26 01:18:16 +09:00
23a08bc8e0 Refactor application structure and add database support
Changed App.xaml.cs to implement dependency injection with Microsoft.Extensions.Hosting, initializing services for LandingWindow and LandingViewModel.

Removed MainWindow.xaml and MainWindow.xaml.cs, shifting to a new landing window structure.

Added Ghost.Database project with necessary database functionality and created ProjectRepository for project management.

Added ProjectInfo and TemplateInfo data models for project attributes.

Added CreateProjectViewModel and LandingViewModel for managing view models using Community Toolkit for MVVM.

Created new XAML pages for project creation, opening projects, and the landing window, along with their corresponding code-behind files.

Added AssemblyInfo.cs for internal visibility to facilitate testing.
2025-03-25 13:13:04 +09:00
356 changed files with 65656 additions and 164 deletions

13
.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
[*]
max_line_length = 400
[*.cs]
csharp_new_line_before_open_brace = all
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
csharp_style_prefer_primary_constructors = false
dotnet_sort_system_directives_first = false
dotnet_separate_import_directive_groups = false
dotnet_style_prefer_collection_expression = false
dotnet_style_collection_initializer = false

1
.gitignore vendored
View File

@@ -35,6 +35,7 @@ bld/
# Visual Studio 2015/2017 cache/options directory # Visual Studio 2015/2017 cache/options directory
.vs/ .vs/
.vscode/
# Uncomment if you have tasks that create the project's static files in wwwroot # Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/ #wwwroot/

297
AGENTS.md Normal file
View File

@@ -0,0 +1,297 @@
# GhostEngine - Agent Development Guide
This guide provides essential information for AI coding agents working on the GhostEngine codebase.
## Project Overview
- **Type**: Game Engine
- **Language**: C#
- **Target Framework**: .NET 10.0
- **Special Features**: ECS architecture, D3D12 rendering, AOT compilation, WinUI 3 editor
- **Platform**: Windows (net10.0-windows10.0.22621.0 for editor projects)
## Build Commands
### Build Entire Solution
```bash
dotnet build GhostEngine.slnx
```
### Build Specific Project
```bash
dotnet build Ghost.Entities/Ghost.Entities.csproj
dotnet build Ghost.Editor/Ghost.Editor.csproj
```
### Build with Configuration
```bash
dotnet build GhostEngine.slnx -c Release
dotnet build GhostEngine.slnx -c Debug
```
### Clean Build
```bash
dotnet clean GhostEngine.slnx
dotnet build GhostEngine.slnx
```
## Test Commands
### Run All Tests (Custom Framework)
Tests use a custom test framework (not xUnit/NUnit/MSTest). Each test project is an executable.
```bash
# Run entity tests
dotnet run --project Ghost.Entities.Test/Ghost.Entities.Test.csproj
# Run shader tests
dotnet run --project Ghost.Shader.Test/Ghost.Shader.Test.csproj
```
### Run Single Test
Tests implement `ITest` interface. To run a specific test, modify the test project's `Program.cs`:
```csharp
// In Ghost.Entities.Test/Program.cs
TestRunner.Run<EntityQueryTest>(); // Run specific test
TestRunner.Run<EntityQueryTest>(10); // Run with 10 iterations
```
### Visual Tests (Graphics)
Graphics tests use WinUI 3 and require running as packaged apps:
```bash
dotnet run --project Ghost.Graphics.Test/Ghost.Graphics.Test.csproj
```
## Code Style Guidelines
### Formatting (from .editorconfig)
- **Braces**: Allman style - all opening braces on new lines
- **Line Length**: Max 400 characters (very permissive)
- **Single-line statements**: Preserved (allowed)
- **Single-line blocks**: Preserved (allowed)
```csharp
// Correct brace style
public void Method()
{
if (condition)
{
DoSomething();
}
}
```
### Imports
- **System directives**: NOT sorted first (dotnet_sort_system_directives_first = false)
- **No grouping**: Import directives not separated by blank lines
- **Order**: Organize by project convention, not alphabetically
```csharp
using Ghost.Core;
using Ghost.Entities;
using Misaki.HighPerformance.Collections;
using System.Diagnostics;
using TerraFX.Interop.DirectX;
```
### Types and Nullability
- **Nullable**: Enabled for all projects
- **Implicit usings**: Enabled
- **Unsafe code**: Allowed in most projects (AllowUnsafeBlocks = True)
- **Primary constructors**: NOT preferred (csharp_style_prefer_primary_constructors = false)
### Naming Conventions
- **Classes/Interfaces**: PascalCase (`EntityManager`, `ICommandBuffer`)
- **Methods**: PascalCase (`CreateEntity`, `GetComponent`)
- **Properties**: PascalCase (`IsSuccess`, `Value`)
- **Fields (private)**: Camel case with underscore prefix (`_entityLocations`, `_world`)
- **Fields (public/internal)**: Camel case, no prefix for struct fields (`archetypeID`, `chunkIndex`)
- **Type parameters**: Single letter or PascalCase (`T`, `TComponent`)
- **Constants**: PascalCase (no SCREAMING_SNAKE_CASE)
```csharp
public class EntityManager
{
private readonly World _world; // Private field
private UnsafeSlotMap<EntityLocation> _entityLocations;
public World World => _world; // Property
public Entity CreateEntity() { } // Method
}
internal struct EntityLocation // Struct
{
public int archetypeID; // Public struct field
public int chunkIndex;
}
```
### Error Handling
**Use Result Types** - Railway-oriented programming pattern:
```csharp
// Custom result types defined in Ghost.Core
public ErrorStatus DoOperation()
{
return ErrorStatus.None; // or ErrorStatus.NotFound, etc.
}
public Result<T> GetValue()
{
if (success)
return Result<T>.Success(value);
else
return Result<T>.Failure("Error message");
}
public Result<T, ErrorStatus> GetValueWithStatus()
{
if (success)
return value; // Implicit conversion
else
return ErrorStatus.NotFound; // Implicit conversion
}
// Extension methods for checking results
result.ThrowIfFailed();
var value = result.GetValueOrThrow();
var value = result.GetValueOrDefault(defaultValue);
if (result.TryGetValue(out var value)) { }
```
**Error Status Values**: None, NotFound, InvalidArgument, InvalidState, InternalError, PermissionDenied, NotSupported, OutOfMemory, Timeout, Cancelled, UnknownError
### Memory and Performance
- **Use unsafe code** when needed for performance-critical paths
- **Span<T> and stackalloc**: Prefer for temporary allocations
- **ref returns**: Use for zero-copy access to internal data
- **Allocator patterns**: Use `Allocator.Persistent` for long-lived allocations
- **AllocationManager**: Create stack scopes for temporary allocations
```csharp
// Stack allocation pattern
var entities = (Span<Entity>)stackalloc Entity[1];
// Using allocation scope
using var scope = AllocationManager.CreateStackScope();
var batchDestroy = new UnsafeList<EntityLocation>(entities.Length, scope.AllocationHandle);
// Ref returns for zero-copy access
public ref T GetSingleton<T>() where T : unmanaged, IComponent
{
var ptr = GetSingleton(ComponentTypeID<T>.Value);
return ref *(T*)ptr;
}
```
### Type Safety Patterns
**Strongly-typed identifiers**:
```csharp
Identifier<IComponent> componentID;
Identifier<Archetype> archetypeID;
Handle<T> resourceHandle;
```
**Generic constraints**:
```csharp
public void Method<T>() where T : unmanaged, IComponent
public void Method<T, E>() where E : struct, Enum
```
### Documentation
- **XML comments**: Required for public APIs
- **Summary tags**: Describe what, not how
- **Remarks**: Add for complex behavior, thread-safety warnings, structural changes
```csharp
/// <summary>
/// Create an entity with specified components.
/// </summary>
/// <param name="set">A set of component space IDs to add to the entities.</param>
/// <returns>The created entity.</returns>
/// <remarks>
/// This method causes structural changes and is not thread-safe.
/// Use <see cref="EntityCommandBuffer"/> to defer changes.
/// </remarks>
public Entity CreateEntity(ComponentSet set) { }
```
### Common Patterns
**ECS Component Registration**:
```csharp
// Type-safe component ID
ComponentTypeID<Transform>.Value
// Component sets for archetypes
var set = new ComponentSet(ComponentTypeID<Transform>.Value, ComponentTypeID<Velocity>.Value);
```
**Disposal Pattern**:
```csharp
private bool _disposed;
~MyClass()
{
Dispose();
}
public void Dispose()
{
if (_disposed) return;
// Cleanup code
_disposed = true;
GC.SuppressFinalize(this);
}
```
**Debug-only validation**:
```csharp
#if DEBUG || GHOST_EDITOR
if (!_isSuccess)
{
throw new InvalidOperationException($"Error: {_message}");
}
#endif
```
## Architecture Notes
### Entity Component System (ECS)
- Archetype-based storage (similar to Unity DOTS)
- Component data stored in chunks
- Queries use bitset signatures for fast matching
- Structural changes move entities between archetypes
### Graphics (D3D12)
- Hardware abstraction via `ICommandBuffer`
- Resource lifetime managed via handles
- Pipeline state objects (PSO) cached in library
- Native interop via TerraFX.Interop
### Custom Dependencies
- `Misaki.HighPerformance.*`: High-performance collections and utilities
- `TerraFX.Interop.*`: Native Windows/DirectX interop
- Custom source generators in `Ghost.Generator`
## Important Rules
1. **Never disable nullable warnings** - fix the root cause
2. **Use Result types** instead of throwing exceptions for expected failures
3. **Document thread-safety** in XML comments for public APIs
4. **AllowUnsafeBlocks** is enabled - use unsafe code when it improves performance
5. **Avoid collection expressions/initializers** (disabled in .editorconfig)
6. **Prefer explicit over implicit** - clarity over brevity
7. **Test changes** by running the appropriate test project executable

View File

@@ -0,0 +1,7 @@
using Ghost.Core.Attributes;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ghost.Graphics")]
[assembly: InternalsVisibleTo("Ghost.Engine")]
[assembly: EngineAssembly]

View File

@@ -0,0 +1,6 @@
namespace Ghost.Core.Attributes;
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class EngineAssemblyAttribute : Attribute
{
}

View File

@@ -0,0 +1,11 @@
namespace Ghost.Core.Contracts;
public interface ICloneable
{
object Clone();
}
public interface ICloneable<T>
{
T Clone();
}

View File

@@ -0,0 +1,6 @@
namespace Ghost.Core.Contracts;
internal interface IReleasable
{
void InternalRelease();
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
<DefineConstants>$(DefineConstants);PLATEFORME_WIN64</DefineConstants>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
<DefineConstants>$(DefineConstants);PLATEFORME_WIN64</DefineConstants>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.4" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="1.2.2" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.3.3" />
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.1" />
<PackageReference Include="System.IO.Hashing" Version="10.0.1" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />
<PackageReference Include="ZLinq" Version="1.5.4" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,110 @@
namespace Ghost.Core.Graphics;
public enum ZTest : byte
{
Disabled,
Less,
LessEqual,
Equal,
GreaterEqual,
Greater,
NotEqual,
Always
}
public enum ZWrite : byte
{
Off,
On
}
public enum Cull : byte
{
Off,
Front,
Back
}
public enum Blend : byte
{
Opaque,
Alpha,
Additive,
Multiply,
PremultipliedAlpha
}
[Flags]
public enum ColorWriteMask : byte
{
None = 0,
Red = 1 << 0,
Green = 1 << 1,
Blue = 1 << 2,
Alpha = 1 << 3,
All = Red | Green | Blue | Alpha
}
public struct PipelineState
{
public ZTest ZTest
{
get; set;
}
public ZWrite ZWrite
{
get; set;
}
public Cull Cull
{
get; set;
}
public Blend Blend
{
get; set;
}
public ColorWriteMask ColorMask
{
get; set;
}
public static PipelineState Default => new PipelineState
{
ZTest = ZTest.LessEqual,
ZWrite = ZWrite.On,
Cull = Cull.Back,
Blend = Blend.Opaque,
ColorMask = ColorWriteMask.All
};
public readonly ulong GetHashCode64()
{
// 32-bit packed key for states controlled by material / overrides.
// layout:
// 0..3 Blend (4 bits)
// 4..6 Cull (3 bits)
// 7..10 DeafaultState (4 bits)
// 11 ZWrite (1 bit)
// 12..15 ColorMask (4 bits)
var key = 0u;
key |= ((uint)Blend & 0xFu) << 0;
key |= ((uint)Cull & 0x7u) << 4;
key |= ((uint)ZTest & 0xFu) << 7;
key |= ((uint)ZWrite & 0x1u) << 11;
key |= ((uint)ColorMask & 0xFu) << 12;
return key;
}
public override readonly int GetHashCode()
{
var code64 = GetHashCode64();
return ((int)code64) ^ (int)(code64 >> 32);
}
}

View File

@@ -0,0 +1,100 @@
namespace Ghost.Core.Graphics;
public enum KeywordSpace
{
Local,
Global,
}
public enum ShaderPropertyType
{
None,
Float, Float2, Float3, Float4,
Float4x4,
Int, Int2, Int3, Int4,
UInt, UInt2, UInt3, UInt4,
Bool, Bool2, Bool3, Bool4,
Texture2D, Texture3D, TextureCube,
Texture2DArray, TextureCubeArray,
Sampler
}
public struct ShaderEntryPoint
{
public string entry;
public string shader;
public readonly bool IsCreated => !string.IsNullOrEmpty(entry) && !string.IsNullOrEmpty(shader);
}
public struct KeywordsGroup
{
public KeywordSpace space;
public List<string> keywords;
}
public struct PropertyDescriptor
{
public ShaderPropertyType type;
public string name;
public object? defaultValue;
}
public struct PassDescriptor
{
public string identifier;
public string name;
public ShaderEntryPoint taskShader;
public ShaderEntryPoint meshShader;
public ShaderEntryPoint pixelShader;
public string[] defines;
public string[] includes;
public KeywordsGroup[] keywords;
public PipelineState localPipeline;
public string? hlsl;
}
public class ShaderDescriptor
{
public string name = string.Empty;
public uint cbufferSize;
public PropertyDescriptor[] globalProperties = null!;
public PropertyDescriptor[] properties = null!;
public PassDescriptor[] passes = null!;
public string? hlsl;
}
public static class ShaderDescriptorExtensions
{
public static uint GetSize(this ShaderPropertyType type)
{
return type switch
{
ShaderPropertyType.Float => 4,
ShaderPropertyType.Float2 => 8,
ShaderPropertyType.Float3 => 12,
ShaderPropertyType.Float4 => 16,
ShaderPropertyType.Float4x4 => 64,
ShaderPropertyType.Int => 4,
ShaderPropertyType.Int2 => 8,
ShaderPropertyType.Int3 => 12,
ShaderPropertyType.Int4 => 16,
ShaderPropertyType.UInt => 4,
ShaderPropertyType.UInt2 => 8,
ShaderPropertyType.UInt3 => 12,
ShaderPropertyType.UInt4 => 16,
ShaderPropertyType.Bool => 4,
ShaderPropertyType.Bool2 => 8,
ShaderPropertyType.Bool3 => 12,
ShaderPropertyType.Bool4 => 16,
ShaderPropertyType.Texture2D => 4, // Bindless resource use uint32
ShaderPropertyType.Texture3D => 4,
ShaderPropertyType.TextureCube => 4,
ShaderPropertyType.Texture2DArray => 4,
ShaderPropertyType.TextureCubeArray => 4,
ShaderPropertyType.Sampler => 4,
_ => 0,
};
}
}

242
Ghost.Core/Handle.cs Normal file
View File

@@ -0,0 +1,242 @@
namespace Ghost.Core;
public readonly struct Handle<T> : IEquatable<Handle<T>>
{
public int ID
{
get => field - 1;
}
public int Generation
{
get => field - 1;
}
public Handle(int id, int generation)
{
ID = id + 1;
Generation = generation + 1;
}
public static Handle<T> Invalid => default;
public readonly bool IsValid => this != Invalid;
public readonly bool IsInvalid => this == Invalid;
public readonly override int GetHashCode()
{
return ID + (Generation << 16);
}
public readonly override bool Equals(object? obj)
{
return obj is Handle<T> id && Equals(id);
}
public override string ToString()
{
return $"Handle<{typeof(T).Name}>({ID}, {Generation})";
}
public readonly bool Equals(Handle<T> other)
{
return ID == other.ID && Generation == other.Generation;
}
public readonly int CompareTo(Handle<T> other)
{
return ID.CompareTo(other.ID);
}
public static bool operator ==(Handle<T> a, Handle<T> b)
{
return a.Equals(b);
}
public static bool operator !=(Handle<T> a, Handle<T> b)
{
return !a.Equals(b);
}
}
public readonly struct Identifier<T> : IEquatable<Identifier<T>>
{
public int Value
{
get => field - 1;
}
public Identifier(int value)
{
Value = value + 1;
}
public static Identifier<T> Invalid => default;
public readonly bool IsValid => this != Invalid;
public readonly bool IsInvalid => this == Invalid;
public readonly override int GetHashCode()
{
return Value;
}
public readonly override bool Equals(object? obj)
{
return obj is Identifier<T> id && Equals(id);
}
public override string ToString()
{
return $"Identifier<{typeof(T).Name}>({Value})";
}
public readonly bool Equals(Identifier<T> other)
{
return Value == other.Value;
}
public readonly int CompareTo(Identifier<T> other)
{
return Value.CompareTo(other.Value);
}
public static bool operator ==(Identifier<T> a, Identifier<T> b)
{
return a.Equals(b);
}
public static bool operator !=(Identifier<T> a, Identifier<T> b)
{
return !a.Equals(b);
}
public static bool operator <(Identifier<T> a, Identifier<T> b)
{
return a.Value < b.Value;
}
public static bool operator >(Identifier<T> a, Identifier<T> b)
{
return a.Value > b.Value;
}
public static bool operator <=(Identifier<T> a, Identifier<T> b)
{
return a.Value <= b.Value;
}
public static bool operator >=(Identifier<T> a, Identifier<T> b)
{
return a.Value >= b.Value;
}
public static implicit operator int(Identifier<T> id) => id.Value;
public static implicit operator Identifier<T>(int value) => new Identifier<T>(value);
}
public readonly struct Key64<T> : IEquatable<Key64<T>>
{
public ulong Value
{
get;
}
public Key64(ulong value)
{
Value = value;
}
public static Key64<T> Invalid => new(0);
public bool IsValid => this != Invalid;
public bool IsInvalid => this == Invalid;
public readonly override int GetHashCode()
{
return Value.GetHashCode();
}
public readonly bool Equals(Key64<T> other)
{
return Value == other.Value;
}
public readonly int CompareTo(Key64<T> other)
{
return Value.CompareTo(other.Value);
}
public readonly override bool Equals(object? obj)
{
return obj is Key64<T> id && Equals(id);
}
public override string ToString()
{
return Value.ToString("X16");
}
public static bool operator ==(Key64<T> a, Key64<T> b)
{
return a.Equals(b);
}
public static bool operator !=(Key64<T> a, Key64<T> b)
{
return !a.Equals(b);
}
}
public readonly struct Key128<T> : IEquatable<Key128<T>>
{
public UInt128 Value
{
get;
}
public Key128(UInt128 value)
{
Value = value;
}
public static Key128<T> Invalid => new(0);
public bool IsValid => this != Invalid;
public bool IsInvalid => this == Invalid;
public readonly override int GetHashCode()
{
return Value.GetHashCode();
}
public readonly bool Equals(Key128<T> other)
{
return Value == other.Value;
}
public readonly int CompareTo(Key128<T> other)
{
return Value.CompareTo(other.Value);
}
public readonly override bool Equals(object? obj)
{
return obj is Key128<T> id && Equals(id);
}
public override string ToString()
{
return Value.ToString("X16");
}
public static bool operator ==(Key128<T> a, Key128<T> b)
{
return a.Equals(b);
}
public static bool operator !=(Key128<T> a, Key128<T> b)
{
return !a.Equals(b);
}
}

214
Ghost.Core/Logging.cs Normal file
View File

@@ -0,0 +1,214 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
namespace Ghost.Core;
public enum LogLevel
{
Info,
Warning,
Error
}
public readonly struct LogMessage
{
public LogLevel Level
{
get;
}
public string Message
{
get;
}
public string? StackTrace
{
get;
}
public DateTime Timestamp
{
get;
}
public LogMessage(LogLevel level, string message, string? stackTrace = null)
{
Level = level;
Message = message;
StackTrace = stackTrace;
Timestamp = DateTime.Now;
}
public override string ToString()
{
if (StackTrace != null)
{
return $"{Timestamp:HH:mm:ss} [{Level}] {Message}\n{StackTrace}";
}
return $"{Timestamp:HH:mm:ss} [{Level}] {Message}";
}
}
public interface ILogger
{
ReadOnlyObservableCollection<LogMessage> Logs
{
get;
}
void Log(string message, LogLevel level);
void Log(Exception exception);
void Assert(bool condition, string message);
void Clear();
}
public static class Logger
{
// TODO: Add file logging.
private class LoggerImpl : ILogger
{
private readonly ObservableCollection<LogMessage> _logs = new();
private readonly ReadOnlyObservableCollection<LogMessage> _readOnly;
private readonly Lock _lock = new();
public ReadOnlyObservableCollection<LogMessage> Logs => _readOnly;
public LoggerImpl()
{
_readOnly = new ReadOnlyObservableCollection<LogMessage>(_logs);
}
[StackTraceHidden]
public void Log(string message, LogLevel level)
{
lock (_lock)
{
_logs.Add(new LogMessage(level, message));
}
}
[StackTraceHidden]
public void Log(Exception exception)
{
lock (_lock)
{
_logs.Add(new LogMessage(LogLevel.Error, exception.Message, exception.StackTrace));
}
}
[StackTraceHidden]
public void Assert(bool condition, string message)
{
lock (_lock)
{
if (!condition)
{
Log(message, LogLevel.Error);
}
}
}
public void Clear()
{
lock (_lock)
{
_logs.Clear();
}
}
}
private static readonly ILogger s_logger = new LoggerImpl();
public static ReadOnlyObservableCollection<LogMessage> Logs => s_logger.Logs;
[StackTraceHidden]
public static void Log(LogLevel level, object? message)
{
s_logger.Log(message?.ToString() ?? "null", level);
}
[StackTraceHidden]
public static void Log(LogLevel level, string message)
{
s_logger.Log(message, level);
}
[StackTraceHidden]
public static void Log(LogLevel level, string format, params object?[] args)
{
s_logger.Log(string.Format(format, args), level);
}
[StackTraceHidden]
public static void LogInfo(object? message)
{
s_logger.Log(message?.ToString() ?? "null", LogLevel.Info);
}
[StackTraceHidden]
public static void LogInfo(string message)
{
s_logger.Log(message, LogLevel.Info);
}
[StackTraceHidden]
public static void LogInfo(string format, params object?[] args)
{
s_logger.Log(string.Format(format, args), LogLevel.Info);
}
[StackTraceHidden]
public static void LogWarning(object? message)
{
s_logger.Log(message?.ToString() ?? "null", LogLevel.Warning);
}
[StackTraceHidden]
public static void LogWarning(string message)
{
s_logger.Log(message, LogLevel.Warning);
}
[StackTraceHidden]
public static void LogWarning(string format, params object?[] args)
{
s_logger.Log(string.Format(format, args), LogLevel.Warning);
}
[StackTraceHidden]
public static void LogError(object? message)
{
s_logger.Log(message?.ToString() ?? "null", LogLevel.Error);
}
[StackTraceHidden]
public static void LogError(string message)
{
s_logger.Log(message, LogLevel.Error);
}
[StackTraceHidden]
public static void LogError(string format, params object?[] args)
{
s_logger.Log(string.Format(format, args), LogLevel.Error);
}
[StackTraceHidden]
public static void LogError(Exception ex)
{
s_logger.Log(ex);
}
[StackTraceHidden]
public static void Assert(bool condition, string message)
{
s_logger.Assert(condition, message);
}
public static void Clear()
{
s_logger.Clear();
}
}

407
Ghost.Core/Result.cs Normal file
View File

@@ -0,0 +1,407 @@
using Misaki.HighPerformance.LowLevel;
using System.Runtime.CompilerServices;
namespace Ghost.Core;
public readonly struct Result
{
private readonly string? _message;
private readonly bool _isSuccess;
public readonly string? Message => _message;
public readonly bool IsSuccess => _isSuccess;
public readonly bool IsFailure => !IsSuccess;
public Result(bool success, string? message = null)
{
_isSuccess = success;
_message = message;
}
public static Result Success()
{
return new Result(true);
}
public static Result Failure(string? message = null)
{
return new Result(false, message);
}
public static Result Failure(Error status)
{
return new Result(false, status.ToString());
}
public static Result<T> Success<T>(T value)
{
return Result<T>.Success(value);
}
public static Result<T> Failure<T>(string? message = null)
{
return Result<T>.Failure(message);
}
public static Result<T> Failure<T>(Error status)
{
return Result<T>.Failure(status.ToString());
}
public void Deconstruct(out bool success, out string? message)
{
success = IsSuccess;
message = Message;
}
public override string ToString() => IsSuccess ? "OK" : $"Error: {Message}";
public static implicit operator bool(Result result) => result.IsSuccess;
}
public readonly struct Result<T>
{
private readonly T _value;
private readonly string? _message;
private readonly bool _isSuccess;
/// <summary>
/// Gets the value. Undefined if the result is a failure.
/// </summary>
public T Value
{
get
{
#if DEBUG || GHOST_EDITOR
if (IsFailure)
{
throw new InvalidOperationException($"Cannot access Value when Result is a failure. {_message}");
}
#endif
return _value;
}
}
public readonly string? Message => _message;
public readonly bool IsSuccess => _isSuccess;
public readonly bool IsFailure => !IsSuccess;
public Result(bool success, T value, string? message = null)
{
_isSuccess = success;
_value = value;
_message = message;
}
public static Result<T> Success(T value)
{
return new Result<T>(true, value);
}
public static Result<T> Failure(string? message = null)
{
return new Result<T>(false, default!, message);
}
public void Deconstruct(out bool success, out T value, out string? message)
{
success = IsSuccess;
value = Value;
message = Message;
}
public override string ToString() => IsSuccess ? $"OK: {Value}" : $"Error: {Message}";
public static implicit operator Result<T>(T? data) => data is not null ? Success(data) : Failure(null);
public static implicit operator Result<T>(Result result) => result.IsSuccess ? Success(default!) : Failure(result.Message);
public static implicit operator bool(Result<T> result) => result.IsSuccess;
}
public enum Error : byte
{
None,
NotFound,
InvalidArgument,
InvalidState,
InternalError,
PermissionDenied,
NotSupported,
OutOfMemory,
Timeout,
Cancelled,
UnknownError,
Success = None,
}
public readonly struct Result<T, E>
where E : struct, Enum
{
private readonly T _value;
private readonly E _error;
/// <summary>
/// Gets the value. Undefined if the result is a failure.
/// </summary>
public T Value
{
get
{
#if DEBUG || GHOST_EDITOR
if (IsFailure)
{
throw new InvalidOperationException($"Cannot access Value when Result is a failure. Error: {_error}");
}
#endif
return _value;
}
}
public E Error => _error;
public bool IsSuccess => EqualityComparer<E>.Default.Equals(_error, default);
public bool IsFailure => !IsSuccess;
public Result(T value, E status)
{
_value = value;
_error = status;
}
public static Result<T, E> Success(T value)
{
return new Result<T, E>(value, default);
}
public static Result<T, E> Failure(E status)
{
return new Result<T, E>(default!, status);
}
public void Deconstruct(out T value, out E status)
{
value = Value;
status = Error;
}
public override string ToString() => $"Value: {_value}, Status: {_error}";
public static implicit operator Result<T, E>(T data) => new(data, default);
public static implicit operator Result<T, E>(E status) => new(default!, status);
public static implicit operator bool(Result<T, E> result) => result.IsSuccess;
}
public readonly ref struct RefResult<T, E>
where E : struct, Enum
{
private readonly ref T _value;
private readonly E _error;
/// <summary>
/// Gets a reference to the value. Undefined if the result is a failure.
/// </summary>
public ref T Value
{
get
{
#if DEBUG || GHOST_EDITOR
if (IsFailure)
{
throw new InvalidOperationException($"Cannot access Value when Result is a failure. Error: {_error}");
}
#endif
return ref _value;
}
}
public E Error => _error;
public bool IsSuccess => EqualityComparer<E>.Default.Equals(_error, default);
public bool IsFailure => !IsSuccess;
public RefResult(ref T value, E error)
{
_value = ref value;
_error = error;
}
public static RefResult<T, E> Success(ref T value)
{
return new RefResult<T, E>(ref value, default);
}
public static RefResult<T, E> Failure(E error)
{
return new RefResult<T, E>(ref Unsafe.NullRef<T>(), error);
}
public void Deconstruct(out bool success, out Ref<T> value, out E status)
{
success = IsSuccess;
value = new Ref<T>(ref Value);
status = Error;
}
public override string ToString() => $"Value: {_value}, Status: {_error}";
public static implicit operator RefResult<T, E>(Ref<T> data) => new(ref data.Get(), default);
public static implicit operator RefResult<T, E>(E error) => new(ref Unsafe.NullRef<T>(), error);
public static implicit operator bool(RefResult<T, E> result) => result.IsSuccess;
}
public static class ResultExtensions
{
public static void ThrowIfFailed(this Error result, [CallerArgumentExpression(nameof(result))] string? op = null)
{
if (result != Error.None)
{
throw new InvalidOperationException($"{op} failed: {result}");
}
}
public static void ThrowIfFailed(this Result result, [CallerArgumentExpression(nameof(result))] string? op = null)
{
if (!result.IsSuccess)
{
throw new InvalidOperationException($"{op} failed: {result.Message}");
}
}
public static T GetValueOrThrow<T>(this Result<T> result, [CallerArgumentExpression(nameof(result))] string? op = null)
{
if (!result.IsSuccess)
{
throw new InvalidOperationException($"{op} failed: {result.Message}");
}
return result.Value;
}
public static T GetValueOrThrow<T, S>(this Result<T, S> result, [CallerArgumentExpression(nameof(result))] string? op = null)
where S : struct, Enum
{
if (!result.IsSuccess)
{
throw new InvalidOperationException($"{op} failed: status {result.Error}");
}
return result.Value;
}
public static T? GetValueOrDefault<T>(this Result<T> result, T? defaultValue = default)
{
return result.IsSuccess ? result.Value : defaultValue;
}
public static T? GetValueOrDefault<T, S>(this Result<T, S> result, T? defaultValue = default)
where S : struct, Enum
{
return result.IsSuccess ? result.Value : defaultValue;
}
public static bool TryGetValue<T>(this Result<T> result, out T value)
{
if (result.IsSuccess)
{
value = result.Value;
return true;
}
value = default!;
return false;
}
public static bool TryGetValue<T, S>(this Result<T, S> result, out T value)
where S : struct, Enum
{
if (result.IsSuccess)
{
value = result.Value;
return true;
}
value = default!;
return false;
}
public static Result OnSuccess(this Result result, Action action)
{
if (result.IsSuccess)
{
action();
}
return result;
}
public static Result<T> OnSuccess<T>(this Result<T> result, Action<T> action)
{
if (result.IsSuccess)
{
action(result.Value);
}
return result;
}
public static Result<T, E> OnSuccess<T, E>(this Result<T, E> result, Action<T> action)
where E : struct, Enum
{
if (result.IsSuccess)
{
action(result.Value);
}
return result;
}
public static Result OnFailed(this Result result, Action<string?> action)
{
if (result.IsFailure)
{
action(result.Message);
}
return result;
}
public static Result<T> OnFailed<T>(this Result<T> result, Action<string?> action)
{
if (result.IsFailure)
{
action(result.Message);
}
return result;
}
public static Result<T, E> OnFailed<T, E>(this Result<T, E> result, Action<E> action)
where E : struct, Enum
{
if (result.IsFailure)
{
action(result.Error);
}
return result;
}
public static Result<U> Then<T, U>(this Result<T> result, Func<T, Result<U>> func)
{
if (result.IsFailure)
{
return Result<U>.Failure(result.Message);
}
return func(result.Value);
}
public static Result<U, E> Then<T, U, E>(this Result<T, E> result, Func<T, Result<U, E>> func)
where E : struct, Enum
{
if (result.IsFailure)
{
return Result<U, E>.Failure(result.Error);
}
return func(result.Value);
}
}

67
Ghost.Core/TypeHandle.cs Normal file
View File

@@ -0,0 +1,67 @@
using System.Runtime.CompilerServices;
namespace Ghost.Core;
public readonly struct TypeHandle
{
public readonly IntPtr Value
{
get;
}
private TypeHandle(IntPtr value)
{
Value = value;
}
/// <summary>
/// Gets the space handle for the specified space.
/// </summary>
/// <param name="type">The space to get the handle for.</param>
/// <returns>The space handle as a nint.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TypeHandle Get(Type type) => new TypeHandle(type.TypeHandle.Value);
/// <summary>
/// Gets the space handle for the specified space.
/// </summary>
/// <typeparam name="T">The space to get the handle for.</typeparam>
/// <returns>The space handle as a nint.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TypeHandle Get<T>() => Get(typeof(T));
/// <summary>
/// Converts a TypeHandle to a Type.
/// </summary>
/// <param name="handle">The TypeHandle to convert.</param>
/// <returns>The corresponding Type.</returns>
public Type? ToType()
{
return Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(Value));
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public static implicit operator TypeHandle(IntPtr value)
{
return new TypeHandle(value);
}
public static implicit operator IntPtr(TypeHandle handle)
{
return handle.Value;
}
public static implicit operator TypeHandle(Type type)
{
return Get(type);
}
public static implicit operator Type?(TypeHandle handle)
{
return handle.ToType();
}
}

View File

@@ -0,0 +1,22 @@
using Misaki.HighPerformance.Buffer;
namespace Ghost.Core.Utilities;
public class CollectionPool<TCollection, TItem>
where TCollection : class, ICollection<TItem>, new()
{
internal static readonly ObjectPool<TCollection> s_pool = new ObjectPool<TCollection>(() => new TCollection(), null, 1);
public static TCollection Rent()
{
return s_pool.Rent();
}
public static void Return(TCollection collection)
{
collection.Clear();
s_pool.Return(collection);
}
}
public class ListPool<T> : CollectionPool<List<T>, T>;

View File

@@ -0,0 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Ghost.Core.Utilities;
internal class EnumUtility
{
}

View File

@@ -0,0 +1,44 @@
using System.Runtime.CompilerServices;
namespace Ghost.Core.Utilities;
public static class Hash
{
private const ulong _PRIME1 = 0xfa517d6985796b7bul;
private const ulong _PRIME2 = 0x589578278297b985ul;
private const ulong _PRIME3 = 0x221147a447814b73ul;
private const ulong _PRIME4 = 0x9e3779b97f4a7c15ul; // Golden Ratio
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Hash64(ulong a, ulong b)
{
return a ^ (b * _PRIME4 + (a << 6) + (a >> 2));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Hash64(ulong a, ulong b, ulong c)
{
ulong h1 = a * _PRIME1;
ulong h2 = b * _PRIME2;
ulong h3 = c * _PRIME3;
ulong h = h1 ^ h2 ^ h3;
h = (h ^ (h >> 33)) * _PRIME4;
return h ^ (h >> 29);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Hash64(ulong a, ulong b, ulong c, ulong d)
{
ulong h1 = a * _PRIME1;
ulong h2 = b * _PRIME2;
ulong h3 = c * _PRIME3;
ulong h4 = d * _PRIME4;
ulong h = h1 ^ h2 ^ h3 ^ h4;
h = (h ^ (h >> 33)) * _PRIME1;
return h ^ (h >> 29);
}
}

View File

@@ -0,0 +1,12 @@
using Ghost.Core.Contracts;
namespace Ghost.Core.Utilities;
internal static class InternalResource
{
public static void Release<T>(ref T? resource)
where T : IReleasable
{
resource?.InternalRelease();
}
}

View File

@@ -0,0 +1,115 @@
using Misaki.HighPerformance.LowLevel;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using TerraFX.Interop.Windows;
namespace Ghost.Core.Utilities;
[SupportedOSPlatform("windows10.0.19041.0")]
internal static unsafe partial class Win32Utility
{
[EditorBrowsable(EditorBrowsableState.Never)]
public readonly ref struct IID_PPV
{
public readonly Guid* iid;
public readonly void** ppv;
public IID_PPV(Guid* iid, void** ppv)
{
this.iid = iid;
this.ppv = ppv;
}
public void Deconstruct(out Guid* iid, out void** ppv)
{
iid = this.iid;
ppv = this.ppv;
}
}
public static Guid* IID_NULL
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in IID.IID_NULL));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IID_PPV IID_PPV_ARGS<T>(ComPtr<T>* comPtr)
where T : unmanaged, IUnknown.Interface
{
return new IID_PPV(Windows.__uuidof<T>(), (void**)comPtr);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Attach<T>(ref this UniquePtr<T> uPtr, T* other)
where T : unmanaged, IUnknown.Interface
{
var ptr = uPtr.Get();
if (ptr != null)
{
var refCount = ptr->Release();
Debug.Assert((refCount != 0) || (ptr != other));
}
uPtr = new UniquePtr<T>(other);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Dispose<T>(ref this UniquePtr<T> uPtr)
where T : unmanaged, IUnknown.Interface
{
var ptr = uPtr.Detach();
if (ptr != null)
{
ptr->Release();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Result ToResult(this HRESULT hr, [CallerArgumentExpression(nameof(hr))] string? op = null)
{
if (hr.SUCCEEDED)
{
return Result.Success();
}
return Result.Failure($"{op} failed with code {hr}");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void** ReleaseAndGetVoidAddressOf<T>(ref this ComPtr<T> comPtr)
where T : unmanaged, IUnknown.Interface
{
return (void**)comPtr.ReleaseAndGetAddressOf();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ComPtr<T> Move<T>(ref this ComPtr<T> comPtr)
where T : unmanaged, IUnknown.Interface
{
var copy = default(ComPtr<T>);
comPtr.Swap(ref copy);
return copy;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool HasFlag<T>(this uint flags, T flag)
where T : Enum
{
return (flags & Unsafe.As<T, uint>(ref flag)) != 0;
}
extension(MemoryLeakException)
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowIfRefCountNonZero(uint count)
{
if (count != 0)
{
throw new MemoryLeakException($"Reference count is not zero: {count}");
}
}
}
}

View File

@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ghost.Shader.Test")]
[assembly: InternalsVisibleTo("Ghost.Graphics")]

View File

@@ -0,0 +1,305 @@
using Misaki.HighPerformance.Mathematics;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace Ghost.DSL.Generator;
public enum PackingRules
{
Exact,
Aligned,
}
[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum)]
public class GenerateHLSLAttribute : Attribute
{
private readonly PackingRules _packingRules;
private readonly string? _outputSource;
public GenerateHLSLAttribute(PackingRules packingRules, string? outputSource)
{
_packingRules = packingRules;
_outputSource = outputSource;
}
}
internal static partial class ShaderStructGenerator
{
private struct ShaderFieldInfo
{
public string name;
public Type fieldType;
public ShaderFieldInfo(string name, Type fieldType)
{
this.name = name;
this.fieldType = fieldType;
}
public ShaderFieldInfo(FieldInfo fieldInfo)
: this(fieldInfo.Name, fieldInfo.FieldType)
{
}
}
private const int _HLSL_VECTOR_REGISTER_SIZE = 16; // 16 bytes (128 bits) for float4
private static void GenerateEnumHLSL(Type type, StringBuilder sb)
{
if (!type.IsEnum)
{
throw new InvalidOperationException($"Type {type.FullName} is not an enum.");
}
var enumName = type.Name;
//var underlyingType = Enum.GetUnderlyingType(space);
//var underlyingTypeName = underlyingType switch
//{
// Type t when t == typeof(byte) || t == typeof(short) || t == typeof(int) => "int",
// Type t when t == typeof(sbyte) || t == typeof(ushort) || t == typeof(uint) => "uint",
// _ => throw new InvalidOperationException($"Unsupported underlying space {underlyingType.FullName} for enum {enumName}."),
//};
// sb.Append(@$"
//enum {enumName} : {underlyingTypeName}
//{{");
var names = Enum.GetNames(type);
var values = Enum.GetValuesAsUnderlyingType(type);
for (var i = 0; i < names.Length; i++)
{
var name = $"{CamelCaseToUnderscoreRegex().Replace(enumName, "_$1")}_{names[i]}";
var value = values.GetValue(i);
// sb.Append(@$"
//{name} = {Value},");
sb.Append(@$"
#define {name.ToUpperInvariant()} {value}"); // Use #define for capability. Enum is only support for newer HLSL versions.
}
// sb.AppendLine(@"
//};");
sb.AppendLine();
}
public static int FindNextFieldThatFits(FieldInfo[] fields, bool[] looked, int startIndex, int size, out int foundIndex)
{
if (size <= 0)
{
foundIndex = -1;
return size;
}
var bestFitIndex = -1;
var bestFitSize = 0;
for (var j = startIndex; j < fields.Length; j++)
{
if (looked[j])
{
continue;
}
var nextField = fields[j];
var nextSize = Marshal.SizeOf(nextField.FieldType);
if (nextSize <= size)
{
if (nextSize == size)
{
foundIndex = j;
return nextSize;
}
if (nextSize > bestFitSize)
{
bestFitSize = nextSize;
bestFitIndex = j;
}
}
}
if (bestFitIndex != -1)
{
foundIndex = bestFitIndex;
return bestFitSize;
}
foundIndex = -1;
return size;
}
private static void GenerateStructHLSL(Type type, PackingRules packingRules, StringBuilder sb)
{
if (!type.IsValueType || type.IsPrimitive)
{
throw new InvalidOperationException($"Type {type.FullName} is not a struct.");
}
var structName = type.Name;
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance)
.Where(static f => f.FieldType.IsValueType).ToArray();
var shaderFields = new ShaderFieldInfo[fields.Length];
if (packingRules == PackingRules.Aligned)
{
var sortedFields = new List<ShaderFieldInfo>(fields.Length);
var looked = new bool[fields.Length];
var paddingIndex = 0;
// Sort the fields to align them to HLSL vector registers (16 bytes)
for (var i = 0; i < fields.Length; i++)
{
if (looked[i])
{
continue;
}
var field = fields[i];
var size = Marshal.SizeOf(field.FieldType);
sortedFields.Add(new ShaderFieldInfo(field));
var registerRemaining = _HLSL_VECTOR_REGISTER_SIZE - (size % _HLSL_VECTOR_REGISTER_SIZE);
while (true)
{
var nextSize = FindNextFieldThatFits(fields, looked, i + 1, registerRemaining, out var nextIndex);
if (nextSize == 0 || nextIndex == -1)
{
break;
}
looked[i] = true;
looked[nextIndex] = true;
sortedFields.Add(new ShaderFieldInfo(fields[nextIndex]));
registerRemaining -= nextSize;
}
if (registerRemaining != 0)
{
// Add padding if necessary
var count = registerRemaining / sizeof(float);
for (var p = 0; p < count; p++)
{
sortedFields.Add(new ShaderFieldInfo($"_padding{paddingIndex++}", typeof(float)));
}
}
}
shaderFields = sortedFields.ToArray();
}
else
{
for (var i = 0; i < fields.Length; i++)
{
shaderFields[i] = new ShaderFieldInfo(fields[i]);
}
}
sb.Append(@$"
struct {structName}
{{");
foreach (var field in shaderFields)
{
var fieldType = field.fieldType;
var fieldName = field.name;
string hlslType;
switch (fieldType)
{
case Type t when t == typeof(float):
hlslType = "float";
break;
case Type t when t == typeof(double):
hlslType = "double";
break;
case Type t when t == typeof(int):
hlslType = "int";
break;
case Type t when t == typeof(uint):
hlslType = "uint";
break;
case Type t when t == typeof(bool):
hlslType = "bool";
break;
case Type t when t == typeof(Vector2):
hlslType = "float2";
break;
case Type t when t == typeof(Vector3):
hlslType = "float3";
break;
case Type t when t == typeof(Vector4):
hlslType = "float4";
break;
case Type t when t == typeof(Matrix4x4):
hlslType = "float4x4";
break;
default:
{
if (fieldType.Namespace == typeof(float2).Namespace)
{
if (fieldType.Name.StartsWith("float")
|| fieldType.Name.StartsWith("double")
|| fieldType.Name.StartsWith("int")
|| fieldType.Name.StartsWith("uint")
|| fieldType.Name.StartsWith("bool"))
{
hlslType = fieldType.Name;
break;
}
}
throw new InvalidOperationException($"Unsupported field type: {fieldType.FullName} in struct {structName}.");
}
}
sb.Append(@$"
{hlslType} {fieldName};");
}
sb.AppendLine(@"
};");
}
public static void GenerateHLSL(ReadOnlySpan<Type> types, PackingRules packingRules, string outputSource)
{
if (!Directory.Exists(Path.GetDirectoryName(outputSource)))
{
throw new DirectoryNotFoundException($"The directory for the output source '{outputSource}' does not exist.");
}
var hlslDefine = $"{Path.GetFileNameWithoutExtension(outputSource).ToUpperInvariant().Replace('.', '_')}_HLSL";
var sb = new StringBuilder();
sb.AppendLine(@$"// Auto-generated HLSL code, please do not edit this file directly.
#ifndef {hlslDefine}
#define {hlslDefine}");
foreach (var type in types)
{
if (type.IsEnum)
{
GenerateEnumHLSL(type, sb);
}
else if (type.IsValueType && !type.IsPrimitive)
{
GenerateStructHLSL(type, packingRules, sb);
}
else
{
continue;
}
}
sb.Append(@"
#endif");
var hlslCode = sb.ToString();
File.WriteAllText(outputSource, hlslCode);
}
[GeneratedRegex("(?<=[a-z])([A-Z])")]
private static partial Regex CamelCaseToUnderscoreRegex();
}

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Antlr4.Runtime.Standard" Version="4.13.1" />
<PackageReference Include="Antlr4BuildTasks" Version="12.11.0" />
</ItemGroup>
<ItemGroup>
<Antlr4 Include="Grammar\GhostShaderLexer.g4">
<Generator>MSBuild:Compile</Generator>
<Listener>false</Listener>
<Visitor>true</Visitor>
</Antlr4>
<Antlr4 Include="Grammar\GhostShaderParser.g4">
<Generator>MSBuild:Compile</Generator>
<Listener>false</Listener>
<Visitor>true</Visitor>
</Antlr4>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Ghost.Core/Ghost.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,38 @@
lexer grammar GhostShaderLexer;
// Keywords
SHADER: 'shader';
PROPERTIES: 'properties';
PIPELINE: 'pipeline';
PASS: 'pass';
DEFINES: 'defines';
KEYWORDS: 'keywords';
INCLUDES: 'includes';
GLOBAL: 'global';
LOCAL: 'local';
HLSL: 'hlsl';
// Punctuation
LBRACE: '{';
RBRACE: '}';
LPAREN: '(';
RPAREN: ')';
LBRACK: '[';
RBRACK: ']';
SEMICOLON: ';';
COMMA: ',';
EQUALS: '=';
COLON: ':';
// Literals
STRING_LITERAL: '"' (~["\r\n] | '\\' .)* '"';
NUMBER: [0-9]+ ('.' [0-9]+)? | '.' [0-9]+;
IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]*;
// Whitespace and Comments
WS: [ \t\r\n]+ -> skip;
LINE_COMMENT: '//' ~[\r\n]* -> skip;
BLOCK_COMMENT: '/*' .*? '*/' -> skip;
ANY_CHAR: . ;

View File

@@ -0,0 +1,99 @@
parser grammar GhostShaderParser;
options {
tokenVocab = GhostShaderLexer;
}
// Top-level rule
shaderFile: shader+ EOF;
shader:
SHADER STRING_LITERAL LBRACE
shaderBody
RBRACE;
shaderBody:
(propertiesBlock | pipelineBlock | passBlock | functionCall)*;
// Properties block
propertiesBlock:
PROPERTIES LBRACE
propertyDeclaration*
RBRACE;
propertyDeclaration:
scope? IDENTIFIER IDENTIFIER (EQUALS LBRACE propertyInitializer RBRACE)? SEMICOLON;
scope:
GLOBAL | LOCAL;
propertyInitializer:
(NUMBER | IDENTIFIER) (COMMA (NUMBER | IDENTIFIER))*;
// Pipeline block
pipelineBlock:
PIPELINE LBRACE
pipelineStatement*
RBRACE;
pipelineStatement:
IDENTIFIER EQUALS IDENTIFIER SEMICOLON;
// Pass block
passBlock:
PASS STRING_LITERAL LBRACE
passBody
RBRACE;
// Template
passBody:
(definesBlock | includesBlock | keywordsBlock | pipelineBlock | hlslBlock | shaderEntry)*;
definesBlock:
DEFINES LBRACE
defineStatement*
RBRACE;
defineStatement:
IDENTIFIER SEMICOLON;
includesBlock:
INCLUDES LBRACE
includeStatement*
RBRACE;
includeStatement:
STRING_LITERAL SEMICOLON;
keywordsBlock:
KEYWORDS LBRACE
keywordStatement*
RBRACE;
keywordStatement:
scope? IDENTIFIER (COMMA IDENTIFIER)* SEMICOLON;
hlslBlock:
HLSL LBRACE
hlslBody
RBRACE;
// Recursively matches content, ensuring braces are balanced.
hlslBody:
(
~(LBRACE | RBRACE) // Match ANY token except open/close braces
|
LBRACE hlslBody RBRACE // Or match a nested block recursively
)*;
shaderEntry:
IDENTIFIER STRING_LITERAL COLON STRING_LITERAL SEMICOLON;
functionCall:
IDENTIFIER LPAREN functionArguments? RPAREN SEMICOLON;
functionArguments:
functionArgument (COMMA functionArgument)*;
functionArgument:
STRING_LITERAL | NUMBER | IDENTIFIER;

View File

@@ -0,0 +1,323 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.DSL.ShaderParser;
using System.Text;
namespace Ghost.DSL.ShaderCompiler;
public struct DSLShaderError
{
public string message;
public int line;
public int column;
public override readonly string ToString()
{
return $"Error at {line}:{column} - {message}";
}
}
internal static class DSLShaderCompiler
{
private const string _GLOBAL_PROPERTY_FILE_NAME = "GlobalData.g.hlsl";
private const string _GENERATED_FILE_HEADER = "// Auto-generated shader file. Please do not edit this file directly.";
private static string GetPassUniqueId(DSLShaderSemantics shader, PassSemantic pass)
{
return $"{shader.name}_{pass.name}";
}
private static PipelineState MeragePipeline(PipelineSemantic? semantic, PipelineState parent)
{
if (semantic == null)
{
return parent;
}
return new PipelineState
{
ZTest = semantic.zTest ?? parent.ZTest,
ZWrite = semantic.zWrite ?? parent.ZWrite,
Cull = semantic.cull ?? parent.Cull,
Blend = semantic.blend ?? parent.Blend,
ColorMask = semantic.colorMask ?? parent.ColorMask
};
}
private static uint CalculateCBufferSize(ReadOnlySpan<PropertyDescriptor> properties)
{
if (properties.IsEmpty)
{
return 0;
}
var currentOffset = 0u;
foreach (var prop in properties)
{
var size = prop.type.GetSize();
if ((currentOffset % 16) + size > 16)
{
currentOffset = (currentOffset + 15u) & ~15u;
}
currentOffset += size;
}
return (currentOffset + 15u) & ~15u;
}
// TODO: Implement shader inheritance resolution, including property and pass merging.
// Currently, we just ignore inheritance.
public static ShaderDescriptor ResolveShader(DSLShaderSemantics semantics)
{
var descriptor = new ShaderDescriptor
{
name = semantics.name,
hlsl = semantics.hlsl
};
var shaderGlobalProperties = semantics.properties?
.Where(p => p.scope == PropertyScope.Global)
.Select(p => new PropertyDescriptor
{
name = p.name,
type = p.type,
defaultValue = p.defaultValue
}).ToArray();
var shaderLocalProperties = semantics.properties?
.Where(p => p.scope == PropertyScope.Local)
.Select(p => new PropertyDescriptor
{
name = p.name,
type = p.type,
defaultValue = p.defaultValue
}).ToArray();
descriptor.globalProperties = shaderGlobalProperties ?? Array.Empty<PropertyDescriptor>();
descriptor.properties = shaderLocalProperties ?? Array.Empty<PropertyDescriptor>();
descriptor.cbufferSize = CalculateCBufferSize(descriptor.properties);
if (semantics.passes != null)
{
descriptor.passes = new PassDescriptor[semantics.passes.Count];
for (int i = 0; i < semantics.passes.Count; i++)
{
var pass = semantics.passes[i];
var localPipeline = MeragePipeline(pass.localPipeline, PipelineState.Default);
descriptor.passes[i] = new PassDescriptor
{
identifier = GetPassUniqueId(semantics, pass),
name = pass.name,
taskShader = pass.taskShader,
meshShader = pass.meshShader,
pixelShader = pass.pixelShader,
localPipeline = localPipeline,
defines = pass.defines?.ToArray() ?? Array.Empty<string>(),
includes = pass.includes?.ToArray() ?? Array.Empty<string>(),
keywords = pass.keywords?.ToArray() ?? Array.Empty<KeywordsGroup>(),
hlsl = pass.hlsl
};
}
}
else
{
descriptor.passes = Array.Empty<PassDescriptor>();
}
return descriptor;
}
public static Result<ShaderDescriptor> CompileShader(string shaderPath, string generatedOutputDirectory)
{
try
{
var source = File.ReadAllText(shaderPath);
// Use ANTLR4 parser
var shaderModels = AntlrShaderCompiler.ParseShaders(source, out var parseErrors);
if (parseErrors.Count != 0)
{
var errorMessages = new StringBuilder();
foreach (var error in parseErrors)
{
errorMessages.AppendLine(error.ToString());
}
return Result.Failure("Failed to parse shader due to errors:\n" + errorMessages.ToString());
}
if (shaderModels.Count == 0)
{
return Result.Failure("No shader found in the provided file.");
}
// Convert to semantics
var model = AntlrShaderCompiler.ConvertToSemantics(shaderModels[0], out var errors);
if (errors.Count != 0 || model == null)
{
var errorMessages = new StringBuilder();
foreach (var error in errors)
{
errorMessages.AppendLine(error.ToString());
}
return Result.Failure("Failed to compile shader due to errors:\n" + errorMessages.ToString());
}
var desc = ResolveShader(model);
var globalPropResult = GenerateGlobalProperties(desc.globalProperties, generatedOutputDirectory);
if (globalPropResult.IsFailure)
{
return Result.Failure("Failed to generate global properties: " + globalPropResult.Message);
}
var generatedResult = GenerateShaderCode(desc, generatedOutputDirectory);
if (generatedResult.IsFailure)
{
return Result.Failure("Failed to generate pass files: " + generatedResult.Message);
}
foreach (ref var pass in desc.passes.AsSpan())
{
if (pass.includes == null)
{
pass.includes = new string[2];
}
else
{
Array.Resize(ref pass.includes, pass.includes.Length + 2);
// Shift existing includes to make room for the two new includes at the front.
pass.includes.AsSpan(0, pass.includes.Length - 2).CopyTo(pass.includes.AsSpan(2));
}
pass.includes[0] = globalPropResult.Value;
pass.includes[1] = generatedResult.Value;
}
return desc;
}
catch (Exception ex)
{
return Result.Failure("Failed to compile shader: " + ex.Message);
}
}
private static string ShaderPropertyTypeToHLSLType(ShaderPropertyType type)
{
return type switch
{
ShaderPropertyType.Float => "float",
ShaderPropertyType.Float2 => "float2",
ShaderPropertyType.Float3 => "float3",
ShaderPropertyType.Float4 => "float4",
ShaderPropertyType.Int => "int",
ShaderPropertyType.Int2 => "int2",
ShaderPropertyType.Int3 => "int3",
ShaderPropertyType.Int4 => "int4",
ShaderPropertyType.UInt => "uint",
ShaderPropertyType.UInt2 => "uint2",
ShaderPropertyType.UInt3 => "uint3",
ShaderPropertyType.UInt4 => "uint4",
ShaderPropertyType.Bool => "bool",
ShaderPropertyType.Bool2 => "bool2",
ShaderPropertyType.Bool3 => "bool3",
ShaderPropertyType.Bool4 => "bool4",
// NOTE: Textures here are bindless, represented as uint (descriptor index).
ShaderPropertyType.Texture2D => "TEXTURE2D",
ShaderPropertyType.Texture3D => "TEXTURE3D",
ShaderPropertyType.TextureCube => "TEXTURECUBE",
ShaderPropertyType.Texture2DArray => "TEXTURE2D_ARRAY",
ShaderPropertyType.TextureCubeArray => "TEXTURECUBE_ARRAY",
ShaderPropertyType.Sampler => "SAMPLER",
_ => throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported shader property type: {type}")
};
}
public static Result<string> GenerateShaderCode(ShaderDescriptor descriptor, string targetDirectory)
{
if (!Directory.Exists(targetDirectory))
{
return Result.Failure("Target directory does not exist.");
}
var outputFileName = descriptor.name.Replace('/', '_');
var outputFilePath = Path.Combine(targetDirectory, outputFileName + ".g.hlsl");
var outputDirectory = Path.GetDirectoryName(outputFilePath);
if (!Directory.Exists(outputDirectory))
{
Directory.CreateDirectory(outputDirectory!);
}
using var fileStream = File.CreateText(outputFilePath);
var fileDefine = outputFileName.Replace('/', '_').ToUpperInvariant() + "_G_HLSL";
var sb = new StringBuilder();
sb.AppendLine(_GENERATED_FILE_HEADER);
sb.AppendLine(@$"
#ifndef {fileDefine}
#define {fileDefine}
#include ""F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Common.hlsl""");
sb.Append(@"
struct PerMaterialData
{");
foreach (var prop in descriptor.properties)
{
sb.Append($@"
{ShaderPropertyTypeToHLSLType(prop.type)} {prop.name};");
}
sb.Append(@"
};");
sb.AppendLine();
sb.AppendLine(@$"
#endif // {fileDefine}");
fileStream.Write(sb.ToString());
return outputFilePath;
}
public static Result<string> GenerateGlobalProperties(ReadOnlySpan<PropertyDescriptor> globalProperties, string targetDirectory)
{
if (!Directory.Exists(targetDirectory))
{
return Result.Failure("Target directory does not exist.");
}
var globalFilePath = Path.Combine(targetDirectory, _GLOBAL_PROPERTY_FILE_NAME);
using var globalFileStream = File.CreateText(globalFilePath);
var sb = new StringBuilder();
sb.AppendLine(_GENERATED_FILE_HEADER);
sb.Append(@"
#ifndef GLOBALDATA_G_HLSL
#define GLOBALDATA_G_HLSL
#include ""F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Common.hlsl""
struct GlobalData
{");
foreach (var prop in globalProperties)
{
sb.Append($@"
{ShaderPropertyTypeToHLSLType(prop.type)} {prop.name};");
}
sb.AppendLine(@"
};
#endif // GLOBALDATA_G_HLSL");
globalFileStream.Write(sb.ToString());
return globalFilePath;
}
}

View File

@@ -0,0 +1,48 @@
using Ghost.Core.Graphics;
namespace Ghost.DSL.ShaderCompiler;
public enum PropertyScope
{
Global,
Local,
}
public class PropertySemantic
{
public PropertyScope scope;
public ShaderPropertyType type;
public string name = string.Empty;
public object? defaultValue;
}
public class PipelineSemantic
{
public ZTest? zTest;
public ZWrite? zWrite;
public Cull? cull;
public Blend? blend;
public ColorWriteMask? colorMask;
}
public class PassSemantic
{
public string name = string.Empty;
public ShaderEntryPoint taskShader;
public ShaderEntryPoint meshShader;
public ShaderEntryPoint pixelShader;
public string? hlsl;
public List<string>? defines;
public List<string>? includes;
public List<KeywordsGroup>? keywords;
public PipelineSemantic? localPipeline;
}
public class DSLShaderSemantics
{
public string name = string.Empty;
public string? hlsl;
public List<PropertySemantic>? properties;
public PipelineSemantic? pipeline;
public List<PassSemantic>? passes;
}

View File

@@ -0,0 +1,383 @@
using Antlr4.Runtime;
using Ghost.Core.Graphics;
using Ghost.DSL.ShaderCompiler;
using Ghost.DSL.ShaderParser.Model;
namespace Ghost.DSL.ShaderParser;
public class AntlrShaderCompiler
{
public static List<ShaderModel> ParseShaders(string source, out List<DSLShaderError> errors)
{
errors = new List<DSLShaderError>();
try
{
var inputStream = new AntlrInputStream(source);
var lexer = new GhostShaderLexer(inputStream);
// Capture lexer errors
lexer.RemoveErrorListeners();
var lexerErrorListener = new ErrorListener(errors);
lexer.AddErrorListener(lexerErrorListener);
var tokenStream = new CommonTokenStream(lexer);
var parser = new GhostShaderParser(tokenStream);
// Capture parser errors
parser.RemoveErrorListeners();
var parserErrorListener = new ErrorListener(errors);
parser.AddErrorListener(parserErrorListener);
var tree = parser.shaderFile();
if (errors.Count > 0)
{
return new List<ShaderModel>();
}
var visitor = new ShaderVisitor();
visitor.Visit(tree);
return visitor.Shaders;
}
catch (Exception ex)
{
errors.Add(new DSLShaderError
{
message = $"Unexpected error during parsing: {ex.Message}",
line = -1,
column = -1
});
return new List<ShaderModel>();
}
}
public static DSLShaderSemantics? ConvertToSemantics(ShaderModel model, out List<DSLShaderError> errors)
{
errors = new List<DSLShaderError>();
if (string.IsNullOrWhiteSpace(model.Name))
{
errors.Add(new DSLShaderError
{
message = "Shader name cannot be empty.",
line = 0,
column = 0
});
return null;
}
var semantics = new DSLShaderSemantics
{
name = model.Name,
properties = ConvertProperties(model.Properties, errors),
pipeline = ConvertPipeline(model.Pipeline, errors)
};
foreach (var pass in model.Passes)
{
var passSemantic = ConvertPass(pass, errors);
if (passSemantic != null)
{
semantics.passes ??= new List<PassSemantic>();
semantics.passes.Add(passSemantic);
}
}
return semantics;
}
private static List<PropertySemantic>? ConvertProperties(PropertiesBlockModel? properties, List<DSLShaderError> errors)
{
if (properties == null || properties.Properties.Count == 0)
{
return null;
}
var result = new List<PropertySemantic>();
var usedNames = new HashSet<string>();
foreach (var prop in properties.Properties)
{
if (usedNames.Contains(prop.Name))
{
errors.Add(new DSLShaderError
{
message = $"Duplicate property name '{prop.Name}'.",
line = 0,
column = 0
});
continue;
}
var semantic = new PropertySemantic
{
name = prop.Name,
scope = prop.Scope?.ToLower() == "global" ? PropertyScope.Global : PropertyScope.Local,
type = ParsePropertyType(prop.Type, errors)
};
if (prop.Initializer.Count > 0)
{
semantic.defaultValue = ParsePropertyValue(semantic.type, prop.Initializer, errors);
}
usedNames.Add(prop.Name);
result.Add(semantic);
}
return result;
}
private static ShaderPropertyType ParsePropertyType(string type, List<DSLShaderError> errors)
{
return type.ToLower() switch
{
"float" => ShaderPropertyType.Float,
"float2" => ShaderPropertyType.Float2,
"float3" => ShaderPropertyType.Float3,
"float4" => ShaderPropertyType.Float4,
"float4x4" => ShaderPropertyType.Float4x4,
"int" => ShaderPropertyType.Int,
"int2" => ShaderPropertyType.Int2,
"int3" => ShaderPropertyType.Int3,
"int4" => ShaderPropertyType.Int4,
"uint" => ShaderPropertyType.UInt,
"uint2" => ShaderPropertyType.UInt2,
"uint3" => ShaderPropertyType.UInt3,
"uint4" => ShaderPropertyType.UInt4,
"bool" => ShaderPropertyType.Bool,
"bool2" => ShaderPropertyType.Bool2,
"bool3" => ShaderPropertyType.Bool3,
"bool4" => ShaderPropertyType.Bool4,
"tex2d" => ShaderPropertyType.Texture2D,
"tex3d" => ShaderPropertyType.Texture3D,
"texcube" => ShaderPropertyType.TextureCube,
"texcube_arr" => ShaderPropertyType.TextureCubeArray,
"tex2d_arr" => ShaderPropertyType.Texture2DArray,
"sampler" => ShaderPropertyType.Sampler,
_ => ShaderPropertyType.None
};
}
private static object? ParsePropertyValue(ShaderPropertyType type, List<string> values, List<DSLShaderError> errors)
{
// For textures, the value is an identifier (e.g., "white", "black")
if (type is ShaderPropertyType.Texture2D or ShaderPropertyType.Texture3D or ShaderPropertyType.TextureCube)
{
return values.Count > 0 ? values[0] : null;
}
// For samplers, no default value
if (type == ShaderPropertyType.Sampler)
{
return null;
}
// For numeric types, parse the values
try
{
return type switch
{
ShaderPropertyType.Float => values.Count > 0 ? float.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture) : 0f,
ShaderPropertyType.Float2 => values.Count >= 2 ? new Misaki.HighPerformance.Mathematics.float2(
float.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture),
float.Parse(values[1], System.Globalization.CultureInfo.InvariantCulture)) : default,
ShaderPropertyType.Float3 => values.Count >= 3 ? new Misaki.HighPerformance.Mathematics.float3(
float.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture),
float.Parse(values[1], System.Globalization.CultureInfo.InvariantCulture),
float.Parse(values[2], System.Globalization.CultureInfo.InvariantCulture)) : default,
ShaderPropertyType.Float4 => values.Count >= 4 ? new Misaki.HighPerformance.Mathematics.float4(
float.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture),
float.Parse(values[1], System.Globalization.CultureInfo.InvariantCulture),
float.Parse(values[2], System.Globalization.CultureInfo.InvariantCulture),
float.Parse(values[3], System.Globalization.CultureInfo.InvariantCulture)) : default,
ShaderPropertyType.Int => values.Count > 0 ? int.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture) : 0,
ShaderPropertyType.Int2 => values.Count >= 2 ? new Misaki.HighPerformance.Mathematics.int2(
int.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture),
int.Parse(values[1], System.Globalization.CultureInfo.InvariantCulture)) : default,
ShaderPropertyType.Int3 => values.Count >= 3 ? new Misaki.HighPerformance.Mathematics.int3(
int.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture),
int.Parse(values[1], System.Globalization.CultureInfo.InvariantCulture),
int.Parse(values[2], System.Globalization.CultureInfo.InvariantCulture)) : default,
ShaderPropertyType.Int4 => values.Count >= 4 ? new Misaki.HighPerformance.Mathematics.int4(
int.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture),
int.Parse(values[1], System.Globalization.CultureInfo.InvariantCulture),
int.Parse(values[2], System.Globalization.CultureInfo.InvariantCulture),
int.Parse(values[3], System.Globalization.CultureInfo.InvariantCulture)) : default,
ShaderPropertyType.UInt => values.Count > 0 ? uint.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture) : 0u,
ShaderPropertyType.Bool => values.Count > 0 && (values[0] == "1" || values[0].ToLower() == "true"),
_ => null
};
}
catch (Exception ex)
{
errors.Add(new DSLShaderError
{
message = $"Failed to parse property value: {ex.Message}",
line = 0,
column = 0
});
return null;
}
}
private static PipelineSemantic? ConvertPipeline(PipelineBlockModel? pipeline, List<DSLShaderError> errors)
{
if (pipeline == null || pipeline.Statements.Count == 0)
{
return null;
}
var semantic = new PipelineSemantic();
foreach (var (key, value) in pipeline.Statements)
{
switch (key.ToLower())
{
case "ztest":
semantic.zTest = value.ToLower() switch
{
"disabled" => ZTest.Disabled,
"less" => ZTest.Less,
"lessequal" => ZTest.LessEqual,
"equal" => ZTest.Equal,
"greaterequal" => ZTest.GreaterEqual,
"greater" => ZTest.Greater,
"notequal" => ZTest.NotEqual,
"always" => ZTest.Always,
_ => ZTest.Disabled
};
break;
case "zwrite":
semantic.zWrite = value.ToLower() == "on" ? ZWrite.On : ZWrite.Off;
break;
case "cull":
semantic.cull = value.ToLower() switch
{
"off" => Cull.Off,
"front" => Cull.Front,
"back" => Cull.Back,
_ => Cull.Off
};
break;
case "blend":
semantic.blend = value.ToLower() switch
{
"opaque" => Blend.Opaque,
"alpha" => Blend.Alpha,
"additive" => Blend.Additive,
"multiply" => Blend.Multiply,
"premultipliedalpha" => Blend.PremultipliedAlpha,
_ => Blend.Opaque
};
break;
case "color_mask":
semantic.colorMask = value.ToLower() == "all" ? ColorWriteMask.All : ColorWriteMask.None;
break;
}
}
return semantic;
}
private static PassSemantic? ConvertPass(PassBlockModel pass, List<DSLShaderError> errors)
{
var semantic = new PassSemantic
{
name = pass.Name,
hlsl = pass.Hlsl?.Code,
defines = pass.Defines?.Defines,
includes = pass.Includes?.Includes,
localPipeline = ConvertPipeline(pass.LocalPipeline, errors)
};
if (pass.Keywords != null)
{
semantic.keywords = new List<KeywordsGroup>();
foreach (var group in pass.Keywords.Groups)
{
var keywordGroup = new KeywordsGroup
{
space = group.Scope?.ToLower() == "global" ? KeywordSpace.Global : KeywordSpace.Local,
keywords = group.Keywords
};
semantic.keywords.Add(keywordGroup);
}
}
foreach (var entry in pass.ShaderEntries)
{
var entryType = entry.EntryType.ToLower();
var shaderEntry = new ShaderEntryPoint
{
shader = entry.ShaderPath,
entry = entry.EntryPoint
};
switch (entryType)
{
case "mesh" or "ms":
semantic.meshShader = shaderEntry;
break;
case "pixel" or "ps":
semantic.pixelShader = shaderEntry;
break;
case "task" or "ts":
semantic.taskShader = shaderEntry;
break;
default:
errors.Add(new DSLShaderError
{
message = $"Unknown shader entry type '{entry.EntryType}'.",
line = 0,
column = 0
});
break;
}
}
if (semantic.meshShader.shader == null || semantic.pixelShader.shader == null)
{
errors.Add(new DSLShaderError
{
message = $"Pass '{pass.Name}' must contain a mesh/ms shader and a pixel/ps shader declaration.",
line = 0,
column = 0
});
}
return semantic;
}
private class ErrorListener : BaseErrorListener, IAntlrErrorListener<int>, IAntlrErrorListener<IToken>
{
private readonly List<DSLShaderError> _errors;
public ErrorListener(List<DSLShaderError> errors)
{
_errors = errors;
}
public void SyntaxError(TextWriter output, IRecognizer recognizer, int offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e)
{
_errors.Add(new DSLShaderError
{
message = msg,
line = line,
column = charPositionInLine
});
}
public new void SyntaxError(TextWriter output, IRecognizer recognizer, IToken offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e)
{
_errors.Add(new DSLShaderError
{
message = msg,
line = line,
column = charPositionInLine
});
}
}
}

View File

@@ -0,0 +1,78 @@
namespace Ghost.DSL.ShaderParser.Model;
public class ShaderModel
{
public string Name { get; set; } = string.Empty;
public PropertiesBlockModel? Properties { get; set; }
public PipelineBlockModel? Pipeline { get; set; }
public List<PassBlockModel> Passes { get; set; } = new();
public List<FunctionCallModel> FunctionCalls { get; set; } = new();
}
public class PropertiesBlockModel
{
public List<PropertyDeclarationModel> Properties { get; set; } = new();
}
public class PropertyDeclarationModel
{
public string? Scope { get; set; }
public string Type { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public List<string> Initializer { get; set; } = new();
}
public class PipelineBlockModel
{
public Dictionary<string, string> Statements { get; set; } = new();
}
public class PassBlockModel
{
public string Name { get; set; } = string.Empty;
public PipelineBlockModel? LocalPipeline { get; set; }
public DefinesBlockModel? Defines { get; set; }
public IncludesBlockModel? Includes { get; set; }
public KeywordsBlockModel? Keywords { get; set; }
public HlslBlockModel? Hlsl { get; set; }
public List<ShaderEntryModel> ShaderEntries { get; set; } = new();
}
public class DefinesBlockModel
{
public List<string> Defines { get; set; } = new();
}
public class IncludesBlockModel
{
public List<string> Includes { get; set; } = new();
}
public class KeywordsBlockModel
{
public List<KeywordGroupModel> Groups { get; set; } = new();
}
public class KeywordGroupModel
{
public string? Scope { get; set; }
public List<string> Keywords { get; set; } = new();
}
public class HlslBlockModel
{
public string Code { get; set; } = string.Empty;
}
public class ShaderEntryModel
{
public string EntryType { get; set; } = string.Empty; // "mesh", "pixel", "task", etc.
public string ShaderPath { get; set; } = string.Empty;
public string EntryPoint { get; set; } = string.Empty;
}
public class FunctionCallModel
{
public string Name { get; set; } = string.Empty;
public List<string> Arguments { get; set; } = new();
}

View File

@@ -0,0 +1,261 @@
using Antlr4.Runtime.Misc;
using Ghost.DSL.ShaderParser.Model;
namespace Ghost.DSL.ShaderParser;
public class ShaderVisitor : GhostShaderParserBaseVisitor<object>
{
public List<ShaderModel> Shaders { get; } = new();
public override object VisitShaderFile([NotNull] GhostShaderParser.ShaderFileContext context)
{
foreach (var shaderContext in context.shader())
{
var shader = (ShaderModel)VisitShader(shaderContext);
Shaders.Add(shader);
}
return Shaders;
}
public override object VisitShader([NotNull] GhostShaderParser.ShaderContext context)
{
var shader = new ShaderModel
{
Name = StripQuotes(context.STRING_LITERAL().GetText())
};
var shaderBody = context.shaderBody();
if (shaderBody != null)
{
foreach (var propBlock in shaderBody.propertiesBlock())
{
shader.Properties = (PropertiesBlockModel)VisitPropertiesBlock(propBlock);
}
foreach (var pipelineBlock in shaderBody.pipelineBlock())
{
shader.Pipeline = (PipelineBlockModel)VisitPipelineBlock(pipelineBlock);
}
foreach (var passBlock in shaderBody.passBlock())
{
shader.Passes.Add((PassBlockModel)VisitPassBlock(passBlock));
}
foreach (var funcCall in shaderBody.functionCall())
{
shader.FunctionCalls.Add((FunctionCallModel)VisitFunctionCall(funcCall));
}
}
return shader;
}
public override object VisitPropertiesBlock([NotNull] GhostShaderParser.PropertiesBlockContext context)
{
var properties = new PropertiesBlockModel();
foreach (var propDecl in context.propertyDeclaration())
{
properties.Properties.Add((PropertyDeclarationModel)VisitPropertyDeclaration(propDecl));
}
return properties;
}
public override object VisitPropertyDeclaration([NotNull] GhostShaderParser.PropertyDeclarationContext context)
{
var property = new PropertyDeclarationModel
{
Type = context.IDENTIFIER(0).GetText(),
Name = context.IDENTIFIER(1).GetText()
};
if (context.scope() != null)
{
property.Scope = context.scope().GetText();
}
if (context.propertyInitializer() != null)
{
var init = context.propertyInitializer();
foreach (var number in init.NUMBER())
{
property.Initializer.Add(number.GetText());
}
foreach (var identifier in init.IDENTIFIER())
{
property.Initializer.Add(identifier.GetText());
}
}
return property;
}
public override object VisitPipelineBlock([NotNull] GhostShaderParser.PipelineBlockContext context)
{
var pipeline = new PipelineBlockModel();
foreach (var statement in context.pipelineStatement())
{
var key = statement.IDENTIFIER(0).GetText();
var value = statement.IDENTIFIER(1).GetText();
pipeline.Statements[key] = value;
}
return pipeline;
}
public override object VisitPassBlock([NotNull] GhostShaderParser.PassBlockContext context)
{
var pass = new PassBlockModel
{
Name = StripQuotes(context.STRING_LITERAL().GetText())
};
var passBody = context.passBody();
if (passBody != null)
{
foreach (var definesBlock in passBody.definesBlock())
{
pass.Defines = (DefinesBlockModel)VisitDefinesBlock(definesBlock);
}
foreach (var includesBlock in passBody.includesBlock())
{
pass.Includes = (IncludesBlockModel)VisitIncludesBlock(includesBlock);
}
foreach (var keywordsBlock in passBody.keywordsBlock())
{
pass.Keywords = (KeywordsBlockModel)VisitKeywordsBlock(keywordsBlock);
}
foreach (var pipelineBlock in passBody.pipelineBlock())
{
pass.LocalPipeline = (PipelineBlockModel)VisitPipelineBlock(pipelineBlock);
}
foreach (var hlslBlock in passBody.hlslBlock())
{
pass.Hlsl = (HlslBlockModel)VisitHlslBlock(hlslBlock);
}
foreach (var shaderEntry in passBody.shaderEntry())
{
pass.ShaderEntries.Add((ShaderEntryModel)VisitShaderEntry(shaderEntry));
}
}
return pass;
}
public override object VisitDefinesBlock([NotNull] GhostShaderParser.DefinesBlockContext context)
{
var defines = new DefinesBlockModel();
foreach (var defineStmt in context.defineStatement())
{
defines.Defines.Add(defineStmt.IDENTIFIER().GetText());
}
return defines;
}
public override object VisitIncludesBlock([NotNull] GhostShaderParser.IncludesBlockContext context)
{
var includes = new IncludesBlockModel();
foreach (var includeStmt in context.includeStatement())
{
includes.Includes.Add(StripQuotes(includeStmt.STRING_LITERAL().GetText()));
}
return includes;
}
public override object VisitKeywordsBlock([NotNull] GhostShaderParser.KeywordsBlockContext context)
{
var keywords = new KeywordsBlockModel();
foreach (var keywordStmt in context.keywordStatement())
{
var group = new KeywordGroupModel();
if (keywordStmt.scope() != null)
{
group.Scope = keywordStmt.scope().GetText();
}
foreach (var identifier in keywordStmt.IDENTIFIER())
{
group.Keywords.Add(identifier.GetText());
}
keywords.Groups.Add(group);
}
return keywords;
}
public override object VisitHlslBlock([NotNull] GhostShaderParser.HlslBlockContext context)
{
var hlsl = new HlslBlockModel();
// Get the text between the braces
var start = context.LBRACE().Symbol.StopIndex + 1;
var stop = context.RBRACE().Symbol.StartIndex - 1;
if (stop >= start)
{
var input = context.Start.InputStream;
hlsl.Code = input.GetText(new Antlr4.Runtime.Misc.Interval(start, stop));
}
return hlsl;
}
public override object VisitShaderEntry([NotNull] GhostShaderParser.ShaderEntryContext context)
{
var entry = new ShaderEntryModel
{
EntryType = context.IDENTIFIER().GetText(),
ShaderPath = StripQuotes(context.STRING_LITERAL(0).GetText()),
EntryPoint = StripQuotes(context.STRING_LITERAL(1).GetText())
};
return entry;
}
public override object VisitFunctionCall([NotNull] GhostShaderParser.FunctionCallContext context)
{
var funcCall = new FunctionCallModel
{
Name = context.IDENTIFIER().GetText()
};
if (context.functionArguments() != null)
{
foreach (var arg in context.functionArguments().functionArgument())
{
var text = arg.GetText();
if (text.StartsWith('"'))
{
text = StripQuotes(text);
}
funcCall.Arguments.Add(text);
}
}
return funcCall;
}
private static string StripQuotes(string text)
{
if (text.Length >= 2 && text.StartsWith('"') && text.EndsWith('"'))
{
return text.Substring(1, text.Length - 2);
}
return text;
}
}

View File

@@ -0,0 +1,9 @@
using Ghost.Core.Attributes;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ghost.Editor")]
[assembly: InternalsVisibleTo("Ghost.Editor.Core")]
[assembly: InternalsVisibleTo("Ghost.UnitTest")]
[assembly: InternalsVisibleTo("Ghost.MicroTest")]
[assembly: EngineAssembly]

Binary file not shown.

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
<PackageReference Include="System.Drawing.Common" Version="4.7.3" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\ProjectTemplates\Empty.zip">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
</ItemGroup>
</Project>

11
Ghost.Data/JsonContext.cs Normal file
View File

@@ -0,0 +1,11 @@
using Ghost.Data.Models;
using System.Text.Json.Serialization;
namespace Ghost.Data;
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(TemplateInfo))]
[JsonSerializable(typeof(ProjectMetadata))]
internal partial class JsonContext : JsonSerializerContext
{
}

View File

@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Ghost.Data.Models;
internal class ProjectInfo
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID
{
get; internal set;
}
public required string Name
{
get; set;
}
public required string MetadataPath
{
get; set;
}
}

View File

@@ -0,0 +1,53 @@
namespace Ghost.Data.Models;
public class ProjectMetadata
{
public const string PROJECT_FILE_EXTENSION_NAME = "gproj";
public Guid ID
{
get; set;
}
public string Name
{
get; set;
}
public Version EngineVersion
{
get; set;
}
public DateTime CreatedAt
{
get; set;
}
public DateTime LastOpened
{
get; set;
}
public ProjectMetadata(string name, Version engineVersion)
{
ID = Guid.NewGuid();
Name = name;
EngineVersion = engineVersion;
CreatedAt = DateTime.UtcNow;
LastOpened = DateTime.UtcNow;
}
// Parameterless constructor for deserialization
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public ProjectMetadata()
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
{
}
}
public readonly struct ProjectMetadataInfo(string path, ProjectMetadata metadata)
{
public readonly string Path => path;
public readonly ProjectMetadata Metadata => metadata;
}

View File

@@ -0,0 +1,44 @@
namespace Ghost.Data.Models;
public class TemplateInfo
{
public required string Name
{
get; set;
}
public string? Description
{
get; set;
}
public required Version TemplateVersion
{
get; set;
}
public required Version EngineVersion
{
get; set;
}
}
public struct TemplateData(string templatePath, TemplateInfo info)
{
private const string _ICON_NAME = "icon.png";
private const string _PREVIEW_NAME = "preview.png";
public string directory = Path.GetDirectoryName(templatePath)!;
public readonly TemplateInfo Info => info;
public readonly Uri GetIconURI()
{
return new Uri(Path.Combine(directory, _ICON_NAME));
}
public readonly Uri GetPreviewURI()
{
return new Uri(Path.Combine(directory, _PREVIEW_NAME));
}
}

View File

@@ -0,0 +1,6 @@
namespace Ghost.Data.Repository;
internal class AssetsRepository
{
}

View File

@@ -0,0 +1,172 @@
using Ghost.Data.Models;
using Ghost.Data.Resources;
using System.Data.SQLite;
namespace Ghost.Data.Repository;
internal static class ProjectRepository
{
private static class Command
{
public const string CONNECTION_STRING = "Data Source={0}\\projects.db;Version=3;";
public const string CREATE_PROJECT_TABLE_STRING = "CREATE TABLE IF NOT EXISTS Projects (ID INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT, MetadataPath TEXT);";
public const string SELECT_PROJECT_STRING = "SELECT * FROM Projects";
public const string INSERT_PROJECT_STRING = "INSERT INTO Projects (Name, MetadataPath) VALUES (@Name, @MetadataPath);";
public const string REMOVE_PROJECT_STRING = "DELETE FROM Projects WHERE ID = @ID;";
public const string UPDATE_PROJECT_STRING = "UPDATE Projects SET Name = @Name, MetadataPath = @MetadataPath WHERE ID = @ID;";
}
private static async Task EnsureTableCreatedAsync(SQLiteConnection connection)
{
using var createCommand = connection.CreateCommand();
createCommand.CommandText = Command.CREATE_PROJECT_TABLE_STRING;
await createCommand.ExecuteNonQueryAsync();
}
public static async IAsyncEnumerable<ProjectInfo> GetAllProjectsAsync()
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
await EnsureTableCreatedAsync(connection);
using var command = connection.CreateCommand();
command.CommandText = Command.SELECT_PROJECT_STRING;
using var reader = command.ExecuteReader();
while (await reader.ReadAsync())
{
var project = new ProjectInfo
{
ID = reader.GetInt32(0),
Name = reader.GetString(1),
MetadataPath = reader.GetString(2),
};
yield return project;
}
}
public static async Task<ProjectInfo?> GetProjectByIdAsync(int id)
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
await EnsureTableCreatedAsync(connection);
using var command = connection.CreateCommand();
command.CommandText = Command.SELECT_PROJECT_STRING + " WHERE ID = @ID;";
command.Parameters.AddWithValue("@ID", id);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return new ProjectInfo
{
ID = reader.GetInt32(0),
Name = reader.GetString(1),
MetadataPath = reader.GetString(2),
};
}
return null;
}
public static async Task<ProjectInfo?> GetProjectByNameAsync(string name)
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
await EnsureTableCreatedAsync(connection);
using var command = connection.CreateCommand();
command.CommandText = Command.SELECT_PROJECT_STRING + " WHERE Name = @Name;";
command.Parameters.AddWithValue("@Name", name);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return new ProjectInfo
{
ID = reader.GetInt32(0),
Name = reader.GetString(1),
MetadataPath = reader.GetString(2),
};
}
return null;
}
public static async Task<ProjectInfo?> GetProjectByMetadataPathAsync(string metadataPath)
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
await EnsureTableCreatedAsync(connection);
using var command = connection.CreateCommand();
command.CommandText = Command.SELECT_PROJECT_STRING + " WHERE MetadataPath = @MetadataPath;";
command.Parameters.AddWithValue("@MetadataPath", metadataPath);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return new ProjectInfo
{
ID = reader.GetInt32(0),
Name = reader.GetString(1),
MetadataPath = reader.GetString(2),
};
}
return null;
}
public static async Task AddProjectAsync(ProjectInfo project)
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
await EnsureTableCreatedAsync(connection);
using var command = connection.CreateCommand();
command.CommandText = Command.INSERT_PROJECT_STRING;
command.Parameters.AddWithValue("@Name", project.Name);
command.Parameters.AddWithValue("@MetadataPath", project.MetadataPath);
await command.ExecuteNonQueryAsync();
}
public static async Task RemoveProjectAsync(ProjectInfo project)
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = Command.REMOVE_PROJECT_STRING;
command.Parameters.AddWithValue("@ID", project.ID);
await command.ExecuteNonQueryAsync();
}
public static async Task UpdateProjectAsync(ProjectInfo project)
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = Command.UPDATE_PROJECT_STRING;
command.Parameters.AddWithValue("@Name", project.Name);
command.Parameters.AddWithValue("@MetadataPath", project.MetadataPath);
command.Parameters.AddWithValue("@ID", project.ID);
await command.ExecuteNonQueryAsync();
}
}

View File

@@ -0,0 +1,8 @@
namespace Ghost.Data.Resources;
public static class AssetsPath
{
public const string ASSETS_FOLDER = "Assets";
public readonly static string s_appIconPath = Path.Combine(AppContext.BaseDirectory, $"{ASSETS_FOLDER}/Icon-256.ico");
}

View File

@@ -0,0 +1,9 @@
namespace Ghost.Data.Resources;
public class DataPath
{
public const string ENGINE_DATA_FOLDER_NAME = "GhostEngine";
public readonly static string s_applicationDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ENGINE_DATA_FOLDER_NAME);
public readonly static string s_projectTemplateFolder = Path.Combine(s_applicationDataFolder, "ProjectTemplates");
}

View File

@@ -0,0 +1,226 @@
using Ghost.Core;
using Ghost.Data.Models;
using Ghost.Data.Repository;
using Ghost.Data.Resources;
using System.IO.Compression;
using System.Text.Json;
namespace Ghost.Data.Services;
internal partial class ProjectService
{
private const string _TEMPLATE_CONTENT_FILE = "content.zip";
public const string ASSETS_FOLDER = "Assets";
public const string CACHE_FOLDER = "Caches";
public const string CONFIG_FOLDER = "Configs";
public static ProjectMetadataInfo CurrentProject
{
get;
set;
}
public static void EnsureDefaultTemplate()
{
var templates = Directory.GetFiles(DataPath.s_projectTemplateFolder, "template.json", SearchOption.AllDirectories);
if (templates.Length > 0)
{
return; // Default template already exists
}
var defaultTemplatePath = Path.Combine(AppContext.BaseDirectory, "Assets/ProjectTemplates/Empty.zip");
ZipFile.ExtractToDirectory(defaultTemplatePath, DataPath.s_projectTemplateFolder, true);
}
public static async IAsyncEnumerable<(string path, TemplateInfo info)> GetProjectTemplatesAsync()
{
var templatesFolder = DataPath.s_projectTemplateFolder;
if (!Directory.Exists(templatesFolder))
{
yield break;
}
var templates = Directory.GetFiles(DataPath.s_projectTemplateFolder, "template.json", SearchOption.AllDirectories);
foreach (var templatePath in templates)
{
var fileStream = File.OpenRead(templatePath);
var templateInfo = await JsonSerializer.DeserializeAsync<TemplateInfo>(fileStream, JsonContext.Default.TemplateInfo);
if (templateInfo == null)
{
continue;
}
yield return (templatePath, templateInfo);
}
}
public static async Task CreateMetadataFileAsync(string path, ProjectMetadata metadata)
{
await using var fileStream = File.Create(path);
await JsonSerializer.SerializeAsync(fileStream, metadata, JsonContext.Default.ProjectMetadata);
}
public static async Task<ProjectMetadata?> LoadMetadataAsync(string ghostprojPath)
{
if (!File.Exists(ghostprojPath))
{
throw new FileNotFoundException("Project metadata file not found.", ghostprojPath);
}
await using var fileStream = File.OpenRead(ghostprojPath);
return await JsonSerializer.DeserializeAsync<ProjectMetadata>(fileStream, JsonContext.Default.ProjectMetadata);
}
public static async Task<Result<ProjectMetadataInfo>> ValidateProjectDirectoryAsync(string? projectDirectory)
{
if (string.IsNullOrWhiteSpace(projectDirectory) || !Directory.Exists(projectDirectory))
{
return Result<ProjectMetadataInfo>.Failure("Project directory is invalid or does not exist.");
}
var projectAssetsPath = Path.Combine(projectDirectory, ASSETS_FOLDER);
var projectConfigPath = Path.Combine(projectDirectory, CONFIG_FOLDER);
if (!Directory.Exists(projectAssetsPath) || !Directory.Exists(projectConfigPath))
{
return Result<ProjectMetadataInfo>.Failure("Project folder structure is invalid.");
}
var metadataPath = Directory.GetFiles(projectDirectory, $"*.{ProjectMetadata.PROJECT_FILE_EXTENSION_NAME}", SearchOption.TopDirectoryOnly).FirstOrDefault();
if (string.IsNullOrWhiteSpace(metadataPath) || !File.Exists(metadataPath))
{
return Result<ProjectMetadataInfo>.Failure("Project metadata file not found.");
}
var metadata = await LoadMetadataAsync(metadataPath);
if (metadata == null)
{
return Result<ProjectMetadataInfo>.Failure("Project metadata file is corrupted or invalid.");
}
return new ProjectMetadataInfo(metadataPath, metadata);
}
private static async ValueTask SetupRequestFolderAsync(string projectDirectory, string templateDirectory)
{
var projectAssetsPath = Path.Combine(projectDirectory, ASSETS_FOLDER);
var projectConfigPath = Path.Combine(projectDirectory, CONFIG_FOLDER);
var templateContentPath = Path.Combine(templateDirectory, _TEMPLATE_CONTENT_FILE);
Directory.CreateDirectory(projectAssetsPath);
if (File.Exists(templateContentPath))
{
await Task.Run(() =>
{
ZipFile.ExtractToDirectory(templateContentPath, projectAssetsPath);
});
}
Directory.CreateDirectory(projectConfigPath);
}
}
internal partial class ProjectService
{
public Task AddProjectAsync(ProjectInfo project)
{
return ProjectRepository.AddProjectAsync(project);
}
public async Task<ProjectInfo> AddProjectAsync(string name, string path)
{
var project = new ProjectInfo
{
Name = name,
MetadataPath = path,
};
await ProjectRepository.AddProjectAsync(project);
return project;
}
public Task RemoveProjectAsync(ProjectInfo project)
{
return ProjectRepository.RemoveProjectAsync(project);
}
public Task UpdateProjectAsync(ProjectInfo project)
{
return ProjectRepository.UpdateProjectAsync(project);
}
public async Task<bool> HasProjectAsync(string path)
{
return await ProjectRepository.GetProjectByMetadataPathAsync(path) != null;
}
public async IAsyncEnumerable<ProjectInfo> GetAllProjectAsync()
{
var badProjectList = new List<ProjectInfo>();
await foreach (var project in ProjectRepository.GetAllProjectsAsync())
{
if (string.IsNullOrWhiteSpace(project.MetadataPath) || !File.Exists(project.MetadataPath))
{
badProjectList.Add(project);
continue;
}
yield return project;
}
foreach (var badProject in badProjectList)
{
await ProjectRepository.RemoveProjectAsync(badProject);
}
}
public async Task<Result<ProjectMetadataInfo>> CreateProjectAsync(string projectName, string projectDirectory, Version engineVersion, string templatePath)
{
try
{
var projectPath = Path.Combine(projectDirectory, projectName);
if (!Directory.Exists(projectPath))
{
Directory.CreateDirectory(projectPath);
}
else
{
// Check if folder is empty
if (Directory.EnumerateFiles(projectPath, "*", SearchOption.AllDirectories).Any())
{
return Result.Failure("Directory is not empty");
}
}
var metadata = new ProjectMetadata(projectName, engineVersion);
var metadataPath = Path.Combine(projectPath, $"{projectName}.{ProjectMetadata.PROJECT_FILE_EXTENSION_NAME}");
await CreateMetadataFileAsync(metadataPath, metadata);
await SetupRequestFolderAsync(projectPath, templatePath);
var info = await AddProjectAsync(projectName, metadataPath);
return Result.Success(new ProjectMetadataInfo(metadataPath, metadata));
}
catch (Exception e)
{
return Result.Failure($"Failed to create project: {e.Message}");
}
}
public async Task<Result<ProjectMetadataInfo>> AddProjectFromDirectoryAsync(string projectDirectory)
{
var result = await ValidateProjectDirectoryAsync(projectDirectory);
if (result.IsFailure)
{
return result;
}
if (await HasProjectAsync(result.Value.Path))
{
return Result.Failure("Project already exists.");
}
await AddProjectAsync(result.Value.Metadata.Name, result.Value.Path);
return result;
}
}

View File

@@ -0,0 +1,97 @@
using Ghost.Core;
namespace Ghost.Editor.Core.AppState;
internal partial class AppStateMachine : IDisposable, IAsyncDisposable
{
private Dictionary<StateKey, Lazy<IAppState>> _states = new();
private IAppState? _current;
private bool _disposed;
public void RegisterState(StateKey key, Func<IAppState> stateFactory)
{
_states[key] = new(stateFactory);
}
public async Task<Result> TransitionToAsync(StateKey stateKey, object? parameter = null)
{
var previous = _current;
if (!_states.TryGetValue(stateKey, out var next))
{
return Result.Failure($"State '{stateKey}' not found.");
}
Result result;
if (previous != null)
{
result = await previous.OnExitingAsync();
if (result.IsFailure)
{
return result;
}
}
result = await next.Value.OnEnteringAsync(parameter);
if (result.IsFailure)
{
if (previous != null)
{
await previous.OnEnteredAsync(parameter);
}
return result;
}
if (previous != null)
{
result = await previous.OnExitedAsync();
if (result.IsFailure)
{
await next.Value.OnExitedAsync();
await previous.OnEnteredAsync(parameter);
return result;
}
}
result = await next.Value.OnEnteredAsync(parameter);
if (result.IsFailure)
{
await next.Value.OnExitedAsync();
if (previous != null)
{
await previous.OnEnteredAsync(parameter);
}
return result;
}
_current = next.Value;
return Result.Success();
}
public void Dispose()
{
DisposeAsync().AsTask().Wait();
}
public async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
_states.Clear();
if (_current != null)
{
await _current.OnExitingAsync();
await _current.OnExitedAsync();
}
_current = null;
_disposed = true;
}
}

View File

@@ -0,0 +1,28 @@
using Ghost.Core;
namespace Ghost.Editor.Core.AppState;
internal interface IAppState
{
/// <summary>
/// Called when exiting the state.
/// </summary>
public ValueTask<Result> OnExitingAsync();
/// <summary>
/// Called when entering the state, right after OnEnteringAsync.
/// <paramref name="parameter">can be used to pass data into the state, such as a project to load.</summary>
/// </summary>
public ValueTask<Result> OnEnteringAsync(object? parameter);
/// <summary>
/// Called when exiting the state, specifically for pose transitions.
/// </summary>
public ValueTask<Result> OnExitedAsync();
/// <summary>
/// Called when entered the state, specifically after the state has been fully initialized and is ready for interaction.
/// </summary>
/// <param name="parameter">can be used to pass data into the state, such as a project to load.</param>
public ValueTask<Result> OnEnteredAsync(object? parameter);
}

View File

@@ -0,0 +1,8 @@
namespace Ghost.Editor.Core.AppState;
internal enum StateKey
{
None,
Landing,
EngineEditor,
}

View File

@@ -0,0 +1,7 @@
using Ghost.Core.Attributes;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ghost.UnitTest")]
[assembly: InternalsVisibleTo("Ghost.Editor")]
[assembly: EngineAssembly]

View File

@@ -0,0 +1,134 @@
# Asset Database Plan
AssetDB is a core component of the Ghost Editor that manages the storage, retrieval, and organization of various assets used within the editor.
This document outlines the plan for implementing the AssetDB, including its structure, functionality, and integration with other components of the Ghost Editor.
## Data Structure
- Asset Metadata: Each asset will have associated metadata, including:
- Unique Identifier (GUID)
- Version (Version of the asset pipeline, not the asset. This is primarily for migration when we redesign the asset pipeline in the future)
- Tags
- Importer Settings
An simplified example of metadata file (filename.png.gmeta):
```json
{
"Guid": "123e4567-e89b-12d3-a456-426614174000",
"Version": 1,
"Tags": ["Environment", "Texture"],
"ImporterSettings": [
"TextureImporter": {
"Version": 1,
"MaxSize": 2048,
"MipLevels": 1
},
"OtherImporter": {
}
]
}
```
- Asset: The base class for all assets.
- Asset Database: A centralized database that stores and manages all assets. It will handle:
- Asset registration and deregistration
- Asset lookup by GUID
- Asset lookup by path
- Automatic handle file creation, remove, rename, move, etc.
- Asset dependency management
- Automatic asset re-importing when source files change.
- Asset tagging.
- Add type specific default importer settings for new asset.
- SQLite (`Microsoft.Data.Sqlite`) for persistent storage and efficient querying.
An simplified data model example in SQLite:
```sql
CREATE TABLE Assets (
Guid TEXT PRIMARY KEY,
Path TEXT NOT NULL,
Type INTEGER,
Version INTEGER,
Tags TEXT,
DependencyGuids TEXT
);
```
## Simplified Workflow
### New Asset Addition
1. A file is added to the project directory.
2. Generates the metadata for the asset with name filename.etx + ".gmeta" (You can get the extension from Ghost.Editor.Core.Utilities.FileExtensions.META_FILE_EXTENSION)
3. Add the asset to the database.
### File Removal
1. A file is removed from the project directory.
2. Deletes the corresponding asset metadata.
3. Remove the asset from the database.
4. Mark dependent assets as dirty for re-importing.
### File Renaming/Moving
1. A file is renamed or moved within the project directory.
2. Check if the new path has an existing metadata file (just in case user move the file with the metadata file together).
- If exists, validate the data and update the database accordingly.
- if not, regenerate the metadata file for the new path and update the database.
3. Delete the old metadata file if exists.
### File Modification
1. A file is modified in the project directory.
2. Check the file hash to see if it has changed.
- If changed, mark the asset as dirty for re-importing.
- If not, do nothing.
### Asset Importing (You don't need to write any assets importer right now, just write the framework and a simple test importer if it's needed for unit test)
1. An asset is marked as dirty.
2. The asset importer for that type processes the asset based on its importer settings.
3. Validate the references and dependencies, report errors if any (for example, missing dependencies).
## Features Checklist
### In Code (API)
- [ ] Find GUID by path
- [ ] Find path by GUID
- [ ] Load asset by GUID (May need special asset loader, not included in this plan, leave an API and TODO comment)
- [ ] API for adding/removing/moving/copying assets
- [ ] API for opening asset in editor or external program
- [ ] API to set asset dirty and save all assets if dirty
- [ ] Get and set asset tags.
- [ ] Refresh asset database (re-scan project directory for changes)
### In Editor (API Only, I will handle the UI part)
- [ ] Asset Browser window to view and manage assets (automatically refresh when assets change)
- [ ] Skip meta file in asset browser view
- [ ] Search assets by name, tag, type, etc.
### In Background
- [ ] File system watcher to monitor changes in the project directory and update the AssetDB accordingly.
- [ ] Automatic asset re-importing when source files change (detect changes via file system watcher and quick hash comparison).
- [ ] Asset dependency management.
- [ ] Validate and fix AssetDB on project load (check for missing/corrupted assets if user add/delete/rename/move files when the editor is not running, etc.)
- [ ] Asset importer system to handle different asset types and their import settings.
## Testing
Make sure everything builds correctly at first.
Write unit tests and integration tests to ensure the AssetDB functions correctly inside the `Ghost.UnitTests` project.
## Critical Considerations
- Performance: Ensure that the AssetDB operations are efficient, especially for large projects with many assets.
- Stability: The meta data files should be the only source of truth for asset information.
The AssetDB should be able to recover from inconsistencies by re-generating data from the meta files.
We still need to trust the meta data files even if they are corrupted or missing by regenerating them when necessary.
Database is used for caching and quick lookup only.
- Asynchronous patterns: Consider using asynchronous operations for file I/O and database operations to avoid blocking the main thread.
Packages like `Microsoft.Data.Sqlite` support async operations.

View File

@@ -0,0 +1,355 @@
using Ghost.Core;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
/// <summary>
/// Create a new asset at the specified path.
/// Generates metadata and adds it to the database.
/// </summary>
/// <param name="assetPath">Path to create the asset at.</param>
/// <param name="content">Content to write to the asset file.</param>
/// <returns>Result indicating success or failure.</returns>
public static async ValueTask<Result> CreateAssetAsync(string assetPath, ReadOnlyMemory<byte> content, CancellationToken token = default)
{
if (AssetsDirectory == null)
{
return Result.Failure("AssetsDirectory not initialized");
}
if (!assetPath.StartsWith(AssetsDirectory.FullName, StringComparison.OrdinalIgnoreCase))
{
return Result.Failure("Asset path must be within the Assets directory");
}
if (File.Exists(assetPath))
{
return Result.Failure("Asset already exists");
}
try
{
var directory = Path.GetDirectoryName(assetPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
using var fs = File.Create(assetPath);
await fs.WriteAsync(content, token);
// GenerateMetaFileAsync will be called automatically by the file watcher
// But we'll call it directly to ensure it's created immediately
await GenerateMetaFileAsync(assetPath, token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to create asset: {ex.Message}");
}
}
/// <summary>
/// Create an empty asset at the specified path.
/// Generates metadata and adds it to the database.
/// </summary>
/// <param name="assetPath">Path to create the asset at.</param>
/// <returns>Result indicating success or failure.</returns>
public static ValueTask<Result> CreateAssetAsync(string assetPath, CancellationToken token = default)
{
return CreateAssetAsync(assetPath, ReadOnlyMemory<byte>.Empty, token);
}
/// <summary>
/// Delete an asset and its metadata.
/// </summary>
/// <param name="guid">GUID of the asset to delete.</param>
/// <returns>Result indicating success or failure.</returns>
public static async ValueTask<Result> DeleteAssetAsync(Guid guid, CancellationToken token = default)
{
var pathResult = GuidToPath(guid);
if (pathResult.IsFailure)
{
return Result.Failure(pathResult.Message);
}
var fullPathResult = GetFullPath(pathResult.Value);
if (fullPathResult.IsFailure)
{
return Result.Failure(fullPathResult.Message);
}
try
{
var assetPath = fullPathResult.Value;
// Delete the asset file
if (File.Exists(assetPath))
{
File.Delete(assetPath);
}
// Delete the .gmeta file
var metaPath = assetPath + Utilities.FileExtensions.META_FILE_EXTENSION;
if (File.Exists(metaPath))
{
File.Delete(metaPath);
}
// Remove from database
await RemoveAssetFromDatabaseAsync(guid, token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to delete asset: {ex.Message}");
}
}
/// <summary>
/// Delete an asset and its metadata by path.
/// </summary>
/// <param name="assetPath">Path to the asset to delete.</param>
/// <returns>Result indicating success or failure.</returns>
public static ValueTask<Result> DeleteAssetAsync(string assetPath, CancellationToken token = default)
{
var guidResult = PathToGuid(assetPath);
if (guidResult.IsFailure)
{
return new ValueTask<Result>(Task.FromResult(Result.Failure(guidResult.Message)));
}
return DeleteAssetAsync(guidResult.Value, token);
}
/// <summary>
/// Move an asset to a new location.
/// </summary>
/// <param name="guid">GUID of the asset to move.</param>
/// <param name="newPath">New path for the asset (relative or absolute).</param>
/// <returns>Result indicating success or failure.</returns>
public static async ValueTask<Result> MoveAssetAsync(Guid guid, string newPath, CancellationToken token = default)
{
var oldPathResult = GuidToPath(guid);
if (oldPathResult.IsFailure)
{
return Result.Failure(oldPathResult.Message);
}
var oldFullPathResult = GetFullPath(oldPathResult.Value);
if (oldFullPathResult.IsFailure)
{
return Result.Failure(oldFullPathResult.Message);
}
if (AssetsDirectory == null)
{
return Result.Failure("AssetsDirectory not initialized");
}
// Ensure new path is absolute and within assets directory
if (!Path.IsPathRooted(newPath))
{
newPath = Path.Combine(AssetsDirectory.FullName, newPath);
}
if (!newPath.StartsWith(AssetsDirectory.FullName, StringComparison.OrdinalIgnoreCase))
{
return Result.Failure("New path must be within the Assets directory");
}
if (File.Exists(newPath))
{
return Result.Failure("A file already exists at the new path");
}
try
{
var directory = Path.GetDirectoryName(newPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// Read metadata and calculate hash before moving
var metaResult = await ReadMetaFileAsync(oldFullPathResult.Value, token);
if (metaResult.IsFailure)
{
return Result.Failure(metaResult.Message);
}
var fileHash = await CalculateFileHashAsync(oldFullPathResult.Value, token);
// Move the asset file
File.Move(oldFullPathResult.Value, newPath);
// Move the .gmeta file
var oldMetaPath = oldFullPathResult.Value + Utilities.FileExtensions.META_FILE_EXTENSION;
var newMetaPath = newPath + Utilities.FileExtensions.META_FILE_EXTENSION;
if (File.Exists(oldMetaPath))
{
File.Move(oldMetaPath, newMetaPath);
}
// Update database directly (bypassing file watcher)
await UpsertAssetAsync(newPath, metaResult.Value, fileHash, null, token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to move asset: {ex.Message}");
}
}
/// <summary>
/// Move an asset to a new location by path.
/// </summary>
/// <param name="oldPath">Current path of the asset.</param>
/// <param name="newPath">New path for the asset (relative or absolute).</param>
/// <returns>Result indicating success or failure.</returns>
public static ValueTask<Result> MoveAssetAsync(string oldPath, string newPath, CancellationToken token = default)
{
var guidResult = PathToGuid(oldPath);
if (guidResult.IsFailure)
{
return ValueTask.FromResult(Result.Failure(guidResult.Message));
}
return MoveAssetAsync(guidResult.Value, newPath, token);
}
/// <summary>
/// Copy an asset to a new location with a new GUID.
/// </summary>
/// <param name="guid">GUID of the asset to copy.</param>
/// <param name="newPath">New path for the copied asset (relative or absolute).</param>
/// <returns>Result containing the new asset's GUID.</returns>
public static async ValueTask<Result<Guid>> CopyAssetAsync(Guid guid, string newPath, CancellationToken token = default)
{
var oldPathResult = GuidToPath(guid);
if (oldPathResult.IsFailure)
{
return Result<Guid>.Failure(oldPathResult.Message);
}
var oldFullPathResult = GetFullPath(oldPathResult.Value);
if (oldFullPathResult.IsFailure)
{
return Result<Guid>.Failure(oldFullPathResult.Message);
}
if (AssetsDirectory == null)
{
return Result<Guid>.Failure("AssetsDirectory not initialized");
}
// Ensure new path is absolute and within assets directory
if (!Path.IsPathRooted(newPath))
{
newPath = Path.Combine(AssetsDirectory.FullName, newPath);
}
if (!newPath.StartsWith(AssetsDirectory.FullName, StringComparison.OrdinalIgnoreCase))
{
return Result<Guid>.Failure("New path must be within the Assets directory");
}
if (File.Exists(newPath))
{
return Result<Guid>.Failure("A file already exists at the new path");
}
try
{
var directory = Path.GetDirectoryName(newPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
await using var oldFs = File.OpenRead(oldFullPathResult.Value);
await using var newFs = File.Create(newPath);
await oldFs.CopyToAsync(newFs, token);
// Generate new metadata with new GUID
await GenerateMetaFileAsync(newPath, token);
// Get the new GUID
var newGuidResult = PathToGuid(newPath);
if (newGuidResult.IsFailure)
{
return Result<Guid>.Failure(newGuidResult.Message);
}
return newGuidResult.Value;
}
catch (Exception ex)
{
return Result<Guid>.Failure($"Failed to copy asset: {ex.Message}");
}
}
/// <summary>
/// Copy an asset to a new location by path.
/// </summary>
/// <param name="sourcePath">Path of the asset to copy.</param>
/// <param name="destPath">New path for the copied asset (relative or absolute).</param>
/// <returns>Result containing the new asset's GUID.</returns>
public static ValueTask<Result<Guid>> CopyAssetAsync(string sourcePath, string destPath, CancellationToken token = default)
{
var guidResult = PathToGuid(sourcePath);
if (guidResult.IsFailure)
{
return new ValueTask<Result<Guid>>(Task.FromResult(Result<Guid>.Failure(guidResult.Message)));
}
return CopyAssetAsync(guidResult.Value, destPath, token);
}
/// <summary>
/// Mark an asset as dirty for re-importing (in-memory only).
/// </summary>
/// <param name="guid">GUID of the asset to mark dirty.</param>
/// <returns>Result indicating success or failure.</returns>
public static Result MarkDirtyAsync(Guid guid, CancellationToken token = default)
{
MarkDirty(guid);
return Result.Success();
}
/// <summary>
/// Import all dirty assets.
/// </summary>
/// <returns>Result indicating success or failure.</returns>
public static async Task<Result> ImportDirtyAssetsAsync(CancellationToken token = default)
{
var dirtyGuids = GetDirtyAssets();
foreach (var guid in dirtyGuids)
{
var pathResult = GuidToPath(guid);
if (pathResult.IsFailure)
{
continue;
}
var fullPathResult = GetFullPath(pathResult.Value);
if (fullPathResult.IsFailure)
{
continue;
}
var result = await ImportAssetAsync(fullPathResult.Value, token);
if (result.IsSuccess)
{
ClearDirty(guid);
}
}
return Result.Success();
}
}

View File

@@ -0,0 +1,128 @@
using Ghost.Core;
using System.Reflection;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
private static readonly Dictionary<Type, AssetImporter> s_importerInstances = new();
/// <summary>
/// Import an asset at the specified path.
/// </summary>
/// <param name="assetPath">Full path to the asset file.</param>
/// <returns>Result indicating success or failure.</returns>
private static async ValueTask<Result> ImportAssetAsync(string assetPath, CancellationToken token = default)
{
var extension = Path.GetExtension(assetPath);
if (!s_importerTypeLookup.TryGetValue(extension, out var importerType))
{
// No importer registered for this file type
return Result.Success();
}
// Get or create importer instance
if (!s_importerInstances.TryGetValue(importerType, out var importerInstance))
{
importerInstance = Activator.CreateInstance(importerType) as AssetImporter;
if (importerInstance is null)
{
return Result.Failure($"Failed to create importer instance for type {importerType.Name}");
}
s_importerInstances[importerType] = importerInstance;
}
// Read metadata
var metaResult = await ReadMetaFileAsync(assetPath, token);
if (metaResult.IsFailure)
{
return Result.Failure($"Failed to read asset metadata: {metaResult.Message}");
}
return await importerInstance.ImportAsync(assetPath, metaResult.Value, token);
}
/// <summary>
/// Get the importer type for a specific file extension.
/// </summary>
/// <param name="extension">File extension (e.g., ".png").</param>
/// <returns>The importer type if found, otherwise null.</returns>
public static Type? GetImporterType(string extension)
{
s_importerTypeLookup.TryGetValue(extension, out var importerType);
return importerType;
}
/// <summary>
/// Get all registered importer types and their supported extensions.
/// </summary>
/// <returns>Dictionary mapping extensions to importer types.</returns>
public static Dictionary<string, Type> GetAllImporters()
{
return new Dictionary<string, Type>(s_importerTypeLookup);
}
/// <summary>
/// Export in-memory asset data to disk.
/// The importer will serialize the data into a format it can later import.
/// </summary>
/// <typeparam name="T">Type of asset data to export.</typeparam>
/// <param name="assetPath">Full path where the asset should be saved.</param>
/// <param name="assetData">In-memory asset data to export.</param>
/// <returns>Result with the GUID of the exported asset.</returns>
public static async ValueTask<Result<Guid>> ExportAssetAsync<T>(string assetPath, T assetData, CancellationToken token = default) where T : class
{
var extension = Path.GetExtension(assetPath);
if (!s_importerTypeLookup.TryGetValue(extension, out var importerType))
{
return Result<Guid>.Failure($"No importer registered for extension {extension}");
}
// Get or create importer instance
if (!s_importerInstances.TryGetValue(importerType, out var importerInstance))
{
importerInstance = Activator.CreateInstance(importerType) as AssetImporter;
if (importerInstance is null)
{
return Result<Guid>.Failure($"Failed to create importer instance for type {importerType.Name}");
}
s_importerInstances[importerType] = importerInstance;
}
// Find and invoke the ExportAsync method
var exportMethod = importerType.GetMethod("ExportAsync", BindingFlags.Public | BindingFlags.Instance);
if (exportMethod == null)
{
return Result<Guid>.Failure($"ExportAsync method not found on importer {importerType.Name}. This importer does not support exporting.");
}
// Generate metadata for the new asset
var result = await GenerateMetaFileAsync(assetPath, token);
if (result.IsFailure)
{
return Result<Guid>.Failure($"Failed to generate metadata: {result.Message}");
}
var metaResult = await ReadMetaFileAsync(assetPath, token);
if (metaResult.IsFailure)
{
return Result<Guid>.Failure($"Failed to read metadata: {metaResult.Message}");
}
result = await importerInstance.ExportAsync(assetPath, assetData, metaResult.Value, token);
if (result.IsFailure)
{
return Result<Guid>.Failure(result.Message);
}
// Calculate file hash and update database
var fileHash = await CalculateFileHashAsync(assetPath, token);
await UpsertAssetAsync(assetPath, metaResult.Value, fileHash, null, token);
return metaResult.Value.Guid;
}
}

View File

@@ -0,0 +1,212 @@
using Ghost.Core;
using Ghost.Data.Services;
using System.Collections.Concurrent;
using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
// Asset cache - stores loaded assets by GUID
private static readonly ConcurrentDictionary<Guid, Asset> s_assetCache = new();
// LRU tracking - stores access time for each cached asset
private static readonly ConcurrentDictionary<Guid, DateTime> s_assetAccessTime = new();
// Maximum number of cached assets before eviction starts
private const int MAX_CACHED_ASSETS = 1000;
// Percentage of cache to evict when limit is reached (evict oldest 20%)
private const float _CACHE_EVICTION_PERCENTAGE = 0.2f;
private static Result<string> GetImportedAssetsDirectory()
{
if (AssetsDirectory == null)
{
return Result<string>.Failure("AssetsDirectory not initialized");
}
var cacheDir = Path.Combine(AssetsDirectory.Parent!.FullName, ProjectService.CACHE_FOLDER, "ImportedAssets");
if (!Directory.Exists(cacheDir))
{
Directory.CreateDirectory(cacheDir);
}
return cacheDir;
}
private static Result<string> GetImportedAssetPath(Guid guid)
{
var importedDirResult = GetImportedAssetsDirectory();
if (importedDirResult.IsFailure)
{
return Result<string>.Failure(importedDirResult.Message);
}
// Store imported assets as {GUID}.asset
var assetDataPath = Path.Combine(importedDirResult.Value, $"{guid}.asset");
return assetDataPath;
}
private static Result<T> LoadAssetInternal<T>(Guid guid) where T : Asset
{
// Check cache first
if (s_assetCache.TryGetValue(guid, out var cachedAsset))
{
// Update access time for LRU
s_assetAccessTime[guid] = DateTime.UtcNow;
if (cachedAsset is T typedAsset)
{
return typedAsset;
}
else
{
return Result<T>.Failure($"Cached asset is of type {cachedAsset.GetType().Name}, expected {typeof(T).Name}");
}
}
// Asset not in cache, load from disk
var assetPathResult = GetImportedAssetPath(guid);
if (assetPathResult.IsFailure)
{
return Result<T>.Failure(assetPathResult.Message);
}
var assetDataPath = assetPathResult.Value;
if (!File.Exists(assetDataPath))
{
return Result<T>.Failure($"Imported asset data not found at {assetDataPath}. Asset may not have been imported yet.");
}
try
{
// Read and deserialize asset data
var json = File.ReadAllText(assetDataPath);
var asset = JsonSerializer.Deserialize<T>(json);
if (asset == null)
{
return Result<T>.Failure("Failed to deserialize asset data");
}
// Add to cache
CacheAsset(guid, asset);
return asset;
}
catch (Exception ex)
{
return Result<T>.Failure($"Failed to load asset: {ex.Message}");
}
}
public static Result<T> LoadAssetAtPath<T>(string assetPath) where T : Asset
{
var guidResult = PathToGuid(assetPath);
if (guidResult.IsFailure)
{
return Result<T>.Failure(guidResult.Message);
}
return LoadAsset<T>(guidResult.Value);
}
private static void CacheAsset(Guid guid, Asset asset)
{
// Check if we need to evict old assets
if (s_assetCache.Count >= MAX_CACHED_ASSETS)
{
EvictOldestAssets();
}
s_assetCache[guid] = asset;
s_assetAccessTime[guid] = DateTime.UtcNow;
}
private static void EvictOldestAssets()
{
var evictionCount = (int)(MAX_CACHED_ASSETS * _CACHE_EVICTION_PERCENTAGE);
// Sort by access time and remove oldest entries
var oldestAssets = s_assetAccessTime
.OrderBy(kvp => kvp.Value)
.Take(evictionCount)
.Select(kvp => kvp.Key)
.ToList();
foreach (var guid in oldestAssets)
{
s_assetCache.TryRemove(guid, out _);
s_assetAccessTime.TryRemove(guid, out _);
}
}
/// <summary>
/// Unload a specific asset from cache.
/// </summary>
/// <param name="guid">GUID of the asset to unload.</param>
public static void UnloadAsset(Guid guid)
{
s_assetCache.TryRemove(guid, out _);
s_assetAccessTime.TryRemove(guid, out _);
}
/// <summary>
/// Unload all assets from cache.
/// </summary>
public static void UnloadAllAssets()
{
s_assetCache.Clear();
s_assetAccessTime.Clear();
}
/// <summary>
/// Check if an asset is currently loaded in cache.
/// </summary>
/// <param name="guid">GUID of the asset.</param>
/// <returns>True if the asset is in cache.</returns>
public static bool IsAssetLoaded(Guid guid)
{
return s_assetCache.ContainsKey(guid);
}
/// <summary>
/// Get cache statistics.
/// </summary>
/// <returns>Tuple of (current cache size, max cache size).</returns>
public static (int currentSize, int maxSize) GetCacheStats()
{
return (s_assetCache.Count, MAX_CACHED_ASSETS);
}
/// <summary>
/// Save an imported asset to disk for later loading.
/// This should be called by importers after processing the source file.
/// </summary>
/// <typeparam name="T">Type of asset data.</typeparam>
/// <param name="guid">GUID of the asset.</param>
/// <param name="assetData">Processed asset data to save.</param>
/// <returns>Result indicating success or failure.</returns>
public static Result SaveImportedAsset<T>(Guid guid, T assetData)
where T : Asset
{
var assetPathResult = GetImportedAssetPath(guid);
if (assetPathResult.IsFailure)
{
return Result.Failure(assetPathResult.Message);
}
try
{
var json = JsonSerializer.Serialize(assetData, s_defaultJsonOptions);
File.WriteAllText(assetPathResult.Value, json);
// Invalidate cache for this asset so it gets reloaded next time
UnloadAsset(guid);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to save imported asset: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,203 @@
using Ghost.Core;
using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
/// <summary>
/// Get the relative path from the assets directory.
/// </summary>
private static Result<string> GetRelativePath(string fullPath)
{
if (AssetsDirectory == null)
{
return Result<string>.Failure("AssetsDirectory not initialized");
}
if (!fullPath.StartsWith(AssetsDirectory.FullName, StringComparison.OrdinalIgnoreCase))
{
return Result<string>.Failure("Path is not within assets directory");
}
return Path.GetRelativePath(AssetsDirectory.FullName, fullPath);
}
/// <summary>
/// Get the full path from a relative path.
/// </summary>
private static Result<string> GetFullPath(string relativePath)
{
if (AssetsDirectory == null)
{
return Result<string>.Failure("AssetsDirectory not initialized");
}
return Path.Combine(AssetsDirectory.FullName, relativePath);
}
/// <summary>
/// Find GUID by asset path.
/// </summary>
/// <param name="assetPath">Full or relative path to the asset.</param>
/// <returns>The GUID of the asset if found.</returns>
public static Result<Guid> PathToGuid(string assetPath)
{
var relativePath = assetPath;
// Convert to relative path if it's a full path
if (Path.IsPathRooted(assetPath))
{
var relResult = GetRelativePath(assetPath);
if (relResult.IsFailure)
{
return Result<Guid>.Failure(relResult.Message);
}
relativePath = relResult.Value;
}
// Normalize path separators
relativePath = relativePath.Replace('\\', '/');
lock (s_dbLock)
{
if (s_pathAssetLookup.TryGetValue(relativePath, out var guid))
{
return guid;
}
}
return Result<Guid>.Failure("Asset not found in database");
}
/// <summary>
/// Find path by GUID.
/// </summary>
/// <param name="guid">GUID of the asset.</param>
/// <returns>The relative path to the asset if found.</returns>
public static Result<string> GuidToPath(Guid guid)
{
lock (s_dbLock)
{
if (s_assetPathLookup.TryGetValue(guid, out var path))
{
return path;
}
}
return Result<string>.Failure("Asset GUID not found in database");
}
/// <summary>
/// Load asset by GUID with caching.
/// </summary>
/// <typeparam name="T">Type of asset to load.</typeparam>
/// <param name="guid">GUID of the asset.</param>
/// <returns>The loaded asset.</returns>
public static Result<T> LoadAsset<T>(Guid guid) where T : Asset
{
// Implemented in AssetDatabase.Loader.cs
return LoadAssetInternal<T>(guid);
}
/// <summary>
/// Get asset tags by GUID.
/// </summary>
/// <param name="guid">GUID of the asset.</param>
/// <returns>List of tags associated with the asset.</returns>
public static async ValueTask<Result<List<string>>> GetAssetTagsAsync(Guid guid, CancellationToken token = default)
{
var pathResult = GuidToPath(guid);
if (pathResult.IsFailure)
{
return Result<List<string>>.Failure(pathResult.Message);
}
var fullPathResult = GetFullPath(pathResult.Value);
if (fullPathResult.IsFailure)
{
return Result<List<string>>.Failure(fullPathResult.Message);
}
var metaResult = await ReadMetaFileAsync(fullPathResult.Value, token);
if (metaResult.IsFailure)
{
return Result<List<string>>.Failure(metaResult.Message);
}
return metaResult.Value.Tags;
}
/// <summary>
/// Set asset tags by GUID.
/// </summary>
/// <param name="guid">GUID of the asset.</param>
/// <param name="tags">New tags for the asset.</param>
/// <returns>Result indicating success or failure.</returns>
public static async ValueTask<Result> SetAssetTagsAsync(Guid guid, List<string> tags, CancellationToken token = default)
{
var pathResult = GuidToPath(guid);
if (pathResult.IsFailure)
{
return Result.Failure(pathResult.Message);
}
var fullPathResult = GetFullPath(pathResult.Value);
if (fullPathResult.IsFailure)
{
return Result.Failure(fullPathResult.Message);
}
var metaResult = await ReadMetaFileAsync(fullPathResult.Value, token);
if (metaResult.IsFailure)
{
return Result.Failure(metaResult.Message);
}
metaResult.Value.Tags = tags;
// Write updated metadata to .gmeta file
var writeResult = await WriteMetaFileAsync(fullPathResult.Value + Utilities.FileExtensions.META_FILE_EXTENSION, metaResult.Value, token);
if (writeResult.IsFailure)
{
return writeResult;
}
// Update database with new tags
var fileHash = await CalculateFileHashAsync(fullPathResult.Value, token);
return await UpsertAssetAsync(fullPathResult.Value, metaResult.Value, fileHash, null, token);
}
/// <summary>
/// Search assets by name pattern.
/// Supports SQL LIKE wildcards: * (any characters) and ? (single character).
/// </summary>
/// <param name="namePattern">Search pattern (e.g., "*.txt", "player?", "test*").</param>
/// <returns>List of matching asset GUIDs.</returns>
public static async Task<List<Guid>> FindAssetsByNameAsync(string namePattern, CancellationToken token = default)
{
return await GetAssetsByNameAsync(namePattern, token);
}
/// <summary>
/// Find assets by tag.
/// </summary>
/// <param name="tag">Tag to search for.</param>
/// <returns>List of asset GUIDs with the specified tag.</returns>
public static async Task<List<Guid>> FindAssetsByTagAsync(string tag, CancellationToken token = default)
{
return await GetAssetsByTagAsync(tag, token);
}
/// <summary>
/// Get all assets in the database.
/// </summary>
/// <returns>Dictionary mapping GUIDs to relative paths.</returns>
public static IReadOnlyDictionary<Guid, string> GetAllAssets()
{
lock (s_dbLock)
{
return s_assetPathLookup.AsReadOnly();
}
}
}

View File

@@ -0,0 +1,251 @@
using Ghost.Core;
using Ghost.Editor.Core.Utilities;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
private static readonly Dictionary<string, Type> s_importerTypeLookup = new();
private static void InitializeMetaData()
{
if (s_watcher == null)
{
throw new InvalidOperationException("AssetDatabase is not initialized. Ensure that Initialize() is called before registering asset importers.");
}
var importerTypes = TypeCache.GetTypes().Where(t => t.GetCustomAttribute<AssetImporterAttribute>() != null);
foreach (var type in importerTypes)
{
var attribute = type.GetCustomAttribute<AssetImporterAttribute>()!;
foreach (var extension in attribute.SupportedExtensions)
{
s_importerTypeLookup[extension] = type;
}
}
s_watcher.Created += OnFSEvent;
s_watcher.Deleted += OnFSEvent;
s_watcher.Changed += OnFSEvent;
s_watcher.Renamed += OnAssetRenamed;
}
private static Result<string> GetMetaFilePath(string assetPath)
{
if (Directory.Exists(assetPath))
{
return Result<string>.Failure("Cannot create metadata for directories");
}
if (Path.GetExtension(assetPath).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase))
{
return Result<string>.Failure("Cannot create metadata for metadata files");
}
return assetPath + FileExtensions.META_FILE_EXTENSION;
}
private static ImporterSettings? GetDefaultSettingsForAsset(string assetPath)
{
var extension = Path.GetExtension(assetPath);
if (s_importerTypeLookup.TryGetValue(extension, out var importerType))
{
var settingsType = importerType.BaseType?.GetGenericArguments()[0];
if (settingsType == null || !typeof(ImporterSettings).IsAssignableFrom(settingsType))
{
return null;
}
return (ImporterSettings?)Activator.CreateInstance(settingsType);
}
return null;
}
/// <summary>
/// Calculate SHA256 hash of a file for change detection.
/// </summary>
private static async Task<string> CalculateFileHashAsync(string filePath, CancellationToken token = default)
{
try
{
await using var stream = File.OpenRead(filePath);
var hash = await SHA256.HashDataAsync(stream, token);
return Convert.ToHexString(hash);
}
catch
{
return string.Empty;
}
}
private static async Task<Result> WriteMetaFileAsync(string metaFilePath, AssetMeta metaData, CancellationToken token = default)
{
try
{
await using var fileStream = File.Create(metaFilePath);
await JsonSerializer.SerializeAsync(fileStream, metaData, s_defaultJsonOptions, token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure(ex.Message);
}
}
/// <summary>
/// Read metadata from a .gmeta file.
/// </summary>
private static async ValueTask<Result<AssetMeta>> ReadMetaFileAsync(string assetPath, CancellationToken token = default)
{
var metaFileResult = GetMetaFilePath(assetPath);
if (metaFileResult.IsFailure)
{
return Result<AssetMeta>.Failure(metaFileResult.Message);
}
if (!File.Exists(metaFileResult.Value))
{
return Result<AssetMeta>.Failure("Metadata file does not exist");
}
try
{
await using var fileStream = File.OpenRead(metaFileResult.Value);
var meta = await JsonSerializer.DeserializeAsync<AssetMeta>(fileStream, s_defaultJsonOptions, token);
if (meta == null)
{
return Result<AssetMeta>.Failure("Failed to deserialize metadata");
}
return meta;
}
catch (Exception ex)
{
return Result<AssetMeta>.Failure($"Failed to read metadata: {ex.Message}");
}
}
internal static async ValueTask<Result> GenerateMetaFileAsync(string assetPath, CancellationToken token = default)
{
Result r;
var metaFileResult = GetMetaFilePath(assetPath);
if (metaFileResult.IsFailure)
{
return Result.Failure(metaFileResult.Message);
}
if (File.Exists(metaFileResult.Value))
{
var existingMetaResult = await ReadMetaFileAsync(assetPath, token);
if (existingMetaResult.IsSuccess)
{
var existingMeta = existingMetaResult.Value;
if (s_assetPathLookup.TryGetValue(existingMeta.Guid, out var path))
{
var relResult = GetRelativePath(assetPath);
if (relResult.IsSuccess && assetPath != path)
{
// GUID conflict - regenerate
existingMeta.Guid = Guid.NewGuid();
r = await WriteMetaFileAsync(metaFileResult.Value, existingMeta, token);
if (r.IsFailure)
{
return r;
}
}
}
// Calculate file hash and update database
var fileHash = await CalculateFileHashAsync(assetPath, token);
await UpsertAssetAsync(assetPath, existingMeta, fileHash, null, token);
return Result.Success();
}
}
// Calculate initial file hash
var fileHash2 = await CalculateFileHashAsync(assetPath, token);
var defaultSettings = GetDefaultSettingsForAsset(assetPath);
var metaData = new AssetMeta
{
Guid = Guid.NewGuid()
};
if (defaultSettings != null)
{
metaData.SetImporterSettings(defaultSettings.GetType().Name, defaultSettings);
}
r = await WriteMetaFileAsync(metaFileResult.Value, metaData, token);
if (r.IsFailure)
{
return r;
}
// Add to database
await UpsertAssetAsync(assetPath, metaData, fileHash2, null, token);
return r;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsMetaFile(string path)
{
return Path.GetExtension(path).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase);
}
private static async void OnFSEvent(object sender, FileSystemEventArgs e)
{
if (IsMetaFile(e.FullPath))
{
return;
}
var type = e.ChangeType switch
{
WatcherChangeTypes.Created => AssetCommandType.FileCreated,
WatcherChangeTypes.Deleted => AssetCommandType.FileDeleted,
WatcherChangeTypes.Changed => AssetCommandType.FileModified,
_ => throw new InvalidOperationException("Unsupported file system event type")
};
await PostCommandAsync(new AssetCommand(type, e.FullPath, Timestamp: DateTime.UtcNow));
}
private static async void OnAssetRenamed(object sender, RenamedEventArgs e)
{
if (IsMetaFile(e.FullPath))
{
return;
}
await PostCommandAsync(new AssetCommand(AssetCommandType.FileRenamed, e.FullPath, e.OldFullPath, DateTime.UtcNow));
}
/// <summary>
/// Mark all assets that depend on the specified asset as dirty.
/// </summary>
private static async Task MarkDependentAssetsDirtyAsync(Guid assetGuid)
{
// TODO: We should have a reverse dependency lookup in the database to avoid scanning all assets.
// Query database for all assets and check their dependencies
var allAssets = GetAllAssets();
foreach (var kvp in allAssets)
{
var dependencies = await GetDependenciesAsync(kvp.Key, CancellationToken.None);
if (dependencies.Contains(assetGuid))
{
MarkDirty(kvp.Key);
}
}
}
}

View File

@@ -0,0 +1,51 @@
using Ghost.Core;
using Ghost.Editor.Core.Utilities;
using System.Diagnostics;
using System.Reflection;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
private static readonly Dictionary<string, Action<string>> s_assetOpenHandlers = new(StringComparer.OrdinalIgnoreCase);
private static void InitializeAssetHandle()
{
var methods = TypeCache.GetTypes()
.SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
.Where(m => m.GetCustomAttribute<AssetOpenHandlerAttribute>() != null &&
m.GetParameters().Length == 1 &&
m.GetParameters()[0].ParameterType == typeof(string));
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<AssetOpenHandlerAttribute>()!;
var del = (Action<string>)Delegate.CreateDelegate(typeof(Action<string>), method);
foreach (var ext in attr.Extensions)
{
if (s_assetOpenHandlers.ContainsKey(ext))
{
Logger.LogError($"Duplicate asset open handler for extension '{ext}' found in method '{method.Name}'. Existing handler will be overwritten.");
}
s_assetOpenHandlers[ext] = del;
}
}
}
public static void OpenAsset(string path)
{
var extension = Path.GetExtension(path);
if (s_assetOpenHandlers.TryGetValue(extension, out var handler))
{
handler(path);
}
else
{
Process.Start(new ProcessStartInfo(path)
{
UseShellExecute = true
});
}
}
}

View File

@@ -0,0 +1,390 @@
using Ghost.Core;
using Ghost.Data.Services;
using Microsoft.Data.Sqlite;
using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
private static SqliteConnection? s_dbConnection;
/// <summary>
/// Initialize the SQLite database for asset caching.
/// </summary>
private static async Task InitializeDatabaseAsync(CancellationToken token = default)
{
if (AssetsDirectory == null)
{
throw new InvalidOperationException("AssetsDirectory is not set. Initialize() must be called first.");
}
var dbPath = Path.Combine(AssetsDirectory.Parent!.FullName, ProjectService.CACHE_FOLDER, "AssetDatabase.db");
var cacheDir = Path.GetDirectoryName(dbPath);
if (!Directory.Exists(cacheDir))
{
Directory.CreateDirectory(cacheDir!);
}
var connectionString = new SqliteConnectionStringBuilder
{
DataSource = dbPath,
Mode = SqliteOpenMode.ReadWriteCreate,
Cache = SqliteCacheMode.Shared
}.ToString();
s_dbConnection = new SqliteConnection(connectionString);
await s_dbConnection.OpenAsync(token);
// Create tables
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS Assets (
Guid TEXT PRIMARY KEY,
Path TEXT NOT NULL,
Version INTEGER NOT NULL,
Tags TEXT,
FileHash TEXT,
DependencyGuids TEXT,
LastModified INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_path ON Assets(Path);
";
await cmd.ExecuteNonQueryAsync(token);
}
/// <summary>
/// Add or update an asset in the database.
/// </summary>
/// <param name="assetPath">Full path to the asset file.</param>
/// <param name="meta">Asset metadata from .gmeta file.</param>
/// <param name="fileHash">SHA256 hash of the asset file content.</param>
/// <param name="dependencies">List of GUIDs this asset depends on (extracted during import).</param>
private static async ValueTask<Result> UpsertAssetAsync(string assetPath, AssetMeta meta, string fileHash, List<Guid>? dependencies = null, CancellationToken token = default)
{
if (s_dbConnection == null)
{
return Result.Failure("Database not initialized");
}
var relativePath = GetRelativePath(assetPath);
if (relativePath.IsFailure)
{
return Result.Failure(relativePath.Message);
}
try
{
lock (s_dbLock)
{
// If this GUID already exists with a different path, remove the old path mapping
if (s_assetPathLookup.TryGetValue(meta.Guid, out var oldPath) && oldPath != relativePath.Value)
{
s_pathAssetLookup.Remove(oldPath);
}
// Update lookups with new path (normalize path separators for consistency)
var normalizedPath = relativePath.Value.Replace('\\', '/');
s_assetPathLookup[meta.Guid] = normalizedPath;
s_pathAssetLookup[normalizedPath] = meta.Guid;
}
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = @"
INSERT OR REPLACE INTO Assets (Guid, Path, Version, Tags, FileHash, DependencyGuids, LastModified)
VALUES (@guid, @path, @version, @tags, @fileHash, @deps, @modified)
";
cmd.Parameters.AddWithValue("@guid", meta.Guid.ToString());
cmd.Parameters.AddWithValue("@path", relativePath.Value);
cmd.Parameters.AddWithValue("@version", meta.Version);
cmd.Parameters.AddWithValue("@tags", JsonSerializer.Serialize(meta.Tags));
cmd.Parameters.AddWithValue("@fileHash", fileHash);
cmd.Parameters.AddWithValue("@deps", JsonSerializer.Serialize(dependencies ?? new List<Guid>()));
cmd.Parameters.AddWithValue("@modified", DateTimeOffset.UtcNow.ToUnixTimeSeconds());
await cmd.ExecuteNonQueryAsync(token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to upsert asset: {ex.Message}");
}
}
/// <summary>
/// Remove an asset from the database.
/// </summary>
private static async Task<Result> RemoveAssetFromDatabaseAsync(Guid guid, CancellationToken token = default)
{
if (s_dbConnection == null)
{
return Result.Failure("Database not initialized");
}
try
{
lock (s_dbLock)
{
if (s_assetPathLookup.TryGetValue(guid, out var path))
{
s_assetPathLookup.Remove(guid);
s_pathAssetLookup.Remove(path);
}
}
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = "DELETE FROM Assets WHERE Guid = @guid";
cmd.Parameters.AddWithValue("@guid", guid.ToString());
await cmd.ExecuteNonQueryAsync(token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to remove asset: {ex.Message}");
}
}
/// <summary>
/// Load all assets from the database into memory cache.
/// </summary>
private static async Task LoadAssetCacheFromDatabaseAsync(CancellationToken token = default)
{
if (s_dbConnection == null)
{
return;
}
try
{
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = "SELECT Guid, Path FROM Assets";
await using var reader = await cmd.ExecuteReaderAsync(token);
while (await reader.ReadAsync(token))
{
var guidStr = reader.GetString(0);
var path = reader.GetString(1);
if (Guid.TryParse(guidStr, out var guid))
{
lock (s_dbLock)
{
s_assetPathLookup[guid] = path;
s_pathAssetLookup[path] = guid;
}
}
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to load asset cache: {ex.Message}");
}
}
/// <summary>
/// Get assets by tag.
/// </summary>
private static async Task<List<Guid>> GetAssetsByTagAsync(string tag, CancellationToken token = default)
{
var result = new List<Guid>();
if (s_dbConnection == null)
{
return result;
}
try
{
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = "SELECT Guid, Tags FROM Assets";
await using var reader = await cmd.ExecuteReaderAsync(token);
while (await reader.ReadAsync(token))
{
var guidStr = reader.GetString(0);
var tagsJson = reader.GetString(1);
if (Guid.TryParse(guidStr, out var guid))
{
var tags = JsonSerializer.Deserialize<List<string>>(tagsJson);
if (tags != null && tags.Contains(tag, StringComparer.OrdinalIgnoreCase))
{
result.Add(guid);
}
}
}
}
catch
{
// Silently fail
}
return result;
}
/// <summary>
/// Get the file hash for an asset from the database.
/// </summary>
private static async Task<string?> GetFileHashAsync(Guid guid, CancellationToken token = default)
{
if (s_dbConnection == null)
{
return null;
}
try
{
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = "SELECT FileHash FROM Assets WHERE Guid = @guid";
cmd.Parameters.AddWithValue("@guid", guid.ToString());
var result = await cmd.ExecuteScalarAsync(token);
return result?.ToString();
}
catch
{
return null;
}
}
/// <summary>
/// Get the dependencies for an asset from the database.
/// </summary>
private static async Task<List<Guid>> GetDependenciesAsync(Guid guid, CancellationToken token = default)
{
if (s_dbConnection == null)
{
return new List<Guid>();
}
try
{
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = "SELECT DependencyGuids FROM Assets WHERE Guid = @guid";
cmd.Parameters.AddWithValue("@guid", guid.ToString());
var result = await cmd.ExecuteScalarAsync(token);
if (result != null)
{
var json = result.ToString();
return JsonSerializer.Deserialize<List<Guid>>(json ?? "[]") ?? new List<Guid>();
}
}
catch
{
// Silently fail
}
return new List<Guid>();
}
/// <summary>
/// Find assets by name pattern using database query with wildcards.
/// </summary>
/// <param name="namePattern">Pattern supporting * (any chars) and ? (single char).</param>
private static async Task<List<Guid>> GetAssetsByNameAsync(string namePattern, CancellationToken token = default)
{
var results = new List<Guid>();
if (s_dbConnection == null)
{
return results;
}
try
{
// Convert wildcard pattern to SQL LIKE pattern
var sqlPattern = namePattern.Replace('*', '%').Replace('?', '_');
await using var cmd = s_dbConnection.CreateCommand();
// Extract just the filename from the path for matching
// SQLite doesn't have a built-in path manipulation, so we search in the full path
// and filter by checking if the pattern matches the filename part
cmd.CommandText = @"
SELECT Guid, Path FROM Assets
WHERE Path LIKE '%' || @pattern || '%'
";
cmd.Parameters.AddWithValue("@pattern", sqlPattern);
await using var reader = await cmd.ExecuteReaderAsync(token);
while (await reader.ReadAsync(token))
{
var guidStr = reader.GetString(0);
var path = reader.GetString(1);
// Extract filename and check if it matches the pattern
var fileName = Path.GetFileName(path);
// Convert pattern to regex for proper matching
var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(namePattern)
.Replace("\\*", ".*")
.Replace("\\?", ".") + "$";
if (System.Text.RegularExpressions.Regex.IsMatch(fileName, regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
if (Guid.TryParse(guidStr, out var guid))
{
results.Add(guid);
}
}
}
}
catch
{
// Silently fail
}
return results;
}
/// <summary>
/// Remove orphaned entries from database (assets that no longer exist on disk).
/// </summary>
private static async Task RemoveOrphanedEntriesAsync(CancellationToken token = default)
{
if (s_dbConnection == null || AssetsDirectory == null)
{
return;
}
try
{
var orphanedGuids = new List<Guid>();
await using var cmd = s_dbConnection.CreateCommand();
cmd.CommandText = "SELECT Guid, Path FROM Assets";
await using var reader = await cmd.ExecuteReaderAsync(token);
while (await reader.ReadAsync(token))
{
var guidStr = reader.GetString(0);
var path = reader.GetString(1);
if (Guid.TryParse(guidStr, out var guid))
{
// Check if file exists
var fullPath = Path.Combine(AssetsDirectory.FullName, path);
if (!File.Exists(fullPath))
{
orphanedGuids.Add(guid);
}
}
}
// Remove orphaned entries
foreach (var guid in orphanedGuids)
{
await RemoveAssetFromDatabaseAsync(guid, token);
}
}
catch
{
// Silently fail - cleanup is best effort
}
}
}

View File

@@ -0,0 +1,531 @@
using Ghost.Core;
using Ghost.Data.Services;
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Channels;
namespace Ghost.Editor.Core.AssetHandle;
/// <summary>
/// Command types for asset database operations.
/// </summary>
internal enum AssetCommandType
{
FileCreated,
FileModified,
FileDeleted,
FileRenamed,
ManualRefresh
}
/// <summary>
/// Represents a command to process an asset operation.
/// </summary>
internal readonly record struct AssetCommand(
AssetCommandType Type,
string Path,
string? OldPath = null,
DateTime Timestamp = default
);
/// <summary>
/// Centralized asset database that manages all assets in the project.
/// Handles asset registration, lookup, importing, and dependency management.
/// Uses SQLite for persistent storage and efficient querying.
/// </summary>
public static partial class AssetDatabase
{
private static FileSystemWatcher? s_watcher;
private static readonly Lock s_dbLock = new();
private static readonly Dictionary<Guid, string> s_assetPathLookup = new();
private static readonly Dictionary<string, Guid> s_pathAssetLookup = new();
// In-memory dirty asset tracking (for runtime modifications only)
// TODO: We do not handle the reimporting of dirty assets yet
private static readonly HashSet<Guid> s_dirtyAssets = new();
// Command buffer pattern - Channel for file system event commands
private static Channel<AssetCommand>? s_commandChannel;
private static Timer? s_commandProcessorTimer;
private static readonly Lock s_commandLock = new();
private static readonly ConcurrentQueue<AssetCommand> s_waitingCommands = new(); // Commands waiting for manual refresh
private static bool s_autoRefreshEnabled = true;
// Initialization guard
private static readonly Lock s_initializationLock = new();
private static bool s_initialized = false;
private static readonly TimeSpan s_debounceDelay = TimeSpan.FromMilliseconds(100);
private static ManualResetEventSlim s_resetEventSlim = new(false);
private static readonly JsonSerializerOptions s_defaultJsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
}
};
public static DirectoryInfo? AssetsDirectory
{
get;
private set;
}
/// <summary>
/// Initialize the asset database.
/// Must be called after project is loaded.
/// </summary>
internal static async Task Initialize(CancellationToken token = default)
{
lock (s_initializationLock)
{
if (s_initialized)
{
return;
}
s_initialized = true;
}
if (ProjectService.CurrentProject.Metadata == null)
{
throw new InvalidOperationException("Project metadata is not initialized. Ensure that the project is loaded before accessing the AssetDatabase.");
}
AssetsDirectory = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(ProjectService.CurrentProject.Path)!, ProjectService.ASSETS_FOLDER));
s_commandChannel = Channel.CreateUnbounded<AssetCommand>(new UnboundedChannelOptions
{
SingleReader = false,
SingleWriter = false
});
// Initialize command processor timer (starts disabled, triggered by events)
s_commandProcessorTimer = new Timer(ProcessPendingCommands, null, Timeout.Infinite, Timeout.Infinite);
await InitializeDatabaseAsync(token);
await LoadAssetCacheFromDatabaseAsync(token);
s_watcher = new FileSystemWatcher
{
Path = AssetsDirectory.FullName,
IncludeSubdirectories = true,
EnableRaisingEvents = true,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite
};
InitializeAssetHandle();
InitializeMetaData();
// TODO: Timestamp fake instead of full scan.
await ValidateAndFixDatabaseAsync(token);
}
/// <summary>
/// Validate the asset database and fix any inconsistencies.
/// Checks for missing/corrupted assets and regenerates metadata as needed.
/// </summary>
private static async Task<Result> ValidateAndFixDatabaseAsync(CancellationToken token = default)
{
if (AssetsDirectory == null)
{
return Result.Failure("AssetsDirectory not initialized");
}
try
{
// Scan all files in assets directory
var allFiles = Directory.EnumerateFiles(AssetsDirectory.FullName, "*.*", SearchOption.AllDirectories)
.Where(f => !f.EndsWith(Utilities.FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase));
// Ensure all files have metadata
foreach (var file in allFiles)
{
var metaPath = file + Utilities.FileExtensions.META_FILE_EXTENSION;
if (!File.Exists(metaPath))
{
await GenerateMetaFileAsync(file, token);
}
else
{
// Validate and update database
var metaResult = await ReadMetaFileAsync(file, token);
if (metaResult.IsSuccess)
{
var fileHash = await CalculateFileHashAsync(file, token);
await UpsertAssetAsync(file, metaResult.Value, fileHash, null, token);
}
else
{
// Corrupted meta file - regenerate
await GenerateMetaFileAsync(file, token);
}
}
}
// Remove orphaned entries from database (files that no longer exist)
await RemoveOrphanedEntriesAsync(token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to validate database: {ex.Message}");
}
}
/// <summary>
/// Refresh the asset database manually.
/// Scans the project directory for changes and processes any queued file system events.
/// </summary>
public static async Task<Result> RefreshAsync(CancellationToken token = default)
{
// Flush waiting commands to channel
while (s_waitingCommands.TryDequeue(out var cmd))
{
s_commandChannel?.Writer.TryWrite(cmd);
}
s_resetEventSlim.Reset();
s_commandChannel?.Writer.TryWrite(new AssetCommand(AssetCommandType.ManualRefresh, string.Empty));
s_commandProcessorTimer?.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
await Task.Run(s_resetEventSlim.Wait, token);
return Result.Success();
}
/// <summary>
/// Mark an asset as dirty (modified in memory but not yet saved).
/// This state is NOT persisted and will be lost on application restart.
/// </summary>
public static void MarkDirty(Guid assetGuid)
{
lock (s_dbLock)
{
s_dirtyAssets.Add(assetGuid);
}
}
/// <summary>
/// Check if an asset is marked as dirty.
/// </summary>
public static bool IsDirty(Guid assetGuid)
{
lock (s_dbLock)
{
return s_dirtyAssets.Contains(assetGuid);
}
}
/// <summary>
/// Get all dirty assets.
/// </summary>
public static Guid[] GetDirtyAssets()
{
lock (s_dbLock)
{
return s_dirtyAssets.ToArray();
}
}
/// <summary>
/// Clear dirty flag for an asset (typically after saving).
/// </summary>
public static void ClearDirty(Guid assetGuid)
{
lock (s_dbLock)
{
s_dirtyAssets.Remove(assetGuid);
}
}
/// <summary>
/// Clear all dirty flags.
/// </summary>
public static void ClearAllDirty()
{
lock (s_dbLock)
{
s_dirtyAssets.Clear();
}
}
/// <summary>
/// Enable or disable automatic asset database refresh.
/// When disabled, file system events are queued and processed only when RefreshAsync() is called.
/// </summary>
public static void SetAutoRefresh(bool enabled)
{
s_autoRefreshEnabled = enabled;
}
internal static void FlushPendingCommands()
{
// Stop timer temporarily
s_commandProcessorTimer?.Change(Timeout.Infinite, Timeout.Infinite);
// Give a tiny bit of time for any in-flight file watcher events to post to channel
Thread.Sleep(50);
// Process all commands now
ProcessPendingCommands(null);
}
private static async ValueTask PostCommandAsync(AssetCommand command, CancellationToken token = default)
{
if (s_commandChannel == null)
{
return;
}
if (s_autoRefreshEnabled)
{
await s_commandChannel.Writer.WriteAsync(command, token);
s_commandProcessorTimer?.Change(s_debounceDelay, Timeout.InfiniteTimeSpan);
}
else
{
s_waitingCommands.Enqueue(command);
}
}
private static async void ProcessPendingCommands(object? state)
{
if (s_commandChannel == null)
{
return;
}
try
{
// // Collect all pending commands
// var commands = new List<AssetCommand>();
//
// while (s_commandChannel.Reader.TryRead(out var cmd))
// {
// commands.Add(cmd);
// }
// // Group commands by path (last command wins)
// var commandsByPath = new Dictionary<string, AssetCommand>();
// foreach (var cmd in commands)
// {
// commandsByPath[cmd.Path] = cmd;
// }
// NOTE: We handle the temp file filtering in each command handler now
// We should able to remove this allocation heavy code
// Filter out temp files (files that were created then deleted)
// lock (s_commandLock)
// {
// var pathsToProcess = commandsByPath.Keys.ToList();
// foreach (var path in pathsToProcess)
// {
// // If file was created/modified but doesn't exist anymore, skip
// if (!File.Exists(path) && commandsByPath[path].Type != AssetCommandType.FileDeleted)
// {
// commandsByPath.Remove(path);
// }
// }
//
// // Clear pending paths
// s_pendingCommandPaths.Clear();
// }
// Execute commands
// NOTE: We many don't need to collect all commands first, just process as we read.
// Channel in c# is thread-safe for multiple readers/writers.
//await foreach (var cmd in s_commandChannel.Reader.ReadAllAsync())
//{
// await ExecuteCommandAsync(cmd);
//}
while (s_commandChannel.Reader.TryRead(out var cmd))
{
await ExecuteCommandAsync(cmd);
}
await ImportDirtyAssetsAsync();
}
catch (Exception ex)
{
Logger.LogError($"Error processing commands: {ex.Message}");
}
finally
{
s_resetEventSlim.Set();
}
}
private static async ValueTask ExecuteCommandAsync(AssetCommand command)
{
switch (command.Type)
{
case AssetCommandType.FileCreated:
await HandleFileCreatedAsync(command.Path);
break;
case AssetCommandType.FileModified:
await HandleFileModifiedAsync(command.Path);
break;
case AssetCommandType.FileDeleted:
await HandleFileDeletedAsync(command.Path);
break;
case AssetCommandType.FileRenamed:
if (command.OldPath != null)
{
await HandleFileRenamedAsync(command.OldPath, command.Path);
}
break;
case AssetCommandType.ManualRefresh:
await ValidateAndFixDatabaseAsync(CancellationToken.None);
break;
}
}
private static async ValueTask HandleFileCreatedAsync(string path)
{
if (!File.Exists(path))
{
return;
}
await GenerateMetaFileAsync(path, CancellationToken.None);
}
private static async ValueTask HandleFileModifiedAsync(string path)
{
if (!File.Exists(path))
{
return;
}
// Check if file hash changed
var metaResult = await ReadMetaFileAsync(path, CancellationToken.None);
if (metaResult.IsFailure)
{
// No .gmeta file - treat this as a new file creation
await HandleFileCreatedAsync(path);
return;
}
var newHash = await CalculateFileHashAsync(path, CancellationToken.None);
var oldHash = await GetFileHashAsync(metaResult.Value.Guid, CancellationToken.None);
if (oldHash != newHash)
{
// File changed - update database and mark as dirty
await UpsertAssetAsync(path, metaResult.Value, newHash, null, CancellationToken.None);
MarkDirty(metaResult.Value.Guid);
}
}
private static async ValueTask HandleFileDeletedAsync(string path)
{
var metaFileResult = GetMetaFilePath(path);
if (metaFileResult.IsSuccess && File.Exists(metaFileResult.Value))
{
try
{
var metaResult = await ReadMetaFileAsync(path, CancellationToken.None);
if (metaResult.IsSuccess)
{
var meta = metaResult.Value;
// Remove from database
await RemoveAssetFromDatabaseAsync(meta.Guid, CancellationToken.None);
// Mark dependent assets as dirty
await MarkDependentAssetsDirtyAsync(meta.Guid);
}
File.Delete(metaFileResult.Value);
}
catch (Exception ex)
{
Logger.LogError($"Error deleting asset metadata: {ex.Message}");
}
}
}
private static async ValueTask HandleFileRenamedAsync(string oldPath, string newPath)
{
var oldMetaPath = oldPath + Utilities.FileExtensions.META_FILE_EXTENSION;
var newMetaPath = newPath + Utilities.FileExtensions.META_FILE_EXTENSION;
if (File.Exists(newMetaPath))
{
// Validate and update
await GenerateMetaFileAsync(newPath, CancellationToken.None);
}
else if (File.Exists(oldMetaPath))
{
// Move meta file
File.Move(oldMetaPath, newMetaPath);
// Update database with new path and recalculated hash
var metaResult = await ReadMetaFileAsync(newPath, CancellationToken.None);
if (metaResult.IsSuccess)
{
var fileHash = await CalculateFileHashAsync(newPath, CancellationToken.None);
await UpsertAssetAsync(newPath, metaResult.Value, fileHash, null, CancellationToken.None);
}
}
else
{
// Generate new meta file
await GenerateMetaFileAsync(newPath, CancellationToken.None);
}
// Delete old meta if it still exists
if (File.Exists(oldMetaPath) && oldMetaPath != newMetaPath)
{
try
{
File.Delete(oldMetaPath);
}
catch
{
}
}
}
internal static void Shutdown()
{
lock (s_initializationLock)
{
if (!s_initialized)
{
return;
}
s_watcher?.Dispose();
s_watcher = null;
s_commandProcessorTimer?.Dispose();
s_commandProcessorTimer = null;
s_dbConnection?.Close();
s_dbConnection?.Dispose();
s_dbConnection = null;
s_assetPathLookup.Clear();
s_pathAssetLookup.Clear();
s_dirtyAssets.Clear();
s_waitingCommands.Clear();
s_importerInstances.Clear();
s_importerTypeLookup.Clear();
s_initialized = false;
}
}
}

View File

@@ -0,0 +1,115 @@
# Asset Database Architecture
This document details the architectural design and data flow of the `AssetHandle` module in Ghost Editor.
## System Overview
The Asset Database acts as the bridge between the raw file system (Source Assets) and the runtime engine (Imported Assets). It maintains a consistent state using a dual-storage approach:
1. **File System**: The source of truth. Contains source files (e.g., `.png`, `.fbx`) and metadata files (`.gmeta`).
2. **SQLite Database**: An acceleration layer (cache) for fast lookups, dependency tracking, and searching.
## Data Flow
### 1. Asset Discovery & Registration
When the editor starts or a file changes:
1. **FileSystemWatcher** detects the change (Create/Delete/Modify/Rename).
2. **Event Handler** queues an `AssetCommand` (debounce mechanism prevents event storms).
3. **Command Processor** executes the command:
* **New File**: Generates a `.gmeta` file with a new GUID and default settings. Adds to SQLite.
* **Modified File**: Checks hash. If changed, marks asset as "Dirty" and updates SQLite.
* **Deleted File**: Removes from SQLite and marks dependents as "Dirty".
### 2. Import Pipeline
The import process converts source formats into engine-ready data.
**Flow:**
1. `AssetDatabase.ImportDirtyAssetsAsync()` or direct `ImportAssetAsync` is called.
2. System looks up the registered `AssetImporter` for the file extension.
3. `AssetImporter.ImportAsync` is invoked with the source path and metadata.
4. Importer reads source file and settings from metadata.
5. Importer processes data (e.g., compiles shaders, compresses textures).
6. Importer calls `AssetDatabase.SaveImportedAsset(guid, data)`.
7. Data is serialized to JSON (or binary) in the `Cache/ImportedAssets` directory as `{GUID}.asset`.
### 3. Loading Pipeline
When the engine requests an asset:
**Flow:**
1. `AssetDatabase.LoadAsset<T>(guid)` is called.
2. **Memory Cache Check**:
* Checks `s_assetCache` (ConcurrentDictionary).
* If found: Updates LRU timestamp and returns object.
* If not found: Proceeds to disk load.
3. **Disk Load**:
* Locates `{GUID}.asset` in `Cache/ImportedAssets`.
* Deserializes the data into the target runtime type (e.g., `TextureAsset`).
4. **Cache Update**:
* Adds new object to `s_assetCache`.
* If cache size > `MAX_CACHED_ASSETS` (1000), evicts oldest 20% based on access time.
## Key Components Diagram
```mermaid
graph TD
User[Editor / User] -->|File Ops| API[AssetDatabase API]
FS[File System] -->|Events| Watcher[FileSystemWatcher]
subgraph AssetDatabase
API --> DB[SQLite Database]
API --> Meta[Meta Handler]
API --> Loader[Asset Loader]
API --> Importer[Import System]
Watcher -->|Queue| Cmd[Command Processor]
Cmd --> Meta
Cmd --> DB
Importer -->|Read| FS
Importer -->|Write| Cache[Imported Assets Cache]
Loader -->|Read| Cache
Loader -->|Check| MemCache[Memory LRU Cache]
end
Meta -->|Read/Write| FS
DB -->|Index| FS
```
## Database Schema (SQLite)
The `AssetDatabase.db` contains a single `Assets` table:
| Column | Type | Description |
|--------|------|-------------|
| **Guid** | TEXT (PK) | The unique identifier of the asset. |
| **Path** | TEXT | Relative path from `Assets/` folder. Indexed for fast lookup. |
| **Version** | INTEGER | Importer version for migration support. |
| **Tags** | TEXT | JSON array of string tags. |
| **FileHash** | TEXT | SHA256 hash of the source file content. |
| **DependencyGuids** | TEXT | JSON array of GUIDs this asset depends on. |
| **LastModified** | INTEGER | Unix timestamp of last modification. |
## Detailed Subsystems
### Metadata System (`.gmeta`)
* **Format**: JSON.
* **Content**: GUID, Version, Tags, ImporterSettings (per importer type).
* **Strategy**: The `.gmeta` file is the *only* place the persistent GUID lives. If the database is corrupted, it can be rebuilt entirely by scanning the file system and reading `.gmeta` files.
### Threading & Safety
* **Locks**:
* `s_dbLock`: Protects in-memory dictionaries (`s_assetPathLookup`) and dirty tracking.
* `s_commandLock`: Protects the command queue for file events.
* **Async**: Heavy I/O operations (DB access, File I/O) are async.
* **Channels**: Uses `System.Threading.Channels` to decouple high-frequency file system events from database processing.
### Importer Registry
* Uses `TypeCache` and reflection to find classes with `[AssetImporter]`.
* Mappings are stored in `s_importerTypeLookup` (Extension -> Type).
* Importers are stateless (instantiated on demand or cached as singletons depending on implementation, currently cached in `s_importerInstances`).
## Future Improvements / Known Limitations
1. **Binary Formats**: Currently, imported assets are stored as JSON. For large assets (textures, models), a binary format is required for performance.
2. **Dependency Graph**: While dependencies are stored, a full graph traversal for complex invalidation (e.g., if A changes, re-import B which depends on A) is partial.
3. **Cross-Process Locking**: SQLite is file-based; concurrent access from multiple editor instances needs careful file locking mode configuration.

View File

@@ -0,0 +1,131 @@
# Asset Database Documentation
The Asset Database is a core component of the Ghost Editor responsible for managing the lifecycle, storage, import, and retrieval of project assets. It provides a unified API for interacting with assets, ensuring that metadata (GUIDs, tags, settings) stays synchronized with files on disk.
## Key Features
- **GUID-based Asset Identification**: Every asset is uniquely identified by a stable GUID, stored in a sidecar `.gmeta` file.
- **Automatic Importing**: Monitors the file system for changes and automatically imports assets using registered importers.
- **Dependency Tracking**: Tracks dependencies between assets to ensure validity and trigger re-imports when dependencies change.
- **Caching**: Implements an LRU (Least Recently Used) cache for loaded assets to optimize performance.
- **SQLite Backed**: Uses a local SQLite database for fast lookups (Path <-> GUID) and metadata queries.
- **Metadata Management**: Handles `.gmeta` files automatically, including generation, validation, and cleanup.
## usage
### Initialization
The Asset Database must be initialized after the project is loaded.
```csharp
await AssetDatabase.Initialize(cancellationToken);
```
### Loading Assets
Assets can be loaded by GUID or by Path.
```csharp
// Load by Path
var result = AssetDatabase.LoadAssetAtPath<TextureAsset>("Assets/Textures/my_texture.png");
if (result.IsSuccess)
{
var texture = result.Value;
}
// Load by GUID
var guid = ...;
var result = AssetDatabase.LoadAsset<TextureAsset>(guid);
```
### File Operations
Always use the `AssetDatabase` API for file operations to ensure metadata is preserved.
```csharp
// Create
await AssetDatabase.CreateAssetAsync("Assets/Data/config.json", dataBytes);
// Move
await AssetDatabase.MoveAssetAsync("Assets/Old/file.txt", "Assets/New/file.txt");
// Copy
await AssetDatabase.CopyAssetAsync("Assets/template.txt", "Assets/instance.txt");
// Delete
await AssetDatabase.DeleteAssetAsync("Assets/garbage.tmp");
```
### Searching
Find assets using wildcards or tags.
```csharp
// Find all PNGs
var guids = await AssetDatabase.FindAssetsByNameAsync("*.png");
// Find assets with a specific tag
var enemyAssets = await AssetDatabase.FindAssetsByTagAsync("Enemy");
```
### Tags
Manage asset tags for organization.
```csharp
// Get tags
var tagsResult = await AssetDatabase.GetAssetTagsAsync(guid);
// Set tags
await AssetDatabase.SetAssetTagsAsync(guid, new List<string> { "Level1", "Prop" });
```
### Opening Assets
Open an asset using its registered handler or the system default.
```csharp
AssetDatabase.OpenAsset("Assets/Docs/readme.txt");
```
## Extending the Asset Database
### Creating a New Importer
To support a new file type, create a class that inherits from `AssetImporter<T>` and decorate it with the `[AssetImporter]` attribute.
```csharp
[AssetImporter(".myfmt")]
internal class MyFormatImporter : AssetImporter<MyFormatSettings>
{
public override async Task<Result> ImportAsync(string assetPath, AssetMeta meta)
{
var settings = GetSettings(meta);
// 1. Read source file
// 2. Process data
// 3. Save imported data using AssetDatabase.SaveImportedAsset
var myAsset = new MyAsset(meta.Guid) { ... };
return AssetDatabase.SaveImportedAsset(meta.Guid, myAsset);
}
}
internal class MyFormatSettings : ImporterSettings
{
public float Scale { get; set; } = 1.0f;
}
```
### Creating an Open Handler
To define custom behavior when an asset is opened (e.g., double-clicked in the editor), use the `[AssetOpenHandler]` attribute.
```csharp
internal static class MyHandlers
{
[AssetOpenHandler(".myfmt")]
private static void OpenMyFormat(string path)
{
// Open custom editor window
}
}
```
## Internal Architecture
- **AssetDatabase.cs**: Core initialization and event coordination.
- **AssetDatabase.SQLite.cs**: Database table management and queries.
- **AssetDatabase.Meta.cs**: `.gmeta` file handling and file system watcher events.
- **AssetDatabase.Importer.cs**: Importer discovery and execution.
- **AssetDatabase.Loader.cs**: Asset loading and caching logic.

View File

@@ -0,0 +1,81 @@
using Ghost.Core;
namespace Ghost.Editor.Core.AssetHandle;
public abstract class AssetImporter
{
/// <summary>
/// Import the asset at the specified path with the given settings.
/// </summary>
/// <param name="assetPath">Full path to the source asset file.</param>
/// <param name="meta">Metadata for the asset.</param>
/// <param name="token">Cancellation token.</param>
/// <returns>Result indicating success or failure.</returns>
public abstract ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default);
/// <summary>
/// Export in-memory asset data to disk.
/// Override this method to support creating assets from code.
/// </summary>
/// <typeparam name="T">Type of asset data to export.</typeparam>
/// <param name="assetPath">Full path where the asset should be saved.</param>
/// <param name="assetData">In-memory asset data to serialize.</param>
/// <param name="meta">Metadata for the asset.</param>
/// <param name="token">Cancellation token.</param>
/// <returns>Result indicating success or failure.</returns>
public virtual ValueTask<Result> ExportAsync<T>(string assetPath, T assetData, AssetMeta meta, CancellationToken token = default)
where T : class
{
return ValueTask.FromResult(Result.Failure("This importer does not support exporting assets."));
}
/// <summary>
/// Validate dependencies referenced by this asset.
/// Dependencies are extracted from asset content during import and stored in the database.
/// </summary>
/// <param name="dependencies">List of dependency GUIDs extracted from the asset.</param>
/// <returns>Result indicating if all dependencies are valid.</returns>
protected virtual ValueTask<Result> ValidateDependenciesAsync(List<Guid> dependencies, CancellationToken token = default)
{
foreach (var dependencyGuid in dependencies)
{
var path = AssetDatabase.GuidToPath(dependencyGuid);
if (path.IsFailure)
{
return ValueTask.FromResult(Result.Failure($"Missing dependency: {dependencyGuid}"));
}
if (!File.Exists(path.Value))
{
return ValueTask.FromResult(Result.Failure($"Dependency file does not exist: {path.Value}"));
}
}
return ValueTask.FromResult(Result.Success());
}
}
public abstract class AssetImporter<TSettings> : AssetImporter
where TSettings : ImporterSettings, new()
{
/// <summary>
/// Get the settings for this importer from the metadata.
/// Creates default settings if none exist.
/// </summary>
/// <param name="meta">Asset metadata.</param>
/// <returns>The importer settings.</returns>
protected TSettings GetSettings(AssetMeta meta)
{
var typeName = GetType().Name;
var settings = meta.GetImporterSettings<TSettings>(typeName);
if (settings != null)
{
return settings;
}
var defaultSettings = new TSettings();
meta.SetImporterSettings(typeName, defaultSettings);
return defaultSettings;
}
}

View File

@@ -0,0 +1,15 @@
namespace Ghost.Editor.Core.AssetHandle;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
internal class AssetImporterAttribute : Attribute
{
public string[] SupportedExtensions
{
get;
}
public AssetImporterAttribute(params string[] supportedExtensions)
{
SupportedExtensions = supportedExtensions;
}
}

View File

@@ -0,0 +1,85 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Ghost.Editor.Core.AssetHandle;
/// <summary>
/// Metadata for an asset, stored in .gmeta files.
/// Contains GUID, version, tags, and importer settings.
/// FileHash and Dependencies are stored in the database only, not in .gmeta files.
/// </summary>
public class AssetMeta
{
/// <summary>
/// Unique identifier for the asset.
/// </summary>
[JsonPropertyName("Guid")]
public Guid Guid
{
get;
set;
}
/// <summary>
/// Version of the asset pipeline (not the asset itself).
/// Used for migration when the asset pipeline is redesigned.
/// </summary>
[JsonPropertyName("Version")]
public int Version
{
get;
set;
} = 1;
/// <summary>
/// Tags for categorizing and searching assets.
/// </summary>
[JsonPropertyName("Tags")]
public List<string> Tags
{
get;
set;
} = new();
/// <summary>
/// Importer settings specific to this asset.
/// The key is the importer type name, and the value is a JSON element containing the settings.
/// Use GetImporterSettings&lt;T&gt;() and SetImporterSettings&lt;T&gt;() to work with strongly-typed settings.
/// </summary>
[JsonPropertyName("ImporterSettings")]
public Dictionary<string, JsonElement> ImporterSettings
{
get;
set;
} = new();
/// <summary>
/// Get importer settings of a specific type.
/// </summary>
public T? GetImporterSettings<T>(string importerName) where T : ImporterSettings
{
if (ImporterSettings.TryGetValue(importerName, out var element))
{
return element.Deserialize<T>();
}
return null;
}
/// <summary>
/// Set importer settings.
/// </summary>
public void SetImporterSettings<T>(string importerName, T settings) where T : ImporterSettings
{
var element = JsonSerializer.SerializeToElement(settings);
ImporterSettings[importerName] = element;
}
/// <summary>
/// Set importer settings (non-generic overload).
/// </summary>
internal void SetImporterSettings(string importerName, ImporterSettings settings)
{
var element = JsonSerializer.SerializeToElement(settings, settings.GetType());
ImporterSettings[importerName] = element;
}
}

View File

@@ -0,0 +1,15 @@
namespace Ghost.Editor.Core.AssetHandle;
[AttributeUsage(AttributeTargets.Method)]
public class AssetOpenHandlerAttribute : Attribute
{
public string[] Extensions
{
get;
}
public AssetOpenHandlerAttribute(params string[] extensions)
{
Extensions = extensions.Select(e => e.StartsWith('.') ? e.ToLowerInvariant() : '.' + e.ToLowerInvariant()).ToArray();
}
}

View File

@@ -0,0 +1,5 @@
namespace Ghost.Editor.Core.AssetHandle;
public abstract class ImporterSettings
{
}

View File

@@ -0,0 +1,70 @@
using Ghost.Core;
namespace Ghost.Editor.Core.AssetHandle.Importers;
/// <summary>
/// Example importer settings for text assets.
/// </summary>
internal class TextImporterSettings : ImporterSettings
{
public string Encoding
{
get;
set;
} = "UTF-8";
public bool TrimWhitespace
{
get;
set;
} = false;
}
/// <summary>
/// Example importer for text files (.txt, .md).
/// This is a simple test importer to demonstrate the asset import system.
/// </summary>
[AssetImporter(".txt", ".md")]
internal class TextImporter : AssetImporter<TextImporterSettings>
{
public override async ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default)
{
var settings = GetSettings(meta);
// Text files typically don't have dependencies
// If they did, you would extract them from the content here
var dependencies = new List<Guid>();
// Validate dependencies
var depResult = await ValidateDependenciesAsync(dependencies);
if (depResult.IsFailure)
{
return depResult;
}
try
{
// Read the file
var content = await File.ReadAllTextAsync(assetPath, token);
if (settings.TrimWhitespace)
{
content = content.Trim();
}
// TODO: Process the text content
// For example:
// - Convert to a specific format
// - Extract metadata
// - Generate assets
// - Save to output folder
// For now, just report success
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to import text asset: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,279 @@
using Ghost.Core;
using System.Text.Json;
namespace Ghost.Editor.Core.AssetHandle.Importers;
/// <summary>
/// Importer settings for texture assets.
/// </summary>
internal class TextureImporterSettings : ImporterSettings
{
/// <summary>
/// Whether to generate mipmaps for the texture.
/// </summary>
public bool GenerateMipmaps
{
get;
set;
} = true;
/// <summary>
/// Whether the texture uses sRGB color space.
/// </summary>
public bool SRGB
{
get;
set;
} = true;
/// <summary>
/// Maximum texture size. Images larger than this will be downscaled.
/// </summary>
public uint MaxSize
{
get;
set;
} = 2048;
/// <summary>
/// Texture compression format.
/// Options: "None", "BC1", "BC3", "BC7"
/// </summary>
public string CompressionFormat
{
get;
set;
} = "None";
/// <summary>
/// Texture filter mode.
/// Options: "Point", "Bilinear", "Trilinear"
/// </summary>
public string FilterMode
{
get;
set;
} = "Bilinear";
/// <summary>
/// Texture wrap mode.
/// Options: "Repeat", "Clamp", "Mirror"
/// </summary>
public string WrapMode
{
get;
set;
} = "Repeat";
}
/// <summary>
/// Importer for texture files (.png, .jpg, .jpeg, .dds, .tga, .bmp).
/// Processes image files and converts them into engine-ready texture assets.
/// </summary>
[AssetImporter(".png", ".jpg", ".jpeg", ".dds", ".tga", ".bmp")]
internal class TextureImporter : AssetImporter<TextureImporterSettings>
{
public override async ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default)
{
var settings = GetSettings(meta);
// Textures typically don't reference other assets as dependencies
// If they did (e.g., normal maps referencing base textures), extract here
var dependencies = new List<Guid>();
// Validate dependencies
var depResult = await ValidateDependenciesAsync(dependencies, token);
if (depResult.IsFailure)
{
return depResult;
}
try
{
// Check if file exists
if (!File.Exists(assetPath))
{
return Result.Failure($"Source texture file not found: {assetPath}");
}
// Get image dimensions (simplified - in real implementation would use image library)
var (width, height) = GetImageDimensions(assetPath);
if (width == 0 || height == 0)
{
return Result.Failure("Failed to read image dimensions");
}
// Apply max size constraint
if (width > settings.MaxSize || height > settings.MaxSize)
{
var scale = Math.Min(settings.MaxSize / (float)width, settings.MaxSize / (float)height);
width = (uint)(width * scale);
height = (uint)(height * scale);
}
// Calculate mipmap count
uint mipLevels = 1;
if (settings.GenerateMipmaps)
{
mipLevels = CalculateMipLevels(width, height);
}
// Determine format
var format = settings.CompressionFormat == "None" ? "RGBA8" : settings.CompressionFormat;
// Create texture asset
var textureAsset = new TextureAsset(meta.Guid, Path.GetFileNameWithoutExtension(assetPath))
{
Width = width,
Height = height,
MipLevels = mipLevels,
Format = format,
IsSRGB = settings.SRGB,
SourcePath = assetPath
};
// Save the imported asset data
var saveResult = AssetDatabase.SaveImportedAsset(meta.Guid, textureAsset);
if (saveResult.IsFailure)
{
return Result.Failure($"Failed to save texture asset: {saveResult.Message}");
}
// In a real implementation, you would:
// 1. Load the image using a library like ImageSharp or StbImageSharp
// 2. Resize if needed
// 3. Generate mipmaps
// 4. Compress if needed
// 5. Save the processed texture data to the ImportedAssets folder
// 6. Update the hash in database
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to import texture: {ex.Message}");
}
}
/// <summary>
/// Get image dimensions from file.
/// Simplified implementation - in production, use an image library.
/// </summary>
private static (uint width, uint height) GetImageDimensions(string imagePath)
{
// This is a placeholder implementation
// In a real implementation, you would use a library like:
// - ImageSharp
// - StbImageSharp
// - DirectXTex (for DDS files)
var extension = Path.GetExtension(imagePath).ToLowerInvariant();
if (extension == ".dds")
{
// For DDS files, read the header
// DDS header format: https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dds-header
return ReadDDSHeader(imagePath);
}
else
{
// For PNG/JPG/etc, we would use an image library
// For now, return placeholder values
return (1024, 1024);
}
}
/// <summary>
/// Read DDS file header to get dimensions.
/// </summary>
private static (uint width, uint height) ReadDDSHeader(string ddsPath)
{
try
{
using var stream = File.OpenRead(ddsPath);
using var reader = new BinaryReader(stream);
// Read magic number (should be "DDS ")
var magic = reader.ReadUInt32();
if (magic != 0x20534444) // "DDS " in little-endian
{
return (0, 0);
}
// Read header size (should be 124)
var headerSize = reader.ReadUInt32();
if (headerSize != 124)
{
return (0, 0);
}
// Skip flags
reader.ReadUInt32();
// Read height and width
var height = reader.ReadUInt32();
var width = reader.ReadUInt32();
return (width, height);
}
catch
{
return (0, 0);
}
}
/// <summary>
/// Export a texture asset from memory to disk.
/// </summary>
public override async ValueTask<Result> ExportAsync<T>(string assetPath, T assetData, AssetMeta meta, CancellationToken token = default)
{
if (assetData is not TextureAsset textureAsset)
{
return Result.Failure($"Asset data is not a TextureAsset, got {typeof(T).Name}");
}
try
{
// In a real implementation, you would:
// 1. Convert the texture data to the appropriate format
// 2. Write the image file (PNG, DDS, etc.)
// 3. Save metadata
// For now, just save metadata as JSON
var json = JsonSerializer.Serialize(textureAsset, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(assetPath, json, token);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to export texture: {ex.Message}");
}
}
/// <summary>
/// Calculate number of mipmap levels for a given texture size.
/// </summary>
private static uint CalculateMipLevels(uint width, uint height)
{
if (width == 0 || height == 0)
{
return 0;
}
uint count = 1;
while (width > 1 || height > 1)
{
width >>= 1;
height >>= 1;
count++;
}
return count;
}
}

View File

@@ -0,0 +1,22 @@
namespace Ghost.Editor.Core.AssetHandle;
/// <summary>
/// The base class for all asset types in the Ghost Editor.
/// </summary>
public abstract class Asset
{
public abstract string Name
{
get; set;
}
public Guid ID
{
get;
}
protected Asset(Guid id)
{
ID = id;
}
}

View File

@@ -0,0 +1,75 @@
namespace Ghost.Editor.Core.AssetHandle;
/// <summary>
/// Represents a texture asset.
/// </summary>
public class TextureAsset : Asset
{
public override string Name
{
get;
set;
}
/// <summary>
/// Width of the texture in pixels.
/// </summary>
public uint Width
{
get;
set;
}
/// <summary>
/// Height of the texture in pixels.
/// </summary>
public uint Height
{
get;
set;
}
/// <summary>
/// Number of mipmap levels.
/// </summary>
public uint MipLevels
{
get;
set;
}
/// <summary>
/// Texture format (e.g., "RGBA8", "BC1", "BC7").
/// </summary>
public string Format
{
get;
set;
}
/// <summary>
/// Whether the texture uses sRGB color space.
/// </summary>
public bool IsSRGB
{
get;
set;
}
/// <summary>
/// Relative path to the source image file.
/// </summary>
public string SourcePath
{
get;
set;
}
public TextureAsset(Guid id, string name) : base(id)
{
Name = name;
Format = "RGBA8";
IsSRGB = true;
SourcePath = string.Empty;
}
}

View File

@@ -0,0 +1,7 @@
namespace Ghost.Editor.Core.Contracts;
public interface INavigationAware
{
public void OnNavigatedTo(object? parameter);
public void OnNavigatedFrom();
}

View File

@@ -0,0 +1,69 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Misaki.HighPerformance.Mathematics;
namespace Ghost.Editor.Core.Controls;
[TemplatePart(Name = "XComponent", Type = typeof(NumberBox))]
[TemplatePart(Name = "YComponent", Type = typeof(NumberBox))]
[TemplatePart(Name = "ZComponent", Type = typeof(NumberBox))]
public sealed partial class Float3Field : ValueControl<float3>
{
private NumberBox? _xComponent;
private NumberBox? _yComponent;
private NumberBox? _zComponent;
public Float3Field()
{
DefaultStyleKey = typeof(Float3Field);
}
protected override void ValueChanged(float3 oldValue, float3 newValue)
{
SyncFromValue();
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_xComponent?.ValueChanged -= OnComponentChanged;
_yComponent?.ValueChanged -= OnComponentChanged;
_zComponent?.ValueChanged -= OnComponentChanged;
_xComponent = GetTemplateChild("XComponent") as NumberBox;
_yComponent = GetTemplateChild("YComponent") as NumberBox;
_zComponent = GetTemplateChild("ZComponent") as NumberBox;
SyncFromValue();
_xComponent?.ValueChanged += OnComponentChanged;
_yComponent?.ValueChanged += OnComponentChanged;
_zComponent?.ValueChanged += OnComponentChanged;
}
private void SyncFromValue()
{
SuppressChangedEvent = true;
_xComponent?.Value = Value.x;
_yComponent?.Value = Value.y;
_zComponent?.Value = Value.z;
SuppressChangedEvent = false;
}
private void OnComponentChanged(NumberBox sender, NumberBoxValueChangedEventArgs args)
{
if (SuppressChangedEvent)
{
return;
}
var newValue = new float3(
(float)(_xComponent?.Value ?? 0),
(float)(_yComponent?.Value ?? 0),
(float)(_zComponent?.Value ?? 0));
RiseChangedEvent(Value, newValue);
Value = newValue;
}
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Core.Controls">
<Style TargetType="local:Float3Field">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:Float3Field">
<Grid ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="X" />
<NumberBox x:Name="XComponent" Grid.Column="1" />
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
Text="Y" />
<NumberBox x:Name="YComponent" Grid.Column="3" />
<TextBlock
Grid.Column="4"
VerticalAlignment="Center"
Text="Z" />
<NumberBox x:Name="ZComponent" Grid.Column="5" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,143 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using System.Reflection;
using Windows.Globalization.NumberFormatting;
namespace Ghost.Editor.Core.Controls;
public sealed partial class PropertyField : ContentControl
{
private static readonly Dictionary<Type, DependencyProperty> _valueProperties = new()
{
{ typeof(TextBox), TextBox.TextProperty },
{ typeof(NumberBox), NumberBox.ValueProperty },
{ typeof(ToggleButton), ToggleButton.IsCheckedProperty },
{ typeof(ToggleSwitch), ToggleSwitch.IsOnProperty },
{ typeof(ComboBox), Selector.SelectedValueProperty },
{ typeof(RangeBase), RangeBase.ValueProperty },
};
private object? _sourceObject;
internal FieldInfo? _propertyInfo;
internal Type? _fieldType;
private object? _lastValue;
public event Action<PropertyField>? OnValueChanged;
public string Label
{
get => (string)GetValue(LabelProperty);
set => SetValue(LabelProperty, value);
}
public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(
nameof(Label),
typeof(string),
typeof(PropertyField),
new PropertyMetadata(default(string)));
public PropertyField()
{
DefaultStyleKey = typeof(PropertyField);
}
private static DependencyProperty? GetValueProperty(Type? fieldType)
{
while (fieldType != null)
{
if (_valueProperties.TryGetValue(fieldType, out var dp))
{
return dp;
}
fieldType = fieldType.BaseType;
}
return null;
}
private static TField ConfigureField<TField>(PropertyField propertyField, FieldInfo fieldInfo, object sourceObject, Func<TField> factory)
where TField : FrameworkElement
{
propertyField._sourceObject = sourceObject;
propertyField._propertyInfo = fieldInfo;
propertyField._fieldType = typeof(TField);
var field = factory();
var dp = GetValueProperty(typeof(TField));
field.SetBinding(dp, new Binding
{
Source = sourceObject,
Path = new PropertyPath(fieldInfo.Name),
Mode = BindingMode.TwoWay,
});
field.RegisterPropertyChangedCallback(dp, (s, e) =>
{
propertyField.OnValueChanged?.Invoke(propertyField);
});
return field;
}
public static PropertyField Create(string label, FieldInfo fieldInfo, object sourceObject)
{
var propertyField = new PropertyField
{
Label = label
};
FrameworkElement content = fieldInfo.FieldType switch
{
Type t when t == typeof(string) => ConfigureField(propertyField, fieldInfo, sourceObject, () => new TextBox()),
Type t when t == typeof(int) || t == typeof(float) || t == typeof(double) => ConfigureField(propertyField, fieldInfo, sourceObject, () => new NumberBox
{
SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Hidden,
AcceptsExpression = true,
NumberFormatter = new DecimalFormatter
{
FractionDigits = t == typeof(int) ? 0 : 9,
}
}),
Type t when t == typeof(bool) => ConfigureField(propertyField, fieldInfo, sourceObject, () => new ToggleSwitch()),
Type t when t == typeof(Enum) => ConfigureField(propertyField, fieldInfo, sourceObject, () => new ComboBox
{
ItemsSource = Enum.GetValues(t),
SelectedValuePath = "Value",
}),
_ => new TextBlock
{
Text = $"Unsupported type: {fieldInfo.FieldType.Name}",
VerticalAlignment = VerticalAlignment.Center,
Foreground = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Red)
},
};
propertyField.Content = content;
return propertyField;
}
public void UpdateValue()
{
if (_sourceObject == null || _propertyInfo == null || _fieldType == null)
{
return;
}
var currentValue = _propertyInfo.GetValue(_sourceObject);
if (Equals(currentValue, _lastValue))
{
return;
}
var dp = GetValueProperty(_fieldType);
if (dp != null)
{
SetValue(dp, _propertyInfo.GetValue(_sourceObject));
_lastValue = currentValue;
}
}
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Core.Controls">
<Style TargetType="local:PropertyField">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:PropertyField">
<Grid Height="32" Margin="2,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="125" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="0,0,0,4"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}"
Text="{TemplateBinding Label}"
TextTrimming="CharacterEllipsis" />
<ContentPresenter
Grid.Column="1"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,13 @@
using Microsoft.UI.Xaml;
namespace Ghost.Editor.Core.Controls;
public partial class ControlsDictionary : ResourceDictionary
{
private const string _DICTIONARY_PATH = "ms-appx:///Ghost.Editor.Core/Controls/ControlsDictionary.xaml";
public ControlsDictionary()
{
Source = new Uri(_DICTIONARY_PATH, UriKind.Absolute);
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/BasicInput/PropertyField.xaml" />
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/BasicInput/Float3Field.xaml" />
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/Internal/ComponentDataView.xaml" />
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/Internal/NavigationTabView.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Core.Controls.Internal">
<Style TargetType="local:ComponentView">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:ComponentView">
<StackPanel Margin="0,0,0,16">
<Border
Padding="8"
HorizontalAlignment="Stretch"
Background="{ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{TemplateBinding HeaderText}" />
</Border>
<StackPanel
x:Name="ContentContainer"
Margin="8,2,2,0"
Spacing="2" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,157 @@
using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Core.Resources;
using Ghost.Editor.Core.Utilities;
using Ghost.Entities;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.Controls.Internal;
internal sealed unsafe partial class ComponentView : Control
{
private delegate void EditorUpdate();
private StackPanel? _contentContainer;
private readonly World? _world;
private readonly Entity _entity = Entity.Invalid;
private readonly Type? _componentType;
private readonly ComponentInfo _componentInfo;
private object? _managedInstance;
private void* _pComponentData;
private ComponentEditor? _customEditor;
private PropertyField[]? _propertyFields;
private EditorUpdate? _editorUpdate;
public string HeaderText
{
get => (string)GetValue(HeaderTextProperty);
set => SetValue(HeaderTextProperty, value);
}
public static readonly DependencyProperty HeaderTextProperty =
DependencyProperty.Register(nameof(HeaderText), typeof(string), typeof(ComponentView), new PropertyMetadata(string.Empty));
internal ComponentView()
{
DefaultStyleKey = typeof(ComponentView);
Unloaded += (s, e) =>
{
_customEditor?.Destroy();
_contentContainer = null;
_customEditor = null;
_propertyFields = null;
};
}
public ComponentView(string header, World world, Entity entity, Type componentType) : this()
{
HeaderText = header;
_world = world;
_entity = entity;
_componentType = componentType;
_componentInfo = ComponentRegistry.GetComponentInfo(componentType);
}
protected override void OnApplyTemplate()
{
_contentContainer = (StackPanel)GetTemplateChild("ContentContainer");
base.OnApplyTemplate();
ReBuild();
}
private void ReflectionUpdate()
{
if (_propertyFields == null)
{
return;
}
foreach (var propertyField in _propertyFields)
{
propertyField.UpdateValue();
}
}
private void CustomEditorUpdate()
{
_customEditor?.Update();
}
public void ReBuild()
{
if (_contentContainer == null)
{
return;
}
_contentContainer.Children.Clear();
if (_world == null || _componentType == null || _entity == Entity.Invalid)
{
return;
}
if (_propertyFields != null)
{
foreach (var propertyField in _propertyFields)
{
propertyField.OnValueChanged -= OnPropertyValueChanged;
}
}
var componentObject = new ComponentObject(_world, _entity);
var editorType = TypeCache.GetTypes().FirstOrDefault(t =>
typeof(ComponentEditor).IsAssignableFrom(t) &&
t.GetCustomAttribute<CustomEditorAttribute>()?.TargetType.IsAssignableFrom(_componentType) == true);
if (editorType != null)
{
_customEditor = (ComponentEditor)Activator.CreateInstance(editorType)!;
_customEditor.Initialize(componentObject);
_customEditor.Create(_contentContainer);
}
else
{
var fields = _componentType.GetFields(StaticResource.ComponentPropertyBindingFlags);
_propertyFields = new PropertyField[fields.Length];
_pComponentData = _world.EntityManager.GetComponent(_entity, _componentInfo.id);
_managedInstance = Marshal.PtrToStructure((nint)_pComponentData, _componentType);
if (_managedInstance == null)
{
return;
}
for (var i = 0; i < fields.Length; i++)
{
var field = fields[i];
var propertyField = PropertyField.Create(field.Name, field, _managedInstance);
propertyField.OnValueChanged += OnPropertyValueChanged;
_propertyFields[i] = propertyField;
_contentContainer.Children.Add(propertyField);
}
}
_editorUpdate = _customEditor == null ? ReflectionUpdate : CustomEditorUpdate;
_editorUpdate();
}
private void OnPropertyValueChanged(PropertyField field)
{
if (_managedInstance == null || _pComponentData == null)
{
return;
}
Marshal.StructureToPtr(_managedInstance, (nint)_pComponentData, false);
}
}

View File

@@ -0,0 +1,42 @@
using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Controls.Internal;
public partial class NavigationTabPage : TabViewItem, INavigationAware
{
public virtual void OnNavigatedTo(object? parameter)
{
}
public virtual void OnNavigatedFrom()
{
}
}
public sealed partial class NavigationTabView : TabView
{
public NavigationTabView()
{
HorizontalAlignment = HorizontalAlignment.Stretch;
VerticalAlignment = VerticalAlignment.Stretch;
SelectionChanged += NavigationTabView_SelectionChanged;
}
private void NavigationTabView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (var oldItem in e.RemovedItems)
{
if (oldItem is NavigationTabPage oldPage)
{
oldPage.OnNavigatedFrom();
}
}
if (SelectedItem is NavigationTabPage newPage)
{
newPage.OnNavigatedTo(null);
}
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Controls.Internal" />

View File

@@ -0,0 +1,70 @@
using Ghost.Editor.Core.Event;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Controls;
public partial class ValueControl<T> : Control
{
private bool _suppressChangedEvent;
protected bool SuppressChangedEvent
{
get => _suppressChangedEvent;
set => _suppressChangedEvent = value;
}
public T Value
{
get => (T)GetValue(ValueProperty);
set
{
if (EqualityComparer<T>.Default.Equals(Value, value))
{
return;
}
SetValue(ValueProperty, value);
}
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(T), typeof(ValueControl<T>), new PropertyMetadata(default(T), ChangedCallback));
public event ValueChangedEventHandler<T>? OnValueChanged;
private static void ChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ValueControl<T> valueControl)
{
valueControl.ValueChanged((T)e.OldValue, (T)e.NewValue);
if (!valueControl._suppressChangedEvent)
{
valueControl.OnValueChanged?.Invoke(valueControl, new((T)e.OldValue, (T)e.NewValue));
}
}
}
protected virtual void ValueChanged(T oldValue, T newValue)
{
}
protected void RiseChangedEvent(T oldValue, T newValue)
{
OnValueChanged?.Invoke(this, new(oldValue, newValue));
}
/// <summary>
/// Sets the _value without notifying the change event.
/// </summary>
/// <param name="value">The new _value to set.</param>
/// <remarks>This method only suppresses the change event notification, not the <see cref="ValueChanged(T, T)"/> method.
/// Useful when you need to change the _value programmatically without triggering the change event.</remarks>
public void SetValueWithoutNotifying(T value)
{
_suppressChangedEvent = true;
SetValue(ValueProperty, value);
_suppressChangedEvent = false;
}
}

View File

@@ -0,0 +1,22 @@
namespace Ghost.Editor.Core.Event;
public delegate void ValueChangedEventHandler<T>(object? sender, ValueChangedEventArgs<T> args);
public class ValueChangedEventArgs<T> : EventArgs
{
public T OldValue
{
get;
}
public T NewValue
{
get;
}
public ValueChangedEventArgs(T oldValue, T newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
}

View File

@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Ghost.Editor.Core</RootNamespace>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<ImplicitUsings>enable</ImplicitUsings>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<!-- in .net 10, field keyword is not preview anymore, but we are still waiting roslyn team to update their code analyzer packages -->
<langversion>preview</langversion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ghost.Data\Ghost.Data.csproj" />
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
<ProjectReference Include="..\Ghost.Engine\Ghost.Engine.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="Controls\BasicInput\PropertyField.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\BasicInput\Vector3Field.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\ControlsDictionary.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\Internal\ComponentDataView.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\Internal\NavigationTabView.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,40 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Inspector;
public abstract class ComponentEditor
{
private ComponentObject _componentObject;
/// <summary>
/// Represents the underlying component object used by this class to manage its functionality.
/// </summary>
protected ComponentObject ComponentObject => _componentObject;
internal void Initialize(ComponentObject componentObject)
{
_componentObject = componentObject;
}
/// <summary>
/// Called when the component editor is created.
/// </summary>
/// <param name="container">The container to add the editor controls to.</param>
public virtual void Create(StackPanel container)
{
}
/// <summary>
/// Called when the component editor needs to update its UI based on the current state of the component data.
/// </summary>
public virtual void Update()
{
}
/// <summary>
/// Called when the component editor is destroyed.
/// </summary>
public virtual void Destroy()
{
}
}

View File

@@ -0,0 +1,27 @@
using Ghost.Entities;
namespace Ghost.Editor.Core.Inspector;
public readonly struct ComponentObject
{
private readonly World _world;
private readonly Entity _entity;
internal ComponentObject(World world, Entity entity)
{
_world = world;
_entity = entity;
}
public ref T GetData<T>()
where T : unmanaged, IComponent
{
return ref _world.EntityManager.GetComponent<T>(_entity);
}
public void SetData<T>(in T data)
where T : unmanaged, IComponent
{
_world.EntityManager.SetComponent(_entity, data);
}
}

View File

@@ -0,0 +1,10 @@
namespace Ghost.Editor.Core.Inspector;
[AttributeUsage(AttributeTargets.Class)]
public class CustomEditorAttribute(Type targetType) : Attribute
{
internal Type TargetType
{
get;
} = targetType;
}

View File

@@ -0,0 +1,13 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Inspector;
public interface IInspectable
{
public IconSource? CreateIcon();
public UIElement? CreateHeader();
public UIElement? CreateInspector();
}

View File

@@ -0,0 +1,12 @@
namespace Ghost.Editor.Core.Inspector;
internal interface IInspectorService
{
public IInspectable? SelectedInspectable
{
get;
set;
}
public event Action? OnSelectionChanged;
}

View File

@@ -0,0 +1,19 @@
namespace Ghost.Editor.Core.Inspector;
public class InspectorService : IInspectorService
{
public IInspectable? SelectedInspectable
{
get => field;
set
{
if (field != value)
{
field = value;
OnSelectionChanged?.Invoke();
}
}
}
public event Action? OnSelectionChanged;
}

View File

@@ -0,0 +1,9 @@
using CommunityToolkit.WinUI.Behaviors;
namespace Ghost.Editor.Core.Notifications;
public interface INotificationService
{
public void ShowNotification(string? message, MessageType type, int duration = 5, string? title = null);
public void ShowNotification(Notification notification);
}

View File

@@ -0,0 +1,9 @@
namespace Ghost.Editor.Core.Notifications;
public enum MessageType
{
Informational,
Success,
Warning,
Error
}

View File

@@ -0,0 +1,49 @@
using CommunityToolkit.WinUI.Behaviors;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Notifications;
public class NotificationService : INotificationService
{
private InfoBar? _infoBar;
private StackedNotificationsBehavior? _notificationQueue;
internal void SetReference(InfoBar infoBar, StackedNotificationsBehavior notificationQueue)
{
_infoBar = infoBar;
_notificationQueue = notificationQueue;
}
public void ShowNotification(string? message, MessageType type, int duration = 5, string? title = null)
{
if (string.IsNullOrWhiteSpace(message))
{
return;
}
var notification = new Notification
{
Message = message,
Severity = (InfoBarSeverity)type,
Duration = TimeSpan.FromSeconds(duration),
Title = title
};
ShowNotification(notification);
}
public void ShowNotification(Notification notification)
{
_notificationQueue?.Show(notification);
}
internal void ClearReference()
{
if (_infoBar != null)
{
_infoBar.IsOpen = false;
}
_infoBar = null;
_notificationQueue = null;
}
}

View File

@@ -0,0 +1,9 @@
namespace Ghost.Editor.Core.Progress;
public interface IProgressService
{
public void ShowProgress(string message, double progress = 0.0);
public void ShowIndeterminateProgress(string message);
public void SetProgress(double progress);
public void HideProgress();
}

View File

@@ -0,0 +1,74 @@
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Runtime.CompilerServices;
namespace Ghost.Editor.Core.Progress;
public class ProgressService : IProgressService
{
private Grid? _progressBarContainer;
private TextBlock? _progressMessage;
private ProgressBar? _progressBar;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsInitialized()
{
return _progressBarContainer != null && _progressMessage != null && _progressBar != null;
}
internal void SetReference(Grid progressBarContainer)
{
_progressBarContainer = progressBarContainer;
_progressMessage = _progressBarContainer.FindChild<TextBlock>();
_progressBar = _progressBarContainer.FindChild<ProgressBar>();
}
public void ShowProgress(string message, double progress = 0.0)
{
if (!IsInitialized())
{
return;
}
_progressBarContainer!.Visibility = Visibility.Visible;
_progressMessage!.Text = message;
_progressBar!.Value = progress;
}
public void ShowIndeterminateProgress(string message)
{
if (!IsInitialized())
{
return;
}
_progressBarContainer!.Visibility = Visibility.Visible;
_progressMessage!.Text = message;
_progressBar!.IsIndeterminate = true;
}
public void SetProgress(double progress)
{
_progressBar!.Value = progress;
}
public void HideProgress()
{
if (!IsInitialized())
{
return;
}
_progressBarContainer!.Visibility = Visibility.Collapsed;
_progressMessage!.Text = string.Empty;
_progressBar!.Value = 0.0;
}
internal void ClearReference()
{
_progressBarContainer = null;
_progressMessage = null;
_progressBar = null;
}
}

View File

@@ -0,0 +1,18 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Resources;
public static class EditorIconSource
{
public static readonly IconSource scene_24 = new FontIconSource
{
Glyph = "\uF159",
FontSize = 24
};
public static readonly IconSource entity_24 = new FontIconSource
{
Glyph = "\uF158",
FontSize = 24
};
}

View File

@@ -0,0 +1,8 @@
using System.Reflection;
namespace Ghost.Editor.Core.Resources;
internal static class StaticResource
{
public static readonly BindingFlags ComponentPropertyBindingFlags = BindingFlags.Public | BindingFlags.Instance;
}

View File

@@ -0,0 +1,45 @@
using Ghost.Entities;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.SceneGraph;
public sealed partial class EntityNode : SceneGraphNode
{
private readonly Entity _entity;
public Entity Entity => _entity;
public override IconSource? CreateIcon()
{
return new FontIconSource
{
Glyph = "\uF158"
};
}
public override UIElement? CreateHeader()
{
return null;
}
public override UIElement? CreateInspector()
{
throw new NotImplementedException();
}
public override DataTemplate GetSceneHierarchyTemplate()
{
var template = @"
<DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" xmlns:sg=""using:Ghost.Editor.Core.SceneGraph"" x:Key=""EntityTemplate"" x:DataType=""sg:SceneGraphNode"">
<TreeViewItem AutomationProperties.Name=""{x:Bind Name, Mode=OneWay}"" ItemsSource=""{x:Bind Children, Mode=OneWay}"">
<StackPanel Margin=""10,0"" Orientation=""Horizontal"">
<FontIcon FontSize=""14"" Glyph=""&#xF158;"" />
<TextBlock Margin=""5,0,0,0"" Text=""{x:Bind Name, Mode=OneWay}"" />
</StackPanel>
</TreeViewItem>
</DataTemplate>";
return (DataTemplate)Microsoft.UI.Xaml.Markup.XamlReader.Load(template);
}
}

View File

@@ -0,0 +1,87 @@
# Architecture Plan: Scene Graph and Scene Representation
The Scene Graph is a hierarchical structure that represents all the objects and entities within a 3D scene in the Ghost Editor.
## Scene Graph (Editor representation of runtime data)
There should be three main types of nodes in the Scene Graph for now:
1. **Scene Graph Node**: The base class for all nodes in the Scene Graph.
2. **Entity Node**: Represents an individual entity within a scene. Name stored here, not runtime component.
3. **Scene Node**: Represents a Scene object, which can contain multiple entities. Name stored here not runtime data.
### Editor World
Editor contains a different world compares to the runtime world. When user click the Play button, we will create a runtime world and load the scene data from the editor world to the runtime world.
This allows us to
1. Unload the runtime only systems like physics, rendering, etc when user stop playing.
2. Load editor only systems like gizmos, debug, etc when user stop playing.
3. Allow editor only entities like editor camera, editor lights, etc to exist in the editor world without affecting the runtime world.
### Editor Hierarchy
The Scene Graph should be represented as a tree structure in the editor (TreeView in WinUI 3), where:
- The top level nodes represents the loaded Scenes in the editor world.
- Levels below the Scene nodes represents the Entity nodes that belong to that scene.
- Each Entity node can have child Entity nodes representing parent-child relationships between entities.
An example hierarchy could look like this:
```
- Scene 1
- Entity A
- Entity B
- Entity C
- Scene 2
- Entity D
```
## Scene (The runtime representation)
A Scene is a collection of entities with SceneID component from a world that are grouped together. There can be multiple scenes in a world.
### Save a Scene
When save a scene, all entities with the SceneID component matching the scene's ID should be included in the saved data.
When an Entity references another Entity in the same scene, we should store the file local id instead of the global entity id.
For example, if Entity A (id: 10, 5th in scene) references Entity B (id: 20, 50th in scene) in the same scene, in the saved data for Entity A,
we should store 50 (the file local id) as the reference to Entity B instead of 20 (the global entity id).
> We does not allow cross-scene references for now because ideally it's not a good practice to have cross-scene references.
> We can use query or singleton pattern to access entities from other scenes if needed because they are in the same world.
### Load a Scene
When loading a scene, we need to reconstruct the entities and their relationships based on the saved data.
1. We allocate the entities in the world and assign them new global entity IDs.
2. We remap the file local IDs to the new global entity IDs and change the references accordingly.
For example if Entity A (file local id: 5) references Entity B (file local id: 50) in the saved data,
we need to find the new global entity IDs for both entities after loading and update the reference in Entity A to point to the new global entity ID of Entity B.
### Data format
The scene data should be stored in a structured format (JSON and binary) that includes:
- List of entities with their components and properties (Entities must in the order that file local id directly maps to the index in the list)
- References between entities using file local IDs
> The name of the saved scene file should match the name of the scene node in the editor.
JSON should only be used in the editor and JSON serialization/deserialization logic should also only exist in the editor codebase (Ghost.Editor.Core). Reflection is allowed here.
Binary format should be used in the runtime for better performance. The runtime codebase (Ghost.Engine) must be aot compatible.
Currently we strict the IComponent to must be unmanaged and blittable types.
However, we also support ManagedEntity and ManagedEntityRef with ScriptComponent to allow OOP like logic for common gameplay logic that DOD pattern is not suitable for.
Serializing/deserializing with those components will be tricky. We can use MemoryPack (already installed) for binary serialization/deserialization because it supports both unmanaged and managed types.
## What need to implement
- [ ] Scene type for the runtime representation if needed
- [ ] Scene Graph data structures (SceneNode, EntityNode)
- [ ] Editor World management (loading/unloading scenes, managing entities)
- [ ] Scene saving/loading logic with file local ID remapping
- [ ] Serialization/deserialization logic for scene data (JSON for editor, binary for runtime)
- [ ] UI integration for displaying and managing the Scene Graph in the editor with WinUI 3 TreeView

View File

@@ -0,0 +1,27 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Core.Inspector;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.ObjectModel;
namespace Ghost.Editor.Core.SceneGraph;
public abstract partial class SceneGraphNode : ObservableObject, IInspectable
{
[ObservableProperty]
public partial string Name
{
get; set;
}
public ObservableCollection<SceneGraphNode> Children
{
get;
} = new();
public abstract IconSource? CreateIcon();
public abstract UIElement? CreateHeader();
public abstract UIElement? CreateInspector();
public abstract DataTemplate GetSceneHierarchyTemplate();
}

View File

@@ -0,0 +1,45 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.SceneGraph;
public sealed partial class SceneNode : SceneGraphNode
{
public override IconSource? CreateIcon()
{
return new FontIconSource
{
Glyph = "\uF156"
};
}
// TODO: Implement custom header and inspector UI for the SceneNode
public override UIElement? CreateHeader()
{
return null;
}
public override UIElement? CreateInspector()
{
return null;
}
public override DataTemplate GetSceneHierarchyTemplate()
{
var template = @"
<DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" xmlns:sg=""using:Ghost.Editor.Core.SceneGraph"" x:DataType=""sg:SceneGraphNode"">
<TreeViewItem
AutomationProperties.Name=""{x:Bind Name, Mode=OneWay}""
Background=""{ThemeResource ControlSolidFillColorDefaultBrush}""
IsExpanded=""True""
ItemsSource=""{ x:Bind Children, Mode=OneWay}"" >
<StackPanel Orientation=""Horizontal"" >
<FontIcon FontSize=""14"" Glyph=""&#xF156;""/>
<TextBlock Margin=""10,0"" Text=""{ x:Bind Name, Mode=OneWay}""/>
</StackPanel>
</TreeViewItem>
</DataTemplate>";
return (DataTemplate)Microsoft.UI.Xaml.Markup.XamlReader.Load(template);
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.UI.Xaml;
namespace Ghost.Editor.Core.Utilities;
public static class EditorApplication
{
private static IServiceProvider? _serviceProvider;
public static Application Current => Application.Current;
internal static void Initialize(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public static T GetService<T>()
where T : class
{
if (_serviceProvider?.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
}
return service;
}
}

View File

@@ -0,0 +1,15 @@
using Ghost.Data.Models;
namespace Ghost.Editor.Core.Utilities;
internal static class FileExtensions
{
public const string META_FILE_EXTENSION = ".gmeta";
public const string PROJECT_FILE_EXTENSION = "." + ProjectMetadata.PROJECT_FILE_EXTENSION_NAME;
public const string TEMPLATE_FILE_EXTENSION = ".gtmpl";
public const string SCENE_FILE_EXTENSION = ".gscene";
public const string ASSET_FILE_EXTENSION = ".gasset";
public const string SHADER_FILE_EXTENSION = ".gshdr";
public const string MATERIAL_FILE_EXTENSION = ".gmat";
}

View File

@@ -0,0 +1,36 @@
using Ghost.Core.Attributes;
using System.Reflection;
namespace Ghost.Editor.Core.Utilities;
public static class TypeCache
{
private static readonly TypeInfo[] s_types;
static TypeCache()
{
var loadableTypes = new List<Type>(512);
var assembliesToScan = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetCustomAttribute<EngineAssemblyAttribute>() != null);
foreach (var assembly in assembliesToScan)
{
try
{
loadableTypes.AddRange(assembly.GetTypes());
}
catch (ReflectionTypeLoadException ex)
{
var types = ex.Types.Where(t => t != null);
loadableTypes.AddRange(types!);
}
}
s_types = loadableTypes.Select(t => t.GetTypeInfo()).ToArray();
}
public static Type[] GetTypes()
{
return s_types;
}
}

View File

@@ -0,0 +1,30 @@
using Ghost.Data.Resources;
using Ghost.Data.Services;
using Ghost.Editor.Core.Utilities;
using Microsoft.UI.Xaml;
namespace Ghost.Editor;
internal static class ActivationHandler
{
private static void FolderInitialization()
{
if (!Directory.Exists(DataPath.s_applicationDataFolder))
{
Directory.CreateDirectory(DataPath.s_applicationDataFolder);
}
if (!Directory.Exists(DataPath.s_projectTemplateFolder))
{
Directory.CreateDirectory(DataPath.s_projectTemplateFolder);
}
}
public static void Handle(LaunchActivatedEventArgs args)
{
FolderInitialization();
ProjectService.EnsureDefaultTemplate();
EditorApplication.Initialize(((App)(Application.Current)).Host.Services);
}
}

Some files were not shown because too many files have changed in this diff Show More