150 Commits

Author SHA1 Message Date
bc78c8fbee docs: add XML summary comments to public Meshlet types and methods 2026-03-17 04:02:28 +00:00
2376fc9414 fix: resolve all build errors in Meshlet LOD pipeline
- Use correct UnsafeList constructor (int capacity, Allocator)
- Use .Count instead of .Length for UnsafeList
- Cast GetUnsafePtr() to typed pointers explicitly
- Use Api.meshopt_* constants (not MeshOptApi) for simplify flags
- Use meshopt_Meshlet instance methods BuildsFlex/BuildsSpatial
- Use correct meshopt_Meshlet field names (vertex_offset, triangle_offset, etc.)
- Fix byte constant overflow with unchecked cast in LockBoundary
- Add Ghost.MeshOptimizer project reference to Ghost.Graphics.csproj
2026-03-17 03:14:09 +00:00
0a3502b858 refactor: optimize memory for mega-meshes and fix collection usage
- Switch large/dynamic collections from StackScope to Allocator.Temp/Persistent to prevent stack overflow
- Remove redundant Resize calls; use capacity in constructor + Add or AsSpan().Fill for initialization
- Correctly propagate Allocator parameter for returned collections
- Ensure all temporary collections are properly disposed before returning
- Refine ClodBuilder, ClodPartition, ClodBoundsHelper, and ClodSimplify for high-scale mesh processing
2026-03-17 02:34:42 +00:00
22fdae1061 cleanup: remove obsolete Clod.cs file 2026-03-17 02:21:01 +00:00
92c503b253 refactor: address PR review feedback on meshlet LOD system
- Remove WORK_SUMMARY.md
- Use Debug.Assert for stride validation in ClodBuilder
- Fix ClodBuilder.Build return value after cluster disposal
- Update ClodPartition to accept AllocationHandle for return collections
- Standardize on camelCase for public fields in ClodConfig, ClodMesh, ClodBounds, etc.
- Remove redundant Resize calls where capacity suffices or Add is used
- Enforce stack allocator usage for internal temporary collections
- Ensure proper allocator propagation for collections returned from methods
2026-03-17 02:18:37 +00:00
f7fb7da496 fix: correct API calls and cleanup documentation
- Replace .Ptr with .GetUnsafePtr() for UnsafeList access
- Use proper MeshOptApi method names (CamelCase): ComputeClusterBounds, BuildMeshletsBound, PartitionClusters, etc.
- Fix SimplifyVertex_Protect constant access
- Remove IMPLEMENTATION_COMPLETE.md
- Rename AGENT_GUIDELINES.md to AGENT.md
2026-03-17 00:23:42 +00:00
2ba60c4bae refactor: improve unsafe collection API usage per review
- Replace float[3] with Vector3 (System.Numerics)
- Use UnsafeList.GetUnsafePtr() instead of fixed blocks
- Use AllocationManager.CreateStackScope() for temporary collections
- Remove redundant properties in ClodConfig (public fields suffice)
- Use MeshOptApi directly instead of Api alias
- Fix method signatures to not require allocator for stack-scoped collections
2026-03-16 23:55:19 +00:00
f2b68955b1 docs: comprehensive implementation summary 2026-03-16 16:12:33 +00:00
85a000e5c4 docs: add work summary for clusterlod translation 2026-03-16 16:05:02 +00:00
301a6d1c45 feat: translate clusterlod to C# and restructure to Ghost.Graphics.Meshlet 2026-03-16 16:01:57 +00:00
e831b71a79 feat(bindings): update C# wrappers for meshopt, nvtt, ufbx
Refactor and regenerate native C# bindings for Ghost.MeshOptimizer, Ghost.Nvtt, and Ghost.Ufbx to match updated native APIs and improve usability.
- Replace meshoptimizer.dll with newer version.
- Move meshoptimizer functions to static methods in partial class; add new meshlet, simplification, quantization features.
- Remove enum wrappers in favor of constants; delete meshopt_Allocator.cs.
- Regenerate native wrappers with PascalCase naming, XML doc comments, and aggressive inlining.
- Implement IDisposable for resource structs; update configs for naming, documentation, and method mapping.
- Update user code to use new wrapper classes and method names.
- Improve documentation and comments for clarity.

BREAKING CHANGE: API surface changes, wrapper class and method names updated, enum wrappers removed, custom allocator deleted.
2026-03-17 00:19:54 +09:00
9bae3e647e docs(README): rewrite and expand documentation
Major rewrite of the README for Ghost.NativeWrapperGen:
- Clarifies project purpose, design, and intended usage.
- Adds concrete code examples of generated wrappers.
- Updates CLI usage and provides validated command lines.
- Replaces old config schema docs with detailed tables for all config fields, including new `remaps` and `actions`.
- Documents the new config-driven routing and remapping system.
- Explains the action-based method routing with conditions and apply steps.
- Details naming conventions and method name derivation.
- Provides a full, modern config example for nvtt.
- Updates recommended workflow for wrapper regeneration and build.
- Emphasizes the design principle of keeping policy in config.
- Removes outdated sections and limitations.
- Improves formatting and accessibility for new users.
2026-03-15 21:15:52 +09:00
6cadd8edeb feat(nativegen)!: refactor to struct-based native wrappers
Major overhaul of native wrapper generation for ufbx and nvtt.
Replaces all hand-written and class-based wrappers with auto-generated partial struct wrappers that directly expose native API methods via pointers. Introduces a new JSON-driven configuration system using "remaps" and "actions" for flexible parameter/return mapping and method routing. Removes legacy config sections and helper classes, focusing solely on method wrappers. Updates all usages and tests to use the new pointer-based API. Cleans up obsolete code and ensures resource management is handled via struct Dispose methods. The result is a thinner, more direct, and maintainable interop layer.

BREAKING CHANGE: All managed wrapper classes and helpers are removed in favor of struct-based pointer wrappers. API usage and resource management patterns have changed.
2026-03-15 20:48:54 +09:00
3e4084c42a Added ufbx warper 2026-03-15 02:19:40 +09:00
cce1cf7256 Added Ufbx 2026-03-14 18:29:18 +09:00
254b08bc81 Added doc folder 2026-03-14 12:33:12 +09:00
912b320d8f Fixed compilation errors;
Added MaterialPalette
2026-03-14 12:27:49 +09:00
8a3b40b4f8 Refactor MeshInstance 2026-03-13 15:10:25 +09:00
619720feee Render extraction system & ECS/graphics refactor
Introduced RenderExtractionSystem for entity-based render data extraction. Added MeshInstance and MeshPalette components with shadow casting support. Refactored QueryBuilder API, SharedComponentStore, and component registration for clarity and flexibility. Updated SystemManager and SystemGroup to use SystemAPI. Replaced RenderingConfig with GraphicsEngineDesc/RenderSystemDesc. RenderFrame now uses CPU/GPU fence values for sync. Removed Camera.cs in favor of ECS-based rendering. Improved Material, RenderingLayerMask, Mesh, and RenderList APIs. Updated package references and fixed naming, error handling, and disposal issues.
2026-03-08 22:51:03 +09:00
bfe8588d76 Refactor render pipeline and resource management APIs
Split IFenceSynchronizer/IRenderSystem interfaces for clarity. Refactor D3D12GraphicsEngine to use IFenceSynchronizer. Update RenderGraph and context to use explicit resource manager/database/allocator references. Add multi-buffering methods to IRGBuilder (stub). Support history access for multi-frame resources. Remove legacy RenderPipelineBase; introduce IRenderPipelineSettings and sealed GhostRenderPipeline. Clean up resource aliasing and pool logic. Improve modularity and future extensibility.
2026-03-03 20:14:22 +09:00
b8af6e8c3a Added RenderPipelineBase 2026-03-02 19:06:19 +09:00
5e42d699c3 Added RenderList and RenderReques.
Added IRenderPipeline.
2026-02-28 21:31:38 +09:00
6f802ac12b Added CopyTexture support in ICommandBuffer 2026-02-26 21:40:07 +09:00
162b71f309 Refactor render graph error handling and resource APIs
- RenderGraph.Compile/Execute now return Error for better failure detection; error handling is propagated throughout compiler and executor.
- Renamed ScheduleReleaseResource to ReleaseResource for clarity; updated all usages.
- ResourceManager now calls ReleaseResource directly on Mesh, Material, and Shader types.
- Camera exposes Actual/Virtual size properties and Render returns Error.
- RenderingContext now uses IResourceManager for mesh/resource ops.
- Replaced custom BinaryWriter with BufferWriter in RenderGraphHasher.
- Improved variable naming, interface signatures, and code formatting.
- Added Error extension for IsSuccess/IsFailure.
- Minor FMOD/native interop and test code cleanups.
- No breaking API changes except for new Error return values on some methods.
2026-02-25 19:08:54 +09:00
30090f84ab Refactor rendering projects 2026-02-24 20:08:26 +09:00
93c58fa7fb Add Ghost.Nvtt C# wrapper and integrate nvtt texture pipeline
- Introduce full managed C# wrapper for NVIDIA Texture Tools (nvtt) with safe handle classes, idiomatic APIs, and managed callback support.
- Integrate Ghost.Nvtt into Ghost.Editor.Core and Ghost.MicroTest; update TextureAssetHandler to use the new nvtt wrapper for texture compression.
- Add comprehensive end-to-end binding test (NvttBindingTest).
- Refactor D3D12 resource management: add deferred/immediate release APIs, update allocator/database usage, and ensure proper resource cleanup.
- Update project files for new native DLL layout and dependency versions.
- Minor API cleanups: EditorApplication properties, D3D12 input layout, and removal of obsolete code.
- Update shaders, tests, and documentation for new APIs and usage patterns.
2026-02-23 17:13:10 +09:00
78e3b4ef31 Merge branch 'develop' of https://github.com/misakieku/GhostEngine into develop 2026-02-18 00:52:23 +09:00
db8ca971a8 Refactor folder structure 2026-02-18 00:52:18 +09:00
638417d4f0 Refactor folder structure 2026-02-18 00:50:46 +09:00
426786397c Modify AssetService 2026-02-05 19:25:48 +09:00
9bbccfc8f8 Update ContextFlyout 2026-02-05 13:52:53 +09:00
eadd13931f Updating ProjectBrowser 2026-02-04 19:08:18 +09:00
59991f47d5 Update editor 2026-02-03 21:49:14 +09:00
9fcf06dbe4 Update icon assets 2026-02-01 01:54:04 +09:00
6505099667 Fixed the issue where the test does not cleanup the temp folder 2026-02-01 01:53:19 +09:00
d263f0c7e1 Imporving AssetDatabase 2026-01-30 21:20:18 +09:00
9f05944d81 Improve AssetDatabase performance. 2026-01-29 20:37:45 +09:00
e71851550b Update asset database 2026-01-29 14:03:24 +09:00
8a5795069f Update AssetDatabase 2026-01-27 14:39:00 +09:00
b505c7c1c0 backup 2026-01-26 22:55:14 +09:00
8d82c0a750 Update plan 2026-01-26 15:58:19 +09:00
8df0b46960 Update scene graph 2026-01-26 13:59:33 +09:00
06a150b899 Fix error 2026-01-26 11:47:00 +09:00
49f54c6b43 Add scene graph draft 2026-01-25 22:06:58 +09:00
fdf831630b feat: implement complete scene graph system with hierarchical editor support
- Add SceneNode and EntityNode classes for editor-only metadata storage
- Implement SceneGraph view-model with O(1) entity lookup via internal caching
- Create IdRemapTable for file-local to global entity ID remapping on load
- Implement SceneSerializationContext for load/save operation tracking
- Add JSON-serializable SceneAssetData, EntityData, and ComponentData models
- Implement SceneSerializer for save/load with validation and reference remapping
- Add comprehensive documentation: README.md, IMPLEMENTATION_GUIDE.md, SYSTEM_SUMMARY.md
- Update Ghost.Editor.Core.csproj to reference Ghost.Entities assembly
- Support parent-child relationships via Hierarchy component
- Enforce no cross-scene entity references
- Keep runtime minimal: only SceneID, Hierarchy, LocalToWorld components
- All editor metadata (names, UI state) stored in editor-only SceneNode/EntityNode classes

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

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

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

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

No breaking changes to the existing engine; all new code is isolated. This serves as a reference implementation for high-performance, extensible material/shader architectures.
2025-12-26 19:19:30 +09:00
a89719bfc9 Refactor pipeline state and render output abstractions
- Replace old pipeline enums/structs with new strongly-typed PipelineState and enums (ZTest, ZWrite, Cull, Blend, ColorWriteMask)
- Redesign pipeline keying: introduce 128-bit GraphicsPipelineKey, MaterialPipelineKey, and PassPipelineKey for robust PSO caching
- Replace IRenderTargetStrategy with IRenderOutput; add SwapChainRenderOutput and TextureRenderOutput
- Update renderer and window code to use new render output abstraction and handle viewport/scissor updates
- Make ShaderPass a readonly struct and Shader a struct; use ID-based pass lookup for efficiency
- Materials now support per-pass pipeline overrides with new keying
- Add defensive checks in D3D12CommandBuffer; update D3D12PipelineLibrary for new keying/state
- Move test shader to test.gsdef and update for new pipeline state syntax
- Remove obsolete files/interfaces and perform general code cleanups
- Update all usages and parsing logic for new pipeline state system
2025-12-24 19:06:34 +09:00
b8ce824292 Updated Handle and Identifier that default is invalid 2025-12-23 14:22:44 +09:00
aa3d9c749b Refactor: add command allocator & render target strategies
Major refactor of graphics infrastructure:
- Introduce ICommandAllocator and D3D12CommandAllocator for explicit command buffer management.
- Change ICommandBuffer.Begin to require an allocator.
- Add IRenderTargetStrategy abstraction with swap chain and texture implementations.
- Update IRenderer to use RenderTargetStrategy instead of direct handle.
- Add DPI scaling support to swap chains (ScaleX/ScaleY, SetScale).
- RenderSystem now supports thread-safe swap chain resize requests.
- Remove persistent copy command buffer; use per-frame allocators.
- Make Logger public/static and clean up API visibility.
- Update .editorconfig and debug layer enablement.
These changes improve modularity, DPI-awareness, and future extensibility.
2025-12-23 00:35:34 +09:00
d23e701f0a refactor IRenderer 2025-12-22 15:59:02 +09:00
2881fda112 Refactor component registration, update deps, improve JSON
- Updated Misaki.HighPerformance package versions in Core and Graphics projects.
- Added IsTrimmable to Ghost.Engine.csproj for trimming support.
- Renamed GetOrRegisterComponent to GetOrRegisterComponentID and updated all usages.
- Component registration codegen now uses a static class with [ModuleInitializer], no longer requires [EngineEntry].
- Improved JSON serialization: added string support, introduced Utf8JsonObjectScope/ArrayScope, and new extension methods for cleaner JSON writing.
- Removed [SkipLocalsInit] from Hierarchy and LocalToWorld.
- Fixed Entity.Invalid to use INVALID_ID for both fields.
- Minor cleanup: clarified comments, reorganized Ghost.Generator in solution, and disabled component serialization generator.
2025-12-21 22:18:25 +09:00
840cf7dd5a Add entities SerializationTest 2025-12-21 13:07:59 +09:00
00b4e82ded ECS refactor: new ComponentSet, serialization, generators
Major ECS API overhaul: added ComponentSet, refactored ComponentRegistry, and updated all entity/component creation methods. Introduced robust custom serialization infrastructure and per-component source generators for registration and (de)serialization. Updated editor, engine, and test code to use new APIs. Improved code quality, naming, and performance throughout. Removed obsolete code and updated dependencies.
2025-12-20 20:41:40 +09:00
3118021272 Merge pull request 'feature/archetype-ecs' (#1) from feature/archetype-ecs into develop
Reviewed-on: Misaki/GhostEngine#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
788 changed files with 88138 additions and 176 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.dll filter=lfs diff=lfs merge=lfs -text

2
.gitignore vendored
View File

@@ -9,6 +9,7 @@
*.user *.user
*.userosscache *.userosscache
*.sln.docstates *.sln.docstates
AGENTS.md
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs *.userprefs
@@ -35,6 +36,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/

212
AGENT.md Normal file
View File

@@ -0,0 +1,212 @@
# GhostEngine — Agent Guidelines
## Repository Overview
GhostEngine is a C# game engine targeting .NET 10 / Windows, built around:
- **ECS runtime** (`Ghost.Entities`, `Ghost.Core`) — high-performance, AOT-compatible
- **Graphics** (`Ghost.Graphics`, `Ghost.Graphics.RHI`, `Ghost.Graphics.D3D12`) — D3D12 RHI
- **Editor** (`Ghost.Editor`, `Ghost.Editor.Core`, `Ghost.DSL`, `Ghost.Data`) — WinUI 3 (WindowsAppSDK)
- **Third-party bindings** (`Ghost.FMOD`, `Ghost.MeshOptimizer`, `Ghost.Nvtt`, `Ghost.Ufbx`)
- **Tools** (`Ghost.NativeWrapperGen`)
Solution file: `src/GhostEngine.slnx`
All commands below should be run from the `src/` directory unless noted.
---
## Build Commands
```shell
# Build entire solution (x64, Debug)
dotnet build GhostEngine.slnx -c Debug -p:Platform=x64
# Build entire solution (Release)
dotnet build GhostEngine.slnx -c Release -p:Platform=x64
# Build a single project
dotnet build Runtime/Ghost.Entities/Ghost.Entities.csproj
# Clean
dotnet clean GhostEngine.slnx
```
> **Note:** Editor projects (`Ghost.Editor`, `Ghost.Editor.Core`) require
> `net10.0-windows10.0.22621.0` and the Windows App SDK. They will only build
> on a Windows machine with the correct SDK installed. Platform-agnostic runtime
> and test projects target plain `net10.0`.
---
## Test Commands
There are two test frameworks in use:
### MSTest — `Ghost.UnitTest`
Standard `dotnet test` runner. Tests are parallelized at method level by default.
Currently the integration test file is `#if false`-guarded until the asset
service is fully wired.
```shell
# Run all MSTest tests
dotnet test Test/Ghost.UnitTest/Ghost.UnitTest.csproj -c Debug -p:Platform=x64
# Run a single test method by name
dotnet test Test/Ghost.UnitTest/Ghost.UnitTest.csproj \
--filter "FullyQualifiedName~TestAutoMetaGeneration_WhenFileCreated"
# Run a single test class
dotnet test Test/Ghost.UnitTest/Ghost.UnitTest.csproj \
--filter "ClassName~AssetDatabaseIntegrationTest"
```
### Custom TestRunner — `Ghost.MicroTest` / `Ghost.Entities.Test`
These are console executables driven by `Ghost.Test.Core.TestRunner`. There is
no `dotnet test` integration; run them directly:
```shell
# Micro tests (native binding smoke tests)
dotnet run --project Test/Ghost.MicroTest/Ghost.MicroTest.csproj
# ECS benchmarks / manual tests
dotnet run --project Test/Ghost.Entities.Test/Ghost.Entities.Test.csproj -c Release
```
To run a specific `ITest` implementation, edit `Program.cs` in the respective
project and call `TestRunner.Run<YourTestClass>()`.
---
## Code Style
### EditorConfig (enforced — `src/.editorconfig`)
- Max line length: **200**
- Opening braces always on a **new line** for all C# constructs
- Single-line statements and blocks are **preserved** (not force-expanded)
- **No** primary constructors (`csharp_style_prefer_primary_constructors = false`)
- `System.*` using directives are **not** sorted first
- Import directive groups are **not** separated by blank lines
- Collection expressions and collection initializer syntax are **disabled**
(`dotnet_style_prefer_collection_expression = false`)
### Language
- C# `latest` (runtime/test projects) or `preview` (editor projects, for `field`
keyword support in .NET 10)
- Nullable reference types: **enabled** everywhere (`<Nullable>enable</Nullable>`)
- Implicit usings: **enabled** (`<ImplicitUsings>enable</ImplicitUsings>`)
- Unsafe blocks: **enabled** where needed (ECS, graphics, native bindings)
### Namespaces & File Layout
- One type per file; file name matches type name exactly
- Namespace matches folder structure: `Ghost.<Module>[.<SubFolder>]`
- `partial` classes are split across files named `TypeName.Purpose.cs`
(e.g. `EntityManager.cs`, `EntityManager.Managed.cs`)
- `AssemblyInfo.cs` holds `[assembly: InternalsVisibleTo(...)]` and assembly
attributes; do not scatter these across regular source files
### Naming Conventions
| Symbol | Convention | Example |
|--------|-----------|---------|
| Private fields | `_camelCase` | `_jobScheduler` |
| Private static fields | `s_camelCase` | `s_worlds`, `s_logger` |
| Constants (public/private) | `UPPER_SNAKE_CASE` | `ASSET_EXTENSION`, `ASSETS_FOLDER_NAME` |
| Properties & public members | `PascalCase` | `EntityManager`, `IsSuccess` |
| Local variables / params | `camelCase` | `entityCapacity`, `signatureHash` |
| Interfaces | `I` prefix | `IComponent`, `ISystem`, `ITest` |
| Generic type parameters | `T`, `TKey`, `TValue`, `E` | |
| Type-tagged structs (handles) | Generic param encodes context | `Handle<T>`, `Identifier<T>`, `Key64<T>` |
### Types & Structs
- Prefer `readonly struct` for value types that are logically immutable.
- Prefer `ref struct` / `readonly ref struct` for stack-only types
(`RefResult<T,E>`, `SystemAPI`, `ChunkView`).
- Use `partial class` to split large classes by concern.
- Avoid primary constructors (disabled by editorconfig).
- Use the `field` keyword (preview feature) for auto-property backing fields
where it simplifies code — only in editor projects that opt in via
`<langversion>preview</langversion>`.
### Imports
- `using` directives at the top of each file, before the `namespace` declaration.
- No blank line between `using` groups (enforced by editorconfig).
- `System.*` namespaces may appear in any order alongside project namespaces.
- Prefer specific `using` imports over global usings for clarity in low-level
performance-critical files.
### Error Handling
- **Return `Result` / `Result<T>` instead of throwing** for expected failures
(file-not-found, invalid args, etc.).
`Result.Success()` / `Result.Failure(message)` or `Result.Failure(Error.XXX)`.
- Use the typed `Error` enum (`Ghost.Core.Error`) for structured error codes.
- Use `result.ThrowIfFailed()` / `result.GetValueOrThrow()` extension methods at
call sites that want throw-on-failure semantics.
- **Throw exceptions** only for programming errors / invariant violations
(corrupt state, null-ref on internal APIs).
- In performance-critical paths, guard validation behind `#if DEBUG || GHOST_EDITOR`
to eliminate overhead in release builds.
- `Logger.LogError(...)` / `Logger.LogWarning(...)` for non-fatal operational
issues; do not use `Console.WriteLine` in production library code.
### Performance Patterns
- Annotate hot paths with `[MethodImpl(MethodImplOptions.AggressiveInlining)]`.
- Annotate log/assert helpers with `[StackTraceHidden]`.
- Prefer `stackalloc` + `Span<T>` over heap allocation for small temporary arrays.
- Use the `Misaki.HighPerformance.*` allocation APIs (`AllocationManager`,
`UnsafeList<T>`, `UnsafeHashMap<T,V>`, etc.) for long-lived unmanaged buffers.
- All runtime/ECS types must be AOT-compatible and trimmable (set
`<IsAotCompatible>True</IsAotCompatible>` and `<IsTrimmable>True</IsTrimmable>`
in Release config).
- Avoid LINQ in hot paths; use `for` loops or `foreach` over `Span<T>`.
### Attributes & Extensibility
- Custom attributes for editor extension points inherit from
`DiscoverableAttributeBase` (discovered at startup via `TypeCache`).
- Use `[UpdateAfter(typeof(X))]` / `[UpdateBefore(typeof(X))]` to declare
`ISystem` ordering dependencies; `SystemGroup.SortSystems()` topologically sorts
them at startup.
- Use `[EditorInjection(ServiceLifetime.Singleton)]` to register editor services
via DI without manual wiring.
### XML Documentation
- All public API surface should have `<summary>` doc-comments.
- Use `<remarks>` for non-obvious behavior or threading constraints.
- Document thread-safety expectations explicitly (see `EntityCommandBuffer` /
`AssetRegistry` as reference).
### Preprocessor Defines
| Define | Meaning |
|--------|---------|
| `DEBUG` | Standard debug build |
| `GHOST_EDITOR` | Editor build (extra validation, reflection helpers) |
| `PLATEFORME_WIN64` | Windows 64-bit platform target |
---
## Project Structure
```
src/
GhostEngine.slnx # Solution
.editorconfig # Formatting rules
Runtime/
Ghost.Core/ # Core types: Result, Handle, Logger, math helpers
Ghost.Engine/ # Engine entry point & loop
Ghost.Entities/ # ECS: World, Entity, Component, System
Ghost.Generator/ # Source generators
Ghost.Graphics/ # High-level graphics API
Ghost.Graphics.RHI/ # Render hardware interface abstractions
Ghost.Graphics.D3D12/ # D3D12 backend
Editor/
Ghost.Editor/ # WinUI 3 shell
Ghost.Editor.Core/ # Editor services, asset registry, inspector
Ghost.DSL/ # Shader DSL compiler
Ghost.Data/ # Serialization / project data models
ThridParty/ # Native binding wrappers (FMOD, MeshOptimizer, Nvtt, Ufbx)
Test/
Ghost.Test.Core/ # Shared ITest / TestRunner infrastructure
Ghost.UnitTest/ # MSTest integration tests
Ghost.MicroTest/ # Native binding smoke tests (console app)
Ghost.Entities.Test/ # ECS benchmarks (BenchmarkDotNet, console app)
Ghost.Shader.Test/ # Shader DSL manual tests (console app)
Tools/
Ghost.NativeWrapperGen/ # Code-gen tool for native wrappers
```

View File

@@ -1,34 +0,0 @@
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Ghost.Editor
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : Application
{
/// <summary>
/// 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().
/// </summary>
public App()
{
InitializeComponent();
}
/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
m_window = new MainWindow();
m_window.Activate();
}
private Window? m_window;
}
}

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class="Ghost.Editor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="Ghost.Editor">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
</StackPanel>
</Window>

View File

@@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Ghost.Editor
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
private void myButton_Click(object sender, RoutedEventArgs e)
{
myButton.Content = "Clicked";
}
}
}

View File

@@ -1,6 +0,0 @@
namespace Ghost.Engine;
public class Class1
{
}

View File

@@ -1,57 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.35906.104
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ghost.Editor", "Ghost.Editor\Ghost.Editor.csproj", "{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ghost.Engine", "Ghost.Engine\Ghost.Engine.csproj", "{1ED62E09-8F36-4671-896B-16C1C1530202}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Debug|ARM64.ActiveCfg = Debug|ARM64
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Debug|ARM64.Build.0 = Debug|ARM64
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Debug|ARM64.Deploy.0 = Debug|ARM64
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Debug|x64.ActiveCfg = Debug|x64
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Debug|x64.Build.0 = Debug|x64
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Debug|x64.Deploy.0 = Debug|x64
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Debug|x86.ActiveCfg = Debug|x86
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Debug|x86.Build.0 = Debug|x86
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Debug|x86.Deploy.0 = Debug|x86
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Release|ARM64.ActiveCfg = Release|ARM64
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Release|ARM64.Build.0 = Release|ARM64
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Release|ARM64.Deploy.0 = Release|ARM64
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Release|x64.ActiveCfg = Release|x64
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Release|x64.Build.0 = Release|x64
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Release|x64.Deploy.0 = Release|x64
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Release|x86.ActiveCfg = Release|x86
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Release|x86.Build.0 = Release|x86
{15AFE3A1-0CAF-4B36-8835-121C4D683BBF}.Release|x86.Deploy.0 = Release|x86
{1ED62E09-8F36-4671-896B-16C1C1530202}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{1ED62E09-8F36-4671-896B-16C1C1530202}.Debug|ARM64.Build.0 = Debug|Any CPU
{1ED62E09-8F36-4671-896B-16C1C1530202}.Debug|x64.ActiveCfg = Debug|Any CPU
{1ED62E09-8F36-4671-896B-16C1C1530202}.Debug|x64.Build.0 = Debug|Any CPU
{1ED62E09-8F36-4671-896B-16C1C1530202}.Debug|x86.ActiveCfg = Debug|Any CPU
{1ED62E09-8F36-4671-896B-16C1C1530202}.Debug|x86.Build.0 = Debug|Any CPU
{1ED62E09-8F36-4671-896B-16C1C1530202}.Release|ARM64.ActiveCfg = Release|Any CPU
{1ED62E09-8F36-4671-896B-16C1C1530202}.Release|ARM64.Build.0 = Release|Any CPU
{1ED62E09-8F36-4671-896B-16C1C1530202}.Release|x64.ActiveCfg = Release|Any CPU
{1ED62E09-8F36-4671-896B-16C1C1530202}.Release|x64.Build.0 = Release|Any CPU
{1ED62E09-8F36-4671-896B-16C1C1530202}.Release|x86.ActiveCfg = Release|Any CPU
{1ED62E09-8F36-4671-896B-16C1C1530202}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0C545827-2ED7-4597-BE3C-30E978C85B9E}
EndGlobalSection
EndGlobal

19
README_julian.md Normal file
View File

@@ -0,0 +1,19 @@
# Julian's Workspace: GhostEngine
Misaki has set up my environment to work from my own fork of "GhostEngine." Here's how I'll manage my workflow:
## What I'll Do
1. Write, commit, and push code changes directly to my local fork.
2. Use the "tea" CLI tool when I'm ready to create PRs back to the original repository.
## Tools & Resources
- **Gitea:** The repository is hosted here. I can authenticate directly.
- **Tea CLI:** Details for creating PRs can be found in the `gitea-pr` and `gitea` skill documentation.
## Collaboration Rules
We'll refine separately; I'll merge when...
...the functionality is complete and tested.
**PR Description Tip:** Include concise changelog markdown.

1125
doc/meshlet-architecture.md Normal file

File diff suppressed because it is too large Load Diff

13
src/.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
[*]
max_line_length = 200
[*.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

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="../../Runtime/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,326 @@
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 int LayoutCBufferProperties(Span<PropertyDescriptor> properties)
{
if (properties.IsEmpty)
{
return 0;
}
var currentOffset = 0;
foreach (ref var prop in properties)
{
var size = prop.type.GetSize();
if ((currentOffset % 16) + size > 16)
{
currentOffset = (currentOffset + 15) & ~15;
}
prop.offset = currentOffset;
prop.size = size;
currentOffset += size;
}
return (currentOffset + 15) & ~15;
}
// 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 = LayoutCBufferProperties(descriptor.properties);
if (semantics.passes != null)
{
descriptor.passes = new PassDescriptor[semantics.passes.Count];
for (var 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/src/Runtime//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/src/Runtime//Ghost.Graphics/Shaders/Includes/Common.hlsl""
struct GlobalData
{");
foreach (var prop in globalProperties)
{
sb.Append($@"
{ShaderPropertyTypeToHLSLType(prop.type)} {prop.name};");
}
sb.AppendLine(@"
};
#endif // GLOBALDATA_G_HLSL");
globalFileStream.Write(sb.ToString());
return globalFilePath;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,179 @@
using Ghost.Editor.Core.Contracts;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.AssetHandler;
public abstract class Asset
{
public Guid ID
{
get;
}
public abstract Guid TypeID
{
get;
}
public Guid[] Dependencies
{
get;
}
public IAssetSettings? Settings
{
get;
}
protected Asset(Guid id, Guid[] dependencies, IAssetSettings? settings)
{
ID = id;
Dependencies = dependencies;
Settings = settings;
}
public virtual ValueTask RefreshAsync(IAssetRegistry db, CancellationToken token = default)
{
return ValueTask.CompletedTask;
}
}
// Do not change the order of the fields in this struct, as it is used for binary serialization/deserialization.
[StructLayout(LayoutKind.Sequential, Size = SIZE)]
internal struct AssetMetadata
{
public const int CURRENT_FORMAT_VERSION = 1;
public const int SIZE = 128; // Fixed size for metadata header. We choose 128 bytes to allow future expansion without breaking compatibility.
public AssetMetadata(Guid id, Guid typeID)
{
FormatVersion = CURRENT_FORMAT_VERSION;
ID = id;
TypeID = typeID;
}
public int FormatVersion
{
get;
}
public Guid ID
{
get;
}
public Guid TypeID
{
get;
}
public int HandlerVersion
{
get; set;
}
public int DependencyCount
{
get; set;
}
public long DependenciesOffset
{
get; set;
}
public long SettingsOffset
{
get; set;
}
public long SettingsSize
{
get; set;
}
public long ContentOffset
{
get; set;
}
public long ContentSize
{
get; set;
}
public static void WriteToStream(Stream stream, scoped ref readonly AssetMetadata metadata)
{
var buffer = MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in metadata, 1));
stream.Write(buffer);
}
public static AssetMetadata ReadFromStream(Stream stream)
{
Span<byte> buffer = stackalloc byte[SIZE];
stream.ReadExactly(buffer);
return Unsafe.ReadUnaligned<AssetMetadata>(ref MemoryMarshal.GetReference(buffer));
}
}
[StructLayout(LayoutKind.Sequential, Size = SIZE)]
public readonly struct DependencyInfo
{
public const int SIZE = 16;
public Guid ID
{
get; init;
}
public readonly ReadOnlySpan<byte> AsBytes()
{
return MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1));
}
}
public readonly struct AssetReference : IEquatable<AssetReference>
{
private readonly int _value;
/// <summary>
/// The index of the asset in the dependency list.
/// </summary>
public int Index
{
get => Math.Abs(_value) - 1;
}
public static AssetReference Null => default;
public readonly bool IsInternal => _value >= 0;
public readonly bool IsExternal => _value < 0;
public bool Equals(AssetReference other)
{
return _value == other._value;
}
public override int GetHashCode()
{
return _value.GetHashCode();
}
public override bool Equals(object? obj)
{
return obj is AssetReference reference && Equals(reference);
}
public static bool operator ==(AssetReference left, AssetReference right)
{
return left.Equals(right);
}
public static bool operator !=(AssetReference left, AssetReference right)
{
return !(left == right);
}
}
public interface IAssetSettings;

View File

@@ -0,0 +1,60 @@
using Ghost.Core;
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.AssetHandler;
[AttributeUsage(AttributeTargets.Class)]
public sealed class CustomAssetHandlerAttribute : Attribute
{
public required string ID
{
get; init;
}
public required string[] SupportedExtensions
{
get; init;
}
public bool AllowCaching
{
get; init;
} = true;
}
public interface IAssetExportOptions;
public interface IAssetHandler
{
ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default);
ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default);
}
public interface IImportableAssetHandler : IAssetHandler
{
ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, CancellationToken token = default);
ValueTask<Result> ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default);
}
public static class AssetHandlerExtensions
{
public static async ValueTask<Result> ImportAsync(this IImportableAssetHandler handler, string sourceFilePath, string targetFilePath, Guid id, CancellationToken token = default)
{
await using var sourceStream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
await using var targetStream = new FileStream(targetFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
return await handler.ImportAsync(sourceStream, targetStream, id, token);
}
public static async ValueTask<Result> ExportAsync(this IImportableAssetHandler handler, string assetFilePath, string targetFilePath, IAssetExportOptions? options, CancellationToken token = default)
{
await using var assetStream = new FileStream(assetFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
await using var targetStream = new FileStream(targetFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
return await handler.ExportAsync(assetStream, targetStream, options, token);
}
public static async ValueTask<Result<Asset>> LoadAsync(this IAssetHandler handler, string assetFilePath, IAssetRegistry assetDatabase, CancellationToken token = default)
{
await using var sourceStream = new FileStream(assetFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
return await handler.LoadAsync(sourceStream, assetDatabase, token);
}
}

View File

@@ -0,0 +1,37 @@
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.AssetHandler;
[AttributeUsage(AttributeTargets.Class)]
public sealed class CustomAssetProcesserAttribute<T> : Attribute
{
public Type Type => typeof(T);
}
public readonly struct AssetProcesserContext
{
public IAssetRegistry Registry
{
get; init;
}
public string AssetPath
{
get; init;
}
public Asset Asset
{
get; init;
}
public IAssetHandler Handler
{
get; init;
}
}
public interface IAssetProcesser
{
ValueTask ProcessAsync(AssetProcesserContext ctx);
}

View File

@@ -0,0 +1,397 @@
using Ghost.Core;
using Ghost.Editor.Core.Contracts;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Image;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static Ghost.Editor.Core.AssetHandler.TextureAssetSettings;
namespace Ghost.Editor.Core.AssetHandler;
public enum TextureType : uint
{
Default,
Normal,
Lightmap,
SingleChannel
}
public enum TextureShape : uint
{
Texture2D,
Texture3D,
TextureCube
}
public enum TextureSize : uint
{
Size256 = 256,
Size512 = 512,
Size1024 = 1024,
Size2048 = 2048,
Size4096 = 4096,
Size8192 = 8192
}
public enum TextureCompressionLevel : uint
{
Low,
Normal,
High
}
public enum MipmapFilter : uint
{
Box,
Triangle,
Kaiser,
MitchellNetravali
}
public class TextureAsset : Asset
{
internal const string _TYPE_ID = "0906F4EB-C3F0-431B-BCEA-132C88AB0C3F";
internal static readonly Guid s_typeGuid = Guid.Parse(_TYPE_ID);
private readonly Handle<Texture> _texture;
public override Guid TypeID => s_typeGuid;
public Handle<Texture> Texture => _texture;
public TextureAsset(Guid id, Guid[] dependencies, IAssetSettings? settings, Handle<Texture> texture)
: base(id, dependencies, settings)
{
_texture = texture;
}
}
public class TextureAssetSettings : IAssetSettings
{
public struct BasicSettings()
{
public TextureType TextureType
{
get; set;
} = TextureType.Default;
public TextureShape TextureShape
{
get; set;
} = TextureShape.Texture2D;
public int Columns
{
get; set;
} = 1;
public int Rows
{
get; set;
} = 1;
public bool IsSRGB
{
get; set;
} = true;
}
public struct AdvancedSettings()
{
public bool StretchToPowerOfTwo
{
get; set;
} = true;
public bool VirtualTexture
{
get; set;
} = false;
public bool GenerateMipmaps
{
get; set;
} = true;
public uint MipmapLevelCount
{
get; set;
} = 0; // 0 means generate full mipmap levels.
public bool GammaCorrection
{
get; set;
} = true;
public bool PremultiplyAlpha
{
get; set;
} = false;
public MipmapFilter MipmapFilter
{
get; set;
} = MipmapFilter.Kaiser;
public TextureCompressionLevel CompressionLevel
{
get; set;
} = TextureCompressionLevel.Normal;
public bool UseBorderColor
{
get; set;
} = false;
public Color128 BorderColor
{
get; set;
} = new Color128(0, 0, 0, 0);
public bool ZeroAlphaBorder
{
get; set;
} = false;
public bool CutoutAlpha
{
get; set;
} = false;
public byte CutoutAlphaThreshold
{
get; set;
} = 127;
public bool ScaleAlphaForMipCoverage
{
get; set;
} = false;
public byte ScaleAlphaForMipCoverageThreshold
{
get; set;
} = 127;
public bool MipmapStreaming
{
get; set;
} = false;
}
public struct SamplerSettings()
{
public TextureSize MaxSize
{
get; set;
} = TextureSize.Size2048;
public TextureFilterMode FilterMode
{
get; set;
} = TextureFilterMode.Anisotropic;
public TextureAddressMode WrapMode
{
get; set;
} = TextureAddressMode.Repeat;
}
public BasicSettings Basic
{
get; set;
} = new BasicSettings();
public AdvancedSettings Advanced
{
get; set;
} = new AdvancedSettings();
public SamplerSettings Sampler
{
get; set;
} = new SamplerSettings();
}
[CustomAssetHandler(ID = TextureAsset._TYPE_ID, SupportedExtensions = new[] { ".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr" })]
internal class TextureAssetHandler : IImportableAssetHandler
{
private const int _CURRENT_VERSION = 1;
private static async ValueTask<Result<long>> WriteSettingsToStreamAsync(TextureAssetSettings settings, Stream stream, CancellationToken token = default)
{
var size = Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>() + Unsafe.SizeOf<SamplerSettings>();
var tempArray = ArrayPool<byte>.Shared.Rent(size);
try
{
ref var address = ref MemoryMarshal.GetReference(tempArray);
Unsafe.WriteUnaligned(ref address, settings.Basic);
Unsafe.WriteUnaligned(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>()), settings.Advanced);
Unsafe.WriteUnaligned(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>()), settings.Sampler);
await stream.WriteAsync(tempArray.AsMemory(0, size), token).ConfigureAwait(false);
return Result.Success<long>(size);
}
catch (Exception ex)
{
return Result.Failure($"Failed to write texture asset settings to stream: {ex.Message}");
}
finally
{
ArrayPool<byte>.Shared.Return(tempArray);
}
}
private static async ValueTask<Result<IAssetSettings>> ReadSettingsFromStreamAsync(Stream stream, CancellationToken token = default)
{
var size = Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>() + Unsafe.SizeOf<SamplerSettings>();
var tempArray = ArrayPool<byte>.Shared.Rent(size);
try
{
await stream.ReadAsync(tempArray.AsMemory(0, size), token).ConfigureAwait(false);
// Use index-based reads after the await to avoid 'ref across await' errors.
var basic = Unsafe.ReadUnaligned<BasicSettings>(ref tempArray[0]);
var advanced = Unsafe.ReadUnaligned<AdvancedSettings>(ref tempArray[Unsafe.SizeOf<BasicSettings>()]);
var sampler = Unsafe.ReadUnaligned<SamplerSettings>(ref tempArray[Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>()]);
var settings = new TextureAssetSettings
{
Basic = basic,
Advanced = advanced,
Sampler = sampler
};
return Result.Success<IAssetSettings>(settings);
}
catch (Exception ex)
{
return Result.Failure($"Failed to read texture asset settings from stream: {ex.Message}");
}
finally
{
ArrayPool<byte>.Shared.Return(tempArray);
}
}
public ValueTask<Result> ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default)
{
throw new NotImplementedException();
}
public async ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, CancellationToken token = default)
{
var info = ImageInfo.FromStream(sourceStream);
if (info.BitsPerChannel <= 0)
{
return Result.Failure($"Unsupported image format with {info.BitsPerChannel} bits per channel.");
}
var isFloat = info.BitsPerChannel > 8;
var width = info.Width;
var height = info.Height;
var colorComponents = info.ColorComponents;
byte[] pixelBytes;
if (isFloat)
{
using var image = ImageResultFloat.FromStream(sourceStream, colorComponents);
var span = MemoryMarshal.AsBytes(image.AsSpan());
pixelBytes = ArrayPool<byte>.Shared.Rent(span.Length);
span.CopyTo(pixelBytes);
}
else
{
using var image = ImageResult.FromStream(sourceStream, colorComponents);
var span = image.AsSpan();
pixelBytes = ArrayPool<byte>.Shared.Rent(span.Length);
span.CopyTo(pixelBytes);
}
try
{
var settings = new TextureAssetSettings();
await Task.Run(() =>
TextureProcessor.CompressToCache(
EditorApplication.CachesFolderPath,
id,
pixelBytes,
width,
height,
isFloat,
colorComponents,
settings),
token).ConfigureAwait(false);
var header = new AssetMetadata(id, TextureAsset.s_typeGuid)
{
HandlerVersion = _CURRENT_VERSION,
SettingsOffset = AssetMetadata.SIZE,
};
targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin);
var sizeResult = await WriteSettingsToStreamAsync(settings, targetStream, token).ConfigureAwait(false);
if (sizeResult.IsFailure)
{
return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}");
}
// Content layout (all little-endian):
// int32 width
// int32 height
// byte isFloat (0 = byte, 1 = float)
// int32 colorComponents (cast of ColorComponents enum)
// byte[] pixelBytes
const int _CONTENT_HEADER_SIZE = 4 + 4 + 1 + 4; // 13 bytes
header.SettingsSize = sizeResult.Value;
header.ContentOffset = header.SettingsOffset + sizeResult.Value;
header.ContentSize = _CONTENT_HEADER_SIZE + pixelBytes.Length;
// Write raw image content
targetStream.Seek(header.ContentOffset, SeekOrigin.Begin);
var contentHeader = ArrayPool<byte>.Shared.Rent(_CONTENT_HEADER_SIZE);
try
{
BitConverter.TryWriteBytes(contentHeader.AsSpan(0, 4), width);
BitConverter.TryWriteBytes(contentHeader.AsSpan(4, 4), height);
contentHeader[8] = isFloat ? (byte)1 : (byte)0;
BitConverter.TryWriteBytes(contentHeader.AsSpan(9, 4), (int)colorComponents);
await targetStream.WriteAsync(contentHeader.AsMemory(0, _CONTENT_HEADER_SIZE), token).ConfigureAwait(false);
}
finally
{
ArrayPool<byte>.Shared.Return(contentHeader);
}
await targetStream.WriteAsync(pixelBytes, token).ConfigureAwait(false);
await targetStream.FlushAsync(token).ConfigureAwait(false);
// Patch header now that all sizes are known
targetStream.Seek(0, SeekOrigin.Begin);
AssetMetadata.WriteToStream(targetStream, ref header);
return Result.Success();
}
finally
{
ArrayPool<byte>.Shared.Return(pixelBytes);
}
}
public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default)
{
throw new NotImplementedException();
}
public ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,228 @@
using Ghost.Nvtt;
using Misaki.HighPerformance.Image;
using Misaki.HighPerformance.LowLevel;
using System.IO.Hashing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace Ghost.Editor.Core.AssetHandler;
/// <summary>
/// Drives the NVTT compression + mipmap pipeline for a single texture asset.
///
/// Responsibilities:
/// 1. Accept raw decoded pixel bytes + settings.
/// 2. Determine the cache file path (<c>CachesFolderPath/TextureCache/&lt;guid&gt;_&lt;hash&gt;.dds</c>).
/// 3. If the cache is already valid (hash matches), skip compression.
/// 4. Otherwise run the full NVTT pipeline and write the DDS to the cache file.
///
/// The caller owns opening/closing all streams; this class only takes spans and paths.
/// </summary>
internal static unsafe class TextureProcessor
{
private const string _TEXTURE_CACHE_SUBFOLDER = "TextureCache";
/// <summary>
/// Compresses <paramref name="pixelData"/> according to <paramref name="settings"/>
/// and writes the result to the texture cache.
///
/// Returns the absolute path of the cache file on success.
/// The cache file is skipped if it already exists with a matching content hash.
/// </summary>
public static string CompressToCache(
string cachesFolderPath,
Guid assetId,
ReadOnlySpan<byte> pixelData,
int width,
int height,
bool isFloat,
ColorComponents colorComponents,
TextureAssetSettings settings)
{
var cacheDir = Path.Combine(cachesFolderPath, _TEXTURE_CACHE_SUBFOLDER);
Directory.CreateDirectory(cacheDir);
var settingsHash = ComputeSettingsHash(settings);
var cacheFileName = $"{assetId:N}_{settingsHash:X16}.dds";
var cachePath = Path.Combine(cacheDir, cacheFileName);
if (File.Exists(cachePath))
{
return cachePath;
}
foreach (var stale in Directory.EnumerateFiles(cacheDir, $"{assetId:N}_*.dds"))
{
File.Delete(stale);
}
RunNvttPipeline(cachePath, pixelData, width, height, isFloat, colorComponents, settings);
return cachePath;
}
private static void RunNvttPipeline(
string outputPath,
ReadOnlySpan<byte> pixelData,
int width,
int height,
bool isFloat,
ColorComponents colorComponents,
TextureAssetSettings settings)
{
using var pSurface = new DisposablePtr<NvttSurface>(NvttSurface.Create());
using var pCompOpts = new DisposablePtr<NvttCompressionOptions>(NvttCompressionOptions.Create());
using var pOutOpts = new DisposablePtr<NvttOutputOptions>(NvttOutputOptions.Create());
using var pCtx = new DisposablePtr<NvttContext>(NvttContext.Create());
var inputFormat = isFloat
? NvttInputFormat.NVTT_InputFormat_RGBA_32F
: NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below
fixed (void* pData = pixelData)
{
pSurface.Get()->SetImageData(inputFormat, width, height, 1, pData, NvttBoolean.NVTT_True, null);
}
// stb gives us RGBA byte order; NVTT BGRA_8UB reads it as BGRA,
// so channels R and B are swapped — fix with swizzle(2,1,0,3).
if (!isFloat)
{
pSurface.Get()->Swizzle(2, 1, 0, 3, null);
}
var maxExtent = (int)settings.Sampler.MaxSize;
if (settings.Advanced.StretchToPowerOfTwo)
{
pSurface.Get()->ResizeMakeSquare(maxExtent,
NvttRoundMode.NVTT_RoundMode_ToNearestPowerOfTwo,
NvttResizeFilter.NVTT_ResizeFilter_Box, null);
}
else if (pSurface.Get()->Width() > maxExtent || pSurface.Get()->Height() > maxExtent)
{
pSurface.Get()->ResizeMax(maxExtent,
NvttRoundMode.NVTT_RoundMode_None,
NvttResizeFilter.NVTT_ResizeFilter_Box, null);
}
if (settings.Advanced.UseBorderColor)
{
var c = settings.Advanced.BorderColor;
pSurface.Get()->SetBorder(c.r, c.g, c.b, c.a, null);
}
else if (settings.Advanced.ZeroAlphaBorder)
{
pSurface.Get()->SetBorder(0f, 0f, 0f, 0f, null);
}
if (settings.Basic.IsSRGB && settings.Advanced.GammaCorrection)
{
pSurface.Get()->ToLinearFromSrgb(null);
}
if (settings.Advanced.PremultiplyAlpha)
{
pSurface.Get()->PremultiplyAlpha(null);
}
pCompOpts.Get()->SetFormat(SelectFormat(settings));
pCompOpts.Get()->SetQuality(SelectQuality(settings.Advanced.CompressionLevel));
if (settings.Advanced.CutoutAlpha)
{
pCompOpts.Get()->SetQuantization(false, false, true,
settings.Advanced.CutoutAlphaThreshold);
}
pOutOpts.Get()->SetOutputHeader(true);
pOutOpts.Get()->SetSrgbFlag(settings.Basic.IsSRGB);
pOutOpts.Get()->SetContainer(NvttContainer.NVTT_Container_DDS10);
pOutOpts.Get()->SetFileName(Encoding.UTF8.GetBytes(outputPath));
var nvttFilter = SelectMipmapFilter(settings.Advanced.MipmapFilter);
int mipmapCount;
if (!settings.Advanced.GenerateMipmaps)
{
mipmapCount = 1;
}
else if (settings.Advanced.MipmapLevelCount == 0)
{
mipmapCount = pSurface.Get()->CountMipmaps(1);
}
else
{
mipmapCount = (int)settings.Advanced.MipmapLevelCount;
}
pCtx.Get()->SetCudaAcceleration(NvttApi.IsCudaSupported());
pCtx.Get()->OutputHeader(pSurface.Get(), mipmapCount, pCompOpts.Get(), pOutOpts.Get());
using var pMip = new DisposablePtr<NvttSurface>(pSurface.Get()->Clone());
for (var level = 0; level < mipmapCount; level++)
{
// Scale alpha for coverage on each pMip (if requested)
if (settings.Advanced.ScaleAlphaForMipCoverage && level > 0)
{
var refCoverage = pMip.Get()->AlphaTestCoverage(
settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3);
pMip.Get()->ScaleAlphaToCoverage(refCoverage,
settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3, null);
}
pCtx.Get()->Compress(pMip.Get(), 0, level, pCompOpts.Get(), pOutOpts.Get());
if (level + 1 < mipmapCount)
{
pMip.Get()->BuildNextMipmapDefaults(nvttFilter, 1, null);
}
}
}
private static NvttFormat SelectFormat(TextureAssetSettings settings)
=> settings.Basic.TextureType switch
{
TextureType.Normal => NvttFormat.NVTT_Format_BC5, // RG normal map
TextureType.SingleChannel => NvttFormat.NVTT_Format_BC4, // single channel
TextureType.Lightmap => NvttFormat.NVTT_Format_BC6U, // HDR lightmap (unsigned)
_ => NvttFormat.NVTT_Format_BC7, // default color
};
private static NvttQuality SelectQuality(TextureCompressionLevel level)
=> level switch
{
TextureCompressionLevel.Low => NvttQuality.NVTT_Quality_Fastest,
TextureCompressionLevel.High => NvttQuality.NVTT_Quality_Production,
_ => NvttQuality.NVTT_Quality_Normal,
};
private static NvttMipmapFilter SelectMipmapFilter(MipmapFilter filter)
=> filter switch
{
MipmapFilter.Box => NvttMipmapFilter.NVTT_MipmapFilter_Box,
MipmapFilter.Triangle => NvttMipmapFilter.NVTT_MipmapFilter_Triangle,
MipmapFilter.MitchellNetravali => NvttMipmapFilter.NVTT_MipmapFilter_Mitchell,
_ => NvttMipmapFilter.NVTT_MipmapFilter_Kaiser,
};
private static ulong ComputeSettingsHash(TextureAssetSettings s)
{
var basicSize = Unsafe.SizeOf<TextureAssetSettings.BasicSettings>();
var advancedSize = Unsafe.SizeOf<TextureAssetSettings.AdvancedSettings>();
var samplerSize = Unsafe.SizeOf<TextureAssetSettings.SamplerSettings>();
var total = basicSize + advancedSize + samplerSize;
Span<byte> buf = stackalloc byte[total];
var basic = s.Basic;
var advanced = s.Advanced;
var sampler = s.Sampler;
MemoryMarshal.Write(buf, in basic);
MemoryMarshal.Write(buf.Slice(basicSize), in advanced);
MemoryMarshal.Write(buf.Slice(basicSize + advancedSize), in sampler);
return XxHash64.HashToUInt64(buf);
}
}

View File

@@ -0,0 +1,102 @@
namespace Ghost.Editor.Core;
/// <summary>
/// The base class for all attributes that can be discovered via <see cref="Utilities.TypeCache"/>.
/// </summary>
public abstract class DiscoverableAttributeBase : Attribute;
[AttributeUsage(AttributeTargets.Method)]
public class AssetOpenHandlerAttribute : DiscoverableAttributeBase
{
public string[] Extensions
{
get;
}
public AssetOpenHandlerAttribute(params string[] extensions)
{
Extensions = extensions.Select(e => e.StartsWith('.') ? e.ToLowerInvariant() : '.' + e.ToLowerInvariant()).ToArray();
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
internal class AssetImporterAttribute : DiscoverableAttributeBase
{
public string[] SupportedExtensions
{
get;
}
public AssetImporterAttribute(params string[] supportedExtensions)
{
SupportedExtensions = supportedExtensions;
}
}
[AttributeUsage(AttributeTargets.Class)]
public class CustomEditorAttribute : DiscoverableAttributeBase
{
internal Type TargetType
{
get;
}
public CustomEditorAttribute(Type targetType)
{
TargetType = targetType;
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
public class EditorInjectionAttribute : DiscoverableAttributeBase
{
public enum ServiceLifetime
{
Singleton,
Transient,
Scoped
}
public ServiceLifetime Lifetime
{
get;
}
public Type? ImplementationType
{
get;
}
public EditorInjectionAttribute(ServiceLifetime lifetime, Type? implementationType = null)
{
Lifetime = lifetime;
ImplementationType = implementationType;
}
}
[AttributeUsage(AttributeTargets.Method)]
public sealed class ContextMenuItemAttribute : DiscoverableAttributeBase
{
public string Tag
{
get;
}
public string Name
{
get;
}
public int Group
{
get;
}
public ContextMenuItemAttribute(string tag, string name, int group = 0)
{
Tag = tag;
Name = name;
Group = group;
}
}

View File

@@ -0,0 +1,49 @@
using Ghost.Core;
using Ghost.Editor.Core.AssetHandler;
namespace Ghost.Editor.Core.Contracts;
public enum AssetChangeType
{
None = 0,
Created,
Deleted,
Modified,
Renamed,
}
public sealed class AssetChangedEventArgs : EventArgs
{
public string AssetPath
{
get;
}
public string? OldAssetPath
{
get;
}
public AssetChangeType ChangeType
{
get;
}
internal AssetChangedEventArgs(string assetPath, string? oldAssetPath, AssetChangeType changeType)
{
AssetPath = assetPath;
OldAssetPath = oldAssetPath;
ChangeType = changeType;
}
}
public interface IAssetRegistry : IDisposable
{
string? GetAssetPath(Guid id);
Guid GetAssetGuid(string assetPath);
ValueTask<Result<Guid>> ImportAssetAsync(string sourceFilePath, string targetAssetPath, CancellationToken token = default);
ValueTask<Result> ReimportAssetAsync(Guid assetId, string sourceFilePath, CancellationToken token = default);
ValueTask<Result<Asset>> LoadAssetAsync(Guid id, CancellationToken token = default);
ValueTask<Result> SaveAssetAsync(Asset asset, CancellationToken token = default);
}

View File

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

View File

@@ -0,0 +1,32 @@
namespace Ghost.Editor.Core.Contracts;
public class InspectorSelectionChangedEventArgs : EventArgs
{
public object? Source
{
get;
}
public IInspectable? Selected
{
get;
}
public InspectorSelectionChangedEventArgs(object? source, IInspectable? selected)
{
Source = source;
Selected = selected;
}
}
public interface IInspectorService
{
IInspectable? Selected
{
get;
}
event EventHandler<InspectorSelectionChangedEventArgs> OnSelectionChanged;
void SetSelected(IInspectable? inspectable, object? source);
}

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
namespace Ghost.Editor.Core.Contracts;
public enum IconSize
{
Small,
Large
}
public interface IPreviewService
{
string GetIconPath(string path, bool isDirectory, IconSize size);
}

View File

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

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,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/BasicInput/PropertyField.xaml" />
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/BasicInput/Float3Field.xaml" />
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/Internal/ComponentView.xaml" />
</ResourceDictionary.MergedDictionaries>
</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 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,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">
<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,42 @@
using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Controls;
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,214 @@
using Ghost.Editor.Core.Utilities;
using Microsoft.UI.Xaml.Controls;
using System.Reflection;
namespace Ghost.Editor.Core.Controls;
public sealed partial class ContextFlyout : MenuFlyout
{
private class MenuNode
{
public required string Name
{
get; init;
}
public MethodInfo? Method
{
get; set;
}
public List<MenuNode> Children
{
get;
} = new();
public int RawGroup
{
get; set;
} = int.MaxValue;
// The calculated group used for sorting (min of children for folders)
public int EffectiveGroup
{
get; set;
}
}
private bool _isPopulated;
public string Tag
{
get; set;
} = string.Empty;
public ContextFlyout()
{
Opening += ContextFlyout_Opening;
}
// Recursively sorts nodes and calculates folder groups
private static void PrepareNodes(List<MenuNode> nodes)
{
if (nodes.Count == 0)
{
return;
}
foreach (var node in nodes)
{
if (node.Children.Count > 0)
{
// Go deep first
PrepareNodes(node.Children);
// A folder's group is determined by its highest priority child (lowest group number).
// This ensures a "File" folder (containing Group 0 items) sits at the top
// alongside other Group 0 leaf items.
node.EffectiveGroup = node.Children.Min(c => c.EffectiveGroup);
}
else
{
node.EffectiveGroup = node.RawGroup;
}
}
// Sort by Group, then by Name
nodes.Sort((a, b) =>
{
var groupCompare = a.EffectiveGroup.CompareTo(b.EffectiveGroup);
return groupCompare != 0
? groupCompare
: string.CompareOrdinal(a.Name, b.Name);
});
}
// Recursively builds the UI elements
private static void BuildNodes(List<MenuNode> nodes, IList<MenuFlyoutItemBase> targetCollection)
{
if (nodes.Count == 0)
{
return;
}
var currentGroup = nodes[0].EffectiveGroup;
foreach (var node in nodes)
{
if (node.EffectiveGroup != currentGroup)
{
targetCollection.Add(new MenuFlyoutSeparator());
currentGroup = node.EffectiveGroup;
}
if (node.Children.Count > 0)
{
var subItem = new MenuFlyoutSubItem
{
Text = node.Name
};
// Recursively render children into the subitem
BuildNodes(node.Children, subItem.Items);
targetCollection.Add(subItem);
}
else
{
var menuItem = new MenuFlyoutItem
{
Text = node.Name
};
var methodToInvoke = node.Method;
menuItem.Click += (_, _) =>
{
methodToInvoke?.Invoke(null, null);
};
targetCollection.Add(menuItem);
}
}
}
private void PopulateContextMenu()
{
var methods = TypeCache.GetMethodsWithAttribute<ContextMenuItemAttribute>();
if (methods == null)
{
return;
}
// 1. Build the Tree
var rootNodes = new List<MenuNode>();
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<ContextMenuItemAttribute>();
if (attr == null)
{
continue;
}
// Filter tags
if (!string.Equals(attr.Tag, Tag, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var nameSpan = attr.Name.AsSpan();
var pathParts = nameSpan.Split('/');
var currentLevel = rootNodes;
MenuNode? currentNode = null;
foreach (var range in pathParts)
{
var part = nameSpan[range.Start..range.End];
MenuNode? foundNode = null;
// Try to find existing node in the current level
foreach (var node in currentLevel)
{
if (part.Equals(node.Name.AsSpan(), StringComparison.Ordinal))
{
foundNode = node;
break;
}
}
if (foundNode == null)
{
foundNode = new MenuNode { Name = part.ToString() };
currentLevel.Add(foundNode);
}
currentNode = foundNode;
// If this is the last part, it's the executable item
if (range.End.Value == nameSpan.Length)
{
currentNode.Method = method;
currentNode.RawGroup = attr.Group;
}
currentLevel = currentNode.Children;
}
}
PrepareNodes(rootNodes);
BuildNodes(rootNodes, Items);
}
private async void ContextFlyout_Opening(object? sender, object e)
{
if (_isPopulated)
{
return;
}
PopulateContextMenu();
_isPopulated = true;
}
}

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,70 @@
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
namespace Ghost.Editor.Core;
public static class EditorApplication
{
public const string ASSETS_FOLDER_NAME = "Assets";
public const string SOURCES_FOLDER_NAME = "Sources";
public const string PACKAGES_FOLDER_NAME = "Packages";
public const string CACHES_FOLDER_NAME = "Caches";
public const string CONFIG_FOLDER_NAME = "Config";
private static IServiceProvider? s_serviceProvider;
private static string s_currentProjectPath = string.Empty;
private static string s_currentProjectName = string.Empty;
private static DispatcherQueue? s_dispatcherQueue;
internal static Application CurrentApplication => Application.Current;
public static string ProjectPath => s_currentProjectPath;
public static string ProjectName => s_currentProjectName;
public static string AssetsFolderPath => Path.Combine(ProjectPath, ASSETS_FOLDER_NAME);
public static string SourcesFolderPath => Path.Combine(ProjectPath, SOURCES_FOLDER_NAME);
public static string PackagesFolderPath => Path.Combine(ProjectPath, PACKAGES_FOLDER_NAME);
public static string CachesFolderPath => Path.Combine(ProjectPath, CACHES_FOLDER_NAME);
public static string ConfigFolderPath => Path.Combine(ProjectPath, CONFIG_FOLDER_NAME);
public static DispatcherQueue DispatcherQueue
{
get
{
if (s_dispatcherQueue is null)
{
throw new InvalidOperationException("DispatcherQueue is not initialized.");
}
return s_dispatcherQueue;
}
}
internal static void Initialize(IServiceProvider serviceProvider, string projectPath, string projectName)
{
s_serviceProvider = serviceProvider;
s_currentProjectPath = projectPath;
s_currentProjectName = projectName;
}
internal static void SetDispatcherQueue(DispatcherQueue dispatcherQueue)
{
s_dispatcherQueue = dispatcherQueue;
}
public static T GetService<T>()
where T : class
{
if (s_serviceProvider?.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices.");
}
return service;
}
internal static void Shutdown()
{
}
}

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,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Ghost.Editor.Core</RootNamespace>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<ImplicitUsings>enable</ImplicitUsings>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<!-- in .net 10, field keyword is not preview anymore, but we are still waiting roslyn team to update their code analyzer packages -->
<langversion>preview</langversion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" />
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.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\Internal\ComponentView.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,9 @@
namespace Ghost.Editor.Core.Notifications;
public enum MessageType
{
Informational,
Success,
Warning,
Error
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
namespace TestProject.AssetDB;
internal partial class AssetRegistry
{
// TODO: Sqlite backend implementation
}

View File

@@ -0,0 +1,510 @@
using Ghost.Core;
using Ghost.Editor.Core.AssetHandler;
using Ghost.Editor.Core.Contracts;
using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace TestProject.AssetDB;
internal class PathComparer : IEqualityComparer<string>
{
private static string ToCanonicalPath(string? path)
{
return path?.Replace('\\', '/').TrimEnd('/') ?? string.Empty;
}
public bool Equals(string? x, string? y)
{
return string.Equals(
ToCanonicalPath(x),
ToCanonicalPath(y),
StringComparison.Ordinal);
}
public int GetHashCode(string str)
{
return ToCanonicalPath(str).GetHashCode(StringComparison.Ordinal);
}
}
// TODO: Path based locking for multi-threaded access?
// Is it actually necessary since this is mostly used in editor environment where single-threaded access is common (99.999%)?
internal partial class AssetRegistry : IAssetRegistry
{
public const string ASSET_EXTENSION = ".gasset";
public const string TEMP_EXTENSION = ".gtemp";
private readonly string _rootDirectory;
private readonly FileSystemWatcher _watcher;
private readonly ConcurrentDictionary<string, Guid> _pathToGuid;
private readonly ConcurrentDictionary<Guid, string> _guidToPath;
private readonly ConcurrentDictionary<nint, IAssetHandler> _cachedHander;
private readonly ConcurrentDictionary<Guid, WeakReference<Asset>> _loadedAssets;
private readonly Dictionary<Guid, HashSet<Guid>> _referencerGraph;
private readonly Dictionary<Guid, HashSet<Guid>> _dependencyCache;
private readonly ConcurrentDictionary<string, bool> _ignoreFileChanges;
private readonly SemaphoreSlim _cacheSlim;
private readonly Lock _pathLock;
public event EventHandler<IAssetRegistry, AssetChangedEventArgs>? OnAssetChanged;
public AssetRegistry(string rootDirectory)
{
if (!Directory.Exists(rootDirectory))
{
throw new DirectoryNotFoundException("The specified root directory does not exist.");
}
if (!Path.IsPathFullyQualified(rootDirectory))
{
throw new InvalidOperationException("The specified root directory must be an absolute path.");
}
_rootDirectory = rootDirectory;
_watcher = new FileSystemWatcher(rootDirectory)
{
IncludeSubdirectories = true,
EnableRaisingEvents = true,
};
_pathToGuid = new ConcurrentDictionary<string, Guid>(4, 512, new PathComparer());
_guidToPath = new ConcurrentDictionary<Guid, string>(4, 512);
_cachedHander = new ConcurrentDictionary<nint, IAssetHandler>(4, 16);
_loadedAssets = new ConcurrentDictionary<Guid, WeakReference<Asset>>(4, 512);
_referencerGraph = new Dictionary<Guid, HashSet<Guid>>();
_dependencyCache = new Dictionary<Guid, HashSet<Guid>>();
_ignoreFileChanges = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
_cacheSlim = new SemaphoreSlim(1, 1);
_pathLock = new Lock();
LoadExistingAssets();
_watcher.Created += OnFileSystemOp;
_watcher.Deleted += OnFileSystemOp;
_watcher.Changed += OnFileSystemOp;
_watcher.Renamed += OnFileSystemRenameOp;
}
// TODO: DB Cache
private unsafe void LoadExistingAssets()
{
Span<byte> guidBuffer = stackalloc byte[sizeof(Guid)];
foreach (var filePath in Directory.EnumerateFiles(_rootDirectory, $"*{ASSET_EXTENSION}", SearchOption.AllDirectories))
{
var relativePath = Path.GetRelativePath(_rootDirectory, filePath);
try
{
var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
try
{
fs.Seek(4, SeekOrigin.Begin); // Skip format version
fs.ReadExactly(guidBuffer);
var guid = Unsafe.ReadUnaligned<Guid>(ref MemoryMarshal.GetReference(guidBuffer));
UpdatePathMapping(relativePath, guid);
}
finally
{
fs.Dispose();
}
}
catch (Exception
#if DEBUG
ex
#endif
)
{
#if DEBUG
System.Diagnostics.Debugger.BreakForUserUnhandledException(ex);
#endif
continue;
}
}
}
private void UpdateGraph(Guid assetId, IEnumerable<Guid> newDependencies)
{
// 1. Clean up old references (reverse)
if (_dependencyCache.TryGetValue(assetId, out var oldDeps))
{
foreach (var dep in oldDeps)
{
if (_referencerGraph.TryGetValue(dep, out var refs))
{
refs.Remove(assetId);
}
}
}
// 2. Set new forward dependencies
var newDepSet = new HashSet<Guid>(newDependencies);
_dependencyCache[assetId] = newDepSet;
// 3. Add new references (reverse)
foreach (var dep in newDepSet)
{
ref var referencers = ref CollectionsMarshal.GetValueRefOrAddDefault(_referencerGraph, dep, out var exists);
if (!exists || referencers is null)
{
referencers = new HashSet<Guid>();
}
referencers.Add(assetId);
}
}
private void UpdatePathMapping(string relativePath, Guid guid)
{
lock (_pathLock)
{
_pathToGuid[relativePath] = guid;
_guidToPath[guid] = relativePath;
}
}
private bool RemovePathMappingByPath(string relativePath)
{
lock (_pathLock)
{
if (_pathToGuid.Remove(relativePath, out var guid))
{
return _guidToPath.TryRemove(guid, out _);
}
}
return false;
}
private async void OnFileSystemOp(object sender, FileSystemEventArgs e)
{
if (_ignoreFileChanges.TryRemove(e.FullPath, out _))
{
return;
}
var relativePath = Path.GetRelativePath(_rootDirectory, e.FullPath);
var ext = Path.GetExtension(relativePath);
var changeType = AssetChangeType.None;
var fireEvent = false;
var isAsset = ext.Equals(ASSET_EXTENSION, StringComparison.Ordinal);
var isTemp = ext.Equals(TEMP_EXTENSION, StringComparison.Ordinal);
switch (e.ChangeType)
{
case WatcherChangeTypes.Created:
changeType = AssetChangeType.Created;
if (!isAsset && !isTemp)
{
var handler = GetAssetHandlerForExtension(ext);
if (handler is IImportableAssetHandler importableHandler)
{
var assetPath = string.Create(e.FullPath.Length - ext.Length + ASSET_EXTENSION.Length, e.FullPath, (destSpan, source) =>
{
source.AsSpan(0, source.Length - ext.Length).CopyTo(destSpan);
ASSET_EXTENSION.AsSpan().CopyTo(destSpan.Slice(source.Length - ext.Length));
});
var newGuid = Guid.NewGuid();
await using var sourceStream = new FileStream(e.FullPath, FileMode.Open, FileAccess.Read);
await using var targetStream = new FileStream(assetPath, FileMode.Create, FileAccess.Write);
await importableHandler.ImportAsync(sourceStream, targetStream, newGuid);
File.Delete(assetPath);
UpdatePathMapping(relativePath, newGuid);
fireEvent = true;
}
}
break;
case WatcherChangeTypes.Deleted:
changeType = AssetChangeType.Deleted;
if (isAsset)
{
fireEvent = RemovePathMappingByPath(relativePath);
}
break;
case WatcherChangeTypes.Changed:
changeType = AssetChangeType.Modified;
fireEvent = isAsset;
break;
case WatcherChangeTypes.All:
// Can this even happen?
break;
default:
break;
}
if (fireEvent)
{
OnAssetChanged?.Invoke(this, new AssetChangedEventArgs(relativePath, null, changeType));
}
}
private void OnFileSystemRenameOp(object sender, RenamedEventArgs e)
{
var ext = Path.GetExtension(e.FullPath);
if (!ext.Equals(ASSET_EXTENSION, StringComparison.Ordinal))
{
return;
}
var oldRelativePath = Path.GetRelativePath(_rootDirectory, e.OldFullPath);
var newRelativePath = Path.GetRelativePath(_rootDirectory, e.FullPath);
if (_pathToGuid.Remove(oldRelativePath, out var guid))
{
UpdatePathMapping(newRelativePath, guid);
OnAssetChanged?.Invoke(this, new AssetChangedEventArgs(newRelativePath, oldRelativePath, AssetChangeType.Renamed));
}
}
public string? GetAssetPath(Guid id)
{
lock (_pathLock)
{
if (_guidToPath.TryGetValue(id, out var path))
{
return path;
}
}
return null;
}
public Guid GetAssetGuid(string path)
{
lock (_pathLock)
{
if (_pathToGuid.TryGetValue(path, out var guid))
{
return guid;
}
}
return Guid.Empty;
}
private IAssetHandler GetAssetHandler(Type type)
{
var typeHandle = type.TypeHandle.Value;
if (_cachedHander.TryGetValue(typeHandle, out var handler))
{
return handler;
}
var obj = Activator.CreateInstance(type);
if (obj is not IAssetHandler newHandler)
{
throw new InvalidOperationException($"Type {type.FullName} is not an IAssetHandler.");
}
var attr = type.GetCustomAttribute<CustomAssetHandlerAttribute>(false);
if (attr is null || attr.AllowCaching)
{
_cachedHander[typeHandle] = newHandler;
}
return newHandler;
}
private IAssetHandler? GetAssetHandlerForExtension(string extension)
{
foreach (var handlerType in AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => typeof(IAssetHandler).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract))
{
var attr = handlerType.GetCustomAttribute<CustomAssetHandlerAttribute>(false);
if (attr is not null && attr.SupportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
return GetAssetHandler(handlerType);
}
}
return null;
}
private IAssetHandler? GetAssetHandlerForTypeId(Guid typeId)
{
foreach (var handlerType in AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => typeof(IAssetHandler).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract))
{
var attr = handlerType.GetCustomAttribute<CustomAssetHandlerAttribute>(false);
if (attr is not null && new Guid(attr.ID) == typeId)
{
return GetAssetHandler(handlerType);
}
}
return null;
}
public async ValueTask<Result<Guid>> ImportAssetAsync(string sourceFilePath, string targetAssetPath, CancellationToken token = default)
{
if (!File.Exists(sourceFilePath))
{
return Result.Failure("Source file not found.");
}
var ext = Path.GetExtension(sourceFilePath);
var handler = GetAssetHandlerForExtension(ext);
if (handler is not IImportableAssetHandler importableHandler)
{
return Result.Failure("No importable asset handler found for the given file extension.");
}
var guid = Guid.NewGuid();
var fullTargetPath = Path.GetFullPath(targetAssetPath, _rootDirectory);
if (!await importableHandler.ImportAsync(sourceFilePath, fullTargetPath, guid, token: token))
{
return Result.Failure("Asset import failed.");
}
UpdatePathMapping(targetAssetPath, guid);
return guid;
}
public async ValueTask<Result> ReimportAssetAsync(Guid assetId, string sourceFilePath, CancellationToken token = default)
{
var assetPath = GetAssetPath(assetId);
if (string.IsNullOrEmpty(assetPath))
{
return Result.Failure("Asset not found in DB");
}
var fullAssetPath = Path.GetFullPath(assetPath, _rootDirectory);
// 2. Identify the Handler
// (You might want to store SourcePath in metadata later so you don't need to pass it here)
var ext = Path.GetExtension(sourceFilePath);
var handler = GetAssetHandlerForExtension(ext);
if (handler is not IImportableAssetHandler importableHandler)
{
return Result.Failure("No importable asset handler found for the given file extension.");
}
_ignoreFileChanges[fullAssetPath] = true;
await using var sourceStream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read);
await using var targetStream = new FileStream(fullAssetPath, FileMode.Create, FileAccess.Write);
await importableHandler.ImportAsync(sourceStream, targetStream, assetId, token);
if (_loadedAssets.TryGetValue(assetId, out var weakRef) && weakRef.TryGetTarget(out var liveAsset))
{
await liveAsset.RefreshAsync(this, token);
}
return Result.Success();
}
public async ValueTask<Result<Asset>> LoadAssetAsync(Guid id, CancellationToken token = default)
{
// TODO: weakRef based locking instead of global lock for better concurrency.
// We should use GetOrAdd here.
if (_loadedAssets.TryGetValue(id, out var weakRef)
&& weakRef.TryGetTarget(out var existingAsset))
{
return existingAsset;
}
await _cacheSlim.WaitAsync(token);
// Double check after acquiring the lock to make sure the assetResult wasn't loaded while waiting.
if (_loadedAssets.TryGetValue(id, out weakRef)
&& weakRef.TryGetTarget(out existingAsset))
{
return existingAsset;
}
try
{
var path = GetAssetPath(id);
if (string.IsNullOrEmpty(path))
{
return null;
}
var assetPath = Path.GetFullPath(path, _rootDirectory);
await using var fs = new FileStream(assetPath, FileMode.Open, FileAccess.Read, FileShare.Read);
int sizeofGuid;
unsafe
{
sizeofGuid = sizeof(Guid);
}
Span<byte> typeIdBuffer = stackalloc byte[sizeofGuid];
fs.Seek(sizeof(int) + sizeofGuid, SeekOrigin.Begin);
fs.ReadExactly(typeIdBuffer);
var guid = Unsafe.ReadUnaligned<Guid>(ref MemoryMarshal.GetReference(typeIdBuffer));
var handler = GetAssetHandlerForTypeId(guid);
if (handler == null)
{
return null;
}
var assetResult = await handler.LoadAsync(fs, this, token);
if (assetResult.IsFailure)
{
return assetResult;
}
var asset = assetResult.Value;
_loadedAssets.AddOrUpdate(id, new WeakReference<Asset>(asset), (key, oldRef) =>
{
// If the early return fails (find existing assetResult), it means either the assetResult haven't been loaded before, or the previous reference has been collected.
// If the assetResult haven't been loaded before, we are in the addValue path, not here.
// If the previous reference has been collected, we can just replace it with the new one.
// Since we are using _cacheSlim to protect this section, we don't need check if the oldRef is still valid because only one thread can be here at a time.
oldRef.SetTarget(asset);
return oldRef;
});
return assetResult;
}
finally
{
_cacheSlim.Release();
}
}
public async ValueTask<Result> SaveAssetAsync(Asset asset, CancellationToken token = default)
{
var path = GetAssetPath(asset.ID);
if (path == null)
{
return Result.Failure("Asset not found.");
}
var handler = GetAssetHandlerForTypeId(asset.TypeID);
if (handler == null)
{
return Result.Failure("No asset handler found for the given asset type.");
}
var fullPath = Path.GetFullPath(path, _rootDirectory);
await using var fs = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
return await handler.SaveAsync(asset, fs, this, token);
}
public void Dispose()
{
_cacheSlim.Dispose();
_watcher.Dispose();
}
}

View File

@@ -0,0 +1,21 @@
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.Services;
public class InspectorService : IInspectorService
{
private IInspectable? _selected;
public IInspectable? Selected => _selected;
public event EventHandler<InspectorSelectionChangedEventArgs>? OnSelectionChanged;
public void SetSelected(IInspectable? inspectable, object? source)
{
if (_selected != inspectable)
{
_selected = inspectable;
OnSelectionChanged?.Invoke(this, new InspectorSelectionChangedEventArgs(source, inspectable));
}
}
}

View File

@@ -0,0 +1,51 @@
using CommunityToolkit.WinUI.Behaviors;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Notifications;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Services;
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,35 @@
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.Services;
internal class PreviewService : IPreviewService
{
public string GetIconPath(string path, bool isDirectory, IconSize size)
{
string iconPath;
if (isDirectory)
{
iconPath = "ms-appx:///Assets/EditorIcons/folder-{0}.png";
}
else
{
// TODO: Generate preview icons dynamically for known file types like images, meshes, materials, etc.
var ext = Path.GetExtension(path);
iconPath = ext switch
{
".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".tiff" or ".svg" => "ms-appx:///Assets/EditorIcons/image-{0}.png",
_ => "ms-appx:///Assets/EditorIcons/document-{0}.png",
};
}
var sizeIndex = size switch
{
IconSize.Small => "0",
IconSize.Large => "1",
_ => "0"
};
iconPath = string.Format(iconPath, sizeIndex);
return iconPath;
}
}

View File

@@ -0,0 +1,75 @@
using CommunityToolkit.WinUI;
using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Runtime.CompilerServices;
namespace Ghost.Editor.Core.Services;
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,53 @@
using Ghost.Editor.Core.AssetHandler;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.Utilities;
public static class AssetHandlerUtility
{
public static async ValueTask SerializeAssetAsync<TSetting>(Stream stream, Guid id, Guid typeID, int handlerVersion, ReadOnlyMemory<Guid> dependencies, IAssetSettings? settings, ReadOnlyMemory<byte> contents, CancellationToken token = default)
where TSetting : IAssetSettings
{
var header = new AssetMetadata(id, TextureAsset.s_typeGuid)
{
HandlerVersion = handlerVersion,
DependenciesOffset = AssetMetadata.SIZE,
DependencyCount = dependencies.Length,
};
var tempArray = ArrayPool<byte>.Shared.Rent(4096);
if (dependencies.Length > 0)
{
stream.Seek(header.DependenciesOffset, SeekOrigin.Begin);
for (var i = 0; i < dependencies.Length; i++)
{
Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(tempArray.AsSpan(0, 16)), dependencies.Span[i]);
await stream.WriteAsync(tempArray.AsMemory(0, 16), token);
}
}
header.SettingsOffset = stream.Position;
// TODO: We can use source generator to generate optimized serializer for settings.
// For now, we just use reflection for simplicity.
if (settings is not null)
{
var properties = typeof(TSetting).GetProperties();
if (properties.Length > 0)
{
using var bw = new BinaryWriter(stream);
for (var i = 0; (i < properties.Length); i++)
{
var property = properties[i];
var value = property.GetValue(settings);
}
}
}
}
}

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,93 @@
using Ghost.Core.Attributes;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.Utilities;
public static class TypeCache
{
private static TypeInfo[] s_types;
private static Dictionary<nint, List<MethodInfo>> s_attributeMethodCache;
static TypeCache()
{
s_types = LoadTypes();
s_attributeMethodCache = FindMethodWithAttribute();
}
private static TypeInfo[] LoadTypes()
{
var loadableTypes = new List<Type>(512);
var assembliesToScan = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetCustomAttribute<EngineAssemblyAttribute>() != null);
foreach (var assembly in assembliesToScan)
{
try
{
loadableTypes.AddRange(assembly.GetTypes());
}
catch (ReflectionTypeLoadException ex)
{
var types = ex.Types.Where(t => t != null);
loadableTypes.AddRange(types!);
}
}
return loadableTypes.Select(t => t.GetTypeInfo()).ToArray();
}
private static Dictionary<nint, List<MethodInfo>> FindMethodWithAttribute()
{
var dict = new Dictionary<nint, List<MethodInfo>>();
foreach (var type in s_types)
{
foreach (var method in type.DeclaredMethods)
{
var attrs = method.GetCustomAttributes<DiscoverableAttributeBase>(false);
foreach (var attr in attrs)
{
var key = attr.GetType().TypeHandle.Value;
ref var methodList = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out var exist);
if (!exist)
{
methodList = new List<MethodInfo>();
}
methodList!.Add(method);
}
}
}
return dict;
}
internal static void Init()
{
// Intentionally left blank.
// This method exists to force the static constructor to run.
}
internal static void Reload()
{
s_types = LoadTypes();
s_attributeMethodCache = FindMethodWithAttribute();
}
public static IReadOnlyCollection<TypeInfo> GetTypes()
{
return s_types;
}
public static IReadOnlyCollection<MethodInfo>? GetMethodsWithAttribute<T>()
where T : DiscoverableAttributeBase
{
var key = typeof(T).TypeHandle.Value;
if (s_attributeMethodCache.TryGetValue(key, out var methods))
{
return methods;
}
return null;
}
}

View File

@@ -0,0 +1,67 @@
using Ghost.Editor.Core.Utilities;
using Ghost.Editor.Models;
using Ghost.Engine;
using System.Reflection;
namespace Ghost.Editor;
internal static class ActivationHandler
{
public static LaunchArguments ParseArguments(ReadOnlySpan<char> args)
{
var arguments = new LaunchArguments();
var properties = typeof(LaunchArguments).GetProperties();
var split = args.Split(' ');
while (split.MoveNext())
{
var range = split.Current;
var arg = args[range.Start..range.End];
if (arg.Length > 2)
{
if (arg[0] == '-' && arg[1] == '-')
{
var argName = arg[2..];
foreach (var property in properties)
{
var propName = property.Name;
var attr = property.GetCustomAttributes<ArgumentNameAttribute>(false).FirstOrDefault();
if (attr != null)
{
propName = attr.Name;
}
if (argName.Equals(propName, StringComparison.OrdinalIgnoreCase))
{
if (split.MoveNext())
{
var valueRange = split.Current;
var value = args[valueRange.Start..valueRange.End];
var convertedValue = Convert.ChangeType(value.ToString(), property.PropertyType);
property.SetValue(arguments, convertedValue);
break;
}
}
}
}
}
}
return arguments;
}
public static async Task HandleAsync(LaunchArguments args)
{
await Task.Run(() =>
{
TypeCache.Init();
((EngineCore)App.GetService<IEngineContext>()).Init();
});
// await ((Core.AssetHandle.AssetService)App.GetService<IAssetService>()).Init();
// TODO: Init other subsystems here.
// await Task.Delay(10000); // Wait 10 seconds to simulate work.
}
}

View File

@@ -3,14 +3,15 @@
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="/Themes/Generic.xaml" />
<core:ControlsDictionary />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@@ -0,0 +1,162 @@
using Ghost.Core;
using Ghost.Editor.Core;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Services;
using Ghost.Editor.View.Pages.EngineEditor;
using Ghost.Editor.View.Windows;
using Ghost.Editor.ViewModels.Controls;
using Ghost.Editor.ViewModels.Pages.EngineEditor;
using Ghost.Editor.ViewModels.Windows;
using Ghost.Engine;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using System.Diagnostics;
using WinUIEx;
namespace Ghost.Editor;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
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>
/// 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().
/// </summary>
internal App()
{
InitializeComponent();
Host = Microsoft.Extensions.Hosting.Host.
CreateDefaultBuilder().
UseContentRoot(AppContext.BaseDirectory).
ConfigureServices((context, services) =>
{
services.AddSingleton<IEngineContext, EngineCore>();
services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<IProgressService, ProgressService>();
services.AddSingleton<IInspectorService, InspectorService>();
services.AddSingleton<IPreviewService, PreviewService>();
// services.AddSingleton<IAssetService, AssetService>();
services.AddSingleton<EngineEditorViewModel>();
services.AddTransient<ProjectBrowserViewModel>();
#region Should be deleted
services.AddTransient<ScenePage>();
services.AddTransient<HierarchyPage>();
services.AddTransient<HierarchyViewModel>();
services.AddTransient<ProjectPage>();
services.AddTransient<ProjectViewModel>();
services.AddTransient<ConsolePage>();
services.AddTransient<ConsoleViewModel>();
services.AddTransient<InspectorPage>();
services.AddTransient<InspectorViewModel>();
#endregion
})
.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;
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
base.OnLaunched(args);
var arguments = ActivationHandler.ParseArguments("--project-path F:/GhostProject/Test2 --project-name Test2"); // args.Arguments
if (!arguments.IsValid())
{
Exit();
return;
}
EditorApplication.Initialize(Host.Services, arguments.ProjectPath, arguments.ProjectName);
// NOTE: We must call DispatcherQueue.GetForCurrentThread() on the UI thread before any await.
EditorApplication.SetDispatcherQueue(DispatcherQueue.GetForCurrentThread());
var splashWindow = new SplashWindow();
splashWindow.Activate();
Window = splashWindow;
await Host.StartAsync();
await ActivationHandler.HandleAsync(arguments);
splashWindow.Hide();
var editorWindow = new EngineEditorWindow();
editorWindow.Activate();
Window = editorWindow;
splashWindow.Close();
}
private void OnClosed(object? sender, WindowEventArgs args)
{
try
{
Host.StopAsync().GetAwaiter().GetResult();
Host.Dispose();
EditorApplication.Shutdown();
}
catch (Exception ex)
{
Debugger.BreakForUserUnhandledException(ex);
}
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError(e.Exception);
#if DEBUG
Debugger.BreakForUserUnhandledException(e.Exception);
#endif
}
}

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: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 432 B

After

Width:  |  Height:  |  Size: 432 B

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 456 B

After

Width:  |  Height:  |  Size: 456 B

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48" width="48px" height="48px">
<defs>
<style>
.cls-1 {
fill: url(#Безымянный_градиент_199);
}
.cls-2 {
fill: #f7c13a;
}
</style>
<linearGradient id=езымянный_градиент_199" data-name="Безымянный градиент 199" x1="11.80415" y1="-22.237" x2="29.6085" y2="45.263" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#6d6d6d"/>
<stop offset="0.12552" stop-color="#626262"/>
<stop offset="0.987" stop-color="#464646"/>
<stop offset="0.998" stop-color="#454545"/>
</linearGradient>
</defs>
<rect class="cls-1" x="6" y="6" width="36" height="36" rx="2"/>
<path class="cls-2" d="M22,27v1.5a.5.5,0,0,1-.5.5H17a8,8,0,0,1-8-8V19.5a.5.5,0,0,1,.5-.5h4.00612a.501.501,0,0,1,.49758.49688C14.05815,23.13441,14.70582,26,15.5,26c.73344,0,1.33761-2.43083,1.46835-5.65979a.50624.50624,0,0,1,.74437-.42711A7.99072,7.99072,0,0,1,22,27Z"/>
<path class="cls-2" d="M39,19.5V21a8,8,0,0,1-8,8H26.5a.5.5,0,0,1-.5-.5V27a7.99072,7.99072,0,0,1,4.28728-7.0869.50624.50624,0,0,1,.74437.42711C31.16239,23.56917,31.76656,26,32.5,26c.79418,0,1.44185-2.86559,1.4963-6.50312A.501.501,0,0,1,34.49388,19H38.5A.5.5,0,0,1,39,19.5Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

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