From 817b32b8d9ce76adb74b7b1779520ae9b80a98d7 Mon Sep 17 00:00:00 2001 From: Misaki Date: Mon, 13 Apr 2026 23:07:52 +0900 Subject: [PATCH] feat(graphics): refactor pipeline keying and allocators Major refactor of graphics pipeline keying, shader cache, and resource allocation. Replaced most Allocator usage with AllocationHandle, modernized logger usage, and unified pipeline state keys. Updated MeshUtility to use AllocationHandle.FreeList. Added new shader pipeline architecture docs and improved error handling throughout. BREAKING CHANGE: Pipeline keying and resource allocation APIs have changed. --- .gitignore | 4 +- docs/notes/shader_pipeline_architecture.md | 457 +++++++++++ .../plans/2026-03-28-dock-layout.md | 669 ---------------- .../plans/2026-03-28-docking-layout.md | 724 ------------------ .../specs/2026-03-28-dock-layout-design.md | 75 -- .../specs/2026-03-28-docking-layout-design.md | 38 - .../ShaderCompiler/DSLShaderCompiler.cs | 5 - src/Editor/Ghost.Editor/App.xaml.cs | 4 +- src/Runtime/Ghost.Core/Ghost.Core.csproj | 4 +- .../Ghost.Core/Graphics/PipelineState.cs | 3 +- .../Ghost.Core/Graphics/ShaderDescriptor.cs | 1 - src/Runtime/Ghost.Core/Logging.cs | 58 +- src/Runtime/Ghost.Core/Result.cs | 27 +- src/Runtime/Ghost.Core/TemJobAllocator.cs | 50 +- .../Ghost.Core/Utilities/BinaryReader.cs | 36 - .../Ghost.Core/Utilities/BufferWriter.cs | 100 ++- src/Runtime/Ghost.Engine/Components/Camera.cs | 1 - .../Ghost.Engine/RenderPipeline/GPUScene.cs | 14 +- .../GhostRenderPipeline.UpdateGPUScene.cs | 50 ++ .../RenderPipeline/GhostRenderPipeline.cs | 33 +- .../GhostRenderPipelineSettings.cs | 2 +- .../Systems/RenderPipelineSystemAttribute.cs | 1 + src/Runtime/Ghost.Entities/Archetype.cs | 18 +- src/Runtime/Ghost.Entities/Component.cs | 13 +- .../Ghost.Entities/EntityCommandBuffer.cs | 2 +- src/Runtime/Ghost.Entities/EntityManager.cs | 6 +- src/Runtime/Ghost.Entities/Query.cs | 4 +- src/Runtime/Ghost.Entities/SharedComponent.cs | 10 +- src/Runtime/Ghost.Entities/System.cs | 2 +- .../D3D12CommandBuffer.cs | 11 +- .../D3D12CommandSignature.cs | 3 +- .../D3D12DescriptorHeap.cs | 7 +- .../D3D12GraphicsEngine.cs | 20 +- .../Ghost.Graphics.D3D12/D3D12Object.cs | 3 +- .../D3D12PipelineLibrary.cs | 60 +- .../D3D12ResourceAllocator.cs | 13 +- .../D3D12ResourceDatabase.cs | 51 +- .../Ghost.Graphics.D3D12/DXGISwapChain.cs | 14 +- .../Utilities/D3D12Utility.cs | 6 +- src/Runtime/Ghost.Graphics.RHI/Common.cs | 78 +- .../Ghost.Graphics.RHI/ICommandBuffer.cs | 5 +- .../Ghost.Graphics.RHI/IPipelineLibrary.cs | 5 +- src/Runtime/Ghost.Graphics.RHI/Keyword.cs | 4 +- src/Runtime/Ghost.Graphics.RHI/RHIUtility.cs | 40 +- .../Ghost.Graphics.RHI/RootSignatureLayout.cs | 7 +- src/Runtime/Ghost.Graphics/Core/Material.cs | 6 +- .../Core/MaterialPaletteStore.cs | 6 +- src/Runtime/Ghost.Graphics/Core/Mesh.cs | 12 +- .../Ghost.Graphics/Core/RenderContext.cs | 111 ++- .../Ghost.Graphics/Core/RenderRequest.cs | 10 +- src/Runtime/Ghost.Graphics/Core/Shader.cs | 45 +- .../Ghost.Graphics/Ghost.Graphics.csproj | 4 - .../RenderGraphModule/RenderGraph.cs | 5 +- .../RenderGraphModule/RenderGraphAliasing.cs | 3 +- .../RenderGraphModule/RenderGraphBuilder.cs | 2 +- .../RenderGraphModule/RenderGraphContext.cs | 82 +- .../RenderGraphModule/RenderGraphExecutor.cs | 6 +- src/Runtime/Ghost.Graphics/RenderPipeline.cs | 185 ++--- src/Runtime/Ghost.Graphics/RenderSystem.cs | 24 +- .../Services/ResourceManager.Pool.cs | 8 +- .../Services/ResourceManager.cs | 50 +- .../Ghost.Graphics/Services/ShaderLibrary.cs | 142 +++- .../Services/SwapChainManager.cs | 10 +- .../Utilities/MeshletUtility.cs | 53 +- .../Utilities/RenderingUtility.cs | 7 +- .../RenderPipeline/TestRenderPipeline.cs | 2 +- .../Systems/RenderExtractionSystem.cs | 6 +- .../Ghost.Graphics.Test/UnitTestApp.xaml.cs | 4 +- .../Utilities/MeshUtility.cs | 10 +- 69 files changed, 1436 insertions(+), 2095 deletions(-) create mode 100644 docs/notes/shader_pipeline_architecture.md delete mode 100644 docs/superpowers/plans/2026-03-28-dock-layout.md delete mode 100644 docs/superpowers/plans/2026-03-28-docking-layout.md delete mode 100644 docs/superpowers/specs/2026-03-28-dock-layout-design.md delete mode 100644 docs/superpowers/specs/2026-03-28-docking-layout-design.md delete mode 100644 src/Runtime/Ghost.Core/Utilities/BinaryReader.cs create mode 100644 src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.UpdateGPUScene.cs diff --git a/.gitignore b/.gitignore index e88f367..d5bb5f2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,9 +11,11 @@ *.sln.docstates AGENTS.md +.opencode/ +.code-review-graph/ + ref/ docfx/ -.opencode/ NUL # User-specific files (MonoDevelop/Xamarin Studio) diff --git a/docs/notes/shader_pipeline_architecture.md b/docs/notes/shader_pipeline_architecture.md new file mode 100644 index 0000000..16d0707 --- /dev/null +++ b/docs/notes/shader_pipeline_architecture.md @@ -0,0 +1,457 @@ +# Shader Pipeline Architecture — Proposed Design + +> Presented as a design walkthrough. Take what's useful, ignore what doesn't fit your vision. + +--- + +## 1. System Topology + +The first decision: **where does each responsibility live?** + +```mermaid +graph TB + subgraph EditorProcess["Ghost.Editor Process"] + FW["FileWatcher
(monitors .ghost DSL files)"] + AR["AssetRegistry
(GUID ↔ file path mapping)"] + EP["Editor UI
(status bar, material inspector)"] + end + + subgraph CompilerProcess["GhostShaderServer Process"] + DSL["DSL Compiler
(Ghost DSL → HLSL)"] + DXC["DXC Compiler
(HLSL → DXIL bytecode)"] + MW["Manifest Writer
(updates variant → hash mapping)"] + end + + subgraph RuntimeGraphics["Ghost.Graphics (Runtime)"] + SL["ShaderLibrary
(reads bytecode from cache)"] + PL["PipelineLibrary
(PSO creation + double-buffer)"] + RGC["RenderGraphContext
(binds PSO per draw call)"] + BR["IShaderCompilationBridge
(interface, 2 methods)"] + end + + subgraph SharedDisk["Shared Disk (ShaderCache/)"] + MF["ShaderManifest.bin
(GUID+variant → content hash)"] + BC["Bytecode Files
(content-addressed .bin blobs)"] + end + + FW -- "file changed event" --> AR + AR -- "GUID + file path
(named pipe)" --> CompilerProcess + DSL --> DXC + DXC -- "bytecode bytes" --> MW + MW -- "write blob" --> BC + MW -- "update entry" --> MF + + SL -- "read blob" --> BC + SL -- "read mapping" --> MF + BR -- "status query
(named pipe)" --> CompilerProcess + + EP -- "poll status" --> BR + + style CompilerProcess fill:#1a1a2e,stroke:#e94560,color:#eee + style EditorProcess fill:#1a1a2e,stroke:#0f3460,color:#eee + style RuntimeGraphics fill:#1a1a2e,stroke:#16213e,color:#eee + style SharedDisk fill:#0f3460,stroke:#533483,color:#eee +``` + +### Why a Separate Process? + +| Concern | In-process compiler | Separate process | +|---------|-------------------|------------------| +| DXC crash | Editor dies | Server restarts, editor lives | +| DXC memory leak | Editor bloats over time | Kill & restart server periodically | +| Parallelism | Threads compete with editor UI | Fully independent CPU budget | +| Build pipeline reuse | Need separate build-time path | Same server binary, different mode | +| Complexity | Lower (one process) | Higher (IPC needed) | + +> [!TIP] +> If the separate process feels like overkill for your current stage, **start with in-process behind the `IShaderCompilationBridge` interface**, then swap the implementation to out-of-process later. The interface is the same either way. + +--- + +## 2. Data Model — The Manifest + +This is the most important data structure in the entire system. It decouples **identity** from **content**. + +```mermaid +graph LR + subgraph ShaderAsset["Shader Asset (on disk)"] + GUID["Asset GUID
e.g. 7f3a-...-c82b
stable forever"] + SRC["Source Code
.ghost DSL file
changes on edit"] + end + + subgraph Manifest["ShaderManifest"] + E1["Entry:
GUID=7f3a | Pass=0 | Variant=0x00
→ ContentHash=0xABCD"] + E2["Entry:
GUID=7f3a | Pass=0 | Variant=0x01
→ ContentHash=0x1234"] + E3["Entry:
GUID=7f3a | Pass=1 | Variant=0x00
→ ContentHash=0x5678"] + end + + subgraph Cache["ShaderCache/ (content addressed)"] + B1["AB/shader_cache_ABCD...bin"] + B2["12/shader_cache_1234...bin"] + B3["56/shader_cache_5678...bin"] + end + + GUID --> E1 + GUID --> E2 + GUID --> E3 + E1 --> B1 + E2 --> B2 + E3 --> B3 + + style ShaderAsset fill:#16213e,stroke:#0f3460,color:#eee + style Manifest fill:#1a1a2e,stroke:#e94560,color:#eee + style Cache fill:#0f3460,stroke:#533483,color:#eee +``` + +### Manifest Entry Structure + +``` +ManifestKey = Hash(AssetGUID + PassIndex + VariantKeywordMask) +ManifestValue = ContentHash (= Hash of compiled bytecode) +``` + +- **ManifestKey** is *structurally* derived — same shader, same pass, same keywords = same key, regardless of source changes. +- **ContentHash** is *content-derived* — changes every time the source code changes. +- When source changes: the ManifestKey stays the same, but the ContentHash it points to gets updated. + +> [!IMPORTANT] +> The `Shader` struct in runtime only needs to know the **AssetGUID**. It never stores or cares about content hashes. The `ShaderLibrary` uses the manifest to translate `(GUID, Pass, Variant) → ContentHash → File`. + +--- + +## 3. Compilation Flow — What Happens When You Save a Shader + +```mermaid +sequenceDiagram + participant User + participant FileSystem + participant Editor as Ghost.Editor + participant Server as ShaderServer + participant Cache as ShaderCache/ + + User->>FileSystem: Save "water.ghost" + FileSystem-->>Editor: FileWatcher event + + Editor->>Editor: Lookup GUID for "water.ghost"
via AssetRegistry + Editor->>Server: CompileRequest {
guid: 7f3a-...,
filePath: "water.ghost",
defines: [...],
platform: D3D12
} + + Note over Server: Mark status = Compiling
for this GUID + + Server->>Server: Read .ghost DSL file + Server->>Server: DSL Compiler: DSL → HLSL + + alt DSL has syntax errors + Server->>Server: Mark status = Error + Server-->>Editor: CompileResult {
status: Error,
errors: [...]
} + Editor->>Editor: Show errors in
console/inspector + else DSL is valid + Server->>Server: For each (pass, variant):
DXC Compile HLSL → DXIL + + alt Any DXC error + Server->>Server: Mark status = Error + Server-->>Editor: CompileResult {
status: Error,
errors: [...]
} + else All variants compiled + Server->>Cache: Write bytecode blobs
(content-addressed) + Server->>Cache: Update manifest entries:
(GUID+pass+variant) → new hash + Server->>Server: Mark status = Ready + Server-->>Editor: CompileResult {
status: Ready,
variantCount: N
} + Editor->>Editor: Show ✓ in status bar + end + end +``` + +### Key Design Decision: Compile All Variants Upfront? + +**No.** Only compile variants that are *currently referenced* by materials in the scene. The editor knows which materials reference which shader (via AssetRegistry), and which keyword combinations those materials use. Ship only what's needed. + +For the edit-time hot-reload, you really only need the specific variants the viewport is currently rendering. The full permutation set is a build-time concern. + +--- + +## 4. Runtime PSO Resolution — The Frame-by-Frame Flow + +This is where most of the complexity lives. Here's what `SetActiveMaterial` does every frame: + +```mermaid +flowchart TD + A["SetActiveMaterial(material)"] --> B["Compute ManifestKey
= f(shader.GUID, passIndex, variantMask)"] + B --> C{"PipelineLibrary
has PSO for
ManifestKey?"} + + C -- "Yes (cache hit)" --> D["Bind existing PSO
to command buffer"] + D --> Z["Done ✓"] + + C -- "No (cache miss)" --> E{"ShaderLibrary
has bytecode for
ManifestKey?"} + + E -- "Yes (manifest hit)" --> F["Read bytecode
from cache file"] + F --> G["Create PSO from bytecode"] + G --> H["Store in PipelineLibrary"] + H --> D + + E -- "No (manifest miss)" --> I{"Is this Editor
or Runtime?"} + + I -- "Runtime
(shipped game)" --> J["Bind Fallback
ERROR PSO ⚠️"] + J --> K["Log error:
missing shader"] + K --> Z + + I -- "Editor" --> L{"Query Bridge:
IsCompiling?"} + + L -- "Status = Compiling" --> M["Bind OLD PSO
(keep previous frame's shader)"] + M --> Z + + L -- "Status = Error" --> N["Bind ERROR PSO
(magenta)"] + N --> Z + + L -- "Status = Ready" --> O["The manifest was just updated.
Re-read manifest entry."] + O --> F + + L -- "Status = NotAvailable" --> J + + style A fill:#533483,stroke:#e94560,color:#eee + style D fill:#16213e,stroke:#0f3460,color:#eee + style J fill:#e94560,stroke:#1a1a2e,color:#eee + style M fill:#0f3460,stroke:#533483,color:#eee + style N fill:#e94560,stroke:#1a1a2e,color:#eee + style Z fill:#16213e,stroke:#16213e,color:#eee +``` + +### The "Keep Old PSO" Strategy — How It Works Mechanically + +This is the part that makes the UX feel seamless. The trick: + +```mermaid +graph LR + subgraph PipelineLibrary + direction TB + K["ManifestKey 0xAABB"] + K --> CURRENT["current: PSO_v2 ✓
(what we render with)"] + K --> PENDING["pending: null
(set during recompilation)"] + end + + style CURRENT fill:#16213e,stroke:#0f3460,color:#eee + style PENDING fill:#1a1a2e,stroke:#e94560,color:#eee +``` + +When shader source changes and recompilation starts: + +```mermaid +graph LR + subgraph PipelineLibrary_During["During Recompilation"] + direction TB + K2["ManifestKey 0xAABB"] + K2 --> CURRENT2["current: PSO_v2 ✓
(still rendering with this)"] + K2 --> PENDING2["pending: COMPILING
(server is working...)"] + end + + style CURRENT2 fill:#16213e,stroke:#0f3460,color:#eee + style PENDING2 fill:#e94560,stroke:#1a1a2e,color:#eee +``` + +When recompilation finishes successfully: + +```mermaid +graph LR + subgraph PipelineLibrary_After["After Swap"] + direction TB + K3["ManifestKey 0xAABB"] + K3 --> CURRENT3["current: PSO_v3 ✓
(new shader, rendering now)"] + K3 --> PENDING3["pending: null
(swap complete)"] + end + + style CURRENT3 fill:#16213e,stroke:#0f3460,color:#eee + style PENDING3 fill:#1a1a2e,stroke:#533483,color:#eee +``` + +> [!NOTE] +> The old `PSO_v2` is **not immediately destroyed**. It stays alive until the GPU is done with any in-flight frames referencing it (tracked by fence value). This prevents use-after-free on the GPU timeline. + +--- + +## 5. Hot-Reload Sequence — The Complete Picture + +Everything combined into one timeline: + +```mermaid +sequenceDiagram + participant User + participant Editor + participant Server as ShaderServer + participant Cache as Disk Cache + participant Runtime as RenderGraphContext + participant GPU + + Note over Runtime,GPU: Frame N: Rendering with PSO_v2 + + User->>Editor: Edit & save "water.ghost" + Editor->>Server: CompileRequest(guid=7f3a) + Server->>Server: status[7f3a] = Compiling + + Note over Runtime,GPU: Frame N+1 + Runtime->>Runtime: SetActiveMaterial() + Runtime->>Runtime: ManifestKey lookup → old hash still there + Runtime->>Runtime: PipelineLibrary has PSO → use it + Note over Runtime: Still rendering with PSO_v2
(user sees no flicker) + + Note over Server: Background: DSL→HLSL→DXC... + + Note over Runtime,GPU: Frame N+2, N+3, ... + Runtime->>Runtime: Same as N+1, no visible change + + Server->>Cache: Write new bytecode files + Server->>Cache: Update manifest:
key(7f3a,0,0) → new_hash + Server->>Server: status[7f3a] = Ready + + Note over Runtime,GPU: Frame N+K (compilation done) + Runtime->>Runtime: SetActiveMaterial() + Runtime->>Runtime: Manifest read → NEW content hash + Runtime->>Runtime: PipelineLibrary miss for new hash + Runtime->>Cache: Read new bytecode + Runtime->>GPU: Create PSO_v3 + Runtime->>Runtime: PipelineLibrary: current=PSO_v3 + Runtime->>Runtime: Bind PSO_v3 + + Note over Runtime,GPU: Frame N+K+1: Rendering with PSO_v3 ✓ + + Runtime->>Runtime: Defer release PSO_v2
(after GPU fence) +``` + +### What the User Sees + +| Frame | Viewport | Status Bar | +|-------|----------|------------| +| N | Water renders normally | — | +| N+1 | Water renders normally (old shader) | 🔄 Compiling water.ghost... | +| N+2 | Water renders normally (old shader) | 🔄 Compiling water.ghost... | +| N+K | Water renders with new shader | ✅ water.ghost compiled (2 variants) | + +**Zero flicker. Zero blocking. Zero pink frames.** + +--- + +## 6. How the Manifest Key Replaces Your Current Hash Problem + +Here's a before/after of your `Shader` struct: + +### Current Design (problematic) +```mermaid +graph TD + subgraph Current["Current: Hash = f(source code)"] + S1["Shader struct"] --> P1["Pass[0].Key = 0xABCD
derived from source hash"] + P1 --> V1["ShaderVariantKey = f(0xABCD, keywords)"] + V1 --> PK1["PipelineKey = f(variant, rtv, dsv)"] + PK1 --> PSO1["PSO lookup in PipelineLibrary"] + + EDIT["User edits source"] -.-> STALE["Pass[0].Key is now STALE ❌
Still 0xABCD, but source changed"] + STALE -.-> WRONG["Looks up OLD bytecode
or worse, the old PSO"] + end + + style STALE fill:#e94560,stroke:#1a1a2e,color:#eee + style WRONG fill:#e94560,stroke:#1a1a2e,color:#eee +``` + +### Proposed Design (stable) +```mermaid +graph TD + subgraph Proposed["Proposed: Key = f(GUID, pass index)"] + S2["Shader struct
assetGUID = 7f3a-..."] --> P2["Pass[0]: index=0
no source hash stored"] + P2 --> MK["ManifestKey = f(7f3a, 0, keywords)"] + MK --> MANIFEST["Manifest Lookup
→ ContentHash = 0x9999"] + MANIFEST --> SL2["ShaderLibrary
→ read 99/shader_cache_9999.bin"] + SL2 --> PSO2["Create or get PSO"] + + EDIT2["User edits source"] -.-> RECOMP["Server recompiles
→ new ContentHash = 0xBBBB"] + RECOMP -.-> MUPD["Manifest updated:
same key → 0xBBBB"] + MUPD -.-> NEXT["Next frame: manifest read
picks up 0xBBBB automatically"] + end + + style RECOMP fill:#0f3460,stroke:#533483,color:#eee + style MUPD fill:#0f3460,stroke:#533483,color:#eee + style NEXT fill:#16213e,stroke:#0f3460,color:#eee +``` + +> [!IMPORTANT] +> **The `Shader` struct never changes.** No unload, no recreate, no generation counter bump. The manifest is the *only* mutable state, and it lives on disk, outside the runtime's object graph. The runtime just reads it. + +--- + +## 7. The Two Interfaces That Make This Work + +Only two abstractions are needed in `Ghost.Graphics` to support the full pipeline: + +```mermaid +classDiagram + class IShaderCompilationBridge { + <> + +TryGetBytecode(manifestKey: ulong, out bytecode: ReadOnlyMemory~byte~) bool + +IsCompiling(manifestKey: ulong) bool + } + + class RuntimeStub { + +TryGetBytecode() → always from ShaderLibrary cache + +IsCompiling() → always false + } + + class EditorImplementation { + -NamedPipeClient _serverConnection + +TryGetBytecode() → check manifest, read cache + +IsCompiling() → query server status + } + + IShaderCompilationBridge <|.. RuntimeStub : "Shipped game" + IShaderCompilationBridge <|.. EditorImplementation : "Editor mode" + + class ShaderLibrary { + -string _cacheDirectory + +GetCache(contentHash: ulong) Result~bytes~ + +GetFromManifest(manifestKey: ulong) Result~bytes~ + } + + EditorImplementation --> ShaderLibrary : reads cache + RuntimeStub --> ShaderLibrary : reads cache +``` + +> [!TIP] +> `RenderGraphContext` doesn't talk to the bridge directly. It talks to `ShaderLibrary`, which internally consults the bridge on cache miss. This keeps the rendering code clean — it never sees compilation status. It just gets bytecode or it doesn't. + +--- + +## 8. Build Pipeline — How Shipped Games Work + +For completeness, here's how the same architecture handles builds: + +```mermaid +flowchart LR + subgraph BuildTime["Build Pipeline"] + SCAN["Scan all materials
in scenes/assets"] --> COLLECT["Collect all referenced
(GUID, pass, variant) tuples"] + COLLECT --> COMPILE["Compile all variants
via ShaderServer"] + COMPILE --> PACK["Package manifest +
bytecode blobs into
game data archive"] + end + + subgraph ShippedGame["Runtime (shipped game)"] + LOAD["Load manifest +
bytecode from archive"] --> LIB["ShaderLibrary
(read-only, all variants pre-cached)"] + LIB --> MISS{"Cache miss?"} + MISS -- "Never
(if build is correct)" --> OK["Create PSO normally"] + MISS -- "Somehow yes
(bug or modding)" --> ERR["Error PSO
+ log warning"] + end + + BuildTime --> ShippedGame + + style BuildTime fill:#1a1a2e,stroke:#0f3460,color:#eee + style ShippedGame fill:#16213e,stroke:#533483,color:#eee +``` + +The beauty: **the same `ShaderLibrary` and `PipelineLibrary` code runs in both editor and shipped game**. The only difference is whether `IShaderCompilationBridge` is the editor implementation or the runtime stub. + +--- + +## Summary of Key Design Decisions + +| # | Decision | Rationale | +|---|----------|-----------| +| 1 | Stable GUID identity, not content hash | Shader struct never needs recreation on edit | +| 2 | Content-addressed cache | Deduplication, easy invalidation, git-friendly | +| 3 | Manifest as the bridge | Decouples identity from compiled output cleanly | +| 4 | Keep old PSO during recompile | Zero flicker, seamless UX | +| 5 | Separate compiler process | Crash isolation, independent resource budget | +| 6 | Two-method interface in runtime | Minimal coupling, easy to stub for shipped game | +| 7 | Deferred PSO release via fence | Prevents GPU use-after-free | +| 8 | Same code path for editor + shipped | Fewer bugs, one pipeline to maintain | diff --git a/docs/superpowers/plans/2026-03-28-dock-layout.md b/docs/superpowers/plans/2026-03-28-dock-layout.md deleted file mode 100644 index f10dafa..0000000 --- a/docs/superpowers/plans/2026-03-28-dock-layout.md +++ /dev/null @@ -1,669 +0,0 @@ -# DockLayout Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Create a fully-featured, dynamically splittable, multi-window docking layout system using WinUI 3. - -**Architecture:** A C# data model (node tree) drives the recursive generation of XAML `Grid` and `NavigationTabView` controls. Drag and drop events mutate the node tree, and the UI automatically reflects the changes. - -**Tech Stack:** C#, WinUI 3, CommunityToolkit.Mvvm (for ObservableObject). - ---- - -### Task 1: Create Core Data Models - -**Files:** -- Create: `src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockNode.cs` -- Create: `src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockGroupNode.cs` -- Create: `src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockPanelNode.cs` - -- [ ] **Step 1: Write `DockNode` base class** - -```csharp -using CommunityToolkit.Mvvm.ComponentModel; - -namespace Ghost.Editor.Core.Controls.Internal.Docking; - -public abstract partial class DockNode : ObservableObject -{ - [ObservableProperty] - private DockGroupNode? _parent; -} -``` - -- [ ] **Step 2: Write `DockGroupNode` class** - -```csharp -using System.Collections.ObjectModel; -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.Core.Controls.Internal.Docking; - -public partial class DockGroupNode : DockNode -{ - [ObservableProperty] - private Orientation _orientation = Orientation.Horizontal; - - public ObservableCollection Children { get; } = new(); - - public void AddChild(DockNode node) - { - node.Parent = this; - Children.Add(node); - } - - public void RemoveChild(DockNode node) - { - if (Children.Remove(node)) - { - node.Parent = null; - } - } -} -``` - -- [ ] **Step 3: Write `DockPanelNode` class** - -```csharp -using System.Collections.ObjectModel; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace Ghost.Editor.Core.Controls.Internal.Docking; - -public partial class DockPanelNode : DockNode -{ - public ObservableCollection Items { get; } = new(); - - [ObservableProperty] - private int _selectedIndex = -1; - - [ObservableProperty] - private object? _selectedItem; -} -``` - -- [ ] **Step 4: Commit** - -```bash -git add src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/* -git commit -m "feat(dock): add core data models for docking system" -``` - ---- - -### Task 2: Implement Tree Renderer in XAML - -**Files:** -- Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.cs` -- Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.xaml` - -- [ ] **Step 1: Add DependencyProperty for Root in DockLayout.cs** - -```csharp -using Ghost.Editor.Core.Controls.Internal.Docking; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.View.Controls; - -public sealed partial class DockLayout : Control -{ - public DockLayout() - { - DefaultStyleKey = typeof(DockLayout); - } - - public DockGroupNode? Root - { - get => (DockGroupNode?)GetValue(RootProperty); - set => SetValue(RootProperty, value); - } - - public static readonly DependencyProperty RootProperty = - DependencyProperty.Register("Root", typeof(DockGroupNode), typeof(DockLayout), new PropertyMetadata(null, OnRootChanged)); - - private static void OnRootChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is DockLayout layout) - { - layout.RenderTree(); - } - } - - private void RenderTree() - { - if (GetTemplateChild("PART_RootGrid") is Grid rootGrid) - { - rootGrid.Children.Clear(); - if (Root != null) - { - var ui = CreateUIForNode(Root); - rootGrid.Children.Add(ui); - } - } - } - - private UIElement CreateUIForNode(DockNode node) - { - if (node is DockGroupNode groupNode) - { - // Simple visualizer for now, full grid logic in next step - var grid = new Grid(); - foreach (var child in groupNode.Children) - { - grid.Children.Add(CreateUIForNode(child)); - } - return grid; - } - else if (node is DockPanelNode panelNode) - { - return new Ghost.Editor.Controls.NavigationTabView - { - ItemsSource = panelNode.Items, - HorizontalAlignment = HorizontalAlignment.Stretch, - VerticalAlignment = VerticalAlignment.Stretch - }; - } - return new Grid(); // Fallback - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - RenderTree(); - } -} -``` - -- [ ] **Step 2: Define ControlTemplate in DockLayout.xaml** - -```xml - - - - -``` - -- [ ] **Step 3: Commit** - -```bash -git add src/Editor/Ghost.Editor/View/Controls/DockLayout.* -git commit -m "feat(dock): implement basic recursive tree renderer" -``` - ---- - -### Task 3: Implement DockGroupNode Grid Builder - -**Files:** -- Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.cs` - -- [ ] **Step 1: Replace `CreateUIForNode` Group logic to generate Columns/Rows and GridSplitters** - -```csharp - private UIElement CreateUIForNode(DockNode node) - { - if (node is DockGroupNode groupNode) - { - var grid = new Grid(); - bool isHorizontal = groupNode.Orientation == Orientation.Horizontal; - int childCount = groupNode.Children.Count; - - for (int i = 0; i < childCount; i++) - { - var childNode = groupNode.Children[i]; - var childUI = CreateUIForNode(childNode); - - if (isHorizontal) - { - grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); - Grid.SetColumn((FrameworkElement)childUI, i * 2); - } - else - { - grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); - Grid.SetRow((FrameworkElement)childUI, i * 2); - } - - grid.Children.Add(childUI); - - // Add GridSplitter between children - if (i < childCount - 1) - { - if (isHorizontal) - { - grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - var splitter = new CommunityToolkit.WinUI.Controls.GridSplitter { Width = 4, HorizontalAlignment = HorizontalAlignment.Center, ResizeDirection = CommunityToolkit.WinUI.Controls.GridSplitter.GridResizeDirection.Columns }; - Grid.SetColumn(splitter, (i * 2) + 1); - grid.Children.Add(splitter); - } - else - { - grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); - var splitter = new CommunityToolkit.WinUI.Controls.GridSplitter { Height = 4, VerticalAlignment = VerticalAlignment.Center, ResizeDirection = CommunityToolkit.WinUI.Controls.GridSplitter.GridResizeDirection.Rows }; - Grid.SetRow(splitter, (i * 2) + 1); - grid.Children.Add(splitter); - } - } - } - - // Listen to CollectionChanged to trigger re-render - groupNode.Children.CollectionChanged -= GroupNode_Children_CollectionChanged; - groupNode.Children.CollectionChanged += GroupNode_Children_CollectionChanged; - - return grid; - } - else if (node is DockPanelNode panelNode) - { - var tabView = new Ghost.Editor.Controls.NavigationTabView - { - TabItemsSource = panelNode.Items, - HorizontalAlignment = HorizontalAlignment.Stretch, - VerticalAlignment = VerticalAlignment.Stretch, - CanDragTabs = true, - AllowDrop = true, - Tag = panelNode // Store reference to data node - }; - return tabView; - } - return new Grid(); - } - - private void GroupNode_Children_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) - { - // For MVP, just re-render the whole tree when a group changes structure - RenderTree(); - } -``` - -- [ ] **Step 2: Commit** - -```bash -git add src/Editor/Ghost.Editor/View/Controls/DockLayout.cs -git commit -m "feat(dock): implement grid and gridsplitter generation for groups" -``` - ---- - -### Task 4: Setup Visual Drop Target Overlay - -**Files:** -- Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.xaml` -- Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.cs` - -- [ ] **Step 1: Add Drop Overlay to ControlTemplate** - -```xml - - - - - - -``` - -- [ ] **Step 2: Add Fields and ApplyTemplate logic in DockLayout.cs** - -```csharp - private Border? _dropTargetOverlay; - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - _dropTargetOverlay = GetTemplateChild("PART_DropTargetOverlay") as Border; - RenderTree(); - } - - // Helper enum for later - public enum DockPosition { Center, Top, Bottom, Left, Right, None } -``` - -- [ ] **Step 3: Commit** - -```bash -git add src/Editor/Ghost.Editor/View/Controls/DockLayout.* -git commit -m "feat(dock): add visual drop target overlay" -``` - ---- - -### Task 5: Implement Drag and Drop Calculations (Highlighting) - -**Files:** -- Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.cs` - -- [ ] **Step 1: Attach TabView Drag Events in `CreateUIForNode`** - -```csharp - else if (node is DockPanelNode panelNode) - { - var tabView = new Ghost.Editor.Controls.NavigationTabView - { - TabItemsSource = panelNode.Items, - HorizontalAlignment = HorizontalAlignment.Stretch, - VerticalAlignment = VerticalAlignment.Stretch, - CanDragTabs = true, - AllowDrop = true, - Tag = panelNode // Store reference to data node - }; - - tabView.DragOver += TabView_DragOver; - tabView.DragLeave += TabView_DragLeave; - tabView.Drop += TabView_Drop; - tabView.TabDragStarting += TabView_TabDragStarting; - - return tabView; - } -``` - -- [ ] **Step 2: Implement Drag Handling Logic** - -```csharp - private object? _draggedItem; - private DockPanelNode? _sourceNode; - private DockPosition _currentDropPosition = DockPosition.None; - - private void TabView_TabDragStarting(Microsoft.UI.Xaml.Controls.TabView sender, Microsoft.UI.Xaml.Controls.TabViewTabDragStartingEventArgs args) - { - _draggedItem = args.Item; - _sourceNode = sender.Tag as DockPanelNode; - args.Data.Properties.Add("DockTab", _draggedItem); // Identify our drag - } - - private void TabView_DragOver(object sender, DragEventArgs e) - { - if (e.DataView.Properties.ContainsKey("DockTab") && sender is FrameworkElement targetElement) - { - e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; - - var position = e.GetPosition(targetElement); - double width = targetElement.ActualWidth; - double height = targetElement.ActualHeight; - - double edgeThreshold = 0.25; // 25% of edge triggers split - - if (position.X < width * edgeThreshold) _currentDropPosition = DockPosition.Left; - else if (position.X > width * (1 - edgeThreshold)) _currentDropPosition = DockPosition.Right; - else if (position.Y < height * edgeThreshold) _currentDropPosition = DockPosition.Top; - else if (position.Y > height * (1 - edgeThreshold)) _currentDropPosition = DockPosition.Bottom; - else _currentDropPosition = DockPosition.Center; - - UpdateDropOverlay(targetElement, _currentDropPosition); - } - } - - private void TabView_DragLeave(object sender, DragEventArgs e) - { - if (_dropTargetOverlay != null) - { - _dropTargetOverlay.Visibility = Visibility.Collapsed; - _currentDropPosition = DockPosition.None; - } - } - - private void UpdateDropOverlay(FrameworkElement targetElement, DockPosition position) - { - if (_dropTargetOverlay == null) return; - if (position == DockPosition.None) - { - _dropTargetOverlay.Visibility = Visibility.Collapsed; - return; - } - - var transform = targetElement.TransformToVisual(this); - var bounds = transform.TransformBounds(new Windows.Foundation.Rect(0, 0, targetElement.ActualWidth, targetElement.ActualHeight)); - - _dropTargetOverlay.Visibility = Visibility.Visible; - _dropTargetOverlay.Width = double.NaN; - _dropTargetOverlay.Height = double.NaN; - - switch (position) - { - case DockPosition.Center: - _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top, ActualWidth - bounds.Right, ActualHeight - bounds.Bottom); - break; - case DockPosition.Left: - _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top, ActualWidth - (bounds.Left + bounds.Width / 2), ActualHeight - bounds.Bottom); - break; - case DockPosition.Right: - _dropTargetOverlay.Margin = new Thickness(bounds.Left + bounds.Width / 2, bounds.Top, ActualWidth - bounds.Right, ActualHeight - bounds.Bottom); - break; - case DockPosition.Top: - _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top, ActualWidth - bounds.Right, ActualHeight - (bounds.Top + bounds.Height / 2)); - break; - case DockPosition.Bottom: - _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top + bounds.Height / 2, ActualWidth - bounds.Right, ActualHeight - bounds.Bottom); - break; - } - } -``` - -- [ ] **Step 3: Commit** - -```bash -git add src/Editor/Ghost.Editor/View/Controls/DockLayout.cs -git commit -m "feat(dock): implement drop highlight calculations" -``` - ---- - -### Task 6: Implement Dropping (Data Tree Mutation) - -**Files:** -- Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.cs` - -- [ ] **Step 1: Implement `TabView_Drop` logic** - -```csharp - private void TabView_Drop(object sender, DragEventArgs e) - { - if (_dropTargetOverlay != null) _dropTargetOverlay.Visibility = Visibility.Collapsed; - - if (_draggedItem == null || _sourceNode == null || !(sender is FrameworkElement targetElement) || !(targetElement.Tag is DockPanelNode targetNode)) - return; - - if (_sourceNode == targetNode && _currentDropPosition == DockPosition.Center) - return; // Reordering within same tab is handled natively by TabView - - // 1. Remove from source - _sourceNode.Items.Remove(_draggedItem); - CleanupEmptyNodes(_sourceNode); - - // 2. Add to target - if (_currentDropPosition == DockPosition.Center) - { - targetNode.Items.Add(_draggedItem); - } - else - { - // Split scenario - var parentGroup = targetNode.Parent; - if (parentGroup != null) - { - int index = parentGroup.Children.IndexOf(targetNode); - parentGroup.Children.RemoveAt(index); - - var newGroup = new DockGroupNode - { - Orientation = (_currentDropPosition == DockPosition.Left || _currentDropPosition == DockPosition.Right) ? Orientation.Horizontal : Orientation.Vertical - }; - - var newPanel = new DockPanelNode(); - newPanel.Items.Add(_draggedItem); - - if (_currentDropPosition == DockPosition.Left || _currentDropPosition == DockPosition.Top) - { - newGroup.AddChild(newPanel); - newGroup.AddChild(targetNode); - } - else - { - newGroup.AddChild(targetNode); - newGroup.AddChild(newPanel); - } - - parentGroup.Children.Insert(index, newGroup); - } - } - - _draggedItem = null; - _sourceNode = null; - _currentDropPosition = DockPosition.None; - } - - private void CleanupEmptyNodes(DockPanelNode panelNode) - { - if (panelNode.Items.Count > 0) return; - - var parentGroup = panelNode.Parent; - if (parentGroup != null) - { - parentGroup.RemoveChild(panelNode); - - // If group only has 1 child left, collapse it - if (parentGroup.Children.Count == 1) - { - var onlyChild = parentGroup.Children[0]; - var grandParent = parentGroup.Parent; - if (grandParent != null) - { - int index = grandParent.Children.IndexOf(parentGroup); - parentGroup.RemoveChild(onlyChild); - grandParent.Children.RemoveAt(index); - grandParent.Children.Insert(index, onlyChild); - } - else if (parentGroup == Root) - { - // If root is collapsing, the only child becomes the new root - parentGroup.RemoveChild(onlyChild); - if (onlyChild is DockGroupNode newRootGroup) - { - Root = newRootGroup; - } - else - { - // Wrap panel in a new group to keep Root as a GroupNode - var wrapperGroup = new DockGroupNode(); - wrapperGroup.AddChild(onlyChild); - Root = wrapperGroup; - } - } - } - } - } -``` - -- [ ] **Step 2: Commit** - -```bash -git add src/Editor/Ghost.Editor/View/Controls/DockLayout.cs -git commit -m "feat(dock): implement tree mutation on drop and empty node cleanup" -``` - ---- - -### Task 7: Implement Window Tear-Off (TabDroppedOutside) - -**Files:** -- Create: `src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml` -- Create: `src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs` -- Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.cs` - -- [ ] **Step 1: Create `DockWindow` wrapper** - -`DockWindow.xaml`: -```xml - - - - - - -``` - -`DockWindow.xaml.cs`: -```csharp -using Ghost.Editor.Core.Controls.Internal.Docking; -using WinUIEx; - -namespace Ghost.Editor.View.Windows; - -public sealed partial class DockWindow : WindowEx -{ - public DockWindow(object initialTabContent) - { - InitializeComponent(); - - // Setup initial single panel layout - var rootGroup = new DockGroupNode(); - var panel = new DockPanelNode(); - panel.Items.Add(initialTabContent); - rootGroup.AddChild(panel); - - PART_DockLayout.Root = rootGroup; - - // Optional: Titlebar setup etc. - } -} -``` - -- [ ] **Step 2: Handle `TabDroppedOutside` in `DockLayout.cs`** - -Modify `CreateUIForNode` in `DockLayout.cs`: -```csharp - // ... inside CreateUIForNode for DockPanelNode ... - tabView.TabDragStarting += TabView_TabDragStarting; - tabView.TabDroppedOutside += TabView_TabDroppedOutside; // NEW - - return tabView; -``` - -Add event handler: -```csharp - private void TabView_TabDroppedOutside(Microsoft.UI.Xaml.Controls.TabView sender, Microsoft.UI.Xaml.Controls.TabViewTabDroppedOutsideEventArgs args) - { - if (_sourceNode != null && _draggedItem != null) - { - // Remove from current tree - _sourceNode.Items.Remove(_draggedItem); - CleanupEmptyNodes(_sourceNode); - - // Create new window - var newWindow = new Ghost.Editor.View.Windows.DockWindow(_draggedItem); - newWindow.Activate(); - - _draggedItem = null; - _sourceNode = null; - _currentDropPosition = DockPosition.None; - if (_dropTargetOverlay != null) _dropTargetOverlay.Visibility = Visibility.Collapsed; - } - } -``` - -- [ ] **Step 3: Commit** - -```bash -git add src/Editor/Ghost.Editor/View/Windows/DockWindow.* src/Editor/Ghost.Editor/View/Controls/DockLayout.cs -git commit -m "feat(dock): implement tab tear-off to new window" -``` diff --git a/docs/superpowers/plans/2026-03-28-docking-layout.md b/docs/superpowers/plans/2026-03-28-docking-layout.md deleted file mode 100644 index ef4b252..0000000 --- a/docs/superpowers/plans/2026-03-28-docking-layout.md +++ /dev/null @@ -1,724 +0,0 @@ -# Docking Layout Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Build a custom WinUI 3 docking layout system for GhostEngine's editor with Unity/Blender-style region highlighting and dynamic tab creation. - -**Architecture:** A UI-driven approach where custom controls (`DockingLayout`, `DockPanel`, `DockGroup`, `DockDocument`) manage their own state and visual tree. Drag-and-drop manipulates the visual tree directly. - -**Tech Stack:** C#, WinUI 3, Windows App SDK - ---- - -### Task 1: Core Enums and Base Classes - -**Files:** -- Create: `src/Editor/Ghost.Editor/View/Controls/Docking/Enums.cs` -- Create: `src/Editor/Ghost.Editor/View/Controls/Docking/DockModule.cs` -- Create: `src/Editor/Ghost.Editor/View/Controls/Docking/DockContainer.cs` - -- [ ] **Step 1: Create Enums** -Create `src/Editor/Ghost.Editor/View/Controls/Docking/Enums.cs`: -```csharp -namespace Ghost.Editor.View.Controls.Docking; - -public enum DockTarget -{ - Center, - Left, - Right, - Top, - Bottom -} -``` - -- [ ] **Step 2: Create DockModule base class** -Create `src/Editor/Ghost.Editor/View/Controls/Docking/DockModule.cs`: -```csharp -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.View.Controls.Docking; - -public abstract class DockModule : Control -{ - public DockContainer? Owner { get; internal set; } - public DockingLayout? Root { get; internal set; } - - public void Detach() - { - Owner?.Children.Remove(this); - Owner = null; - } -} -``` - -- [ ] **Step 3: Create DockContainer base class** -Create `src/Editor/Ghost.Editor/View/Controls/Docking/DockContainer.cs`: -```csharp -using System.Collections.ObjectModel; - -namespace Ghost.Editor.View.Controls.Docking; - -public abstract class DockContainer : DockModule -{ - public ObservableCollection Children { get; } = new(); - - protected DockContainer() - { - Children.CollectionChanged += OnChildrenChanged; - } - - private void OnChildrenChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) - { - if (e.OldItems != null) - { - foreach (DockModule module in e.OldItems) - { - module.Owner = null; - } - } - - if (e.NewItems != null) - { - foreach (DockModule module in e.NewItems) - { - module.Owner = this; - module.Root = Root; - } - } - - OnChildrenUpdated(); - } - - protected virtual void OnChildrenUpdated() { } -} -``` - -- [ ] **Step 4: Commit** -```bash -git add src/Editor/Ghost.Editor/View/Controls/Docking/Enums.cs src/Editor/Ghost.Editor/View/Controls/Docking/DockModule.cs src/Editor/Ghost.Editor/View/Controls/Docking/DockContainer.cs -git commit -m "feat(docking): add core enums and base classes" -``` - ---- - -### Task 2: DockDocument and DockGroup - -**Files:** -- Create: `src/Editor/Ghost.Editor/View/Controls/Docking/DockDocument.cs` -- Create: `src/Editor/Ghost.Editor/View/Controls/Docking/DockGroup.cs` -- Create: `src/Editor/Ghost.Editor/View/Controls/Docking/DockGroup.xaml` - -- [ ] **Step 1: Create DockDocument** -Create `src/Editor/Ghost.Editor/View/Controls/Docking/DockDocument.cs`: -```csharp -using Microsoft.UI.Xaml; - -namespace Ghost.Editor.View.Controls.Docking; - -public class DockDocument : DockModule -{ - public static readonly DependencyProperty TitleProperty = DependencyProperty.Register( - nameof(Title), typeof(string), typeof(DockDocument), new PropertyMetadata(string.Empty)); - - public static readonly DependencyProperty ContentProperty = DependencyProperty.Register( - nameof(Content), typeof(object), typeof(DockDocument), new PropertyMetadata(null)); - - public string Title - { - get => (string)GetValue(TitleProperty); - set => SetValue(TitleProperty, value); - } - - public object Content - { - get => GetValue(ContentProperty); - set => SetValue(ContentProperty, value); - } - - public DockDocument() - { - DefaultStyleKey = typeof(DockDocument); - } -} -``` - -- [ ] **Step 2: Create DockGroup XAML** -Create `src/Editor/Ghost.Editor/View/Controls/Docking/DockGroup.xaml`: -```xml - - - - -``` - -- [ ] **Step 3: Create DockGroup Code-Behind** -Create `src/Editor/Ghost.Editor/View/Controls/Docking/DockGroup.cs`: -```csharp -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.View.Controls.Docking; - -[TemplatePart(Name = "PART_TabView", Type = typeof(TabView))] -public class DockGroup : DockContainer -{ - private TabView? _tabView; - - public DockGroup() - { - DefaultStyleKey = typeof(DockGroup); - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - _tabView = GetTemplateChild("PART_TabView") as TabView; - UpdateTabs(); - } - - protected override void OnChildrenUpdated() - { - UpdateTabs(); - } - - private void UpdateTabs() - { - if (_tabView == null) return; - - _tabView.TabItems.Clear(); - foreach (var child in Children) - { - if (child is DockDocument doc) - { - var tabItem = new TabViewItem - { - Header = doc.Title, - Content = doc.Content, - Tag = doc - }; - _tabView.TabItems.Add(tabItem); - } - } - } -} -``` - -- [ ] **Step 4: Commit** -```bash -git add src/Editor/Ghost.Editor/View/Controls/Docking/DockDocument.cs src/Editor/Ghost.Editor/View/Controls/Docking/DockGroup.cs src/Editor/Ghost.Editor/View/Controls/Docking/DockGroup.xaml -git commit -m "feat(docking): add DockDocument and DockGroup" -``` - ---- - -### Task 3: DockPanel - -**Files:** -- Create: `src/Editor/Ghost.Editor/View/Controls/Docking/DockPanel.cs` -- Create: `src/Editor/Ghost.Editor/View/Controls/Docking/DockPanel.xaml` - -- [ ] **Step 1: Create DockPanel XAML** -Create `src/Editor/Ghost.Editor/View/Controls/Docking/DockPanel.xaml`: -```xml - - - - -``` - -- [ ] **Step 2: Create DockPanel Code-Behind** -Create `src/Editor/Ghost.Editor/View/Controls/Docking/DockPanel.cs`: -```csharp -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using CommunityToolkit.WinUI.Controls; - -namespace Ghost.Editor.View.Controls.Docking; - -[TemplatePart(Name = "PART_Grid", Type = typeof(Grid))] -public class DockPanel : DockContainer -{ - public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( - nameof(Orientation), typeof(Orientation), typeof(DockPanel), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged)); - - public Orientation Orientation - { - get => (Orientation)GetValue(OrientationProperty); - set => SetValue(OrientationProperty, value); - } - - private Grid? _grid; - - public DockPanel() - { - DefaultStyleKey = typeof(DockPanel); - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - _grid = GetTemplateChild("PART_Grid") as Grid; - UpdateLayoutStructure(); - } - - protected override void OnChildrenUpdated() - { - UpdateLayoutStructure(); - } - - private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - ((DockPanel)d).UpdateLayoutStructure(); - } - - private void UpdateLayoutStructure() - { - if (_grid == null) return; - - _grid.Children.Clear(); - _grid.RowDefinitions.Clear(); - _grid.ColumnDefinitions.Clear(); - - if (Children.Count == 0) return; - - if (Orientation == Orientation.Horizontal) - { - for (int i = 0; i < Children.Count; i++) - { - _grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); - var child = Children[i]; - Grid.SetColumn(child, i * 2); - _grid.Children.Add(child); - - if (i < Children.Count - 1) - { - _grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Columns, Width = 4 }; - Grid.SetColumn(splitter, i * 2 + 1); - _grid.Children.Add(splitter); - } - } - } - else - { - for (int i = 0; i < Children.Count; i++) - { - _grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); - var child = Children[i]; - Grid.SetRow(child, i * 2); - _grid.Children.Add(child); - - if (i < Children.Count - 1) - { - _grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); - var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Rows, Height = 4 }; - Grid.SetRow(splitter, i * 2 + 1); - _grid.Children.Add(splitter); - } - } - } - } -} -``` - -- [ ] **Step 3: Commit** -```bash -git add src/Editor/Ghost.Editor/View/Controls/Docking/DockPanel.cs src/Editor/Ghost.Editor/View/Controls/Docking/DockPanel.xaml -git commit -m "feat(docking): add DockPanel" -``` - ---- - -### Task 4: DockRegionHighlight and DockingLayout - -**Files:** -- Create: `src/Editor/Ghost.Editor/View/Controls/Docking/DockRegionHighlight.cs` -- Create: `src/Editor/Ghost.Editor/View/Controls/Docking/DockRegionHighlight.xaml` -- Create: `src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.cs` -- Create: `src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.xaml` - -- [ ] **Step 1: Create DockRegionHighlight XAML** -Create `src/Editor/Ghost.Editor/View/Controls/Docking/DockRegionHighlight.xaml`: -```xml - - - - -``` - -- [ ] **Step 2: Create DockRegionHighlight Code-Behind** -Create `src/Editor/Ghost.Editor/View/Controls/Docking/DockRegionHighlight.cs`: -```csharp -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.View.Controls.Docking; - -public class DockRegionHighlight : Control -{ - public DockRegionHighlight() - { - DefaultStyleKey = typeof(DockRegionHighlight); - IsHitTestVisible = false; - } -} -``` - -- [ ] **Step 3: Create DockingLayout XAML** -Create `src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.xaml`: -```xml - - - - -``` - -- [ ] **Step 4: Create DockingLayout Code-Behind** -Create `src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.cs`: -```csharp -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; - -namespace Ghost.Editor.View.Controls.Docking; - -[TemplatePart(Name = "PART_Content", Type = typeof(ContentPresenter))] -[TemplatePart(Name = "PART_OverlayCanvas", Type = typeof(Canvas))] -[TemplatePart(Name = "PART_Highlight", Type = typeof(DockRegionHighlight))] -public class DockingLayout : Control -{ - public static readonly DependencyProperty RootPanelProperty = DependencyProperty.Register( - nameof(RootPanel), typeof(DockPanel), typeof(DockingLayout), new PropertyMetadata(null, OnRootPanelChanged)); - - public DockPanel? RootPanel - { - get => (DockPanel?)GetValue(RootPanelProperty); - set => SetValue(RootPanelProperty, value); - } - - private Canvas? _overlayCanvas; - private DockRegionHighlight? _highlight; - - public DockingLayout() - { - DefaultStyleKey = typeof(DockingLayout); - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - _overlayCanvas = GetTemplateChild("PART_OverlayCanvas") as Canvas; - _highlight = GetTemplateChild("PART_Highlight") as DockRegionHighlight; - } - - private static void OnRootPanelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is DockingLayout layout && e.NewValue is DockPanel panel) - { - panel.Root = layout; - } - } - - public void AddDocument(DockDocument document, DockTarget target, DockGroup? targetGroup = null) - { - if (RootPanel == null) - { - RootPanel = new DockPanel(); - } - - if (targetGroup == null) - { - if (RootPanel.Children.Count == 0) - { - var group = new DockGroup(); - group.Children.Add(document); - RootPanel.Children.Add(group); - return; - } - targetGroup = RootPanel.Children[0] as DockGroup; - } - - if (targetGroup != null) - { - targetGroup.Children.Add(document); - } - } -} -``` - -- [ ] **Step 5: Commit** -```bash -git add src/Editor/Ghost.Editor/View/Controls/Docking/DockRegionHighlight.cs src/Editor/Ghost.Editor/View/Controls/Docking/DockRegionHighlight.xaml src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.cs src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.xaml -git commit -m "feat(docking): add DockRegionHighlight and DockingLayout" -``` - ---- - -### Task 5: Drag and Drop Logic - -**Files:** -- Modify: `src/Editor/Ghost.Editor/View/Controls/Docking/DockGroup.cs` -- Modify: `src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.cs` - -- [ ] **Step 1: Implement Drag and Drop in DockGroup** -Modify `src/Editor/Ghost.Editor/View/Controls/Docking/DockGroup.cs` to handle drag events on the TabView: -```csharp -// Add to OnApplyTemplate: -if (_tabView != null) -{ - _tabView.TabDragStarting += OnTabDragStarting; - _tabView.TabDroppedOutside += OnTabDroppedOutside; - _tabView.DragOver += OnDragOver; - _tabView.Drop += OnDrop; - _tabView.DragLeave += OnDragLeave; -} - -// Add methods: -private void OnTabDragStarting(TabView sender, TabViewTabDragStartingEventArgs args) -{ - if (args.Tab.Tag is DockDocument doc) - { - args.Data.Properties.Add("DockDocument", doc); - doc.Detach(); - } -} - -private void OnTabDroppedOutside(TabView sender, TabViewTabDroppedOutsideEventArgs args) -{ - if (args.Tab.Tag is DockDocument doc) - { - Root?.CreateFloatingWindow(doc); - } -} - -private void OnDragOver(object sender, DragEventArgs e) -{ - if (e.DataView.Properties.ContainsKey("DockDocument")) - { - e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; - Root?.ShowHighlight(this, e.GetPosition(this)); - } -} - -private void OnDrop(object sender, DragEventArgs e) -{ - if (e.DataView.Properties.TryGetValue("DockDocument", out var obj) && obj is DockDocument doc) - { - Root?.HandleDrop(doc, this, e.GetPosition(this)); - } -} - -private void OnDragLeave(object sender, DragEventArgs e) -{ - Root?.HideHighlight(); -} -``` - -- [ ] **Step 2: Implement Highlight and Drop in DockingLayout** -Modify `src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.cs` to add `ShowHighlight`, `HideHighlight`, `HandleDrop`, and `CreateFloatingWindow`: -```csharp -// Add methods: -internal void ShowHighlight(DockGroup targetGroup, Windows.Foundation.Point position) -{ - if (_highlight == null || _overlayCanvas == null) return; - - _highlight.Visibility = Visibility.Visible; - var target = CalculateDockTarget(targetGroup, position); - - // Calculate rect based on target (simplified for brevity, needs actual math based on targetGroup's ActualWidth/Height) - double width = targetGroup.ActualWidth; - double height = targetGroup.ActualHeight; - double x = 0, y = 0; - - switch (target) - { - case DockTarget.Left: width /= 2; break; - case DockTarget.Right: width /= 2; x = width; break; - case DockTarget.Top: height /= 2; break; - case DockTarget.Bottom: height /= 2; y = height; break; - case DockTarget.Center: break; - } - - var transform = targetGroup.TransformToVisual(_overlayCanvas); - var point = transform.TransformPoint(new Windows.Foundation.Point(x, y)); - - Canvas.SetLeft(_highlight, point.X); - Canvas.SetTop(_highlight, point.Y); - _highlight.Width = width; - _highlight.Height = height; -} - -internal void HideHighlight() -{ - if (_highlight != null) _highlight.Visibility = Visibility.Collapsed; -} - -internal void HandleDrop(DockDocument doc, DockGroup targetGroup, Windows.Foundation.Point position) -{ - HideHighlight(); - var target = CalculateDockTarget(targetGroup, position); - - if (target == DockTarget.Center) - { - targetGroup.Children.Add(doc); - } - else - { - // Split logic: create new DockPanel, move targetGroup and doc into it - var parentPanel = targetGroup.Owner as DockPanel; - if (parentPanel != null) - { - int index = parentPanel.Children.IndexOf(targetGroup); - targetGroup.Detach(); - - var newPanel = new DockPanel { Orientation = (target == DockTarget.Left || target == DockTarget.Right) ? Orientation.Horizontal : Orientation.Vertical }; - var newGroup = new DockGroup(); - newGroup.Children.Add(doc); - - if (target == DockTarget.Left || target == DockTarget.Top) - { - newPanel.Children.Add(newGroup); - newPanel.Children.Add(targetGroup); - } - else - { - newPanel.Children.Add(targetGroup); - newPanel.Children.Add(newGroup); - } - - parentPanel.Children.Insert(index, newPanel); - } - } -} - -private DockTarget CalculateDockTarget(DockGroup group, Windows.Foundation.Point position) -{ - double w = group.ActualWidth; - double h = group.ActualHeight; - double x = position.X; - double y = position.Y; - - if (x < w * 0.25) return DockTarget.Left; - if (x > w * 0.75) return DockTarget.Right; - if (y < h * 0.25) return DockTarget.Top; - if (y > h * 0.75) return DockTarget.Bottom; - return DockTarget.Center; -} - -internal void CreateFloatingWindow(DockDocument doc) -{ - // To be implemented in Task 6 -} -``` - -- [ ] **Step 3: Commit** -```bash -git add src/Editor/Ghost.Editor/View/Controls/Docking/DockGroup.cs src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.cs -git commit -m "feat(docking): implement drag and drop logic" -``` - ---- - -### Task 6: Floating Window - -**Files:** -- Create: `src/Editor/Ghost.Editor/View/Controls/Docking/FloatingWindow.cs` -- Modify: `src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.cs` - -- [ ] **Step 1: Create FloatingWindow** -Create `src/Editor/Ghost.Editor/View/Controls/Docking/FloatingWindow.cs`: -```csharp -using Microsoft.UI.Xaml; - -namespace Ghost.Editor.View.Controls.Docking; - -public class FloatingWindow : Window -{ - public FloatingWindow(DockDocument document) - { - var layout = new DockingLayout(); - var group = new DockGroup(); - group.Children.Add(document); - - var panel = new DockPanel(); - panel.Children.Add(group); - layout.RootPanel = panel; - - Content = layout; - - // Basic window setup - AppWindow.Resize(new Windows.Graphics.SizeInt32(800, 600)); - } -} -``` - -- [ ] **Step 2: Update DockingLayout** -Modify `src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.cs` to implement `CreateFloatingWindow`: -```csharp -internal void CreateFloatingWindow(DockDocument doc) -{ - var window = new FloatingWindow(doc); - window.Activate(); -} -``` - -- [ ] **Step 3: Commit** -```bash -git add src/Editor/Ghost.Editor/View/Controls/Docking/FloatingWindow.cs src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.cs -git commit -m "feat(docking): add floating window support" -``` diff --git a/docs/superpowers/specs/2026-03-28-dock-layout-design.md b/docs/superpowers/specs/2026-03-28-dock-layout-design.md deleted file mode 100644 index 5846f47..0000000 --- a/docs/superpowers/specs/2026-03-28-dock-layout-design.md +++ /dev/null @@ -1,75 +0,0 @@ -# DockLayout System Design - -## Purpose -To create a fully-featured docking layout system for the Ghost Engine Editor using WinUI 3, supporting tab tearing, window popping, and dynamic splitting of regions in a style similar to Unity or Blender. - -## Architecture - -The DockLayout will be entirely driven by a C# data model that represents a tree of nodes. The UI (`DockLayout` control) will observe this tree and recursively generate the corresponding XAML `Grid` and `TabView` elements. - -### Core Data Model (The Node Tree) - -```csharp -public abstract class DockNode : INotifyPropertyChanged { } - -// Represents a split region (Grid) -public class DockGroupNode : DockNode -{ - public Orientation Orientation { get; set; } // Horizontal or Vertical - public ObservableCollection Children { get; } - public ObservableCollection Sizes { get; } // Replaces Ratios for better WinUI 3 Grid binding -} - -// Represents a leaf node containing a TabView -public class DockPanelNode : DockNode -{ - // The items shown in the TabView - public ObservableCollection Items { get; } - public int SelectedIndex { get; set; } -} -``` - -### Visual Components - -1. **`DockLayout` (Control)** - * The root control. - * Takes a `DockNode` (usually a `DockGroupNode`) as its `Root`. - * Listens to Drag/Drop events to render the transparent drop target overlay over the layout. - -2. **Node Renderers** - * A recursive template selector or code-behind builder that converts `DockGroupNode` into a `Grid` with `GridSplitter`s. - * Converts `DockPanelNode` into a `NavigationTabView` (or standard `TabView` with customized drag behaviors). - -3. **`DockDropTarget` (Visual Overlay)** - * A simple XAML structure (e.g., a colored `Border` with opacity) that highlights a portion of a `DockPanelNode` based on mouse position during a drag operation (Left/Right/Top/Bottom 25% for splitting, Center 50% for merging). - -## Interactions & Data Flow - -### 1. Internal Dragging (Within the same window) -* User starts dragging a tab. -* The `DockLayout` tracks the mouse pointer `DragOver` events. -* It determines which `DockPanelNode` the mouse is currently hovering over. -* It calculates relative coordinates to show the Unity-style drop highlight. -* On **Drop**: - * If dropped in the **center**: The tab object is moved from its source `DockPanelNode.Items` to the target `DockPanelNode.Items`. - * If dropped on an **edge** (e.g., Right): The target `DockPanelNode` is removed from its parent `DockGroupNode`. A new `DockGroupNode` (Horizontal) is created to replace it. The target node and a *new* `DockPanelNode` (containing the dragged tab) are added as children to this new group. - -### 2. Window Tear-Off (Full Docking) -* User drags a tab completely outside the main window. -* `TabView.TabDroppedOutside` is triggered. -* The system creates a new WinUI 3 `Window`. -* A new `DockLayout` instance is placed in this window. -* The dragged tab object is removed from its original tree and added to a new `DockPanelNode` inside the new window's tree. -* *Note: Because WinUI 3 supports multiple windows on the same UI thread, we don't have to worry about cross-thread marshaling of UI elements, making this much simpler than UWP.* - -### 3. Empty Node Cleanup -* When a `DockPanelNode`'s `Items` collection reaches 0 (the last tab is dragged away), it is removed from the tree. -* If its parent `DockGroupNode` now only has 1 child remaining, that `DockGroupNode` is removed and replaced by its single child, collapsing the tree. - -## Implementation Phases -1. Define the Data Model (`DockNode` structure). -2. Implement the recursive UI generation (binding the tree to nested Grids and TabViews). -3. Implement basic tab moving (Merge) between existing `DockPanelNode`s. -4. Implement edge dropping (Split) and the drop target highlight overlay. -5. Implement empty node cleanup logic. -6. Implement multi-window tear-off via `TabDroppedOutside`. diff --git a/docs/superpowers/specs/2026-03-28-docking-layout-design.md b/docs/superpowers/specs/2026-03-28-docking-layout-design.md deleted file mode 100644 index 902e5eb..0000000 --- a/docs/superpowers/specs/2026-03-28-docking-layout-design.md +++ /dev/null @@ -1,38 +0,0 @@ -# Docking Layout Design - -## Overview -A custom WinUI 3 docking layout system for GhostEngine's editor, inspired by `WinUI.Dock` but tailored to support Unity/Blender-style region highlighting and dynamic tab creation. - -## Architecture & Components - -The system uses a UI-driven approach where custom controls manage their own state and visual tree. - -- **`DockingLayout`**: The root control. Manages the overall state, drag-and-drop coordination, and floating windows. -- **`DockPanel`**: A container that splits its area horizontally or vertically (using a `Grid` with `GridSplitter`s). -- **`DockGroup`**: A container that holds multiple documents, rendered as a WinUI 3 `TabView`. -- **`DockDocument`**: The actual content item, representing a single tab and its payload. -- **`FloatingWindow`**: A separate WinUI `Window` that hosts a minimal `DockingLayout` internally for torn-off tabs. -- **`DockRegionHighlight`**: A visual overlay control (e.g., a semi-transparent blue rectangle) used during drag-and-drop to show exactly where the drop will occur (Left, Right, Top, Bottom, or Center). - -## Drag and Drop Flow - -1. **Start**: Dragging a `TabViewItem` initiates a drag operation. We store a reference to the dragged `DockDocument`. -2. **Over**: As the pointer moves over a `DockGroup` or `DockPanel`, we calculate the relative pointer position to determine the target region (Left 25%, Right 25%, Top 25%, Bottom 25%, or Center 50%). We display the `DockRegionHighlight` over that specific area. -3. **Drop**: - - If dropped on an edge region (Left/Right/Top/Bottom), we split the target container: we create a new `DockPanel`, move the existing content to one side, and place the dragged document on the other. - - If dropped in the Center of a `DockGroup`, we add the document as a new tab to that group. - - If dropped outside the main window bounds, we create a new `FloatingWindow` and place the document inside it. - -## Dynamic Creation API - -The `DockingLayout` will expose methods to allow programmatically adding new panels or tabs at runtime (e.g., for a "plus icon" scenario). - -```csharp -public void AddDocument(DockDocument document, DockTarget target, DockGroup? targetGroup = null); -``` - -## Implementation Details - -- The controls will be created under `src\Editor\Ghost.Editor\View\Controls\Docking\`. -- We will use WinUI 3's built-in drag and drop APIs (`CanDrag`, `DragStarting`, `AllowDrop`, `DragOver`, `Drop`). -- The `DockRegionHighlight` will be a `Border` or `Rectangle` added to the `DockingLayout`'s visual tree (e.g., in a `Popup` or an overlay `Grid`) and positioned absolutely based on the current drag target. diff --git a/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs b/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs index d54dc2f..ed2d720 100644 --- a/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs +++ b/src/Editor/Ghost.DSL/ShaderCompiler/DSLShaderCompiler.cs @@ -140,13 +140,8 @@ internal static class DSLShaderCompiler var pixelShaderCode = new ShaderCode { code = result.Value, entryPoint = pass.pixelShader.entry }; - var asHash = Hash.Combine64(GetUniqueId(amplificationShaderCode.code), GetUniqueId(amplificationShaderCode.entryPoint)); - var msHash = Hash.Combine64(GetUniqueId(meshShaderCode.code), GetUniqueId(meshShaderCode.entryPoint)); - var psHash = Hash.Combine64(GetUniqueId(pixelShaderCode.code), GetUniqueId(pixelShaderCode.entryPoint)); - passes[i] = new PassDescriptor { - identifier = Hash.Combine64(GetUniqueId(semantics.name + pass.name), asHash, msHash, psHash), name = pass.name, amplificationShaderCode = amplificationShaderCode, diff --git a/src/Editor/Ghost.Editor/App.xaml.cs b/src/Editor/Ghost.Editor/App.xaml.cs index d1f6a1b..271e2f9 100644 --- a/src/Editor/Ghost.Editor/App.xaml.cs +++ b/src/Editor/Ghost.Editor/App.xaml.cs @@ -166,9 +166,9 @@ public partial class App : Application private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) { - Logger.LogError(e.Exception); + Logger.Error(e.Exception); #if DEBUG Debugger.BreakForUserUnhandledException(e.Exception); #endif } -} \ No newline at end of file +} diff --git a/src/Runtime/Ghost.Core/Ghost.Core.csproj b/src/Runtime/Ghost.Core/Ghost.Core.csproj index 13df9a4..0d8d178 100644 --- a/src/Runtime/Ghost.Core/Ghost.Core.csproj +++ b/src/Runtime/Ghost.Core/Ghost.Core.csproj @@ -20,8 +20,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Runtime/Ghost.Core/Graphics/PipelineState.cs b/src/Runtime/Ghost.Core/Graphics/PipelineState.cs index 0c845be..9ee82e0 100644 --- a/src/Runtime/Ghost.Core/Graphics/PipelineState.cs +++ b/src/Runtime/Ghost.Core/Graphics/PipelineState.cs @@ -72,7 +72,6 @@ public struct PipelineState get; set; } - public static PipelineState Default => new PipelineState { ZTest = ZTest.LessEqual, @@ -107,4 +106,4 @@ public struct PipelineState var code64 = GetHashCode64(); return ((int)code64) ^ (int)(code64 >> 32); } -} \ No newline at end of file +} diff --git a/src/Runtime/Ghost.Core/Graphics/ShaderDescriptor.cs b/src/Runtime/Ghost.Core/Graphics/ShaderDescriptor.cs index a3481c4..998046e 100644 --- a/src/Runtime/Ghost.Core/Graphics/ShaderDescriptor.cs +++ b/src/Runtime/Ghost.Core/Graphics/ShaderDescriptor.cs @@ -48,7 +48,6 @@ public struct PassDescriptor { public GraphicsShaderDescriptor shader; - public ulong identifier; public string name; public ShaderCode amplificationShaderCode; diff --git a/src/Runtime/Ghost.Core/Logging.cs b/src/Runtime/Ghost.Core/Logging.cs index 86eb0c6..0564891 100644 --- a/src/Runtime/Ghost.Core/Logging.cs +++ b/src/Runtime/Ghost.Core/Logging.cs @@ -1,6 +1,8 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace Ghost.Core; @@ -151,90 +153,105 @@ public static class Logger public static LogCollection Logs => s_logger.Logs; [StackTraceHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Log(LogLevel level, object? message) { s_logger.Log(message?.ToString() ?? "null", level); } [StackTraceHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Log(LogLevel level, string message) { s_logger.Log(message, level); } [StackTraceHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Log(LogLevel level, string format, params object?[] args) { s_logger.Log(string.Format(format, args), level); } [StackTraceHidden] - public static void LogInfo(object? message) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Info(object? message) { s_logger.Log(message?.ToString() ?? "null", LogLevel.Info); } [StackTraceHidden] - public static void LogInfo(string message) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Info(string message) { s_logger.Log(message, LogLevel.Info); } [StackTraceHidden] - public static void LogInfo(string format, params object?[] args) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Info(string format, params object?[] args) { s_logger.Log(string.Format(format, args), LogLevel.Info); } [StackTraceHidden] - public static void LogWarning(object? message) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Warning(object? message) { s_logger.Log(message?.ToString() ?? "null", LogLevel.Warning); } [StackTraceHidden] - public static void LogWarning(string message) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Warning(string message) { s_logger.Log(message, LogLevel.Warning); } [StackTraceHidden] - public static void LogWarning(string format, params object?[] args) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Warning(string format, params object?[] args) { s_logger.Log(string.Format(format, args), LogLevel.Warning); } [StackTraceHidden] - public static void LogError(object? message) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Error(object? message) { s_logger.Log(message?.ToString() ?? "null", LogLevel.Error); } [StackTraceHidden] - public static void LogError(string message) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Error(string message) { s_logger.Log(message, LogLevel.Error); } [StackTraceHidden] - public static void LogError(string format, params object?[] args) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Error(string format, params object?[] args) { s_logger.Log(string.Format(format, args), LogLevel.Error); } [StackTraceHidden] - public static void LogError(Exception ex) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Error(Exception ex) { s_logger.Log(ex); } [StackTraceHidden] - public static void Assert(bool condition, string message) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Assert(bool condition, [CallerArgumentExpression(nameof(condition))] string? message = null) { - s_logger.Assert(condition, message); + s_logger.Assert(condition, message ?? "null"); } [StackTraceHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] [Conditional("DEBUG")] [Conditional("GHOST_EDITOR")] public static void Debug(object? message) @@ -243,6 +260,7 @@ public static class Logger } [StackTraceHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] [Conditional("DEBUG")] [Conditional("GHOST_EDITOR")] public static void Debug(string message) @@ -251,10 +269,26 @@ public static class Logger } [StackTraceHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] [Conditional("DEBUG")] [Conditional("GHOST_EDITOR")] public static void Debug(string format, params object?[] args) { s_logger.Log(string.Format(format, args), LogLevel.Debug); } + + [StackTraceHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Conditional("DEBUG")] + [Conditional("GHOST_EDITOR")] + public static void DebugAssert([DoesNotReturnIf(false)] bool condition, [CallerArgumentExpression(nameof(condition))] string? message = null) + { + s_logger.Assert(condition, message?.ToString() ?? "null"); +#if DEBUG + if (!condition) + { + System.Diagnostics.Debug.Fail(message ?? "Assertion failed."); + } +#endif + } } diff --git a/src/Runtime/Ghost.Core/Result.cs b/src/Runtime/Ghost.Core/Result.cs index fcdfaa3..8dd9b61 100644 --- a/src/Runtime/Ghost.Core/Result.cs +++ b/src/Runtime/Ghost.Core/Result.cs @@ -353,8 +353,8 @@ public static class ResultExtensions return result.Value; } - public static T GetValueOrThrow(this Result result, [CallerArgumentExpression(nameof(result))] string? op = null) - where S : struct, Enum + public static T GetValueOrThrow(this Result result, [CallerArgumentExpression(nameof(result))] string? op = null) + where E : struct, Enum { if (!result.IsSuccess) { @@ -364,17 +364,34 @@ public static class ResultExtensions return result.Value; } + public static ref T GetValueOrThrow(this RefResult result, [CallerArgumentExpression(nameof(result))] string? op = null) + where E : struct, Enum + { + if (!result.IsSuccess) + { + throw new InvalidOperationException($"{op} failed: status {result.Error}"); + } + + return ref result.Value; + } + public static T? GetValueOrDefault(this Result result, T? defaultValue = default) { return result.IsSuccess ? result.Value : defaultValue; } - public static T? GetValueOrDefault(this Result result, T? defaultValue = default) - where S : struct, Enum + public static T? GetValueOrDefault(this Result result, T? defaultValue = default) + where E : struct, Enum { return result.IsSuccess ? result.Value : defaultValue; } + public static ref T GetValueOrDefault(this RefResult result) + where E : struct, Enum + { + return ref result.IsSuccess ? ref result.Value : ref Unsafe.NullRef(); + } + public static bool TryGetValue(this Result result, out T value) { if (result.IsSuccess) @@ -529,4 +546,4 @@ public static class ResultExtensions return onFailure(result.Error); } } -} \ No newline at end of file +} diff --git a/src/Runtime/Ghost.Core/TemJobAllocator.cs b/src/Runtime/Ghost.Core/TemJobAllocator.cs index 58a3371..78a540e 100644 --- a/src/Runtime/Ghost.Core/TemJobAllocator.cs +++ b/src/Runtime/Ghost.Core/TemJobAllocator.cs @@ -1,6 +1,5 @@ using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Utilities; -using System.Diagnostics; using System.Runtime.CompilerServices; namespace Ghost.Core; @@ -13,7 +12,7 @@ public unsafe partial struct TempJobAllocator internal static void Initialize(nuint capacity) { - Debug.Assert(_pAllocator == null, "TempJobAllocator is already initialized."); + Logger.DebugAssert(_pAllocator == null, "TempJobAllocator is already initialized."); _pAllocator = (TempJobAllocator*)Malloc((nuint)sizeof(TempJobAllocator)); } @@ -71,52 +70,29 @@ public unsafe partial struct TempJobAllocator : IAllocator State = Unsafe.AsPointer(ref this), Alloc = &Allocate, Realloc = &Reallocate, - Free = &Free, -#if MHP_ENABLE_SAFETY_CHECKS - IsValid = &IsValid, -#else - IsValid = null, -#endif + Free = &Free }; } - private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption -#if MHP_ENABLE_SAFETY_CHECKS - , MemoryHandle* pHandle -#endif - ) + private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption) { var pSelf = (TempJobAllocator*)instance; var pCurrentArena = pSelf->_pArena + pSelf->_currentFrameIndex; var ptr = pCurrentArena->Allocate(size, alignment, allocationOption); if (ptr == null) { -#if MHP_ENABLE_SAFETY_CHECKS - *pHandle = MemoryHandle.Invalid; -#endif return null; } Interlocked.Increment(ref pSelf->_allocationsPerFrame[pSelf->_currentFrameIndex]); -#if MHP_ENABLE_SAFETY_CHECKS - *pHandle = new MemoryHandle(_MAGIC_ID, pSelf->_currentFrameCount); -#endif return ptr; } - private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption -#if MHP_ENABLE_SAFETY_CHECKS - , MemoryHandle* pHandle -#endif - ) + private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption) { if (ptr == null) { - return Allocate(instance, newSize, alignment, allocationOption -#if MHP_ENABLE_SAFETY_CHECKS - , pHandle -#endif - ); + return Allocate(instance, newSize, alignment, allocationOption); } var pSelf = (TempJobAllocator*)instance; @@ -132,24 +108,12 @@ public unsafe partial struct TempJobAllocator : IAllocator return newPtr; } - private static void Free(void* instance, void* ptr -#if MHP_ENABLE_SAFETY_CHECKS - , MemoryHandle handle -#endif - ) + private static void Free(void* instance, void* ptr) { var pSelf = (TempJobAllocator*)instance; Interlocked.Decrement(ref pSelf->_allocationsPerFrame[pSelf->_currentFrameIndex]); } -#if MHP_ENABLE_SAFETY_CHECKS - private static bool IsValid(void* instance, MemoryHandle handle) - { - var pSelf = (TempJobAllocator*)instance; - return handle.ID == _MAGIC_ID && handle.Generation > pSelf->_currentFrameCount - _FRAME_LATENCY; - } -#endif - public int AdvanceFrame() { var allocations = Interlocked.Exchange(ref _allocationsPerFrame[_currentFrameIndex], 0); @@ -161,4 +125,4 @@ public unsafe partial struct TempJobAllocator : IAllocator return allocations; } -} \ No newline at end of file +} diff --git a/src/Runtime/Ghost.Core/Utilities/BinaryReader.cs b/src/Runtime/Ghost.Core/Utilities/BinaryReader.cs deleted file mode 100644 index 7a84cfd..0000000 --- a/src/Runtime/Ghost.Core/Utilities/BinaryReader.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Ghost.Core.Utilities; - -public ref struct BinaryReader -{ - private readonly Span _buffer; - private int _position; - - public int Position - { - readonly get => _position; - set => _position = value; - } - - public BinaryReader(Span buffer) - { - _buffer = buffer; - _position = 0; - } - - public T Read() - where T : unmanaged - { - var value = Unsafe.ReadUnaligned(ref _buffer[_position]); - _position += Unsafe.SizeOf(); - return value; - } - - public ReadOnlySpan ReadBytes(int length) - { - var span = _buffer.Slice(_position, length); - _position += length; - return span; - } -} diff --git a/src/Runtime/Ghost.Core/Utilities/BufferWriter.cs b/src/Runtime/Ghost.Core/Utilities/BufferWriter.cs index 9b60971..c38ac81 100644 --- a/src/Runtime/Ghost.Core/Utilities/BufferWriter.cs +++ b/src/Runtime/Ghost.Core/Utilities/BufferWriter.cs @@ -1,10 +1,11 @@ using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Ghost.Core.Utilities; -public struct BufferWriter : IDisposable +public unsafe struct BufferWriter : IDisposable { private UnsafeList _buffer; private int _position; @@ -21,17 +22,21 @@ public struct BufferWriter : IDisposable _position = 0; } - public unsafe void Write(T value) + public void Write(T value) where T : unmanaged { Unsafe.WriteUnaligned(ref _buffer[_position], value); _position += sizeof(T); } - public void WriteBytes(ReadOnlySpan data) + public void WriteSpan(ReadOnlySpan data) + where T : unmanaged { - data.CopyTo(_buffer.AsSpan().Slice(_position, data.Length)); - _position += data.Length; + var size = sizeof(T) * data.Length; + var byteSpan = MemoryMarshal.AsBytes(data); + + byteSpan.CopyTo(_buffer.AsSpan().Slice(_position, size)); + _position += size; } public Span ReserveSpan(int length) @@ -51,3 +56,88 @@ public struct BufferWriter : IDisposable _buffer.Dispose(); } } + +public unsafe ref struct SpanWriter +{ + private Span _buffer; + private int _position; + + public int Position + { + readonly get => _position; + set => _position = value; + } + + public SpanWriter(Span buffer) + { + _buffer = buffer; + _position = 0; + } + + public void Write(T value) + where T : unmanaged + { + Unsafe.WriteUnaligned(ref _buffer[_position], value); + _position += sizeof(T); + } + + public void WriteSpan(ReadOnlySpan data) + where T : unmanaged + { + var size = sizeof(T) * data.Length; + var byteSpan = MemoryMarshal.AsBytes(data); + + byteSpan.CopyTo(_buffer.Slice(_position, size)); + _position += size; + + } + + public readonly Span AsSpan() + { + return _buffer; + } +} + +public unsafe ref struct SpanReader +{ + private readonly Span _buffer; + private int _position; + + public int Position + { + readonly get => _position; + set => _position = value; + } + + public SpanReader(Span buffer) + { + _buffer = buffer; + _position = 0; + } + + public T Read() + where T : unmanaged + { + var value = Unsafe.ReadUnaligned(ref _buffer[_position]); + _position += Unsafe.SizeOf(); + return value; + } + + public ReadOnlySpan ReadSpan(int length) + where T : unmanaged + { + var size = sizeof(T) * length; + var span = MemoryMarshal.Cast(_buffer.Slice(_position, size)); + + _position += size; + return span; + } + + public ReadOnlySpan ReadToEnd() + where T : unmanaged + { + var span = MemoryMarshal.Cast(_buffer.Slice(_position)); + _position += span.Length; + return span; + } +} diff --git a/src/Runtime/Ghost.Engine/Components/Camera.cs b/src/Runtime/Ghost.Engine/Components/Camera.cs index 5d3c874..d09d100 100644 --- a/src/Runtime/Ghost.Engine/Components/Camera.cs +++ b/src/Runtime/Ghost.Engine/Components/Camera.cs @@ -1,7 +1,6 @@ using Ghost.Core; using Ghost.Entities; using Ghost.Graphics.Core; -using Ghost.Graphics.RenderPipeline; using Ghost.Graphics.RHI; using Misaki.HighPerformance.Mathematics; diff --git a/src/Runtime/Ghost.Engine/RenderPipeline/GPUScene.cs b/src/Runtime/Ghost.Engine/RenderPipeline/GPUScene.cs index dca06bd..7edab20 100644 --- a/src/Runtime/Ghost.Engine/RenderPipeline/GPUScene.cs +++ b/src/Runtime/Ghost.Engine/RenderPipeline/GPUScene.cs @@ -1,6 +1,5 @@ using Ghost.Core; using Ghost.Graphics.RHI; -using System.Diagnostics; namespace Ghost.Engine.RenderPipeline; @@ -16,6 +15,8 @@ internal unsafe class GPUScene : IDisposable private uint _requiredResize; private bool _disposed; + public Handle SceneBuffer => _sceneBuffer; + internal GPUScene(IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, uint initialCount) { _resourceAllocator = resourceAllocator; @@ -30,7 +31,7 @@ internal unsafe class GPUScene : IDisposable }; _sceneBuffer = _resourceAllocator.CreateBuffer(in bufferDesc, "SceneBuffer"); - Debug.Assert(_sceneBuffer.IsValid, "Failed to create GPUScene buffer."); + Logger.DebugAssert(_sceneBuffer.IsValid, "Failed to create GPUScene buffer."); _capacity = initialCount; } @@ -48,22 +49,21 @@ internal unsafe class GPUScene : IDisposable return; } - var newCapacity = _capacity * 2; - newCapacity = Math.Max(newCapacity, _capacity + _requiredResize); + var newCapacity = Math.Max(_capacity * 2, _capacity + _requiredResize); var newBufferDesc = new BufferDesc { - Size = (ulong)newCapacity * (ulong)sizeof(InstanceData), + Size = newCapacity * (ulong)sizeof(InstanceData), Stride = (uint)sizeof(InstanceData), Usage = BufferUsage.Structured | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource, HeapType = HeapType.Default, }; var newBuffer = _resourceAllocator.CreateBuffer(in newBufferDesc, "SceneBuffer_Resized"); - Debug.Assert(newBuffer.IsValid); + Logger.DebugAssert(newBuffer.IsValid); // Copy existing data to the new buffer - cmd.CopyBuffer(newBuffer, _sceneBuffer, 0, 0, (ulong)_instanceCount * (ulong)sizeof(InstanceData)); + cmd.CopyBuffer(newBuffer, _sceneBuffer, 0, 0, _instanceCount * (ulong)sizeof(InstanceData)); // Replace old buffer with the new one _resourceDatabase.ReleaseResource(_sceneBuffer.AsResource()); diff --git a/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.UpdateGPUScene.cs b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.UpdateGPUScene.cs new file mode 100644 index 0000000..98359f1 --- /dev/null +++ b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.UpdateGPUScene.cs @@ -0,0 +1,50 @@ +using Ghost.Core; +using Ghost.Core.Graphics; +using Ghost.Graphics.Core; +using Ghost.Graphics.RHI; +using Misaki.HighPerformance.Mathematics; + +namespace Ghost.Engine.RenderPipeline; + +[GenerateShaderProperty("Internal/UpdateGPUScene")] +public partial struct UpdateGPUSceneShaderProperty +{ + public uint gpuSceneBuffer; + public uint addBuffer; + public uint addCount; + public uint removeBuffer; + public uint removeCount; +} + +internal partial class GhostRenderPipeline +{ + public void UpdateGPUScene(RenderContext ctx, Handle addBuffer, int addCount, Handle removeBuffer, int removeCount) + { + if (addCount <= 0 && removeCount <= 0) + { + Logger.DebugAssert(addBuffer.IsInvalid && removeBuffer.IsInvalid, "Buffers should be invalid when there are no updates."); + return; // No updates needed + } + + // NOTE: We dispatch it here instead of in render graph is because the update does not perform every frame. + // The topology change of the graph will trigger the recompilation of the render graph, which is expensive. + // Currently the render graph does not support import invalid resources, which means we can not handle the early return in the render func. + // Furthermore, updating the GPU scene does not rely on other resources and passes, it's isolated and always run before the actual rendering. + // So it's fine to dispatch it here directly. + + var property = new UpdateGPUSceneShaderProperty + { + gpuSceneBuffer = ctx.ResourceDatabase.GetBindlessIndex(_gpuScene.SceneBuffer.AsResource(), BindlessAccess.UnorderedAccess), + addBuffer = ctx.ResourceDatabase.GetBindlessIndex(addBuffer.AsResource()), + addCount = (uint)addCount, + removeBuffer = ctx.ResourceDatabase.GetBindlessIndex(removeBuffer.AsResource()), + removeCount = (uint)removeCount + }; + + // TODO: Write and load the shader. This is just a placeholder for now. + var shader = default(Handle); + var keywords = new LocalKeywordSet(); + + ctx.DispatchCompute(shader, 0, in keywords, in property, new uint3()); + } +} diff --git a/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.cs b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.cs index 2d48e7f..8429b3b 100644 --- a/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.cs +++ b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipeline.cs @@ -10,7 +10,7 @@ using System.Diagnostics; namespace Ghost.Engine.RenderPipeline; -internal class GhostRenderPipeline : IRenderPipeline +internal partial class GhostRenderPipeline : IRenderPipeline { private struct AddInstanceData { @@ -39,11 +39,11 @@ internal class GhostRenderPipeline : IRenderPipeline { _renderSystem = renderSystem; - _renderGraph = new RenderGraph(renderSystem.ResourceManager, renderSystem.GraphicsEngine); + _renderGraph = new RenderGraph(renderSystem); _gpuScene = new GPUScene(renderSystem.GraphicsEngine.ResourceAllocator, renderSystem.GraphicsEngine.ResourceDatabase, 102_400u); // 102.4k objects should be enough for now } - private static unsafe Handle CreateAddInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase) + private static unsafe Handle CreateAddInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase, out int count) { if (!ghostPayload.AddRequest.IsEmpty) { @@ -82,13 +82,16 @@ internal class GhostRenderPipeline : IRenderPipeline } resourceDatabase.UnmapResource(addBuffer.AsResource(), 0, null); + + count = i; return addBuffer; } + count = 0; return default; } - private static unsafe Handle CreateRemoveInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase) + private static unsafe Handle CreateRemoveInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase, out int count) { if (!ghostPayload.RemoveRequest.IsEmpty) { @@ -116,9 +119,12 @@ internal class GhostRenderPipeline : IRenderPipeline } resourceDatabase.UnmapResource(removeBuffer.AsResource(), 0, null); + + count = i; return removeBuffer; } + count = 0; return default; } @@ -131,15 +137,20 @@ internal class GhostRenderPipeline : IRenderPipeline foreach (ref readonly var request in ghostPayload.RenderRequests) { - if (!RenderPipelineUtility.GetVPMatrices(_renderSystem, in request, out var view, out var projection, out var screenSize)) + try { - continue; + using var viewData = new RenderViewData(_renderSystem.SwapChainManager, resourceDatabase, in request); + RenderPipelineUtility.GetVPMatrices(in request, viewData.ScreenSize, out var view, out var projection); + + var addBuffer = CreateAddInstanceBuffer(ghostPayload, resourceManager, resourceDatabase, out var addCount); + var removeBuffer = CreateRemoveInstanceBuffer(ghostPayload, resourceManager, resourceDatabase, out var removeCount); + + UpdateGPUScene(ctx, addBuffer, addCount, removeBuffer, removeCount); + } + catch (Exception ex) + { + Logger.Error(ex); } - - var addBuffer = CreateAddInstanceBuffer(ghostPayload, resourceManager, resourceDatabase); - var removeBuffer = CreateRemoveInstanceBuffer(ghostPayload, resourceManager, resourceDatabase); - - } } diff --git a/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipelineSettings.cs b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipelineSettings.cs index 2e7cc5f..84d6801 100644 --- a/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipelineSettings.cs +++ b/src/Runtime/Ghost.Engine/RenderPipeline/GhostRenderPipelineSettings.cs @@ -39,7 +39,7 @@ internal sealed class GhostRenderPayload : IRenderPayload { _renderPipeline = renderPipeline; - _renderRequests = new UnsafeList(4, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent); + _renderRequests = new UnsafeList(4, Misaki.HighPerformance.LowLevel.Buffer.AllocationHandle.Persistent); _addRequest = new ConcurrentQueue(); _removeRequest = new ConcurrentQueue(); } diff --git a/src/Runtime/Ghost.Engine/Systems/RenderPipelineSystemAttribute.cs b/src/Runtime/Ghost.Engine/Systems/RenderPipelineSystemAttribute.cs index eb20d73..dd8e12c 100644 --- a/src/Runtime/Ghost.Engine/Systems/RenderPipelineSystemAttribute.cs +++ b/src/Runtime/Ghost.Engine/Systems/RenderPipelineSystemAttribute.cs @@ -15,6 +15,7 @@ public class RenderPipelineSystemAttribute : RenderPipelineSystemAttribute public override Type SettingsType => typeof(T); } +[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public static class RenderPipelineSystemRegistry { private static readonly Dictionary>> s_renderPipelineSystems = new(); diff --git a/src/Runtime/Ghost.Entities/Archetype.cs b/src/Runtime/Ghost.Entities/Archetype.cs index 4124c70..57aa05c 100644 --- a/src/Runtime/Ghost.Entities/Archetype.cs +++ b/src/Runtime/Ghost.Entities/Archetype.cs @@ -124,8 +124,8 @@ internal unsafe struct Chunk : IDisposable public Chunk(int bufferSize, int capacity, int componentCount, uint globalVersion) { - _data = new UnsafeArray(bufferSize, Allocator.Persistent, AllocationOption.Clear); - _versions = new UnsafeArray(componentCount, Allocator.Persistent); + _data = new UnsafeArray(bufferSize, AllocationHandle.Persistent, AllocationOption.Clear); + _versions = new UnsafeArray(componentCount, AllocationHandle.Persistent); _capacity = capacity; _count = 0; @@ -200,13 +200,13 @@ internal unsafe struct Archetype : IDisposable _id = id; _worldID = worldID; - _chunks = new UnsafeList(4, Allocator.Persistent); - _edgesAdd = new UnsafeList(4, Allocator.Persistent); - _edgesRemove = new UnsafeList(4, Allocator.Persistent); + _chunks = new UnsafeList(4, AllocationHandle.Persistent); + _edgesAdd = new UnsafeList(4, AllocationHandle.Persistent); + _edgesRemove = new UnsafeList(4, AllocationHandle.Persistent); if (componentIds.IsEmpty) { - _signature = new UnsafeBitSet(1, Allocator.Persistent, AllocationOption.Clear); + _signature = new UnsafeBitSet(1, AllocationHandle.Persistent, AllocationOption.Clear); _hash = 0; _signature.ClearAll(); @@ -224,7 +224,7 @@ internal unsafe struct Archetype : IDisposable } } - _signature = new UnsafeBitSet(highestComponentID + 1, Allocator.Persistent, AllocationOption.Clear); + _signature = new UnsafeBitSet(highestComponentID + 1, AllocationHandle.Persistent, AllocationOption.Clear); _hash = _signature.GetHashCode(); CalculateLayout(componentIds); @@ -268,8 +268,8 @@ internal unsafe struct Archetype : IDisposable _maxComponentID = maxComponentID; _entityCapacity = Chunk.CHUNK_BUFFER_SIZE / bytesPerEntity; - _layouts = new UnsafeArray(components.Length, Allocator.Persistent); - _componentIDToLayoutIndex = new UnsafeArray(_maxComponentID + 1, Allocator.Persistent); + _layouts = new UnsafeArray(components.Length, AllocationHandle.Persistent); + _componentIDToLayoutIndex = new UnsafeArray(_maxComponentID + 1, AllocationHandle.Persistent); _componentIDToLayoutIndex.AsSpan().Fill(-1); diff --git a/src/Runtime/Ghost.Entities/Component.cs b/src/Runtime/Ghost.Entities/Component.cs index 20d2a20..85e2222 100644 --- a/src/Runtime/Ghost.Entities/Component.cs +++ b/src/Runtime/Ghost.Entities/Component.cs @@ -168,11 +168,11 @@ public partial class ComponentManager : IDisposable { _world = world; - _archetypes = new UnsafeList(16, Allocator.Persistent); - _entityQueries = new UnsafeList(16, Allocator.Persistent); + _archetypes = new UnsafeList(16, AllocationHandle.Persistent); + _entityQueries = new UnsafeList(16, AllocationHandle.Persistent); - _archetypeLookup = new UnsafeHashMap>(16, Allocator.Persistent); - _querieLookup = new UnsafeHashMap>(16, Allocator.Persistent); + _archetypeLookup = new UnsafeHashMap>(16, AllocationHandle.Persistent); + _querieLookup = new UnsafeHashMap>(16, AllocationHandle.Persistent); // Create the empty archetype CreateArchetype(ReadOnlySpan>.Empty, 0); @@ -297,11 +297,6 @@ public struct ComponentSet : IDisposable, IEquatable _hashCode = -1; } - public ComponentSet(Allocator allocator, params ReadOnlySpan> components) - : this(AllocationManager.GetAllocationHandle(allocator), components) - { - } - public readonly bool Equals(ComponentSet other) { return _hashCode == other._hashCode; diff --git a/src/Runtime/Ghost.Entities/EntityCommandBuffer.cs b/src/Runtime/Ghost.Entities/EntityCommandBuffer.cs index 3c7baa7..5b5ea2e 100644 --- a/src/Runtime/Ghost.Entities/EntityCommandBuffer.cs +++ b/src/Runtime/Ghost.Entities/EntityCommandBuffer.cs @@ -25,7 +25,7 @@ public unsafe class EntityCommandBuffer : IDisposable public EntityCommandBuffer(EntityManager entityManager) { _entityManager = entityManager; - _buffer = new UnsafeList(4096, Allocator.Persistent); + _buffer = new UnsafeList(4096, AllocationHandle.Persistent); } ~EntityCommandBuffer() diff --git a/src/Runtime/Ghost.Entities/EntityManager.cs b/src/Runtime/Ghost.Entities/EntityManager.cs index 0a2ae7a..d6f19ee 100644 --- a/src/Runtime/Ghost.Entities/EntityManager.cs +++ b/src/Runtime/Ghost.Entities/EntityManager.cs @@ -50,7 +50,7 @@ public unsafe partial class EntityManager : IDisposable internal EntityManager(World world, int initialCapacity) { _world = world; - _entityLocations = new UnsafeSlotMap(initialCapacity, Allocator.Persistent, AllocationOption.Clear); + _entityLocations = new UnsafeSlotMap(initialCapacity, AllocationHandle.Persistent, AllocationOption.Clear); _scriptComponents = new SlotMap>(initialCapacity / 2); // _storages = new IManagedComponentStorage[16]; } @@ -638,7 +638,7 @@ public unsafe partial class EntityManager : IDisposable newArchetype.SetComponentData(newChunkIndex, newRowIndex, componentID, pComponent); var r = oldArchetype.RemoveEntity(location.chunkIndex, location.rowIndex); - Debug.Assert(r == Error.None); // We assert it because the entity should exist if the whole system is consistent. + Logger.DebugAssert(r == Error.None); // We assert it because the entity should exist if the whole system is consistent. if (r != Error.None) { return r; @@ -742,7 +742,7 @@ public unsafe partial class EntityManager : IDisposable newArchetype.SetEntity(newChunkIndex, newRowIndex, entity); var r = oldArchetype.RemoveEntity(location.chunkIndex, location.rowIndex); - Debug.Assert(r == Error.None); // We assert it because the entity should exist if the whole system is consistent. + Logger.DebugAssert(r == Error.None); // We assert it because the entity should exist if the whole system is consistent. if (r != Error.None) { return r; diff --git a/src/Runtime/Ghost.Entities/Query.cs b/src/Runtime/Ghost.Entities/Query.cs index c264b73..5b5e8a5 100644 --- a/src/Runtime/Ghost.Entities/Query.cs +++ b/src/Runtime/Ghost.Entities/Query.cs @@ -345,7 +345,7 @@ public unsafe partial struct EntityQuery : IDisposable _id = id; _worldID = worldID; _mask = mask; - _matchingArchetypes = new UnsafeList>(8, Allocator.Persistent); + _matchingArchetypes = new UnsafeList>(8, AllocationHandle.Persistent); } // TODO: Fetching layout every time is not optimal. Cache them? @@ -642,7 +642,7 @@ public ref partial struct QueryBuilder : IDisposable public Identifier Build(World world, bool dispose = true) { - BuildQueryMask(AllocationManager.GetAllocationHandle(Allocator.Persistent), out var mask); + BuildQueryMask(AllocationHandle.Persistent, out var mask); var maskHash = mask.GetHashCode(); var queryID = world.ComponentManager.GetEntityQueryIDByMaskHash(maskHash); diff --git a/src/Runtime/Ghost.Entities/SharedComponent.cs b/src/Runtime/Ghost.Entities/SharedComponent.cs index 55e730d..5faee52 100644 --- a/src/Runtime/Ghost.Entities/SharedComponent.cs +++ b/src/Runtime/Ghost.Entities/SharedComponent.cs @@ -56,7 +56,7 @@ internal sealed unsafe class SharedComponentStore : IDisposable public SharedComponentStore(int initialCapacity = 16) { - _perType = new UnsafeHashMap(initialCapacity, Allocator.Persistent); + _perType = new UnsafeHashMap(initialCapacity, AllocationHandle.Persistent); } ~SharedComponentStore() @@ -203,9 +203,9 @@ internal sealed unsafe class SharedComponentStore : IDisposable var store = new TypeStore { typeSize = typeSize, - data = new UnsafeList(typeSize * 16, Allocator.Persistent), - infos = new UnsafeList(16, Allocator.Persistent), - hashLookup = new UnsafeHashMap(16, Allocator.Persistent), + data = new UnsafeList(typeSize * 16, AllocationHandle.Persistent), + infos = new UnsafeList(16, AllocationHandle.Persistent), + hashLookup = new UnsafeHashMap(16, AllocationHandle.Persistent), freeListHead = 0, versionCounter = 0 }; @@ -217,7 +217,7 @@ internal sealed unsafe class SharedComponentStore : IDisposable _perType.Add(componentTypeId, store); existing = ref _perType.GetValueRef(componentTypeId, out exist); - Debug.Assert(exist); + Logger.DebugAssert(exist); return ref existing; } diff --git a/src/Runtime/Ghost.Entities/System.cs b/src/Runtime/Ghost.Entities/System.cs index f1cc793..09408a7 100644 --- a/src/Runtime/Ghost.Entities/System.cs +++ b/src/Runtime/Ghost.Entities/System.cs @@ -61,7 +61,7 @@ public abstract class SystemBase : ISystem { if (!_requiredQueries.IsCreated) { - _requiredQueries = new UnsafeList(4, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent); + _requiredQueries = new UnsafeList(4, Misaki.HighPerformance.LowLevel.Buffer.AllocationHandle.Persistent); } _requiredQueries.Add(queryID.Value); diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12CommandBuffer.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12CommandBuffer.cs index 67389f3..d01b88d 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12CommandBuffer.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12CommandBuffer.cs @@ -1,4 +1,5 @@ using Ghost.Core; +using Ghost.Core.Graphics; using Ghost.Graphics.D3D12.Utilities; using Ghost.Graphics.RHI; using Misaki.HighPerformance.LowLevel; @@ -473,7 +474,7 @@ internal unsafe class D3D12CommandBuffer : D3D12ObjectRSSetViewports(1, &d3d12Viewport); } - public void SetPipelineState(Key128 pipelineKey) + public void SetPipelineState(Key128 pipelineKey) { AssertNotDisposed(); ThrowIfNotRecording(); @@ -701,7 +702,7 @@ internal unsafe class D3D12CommandBuffer : D3D12ObjectGetGPUVirtualAddress() + offset, SizeInBytes = (uint)(record.ResourcePtr.Get()->GetDesc().Width - offset), - StrideInBytes = record.desc.BufferDescription.Stride + StrideInBytes = record.desc.BufferDescriptor.Stride }; pNativeObject->IASetVertexBuffers(slot, 1, &vbView); @@ -859,7 +860,7 @@ internal unsafe class D3D12CommandBuffer : D3D12Object (IntPtr)NativeObject.Get(); - public D3D12CommandSignature(D3D12RenderDevice device, D3D12PipelineLibrary pipelineLibrary, ref readonly CommandSignatureDesc desc, Key128 pipelineKey) + public D3D12CommandSignature(D3D12RenderDevice device, D3D12PipelineLibrary pipelineLibrary, ref readonly CommandSignatureDesc desc, Key128 pipelineKey) : base(CreateCommandSignature(device, pipelineLibrary, in desc)) { } diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12DescriptorHeap.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12DescriptorHeap.cs index 0268131..23c67b0 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12DescriptorHeap.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12DescriptorHeap.cs @@ -1,3 +1,4 @@ +using Ghost.Core; using Ghost.Graphics.D3D12.Utilities; using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel.Collections; @@ -67,7 +68,7 @@ internal unsafe class D3D12DescriptorHeap : IDisposable Stride = device.NativeObject.Get()->GetDescriptorHandleIncrementSize(type); var success = AllocateResources(numDescriptors); - Debug.Assert(success); + Logger.DebugAssert(success); _heap.Get()->SetName(name); if (ShaderVisible) @@ -240,7 +241,7 @@ internal unsafe class D3D12DescriptorHeap : IDisposable if (!_allocatedDescriptors.IsCreated) { - _allocatedDescriptors = new UnsafeBitSet(numDescriptors, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent, Misaki.HighPerformance.LowLevel.Buffer.AllocationOption.Clear); + _allocatedDescriptors = new UnsafeBitSet(numDescriptors, Misaki.HighPerformance.LowLevel.Buffer.AllocationHandle.Persistent, Misaki.HighPerformance.LowLevel.Buffer.AllocationOption.Clear); } else { @@ -299,7 +300,7 @@ internal unsafe class D3D12DescriptorHeap : IDisposable /// public void Dispose() { - Debug.Assert(NumAllocatedDescriptors == 0); + Logger.DebugAssert(NumAllocatedDescriptors == 0); _heap.Dispose(); _shaderVisibleHeap.Dispose(); diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12GraphicsEngine.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12GraphicsEngine.cs index f0c9bbb..49a79fd 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12GraphicsEngine.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12GraphicsEngine.cs @@ -3,9 +3,9 @@ #endif using Ghost.Core; +using Ghost.Core.Graphics; using Ghost.Graphics.RHI; using Misaki.HighPerformance.Utilities; -using System.Diagnostics; using System.Runtime.CompilerServices; namespace Ghost.Graphics.D3D12; @@ -80,13 +80,13 @@ internal class D3D12GraphicsEngine : IGraphicsEngine public ICommandAllocator CreateCommandAllocator(CommandBufferType type = CommandBufferType.Graphics) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return new D3D12CommandAllocator(_device, type); } public ICommandBuffer CreateCommandBuffer(CommandBufferType type = CommandBufferType.Graphics) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return new D3D12CommandBuffer( _device, @@ -99,7 +99,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine public ICommandBuffer GetPooledCommandBuffer(CommandBufferType type = CommandBufferType.Graphics) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); for (var i = 0; i < _commandBufferPool.Count; i++) { @@ -116,25 +116,25 @@ internal class D3D12GraphicsEngine : IGraphicsEngine public void ReturnPooledCommandBuffer(ICommandBuffer commandBuffer) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); _commandBufferReturnQueue.Enqueue(new CommandBufferReturnEntry(commandBuffer, _cpuFrame)); } public ISwapChain CreateSwapChain(SwapChainDesc desc) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return new DXGISwapChain(_resourceDatabase, _descriptorAllocator, _device, desc, _desc.FrameBufferCount); } - public ICommandSignature CreateCommandSignature(ref readonly CommandSignatureDesc desc, Key128 pipelineKey) + public ICommandSignature CreateCommandSignature(ref readonly CommandSignatureDesc desc, Key128 pipelineKey) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return new D3D12CommandSignature(_device, _pipelineLibrary, in desc, pipelineKey); } public void BeginFrame(ulong cpuFrame) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); _cpuFrame = cpuFrame; _resourceDatabase.BeginFrame(cpuFrame); @@ -142,7 +142,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine public void EndFrame(ulong gpuFrame) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); _resourceDatabase.EndFrame(gpuFrame); diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12Object.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12Object.cs index 48cc0d5..5f0b2b8 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12Object.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12Object.cs @@ -4,6 +4,7 @@ using Ghost.Graphics.D3D12.Utilities; using Ghost.Graphics.RHI; using System.Diagnostics; using System.Runtime.CompilerServices; +using Ghost.Core; namespace Ghost.Graphics.D3D12; @@ -51,7 +52,7 @@ public unsafe abstract class D3D12Object: IRHIObject [MethodImpl(MethodImplOptions.AggressiveInlining)] protected void AssertNotDisposed() { - Debug.Assert(_nativeObject.Get() != null, "Object has been disposed."); + Logger.DebugAssert(_nativeObject.Get() != null, "Object has been disposed."); } protected virtual void Dispose(bool disposing) diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12PipelineLibrary.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12PipelineLibrary.cs index 8003652..98e662d 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12PipelineLibrary.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12PipelineLibrary.cs @@ -60,7 +60,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object { _device = device; - _pipelineCache = new UnsafeHashMap(32, Allocator.Persistent); + _pipelineCache = new UnsafeHashMap(32, AllocationHandle.Persistent); CreateDefaultRootSignature().ThrowIfFailed(); } @@ -129,7 +129,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object } var size = pNativeObject->GetSerializedSize(); - using var buffer = new UnsafeArray((int)size, Allocator.Persistent); // We use persistent Heap allocation instead of stack allocation to avoid stack overflow for large pipeline libraries. + using var buffer = new UnsafeArray((int)size, AllocationHandle.Persistent); // We use persistent Heap allocation instead of stack allocation to avoid stack overflow for large pipeline libraries. ThrowIfFailed(pNativeObject->Serialize(buffer.GetUnsafePtr(), size)); @@ -157,7 +157,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object } var hr = pNativeObject->LoadPipeline(pKeyStr, pStreamDesc, __uuidof(pPipelineState), (void**)&pPipelineState); - + if (hr == E.E_INVALIDARG) { // Pipeline not found in the library, create a new one. @@ -177,37 +177,37 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object return Result.Success(); } - public Result> CreateGraphicsPipeline(ref readonly GraphicsPSODescriptor descriptor, ReadOnlySpan asByteCode, ReadOnlySpan msByteCode, ReadOnlySpan psByteCode) + public Result> CreateGraphicsPipeline(ref readonly GraphicsPSODesc desc) { AssertNotDisposed(); - if (descriptor.RtvFormats.Length > D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT) + if (desc.RtvFormats.Length > D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT) { return Result.Failure($"RTV format count exceeds the maximum supported render target count of {D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT}."); } - var passPipelineKey = new PassPipelineHash(descriptor.RtvFormats, descriptor.DsvFormat); - var pipelineKey = RHIUtility.CreateGraphicsPipelineKey(descriptor.VariantKey, descriptor.PipelineOption, passPipelineKey); + var passAttachmentKey = new PassAttachmentHash(desc.RtvFormats, desc.DsvFormat); + var pipelineKey = RHIUtility.CreateGraphicsPipelineKey(desc.VariantKey, desc.PipelineOption, passAttachmentKey); if (!_pipelineCache.ContainsKey(pipelineKey)) { - fixed (byte* pASByteCode = asByteCode, pMSByteCode = msByteCode, pPSByteCode = psByteCode) + fixed (byte* pASByteCode = desc.AsCode, pMSByteCode = desc.MsCode, pPSByteCode = desc.PsCode) { - var desc = new D3DX12_MESH_SHADER_PIPELINE_STATE_DESC + var msPipelinedesc = new D3DX12_MESH_SHADER_PIPELINE_STATE_DESC { pRootSignature = _defaultRootSignature.Get(), - MS = new D3D12_SHADER_BYTECODE(pMSByteCode, (nuint)msByteCode.Length), - PS = new D3D12_SHADER_BYTECODE(pPSByteCode, (nuint)psByteCode.Length), + MS = new D3D12_SHADER_BYTECODE(pMSByteCode, (nuint)desc.MsCode.Length), + PS = new D3D12_SHADER_BYTECODE(pPSByteCode, (nuint)desc.PsCode.Length), PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, SampleMask = UINT32_MAX, SampleDesc = new DXGI_SAMPLE_DESC(1, 0), - NumRenderTargets = (uint)descriptor.RtvFormats.Length, - DSVFormat = descriptor.DsvFormat.ToDXGIFormat(), - DepthStencilState = BuildDepthStencil(descriptor.PipelineOption.ZTest, descriptor.PipelineOption.ZWrite), + NumRenderTargets = (uint)desc.RtvFormats.Length, + DSVFormat = desc.DsvFormat.ToDXGIFormat(), + DepthStencilState = BuildDepthStencil(desc.PipelineOption.ZTest, desc.PipelineOption.ZWrite), NodeMask = 0, Flags = D3D12_PIPELINE_STATE_FLAG_NONE, - BlendState = descriptor.PipelineOption.Blend switch + BlendState = desc.PipelineOption.Blend switch { Blend.Opaque => D3D12Utility.D3D12_BLEND_DESC_OPAQUE, Blend.Alpha => D3D12Utility.D3D12_BLEND_DESC_ALPHA_BLEND, @@ -216,7 +216,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object Blend.PremultipliedAlpha => D3D12Utility.D3D12_BLEND_DESC_PREMULTIPLIED, _ => D3D12Utility.D3D12_BLEND_DESC_OPAQUE }, - RasterizerState = descriptor.PipelineOption.Cull switch + RasterizerState = desc.PipelineOption.Cull switch { Cull.Off => D3D12Utility.D3D12_RASTERIZER_DESC_CULL_NONE, Cull.Front => D3D12Utility.D3D12_RASTERIZER_DESC_CULL_CLOCKWISE, @@ -225,25 +225,25 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object }, }; - if (asByteCode.Length != 0) + if (desc.AsCode.Length != 0) { - desc.AS = new D3D12_SHADER_BYTECODE(pASByteCode, (nuint)asByteCode.Length); + msPipelinedesc.AS = new D3D12_SHADER_BYTECODE(pASByteCode, (nuint)desc.AsCode.Length); } - for (var i = 0; i < descriptor.RtvFormats.Length; i++) + for (var i = 0; i < desc.RtvFormats.Length; i++) { - desc.RTVFormats[i] = descriptor.RtvFormats[i].ToDXGIFormat(); - desc.BlendState.RenderTarget[i].RenderTargetWriteMask = (byte)((int)descriptor.PipelineOption.ColorMask & 0x0F); + msPipelinedesc.RTVFormats[i] = desc.RtvFormats[i].ToDXGIFormat(); + msPipelinedesc.BlendState.RenderTarget[i].RenderTargetWriteMask = (byte)((int)desc.PipelineOption.ColorMask & 0x0F); } - var meshStream = new CD3DX12_PIPELINE_MESH_STATE_STREAM(in desc); + var meshStream = new CD3DX12_PIPELINE_MESH_STATE_STREAM(in msPipelinedesc); var streamDesc = new D3D12_PIPELINE_STATE_STREAM_DESC { pPipelineStateSubobjectStream = &meshStream, SizeInBytes = (nuint)sizeof(CD3DX12_PIPELINE_MESH_STATE_STREAM) }; - var result = CreatePSO(descriptor.VariantKey, pipelineKey, &streamDesc); + var result = CreatePSO(desc.VariantKey, pipelineKey, &streamDesc); if (result.IsFailure) { return result; @@ -254,25 +254,25 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object return pipelineKey; } - public Result> CreateComputePipeline(ref readonly ComputePSODescriptor descriptor, ReadOnlySpan csBytecode) + public Result> CreateComputePipeline(ref readonly ComputePSODesc desc) { AssertNotDisposed(); - var pipelineKey = RHIUtility.CreateComputePipelineKey(descriptor.VariantKey); + var pipelineKey = RHIUtility.CreateComputePipelineKey(desc.VariantKey); if (!_pipelineCache.ContainsKey(pipelineKey)) { - fixed (byte* pCSByteCode = csBytecode) + fixed (byte* pCSByteCode = desc.CsCode) { - var byteCode = new D3D12_SHADER_BYTECODE(pCSByteCode, (nuint)csBytecode.Length); - var desc = new CD3DX12_PIPELINE_STATE_STREAM_CS(in byteCode); + var byteCode = new D3D12_SHADER_BYTECODE(pCSByteCode, (nuint)desc.CsCode.Length); + var csPipelineDesc = new CD3DX12_PIPELINE_STATE_STREAM_CS(in byteCode); var streamDesc = new D3D12_PIPELINE_STATE_STREAM_DESC { - pPipelineStateSubobjectStream = &desc, + pPipelineStateSubobjectStream = &csPipelineDesc, SizeInBytes = (nuint)sizeof(CD3DX12_PIPELINE_STATE_STREAM_CS) }; - var result = CreatePSO(descriptor.VariantKey, pipelineKey, &streamDesc); + var result = CreatePSO(desc.VariantKey, pipelineKey, &streamDesc); if (result.IsFailure) { return result; diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceAllocator.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceAllocator.cs index 7a40815..e4f39f4 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceAllocator.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceAllocator.cs @@ -2,7 +2,6 @@ using Ghost.Core; using Ghost.Graphics.D3D12.Utilities; using Ghost.Graphics.RHI; using Misaki.HighPerformance.LowLevel; -using System.Diagnostics; using System.Runtime.CompilerServices; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -510,11 +509,11 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator D3D12_RESOURCE_DESC d3d12Desc; if (desc.Type == ResourceType.Texture) { - d3d12Desc = desc.TextureDescription.ToD3D12ResourceDesc(); + d3d12Desc = desc.TextureDescriptor.ToD3D12ResourceDesc(); } else { - d3d12Desc = desc.BufferDescription.ToD3D12ResourceDesc(); + d3d12Desc = desc.BufferDescriptor.ToD3D12ResourceDesc(); } var info = _device.NativeObject.Get()->GetResourceAllocationInfo(0, 1, &d3d12Desc); @@ -559,7 +558,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator public Handle CreateTexture(ref readonly TextureDesc desc, string? name = null, CreationOptions options = default) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); CheckTexture2DSize(desc.Width, desc.Height); @@ -659,7 +658,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator public Handle CreateRenderTarget(ref readonly RenderTargetDesc desc, string? name = null, CreationOptions options = default) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); var textureDesc = desc.ToTextureDescription(); return CreateTexture(in textureDesc, name, options); @@ -667,7 +666,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator public Handle CreateBuffer(ref readonly BufferDesc desc, string? name = null, CreationOptions options = default) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); CheckBufferSize(desc.Size); var resourceDesc = desc.ToD3D12ResourceDesc(); @@ -771,7 +770,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator public Identifier CreateSampler(ref readonly SamplerDesc desc) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); if (_resourceDatabase.TryGetSampler(in desc, out var id)) { diff --git a/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs b/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs index 0e5e431..7bb94ab 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs @@ -4,7 +4,6 @@ using Ghost.Graphics.RHI; using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; -using Misaki.HighPerformance.LowLevel.Utilities; using System.Diagnostics; using System.Runtime.InteropServices; using TerraFX.Interop.DirectX; @@ -16,19 +15,19 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase internal unsafe record struct ResourceRecord { [StructLayout(LayoutKind.Explicit)] - public struct ResourceUnion + public struct __resource_union { [FieldOffset(0)] public UniquePtr allocation; [FieldOffset(0)] public UniquePtr resource; - public ResourceUnion(D3D12MA_Allocation* allocation) + public __resource_union(D3D12MA_Allocation* allocation) { this.allocation = allocation; } - public ResourceUnion(ID3D12Resource* resource) + public __resource_union(ID3D12Resource* resource) { this.resource = resource; } @@ -36,7 +35,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase public ResourceDesc desc; public ResourceViewGroup viewGroup; - public ResourceUnion resource; + public __resource_union resource; public ResourceBarrierData barrierData; @@ -47,7 +46,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase public ResourceRecord(D3D12MA_Allocation* allocation, ResourceBarrierData barrierData, ResourceViewGroup viewGroup, ResourceDesc desc) { - this.resource = new ResourceUnion(allocation); + this.resource = new __resource_union(allocation); this.isExternal = false; this.viewGroup = viewGroup; @@ -57,7 +56,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase public ResourceRecord(ID3D12Resource* resource, ResourceBarrierData barrierData, ResourceViewGroup viewGroup, ResourceDesc desc) { - this.resource = new ResourceUnion(resource); + this.resource = new __resource_union(resource); this.isExternal = true; this.viewGroup = viewGroup; @@ -118,13 +117,13 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase { _descriptorAllocator = descriptorAllocator; - _resources = new UnsafeSlotMap(64, Allocator.Persistent, AllocationOption.Clear); - _samplers = new UnsafeHashMap>(32, Allocator.Persistent); + _resources = new UnsafeSlotMap(64, AllocationHandle.Persistent, AllocationOption.Clear); + _samplers = new UnsafeHashMap>(32, AllocationHandle.Persistent); #if DEBUG || GHOST_EDITOR _resourceName = new Dictionary, string>(64); #endif - _releaseQueue = new UnsafeQueue(32, Allocator.Persistent); + _releaseQueue = new UnsafeQueue(32, AllocationHandle.Persistent); } ~D3D12ResourceDatabase() @@ -134,7 +133,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase internal Handle ImportExternalResource(ID3D12Resource* pResource, ResourceBarrierData initialBarrierData, ResourceViewGroup viewGroup, ResourceDesc desc, string? name = null) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); if (pResource == null) { @@ -173,7 +172,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase internal Handle AddAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData initialBarrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); if (allocation == null) { @@ -217,25 +216,25 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase public void EnterParallelRead() { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); Interlocked.Exchange(ref _writeLock, 1); } public void ExitParallelRead() { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); Interlocked.Exchange(ref _writeLock, 0); } public bool HasResource(Handle handle) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return _resources.Contains(handle.ID, handle.Generation); } public RefResult GetResourceRecord(Handle handle) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); if (!exist) @@ -296,7 +295,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase var r = GetResourceRecord(handle); if (r.IsFailure || !r.Value.Allocated) { - return ~0u; + return uint.MaxValue; } return access switch @@ -310,7 +309,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase public string? GetResourceName(Handle handle) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); #if DEBUG || GHOST_EDITOR if (_resourceName.TryGetValue(handle, out var name)) @@ -323,7 +322,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase public void ReleaseResource(Handle handle) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); if (!_resources.TryGetElementAt(handle.ID, handle.Generation, out var record)) { @@ -342,7 +341,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase public void ReleaseResourceImmediately(Handle handle) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); if (!exist || !info.Allocated) @@ -356,7 +355,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase public Identifier AddSampler(ref readonly SamplerDesc desc, int id) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); if (_samplers.ContainsKey(desc)) { @@ -371,13 +370,13 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase public bool TryGetSampler(ref readonly SamplerDesc desc, out Identifier id) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return _samplers.TryGetValue(desc, out id); } public void ReleaseSampler(Identifier id) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); // NOTE: We almost never release samplers individually, because they are cheap and can be reused. // Ideally we would release all samplers at once when disposing the ResourceDatabase. @@ -447,13 +446,13 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase internal void BeginFrame(ulong cpuFrame) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); _cpuFrame = cpuFrame; } internal void EndFrame(ulong gpuFrame) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); while (_releaseQueue.TryPeek(out var toRelease) && toRelease.fenceValue < gpuFrame) { @@ -464,7 +463,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase internal void ReleaseAllResourcesImmediately() { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); foreach (ref var entry in _releaseQueue) { diff --git a/src/Runtime/Ghost.Graphics.D3D12/DXGISwapChain.cs b/src/Runtime/Ghost.Graphics.D3D12/DXGISwapChain.cs index 5ca9415..7a09e2f 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/DXGISwapChain.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/DXGISwapChain.cs @@ -108,7 +108,7 @@ internal unsafe class DXGISwapChain : ISwapChain public DXGISwapChain(D3D12ResourceDatabase resourceDatabase, D3D12DescriptorAllocator descriptorAllocator, D3D12RenderDevice device, SwapChainDesc desc, uint bufferCount) { - Debug.Assert(bufferCount >= 2); + Logger.DebugAssert(bufferCount >= 2); _resourceDatabase = resourceDatabase; _descriptorAllocator = descriptorAllocator; @@ -117,7 +117,7 @@ internal unsafe class DXGISwapChain : ISwapChain var pSfwapChain = CreateSwapChain(device, desc, bufferCount); _swapChain.Attach(pSfwapChain); - _backBuffers = new UnsafeArray>((int)bufferCount, Allocator.Persistent); + _backBuffers = new UnsafeArray>((int)bufferCount, AllocationHandle.Persistent); Width = desc.Width; Height = desc.Height; @@ -168,20 +168,20 @@ internal unsafe class DXGISwapChain : ISwapChain [MethodImpl(MethodImplOptions.AggressiveInlining)] public Handle GetCurrentBackBuffer() { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return _backBuffers[_swapChain.Get()->GetCurrentBackBufferIndex()]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpan> GetBackBuffers() { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return _backBuffers.AsSpan(); } public void Present(bool vsync = true) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); var presentFlags = 0u; var syncInterval = vsync ? 1u : 0u; @@ -192,7 +192,7 @@ internal unsafe class DXGISwapChain : ISwapChain public void Resize(uint width, uint height) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); if (Width == width && Height == height) { @@ -215,7 +215,7 @@ internal unsafe class DXGISwapChain : ISwapChain public void SetScale(float scaleX, float scaleY) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); var inverseScaleX = 1.0f / scaleX; var inverseScaleY = 1.0f / scaleY; diff --git a/src/Runtime/Ghost.Graphics.D3D12/Utilities/D3D12Utility.cs b/src/Runtime/Ghost.Graphics.D3D12/Utilities/D3D12Utility.cs index ad21d83..551c633 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/Utilities/D3D12Utility.cs +++ b/src/Runtime/Ghost.Graphics.D3D12/Utilities/D3D12Utility.cs @@ -53,9 +53,9 @@ internal static unsafe class D3D12Utility var ptr = uPtr.Get(); if (ptr != null) { - Debug.Assert(ptr != other); + Logger.DebugAssert(ptr != other); var refCount = ptr->Release(); - Debug.Assert(refCount == 0); + Logger.DebugAssert(refCount == 0); } uPtr = new UniquePtr(other); @@ -69,7 +69,7 @@ internal static unsafe class D3D12Utility if (ptr != null) { var refCount = ptr->Release(); - //Debug.Assert(refCount == 0); + //Logger.DebugAssert(refCount == 0); } } diff --git a/src/Runtime/Ghost.Graphics.RHI/Common.cs b/src/Runtime/Ghost.Graphics.RHI/Common.cs index aebdadd..1fb64b3 100644 --- a/src/Runtime/Ghost.Graphics.RHI/Common.cs +++ b/src/Runtime/Ghost.Graphics.RHI/Common.cs @@ -166,8 +166,6 @@ public struct ResourceRange } public readonly struct ShaderVariant; -public readonly struct GraphicsPipeline; -public readonly struct ComputePipeline; public readonly struct ShaderPass { @@ -187,11 +185,11 @@ public readonly struct ShaderPass } } -public readonly struct PassPipelineHash : IEquatable +public readonly struct PassAttachmentHash : IEquatable { public readonly UInt128 value; - public PassPipelineHash(ReadOnlySpan rtvFormats, TextureFormat dsvFormat) + public PassAttachmentHash(ReadOnlySpan rtvFormats, TextureFormat dsvFormat) { if (rtvFormats.Length > 8) { @@ -211,16 +209,21 @@ public readonly struct PassPipelineHash : IEquatable value = new UInt128(rtvPart, (ulong)dsvFormat); } - public bool Equals(PassPipelineHash other) => value == other.value; - public override bool Equals(object? obj) => obj is PassPipelineHash other && Equals(other); + public bool Equals(PassAttachmentHash other) => value == other.value; + public override bool Equals(object? obj) => obj is PassAttachmentHash other && Equals(other); public override int GetHashCode() => value.GetHashCode(); - public static bool operator ==(PassPipelineHash left, PassPipelineHash right) => left.Equals(right); - public static bool operator !=(PassPipelineHash left, PassPipelineHash right) => !(left == right); + public static bool operator ==(PassAttachmentHash left, PassAttachmentHash right) => left.Equals(right); + public static bool operator !=(PassAttachmentHash left, PassAttachmentHash right) => !(left == right); } -public ref struct GraphicsPSODescriptor +public ref struct GraphicsPSODesc { + public UInt128 CompiledHash + { + get; set; + } + public Key64 VariantKey { get; set; @@ -240,14 +243,39 @@ public ref struct GraphicsPSODescriptor { get; set; } + + public ReadOnlySpan AsCode + { + get; set; + } + + public ReadOnlySpan MsCode + { + get; set; + } + + public ReadOnlySpan PsCode + { + get; set; + } } -public ref struct ComputePSODescriptor +public ref struct ComputePSODesc { + public UInt128 CompiledHash + { + get; set; + } + public Key64 VariantKey { get; set; } + + public ReadOnlySpan CsCode + { + get; set; + } } public readonly struct CBufferPropertyInfo @@ -640,7 +668,7 @@ public struct BarrierDesc public record struct ResourceDesc { [StructLayout(LayoutKind.Explicit)] - internal struct resource_union + internal struct __union { [FieldOffset(0)] public TextureDesc textureDescription; @@ -648,7 +676,7 @@ public record struct ResourceDesc public BufferDesc bufferDescription; } - private resource_union _desc; + private __union _desc; public ResourceType Type { @@ -656,21 +684,21 @@ public record struct ResourceDesc } [UnscopedRef] - public ref TextureDesc TextureDescription + public ref TextureDesc TextureDescriptor { get { - Debug.Assert(Type == ResourceType.Texture); + Logger.DebugAssert(Type == ResourceType.Texture); return ref _desc.textureDescription; } } [UnscopedRef] - public ref BufferDesc BufferDescription + public ref BufferDesc BufferDescriptor { get { - Debug.Assert(Type == ResourceType.Buffer); + Logger.DebugAssert(Type == ResourceType.Buffer); return ref _desc.bufferDescription; } } @@ -680,7 +708,7 @@ public record struct ResourceDesc return new ResourceDesc { Type = ResourceType.Buffer, - BufferDescription = desc + BufferDescriptor = desc }; } @@ -689,7 +717,7 @@ public record struct ResourceDesc return new ResourceDesc { Type = ResourceType.Texture, - TextureDescription = desc + TextureDescriptor = desc }; } @@ -702,8 +730,8 @@ public record struct ResourceDesc return Type switch { - ResourceType.Texture => TextureDescription.Equals(other.TextureDescription), - ResourceType.Buffer => BufferDescription.Equals(other.BufferDescription), + ResourceType.Texture => TextureDescriptor.Equals(other.TextureDescriptor), + ResourceType.Buffer => BufferDescriptor.Equals(other.BufferDescriptor), _ => throw new InvalidOperationException($"Unknown resource type: {Type}") }; } @@ -712,8 +740,8 @@ public record struct ResourceDesc { return Type switch { - ResourceType.Texture => HashCode.Combine(Type, TextureDescription), - ResourceType.Buffer => HashCode.Combine(Type, BufferDescription), + ResourceType.Texture => HashCode.Combine(Type, TextureDescriptor), + ResourceType.Buffer => HashCode.Combine(Type, BufferDescriptor), _ => throw new InvalidOperationException($"Unknown resource type: {Type}") }; } @@ -722,8 +750,8 @@ public record struct ResourceDesc { return Type switch { - ResourceType.Texture => $"Texture: {TextureDescription}", - ResourceType.Buffer => $"Buffer: {BufferDescription}", + ResourceType.Texture => $"Texture: {TextureDescriptor}", + ResourceType.Buffer => $"Buffer: {BufferDescriptor}", _ => $"Unknown resource type: {Type}" }; } @@ -1649,4 +1677,4 @@ public enum SetWorkGraphFlags { None, Initialize -} \ No newline at end of file +} diff --git a/src/Runtime/Ghost.Graphics.RHI/ICommandBuffer.cs b/src/Runtime/Ghost.Graphics.RHI/ICommandBuffer.cs index eb3f420..8ec674f 100644 --- a/src/Runtime/Ghost.Graphics.RHI/ICommandBuffer.cs +++ b/src/Runtime/Ghost.Graphics.RHI/ICommandBuffer.cs @@ -1,4 +1,5 @@ using Ghost.Core; +using Ghost.Core.Graphics; namespace Ghost.Graphics.RHI; @@ -97,7 +98,7 @@ public interface ICommandBuffer : IRHIObject /// Sets the pipeline state object /// /// Pipeline state to set - void SetPipelineState(Key128 pipelineKey); + void SetPipelineState(Key128 pipelineKey); /// /// Sets the constant buffer view for the specified slot in the graphics pipeline. @@ -216,4 +217,4 @@ public interface ICommandBuffer : IRHIObject /// A handle to an intermediate GPU resource used to stage the subresource data before copying to the destination resource. /// A span containing the data for each subresource to update. Each element represents a subresource and its associated data. void UpdateSubResources(Handle resource, Handle intermediate, params ReadOnlySpan subResources); -} \ No newline at end of file +} diff --git a/src/Runtime/Ghost.Graphics.RHI/IPipelineLibrary.cs b/src/Runtime/Ghost.Graphics.RHI/IPipelineLibrary.cs index 24f80e5..ac2c5b3 100644 --- a/src/Runtime/Ghost.Graphics.RHI/IPipelineLibrary.cs +++ b/src/Runtime/Ghost.Graphics.RHI/IPipelineLibrary.cs @@ -1,4 +1,5 @@ using Ghost.Core; +using Ghost.Core.Graphics; namespace Ghost.Graphics.RHI; @@ -6,6 +7,6 @@ public interface IPipelineLibrary : IDisposable { void SaveLibraryToDisk(string filePath); bool HasPipelineStateObject(UInt128 key); - Result> CreateGraphicsPipeline(ref readonly GraphicsPSODescriptor descriptor, ReadOnlySpan asByteCode, ReadOnlySpan msByteCode, ReadOnlySpan psByteCode); - Result> CreateComputePipeline(ref readonly ComputePSODescriptor descriptor, ReadOnlySpan csByteCode); + Result> CreateGraphicsPipeline(ref readonly GraphicsPSODesc desc); + Result> CreateComputePipeline(ref readonly ComputePSODesc desc); } diff --git a/src/Runtime/Ghost.Graphics.RHI/Keyword.cs b/src/Runtime/Ghost.Graphics.RHI/Keyword.cs index 324a3cb..cb5c5be 100644 --- a/src/Runtime/Ghost.Graphics.RHI/Keyword.cs +++ b/src/Runtime/Ghost.Graphics.RHI/Keyword.cs @@ -39,7 +39,7 @@ public unsafe struct LocalKeywordSet } } - public ulong GetHash64() + public ulong GetHashCode64() { var hash = 14695981039346656037ul; // FNV Offset basis @@ -124,4 +124,4 @@ public unsafe struct LocalKeywordSet return result; } -} \ No newline at end of file +} diff --git a/src/Runtime/Ghost.Graphics.RHI/RHIUtility.cs b/src/Runtime/Ghost.Graphics.RHI/RHIUtility.cs index bed7cd3..60d6dbd 100644 --- a/src/Runtime/Ghost.Graphics.RHI/RHIUtility.cs +++ b/src/Runtime/Ghost.Graphics.RHI/RHIUtility.cs @@ -10,11 +10,11 @@ namespace Ghost.Graphics.RHI; public static class RHIUtility { public const int MAX_RENDER_TARGETS = 8; + public const ulong SHADER_ID_MASK = 0xFFFFFFFFFFFFFFF0ul; public const ulong PIPELINE_KEY_MASK = 0xFFFFFFFFFFFFFFF0ul; public const ulong GRAPHICS_PIPELINE_KEY_FLAG = 0x1ul; public const ulong COMPUTE_PIPELINE_KEY_FLAG = 0x2ul; - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetBytesPerPixel(this TextureFormat format) { @@ -149,6 +149,20 @@ public static class RHIUtility } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong GetShaderID(string shaderName) + { + var hash = XxHash64.HashToUInt64(MemoryMarshal.AsBytes(shaderName.AsSpan())); + return hash & SHADER_ID_MASK; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong GetPassID(ulong shaderID, int passIndex) + { + Logger.DebugAssert(passIndex >= 0 && passIndex < 16, "Pass index must be between 0 and 15 to fit within the shader ID mask."); + return shaderID | ((ulong)passIndex & 0xFul); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Key64 CreateShaderPassKey(ulong passID, ulong compiledHash) { @@ -158,11 +172,11 @@ public static class RHIUtility [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Key64 CreateShaderVariantKey(ulong passKey, ref readonly LocalKeywordSet keywords) { - var keywordHash = keywords.GetHash64(); + var keywordHash = keywords.GetHashCode64(); return new Key64(Hash.Combine64(passKey, keywordHash)); } - public static unsafe Key128 CreateGraphicsPipelineKey(Key64 shaderVariantKey, PipelineState pipelineState, PassPipelineHash passKey) + public static unsafe Key128 CreateGraphicsPipelineKey(ulong compiledHash, PipelineState pipelineState, PassAttachmentHash passAttachmentHash) { // Order-sensitive 128-bit mix. Cheap and stable, avoids span hashing. static ulong Mix64(ulong x) @@ -175,10 +189,10 @@ public static class RHIUtility return x; } - var mLo = shaderVariantKey.Value; + var mLo = compiledHash; var mHi = pipelineState.GetHashCode64(); - var pPasskey = (ulong*)&passKey.value; + var pPasskey = (ulong*)&passAttachmentHash.value; var pLo = pPasskey[0]; var pHi = pPasskey[1]; @@ -188,19 +202,25 @@ public static class RHIUtility lo = lo & PIPELINE_KEY_MASK | GRAPHICS_PIPELINE_KEY_FLAG; // Ensure graphics pipeline keys are distinguishable from compute pipeline keys. - return new Key128(new UInt128(hi, lo)); + return new Key128(new UInt128(hi, lo)); } - public static Key128 CreateComputePipelineKey(Key64 shaderVariantKey) + public static Key128 CreateComputePipelineKey(ulong compiledHash) { - var shaderHash = shaderVariantKey.Value; - var stateHash = ~shaderVariantKey.Value; + // Since compute shader don't have blend state or attachment configurations, we can afford a simpler key generation. + // Just use the compiled hash with a distinct flag to avoid collisions with graphics pipeline keys. +#if true + return new Key128(new UInt128(compiledHash, compiledHash ^ COMPUTE_PIPELINE_KEY_FLAG)); +#else + var shaderHash = compiledHash; + var stateHash = ~compiledHash; // Simple XOR mix. Not as robust as the graphics pipeline key, but sufficient for compute shaders which have fewer variants. var hi = shaderHash ^ (stateHash + 0x9E3779B97F4A7C15ul) ^ (shaderHash * 0xD6E8FEB86659FD93ul); var lo = stateHash ^ (shaderHash + 0xC2B2AE3D27D4EB4Ful) ^ (stateHash * 0x165667B19E3779F9ul); lo = lo & PIPELINE_KEY_MASK | COMPUTE_PIPELINE_KEY_FLAG; // Ensure compute pipeline keys are distinguishable from graphics pipeline keys. return new Key128(new UInt128(hi, lo)); +#endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -208,4 +228,4 @@ public static class RHIUtility { return key.TryFormat(destination, out var _, "X16"); } -} \ No newline at end of file +} diff --git a/src/Runtime/Ghost.Graphics.RHI/RootSignatureLayout.cs b/src/Runtime/Ghost.Graphics.RHI/RootSignatureLayout.cs index 6299aa4..caadfb9 100644 --- a/src/Runtime/Ghost.Graphics.RHI/RootSignatureLayout.cs +++ b/src/Runtime/Ghost.Graphics.RHI/RootSignatureLayout.cs @@ -14,7 +14,7 @@ public static class RootSignatureLayout public struct PushConstantsData { public const uint NUM_32BITS_VALUE = 12u / sizeof(uint); - + [FieldOffset(0)] public uint frameBuffer; [FieldOffset(4)] @@ -23,6 +23,11 @@ public struct PushConstantsData public uint instanceIndex; [FieldOffset(8)] public uint propertyBuffer; + + public ReadOnlySpan AsUInts() + { + return MemoryMarshal.CreateReadOnlySpan(ref frameBuffer, (int)NUM_32BITS_VALUE); + } } [StructLayout(LayoutKind.Sequential)] diff --git a/src/Runtime/Ghost.Graphics/Core/Material.cs b/src/Runtime/Ghost.Graphics/Core/Material.cs index 151cf20..f81476f 100644 --- a/src/Runtime/Ghost.Graphics/Core/Material.cs +++ b/src/Runtime/Ghost.Graphics/Core/Material.cs @@ -24,7 +24,7 @@ internal struct CBufferCache : IResourceReleasable public CBufferCache(Handle buffer, uint bufferSize) { _size = bufferSize; - _cpuData = new UnsafeArray((int)bufferSize, Allocator.Persistent); + _cpuData = new UnsafeArray((int)bufferSize, AllocationHandle.Persistent); _gpuResource = buffer; } @@ -93,7 +93,7 @@ public struct Material : IResourceReleasable { if (!_passPipelineOverride.IsCreated) { - _passPipelineOverride = new UnsafeArray(shader.PassCount, Allocator.Persistent); + _passPipelineOverride = new UnsafeArray(shader.PassCount, AllocationHandle.Persistent); } else { @@ -104,7 +104,7 @@ public struct Material : IResourceReleasable _keywordMask.Clear(); for (var i = 0; i < shader.PassCount; i++) { - ref var pass = ref shader.GetPassReference(i); + ref readonly var pass = ref shader.GetPassReference(i); _passPipelineOverride[i] = new PipelineOverride { shaderPass = pass.Key, diff --git a/src/Runtime/Ghost.Graphics/Core/MaterialPaletteStore.cs b/src/Runtime/Ghost.Graphics/Core/MaterialPaletteStore.cs index 2101986..db5ff9d 100644 --- a/src/Runtime/Ghost.Graphics/Core/MaterialPaletteStore.cs +++ b/src/Runtime/Ghost.Graphics/Core/MaterialPaletteStore.cs @@ -50,8 +50,8 @@ internal sealed class MaterialPaletteStore : IDisposable initialCapacity = 16; } - _entries = new UnsafeList(initialCapacity + 1, Allocator.Persistent); - _lookup = new UnsafeHashMap(initialCapacity * 2, Allocator.Persistent); + _entries = new UnsafeList(initialCapacity + 1, AllocationHandle.Persistent); + _lookup = new UnsafeHashMap(initialCapacity * 2, AllocationHandle.Persistent); _freeListHead = 0; } @@ -131,7 +131,7 @@ internal sealed class MaterialPaletteStore : IDisposable if (!newEntry.materials.IsCreated) { - newEntry.materials = new UnsafeList>(materials.Length, Allocator.Persistent); + newEntry.materials = new UnsafeList>(materials.Length, AllocationHandle.Persistent); } else { diff --git a/src/Runtime/Ghost.Graphics/Core/Mesh.cs b/src/Runtime/Ghost.Graphics/Core/Mesh.cs index e39c3ff..4fb862e 100644 --- a/src/Runtime/Ghost.Graphics/Core/Mesh.cs +++ b/src/Runtime/Ghost.Graphics/Core/Mesh.cs @@ -186,8 +186,8 @@ public struct Mesh : IResourceReleasable internal Mesh(ReadOnlySpan vertices, ReadOnlySpan indices, Handle vertexBuffer, Handle indexBuffer) { - Vertices = new UnsafeList(vertices.Length, Allocator.Persistent); - Indices = new UnsafeList(indices.Length, Allocator.Persistent); + Vertices = new UnsafeList(vertices.Length, AllocationHandle.Persistent); + Indices = new UnsafeList(indices.Length, AllocationHandle.Persistent); Vertices.CopyFrom(vertices); Indices.CopyFrom(indices); VertexBuffer = vertexBuffer; @@ -298,10 +298,10 @@ public struct Mesh : IResourceReleasable ref var data = ref mesh->_meshletData; // Ensure lists are initialized - if (!data.groups.IsCreated) data.groups = new UnsafeList(16, Allocator.Persistent); - if (!data.meshlets.IsCreated) data.meshlets = new UnsafeList(64, Allocator.Persistent); - if (!data.meshletVertices.IsCreated) data.meshletVertices = new UnsafeList(128, Allocator.Persistent); - if (!data.meshletTriangles.IsCreated) data.meshletTriangles = new UnsafeList(128, Allocator.Persistent); + if (!data.groups.IsCreated) data.groups = new UnsafeList(16, AllocationHandle.Persistent); + if (!data.meshlets.IsCreated) data.meshlets = new UnsafeList(64, AllocationHandle.Persistent); + if (!data.meshletVertices.IsCreated) data.meshletVertices = new UnsafeList(128, AllocationHandle.Persistent); + if (!data.meshletTriangles.IsCreated) data.meshletTriangles = new UnsafeList(128, AllocationHandle.Persistent); var meshletGroup = new MeshletGroup { diff --git a/src/Runtime/Ghost.Graphics/Core/RenderContext.cs b/src/Runtime/Ghost.Graphics/Core/RenderContext.cs index ca7e336..50ac5ce 100644 --- a/src/Runtime/Ghost.Graphics/Core/RenderContext.cs +++ b/src/Runtime/Ghost.Graphics/Core/RenderContext.cs @@ -4,7 +4,8 @@ using Ghost.Graphics.Services; using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Utilities; -using System.Diagnostics; +using Misaki.HighPerformance.Mathematics; +using System.Runtime.InteropServices; namespace Ghost.Graphics.Core; @@ -12,21 +13,23 @@ namespace Ghost.Graphics.Core; public readonly unsafe ref struct RenderContext { private readonly ResourceManager _resourceManager; + private readonly ShaderLibrary _shaderLibrary; private readonly IGraphicsEngine _engine; - private readonly ICommandBuffer _cmd; + private readonly ICommandBuffer _commandBuffer; - public ICommandBuffer CommandBuffer => _cmd; + public ICommandBuffer CommandBuffer => _commandBuffer; public ResourceManager ResourceManager => _resourceManager; public IResourceAllocator ResourceAllocator => _engine.ResourceAllocator; public IResourceDatabase ResourceDatabase => _engine.ResourceDatabase; public IPipelineLibrary PipelineLibrary => _engine.PipelineLibrary; - internal RenderContext(ResourceManager resourceManager, IGraphicsEngine engine, ICommandBuffer cmd) + internal RenderContext(ResourceManager resourceManager, ShaderLibrary shaderLibrary, IGraphicsEngine engine, ICommandBuffer cmd) { _resourceManager = resourceManager; + _shaderLibrary = shaderLibrary; _engine = engine; - _cmd = cmd; + _commandBuffer = cmd; } private void TransitionBarrier(Handle resource, bool isTexture, BarrierLayout newLayout, BarrierAccess newAccess, BarrierSync newSync) @@ -41,7 +44,7 @@ public readonly unsafe ref struct RenderContext desc = BarrierDesc.Buffer(resource, newSync, newAccess); } - _cmd.Barrier(desc); + _commandBuffer.Barrier(desc); } public void UploadBuffer(Handle buffer, params ReadOnlySpan data) @@ -53,10 +56,10 @@ public readonly unsafe ref struct RenderContext return; } - Debug.Assert(r.Value.Type == ResourceType.Buffer); + Logger.DebugAssert(r.Value.Type == ResourceType.Buffer); var sizeInBytes = (nuint)(data.Length * sizeof(T)); - var memoryType = r.Value.BufferDescription.HeapType; + var memoryType = r.Value.BufferDescriptor.HeapType; if (memoryType == HeapType.Upload) { @@ -89,7 +92,7 @@ public readonly unsafe ref struct RenderContext _engine.ResourceDatabase.UnmapResource(uploadHandle.AsResource(), 0, null); } - _cmd.CopyBuffer(buffer, uploadHandle, 0, 0, sizeInBytes); + _commandBuffer.CopyBuffer(buffer, uploadHandle, 0, 0, sizeInBytes); } } @@ -127,8 +130,8 @@ public readonly unsafe ref struct RenderContext public Handle CreateMesh(ReadOnlySpan vertices, ReadOnlySpan indices, bool staticMesh) { - var vertexList = new UnsafeList(vertices.Length, Allocator.Persistent); - var indexList = new UnsafeList(indices.Length, Allocator.Persistent); + var vertexList = new UnsafeList(vertices.Length, AllocationHandle.Persistent); + var indexList = new UnsafeList(indices.Length, AllocationHandle.Persistent); vertexList.CopyFrom(vertices); indexList.CopyFrom(indices); @@ -262,7 +265,7 @@ public readonly unsafe ref struct RenderContext where T : unmanaged { var desc = ResourceDatabase.GetResourceDescription(texture.AsResource()).GetValueOrThrow(); - desc.TextureDescription.Format.GetSurfaceInfo(desc.TextureDescription.Width, desc.TextureDescription.Height, out var rowPitch, out var slicePitch, out _); + desc.TextureDescriptor.Format.GetSurfaceInfo(desc.TextureDescriptor.Width, desc.TextureDescriptor.Height, out var rowPitch, out var slicePitch, out _); var requiredSize = ResourceDatabase.GetIntermediateResourceSize(texture.AsResource(), 0, 1); var uploadDesc = new BufferDesc @@ -289,7 +292,89 @@ public readonly unsafe ref struct RenderContext slicePitch = slicePitch }; - _cmd.UpdateSubResources(texture.AsResource(), uploadHandle.AsResource(), subresourceData); + _commandBuffer.UpdateSubResources(texture.AsResource(), uploadHandle.AsResource(), subresourceData); } } + + public void DispatchCompute(Handle compute, int entryIndex, ref readonly LocalKeywordSet keywordSet, ref readonly T property, uint3 threadGroupCount) + where T : unmanaged + { + ref var shader = ref ResourceManager.GetComputeShaderReference(compute).GetValueOrThrow(); + + var entryHash = shader.GetEntryID(entryIndex); + var variantKey = RHIUtility.CreateShaderVariantKey(entryHash, in keywordSet); + + // TODO: Refactor this into a helper method. + var (compiledHash, error) = _shaderLibrary.GetCompiledHash(variantKey); + if (error.IsFailure) + { + // TODO: Fallback to an error material. + Logger.Debug($"No compiled shader found for compute shader {shader.UniqueID} with entry point {entryIndex} and keywords {keywordSet}."); + return; + } + + var pipelineKey = RHIUtility.CreateComputePipelineKey(compiledHash); + + if (!PipelineLibrary.HasPipelineStateObject(pipelineKey)) + { + using var scope = AllocationManager.CreateStackScope(); + var compiledCacheResult = _shaderLibrary.GetCompiledCache(shader.UniqueID, entryIndex, scope.AllocationHandle); + if (compiledCacheResult.IsFailure) + { + // TODO: Fallback to a checkerboard shader. + throw new InvalidOperationException("Failed to load compiled shader cache for pipeline state object creation."); + } + + var cache = compiledCacheResult.Value; + Logger.DebugAssert(cache.compiledHash == compiledHash); + + ShaderLibrary.ParseCacheData(cache.byteCode, out _, out var byteCodeOffsets, out var byteCodes); + Logger.DebugAssert(byteCodeOffsets.Length == 1); + + var psoDes = new ComputePSODesc + { + CompiledHash = compiledHash, + VariantKey = variantKey, + CsCode = byteCodes.Slice((int)byteCodeOffsets[0]), + }; + + PipelineLibrary.CreateComputePipeline(in psoDes).GetValueOrThrow(); + } + + _commandBuffer.SetPipelineState(pipelineKey); + + + var propertySpan = MemoryMarshal.AsBytes(new ReadOnlySpan(in property)); + // TODO: Placed resource has 64k alignment requirement, which can waste lots of memory. We can allocate a large buffer and slice it for each dispatch to avoid this issue. + var propertyBufferDesc = new BufferDesc + { + Size = (uint)propertySpan.Length, + Stride = (uint)sizeof(T), + Usage = BufferUsage.Raw | BufferUsage.ShaderResource, + HeapType = HeapType.Upload, + }; + var properyBuffer = ResourceManager.CreateTransientBuffer(in propertyBufferDesc); + + var mappedData = ResourceDatabase.MapResource(properyBuffer.AsResource(), 0, null); + Logger.DebugAssert(mappedData != null, "Failed to map property buffer."); + + fixed (byte* pData = propertySpan) + { + MemoryUtility.MemCpy(mappedData, pData, (nuint)propertySpan.Length); + } + + error = ResourceDatabase.UnmapResource(properyBuffer.AsResource(), 0, null); + Logger.DebugAssert(error.IsSuccess, $"Failed to unmap property buffer: {error}."); + + var pushConstant = new PushConstantsData + { + // TODO: Support frame and view buffer. + frameBuffer = 0, + viewBuffer = 0, + propertyBuffer = ResourceDatabase.GetBindlessIndex(properyBuffer.AsResource()), + }; + + _commandBuffer.SetGraphicsRoot32Constants(0, pushConstant.AsUInts()); + _commandBuffer.DispatchCompute(threadGroupCount.x, threadGroupCount.y, threadGroupCount.z); + } } diff --git a/src/Runtime/Ghost.Graphics/Core/RenderRequest.cs b/src/Runtime/Ghost.Graphics/Core/RenderRequest.cs index d516dac..72773d6 100644 --- a/src/Runtime/Ghost.Graphics/Core/RenderRequest.cs +++ b/src/Runtime/Ghost.Graphics/Core/RenderRequest.cs @@ -18,21 +18,21 @@ public enum GateFit : uint public struct Frustum { [InlineArray(6)] - public struct plane_array + public struct __plane_array { private float4 plane; } [InlineArray(8)] - public struct corner_array + public struct __corner_array { private float3 corner; } - public plane_array planes; - public corner_array corners; + public __plane_array planes; + public __corner_array corners; - public static void CalculateFrustumPlanes(float4x4 finalMatrix, ref plane_array outPlanes) + public static void CalculateFrustumPlanes(float4x4 finalMatrix, ref __plane_array outPlanes) { const int planeFrustumLeft = 0; const int planeFrustumRight = 1; diff --git a/src/Runtime/Ghost.Graphics/Core/Shader.cs b/src/Runtime/Ghost.Graphics/Core/Shader.cs index 1f62800..dc98fa3 100644 --- a/src/Runtime/Ghost.Graphics/Core/Shader.cs +++ b/src/Runtime/Ghost.Graphics/Core/Shader.cs @@ -3,7 +3,6 @@ using Ghost.Core.Graphics; using Ghost.Graphics.RHI; using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -16,9 +15,6 @@ public partial struct Shader private static readonly Dictionary s_passNameToID = new Dictionary(); private static int s_nextPassID = 0; - private static readonly Dictionary s_propertyNameToID = new Dictionary(); - private static int s_nextPropertyID = 0; - private static readonly Dictionary s_keywordNameToID = new Dictionary(); private static readonly Dictionary s_keywordIDToName = new Dictionary(); private static int s_nextKeywordID = 0; @@ -34,17 +30,6 @@ public partial struct Shader return id; } - public static Identifier GetPropertyID(string propertyName) - { - ref var id = ref CollectionsMarshal.GetValueRefOrAddDefault(s_propertyNameToID, propertyName, out var exists); - if (!exists) - { - id = s_nextPropertyID++; - } - - return id; - } - public static int GetKeywordID(string keywordName) { ref var id = ref CollectionsMarshal.GetValueRefOrAddDefault(s_keywordNameToID, keywordName, out var exists); @@ -75,6 +60,7 @@ public partial struct Shader /// public partial struct Shader : IResourceReleasable { + private readonly ulong _nameHash; private readonly uint _propertyBufferSize; private UnsafeArray _shaderPasses; private UnsafeHashMap _passIDToLocal; @@ -83,15 +69,17 @@ public partial struct Shader : IResourceReleasable // TODO: Tag to pass index for fast lookup. // We can use a int array since the number and index of tags are fixed at compile time. + public readonly ulong UniqueID => _nameHash; public readonly int PassCount => _shaderPasses.Count; public readonly uint PropertyBufferSize => _propertyBufferSize; internal Shader(GraphicsShaderDescriptor descriptor) { + _nameHash = RHIUtility.GetShaderID(descriptor.name); _propertyBufferSize = descriptor.propertyBufferSize; - _shaderPasses = new UnsafeArray(descriptor.passes.Length, Allocator.Persistent); - _passIDToLocal = new UnsafeHashMap(descriptor.passes.Length, Allocator.Persistent); - _keywordIDToLocal = new UnsafeHashMap(32, Allocator.Persistent); + _shaderPasses = new UnsafeArray(descriptor.passes.Length, AllocationHandle.Persistent); + _passIDToLocal = new UnsafeHashMap(descriptor.passes.Length, AllocationHandle.Persistent); + _keywordIDToLocal = new UnsafeHashMap(32, AllocationHandle.Persistent); for (var i = 0; i < descriptor.passes.Length; i++) { @@ -129,7 +117,7 @@ public partial struct Shader : IResourceReleasable _shaderPasses[i] = new ShaderPass { - Key = pass.identifier, + Key = RHIUtility.GetPassID(_nameHash, i), DefaultState = pass.localPipeline, KeywordIDs = keywords, }; @@ -172,7 +160,7 @@ public partial struct Shader : IResourceReleasable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ref ShaderPass GetPassReference(int index) + public ref readonly ShaderPass GetPassReference(int index) { return ref _shaderPasses[index]; } @@ -200,25 +188,26 @@ public partial struct Shader : IResourceReleasable public unsafe partial struct ComputeShader : IResourceReleasable { - private fixed ulong _entryHash[8]; - private readonly int _entryPointCount; + private readonly ulong _nameHash; + private fixed ulong entryHashes[8]; // Support up to 8 entry points for now, can be extended if needed. private readonly uint _propertyBufferSize; private LocalKeywordSet _localKeywordSet; private UnsafeHashMap _keywordIDToLocal; + public readonly ulong UniqueID => _nameHash; public readonly uint PropertyBufferSize => _propertyBufferSize; internal ComputeShader(ComputeShaderDescriptor descriptor) { + _nameHash = RHIUtility.GetShaderID(descriptor.name); _propertyBufferSize = descriptor.propertyBufferSize; - _entryPointCount = descriptor.shaderCodes.Length; - _keywordIDToLocal = new UnsafeHashMap(32, Allocator.Persistent); + _keywordIDToLocal = new UnsafeHashMap(32, AllocationHandle.Persistent); for (var i = 0; i < descriptor.shaderCodes.Length; i++) { - _entryHash[i] = descriptor.shaderCodes[i].GetHashCode64(); + entryHashes[i] = RHIUtility.GetPassID(_nameHash, i); } var localKeywordIndex = 0; @@ -244,10 +233,10 @@ public unsafe partial struct ComputeShader : IResourceReleasable } } - public ulong GetEntryHash(int entryPointIndex) + public ulong GetEntryID(int entryIndex) { - Debug.Assert(entryPointIndex >= 0 && entryPointIndex < _entryPointCount); - return _entryHash[entryPointIndex]; + Logger.DebugAssert(entryIndex >= 0 && entryIndex < 8, "Entry index out of bounds."); + return entryHashes[entryIndex]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Runtime/Ghost.Graphics/Ghost.Graphics.csproj b/src/Runtime/Ghost.Graphics/Ghost.Graphics.csproj index 8e3d688..4002b0b 100644 --- a/src/Runtime/Ghost.Graphics/Ghost.Graphics.csproj +++ b/src/Runtime/Ghost.Graphics/Ghost.Graphics.csproj @@ -17,10 +17,6 @@ True - - - - all diff --git a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraph.cs b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraph.cs index 13fd791..9003b62 100644 --- a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraph.cs +++ b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraph.cs @@ -1,7 +1,6 @@ using Ghost.Core; using Ghost.Graphics.RHI; using System.Diagnostics; -using Ghost.Graphics.Services; namespace Ghost.Graphics.RenderGraphModule; @@ -114,7 +113,7 @@ public sealed class RenderGraph : IDisposable } var desc = r.Value; - return _resources.ImportTexture(in desc.TextureDescription, texture, name, clearColor, clearDepth, clearStencil, clearAtFirstUse, discardAtLastUse); + return _resources.ImportTexture(in desc.TextureDescriptor, texture, name, clearColor, clearDepth, clearStencil, clearAtFirstUse, discardAtLastUse); } /// @@ -132,7 +131,7 @@ public sealed class RenderGraph : IDisposable } var desc = r.Value; - return _resources.ImportBuffer(in desc.BufferDescription, buffer, name); + return _resources.ImportBuffer(in desc.BufferDescriptor, buffer, name); } public IRasterRenderGraphBuilder AddRasterRenderPass(string name, out TPassData passData) diff --git a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs index 90fbaf6..14bec8e 100644 --- a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs +++ b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphAliasing.cs @@ -1,3 +1,4 @@ +using Ghost.Core; using Ghost.Core.Utilities; using Ghost.Graphics.RHI; using System.Diagnostics; @@ -359,7 +360,7 @@ internal sealed class ResourceAliasingManager logicalIndex, alignment); - Debug.Assert(success, "Simulation allocation failed - heap should be unlimited in size"); + Logger.DebugAssert(success, "Simulation allocation failed - heap should be unlimited in size"); } // Get peak usage from simulation diff --git a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs index e35b488..612073d 100644 --- a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs +++ b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphBuilder.cs @@ -279,7 +279,7 @@ internal class RenderGraphBuilder : IRasterRenderGraphBuilder, IComputeRenderGra { ThrowIfDisposed(); - Debug.Assert(index >= 0 && index < _pass.colorAccess.Length, "Color attachment index out of range."); + Logger.DebugAssert(index >= 0 && index < _pass.colorAccess.Length, "Color attachment index out of range."); var id = UseTexture(texture, flags); if (_pass.colorAccess[index].id == id || _pass.colorAccess[index].id.IsInvalid) diff --git a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphContext.cs b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphContext.cs index 9e750d1..1008e0b 100644 --- a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphContext.cs +++ b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphContext.cs @@ -2,6 +2,7 @@ using Ghost.Core; using Ghost.Graphics.Core; using Ghost.Graphics.RHI; using Ghost.Graphics.Services; +using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.Mathematics; namespace Ghost.Graphics.RenderGraphModule; @@ -30,14 +31,16 @@ public interface IRasterRenderContext : IRenderGraphContext void SetActiveMaterial(Handle material); void SetActiveMaterial(ref readonly Material material); + [Obsolete("No point to set active mesh anymore in gpu driven pipeline.")] void SetActiveMesh(Handle mesh); + [Obsolete("No point to set active mesh anymore in gpu driven pipeline.")] void SetActiveMesh(ref readonly Mesh mesh); void DispatchMesh(uint3 threadGroupCount); } public interface IComputeRenderContext : IRenderGraphContext { - void SetActiveCompute(Handle computeShader, uint entryIndex); + void SetActiveCompute(Handle computeShader, int entryIndex); void DispatchCompute(uint3 threadGroupCount); } @@ -160,35 +163,58 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext } ref var shader = ref shaderResult.Value; - ref var pass = ref shader.GetPassReference(material.ActivePassIndex); + ref readonly var pass = ref shader.GetPassReference(material.ActivePassIndex); - var passPipelineHash = new PassPipelineHash(_rtvFormats, _dsvFormat); + var passPipelineHash = new PassAttachmentHash(_rtvFormats, _dsvFormat); var materialPipeline = material.GetPassPipelineOverride(material.ActivePassIndex); // Mask out the keywords that are not used in this pass. var variantMask = material._keywordMask & pass.KeywordIDs; - var shaderVariantKey = RHIUtility.CreateShaderVariantKey(pass.Key, in variantMask); - var pipelineKey = RHIUtility.CreateGraphicsPipelineKey(shaderVariantKey, materialPipeline, passPipelineHash); + var variantKey = RHIUtility.CreateShaderVariantKey(pass.Key, in variantMask); + + var (compiledHash, error) = _shaderLibrary.GetCompiledHash(variantKey); + if (error.IsFailure) + { + // TODO: Fallback to a default shader or show an error material. + return; + } + + var pipelineKey = RHIUtility.CreateGraphicsPipelineKey(compiledHash, materialPipeline, passPipelineHash); if (!_pipelineLibrary.HasPipelineStateObject(pipelineKey)) { - var compiledCacheResult = _shaderLibrary.GetCache(shaderVariantKey); + using var scope = AllocationManager.CreateStackScope(); + var compiledCacheResult = _shaderLibrary.GetCompiledCache(shader.UniqueID, material.ActivePassIndex, scope.AllocationHandle); if (compiledCacheResult.IsFailure) { throw new InvalidOperationException("Failed to load compiled shader cache for pipeline state object creation."); } - var psoDes = new GraphicsPSODescriptor + var cache = compiledCacheResult.Value; + Logger.DebugAssert(cache.compiledHash == compiledHash); + + ShaderLibrary.ParseCacheData(cache.byteCode, out _, out var byteCodeOffsets, out var byteCodes); + Logger.DebugAssert(byteCodeOffsets.Length == 3); // as, ms, ps + + var asByteCode = byteCodes.Slice((int)byteCodeOffsets[0], (int)(byteCodeOffsets[1] - byteCodeOffsets[0])); + var msByteCode = byteCodes.Slice((int)byteCodeOffsets[1], (int)(byteCodeOffsets[2] - byteCodeOffsets[1])); + var psByteCode = byteCodes.Slice((int)byteCodeOffsets[2]); + + var psoDes = new GraphicsPSODesc { - VariantKey = shaderVariantKey, + CompiledHash = compiledHash, + VariantKey = variantKey, PipelineOption = materialPipeline, RtvFormats = _rtvFormats.AsSpan(0, _rtvCount), DsvFormat = _dsvFormat, + + AsCode = asByteCode, + MsCode = msByteCode, + PsCode = psByteCode, }; - var compiled = compiledCacheResult.Value; - _pipelineLibrary.CreateGraphicsPipeline(in psoDes, in compiled).GetValueOrThrow(); + _pipelineLibrary.CreateGraphicsPipeline(in psoDes).GetValueOrThrow(); } _activePerMaterialData = material._cBufferCache.GpuResource; @@ -238,7 +264,7 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext _commandBuffer.DispatchMesh(threadGroupCount.x, threadGroupCount.y, threadGroupCount.z); } - public void SetActiveCompute(Handle computeShader, uint entryIndex) + public void SetActiveCompute(Handle computeShader, int entryIndex) { var r = _resourceManager.GetComputeShaderReference(computeShader); if (r.IsFailure) @@ -247,30 +273,50 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext } ref var shader = ref r.Value; - var entryHash = shader.GetEntryHash((int)entryIndex); + var entryHash = shader.GetEntryID(entryIndex); var keywordSet = new LocalKeywordSet(); // TODO: Support keywords in compute shader. var variantKey = RHIUtility.CreateShaderVariantKey(entryHash, in keywordSet); - var pipelineKey = RHIUtility.CreateComputePipelineKey(variantKey); + + var (compiledHash, error) = _shaderLibrary.GetCompiledHash(variantKey); + if (error.IsFailure) + { + // TODO: Fallback to a default shader or show an error material. + return; + } + + var pipelineKey = RHIUtility.CreateComputePipelineKey(compiledHash); if (!_pipelineLibrary.HasPipelineStateObject(pipelineKey)) { - var compiledCacheResult = _shaderCompiler.GetCompiledCache(variantKey); + using var scope = AllocationManager.CreateStackScope(); + var compiledCacheResult = _shaderLibrary.GetCompiledCache(shader.UniqueID, entryIndex, scope.AllocationHandle); if (compiledCacheResult.IsFailure) { throw new InvalidOperationException("Failed to load compiled shader cache for pipeline state object creation."); } - var psoDes = new ComputePSODescriptor + + var cache = compiledCacheResult.Value; + Logger.DebugAssert(cache.compiledHash == compiledHash); + + ShaderLibrary.ParseCacheData(cache.byteCode, out _, out var byteCodeOffsets, out var byteCodes); + Logger.DebugAssert(byteCodeOffsets.Length == 1); + + var psoDes = new ComputePSODesc { + CompiledHash = compiledHash, VariantKey = variantKey, + CsCode = byteCodes.Slice((int)byteCodeOffsets[0]), }; - var compiled = compiledCacheResult.Value; - _pipelineLibrary.CreateComputePipeline(in psoDes, in compiled).GetValueOrThrow(); + + _pipelineLibrary.CreateComputePipeline(in psoDes).GetValueOrThrow(); } + + _commandBuffer.SetPipelineState(pipelineKey); } public void DispatchCompute(uint3 threadGroupCount) { - throw new NotImplementedException(); + } public ICommandBuffer GetCommandBufferUnsafe() diff --git a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphExecutor.cs b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphExecutor.cs index a8bb421..d8bec56 100644 --- a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphExecutor.cs +++ b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphExecutor.cs @@ -26,8 +26,8 @@ internal sealed class RenderGraphExecutor private void SetViewport(ReadOnlySpan color, DepthStencilInfo depthStencil) { - // This should not happened since the compiler should have rejected any render pass with an invalid render target configuration, but just in case, we use Debug.Assert to validate our assumptions. - Debug.Assert(color.Length > 0 || depthStencil.texture.IsValid); + // This should not happened since the compiler should have rejected any render pass with an invalid render target configuration, but just in case, we use Logger.DebugAssert to validate our assumptions. + Logger.DebugAssert(color.Length > 0 || depthStencil.texture.IsValid); ViewportDesc viewportDesc = default; ScissorRectDesc scissorDesc = default; @@ -178,7 +178,7 @@ internal sealed class RenderGraphExecutor else { // All the reaster pass should be merged into native render pass, so if we encounter a raster pass here, it means something went wrong during compilation. - Debug.Assert(pass.type != RenderPassType.Raster); + Logger.DebugAssert(pass.type != RenderPassType.Raster); // Compute pass or Unsafe pass var e = ExecuteBarriersForPass(commandBuffer, logicalPassIndex, ref barrierIndex, compiledBarriers); diff --git a/src/Runtime/Ghost.Graphics/RenderPipeline.cs b/src/Runtime/Ghost.Graphics/RenderPipeline.cs index a4ddd35..8c290a3 100644 --- a/src/Runtime/Ghost.Graphics/RenderPipeline.cs +++ b/src/Runtime/Ghost.Graphics/RenderPipeline.cs @@ -1,6 +1,7 @@ using Ghost.Core; using Ghost.Graphics.Core; using Ghost.Graphics.RHI; +using Ghost.Graphics.Services; using Misaki.HighPerformance.Mathematics; namespace Ghost.Graphics; @@ -24,107 +25,115 @@ public interface IRenderPipeline : IDisposable void Render(RenderContext ctx, int frameIndex, IRenderPayload payload); } -public static class RenderPipelineUtility +public readonly ref struct RenderViewData : IDisposable { - public static bool GetVPMatrices(RenderSystem renderSystem, ref readonly RenderRequest request, out float4x4 view, out float4x4 projection, out uint2 screenSize) + private readonly ref readonly RenderRequest _request; + private readonly SwapChainManager _swapChainManager; + + private readonly Handle _colorTexture; + private readonly uint2 _screenSize; + + public readonly ref readonly RenderRequest Request => ref _request; + public readonly Handle ColorTexture => _colorTexture; + public readonly uint2 ScreenSize => _screenSize; + + public RenderViewData(SwapChainManager swapChainManager, IResourceDatabase resourceDatabase, ref readonly RenderRequest request) { - Handle rtHandle; + _request = ref request; + _swapChainManager = swapChainManager; + if (request.swapChainIndex < 0) { - rtHandle = request.colorTarget; + _colorTexture = request.colorTarget; + Logger.DebugAssert(_colorTexture.IsValid, "Invalid color target texture."); } - else if (renderSystem.SwapChainManager.TryGetSwapChain(request.swapChainIndex, out var swapChain)) + else if (swapChainManager.TryGetSwapChain(request.swapChainIndex, out var swapChain)) { - rtHandle = swapChain.GetCurrentBackBuffer(); + _colorTexture = swapChain.GetCurrentBackBuffer(); } else { - view = default; - projection = default; - screenSize = default; - - return false; + throw new InvalidOperationException($"Invalid swap chain index: {request.swapChainIndex}"); } - try + var (desc, error) = resourceDatabase.GetResourceDescription(_colorTexture.AsResource()); + if (error.IsFailure) { - var rtResult = renderSystem.GraphicsEngine.ResourceDatabase.GetResourceDescription(rtHandle.AsResource()); - if (rtResult.IsFailure) - { - view = default; - projection = default; - screenSize = default; - - return false; - } - - screenSize = new uint2(rtResult.Value.TextureDescription.Width, rtResult.Value.TextureDescription.Height); - var aspectScreen = (float)screenSize.x / screenSize.y; - - view = math.inverse(request.view.localToWorld); - - var vfov = 2.0f * math.atan(request.view.sensorSize.y / (2.0f * request.view.focalLength)); - var hfov = 2.0f * math.atan(request.view.sensorSize.x / (2.0f * request.view.focalLength)); - var aspectSensor = request.view.sensorSize.x / request.view.sensorSize.y; - - float vfovF; - switch (request.view.gateFit) - { - case GateFit.Vertical: - vfovF = vfov; - break; - - case GateFit.Horizontal: - // Adjust VFOV so that the sensor width fits the screen width - var horizontalAspectBuffer = math.tan(hfov * 0.5f); - vfovF = 2.0f * math.atan(horizontalAspectBuffer / aspectScreen); - break; - - case GateFit.Fill: - if (aspectSensor > aspectScreen) - { - goto case GateFit.Vertical; - } - else - { - goto case GateFit.Horizontal; - } - - case GateFit.Overscan: - if (aspectSensor > aspectScreen) - { - goto case GateFit.Horizontal; - } - else - { - goto case GateFit.Vertical; - } - default: - vfovF = vfov; - break; - } - - var m_11 = 1.0f / math.tan(vfovF * 0.5f); - var m_00 = m_11 / aspectScreen; - var m_22 = request.view.farClipPlane / (request.view.farClipPlane - request.view.nearClipPlane); - var m_23 = -(request.view.farClipPlane * request.view.nearClipPlane) / (request.view.farClipPlane - request.view.nearClipPlane); - - projection = new float4x4 - ( - m_00, 0, 0, 0, - 0, m_11, 0, 0, - 0, 0, m_22, m_23, - 0, 0, 1, 0 - ); - - return true; + throw new InvalidOperationException($"Failed to get resource description for color target texture. Error: {error}"); } - finally + + _screenSize = new uint2(desc.TextureDescriptor.Width, desc.TextureDescriptor.Height); + } + + public void Dispose() + { + if (_request.swapChainIndex >= 0) { - if (request.swapChainIndex >= 0) - { - renderSystem.SwapChainManager.ReleaseSwapChain(request.swapChainIndex); - } + _swapChainManager.ReleaseSwapChain(_request.swapChainIndex); } } -} \ No newline at end of file +} + +public static class RenderPipelineUtility +{ + public static void GetVPMatrices(ref readonly RenderRequest request, uint2 screenSize, out float4x4 view, out float4x4 projection) + { + var aspectScreen = (float)screenSize.x / screenSize.y; + + view = math.inverse(request.view.localToWorld); + + var vfov = 2.0f * math.atan(request.view.sensorSize.y / (2.0f * request.view.focalLength)); + var hfov = 2.0f * math.atan(request.view.sensorSize.x / (2.0f * request.view.focalLength)); + var aspectSensor = request.view.sensorSize.x / request.view.sensorSize.y; + + float vfovF; + switch (request.view.gateFit) + { + case GateFit.Vertical: + vfovF = vfov; + break; + + case GateFit.Horizontal: + // Adjust VFOV so that the sensor width fits the screen width + var horizontalAspectBuffer = math.tan(hfov * 0.5f); + vfovF = 2.0f * math.atan(horizontalAspectBuffer / aspectScreen); + break; + + case GateFit.Fill: + if (aspectSensor > aspectScreen) + { + goto case GateFit.Vertical; + } + else + { + goto case GateFit.Horizontal; + } + + case GateFit.Overscan: + if (aspectSensor > aspectScreen) + { + goto case GateFit.Horizontal; + } + else + { + goto case GateFit.Vertical; + } + default: + vfovF = vfov; + break; + } + + var m_11 = 1.0f / math.tan(vfovF * 0.5f); + var m_00 = m_11 / aspectScreen; + var m_22 = request.view.farClipPlane / (request.view.farClipPlane - request.view.nearClipPlane); + var m_23 = -(request.view.farClipPlane * request.view.nearClipPlane) / (request.view.farClipPlane - request.view.nearClipPlane); + + projection = new float4x4 + ( + m_00, 0, 0, 0, + 0, m_11, 0, 0, + 0, 0, m_22, m_23, + 0, 0, 1, 0 + ); + } +} diff --git a/src/Runtime/Ghost.Graphics/RenderSystem.cs b/src/Runtime/Ghost.Graphics/RenderSystem.cs index 45fbab3..444f4de 100644 --- a/src/Runtime/Ghost.Graphics/RenderSystem.cs +++ b/src/Runtime/Ghost.Graphics/RenderSystem.cs @@ -124,8 +124,8 @@ public class RenderSystem : IDisposable get => _renderPipelineSettings; set { - Debug.Assert(value != null, "RenderPipelineSettings cannot be set to null."); - Debug.Assert(!_disposed, "Cannot set RenderPipelineSettings on a disposed RenderSystem."); + Logger.DebugAssert(value != null, "RenderPipelineSettings cannot be set to null."); + Logger.DebugAssert(!_disposed, "Cannot set RenderPipelineSettings on a disposed RenderSystem."); if (value == _renderPipelineSettings) { @@ -229,7 +229,7 @@ public class RenderSystem : IDisposable #if DEBUG Debugger.Break(); #endif - Logger.LogError($"Render failed: {result.Message}"); + Logger.Error($"Render failed: {result.Message}"); } var waitHandles = new WaitHandle[] { null!, _shutdownEvent }; @@ -295,7 +295,7 @@ public class RenderSystem : IDisposable { cmd.Begin(frameResource.CommandAllocator); - var ctx = new RenderContext(_resourceManager, _graphicsEngine, cmd); + var ctx = new RenderContext(_resourceManager, _shaderLibrary, _graphicsEngine, cmd); _renderPipeline.Render(ctx, frameIndex, frameResource.RenderPayload); _swapChainManager.TransitionToPresent(cmd); @@ -337,7 +337,7 @@ public class RenderSystem : IDisposable internal void Start() { - Debug.Assert(!_disposed, "Cannot start a disposed RenderSystem."); + Logger.DebugAssert(!_disposed, "Cannot start a disposed RenderSystem."); if (_isRunning) { @@ -350,7 +350,7 @@ public class RenderSystem : IDisposable internal void Stop() { - Debug.Assert(!_disposed, "Cannot stop a disposed RenderSystem."); + Logger.DebugAssert(!_disposed, "Cannot stop a disposed RenderSystem."); if (!_isRunning) { @@ -364,7 +364,7 @@ public class RenderSystem : IDisposable internal void SignalCPUReady() { - Debug.Assert(!_disposed, "Cannot signal CPU ready on a disposed RenderSystem."); + Logger.DebugAssert(!_disposed, "Cannot signal CPU ready on a disposed RenderSystem."); var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount); ref var frameResource = ref _frameResources[eventIndex]; @@ -375,13 +375,13 @@ public class RenderSystem : IDisposable internal void RequestSwapChainResize(ISwapChain swapChain, uint2 newSize) { - Debug.Assert(!_disposed, "Cannot request swap chain resize on a disposed RenderSystem."); + Logger.DebugAssert(!_disposed, "Cannot request swap chain resize on a disposed RenderSystem."); _resizeRequest.AddOrUpdate(swapChain, newSize, (_, _) => newSize); } internal bool TryAcquireCPUFrame() { - Debug.Assert(!_disposed, "Cannot acquire CPU frame on a disposed RenderSystem."); + Logger.DebugAssert(!_disposed, "Cannot acquire CPU frame on a disposed RenderSystem."); var requiredGpuFence = _cpuFenceValue < _config.FrameBufferCount ? 0 : _cpuFenceValue - _config.FrameBufferCount + 1; @@ -398,7 +398,7 @@ public class RenderSystem : IDisposable public bool WaitForGPUReady(int timeOut = -1) { - Debug.Assert(!_disposed, "Cannot wait for GPU ready on a disposed RenderSystem."); + Logger.DebugAssert(!_disposed, "Cannot wait for GPU ready on a disposed RenderSystem."); var submittedFenceValue = Volatile.Read(ref _submittedFenceValue); if (submittedFenceValue == 0) @@ -412,7 +412,7 @@ public class RenderSystem : IDisposable public void WaitIdle() { - Debug.Assert(!_disposed, "Cannot wait idle on a disposed RenderSystem."); + Logger.DebugAssert(!_disposed, "Cannot wait idle on a disposed RenderSystem."); foreach (var frameResource in _frameResources) { if (frameResource.FenceValue > 0) @@ -424,7 +424,7 @@ public class RenderSystem : IDisposable public IRenderPayload GetCurrentFramePayload() { - Debug.Assert(!_disposed, "Cannot get current frame payload from a disposed RenderSystem."); + Logger.DebugAssert(!_disposed, "Cannot get current frame payload from a disposed RenderSystem."); var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount); ref var frameResource = ref _frameResources[eventIndex]; diff --git a/src/Runtime/Ghost.Graphics/Services/ResourceManager.Pool.cs b/src/Runtime/Ghost.Graphics/Services/ResourceManager.Pool.cs index 15669ec..621609e 100644 --- a/src/Runtime/Ghost.Graphics/Services/ResourceManager.Pool.cs +++ b/src/Runtime/Ghost.Graphics/Services/ResourceManager.Pool.cs @@ -27,11 +27,11 @@ public partial class ResourceManager public ulong retireFrame; } - private UnsafeList _activePages = new UnsafeList(4, Allocator.Persistent); - private UnsafeQueue _freePages = new UnsafeQueue(4, Allocator.Persistent); - private UnsafeQueue _retiringPages = new UnsafeQueue(4, Allocator.Persistent); + private UnsafeList _activePages = new UnsafeList(4, AllocationHandle.Persistent); + private UnsafeQueue _freePages = new UnsafeQueue(4, AllocationHandle.Persistent); + private UnsafeQueue _retiringPages = new UnsafeQueue(4, AllocationHandle.Persistent); - private UnsafeList> _frameTransientResources = new UnsafeList>(4, Allocator.Persistent); + private UnsafeList> _frameTransientResources = new UnsafeList>(4, AllocationHandle.Persistent); private static bool IsHeapFlagsCompatible(HeapFlags pageHeapFlags, HeapFlags requiredHeapFlags) { diff --git a/src/Runtime/Ghost.Graphics/Services/ResourceManager.cs b/src/Runtime/Ghost.Graphics/Services/ResourceManager.cs index 5185e65..14123b2 100644 --- a/src/Runtime/Ghost.Graphics/Services/ResourceManager.cs +++ b/src/Runtime/Ghost.Graphics/Services/ResourceManager.cs @@ -44,10 +44,10 @@ public sealed partial class ResourceManager : IDisposable _resourceAllocator = resourceAllocator; _resourceDatabase = resourceDatabase; - _meshes = new UnsafeSlotMap(64, Allocator.Persistent); - _materials = new UnsafeSlotMap(64, Allocator.Persistent); - _shaders = new UnsafeSlotMap(16, Allocator.Persistent); - _computeShaders = new UnsafeSlotMap(16, Allocator.Persistent); + _meshes = new UnsafeSlotMap(64, AllocationHandle.Persistent); + _materials = new UnsafeSlotMap(64, AllocationHandle.Persistent); + _shaders = new UnsafeSlotMap(16, AllocationHandle.Persistent); + _computeShaders = new UnsafeSlotMap(16, AllocationHandle.Persistent); _materialPalettes = new MaterialPaletteStore(); } @@ -59,25 +59,25 @@ public sealed partial class ResourceManager : IDisposable internal void BeginFrame(ulong submittedFrame) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); _submittedFrame = submittedFrame; } internal void EndFrame(ulong completedFrame) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); EndFramePool(completedFrame); } public void EnterParallelRead() { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); Volatile.Write(ref _writeLock, 1); } public void ExitParallelRead() { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); Volatile.Write(ref _writeLock, 0); } @@ -89,7 +89,7 @@ public sealed partial class ResourceManager : IDisposable /// An representing the newly created mesh. public unsafe Handle CreateMesh(UnsafeList vertices, UnsafeList indices) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); var spinner = new SpinWait(); while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0) @@ -152,7 +152,7 @@ public sealed partial class ResourceManager : IDisposable /// An representing the newly created material. public Handle CreateMaterial(Handle shader) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); var spinner = new SpinWait(); while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0) @@ -184,7 +184,7 @@ public sealed partial class ResourceManager : IDisposable /// The viewGroup containing the shader's properties and passes. public Handle CreateGraphicsShader(GraphicsShaderDescriptor descriptor) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); var spinner = new SpinWait(); while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0) @@ -207,7 +207,7 @@ public sealed partial class ResourceManager : IDisposable public Handle CreateComputeShader(ComputeShaderDescriptor descriptor) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); var spinner = new SpinWait(); while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0) { @@ -233,7 +233,7 @@ public sealed partial class ResourceManager : IDisposable /// true if a mesh with the specified Handle exists; otherwise, false. public bool HasMesh(Handle handle) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return _meshes.Contains(handle.ID, handle.Generation); } @@ -259,7 +259,7 @@ public sealed partial class ResourceManager : IDisposable /// The handle of the mesh to release. Must refer to a mesh that was previously created and not already released. public void ReleaseMesh(Handle handle) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); ref var mesh = ref _meshes.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); if (!exist) @@ -278,7 +278,7 @@ public sealed partial class ResourceManager : IDisposable /// true if a material with the specified handle exists; otherwise, false. public bool HasMaterial(Handle handle) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return _materials.Contains(handle.ID, handle.Generation); } @@ -304,7 +304,7 @@ public sealed partial class ResourceManager : IDisposable /// The handle of the material to release. Must refer to a material that has been previously acquired. public void ReleaseMaterial(Handle handle) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); ref var material = ref _materials.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); if (!exist) @@ -323,7 +323,7 @@ public sealed partial class ResourceManager : IDisposable /// The palette index. Index 0 represents an empty palette. public int GetOrCreateMaterialPalette(ReadOnlySpan> materials) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); foreach (var material in materials) { @@ -342,7 +342,7 @@ public sealed partial class ResourceManager : IDisposable /// The palette index to validate. public bool HasMaterialPalette(Identifier paletteID) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return _materialPalettes.IsValid(paletteID); } @@ -352,7 +352,7 @@ public sealed partial class ResourceManager : IDisposable /// The palette index to query. public MaterialPalette GetMaterialPaletteInfo(Identifier paletteID) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return _materialPalettes.GetInfo(paletteID); } @@ -363,7 +363,7 @@ public sealed partial class ResourceManager : IDisposable /// The material slot inside the palette. public Handle GetMaterialPaletteMaterial(Identifier paletteID, int localMaterialIndex) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return _materialPalettes.GetMaterial(paletteID, localMaterialIndex); } @@ -373,7 +373,7 @@ public sealed partial class ResourceManager : IDisposable /// The palette index to release. public void ReleaseMaterialPalette(Identifier paletteID) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); _materialPalettes.Release(paletteID); } @@ -384,7 +384,7 @@ public sealed partial class ResourceManager : IDisposable /// true if a shader with the specified identifier exists; otherwise, false. public bool HasShader(Handle id) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return _shaders.Contains(id.ID, id.Generation); } @@ -410,7 +410,7 @@ public sealed partial class ResourceManager : IDisposable /// The identifier of the shader to release. Must refer to a valid, previously created shader. public void ReleaseShader(Handle handle) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); ref var shader = ref _shaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); if (!exist) @@ -429,7 +429,7 @@ public sealed partial class ResourceManager : IDisposable /// true if a compute shader with the specified identifier exists; otherwise, false. public bool HasComputeShader(Handle id) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); return _computeShaders.Contains(id.ID, id.Generation); } @@ -455,7 +455,7 @@ public sealed partial class ResourceManager : IDisposable /// The identifier of the compute shader to release. Must refer to a valid, previously created ComputeShader. public void ReleaseComputeShader(Handle handle) { - Debug.Assert(!_disposed); + Logger.DebugAssert(!_disposed); ref var computeShader = ref _computeShaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); if (!exist) diff --git a/src/Runtime/Ghost.Graphics/Services/ShaderLibrary.cs b/src/Runtime/Ghost.Graphics/Services/ShaderLibrary.cs index 88638fb..90477fd 100644 --- a/src/Runtime/Ghost.Graphics/Services/ShaderLibrary.cs +++ b/src/Runtime/Ghost.Graphics/Services/ShaderLibrary.cs @@ -1,52 +1,82 @@ using Ghost.Core; +using Ghost.Core.Utilities; +using Ghost.Graphics.RHI; using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; +using System.IO.Hashing; using System.Runtime.CompilerServices; namespace Ghost.Graphics.Services; -internal class ShaderLibrary : IDisposable +internal unsafe struct ShaderByteCode { + public byte* pCode; + public ulong size; +} + +internal struct ShaderCache : IDisposable +{ + public MemoryBlock byteCode; + public ulong compiledHash; + + public void Dispose() + { + byteCode.Dispose(); + } +} + +internal unsafe class ShaderLibrary : IDisposable +{ + public struct CacheHeader + { + public ulong id; + public int index; + public int byteCodeOffsetCount; + } + private struct CacheEntry: IDisposable { - public UnsafeArray> byteCode; + public UnsafeArray cache; - public void Insert(int index, ReadOnlySpan data) + public void UpdateCache(int index, ref MemoryBlock data, ulong hash) { - if (index >= byteCode.Length) + if (index >= cache.Length) { - var newByteCode = new UnsafeArray>(index + 1, Allocator.Persistent); - for (int i = 0; i < byteCode.Length; i++) + var newByteCode = new UnsafeArray(index + 1, AllocationHandle.Persistent); + for (int i = 0; i < cache.Length; i++) { - newByteCode[i] = byteCode[i]; + newByteCode[i] = cache[i]; } - byteCode.Dispose(); - byteCode = newByteCode; + cache.Dispose(); + cache = newByteCode; } - var byteData = new UnsafeArray(data.Length, Allocator.Persistent); - byteData.CopyFrom(data); - byteCode[index] = byteData; + cache[index].byteCode = data; + cache[index].compiledHash = hash; } public readonly void Dispose() { - for (int i = 0; i < byteCode.Length; i++) + for (int i = 0; i < cache.Length; i++) { - byteCode[i].Dispose(); + cache[i].Dispose(); } + + cache.Dispose(); } } private UnsafeHashMap _inMemoryCache; + private UnsafeHashMap _variantToCompiledHash; private readonly string _cacheDirectory; private readonly IShaderCompilationBridge? _shaderCompilationBridge; internal ShaderLibrary(IShaderCompilationBridge? shaderCompilationBridge, string cacheDirectory) { - _inMemoryCache = new UnsafeHashMap(16, Allocator.Persistent); + _inMemoryCache = new UnsafeHashMap(16, AllocationHandle.Persistent); + _variantToCompiledHash = new UnsafeHashMap(16, AllocationHandle.Persistent); _cacheDirectory = cacheDirectory; _shaderCompilationBridge = shaderCompilationBridge; @@ -66,33 +96,91 @@ internal class ShaderLibrary : IDisposable return Path.Combine(folderPath, $"shader_cache_{hashString}.bin"); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void CacheCompiledResult(ulong id, int index, ReadOnlySpan byteCode) + public static void ParseCacheData(MemoryBlock data, out CacheHeader header, out ReadOnlySpan offsets, out ReadOnlySpan byteCodes) { - var data = new UnsafeArray(byteCode.Length, Allocator.Persistent); - data.CopyFrom(byteCode); + Logger.DebugAssert(data.IsCreated); - ref var entry = ref _inMemoryCache.GetValueRefOrAddDefault(id, out var exists); - entry.Insert(index, byteCode); + var reader = new SpanReader(data.AsSpan()); + header = reader.Read(); + offsets = reader.ReadSpan(header.byteCodeOffsetCount); + byteCodes = reader.ReadToEnd(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Result, Error> GetCache(ulong id, int index, AllocationHandle allocationHandle) + public void CacheCompiledResult(ulong id, int index, Key64 variantKey, ReadOnlySpan byteCodes) + { + var header = new CacheHeader + { + id = id, + index = index, + byteCodeOffsetCount = byteCodes.Length, + }; + + var offsets = stackalloc ulong[byteCodes.Length]; + var offset = (ulong)(sizeof(CacheHeader) + (sizeof(ulong) * byteCodes.Length)); + for (var i = 0; i < byteCodes.Length; i++) + { + offsets[i] = offset; + offset += byteCodes[i].size; + } + + var data = new MemoryBlock((nuint)offset, 8, AllocationHandle.Persistent); + var writer = new SpanWriter(data.AsSpan()); + + writer.Write(header); + + for (var i = 0; i < byteCodes.Length; i++) + { + writer.Write(offsets[i]); + } + + for (var i = 0; i < byteCodes.Length; i++) + { + var byteCode = byteCodes[i]; + var src = new ReadOnlySpan(byteCode.pCode, (int)byteCode.size); + writer.WriteSpan(src); + } + + var codeHash = XxHash64.HashToUInt64(data.AsSpan()); + _variantToCompiledHash[variantKey] = codeHash; + + ref var entry = ref _inMemoryCache.GetValueRefOrAddDefault(id, out var exists); + entry.UpdateCache(index, ref data, codeHash); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Result GetCompiledCache(ulong id, int index, AllocationHandle allocationHandle) { if (_inMemoryCache.TryGetValue(id, out var entry)) { - if (index < entry.byteCode.Length) + if (index < entry.cache.Length) { - var byteCode = entry.byteCode[index]; - var result = new UnsafeArray(byteCode.Length, allocationHandle); - result.CopyFrom(byteCode); - return result; + var shaderCache = entry.cache[index]; + var result = new MemoryBlock(shaderCache.byteCode.Size, shaderCache.byteCode.Alignment, allocationHandle); + + result.CopyFrom(shaderCache.byteCode.AsSpan()); + return new ShaderCache + { + byteCode = result, + compiledHash = shaderCache.compiledHash, + }; } } return Error.NotFound; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Result GetCompiledHash(Key64 variantKey) + { + if (_variantToCompiledHash.TryGetValue(variantKey, out var compiledHash)) + { + return compiledHash; + } + + return Error.NotFound; + } + public void Dispose() { foreach (var kvp in _inMemoryCache) diff --git a/src/Runtime/Ghost.Graphics/Services/SwapChainManager.cs b/src/Runtime/Ghost.Graphics/Services/SwapChainManager.cs index e67696f..953c732 100644 --- a/src/Runtime/Ghost.Graphics/Services/SwapChainManager.cs +++ b/src/Runtime/Ghost.Graphics/Services/SwapChainManager.cs @@ -32,19 +32,19 @@ internal sealed class SwapChainRecord } } - public bool ReleaseRef() + public int ReleaseRef() { while (true) { var current = Volatile.Read(ref _refCount); if (current == 0) { - return false; + return 0; } if (Interlocked.CompareExchange(ref _refCount, current - 1, current) == current) { - return (current - 1) == 0; + return current - 1; } } } @@ -109,13 +109,13 @@ public class SwapChainManager : IDisposable { var record = Volatile.Read(ref _swapChains[index]); - if (record != null && record.ReleaseRef()) + if (record != null && record.ReleaseRef() == 0) { record.SwapChain.Dispose(); Interlocked.CompareExchange(ref _swapChains[index], null, record); } } - + public void TransitionToPresent(ICommandBuffer commandBuffer) { for (int i = 0; i < MAX_SWAP_CHAINS; i++) diff --git a/src/Runtime/Ghost.Graphics/Utilities/MeshletUtility.cs b/src/Runtime/Ghost.Graphics/Utilities/MeshletUtility.cs index 994634e..8cd69ad 100644 --- a/src/Runtime/Ghost.Graphics/Utilities/MeshletUtility.cs +++ b/src/Runtime/Ghost.Graphics/Utilities/MeshletUtility.cs @@ -3,6 +3,7 @@ // TODO: This file should be moved to editor project since there is no reason we need to build meshlets and LOD at runtime. +using Ghost.Core; using Ghost.MeshOptimizer; using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; @@ -178,7 +179,7 @@ public static unsafe class MeshletUtility private static ClodBounds MergeBounds(UnsafeList clusters, UnsafeList group) { - using var boundsList = new UnsafeArray(group.Count, Allocator.FreeList); + using var boundsList = new UnsafeArray(group.Count, AllocationHandle.FreeList); for (var j = 0; j < group.Count; j++) { boundsList[j] = (clusters[group[j]].bounds); @@ -210,9 +211,9 @@ public static unsafe class MeshletUtility { var maxMeshlets = MeshOptApi.BuildMeshletsBound(indexCount, config.maxVertices, config.minTriangles); - using var meshlets = new UnsafeArray((int)maxMeshlets, Allocator.FreeList); - using var meshletVertices = new UnsafeArray((int)indexCount, Allocator.FreeList); - using var meshletTriangles = new UnsafeArray((int)indexCount, Allocator.FreeList); + using var meshlets = new UnsafeArray((int)maxMeshlets, AllocationHandle.FreeList); + using var meshletVertices = new UnsafeArray((int)indexCount, AllocationHandle.FreeList); + using var meshletTriangles = new UnsafeArray((int)indexCount, AllocationHandle.FreeList); var pMeshlets = (meshopt_Meshlet*)meshlets.GetUnsafePtr(); var pMeshletVertices = (uint*)meshletVertices.GetUnsafePtr(); @@ -240,7 +241,7 @@ public static unsafe class MeshletUtility ); } - var clusters = new UnsafeList((int)meshletCount, Allocator.FreeList); + var clusters = new UnsafeList((int)meshletCount, AllocationHandle.FreeList); for (nuint i = 0; i < meshletCount; i++) { @@ -259,9 +260,9 @@ public static unsafe class MeshletUtility var cluster = new Cluster { vertices = meshlet.vertex_count, - indices = new UnsafeList((int)(meshlet.triangle_count * 3), Allocator.FreeList), - uniqueVertices = new UnsafeList((int)meshlet.vertex_count, Allocator.FreeList), - localIndices = new UnsafeList((int)(meshlet.triangle_count * 3), Allocator.FreeList), + indices = new UnsafeList((int)(meshlet.triangle_count * 3), AllocationHandle.FreeList), + uniqueVertices = new UnsafeList((int)meshlet.vertex_count, AllocationHandle.FreeList), + localIndices = new UnsafeList((int)(meshlet.triangle_count * 3), AllocationHandle.FreeList), group = -1, refined = -1 }; @@ -332,8 +333,8 @@ public static unsafe class MeshletUtility { if (pending.Count <= (int)config.partitionSize) { - var single = new UnsafeList>(1, Allocator.FreeList); - var pendingcpy = new UnsafeList(pending.Count, Allocator.FreeList); + var single = new UnsafeList>(1, AllocationHandle.FreeList); + var pendingcpy = new UnsafeList(pending.Count, AllocationHandle.FreeList); pendingcpy.AddRange(pending.AsSpan()); single.Add(pendingcpy); @@ -347,8 +348,8 @@ public static unsafe class MeshletUtility totalIndexCount += (nuint)clusters[pending[i]].indices.Count; } - using var clusterIndices = new UnsafeList((int)totalIndexCount, Allocator.FreeList); - using var clusterCounts = new UnsafeList(pending.Count, Allocator.FreeList); + using var clusterIndices = new UnsafeList((int)totalIndexCount, AllocationHandle.FreeList); + using var clusterCounts = new UnsafeList(pending.Count, AllocationHandle.FreeList); nuint offset = 0; for (var i = 0; i < pending.Count; i++) @@ -363,7 +364,7 @@ public static unsafe class MeshletUtility offset += (nuint)cluster.indices.Count; } - using var clusterPart = new UnsafeArray(pending.Count, Allocator.FreeList); + using var clusterPart = new UnsafeArray(pending.Count, AllocationHandle.FreeList); var partitionCount = MeshOptApi.PartitionClusters( (uint*)clusterPart.GetUnsafePtr(), @@ -377,10 +378,10 @@ public static unsafe class MeshletUtility config.partitionSize ); - var partitions = new UnsafeList>((int)partitionCount, Allocator.FreeList); + var partitions = new UnsafeList>((int)partitionCount, AllocationHandle.FreeList); for (nuint i = 0; i < partitionCount; i++) { - partitions.Add(new UnsafeList((int)(config.partitionSize + config.partitionSize / 3), Allocator.FreeList)); + partitions.Add(new UnsafeList((int)(config.partitionSize + config.partitionSize / 3), AllocationHandle.FreeList)); } for (var i = 0; i < pending.Count; i++) @@ -393,7 +394,7 @@ public static unsafe class MeshletUtility private static int OutputGroup(ref readonly ClodConfig config, ref readonly ClodMesh mesh, UnsafeList clusters, UnsafeList group, ClodBounds simplified, int depth, void* outputContext, ClodOutputDelegate? outputCallback) { - using var groupClusters = new UnsafeList(group.Count, Allocator.FreeList); + using var groupClusters = new UnsafeList(group.Count, AllocationHandle.FreeList); for (var i = 0; i < group.Count; i++) { @@ -429,8 +430,8 @@ public static unsafe class MeshletUtility private static void SimplifyFallback(ref UnsafeArray lod, ref readonly ClodMesh mesh, ReadOnlyUnsafeCollection indices, ReadOnlyUnsafeCollection locks, nuint target_count, float* error) { - using var subset = new UnsafeArray(indices.Count, Allocator.FreeList); - using var subset_locks = new UnsafeArray(indices.Count, Allocator.FreeList); + using var subset = new UnsafeArray(indices.Count, AllocationHandle.FreeList); + using var subset_locks = new UnsafeArray(indices.Count, AllocationHandle.FreeList); lod.Resize(indices.Count); @@ -440,7 +441,7 @@ public static unsafe class MeshletUtility for (var i = 0; i < indices.Count; ++i) { var v = indices[i]; - Debug.Assert(v < mesh.vertexCount); + Logger.DebugAssert(v < mesh.vertexCount); subset[i].x = mesh.vertexPositions[v * positions_stride + 0]; subset[i].y = mesh.vertexPositions[v * positions_stride + 1]; @@ -466,7 +467,7 @@ public static unsafe class MeshletUtility public static UnsafeArray Simplify(ref readonly ClodConfig config, ref readonly ClodMesh mesh, ReadOnlyUnsafeCollection indices, ReadOnlyUnsafeCollection locks, nuint targetCount, float* error) { - var lod = new UnsafeArray(indices.Count, Allocator.FreeList); + var lod = new UnsafeArray(indices.Count, AllocationHandle.FreeList); if (targetCount >= (nuint)indices.Count) { @@ -577,10 +578,10 @@ public static unsafe class MeshletUtility /// The total count of generated clusters. public static nuint Build(ref readonly ClodConfig config, ref readonly ClodMesh mesh, void* outputContext, ClodOutputDelegate? outputCallback) { - Debug.Assert(mesh.vertexAttributesStride % sizeof(float) == 0, "vertexAttributesStride must be a multiple of sizeof(float)"); + Logger.DebugAssert(mesh.vertexAttributesStride % sizeof(float) == 0, "vertexAttributesStride must be a multiple of sizeof(float)"); - using var locks = new UnsafeArray((int)mesh.vertexCount, Allocator.FreeList, AllocationOption.Clear); ; - using var remap = new UnsafeArray((int)mesh.vertexCount, Allocator.FreeList); + using var locks = new UnsafeArray((int)mesh.vertexCount, AllocationHandle.FreeList, AllocationOption.Clear); ; + using var remap = new UnsafeArray((int)mesh.vertexCount, AllocationHandle.FreeList); MeshOptApi.GeneratePositionRemap((uint*)remap.GetUnsafePtr(), mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride); @@ -610,7 +611,7 @@ public static unsafe class MeshletUtility clusters[i].bounds = ComputeBounds(in mesh, clusters[i].indices, 0.0f); } - using var pending = new UnsafeList(clusters.Count, Allocator.FreeList); + using var pending = new UnsafeList(clusters.Count, AllocationHandle.FreeList); for (var i = 0; i < clusters.Count; i++) { pending.Add(i); @@ -627,7 +628,7 @@ public static unsafe class MeshletUtility for (var i = 0; i < groups.Count; i++) { - using var merged = new UnsafeList(groups[i].Count * (int)config.maxTriangles * 3, Allocator.FreeList); + using var merged = new UnsafeList(groups[i].Count * (int)config.maxTriangles * 3, AllocationHandle.FreeList); for (var j = 0; j < groups[i].Count; j++) { var clusterIndices = clusters[groups[i][j]].indices; @@ -690,4 +691,4 @@ public static unsafe class MeshletUtility return finalClusterCount; } -} \ No newline at end of file +} diff --git a/src/Runtime/Ghost.Graphics/Utilities/RenderingUtility.cs b/src/Runtime/Ghost.Graphics/Utilities/RenderingUtility.cs index bb8bab2..5d8c284 100644 --- a/src/Runtime/Ghost.Graphics/Utilities/RenderingUtility.cs +++ b/src/Runtime/Ghost.Graphics/Utilities/RenderingUtility.cs @@ -1,7 +1,6 @@ using Ghost.Core; using Ghost.Graphics.RHI; using Misaki.HighPerformance.LowLevel.Utilities; -using System.Diagnostics; using Ghost.Graphics.Services; namespace Ghost.Graphics.Utilities; @@ -17,10 +16,10 @@ public static unsafe class RenderingUtility return; } - Debug.Assert(r.Value.Type == ResourceType.Buffer); + Logger.DebugAssert(r.Value.Type == ResourceType.Buffer); var sizeInBytes = (nuint)(data.Length * sizeof(T)); - var memoryType = r.Value.BufferDescription.HeapType; + var memoryType = r.Value.BufferDescriptor.HeapType; if (memoryType == HeapType.Upload) { @@ -61,7 +60,7 @@ public static unsafe class RenderingUtility where T : unmanaged { var desc = resourceDatabase.GetResourceDescription(texture.AsResource()).GetValueOrThrow(); - desc.TextureDescription.Format.GetSurfaceInfo(desc.TextureDescription.Width, desc.TextureDescription.Height, out var rowPitch, out var slicePitch, out _); + desc.TextureDescriptor.Format.GetSurfaceInfo(desc.TextureDescriptor.Width, desc.TextureDescriptor.Height, out var rowPitch, out var slicePitch, out _); var requiredSize = resourceDatabase.GetIntermediateResourceSize(texture.AsResource(), 0, 1); var uploadDesc = new BufferDesc diff --git a/src/Test/Ghost.Graphics.Test/RenderPipeline/TestRenderPipeline.cs b/src/Test/Ghost.Graphics.Test/RenderPipeline/TestRenderPipeline.cs index e0f1e94..1bf815b 100644 --- a/src/Test/Ghost.Graphics.Test/RenderPipeline/TestRenderPipeline.cs +++ b/src/Test/Ghost.Graphics.Test/RenderPipeline/TestRenderPipeline.cs @@ -139,7 +139,7 @@ public unsafe partial class TestRenderPipeline : IRenderPipeline continue; } - var rtSize = new uint2(rtResult.Value.TextureDescription.Width, rtResult.Value.TextureDescription.Height); + var rtSize = new uint2(rtResult.Value.TextureDescriptor.Width, rtResult.Value.TextureDescriptor.Height); var aspectScreen = (float)rtSize.x / rtSize.y; diff --git a/src/Test/Ghost.Graphics.Test/Systems/RenderExtractionSystem.cs b/src/Test/Ghost.Graphics.Test/Systems/RenderExtractionSystem.cs index 9ffb64a..1c519f9 100644 --- a/src/Test/Ghost.Graphics.Test/Systems/RenderExtractionSystem.cs +++ b/src/Test/Ghost.Graphics.Test/Systems/RenderExtractionSystem.cs @@ -52,9 +52,9 @@ public class RenderExtractionSystem : ISystem ref readonly var camLtwRef = ref camLtw.Get(); // TODO: Classify transparent objects into a separate render list and render via oit. - var renderList = new RenderList(1, 64, Allocator.FreeList); - var transparentRenderList = new RenderList(1, 64, Allocator.FreeList); - var shadowCasterRenderList = new RenderList(1, 64, Allocator.FreeList); + var renderList = new RenderList(1, 64, AllocationHandle.FreeList); + var transparentRenderList = new RenderList(1, 64, AllocationHandle.FreeList); + var shadowCasterRenderList = new RenderList(1, 64, AllocationHandle.FreeList); // TODO: This chould be done in earallel jobs. foreach (var chunk in meshQuery.GetChunkIterator()) diff --git a/src/Test/Ghost.Graphics.Test/UnitTestApp.xaml.cs b/src/Test/Ghost.Graphics.Test/UnitTestApp.xaml.cs index 105350f..718ddde 100644 --- a/src/Test/Ghost.Graphics.Test/UnitTestApp.xaml.cs +++ b/src/Test/Ghost.Graphics.Test/UnitTestApp.xaml.cs @@ -36,11 +36,11 @@ public partial class UnitTestApp : Application UnhandledException += (sender, e) => { - Logger.LogError(e.Exception); + Logger.Error(e.Exception); #if DEBUG System.Diagnostics.Debugger.Break(); #endif Environment.FailFast("Unhandled exception", e.Exception); }; } -} \ No newline at end of file +} diff --git a/src/Test/Ghost.Graphics.Test/Utilities/MeshUtility.cs b/src/Test/Ghost.Graphics.Test/Utilities/MeshUtility.cs index e4a945f..5e322d6 100644 --- a/src/Test/Ghost.Graphics.Test/Utilities/MeshUtility.cs +++ b/src/Test/Ghost.Graphics.Test/Utilities/MeshUtility.cs @@ -50,7 +50,7 @@ internal static class MeshUtility space_conversion = ufbx_space_conversion.UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY, }; - using var str = new UnsafeArray(Encoding.UTF8.GetByteCount(filePath) + 1, Allocator.FreeList); + using var str = new UnsafeArray(Encoding.UTF8.GetByteCount(filePath) + 1, AllocationHandle.FreeList); var count = Encoding.UTF8.GetBytes(filePath, str.AsSpan()); str[count] = 0; @@ -60,7 +60,7 @@ internal static class MeshUtility return Result.Failure(error.description.ToString()); } - using var flatVertices = new UnsafeList(1024, Allocator.FreeList); + using var flatVertices = new UnsafeList(1024, AllocationHandle.FreeList); var needComputeNormals = false; @@ -83,7 +83,7 @@ internal static class MeshUtility var maxScratchIndices = (int)(pMesh->max_face_triangles * 3u); - using var triIndicesArray = new UnsafeArray(maxScratchIndices, Allocator.FreeList); + using var triIndicesArray = new UnsafeArray(maxScratchIndices, AllocationHandle.FreeList); for (var j = 0u; j < pMesh->num_faces; j++) { @@ -142,8 +142,8 @@ internal static class MeshUtility var numIndices = (uint)flatVertices.Count; - using var weldedIndices = new UnsafeArray((int)numIndices, Allocator.FreeList); - using var cachedIndices = new UnsafeArray((int)numIndices, Allocator.FreeList); + using var weldedIndices = new UnsafeArray((int)numIndices, AllocationHandle.FreeList); + using var cachedIndices = new UnsafeArray((int)numIndices, AllocationHandle.FreeList); var stream = new ufbx_vertex_stream {