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 {