109 Commits

Author SHA1 Message Date
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
351 changed files with 62260 additions and 163 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);
}
}

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

@@ -0,0 +1,196 @@
using System.Collections.ObjectModel;
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);
}
public void Log(string message, LogLevel level)
{
lock (_lock)
{
_logs.Add(new LogMessage(level, message));
}
}
public void Log(Exception exception)
{
lock (_lock)
{
_logs.Add(new LogMessage(LogLevel.Error, exception.Message, exception.StackTrace));
}
}
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;
public static void Log(LogLevel level, object? message)
{
s_logger.Log(message?.ToString() ?? "null", level);
}
public static void Log(LogLevel level, string message)
{
s_logger.Log(message, level);
}
public static void Log(LogLevel level, string format, params object?[] args)
{
s_logger.Log(string.Format(format, args), level);
}
public static void LogInfo(object? message)
{
s_logger.Log(message?.ToString() ?? "null", LogLevel.Info);
}
public static void LogInfo(string message)
{
s_logger.Log(message, LogLevel.Info);
}
public static void LogInfo(string format, params object?[] args)
{
s_logger.Log(string.Format(format, args), LogLevel.Info);
}
public static void LogWarning(object? message)
{
s_logger.Log(message?.ToString() ?? "null", LogLevel.Warning);
}
public static void LogWarning(string message)
{
s_logger.Log(message, LogLevel.Warning);
}
public static void LogWarning(string format, params object?[] args)
{
s_logger.Log(string.Format(format, args), LogLevel.Warning);
}
public static void LogError(object? message)
{
s_logger.Log(message?.ToString() ?? "null", LogLevel.Error);
}
public static void LogError(string message)
{
s_logger.Log(message, LogLevel.Error);
}
public static void LogError(string format, params object?[] args)
{
s_logger.Log(string.Format(format, args), LogLevel.Error);
}
public static void LogError(Exception ex)
{
s_logger.Log(ex);
}
public static void Assert(bool condition, string message)
{
s_logger.Assert(condition, message);
}
public static void Clear()
{
s_logger.Clear();
}
}

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

@@ -0,0 +1,386 @@
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;
}
}

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,7 @@
using Ghost.Core.Attributes;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ghost.Editor")]
[assembly: InternalsVisibleTo("Ghost.Editor.Core")]
[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,51 @@
namespace Ghost.Data.Models;
public class ProjectMetadata
{
public const string PROJECT_EXTENSION = "ghostproj";
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
public ProjectMetadata()
{
}
}
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,225 @@
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 CONFIG_FOLDER = "ProjectConfig";
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_EXTENSION}", 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_EXTENSION}");
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 Task<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 Task<Result> OnEnteringAsync(object? parameter);
/// <summary>
/// Called when exiting the state, specifically for pose transitions.
/// </summary>
public Task<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 Task<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,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,170 @@
using Ghost.Core;
using Ghost.Editor.Core.Utilities;
using System.Reflection;
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 readonly Dictionary<Guid, string> s_assetPathLookup = new();
private static readonly Dictionary<string, Guid> s_pathAssetLookup = 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 += OnAssetCreated;
s_watcher.Deleted += OnAssetDeleted;
s_watcher.Renamed += OnAssetRenamed;
}
private static Result<string, Error> GetMetaFilePath(string assetPath)
{
if (Directory.Exists(assetPath))
{
return Error.NotFound;
}
if (Path.GetExtension(assetPath).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase))
{
return Error.InvalidState;
}
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;
}
private static async Task<Result> WriteMetaFileAsync(string metaFilePath, AssetMeta metaData)
{
using var fileStream = File.Create(metaFilePath);
try
{
await JsonSerializer.SerializeAsync(fileStream, metaData);
}
catch (Exception ex)
{
return Result.Failure(ex.Message);
}
return Result.Success();
}
internal static async Task<Result> GenerateMetaFileAsync(string assetPath)
{
Result r;
var metaFileResult = GetMetaFilePath(assetPath);
if (metaFileResult.IsFailure)
{
return Result.Failure(metaFileResult.Error);
}
if (File.Exists(metaFileResult.Value))
{
using var fileStream = File.OpenRead(metaFileResult.Value);
var existingMeta = await JsonSerializer.DeserializeAsync<AssetMeta>(fileStream);
if (existingMeta != null && s_assetPathLookup.TryGetValue(existingMeta.Guid, out var path))
{
if (assetPath != path)
{
existingMeta.Guid = Guid.NewGuid();
r = await WriteMetaFileAsync(metaFileResult.Value, existingMeta);
if (r.IsFailure)
{
return r;
}
}
}
return Result.Success();
}
var defaultSettings = GetDefaultSettingsForAsset(assetPath);
var metaData = new AssetMeta
{
Guid = Guid.NewGuid(),
Settings = defaultSettings
};
r = await WriteMetaFileAsync(metaFileResult.Value, metaData);
return r;
}
private static async void OnAssetCreated(object sender, FileSystemEventArgs e)
{
await GenerateMetaFileAsync(e.FullPath);
}
private static void OnAssetDeleted(object sender, FileSystemEventArgs e)
{
var metaFileResult = GetMetaFilePath(e.FullPath);
if (metaFileResult.IsSuccess && File.Exists(metaFileResult.Value))
{
try
{
var meta = JsonSerializer.Deserialize<AssetMeta>(File.ReadAllText(metaFileResult.Value));
if (meta != null
&& s_assetPathLookup.TryGetValue(meta.Guid, out var path)
&& path == e.FullPath)
{
s_assetPathLookup.Remove(meta.Guid);
}
File.Delete(metaFileResult.Value);
}
catch (Exception ex)
{
Logger.LogError(ex);
}
}
}
private static async void OnAssetRenamed(object sender, RenamedEventArgs e)
{
var oldMetaPath = e.OldFullPath + FileExtensions.META_FILE_EXTENSION;
var newMetaPath = e.FullPath + FileExtensions.META_FILE_EXTENSION;
if (File.Exists(oldMetaPath))
{
File.Move(oldMetaPath, newMetaPath);
}
else
{
await GenerateMetaFileAsync(e.FullPath);
}
}
}

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>> _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 (_assetOpenHandlers.ContainsKey(ext))
{
Logger.LogError($"Duplicate asset open handler for extension '{ext}' found in method '{method.Name}'. Existing handler will be overwritten.");
}
_assetOpenHandlers[ext] = del;
}
}
}
public static void OpenAsset(string path)
{
var extension = Path.GetExtension(path);
if (_assetOpenHandlers.TryGetValue(extension, out var handler))
{
handler(path);
}
else
{
Process.Start(new ProcessStartInfo(path)
{
UseShellExecute = true
});
}
}
}

View File

@@ -0,0 +1,33 @@
using Ghost.Data.Services;
namespace Ghost.Editor.Core.AssetHandle;
public static partial class AssetDatabase
{
private static FileSystemWatcher? s_watcher;
public static DirectoryInfo? AssetsDirectory
{
get;
private set;
}
internal static void Initialize()
{
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_watcher = new FileSystemWatcher
{
Path = AssetsDirectory.FullName,
IncludeSubdirectories = true,
EnableRaisingEvents = true
};
InitializeAssetHandle();
InitializeMetaData();
}
}

View File

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

View File

@@ -0,0 +1,16 @@
namespace Ghost.Editor.Core.AssetHandle;
internal class AssetMeta
{
public Guid Guid
{
get;
set;
}
public ImporterSettings? Settings
{
get;
set;
}
}

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,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/Vector3Field.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,45 @@
<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.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
</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,22 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Inspector;
public interface IInspectable
{
public IconSource? Icon
{
get;
}
public UIElement? HeaderContent
{
get;
}
public UIElement? InspectorContent
{
get;
}
}

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,10 @@
using Ghost.Entities;
namespace Ghost.Editor.Core.SceneGraph;
public sealed partial class EntityNode : SceneGraphNode
{
private readonly Entity _entity;
public Entity Entity => _entity;
}

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 (e.g., JSON or 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 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,18 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
namespace Ghost.Editor.Core.SceneGraph;
public abstract partial class SceneGraphNode : ObservableObject
{
[ObservableProperty]
public partial string Name
{
get; set;
}
public ObservableCollection<SceneGraphNode> Children
{
get;
} = new();
}

View File

@@ -0,0 +1,5 @@
namespace Ghost.Editor.Core.SceneGraph;
public sealed partial class SceneNode : SceneGraphNode
{
}

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,13 @@
namespace Ghost.Editor.Core.Utilities;
internal static class FileExtensions
{
public const string META_FILE_EXTENSION = ".gmeta";
public const string PROJECT_FILE_EXTENSION = ".gproj";
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>();
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);
}
}

View File

@@ -3,14 +3,16 @@
x:Class="Ghost.Editor.App" x:Class="Ghost.Editor.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:core="using:Ghost.Editor.Core.Controls"
xmlns:local="using:Ghost.Editor"> xmlns:local="using:Ghost.Editor">
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" /> <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here --> <ResourceDictionary Source="ms-appx:///Microsoft.UI.Xaml/DensityStyles/Compact.xaml" />
<core:ControlsDictionary />
<ResourceDictionary Source="/Themes/Override.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@@ -1,34 +1,113 @@
using Microsoft.UI.Xaml; using Ghost.Core;
using Ghost.Editor.Core.AppState;
using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Core.Notifications;
using Ghost.Editor.Core.Progress;
using Ghost.Editor.Utilities;
using Ghost.Engine.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Ghost.Editor namespace Ghost.Editor;
{
/// <summary> /// <summary>
/// Provides application-specific behavior to supplement the default Application class. /// Provides application-specific behavior to supplement the default Application class.
/// </summary> /// </summary>
public partial class App : Application public partial class App : Application
{ {
private Window? _window;
internal static Window? Window
{
get => (Current as App)!._window;
set
{
if (Current is App app)
{
// HACK: As far as I can tell, there is no proper application shutdown event in WinUI 3.
app._window?.Closed -= app.OnClosed;
app._window = value;
app._window?.Closed += app.OnClosed;
}
}
}
internal IHost Host
{
get;
}
/// <summary> /// <summary>
/// Initializes the singleton application object. This is the first line of authored code /// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain(). /// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary> /// </summary>
public App() internal App()
{ {
InitializeComponent(); InitializeComponent();
Host = Microsoft.Extensions.Hosting.Host.
CreateDefaultBuilder().
UseContentRoot(AppContext.BaseDirectory).
ConfigureServices((context, services) =>
{
HostHelper.AddLandingScope(context, services);
HostHelper.AddEngineScope(context, services);
services.AddSingleton<AppStateMachine>();
services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<IProgressService, ProgressService>();
services.AddSingleton<IInspectorService, InspectorService>();
})
.Build();
UnhandledException += App_UnhandledException;
}
internal static IServiceScope CreateScope()
{
return (Current as App)!.Host.Services.CreateScope();
}
public static T GetService<T>() where T : class
{
if ((Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
}
return service;
} }
/// <summary> /// <summary>
/// Invoked when the application is launched. /// Invoked when the application is launched.
/// </summary> /// </summary>
/// <param name="args">Details about the launch request and process.</param> /// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args) protected override async void OnLaunched(LaunchActivatedEventArgs args)
{ {
m_window = new MainWindow(); base.OnLaunched(args);
m_window.Activate();
await Host.StartAsync();
ActivationHandler.Handle(args);
var stateMachine = GetService<AppStateMachine>();
stateMachine.RegisterState(StateKey.Landing, () => new LandingState());
stateMachine.RegisterState(StateKey.EngineEditor, () => new EditorState());
await stateMachine.TransitionToAsync(StateKey.Landing);
} }
private Window? m_window; private void OnClosed(object? sender, WindowEventArgs args)
{
Host.StopAsync().GetAwaiter().GetResult();
Host.Dispose();
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError(e.Exception);
} }
} }

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

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