-
🔌
-
-
Framework Integration — Works with Avalonia, MAUI, WinForms, WinUI, WPF, and Uno Platform.
+
+
+
+
+ DOCUMENTATION
+ Learn at your own pace.
+
+
-
-
-
🚀
-
-
Modern .NET — Built for .NET 10.0+ with nullable types, spans, and the latest C# features.
+
+
-
-
----
-
-## GPU Features at a Glance
-
-
-
- Graphics
- Vertex and pixel shaders with custom render states.
-
-
- Compute
- General-purpose GPU computing for image processing, simulations, and more.
-
-
- Ray Tracing
- Hardware-accelerated BLAS/TLAS with RayQuery in any shader stage.
-
-
- Mesh Shading
- GPU-driven geometry with mesh and amplification shaders for meshlet processing.
-
-
-
----
-
-## How It Works
-
-
-
-
1
-
-
Initialize
-
Create a graphics context with your preferred backend (DirectX 12, Metal 4, or Vulkan 1.4).
-
-
-
-
2
-
-
Create Resources
-
Define buffers, textures, shaders, and pipelines.
-
-
-
-
3
-
-
Record Commands
-
Build command buffers with draw, compute, or mesh shading dispatches.
-
-
-
-
4
-
-
Submit & Present
-
Execute on GPU and present to screen or export to textures.
-
-
-
-
----
-
-## Get Started
-
-
-
-
Ready to build?
-
Follow our step-by-step tutorials from Hello Triangle to advanced ray tracing and mesh shading.
-
Start Learning →
-
-
+
+
diff --git a/documents/templates/ManagedReference.extension.js b/documents/templates/ManagedReference.extension.js
new file mode 100644
index 00000000..64fa9aeb
--- /dev/null
+++ b/documents/templates/ManagedReference.extension.js
@@ -0,0 +1,11 @@
+exports.postTransform = function (model) {
+ if (!model.namespace?.uid || !model.namespace.specName) {
+ return model;
+ }
+
+ for (const name of model.namespace.specName) {
+ name.value = `
${model.namespace.uid}`;
+ }
+
+ return model;
+};
diff --git a/documents/templates/public/main.css b/documents/templates/public/main.css
index 7fdbe873..434fd288 100644
--- a/documents/templates/public/main.css
+++ b/documents/templates/public/main.css
@@ -1,2061 +1,3777 @@
-/**
- * Zenith.NET Documentation — "Signal" Theme
- * A modern, luminous design language for GPU programming docs.
+a.external[href]::after {
+ content: none;
+}
+
+.next-article .bi-chevron-right {
+ display: none;
+}
+
+/**
+ * Zenith.NET documentation theme.
+ * Brand-led landing page plus a compact, readable DocFX shell.
*/
/* ==========================================================================
- 1. Design Tokens
+ Design tokens
========================================================================== */
:root {
- /* Brand gradient endpoints */
- --brand-blue: #2563EB;
- --brand-violet: #7C3AED;
- /* Functional accent (flat fallback) */
- --accent: #2563EB;
- --accent-hover: #1D4ED8;
- --accent-soft: rgba(37, 99, 235, 0.08);
- --accent-soft-mid: rgba(37, 99, 235, 0.14);
- --accent-ring: rgba(37, 99, 235, 0.30);
- /* Text hierarchy */
- --text-primary: #0F172A;
- --text-secondary: #475569;
- --text-tertiary: #94A3B8;
- /* Surfaces */
- --bg: #FAFAFA;
- --surface: #FFFFFF;
- --surface-raised: #FFFFFF;
- --surface-sunken: #F1F5F9;
- /* Borders */
- --border: #E2E8F0;
- --border-strong: #CBD5E1;
- /* Elevations */
- --shadow-xs: 0 1px 2px rgba(0,0,0,0.05);
- --shadow-sm: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
- --shadow-md: 0 4px 12px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.04);
- --shadow-lg: 0 12px 32px rgba(0,0,0,0.08), 0 4px 8px rgba(0,0,0,0.04);
- /* Radii */
- --radius-xs: 4px;
- --radius-sm: 6px;
- --radius-md: 10px;
- --radius-lg: 14px;
- --radius-full: 9999px;
- /* Motion */
- --ease: cubic-bezier(0.22, 1, 0.36, 1);
- --duration: 0.18s;
- /* Gradients */
- --gradient-brand: linear-gradient(135deg, var(--brand-blue), var(--brand-violet));
- --gradient-subtle: linear-gradient(135deg, rgba(37,99,235,0.05), rgba(124,58,237,0.05));
- --gradient-glow: linear-gradient(135deg, rgba(37,99,235,0.12), rgba(124,58,237,0.12));
+ --zenith-navy-900: #231e2e;
+ --zenith-purple-700: #512bd4;
+ --zenith-magenta-600: #512bd4;
+ --zenith-blue-500: #536dfe;
+ --zenith-green-500: #0f9f78;
+ --zenith-text: #231e2e;
+ --zenith-text-soft: #655f70;
+ --zenith-text-faint: #918a9d;
+ --zenith-surface: #fbfafc;
+ --zenith-surface-soft: #f3f1f7;
+ --zenith-surface-tint: #f6f1ff;
+ --zenith-border: #e1dce8;
+ --zenith-border-strong: #cec6da;
+ --zenith-code-surface: #f6f8fc;
+ --zenith-code-toolbar: #eef2f8;
+ --zenith-code-border: #d8deea;
+ --zenith-code-text: #1f2430;
+ --zenith-code-muted: #687386;
+ --zenith-code-hover: #e2e7f1;
+ --zenith-code-accent: #512bd4;
+ --zenith-code-accent-border: rgba(81, 43, 212, 0.34);
+ --zenith-code-success: #0f766e;
+ --zenith-inline-code: #512bd4;
+ --zenith-inline-code-background: rgba(81, 43, 212, 0.08);
+ --zenith-table-surface: #ffffff;
+ --zenith-table-header: #eef2f7;
+ --zenith-table-stripe: #f8fafc;
+ --zenith-table-hover: #f1f5fb;
+ --zenith-table-border: #d8deea;
+ --zenith-table-text: #4b5565;
+ --zenith-table-heading: #1f2937;
+ --zenith-link: #512bd4;
+ --zenith-link-hover: #7652e8;
+ --zenith-header-surface: rgba(251, 250, 252, 0.95);
+ --zenith-header-border: rgba(35, 30, 46, 0.12);
+ --zenith-nav-text: #70697c;
+ --zenith-nav-active: #231e2e;
+ --zenith-search-surface: #f2f0f5;
+ --zenith-search-border: #e1dce8;
+ --zenith-search-focus: #7150e8;
+ --zenith-accent: #512bd4;
+ --zenith-accent-soft: rgba(81, 43, 212, 0.08);
+ --zenith-logo-width: 115px;
+ --zenith-info: var(--zenith-blue-500);
+ --zenith-success: var(--zenith-green-500);
+ --zenith-warning: #c76620;
+ --zenith-danger: #d94f70;
+ --zenith-syntax-keyword: #af00db;
+ --zenith-syntax-literal: #0000ff;
+ --zenith-syntax-string: #a31515;
+ --zenith-syntax-type: #267f99;
+ --zenith-syntax-function: #795e26;
+ --zenith-syntax-variable: #001080;
+ --zenith-syntax-comment: #008000;
+ --zenith-syntax-number: #098658;
+ --zenith-syntax-meta: #9f4f00;
+ --zenith-syntax-symbol: #b0006d;
+ --zenith-flow-space: 24px;
+ --zenith-panel-radius: var(--zenith-radius);
+ --zenith-control-radius: var(--zenith-radius-small);
+ --zenith-copy-size: 30px;
+ --zenith-toolbar-height: 36px;
+ --zenith-code-padding-block: 12px;
+ --zenith-code-padding-inline: 14px;
+ --zenith-table-padding-block: 12px;
+ --zenith-table-padding-inline: 16px;
+ --zenith-brand-gradient: linear-gradient(110deg, #512bd4 0%, #7150e8 56%, #34c9f5 100%);
+ --zenith-shadow-small: 0 2px 8px rgba(41, 27, 69, 0.07);
+ --zenith-shadow-medium: 0 12px 30px rgba(41, 27, 69, 0.12);
+ --zenith-radius-small: 5px;
+ --zenith-radius: 8px;
+ --zenith-header-height: 78px;
+ --zenith-header-bar-height: 58px;
+ --zenith-header-inset: 10px;
+ --zenith-header-inline-inset: clamp(10px, 1vw, 18px);
+ --zenith-content-width: 1200px;
+ --zenith-docs-width: 1760px;
+ --zenith-page-gutter: clamp(16px, 1.25vw, 40px);
+ --zenith-column-gap: clamp(20px, 1.5vw, 28px);
+ --zenith-sidebar-width: clamp(240px, 18vw, 300px);
+ --zenith-affix-width: 14%;
+ --zenith-api-sidebar-width: clamp(280px, 21vw, 340px);
+ --zenith-scrollbar-size: 8px;
+ --zenith-scrollbar-inset: 2px;
+ --zenith-scrollbar-edge-gap: 4px;
+ --zenith-scrollbar-min-thumb: 36px;
+ --zenith-scrollbar-thumb: color-mix(in srgb, var(--zenith-text-faint) 62%, transparent);
+ --zenith-scrollbar-thumb-hover: color-mix(in srgb, var(--zenith-text-soft) 82%, transparent);
+ --zenith-ease: cubic-bezier(0.22, 1, 0.36, 1);
+ --bs-body-font-family: "Aptos", "Segoe UI Variable Text", sans-serif;
+ --bs-body-color: var(--zenith-text);
+ --bs-body-bg: var(--zenith-surface);
+ --bs-link-color: var(--zenith-link);
+ --bs-link-color-rgb: 81, 43, 212;
+ --bs-link-hover-color: var(--zenith-link-hover);
+ --bs-link-hover-color-rgb: 118, 82, 232;
+ --bs-border-color: var(--zenith-border);
+ --bs-border-radius: var(--zenith-radius-small);
}
[data-bs-theme="dark"] {
- --brand-blue: #60A5FA;
- --brand-violet: #A78BFA;
- --accent: #60A5FA;
- --accent-hover: #93C5FD;
- --accent-soft: rgba(96, 165, 250, 0.10);
- --accent-soft-mid: rgba(96, 165, 250, 0.18);
- --accent-ring: rgba(96, 165, 250, 0.30);
- --text-primary: #F1F5F9;
- --text-secondary: #94A3B8;
- --text-tertiary: #64748B;
- --bg: #09090B;
- --surface: #18181B;
- --surface-raised: #1E1E22;
- --surface-sunken: #111114;
- --border: rgba(255,255,255,0.08);
- --border-strong: rgba(255,255,255,0.14);
- --shadow-xs: 0 1px 2px rgba(0,0,0,0.30);
- --shadow-sm: 0 1px 3px rgba(0,0,0,0.40), 0 1px 2px rgba(0,0,0,0.30);
- --shadow-md: 0 4px 12px rgba(0,0,0,0.40), 0 2px 4px rgba(0,0,0,0.30);
- --shadow-lg: 0 12px 32px rgba(0,0,0,0.50), 0 4px 8px rgba(0,0,0,0.35);
- --gradient-subtle: linear-gradient(135deg, rgba(96,165,250,0.06), rgba(167,139,250,0.06));
- --gradient-glow: linear-gradient(135deg, rgba(96,165,250,0.15), rgba(167,139,250,0.15));
+ --zenith-purple-700: #b9a5ff;
+ --zenith-magenta-600: #d1c4ff;
+ --zenith-text: #f5f1fa;
+ --zenith-text-soft: #b9b1c4;
+ --zenith-text-faint: #81798d;
+ --zenith-surface: #100d16;
+ --zenith-surface-soft: #17121f;
+ --zenith-surface-tint: #1d1728;
+ --zenith-border: #312940;
+ --zenith-border-strong: #463a5a;
+ --zenith-code-surface: #191621;
+ --zenith-code-toolbar: #211d2a;
+ --zenith-code-border: #393243;
+ --zenith-code-text: #e4dfea;
+ --zenith-code-muted: #aaa1b5;
+ --zenith-code-hover: #2d2737;
+ --zenith-code-accent: #b9a5ff;
+ --zenith-code-accent-border: rgba(185, 165, 255, 0.42);
+ --zenith-code-success: #70cdb5;
+ --zenith-inline-code: #b69cff;
+ --zenith-inline-code-background: rgba(155, 123, 255, 0.13);
+ --zenith-shadow-small: 0 2px 8px rgba(0, 0, 0, 0.24);
+ --zenith-shadow-medium: 0 12px 30px rgba(0, 0, 0, 0.34);
+ --zenith-table-surface: #17131e;
+ --zenith-table-header: #211b2b;
+ --zenith-table-stripe: #1b1624;
+ --zenith-table-hover: #241d30;
+ --zenith-table-border: #393044;
+ --zenith-table-text: #c0b8ca;
+ --zenith-table-heading: #f1edf6;
+ --zenith-header-surface: rgba(16, 13, 22, 0.95);
+ --zenith-link: #b9a5ff;
+ --zenith-link-hover: #d1c4ff;
+ --zenith-nav-active: #f7f3ff;
+ --zenith-search-surface: #191421;
+ --zenith-search-border: #312940;
+ --zenith-info: #8fa2ff;
+ --zenith-success: #70cdb5;
+ --zenith-warning: #ffb16f;
+ --zenith-danger: #ff8fa3;
+ --zenith-syntax-keyword: #c586c0;
+ --zenith-syntax-literal: #569cd6;
+ --zenith-syntax-string: #ce9178;
+ --zenith-syntax-type: #4ec9b0;
+ --zenith-syntax-function: #dcdcaa;
+ --zenith-syntax-variable: #9cdcfe;
+ --zenith-syntax-comment: #6a9955;
+ --zenith-syntax-number: #b5cea8;
+ --zenith-syntax-meta: #d7ba7d;
+ --zenith-syntax-symbol: #c8c8c8;
+ --bs-body-color: var(--zenith-text);
+ --bs-body-bg: var(--zenith-surface);
+ --bs-link-color: var(--zenith-link);
+ --bs-link-color-rgb: 185, 165, 255;
+ --bs-link-hover-color: var(--zenith-link-hover);
+ --bs-link-hover-color-rgb: 209, 196, 255;
+ --bs-border-color: var(--zenith-border);
}
/* ==========================================================================
- 2. Base
+ Base and typography
========================================================================== */
-body {
- background-color: var(--bg);
- color: var(--text-primary);
- padding-top: 60px !important;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- position: relative;
+html {
+ scroll-padding-top: calc(var(--zenith-header-height) + 24px);
}
- /* Page-wide decorative mesh gradient */
- body::before {
- content: '';
- position: fixed;
- inset: 0;
- pointer-events: none;
- z-index: 0;
- background: radial-gradient(50% 40% at 20% 20%, rgba(37,99,235,0.06) 0%, transparent 70%), radial-gradient(45% 35% at 75% 30%, rgba(124,58,237,0.05) 0%, transparent 70%), radial-gradient(40% 40% at 50% 80%, rgba(37,99,235,0.04) 0%, transparent 70%);
+@supports not selector(::-webkit-scrollbar) {
+ * {
+ scrollbar-color: var(--zenith-scrollbar-thumb) transparent;
+ scrollbar-width: thin;
}
+}
-[data-bs-theme="dark"] body::before {
- background: radial-gradient(50% 40% at 20% 20%, rgba(96,165,250,0.08) 0%, transparent 70%), radial-gradient(45% 35% at 75% 30%, rgba(167,139,250,0.07) 0%, transparent 70%), radial-gradient(40% 40% at 50% 80%, rgba(96,165,250,0.05) 0%, transparent 70%);
+*::-webkit-scrollbar {
+ width: var(--zenith-scrollbar-size);
+ height: var(--zenith-scrollbar-size);
}
-::selection {
- background: var(--accent-soft-mid);
- color: var(--text-primary);
+*::-webkit-scrollbar-track,
+*::-webkit-scrollbar-corner {
+ background: transparent;
}
-/* Full-width layout */
-main.container-xxl,
-header .container-xxl {
- max-width: 100% !important;
- padding-left: 2rem !important;
- padding-right: 2rem !important;
+*::-webkit-scrollbar-thumb {
+ background-color: var(--zenith-scrollbar-thumb);
+ background-clip: padding-box;
+ border: var(--zenith-scrollbar-inset) solid transparent;
+ border-radius: 999px;
}
-/* ==========================================================================
- 3. Navbar
- ========================================================================== */
+ *::-webkit-scrollbar-thumb:hover {
+ background-color: var(--zenith-scrollbar-thumb-hover);
+ }
-header.bg-body,
-.navbar {
- --bs-bg-opacity: 0;
- position: fixed !important;
- top: 0 !important;
- left: 0 !important;
- right: 0 !important;
- z-index: 1030 !important;
- background: color-mix(in srgb, var(--bg) 85%, transparent) !important;
- backdrop-filter: blur(12px) saturate(1.4) !important;
- -webkit-backdrop-filter: blur(12px) saturate(1.4) !important;
- border: none !important;
- border-bottom: 1px solid var(--border) !important;
- box-shadow: none !important;
+body {
+ min-width: 0;
+ padding-top: var(--zenith-header-height) !important;
+ background: var(--zenith-surface);
+ color: var(--zenith-text);
+ font-family: var(--bs-body-font-family);
+ font-size: 16px;
+ line-height: 1.68;
+ letter-spacing: 0;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
}
-.dropdown-menu {
- --bs-dropdown-bg: transparent;
- position: absolute !important;
- top: 100% !important;
- background: color-mix(in srgb, var(--surface) 95%, transparent) !important;
- backdrop-filter: blur(16px) !important;
- -webkit-backdrop-filter: blur(16px) !important;
- border: 1px solid var(--border) !important;
- border-radius: var(--radius-md) !important;
- box-shadow: var(--shadow-lg) !important;
- padding: 4px !important;
- margin-top: 4px !important;
- overflow: visible;
- z-index: 1040 !important;
+::selection {
+ background: rgba(198, 0, 189, 0.18);
+ color: var(--zenith-text);
}
-.dropdown-item {
- border-radius: var(--radius-sm) !important;
- transition: all var(--duration) var(--ease) !important;
- color: var(--text-primary) !important;
- padding: 6px 10px !important;
+a {
+ color: var(--zenith-link);
+ text-decoration-color: rgba(81, 43, 212, 0.3);
+ text-underline-offset: 3px;
+ transition: color 160ms ease, text-decoration-color 160ms ease;
}
- .dropdown-item:hover,
- .dropdown-item:focus {
- background: var(--accent-soft) !important;
- color: var(--accent) !important;
+ a:hover,
+ a:focus {
+ color: var(--zenith-link-hover);
+ text-decoration: none;
}
-.navbar #navbar,
-.navbar #navbar > * {
- display: flex;
- align-items: center;
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ color: var(--zenith-text);
+ font-family: "Aptos Display", "Segoe UI Variable Display", "Bahnschrift", sans-serif;
+ font-weight: 750;
+ letter-spacing: 0;
}
-.navbar .navbar-nav {
- display: flex;
- gap: 2px;
+img {
+ max-width: 100%;
}
-.navbar .nav-link {
- position: relative;
- padding: 6px 12px !important;
- border-radius: var(--radius-sm) !important;
- font-size: 0.875rem !important;
- font-weight: 500 !important;
- color: var(--text-secondary) !important;
- transition: all var(--duration) var(--ease) !important;
- text-decoration: none !important;
+:focus-visible {
+ outline: 3px solid rgba(113, 56, 232, 0.42) !important;
+ outline-offset: 3px;
}
- .navbar .nav-link:hover {
- background: var(--accent-soft) !important;
- color: var(--text-primary) !important;
- }
-
- .navbar .nav-link.active {
- color: var(--accent) !important;
- font-weight: 600 !important;
- }
+/* ==========================================================================
+ Global header
+ ========================================================================== */
-.navbar .icons {
- display: flex;
- align-items: center;
- gap: 2px;
+body > header.bg-body,
+body > header {
+ position: fixed !important;
+ top: var(--zenith-header-inset);
+ left: 50%;
+ z-index: 1030;
+ width: min(calc(100% - (2 * var(--zenith-header-inline-inset))), var(--zenith-docs-width));
+ height: var(--zenith-header-bar-height);
+ background: var(--zenith-header-surface) !important;
+ border: 1px solid var(--zenith-header-border) !important;
+ border-radius: var(--zenith-radius);
+ box-shadow: 0 8px 24px rgba(20, 18, 32, 0.08);
+ backdrop-filter: blur(16px) saturate(1.25);
+ -webkit-backdrop-filter: blur(16px) saturate(1.25);
+ transform: translateX(-50%);
+ transition: background-color 240ms ease, border-color 240ms ease, box-shadow 240ms ease;
+}
+
+ body > header.is-scrolled {
+ box-shadow: 0 12px 32px rgba(20, 18, 32, 0.13);
+ }
+
+[data-bs-theme="dark"] body > header.is-scrolled {
+ box-shadow: 0 14px 34px rgba(0, 0, 0, 0.38);
+}
+
+.zenith-reading-progress {
+ position: absolute;
+ right: 8px;
+ bottom: -1px;
+ left: 8px;
+ z-index: 2;
+ height: 2px;
+ overflow: hidden;
+ pointer-events: none;
}
- .navbar .icons > a.btn,
- .navbar .icons .dropdown > a.btn {
- display: inline-flex !important;
- align-items: center !important;
- justify-content: center !important;
- width: 34px !important;
- height: 34px !important;
- padding: 0 !important;
- border-radius: var(--radius-sm) !important;
- color: var(--text-secondary) !important;
- transition: all var(--duration) var(--ease) !important;
+ .zenith-reading-progress > span {
+ display: block;
+ width: 100%;
+ height: 100%;
+ background: var(--zenith-brand-gradient);
+ box-shadow: 0 0 10px rgba(81, 43, 212, 0.34);
+ transform: scaleX(0);
+ transform-origin: left center;
+ will-change: transform;
}
- .navbar .icons > a.btn:hover,
- .navbar .icons .dropdown > a.btn:hover {
- background: var(--accent-soft) !important;
- color: var(--accent) !important;
- }
-
- .navbar .icons > a.btn i,
- .navbar .icons .dropdown > a.btn i {
- font-size: 1.05rem;
- }
-
- /* Theme switcher dropdown — match icon button size */
- .navbar .icons .dropdown > a.btn {
- width: auto !important;
- height: 34px !important;
- padding: 6px 10px !important;
+@supports (animation-timeline: scroll()) {
+ .zenith-reading-progress > span {
+ animation: zenith-reading-progress linear both;
+ animation-timeline: scroll(root block);
+ animation-range: 0% 100%;
}
+}
-/* Search box — smaller width and spacing from theme switcher */
-.navbar #navbar form.search {
- margin-left: 8px !important;
+[data-bs-theme="dark"] body > header.bg-body,
+[data-bs-theme="dark"] body > header {
+ background: var(--zenith-header-surface) !important;
+ border-color: var(--zenith-border) !important;
}
- .navbar #navbar form.search > input {
- max-width: 180px !important;
- height: 34px !important;
- font-size: 0.85rem !important;
- }
+body[data-layout="landing"] > header.bg-body,
+body[data-layout="landing"] > header {
+ background: rgba(251, 250, 252, 0.58) !important;
+ border-color: rgba(35, 30, 46, 0.08) !important;
+ box-shadow: 0 8px 24px rgba(41, 27, 69, 0.045);
+}
-#logo {
- height: 50px;
+[data-bs-theme="dark"] body[data-layout="landing"] > header.bg-body,
+[data-bs-theme="dark"] body[data-layout="landing"] > header {
+ background: rgba(16, 13, 22, 0.5) !important;
+ border-color: rgba(185, 165, 255, 0.1) !important;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
-/* ==========================================================================
- 4. Hero Section
- ========================================================================== */
+body[data-layout="landing"] > header.is-scrolled {
+ background: rgba(251, 250, 252, 0.88) !important;
+ border-color: rgba(35, 30, 46, 0.12) !important;
+ box-shadow: 0 12px 32px rgba(41, 27, 69, 0.11);
+}
-.hero {
- text-align: center;
- padding: 4rem 2rem 4.5rem;
+[data-bs-theme="dark"] body[data-layout="landing"] > header.is-scrolled {
+ background: rgba(16, 13, 22, 0.84) !important;
+ border-color: rgba(185, 165, 255, 0.16) !important;
+ box-shadow: 0 14px 34px rgba(0, 0, 0, 0.28);
}
-.hero-content {
- max-width: 720px;
- margin: 0 auto;
+.navbar {
+ width: 100%;
+ height: 100%;
+ min-height: 0;
+ padding: 0 !important;
}
-.hero-logo {
- width: 80px;
- height: 80px;
- margin-bottom: 1.75rem;
- filter: drop-shadow(0 4px 12px rgba(37,99,235,0.20));
+ .navbar > .container-xxl {
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ max-width: var(--zenith-content-width) !important;
+ min-height: 0;
+ margin: 0 auto;
+ padding: 0 18px !important;
+ }
+
+.navbar-brand {
+ position: relative;
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ margin-right: 10px !important;
+ padding: 0 !important;
}
-[data-bs-theme="dark"] .hero-logo {
- filter: drop-shadow(0 4px 16px rgba(96,165,250,0.30));
+#logo {
+ display: block;
+ width: var(--zenith-logo-width);
+ height: 36px;
+ object-fit: contain;
+ object-position: left center;
+ transform-origin: left center;
+ transition: filter 220ms ease, transform 220ms var(--zenith-ease);
}
-.hero h1 {
- width: fit-content;
- margin: 0 auto 0.5rem;
- font-size: 3.5rem;
- font-weight: 800;
- letter-spacing: -0.035em;
- line-height: 1.1;
- border: none;
- background: var(--gradient-brand);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
+.navbar-brand:hover #logo,
+.navbar-brand:focus-visible #logo {
+ filter: drop-shadow(0 5px 10px rgba(81, 43, 212, 0.18));
+ transform: translateY(-1px) scale(1.012);
}
-.hero-tagline {
- font-size: 1.25rem;
- font-weight: 500;
- margin: 0 0 1rem;
- color: var(--text-primary);
- letter-spacing: -0.01em;
+.navbar-brand:active #logo {
+ transform: scale(0.992);
}
-.hero-description {
- font-size: 1.05rem;
- max-width: 560px;
- margin: 0 auto 2.25rem;
- color: var(--text-secondary);
- line-height: 1.7;
+.navbar-collapse,
+#navbar {
+ min-width: 0;
}
-.hero-buttons {
+#navbar {
display: flex;
- justify-content: center;
- gap: 0.75rem;
- flex-wrap: wrap;
+ flex: 1 1 auto;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 6px;
}
- .hero-buttons .btn,
- .btn,
- button.btn,
- a.btn {
- border-radius: var(--radius-sm) !important;
+ #navbar .navbar-nav {
+ display: flex;
+ order: 1;
+ flex: 0 1 auto;
+ align-items: center;
+ gap: 2px;
+ margin-right: auto;
}
- .hero-buttons .btn {
+ #navbar .nav-link {
+ position: relative;
display: inline-flex;
align-items: center;
- justify-content: center;
- padding: 0.625rem 1.5rem;
- font-size: 0.9rem;
+ height: 36px;
+ padding: 0 12px !important;
+ color: var(--zenith-nav-text) !important;
+ border-radius: 0 !important;
+ font-size: 14px;
font-weight: 600;
+ line-height: 1;
+ letter-spacing: 0;
text-decoration: none;
- border: none;
- outline: none;
- transition: all var(--duration) var(--ease);
- gap: 0.375rem;
+ transition: color 180ms ease, background-color 180ms ease, transform 180ms var(--zenith-ease);
}
- .hero-buttons .btn:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 2px;
- }
-
- .hero-buttons .btn-primary {
- background: var(--gradient-brand);
- color: #FFFFFF;
- box-shadow: var(--shadow-sm), 0 0 0 1px rgba(37,99,235,0.10);
- }
-
- .hero-buttons .btn-primary:hover {
- box-shadow: var(--shadow-md), 0 0 20px rgba(37,99,235,0.20);
- transform: translateY(-1px);
- }
+[data-bs-theme="dark"] #navbar .nav-link {
+ color: var(--zenith-text-soft) !important;
+}
- .hero-buttons .btn-secondary {
- background: var(--surface) !important;
- color: var(--text-primary);
- border: 1px solid var(--border-strong) !important;
- box-shadow: var(--shadow-xs);
- }
+#navbar .nav-link::after {
+ content: "";
+ position: absolute;
+ right: 12px;
+ bottom: 4px;
+ left: 12px;
+ height: 2px;
+ background: var(--zenith-accent);
+ opacity: 0;
+ transform: scaleX(0.3);
+ transition: opacity 160ms ease, transform 160ms ease;
+}
- .hero-buttons .btn-secondary:hover {
- border-color: var(--accent) !important;
- color: var(--accent);
- transform: translateY(-1px);
- box-shadow: var(--shadow-sm);
- }
+#navbar .nav-link:hover,
+#navbar .nav-link.active {
+ color: var(--zenith-nav-active) !important;
+}
-/* ==========================================================================
- 5. Features Grid
- ========================================================================== */
+[data-bs-theme="dark"] #navbar .nav-link:hover,
+[data-bs-theme="dark"] #navbar .nav-link.active {
+ color: var(--zenith-nav-active) !important;
+}
-.features {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: 1rem;
- margin: 2rem 0;
+#navbar .nav-link:hover::after,
+#navbar .nav-link.active::after {
+ opacity: 1;
+ transform: scaleX(1);
}
-.feature {
- padding: 1.5rem;
- border-radius: var(--radius-md);
- border: 1px solid var(--border);
- background: var(--surface);
- transition: all var(--duration) var(--ease);
+#navbar form.search {
+ order: 2 !important;
+ width: 210px;
+ margin: 0 !important;
}
- .feature:hover {
- border-color: var(--border-strong);
- box-shadow: var(--shadow-md);
- transform: translateY(-2px);
+ #navbar form.search > i {
+ left: 12px;
+ color: var(--zenith-text-faint);
}
- .feature h3 {
- font-size: 1.05rem;
- font-weight: 600;
- margin: 0 0 0.5rem;
- color: var(--text-primary);
+ #navbar form.search > input {
+ width: 100%;
+ height: 36px;
+ padding-left: 36px;
+ background: var(--zenith-search-surface);
+ color: var(--zenith-text);
+ border: 1px solid var(--zenith-search-border);
+ border-radius: 6px;
+ box-shadow: none;
+ font-size: 13px;
}
- .feature p {
- font-size: 0.875rem;
- color: var(--text-secondary);
- margin: 0;
- line-height: 1.6;
+ #navbar form.search.zenith-search {
+ position: relative;
+ display: flex;
+ width: 36px;
+ min-width: 36px;
+ height: 36px;
+ align-items: center;
+ overflow: hidden;
+ border-radius: 6px;
+ transition: width 300ms var(--zenith-ease), box-shadow 180ms ease;
}
-/* ==========================================================================
- 6. Technology Section
- ========================================================================== */
+ #navbar form.search.zenith-search.is-expanded {
+ width: 230px;
+ box-shadow: 0 8px 22px rgba(20, 15, 30, 0.1);
+ }
-.tech-section {
- display: flex;
- flex-direction: column;
- gap: 2rem;
- margin: 2rem 0;
-}
+ #navbar form.search.zenith-search > i {
+ display: none;
+ }
-.tech-group {
- text-align: center;
+ #navbar form.search.zenith-search > input {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ width: 100%;
+ padding-right: 12px;
+ padding-left: 38px;
+ opacity: 0;
+ pointer-events: none;
+ transform: translateX(8px);
+ transition: opacity 180ms ease, transform 300ms var(--zenith-ease), border-color 180ms ease, background-color 180ms ease;
+ }
+
+ #navbar form.search.zenith-search.is-expanded > input {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateX(0);
+ }
+
+.zenith-search-toggle {
+ position: relative;
+ z-index: 1;
+ display: inline-flex;
+ width: 36px;
+ height: 36px;
+ flex: 0 0 36px;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ color: var(--zenith-text-soft);
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ font-size: 15px;
+ transition: color 180ms ease, background-color 180ms ease, border-color 180ms ease, transform 220ms var(--zenith-ease);
}
- .tech-group h4 {
- font-size: 0.7rem;
- font-weight: 600;
- color: var(--text-tertiary);
- margin: 0 0 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.1em;
+ .zenith-search-toggle:hover,
+ .zenith-search-toggle:focus-visible {
+ color: var(--zenith-accent);
+ background: transparent;
+ border-color: transparent;
}
-.tech-icons {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- gap: 0.625rem;
+#navbar form.search.zenith-search.is-expanded .zenith-search-toggle {
+ color: var(--zenith-accent);
+ border-color: transparent;
+ transform: rotate(-8deg) scale(0.94);
+}
+
+#navbar form.search.zenith-search.is-loading .zenith-search-toggle {
+ color: var(--zenith-text-faint);
}
-.tech-item {
+.navbar .icons {
+ order: 3 !important;
display: flex;
+ flex: 0 0 auto;
align-items: center;
- justify-content: center;
- padding: 0.5rem 1rem;
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- transition: all var(--duration) var(--ease);
+ gap: 6px;
}
- .tech-item:hover {
- border-color: var(--accent-ring);
- background: var(--accent-soft);
+ .navbar .icons .zenith-theme-toggle {
+ order: 1;
}
- .tech-item .tech-text {
- font-size: 0.875rem;
- font-weight: 600;
- color: var(--accent);
+ .navbar .icons > a.btn {
+ order: 2;
}
-/* ==========================================================================
- 7. Cards Grid
- ========================================================================== */
+ .navbar .icons .btn,
+ .navbar .icons > a.btn {
+ display: inline-flex !important;
+ align-items: center;
+ justify-content: center;
+ min-width: 36px;
+ height: 36px;
+ padding: 0 9px !important;
+ color: var(--zenith-text-soft) !important;
+ background: transparent !important;
+ border: 0 !important;
+ border-radius: 6px !important;
+ box-shadow: none !important;
+ transition: color 180ms ease, background-color 180ms ease, transform 180ms var(--zenith-ease) !important;
+ }
+
+ .zenith-search-toggle > i,
+ .navbar .icons .btn > i,
+ .navbar .icons > a.btn > i {
+ display: inline-flex;
+ width: 16px;
+ height: 16px;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ line-height: 1;
+ transform-origin: center;
+ }
-.cards {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
- gap: 1rem;
- margin: 2rem 0;
-}
+ .zenith-search-toggle > i::before,
+ .navbar .icons .btn > i::before,
+ .navbar .icons > a.btn > i::before {
+ display: block;
+ line-height: 1;
+ vertical-align: 0;
+ }
-.card {
- display: flex;
- flex-direction: column;
- padding: 1.5rem;
- border-radius: var(--radius-md);
- border: 1px solid var(--border);
- background: var(--surface);
- transition: all var(--duration) var(--ease);
-}
+ .navbar .icons .zenith-theme-toggle > i {
+ transition: color 180ms ease, transform 220ms var(--zenith-ease);
+ }
- .card:hover {
- border-color: var(--border-strong);
- box-shadow: var(--shadow-md);
- transform: translateY(-2px);
+ .navbar .icons .btn.zenith-theme-toggle:hover,
+ .navbar .icons .btn.zenith-theme-toggle:focus-visible {
+ color: var(--zenith-accent) !important;
+ background: transparent !important;
+ transform: none !important;
}
- .card h3 {
- font-size: 1.05rem;
- font-weight: 600;
- margin: 0 0 0.5rem;
- color: var(--text-primary);
+ .navbar .icons .zenith-theme-toggle:hover > i {
+ transform: rotate(12deg) scale(1.08);
}
- .card p {
- font-size: 0.875rem;
- color: var(--text-secondary);
- margin: 0;
- line-height: 1.6;
- flex-grow: 1;
+ .navbar .icons .btn:hover,
+ .navbar .icons > a.btn:hover {
+ color: var(--zenith-accent) !important;
+ background: var(--zenith-accent-soft) !important;
+ transform: translateY(-1px);
}
- .card a {
- display: inline-block;
- margin-top: 1rem;
- color: var(--accent);
- font-size: 0.8125rem;
- font-weight: 600;
- text-decoration: none;
- transition: color var(--duration) var(--ease);
+ .navbar .icons > a.btn:hover,
+ .navbar .icons > a.btn:focus-visible {
+ color: var(--zenith-accent) !important;
+ background: transparent !important;
+ transform: none !important;
}
- .card a:hover {
- color: var(--accent-hover);
- }
+ .navbar .icons > a.btn > i {
+ transition: color 180ms ease, transform 220ms var(--zenith-ease);
+ }
-/* ==========================================================================
- 8. Workflow Section
- ========================================================================== */
+ .navbar .icons > a.btn:hover > i {
+ transform: scale(1.08);
+ }
-.workflow {
- display: flex;
- flex-direction: column;
- margin: 2rem 0;
- --step-size: 44px;
- --step-line: 2px;
-}
+ .navbar .icons .zenith-theme-toggle.is-theme-leaving > i {
+ animation: zenith-theme-icon-leave 120ms ease-in forwards;
+ }
-.workflow-step {
- display: flex;
- align-items: center;
- gap: 1.25rem;
- position: relative;
- padding-bottom: 1.25rem;
+ .navbar .icons .zenith-theme-toggle.is-theme-entering > i {
+ animation: zenith-theme-icon-enter 260ms var(--zenith-ease) both;
+ }
+
+@keyframes zenith-theme-icon-leave {
+ to {
+ opacity: 0;
+ transform: rotate(70deg) scale(0.52);
+ }
}
- .workflow-step:last-child {
- padding-bottom: 0;
+@keyframes zenith-theme-icon-enter {
+ from {
+ opacity: 0;
+ transform: rotate(-70deg) scale(0.52);
}
- .workflow-step:not(:last-child)::after {
- content: '';
- position: absolute;
- left: calc((var(--step-size) - var(--step-line)) / 2);
- top: 50%;
- width: var(--step-line);
- height: 100%;
- background: var(--border-strong);
- z-index: 0;
- }
-
-.step-number {
- width: var(--step-size);
- height: var(--step-size);
- min-width: var(--step-size);
- border-radius: 50%;
- background: var(--gradient-brand);
- color: #FFFFFF;
- font-size: 1rem;
- font-weight: 700;
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1;
- flex-shrink: 0;
- box-shadow: var(--shadow-sm), 0 0 0 3px var(--bg);
+ 68% {
+ opacity: 1;
+ transform: rotate(8deg) scale(1.12);
+ }
+
+ to {
+ opacity: 1;
+ transform: rotate(0) scale(1);
+ }
+}
+
+.dropdown-menu {
+ padding: 5px !important;
+ background: var(--zenith-surface) !important;
+ border: 1px solid var(--zenith-border) !important;
+ border-radius: 7px !important;
+ box-shadow: var(--zenith-shadow-medium) !important;
}
-.step-content {
- flex: 1;
- padding: 1rem 1.25rem;
- border-radius: var(--radius-md);
- border: 1px solid var(--border);
- background: var(--surface);
- transition: all var(--duration) var(--ease);
+.dropdown-item {
+ color: var(--zenith-text) !important;
+ border-radius: 5px !important;
}
- .step-content:hover {
- border-color: var(--border-strong);
- box-shadow: var(--shadow-sm);
+ .dropdown-item:hover,
+ .dropdown-item:focus {
+ color: var(--zenith-purple-700) !important;
+ background: rgba(113, 56, 232, 0.08) !important;
}
- .step-content h4 {
- font-size: 0.95rem;
- font-weight: 600;
- margin: 0 0 0.25rem;
- color: var(--text-primary);
+/* ==========================================================================
+ Landing layout reset
+ ========================================================================== */
+
+body[data-layout="landing"] {
+ overflow-x: clip;
+ padding-top: 0 !important;
+}
+
+ body[data-layout="landing"] > main.container-xxl {
+ width: 100%;
+ max-width: none !important;
+ margin: 0 !important;
+ padding: 0 !important;
}
- .step-content p {
- font-size: 0.875rem;
- color: var(--text-secondary);
+ body[data-layout="landing"] main > .content {
+ width: 100%;
+ max-width: none;
margin: 0;
- line-height: 1.6;
+ padding: 0;
}
-/* ==========================================================================
- 9. Badges
- ========================================================================== */
+ body[data-layout="landing"] .actionbar,
+ body[data-layout="landing"] .next-article,
+ body[data-layout="landing"] main > .affix {
+ display: none !important;
+ }
-.community-badges,
-.hero-badges {
- display: flex;
- justify-content: center;
- flex-wrap: wrap;
- gap: 0.5rem;
+ body[data-layout="landing"] article {
+ width: 100%;
+ max-width: none;
+ margin: 0;
+ padding: 0;
+ }
+
+.landing-page {
+ width: 100%;
+ color: var(--zenith-text);
}
-.community-badges {
- gap: 0.875rem;
- margin: 2rem 0;
+.landing-shell {
+ width: calc(100% - clamp(36px, 4vw, 64px));
+ max-width: 1440px;
+ margin: 0 auto;
}
-.hero-badges {
- margin-top: 1.75rem;
+.landing-page p {
+ color: var(--zenith-text-soft);
}
- .community-badges a,
- .hero-badges a {
- transition: opacity var(--duration) var(--ease), transform var(--duration) var(--ease);
- }
+/* ==========================================================================
+ Landing hero
+ ========================================================================== */
- .community-badges a:hover,
- .hero-badges a:hover {
- transform: translateY(-1px);
- opacity: 0.80;
- }
+.hero-copy {
+ min-width: 0;
+ padding: 24px 0;
+}
- .hero-badges img {
- height: 22px;
+ .hero-copy h1 {
+ max-width: 660px;
+ margin: 0 0 22px;
+ color: var(--zenith-navy-900);
+ border: 0;
+ font-size: 53px;
+ font-weight: 800;
+ line-height: 1.04;
+ letter-spacing: 0;
}
-/* ==========================================================================
- 10. Technology Bar
- ========================================================================== */
+[data-bs-theme="dark"] .hero-copy h1 {
+ color: #f3f6ff;
+}
+
+.hero-copy h1 span {
+ color: var(--zenith-magenta-600);
+ background: var(--zenith-brand-gradient);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.hero-lede {
+ max-width: 590px;
+ margin: 0 0 26px;
+ font-size: 17px;
+ line-height: 1.75;
+}
-.tech-bar {
+.hero-actions {
display: flex;
- justify-content: center;
align-items: center;
- gap: 2rem;
flex-wrap: wrap;
- padding: 1rem 2rem;
- background: var(--surface);
- border-radius: var(--radius-lg);
- border: 1px solid var(--border);
- margin-bottom: 2rem;
+ gap: 12px;
}
-.tech-bar-group {
- display: flex;
+.landing-button {
+ display: inline-flex;
align-items: center;
- gap: 0.5rem;
+ justify-content: center;
+ min-height: 46px;
+ padding: 0 19px;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 750;
+ line-height: 1;
+ text-decoration: none !important;
+ white-space: nowrap;
+ transition: color 170ms ease, background 170ms ease, border-color 170ms ease, box-shadow 170ms ease, transform 170ms ease;
}
-.tech-bar-label {
- font-size: 0.7rem;
- font-weight: 600;
- color: var(--text-tertiary);
- text-transform: uppercase;
- letter-spacing: 0.1em;
+.landing-button-primary {
+ color: #ffffff !important;
+ background: var(--zenith-brand-gradient);
+ box-shadow: 0 8px 18px rgba(181, 12, 189, 0.22);
}
-.tech-bar-items {
- font-size: 0.875rem;
- font-weight: 600;
- background: var(--gradient-brand);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-}
+ .landing-button-primary:hover {
+ color: #ffffff !important;
+ box-shadow: 0 11px 24px rgba(181, 12, 189, 0.3);
+ transform: translateY(-2px);
+ }
-.tech-bar-divider {
- width: 1px;
- height: 20px;
- background: var(--border-strong);
+.landing-button-secondary {
+ color: var(--zenith-navy-900) !important;
+ background: rgba(255, 255, 255, 0.86);
+ border-color: rgba(30, 44, 76, 0.13);
+ box-shadow: var(--zenith-shadow-small);
}
-/* ==========================================================================
- 11. Highlight Grid
- ========================================================================== */
+[data-bs-theme="dark"] .landing-button-secondary {
+ color: var(--zenith-text) !important;
+ background: rgba(27, 29, 38, 0.9);
+ border-color: var(--zenith-border-strong);
+}
-.highlight-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 0.75rem;
- margin: 2rem 0;
+.landing-button-secondary:hover {
+ color: var(--zenith-purple-700) !important;
+ background: var(--zenith-surface);
+ border-color: rgba(113, 56, 232, 0.36);
+ transform: translateY(-2px);
}
-.highlight-item {
+.backend-support {
display: flex;
- align-items: flex-start;
- gap: 0.875rem;
- padding: 1.125rem 1.25rem;
- background: var(--surface);
- border-radius: var(--radius-md);
- border: 1px solid var(--border);
- transition: all var(--duration) var(--ease);
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 30px;
}
- .highlight-item:hover {
- border-color: var(--border-strong);
- box-shadow: var(--shadow-sm);
- }
+.backend-label {
+ margin-right: 3px;
+ color: var(--zenith-text-faint);
+ font-size: 11px;
+ font-weight: 650;
+}
-.highlight-icon {
- font-size: 1.2rem;
+.backend-pill {
+ display: inline-flex;
+ align-items: center;
+ min-height: 26px;
+ padding: 3px 10px;
+ color: #384966;
+ background: rgba(255, 255, 255, 0.82);
+ border: 1px solid rgba(26, 46, 79, 0.09);
+ border-radius: 999px;
+ font-size: 10px;
+ font-weight: 750;
line-height: 1;
- flex-shrink: 0;
- margin-top: 1px;
}
-.highlight-text {
- font-size: 0.875rem;
- color: var(--text-secondary);
- line-height: 1.55;
+[data-bs-theme="dark"] .backend-pill {
+ color: var(--zenith-text-soft);
+ background: rgba(24, 26, 34, 0.82);
+ border-color: var(--zenith-border);
}
- .highlight-text strong {
- color: var(--text-primary);
- font-weight: 600;
- }
-
/* ==========================================================================
- 12. Feature List
+ Documentation shell
========================================================================== */
-.feature-list {
- display: flex;
- flex-direction: column;
- gap: 0.375rem;
- margin: 2rem 0;
+body:not([data-layout="landing"]) > main.container-xxl {
+ display: grid;
+ grid-template-columns: var(--zenith-sidebar-width) minmax(0, 1fr) var(--zenith-affix-width);
+ width: 100%;
+ max-width: var(--zenith-docs-width) !important;
+ min-height: calc(100vh - var(--zenith-header-height));
+ margin: 0 auto;
+ padding: 0 var(--zenith-page-gutter) !important;
+ column-gap: var(--zenith-column-gap);
}
-.feature-list-item {
- display: flex;
- align-items: baseline;
- gap: 1rem;
- padding: 0.75rem 1.125rem;
- background: var(--surface);
- border-radius: var(--radius-sm);
- border: 1px solid var(--border);
- transition: all var(--duration) var(--ease);
+body:is([data-yaml-mime="ManagedReference"], [data-zenith-api]) > main.container-xxl {
+ grid-template-columns: var(--zenith-api-sidebar-width) minmax(0, 1fr) var(--zenith-api-sidebar-width);
}
- .feature-list-item:hover {
- border-color: var(--border-strong);
- background: var(--accent-soft);
- }
+body:not([data-layout="landing"]) > main.container-xxl > .toc-offcanvas {
+ width: 100% !important;
+ max-width: none;
+}
-.feature-list-title {
- font-size: 0.875rem;
- font-weight: 600;
- color: var(--text-primary);
- white-space: nowrap;
- min-width: 140px;
+body:not([data-layout="landing"]) main > .content {
+ grid-column: 2;
+ min-width: 0;
+ width: auto;
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+ padding: 24px 0 70px;
}
-.feature-list-desc {
- font-size: 0.875rem;
- color: var(--text-secondary);
- line-height: 1.55;
+body:not([data-layout="landing"]) main > .affix {
+ grid-column: 3;
+ min-width: 0;
+ width: 100% !important;
+ max-width: 100%;
+ padding-top: 16px;
}
-/* ==========================================================================
- 13. CTA Section
- ========================================================================== */
+body[data-yaml-mime="ManagedReference"]:not([data-search]) > main.container-xxl > .affix {
+ position: sticky;
+ top: var(--zenith-header-height);
+ align-self: start;
+ height: calc(100vh - var(--zenith-header-height));
+ max-height: none;
+ overflow: visible;
+}
-.cta-section {
- display: flex;
- align-items: stretch;
- gap: 2.5rem;
- padding: 2rem 2.25rem;
- background: var(--gradient-subtle);
- border-radius: var(--radius-lg);
- border: 1px solid var(--border);
- margin: 2rem 0;
- position: relative;
- overflow: hidden;
+.actionbar {
+ min-height: 34px;
+ margin-bottom: 18px;
}
- /* Subtle accent line at top */
- .cta-section::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 2px;
- background: var(--gradient-brand);
- }
+.breadcrumb {
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ font-size: 12px;
+}
-.cta-main {
- flex: 1;
+.breadcrumb-item,
+.breadcrumb-item a {
+ color: var(--zenith-text-faint);
+ text-decoration: none !important;
}
- .cta-main h3 {
- font-size: 1.375rem;
- font-weight: 700;
- color: var(--text-primary);
- margin: 0 0 0.5rem;
- letter-spacing: -0.01em;
+ .breadcrumb-item.active,
+ .breadcrumb-item a:hover {
+ color: var(--zenith-purple-700);
}
- .cta-main p {
- font-size: 0.95rem;
- color: var(--text-secondary);
- margin: 0 0 1.25rem;
- line-height: 1.6;
+ .breadcrumb-item + .breadcrumb-item::before {
+ color: var(--zenith-border-strong);
}
- .cta-main .btn-primary {
- display: inline-flex;
- align-items: center;
- padding: 0.625rem 1.5rem;
- font-size: 0.9rem;
- font-weight: 600;
- text-decoration: none;
- border-radius: var(--radius-sm) !important;
- border: none;
- outline: none;
- background: var(--gradient-brand);
- color: #FFFFFF;
- transition: all var(--duration) var(--ease);
- box-shadow: var(--shadow-sm);
- }
-
- .cta-main .btn-primary:hover {
- box-shadow: var(--shadow-md), 0 0 20px rgba(37,99,235,0.18);
- transform: translateY(-1px);
- }
+body:not([data-layout="landing"]) article {
+ width: 100%;
+ margin: 0 auto;
+}
- .cta-main .btn-primary:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 2px;
- }
+article > h1:first-child,
+article > h1:first-of-type {
+ margin-top: 0;
+}
-.cta-links {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- justify-content: center;
- margin-left: auto;
- gap: 0.5rem;
+article h1 {
+ margin: 0 0 18px;
+ padding: 0 0 16px;
+ border-bottom: 1px solid var(--zenith-border);
+ font-size: 36px;
+ font-weight: 800;
+ line-height: 1.18;
}
-.cta-link {
- display: flex;
- align-items: center;
- justify-content: flex-start;
- gap: 0.625rem;
- padding: 0.625rem 1.125rem;
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- text-decoration: none;
- transition: all var(--duration) var(--ease);
- min-width: 200px;
+article h2 {
+ position: relative;
+ margin: 42px 0 17px;
+ padding: 0 0 10px;
+ border-bottom: 1px solid var(--zenith-border);
+ font-size: 25px;
+ line-height: 1.25;
}
- .cta-link:hover {
- border-color: var(--accent-ring);
- background: var(--accent-soft);
- text-decoration: none;
- transform: translateY(-1px);
- box-shadow: var(--shadow-sm);
+ article h2::after {
+ content: "";
+ position: absolute;
+ bottom: -1px;
+ left: 0;
+ width: 52px;
+ height: 2px;
+ background: var(--zenith-brand-gradient);
+ border-radius: 2px;
+ transform: scaleX(1);
+ transform-origin: left center;
+ transition: transform 520ms var(--zenith-ease);
}
-.cta-link-icon {
- font-size: 1rem;
+html.zenith-motion-ready article h2.zenith-reveal:not(.is-visible)::after {
+ transform: scaleX(0);
+ transition: none;
}
-.cta-link-text {
- font-size: 0.875rem;
- font-weight: 600;
- color: var(--text-primary);
+html.zenith-motion-ready article h2.is-visible::after {
+ transform: scaleX(1);
}
-/* ==========================================================================
- 14. Article Content
- ========================================================================== */
+article h3 {
+ margin: 30px 0 12px;
+ font-size: 19px;
+ line-height: 1.35;
+}
-article {
- font-size: 0.95rem;
- color: var(--text-primary);
- line-height: 1.75;
+article h4 {
+ margin: 24px 0 10px;
+ font-size: 16px;
}
- article h1 {
- font-size: 2rem;
- font-weight: 750;
- color: var(--text-primary);
- margin: 0 0 1.5rem;
- padding-bottom: 0.75rem;
- border-bottom: 1px solid var(--border);
- letter-spacing: -0.025em;
- }
+article p,
+article li,
+article dd,
+article td {
+ color: var(--zenith-text-soft);
+}
- article h2 {
- font-size: 1.4rem;
- font-weight: 700;
- color: var(--text-primary);
- margin: 2.5rem 0 1rem;
- position: relative;
- padding-left: 0.875rem;
- letter-spacing: -0.02em;
- }
+article p {
+ margin-bottom: 16px;
+}
- article h2::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0.15em;
- bottom: 0.15em;
- width: 3px;
- background: var(--gradient-brand);
- border-radius: 2px;
- }
+article strong {
+ color: var(--zenith-text);
+ font-weight: 750;
+}
- article h3 {
- font-size: 1.15rem;
- font-weight: 650;
- color: var(--text-primary);
- margin: 2rem 0 0.625rem;
- letter-spacing: -0.01em;
- }
+article ul,
+article ol {
+ padding-left: 1.35rem;
+}
- article h4 {
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 1.5rem 0 0.5rem;
- }
+article li {
+ margin: 5px 0;
+}
- article p {
- margin: 0 0 1rem;
- color: var(--text-primary);
+ article li::marker {
+ color: var(--zenith-purple-700);
}
- article a {
- color: var(--accent);
- text-decoration: none;
- transition: color var(--duration) var(--ease);
- }
+article img:not(.svg) {
+ border-radius: 7px;
+}
- article a:hover {
- color: var(--accent-hover);
- text-decoration: underline;
- text-underline-offset: 2px;
- }
+article a:not(.btn):not(.landing-button) {
+ font-weight: 600;
+}
- article ul,
- article ol {
- margin: 0 0 1rem;
- padding-left: 1.5rem;
- }
+article .anchorjs-link {
+ display: none !important;
+}
+
+article hr {
+ margin: 34px 0;
+ border-color: var(--zenith-border);
+ opacity: 1;
+}
+
+article blockquote {
+ margin: 22px 0;
+ padding: 14px 18px;
+ color: var(--zenith-text-soft);
+ background: var(--zenith-surface-tint);
+ border-left: 3px solid var(--zenith-purple-700);
+ border-radius: 0 6px 6px 0;
+}
- article li {
- margin-bottom: 0.375rem;
- color: var(--text-primary);
+ article blockquote > :last-child {
+ margin-bottom: 0;
}
- /* Code blocks */
- article pre {
- position: relative;
- border-radius: var(--radius-md);
- border: 1px solid var(--border);
- background: var(--surface-sunken);
- margin: 1.5rem 0;
- overflow: hidden;
- box-shadow: var(--shadow-xs);
+code {
+ padding: 0.14em 0.38em;
+ color: var(--zenith-inline-code);
+ background: var(--zenith-inline-code-background);
+ border-radius: 4px;
+ font-family: "Cascadia Code", "Cascadia Mono", Consolas, monospace;
+ font-size: 0.88em;
+}
+
+article pre,
+.codewrapper pre {
+ position: relative;
+ margin: 20px 0;
+ padding: 22px 24px;
+ overflow: auto;
+ color: var(--zenith-code-text);
+ background: var(--zenith-code-surface) !important;
+ border: 1px solid var(--zenith-code-border) !important;
+ border-radius: 6px !important;
+ box-shadow: none;
+ font-family: "Cascadia Code", "Cascadia Mono", Consolas, monospace;
+ font-size: 13px;
+ line-height: 1.7;
+}
+
+.doc-code-frame {
+ position: relative;
+ margin: var(--zenith-flow-space) 0;
+ overflow: hidden;
+ color: var(--zenith-code-text);
+ background: var(--zenith-code-surface);
+ border: 1px solid var(--zenith-code-border);
+ border-radius: var(--zenith-panel-radius);
+ transition: border-color 180ms ease, box-shadow 220ms ease;
+}
+
+ .doc-code-frame::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 2;
+ height: 2px;
+ pointer-events: none;
+ background: var(--zenith-brand-gradient);
+ opacity: 0;
+ transform: scaleX(0.35);
+ transform-origin: left center;
+ transition: opacity 180ms ease, transform 360ms var(--zenith-ease);
}
- article pre .code-action,
- article pre .btn.code-action,
- article pre button.btn {
- position: absolute !important;
- top: 0.5rem !important;
- right: 0.5rem !important;
- z-index: 10 !important;
- padding: 0.25rem 0.5rem !important;
- }
+ .doc-code-frame:hover,
+ .doc-code-frame:focus-within {
+ border-color: var(--zenith-code-accent-border);
+ box-shadow: 0 10px 26px rgba(35, 30, 46, 0.08);
+ }
- article pre code {
- display: block;
- padding: 1.125rem;
- margin: 0;
- overflow-x: auto;
- font-size: 0.8125rem;
- line-height: 1.7;
- background: transparent;
- }
+[data-bs-theme="dark"] .doc-code-frame:hover,
+[data-bs-theme="dark"] .doc-code-frame:focus-within {
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);
+}
- article pre code.hljs,
- article pre .hljs {
- background: transparent !important;
- }
+.doc-code-frame:hover::before,
+.doc-code-frame:focus-within::before {
+ opacity: 0.86;
+ transform: scaleX(1);
+}
- article pre code::-webkit-scrollbar {
- height: 6px;
- background: transparent;
- }
+.doc-code-toolbar {
+ display: flex;
+ height: var(--zenith-toolbar-height);
+ align-items: center;
+ padding: 0 4px 0 12px;
+ color: var(--zenith-code-muted);
+ background: var(--zenith-code-toolbar);
+ border-bottom: 1px solid var(--zenith-code-border);
+}
- article pre code::-webkit-scrollbar-thumb {
- background: var(--border-strong);
- border-radius: 3px;
- }
+.doc-code-language {
+ font-family: "Cascadia Code", "Cascadia Mono", Consolas, monospace;
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+}
- article pre code::-webkit-scrollbar-thumb:hover {
- background: var(--text-tertiary);
- }
+.doc-code-copy {
+ display: inline-flex;
+ width: var(--zenith-copy-size);
+ min-width: var(--zenith-copy-size);
+ height: var(--zenith-copy-size);
+ align-items: center;
+ justify-content: center;
+ margin-left: auto;
+ padding: 0;
+ color: var(--zenith-code-muted);
+ background: var(--zenith-code-surface);
+ border: 1px solid var(--zenith-code-border);
+ border-radius: var(--zenith-control-radius);
+ font-family: Aptos, "Segoe UI Variable Text", sans-serif;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease, transform 160ms var(--zenith-ease);
+}
- article code {
- font-size: 0.8125rem;
- font-family: 'Cascadia Code', 'JetBrains Mono', 'Fira Code', Consolas, monospace;
+ .doc-code-copy:active {
+ transform: scale(0.92);
}
- /* Inline code */
- article p code,
- article li code,
- article td code {
- background: var(--accent-soft);
- color: var(--accent);
- padding: 0.125rem 0.4rem;
- border-radius: var(--radius-xs);
- font-size: 0.8rem;
- border: 1px solid var(--accent-ring);
- font-weight: 500;
+ .doc-code-copy i {
+ font-size: 13px;
}
- article hr {
- border: none;
- border-top: 1px solid var(--border);
- margin: 2.5rem 0;
+ .doc-code-copy:hover {
+ color: var(--zenith-code-accent);
+ background: var(--zenith-code-hover);
+ border-color: var(--zenith-code-accent-border);
}
- article blockquote {
- position: relative;
- padding: 0.875rem 1.25rem;
- margin: 1.5rem 0;
- background: var(--gradient-subtle);
- border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
- border-left: 3px solid var(--accent);
- }
-
- article blockquote p {
- margin: 0;
- color: var(--text-secondary);
- font-size: 0.9rem;
- line-height: 1.65;
- }
-
- article blockquote p:not(:last-child) {
- margin-bottom: 0.5rem;
- }
-
- /* Images */
- article img:not(#logo):not(.hero-logo):not([src*="shields.io"]):not([src*="badge"]) {
- max-width: 100%;
- height: auto;
- border-radius: var(--radius-md);
- margin: 1.5rem 0;
- box-shadow: var(--shadow-md);
- border: 1px solid var(--border);
- transition: box-shadow var(--duration) var(--ease);
+ .doc-code-copy.is-copied {
+ color: var(--zenith-code-success);
+ background: rgba(15, 118, 110, 0.1);
+ border-color: rgba(15, 118, 110, 0.28);
}
- article img:not(#logo):not(.hero-logo):not([src*="shields.io"]):not([src*="badge"]):hover {
- box-shadow: var(--shadow-lg);
+ .doc-code-copy.is-copied i {
+ animation: zenith-copy-confirm 320ms var(--zenith-ease) both;
}
-#logo,
-.hero-logo,
-.hero-badges img,
-.community-badges img {
- box-shadow: none !important;
- border: none !important;
- transform: none !important;
-}
-
- #logo:hover,
- .hero-badges img:hover,
- .community-badges img:hover {
- box-shadow: none !important;
- transform: none !important;
+ .doc-code-copy.is-failed {
+ color: #b42318;
+ background: rgba(180, 35, 24, 0.08);
+ border-color: rgba(180, 35, 24, 0.24);
}
-article figure {
- margin: 1.5rem 0;
- padding: 0;
+article .doc-code-frame > pre,
+.codewrapper .doc-code-frame > pre {
+ margin: 0;
+ padding: var(--zenith-code-padding-block) var(--zenith-code-padding-inline);
+ background: transparent !important;
+ border: 0 !important;
+ border-radius: 0 !important;
+ box-shadow: none;
}
- article figure img {
- margin: 0;
- }
-
-article figcaption {
- text-align: center;
- font-size: 0.8125rem;
- color: var(--text-secondary);
- margin-top: 0.5rem;
- font-style: italic;
+article pre code,
+.codewrapper pre code {
+ padding: 0;
+ color: inherit;
+ background: transparent;
+ border-radius: 0;
+ font-size: inherit;
}
-/* ==========================================================================
- 15. Table Styles
- ========================================================================== */
-
-article .table-responsive,
-article > table {
- margin: 1.5rem 0 !important;
+.shiki,
+.shiki span {
+ color: var(--shiki-light, var(--zenith-code-text));
+ background-color: transparent !important;
+ font-style: var(--shiki-light-font-style, inherit);
+ font-weight: var(--shiki-light-font-weight, inherit);
+ text-decoration: var(--shiki-light-text-decoration, inherit);
}
-article table {
- width: 100% !important;
- margin: 0 !important;
- font-size: 0.875rem !important;
- border-collapse: separate !important;
- border-spacing: 0 !important;
- border: 1px solid var(--border) !important;
- border-radius: var(--radius-md) !important;
- background: var(--surface) !important;
- box-shadow: var(--shadow-xs) !important;
+[data-bs-theme="dark"] .shiki,
+[data-bs-theme="dark"] .shiki span {
+ color: var(--shiki-dark, var(--zenith-code-text));
+ font-style: var(--shiki-dark-font-style, inherit);
+ font-weight: var(--shiki-dark-font-weight, inherit);
+ text-decoration: var(--shiki-dark-text-decoration, inherit);
}
-.table-responsive {
- overflow: visible !important;
+.codewrapper {
+ margin: 18px 0;
}
-article thead {
- background: var(--surface-sunken) !important;
-}
+ .codewrapper .btn-copy,
+ pre .btn-copy {
+ color: var(--zenith-code-muted) !important;
+ background: var(--zenith-code-toolbar) !important;
+ border: 1px solid var(--zenith-code-border) !important;
+ border-radius: 5px !important;
+ }
-article th {
- padding: 0.75rem 1rem !important;
- text-align: center !important;
- font-size: 0.75rem !important;
- font-weight: 600 !important;
- text-transform: uppercase !important;
- letter-spacing: 0.06em !important;
- color: var(--text-tertiary) !important;
- background: transparent !important;
- border: none !important;
- white-space: nowrap !important;
- vertical-align: middle !important;
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-section,
+.hljs-link {
+ color: var(--zenith-syntax-keyword);
}
-article thead tr th:first-child {
- border-top-left-radius: var(--radius-md) !important;
+.hljs-literal {
+ color: var(--zenith-syntax-literal);
}
-article thead tr th:last-child {
- border-top-right-radius: var(--radius-md) !important;
+.hljs-string,
+.hljs-symbol,
+.hljs-bullet,
+.hljs-addition,
+.hljs-template-tag,
+.hljs-template-variable {
+ color: var(--zenith-syntax-string);
}
-article tbody tr {
- transition: background var(--duration) var(--ease) !important;
+.hljs-title,
+.hljs-name,
+.hljs-type,
+.hljs-attribute,
+.hljs-built_in {
+ color: var(--zenith-syntax-type);
}
- article tbody tr:hover {
- background: var(--accent-soft) !important;
+ .hljs-title.function_,
+ .hljs-function > .hljs-title,
+ .hljs-property,
+ .hljs-attr {
+ color: var(--zenith-syntax-function);
}
-article td {
- padding: 0.625rem 1rem !important;
- text-align: left !important;
- color: var(--text-primary) !important;
- background: transparent !important;
- border: none !important;
- border-top: 1px solid var(--border) !important;
- vertical-align: middle !important;
+.hljs-variable,
+.hljs-params,
+.hljs-subst {
+ color: var(--zenith-syntax-variable);
}
-article tbody tr:first-child td {
- border-top: none !important;
+.hljs-comment,
+.hljs-quote,
+.hljs-deletion {
+ color: var(--zenith-syntax-comment);
}
-article tbody tr:last-child td:first-child {
- border-bottom-left-radius: var(--radius-md) !important;
+.hljs-number {
+ color: var(--zenith-syntax-number);
}
-article tbody tr:last-child td:last-child {
- border-bottom-right-radius: var(--radius-md) !important;
+.hljs-meta,
+.hljs-meta-keyword,
+.hljs-doctag {
+ color: var(--zenith-syntax-meta);
}
-article th:not(:last-child),
-article td:not(:last-child) {
- background-image: linear-gradient(to bottom, transparent 15%, var(--border) 15%, var(--border) 85%, transparent 85%) !important;
- background-size: 1px 100% !important;
- background-position: right !important;
- background-repeat: no-repeat !important;
+.hljs-symbol,
+.hljs-regexp {
+ color: var(--zenith-syntax-symbol);
}
-article td a {
- white-space: nowrap !important;
+.hljs-operator,
+.hljs-punctuation {
+ color: var(--zenith-code-muted);
}
/* ==========================================================================
- 16. Status Badges
+ Tables, alerts, tabs, and API surfaces
========================================================================== */
-.status-yes,
-.status-no {
- display: inline-block;
- padding: 0.15rem 0.5rem;
- border-radius: var(--radius-full);
- font-size: 0.75rem;
- font-weight: 600;
+.doc-table-frame {
+ position: relative;
+ margin: var(--zenith-flow-space) 0;
+ overflow: hidden;
+ background: var(--zenith-table-surface);
+ border: 1px solid var(--zenith-table-border);
+ border-radius: var(--zenith-panel-radius);
+ transition: border-color 180ms ease, box-shadow 220ms ease;
}
-.status-yes {
- background: var(--accent-soft);
- color: var(--accent);
- border: 1px solid var(--accent-ring);
-}
+ .doc-table-frame:hover,
+ .doc-table-frame:focus-within {
+ border-color: color-mix(in srgb, var(--zenith-purple-700) 34%, var(--zenith-table-border));
+ box-shadow: 0 10px 24px rgba(35, 30, 46, 0.06);
+ }
-.status-no {
- background: var(--surface-sunken);
- color: var(--text-tertiary);
- border: 1px solid var(--border);
-}
+ .doc-table-frame::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 2;
+ width: 22px;
+ pointer-events: none;
+ background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--zenith-table-surface) 94%, transparent));
+ opacity: 0;
+ transition: opacity 180ms ease;
+ }
-/* ==========================================================================
- 17. Alerts
- ========================================================================== */
+ .doc-table-frame.is-scrollable:not(.is-scroll-end)::after {
+ opacity: 1;
+ }
-.alert {
- padding: 0.875rem 1.125rem;
- margin: 1.5rem 0;
- border-radius: var(--radius-sm);
- border-left-width: 3px !important;
- font-size: 0.875rem;
- line-height: 1.6;
- color: var(--text-primary);
+ .doc-table-frame > .table-responsive {
+ margin: 0;
+ overflow-x: auto;
+ border: 0;
+ border-radius: 0;
+ }
+
+article .doc-table-frame table,
+article .doc-table-frame .table {
+ --bs-table-color: var(--zenith-table-text);
+ --bs-table-bg: var(--zenith-table-surface);
+ --bs-table-border-color: var(--zenith-table-border);
+ --bs-table-striped-bg: var(--zenith-table-stripe);
+ --bs-table-hover-bg: var(--zenith-table-hover);
+ width: 100%;
+ min-width: 560px;
+ margin: 0;
+ color: var(--zenith-table-text);
+ background: var(--zenith-table-surface);
+ border: 0;
+ border-collapse: collapse;
+ border-radius: 0;
+ font-size: 14px;
+ line-height: 1.5;
}
- .alert p:last-child {
- margin-bottom: 0;
- }
-
- .alert a {
- font-weight: 600;
- text-decoration: underline;
- text-underline-offset: 2px;
+ article .doc-table-frame table > :not(caption) > * {
+ border: 0;
}
- .alert a:hover {
- text-decoration: none;
+ article .doc-table-frame table > :not(caption) > * > * {
+ box-shadow: none;
}
-.alert-primary {
- background: rgba(37, 99, 235, 0.05);
- border: 1px solid rgba(37, 99, 235, 0.15);
- border-left-color: var(--accent) !important;
+article .doc-table-frame th {
+ padding: var(--zenith-table-padding-block) var(--zenith-table-padding-inline);
+ color: var(--zenith-table-heading);
+ background: var(--zenith-table-header);
+ border: 0;
+ border-right: 1px solid var(--zenith-table-border);
+ border-bottom: 1px solid var(--zenith-table-border);
+ font-size: 12px;
+ font-weight: 700;
+ text-align: left;
+ text-transform: none;
+ vertical-align: bottom;
}
- .alert-primary a {
- color: var(--accent);
- }
-
-.alert-secondary {
- background: var(--surface-sunken);
- border: 1px solid var(--border);
- border-left-color: var(--border-strong) !important;
+article .doc-table-frame td {
+ padding: var(--zenith-table-padding-block) var(--zenith-table-padding-inline);
+ background: var(--zenith-table-surface);
+ border: 0;
+ border-right: 1px solid var(--zenith-table-border);
+ border-bottom: 1px solid var(--zenith-table-border);
+ vertical-align: middle;
+ transition: background-color 160ms ease, color 160ms ease;
}
- .alert-secondary a {
- color: var(--text-secondary);
+ article .doc-table-frame th:last-child,
+ article .doc-table-frame td:last-child {
+ border-right: 0;
}
-.alert-success {
- background: rgba(22, 163, 74, 0.05);
- border: 1px solid rgba(22, 163, 74, 0.15);
- border-left-color: #16A34A !important;
+article .doc-table-frame tbody tr:nth-child(even) td {
+ background: var(--zenith-table-stripe);
}
- .alert-success a {
- color: #16A34A;
- }
+article .doc-table-frame tbody tr:last-child td {
+ border-bottom: 0;
+}
-.alert-danger {
- background: rgba(220, 38, 38, 0.05);
- border: 1px solid rgba(220, 38, 38, 0.15);
- border-left-color: #DC2626 !important;
+article .doc-table-frame tbody tr:hover td {
+ background: var(--zenith-table-hover);
}
- .alert-danger a {
- color: #DC2626;
- }
+article .doc-table-frame tbody td:first-child {
+ color: var(--zenith-table-heading);
+ font-weight: 650;
+}
-.alert-warning {
- background: rgba(217, 119, 6, 0.05);
- border: 1px solid rgba(217, 119, 6, 0.15);
- border-left-color: #D97706 !important;
+article .doc-table-frame td > code:only-child {
+ white-space: nowrap;
}
- .alert-warning a {
- color: #D97706;
+.alert {
+ position: relative;
+ margin: var(--zenith-flow-space) 0;
+ padding: 16px 18px;
+ color: var(--zenith-text-soft);
+ background: var(--zenith-surface-soft);
+ border: 1px solid var(--zenith-border);
+ border-left-width: 4px;
+ border-radius: var(--zenith-panel-radius);
+ font-size: 14px;
+ transition: border-color 180ms ease, box-shadow 220ms ease, transform 220ms var(--zenith-ease);
+}
+
+ .alert:hover,
+ .alert:focus-within {
+ box-shadow: 0 8px 22px rgba(35, 30, 46, 0.07);
+ transform: translateY(-1px);
}
-.alert-info {
- background: rgba(6, 182, 212, 0.05);
- border: 1px solid rgba(6, 182, 212, 0.15);
- border-left-color: #0891B2 !important;
+[data-bs-theme="dark"] .alert:hover,
+[data-bs-theme="dark"] .alert:focus-within {
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2);
}
- .alert-info a {
- color: #0891B2;
- }
+.alert > h5,
+.alert > .alert-title {
+ margin-bottom: 5px;
+ color: var(--zenith-text);
+ font-size: 13px;
+ font-weight: 850;
+}
-.alert-light {
- background: var(--surface-sunken);
- border: 1px solid var(--border);
- border-left-color: var(--border-strong) !important;
+.alert-info,
+.alert-note {
+ background: color-mix(in srgb, var(--zenith-info) 8%, transparent);
+ border-left-color: var(--zenith-info);
}
- .alert-light a {
- color: var(--accent);
- }
+.alert-tip,
+.alert-success {
+ background: color-mix(in srgb, var(--zenith-success) 8%, transparent);
+ border-left-color: var(--zenith-success);
+}
-.alert-dark {
- background: var(--surface-sunken);
- border: 1px solid var(--border-strong);
- border-left-color: var(--text-secondary) !important;
+.alert-warning,
+.alert-important {
+ background: color-mix(in srgb, var(--zenith-warning) 8%, transparent);
+ border-left-color: var(--zenith-warning);
}
- .alert-dark a {
- color: var(--text-primary);
- }
+.alert-danger,
+.alert-caution {
+ background: color-mix(in srgb, var(--zenith-danger) 8%, transparent);
+ border-left-color: var(--zenith-danger);
+}
-/* Dark mode alert overrides */
-[data-bs-theme="dark"] .alert-primary {
- background: rgba(96, 165, 250, 0.06);
- border-color: rgba(96, 165, 250, 0.18);
- border-left-color: var(--accent) !important;
+.tabGroup {
+ margin: var(--zenith-flow-space) 0;
+ border: 1px solid var(--zenith-border);
+ border-radius: var(--zenith-panel-radius);
+ overflow: hidden;
}
- [data-bs-theme="dark"] .alert-primary a {
- color: var(--accent);
+ .tabGroup > ul.nav-tabs {
+ padding: 0 10px;
+ background: var(--zenith-surface-soft);
+ border-bottom: 1px solid var(--zenith-border);
}
-[data-bs-theme="dark"] .alert-success {
- background: rgba(74, 222, 128, 0.06);
- border-color: rgba(74, 222, 128, 0.18);
- border-left-color: #4ADE80 !important;
-}
+ .tabGroup > ul.nav-tabs > li > a {
+ padding: 10px 12px;
+ color: var(--zenith-text-soft);
+ border: 0;
+ border-bottom: 2px solid transparent;
+ font-size: 13px;
+ text-decoration: none;
+ }
+
+ .tabGroup > ul.nav-tabs > li.active > a,
+ .tabGroup > ul.nav-tabs > li > a:hover {
+ color: var(--zenith-purple-700);
+ border-bottom-color: var(--zenith-purple-700);
+ }
- [data-bs-theme="dark"] .alert-success a {
- color: #86EFAC;
+ .tabGroup > section {
+ padding: 18px;
}
-[data-bs-theme="dark"] .alert-danger {
- background: rgba(248, 113, 113, 0.06);
- border-color: rgba(248, 113, 113, 0.18);
- border-left-color: #F87171 !important;
+article .inheritance,
+article .implements,
+article .inheritedMembers,
+article .derivedClasses {
+ color: var(--zenith-text-soft);
}
- [data-bs-theme="dark"] .alert-danger a {
- color: #FCA5A5;
- }
-
-[data-bs-theme="dark"] .alert-warning {
- background: rgba(251, 191, 36, 0.06);
- border-color: rgba(251, 191, 36, 0.18);
- border-left-color: #FBBF24 !important;
+article dl.typelist,
+article dl.parameters,
+article dl.returns {
+ margin: 18px 0;
}
- [data-bs-theme="dark"] .alert-warning a {
- color: #FCD34D;
- }
+article dl dt {
+ color: var(--zenith-text);
+ font-weight: 750;
+}
-[data-bs-theme="dark"] .alert-info {
- background: rgba(34, 211, 238, 0.06);
- border-color: rgba(34, 211, 238, 0.18);
- border-left-color: #22D3EE !important;
+article .section {
+ scroll-margin-top: calc(var(--zenith-header-height) + 20px);
}
- [data-bs-theme="dark"] .alert-info a {
- color: #67E8F9;
- }
+article h3[id],
+article a[data-uid] + h3 {
+ padding-top: 5px;
+}
/* ==========================================================================
- 18. Sidebar Navigation
- ========================================================================== */
-
-.sidebar,
-.sidefilter {
- background: transparent !important;
+ Side navigation and in-page navigation
+ ========================================================================== */
+
+#sidetoggle {
+ position: sticky;
+ top: var(--zenith-header-height);
+ align-self: start;
+ height: calc(100vh - var(--zenith-header-height));
+ overflow: hidden;
+ border-right: 1px solid var(--zenith-border);
}
-.affix {
- position: sticky !important;
- top: 80px !important;
- height: fit-content !important;
- max-height: calc(100vh - 100px) !important;
- overflow-y: auto !important;
- background: var(--surface) !important;
- border: 1px solid var(--border) !important;
- border-radius: var(--radius-md) !important;
- box-shadow: var(--shadow-xs) !important;
- padding: 0.75rem !important;
- margin-top: 1rem !important;
-}
-
- .affix > h5,
- .affix .border-bottom {
- display: block !important;
- width: 100% !important;
- text-align: left !important;
- font-size: 0.6875rem !important;
- font-weight: 600 !important;
- color: var(--text-tertiary) !important;
- margin: 0 0 0.5rem !important;
- padding: 0 0.25rem 0.5rem !important;
- border: none !important;
- border-bottom: 1px solid var(--border) !important;
- text-transform: uppercase;
- letter-spacing: 0.08em;
- }
-
- .affix ul {
- display: flex !important;
- flex-direction: column !important;
- gap: 1px !important;
- padding: 0 !important;
- margin: 0 !important;
- list-style: none !important;
+ #sidetoggle > div {
+ height: 100%;
}
- .affix ul li {
- margin: 0 !important;
- padding: 0 !important;
- }
-
- .affix ul li a {
- display: block !important;
- padding: 0.3rem 0.5rem !important;
- margin: 0 !important;
- border-radius: var(--radius-xs) !important;
- font-size: 0.8rem !important;
- font-weight: 500 !important;
- text-decoration: none !important;
- transition: all var(--duration) var(--ease) !important;
- background: transparent !important;
- border: 1px solid transparent !important;
- cursor: pointer !important;
- line-height: 1.4 !important;
- white-space: nowrap !important;
- overflow: hidden !important;
- text-overflow: ellipsis !important;
- }
+.sidefilter {
+ padding: 18px 14px 10px 0;
+}
- .affix .link-body-emphasis {
- color: var(--text-primary) !important;
- }
+.toc-filter {
+ position: relative;
+}
- .affix .link-secondary {
- color: var(--text-secondary) !important;
- font-size: 0.75rem !important;
- font-weight: 400 !important;
+ .toc-filter > input,
+ #toc_filter_input {
+ width: 100%;
+ height: 34px;
+ padding: 7px 12px 7px 33px;
+ color: var(--zenith-text);
+ background: var(--zenith-surface-soft);
+ border: 1px solid var(--zenith-border);
+ border-radius: 6px;
+ box-shadow: none;
+ font-size: 12px;
}
- .affix ul li a:hover,
- .affix ul li.active > a,
- .affix ul li a.active {
- background: var(--accent-soft) !important;
- color: var(--accent) !important;
+ .toc-filter .filter-icon {
+ position: absolute;
+ top: 50%;
+ left: 11px;
+ z-index: 1;
+ display: inline-flex;
+ width: 16px;
+ height: 16px;
+ align-items: center;
+ justify-content: center;
+ color: var(--zenith-text-faint);
+ line-height: 1;
+ transform: translateY(-50%);
}
- .affix ul ul {
- margin-left: 0.625rem !important;
- padding-left: 0 !important;
- border-left: none !important;
- gap: 0 !important;
- }
+.sidetoc {
+ height: calc(100% - 62px);
+ padding: 4px 12px 30px 0;
+ overflow: auto;
+}
.toc {
- padding: 0.5rem !important;
-}
-
- .toc .nav > li > a,
- .toc .nav > li > .expand-stub + a {
- padding: 0.4rem 0.75rem !important;
- margin: 1px 0 !important;
- border-radius: var(--radius-sm) !important;
- color: var(--text-secondary) !important;
- font-size: 0.8125rem !important;
- font-weight: 500 !important;
- transition: all var(--duration) var(--ease) !important;
- display: block !important;
- text-decoration: none !important;
- background: transparent !important;
- }
+ padding: 0 16px 28px;
+ font-size: 13px;
+}
- .toc .nav > li > a:hover,
- .toc .nav > li > .expand-stub + a:hover {
- background: var(--accent-soft) !important;
- color: var(--accent) !important;
- }
+#toc.zenith-scroll-host > .overflow-y-auto,
+main > .affix.zenith-scroll-host > #affix {
+ scrollbar-width: none;
+}
- .toc .nav > li.active > a,
- .toc .nav > li.active > .expand-stub + a,
- .toc .nav > li > a.active {
- background: var(--accent-soft-mid) !important;
- color: var(--accent) !important;
- font-weight: 600 !important;
- }
+#toc > .overflow-y-auto {
+ overflow-x: hidden !important;
+}
- .toc .nav > li.active > a::before,
- .toc .nav > li > a.active::before {
- content: '' !important;
- position: absolute !important;
- left: 0 !important;
- top: 20% !important;
- bottom: 20% !important;
- width: 2px !important;
- background: var(--accent) !important;
- border-radius: 0 2px 2px 0 !important;
- }
+#toc.zenith-scroll-host > .overflow-y-auto::-webkit-scrollbar,
+main > .affix.zenith-scroll-host > #affix::-webkit-scrollbar {
+ display: none;
+ width: 0;
+ height: 0;
+}
- .toc .nav > li > .expand-stub {
- width: 26px !important;
- height: 26px !important;
- display: inline-flex !important;
- align-items: center !important;
- justify-content: center !important;
- border-radius: var(--radius-xs) !important;
- cursor: pointer !important;
- transition: all var(--duration) var(--ease) !important;
- margin-right: 2px !important;
- flex-shrink: 0 !important;
- background: transparent !important;
- }
+.zenith-scroll-host {
+ position: relative;
+}
- .toc .nav > li > .expand-stub:hover {
- background: var(--accent-soft) !important;
- }
+.zenith-overlay-scrollbar {
+ position: absolute;
+ right: var(--zenith-scrollbar-inset);
+ z-index: 8;
+ width: var(--zenith-scrollbar-size);
+ opacity: 0;
+ pointer-events: none;
+ touch-action: none;
+ transition: opacity 160ms ease;
+}
- .toc .nav > li > .expand-stub::before {
- font-size: 0.65rem !important;
- color: var(--text-tertiary) !important;
- transition: transform var(--duration) var(--ease) !important;
- }
+main > .affix > .zenith-overlay-scrollbar {
+ right: calc(-1 * (var(--zenith-scrollbar-size) + var(--zenith-scrollbar-edge-gap)));
+}
+
+.zenith-overlay-scrollbar > span {
+ position: absolute;
+ top: 0;
+ right: var(--zenith-scrollbar-inset);
+ left: var(--zenith-scrollbar-inset);
+ min-height: var(--zenith-scrollbar-min-thumb);
+ background: var(--zenith-scrollbar-thumb);
+ border-radius: 999px;
+ cursor: grab;
+ transition: background-color 160ms ease;
+}
- .toc .nav > li.in > .expand-stub::before,
- .toc .nav > li.expanded > .expand-stub::before {
- transform: rotate(90deg) !important;
+.zenith-scroll-host.has-scroll-range:hover > .zenith-overlay-scrollbar,
+.zenith-scroll-host.has-scroll-range:focus-within > .zenith-overlay-scrollbar,
+.zenith-scroll-host.has-scroll-range.is-scrolling > .zenith-overlay-scrollbar,
+.zenith-overlay-scrollbar.is-dragging {
+ opacity: 1;
+ pointer-events: auto;
+}
+
+ .zenith-overlay-scrollbar > span:hover,
+ .zenith-overlay-scrollbar.is-dragging > span {
+ background: var(--zenith-scrollbar-thumb-hover);
}
- .toc .nav .nav {
- padding-left: 0.625rem !important;
- margin-left: 0.625rem !important;
- border-left: 1px solid var(--border) !important;
+ .zenith-overlay-scrollbar.is-dragging > span {
+ cursor: grabbing;
}
- .toc .nav .nav > li > a {
- font-size: 0.78rem !important;
- padding: 0.35rem 0.625rem !important;
- color: var(--text-secondary) !important;
- border-radius: var(--radius-xs) !important;
- }
+.zenith-overlay-scrollbar:focus-visible {
+ outline: 0;
+}
- .toc .nav .nav > li > a:hover,
- .toc .nav .nav > li.active > a {
- background: var(--accent-soft) !important;
- color: var(--accent) !important;
- }
+ .zenith-overlay-scrollbar:focus-visible > span {
+ background: var(--zenith-scrollbar-thumb-hover);
+ box-shadow: 0 0 0 1px var(--zenith-accent);
+ }
- .toc .nav .nav > li.active > a {
- font-weight: 600 !important;
+#toc > form.filter {
+ position: relative;
+ margin: 16px 0 18px;
+}
+
+ #toc > form.filter input {
+ height: 36px;
+ padding-left: 36px;
+ color: var(--zenith-text);
+ background: var(--zenith-search-surface);
+ border-color: var(--zenith-search-border);
+ border-radius: 6px;
+ box-shadow: none;
+ font-size: 13px;
+ }
+
+ #navbar form.search > input:focus,
+ #toc > form.filter input:focus,
+ .toc-filter > input:focus,
+ #toc_filter_input:focus {
+ outline: 0 !important;
+ color: var(--zenith-text);
+ background: var(--zenith-surface);
+ border-color: var(--zenith-search-focus);
+ box-shadow: inset 0 0 0 1px rgba(81, 43, 212, 0.22);
}
-/* Sidebar filter / search */
-.sidefilter .filter-input,
-.sidetoc .filter,
-.sidebar input.form-control[type="search"],
-#toc .filter input.form-control {
- background: var(--surface-sunken) !important;
- border: 1px solid var(--border) !important;
- border-radius: var(--radius-sm) !important;
- padding: 0.4rem 0.875rem 0.4rem 2rem !important;
- font-size: 0.8rem !important;
- color: var(--text-primary) !important;
- transition: all var(--duration) var(--ease) !important;
- width: 100% !important;
- box-shadow: none !important;
-}
-
- .sidefilter .filter-input:focus,
- .sidetoc .filter:focus,
- .sidebar input.form-control[type="search"]:focus,
- #toc .filter input.form-control:focus {
- outline: none !important;
- border-color: var(--accent) !important;
- box-shadow: 0 0 0 3px var(--accent-ring) !important;
- background: var(--surface) !important;
+ #toc > form.filter i {
+ position: absolute;
+ top: 50%;
+ left: 12px;
+ z-index: 1;
+ display: inline-flex;
+ width: 16px;
+ height: 16px;
+ align-items: center;
+ justify-content: center;
+ color: var(--zenith-text-faint);
+ line-height: 1;
+ transform: translateY(-50%);
}
- .sidefilter .filter-input::placeholder,
- .sidetoc .filter::placeholder,
- .sidebar input.form-control[type="search"]::placeholder,
- #toc .filter input.form-control::placeholder {
- color: var(--text-tertiary) !important;
- }
+.toc ul {
+ margin: 0;
+ padding-left: 0;
+ list-style: none;
+}
-#toc .filter {
- position: relative !important;
- margin-bottom: 0.75rem !important;
+.toc li > a {
+ white-space: nowrap;
}
- #toc .filter i {
- position: absolute !important;
- left: 0.5rem !important;
- top: 50% !important;
- transform: translateY(-50%) !important;
- color: var(--text-tertiary) !important;
- font-size: 0.8rem !important;
- z-index: 1 !important;
- pointer-events: none !important;
- }
+#toc > .overflow-y-auto > ul > li > a wbr {
+ display: none;
+}
-#toc .flex-fill.overflow-y-auto {
- --toc-expand-width: 16px;
- padding: 0 0.25rem !important;
+.toc ul ul {
+ margin: 3px 0 9px 10px;
+ padding-left: 11px;
+ border-left: 1px solid var(--zenith-border);
}
- #toc .flex-fill.overflow-y-auto ul {
- list-style: none !important;
- padding: 0 !important;
- margin: 0 !important;
- }
+.toc li {
+ position: relative;
+}
- #toc .flex-fill.overflow-y-auto > ul > li {
- margin-top: 1px !important;
- padding: 0 !important;
+ .toc li > a {
+ position: relative;
+ display: block;
+ margin: 1px 0;
+ padding: 6px 9px;
+ color: var(--zenith-text-soft);
+ border-radius: 5px;
+ font-weight: 550;
+ line-height: 1.35;
+ text-decoration: none !important;
+ transition: color 160ms ease, background-color 160ms ease, box-shadow 180ms ease, transform 180ms var(--zenith-ease);
}
- #toc .flex-fill.overflow-y-auto > ul > li:first-child {
- margin-top: 0 !important;
+ .toc li > a:hover {
+ color: var(--zenith-purple-700);
+ background: rgba(113, 56, 232, 0.06);
}
- #toc .flex-fill.overflow-y-auto li {
- margin: 0 !important;
- padding: 0 !important;
- }
-
- #toc .flex-fill.overflow-y-auto li.expander {
- display: flex !important;
- flex-wrap: wrap !important;
- align-items: stretch !important;
- border-radius: var(--radius-xs) !important;
+ .toc li.active:not(.expander) > a,
+ .toc li > a.active {
+ color: var(--zenith-nav-active);
+ background: transparent;
+ font-weight: 750;
+ box-shadow: none;
+ text-decoration: none !important;
}
- #toc .flex-fill.overflow-y-auto li.expander > .expand-stub,
- #toc .flex-fill.overflow-y-auto li.expander > a {
- background: transparent !important;
- border: none !important;
+ .toc li.active:not(.expander) > a::before,
+ .toc li > a.active::before {
+ content: "";
+ position: absolute;
+ top: 50%;
+ bottom: auto;
+ left: 0;
+ width: 2px;
+ height: 14px;
+ background: var(--zenith-purple-700);
+ border-radius: 2px;
+ transform: translateY(-50%);
}
- #toc .flex-fill.overflow-y-auto li.expander:hover > .expand-stub,
- #toc .flex-fill.overflow-y-auto li.expander:hover > a,
- #toc .flex-fill.overflow-y-auto li.expander.active > .expand-stub,
- #toc .flex-fill.overflow-y-auto li.expander.active > a,
- #toc .flex-fill.overflow-y-auto li.expander > a.active {
- background: var(--accent-soft) !important;
- }
+ .toc li.expander > a {
+ color: var(--zenith-text);
+ font-weight: 700;
+ }
- #toc .flex-fill.overflow-y-auto li.expander:hover > a {
- color: var(--accent) !important;
- }
+.toc > div > ul > li:first-child > a {
+ color: var(--zenith-purple-700);
+ font-size: 11px;
+ font-weight: 800;
+ text-transform: uppercase;
+}
- #toc .flex-fill.overflow-y-auto li .expand-stub {
- display: inline-flex !important;
- align-items: center !important;
- justify-content: center !important;
- width: calc(var(--toc-expand-width) + 0.5rem) !important;
- height: auto !important;
- min-height: 26px !important;
- border-radius: var(--radius-xs) 0 0 var(--radius-xs) !important;
- transition: all var(--duration) var(--ease) !important;
- cursor: pointer !important;
- flex-shrink: 0 !important;
- padding: 0.4rem 0 0.4rem 0.4rem !important;
- }
+.toc .expand-stub {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ display: block;
+ width: 14px;
+ height: 31px;
+ color: var(--zenith-text-faint);
+ cursor: pointer;
+}
- #toc .flex-fill.overflow-y-auto li .expand-stub::before {
- margin-left: 0 !important;
- }
+ .toc .expand-stub::before {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ display: block;
+ width: 14px;
+ height: 14px;
+ margin: 0;
+ line-height: 14px;
+ text-align: center;
+ transform: translate(-50%, -50%);
+ transform-origin: center;
+ transition: color 160ms ease, transform 220ms var(--zenith-ease);
+ }
- #toc .flex-fill.overflow-y-auto li.expander > a {
- border-radius: 0 var(--radius-xs) var(--radius-xs) 0 !important;
- padding: 0.4rem 0.4rem 0.4rem 0.125rem !important;
- }
+.toc li.expanded > .expand-stub::before {
+ transform: translate(-50%, -50%) rotate(90deg);
+}
+
+#affix {
+ position: static;
+ top: auto;
+ max-height: calc(100vh - var(--zenith-header-height) - 32px);
+ padding-left: 0;
+ overflow: auto;
+ border-left: 0;
+}
- #toc .flex-fill.overflow-y-auto li.expander > ul {
- width: 100% !important;
- flex-basis: 100% !important;
- margin-top: 2px !important;
- margin-bottom: 2px !important;
+ #affix::before {
+ content: none;
+ }
+
+ #affix > h5 {
+ display: block;
+ width: 100%;
+ margin: 0 0 14px;
+ padding: 0 0 12px;
+ color: var(--zenith-text-faint);
+ border-bottom: 1px solid var(--zenith-border) !important;
+ font-size: 10px;
+ font-weight: 850;
+ text-transform: uppercase;
+ }
+
+ #affix ul {
+ margin: 0;
+ padding: 0;
+ border-left: 0;
+ list-style: none;
+ }
+
+ #affix a {
+ position: relative;
+ display: block;
+ overflow: hidden;
+ margin: 1px 0;
+ padding: 6px 9px;
+ color: var(--zenith-text-faint);
+ border: 0;
+ border-radius: 6px;
+ font-size: 12px;
+ line-height: 1.4;
+ text-decoration: none !important;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ transition: color 160ms ease, background-color 160ms ease, box-shadow 180ms ease, transform 180ms var(--zenith-ease);
+ }
+
+ #affix a.link-body-emphasis {
+ font-weight: 600;
}
- #toc .flex-fill.overflow-y-auto li > a {
- display: inline-block !important;
- padding: 0.35rem 0.4rem !important;
- margin: 0 !important;
- border-radius: var(--radius-xs) !important;
- font-size: 0.8rem !important;
- font-weight: 500 !important;
- text-decoration: none !important;
- transition: all var(--duration) var(--ease) !important;
- background: transparent !important;
- border: 1px solid transparent !important;
- cursor: pointer !important;
- line-height: 1.3 !important;
- color: var(--text-secondary) !important;
- flex: 1 !important;
+ #affix a.link-secondary {
+ padding-left: 18px;
+ font-size: 11px;
+ font-weight: 400;
}
- #toc .flex-fill.overflow-y-auto li:not(.expander) > a {
- display: block !important;
+ #affix a wbr {
+ display: none;
}
- #toc .flex-fill.overflow-y-auto li:not(.expander) > a:hover {
- background: var(--accent-soft) !important;
- color: var(--accent) !important;
- }
+body[data-yaml-mime="ManagedReference"] #affix {
+ position: static;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
- #toc .flex-fill.overflow-y-auto li.active > a,
- #toc .flex-fill.overflow-y-auto li > a.active {
- background: var(--accent-soft-mid) !important;
- color: var(--accent) !important;
- font-weight: 600 !important;
- }
+#affix a:hover {
+ color: var(--zenith-purple-700);
+ background: rgba(113, 56, 232, 0.06);
+}
- #toc .flex-fill.overflow-y-auto ul ul {
- position: relative !important;
- margin-left: calc(var(--toc-expand-width) + 12px) !important;
- padding: 0 !important;
+#affix a.active,
+#affix a.is-active {
+ color: var(--zenith-nav-active) !important;
+ background: transparent;
+ font-weight: 700;
+ box-shadow: none;
+}
+
+ #affix a.active::before,
+ #affix a.is-active::before {
+ content: "";
+ position: absolute;
+ top: 50%;
+ bottom: auto;
+ left: 0;
+ width: 2px;
+ height: 14px;
+ background: var(--zenith-purple-700);
+ border-radius: 2px;
+ transform: translateY(-50%);
}
- #toc .flex-fill.overflow-y-auto ul ul li {
- margin-top: 1px !important;
- }
+#affix a.link-secondary.active::before,
+#affix a.link-secondary.is-active::before {
+ left: 8px;
+}
- #toc .flex-fill.overflow-y-auto ul ul li:first-child {
- margin-top: 0 !important;
- }
+[data-bs-theme="dark"] .toc li > a:hover,
+[data-bs-theme="dark"] #affix a:hover {
+ color: #d1c4ff !important;
+}
- #toc .flex-fill.overflow-y-auto ul ul li > a {
- font-size: 0.75rem !important;
- padding: 0.25rem 0.375rem !important;
- }
+[data-bs-theme="dark"] .toc li.active:not(.expander) > a,
+[data-bs-theme="dark"] .toc li > a.active,
+[data-bs-theme="dark"] #affix a.active,
+[data-bs-theme="dark"] #affix a.is-active {
+ color: var(--zenith-nav-active) !important;
+}
+
+.doc-code-frame pre > .code-action {
+ display: none !important;
+}
+
+@media (max-width: 575.98px) {
+ article .doc-code-frame > pre {
+ padding: 12px;
+ }
+
+ .doc-code-toolbar {
+ padding-left: 12px;
+ }
+}
/* ==========================================================================
- 19. Breadcrumb Navigation
+ Search, footer, pagination
========================================================================== */
-#breadcrumb {
- margin-bottom: 1.5rem;
+#search-results {
+ width: 100%;
+ max-width: var(--zenith-docs-width) !important;
+ min-height: 65vh;
+ margin: 0 auto;
+ padding: 42px var(--zenith-page-gutter) !important;
}
-.breadcrumb {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 0.25rem;
- padding: 0.4rem 0.75rem;
- margin: 0;
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- list-style: none;
+body[data-search] > main.container-xxl {
+ display: none !important;
}
-.breadcrumb-item {
- display: flex;
- align-items: center;
- font-size: 0.8rem;
- color: var(--text-secondary);
+body[data-search] #search-results {
+ display: grid;
+ grid-template-columns: var(--zenith-sidebar-width) minmax(0, 1fr) var(--zenith-affix-width);
+ column-gap: var(--zenith-column-gap);
}
- .breadcrumb-item a {
- color: var(--text-secondary);
- text-decoration: none;
- padding: 0.15rem 0.3rem;
- border-radius: var(--radius-xs);
- transition: all var(--duration) var(--ease);
+body:is([data-yaml-mime="ManagedReference"], [data-zenith-api])[data-search] #search-results {
+ grid-template-columns: var(--zenith-api-sidebar-width) minmax(0, 1fr) var(--zenith-api-sidebar-width);
+}
+
+body[data-search] #search-results > * {
+ grid-column: 2;
+ min-width: 0;
+}
+
+#search-results > .sr-items {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ gap: var(--zenith-column-gap);
+}
+
+#search-results .sr-item {
+ min-width: 0;
+ padding: 18px 20px;
+ background: var(--zenith-surface);
+ border: 1px solid var(--zenith-border);
+ border-radius: var(--zenith-panel-radius);
+ animation: zenith-reveal-in 420ms var(--zenith-ease) backwards;
+ transition: border-color 160ms ease, box-shadow 200ms ease, transform 200ms var(--zenith-ease);
+}
+
+ #search-results .sr-item:nth-child(2) {
+ animation-delay: 35ms;
}
- .breadcrumb-item a:hover {
- color: var(--accent);
- background: var(--accent-soft);
- text-decoration: none;
- }
+ #search-results .sr-item:nth-child(3) {
+ animation-delay: 70ms;
+ }
- .breadcrumb-item + .breadcrumb-item::before {
- content: '';
- display: inline-block;
- width: 5px;
- height: 5px;
- margin-right: 0.3rem;
- border-right: 1.5px solid var(--text-tertiary);
- border-top: 1.5px solid var(--text-tertiary);
- transform: rotate(45deg);
+ #search-results .sr-item:nth-child(4) {
+ animation-delay: 105ms;
}
- .breadcrumb-item:last-child {
- color: var(--text-primary);
- font-weight: 500;
+ #search-results .sr-item:nth-child(5) {
+ animation-delay: 140ms;
}
- .breadcrumb-item:last-child a {
- color: var(--text-primary);
- pointer-events: none;
- }
+ #search-results .sr-item:nth-child(n + 6) {
+ animation-delay: 175ms;
+ }
-/* ==========================================================================
- 20. Hide UI Elements
- ========================================================================== */
+ #search-results .sr-item:hover {
+ border-color: rgba(113, 56, 232, 0.36);
+ box-shadow: var(--zenith-shadow-small);
+ transform: translateY(-2px);
+ }
-article h1 > a:not(.header-action),
-article h2 > a:not(.header-action),
-article h3 > a:not(.header-action),
-article h4 > a:not(.header-action),
-article h5 > a:not(.header-action),
-article h6 > a:not(.header-action),
-a[href^="http"]::after,
-a[href^="https"]::after {
- display: none !important;
+#search-results .item-title {
+ overflow-wrap: anywhere;
+ color: var(--zenith-purple-700);
+ font-size: 17px;
+ font-weight: 750;
+ text-decoration: none;
}
-/* ==========================================================================
- 21. Search Results
- ========================================================================== */
+#search-results .item-href {
+ overflow-wrap: anywhere;
+ color: var(--zenith-text-faint);
+ font-size: 11px;
+}
-body[data-search] > .search-results {
- max-width: 820px;
- margin: 0 auto;
- padding: 2rem 1.5rem 3rem;
+#search-results .item-brief {
+ overflow-wrap: anywhere;
+ color: var(--zenith-text-soft);
+ font-size: 14px;
}
-#search-results {
- line-height: 1.7;
+html.zenith-motion-ready .zenith-reveal.is-visible {
+ animation: zenith-reveal-in 720ms var(--zenith-ease) var(--zenith-reveal-delay, 0ms) backwards;
+}
+
+html.zenith-motion-ready .zenith-reveal[data-reveal-from="left"].is-visible {
+ animation-name: zenith-reveal-left;
}
- #search-results > .search-list {
- font-size: 0.9rem;
- color: var(--text-secondary);
- margin-bottom: 1.5rem;
- padding-bottom: 1rem;
- border-bottom: 1px solid var(--border);
+html.zenith-motion-ready .zenith-reveal[data-reveal-from="right"].is-visible {
+ animation-name: zenith-reveal-right;
+}
+
+@keyframes zenith-copy-confirm {
+ 0% {
+ transform: scale(0.72);
}
- #search-results > .sr-items {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
+ 58% {
+ transform: scale(1.16);
}
- #search-results > .sr-items > .sr-item {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- padding: 1.25rem 1.5rem;
- margin-bottom: 0;
- transition: border-color var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
- }
+ 100% {
+ transform: scale(1);
+ }
+}
- #search-results > .sr-items > .sr-item:hover {
- border-color: var(--accent);
- box-shadow: var(--shadow-sm);
- }
+@keyframes zenith-reveal-in {
+ from {
+ opacity: 0;
+ transform: translateY(18px) scale(0.985);
+ }
- #search-results > .sr-items > .sr-item > .item-title {
- font-size: 1.125rem;
- font-weight: 600;
- color: var(--accent);
- text-decoration: none;
- display: block;
- margin-bottom: 0.35rem;
- }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
- #search-results > .sr-items > .sr-item > .item-title:hover {
- color: var(--accent-hover);
- }
+@keyframes zenith-reveal-left {
+ from {
+ opacity: 0;
+ transform: translateX(-24px) scale(0.985);
+ }
- #search-results > .sr-items > .sr-item > .item-href {
- font-size: 0.8rem;
- color: var(--text-tertiary);
- margin-bottom: 0.5rem;
- word-break: break-all;
- }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
- #search-results > .sr-items > .sr-item > .item-brief {
- font-size: 0.875rem;
- color: var(--text-secondary);
- line-height: 1.6;
- }
+@keyframes zenith-reveal-right {
+ from {
+ opacity: 0;
+ transform: translateX(24px) scale(0.985);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@keyframes zenith-reading-progress {
+ from {
+ transform: scaleX(0);
+ }
+
+ to {
+ transform: scaleX(1);
+ }
+}
+
+.next-article {
+ width: 100%;
+ margin: 26px auto 0;
+ padding-top: 18px;
+ border-color: var(--zenith-border) !important;
+}
+
+ .next-article a {
+ text-decoration: none !important;
+ }
+
+body > footer {
+ position: relative;
+ z-index: 2;
+ color: var(--zenith-text-faint) !important;
+ background: var(--zenith-surface-soft);
+ border-color: var(--zenith-border) !important;
+ font-size: 12px;
+}
+
+ body > footer .container-xxl {
+ max-width: var(--zenith-content-width) !important;
+ padding: 0 18px !important;
+ }
+
+ body > footer a {
+ color: var(--zenith-purple-700);
+ }
/* ==========================================================================
- 22. Responsive Design
+ Responsive behavior
========================================================================== */
-@media (max-width: 768px) {
- .hero {
- padding: 2.5rem 1.25rem 3rem;
+@media (max-width: 1180px) {
+ #navbar form.search {
+ width: 165px;
}
- .hero h1 {
- font-size: 2.25rem;
- }
+ .hero-copy h1 {
+ font-size: 46px;
+ }
- .hero-tagline {
- font-size: 1.1rem;
+ body:not([data-layout="landing"]) > main.container-xxl {
+ grid-template-columns: var(--zenith-sidebar-width) minmax(0, 1fr);
+ column-gap: 20px;
}
- .hero-description {
- font-size: 0.95rem;
+ body:is([data-yaml-mime="ManagedReference"], [data-zenith-api]) > main.container-xxl {
+ grid-template-columns: minmax(320px, 28%) minmax(0, 1fr);
}
- .hero-logo {
- width: 64px;
- height: 64px;
+ body[data-search] #search-results {
+ grid-template-columns: var(--zenith-sidebar-width) minmax(0, 1fr);
+ column-gap: 20px;
}
- .tech-item {
- padding: 0.4rem 0.75rem;
+ body:is([data-yaml-mime="ManagedReference"], [data-zenith-api])[data-search] #search-results {
+ grid-template-columns: minmax(320px, 28%) minmax(0, 1fr);
}
- .workflow {
- --step-size: 38px;
+ body:not([data-layout="landing"]) main > .affix {
+ display: none !important;
}
+}
- .step-number {
- font-size: 0.9rem;
+@media (max-width: 991.98px) {
+ :root {
+ --zenith-header-height: 70px;
+ --zenith-header-bar-height: 54px;
+ --zenith-header-inset: 8px;
}
- .step-content {
- padding: 0.875rem 1rem;
+ #logo {
+ width: 109px;
+ height: 34px;
}
- article h1 {
- font-size: 1.625rem;
+ .navbar-brand {
+ margin-right: 18px !important;
}
- article h2 {
- font-size: 1.25rem;
- padding-left: 0.75rem;
- position: relative;
+ #navbar .nav-link {
+ padding: 0 8px !important;
+ font-size: 13px;
}
- article h2::before {
- content: '';
- display: block;
- position: absolute;
- left: 0;
- top: 0.15em;
- bottom: 0.15em;
- width: 3px;
- background: var(--gradient-brand);
- border-radius: 2px;
+ #navbar .nav-link::after {
+ right: 8px;
+ left: 8px;
}
- article h3 {
- font-size: 1.05rem;
+ #navbar form.search {
+ display: none;
}
- article pre {
- border-radius: var(--radius-sm);
+ .hero-copy {
+ max-width: 690px;
+ margin: 0 auto;
+ text-align: center;
}
- .status-yes,
- .status-no {
- padding: 0.1rem 0.4rem;
- font-size: 0.7rem;
+ .hero-copy h1,
+ .hero-lede {
+ margin-right: auto;
+ margin-left: auto;
+ }
+
+ .hero-actions,
+ .backend-support {
+ justify-content: center;
}
- .tech-bar {
- flex-direction: column;
- gap: 0.75rem;
- padding: 0.875rem 1.125rem;
+ body:not([data-layout="landing"]) > main.container-xxl {
+ grid-template-columns: var(--zenith-sidebar-width) minmax(0, 1fr);
+ padding-right: 14px !important;
+ padding-left: 14px !important;
+ column-gap: 18px;
}
- .tech-bar-group {
- flex-direction: column;
- gap: 0.25rem;
- text-align: center;
+ body:is([data-yaml-mime="ManagedReference"], [data-zenith-api]) > main.container-xxl {
+ grid-template-columns: minmax(320px, 34%) minmax(0, 1fr);
}
- .tech-bar-divider {
- width: 80%;
- height: 1px;
+ body[data-search] #search-results {
+ grid-template-columns: var(--zenith-sidebar-width) minmax(0, 1fr);
+ column-gap: 18px;
}
- .highlight-grid {
- grid-template-columns: 1fr;
+ body:is([data-yaml-mime="ManagedReference"], [data-zenith-api])[data-search] #search-results {
+ grid-template-columns: minmax(320px, 34%) minmax(0, 1fr);
}
+}
- .highlight-item {
- padding: 0.875rem;
+@media (max-width: 767.98px) {
+ :root {
+ --zenith-header-height: 64px;
+ --zenith-header-bar-height: 64px;
+ --zenith-header-inset: 0px;
+ --zenith-header-inline-inset: 0px;
}
- .feature-list-item {
- flex-direction: column;
- gap: 0.25rem;
+ body > header.bg-body,
+ body > header {
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: auto;
+ min-height: var(--zenith-header-height);
+ border: 0 !important;
+ border-bottom: 1px solid var(--zenith-header-border) !important;
+ border-radius: 0;
+ box-shadow: 0 1px 6px rgba(20, 28, 58, 0.04);
+ transform: none;
+ }
+
+ .navbar > .container-xxl {
+ min-height: var(--zenith-header-bar-height);
+ }
+
+ .navbar .navbar-toggler,
+ .navbar button[data-bs-toggle="collapse"] {
+ color: var(--zenith-text);
+ border: 0 !important;
+ border-radius: 6px !important;
}
- .feature-list-title {
- min-width: auto;
+ .navbar-collapse {
+ position: absolute;
+ top: var(--zenith-header-bar-height);
+ right: 0;
+ left: 0;
+ padding: 10px 18px 16px;
+ background: var(--zenith-surface);
+ border-bottom: 1px solid var(--zenith-border);
+ box-shadow: var(--zenith-shadow-medium);
}
- .cta-section {
+ #navbar {
+ display: flex;
+ align-items: stretch;
flex-direction: column;
- padding: 1.5rem 1.25rem;
- text-align: center;
+ gap: 8px;
}
- .cta-section::before {
- /* Shift gradient line to left side on mobile */
- left: 0;
- right: auto;
- top: 0;
- bottom: 0;
- width: 3px;
- height: 100%;
+ #navbar .navbar-nav {
+ order: 1;
+ width: 100%;
+ align-items: stretch;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ #navbar .nav-link {
+ padding: 10px 12px !important;
+ border-radius: 5px !important;
+ }
+
+ #navbar .nav-link::after {
+ display: none;
+ }
+
+ #navbar form.search {
+ display: block;
+ order: 2 !important;
+ width: 100%;
}
- .cta-main h3 {
- font-size: 1.25rem;
+ #navbar form.search.zenith-search {
+ display: flex;
+ width: 36px;
+ }
+
+ #navbar form.search.zenith-search.is-expanded {
+ width: 100%;
+ }
+
+ .navbar .icons {
+ order: 3 !important;
+ justify-content: flex-start;
}
- .cta-links {
- width: 100%;
- align-items: stretch;
+ .landing-shell {
+ width: calc(100% - 28px);
}
- .cta-link {
- justify-content: center;
- width: 100%;
- min-width: auto;
+ .hero-copy h1 {
+ font-size: 40px;
+ line-height: 1.08;
}
- .breadcrumb {
- padding: 0.35rem 0.5rem;
+ .hero-lede {
+ font-size: 16px;
}
- .breadcrumb-item {
- font-size: 0.75rem;
+ body:not([data-layout="landing"]) > main.container-xxl {
+ display: block;
+ padding: 0 16px !important;
}
- .alert {
- padding: 0.75rem 0.875rem;
- font-size: 0.8125rem;
+ body:not([data-layout="landing"]) main > .content {
+ padding-top: 18px;
}
- .actionbar {
- display: flex;
- align-items: center;
- gap: 0.75rem;
+ body[data-search] #search-results {
+ display: block;
}
- .actionbar > button {
- flex-shrink: 0;
- margin-top: 0 !important;
- margin-left: 0 !important;
- }
+ #sidetoggle {
+ position: static;
+ height: auto;
+ border-right: 0;
+ }
+
+ .sidetoc {
+ height: auto;
+ }
+
+ article h1 {
+ font-size: 31px;
+ }
+
+ article h2 {
+ font-size: 23px;
+ }
+}
+
+@media (max-width: 575.98px) {
+ #logo {
+ width: 102px;
+ height: 32px;
+ }
+
+ .navbar > .container-xxl {
+ padding-right: 12px !important;
+ padding-left: 12px !important;
+ }
+
+ .hero-copy h1 {
+ font-size: 34px;
+ }
+
+ .hero-actions .landing-button {
+ width: 100%;
+ }
+
+ .backend-label {
+ width: 100%;
+ }
+
+ article h1 {
+ font-size: 28px;
+ }
+
+ article h2 {
+ margin-top: 35px;
+ font-size: 21px;
+ }
+
+ article pre,
+ .codewrapper pre {
+ padding: 18px;
+ font-size: 11px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+
+ .zenith-reading-progress > span {
+ animation: none !important;
+ transform: scaleX(0) !important;
+ transition: none !important;
+ }
+
+ html.zenith-motion-ready .zenith-reveal {
+ opacity: 1 !important;
+ transform: none !important;
+ }
+}
+
+@media print {
+ body {
+ padding-top: 0 !important;
+ }
+
+ body > header,
+ #sidetoggle,
+ main > .affix,
+ body > footer,
+ .next-article {
+ display: none !important;
+ }
+
+ body:not([data-layout="landing"]) > main.container-xxl {
+ display: block;
+ max-width: none !important;
+ padding: 0 !important;
+ }
+
+ article {
+ max-width: none !important;
+ }
+}
+
+/* ==========================================================================
+ Renderer Console V2
+ ========================================================================== */
+
+.render-hero {
+ --render-viewport-surface: #17131f;
+ --render-viewport-toolbar: #211b2b;
+ --render-viewport-border: rgba(53, 43, 66, 0.46);
+ --render-viewport-text: #f1edf6;
+ --render-viewport-muted: #aaa1b5;
+ --render-viewport-shadow: 0 24px 58px rgba(0, 0, 0, 0.26), 0 3px 12px rgba(0, 0, 0, 0.18);
+ position: relative;
+ min-height: calc(620px + var(--zenith-header-height));
+ overflow: hidden;
+ color: #ffffff;
+ background: #100d16;
+}
+
+.render-hero-inner {
+ position: relative;
+ z-index: 2;
+ display: grid;
+ grid-template-columns: minmax(0, 0.9fr) minmax(470px, 1.1fr);
+ gap: clamp(28px, 3vw, 48px);
+ min-height: calc(620px + var(--zenith-header-height));
+ align-items: center;
+ padding-top: calc(48px + var(--zenith-header-height));
+ padding-bottom: 48px;
+}
+
+.render-hero .hero-copy {
+ width: 100%;
+ max-width: 650px;
+ margin: 0;
+ padding: 0;
+ text-align: left;
+ animation: none;
+}
+
+ .render-hero .hero-copy h1,
+ .render-hero .hero-lede,
+ .render-hero .hero-actions,
+ .render-hero .backend-support {
+ animation: render-hero-copy-in 720ms var(--zenith-ease) both;
+ }
+
+ .render-hero .hero-copy h1 {
+ animation-delay: 80ms;
+ }
+
+.render-hero .hero-lede {
+ animation-delay: 170ms;
+}
+
+.render-hero .hero-actions {
+ animation-delay: 260ms;
+}
+
+.render-hero .backend-support {
+ animation-delay: 350ms;
+}
+
+.render-viewport {
+ position: relative;
+ width: 100%;
+ min-width: 0;
+ overflow: hidden;
+ color: var(--render-viewport-text);
+ background: var(--render-viewport-surface);
+ border: 1px solid var(--render-viewport-border);
+ border-radius: 6px;
+ box-shadow: var(--render-viewport-shadow);
+ animation: render-scene-in 820ms var(--zenith-ease) 210ms both;
+}
+
+ .render-viewport::before {
+ content: "";
+ position: absolute;
+ top: -1px;
+ right: 0;
+ left: 0;
+ z-index: 3;
+ height: 1px;
+ pointer-events: none;
+ background: linear-gradient(90deg, #512bd4, #7c4dff 52%, #34c9f5);
+ }
+
+.render-viewport-toolbar {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ height: 38px;
+ align-items: center;
+ gap: 12px;
+ padding: 0 8px 0 13px;
+ color: var(--render-viewport-muted);
+ background: var(--render-viewport-toolbar);
+ border-bottom: 1px solid var(--render-viewport-border);
+ font-family: "Cascadia Code", "Cascadia Mono", Consolas, monospace;
+ font-size: 10px;
+ line-height: 1;
+}
+
+.render-viewport-title,
+.render-viewport-details {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ white-space: nowrap;
+}
+
+.render-viewport-title {
+ color: var(--render-viewport-text);
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+.zenith-carousel-status {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+.render-viewport-title > i {
+ color: var(--zenith-purple-700);
+ font-size: 12px;
+}
+
+.render-viewport-details {
+ margin-left: auto;
+}
+
+.render-viewport-open {
+ display: inline-flex;
+ width: 28px;
+ height: 28px;
+ align-items: center;
+ justify-content: center;
+ color: var(--render-viewport-muted) !important;
+ border-radius: 4px;
+ text-decoration: none !important;
+ transition: color 160ms ease, background-color 160ms ease;
+}
+
+ .render-viewport-open:hover,
+ .render-viewport-open:focus-visible {
+ color: var(--render-viewport-text) !important;
+ background: rgba(255, 255, 255, 0.08);
+ }
+
+.render-carousel-controls {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+}
+
+.render-viewport-canvas {
+ position: relative;
+ aspect-ratio: 16 / 9;
+ overflow: hidden;
+ background: #dce6f2;
+}
+
+ .render-viewport-canvas > a {
+ display: block;
+ width: 100%;
+ height: 100%;
+ color: transparent;
+ text-decoration: none;
+ }
+
+.render-carousel-slide {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 480ms ease;
+}
+
+ .render-carousel-slide.is-active {
+ z-index: 1;
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+.render-carousel-control {
+ display: none;
+ width: 28px;
+ height: 28px;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ color: var(--render-viewport-muted);
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease;
+}
+
+ .render-carousel-control:hover,
+ .render-carousel-control:focus-visible {
+ color: var(--render-viewport-text);
+ background: rgba(255, 255, 255, 0.08);
+ border-color: rgba(255, 255, 255, 0.12);
+ }
+
+.render-carousel-progress {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 3;
+ display: none;
+ height: 3px;
+ overflow: hidden;
+ pointer-events: none;
+ background: rgba(16, 13, 22, 0.26);
+}
+
+ .render-carousel-progress > span {
+ display: block;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, #7847ff, #43d8ff);
+ box-shadow: 0 0 8px rgba(124, 77, 255, 0.42);
+ transform: scaleX(0);
+ transform-origin: left center;
+ }
+
+ .render-carousel-progress > span.is-timing {
+ animation: render-carousel-timing 6500ms linear forwards;
+ animation-play-state: paused;
+ }
+
+.render-viewport.is-carousel-ready .render-carousel-control,
+.render-viewport.is-carousel-ready .render-carousel-progress {
+ display: flex;
+}
+
+.render-viewport.is-carousel-playing .render-carousel-progress > span.is-timing {
+ animation-play-state: running;
+}
+
+.render-hero-media {
+ display: block;
+ width: 100%;
+ height: 100%;
+ max-width: none;
+ object-fit: cover;
+ object-position: center;
+ border: 0 !important;
+ border-radius: 0 !important;
+ outline: 0;
+ box-shadow: none;
+}
+
+@media (max-width: 767.98px) {
+ .render-carousel-control {
+ width: 26px;
+ height: 26px;
+ }
+
+ .render-carousel-control[data-carousel-toggle] {
+ display: none !important;
+ }
+
+ .render-carousel-progress {
+ display: none !important;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .render-carousel-slide,
+ .render-carousel-control {
+ transition: none !important;
+ }
+
+ .render-carousel-control[data-carousel-toggle] {
+ display: none !important;
+ }
+
+ .render-carousel-progress {
+ display: none !important;
+ }
+}
+
+@keyframes render-carousel-timing {
+ from {
+ transform: scaleX(0);
+ }
+
+ to {
+ transform: scaleX(1);
+ }
+}
+
+.render-hero .hero-copy h1 {
+ max-width: 650px;
+ margin: 0 0 24px;
+ padding: 0 0 6px;
+ color: #f4faf8;
+ font-size: 62px;
+ font-weight: 760;
+ line-height: 1.08;
+ overflow: visible;
+ text-shadow: 0 2px 24px rgba(0, 0, 0, 0.28);
+}
+
+ .render-hero .hero-copy h1 span {
+ display: block;
+ width: fit-content;
+ margin-top: -4px;
+ padding: 10px 0;
+ color: #43d8ff;
+ background: none;
+ white-space: nowrap;
+ -webkit-text-fill-color: currentColor;
+ }
+
+@media (min-width: 992px) {
+ .render-hero .hero-copy h1 {
+ font-size: clamp(41px, calc(4.4643vw - 3.2857px), 62px);
+ }
+}
+
+.render-hero .hero-lede {
+ max-width: 620px;
+ margin-bottom: 30px;
+ padding-bottom: 2px;
+ color: #c5bdce;
+ font-size: 17px;
+ line-height: 1.8;
+ overflow: visible;
+}
+
+.render-hero .landing-button-primary {
+ color: #ffffff !important;
+ background: #512bd4;
+ box-shadow: none;
+}
+
+ .render-hero .landing-button-primary:hover {
+ color: #ffffff !important;
+ background: #6844df;
+ box-shadow: 0 12px 28px rgba(81, 43, 212, 0.3);
+ }
+
+.render-hero .landing-button-secondary {
+ color: #f8f5fc !important;
+ background: rgba(16, 13, 22, 0.5);
+ border-color: rgba(222, 213, 235, 0.36);
+ box-shadow: none;
+ backdrop-filter: blur(8px);
+}
+
+ .render-hero .landing-button-secondary:hover {
+ color: #ffffff !important;
+ background: rgba(43, 32, 57, 0.84);
+ border-color: rgba(67, 216, 255, 0.7);
+ }
+
+.render-hero .backend-support {
+ gap: 7px;
+ margin-top: 34px;
+}
+
+.render-hero .backend-label {
+ color: #8b8298;
+ font-family: "Cascadia Code", "Cascadia Mono", Consolas, monospace;
+ font-size: 9px;
+ line-height: 1;
+ letter-spacing: 0;
+}
+
+.render-hero .backend-pill {
+ --backend-dot: #58b947;
+ --backend-dot-glow: #58b947;
+ --backend-dot-ring: transparent;
+ min-height: 25px;
+ gap: 7px;
+ color: #d2cadb;
+ background: rgba(16, 13, 22, 0.58);
+ border-color: rgba(206, 194, 221, 0.24);
+ border-radius: 4px;
+ font-family: "Cascadia Code", "Cascadia Mono", Consolas, monospace;
+ font-size: 9px;
+ font-weight: 650;
+ backdrop-filter: blur(8px);
+}
+
+ .render-hero .backend-pill::before {
+ content: "";
+ width: 6px;
+ height: 6px;
+ flex: 0 0 6px;
+ background: var(--backend-dot);
+ border-radius: 50%;
+ box-shadow: inset 0 0 0 1px var(--backend-dot-ring), 0 0 8px color-mix(in srgb, var(--backend-dot-glow) 48%, transparent);
+ }
+
+.render-hero .backend-metal {
+ --backend-dot: linear-gradient(135deg, #ffffff 0%, #a6afb9 48%, #e4e8ec 100%);
+ --backend-dot-glow: #c9d0d7;
+ --backend-dot-ring: rgba(74, 82, 92, 0.38);
+}
+
+.render-hero .backend-vulkan {
+ --backend-dot: #b8202d;
+ --backend-dot-glow: #d63b45;
+}
+
+.toc .nav > li > a:hover,
+.toc .nav > li.active > a,
+.toc .nav > li > a.active {
+ color: var(--zenith-purple-700);
+}
+
+.toc .nav > li > a:hover {
+ background: rgba(81, 43, 212, 0.07);
+}
+
+.toc .nav > li.active > a,
+.toc .nav > li > a.active {
+ background: transparent;
+}
+
+@keyframes render-scene-in {
+ from {
+ opacity: 0;
+ transform: translateX(24px) scale(0.985);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0) scale(1);
+ }
+}
+
+@keyframes render-hero-copy-in {
+ from {
+ opacity: 0;
+ transform: translateY(16px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@media (max-width: 991.98px) {
+ .render-hero,
+ .render-hero-inner {
+ min-height: 0;
+ }
+
+ .render-hero-inner {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 42px;
+ padding-top: calc(56px + var(--zenith-header-height));
+ padding-bottom: 64px;
+ }
+
+ .render-hero .hero-copy {
+ max-width: none;
+ }
+
+ .render-viewport {
+ width: min(760px, 100%);
+ margin: 0 auto;
+ }
+
+ .render-hero .hero-actions,
+ .render-hero .backend-support {
+ justify-content: flex-start;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .render-hero-inner {
+ gap: 34px;
+ padding-top: calc(48px + var(--zenith-header-height));
+ padding-bottom: 54px;
+ }
+
+ .render-hero .hero-copy h1 {
+ font-size: 43px;
+ }
+}
+
+@media (max-width: 575.98px) {
+ .render-hero .hero-copy h1 {
+ font-size: 34px;
+ line-height: 1.04;
+ }
+
+ .render-hero .hero-copy h1 span {
+ margin-top: 0;
+ padding: 6px 0 7px;
+ }
+
+ .render-hero .hero-lede {
+ font-size: 15px;
+ }
+
+ .render-hero .backend-label {
+ width: 100%;
+ }
+
+ .render-viewport-toolbar {
+ gap: 8px;
+ padding-left: 10px;
+ font-size: 9px;
+ }
+}
+
+@media (max-width: 374.98px) {
+ .render-hero .hero-copy h1 span {
+ font-size: 32px;
+ }
+
+ .render-viewport-details > :not(.render-viewport-position) {
+ display: none;
+ }
+}
+
+@media (max-width: 349.98px) {
+ .render-hero .hero-copy h1 span {
+ font-size: 29px;
+ }
+}
+
+/* ==========================================================================
+ Layout and border refinement
+ ========================================================================== */
+
+.landing-button {
+ gap: 9px;
+}
+
+@media (min-width: 768px) {
+ .navbar > .container-xxl {
+ max-width: none !important;
+ padding-right: 18px !important;
+ padding-left: 18px !important;
+ transform: none;
+ }
+
+ .navbar-brand {
+ margin-right: 10px !important;
+ }
+
+ #navbar {
+ gap: 6px;
+ }
+
+ #navbar .navbar-nav {
+ margin-right: auto !important;
+ }
+
+ #navbar form.search {
+ margin-left: 0 !important;
+ }
+
+ .navbar .icons {
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+ }
+}
+
+article pre code.hljs,
+.codewrapper pre code.hljs {
+ padding: 0 !important;
+ overflow: visible;
+ color: inherit;
+ background: transparent !important;
+}
+
+/* ==========================================================================
+ Theme-aware landing hero
+ ========================================================================== */
+
+:root:not([data-bs-theme="dark"]) .render-hero {
+ --render-viewport-surface: #ffffff;
+ --render-viewport-toolbar: rgba(248, 247, 250, 0.96);
+ --render-viewport-border: rgba(52, 43, 69, 0.15);
+ --render-viewport-text: #342b45;
+ --render-viewport-muted: #746b7e;
+ --render-viewport-shadow: 0 18px 46px rgba(41, 27, 69, 0.1), 0 2px 8px rgba(41, 27, 69, 0.06);
+ color: #231e2e;
+ background-color: #fbfafc;
+ background-image: none;
+}
+
+:root:not([data-bs-theme="dark"]) .render-viewport-open:hover,
+:root:not([data-bs-theme="dark"]) .render-viewport-open:focus-visible {
+ background: rgba(81, 43, 212, 0.08);
+}
+
+[data-bs-theme="dark"] .render-hero {
+ background-color: #100d16;
+ background-image: linear-gradient(135deg, rgba(155, 123, 255, 0.1) 0%, rgba(155, 123, 255, 0.025) 38%, rgba(67, 216, 255, 0.06) 78%, transparent 100%);
+}
+
+:root:not([data-bs-theme="dark"]) .render-hero .hero-copy h1 {
+ color: #2b2435;
+ text-shadow: none;
+}
+
+ :root:not([data-bs-theme="dark"]) .render-hero .hero-copy h1 span {
+ color: #512bd4;
+ }
+
+:root:not([data-bs-theme="dark"]) .render-hero .hero-lede {
+ color: #655d70;
+}
+
+:root:not([data-bs-theme="dark"]) .render-hero .landing-button-secondary {
+ color: #342b45 !important;
+ background: rgba(255, 255, 255, 0.72);
+ border-color: rgba(52, 43, 69, 0.22);
+}
+
+ :root:not([data-bs-theme="dark"]) .render-hero .landing-button-secondary:hover {
+ color: #512bd4 !important;
+ background: rgba(255, 255, 255, 0.94);
+ border-color: rgba(81, 43, 212, 0.46);
+ }
+
+:root:not([data-bs-theme="dark"]) .render-hero .backend-label {
+ color: #746b7e;
+}
+
+:root:not([data-bs-theme="dark"]) .render-hero .backend-pill {
+ color: #40374b;
+ background: rgba(255, 255, 255, 0.68);
+ border-color: rgba(64, 52, 81, 0.2);
+}
+
+/* ==========================================================================
+ Reference-inspired landing sections
+ ========================================================================== */
+
+body[data-layout="landing"] > footer {
+ display: none;
+}
+
+.landing-page .features-section {
+ padding: 72px 0 104px;
+}
+
+.landing-page .section-heading,
+.landing-page .resources-heading {
+ max-width: 760px;
+ margin: 0 auto 56px;
+ text-align: center;
+}
+
+.landing-page .section-kicker {
+ display: block;
+ margin-bottom: 14px;
+ color: var(--zenith-purple-700);
+ font-family: "Cascadia Code", "Cascadia Mono", Consolas, monospace;
+ font-size: 10px;
+ font-weight: 800;
+ line-height: 1;
+ letter-spacing: 0;
+}
+
+.landing-page .section-heading h2,
+.landing-page .architecture-copy h2,
+.landing-page .resources-heading h2 {
+ margin: 0;
+ padding: 0;
+ color: #182746;
+ border: 0;
+ font-size: 39px;
+ font-weight: 800;
+ line-height: 1.08;
+ letter-spacing: 0;
+}
+
+ .landing-page .section-heading h2 span,
+ .landing-page .architecture-copy h2 span {
+ padding: 6px 5px 7px;
+ color: #512bd4;
+ background: linear-gradient(100deg, #512bd4, #c416b9);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ }
+
+ .landing-page .section-heading h2 > span {
+ display: block;
+ width: fit-content;
+ margin-inline: auto;
+ }
+
+ .landing-page .architecture-copy h2 > span {
+ display: inline-block;
+ margin-left: -5px;
+ }
+
+ .landing-page .section-heading h2 > .anchorjs-link {
+ display: none;
+ }
+
+.landing-page h2::after {
+ content: none;
+}
+
+.landing-page .section-heading p {
+ max-width: 680px;
+ margin: 18px auto 0;
+ color: #6b7180;
+ font-size: 15px;
+ line-height: 1.65;
+}
+
+[data-bs-theme="dark"] .landing-page .section-heading h2,
+[data-bs-theme="dark"] .landing-page .architecture-copy h2,
+[data-bs-theme="dark"] .landing-page .resources-heading h2 {
+ color: #f4f1fa;
+}
+
+[data-bs-theme="dark"] .landing-page .section-heading p {
+ color: #aca4b7;
+}
+
+.landing-page .feature-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 22px;
+}
+
+.landing-page .feature-card {
+ display: flex;
+ min-height: 214px;
+ flex-direction: column;
+ padding: 26px 24px;
+ justify-content: flex-start;
+ background: rgba(255, 255, 255, 0.84);
+ border: 1px solid #e4e6ee;
+ border-radius: var(--zenith-panel-radius);
+ box-shadow: 0 8px 24px rgba(31, 43, 74, 0.045);
+ transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
+}
+
+ .landing-page .feature-card::before {
+ display: none;
+ }
+
+ .landing-page .feature-card:hover {
+ background: #ffffff;
+ border-color: rgba(81, 43, 212, 0.24);
+ box-shadow: 0 12px 30px rgba(31, 43, 74, 0.08);
+ transform: translateY(-2px);
+ }
+
+.landing-page .feature-icon {
+ display: inline-flex;
+ width: 40px;
+ height: 40px;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 22px;
+ border: 0;
+ border-radius: var(--zenith-panel-radius);
+ font-size: 18px;
+}
+
+.landing-page .feature-icon-violet {
+ color: #512bd4;
+ background: #f1edff !important;
+}
+
+.landing-page .feature-icon-pink {
+ color: #d31491;
+ background: #fff0f8 !important;
+}
+
+.landing-page .feature-icon-blue {
+ color: #1671e9;
+ background: #edf5ff !important;
+}
+
+.landing-page .feature-icon-cyan {
+ color: #087f9c;
+ background: #e9faff !important;
+}
+
+.landing-page .feature-card h3 {
+ margin: 0 0 8px;
+ padding: 0;
+ color: #172a4d;
+ font-size: 18px;
+ line-height: 1.3;
+}
+
+.landing-page .feature-card p {
+ margin: 0;
+ color: #697184;
+ font-size: 14px;
+ line-height: 1.6;
+}
+
+[data-bs-theme="dark"] .landing-page .feature-card {
+ background: rgba(25, 20, 34, 0.88);
+ border-color: #352c45;
+ box-shadow: none;
+}
+
+ [data-bs-theme="dark"] .landing-page .feature-card:hover {
+ background: #20182b;
+ border-color: rgba(155, 123, 255, 0.38);
+ box-shadow: 0 12px 30px rgba(4, 3, 7, 0.28);
+ }
+
+ [data-bs-theme="dark"] .landing-page .feature-card h3 {
+ color: #f4f1fa;
+ }
+
+ [data-bs-theme="dark"] .landing-page .feature-card p {
+ color: #aaa3b4;
+ }
+
+.architecture-section {
+ padding: 96px 0;
+}
+
+.architecture-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ align-items: center;
+ gap: clamp(48px, 6vw, 88px);
+}
+
+.architecture-copy .section-kicker {
+ margin-bottom: 14px;
+ text-align: left;
+}
+
+.architecture-copy h2 {
+ max-width: 560px;
+}
+
+.architecture-copy > p {
+ max-width: 610px;
+ margin: 20px 0 28px;
+ color: #626a7a;
+ font-size: 15px;
+ line-height: 1.7;
+}
+
+.architecture-points {
+ display: grid;
+ gap: 12px;
+}
+
+ .architecture-points > a {
+ display: grid;
+ grid-template-columns: 26px minmax(0, 1fr);
+ gap: 12px;
+ align-items: center;
+ padding: 14px 16px;
+ color: #172a4d;
+ background: rgba(255, 255, 255, 0.82);
+ border: 1px solid rgba(81, 43, 212, 0.1);
+ border-radius: 8px;
+ text-decoration: none;
+ transition: border-color 160ms ease, transform 160ms ease;
+ }
+
+ .architecture-points > a:hover {
+ border-color: rgba(81, 43, 212, 0.32);
+ transform: translateX(3px);
+ }
+
+ .architecture-points .architecture-step {
+ display: inline-flex;
+ width: 26px;
+ height: 26px;
+ align-items: center;
+ justify-content: center;
+ color: var(--zenith-purple-700);
+ background: rgba(81, 43, 212, 0.08);
+ border: 1px solid rgba(81, 43, 212, 0.18);
+ border-radius: 50%;
+ font-family: "Cascadia Code", "Cascadia Mono", Consolas, monospace;
+ font-size: 9px;
+ font-weight: 800;
+ line-height: 1;
+ }
+
+ .architecture-points span,
+ .architecture-points strong,
+ .architecture-points small {
+ display: block;
+ }
+
+ .architecture-points strong {
+ margin-bottom: 3px;
+ font-size: 13px;
+ }
+
+ .architecture-points small {
+ color: #777d8c;
+ font-size: 11px;
+ line-height: 1.45;
+ }
+
+[data-bs-theme="dark"] .architecture-copy > p {
+ color: #aaa3b4;
+}
+
+[data-bs-theme="dark"] .architecture-points > a {
+ color: #f4f1fa;
+ background: rgba(31, 24, 43, 0.88);
+ border-color: #3b304d;
+}
+
+[data-bs-theme="dark"] .architecture-points small {
+ color: #9e96aa;
+}
+
+.resources-section {
+ padding: 90px 0 96px;
+}
+
+.landing-page .resources-heading {
+ margin-bottom: 50px;
+}
+
+ .landing-page .resources-heading h2 {
+ font-size: 36px;
+ }
+
+.resource-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 22px;
+}
+
+.resource-card {
+ display: flex;
+ min-height: 244px;
+ flex-direction: column;
+ padding: 28px;
+ overflow: hidden;
+ color: #ffffff !important;
+ border-radius: 8px;
+ box-shadow: 0 14px 30px rgba(31, 43, 91, 0.12);
+ text-decoration: none !important;
+ transition: transform 170ms ease, box-shadow 170ms ease;
+}
+
+ .resource-card:hover {
+ color: #ffffff !important;
+ box-shadow: 0 18px 38px rgba(31, 43, 91, 0.18);
+ transform: translateY(-4px);
+ }
+
+.resource-card-docs {
+ background: linear-gradient(135deg, #512bd4, #7542ed);
+}
+
+.resource-card-nuget {
+ background: linear-gradient(135deg, #d10ba7, #ec35bd);
+}
+
+.resource-card-community {
+ background: linear-gradient(135deg, #0869b8, #2689dc);
+}
+
+.resource-icon {
+ display: inline-flex;
+ width: 38px;
+ height: 38px;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 26px;
+ background: rgba(255, 255, 255, 0.16);
+ border-radius: 8px;
+ font-size: 18px;
+}
+
+.resource-title {
+ display: block;
+ margin: 0 0 9px;
+ padding: 0;
+ color: #ffffff;
+ font-size: 18px;
+ font-weight: 800;
+}
+
+.resource-card p {
+ margin: 0 0 24px;
+ color: rgba(255, 255, 255, 0.82);
+ font-size: 13px;
+ line-height: 1.6;
+}
+
+.resource-card > strong {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: auto;
+ color: #ffffff;
+ font-size: 13px;
+}
+
+/* Keep every landing section on one continuous theme canvas. */
+.landing-page {
+ --landing-canvas: #fbfafc;
+ --landing-atmosphere: linear-gradient(145deg, rgba(81, 43, 212, 0.055) 0%, rgba(81, 43, 212, 0.012) 46%, rgba(22, 113, 233, 0.04) 100%);
+ --landing-grid-row: rgba(81, 43, 212, 0.034);
+ --landing-grid-column: rgba(52, 201, 245, 0.03);
+ --landing-footer-text: #667187;
+ --landing-footer-heading-primary: #512bd4;
+ --landing-footer-heading-secondary: #d31491;
+ --landing-footer-link: #60708a;
+ --landing-footer-link-hover: #512bd4;
+ --landing-footer-icon: #512bd4;
+ --landing-footer-icon-bg: rgba(81, 43, 212, 0.08);
+ --landing-footer-icon-bg-hover: rgba(81, 43, 212, 0.15);
+ --landing-footer-rule: rgba(24, 39, 70, 0.1);
+ background-color: var(--landing-canvas);
+ background-image: var(--landing-atmosphere), linear-gradient(var(--landing-grid-row) 1px, transparent 1px), linear-gradient(90deg, var(--landing-grid-column) 1px, transparent 1px);
+ background-size: 100% 100%, 64px 64px, 64px 64px;
+}
+
+:root:not([data-bs-theme="dark"]) .landing-page .render-hero {
+ background-color: transparent;
+ background-image: none;
+}
+
+[data-bs-theme="dark"] .landing-page {
+ --landing-canvas: #100d16;
+ --landing-atmosphere: linear-gradient(145deg, rgba(155, 123, 255, 0.055) 0%, rgba(155, 123, 255, 0.012) 46%, rgba(67, 216, 255, 0.035) 100%);
+ --landing-grid-row: rgba(155, 123, 255, 0.038);
+ --landing-grid-column: rgba(67, 216, 255, 0.03);
+ --landing-footer-text: #9f98ab;
+ --landing-footer-heading-primary: #b9a5ff;
+ --landing-footer-heading-secondary: #ff70c0;
+ --landing-footer-link: #aaa3b8;
+ --landing-footer-link-hover: #d1c4ff;
+ --landing-footer-icon: #b9a5ff;
+ --landing-footer-icon-bg: rgba(155, 123, 255, 0.1);
+ --landing-footer-icon-bg-hover: rgba(155, 123, 255, 0.18);
+ --landing-footer-rule: rgba(222, 213, 235, 0.1);
+}
+
+ [data-bs-theme="dark"] .landing-page .render-hero {
+ background-color: transparent;
+ background-image: none;
+ }
+
+.landing-page .features-section,
+.landing-page .architecture-section,
+.landing-page .resources-section {
+ background: transparent;
+}
+
+.landing-footer {
+ padding: 78px 0 34px;
+ color: var(--landing-footer-text);
+ background: transparent;
+}
+
+.landing-footer-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.45fr) minmax(180px, 0.7fr) minmax(180px, 0.7fr);
+ gap: 72px;
+ padding-bottom: 58px;
+}
+
+.landing-footer-logo {
+ display: block;
+ width: auto;
+ height: 48px;
+}
+
+.landing-footer-brand p {
+ max-width: 480px;
+ margin: 24px 0 28px;
+ color: var(--landing-footer-text);
+ font-size: 15px;
+ line-height: 1.7;
+}
+
+.landing-footer-github {
+ display: inline-flex;
+ width: 42px;
+ height: 42px;
+ align-items: center;
+ justify-content: center;
+ color: var(--landing-footer-icon) !important;
+ background: var(--landing-footer-icon-bg);
+ border-radius: 8px;
+ font-size: 20px;
+}
+
+ .landing-footer-github:hover {
+ color: var(--landing-footer-link-hover) !important;
+ background: var(--landing-footer-icon-bg-hover);
+ }
+
+.landing-footer-column {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 14px;
+}
+
+.landing-footer-heading {
+ display: block;
+ margin: 0 0 10px;
+ padding: 0;
+ color: var(--landing-footer-heading-primary);
+ font-size: 14px;
+ font-weight: 800;
+}
+
+.landing-footer-column:last-child .landing-footer-heading {
+ color: var(--landing-footer-heading-secondary);
+}
+
+.landing-footer-column a {
+ color: var(--landing-footer-link);
+ font-size: 14px;
+ text-decoration: none;
+}
+
+ .landing-footer-column a:hover {
+ color: var(--landing-footer-link-hover);
+ }
+
+.landing-footer-bottom {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-top: 30px;
+ border-top: 1px solid var(--landing-footer-rule);
+ font-family: "Cascadia Code", "Cascadia Mono", Consolas, monospace;
+ font-size: 12px;
+}
+
+ .landing-footer-bottom > span:last-child {
+ display: flex;
+ gap: 28px;
+ }
+
+ .landing-footer-bottom a {
+ color: var(--landing-footer-link);
+ text-decoration: none;
+ }
+
+ .landing-footer-bottom a:hover {
+ color: var(--landing-footer-link-hover);
+ }
+
+@media (max-width: 991.98px) {
+ .landing-page .features-section,
+ .architecture-section,
+ .resources-section {
+ padding-top: 78px;
+ padding-bottom: 82px;
+ }
+
+ .landing-page .feature-grid,
+ .resource-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .architecture-grid {
+ grid-template-columns: 1fr;
+ gap: 46px;
+ }
+
+ .architecture-copy {
+ max-width: 760px;
+ width: 100%;
+ margin: 0 auto;
+ }
+
+ .landing-footer-grid {
+ grid-template-columns: minmax(0, 1fr) 180px 180px;
+ gap: 36px;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .landing-page .section-heading,
+ .landing-page .resources-heading {
+ margin-bottom: 38px;
+ }
+
+ .landing-page .section-heading h2,
+ .landing-page .architecture-copy h2,
+ .landing-page .resources-heading h2 {
+ font-size: 31px;
+ }
+
+ .landing-page .feature-grid,
+ .resource-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .landing-page .feature-card {
+ min-height: 0;
+ }
+
+ .landing-footer-grid {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .landing-footer-brand {
+ grid-column: 1 / -1;
+ }
+}
+
+@media (max-width: 575.98px) {
+ .landing-page .features-section,
+ .architecture-section,
+ .resources-section {
+ padding-top: 64px;
+ padding-bottom: 68px;
+ }
+
+ .landing-page .feature-card,
+ .resource-card {
+ padding: 24px;
+ }
+
+ .architecture-points > a {
+ padding: 13px 14px;
+ }
+
+ .landing-footer {
+ padding-top: 62px;
+ }
+
+ .landing-footer-grid {
+ grid-template-columns: 1fr;
+ gap: 42px;
+ padding-bottom: 44px;
+ }
+
+ .landing-footer-brand,
+ .landing-footer-column {
+ grid-column: auto;
+ }
+
+ .landing-footer-bottom {
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 16px;
+ }
}
diff --git a/documents/templates/public/main.js b/documents/templates/public/main.js
index fbf12aad..7ddb5fa4 100644
--- a/documents/templates/public/main.js
+++ b/documents/templates/public/main.js
@@ -1,4 +1,90 @@
-export default {
+const shikiModuleUrl = 'https://esm.sh/shiki@4.3.1';
+const slangGrammarUrl = 'https://raw.githubusercontent.com/shader-slang/slang-vscode-extension/v2.0.10/syntaxes/slang.tmLanguage.json';
+const shikiLanguageAliases = {
+ bash: 'bash',
+ console: 'shellsession',
+ cs: 'csharp',
+ csharp: 'csharp',
+ json: 'json',
+ powershell: 'powershell',
+ shell: 'bash',
+ slang: 'slang',
+ text: 'text',
+ txt: 'text',
+ xml: 'xml',
+ yaml: 'yaml',
+ yml: 'yaml'
+};
+
+let fallbackHighlighter;
+let shikiHighlighter;
+
+const loadShiki = () => {
+ shikiHighlighter ??= Promise.all([
+ import(shikiModuleUrl),
+ fetch(slangGrammarUrl, { cache: 'force-cache' }).then(response => {
+ if (!response.ok) throw new Error(`Unable to load Slang grammar: HTTP ${response.status}`);
+ return response.json();
+ })
+ ]).then(([{ createHighlighter }, slangGrammar]) => {
+ slangGrammar.name = 'slang';
+
+ return createHighlighter({
+ themes: ['light-plus', 'dark-plus'],
+ langs: ['bash', 'csharp', 'json', 'powershell', 'shellsession', 'xml', 'yaml', slangGrammar]
+ });
+ });
+
+ return shikiHighlighter;
+};
+
+const highlightCode = async (code, language) => {
+ const source = code.textContent;
+ const pre = code.closest('pre');
+
+ try {
+ const highlighter = await loadShiki();
+ const highlighted = highlighter.codeToHtml(source, {
+ lang: shikiLanguageAliases[language] || 'text',
+ themes: { light: 'light-plus', dark: 'dark-plus' },
+ defaultColor: false
+ });
+ const template = document.createElement('template');
+ template.innerHTML = highlighted;
+ const shikiPre = template.content.querySelector('pre');
+ const shikiCode = shikiPre?.querySelector('code');
+ if (!pre || !shikiPre || !shikiCode) throw new Error('Shiki returned invalid markup.');
+
+ code.innerHTML = shikiCode.innerHTML;
+ code.classList.remove('hljs');
+ code.dataset.highlighted = 'yes';
+ code.dataset.shiki = 'true';
+ pre.classList.add('shiki', 'shiki-themes', 'light-plus', 'dark-plus');
+
+ for (const property of shikiPre.style) {
+ if (property.startsWith('--shiki-')) {
+ pre.style.setProperty(property, shikiPre.style.getPropertyValue(property));
+ }
+ }
+ } catch (error) {
+ console.warn('Shiki highlighting failed; using the DocFX highlighter.', error);
+ code.textContent = source;
+ code.classList.remove('hljs');
+ code.removeAttribute('data-highlighted');
+
+ try {
+ fallbackHighlighter?.highlightElement(code);
+ } catch {
+ code.dataset.highlighted = 'yes';
+ }
+ }
+};
+
+export default {
+ configureHljs: hljs => {
+ fallbackHighlighter = hljs;
+ hljs.registerAliases(['slang'], { languageName: 'cpp' });
+ },
iconLinks: [
{
icon: 'github',
@@ -7,10 +93,884 @@
}
],
start: () => {
- // Prevent short table cells from wrapping (e.g. "DirectX 12", "Vulkan 1.4")
- for (const td of document.querySelectorAll('article td')) {
- if (td.textContent.trim().length <= 20) {
- td.style.whiteSpace = 'nowrap';
+ const rootPath = document.querySelector('meta[name="docfx:rel"]')?.content || '';
+ const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
+ const observeUntil = (target, options, initialize, timeout = 5000) => {
+ if (initialize() || !target) return;
+
+ let timeoutId;
+ const observer = new MutationObserver(() => {
+ if (!initialize()) return;
+ observer.disconnect();
+ window.clearTimeout(timeoutId);
+ });
+ observer.observe(target, options);
+ timeoutId = window.setTimeout(() => observer.disconnect(), timeout);
+ };
+ document.body.toggleAttribute('data-zenith-api', /\/api(?:\/|$)/i.test(window.location.pathname));
+
+ const initializeThemeCycle = () => {
+ const themes = [
+ { value: 'light', label: 'Light', icon: 'bi-sun' },
+ { value: 'dark', label: 'Dark', icon: 'bi-moon' },
+ { value: 'auto', label: 'Auto', icon: 'bi-circle-half' }
+ ];
+ const systemTheme = window.matchMedia('(prefers-color-scheme: dark)');
+ const navbar = document.getElementById('navbar');
+ if (!navbar) return;
+
+ const enhanceThemeToggle = () => {
+ const dropdown = navbar.querySelector('.icons .dropdown');
+ const currentToggle = dropdown?.querySelector('.dropdown-toggle');
+ if (!dropdown || !currentToggle) return false;
+
+ const themeToggle = document.createElement('button');
+ const icon = document.createElement('i');
+ let animationTimer;
+
+ themeToggle.type = 'button';
+ themeToggle.className = 'btn border-0 zenith-theme-toggle';
+ icon.setAttribute('aria-hidden', 'true');
+ themeToggle.append(icon);
+ dropdown.replaceWith(themeToggle);
+
+ const getTheme = () => {
+ const storedTheme = window.localStorage.getItem('theme');
+ return themes.find(theme => theme.value === storedTheme) || themes[2];
+ };
+
+ const getNextTheme = theme => themes[(themes.indexOf(theme) + 1) % themes.length];
+
+ const renderTheme = theme => {
+ const nextTheme = getNextTheme(theme);
+ icon.className = `bi ${theme.icon}`;
+ themeToggle.dataset.theme = theme.value;
+ themeToggle.title = `${theme.label} theme; switch to ${nextTheme.label}`;
+ themeToggle.setAttribute(
+ 'aria-label',
+ `${theme.label} theme. Switch to ${nextTheme.label}.`
+ );
+ };
+
+ const applyTheme = theme => {
+ const resolvedTheme = theme.value === 'auto'
+ ? (systemTheme.matches ? 'dark' : 'light')
+ : theme.value;
+ window.localStorage.setItem('theme', theme.value);
+ document.documentElement.dataset.bsTheme = resolvedTheme;
+ renderTheme(theme);
+ };
+
+ const switchTheme = () => {
+ const nextTheme = getNextTheme(getTheme());
+ window.clearTimeout(animationTimer);
+
+ if (reducedMotion.matches) {
+ applyTheme(nextTheme);
+ return;
+ }
+
+ themeToggle.disabled = true;
+ themeToggle.classList.remove('is-theme-entering');
+ themeToggle.classList.add('is-theme-leaving');
+ animationTimer = window.setTimeout(() => {
+ applyTheme(nextTheme);
+ themeToggle.classList.remove('is-theme-leaving');
+ themeToggle.classList.add('is-theme-entering');
+ animationTimer = window.setTimeout(() => {
+ themeToggle.classList.remove('is-theme-entering');
+ themeToggle.disabled = false;
+ }, 260);
+ }, 120);
+ };
+
+ themeToggle.addEventListener('click', switchTheme);
+ systemTheme.addEventListener('change', () => {
+ const theme = getTheme();
+ if (theme.value === 'auto') applyTheme(theme);
+ });
+ applyTheme(getTheme());
+ return true;
+ };
+
+ observeUntil(navbar, { childList: true, subtree: true }, enhanceThemeToggle);
+ };
+
+ const initializeExpandableSearch = () => {
+ const form = document.querySelector('#navbar form.search');
+ const input = form?.querySelector('#search-query');
+ const nativeIcon = form?.querySelector(':scope > i');
+ if (!form || !input || form.classList.contains('zenith-search')) return;
+
+ const toggle = document.createElement('button');
+ const icon = document.createElement('i');
+ toggle.type = 'button';
+ toggle.className = 'zenith-search-toggle';
+ toggle.setAttribute('aria-controls', input.id);
+ toggle.setAttribute('aria-expanded', 'false');
+ toggle.setAttribute('aria-label', 'Open search');
+ toggle.title = 'Search';
+ icon.className = 'bi bi-search';
+ icon.setAttribute('aria-hidden', 'true');
+ toggle.append(icon);
+ nativeIcon?.setAttribute('aria-hidden', 'true');
+ form.prepend(toggle);
+ form.classList.add('zenith-search');
+ const searchPlaceholder = input.placeholder || 'Search';
+
+ const updateAvailability = () => {
+ const ready = !input.disabled;
+ const expanded = form.classList.contains('is-expanded');
+ form.classList.toggle('is-loading', !ready);
+ form.setAttribute('aria-busy', String(!ready));
+ toggle.setAttribute('aria-label', expanded ? 'Close search' : 'Open search');
+ toggle.title = ready ? (expanded ? 'Close search' : 'Search') : 'Search is loading';
+ icon.className = 'bi bi-search';
+ input.placeholder = ready ? searchPlaceholder : 'Search is loading...';
+ };
+ const availabilityObserver = new MutationObserver(updateAvailability);
+ availabilityObserver.observe(input, { attributes: true, attributeFilter: ['disabled'] });
+ updateAvailability();
+ let pendingFocusObserver;
+
+ const setExpanded = (expanded, focusInput = false) => {
+ if (!expanded) {
+ pendingFocusObserver?.disconnect();
+ pendingFocusObserver = undefined;
+ }
+ form.classList.toggle('is-expanded', expanded);
+ toggle.setAttribute('aria-expanded', String(expanded));
+ toggle.setAttribute('aria-label', expanded ? 'Close search' : 'Open search');
+ toggle.title = input.disabled ? 'Search is loading' : (expanded ? 'Close search' : 'Search');
+
+ if (!focusInput) return;
+ const focusWhenReady = () => {
+ if (input.disabled) return false;
+ input.focus({ preventScroll: true });
+ return true;
+ };
+ if (focusWhenReady()) return;
+
+ pendingFocusObserver?.disconnect();
+ pendingFocusObserver = new MutationObserver(() => {
+ if (!form.classList.contains('is-expanded')) {
+ pendingFocusObserver?.disconnect();
+ pendingFocusObserver = undefined;
+ return;
+ }
+ if (!focusWhenReady()) return;
+ pendingFocusObserver?.disconnect();
+ pendingFocusObserver = undefined;
+ });
+ pendingFocusObserver.observe(input, { attributes: true, attributeFilter: ['disabled'] });
+ };
+
+ const clearSearch = () => {
+ if (!input.value) return;
+ input.value = '';
+ input.dispatchEvent(new Event('input', { bubbles: true }));
+ };
+
+ toggle.addEventListener('click', () => {
+ const expanded = form.classList.contains('is-expanded');
+ if (expanded) {
+ clearSearch();
+ setExpanded(false);
+ toggle.focus({ preventScroll: true });
+ } else {
+ setExpanded(true, true);
+ }
+ });
+
+ input.addEventListener('focus', () => setExpanded(true));
+ input.addEventListener('keydown', event => {
+ if (event.key !== 'Escape') return;
+ event.preventDefault();
+ clearSearch();
+ setExpanded(false);
+ toggle.focus({ preventScroll: true });
+ });
+ document.addEventListener('pointerdown', event => {
+ if (!form.classList.contains('is-expanded') || form.contains(event.target)) return;
+ if (!input.value && !document.body.hasAttribute('data-search')) setExpanded(false);
+ });
+
+ if (input.value || document.body.hasAttribute('data-search')) setExpanded(true);
+ };
+
+ const initializeReadingMotion = () => {
+ const header = document.querySelector('body > header');
+ if (!header) return;
+
+ const progress = document.createElement('div');
+ const progressValue = document.createElement('span');
+ progress.className = 'zenith-reading-progress';
+ progress.setAttribute('aria-hidden', 'true');
+ progress.append(progressValue);
+ header.append(progress);
+
+ const hasScrollTimeline = window.CSS?.supports?.('animation-timeline: scroll()') === true;
+
+ const updateScrollState = () => {
+ const scrollRange = Math.max(1, document.documentElement.scrollHeight - window.innerHeight);
+ const value = Math.min(1, Math.max(0, window.scrollY / scrollRange));
+ if (!hasScrollTimeline) progressValue.style.transform = `scaleX(${value.toFixed(4)})`;
+ header.classList.toggle('is-scrolled', window.scrollY > 8);
+ };
+
+ window.addEventListener('scroll', updateScrollState, { passive: true });
+ window.addEventListener('resize', updateScrollState, { passive: true });
+ updateScrollState();
+ };
+
+ const initializeOverlayScrollbars = () => {
+ const initialized = new WeakSet();
+ const hideTimers = new WeakMap();
+ const updates = new WeakMap();
+
+ const attach = (scroller, host) => {
+ if (!scroller || !host) return;
+ if (initialized.has(scroller)) {
+ updates.get(scroller)?.();
+ return;
+ }
+ initialized.add(scroller);
+ host.classList.add('zenith-scroll-host');
+
+ const track = document.createElement('span');
+ const thumb = document.createElement('span');
+ track.className = 'zenith-overlay-scrollbar';
+ track.setAttribute('role', 'scrollbar');
+ track.setAttribute('aria-label', host.id === 'toc'
+ ? 'Table of contents scrollbar'
+ : 'In this article scrollbar');
+ track.setAttribute('aria-orientation', 'vertical');
+ track.setAttribute('aria-valuemin', '0');
+ if (!scroller.id) scroller.id = `${host.id || 'zenith'}-scroll-region`;
+ track.setAttribute('aria-controls', scroller.id);
+ track.append(thumb);
+ host.append(track);
+
+ const update = () => {
+ const hostRect = host.getBoundingClientRect();
+ const scrollerRect = scroller.getBoundingClientRect();
+ const trackStyle = window.getComputedStyle(track);
+ const scrollbarSize = Number.parseFloat(
+ trackStyle.getPropertyValue('--zenith-scrollbar-size')
+ ) || track.offsetWidth;
+ const edgeGap = Number.parseFloat(
+ trackStyle.getPropertyValue('--zenith-scrollbar-edge-gap')
+ ) || (scrollbarSize / 2);
+ const minThumbHeight = Number.parseFloat(
+ trackStyle.getPropertyValue('--zenith-scrollbar-min-thumb')
+ ) || (scrollbarSize * 4.5);
+ const trackHeight = Math.max(0, scroller.clientHeight - (edgeGap * 2));
+ const scrollRange = Math.max(0, scroller.scrollHeight - scroller.clientHeight);
+ const thumbHeight = scrollRange
+ ? Math.max(minThumbHeight, trackHeight * (scroller.clientHeight / scroller.scrollHeight))
+ : trackHeight;
+ const thumbRange = Math.max(0, trackHeight - thumbHeight);
+ const thumbOffset = scrollRange ? (scroller.scrollTop / scrollRange) * thumbRange : 0;
+
+ track.style.top = `${scrollerRect.top - hostRect.top + edgeGap}px`;
+ track.style.height = `${trackHeight}px`;
+ track.tabIndex = scrollRange > 1 ? 0 : -1;
+ track.setAttribute('aria-valuemax', `${Math.round(scrollRange)}`);
+ track.setAttribute('aria-valuenow', `${Math.round(scroller.scrollTop)}`);
+ thumb.style.height = `${thumbHeight}px`;
+ thumb.style.transform = `translateY(${thumbOffset}px)`;
+ host.classList.toggle('has-scroll-range', scrollRange > 1);
+ };
+ updates.set(scroller, update);
+
+ const showWhileScrolling = () => {
+ host.classList.add('is-scrolling');
+ window.clearTimeout(hideTimers.get(host));
+ hideTimers.set(host, window.setTimeout(() => {
+ host.classList.remove('is-scrolling');
+ }, 700));
+ update();
+ };
+
+ scroller.addEventListener('scroll', showWhileScrolling, { passive: true });
+ window.addEventListener('resize', update, { passive: true });
+ const resizeObserver = typeof ResizeObserver === 'undefined'
+ ? null
+ : new ResizeObserver(update);
+ resizeObserver?.observe(scroller);
+ resizeObserver?.observe(host);
+ new MutationObserver(update).observe(scroller, { childList: true, subtree: true });
+
+ track.addEventListener('keydown', event => {
+ const lineStep = scroller.clientHeight / 10;
+ const pageStep = scroller.clientHeight;
+ const scrollRange = Math.max(0, scroller.scrollHeight - scroller.clientHeight);
+ const target = {
+ ArrowUp: scroller.scrollTop - lineStep,
+ ArrowDown: scroller.scrollTop + lineStep,
+ PageUp: scroller.scrollTop - pageStep,
+ PageDown: scroller.scrollTop + pageStep,
+ Home: 0,
+ End: scrollRange
+ }[event.key];
+ if (target === undefined) return;
+
+ event.preventDefault();
+ scroller.scrollTop = Math.min(scrollRange, Math.max(0, target));
+ });
+
+ track.addEventListener('pointerdown', event => {
+ if (!host.classList.contains('has-scroll-range')) return;
+ event.preventDefault();
+
+ const trackRect = track.getBoundingClientRect();
+ const thumbRect = thumb.getBoundingClientRect();
+ const startY = event.clientY;
+ let startScrollTop = scroller.scrollTop;
+ const thumbRange = Math.max(1, trackRect.height - thumbRect.height);
+ const scrollRange = Math.max(0, scroller.scrollHeight - scroller.clientHeight);
+
+ if (event.target !== thumb) {
+ const targetOffset = Math.min(
+ thumbRange,
+ Math.max(0, event.clientY - trackRect.top - (thumbRect.height / 2))
+ );
+ scroller.scrollTop = (targetOffset / thumbRange) * scrollRange;
+ startScrollTop = scroller.scrollTop;
+ }
+
+ track.classList.add('is-dragging');
+ track.setPointerCapture(event.pointerId);
+ const move = moveEvent => {
+ scroller.scrollTop = startScrollTop
+ + ((moveEvent.clientY - startY) / thumbRange) * scrollRange;
+ };
+ const stop = () => {
+ track.classList.remove('is-dragging');
+ track.removeEventListener('pointermove', move);
+ track.removeEventListener('pointerup', stop);
+ track.removeEventListener('pointercancel', stop);
+ };
+ track.addEventListener('pointermove', move);
+ track.addEventListener('pointerup', stop);
+ track.addEventListener('pointercancel', stop);
+ });
+ update();
+ };
+
+ const initialize = () => {
+ const tocScroller = document.querySelector('#toc > .overflow-y-auto');
+ attach(tocScroller, document.querySelector('#toc'));
+
+ const affix = document.querySelector('#affix');
+ attach(affix, affix?.parentElement);
+ };
+
+ const observer = new MutationObserver(initialize);
+ for (const host of document.querySelectorAll('#toc, main > .affix')) {
+ observer.observe(host, { childList: true, subtree: true });
+ }
+ initialize();
+ };
+
+ const copyText = async (text) => {
+ try {
+ await navigator.clipboard.writeText(text);
+ return;
+ } catch {
+ const textarea = document.createElement('textarea');
+ textarea.value = text;
+ textarea.setAttribute('readonly', '');
+ textarea.style.position = 'fixed';
+ textarea.style.opacity = '0';
+ document.body.append(textarea);
+ textarea.select();
+
+ const copied = document.execCommand('copy');
+ textarea.remove();
+ if (!copied) throw new Error('Copy failed');
+ }
+ };
+
+ const copyResetTimers = new WeakMap();
+ const setCopyButtonState = (button, state) => {
+ const defaultLabel = button.dataset.copyLabel || button.getAttribute('aria-label') || 'Copy';
+ const icon = button.querySelector('i');
+ const feedback = {
+ idle: { icon: 'bi bi-copy', label: defaultLabel },
+ copied: { icon: 'bi bi-check2', label: 'Copied' },
+ failed: { icon: 'bi bi-x-lg', label: 'Copy failed' }
+ }[state];
+
+ button.dataset.copyLabel = defaultLabel;
+ button.classList.toggle('is-copied', state === 'copied');
+ button.classList.toggle('is-failed', state === 'failed');
+ button.setAttribute('aria-label', feedback.label);
+ button.title = feedback.label;
+ if (icon) icon.className = feedback.icon;
+
+ const activeTimer = copyResetTimers.get(button);
+ if (activeTimer) window.clearTimeout(activeTimer);
+
+ if (state === 'idle') {
+ copyResetTimers.delete(button);
+ } else {
+ copyResetTimers.set(button, window.setTimeout(() => {
+ copyResetTimers.delete(button);
+ setCopyButtonState(button, 'idle');
+ }, 2000));
+ }
+ };
+
+ const copyWithFeedback = async (button, text) => {
+ try {
+ await copyText(text);
+ setCopyButtonState(button, 'copied');
+ } catch {
+ setCopyButtonState(button, 'failed');
+ }
+ };
+
+ const brand = document.querySelector('.navbar-brand');
+ if (brand && !brand.hasAttribute('aria-label')) {
+ brand.setAttribute('aria-label', 'Zenith.NET home');
+ }
+ initializeThemeCycle();
+ initializeExpandableSearch();
+
+ const languageNames = {
+ bash: 'Shell',
+ csharp: 'C#',
+ cs: 'C#',
+ console: 'Console',
+ json: 'JSON',
+ powershell: 'PowerShell',
+ shell: 'Shell',
+ slang: 'Slang',
+ xml: 'XML',
+ yaml: 'YAML',
+ yml: 'YAML'
+ };
+
+ const createCodeFrame = pre => {
+ const code = pre.querySelector('code');
+ if (!code) return null;
+
+ const languageClass = [...code.classList].find(name => name.startsWith('language-') || name.startsWith('lang-'));
+ const language = languageClass?.replace(/^language-|^lang-/, '').toLowerCase() || 'text';
+ const existingFrame = pre.closest('.doc-code-frame');
+ if (existingFrame) return { code, language };
+ const frame = document.createElement('div');
+ const toolbar = document.createElement('div');
+ const languageLabel = document.createElement('span');
+ const copy = document.createElement('button');
+
+ frame.className = 'doc-code-frame';
+ toolbar.className = 'doc-code-toolbar';
+ languageLabel.className = 'doc-code-language';
+ languageLabel.textContent = languageNames[language] || language.toUpperCase();
+ copy.type = 'button';
+ copy.className = 'doc-code-copy';
+ copy.setAttribute('aria-label', 'Copy code block');
+ copy.innerHTML = '
';
+ setCopyButtonState(copy, 'idle');
+
+ pre.parentNode.insertBefore(frame, pre);
+ frame.append(toolbar, pre);
+ toolbar.append(languageLabel);
+ toolbar.append(copy);
+ pre.querySelector(':scope > .code-action')?.remove();
+
+ copy.addEventListener('click', async () => {
+ await copyWithFeedback(copy, code.textContent);
+ });
+
+ return { code, language };
+ };
+
+ for (const pre of document.querySelectorAll('body:not([data-layout="landing"]) article pre')) {
+ const controls = createCodeFrame(pre);
+ if (controls) void highlightCode(controls.code, controls.language);
+ }
+
+ for (const table of document.querySelectorAll('body:not([data-layout="landing"]) article table')) {
+ if (table.closest('.doc-table-frame')) continue;
+
+ const frame = document.createElement('div');
+ frame.className = 'doc-table-frame';
+ const responsive = table.closest('.table-responsive');
+ const target = responsive || table;
+ target.parentNode.insertBefore(frame, target);
+ frame.append(target);
+ }
+
+ const initializeTableScrollCues = () => {
+ const frames = [...document.querySelectorAll('.doc-table-frame')];
+ if (!frames.length) return;
+
+ const observedScrollers = new WeakSet();
+ const updates = new WeakMap();
+ const resizeObserver = typeof ResizeObserver === 'undefined'
+ ? null
+ : new ResizeObserver(entries => {
+ for (const entry of entries) updates.get(entry.target)?.();
+ });
+
+ const initializeFrame = frame => {
+ const scroller = frame.querySelector('.table-responsive');
+ if (!scroller || observedScrollers.has(scroller)) return false;
+ observedScrollers.add(scroller);
+
+ const update = () => {
+ const maxScroll = Math.max(0, scroller.scrollWidth - scroller.clientWidth);
+ frame.classList.toggle('is-scrollable', maxScroll > 1);
+ frame.classList.toggle('is-scroll-end', scroller.scrollLeft >= maxScroll - 1);
+ };
+
+ scroller.addEventListener('scroll', update, { passive: true });
+ if (!resizeObserver) window.addEventListener('resize', update, { passive: true });
+ updates.set(scroller, update);
+ resizeObserver?.observe(scroller);
+
+ const table = scroller.querySelector('table');
+ if (table) {
+ updates.set(table, update);
+ resizeObserver?.observe(table);
+ }
+ update();
+ return true;
+ };
+
+ const pendingFrames = new Set(frames);
+ const initializePendingFrames = () => {
+ for (const frame of pendingFrames) {
+ if (initializeFrame(frame)) pendingFrames.delete(frame);
+ }
+ if (!pendingFrames.size) mutationObserver.disconnect();
+ };
+ const mutationObserver = new MutationObserver(initializePendingFrames);
+
+ for (const frame of pendingFrames) {
+ mutationObserver.observe(frame, { childList: true, subtree: true });
+ }
+ initializePendingFrames();
+ };
+
+ const initializeTutorialCarousel = () => {
+ const carousel = document.querySelector('[data-tutorial-carousel]');
+ if (!carousel) return;
+
+ const slides = [...carousel.querySelectorAll('.render-carousel-slide')];
+ const title = carousel.querySelector('[data-carousel-title]');
+ const position = carousel.querySelector('[data-carousel-position]');
+ const status = carousel.querySelector('[data-carousel-status]');
+ const progress = carousel.querySelector('[data-carousel-progress]');
+ const openLink = carousel.querySelector('[data-carousel-open]');
+ const toggleButton = carousel.querySelector('[data-carousel-toggle]');
+ const previousButton = carousel.querySelector('[data-carousel-prev]');
+ const nextButton = carousel.querySelector('[data-carousel-next]');
+ const mobileViewport = window.matchMedia('(max-width: 767.98px)');
+ const autoPlayDuration = 6500;
+ let activeIndex = Math.max(0, slides.findIndex(slide => slide.classList.contains('is-active')));
+ let autoPlayTimer = 0;
+ let autoPlayRemaining = autoPlayDuration;
+ let autoPlayStartedAt = 0;
+ let isAutoPlayPaused = false;
+ let isVisible = true;
+ let touchStartX = 0;
+ let touchStartY = 0;
+
+ if (slides.length < 2 || !title || !position || !progress || !openLink || !toggleButton || !previousButton || !nextButton) return;
+
+ const prepareSlide = index => {
+ const image = slides[index]?.querySelector('img[data-src]');
+ if (!image?.dataset.src) return;
+
+ image.src = image.dataset.src;
+ image.removeAttribute('data-src');
+ };
+
+ const stopAutoPlay = () => {
+ window.clearTimeout(autoPlayTimer);
+ autoPlayTimer = 0;
+ if (autoPlayStartedAt) {
+ autoPlayRemaining = Math.max(0, autoPlayRemaining - (performance.now() - autoPlayStartedAt));
+ autoPlayStartedAt = 0;
+ }
+ carousel.classList.remove('is-carousel-playing');
+ };
+
+ const canAutoPlay = () => !isAutoPlayPaused &&
+ !reducedMotion.matches &&
+ !mobileViewport.matches &&
+ !document.hidden &&
+ isVisible &&
+ !carousel.matches(':hover') &&
+ !carousel.matches(':focus-within');
+
+ const scheduleAutoPlay = () => {
+ if (!canAutoPlay()) {
+ stopAutoPlay();
+ return;
+ }
+ if (autoPlayTimer) return;
+
+ autoPlayStartedAt = performance.now();
+ carousel.classList.add('is-carousel-playing');
+ autoPlayTimer = window.setTimeout(() => {
+ autoPlayTimer = 0;
+ autoPlayStartedAt = 0;
+ autoPlayRemaining = autoPlayDuration;
+ showSlide(activeIndex + 1);
+ }, Math.max(16, autoPlayRemaining));
+ };
+
+ const showSlide = (index, announce = false) => {
+ stopAutoPlay();
+ activeIndex = (index + slides.length) % slides.length;
+ prepareSlide(activeIndex);
+ prepareSlide((activeIndex + 1) % slides.length);
+
+ for (const [slideIndex, slide] of slides.entries()) {
+ const isActive = slideIndex === activeIndex;
+ slide.classList.toggle('is-active', isActive);
+ slide.setAttribute('aria-hidden', String(!isActive));
+ if (isActive) {
+ slide.setAttribute('aria-current', 'true');
+ } else {
+ slide.removeAttribute('aria-current');
+ }
+ slide.tabIndex = isActive ? 0 : -1;
+ }
+
+ const activeSlide = slides[activeIndex];
+ const activeTitle = activeSlide.dataset.carouselTitle || `Tutorial ${activeIndex + 1}`;
+ title.textContent = activeTitle;
+ position.textContent = `${activeIndex + 1} / ${slides.length}`;
+ openLink.href = activeSlide.href;
+ openLink.setAttribute('aria-label', `Open ${activeTitle} tutorial`);
+ if (announce && status) {
+ status.textContent = `${activeTitle}, slide ${activeIndex + 1} of ${slides.length}`;
+ }
+
+ autoPlayRemaining = autoPlayDuration;
+ progress.classList.remove('is-timing');
+ void progress.offsetWidth;
+ progress.classList.add('is-timing');
+ scheduleAutoPlay();
+ };
+
+ const updateToggleButton = () => {
+ const label = isAutoPlayPaused ? 'Start automatic slide rotation' : 'Pause automatic slide rotation';
+ toggleButton.setAttribute('aria-label', label);
+ toggleButton.title = label;
+ toggleButton.innerHTML = `
`;
+ };
+
+ toggleButton.addEventListener('click', () => {
+ isAutoPlayPaused = !isAutoPlayPaused;
+ updateToggleButton();
+ if (isAutoPlayPaused) {
+ stopAutoPlay();
+ } else {
+ autoPlayRemaining = autoPlayDuration;
+ scheduleAutoPlay();
+ }
+ });
+ previousButton.addEventListener('click', () => showSlide(activeIndex - 1, true));
+ nextButton.addEventListener('click', () => showSlide(activeIndex + 1, true));
+
+ carousel.addEventListener('keydown', event => {
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
+
+ event.preventDefault();
+ showSlide(activeIndex + (event.key === 'ArrowRight' ? 1 : -1), true);
+ });
+ carousel.addEventListener('mouseenter', stopAutoPlay);
+ carousel.addEventListener('mouseleave', scheduleAutoPlay);
+ carousel.addEventListener('focusin', stopAutoPlay);
+ carousel.addEventListener('focusout', () => window.requestAnimationFrame(scheduleAutoPlay));
+ carousel.addEventListener('touchstart', event => {
+ const touch = event.changedTouches[0];
+ touchStartX = touch.clientX;
+ touchStartY = touch.clientY;
+ stopAutoPlay();
+ }, { passive: true });
+ carousel.addEventListener('touchend', event => {
+ const touch = event.changedTouches[0];
+ const deltaX = touch.clientX - touchStartX;
+ const deltaY = touch.clientY - touchStartY;
+ if (Math.abs(deltaX) >= 44 && Math.abs(deltaX) > Math.abs(deltaY)) {
+ showSlide(activeIndex + (deltaX < 0 ? 1 : -1), true);
+ } else {
+ scheduleAutoPlay();
+ }
+ }, { passive: true });
+
+ document.addEventListener('visibilitychange', scheduleAutoPlay);
+ reducedMotion.addEventListener('change', scheduleAutoPlay);
+ mobileViewport.addEventListener('change', scheduleAutoPlay);
+
+ if ('IntersectionObserver' in window) {
+ const observer = new IntersectionObserver(entries => {
+ isVisible = entries[0]?.isIntersecting ?? true;
+ scheduleAutoPlay();
+ }, { threshold: 0.2 });
+ observer.observe(carousel);
+ }
+
+ carousel.classList.add('is-carousel-ready');
+ showSlide(activeIndex);
+ };
+
+ const initializeRevealMotion = () => {
+ const selectors = document.body.matches('[data-layout="landing"]')
+ ? [
+ '.section-heading',
+ '.feature-card',
+ '.architecture-copy',
+ '.resources-heading',
+ '.resource-card',
+ '.landing-footer-grid > *'
+ ]
+ : [
+ 'article h1:first-of-type',
+ 'article h2',
+ 'article .alert',
+ 'article .doc-code-frame',
+ 'article .doc-table-frame',
+ 'article .tabGroup'
+ ];
+ const targets = [...document.querySelectorAll(selectors.join(','))]
+ .filter(element => element.getClientRects().length);
+ if (!targets.length || reducedMotion.matches) {
+ targets.forEach(element => element.classList.add('is-visible'));
+ return;
+ }
+
+ for (const [index, target] of targets.entries()) {
+ target.classList.add('zenith-reveal');
+ target.style.setProperty('--zenith-reveal-delay', `${Math.min(index % 4, 3) * 45}ms`);
+ if (document.body.matches('[data-layout="landing"]')) {
+ const direction = target.matches('.architecture-grid > .architecture-copy:first-child')
+ ? 'left'
+ : target.matches('.architecture-grid > .architecture-copy:last-child')
+ ? 'right'
+ : 'up';
+ target.dataset.revealFrom = direction;
+ }
+ }
+ document.documentElement.classList.add('zenith-motion-ready');
+
+ const pending = new Set(targets);
+ const revealVisibleTargets = () => {
+ const revealBoundary = window.innerHeight * 0.92;
+ for (const target of pending) {
+ const rect = target.getBoundingClientRect();
+ if (rect.top >= revealBoundary || rect.bottom <= 0) continue;
+
+ target.classList.add('is-visible');
+ pending.delete(target);
+ }
+
+ if (!pending.size) {
+ window.removeEventListener('scroll', revealVisibleTargets);
+ window.removeEventListener('resize', revealVisibleTargets);
+ }
+ };
+
+ window.addEventListener('scroll', revealVisibleTargets, { passive: true });
+ window.addEventListener('resize', revealVisibleTargets, { passive: true });
+ revealVisibleTargets();
+ };
+
+ initializeReadingMotion();
+ initializeOverlayScrollbars();
+ initializeTableScrollCues();
+ initializeTutorialCarousel();
+
+ const initializeAffix = () => {
+ const links = [...document.querySelectorAll('#affix a[href^="#"]')];
+ const targetGroups = new Map();
+ const targetIndexes = new Map();
+
+ for (const link of links) {
+ const targetId = decodeURIComponent(link.hash.slice(1));
+ if (!targetGroups.has(targetId)) {
+ targetGroups.set(targetId, [...document.querySelectorAll(`[id="${CSS.escape(targetId)}"]`)]);
+ }
+ }
+
+ const affixLinks = links
+ .map(link => {
+ const targetId = decodeURIComponent(link.hash.slice(1));
+ const targetIndex = targetIndexes.get(targetId) || 0;
+ const targets = targetGroups.get(targetId) || [];
+ const target = targets[targetIndex] || targets[0];
+ targetIndexes.set(targetId, targetIndex + 1);
+
+ if (target && targetIndex > 0) {
+ const uniqueId = `${targetId}--${targetIndex + 1}`;
+ target.id = uniqueId;
+ link.setAttribute('href', `#${uniqueId}`);
+ }
+
+ return { link, target };
+ })
+ .filter(item => {
+ const isVisible = item.target?.getClientRects().length;
+ if (!isVisible) item.link.closest('li')?.style.setProperty('display', 'none');
+ return isVisible;
+ });
+ if (!affixLinks.length) return false;
+
+ const updateAffix = () => {
+ const scrollPaddingTop = Number.parseFloat(
+ window.getComputedStyle(document.documentElement).scrollPaddingTop
+ ) || 0;
+ let current = affixLinks[0];
+ let start = 0;
+ let end = affixLinks.length - 1;
+
+ while (start <= end) {
+ const middle = Math.floor((start + end) / 2);
+ const item = affixLinks[middle];
+ const scrollMarginTop = Number.parseFloat(
+ window.getComputedStyle(item.target).scrollMarginTop
+ ) || 0;
+ const targetOffset = scrollPaddingTop + scrollMarginTop + 1;
+
+ if (item.target.getBoundingClientRect().top <= targetOffset) {
+ current = item;
+ start = middle + 1;
+ } else {
+ end = middle - 1;
+ }
+ }
+
+ for (const item of affixLinks) {
+ item.link.classList.toggle('is-active', item === current);
+ }
+ };
+
+ window.addEventListener('scroll', updateAffix, { passive: true });
+ updateAffix();
+ return true;
+ };
+
+ const normalizePath = path => path.replace(/\/index\.html$/, '/');
+ const siteRootPath = normalizePath(new URL(rootPath || './', window.location.href).pathname);
+ const currentPath = normalizePath(window.location.pathname);
+ for (const link of document.querySelectorAll('#navbar .nav-link')) {
+ const linkPath = normalizePath(new URL(link.href, window.location.href).pathname);
+ const isHome = linkPath === siteRootPath && currentPath === siteRootPath;
+ const isSection = linkPath !== siteRootPath && currentPath.startsWith(linkPath);
+ const isActive = isHome || isSection;
+ link.classList.toggle('active', isActive);
+ if (isActive) {
+ link.setAttribute('aria-current', 'page');
+ } else {
+ link.removeAttribute('aria-current');
}
}
@@ -19,10 +979,10 @@
dl.style.display = 'none';
}
- // Hide protected and override members from API pages
+ // Hide protected members from API pages
for (const code of document.querySelectorAll('.codewrapper pre code')) {
const text = code.textContent.trim();
- if (!/^protected\s/.test(text) && !/\boverride\b/.test(text)) continue;
+ if (!/^(?:private\s+)?protected\b/.test(text)) continue;
const wrapper = code.closest('.codewrapper');
if (!wrapper) continue;
@@ -70,25 +1030,78 @@
}
}
+ observeUntil(
+ document.querySelector('main > .affix'),
+ { childList: true, subtree: true },
+ initializeAffix
+ );
+
+ initializeRevealMotion();
+
// Search results: navigate in same tab with back button support
- document.addEventListener('click', (e) => {
- const link = e.target.closest('#search-results .sr-item a');
+ document.addEventListener('click', event => {
+ const link = event.target instanceof Element
+ ? event.target.closest('#search-results .sr-item a')
+ : null;
if (!link) return;
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey ||
+ event.shiftKey || event.altKey || link.hasAttribute('download') ||
+ (link.target && link.target !== '_self')) return;
- e.preventDefault();
+ event.preventDefault();
const query = document.getElementById('search-query')?.value || '';
- history.replaceState({ search: true, query }, '');
+ history.replaceState({ search: true, query, scrollY: window.scrollY }, '');
window.location.href = link.href;
});
- window.addEventListener('popstate', (e) => {
- if (e.state?.search) {
- const input = document.getElementById('search-query');
- if (input) {
- input.value = e.state.query;
- input.dispatchEvent(new Event('input', { bubbles: true }));
- }
- }
- });
+ const restoreSearchState = (state) => {
+ if (!state?.search) return;
+
+ const input = document.getElementById('search-query');
+ if (!input) return;
+
+ input.value = state.query || '';
+ input.dispatchEvent(new Event('input', { bubbles: true }));
+
+ if (!Number.isFinite(state.scrollY)) return;
+
+ const restoreScroll = () => {
+ if (!document.body.hasAttribute('data-search')) return false;
+
+ const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
+ if (maxScroll < state.scrollY) return false;
+
+ window.scrollTo(0, state.scrollY);
+ return true;
+ };
+
+ if (restoreScroll()) return;
+
+ const observer = new MutationObserver(() => {
+ if (restoreScroll()) observer.disconnect();
+ });
+ observer.observe(document.body, { attributes: true, childList: true, subtree: true });
+ window.setTimeout(() => {
+ restoreScroll();
+ observer.disconnect();
+ }, 2000);
+ };
+
+ window.addEventListener('popstate', (e) => restoreSearchState(e.state));
+
+ const restoreInitialSearchState = () => {
+ if (!history.state?.search) return;
+
+ const input = document.getElementById('search-query');
+ if (!input) return;
+
+ observeUntil(input, { attributes: true, attributeFilter: ['disabled'] }, () => {
+ if (input.disabled) return;
+ restoreSearchState(history.state);
+ return true;
+ });
+ };
+
+ restoreInitialSearchState();
}
-}
\ No newline at end of file
+}
diff --git a/documents/tutorials/advanced/mesh-shading.md b/documents/tutorials/advanced/mesh-shading.md
deleted file mode 100644
index 19440d74..00000000
--- a/documents/tutorials/advanced/mesh-shading.md
+++ /dev/null
@@ -1,694 +0,0 @@
-# Mesh Shading
-
-In this tutorial, you'll render 1,000 procedural UV spheres using the mesh shader pipeline with GPU-driven frustum culling. This demonstrates the modern mesh shading approach where geometry is generated and culled entirely on the GPU.
-
-> [!NOTE]
-> This tutorial requires a GPU with mesh shading support (e.g., NVIDIA Turing+, AMD RDNA 2+, or Apple M3+).
-
-## Overview
-
-This tutorial covers:
-
-- Creating a **mesh shading pipeline** with amplification, mesh, and pixel stages
-- Generating **procedural sphere geometry** (vertices and triangles) on the CPU
-- Implementing **GPU-driven frustum culling** in the amplification shader
-- Using `groupshared` memory and atomic operations for visible instance compaction
-- Extracting **frustum planes** from the view-projection matrix
-- Dispatching mesh groups with `DispatchMesh`
-
-## Key Concepts
-
-### Mesh Shader Pipeline
-
-The mesh shader pipeline replaces the traditional vertex/geometry pipeline:
-
-| Stage | Role | Thread Group Size |
-|-------|------|-------------------|
-| **Amplification** | Decides which mesh groups to spawn (culling) | 32 |
-| **Mesh** | Outputs vertices and triangles per group | 120 |
-| **Pixel** | Standard fragment shading | — |
-
-### Frustum Culling
-
-The amplification shader tests each instance's bounding sphere against 6 frustum planes. Only visible instances are passed to mesh shader groups via a payload:
-
-```
-Payload { InstanceIndices[ASGroupSize] }
-
-// Amplification:
-visible = !IsFrustumCulled(position, radius)
-if (visible) payload.InstanceIndices[atomicAdd(count)] = instanceIndex
-DispatchMesh(visibleCount, 1, 1, payload)
-```
-
-## The Renderer Class
-
-Create the file `Renderers/MeshShadingRenderer.cs`:
-
-```csharp
-namespace ZenithTutorials.Renderers;
-
-internal unsafe class MeshShadingRenderer : IRenderer
-{
- private const uint ASGroupSize = 32;
- private const uint MeshGroupSize = 120;
- private const uint GridSize = 10;
- private const uint TotalInstances = GridSize * GridSize * GridSize;
- private const uint DispatchGroupCount = (TotalInstances + ASGroupSize - 1) / ASGroupSize;
-
- private const string ShaderSource = """
- static const uint GridSize = 10;
- static const uint TotalInstances = GridSize * GridSize * GridSize;
- static const float InstanceSpacing = 2.5;
- static const uint ASGroupSize = 32;
- static const float BoundingSphereRadius = 0.5;
-
- static const uint SphereVertexCount = 62;
- static const uint SphereTriangleCount = 120;
- static const float GridOffset = float(GridSize - 1) * 0.5 * InstanceSpacing;
-
- struct Vertex
- {
- private float4 PositionAndPadding;
-
- private float4 NormalAndPadding;
-
- property float3 Position
- {
- get {
- return PositionAndPadding.xyz;
- }
- }
-
- property float3 Normal
- {
- get {
- return NormalAndPadding.xyz;
- }
- }
- };
-
- struct Triangle
- {
- private uint4 IndicesAndPadding;
-
- property uint3 Indices
- {
- get {
- return IndicesAndPadding.xyz;
- }
- }
- };
-
- struct Payload
- {
- uint InstanceIndices[ASGroupSize];
- };
-
- struct VertexOutput
- {
- float4 Position : SV_POSITION;
-
- float3 WorldNormal : WORLDNORMAL;
-
- float3 Color : COLOR;
- };
-
- struct Constants
- {
- float4x4 ViewProjection;
-
- float4 FrustumPlanes[6];
-
- private float4 TimeAndLightDirection;
-
- property float Time
- {
- get {
- return TimeAndLightDirection.x;
- }
- }
-
- property float3 LightDirection
- {
- get {
- return TimeAndLightDirection.yzw;
- }
- }
- };
-
- void DecomposeInstanceID(uint id, out uint x, out uint y, out uint z)
- {
- x = id % GridSize;
- y = (id / GridSize) % GridSize;
- z = id / (GridSize * GridSize);
- }
-
- float3 InstancePosition(uint id)
- {
- uint x, y, z;
- DecomposeInstanceID(id, x, y, z);
- return float3(x, y, z) * InstanceSpacing - GridOffset;
- }
-
- float3 InstanceColor(uint id)
- {
- uint x, y, z;
- DecomposeInstanceID(id, x, y, z);
- return float3(x, y, z) / float(GridSize - 1);
- }
-
- bool IsFrustumCulled(float3 center, float radius)
- {
- for (uint i = 0; i < 6; i++)
- {
- float4 plane = constants.FrustumPlanes[i];
- if (dot(plane.xyz, center) + plane.w < -radius)
- {
- return true;
- }
- }
- return false;
- }
-
- ConstantBuffer
constants;
- StructuredBuffer vertices;
- StructuredBuffer indices;
-
- groupshared Payload s_payload;
- groupshared uint s_visibleCount;
-
- [shader("amplification")]
- [numthreads(ASGroupSize, 1, 1)]
- void ASMain(uint groupID: SV_GroupID, uint groupThreadID: SV_GroupThreadID)
- {
- uint instanceIndex = groupID * ASGroupSize + groupThreadID;
-
- bool visible = false;
- if (instanceIndex < TotalInstances)
- {
- float3 worldPos = InstancePosition(instanceIndex);
- visible = !IsFrustumCulled(worldPos, BoundingSphereRadius);
- }
-
- if (groupThreadID == 0)
- {
- s_visibleCount = 0;
- }
-
- GroupMemoryBarrierWithGroupSync();
-
- if (visible)
- {
- uint offset;
- InterlockedAdd(s_visibleCount, 1, offset);
- s_payload.InstanceIndices[offset] = instanceIndex;
- }
-
- GroupMemoryBarrierWithGroupSync();
-
- DispatchMesh(s_visibleCount, 1, 1, s_payload);
- }
-
- [shader("mesh")]
- [numthreads(120, 1, 1)]
- [outputtopology("triangle")]
- void MSMain(uint groupID: SV_GroupID, uint groupThreadID: SV_GroupThreadID, in payload Payload meshPayload,
- OutputVertices outVertices, OutputIndices outIndices)
- {
- uint instanceIndex = meshPayload.InstanceIndices[groupID];
- float3 instancePos = InstancePosition(instanceIndex);
- float3 color = InstanceColor(instanceIndex);
-
- SetMeshOutputCounts(SphereVertexCount, SphereTriangleCount);
-
- if (groupThreadID < SphereVertexCount)
- {
- Vertex v = vertices[groupThreadID];
- float3 worldPos = v.Position + instancePos;
-
- VertexOutput output;
- output.Position = mul(float4(worldPos, 1.0), constants.ViewProjection);
- output.WorldNormal = v.Normal;
- output.Color = color;
-
- outVertices[groupThreadID] = output;
- }
-
- if (groupThreadID < SphereTriangleCount)
- {
- outIndices[groupThreadID] = indices[groupThreadID].Indices;
- }
- }
-
- [shader("pixel")]
- float4 PSMain(VertexOutput input) : SV_TARGET
- {
- float3 lightDir = normalize(constants.LightDirection);
- float3 normal = normalize(input.WorldNormal);
- float ndotl = max(dot(normal, lightDir), 0.0);
-
- float3 ambient = input.Color * 0.15;
- float3 diffuse = input.Color * ndotl * 0.85;
-
- return float4(ambient + diffuse, 1.0);
- }
- """;
-
- private readonly Buffer vertexBuffer;
- private readonly Buffer indexBuffer;
- private readonly Buffer constantsBuffer;
- private readonly ResourceLayout resourceLayout;
- private readonly ResourceTable resourceTable;
- private readonly MeshShadingPipeline pipeline;
-
- private float totalTime;
-
- public MeshShadingRenderer()
- {
- if (!App.Context.Capabilities.MeshShadingSupported)
- {
- throw new NotSupportedException("Mesh shading is not supported on this device.");
- }
-
- const int lonSegments = 12;
- const int latSegments = 6;
- const float radius = 0.5f;
-
- List sphereVertices = [];
- List sphereTriangles = [];
-
- sphereVertices.Add(new() { Position = new(0, radius, 0), Normal = Vector3.UnitY });
-
- for (int lat = 1; lat < latSegments; lat++)
- {
- float phi = MathF.PI * lat / latSegments;
- float sinPhi = MathF.Sin(phi);
- float cosPhi = MathF.Cos(phi);
-
- for (int lon = 0; lon < lonSegments; lon++)
- {
- float theta = 2.0f * MathF.PI * lon / lonSegments;
- Vector3 normal = new(sinPhi * MathF.Cos(theta), cosPhi, sinPhi * MathF.Sin(theta));
-
- sphereVertices.Add(new() { Position = normal * radius, Normal = normal });
- }
- }
-
- sphereVertices.Add(new() { Position = new(0, -radius, 0), Normal = -Vector3.UnitY });
-
- for (int lon = 0; lon < lonSegments; lon++)
- {
- uint next = (uint)((lon + 1) % lonSegments);
-
- sphereTriangles.Add(new() { Index0 = 0, Index1 = (uint)(1 + lon), Index2 = 1 + next });
- }
-
- for (int lat = 0; lat < latSegments - 2; lat++)
- {
- for (int lon = 0; lon < lonSegments; lon++)
- {
- uint next = (uint)((lon + 1) % lonSegments);
- uint tl = (uint)(1 + (lat * lonSegments) + lon);
- uint tr = (uint)(1 + (lat * lonSegments)) + next;
- uint bl = (uint)(1 + ((lat + 1) * lonSegments) + lon);
- uint br = (uint)(1 + ((lat + 1) * lonSegments)) + next;
-
- sphereTriangles.Add(new() { Index0 = tl, Index1 = bl, Index2 = tr });
- sphereTriangles.Add(new() { Index0 = tr, Index1 = bl, Index2 = br });
- }
- }
-
- uint bottomPole = (uint)(sphereVertices.Count - 1);
- uint lastRing = 1 + ((latSegments - 2) * lonSegments);
-
- for (int lon = 0; lon < lonSegments; lon++)
- {
- uint next = (uint)((lon + 1) % lonSegments);
-
- sphereTriangles.Add(new() { Index0 = bottomPole, Index1 = lastRing + next, Index2 = lastRing + (uint)lon });
- }
-
- Vertex[] vertexData = [.. sphereVertices];
- Triangle[] triangleData = [.. sphereTriangles];
-
- vertexBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(Vertex) * vertexData.Length),
- StrideInBytes = (uint)sizeof(Vertex),
- Flags = BufferUsageFlags.ShaderResource
- });
- vertexBuffer.Upload(vertexData, 0);
-
- indexBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(Triangle) * triangleData.Length),
- StrideInBytes = (uint)sizeof(Triangle),
- Flags = BufferUsageFlags.ShaderResource
- });
- indexBuffer.Upload(triangleData, 0);
-
- constantsBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)sizeof(Constants),
- StrideInBytes = (uint)sizeof(Constants),
- Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite
- });
-
- resourceLayout = App.Context.CreateResourceLayout(new()
- {
- Bindings = BindingHelper.Bindings
- (
- new() { Type = ResourceType.ConstantBuffer, Count = 1, StageFlags = ShaderStageFlags.Amplification | ShaderStageFlags.Mesh | ShaderStageFlags.Pixel },
- new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Mesh },
- new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Mesh }
- )
- });
-
- resourceTable = App.Context.CreateResourceTable(new()
- {
- Layout = resourceLayout,
- Resources = [constantsBuffer, vertexBuffer, indexBuffer]
- });
-
- using Shader ampShader = App.Context.LoadShaderFromSource(ShaderSource, "ASMain", ShaderStageFlags.Amplification);
- using Shader meshShader = App.Context.LoadShaderFromSource(ShaderSource, "MSMain", ShaderStageFlags.Mesh);
- using Shader pixelShader = App.Context.LoadShaderFromSource(ShaderSource, "PSMain", ShaderStageFlags.Pixel);
-
- pipeline = App.Context.CreateMeshShadingPipeline(new()
- {
- RenderStates = new()
- {
- RasterizerState = RasterizerStates.CullBack,
- DepthStencilState = DepthStencilStates.Default,
- BlendState = BlendStates.Opaque
- },
- Amplification = ampShader,
- Mesh = meshShader,
- Pixel = pixelShader,
- ResourceLayout = resourceLayout,
- PrimitiveTopology = PrimitiveTopology.TriangleList,
- Output = App.FrameBuffer.Output,
- AmplificationThreadGroupSizeX = ASGroupSize,
- AmplificationThreadGroupSizeY = 1,
- AmplificationThreadGroupSizeZ = 1,
- MeshThreadGroupSizeX = MeshGroupSize,
- MeshThreadGroupSizeY = 1,
- MeshThreadGroupSizeZ = 1
- });
- }
-
- public void Update(double deltaTime)
- {
- totalTime += (float)deltaTime;
-
- float angle = totalTime * 0.3f;
-
- Vector3 cameraPos = new(35.0f * MathF.Sin(angle), 20.0f * MathF.Sin(totalTime * 0.2f), 35.0f * MathF.Cos(angle));
-
- Matrix4x4 view = Matrix4x4.CreateLookAt(cameraPos, Vector3.Zero, Vector3.UnitY);
- Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)App.Width / App.Height, 0.1f, 200.0f);
- Matrix4x4 viewProjection = view * projection;
-
- constantsBuffer.Upload([new Constants()
- {
- ViewProjection = viewProjection,
- FrustumPlane0 = NormalizePlane(new(viewProjection.M11 + viewProjection.M14, viewProjection.M21 + viewProjection.M24, viewProjection.M31 + viewProjection.M34, viewProjection.M41 + viewProjection.M44)),
- FrustumPlane1 = NormalizePlane(new(viewProjection.M14 - viewProjection.M11, viewProjection.M24 - viewProjection.M21, viewProjection.M34 - viewProjection.M31, viewProjection.M44 - viewProjection.M41)),
- FrustumPlane2 = NormalizePlane(new(viewProjection.M12 + viewProjection.M14, viewProjection.M22 + viewProjection.M24, viewProjection.M32 + viewProjection.M34, viewProjection.M42 + viewProjection.M44)),
- FrustumPlane3 = NormalizePlane(new(viewProjection.M14 - viewProjection.M12, viewProjection.M24 - viewProjection.M22, viewProjection.M34 - viewProjection.M32, viewProjection.M44 - viewProjection.M42)),
- FrustumPlane4 = NormalizePlane(new(viewProjection.M13, viewProjection.M23, viewProjection.M33, viewProjection.M43)),
- FrustumPlane5 = NormalizePlane(new(viewProjection.M14 - viewProjection.M13, viewProjection.M24 - viewProjection.M23, viewProjection.M34 - viewProjection.M33, viewProjection.M44 - viewProjection.M43)),
- Time = totalTime,
- LightDirection = -Vector3.Normalize(cameraPos)
- }], 0);
- }
-
- public void Render()
- {
- CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer();
-
- commandBuffer.BeginRenderPass(App.FrameBuffer, new()
- {
- ColorValues = [new(0.05f, 0.05f, 0.08f, 1.0f)],
- Depth = 1.0f,
- Stencil = 0,
- Flags = ClearFlags.All
- }, resourceTable);
-
- commandBuffer.SetPipeline(pipeline);
- commandBuffer.SetResourceTable(resourceTable);
- commandBuffer.DispatchMesh(DispatchGroupCount, 1, 1);
-
- commandBuffer.EndRenderPass();
-
- commandBuffer.Submit(waitForCompletion: true);
- }
-
- public void Resize(uint width, uint height)
- {
- }
-
- public void Dispose()
- {
- pipeline.Dispose();
- resourceTable.Dispose();
- resourceLayout.Dispose();
- constantsBuffer.Dispose();
- indexBuffer.Dispose();
- vertexBuffer.Dispose();
- }
-
- private static Vector4 NormalizePlane(Vector4 plane)
- {
- return plane / new Vector3(plane.X, plane.Y, plane.Z).Length();
- }
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 32)]
-file struct Vertex
-{
- [FieldOffset(0)]
- public Vector3 Position;
-
- [FieldOffset(16)]
- public Vector3 Normal;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 16)]
-file struct Triangle
-{
- [FieldOffset(0)]
- public uint Index0;
-
- [FieldOffset(4)]
- public uint Index1;
-
- [FieldOffset(8)]
- public uint Index2;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 176)]
-file struct Constants
-{
- [FieldOffset(0)]
- public Matrix4x4 ViewProjection;
-
- [FieldOffset(64)]
- public Vector4 FrustumPlane0;
-
- [FieldOffset(80)]
- public Vector4 FrustumPlane1;
-
- [FieldOffset(96)]
- public Vector4 FrustumPlane2;
-
- [FieldOffset(112)]
- public Vector4 FrustumPlane3;
-
- [FieldOffset(128)]
- public Vector4 FrustumPlane4;
-
- [FieldOffset(144)]
- public Vector4 FrustumPlane5;
-
- [FieldOffset(160)]
- public float Time;
-
- [FieldOffset(164)]
- public Vector3 LightDirection;
-}
-```
-
-## Running the Tutorial
-
-Run the application and select **7. Mesh Shading** from the menu:
-
-```bash
-dotnet run
-```
-
-## Result
-
-
-
-## Code Breakdown
-
-### Procedural Sphere Geometry
-
-The sphere is generated as a UV sphere with 12 longitude and 6 latitude segments, producing 62 vertices and 120 triangles:
-
-```csharp
-sphereVertices.Add(new() { Position = new(0, radius, 0), Normal = Vector3.UnitY });
-
-for (int lat = 1; lat < latSegments; lat++)
-{
- float phi = MathF.PI * lat / latSegments;
- float sinPhi = MathF.Sin(phi);
- float cosPhi = MathF.Cos(phi);
-
- for (int lon = 0; lon < lonSegments; lon++)
- {
- float theta = 2.0f * MathF.PI * lon / lonSegments;
- Vector3 normal = new(sinPhi * MathF.Cos(theta), cosPhi, sinPhi * MathF.Sin(theta));
-
- sphereVertices.Add(new() { Position = normal * radius, Normal = normal });
- }
-}
-
-sphereVertices.Add(new() { Position = new(0, -radius, 0), Normal = -Vector3.UnitY });
-```
-
-The vertex and index data are stored in `StructuredBuffer` resources (not vertex/index buffers), since mesh shaders read geometry data directly.
-
-### Mesh Shading Pipeline
-
-The pipeline configuration specifies thread group sizes for both amplification and mesh stages:
-
-```csharp
-pipeline = App.Context.CreateMeshShadingPipeline(new()
-{
- RenderStates = new()
- {
- RasterizerState = RasterizerStates.CullBack,
- DepthStencilState = DepthStencilStates.Default,
- BlendState = BlendStates.Opaque
- },
- Amplification = ampShader,
- Mesh = meshShader,
- Pixel = pixelShader,
- ResourceLayout = resourceLayout,
- PrimitiveTopology = PrimitiveTopology.TriangleList,
- Output = App.FrameBuffer.Output,
- AmplificationThreadGroupSizeX = ASGroupSize,
- AmplificationThreadGroupSizeY = 1,
- AmplificationThreadGroupSizeZ = 1,
- MeshThreadGroupSizeX = MeshGroupSize,
- MeshThreadGroupSizeY = 1,
- MeshThreadGroupSizeZ = 1
-});
-```
-
-### Amplification Shader (Culling)
-
-The amplification shader tests each instance against the camera frustum and only dispatches mesh groups for visible instances:
-
-```csharp
-[shader("amplification")]
-[numthreads(ASGroupSize, 1, 1)]
-void ASMain(uint groupID: SV_GroupID, uint groupThreadID: SV_GroupThreadID)
-{
- uint instanceIndex = groupID * ASGroupSize + groupThreadID;
-
- bool visible = false;
- if (instanceIndex < TotalInstances)
- {
- float3 worldPos = InstancePosition(instanceIndex);
- visible = !IsFrustumCulled(worldPos, BoundingSphereRadius);
- }
-
- if (groupThreadID == 0)
- {
- s_visibleCount = 0;
- }
-
- GroupMemoryBarrierWithGroupSync();
-
- if (visible)
- {
- uint offset;
- InterlockedAdd(s_visibleCount, 1, offset);
- s_payload.InstanceIndices[offset] = instanceIndex;
- }
-
- GroupMemoryBarrierWithGroupSync();
-
- DispatchMesh(s_visibleCount, 1, 1, s_payload);
-}
-```
-
-**Key steps:**
-1. Each thread checks one instance against 6 frustum planes
-2. Visible instances are compacted into a `groupshared` payload using `InterlockedAdd`
-3. `DispatchMesh` spawns only as many mesh groups as there are visible instances
-
-### Frustum Plane Extraction
-
-Frustum planes are extracted from the view-projection matrix on the CPU:
-
-```csharp
-FrustumPlane0 = NormalizePlane(new(viewProjection.M11 + viewProjection.M14, viewProjection.M21 + viewProjection.M24, viewProjection.M31 + viewProjection.M34, viewProjection.M41 + viewProjection.M44)),
-```
-
-| Plane | Extraction |
-|-------|-----------|
-| Left | Row 4 + Row 1 |
-| Right | Row 4 - Row 1 |
-| Bottom | Row 4 + Row 2 |
-| Top | Row 4 - Row 2 |
-| Near | Row 3 |
-| Far | Row 4 - Row 3 |
-
-### Constants Layout
-
-The `Constants` struct packs all per-frame data into 176 bytes:
-
-```csharp
-[StructLayout(LayoutKind.Explicit, Size = 176)]
-file struct Constants
-{
- [FieldOffset(0)]
- public Matrix4x4 ViewProjection;
-
- [FieldOffset(64)]
- public Vector4 FrustumPlane0;
-
- [FieldOffset(80)]
- public Vector4 FrustumPlane1;
-
- [FieldOffset(96)]
- public Vector4 FrustumPlane2;
-
- [FieldOffset(112)]
- public Vector4 FrustumPlane3;
-
- [FieldOffset(128)]
- public Vector4 FrustumPlane4;
-
- [FieldOffset(144)]
- public Vector4 FrustumPlane5;
-
- [FieldOffset(160)]
- public float Time;
-
- [FieldOffset(164)]
- public Vector3 LightDirection;
-}
-```
-
-The constant buffer is shared across all three shader stages (`Amplification | Mesh | Pixel`), so the amplification shader can read frustum planes while the pixel shader reads the light direction.
-
-## Source Code
-
-> [!TIP]
-> View the complete source code on GitHub: [MeshShadingRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Renderers/MeshShadingRenderer.cs)
diff --git a/documents/tutorials/advanced/ray-tracing.md b/documents/tutorials/advanced/ray-tracing.md
deleted file mode 100644
index 4b24c3a9..00000000
--- a/documents/tutorials/advanced/ray-tracing.md
+++ /dev/null
@@ -1,949 +0,0 @@
-# Ray Tracing
-
-In this tutorial, you'll build a real-time ray tracer using hardware-accelerated ray tracing. The scene features three colored spheres on a checkerboard floor with an animated orbiting camera, soft shadows, reflections, and ACES tone mapping — all driven by a compute shader using `RayQuery`.
-
-> [!NOTE]
-> This tutorial requires a GPU with ray tracing support (e.g., NVIDIA RTX, AMD RDNA 2+, or Apple M1+).
-
-## Overview
-
-This tutorial covers:
-
-- Building **Bottom-Level** and **Top-Level Acceleration Structures** (BLAS/TLAS)
-- Using **triangle geometry** for the floor and **procedural AABBs** for spheres
-- Tracing rays with `RayQuery` in a compute shader
-- Implementing **soft shadows**, **reflections**, and **Fresnel** effects
-- Applying **ACES tone mapping** for cinematic color grading
-- Dynamically **resizing** the output texture on window resize
-
-## Key Concepts
-
-### Acceleration Structure Hierarchy
-
-Ray tracing uses a two-level acceleration structure:
-
-| Level | Purpose | Content |
-|-------|---------|---------|
-| **BLAS** (Bottom-Level) | Geometry containers | Triangle meshes or procedural AABBs |
-| **TLAS** (Top-Level) | Scene graph | References to BLAS instances with transforms |
-
-This tutorial builds two BLAS:
-- **Floor BLAS**: A triangle mesh (2 triangles forming a 100×100 quad)
-- **Sphere BLAS**: 3 procedural AABBs (bounding boxes for sphere intersection)
-
-Both are combined into one TLAS for the scene.
-
-### RayQuery
-
-Instead of using a dedicated ray tracing pipeline, Zenith.NET uses `RayQuery` in compute shaders. This inline approach traces rays within any shader stage:
-
-```
-RayQuery query;
-query.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, ray);
-
-while (query.Proceed())
-{
- // Handle procedural intersections
-}
-
-if (query.CommittedStatus() == COMMITTED_TRIANGLE_HIT)
-{
- // Handle triangle hit
-}
-```
-
-## The Renderer Class
-
-Create the file `Renderers/RayTracingRenderer.cs`:
-
-```csharp
-namespace ZenithTutorials.Renderers;
-
-internal unsafe class RayTracingRenderer : IRenderer
-{
- private const uint ThreadGroupSize = 16;
-
- private const string ShaderSource = """
- static const float RayEpsilon = 0.001;
- static const float TwoPi = 6.2831853;
- static const uint SphereCount = 3;
- static const uint ShadowSamples = 6;
- static const uint ReflectionSamples = 4;
- static const float ShadowMin = 0.3;
- static const float SunRadius = 0.04;
- static const float SphereRoughness = 0.05;
- static const float SphereF0 = 0.15;
- static const float FloorFadeStart = 8.0;
- static const float FloorFadeRange = 20.0;
-
- static const float3 FloorNormal = float3(0.0, 1.0, 0.0);
- static const float3 LightDir = float3(0.6667, 0.6667, -0.3333);
- static const float3 LightColor = float3(1.0, 0.98, 0.95);
- static const float3 AmbientColor = float3(0.15, 0.15, 0.2);
-
- struct Constants
- {
- private float4 PositionAndPadding;
-
- property float3 Position
- {
- get {
- return PositionAndPadding.xyz;
- }
- }
- };
-
- struct Sphere
- {
- private float4 CenterAndRadius;
-
- private float4 ColorAndPadding;
-
- property float3 Center
- {
- get {
- return CenterAndRadius.xyz;
- }
- }
-
- property float Radius
- {
- get {
- return CenterAndRadius.w;
- }
- }
-
- property float3 Color
- {
- get {
- return ColorAndPadding.xyz;
- }
- }
- };
-
- RaytracingAccelerationStructure scene;
- ConstantBuffer constants;
- StructuredBuffer spheres;
- RWTexture2D outputTexture;
-
- float3 SampleSky(float3 direction)
- {
- float t = 0.5 * (direction.y + 1.0);
- float3 horizon = float3(0.7, 0.85, 1.0);
- float3 zenith = float3(0.3, 0.5, 1.0);
- float3 sky = lerp(horizon, zenith, saturate(t));
-
- float sunDot = dot(direction, LightDir);
- sky += LightColor * smoothstep(0.995, 0.999, sunDot) * 3.0;
-
- return sky;
- }
-
- float3 ACESFilm(float3 x)
- {
- x *= 1.6;
- float3 a = x * (x * 2.51 + 0.03);
- float3 b = x * (x * 2.43 + 0.59) + 0.14;
- float3 result = saturate(a / b);
-
- float luma = dot(result, float3(0.2126, 0.7152, 0.0722));
- result = saturate(lerp(float3(luma, luma, luma), result, 1.5));
-
- return result;
- }
-
- float SchlickFresnel(float cosTheta, float f0)
- {
- return f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0);
- }
-
- float3 ShadeCheckerboard(float3 hitPoint, float3 normal, float3 rayDirection, bool softShadow, out float shadow)
- {
- float2 fw = max(abs(fwidth_approx(hitPoint.xz)), 0.001);
- float2 fractPos = fract(hitPoint.xz) - 0.5;
- float2 filtered = clamp(fractPos / fw, -0.5, 0.5);
- float checker = 0.5 - 0.5 * filtered.x * filtered.y;
- float3 baseColor = lerp(float3(0.787, 0.787, 0.787), float3(0.1, 0.1, 0.1), checker);
-
- float NdotL = max(dot(normal, LightDir), 0.0);
- float3 shadowOrigin = hitPoint + normal * RayEpsilon;
- shadow = softShadow ? lerp(ShadowMin, 1.0, TraceSoftShadow(shadowOrigin, LightDir, hitPoint.xz * 100.0)) :
- (TraceShadowRay(shadowOrigin, LightDir) ? ShadowMin : 1.0);
-
- float3 litColor = baseColor * AmbientColor + baseColor * LightColor * NdotL * shadow;
-
- float ao = 1.0;
- for (uint i = 0; i < SphereCount; i++)
- {
- float3 toSphere = spheres[i].Center - hitPoint;
- float horizDist = length(toSphere.xz);
- float r = spheres[i].Radius;
- float occl = saturate(1.0 - horizDist / (r * 2.0));
- float hFactor = saturate(1.0 - toSphere.y / (r * 3.0));
- ao -= occl * hFactor * 0.4;
- }
-
- litColor *= max(ao, 0.3);
-
- float dist = length(hitPoint.xz);
- float fade = saturate((dist - FloorFadeStart) / FloorFadeRange);
- return lerp(litColor, SampleSky(rayDirection), fade);
- }
-
- float3 ShadeSphere(float3 hitPoint, float3 normal, float3 sphereColor, float3 viewDir, bool softShadow)
- {
- float NdotL = max(dot(normal, LightDir), 0.0);
-
- float3 halfDir = normalize(LightDir + viewDir);
- float spec = pow(max(dot(normal, halfDir), 0.0), 64.0);
-
- float3 shadowOrigin = hitPoint + normal * RayEpsilon;
- float shadow = softShadow ? lerp(ShadowMin, 1.0, TraceSoftShadow(shadowOrigin, LightDir, hitPoint.xz * 100.0)) :
- (TraceShadowRay(shadowOrigin, LightDir) ? ShadowMin : 1.0);
-
- float3 diffuse = sphereColor * LightColor * NdotL * shadow;
- float3 specular = LightColor * spec * shadow;
- float3 ambient = sphereColor * AmbientColor;
-
- return ambient + diffuse + specular;
- }
-
- float3 TraceReflection(float3 origin, float3 direction)
- {
- RayDesc reflectRay;
- reflectRay.Origin = origin;
- reflectRay.Direction = direction;
- reflectRay.TMin = RayEpsilon;
- reflectRay.TMax = 1000.0;
-
- float3 sphereNormal = float3(0.0);
- float3 sphereColor = float3(0.0);
-
- RayQuery query;
- query.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, reflectRay);
-
- while (query.Proceed())
- {
- if (query.CandidateType() == CANDIDATE_PROCEDURAL_PRIMITIVE)
- {
- uint sphereIndex = query.CandidatePrimitiveIndex();
- Sphere sphere = spheres[sphereIndex];
-
- float3 ro = query.CandidateObjectRayOrigin();
- float3 rd = query.CandidateObjectRayDirection();
-
- float t = IntersectSphere(ro, rd, sphere);
-
- if (t >= query.RayTMin() && t <= query.CommittedRayT())
- {
- float3 hitPoint = ro + rd * t;
- sphereNormal = normalize(hitPoint - sphere.Center);
- sphereColor = sphere.Color;
- query.CommitProceduralPrimitiveHit(t);
- }
- }
- }
-
- if (query.CommittedStatus() == COMMITTED_TRIANGLE_HIT)
- {
- float3 hitPoint = reflectRay.Origin + reflectRay.Direction * query.CommittedRayT();
- float unused;
- return ShadeCheckerboard(hitPoint, FloorNormal, reflectRay.Direction, false, unused);
- }
- else if (query.CommittedStatus() == COMMITTED_PROCEDURAL_PRIMITIVE_HIT)
- {
- float3 hitPoint = reflectRay.Origin + reflectRay.Direction * query.CommittedRayT();
- float3 viewDir = normalize(origin - hitPoint);
- return ShadeSphere(hitPoint, sphereNormal, sphereColor, viewDir, false);
- }
- else
- {
- return SampleSky(direction);
- }
- }
-
- float IntersectSphere(float3 origin, float3 direction, Sphere sphere)
- {
- float3 oc = origin - sphere.Center;
-
- float b = dot(oc, direction);
- float c = dot(oc, oc) - sphere.Radius * sphere.Radius;
- float discriminant = b * b - c;
-
- if (discriminant > 0.0)
- {
- float sqrtD = sqrt(discriminant);
- float t1 = -b - sqrtD;
-
- if (t1 > 0.0)
- {
- return t1;
- }
-
- float t2 = -b + sqrtD;
-
- if (t2 > 0.0)
- {
- return t2;
- }
- }
-
- return -1.0;
- }
-
- float2 fwidth_approx(float2 p)
- {
- float2 dx = float2(0.02, 0.0);
- float2 dy = float2(0.0, 0.02);
- return abs(fract(p + dx) - fract(p)) + abs(fract(p + dy) - fract(p));
- }
-
- float Hash(float2 p)
- {
- float3 p3 = fract(float3(p.xyx) * 0.1031);
- p3 += dot(p3, p3.yzx + 33.33);
- return fract((p3.x + p3.y) * p3.z);
- }
-
- bool TraceShadowRay(float3 origin, float3 direction)
- {
- RayDesc shadowRay;
- shadowRay.Origin = origin;
- shadowRay.Direction = direction;
- shadowRay.TMin = RayEpsilon;
- shadowRay.TMax = 1000.0;
-
- RayQuery shadowQuery;
- shadowQuery.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, shadowRay);
-
- while (shadowQuery.Proceed())
- {
- if (shadowQuery.CandidateType() == CANDIDATE_PROCEDURAL_PRIMITIVE)
- {
- uint sphereIndex = shadowQuery.CandidatePrimitiveIndex();
- Sphere sphere = spheres[sphereIndex];
-
- float3 ro = shadowQuery.CandidateObjectRayOrigin();
- float3 rd = shadowQuery.CandidateObjectRayDirection();
-
- float t = IntersectSphere(ro, rd, sphere);
-
- if (t >= shadowQuery.RayTMin() && t <= shadowQuery.CommittedRayT())
- {
- shadowQuery.CommitProceduralPrimitiveHit(t);
- }
- }
- }
-
- return shadowQuery.CommittedStatus() != COMMITTED_NOTHING;
- }
-
- float TraceSoftShadow(float3 origin, float3 direction, float2 pixelSeed)
- {
- float3 tangent = normalize(cross(direction, float3(0.0, 1.0, 0.0)));
- float3 bitangent = cross(direction, tangent);
-
- float lit = 0.0;
- for (uint i = 0; i < ShadowSamples; i++)
- {
- float h = Hash(pixelSeed + float2(float(i) * 7.13, float(i) * 3.71));
- float angle = (float(i) + h) * (TwoPi / float(ShadowSamples));
- float radius = sqrt(Hash(pixelSeed + float2(float(i) * 11.07, 0.0))) * SunRadius;
- float3 jitteredDir = normalize(direction + tangent * cos(angle) * radius + bitangent * sin(angle) * radius);
-
- if (!TraceShadowRay(origin, jitteredDir))
- {
- lit += 1.0;
- }
- }
-
- return lit / float(ShadowSamples);
- }
-
- float3 TraceRoughReflection(float3 origin, float3 reflectDir, float3 normal, float roughness, float2 pixelSeed)
- {
- float3 tangent = normalize(cross(reflectDir, normal));
- float3 bitangent = cross(reflectDir, tangent);
-
- float3 accum = float3(0.0);
- for (uint i = 0; i < ReflectionSamples; i++)
- {
- float h1 = Hash(pixelSeed + float2(float(i) * 5.17, float(i) * 9.23));
- float h2 = Hash(pixelSeed + float2(float(i) * 13.37, float(i) * 2.91));
- float angle = h1 * TwoPi;
- float radius = sqrt(h2) * roughness;
- float3 jitteredDir = normalize(reflectDir + tangent * cos(angle) * radius + bitangent * sin(angle) * radius);
- accum += TraceReflection(origin, jitteredDir);
- }
-
- return accum / float(ReflectionSamples);
- }
-
- float3 ShadeFloor(float3 hitPoint, float3 rayDir, float3 cameraPos)
- {
- float shadow;
- float3 directColor = ShadeCheckerboard(hitPoint, FloorNormal, rayDir, true, shadow);
-
- float3 viewDir = normalize(cameraPos - hitPoint);
- float3 halfDir = normalize(LightDir + viewDir);
- float floorSpec = pow(max(dot(FloorNormal, halfDir), 0.0), 128.0);
- float specDist = length(hitPoint.xz);
- float specFade = 1.0 - saturate((specDist - FloorFadeStart) / FloorFadeRange);
- directColor += LightColor * floorSpec * 0.4 * specFade * shadow;
-
- float3 reflectDir = reflect(rayDir, FloorNormal);
- float3 reflectColor = TraceReflection(hitPoint + FloorNormal * RayEpsilon, reflectDir);
- float fresnel = SchlickFresnel(max(dot(FloorNormal, viewDir), 0.0), 0.02);
-
- return lerp(directColor, reflectColor, fresnel);
- }
-
- float3 ShadePrimarySphere(float3 hitPoint, float3 rayDir, float3 cameraPos, float3 normal, float3 sphereColor)
- {
- float3 viewDir = normalize(cameraPos - hitPoint);
-
- float3 directColor = ShadeSphere(hitPoint, normal, sphereColor, viewDir, true);
-
- float3 reflectDir = reflect(rayDir, normal);
- float3 reflectColor = TraceRoughReflection(hitPoint + normal * RayEpsilon, reflectDir, normal, SphereRoughness, hitPoint.xz * 100.0);
- float fresnel = SchlickFresnel(max(dot(normal, viewDir), 0.0), SphereF0);
-
- return lerp(directColor, reflectColor, fresnel);
- }
-
- [numthreads(16, 16, 1)]
- void CSMain(uint3 dispatchThreadID: SV_DispatchThreadID)
- {
- uint2 pixelCoord = dispatchThreadID.xy;
-
- uint width, height;
- outputTexture.GetDimensions(width, height);
-
- if (pixelCoord.x >= width || pixelCoord.y >= height)
- {
- return;
- }
-
- float2 uv = (float2(pixelCoord) + 0.5) / float2(width, height);
- float2 ndc = uv * 2.0 - 1.0;
- ndc.y = -ndc.y;
-
- float aspectRatio = float(width) / float(height);
- float fov = tan(radians(45.0) * 0.5);
-
- float3 cameraPos = constants.Position;
- float3 cameraTarget = float3(0.0, 0.5, 0.0);
- float3 cameraUp = float3(0.0, 1.0, 0.0);
-
- float3 forward = normalize(cameraTarget - cameraPos);
- float3 right = normalize(cross(forward, cameraUp));
- float3 up = cross(right, forward);
-
- float3 rayDir = normalize(forward + ndc.x * aspectRatio * fov * right + ndc.y * fov * up);
-
- RayDesc ray;
- ray.Origin = cameraPos;
- ray.Direction = rayDir;
- ray.TMin = RayEpsilon;
- ray.TMax = 1000.0;
-
- float3 sphereHitNormal = float3(0.0);
- float3 sphereHitColor = float3(0.0);
-
- RayQuery query;
- query.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, ray);
-
- while (query.Proceed())
- {
- if (query.CandidateType() == CANDIDATE_PROCEDURAL_PRIMITIVE)
- {
- uint sphereIndex = query.CandidatePrimitiveIndex();
- Sphere sphere = spheres[sphereIndex];
-
- float3 ro = query.CandidateObjectRayOrigin();
- float3 rd = query.CandidateObjectRayDirection();
-
- float t = IntersectSphere(ro, rd, sphere);
-
- if (t >= query.RayTMin() && t <= query.CommittedRayT())
- {
- float3 hitPoint = ro + rd * t;
-
- sphereHitNormal = normalize(hitPoint - sphere.Center);
- sphereHitColor = sphere.Color;
-
- query.CommitProceduralPrimitiveHit(t);
- }
- }
- }
-
- float3 color;
-
- if (query.CommittedStatus() == COMMITTED_TRIANGLE_HIT)
- {
- float3 hitPoint = ray.Origin + ray.Direction * query.CommittedRayT();
- color = ShadeFloor(hitPoint, rayDir, cameraPos);
- }
- else if (query.CommittedStatus() == COMMITTED_PROCEDURAL_PRIMITIVE_HIT)
- {
- float3 hitPoint = ray.Origin + ray.Direction * query.CommittedRayT();
- color = ShadePrimarySphere(hitPoint, rayDir, cameraPos, sphereHitNormal, sphereHitColor);
- }
- else
- {
- color = SampleSky(rayDir);
- }
-
- color = ACESFilm(color);
-
- outputTexture[pixelCoord] = float4(color, 1.0);
- }
- """;
-
- private readonly Buffer floorVertexBuffer;
- private readonly Buffer floorIndexBuffer;
- private readonly Buffer aabbBuffer;
- private readonly BottomLevelAccelerationStructure floorBlas;
- private readonly BottomLevelAccelerationStructure sphereBlas;
- private readonly TopLevelAccelerationStructure tlas;
- private readonly Buffer constantsBuffer;
- private readonly Buffer sphereBuffer;
- private readonly ResourceLayout resourceLayout;
- private readonly ComputePipeline pipeline;
-
- private Texture? outputTexture;
- private ResourceTable? resourceTable;
- private float totalTime;
-
- public RayTracingRenderer()
- {
- if (!App.Context.Capabilities.RayTracingSupported)
- {
- throw new NotSupportedException("Ray tracing is not supported on this device.");
- }
-
- Vector3[] floorVertices =
- [
- new(-50.0f, 0.0f, -50.0f),
- new( 50.0f, 0.0f, -50.0f),
- new( 50.0f, 0.0f, 50.0f),
- new(-50.0f, 0.0f, 50.0f)
- ];
- uint[] floorIndices = [0, 1, 2, 0, 2, 3];
-
- floorVertexBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(Vector3) * floorVertices.Length),
- StrideInBytes = (uint)sizeof(Vector3),
- Flags = BufferUsageFlags.Vertex | BufferUsageFlags.AccelerationStructure
- });
- floorVertexBuffer.Upload(floorVertices, 0);
-
- floorIndexBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(uint) * floorIndices.Length),
- StrideInBytes = sizeof(uint),
- Flags = BufferUsageFlags.Index | BufferUsageFlags.AccelerationStructure
- });
- floorIndexBuffer.Upload(floorIndices, 0);
-
- Sphere[] spheres =
- [
- new() { Center = new(-2.0f, 1.0f, 1.0f), Radius = 1.0f, Color = new(0.8f, 0.2f, 0.2f) },
- new() { Center = new( 2.0f, 1.2f, -1.0f), Radius = 1.2f, Color = new(0.2f, 0.4f, 0.8f) },
- new() { Center = new( 0.0f, 0.6f, -3.0f), Radius = 0.6f, Color = new(0.9f, 0.7f, 0.2f) }
- ];
-
- Vector3[] aabbs = new Vector3[spheres.Length * 2];
- for (int i = 0; i < spheres.Length; i++)
- {
- aabbs[i * 2] = spheres[i].Center - new Vector3(spheres[i].Radius);
- aabbs[(i * 2) + 1] = spheres[i].Center + new Vector3(spheres[i].Radius);
- }
-
- aabbBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(Vector3) * aabbs.Length),
- StrideInBytes = (uint)(sizeof(Vector3) * 2),
- Flags = BufferUsageFlags.ShaderResource | BufferUsageFlags.AccelerationStructure
- });
- aabbBuffer.Upload(aabbs, 0);
-
- CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer();
-
- floorBlas = commandBuffer.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc
- {
- Geometries =
- [
- new()
- {
- Type = RayTracingGeometryType.Triangles,
- Triangles = new()
- {
- VertexBuffer = floorVertexBuffer,
- VertexFormat = PixelFormat.R32G32B32Float,
- VertexCount = (uint)floorVertices.Length,
- VertexStrideInBytes = (uint)sizeof(Vector3),
- IndexBuffer = floorIndexBuffer,
- IndexFormat = IndexFormat.UInt32,
- IndexCount = (uint)floorIndices.Length,
- Transform = Matrix4x4.Identity
- },
- Flags = RayTracingGeometryFlags.Opaque
- }
- ],
- Flags = AccelerationStructureBuildFlags.PreferFastTrace
- });
-
- sphereBlas = commandBuffer.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc
- {
- Geometries =
- [
- new()
- {
- Type = RayTracingGeometryType.AABBs,
- AABBs = new()
- {
- Buffer = aabbBuffer,
- Count = (uint)spheres.Length,
- StrideInBytes = (uint)(sizeof(Vector3) * 2)
- },
- Flags = RayTracingGeometryFlags.Opaque
- }
- ],
- Flags = AccelerationStructureBuildFlags.PreferFastTrace
- });
-
- tlas = commandBuffer.BuildAccelerationStructure(new TopLevelAccelerationStructureDesc
- {
- Instances =
- [
- new()
- {
- AccelerationStructure = floorBlas,
- ID = 0,
- Mask = 0xFF,
- Transform = Matrix4x4.Identity,
- Flags = RayTracingInstanceFlags.None
- },
- new()
- {
- AccelerationStructure = sphereBlas,
- ID = 1,
- Mask = 0xFF,
- Transform = Matrix4x4.Identity,
- Flags = RayTracingInstanceFlags.None
- }
- ],
- Flags = AccelerationStructureBuildFlags.PreferFastTrace
- });
-
- commandBuffer.Submit(waitForCompletion: true);
-
- constantsBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)sizeof(Constants),
- StrideInBytes = (uint)sizeof(Constants),
- Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite
- });
-
- sphereBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(Sphere) * spheres.Length),
- StrideInBytes = (uint)sizeof(Sphere),
- Flags = BufferUsageFlags.ShaderResource
- });
- sphereBuffer.Upload(spheres, 0);
-
- resourceLayout = App.Context.CreateResourceLayout(new()
- {
- Bindings = BindingHelper.Bindings
- (
- new() { Type = ResourceType.AccelerationStructure, Count = 1, StageFlags = ShaderStageFlags.Compute },
- new() { Type = ResourceType.ConstantBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute },
- new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute },
- new() { Type = ResourceType.TextureReadWrite, Count = 1, StageFlags = ShaderStageFlags.Compute }
- )
- });
-
- using Shader computeShader = App.Context.LoadShaderFromSource(ShaderSource, "CSMain", ShaderStageFlags.Compute);
-
- pipeline = App.Context.CreateComputePipeline(new()
- {
- Compute = computeShader,
- ResourceLayout = resourceLayout,
- ThreadGroupSizeX = ThreadGroupSize,
- ThreadGroupSizeY = ThreadGroupSize,
- ThreadGroupSizeZ = 1
- });
- }
-
- public void Update(double deltaTime)
- {
- totalTime += (float)deltaTime;
-
- float angle = totalTime * 0.3f;
-
- constantsBuffer.Upload([new Constants()
- {
- Position = new(12.0f * MathF.Sin(angle), 4.0f + MathF.Sin(totalTime * 0.2f), -12.0f * MathF.Cos(angle))
- }], 0);
- }
-
- public void Render()
- {
- outputTexture ??= App.Context.CreateTexture(new()
- {
- Type = TextureType.Texture2D,
- Format = PixelFormat.B8G8R8A8UNorm,
- Width = App.Width,
- Height = App.Height,
- Depth = 1,
- MipLevels = 1,
- ArrayLayers = 1,
- SampleCount = SampleCount.Count1,
- Flags = TextureUsageFlags.ShaderResource | TextureUsageFlags.UnorderedAccess
- });
-
- resourceTable ??= App.Context.CreateResourceTable(new()
- {
- Layout = resourceLayout,
- Resources = [tlas, constantsBuffer, sphereBuffer, outputTexture]
- });
-
- CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer();
-
- commandBuffer.SetPipeline(pipeline);
- commandBuffer.SetResourceTable(resourceTable);
-
- uint dispatchX = (App.Width + ThreadGroupSize - 1) / ThreadGroupSize;
- uint dispatchY = (App.Height + ThreadGroupSize - 1) / ThreadGroupSize;
-
- commandBuffer.Dispatch(dispatchX, dispatchY, 1);
-
- commandBuffer.CopyTexture(outputTexture,
- default,
- default,
- App.FrameBuffer.Desc.ColorAttachments[0].Target,
- default,
- default,
- new() { Width = App.Width, Height = App.Height, Depth = 1 });
-
- commandBuffer.Submit(waitForCompletion: true);
- }
-
- public void Resize(uint width, uint height)
- {
- resourceTable?.Dispose();
- resourceTable = null;
-
- outputTexture?.Dispose();
- outputTexture = null;
- }
-
- public void Dispose()
- {
- resourceTable?.Dispose();
- outputTexture?.Dispose();
-
- pipeline.Dispose();
- resourceLayout.Dispose();
- sphereBuffer.Dispose();
- constantsBuffer.Dispose();
- tlas.Dispose();
- sphereBlas.Dispose();
- floorBlas.Dispose();
- aabbBuffer.Dispose();
- floorIndexBuffer.Dispose();
- floorVertexBuffer.Dispose();
- }
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 16)]
-file struct Constants
-{
- [FieldOffset(0)]
- public Vector3 Position;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 32)]
-file struct Sphere
-{
- [FieldOffset(0)]
- public Vector3 Center;
-
- [FieldOffset(12)]
- public float Radius;
-
- [FieldOffset(16)]
- public Vector3 Color;
-}
-```
-
-## Running the Tutorial
-
-Run the application and select **6. Ray Tracing** from the menu:
-
-```bash
-dotnet run
-```
-
-## Result
-
-
-
-## Code Breakdown
-
-### Acceleration Structures
-
-The floor uses triangle geometry, while spheres use procedural AABBs:
-
-```csharp
-floorBlas = commandBuffer.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc
-{
- Geometries =
- [
- new()
- {
- Type = RayTracingGeometryType.Triangles,
- Triangles = new()
- {
- VertexBuffer = floorVertexBuffer,
- VertexFormat = PixelFormat.R32G32B32Float,
- VertexCount = (uint)floorVertices.Length,
- VertexStrideInBytes = (uint)sizeof(Vector3),
- IndexBuffer = floorIndexBuffer,
- IndexFormat = IndexFormat.UInt32,
- IndexCount = (uint)floorIndices.Length,
- Transform = Matrix4x4.Identity
- },
- Flags = RayTracingGeometryFlags.Opaque
- }
- ],
- Flags = AccelerationStructureBuildFlags.PreferFastTrace
-});
-```
-
-For procedural geometry, AABBs (axis-aligned bounding boxes) are provided as min/max pairs. The actual intersection is computed in the shader:
-
-```csharp
-Vector3[] aabbs = new Vector3[spheres.Length * 2];
-
-for (int i = 0; i < spheres.Length; i++)
-{
- aabbs[i * 2] = spheres[i].Center - new Vector3(spheres[i].Radius);
- aabbs[(i * 2) + 1] = spheres[i].Center + new Vector3(spheres[i].Radius);
-}
-```
-
-### TLAS Assembly
-
-The top-level structure combines both BLAS instances:
-
-```csharp
-tlas = commandBuffer.BuildAccelerationStructure(new TopLevelAccelerationStructureDesc
-{
- Instances =
- [
- new()
- {
- AccelerationStructure = floorBlas,
- ID = 0,
- Mask = 0xFF,
- Transform = Matrix4x4.Identity,
- Flags = RayTracingInstanceFlags.None
- },
- new()
- {
- AccelerationStructure = sphereBlas,
- ID = 1,
- Mask = 0xFF,
- Transform = Matrix4x4.Identity,
- Flags = RayTracingInstanceFlags.None
- }
- ],
- Flags = AccelerationStructureBuildFlags.PreferFastTrace
-});
-```
-
-### Resource Layout
-
-The compute shader accesses four resources — acceleration structure, constants, sphere data, and the output texture:
-
-```csharp
-resourceLayout = App.Context.CreateResourceLayout(new()
-{
- Bindings = BindingHelper.Bindings
- (
- new() { Type = ResourceType.AccelerationStructure, Count = 1, StageFlags = ShaderStageFlags.Compute },
- new() { Type = ResourceType.ConstantBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute },
- new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute },
- new() { Type = ResourceType.TextureReadWrite, Count = 1, StageFlags = ShaderStageFlags.Compute }
- )
-});
-```
-
-### Animated Camera
-
-The camera orbits the scene, creating a cinematic flythrough:
-
-```csharp
-public void Update(double deltaTime)
-{
- totalTime += (float)deltaTime;
-
- float angle = totalTime * 0.3f;
-
- constantsBuffer.Upload([new Constants()
- {
- Position = new(12.0f * MathF.Sin(angle), 4.0f + MathF.Sin(totalTime * 0.2f), -12.0f * MathF.Cos(angle))
- }], 0);
-}
-```
-
-The camera position traces a circle of radius 12 with vertical bobbing.
-
-### Dynamic Resize
-
-The output texture and resource table are recreated when the window resizes:
-
-```csharp
-public void Resize(uint width, uint height)
-{
- resourceTable?.Dispose();
- resourceTable = null;
-
- outputTexture?.Dispose();
- outputTexture = null;
-}
-```
-
-Using nullable fields with `??=` in `Render()` provides lazy reallocation:
-
-```csharp
-outputTexture ??= App.Context.CreateTexture(new() { ... });
-resourceTable ??= App.Context.CreateResourceTable(new() { ... });
-```
-
-### Shader Rendering Techniques
-
-The shader implements several rendering techniques:
-
-| Technique | Function | Description |
-|-----------|----------|-------------|
-| Sky gradient | `SampleSky` | Horizon-to-zenith color blend with sun disk |
-| Soft shadows | `TraceSoftShadow` | Jittered shadow rays simulating area light |
-| Reflections | `TraceReflection` / `TraceRoughReflection` | Single and multi-sample reflection rays |
-| Fresnel | `SchlickFresnel` | Angle-dependent reflectivity |
-| Tone mapping | `ACESFilm` | ACES filmic curve with saturation boost |
-| Checkerboard | `ShadeCheckerboard` | Anti-aliased procedural floor pattern |
-| Sphere AO | Per-sphere loop | Contact-based ambient occlusion on floor |
-
-## Next Steps
-
-- [Mesh Shading](mesh-shading.md) - Use the modern mesh shader pipeline with GPU-driven culling
-
-## Source Code
-
-> [!TIP]
-> View the complete source code on GitHub: [RayTracingRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Renderers/RayTracingRenderer.cs)
diff --git a/documents/tutorials/getting-started/application-host.md b/documents/tutorials/getting-started/application-host.md
new file mode 100644
index 00000000..a0e3a224
--- /dev/null
+++ b/documents/tutorials/getting-started/application-host.md
@@ -0,0 +1,291 @@
+# Application Host
+
+Every workload guide runs inside the same application host. The host owns the window and graphics context, creates the swap chain, records the outer frame transitions, and presents textures produced by compute workloads.
+
+This page explains that contract before the guides begin creating workload resources. Platform integration is host infrastructure and remains in the complete reference source rather than the walkthrough.
+
+## Responsibilities
+
+The shared host is responsible for:
+
+- selecting a supported graphics API for the current operating system;
+- creating a window surface and swap chain;
+- providing the active `GraphicsContext` and framebuffer dimensions;
+- obtaining one graphics command buffer per frame;
+- transitioning the swap-chain drawable into and out of attachment layout;
+- forwarding update, render, and resize events to the active workload;
+- submitting work, waiting for completion, and presenting the drawable;
+- loading shared assets and presenting offscreen textures.
+
+The workload renderer remains responsible for its own buffers, textures, pipelines, commands, transitions, and disposal.
+
+## Renderer contract
+
+Create `IRenderer.cs`:
+
+```csharp
+namespace ZenithTutorials;
+
+internal interface IRenderer : IDisposable
+{
+ void Update(double deltaTime);
+
+ void Render(CommandBuffer commandBuffer, Texture drawable);
+
+ void Resize(uint width, uint height);
+}
+```
+
+The host calls `Update` before rendering, passes the current swap-chain texture to `Render`, and reports framebuffer size changes through `Resize`. Extending `IDisposable` makes each guide state its GPU ownership explicitly.
+
+## Create the graphics context
+
+`App` exposes one shared graphics context. Its static constructor selects DirectX 12 on Windows, Metal 4 on macOS, or Vulkan 1.4 on Linux.
+
+The complete `App.cs` contains the required imports and declares `App` as an `unsafe` static class because its upload helpers use pointers.
+
+```csharp
+static App()
+{
+ if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsLinux())
+ {
+ throw new PlatformNotSupportedException("The tutorials support Windows, macOS, and Linux.");
+ }
+
+ if (OperatingSystem.IsWindows())
+ {
+ Context = GraphicsContext.CreateDirectX12(useValidationLayer: true);
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ Context = GraphicsContext.CreateMetal(useValidationLayer: true);
+ }
+ else
+ {
+ Context = GraphicsContext.CreateVulkan(useValidationLayer: true);
+ }
+
+ Context.ValidationMessage += static (_, args) => Console.WriteLine($"[{args.Severity}] {args.Message}");
+}
+```
+
+Validation remains enabled throughout the guides. Validation messages are written to the terminal and should remain free of errors when a workload is running correctly.
+
+The host also publishes the shared color format and current framebuffer dimensions:
+
+```csharp
+public static GraphicsContext Context { get; }
+
+public static PixelFormat ColorFormat => PixelFormat.B8G8R8A8UNorm;
+
+public static uint Width => (uint)(window?.FramebufferSize.X ?? 0);
+
+public static uint Height => (uint)(window?.FramebufferSize.Y ?? 0);
+```
+
+Framebuffer dimensions, rather than logical window dimensions, are used for textures, viewports, and projection aspect ratios. This matters on high-density displays.
+
+## Create the window and swap chain
+
+Silk.NET creates a window without an OpenGL context because Zenith.NET owns the native graphics API:
+
+```csharp
+window = Window.Create(WindowOptions.Default with
+{
+ API = GraphicsAPI.None,
+ Title = "Zenith.NET Tutorials"
+});
+
+window.Initialize();
+window.Center();
+```
+
+The host creates a Zenith.NET `Surface` from the window. That platform integration is contained in `App.cs` and `CocoaHelper.cs`; workload renderers depend only on the resulting swap chain. The surface and shared color format define that swap chain:
+
+```csharp
+swapChain = Context.CreateSwapChain(new()
+{
+ Surface = surface,
+ Format = ColorFormat
+});
+```
+
+## Follow one frame
+
+The outer frame has the same shape for every workload:
+
+```mermaid
+flowchart LR
+ A[Update workload] --> B[Acquire command buffer]
+ B --> C[Drawable to ColorAttachment]
+ C --> D[Record workload]
+ D --> E[Drawable to Present]
+ E --> F[Submit and wait]
+ F --> G[Present swap chain]
+```
+
+The render callback records those operations in order:
+
+```csharp
+window.Render += _ =>
+{
+ if (Width is 0 || Height is 0)
+ {
+ return;
+ }
+
+ CommandBuffer commandBuffer = Context.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+
+ renderer.Render(commandBuffer, swapChain.Drawable);
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.ColorAttachment, TextureLayout.Present);
+
+ commandBuffer.Submit().Wait();
+
+ swapChain.Present();
+};
+```
+
+The workload starts with a drawable already in `ColorAttachment` layout and must leave it in that layout. The host performs the final transition to `Present`.
+
+`Submit().Wait()` deliberately permits only one frame of work at a time. That makes resource lifetime and CPU uploads easier to follow, but it is not a production frame scheduler. Applications that keep multiple frames in flight need per-frame resources and explicit reuse synchronization.
+
+## Update and resize
+
+The update callback forwards elapsed seconds:
+
+```csharp
+window.Update += delta =>
+{
+ if (Width is 0 || Height is 0)
+ {
+ return;
+ }
+
+ renderer.Update(delta);
+};
+```
+
+The resize callback lets the workload release size-dependent resources before resizing the swap chain:
+
+```csharp
+window.Resize += _ =>
+{
+ if (Width is 0 || Height is 0)
+ {
+ return;
+ }
+
+ renderer.Resize(Width, Height);
+ swapChain.Resize(Width, Height);
+};
+```
+
+Zero-size checks cover minimized windows. A workload normally sets a depth or output texture to `null` during `Resize`; its next `Render` call creates a replacement at the new framebuffer size.
+
+## Upload shared data
+
+The guides use one helper for small static and CPU-updated buffers:
+
+```csharp
+public static Buffer LoadBuffer(T[] data, BufferUsages usages) where T : unmanaged
+{
+ Buffer buffer = Context.CreateBuffer(new()
+ {
+ SizeInBytes = (uint)(sizeof(T) * data.Length),
+ StrideInBytes = (uint)sizeof(T),
+ Usages = usages,
+ Residency = MemoryResidency.CpuWriteOnly
+ });
+
+ fixed (T* pointer = data)
+ {
+ buffer.Upload(0, new()
+ {
+ Pointer = (nint)pointer,
+ SizeInBytes = (uint)(sizeof(T) * data.Length)
+ });
+ }
+
+ return buffer;
+}
+```
+
+`SizeInBytes` controls allocation size, while `StrideInBytes` describes one array element. `BufferUsages` states how later commands or shaders consume the buffer. The helper uses `CpuWriteOnly` residency because these compact guides favor direct uploads over a staging system.
+
+## Present offscreen textures
+
+Compute Shader and Ray Tracing do not render directly into the swap-chain drawable. They write an offscreen texture, transition it to `Sampled`, and pass it to `App.PresentTexture`.
+
+`TexturePresenter` owns a sampler, a two-handle constant buffer, and a fullscreen-triangle graphics pipeline:
+
+The complete presenter is also declared `unsafe` because it uploads a pointer to its local `Constants` value.
+
+| Resource | Role |
+| --- | --- |
+| Sampler | Samples the input texture with linear filtering and clamp addressing |
+| Constant buffer | Carries sampled-texture and sampler handles to Slang |
+| Graphics pipeline | Draws a fullscreen triangle into the current drawable |
+
+The presenter calculates a destination rectangle, uploads the two handles, and records a small render pass:
+
+```csharp
+Constants constants = new()
+{
+ Image = texture.SampledHandle,
+ Sampler = sampler.Handle
+};
+
+constantBuffer.Upload(0, new()
+{
+ Pointer = (nint)(&constants),
+ SizeInBytes = (uint)sizeof(Constants)
+});
+```
+
+```csharp
+commandBuffer.BeginRenderPass([ColorAttachment.Clear(drawable, new(0.04f, 0.055f, 0.075f, 1.0f))], null);
+
+commandBuffer.SetPipeline(pipeline);
+commandBuffer.SetViewports([new() { X = x, Y = y, Width = width, Height = height, MaxDepth = 1.0f }]);
+commandBuffer.SetScissors([new() { X = x, Y = y, Width = width, Height = height }]);
+commandBuffer.SetConstantBuffer(constantBuffer, 0);
+
+commandBuffer.Draw(3, 1, 0, 0);
+
+commandBuffer.EndRenderPass();
+```
+
+Its vertex shader derives a fullscreen triangle from `SV_VertexID`, so no vertex buffer is required. Workload guides treat this presenter as host infrastructure and focus on producing the sampled texture correctly.
+
+## Resource lifetime
+
+The `using TRenderer renderer = new();` declaration scopes the active renderer to the `try` body, so it is disposed when that body exits. The `finally` block then releases the host-owned presenter, swap chain, window, and graphics context:
+
+```csharp
+finally
+{
+ texturePresenter?.Dispose();
+ swapChain?.Dispose();
+ window?.Dispose();
+
+ Context.Dispose();
+}
+```
+
+The sample disposes temporary shader objects after pipeline creation. Buffers and textures remain alive until every submitted GPU command that references them has completed. A top-level acceleration structure is a stronger ownership example: keep every BLAS it instances alive until the TLAS is no longer used.
+
+## Complete source
+
+Use the complete host implementation as the shared starting point for the workload guides:
+
+- [App.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/App.cs)
+- [IRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/IRenderer.cs)
+- [CocoaHelper.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/CocoaHelper.cs)
+- [TexturePresenter.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/TexturePresenter.cs)
+- [PresentTexture.slang](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Assets/Shaders/PresentTexture.slang)
+- [Program.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Program.cs)
+
+The reference `Program.cs` presents a menu for all six completed renderers. Continue with [Hello Triangle](../guides/hello-triangle.md) to create the first workload renderer and understand the first menu entry.
diff --git a/documents/tutorials/getting-started/hello-triangle.md b/documents/tutorials/getting-started/hello-triangle.md
deleted file mode 100644
index 1bc4f4b3..00000000
--- a/documents/tutorials/getting-started/hello-triangle.md
+++ /dev/null
@@ -1,322 +0,0 @@
-# Hello Triangle
-
-In this tutorial, you'll create a renderer that draws a single colored triangle on screen. This is the classic starting point for graphics programming — establishing the graphics pipeline, defining vertex data, and issuing a draw call.
-
-## Overview
-
-This tutorial covers:
-
-- Defining a **Slang shader** with vertex and pixel stages
-- Creating a **vertex buffer** with position and color data
-- Configuring an **input layout** to describe vertex attributes
-- Building a **graphics pipeline** with render states
-- Recording and submitting **command buffers** to render a frame
-
-## The Renderer Class
-
-Create the file `Renderers/HelloTriangleRenderer.cs`:
-
-```csharp
-namespace ZenithTutorials.Renderers;
-
-internal unsafe class HelloTriangleRenderer : IRenderer
-{
- private const string ShaderSource = """
- struct VSInput
- {
- float3 Position : POSITION0;
-
- float4 Color : COLOR0;
- };
-
- struct PSInput
- {
- float4 Position : SV_POSITION;
-
- float4 Color : COLOR;
- };
-
- PSInput VSMain(VSInput input)
- {
- PSInput output;
- output.Position = float4(input.Position, 1.0);
- output.Color = input.Color;
-
- return output;
- }
-
- float4 PSMain(PSInput input) : SV_TARGET
- {
- return input.Color;
- }
- """;
-
- private readonly Buffer vertexBuffer;
- private readonly GraphicsPipeline pipeline;
-
- public HelloTriangleRenderer()
- {
- Vertex[] vertices =
- [
- new(new( 0.0f, 0.5f, 0.0f), new(1.0f, 0.0f, 0.0f, 1.0f)),
- new(new( 0.5f, -0.5f, 0.0f), new(0.0f, 1.0f, 0.0f, 1.0f)),
- new(new(-0.5f, -0.5f, 0.0f), new(0.0f, 0.0f, 1.0f, 1.0f)),
- ];
-
- vertexBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(Vertex) * vertices.Length),
- StrideInBytes = (uint)sizeof(Vertex),
- Flags = BufferUsageFlags.Vertex | BufferUsageFlags.MapWrite
- });
- vertexBuffer.Upload(vertices, 0);
-
- InputLayout inputLayout = new();
- inputLayout.Add(new() { Format = ElementFormat.Float3, Semantic = ElementSemantic.Position });
- inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Color });
-
- using Shader vertexShader = App.Context.LoadShaderFromSource(ShaderSource, "VSMain", ShaderStageFlags.Vertex);
- using Shader pixelShader = App.Context.LoadShaderFromSource(ShaderSource, "PSMain", ShaderStageFlags.Pixel);
-
- pipeline = App.Context.CreateGraphicsPipeline(new()
- {
- RenderStates = new()
- {
- RasterizerState = RasterizerStates.CullNone,
- DepthStencilState = DepthStencilStates.Default,
- BlendState = BlendStates.Opaque
- },
- Vertex = vertexShader,
- Pixel = pixelShader,
- ResourceLayout = null,
- InputLayouts = [inputLayout],
- PrimitiveTopology = PrimitiveTopology.TriangleList,
- Output = App.FrameBuffer.Output
- });
- }
-
- public void Update(double deltaTime)
- {
- }
-
- public void Render()
- {
- CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer();
-
- commandBuffer.BeginRenderPass(App.FrameBuffer, new()
- {
- ColorValues = [new(0.1f, 0.1f, 0.1f, 1.0f)],
- Depth = 1.0f,
- Stencil = 0,
- Flags = ClearFlags.All
- });
-
- commandBuffer.SetPipeline(pipeline);
- commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0);
- commandBuffer.Draw(3, 1, 0, 0);
-
- commandBuffer.EndRenderPass();
-
- commandBuffer.Submit(waitForCompletion: true);
- }
-
- public void Resize(uint width, uint height)
- {
- }
-
- public void Dispose()
- {
- pipeline.Dispose();
- vertexBuffer.Dispose();
- }
-}
-
-[StructLayout(LayoutKind.Sequential)]
-file struct Vertex(Vector3 position, Vector4 color)
-{
- public Vector3 Position = position;
-
- public Vector4 Color = color;
-}
-```
-
-## Running the Tutorial
-
-Run the application and select **1. Hello Triangle** from the menu:
-
-```bash
-dotnet run
-```
-
-## Result
-
-
-
-## Code Breakdown
-
-### Shader
-
-The shader is written inline as a Slang source string. It defines two stages:
-
-```csharp
-private const string ShaderSource = """
- struct VSInput
- {
- float3 Position : POSITION0;
-
- float4 Color : COLOR0;
- };
-
- struct PSInput
- {
- float4 Position : SV_POSITION;
-
- float4 Color : COLOR;
- };
-
- PSInput VSMain(VSInput input)
- {
- PSInput output;
- output.Position = float4(input.Position, 1.0);
- output.Color = input.Color;
-
- return output;
- }
-
- float4 PSMain(PSInput input) : SV_TARGET
- {
- return input.Color;
- }
- """;
-```
-
-- **VSMain**: Converts the 3D position to clip space and passes the color through
-- **PSMain**: Outputs the interpolated vertex color
-
-### Vertex Data
-
-Three vertices define the triangle with red, green, and blue colors:
-
-```csharp
-Vertex[] vertices =
-[
- new(new( 0.0f, 0.5f, 0.0f), new(1.0f, 0.0f, 0.0f, 1.0f)),
- new(new( 0.5f, -0.5f, 0.0f), new(0.0f, 1.0f, 0.0f, 1.0f)),
- new(new(-0.5f, -0.5f, 0.0f), new(0.0f, 0.0f, 1.0f, 1.0f)),
-];
-```
-
-The `Vertex` struct is defined as a `file`-scoped type with sequential layout:
-
-```csharp
-[StructLayout(LayoutKind.Sequential)]
-file struct Vertex(Vector3 position, Vector4 color)
-{
- public Vector3 Position = position;
-
- public Vector4 Color = color;
-}
-```
-
-### Vertex Buffer
-
-The buffer is created with `Vertex | MapWrite` flags. `MapWrite` enables CPU-side uploads:
-
-```csharp
-vertexBuffer = App.Context.CreateBuffer(new()
-{
- SizeInBytes = (uint)(sizeof(Vertex) * vertices.Length),
- StrideInBytes = (uint)sizeof(Vertex),
- Flags = BufferUsageFlags.Vertex | BufferUsageFlags.MapWrite
-});
-vertexBuffer.Upload(vertices, 0);
-```
-
-### Input Layout
-
-The input layout tells the pipeline how to interpret vertex data. The order must match the shader's `VSInput`:
-
-```csharp
-InputLayout inputLayout = new();
-inputLayout.Add(new() { Format = ElementFormat.Float3, Semantic = ElementSemantic.Position });
-inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Color });
-```
-
-### Graphics Pipeline
-
-The pipeline binds everything together — shaders, render states, input layout, and output format:
-
-```csharp
-pipeline = App.Context.CreateGraphicsPipeline(new()
-{
- RenderStates = new()
- {
- RasterizerState = RasterizerStates.CullNone,
- DepthStencilState = DepthStencilStates.Default,
- BlendState = BlendStates.Opaque
- },
- Vertex = vertexShader,
- Pixel = pixelShader,
- ResourceLayout = null,
- InputLayouts = [inputLayout],
- PrimitiveTopology = PrimitiveTopology.TriangleList,
- Output = App.FrameBuffer.Output
-});
-```
-
-| Property | Value | Purpose |
-|----------|-------|---------|
-| `RasterizerState` | `CullNone` | No face culling (both sides visible) |
-| `DepthStencilState` | `Default` | Standard depth testing |
-| `BlendState` | `Opaque` | No transparency |
-| `ResourceLayout` | `null` | No bound resources needed |
-| `PrimitiveTopology` | `TriangleList` | Every 3 vertices form a triangle |
-
-### Rendering
-
-Each frame, a command buffer records the draw commands:
-
-```csharp
-CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer();
-
-commandBuffer.BeginRenderPass(App.FrameBuffer, new()
-{
- ColorValues = [new(0.1f, 0.1f, 0.1f, 1.0f)],
- Depth = 1.0f,
- Stencil = 0,
- Flags = ClearFlags.All
-});
-
-commandBuffer.SetPipeline(pipeline);
-commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0);
-commandBuffer.Draw(3, 1, 0, 0);
-
-commandBuffer.EndRenderPass();
-
-commandBuffer.Submit(waitForCompletion: true);
-```
-
-`Draw(3, 1, 0, 0)` draws 3 vertices, 1 instance, starting at vertex 0 and instance 0.
-
-Note that `BeginRenderPass` does not pass a `ResourceTable` because this renderer has no bound resources.
-
-### Resource Cleanup
-
-All GPU resources must be disposed in reverse order of creation:
-
-```csharp
-public void Dispose()
-{
- pipeline.Dispose();
- vertexBuffer.Dispose();
-}
-```
-
-## Next Steps
-
-- [Textured Quad](textured-quad.md) - Add textures, index buffers, and samplers
-
-## Source Code
-
-> [!TIP]
-> View the complete source code on GitHub: [HelloTriangleRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Renderers/HelloTriangleRenderer.cs)
diff --git a/documents/tutorials/getting-started/prerequisites.md b/documents/tutorials/getting-started/prerequisites.md
deleted file mode 100644
index 90ca0bbb..00000000
--- a/documents/tutorials/getting-started/prerequisites.md
+++ /dev/null
@@ -1,460 +0,0 @@
-# Prerequisites
-
-Before starting the tutorials, you need to set up the project and create the shared framework code that all tutorials will use.
-
-## Development Environment
-
-- .NET 10.0 SDK or later
-- A GPU with DirectX 12, Metal 4, or Vulkan 1.4 support
-- Visual Studio 2026, VS Code, or JetBrains Rider
-
-> [!NOTE]
-> These tutorials target desktop platforms: Windows, macOS, and Linux.
-
-## Creating the Project
-
-```bash
-dotnet new console -n ZenithTutorials
-cd ZenithTutorials
-```
-
-### Required Packages
-
-```bash
-dotnet add package Zenith.NET.DirectX12
-dotnet add package Zenith.NET.Metal
-dotnet add package Zenith.NET.Vulkan
-dotnet add package Zenith.NET.Extensions.ImageSharp
-dotnet add package Zenith.NET.Extensions.Slang
-dotnet add package Silk.NET.Windowing
-dotnet add package Silk.NET.Input
-```
-
-### Project Configuration
-
-Your `.csproj` should look like this:
-
-```xml
-
-
-
- Exe
- net10.0
- enable
- enable
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- PreserveNewest
-
-
-
-
-```
-
-> [!NOTE]
-> `AllowUnsafeBlocks` is required because the tutorials use `sizeof` with custom structs for GPU buffer sizing.
-
-## Project Structure
-
-```
-ZenithTutorials/
-├── Program.cs
-├── App.cs
-├── IRenderer.cs
-├── BindingHelper.cs
-├── CocoaHelper.cs
-├── Usings.cs
-├── Assets/
-│ └── shoko.png
-└── Renderers/
- ├── HelloTriangleRenderer.cs
- ├── TexturedQuadRenderer.cs
- ├── SpinningCubeRenderer.cs
- ├── ComputeShaderRenderer.cs
- ├── IndirectDrawingRenderer.cs
- ├── RayTracingRenderer.cs
- └── MeshShadingRenderer.cs
-```
-
-### Asset File
-
-Save the following image as `Assets/shoko.png` in your project (right-click → Save As):
-
-
-
-## Framework Code
-
-The following files provide the shared infrastructure for all tutorials. Copy each file into your project.
-
-### Usings.cs
-
-```csharp
-global using System.Numerics;
-global using System.Runtime.CompilerServices;
-global using System.Runtime.InteropServices;
-global using Zenith.NET;
-global using Zenith.NET.Extensions.ImageSharp;
-global using Zenith.NET.Extensions.Slang;
-global using Buffer = Zenith.NET.Buffer;
-```
-
-### IRenderer.cs
-
-All tutorial renderers implement this interface:
-
-```csharp
-namespace ZenithTutorials;
-
-internal interface IRenderer : IDisposable
-{
- void Update(double deltaTime);
-
- void Render();
-
- void Resize(uint width, uint height);
-}
-```
-
-| Method | Called | Purpose |
-|--------|-------|---------|
-| `Update` | Every frame | Update logic (animations, transforms) |
-| `Render` | Every frame | Issue GPU commands |
-| `Resize` | On window resize | Recreate size-dependent resources |
-| `Dispose` | On exit | Clean up GPU resources |
-
-### App.cs
-
-The application framework manages window creation, graphics context initialization, and the render loop:
-
-```csharp
-using Silk.NET.Windowing;
-using Zenith.NET.DirectX12;
-using Zenith.NET.Metal;
-using Zenith.NET.Vulkan;
-
-namespace ZenithTutorials;
-
-internal static class App
-{
- private static readonly IWindow window;
- private static readonly SwapChain swapChain;
-
- static App()
- {
- if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsLinux())
- {
- throw new PlatformNotSupportedException("This application only supports Windows, macOS, and Linux.");
- }
-
- if (OperatingSystem.IsWindows())
- {
- Context = GraphicsContext.CreateDirectX12(useValidationLayer: true);
- }
- else if (OperatingSystem.IsMacOS())
- {
- Context = GraphicsContext.CreateMetal(useValidationLayer: true);
- }
- else
- {
- Context = GraphicsContext.CreateVulkan(useValidationLayer: true);
- }
-
- Context.ValidationMessage += static (sender, args) => Console.WriteLine($"[{args.Source} - {args.Severity}] {args.Message}");
-
- window = Window.Create(WindowOptions.Default with
- {
- API = GraphicsAPI.None,
- Title = "Zenith Tutorials",
- Size = new(1280, 720)
- });
- window.Initialize();
- window.Center();
-
- Surface surface;
- if (OperatingSystem.IsWindows())
- {
- surface = Surface.Win32(window.Native!.Win32!.Value.Hwnd, Width, Height);
- }
- else if (OperatingSystem.IsMacOS())
- {
- surface = Surface.Apple(CocoaHelper.CreateLayer(window.Native!.Cocoa!.Value), Width, Height);
- }
- else
- {
- surface = Surface.Xlib(window.Native!.X11!.Value.Display, (nint)window.Native.X11.Value.Window, Width, Height);
- }
-
- swapChain = Context.CreateSwapChain(new() { Surface = surface, ColorTargetFormat = PixelFormat.B8G8R8A8UNorm, DepthStencilTargetFormat = PixelFormat.D32FloatS8UInt });
- }
-
- public static GraphicsContext Context { get; }
-
- public static uint Width => (uint)window.FramebufferSize.X;
-
- public static uint Height => (uint)window.FramebufferSize.Y;
-
- public static FrameBuffer FrameBuffer => swapChain.FrameBuffer;
-
- public static void Run() where TRenderer : IRenderer, new()
- {
- try
- {
- using TRenderer renderer = new();
-
- window.Update += delta =>
- {
- if (Width is 0 || Height is 0)
- {
- return;
- }
-
- renderer.Update(delta);
- };
-
- window.Render += delta =>
- {
- if (Width is 0 || Height is 0)
- {
- return;
- }
-
- renderer.Render();
- swapChain.Present();
- };
-
- window.Resize += size =>
- {
- if (Width is 0 || Height is 0)
- {
- return;
- }
-
- renderer.Resize(Width, Height);
- swapChain.Resize(Width, Height);
- };
-
- window.Run();
- }
- finally
- {
- swapChain.Dispose();
- window.Dispose();
-
- Context.Dispose();
- }
- }
-}
-```
-
-`App` provides:
-
-| Member | Description |
-|--------|-------------|
-| `Context` | The `GraphicsContext` for the current platform |
-| `Width` / `Height` | Current framebuffer dimensions |
-| `FrameBuffer` | The swap chain's current frame buffer |
-| `Run()` | Creates a renderer, runs the window loop, and cleans up on exit |
-
-### BindingHelper.cs
-
-Each graphics backend (DirectX 12, Metal, Vulkan) uses different resource binding index conventions. `BindingHelper` assigns the correct indices automatically:
-
-```csharp
-namespace ZenithTutorials;
-
-internal static class BindingHelper
-{
- public static ResourceBinding[] Bindings(params ResourceBinding[] bindings)
- {
- switch (App.Context.Backend)
- {
- case Backend.DirectX12:
- {
- uint cbvIndex = 0;
- uint srvIndex = 0;
- uint uavIndex = 0;
- uint samplerIndex = 0;
-
- for (int i = 0; i < bindings.Length; i++)
- {
- ref ResourceBinding binding = ref bindings[i];
-
- binding = binding with
- {
- Index = binding.Type switch
- {
- ResourceType.ConstantBuffer => cbvIndex++,
-
- ResourceType.StructuredBuffer or
- ResourceType.Texture or
- ResourceType.AccelerationStructure => srvIndex++,
-
- ResourceType.StructuredBufferReadWrite or
- ResourceType.TextureReadWrite => uavIndex++,
-
- ResourceType.Sampler => samplerIndex++,
-
- _ => binding.Index
- }
- };
- }
- }
- break;
-
- case Backend.Metal:
- {
- uint bufferIndex = 0;
- uint textureIndex = 0;
- uint samplerIndex = 0;
-
- for (int i = 0; i < bindings.Length; i++)
- {
- ref ResourceBinding binding = ref bindings[i];
-
- binding = binding with
- {
- Index = binding.Type switch
- {
- ResourceType.ConstantBuffer or
- ResourceType.StructuredBuffer or
- ResourceType.StructuredBufferReadWrite or
- ResourceType.AccelerationStructure => bufferIndex++,
-
- ResourceType.Texture or
- ResourceType.TextureReadWrite => textureIndex++,
-
- ResourceType.Sampler => samplerIndex++,
-
- _ => binding.Index
- }
- };
- }
- }
- break;
-
- case Backend.Vulkan:
- {
- for (int i = 0; i < bindings.Length; i++)
- {
- ref ResourceBinding binding = ref bindings[i];
-
- binding = binding with { Index = (uint)i };
- }
- }
- break;
- }
-
- return bindings;
- }
-}
-```
-
-| Backend | Index Strategy |
-|---------|---------------|
-| **DirectX 12** | Separate counters per register type (CBV, SRV, UAV, Sampler) |
-| **Metal** | Separate counters per resource category (Buffer, Texture, Sampler) |
-| **Vulkan** | Sequential binding indices |
-
-### CocoaHelper.cs
-
-Required for macOS to create a `CAMetalLayer` for the window surface:
-
-```csharp
-namespace ZenithTutorials;
-
-internal static partial class CocoaHelper
-{
- private const string LibObjC = "/usr/lib/libobjc.A.dylib";
-
- [LibraryImport(LibObjC, EntryPoint = "objc_getClass")]
- private static partial nint GetClass([MarshalAs(UnmanagedType.LPUTF8Str)] string name);
-
- [LibraryImport(LibObjC, EntryPoint = "sel_registerName")]
- private static partial nint Selector([MarshalAs(UnmanagedType.LPUTF8Str)] string name);
-
- [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
- private static partial nint Send(nint receiver, nint selector);
-
- [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
- private static partial nint Send(nint receiver, nint selector, [MarshalAs(UnmanagedType.I1)] bool arg);
-
- [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
- private static partial nint Send(nint receiver, nint selector, nint arg);
-
- public static nint CreateLayer(nint cocoa)
- {
- nint layer = Send(GetClass("CAMetalLayer"), Selector("layer"));
- Send(layer, Selector("retain"));
-
- nint view = Send(cocoa, Selector("contentView"));
- Send(view, Selector("setWantsLayer:"), true);
- Send(view, Selector("setLayer:"), layer);
-
- return layer;
- }
-}
-```
-
-> [!NOTE]
-> On Windows and Linux, this file is not used but must be present to compile.
-
-### Program.cs
-
-The entry point provides an interactive tutorial selector:
-
-```csharp
-using ZenithTutorials;
-using ZenithTutorials.Renderers;
-
-(string Name, Action Run)[] tutorials =
-[
- ("Hello Triangle", App.Run),
- ("Textured Quad", App.Run),
- ("Spinning Cube", App.Run),
- ("Compute Shader", App.Run),
- ("Indirect Drawing", App.Run),
- ("Ray Tracing", App.Run),
- ("Mesh Shading", App.Run)
-];
-
-for (int i = 0; i < tutorials.Length; i++)
-{
- Console.WriteLine($"{i + 1}. {tutorials[i].Name}");
-}
-
-Console.Write("Select a tutorial to run: ");
-
-if (int.TryParse(Console.ReadKey().KeyChar.ToString(), out int choice) && choice >= 1 && choice <= tutorials.Length)
-{
- Console.WriteLine($"\nRunning '{tutorials[choice - 1].Name}' tutorial...");
-
- tutorials[choice - 1].Run();
-}
-```
-
-> [!TIP]
-> If you are following the tutorials sequentially, comment out renderers you haven't implemented yet to avoid build errors.
-
-## Next Steps
-
-With the framework in place, you're ready to start the first tutorial:
-
-- [Hello Triangle](hello-triangle.md) - Render your first triangle with a graphics pipeline
-
-## Source Code
-
-> [!TIP]
-> View the complete tutorial project on GitHub: [ZenithTutorials](https://github.com/qian-o/ZenithTutorials)
diff --git a/documents/tutorials/getting-started/project-setup.md b/documents/tutorials/getting-started/project-setup.md
new file mode 100644
index 00000000..065e9e12
--- /dev/null
+++ b/documents/tutorials/getting-started/project-setup.md
@@ -0,0 +1,134 @@
+# Project Setup
+
+This page creates the .NET project used throughout the guides. It installs the current Zenith.NET packages, enables the low-level C# features used for GPU uploads, and configures shader and texture assets for the build output.
+
+The runnable reference implementation is maintained separately in [ZenithTutorials](https://github.com/qian-o/ZenithTutorials). You do not need to clone that repository to follow the guides.
+
+## Requirements
+
+Install the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) and a current graphics driver. Verify the SDK from a terminal:
+
+```console
+dotnet --list-sdks
+```
+
+Zenith.NET selects a supported graphics API for the current operating system:
+
+| Operating system | Graphics API |
+| --- | --- |
+| Windows | DirectX 12 |
+| macOS | Metal 4 |
+| Linux | Vulkan 1.4 |
+
+Ray tracing and mesh shading are optional device capabilities. The corresponding guides check support at runtime before creating those resources.
+
+## Create the project
+
+Create a .NET 10 console application and enter its directory:
+
+```console
+dotnet new console --framework net10.0 --name ZenithTutorials
+cd ZenithTutorials
+```
+
+Add the windowing package, the Zenith.NET graphics packages, and the ImageSharp extension. No version is specified, so the CLI selects the latest compatible package available from the configured NuGet sources.
+
+```console
+dotnet package add Silk.NET.Windowing
+dotnet package add Zenith.NET
+dotnet package add Zenith.NET.DirectX12
+dotnet package add Zenith.NET.Metal
+dotnet package add Zenith.NET.Vulkan
+dotnet package add Zenith.NET.Extensions.ImageSharp
+```
+
+All three graphics packages can remain in one project. The application host creates only the graphics context selected for the current operating system.
+
+## Configure the project
+
+Open `ZenithTutorials.csproj`. Keep the package references generated by the CLI and make sure the main property group contains these settings:
+
+```xml
+
+ Exe
+ net10.0
+ enable
+ enable
+ true
+
+```
+
+Several guides upload unmanaged structures by pointer, so `AllowUnsafeBlocks` is required. Nullable analysis remains enabled for resources that are created lazily or recreated after a resize.
+
+`AllowUnsafeBlocks` permits unsafe code to compile; the type or method containing each pointer expression must still be declared `unsafe`. The reference host and the renderers that upload structures by pointer include that declaration in their complete source files.
+
+Shaders and textures are opened at runtime from the application directory. Add this item group after the package references:
+
+```xml
+
+
+ PreserveNewest
+
+
+```
+
+`PreserveNewest` copies changed assets without rewriting every file on every build.
+
+## Create the source layout
+
+Create the following folders and files as the guides introduce them:
+
+```text
+ZenithTutorials/
+├── Assets/
+│ ├── Shaders/
+│ └── Textures/
+├── Renderers/
+├── App.cs
+├── CocoaHelper.cs
+├── IRenderer.cs
+├── Program.cs
+├── TexturePresenter.cs
+├── Usings.cs
+└── ZenithTutorials.csproj
+```
+
+The application host is shared by every guide. Each workload adds one renderer under `Renderers` and one Slang source file under `Assets/Shaders`.
+
+These guides are walkthroughs of the current reference implementation rather than file-by-file listings. Each code fence is a contiguous excerpt from that implementation. Use the **Complete source** links at the end of a page when assembling the runnable file; the surrounding text explains why the excerpt exists and how it participates in the workload.
+
+## Add shared namespaces
+
+Create `Usings.cs`:
+
+```csharp
+global using System.Numerics;
+global using System.Runtime.CompilerServices;
+global using System.Runtime.InteropServices;
+global using Zenith.NET;
+global using Zenith.NET.Extensions.ImageSharp;
+global using Buffer = Zenith.NET.Buffer;
+```
+
+The alias on the final line resolves the name shared by `System.Buffer` and `Zenith.NET.Buffer`. `System.Numerics` supplies the vectors and matrices used by both the CPU data structures and the camera calculations.
+
+## Build the empty project
+
+Build once before adding the host:
+
+```console
+dotnet build
+```
+
+A successful build confirms that the SDK and packages restore correctly. No graphics device or window is created during `dotnet build`; those operations begin when the application runs.
+
+Continue with [Application Host](application-host.md) to add the shared window, graphics context, swap chain, and frame loop.
+
+## Reference project
+
+The finished project configuration and shared usings are available in the tutorial repository:
+
+- [ZenithTutorials.csproj](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/ZenithTutorials.csproj)
+- [Usings.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Usings.cs)
+
+The reference repository uses local project references so it can track Zenith.NET development. For a standalone reader project, keep the NuGet package references created above.
diff --git a/documents/tutorials/getting-started/spinning-cube.md b/documents/tutorials/getting-started/spinning-cube.md
deleted file mode 100644
index b1fae9b3..00000000
--- a/documents/tutorials/getting-started/spinning-cube.md
+++ /dev/null
@@ -1,350 +0,0 @@
-# Spinning Cube
-
-In this tutorial, you'll render a spinning 3D cube with per-vertex colors. This introduces constant buffers for uploading transformation matrices, and per-frame updates for animation.
-
-## Overview
-
-This tutorial covers:
-
-- Building **Model/View/Projection** transformation matrices
-- Creating and updating a **constant buffer** each frame
-- Using **back-face culling** and **depth testing** for 3D rendering
-- Animating object rotation over time in the `Update` loop
-
-## The Renderer Class
-
-Create the file `Renderers/SpinningCubeRenderer.cs`:
-
-```csharp
-namespace ZenithTutorials.Renderers;
-
-internal unsafe class SpinningCubeRenderer : IRenderer
-{
- private const string ShaderSource = """
- struct VSInput
- {
- float3 Position : POSITION0;
-
- float4 Color : COLOR0;
- };
-
- struct PSInput
- {
- float4 Position : SV_POSITION;
-
- float4 Color : COLOR;
- };
-
- struct Constants
- {
- float4x4 Model;
-
- float4x4 View;
-
- float4x4 Projection;
- };
-
- ConstantBuffer constants;
-
- PSInput VSMain(VSInput input)
- {
- float4x4 mvp = mul(mul(constants.Model, constants.View), constants.Projection);
-
- PSInput output;
- output.Position = mul(float4(input.Position, 1.0), mvp);
- output.Color = input.Color;
-
- return output;
- }
-
- float4 PSMain(PSInput input) : SV_TARGET
- {
- return input.Color;
- }
- """;
-
- private readonly Buffer vertexBuffer;
- private readonly Buffer indexBuffer;
- private readonly Buffer constantsBuffer;
- private readonly ResourceLayout resourceLayout;
- private readonly ResourceTable resourceTable;
- private readonly GraphicsPipeline pipeline;
-
- private float rotationAngle;
-
- public SpinningCubeRenderer()
- {
- Vertex[] vertices =
- [
- new(new(-0.5f, -0.5f, 0.5f), new(1.0f, 0.0f, 0.0f, 1.0f)),
- new(new( 0.5f, -0.5f, 0.5f), new(0.0f, 1.0f, 0.0f, 1.0f)),
- new(new( 0.5f, 0.5f, 0.5f), new(0.0f, 0.0f, 1.0f, 1.0f)),
- new(new(-0.5f, 0.5f, 0.5f), new(1.0f, 1.0f, 0.0f, 1.0f)),
- new(new(-0.5f, -0.5f, -0.5f), new(1.0f, 0.0f, 1.0f, 1.0f)),
- new(new( 0.5f, -0.5f, -0.5f), new(0.0f, 1.0f, 1.0f, 1.0f)),
- new(new( 0.5f, 0.5f, -0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)),
- new(new(-0.5f, 0.5f, -0.5f), new(0.5f, 0.5f, 0.5f, 1.0f))
- ];
-
- uint[] indices =
- [
- 0, 1, 2, 0, 2, 3,
- 5, 4, 7, 5, 7, 6,
- 4, 0, 3, 4, 3, 7,
- 1, 5, 6, 1, 6, 2,
- 3, 2, 6, 3, 6, 7,
- 4, 5, 1, 4, 1, 0
- ];
-
- vertexBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(Vertex) * vertices.Length),
- StrideInBytes = (uint)sizeof(Vertex),
- Flags = BufferUsageFlags.Vertex | BufferUsageFlags.MapWrite
- });
- vertexBuffer.Upload(vertices, 0);
-
- indexBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(uint) * indices.Length),
- StrideInBytes = sizeof(uint),
- Flags = BufferUsageFlags.Index | BufferUsageFlags.MapWrite
- });
- indexBuffer.Upload(indices, 0);
-
- constantsBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)sizeof(Constants),
- StrideInBytes = (uint)sizeof(Constants),
- Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite
- });
-
- resourceLayout = App.Context.CreateResourceLayout(new()
- {
- Bindings = BindingHelper.Bindings
- (
- new ResourceBinding() { Type = ResourceType.ConstantBuffer, Count = 1, StageFlags = ShaderStageFlags.Vertex }
- )
- });
-
- resourceTable = App.Context.CreateResourceTable(new()
- {
- Layout = resourceLayout,
- Resources = [constantsBuffer]
- });
-
- InputLayout inputLayout = new();
- inputLayout.Add(new() { Format = ElementFormat.Float3, Semantic = ElementSemantic.Position });
- inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Color });
-
- using Shader vertexShader = App.Context.LoadShaderFromSource(ShaderSource, "VSMain", ShaderStageFlags.Vertex);
- using Shader pixelShader = App.Context.LoadShaderFromSource(ShaderSource, "PSMain", ShaderStageFlags.Pixel);
-
- pipeline = App.Context.CreateGraphicsPipeline(new()
- {
- RenderStates = new()
- {
- RasterizerState = RasterizerStates.CullBack,
- DepthStencilState = DepthStencilStates.Default,
- BlendState = BlendStates.Opaque
- },
- Vertex = vertexShader,
- Pixel = pixelShader,
- ResourceLayout = resourceLayout,
- InputLayouts = [inputLayout],
- PrimitiveTopology = PrimitiveTopology.TriangleList,
- Output = App.FrameBuffer.Output
- });
- }
-
- public void Update(double deltaTime)
- {
- rotationAngle += (float)deltaTime;
-
- Matrix4x4 model = Matrix4x4.CreateRotationY(rotationAngle) * Matrix4x4.CreateRotationX(rotationAngle * 0.5f);
- Matrix4x4 view = Matrix4x4.CreateLookAt(new(0, 0, 3), Vector3.Zero, Vector3.UnitY);
- Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)App.Width / App.Height, 0.1f, 100.0f);
-
- constantsBuffer.Upload([new Constants() { Model = model, View = view, Projection = projection }], 0);
- }
-
- public void Render()
- {
- CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer();
-
- commandBuffer.BeginRenderPass(App.FrameBuffer, new()
- {
- ColorValues = [new(0.1f, 0.1f, 0.1f, 1.0f)],
- Depth = 1.0f,
- Stencil = 0,
- Flags = ClearFlags.All
- }, resourceTable);
-
- commandBuffer.SetPipeline(pipeline);
- commandBuffer.SetResourceTable(resourceTable);
- commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0);
- commandBuffer.SetIndexBuffer(indexBuffer, 0, IndexFormat.UInt32);
- commandBuffer.DrawIndexed(36, 1, 0, 0, 0);
-
- commandBuffer.EndRenderPass();
-
- commandBuffer.Submit(waitForCompletion: true);
- }
-
- public void Resize(uint width, uint height)
- {
- }
-
- public void Dispose()
- {
- pipeline.Dispose();
- resourceTable.Dispose();
- resourceLayout.Dispose();
- constantsBuffer.Dispose();
- indexBuffer.Dispose();
- vertexBuffer.Dispose();
- }
-}
-
-[StructLayout(LayoutKind.Sequential)]
-file struct Vertex(Vector3 position, Vector4 color)
-{
- public Vector3 Position = position;
-
- public Vector4 Color = color;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 192)]
-file struct Constants
-{
- [FieldOffset(0)]
- public Matrix4x4 Model;
-
- [FieldOffset(64)]
- public Matrix4x4 View;
-
- [FieldOffset(128)]
- public Matrix4x4 Projection;
-}
-```
-
-## Running the Tutorial
-
-Run the application and select **3. Spinning Cube** from the menu:
-
-```bash
-dotnet run
-```
-
-## Result
-
-
-
-## Code Breakdown
-
-### Shader
-
-The vertex shader computes the Model-View-Projection transform:
-
-```csharp
-private const string ShaderSource = """
- struct Constants
- {
- float4x4 Model;
-
- float4x4 View;
-
- float4x4 Projection;
- };
-
- ConstantBuffer constants;
-
- PSInput VSMain(VSInput input)
- {
- float4x4 mvp = mul(mul(constants.Model, constants.View), constants.Projection);
-
- PSInput output;
- output.Position = mul(float4(input.Position, 1.0), mvp);
- output.Color = input.Color;
-
- return output;
- }
- """;
-```
-
-`ConstantBuffer` gives the shader access to the CPU-uploaded matrices.
-
-### Constant Buffer
-
-A constant buffer is created for the MVP matrices, updated every frame:
-
-```csharp
-constantsBuffer = App.Context.CreateBuffer(new()
-{
- SizeInBytes = (uint)sizeof(Constants),
- StrideInBytes = (uint)sizeof(Constants),
- Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite
-});
-```
-
-The `Constants` struct uses explicit layout to match HLSL/Slang packing rules:
-
-```csharp
-[StructLayout(LayoutKind.Explicit, Size = 192)]
-file struct Constants
-{
- [FieldOffset(0)]
- public Matrix4x4 Model;
-
- [FieldOffset(64)]
- public Matrix4x4 View;
-
- [FieldOffset(128)]
- public Matrix4x4 Projection;
-}
-```
-
-Each `Matrix4x4` is 64 bytes (4x4 floats), giving a total size of 192 bytes.
-
-### Animation
-
-The `Update` method accumulates time and builds transformation matrices:
-
-```csharp
-public void Update(double deltaTime)
-{
- rotationAngle += (float)deltaTime;
-
- Matrix4x4 model = Matrix4x4.CreateRotationY(rotationAngle) * Matrix4x4.CreateRotationX(rotationAngle * 0.5f);
- Matrix4x4 view = Matrix4x4.CreateLookAt(new(0, 0, 3), Vector3.Zero, Vector3.UnitY);
- Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)App.Width / App.Height, 0.1f, 100.0f);
-
- constantsBuffer.Upload([new Constants() { Model = model, View = view, Projection = projection }], 0);
-}
-```
-
-| Matrix | Purpose |
-|--------|---------|
-| **Model** | Combined Y and X rotation, creating a tumbling effect |
-| **View** | Camera at `(0, 0, 3)` looking at the origin |
-| **Projection** | Perspective with 45-degree FOV |
-
-### Render States
-
-The pipeline now uses `CullBack` instead of `CullNone`:
-
-```csharp
-RasterizerState = RasterizerStates.CullBack,
-DepthStencilState = DepthStencilStates.Default,
-```
-
-Back-face culling discards triangles facing away from the camera, which is essential for 3D rendering performance.
-
-## Next Steps
-
-- [Compute Shader](../intermediate/compute-shader.md) - Process textures on the GPU with compute pipelines
-
-## Source Code
-
-> [!TIP]
-> View the complete source code on GitHub: [SpinningCubeRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Renderers/SpinningCubeRenderer.cs)
diff --git a/documents/tutorials/getting-started/textured-quad.md b/documents/tutorials/getting-started/textured-quad.md
deleted file mode 100644
index eafffde4..00000000
--- a/documents/tutorials/getting-started/textured-quad.md
+++ /dev/null
@@ -1,343 +0,0 @@
-# Textured Quad
-
-In this tutorial, you'll render a textured quad using an index buffer, a texture loaded from file, and a sampler. This introduces resource binding — connecting GPU resources like textures and samplers to shaders through resource layouts and tables.
-
-## Overview
-
-This tutorial covers:
-
-- Using an **index buffer** to share vertices between triangles
-- Loading a **texture** from an image file
-- Creating a **sampler** with filtering and address modes
-- Defining a **resource layout** and **resource table** to bind resources to shaders
-- Using `BindingHelper` for cross-backend resource binding
-
-## The Renderer Class
-
-Create the file `Renderers/TexturedQuadRenderer.cs`:
-
-```csharp
-namespace ZenithTutorials.Renderers;
-
-internal unsafe class TexturedQuadRenderer : IRenderer
-{
- private const string ShaderSource = """
- struct VSInput
- {
- float3 Position : POSITION0;
-
- float2 TexCoord : TEXCOORD0;
- };
-
- struct PSInput
- {
- float4 Position : SV_POSITION;
-
- float2 TexCoord : TEXCOORD;
- };
-
- Texture2D texture;
- SamplerState sampler;
-
- PSInput VSMain(VSInput input)
- {
- PSInput output;
- output.Position = float4(input.Position, 1.0);
- output.TexCoord = input.TexCoord;
-
- return output;
- }
-
- float4 PSMain(PSInput input) : SV_TARGET
- {
- return texture.Sample(sampler, input.TexCoord);
- }
- """;
-
- private readonly Buffer vertexBuffer;
- private readonly Buffer indexBuffer;
- private readonly Texture texture;
- private readonly Sampler sampler;
- private readonly ResourceLayout resourceLayout;
- private readonly ResourceTable resourceTable;
- private readonly GraphicsPipeline pipeline;
-
- public TexturedQuadRenderer()
- {
- Vertex[] vertices =
- [
- new(new(-0.5f, 0.5f, 0.0f), new(0.0f, 0.0f)),
- new(new( 0.5f, 0.5f, 0.0f), new(1.0f, 0.0f)),
- new(new( 0.5f, -0.5f, 0.0f), new(1.0f, 1.0f)),
- new(new(-0.5f, -0.5f, 0.0f), new(0.0f, 1.0f))
- ];
-
- uint[] indices = [0, 1, 2, 0, 2, 3];
-
- vertexBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(Vertex) * vertices.Length),
- StrideInBytes = (uint)sizeof(Vertex),
- Flags = BufferUsageFlags.Vertex | BufferUsageFlags.MapWrite
- });
- vertexBuffer.Upload(vertices, 0);
-
- indexBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(uint) * indices.Length),
- StrideInBytes = sizeof(uint),
- Flags = BufferUsageFlags.Index | BufferUsageFlags.MapWrite
- });
- indexBuffer.Upload(indices, 0);
-
- texture = App.Context.LoadTextureFromFile(Path.Combine(AppContext.BaseDirectory, "Assets", "shoko.png"), generateMipMaps: true);
-
- sampler = App.Context.CreateSampler(new()
- {
- U = AddressMode.Clamp,
- V = AddressMode.Clamp,
- W = AddressMode.Clamp,
- Filter = Filter.MinLinearMagLinearMipLinear,
- MaxLod = uint.MaxValue
- });
-
- resourceLayout = App.Context.CreateResourceLayout(new()
- {
- Bindings = BindingHelper.Bindings
- (
- new() { Type = ResourceType.Texture, Count = 1, StageFlags = ShaderStageFlags.Pixel },
- new() { Type = ResourceType.Sampler, Count = 1, StageFlags = ShaderStageFlags.Pixel }
- )
- });
-
- resourceTable = App.Context.CreateResourceTable(new()
- {
- Layout = resourceLayout,
- Resources = [texture, sampler]
- });
-
- InputLayout inputLayout = new();
- inputLayout.Add(new() { Format = ElementFormat.Float3, Semantic = ElementSemantic.Position });
- inputLayout.Add(new() { Format = ElementFormat.Float2, Semantic = ElementSemantic.TexCoord });
-
- using Shader vertexShader = App.Context.LoadShaderFromSource(ShaderSource, "VSMain", ShaderStageFlags.Vertex);
- using Shader pixelShader = App.Context.LoadShaderFromSource(ShaderSource, "PSMain", ShaderStageFlags.Pixel);
-
- pipeline = App.Context.CreateGraphicsPipeline(new()
- {
- RenderStates = new()
- {
- RasterizerState = RasterizerStates.CullNone,
- DepthStencilState = DepthStencilStates.Default,
- BlendState = BlendStates.Opaque
- },
- Vertex = vertexShader,
- Pixel = pixelShader,
- ResourceLayout = resourceLayout,
- InputLayouts = [inputLayout],
- PrimitiveTopology = PrimitiveTopology.TriangleList,
- Output = App.FrameBuffer.Output
- });
- }
-
- public void Update(double deltaTime)
- {
- }
-
- public void Render()
- {
- CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer();
-
- commandBuffer.BeginRenderPass(App.FrameBuffer, new()
- {
- ColorValues = [new(0.1f, 0.1f, 0.1f, 1.0f)],
- Depth = 1.0f,
- Stencil = 0,
- Flags = ClearFlags.All
- }, resourceTable);
-
- commandBuffer.SetPipeline(pipeline);
- commandBuffer.SetResourceTable(resourceTable);
- commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0);
- commandBuffer.SetIndexBuffer(indexBuffer, 0, IndexFormat.UInt32);
- commandBuffer.DrawIndexed(6, 1, 0, 0, 0);
-
- commandBuffer.EndRenderPass();
-
- commandBuffer.Submit(waitForCompletion: true);
- }
-
- public void Resize(uint width, uint height)
- {
- }
-
- public void Dispose()
- {
- pipeline.Dispose();
- resourceTable.Dispose();
- resourceLayout.Dispose();
- sampler.Dispose();
- texture.Dispose();
- indexBuffer.Dispose();
- vertexBuffer.Dispose();
- }
-}
-
-[StructLayout(LayoutKind.Sequential)]
-file struct Vertex(Vector3 position, Vector2 texCoord)
-{
- public Vector3 Position = position;
-
- public Vector2 TexCoord = texCoord;
-}
-```
-
-## Running the Tutorial
-
-Run the application and select **2. Textured Quad** from the menu:
-
-```bash
-dotnet run
-```
-
-## Result
-
-
-
-## Code Breakdown
-
-### Shader
-
-The pixel shader samples a texture using UV coordinates:
-
-```csharp
-private const string ShaderSource = """
- struct VSInput
- {
- float3 Position : POSITION0;
-
- float2 TexCoord : TEXCOORD0;
- };
-
- struct PSInput
- {
- float4 Position : SV_POSITION;
-
- float2 TexCoord : TEXCOORD;
- };
-
- Texture2D texture;
- SamplerState sampler;
-
- PSInput VSMain(VSInput input)
- {
- PSInput output;
- output.Position = float4(input.Position, 1.0);
- output.TexCoord = input.TexCoord;
-
- return output;
- }
-
- float4 PSMain(PSInput input) : SV_TARGET
- {
- return texture.Sample(sampler, input.TexCoord);
- }
- """;
-```
-
-`Texture2D` and `SamplerState` are declared as global resources. The pixel shader uses `texture.Sample(sampler, uv)` to fetch filtered texel colors.
-
-### Index Buffer
-
-Instead of duplicating vertices, an index buffer references shared vertices:
-
-```csharp
-Vertex[] vertices =
-[
- new(new(-0.5f, 0.5f, 0.0f), new(0.0f, 0.0f)),
- new(new( 0.5f, 0.5f, 0.0f), new(1.0f, 0.0f)),
- new(new( 0.5f, -0.5f, 0.0f), new(1.0f, 1.0f)),
- new(new(-0.5f, -0.5f, 0.0f), new(0.0f, 1.0f))
-];
-
-uint[] indices = [0, 1, 2, 0, 2, 3];
-```
-
-Two triangles (indices `0,1,2` and `0,2,3`) share vertices 0 and 2 to form the quad.
-
-### Texture and Sampler
-
-The texture is loaded from a file with mipmaps generated automatically:
-
-```csharp
-texture = App.Context.LoadTextureFromFile(Path.Combine(AppContext.BaseDirectory, "Assets", "shoko.png"), generateMipMaps: true);
-
-sampler = App.Context.CreateSampler(new()
-{
- U = AddressMode.Clamp,
- V = AddressMode.Clamp,
- W = AddressMode.Clamp,
- Filter = Filter.MinLinearMagLinearMipLinear,
- MaxLod = uint.MaxValue
-});
-```
-
-| Property | Value | Purpose |
-|----------|-------|---------|
-| `AddressMode.Clamp` | U, V, W | Clamp UVs to `[0,1]` — no texture wrapping |
-| `Filter` | `MinLinearMagLinearMipLinear` | Trilinear filtering for smooth sampling |
-| `MaxLod` | `uint.MaxValue` | Allow all mipmap levels |
-
-### Resource Binding
-
-Resources are exposed to shaders through a layout and table:
-
-```csharp
-resourceLayout = App.Context.CreateResourceLayout(new()
-{
- Bindings = BindingHelper.Bindings
- (
- new() { Type = ResourceType.Texture, Count = 1, StageFlags = ShaderStageFlags.Pixel },
- new() { Type = ResourceType.Sampler, Count = 1, StageFlags = ShaderStageFlags.Pixel }
- )
-});
-
-resourceTable = App.Context.CreateResourceTable(new()
-{
- Layout = resourceLayout,
- Resources = [texture, sampler]
-});
-```
-
-`BindingHelper.Bindings` assigns the correct binding indices per backend. `StageFlags` controls which shader stages can access each resource.
-
-### Rendering
-
-The render pass now receives the `resourceTable`, and uses indexed drawing:
-
-```csharp
-commandBuffer.BeginRenderPass(App.FrameBuffer, new()
-{
- ColorValues = [new(0.1f, 0.1f, 0.1f, 1.0f)],
- Depth = 1.0f,
- Stencil = 0,
- Flags = ClearFlags.All
-}, resourceTable);
-
-commandBuffer.SetPipeline(pipeline);
-commandBuffer.SetResourceTable(resourceTable);
-commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0);
-commandBuffer.SetIndexBuffer(indexBuffer, 0, IndexFormat.UInt32);
-commandBuffer.DrawIndexed(6, 1, 0, 0, 0);
-```
-
-`DrawIndexed(6, 1, 0, 0, 0)` draws 6 indices (2 triangles), 1 instance.
-
-## Next Steps
-
-- [Spinning Cube](spinning-cube.md) - Add 3D transformations with constant buffers
-
-## Source Code
-
-> [!TIP]
-> View the complete source code on GitHub: [TexturedQuadRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Renderers/TexturedQuadRenderer.cs)
diff --git a/documents/tutorials/guides/compute-shader.md b/documents/tutorials/guides/compute-shader.md
new file mode 100644
index 00000000..66b1f54d
--- /dev/null
+++ b/documents/tutorials/guides/compute-shader.md
@@ -0,0 +1,253 @@
+# Compute Shader
+
+Compute Shader produces an image without a render pass. A compute pipeline reads a sampled texture, converts one pixel per thread to grayscale, and writes a storage texture. The host then presents that output with its shared fullscreen-triangle pipeline.
+
+The grayscale conversion makes the result easy to inspect, but the guide focuses on Zenith.NET resource handles, compute dispatch, and texture layout transitions rather than color-science derivations.
+
+## Result
+
+
+
+The source image is processed once at its original dimensions. It is displayed at those dimensions while the framebuffer is at least as large as the image. Threads outside the image bounds are discarded when the dimensions are not exact multiples of the thread-group size.
+
+## Frame overview
+
+```mermaid
+flowchart LR
+ A{Already processed?} -- No --> B[Output to Storage]
+ B --> C[Bind compute pipeline and constants]
+ C --> D[Dispatch thread groups]
+ D --> E[Output to Sampled]
+ E --> F[Present texture]
+ A -- Yes --> F
+```
+
+The output is static, so the compute pass runs only during the first frame. Later frames reuse the sampled result.
+
+## Resource map
+
+| Resource | Usage | Access |
+| --- | --- | --- |
+| Input texture | `Sampled` | Compute shader reads pixels |
+| Output texture | `Storage` and `Sampled` | Compute shader writes; presenter reads |
+| Constant buffer | `Constant` | Width, height, and two resource handles |
+| Compute shader | `CSMain` | One thread per output pixel |
+| Compute pipeline | Compute dispatch | Binds the shader executable |
+
+The output texture needs both usages at creation time because it changes roles after the dispatch.
+
+## Load the input texture
+
+Place an image at `Assets/Textures/shoko.png`. The project configuration from Project Setup copies it next to the executable. The ImageSharp extension loads it into a sampled Zenith.NET texture:
+
+```csharp
+inputTexture = App.LoadTexture("shoko.png", false);
+```
+
+The final argument disables mipmap generation because this workload reads exact source pixels with `Texture2D.Load`.
+
+`App.LoadTexture` completes the upload before returning and leaves the loaded mip in `Sampled` layout. The compute pass therefore needs no input-texture transition; only the output texture changes layout during `Render`.
+
+## Create the output texture
+
+Create a floating-point texture with the same dimensions as the source:
+
+```csharp
+outputTexture = App.Context.CreateTexture(new()
+{
+ Type = TextureType.Texture2D,
+ Format = PixelFormat.R32G32B32A32Float,
+ Width = inputTexture.Desc.Width,
+ Height = inputTexture.Desc.Height,
+ Depth = 1,
+ MipLevels = 1,
+ ArrayLayers = 1,
+ SampleCount = SampleCount.Count1,
+ Usages = TextureUsages.Sampled | TextureUsages.Storage
+});
+```
+
+`R32G32B32A32Float` matches the shader's writable `float4` element type. `Storage` permits writes during compute; `Sampled` permits reads by `TexturePresenter` afterward.
+
+## Data contract
+
+The constant buffer passes image dimensions and descriptor handles:
+
+```slang
+struct Constants
+{
+ uint Width;
+
+ uint Height;
+
+ DescriptorHandle Input;
+
+ DescriptorHandle> Output;
+};
+
+ConstantBuffer constants;
+```
+
+The declaration after the structure binds that contract as the constant-buffer view read by `CSMain`.
+
+The C# layout matches the two four-byte integers followed by two eight-byte handles:
+
+```csharp
+[StructLayout(LayoutKind.Explicit, Size = 256)]
+file struct Constants
+{
+ [FieldOffset(0)]
+ public uint Width;
+
+ [FieldOffset(4)]
+ public uint Height;
+
+ [FieldOffset(8)]
+ public ResourceHandle Input;
+
+ [FieldOffset(16)]
+ public ResourceHandle Output;
+}
+```
+
+| Offset | C# value | Slang view |
+| ---: | --- | --- |
+| 0 | `inputTexture.Desc.Width` | `uint Width` |
+| 4 | `inputTexture.Desc.Height` | `uint Height` |
+| 8 | `inputTexture.SampledHandle` | `DescriptorHandle` |
+| 16 | `outputTexture.StorageHandle` | `DescriptorHandle>` |
+
+The structure reserves a 256-byte constant-buffer allocation while its semantic fields occupy the leading bytes. The offsets, rather than the C# field names, define the shared binary contract.
+
+Initialize and upload one value:
+
+```csharp
+Constants constants = new()
+{
+ Width = inputTexture.Desc.Width,
+ Height = inputTexture.Desc.Height,
+ Input = inputTexture.SampledHandle,
+ Output = outputTexture.StorageHandle
+};
+
+constantBuffer = App.LoadBuffer([constants], BufferUsages.Constant);
+```
+
+## Shader interface
+
+The shader declares a $16 \times 16$ thread group:
+
+```slang
+[shader("compute")]
+[numthreads(16, 16, 1)]
+void CSMain(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ uint2 pixel = dispatchThreadID.xy;
+
+ if (pixel.x >= constants.Width || pixel.y >= constants.Height)
+ {
+ return;
+ }
+
+ float4 color = constants.Input.Load(int3(pixel, 0));
+ float3 linear = pow(color.rgb, 2.2);
+ float grayscale = dot(linear, float3(0.2126, 0.7152, 0.0722));
+ grayscale = pow(grayscale, 1.0 / 2.2);
+
+ constants.Output[pixel] = float4(grayscale, grayscale, grayscale, color.a);
+}
+```
+
+`SV_DispatchThreadID` is the global thread coordinate across all dispatched groups. The bounds check is essential because group counts round up to cover partial groups at the image edges.
+
+The middle lines approximate conversion to linear light, calculate luminance, and convert back for display. That effect can be replaced without changing the pipeline or resource flow; the important interface is one sampled read and one storage write at the same pixel coordinate.
+
+## Create the compute pipeline
+
+Compile the compute entry point and create its pipeline:
+
+```csharp
+using Shader computeShader = App.LoadShader("ComputeShader.slang", "CSMain");
+
+computePipeline = App.Context.CreateComputePipeline(new() { ComputeShader = computeShader });
+```
+
+A compute pipeline has no attachment formats or rasterizer state. It executes outside a render pass.
+
+## Dispatch the workload
+
+Before the first dispatch, transition the output from its undefined initial contents into writable storage layout:
+
+```csharp
+commandBuffer.Transition(outputTexture, default, TextureLayout.Undefined, TextureLayout.Storage);
+
+commandBuffer.SetPipeline(computePipeline);
+commandBuffer.SetConstantBuffer(constantBuffer, 0);
+commandBuffer.Dispatch((inputTexture.Desc.Width + ThreadGroupSize - 1) / ThreadGroupSize, (inputTexture.Desc.Height + ThreadGroupSize - 1) / ThreadGroupSize, 1);
+```
+
+For a dimension $D$ and group size $G$, integer ceiling division is:
+
+$$
+\left\lceil \frac{D}{G} \right\rceil = \frac{D + G - 1}{G}
+$$
+
+After all storage writes, transition the texture for sampled reads:
+
+```csharp
+commandBuffer.Transition(outputTexture, default, TextureLayout.Storage, TextureLayout.Sampled);
+
+processed = true;
+```
+
+The command buffer preserves this order: transition, dispatch, then transition. The presenter consumes the texture only after it reaches `Sampled` layout.
+
+## Present the output
+
+Every frame, including the first, hands the sampled result to the shared presenter:
+
+```csharp
+App.PresentTexture(commandBuffer, drawable, outputTexture, false);
+```
+
+Passing `false` preserves the texture's original size and centers it when the framebuffer is at least as large in both dimensions. In a smaller framebuffer, the presenter clamps each viewport dimension to the available size. The presenter records the graphics render pass that writes the swap-chain drawable.
+
+## Lifetime
+
+This image does not depend on the framebuffer size, so `Resize` is empty. Dispose each renderer-owned resource after the final submitted frame has completed:
+
+```csharp
+public void Dispose()
+{
+ computePipeline.Dispose();
+ constantBuffer.Dispose();
+ outputTexture.Dispose();
+ inputTexture.Dispose();
+}
+```
+
+## Inspect the result
+
+Run the host and select **Compute Shader**. Confirm that:
+
+- the complete source image appears in grayscale;
+- no unprocessed strip appears along the right or bottom edge;
+- resizing to a framebuffer at least as large as the image preserves its original display size;
+- validation reports no storage/sampled usage or layout errors.
+
+An unprocessed edge usually indicates floor division or a missing bounds check. A blank result often indicates an incorrect descriptor handle or a missing `Storage` to `Sampled` transition.
+
+## Explore further
+
+1. Replace the grayscale calculation with `constants.Output[pixel] = color` to verify a direct copy.
+2. Invert `color.rgb` while preserving alpha.
+3. Change the declared thread-group size and update the C# `ThreadGroupSize` constant to match.
+4. Set `fitToWindow` to `true` when presenting and compare the viewport behavior.
+
+## Complete source
+
+- [ComputeShaderRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Renderers/ComputeShaderRenderer.cs)
+- [ComputeShader.slang](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Assets/Shaders/ComputeShader.slang)
+- [Input texture](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Assets/Textures/shoko.png)
+
+Continue with [Ray Tracing](ray-tracing.md) to reuse the compute-output flow for an image regenerated from inline ray queries every frame.
diff --git a/documents/tutorials/guides/hello-triangle.md b/documents/tutorials/guides/hello-triangle.md
new file mode 100644
index 00000000..002017d3
--- /dev/null
+++ b/documents/tutorials/guides/hello-triangle.md
@@ -0,0 +1,216 @@
+# Hello Triangle
+
+Hello Triangle is the smallest complete graphics workload in the series. It uploads three colored vertices, compiles one vertex and one fragment entry point, creates a graphics pipeline, and records a single draw into the swap-chain texture.
+
+This guide focuses on how vertex data moves from C# through a Zenith.NET pipeline into Slang. The shared window, command submission, and presentation loop come from [Application Host](../getting-started/application-host.md).
+
+## Result
+
+
+
+The final image is one triangle on a dark clear color. Red, green, and blue values are supplied per vertex and interpolated by the rasterizer across the covered fragments.
+
+## Frame overview
+
+The renderer creates its long-lived resources once. Each frame then records one render pass:
+
+```mermaid
+flowchart LR
+ A[Drawable in ColorAttachment] --> B[Begin render pass and clear]
+ B --> C[Bind graphics pipeline]
+ C --> D[Bind vertex buffer]
+ D --> E[Draw 3 vertices]
+ E --> F[End render pass]
+```
+
+The host transitions the drawable into `ColorAttachment` before calling the renderer and transitions it to `Present` afterward.
+
+## Resource map
+
+| Resource | Created from | Consumed by |
+| --- | --- | --- |
+| Vertex buffer | Three `Vertex` values | Vertex input stage |
+| Vertex shader | `VSMain` in `HelloTriangle.slang` | Graphics pipeline |
+| Fragment shader | `FSMain` in `HelloTriangle.slang` | Graphics pipeline |
+| Graphics pipeline | Shaders, input layout, attachment format, render state | Draw command |
+| Swap-chain drawable | Application host | Color attachment |
+
+There is no index buffer, constant buffer, depth attachment, or descriptor handle in this first workload.
+
+## Data contract
+
+The CPU vertex stores a three-component position followed by a four-component color:
+
+```csharp
+[StructLayout(LayoutKind.Sequential)]
+file struct Vertex(Vector3 position, Vector4 color)
+{
+ public Vector3 Position = position;
+
+ public Vector4 Color = color;
+}
+```
+
+The corresponding Slang input uses matching attribute types and semantic names:
+
+```slang
+struct VSInput
+{
+ float3 Position : POSITION0;
+
+ float4 Color : COLOR0;
+};
+```
+
+An `InputLayout` connects the buffer fields to those semantics:
+
+```csharp
+InputLayout inputLayout = new();
+inputLayout.Add(new() { Format = ElementFormat.Float3, Semantic = ElementSemantic.Position });
+inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Color });
+```
+
+The layout order matches the C# field order. `App.LoadBuffer` supplies `sizeof(Vertex)` as the buffer stride, so the GPU advances by one complete position-and-color record for each vertex.
+
+## Create the vertex buffer
+
+The renderer defines three positions directly in clip space. Their colors become the three corners visible in the result:
+
+```csharp
+Vertex[] vertices =
+[
+ new(new(0.0f, 0.6f, 0.0f), new(1.0f, 0.2f, 0.15f, 1.0f)),
+ new(new(0.6f, -0.5f, 0.0f), new(0.15f, 0.85f, 0.35f, 1.0f)),
+ new(new(-0.6f, -0.5f, 0.0f), new(0.2f, 0.45f, 1.0f, 1.0f))
+];
+
+vertexBuffer = App.LoadBuffer(vertices, BufferUsages.Vertex);
+```
+
+`BufferUsages.Vertex` declares that later commands will bind this allocation as vertex input. Because the coordinates already lie in clip space, the workload does not need model, view, or projection matrices.
+
+## Shader interface
+
+The vertex shader converts each `float3` position into homogeneous clip-space coordinates and forwards the color:
+
+```slang
+struct FSInput
+{
+ float4 Position : SV_POSITION;
+
+ float4 Color : COLOR0;
+};
+
+[shader("vertex")]
+FSInput VSMain(VSInput input)
+{
+ FSInput output;
+ output.Position = float4(input.Position, 1.0);
+ output.Color = input.Color;
+
+ return output;
+}
+```
+
+`SV_POSITION` is consumed by rasterization. `COLOR0` is a user varying: the rasterizer interpolates it, then supplies the interpolated value to the fragment shader.
+
+```slang
+[shader("fragment")]
+float4 FSMain(FSInput input) : SV_TARGET
+{
+ return input.Color;
+}
+```
+
+`SV_TARGET` marks the returned value as the color written to the active color attachment.
+
+## Create the pipeline
+
+Compile both named entry points and create the graphics pipeline:
+
+```csharp
+using Shader vertexShader = App.LoadShader("HelloTriangle.slang", "VSMain");
+using Shader fragmentShader = App.LoadShader("HelloTriangle.slang", "FSMain");
+
+pipeline = App.Context.CreateGraphicsPipeline(new()
+{
+ VertexShader = vertexShader,
+ FragmentShader = fragmentShader,
+ InputLayouts = [inputLayout],
+ PrimitiveTopology = PrimitiveTopology.TriangleList,
+ AttachmentFormats = new()
+ {
+ ColorFormats = [App.ColorFormat],
+ SampleCount = SampleCount.Count1
+ },
+ RenderState = new()
+ {
+ Rasterizer = RasterizerState.CullNone(),
+ DepthStencil = DepthStencilState.DepthNone(),
+ Blend = BlendState.Opaque()
+ }
+});
+```
+
+The attachment format must match the swap chain. A triangle list consumes every group of three vertices as one triangle. Culling and depth testing are unnecessary because this workload has one two-dimensional primitive and no depth attachment.
+
+The `using` declarations limit the temporary `Shader` objects to pipeline creation.
+
+## Record the frame
+
+The render method clears the drawable, binds the two resources required by the draw, and emits one triangle:
+
+```csharp
+public void Render(CommandBuffer commandBuffer, Texture drawable)
+{
+ commandBuffer.BeginRenderPass([ColorAttachment.Clear(drawable, new(0.04f, 0.055f, 0.075f, 1.0f))], null);
+
+ commandBuffer.SetPipeline(pipeline);
+ commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0);
+
+ commandBuffer.Draw(3, 1, 0, 0);
+
+ commandBuffer.EndRenderPass();
+}
+```
+
+The arguments to `Draw` request three vertices, one instance, first vertex zero, and first instance zero. No explicit viewport or scissor is needed here; the render pass uses the drawable dimensions.
+
+## Resource lifetime
+
+This renderer has no animated or size-dependent resources, so `Update` and `Resize` remain empty. Dispose both renderer-owned resources after the final submitted frame has completed:
+
+```csharp
+public void Dispose()
+{
+ pipeline.Dispose();
+ vertexBuffer.Dispose();
+}
+```
+
+## Inspect the result
+
+Run the tutorial host and select **Hello Triangle**. Confirm that:
+
+- the triangle appears centered on the dark background;
+- all three corner colors are visible and interpolate smoothly;
+- resizing the window preserves the workload without recreating resources;
+- the validation layer reports no errors.
+
+If the pipeline is rejected, first compare `App.ColorFormat` with `AttachmentFormats.ColorFormats`. If geometry is missing or corrupted, compare the C# field order, `InputLayout`, and Slang semantics.
+
+## Explore further
+
+Try these changes independently:
+
+1. Change the x and y components while keeping them in $[-1,1]$; leave z at 0 so the vertices remain in the visible depth range.
+2. Replace all three colors with one constant color and observe that interpolation no longer produces a gradient.
+3. Reverse two vertices and enable back-face culling to inspect how winding affects visibility.
+4. Add a fourth vertex and change the topology to explore how a triangle list groups input vertices.
+
+## Complete source
+
+- [HelloTriangleRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Renderers/HelloTriangleRenderer.cs)
+- [HelloTriangle.slang](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Assets/Shaders/HelloTriangle.slang)
+
+Continue with [Spinning Cube](spinning-cube.md) to add indexed geometry, transformation constants, back-face culling, and a depth attachment.
diff --git a/documents/tutorials/guides/indirect-drawing.md b/documents/tutorials/guides/indirect-drawing.md
new file mode 100644
index 00000000..af6ab622
--- /dev/null
+++ b/documents/tutorials/guides/indirect-drawing.md
@@ -0,0 +1,289 @@
+# Indirect Drawing
+
+Indirect Drawing reuses the depth-tested cube pipeline to render a $5 \times 5$ grid of animated instances. One structured buffer stores the model matrix and tint for each cube, while one indirect argument record supplies the indexed draw counts to the GPU.
+
+Two mechanisms are involved:
+
+- **instancing** draws the shared geometry multiple times and assigns each invocation an `SV_InstanceID`;
+- **indirect drawing** reads draw parameters from a buffer instead of C# method arguments.
+
+The CPU still updates instance data in this workload. The indirect buffer demonstrates the command format; it is not generated by a GPU culling pass.
+
+## Result
+
+
+
+Twenty-five cubes share one vertex buffer, one index buffer, one graphics pipeline, and one indirect draw record. Their positions, rotations, and colors vary through structured instance data.
+
+## Frame overview
+
+```mermaid
+flowchart LR
+ A[Update 25 Instance records] --> B[Upload structured buffer]
+ B --> C[Begin color and depth pass]
+ C --> D[Bind geometry, constants, and pipeline]
+ D --> E[DrawIndexedIndirect one record]
+ E --> F[End render pass]
+```
+
+The indirect record is static in this example; only the instance buffer changes each frame.
+
+## Resource map
+
+| Resource | Usage | Contents |
+| --- | --- | --- |
+| Vertex buffer | `Vertex` | Shared cube corners |
+| Index buffer | `Index` | 36 cube indices |
+| Instance buffer | `StorageReadOnly` | 25 model matrices and colors |
+| Indirect buffer | `Indirect` | One `IndirectDrawIndexedArgs` record |
+| Constant buffer | `Constant` | View, projection, and instance-buffer handle |
+| Graphics pipeline | Indexed instanced rendering | Depth and back-face culling enabled |
+| Depth texture | Depth-stencil attachment | Recreated after resize |
+
+The shader reaches the structured buffer through a descriptor handle stored in the constant buffer. It does not bind the instance buffer as vertex input.
+
+## Define the indirect command
+
+The indirect structure contains the same values supplied to a direct indexed draw:
+
+```csharp
+IndirectDrawIndexedArgs arguments = new()
+{
+ IndexCount = (uint)indices.Length,
+ InstanceCount = GridSize * GridSize
+};
+```
+
+Unassigned fields remain zero, so this record means:
+
+| Field | Value | Meaning |
+| --- | ---: | --- |
+| `IndexCount` | 36 | Indices consumed per instance |
+| `InstanceCount` | 25 | Number of cube instances |
+| `FirstIndex` | 0 | Start of the index buffer |
+| `VertexOffset` | 0 | Added to each selected index |
+| `FirstInstance` | 0 | Starting system instance ID |
+
+Upload one record with `BufferUsages.Indirect`:
+
+```csharp
+indirectBuffer = App.LoadBuffer([arguments], BufferUsages.Indirect);
+```
+
+The final draw call requests one record from this buffer. It does not pass the instance count separately.
+
+## Instance data contract
+
+Each instance occupies 80 bytes in both C# and Slang:
+
+```csharp
+[StructLayout(LayoutKind.Explicit, Size = 80)]
+file struct Instance
+{
+ [FieldOffset(0)]
+ public Matrix4x4 Model;
+
+ [FieldOffset(64)]
+ public Vector4 Color;
+}
+```
+
+```slang
+struct Instance
+{
+ float4x4 Model;
+
+ float4 Color;
+};
+```
+
+The renderer allocates enough structured-buffer elements for the complete grid:
+
+```csharp
+instanceBuffer = App.LoadBuffer(new Instance[GridSize * GridSize], BufferUsages.StorageReadOnly);
+```
+
+`App.LoadBuffer` records `sizeof(Instance)` as the stride. The shader therefore indexes a sequence of 80-byte records through `StructuredBuffer`.
+
+## Constant-buffer contract
+
+The vertex shader also needs the camera matrices and a handle to the instance buffer:
+
+```slang
+struct Constants
+{
+ float4x4 View;
+
+ float4x4 Projection;
+
+ DescriptorHandle> Instances;
+};
+```
+
+The corresponding C# offsets place the handle after two 64-byte matrices:
+
+```csharp
+[StructLayout(LayoutKind.Explicit, Size = 256)]
+file struct Constants
+{
+ [FieldOffset(0)]
+ public Matrix4x4 View;
+
+ [FieldOffset(64)]
+ public Matrix4x4 Projection;
+
+ [FieldOffset(128)]
+ public ResourceHandle Instances;
+}
+```
+
+On initialization and resize, upload `instanceBuffer.StorageReadOnlyHandle` with the current view and projection:
+
+```csharp
+Constants constants = new()
+{
+ View = Matrix4x4.CreateLookAt(new(0.0f, 0.0f, 8.0f), Vector3.Zero, Vector3.UnitY),
+ Projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)width / height, 0.1f, 100.0f),
+ Instances = instanceBuffer.StorageReadOnlyHandle
+};
+```
+
+## Select an instance in Slang
+
+`SV_InstanceID` is generated by the draw command. It is a system value, not an attribute in the C# `InputLayout`:
+
+```slang
+struct VSInput
+{
+ float3 Position : POSITION0;
+
+ float4 Color : COLOR0;
+
+ uint InstanceID : SV_InstanceID;
+};
+```
+
+The vertex shader uses that ID to select one structured-buffer element:
+
+```slang
+[shader("vertex")]
+VSOutput VSMain(VSInput input)
+{
+ Instance instance = constants.Instances[input.InstanceID];
+ float4 worldPosition = mul(float4(input.Position, 1.0), instance.Model);
+ float4 viewPosition = mul(worldPosition, constants.View);
+
+ VSOutput output;
+ output.Position = mul(viewPosition, constants.Projection);
+ output.Color = input.Color * instance.Color;
+
+ return output;
+}
+```
+
+Every invocation reads the same cube geometry. The instance record supplies only its world transform and tint.
+
+## Update instance records
+
+The CPU calculates one record for every grid coordinate:
+
+```csharp
+for (uint index = 0; index < GridSize * GridSize; index++)
+{
+ uint x = index % GridSize;
+ uint y = index / GridSize;
+ float offsetX = (x - ((GridSize - 1) * 0.5f)) * 1.5f;
+ float offsetY = (y - ((GridSize - 1) * 0.5f)) * 1.5f;
+ float rotation = rotationAngle * (1.0f + (index * 0.1f));
+
+ pointer[index] = new()
+ {
+ Model = Matrix4x4.CreateScale(0.4f) * Matrix4x4.CreateRotationY(rotation) * Matrix4x4.CreateRotationX(rotation * 0.5f) * Matrix4x4.CreateTranslation(offsetX, offsetY, 0.0f),
+ Color = new((float)x / GridSize, (float)y / GridSize, 1.0f - ((float)x / GridSize), 1.0f)
+ };
+}
+```
+
+After filling the array, upload all records in one contiguous copy:
+
+```csharp
+instanceBuffer.Upload(0, new()
+{
+ Pointer = (nint)pointer,
+ SizeInBytes = (uint)(sizeof(Instance) * instances.Length)
+});
+```
+
+This direct CPU update is intentionally simple. A larger renderer would normally avoid allocating a new managed array every frame and would coordinate dynamic-buffer reuse across frames in flight.
+
+The shared host waits for each submitted frame before the next update, so this single instance buffer is not overwritten while an earlier frame is reading it.
+
+## Record the indirect draw
+
+Create the depth attachment on first use and transition it from `Undefined` to `DepthStencilAttachment`. The texture remains in that layout until resize replaces it. Then begin a color/depth render pass, bind the shared geometry and constants, and execute one indirect record:
+
+```csharp
+if (depthTexture is null)
+{
+ depthTexture = App.Context.CreateTexture(TextureDesc.DepthStencilAttachment(PixelFormat.D32FloatS8UInt, App.Width, App.Height, SampleCount.Count1));
+
+ commandBuffer.Transition(depthTexture, default, TextureLayout.Undefined, TextureLayout.DepthStencilAttachment);
+}
+
+commandBuffer.BeginRenderPass([ColorAttachment.Clear(drawable, new(0.04f, 0.055f, 0.075f, 1.0f))], DepthStencilAttachment.Clear(depthTexture, 1.0f, 0));
+
+commandBuffer.SetPipeline(pipeline);
+commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0);
+commandBuffer.SetIndexBuffer(indexBuffer, 0, IndexFormat.UInt32);
+commandBuffer.SetConstantBuffer(constantBuffer, 0);
+
+commandBuffer.DrawIndexedIndirect(indirectBuffer, 0, 1);
+
+commandBuffer.EndRenderPass();
+```
+
+The arguments mean indirect-buffer byte offset zero and draw count one. That one record expands to an indexed draw with 25 instances.
+
+The GPU does not read the indirect buffer through a shader descriptor. The command processor interprets it because the allocation was created with `BufferUsages.Indirect` and passed to `DrawIndexedIndirect`.
+
+## Resize and lifetime
+
+Resize invalidates the depth texture and uploads a projection for the new aspect ratio. Geometry, indirect arguments, and instance-buffer capacity remain unchanged.
+
+Release resources in dependency order:
+
+```csharp
+depthTexture?.Dispose();
+
+pipeline.Dispose();
+constantBuffer.Dispose();
+instanceBuffer.Dispose();
+indirectBuffer.Dispose();
+indexBuffer.Dispose();
+vertexBuffer.Dispose();
+```
+
+## Inspect the result
+
+Run the host and select **Indirect Drawing**. Confirm that:
+
+- 25 cubes appear in a centered grid;
+- each cube rotates and receives a distinct tint;
+- resize preserves the grid proportions and depth behavior;
+- validation reports no buffer-usage, structured-stride, or indirect-command errors.
+
+If every cube uses the same transform, inspect `SV_InstanceID` and the structured-buffer handle. If only one cube appears, inspect `InstanceCount` in the indirect record. If the draw is rejected, confirm `BufferUsages.Indirect` and the record layout.
+
+## Explore further
+
+1. Replace `DrawIndexedIndirect` temporarily with `DrawIndexed(36, GridSize * GridSize, 0, 0, 0)` and verify that the picture remains equivalent.
+2. Change `GridSize`, then update buffer capacity and camera distance together.
+3. Set `FirstInstance` to a nonzero value and account for it in the instance buffer.
+4. Create multiple indirect records and increase the final `drawCount` argument.
+
+## Complete source
+
+- [IndirectDrawingRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Renderers/IndirectDrawingRenderer.cs)
+- [IndirectDrawing.slang](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Assets/Shaders/IndirectDrawing.slang)
+
+Continue with [Ray Tracing](ray-tracing.md) to see a different kind of GPU-readable scene structure, or [Mesh Shading](mesh-shading.md) to move geometry emission into programmable shader stages.
diff --git a/documents/tutorials/guides/mesh-shading.md b/documents/tutorials/guides/mesh-shading.md
new file mode 100644
index 00000000..a8ae80a2
--- /dev/null
+++ b/documents/tutorials/guides/mesh-shading.md
@@ -0,0 +1,454 @@
+# Mesh Shading
+
+Mesh Shading replaces fixed vertex and index fetch with programmable task and mesh stages. This workload renders a $10 \times 10 \times 10$ sphere grid. Task threads test candidate instances against the camera frustum, compact visible IDs into a payload, and launch one mesh workgroup for each surviving sphere.
+
+This guide focuses on the Zenith.NET mesh-shading pipeline, structured geometry, C#/Slang layout, task payload, synchronization, and mesh dispatch. The UV-sphere construction and final diffuse-lighting calculation support the sample result but are not derived as general geometry or lighting algorithms.
+
+## Result
+
+
+
+The camera moves around 1,000 possible sphere instances. Only instances intersecting the view frustum produce mesh workgroups and rasterized geometry.
+
+> [!IMPORTANT]
+> Mesh shading is optional. Check `App.Context.Capabilities.MeshShadingSupported` before creating shaders, buffers, or a mesh-shading pipeline.
+
+## Frame overview
+
+```mermaid
+flowchart LR
+ A[Update camera and frustum constants] --> B[Begin color and depth pass]
+ B --> C[Bind mesh-shading pipeline]
+ C --> D[Bind constants]
+ D --> E[Dispatch 32 task groups]
+ E --> F[Task groups compact visible IDs]
+ F --> G[Mesh groups emit sphere geometry]
+ G --> H[Rasterize and shade fragments]
+```
+
+No vertex or index buffer is bound through traditional command-buffer slots. The mesh shader reads source geometry through descriptor handles and writes pipeline outputs directly.
+
+## Pipeline stages
+
+```mermaid
+flowchart TD
+ A[32 candidate IDs per task group] --> B[Frustum test]
+ B --> C[groupshared payload]
+ C --> D[DispatchMesh visible count]
+ D --> E[One mesh group per visible ID]
+ E --> F[Emit 62 vertices]
+ E --> G[Emit 120 triangles]
+ F --> H[Rasterization]
+ G --> H
+ H --> I[Fragment shader]
+```
+
+Zenith.NET calls the first stage `TaskShader`; Slang declares the corresponding entry point with `[shader("task")]`. Other APIs may call this an amplification or object stage.
+
+## Resource map
+
+| Resource | Usage | Role |
+| --- | --- | --- |
+| Vertex buffer | `StorageReadOnly` | Source sphere positions and normals |
+| Triangle buffer | `StorageReadOnly` | Source sphere index triplets |
+| Constant buffer | `Constant` | View-projection, frustum, light, and two handles |
+| Task shader | `ASMain` | Culls and compacts candidate instance IDs |
+| Mesh shader | `MSMain` | Emits transformed vertices and primitive indices |
+| Fragment shader | `FSMain` | Applies simple directional lighting |
+| Mesh-shading pipeline | Task, mesh, and fragment stages | Rasterized output |
+| Depth texture | Depth-stencil attachment | Recreated after resize |
+
+The source geometry remains in structured buffers for the renderer lifetime. Each visible instance reuses the same 62 vertices and 120 triangles.
+
+## Check device support
+
+Reject unsupported devices before generating or uploading the source mesh:
+
+```csharp
+if (!App.Context.Capabilities.MeshShadingSupported)
+{
+ throw new PlatformNotSupportedException("Mesh Shading is not supported by the selected device.");
+}
+```
+
+The capability controls creation and execution of the task/mesh pipeline. It is independent of the traditional graphics pipeline used by earlier guides.
+
+## Create the source mesh
+
+The CPU generates one compact UV sphere with 62 vertices and 120 triangles. It creates a north pole, five latitude rings, a south pole, and indices for both caps and the ring quads.
+
+The complete generation loop is available in the renderer source. Its important output for this workload is two tightly defined arrays:
+
+```csharp
+vertexBuffer = App.LoadBuffer([.. sphereVertices], BufferUsages.StorageReadOnly);
+triangleBuffer = App.LoadBuffer([.. sphereTriangles], BufferUsages.StorageReadOnly);
+constantBuffer = App.LoadBuffer([new Constants()], BufferUsages.Constant);
+```
+
+These are structured shader inputs, not traditional vertex and index bindings. Their element strides come from the explicit C# structures described next.
+
+## Geometry data contract
+
+Three-component vectors can receive different alignment across shader targets. The C# vertex therefore places position and normal at separate 16-byte boundaries:
+
+```csharp
+[StructLayout(LayoutKind.Explicit, Size = 32)]
+file struct Vertex
+{
+ [FieldOffset(0)]
+ public Vector3 Position;
+
+ [FieldOffset(16)]
+ public Vector3 Normal;
+}
+```
+
+The Slang side uses two `float4` backing values:
+
+```slang
+struct Vertex
+{
+ private float4 PositionAndPadding;
+
+ private float4 NormalAndPadding;
+
+ property float3 Position
+ {
+ get {
+ return PositionAndPadding.xyz;
+ }
+ }
+
+ property float3 Normal
+ {
+ get {
+ return NormalAndPadding.xyz;
+ }
+ }
+};
+```
+
+Triangle elements follow the same pattern. C# stores three indices in a 16-byte record:
+
+```csharp
+[StructLayout(LayoutKind.Explicit, Size = 16)]
+file struct Triangle
+{
+ [FieldOffset(0)]
+ public uint Index0;
+
+ [FieldOffset(4)]
+ public uint Index1;
+
+ [FieldOffset(8)]
+ public uint Index2;
+}
+```
+
+Slang reads one `uint4` and exposes its first three lanes:
+
+```slang
+struct Triangle
+{
+ private uint4 IndicesAndPadding;
+
+ property uint3 Indices
+ {
+ get {
+ return IndicesAndPadding.xyz;
+ }
+ }
+};
+```
+
+| Structured element | C# size | Slang backing storage |
+| --- | ---: | --- |
+| `Vertex` | 32 bytes | Two `float4` values |
+| `Triangle` | 16 bytes | One `uint4` value |
+
+Explicit backing storage keeps these strides consistent across the supported graphics APIs.
+
+## Frame constants
+
+The constant buffer supplies one view-projection matrix, six frustum planes, a light direction, and two structured-buffer handles:
+
+```csharp
+[StructLayout(LayoutKind.Explicit, Size = 256)]
+file struct Constants
+{
+ [FieldOffset(0)]
+ public Matrix4x4 ViewProjection;
+
+ [FieldOffset(64)]
+ public FrustumPlanes FrustumPlanes;
+
+ [FieldOffset(160)]
+ public Vector3 LightDirection;
+
+ [FieldOffset(176)]
+ public ResourceHandle Vertices;
+
+ [FieldOffset(184)]
+ public ResourceHandle Triangles;
+}
+```
+
+`FrustumPlanes` is an inline array of six `Vector4` values, occupying 96 bytes from offset 64 through 159. Slang backs `LightDirection` with a `float4`, so the handles begin at byte 176 rather than immediately after the 12-byte C# `Vector3`.
+
+```slang
+struct Constants
+{
+ float4x4 ViewProjection;
+
+ float4 FrustumPlanes[6];
+
+ private float4 LightDirectionAndPadding;
+
+ DescriptorHandle> Vertices;
+
+ DescriptorHandle> Triangles;
+
+ property float3 LightDirection
+ {
+ get {
+ return LightDirectionAndPadding.xyz;
+ }
+ }
+};
+```
+
+The renderer extracts and normalizes the six planes from the current view-projection matrix. That matrix extraction is supporting camera math; the mesh-shading interface only requires six consistently oriented `float4` plane equations.
+
+## Create the pipeline
+
+Compile all three stage entry points:
+
+```csharp
+using Shader taskShader = App.LoadShader("MeshShading.slang", "ASMain");
+using Shader meshShader = App.LoadShader("MeshShading.slang", "MSMain");
+using Shader fragmentShader = App.LoadShader("MeshShading.slang", "FSMain");
+```
+
+Create a mesh-shading pipeline with the same color, depth, culling, and blend configuration used by the spinning cube:
+
+```csharp
+pipeline = App.Context.CreateMeshShadingPipeline(new()
+{
+ TaskShader = taskShader,
+ MeshShader = meshShader,
+ FragmentShader = fragmentShader,
+ PrimitiveTopology = PrimitiveTopology.TriangleList,
+ AttachmentFormats = new()
+ {
+ ColorFormats = [App.ColorFormat],
+ DepthStencilFormat = PixelFormat.D32FloatS8UInt,
+ SampleCount = SampleCount.Count1
+ },
+ RenderState = new()
+ {
+ Rasterizer = RasterizerState.CullBack(),
+ DepthStencil = DepthStencilState.DepthReadWrite(),
+ Blend = BlendState.Opaque()
+ }
+});
+```
+
+There is no `InputLayouts` field because fixed vertex fetch is not part of this pipeline. `TaskShader` is optional at the API level, but this workload uses it to generate mesh work dynamically.
+
+## Map instance IDs
+
+A linear ID maps into the $10 \times 10 \times 10$ grid:
+
+```slang
+void DecomposeInstanceID(uint id, out uint x, out uint y, out uint z)
+{
+ x = id % GridSize;
+ y = (id / GridSize) % GridSize;
+ z = id / (GridSize * GridSize);
+}
+```
+
+Helper functions convert those coordinates into world positions and colors. This arithmetic controls sample placement; it does not alter the task or mesh pipeline contract.
+
+## Cull and compact in the task shader
+
+The task stage declares 32 threads and two groupshared values:
+
+```slang
+groupshared Payload s_payload;
+groupshared uint s_visibleCount;
+
+[shader("task")]
+[numthreads(ASGroupSize, 1, 1)]
+void ASMain(uint groupID: SV_GroupID, uint groupThreadID: SV_GroupThreadID)
+```
+
+Each thread derives one candidate instance and tests its bounding sphere against all six frustum planes:
+
+```slang
+uint instanceIndex = groupID * ASGroupSize + groupThreadID;
+
+bool visible = false;
+if (instanceIndex < GridSize * GridSize * GridSize)
+{
+ float3 worldPos = InstancePosition(instanceIndex);
+ visible = !IsFrustumCulled(worldPos, BoundingSphereRadius);
+}
+```
+
+Thread zero resets the shared counter, then all threads synchronize:
+
+```slang
+if (groupThreadID == 0)
+{
+ s_visibleCount = 0;
+}
+
+GroupMemoryBarrierWithGroupSync();
+```
+
+Visible threads reserve unique payload slots with an atomic increment:
+
+```slang
+if (visible)
+{
+ uint offset;
+ InterlockedAdd(s_visibleCount, 1, offset);
+ s_payload.InstanceIndices[offset] = instanceIndex;
+}
+
+GroupMemoryBarrierWithGroupSync();
+
+DispatchMesh(s_visibleCount, 1, 1, s_payload);
+```
+
+The second barrier ensures all payload writes are visible before the task group launches mesh work. A task group dispatches between zero and 32 mesh groups, one for each compacted instance ID.
+
+## Emit geometry in the mesh shader
+
+The mesh shader receives the payload, selects one instance, and declares its output capacity:
+
+```slang
+[shader("mesh")]
+[numthreads(120, 1, 1)]
+[outputtopology("triangle")]
+void MSMain(uint groupID: SV_GroupID, uint groupThreadID: SV_GroupThreadID, in payload Payload meshPayload, OutputVertices outVertices, OutputIndices outIndices)
+{
+ uint instanceIndex = meshPayload.InstanceIndices[groupID];
+ float3 instancePos = InstancePosition(instanceIndex);
+ float3 color = InstanceColor(instanceIndex);
+
+ SetMeshOutputCounts(SphereVertexCount, SphereTriangleCount);
+```
+
+The group has 120 threads because the source sphere contains 120 triangles. Threads zero through 61 also transform and emit the 62 vertices:
+
+```slang
+if (groupThreadID < SphereVertexCount)
+{
+ Vertex v = constants.Vertices[groupThreadID];
+ float3 worldPos = v.Position + instancePos;
+
+ VertexOutput output;
+ output.Position = mul(float4(worldPos, 1.0), constants.ViewProjection);
+ output.WorldNormal = v.Normal;
+ output.Color = color;
+
+ outVertices[groupThreadID] = output;
+}
+
+if (groupThreadID < SphereTriangleCount)
+{
+ outIndices[groupThreadID] = constants.Triangles[groupThreadID].Indices;
+}
+```
+
+`SetMeshOutputCounts` establishes the valid output ranges before writes occur. The fragment shader receives the emitted normal and color, then applies a compact ambient-plus-diffuse lighting calculation.
+
+## Update frame data
+
+Each update moves the camera, creates the view-projection matrix, extracts frustum planes, and supplies the two storage handles:
+
+```csharp
+Constants constants = new()
+{
+ ViewProjection = viewProjection,
+ FrustumPlanes = new(viewProjection),
+ LightDirection = -Vector3.Normalize(cameraPosition),
+ Vertices = vertexBuffer.StorageReadOnlyHandle,
+ Triangles = triangleBuffer.StorageReadOnlyHandle
+};
+```
+
+The same matrix controls both emitted vertex positions and the planes used for culling, so visibility and rasterization share one camera definition.
+
+## Record the mesh dispatch
+
+Create the depth attachment on first use and transition it from `Undefined` to `DepthStencilAttachment`. Begin a color/depth render pass, bind the pipeline and constants, dispatch the task groups, and end the pass:
+
+```csharp
+if (depthTexture is null)
+{
+ depthTexture = App.Context.CreateTexture(TextureDesc.DepthStencilAttachment(PixelFormat.D32FloatS8UInt, App.Width, App.Height, SampleCount.Count1));
+
+ commandBuffer.Transition(depthTexture, default, TextureLayout.Undefined, TextureLayout.DepthStencilAttachment);
+}
+
+commandBuffer.BeginRenderPass([ColorAttachment.Clear(drawable, new(0.05f, 0.05f, 0.08f, 1.0f))], DepthStencilAttachment.Clear(depthTexture, 1.0f, 0));
+
+commandBuffer.SetPipeline(pipeline);
+commandBuffer.SetConstantBuffer(constantBuffer, 0);
+
+commandBuffer.DispatchMesh(32, 1, 1);
+
+commandBuffer.EndRenderPass();
+```
+
+There are 1,000 candidate instances and 32 task threads per group:
+
+$$
+\left\lceil \frac{1000}{32} \right\rceil = 32
+$$
+
+The bounds check discards candidate IDs at or above 1,000 in the final task group. Frustum testing then determines how many mesh groups each task group launches.
+
+## Resize and lifetime
+
+Only the depth attachment depends on framebuffer dimensions. Dispose it during `Resize` and let the next frame create a replacement. The source mesh, pipeline, and constant-buffer allocation remain valid.
+
+Release the optional depth texture, then the pipeline and its buffers:
+
+```csharp
+depthTexture?.Dispose();
+
+pipeline.Dispose();
+constantBuffer.Dispose();
+triangleBuffer.Dispose();
+vertexBuffer.Dispose();
+```
+
+## Inspect the result
+
+Run the host and select **Mesh Shading** on a supported device. Confirm that:
+
+- the camera moves around a colored sphere grid;
+- visible spheres remain complete as they approach the viewport edges;
+- resize preserves depth behavior and camera proportions;
+- validation reports no structured-stride, payload, output-count, or attachment errors.
+
+Missing spheres inside the view often indicate incorrect frustum-plane orientation or a missing groupshared barrier. Corrupted geometry points to a C#/Slang stride mismatch or output counts that disagree with the source mesh. A rejected pipeline usually indicates missing mesh-shading capability or mismatched shader stages.
+
+## Explore further
+
+1. Replace `IsFrustumCulled` with `false` and compare the number of generated mesh groups.
+2. Visualize visibility by assigning a constant color per task group.
+3. Change `InstanceSpacing` without modifying the source sphere buffers.
+4. Add a mesh entry point with no payload parameter that selects instance zero, create a pipeline with `TaskShader = null`, and dispatch one mesh group to isolate geometry emission.
+5. Replace the UV sphere with another mesh while keeping the vertex and triangle contracts unchanged.
+
+## Complete source
+
+- [MeshShadingRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Renderers/MeshShadingRenderer.cs)
+- [MeshShading.slang](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Assets/Shaders/MeshShading.slang)
+
+This completes the guide set. Use the [Zenith.NET documentation](../../docs/index.md) and [API reference](../../api/index.md) to inspect the underlying resource and command types in more detail.
diff --git a/documents/tutorials/guides/ray-tracing.md b/documents/tutorials/guides/ray-tracing.md
new file mode 100644
index 00000000..73d43a2a
--- /dev/null
+++ b/documents/tutorials/guides/ray-tracing.md
@@ -0,0 +1,441 @@
+# Ray Tracing
+
+Ray Tracing reuses the compute-output flow from Compute Shader, but each thread now traces a camera ray through an acceleration structure. The scene contains a triangle floor and three procedural spheres represented by axis-aligned bounding boxes.
+
+This guide explains the Zenith.NET acceleration structures, resource handles, inline ray-query interface, output texture, and synchronization. The complete shader also implements soft shadows, rough reflections, checkerboard filtering, and tone mapping; those supporting effects are summarized rather than derived line by line.
+
+## Result
+
+
+
+The camera orbits a checkerboard floor and three colored spheres. Primary rays determine the visible surface, while additional queries produce shadows and reflections.
+
+> [!IMPORTANT]
+> Inline ray tracing is optional. Check `App.Context.Capabilities.RayTracingSupported` before creating any acceleration-structure resources.
+
+## Frame overview
+
+Acceleration structures are built once during renderer construction. Every frame updates the camera, regenerates the output texture, and presents it:
+
+```mermaid
+flowchart LR
+ A[Update camera constants] --> B[Output to Storage]
+ B --> C[Bind ray-query compute pipeline]
+ C --> D[Dispatch one thread per pixel]
+ D --> E[Output to Sampled]
+ E --> F[Present texture]
+```
+
+The output is recreated after a framebuffer resize, but the scene geometry and acceleration structures remain valid.
+
+## Scene structure
+
+The acceleration structures form a two-level hierarchy:
+
+```mermaid
+flowchart TD
+ A[TLAS] --> B[Floor instance]
+ A --> C[Sphere instance]
+ B --> D[Triangle BLAS]
+ C --> E[AABB BLAS]
+ D --> F[Floor vertex and index buffers]
+ E --> G[AABB buffer]
+ H[Sphere structured buffer] --> I[Inline sphere-intersection logic in CSMain]
+```
+
+- A bottom-level acceleration structure, or **BLAS**, describes geometry.
+- A top-level acceleration structure, or **TLAS**, instances one or more BLAS objects into the traced scene.
+- Triangle geometry can be intersected directly by traversal hardware.
+- An AABB narrows traversal to a candidate region; the shader must test and commit the exact procedural intersection.
+
+The sphere structured buffer is not part of the AABB geometry description. The shader reads it separately to recover each candidate's center, radius, and color.
+
+## Resource map
+
+| Resource | Usage | Role |
+| --- | --- | --- |
+| Floor vertex buffer | `StorageReadOnly` | Triangle BLAS positions |
+| Floor index buffer | `StorageReadOnly` | Triangle BLAS indices |
+| AABB buffer | `StorageReadOnly` | Procedural BLAS bounds |
+| Sphere buffer | `StorageReadOnly` | Exact intersection and material data |
+| Floor BLAS | Acceleration structure | Triangle geometry |
+| Sphere BLAS | Acceleration structure | Procedural AABB geometry |
+| TLAS | Acceleration structure | Scene instances supplied to ray queries |
+| Constant buffer | `Constant` | Camera and three resource handles |
+| Output texture | `Storage` and `Sampled` | Compute output and presenter input |
+| Compute pipeline | `CSMain` | Primary and secondary inline queries |
+
+Build-input buffers must remain alive until the BLAS build or update submission completes. The BLAS objects themselves must remain alive for as long as a TLAS that instances them is in use. The sphere buffer has an additional lifetime requirement because the shader reads it during every frame.
+
+## Check device support
+
+Fail before allocating workload resources when the selected device does not expose ray tracing:
+
+```csharp
+if (!App.Context.Capabilities.RayTracingSupported)
+{
+ throw new PlatformNotSupportedException("Ray Tracing is not supported by the selected device.");
+}
+```
+
+Check the capability before creating the shaders and acceleration structures required by this workload.
+
+## Create scene geometry
+
+The floor uses four positions and six indices. Three `Sphere` values provide procedural geometry and shading data. Convert each sphere into an AABB:
+
+```csharp
+Aabb[] aabbs = new Aabb[spheres.Length];
+for (int index = 0; index < spheres.Length; index++)
+{
+ aabbs[index] = new(spheres[index].Center - new Vector3(spheres[index].Radius), spheres[index].Center + new Vector3(spheres[index].Radius));
+}
+```
+
+Upload all four arrays:
+
+```csharp
+floorVertexBuffer = App.LoadBuffer(floorVertices, BufferUsages.StorageReadOnly);
+floorIndexBuffer = App.LoadBuffer(floorIndices, BufferUsages.StorageReadOnly);
+aabbBuffer = App.LoadBuffer(aabbs, BufferUsages.StorageReadOnly);
+sphereBuffer = App.LoadBuffer(spheres, BufferUsages.StorageReadOnly);
+constantBuffer = App.LoadBuffer([new Constants()], BufferUsages.Constant);
+```
+
+These buffers are read during acceleration-structure construction or shader execution. They are not vertex and index bindings in a graphics render pass.
+
+## Create the compute pipeline
+
+Load the `CSMain` entry point and create the compute pipeline used for every inline ray-query dispatch:
+
+```csharp
+using Shader computeShader = App.LoadShader("RayTracing.slang", "CSMain");
+
+rayTracingPipeline = App.Context.CreateComputePipeline(new() { ComputeShader = computeShader });
+```
+
+The acceleration structures and descriptor handles become inputs to this pipeline; no separate ray-tracing pipeline or intersection-shader stage is created.
+
+## Build the bottom-level structures
+
+Record acceleration-structure construction on a compute command buffer:
+
+```csharp
+CommandBuffer commandBuffer = App.Context.ComputeQueue.CommandBuffer();
+```
+
+The sample submits this one-time build and waits for it to complete before rendering begins.
+
+Describe the indexed triangle geometry for the floor:
+
+```csharp
+floorBlas = commandBuffer.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc()
+{
+ Geometries =
+ [
+ RayTracingGeometry.Triangles(new()
+ {
+ VertexBuffer = floorVertexBuffer,
+ VertexFormat = PixelFormat.R32G32B32Float,
+ VertexCount = (uint)floorVertices.Length,
+ VertexStrideInBytes = (uint)sizeof(Vector3),
+ IndexBuffer = floorIndexBuffer,
+ IndexFormat = IndexFormat.UInt32,
+ IndexCount = (uint)floorIndices.Length,
+ Transform = Matrix4x4.Identity
+ }, true)
+ ],
+ BuildFlags = AccelerationStructureBuildFlags.PreferFastTrace
+});
+```
+
+The vertex format and stride tell the builder how to read each `Vector3`. The final `true` marks the geometry opaque, allowing traversal to accept triangle hits without an any-hit stage.
+
+The procedural BLAS instead reads an array of AABBs:
+
+```csharp
+sphereBlas = commandBuffer.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc()
+{
+ Geometries =
+ [
+ RayTracingGeometry.Aabbs(new()
+ {
+ Buffer = aabbBuffer,
+ Count = (uint)spheres.Length,
+ StrideInBytes = aabbBuffer.Desc.StrideInBytes
+ }, true)
+ ],
+ BuildFlags = AccelerationStructureBuildFlags.PreferFastTrace
+});
+```
+
+The AABBs accelerate traversal, but they do not turn boxes into visible surfaces. `CSMain` later performs the exact sphere intersection for each procedural candidate.
+
+## Build the top-level scene
+
+Create one identity-transform instance for each BLAS:
+
+```csharp
+tlas = commandBuffer.BuildAccelerationStructure(new TopLevelAccelerationStructureDesc()
+{
+ Instances =
+ [
+ new()
+ {
+ AccelerationStructure = floorBlas,
+ InstanceId = 0,
+ VisibilityMask = 0xFF,
+ Transform = Matrix4x4.Identity
+ },
+ new()
+ {
+ AccelerationStructure = sphereBlas,
+ InstanceId = 1,
+ VisibilityMask = 0xFF,
+ Transform = Matrix4x4.Identity
+ }
+ ],
+ BuildFlags = AccelerationStructureBuildFlags.PreferFastTrace
+});
+
+commandBuffer.Submit().Wait();
+```
+
+The CPU wait ensures both BLAS objects and the TLAS are complete before construction returns. The instance IDs distinguish scene entries; procedural primitive indices still identify individual AABBs inside the sphere BLAS.
+
+## Data contract
+
+Shared C#/Slang layout requires care around three-component vectors. The CPU sphere packs center and radius into the first 16 bytes, followed by color at byte 16:
+
+```csharp
+[StructLayout(LayoutKind.Explicit, Size = 32)]
+file struct Sphere
+{
+ [FieldOffset(0)]
+ public Vector3 Center;
+
+ [FieldOffset(12)]
+ public float Radius;
+
+ [FieldOffset(16)]
+ public Vector3 Color;
+}
+```
+
+Slang uses two `float4` backing fields and exposes semantic properties:
+
+```slang
+struct Sphere
+{
+ private float4 CenterAndRadius;
+
+ private float4 ColorAndPadding;
+
+ property float3 Center
+ {
+ get {
+ return CenterAndRadius.xyz;
+ }
+ }
+
+ property float Radius
+ {
+ get {
+ return CenterAndRadius.w;
+ }
+ }
+
+ property float3 Color
+ {
+ get {
+ return ColorAndPadding.xyz;
+ }
+ }
+};
+```
+
+This explicit storage keeps the structured-buffer stride at 32 bytes across the supported graphics APIs.
+
+The frame constants use the same technique for camera position, followed by three handles:
+
+| Offset | C# | Slang |
+| ---: | --- | --- |
+| 0 | `Vector3 Position` | `PositionAndPadding.xyz` |
+| 16 | `tlas.Handle` | `RaytracingAccelerationStructure Scene` |
+| 24 | `sphereBuffer.StorageReadOnlyHandle` | `StructuredBuffer Spheres` |
+| 32 | `outputTexture.StorageHandle` | `RWTexture2D OutputTexture` |
+
+## Inline ray-query interface
+
+Each compute thread reconstructs one camera ray and begins traversal through the TLAS:
+
+```slang
+RayDesc ray;
+ray.Origin = cameraPos;
+ray.Direction = rayDir;
+ray.TMin = RayEpsilon;
+ray.TMax = 1000.0;
+
+float3 sphereHitNormal = float3(0.0);
+float3 sphereHitColor = float3(0.0);
+
+RayQuery query;
+query.TraceRayInline(constants.Scene, RAY_FLAG_NONE, 0xFF, ray);
+```
+
+The two temporary values preserve the normal and material color produced by a procedural sphere candidate after that candidate is committed.
+
+Triangle intersections can become committed hits automatically. Procedural AABBs are reported as candidates and require an exact test:
+
+```slang
+while (query.Proceed())
+{
+ if (query.CandidateType() == CANDIDATE_PROCEDURAL_PRIMITIVE)
+ {
+ uint sphereIndex = query.CandidatePrimitiveIndex();
+ Sphere sphere = constants.Spheres[sphereIndex];
+
+ float3 ro = query.CandidateObjectRayOrigin();
+ float3 rd = query.CandidateObjectRayDirection();
+
+ float t = IntersectSphere(ro, rd, sphere);
+
+ if (t >= query.RayTMin() && t <= query.CommittedRayT())
+ {
+ float3 hitPoint = ro + rd * t;
+
+ sphereHitNormal = normalize(hitPoint - sphere.Center);
+ sphereHitColor = sphere.Color;
+
+ query.CommitProceduralPrimitiveHit(t);
+ }
+ }
+}
+```
+
+`CandidatePrimitiveIndex` maps the AABB candidate back to its sphere record. The shader computes a positive intersection distance and commits it only when it lies inside the query's valid interval.
+
+After traversal, committed status selects the visible surface:
+
+```slang
+if (query.CommittedStatus() == COMMITTED_TRIANGLE_HIT)
+{
+ float3 hitPoint = ray.Origin + ray.Direction * query.CommittedRayT();
+ color = ShadeFloor(hitPoint, rayDir, cameraPos);
+}
+else if (query.CommittedStatus() == COMMITTED_PROCEDURAL_PRIMITIVE_HIT)
+{
+ float3 hitPoint = ray.Origin + ray.Direction * query.CommittedRayT();
+ color = ShadePrimarySphere(hitPoint, rayDir, cameraPos, sphereHitNormal, sphereHitColor);
+}
+else
+{
+ color = SampleSky(rayDir);
+}
+```
+
+## Supporting shading flow
+
+The remaining shader functions improve the sample image but do not change the Zenith.NET resource interface:
+
+```mermaid
+flowchart TD
+ A[CSMain primary query] --> B[Shade floor]
+ A --> C[Shade sphere]
+ A --> D[Sample sky]
+ B --> E[Shadow queries]
+ B --> F[Reflection query]
+ C --> E
+ C --> G[Rough reflection queries]
+ B --> H[ACES tone mapping]
+ C --> H
+ D --> H
+```
+
+Shadow rays reuse the TLAS with `RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH`. Reflection rays use the same procedural-candidate loop as the primary query. Multiple jittered directions soften shadows and reflections. Finally, `ACESFilm` maps the accumulated HDR color into the output range.
+
+You can replace these helpers with simpler colors while preserving the acceleration structures, descriptors, pipeline, dispatch, and texture transitions described by this guide.
+
+## Create and update the output
+
+Create an `R32G32B32A32Float` texture with `Sampled | Storage` usage when the renderer first sees a valid framebuffer size. A new texture is initially established as sampled because every subsequent frame follows the same repeating cycle:
+
+```text
+Sampled -> Storage -> compute writes -> Sampled -> presenter reads
+```
+
+Update the camera and resource handles before dispatch:
+
+```csharp
+Constants constants = new()
+{
+ Position = new(12.0f * MathF.Sin(totalTime * 0.3f), 4.0f + MathF.Sin(totalTime * 0.2f), -12.0f * MathF.Cos(totalTime * 0.3f)),
+ Scene = tlas.Handle,
+ Spheres = sphereBuffer.StorageReadOnlyHandle,
+ OutputTexture = outputTexture.StorageHandle
+};
+```
+
+Then record the state changes and dispatch:
+
+```csharp
+commandBuffer.Transition(outputTexture, default, TextureLayout.Sampled, TextureLayout.Storage);
+
+commandBuffer.SetPipeline(rayTracingPipeline);
+commandBuffer.SetConstantBuffer(constantBuffer, 0);
+commandBuffer.Dispatch((App.Width + ThreadGroupSize - 1) / ThreadGroupSize, (App.Height + ThreadGroupSize - 1) / ThreadGroupSize, 1);
+
+commandBuffer.Transition(outputTexture, default, TextureLayout.Storage, TextureLayout.Sampled);
+
+App.PresentTexture(commandBuffer, drawable, outputTexture, true);
+```
+
+Unlike the static grayscale image, this workload dispatches every frame because the camera moves. Passing `true` to the presenter fits the output to the framebuffer.
+
+## Resize and lifetime
+
+Resize disposes only the framebuffer-sized output texture. The next frame recreates it and uploads its new storage handle.
+
+Dispose resources from users to dependencies:
+
+```csharp
+outputTexture?.Dispose();
+
+rayTracingPipeline.Dispose();
+constantBuffer.Dispose();
+tlas.Dispose();
+sphereBlas.Dispose();
+floorBlas.Dispose();
+sphereBuffer.Dispose();
+aabbBuffer.Dispose();
+floorIndexBuffer.Dispose();
+floorVertexBuffer.Dispose();
+```
+
+The TLAS is released before the BLAS objects it instances. The sphere buffer remains live because ray-query shading reads it every frame.
+
+## Inspect the result
+
+Run the host and select **Ray Tracing** on a supported device. Confirm that:
+
+- the floor, three spheres, sky, shadows, and reflections are visible;
+- the camera orbits continuously;
+- resize recreates an output that fills the new framebuffer;
+- validation reports no acceleration-structure lifetime, descriptor, or texture-layout errors.
+
+A sky-only image often indicates a missing TLAS handle or visibility-mask mismatch. Box-shaped procedural hits indicate that AABBs are being treated as bounds without committing the exact sphere intersection. A stale or blank image usually points to the output handle or `Sampled`/`Storage` transition sequence.
+
+## Explore further
+
+1. Return a constant color for each committed status to inspect traversal without the supporting shading functions.
+2. Set `ShadowSamples` and `ReflectionSamples` to `1` and compare cost and image quality.
+3. Disable reflections while leaving primary and shadow queries unchanged.
+4. Move one TLAS instance with its transform instead of changing the source geometry.
+5. Change a sphere radius and update both its structured-buffer record and AABB.
+
+## Complete source
+
+- [RayTracingRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Renderers/RayTracingRenderer.cs)
+- [RayTracing.slang](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Assets/Shaders/RayTracing.slang)
+
+Continue with [Mesh Shading](mesh-shading.md) to explore another optional workload that replaces fixed vertex and index fetch with programmable geometry emission.
diff --git a/documents/tutorials/guides/spinning-cube.md b/documents/tutorials/guides/spinning-cube.md
new file mode 100644
index 00000000..a9c41c9e
--- /dev/null
+++ b/documents/tutorials/guides/spinning-cube.md
@@ -0,0 +1,265 @@
+# Spinning Cube
+
+Spinning Cube extends the first graphics workload into three dimensions. It reuses vertex input and a graphics pipeline, then adds indexed geometry, model-view-projection constants, back-face culling, a depth attachment, animation, and size-dependent resource handling.
+
+The guide focuses on the Zenith.NET objects and their lifetime. Matrix construction uses `System.Numerics`; a full derivation of perspective projection is outside the workload boundary.
+
+## Result
+
+
+
+The cube rotates around two axes. Back faces are culled, and the depth attachment ensures that nearer fragments hide farther surfaces.
+
+## Frame overview
+
+Initialization uploads eight shared vertices and 36 indices, creates a constant buffer, and builds a depth-enabled pipeline. Per frame:
+
+```mermaid
+flowchart LR
+ A[Update MVP constants] --> B{Depth texture exists?}
+ B -- No --> C[Create and transition depth texture]
+ B -- Yes --> D[Begin color and depth render pass]
+ C --> D
+ D --> E[Bind pipeline and buffers]
+ E --> F[DrawIndexed 36]
+ F --> G[End render pass]
+```
+
+On resize, the renderer disposes only the depth texture. The next frame recreates it with the new framebuffer dimensions.
+
+## Resource map
+
+| Resource | Usage | Lifetime |
+| --- | --- | --- |
+| Vertex buffer | Eight cube positions and colors | Renderer |
+| Index buffer | Twelve triangles sharing those vertices | Renderer |
+| Constant buffer | Model, view, and projection matrices | Renderer; updated each frame |
+| Graphics pipeline | Depth-tested indexed drawing | Renderer |
+| Depth texture | `D32FloatS8UInt` attachment | Recreated after resize |
+| Swap-chain drawable | Color attachment | Host-owned |
+
+The depth format appears in both the pipeline attachment declaration and the texture description. Those values must agree.
+
+## Geometry and indexed drawing
+
+Eight vertices describe the unique corners of the cube. The index buffer selects them to build six faces, two triangles per face:
+
+```csharp
+uint[] indices =
+[
+ 0, 1, 2, 0, 2, 3,
+ 5, 4, 7, 5, 7, 6,
+ 4, 0, 3, 4, 3, 7,
+ 1, 5, 6, 1, 6, 2,
+ 3, 2, 6, 3, 6, 7,
+ 4, 5, 1, 4, 1, 0
+];
+
+vertexBuffer = App.LoadBuffer(vertices, BufferUsages.Vertex);
+indexBuffer = App.LoadBuffer(indices, BufferUsages.Index);
+```
+
+Using indices avoids duplicating a complete position-and-color record for every triangle corner. The element type is `uint`, so the render method binds it as `IndexFormat.UInt32`.
+
+## Data contract
+
+The Slang constant buffer contains three consecutive matrices:
+
+```slang
+struct Constants
+{
+ float4x4 Model;
+
+ float4x4 View;
+
+ float4x4 Projection;
+};
+
+ConstantBuffer constants;
+```
+
+The C# structure fixes each 64-byte matrix at the corresponding offset:
+
+```csharp
+[StructLayout(LayoutKind.Explicit, Size = 192)]
+file struct Constants
+{
+ [FieldOffset(0)]
+ public Matrix4x4 Model;
+
+ [FieldOffset(64)]
+ public Matrix4x4 View;
+
+ [FieldOffset(128)]
+ public Matrix4x4 Projection;
+}
+```
+
+| Offset | C# | Slang | Size |
+| ---: | --- | --- | ---: |
+| 0 | `Matrix4x4 Model` | `float4x4 Model` | 64 bytes |
+| 64 | `Matrix4x4 View` | `float4x4 View` | 64 bytes |
+| 128 | `Matrix4x4 Projection` | `float4x4 Projection` | 64 bytes |
+
+Explicit offsets keep the shared binary contract visible. The total structure size is 192 bytes.
+
+## Transform vertices
+
+The vertex shader composes the three matrices and transforms each position:
+
+```slang
+[shader("vertex")]
+FSInput VSMain(VSInput input)
+{
+ float4x4 modelView = mul(constants.Model, constants.View);
+ float4x4 mvp = mul(modelView, constants.Projection);
+
+ FSInput output;
+ output.Position = mul(float4(input.Position, 1.0), mvp);
+ output.Color = input.Color;
+
+ return output;
+}
+```
+
+`System.Numerics.Matrix4x4` and this shader use the same row-vector order: model, then view, then projection. The fragment shader simply returns the interpolated vertex color.
+
+## Enable depth and culling
+
+The graphics pipeline adds a depth format and enables both back-face culling and depth read/write:
+
+```csharp
+pipeline = App.Context.CreateGraphicsPipeline(new()
+{
+ VertexShader = vertexShader,
+ FragmentShader = fragmentShader,
+ InputLayouts = [inputLayout],
+ PrimitiveTopology = PrimitiveTopology.TriangleList,
+ AttachmentFormats = new()
+ {
+ ColorFormats = [App.ColorFormat],
+ DepthStencilFormat = PixelFormat.D32FloatS8UInt,
+ SampleCount = SampleCount.Count1
+ },
+ RenderState = new()
+ {
+ Rasterizer = RasterizerState.CullBack(),
+ DepthStencil = DepthStencilState.DepthReadWrite(),
+ Blend = BlendState.Opaque()
+ }
+});
+```
+
+Back-face culling removes triangles whose winding faces away from the camera. Depth testing compares overlapping fragments, while depth writes preserve the nearest accepted value for later triangles.
+
+## Update transformation constants
+
+`Update` advances a frame-rate-independent angle and constructs the three matrices:
+
+```csharp
+rotationAngle += (float)deltaTime;
+
+Constants constants = new()
+{
+ Model = Matrix4x4.CreateRotationY(rotationAngle) * Matrix4x4.CreateRotationX(rotationAngle * 0.5f),
+ View = Matrix4x4.CreateLookAt(new(0.0f, 0.0f, 3.0f), Vector3.Zero, Vector3.UnitY),
+ Projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)App.Width / App.Height, 0.1f, 100.0f)
+};
+```
+
+The framebuffer aspect ratio keeps the projection proportional after a resize. Upload exactly one `Constants` value:
+
+```csharp
+constantBuffer.Upload(0, new()
+{
+ Pointer = (nint)(&constants),
+ SizeInBytes = (uint)sizeof(Constants)
+});
+```
+
+The teaching host waits for each submitted frame, so overwriting this single constant buffer does not race a previous frame. A multi-frame application needs separate per-frame storage or equivalent synchronization.
+
+## Create the depth attachment
+
+The depth texture is created lazily after the window has a valid framebuffer size:
+
+```csharp
+if (depthTexture is null)
+{
+ depthTexture = App.Context.CreateTexture(TextureDesc.DepthStencilAttachment(PixelFormat.D32FloatS8UInt, App.Width, App.Height, SampleCount.Count1));
+
+ commandBuffer.Transition(depthTexture, default, TextureLayout.Undefined, TextureLayout.DepthStencilAttachment);
+}
+```
+
+The first transition establishes `DepthStencilAttachment` layout. The texture remains in that layout while reused by later frames.
+
+## Record the frame
+
+Begin a render pass with both color and depth attachments, bind all inputs, and issue an indexed draw:
+
+```csharp
+commandBuffer.BeginRenderPass([ColorAttachment.Clear(drawable, new(0.04f, 0.055f, 0.075f, 1.0f))], DepthStencilAttachment.Clear(depthTexture, 1.0f, 0));
+
+commandBuffer.SetPipeline(pipeline);
+commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0);
+commandBuffer.SetIndexBuffer(indexBuffer, 0, IndexFormat.UInt32);
+commandBuffer.SetConstantBuffer(constantBuffer, 0);
+
+commandBuffer.DrawIndexed(36, 1, 0, 0, 0);
+
+commandBuffer.EndRenderPass();
+```
+
+Depth is cleared to `1.0` for each frame. `DrawIndexed` consumes all 36 indices for one instance, starting at index and vertex offsets zero.
+
+## Resize and lifetime
+
+The depth attachment dimensions must match the drawable. Dispose it on resize and recreate it during the next render:
+
+```csharp
+public void Resize(uint width, uint height)
+{
+ depthTexture?.Dispose();
+ depthTexture = null;
+}
+```
+
+Release the optional depth texture first, then the pipeline and its input resources:
+
+```csharp
+public void Dispose()
+{
+ depthTexture?.Dispose();
+
+ pipeline.Dispose();
+ constantBuffer.Dispose();
+ indexBuffer.Dispose();
+ vertexBuffer.Dispose();
+}
+```
+
+## Inspect the result
+
+Run the host and select **Spinning Cube**. Confirm that:
+
+- the cube rotates smoothly and hidden surfaces do not bleed through;
+- resizing changes the projection without stretching the cube;
+- the depth texture is recreated after resize;
+- the validation layer reports no attachment-format or layout errors.
+
+If faces disappear unexpectedly, inspect index winding and culling. If distant faces draw over near faces, confirm that the pipeline enables depth read/write and that a depth attachment is supplied to the render pass.
+
+## Explore further
+
+1. Replace `CullBack` with `CullNone` and compare the result.
+2. Disable depth testing while leaving the depth attachment present.
+3. Change the near and far projection planes and observe depth precision at different distances.
+4. Give each face its own vertices and colors to see the tradeoff between shared indices and per-face attributes.
+
+## Complete source
+
+- [SpinningCubeRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Renderers/SpinningCubeRenderer.cs)
+- [SpinningCube.slang](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Assets/Shaders/SpinningCube.slang)
+
+Continue with [Indirect Drawing](indirect-drawing.md) to reuse this depth-tested cube for 25 instances driven by one indirect argument record, or branch to [Compute Shader](compute-shader.md) to produce an image without rasterized geometry.
diff --git a/documents/tutorials/index.md b/documents/tutorials/index.md
index 77947dd4..9d3b2af6 100644
--- a/documents/tutorials/index.md
+++ b/documents/tutorials/index.md
@@ -1,75 +1,36 @@
# Tutorials
-Welcome to the Zenith.NET tutorials! These step-by-step guides will help you learn how to use Zenith.NET for GPU programming.
+The Zenith.NET tutorials examine complete rendering workloads built with C# and the Slang shader language. Each guide focuses on the resources, pipelines, commands, data contracts, and lifetime rules that connect a workload to Zenith.NET.
-## Getting Started
+Begin with Project Setup and Application Host. The six guides then move from a first graphics pipeline to compute, indirect drawing, inline ray queries, and programmable geometry.
-New to Zenith.NET? Start here to set up your environment and render your first graphics.
-
-| Tutorial | Description |
-|----------|-------------|
-| [Prerequisites](getting-started/prerequisites.md) | Set up your development environment with `App` framework, `IRenderer` interface, and `BindingHelper` |
-| [Hello Triangle](getting-started/hello-triangle.md) | Create vertex buffers, compile Slang shaders, and build your first graphics pipeline |
-| [Textured Quad](getting-started/textured-quad.md) | Load textures, create samplers, and bind resources with `ResourceLayout` and `ResourceTable` |
-| [Spinning Cube](getting-started/spinning-cube.md) | Use constant buffers for MVP matrices and render 3D geometry with depth testing |
-
-## Intermediate
-
-Build on the basics with GPU compute and advanced rendering techniques.
-
-| Tutorial | Description |
-|----------|-------------|
-| [Compute Shader](intermediate/compute-shader.md) | Create compute pipelines for GPU image processing (grayscale conversion) |
-| [Indirect Drawing](intermediate/indirect-drawing.md) | GPU-driven rendering with `DrawIndexedIndirect` for multi-instance drawing |
-
-## Advanced
-
-Explore cutting-edge GPU features for modern rendering (requires hardware support).
-
-| Tutorial | Description | Requirement |
-|----------|-------------|-------------|
-| [Ray Tracing](advanced/ray-tracing.md) | Build acceleration structures (BLAS/TLAS), use `RayQuery` for ray tracing with soft shadows, reflections, and ACES tonemapping | `RayTracingSupported` |
-| [Mesh Shading](advanced/mesh-shading.md) | Render 1,000 sphere instances with amplification shader frustum culling and mesh shading pipeline | `MeshShadingSupported` |
-
-## Tutorial Structure
-
-Each tutorial follows a consistent pattern:
-
-1. **Overview** - What you'll build and the key concepts covered
-2. **Key Concepts** (advanced tutorials) - In-depth explanation of new API features
-3. **Renderer Class** - Complete, runnable implementation code
-4. **Running the Tutorial** - How to switch renderers and run the example
-5. **Result** - Screenshot of the expected output
-6. **Code Breakdown** - Step-by-step explanation of important code sections
+> [!NOTE]
+> The guides explain the Zenith.NET integration points in detail. Supporting graphics techniques such as sampling patterns, tone mapping, and procedural mesh generation are summarized when they are not essential to the API workflow. Their complete implementations remain available in the tutorial source.
-All tutorials share the same `App` framework. Run `dotnet run` and select a tutorial from the interactive menu in `Program.cs`.
+## Getting started
-## Learning Path
+- [Project Setup](getting-started/project-setup.md) creates the .NET 10 project and configures runtime assets.
+- [Application Host](getting-started/application-host.md) follows the cross-platform window, swap chain, frame loop, and offscreen texture presenter.
-| Stage | You Will Learn |
-|-------|----------------|
-| **Prerequisites** | Set up the application framework, graphics context, and cross-platform resource binding |
-| **Hello Triangle** | Create GPU buffers, compile shaders, configure graphics pipelines, and submit draw commands |
-| **Textured Quad** | Load and sample textures, use index buffers, and bind shader resources |
-| **Spinning Cube** | Pass data to shaders via constant buffers, implement 3D transformations, and enable depth testing |
-| **Compute Shader** | Run general-purpose GPU computations for image processing |
-| **Indirect Drawing** | Let the GPU control draw parameters for efficient multi-instance rendering |
-| **Ray Tracing** | Build acceleration structures, trace rays with `RayQuery`, implement soft shadows, reflections, Fresnel, and ACES tonemapping |
-| **Mesh Shading** | Use amplification shaders for GPU-driven frustum culling with mesh shading at scale (1,000 instances) |
+## Guides
-## Requirements
+| Guide | Result | Focus |
+| --- | --- | --- |
+| [Hello Triangle](guides/hello-triangle.md) | A vertex-colored triangle | Vertex input, graphics pipeline, render pass, and draw command |
+| [Spinning Cube](guides/spinning-cube.md) | A rotating depth-tested cube | Indexed geometry, constant buffers, transforms, depth, and resize |
+| [Compute Shader](guides/compute-shader.md) | A grayscale image | Storage textures, descriptor handles, dispatch, and layout transitions |
+| [Indirect Drawing](guides/indirect-drawing.md) | A grid of animated cubes | Structured instance data and indirect draw arguments |
+| [Ray Tracing](guides/ray-tracing.md) | Procedural spheres above a floor | Acceleration structures, inline ray queries, and compute output |
+| [Mesh Shading](guides/mesh-shading.md) | A culled sphere grid | Task and mesh shaders, payload compaction, and mesh dispatch |
-Before starting, ensure you have:
+The complete, runnable implementations are maintained in the [ZenithTutorials repository](https://github.com/qian-o/ZenithTutorials). Code shown in these pages is static so it remains readable and searchable with the rest of the documentation; each guide links its complete renderer and shader at the end.
-- .NET 10.0 SDK or later
-- A GPU with DirectX 12, Metal 4, or Vulkan 1.4 support
-- Visual Studio 2026, VS Code, or JetBrains Rider
+## Suggested paths
-> [!NOTE]
-> These tutorials are designed for desktop platforms (Windows, macOS, and Linux).
-> See [Prerequisites](getting-started/prerequisites.md) for detailed platform support and setup instructions.
+Complete [Hello Triangle](guides/hello-triangle.md) first. From there:
-## Source Code
+- continue with [Spinning Cube](guides/spinning-cube.md) and [Indirect Drawing](guides/indirect-drawing.md) for the graphics path;
+- continue with [Compute Shader](guides/compute-shader.md) and [Ray Tracing](guides/ray-tracing.md) for compute-produced images;
+- read [Mesh Shading](guides/mesh-shading.md) after Spinning Cube when you are comfortable with graphics pipelines, depth attachments, and GPU thread groups.
-> [!TIP]
-> The complete source code for all tutorials is available on GitHub: [ZenithTutorials](https://github.com/qian-o/ZenithTutorials)
+Ray tracing and mesh shading are optional device capabilities. Their guides show how to test support before creating workload-specific resources.
diff --git a/documents/tutorials/intermediate/compute-shader.md b/documents/tutorials/intermediate/compute-shader.md
deleted file mode 100644
index 8d3be520..00000000
--- a/documents/tutorials/intermediate/compute-shader.md
+++ /dev/null
@@ -1,280 +0,0 @@
-# Compute Shader
-
-In this tutorial, you'll use a compute pipeline to process an image on the GPU — converting it from color to grayscale. This introduces compute shaders, read/write textures, and dispatching work groups.
-
-## Overview
-
-This tutorial covers:
-
-- Creating a **compute pipeline** with thread group configuration
-- Using `Texture2D` (read-only) and `RWTexture2D` (read-write) resources
-- **Dispatching** compute work groups based on texture dimensions
-- Performing **linearize → grayscale → gamma** color conversion
-- Copying the processed texture to the frame buffer with centered placement
-
-## The Renderer Class
-
-Create the file `Renderers/ComputeShaderRenderer.cs`:
-
-```csharp
-namespace ZenithTutorials.Renderers;
-
-internal class ComputeShaderRenderer : IRenderer
-{
- private const uint ThreadGroupSize = 16;
-
- private const string ShaderSource = """
- Texture2D inputTexture;
- RWTexture2D outputTexture;
-
- [numthreads(16, 16, 1)]
- void CSMain(uint3 dispatchThreadID: SV_DispatchThreadID)
- {
- uint width, height;
- outputTexture.GetDimensions(width, height);
-
- if (dispatchThreadID.x >= width || dispatchThreadID.y >= height)
- {
- return;
- }
-
- float4 color = inputTexture[dispatchThreadID.xy];
-
- float3 linear = pow(color.rgb, 2.2);
- float gray = dot(linear, float3(0.2126, 0.7152, 0.0722));
- gray = pow(gray, 1.0 / 2.2);
-
- outputTexture[dispatchThreadID.xy] = float4(gray, gray, gray, color.a);
- }
- """;
-
- private readonly Texture inputTexture;
- private readonly Texture outputTexture;
- private readonly ResourceLayout resourceLayout;
- private readonly ResourceTable resourceTable;
- private readonly ComputePipeline pipeline;
-
- private bool processed;
-
- public ComputeShaderRenderer()
- {
- inputTexture = App.Context.LoadTextureFromFile(Path.Combine(AppContext.BaseDirectory, "Assets", "shoko.png"), generateMipMaps: false);
-
- outputTexture = App.Context.CreateTexture(new()
- {
- Type = TextureType.Texture2D,
- Format = PixelFormat.B8G8R8A8UNorm,
- Width = inputTexture.Desc.Width,
- Height = inputTexture.Desc.Height,
- Depth = 1,
- MipLevels = 1,
- ArrayLayers = 1,
- SampleCount = SampleCount.Count1,
- Flags = TextureUsageFlags.ShaderResource | TextureUsageFlags.UnorderedAccess
- });
-
- resourceLayout = App.Context.CreateResourceLayout(new()
- {
- Bindings = BindingHelper.Bindings
- (
- new() { Type = ResourceType.Texture, Count = 1, StageFlags = ShaderStageFlags.Compute },
- new() { Type = ResourceType.TextureReadWrite, Count = 1, StageFlags = ShaderStageFlags.Compute }
- )
- });
-
- resourceTable = App.Context.CreateResourceTable(new()
- {
- Layout = resourceLayout,
- Resources = [inputTexture, outputTexture]
- });
-
- using Shader computeShader = App.Context.LoadShaderFromSource(ShaderSource, "CSMain", ShaderStageFlags.Compute);
-
- pipeline = App.Context.CreateComputePipeline(new()
- {
- Compute = computeShader,
- ResourceLayout = resourceLayout,
- ThreadGroupSizeX = ThreadGroupSize,
- ThreadGroupSizeY = ThreadGroupSize,
- ThreadGroupSizeZ = 1
- });
- }
-
- public void Update(double deltaTime)
- {
- }
-
- public void Render()
- {
- CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer();
-
- if (!processed)
- {
- uint dispatchX = (inputTexture.Desc.Width + ThreadGroupSize - 1) / ThreadGroupSize;
- uint dispatchY = (inputTexture.Desc.Height + ThreadGroupSize - 1) / ThreadGroupSize;
-
- commandBuffer.SetPipeline(pipeline);
- commandBuffer.SetResourceTable(resourceTable);
- commandBuffer.Dispatch(dispatchX, dispatchY, 1);
-
- processed = true;
- }
-
- Texture colorTarget = App.FrameBuffer.Desc.ColorAttachments[0].Target;
-
- uint copyWidth = Math.Min(outputTexture.Desc.Width, App.Width);
- uint copyHeight = Math.Min(outputTexture.Desc.Height, App.Height);
-
- uint srcX = (outputTexture.Desc.Width - copyWidth) / 2;
- uint srcY = (outputTexture.Desc.Height - copyHeight) / 2;
- uint destX = (App.Width - copyWidth) / 2;
- uint destY = (App.Height - copyHeight) / 2;
-
- commandBuffer.CopyTexture(outputTexture,
- default,
- new() { X = srcX, Y = srcY, Z = 0 },
- colorTarget,
- default,
- new() { X = destX, Y = destY, Z = 0 },
- new() { Width = copyWidth, Height = copyHeight, Depth = 1 });
-
- commandBuffer.Submit(waitForCompletion: true);
- }
-
- public void Resize(uint width, uint height)
- {
- }
-
- public void Dispose()
- {
- pipeline.Dispose();
- resourceTable.Dispose();
- resourceLayout.Dispose();
- outputTexture.Dispose();
- inputTexture.Dispose();
- }
-}
-```
-
-## Running the Tutorial
-
-Run the application and select **4. Compute Shader** from the menu:
-
-```bash
-dotnet run
-```
-
-## Result
-
-
-
-## Code Breakdown
-
-### Shader
-
-The compute shader processes each pixel independently in 16×16 thread groups:
-
-```csharp
-private const string ShaderSource = """
- Texture2D inputTexture;
- RWTexture2D outputTexture;
-
- [numthreads(16, 16, 1)]
- void CSMain(uint3 dispatchThreadID: SV_DispatchThreadID)
- {
- uint width, height;
- outputTexture.GetDimensions(width, height);
-
- if (dispatchThreadID.x >= width || dispatchThreadID.y >= height)
- {
- return;
- }
-
- float4 color = inputTexture[dispatchThreadID.xy];
-
- float3 linear = pow(color.rgb, 2.2);
- float gray = dot(linear, float3(0.2126, 0.7152, 0.0722));
- gray = pow(gray, 1.0 / 2.2);
-
- outputTexture[dispatchThreadID.xy] = float4(gray, gray, gray, color.a);
- }
- """;
-```
-
-The grayscale conversion follows three steps:
-
-1. **Linearize**: `pow(color.rgb, 2.2)` removes sRGB gamma
-2. **Luminance**: `dot(linear, float3(0.2126, 0.7152, 0.0722))` computes perceptual brightness using Rec. 709 coefficients
-3. **Re-encode**: `pow(gray, 1.0 / 2.2)` applies gamma correction
-
-### Compute Pipeline
-
-Unlike the graphics pipeline, a compute pipeline has no vertex/pixel stages or render states:
-
-```csharp
-pipeline = App.Context.CreateComputePipeline(new()
-{
- Compute = computeShader,
- ResourceLayout = resourceLayout,
- ThreadGroupSizeX = ThreadGroupSize,
- ThreadGroupSizeY = ThreadGroupSize,
- ThreadGroupSizeZ = 1
-});
-```
-
-The thread group size (16×16×1) defines how many threads run per group. This must match the `[numthreads]` attribute in the shader.
-
-### Output Texture
-
-The output texture is created with `UnorderedAccess` to allow compute shader writes:
-
-```csharp
-outputTexture = App.Context.CreateTexture(new()
-{
- Type = TextureType.Texture2D,
- Format = PixelFormat.B8G8R8A8UNorm,
- Width = inputTexture.Desc.Width,
- Height = inputTexture.Desc.Height,
- Depth = 1,
- MipLevels = 1,
- ArrayLayers = 1,
- SampleCount = SampleCount.Count1,
- Flags = TextureUsageFlags.ShaderResource | TextureUsageFlags.UnorderedAccess
-});
-```
-
-| Flag | Purpose |
-|------|---------|
-| `ShaderResource` | Can be read as `Texture2D` in shaders |
-| `UnorderedAccess` | Can be written as `RWTexture2D` in compute shaders |
-
-### Dispatch and Copy
-
-The compute shader runs once, then the result is copied centered to the frame buffer each frame:
-
-```csharp
-if (!processed)
-{
- uint dispatchX = (inputTexture.Desc.Width + ThreadGroupSize - 1) / ThreadGroupSize;
- uint dispatchY = (inputTexture.Desc.Height + ThreadGroupSize - 1) / ThreadGroupSize;
-
- commandBuffer.SetPipeline(pipeline);
- commandBuffer.SetResourceTable(resourceTable);
- commandBuffer.Dispatch(dispatchX, dispatchY, 1);
-
- processed = true;
-}
-```
-
-The dispatch count is computed as `ceil(dimension / threadGroupSize)` to ensure all pixels are covered.
-
-The `CopyTexture` call copies the result centered within the swap chain's color target, handling cases where the image and window have different sizes.
-
-## Next Steps
-
-- [Indirect Drawing](indirect-drawing.md) - Draw multiple instances with GPU-driven indirect commands
-
-## Source Code
-
-> [!TIP]
-> View the complete source code on GitHub: [ComputeShaderRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Renderers/ComputeShaderRenderer.cs)
diff --git a/documents/tutorials/intermediate/indirect-drawing.md b/documents/tutorials/intermediate/indirect-drawing.md
deleted file mode 100644
index f2f2380c..00000000
--- a/documents/tutorials/intermediate/indirect-drawing.md
+++ /dev/null
@@ -1,433 +0,0 @@
-# Indirect Drawing
-
-In this tutorial, you'll render a 5×5 grid of spinning cubes using indirect drawing and GPU instancing. This introduces indirect draw buffers, structured buffers for per-instance data, and shows how to drive draw calls from GPU-accessible memory.
-
-## Overview
-
-This tutorial covers:
-
-- Creating an **indirect draw buffer** with `IndirectDrawIndexedArgs`
-- Using a **structured buffer** to store per-instance transforms and colors
-- Updating instance data per-frame for independent animations
-- Issuing a single `DrawIndexedIndirect` call to render all instances
-- Setting up view/projection matrices in `Resize` for window-independent rendering
-
-## The Renderer Class
-
-Create the file `Renderers/IndirectDrawingRenderer.cs`:
-
-```csharp
-namespace ZenithTutorials.Renderers;
-
-internal unsafe class IndirectDrawingRenderer : IRenderer
-{
- private const int InstanceCount = 25;
-
- private const string ShaderSource = """
- struct VSInput
- {
- float3 Position : POSITION0;
-
- float4 Color : COLOR0;
-
- uint InstanceID : SV_InstanceID;
- };
-
- struct PSInput
- {
- float4 Position : SV_POSITION;
-
- float4 Color : COLOR;
- };
-
- struct Constants
- {
- float4x4 View;
-
- float4x4 Projection;
- };
-
- struct Instance
- {
- float4x4 Model;
-
- float4 Color;
- };
-
- ConstantBuffer constants;
- StructuredBuffer instances;
-
- PSInput VSMain(VSInput input)
- {
- Instance instance = instances[input.InstanceID];
-
- float4 worldPos = mul(float4(input.Position, 1.0), instance.Model);
- float4 viewPos = mul(worldPos, constants.View);
-
- PSInput output;
- output.Position = mul(viewPos, constants.Projection);
- output.Color = input.Color * instance.Color;
-
- return output;
- }
-
- float4 PSMain(PSInput input) : SV_TARGET
- {
- return input.Color;
- }
- """;
-
- private readonly Buffer vertexBuffer;
- private readonly Buffer indexBuffer;
- private readonly Buffer indirectBuffer;
- private readonly Buffer constantsBuffer;
- private readonly Buffer instanceBuffer;
- private readonly ResourceLayout resourceLayout;
- private readonly ResourceTable resourceTable;
- private readonly GraphicsPipeline pipeline;
-
- private float rotationAngle;
-
- public IndirectDrawingRenderer()
- {
- Vertex[] vertices =
- [
- new(new(-0.5f, -0.5f, 0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)),
- new(new( 0.5f, -0.5f, 0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)),
- new(new( 0.5f, 0.5f, 0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)),
- new(new(-0.5f, 0.5f, 0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)),
- new(new(-0.5f, -0.5f, -0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)),
- new(new( 0.5f, -0.5f, -0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)),
- new(new( 0.5f, 0.5f, -0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)),
- new(new(-0.5f, 0.5f, -0.5f), new(1.0f, 1.0f, 1.0f, 1.0f))
- ];
-
- uint[] indices =
- [
- 0, 1, 2, 0, 2, 3,
- 5, 4, 7, 5, 7, 6,
- 4, 0, 3, 4, 3, 7,
- 1, 5, 6, 1, 6, 2,
- 3, 2, 6, 3, 6, 7,
- 4, 5, 1, 4, 1, 0
- ];
-
- vertexBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(Vertex) * vertices.Length),
- StrideInBytes = (uint)sizeof(Vertex),
- Flags = BufferUsageFlags.Vertex | BufferUsageFlags.MapWrite
- });
- vertexBuffer.Upload(vertices, 0);
-
- indexBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(uint) * indices.Length),
- StrideInBytes = sizeof(uint),
- Flags = BufferUsageFlags.Index | BufferUsageFlags.MapWrite
- });
- indexBuffer.Upload(indices, 0);
-
- indirectBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)sizeof(IndirectDrawIndexedArgs),
- StrideInBytes = (uint)sizeof(IndirectDrawIndexedArgs),
- Flags = BufferUsageFlags.Indirect | BufferUsageFlags.MapWrite
- });
-
- indirectBuffer.Upload([new IndirectDrawIndexedArgs()
- {
- IndexCount = (uint)indices.Length,
- InstanceCount = InstanceCount,
- FirstIndex = 0,
- VertexOffset = 0,
- FirstInstance = 0
- }], 0);
-
- constantsBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)sizeof(Constants),
- StrideInBytes = (uint)sizeof(Constants),
- Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite
- });
- Resize(App.Width, App.Height);
-
- instanceBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(Instance) * InstanceCount),
- StrideInBytes = (uint)sizeof(Instance),
- Flags = BufferUsageFlags.ShaderResource | BufferUsageFlags.MapWrite
- });
-
- resourceLayout = App.Context.CreateResourceLayout(new()
- {
- Bindings = BindingHelper.Bindings
- (
- new() { Type = ResourceType.ConstantBuffer, Count = 1, StageFlags = ShaderStageFlags.Vertex },
- new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Vertex }
- )
- });
-
- resourceTable = App.Context.CreateResourceTable(new()
- {
- Layout = resourceLayout,
- Resources = [constantsBuffer, instanceBuffer]
- });
-
- InputLayout inputLayout = new();
- inputLayout.Add(new() { Format = ElementFormat.Float3, Semantic = ElementSemantic.Position });
- inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Color });
-
- using Shader vertexShader = App.Context.LoadShaderFromSource(ShaderSource, "VSMain", ShaderStageFlags.Vertex);
- using Shader pixelShader = App.Context.LoadShaderFromSource(ShaderSource, "PSMain", ShaderStageFlags.Pixel);
-
- pipeline = App.Context.CreateGraphicsPipeline(new()
- {
- RenderStates = new()
- {
- RasterizerState = RasterizerStates.CullBack,
- DepthStencilState = DepthStencilStates.Default,
- BlendState = BlendStates.Opaque
- },
- Vertex = vertexShader,
- Pixel = pixelShader,
- ResourceLayout = resourceLayout,
- InputLayouts = [inputLayout],
- PrimitiveTopology = PrimitiveTopology.TriangleList,
- Output = App.FrameBuffer.Output
- });
- }
-
- public void Update(double deltaTime)
- {
- rotationAngle += (float)deltaTime;
-
- Instance[] instances = new Instance[InstanceCount];
-
- int index = 0;
- int gridSize = (int)Math.Sqrt(InstanceCount);
-
- for (int y = 0; y < gridSize; y++)
- {
- for (int x = 0; x < gridSize; x++)
- {
- float offsetX = (x - (gridSize / 2)) * 1.5f;
- float offsetY = (y - (gridSize / 2)) * 1.5f;
- float rotation = rotationAngle * (1.0f + (index * 0.1f));
-
- instances[index] = new()
- {
- Model = Matrix4x4.CreateScale(0.4f)
- * Matrix4x4.CreateRotationY(rotation)
- * Matrix4x4.CreateRotationX(rotation * 0.5f)
- * Matrix4x4.CreateTranslation(offsetX, offsetY, 0),
- Color = new((float)x / gridSize, (float)y / gridSize, 1.0f - ((float)x / gridSize), 1.0f)
- };
-
- index++;
- }
- }
-
- instanceBuffer.Upload(instances, 0);
- }
-
- public void Render()
- {
- CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer();
-
- commandBuffer.BeginRenderPass(App.FrameBuffer, new()
- {
- ColorValues = [new(0.1f, 0.1f, 0.15f, 1.0f)],
- Depth = 1.0f,
- Stencil = 0,
- Flags = ClearFlags.All
- }, resourceTable);
-
- commandBuffer.SetPipeline(pipeline);
- commandBuffer.SetResourceTable(resourceTable);
- commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0);
- commandBuffer.SetIndexBuffer(indexBuffer, 0, IndexFormat.UInt32);
- commandBuffer.DrawIndexedIndirect(indirectBuffer, 0, 1);
-
- commandBuffer.EndRenderPass();
-
- commandBuffer.Submit(waitForCompletion: true);
- }
-
- public void Resize(uint width, uint height)
- {
- Matrix4x4 view = Matrix4x4.CreateLookAt(new(0, 0, 8), Vector3.Zero, Vector3.UnitY);
- Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)width / height, 0.1f, 100.0f);
-
- constantsBuffer.Upload([new Constants() { View = view, Projection = projection }], 0);
- }
-
- public void Dispose()
- {
- pipeline.Dispose();
- resourceTable.Dispose();
- resourceLayout.Dispose();
- instanceBuffer.Dispose();
- constantsBuffer.Dispose();
- indirectBuffer.Dispose();
- indexBuffer.Dispose();
- vertexBuffer.Dispose();
- }
-}
-
-[StructLayout(LayoutKind.Sequential)]
-file struct Vertex(Vector3 position, Vector4 color)
-{
- public Vector3 Position = position;
-
- public Vector4 Color = color;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 128)]
-file struct Constants
-{
- [FieldOffset(0)]
- public Matrix4x4 View;
-
- [FieldOffset(64)]
- public Matrix4x4 Projection;
-}
-
-[StructLayout(LayoutKind.Explicit, Size = 80)]
-file struct Instance
-{
- [FieldOffset(0)]
- public Matrix4x4 Model;
-
- [FieldOffset(64)]
- public Vector4 Color;
-}
-```
-
-## Running the Tutorial
-
-Run the application and select **5. Indirect Drawing** from the menu:
-
-```bash
-dotnet run
-```
-
-## Result
-
-
-
-## Code Breakdown
-
-### Shader
-
-The vertex shader reads per-instance data from a `StructuredBuffer`:
-
-```csharp
-ConstantBuffer constants;
-StructuredBuffer instances;
-
-PSInput VSMain(VSInput input)
-{
- Instance instance = instances[input.InstanceID];
-
- float4 worldPos = mul(float4(input.Position, 1.0), instance.Model);
- float4 viewPos = mul(worldPos, constants.View);
-
- PSInput output;
- output.Position = mul(viewPos, constants.Projection);
- output.Color = input.Color * instance.Color;
-
- return output;
-}
-```
-
-`SV_InstanceID` provides the instance index, used to look up the per-instance model matrix and color from the structured buffer.
-
-### Indirect Draw Buffer
-
-The draw arguments are stored in a GPU buffer instead of being passed as CPU parameters:
-
-```csharp
-indirectBuffer = App.Context.CreateBuffer(new()
-{
- SizeInBytes = (uint)sizeof(IndirectDrawIndexedArgs),
- StrideInBytes = (uint)sizeof(IndirectDrawIndexedArgs),
- Flags = BufferUsageFlags.Indirect | BufferUsageFlags.MapWrite
-});
-
-indirectBuffer.Upload([new IndirectDrawIndexedArgs()
-{
- IndexCount = (uint)indices.Length,
- InstanceCount = InstanceCount,
- FirstIndex = 0,
- VertexOffset = 0,
- FirstInstance = 0
-}], 0);
-```
-
-`IndirectDrawIndexedArgs` mirrors the standard GPU indirect draw structure. Using `DrawIndexedIndirect` instead of `DrawIndexed` allows the GPU to read draw parameters from a buffer, enabling GPU-driven rendering scenarios.
-
-### Structured Buffer
-
-Per-instance data (model matrix + color) is uploaded to a structured buffer each frame:
-
-```csharp
-instanceBuffer = App.Context.CreateBuffer(new()
-{
- SizeInBytes = (uint)(sizeof(Instance) * InstanceCount),
- StrideInBytes = (uint)sizeof(Instance),
- Flags = BufferUsageFlags.ShaderResource | BufferUsageFlags.MapWrite
-});
-```
-
-The `Instance` struct is 80 bytes — a 64-byte `Matrix4x4` plus a 16-byte `Vector4`:
-
-```csharp
-[StructLayout(LayoutKind.Explicit, Size = 80)]
-file struct Instance
-{
- [FieldOffset(0)]
- public Matrix4x4 Model;
-
- [FieldOffset(64)]
- public Vector4 Color;
-}
-```
-
-### Per-Instance Animation
-
-Each cube gets a unique rotation speed and color based on its grid position:
-
-```csharp
-instances[index] = new()
-{
- Model = Matrix4x4.CreateScale(0.4f)
- * Matrix4x4.CreateRotationY(rotation)
- * Matrix4x4.CreateRotationX(rotation * 0.5f)
- * Matrix4x4.CreateTranslation(offsetX, offsetY, 0),
- Color = new((float)x / gridSize, (float)y / gridSize, 1.0f - ((float)x / gridSize), 1.0f)
-};
-```
-
-### View/Projection in Resize
-
-View and projection matrices are set in `Resize` rather than `Update`, since the camera is static and only the aspect ratio changes:
-
-```csharp
-public void Resize(uint width, uint height)
-{
- Matrix4x4 view = Matrix4x4.CreateLookAt(new(0, 0, 8), Vector3.Zero, Vector3.UnitY);
- Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)width / height, 0.1f, 100.0f);
-
- constantsBuffer.Upload([new Constants() { View = view, Projection = projection }], 0);
-}
-```
-
-## Next Steps
-
-- [Ray Tracing](../advanced/ray-tracing.md) - Cast rays with hardware acceleration structures
-
-## Source Code
-
-> [!TIP]
-> View the complete source code on GitHub: [IndirectDrawingRenderer.cs](https://github.com/qian-o/ZenithTutorials/blob/master/ZenithTutorials/Renderers/IndirectDrawingRenderer.cs)
diff --git a/documents/tutorials/toc.yml b/documents/tutorials/toc.yml
index 38fb6a6a..0616191a 100644
--- a/documents/tutorials/toc.yml
+++ b/documents/tutorials/toc.yml
@@ -1,24 +1,24 @@
- name: Tutorials
href: index.md
+
- name: Getting Started
items:
- - name: Prerequisites
- href: getting-started/prerequisites.md
+ - name: Project Setup
+ href: getting-started/project-setup.md
+ - name: Application Host
+ href: getting-started/application-host.md
+
+- name: Guides
+ items:
- name: Hello Triangle
- href: getting-started/hello-triangle.md
- - name: Textured Quad
- href: getting-started/textured-quad.md
+ href: guides/hello-triangle.md
- name: Spinning Cube
- href: getting-started/spinning-cube.md
-- name: Intermediate
- items:
+ href: guides/spinning-cube.md
- name: Compute Shader
- href: intermediate/compute-shader.md
+ href: guides/compute-shader.md
- name: Indirect Drawing
- href: intermediate/indirect-drawing.md
-- name: Advanced
- items:
+ href: guides/indirect-drawing.md
- name: Ray Tracing
- href: advanced/ray-tracing.md
+ href: guides/ray-tracing.md
- name: Mesh Shading
- href: advanced/mesh-shading.md
+ href: guides/mesh-shading.md
diff --git a/sources/Directory.Packages.props b/sources/Directory.Packages.props
index 8a77753e..44203098 100644
--- a/sources/Directory.Packages.props
+++ b/sources/Directory.Packages.props
@@ -5,23 +5,23 @@
-
+
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/sources/Experiments/CornellBox/App.cs b/sources/Experiments/CornellBox/App.cs
index 8646e411..2097c79d 100644
--- a/sources/Experiments/CornellBox/App.cs
+++ b/sources/Experiments/CornellBox/App.cs
@@ -45,7 +45,7 @@ static App()
Context = GraphicsContext.CreateVulkan(useValidationLayer: true);
}
- Context.ValidationMessage += static (sender, args) => Console.WriteLine($"[{args.Source} - {args.Severity}] {args.Message}");
+ Context.ValidationMessage += static (sender, args) => Console.WriteLine($"[{args.Severity}] {args.Message}");
window = Window.Create(WindowOptions.Default with
{
@@ -72,9 +72,19 @@ static App()
surface = Surface.Xlib(window.Native!.X11!.Value.Display, (nint)window.Native.X11.Value.Window, Width, Height);
}
- swapChain = Context.CreateSwapChain(new() { Surface = surface, ColorTargetFormat = PixelFormat.B8G8R8A8UNorm, DepthStencilTargetFormat = PixelFormat.D32FloatS8UInt });
- imGui = new(input, swapChain.FrameBuffer.Output);
- camera = new(input, Matrix4x4.CreateTranslation(278f, 273f, -800f))
+ swapChain = Context.CreateSwapChain(new()
+ {
+ Surface = surface,
+ Format = PixelFormat.B8G8R8A8UNorm
+ });
+
+ imGui = new(input, new()
+ {
+ ColorFormats = [PixelFormat.B8G8R8A8UNorm],
+ SampleCount = SampleCount.Count1
+ });
+
+ camera = new(input, Matrix4x4.CreateTranslation(278.0f, 273.0f, -800.0f))
{
Speed = 240.0f,
FarPlane = 2000.0f
@@ -117,52 +127,45 @@ public static void Run()
imGui.Update(delta, width, height);
camera.Update(delta, width, height);
- // ImGui
- {
- ImGui.GetBackgroundDrawList().AddImage(imGui.Binding(activeRenderer.Color), new(0, 0), new(Width / DpiScale.X, Height / DpiScale.Y));
-
- ImGui.SetNextWindowPos(new(10, 10), ImGuiCond.FirstUseEver);
- if (ImGui.Begin("Cornell Box", ImGuiWindowFlags.AlwaysAutoResize))
- {
- ImGui.Text($"Backend: {Context.Backend}");
- ImGui.Text(Context.Capabilities.DeviceName);
+ ImGui.GetBackgroundDrawList().AddImage(imGui.Binding(activeRenderer.Color), new(0, 0), new(Width / DpiScale.X, Height / DpiScale.Y));
- ImGui.Separator();
+ ImGuiHelper.Overlay(() =>
+ {
+ ImGui.Text(Context.Capabilities.DeviceName);
+ ImGui.Text($"GraphicsApi: {Context.GraphicsApi}");
+ ImGui.Text($"FPS: {ImGui.GetIO().Framerate:F1}");
+ });
- ImGui.Text("Render Mode:");
+ ImGuiHelper.Settings(() =>
+ {
+ ImGui.Text("Render Mode:");
- if (Context.Capabilities.RayTracingSupported)
+ if (Context.Capabilities.RayTracingSupported)
+ {
+ if (ImGui.RadioButton("Path Tracing", currentMode is 0) && currentMode is not 0)
{
- if (ImGui.RadioButton("Path Tracing", currentMode is 0) && currentMode is not 0)
- {
- pathTracer!.FrameCount = 0;
-
- currentMode = 0;
- activeRenderer = pathTracer;
- }
+ currentMode = 0;
+ activeRenderer = pathTracer!;
- ImGui.SameLine();
+ pathTracer!.FrameCount = 0;
}
- if (ImGui.RadioButton("Rasterization", currentMode is 1) && currentMode is not 1)
- {
- currentMode = 1;
- activeRenderer = rasterizer;
- }
+ ImGui.SameLine();
+ }
- ImGui.Separator();
+ if (ImGui.RadioButton("Rasterization", currentMode is 1) && currentMode is not 1)
+ {
+ currentMode = 1;
+ activeRenderer = rasterizer;
+ }
- if (currentMode is 0 && pathTracer is not null)
- {
- ImGui.Text($"SPP: {pathTracer.FrameCount}");
- }
+ ImGui.Separator();
- ImGui.Text($"FPS: {ImGui.GetIO().Framerate:F1}");
+ if (currentMode is 0 && pathTracer is not null)
+ {
+ ImGui.Text($"SPP: {pathTracer.FrameCount}");
}
- ImGui.End();
- }
-
- activeRenderer.Update(camera);
+ });
};
window.Render += _ =>
@@ -172,13 +175,18 @@ public static void Run()
return;
}
- CommandBuffer commandBuffer = Context.Graphics.CommandBuffer();
+ CommandBuffer commandBuffer = Context.GraphicsQueue.CommandBuffer();
+ activeRenderer.Update(camera);
activeRenderer.Render(commandBuffer);
- imGui.Render(commandBuffer, swapChain.FrameBuffer, ClearValues.Default);
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
- commandBuffer.Submit(true);
+ imGui.Render(commandBuffer, ColorAttachment.DontCare(swapChain.Drawable));
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.ColorAttachment, TextureLayout.Present);
+
+ commandBuffer.Submit().Wait();
swapChain.Present();
};
@@ -205,7 +213,5 @@ public static void Run()
window.Dispose();
Context.Dispose();
-
- Console.WriteLine("Exited cleanly.");
}
}
diff --git a/sources/Experiments/CornellBox/Assets/Shaders/.clang-format b/sources/Experiments/CornellBox/Assets/Shaders/.clang-format
index c6996ed2..ff17eaf4 100644
--- a/sources/Experiments/CornellBox/Assets/Shaders/.clang-format
+++ b/sources/Experiments/CornellBox/Assets/Shaders/.clang-format
@@ -4,13 +4,14 @@ BasedOnStyle: Microsoft
# Indentation
IndentWidth: 4
+ContinuationIndentWidth: 4
TabWidth: 4
UseTab: Never
IndentCaseLabels: true
-AccessModifierOffset: -4
+NamespaceIndentation: All
-# Braces - Allman style
-BreakBeforeBraces: Allman
+# Brace placement
+BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: true
AfterClass: true
@@ -19,64 +20,27 @@ BraceWrapping:
AfterFunction: true
AfterNamespace: true
AfterStruct: true
- AfterUnion: true
BeforeCatch: true
BeforeElse: true
+ BeforeWhile: false
IndentBraces: false
+ SplitEmptyFunction: true
+ SplitEmptyNamespace: true
+ SplitEmptyRecord: true
-# Column limit
-ColumnLimit: 120
-
-# Line breaking
-BreakBeforeBinaryOperators: None
-BreakBeforeTernaryOperators: false
-AlwaysBreakAfterReturnType: None
-
-# Alignment
-AlignAfterOpenBracket: Align
-AlignConsecutiveAssignments: None
-AlignConsecutiveDeclarations: None
-AlignOperands: Align
-AlignTrailingComments: false
-
-# Function arguments and parameters - one per line when wrapped
-BinPackArguments: false
-BinPackParameters: true
-AllowAllArgumentsOnNextLine: false
-AllowAllParametersOfDeclarationOnNextLine: false
-
-# Short statements
+# Block bodies
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
-# Spaces
-SpaceAfterCStyleCast: false
-SpaceBeforeAssignmentOperators: true
-SpaceBeforeParens: ControlStatements
-SpaceInEmptyParentheses: false
-SpacesBeforeTrailingComments: 1
-SpacesInAngles: false
-SpacesInParentheses: false
-SpacesInSquareBrackets: false
-
-# Empty lines
+# Line wrapping
+ColumnLimit: 0
KeepEmptyLinesAtTheStartOfBlocks: false
MaxEmptyLinesToKeep: 1
-
-# Other
-NamespaceIndentation: All
-PointerAlignment: Left
-SortIncludes: false
-SortUsingDeclarations: false
ReflowComments: false
-# Penalty settings
-PenaltyBreakAssignment: 1000000
-PenaltyBreakBeforeFirstCallParameter: 500
-PenaltyBreakComment: 300
-PenaltyBreakString: 1000
-PenaltyExcessCharacter: 1
-PenaltyReturnTypeOnItsOwnLine: 1000000
\ No newline at end of file
+# Declaration order
+SortIncludes: false
+SortUsingDeclarations: false
\ No newline at end of file
diff --git a/sources/Experiments/CornellBox/Assets/Shaders/PathTracing.slang b/sources/Experiments/CornellBox/Assets/Shaders/PathTracing.slang
index c171acc2..4ea5ad30 100644
--- a/sources/Experiments/CornellBox/Assets/Shaders/PathTracing.slang
+++ b/sources/Experiments/CornellBox/Assets/Shaders/PathTracing.slang
@@ -8,7 +8,7 @@ struct Vertex
{
private float4 PositionAndPadding;
- private float4 NormalAndMaterialID;
+ private float4 NormalAndMaterialId;
property float3 Position
{
@@ -20,14 +20,14 @@ struct Vertex
property float3 Normal
{
get {
- return NormalAndMaterialID.xyz;
+ return NormalAndMaterialId.xyz;
}
}
- property uint MaterialID
+ property uint MaterialId
{
get {
- return asuint(NormalAndMaterialID.w);
+ return asuint(NormalAndMaterialId.w);
}
}
};
@@ -36,13 +36,7 @@ struct Material
{
private float4 AlbedoAndEmission;
- float Metallic;
-
- float Roughness;
-
- private float padding0;
-
- private float padding1;
+ private float4 MetallicRoughnessAndPadding;
property float3 Albedo
{
@@ -57,9 +51,23 @@ struct Material
return AlbedoAndEmission.w;
}
}
+
+ property float Metallic
+ {
+ get {
+ return MetallicRoughnessAndPadding.x;
+ }
+ }
+
+ property float Roughness
+ {
+ get {
+ return MetallicRoughnessAndPadding.y;
+ }
+ }
};
-struct CameraParams
+struct PathTracingConstants
{
float4x4 InvView;
@@ -67,13 +75,19 @@ struct CameraParams
private float4 PositionAndPadding;
- uint FrameCount;
+ private uint4 FrameCountWidthHeightAndPadding;
+
+ DescriptorHandle Scene;
- uint Width;
+ DescriptorHandle> Vertices;
- uint Height;
+ DescriptorHandle> Indices;
- private float padding0;
+ DescriptorHandle> Materials;
+
+ DescriptorHandle> AccumulationTexture;
+
+ DescriptorHandle> OutputTexture;
property float3 Position
{
@@ -81,15 +95,30 @@ struct CameraParams
return PositionAndPadding.xyz;
}
}
+
+ property uint FrameCount
+ {
+ get {
+ return FrameCountWidthHeightAndPadding.x;
+ }
+ }
+
+ property uint Width
+ {
+ get {
+ return FrameCountWidthHeightAndPadding.y;
+ }
+ }
+
+ property uint Height
+ {
+ get {
+ return FrameCountWidthHeightAndPadding.z;
+ }
+ }
};
-RaytracingAccelerationStructure scene;
-ConstantBuffer camera;
-StructuredBuffer vertices;
-StructuredBuffer indices;
-StructuredBuffer materials;
-RWTexture2D accumTexture;
-RWTexture2D outputTexture;
+ConstantBuffer pathTracing;
float DistributionGGX(float NdotH, float roughness)
{
@@ -118,7 +147,7 @@ float3 FresnelSchlick(float cosTheta, float3 F0)
return F0 + (1.0 - F0) * pow(saturate(1.0 - cosTheta), 5.0);
}
-float3 evaluateBRDF(float3 N, float3 V, float3 L, Material mat)
+float3 EvaluateBRDF(float3 N, float3 V, float3 L, Material mat)
{
float roughness = max(mat.Roughness, 0.04);
float NdotL = max(dot(N, L), 0.0);
@@ -141,14 +170,14 @@ float3 evaluateBRDF(float3 N, float3 V, float3 L, Material mat)
return diffuse + specular;
}
-float powerHeuristic(float pdfA, float pdfB)
+float PowerHeuristic(float pdfA, float pdfB)
{
float a2 = pdfA * pdfA;
return a2 / (a2 + pdfB * pdfB + 0.0001);
}
-float computeBrdfPdf(float3 N, float3 V, float3 L, float roughness, float specProb)
+float ComputeBRDFPDF(float3 N, float3 V, float3 L, float roughness, float specProb)
{
float NdotL = max(dot(N, L), 0.0);
if (NdotL <= 0.0)
@@ -171,12 +200,27 @@ float computeBrdfPdf(float3 N, float3 V, float3 L, float roughness, float specPr
return specProb * pdfSpec + (1.0 - specProb) * pdfDiff;
}
-float3 sampleGGXHalfVector(float3 N, float roughness, inout uint seed)
+uint PCGHash(uint input)
+{
+ uint state = input * 747796405u + 2891336453u;
+ uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
+
+ return (word >> 22u) ^ word;
+}
+
+float RandomFloat(inout uint seed)
+{
+ seed = PCGHash(seed);
+
+ return float(seed) / 4294967295.0;
+}
+
+float3 SampleGGXHalfVector(float3 N, float roughness, inout uint seed)
{
float a = roughness * roughness;
- float r1 = randomFloat(seed);
- float r2 = randomFloat(seed);
+ float r1 = RandomFloat(seed);
+ float r2 = RandomFloat(seed);
float phi = 2.0 * PI * r1;
float cosTheta = sqrt((1.0 - r2) / (1.0 + (a * a - 1.0) * r2));
@@ -190,25 +234,10 @@ float3 sampleGGXHalfVector(float3 N, float roughness, inout uint seed)
return normalize(u * cos(phi) * sinTheta + v * sin(phi) * sinTheta + w * cosTheta);
}
-uint pcgHash(uint input)
-{
- uint state = input * 747796405u + 2891336453u;
- uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
-
- return (word >> 22u) ^ word;
-}
-
-float randomFloat(inout uint seed)
-{
- seed = pcgHash(seed);
-
- return float(seed) / 4294967295.0;
-}
-
-float3 cosineSampleHemisphere(float3 normal, inout uint seed)
+float3 CosineSampleHemisphere(float3 normal, inout uint seed)
{
- float r1 = randomFloat(seed);
- float r2 = randomFloat(seed);
+ float r1 = RandomFloat(seed);
+ float r2 = RandomFloat(seed);
float phi = 2.0 * PI * r1;
float sinTheta = sqrt(r2);
@@ -222,7 +251,7 @@ float3 cosineSampleHemisphere(float3 normal, inout uint seed)
return normalize(u * cos(phi) * sinTheta + v * sin(phi) * sinTheta + w * cosTheta);
}
-bool traceShadowRay(float3 origin, float3 direction, float maxDist)
+bool TraceShadowRay(float3 origin, float3 direction, float maxDist)
{
RayDesc shadowRay;
shadowRay.Origin = origin;
@@ -236,7 +265,7 @@ bool traceShadowRay(float3 origin, float3 direction, float maxDist)
}
RayQuery shadowQuery;
- shadowQuery.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, shadowRay);
+ shadowQuery.TraceRayInline(pathTracing.Scene, RAY_FLAG_NONE, 0xFF, shadowRay);
while (shadowQuery.Proceed())
{
@@ -245,11 +274,11 @@ bool traceShadowRay(float3 origin, float3 direction, float maxDist)
return shadowQuery.CommittedStatus() != COMMITTED_NOTHING;
}
-float3 sampleLightDirect(float3 hitPos, float3 hitNormal, float3 geoNormal, float3 V, Material mat, float roughness,
+float3 SampleLightDirect(float3 hitPos, float3 hitNormal, float3 geoNormal, float3 V, Material mat, float roughness,
float specProb, inout uint rng)
{
- float r1 = randomFloat(rng);
- float r2 = randomFloat(rng);
+ float r1 = RandomFloat(rng);
+ float r2 = RandomFloat(rng);
float3 lightPoint = float3(lerp(LightMin.x, LightMax.x, r1), LightMin.y, lerp(LightMin.z, LightMax.z, r2));
@@ -270,24 +299,24 @@ float3 sampleLightDirect(float3 hitPos, float3 hitNormal, float3 geoNormal, floa
}
float3 shadowOrigin = dot(geoNormal, L) > 0.0 ? hitPos + geoNormal * 0.001 : hitPos - geoNormal * 0.001;
- if (traceShadowRay(shadowOrigin, L, dist))
+ if (TraceShadowRay(shadowOrigin, L, dist))
{
return float3(0.0, 0.0, 0.0);
}
- Material lightMat = materials[3];
+ Material lightMat = pathTracing.Materials[3];
float3 lightEmission = lightMat.Albedo * lightMat.Emission;
float pdfLight = (dist * dist) / (lightCosine * LightArea);
- float pdfBrdf = computeBrdfPdf(hitNormal, V, L, roughness, specProb);
- float misWeight = powerHeuristic(pdfLight, pdfBrdf);
+ float pdfBrdf = ComputeBRDFPDF(hitNormal, V, L, roughness, specProb);
+ float misWeight = PowerHeuristic(pdfLight, pdfBrdf);
- float3 brdf = evaluateBRDF(hitNormal, V, L, mat);
+ float3 brdf = EvaluateBRDF(hitNormal, V, L, mat);
return lightEmission * brdf * NdotL * misWeight / pdfLight;
}
-float3 tracePath(float3 origin, float3 direction, inout uint rng)
+float3 TracePath(float3 origin, float3 direction, inout uint rng)
{
float3 throughput = float3(1.0, 1.0, 1.0);
float3 radiance = float3(0.0, 0.0, 0.0);
@@ -301,7 +330,7 @@ float3 tracePath(float3 origin, float3 direction, inout uint rng)
ray.TMax = 100000.0;
RayQuery query;
- query.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, ray);
+ query.TraceRayInline(pathTracing.Scene, RAY_FLAG_NONE, 0xFF, ray);
while (query.Proceed())
{
@@ -320,13 +349,13 @@ float3 tracePath(float3 origin, float3 direction, inout uint rng)
float t = query.CommittedRayT();
float3 hitPos = origin + direction * t;
- uint i0 = indices[primIdx * 3 + 0];
- uint i1 = indices[primIdx * 3 + 1];
- uint i2 = indices[primIdx * 3 + 2];
+ uint i0 = pathTracing.Indices[primIdx * 3 + 0];
+ uint i1 = pathTracing.Indices[primIdx * 3 + 1];
+ uint i2 = pathTracing.Indices[primIdx * 3 + 2];
- Vertex v0 = vertices[i0];
- Vertex v1 = vertices[i1];
- Vertex v2 = vertices[i2];
+ Vertex v0 = pathTracing.Vertices[i0];
+ Vertex v1 = pathTracing.Vertices[i1];
+ Vertex v2 = pathTracing.Vertices[i2];
float3 baryWeights = float3(1.0 - bary.x - bary.y, bary.x, bary.y);
float3 normal = normalize(v0.Normal * baryWeights.x + v1.Normal * baryWeights.y + v2.Normal * baryWeights.z);
@@ -337,7 +366,7 @@ float3 tracePath(float3 origin, float3 direction, inout uint rng)
normal = -normal;
}
- Material mat = materials[v0.MaterialID];
+ Material mat = pathTracing.Materials[v0.MaterialId];
if (mat.Emission > 0.0)
{
@@ -358,14 +387,14 @@ float3 tracePath(float3 origin, float3 direction, inout uint rng)
float total = specWeight + diffWeight;
float specProb = specWeight / total;
- radiance += throughput * sampleLightDirect(hitPos, normal, geoNormal, V, mat, roughness, specProb, rng);
+ radiance += throughput * SampleLightDirect(hitPos, normal, geoNormal, V, mat, roughness, specProb, rng);
float3 newDir;
float NdotV = max(dot(normal, V), 0.001);
- if (randomFloat(rng) < specProb)
+ if (RandomFloat(rng) < specProb)
{
- float3 H = sampleGGXHalfVector(normal, roughness, rng);
+ float3 H = SampleGGXHalfVector(normal, roughness, rng);
newDir = reflect(-V, H);
float NdotL = dot(normal, newDir);
@@ -385,7 +414,7 @@ float3 tracePath(float3 origin, float3 direction, inout uint rng)
}
else
{
- newDir = cosineSampleHemisphere(normal, rng);
+ newDir = CosineSampleHemisphere(normal, rng);
float3 H = normalize(V + newDir);
float HdotV = max(dot(H, V), 0.0);
@@ -403,7 +432,7 @@ float3 tracePath(float3 origin, float3 direction, inout uint rng)
{
float p = max(throughput.r, max(throughput.g, throughput.b));
- if (randomFloat(rng) > p)
+ if (RandomFloat(rng) > p)
{
break;
}
@@ -415,7 +444,7 @@ float3 tracePath(float3 origin, float3 direction, inout uint rng)
return radiance;
}
-float halton(uint index, uint base)
+float Halton(uint index, uint base)
{
float result = 0.0;
float f = 1.0;
@@ -432,51 +461,52 @@ float halton(uint index, uint base)
return result;
}
+[shader("compute")]
[numthreads(16, 16, 1)]
void CSMain(uint3 dispatchThreadID: SV_DispatchThreadID)
{
uint2 pixel = dispatchThreadID.xy;
- if (pixel.x >= camera.Width || pixel.y >= camera.Height)
+ if (pixel.x >= pathTracing.Width || pixel.y >= pathTracing.Height)
{
return;
}
- uint rng = pcgHash(pixel.x + pixel.y * camera.Width + camera.FrameCount * camera.Width * camera.Height);
+ uint rng = PCGHash(pixel.x + pixel.y * pathTracing.Width + pathTracing.FrameCount * pathTracing.Width * pathTracing.Height);
- uint sampleIndex = camera.FrameCount + 1;
- float hx = halton(sampleIndex, 2);
- float hy = halton(sampleIndex, 3);
- float ox = randomFloat(rng) * 0.5 - 0.25;
- float oy = randomFloat(rng) * 0.5 - 0.25;
+ uint sampleIndex = pathTracing.FrameCount + 1;
+ float hx = Halton(sampleIndex, 2);
+ float hy = Halton(sampleIndex, 3);
+ float ox = RandomFloat(rng) * 0.5 - 0.25;
+ float oy = RandomFloat(rng) * 0.5 - 0.25;
float2 jitter = frac(float2(hx + ox, hy + oy));
- float2 uv = (float2(pixel) + jitter) / float2(camera.Width, camera.Height);
+ float2 uv = (float2(pixel) + jitter) / float2(pathTracing.Width, pathTracing.Height);
float2 ndc = uv * 2.0 - 1.0;
ndc.y = -ndc.y;
- float4 target = mul(float4(ndc, 1.0, 1.0), camera.InvProjection);
+ float4 target = mul(float4(ndc, 1.0, 1.0), pathTracing.InvProjection);
float3 localDir = normalize(target.xyz / target.w);
- float3 direction = normalize(mul(float4(localDir, 0.0), camera.InvView).xyz);
- float3 origin = camera.Position;
+ float3 direction = normalize(mul(float4(localDir, 0.0), pathTracing.InvView).xyz);
+ float3 origin = pathTracing.Position;
- float3 color = tracePath(origin, direction, rng);
+ float3 color = TracePath(origin, direction, rng);
color = min(color, float3(30.0, 30.0, 30.0));
- float4 prev = accumTexture[pixel];
float4 accumulated;
- if (camera.FrameCount == 0)
+ if (pathTracing.FrameCount == 0)
{
accumulated = float4(color, 1.0);
}
else
{
+ float4 prev = pathTracing.AccumulationTexture[pixel];
accumulated = prev + float4(color, 1.0);
}
- accumTexture[pixel] = accumulated;
+ pathTracing.AccumulationTexture[pixel] = accumulated;
- float3 avg = accumulated.rgb / float(camera.FrameCount + 1);
+ float3 avg = accumulated.rgb / float(pathTracing.FrameCount + 1);
// ACES tonemapping
float3 a = avg * (avg * 2.51 + 0.03);
@@ -484,5 +514,5 @@ void CSMain(uint3 dispatchThreadID: SV_DispatchThreadID)
avg = saturate(a / b);
avg = pow(avg, 1.0 / 2.2);
- outputTexture[pixel] = float4(avg, 1.0);
+ pathTracing.OutputTexture[pixel] = float4(avg, 1.0);
}
diff --git a/sources/Experiments/CornellBox/Assets/Shaders/Rasterization.slang b/sources/Experiments/CornellBox/Assets/Shaders/Rasterization.slang
index 17135159..1e32c157 100644
--- a/sources/Experiments/CornellBox/Assets/Shaders/Rasterization.slang
+++ b/sources/Experiments/CornellBox/Assets/Shaders/Rasterization.slang
@@ -1,14 +1,10 @@
-struct Material
+static const float PI = 3.14159265;
+
+struct Material
{
private float4 AlbedoAndEmission;
- float Metallic;
-
- float Roughness;
-
- private float padding0;
-
- private float padding1;
+ private float4 MetallicRoughnessAndPadding;
property float3 Albedo
{
@@ -23,9 +19,23 @@
return AlbedoAndEmission.w;
}
}
+
+ property float Metallic
+ {
+ get {
+ return MetallicRoughnessAndPadding.x;
+ }
+ }
+
+ property float Roughness
+ {
+ get {
+ return MetallicRoughnessAndPadding.y;
+ }
+ }
};
-struct RasterConstants
+struct RasterizationConstants
{
float4x4 Model;
@@ -33,16 +43,18 @@ struct RasterConstants
float4x4 Projection;
- private float4 LightPosAndPadding;
+ private float4 LightPositionAndPadding;
private float4 LightColorAndPadding;
- private float4 CameraPosAndPadding;
+ private float4 CameraPositionAndPadding;
- property float3 LightPos
+ DescriptorHandle> Materials;
+
+ property float3 LightPosition
{
get {
- return LightPosAndPadding.xyz;
+ return LightPositionAndPadding.xyz;
}
}
@@ -53,10 +65,10 @@ struct RasterConstants
}
}
- property float3 CameraPos
+ property float3 CameraPosition
{
get {
- return CameraPosAndPadding.xyz;
+ return CameraPositionAndPadding.xyz;
}
}
};
@@ -65,7 +77,7 @@ struct VSInput
{
private float4 PositionAndPadding : POSITION0;
- private float4 NormalAndMaterialID : NORMAL0;
+ private float4 NormalAndMaterialId : NORMAL0;
property float3 Position
{
@@ -77,19 +89,19 @@ struct VSInput
property float3 Normal
{
get {
- return NormalAndMaterialID.xyz;
+ return NormalAndMaterialId.xyz;
}
}
- property uint MaterialID
+ property uint MaterialId
{
get {
- return asuint(NormalAndMaterialID.w);
+ return asuint(NormalAndMaterialId.w);
}
}
};
-struct PSInput
+struct FSInput
{
float4 Position : SV_POSITION;
@@ -97,20 +109,17 @@ struct PSInput
float3 Normal : TEXCOORD1;
- nointerpolation uint MaterialID : TEXCOORD2;
+ nointerpolation uint MaterialId : TEXCOORD2;
};
-ConstantBuffer raster;
-StructuredBuffer materials;
-
-static const float PI = 3.14159265;
+ConstantBuffer rasterization;
float3 ACESFilm(float3 x)
{
return saturate((x * (x * 2.51 + 0.03)) / (x * (x * 2.43 + 0.59) + 0.14));
}
-float3 toSRGB(float3 linear)
+float3 ToSRGB(float3 linear)
{
return pow(linear, 1.0 / 2.2);
}
@@ -119,15 +128,17 @@ float DistributionGGX(float NdotH, float roughness)
{
float a = roughness * roughness;
float a2 = a * a;
- float d = NdotH * NdotH * (a2 - 1.0) + 1.0;
- return a2 / (PI * d * d);
+ float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
+
+ return a2 / (PI * denom * denom);
}
-float GeometrySchlickGGX(float NdotV, float roughness)
+float GeometrySchlickGGX(float NdotX, float roughness)
{
float r = roughness + 1.0;
float k = (r * r) / 8.0;
- return NdotV / (NdotV * (1.0 - k) + k);
+
+ return NdotX / (NdotX * (1.0 - k) + k);
}
float GeometrySmith(float NdotV, float NdotL, float roughness)
@@ -140,35 +151,37 @@ float3 FresnelSchlick(float cosTheta, float3 F0)
return F0 + (1.0 - F0) * pow(saturate(1.0 - cosTheta), 5.0);
}
-PSInput VSMain(VSInput input)
+[shader("vertex")]
+FSInput VSMain(VSInput input)
{
- float4 worldPos = mul(float4(input.Position, 1.0), raster.Model);
+ float4 worldPos = mul(float4(input.Position, 1.0), rasterization.Model);
- PSInput output;
- output.Position = mul(mul(worldPos, raster.View), raster.Projection);
+ FSInput output;
+ output.Position = mul(mul(worldPos, rasterization.View), rasterization.Projection);
output.WorldPos = worldPos.xyz;
- output.Normal = normalize(mul(float4(input.Normal, 0.0), raster.Model).xyz);
- output.MaterialID = input.MaterialID;
+ output.Normal = normalize(mul(float4(input.Normal, 0.0), rasterization.Model).xyz);
+ output.MaterialId = input.MaterialId;
return output;
}
-float4 PSMain(PSInput input) : SV_TARGET
+[shader("fragment")]
+float4 FSMain(FSInput input) : SV_TARGET
{
- Material mat = materials[input.MaterialID];
+ Material mat = rasterization.Materials[input.MaterialId];
if (mat.Emission > 0.0)
{
float3 emissive = mat.Albedo * mat.Emission;
float3 mapped = emissive / (emissive + 1.0);
- return float4(toSRGB(mapped), 1.0);
+ return float4(ToSRGB(mapped), 1.0);
}
float3 N = normalize(input.Normal);
float3 worldPos = input.WorldPos;
- float3 V = normalize(raster.CameraPos - worldPos);
- float3 toLight = raster.LightPos - worldPos;
+ float3 V = normalize(rasterization.CameraPosition - worldPos);
+ float3 toLight = rasterization.LightPosition - worldPos;
float dist = length(toLight);
float3 L = toLight / dist;
float3 H = normalize(L + V);
@@ -192,14 +205,14 @@ float4 PSMain(PSInput input) : SV_TARGET
float3 diffuse = kD * mat.Albedo / PI;
float atten = 1.0 / (dist * dist) * 80000.0;
- float3 Lo = (diffuse + specular) * raster.LightColor * NdotL * atten;
+ float3 Lo = (diffuse + specular) * rasterization.LightColor * NdotL * atten;
float hemiFactor = N.y * 0.5 + 0.5;
float3 ambient = mat.Albedo * lerp(0.03, 0.10, hemiFactor) * (1.0 - metallic * 0.7);
float3 color = ambient + Lo;
- color = toSRGB(ACESFilm(color));
+ color = ToSRGB(ACESFilm(color));
return float4(color, 1.0);
}
diff --git a/sources/Experiments/CornellBox/CornellBox.csproj b/sources/Experiments/CornellBox/CornellBox.csproj
index 65e6ad06..89e4254c 100644
--- a/sources/Experiments/CornellBox/CornellBox.csproj
+++ b/sources/Experiments/CornellBox/CornellBox.csproj
@@ -7,7 +7,6 @@
-
diff --git a/sources/Experiments/CornellBox/Handlers/ImGuiHandler.cs b/sources/Experiments/CornellBox/Handlers/ImGuiHandler.cs
index 4fbd18ea..e23892f1 100644
--- a/sources/Experiments/CornellBox/Handlers/ImGuiHandler.cs
+++ b/sources/Experiments/CornellBox/Handlers/ImGuiHandler.cs
@@ -11,7 +11,7 @@ internal class ImGuiHandler : ImGuiController, IImGuiPlatformBindings
private readonly IMouse mouse;
private readonly IKeyboard keyboard;
- public ImGuiHandler(IInputContext input, Output output) : base(App.Context, output, ImGuiColorSpace.Legacy, Path.Combine(AppContext.BaseDirectory, "Assets", "Fonts", "msyh.ttf"), OtherSetup)
+ public ImGuiHandler(IInputContext input, AttachmentFormats attachmentFormats) : base(App.Context, attachmentFormats, ImGuiColorSpace.Legacy, Path.Combine(AppContext.BaseDirectory, "Assets", "Fonts", "msyh.ttf"), OtherSetup)
{
mouse = input.Mice[0];
mouse.MouseDown += OnMouseDown;
diff --git a/sources/Experiments/CornellBox/Helpers/BindingHelper.cs b/sources/Experiments/CornellBox/Helpers/BindingHelper.cs
deleted file mode 100644
index bcfd50ef..00000000
--- a/sources/Experiments/CornellBox/Helpers/BindingHelper.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-using Zenith.NET;
-
-namespace CornellBox.Helpers;
-
-internal static class BindingHelper
-{
- public static ResourceBinding[] Bindings(params ResourceBinding[] bindings)
- {
- switch (App.Context.Backend)
- {
- case Backend.DirectX12:
- {
- uint cbvIndex = 0;
- uint srvIndex = 0;
- uint uavIndex = 0;
- uint samplerIndex = 0;
-
- for (int i = 0; i < bindings.Length; i++)
- {
- ref ResourceBinding binding = ref bindings[i];
-
- binding = binding with
- {
- Index = binding.Type switch
- {
- ResourceType.ConstantBuffer => cbvIndex++,
-
- ResourceType.StructuredBuffer or
- ResourceType.Texture or
- ResourceType.AccelerationStructure => srvIndex++,
-
- ResourceType.StructuredBufferReadWrite or
- ResourceType.TextureReadWrite => uavIndex++,
-
- ResourceType.Sampler => samplerIndex++,
-
- _ => binding.Index
- }
- };
- }
- }
- break;
-
- case Backend.Metal:
- {
- uint bufferIndex = 0;
- uint textureIndex = 0;
- uint samplerIndex = 0;
-
- for (int i = 0; i < bindings.Length; i++)
- {
- ref ResourceBinding binding = ref bindings[i];
-
- binding = binding with
- {
- Index = binding.Type switch
- {
- ResourceType.ConstantBuffer or
- ResourceType.StructuredBuffer or
- ResourceType.StructuredBufferReadWrite or
- ResourceType.AccelerationStructure => bufferIndex++,
-
- ResourceType.Texture or
- ResourceType.TextureReadWrite => textureIndex++,
-
- ResourceType.Sampler => samplerIndex++,
-
- _ => binding.Index
- }
- };
- }
- }
- break;
-
- case Backend.Vulkan:
- {
- for (int i = 0; i < bindings.Length; i++)
- {
- ref ResourceBinding binding = ref bindings[i];
-
- binding = binding with { Index = (uint)i };
- }
- }
- break;
- }
-
- return bindings;
- }
-}
diff --git a/sources/Experiments/CornellBox/Helpers/CocoaHelper.cs b/sources/Experiments/CornellBox/Helpers/CocoaHelper.cs
index acb3879d..5b0d97ae 100644
--- a/sources/Experiments/CornellBox/Helpers/CocoaHelper.cs
+++ b/sources/Experiments/CornellBox/Helpers/CocoaHelper.cs
@@ -24,7 +24,6 @@ internal static partial class CocoaHelper
public static nint CreateLayer(nint cocoa)
{
nint layer = Send(GetClass("CAMetalLayer"), Selector("layer"));
- Send(layer, Selector("retain"));
nint view = Send(cocoa, Selector("contentView"));
Send(view, Selector("setWantsLayer:"), true);
diff --git a/sources/Experiments/CornellBox/Helpers/CornellBoxGeometry.cs b/sources/Experiments/CornellBox/Helpers/CornellBoxGeometry.cs
index 0d378131..431304ae 100644
--- a/sources/Experiments/CornellBox/Helpers/CornellBoxGeometry.cs
+++ b/sources/Experiments/CornellBox/Helpers/CornellBoxGeometry.cs
@@ -82,12 +82,48 @@ public static void Create(out Vertex[] vertices, out uint[] indices, out Materia
indices = [.. indicesList];
materials =
[
- new() { Albedo = new(0.63f, 0.06f, 0.06f), Emission = 0.00f, Metallic = 0.0f, Roughness = 0.90f },
- new() { Albedo = new(0.14f, 0.45f, 0.09f), Emission = 0.00f, Metallic = 0.0f, Roughness = 0.90f },
- new() { Albedo = new(0.73f, 0.71f, 0.68f), Emission = 0.00f, Metallic = 0.0f, Roughness = 0.90f },
- new() { Albedo = new(1.00f, 0.85f, 0.60f), Emission = 25.0f, Metallic = 0.0f, Roughness = 0.50f },
- new() { Albedo = new(0.73f, 0.71f, 0.68f), Emission = 0.00f, Metallic = 0.0f, Roughness = 0.30f },
- new() { Albedo = new(0.95f, 0.93f, 0.88f), Emission = 0.00f, Metallic = 1.0f, Roughness = 0.05f }
+ new()
+ {
+ Albedo = new(0.63f, 0.06f, 0.06f),
+ Emission = 0.00f,
+ Metallic = 0.0f,
+ Roughness = 0.90f
+ },
+ new()
+ {
+ Albedo = new(0.14f, 0.45f, 0.09f),
+ Emission = 0.00f,
+ Metallic = 0.0f,
+ Roughness = 0.90f
+ },
+ new()
+ {
+ Albedo = new(0.73f, 0.71f, 0.68f),
+ Emission = 0.00f,
+ Metallic = 0.0f,
+ Roughness = 0.90f
+ },
+ new()
+ {
+ Albedo = new(1.00f, 0.85f, 0.60f),
+ Emission = 25.0f,
+ Metallic = 0.0f,
+ Roughness = 0.50f
+ },
+ new()
+ {
+ Albedo = new(0.73f, 0.71f, 0.68f),
+ Emission = 0.00f,
+ Metallic = 0.0f,
+ Roughness = 0.30f
+ },
+ new()
+ {
+ Albedo = new(0.95f, 0.93f, 0.88f),
+ Emission = 0.00f,
+ Metallic = 1.0f,
+ Roughness = 0.05f
+ }
];
}
@@ -97,16 +133,39 @@ private static void AddQuad(List vertices,
Vector3 v1,
Vector3 v2,
Vector3 v3,
- uint materialID)
+ uint materialId)
{
Vector3 normal = Vector3.Normalize(Vector3.Cross(v1 - v0, v2 - v0));
uint startIndex = (uint)vertices.Count;
- vertices.Add(new() { Position = v0, Normal = normal, MaterialID = materialID });
- vertices.Add(new() { Position = v1, Normal = normal, MaterialID = materialID });
- vertices.Add(new() { Position = v2, Normal = normal, MaterialID = materialID });
- vertices.Add(new() { Position = v3, Normal = normal, MaterialID = materialID });
+ vertices.Add(new()
+ {
+ Position = v0,
+ Normal = normal,
+ MaterialId = materialId
+ });
+
+ vertices.Add(new()
+ {
+ Position = v1,
+ Normal = normal,
+ MaterialId = materialId
+ });
+
+ vertices.Add(new()
+ {
+ Position = v2,
+ Normal = normal,
+ MaterialId = materialId
+ });
+
+ vertices.Add(new()
+ {
+ Position = v3,
+ Normal = normal,
+ MaterialId = materialId
+ });
indices.Add(startIndex);
indices.Add(startIndex + 1);
@@ -127,7 +186,7 @@ internal struct Vertex
public Vector3 Normal;
[FieldOffset(28)]
- public uint MaterialID;
+ public uint MaterialId;
}
[StructLayout(LayoutKind.Explicit, Size = 32)]
diff --git a/sources/Experiments/CornellBox/Helpers/ImGuiHelper.cs b/sources/Experiments/CornellBox/Helpers/ImGuiHelper.cs
new file mode 100644
index 00000000..16d808c0
--- /dev/null
+++ b/sources/Experiments/CornellBox/Helpers/ImGuiHelper.cs
@@ -0,0 +1,32 @@
+using Hexa.NET.ImGui;
+
+namespace CornellBox.Helpers;
+
+internal static class ImGuiHelper
+{
+ public static void Overlay(Action action)
+ {
+ ImGui.SetNextWindowPos(new(10, 10), ImGuiCond.Always, new(0, 0));
+ ImGui.SetNextWindowBgAlpha(0.35f);
+
+ if (ImGui.Begin("Overlay", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoDecoration))
+ {
+ action();
+ }
+
+ ImGui.End();
+ }
+
+ public static void Settings(Action action)
+ {
+ ImGui.SetNextWindowPos(new(ImGui.GetIO().DisplaySize.X - 10, 10), ImGuiCond.Always, new(1, 0));
+ ImGui.SetNextWindowCollapsed(true, ImGuiCond.FirstUseEver);
+
+ if (ImGui.Begin("Settings", ImGuiWindowFlags.AlwaysAutoResize))
+ {
+ action();
+ }
+
+ ImGui.End();
+ }
+}
diff --git a/sources/Experiments/CornellBox/Renderers/PathTracingRenderer.cs b/sources/Experiments/CornellBox/Renderers/PathTracingRenderer.cs
index d75ec775..666e4af9 100644
--- a/sources/Experiments/CornellBox/Renderers/PathTracingRenderer.cs
+++ b/sources/Experiments/CornellBox/Renderers/PathTracingRenderer.cs
@@ -3,7 +3,6 @@
using CornellBox.Handlers;
using CornellBox.Helpers;
using Zenith.NET;
-using Zenith.NET.Extensions.Slang;
using Buffer = Zenith.NET.Buffer;
namespace CornellBox.Renderers;
@@ -14,15 +13,13 @@ internal unsafe class PathTracingRenderer : Renderer
private readonly Buffer vertexBuffer;
private readonly Buffer indexBuffer;
- private readonly Buffer materialBuffer;
- private readonly Buffer cameraBuffer;
- private readonly BottomLevelAccelerationStructure blas;
- private readonly TopLevelAccelerationStructure tlas;
- private readonly ResourceLayout resourceLayout;
+ private readonly Buffer constantBuffer;
private readonly ComputePipeline pipeline;
+ private readonly BottomLevelAccelerationStructure blas;
+ private readonly TopLevelAccelerationStructure tlas;
+ private readonly Buffer materialBuffer;
private Texture? accumulationTexture;
- private ResourceTable? resourceTable;
private Matrix4x4 lastView;
private Matrix4x4 lastProjection;
@@ -35,34 +32,48 @@ public PathTracingRenderer()
{
SizeInBytes = (uint)(sizeof(Vertex) * vertices.Length),
StrideInBytes = (uint)sizeof(Vertex),
- Flags = BufferUsageFlags.ShaderResource | BufferUsageFlags.AccelerationStructure
+ Usages = BufferUsages.StorageReadOnly | BufferUsages.TransferDst,
+ Residency = MemoryResidency.GpuOnly
});
- vertexBuffer.Upload(vertices, 0);
+
+ fixed (Vertex* pointer = vertices)
+ {
+ vertexBuffer.Upload(0, new()
+ {
+ Pointer = (nint)pointer,
+ SizeInBytes = (uint)(sizeof(Vertex) * vertices.Length)
+ });
+ }
indexBuffer = App.Context.CreateBuffer(new()
{
SizeInBytes = (uint)(sizeof(uint) * indices.Length),
StrideInBytes = sizeof(uint),
- Flags = BufferUsageFlags.ShaderResource | BufferUsageFlags.AccelerationStructure
+ Usages = BufferUsages.StorageReadOnly | BufferUsages.TransferDst,
+ Residency = MemoryResidency.GpuOnly
});
- indexBuffer.Upload(indices, 0);
- materialBuffer = App.Context.CreateBuffer(new()
+ fixed (uint* pointer = indices)
{
- SizeInBytes = (uint)(sizeof(Material) * materials.Length),
- StrideInBytes = (uint)sizeof(Material),
- Flags = BufferUsageFlags.ShaderResource
- });
- materialBuffer.Upload(materials, 0);
+ indexBuffer.Upload(0, new()
+ {
+ Pointer = (nint)pointer,
+ SizeInBytes = (uint)(sizeof(uint) * indices.Length)
+ });
+ }
- cameraBuffer = App.Context.CreateBuffer(new()
+ constantBuffer = App.Context.CreateBuffer(new()
{
- SizeInBytes = (uint)sizeof(CameraParams),
- StrideInBytes = (uint)sizeof(CameraParams),
- Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite
+ SizeInBytes = (uint)sizeof(PathTracingConstants),
+ Usages = BufferUsages.Constant,
+ Residency = MemoryResidency.CpuWriteOnly
});
- CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer();
+ using Shader computeShader = App.Context.CreateShader(ZenithCompiler.CompileFromFile(App.Context.GraphicsApi, ShaderPath("PathTracing.slang"), "CSMain"));
+
+ pipeline = App.Context.CreateComputePipeline(new() { ComputeShader = computeShader });
+
+ CommandBuffer commandBuffer = App.Context.ComputeQueue.CommandBuffer();
blas = commandBuffer.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc
{
@@ -70,8 +81,8 @@ public PathTracingRenderer()
[
new()
{
- Type = RayTracingGeometryType.Triangles,
- Triangles = new()
+ Type = RayTracingGeometryType.Triangle,
+ TriangleGeometry = new()
{
VertexBuffer = vertexBuffer,
VertexFormat = PixelFormat.R32G32B32Float,
@@ -82,10 +93,10 @@ public PathTracingRenderer()
IndexCount = (uint)indices.Length,
Transform = Matrix4x4.Identity
},
- Flags = RayTracingGeometryFlags.Opaque
+ IsOpaque = true
}
],
- Flags = AccelerationStructureBuildFlags.PreferFastTrace
+ BuildFlags = AccelerationStructureBuildFlags.PreferFastTrace
});
tlas = commandBuffer.BuildAccelerationStructure(new TopLevelAccelerationStructureDesc
@@ -95,41 +106,33 @@ public PathTracingRenderer()
new()
{
AccelerationStructure = blas,
- ID = 0,
- Mask = 0xFF,
+ InstanceId = 0,
+ VisibilityMask = 0xFF,
Transform = Matrix4x4.Identity,
Flags = RayTracingInstanceFlags.None
}
],
- Flags = AccelerationStructureBuildFlags.PreferFastTrace
+ BuildFlags = AccelerationStructureBuildFlags.PreferFastTrace
});
- commandBuffer.Submit(waitForCompletion: true);
+ commandBuffer.Submit().Wait();
- resourceLayout = App.Context.CreateResourceLayout(new()
+ materialBuffer = App.Context.CreateBuffer(new()
{
- Bindings = BindingHelper.Bindings
- (
- new() { Type = ResourceType.AccelerationStructure, Count = 1, StageFlags = ShaderStageFlags.Compute },
- new() { Type = ResourceType.ConstantBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute },
- new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute },
- new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute },
- new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute },
- new() { Type = ResourceType.TextureReadWrite, Count = 1, StageFlags = ShaderStageFlags.Compute },
- new() { Type = ResourceType.TextureReadWrite, Count = 1, StageFlags = ShaderStageFlags.Compute }
- )
+ SizeInBytes = (uint)(sizeof(Material) * materials.Length),
+ StrideInBytes = (uint)sizeof(Material),
+ Usages = BufferUsages.StorageReadOnly | BufferUsages.TransferDst,
+ Residency = MemoryResidency.GpuOnly
});
- using Shader computeShader = App.Context.LoadShaderFromFile(ShaderPath("PathTracing.slang"), "CSMain", ShaderStageFlags.Compute);
-
- pipeline = App.Context.CreateComputePipeline(new()
+ fixed (Material* pointer = materials)
{
- Compute = computeShader,
- ResourceLayout = resourceLayout,
- ThreadGroupSizeX = ThreadGroupSize,
- ThreadGroupSizeY = ThreadGroupSize,
- ThreadGroupSizeZ = 1
- });
+ materialBuffer.Upload(0, new()
+ {
+ Pointer = (nint)pointer,
+ SizeInBytes = (uint)(sizeof(Material) * materials.Length)
+ });
+ }
}
public uint FrameCount { get; set; }
@@ -150,46 +153,45 @@ public override void Update(CameraHandler camera)
Matrix4x4.Invert(view, out Matrix4x4 invView);
Matrix4x4.Invert(projection, out Matrix4x4 invProjection);
- cameraBuffer.Upload([new()
+ PathTracingConstants parameters = new()
{
InvView = invView,
InvProjection = invProjection,
Position = camera.Position,
FrameCount = FrameCount,
Width = App.Width,
- Height = App.Height
- }], 0);
+ Height = App.Height,
+ Scene = tlas.Handle,
+ Vertices = vertexBuffer.StorageReadOnlyHandle,
+ Indices = indexBuffer.StorageReadOnlyHandle,
+ Materials = materialBuffer.StorageReadOnlyHandle,
+ AccumulationTexture = accumulationTexture!.StorageHandle,
+ OutputTexture = Color.StorageHandle
+ };
+
+ constantBuffer.Upload(0, new()
+ {
+ Pointer = (nint)(¶meters),
+ SizeInBytes = (uint)sizeof(PathTracingConstants)
+ });
}
public override void Render(CommandBuffer commandBuffer)
{
- if (resourceTable is null || accumulationTexture is null)
- {
- accumulationTexture = App.Context.CreateTexture(new()
- {
- Type = TextureType.Texture2D,
- Format = PixelFormat.R32G32B32A32Float,
- Width = App.Width,
- Height = App.Height,
- Depth = 1,
- MipLevels = 1,
- ArrayLayers = 1,
- SampleCount = SampleCount.Count1,
- Flags = TextureUsageFlags.ShaderResource | TextureUsageFlags.UnorderedAccess
- });
+ commandBuffer.Transition(Color, default, TextureLayout.Undefined, TextureLayout.Storage);
- resourceTable = App.Context.CreateResourceTable(new()
- {
- Layout = resourceLayout,
- Resources = [tlas, cameraBuffer, vertexBuffer, indexBuffer, materialBuffer, accumulationTexture, Color]
- });
+ if (FrameCount is 0)
+ {
+ commandBuffer.Transition(accumulationTexture!, default, TextureLayout.Undefined, TextureLayout.Storage);
}
commandBuffer.SetPipeline(pipeline);
- commandBuffer.SetResourceTable(resourceTable);
+ commandBuffer.SetConstantBuffer(constantBuffer, 0);
commandBuffer.Dispatch((App.Width + ThreadGroupSize - 1) / ThreadGroupSize, (App.Height + ThreadGroupSize - 1) / ThreadGroupSize, 1);
+ commandBuffer.Transition(Color, default, TextureLayout.Storage, TextureLayout.Sampled);
+
FrameCount++;
}
@@ -197,35 +199,40 @@ public override void Resize(uint width, uint height)
{
base.Resize(width, height);
- resourceTable?.Dispose();
- resourceTable = null;
-
accumulationTexture?.Dispose();
- accumulationTexture = null;
+ accumulationTexture = App.Context.CreateTexture(new()
+ {
+ Type = TextureType.Texture2D,
+ Format = PixelFormat.R32G32B32A32Float,
+ Width = width,
+ Height = height,
+ Depth = 1,
+ MipLevels = 1,
+ ArrayLayers = 1,
+ SampleCount = SampleCount.Count1,
+ Usages = TextureUsages.Sampled | TextureUsages.Storage
+ });
FrameCount = 0;
}
public override void Dispose()
{
- base.Dispose();
-
- resourceTable?.Dispose();
- accumulationTexture?.Dispose();
-
- pipeline.Dispose();
- resourceLayout.Dispose();
+ materialBuffer.Dispose();
tlas.Dispose();
blas.Dispose();
- cameraBuffer.Dispose();
- materialBuffer.Dispose();
+ pipeline.Dispose();
+ constantBuffer.Dispose();
indexBuffer.Dispose();
vertexBuffer.Dispose();
+ accumulationTexture?.Dispose();
+
+ base.Dispose();
}
}
-[StructLayout(LayoutKind.Explicit, Size = 160)]
-file struct CameraParams
+[StructLayout(LayoutKind.Explicit, Size = 208)]
+file struct PathTracingConstants
{
[FieldOffset(0)]
public Matrix4x4 InvView;
@@ -244,4 +251,22 @@ file struct CameraParams
[FieldOffset(152)]
public uint Height;
+
+ [FieldOffset(160)]
+ public ResourceHandle Scene;
+
+ [FieldOffset(168)]
+ public ResourceHandle Vertices;
+
+ [FieldOffset(176)]
+ public ResourceHandle Indices;
+
+ [FieldOffset(184)]
+ public ResourceHandle Materials;
+
+ [FieldOffset(192)]
+ public ResourceHandle AccumulationTexture;
+
+ [FieldOffset(200)]
+ public ResourceHandle OutputTexture;
}
diff --git a/sources/Experiments/CornellBox/Renderers/RasterizationRenderer.cs b/sources/Experiments/CornellBox/Renderers/RasterizationRenderer.cs
index ce9a8cc4..05ac34db 100644
--- a/sources/Experiments/CornellBox/Renderers/RasterizationRenderer.cs
+++ b/sources/Experiments/CornellBox/Renderers/RasterizationRenderer.cs
@@ -3,146 +3,152 @@
using CornellBox.Handlers;
using CornellBox.Helpers;
using Zenith.NET;
-using Zenith.NET.Extensions.Slang;
using Buffer = Zenith.NET.Buffer;
namespace CornellBox.Renderers;
internal unsafe class RasterizationRenderer : Renderer
{
+ private readonly uint indexCount;
+
private readonly Buffer vertexBuffer;
private readonly Buffer indexBuffer;
- private readonly Buffer materialBuffer;
private readonly Buffer constantBuffer;
- private readonly uint indexCount;
- private readonly ResourceLayout resourceLayout;
- private readonly ResourceTable resourceTable;
private readonly GraphicsPipeline pipeline;
+ private readonly Buffer materialBuffer;
+
public RasterizationRenderer()
{
CornellBoxGeometry.Create(out Vertex[] vertices, out uint[] indices, out Material[] materials);
indexCount = (uint)indices.Length;
- vertexBuffer = App.Context.CreateBuffer(new()
- {
- SizeInBytes = (uint)(sizeof(Vertex) * vertices.Length),
- StrideInBytes = (uint)sizeof(Vertex),
- Flags = BufferUsageFlags.Vertex
- });
- vertexBuffer.Upload(vertices, 0);
+ vertexBuffer = App.Context.CreateBuffer(BufferDesc.Vertex((uint)(sizeof(Vertex) * vertices.Length)));
- indexBuffer = App.Context.CreateBuffer(new()
+ fixed (Vertex* pointer = vertices)
{
- SizeInBytes = (uint)(sizeof(uint) * indices.Length),
- StrideInBytes = sizeof(uint),
- Flags = BufferUsageFlags.Index
- });
- indexBuffer.Upload(indices, 0);
+ vertexBuffer.Upload(0, new()
+ {
+ Pointer = (nint)pointer,
+ SizeInBytes = (uint)(sizeof(Vertex) * vertices.Length)
+ });
+ }
+
+ indexBuffer = App.Context.CreateBuffer(BufferDesc.Index((uint)(sizeof(uint) * indices.Length)));
- materialBuffer = App.Context.CreateBuffer(new()
+ fixed (uint* pointer = indices)
{
- SizeInBytes = (uint)(sizeof(Material) * materials.Length),
- StrideInBytes = (uint)sizeof(Material),
- Flags = BufferUsageFlags.ShaderResource
- });
- materialBuffer.Upload(materials, 0);
+ indexBuffer.Upload(0, new()
+ {
+ Pointer = (nint)pointer,
+ SizeInBytes = (uint)(sizeof(uint) * indices.Length)
+ });
+ }
constantBuffer = App.Context.CreateBuffer(new()
{
- SizeInBytes = (uint)sizeof(RasterConstants),
- StrideInBytes = (uint)sizeof(RasterConstants),
- Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite
+ SizeInBytes = (uint)sizeof(RasterizationConstants),
+ Usages = BufferUsages.Constant,
+ Residency = MemoryResidency.CpuWriteOnly
});
- resourceLayout = App.Context.CreateResourceLayout(new()
+ InputLayout inputLayout = new();
+ inputLayout.Add(new()
{
- Bindings = BindingHelper.Bindings
- (
- new() { Type = ResourceType.ConstantBuffer, Count = 1, StageFlags = ShaderStageFlags.Vertex | ShaderStageFlags.Pixel },
- new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Pixel }
- )
+ Format = ElementFormat.Float4,
+ Semantic = ElementSemantic.Position
});
- resourceTable = App.Context.CreateResourceTable(new()
+ inputLayout.Add(new()
{
- Layout = resourceLayout,
- Resources = [constantBuffer, materialBuffer]
+ Format = ElementFormat.Float4,
+ Semantic = ElementSemantic.Normal
});
- InputLayout inputLayout = new();
- inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Position });
- inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Normal });
-
- using Shader vertexShader = App.Context.LoadShaderFromFile(ShaderPath("Rasterization.slang"), "VSMain", ShaderStageFlags.Vertex);
- using Shader pixelShader = App.Context.LoadShaderFromFile(ShaderPath("Rasterization.slang"), "PSMain", ShaderStageFlags.Pixel);
+ using Shader vertexShader = App.Context.CreateShader(ZenithCompiler.CompileFromFile(App.Context.GraphicsApi, ShaderPath("Rasterization.slang"), "VSMain"));
+ using Shader fragmentShader = App.Context.CreateShader(ZenithCompiler.CompileFromFile(App.Context.GraphicsApi, ShaderPath("Rasterization.slang"), "FSMain"));
pipeline = App.Context.CreateGraphicsPipeline(new()
{
- RenderStates = new()
- {
- RasterizerState = RasterizerStates.CullNone,
- DepthStencilState = DepthStencilStates.Default,
- BlendState = BlendStates.Opaque
- },
- Vertex = vertexShader,
- Pixel = pixelShader,
- ResourceLayout = resourceLayout,
+ VertexShader = vertexShader,
+ FragmentShader = fragmentShader,
InputLayouts = [inputLayout],
PrimitiveTopology = PrimitiveTopology.TriangleList,
- Output = FrameBuffer.Output
+ AttachmentFormats = AttachmentFormats,
+ RenderState = new()
+ {
+ Rasterizer = RasterizerState.CullNone(),
+ DepthStencil = DepthStencilState.DepthReadWrite(),
+ Blend = BlendState.Opaque()
+ }
});
+
+ materialBuffer = App.Context.CreateBuffer(BufferDesc.StorageReadOnly((uint)(sizeof(Material) * materials.Length), (uint)sizeof(Material)));
+
+ fixed (Material* pointer = materials)
+ {
+ materialBuffer.Upload(0, new()
+ {
+ Pointer = (nint)pointer,
+ SizeInBytes = (uint)(sizeof(Material) * materials.Length)
+ });
+ }
}
public override void Update(CameraHandler camera)
{
- constantBuffer.Upload([new()
+ RasterizationConstants parameters = new()
{
Model = Matrix4x4.Identity,
View = camera.View,
Projection = camera.Projection,
- LightPos = new(278.0f, 547.0f, 280.0f),
+ LightPosition = new(278.0f, 547.0f, 280.0f),
LightColor = new(2.0f, 1.8f, 1.4f),
- CameraPos = camera.Position
- }], 0);
+ CameraPosition = camera.Position,
+ Materials = materialBuffer.StorageReadOnlyHandle
+ };
+
+ constantBuffer.Upload(0, new()
+ {
+ Pointer = (nint)(¶meters),
+ SizeInBytes = (uint)sizeof(RasterizationConstants)
+ });
}
public override void Render(CommandBuffer commandBuffer)
{
- commandBuffer.BeginRenderPass(FrameBuffer, new()
- {
- ColorValues = [new(0.51f, 0.518f, 0.557f, 1.0f)],
- Depth = 1.0f,
- Stencil = 0,
- Flags = ClearFlags.All
- }, resourceTable);
+ commandBuffer.Transition(Color, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+ commandBuffer.Transition(DepthStencil, default, TextureLayout.Undefined, TextureLayout.DepthStencilAttachment);
+
+ commandBuffer.BeginRenderPass([ColorAttachment.Clear(Color, new(0.51f, 0.518f, 0.557f, 1.0f))], DepthStencilAttachment.Clear(DepthStencil, 1.0f, 0));
commandBuffer.SetPipeline(pipeline);
- commandBuffer.SetResourceTable(resourceTable);
commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0);
commandBuffer.SetIndexBuffer(indexBuffer, 0, IndexFormat.UInt32);
+ commandBuffer.SetConstantBuffer(constantBuffer, 0);
+
commandBuffer.DrawIndexed(indexCount, 1, 0, 0, 0);
commandBuffer.EndRenderPass();
+
+ commandBuffer.Transition(Color, default, TextureLayout.ColorAttachment, TextureLayout.Sampled);
}
public override void Dispose()
{
- base.Dispose();
-
+ materialBuffer.Dispose();
pipeline.Dispose();
- resourceTable.Dispose();
- resourceLayout.Dispose();
constantBuffer.Dispose();
- materialBuffer.Dispose();
indexBuffer.Dispose();
vertexBuffer.Dispose();
+
+ base.Dispose();
}
}
-[StructLayout(LayoutKind.Explicit, Size = 240)]
-file struct RasterConstants
+[StructLayout(LayoutKind.Explicit, Size = 256)]
+file struct RasterizationConstants
{
[FieldOffset(0)]
public Matrix4x4 Model;
@@ -154,11 +160,14 @@ file struct RasterConstants
public Matrix4x4 Projection;
[FieldOffset(192)]
- public Vector3 LightPos;
+ public Vector3 LightPosition;
[FieldOffset(208)]
public Vector3 LightColor;
[FieldOffset(224)]
- public Vector3 CameraPos;
+ public Vector3 CameraPosition;
+
+ [FieldOffset(240)]
+ public ResourceHandle Materials;
}
diff --git a/sources/Experiments/CornellBox/Renderers/Renderer.cs b/sources/Experiments/CornellBox/Renderers/Renderer.cs
index 4dd9bd99..9c6b52ee 100644
--- a/sources/Experiments/CornellBox/Renderers/Renderer.cs
+++ b/sources/Experiments/CornellBox/Renderers/Renderer.cs
@@ -14,7 +14,12 @@ protected Renderer()
public Texture DepthStencil { get; private set; } = null!;
- public FrameBuffer FrameBuffer { get; private set; } = null!;
+ public AttachmentFormats AttachmentFormats => new()
+ {
+ ColorFormats = [Color.Desc.Format],
+ DepthStencilFormat = DepthStencil.Desc.Format,
+ SampleCount = Color.Desc.SampleCount
+ };
public abstract void Update(CameraHandler camera);
@@ -22,7 +27,6 @@ protected Renderer()
public virtual void Resize(uint width, uint height)
{
- FrameBuffer?.Dispose();
DepthStencil?.Dispose();
Color?.Dispose();
@@ -36,7 +40,7 @@ public virtual void Resize(uint width, uint height)
MipLevels = 1,
ArrayLayers = 1,
SampleCount = SampleCount.Count1,
- Flags = TextureUsageFlags.RenderTarget | TextureUsageFlags.ShaderResource | TextureUsageFlags.UnorderedAccess
+ Usages = TextureUsages.Sampled | TextureUsages.Storage | TextureUsages.ColorAttachment
});
DepthStencil = App.Context.CreateTexture(new()
@@ -49,19 +53,12 @@ public virtual void Resize(uint width, uint height)
MipLevels = 1,
ArrayLayers = 1,
SampleCount = SampleCount.Count1,
- Flags = TextureUsageFlags.DepthStencil
- });
-
- FrameBuffer = App.Context.CreateFrameBuffer(new()
- {
- ColorAttachments = [new() { Target = Color }],
- DepthStencilAttachment = new() { Target = DepthStencil }
+ Usages = TextureUsages.DepthStencilAttachment
});
}
public virtual void Dispose()
{
- FrameBuffer.Dispose();
DepthStencil.Dispose();
Color.Dispose();
}
diff --git a/sources/Experiments/FluidTank/App.cs b/sources/Experiments/FluidTank/App.cs
new file mode 100644
index 00000000..876c53fd
--- /dev/null
+++ b/sources/Experiments/FluidTank/App.cs
@@ -0,0 +1,282 @@
+using System.Numerics;
+using FluidTank.Handlers;
+using FluidTank.Helpers;
+using Hexa.NET.ImGui;
+using Silk.NET.Input;
+using Silk.NET.Windowing;
+using Zenith.NET;
+using Zenith.NET.DirectX12;
+using Zenith.NET.Metal;
+using Zenith.NET.Vulkan;
+
+namespace FluidTank;
+
+internal static class App
+{
+ private static readonly IWindow window;
+ private static readonly IInputContext input;
+ private static readonly SwapChain swapChain;
+ private static readonly ImGuiHandler imGui;
+ private static readonly CameraHandler camera;
+ private static readonly FluidTankRenderer renderer;
+
+ static App()
+ {
+ if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsLinux())
+ {
+ throw new PlatformNotSupportedException("This application only supports Windows, macOS, and Linux.");
+ }
+
+ if (OperatingSystem.IsWindows())
+ {
+ Context = GraphicsContext.CreateDirectX12(useValidationLayer: true);
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ Context = GraphicsContext.CreateMetal(useValidationLayer: true);
+ }
+ else
+ {
+ Context = GraphicsContext.CreateVulkan(useValidationLayer: true);
+ }
+
+ Context.ValidationMessage += static (sender, args) => Console.WriteLine($"[{args.Severity}] {args.Message}");
+
+ window = Window.Create(WindowOptions.Default with
+ {
+ API = GraphicsAPI.None,
+ Title = "Fluid Tank - Zenith.NET",
+ Size = new(1280, 720)
+ });
+ window.Initialize();
+ window.Center();
+
+ input = window.CreateInput();
+
+ Surface surface;
+ if (OperatingSystem.IsWindows())
+ {
+ surface = Surface.Win32(window.Native!.Win32!.Value.Hwnd, Width, Height);
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ surface = Surface.Apple(CocoaHelper.CreateLayer(window.Native!.Cocoa!.Value), Width, Height);
+ }
+ else
+ {
+ if (window.Native?.X11 is not { } x11)
+ {
+ throw new PlatformNotSupportedException("FluidTank requires an X11 or XWayland window on Linux.");
+ }
+
+ surface = Surface.Xlib(x11.Display, (nint)x11.Window, Width, Height);
+ }
+
+ swapChain = Context.CreateSwapChain(new()
+ {
+ Surface = surface,
+ Format = PixelFormat.B8G8R8A8UNorm
+ });
+
+ imGui = new(input, new()
+ {
+ ColorFormats = [PixelFormat.B8G8R8A8UNorm],
+ SampleCount = SampleCount.Count1
+ });
+
+ camera = new(input, new(9.2f, 5.3f, -10.8f), new(0.0f, 1.45f, 0.0f))
+ {
+ Speed = 4.0f,
+ NearPlane = 0.05f,
+ FarPlane = 80.0f,
+ Fov = 48.0f
+ };
+
+ renderer = new();
+ }
+
+ public static GraphicsContext Context { get; }
+
+ public static uint Width => (uint)window.FramebufferSize.X;
+
+ public static uint Height => (uint)window.FramebufferSize.Y;
+
+ public static Vector2 DpiScale => (Vector2)window.FramebufferSize / (Vector2)window.Size;
+
+ public static void Run()
+ {
+ window.Update += delta =>
+ {
+ if (Width is 0 || Height is 0)
+ {
+ return;
+ }
+
+ uint width = (uint)(Width / DpiScale.X);
+ uint height = (uint)(Height / DpiScale.Y);
+
+ imGui.Update(delta, width, height);
+ camera.Update(delta, width, height);
+ renderer.Update(camera, delta);
+
+ if (camera.TryConsumeClickRay(out Vector3 origin, out Vector3 direction) && !ImGui.GetIO().WantCaptureMouse)
+ {
+ renderer.PushFluid(origin, direction);
+ }
+
+ ImGui.GetBackgroundDrawList().AddImage(imGui.Binding(renderer.Color), new(0, 0), new(Width / DpiScale.X, Height / DpiScale.Y));
+
+ ImGuiHelper.Overlay(() =>
+ {
+ ImGui.Text(Context.Capabilities.DeviceName);
+ ImGui.Text($"GraphicsApi: {Context.GraphicsApi}");
+ ImGui.Text($"FPS: {ImGui.GetIO().Framerate:F1}");
+ ImGui.Text($"Particles: {renderer.ParticleCount:N0}");
+ });
+
+ ImGuiHelper.Settings(() =>
+ {
+ ImGui.Text("Run");
+
+ bool paused = renderer.Paused;
+ if (ImGui.Checkbox("Pause", ref paused))
+ {
+ renderer.Paused = paused;
+ }
+
+ ImGui.SameLine();
+
+ if (ImGui.Button("Reset dam"))
+ {
+ renderer.Reset();
+ }
+
+ ImGui.Separator();
+
+ ImGui.Text("Motion");
+
+ bool waveMakerEnabled = renderer.WaveMakerEnabled;
+ if (ImGui.Checkbox("Wave maker", ref waveMakerEnabled))
+ {
+ renderer.WaveMakerEnabled = waveMakerEnabled;
+ }
+
+ ImGui.BeginDisabled(!waveMakerEnabled);
+
+ float waveAmplitude = renderer.WaveAmplitude;
+ if (ImGui.SliderFloat("Wave amplitude", ref waveAmplitude, 0.0f, 0.34f, "%.2f m"))
+ {
+ renderer.WaveAmplitude = waveAmplitude;
+ }
+
+ float waveFrequency = renderer.WaveFrequency;
+ if (ImGui.SliderFloat("Wave frequency", ref waveFrequency, 0.2f, 2.5f, "%.2f Hz"))
+ {
+ renderer.WaveFrequency = waveFrequency;
+ }
+
+ ImGui.EndDisabled();
+
+ ImGui.Separator();
+
+ ImGui.Text("Display");
+
+ if (ImGui.RadioButton("Water", renderer.ViewMode == FluidViewMode.Water))
+ {
+ renderer.ViewMode = FluidViewMode.Water;
+ }
+
+ ImGui.SameLine();
+
+ if (ImGui.RadioButton("Particles", renderer.ViewMode == FluidViewMode.Particles))
+ {
+ renderer.ViewMode = FluidViewMode.Particles;
+ }
+
+ if (renderer.ViewMode is FluidViewMode.Water)
+ {
+ float clarity = renderer.Clarity;
+ if (ImGui.SliderFloat("Clarity", ref clarity, 0.25f, 2.0f, "%.2f"))
+ {
+ renderer.Clarity = clarity;
+ }
+
+ float refraction = renderer.RefractionStrength;
+ if (ImGui.SliderFloat("Refraction", ref refraction, 0.0f, 1.5f, "%.2f"))
+ {
+ renderer.RefractionStrength = refraction;
+ }
+ }
+
+ if (ImGui.CollapsingHeader("Advanced simulation"))
+ {
+ float flipRatio = renderer.FlipRatio;
+ if (ImGui.SliderFloat("FLIP ratio", ref flipRatio, 0.0f, 1.0f, "%.2f"))
+ {
+ renderer.FlipRatio = flipRatio;
+ }
+
+ int pressureIterations = renderer.PressureIterations;
+ if (ImGui.SliderInt("Pressure iterations", ref pressureIterations, 4, 32))
+ {
+ renderer.PressureIterations = pressureIterations;
+ }
+
+ float velocityDamping = renderer.VelocityDamping;
+ if (ImGui.SliderFloat("Velocity damping", ref velocityDamping, 0.97f, 1.0f, "%.3f"))
+ {
+ renderer.VelocityDamping = velocityDamping;
+ }
+
+ ImGui.Text("Solver: APIC / FLIP");
+ }
+ });
+ };
+
+ window.Render += _ =>
+ {
+ if (Width is 0 || Height is 0)
+ {
+ return;
+ }
+
+ TimelineValue simulationReady = renderer.Simulate();
+
+ CommandBuffer sceneCommandBuffer = Context.GraphicsQueue.CommandBuffer();
+ renderer.RenderScene(sceneCommandBuffer);
+ sceneCommandBuffer.Submit();
+
+ CommandBuffer commandBuffer = Context.GraphicsQueue.CommandBuffer();
+ renderer.RenderFluid(commandBuffer);
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+ imGui.Render(commandBuffer, ColorAttachment.DontCare(swapChain.Drawable));
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.ColorAttachment, TextureLayout.Present);
+
+ commandBuffer.Submit(simulationReady).Wait();
+
+ swapChain.Present();
+ };
+
+ window.Resize += _ =>
+ {
+ if (Width is 0 || Height is 0)
+ {
+ return;
+ }
+
+ renderer.Resize(Width, Height);
+ swapChain.Resize(Width, Height);
+ };
+
+ window.Run();
+
+ renderer.Dispose();
+ imGui.Dispose();
+ swapChain.Dispose();
+ input.Dispose();
+ window.Dispose();
+
+ Context.Dispose();
+ }
+}
diff --git a/sources/Experiments/FluidTank/Assets/Fonts/msyh.ttf b/sources/Experiments/FluidTank/Assets/Fonts/msyh.ttf
new file mode 100644
index 00000000..2bf27863
Binary files /dev/null and b/sources/Experiments/FluidTank/Assets/Fonts/msyh.ttf differ
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/.clang-format b/sources/Experiments/FluidTank/Assets/Shaders/.clang-format
new file mode 100644
index 00000000..ff17eaf4
--- /dev/null
+++ b/sources/Experiments/FluidTank/Assets/Shaders/.clang-format
@@ -0,0 +1,46 @@
+---
+Language: CSharp
+BasedOnStyle: Microsoft
+
+# Indentation
+IndentWidth: 4
+ContinuationIndentWidth: 4
+TabWidth: 4
+UseTab: Never
+IndentCaseLabels: true
+NamespaceIndentation: All
+
+# Brace placement
+BreakBeforeBraces: Custom
+BraceWrapping:
+ AfterCaseLabel: true
+ AfterClass: true
+ AfterControlStatement: Always
+ AfterEnum: true
+ AfterFunction: true
+ AfterNamespace: true
+ AfterStruct: true
+ BeforeCatch: true
+ BeforeElse: true
+ BeforeWhile: false
+ IndentBraces: false
+ SplitEmptyFunction: true
+ SplitEmptyNamespace: true
+ SplitEmptyRecord: true
+
+# Block bodies
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLoopsOnASingleLine: false
+
+# Line wrapping
+ColumnLimit: 0
+KeepEmptyLinesAtTheStartOfBlocks: false
+MaxEmptyLinesToKeep: 1
+ReflowComments: false
+
+# Declaration order
+SortIncludes: false
+SortUsingDeclarations: false
\ No newline at end of file
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/FluidBlur.slang b/sources/Experiments/FluidTank/Assets/Shaders/FluidBlur.slang
new file mode 100644
index 00000000..d5e9e035
--- /dev/null
+++ b/sources/Experiments/FluidTank/Assets/Shaders/FluidBlur.slang
@@ -0,0 +1,189 @@
+struct BlurConstants
+{
+ private uint4 DimensionsAndDirection;
+
+ private float4 FilterParameters;
+
+ DescriptorHandle> InputTexture;
+
+ DescriptorHandle> OutputTexture;
+
+ property uint Width
+ {
+ get {
+ return DimensionsAndDirection.x;
+ }
+ }
+
+ property uint Height
+ {
+ get {
+ return DimensionsAndDirection.y;
+ }
+ }
+
+ property uint Direction
+ {
+ get {
+ return DimensionsAndDirection.z;
+ }
+ }
+
+ property uint Radius
+ {
+ get {
+ return DimensionsAndDirection.w;
+ }
+ }
+
+ property float SpatialSigma
+ {
+ get {
+ return FilterParameters.x;
+ }
+ }
+
+ property float DepthSigma
+ {
+ get {
+ return FilterParameters.y;
+ }
+ }
+
+ property float ProjectedRadiusScale
+ {
+ get {
+ return FilterParameters.z;
+ }
+ }
+};
+
+ConstantBuffer blur;
+
+[shader("compute")]
+[numthreads(16, 16, 1)]
+void BlurCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ uint2 pixel = dispatchThreadID.xy;
+ if (pixel.x >= blur.Width || pixel.y >= blur.Height)
+ {
+ return;
+ }
+
+ float centerDepth = blur.InputTexture.Load(int3(pixel, 0));
+ if (centerDepth <= 0.0)
+ {
+ int2 direction = blur.Direction == 0 ? int2(1, 0) : int2(0, 1);
+ int bridgeRadius = min(int(blur.Radius), 4);
+ float negativeDepth = 0.0;
+ float positiveDepth = 0.0;
+ int negativeDistance = 0;
+ int positiveDistance = 0;
+
+ for (int offset = 1; offset <= bridgeRadius; offset++)
+ {
+ int2 negativePixel = clamp(int2(pixel) - direction * offset, int2(0, 0), int2(int(blur.Width) - 1, int(blur.Height) - 1));
+ int2 positivePixel = clamp(int2(pixel) + direction * offset, int2(0, 0), int2(int(blur.Width) - 1, int(blur.Height) - 1));
+
+ if (negativeDepth <= 0.0)
+ {
+ negativeDepth = blur.InputTexture.Load(int3(negativePixel, 0));
+ negativeDistance = negativeDepth > 0.0 ? offset : 0;
+ }
+
+ if (positiveDepth <= 0.0)
+ {
+ positiveDepth = blur.InputTexture.Load(int3(positivePixel, 0));
+ positiveDistance = positiveDepth > 0.0 ? offset : 0;
+ }
+
+ if (negativeDepth > 0.0 && positiveDepth > 0.0)
+ {
+ break;
+ }
+ }
+
+ float depthDifference = abs(negativeDepth - positiveDepth);
+ float bridgeRange = blur.DepthSigma * (0.85 + min(negativeDepth, positiveDepth) * 0.015) * 2.5;
+
+ if (negativeDepth > 0.0 && positiveDepth > 0.0 && depthDifference < bridgeRange)
+ {
+ float distanceSum = float(negativeDistance + positiveDistance);
+ blur.OutputTexture[pixel] = (negativeDepth * positiveDistance + positiveDepth * negativeDistance) / distanceSum;
+
+ return;
+ }
+
+ blur.OutputTexture[pixel] = 0.0;
+
+ return;
+ }
+
+ float weightedDepth = centerDepth;
+ float weightSum = 1.0;
+ int2 direction = blur.Direction == 0 ? int2(1, 0) : int2(0, 1);
+ float projectedRadius = blur.ProjectedRadiusScale / max(centerDepth, 0.1);
+ int localRadius = clamp(int(projectedRadius * 1.35 + 0.5), 4, int(blur.Radius));
+ float spatialSigma = max(blur.SpatialSigma, float(localRadius) * 0.58);
+ float range = blur.DepthSigma * (0.85 + centerDepth * 0.015);
+
+ for (int offset = 1; offset <= localRadius; offset++)
+ {
+ float spatialWeight = exp(-float(offset * offset) / (2.0 * spatialSigma * spatialSigma));
+
+ for (int sign = -1; sign <= 1; sign += 2)
+ {
+ int2 samplePixel = clamp(int2(pixel) + direction * offset * sign, int2(0, 0), int2(int(blur.Width) - 1, int(blur.Height) - 1));
+ float sampleDepth = blur.InputTexture.Load(int3(samplePixel, 0));
+
+ if (sampleDepth <= 0.0)
+ {
+ continue;
+ }
+
+ float difference = abs(sampleDepth - centerDepth);
+
+ if (difference > range * 2.5)
+ {
+ continue;
+ }
+
+ float depthWeight = exp(-(difference * difference) / (2.0 * range * range));
+ float weight = spatialWeight * depthWeight;
+
+ weightedDepth += sampleDepth * weight;
+ weightSum += weight;
+ }
+ }
+
+ blur.OutputTexture[pixel] = weightedDepth / weightSum;
+}
+
+[shader("compute")]
+[numthreads(16, 16, 1)]
+void BlurThicknessCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ uint2 pixel = dispatchThreadID.xy;
+ if (pixel.x >= blur.Width || pixel.y >= blur.Height)
+ {
+ return;
+ }
+
+ float weightedThickness = blur.InputTexture.Load(int3(pixel, 0));
+ float weightSum = 1.0;
+ int2 direction = blur.Direction == 0 ? int2(1, 0) : int2(0, 1);
+
+ for (int offset = 1; offset <= int(blur.Radius); offset++)
+ {
+ float weight = exp(-float(offset * offset) / (2.0 * blur.SpatialSigma * blur.SpatialSigma));
+
+ for (int sign = -1; sign <= 1; sign += 2)
+ {
+ int2 samplePixel = clamp(int2(pixel) + direction * offset * sign, int2(0, 0), int2(int(blur.Width) - 1, int(blur.Height) - 1));
+ weightedThickness += blur.InputTexture.Load(int3(samplePixel, 0)) * weight;
+ weightSum += weight;
+ }
+ }
+
+ blur.OutputTexture[pixel] = weightedThickness / weightSum;
+}
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/FluidCommon.slang b/sources/Experiments/FluidTank/Assets/Shaders/FluidCommon.slang
new file mode 100644
index 00000000..da4fa9d4
--- /dev/null
+++ b/sources/Experiments/FluidTank/Assets/Shaders/FluidCommon.slang
@@ -0,0 +1,29 @@
+static const float PI = 3.14159265359;
+
+struct Particle
+{
+ float4 PositionDensity;
+
+ float4 VelocityLambda;
+};
+
+float3 ParticlePosition(Particle particle)
+{
+ return particle.PositionDensity.xyz;
+}
+
+float3 ParticleVelocity(Particle particle)
+{
+ return particle.VelocityLambda.xyz;
+}
+
+float Hash11(uint value)
+{
+ value ^= value >> 16;
+ value *= 0x7feb352du;
+ value ^= value >> 15;
+ value *= 0x846ca68bu;
+ value ^= value >> 16;
+
+ return float(value) / 4294967295.0;
+}
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang b/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
new file mode 100644
index 00000000..be78fb65
--- /dev/null
+++ b/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
@@ -0,0 +1,270 @@
+#include "SceneCommon.slang"
+
+struct CompositeConstants
+{
+ float4x4 InvView;
+
+ float4x4 InvProjection;
+
+ private float4 CameraPositionAndTime;
+
+ private float4 SunDirectionAndClarity;
+
+ private float4 WaterColorAndRefraction;
+
+ private float4 AbsorptionAndIor;
+
+ private uint4 ScreenAndRenderMode;
+
+ DescriptorHandle SceneColor;
+
+ DescriptorHandle SceneDepth;
+
+ DescriptorHandle FluidDepth;
+
+ DescriptorHandle Thickness;
+
+ DescriptorHandle Attributes;
+
+ DescriptorHandle Reflection;
+
+ DescriptorHandle Sampler;
+
+ property float3 CameraPosition
+ {
+ get {
+ return CameraPositionAndTime.xyz;
+ }
+ }
+
+ property float Time
+ {
+ get {
+ return CameraPositionAndTime.w;
+ }
+ }
+
+ property float3 SunDirection
+ {
+ get {
+ return SunDirectionAndClarity.xyz;
+ }
+ }
+
+ property float Clarity
+ {
+ get {
+ return SunDirectionAndClarity.w;
+ }
+ }
+
+ property float3 WaterColor
+ {
+ get {
+ return WaterColorAndRefraction.xyz;
+ }
+ }
+
+ property float RefractionStrength
+ {
+ get {
+ return WaterColorAndRefraction.w;
+ }
+ }
+
+ property float3 Absorption
+ {
+ get {
+ return AbsorptionAndIor.xyz;
+ }
+ }
+
+ property float Ior
+ {
+ get {
+ return AbsorptionAndIor.w;
+ }
+ }
+
+ property uint Width
+ {
+ get {
+ return ScreenAndRenderMode.x;
+ }
+ }
+
+ property uint Height
+ {
+ get {
+ return ScreenAndRenderMode.y;
+ }
+ }
+
+ property uint RenderMode
+ {
+ get {
+ return ScreenAndRenderMode.z;
+ }
+ }
+
+ property bool RayTracingEnabled
+ {
+ get {
+ return ScreenAndRenderMode.w != 0;
+ }
+ }
+};
+
+struct FullscreenOutput
+{
+ float4 Position : SV_POSITION;
+
+ float2 UV : TEXCOORD0;
+};
+
+ConstantBuffer composite;
+
+[shader("vertex")]
+FullscreenOutput FullscreenVS(uint vertexId: SV_VertexID)
+{
+ float2 uv = float2((vertexId << 1) & 2, vertexId & 2);
+
+ FullscreenOutput output;
+ output.Position = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
+ output.UV = uv;
+
+ return output;
+}
+
+float3 ViewRay(float2 uv)
+{
+ float2 ndc = uv * 2.0 - 1.0;
+ ndc.y = -ndc.y;
+ float4 target = mul(float4(ndc, 1.0, 1.0), composite.InvProjection);
+
+ return normalize(target.xyz / target.w);
+}
+
+float3 ReconstructViewPosition(float2 uv, float linearDepth)
+{
+ float3 ray = ViewRay(uv);
+
+ return ray * linearDepth / max(-ray.z, 0.0001);
+}
+
+float3 ReconstructWorldPosition(float2 uv, float linearDepth)
+{
+ return mul(float4(ReconstructViewPosition(uv, linearDepth), 1.0), composite.InvView).xyz;
+}
+
+float3 ReconstructNormal(float2 uv, float centerDepth)
+{
+ float2 texel = 5.0 / float2(composite.Width, composite.Height);
+ float depthLeft = composite.FluidDepth.SampleLevel(composite.Sampler, uv - float2(texel.x, 0.0), 0.0).r;
+ float depthRight = composite.FluidDepth.SampleLevel(composite.Sampler, uv + float2(texel.x, 0.0), 0.0).r;
+ float depthUp = composite.FluidDepth.SampleLevel(composite.Sampler, uv - float2(0.0, texel.y), 0.0).r;
+ float depthDown = composite.FluidDepth.SampleLevel(composite.Sampler, uv + float2(0.0, texel.y), 0.0).r;
+
+ depthLeft = depthLeft > 0.0 && abs(depthLeft - centerDepth) < 0.75 ? depthLeft : centerDepth;
+ depthRight = depthRight > 0.0 && abs(depthRight - centerDepth) < 0.75 ? depthRight : centerDepth;
+ depthUp = depthUp > 0.0 && abs(depthUp - centerDepth) < 0.75 ? depthUp : centerDepth;
+ depthDown = depthDown > 0.0 && abs(depthDown - centerDepth) < 0.75 ? depthDown : centerDepth;
+
+ float3 left = ReconstructViewPosition(uv - float2(texel.x, 0.0), depthLeft);
+ float3 right = ReconstructViewPosition(uv + float2(texel.x, 0.0), depthRight);
+ float3 up = ReconstructViewPosition(uv - float2(0.0, texel.y), depthUp);
+ float3 down = ReconstructViewPosition(uv + float2(0.0, texel.y), depthDown);
+ float3 normal = normalize(cross(down - up, right - left));
+
+ if (normal.z < 0.0)
+ {
+ normal = -normal;
+ }
+
+ return normal;
+}
+
+float3 SceneAt(float2 uv)
+{
+ float depth = composite.SceneDepth.SampleLevel(composite.Sampler, uv, 0.0).r;
+ if (depth <= 0.0)
+ {
+ float3 rayView = ViewRay(uv);
+ float3 rayWorld = normalize(mul(float4(rayView, 0.0), composite.InvView).xyz);
+
+ return SkyColor(rayWorld);
+ }
+
+ return composite.SceneColor.SampleLevel(composite.Sampler, uv, 0.0).rgb;
+}
+
+float4 ShadeWater(FullscreenOutput input)
+{
+ float2 uv = input.UV;
+
+ if (composite.RenderMode == 1)
+ {
+ return float4(ToSRGB(ACESFilm(SceneAt(uv))), 1.0);
+ }
+
+ float fluidDepth = composite.FluidDepth.SampleLevel(composite.Sampler, uv, 0.0).r;
+
+ if (fluidDepth <= 0.0)
+ {
+ float3 background = SceneAt(uv);
+
+ return float4(ToSRGB(ACESFilm(background)), 1.0);
+ }
+
+ float thickness = composite.Thickness.SampleLevel(composite.Sampler, uv, 0.0).r;
+ float4 attributes = composite.Attributes.SampleLevel(composite.Sampler, uv, 0.0);
+ float surfaceConfidence = smoothstep(0.105, 0.135, thickness);
+
+ if (surfaceConfidence <= 0.01)
+ {
+ return float4(ToSRGB(ACESFilm(SceneAt(uv))), 1.0);
+ }
+
+ float3 normalView = ReconstructNormal(uv, fluidDepth);
+ float3 normalWorld = normalize(mul(float4(normalView, 0.0), composite.InvView).xyz);
+
+ float3 worldPosition = ReconstructWorldPosition(uv, fluidDepth);
+ float3 viewDirection = normalize(composite.CameraPosition - worldPosition);
+ float ndotv = saturate(dot(normalWorld, viewDirection));
+ float f0 = pow((composite.Ior - 1.0) / (composite.Ior + 1.0), 2.0);
+ float fresnel = f0 + (1.0 - f0) * pow(1.0 - ndotv, 5.0);
+
+ float distortionScale = composite.RefractionStrength * (0.008 + min(thickness, 3.0) * 0.0035) / (0.35 + fluidDepth * 0.055);
+ float2 refractedUv = clamp(uv + normalView.xy * distortionScale, 0.001, 0.999);
+
+ float3 reflected = composite.RayTracingEnabled ? composite.Reflection.SampleLevel(composite.Sampler, uv, 0.0).rgb : SkyColor(reflect(-viewDirection, normalWorld));
+ float3 refracted = SceneAt(refractedUv);
+
+ float opticalDepth = min(thickness * 0.82, 6.0);
+ float3 transmission = exp(-composite.Absorption * opticalDepth / max(composite.Clarity, 0.05));
+ float3 inScatter = composite.WaterColor * (1.0 - transmission) * (0.34 + 0.26 * saturate(normalWorld.y));
+ refracted = refracted * transmission + inScatter;
+
+ float3 lightDirection = normalize(-composite.SunDirection);
+ float3 halfVector = normalize(viewDirection + lightDirection);
+ float sunSpecular = pow(saturate(dot(normalWorld, halfVector)), 240.0) * 3.2;
+
+ float densityEdge = saturate((0.68 - attributes.y) * 3.0);
+ float speedFoam = saturate((attributes.x - 0.28) * 1.8);
+ float thinFoam = saturate((0.22 - thickness) * 5.0);
+ float foam = saturate(speedFoam * thinFoam * 0.72 + densityEdge * speedFoam * 0.22);
+
+ float3 color = lerp(refracted, reflected, fresnel);
+ color += sunSpecular * float3(1.0, 0.94, 0.78);
+ color = lerp(color, float3(0.76, 0.92, 0.96), foam * 0.72);
+
+ color = lerp(SceneAt(uv), color, surfaceConfidence);
+
+ return float4(ToSRGB(ACESFilm(color)), 1.0);
+}
+
+[shader("fragment")]
+float4 CompositeFS(FullscreenOutput input) : SV_TARGET
+{
+ return ShadeWater(input);
+}
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/FluidReflection.slang b/sources/Experiments/FluidTank/Assets/Shaders/FluidReflection.slang
new file mode 100644
index 00000000..d46c804f
--- /dev/null
+++ b/sources/Experiments/FluidTank/Assets/Shaders/FluidReflection.slang
@@ -0,0 +1,185 @@
+#include "SceneCommon.slang"
+
+struct ReflectionConstants
+{
+ float4x4 InvView;
+
+ float4x4 InvProjection;
+
+ private float4 CameraPositionAndTime;
+
+ private float4 SunDirectionAndPadding;
+
+ private uint4 DimensionsAndPadding;
+
+ DescriptorHandle> FluidDepth;
+
+ DescriptorHandle Scene;
+
+ DescriptorHandle> Vertices;
+
+ DescriptorHandle> Indices;
+
+ DescriptorHandle> Materials;
+
+ DescriptorHandle> OutputTexture;
+
+ property float3 CameraPosition
+ {
+ get {
+ return CameraPositionAndTime.xyz;
+ }
+ }
+
+ property float Time
+ {
+ get {
+ return CameraPositionAndTime.w;
+ }
+ }
+
+ property float3 SunDirection
+ {
+ get {
+ return SunDirectionAndPadding.xyz;
+ }
+ }
+
+ property uint Width
+ {
+ get {
+ return DimensionsAndPadding.x;
+ }
+ }
+
+ property uint Height
+ {
+ get {
+ return DimensionsAndPadding.y;
+ }
+ }
+};
+
+ConstantBuffer reflection;
+
+float3 ViewRay(float2 uv)
+{
+ float2 ndc = uv * 2.0 - 1.0;
+ ndc.y = -ndc.y;
+ float4 target = mul(float4(ndc, 1.0, 1.0), reflection.InvProjection);
+
+ return normalize(target.xyz / target.w);
+}
+
+float3 ViewPosition(float2 uv, float linearDepth)
+{
+ float3 ray = ViewRay(uv);
+
+ return ray * linearDepth / max(-ray.z, 0.0001);
+}
+
+float3 WorldPosition(float2 uv, float linearDepth)
+{
+ return mul(float4(ViewPosition(uv, linearDepth), 1.0), reflection.InvView).xyz;
+}
+
+float3 SurfaceNormal(uint2 pixel, float depth)
+{
+ const uint Radius = 5;
+ uint2 leftPixel = uint2(pixel.x > Radius ? pixel.x - Radius : 0, pixel.y);
+ uint2 rightPixel = uint2(min(pixel.x + Radius, reflection.Width - 1), pixel.y);
+ uint2 upPixel = uint2(pixel.x, pixel.y > Radius ? pixel.y - Radius : 0);
+ uint2 downPixel = uint2(pixel.x, min(pixel.y + Radius, reflection.Height - 1));
+ float depthLeft = reflection.FluidDepth.Load(int3(leftPixel, 0));
+ float depthRight = reflection.FluidDepth.Load(int3(rightPixel, 0));
+ float depthUp = reflection.FluidDepth.Load(int3(upPixel, 0));
+ float depthDown = reflection.FluidDepth.Load(int3(downPixel, 0));
+
+ depthLeft = depthLeft > 0.0 && abs(depthLeft - depth) < 0.75 ? depthLeft : depth;
+ depthRight = depthRight > 0.0 && abs(depthRight - depth) < 0.75 ? depthRight : depth;
+ depthUp = depthUp > 0.0 && abs(depthUp - depth) < 0.75 ? depthUp : depth;
+ depthDown = depthDown > 0.0 && abs(depthDown - depth) < 0.75 ? depthDown : depth;
+
+ float2 dimensions = float2(reflection.Width, reflection.Height);
+ float3 left = ViewPosition((float2(leftPixel) + 0.5) / dimensions, depthLeft);
+ float3 right = ViewPosition((float2(rightPixel) + 0.5) / dimensions, depthRight);
+ float3 up = ViewPosition((float2(upPixel) + 0.5) / dimensions, depthUp);
+ float3 down = ViewPosition((float2(downPixel) + 0.5) / dimensions, depthDown);
+ float3 normalVector = cross(down - up, right - left);
+ float normalLengthSquared = dot(normalVector, normalVector);
+ float3 normal = normalLengthSquared > 0.0 ? normalVector * rsqrt(normalLengthSquared) : float3(0.0, 0.0, 1.0);
+
+ if (normal.z < 0.0)
+ {
+ normal = -normal;
+ }
+
+ return normalize(mul(float4(normal, 0.0), reflection.InvView).xyz);
+}
+
+float3 TraceScene(float3 origin, float3 direction)
+{
+ RayDesc ray;
+ ray.Origin = origin;
+ ray.Direction = direction;
+ ray.TMin = 0.02;
+ ray.TMax = 120.0;
+
+ RayQuery query;
+ query.TraceRayInline(reflection.Scene, RAY_FLAG_CULL_BACK_FACING_TRIANGLES, 0xFF, ray);
+ while (query.Proceed())
+ {
+ }
+
+ if (query.CommittedStatus() == COMMITTED_NOTHING)
+ {
+ return SkyColor(direction);
+ }
+
+ uint primitive = query.CommittedPrimitiveIndex();
+ float2 barycentric = query.CommittedTriangleBarycentrics();
+ uint i0 = reflection.Indices[primitive * 3 + 0];
+ uint i1 = reflection.Indices[primitive * 3 + 1];
+ uint i2 = reflection.Indices[primitive * 3 + 2];
+ SceneVertex v0 = reflection.Vertices[i0];
+ SceneVertex v1 = reflection.Vertices[i1];
+ SceneVertex v2 = reflection.Vertices[i2];
+ float3 weights = float3(1.0 - barycentric.x - barycentric.y, barycentric.x, barycentric.y);
+ float3 normal = normalize(v0.Normal * weights.x + v1.Normal * weights.y + v2.Normal * weights.z);
+ SceneMaterial material = reflection.Materials[v0.MaterialId];
+ float3 lightDirection = normalize(-reflection.SunDirection);
+ float diffuse = 0.18 + saturate(dot(normal, lightDirection)) * 1.65;
+ float3 environment = SkyColor(reflect(direction, normal));
+ float3 f0 = lerp(float3(0.035), material.Albedo, material.Metallic);
+ float3 fresnel = FresnelSchlick(saturate(dot(-direction, normal)), f0);
+
+ return material.Albedo * diffuse * (1.0 - material.Metallic) + environment * fresnel * (1.0 - material.Roughness * 0.7);
+}
+
+[shader("compute")]
+[numthreads(16, 16, 1)]
+void ReflectionCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ uint2 pixel = dispatchThreadID.xy;
+ if (pixel.x >= reflection.Width || pixel.y >= reflection.Height)
+ {
+ return;
+ }
+
+ float depth = reflection.FluidDepth.Load(int3(pixel, 0));
+ if (depth <= 0.0)
+ {
+ reflection.OutputTexture[pixel] = float4(0.0, 0.0, 0.0, 0.0);
+
+ return;
+ }
+
+ float2 uv = (float2(pixel) + 0.5) / float2(reflection.Width, reflection.Height);
+ float3 position = WorldPosition(uv, depth);
+ float3 normal = SurfaceNormal(pixel, depth);
+ float3 viewDirection = normalize(reflection.CameraPosition - position);
+ float3 direction = reflect(-viewDirection, normal);
+ float3 color = TraceScene(position + normal * 0.025, direction);
+
+ reflection.OutputTexture[pixel] = float4(color, 1.0);
+}
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/FluidSimulation.slang b/sources/Experiments/FluidTank/Assets/Shaders/FluidSimulation.slang
new file mode 100644
index 00000000..d9893e6c
--- /dev/null
+++ b/sources/Experiments/FluidTank/Assets/Shaders/FluidSimulation.slang
@@ -0,0 +1,1000 @@
+#include "FluidCommon.slang"
+
+static const uint CellAir = 0;
+static const uint CellFluid = 1;
+static const uint CellSolid = 2;
+static const float AccumulationScale = 65536.0;
+static const float ParticleSpacingScale = 1.67;
+
+struct SimulationConstants
+{
+ private float4 TankMinAndTimeStep;
+
+ private float4 TankMaxAndTime;
+
+ private float4 GridParameters;
+
+ private float4 WaveAndInteraction;
+
+ private float4 InteractionOriginAndRadius;
+
+ private float4 InteractionDirectionAndDensity;
+
+ private uint4 Counts;
+
+ private uint4 GridAndPressure;
+
+ private uint4 DamAndSubsteps;
+
+ DescriptorHandle> Particles;
+
+ DescriptorHandle> PreviousPositions;
+
+ DescriptorHandle> ParticleAffine;
+
+ DescriptorHandle> GridAccumulation;
+
+ DescriptorHandle> GridVelocity;
+
+ DescriptorHandle> GridVelocityOld;
+
+ DescriptorHandle> CellTypes;
+
+ DescriptorHandle> Divergence;
+
+ DescriptorHandle> PressureA;
+
+ property float3 TankMin
+ {
+ get {
+ return TankMinAndTimeStep.xyz;
+ }
+ }
+
+ property float TimeStep
+ {
+ get {
+ return TankMinAndTimeStep.w;
+ }
+ }
+
+ property float3 TankMax
+ {
+ get {
+ return TankMaxAndTime.xyz;
+ }
+ }
+
+ property float Time
+ {
+ get {
+ return TankMaxAndTime.w;
+ }
+ }
+
+ property float GridSpacing
+ {
+ get {
+ return GridParameters.x;
+ }
+ }
+
+ property float InverseGridSpacing
+ {
+ get {
+ return GridParameters.y;
+ }
+ }
+
+ property float FlipRatio
+ {
+ get {
+ return GridParameters.z;
+ }
+ }
+
+ property float VelocityDamping
+ {
+ get {
+ return GridParameters.w;
+ }
+ }
+
+ property float WaveAmplitude
+ {
+ get {
+ return WaveAndInteraction.x;
+ }
+ }
+
+ property float WaveFrequency
+ {
+ get {
+ return WaveAndInteraction.y;
+ }
+ }
+
+ property float InteractionRadius
+ {
+ get {
+ return WaveAndInteraction.z;
+ }
+ }
+
+ property float InteractionStrength
+ {
+ get {
+ return WaveAndInteraction.w;
+ }
+ }
+
+ property float3 InteractionOrigin
+ {
+ get {
+ return InteractionOriginAndRadius.xyz;
+ }
+ }
+
+ property float ParticleRadius
+ {
+ get {
+ return InteractionOriginAndRadius.w;
+ }
+ }
+
+ property float3 InteractionDirection
+ {
+ get {
+ return InteractionDirectionAndDensity.xyz;
+ }
+ }
+
+ property float RestDensity
+ {
+ get {
+ return InteractionDirectionAndDensity.w;
+ }
+ }
+
+ property uint ParticleCount
+ {
+ get {
+ return Counts.x;
+ }
+ }
+
+ property uint CellCount
+ {
+ get {
+ return Counts.y;
+ }
+ }
+
+ property uint GridPointCount
+ {
+ get {
+ return Counts.z;
+ }
+ }
+
+ property bool WaveMakerEnabled
+ {
+ get {
+ return Counts.w != 0;
+ }
+ }
+
+ property uint3 GridDimensions
+ {
+ get {
+ return GridAndPressure.xyz;
+ }
+ }
+
+ property uint PressureIterations
+ {
+ get {
+ return GridAndPressure.w;
+ }
+ }
+
+ property uint3 DamDimensions
+ {
+ get {
+ return DamAndSubsteps.xyz;
+ }
+ }
+
+ property uint Substeps
+ {
+ get {
+ return DamAndSubsteps.w;
+ }
+ }
+};
+
+ConstantBuffer simulation;
+
+float RoundedBoxSdf(float3 position, float3 center, float3 halfExtents, float radius)
+{
+ float3 q = abs(position - center) - halfExtents + radius;
+
+ return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - radius;
+}
+
+float CylinderSdf(float3 position, float3 center, float radius, float halfHeight)
+{
+ float2 d = abs(float2(length(position.xz - center.xz), position.y - center.y)) - float2(radius, halfHeight);
+
+ return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
+}
+
+float OrientedBoxSdf(float3 position, float3 center, float3 halfExtents, float angle)
+{
+ float cosine = cos(angle);
+ float sine = sin(angle);
+ float3 local = position - center;
+ local.xy = float2(cosine * local.x + sine * local.y, -sine * local.x + cosine * local.y);
+ float3 q = abs(local) - halfExtents;
+
+ return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
+}
+
+float ObstacleSdf(float3 position)
+{
+ float roundedBox = RoundedBoxSdf(position, float3(-1.45, 0.82, 0.95), float3(0.72, 0.82, 0.72), 0.0);
+ float cylinder = CylinderSdf(position, float3(1.15, 0.92, -0.85), 0.62, 0.92);
+ float ramp = OrientedBoxSdf(position, float3(3.45, 0.62, 0.55), float3(1.15, 0.16, 1.05), -0.35);
+
+ return min(roundedBox, min(cylinder, ramp));
+}
+
+float3 ObstacleNormal(float3 position)
+{
+ float epsilon = simulation.ParticleRadius * 0.1;
+ float3 x = float3(epsilon, 0.0, 0.0);
+ float3 y = float3(0.0, epsilon, 0.0);
+ float3 z = float3(0.0, 0.0, epsilon);
+
+ return normalize(float3(ObstacleSdf(position + x) - ObstacleSdf(position - x),
+ ObstacleSdf(position + y) - ObstacleSdf(position - y),
+ ObstacleSdf(position + z) - ObstacleSdf(position - z)));
+}
+
+bool CellInBounds(int3 cell)
+{
+ return all(cell >= 0) && all(cell < int3(simulation.GridDimensions));
+}
+
+bool GridPointInBounds(int3 point)
+{
+ return all(point >= 0) && all(point <= int3(simulation.GridDimensions));
+}
+
+uint CellIndex(int3 cell)
+{
+ return uint(cell.x) + uint(cell.y) * simulation.GridDimensions.x + uint(cell.z) * simulation.GridDimensions.x * simulation.GridDimensions.y;
+}
+
+uint GridIndex(int3 point)
+{
+ uint width = simulation.GridDimensions.x + 1;
+ uint height = simulation.GridDimensions.y + 1;
+
+ return uint(point.x) + uint(point.y) * width + uint(point.z) * width * height;
+}
+
+int3 CellCoordinates(uint index)
+{
+ uint slice = simulation.GridDimensions.x * simulation.GridDimensions.y;
+ uint z = index / slice;
+ uint remainder = index - z * slice;
+ uint y = remainder / simulation.GridDimensions.x;
+ uint x = remainder - y * simulation.GridDimensions.x;
+
+ return int3(x, y, z);
+}
+
+int3 PressureCellCoordinates(uint index, uint parity)
+{
+ uint pairsPerRow = (simulation.GridDimensions.x + 1) / 2;
+ uint z = index / (pairsPerRow * simulation.GridDimensions.y);
+ uint remainder = index - z * pairsPerRow * simulation.GridDimensions.y;
+ uint y = remainder / pairsPerRow;
+ uint pairX = remainder - y * pairsPerRow;
+ uint x = pairX * 2 + ((y + z + parity) & 1);
+
+ return int3(x, y, z);
+}
+
+int3 GridCoordinates(uint index)
+{
+ uint width = simulation.GridDimensions.x + 1;
+ uint height = simulation.GridDimensions.y + 1;
+ uint slice = width * height;
+ uint z = index / slice;
+ uint remainder = index - z * slice;
+ uint y = remainder / width;
+ uint x = remainder - y * width;
+
+ return int3(x, y, z);
+}
+
+uint CellTypeAt(int3 cell)
+{
+ if (!CellInBounds(cell))
+ {
+ return CellSolid;
+ }
+
+ return simulation.CellTypes[CellIndex(cell)];
+}
+
+float PressureAt(int3 cell)
+{
+ return CellTypeAt(cell) == CellFluid ? simulation.PressureA[CellIndex(cell)] : 0.0;
+}
+
+float ParticleDensityAt(int3 cell)
+{
+ uint index = GridIndex(cell);
+ uint width = simulation.GridDimensions.x + 1;
+ uint slice = width * (simulation.GridDimensions.y + 1);
+ float weight = float(simulation.GridAccumulation[simulation.GridPointCount + index]) + float(simulation.GridAccumulation[simulation.GridPointCount + index + 1]) + float(simulation.GridAccumulation[simulation.GridPointCount * 3 + index]) + float(simulation.GridAccumulation[simulation.GridPointCount * 3 + index + width]) + float(simulation.GridAccumulation[simulation.GridPointCount * 5 + index]) + float(simulation.GridAccumulation[simulation.GridPointCount * 5 + index + slice]);
+
+ return weight / (6.0 * AccumulationScale);
+}
+
+float3 FaceOffset(uint component)
+{
+ if (component == 0)
+ {
+ return float3(0.0, 0.5, 0.5);
+ }
+
+ if (component == 1)
+ {
+ return float3(0.5, 0.0, 0.5);
+ }
+
+ return float3(0.5, 0.5, 0.0);
+}
+
+bool FaceIsValid(uint component, int3 point)
+{
+ if (!GridPointInBounds(point))
+ {
+ return false;
+ }
+
+ if (component == 0)
+ {
+ return point.y < int(simulation.GridDimensions.y) && point.z < int(simulation.GridDimensions.z);
+ }
+
+ if (component == 1)
+ {
+ return point.x < int(simulation.GridDimensions.x) && point.z < int(simulation.GridDimensions.z);
+ }
+
+ return point.x < int(simulation.GridDimensions.x) && point.y < int(simulation.GridDimensions.y);
+}
+
+void FaceCells(uint component, int3 point, out int3 negativeCell, out int3 positiveCell)
+{
+ negativeCell = point;
+ positiveCell = point;
+
+ if (component == 0)
+ {
+ negativeCell.x--;
+ }
+ else if (component == 1)
+ {
+ negativeCell.y--;
+ }
+ else
+ {
+ negativeCell.z--;
+ }
+}
+
+bool FaceIsOpen(uint component, int3 point)
+{
+ if (!FaceIsValid(component, point))
+ {
+ return false;
+ }
+
+ int3 negativeCell;
+ int3 positiveCell;
+ FaceCells(component, point, negativeCell, positiveCell);
+
+ return CellTypeAt(negativeCell) != CellSolid && CellTypeAt(positiveCell) != CellSolid;
+}
+
+bool FaceTouchesFluid(uint component, int3 point)
+{
+ int3 negativeCell;
+ int3 positiveCell;
+ FaceCells(component, point, negativeCell, positiveCell);
+
+ return CellTypeAt(negativeCell) == CellFluid || CellTypeAt(positiveCell) == CellFluid;
+}
+
+float3 QuadraticWeights(float value)
+{
+ return float3(0.5 * (1.5 - value) * (1.5 - value),
+ 0.75 - (value - 1.0) * (value - 1.0),
+ 0.5 * (value - 0.5) * (value - 0.5));
+}
+
+void ScatterComponent(float3 position, float3 velocity, float3 affineRow, uint component)
+{
+ float3 offset = FaceOffset(component);
+ float3 coordinate = (position - simulation.TankMin) * simulation.InverseGridSpacing - offset;
+ int3 base = int3(floor(coordinate - 0.5));
+ float3 fractional = coordinate - float3(base);
+ float3 weightX = QuadraticWeights(fractional.x);
+ float3 weightY = QuadraticWeights(fractional.y);
+ float3 weightZ = QuadraticWeights(fractional.z);
+
+ for (int z = 0; z < 3; z++)
+ {
+ for (int y = 0; y < 3; y++)
+ {
+ for (int x = 0; x < 3; x++)
+ {
+ int3 point = base + int3(x, y, z);
+ if (!FaceIsValid(component, point))
+ {
+ continue;
+ }
+
+ float weight = weightX[x] * weightY[y] * weightZ[z];
+ float3 facePosition = simulation.TankMin + (float3(point) + offset) * simulation.GridSpacing;
+ float momentum = clamp(velocity[component] + dot(affineRow, facePosition - position), -12.0, 12.0);
+ uint index = GridIndex(point);
+ uint valueOffset = component * 2 * simulation.GridPointCount;
+ uint weightOffset = valueOffset + simulation.GridPointCount;
+ int fixedWeight = int(round(weight * AccumulationScale));
+ int fixedMomentum = int(round(momentum * weight * AccumulationScale));
+ InterlockedAdd(simulation.GridAccumulation[valueOffset + index], fixedMomentum);
+ InterlockedAdd(simulation.GridAccumulation[weightOffset + index], fixedWeight);
+ }
+ }
+ }
+}
+
+bool FaceCanSample(uint component, int3 point)
+{
+ if (!FaceIsValid(component, point))
+ {
+ return false;
+ }
+
+ int3 negativeCell;
+ int3 positiveCell;
+ FaceCells(component, point, negativeCell, positiveCell);
+ uint negativeType = CellTypeAt(negativeCell);
+ uint positiveType = CellTypeAt(positiveCell);
+
+ return negativeType != CellSolid && positiveType != CellSolid && (negativeType == CellFluid || positiveType == CellFluid);
+}
+
+void SampleComponentPair(float3 position,
+ uint component,
+ out float currentVelocity,
+ out float previousVelocity,
+ out float3 affineRow)
+{
+ float3 offset = FaceOffset(component);
+ float3 coordinate = (position - simulation.TankMin) * simulation.InverseGridSpacing - offset;
+ int3 base = int3(floor(coordinate - 0.5));
+ float3 fractional = coordinate - float3(base);
+ float3 weightX = QuadraticWeights(fractional.x);
+ float3 weightY = QuadraticWeights(fractional.y);
+ float3 weightZ = QuadraticWeights(fractional.z);
+ currentVelocity = 0.0;
+ previousVelocity = 0.0;
+ float3 moment = float3(0.0, 0.0, 0.0);
+ float3 firstMoment = float3(0.0, 0.0, 0.0);
+ float3 secondMoment = float3(0.0, 0.0, 0.0);
+ float weightSum = 0.0;
+
+ for (int z = 0; z < 3; z++)
+ {
+ for (int y = 0; y < 3; y++)
+ {
+ for (int x = 0; x < 3; x++)
+ {
+ int3 point = base + int3(x, y, z);
+ if (!FaceCanSample(component, point))
+ {
+ continue;
+ }
+
+ float weight = weightX[x] * weightY[y] * weightZ[z];
+ uint index = GridIndex(point);
+ float faceVelocity = simulation.GridVelocity[index][component];
+ float oldFaceVelocity = simulation.GridVelocityOld[index][component];
+ float3 facePosition = simulation.TankMin + (float3(point) + offset) * simulation.GridSpacing;
+ float3 delta = facePosition - position;
+ currentVelocity += weight * faceVelocity;
+ previousVelocity += weight * oldFaceVelocity;
+ moment += weight * faceVelocity * delta;
+ firstMoment += weight * delta;
+ secondMoment += weight * delta * delta;
+ weightSum += weight;
+ }
+ }
+ }
+
+ if (weightSum <= 0.0001)
+ {
+ currentVelocity = 0.0;
+ previousVelocity = 0.0;
+ affineRow = float3(0.0, 0.0, 0.0);
+
+ return;
+ }
+
+ currentVelocity /= weightSum;
+ previousVelocity /= weightSum;
+ float3 centeredMoment = moment - currentVelocity * firstMoment;
+ float3 variance = secondMoment - firstMoment * firstMoment / weightSum;
+ affineRow = centeredMoment / max(variance, simulation.GridSpacing * simulation.GridSpacing * 0.02);
+ affineRow = clamp(affineRow, -24.0, 24.0);
+}
+
+float SampleGridComponent(float3 position, uint component)
+{
+ float3 offset = FaceOffset(component);
+ float3 coordinate = (position - simulation.TankMin) * simulation.InverseGridSpacing - offset;
+ int3 base = int3(floor(coordinate - 0.5));
+ float3 fractional = coordinate - float3(base);
+ float3 weightX = QuadraticWeights(fractional.x);
+ float3 weightY = QuadraticWeights(fractional.y);
+ float3 weightZ = QuadraticWeights(fractional.z);
+ float velocity = 0.0;
+ float weightSum = 0.0;
+
+ for (int z = 0; z < 3; z++)
+ {
+ for (int y = 0; y < 3; y++)
+ {
+ for (int x = 0; x < 3; x++)
+ {
+ int3 point = base + int3(x, y, z);
+ if (!FaceCanSample(component, point))
+ {
+ continue;
+ }
+
+ float weight = weightX[x] * weightY[y] * weightZ[z];
+ velocity += weight * simulation.GridVelocity[GridIndex(point)][component];
+ weightSum += weight;
+ }
+ }
+ }
+
+ return weightSum > 0.0001 ? velocity / weightSum : 0.0;
+}
+
+float3 SampleGridVelocity(float3 position)
+{
+ return float3(SampleGridComponent(position, 0),
+ SampleGridComponent(position, 1),
+ SampleGridComponent(position, 2));
+}
+
+bool ResolveCollisions(inout float3 position, inout float3 velocity)
+{
+ float3 minimum = simulation.TankMin + simulation.ParticleRadius;
+ float3 maximum = simulation.TankMax - simulation.ParticleRadius;
+ bool collided = false;
+
+ if (position.x < minimum.x)
+ {
+ position.x = minimum.x;
+ velocity.x = max(velocity.x, 0.0);
+ collided = true;
+ }
+ else if (position.x > maximum.x)
+ {
+ position.x = maximum.x;
+ velocity.x = min(velocity.x, 0.0);
+ collided = true;
+ }
+
+ if (position.y < minimum.y)
+ {
+ position.y = minimum.y;
+ velocity.y = max(velocity.y, 0.0);
+ velocity.xz *= 0.998;
+ collided = true;
+ }
+ else if (position.y > maximum.y)
+ {
+ position.y = maximum.y;
+ velocity.y = min(velocity.y, 0.0);
+ collided = true;
+ }
+
+ if (position.z < minimum.z)
+ {
+ position.z = minimum.z;
+ velocity.z = max(velocity.z, 0.0);
+ collided = true;
+ }
+ else if (position.z > maximum.z)
+ {
+ position.z = maximum.z;
+ velocity.z = min(velocity.z, 0.0);
+ collided = true;
+ }
+
+ float distance = ObstacleSdf(position);
+ if (distance < simulation.ParticleRadius)
+ {
+ float3 normal = ObstacleNormal(position);
+ position += normal * (simulation.ParticleRadius - distance);
+ velocity -= normal * min(dot(velocity, normal), 0.0);
+ velocity *= 0.999;
+ collided = true;
+ }
+
+ position = clamp(position, minimum, maximum);
+
+ return collided;
+}
+
+[shader("compute")]
+[numthreads(256, 1, 1)]
+void ResetCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ uint index = dispatchThreadID.x;
+ if (index >= simulation.ParticleCount)
+ {
+ return;
+ }
+
+ uint x = index % simulation.DamDimensions.x;
+ uint y = (index / simulation.DamDimensions.x) % simulation.DamDimensions.y;
+ uint z = index / (simulation.DamDimensions.x * simulation.DamDimensions.y);
+ float spacing = simulation.ParticleRadius * ParticleSpacingScale;
+ float3 jitter = float3(Hash11(index * 3 + 0), Hash11(index * 3 + 1), Hash11(index * 3 + 2)) - 0.5;
+ float3 position = simulation.TankMin + simulation.ParticleRadius * float3(1.05, 1.25, 1.25);
+ position += float3(x, y, z) * spacing + jitter * spacing * 0.02;
+
+ Particle particle;
+ particle.PositionDensity = float4(position, simulation.RestDensity);
+ particle.VelocityLambda = float4(0.0, 0.0, 0.0, 0.0);
+ simulation.Particles[index] = particle;
+ simulation.PreviousPositions[index] = float4(position, 0.0);
+ simulation.ParticleAffine[index * 3 + 0] = float4(0.0, 0.0, 0.0, 0.0);
+ simulation.ParticleAffine[index * 3 + 1] = float4(0.0, 0.0, 0.0, 0.0);
+ simulation.ParticleAffine[index * 3 + 2] = float4(0.0, 0.0, 0.0, 0.0);
+}
+
+[shader("compute")]
+[numthreads(256, 1, 1)]
+void InitializeGridCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ uint index = dispatchThreadID.x;
+ if (index < simulation.CellCount)
+ {
+ int3 cell = CellCoordinates(index);
+ float3 center = simulation.TankMin + (float3(cell) + 0.5) * simulation.GridSpacing;
+ simulation.CellTypes[index] = ObstacleSdf(center) < 0.0 ? CellSolid : CellAir;
+ simulation.PressureA[index] = 0.0;
+ }
+}
+
+[shader("compute")]
+[numthreads(256, 1, 1)]
+void ClearGridCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ uint index = dispatchThreadID.x;
+
+ if (index < simulation.GridPointCount * 6)
+ {
+ simulation.GridAccumulation[index] = 0;
+ }
+
+ if (index < simulation.CellCount)
+ {
+ simulation.CellTypes[index] = simulation.CellTypes[index] == CellSolid ? CellSolid : CellAir;
+ }
+}
+
+void ParticleToGrid(uint index, bool savePreviousPosition)
+{
+ if (index >= simulation.ParticleCount)
+ {
+ return;
+ }
+
+ Particle particle = simulation.Particles[index];
+ float3 position = ParticlePosition(particle);
+ float3 velocity = ParticleVelocity(particle);
+
+ if (savePreviousPosition)
+ {
+ simulation.PreviousPositions[index] = float4(position, 0.0);
+ }
+
+ int3 cell = clamp(int3(floor((position - simulation.TankMin) * simulation.InverseGridSpacing)),
+ int3(0, 0, 0), int3(simulation.GridDimensions) - 1);
+ InterlockedMax(simulation.CellTypes[CellIndex(cell)], CellFluid);
+ ScatterComponent(position, velocity, simulation.ParticleAffine[index * 3 + 0].xyz, 0);
+ ScatterComponent(position, velocity, simulation.ParticleAffine[index * 3 + 1].xyz, 1);
+ ScatterComponent(position, velocity, simulation.ParticleAffine[index * 3 + 2].xyz, 2);
+}
+
+[shader("compute")]
+[numthreads(128, 1, 1)]
+void BeginParticleToGridCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ ParticleToGrid(dispatchThreadID.x, true);
+}
+
+[shader("compute")]
+[numthreads(128, 1, 1)]
+void ParticleToGridCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ ParticleToGrid(dispatchThreadID.x, false);
+}
+
+[shader("compute")]
+[numthreads(256, 1, 1)]
+void NormalizeAndApplyForcesCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ uint index = dispatchThreadID.x;
+ if (index >= simulation.GridPointCount)
+ {
+ return;
+ }
+
+ int3 point = GridCoordinates(index);
+ float3 velocity = float3(0.0, 0.0, 0.0);
+
+ for (uint component = 0; component < 3; component++)
+ {
+ uint valueOffset = component * 2 * simulation.GridPointCount;
+ uint weightOffset = valueOffset + simulation.GridPointCount;
+ int weight = simulation.GridAccumulation[weightOffset + index];
+
+ if (weight > 0 && FaceIsOpen(component, point))
+ {
+ velocity[component] = float(simulation.GridAccumulation[valueOffset + index]) / float(weight);
+ }
+ }
+
+ simulation.GridVelocityOld[index] = float4(velocity, 0.0);
+
+ if (FaceIsOpen(1, point) && FaceTouchesFluid(1, point))
+ {
+ velocity.y -= 9.81 * simulation.TimeStep;
+ }
+
+ if (simulation.WaveMakerEnabled && point.x <= 2 && FaceIsOpen(0, point) && FaceTouchesFluid(0, point))
+ {
+ float phase = simulation.Time * simulation.WaveFrequency * 2.0 * PI + float(point.z) * 0.13;
+ float targetVelocity = simulation.WaveAmplitude * simulation.WaveFrequency * 2.0 * PI * cos(phase);
+ velocity.x = lerp(velocity.x, targetVelocity, exp(-float(point.x) * 0.65));
+ }
+
+ if (simulation.InteractionStrength > 0.0)
+ {
+ for (uint component = 0; component < 3; component++)
+ {
+ if (!FaceIsOpen(component, point) || !FaceTouchesFluid(component, point))
+ {
+ continue;
+ }
+
+ float3 facePosition = simulation.TankMin + (float3(point) + FaceOffset(component)) * simulation.GridSpacing;
+ float rayDistance = dot(facePosition - simulation.InteractionOrigin, simulation.InteractionDirection);
+ float3 closestPoint = simulation.InteractionOrigin + simulation.InteractionDirection * max(rayDistance, 0.0);
+ float distance = length(facePosition - closestPoint);
+
+ if (rayDistance > 0.0 && distance < simulation.InteractionRadius)
+ {
+ float falloff = 1.0 - distance / simulation.InteractionRadius;
+ velocity[component] += (simulation.InteractionDirection[component] + (component == 1 ? 0.45 : 0.0)) * simulation.InteractionStrength * falloff / float(simulation.Substeps);
+ }
+ }
+ }
+
+ simulation.GridVelocity[index] = float4(velocity, 0.0);
+}
+
+[shader("compute")]
+[numthreads(256, 1, 1)]
+void DivergenceCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ uint index = dispatchThreadID.x;
+ if (index >= simulation.CellCount)
+ {
+ return;
+ }
+
+ if (simulation.CellTypes[index] != CellFluid)
+ {
+ simulation.Divergence[index] = 0.0;
+ simulation.PressureA[index] = 0.0;
+
+ return;
+ }
+
+ int3 cell = CellCoordinates(index);
+ float3 negativeVelocity = simulation.GridVelocity[GridIndex(cell)].xyz;
+ float velocityLeft = negativeVelocity.x;
+ float velocityRight = simulation.GridVelocity[GridIndex(cell + int3(1, 0, 0))].x;
+ float velocityBottom = negativeVelocity.y;
+ float velocityTop = simulation.GridVelocity[GridIndex(cell + int3(0, 1, 0))].y;
+ float velocityBack = negativeVelocity.z;
+ float velocityFront = simulation.GridVelocity[GridIndex(cell + int3(0, 0, 1))].z;
+ float divergence = (velocityRight - velocityLeft + velocityTop - velocityBottom + velocityFront - velocityBack) * simulation.InverseGridSpacing;
+ float particleSpacing = simulation.ParticleRadius * ParticleSpacingScale;
+ float targetDensity = simulation.GridSpacing * simulation.GridSpacing * simulation.GridSpacing / (particleSpacing * particleSpacing * particleSpacing);
+ float compression = max(ParticleDensityAt(cell) / targetDensity - 1.0, 0.0);
+ float expansion = min(compression * 0.1 / simulation.TimeStep, 2.0);
+ simulation.Divergence[index] = divergence - expansion;
+}
+
+void RelaxPressure(int3 cell)
+{
+ if (any(cell >= int3(simulation.GridDimensions)))
+ {
+ return;
+ }
+
+ uint index = CellIndex(cell);
+ if (simulation.CellTypes[index] != CellFluid)
+ {
+ return;
+ }
+
+ int3 neighbors[6] =
+ {
+ cell + int3(-1, 0, 0), cell + int3(1, 0, 0),
+ cell + int3(0, -1, 0), cell + int3(0, 1, 0),
+ cell + int3(0, 0, -1), cell + int3(0, 0, 1)
+ };
+ float sum = 0.0;
+ float denominator = 0.0;
+
+ for (uint neighborIndex = 0; neighborIndex < 6; neighborIndex++)
+ {
+ uint type = CellTypeAt(neighbors[neighborIndex]);
+ if (type == CellSolid)
+ {
+ continue;
+ }
+
+ denominator += 1.0;
+ if (type == CellFluid)
+ {
+ sum += simulation.PressureA[CellIndex(neighbors[neighborIndex])];
+ }
+ }
+
+ float rightHandSide = simulation.Divergence[index] * simulation.GridSpacing * simulation.GridSpacing / simulation.TimeStep;
+ float pressure = denominator > 0.0 ? (sum - rightHandSide) / denominator : 0.0;
+ simulation.PressureA[index] = lerp(simulation.PressureA[index], pressure, 1.6);
+}
+
+[shader("compute")]
+[numthreads(256, 1, 1)]
+void PressureRedCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ RelaxPressure(PressureCellCoordinates(dispatchThreadID.x, 0));
+}
+
+[shader("compute")]
+[numthreads(256, 1, 1)]
+void PressureBlackCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ RelaxPressure(PressureCellCoordinates(dispatchThreadID.x, 1));
+}
+
+[shader("compute")]
+[numthreads(256, 1, 1)]
+void ProjectGridCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ uint index = dispatchThreadID.x;
+ if (index >= simulation.GridPointCount)
+ {
+ return;
+ }
+
+ int3 point = GridCoordinates(index);
+ float3 velocity = simulation.GridVelocity[index].xyz;
+
+ for (uint component = 0; component < 3; component++)
+ {
+ int3 negativeCell;
+ int3 positiveCell;
+ FaceCells(component, point, negativeCell, positiveCell);
+ uint negativeType = CellTypeAt(negativeCell);
+ uint positiveType = CellTypeAt(positiveCell);
+
+ if (negativeType == CellSolid || positiveType == CellSolid)
+ {
+ velocity[component] = 0.0;
+ continue;
+ }
+
+ if (negativeType != CellFluid && positiveType != CellFluid)
+ {
+ continue;
+ }
+
+ float negativePressure = negativeType == CellFluid ? simulation.PressureA[CellIndex(negativeCell)] : 0.0;
+ float positivePressure = positiveType == CellFluid ? simulation.PressureA[CellIndex(positiveCell)] : 0.0;
+ velocity[component] -= simulation.TimeStep * (positivePressure - negativePressure) * simulation.InverseGridSpacing;
+ velocity[component] = clamp(velocity[component], -8.0, 8.0);
+ }
+
+ simulation.GridVelocity[index] = float4(velocity, 0.0);
+}
+
+[shader("compute")]
+[numthreads(128, 1, 1)]
+void GridToParticleAndAdvectCS(uint3 dispatchThreadID: SV_DispatchThreadID)
+{
+ uint index = dispatchThreadID.x;
+ if (index >= simulation.ParticleCount)
+ {
+ return;
+ }
+
+ Particle particle = simulation.Particles[index];
+ float3 position = ParticlePosition(particle);
+ float3 oldVelocity = ParticleVelocity(particle);
+ float3 affineX;
+ float3 affineY;
+ float3 affineZ;
+ float3 picVelocity;
+ float3 oldGridVelocity;
+ SampleComponentPair(position, 0, picVelocity.x, oldGridVelocity.x, affineX);
+ SampleComponentPair(position, 1, picVelocity.y, oldGridVelocity.y, affineY);
+ SampleComponentPair(position, 2, picVelocity.z, oldGridVelocity.z, affineZ);
+ float3 flipVelocity = oldVelocity + picVelocity - oldGridVelocity;
+ float damping = pow(simulation.VelocityDamping, simulation.TimeStep * 60.0);
+ float3 velocity = lerp(picVelocity, flipVelocity, simulation.FlipRatio) * damping;
+ float speed = length(velocity);
+
+ if (speed > 7.5)
+ {
+ velocity *= 7.5 / speed;
+ }
+
+ if (speed < 0.012 && !simulation.WaveMakerEnabled && simulation.InteractionStrength <= 0.0)
+ {
+ velocity = float3(0.0, 0.0, 0.0);
+ }
+
+ float3 midpoint = position + picVelocity * simulation.TimeStep * 0.5;
+ float3 advectionVelocity = SampleGridVelocity(midpoint);
+ position += advectionVelocity * simulation.TimeStep;
+ bool collided = ResolveCollisions(position, velocity);
+ particle.PositionDensity = float4(position, simulation.RestDensity);
+ particle.VelocityLambda = float4(velocity, 0.0);
+ simulation.Particles[index] = particle;
+
+ if (collided)
+ {
+ simulation.ParticleAffine[index * 3 + 0] = float4(0.0, 0.0, 0.0, 0.0);
+ simulation.ParticleAffine[index * 3 + 1] = float4(0.0, 0.0, 0.0, 0.0);
+ simulation.ParticleAffine[index * 3 + 2] = float4(0.0, 0.0, 0.0, 0.0);
+ }
+ else
+ {
+ simulation.ParticleAffine[index * 3 + 0] = float4(affineX, 0.0);
+ simulation.ParticleAffine[index * 3 + 1] = float4(affineY, 0.0);
+ simulation.ParticleAffine[index * 3 + 2] = float4(affineZ, 0.0);
+ }
+}
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang b/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang
new file mode 100644
index 00000000..f0b7b4fc
--- /dev/null
+++ b/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang
@@ -0,0 +1,278 @@
+#include "FluidCommon.slang"
+
+struct SurfaceConstants
+{
+ float4x4 View;
+
+ float4x4 Projection;
+
+ private float4 CameraRightAndRadius;
+
+ private float4 CameraUpAndRestDensity;
+
+ private uint4 CountsAndDimensions;
+
+ private float4 InterpolationParameters;
+
+ DescriptorHandle> Particles;
+
+ DescriptorHandle> PreviousPositions;
+
+ DescriptorHandle SceneDepth;
+
+ DescriptorHandle Sampler;
+
+ property float3 CameraRight
+ {
+ get {
+ return CameraRightAndRadius.xyz;
+ }
+ }
+
+ property float ParticleRadius
+ {
+ get {
+ return CameraRightAndRadius.w;
+ }
+ }
+
+ property float3 CameraUp
+ {
+ get {
+ return CameraUpAndRestDensity.xyz;
+ }
+ }
+
+ property float RestDensity
+ {
+ get {
+ return CameraUpAndRestDensity.w;
+ }
+ }
+
+ property uint ParticleCount
+ {
+ get {
+ return CountsAndDimensions.x;
+ }
+ }
+
+ property uint RenderMode
+ {
+ get {
+ return CountsAndDimensions.y;
+ }
+ }
+
+ property uint Width
+ {
+ get {
+ return CountsAndDimensions.z;
+ }
+ }
+
+ property uint Height
+ {
+ get {
+ return CountsAndDimensions.w;
+ }
+ }
+
+ property float InterpolationAlpha
+ {
+ get {
+ return InterpolationParameters.x;
+ }
+ }
+};
+
+struct SurfaceVSOutput
+{
+ float4 Position : SV_POSITION;
+
+ float2 Corner : TEXCOORD0;
+
+ float3 CenterView : TEXCOORD1;
+
+ float Speed : TEXCOORD2;
+
+ float Density : TEXCOORD3;
+};
+
+struct DepthOutput
+{
+ float LinearDepth : SV_TARGET0;
+
+ float4 Attributes : SV_TARGET1;
+
+ float DeviceDepth : SV_DEPTH;
+};
+
+struct ThicknessOutput
+{
+ float Thickness : SV_TARGET;
+
+ float DeviceDepth : SV_DEPTH;
+};
+
+struct ParticleOutput
+{
+ float4 Color : SV_TARGET;
+
+ float DeviceDepth : SV_DEPTH;
+};
+
+ConstantBuffer surface;
+
+bool OccludedByScene(SurfaceVSOutput input, float linearDepth)
+{
+ float2 uv = input.Position.xy / float2(surface.Width, surface.Height);
+ float sceneDepth = surface.SceneDepth.SampleLevel(surface.Sampler, uv, 0.0).r;
+
+ return surface.RenderMode == 0 && sceneDepth > 0.0 && linearDepth >= sceneDepth;
+}
+
+float2 QuadCorner(uint vertexId)
+{
+ if (vertexId == 0)
+ return float2(-1.0, -1.0);
+ if (vertexId == 1)
+ return float2(1.0, -1.0);
+ if (vertexId == 2)
+ return float2(-1.0, 1.0);
+
+ return float2(1.0, 1.0);
+}
+
+[shader("vertex")]
+SurfaceVSOutput SurfaceVS(uint vertexId: SV_VertexID, uint instanceId: SV_InstanceID)
+{
+ Particle particle = surface.Particles[instanceId];
+ float2 corner = QuadCorner(vertexId);
+ float3 worldPosition = lerp(surface.PreviousPositions[instanceId].xyz, particle.PositionDensity.xyz, surface.InterpolationAlpha);
+ float3 velocity = particle.VelocityLambda.xyz;
+ float speed = length(velocity);
+ float density = particle.PositionDensity.w / surface.RestDensity;
+ float2 offset = corner;
+
+ if (surface.RenderMode == 0)
+ {
+ float2 projectedVelocity = float2(dot(velocity, surface.CameraRight), dot(velocity, surface.CameraUp));
+ float projectedSpeed = length(projectedVelocity);
+ float2 direction = projectedSpeed > 0.001 ? projectedVelocity / projectedSpeed : float2(1.0, 0.0);
+ float stretchWeight = smoothstep(0.35, 3.2, speed) * (0.45 + 0.55 * (1.0 - smoothstep(0.68, 1.05, density)));
+ float majorScale = 1.0 + stretchWeight * 1.65;
+ float minorScale = 1.0 - stretchWeight * 0.25;
+ offset = direction * corner.x * majorScale + float2(-direction.y, direction.x) * corner.y * minorScale;
+ }
+
+ float3 billboardPosition = worldPosition + (surface.CameraRight * offset.x + surface.CameraUp * offset.y) * surface.ParticleRadius;
+ float4 centerView = mul(float4(worldPosition, 1.0), surface.View);
+
+ SurfaceVSOutput output;
+ output.Position = mul(mul(float4(billboardPosition, 1.0), surface.View), surface.Projection);
+ output.Corner = corner;
+ output.CenterView = centerView.xyz;
+ output.Speed = speed;
+ output.Density = density;
+
+ return output;
+}
+
+[shader("fragment")]
+DepthOutput DepthFS(SurfaceVSOutput input)
+{
+ float radiusSquared = dot(input.Corner, input.Corner);
+ if (radiusSquared > 1.0)
+ {
+ discard;
+ }
+
+ float sphereZ = sqrt(max(1.0 - radiusSquared, 0.0));
+ float3 viewPosition = input.CenterView + float3(input.Corner * surface.ParticleRadius, surface.ParticleRadius * 0.62);
+ float4 clipPosition = mul(float4(viewPosition, 1.0), surface.Projection);
+
+ if (OccludedByScene(input, -viewPosition.z))
+ {
+ discard;
+ }
+
+ DepthOutput output;
+ output.LinearDepth = -viewPosition.z;
+ output.Attributes = float4(input.Speed, input.Density, sphereZ, 1.0);
+ output.DeviceDepth = clipPosition.z / clipPosition.w;
+
+ return output;
+}
+
+[shader("fragment")]
+ThicknessOutput ThicknessFS(SurfaceVSOutput input)
+{
+ float radiusSquared = dot(input.Corner, input.Corner);
+ if (radiusSquared > 1.0)
+ {
+ discard;
+ }
+
+ float sphereZ = sqrt(max(1.0 - radiusSquared, 0.0));
+ float3 viewPosition = input.CenterView + float3(input.Corner * surface.ParticleRadius, sphereZ * surface.ParticleRadius);
+ float4 clipPosition = mul(float4(viewPosition, 1.0), surface.Projection);
+
+ if (OccludedByScene(input, -viewPosition.z))
+ {
+ discard;
+ }
+
+ ThicknessOutput output;
+ output.Thickness = 1.08 * surface.ParticleRadius * sphereZ;
+ output.DeviceDepth = clipPosition.z / clipPosition.w;
+
+ return output;
+}
+
+float3 SpeedColor(float speed)
+{
+ float value = saturate(speed / 6.5);
+ float3 blue = float3(0.015, 0.12, 0.95);
+ float3 cyan = float3(0.0, 0.82, 1.0);
+ float3 yellow = float3(1.0, 0.84, 0.02);
+ float3 red = float3(0.95, 0.025, 0.01);
+
+ if (value < 0.34)
+ {
+ return lerp(blue, cyan, value / 0.34);
+ }
+
+ if (value < 0.70)
+ {
+ return lerp(cyan, yellow, (value - 0.34) / 0.36);
+ }
+
+ return lerp(yellow, red, (value - 0.70) / 0.30);
+}
+
+[shader("fragment")]
+ParticleOutput ParticleFS(SurfaceVSOutput input)
+{
+ float radiusSquared = dot(input.Corner, input.Corner);
+ if (radiusSquared > 1.0)
+ {
+ discard;
+ }
+
+ float sphereZ = sqrt(max(1.0 - radiusSquared, 0.0));
+ float3 normal = normalize(float3(input.Corner, sphereZ));
+ float3 lightDirection = normalize(float3(-0.38, 0.62, 0.69));
+ float diffuse = 0.24 + 0.76 * saturate(dot(normal, lightDirection));
+ float specular = pow(saturate(dot(reflect(-lightDirection, normal), float3(0.0, 0.0, 1.0))), 36.0) * 0.42;
+ float rim = pow(1.0 - sphereZ, 3.0) * 0.12;
+ float3 color = SpeedColor(input.Speed) * (diffuse + rim) + specular;
+ float3 viewPosition = input.CenterView + float3(input.Corner * surface.ParticleRadius, sphereZ * surface.ParticleRadius);
+ float4 clipPosition = mul(float4(viewPosition, 1.0), surface.Projection);
+
+ ParticleOutput output;
+ output.Color = float4(pow(saturate(color), 1.0 / 2.2), 1.0);
+ output.DeviceDepth = clipPosition.z / clipPosition.w;
+
+ return output;
+}
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/Glass.slang b/sources/Experiments/FluidTank/Assets/Shaders/Glass.slang
new file mode 100644
index 00000000..9b01bdf2
--- /dev/null
+++ b/sources/Experiments/FluidTank/Assets/Shaders/Glass.slang
@@ -0,0 +1,69 @@
+#include "SceneCommon.slang"
+
+struct GlassConstants
+{
+ float4x4 View;
+
+ float4x4 Projection;
+
+ private float4 CameraPositionAndTime;
+
+ property float3 CameraPosition
+ {
+ get {
+ return CameraPositionAndTime.xyz;
+ }
+ }
+
+ property float Time
+ {
+ get {
+ return CameraPositionAndTime.w;
+ }
+ }
+};
+
+struct VSInput
+{
+ float4 Position : POSITION;
+
+ float4 NormalAndMaterial : NORMAL;
+};
+
+struct VSOutput
+{
+ float4 Position : SV_POSITION;
+
+ float3 WorldPosition : TEXCOORD0;
+
+ float3 Normal : TEXCOORD1;
+};
+
+ConstantBuffer glass;
+
+[shader("vertex")]
+VSOutput GlassVS(VSInput input)
+{
+ VSOutput output;
+ output.Position = mul(mul(float4(input.Position.xyz, 1.0), glass.View), glass.Projection);
+ output.WorldPosition = input.Position.xyz;
+ output.Normal = input.NormalAndMaterial.xyz;
+
+ return output;
+}
+
+[shader("fragment")]
+float4 GlassFS(VSOutput input) : SV_TARGET
+{
+ float3 normal = normalize(input.Normal);
+ float3 viewDirection = normalize(glass.CameraPosition - input.WorldPosition);
+ float facing = abs(dot(normal, viewDirection));
+ float fresnel = 0.028 + 0.972 * pow(1.0 - facing, 5.0);
+ float verticalEdge = pow(saturate(abs(input.WorldPosition.y - 2.6) / 2.6), 18.0);
+ float micro = 0.5 + 0.5 * sin(input.WorldPosition.x * 31.0 + input.WorldPosition.y * 17.0 + glass.Time * 0.1);
+ float alpha = saturate(0.018 + fresnel * 0.34 + verticalEdge * 0.04 + micro * 0.004);
+ float3 tint = lerp(float3(0.42, 0.74, 0.78), float3(0.83, 0.96, 1.0), fresnel);
+ float3 color = ToSRGB(ACESFilm(tint * (0.42 + fresnel * 0.75)));
+
+ return float4(color * alpha, alpha);
+}
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang b/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang
new file mode 100644
index 00000000..b69b0367
--- /dev/null
+++ b/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang
@@ -0,0 +1,122 @@
+#include "SceneCommon.slang"
+
+struct SceneConstants
+{
+ float4x4 View;
+
+ float4x4 Projection;
+
+ private float4 CameraPositionAndTime;
+
+ private float4 LightDirectionAndIntensity;
+
+ DescriptorHandle> Materials;
+
+ property float3 CameraPosition
+ {
+ get {
+ return CameraPositionAndTime.xyz;
+ }
+ }
+
+ property float Time
+ {
+ get {
+ return CameraPositionAndTime.w;
+ }
+ }
+
+ property float3 LightDirection
+ {
+ get {
+ return LightDirectionAndIntensity.xyz;
+ }
+ }
+
+ property float LightIntensity
+ {
+ get {
+ return LightDirectionAndIntensity.w;
+ }
+ }
+};
+
+struct VSInput
+{
+ float4 Position : POSITION;
+
+ float4 NormalAndMaterial : NORMAL;
+};
+
+struct VSOutput
+{
+ float4 Position : SV_POSITION;
+
+ float3 WorldPosition : TEXCOORD0;
+
+ float3 Normal : TEXCOORD1;
+
+ float ViewDepth : TEXCOORD2;
+
+ nointerpolation uint MaterialId : TEXCOORD3;
+};
+
+struct FSOutput
+{
+ float4 Color : SV_TARGET0;
+
+ float LinearDepth : SV_TARGET1;
+};
+
+ConstantBuffer scene;
+
+[shader("vertex")]
+VSOutput VSMain(VSInput input)
+{
+ float4 worldPosition = float4(input.Position.xyz, 1.0);
+ float4 viewPosition = mul(worldPosition, scene.View);
+
+ VSOutput output;
+ output.Position = mul(viewPosition, scene.Projection);
+ output.WorldPosition = input.Position.xyz;
+ output.Normal = input.NormalAndMaterial.xyz;
+ output.ViewDepth = -viewPosition.z;
+ output.MaterialId = asuint(input.NormalAndMaterial.w);
+
+ return output;
+}
+
+[shader("fragment")]
+FSOutput FSMain(VSOutput input)
+{
+ SceneMaterial material = scene.Materials[input.MaterialId];
+ float3 normal = normalize(input.Normal);
+ float3 viewDirection = normalize(scene.CameraPosition - input.WorldPosition);
+ float3 lightDirection = normalize(-scene.LightDirection);
+ float3 halfVector = normalize(viewDirection + lightDirection);
+
+ float ndotl = saturate(dot(normal, lightDirection));
+ float ndoth = saturate(dot(normal, halfVector));
+ float roughness = max(material.Roughness, 0.035);
+ float exponent = max(2.0 / (roughness * roughness) - 2.0, 1.0);
+ float3 f0 = lerp(float3(0.035, 0.038, 0.04), material.Albedo, material.Metallic);
+ float3 fresnel = FresnelSchlick(saturate(dot(halfVector, viewDirection)), f0);
+ float3 diffuse = material.Albedo * (1.0 - material.Metallic) * (0.20 + ndotl * scene.LightIntensity);
+ float3 specular = fresnel * pow(ndoth, exponent) * (0.18 + scene.LightIntensity * 1.8);
+ float3 environment = SkyColor(reflect(-viewDirection, normal)) * fresnel * (0.18 + 0.82 * (1.0 - roughness));
+
+ if (input.MaterialId == 0)
+ {
+ float gridX = smoothstep(0.42, 0.49, abs(frac(input.WorldPosition.x * 0.5) - 0.5));
+ float gridZ = smoothstep(0.42, 0.49, abs(frac(input.WorldPosition.z * 0.5) - 0.5));
+ diffuse *= lerp(0.84, 1.07, max(gridX, gridZ));
+ }
+
+ float3 color = diffuse + specular + environment + material.Albedo * material.Emission;
+
+ FSOutput output;
+ output.Color = float4(color, 1.0);
+ output.LinearDepth = input.ViewDepth;
+
+ return output;
+}
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/SceneCommon.slang b/sources/Experiments/FluidTank/Assets/Shaders/SceneCommon.slang
new file mode 100644
index 00000000..ed98efeb
--- /dev/null
+++ b/sources/Experiments/FluidTank/Assets/Shaders/SceneCommon.slang
@@ -0,0 +1,89 @@
+struct SceneVertex
+{
+ private float4 PositionAndPadding;
+
+ private float4 NormalAndMaterial;
+
+ property float3 Position
+ {
+ get {
+ return PositionAndPadding.xyz;
+ }
+ }
+
+ property float3 Normal
+ {
+ get {
+ return NormalAndMaterial.xyz;
+ }
+ }
+
+ property uint MaterialId
+ {
+ get {
+ return asuint(NormalAndMaterial.w);
+ }
+ }
+};
+
+struct SceneMaterial
+{
+ private float4 AlbedoAndRoughness;
+
+ private float4 MetallicEmissionAndPadding;
+
+ property float3 Albedo
+ {
+ get {
+ return AlbedoAndRoughness.xyz;
+ }
+ }
+
+ property float Roughness
+ {
+ get {
+ return AlbedoAndRoughness.w;
+ }
+ }
+
+ property float Metallic
+ {
+ get {
+ return MetallicEmissionAndPadding.x;
+ }
+ }
+
+ property float Emission
+ {
+ get {
+ return MetallicEmissionAndPadding.y;
+ }
+ }
+};
+
+float3 SkyColor(float3 direction)
+{
+ float horizon = saturate(direction.y * 0.5 + 0.5);
+ float3 sky = lerp(float3(0.62, 0.76, 0.82), float3(0.055, 0.18, 0.34), pow(horizon, 0.62));
+ float3 sunDirection = normalize(float3(-0.38, 0.83, -0.42));
+ float sunAlignment = saturate(dot(direction, sunDirection));
+ float sun = pow(sunAlignment, 720.0);
+ float halo = pow(sunAlignment, 18.0);
+
+ return sky + float3(10.0, 8.2, 5.3) * sun + float3(0.55, 0.42, 0.25) * halo;
+}
+
+float3 FresnelSchlick(float cosine, float3 f0)
+{
+ return f0 + (1.0 - f0) * pow(saturate(1.0 - cosine), 5.0);
+}
+
+float3 ACESFilm(float3 color)
+{
+ return saturate((color * (color * 2.51 + 0.03)) / (color * (color * 2.43 + 0.59) + 0.14));
+}
+
+float3 ToSRGB(float3 color)
+{
+ return pow(max(color, 0.0), 1.0 / 2.2);
+}
diff --git a/sources/Experiments/FluidTank/FluidSimulation.cs b/sources/Experiments/FluidTank/FluidSimulation.cs
new file mode 100644
index 00000000..547be4fd
--- /dev/null
+++ b/sources/Experiments/FluidTank/FluidSimulation.cs
@@ -0,0 +1,386 @@
+using System.Numerics;
+using System.Runtime.InteropServices;
+using FluidTank.Helpers;
+using Zenith.NET;
+using Buffer = Zenith.NET.Buffer;
+
+namespace FluidTank;
+
+internal unsafe class FluidSimulation : IDisposable
+{
+ private static readonly Vector3 TankMin = new(-6.0f, 0.0f, -3.0f);
+ private static readonly Vector3 TankMax = new(6.0f, 5.2f, 3.0f);
+ private static readonly (uint X, uint Y, uint Z) DamDimensions = (40, 48, 59);
+ private const float GridSpacing = 6.0f / 33.0f;
+
+ private readonly Buffer constantBuffer;
+ private readonly Buffer particles;
+ private readonly Buffer previousPositions;
+ private readonly Buffer particleAffine;
+ private readonly Buffer gridAccumulation;
+ private readonly Buffer gridVelocity;
+ private readonly Buffer gridVelocityOld;
+ private readonly Buffer cellTypes;
+ private readonly Buffer divergence;
+ private readonly Buffer pressureA;
+ private readonly uint pressureParityDispatchCount;
+
+ private readonly ComputePipeline resetPipeline;
+ private readonly ComputePipeline initializeGridPipeline;
+ private readonly ComputePipeline clearGridPipeline;
+ private readonly ComputePipeline beginParticleToGridPipeline;
+ private readonly ComputePipeline particleToGridPipeline;
+ private readonly ComputePipeline normalizeAndApplyForcesPipeline;
+ private readonly ComputePipeline divergencePipeline;
+ private readonly ComputePipeline pressureRedPipeline;
+ private readonly ComputePipeline pressureBlackPipeline;
+ private readonly ComputePipeline projectGridPipeline;
+ private readonly ComputePipeline gridToParticleAndAdvectPipeline;
+
+ private bool resetRequested = true;
+ private Vector3 interactionOrigin;
+ private Vector3 interactionDirection;
+ private float interactionStrength;
+ private TimelineValue ready;
+
+ public FluidSimulation()
+ {
+ ParticleCount = DamDimensions.X * DamDimensions.Y * DamDimensions.Z;
+
+ Vector3 tankExtent = TankMax - TankMin;
+ GridDimensions = new((uint)MathF.Ceiling(tankExtent.X / GridSpacing),
+ (uint)MathF.Ceiling(tankExtent.Y / GridSpacing),
+ (uint)MathF.Ceiling(tankExtent.Z / GridSpacing));
+ CellCount = GridDimensions.X * GridDimensions.Y * GridDimensions.Z;
+ GridPointCount = (GridDimensions.X + 1) * (GridDimensions.Y + 1) * (GridDimensions.Z + 1);
+ pressureParityDispatchCount = (GridDimensions.X + 1) / 2 * GridDimensions.Y * GridDimensions.Z;
+
+ constantBuffer = GraphicsHelper.CreateConstantBuffer();
+
+ particles = GraphicsHelper.CreateBuffer(ParticleCount, 32, BufferUsages.StorageReadOnly | BufferUsages.StorageReadWrite);
+ previousPositions = GraphicsHelper.CreateBuffer(ParticleCount, 16, BufferUsages.StorageReadOnly | BufferUsages.StorageReadWrite);
+ particleAffine = GraphicsHelper.CreateBuffer(ParticleCount * 3, 16, BufferUsages.StorageReadWrite);
+ gridAccumulation = GraphicsHelper.CreateBuffer(GridPointCount * 6, sizeof(int), BufferUsages.StorageReadWrite);
+ gridVelocity = GraphicsHelper.CreateBuffer(GridPointCount, 16, BufferUsages.StorageReadWrite);
+ gridVelocityOld = GraphicsHelper.CreateBuffer(GridPointCount, 16, BufferUsages.StorageReadWrite);
+ cellTypes = GraphicsHelper.CreateBuffer(CellCount, sizeof(uint), BufferUsages.StorageReadWrite);
+ divergence = GraphicsHelper.CreateBuffer(CellCount, sizeof(float), BufferUsages.StorageReadWrite);
+ pressureA = GraphicsHelper.CreateBuffer(CellCount, sizeof(float), BufferUsages.StorageReadWrite);
+
+ resetPipeline = GraphicsHelper.CreateComputePipeline("FluidSimulation.slang", "ResetCS");
+ initializeGridPipeline = GraphicsHelper.CreateComputePipeline("FluidSimulation.slang", "InitializeGridCS");
+ clearGridPipeline = GraphicsHelper.CreateComputePipeline("FluidSimulation.slang", "ClearGridCS");
+ beginParticleToGridPipeline = GraphicsHelper.CreateComputePipeline("FluidSimulation.slang", "BeginParticleToGridCS");
+ particleToGridPipeline = GraphicsHelper.CreateComputePipeline("FluidSimulation.slang", "ParticleToGridCS");
+ normalizeAndApplyForcesPipeline = GraphicsHelper.CreateComputePipeline("FluidSimulation.slang", "NormalizeAndApplyForcesCS");
+ divergencePipeline = GraphicsHelper.CreateComputePipeline("FluidSimulation.slang", "DivergenceCS");
+ pressureRedPipeline = GraphicsHelper.CreateComputePipeline("FluidSimulation.slang", "PressureRedCS");
+ pressureBlackPipeline = GraphicsHelper.CreateComputePipeline("FluidSimulation.slang", "PressureBlackCS");
+ projectGridPipeline = GraphicsHelper.CreateComputePipeline("FluidSimulation.slang", "ProjectGridCS");
+ gridToParticleAndAdvectPipeline = GraphicsHelper.CreateComputePipeline("FluidSimulation.slang", "GridToParticleAndAdvectCS");
+ }
+
+ public uint ParticleCount { get; }
+
+ public uint CellCount { get; }
+
+ public uint GridPointCount { get; }
+
+ public (uint X, uint Y, uint Z) GridDimensions { get; }
+
+ public ResourceHandle ParticleHandle => particles.StorageReadOnlyHandle;
+
+ public ResourceHandle PreviousPositionHandle => previousPositions.StorageReadOnlyHandle;
+
+ public const float ParticleRadius = GridSpacing * 0.316f;
+
+ public const float RestDensity = 5.52f;
+
+ public float FlipRatio { get; set; } = 0.97f;
+
+ public float VelocityDamping { get; set; } = 0.9998f;
+
+ public float WaveAmplitude { get; set; } = 0.12f;
+
+ public float WaveFrequency { get; set; } = 0.58f;
+
+ public bool WaveMakerEnabled { get; set; }
+
+ public int PressureIterations
+ {
+ get;
+ set => field = Math.Clamp(value, 4, 32);
+ } = 18;
+
+ public void Reset()
+ {
+ resetRequested = true;
+ }
+
+ public void Push(Vector3 origin, Vector3 direction)
+ {
+ interactionOrigin = origin;
+ interactionDirection = Vector3.Normalize(direction);
+ interactionStrength = 4.8f;
+ }
+
+ public TimelineValue Step(double totalTime, double deltaSeconds, bool paused)
+ {
+ const uint substeps = 2;
+
+ if (paused && !resetRequested)
+ {
+ return ready;
+ }
+
+ float frameTime = (float)deltaSeconds;
+ float timeStep = paused ? 0.0f : frameTime / substeps;
+
+ SimulationConstants parameters = new()
+ {
+ TankMin = TankMin,
+ TimeStep = MathF.Max(timeStep, 0.000001f),
+ TankMax = TankMax,
+ Time = (float)totalTime,
+ GridSpacing = GridSpacing,
+ InverseGridSpacing = 1.0f / GridSpacing,
+ FlipRatio = FlipRatio,
+ VelocityDamping = VelocityDamping,
+ WaveAmplitude = WaveAmplitude,
+ WaveFrequency = WaveFrequency,
+ InteractionRadius = 1.15f,
+ InteractionStrength = interactionStrength,
+ InteractionOrigin = interactionOrigin,
+ ParticleRadius = ParticleRadius,
+ InteractionDirection = interactionDirection,
+ RestDensity = RestDensity,
+ ParticleCount = ParticleCount,
+ CellCount = CellCount,
+ GridPointCount = GridPointCount,
+ WaveMakerEnabled = WaveMakerEnabled ? 1u : 0u,
+ GridX = GridDimensions.X,
+ GridY = GridDimensions.Y,
+ GridZ = GridDimensions.Z,
+ PressureIterations = (uint)PressureIterations,
+ DamX = DamDimensions.X,
+ DamY = DamDimensions.Y,
+ DamZ = DamDimensions.Z,
+ Substeps = substeps,
+ Particles = particles.StorageReadWriteHandle,
+ PreviousPositions = previousPositions.StorageReadWriteHandle,
+ ParticleAffine = particleAffine.StorageReadWriteHandle,
+ GridAccumulation = gridAccumulation.StorageReadWriteHandle,
+ GridVelocity = gridVelocity.StorageReadWriteHandle,
+ GridVelocityOld = gridVelocityOld.StorageReadWriteHandle,
+ CellTypes = cellTypes.StorageReadWriteHandle,
+ Divergence = divergence.StorageReadWriteHandle,
+ PressureA = pressureA.StorageReadWriteHandle
+ };
+
+ constantBuffer.Upload(0, new()
+ {
+ Pointer = (nint)(¶meters),
+ SizeInBytes = (uint)sizeof(SimulationConstants)
+ });
+
+ CommandBuffer commandBuffer = App.Context.ComputeQueue.CommandBuffer();
+
+ if (resetRequested)
+ {
+ Dispatch(commandBuffer, resetPipeline, ParticleCount);
+ Dispatch(commandBuffer, initializeGridPipeline, CellCount);
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.ComputeShading);
+ resetRequested = false;
+ }
+
+ if (!paused)
+ {
+ for (uint substep = 0; substep < substeps; substep++)
+ {
+ Dispatch(commandBuffer, clearGridPipeline, Math.Max(GridPointCount * 6, CellCount));
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.ComputeShading);
+
+ Dispatch(commandBuffer, substep is 0 ? beginParticleToGridPipeline : particleToGridPipeline, ParticleCount);
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.ComputeShading);
+
+ Dispatch(commandBuffer, normalizeAndApplyForcesPipeline, GridPointCount);
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.ComputeShading);
+
+ Dispatch(commandBuffer, divergencePipeline, CellCount);
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.ComputeShading);
+
+ for (int iteration = 0; iteration < PressureIterations; iteration++)
+ {
+ Dispatch(commandBuffer, pressureRedPipeline, pressureParityDispatchCount);
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.ComputeShading);
+
+ Dispatch(commandBuffer, pressureBlackPipeline, pressureParityDispatchCount);
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.ComputeShading);
+ }
+
+ Dispatch(commandBuffer, projectGridPipeline, GridPointCount);
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.ComputeShading);
+
+ Dispatch(commandBuffer, gridToParticleAndAdvectPipeline, ParticleCount);
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.ComputeShading);
+ }
+ }
+
+ if (!paused)
+ {
+ interactionStrength = 0.0f;
+ }
+
+ return ready = commandBuffer.Submit();
+ }
+
+ public void Dispose()
+ {
+ gridToParticleAndAdvectPipeline.Dispose();
+ projectGridPipeline.Dispose();
+ pressureBlackPipeline.Dispose();
+ pressureRedPipeline.Dispose();
+ divergencePipeline.Dispose();
+ normalizeAndApplyForcesPipeline.Dispose();
+ particleToGridPipeline.Dispose();
+ beginParticleToGridPipeline.Dispose();
+ clearGridPipeline.Dispose();
+ initializeGridPipeline.Dispose();
+ resetPipeline.Dispose();
+
+ pressureA.Dispose();
+ divergence.Dispose();
+ cellTypes.Dispose();
+ gridVelocityOld.Dispose();
+ gridVelocity.Dispose();
+ gridAccumulation.Dispose();
+ particleAffine.Dispose();
+ previousPositions.Dispose();
+ particles.Dispose();
+ constantBuffer.Dispose();
+ }
+
+ private void Dispatch(CommandBuffer commandBuffer, ComputePipeline pipeline, uint count)
+ {
+ uint groupSize = pipeline.Desc.ComputeShader.Desc.ThreadGroupSize.X;
+
+ commandBuffer.SetPipeline(pipeline);
+ commandBuffer.SetConstantBuffer(constantBuffer, 0);
+ commandBuffer.Dispatch((count + groupSize - 1) / groupSize, 1, 1);
+ }
+}
+
+[StructLayout(LayoutKind.Explicit, Size = 216)]
+file struct SimulationConstants
+{
+ [FieldOffset(0)]
+ public Vector3 TankMin;
+
+ [FieldOffset(12)]
+ public float TimeStep;
+
+ [FieldOffset(16)]
+ public Vector3 TankMax;
+
+ [FieldOffset(28)]
+ public float Time;
+
+ [FieldOffset(32)]
+ public float GridSpacing;
+
+ [FieldOffset(36)]
+ public float InverseGridSpacing;
+
+ [FieldOffset(40)]
+ public float FlipRatio;
+
+ [FieldOffset(44)]
+ public float VelocityDamping;
+
+ [FieldOffset(48)]
+ public float WaveAmplitude;
+
+ [FieldOffset(52)]
+ public float WaveFrequency;
+
+ [FieldOffset(56)]
+ public float InteractionRadius;
+
+ [FieldOffset(60)]
+ public float InteractionStrength;
+
+ [FieldOffset(64)]
+ public Vector3 InteractionOrigin;
+
+ [FieldOffset(76)]
+ public float ParticleRadius;
+
+ [FieldOffset(80)]
+ public Vector3 InteractionDirection;
+
+ [FieldOffset(92)]
+ public float RestDensity;
+
+ [FieldOffset(96)]
+ public uint ParticleCount;
+
+ [FieldOffset(100)]
+ public uint CellCount;
+
+ [FieldOffset(104)]
+ public uint GridPointCount;
+
+ [FieldOffset(108)]
+ public uint WaveMakerEnabled;
+
+ [FieldOffset(112)]
+ public uint GridX;
+
+ [FieldOffset(116)]
+ public uint GridY;
+
+ [FieldOffset(120)]
+ public uint GridZ;
+
+ [FieldOffset(124)]
+ public uint PressureIterations;
+
+ [FieldOffset(128)]
+ public uint DamX;
+
+ [FieldOffset(132)]
+ public uint DamY;
+
+ [FieldOffset(136)]
+ public uint DamZ;
+
+ [FieldOffset(140)]
+ public uint Substeps;
+
+ [FieldOffset(144)]
+ public ResourceHandle Particles;
+
+ [FieldOffset(152)]
+ public ResourceHandle PreviousPositions;
+
+ [FieldOffset(160)]
+ public ResourceHandle ParticleAffine;
+
+ [FieldOffset(168)]
+ public ResourceHandle GridAccumulation;
+
+ [FieldOffset(176)]
+ public ResourceHandle GridVelocity;
+
+ [FieldOffset(184)]
+ public ResourceHandle GridVelocityOld;
+
+ [FieldOffset(192)]
+ public ResourceHandle CellTypes;
+
+ [FieldOffset(200)]
+ public ResourceHandle Divergence;
+
+ [FieldOffset(208)]
+ public ResourceHandle PressureA;
+}
diff --git a/sources/Experiments/FluidTank/FluidTank.csproj b/sources/Experiments/FluidTank/FluidTank.csproj
new file mode 100644
index 00000000..08be1b19
--- /dev/null
+++ b/sources/Experiments/FluidTank/FluidTank.csproj
@@ -0,0 +1,26 @@
+
+
+
+ Exe
+ $(StandardTargetFramework)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
\ No newline at end of file
diff --git a/sources/Experiments/FluidTank/FluidTankRenderer.cs b/sources/Experiments/FluidTank/FluidTankRenderer.cs
new file mode 100644
index 00000000..068a679d
--- /dev/null
+++ b/sources/Experiments/FluidTank/FluidTankRenderer.cs
@@ -0,0 +1,897 @@
+using System.Numerics;
+using System.Runtime.InteropServices;
+using FluidTank.Handlers;
+using FluidTank.Helpers;
+using Zenith.NET;
+using Buffer = Zenith.NET.Buffer;
+
+namespace FluidTank;
+
+internal enum FluidViewMode
+{
+ Water,
+ Particles
+}
+
+internal unsafe class FluidTankRenderer : IDisposable
+{
+ private const double FixedSimulationStep = 1.0 / 30.0;
+ private const int DepthSmoothingIterations = 4;
+
+ private readonly FluidSimulation simulation;
+ private readonly Buffer sceneVertexBuffer;
+ private readonly Buffer sceneIndexBuffer;
+ private readonly Buffer glassVertexBuffer;
+ private readonly Buffer glassIndexBuffer;
+ private readonly Buffer materialBuffer;
+ private readonly Buffer sceneConstantBuffer;
+ private readonly Buffer surfaceConstantBuffer;
+ private readonly Buffer blurConstantBuffer;
+ private readonly Buffer compositeConstantBuffer;
+ private readonly Buffer reflectionConstantBuffer;
+ private readonly Buffer glassConstantBuffer;
+ private readonly Sampler linearSampler;
+
+ private readonly GraphicsPipeline scenePipeline;
+ private readonly GraphicsPipeline fluidDepthPipeline;
+ private readonly GraphicsPipeline fluidThicknessPipeline;
+ private readonly GraphicsPipeline particlePipeline;
+ private readonly ComputePipeline blurPipeline;
+ private readonly ComputePipeline blurThicknessPipeline;
+ private readonly GraphicsPipeline compositePipeline;
+ private readonly GraphicsPipeline glassPipeline;
+ private readonly ComputePipeline? reflectionPipeline;
+
+ private readonly BottomLevelAccelerationStructure? sceneBlas;
+ private readonly TopLevelAccelerationStructure? sceneTlas;
+ private readonly uint sceneIndexCount;
+ private readonly uint glassIndexCount;
+
+ private Texture sceneColor = null!;
+ private Texture sceneLinearDepth = null!;
+ private Texture fluidAttributes = null!;
+ private Texture reconstructionDepth = null!;
+ private Texture smoothDepthA = null!;
+ private Texture smoothDepthB = null!;
+ private Texture smoothThicknessA = null!;
+ private Texture smoothThicknessB = null!;
+ private Texture? reflection;
+
+ private Matrix4x4 view;
+ private Matrix4x4 projection;
+ private Vector3 cameraPosition;
+ private Vector3 cameraRight;
+ private Vector3 cameraUp;
+ private double totalTime;
+ private double simulationTime;
+ private double simulationAccumulator = FixedSimulationStep;
+ private TimelineValue simulationReady;
+
+ public FluidTankRenderer()
+ {
+ FluidTankGeometry.CreateScene(out SceneVertex[] sceneVertices, out uint[] sceneIndices, out SceneMaterial[] materials);
+ FluidTankGeometry.CreateGlass(out SceneVertex[] glassVertices, out uint[] glassIndices);
+
+ sceneIndexCount = (uint)sceneIndices.Length;
+ glassIndexCount = (uint)glassIndices.Length;
+
+ CommandBuffer uploadCommandBuffer = App.Context.TransferQueue.CommandBuffer();
+ sceneVertexBuffer = GraphicsHelper.LoadBuffer(uploadCommandBuffer, sceneVertices, BufferUsages.Vertex | BufferUsages.StorageReadOnly);
+ sceneIndexBuffer = GraphicsHelper.LoadBuffer(uploadCommandBuffer, sceneIndices, BufferUsages.Index | BufferUsages.StorageReadOnly);
+ glassVertexBuffer = GraphicsHelper.LoadBuffer(uploadCommandBuffer, glassVertices, BufferUsages.Vertex);
+ glassIndexBuffer = GraphicsHelper.LoadBuffer(uploadCommandBuffer, glassIndices, BufferUsages.Index);
+ materialBuffer = GraphicsHelper.LoadBuffer(uploadCommandBuffer, materials, BufferUsages.StorageReadOnly);
+
+ uploadCommandBuffer.Submit().Wait();
+
+ sceneConstantBuffer = GraphicsHelper.CreateConstantBuffer();
+ surfaceConstantBuffer = GraphicsHelper.CreateConstantBuffer();
+ blurConstantBuffer = GraphicsHelper.CreateConstantBuffer(1024);
+ compositeConstantBuffer = GraphicsHelper.CreateConstantBuffer();
+ reflectionConstantBuffer = GraphicsHelper.CreateConstantBuffer();
+ glassConstantBuffer = GraphicsHelper.CreateConstantBuffer();
+ linearSampler = App.Context.CreateSampler(SamplerDesc.LinearClamp());
+
+ Resize(App.Width, App.Height);
+
+ InputLayout inputLayout = new();
+ inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Position });
+ inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Normal });
+
+ scenePipeline = GraphicsHelper.CreateGraphicsPipeline("Scene.slang", "VSMain", "FSMain", [inputLayout], new()
+ {
+ ColorFormats = [PixelFormat.R16G16B16A16Float, PixelFormat.R32Float],
+ DepthStencilFormat = PixelFormat.D32FloatS8UInt,
+ SampleCount = SampleCount.Count1
+ }, RasterizerState.CullBack(), DepthStencilState.DepthReadWrite(), BlendState.Opaque());
+
+ using Shader surfaceVertexShader = GraphicsHelper.LoadShader("FluidSurface.slang", "SurfaceVS");
+
+ fluidDepthPipeline = GraphicsHelper.CreateGraphicsPipeline(surfaceVertexShader, "FluidSurface.slang", "DepthFS", [], new()
+ {
+ ColorFormats = [PixelFormat.R32Float, PixelFormat.R16G16B16A16Float],
+ DepthStencilFormat = PixelFormat.D32FloatS8UInt,
+ SampleCount = SampleCount.Count1
+ }, RasterizerState.CullNone(), DepthStencilState.DepthReadWrite(), BlendState.Opaque(), PrimitiveTopology.TriangleStrip);
+
+ fluidThicknessPipeline = GraphicsHelper.CreateGraphicsPipeline(surfaceVertexShader, "FluidSurface.slang", "ThicknessFS", [], new()
+ {
+ ColorFormats = [PixelFormat.R16Float],
+ DepthStencilFormat = PixelFormat.D32FloatS8UInt,
+ SampleCount = SampleCount.Count1
+ }, RasterizerState.CullNone(), DepthStencilState.DepthRead(), AdditiveBlend(), PrimitiveTopology.TriangleStrip);
+
+ particlePipeline = GraphicsHelper.CreateGraphicsPipeline(surfaceVertexShader, "FluidSurface.slang", "ParticleFS", [], new()
+ {
+ ColorFormats = [PixelFormat.B8G8R8A8UNorm],
+ DepthStencilFormat = PixelFormat.D32FloatS8UInt,
+ SampleCount = SampleCount.Count1
+ }, RasterizerState.CullNone(), DepthStencilState.DepthReadWrite(), BlendState.Opaque(), PrimitiveTopology.TriangleStrip);
+
+ blurPipeline = GraphicsHelper.CreateComputePipeline("FluidBlur.slang", "BlurCS");
+ blurThicknessPipeline = GraphicsHelper.CreateComputePipeline("FluidBlur.slang", "BlurThicknessCS");
+
+ compositePipeline = GraphicsHelper.CreateGraphicsPipeline("FluidComposite.slang", "FullscreenVS", "CompositeFS", [], new()
+ {
+ ColorFormats = [PixelFormat.B8G8R8A8UNorm],
+ SampleCount = SampleCount.Count1
+ }, RasterizerState.CullNone(), DepthStencilState.DepthNone(), BlendState.Opaque());
+
+ glassPipeline = GraphicsHelper.CreateGraphicsPipeline("Glass.slang", "GlassVS", "GlassFS", [inputLayout], new()
+ {
+ ColorFormats = [PixelFormat.B8G8R8A8UNorm],
+ DepthStencilFormat = PixelFormat.D32FloatS8UInt,
+ SampleCount = SampleCount.Count1
+ }, RasterizerState.CullNone(), DepthStencilState.DepthRead(), BlendState.AlphaBlend());
+
+ simulation = new();
+
+ if (App.Context.Capabilities.RayTracingSupported)
+ {
+ CommandBuffer commandBuffer = App.Context.ComputeQueue.CommandBuffer();
+
+ sceneBlas = commandBuffer.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc
+ {
+ Geometries =
+ [
+ new()
+ {
+ Type = RayTracingGeometryType.Triangle,
+ TriangleGeometry = new()
+ {
+ VertexBuffer = sceneVertexBuffer,
+ VertexFormat = PixelFormat.R32G32B32Float,
+ VertexCount = (uint)sceneVertices.Length,
+ VertexStrideInBytes = (uint)sizeof(SceneVertex),
+ IndexBuffer = sceneIndexBuffer,
+ IndexFormat = IndexFormat.UInt32,
+ IndexCount = sceneIndexCount,
+ Transform = Matrix4x4.Identity
+ },
+ IsOpaque = true
+ }
+ ],
+ BuildFlags = AccelerationStructureBuildFlags.PreferFastTrace
+ });
+
+ sceneTlas = commandBuffer.BuildAccelerationStructure(new TopLevelAccelerationStructureDesc
+ {
+ Instances =
+ [
+ new()
+ {
+ AccelerationStructure = sceneBlas,
+ InstanceId = 0,
+ VisibilityMask = 0xFF,
+ Transform = Matrix4x4.Identity,
+ Flags = RayTracingInstanceFlags.None
+ }
+ ],
+ BuildFlags = AccelerationStructureBuildFlags.PreferFastTrace
+ });
+
+ commandBuffer.Submit().Wait();
+
+ reflectionPipeline = GraphicsHelper.CreateComputePipeline("FluidReflection.slang", "ReflectionCS");
+ }
+ }
+
+ public Texture Color { get; private set; } = null!;
+
+ public Texture DepthStencil { get; private set; } = null!;
+
+ public bool RayTracingEnabled => reflectionPipeline is not null;
+
+ public uint ParticleCount => simulation.ParticleCount;
+
+ public bool Paused { get; set; }
+
+ public bool WaveMakerEnabled
+ {
+ get => simulation.WaveMakerEnabled;
+ set => simulation.WaveMakerEnabled = value;
+ }
+
+ public float WaveAmplitude
+ {
+ get => simulation.WaveAmplitude;
+ set => simulation.WaveAmplitude = value;
+ }
+
+ public float WaveFrequency
+ {
+ get => simulation.WaveFrequency;
+ set => simulation.WaveFrequency = value;
+ }
+
+ public float FlipRatio
+ {
+ get => simulation.FlipRatio;
+ set => simulation.FlipRatio = value;
+ }
+
+ public float VelocityDamping
+ {
+ get => simulation.VelocityDamping;
+ set => simulation.VelocityDamping = value;
+ }
+
+ public int PressureIterations
+ {
+ get => simulation.PressureIterations;
+ set => simulation.PressureIterations = value;
+ }
+
+ public float Clarity { get; set; } = 1.05f;
+
+ public float RefractionStrength { get; set; } = 0.82f;
+
+ public FluidViewMode ViewMode { get; set; }
+
+ public void Update(CameraHandler camera, double delta)
+ {
+ view = camera.View;
+ projection = camera.Projection;
+ cameraPosition = camera.Position;
+ cameraRight = camera.Right;
+ cameraUp = camera.Up;
+
+ if (!Paused)
+ {
+ double frameTime = Math.Min(delta, FixedSimulationStep);
+ totalTime += frameTime;
+ simulationAccumulator = Math.Min(simulationAccumulator + frameTime, FixedSimulationStep * 2.0);
+ }
+ }
+
+ public void PushFluid(Vector3 origin, Vector3 direction)
+ {
+ simulation.Push(origin, direction);
+ }
+
+ public void Reset()
+ {
+ simulation.Reset();
+ Paused = false;
+ totalTime = 0.0;
+ simulationTime = 0.0;
+ simulationAccumulator = FixedSimulationStep;
+ }
+
+ public TimelineValue Simulate()
+ {
+ if (Paused)
+ {
+ simulationReady = simulation.Step(simulationTime, FixedSimulationStep, true);
+ }
+ else if (simulationAccumulator >= FixedSimulationStep)
+ {
+ simulationTime += FixedSimulationStep;
+ simulationAccumulator -= FixedSimulationStep;
+ simulationReady = simulation.Step(simulationTime, FixedSimulationStep, false);
+ }
+
+ UploadFrameConstants();
+
+ return simulationReady;
+ }
+
+ public void RenderScene(CommandBuffer commandBuffer)
+ {
+ commandBuffer.Transition(sceneColor, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+ commandBuffer.Transition(sceneLinearDepth, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+ commandBuffer.Transition(DepthStencil, default, TextureLayout.Undefined, TextureLayout.DepthStencilAttachment);
+
+ commandBuffer.BeginRenderPass(
+ [
+ ColorAttachment.Clear(sceneColor, new(0.0f, 0.0f, 0.0f, 1.0f)),
+ ColorAttachment.Clear(sceneLinearDepth, Vector4.Zero)
+ ], DepthStencilAttachment.Clear(DepthStencil, 1.0f, 0));
+ commandBuffer.SetPipeline(scenePipeline);
+ commandBuffer.SetVertexBuffer(sceneVertexBuffer, 0, 0);
+ commandBuffer.SetIndexBuffer(sceneIndexBuffer, 0, IndexFormat.UInt32);
+ commandBuffer.SetConstantBuffer(sceneConstantBuffer, 0);
+ commandBuffer.DrawIndexed(sceneIndexCount, 1, 0, 0, 0);
+ commandBuffer.EndRenderPass();
+
+ commandBuffer.Transition(sceneColor, default, TextureLayout.ColorAttachment, TextureLayout.Sampled);
+ commandBuffer.Transition(sceneLinearDepth, default, TextureLayout.ColorAttachment, TextureLayout.Sampled);
+ }
+
+ public void RenderFluid(CommandBuffer commandBuffer)
+ {
+ if (ViewMode is FluidViewMode.Particles)
+ {
+ RenderParticles(commandBuffer);
+
+ return;
+ }
+
+ commandBuffer.Transition(reconstructionDepth, default, TextureLayout.Undefined, TextureLayout.DepthStencilAttachment);
+ commandBuffer.Transition(smoothThicknessA, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+ commandBuffer.BeginRenderPass([ColorAttachment.Clear(smoothThicknessA, Vector4.Zero)], DepthStencilAttachment.Clear(reconstructionDepth, 1.0f, 0));
+ commandBuffer.SetPipeline(fluidThicknessPipeline);
+ commandBuffer.SetConstantBuffer(surfaceConstantBuffer, 0);
+ commandBuffer.Draw(4, simulation.ParticleCount, 0, 0);
+ commandBuffer.EndRenderPass();
+ commandBuffer.Transition(smoothThicknessA, default, TextureLayout.ColorAttachment, TextureLayout.Sampled);
+
+ commandBuffer.Transition(smoothDepthA, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+ commandBuffer.Transition(fluidAttributes, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+
+ commandBuffer.BeginRenderPass(
+ [
+ ColorAttachment.Clear(smoothDepthA, Vector4.Zero),
+ ColorAttachment.Clear(fluidAttributes, Vector4.Zero)
+ ], DepthStencilAttachment.Clear(reconstructionDepth, 1.0f, 0));
+ commandBuffer.SetPipeline(fluidDepthPipeline);
+ commandBuffer.SetConstantBuffer(surfaceConstantBuffer, 0);
+ commandBuffer.Draw(4, simulation.ParticleCount, 0, 0);
+ commandBuffer.EndRenderPass();
+
+ commandBuffer.Transition(smoothDepthA, default, TextureLayout.ColorAttachment, TextureLayout.Sampled);
+ commandBuffer.Transition(fluidAttributes, default, TextureLayout.ColorAttachment, TextureLayout.Sampled);
+
+ uint reconstructionWidth = smoothDepthA.Desc.Width;
+ uint reconstructionHeight = smoothDepthA.Desc.Height;
+
+ commandBuffer.SetPipeline(blurPipeline);
+
+ for (int iteration = 0; iteration < DepthSmoothingIterations; iteration++)
+ {
+ TextureLayout smoothDepthBLayout = iteration is 0 ? TextureLayout.Undefined : TextureLayout.Sampled;
+ commandBuffer.Transition(smoothDepthB, default, smoothDepthBLayout, TextureLayout.Storage);
+ commandBuffer.SetConstantBuffer(blurConstantBuffer, 0);
+ GraphicsHelper.Dispatch(commandBuffer, blurPipeline, reconstructionWidth, reconstructionHeight);
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.ComputeShading);
+ commandBuffer.Transition(smoothDepthB, default, TextureLayout.Storage, TextureLayout.Sampled);
+
+ commandBuffer.Transition(smoothDepthA, default, TextureLayout.Sampled, TextureLayout.Storage);
+ commandBuffer.SetConstantBuffer(blurConstantBuffer, 256);
+ GraphicsHelper.Dispatch(commandBuffer, blurPipeline, reconstructionWidth, reconstructionHeight);
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.ComputeShading);
+ commandBuffer.Transition(smoothDepthA, default, TextureLayout.Storage, TextureLayout.Sampled);
+ }
+
+ commandBuffer.Transition(smoothThicknessB, default, TextureLayout.Undefined, TextureLayout.Storage);
+ commandBuffer.SetPipeline(blurThicknessPipeline);
+ commandBuffer.SetConstantBuffer(blurConstantBuffer, 512);
+ GraphicsHelper.Dispatch(commandBuffer, blurThicknessPipeline, reconstructionWidth, reconstructionHeight);
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.ComputeShading);
+ commandBuffer.Transition(smoothThicknessB, default, TextureLayout.Storage, TextureLayout.Sampled);
+
+ commandBuffer.Transition(smoothThicknessA, default, TextureLayout.Sampled, TextureLayout.Storage);
+ commandBuffer.SetConstantBuffer(blurConstantBuffer, 768);
+ GraphicsHelper.Dispatch(commandBuffer, blurThicknessPipeline, reconstructionWidth, reconstructionHeight);
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.FragmentShading);
+ commandBuffer.Transition(smoothThicknessA, default, TextureLayout.Storage, TextureLayout.Sampled);
+
+ if (reflectionPipeline is not null)
+ {
+ commandBuffer.Transition(reflection!, default, TextureLayout.Undefined, TextureLayout.Storage);
+ commandBuffer.SetPipeline(reflectionPipeline);
+ commandBuffer.SetConstantBuffer(reflectionConstantBuffer, 0);
+ GraphicsHelper.Dispatch(commandBuffer, reflectionPipeline, reconstructionWidth, reconstructionHeight);
+ commandBuffer.Barrier(BarrierStages.ComputeShading, BarrierStages.FragmentShading);
+ commandBuffer.Transition(reflection!, default, TextureLayout.Storage, TextureLayout.Sampled);
+ }
+
+ commandBuffer.Transition(Color, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+ commandBuffer.BeginRenderPass([ColorAttachment.DontCare(Color)], null);
+ commandBuffer.SetPipeline(compositePipeline);
+ commandBuffer.SetConstantBuffer(compositeConstantBuffer, 0);
+ commandBuffer.Draw(3, 1, 0, 0);
+ commandBuffer.EndRenderPass();
+
+ commandBuffer.Transition(Color, default, TextureLayout.ColorAttachment, TextureLayout.ColorAttachment);
+ commandBuffer.Transition(DepthStencil, default, TextureLayout.DepthStencilAttachment, TextureLayout.DepthStencilAttachment);
+ commandBuffer.BeginRenderPass([ColorAttachment.Load(Color)], DepthStencilAttachment.Load(DepthStencil));
+ DrawGlass(commandBuffer);
+ commandBuffer.EndRenderPass();
+
+ commandBuffer.Transition(Color, default, TextureLayout.ColorAttachment, TextureLayout.Sampled);
+ }
+
+ public void Resize(uint width, uint height)
+ {
+ DisposeTargets();
+
+ Color = GraphicsHelper.CreateTexture(PixelFormat.B8G8R8A8UNorm, width, height, TextureUsages.Sampled | TextureUsages.ColorAttachment);
+ DepthStencil = GraphicsHelper.CreateTexture(PixelFormat.D32FloatS8UInt, width, height, TextureUsages.DepthStencilAttachment);
+ sceneColor = GraphicsHelper.CreateTexture(PixelFormat.R16G16B16A16Float, width, height, TextureUsages.Sampled | TextureUsages.ColorAttachment);
+ sceneLinearDepth = GraphicsHelper.CreateTexture(PixelFormat.R32Float, width, height, TextureUsages.Sampled | TextureUsages.ColorAttachment);
+ uint reconstructionWidth = Math.Max((width + 2) / 3, 1u);
+ uint reconstructionHeight = Math.Max((height + 2) / 3, 1u);
+ reconstructionDepth = GraphicsHelper.CreateTexture(PixelFormat.D32FloatS8UInt, reconstructionWidth, reconstructionHeight, TextureUsages.DepthStencilAttachment);
+ fluidAttributes = GraphicsHelper.CreateTexture(PixelFormat.R16G16B16A16Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.ColorAttachment);
+ smoothDepthA = GraphicsHelper.CreateTexture(PixelFormat.R32Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage | TextureUsages.ColorAttachment);
+ smoothDepthB = GraphicsHelper.CreateTexture(PixelFormat.R32Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage);
+ smoothThicknessA = GraphicsHelper.CreateTexture(PixelFormat.R16Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage | TextureUsages.ColorAttachment);
+ smoothThicknessB = GraphicsHelper.CreateTexture(PixelFormat.R16Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage);
+ reflection = App.Context.Capabilities.RayTracingSupported
+ ? GraphicsHelper.CreateTexture(PixelFormat.R16G16B16A16Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage)
+ : null;
+ }
+
+ public void Dispose()
+ {
+ simulation.Dispose();
+
+ sceneTlas?.Dispose();
+ sceneBlas?.Dispose();
+ reflectionPipeline?.Dispose();
+ glassPipeline.Dispose();
+ compositePipeline.Dispose();
+ blurThicknessPipeline.Dispose();
+ blurPipeline.Dispose();
+ particlePipeline.Dispose();
+ fluidThicknessPipeline.Dispose();
+ fluidDepthPipeline.Dispose();
+ scenePipeline.Dispose();
+
+ linearSampler.Dispose();
+ glassConstantBuffer.Dispose();
+ reflectionConstantBuffer.Dispose();
+ compositeConstantBuffer.Dispose();
+ blurConstantBuffer.Dispose();
+ surfaceConstantBuffer.Dispose();
+ sceneConstantBuffer.Dispose();
+ materialBuffer.Dispose();
+ glassIndexBuffer.Dispose();
+ glassVertexBuffer.Dispose();
+ sceneIndexBuffer.Dispose();
+ sceneVertexBuffer.Dispose();
+
+ DisposeTargets();
+ }
+
+ private void UploadFrameConstants()
+ {
+ Matrix4x4.Invert(view, out Matrix4x4 invView);
+ Matrix4x4.Invert(projection, out Matrix4x4 invProjection);
+ Vector3 sunDirection = Vector3.Normalize(new(-0.38f, -0.83f, -0.42f));
+ float surfaceRadius = FluidSimulation.ParticleRadius * (ViewMode is FluidViewMode.Particles ? 0.78f : 1.28f);
+ float interpolationAlpha = Paused ? 1.0f : (float)Math.Clamp(simulationAccumulator / FixedSimulationStep, 0.0, 1.0);
+
+ SceneConstants scene = new()
+ {
+ View = view,
+ Projection = projection,
+ CameraPosition = cameraPosition,
+ Time = (float)totalTime,
+ LightDirection = sunDirection,
+ LightIntensity = 1.55f,
+ Materials = materialBuffer.StorageReadOnlyHandle
+ };
+ GraphicsHelper.Upload(sceneConstantBuffer, 0, &scene, (uint)sizeof(SceneConstants));
+
+ SurfaceConstants surface = new()
+ {
+ View = view,
+ Projection = projection,
+ CameraRight = cameraRight,
+ ParticleRadius = surfaceRadius,
+ CameraUp = cameraUp,
+ RestDensity = FluidSimulation.RestDensity,
+ ParticleCount = simulation.ParticleCount,
+ RenderMode = (uint)ViewMode,
+ Width = ViewMode is FluidViewMode.Particles ? App.Width : smoothDepthA.Desc.Width,
+ Height = ViewMode is FluidViewMode.Particles ? App.Height : smoothDepthA.Desc.Height,
+ InterpolationAlpha = interpolationAlpha,
+ Particles = simulation.ParticleHandle,
+ PreviousPositions = simulation.PreviousPositionHandle,
+ SceneDepth = sceneLinearDepth.SampledHandle,
+ Sampler = linearSampler.Handle
+ };
+ GraphicsHelper.Upload(surfaceConstantBuffer, 0, &surface, (uint)sizeof(SurfaceConstants));
+
+ uint reconstructionWidth = smoothDepthA.Desc.Width;
+ uint reconstructionHeight = smoothDepthA.Desc.Height;
+
+ CompositeConstants composite = new()
+ {
+ InvView = invView,
+ InvProjection = invProjection,
+ CameraPosition = cameraPosition,
+ Time = (float)totalTime,
+ SunDirection = sunDirection,
+ Clarity = Clarity,
+ WaterColor = new(0.018f, 0.24f, 0.34f),
+ RefractionStrength = RefractionStrength,
+ Absorption = new(0.54f, 0.14f, 0.052f),
+ Ior = 1.333f,
+ Width = reconstructionWidth,
+ Height = reconstructionHeight,
+ RenderMode = (uint)ViewMode,
+ RayTracingEnabled = reflectionPipeline is not null ? 1u : 0u,
+ SceneColor = sceneColor.SampledHandle,
+ SceneDepth = sceneLinearDepth.SampledHandle,
+ FluidDepth = smoothDepthA.SampledHandle,
+ Thickness = smoothThicknessA.SampledHandle,
+ Attributes = fluidAttributes.SampledHandle,
+ Reflection = reflection?.SampledHandle ?? default,
+ Sampler = linearSampler.Handle
+ };
+ GraphicsHelper.Upload(compositeConstantBuffer, 0, &composite, (uint)sizeof(CompositeConstants));
+
+ if (ViewMode is FluidViewMode.Water)
+ {
+ BlurConstants horizontal = new()
+ {
+ Width = reconstructionWidth,
+ Height = reconstructionHeight,
+ Direction = 0,
+ Radius = 18,
+ SpatialSigma = 4.0f,
+ DepthSigma = surfaceRadius * 1.4f,
+ ProjectedRadiusScale = surfaceRadius * projection.M22 * reconstructionHeight * 0.5f,
+ InputTexture = smoothDepthA.SampledHandle,
+ OutputTexture = smoothDepthB.StorageHandle
+ };
+ GraphicsHelper.Upload(blurConstantBuffer, 0, &horizontal, (uint)sizeof(BlurConstants));
+
+ BlurConstants vertical = horizontal;
+ vertical.Direction = 1;
+ vertical.InputTexture = smoothDepthB.SampledHandle;
+ vertical.OutputTexture = smoothDepthA.StorageHandle;
+ GraphicsHelper.Upload(blurConstantBuffer, 256, &vertical, (uint)sizeof(BlurConstants));
+
+ BlurConstants horizontalThickness = horizontal;
+ horizontalThickness.Radius = 12;
+ horizontalThickness.SpatialSigma = 6.0f;
+ horizontalThickness.InputTexture = smoothThicknessA.SampledHandle;
+ horizontalThickness.OutputTexture = smoothThicknessB.StorageHandle;
+ GraphicsHelper.Upload(blurConstantBuffer, 512, &horizontalThickness, (uint)sizeof(BlurConstants));
+
+ BlurConstants verticalThickness = horizontalThickness;
+ verticalThickness.Direction = 1;
+ verticalThickness.InputTexture = smoothThicknessB.SampledHandle;
+ verticalThickness.OutputTexture = smoothThicknessA.StorageHandle;
+ GraphicsHelper.Upload(blurConstantBuffer, 768, &verticalThickness, (uint)sizeof(BlurConstants));
+
+ if (reflectionPipeline is not null)
+ {
+ ReflectionConstants reflectionParameters = new()
+ {
+ InvView = invView,
+ InvProjection = invProjection,
+ CameraPosition = cameraPosition,
+ Time = (float)totalTime,
+ SunDirection = sunDirection,
+ Width = reconstructionWidth,
+ Height = reconstructionHeight,
+ FluidDepth = smoothDepthA.SampledHandle,
+ Scene = sceneTlas!.Handle,
+ Vertices = sceneVertexBuffer.StorageReadOnlyHandle,
+ Indices = sceneIndexBuffer.StorageReadOnlyHandle,
+ Materials = materialBuffer.StorageReadOnlyHandle,
+ OutputTexture = reflection!.StorageHandle
+ };
+ GraphicsHelper.Upload(reflectionConstantBuffer, 0, &reflectionParameters, (uint)sizeof(ReflectionConstants));
+ }
+ }
+
+ GlassConstants glass = new()
+ {
+ View = view,
+ Projection = projection,
+ CameraPosition = cameraPosition,
+ Time = (float)totalTime
+ };
+ GraphicsHelper.Upload(glassConstantBuffer, 0, &glass, (uint)sizeof(GlassConstants));
+ }
+
+ private void RenderParticles(CommandBuffer commandBuffer)
+ {
+ commandBuffer.Transition(Color, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+ commandBuffer.BeginRenderPass([ColorAttachment.DontCare(Color)], null);
+ commandBuffer.SetPipeline(compositePipeline);
+ commandBuffer.SetConstantBuffer(compositeConstantBuffer, 0);
+ commandBuffer.Draw(3, 1, 0, 0);
+ commandBuffer.EndRenderPass();
+
+ commandBuffer.Transition(Color, default, TextureLayout.ColorAttachment, TextureLayout.ColorAttachment);
+ commandBuffer.Transition(DepthStencil, default, TextureLayout.DepthStencilAttachment, TextureLayout.DepthStencilAttachment);
+ commandBuffer.BeginRenderPass([ColorAttachment.Load(Color)], DepthStencilAttachment.Load(DepthStencil));
+ commandBuffer.SetPipeline(particlePipeline);
+ commandBuffer.SetConstantBuffer(surfaceConstantBuffer, 0);
+ commandBuffer.Draw(4, simulation.ParticleCount, 0, 0);
+ DrawGlass(commandBuffer);
+ commandBuffer.EndRenderPass();
+
+ commandBuffer.Transition(Color, default, TextureLayout.ColorAttachment, TextureLayout.Sampled);
+ }
+
+ private void DrawGlass(CommandBuffer commandBuffer)
+ {
+ commandBuffer.SetPipeline(glassPipeline);
+ commandBuffer.SetVertexBuffer(glassVertexBuffer, 0, 0);
+ commandBuffer.SetIndexBuffer(glassIndexBuffer, 0, IndexFormat.UInt32);
+ commandBuffer.SetConstantBuffer(glassConstantBuffer, 0);
+ commandBuffer.DrawIndexed(glassIndexCount, 1, 0, 0, 0);
+ }
+
+ private void DisposeTargets()
+ {
+ reflection?.Dispose();
+ reconstructionDepth?.Dispose();
+ smoothThicknessB?.Dispose();
+ smoothThicknessA?.Dispose();
+ smoothDepthB?.Dispose();
+ smoothDepthA?.Dispose();
+ fluidAttributes?.Dispose();
+ sceneLinearDepth?.Dispose();
+ sceneColor?.Dispose();
+ DepthStencil?.Dispose();
+ Color?.Dispose();
+ }
+
+ private static BlendState AdditiveBlend()
+ {
+ return new()
+ {
+ ColorAttachment0 = new()
+ {
+ IsBlendingEnabled = true,
+ SrcRgbFactor = BlendFactor.One,
+ DstRgbFactor = BlendFactor.One,
+ RgbOp = BlendOp.Add,
+ SrcAlphaFactor = BlendFactor.One,
+ DstAlphaFactor = BlendFactor.One,
+ AlphaOp = BlendOp.Add,
+ ColorWrites = ColorWrites.All
+ }
+ };
+ }
+}
+
+[StructLayout(LayoutKind.Explicit, Size = 176)]
+file struct SceneConstants
+{
+ [FieldOffset(0)]
+ public Matrix4x4 View;
+
+ [FieldOffset(64)]
+ public Matrix4x4 Projection;
+
+ [FieldOffset(128)]
+ public Vector3 CameraPosition;
+
+ [FieldOffset(140)]
+ public float Time;
+
+ [FieldOffset(144)]
+ public Vector3 LightDirection;
+
+ [FieldOffset(156)]
+ public float LightIntensity;
+
+ [FieldOffset(160)]
+ public ResourceHandle Materials;
+}
+
+[StructLayout(LayoutKind.Explicit, Size = 224)]
+file struct SurfaceConstants
+{
+ [FieldOffset(0)]
+ public Matrix4x4 View;
+
+ [FieldOffset(64)]
+ public Matrix4x4 Projection;
+
+ [FieldOffset(128)]
+ public Vector3 CameraRight;
+
+ [FieldOffset(140)]
+ public float ParticleRadius;
+
+ [FieldOffset(144)]
+ public Vector3 CameraUp;
+
+ [FieldOffset(156)]
+ public float RestDensity;
+
+ [FieldOffset(160)]
+ public uint ParticleCount;
+
+ [FieldOffset(164)]
+ public uint RenderMode;
+
+ [FieldOffset(168)]
+ public uint Width;
+
+ [FieldOffset(172)]
+ public uint Height;
+
+ [FieldOffset(176)]
+ public float InterpolationAlpha;
+
+ [FieldOffset(192)]
+ public ResourceHandle Particles;
+
+ [FieldOffset(200)]
+ public ResourceHandle PreviousPositions;
+
+ [FieldOffset(208)]
+ public ResourceHandle SceneDepth;
+
+ [FieldOffset(216)]
+ public ResourceHandle Sampler;
+}
+
+[StructLayout(LayoutKind.Explicit, Size = 48)]
+file struct BlurConstants
+{
+ [FieldOffset(0)]
+ public uint Width;
+
+ [FieldOffset(4)]
+ public uint Height;
+
+ [FieldOffset(8)]
+ public uint Direction;
+
+ [FieldOffset(12)]
+ public uint Radius;
+
+ [FieldOffset(16)]
+ public float SpatialSigma;
+
+ [FieldOffset(20)]
+ public float DepthSigma;
+
+ [FieldOffset(24)]
+ public float ProjectedRadiusScale;
+
+ [FieldOffset(32)]
+ public ResourceHandle InputTexture;
+
+ [FieldOffset(40)]
+ public ResourceHandle OutputTexture;
+}
+
+[StructLayout(LayoutKind.Explicit, Size = 272)]
+file struct CompositeConstants
+{
+ [FieldOffset(0)]
+ public Matrix4x4 InvView;
+
+ [FieldOffset(64)]
+ public Matrix4x4 InvProjection;
+
+ [FieldOffset(128)]
+ public Vector3 CameraPosition;
+
+ [FieldOffset(140)]
+ public float Time;
+
+ [FieldOffset(144)]
+ public Vector3 SunDirection;
+
+ [FieldOffset(156)]
+ public float Clarity;
+
+ [FieldOffset(160)]
+ public Vector3 WaterColor;
+
+ [FieldOffset(172)]
+ public float RefractionStrength;
+
+ [FieldOffset(176)]
+ public Vector3 Absorption;
+
+ [FieldOffset(188)]
+ public float Ior;
+
+ [FieldOffset(192)]
+ public uint Width;
+
+ [FieldOffset(196)]
+ public uint Height;
+
+ [FieldOffset(200)]
+ public uint RenderMode;
+
+ [FieldOffset(204)]
+ public uint RayTracingEnabled;
+
+ [FieldOffset(208)]
+ public ResourceHandle SceneColor;
+
+ [FieldOffset(216)]
+ public ResourceHandle SceneDepth;
+
+ [FieldOffset(224)]
+ public ResourceHandle FluidDepth;
+
+ [FieldOffset(232)]
+ public ResourceHandle Thickness;
+
+ [FieldOffset(240)]
+ public ResourceHandle Attributes;
+
+ [FieldOffset(248)]
+ public ResourceHandle Reflection;
+
+ [FieldOffset(256)]
+ public ResourceHandle Sampler;
+}
+
+[StructLayout(LayoutKind.Explicit, Size = 224)]
+file struct ReflectionConstants
+{
+ [FieldOffset(0)]
+ public Matrix4x4 InvView;
+
+ [FieldOffset(64)]
+ public Matrix4x4 InvProjection;
+
+ [FieldOffset(128)]
+ public Vector3 CameraPosition;
+
+ [FieldOffset(140)]
+ public float Time;
+
+ [FieldOffset(144)]
+ public Vector3 SunDirection;
+
+ [FieldOffset(160)]
+ public uint Width;
+
+ [FieldOffset(164)]
+ public uint Height;
+
+ [FieldOffset(176)]
+ public ResourceHandle FluidDepth;
+
+ [FieldOffset(184)]
+ public ResourceHandle Scene;
+
+ [FieldOffset(192)]
+ public ResourceHandle Vertices;
+
+ [FieldOffset(200)]
+ public ResourceHandle Indices;
+
+ [FieldOffset(208)]
+ public ResourceHandle Materials;
+
+ [FieldOffset(216)]
+ public ResourceHandle OutputTexture;
+}
+
+[StructLayout(LayoutKind.Explicit, Size = 144)]
+file struct GlassConstants
+{
+ [FieldOffset(0)]
+ public Matrix4x4 View;
+
+ [FieldOffset(64)]
+ public Matrix4x4 Projection;
+
+ [FieldOffset(128)]
+ public Vector3 CameraPosition;
+
+ [FieldOffset(140)]
+ public float Time;
+}
diff --git a/sources/Experiments/FluidTank/Handlers/CameraHandler.cs b/sources/Experiments/FluidTank/Handlers/CameraHandler.cs
new file mode 100644
index 00000000..a062aa82
--- /dev/null
+++ b/sources/Experiments/FluidTank/Handlers/CameraHandler.cs
@@ -0,0 +1,175 @@
+using System.Numerics;
+using Silk.NET.Input;
+
+namespace FluidTank.Handlers;
+
+internal class CameraHandler
+{
+ private readonly HashSet keyDowns = [];
+
+ private Vector2? lastMousePosition;
+ private Vector2? clickPosition;
+
+ public CameraHandler(IInputContext input, Vector3 position, Vector3 target)
+ {
+ IMouse mouse = input.Mice[0];
+ mouse.MouseDown += OnMouseDown;
+ mouse.MouseUp += OnMouseUp;
+ mouse.MouseMove += OnMouseMove;
+
+ IKeyboard keyboard = input.Keyboards[0];
+ keyboard.KeyDown += OnKeyDown;
+ keyboard.KeyUp += OnKeyUp;
+
+ Position = position;
+ Forward = Vector3.Normalize(target - position);
+ Right = Vector3.Normalize(Vector3.Cross(Forward, Vector3.UnitY));
+ Up = Vector3.Normalize(Vector3.Cross(Right, Forward));
+ }
+
+ public Vector2 Size { get; private set; } = new(800, 600);
+
+ public Vector3 Position { get; private set; }
+
+ public Vector3 Forward { get; private set; }
+
+ public Vector3 Right { get; private set; }
+
+ public Vector3 Up { get; private set; }
+
+ public float NearPlane { get; set; } = 0.1f;
+
+ public float FarPlane { get; set; } = 200.0f;
+
+ public float Fov { get; set; } = 45.0f;
+
+ public float Speed { get; set; } = 12.0f;
+
+ public float AspectRatio => Size.X / Size.Y;
+
+ public Matrix4x4 View => Matrix4x4.CreateLookAt(Position, Position + Forward, Up);
+
+ public Matrix4x4 Projection => Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(Fov), AspectRatio, NearPlane, FarPlane);
+
+ public void Update(double delta, uint width, uint height)
+ {
+ Size = new(width, height);
+
+ float distance = Speed * (float)delta;
+
+ if (keyDowns.Contains(Key.W))
+ {
+ Position += Forward * distance;
+ }
+
+ if (keyDowns.Contains(Key.S))
+ {
+ Position -= Forward * distance;
+ }
+
+ if (keyDowns.Contains(Key.A))
+ {
+ Position -= Right * distance;
+ }
+
+ if (keyDowns.Contains(Key.D))
+ {
+ Position += Right * distance;
+ }
+
+ if (keyDowns.Contains(Key.Q))
+ {
+ Position -= Up * distance;
+ }
+
+ if (keyDowns.Contains(Key.E))
+ {
+ Position += Up * distance;
+ }
+ }
+
+ public bool TryConsumeClickRay(out Vector3 origin, out Vector3 direction)
+ {
+ if (!clickPosition.HasValue)
+ {
+ origin = default;
+ direction = default;
+
+ return false;
+ }
+
+ Vector2 position = clickPosition.Value;
+ clickPosition = null;
+
+ float x = (position.X / Size.X * 2.0f) - 1.0f;
+ float y = 1.0f - (position.Y / Size.Y * 2.0f);
+
+ Matrix4x4.Invert(Projection, out Matrix4x4 invProjection);
+ Matrix4x4.Invert(View, out Matrix4x4 invView);
+
+ Vector4 target = Vector4.Transform(new Vector4(x, y, 1.0f, 1.0f), invProjection);
+ Vector3 localDirection = Vector3.Normalize(new(target.X / target.W, target.Y / target.W, target.Z / target.W));
+
+ origin = Position;
+ direction = Vector3.Normalize(Vector3.TransformNormal(localDirection, invView));
+
+ return true;
+ }
+
+ private void OnMouseDown(IMouse mouse, MouseButton button)
+ {
+ if (button is MouseButton.Right)
+ {
+ lastMousePosition = mouse.Position;
+ }
+ else if (button is MouseButton.Left)
+ {
+ clickPosition = mouse.Position;
+ }
+ }
+
+ private void OnMouseUp(IMouse mouse, MouseButton button)
+ {
+ if (button is MouseButton.Right)
+ {
+ lastMousePosition = null;
+ }
+ }
+
+ private void OnMouseMove(IMouse mouse, Vector2 position)
+ {
+ const float clipRadians = 89.0f * MathF.PI / 180.0f;
+
+ if (!lastMousePosition.HasValue)
+ {
+ return;
+ }
+
+ float pixelToRadianX = MathF.PI / Size.X;
+ float pixelToRadianY = MathF.PI / Size.Y;
+ Vector2 delta = position - lastMousePosition.Value;
+ float yaw = -(delta.X * pixelToRadianX);
+ float pitch = -(delta.Y * pixelToRadianY);
+ float newPitch = Math.Clamp(MathF.Asin(Forward.Y) + pitch, -clipRadians, clipRadians);
+
+ pitch = newPitch - MathF.Asin(Forward.Y);
+
+ Forward = Vector3.TransformNormal(Forward, Matrix4x4.CreateFromAxisAngle(Up, yaw));
+ Forward = Vector3.TransformNormal(Forward, Matrix4x4.CreateFromAxisAngle(Right, pitch));
+ Forward = Vector3.Normalize(Forward);
+ Right = Vector3.Normalize(Vector3.Cross(Forward, Vector3.UnitY));
+ Up = Vector3.Normalize(Vector3.Cross(Right, Forward));
+
+ lastMousePosition = position;
+ }
+
+ private void OnKeyDown(IKeyboard keyboard, Key key, int scanCode)
+ {
+ keyDowns.Add(key);
+ }
+
+ private void OnKeyUp(IKeyboard keyboard, Key key, int scanCode)
+ {
+ keyDowns.Remove(key);
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/FluidTank/Handlers/ImGuiHandler.cs b/sources/Experiments/FluidTank/Handlers/ImGuiHandler.cs
new file mode 100644
index 00000000..7a98b6d7
--- /dev/null
+++ b/sources/Experiments/FluidTank/Handlers/ImGuiHandler.cs
@@ -0,0 +1,196 @@
+using System.Numerics;
+using Hexa.NET.ImGui;
+using Silk.NET.Input;
+using Zenith.NET;
+using Zenith.NET.Extensions.ImGui;
+
+namespace FluidTank.Handlers;
+
+internal class ImGuiHandler : ImGuiController, IImGuiPlatformBindings
+{
+ private readonly IMouse mouse;
+ private readonly IKeyboard keyboard;
+
+ public ImGuiHandler(IInputContext input, AttachmentFormats attachmentFormats) : base(App.Context, attachmentFormats, ImGuiColorSpace.Legacy, Path.Combine(AppContext.BaseDirectory, "Assets", "Fonts", "msyh.ttf"), OtherSetup)
+ {
+ mouse = input.Mice[0];
+ mouse.MouseDown += OnMouseDown;
+ mouse.MouseUp += OnMouseUp;
+ mouse.MouseMove += OnMouseMove;
+ mouse.Scroll += OnMouseScroll;
+
+ keyboard = input.Keyboards[0];
+ keyboard.KeyDown += OnKeyDown;
+ keyboard.KeyUp += OnKeyUp;
+ keyboard.KeyChar += OnKeyChar;
+
+ PlatformBindings = this;
+ }
+
+ private void OnMouseDown(IMouse mouse, MouseButton button)
+ {
+ MouseDown(button switch
+ {
+ MouseButton.Left => ImGuiMouseButton.Left,
+ MouseButton.Right => ImGuiMouseButton.Right,
+ MouseButton.Middle => ImGuiMouseButton.Middle,
+ _ => (int)ImGuiMouseButton.Count + (int)button - ImGuiMouseButton.Middle
+ });
+ }
+
+ private void OnMouseUp(IMouse mouse, MouseButton button)
+ {
+ MouseUp(button switch
+ {
+ MouseButton.Left => ImGuiMouseButton.Left,
+ MouseButton.Right => ImGuiMouseButton.Right,
+ MouseButton.Middle => ImGuiMouseButton.Middle,
+ _ => (int)ImGuiMouseButton.Count + (int)button - ImGuiMouseButton.Middle
+ });
+ }
+
+ private void OnMouseMove(IMouse mouse, Vector2 position)
+ {
+ MouseMove(position);
+ }
+
+ private void OnMouseScroll(IMouse mouse, ScrollWheel offset)
+ {
+ MouseWheel(new(offset.X, offset.Y));
+ }
+
+ private void OnKeyDown(IKeyboard keyboard, Key key, int scanCode)
+ {
+ KeyDown(TranslateInputKeyToImGuiKey(key));
+ KeyDown(TranslateInputKeyToImGuiModifier(key));
+ }
+
+ private void OnKeyUp(IKeyboard keyboard, Key key, int scanCode)
+ {
+ KeyUp(TranslateInputKeyToImGuiKey(key));
+ KeyUp(TranslateInputKeyToImGuiModifier(key));
+ }
+
+ private void OnKeyChar(IKeyboard keyboard, char c)
+ {
+ KeyChar(c);
+ }
+
+ private static ImGuiKey TranslateInputKeyToImGuiKey(Key key)
+ {
+ return key switch
+ {
+ Key.Tab => ImGuiKey.Tab,
+ Key.Left => ImGuiKey.LeftArrow,
+ Key.Right => ImGuiKey.RightArrow,
+ Key.Up => ImGuiKey.UpArrow,
+ Key.Down => ImGuiKey.DownArrow,
+ Key.PageUp => ImGuiKey.PageUp,
+ Key.PageDown => ImGuiKey.PageDown,
+ Key.Home => ImGuiKey.Home,
+ Key.End => ImGuiKey.End,
+ Key.Insert => ImGuiKey.Insert,
+ Key.Delete => ImGuiKey.Delete,
+ Key.Backspace => ImGuiKey.Backspace,
+ Key.Space => ImGuiKey.Space,
+ Key.Enter => ImGuiKey.Enter,
+ Key.Escape => ImGuiKey.Escape,
+ Key.Apostrophe => ImGuiKey.Apostrophe,
+ Key.Comma => ImGuiKey.Comma,
+ Key.Minus => ImGuiKey.Minus,
+ Key.Period => ImGuiKey.Period,
+ Key.Slash => ImGuiKey.Slash,
+ Key.Semicolon => ImGuiKey.Semicolon,
+ Key.Equal => ImGuiKey.Equal,
+ Key.LeftBracket => ImGuiKey.LeftBracket,
+ Key.BackSlash => ImGuiKey.Backslash,
+ Key.RightBracket => ImGuiKey.RightBracket,
+ Key.GraveAccent => ImGuiKey.GraveAccent,
+ Key.CapsLock => ImGuiKey.CapsLock,
+ Key.ScrollLock => ImGuiKey.ScrollLock,
+ Key.NumLock => ImGuiKey.NumLock,
+ Key.PrintScreen => ImGuiKey.PrintScreen,
+ Key.Pause => ImGuiKey.Pause,
+ Key.Keypad0 => ImGuiKey.Keypad0,
+ Key.Keypad1 => ImGuiKey.Keypad1,
+ Key.Keypad2 => ImGuiKey.Keypad2,
+ Key.Keypad3 => ImGuiKey.Keypad3,
+ Key.Keypad4 => ImGuiKey.Keypad4,
+ Key.Keypad5 => ImGuiKey.Keypad5,
+ Key.Keypad6 => ImGuiKey.Keypad6,
+ Key.Keypad7 => ImGuiKey.Keypad7,
+ Key.Keypad8 => ImGuiKey.Keypad8,
+ Key.Keypad9 => ImGuiKey.Keypad9,
+ Key.KeypadDecimal => ImGuiKey.KeypadDecimal,
+ Key.KeypadDivide => ImGuiKey.KeypadDivide,
+ Key.KeypadMultiply => ImGuiKey.KeypadMultiply,
+ Key.KeypadSubtract => ImGuiKey.KeypadSubtract,
+ Key.KeypadAdd => ImGuiKey.KeypadAdd,
+ Key.KeypadEnter => ImGuiKey.KeypadEnter,
+ Key.KeypadEqual => ImGuiKey.KeypadEqual,
+ Key.ShiftLeft => ImGuiKey.LeftShift,
+ Key.ControlLeft => ImGuiKey.LeftCtrl,
+ Key.AltLeft => ImGuiKey.LeftAlt,
+ Key.SuperLeft => ImGuiKey.LeftSuper,
+ Key.ShiftRight => ImGuiKey.RightShift,
+ Key.ControlRight => ImGuiKey.RightCtrl,
+ Key.AltRight => ImGuiKey.RightAlt,
+ Key.SuperRight => ImGuiKey.RightSuper,
+ Key.Menu => ImGuiKey.Menu,
+ >= Key.Number0 and <= Key.Number9 => ImGuiKey.Key0 + (key - Key.Number0),
+ >= Key.A and <= Key.Z => ImGuiKey.A + (key - Key.A),
+ >= Key.F1 and <= Key.F24 => ImGuiKey.F1 + (key - Key.F1),
+ _ => ImGuiKey.None
+ };
+ }
+
+ private static ImGuiKey TranslateInputKeyToImGuiModifier(Key key)
+ {
+ return key switch
+ {
+ Key.ShiftLeft or Key.ShiftRight => ImGuiKey.ModShift,
+ Key.ControlLeft or Key.ControlRight => ImGuiKey.ModCtrl,
+ Key.AltLeft or Key.AltRight => ImGuiKey.ModAlt,
+ Key.SuperLeft or Key.SuperRight => ImGuiKey.ModSuper,
+ _ => ImGuiKey.None
+ };
+ }
+
+ private static unsafe void OtherSetup(ImGuiIOPtr io)
+ {
+ io.ConfigFlags |= ImGuiConfigFlags.DockingEnable;
+ io.DisplayFramebufferScale = App.DpiScale;
+ io.IniFilename = null;
+ }
+
+ public void SetCursor(ImGuiMouseCursor cursor)
+ {
+ mouse.Cursor.StandardCursor = cursor switch
+ {
+ ImGuiMouseCursor.Arrow => StandardCursor.Arrow,
+ ImGuiMouseCursor.TextInput => StandardCursor.IBeam,
+ ImGuiMouseCursor.ResizeAll => StandardCursor.ResizeAll,
+ ImGuiMouseCursor.ResizeNs => StandardCursor.VResize,
+ ImGuiMouseCursor.ResizeEw => StandardCursor.HResize,
+ ImGuiMouseCursor.ResizeNesw => StandardCursor.NeswResize,
+ ImGuiMouseCursor.ResizeNwse => StandardCursor.NwseResize,
+ ImGuiMouseCursor.Hand => StandardCursor.Hand,
+ ImGuiMouseCursor.NotAllowed => StandardCursor.NotAllowed,
+ _ => StandardCursor.Default
+ };
+ }
+
+ public string GetClipboardText()
+ {
+ return keyboard.ClipboardText;
+ }
+
+ public void SetClipboardText(string text)
+ {
+ keyboard.ClipboardText = text;
+ }
+
+ public void SetImeData(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data)
+ {
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/FluidTank/Helpers/CocoaHelper.cs b/sources/Experiments/FluidTank/Helpers/CocoaHelper.cs
new file mode 100644
index 00000000..dfe56d56
--- /dev/null
+++ b/sources/Experiments/FluidTank/Helpers/CocoaHelper.cs
@@ -0,0 +1,34 @@
+using System.Runtime.InteropServices;
+
+namespace FluidTank.Helpers;
+
+internal static partial class CocoaHelper
+{
+ private const string LibObjC = "/usr/lib/libobjc.A.dylib";
+
+ [LibraryImport(LibObjC, EntryPoint = "objc_getClass")]
+ private static partial nint GetClass([MarshalAs(UnmanagedType.LPUTF8Str)] string name);
+
+ [LibraryImport(LibObjC, EntryPoint = "sel_registerName")]
+ private static partial nint Selector([MarshalAs(UnmanagedType.LPUTF8Str)] string name);
+
+ [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
+ private static partial nint Send(nint receiver, nint selector);
+
+ [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
+ private static partial nint Send(nint receiver, nint selector, [MarshalAs(UnmanagedType.I1)] bool arg);
+
+ [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
+ private static partial nint Send(nint receiver, nint selector, nint arg);
+
+ public static nint CreateLayer(nint cocoa)
+ {
+ nint layer = Send(GetClass("CAMetalLayer"), Selector("layer"));
+ nint view = Send(cocoa, Selector("contentView"));
+
+ Send(view, Selector("setWantsLayer:"), true);
+ Send(view, Selector("setLayer:"), layer);
+
+ return layer;
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/FluidTank/Helpers/FluidTankGeometry.cs b/sources/Experiments/FluidTank/Helpers/FluidTankGeometry.cs
new file mode 100644
index 00000000..0344ef6d
--- /dev/null
+++ b/sources/Experiments/FluidTank/Helpers/FluidTankGeometry.cs
@@ -0,0 +1,190 @@
+using System.Numerics;
+using System.Runtime.InteropServices;
+
+namespace FluidTank.Helpers;
+
+internal static class FluidTankGeometry
+{
+ public static void CreateScene(out SceneVertex[] vertices, out uint[] indices, out SceneMaterial[] materials)
+ {
+ List verticesList = [];
+ List indicesList = [];
+
+ AddBox(verticesList, indicesList, new(-6.25f, -0.28f, -3.25f), new(6.25f, 0.0f, 3.25f), 0);
+ AddBox(verticesList, indicesList, new(-2.17f, 0.0f, 0.23f), new(-0.73f, 1.64f, 1.67f), 1);
+ AddCylinder(verticesList, indicesList, new(1.15f, 0.0f, -0.85f), 0.62f, 1.84f, 32, 2);
+ AddTransformedBox(verticesList,
+ indicesList,
+ new(3.45f, 0.62f, 0.55f),
+ new(1.15f, 0.16f, 1.05f),
+ Matrix4x4.CreateRotationZ(-0.35f),
+ 3);
+
+ const float frameWidth = 0.075f;
+
+ AddBox(verticesList, indicesList, new(-6.10f, 0.0f, -3.10f), new(6.10f, frameWidth, -2.95f), 4);
+ AddBox(verticesList, indicesList, new(-6.10f, 0.0f, 2.95f), new(6.10f, frameWidth, 3.10f), 4);
+ AddBox(verticesList, indicesList, new(-6.10f, 0.0f, -2.95f), new(-5.95f, frameWidth, 2.95f), 4);
+ AddBox(verticesList, indicesList, new(5.95f, 0.0f, -2.95f), new(6.10f, frameWidth, 2.95f), 4);
+
+ AddBox(verticesList, indicesList, new(-6.10f, 0.0f, -3.10f), new(-5.95f, 5.25f, -2.95f), 4);
+ AddBox(verticesList, indicesList, new(5.95f, 0.0f, -3.10f), new(6.10f, 5.25f, -2.95f), 4);
+ AddBox(verticesList, indicesList, new(-6.10f, 0.0f, 2.95f), new(-5.95f, 5.25f, 3.10f), 4);
+ AddBox(verticesList, indicesList, new(5.95f, 0.0f, 2.95f), new(6.10f, 5.25f, 3.10f), 4);
+
+ AddBox(verticesList, indicesList, new(-6.10f, 5.12f, -3.10f), new(6.10f, 5.27f, -2.95f), 4);
+ AddBox(verticesList, indicesList, new(-6.10f, 5.12f, 2.95f), new(6.10f, 5.27f, 3.10f), 4);
+ AddBox(verticesList, indicesList, new(-6.10f, 5.12f, -2.95f), new(-5.95f, 5.27f, 2.95f), 4);
+ AddBox(verticesList, indicesList, new(5.95f, 5.12f, -2.95f), new(6.10f, 5.27f, 2.95f), 4);
+
+ vertices = [.. verticesList];
+ indices = [.. indicesList];
+ materials =
+ [
+ new() { Albedo = new(0.38f, 0.52f, 0.55f), Roughness = 0.48f, Metallic = 0.04f },
+ new() { Albedo = new(0.12f, 0.28f, 0.31f), Roughness = 0.24f, Metallic = 0.18f },
+ new() { Albedo = new(0.56f, 0.63f, 0.66f), Roughness = 0.16f, Metallic = 0.72f },
+ new() { Albedo = new(0.08f, 0.20f, 0.28f), Roughness = 0.18f, Metallic = 0.35f },
+ new() { Albedo = new(0.55f, 0.68f, 0.72f), Roughness = 0.10f, Metallic = 0.88f }
+ ];
+ }
+
+ public static void CreateGlass(out SceneVertex[] vertices, out uint[] indices)
+ {
+ List verticesList = [];
+ List indicesList = [];
+
+ AddQuad(verticesList, indicesList, new(-6.0f, 0.0f, -3.0f), new(6.0f, 0.0f, -3.0f), new(6.0f, 5.2f, -3.0f), new(-6.0f, 5.2f, -3.0f), 0);
+ AddQuad(verticesList, indicesList, new(6.0f, 0.0f, 3.0f), new(-6.0f, 0.0f, 3.0f), new(-6.0f, 5.2f, 3.0f), new(6.0f, 5.2f, 3.0f), 0);
+ AddQuad(verticesList, indicesList, new(-6.0f, 0.0f, 3.0f), new(-6.0f, 0.0f, -3.0f), new(-6.0f, 5.2f, -3.0f), new(-6.0f, 5.2f, 3.0f), 0);
+ AddQuad(verticesList, indicesList, new(6.0f, 0.0f, -3.0f), new(6.0f, 0.0f, 3.0f), new(6.0f, 5.2f, 3.0f), new(6.0f, 5.2f, -3.0f), 0);
+
+ vertices = [.. verticesList];
+ indices = [.. indicesList];
+ }
+
+ private static void AddBox(List vertices, List indices, Vector3 minimum, Vector3 maximum, uint materialId)
+ {
+ AddTransformedBox(vertices, indices, (minimum + maximum) * 0.5f, (maximum - minimum) * 0.5f, Matrix4x4.Identity, materialId);
+ }
+
+ private static void AddTransformedBox(List vertices,
+ List indices,
+ Vector3 center,
+ Vector3 halfExtents,
+ Matrix4x4 rotation,
+ uint materialId)
+ {
+ Vector3[] local =
+ [
+ new(-halfExtents.X, -halfExtents.Y, -halfExtents.Z),
+ new(halfExtents.X, -halfExtents.Y, -halfExtents.Z),
+ new(halfExtents.X, halfExtents.Y, -halfExtents.Z),
+ new(-halfExtents.X, halfExtents.Y, -halfExtents.Z),
+ new(-halfExtents.X, -halfExtents.Y, halfExtents.Z),
+ new(halfExtents.X, -halfExtents.Y, halfExtents.Z),
+ new(halfExtents.X, halfExtents.Y, halfExtents.Z),
+ new(-halfExtents.X, halfExtents.Y, halfExtents.Z)
+ ];
+
+ Vector3[] points = new Vector3[local.Length];
+ for (int index = 0; index < local.Length; index++)
+ {
+ points[index] = Vector3.Transform(local[index], rotation) + center;
+ }
+
+ AddQuad(vertices, indices, points[1], points[0], points[3], points[2], materialId);
+ AddQuad(vertices, indices, points[4], points[5], points[6], points[7], materialId);
+ AddQuad(vertices, indices, points[0], points[4], points[7], points[3], materialId);
+ AddQuad(vertices, indices, points[5], points[1], points[2], points[6], materialId);
+ AddQuad(vertices, indices, points[3], points[7], points[6], points[2], materialId);
+ AddQuad(vertices, indices, points[0], points[1], points[5], points[4], materialId);
+ }
+
+ private static void AddCylinder(List vertices,
+ List indices,
+ Vector3 baseCenter,
+ float radius,
+ float height,
+ int segments,
+ uint materialId)
+ {
+ for (int segment = 0; segment < segments; segment++)
+ {
+ float angle0 = segment * MathF.Tau / segments;
+ float angle1 = (segment + 1) * MathF.Tau / segments;
+ Vector3 radial0 = new(MathF.Cos(angle0) * radius, 0.0f, MathF.Sin(angle0) * radius);
+ Vector3 radial1 = new(MathF.Cos(angle1) * radius, 0.0f, MathF.Sin(angle1) * radius);
+
+ AddQuad(vertices, indices, baseCenter + radial1, baseCenter + radial0, baseCenter + radial0 + (Vector3.UnitY * height), baseCenter + radial1 + (Vector3.UnitY * height), materialId);
+ AddTriangle(vertices, indices, baseCenter + (Vector3.UnitY * height), baseCenter + radial1 + (Vector3.UnitY * height), baseCenter + radial0 + (Vector3.UnitY * height), materialId);
+ }
+ }
+
+ private static void AddTriangle(List vertices, List indices, Vector3 v0, Vector3 v1, Vector3 v2, uint materialId)
+ {
+ Vector3 normal = Vector3.Normalize(Vector3.Cross(v1 - v0, v2 - v0));
+ uint startIndex = (uint)vertices.Count;
+
+ vertices.Add(new() { Position = v0, Normal = normal, MaterialId = materialId });
+ vertices.Add(new() { Position = v1, Normal = normal, MaterialId = materialId });
+ vertices.Add(new() { Position = v2, Normal = normal, MaterialId = materialId });
+
+ indices.Add(startIndex);
+ indices.Add(startIndex + 1);
+ indices.Add(startIndex + 2);
+ }
+
+ private static void AddQuad(List vertices,
+ List indices,
+ Vector3 v0,
+ Vector3 v1,
+ Vector3 v2,
+ Vector3 v3,
+ uint materialId)
+ {
+ Vector3 normal = Vector3.Normalize(Vector3.Cross(v1 - v0, v2 - v0));
+ uint startIndex = (uint)vertices.Count;
+
+ vertices.Add(new() { Position = v0, Normal = normal, MaterialId = materialId });
+ vertices.Add(new() { Position = v1, Normal = normal, MaterialId = materialId });
+ vertices.Add(new() { Position = v2, Normal = normal, MaterialId = materialId });
+ vertices.Add(new() { Position = v3, Normal = normal, MaterialId = materialId });
+
+ indices.Add(startIndex);
+ indices.Add(startIndex + 1);
+ indices.Add(startIndex + 2);
+ indices.Add(startIndex);
+ indices.Add(startIndex + 2);
+ indices.Add(startIndex + 3);
+ }
+}
+
+[StructLayout(LayoutKind.Explicit, Size = 32)]
+internal struct SceneVertex
+{
+ [FieldOffset(0)]
+ public Vector3 Position;
+
+ [FieldOffset(16)]
+ public Vector3 Normal;
+
+ [FieldOffset(28)]
+ public uint MaterialId;
+}
+
+[StructLayout(LayoutKind.Explicit, Size = 32)]
+internal struct SceneMaterial
+{
+ [FieldOffset(0)]
+ public Vector3 Albedo;
+
+ [FieldOffset(12)]
+ public float Roughness;
+
+ [FieldOffset(16)]
+ public float Metallic;
+
+ [FieldOffset(20)]
+ public float Emission;
+}
diff --git a/sources/Experiments/FluidTank/Helpers/GraphicsHelper.cs b/sources/Experiments/FluidTank/Helpers/GraphicsHelper.cs
new file mode 100644
index 00000000..374fdfef
--- /dev/null
+++ b/sources/Experiments/FluidTank/Helpers/GraphicsHelper.cs
@@ -0,0 +1,151 @@
+using Zenith.NET;
+using Buffer = Zenith.NET.Buffer;
+
+namespace FluidTank.Helpers;
+
+internal static unsafe class GraphicsHelper
+{
+ public static string ShaderPath(params string[] paths)
+ {
+ return Path.Combine([AppContext.BaseDirectory, "Assets", "Shaders", .. paths]);
+ }
+
+ public static Buffer CreateBuffer(uint count, uint strideInBytes, BufferUsages usages)
+ {
+ return App.Context.CreateBuffer(new()
+ {
+ SizeInBytes = count * strideInBytes,
+ StrideInBytes = strideInBytes,
+ Usages = usages,
+ Residency = MemoryResidency.GpuOnly
+ });
+ }
+
+ public static Buffer LoadBuffer(CommandBuffer commandBuffer, T[] data, BufferUsages usages) where T : unmanaged
+ {
+ Buffer buffer = CreateBuffer((uint)data.Length, (uint)sizeof(T), usages | BufferUsages.TransferDst);
+
+ fixed (T* pointer = data)
+ {
+ commandBuffer.Upload(buffer, 0, new()
+ {
+ Pointer = (nint)pointer,
+ SizeInBytes = (uint)(sizeof(T) * data.Length)
+ });
+ }
+
+ return buffer;
+ }
+
+ public static Buffer CreateConstantBuffer(uint sizeInBytes)
+ {
+ return App.Context.CreateBuffer(new()
+ {
+ SizeInBytes = sizeInBytes,
+ Usages = BufferUsages.Constant,
+ Residency = MemoryResidency.CpuWriteOnly
+ });
+ }
+
+ public static Buffer CreateConstantBuffer() where T : unmanaged
+ {
+ return CreateConstantBuffer((uint)sizeof(T));
+ }
+
+ public static Texture CreateTexture(PixelFormat format, uint width, uint height, TextureUsages usages)
+ {
+ return App.Context.CreateTexture(new()
+ {
+ Type = TextureType.Texture2D,
+ Format = format,
+ Width = width,
+ Height = height,
+ Depth = 1,
+ MipLevels = 1,
+ ArrayLayers = 1,
+ SampleCount = SampleCount.Count1,
+ Usages = usages
+ });
+ }
+
+ public static Shader LoadShader(string file, string entryPoint)
+ {
+ return App.Context.CreateShader(ZenithCompiler.CompileFromFile(App.Context.GraphicsApi, ShaderPath(file), entryPoint));
+ }
+
+ public static GraphicsPipeline CreateGraphicsPipeline(string file,
+ string vertexEntryPoint,
+ string fragmentEntryPoint,
+ InputLayout[] inputLayouts,
+ AttachmentFormats attachmentFormats,
+ RasterizerState rasterizer,
+ DepthStencilState depthStencil,
+ BlendState blend,
+ PrimitiveTopology primitiveTopology = PrimitiveTopology.TriangleList)
+ {
+ using Shader vertexShader = LoadShader(file, vertexEntryPoint);
+
+ return CreateGraphicsPipeline(vertexShader,
+ file,
+ fragmentEntryPoint,
+ inputLayouts,
+ attachmentFormats,
+ rasterizer,
+ depthStencil,
+ blend,
+ primitiveTopology);
+ }
+
+ public static GraphicsPipeline CreateGraphicsPipeline(Shader vertexShader,
+ string file,
+ string fragmentEntryPoint,
+ InputLayout[] inputLayouts,
+ AttachmentFormats attachmentFormats,
+ RasterizerState rasterizer,
+ DepthStencilState depthStencil,
+ BlendState blend,
+ PrimitiveTopology primitiveTopology = PrimitiveTopology.TriangleList)
+ {
+ using Shader fragmentShader = LoadShader(file, fragmentEntryPoint);
+
+ return App.Context.CreateGraphicsPipeline(new()
+ {
+ VertexShader = vertexShader,
+ FragmentShader = fragmentShader,
+ InputLayouts = inputLayouts,
+ PrimitiveTopology = primitiveTopology,
+ AttachmentFormats = attachmentFormats,
+ RenderState = new()
+ {
+ Rasterizer = rasterizer,
+ DepthStencil = depthStencil,
+ Blend = blend
+ }
+ });
+ }
+
+ public static ComputePipeline CreateComputePipeline(string file, string entryPoint)
+ {
+ using Shader shader = LoadShader(file, entryPoint);
+
+ return App.Context.CreateComputePipeline(new() { ComputeShader = shader });
+ }
+
+ public static void Dispatch(CommandBuffer commandBuffer, ComputePipeline pipeline, uint width, uint height)
+ {
+ ThreadGroupSize groupSize = pipeline.Desc.ComputeShader.Desc.ThreadGroupSize;
+
+ commandBuffer.Dispatch((width + groupSize.X - 1) / groupSize.X,
+ (height + groupSize.Y - 1) / groupSize.Y,
+ 1);
+ }
+
+ public static void Upload(Buffer buffer, uint offsetInBytes, void* data, uint sizeInBytes)
+ {
+ buffer.Upload(offsetInBytes, new()
+ {
+ Pointer = (nint)data,
+ SizeInBytes = sizeInBytes
+ });
+ }
+}
diff --git a/sources/Experiments/FluidTank/Helpers/ImGuiHelper.cs b/sources/Experiments/FluidTank/Helpers/ImGuiHelper.cs
new file mode 100644
index 00000000..76a90407
--- /dev/null
+++ b/sources/Experiments/FluidTank/Helpers/ImGuiHelper.cs
@@ -0,0 +1,32 @@
+using Hexa.NET.ImGui;
+
+namespace FluidTank.Helpers;
+
+internal static class ImGuiHelper
+{
+ public static void Overlay(Action action)
+ {
+ ImGui.SetNextWindowPos(new(10, 10), ImGuiCond.Always, new(0, 0));
+ ImGui.SetNextWindowBgAlpha(0.35f);
+
+ if (ImGui.Begin("Overlay", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoDecoration))
+ {
+ action();
+ }
+
+ ImGui.End();
+ }
+
+ public static void Settings(Action action)
+ {
+ ImGui.SetNextWindowPos(new(ImGui.GetIO().DisplaySize.X - 10, 10), ImGuiCond.Always, new(1, 0));
+ ImGui.SetNextWindowCollapsed(true, ImGuiCond.FirstUseEver);
+
+ if (ImGui.Begin("Settings", ImGuiWindowFlags.AlwaysAutoResize))
+ {
+ action();
+ }
+
+ ImGui.End();
+ }
+}
diff --git a/sources/Experiments/FluidTank/Program.cs b/sources/Experiments/FluidTank/Program.cs
new file mode 100644
index 00000000..4e172a48
--- /dev/null
+++ b/sources/Experiments/FluidTank/Program.cs
@@ -0,0 +1,3 @@
+using FluidTank;
+
+App.Run();
\ No newline at end of file
diff --git a/sources/Experiments/MemoryPlayground/Properties/launchSettings.json b/sources/Experiments/FluidTank/Properties/launchSettings.json
similarity index 62%
rename from sources/Experiments/MemoryPlayground/Properties/launchSettings.json
rename to sources/Experiments/FluidTank/Properties/launchSettings.json
index c8bb0840..a915f1e6 100644
--- a/sources/Experiments/MemoryPlayground/Properties/launchSettings.json
+++ b/sources/Experiments/FluidTank/Properties/launchSettings.json
@@ -1,9 +1,9 @@
{
"profiles": {
- "MemoryPlayground": {
+ "FluidTank": {
"commandName": "Project"
},
- "MemoryPlayground (WSL)": {
+ "FluidTank (WSL)": {
"commandName": "WSL2"
}
}
diff --git a/sources/Experiments/MemoryPlayground/MemoryPlayground.csproj b/sources/Experiments/MemoryPlayground/MemoryPlayground.csproj
deleted file mode 100644
index 1a30047f..00000000
--- a/sources/Experiments/MemoryPlayground/MemoryPlayground.csproj
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- Exe
- $(StandardTargetFramework)
-
-
-
-
-
-
-
diff --git a/sources/Experiments/MemoryPlayground/Program.cs b/sources/Experiments/MemoryPlayground/Program.cs
deleted file mode 100644
index 1ce2f53f..00000000
--- a/sources/Experiments/MemoryPlayground/Program.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Zenith.NET;
-
-string[] values = ["Hello, World!", "你好,世界!", "こんにちは、世界!"];
-
-using ZenithMarshal.Scope scope = new();
-
-nint pointer = ZenithMarshal.StringArrayToPointer(scope, values, StringEncoding.UTF8);
-
-foreach (string value in ZenithMarshal.StringArrayFromPointer(pointer, (uint)values.Length, StringEncoding.UTF8))
-{
- Console.WriteLine(value);
-}
\ No newline at end of file
diff --git a/sources/Experiments/PlatformDetection/Program.cs b/sources/Experiments/PlatformDetection/Program.cs
index 6cec30be..43018cd8 100644
--- a/sources/Experiments/PlatformDetection/Program.cs
+++ b/sources/Experiments/PlatformDetection/Program.cs
@@ -3,26 +3,26 @@
using Zenith.NET.Metal;
using Zenith.NET.Vulkan;
-foreach (Backend backend in Enum.GetValues())
+foreach (GraphicsApi graphicsApi in Enum.GetValues())
{
try
{
- using GraphicsContext context = backend switch
+ using GraphicsContext context = graphicsApi switch
{
- Backend.DirectX12 => GraphicsContext.CreateDirectX12(true),
- Backend.Metal => GraphicsContext.CreateMetal(true),
- Backend.Vulkan => GraphicsContext.CreateVulkan(true),
+ GraphicsApi.DirectX12 => GraphicsContext.CreateDirectX12(true),
+ GraphicsApi.Metal => GraphicsContext.CreateMetal(true),
+ GraphicsApi.Vulkan => GraphicsContext.CreateVulkan(true),
_ => throw new NotSupportedException()
};
- Console.WriteLine($"Backend {backend} is supported.");
+ Console.WriteLine($"GraphicsApi {graphicsApi} is supported.");
Console.WriteLine($" Device Name: {context.Capabilities.DeviceName}");
Console.WriteLine($" Ray Tracing Supported: {context.Capabilities.RayTracingSupported}");
Console.WriteLine($" Mesh Shading Supported: {context.Capabilities.MeshShadingSupported}");
}
catch (Exception)
{
- Console.WriteLine($"Backend {backend} is not supported.");
+ Console.WriteLine($"GraphicsApi {graphicsApi} is not supported.");
}
Console.WriteLine();
diff --git a/sources/Extensions/Zenith.NET.Extensions.ImGui/ImGuiController.cs b/sources/Extensions/Zenith.NET.Extensions.ImGui/ImGuiController.cs
index 455a570f..0a9771d1 100644
--- a/sources/Extensions/Zenith.NET.Extensions.ImGui/ImGuiController.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.ImGui/ImGuiController.cs
@@ -25,7 +25,7 @@ public unsafe class ImGuiController : DisposableObject
private bool frameBegun;
private ZenithMarshal.Scope? clipboardScope;
- public ImGuiController(GraphicsContext context, Output output, ImGuiColorSpace colorSpace, string? fontPath = null, Action? otherSetup = null)
+ public ImGuiController(GraphicsContext context, AttachmentFormats attachmentFormats, ImGuiColorSpace colorSpace, string? fontPath = null, Action? otherSetup = null)
{
HexaImGui.SetCurrentContext(Context = HexaImGui.CreateContext());
@@ -46,7 +46,7 @@ public ImGuiController(GraphicsContext context, Output output, ImGuiColorSpace c
otherSetup?.Invoke(io);
- renderer = new(context, output, colorSpace);
+ renderer = new(context, attachmentFormats, colorSpace);
platformGetClipboardText = PlatformGetClipboardText;
platformSetClipboardText = PlatformSetClipboardText;
@@ -140,7 +140,7 @@ public void Update(double delta, uint width, uint height)
frameBegun = true;
}
- public void Render(CommandBuffer commandBuffer, FrameBuffer frameBuffer, ClearValue clearValue)
+ public void Render(CommandBuffer commandBuffer, ColorAttachment colorAttachment)
{
HexaImGui.SetCurrentContext(Context);
@@ -148,7 +148,7 @@ public void Render(CommandBuffer commandBuffer, FrameBuffer frameBuffer, ClearVa
{
HexaImGui.Render();
- renderer.Render(commandBuffer, frameBuffer, clearValue, HexaImGui.GetDrawData());
+ renderer.Render(commandBuffer, colorAttachment, HexaImGui.GetDrawData());
frameBegun = false;
}
diff --git a/sources/Extensions/Zenith.NET.Extensions.ImGui/ImGuiRenderer.cs b/sources/Extensions/Zenith.NET.Extensions.ImGui/ImGuiRenderer.cs
index 99b5f875..fd3dc5ad 100644
--- a/sources/Extensions/Zenith.NET.Extensions.ImGui/ImGuiRenderer.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.ImGui/ImGuiRenderer.cs
@@ -1,4 +1,5 @@
-using System.Numerics;
+using System.Buffers;
+using System.Numerics;
using System.Runtime.InteropServices;
using Hexa.NET.ImGui;
@@ -6,212 +7,116 @@ namespace Zenith.NET.Extensions.ImGui;
internal unsafe class ImGuiRenderer : DisposableObject
{
- public const string Source = @"
+ public const string Source = """
struct VSInput
{
float2 Position : POSITION0;
-
+
float2 UV : TEXCOORD0;
-
+
float4 Color : COLOR0;
};
struct VSOutput
{
float4 Position : SV_POSITION;
-
+
float2 UV : TEXCOORD0;
-
+
float4 Color : COLOR0;
};
struct Constants
{
float4x4 Projection;
+
+ DescriptorHandle Texture;
+
+ DescriptorHandle Sampler;
};
-uniform Constants constants;
-uniform Texture2D texture;
-uniform SamplerState sampler;
+ConstantBuffer constants;
float3 SrgbToLinear(float3 srgb)
{
return srgb * (srgb * (srgb * 0.305306011 + 0.682171111) + 0.012522878);
}
+[shader("vertex")]
VSOutput VSMain(VSInput input)
{
VSOutput output;
-
+
output.Position = mul(float4(input.Position, 0.0, 1.0), constants.Projection);
output.UV = input.UV;
output.Color = input.Color;
-
+
#if 0
output.Color.rgb = SrgbToLinear(output.Color.rgb);
#endif
-
+
return output;
}
-float4 PSMain(VSOutput input) : SV_TARGET
+[shader("fragment")]
+float4 FSMain(VSOutput input) : SV_TARGET
{
- return input.Color * texture.Sample(sampler, input.UV);
+ return input.Color * (*constants.Texture).Sample(*constants.Sampler, input.UV);
}
-";
+""";
- private readonly string[] Dxil =
- [
- // Vertex Shader - Legacy
- "4458424322A36D6E02DEF08BE6B9959E5635FACE0100000038120000070000003C0000004C000000D40000006001000090020000DC090000F8090000534649300800000000000000000000004953473180000000030000000800000000000000680000000000000000000000030000000000000003030000000000000000000071000000000000000000000003000000010000000303000000000000000000007A000000000000000000000003000000020000000F0F000000000000504F534954494F4E00544558434F4F524400434F4C4F52004F5347318400000003000000080000000000000068000000000000000100000003000000000000000F00000000000000000000007400000000000000000000000300000001000000030C000000000000000000007D000000000000000000000003000000020000000F0000000000000053565F506F736974696F6E00544558434F4F524400434F4C4F5200005053563028010000340000000100000000000000000000000000000000000000FFFFFFFF010000000303000303000000000000000000000000000000280000000100000018000000020000000000000000000000000000000D000000000000003000000000504F534954494F4E00544558434F4F524400434F4C4F5200544558434F4F524400434F4C4F520056534D61696E0000010000000000000010000000010000000000000001004200030000000A000000000000000101420003000000130000000000000001024400030000000000000000000000010044030304000019000000000000000101420003020000220000000000000001024400030200000F0000000F00000000000000000000001000000020000000000000000000000000010000000200000004000000080000535441544407000066000100D10100004458494C06010000100000002C0700004243C0DE210C0000C80100000B82200002000000130000000781239141C80449061032399201840C250508191E048B628018450242920B42C41032143808184B0A32628848901420434688A500193242E4480E901123C4504151818CE183E58A0431460651180000080000001B8CE0FFFFFFFF074002A80D84F0FFFFFFFF03206D3086FFFFFFFF1F0009A800491800000300000013826042204C080600000000892000003F00000032228809206485041323A484041323E384A19014124C8C8C0B84C44C1088C1084009000A6600E608C0608E0029C64010444190510C8020886220E4A8E1F227EC21249FDBA8622526BFB86D440CC3305071CF70F913F610921F02CDB01028580AA1100C414D29066218067A6E1B2E7FC21E42F2574272A84820D2C879886842080909045108866022920E1A2E7FC21E42F25742DA906640044110C51C41500A86A0888AAC81806104624882EC2C610124C9678029422EBF581C60F2711F4781769234459430F99CD34813D04CD24FA360BB4A9A224A987C7071026049810860A49F4601778EB13801B0A4400430D24FA3A0232F1D8839025000000000131472C08774608736688779680372C0870DAE500E6DD00E7A500E6D000F7A300772A0077320076D900E71A0077320076D900E78A00778D006E9100776A0077160076D900E7320077A300772D006E9600774A0077640076D600E7160077A100776D006E6300772A0077320076D600E7640077A600774D006EE80077A100776A0077320077A60077430E4098000000000000000000060C86300011000000000000000C090070102400000000000000080214F0304C00000000000000000439E0708800000000000000000863C12100001000000000000000C792C20000200000000000000C802010016000000321E981419114C908C092647C60443224AA01846000A30A00C8AA010CAA124CAA3200AAC28A82889118022288432A0B000010111A89B01206F0680BE190002C77218E679000002030000111002C1000405E2660000000000791800009F0000001A034C90460213C43120C31B4381934BB30BA32B4B018971C1718171A989919901419931C3A991C9A919334BD910041304E2982010C8066120260844B241300C0A76731B06842026081F47E78CED4D2C8C0D2A4C2EAC6DEE0B668240281B1043590C6360800D41B3810000079820741A97B137B739BA3037BAB92F980902B16C208848323608CC3441E03C3250726F6A656374696F6E5F30130482992010CD8601C3860902E14C108867034258572619DAB64160B80982B76D208845323608CCB7E13020AAEA3C30982008C00660C3608CC1186C08C860C3308841194C10C0A0DB109C010DABA9A6B0343722504F534954494F4E1384829A2014D586C0982014D6064192362C861AAC011BB4011B0C6E60B0C11B10A12AC21A7A7A9222DAB00C71B0066CD0066C30B8C1C0066FC062E889E9496A82505C130402DA204875B061B9E6600DD8A00DD860A0838B0DEC60C300077270075CA6ACBEA0DEE6D2E8D2DEDC260805B66131F2600DF4A00DE860A003830DEC60C332C4C11AB0411BB8C1E006031BBCC186E59A83356083367083810E2E36B0830DC31EF0411F6C18F0C00F80094218641B04031436146290067FF084020D33B6B730BAB9090211B148739BA39B9B2010128DB9B4B32F36321A7369675F7374130462DA808C02299482299C0229A0422A546163B36B7349232B73A39B120455C8F05CECCAE4E6D2DEDCA6044413323C17BB3036BB32B9298151870CCF650E2D8CAC4CAEE98DAC8C6D4A809421C373912B9B7BAB931B2B9B9B123895C8F05CE8F2E0CA82DCDCDEE8C2E8D2DEDCE6A60860500675C8F05CECD2CAEE92C8A6E8C2E8CAA6046750870CCFA5CC8D4E2E0FEA2DCD8D6E6E4A100A5DC8F05CC6DEEADCE8CAE4E6A604A900791800004C0000003308801CC4E11C6614013D88433884C38C4280077978077398710CE6000FED100EF4800E330C421EC2C11DCEA11C6630053D88433884831BCC033DC8433D8C033DCC788C7470077B08077948877070077A700376788770208719CC110EEC900EE1300F6E300FE3F00EF0500E3310C41DDE211CD8211DC2611E6630893BBC833BD04339B4033CBC833C84033BCCF0147660077B680737688772680737808770908770600776280776F8057678877780875F08877118877298877998812CEEF00EEEE00EF5C00EEC300362C8A11CE4A11CCCA11CE4A11CDC611CCA211CC4811DCA6106D6904339C84339984339C84339B8C33894433888033B94C32FBC833CFC823BD4033BB0C38CC821077C70037210877370037B080779608770C88777A8077A98813CE4800F6E400FE5D00EF0000000712000001F0000000660BCAC09208D1550C3E53B8F0F348D3301131102CDB01036B00D97EF3CBE1050454144A5030C25610002E617B76D06DD70F9CEE30B11014C440834C3427C91C36C4833208D6101D370F9CEE32F0E3088CD434D7E71DB26500D97EF3CBE3439118152D3434D7E71DB46200D97EF3CFE4444130244985FDC360000000000000048415348140000000000000082D156255E85851D3995C0A0E0DA81FE4458494C38080000660001000E0200004458494C0601000010000000200800004243C0DE210C0000050200000B82200002000000130000000781239141C80449061032399201840C250508191E048B628018450242920B42C41032143808184B0A32628848901420434688A500193242E4480E901123C4504151818CE183E58A0431460651180000080000001B8CE0FFFFFFFF074002A80D84F0FFFFFFFF03206D3086FFFFFFFF1F0009A800491800000300000013826042204C080600000000892000003F00000032228809206485041323A484041323E384A19014124C8C8C0B84C44C1088C1084009000A6600E608C0608E0029C64010444190510C8020886220E4A8E1F227EC21249FDBA8622526BFB86D440CC3305071CF70F913F610921F02CDB01028580AA1100C414D29066218067A6E1B2E7FC21E42F2574272A84820D2C879886842080909045108866022920E1A2E7FC21E42F25742DA906640044110C51C41500A86A0888AAC81806104624882EC2C610124C9678029422EBF581C60F2711F4781769234459430F99CD34813D04CD24FA360BB4A9A224A987C7071026049810860A49F4601778EB13801B0A4400430D24FA3A0232F1D8839025000000000131472C08774608736688779680372C0870DAE500E6DD00E7A500E6D000F7A300772A0077320076D900E71A0077320076D900E78A00778D006E9100776A0077160076D900E7320077A300772D006E9600774A0077640076D600E7160077A100776D006E6300772A0077320076D600E7640077A600774D006EE80077A100776A0077320077A60077430E4090000000000000000000060C86300011000000000000000C090070102400000000000000080214F0304C00000000000000000439E0708800000000000000000863C12100001000000000000000C792C20000200000000000000C802010011000000321E981419114C908C092647C60443224AA0188A6204A00003CAA008CA838A92180128824228030A0B10101081C0B11C86791E0080C000004004844030004181BE19000079180000650000001A034C90460213C43120C31B4381934BB30BA32B4B018971C1718171A989919901419931C3A991C9A919334BD910041304E2982010C8066120260844B241180C0A76731B0684202608843241F82C02130462D980280BA32843036C089C0D04003CC004010CAA0D41344110001A56534D61696E44A09EA692A8929E9C2608C5334128A00D81324128A20902C16C10346DC3A254D6855D43A65C1B11AA22ACA1A72729A20DCBD05917760DD9706D1304A26131F4C4F424354128A40902E16C103431D8B07C60605DD83584C1778DC18681F3C880CB94D517D4DB5C1A5DDA9BDB04A198362C8A1958678085C11006CA35061B96A1B32E2C1BB2E1DA362C1F185817960D61F05D63B06140833450830D4319AC01304108036A83A0B4C18662A2D80072832A6C6C766D2E6964656E745382A00A199E8B5D99DC5CDA9BDB9480684286E76217C6665726372530EA90E1B9CCA1859195C935BD9195B14D09903264782E7265736F75726365735382A70E199E8B5D5AD95D12D9145D185DD99420AA4386E752E646279707F596E646373725700300000000791800004C0000003308801CC4E11C6614013D88433884C38C4280077978077398710CE6000FED100EF4800E330C421EC2C11DCEA11C6630053D88433884831BCC033DC8433D8C033DCC788C7470077B08077948877070077A700376788770208719CC110EEC900EE1300F6E300FE3F00EF0500E3310C41DDE211CD8211DC2611E6630893BBC833BD04339B4033CBC833C84033BCCF0147660077B680737688772680737808770908770600776280776F8057678877780875F08877118877298877998812CEEF00EEEE00EF5C00EEC300362C8A11CE4A11CCCA11CE4A11CDC611CCA211CC4811DCA6106D6904339C84339984339C84339B8C33894433888033B94C32FBC833CFC823BD4033BB0C38CC821077C70037210877370037B080779608770C88777A8077A98813CE4800F6E400FE5D00EF0000000712000001F0000000660BCAC09208D1550C3E53B8F0F348D3301131102CDB01036B00D97EF3CBE1050454144A5030C25610002E617B76D06DD70F9CEE30B11014C440834C3427C91C36C4833208D6101D370F9CEE32F0E3088CD434D7E71DB26500D97EF3CBE3439118152D3434D7E71DB46200D97EF3CFE4444130244985FDC36000000612000007A0000001304412C100000000900000044944221CC001457D995EC40C10E9406152540D508005173108AA24414CD21780B000000230608008260607587900D230607008260207D47208C18240008828141060DB655CC8841028020181865E064DCD18C18240008828161068FD659CE8841028020181867006D5EF58C182400088281810691187C1934629000200806461A486300064B34629000200806861A4C6110069B34629000200806C61A50622006CD346270002008068D1A48C9188C2604C06882108C260CC26802318C181C00088241F30697B306A30901309A2004A30983309A400C230607008260D0D00137ADC16842008C2608C168C2208C26108339917C460C100004C1E0C983318894C08C003A0651F219314000100483870FCC8062020B10E89874C967C4000140100C9E3F4883CB092C50A06394269F1103040041307844810D3428B08081CE88410280201820A610077EE00778A08C182400088201620A71E0077E9006C788410280201820A610077EE00777408C182400088201620A71E0077E5007C188410280201820A61007A1E00778F08D182400088201620A71100A7E9006DE88410280201820A610077BE0077830062306090082608098421CEC811FA481188C182400088201620A71B0077E700761306290002008068829C4C11EF8411D80010200000000",
-
- // Vertex Shader - Linear
- "44584243D753A620DDB4C72961AF9BB49563BC6A0100000094120000070000003C0000004C000000D40000006001000090020000E0090000FC090000534649300800000000000000000000004953473180000000030000000800000000000000680000000000000000000000030000000000000003030000000000000000000071000000000000000000000003000000010000000303000000000000000000007A000000000000000000000003000000020000000F0F000000000000504F534954494F4E00544558434F4F524400434F4C4F52004F5347318400000003000000080000000000000068000000000000000100000003000000000000000F00000000000000000000007400000000000000000000000300000001000000030C000000000000000000007D000000000000000000000003000000020000000F0000000000000053565F506F736974696F6E00544558434F4F524400434F4C4F5200005053563028010000340000000100000000000000000000000000000000000000FFFFFFFF010000000303000303000000000000000000000000000000280000000100000018000000020000000000000000000000000000000D000000000000003000000000504F534954494F4E00544558434F4F524400434F4C4F5200544558434F4F524400434F4C4F520056534D61696E0000010000000000000010000000010000000000000001004200030000000A000000000000000101420003000000130000000000000001024400030000000000000000000000010044030304000019000000000000000101420003020000220000000000000001024400030200000F0000000F00000000000000000000001000000020000000000000000000000000010000000200000004000000080000535441544807000066000100D20100004458494C0601000010000000300700004243C0DE210C0000C90100000B82200002000000130000000781239141C80449061032399201840C250508191E048B628018450242920B42C41032143808184B0A32628848901420434688A500193242E4480E901123C4504151818CE183E58A0431460651180000080000001B8CE0FFFFFFFF074002A80D84F0FFFFFFFF03206D3086FFFFFFFF1F0009A800491800000300000013826042204C080600000000892000003F00000032228809206485041323A484041323E384A19014124C8C8C0B84C44C1088C1084009000A6600E608C0608E0029C64010444190510C8020886220E4A8E1F227EC21249FDBA8622526BFB86D440CC3305071CF70F913F610921F02CDB01028580AA1100C414D29066218067A6E1B2E7FC21E42F2574272A84820D2C879886842080909045108866022920E1A2E7FC21E42F25742DA906640044110C51C41500A86A0888AAC81806104624882EC2C610124C9678029422EBF581C60F2711F4781769234459430F99CD34813D04CD24FA360BB4A9A224A987C7071026049810860A49F4601778EB13801B0A4400430D24FA3A0232F1D8839025000000000131472C08774608736688779680372C0870DAE500E6DD00E7A500E6D000F7A300772A0077320076D900E71A0077320076D900E78A00778D006E9100776A0077160076D900E7320077A300772D006E9600774A0077640076D600E7160077A100776D006E6300772A0077320076D600E7640077A600774D006EE80077A100776A0077320077A60077430E4098000000000000000000060C86300011000000000000000C090070102400000000000000080214F0304C00000000000000000439E0708800000000000000000863C12100001000000000000000C792C20000200000000000000C802010016000000321E981819114C908C092647C60443224AA01846000A30A00C8AA010CAA124CAA35CCAAF208A828A92180128824228030A0B10101081BA1900F26600E89B0120702C87619E070020300000100121100C4050206E0600000079180000A00000001A034C90460213C43120C31B4381934BB30BA32B4B018971C1718171A989919901419931C3A991C9A919334BD910041304E2982010C8066120260844B241300C0A76731B06842026085F47E78CED4D2C8C0D2A4C2EAC6DEE0B668240281B1043590C6360800D41B3810000079820741B97B137B739BA3037BAB92F980902B16C208848323608CC3441E03E3250726F6A656374696F6E5F30130482992010CD8601C3860902E14C108867034258572619DAB64160B80982C76D208845323608CCB7E13020AAEA3C30982008C00660C3608CC1186C08C860C3308841194C10C0C0DB109C010DABA9A6B0343722504F534954494F4E1384A29A2014D686C0982014D7064192362C861AAC011BB4011B0C6E60B0C11B10A12AC21A7A7A9222DAB00C71B0066CD0066C30B8C1C0066FC062E889E9496A825060130402DA204875B061B9E6600DD8A00DD860A0838B0DEC60C300077270075CA6ACBEA0DEE6D2E8D2DEDC260845B66131F2600DF4A00DE860A003830DEC60C332C4C11AB0411BB8C1E006031BBCC186E59A83356083367083810E2E36B0830DC31EF0411F6C18F0C00F80094218681B04031436146290067FF084020D33B6B730BAB9090211B148739BA39B9B2010128DB9B4B32F36B2090231D1984B3BFB9AA39B2010D406641448A1144CE11450211554A10A1B9B5D9B4B1A59991BDD9420A84286E76257263797F6E63625209A90E1B9D885B1D995C94D098C3A64782E7368616465724D6F64656C5302A40C199E8B5CD9DC5B9DDC58D9DC94C0A94486E74297075716E4E6F646174697F6E6363745008332A84386E76297567697443645174657362538833A64782E656E747279506F696E7473538250E84286E732F656E7465726373725500500791800004C0000003308801CC4E11C6614013D88433884C38C4280077978077398710CE6000FED100EF4800E330C421EC2C11DCEA11C6630053D88433884831BCC033DC8433D8C033DCC788C7470077B08077948877070077A700376788770208719CC110EEC900EE1300F6E300FE3F00EF0500E3310C41DDE211CD8211DC2611E6630893BBC833BD04339B4033CBC833C84033BCCF0147660077B680737688772680737808770908770600776280776F8057678877780875F08877118877298877998812CEEF00EEEE00EF5C00EEC300362C8A11CE4A11CCCA11CE4A11CDC611CCA211CC4811DCA6106D6904339C84339984339C84339B8C33894433888033B94C32FBC833CFC823BD4033BB0C30CC421077C70037A288776808719D1430EF8E006E4200EE7E006F6100EF2C00EE1900FEF500FF4000000712000001F0000000660BCAC09208D1550C3E53B8F0F348D3301131102CDB01036B00D97EF3CBE1050454144A5030C25610002E617B76D06DD70F9CEE30B11014C440834C3427C91C36C4833208D6101D370F9CEE32F0E3088CD434D7E71DB26500D97EF3CBE3439118152D3434D7E71DB46200D97EF3CFE4444130244985FDC3600000000000000484153481400000000000000F9DD1AE5250EB9E439EBECBF95D9943F4458494C9008000066000100240200004458494C0601000010000000780800004243C0DE210C00001B0200000B82200002000000130000000781239141C80449061032399201840C250508191E048B628018450242920B42C41032143808184B0A32628848901420434688A500193242E4480E901123C4504151818CE183E58A0431460651180000080000001B8CE0FFFFFFFF074002A80D84F0FFFFFFFF03206D3086FFFFFFFF1F0009A800491800000300000013826042204C080600000000892000003F00000032228809206485041323A484041323E384A19014124C8C8C0B84C44C1088C1084009000A6600E608C0608E0029C64010444190510C8020886220E4A8E1F227EC21249FDBA8622526BFB86D440CC3305071CF70F913F610921F02CDB01028580AA1100C414D29066218067A6E1B2E7FC21E42F2574272A84820D2C879886842080909045108866022920E1A2E7FC21E42F25742DA906640044110C51C41500A86A0888AAC81806104624882EC2C610124C9678029422EBF581C60F2711F4781769234459430F99CD34813D04CD24FA360BB4A9A224A987C7071026049810860A49F4601778EB13801B0A4400430D24FA3A0232F1D8839025000000000131472C08774608736688779680372C0870DAE500E6DD00E7A500E6D000F7A300772A0077320076D900E71A0077320076D900E78A00778D006E9100776A0077160076D900E7320077A300772D006E9600774A0077640076D600E7160077A100776D006E6300772A0077320076D600E7640077A600774D006EE80077A100776A0077320077A60077430E4090000000000000000000060C86300011000000000000000C090070102400000000000000080214F0304C00000000000000000439E0708800000000000000000863C12100001000000000000000C792C20000200000000000000C802010011000000321E981419114C908C092647C60443224AA0188A6204A00003CAA008CA838A92180128824228030A0B10101081C0B11C86791E0080C000004004844030004181BE19000079180000650000001A034C90460213C43120C31B4381934BB30BA32B4B018971C1718171A989919901419931C3A991C9A919334BD910041304E2982010C8066120260844B241180C0A76731B0684202608843241F82C02130462D980280BA32843036C089C0D04003CC004010CAA0D41344110001A56534D61696E44A09EA692A8929E9C2608C5334128A00D81324128A20902C16C10346DC3A254D6855D43A65C1B11AA22ACA1A72729A20DCBD05917760DD9706D1304A26131F4C4F424354128A40902E16C103431D8B07C60605DD83584C1778DC18681F3C880CB94D517D4DB5C1A5DDA9BDB04A198362C8A1958678085C11006CA35061B96A1B32E2C1BB2E1DA362C1F185817960D61F05D63B06140833450830D4319AC01304108036A83A0B4C18662A2D80072832A6C6C766D2E6964656E745382A00A199E8B5D99DC5CDA9BDB9480684286E76217C6665726372530EA90E1B9CCA1859195C935BD9195B14D09903264782E7265736F75726365735382A70E199E8B5D5AD95D12D9145D185DD99420AA4386E752E646279707F596E646373725700300000000791800004C0000003308801CC4E11C6614013D88433884C38C4280077978077398710CE6000FED100EF4800E330C421EC2C11DCEA11C6630053D88433884831BCC033DC8433D8C033DCC788C7470077B08077948877070077A700376788770208719CC110EEC900EE1300F6E300FE3F00EF0500E3310C41DDE211CD8211DC2611E6630893BBC833BD04339B4033CBC833C84033BCCF0147660077B680737688772680737808770908770600776280776F8057678877780875F08877118877298877998812CEEF00EEEE00EF5C00EEC300362C8A11CE4A11CCCA11CE4A11CDC611CCA211CC4811DCA6106D6904339C84339984339C84339B8C33894433888033B94C32FBC833CFC823BD4033BB0C30CC421077C70037A288776808719D1430EF8E006E4200EE7E006F6100EF2C00EE1900FEF500FF4000000712000001F0000000660BCAC09208D1550C3E53B8F0F348D3301131102CDB01036B00D97EF3CBE1050454144A5030C25610002E617B76D06DD70F9CEE30B11014C440834C3427C91C36C4833208D6101D370F9CEE32F0E3088CD434D7E71DB26500D97EF3CBE3439118152D3434D7E71DB46200D97EF3CFE4444130244985FDC3600000061200000900000001304412C100000000E000000548D005051024494462914C20C4071955DC90E14EC000D6304A2B9EAA4374640DA682F7F14CD21640B5173108AA244348C11C8288DA71F002306080082606081C12170CF88C10180201848627004C48841028020181867D06C1EC68C18240008828181060EF745CD8841028020181869F07460903923060900826060A801E48501F68C182400088281B10651198801078D182400088281C10692198C41158D182400088281D10613199081278D182400088281E106541994C1358D181C00088241D306526206A30901309A2004A30983309A400C230607008260D0C8C1E5B8C16842008C2608C168C2208C2610C388C1018020183477C04D6E309A1000A30942309A3008A309C4604E249F11030400413078F8600C22253023808E41947C460C100004C1E0F90333A098C002043A265DF2193140001004834714D2E072020B14E818A5C967C4000140100C9E5260030D0A2C60A063636006F2B1313803F9D818A0817C6C6003F8D8D006F0B1C10DE063431AC8C78634908F0D69201F1BE6003E36D0017C6CA803F8D8F006F2B1E10DE463C31BC867C4200140100C105C000558800555C8460C120004C100C1055080055810056BC4200140100C105C00055880855498460C120004C100C10550800558380568C4200140100C105C00855980055570831183040041304070011466011644A10D460C120004C100C10550680558508561C4200140100C105C00855680055110460C120004C100C10550680558488560C4200140100C105C0085568085537803040000000000",
-
- // Pixel Shader
- "44584243EE63AC19F937568890164F4A4BFF155601000000D4100000070000003C0000004C000000D800000014010000240200001C0900003809000053464930080000000000000000000000495347318400000003000000080000000000000068000000000000000100000003000000000000000F000000000000000000000074000000000000000000000003000000010000000303000000000000000000007D000000000000000000000003000000020000000F0F00000000000053565F506F736974696F6E00544558434F4F524400434F4C4F5200004F5347313400000001000000080000000000000028000000000000004000000003000000000000000F0000000000000053565F5461726765740000005053563008010000340000000000000000000000000000000000000000000000FFFFFFFF000000000301000301000000000000000000000000000000100000000200000018000000010000000000000000000000000000000E000000000000000300000000000000000000000000000002000000000000001800000000544558434F4F524400434F4C4F520050534D61696E000001000000000000001000000000000000000000000100440303040000010000000000000001014200030200000A00000000000000010244000302000000000000000000000100441003000000000000000000000000000000000000000F0000000F00000000000000000000000100000002000000040000000800000053544154F006000066000000BC0100004458494C0601000010000000D80600004243C0DE210C0000B30100000B82200002000000130000000781239141C80449061032399201840C250508191E048B628014450242920B42A41032143808184B0A32528848901420434688A500193242E4480E909122C4504151818CE183E58A0429460651180000080000001B8CE0FFFFFFFF074002A80D84F0FFFFFFFF03206D3086FFFFFFFF1F0009A800491800000300000013826042204C080600000000892000005000000032224809206485049322A484049322E384A19014124C8A8C0B84A44C107C23002500146600E608C0608E0029C620841442A61880105206A19B86CB9FB08790FC95905662F28BDB46C51863102AF70C973F610F21F921D00C0B8182551845181B630C42C8A076DB70F913F61092BF12924345029146CE43441342484820A4108CB047F0A0E1F227EC21247F25A40D6906441042CA1C41500A46249944070286118861A636180776088779980737A08572C0077AA8077928073920053EB0877218077A780779E0037360877708077A600330A0033F00033FD0033D688774808779F8057AC807782807149099C4601CD8211CE6611EDC8016CA011FE8A11EE4A11CE48014F8C01ECA611CE8E11DE4810FCC811DDE211CE8810DC0800EFC000CFC00091753BE499A224A987C16609E8588D80998081410DAE940E608400100000000131472C08774608736688779680372C0870DAE500E6DD00E7A500E6D000F7A300772A0077320076D900E71A0077320076D900E78A00778D006E9100776A0077160076D900E7320077A300772D006E9600774A0077640076D600E7160077A100776D006E6300772A0077320076D600E7640077A600774D006EE80077A100776A0077320077A60077430E4098000000000000000000060C86300011000000000000000C090070102400000000000000080214F0304C00000000000000000431E0808800000000000000000863C1410000100000000000000648100000011000000321E981419114C908C092647C604432225500C230045501265501E855030544AA20C0A6104A0080A847C01020222109F01A03E96C310000000CF0300814020A46700000079180000870000001A034C90460213C43120C31B4381934BB30BA32B4B018971C1718171A989919901419931C3A991C9A919334BD910041304C2982010C7066120260804B241300C0A70731B06842026081AC684AE0C8FAE4EAEEC0B66824024130442D92018CD86C45016C31818C3D9103C1304CE623217D606C75626F705B3013122C9300603D8104C1B080800A80982006C00360CC6756D08B00DC360651384EEDA106C34A0A69AC2D2DCB84C597D41BDCDA5D1A5BDB94D108A678250401B0263825044138442DAB018DE0706612006831818630010A12AC21A7A7A92229A2014D304815836086770061B96A10CBE3108033318CC601803346031F4C4F424354120980DC219ACC1868551836F0CC2C00C063160C6800D360C6490066DC064CAEA8B2A4CEEAC8C6E8250501B16E30D3E3808833118C4C0180336D810C4C186C10DE400982078D506C1A0830D85D5CD41550734CCD8DEC2E8E6260844C322CD6D8E6E6E82403834E6D2CEBED8C868CCA59D7DCDD111A12BC3FB727B936BDBA0DC011EE4811EEC01C10778D00743153636BB369734B23237BA294150850CCFC5AE4C6E2EEDCD6D4A403421C373B10B63B32B939B121875C8F05CE6D0C2C8CAE49ADEC8CAD8A6044819323C17B9B2B9B73AB9B1B2B9290155890CCF852E0FAE2CC8CDED8D2E8C2EEDCD6D6E4A90D521C373B14B2BBB4B229BA20BA32B9B126C75C8F05CCADCE8E4F2A0DED2DCE8E6A60475D0850CCF65ECADCE8DAE4C6E6E4AD007000000791800004C0000003308801CC4E11C6614013D88433884C38C4280077978077398710CE6000FED100EF4800E330C421EC2C11DCEA11C6630053D88433884831BCC033DC8433D8C033DCC788C7470077B08077948877070077A700376788770208719CC110EEC900EE1300F6E300FE3F00EF0500E3310C41DDE211CD8211DC2611E6630893BBC833BD04339B4033CBC833C84033BCCF0147660077B680737688772680737808770908770600776280776F8057678877780875F08877118877298877998812CEEF00EEEE00EF5C00EEC300362C8A11CE4A11CCCA11CE4A11CDC611CCA211CC4811DCA6106D6904339C84339984339C84339B8C33894433888033B94C32FBC833CFC823BD4033BB0C30CC421077C70037A288776808719D1430EF8E006E4200EE7E006F6100EF2C00EE1900FEF500FF400000071200000190000000660A4AC09208D1150C3E53B8F0F348D3301131102CDB01056D00D97EF3CBE1011C0448440332CC41739CC863403D21816300D97EF3CFEE20083D83CD4E417B76D03D070F9CEE34B00F32C845FDCB60954C3E53B8F2F4D4E44A0D4F450935FDC36000000000000004841534814000000000000009F974238F30DDDF016CFF4F093C3E59C4458494C9407000066000000E50100004458494C06010000100000007C0700004243C0DE210C0000DC0100000B82200002000000130000000781239141C80449061032399201840C250508191E048B628014450242920B42A41032143808184B0A32528848901420434688A500193242E4480E909122C4504151818CE183E58A0429460651180000080000001B8CE0FFFFFFFF074002A80D84F0FFFFFFFF03206D3086FFFFFFFF1F0009A800491800000300000013826042204C080600000000892000005000000032224809206485049322A484049322E384A19014124C8A8C0B84A44C107C23002500146600E608C0608E0029C620841442A61880105206A19B86CB9FB08790FC95905662F28BDB46C51863102AF70C973F610F21F921D00C0B8182551845181B630C42C8A076DB70F913F61092BF12924345029146CE43441342484820A4108CB047F0A0E1F227EC21247F25A40D6906441042CA1C41500A46249944070286118861A636180776088779980737A08572C0077AA8077928073920053EB0877218077A780779E0037360877708077A600330A0033F00033FD0033D688774808779F8057AC807782807149099C4601CD8211CE6611EDC8016CA011FE8A11EE4A11CE48014F8C01ECA611CE8E11DE4810FCC811DDE211CE8810DC0800EFC000CFC00091753BE499A224A987C16609E8588D80998081410DAE940E608400100000000131472C08774608736688779680372C0870DAE500E6DD00E7A500E6D000F7A300772A0077320076D900E71A0077320076D900E78A00778D006E9100776A0077160076D900E7320077A300772D006E9600774A0077640076D600E7160077A100776D006E6300772A0077320076D600E7640077A600774D006EE80077A100776A0077320077A60077430E4090000000000000000000060C86300011000000000000000C090070102400000000000000080214F0304C00000000000000000431E0808800000000000000000863C1410000100000000000000648100000011000000321E981419114C908C092647C604432225500C4531025004255106E541A524CAA01046008AA040C81720202002F11900EA63390C010000F03C00100804427A060000000079180000650000001A034C90460213C43120C31B4381934BB30BA32B4B018971C1718171A989919901419931C3A991C9A919334BD910041304C2982010C7066120260804B241180C0A70731B0684202608443241D02E02130442992010CB0641713624CAC228CAD028CF86009A2070D5064491184519146043306D202200A026089DB521B026080240036AAA292CCD8DCB94D517D4DB5C1A5DDA9BDB04A1702608C5B32150260805344128A20D8BA26D5CE70D9EF20144A88AB0869E9EA488260885344120980DC2188CC186650883EDEBC4601083E1230316434F4C4F521304A2D9208CC1196C581A33D8BE4E0C06AFF9D060C300066590064CA6ACBEA8C2E4CECAE82608C5B46151D6606383EE1B3CE543830D411B6C18D4C00D800982476D101438D85060D91B547150858DCDAECD258DACCC8D6E4A105421C373B12B939B4B7B739B12104DC8F05CECC2D8ECCAE4A604461D323C9739B430B232B9A637B232B6290152860CCF45AE6CEEAD4E6EAC6C6E4A40D521C373B14B2BBB4B229BA20BA32B9B125875C8F05CCADCE8E4F2A0DED2DCE8E6A6047100000000791800004C0000003308801CC4E11C6614013D88433884C38C4280077978077398710CE6000FED100EF4800E330C421EC2C11DCEA11C6630053D88433884831BCC033DC8433D8C033DCC788C7470077B08077948877070077A700376788770208719CC110EEC900EE1300F6E300FE3F00EF0500E3310C41DDE211CD8211DC2611E6630893BBC833BD04339B4033CBC833C84033BCCF0147660077B680737688772680737808770908770600776280776F8057678877780875F08877118877298877998812CEEF00EEEE00EF5C00EEC300362C8A11CE4A11CCCA11CE4A11CDC611CCA211CC4811DCA6106D6904339C84339984339C84339B8C33894433888033B94C32FBC833CFC823BD4033BB0C30CC421077C70037A288776808719D1430EF8E006E4200EE7E006F6100EF2C00EE1900FEF500FF400000071200000190000000660A4AC09208D1150C3E53B8F0F348D3301131102CDB01056D00D97EF3CBE1011C0448440332CC41739CC863403D21816300D97EF3CFEE20083D83CD4E417B76D03D070F9CEE34B00F32C845FDCB60954C3E53B8F2F4D4E44A0D4F450935FDC3600000061200000490000001304412C100000000B00000034470088CC0014422994EC40C10E94244471141E9512A03787A07873085F427204600E22499287C60C0000002306080082604089C13278CF8801028020185063C00C1F34629000200806061A3C1F1860D088410280201818690081411824D1884102802018186A1085811864D2884102802018186B2089C11874D3884102802018186C309D011970D4884102802018186D40A1411934D588C101802018406B2021CD88C101802018406C3021CD88C103802018346E100901312C0B1AA001B68C2604C06882108C260CC268023118D1C8C788463E4634F231A291CF8841028020182078F0C5411CA40131629000200806081E7C711007D930629000200806081E7C71100768208C18240008820182075F1CC4C11A0408000000000000"
- ];
-
- private readonly ResourceLayoutDesc DxilDesc = new()
- {
- Bindings =
- [
- new() { Type = ResourceType.ConstantBuffer, Index = 0, Count = 1, StageFlags = ShaderStageFlags.Vertex },
- new() { Type = ResourceType.Texture, Index = 0, Count = 1, StageFlags = ShaderStageFlags.Pixel },
- new() { Type = ResourceType.Sampler, Index = 0, Count = 1, StageFlags = ShaderStageFlags.Pixel }
- ]
- };
-
- private readonly string[] Spirv =
- [
- // Vertex Shader - Legacy
- "030223070006010000002800490000000000000011000200010000000E00030000000000010000000F000C00000000000200000056534D61696E0000360000003A0000003D000000150000002B000000310000001C000000030003000B00000001000000050009000B000000436F6E7374616E74735F7374643134305F6C6F676963616C00000000060006000B0000000000000050726F6A656374696F6E00000500060015000000696E7075742E506F736974696F6E0000050007001A000000436F6E7374616E74735F73746431343000000000060006001A0000000000000050726F6A656374696F6E00000500070019000000476C6F62616C506172616D735F73746431343000060006001900000000000000636F6E7374616E7473000000050006001C000000676C6F62616C506172616D7300000000050005002B000000696E7075742E5556000000000500050031000000696E7075742E436F6C6F7200050009003A000000656E747279506F696E74506172616D5F56534D61696E2E555600000005000A003D000000656E747279506F696E74506172616D5F56534D61696E2E436F6C6F7200000000050004000200000056534D61696E000047000400150000001E00000000000000480005001A000000000000002300000000000000480004001A0000000000000005000000480005001A0000000000000007000000100000004700030019000000020000004800050019000000000000002300000000000000470004001C0000002100000000000000470004001C0000002200000000000000470004002B0000001E0000000100000047000400310000001E0000000200000047000400360000000B00000000000000470004003A0000001E00000000000000470004003D0000001E0000000100000013000200010000002100030003000000010000001600030006000000200000001700040007000000060000000400000017000400080000000600000002000000180004000C00000007000000040000001E0003000B0000000C000000150004000F00000020000000010000002B0004000F0000001000000000000000200004001400000001000000080000002B0004000600000017000000000000002B00040006000000180000000000803F1E0003001A0000000C0000001E000300190000001A000000200004001B0000000200000019000000200004001D000000020000001A0000002000040030000000010000000700000020000400350000000300000007000000200004003900000003000000080000003B0004001400000015000000010000003B0004001B0000001C000000020000003B000400140000002B000000010000003B0004003000000031000000010000003B0004003500000036000000030000003B000400390000003A000000030000003B000400350000003D000000030000003600050001000000020000000000000003000000F8000200040000003D000400080000001300000015000000500006000700000016000000130000001700000018000000410005001D0000001E0000001C000000100000003D0004001A0000001F0000001E000000900104000B000000200000001F000000510005000C00000048000000200000000000000091000500070000002500000048000000160000003D000400080000002A0000002B0000003D000400070000002F000000310000003E00030036000000250000003E0003003A0000002A0000003E0003003D0000002F000000FD00010038000100",
-
- // Vertex Shader - Linear
- "030223070006010000002800750000000000000011000200010000000E00030000000000010000000F000C00000000000200000056534D61696E0000500000005400000057000000150000002B000000310000001C000000030003000B00000001000000050009000B000000436F6E7374616E74735F7374643134305F6C6F676963616C00000000060006000B0000000000000050726F6A656374696F6E00000500060015000000696E7075742E506F736974696F6E0000050007001A000000436F6E7374616E74735F73746431343000000000060006001A0000000000000050726F6A656374696F6E00000500070019000000476C6F62616C506172616D735F73746431343000060006001900000000000000636F6E7374616E7473000000050006001C000000676C6F62616C506172616D7300000000050005002B000000696E7075742E5556000000000500050031000000696E7075742E436F6C6F72000500090054000000656E747279506F696E74506172616D5F56534D61696E2E555600000005000A0057000000656E747279506F696E74506172616D5F56534D61696E2E436F6C6F7200000000050004000200000056534D61696E000047000400150000001E00000000000000480005001A000000000000002300000000000000480004001A0000000000000005000000480005001A0000000000000007000000100000004700030019000000020000004800050019000000000000002300000000000000470004001C0000002100000000000000470004001C0000002200000000000000470004002B0000001E0000000100000047000400310000001E0000000200000047000400500000000B0000000000000047000400540000001E0000000000000047000400570000001E0000000100000013000200010000002100030003000000010000001600030006000000200000001700040007000000060000000400000017000400080000000600000002000000180004000C00000007000000040000001E0003000B0000000C000000150004000F00000020000000010000002B0004000F0000001000000000000000200004001400000001000000080000002B0004000600000017000000000000002B00040006000000180000000000803F1E0003001A0000000C0000001E000300190000001A000000200004001B0000000200000019000000200004001D000000020000001A00000020000400300000000100000007000000170004003300000006000000030000002B000400060000003B00000012519C3E2B000400060000003D000000C4A22E3F2B0004000600000041000000C22C4D3C200004004F0000000300000007000000200004005300000003000000080000003B0004001400000015000000010000003B0004001B0000001C000000020000003B000400140000002B000000010000003B0004003000000031000000010000003B0004004F00000050000000030000003B0004005300000054000000030000003B0004004F00000057000000030000002C000600330000006D0000003D0000003D0000003D0000002C000600330000006E0000004100000041000000410000003600050001000000020000000000000003000000F8000200040000003D000400080000001300000015000000500006000700000016000000130000001700000018000000410005001D0000001E0000001C000000100000003D0004001A0000001F0000001E000000900104000B000000200000001F000000510005000C0000006C00000020000000000000009100050007000000250000006C000000160000003D000400080000002A0000002B0000003D000400070000002F000000310000004F00080033000000340000002F0000002F0000000000000001000000020000008E000500330000005D000000340000003B00000081000500330000005F0000005D0000006D000000850005003300000060000000340000005F000000810005003300000062000000600000006E00000085000500330000006300000034000000620000005100050006000000470000006300000000000000520006000700000070000000470000002F00000000000000510005000600000049000000630000000100000052000600070000007200000049000000700000000100000051000500060000004B00000063000000020000005200060007000000740000004B00000072000000020000003E00030050000000250000003E000300540000002A0000003E0003005700000074000000FD00010038000100",
-
- // Pixel Shader
- "0302230700060100000028001E0000000000000011000200010000000E00030000000000010000000F000A00040000000200000050534D61696E000011000000150000001D0000000D00000009000000100003000200000007000000030003000B000000010000000500050009000000696E7075742E436F6C6F7200050005000D000000696E7075742E55560000000005000400110000007465787475726500050004001500000073616D706C657200050006001800000073616D706C6564496D61676500000000050004001900000073616D706C656400050008001D000000656E747279506F696E74506172616D5F50534D61696E0000050004000200000050534D61696E000047000400090000001E00000001000000470004000D0000001E0000000000000047000400110000002100000001000000470004001100000022000000000000004700040015000000210000000200000047000400150000002200000000000000470004001D0000001E0000000000000013000200010000002100030003000000010000001600030005000000200000001700040006000000050000000400000020000400080000000100000006000000170004000A0000000500000002000000200004000C000000010000000A000000190009000E000000050000000100000002000000000000000000000001000000000000002000040010000000000000000E0000001A00020012000000200004001400000000000000120000001B000300170000000E000000200004001C00000003000000060000003B0004000800000009000000010000003B0004000C0000000D000000010000003B0004001000000011000000000000003B0004001400000015000000000000003B0004001C0000001D000000030000003600050001000000020000000000000003000000F8000200040000003D0004000600000007000000090000003D0004000A0000000B0000000D0000003D0004000E0000000F000000110000003D0004001200000013000000150000005600050017000000180000000F00000013000000570006000600000019000000180000000B0000000000000085000500060000001B00000007000000190000003E0003001D0000001B000000FD00010038000100"
- ];
-
- private readonly ResourceLayoutDesc SpirvDesc = new()
- {
- Bindings =
- [
- new() { Type = ResourceType.ConstantBuffer, Index = 0, Count = 1, StageFlags = ShaderStageFlags.Vertex },
- new() { Type = ResourceType.Texture, Index = 1, Count = 1, StageFlags = ShaderStageFlags.Pixel },
- new() { Type = ResourceType.Sampler, Index = 2, Count = 1, StageFlags = ShaderStageFlags.Pixel }
- ]
- };
-
- private readonly string[] Metallib =
- [
- // Vertex Shader - Legacy
- "4D544C4201800200070000810E000000181300000000000058000000000000008A00000000000000160100000000000039000000000000004F0100000000000008000000000000005701000000000000300E000000000000010000008A0000004E414D45070056534D61696E00545950450100004841534820000EAC3941BE1430927847F5EEDB1D3E2EC5534205A80228D8C9BA1EB0FC38937B4F464654180000000000000000000000000000000000000000000000000056455253080002000600030001004D44535A0800300E00000000000052464C5408000400000000000000454E4454524C53541000870F000000000000910300000000000055554944100064EF3F7D1C4B30FF8EC74A1716027506454E4454390000005641545420000300506F736974696F6E5F3100008055565F31000180436F6C6F725F310002805641545905000300040406454E445408000000454E4454DEC0170B0000000014000000140E0000FFFFFFFF4243C0DE3514000003000000620C3024801005C814000000210C00004C0300000B02210002000000160000000781239141C80449061032399201840C250508191E048B628014450242920B42A41032143808184B0A3252884870C421234412878C1041920264C808B1142043468820C901325284182A282A90317CB05C9120C5C800000089200000250000003222480920624600212B249814212524981419270C85A4906052645C20246582209A0118462080610401B843104109C24CD43CD0833CD4C338D0831BB44339D04338B0831EE8413B84033DC8433AE0430A084AD21451C2E4734E234D4033493F9D82829534459430F9E0E204C0920211C0483F9D0233887008A6184204446920608E000CE608406110211086110866040000000051180000630000001BFA23F8FFFFFFFF013005C00F003800FE00908009A0803E20C2011EE0411EDE011FDAC01CEAC11DC6A10DCC011EDAA01DC2811ED001A00779A8877200087390877068877268037878877470077A28077900C2811DD80120DA211DDCA10DD8A11CCE211CD8A10DECA11CC6811EDE411EDAE01ED2811CE8011D803890033C00067778873610877A480776A08774708779000877788736480777308779680373808736688770A0077400E8411EEAA11C00C21DDEA10DDC211CDC611EDAC01CE0A10DDA211CE8011D007A90877A280780708777688379488773708772208736D0877290877798873630077868837608077A4007801EE4A11ECA0120DCE11DDA801EE4211CE0011ED2C11DCEA10DDA211CE8011D007A90877A28078098077A08877158873680077978077A288771A0877790873610877A30077328077968837948077D2807000F00A21EDC611EC2C11CCAA10DCC011EDAA01DC2811ED001A00779A8877200367402012C002900D5100EE9200F6D200EF5600EE6500EF2D006EEF00E6D100EEC900EE1300F00000049180000010000001384400013B870480779B0033AF8057B90033B688370800778608772688376088771788779C08738A0033780033780830DB7510E6D000F7A600774A0077640077A600774D006E910077A80077A80076D900E78A00778A00778D006E9100776A0077160077A100776D006E9300772A0077320077A300772D006E9600774A0077640077A600774D006E6300772A0077320077A300772D006E6600774A0077640077A600774D006F6100776A0077160077A100776D006F6200774A0077320077A300772D006F6300772A0077320077A300772D006F6400778A0077640077A600774D006F6600774A0077640077A600774D006F6900776A00771200778A00771200778D006F610077280077A10077280077A10077280076D600F71900772A00772500776A00772500776D006F620077560077A20077560077A20077560076D600F75100772A00775100772A00775100772D006F6100770200774A0077100077240077A100770200774D006EE80077A100776A0077320071A210C69BCAC0920CD900A30120000020000000100000000000390D82050D4690000200B0400000A000000321E981019114C908C092647C6044362255008E55004E5538002055120C530025006051800000000B1180000A50000003308801CC4E11C6614013D88433884C38C4280077978077398710CE6000FED100EF4800E330C421EC2C11DCEA11C6630053D88433884831BCC033DC8433D8C033DCC788C7470077B08077948877070077A700376788770208719CC110EEC900EE1300F6E300FE3F00EF0500E3310C41DDE211CD8211DC2611E6630893BBC833BD04339B4033CBC833C84033BCCF0147660077B680737688772680737808770908770600776280776F8057678877780875F08877118877298877998812CEEF00EEEE00EF5C00EEC300362C8A11CE4A11CCCA11CE4A11CDC611CCA211CC4811DCA6106D6904339C84339984339C84339B8C33894433888033B94C32FBC833CFC823BD4033BB0C30CC7698770588772708374680778608774188774A08719CE530FEE000FF2500EE4900EE3400FE1200EEC500E3320281DDCC11EC2411ED2211CDC811EDCE01CE4E11DEA011E66185138B0433A9C833BCC50247660077B68073760877778077898514CF4900FF0500E331E6A1ECA611CE8211DDEC11D7E011EE4A11CCC211DF0610654858338CCC33BB0433DD04339FCC23CE4433B88C33BB0C38CC50A877998877718877408077A28077298815CE3100EECC00EE5500EF33023C1D2411EE4E117D8E11DDE011E6648193BB0833DB4831B84C3388C4339CCC33CB8C139C8C33BD4033CCC48B471080776600771088771588719DBC60EEC600FEDE006F0200FE5300FE5200FF6500E6E100EE3300EE5300FF3E006E9E00EE4500EF83023E2EC611CC2811DD8E117EC211DE6211DC4211DD8211DE8211F66209D3BBC433DB80339948339CC58BC7070077778077A08077A488777708719CE870EE5100EF0100EECC00EEF300EF3900EF4500E33283008877490073730877A708771A08774780777F88573908777A80778980700000000792000001D010000721E482043880C19097232482023818C9191D144A01028643C3132428E9021A398066400560000004A63611BB44171506C1C193091C128916310CB01490A7128484421CCB24497E30000000077636861725F73697A656672616D652D706F696E7465726169722E6D61785F6465766963655F627566666572736169722E6D61785F636F6E7374616E745F627566666572736169722E6D61785F74687265616467726F75705F627566666572736169722E6D61785F74657874757265736169722E6D61785F726561645F77726974655F74657874757265736169722E6D61785F73616D706C6572734170706C65206D6574616C2076657273696F6E2033323032332E38363420286D6574616C66652D33323032332E383634294D6574616C6169722E636F6D70696C652E64656E6F726D735F64697361626C656169722E636F6D70696C652E666173745F6D6174685F656E61626C656169722E636F6D70696C652E6672616D656275666665725F66657463685F656E61626C656169722E706F736974696F6E6169722E6172675F747970655F6E616D65666C6F6174346169722E6172675F6E616D65506F736974696F6E5F306169722E7665727465785F6F75747075747573657228544558434F4F524429666C6F61743255565F307573657228434F4C4F5229436F6C6F725F306169722E7665727465785F696E7075746169722E6C6F636174696F6E5F696E646578506F736974696F6E5F3155565F31436F6C6F725F316169722E6275666665726169722E726561646169722E616464726573735F73706163656169722E7374727563745F747970655F696E666F666C6F617434783450726F6A656374696F6E5F30436F6E7374616E74735F30636F6E7374616E74735F306169722E6172675F747970655F73697A656169722E6172675F747970655F616C69676E5F73697A65476C6F62616C506172616D735F30676C6F62616C506172616D735F3026760000000000003082C00423088C3082C00C23080C3182C01423088C3182C01C23080C3282C02423088C3282C02C23080A3082C030330C6A10ACC10C031B086D30C3C006831BCC30B001E106330C6C50B8C10C031B186F30C3C006071CCC30B00112073304C90C431BC8C11CCC402874A006733043B0CC10303304CD0C85F34091348331514F15593318D3F5401136C3500AA6700A332473906973A0064F156D33246A90696AA0064F157133246D90696DA0060F147533147360077320066330C31106AC3007763007645006334874E06973A0061F18B44118B48219D8C119C4C18306511ACC40A4822AAC822BCC30D4012ABCC29D01C0711CC7711CC7711CC7B9811BB8811BB8811BB8811B5874A0079665B9011DD0011DE0022EE0022B90836A80828C042628233636BB3697B637B23AB6321733B6B0B3B951103BB8033CC8033DD8033EE88354D8D8ECDA5CD2C8CADCE84609FC2097B0343917BB32B9B9B437B751823F482A2C4DCE852DCCEDAC2EECACECCBAE4C6E2EEDCD6D940014720A4B9373197B6B834B632BFB7A83A34B7B739B1B65080551188554C2D2E45CECCAE4E8CAF046095E01000000A9180000250000000B0A7228877780077A587098433DB8C338B04339D0C382E61CC6A10DE8411EC2C11DE6211DE8211DDEC11D1634E3600EE7500FE1200FE4400FE1200FE7500EF4B08081077928877060077678877108077A28077258709CC338B4013BA4833D94C3026B1CD8211CDCE11CDC201CE4611CDC201CE8811EC2611CD0A11CC8611CC2811DD861C1010FF4200FE1500FF4800E00000000D11000000600000007CC3CA4833B9C033B94033DA0833C94433890C30100000061200000360000001304412C1000000006000000D446004AA00CE88D008C45044110909801A0310300000000F1300000140000002247C890510A841C00000000980500006169722D616C6961732D73636F7065732856534D61696E296169722D616C6961732D73636F70652D61726728332900002B8459888515032DCC822C6C08680100FD18C8E138CE414128830C0D61A010887F3F06B34892A45010CA204384242804E28F4101FEFD1811745D974341283804E03FDBD004C06C437005B30DC1256C1010030000040000005B86E0A0852D4371D0C2964139680100000000007120000003000000320E1022840096060000000000000000650C00001F000000120394F0000000000300000006000000090000004C000000010000005800000000000000580000000100000070000000000000000F0000001C00000000000000060000000600000000000000700000000000000000000000010000000000000000000000060000000000000006000000FFFFFFFF00240000000000005D0C00000E0000001203946B0000000056534D61696E33323032332E38363461697236345F7632362D6170706C652D6D61636F737831342E302E3000000000000000000000000000010000008D030000524255467D03000000000000000000000018000000414952521000180004001000000000000000140010000000000000000400000000000000100000000400000001000000000000000A000000E00200009002000034020000F0010000AC0100007C0100004C010000D40000006800000004000000F4FDFFFF01000800040000009EFFFFFF04000000010000001400000010001000000000000400000008000C0010000000400000001C000000040000000C00000050726F6A656374696F6E5F300000000008000000666C6F617434783400000000B0FDFFFF010008000C0000000000060008000400060000000400000001000000140000001000140004000000080000000C00100010000000090000004000000018000000040000000B000000636F6E7374616E74735F30000B000000436F6E7374616E74735F3000BCFEFFFF00000400200000001C0024000000000008000C000600070010000000140018001C0020001C0000000000010200000000010000000800000040000000100000001C000000040000000E000000676C6F62616C506172616D735F3000000E000000476C6F62616C506172616D735F30000030FFFFFF0420040004000000A6FFFFFF0200000001000000500100000400000007000000436F6C6F725F31005CFFFFFF0420040004000000D2FFFFFF0100000001000000CC000000040000000400000055565F3100000000E4FEFFFF042004001400000000000E0014000000040008000C0010000E000000000000000100000090000000040000000A000000506F736974696F6E5F310000C8FFFFFF0400020004000000C0FFFFFF18000000BC0000000400000007000000436F6C6F725F30000B0000007573657228434F4C4F52290008000C000400080008000000040002001400000010001000000004000000000008000C00100000002400000014000000040000000400000055565F300000000006000000666C6F61743200000E0000007573657228544558434F4F5244290000BCFFFFFF020002001400000000000E000C00000000000000040008000E00000018000000040000000A000000506F736974696F6E5F30000006000000666C6F617434000008000E000400080008000000030000001000000000000A001000040008000C000A000000300000001C000000040000000400000004000000050000000600000007000000030000000100000002000000030000000600000056534D61696E0000454E4454",
-
- // Vertex Shader - Linear
- "4D544C4201800200070000810E000000881300000000000058000000000000008A00000000000000160100000000000039000000000000004F0100000000000008000000000000005701000000000000A00E000000000000010000008A0000004E414D45070056534D61696E005459504501000048415348200018279E3BBD163C92118F96D26DD622BB504BD02519E79C64165EE9EDB89C0D444F464654180000000000000000000000000000000000000000000000000056455253080002000600030001004D44535A0800A00E00000000000052464C5408000400000000000000454E4454524C53541000F70F0000000000009103000000000000555549441000C24CC1BA27A533429FAEB8AF316BA7F7454E4454390000005641545420000300506F736974696F6E5F3100008055565F31000180436F6C6F725F310002805641545905000300040406454E445408000000454E4454DEC0170B0000000014000000840E0000FFFFFFFF4243C0DE3514000003000000620C3024801005C814000000210C0000680300000B02210002000000160000000781239141C80449061032399201840C250508191E048B628014450242920B42A41032143808184B0A3252884870C421234412878C1041920264C808B1142043468820C901325284182A282A90317CB05C9120C5C800000089200000270000003222480920624600212B249814212524981419270C85A4906052645C20246582609A0118462080610401B843104109C24CD43CD0833CD4C338D0831BB44339D04338B0831EE8413B84033DC8433AE0430A084AD21451C2E4734E234D4033493F9D82829534459430F9E0E204C0920211C0483F9D0233887008A6184204446920608E000CE6084061102110861108661861608611066004000000000051180000630000001BFA23F8FFFFFFFF013005C00F003800FE00908009A0803E20C2011EE0411EDE011FDAC01CEAC11DC6A10DCC011EDAA01DC2811ED001A00779A8877200087390877068877268037878877470077A28077900C2811DD80120DA211DDCA10DD8A11CCE211CD8A10DECA11CC6811EDE411EDAE01ED2811CE8011D803890033C00067778873610877A480776A08774708779000877788736480777308779680373808736688770A0077400E8411EEAA11C00C21DDEA10DDC211CDC611EDAC01CE0A10DDA211CE8011D007A90877A280780708777688379488773708772208736D0877290877798873630077868837608077A4007801EE4A11ECA0120DCE11DDA801EE4211CE0011ED2C11DCEA10DDA211CE8011D007A90877A28078098077A08877158873680077978077A288771A0877790873610877A30077328077968837948077D2807000F00A21EDC611EC2C11CCAA10DCC011EDAA01DC2811ED001A00779A8877200367402012C002900D5100EE9200F6D200EF5600EE6500EF2D006EEF00E6D100EEC900EE1300F00000049180000010000001384400013B870480779B0033AF8057B90033B688370800778608772688376088771788779C08738A0033780033780830DB7510E6D000F7A600774A0077640077A600774D006E910077A80077A80076D900E78A00778A00778D006E9100776A0077160077A100776D006E9300772A0077320077A300772D006E9600774A0077640077A600774D006E6300772A0077320077A300772D006E6600774A0077640077A600774D006F6100776A0077160077A100776D006F6200774A0077320077A300772D006F6300772A0077320077A300772D006F6400778A0077640077A600774D006F6600774A0077640077A600774D006F6900776A00771200778A00771200778D006F610077280077A10077280077A10077280076D600F71900772A00772500776A00772500776D006F620077560077A20077560077A20077560076D600F75100772A00775100772A00775100772D006F6100770200774A0077100077240077A100770200774D006EE80077A100776A0077320071A210C69BCAC0920CD900A30120000020000000100000000000390D82050546D0000200B0400000A000000321E981019114C908C092647C6044362255008E55004E5538002055120C530025006051800000000B1180000A50000003308801CC4E11C6614013D88433884C38C4280077978077398710CE6000FED100EF4800E330C421EC2C11DCEA11C6630053D88433884831BCC033DC8433D8C033DCC788C7470077B08077948877070077A700376788770208719CC110EEC900EE1300F6E300FE3F00EF0500E3310C41DDE211CD8211DC2611E6630893BBC833BD04339B4033CBC833C84033BCCF0147660077B680737688772680737808770908770600776280776F8057678877780875F08877118877298877998812CEEF00EEEE00EF5C00EEC300362C8A11CE4A11CCCA11CE4A11CDC611CCA211CC4811DCA6106D6904339C84339984339C84339B8C33894433888033B94C32FBC833CFC823BD4033BB0C30CC7698770588772708374680778608774188774A08719CE530FEE000FF2500EE4900EE3400FE1200EEC500E3320281DDCC11EC2411ED2211CDC811EDCE01CE4E11DEA011E66185138B0433A9C833BCC50247660077B68073760877778077898514CF4900FF0500E331E6A1ECA611CE8211DDEC11D7E011EE4A11CCC211DF0610654858338CCC33BB0433DD04339FCC23CE4433B88C33BB0C38CC50A877998877718877408077A28077298815CE3100EECC00EE5500EF33023C1D2411EE4E117D8E11DDE011E6648193BB0833DB4831B84C3388C4339CCC33CB8C139C8C33BD4033CCC48B471080776600771088771588719DBC60EEC600FEDE006F0200FE5300FE5200FF6500E6E100EE3300EE5300FF3E006E9E00EE4500EF83023E2EC611CC2811DD8E117EC211DE6211DC4211DD8211DE8211F66209D3BBC433DB80339948339CC58BC7070077778077A08077A488777708719CE870EE5100EF0100EECC00EEF300EF3900EF4500E33283008877490073730877A708771A08774780777F88573908777A80778980700000000792000001D010000721E482043880C19097232482023818C9191D144A01028643C3132428E9021A398066400560000004A63611BB44171506C1C193091C128916310CB01490A7128484421CCB24497E30000000077636861725F73697A656672616D652D706F696E7465726169722E6D61785F6465766963655F627566666572736169722E6D61785F636F6E7374616E745F627566666572736169722E6D61785F74687265616467726F75705F627566666572736169722E6D61785F74657874757265736169722E6D61785F726561645F77726974655F74657874757265736169722E6D61785F73616D706C6572734170706C65206D6574616C2076657273696F6E2033323032332E38363420286D6574616C66652D33323032332E383634294D6574616C6169722E636F6D70696C652E64656E6F726D735F64697361626C656169722E636F6D70696C652E666173745F6D6174685F656E61626C656169722E636F6D70696C652E6672616D656275666665725F66657463685F656E61626C656169722E706F736974696F6E6169722E6172675F747970655F6E616D65666C6F6174346169722E6172675F6E616D65506F736974696F6E5F306169722E7665727465785F6F75747075747573657228544558434F4F524429666C6F61743255565F307573657228434F4C4F5229436F6C6F725F306169722E7665727465785F696E7075746169722E6C6F636174696F6E5F696E646578506F736974696F6E5F3155565F31436F6C6F725F316169722E6275666665726169722E726561646169722E616464726573735F73706163656169722E7374727563745F747970655F696E666F666C6F617434783450726F6A656374696F6E5F30436F6E7374616E74735F30636F6E7374616E74735F306169722E6172675F747970655F73697A656169722E6172675F747970655F616C69676E5F73697A65476C6F62616C506172616D735F30676C6F62616C506172616D735F3026760000000000003082C00423088C3082C00C23080C3182C01423088C3182C01C23080C3282C02423088C3282C02C23080A3082C030330C6A10ACC10C031B086D30C3C006831BCC30B001E106330C6C50B8C10C031B186F30C3C006071CCC30B00112073304C90C431BC8C11CCC402874A006733043B0CC10303304CD0C85F34091348331514F15593318D3F5401136C3500AA6700A332473906973A0064F156D33246A90696AA0064F157133246D90696DA0060F147533147360077320066330C31106AC3007763007645006334874E06973A0061F18B44118B48219D8C119C4C18306511ACC40A4822AAC822BCC30D4012ABCC29D01C0711CC7711CC7711CC7B9811BB8811BB8811BB8811B5874A0079665B9011DD0011DE0022EE0022B90836A80828C042628233636BB3697B637B23AB6321733B6B0B3B951103BB8033CC8033DD8033EE88354D8D8ECDA5CD2C8CADCE84609FC2097B0343917BB32B9B9B437B751823F482A2C4DCE852DCCEDAC2EECACECCBAE4C6E2EEDCD6D940014720A4B9373197B6B834B632BFB7A83A34B7B739B1B65080551188554C2D2E45CECCAE4E8CAF046095E01000000A9180000250000000B0A7228877780077A587098433DB8C338B04339D0C382E61CC6A10DE8411EC2C11DE6211DE8211DDEC11D1634E3600EE7500FE1200FE4400FE1200FE7500EF4B08081077928877060077678877108077A28077258709CC338B4013BA4833D94C3026B1CD8211CDCE11CDC201CE4611CDC201CE8811EC2611CD0A11CC8611CC2811DD861C1010FF4200FE1500FF4800E00000000D11000000600000007CC3CA4833B9C033B94033DA0833C94433890C30100000061200000500000001304412C1000000018000000C46600A88D00944019D01B01188B08822098835002221A8B0002E12038D600040285D100123300346600288E35C8288DA79F8CD278FAC9288DA7DF580369A3BDFC9136DACB1F69A3BDFC8D3588E6AA939E68AE3AE989E6AA93DE680000000000F1300000140000002247C890510A841C00000000980500006169722D616C6961732D73636F7065732856534D61696E296169722D616C6961732D73636F70652D61726728332900002B8459888515032DCC822C6C08680100FD18D1735DD7434128830C16E2A010887F3F46356DDB265110CA2083C6442804E28F4101FEFD181A06066000061605A1E01080FF208337511844E28F4104FE180CE28F8104FE1814E23FC8104CDC20435006DC6C439600B30D011A04B30DC1206C101003040000005B86E0A0852D4371D0C2964139680100000000007120000003000000320E1022840098060000000000000000650C00001F000000120394F0000000000300000006000000090000004C000000010000005800000000000000580000000100000070000000000000000F0000001C00000000000000060000000600000000000000700000000000000000000000010000000000000000000000060000000000000006000000FFFFFFFF00240000000000005D0C00000E0000001203946B0000000056534D61696E33323032332E38363461697236345F7632362D6170706C652D6D61636F737831342E302E3000000000000000000000000000010000008D030000524255467D03000000000000000000000018000000414952521000180004001000000000000000140010000000000000000400000000000000100000000400000001000000000000000A000000E00200009002000034020000F0010000AC0100007C0100004C010000D40000006800000004000000F4FDFFFF01000800040000009EFFFFFF04000000010000001400000010001000000000000400000008000C0010000000400000001C000000040000000C00000050726F6A656374696F6E5F300000000008000000666C6F617434783400000000B0FDFFFF010008000C0000000000060008000400060000000400000001000000140000001000140004000000080000000C00100010000000090000004000000018000000040000000B000000636F6E7374616E74735F30000B000000436F6E7374616E74735F3000BCFEFFFF00000400200000001C0024000000000008000C000600070010000000140018001C0020001C0000000000010200000000010000000800000040000000100000001C000000040000000E000000676C6F62616C506172616D735F3000000E000000476C6F62616C506172616D735F30000030FFFFFF0420040004000000A6FFFFFF0200000001000000500100000400000007000000436F6C6F725F31005CFFFFFF0420040004000000D2FFFFFF0100000001000000CC000000040000000400000055565F3100000000E4FEFFFF042004001400000000000E0014000000040008000C0010000E000000000000000100000090000000040000000A000000506F736974696F6E5F310000C8FFFFFF0400020004000000C0FFFFFF18000000BC0000000400000007000000436F6C6F725F30000B0000007573657228434F4C4F52290008000C000400080008000000040002001400000010001000000004000000000008000C00100000002400000014000000040000000400000055565F300000000006000000666C6F61743200000E0000007573657228544558434F4F5244290000BCFFFFFF020002001400000000000E000C00000000000000040008000E00000018000000040000000A000000506F736974696F6E5F30000006000000666C6F617434000008000E000400080008000000030000001000000000000A001000040008000C000A000000300000001C000000040000000400000004000000050000000600000007000000030000000100000002000000030000000600000056534D61696E0000454E4454",
-
- // Pixel Shader
- "4D544C4201800200070000810E000000D81100000000000058000000000000008A00000000000000160100000000000008000000000000001E0100000000000008000000000000002601000000000000000E000000000000010000008A0000004E414D45070050534D61696E00545950450100014841534820008335EA1F82646DCCC623827DA6EBF0258B1E224AA4218D60D70CC077494E05494F464654180000000000000000000000000000000000000000000000000056455253080002000600030001004D44535A0800000E00000000000052464C5408000400000000000000454E4454524C53541000260F000000000000B202000000000000555549441000C710A16106E43B598CE29DE59990D629454E445408000000454E445408000000454E4454DEC0170B0000000014000000EC0D0000FFFFFFFF4243C0DE3514000003000000620C3024801005C814000000210C0000350300000B02210002000000160000000781239141C80449061032399201840C250508191E048B628014450242920B42A41032143808184B0A3252884870C421234412878C1041920264C808B1142043468820C901325284182A282A90317CB05C9120C5C8000000892000001F0000003222480920624600212B249814212524981419270C85A4906052645C20246582609A01184620801B84610401404A9A224A98FC7F22AE898A88DF1EFE698C00184420028CA429A284C9FF2580791622FAA731026010C1108C214608E5109A23408E10D41C4130470006C30842639455CE608E01D0E80D048C0000000051180000670000001BF623F8FFFFFFFF015803C014003F002460022AA00F8870800778908777C0873630877A708771680373808736688770A0077400E8411EEAA11C00C21CE4211CDAA11CDA001EDE211DDC811ECA411E807060077600887648077768037628877308077668037B288771A08777908736B8877420077A4007200EE4000F80C11DDEA10DC4A11ED2811DE8211DDC611E00C21DDEA10DD2C11DCC611EDAC01CE0A10DDA211CE8011D007A90877A28078070877768037708077798873630077868837608077A4007801EE4A11ECA0120DCE11DDA601ED2E11CDCA11CC8A10DF4A11CE4E11DE6A10DCC011EDAA01DC2811ED001A00779A88772000877788736A0077908077880877470877368837608077A4007801EE4A11ECA0120E6811EC2611CD6A10DE0411EDE811ECA611CE8E11DE4A10DC4A11ECCC11CCA411EDA601ED2411FCA01C00380A80777988770308772680373808736688770A0077400E8411EEAA11C800D843000A4B0011AFEFFFFFF7F00DA005803C014003F0024A002FA60834104C002541B8C420016A0DAC018FFFFFFFF3F006D00AC01200115D00700491800000300000013844098300C44316130880213B870480779B0033AF8057B90033B688370800778608772688376088771788779C08738A0033780033780830DB7510E6D000F7A600774A0077640077A600774D006E910077A80077A80076D900E78A00778A00778D006E9100776A0077160077A100776D006E9300772A0077320077A300772D006E9600774A0077640077A600774D006E6300772A0077320077A300772D006E6600774A0077640077A600774D006F6100776A0077160077A100776D006F6200774A0077320077A300772D006F6300772A0077320077A300772D006F6400778A0077640077A600774D006F6600774A0077640077A600774D006F6900776A00771200778A00771200778D006F610077280077A10077280077A10077280076D600F71900772A00772500776A00772500776D006F620077560077A20077560077A20077560076D600F75100772A00775100772A00775100772D006F6100770200774A0077100077240077A100770200774D006EE80077A100776A0077320071A210C69A4AC0920CD900A301000000200000001000000000003185219DB0304800000002000000000006000121B048ADE0C000064810009000000321E981019114C908C092647C604436A255008E55004E5538002055120C5300250060000B1180000A50000003308801CC4E11C6614013D88433884C38C4280077978077398710CE6000FED100EF4800E330C421EC2C11DCEA11C6630053D88433884831BCC033DC8433D8C033DCC788C7470077B08077948877070077A700376788770208719CC110EEC900EE1300F6E300FE3F00EF0500E3310C41DDE211CD8211DC2611E6630893BBC833BD04339B4033CBC833C84033BCCF0147660077B680737688772680737808770908770600776280776F8057678877780875F08877118877298877998812CEEF00EEEE00EF5C00EEC300362C8A11CE4A11CCCA11CE4A11CDC611CCA211CC4811DCA6106D6904339C84339984339C84339B8C33894433888033B94C32FBC833CFC823BD4033BB0C30CC7698770588772708374680778608774188774A08719CE530FEE000FF2500EE4900EE3400FE1200EEC500E3320281DDCC11EC2411ED2211CDC811EDCE01CE4E11DEA011E66185138B0433A9C833BCC50247660077B68073760877778077898514CF4900FF0500E331E6A1ECA611CE8211DDEC11D7E011EE4A11CCC211DF0610654858338CCC33BB0433DD04339FCC23CE4433B88C33BB0C38CC50A877998877718877408077A28077298815CE3100EECC00EE5500EF33023C1D2411EE4E117D8E11DDE011E6648193BB0833DB4831B84C3388C4339CCC33CB8C139C8C33BD4033CCC48B471080776600771088771588719DBC60EEC600FEDE006F0200FE5300FE5200FF6500E6E100EE3300EE5300FF3E006E9E00EE4500EF83023E2EC611CC2811DD8E117EC211DE6211DC4211DD8211DE8211F66209D3BBC433DB80339948339CC58BC7070077778077A08077A488777708719CE870EE5100EF0100EECC00EEF300EF3900EF4500E33283008877490073730877A708771A08774780777F88573908777A807789807000000007920000008010000721E482043880C19097232482023818C9191D144A01028643C3132428E9021A3680660604F0000004A63611BB44171506C1C194491C12092A33C06B11C8CA4388BA460C97224000077636861725F73697A656672616D652D706F696E7465726169722E6D61785F6465766963655F627566666572736169722E6D61785F636F6E7374616E745F627566666572736169722E6D61785F74687265616467726F75705F627566666572736169722E6D61785F74657874757265736169722E6D61785F726561645F77726974655F74657874757265736169722E6D61785F73616D706C6572734170706C65206D6574616C2076657273696F6E2033323032332E38363420286D6574616C66652D33323032332E383634294D6574616C6169722E636F6D70696C652E64656E6F726D735F64697361626C656169722E636F6D70696C652E666173745F6D6174685F656E61626C656169722E636F6D70696C652E6672616D656275666665725F66657463685F656E61626C656169722E72656E6465725F7461726765746169722E6172675F747970655F6E616D65666C6F6174346169722E6172675F6E616D656F75747075745F306169722E667261676D656E745F696E7075747573657228544558434F4F5244296169722E63656E7465726169722E7065727370656374697665666C6F61743255565F307573657228434F4C4F5229436F6C6F725F306169722E706F736974696F6E6169722E6E6F5F7065727370656374697665506F736974696F6E5F306169722E6172675F756E757365646169722E746578747572656169722E6C6F636174696F6E5F696E6465786169722E73616D706C657465787475726532643C666C6F61742C2073616D706C653E746578747572655F316169722E73616D706C657273616D706C657273616D706C65725F31000026630000000000003082D0082308CD3082D01023084D3182D0182308CD3182D02023084D3282D0282308CD3282D03023080930C37006011ACC30A481A006330C6930ACC10C431A106B30C39006C51ACC30A481C106330C6970B4C10C431A206E304390CC30A8C11BC0C10C8412076700073304CB0C013343D0CC70387000070F14493304A130430207135559CF1561332467306595F540913643A2065BC53D50D479332871F081011C9C41183C62108DC10C091A900118C0C1193C651099C10CC52890422998C229CC30C88128A0C28D01C0711CC7711CC7711CE7066EE0066EE0066EE0066E60D1811E5896650A1C2BB0022BD8033CB0828C042628233636BB3697B637B23AB6321733B6B0B3B951103998033AA8033BB8033CC88354D8D8ECDA5CD2C8CADCE84609F42097B0343917BB32B9B9B437B751823D482A2C4DCE852DCCEDAC2EECACECCBAE4C6E2EEDCD6D94800F720A4B9373197B6B834B632BFB7A83A34B7B739B1B65E8033FF88364C2D2E45CCCE4C2CEDACADCE84609500100A9180000250000000B0A7228877780077A587098433DB8C338B04339D0C382E61CC6A10DE8411EC2C11DE6211DE8211DDEC11D1634E3600EE7500FE1200FE4400FE1200FE7500EF4B08081077928877060077678877108077A28077258709CC338B4013BA4833D94C3026B1CD8211CDCE11CDC201CE4611CDC201CE8811EC2611CD0A11CC8611CC2811DD861C1010FF4200FE1500FF4800E00000000D11000000600000007CC3CA4833B9C033B94033DA0833C94433890C30100000061200000310000001304412C1000000004000000C46A600480DC08008111001233000000F13000001C0000002247C890510E042B00000000188601006169722D616C6961732D73636F7065732850534D61696E296169722D616C6961732D73636F70652D73616D706C6572736169722D616C6961732D73636F70652D74657874757265732B8456508515832BB4C22AAC185EA115586183F00AAE00002306CD108260F05887A114032108CC68420060B088FF6C0311001B04C4000000020000005B06E07805000000000000007120000003000000320E1022840084060000000000000000650C00002500000012039428010000000300000021000000090000004C000000010000005800000000000000580000000200000088000000000000002A0000001C00000000000000060000000600000000000000880000000000000000000000020000000000000000000000060000000000000006000000FFFFFFFF00240000060000001B000000060000001B000000FFFFFFFF08240000000000005D0C000015000000120394A60000000050534D61696E6169722E73616D706C655F746578747572655F32642E763466333233323032332E38363461697236345F7632362D6170706C652D6D61636F737831342E302E3000000000000001000000AE020000524255469E02000000000000000000000000140000004149525200000A0018000400100014000A000000000000000400000000000000100000000400000001000000000000000700000008020000AC0100004801000008010000C000000054000000040000001CFEFFFF010004001400000000000E0014000000040008000C0010000E000000000000000100000018000000040000000900000073616D706C65725F310000000700000073616D706C65720068FEFFFF0200040018000000000012001800000008000C00070000001000140012000000000000040000000001000000180000000400000009000000746578747572655F31000000180000007465787475726532643C666C6F61742C2073616D706C653E00000000C4FFFFFF02300400140000001000100000000000000008000C000700100000000000000100010000040000000A000000506F736974696F6E5F30000008000C0004000800080000000030040004000000C2FFFFFF0000010118000000C80000000400000007000000436F6C6F725F30000B0000007573657228434F4C4F52290050FFFFFF0030040018000000000012001400000008000000060007000C00100012000000000001012400000014000000040000000400000055565F300000000006000000666C6F61743200000E0000007573657228544558434F4F5244290000B0FFFFFF0010020018000000000012001400000004000800000000000C0010001200000000000000000000001800000004000000080000006F75747075745F300000000006000000666C6F617434000008000E000400080008000000010000001000000000000A001000040008000C000A0000002C000000200000000400000005000000020000000300000004000000050000000600000001000000010000000600000050534D61696E0000454E4454"
- ];
-
- private readonly ResourceLayoutDesc MetallibDesc = new()
- {
- Bindings =
- [
- new() { Type = ResourceType.ConstantBuffer, Index = 0, Count = 1, StageFlags = ShaderStageFlags.Vertex },
- new() { Type = ResourceType.Texture, Index = 0, Count = 1, StageFlags = ShaderStageFlags.Pixel },
- new() { Type = ResourceType.Sampler, Index = 0, Count = 1, StageFlags = ShaderStageFlags.Pixel }
- ]
- };
-
- private readonly Buffer constants;
private readonly Sampler sampler;
- private readonly ResourceLayout resourceLayout;
private readonly GraphicsPipeline graphicsPipeline;
private readonly Dictionary textureBindings = [];
private readonly Dictionary textureViewBindings = [];
- private readonly Dictionary resourceTableBindings = [];
+ private readonly SortedList resourceHandleBindings = new(Comparer.Create(static (x, y) => x.Handle.CompareTo(y.Handle)));
private readonly Dictionary drawDataTextures = [];
private Buffer? vertexBuffer;
private Buffer? indexBuffer;
+ private Buffer? constantBuffer;
- public ImGuiRenderer(GraphicsContext context, Output output, ImGuiColorSpace colorSpace)
+ public ImGuiRenderer(GraphicsContext context, AttachmentFormats attachmentFormats, ImGuiColorSpace colorSpace)
{
- byte[] vertexShaderBytes = [];
- byte[] pixelShaderBytes = [];
- ResourceLayoutDesc resourceLayoutDesc = default;
- switch (context.Backend)
- {
- case Backend.DirectX12:
- vertexShaderBytes = Convert.FromHexString(colorSpace is ImGuiColorSpace.Legacy ? Dxil[0] : Dxil[1]);
- pixelShaderBytes = Convert.FromHexString(Dxil[2]);
- resourceLayoutDesc = DxilDesc;
- break;
-
- case Backend.Vulkan:
- vertexShaderBytes = Convert.FromHexString(colorSpace is ImGuiColorSpace.Legacy ? Spirv[0] : Spirv[1]);
- pixelShaderBytes = Convert.FromHexString(Spirv[2]);
- resourceLayoutDesc = SpirvDesc;
- break;
-
- case Backend.Metal:
- vertexShaderBytes = Convert.FromHexString(colorSpace is ImGuiColorSpace.Legacy ? Metallib[0] : Metallib[1]);
- pixelShaderBytes = Convert.FromHexString(Metallib[2]);
- resourceLayoutDesc = MetallibDesc;
- break;
- }
+ string source = Source.Replace("#if 0", $"#if {(colorSpace is ImGuiColorSpace.Legacy ? 0 : 1)}");
- using Shader vertex = context.CreateShader(new()
- {
- ShaderBytes = vertexShaderBytes,
- EntryPoint = "VSMain",
- Stage = ShaderStageFlags.Vertex
- });
+ sampler = context.CreateSampler(SamplerDesc.PointClamp());
+
+ using Shader vertex = context.CreateShader(ZenithCompiler.CompileFromSource(context.GraphicsApi, source, "VSMain"));
+ using Shader fragment = context.CreateShader(ZenithCompiler.CompileFromSource(context.GraphicsApi, source, "FSMain"));
- using Shader pixel = context.CreateShader(new()
+ InputLayout inputLayout = new();
+ inputLayout.Add(new()
{
- ShaderBytes = pixelShaderBytes,
- EntryPoint = "PSMain",
- Stage = ShaderStageFlags.Pixel
+ Format = ElementFormat.Float2,
+ Semantic = ElementSemantic.Position
});
- constants = context.CreateBuffer(new()
+ inputLayout.Add(new()
{
- SizeInBytes = (uint)sizeof(Constants),
- StrideInBytes = (uint)sizeof(Constants),
- Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite
+ Format = ElementFormat.Float2,
+ Semantic = ElementSemantic.TexCoord
});
- sampler = context.CreateSampler(new()
+ inputLayout.Add(new()
{
- U = AddressMode.Clamp,
- V = AddressMode.Clamp,
- W = AddressMode.Clamp,
- Filter = Filter.MinPointMagPointMipPoint
+ Format = ElementFormat.UByte4UNorm,
+ Semantic = ElementSemantic.Color
});
- InputLayout inputLayout = new();
- inputLayout.Add(new() { Format = ElementFormat.Float2, Semantic = ElementSemantic.Position });
- inputLayout.Add(new() { Format = ElementFormat.Float2, Semantic = ElementSemantic.TexCoord });
- inputLayout.Add(new() { Format = ElementFormat.UByte4Normalized, Semantic = ElementSemantic.Color });
-
graphicsPipeline = context.CreateGraphicsPipeline(new()
{
- RenderStates = new()
- {
- RasterizerState = RasterizerStates.Default,
- DepthStencilState = DepthStencilStates.None,
- BlendState = BlendStates.AlphaBlend
- },
- Vertex = vertex,
- Pixel = pixel,
- ResourceLayout = resourceLayout = context.CreateResourceLayout(resourceLayoutDesc),
+ VertexShader = vertex,
+ FragmentShader = fragment,
InputLayouts = [inputLayout],
PrimitiveTopology = PrimitiveTopology.TriangleList,
- Output = output
+ AttachmentFormats = attachmentFormats,
+ RenderState = new()
+ {
+ Rasterizer = RasterizerState.CullNone(),
+ DepthStencil = DepthStencilState.DepthNone(),
+ Blend = BlendState.NonPremultiplied()
+ }
});
Context = context;
@@ -224,16 +129,12 @@ public ImTextureID Binding(Texture texture)
if (!textureBindings.TryGetValue(texture, out ImTextureID textureID))
{
ulong id = 0;
- while (resourceTableBindings.ContainsKey(id))
+ while (resourceHandleBindings.ContainsKey(id))
{
id++;
}
- resourceTableBindings[textureBindings[texture] = textureID = id] = Context.CreateResourceTable(new()
- {
- Layout = resourceLayout,
- Resources = [constants, texture, sampler]
- });
+ resourceHandleBindings[textureBindings[texture] = textureID = id] = texture.SampledHandle;
}
return textureID;
@@ -244,22 +145,18 @@ public ImTextureID Binding(TextureView textureView)
if (!textureViewBindings.TryGetValue(textureView, out ImTextureID textureID))
{
ulong id = 0;
- while (resourceTableBindings.ContainsKey(id))
+ while (resourceHandleBindings.ContainsKey(id))
{
id++;
}
- resourceTableBindings[textureViewBindings[textureView] = textureID = id] = Context.CreateResourceTable(new()
- {
- Layout = resourceLayout,
- Resources = [constants, textureView, sampler]
- });
+ resourceHandleBindings[textureViewBindings[textureView] = textureID = id] = textureView.SampledHandle;
}
return textureID;
}
- public void Render(CommandBuffer commandBuffer, FrameBuffer frameBuffer, ClearValue clearValue, ImDrawDataPtr drawData)
+ public void Render(CommandBuffer commandBuffer, ColorAttachment colorAttachment, ImDrawDataPtr drawData)
{
if (drawData.CmdListsCount is 0)
{
@@ -276,32 +173,46 @@ public void Render(CommandBuffer commandBuffer, FrameBuffer frameBuffer, ClearVa
{
case ImTextureStatus.WantCreate:
{
- Texture texture = Context.CreateTexture(new()
+ Texture texture = Context.CreateTexture(TextureDesc.Texture2D(textureData.Format is ImTextureFormat.Rgba32 ? PixelFormat.R8G8B8A8UNorm : PixelFormat.R8UNorm,
+ (uint)textureData.Width,
+ (uint)textureData.Height,
+ 1,
+ SampleCount.Count1));
+
+ Extent3D extent = new()
{
- Type = TextureType.Texture2D,
- Format = textureData.Format is ImTextureFormat.Rgba32 ? PixelFormat.R8G8B8A8UNorm : PixelFormat.R8UNorm,
Width = (uint)textureData.Width,
Height = (uint)textureData.Height,
- Depth = 1,
- MipLevels = 1,
- ArrayLayers = 1,
- SampleCount = SampleCount.Count1,
- Flags = TextureUsageFlags.ShaderResource
- });
-
- TextureExtent extent = new() { Width = (uint)textureData.Width, Height = (uint)textureData.Height, Depth = 1 };
+ Depth = 1
+ };
if (textureData.Format is ImTextureFormat.Rgba32)
{
- ReadOnlySpan pixels = new(textureData.Pixels, textureData.Width * textureData.Height);
-
- commandBuffer.Upload(texture, default, default, extent, pixels);
+ TextureData data = new()
+ {
+ Pointer = (nint)textureData.Pixels,
+ SizeInBytes = ZenithHelper.SizeInBytes(texture.Desc.Format, extent.Width, extent.Height),
+ RowStrideInBytes = ZenithHelper.RowStrideInBytes(texture.Desc.Format, extent.Width, extent.Height),
+ SliceStrideInBytes = ZenithHelper.SliceStrideInBytes(texture.Desc.Format, extent.Width, extent.Height)
+ };
+
+ commandBuffer.Transition(texture, default, TextureLayout.Undefined, TextureLayout.CopyDst);
+ commandBuffer.Upload(texture, default, default, extent, data);
+ commandBuffer.Transition(texture, default, TextureLayout.CopyDst, TextureLayout.Sampled);
}
else
{
- ReadOnlySpan pixels = new(textureData.Pixels, textureData.Width * textureData.Height);
-
- commandBuffer.Upload(texture, default, default, extent, pixels);
+ TextureData data = new()
+ {
+ Pointer = (nint)textureData.Pixels,
+ SizeInBytes = ZenithHelper.SizeInBytes(texture.Desc.Format, extent.Width, extent.Height),
+ RowStrideInBytes = ZenithHelper.RowStrideInBytes(texture.Desc.Format, extent.Width, extent.Height),
+ SliceStrideInBytes = ZenithHelper.SliceStrideInBytes(texture.Desc.Format, extent.Width, extent.Height)
+ };
+
+ commandBuffer.Transition(texture, default, TextureLayout.Undefined, TextureLayout.CopyDst);
+ commandBuffer.Upload(texture, default, default, extent, data);
+ commandBuffer.Transition(texture, default, TextureLayout.CopyDst, TextureLayout.Sampled);
}
textureData.SetTexID(Binding(texture));
@@ -319,32 +230,63 @@ public void Render(CommandBuffer commandBuffer, FrameBuffer frameBuffer, ClearVa
{
ImTextureRect rect = textureData.Updates[j];
- TextureOffset offset = new() { X = rect.X, Y = rect.Y, Z = 0 };
- TextureExtent extent = new() { Width = rect.W, Height = rect.H, Depth = 1 };
+ Offset3D offset = new()
+ {
+ X = rect.X,
+ Y = rect.Y,
+ Z = 0
+ };
+
+ Extent3D extent = new()
+ {
+ Width = rect.W,
+ Height = rect.H,
+ Depth = 1
+ };
using ZenithMarshal.Scope scope = new();
if (textureData.Format is ImTextureFormat.Rgba32)
{
- Span pixels = new((int*)ZenithMarshal.Allocate(scope, (uint)(rect.W * rect.H)), rect.W * rect.H);
+ int* pointer = (int*)ZenithMarshal.Allocate(scope, (uint)(rect.W * rect.H));
for (ushort k = 0; k < rect.H; k++)
{
- new ReadOnlySpan(textureData.GetPixelsAt(rect.X, rect.Y + k), rect.W).CopyTo(pixels.Slice(k * rect.W, rect.W));
+ new ReadOnlySpan(textureData.GetPixelsAt(rect.X, rect.Y + k), rect.W).CopyTo(new(pointer + (k * rect.W), rect.W));
}
- commandBuffer.Upload(texture, default, offset, extent, pixels);
+ TextureData data = new()
+ {
+ Pointer = (nint)pointer,
+ SizeInBytes = (uint)(sizeof(int) * rect.W * rect.H),
+ RowStrideInBytes = ZenithHelper.RowStrideInBytes(texture.Desc.Format, extent.Width, extent.Height),
+ SliceStrideInBytes = ZenithHelper.SliceStrideInBytes(texture.Desc.Format, extent.Width, extent.Height)
+ };
+
+ commandBuffer.Transition(texture, default, TextureLayout.Sampled, TextureLayout.CopyDst);
+ commandBuffer.Upload(texture, default, offset, extent, data);
+ commandBuffer.Transition(texture, default, TextureLayout.CopyDst, TextureLayout.Sampled);
}
else
{
- Span pixels = new((byte*)ZenithMarshal.Allocate(scope, (uint)(rect.W * rect.H)), rect.W * rect.H);
+ byte* pointer = (byte*)ZenithMarshal.Allocate(scope, (uint)(rect.W * rect.H));
for (ushort k = 0; k < rect.H; k++)
{
- new ReadOnlySpan(textureData.GetPixelsAt(rect.X, rect.Y + k), rect.W).CopyTo(pixels.Slice(k * rect.W, rect.W));
+ new ReadOnlySpan(textureData.GetPixelsAt(rect.X, rect.Y + k), rect.W).CopyTo(new(pointer + (k * rect.W), rect.W));
}
- commandBuffer.Upload(texture, default, offset, extent, pixels);
+ TextureData data = new()
+ {
+ Pointer = (nint)pointer,
+ SizeInBytes = (uint)(rect.W * rect.H),
+ RowStrideInBytes = ZenithHelper.RowStrideInBytes(texture.Desc.Format, extent.Width, extent.Height),
+ SliceStrideInBytes = ZenithHelper.SliceStrideInBytes(texture.Desc.Format, extent.Width, extent.Height)
+ };
+
+ commandBuffer.Transition(texture, default, TextureLayout.Sampled, TextureLayout.CopyDst);
+ commandBuffer.Upload(texture, default, offset, extent, data);
+ commandBuffer.Transition(texture, default, TextureLayout.CopyDst, TextureLayout.Sampled);
}
}
}
@@ -373,12 +315,11 @@ public void Render(CommandBuffer commandBuffer, FrameBuffer frameBuffer, ClearVa
if (vertexBuffer is null || vertexBuffer.Desc.SizeInBytes < totalVertexSizeInBytes)
{
vertexBuffer?.Dispose();
-
vertexBuffer = Context.CreateBuffer(new()
{
SizeInBytes = totalVertexSizeInBytes,
- StrideInBytes = (uint)sizeof(ImDrawVert),
- Flags = BufferUsageFlags.Vertex | BufferUsageFlags.MapWrite
+ Usages = BufferUsages.Vertex,
+ Residency = MemoryResidency.CpuWriteOnly
});
}
@@ -389,8 +330,20 @@ public void Render(CommandBuffer commandBuffer, FrameBuffer frameBuffer, ClearVa
indexBuffer = Context.CreateBuffer(new()
{
SizeInBytes = totalIndexSizeInBytes,
- StrideInBytes = sizeof(ushort),
- Flags = BufferUsageFlags.Index | BufferUsageFlags.MapWrite
+ Usages = BufferUsages.Index,
+ Residency = MemoryResidency.CpuWriteOnly
+ });
+ }
+
+ uint totalConstantSizeInBytes = (uint)(sizeof(Constants) * (int)(resourceHandleBindings.Count * 1.2));
+ if (constantBuffer is null || constantBuffer.Desc.SizeInBytes < totalConstantSizeInBytes)
+ {
+ constantBuffer?.Dispose();
+ constantBuffer = Context.CreateBuffer(new()
+ {
+ SizeInBytes = totalConstantSizeInBytes,
+ Usages = BufferUsages.Constant,
+ Residency = MemoryResidency.CpuWriteOnly
});
}
@@ -398,29 +351,55 @@ public void Render(CommandBuffer commandBuffer, FrameBuffer frameBuffer, ClearVa
{
ImDrawListPtr drawListPtr = drawData.CmdLists[i];
- ReadOnlySpan verts = new(drawListPtr.VtxBuffer.Data, drawListPtr.VtxBuffer.Size);
- ReadOnlySpan indices = new(drawListPtr.IdxBuffer.Data, drawListPtr.IdxBuffer.Size);
+ vertexBuffer.Upload((uint)(sizeof(ImDrawVert) * vertexOffset), new()
+ {
+ Pointer = (nint)drawListPtr.VtxBuffer.Data,
+ SizeInBytes = (uint)(sizeof(ImDrawVert) * drawListPtr.VtxBuffer.Size)
+ });
- vertexBuffer.Upload(verts, (uint)(sizeof(ImDrawVert) * vertexOffset));
- indexBuffer.Upload(indices, (uint)(sizeof(ushort) * indexOffset));
+ indexBuffer.Upload((uint)(sizeof(ushort) * indexOffset), new()
+ {
+ Pointer = (nint)drawListPtr.IdxBuffer.Data,
+ SizeInBytes = (uint)(sizeof(ushort) * drawListPtr.IdxBuffer.Size)
+ });
vertexOffset += drawListPtr.VtxBuffer.Size;
indexOffset += drawListPtr.IdxBuffer.Size;
}
- constants.Upload([new Constants
+ Matrix4x4 projection = Matrix4x4.CreateOrthographicOffCenter(drawData.DisplayPos.X,
+ drawData.DisplayPos.X + drawData.DisplaySize.X,
+ drawData.DisplayPos.Y + drawData.DisplaySize.Y,
+ drawData.DisplayPos.Y,
+ 0.0f,
+ 1.0f);
+
+ Constants[] constants = ArrayPool.Shared.Rent(resourceHandleBindings.Count);
+
+ for (int i = 0; i < resourceHandleBindings.Count; i++)
+ {
+ constants[i] = new()
+ {
+ Projection = projection,
+ Texture = resourceHandleBindings.Values[i],
+ Sampler = sampler.Handle
+ };
+ }
+
+ fixed (Constants* pointer = constants)
{
- Projection = Matrix4x4.CreateOrthographicOffCenter(drawData.DisplayPos.X,
- drawData.DisplayPos.X + drawData.DisplaySize.X,
- drawData.DisplayPos.Y + drawData.DisplaySize.Y,
- drawData.DisplayPos.Y,
- 0.0f,
- 1.0f)
- }], 0);
+ constantBuffer.Upload(0, new()
+ {
+ Pointer = (nint)pointer,
+ SizeInBytes = (uint)(sizeof(Constants) * resourceHandleBindings.Count)
+ });
+ }
+
+ ArrayPool.Shared.Return(constants);
commandBuffer.BeginDebugEvent("ImGui");
- commandBuffer.BeginRenderPass(frameBuffer, clearValue, resourceTableBindings.Values);
+ commandBuffer.BeginRenderPass([colorAttachment], null);
commandBuffer.SetPipeline(graphicsPipeline);
commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0);
@@ -456,7 +435,7 @@ public void Render(CommandBuffer commandBuffer, FrameBuffer frameBuffer, ClearVa
}
commandBuffer.SetScissors([scissor]);
- commandBuffer.SetResourceTable(resourceTableBindings[drawCmd.TexRef.GetTexID()]);
+ commandBuffer.SetConstantBuffer(constantBuffer, (uint)(sizeof(Constants) * resourceHandleBindings.IndexOfKey(drawCmd.TexRef.GetTexID())));
commandBuffer.DrawIndexed(drawCmd.ElemCount, 1, (uint)(drawCmd.IdxOffset + indexOffset), (int)(drawCmd.VtxOffset + vertexOffset), 0);
}
}
@@ -472,6 +451,7 @@ public void Render(CommandBuffer commandBuffer, FrameBuffer frameBuffer, ClearVa
protected override void Destroy()
{
+ constantBuffer?.Dispose();
indexBuffer?.Dispose();
vertexBuffer?.Dispose();
@@ -481,23 +461,17 @@ protected override void Destroy()
}
drawDataTextures.Clear();
- foreach (ResourceTable resourceTable in resourceTableBindings.Values)
- {
- resourceTable.Dispose();
- }
- resourceTableBindings.Clear();
+ resourceHandleBindings.Clear();
textureViewBindings.Clear();
textureBindings.Clear();
graphicsPipeline.Dispose();
- resourceLayout.Dispose();
sampler.Dispose();
- constants.Dispose();
}
private void RemoveDestroyedBindings()
{
- ImTextureID[] destroyedTextureIDs = [.. resourceTableBindings.Keys.Where(textureID =>
+ ImTextureID[] destroyedTextureIDs = [.. resourceHandleBindings.Keys.Where(textureID =>
{
if (textureBindings.FirstOrDefault(kv => kv.Value == textureID).Key is Texture texture)
{
@@ -514,10 +488,7 @@ private void RemoveDestroyedBindings()
foreach (ImTextureID textureID in destroyedTextureIDs)
{
- if (resourceTableBindings.Remove(textureID, out ResourceTable? resourceTable))
- {
- resourceTable.Dispose();
- }
+ resourceHandleBindings.Remove(textureID);
if (textureViewBindings.FirstOrDefault(kv => kv.Value == textureID).Key is TextureView textureView)
{
@@ -532,9 +503,15 @@ private void RemoveDestroyedBindings()
}
}
-[StructLayout(LayoutKind.Explicit, Size = 64)]
+[StructLayout(LayoutKind.Explicit, Size = 256)]
file struct Constants
{
[FieldOffset(0)]
public Matrix4x4 Projection;
+
+ [FieldOffset(64)]
+ public ResourceHandle Texture;
+
+ [FieldOffset(72)]
+ public ResourceHandle Sampler;
}
diff --git a/sources/Extensions/Zenith.NET.Extensions.ImageSharp/Extensions.cs b/sources/Extensions/Zenith.NET.Extensions.ImageSharp/Extensions.cs
index bfb4c7fe..7ae2ef48 100644
--- a/sources/Extensions/Zenith.NET.Extensions.ImageSharp/Extensions.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.ImageSharp/Extensions.cs
@@ -14,39 +14,75 @@ public Texture LoadTextureFromStream(Stream stream, bool generateMipMaps = true)
uint mipLevels = generateMipMaps ? ZenithHelper.MipLevels((uint)image.Width, (uint)image.Height, 1) : 1;
- Texture texture = context.CreateTexture(new()
- {
- Type = TextureType.Texture2D,
- Format = PixelFormat.R8G8B8A8UNorm,
- Width = (uint)image.Width,
- Height = (uint)image.Height,
- Depth = 1,
- MipLevels = mipLevels,
- ArrayLayers = 1,
- SampleCount = SampleCount.Count1,
- Flags = TextureUsageFlags.ShaderResource
- });
+ Texture texture = context.CreateTexture(TextureDesc.Texture2D(PixelFormat.R8G8B8A8UNorm,
+ (uint)image.Width,
+ (uint)image.Height,
+ mipLevels,
+ SampleCount.Count1));
Rgba32[] pixels = new Rgba32[image.Width * image.Height];
image.CopyPixelDataTo(pixels);
- CommandBuffer commandBuffer = context.Copy.CommandBuffer();
-
- commandBuffer.Upload(texture, default, default, new() { Width = (uint)image.Width, Height = (uint)image.Height, Depth = 1 }, pixels);
+ CommandBuffer commandBuffer = context.GraphicsQueue.CommandBuffer();
- for (uint i = 1; i < mipLevels; i++)
+ unsafe
{
- ZenithHelper.MipDimensions((uint)image.Width, (uint)image.Height, 1, i, out uint mipWidth, out uint mipHeight, out _);
+ fixed (Rgba32* pPixels = pixels)
+ {
+ Extent3D extent = new()
+ {
+ Width = (uint)image.Width,
+ Height = (uint)image.Height,
+ Depth = 1
+ };
+
+ TextureData data = new()
+ {
+ Pointer = (nint)pPixels,
+ SizeInBytes = (uint)(sizeof(Rgba32) * pixels.Length),
+ RowStrideInBytes = ZenithHelper.RowStrideInBytes(PixelFormat.R8G8B8A8UNorm, extent.Width, extent.Height),
+ SliceStrideInBytes = ZenithHelper.SliceStrideInBytes(PixelFormat.R8G8B8A8UNorm, extent.Width, extent.Height)
+ };
+
+ commandBuffer.Transition(texture, default, TextureLayout.Undefined, TextureLayout.CopyDst);
+ commandBuffer.Upload(texture, default, default, extent, data);
+ commandBuffer.Transition(texture, default, TextureLayout.CopyDst, TextureLayout.Sampled);
+ }
+
+ for (uint i = 1; i < mipLevels; i++)
+ {
+ ZenithHelper.MipDimensions((uint)image.Width, (uint)image.Height, 1, i, out uint mipWidth, out uint mipHeight, out _);
+
+ using Image mipImage = image.Clone(ctx => ctx.Resize((int)mipWidth, (int)mipHeight, KnownResamplers.MitchellNetravali));
+
+ pixels = new Rgba32[mipWidth * mipHeight];
+ mipImage.CopyPixelDataTo(pixels);
- using Image mipImage = image.Clone(ctx => ctx.Resize((int)mipWidth, (int)mipHeight, KnownResamplers.MitchellNetravali));
+ fixed (Rgba32* pPixels = pixels)
+ {
+ Extent3D extent = new()
+ {
+ Width = mipWidth,
+ Height = mipHeight,
+ Depth = 1
+ };
- pixels = new Rgba32[mipWidth * mipHeight];
- mipImage.CopyPixelDataTo(pixels);
+ TextureData data = new()
+ {
+ Pointer = (nint)pPixels,
+ SizeInBytes = (uint)(sizeof(Rgba32) * pixels.Length),
+ RowStrideInBytes = ZenithHelper.RowStrideInBytes(PixelFormat.R8G8B8A8UNorm, extent.Width, extent.Height),
+ SliceStrideInBytes = ZenithHelper.SliceStrideInBytes(PixelFormat.R8G8B8A8UNorm, extent.Width, extent.Height)
+ };
- commandBuffer.Upload(texture, new() { MipLevel = i }, default, new() { Width = mipWidth, Height = mipHeight, Depth = 1 }, pixels);
+ commandBuffer.Transition(texture, new() { MipLevel = i }, TextureLayout.Undefined, TextureLayout.CopyDst);
+ commandBuffer.Upload(texture, new() { MipLevel = i }, default, extent, data);
+ commandBuffer.Transition(texture, new() { MipLevel = i }, TextureLayout.CopyDst, TextureLayout.Sampled);
+ }
+ }
}
- commandBuffer.Submit(true);
+ commandBuffer.Submit().Wait();
return texture;
}
diff --git a/sources/Extensions/Zenith.NET.Extensions.ImageSharp/Zenith.NET.Extensions.ImageSharp.csproj b/sources/Extensions/Zenith.NET.Extensions.ImageSharp/Zenith.NET.Extensions.ImageSharp.csproj
index 9b04ef4c..4bf295f1 100644
--- a/sources/Extensions/Zenith.NET.Extensions.ImageSharp/Zenith.NET.Extensions.ImageSharp.csproj
+++ b/sources/Extensions/Zenith.NET.Extensions.ImageSharp/Zenith.NET.Extensions.ImageSharp.csproj
@@ -2,6 +2,7 @@
$(StandardTargetFramework)
+ $(SIXLABORS_LICENSE_KEY)
diff --git a/sources/Extensions/Zenith.NET.Extensions.Slang/Extensions.cs b/sources/Extensions/Zenith.NET.Extensions.Slang/Extensions.cs
deleted file mode 100644
index 87749a66..00000000
--- a/sources/Extensions/Zenith.NET.Extensions.Slang/Extensions.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using Slangc.NET;
-
-namespace Zenith.NET.Extensions.Slang;
-
-public static class Extensions
-{
- extension(GraphicsContext context)
- {
- public Shader LoadShaderFromFile(string file, string entryPoint, ShaderStageFlags stage, string[]? searchPaths = null)
- {
- List arguments =
- [
- file,
- "-entry", entryPoint,
- "-stage", stage.ToString().ToLowerInvariant(),
- "-matrix-layout-row-major"
- ];
-
- if (searchPaths is not null)
- {
- foreach (string path in searchPaths)
- {
- arguments.AddRange(["-I", path]);
- }
- }
-
- arguments.Add("-target");
-
- switch (context.Backend)
- {
- case Backend.DirectX12:
- arguments.AddRange(["dxil", "-profile", "sm_6_6"]);
- break;
-
- case Backend.Metal:
- arguments.AddRange(["metallib", "-capability", "metallib_latest"]);
- break;
-
- case Backend.Vulkan:
- arguments.AddRange(["spirv", "-capability", "spirv_latest", "-fvk-use-entrypoint-name"]);
- break;
- }
-
- return context.CreateShader(new() { ShaderBytes = SlangCompiler.Compile([.. arguments]), EntryPoint = entryPoint, Stage = stage });
- }
-
- public Shader LoadShaderFromSource(string source, string entryPoint, ShaderStageFlags stage, string[]? searchPaths = null)
- {
- string file = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.slang");
-
- File.WriteAllText(file, source);
-
- try
- {
- return context.LoadShaderFromFile(file, entryPoint, stage, searchPaths);
- }
- finally
- {
- File.Delete(file);
- }
- }
- }
-}
diff --git a/sources/Extensions/Zenith.NET.Extensions.Slang/Zenith.NET.Extensions.Slang.csproj b/sources/Extensions/Zenith.NET.Extensions.Slang/Zenith.NET.Extensions.Slang.csproj
deleted file mode 100644
index f7e52318..00000000
--- a/sources/Extensions/Zenith.NET.Extensions.Slang/Zenith.NET.Extensions.Slang.csproj
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
- $(StandardTargetFramework)
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/sources/NuGet.Packaging.props b/sources/NuGet.Packaging.props
index 116220c4..f80d76e1 100644
--- a/sources/NuGet.Packaging.props
+++ b/sources/NuGet.Packaging.props
@@ -7,10 +7,10 @@
$(MSBuildThisFileDirectory)..\.nuget
- 0.0.8
+ 1.0.0-rc
qian-o
Copyright (c) 2026 qian-o
- Zenith.NET is a modern, cross-platform graphics and compute library for .NET. It provides a unified GPU programming interface supporting DirectX12, Metal, and Vulkan backends.
+ Zenith.NET is a modern rendering hardware interface for .NET with one consistent C# API for graphics and compute across DirectX 12, Metal 4, and Vulkan 1.4.
Git
diff --git a/sources/Views/Zenith.NET.Views.Avalonia/Surface.cs b/sources/Views/Zenith.NET.Views.Avalonia/Surface.cs
index 78cb390a..e693f9b6 100644
--- a/sources/Views/Zenith.NET.Views.Avalonia/Surface.cs
+++ b/sources/Views/Zenith.NET.Views.Avalonia/Surface.cs
@@ -1,121 +1,67 @@
-using System.Runtime.CompilerServices;
-using Avalonia.Media.Imaging;
+using Avalonia.Media.Imaging;
using Avalonia.Platform;
using AvaloniaPixelFormat = Avalonia.Platform.PixelFormat;
namespace Zenith.NET.Views.Avalonia;
-internal unsafe class Surface : DisposableObject
+internal class Surface(GraphicsContext graphicsContext, uint width, uint height) : DisposableObject
{
- private readonly Texture color;
- private readonly Texture depthStencil;
- private readonly Buffer pixels;
-
- public Surface(GraphicsContext graphicsContext, uint width, uint height)
+ public Texture Drawable { get; } = graphicsContext.CreateTexture(new()
{
- color = graphicsContext.CreateTexture(new()
- {
- Type = TextureType.Texture2D,
- Format = ZenithViewHelper.ColorFormat,
- Width = width,
- Height = height,
- Depth = 1,
- MipLevels = 1,
- ArrayLayers = 1,
- SampleCount = SampleCount.Count1,
- Flags = TextureUsageFlags.RenderTarget
- });
-
- depthStencil = graphicsContext.CreateTexture(new()
- {
- Type = TextureType.Texture2D,
- Format = ZenithViewHelper.DepthStencilFormat,
- Width = width,
- Height = height,
- Depth = 1,
- MipLevels = 1,
- ArrayLayers = 1,
- SampleCount = SampleCount.Count1,
- Flags = TextureUsageFlags.DepthStencil
- });
-
- pixels = graphicsContext.CreateBuffer(new()
- {
- SizeInBytes = ZenithHelper.Align(width * 4, GraphicsContext.TextureRowPitchAlignment) * height,
- StrideInBytes = 4,
- Flags = BufferUsageFlags.MapRead
- });
-
- FrameBuffer = graphicsContext.CreateFrameBuffer(new()
- {
- ColorAttachments = [new() { Target = color }],
- DepthStencilAttachment = new() { Target = depthStencil }
- });
-
- WriteableBitmap = new(new((int)width, (int)height), new(96, 96), ColorFormat(), AlphaFormat.Premul);
-
- GraphicsContext = graphicsContext;
- Width = width;
- Height = height;
- }
-
- public FrameBuffer FrameBuffer { get; }
-
- public WriteableBitmap WriteableBitmap { get; }
+ Type = TextureType.Texture2D,
+ Format = ZenithViewHelper.DrawableFormat,
+ Width = width,
+ Height = height,
+ Depth = 1,
+ MipLevels = 1,
+ ArrayLayers = 1,
+ SampleCount = SampleCount.Count1,
+ Usages = TextureUsages.ColorAttachment | TextureUsages.TransferSrc | TextureUsages.TransferDst
+ });
- public GraphicsContext GraphicsContext { get; }
+ public WriteableBitmap Bitmap { get; } = new(new((int)width, (int)height), new(96, 96), DrawableFormat(), AlphaFormat.Premul);
- public uint Width { get; }
+ public uint Width { get; } = width;
- public uint Height { get; }
+ public uint Height { get; } = height;
- public void Present()
+ public void Flush(CommandBuffer commandBuffer)
{
- CommandBuffer commandBuffer = GraphicsContext.Copy.CommandBuffer();
- commandBuffer.CopyTextureToBuffer(color, default, default, new() { Width = Width, Height = Height, Depth = 1 }, pixels, 0);
- commandBuffer.Submit(true);
-
- uint rowPitchInBytes = ZenithHelper.Align(Width * 4, GraphicsContext.TextureRowPitchAlignment);
-
- using ILockedFramebuffer lockedFramebuffer = WriteableBitmap.Lock();
+ using ILockedFramebuffer lockedFramebuffer = Bitmap.Lock();
- MappedMemory mappedMemory = pixels.Map();
-
- if (lockedFramebuffer.RowBytes == rowPitchInBytes)
+ Extent3D extent = new()
{
- Unsafe.CopyBlock((void*)lockedFramebuffer.Address, (void*)mappedMemory.Pointer, mappedMemory.SizeInBytes);
- }
- else
+ Width = Width,
+ Height = Height,
+ Depth = 1
+ };
+
+ TextureData data = new()
{
- Parallel.For(0, Height, y =>
- {
- byte* srcPtr = (byte*)mappedMemory.Pointer + (rowPitchInBytes * y);
- byte* dstPtr = (byte*)lockedFramebuffer.Address + (lockedFramebuffer.RowBytes * y);
+ Pointer = lockedFramebuffer.Address,
+ SizeInBytes = (uint)(lockedFramebuffer.RowBytes * Height),
+ RowStrideInBytes = (uint)lockedFramebuffer.RowBytes,
+ SliceStrideInBytes = (uint)(lockedFramebuffer.RowBytes * Height)
+ };
- Unsafe.CopyBlock(dstPtr, srcPtr, (uint)lockedFramebuffer.RowBytes);
- });
- }
+ commandBuffer.Download(Drawable, default, default, extent, data);
- pixels.Unmap();
+ commandBuffer.Submit().Wait();
}
protected override void Destroy()
{
- WriteableBitmap.Dispose();
- FrameBuffer.Dispose();
-
- pixels.Dispose();
- depthStencil.Dispose();
- color.Dispose();
+ Bitmap.Dispose();
+ Drawable.Dispose();
}
- private static AvaloniaPixelFormat ColorFormat()
+ private static AvaloniaPixelFormat DrawableFormat()
{
- return ZenithViewHelper.ColorFormat switch
+ return ZenithViewHelper.DrawableFormat switch
{
PixelFormat.R8G8B8A8UNorm => AvaloniaPixelFormat.Rgba8888,
PixelFormat.B8G8R8A8UNorm => AvaloniaPixelFormat.Bgra8888,
- _ => throw new NotSupportedException($"Pixel format {ZenithViewHelper.ColorFormat} is not supported.")
+ _ => default
};
}
}
diff --git a/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs b/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs
index c8a9b38b..6557c768 100644
--- a/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs
@@ -37,7 +37,7 @@ public override void Render(DrawingContext context)
{
if (surface is not null)
{
- context.DrawImage(surface.WriteableBitmap, new(0, 0, Bounds.Width, Bounds.Height));
+ context.DrawImage(surface.Bitmap, new(0, 0, Bounds.Width, Bounds.Height));
}
if (Design.IsDesignMode)
@@ -55,7 +55,7 @@ public override void Render(DrawingContext context)
Typeface typeface = new(FontFamily, FontStyle, FontWeight, FontStretch);
double fontSize = Math.Clamp(Bounds.Height / 15.0, 14.0, 48.0);
- double dpi = VisualRoot?.RenderScaling ?? 1.0;
+ double dpi = TopLevel.GetTopLevel(this)?.RenderScaling ?? 1.0;
FormattedText shadowText = new("ZenithView",
CultureInfo.CurrentCulture,
@@ -71,8 +71,8 @@ public override void Render(DrawingContext context)
fontSize * dpi,
new SolidColorBrush(Colors.White) { Opacity = 0.98 });
- float x = (float)(Bounds.Width - mainText.Width) / 2;
- float y = (float)(Bounds.Height - mainText.Height) / 2;
+ double x = (Bounds.Width - mainText.Width) / 2.0;
+ double y = (Bounds.Height - mainText.Height) / 2.0;
context.DrawText(shadowText, new(x + 1.0, y + 1.0));
context.DrawText(mainText, new(x, y));
@@ -81,7 +81,7 @@ public override void Render(DrawingContext context)
void IZenithView.UI(Action action)
{
- Dispatcher.UIThread.Invoke(action);
+ Dispatcher.Invoke(action);
}
void IZenithView.EnsureResources()
@@ -104,19 +104,25 @@ void IZenithView.EnsureResources()
void IZenithView.Tick()
{
- if (surface is null)
+ if (GraphicsContext is null || surface is null)
{
return;
}
+ CommandBuffer commandBuffer = GraphicsContext.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(surface.Drawable, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+
UpdateRequested?.Invoke(this, new(scheduler.UpdateSeconds, scheduler.TotalSeconds));
- RenderRequested?.Invoke(this, new(scheduler.RenderSeconds, scheduler.TotalSeconds, surface.FrameBuffer));
+ RenderRequested?.Invoke(this, new(scheduler.RenderSeconds, scheduler.TotalSeconds, commandBuffer, surface.Drawable));
+
+ commandBuffer.Transition(surface.Drawable, default, TextureLayout.ColorAttachment, TextureLayout.CopySrc);
+
+ surface.Flush(commandBuffer);
}
void IZenithView.Present()
{
- surface?.Present();
-
InvalidateVisual();
}
diff --git a/sources/Views/Zenith.NET.Views.Maui/Platforms/Android/MauiZenithView.cs b/sources/Views/Zenith.NET.Views.Maui/Platforms/Android/MauiZenithView.cs
index f42077e5..8dd4bcf5 100644
--- a/sources/Views/Zenith.NET.Views.Maui/Platforms/Android/MauiZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.Maui/Platforms/Android/MauiZenithView.cs
@@ -26,8 +26,7 @@ public void EnsureResources()
swapChain = handler.VirtualView.GraphicsContext.CreateSwapChain(new()
{
Surface = Surface.Android(ANativeWindowFromSurface(JniEnvironment.EnvironmentPointer, Holder!.Surface!.Handle), width, height),
- ColorTargetFormat = ZenithViewHelper.ColorFormat,
- DepthStencilTargetFormat = ZenithViewHelper.DepthStencilFormat
+ Format = ZenithViewHelper.DrawableFormat
});
}
else if (swapChain.Desc.Surface.Width != width || swapChain.Desc.Surface.Height != height)
@@ -38,13 +37,21 @@ public void EnsureResources()
public void Tick()
{
- if (!ValidateSurface() || swapChain is null)
+ if (!ValidateSurface() || handler.VirtualView.GraphicsContext is null || swapChain is null)
{
return;
}
+ CommandBuffer commandBuffer = handler.VirtualView.GraphicsContext.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+
handler.VirtualView.OnUpdateRequested();
- handler.VirtualView.OnRenderRequested(swapChain.FrameBuffer);
+ handler.VirtualView.OnRenderRequested(commandBuffer, swapChain.Drawable);
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.ColorAttachment, TextureLayout.Present);
+
+ commandBuffer.Submit().Wait();
}
public void Present()
diff --git a/sources/Views/Zenith.NET.Views.Maui/Platforms/MacCatalyst/MauiZenithView.cs b/sources/Views/Zenith.NET.Views.Maui/Platforms/MacCatalyst/MauiZenithView.cs
index 76267c13..35d0d1c2 100644
--- a/sources/Views/Zenith.NET.Views.Maui/Platforms/MacCatalyst/MauiZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.Maui/Platforms/MacCatalyst/MauiZenithView.cs
@@ -27,8 +27,7 @@ public void EnsureResources()
swapChain = handler.VirtualView.GraphicsContext.CreateSwapChain(new()
{
Surface = Surface.Apple(Layer.Handle, width, height),
- ColorTargetFormat = ZenithViewHelper.ColorFormat,
- DepthStencilTargetFormat = ZenithViewHelper.DepthStencilFormat
+ Format = ZenithViewHelper.DrawableFormat
});
}
else if (swapChain.Desc.Surface.Width != width || swapChain.Desc.Surface.Height != height)
@@ -39,13 +38,21 @@ public void EnsureResources()
public void Tick()
{
- if (swapChain is null)
+ if (handler.VirtualView.GraphicsContext is null || swapChain is null)
{
return;
}
+ CommandBuffer commandBuffer = handler.VirtualView.GraphicsContext.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+
handler.VirtualView.OnUpdateRequested();
- handler.VirtualView.OnRenderRequested(swapChain.FrameBuffer);
+ handler.VirtualView.OnRenderRequested(commandBuffer, swapChain.Drawable);
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.ColorAttachment, TextureLayout.Present);
+
+ commandBuffer.Submit().Wait();
}
public void Present()
diff --git a/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/D3D.cs b/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/D3D.cs
index 9be621f6..a81baf70 100644
--- a/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/D3D.cs
+++ b/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/D3D.cs
@@ -7,29 +7,29 @@ namespace Zenith.NET.Views.Maui.Platforms.Windows;
internal static unsafe class D3D
{
- public static ComPtr Factory;
+ public static ComPtr Factory = new();
- public static ComPtr Device;
+ public static ComPtr Device = new();
- public static ComPtr DeviceContext;
+ public static ComPtr DeviceContext = new();
static D3D()
{
DXGI = DXGI.GetApi(null);
D3D11 = D3D11.GetApi(null);
- Success(DXGI.CreateDXGIFactory2(0, out Factory));
+ Success(DXGI.CreateDXGIFactory2(0, SilkMarshal.GuidPtrOf(), (void**)Factory.GetAddressOf()));
- Success(D3D11.CreateDevice(default(ComPtr),
+ Success(D3D11.CreateDevice(default,
D3DDriverType.Hardware,
0,
(uint)CreateDeviceFlag.BgraSupport,
- null,
+ default,
0,
D3D11.SdkVersion,
- ref Device,
- null,
- ref DeviceContext));
+ Device.GetAddressOf(),
+ default,
+ DeviceContext.GetAddressOf()));
}
public static DXGI DXGI { get; }
diff --git a/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/D3DTexture.cs b/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/D3DTexture.cs
deleted file mode 100644
index 9e17d24a..00000000
--- a/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/D3DTexture.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-using Silk.NET.Core.Native;
-using Silk.NET.Direct3D11;
-using Silk.NET.DXGI;
-
-namespace Zenith.NET.Views.Maui.Platforms.Windows;
-
-internal unsafe partial class D3DTexture : DisposableObject
-{
- [LibraryImport("kernel32")]
- private static partial int CloseHandle(nint hObject);
-
- public ComPtr SwapChain;
-
- public ComPtr Texture;
-
- public ComPtr Mutex;
-
- public nint Handle;
-
- public nint SharedHandle;
-
- private ulong key;
-
- public D3DTexture(uint width, uint height)
- {
- SwapChainDesc1 swapChainDesc = new()
- {
- Width = width,
- Height = height,
- Format = ColorFormat(),
- SampleDesc = new() { Count = 1, Quality = 0 },
- BufferUsage = DXGI.UsageRenderTargetOutput,
- BufferCount = 3,
- Scaling = Scaling.Stretch,
- SwapEffect = SwapEffect.FlipSequential
- };
-
- D3D.Success(D3D.Factory.CreateSwapChainForComposition((IUnknown*)D3D.Device.Handle, &swapChainDesc, (IDXGIOutput*)null, SwapChain.GetAddressOf()));
-
- Texture2DDesc texture2DDesc = new()
- {
- Width = width,
- Height = height,
- MipLevels = 1,
- ArraySize = 1,
- Format = ColorFormat(),
- SampleDesc = new() { Count = 1, Quality = 0 },
- BindFlags = (uint)BindFlag.RenderTarget,
- MiscFlags = (uint)(ResourceMiscFlag.SharedNthandle | ResourceMiscFlag.SharedKeyedmutex)
- };
-
- D3D.Success(D3D.Device.CreateTexture2D(&texture2DDesc, null, ref Texture));
-
- D3D.Success(Texture.QueryInterface(out Mutex));
-
- using ComPtr resource = Texture.QueryInterface();
-
- void* sharedHandle = null;
- D3D.Success(resource.CreateSharedHandle((SecurityAttributes*)null, DXGI.SharedResourceRead | DXGI.SharedResourceWrite, (char*)null, &sharedHandle));
-
- Handle = (nint)Texture.Handle;
- SharedHandle = (nint)sharedHandle;
-
- Width = width;
- Height = height;
- }
-
- public uint Width { get; }
-
- public uint Height { get; }
-
- public void AcquireSync()
- {
- D3D.Success(Mutex.AcquireSync(key++, uint.MaxValue));
- }
-
- public void ReleaseSync()
- {
- D3D.Success(Mutex.ReleaseSync(key));
- }
-
- public void Present()
- {
- D3D.Success(SwapChain.GetBuffer(0, out ComPtr backBuffer));
-
- AcquireSync();
-
- D3D.DeviceContext.CopyResource((ID3D11Resource*)backBuffer.Handle, (ID3D11Resource*)Texture.Handle);
- D3D.DeviceContext.Flush();
-
- ReleaseSync();
-
- backBuffer.Dispose();
-
- D3D.Success(SwapChain.Present(1, 0));
- }
-
- protected override void Destroy()
- {
- if (CloseHandle(SharedHandle) is 0)
- {
- Debug.WriteLine("Failed to close shared handle.");
- }
-
- Mutex.Dispose();
- Texture.Dispose();
- SwapChain.Dispose();
- }
-
- private static Format ColorFormat()
- {
- return ZenithViewHelper.ColorFormat switch
- {
- PixelFormat.R8G8B8A8UNorm => Format.FormatR8G8B8A8Unorm,
- PixelFormat.B8G8R8A8UNorm => Format.FormatB8G8R8A8Unorm,
- _ => throw new NotSupportedException($"Pixel format {ZenithViewHelper.ColorFormat} is not supported.")
- };
- }
-}
\ No newline at end of file
diff --git a/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/MauiZenithView.cs b/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/MauiZenithView.cs
index 5242cf0e..8bc71bdd 100644
--- a/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/MauiZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/MauiZenithView.cs
@@ -5,8 +5,7 @@ namespace Zenith.NET.Views.Maui.Platforms.Windows;
internal unsafe partial class MauiZenithView(ZenithViewHandler handler) : SwapChainPanel
{
- private D3DTexture? texture;
- private SwapChain? swapChain;
+ private Surface? surface;
public void EnsureResources()
{
@@ -18,50 +17,47 @@ public void EnsureResources()
uint width = Math.Clamp((uint)Math.Ceiling(ActualWidth), 1, uint.MaxValue);
uint height = Math.Clamp((uint)Math.Ceiling(ActualHeight), 1, uint.MaxValue);
- if (texture is null || texture.Width != width || texture.Height != height || swapChain is null)
+ if (surface is null || surface.Width != width || surface.Height != height)
{
ReleaseResources();
- texture = new(width, height);
+ surface = new(handler.VirtualView.GraphicsContext, width, height);
- swapChain = handler.VirtualView.GraphicsContext.CreateSwapChain(new()
- {
- Surface = Surface.D3D11Interop(texture.SharedHandle, width, height),
- ColorTargetFormat = ZenithViewHelper.ColorFormat,
- DepthStencilTargetFormat = ZenithViewHelper.DepthStencilFormat
- });
-
- this.As().SetSwapChain(texture.SwapChain);
+ this.As().SetSwapChain(surface.SwapChain);
}
}
public void Tick()
{
- if (texture is null || swapChain is null)
+ if (handler.VirtualView.GraphicsContext is null || surface is null)
{
return;
}
- texture.AcquireSync();
+ surface.AcquireSync();
+
+ CommandBuffer commandBuffer = handler.VirtualView.GraphicsContext.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(surface.Drawable, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
handler.VirtualView.OnUpdateRequested();
- handler.VirtualView.OnRenderRequested(swapChain.FrameBuffer);
+ handler.VirtualView.OnRenderRequested(commandBuffer, surface.Drawable);
- texture.ReleaseSync();
+ commandBuffer.Transition(surface.Drawable, default, TextureLayout.ColorAttachment, TextureLayout.Common);
+
+ commandBuffer.Submit().Wait();
+
+ surface.ReleaseSync();
}
public void Present()
{
- swapChain?.Present();
- texture?.Present();
+ surface?.Present();
}
public void ReleaseResources()
{
- swapChain?.Dispose();
- swapChain = null;
-
- texture?.Dispose();
- texture = null;
+ surface?.Dispose();
+ surface = null;
}
}
diff --git a/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs b/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs
new file mode 100644
index 00000000..52163433
--- /dev/null
+++ b/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs
@@ -0,0 +1,130 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Silk.NET.Core.Native;
+using Silk.NET.Direct3D11;
+using Silk.NET.DXGI;
+
+namespace Zenith.NET.Views.Maui.Platforms.Windows;
+
+internal unsafe partial class Surface : DisposableObject
+{
+ [LibraryImport("kernel32")]
+ private static partial int CloseHandle(nint hObject);
+
+ public ComPtr SwapChain = new();
+
+ public ComPtr Texture = new();
+
+ public ComPtr Mutex = new();
+
+ public nint SharedHandle;
+
+ private ulong key;
+
+ public Surface(GraphicsContext graphicsContext, uint width, uint height)
+ {
+ SwapChainDesc1 swapChainDesc = new()
+ {
+ Width = width,
+ Height = height,
+ Format = DrawableFormat(),
+ SampleDesc = new() { Count = 1 },
+ BufferUsage = DXGI.UsageRenderTargetOutput,
+ BufferCount = 3,
+ Scaling = Scaling.Stretch,
+ SwapEffect = SwapEffect.FlipSequential
+ };
+
+ D3D.Success(D3D.Factory.CreateSwapChainForComposition((IUnknown*)D3D.Device.Handle, &swapChainDesc, default(IDXGIOutput*), SwapChain.GetAddressOf()));
+
+ Texture2DDesc texture2DDesc = new()
+ {
+ Width = width,
+ Height = height,
+ MipLevels = 1,
+ ArraySize = 1,
+ Format = DrawableFormat(),
+ SampleDesc = new() { Count = 1 },
+ BindFlags = (uint)BindFlag.RenderTarget,
+ MiscFlags = (uint)(ResourceMiscFlag.SharedKeyedmutex | ResourceMiscFlag.SharedNthandle)
+ };
+
+ D3D.Success(D3D.Device.CreateTexture2D(&texture2DDesc, default(SubresourceData*), Texture.GetAddressOf()));
+
+ D3D.Success(Texture.QueryInterface(SilkMarshal.GuidPtrOf(), (void**)Mutex.GetAddressOf()));
+
+ using ComPtr resource = new();
+ D3D.Success(Texture.QueryInterface(SilkMarshal.GuidPtrOf(), (void**)resource.GetAddressOf()));
+
+ void* sharedHandle = null;
+ D3D.Success(resource.CreateSharedHandle(default(SecurityAttributes*), DXGI.SharedResourceRead | DXGI.SharedResourceWrite, default(char*), &sharedHandle));
+
+ Drawable = graphicsContext.CreateTexture(new()
+ {
+ Type = TextureType.Texture2D,
+ Format = ZenithViewHelper.DrawableFormat,
+ Width = Width = width,
+ Height = Height = height,
+ Depth = 1,
+ MipLevels = 1,
+ ArrayLayers = 1,
+ SampleCount = SampleCount.Count1,
+ Usages = TextureUsages.ColorAttachment | TextureUsages.TransferDst
+ }, NativeTextureType.D3D11TextureNtHandle, SharedHandle = (nint)sharedHandle);
+ }
+
+ public uint Width { get; }
+
+ public uint Height { get; }
+
+ public Texture Drawable { get; }
+
+ public void AcquireSync()
+ {
+ D3D.Success(Mutex.AcquireSync(key++, uint.MaxValue));
+ }
+
+ public void ReleaseSync()
+ {
+ D3D.Success(Mutex.ReleaseSync(key));
+ }
+
+ public void Present()
+ {
+ using ComPtr backBuffer = new();
+ D3D.Success(SwapChain.GetBuffer(0, SilkMarshal.GuidPtrOf(), (void**)backBuffer.GetAddressOf()));
+
+ AcquireSync();
+
+ D3D.DeviceContext.CopyResource((ID3D11Resource*)backBuffer.Handle, (ID3D11Resource*)Texture.Handle);
+ D3D.DeviceContext.Flush();
+
+ ReleaseSync();
+
+ D3D.Success(SwapChain.Present(1, 0));
+ }
+
+ protected override void Destroy()
+ {
+ Drawable.Dispose();
+
+ if (CloseHandle(SharedHandle) is 0)
+ {
+ Debug.WriteLine("Failed to close shared handle.");
+ }
+
+ Mutex.Dispose();
+ Texture.Dispose();
+ SwapChain.Dispose();
+ }
+
+ private static Format DrawableFormat()
+ {
+ return ZenithViewHelper.DrawableFormat switch
+ {
+ PixelFormat.R8G8B8A8UNorm => Format.FormatR8G8B8A8Unorm,
+ PixelFormat.B8G8R8A8UNorm => Format.FormatB8G8R8A8Unorm,
+ _ => default
+ };
+ }
+}
\ No newline at end of file
diff --git a/sources/Views/Zenith.NET.Views.Maui/Platforms/iOS/MauiZenithView.cs b/sources/Views/Zenith.NET.Views.Maui/Platforms/iOS/MauiZenithView.cs
index 770ce036..3a5bddc1 100644
--- a/sources/Views/Zenith.NET.Views.Maui/Platforms/iOS/MauiZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.Maui/Platforms/iOS/MauiZenithView.cs
@@ -27,8 +27,7 @@ public void EnsureResources()
swapChain = handler.VirtualView.GraphicsContext.CreateSwapChain(new()
{
Surface = Surface.Apple(Layer.Handle, width, height),
- ColorTargetFormat = ZenithViewHelper.ColorFormat,
- DepthStencilTargetFormat = ZenithViewHelper.DepthStencilFormat
+ Format = ZenithViewHelper.DrawableFormat
});
}
else if (swapChain.Desc.Surface.Width != width || swapChain.Desc.Surface.Height != height)
@@ -39,13 +38,21 @@ public void EnsureResources()
public void Tick()
{
- if (swapChain is null)
+ if (handler.VirtualView.GraphicsContext is null || swapChain is null)
{
return;
}
+ CommandBuffer commandBuffer = handler.VirtualView.GraphicsContext.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+
handler.VirtualView.OnUpdateRequested();
- handler.VirtualView.OnRenderRequested(swapChain.FrameBuffer);
+ handler.VirtualView.OnRenderRequested(commandBuffer, swapChain.Drawable);
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.ColorAttachment, TextureLayout.Present);
+
+ commandBuffer.Submit().Wait();
}
public void Present()
diff --git a/sources/Views/Zenith.NET.Views.Maui/ZenithView.cs b/sources/Views/Zenith.NET.Views.Maui/ZenithView.cs
index 0c554666..cff11353 100644
--- a/sources/Views/Zenith.NET.Views.Maui/ZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.Maui/ZenithView.cs
@@ -1,61 +1,4 @@
-using Microsoft.Maui.Handlers;
-#if ANDROID
-using Zenith.NET.Views.Maui.Platforms.Android;
-#elif IOS
-using Zenith.NET.Views.Maui.Platforms.iOS;
-#elif MACCATALYST
-using Zenith.NET.Views.Maui.Platforms.MacCatalyst;
-#elif WINDOWS
-using Zenith.NET.Views.Maui.Platforms.Windows;
-#endif
-
-namespace Zenith.NET.Views.Maui;
-
-internal class ZenithViewHandler() : ViewHandler(mapper, commandMapper)
-{
- private static readonly PropertyMapper mapper = new(ViewMapper)
- {
- [nameof(ZenithView.Background)] = MapBackground
- };
-
- private static readonly CommandMapper commandMapper = new(ViewCommandMapper)
- {
- [nameof(IZenithView.EnsureResources)] = MapEnsureResources,
- [nameof(IZenithView.Tick)] = MapTick,
- [nameof(IZenithView.Present)] = MapPresent,
- [nameof(IZenithView.ReleaseResources)] = MapReleaseResources
- };
-
- protected override MauiZenithView CreatePlatformView()
- {
- return new(this);
- }
-
- private static void MapBackground(ZenithViewHandler handler, ZenithView view)
- {
- // ZenithView does not support Background property.
- }
-
- private static void MapEnsureResources(ZenithViewHandler handler, ZenithView view, object? arg3)
- {
- handler.PlatformView.EnsureResources();
- }
-
- private static void MapTick(ZenithViewHandler handler, ZenithView view, object? arg3)
- {
- handler.PlatformView.Tick();
- }
-
- private static void MapPresent(ZenithViewHandler handler, ZenithView view, object? arg3)
- {
- handler.PlatformView.Present();
- }
-
- private static void MapReleaseResources(ZenithViewHandler handler, ZenithView view, object? arg3)
- {
- handler.PlatformView.ReleaseResources();
- }
-}
+namespace Zenith.NET.Views.Maui;
public partial class ZenithView : View, IZenithView
{
@@ -86,9 +29,9 @@ internal void OnUpdateRequested()
UpdateRequested?.Invoke(this, new(scheduler.UpdateSeconds, scheduler.TotalSeconds));
}
- internal void OnRenderRequested(FrameBuffer frameBuffer)
+ internal void OnRenderRequested(CommandBuffer commandBuffer, Texture drawable)
{
- RenderRequested?.Invoke(this, new(scheduler.RenderSeconds, scheduler.TotalSeconds, frameBuffer));
+ RenderRequested?.Invoke(this, new(scheduler.RenderSeconds, scheduler.TotalSeconds, commandBuffer, drawable));
}
void IZenithView.UI(Action action)
diff --git a/sources/Views/Zenith.NET.Views.Maui/ZenithViewHandler.cs b/sources/Views/Zenith.NET.Views.Maui/ZenithViewHandler.cs
new file mode 100644
index 00000000..2aafa944
--- /dev/null
+++ b/sources/Views/Zenith.NET.Views.Maui/ZenithViewHandler.cs
@@ -0,0 +1,58 @@
+using Microsoft.Maui.Handlers;
+#if ANDROID
+using Zenith.NET.Views.Maui.Platforms.Android;
+#elif IOS
+using Zenith.NET.Views.Maui.Platforms.iOS;
+#elif MACCATALYST
+using Zenith.NET.Views.Maui.Platforms.MacCatalyst;
+#elif WINDOWS
+using Zenith.NET.Views.Maui.Platforms.Windows;
+#endif
+
+namespace Zenith.NET.Views.Maui;
+
+internal class ZenithViewHandler() : ViewHandler(mapper, commandMapper)
+{
+ private static readonly PropertyMapper mapper = new(ViewMapper)
+ {
+ [nameof(ZenithView.Background)] = MapBackground
+ };
+
+ private static readonly CommandMapper commandMapper = new(ViewCommandMapper)
+ {
+ [nameof(IZenithView.EnsureResources)] = MapEnsureResources,
+ [nameof(IZenithView.Tick)] = MapTick,
+ [nameof(IZenithView.Present)] = MapPresent,
+ [nameof(IZenithView.ReleaseResources)] = MapReleaseResources
+ };
+
+ protected override MauiZenithView CreatePlatformView()
+ {
+ return new(this);
+ }
+
+ private static void MapBackground(ZenithViewHandler handler, ZenithView view)
+ {
+ // ZenithView does not support Background property.
+ }
+
+ private static void MapEnsureResources(ZenithViewHandler handler, ZenithView view, object? arg3)
+ {
+ handler.PlatformView.EnsureResources();
+ }
+
+ private static void MapTick(ZenithViewHandler handler, ZenithView view, object? arg3)
+ {
+ handler.PlatformView.Tick();
+ }
+
+ private static void MapPresent(ZenithViewHandler handler, ZenithView view, object? arg3)
+ {
+ handler.PlatformView.Present();
+ }
+
+ private static void MapReleaseResources(ZenithViewHandler handler, ZenithView view, object? arg3)
+ {
+ handler.PlatformView.ReleaseResources();
+ }
+}
\ No newline at end of file
diff --git a/sources/Views/Zenith.NET.Views.WPF/D3D.cs b/sources/Views/Zenith.NET.Views.WPF/D3D.cs
index cf7bf6f5..4c8eaa64 100644
--- a/sources/Views/Zenith.NET.Views.WPF/D3D.cs
+++ b/sources/Views/Zenith.NET.Views.WPF/D3D.cs
@@ -2,7 +2,6 @@
using Silk.NET.Core.Native;
using Silk.NET.Direct3D11;
using Silk.NET.Direct3D9;
-using Silk.NET.DXGI;
using Format = Silk.NET.Direct3D9.Format;
using PresentParameters = Silk.NET.Direct3D9.PresentParameters;
@@ -10,20 +9,20 @@ namespace Zenith.NET.Views.WPF;
internal static unsafe class D3D
{
- public static ComPtr D3D9Ex;
+ public static ComPtr D3D9Ex = new();
- public static ComPtr D3D9DeviceEx;
+ public static ComPtr D3D9DeviceEx = new();
- public static ComPtr D3D11Device;
+ public static ComPtr D3D11Device = new();
- public static ComPtr D3D11DeviceContext;
+ public static ComPtr D3D11DeviceContext = new();
static D3D()
{
D3D9 = D3D9.GetApi(null);
D3D11 = D3D11.GetApi(null);
- Success(D3D9.Direct3DCreate9Ex(D3D9.SdkVersion, ref D3D9Ex));
+ Success(D3D9.Direct3DCreate9Ex(D3D9.SdkVersion, D3D9Ex.GetAddressOf()));
PresentParameters present = new()
{
@@ -40,19 +39,19 @@ static D3D()
0,
D3D9.CreateHardwareVertexprocessing | D3D9.CreateMultithreaded | D3D9.CreatePuredevice | D3D9.CreateFpuPreserve,
&present,
- (Displaymodeex*)null,
- ref D3D9DeviceEx));
+ default(Displaymodeex*),
+ D3D9DeviceEx.GetAddressOf()));
- Success(D3D11.CreateDevice(default(ComPtr),
+ Success(D3D11.CreateDevice(default,
D3DDriverType.Hardware,
0,
(uint)CreateDeviceFlag.BgraSupport,
- null,
+ default,
0,
D3D11.SdkVersion,
- ref D3D11Device,
- null,
- ref D3D11DeviceContext));
+ D3D11Device.GetAddressOf(),
+ default,
+ D3D11DeviceContext.GetAddressOf()));
}
public static D3D9 D3D9 { get; }
diff --git a/sources/Views/Zenith.NET.Views.WPF/D3DTexture.cs b/sources/Views/Zenith.NET.Views.WPF/D3DTexture.cs
deleted file mode 100644
index 03615e07..00000000
--- a/sources/Views/Zenith.NET.Views.WPF/D3DTexture.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-using System.Windows.Interop;
-using Silk.NET.Core.Native;
-using Silk.NET.Direct3D11;
-using Silk.NET.Direct3D9;
-using Silk.NET.DXGI;
-using D3D9Format = Silk.NET.Direct3D9.Format;
-using DXGIFormat = Silk.NET.DXGI.Format;
-
-namespace Zenith.NET.Views.WPF;
-
-internal unsafe partial class D3DTexture : DisposableObject
-{
- [LibraryImport("kernel32")]
- private static partial int CloseHandle(nint hObject);
-
- public ComPtr D3D9RenderTarget;
-
- public ComPtr D3D9RenderSurface;
-
- public ComPtr D3D9SharedTexture;
-
- public ComPtr D3D11RenderTarget;
-
- public ComPtr D3D11Mutex;
-
- public nint SharedHandle;
-
- private ulong key;
-
- public D3DTexture(uint width, uint height, D3DImage image)
- {
- void* sharedHandle = null;
- D3D.Success(D3D.D3D9DeviceEx.CreateTexture(width,
- height,
- 1,
- D3D9.UsageRendertarget,
- D3D9Format.A8R8G8B8,
- Pool.Default,
- ref D3D9RenderTarget,
- &sharedHandle));
-
- D3D.Success(D3D9RenderTarget.GetSurfaceLevel(0, ref D3D9RenderSurface));
- D3D.Success(D3D.D3D11Device.OpenSharedResource(sharedHandle, out D3D9SharedTexture));
-
- Texture2DDesc desc = new()
- {
- Width = width,
- Height = height,
- MipLevels = 1,
- ArraySize = 1,
- Format = ColorFormat(),
- SampleDesc = new() { Count = 1, Quality = 0 },
- BindFlags = (uint)BindFlag.RenderTarget,
- MiscFlags = (uint)(ResourceMiscFlag.SharedNthandle | ResourceMiscFlag.SharedKeyedmutex)
- };
-
- D3D.Success(D3D.D3D11Device.CreateTexture2D(&desc, null, ref D3D11RenderTarget));
-
- D3D.Success(D3D11RenderTarget.QueryInterface(out D3D11Mutex));
-
- using ComPtr resource = D3D11RenderTarget.QueryInterface();
-
- sharedHandle = null;
- D3D.Success(resource.CreateSharedHandle((SecurityAttributes*)null, DXGI.SharedResourceRead | DXGI.SharedResourceWrite, (char*)null, &sharedHandle));
-
- SharedHandle = (nint)sharedHandle;
-
- Width = width;
- Height = height;
- Image = image;
- }
-
- public uint Width { get; }
-
- public uint Height { get; }
-
- public D3DImage Image { get; }
-
- public void AcquireSync()
- {
- D3D.Success(D3D11Mutex.AcquireSync(key++, uint.MaxValue));
- }
-
- public void ReleaseSync()
- {
- D3D.Success(D3D11Mutex.ReleaseSync(key));
- }
-
- public void Present()
- {
- Image.Lock();
- Image.SetBackBuffer(D3DResourceType.IDirect3DSurface9, (nint)D3D9RenderSurface.Handle);
-
- AcquireSync();
-
- D3D.D3D11DeviceContext.CopyResource((ID3D11Resource*)D3D9SharedTexture.Handle, (ID3D11Resource*)D3D11RenderTarget.Handle);
- D3D.D3D11DeviceContext.Flush();
-
- ReleaseSync();
-
- Image.AddDirtyRect(new(0, 0, (int)Width, (int)Height));
- Image.Unlock();
- }
-
- protected override void Destroy()
- {
- if (CloseHandle(SharedHandle) is 0)
- {
- Debug.WriteLine("Failed to close shared handle.");
- }
-
- D3D11Mutex.Dispose();
- D3D11RenderTarget.Dispose();
- D3D9SharedTexture.Dispose();
- D3D9RenderSurface.Dispose();
- D3D9RenderTarget.Dispose();
- }
-
- private static DXGIFormat ColorFormat()
- {
- return ZenithViewHelper.ColorFormat switch
- {
- PixelFormat.R8G8B8A8UNorm => DXGIFormat.FormatR8G8B8A8Unorm,
- PixelFormat.B8G8R8A8UNorm => DXGIFormat.FormatB8G8R8A8Unorm,
- _ => throw new NotSupportedException($"Pixel format {ZenithViewHelper.ColorFormat} is not supported.")
- };
- }
-}
\ No newline at end of file
diff --git a/sources/Views/Zenith.NET.Views.WPF/Surface.cs b/sources/Views/Zenith.NET.Views.WPF/Surface.cs
new file mode 100644
index 00000000..332cd3c7
--- /dev/null
+++ b/sources/Views/Zenith.NET.Views.WPF/Surface.cs
@@ -0,0 +1,140 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Windows.Interop;
+using Silk.NET.Core.Native;
+using Silk.NET.Direct3D11;
+using Silk.NET.Direct3D9;
+using Silk.NET.DXGI;
+using D3D9Format = Silk.NET.Direct3D9.Format;
+using DXGIFormat = Silk.NET.DXGI.Format;
+
+namespace Zenith.NET.Views.WPF;
+
+internal unsafe partial class Surface : DisposableObject
+{
+ [LibraryImport("kernel32")]
+ private static partial int CloseHandle(nint hObject);
+
+ public ComPtr D3D9RenderTarget = new();
+
+ public ComPtr D3D9RenderSurface = new();
+
+ public ComPtr D3D9SharedTexture = new();
+
+ public ComPtr D3D11RenderTarget = new();
+
+ public ComPtr D3D11Mutex = new();
+
+ public nint SharedHandle;
+
+ private ulong key;
+
+ public Surface(GraphicsContext graphicsContext, uint width, uint height)
+ {
+ void* sharedHandle = null;
+ D3D.Success(D3D.D3D9DeviceEx.CreateTexture(width,
+ height,
+ 1,
+ D3D9.UsageRendertarget,
+ D3D9Format.A8R8G8B8,
+ Pool.Default,
+ D3D9RenderTarget.GetAddressOf(),
+ &sharedHandle));
+
+ D3D.Success(D3D9RenderTarget.GetSurfaceLevel(0, D3D9RenderSurface.GetAddressOf()));
+ D3D.Success(D3D.D3D11Device.OpenSharedResource(sharedHandle, SilkMarshal.GuidPtrOf(), (void**)D3D9SharedTexture.GetAddressOf()));
+
+ Texture2DDesc desc = new()
+ {
+ Width = width,
+ Height = height,
+ MipLevels = 1,
+ ArraySize = 1,
+ Format = DrawableFormat(),
+ SampleDesc = new() { Count = 1 },
+ BindFlags = (uint)BindFlag.RenderTarget,
+ MiscFlags = (uint)(ResourceMiscFlag.SharedKeyedmutex | ResourceMiscFlag.SharedNthandle)
+ };
+
+ D3D.Success(D3D.D3D11Device.CreateTexture2D(&desc, default(SubresourceData*), D3D11RenderTarget.GetAddressOf()));
+
+ D3D.Success(D3D11RenderTarget.QueryInterface(SilkMarshal.GuidPtrOf(), (void**)D3D11Mutex.GetAddressOf()));
+
+ using ComPtr resource = new();
+ D3D.Success(D3D11RenderTarget.QueryInterface(SilkMarshal.GuidPtrOf(), (void**)resource.GetAddressOf()));
+
+ sharedHandle = null;
+ D3D.Success(resource.CreateSharedHandle(default(SecurityAttributes*), DXGI.SharedResourceRead | DXGI.SharedResourceWrite, default(char*), &sharedHandle));
+
+ Drawable = graphicsContext.CreateTexture(new()
+ {
+ Type = TextureType.Texture2D,
+ Format = ZenithViewHelper.DrawableFormat,
+ Width = Width = width,
+ Height = Height = height,
+ Depth = 1,
+ MipLevels = 1,
+ ArrayLayers = 1,
+ SampleCount = SampleCount.Count1,
+ Usages = TextureUsages.ColorAttachment | TextureUsages.TransferDst
+ }, NativeTextureType.D3D11TextureNtHandle, SharedHandle = (nint)sharedHandle);
+ }
+
+ public uint Width { get; }
+
+ public uint Height { get; }
+
+ public Texture Drawable { get; }
+
+ public void AcquireSync()
+ {
+ D3D.Success(D3D11Mutex.AcquireSync(key++, uint.MaxValue));
+ }
+
+ public void ReleaseSync()
+ {
+ D3D.Success(D3D11Mutex.ReleaseSync(key));
+ }
+
+ public void Present(D3DImage image)
+ {
+ image.Lock();
+ image.SetBackBuffer(D3DResourceType.IDirect3DSurface9, (nint)D3D9RenderSurface.Handle);
+
+ AcquireSync();
+
+ D3D.D3D11DeviceContext.CopyResource((ID3D11Resource*)D3D9SharedTexture.Handle, (ID3D11Resource*)D3D11RenderTarget.Handle);
+ D3D.D3D11DeviceContext.Flush();
+
+ ReleaseSync();
+
+ image.AddDirtyRect(new(0, 0, (int)Width, (int)Height));
+ image.Unlock();
+ }
+
+ protected override void Destroy()
+ {
+ Drawable.Dispose();
+
+ if (CloseHandle(SharedHandle) is 0)
+ {
+ Debug.WriteLine("Failed to close shared handle.");
+ }
+
+ D3D11Mutex.Dispose();
+ D3D11RenderTarget.Dispose();
+ D3D9SharedTexture.Dispose();
+ D3D9RenderSurface.Dispose();
+ D3D9RenderTarget.Dispose();
+ }
+
+ private static DXGIFormat DrawableFormat()
+ {
+ return ZenithViewHelper.DrawableFormat switch
+ {
+ PixelFormat.R8G8B8A8UNorm => DXGIFormat.FormatR8G8B8A8Unorm,
+ PixelFormat.B8G8R8A8UNorm => DXGIFormat.FormatB8G8R8A8Unorm,
+ _ => default
+ };
+ }
+}
\ No newline at end of file
diff --git a/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs b/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs
index 29b914e4..c0d80027 100644
--- a/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs
@@ -17,8 +17,7 @@ public class ZenithView : Control, IZenithView
private readonly D3DImage image;
private readonly FrameScheduler scheduler;
- private D3DTexture? texture;
- private SwapChain? swapChain;
+ private Surface? surface;
public ZenithView()
{
@@ -79,8 +78,8 @@ protected override void OnRender(DrawingContext drawingContext)
new SolidColorBrush(Colors.White) { Opacity = 0.98 },
dpi);
- float x = (float)(ActualWidth - mainText.Width) / 2;
- float y = (float)(ActualHeight - mainText.Height) / 2;
+ double x = (ActualWidth - mainText.Width) / 2.0;
+ double y = (ActualHeight - mainText.Height) / 2.0;
drawingContext.DrawText(shadowText, new(x + 1.0, y + 1.0));
drawingContext.DrawText(mainText, new(x, y));
@@ -102,50 +101,47 @@ void IZenithView.EnsureResources()
uint width = Math.Clamp((uint)Math.Ceiling(ActualWidth), 1, uint.MaxValue);
uint height = Math.Clamp((uint)Math.Ceiling(ActualHeight), 1, uint.MaxValue);
- if (texture is null || texture.Width != width || texture.Height != height || swapChain is null)
+ if (surface is null || surface.Width != width || surface.Height != height)
{
((IZenithView)this).ReleaseResources();
- texture = new(width, height, image);
-
- swapChain = GraphicsContext.CreateSwapChain(new()
- {
- Surface = Surface.D3D11Interop(texture.SharedHandle, width, height),
- ColorTargetFormat = ZenithViewHelper.ColorFormat,
- DepthStencilTargetFormat = ZenithViewHelper.DepthStencilFormat
- });
+ surface = new(GraphicsContext, width, height);
}
}
void IZenithView.Tick()
{
- if (texture is null || swapChain is null)
+ if (GraphicsContext is null || surface is null)
{
return;
}
- texture.AcquireSync();
+ surface.AcquireSync();
+
+ CommandBuffer commandBuffer = GraphicsContext.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(surface.Drawable, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
UpdateRequested?.Invoke(this, new(scheduler.UpdateSeconds, scheduler.TotalSeconds));
- RenderRequested?.Invoke(this, new(scheduler.RenderSeconds, scheduler.TotalSeconds, swapChain.FrameBuffer));
+ RenderRequested?.Invoke(this, new(scheduler.RenderSeconds, scheduler.TotalSeconds, commandBuffer, surface.Drawable));
- texture.ReleaseSync();
+ commandBuffer.Transition(surface.Drawable, default, TextureLayout.ColorAttachment, TextureLayout.Common);
+
+ commandBuffer.Submit().Wait();
+
+ surface.ReleaseSync();
}
void IZenithView.Present()
{
- swapChain?.Present();
- texture?.Present();
+ surface?.Present(image);
InvalidateVisual();
}
void IZenithView.ReleaseResources()
{
- swapChain?.Dispose();
- swapChain = null;
-
- texture?.Dispose();
- texture = null;
+ surface?.Dispose();
+ surface = null;
}
}
diff --git a/sources/Views/Zenith.NET.Views.WinForms/ZenithView.cs b/sources/Views/Zenith.NET.Views.WinForms/ZenithView.cs
index c92671d4..97d6a1e3 100644
--- a/sources/Views/Zenith.NET.Views.WinForms/ZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.WinForms/ZenithView.cs
@@ -81,8 +81,7 @@ void IZenithView.EnsureResources()
swapChain = GraphicsContext.CreateSwapChain(new()
{
Surface = Surface.Win32(Handle, width, height),
- ColorTargetFormat = ZenithViewHelper.ColorFormat,
- DepthStencilTargetFormat = ZenithViewHelper.DepthStencilFormat
+ Format = ZenithViewHelper.DrawableFormat
});
}
else if (swapChain.Desc.Surface.Width != width || swapChain.Desc.Surface.Height != height)
@@ -93,13 +92,21 @@ void IZenithView.EnsureResources()
void IZenithView.Tick()
{
- if (swapChain is null)
+ if (GraphicsContext is null || swapChain is null)
{
return;
}
+ CommandBuffer commandBuffer = GraphicsContext.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+
UpdateRequested?.Invoke(this, new(scheduler.UpdateSeconds, scheduler.TotalSeconds));
- RenderRequested?.Invoke(this, new(scheduler.RenderSeconds, scheduler.TotalSeconds, swapChain.FrameBuffer));
+ RenderRequested?.Invoke(this, new(scheduler.RenderSeconds, scheduler.TotalSeconds, commandBuffer, swapChain.Drawable));
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.ColorAttachment, TextureLayout.Present);
+
+ commandBuffer.Submit().Wait();
}
void IZenithView.Present()
diff --git a/sources/Views/Zenith.NET.Views.WinUI/ZenithView.Uno.cs b/sources/Views/Zenith.NET.Views.WinUI/ZenithView.Uno.cs
index 5706b434..a380ff20 100644
--- a/sources/Views/Zenith.NET.Views.WinUI/ZenithView.Uno.cs
+++ b/sources/Views/Zenith.NET.Views.WinUI/ZenithView.Uno.cs
@@ -23,19 +23,27 @@ void IZenithView.EnsureResources()
{
((IZenithView)this).ReleaseResources();
- Background = new ImageBrush() { ImageSource = (surface = new(GraphicsContext, width, height)).WriteableBitmap };
+ Background = new ImageBrush() { ImageSource = (surface = new(GraphicsContext, width, height)).Bitmap };
}
}
void IZenithView.Tick()
{
- if (surface is null)
+ if (GraphicsContext is null || surface is null)
{
return;
}
+ CommandBuffer commandBuffer = GraphicsContext.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(surface.Drawable, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+
UpdateRequested?.Invoke(this, new(scheduler.UpdateSeconds, scheduler.TotalSeconds));
- RenderRequested?.Invoke(this, new(scheduler.RenderSeconds, scheduler.TotalSeconds, surface.FrameBuffer));
+ RenderRequested?.Invoke(this, new(scheduler.RenderSeconds, scheduler.TotalSeconds, commandBuffer, surface.Drawable));
+
+ commandBuffer.Transition(surface.Drawable, default, TextureLayout.ColorAttachment, TextureLayout.CopySrc);
+
+ surface.Flush(commandBuffer);
}
void IZenithView.Present()
@@ -50,128 +58,74 @@ void IZenithView.ReleaseResources()
}
}
-internal unsafe class Surface : DisposableObject
+internal unsafe class Surface(GraphicsContext context, uint width, uint height) : DisposableObject
{
- private readonly Texture color;
- private readonly Texture depthStencil;
- private readonly Buffer pixels;
+ private readonly byte[] pixels = new byte[width * height * 4];
- public Surface(GraphicsContext context, uint width, uint height)
+ public Texture Drawable { get; } = context.CreateTexture(new()
{
- color = context.CreateTexture(new()
- {
- Type = TextureType.Texture2D,
- Format = ZenithViewHelper.ColorFormat,
- Width = width,
- Height = height,
- Depth = 1,
- MipLevels = 1,
- ArrayLayers = 1,
- SampleCount = SampleCount.Count1,
- Flags = TextureUsageFlags.RenderTarget
- });
-
- depthStencil = context.CreateTexture(new()
- {
- Type = TextureType.Texture2D,
- Format = ZenithViewHelper.DepthStencilFormat,
- Width = width,
- Height = height,
- Depth = 1,
- MipLevels = 1,
- ArrayLayers = 1,
- SampleCount = SampleCount.Count1,
- Flags = TextureUsageFlags.DepthStencil
- });
-
- pixels = context.CreateBuffer(new()
- {
- SizeInBytes = ZenithHelper.Align(width * 4, GraphicsContext.TextureRowPitchAlignment) * height,
- StrideInBytes = 4,
- Flags = BufferUsageFlags.MapRead
- });
-
- FrameBuffer = context.CreateFrameBuffer(new()
- {
- ColorAttachments = [new() { Target = color }],
- DepthStencilAttachment = new() { Target = depthStencil }
- });
-
- WriteableBitmap = new((int)width, (int)height);
-
- Context = context;
- Width = width;
- Height = height;
- }
-
- public FrameBuffer FrameBuffer { get; }
-
- public WriteableBitmap WriteableBitmap { get; }
+ Type = TextureType.Texture2D,
+ Format = ZenithViewHelper.DrawableFormat,
+ Width = width,
+ Height = height,
+ Depth = 1,
+ MipLevels = 1,
+ ArrayLayers = 1,
+ SampleCount = SampleCount.Count1,
+ Usages = TextureUsages.ColorAttachment | TextureUsages.TransferSrc | TextureUsages.TransferDst
+ });
- public GraphicsContext Context { get; }
+ public WriteableBitmap Bitmap { get; } = new((int)width, (int)height);
- public uint Width { get; }
+ public uint Width { get; } = width;
- public uint Height { get; }
+ public uint Height { get; } = height;
- public void Present()
+ public void Flush(CommandBuffer commandBuffer)
{
- CommandBuffer commandBuffer = Context.Graphics.CommandBuffer();
- commandBuffer.CopyTextureToBuffer(color, default, default, new() { Width = Width, Height = Height, Depth = 1 }, pixels, 0);
- commandBuffer.Submit(true);
+ fixed (byte* pPixels = pixels)
+ {
+ Extent3D extent = new()
+ {
+ Width = Width,
+ Height = Height,
+ Depth = 1
+ };
- uint rowPitchInBytes = ZenithHelper.Align(Width * 4, GraphicsContext.TextureRowPitchAlignment);
+ TextureData data = new()
+ {
+ Pointer = (nint)pPixels,
+ SizeInBytes = (uint)pixels.Length,
+ RowStrideInBytes = Width * 4,
+ SliceStrideInBytes = (uint)pixels.Length
+ };
- using (Stream stream = WriteableBitmap.PixelBuffer.AsStream())
- {
- MappedMemory mappedMemory = pixels.Map();
+ commandBuffer.Download(Drawable, default, default, extent, data);
- byte* pointer = (byte*)mappedMemory.Pointer;
+ commandBuffer.Submit().Wait();
+ }
- switch (ZenithViewHelper.ColorFormat)
+ if (ZenithViewHelper.DrawableFormat is PixelFormat.R8G8B8A8UNorm)
+ {
+ for (int i = 0; i < pixels.Length; i += 4)
{
- case PixelFormat.R8G8B8A8UNorm:
- for (uint y = 0; y < Height; y++)
- {
- for (uint x = 0; x < Width; x++)
- {
- stream.WriteByte(pointer[(x * 4) + 2]);
- stream.WriteByte(pointer[(x * 4) + 1]);
- stream.WriteByte(pointer[(x * 4) + 0]);
- stream.WriteByte(pointer[(x * 4) + 3]);
- }
-
- pointer += rowPitchInBytes;
- }
- break;
-
- case PixelFormat.B8G8R8A8UNorm:
- for (uint y = 0; y < Height; y++)
- {
- stream.Write([.. new ReadOnlySpan(pointer, (int)(Width * 4))]);
-
- pointer += rowPitchInBytes;
- }
- break;
-
- default:
- throw new NotSupportedException($"Pixel format {ZenithViewHelper.ColorFormat} is not supported.");
+ (pixels[i], pixels[i + 2]) = (pixels[i + 2], pixels[i]);
}
-
- pixels.Unmap();
}
- WriteableBitmap.Invalidate();
+ using Stream stream = Bitmap.PixelBuffer.AsStream();
+ stream.Write(pixels);
}
- protected override void Destroy()
+ public void Present()
{
- WriteableBitmap.Dispose();
- FrameBuffer.Dispose();
+ Bitmap.Invalidate();
+ }
- pixels.Dispose();
- depthStencil.Dispose();
- color.Dispose();
+ protected override void Destroy()
+ {
+ Bitmap.Dispose();
+ Drawable.Dispose();
}
}
#endif
\ No newline at end of file
diff --git a/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs b/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
index 043804dc..875ee498 100644
--- a/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
+++ b/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
@@ -11,8 +11,7 @@ namespace Zenith.NET.Views.WinUI;
public unsafe partial class ZenithView
{
- private D3DTexture? texture;
- private SwapChain? swapChain;
+ private Surface? surface;
void IZenithView.EnsureResources()
{
@@ -24,79 +23,76 @@ void IZenithView.EnsureResources()
uint width = Math.Clamp((uint)Math.Ceiling(ActualWidth), 1, uint.MaxValue);
uint height = Math.Clamp((uint)Math.Ceiling(ActualHeight), 1, uint.MaxValue);
- if (texture is null || texture.Width != width || texture.Height != height || swapChain is null)
+ if (surface is null || surface.Width != width || surface.Height != height)
{
((IZenithView)this).ReleaseResources();
- texture = new(width, height);
+ surface = new(GraphicsContext, width, height);
- swapChain = GraphicsContext.CreateSwapChain(new()
- {
- Surface = Surface.D3D11Interop(texture.SharedHandle, width, height),
- ColorTargetFormat = ZenithViewHelper.ColorFormat,
- DepthStencilTargetFormat = ZenithViewHelper.DepthStencilFormat
- });
-
- this.As().SetSwapChain(texture.SwapChain);
+ this.As().SetSwapChain(surface.SwapChain);
}
}
void IZenithView.Tick()
{
- if (texture is null || swapChain is null)
+ if (GraphicsContext is null || surface is null)
{
return;
}
- texture.AcquireSync();
+ surface.AcquireSync();
+
+ CommandBuffer commandBuffer = GraphicsContext.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(surface.Drawable, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
UpdateRequested?.Invoke(this, new(scheduler.UpdateSeconds, scheduler.TotalSeconds));
- RenderRequested?.Invoke(this, new(scheduler.RenderSeconds, scheduler.TotalSeconds, swapChain.FrameBuffer));
+ RenderRequested?.Invoke(this, new(scheduler.RenderSeconds, scheduler.TotalSeconds, commandBuffer, surface.Drawable));
+
+ commandBuffer.Transition(surface.Drawable, default, TextureLayout.ColorAttachment, TextureLayout.Common);
- texture.ReleaseSync();
+ commandBuffer.Submit().Wait();
+
+ surface.ReleaseSync();
}
void IZenithView.Present()
{
- swapChain?.Present();
- texture?.Present();
+ surface?.Present();
}
void IZenithView.ReleaseResources()
{
- swapChain?.Dispose();
- swapChain = null;
-
- texture?.Dispose();
- texture = null;
+ surface?.Dispose();
+ surface = null;
}
}
internal static unsafe class D3D
{
- public static ComPtr Factory;
+ public static ComPtr Factory = new();
- public static ComPtr Device;
+ public static ComPtr Device = new();
- public static ComPtr DeviceContext;
+ public static ComPtr DeviceContext = new();
static D3D()
{
DXGI = DXGI.GetApi(null);
D3D11 = D3D11.GetApi(null);
- Success(DXGI.CreateDXGIFactory2(0, out Factory));
+ Success(DXGI.CreateDXGIFactory2(0, SilkMarshal.GuidPtrOf(), (void**)Factory.GetAddressOf()));
- Success(D3D11.CreateDevice(default(ComPtr),
+ Success(D3D11.CreateDevice(default,
D3DDriverType.Hardware,
0,
(uint)CreateDeviceFlag.BgraSupport,
- null,
+ default,
0,
D3D11.SdkVersion,
- ref Device,
- null,
- ref DeviceContext));
+ Device.GetAddressOf(),
+ default,
+ DeviceContext.GetAddressOf()));
}
public static DXGI DXGI { get; }
@@ -112,38 +108,36 @@ public static void Success(int result)
}
}
-internal unsafe partial class D3DTexture : DisposableObject
+internal unsafe partial class Surface : DisposableObject
{
[LibraryImport("kernel32")]
private static partial int CloseHandle(nint hObject);
- public ComPtr SwapChain;
-
- public ComPtr Texture;
+ public ComPtr SwapChain = new();
- public ComPtr Mutex;
+ public ComPtr Texture = new();
- public nint Handle;
+ public ComPtr Mutex = new();
public nint SharedHandle;
private ulong key;
- public D3DTexture(uint width, uint height)
+ public Surface(GraphicsContext graphicsContext, uint width, uint height)
{
SwapChainDesc1 swapChainDesc = new()
{
Width = width,
Height = height,
- Format = ColorFormat(),
- SampleDesc = new() { Count = 1, Quality = 0 },
+ Format = DrawableFormat(),
+ SampleDesc = new() { Count = 1 },
BufferUsage = DXGI.UsageRenderTargetOutput,
BufferCount = 3,
Scaling = Scaling.Stretch,
SwapEffect = SwapEffect.FlipSequential
};
- D3D.Success(D3D.Factory.CreateSwapChainForComposition((IUnknown*)D3D.Device.Handle, &swapChainDesc, (IDXGIOutput*)null, SwapChain.GetAddressOf()));
+ D3D.Success(D3D.Factory.CreateSwapChainForComposition((IUnknown*)D3D.Device.Handle, &swapChainDesc, default(IDXGIOutput*), SwapChain.GetAddressOf()));
Texture2DDesc texture2DDesc = new()
{
@@ -151,32 +145,42 @@ public D3DTexture(uint width, uint height)
Height = height,
MipLevels = 1,
ArraySize = 1,
- Format = ColorFormat(),
- SampleDesc = new() { Count = 1, Quality = 0 },
+ Format = DrawableFormat(),
+ SampleDesc = new() { Count = 1 },
BindFlags = (uint)BindFlag.RenderTarget,
- MiscFlags = (uint)(ResourceMiscFlag.SharedNthandle | ResourceMiscFlag.SharedKeyedmutex)
+ MiscFlags = (uint)(ResourceMiscFlag.SharedKeyedmutex | ResourceMiscFlag.SharedNthandle)
};
- D3D.Success(D3D.Device.CreateTexture2D(&texture2DDesc, null, ref Texture));
+ D3D.Success(D3D.Device.CreateTexture2D(&texture2DDesc, default(SubresourceData*), Texture.GetAddressOf()));
- D3D.Success(Texture.QueryInterface(out Mutex));
+ D3D.Success(Texture.QueryInterface(SilkMarshal.GuidPtrOf(), (void**)Mutex.GetAddressOf()));
- using ComPtr