feat(graphics): refactor pipeline keying and allocators

Major refactor of graphics pipeline keying, shader cache, and resource allocation.
Replaced most Allocator usage with AllocationHandle, modernized logger usage,
and unified pipeline state keys. Updated MeshUtility to use AllocationHandle.FreeList.
Added new shader pipeline architecture docs and improved error handling throughout.

BREAKING CHANGE: Pipeline keying and resource allocation APIs have changed.
This commit is contained in:
2026-04-13 23:07:52 +09:00
parent c66fda5332
commit 817b32b8d9
69 changed files with 1436 additions and 2095 deletions

4
.gitignore vendored
View File

@@ -11,9 +11,11 @@
*.sln.docstates *.sln.docstates
AGENTS.md AGENTS.md
.opencode/
.code-review-graph/
ref/ ref/
docfx/ docfx/
.opencode/
NUL NUL
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)

View File

@@ -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<br/>(monitors .ghost DSL files)"]
AR["AssetRegistry<br/>(GUID ↔ file path mapping)"]
EP["Editor UI<br/>(status bar, material inspector)"]
end
subgraph CompilerProcess["GhostShaderServer Process"]
DSL["DSL Compiler<br/>(Ghost DSL → HLSL)"]
DXC["DXC Compiler<br/>(HLSL → DXIL bytecode)"]
MW["Manifest Writer<br/>(updates variant → hash mapping)"]
end
subgraph RuntimeGraphics["Ghost.Graphics (Runtime)"]
SL["ShaderLibrary<br/>(reads bytecode from cache)"]
PL["PipelineLibrary<br/>(PSO creation + double-buffer)"]
RGC["RenderGraphContext<br/>(binds PSO per draw call)"]
BR["IShaderCompilationBridge<br/>(interface, 2 methods)"]
end
subgraph SharedDisk["Shared Disk (ShaderCache/)"]
MF["ShaderManifest.bin<br/>(GUID+variant → content hash)"]
BC["Bytecode Files<br/>(content-addressed .bin blobs)"]
end
FW -- "file changed event" --> AR
AR -- "GUID + file path<br/>(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<br/>(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<br/><i>e.g. 7f3a-...-c82b</i><br/>stable forever"]
SRC["Source Code<br/><i>.ghost DSL file</i><br/>changes on edit"]
end
subgraph Manifest["ShaderManifest"]
E1["Entry:<br/>GUID=7f3a | Pass=0 | Variant=0x00<br/>→ ContentHash=0xABCD"]
E2["Entry:<br/>GUID=7f3a | Pass=0 | Variant=0x01<br/>→ ContentHash=0x1234"]
E3["Entry:<br/>GUID=7f3a | Pass=1 | Variant=0x00<br/>→ 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"<br/>via AssetRegistry
Editor->>Server: CompileRequest {<br/> guid: 7f3a-...,<br/> filePath: "water.ghost",<br/> defines: [...],<br/> platform: D3D12<br/>}
Note over Server: Mark status = Compiling<br/>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 {<br/> status: Error,<br/> errors: [...]<br/>}
Editor->>Editor: Show errors in<br/>console/inspector
else DSL is valid
Server->>Server: For each (pass, variant):<br/>DXC Compile HLSL → DXIL
alt Any DXC error
Server->>Server: Mark status = Error
Server-->>Editor: CompileResult {<br/> status: Error,<br/> errors: [...]<br/>}
else All variants compiled
Server->>Cache: Write bytecode blobs<br/>(content-addressed)
Server->>Cache: Update manifest entries:<br/>(GUID+pass+variant) → new hash
Server->>Server: Mark status = Ready
Server-->>Editor: CompileResult {<br/> status: Ready,<br/> variantCount: N<br/>}
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<br/>= f(shader.GUID, passIndex, variantMask)"]
B --> C{"PipelineLibrary<br/>has PSO for<br/>ManifestKey?"}
C -- "Yes (cache hit)" --> D["Bind existing PSO<br/>to command buffer"]
D --> Z["Done ✓"]
C -- "No (cache miss)" --> E{"ShaderLibrary<br/>has bytecode for<br/>ManifestKey?"}
E -- "Yes (manifest hit)" --> F["Read bytecode<br/>from cache file"]
F --> G["Create PSO from bytecode"]
G --> H["Store in PipelineLibrary"]
H --> D
E -- "No (manifest miss)" --> I{"Is this Editor<br/>or Runtime?"}
I -- "Runtime<br/>(shipped game)" --> J["Bind Fallback<br/>ERROR PSO ⚠️"]
J --> K["Log error:<br/>missing shader"]
K --> Z
I -- "Editor" --> L{"Query Bridge:<br/>IsCompiling?"}
L -- "Status = Compiling" --> M["Bind OLD PSO<br/>(keep previous frame's shader)"]
M --> Z
L -- "Status = Error" --> N["Bind ERROR PSO<br/>(magenta)"]
N --> Z
L -- "Status = Ready" --> O["The manifest was just updated.<br/>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 ✓<br/>(what we render with)"]
K --> PENDING["pending: null<br/>(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 ✓<br/>(still rendering with this)"]
K2 --> PENDING2["pending: COMPILING<br/>(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 ✓<br/>(new shader, rendering now)"]
K3 --> PENDING3["pending: null<br/>(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<br/>(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:<br/>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<br/>(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<br/><i>derived from source hash</i>"]
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 ❌<br/>Still 0xABCD, but source changed"]
STALE -.-> WRONG["Looks up OLD bytecode<br/>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<br/>assetGUID = 7f3a-..."] --> P2["Pass[0]: index=0<br/><i>no source hash stored</i>"]
P2 --> MK["ManifestKey = f(7f3a, 0, keywords)"]
MK --> MANIFEST["Manifest Lookup<br/>→ ContentHash = 0x9999"]
MANIFEST --> SL2["ShaderLibrary<br/>→ read 99/shader_cache_9999.bin"]
SL2 --> PSO2["Create or get PSO"]
EDIT2["User edits source"] -.-> RECOMP["Server recompiles<br/>→ new ContentHash = 0xBBBB"]
RECOMP -.-> MUPD["Manifest updated:<br/>same key → 0xBBBB"]
MUPD -.-> NEXT["Next frame: manifest read<br/>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 {
<<interface>>
+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<br/>in scenes/assets"] --> COLLECT["Collect all referenced<br/>(GUID, pass, variant) tuples"]
COLLECT --> COMPILE["Compile all variants<br/>via ShaderServer"]
COMPILE --> PACK["Package manifest +<br/>bytecode blobs into<br/>game data archive"]
end
subgraph ShippedGame["Runtime (shipped game)"]
LOAD["Load manifest +<br/>bytecode from archive"] --> LIB["ShaderLibrary<br/>(read-only, all variants pre-cached)"]
LIB --> MISS{"Cache miss?"}
MISS -- "Never<br/>(if build is correct)" --> OK["Create PSO normally"]
MISS -- "Somehow yes<br/>(bug or modding)" --> ERR["Error PSO<br/>+ 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 |

View File

@@ -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<DockNode> 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<object> 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
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.View.Controls">
<Style TargetType="local:DockLayout">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockLayout">
<Grid x:Name="PART_RootGrid" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
```
- [ ] **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
<ControlTemplate TargetType="local:DockLayout">
<Grid>
<Grid x:Name="PART_RootGrid" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" />
<Border x:Name="PART_DropTargetOverlay"
Background="#660078D4"
BorderBrush="#FF0078D4"
BorderThickness="2"
Visibility="Collapsed"
IsHitTestVisible="False" />
</Grid>
</ControlTemplate>
```
- [ ] **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
<?xml version="1.0" encoding="utf-8" ?>
<winex:WindowEx
x:Class="Ghost.Editor.View.Windows.DockWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Ghost.Editor.View.Controls"
xmlns:winex="using:WinUIEx">
<Grid>
<controls:DockLayout x:Name="PART_DockLayout" />
</Grid>
</winex:WindowEx>
```
`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"
```

View File

@@ -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<DockModule> 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
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.View.Controls.Docking">
<Style TargetType="local:DockGroup">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockGroup">
<Grid>
<TabView x:Name="PART_TabView"
IsAddTabButtonVisible="False"
CanDragTabs="True"
CanReorderTabs="True"
AllowDrop="True">
</TabView>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
```
- [ ] **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
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.View.Controls.Docking">
<Style TargetType="local:DockPanel">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockPanel">
<Grid x:Name="PART_Grid" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
```
- [ ] **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
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.View.Controls.Docking">
<Style TargetType="local:DockRegionHighlight">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockRegionHighlight">
<Border Background="#400078D7" BorderBrush="#800078D7" BorderThickness="2" CornerRadius="4" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
```
- [ ] **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
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.View.Controls.Docking">
<Style TargetType="local:DockingLayout">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockingLayout">
<Grid>
<ContentPresenter x:Name="PART_Content" Content="{TemplateBinding RootPanel}" />
<Canvas x:Name="PART_OverlayCanvas" IsHitTestVisible="False">
<local:DockRegionHighlight x:Name="PART_Highlight" Visibility="Collapsed" />
</Canvas>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
```
- [ ] **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"
```

View File

@@ -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<DockNode> Children { get; }
public ObservableCollection<GridLength> 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<object> 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`.

View File

@@ -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.

View File

@@ -140,13 +140,8 @@ internal static class DSLShaderCompiler
var pixelShaderCode = new ShaderCode { code = result.Value, entryPoint = pass.pixelShader.entry }; 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 passes[i] = new PassDescriptor
{ {
identifier = Hash.Combine64(GetUniqueId(semantics.name + pass.name), asHash, msHash, psHash),
name = pass.name, name = pass.name,
amplificationShaderCode = amplificationShaderCode, amplificationShaderCode = amplificationShaderCode,

View File

@@ -166,9 +166,9 @@ public partial class App : Application
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{ {
Logger.LogError(e.Exception); Logger.Error(e.Exception);
#if DEBUG #if DEBUG
Debugger.BreakForUserUnhandledException(e.Exception); Debugger.BreakForUserUnhandledException(e.Exception);
#endif #endif
} }
} }

View File

@@ -20,8 +20,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.7" /> <PackageReference Include="Misaki.HighPerformance" Version="1.0.7" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="1.5.8" /> <PackageReference Include="Misaki.HighPerformance.Jobs" Version="1.5.9" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.11"> <PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.13">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@@ -72,7 +72,6 @@ public struct PipelineState
get; set; get; set;
} }
public static PipelineState Default => new PipelineState public static PipelineState Default => new PipelineState
{ {
ZTest = ZTest.LessEqual, ZTest = ZTest.LessEqual,
@@ -107,4 +106,4 @@ public struct PipelineState
var code64 = GetHashCode64(); var code64 = GetHashCode64();
return ((int)code64) ^ (int)(code64 >> 32); return ((int)code64) ^ (int)(code64 >> 32);
} }
} }

View File

@@ -48,7 +48,6 @@ public struct PassDescriptor
{ {
public GraphicsShaderDescriptor shader; public GraphicsShaderDescriptor shader;
public ulong identifier;
public string name; public string name;
public ShaderCode amplificationShaderCode; public ShaderCode amplificationShaderCode;

View File

@@ -1,6 +1,8 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace Ghost.Core; namespace Ghost.Core;
@@ -151,90 +153,105 @@ public static class Logger
public static LogCollection Logs => s_logger.Logs; public static LogCollection Logs => s_logger.Logs;
[StackTraceHidden] [StackTraceHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Log(LogLevel level, object? message) public static void Log(LogLevel level, object? message)
{ {
s_logger.Log(message?.ToString() ?? "null", level); s_logger.Log(message?.ToString() ?? "null", level);
} }
[StackTraceHidden] [StackTraceHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Log(LogLevel level, string message) public static void Log(LogLevel level, string message)
{ {
s_logger.Log(message, level); s_logger.Log(message, level);
} }
[StackTraceHidden] [StackTraceHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Log(LogLevel level, string format, params object?[] args) public static void Log(LogLevel level, string format, params object?[] args)
{ {
s_logger.Log(string.Format(format, args), level); s_logger.Log(string.Format(format, args), level);
} }
[StackTraceHidden] [StackTraceHidden]
public static void LogInfo(object? message) [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Info(object? message)
{ {
s_logger.Log(message?.ToString() ?? "null", LogLevel.Info); s_logger.Log(message?.ToString() ?? "null", LogLevel.Info);
} }
[StackTraceHidden] [StackTraceHidden]
public static void LogInfo(string message) [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Info(string message)
{ {
s_logger.Log(message, LogLevel.Info); s_logger.Log(message, LogLevel.Info);
} }
[StackTraceHidden] [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); s_logger.Log(string.Format(format, args), LogLevel.Info);
} }
[StackTraceHidden] [StackTraceHidden]
public static void LogWarning(object? message) [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Warning(object? message)
{ {
s_logger.Log(message?.ToString() ?? "null", LogLevel.Warning); s_logger.Log(message?.ToString() ?? "null", LogLevel.Warning);
} }
[StackTraceHidden] [StackTraceHidden]
public static void LogWarning(string message) [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Warning(string message)
{ {
s_logger.Log(message, LogLevel.Warning); s_logger.Log(message, LogLevel.Warning);
} }
[StackTraceHidden] [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); s_logger.Log(string.Format(format, args), LogLevel.Warning);
} }
[StackTraceHidden] [StackTraceHidden]
public static void LogError(object? message) [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Error(object? message)
{ {
s_logger.Log(message?.ToString() ?? "null", LogLevel.Error); s_logger.Log(message?.ToString() ?? "null", LogLevel.Error);
} }
[StackTraceHidden] [StackTraceHidden]
public static void LogError(string message) [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Error(string message)
{ {
s_logger.Log(message, LogLevel.Error); s_logger.Log(message, LogLevel.Error);
} }
[StackTraceHidden] [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); s_logger.Log(string.Format(format, args), LogLevel.Error);
} }
[StackTraceHidden] [StackTraceHidden]
public static void LogError(Exception ex) [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Error(Exception ex)
{ {
s_logger.Log(ex); s_logger.Log(ex);
} }
[StackTraceHidden] [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] [StackTraceHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Conditional("DEBUG")] [Conditional("DEBUG")]
[Conditional("GHOST_EDITOR")] [Conditional("GHOST_EDITOR")]
public static void Debug(object? message) public static void Debug(object? message)
@@ -243,6 +260,7 @@ public static class Logger
} }
[StackTraceHidden] [StackTraceHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Conditional("DEBUG")] [Conditional("DEBUG")]
[Conditional("GHOST_EDITOR")] [Conditional("GHOST_EDITOR")]
public static void Debug(string message) public static void Debug(string message)
@@ -251,10 +269,26 @@ public static class Logger
} }
[StackTraceHidden] [StackTraceHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Conditional("DEBUG")] [Conditional("DEBUG")]
[Conditional("GHOST_EDITOR")] [Conditional("GHOST_EDITOR")]
public static void Debug(string format, params object?[] args) public static void Debug(string format, params object?[] args)
{ {
s_logger.Log(string.Format(format, args), LogLevel.Debug); 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
}
} }

View File

@@ -353,8 +353,8 @@ public static class ResultExtensions
return result.Value; return result.Value;
} }
public static T GetValueOrThrow<T, S>(this Result<T, S> result, [CallerArgumentExpression(nameof(result))] string? op = null) public static T GetValueOrThrow<T, E>(this Result<T, E> result, [CallerArgumentExpression(nameof(result))] string? op = null)
where S : struct, Enum where E : struct, Enum
{ {
if (!result.IsSuccess) if (!result.IsSuccess)
{ {
@@ -364,17 +364,34 @@ public static class ResultExtensions
return result.Value; return result.Value;
} }
public static ref T GetValueOrThrow<T, E>(this RefResult<T, E> 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<T>(this Result<T> result, T? defaultValue = default) public static T? GetValueOrDefault<T>(this Result<T> result, T? defaultValue = default)
{ {
return result.IsSuccess ? result.Value : defaultValue; return result.IsSuccess ? result.Value : defaultValue;
} }
public static T? GetValueOrDefault<T, S>(this Result<T, S> result, T? defaultValue = default) public static T? GetValueOrDefault<T, E>(this Result<T, E> result, T? defaultValue = default)
where S : struct, Enum where E : struct, Enum
{ {
return result.IsSuccess ? result.Value : defaultValue; return result.IsSuccess ? result.Value : defaultValue;
} }
public static ref T GetValueOrDefault<T, E>(this RefResult<T, E> result)
where E : struct, Enum
{
return ref result.IsSuccess ? ref result.Value : ref Unsafe.NullRef<T>();
}
public static bool TryGetValue<T>(this Result<T> result, out T value) public static bool TryGetValue<T>(this Result<T> result, out T value)
{ {
if (result.IsSuccess) if (result.IsSuccess)
@@ -529,4 +546,4 @@ public static class ResultExtensions
return onFailure(result.Error); return onFailure(result.Error);
} }
} }
} }

View File

@@ -1,6 +1,5 @@
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Ghost.Core; namespace Ghost.Core;
@@ -13,7 +12,7 @@ public unsafe partial struct TempJobAllocator
internal static void Initialize(nuint capacity) 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)); _pAllocator = (TempJobAllocator*)Malloc((nuint)sizeof(TempJobAllocator));
} }
@@ -71,52 +70,29 @@ public unsafe partial struct TempJobAllocator : IAllocator
State = Unsafe.AsPointer(ref this), State = Unsafe.AsPointer(ref this),
Alloc = &Allocate, Alloc = &Allocate,
Realloc = &Reallocate, Realloc = &Reallocate,
Free = &Free, Free = &Free
#if MHP_ENABLE_SAFETY_CHECKS
IsValid = &IsValid,
#else
IsValid = null,
#endif
}; };
} }
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption)
#if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle* pHandle
#endif
)
{ {
var pSelf = (TempJobAllocator*)instance; var pSelf = (TempJobAllocator*)instance;
var pCurrentArena = pSelf->_pArena + pSelf->_currentFrameIndex; var pCurrentArena = pSelf->_pArena + pSelf->_currentFrameIndex;
var ptr = pCurrentArena->Allocate(size, alignment, allocationOption); var ptr = pCurrentArena->Allocate(size, alignment, allocationOption);
if (ptr == null) if (ptr == null)
{ {
#if MHP_ENABLE_SAFETY_CHECKS
*pHandle = MemoryHandle.Invalid;
#endif
return null; return null;
} }
Interlocked.Increment(ref pSelf->_allocationsPerFrame[pSelf->_currentFrameIndex]); Interlocked.Increment(ref pSelf->_allocationsPerFrame[pSelf->_currentFrameIndex]);
#if MHP_ENABLE_SAFETY_CHECKS
*pHandle = new MemoryHandle(_MAGIC_ID, pSelf->_currentFrameCount);
#endif
return ptr; return ptr;
} }
private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption)
#if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle* pHandle
#endif
)
{ {
if (ptr == null) if (ptr == null)
{ {
return Allocate(instance, newSize, alignment, allocationOption return Allocate(instance, newSize, alignment, allocationOption);
#if MHP_ENABLE_SAFETY_CHECKS
, pHandle
#endif
);
} }
var pSelf = (TempJobAllocator*)instance; var pSelf = (TempJobAllocator*)instance;
@@ -132,24 +108,12 @@ public unsafe partial struct TempJobAllocator : IAllocator
return newPtr; return newPtr;
} }
private static void Free(void* instance, void* ptr private static void Free(void* instance, void* ptr)
#if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle handle
#endif
)
{ {
var pSelf = (TempJobAllocator*)instance; var pSelf = (TempJobAllocator*)instance;
Interlocked.Decrement(ref pSelf->_allocationsPerFrame[pSelf->_currentFrameIndex]); 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() public int AdvanceFrame()
{ {
var allocations = Interlocked.Exchange(ref _allocationsPerFrame[_currentFrameIndex], 0); var allocations = Interlocked.Exchange(ref _allocationsPerFrame[_currentFrameIndex], 0);
@@ -161,4 +125,4 @@ public unsafe partial struct TempJobAllocator : IAllocator
return allocations; return allocations;
} }
} }

View File

@@ -1,36 +0,0 @@
using System.Runtime.CompilerServices;
namespace Ghost.Core.Utilities;
public ref struct BinaryReader
{
private readonly Span<byte> _buffer;
private int _position;
public int Position
{
readonly get => _position;
set => _position = value;
}
public BinaryReader(Span<byte> buffer)
{
_buffer = buffer;
_position = 0;
}
public T Read<T>()
where T : unmanaged
{
var value = Unsafe.ReadUnaligned<T>(ref _buffer[_position]);
_position += Unsafe.SizeOf<T>();
return value;
}
public ReadOnlySpan<byte> ReadBytes(int length)
{
var span = _buffer.Slice(_position, length);
_position += length;
return span;
}
}

View File

@@ -1,10 +1,11 @@
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Core.Utilities; namespace Ghost.Core.Utilities;
public struct BufferWriter : IDisposable public unsafe struct BufferWriter : IDisposable
{ {
private UnsafeList<byte> _buffer; private UnsafeList<byte> _buffer;
private int _position; private int _position;
@@ -21,17 +22,21 @@ public struct BufferWriter : IDisposable
_position = 0; _position = 0;
} }
public unsafe void Write<T>(T value) public void Write<T>(T value)
where T : unmanaged where T : unmanaged
{ {
Unsafe.WriteUnaligned(ref _buffer[_position], value); Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += sizeof(T); _position += sizeof(T);
} }
public void WriteBytes(ReadOnlySpan<byte> data) public void WriteSpan<T>(ReadOnlySpan<T> data)
where T : unmanaged
{ {
data.CopyTo(_buffer.AsSpan().Slice(_position, data.Length)); var size = sizeof(T) * data.Length;
_position += data.Length; var byteSpan = MemoryMarshal.AsBytes(data);
byteSpan.CopyTo(_buffer.AsSpan().Slice(_position, size));
_position += size;
} }
public Span<byte> ReserveSpan(int length) public Span<byte> ReserveSpan(int length)
@@ -51,3 +56,88 @@ public struct BufferWriter : IDisposable
_buffer.Dispose(); _buffer.Dispose();
} }
} }
public unsafe ref struct SpanWriter
{
private Span<byte> _buffer;
private int _position;
public int Position
{
readonly get => _position;
set => _position = value;
}
public SpanWriter(Span<byte> buffer)
{
_buffer = buffer;
_position = 0;
}
public void Write<T>(T value)
where T : unmanaged
{
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += sizeof(T);
}
public void WriteSpan<T>(ReadOnlySpan<T> 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<byte> AsSpan()
{
return _buffer;
}
}
public unsafe ref struct SpanReader
{
private readonly Span<byte> _buffer;
private int _position;
public int Position
{
readonly get => _position;
set => _position = value;
}
public SpanReader(Span<byte> buffer)
{
_buffer = buffer;
_position = 0;
}
public T Read<T>()
where T : unmanaged
{
var value = Unsafe.ReadUnaligned<T>(ref _buffer[_position]);
_position += Unsafe.SizeOf<T>();
return value;
}
public ReadOnlySpan<T> ReadSpan<T>(int length)
where T : unmanaged
{
var size = sizeof(T) * length;
var span = MemoryMarshal.Cast<byte, T>(_buffer.Slice(_position, size));
_position += size;
return span;
}
public ReadOnlySpan<T> ReadToEnd<T>()
where T : unmanaged
{
var span = MemoryMarshal.Cast<byte, T>(_buffer.Slice(_position));
_position += span.Length;
return span;
}
}

View File

@@ -1,7 +1,6 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Entities; using Ghost.Entities;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RenderPipeline;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;

View File

@@ -1,6 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using System.Diagnostics;
namespace Ghost.Engine.RenderPipeline; namespace Ghost.Engine.RenderPipeline;
@@ -16,6 +15,8 @@ internal unsafe class GPUScene : IDisposable
private uint _requiredResize; private uint _requiredResize;
private bool _disposed; private bool _disposed;
public Handle<GPUBuffer> SceneBuffer => _sceneBuffer;
internal GPUScene(IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, uint initialCount) internal GPUScene(IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, uint initialCount)
{ {
_resourceAllocator = resourceAllocator; _resourceAllocator = resourceAllocator;
@@ -30,7 +31,7 @@ internal unsafe class GPUScene : IDisposable
}; };
_sceneBuffer = _resourceAllocator.CreateBuffer(in bufferDesc, "SceneBuffer"); _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; _capacity = initialCount;
} }
@@ -48,22 +49,21 @@ internal unsafe class GPUScene : IDisposable
return; return;
} }
var newCapacity = _capacity * 2; var newCapacity = Math.Max(_capacity * 2, _capacity + _requiredResize);
newCapacity = Math.Max(newCapacity, _capacity + _requiredResize);
var newBufferDesc = new BufferDesc var newBufferDesc = new BufferDesc
{ {
Size = (ulong)newCapacity * (ulong)sizeof(InstanceData), Size = newCapacity * (ulong)sizeof(InstanceData),
Stride = (uint)sizeof(InstanceData), Stride = (uint)sizeof(InstanceData),
Usage = BufferUsage.Structured | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource, Usage = BufferUsage.Structured | BufferUsage.UnorderedAccess | BufferUsage.ShaderResource,
HeapType = HeapType.Default, HeapType = HeapType.Default,
}; };
var newBuffer = _resourceAllocator.CreateBuffer(in newBufferDesc, "SceneBuffer_Resized"); var newBuffer = _resourceAllocator.CreateBuffer(in newBufferDesc, "SceneBuffer_Resized");
Debug.Assert(newBuffer.IsValid); Logger.DebugAssert(newBuffer.IsValid);
// Copy existing data to the new buffer // 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 // Replace old buffer with the new one
_resourceDatabase.ReleaseResource(_sceneBuffer.AsResource()); _resourceDatabase.ReleaseResource(_sceneBuffer.AsResource());

View File

@@ -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<GPUBuffer> addBuffer, int addCount, Handle<GPUBuffer> 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<ComputeShader>);
var keywords = new LocalKeywordSet();
ctx.DispatchCompute(shader, 0, in keywords, in property, new uint3());
}
}

View File

@@ -10,7 +10,7 @@ using System.Diagnostics;
namespace Ghost.Engine.RenderPipeline; namespace Ghost.Engine.RenderPipeline;
internal class GhostRenderPipeline : IRenderPipeline internal partial class GhostRenderPipeline : IRenderPipeline
{ {
private struct AddInstanceData private struct AddInstanceData
{ {
@@ -39,11 +39,11 @@ internal class GhostRenderPipeline : IRenderPipeline
{ {
_renderSystem = renderSystem; _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 _gpuScene = new GPUScene(renderSystem.GraphicsEngine.ResourceAllocator, renderSystem.GraphicsEngine.ResourceDatabase, 102_400u); // 102.4k objects should be enough for now
} }
private static unsafe Handle<GPUBuffer> CreateAddInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase) private static unsafe Handle<GPUBuffer> CreateAddInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase, out int count)
{ {
if (!ghostPayload.AddRequest.IsEmpty) if (!ghostPayload.AddRequest.IsEmpty)
{ {
@@ -82,13 +82,16 @@ internal class GhostRenderPipeline : IRenderPipeline
} }
resourceDatabase.UnmapResource(addBuffer.AsResource(), 0, null); resourceDatabase.UnmapResource(addBuffer.AsResource(), 0, null);
count = i;
return addBuffer; return addBuffer;
} }
count = 0;
return default; return default;
} }
private static unsafe Handle<GPUBuffer> CreateRemoveInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase) private static unsafe Handle<GPUBuffer> CreateRemoveInstanceBuffer(GhostRenderPayload ghostPayload, ResourceManager resourceManager, IResourceDatabase resourceDatabase, out int count)
{ {
if (!ghostPayload.RemoveRequest.IsEmpty) if (!ghostPayload.RemoveRequest.IsEmpty)
{ {
@@ -116,9 +119,12 @@ internal class GhostRenderPipeline : IRenderPipeline
} }
resourceDatabase.UnmapResource(removeBuffer.AsResource(), 0, null); resourceDatabase.UnmapResource(removeBuffer.AsResource(), 0, null);
count = i;
return removeBuffer; return removeBuffer;
} }
count = 0;
return default; return default;
} }
@@ -131,15 +137,20 @@ internal class GhostRenderPipeline : IRenderPipeline
foreach (ref readonly var request in ghostPayload.RenderRequests) 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);
} }
} }

View File

@@ -39,7 +39,7 @@ internal sealed class GhostRenderPayload : IRenderPayload
{ {
_renderPipeline = renderPipeline; _renderPipeline = renderPipeline;
_renderRequests = new UnsafeList<RenderRequest>(4, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent); _renderRequests = new UnsafeList<RenderRequest>(4, Misaki.HighPerformance.LowLevel.Buffer.AllocationHandle.Persistent);
_addRequest = new ConcurrentQueue<AddInstanceRequest>(); _addRequest = new ConcurrentQueue<AddInstanceRequest>();
_removeRequest = new ConcurrentQueue<RemoveInstanceRequest>(); _removeRequest = new ConcurrentQueue<RemoveInstanceRequest>();
} }

View File

@@ -15,6 +15,7 @@ public class RenderPipelineSystemAttribute<T> : RenderPipelineSystemAttribute
public override Type SettingsType => typeof(T); public override Type SettingsType => typeof(T);
} }
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public static class RenderPipelineSystemRegistry public static class RenderPipelineSystemRegistry
{ {
private static readonly Dictionary<nint, List<Func<ISystem>>> s_renderPipelineSystems = new(); private static readonly Dictionary<nint, List<Func<ISystem>>> s_renderPipelineSystems = new();

View File

@@ -124,8 +124,8 @@ internal unsafe struct Chunk : IDisposable
public Chunk(int bufferSize, int capacity, int componentCount, uint globalVersion) public Chunk(int bufferSize, int capacity, int componentCount, uint globalVersion)
{ {
_data = new UnsafeArray<byte>(bufferSize, Allocator.Persistent, AllocationOption.Clear); _data = new UnsafeArray<byte>(bufferSize, AllocationHandle.Persistent, AllocationOption.Clear);
_versions = new UnsafeArray<uint>(componentCount, Allocator.Persistent); _versions = new UnsafeArray<uint>(componentCount, AllocationHandle.Persistent);
_capacity = capacity; _capacity = capacity;
_count = 0; _count = 0;
@@ -200,13 +200,13 @@ internal unsafe struct Archetype : IDisposable
_id = id; _id = id;
_worldID = worldID; _worldID = worldID;
_chunks = new UnsafeList<Chunk>(4, Allocator.Persistent); _chunks = new UnsafeList<Chunk>(4, AllocationHandle.Persistent);
_edgesAdd = new UnsafeList<Edge>(4, Allocator.Persistent); _edgesAdd = new UnsafeList<Edge>(4, AllocationHandle.Persistent);
_edgesRemove = new UnsafeList<Edge>(4, Allocator.Persistent); _edgesRemove = new UnsafeList<Edge>(4, AllocationHandle.Persistent);
if (componentIds.IsEmpty) if (componentIds.IsEmpty)
{ {
_signature = new UnsafeBitSet(1, Allocator.Persistent, AllocationOption.Clear); _signature = new UnsafeBitSet(1, AllocationHandle.Persistent, AllocationOption.Clear);
_hash = 0; _hash = 0;
_signature.ClearAll(); _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(); _hash = _signature.GetHashCode();
CalculateLayout(componentIds); CalculateLayout(componentIds);
@@ -268,8 +268,8 @@ internal unsafe struct Archetype : IDisposable
_maxComponentID = maxComponentID; _maxComponentID = maxComponentID;
_entityCapacity = Chunk.CHUNK_BUFFER_SIZE / bytesPerEntity; _entityCapacity = Chunk.CHUNK_BUFFER_SIZE / bytesPerEntity;
_layouts = new UnsafeArray<ComponentMemoryLayout>(components.Length, Allocator.Persistent); _layouts = new UnsafeArray<ComponentMemoryLayout>(components.Length, AllocationHandle.Persistent);
_componentIDToLayoutIndex = new UnsafeArray<int>(_maxComponentID + 1, Allocator.Persistent); _componentIDToLayoutIndex = new UnsafeArray<int>(_maxComponentID + 1, AllocationHandle.Persistent);
_componentIDToLayoutIndex.AsSpan().Fill(-1); _componentIDToLayoutIndex.AsSpan().Fill(-1);

View File

@@ -168,11 +168,11 @@ public partial class ComponentManager : IDisposable
{ {
_world = world; _world = world;
_archetypes = new UnsafeList<Archetype>(16, Allocator.Persistent); _archetypes = new UnsafeList<Archetype>(16, AllocationHandle.Persistent);
_entityQueries = new UnsafeList<EntityQuery>(16, Allocator.Persistent); _entityQueries = new UnsafeList<EntityQuery>(16, AllocationHandle.Persistent);
_archetypeLookup = new UnsafeHashMap<int, Identifier<Archetype>>(16, Allocator.Persistent); _archetypeLookup = new UnsafeHashMap<int, Identifier<Archetype>>(16, AllocationHandle.Persistent);
_querieLookup = new UnsafeHashMap<int, Identifier<EntityQuery>>(16, Allocator.Persistent); _querieLookup = new UnsafeHashMap<int, Identifier<EntityQuery>>(16, AllocationHandle.Persistent);
// Create the empty archetype // Create the empty archetype
CreateArchetype(ReadOnlySpan<Identifier<IComponent>>.Empty, 0); CreateArchetype(ReadOnlySpan<Identifier<IComponent>>.Empty, 0);
@@ -297,11 +297,6 @@ public struct ComponentSet : IDisposable, IEquatable<ComponentSet>
_hashCode = -1; _hashCode = -1;
} }
public ComponentSet(Allocator allocator, params ReadOnlySpan<Identifier<IComponent>> components)
: this(AllocationManager.GetAllocationHandle(allocator), components)
{
}
public readonly bool Equals(ComponentSet other) public readonly bool Equals(ComponentSet other)
{ {
return _hashCode == other._hashCode; return _hashCode == other._hashCode;

View File

@@ -25,7 +25,7 @@ public unsafe class EntityCommandBuffer : IDisposable
public EntityCommandBuffer(EntityManager entityManager) public EntityCommandBuffer(EntityManager entityManager)
{ {
_entityManager = entityManager; _entityManager = entityManager;
_buffer = new UnsafeList<byte>(4096, Allocator.Persistent); _buffer = new UnsafeList<byte>(4096, AllocationHandle.Persistent);
} }
~EntityCommandBuffer() ~EntityCommandBuffer()

View File

@@ -50,7 +50,7 @@ public unsafe partial class EntityManager : IDisposable
internal EntityManager(World world, int initialCapacity) internal EntityManager(World world, int initialCapacity)
{ {
_world = world; _world = world;
_entityLocations = new UnsafeSlotMap<EntityLocation>(initialCapacity, Allocator.Persistent, AllocationOption.Clear); _entityLocations = new UnsafeSlotMap<EntityLocation>(initialCapacity, AllocationHandle.Persistent, AllocationOption.Clear);
_scriptComponents = new SlotMap<List<ScriptComponent>>(initialCapacity / 2); _scriptComponents = new SlotMap<List<ScriptComponent>>(initialCapacity / 2);
// _storages = new IManagedComponentStorage[16]; // _storages = new IManagedComponentStorage[16];
} }
@@ -638,7 +638,7 @@ public unsafe partial class EntityManager : IDisposable
newArchetype.SetComponentData(newChunkIndex, newRowIndex, componentID, pComponent); newArchetype.SetComponentData(newChunkIndex, newRowIndex, componentID, pComponent);
var r = oldArchetype.RemoveEntity(location.chunkIndex, location.rowIndex); 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) if (r != Error.None)
{ {
return r; return r;
@@ -742,7 +742,7 @@ public unsafe partial class EntityManager : IDisposable
newArchetype.SetEntity(newChunkIndex, newRowIndex, entity); newArchetype.SetEntity(newChunkIndex, newRowIndex, entity);
var r = oldArchetype.RemoveEntity(location.chunkIndex, location.rowIndex); 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) if (r != Error.None)
{ {
return r; return r;

View File

@@ -345,7 +345,7 @@ public unsafe partial struct EntityQuery : IDisposable
_id = id; _id = id;
_worldID = worldID; _worldID = worldID;
_mask = mask; _mask = mask;
_matchingArchetypes = new UnsafeList<Identifier<Archetype>>(8, Allocator.Persistent); _matchingArchetypes = new UnsafeList<Identifier<Archetype>>(8, AllocationHandle.Persistent);
} }
// TODO: Fetching layout every time is not optimal. Cache them? // TODO: Fetching layout every time is not optimal. Cache them?
@@ -642,7 +642,7 @@ public ref partial struct QueryBuilder : IDisposable
public Identifier<EntityQuery> Build(World world, bool dispose = true) public Identifier<EntityQuery> 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 maskHash = mask.GetHashCode();
var queryID = world.ComponentManager.GetEntityQueryIDByMaskHash(maskHash); var queryID = world.ComponentManager.GetEntityQueryIDByMaskHash(maskHash);

View File

@@ -56,7 +56,7 @@ internal sealed unsafe class SharedComponentStore : IDisposable
public SharedComponentStore(int initialCapacity = 16) public SharedComponentStore(int initialCapacity = 16)
{ {
_perType = new UnsafeHashMap<int, TypeStore>(initialCapacity, Allocator.Persistent); _perType = new UnsafeHashMap<int, TypeStore>(initialCapacity, AllocationHandle.Persistent);
} }
~SharedComponentStore() ~SharedComponentStore()
@@ -203,9 +203,9 @@ internal sealed unsafe class SharedComponentStore : IDisposable
var store = new TypeStore var store = new TypeStore
{ {
typeSize = typeSize, typeSize = typeSize,
data = new UnsafeList<byte>(typeSize * 16, Allocator.Persistent), data = new UnsafeList<byte>(typeSize * 16, AllocationHandle.Persistent),
infos = new UnsafeList<EntryInfo>(16, Allocator.Persistent), infos = new UnsafeList<EntryInfo>(16, AllocationHandle.Persistent),
hashLookup = new UnsafeHashMap<long, int>(16, Allocator.Persistent), hashLookup = new UnsafeHashMap<long, int>(16, AllocationHandle.Persistent),
freeListHead = 0, freeListHead = 0,
versionCounter = 0 versionCounter = 0
}; };
@@ -217,7 +217,7 @@ internal sealed unsafe class SharedComponentStore : IDisposable
_perType.Add(componentTypeId, store); _perType.Add(componentTypeId, store);
existing = ref _perType.GetValueRef(componentTypeId, out exist); existing = ref _perType.GetValueRef(componentTypeId, out exist);
Debug.Assert(exist); Logger.DebugAssert(exist);
return ref existing; return ref existing;
} }

View File

@@ -61,7 +61,7 @@ public abstract class SystemBase : ISystem
{ {
if (!_requiredQueries.IsCreated) if (!_requiredQueries.IsCreated)
{ {
_requiredQueries = new UnsafeList<int>(4, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent); _requiredQueries = new UnsafeList<int>(4, Misaki.HighPerformance.LowLevel.Buffer.AllocationHandle.Persistent);
} }
_requiredQueries.Add(queryID.Value); _requiredQueries.Add(queryID.Value);

View File

@@ -1,4 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.Graphics.D3D12.Utilities; using Ghost.Graphics.D3D12.Utilities;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel;
@@ -473,7 +474,7 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
ref var record = ref recordResult.Value; ref var record = ref recordResult.Value;
var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.rtv); var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.rtv);
var format = record.desc.TextureDescription.Format.ToDXGIFormat(); var format = record.desc.TextureDescriptor.Format.ToDXGIFormat();
var clearColor = rtDesc.ClearColor; var clearColor = rtDesc.ClearColor;
// Map load operation // Map load operation
@@ -527,7 +528,7 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
ref var record = ref recordResult.Value; ref var record = ref recordResult.Value;
var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.dsv); var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.dsv);
var format = record.desc.TextureDescription.Format.ToDXGIFormat(); var format = record.desc.TextureDescriptor.Format.ToDXGIFormat();
// Map depth load operation // Map depth load operation
var depthLoadAccessType = depthDesc.DepthLoadOp switch var depthLoadAccessType = depthDesc.DepthLoadOp switch
@@ -638,7 +639,7 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
pNativeObject->RSSetViewports(1, &d3d12Viewport); pNativeObject->RSSetViewports(1, &d3d12Viewport);
} }
public void SetPipelineState(Key128<GraphicsPipeline> pipelineKey) public void SetPipelineState(Key128<PipelineState> pipelineKey)
{ {
AssertNotDisposed(); AssertNotDisposed();
ThrowIfNotRecording(); ThrowIfNotRecording();
@@ -701,7 +702,7 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
{ {
BufferLocation = record.ResourcePtr.Get()->GetGPUVirtualAddress() + offset, BufferLocation = record.ResourcePtr.Get()->GetGPUVirtualAddress() + offset,
SizeInBytes = (uint)(record.ResourcePtr.Get()->GetDesc().Width - offset), SizeInBytes = (uint)(record.ResourcePtr.Get()->GetDesc().Width - offset),
StrideInBytes = record.desc.BufferDescription.Stride StrideInBytes = record.desc.BufferDescriptor.Stride
}; };
pNativeObject->IASetVertexBuffers(slot, 1, &vbView); pNativeObject->IASetVertexBuffers(slot, 1, &vbView);
@@ -859,7 +860,7 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
IncrementCommandCount(); IncrementCommandCount();
Debug.Assert(commandSignature is D3D12CommandSignature); Logger.DebugAssert(commandSignature is D3D12CommandSignature);
var resource = _resourceDatabase.GetResource(argumentBuffer.AsResource()); var resource = _resourceDatabase.GetResource(argumentBuffer.AsResource());
var countResource = _resourceDatabase.GetResource(countBuffer.AsResource()); var countResource = _resourceDatabase.GetResource(countBuffer.AsResource());

View File

@@ -1,4 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using TerraFX.Interop.DirectX; using TerraFX.Interop.DirectX;
@@ -88,7 +89,7 @@ internal unsafe class D3D12CommandSignature : D3D12Object<ID3D12CommandSignature
public IntPtr NativePointer => (IntPtr)NativeObject.Get(); public IntPtr NativePointer => (IntPtr)NativeObject.Get();
public D3D12CommandSignature(D3D12RenderDevice device, D3D12PipelineLibrary pipelineLibrary, ref readonly CommandSignatureDesc desc, Key128<GraphicsPipeline> pipelineKey) public D3D12CommandSignature(D3D12RenderDevice device, D3D12PipelineLibrary pipelineLibrary, ref readonly CommandSignatureDesc desc, Key128<PipelineState> pipelineKey)
: base(CreateCommandSignature(device, pipelineLibrary, in desc)) : base(CreateCommandSignature(device, pipelineLibrary, in desc))
{ {
} }

View File

@@ -1,3 +1,4 @@
using Ghost.Core;
using Ghost.Graphics.D3D12.Utilities; using Ghost.Graphics.D3D12.Utilities;
using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
@@ -67,7 +68,7 @@ internal unsafe class D3D12DescriptorHeap : IDisposable
Stride = device.NativeObject.Get()->GetDescriptorHandleIncrementSize(type); Stride = device.NativeObject.Get()->GetDescriptorHandleIncrementSize(type);
var success = AllocateResources(numDescriptors); var success = AllocateResources(numDescriptors);
Debug.Assert(success); Logger.DebugAssert(success);
_heap.Get()->SetName(name); _heap.Get()->SetName(name);
if (ShaderVisible) if (ShaderVisible)
@@ -240,7 +241,7 @@ internal unsafe class D3D12DescriptorHeap : IDisposable
if (!_allocatedDescriptors.IsCreated) 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 else
{ {
@@ -299,7 +300,7 @@ internal unsafe class D3D12DescriptorHeap : IDisposable
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
Debug.Assert(NumAllocatedDescriptors == 0); Logger.DebugAssert(NumAllocatedDescriptors == 0);
_heap.Dispose(); _heap.Dispose();
_shaderVisibleHeap.Dispose(); _shaderVisibleHeap.Dispose();

View File

@@ -3,9 +3,9 @@
#endif #endif
using Ghost.Core; using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Utilities; using Misaki.HighPerformance.Utilities;
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Ghost.Graphics.D3D12; namespace Ghost.Graphics.D3D12;
@@ -80,13 +80,13 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
public ICommandAllocator CreateCommandAllocator(CommandBufferType type = CommandBufferType.Graphics) public ICommandAllocator CreateCommandAllocator(CommandBufferType type = CommandBufferType.Graphics)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return new D3D12CommandAllocator(_device, type); return new D3D12CommandAllocator(_device, type);
} }
public ICommandBuffer CreateCommandBuffer(CommandBufferType type = CommandBufferType.Graphics) public ICommandBuffer CreateCommandBuffer(CommandBufferType type = CommandBufferType.Graphics)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return new D3D12CommandBuffer( return new D3D12CommandBuffer(
_device, _device,
@@ -99,7 +99,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
public ICommandBuffer GetPooledCommandBuffer(CommandBufferType type = CommandBufferType.Graphics) public ICommandBuffer GetPooledCommandBuffer(CommandBufferType type = CommandBufferType.Graphics)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
for (var i = 0; i < _commandBufferPool.Count; i++) for (var i = 0; i < _commandBufferPool.Count; i++)
{ {
@@ -116,25 +116,25 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
public void ReturnPooledCommandBuffer(ICommandBuffer commandBuffer) public void ReturnPooledCommandBuffer(ICommandBuffer commandBuffer)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
_commandBufferReturnQueue.Enqueue(new CommandBufferReturnEntry(commandBuffer, _cpuFrame)); _commandBufferReturnQueue.Enqueue(new CommandBufferReturnEntry(commandBuffer, _cpuFrame));
} }
public ISwapChain CreateSwapChain(SwapChainDesc desc) public ISwapChain CreateSwapChain(SwapChainDesc desc)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return new DXGISwapChain(_resourceDatabase, _descriptorAllocator, _device, desc, _desc.FrameBufferCount); return new DXGISwapChain(_resourceDatabase, _descriptorAllocator, _device, desc, _desc.FrameBufferCount);
} }
public ICommandSignature CreateCommandSignature(ref readonly CommandSignatureDesc desc, Key128<GraphicsPipeline> pipelineKey) public ICommandSignature CreateCommandSignature(ref readonly CommandSignatureDesc desc, Key128<PipelineState> pipelineKey)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return new D3D12CommandSignature(_device, _pipelineLibrary, in desc, pipelineKey); return new D3D12CommandSignature(_device, _pipelineLibrary, in desc, pipelineKey);
} }
public void BeginFrame(ulong cpuFrame) public void BeginFrame(ulong cpuFrame)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
_cpuFrame = cpuFrame; _cpuFrame = cpuFrame;
_resourceDatabase.BeginFrame(cpuFrame); _resourceDatabase.BeginFrame(cpuFrame);
@@ -142,7 +142,7 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
public void EndFrame(ulong gpuFrame) public void EndFrame(ulong gpuFrame)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
_resourceDatabase.EndFrame(gpuFrame); _resourceDatabase.EndFrame(gpuFrame);

View File

@@ -4,6 +4,7 @@ using Ghost.Graphics.D3D12.Utilities;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Ghost.Core;
namespace Ghost.Graphics.D3D12; namespace Ghost.Graphics.D3D12;
@@ -51,7 +52,7 @@ public unsafe abstract class D3D12Object<T>: IRHIObject
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void AssertNotDisposed() 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) protected virtual void Dispose(bool disposing)

View File

@@ -60,7 +60,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
{ {
_device = device; _device = device;
_pipelineCache = new UnsafeHashMap<UInt128, D3D12PipelineState>(32, Allocator.Persistent); _pipelineCache = new UnsafeHashMap<UInt128, D3D12PipelineState>(32, AllocationHandle.Persistent);
CreateDefaultRootSignature().ThrowIfFailed(); CreateDefaultRootSignature().ThrowIfFailed();
} }
@@ -129,7 +129,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
} }
var size = pNativeObject->GetSerializedSize(); var size = pNativeObject->GetSerializedSize();
using var buffer = new UnsafeArray<byte>((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<byte>((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)); ThrowIfFailed(pNativeObject->Serialize(buffer.GetUnsafePtr(), size));
@@ -157,7 +157,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
} }
var hr = pNativeObject->LoadPipeline(pKeyStr, pStreamDesc, __uuidof(pPipelineState), (void**)&pPipelineState); var hr = pNativeObject->LoadPipeline(pKeyStr, pStreamDesc, __uuidof(pPipelineState), (void**)&pPipelineState);
if (hr == E.E_INVALIDARG) if (hr == E.E_INVALIDARG)
{ {
// Pipeline not found in the library, create a new one. // Pipeline not found in the library, create a new one.
@@ -177,37 +177,37 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
return Result.Success(); return Result.Success();
} }
public Result<Key128<GraphicsPipeline>> CreateGraphicsPipeline(ref readonly GraphicsPSODescriptor descriptor, ReadOnlySpan<byte> asByteCode, ReadOnlySpan<byte> msByteCode, ReadOnlySpan<byte> psByteCode) public Result<Key128<PipelineState>> CreateGraphicsPipeline(ref readonly GraphicsPSODesc desc)
{ {
AssertNotDisposed(); 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}."); 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 passAttachmentKey = new PassAttachmentHash(desc.RtvFormats, desc.DsvFormat);
var pipelineKey = RHIUtility.CreateGraphicsPipelineKey(descriptor.VariantKey, descriptor.PipelineOption, passPipelineKey); var pipelineKey = RHIUtility.CreateGraphicsPipelineKey(desc.VariantKey, desc.PipelineOption, passAttachmentKey);
if (!_pipelineCache.ContainsKey(pipelineKey)) 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(), pRootSignature = _defaultRootSignature.Get(),
MS = new D3D12_SHADER_BYTECODE(pMSByteCode, (nuint)msByteCode.Length), MS = new D3D12_SHADER_BYTECODE(pMSByteCode, (nuint)desc.MsCode.Length),
PS = new D3D12_SHADER_BYTECODE(pPSByteCode, (nuint)psByteCode.Length), PS = new D3D12_SHADER_BYTECODE(pPSByteCode, (nuint)desc.PsCode.Length),
PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE,
SampleMask = UINT32_MAX, SampleMask = UINT32_MAX,
SampleDesc = new DXGI_SAMPLE_DESC(1, 0), SampleDesc = new DXGI_SAMPLE_DESC(1, 0),
NumRenderTargets = (uint)descriptor.RtvFormats.Length, NumRenderTargets = (uint)desc.RtvFormats.Length,
DSVFormat = descriptor.DsvFormat.ToDXGIFormat(), DSVFormat = desc.DsvFormat.ToDXGIFormat(),
DepthStencilState = BuildDepthStencil(descriptor.PipelineOption.ZTest, descriptor.PipelineOption.ZWrite), DepthStencilState = BuildDepthStencil(desc.PipelineOption.ZTest, desc.PipelineOption.ZWrite),
NodeMask = 0, NodeMask = 0,
Flags = D3D12_PIPELINE_STATE_FLAG_NONE, Flags = D3D12_PIPELINE_STATE_FLAG_NONE,
BlendState = descriptor.PipelineOption.Blend switch BlendState = desc.PipelineOption.Blend switch
{ {
Blend.Opaque => D3D12Utility.D3D12_BLEND_DESC_OPAQUE, Blend.Opaque => D3D12Utility.D3D12_BLEND_DESC_OPAQUE,
Blend.Alpha => D3D12Utility.D3D12_BLEND_DESC_ALPHA_BLEND, Blend.Alpha => D3D12Utility.D3D12_BLEND_DESC_ALPHA_BLEND,
@@ -216,7 +216,7 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
Blend.PremultipliedAlpha => D3D12Utility.D3D12_BLEND_DESC_PREMULTIPLIED, Blend.PremultipliedAlpha => D3D12Utility.D3D12_BLEND_DESC_PREMULTIPLIED,
_ => D3D12Utility.D3D12_BLEND_DESC_OPAQUE _ => D3D12Utility.D3D12_BLEND_DESC_OPAQUE
}, },
RasterizerState = descriptor.PipelineOption.Cull switch RasterizerState = desc.PipelineOption.Cull switch
{ {
Cull.Off => D3D12Utility.D3D12_RASTERIZER_DESC_CULL_NONE, Cull.Off => D3D12Utility.D3D12_RASTERIZER_DESC_CULL_NONE,
Cull.Front => D3D12Utility.D3D12_RASTERIZER_DESC_CULL_CLOCKWISE, Cull.Front => D3D12Utility.D3D12_RASTERIZER_DESC_CULL_CLOCKWISE,
@@ -225,25 +225,25 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
}, },
}; };
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(); msPipelinedesc.RTVFormats[i] = desc.RtvFormats[i].ToDXGIFormat();
desc.BlendState.RenderTarget[i].RenderTargetWriteMask = (byte)((int)descriptor.PipelineOption.ColorMask & 0x0F); 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 var streamDesc = new D3D12_PIPELINE_STATE_STREAM_DESC
{ {
pPipelineStateSubobjectStream = &meshStream, pPipelineStateSubobjectStream = &meshStream,
SizeInBytes = (nuint)sizeof(CD3DX12_PIPELINE_MESH_STATE_STREAM) 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) if (result.IsFailure)
{ {
return result; return result;
@@ -254,25 +254,25 @@ internal unsafe class D3D12PipelineLibrary : D3D12Object<ID3D12PipelineLibrary1>
return pipelineKey; return pipelineKey;
} }
public Result<Key128<ComputePipeline>> CreateComputePipeline(ref readonly ComputePSODescriptor descriptor, ReadOnlySpan<byte> csBytecode) public Result<Key128<PipelineState>> CreateComputePipeline(ref readonly ComputePSODesc desc)
{ {
AssertNotDisposed(); AssertNotDisposed();
var pipelineKey = RHIUtility.CreateComputePipelineKey(descriptor.VariantKey); var pipelineKey = RHIUtility.CreateComputePipelineKey(desc.VariantKey);
if (!_pipelineCache.ContainsKey(pipelineKey)) if (!_pipelineCache.ContainsKey(pipelineKey))
{ {
fixed (byte* pCSByteCode = csBytecode) fixed (byte* pCSByteCode = desc.CsCode)
{ {
var byteCode = new D3D12_SHADER_BYTECODE(pCSByteCode, (nuint)csBytecode.Length); var byteCode = new D3D12_SHADER_BYTECODE(pCSByteCode, (nuint)desc.CsCode.Length);
var desc = new CD3DX12_PIPELINE_STATE_STREAM_CS(in byteCode); var csPipelineDesc = new CD3DX12_PIPELINE_STATE_STREAM_CS(in byteCode);
var streamDesc = new D3D12_PIPELINE_STATE_STREAM_DESC var streamDesc = new D3D12_PIPELINE_STATE_STREAM_DESC
{ {
pPipelineStateSubobjectStream = &desc, pPipelineStateSubobjectStream = &csPipelineDesc,
SizeInBytes = (nuint)sizeof(CD3DX12_PIPELINE_STATE_STREAM_CS) 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) if (result.IsFailure)
{ {
return result; return result;

View File

@@ -2,7 +2,6 @@ using Ghost.Core;
using Ghost.Graphics.D3D12.Utilities; using Ghost.Graphics.D3D12.Utilities;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel;
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using TerraFX.Interop.DirectX; using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows; using TerraFX.Interop.Windows;
@@ -510,11 +509,11 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
D3D12_RESOURCE_DESC d3d12Desc; D3D12_RESOURCE_DESC d3d12Desc;
if (desc.Type == ResourceType.Texture) if (desc.Type == ResourceType.Texture)
{ {
d3d12Desc = desc.TextureDescription.ToD3D12ResourceDesc(); d3d12Desc = desc.TextureDescriptor.ToD3D12ResourceDesc();
} }
else else
{ {
d3d12Desc = desc.BufferDescription.ToD3D12ResourceDesc(); d3d12Desc = desc.BufferDescriptor.ToD3D12ResourceDesc();
} }
var info = _device.NativeObject.Get()->GetResourceAllocationInfo(0, 1, &d3d12Desc); var info = _device.NativeObject.Get()->GetResourceAllocationInfo(0, 1, &d3d12Desc);
@@ -559,7 +558,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
public Handle<GPUTexture> CreateTexture(ref readonly TextureDesc desc, string? name = null, CreationOptions options = default) public Handle<GPUTexture> CreateTexture(ref readonly TextureDesc desc, string? name = null, CreationOptions options = default)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
CheckTexture2DSize(desc.Width, desc.Height); CheckTexture2DSize(desc.Width, desc.Height);
@@ -659,7 +658,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
public Handle<GPUTexture> CreateRenderTarget(ref readonly RenderTargetDesc desc, string? name = null, CreationOptions options = default) public Handle<GPUTexture> CreateRenderTarget(ref readonly RenderTargetDesc desc, string? name = null, CreationOptions options = default)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
var textureDesc = desc.ToTextureDescription(); var textureDesc = desc.ToTextureDescription();
return CreateTexture(in textureDesc, name, options); return CreateTexture(in textureDesc, name, options);
@@ -667,7 +666,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
public Handle<GPUBuffer> CreateBuffer(ref readonly BufferDesc desc, string? name = null, CreationOptions options = default) public Handle<GPUBuffer> CreateBuffer(ref readonly BufferDesc desc, string? name = null, CreationOptions options = default)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
CheckBufferSize(desc.Size); CheckBufferSize(desc.Size);
var resourceDesc = desc.ToD3D12ResourceDesc(); var resourceDesc = desc.ToD3D12ResourceDesc();
@@ -771,7 +770,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
public Identifier<Sampler> CreateSampler(ref readonly SamplerDesc desc) public Identifier<Sampler> CreateSampler(ref readonly SamplerDesc desc)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
if (_resourceDatabase.TryGetSampler(in desc, out var id)) if (_resourceDatabase.TryGetSampler(in desc, out var id))
{ {

View File

@@ -4,7 +4,6 @@ using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel; using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using TerraFX.Interop.DirectX; using TerraFX.Interop.DirectX;
@@ -16,19 +15,19 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
internal unsafe record struct ResourceRecord internal unsafe record struct ResourceRecord
{ {
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Explicit)]
public struct ResourceUnion public struct __resource_union
{ {
[FieldOffset(0)] [FieldOffset(0)]
public UniquePtr<D3D12MA_Allocation> allocation; public UniquePtr<D3D12MA_Allocation> allocation;
[FieldOffset(0)] [FieldOffset(0)]
public UniquePtr<ID3D12Resource> resource; public UniquePtr<ID3D12Resource> resource;
public ResourceUnion(D3D12MA_Allocation* allocation) public __resource_union(D3D12MA_Allocation* allocation)
{ {
this.allocation = allocation; this.allocation = allocation;
} }
public ResourceUnion(ID3D12Resource* resource) public __resource_union(ID3D12Resource* resource)
{ {
this.resource = resource; this.resource = resource;
} }
@@ -36,7 +35,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
public ResourceDesc desc; public ResourceDesc desc;
public ResourceViewGroup viewGroup; public ResourceViewGroup viewGroup;
public ResourceUnion resource; public __resource_union resource;
public ResourceBarrierData barrierData; public ResourceBarrierData barrierData;
@@ -47,7 +46,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
public ResourceRecord(D3D12MA_Allocation* allocation, ResourceBarrierData barrierData, ResourceViewGroup viewGroup, ResourceDesc desc) 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.isExternal = false;
this.viewGroup = viewGroup; this.viewGroup = viewGroup;
@@ -57,7 +56,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
public ResourceRecord(ID3D12Resource* resource, ResourceBarrierData barrierData, ResourceViewGroup viewGroup, ResourceDesc desc) 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.isExternal = true;
this.viewGroup = viewGroup; this.viewGroup = viewGroup;
@@ -118,13 +117,13 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
{ {
_descriptorAllocator = descriptorAllocator; _descriptorAllocator = descriptorAllocator;
_resources = new UnsafeSlotMap<ResourceRecord>(64, Allocator.Persistent, AllocationOption.Clear); _resources = new UnsafeSlotMap<ResourceRecord>(64, AllocationHandle.Persistent, AllocationOption.Clear);
_samplers = new UnsafeHashMap<SamplerDesc, Identifier<Sampler>>(32, Allocator.Persistent); _samplers = new UnsafeHashMap<SamplerDesc, Identifier<Sampler>>(32, AllocationHandle.Persistent);
#if DEBUG || GHOST_EDITOR #if DEBUG || GHOST_EDITOR
_resourceName = new Dictionary<Handle<GPUResource>, string>(64); _resourceName = new Dictionary<Handle<GPUResource>, string>(64);
#endif #endif
_releaseQueue = new UnsafeQueue<ReleaseEntry>(32, Allocator.Persistent); _releaseQueue = new UnsafeQueue<ReleaseEntry>(32, AllocationHandle.Persistent);
} }
~D3D12ResourceDatabase() ~D3D12ResourceDatabase()
@@ -134,7 +133,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
internal Handle<GPUResource> ImportExternalResource(ID3D12Resource* pResource, ResourceBarrierData initialBarrierData, ResourceViewGroup viewGroup, ResourceDesc desc, string? name = null) internal Handle<GPUResource> ImportExternalResource(ID3D12Resource* pResource, ResourceBarrierData initialBarrierData, ResourceViewGroup viewGroup, ResourceDesc desc, string? name = null)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
if (pResource == null) if (pResource == null)
{ {
@@ -173,7 +172,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
internal Handle<GPUResource> AddAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData initialBarrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null) internal Handle<GPUResource> AddAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData initialBarrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
if (allocation == null) if (allocation == null)
{ {
@@ -217,25 +216,25 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
public void EnterParallelRead() public void EnterParallelRead()
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
Interlocked.Exchange(ref _writeLock, 1); Interlocked.Exchange(ref _writeLock, 1);
} }
public void ExitParallelRead() public void ExitParallelRead()
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
Interlocked.Exchange(ref _writeLock, 0); Interlocked.Exchange(ref _writeLock, 0);
} }
public bool HasResource(Handle<GPUResource> handle) public bool HasResource(Handle<GPUResource> handle)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return _resources.Contains(handle.ID, handle.Generation); return _resources.Contains(handle.ID, handle.Generation);
} }
public RefResult<ResourceRecord, Error> GetResourceRecord(Handle<GPUResource> handle) public RefResult<ResourceRecord, Error> GetResourceRecord(Handle<GPUResource> handle)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist) if (!exist)
@@ -296,7 +295,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
var r = GetResourceRecord(handle); var r = GetResourceRecord(handle);
if (r.IsFailure || !r.Value.Allocated) if (r.IsFailure || !r.Value.Allocated)
{ {
return ~0u; return uint.MaxValue;
} }
return access switch return access switch
@@ -310,7 +309,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
public string? GetResourceName(Handle<GPUResource> handle) public string? GetResourceName(Handle<GPUResource> handle)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
#if DEBUG || GHOST_EDITOR #if DEBUG || GHOST_EDITOR
if (_resourceName.TryGetValue(handle, out var name)) if (_resourceName.TryGetValue(handle, out var name))
@@ -323,7 +322,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
public void ReleaseResource(Handle<GPUResource> handle) public void ReleaseResource(Handle<GPUResource> handle)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
if (!_resources.TryGetElementAt(handle.ID, handle.Generation, out var record)) if (!_resources.TryGetElementAt(handle.ID, handle.Generation, out var record))
{ {
@@ -342,7 +341,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
public void ReleaseResourceImmediately(Handle<GPUResource> handle) public void ReleaseResourceImmediately(Handle<GPUResource> handle)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist || !info.Allocated) if (!exist || !info.Allocated)
@@ -356,7 +355,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
public Identifier<Sampler> AddSampler(ref readonly SamplerDesc desc, int id) public Identifier<Sampler> AddSampler(ref readonly SamplerDesc desc, int id)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
if (_samplers.ContainsKey(desc)) if (_samplers.ContainsKey(desc))
{ {
@@ -371,13 +370,13 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
public bool TryGetSampler(ref readonly SamplerDesc desc, out Identifier<Sampler> id) public bool TryGetSampler(ref readonly SamplerDesc desc, out Identifier<Sampler> id)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return _samplers.TryGetValue(desc, out id); return _samplers.TryGetValue(desc, out id);
} }
public void ReleaseSampler(Identifier<Sampler> id) public void ReleaseSampler(Identifier<Sampler> id)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
// NOTE: We almost never release samplers individually, because they are cheap and can be reused. // 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. // 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) internal void BeginFrame(ulong cpuFrame)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
_cpuFrame = cpuFrame; _cpuFrame = cpuFrame;
} }
internal void EndFrame(ulong gpuFrame) internal void EndFrame(ulong gpuFrame)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
while (_releaseQueue.TryPeek(out var toRelease) && toRelease.fenceValue < gpuFrame) while (_releaseQueue.TryPeek(out var toRelease) && toRelease.fenceValue < gpuFrame)
{ {
@@ -464,7 +463,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
internal void ReleaseAllResourcesImmediately() internal void ReleaseAllResourcesImmediately()
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
foreach (ref var entry in _releaseQueue) foreach (ref var entry in _releaseQueue)
{ {

View File

@@ -108,7 +108,7 @@ internal unsafe class DXGISwapChain : ISwapChain
public DXGISwapChain(D3D12ResourceDatabase resourceDatabase, D3D12DescriptorAllocator descriptorAllocator, D3D12RenderDevice device, SwapChainDesc desc, uint bufferCount) public DXGISwapChain(D3D12ResourceDatabase resourceDatabase, D3D12DescriptorAllocator descriptorAllocator, D3D12RenderDevice device, SwapChainDesc desc, uint bufferCount)
{ {
Debug.Assert(bufferCount >= 2); Logger.DebugAssert(bufferCount >= 2);
_resourceDatabase = resourceDatabase; _resourceDatabase = resourceDatabase;
_descriptorAllocator = descriptorAllocator; _descriptorAllocator = descriptorAllocator;
@@ -117,7 +117,7 @@ internal unsafe class DXGISwapChain : ISwapChain
var pSfwapChain = CreateSwapChain(device, desc, bufferCount); var pSfwapChain = CreateSwapChain(device, desc, bufferCount);
_swapChain.Attach(pSfwapChain); _swapChain.Attach(pSfwapChain);
_backBuffers = new UnsafeArray<Handle<GPUTexture>>((int)bufferCount, Allocator.Persistent); _backBuffers = new UnsafeArray<Handle<GPUTexture>>((int)bufferCount, AllocationHandle.Persistent);
Width = desc.Width; Width = desc.Width;
Height = desc.Height; Height = desc.Height;
@@ -168,20 +168,20 @@ internal unsafe class DXGISwapChain : ISwapChain
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Handle<GPUTexture> GetCurrentBackBuffer() public Handle<GPUTexture> GetCurrentBackBuffer()
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return _backBuffers[_swapChain.Get()->GetCurrentBackBufferIndex()]; return _backBuffers[_swapChain.Get()->GetCurrentBackBufferIndex()];
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<Handle<GPUTexture>> GetBackBuffers() public ReadOnlySpan<Handle<GPUTexture>> GetBackBuffers()
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return _backBuffers.AsSpan(); return _backBuffers.AsSpan();
} }
public void Present(bool vsync = true) public void Present(bool vsync = true)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
var presentFlags = 0u; var presentFlags = 0u;
var syncInterval = vsync ? 1u : 0u; var syncInterval = vsync ? 1u : 0u;
@@ -192,7 +192,7 @@ internal unsafe class DXGISwapChain : ISwapChain
public void Resize(uint width, uint height) public void Resize(uint width, uint height)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
if (Width == width && Height == height) if (Width == width && Height == height)
{ {
@@ -215,7 +215,7 @@ internal unsafe class DXGISwapChain : ISwapChain
public void SetScale(float scaleX, float scaleY) public void SetScale(float scaleX, float scaleY)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
var inverseScaleX = 1.0f / scaleX; var inverseScaleX = 1.0f / scaleX;
var inverseScaleY = 1.0f / scaleY; var inverseScaleY = 1.0f / scaleY;

View File

@@ -53,9 +53,9 @@ internal static unsafe class D3D12Utility
var ptr = uPtr.Get(); var ptr = uPtr.Get();
if (ptr != null) if (ptr != null)
{ {
Debug.Assert(ptr != other); Logger.DebugAssert(ptr != other);
var refCount = ptr->Release(); var refCount = ptr->Release();
Debug.Assert(refCount == 0); Logger.DebugAssert(refCount == 0);
} }
uPtr = new UniquePtr<T>(other); uPtr = new UniquePtr<T>(other);
@@ -69,7 +69,7 @@ internal static unsafe class D3D12Utility
if (ptr != null) if (ptr != null)
{ {
var refCount = ptr->Release(); var refCount = ptr->Release();
//Debug.Assert(refCount == 0); //Logger.DebugAssert(refCount == 0);
} }
} }

View File

@@ -166,8 +166,6 @@ public struct ResourceRange
} }
public readonly struct ShaderVariant; public readonly struct ShaderVariant;
public readonly struct GraphicsPipeline;
public readonly struct ComputePipeline;
public readonly struct ShaderPass public readonly struct ShaderPass
{ {
@@ -187,11 +185,11 @@ public readonly struct ShaderPass
} }
} }
public readonly struct PassPipelineHash : IEquatable<PassPipelineHash> public readonly struct PassAttachmentHash : IEquatable<PassAttachmentHash>
{ {
public readonly UInt128 value; public readonly UInt128 value;
public PassPipelineHash(ReadOnlySpan<TextureFormat> rtvFormats, TextureFormat dsvFormat) public PassAttachmentHash(ReadOnlySpan<TextureFormat> rtvFormats, TextureFormat dsvFormat)
{ {
if (rtvFormats.Length > 8) if (rtvFormats.Length > 8)
{ {
@@ -211,16 +209,21 @@ public readonly struct PassPipelineHash : IEquatable<PassPipelineHash>
value = new UInt128(rtvPart, (ulong)dsvFormat); value = new UInt128(rtvPart, (ulong)dsvFormat);
} }
public bool Equals(PassPipelineHash other) => value == other.value; public bool Equals(PassAttachmentHash other) => value == other.value;
public override bool Equals(object? obj) => obj is PassPipelineHash other && Equals(other); public override bool Equals(object? obj) => obj is PassAttachmentHash other && Equals(other);
public override int GetHashCode() => value.GetHashCode(); public override int GetHashCode() => value.GetHashCode();
public static bool operator ==(PassPipelineHash left, PassPipelineHash right) => left.Equals(right); public static bool operator ==(PassAttachmentHash left, PassAttachmentHash right) => left.Equals(right);
public static bool operator !=(PassPipelineHash left, PassPipelineHash right) => !(left == 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<ShaderVariant> VariantKey public Key64<ShaderVariant> VariantKey
{ {
get; set; get; set;
@@ -240,14 +243,39 @@ public ref struct GraphicsPSODescriptor
{ {
get; set; get; set;
} }
public ReadOnlySpan<byte> AsCode
{
get; set;
}
public ReadOnlySpan<byte> MsCode
{
get; set;
}
public ReadOnlySpan<byte> PsCode
{
get; set;
}
} }
public ref struct ComputePSODescriptor public ref struct ComputePSODesc
{ {
public UInt128 CompiledHash
{
get; set;
}
public Key64<ShaderVariant> VariantKey public Key64<ShaderVariant> VariantKey
{ {
get; set; get; set;
} }
public ReadOnlySpan<byte> CsCode
{
get; set;
}
} }
public readonly struct CBufferPropertyInfo public readonly struct CBufferPropertyInfo
@@ -640,7 +668,7 @@ public struct BarrierDesc
public record struct ResourceDesc public record struct ResourceDesc
{ {
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Explicit)]
internal struct resource_union internal struct __union
{ {
[FieldOffset(0)] [FieldOffset(0)]
public TextureDesc textureDescription; public TextureDesc textureDescription;
@@ -648,7 +676,7 @@ public record struct ResourceDesc
public BufferDesc bufferDescription; public BufferDesc bufferDescription;
} }
private resource_union _desc; private __union _desc;
public ResourceType Type public ResourceType Type
{ {
@@ -656,21 +684,21 @@ public record struct ResourceDesc
} }
[UnscopedRef] [UnscopedRef]
public ref TextureDesc TextureDescription public ref TextureDesc TextureDescriptor
{ {
get get
{ {
Debug.Assert(Type == ResourceType.Texture); Logger.DebugAssert(Type == ResourceType.Texture);
return ref _desc.textureDescription; return ref _desc.textureDescription;
} }
} }
[UnscopedRef] [UnscopedRef]
public ref BufferDesc BufferDescription public ref BufferDesc BufferDescriptor
{ {
get get
{ {
Debug.Assert(Type == ResourceType.Buffer); Logger.DebugAssert(Type == ResourceType.Buffer);
return ref _desc.bufferDescription; return ref _desc.bufferDescription;
} }
} }
@@ -680,7 +708,7 @@ public record struct ResourceDesc
return new ResourceDesc return new ResourceDesc
{ {
Type = ResourceType.Buffer, Type = ResourceType.Buffer,
BufferDescription = desc BufferDescriptor = desc
}; };
} }
@@ -689,7 +717,7 @@ public record struct ResourceDesc
return new ResourceDesc return new ResourceDesc
{ {
Type = ResourceType.Texture, Type = ResourceType.Texture,
TextureDescription = desc TextureDescriptor = desc
}; };
} }
@@ -702,8 +730,8 @@ public record struct ResourceDesc
return Type switch return Type switch
{ {
ResourceType.Texture => TextureDescription.Equals(other.TextureDescription), ResourceType.Texture => TextureDescriptor.Equals(other.TextureDescriptor),
ResourceType.Buffer => BufferDescription.Equals(other.BufferDescription), ResourceType.Buffer => BufferDescriptor.Equals(other.BufferDescriptor),
_ => throw new InvalidOperationException($"Unknown resource type: {Type}") _ => throw new InvalidOperationException($"Unknown resource type: {Type}")
}; };
} }
@@ -712,8 +740,8 @@ public record struct ResourceDesc
{ {
return Type switch return Type switch
{ {
ResourceType.Texture => HashCode.Combine(Type, TextureDescription), ResourceType.Texture => HashCode.Combine(Type, TextureDescriptor),
ResourceType.Buffer => HashCode.Combine(Type, BufferDescription), ResourceType.Buffer => HashCode.Combine(Type, BufferDescriptor),
_ => throw new InvalidOperationException($"Unknown resource type: {Type}") _ => throw new InvalidOperationException($"Unknown resource type: {Type}")
}; };
} }
@@ -722,8 +750,8 @@ public record struct ResourceDesc
{ {
return Type switch return Type switch
{ {
ResourceType.Texture => $"Texture: {TextureDescription}", ResourceType.Texture => $"Texture: {TextureDescriptor}",
ResourceType.Buffer => $"Buffer: {BufferDescription}", ResourceType.Buffer => $"Buffer: {BufferDescriptor}",
_ => $"Unknown resource type: {Type}" _ => $"Unknown resource type: {Type}"
}; };
} }
@@ -1649,4 +1677,4 @@ public enum SetWorkGraphFlags
{ {
None, None,
Initialize Initialize
} }

View File

@@ -1,4 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Core.Graphics;
namespace Ghost.Graphics.RHI; namespace Ghost.Graphics.RHI;
@@ -97,7 +98,7 @@ public interface ICommandBuffer : IRHIObject
/// Sets the pipeline state object /// Sets the pipeline state object
/// </summary> /// </summary>
/// <param name="pipelineKey">Pipeline state to set</param> /// <param name="pipelineKey">Pipeline state to set</param>
void SetPipelineState(Key128<GraphicsPipeline> pipelineKey); void SetPipelineState(Key128<PipelineState> pipelineKey);
/// <summary> /// <summary>
/// Sets the constant buffer view for the specified slot in the graphics pipeline. /// Sets the constant buffer view for the specified slot in the graphics pipeline.
@@ -216,4 +217,4 @@ public interface ICommandBuffer : IRHIObject
/// <param name="intermediate">A handle to an intermediate GPU resource used to stage the subresource data before copying to the destination resource.</param> /// <param name="intermediate">A handle to an intermediate GPU resource used to stage the subresource data before copying to the destination resource.</param>
/// <param name="subResources">A span containing the data for each subresource to update. Each element represents a subresource and its associated data.</param> /// <param name="subResources">A span containing the data for each subresource to update. Each element represents a subresource and its associated data.</param>
void UpdateSubResources(Handle<GPUResource> resource, Handle<GPUResource> intermediate, params ReadOnlySpan<SubResourceData> subResources); void UpdateSubResources(Handle<GPUResource> resource, Handle<GPUResource> intermediate, params ReadOnlySpan<SubResourceData> subResources);
} }

View File

@@ -1,4 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Core.Graphics;
namespace Ghost.Graphics.RHI; namespace Ghost.Graphics.RHI;
@@ -6,6 +7,6 @@ public interface IPipelineLibrary : IDisposable
{ {
void SaveLibraryToDisk(string filePath); void SaveLibraryToDisk(string filePath);
bool HasPipelineStateObject(UInt128 key); bool HasPipelineStateObject(UInt128 key);
Result<Key128<GraphicsPipeline>> CreateGraphicsPipeline(ref readonly GraphicsPSODescriptor descriptor, ReadOnlySpan<byte> asByteCode, ReadOnlySpan<byte> msByteCode, ReadOnlySpan<byte> psByteCode); Result<Key128<PipelineState>> CreateGraphicsPipeline(ref readonly GraphicsPSODesc desc);
Result<Key128<ComputePipeline>> CreateComputePipeline(ref readonly ComputePSODescriptor descriptor, ReadOnlySpan<byte> csByteCode); Result<Key128<PipelineState>> CreateComputePipeline(ref readonly ComputePSODesc desc);
} }

View File

@@ -39,7 +39,7 @@ public unsafe struct LocalKeywordSet
} }
} }
public ulong GetHash64() public ulong GetHashCode64()
{ {
var hash = 14695981039346656037ul; // FNV Offset basis var hash = 14695981039346656037ul; // FNV Offset basis
@@ -124,4 +124,4 @@ public unsafe struct LocalKeywordSet
return result; return result;
} }
} }

View File

@@ -10,11 +10,11 @@ namespace Ghost.Graphics.RHI;
public static class RHIUtility public static class RHIUtility
{ {
public const int MAX_RENDER_TARGETS = 8; public const int MAX_RENDER_TARGETS = 8;
public const ulong SHADER_ID_MASK = 0xFFFFFFFFFFFFFFF0ul;
public const ulong PIPELINE_KEY_MASK = 0xFFFFFFFFFFFFFFF0ul; public const ulong PIPELINE_KEY_MASK = 0xFFFFFFFFFFFFFFF0ul;
public const ulong GRAPHICS_PIPELINE_KEY_FLAG = 0x1ul; public const ulong GRAPHICS_PIPELINE_KEY_FLAG = 0x1ul;
public const ulong COMPUTE_PIPELINE_KEY_FLAG = 0x2ul; public const ulong COMPUTE_PIPELINE_KEY_FLAG = 0x2ul;
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint GetBytesPerPixel(this TextureFormat format) 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Key64<ShaderPass> CreateShaderPassKey(ulong passID, ulong compiledHash) public static Key64<ShaderPass> CreateShaderPassKey(ulong passID, ulong compiledHash)
{ {
@@ -158,11 +172,11 @@ public static class RHIUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Key64<ShaderVariant> CreateShaderVariantKey(ulong passKey, ref readonly LocalKeywordSet keywords) public static Key64<ShaderVariant> CreateShaderVariantKey(ulong passKey, ref readonly LocalKeywordSet keywords)
{ {
var keywordHash = keywords.GetHash64(); var keywordHash = keywords.GetHashCode64();
return new Key64<ShaderVariant>(Hash.Combine64(passKey, keywordHash)); return new Key64<ShaderVariant>(Hash.Combine64(passKey, keywordHash));
} }
public static unsafe Key128<GraphicsPipeline> CreateGraphicsPipelineKey(Key64<ShaderVariant> shaderVariantKey, PipelineState pipelineState, PassPipelineHash passKey) public static unsafe Key128<PipelineState> CreateGraphicsPipelineKey(ulong compiledHash, PipelineState pipelineState, PassAttachmentHash passAttachmentHash)
{ {
// Order-sensitive 128-bit mix. Cheap and stable, avoids span hashing. // Order-sensitive 128-bit mix. Cheap and stable, avoids span hashing.
static ulong Mix64(ulong x) static ulong Mix64(ulong x)
@@ -175,10 +189,10 @@ public static class RHIUtility
return x; return x;
} }
var mLo = shaderVariantKey.Value; var mLo = compiledHash;
var mHi = pipelineState.GetHashCode64(); var mHi = pipelineState.GetHashCode64();
var pPasskey = (ulong*)&passKey.value; var pPasskey = (ulong*)&passAttachmentHash.value;
var pLo = pPasskey[0]; var pLo = pPasskey[0];
var pHi = pPasskey[1]; 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. lo = lo & PIPELINE_KEY_MASK | GRAPHICS_PIPELINE_KEY_FLAG; // Ensure graphics pipeline keys are distinguishable from compute pipeline keys.
return new Key128<GraphicsPipeline>(new UInt128(hi, lo)); return new Key128<PipelineState>(new UInt128(hi, lo));
} }
public static Key128<ComputePipeline> CreateComputePipelineKey(Key64<ShaderVariant> shaderVariantKey) public static Key128<PipelineState> CreateComputePipelineKey(ulong compiledHash)
{ {
var shaderHash = shaderVariantKey.Value; // Since compute shader don't have blend state or attachment configurations, we can afford a simpler key generation.
var stateHash = ~shaderVariantKey.Value; // Just use the compiled hash with a distinct flag to avoid collisions with graphics pipeline keys.
#if true
return new Key128<PipelineState>(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. // 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 hi = shaderHash ^ (stateHash + 0x9E3779B97F4A7C15ul) ^ (shaderHash * 0xD6E8FEB86659FD93ul);
var lo = stateHash ^ (shaderHash + 0xC2B2AE3D27D4EB4Ful) ^ (stateHash * 0x165667B19E3779F9ul); 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. lo = lo & PIPELINE_KEY_MASK | COMPUTE_PIPELINE_KEY_FLAG; // Ensure compute pipeline keys are distinguishable from graphics pipeline keys.
return new Key128<ComputePipeline>(new UInt128(hi, lo)); return new Key128<ComputePipeline>(new UInt128(hi, lo));
#endif
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -208,4 +228,4 @@ public static class RHIUtility
{ {
return key.TryFormat(destination, out var _, "X16"); return key.TryFormat(destination, out var _, "X16");
} }
} }

View File

@@ -14,7 +14,7 @@ public static class RootSignatureLayout
public struct PushConstantsData public struct PushConstantsData
{ {
public const uint NUM_32BITS_VALUE = 12u / sizeof(uint); public const uint NUM_32BITS_VALUE = 12u / sizeof(uint);
[FieldOffset(0)] [FieldOffset(0)]
public uint frameBuffer; public uint frameBuffer;
[FieldOffset(4)] [FieldOffset(4)]
@@ -23,6 +23,11 @@ public struct PushConstantsData
public uint instanceIndex; public uint instanceIndex;
[FieldOffset(8)] [FieldOffset(8)]
public uint propertyBuffer; public uint propertyBuffer;
public ReadOnlySpan<uint> AsUInts()
{
return MemoryMarshal.CreateReadOnlySpan(ref frameBuffer, (int)NUM_32BITS_VALUE);
}
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]

View File

@@ -24,7 +24,7 @@ internal struct CBufferCache : IResourceReleasable
public CBufferCache(Handle<GPUBuffer> buffer, uint bufferSize) public CBufferCache(Handle<GPUBuffer> buffer, uint bufferSize)
{ {
_size = bufferSize; _size = bufferSize;
_cpuData = new UnsafeArray<byte>((int)bufferSize, Allocator.Persistent); _cpuData = new UnsafeArray<byte>((int)bufferSize, AllocationHandle.Persistent);
_gpuResource = buffer; _gpuResource = buffer;
} }
@@ -93,7 +93,7 @@ public struct Material : IResourceReleasable
{ {
if (!_passPipelineOverride.IsCreated) if (!_passPipelineOverride.IsCreated)
{ {
_passPipelineOverride = new UnsafeArray<PipelineOverride>(shader.PassCount, Allocator.Persistent); _passPipelineOverride = new UnsafeArray<PipelineOverride>(shader.PassCount, AllocationHandle.Persistent);
} }
else else
{ {
@@ -104,7 +104,7 @@ public struct Material : IResourceReleasable
_keywordMask.Clear(); _keywordMask.Clear();
for (var i = 0; i < shader.PassCount; i++) 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 _passPipelineOverride[i] = new PipelineOverride
{ {
shaderPass = pass.Key, shaderPass = pass.Key,

View File

@@ -50,8 +50,8 @@ internal sealed class MaterialPaletteStore : IDisposable
initialCapacity = 16; initialCapacity = 16;
} }
_entries = new UnsafeList<Entry>(initialCapacity + 1, Allocator.Persistent); _entries = new UnsafeList<Entry>(initialCapacity + 1, AllocationHandle.Persistent);
_lookup = new UnsafeHashMap<ulong, int>(initialCapacity * 2, Allocator.Persistent); _lookup = new UnsafeHashMap<ulong, int>(initialCapacity * 2, AllocationHandle.Persistent);
_freeListHead = 0; _freeListHead = 0;
} }
@@ -131,7 +131,7 @@ internal sealed class MaterialPaletteStore : IDisposable
if (!newEntry.materials.IsCreated) if (!newEntry.materials.IsCreated)
{ {
newEntry.materials = new UnsafeList<Handle<Material>>(materials.Length, Allocator.Persistent); newEntry.materials = new UnsafeList<Handle<Material>>(materials.Length, AllocationHandle.Persistent);
} }
else else
{ {

View File

@@ -186,8 +186,8 @@ public struct Mesh : IResourceReleasable
internal Mesh(ReadOnlySpan<Vertex> vertices, ReadOnlySpan<uint> indices, Handle<GPUBuffer> vertexBuffer, Handle<GPUBuffer> indexBuffer) internal Mesh(ReadOnlySpan<Vertex> vertices, ReadOnlySpan<uint> indices, Handle<GPUBuffer> vertexBuffer, Handle<GPUBuffer> indexBuffer)
{ {
Vertices = new UnsafeList<Vertex>(vertices.Length, Allocator.Persistent); Vertices = new UnsafeList<Vertex>(vertices.Length, AllocationHandle.Persistent);
Indices = new UnsafeList<uint>(indices.Length, Allocator.Persistent); Indices = new UnsafeList<uint>(indices.Length, AllocationHandle.Persistent);
Vertices.CopyFrom(vertices); Vertices.CopyFrom(vertices);
Indices.CopyFrom(indices); Indices.CopyFrom(indices);
VertexBuffer = vertexBuffer; VertexBuffer = vertexBuffer;
@@ -298,10 +298,10 @@ public struct Mesh : IResourceReleasable
ref var data = ref mesh->_meshletData; ref var data = ref mesh->_meshletData;
// Ensure lists are initialized // Ensure lists are initialized
if (!data.groups.IsCreated) data.groups = new UnsafeList<MeshletGroup>(16, Allocator.Persistent); if (!data.groups.IsCreated) data.groups = new UnsafeList<MeshletGroup>(16, AllocationHandle.Persistent);
if (!data.meshlets.IsCreated) data.meshlets = new UnsafeList<Meshlet>(64, Allocator.Persistent); if (!data.meshlets.IsCreated) data.meshlets = new UnsafeList<Meshlet>(64, AllocationHandle.Persistent);
if (!data.meshletVertices.IsCreated) data.meshletVertices = new UnsafeList<uint>(128, Allocator.Persistent); if (!data.meshletVertices.IsCreated) data.meshletVertices = new UnsafeList<uint>(128, AllocationHandle.Persistent);
if (!data.meshletTriangles.IsCreated) data.meshletTriangles = new UnsafeList<uint>(128, Allocator.Persistent); if (!data.meshletTriangles.IsCreated) data.meshletTriangles = new UnsafeList<uint>(128, AllocationHandle.Persistent);
var meshletGroup = new MeshletGroup var meshletGroup = new MeshletGroup
{ {

View File

@@ -4,7 +4,8 @@ using Ghost.Graphics.Services;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
using System.Diagnostics; using Misaki.HighPerformance.Mathematics;
using System.Runtime.InteropServices;
namespace Ghost.Graphics.Core; namespace Ghost.Graphics.Core;
@@ -12,21 +13,23 @@ namespace Ghost.Graphics.Core;
public readonly unsafe ref struct RenderContext public readonly unsafe ref struct RenderContext
{ {
private readonly ResourceManager _resourceManager; private readonly ResourceManager _resourceManager;
private readonly ShaderLibrary _shaderLibrary;
private readonly IGraphicsEngine _engine; 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 ResourceManager ResourceManager => _resourceManager;
public IResourceAllocator ResourceAllocator => _engine.ResourceAllocator; public IResourceAllocator ResourceAllocator => _engine.ResourceAllocator;
public IResourceDatabase ResourceDatabase => _engine.ResourceDatabase; public IResourceDatabase ResourceDatabase => _engine.ResourceDatabase;
public IPipelineLibrary PipelineLibrary => _engine.PipelineLibrary; 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; _resourceManager = resourceManager;
_shaderLibrary = shaderLibrary;
_engine = engine; _engine = engine;
_cmd = cmd; _commandBuffer = cmd;
} }
private void TransitionBarrier(Handle<GPUResource> resource, bool isTexture, BarrierLayout newLayout, BarrierAccess newAccess, BarrierSync newSync) private void TransitionBarrier(Handle<GPUResource> 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); desc = BarrierDesc.Buffer(resource, newSync, newAccess);
} }
_cmd.Barrier(desc); _commandBuffer.Barrier(desc);
} }
public void UploadBuffer<T>(Handle<GPUBuffer> buffer, params ReadOnlySpan<T> data) public void UploadBuffer<T>(Handle<GPUBuffer> buffer, params ReadOnlySpan<T> data)
@@ -53,10 +56,10 @@ public readonly unsafe ref struct RenderContext
return; return;
} }
Debug.Assert(r.Value.Type == ResourceType.Buffer); Logger.DebugAssert(r.Value.Type == ResourceType.Buffer);
var sizeInBytes = (nuint)(data.Length * sizeof(T)); var sizeInBytes = (nuint)(data.Length * sizeof(T));
var memoryType = r.Value.BufferDescription.HeapType; var memoryType = r.Value.BufferDescriptor.HeapType;
if (memoryType == HeapType.Upload) if (memoryType == HeapType.Upload)
{ {
@@ -89,7 +92,7 @@ public readonly unsafe ref struct RenderContext
_engine.ResourceDatabase.UnmapResource(uploadHandle.AsResource(), 0, null); _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<Mesh> CreateMesh(ReadOnlySpan<Vertex> vertices, ReadOnlySpan<uint> indices, bool staticMesh) public Handle<Mesh> CreateMesh(ReadOnlySpan<Vertex> vertices, ReadOnlySpan<uint> indices, bool staticMesh)
{ {
var vertexList = new UnsafeList<Vertex>(vertices.Length, Allocator.Persistent); var vertexList = new UnsafeList<Vertex>(vertices.Length, AllocationHandle.Persistent);
var indexList = new UnsafeList<uint>(indices.Length, Allocator.Persistent); var indexList = new UnsafeList<uint>(indices.Length, AllocationHandle.Persistent);
vertexList.CopyFrom(vertices); vertexList.CopyFrom(vertices);
indexList.CopyFrom(indices); indexList.CopyFrom(indices);
@@ -262,7 +265,7 @@ public readonly unsafe ref struct RenderContext
where T : unmanaged where T : unmanaged
{ {
var desc = ResourceDatabase.GetResourceDescription(texture.AsResource()).GetValueOrThrow(); 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 requiredSize = ResourceDatabase.GetIntermediateResourceSize(texture.AsResource(), 0, 1);
var uploadDesc = new BufferDesc var uploadDesc = new BufferDesc
@@ -289,7 +292,89 @@ public readonly unsafe ref struct RenderContext
slicePitch = slicePitch slicePitch = slicePitch
}; };
_cmd.UpdateSubResources(texture.AsResource(), uploadHandle.AsResource(), subresourceData); _commandBuffer.UpdateSubResources(texture.AsResource(), uploadHandle.AsResource(), subresourceData);
} }
} }
public void DispatchCompute<T>(Handle<ComputeShader> 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<T>(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);
}
} }

View File

@@ -18,21 +18,21 @@ public enum GateFit : uint
public struct Frustum public struct Frustum
{ {
[InlineArray(6)] [InlineArray(6)]
public struct plane_array public struct __plane_array
{ {
private float4 plane; private float4 plane;
} }
[InlineArray(8)] [InlineArray(8)]
public struct corner_array public struct __corner_array
{ {
private float3 corner; private float3 corner;
} }
public plane_array planes; public __plane_array planes;
public corner_array corners; 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 planeFrustumLeft = 0;
const int planeFrustumRight = 1; const int planeFrustumRight = 1;

View File

@@ -3,7 +3,6 @@ using Ghost.Core.Graphics;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -16,9 +15,6 @@ public partial struct Shader
private static readonly Dictionary<string, int> s_passNameToID = new Dictionary<string, int>(); private static readonly Dictionary<string, int> s_passNameToID = new Dictionary<string, int>();
private static int s_nextPassID = 0; private static int s_nextPassID = 0;
private static readonly Dictionary<string, int> s_propertyNameToID = new Dictionary<string, int>();
private static int s_nextPropertyID = 0;
private static readonly Dictionary<string, int> s_keywordNameToID = new Dictionary<string, int>(); private static readonly Dictionary<string, int> s_keywordNameToID = new Dictionary<string, int>();
private static readonly Dictionary<int, string> s_keywordIDToName = new Dictionary<int, string>(); private static readonly Dictionary<int, string> s_keywordIDToName = new Dictionary<int, string>();
private static int s_nextKeywordID = 0; private static int s_nextKeywordID = 0;
@@ -34,17 +30,6 @@ public partial struct Shader
return id; return id;
} }
public static Identifier<ShaderProperty> 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) public static int GetKeywordID(string keywordName)
{ {
ref var id = ref CollectionsMarshal.GetValueRefOrAddDefault(s_keywordNameToID, keywordName, out var exists); ref var id = ref CollectionsMarshal.GetValueRefOrAddDefault(s_keywordNameToID, keywordName, out var exists);
@@ -75,6 +60,7 @@ public partial struct Shader
/// </summary> /// </summary>
public partial struct Shader : IResourceReleasable public partial struct Shader : IResourceReleasable
{ {
private readonly ulong _nameHash;
private readonly uint _propertyBufferSize; private readonly uint _propertyBufferSize;
private UnsafeArray<ShaderPass> _shaderPasses; private UnsafeArray<ShaderPass> _shaderPasses;
private UnsafeHashMap<int, int> _passIDToLocal; private UnsafeHashMap<int, int> _passIDToLocal;
@@ -83,15 +69,17 @@ public partial struct Shader : IResourceReleasable
// TODO: Tag to pass index for fast lookup. // 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. // 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 int PassCount => _shaderPasses.Count;
public readonly uint PropertyBufferSize => _propertyBufferSize; public readonly uint PropertyBufferSize => _propertyBufferSize;
internal Shader(GraphicsShaderDescriptor descriptor) internal Shader(GraphicsShaderDescriptor descriptor)
{ {
_nameHash = RHIUtility.GetShaderID(descriptor.name);
_propertyBufferSize = descriptor.propertyBufferSize; _propertyBufferSize = descriptor.propertyBufferSize;
_shaderPasses = new UnsafeArray<ShaderPass>(descriptor.passes.Length, Allocator.Persistent); _shaderPasses = new UnsafeArray<ShaderPass>(descriptor.passes.Length, AllocationHandle.Persistent);
_passIDToLocal = new UnsafeHashMap<int, int>(descriptor.passes.Length, Allocator.Persistent); _passIDToLocal = new UnsafeHashMap<int, int>(descriptor.passes.Length, AllocationHandle.Persistent);
_keywordIDToLocal = new UnsafeHashMap<int, int>(32, Allocator.Persistent); _keywordIDToLocal = new UnsafeHashMap<int, int>(32, AllocationHandle.Persistent);
for (var i = 0; i < descriptor.passes.Length; i++) for (var i = 0; i < descriptor.passes.Length; i++)
{ {
@@ -129,7 +117,7 @@ public partial struct Shader : IResourceReleasable
_shaderPasses[i] = new ShaderPass _shaderPasses[i] = new ShaderPass
{ {
Key = pass.identifier, Key = RHIUtility.GetPassID(_nameHash, i),
DefaultState = pass.localPipeline, DefaultState = pass.localPipeline,
KeywordIDs = keywords, KeywordIDs = keywords,
}; };
@@ -172,7 +160,7 @@ public partial struct Shader : IResourceReleasable
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref ShaderPass GetPassReference(int index) public ref readonly ShaderPass GetPassReference(int index)
{ {
return ref _shaderPasses[index]; return ref _shaderPasses[index];
} }
@@ -200,25 +188,26 @@ public partial struct Shader : IResourceReleasable
public unsafe partial struct ComputeShader : IResourceReleasable public unsafe partial struct ComputeShader : IResourceReleasable
{ {
private fixed ulong _entryHash[8]; private readonly ulong _nameHash;
private readonly int _entryPointCount; private fixed ulong entryHashes[8]; // Support up to 8 entry points for now, can be extended if needed.
private readonly uint _propertyBufferSize; private readonly uint _propertyBufferSize;
private LocalKeywordSet _localKeywordSet; private LocalKeywordSet _localKeywordSet;
private UnsafeHashMap<int, int> _keywordIDToLocal; private UnsafeHashMap<int, int> _keywordIDToLocal;
public readonly ulong UniqueID => _nameHash;
public readonly uint PropertyBufferSize => _propertyBufferSize; public readonly uint PropertyBufferSize => _propertyBufferSize;
internal ComputeShader(ComputeShaderDescriptor descriptor) internal ComputeShader(ComputeShaderDescriptor descriptor)
{ {
_nameHash = RHIUtility.GetShaderID(descriptor.name);
_propertyBufferSize = descriptor.propertyBufferSize; _propertyBufferSize = descriptor.propertyBufferSize;
_entryPointCount = descriptor.shaderCodes.Length;
_keywordIDToLocal = new UnsafeHashMap<int, int>(32, Allocator.Persistent); _keywordIDToLocal = new UnsafeHashMap<int, int>(32, AllocationHandle.Persistent);
for (var i = 0; i < descriptor.shaderCodes.Length; i++) for (var i = 0; i < descriptor.shaderCodes.Length; i++)
{ {
_entryHash[i] = descriptor.shaderCodes[i].GetHashCode64(); entryHashes[i] = RHIUtility.GetPassID(_nameHash, i);
} }
var localKeywordIndex = 0; 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); Logger.DebugAssert(entryIndex >= 0 && entryIndex < 8, "Entry index out of bounds.");
return _entryHash[entryPointIndex]; return entryHashes[entryIndex];
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -17,10 +17,6 @@
<IsTrimmable>True</IsTrimmable> <IsTrimmable>True</IsTrimmable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Include="TestCompute.gcomp" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Misaki.HighPerformance.Analyzer" Version="1.1.0"> <PackageReference Include="Misaki.HighPerformance.Analyzer" Version="1.1.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View File

@@ -1,7 +1,6 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using System.Diagnostics; using System.Diagnostics;
using Ghost.Graphics.Services;
namespace Ghost.Graphics.RenderGraphModule; namespace Ghost.Graphics.RenderGraphModule;
@@ -114,7 +113,7 @@ public sealed class RenderGraph : IDisposable
} }
var desc = r.Value; 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);
} }
/// <summary> /// <summary>
@@ -132,7 +131,7 @@ public sealed class RenderGraph : IDisposable
} }
var desc = r.Value; var desc = r.Value;
return _resources.ImportBuffer(in desc.BufferDescription, buffer, name); return _resources.ImportBuffer(in desc.BufferDescriptor, buffer, name);
} }
public IRasterRenderGraphBuilder AddRasterRenderPass<TPassData>(string name, out TPassData passData) public IRasterRenderGraphBuilder AddRasterRenderPass<TPassData>(string name, out TPassData passData)

View File

@@ -1,3 +1,4 @@
using Ghost.Core;
using Ghost.Core.Utilities; using Ghost.Core.Utilities;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using System.Diagnostics; using System.Diagnostics;
@@ -359,7 +360,7 @@ internal sealed class ResourceAliasingManager
logicalIndex, logicalIndex,
alignment); 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 // Get peak usage from simulation

View File

@@ -279,7 +279,7 @@ internal class RenderGraphBuilder : IRasterRenderGraphBuilder, IComputeRenderGra
{ {
ThrowIfDisposed(); 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); var id = UseTexture(texture, flags);
if (_pass.colorAccess[index].id == id || _pass.colorAccess[index].id.IsInvalid) if (_pass.colorAccess[index].id == id || _pass.colorAccess[index].id.IsInvalid)

View File

@@ -2,6 +2,7 @@ using Ghost.Core;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Ghost.Graphics.Services; using Ghost.Graphics.Services;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
namespace Ghost.Graphics.RenderGraphModule; namespace Ghost.Graphics.RenderGraphModule;
@@ -30,14 +31,16 @@ public interface IRasterRenderContext : IRenderGraphContext
void SetActiveMaterial(Handle<Material> material); void SetActiveMaterial(Handle<Material> material);
void SetActiveMaterial(ref readonly Material material); void SetActiveMaterial(ref readonly Material material);
[Obsolete("No point to set active mesh anymore in gpu driven pipeline.")]
void SetActiveMesh(Handle<Mesh> mesh); void SetActiveMesh(Handle<Mesh> mesh);
[Obsolete("No point to set active mesh anymore in gpu driven pipeline.")]
void SetActiveMesh(ref readonly Mesh mesh); void SetActiveMesh(ref readonly Mesh mesh);
void DispatchMesh(uint3 threadGroupCount); void DispatchMesh(uint3 threadGroupCount);
} }
public interface IComputeRenderContext : IRenderGraphContext public interface IComputeRenderContext : IRenderGraphContext
{ {
void SetActiveCompute(Handle<ComputeShader> computeShader, uint entryIndex); void SetActiveCompute(Handle<ComputeShader> computeShader, int entryIndex);
void DispatchCompute(uint3 threadGroupCount); void DispatchCompute(uint3 threadGroupCount);
} }
@@ -160,35 +163,58 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
} }
ref var shader = ref shaderResult.Value; 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); var materialPipeline = material.GetPassPipelineOverride(material.ActivePassIndex);
// Mask out the keywords that are not used in this pass. // Mask out the keywords that are not used in this pass.
var variantMask = material._keywordMask & pass.KeywordIDs; var variantMask = material._keywordMask & pass.KeywordIDs;
var shaderVariantKey = RHIUtility.CreateShaderVariantKey(pass.Key, in variantMask); var variantKey = RHIUtility.CreateShaderVariantKey(pass.Key, in variantMask);
var pipelineKey = RHIUtility.CreateGraphicsPipelineKey(shaderVariantKey, materialPipeline, passPipelineHash);
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)) 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) if (compiledCacheResult.IsFailure)
{ {
throw new InvalidOperationException("Failed to load compiled shader cache for pipeline state object creation."); 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, PipelineOption = materialPipeline,
RtvFormats = _rtvFormats.AsSpan(0, _rtvCount), RtvFormats = _rtvFormats.AsSpan(0, _rtvCount),
DsvFormat = _dsvFormat, DsvFormat = _dsvFormat,
AsCode = asByteCode,
MsCode = msByteCode,
PsCode = psByteCode,
}; };
var compiled = compiledCacheResult.Value; _pipelineLibrary.CreateGraphicsPipeline(in psoDes).GetValueOrThrow();
_pipelineLibrary.CreateGraphicsPipeline(in psoDes, in compiled).GetValueOrThrow();
} }
_activePerMaterialData = material._cBufferCache.GpuResource; _activePerMaterialData = material._cBufferCache.GpuResource;
@@ -238,7 +264,7 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
_commandBuffer.DispatchMesh(threadGroupCount.x, threadGroupCount.y, threadGroupCount.z); _commandBuffer.DispatchMesh(threadGroupCount.x, threadGroupCount.y, threadGroupCount.z);
} }
public void SetActiveCompute(Handle<ComputeShader> computeShader, uint entryIndex) public void SetActiveCompute(Handle<ComputeShader> computeShader, int entryIndex)
{ {
var r = _resourceManager.GetComputeShaderReference(computeShader); var r = _resourceManager.GetComputeShaderReference(computeShader);
if (r.IsFailure) if (r.IsFailure)
@@ -247,30 +273,50 @@ internal sealed class RenderGraphContext : IUnsafeRenderContext
} }
ref var shader = ref r.Value; 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 keywordSet = new LocalKeywordSet(); // TODO: Support keywords in compute shader.
var variantKey = RHIUtility.CreateShaderVariantKey(entryHash, in keywordSet); 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)) 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) if (compiledCacheResult.IsFailure)
{ {
throw new InvalidOperationException("Failed to load compiled shader cache for pipeline state object creation."); 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, 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) public void DispatchCompute(uint3 threadGroupCount)
{ {
throw new NotImplementedException();
} }
public ICommandBuffer GetCommandBufferUnsafe() public ICommandBuffer GetCommandBufferUnsafe()

View File

@@ -26,8 +26,8 @@ internal sealed class RenderGraphExecutor
private void SetViewport(ReadOnlySpan<RenderTargetInfo> color, DepthStencilInfo depthStencil) private void SetViewport(ReadOnlySpan<RenderTargetInfo> 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. // 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.
Debug.Assert(color.Length > 0 || depthStencil.texture.IsValid); Logger.DebugAssert(color.Length > 0 || depthStencil.texture.IsValid);
ViewportDesc viewportDesc = default; ViewportDesc viewportDesc = default;
ScissorRectDesc scissorDesc = default; ScissorRectDesc scissorDesc = default;
@@ -178,7 +178,7 @@ internal sealed class RenderGraphExecutor
else 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. // 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 // Compute pass or Unsafe pass
var e = ExecuteBarriersForPass(commandBuffer, logicalPassIndex, ref barrierIndex, compiledBarriers); var e = ExecuteBarriersForPass(commandBuffer, logicalPassIndex, ref barrierIndex, compiledBarriers);

View File

@@ -1,6 +1,7 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
namespace Ghost.Graphics; namespace Ghost.Graphics;
@@ -24,107 +25,115 @@ public interface IRenderPipeline : IDisposable
void Render(RenderContext ctx, int frameIndex, IRenderPayload payload); 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<GPUTexture> _colorTexture;
private readonly uint2 _screenSize;
public readonly ref readonly RenderRequest Request => ref _request;
public readonly Handle<GPUTexture> ColorTexture => _colorTexture;
public readonly uint2 ScreenSize => _screenSize;
public RenderViewData(SwapChainManager swapChainManager, IResourceDatabase resourceDatabase, ref readonly RenderRequest request)
{ {
Handle<GPUTexture> rtHandle; _request = ref request;
_swapChainManager = swapChainManager;
if (request.swapChainIndex < 0) 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 else
{ {
view = default; throw new InvalidOperationException($"Invalid swap chain index: {request.swapChainIndex}");
projection = default;
screenSize = default;
return false;
} }
try var (desc, error) = resourceDatabase.GetResourceDescription(_colorTexture.AsResource());
if (error.IsFailure)
{ {
var rtResult = renderSystem.GraphicsEngine.ResourceDatabase.GetResourceDescription(rtHandle.AsResource()); throw new InvalidOperationException($"Failed to get resource description for color target texture. Error: {error}");
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;
} }
finally
_screenSize = new uint2(desc.TextureDescriptor.Width, desc.TextureDescriptor.Height);
}
public void Dispose()
{
if (_request.swapChainIndex >= 0)
{ {
if (request.swapChainIndex >= 0) _swapChainManager.ReleaseSwapChain(_request.swapChainIndex);
{
renderSystem.SwapChainManager.ReleaseSwapChain(request.swapChainIndex);
}
} }
} }
} }
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
);
}
}

View File

@@ -124,8 +124,8 @@ public class RenderSystem : IDisposable
get => _renderPipelineSettings; get => _renderPipelineSettings;
set set
{ {
Debug.Assert(value != null, "RenderPipelineSettings cannot be set to null."); Logger.DebugAssert(value != null, "RenderPipelineSettings cannot be set to null.");
Debug.Assert(!_disposed, "Cannot set RenderPipelineSettings on a disposed RenderSystem."); Logger.DebugAssert(!_disposed, "Cannot set RenderPipelineSettings on a disposed RenderSystem.");
if (value == _renderPipelineSettings) if (value == _renderPipelineSettings)
{ {
@@ -229,7 +229,7 @@ public class RenderSystem : IDisposable
#if DEBUG #if DEBUG
Debugger.Break(); Debugger.Break();
#endif #endif
Logger.LogError($"Render failed: {result.Message}"); Logger.Error($"Render failed: {result.Message}");
} }
var waitHandles = new WaitHandle[] { null!, _shutdownEvent }; var waitHandles = new WaitHandle[] { null!, _shutdownEvent };
@@ -295,7 +295,7 @@ public class RenderSystem : IDisposable
{ {
cmd.Begin(frameResource.CommandAllocator); 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); _renderPipeline.Render(ctx, frameIndex, frameResource.RenderPayload);
_swapChainManager.TransitionToPresent(cmd); _swapChainManager.TransitionToPresent(cmd);
@@ -337,7 +337,7 @@ public class RenderSystem : IDisposable
internal void Start() internal void Start()
{ {
Debug.Assert(!_disposed, "Cannot start a disposed RenderSystem."); Logger.DebugAssert(!_disposed, "Cannot start a disposed RenderSystem.");
if (_isRunning) if (_isRunning)
{ {
@@ -350,7 +350,7 @@ public class RenderSystem : IDisposable
internal void Stop() internal void Stop()
{ {
Debug.Assert(!_disposed, "Cannot stop a disposed RenderSystem."); Logger.DebugAssert(!_disposed, "Cannot stop a disposed RenderSystem.");
if (!_isRunning) if (!_isRunning)
{ {
@@ -364,7 +364,7 @@ public class RenderSystem : IDisposable
internal void SignalCPUReady() 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); var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
ref var frameResource = ref _frameResources[eventIndex]; ref var frameResource = ref _frameResources[eventIndex];
@@ -375,13 +375,13 @@ public class RenderSystem : IDisposable
internal void RequestSwapChainResize(ISwapChain swapChain, uint2 newSize) 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); _resizeRequest.AddOrUpdate(swapChain, newSize, (_, _) => newSize);
} }
internal bool TryAcquireCPUFrame() 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; var requiredGpuFence = _cpuFenceValue < _config.FrameBufferCount ? 0 : _cpuFenceValue - _config.FrameBufferCount + 1;
@@ -398,7 +398,7 @@ public class RenderSystem : IDisposable
public bool WaitForGPUReady(int timeOut = -1) 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); var submittedFenceValue = Volatile.Read(ref _submittedFenceValue);
if (submittedFenceValue == 0) if (submittedFenceValue == 0)
@@ -412,7 +412,7 @@ public class RenderSystem : IDisposable
public void WaitIdle() 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) foreach (var frameResource in _frameResources)
{ {
if (frameResource.FenceValue > 0) if (frameResource.FenceValue > 0)
@@ -424,7 +424,7 @@ public class RenderSystem : IDisposable
public IRenderPayload GetCurrentFramePayload() 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); var eventIndex = (int)(_cpuFenceValue % _config.FrameBufferCount);
ref var frameResource = ref _frameResources[eventIndex]; ref var frameResource = ref _frameResources[eventIndex];

View File

@@ -27,11 +27,11 @@ public partial class ResourceManager
public ulong retireFrame; public ulong retireFrame;
} }
private UnsafeList<Page> _activePages = new UnsafeList<Page>(4, Allocator.Persistent); private UnsafeList<Page> _activePages = new UnsafeList<Page>(4, AllocationHandle.Persistent);
private UnsafeQueue<Page> _freePages = new UnsafeQueue<Page>(4, Allocator.Persistent); private UnsafeQueue<Page> _freePages = new UnsafeQueue<Page>(4, AllocationHandle.Persistent);
private UnsafeQueue<RetiringPage> _retiringPages = new UnsafeQueue<RetiringPage>(4, Allocator.Persistent); private UnsafeQueue<RetiringPage> _retiringPages = new UnsafeQueue<RetiringPage>(4, AllocationHandle.Persistent);
private UnsafeList<Handle<GPUResource>> _frameTransientResources = new UnsafeList<Handle<GPUResource>>(4, Allocator.Persistent); private UnsafeList<Handle<GPUResource>> _frameTransientResources = new UnsafeList<Handle<GPUResource>>(4, AllocationHandle.Persistent);
private static bool IsHeapFlagsCompatible(HeapFlags pageHeapFlags, HeapFlags requiredHeapFlags) private static bool IsHeapFlagsCompatible(HeapFlags pageHeapFlags, HeapFlags requiredHeapFlags)
{ {

View File

@@ -44,10 +44,10 @@ public sealed partial class ResourceManager : IDisposable
_resourceAllocator = resourceAllocator; _resourceAllocator = resourceAllocator;
_resourceDatabase = resourceDatabase; _resourceDatabase = resourceDatabase;
_meshes = new UnsafeSlotMap<Mesh>(64, Allocator.Persistent); _meshes = new UnsafeSlotMap<Mesh>(64, AllocationHandle.Persistent);
_materials = new UnsafeSlotMap<Material>(64, Allocator.Persistent); _materials = new UnsafeSlotMap<Material>(64, AllocationHandle.Persistent);
_shaders = new UnsafeSlotMap<Shader>(16, Allocator.Persistent); _shaders = new UnsafeSlotMap<Shader>(16, AllocationHandle.Persistent);
_computeShaders = new UnsafeSlotMap<ComputeShader>(16, Allocator.Persistent); _computeShaders = new UnsafeSlotMap<ComputeShader>(16, AllocationHandle.Persistent);
_materialPalettes = new MaterialPaletteStore(); _materialPalettes = new MaterialPaletteStore();
} }
@@ -59,25 +59,25 @@ public sealed partial class ResourceManager : IDisposable
internal void BeginFrame(ulong submittedFrame) internal void BeginFrame(ulong submittedFrame)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
_submittedFrame = submittedFrame; _submittedFrame = submittedFrame;
} }
internal void EndFrame(ulong completedFrame) internal void EndFrame(ulong completedFrame)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
EndFramePool(completedFrame); EndFramePool(completedFrame);
} }
public void EnterParallelRead() public void EnterParallelRead()
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
Volatile.Write(ref _writeLock, 1); Volatile.Write(ref _writeLock, 1);
} }
public void ExitParallelRead() public void ExitParallelRead()
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
Volatile.Write(ref _writeLock, 0); Volatile.Write(ref _writeLock, 0);
} }
@@ -89,7 +89,7 @@ public sealed partial class ResourceManager : IDisposable
/// <returns>An <see cref="Identifier{Mesh}"/> representing the newly created mesh.</returns> /// <returns>An <see cref="Identifier{Mesh}"/> representing the newly created mesh.</returns>
public unsafe Handle<Mesh> CreateMesh(UnsafeList<Vertex> vertices, UnsafeList<uint> indices) public unsafe Handle<Mesh> CreateMesh(UnsafeList<Vertex> vertices, UnsafeList<uint> indices)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
var spinner = new SpinWait(); var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0) while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
@@ -152,7 +152,7 @@ public sealed partial class ResourceManager : IDisposable
/// <returns>An <see cref="Handle{Material}"/> representing the newly created material.</returns> /// <returns>An <see cref="Handle{Material}"/> representing the newly created material.</returns>
public Handle<Material> CreateMaterial(Handle<Shader> shader) public Handle<Material> CreateMaterial(Handle<Shader> shader)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
var spinner = new SpinWait(); var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0) while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
@@ -184,7 +184,7 @@ public sealed partial class ResourceManager : IDisposable
/// <param name="descriptor">The viewGroup containing the shader's properties and passes.</param> /// <param name="descriptor">The viewGroup containing the shader's properties and passes.</param>
public Handle<Shader> CreateGraphicsShader(GraphicsShaderDescriptor descriptor) public Handle<Shader> CreateGraphicsShader(GraphicsShaderDescriptor descriptor)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
var spinner = new SpinWait(); var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0) while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
@@ -207,7 +207,7 @@ public sealed partial class ResourceManager : IDisposable
public Handle<ComputeShader> CreateComputeShader(ComputeShaderDescriptor descriptor) public Handle<ComputeShader> CreateComputeShader(ComputeShaderDescriptor descriptor)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
var spinner = new SpinWait(); var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0) while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{ {
@@ -233,7 +233,7 @@ public sealed partial class ResourceManager : IDisposable
/// <returns>true if a mesh with the specified Handle exists; otherwise, false.</returns> /// <returns>true if a mesh with the specified Handle exists; otherwise, false.</returns>
public bool HasMesh(Handle<Mesh> handle) public bool HasMesh(Handle<Mesh> handle)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return _meshes.Contains(handle.ID, handle.Generation); return _meshes.Contains(handle.ID, handle.Generation);
} }
@@ -259,7 +259,7 @@ public sealed partial class ResourceManager : IDisposable
/// <param name="handle">The handle of the mesh to release. Must refer to a mesh that was previously created and not already released.</param> /// <param name="handle">The handle of the mesh to release. Must refer to a mesh that was previously created and not already released.</param>
public void ReleaseMesh(Handle<Mesh> handle) public void ReleaseMesh(Handle<Mesh> handle)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
ref var mesh = ref _meshes.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); ref var mesh = ref _meshes.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist) if (!exist)
@@ -278,7 +278,7 @@ public sealed partial class ResourceManager : IDisposable
/// <returns>true if a material with the specified handle exists; otherwise, false.</returns> /// <returns>true if a material with the specified handle exists; otherwise, false.</returns>
public bool HasMaterial(Handle<Material> handle) public bool HasMaterial(Handle<Material> handle)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return _materials.Contains(handle.ID, handle.Generation); return _materials.Contains(handle.ID, handle.Generation);
} }
@@ -304,7 +304,7 @@ public sealed partial class ResourceManager : IDisposable
/// <param name="handle">The handle of the material to release. Must refer to a material that has been previously acquired.</param> /// <param name="handle">The handle of the material to release. Must refer to a material that has been previously acquired.</param>
public void ReleaseMaterial(Handle<Material> handle) public void ReleaseMaterial(Handle<Material> handle)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
ref var material = ref _materials.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); ref var material = ref _materials.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist) if (!exist)
@@ -323,7 +323,7 @@ public sealed partial class ResourceManager : IDisposable
/// <returns>The palette index. Index 0 represents an empty palette.</returns> /// <returns>The palette index. Index 0 represents an empty palette.</returns>
public int GetOrCreateMaterialPalette(ReadOnlySpan<Handle<Material>> materials) public int GetOrCreateMaterialPalette(ReadOnlySpan<Handle<Material>> materials)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
foreach (var material in materials) foreach (var material in materials)
{ {
@@ -342,7 +342,7 @@ public sealed partial class ResourceManager : IDisposable
/// <param name="paletteID">The palette index to validate.</param> /// <param name="paletteID">The palette index to validate.</param>
public bool HasMaterialPalette(Identifier<MaterialPalette> paletteID) public bool HasMaterialPalette(Identifier<MaterialPalette> paletteID)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return _materialPalettes.IsValid(paletteID); return _materialPalettes.IsValid(paletteID);
} }
@@ -352,7 +352,7 @@ public sealed partial class ResourceManager : IDisposable
/// <param name="paletteID">The palette index to query.</param> /// <param name="paletteID">The palette index to query.</param>
public MaterialPalette GetMaterialPaletteInfo(Identifier<MaterialPalette> paletteID) public MaterialPalette GetMaterialPaletteInfo(Identifier<MaterialPalette> paletteID)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return _materialPalettes.GetInfo(paletteID); return _materialPalettes.GetInfo(paletteID);
} }
@@ -363,7 +363,7 @@ public sealed partial class ResourceManager : IDisposable
/// <param name="localMaterialIndex">The material slot inside the palette.</param> /// <param name="localMaterialIndex">The material slot inside the palette.</param>
public Handle<Material> GetMaterialPaletteMaterial(Identifier<MaterialPalette> paletteID, int localMaterialIndex) public Handle<Material> GetMaterialPaletteMaterial(Identifier<MaterialPalette> paletteID, int localMaterialIndex)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return _materialPalettes.GetMaterial(paletteID, localMaterialIndex); return _materialPalettes.GetMaterial(paletteID, localMaterialIndex);
} }
@@ -373,7 +373,7 @@ public sealed partial class ResourceManager : IDisposable
/// <param name="paletteID">The palette index to release.</param> /// <param name="paletteID">The palette index to release.</param>
public void ReleaseMaterialPalette(Identifier<MaterialPalette> paletteID) public void ReleaseMaterialPalette(Identifier<MaterialPalette> paletteID)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
_materialPalettes.Release(paletteID); _materialPalettes.Release(paletteID);
} }
@@ -384,7 +384,7 @@ public sealed partial class ResourceManager : IDisposable
/// <returns>true if a shader with the specified identifier exists; otherwise, false.</returns> /// <returns>true if a shader with the specified identifier exists; otherwise, false.</returns>
public bool HasShader(Handle<Shader> id) public bool HasShader(Handle<Shader> id)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return _shaders.Contains(id.ID, id.Generation); return _shaders.Contains(id.ID, id.Generation);
} }
@@ -410,7 +410,7 @@ public sealed partial class ResourceManager : IDisposable
/// <param name="handle">The identifier of the shader to release. Must refer to a valid, previously created shader.</param> /// <param name="handle">The identifier of the shader to release. Must refer to a valid, previously created shader.</param>
public void ReleaseShader(Handle<Shader> handle) public void ReleaseShader(Handle<Shader> handle)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
ref var shader = ref _shaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); ref var shader = ref _shaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist) if (!exist)
@@ -429,7 +429,7 @@ public sealed partial class ResourceManager : IDisposable
/// <returns>true if a compute shader with the specified identifier exists; otherwise, false.</returns> /// <returns>true if a compute shader with the specified identifier exists; otherwise, false.</returns>
public bool HasComputeShader(Handle<ComputeShader> id) public bool HasComputeShader(Handle<ComputeShader> id)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
return _computeShaders.Contains(id.ID, id.Generation); return _computeShaders.Contains(id.ID, id.Generation);
} }
@@ -455,7 +455,7 @@ public sealed partial class ResourceManager : IDisposable
/// <param name="handle">The identifier of the compute shader to release. Must refer to a valid, previously created ComputeShader.</param> /// <param name="handle">The identifier of the compute shader to release. Must refer to a valid, previously created ComputeShader.</param>
public void ReleaseComputeShader(Handle<ComputeShader> handle) public void ReleaseComputeShader(Handle<ComputeShader> handle)
{ {
Debug.Assert(!_disposed); Logger.DebugAssert(!_disposed);
ref var computeShader = ref _computeShaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); ref var computeShader = ref _computeShaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist) if (!exist)

View File

@@ -1,52 +1,82 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Core.Utilities;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using System.IO.Hashing;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Ghost.Graphics.Services; 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 private struct CacheEntry: IDisposable
{ {
public UnsafeArray<UnsafeArray<byte>> byteCode; public UnsafeArray<ShaderCache> cache;
public void Insert(int index, ReadOnlySpan<byte> data) public void UpdateCache(int index, ref MemoryBlock data, ulong hash)
{ {
if (index >= byteCode.Length) if (index >= cache.Length)
{ {
var newByteCode = new UnsafeArray<UnsafeArray<byte>>(index + 1, Allocator.Persistent); var newByteCode = new UnsafeArray<ShaderCache>(index + 1, AllocationHandle.Persistent);
for (int i = 0; i < byteCode.Length; i++) for (int i = 0; i < cache.Length; i++)
{ {
newByteCode[i] = byteCode[i]; newByteCode[i] = cache[i];
} }
byteCode.Dispose(); cache.Dispose();
byteCode = newByteCode; cache = newByteCode;
} }
var byteData = new UnsafeArray<byte>(data.Length, Allocator.Persistent); cache[index].byteCode = data;
byteData.CopyFrom(data); cache[index].compiledHash = hash;
byteCode[index] = byteData;
} }
public readonly void Dispose() 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<ulong, CacheEntry> _inMemoryCache; private UnsafeHashMap<ulong, CacheEntry> _inMemoryCache;
private UnsafeHashMap<ulong, ulong> _variantToCompiledHash;
private readonly string _cacheDirectory; private readonly string _cacheDirectory;
private readonly IShaderCompilationBridge? _shaderCompilationBridge; private readonly IShaderCompilationBridge? _shaderCompilationBridge;
internal ShaderLibrary(IShaderCompilationBridge? shaderCompilationBridge, string cacheDirectory) internal ShaderLibrary(IShaderCompilationBridge? shaderCompilationBridge, string cacheDirectory)
{ {
_inMemoryCache = new UnsafeHashMap<ulong, CacheEntry>(16, Allocator.Persistent); _inMemoryCache = new UnsafeHashMap<ulong, CacheEntry>(16, AllocationHandle.Persistent);
_variantToCompiledHash = new UnsafeHashMap<ulong, ulong>(16, AllocationHandle.Persistent);
_cacheDirectory = cacheDirectory; _cacheDirectory = cacheDirectory;
_shaderCompilationBridge = shaderCompilationBridge; _shaderCompilationBridge = shaderCompilationBridge;
@@ -66,33 +96,91 @@ internal class ShaderLibrary : IDisposable
return Path.Combine(folderPath, $"shader_cache_{hashString}.bin"); return Path.Combine(folderPath, $"shader_cache_{hashString}.bin");
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ParseCacheData(MemoryBlock data, out CacheHeader header, out ReadOnlySpan<ulong> offsets, out ReadOnlySpan<byte> byteCodes)
public void CacheCompiledResult(ulong id, int index, ReadOnlySpan<byte> byteCode)
{ {
var data = new UnsafeArray<byte>(byteCode.Length, Allocator.Persistent); Logger.DebugAssert(data.IsCreated);
data.CopyFrom(byteCode);
ref var entry = ref _inMemoryCache.GetValueRefOrAddDefault(id, out var exists); var reader = new SpanReader(data.AsSpan<byte>());
entry.Insert(index, byteCode); header = reader.Read<CacheHeader>();
offsets = reader.ReadSpan<ulong>(header.byteCodeOffsetCount);
byteCodes = reader.ReadToEnd<byte>();
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Result<UnsafeArray<byte>, Error> GetCache(ulong id, int index, AllocationHandle allocationHandle) public void CacheCompiledResult(ulong id, int index, Key64<ShaderVariant> variantKey, ReadOnlySpan<ShaderByteCode> 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<byte>());
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<byte>(byteCode.pCode, (int)byteCode.size);
writer.WriteSpan(src);
}
var codeHash = XxHash64.HashToUInt64(data.AsSpan<byte>());
_variantToCompiledHash[variantKey] = codeHash;
ref var entry = ref _inMemoryCache.GetValueRefOrAddDefault(id, out var exists);
entry.UpdateCache(index, ref data, codeHash);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Result<ShaderCache, Error> GetCompiledCache(ulong id, int index, AllocationHandle allocationHandle)
{ {
if (_inMemoryCache.TryGetValue(id, out var entry)) if (_inMemoryCache.TryGetValue(id, out var entry))
{ {
if (index < entry.byteCode.Length) if (index < entry.cache.Length)
{ {
var byteCode = entry.byteCode[index]; var shaderCache = entry.cache[index];
var result = new UnsafeArray<byte>(byteCode.Length, allocationHandle); var result = new MemoryBlock(shaderCache.byteCode.Size, shaderCache.byteCode.Alignment, allocationHandle);
result.CopyFrom(byteCode);
return result; result.CopyFrom(shaderCache.byteCode.AsSpan<byte>());
return new ShaderCache
{
byteCode = result,
compiledHash = shaderCache.compiledHash,
};
} }
} }
return Error.NotFound; return Error.NotFound;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Result<ulong, Error> GetCompiledHash(Key64<ShaderVariant> variantKey)
{
if (_variantToCompiledHash.TryGetValue(variantKey, out var compiledHash))
{
return compiledHash;
}
return Error.NotFound;
}
public void Dispose() public void Dispose()
{ {
foreach (var kvp in _inMemoryCache) foreach (var kvp in _inMemoryCache)

View File

@@ -32,19 +32,19 @@ internal sealed class SwapChainRecord
} }
} }
public bool ReleaseRef() public int ReleaseRef()
{ {
while (true) while (true)
{ {
var current = Volatile.Read(ref _refCount); var current = Volatile.Read(ref _refCount);
if (current == 0) if (current == 0)
{ {
return false; return 0;
} }
if (Interlocked.CompareExchange(ref _refCount, current - 1, current) == current) 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]); var record = Volatile.Read(ref _swapChains[index]);
if (record != null && record.ReleaseRef()) if (record != null && record.ReleaseRef() == 0)
{ {
record.SwapChain.Dispose(); record.SwapChain.Dispose();
Interlocked.CompareExchange(ref _swapChains[index], null, record); Interlocked.CompareExchange(ref _swapChains[index], null, record);
} }
} }
public void TransitionToPresent(ICommandBuffer commandBuffer) public void TransitionToPresent(ICommandBuffer commandBuffer)
{ {
for (int i = 0; i < MAX_SWAP_CHAINS; i++) for (int i = 0; i < MAX_SWAP_CHAINS; i++)

View File

@@ -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. // 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 Ghost.MeshOptimizer;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
@@ -178,7 +179,7 @@ public static unsafe class MeshletUtility
private static ClodBounds MergeBounds(UnsafeList<Cluster> clusters, UnsafeList<int> group) private static ClodBounds MergeBounds(UnsafeList<Cluster> clusters, UnsafeList<int> group)
{ {
using var boundsList = new UnsafeArray<ClodBounds>(group.Count, Allocator.FreeList); using var boundsList = new UnsafeArray<ClodBounds>(group.Count, AllocationHandle.FreeList);
for (var j = 0; j < group.Count; j++) for (var j = 0; j < group.Count; j++)
{ {
boundsList[j] = (clusters[group[j]].bounds); boundsList[j] = (clusters[group[j]].bounds);
@@ -210,9 +211,9 @@ public static unsafe class MeshletUtility
{ {
var maxMeshlets = MeshOptApi.BuildMeshletsBound(indexCount, config.maxVertices, config.minTriangles); var maxMeshlets = MeshOptApi.BuildMeshletsBound(indexCount, config.maxVertices, config.minTriangles);
using var meshlets = new UnsafeArray<meshopt_Meshlet>((int)maxMeshlets, Allocator.FreeList); using var meshlets = new UnsafeArray<meshopt_Meshlet>((int)maxMeshlets, AllocationHandle.FreeList);
using var meshletVertices = new UnsafeArray<uint>((int)indexCount, Allocator.FreeList); using var meshletVertices = new UnsafeArray<uint>((int)indexCount, AllocationHandle.FreeList);
using var meshletTriangles = new UnsafeArray<byte>((int)indexCount, Allocator.FreeList); using var meshletTriangles = new UnsafeArray<byte>((int)indexCount, AllocationHandle.FreeList);
var pMeshlets = (meshopt_Meshlet*)meshlets.GetUnsafePtr(); var pMeshlets = (meshopt_Meshlet*)meshlets.GetUnsafePtr();
var pMeshletVertices = (uint*)meshletVertices.GetUnsafePtr(); var pMeshletVertices = (uint*)meshletVertices.GetUnsafePtr();
@@ -240,7 +241,7 @@ public static unsafe class MeshletUtility
); );
} }
var clusters = new UnsafeList<Cluster>((int)meshletCount, Allocator.FreeList); var clusters = new UnsafeList<Cluster>((int)meshletCount, AllocationHandle.FreeList);
for (nuint i = 0; i < meshletCount; i++) for (nuint i = 0; i < meshletCount; i++)
{ {
@@ -259,9 +260,9 @@ public static unsafe class MeshletUtility
var cluster = new Cluster var cluster = new Cluster
{ {
vertices = meshlet.vertex_count, vertices = meshlet.vertex_count,
indices = new UnsafeList<uint>((int)(meshlet.triangle_count * 3), Allocator.FreeList), indices = new UnsafeList<uint>((int)(meshlet.triangle_count * 3), AllocationHandle.FreeList),
uniqueVertices = new UnsafeList<uint>((int)meshlet.vertex_count, Allocator.FreeList), uniqueVertices = new UnsafeList<uint>((int)meshlet.vertex_count, AllocationHandle.FreeList),
localIndices = new UnsafeList<byte>((int)(meshlet.triangle_count * 3), Allocator.FreeList), localIndices = new UnsafeList<byte>((int)(meshlet.triangle_count * 3), AllocationHandle.FreeList),
group = -1, group = -1,
refined = -1 refined = -1
}; };
@@ -332,8 +333,8 @@ public static unsafe class MeshletUtility
{ {
if (pending.Count <= (int)config.partitionSize) if (pending.Count <= (int)config.partitionSize)
{ {
var single = new UnsafeList<UnsafeList<int>>(1, Allocator.FreeList); var single = new UnsafeList<UnsafeList<int>>(1, AllocationHandle.FreeList);
var pendingcpy = new UnsafeList<int>(pending.Count, Allocator.FreeList); var pendingcpy = new UnsafeList<int>(pending.Count, AllocationHandle.FreeList);
pendingcpy.AddRange(pending.AsSpan()); pendingcpy.AddRange(pending.AsSpan());
single.Add(pendingcpy); single.Add(pendingcpy);
@@ -347,8 +348,8 @@ public static unsafe class MeshletUtility
totalIndexCount += (nuint)clusters[pending[i]].indices.Count; totalIndexCount += (nuint)clusters[pending[i]].indices.Count;
} }
using var clusterIndices = new UnsafeList<uint>((int)totalIndexCount, Allocator.FreeList); using var clusterIndices = new UnsafeList<uint>((int)totalIndexCount, AllocationHandle.FreeList);
using var clusterCounts = new UnsafeList<uint>(pending.Count, Allocator.FreeList); using var clusterCounts = new UnsafeList<uint>(pending.Count, AllocationHandle.FreeList);
nuint offset = 0; nuint offset = 0;
for (var i = 0; i < pending.Count; i++) for (var i = 0; i < pending.Count; i++)
@@ -363,7 +364,7 @@ public static unsafe class MeshletUtility
offset += (nuint)cluster.indices.Count; offset += (nuint)cluster.indices.Count;
} }
using var clusterPart = new UnsafeArray<uint>(pending.Count, Allocator.FreeList); using var clusterPart = new UnsafeArray<uint>(pending.Count, AllocationHandle.FreeList);
var partitionCount = MeshOptApi.PartitionClusters( var partitionCount = MeshOptApi.PartitionClusters(
(uint*)clusterPart.GetUnsafePtr(), (uint*)clusterPart.GetUnsafePtr(),
@@ -377,10 +378,10 @@ public static unsafe class MeshletUtility
config.partitionSize config.partitionSize
); );
var partitions = new UnsafeList<UnsafeList<int>>((int)partitionCount, Allocator.FreeList); var partitions = new UnsafeList<UnsafeList<int>>((int)partitionCount, AllocationHandle.FreeList);
for (nuint i = 0; i < partitionCount; i++) for (nuint i = 0; i < partitionCount; i++)
{ {
partitions.Add(new UnsafeList<int>((int)(config.partitionSize + config.partitionSize / 3), Allocator.FreeList)); partitions.Add(new UnsafeList<int>((int)(config.partitionSize + config.partitionSize / 3), AllocationHandle.FreeList));
} }
for (var i = 0; i < pending.Count; i++) 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<Cluster> clusters, UnsafeList<int> group, ClodBounds simplified, int depth, void* outputContext, ClodOutputDelegate? outputCallback) private static int OutputGroup(ref readonly ClodConfig config, ref readonly ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> group, ClodBounds simplified, int depth, void* outputContext, ClodOutputDelegate? outputCallback)
{ {
using var groupClusters = new UnsafeList<ClodCluster>(group.Count, Allocator.FreeList); using var groupClusters = new UnsafeList<ClodCluster>(group.Count, AllocationHandle.FreeList);
for (var i = 0; i < group.Count; i++) for (var i = 0; i < group.Count; i++)
{ {
@@ -429,8 +430,8 @@ public static unsafe class MeshletUtility
private static void SimplifyFallback(ref UnsafeArray<uint> lod, ref readonly ClodMesh mesh, ReadOnlyUnsafeCollection<uint> indices, ReadOnlyUnsafeCollection<byte> locks, nuint target_count, float* error) private static void SimplifyFallback(ref UnsafeArray<uint> lod, ref readonly ClodMesh mesh, ReadOnlyUnsafeCollection<uint> indices, ReadOnlyUnsafeCollection<byte> locks, nuint target_count, float* error)
{ {
using var subset = new UnsafeArray<SloppyVertex>(indices.Count, Allocator.FreeList); using var subset = new UnsafeArray<SloppyVertex>(indices.Count, AllocationHandle.FreeList);
using var subset_locks = new UnsafeArray<byte>(indices.Count, Allocator.FreeList); using var subset_locks = new UnsafeArray<byte>(indices.Count, AllocationHandle.FreeList);
lod.Resize(indices.Count); lod.Resize(indices.Count);
@@ -440,7 +441,7 @@ public static unsafe class MeshletUtility
for (var i = 0; i < indices.Count; ++i) for (var i = 0; i < indices.Count; ++i)
{ {
var v = indices[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].x = mesh.vertexPositions[v * positions_stride + 0];
subset[i].y = mesh.vertexPositions[v * positions_stride + 1]; subset[i].y = mesh.vertexPositions[v * positions_stride + 1];
@@ -466,7 +467,7 @@ public static unsafe class MeshletUtility
public static UnsafeArray<uint> Simplify(ref readonly ClodConfig config, ref readonly ClodMesh mesh, ReadOnlyUnsafeCollection<uint> indices, ReadOnlyUnsafeCollection<byte> locks, nuint targetCount, float* error) public static UnsafeArray<uint> Simplify(ref readonly ClodConfig config, ref readonly ClodMesh mesh, ReadOnlyUnsafeCollection<uint> indices, ReadOnlyUnsafeCollection<byte> locks, nuint targetCount, float* error)
{ {
var lod = new UnsafeArray<uint>(indices.Count, Allocator.FreeList); var lod = new UnsafeArray<uint>(indices.Count, AllocationHandle.FreeList);
if (targetCount >= (nuint)indices.Count) if (targetCount >= (nuint)indices.Count)
{ {
@@ -577,10 +578,10 @@ public static unsafe class MeshletUtility
/// <returns>The total count of generated clusters.</returns> /// <returns>The total count of generated clusters.</returns>
public static nuint Build(ref readonly ClodConfig config, ref readonly ClodMesh mesh, void* outputContext, ClodOutputDelegate? outputCallback) 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<byte>((int)mesh.vertexCount, Allocator.FreeList, AllocationOption.Clear); ; using var locks = new UnsafeArray<byte>((int)mesh.vertexCount, AllocationHandle.FreeList, AllocationOption.Clear); ;
using var remap = new UnsafeArray<uint>((int)mesh.vertexCount, Allocator.FreeList); using var remap = new UnsafeArray<uint>((int)mesh.vertexCount, AllocationHandle.FreeList);
MeshOptApi.GeneratePositionRemap((uint*)remap.GetUnsafePtr(), mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride); 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); clusters[i].bounds = ComputeBounds(in mesh, clusters[i].indices, 0.0f);
} }
using var pending = new UnsafeList<int>(clusters.Count, Allocator.FreeList); using var pending = new UnsafeList<int>(clusters.Count, AllocationHandle.FreeList);
for (var i = 0; i < clusters.Count; i++) for (var i = 0; i < clusters.Count; i++)
{ {
pending.Add(i); pending.Add(i);
@@ -627,7 +628,7 @@ public static unsafe class MeshletUtility
for (var i = 0; i < groups.Count; i++) for (var i = 0; i < groups.Count; i++)
{ {
using var merged = new UnsafeList<uint>(groups[i].Count * (int)config.maxTriangles * 3, Allocator.FreeList); using var merged = new UnsafeList<uint>(groups[i].Count * (int)config.maxTriangles * 3, AllocationHandle.FreeList);
for (var j = 0; j < groups[i].Count; j++) for (var j = 0; j < groups[i].Count; j++)
{ {
var clusterIndices = clusters[groups[i][j]].indices; var clusterIndices = clusters[groups[i][j]].indices;
@@ -690,4 +691,4 @@ public static unsafe class MeshletUtility
return finalClusterCount; return finalClusterCount;
} }
} }

View File

@@ -1,7 +1,6 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
using System.Diagnostics;
using Ghost.Graphics.Services; using Ghost.Graphics.Services;
namespace Ghost.Graphics.Utilities; namespace Ghost.Graphics.Utilities;
@@ -17,10 +16,10 @@ public static unsafe class RenderingUtility
return; return;
} }
Debug.Assert(r.Value.Type == ResourceType.Buffer); Logger.DebugAssert(r.Value.Type == ResourceType.Buffer);
var sizeInBytes = (nuint)(data.Length * sizeof(T)); var sizeInBytes = (nuint)(data.Length * sizeof(T));
var memoryType = r.Value.BufferDescription.HeapType; var memoryType = r.Value.BufferDescriptor.HeapType;
if (memoryType == HeapType.Upload) if (memoryType == HeapType.Upload)
{ {
@@ -61,7 +60,7 @@ public static unsafe class RenderingUtility
where T : unmanaged where T : unmanaged
{ {
var desc = resourceDatabase.GetResourceDescription(texture.AsResource()).GetValueOrThrow(); 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 requiredSize = resourceDatabase.GetIntermediateResourceSize(texture.AsResource(), 0, 1);
var uploadDesc = new BufferDesc var uploadDesc = new BufferDesc

View File

@@ -139,7 +139,7 @@ public unsafe partial class TestRenderPipeline : IRenderPipeline
continue; 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; var aspectScreen = (float)rtSize.x / rtSize.y;

View File

@@ -52,9 +52,9 @@ public class RenderExtractionSystem : ISystem
ref readonly var camLtwRef = ref camLtw.Get(); ref readonly var camLtwRef = ref camLtw.Get();
// TODO: Classify transparent objects into a separate render list and render via oit. // TODO: Classify transparent objects into a separate render list and render via oit.
var renderList = new RenderList(1, 64, Allocator.FreeList); var renderList = new RenderList(1, 64, AllocationHandle.FreeList);
var transparentRenderList = new RenderList(1, 64, Allocator.FreeList); var transparentRenderList = new RenderList(1, 64, AllocationHandle.FreeList);
var shadowCasterRenderList = new RenderList(1, 64, Allocator.FreeList); var shadowCasterRenderList = new RenderList(1, 64, AllocationHandle.FreeList);
// TODO: This chould be done in earallel jobs. // TODO: This chould be done in earallel jobs.
foreach (var chunk in meshQuery.GetChunkIterator()) foreach (var chunk in meshQuery.GetChunkIterator())

View File

@@ -36,11 +36,11 @@ public partial class UnitTestApp : Application
UnhandledException += (sender, e) => UnhandledException += (sender, e) =>
{ {
Logger.LogError(e.Exception); Logger.Error(e.Exception);
#if DEBUG #if DEBUG
System.Diagnostics.Debugger.Break(); System.Diagnostics.Debugger.Break();
#endif #endif
Environment.FailFast("Unhandled exception", e.Exception); Environment.FailFast("Unhandled exception", e.Exception);
}; };
} }
} }

View File

@@ -50,7 +50,7 @@ internal static class MeshUtility
space_conversion = ufbx_space_conversion.UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY, space_conversion = ufbx_space_conversion.UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY,
}; };
using var str = new UnsafeArray<byte>(Encoding.UTF8.GetByteCount(filePath) + 1, Allocator.FreeList); using var str = new UnsafeArray<byte>(Encoding.UTF8.GetByteCount(filePath) + 1, AllocationHandle.FreeList);
var count = Encoding.UTF8.GetBytes(filePath, str.AsSpan()); var count = Encoding.UTF8.GetBytes(filePath, str.AsSpan());
str[count] = 0; str[count] = 0;
@@ -60,7 +60,7 @@ internal static class MeshUtility
return Result.Failure(error.description.ToString()); return Result.Failure(error.description.ToString());
} }
using var flatVertices = new UnsafeList<Vertex>(1024, Allocator.FreeList); using var flatVertices = new UnsafeList<Vertex>(1024, AllocationHandle.FreeList);
var needComputeNormals = false; var needComputeNormals = false;
@@ -83,7 +83,7 @@ internal static class MeshUtility
var maxScratchIndices = (int)(pMesh->max_face_triangles * 3u); var maxScratchIndices = (int)(pMesh->max_face_triangles * 3u);
using var triIndicesArray = new UnsafeArray<uint>(maxScratchIndices, Allocator.FreeList); using var triIndicesArray = new UnsafeArray<uint>(maxScratchIndices, AllocationHandle.FreeList);
for (var j = 0u; j < pMesh->num_faces; j++) for (var j = 0u; j < pMesh->num_faces; j++)
{ {
@@ -142,8 +142,8 @@ internal static class MeshUtility
var numIndices = (uint)flatVertices.Count; var numIndices = (uint)flatVertices.Count;
using var weldedIndices = new UnsafeArray<uint>((int)numIndices, Allocator.FreeList); using var weldedIndices = new UnsafeArray<uint>((int)numIndices, AllocationHandle.FreeList);
using var cachedIndices = new UnsafeArray<uint>((int)numIndices, Allocator.FreeList); using var cachedIndices = new UnsafeArray<uint>((int)numIndices, AllocationHandle.FreeList);
var stream = new ufbx_vertex_stream var stream = new ufbx_vertex_stream
{ {