diff --git a/Zenith.NET.slnx b/Zenith.NET.slnx
index 3e34441f..b277f91f 100644
--- a/Zenith.NET.slnx
+++ b/Zenith.NET.slnx
@@ -3,6 +3,7 @@
+
diff --git a/sources/Directory.Packages.props b/sources/Directory.Packages.props
index 44203098..27fc5529 100644
--- a/sources/Directory.Packages.props
+++ b/sources/Directory.Packages.props
@@ -5,7 +5,7 @@
-
+
@@ -19,9 +19,9 @@
-
-
-
+
+
+
\ No newline at end of file
diff --git a/sources/Experiments/CornellBox/App.cs b/sources/Experiments/CornellBox/App.cs
index 2097c79d..bbebc9c7 100644
--- a/sources/Experiments/CornellBox/App.cs
+++ b/sources/Experiments/CornellBox/App.cs
@@ -49,9 +49,9 @@ static App()
window = Window.Create(WindowOptions.Default with
{
+ Size = new(1280, 720),
API = GraphicsAPI.None,
- Title = "Cornell Box - Zenith.NET",
- Size = new(1280, 720)
+ Title = "Cornell Box - Zenith.NET"
});
window.Initialize();
window.Center();
@@ -86,8 +86,8 @@ static App()
camera = new(input, Matrix4x4.CreateTranslation(278.0f, 273.0f, -800.0f))
{
- Speed = 240.0f,
- FarPlane = 2000.0f
+ FarPlane = 2000.0f,
+ Speed = 240.0f
};
rasterizer = new();
diff --git a/sources/Experiments/FluidTank/App.cs b/sources/Experiments/FluidTank/App.cs
index 876c53fd..36eb12ba 100644
--- a/sources/Experiments/FluidTank/App.cs
+++ b/sources/Experiments/FluidTank/App.cs
@@ -44,9 +44,9 @@ static App()
window = Window.Create(WindowOptions.Default with
{
+ Size = new(1280, 720),
API = GraphicsAPI.None,
- Title = "Fluid Tank - Zenith.NET",
- Size = new(1280, 720)
+ Title = "Fluid Tank - Zenith.NET"
});
window.Initialize();
window.Center();
@@ -86,10 +86,10 @@ static App()
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
+ Fov = 48.0f,
+ Speed = 4.0f
};
renderer = new();
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang b/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
index be78fb65..9fd415d7 100644
--- a/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
+++ b/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
@@ -184,9 +184,8 @@ float3 ReconstructNormal(float2 uv, float centerDepth)
return normal;
}
-float3 SceneAt(float2 uv)
+float3 SceneAt(float2 uv, float depth)
{
- float depth = composite.SceneDepth.SampleLevel(composite.Sampler, uv, 0.0).r;
if (depth <= 0.0)
{
float3 rayView = ViewRay(uv);
@@ -198,22 +197,62 @@ float3 SceneAt(float2 uv)
return composite.SceneColor.SampleLevel(composite.Sampler, uv, 0.0).rgb;
}
+float3 SceneAt(float2 uv)
+{
+ float depth = composite.SceneDepth.SampleLevel(composite.Sampler, uv, 0.0).r;
+
+ return SceneAt(uv, depth);
+}
+
+bool FrameOccludesFluid(FullscreenOutput input, float sceneDepth)
+{
+ if (composite.SceneColor.Load(int3(int2(input.Position.xy), 0)).a < 0.5)
+ {
+ return false;
+ }
+
+ int2 fluidDimensions = int2(composite.Width, composite.Height);
+ int2 baseFluidPixel = int2(floor(input.UV * float2(fluidDimensions) - 0.5));
+ float closestFluidDepth = 0.0;
+
+ for (int y = 0; y < 2; y++)
+ {
+ for (int x = 0; x < 2; x++)
+ {
+ int2 samplePixel = clamp(baseFluidPixel + int2(x, y), int2(0, 0), fluidDimensions - 1);
+ float sampleDepth = composite.Attributes.Load(int3(samplePixel, 0)).w;
+
+ if (sampleDepth > 0.0 && (closestFluidDepth <= 0.0 || sampleDepth < closestFluidDepth))
+ {
+ closestFluidDepth = sampleDepth;
+ }
+ }
+ }
+
+ return closestFluidDepth <= 0.0 || closestFluidDepth >= sceneDepth;
+}
+
float4 ShadeWater(FullscreenOutput input)
{
float2 uv = input.UV;
+ float sceneDepth = composite.SceneDepth.Load(int3(int2(input.Position.xy), 0)).r;
+ float3 scene = SceneAt(uv, sceneDepth);
if (composite.RenderMode == 1)
{
- return float4(ToSRGB(ACESFilm(SceneAt(uv))), 1.0);
+ return float4(ToSRGB(ACESFilm(scene)), 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(scene)), 1.0);
+ }
- return float4(ToSRGB(ACESFilm(background)), 1.0);
+ if (sceneDepth > 0.0 && (fluidDepth >= sceneDepth || FrameOccludesFluid(input, sceneDepth)))
+ {
+ return float4(ToSRGB(ACESFilm(scene)), 1.0);
}
float thickness = composite.Thickness.SampleLevel(composite.Sampler, uv, 0.0).r;
@@ -222,7 +261,7 @@ float4 ShadeWater(FullscreenOutput input)
if (surfaceConfidence <= 0.01)
{
- return float4(ToSRGB(ACESFilm(SceneAt(uv))), 1.0);
+ return float4(ToSRGB(ACESFilm(scene)), 1.0);
}
float3 normalView = ReconstructNormal(uv, fluidDepth);
@@ -258,7 +297,7 @@ float4 ShadeWater(FullscreenOutput input)
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);
+ color = lerp(scene, color, surfaceConfidence);
return float4(ToSRGB(ACESFilm(color)), 1.0);
}
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang b/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang
index f0b7b4fc..cfc68e2b 100644
--- a/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang
+++ b/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang
@@ -191,15 +191,16 @@ DepthOutput DepthFS(SurfaceVSOutput input)
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);
+ float linearDepth = -viewPosition.z;
- if (OccludedByScene(input, -viewPosition.z))
+ if (OccludedByScene(input, linearDepth))
{
discard;
}
DepthOutput output;
- output.LinearDepth = -viewPosition.z;
- output.Attributes = float4(input.Speed, input.Density, sphereZ, 1.0);
+ output.LinearDepth = linearDepth;
+ output.Attributes = float4(input.Speed, input.Density, sphereZ, linearDepth);
output.DeviceDepth = clipPosition.z / clipPosition.w;
return output;
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang b/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang
index b69b0367..91908b11 100644
--- a/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang
+++ b/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang
@@ -1,5 +1,7 @@
#include "SceneCommon.slang"
+static const uint FrameMaterialId = 4;
+
struct SceneConstants
{
float4x4 View;
@@ -115,7 +117,7 @@ FSOutput FSMain(VSOutput input)
float3 color = diffuse + specular + environment + material.Albedo * material.Emission;
FSOutput output;
- output.Color = float4(color, 1.0);
+ output.Color = float4(color, input.MaterialId == FrameMaterialId ? 1.0 : 0.0);
output.LinearDepth = input.ViewDepth;
return output;
diff --git a/sources/Experiments/FluidTank/FluidSimulation.cs b/sources/Experiments/FluidTank/FluidSimulation.cs
index 547be4fd..5198fc10 100644
--- a/sources/Experiments/FluidTank/FluidSimulation.cs
+++ b/sources/Experiments/FluidTank/FluidSimulation.cs
@@ -48,9 +48,7 @@ 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));
+ 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;
diff --git a/sources/Experiments/FluidTank/FluidTankRenderer.cs b/sources/Experiments/FluidTank/FluidTankRenderer.cs
index 068a679d..9feb59a3 100644
--- a/sources/Experiments/FluidTank/FluidTankRenderer.cs
+++ b/sources/Experiments/FluidTank/FluidTankRenderer.cs
@@ -10,6 +10,7 @@ namespace FluidTank;
internal enum FluidViewMode
{
Water,
+
Particles
}
@@ -109,7 +110,7 @@ public FluidTankRenderer()
fluidDepthPipeline = GraphicsHelper.CreateGraphicsPipeline(surfaceVertexShader, "FluidSurface.slang", "DepthFS", [], new()
{
- ColorFormats = [PixelFormat.R32Float, PixelFormat.R16G16B16A16Float],
+ ColorFormats = [PixelFormat.R32Float, PixelFormat.R32G32B32A32Float],
DepthStencilFormat = PixelFormat.D32FloatS8UInt,
SampleCount = SampleCount.Count1
}, RasterizerState.CullNone(), DepthStencilState.DepthReadWrite(), BlendState.Opaque(), PrimitiveTopology.TriangleStrip);
@@ -302,11 +303,7 @@ public void RenderScene(CommandBuffer commandBuffer)
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.BeginRenderPass([ColorAttachment.Clear(sceneColor, Vector4.Zero), 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);
@@ -339,11 +336,7 @@ public void RenderFluid(CommandBuffer commandBuffer)
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.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);
@@ -423,14 +416,12 @@ public void Resize(uint width, uint height)
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);
+ fluidAttributes = GraphicsHelper.CreateTexture(PixelFormat.R32G32B32A32Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | 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;
+ reflection = App.Context.Capabilities.RayTracingSupported ? GraphicsHelper.CreateTexture(PixelFormat.R16G16B16A16Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage) : null;
}
public void Dispose()
diff --git a/sources/Experiments/InkCanvas/App.cs b/sources/Experiments/InkCanvas/App.cs
new file mode 100644
index 00000000..7917a439
--- /dev/null
+++ b/sources/Experiments/InkCanvas/App.cs
@@ -0,0 +1,135 @@
+using System.Numerics;
+using InkCanvas.Drawing;
+using InkCanvas.Helpers;
+using Silk.NET.Input;
+using Silk.NET.Windowing;
+using Zenith.NET;
+using Zenith.NET.DirectX12;
+using Zenith.NET.Metal;
+using Zenith.NET.Vulkan;
+
+namespace InkCanvas;
+
+internal static class App
+{
+ private static readonly IWindow window;
+ private static readonly IInputContext input;
+ private static readonly SwapChain swapChain;
+ private static readonly CanvasController canvas;
+
+ 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 (_, args) =>
+ {
+ // These validation messages are caused by known issues in Skia's D3D12 backend.
+ if (args.Message.StartsWith("ID3D12DescriptorHeap::GetGPUDescriptorHandleForHeapStart:", StringComparison.Ordinal)
+ || args.Message.StartsWith("ID3D12Device::CreateSampler2:", StringComparison.Ordinal)
+ || args.Message.StartsWith("ID3D12CommandList::ClearRenderTargetView:", StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ Console.WriteLine($"[{args.Severity}] {args.Message}");
+ };
+
+ window = Window.Create(WindowOptions.Default with
+ {
+ Size = new(1280, 800),
+ API = GraphicsAPI.None,
+ Title = "Ink Canvas - Zenith.NET"
+ });
+ 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
+ {
+ surface = Surface.Xlib(window.Native!.X11!.Value.Display, (nint)window.Native.X11.Value.Window, Width, Height);
+ }
+
+ swapChain = Context.CreateSwapChain(new()
+ {
+ Surface = surface,
+ Format = PixelFormat.B8G8R8A8UNorm
+ });
+
+ canvas = new(Context, input, Width, Height);
+ }
+
+ 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.Render += _ =>
+ {
+ if (Width is 0 || Height is 0)
+ {
+ return;
+ }
+
+ CommandBuffer commandBuffer = Context.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.Undefined, TextureLayout.CopyDst);
+ canvas.Render(commandBuffer, swapChain.Drawable, DpiScale);
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.CopyDst, TextureLayout.Present);
+
+ commandBuffer.Submit().Wait();
+
+ swapChain.Present();
+ };
+
+ window.FramebufferResize += _ =>
+ {
+ if (Width is 0 || Height is 0)
+ {
+ return;
+ }
+
+ canvas.Resize(Width, Height);
+ swapChain.Resize(Width, Height);
+ };
+
+ window.Run();
+
+ canvas.Dispose();
+ swapChain.Dispose();
+ input.Dispose();
+ window.Dispose();
+
+ Context.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/InkCanvas/Drawing/Canvas.cs b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
new file mode 100644
index 00000000..01100288
--- /dev/null
+++ b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
@@ -0,0 +1,273 @@
+using SkiaSharp;
+
+namespace InkCanvas.Drawing;
+
+internal class Canvas : IDisposable
+{
+ private const float EraserRadius = 22.0f;
+
+ private static readonly SKColor Surface = new(22, 24, 30);
+ private static readonly SKColor Grid = new(32, 35, 43);
+ private static readonly SKColor Cursor = new(150, 158, 172, 200);
+
+ private readonly Toolbar toolbar = new();
+ private readonly List strokes = [];
+
+ private readonly SKPathBuilder eraserBuilder = new();
+ private readonly SKPaint fillPaint = new();
+ private readonly SKPaint strokePaint = new()
+ {
+ Style = SKPaintStyle.Stroke,
+ StrokeCap = SKStrokeCap.Round,
+ StrokeJoin = SKStrokeJoin.Round
+ };
+
+ private SKPicture? cachedPicture;
+ private int nodeCount;
+ private bool pictureDirty = true;
+
+ private Stroke? activeStroke;
+ private SKPoint pointer;
+ private bool erasing;
+ private bool erasePending;
+
+ private SKRect drawingArea;
+ private SKSize size;
+
+ public bool MSAA => toolbar.MSAA;
+
+ private bool CanClear => strokes.Count > 0 || activeStroke is not null || erasing || erasePending;
+
+ public void Draw(SKCanvas canvas, float width, float height)
+ {
+ EnsureLayout(width, height);
+
+ if (erasePending)
+ {
+ ApplyEraser();
+ }
+
+ fillPaint.Color = Surface;
+ canvas.DrawRect(0.0f, 0.0f, width, height, fillPaint);
+
+ if (pictureDirty)
+ {
+ cachedPicture?.Dispose();
+ cachedPicture = RecordCanvas();
+ pictureDirty = false;
+ }
+
+ canvas.Save();
+ canvas.ClipRect(drawingArea);
+ canvas.DrawPicture(cachedPicture);
+
+ if (activeStroke is not null)
+ {
+ strokePaint.Color = activeStroke.Color;
+ strokePaint.StrokeWidth = activeStroke.Width;
+ canvas.DrawPath(activeStroke.Path, strokePaint);
+ canvas.DrawLine(activeStroke.TailStart, activeStroke.TailEnd, strokePaint);
+ }
+
+ if (erasing && drawingArea.Contains(pointer.X, pointer.Y))
+ {
+ DrawEraserCursor(canvas);
+ }
+
+ canvas.Restore();
+
+ toolbar.Draw(canvas, strokes.Count, nodeCount, CanClear);
+ }
+
+ public void PointerMove(SKPoint position)
+ {
+ pointer = position;
+
+ if (erasing)
+ {
+ eraserBuilder.LineTo(position);
+ erasePending = true;
+ }
+ else
+ {
+ activeStroke?.Add(position);
+ }
+ }
+
+ public void PointerDown(SKPoint position, bool erase)
+ {
+ if (!erase && toolbar.IsClearButton(position))
+ {
+ pointer = position;
+ ClearCanvas();
+ }
+ else if (activeStroke is null && !erasing)
+ {
+ pointer = position;
+
+ if (!erase && position.Y <= Toolbar.ToolbarHeight)
+ {
+ toolbar.SelectAt(position);
+ }
+ else if (drawingArea.Contains(position.X, position.Y))
+ {
+ if (erase)
+ {
+ erasing = true;
+ eraserBuilder.MoveTo(position);
+ eraserBuilder.LineTo(position);
+ erasePending = true;
+ }
+ else
+ {
+ activeStroke = new(toolbar.SelectedColor, toolbar.SelectedStrokeWidth);
+ activeStroke.Add(position);
+ }
+ }
+ }
+ }
+
+ public void PointerUp(bool erase)
+ {
+ if (erase && erasing)
+ {
+ erasing = false;
+
+ if (erasePending)
+ {
+ ApplyEraser();
+ }
+ }
+ else if (!erase && activeStroke is not null)
+ {
+ activeStroke.Complete(strokePaint);
+ strokes.Add(activeStroke);
+ activeStroke = null;
+ pictureDirty = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (Stroke stroke in strokes)
+ {
+ stroke.Dispose();
+ }
+
+ strokes.Clear();
+ activeStroke?.Dispose();
+ cachedPicture?.Dispose();
+
+ eraserBuilder.Dispose();
+ strokePaint.Dispose();
+ fillPaint.Dispose();
+
+ toolbar.Dispose();
+ }
+
+ private void EnsureLayout(float width, float height)
+ {
+ if (width == size.Width && height == size.Height)
+ {
+ return;
+ }
+
+ size = new(width, height);
+ drawingArea = new(0.0f, Toolbar.ToolbarHeight, width, MathF.Max(Toolbar.ToolbarHeight, height - Toolbar.StatusHeight));
+
+ toolbar.Resize(width, height);
+ pictureDirty = true;
+ }
+
+ private SKPicture RecordCanvas()
+ {
+ const float spacing = 32.0f;
+
+ using SKPictureRecorder recorder = new();
+ SKCanvas canvas = recorder.BeginRecording(drawingArea);
+ int nodes = 0;
+
+ strokePaint.Color = Grid;
+ strokePaint.StrokeWidth = 1.0f;
+
+ for (float x = drawingArea.Left + spacing; x < drawingArea.Right; x += spacing)
+ {
+ canvas.DrawLine(x, drawingArea.Top, x, drawingArea.Bottom, strokePaint);
+ }
+
+ for (float y = drawingArea.Top + spacing; y < drawingArea.Bottom; y += spacing)
+ {
+ canvas.DrawLine(drawingArea.Left, y, drawingArea.Right, y, strokePaint);
+ }
+
+ foreach (Stroke stroke in strokes)
+ {
+ fillPaint.Color = stroke.Color;
+ canvas.DrawPath(stroke.Path, fillPaint);
+ nodes += stroke.NodeCount;
+ }
+
+ nodeCount = nodes;
+
+ return recorder.EndRecording();
+ }
+
+ private void DrawEraserCursor(SKCanvas canvas)
+ {
+ strokePaint.Color = Cursor;
+ strokePaint.StrokeWidth = 1.5f;
+ canvas.DrawCircle(pointer, EraserRadius, strokePaint);
+ }
+
+ private void ClearCanvas()
+ {
+ if (CanClear)
+ {
+ foreach (Stroke stroke in strokes)
+ {
+ stroke.Dispose();
+ }
+
+ strokes.Clear();
+ activeStroke?.Dispose();
+ activeStroke = null;
+ eraserBuilder.Reset();
+ erasing = false;
+ erasePending = false;
+ pictureDirty = true;
+ }
+ }
+
+ private void ApplyEraser()
+ {
+ using SKPath centerline = eraserBuilder.Detach();
+
+ if (erasing)
+ {
+ eraserBuilder.MoveTo(pointer);
+ }
+
+ strokePaint.StrokeWidth = EraserRadius * 2.0f;
+
+ using SKPath eraser = strokePaint.GetFillPath(centerline)!;
+ SKRect eraserBounds = eraser.Bounds;
+
+ erasePending = false;
+
+ for (int index = strokes.Count - 1; index >= 0; index--)
+ {
+ Stroke stroke = strokes[index];
+
+ if (stroke.Erase(eraser, eraserBounds))
+ {
+ if (stroke.IsEmpty)
+ {
+ stroke.Dispose();
+ strokes.RemoveAt(index);
+ }
+
+ pictureDirty = true;
+ }
+ }
+ }
+}
diff --git a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
new file mode 100644
index 00000000..4fb096b2
--- /dev/null
+++ b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
@@ -0,0 +1,103 @@
+using System.Numerics;
+using Silk.NET.Input;
+using Zenith.NET;
+using Zenith.NET.Extensions.Skia;
+
+namespace InkCanvas.Drawing;
+
+internal class CanvasController : IDisposable
+{
+ private readonly Canvas canvas = new();
+
+ private SKTexture texture;
+
+ public CanvasController(GraphicsContext context, IInputContext input, uint width, uint height)
+ {
+ Context = context;
+
+ texture = CreateTexture(width, height);
+
+ IMouse mouse = input.Mice[0];
+ mouse.MouseDown += OnMouseDown;
+ mouse.MouseUp += OnMouseUp;
+ mouse.MouseMove += OnMouseMove;
+ }
+
+ public GraphicsContext Context { get; }
+
+ public void Render(CommandBuffer commandBuffer, Texture target, Vector2 dpiScale)
+ {
+ float width = texture.Desc.Width / dpiScale.X;
+ float height = texture.Desc.Height / dpiScale.Y;
+
+ texture.Render((skiaCanvas) =>
+ {
+ skiaCanvas.Save();
+ skiaCanvas.Scale(dpiScale.X, dpiScale.Y);
+
+ canvas.Draw(skiaCanvas, width, height);
+
+ skiaCanvas.Restore();
+ });
+
+ commandBuffer.Transition(texture, default, texture.RequiredLayout, TextureLayout.CopySrc);
+ commandBuffer.CopyTexture(texture, default, default, target, default, default, new()
+ {
+ Width = texture.Desc.Width,
+ Height = texture.Desc.Height,
+ Depth = 1
+ });
+ commandBuffer.Transition(texture, default, TextureLayout.CopySrc, texture.RequiredLayout);
+ }
+
+ public void Resize(uint width, uint height)
+ {
+ texture.Dispose();
+ texture = CreateTexture(width, height);
+ }
+
+ public void Dispose()
+ {
+ canvas.Dispose();
+ texture.Dispose();
+ }
+
+ private SKTexture CreateTexture(uint width, uint height)
+ {
+ return Context.CreateSKTexture(new()
+ {
+ Format = PixelFormat.B8G8R8A8UNorm,
+ Width = width,
+ Height = height,
+ IsMultisamplingEnabled = canvas.MSAA
+ });
+ }
+
+ private void OnMouseDown(IMouse mouse, MouseButton button)
+ {
+ if (button is MouseButton.Left or MouseButton.Right)
+ {
+ bool msaa = canvas.MSAA;
+
+ canvas.PointerDown(new(mouse.Position.X, mouse.Position.Y), button is MouseButton.Right);
+
+ if (msaa != canvas.MSAA)
+ {
+ Resize(texture.Desc.Width, texture.Desc.Height);
+ }
+ }
+ }
+
+ private void OnMouseUp(IMouse mouse, MouseButton button)
+ {
+ if (button is MouseButton.Left or MouseButton.Right)
+ {
+ canvas.PointerUp(button is MouseButton.Right);
+ }
+ }
+
+ private void OnMouseMove(IMouse mouse, Vector2 position)
+ {
+ canvas.PointerMove(new(position.X, position.Y));
+ }
+}
diff --git a/sources/Experiments/InkCanvas/Drawing/Stroke.cs b/sources/Experiments/InkCanvas/Drawing/Stroke.cs
new file mode 100644
index 00000000..4ebd5b59
--- /dev/null
+++ b/sources/Experiments/InkCanvas/Drawing/Stroke.cs
@@ -0,0 +1,105 @@
+using SkiaSharp;
+
+namespace InkCanvas.Drawing;
+
+internal class Stroke(SKColor color, float width) : IDisposable
+{
+ private const float MinimumPointDistance = 1.5f;
+
+ private SKPathBuilder? builder = new();
+ private SKPath? path;
+
+ private SKRect bounds;
+ private SKPoint tailStart;
+ private SKPoint tailEnd;
+ private int sampleCount;
+
+ public SKColor Color { get; } = color;
+
+ public float Width { get; } = width;
+
+ public int NodeCount => Path.PointCount;
+
+ public bool IsEmpty => Path.IsEmpty;
+
+ public SKPath Path => path ??= builder!.Snapshot();
+
+ public SKPoint TailStart => tailStart;
+
+ public SKPoint TailEnd => tailEnd;
+
+ public void Add(SKPoint point)
+ {
+ if (sampleCount is 0 || DistanceSquared(tailEnd, point) >= MinimumPointDistance * MinimumPointDistance)
+ {
+ path?.Dispose();
+ path = null;
+
+ if (sampleCount is 0)
+ {
+ builder!.MoveTo(point);
+ tailStart = point;
+ }
+ else if (sampleCount > 1)
+ {
+ tailStart = new((tailEnd.X + point.X) * 0.5f, (tailEnd.Y + point.Y) * 0.5f);
+ builder!.QuadTo(tailEnd, tailStart);
+ }
+
+ tailEnd = point;
+ sampleCount++;
+ }
+ }
+
+ public void Complete(SKPaint paint)
+ {
+ builder!.LineTo(tailEnd);
+
+ path?.Dispose();
+
+ using SKPath centerline = builder.Detach();
+
+ builder.Dispose();
+ builder = null;
+
+ paint.StrokeWidth = Width;
+ path = paint.GetFillPath(centerline)!;
+
+ bounds = path.TightBounds;
+ }
+
+ public bool Erase(SKPath eraser, SKRect eraserBounds)
+ {
+ if (bounds.IntersectsWith(eraserBounds))
+ {
+ SKPath source = Path;
+
+ using SKPath? overlap = source.Op(eraser, SKPathOp.Intersect);
+
+ if (overlap?.IsEmpty is false && source.Op(eraser, SKPathOp.Difference) is { } result)
+ {
+ source.Dispose();
+ path = result;
+ bounds = result.TightBounds;
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public void Dispose()
+ {
+ path?.Dispose();
+ builder?.Dispose();
+ }
+
+ private static float DistanceSquared(SKPoint first, SKPoint second)
+ {
+ float dx = first.X - second.X;
+ float dy = first.Y - second.Y;
+
+ return (dx * dx) + (dy * dy);
+ }
+}
diff --git a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
new file mode 100644
index 00000000..c6be8471
--- /dev/null
+++ b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
@@ -0,0 +1,230 @@
+using SkiaSharp;
+
+namespace InkCanvas.Drawing;
+
+internal class Toolbar : IDisposable
+{
+ public const float ToolbarHeight = 64.0f;
+ public const float StatusHeight = 34.0f;
+
+ private const float SwatchSize = 30.0f;
+ private const float SwatchGap = 12.0f;
+ private const float CheckboxSize = 18.0f;
+ private const float MSAAWidth = 72.0f;
+ private const float ButtonWidth = 64.0f;
+
+ private static readonly SKColor Panel = new(31, 34, 43);
+ private static readonly SKColor Divider = new(48, 52, 63);
+ private static readonly SKColor Label = new(150, 158, 172);
+ private static readonly SKColor Highlight = new(238, 242, 248);
+ private static readonly SKColor Selected = new(58, 64, 78);
+
+ private static readonly SKColor[] Palette =
+ [
+ new(238, 242, 248),
+ new(236, 108, 96),
+ new(238, 178, 74),
+ new(96, 196, 154),
+ new(102, 158, 240),
+ new(178, 134, 234)
+ ];
+
+ private static readonly float[] StrokeWidths = [2.0f, 4.0f, 8.0f, 16.0f];
+
+ private readonly SKRect[] swatchRects = new SKRect[Palette.Length];
+ private readonly SKRect[] strokeWidthRects = new SKRect[StrokeWidths.Length];
+
+ private readonly SKFont labelFont;
+ private readonly SKPaint fillPaint = new();
+ private readonly SKPaint strokePaint = new() { Style = SKPaintStyle.Stroke };
+
+ private SKRect msaaRect;
+ private SKRect clearRect;
+ private SKSize size;
+
+ private int colorIndex;
+ private int strokeWidthIndex = 1;
+
+ public Toolbar()
+ {
+ string family = OperatingSystem.IsMacOS() ? "SF Pro Text" : OperatingSystem.IsWindows() ? "Segoe UI" : "Noto Sans";
+
+ using SKTypeface typeface = SKTypeface.FromFamilyName(family, SKFontStyle.Normal);
+
+ labelFont = new(typeface, 12.0f)
+ {
+ Subpixel = true,
+ Edging = SKFontEdging.SubpixelAntialias,
+ Hinting = SKFontHinting.Slight
+ };
+ }
+
+ public SKColor SelectedColor => Palette[colorIndex];
+
+ public float SelectedStrokeWidth => StrokeWidths[strokeWidthIndex];
+
+ public bool MSAA { get; private set; } = true;
+
+ public void Resize(float width, float height)
+ {
+ const float top = (ToolbarHeight - SwatchSize) * 0.5f;
+ const float bottom = top + SwatchSize;
+
+ size = new(width, height);
+
+ for (int index = 0; index < swatchRects.Length; index++)
+ {
+ float left = SwatchGap + (index * (SwatchSize + SwatchGap));
+ swatchRects[index] = new(left, top, left + SwatchSize, bottom);
+ }
+
+ float strokeWidthLeft = swatchRects[^1].Right + (SwatchGap * 2.0f);
+
+ for (int index = 0; index < strokeWidthRects.Length; index++)
+ {
+ float left = strokeWidthLeft + (index * (SwatchSize + SwatchGap));
+ strokeWidthRects[index] = new(left, top, left + SwatchSize, bottom);
+ }
+
+ float msaaLeft = strokeWidthRects[^1].Right + (SwatchGap * 2.0f);
+ msaaRect = new(msaaLeft, top, msaaLeft + MSAAWidth, bottom);
+
+ float clearLeft = MathF.Max(msaaRect.Right + (SwatchGap * 2.0f), width - SwatchGap - ButtonWidth);
+ clearRect = new(clearLeft, top, clearLeft + ButtonWidth, bottom);
+ }
+
+ public void Draw(SKCanvas canvas, int strokeCount, int nodeCount, bool canClear)
+ {
+ DrawToolbar(canvas, canClear);
+ DrawStatus(canvas, strokeCount, nodeCount);
+ }
+
+ public void SelectAt(SKPoint position)
+ {
+ int swatch = IndexAt(swatchRects, position);
+ int strokeWidth = IndexAt(strokeWidthRects, position);
+
+ if (swatch >= 0)
+ {
+ colorIndex = swatch;
+ }
+ else if (strokeWidth >= 0)
+ {
+ strokeWidthIndex = strokeWidth;
+ }
+ else if (msaaRect.Contains(position.X, position.Y))
+ {
+ MSAA = !MSAA;
+ }
+ }
+
+ public bool IsClearButton(SKPoint position)
+ {
+ return clearRect.Contains(position.X, position.Y);
+ }
+
+ public void Dispose()
+ {
+ strokePaint.Dispose();
+ fillPaint.Dispose();
+ labelFont.Dispose();
+ }
+
+ private void DrawToolbar(SKCanvas canvas, bool canClear)
+ {
+ const float checkboxTop = (ToolbarHeight - CheckboxSize) * 0.5f;
+
+ fillPaint.Color = Panel;
+ canvas.DrawRect(0.0f, 0.0f, size.Width, ToolbarHeight, fillPaint);
+
+ fillPaint.Color = Divider;
+ canvas.DrawRect(0.0f, ToolbarHeight - 1.0f, size.Width, 1.0f, fillPaint);
+
+ for (int index = 0; index < Palette.Length; index++)
+ {
+ SKRect swatch = swatchRects[index];
+
+ fillPaint.Color = Palette[index];
+ canvas.DrawRoundRect(swatch, 6.0f, 6.0f, fillPaint);
+
+ if (index == colorIndex)
+ {
+ strokePaint.Color = Highlight;
+ strokePaint.StrokeWidth = 2.0f;
+ canvas.DrawRoundRect(SKRect.Inflate(swatch, 4.0f, 4.0f), 9.0f, 9.0f, strokePaint);
+ }
+ }
+
+ for (int index = 0; index < StrokeWidths.Length; index++)
+ {
+ SKRect slot = strokeWidthRects[index];
+
+ fillPaint.Color = index == strokeWidthIndex ? Selected : Panel;
+ canvas.DrawRoundRect(slot, 6.0f, 6.0f, fillPaint);
+
+ fillPaint.Color = Palette[colorIndex];
+ canvas.DrawCircle(slot.MidX, slot.MidY, StrokeWidths[index] * 0.5f, fillPaint);
+ }
+
+ SKRect checkbox = new(msaaRect.Left, checkboxTop, msaaRect.Left + CheckboxSize, checkboxTop + CheckboxSize);
+
+ fillPaint.Color = MSAA ? Selected : Panel;
+ canvas.DrawRoundRect(checkbox, 4.0f, 4.0f, fillPaint);
+
+ strokePaint.Color = MSAA ? Highlight : Label;
+ strokePaint.StrokeWidth = 1.5f;
+ canvas.DrawRoundRect(checkbox, 4.0f, 4.0f, strokePaint);
+
+ if (MSAA)
+ {
+ strokePaint.StrokeWidth = 2.0f;
+ canvas.DrawLine(checkbox.Left + 4.0f, checkbox.MidY, checkbox.Left + 8.0f, checkbox.Bottom - 4.0f, strokePaint);
+ canvas.DrawLine(checkbox.Left + 8.0f, checkbox.Bottom - 4.0f, checkbox.Right - 3.0f, checkbox.Top + 4.0f, strokePaint);
+ }
+
+ fillPaint.Color = Label;
+ canvas.DrawText("MSAA", checkbox.Right + 8.0f, msaaRect.MidY + 4.0f, SKTextAlign.Left, labelFont, fillPaint);
+
+ SKColor accent = canClear ? Label : Divider;
+
+ fillPaint.Color = Panel;
+ canvas.DrawRoundRect(clearRect, 6.0f, 6.0f, fillPaint);
+
+ strokePaint.Color = accent;
+ strokePaint.StrokeWidth = 1.5f;
+ canvas.DrawRoundRect(clearRect, 6.0f, 6.0f, strokePaint);
+
+ fillPaint.Color = accent;
+ canvas.DrawText("CLEAR", clearRect.MidX, clearRect.MidY + 4.0f, SKTextAlign.Center, labelFont, fillPaint);
+ }
+
+ private void DrawStatus(SKCanvas canvas, int strokeCount, int nodeCount)
+ {
+ float top = size.Height - StatusHeight;
+
+ fillPaint.Color = Panel;
+ canvas.DrawRect(0.0f, top, size.Width, StatusHeight, fillPaint);
+
+ fillPaint.Color = Divider;
+ canvas.DrawRect(0.0f, top, size.Width, 1.0f, fillPaint);
+
+ float baseline = top + (StatusHeight * 0.5f) + 4.0f;
+
+ fillPaint.Color = Label;
+ canvas.DrawText($"STROKES {strokeCount} NODES {nodeCount}", SwatchGap, baseline, SKTextAlign.Left, labelFont, fillPaint);
+ canvas.DrawText("DRAG TO DRAW RIGHT DRAG TO ERASE", size.Width - SwatchGap, baseline, SKTextAlign.Right, labelFont, fillPaint);
+ }
+
+ private static int IndexAt(SKRect[] rects, SKPoint position)
+ {
+ for (int index = 0; index < rects.Length; index++)
+ {
+ if (rects[index].Contains(position.X, position.Y))
+ {
+ return index;
+ }
+ }
+
+ return -1;
+ }
+}
diff --git a/sources/Experiments/InkCanvas/Helpers/CocoaHelper.cs b/sources/Experiments/InkCanvas/Helpers/CocoaHelper.cs
new file mode 100644
index 00000000..3bec63f1
--- /dev/null
+++ b/sources/Experiments/InkCanvas/Helpers/CocoaHelper.cs
@@ -0,0 +1,34 @@
+using System.Runtime.InteropServices;
+
+namespace InkCanvas.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/InkCanvas/InkCanvas.csproj b/sources/Experiments/InkCanvas/InkCanvas.csproj
new file mode 100644
index 00000000..8b441176
--- /dev/null
+++ b/sources/Experiments/InkCanvas/InkCanvas.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ $(StandardTargetFramework)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sources/Experiments/InkCanvas/Program.cs b/sources/Experiments/InkCanvas/Program.cs
new file mode 100644
index 00000000..6ef9a23a
--- /dev/null
+++ b/sources/Experiments/InkCanvas/Program.cs
@@ -0,0 +1,3 @@
+using InkCanvas;
+
+App.Run();
\ No newline at end of file
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/Extensions.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/Extensions.cs
new file mode 100644
index 00000000..5e55de6c
--- /dev/null
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/Extensions.cs
@@ -0,0 +1,34 @@
+namespace Zenith.NET.Extensions.Skia;
+
+public static class Extensions
+{
+ private static readonly Lock @lock = new();
+ private static readonly Dictionary renderers = [];
+
+ extension(GraphicsContext context)
+ {
+ public SKTexture CreateSKTexture(SKTextureDesc desc)
+ {
+ using Lock.Scope _ = @lock.EnterScope();
+
+ if (!renderers.TryGetValue(context, out SKRenderer? renderer))
+ {
+ renderers[context] = renderer = new(context);
+ }
+
+ renderer.AddReference();
+
+ return new(renderer, desc);
+ }
+ }
+
+ internal static void ReleaseRenderer(SKRenderer renderer)
+ {
+ using Lock.Scope _ = @lock.EnterScope();
+
+ if (renderer.RemoveReference() && renderers.Remove(renderer.Context))
+ {
+ renderer.Dispose();
+ }
+ }
+}
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
new file mode 100644
index 00000000..77dd5aa9
--- /dev/null
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
@@ -0,0 +1,88 @@
+using SkiaSharp;
+
+namespace Zenith.NET.Extensions.Skia;
+
+internal static class SKFormats
+{
+ public static SKColorType Skia(PixelFormat pixelFormat)
+ {
+ return pixelFormat switch
+ {
+ PixelFormat.R8UNorm => SKColorType.Gray8,
+ PixelFormat.R16Float => SKColorType.AlphaF16,
+ PixelFormat.R8G8B8A8UNorm => SKColorType.Rgba8888,
+ PixelFormat.R8G8B8A8SRgb => SKColorType.Srgba8888,
+ PixelFormat.R16G16B16A16Float => SKColorType.RgbaF16,
+ PixelFormat.R32G32B32A32Float => SKColorType.RgbaF32,
+ PixelFormat.B8G8R8A8UNorm => SKColorType.Bgra8888,
+ _ => default
+ };
+ }
+
+ public static uint DirectX12(PixelFormat pixelFormat)
+ {
+ return pixelFormat switch
+ {
+ PixelFormat.R8UNorm => 61,
+ PixelFormat.R16Float => 54,
+ PixelFormat.R8G8B8A8UNorm => 28,
+ PixelFormat.R8G8B8A8SRgb => 29,
+ PixelFormat.R16G16B16A16Float => 10,
+ PixelFormat.R32G32B32A32Float => 2,
+ PixelFormat.B8G8R8A8UNorm => 87,
+ _ => default
+ };
+ }
+
+ public static uint Vulkan(PixelFormat pixelFormat)
+ {
+ return pixelFormat switch
+ {
+ PixelFormat.R8UNorm => 9,
+ PixelFormat.R16Float => 76,
+ PixelFormat.R8G8B8A8UNorm => 37,
+ PixelFormat.R8G8B8A8SRgb => 43,
+ PixelFormat.R16G16B16A16Float => 97,
+ PixelFormat.R32G32B32A32Float => 109,
+ PixelFormat.B8G8R8A8UNorm => 44,
+ _ => default
+ };
+ }
+
+ public static uint Vulkan(TextureUsages textureUsages)
+ {
+ uint result = default;
+
+ if (textureUsages.HasFlag(TextureUsages.Sampled))
+ {
+ result |= 1 << 2;
+ }
+
+ if (textureUsages.HasFlag(TextureUsages.Storage))
+ {
+ result |= 1 << 3;
+ }
+
+ if (textureUsages.HasFlag(TextureUsages.ColorAttachment))
+ {
+ result |= 1 << 4;
+ }
+
+ if (textureUsages.HasFlag(TextureUsages.DepthStencilAttachment))
+ {
+ result |= 1 << 5;
+ }
+
+ if (textureUsages.HasFlag(TextureUsages.TransferSrc))
+ {
+ result |= 1 << 0;
+ }
+
+ if (textureUsages.HasFlag(TextureUsages.TransferDst))
+ {
+ result |= 1 << 1;
+ }
+
+ return result;
+ }
+}
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
new file mode 100644
index 00000000..b333b4f2
--- /dev/null
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
@@ -0,0 +1,192 @@
+using System.Runtime.InteropServices;
+using SkiaSharp;
+
+namespace Zenith.NET.Extensions.Skia;
+
+internal unsafe class SKRenderer : DisposableObject
+{
+ private readonly Lock @lock = new();
+ private readonly nint commandQueue;
+
+ private uint referenceCount;
+
+ public SKRenderer(GraphicsContext context)
+ {
+ Context = context;
+
+ GRContextOptions options = new()
+ {
+ AvoidStencilBuffers = true,
+ RuntimeProgramCacheSize = 1024
+ };
+
+ switch (context.GraphicsApi)
+ {
+ case GraphicsApi.DirectX12:
+ {
+ using GRD3DBackendContext backendContext = new()
+ {
+ Adapter = context.GetNativeObject(NativeObjectType.D3D12Adapter),
+ Device = context.GetNativeObject(NativeObjectType.D3D12Device),
+ Queue = context.GraphicsQueue.GetNativeObject(NativeObjectType.D3D12CommandQueue)
+ };
+
+ GRContext = GRContext.CreateDirect3D(backendContext, options);
+ }
+ break;
+
+ case GraphicsApi.Metal:
+ {
+ nint device = context.GetNativeObject(NativeObjectType.MTLDevice);
+
+ using GRMtlBackendContext backendContext = new()
+ {
+ DeviceHandle = device,
+ QueueHandle = commandQueue = SKObjectiveC.SendMessage(device, "newCommandQueue")
+ };
+
+ GRContext = GRContext.CreateMetal(backendContext, options);
+ }
+ break;
+
+ case GraphicsApi.Vulkan:
+ {
+ delegate* unmanaged getInstanceProcAddr = (delegate* unmanaged)context.GetNativeObject(NativeObjectType.VulkanGetInstanceProcAddr);
+ delegate* unmanaged getDeviceProcAddr = (delegate* unmanaged)context.GetNativeObject(NativeObjectType.VulkanGetDeviceProcAddr);
+
+ nint instance = context.GetNativeObject(NativeObjectType.VulkanInstance);
+ nint physicalDevice = context.GetNativeObject(NativeObjectType.VulkanPhysicalDevice);
+
+ using GRVkExtensions extensions = GRVkExtensions.Create(GetProcedureAddress, instance, physicalDevice, null, null);
+
+ using GRVkBackendContext backendContext = new()
+ {
+ VkInstance = instance,
+ VkPhysicalDevice = physicalDevice,
+ VkDevice = context.GetNativeObject(NativeObjectType.VulkanDevice),
+ VkQueue = context.GraphicsQueue.GetNativeObject(NativeObjectType.VulkanQueue),
+ GraphicsQueueIndex = (uint)context.GraphicsQueue.GetNativeObject(NativeObjectType.VulkanQueueFamilyIndex),
+ MaxAPIVersion = (1u << 22) | (4u << 12),
+ Extensions = extensions,
+ GetProcedureAddress = GetProcedureAddress
+ };
+
+ GRContext = GRContext.CreateVulkan(backendContext, options);
+
+ nint GetProcedureAddress(string name, nint instance, nint device)
+ {
+ using ZenithMarshal.Scope scope = new();
+
+ byte* pointer = (byte*)ZenithMarshal.StringToPointer(scope, name, StringEncoding.UTF8);
+
+ return device is 0 ? getInstanceProcAddr(instance, pointer) : getDeviceProcAddr(device, pointer);
+ }
+ }
+ break;
+
+ default:
+ GRContext = default!;
+ break;
+ }
+ }
+
+ public GraphicsContext Context { get; }
+
+ public GRContext GRContext { get; }
+
+ public void AddReference()
+ {
+ referenceCount++;
+ }
+
+ public bool RemoveReference()
+ {
+ return --referenceCount is 0;
+ }
+
+ public void Render(SKSurface surface, Action render)
+ {
+ using Lock.Scope _ = @lock.EnterScope();
+
+ render(surface.Canvas);
+
+ GRContext.Flush(surface);
+ GRContext.Submit(true);
+ }
+
+ protected override void Destroy()
+ {
+ GRContext.Dispose();
+
+ if (Context.GraphicsApi is GraphicsApi.Metal)
+ {
+ SKObjectiveC.Release(commandQueue);
+ }
+ }
+
+ public GRBackendTexture CreateBackendTexture(Texture texture, bool isMultisamplingEnabled)
+ {
+ switch (Context.GraphicsApi)
+ {
+ case GraphicsApi.DirectX12:
+ return new((int)texture.Desc.Width, (int)texture.Desc.Height, new GRD3DTextureResourceInfo()
+ {
+ Resource = texture.GetNativeObject(NativeObjectType.D3D12Resource),
+ ResourceState = isMultisamplingEnabled ? 0x1000u : 0x4u,
+ Format = SKFormats.DirectX12(texture.Desc.Format),
+ SampleCount = 1,
+ LevelCount = 1,
+ SampleQualityPattern = isMultisamplingEnabled ? uint.MaxValue : 0
+ });
+
+ case GraphicsApi.Metal:
+ return new((int)texture.Desc.Width, (int)texture.Desc.Height, false, new GRMtlTextureInfo() { TextureHandle = texture.GetNativeObject(NativeObjectType.MTLTexture) });
+
+ case GraphicsApi.Vulkan:
+ uint graphicsQueueFamily = (uint)Context.GraphicsQueue.GetNativeObject(NativeObjectType.VulkanQueueFamilyIndex);
+ uint computeQueueFamily = (uint)Context.ComputeQueue.GetNativeObject(NativeObjectType.VulkanQueueFamilyIndex);
+ uint transferQueueFamily = (uint)Context.TransferQueue.GetNativeObject(NativeObjectType.VulkanQueueFamilyIndex);
+ bool concurrent = graphicsQueueFamily != computeQueueFamily || graphicsQueueFamily != transferQueueFamily;
+
+ return new((int)texture.Desc.Width, (int)texture.Desc.Height, new GRVkImageInfo()
+ {
+ Image = (ulong)texture.GetNativeObject(NativeObjectType.VulkanImage),
+ Alloc = new()
+ {
+ Memory = (ulong)texture.GetNativeObject(NativeObjectType.VulkanDeviceMemory),
+ Offset = (ulong)texture.GetNativeObject(NativeObjectType.VulkanDeviceMemoryOffset),
+ Size = Context.GetSizeAndAlignment(texture.Desc).SizeInBytes
+ },
+ ImageLayout = isMultisamplingEnabled ? 7u : 2u,
+ Format = SKFormats.Vulkan(texture.Desc.Format),
+ ImageUsageFlags = SKFormats.Vulkan(texture.Desc.Usages),
+ SampleCount = 1,
+ LevelCount = 1,
+ CurrentQueueFamily = concurrent ? uint.MaxValue : graphicsQueueFamily,
+ SharingMode = concurrent ? 1u : 0u
+ });
+
+ default:
+ return default!;
+ }
+ }
+}
+
+internal static partial class SKObjectiveC
+{
+ private const string LibObjC = "/usr/lib/libobjc.A.dylib";
+
+ [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
+ private static partial nint SendMessage(nint receiver, nint selector);
+
+ [LibraryImport(LibObjC, EntryPoint = "sel_registerName")]
+ private static partial nint RegisterName([MarshalAs(UnmanagedType.LPUTF8Str)] string name);
+
+ [LibraryImport(LibObjC, EntryPoint = "objc_release")]
+ public static partial void Release(nint value);
+
+ public static nint SendMessage(nint receiver, string selector)
+ {
+ return SendMessage(receiver, RegisterName(selector));
+ }
+}
\ No newline at end of file
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
new file mode 100644
index 00000000..c304452a
--- /dev/null
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
@@ -0,0 +1,70 @@
+using System.Numerics;
+using SkiaSharp;
+
+namespace Zenith.NET.Extensions.Skia;
+
+public class SKTexture : DisposableObject
+{
+ private readonly Texture texture;
+ private readonly SKSurface surface;
+
+ internal SKTexture(SKRenderer renderer, SKTextureDesc desc)
+ {
+ Renderer = renderer;
+
+ texture = renderer.Context.CreateTexture(new()
+ {
+ Type = TextureType.Texture2D,
+ Format = desc.Format,
+ Width = desc.Width,
+ Height = desc.Height,
+ Depth = 1,
+ MipLevels = 1,
+ ArrayLayers = 1,
+ SampleCount = SampleCount.Count1,
+ Usages = TextureUsages.Sampled | TextureUsages.ColorAttachment | TextureUsages.TransferSrc | TextureUsages.TransferDst
+ });
+
+ CommandBuffer commandBuffer = renderer.Context.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(texture, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+
+ commandBuffer.BeginRenderPass([ColorAttachment.Clear(texture, Vector4.Zero)], null);
+ commandBuffer.EndRenderPass();
+
+ if ((RequiredLayout = desc.IsMultisamplingEnabled ? TextureLayout.ResolveDst : TextureLayout.ColorAttachment) is not TextureLayout.ColorAttachment)
+ {
+ commandBuffer.Transition(texture, default, TextureLayout.ColorAttachment, RequiredLayout);
+ }
+
+ commandBuffer.Submit().Wait();
+
+ using GRBackendTexture backendTexture = renderer.CreateBackendTexture(texture, desc.IsMultisamplingEnabled);
+
+ surface = SKSurface.Create(renderer.GRContext, backendTexture, GRSurfaceOrigin.TopLeft, desc.IsMultisamplingEnabled ? 4 : 1, SKFormats.Skia(desc.Format));
+ }
+
+ internal SKRenderer Renderer { get; }
+
+ public ref readonly TextureDesc Desc => ref texture.Desc;
+
+ public TextureLayout RequiredLayout { get; }
+
+ public void Render(Action render)
+ {
+ Renderer.Render(surface, render);
+ }
+
+ protected override void Destroy()
+ {
+ surface.Dispose();
+ texture.Dispose();
+
+ Extensions.ReleaseRenderer(Renderer);
+ }
+
+ public static implicit operator Texture(SKTexture texture)
+ {
+ return texture.texture;
+ }
+}
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs
new file mode 100644
index 00000000..65c29c7f
--- /dev/null
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs
@@ -0,0 +1,12 @@
+namespace Zenith.NET.Extensions.Skia;
+
+public struct SKTextureDesc
+{
+ public PixelFormat Format;
+
+ public uint Width;
+
+ public uint Height;
+
+ public bool IsMultisamplingEnabled;
+}
diff --git a/sources/NuGet.Packaging.props b/sources/NuGet.Packaging.props
index f80d76e1..e60169d2 100644
--- a/sources/NuGet.Packaging.props
+++ b/sources/NuGet.Packaging.props
@@ -7,7 +7,7 @@
$(MSBuildThisFileDirectory)..\.nuget
- 1.0.0-rc
+ 1.0.0
qian-o
Copyright (c) 2026 qian-o
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.
diff --git a/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs b/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs
index 6557c768..5f4ec710 100644
--- a/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs
@@ -81,7 +81,7 @@ public override void Render(DrawingContext context)
void IZenithView.UI(Action action)
{
- Dispatcher.Invoke(action);
+ Dispatcher.InvokeAsync(action);
}
void IZenithView.EnsureResources()
diff --git a/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs b/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs
index 52163433..e41577a8 100644
--- a/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs
+++ b/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs
@@ -11,6 +11,8 @@ internal unsafe partial class Surface : DisposableObject
[LibraryImport("kernel32")]
private static partial int CloseHandle(nint hObject);
+ public ComPtr Query = new();
+
public ComPtr SwapChain = new();
public ComPtr Texture = new();
@@ -23,6 +25,9 @@ internal unsafe partial class Surface : DisposableObject
public Surface(GraphicsContext graphicsContext, uint width, uint height)
{
+ QueryDesc queryDesc = new();
+ D3D.Success(D3D.Device.CreateQuery(&queryDesc, Query.GetAddressOf()));
+
SwapChainDesc1 swapChainDesc = new()
{
Width = width,
@@ -97,7 +102,12 @@ public void Present()
AcquireSync();
D3D.DeviceContext.CopyResource((ID3D11Resource*)backBuffer.Handle, (ID3D11Resource*)Texture.Handle);
- D3D.DeviceContext.Flush();
+ D3D.DeviceContext.End(Query);
+
+ while (D3D.DeviceContext.GetData(Query, default, 0, 0) is 1)
+ {
+ Thread.Yield();
+ }
ReleaseSync();
@@ -116,6 +126,7 @@ protected override void Destroy()
Mutex.Dispose();
Texture.Dispose();
SwapChain.Dispose();
+ Query.Dispose();
}
private static Format DrawableFormat()
diff --git a/sources/Views/Zenith.NET.Views.WPF/Surface.cs b/sources/Views/Zenith.NET.Views.WPF/Surface.cs
index 332cd3c7..92e36f0e 100644
--- a/sources/Views/Zenith.NET.Views.WPF/Surface.cs
+++ b/sources/Views/Zenith.NET.Views.WPF/Surface.cs
@@ -15,6 +15,8 @@ internal unsafe partial class Surface : DisposableObject
[LibraryImport("kernel32")]
private static partial int CloseHandle(nint hObject);
+ public ComPtr Query = new();
+
public ComPtr D3D9RenderTarget = new();
public ComPtr D3D9RenderSurface = new();
@@ -31,6 +33,9 @@ internal unsafe partial class Surface : DisposableObject
public Surface(GraphicsContext graphicsContext, uint width, uint height)
{
+ QueryDesc queryDesc = new();
+ D3D.Success(D3D.D3D11Device.CreateQuery(&queryDesc, Query.GetAddressOf()));
+
void* sharedHandle = null;
D3D.Success(D3D.D3D9DeviceEx.CreateTexture(width,
height,
@@ -104,7 +109,12 @@ public void Present(D3DImage image)
AcquireSync();
D3D.D3D11DeviceContext.CopyResource((ID3D11Resource*)D3D9SharedTexture.Handle, (ID3D11Resource*)D3D11RenderTarget.Handle);
- D3D.D3D11DeviceContext.Flush();
+ D3D.D3D11DeviceContext.End(Query);
+
+ while (D3D.D3D11DeviceContext.GetData(Query, default, 0, 0) is 1)
+ {
+ Thread.Yield();
+ }
ReleaseSync();
@@ -126,6 +136,7 @@ protected override void Destroy()
D3D9SharedTexture.Dispose();
D3D9RenderSurface.Dispose();
D3D9RenderTarget.Dispose();
+ Query.Dispose();
}
private static DXGIFormat DrawableFormat()
diff --git a/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs b/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs
index c0d80027..ab827d58 100644
--- a/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs
@@ -4,6 +4,7 @@
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
+using System.Windows.Threading;
namespace Zenith.NET.Views.WPF;
@@ -88,7 +89,7 @@ protected override void OnRender(DrawingContext drawingContext)
void IZenithView.UI(Action action)
{
- Dispatcher.Invoke(action);
+ Dispatcher.InvokeAsync(action);
}
void IZenithView.EnsureResources()
diff --git a/sources/Views/Zenith.NET.Views.WinForms/ZenithView.cs b/sources/Views/Zenith.NET.Views.WinForms/ZenithView.cs
index 97d6a1e3..6465e2c2 100644
--- a/sources/Views/Zenith.NET.Views.WinForms/ZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.WinForms/ZenithView.cs
@@ -63,7 +63,7 @@ protected override void OnPaint(PaintEventArgs e)
void IZenithView.UI(Action action)
{
- Invoke(action);
+ InvokeAsync(action);
}
void IZenithView.EnsureResources()
diff --git a/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs b/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
index 875ee498..f0dd43f6 100644
--- a/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
+++ b/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
@@ -113,6 +113,8 @@ internal unsafe partial class Surface : DisposableObject
[LibraryImport("kernel32")]
private static partial int CloseHandle(nint hObject);
+ public ComPtr Query = new();
+
public ComPtr SwapChain = new();
public ComPtr Texture = new();
@@ -125,6 +127,9 @@ internal unsafe partial class Surface : DisposableObject
public Surface(GraphicsContext graphicsContext, uint width, uint height)
{
+ QueryDesc queryDesc = new();
+ D3D.Success(D3D.Device.CreateQuery(&queryDesc, Query.GetAddressOf()));
+
SwapChainDesc1 swapChainDesc = new()
{
Width = width,
@@ -199,7 +204,12 @@ public void Present()
AcquireSync();
D3D.DeviceContext.CopyResource((ID3D11Resource*)backBuffer.Handle, (ID3D11Resource*)Texture.Handle);
- D3D.DeviceContext.Flush();
+ D3D.DeviceContext.End(Query);
+
+ while (D3D.DeviceContext.GetData(Query, default, 0, 0) is 1)
+ {
+ Thread.Yield();
+ }
ReleaseSync();
@@ -218,6 +228,7 @@ protected override void Destroy()
Mutex.Dispose();
Texture.Dispose();
SwapChain.Dispose();
+ Query.Dispose();
}
private static Format DrawableFormat()
diff --git a/sources/Zenith.NET.DirectX12/DXBottomLevelAccelerationStructure.cs b/sources/Zenith.NET.DirectX12/DXBottomLevelAccelerationStructure.cs
index 34534b32..3a89ebb8 100644
--- a/sources/Zenith.NET.DirectX12/DXBottomLevelAccelerationStructure.cs
+++ b/sources/Zenith.NET.DirectX12/DXBottomLevelAccelerationStructure.cs
@@ -70,7 +70,12 @@ public void Update(DXCommandBuffer commandBuffer, BottomLevelAccelerationStructu
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12GpuVirtualAddress => (nint)AccelerationStructure.GPUVirtualAddress,
+ NativeObjectType.D3D12Resource => (nint)AccelerationStructure.Resource.Handle,
+ _ => default
+ };
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET.DirectX12/DXBuffer.cs b/sources/Zenith.NET.DirectX12/DXBuffer.cs
index 9cf81f2c..90b92467 100644
--- a/sources/Zenith.NET.DirectX12/DXBuffer.cs
+++ b/sources/Zenith.NET.DirectX12/DXBuffer.cs
@@ -89,7 +89,12 @@ public DXBuffer(DXGraphicsContext context, BufferDesc desc, ResourceFlags flags)
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12GpuVirtualAddress => (nint)GPUVirtualAddress,
+ NativeObjectType.D3D12Resource => (nint)Resource.Handle,
+ _ => default
+ };
}
public override nint Map()
diff --git a/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs b/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs
index dd86760d..15f9f0ee 100644
--- a/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs
+++ b/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs
@@ -21,7 +21,11 @@ public DXCommandBuffer(DXGraphicsContext context, DXCommandQueue queue) : base(c
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12GraphicsCommandList => (nint)CommandList.Handle,
+ _ => default
+ };
}
protected override void BarrierImpl(BarrierStages before, BarrierStages after)
@@ -51,6 +55,11 @@ protected override void TransitionImpl(Texture texture, TextureSubresource subre
{
DXTexture dxTexture = texture.DirectX12();
+ if (!dxTexture.CanTransition)
+ {
+ return;
+ }
+
(BarrierSync syncBefore, BarrierAccess accessBefore, BarrierLayout layoutBefore) = DXFormats.DirectX12(before);
(BarrierSync syncAfter, BarrierAccess accessAfter, BarrierLayout layoutAfter) = DXFormats.DirectX12(after);
@@ -276,12 +285,12 @@ protected override void BeginRenderPassImpl(ReadOnlySpan colorA
Type = DXFormats.DirectX12(attachment.DepthLoadOp),
Clear = new() { ClearValue = clearValue }
},
- DepthEndingAccess = new() { Type = ZenithHelper.HasDepth(texture.Desc.Format) ? DXFormats.DirectX12(attachment.DepthStoreOp) : RenderPassEndingAccessType.NoAccess },
StencilBeginningAccess = new()
{
Type = ZenithHelper.HasStencil(texture.Desc.Format) ? DXFormats.DirectX12(attachment.StencilLoadOp) : RenderPassBeginningAccessType.NoAccess,
Clear = new() { ClearValue = clearValue }
},
+ DepthEndingAccess = new() { Type = ZenithHelper.HasDepth(texture.Desc.Format) ? DXFormats.DirectX12(attachment.DepthStoreOp) : RenderPassEndingAccessType.NoAccess },
StencilEndingAccess = new() { Type = ZenithHelper.HasStencil(texture.Desc.Format) ? DXFormats.DirectX12(attachment.StencilStoreOp) : RenderPassEndingAccessType.NoAccess }
};
}
diff --git a/sources/Zenith.NET.DirectX12/DXCommandQueue.cs b/sources/Zenith.NET.DirectX12/DXCommandQueue.cs
index 4eef6297..d95832fb 100644
--- a/sources/Zenith.NET.DirectX12/DXCommandQueue.cs
+++ b/sources/Zenith.NET.DirectX12/DXCommandQueue.cs
@@ -22,7 +22,11 @@ public DXCommandQueue(DXGraphicsContext context, CommandQueueType type) : base(c
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12CommandQueue => (nint)CommandQueue.Handle,
+ _ => default
+ };
}
protected override CommandBuffer CreateCommandBuffer()
diff --git a/sources/Zenith.NET.DirectX12/DXFormats.cs b/sources/Zenith.NET.DirectX12/DXFormats.cs
index 762a56d3..94f0d586 100644
--- a/sources/Zenith.NET.DirectX12/DXFormats.cs
+++ b/sources/Zenith.NET.DirectX12/DXFormats.cs
@@ -618,11 +618,6 @@ public static ResourceFlags DirectX12(TextureUsages textureUsages)
{
ResourceFlags result = default;
- if (!textureUsages.HasFlag(TextureUsages.Sampled))
- {
- result |= ResourceFlags.DenyShaderResource;
- }
-
if (textureUsages.HasFlag(TextureUsages.Storage))
{
result |= ResourceFlags.AllowUnorderedAccess;
@@ -635,6 +630,11 @@ public static ResourceFlags DirectX12(TextureUsages textureUsages)
if (textureUsages.HasFlag(TextureUsages.DepthStencilAttachment))
{
+ if (!textureUsages.HasFlag(TextureUsages.Sampled))
+ {
+ result |= ResourceFlags.DenyShaderResource;
+ }
+
result |= ResourceFlags.AllowDepthStencil;
}
diff --git a/sources/Zenith.NET.DirectX12/DXGraphicsContext.cs b/sources/Zenith.NET.DirectX12/DXGraphicsContext.cs
index 8b6d4cd2..04959410 100644
--- a/sources/Zenith.NET.DirectX12/DXGraphicsContext.cs
+++ b/sources/Zenith.NET.DirectX12/DXGraphicsContext.cs
@@ -40,7 +40,12 @@ internal unsafe class DXGraphicsContext(bool useValidationLayer) : GraphicsConte
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12Adapter => (nint)Adapter.Handle,
+ NativeObjectType.D3D12Device => (nint)Device.Handle,
+ _ => default
+ };
}
protected override void Initialize(bool useValidationLayer,
@@ -167,7 +172,7 @@ protected override Texture CreateTextureImpl(TextureDesc desc, NativeTextureType
ComPtr resource = new();
Device.OpenSharedHandle((void*)nativeTexture, SilkMarshal.GuidPtrOf(), (void**)resource.GetAddressOf()).Success();
- return new DXTexture(this, desc, resource);
+ return new DXTexture(this, desc, resource, false);
}
protected override TextureView CreateTextureViewImpl(TextureViewDesc desc)
diff --git a/sources/Zenith.NET.DirectX12/DXHeap.cs b/sources/Zenith.NET.DirectX12/DXHeap.cs
index eeb420dc..f37ed3aa 100644
--- a/sources/Zenith.NET.DirectX12/DXHeap.cs
+++ b/sources/Zenith.NET.DirectX12/DXHeap.cs
@@ -60,7 +60,7 @@ protected override Texture CreateTextureImpl(ulong offsetInBytes, TextureDesc de
SilkMarshal.GuidPtrOf(),
(void**)resource.GetAddressOf()).Success();
- return new DXTexture(Context, desc, resource);
+ return new DXTexture(Context, desc, resource, true);
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET.DirectX12/DXSwapChain.cs b/sources/Zenith.NET.DirectX12/DXSwapChain.cs
index c023f988..a75db726 100644
--- a/sources/Zenith.NET.DirectX12/DXSwapChain.cs
+++ b/sources/Zenith.NET.DirectX12/DXSwapChain.cs
@@ -112,7 +112,7 @@ private void CreateTextures()
ComPtr resource = new();
SwapChain.GetBuffer((uint)i, SilkMarshal.GuidPtrOf(), (void**)resource.GetAddressOf()).Success();
- textures[i] = new(Context, desc, resource);
+ textures[i] = new(Context, desc, resource, true);
}
index = SwapChain.GetCurrentBackBufferIndex();
diff --git a/sources/Zenith.NET.DirectX12/DXTexture.cs b/sources/Zenith.NET.DirectX12/DXTexture.cs
index ccb19acb..d5532720 100644
--- a/sources/Zenith.NET.DirectX12/DXTexture.cs
+++ b/sources/Zenith.NET.DirectX12/DXTexture.cs
@@ -11,6 +11,8 @@ internal unsafe class DXTexture : Texture
public ComPtr Resource;
+ public bool CanTransition;
+
public DXTexture(DXGraphicsContext context, TextureDesc desc) : base(context, desc)
{
ResourceDesc1 resourceDesc = ResourceDesc(desc);
@@ -28,6 +30,8 @@ public DXTexture(DXGraphicsContext context, TextureDesc desc) : base(context, de
SilkMarshal.GuidPtrOf(),
(void**)Resource.GetAddressOf()).Success();
+ CanTransition = true;
+
View = new(context, new()
{
Texture = this,
@@ -37,9 +41,10 @@ public DXTexture(DXGraphicsContext context, TextureDesc desc) : base(context, de
});
}
- public DXTexture(DXGraphicsContext context, TextureDesc desc, ComPtr resource) : base(context, desc)
+ public DXTexture(DXGraphicsContext context, TextureDesc desc, ComPtr resource, bool canTransition) : base(context, desc)
{
Resource = resource;
+ CanTransition = canTransition;
View = new(context, new()
{
@@ -211,7 +216,11 @@ public CpuDescriptorHandle GetDsvHandle(TextureSubresource subresource)
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12Resource => (nint)Resource.Handle,
+ _ => default
+ };
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET.DirectX12/DXTimeline.cs b/sources/Zenith.NET.DirectX12/DXTimeline.cs
index 07f6e77c..7e7f6d42 100644
--- a/sources/Zenith.NET.DirectX12/DXTimeline.cs
+++ b/sources/Zenith.NET.DirectX12/DXTimeline.cs
@@ -20,7 +20,11 @@ public DXTimeline(DXGraphicsContext context, DXCommandQueue queue) : base(contex
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12Fence => (nint)Fence.Handle,
+ _ => default
+ };
}
protected override ulong GetCompletedValue()
diff --git a/sources/Zenith.NET.DirectX12/DXTopLevelAccelerationStructure.cs b/sources/Zenith.NET.DirectX12/DXTopLevelAccelerationStructure.cs
index b32c76b7..b0771bda 100644
--- a/sources/Zenith.NET.DirectX12/DXTopLevelAccelerationStructure.cs
+++ b/sources/Zenith.NET.DirectX12/DXTopLevelAccelerationStructure.cs
@@ -82,7 +82,12 @@ public void Update(DXCommandBuffer commandBuffer, TopLevelAccelerationStructureD
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12GpuVirtualAddress => (nint)AccelerationStructure.GPUVirtualAddress,
+ NativeObjectType.D3D12Resource => (nint)AccelerationStructure.Resource.Handle,
+ _ => default
+ };
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET.Metal/MTLGraphicsContext.cs b/sources/Zenith.NET.Metal/MTLGraphicsContext.cs
index f2ecdd4d..5dfe9013 100644
--- a/sources/Zenith.NET.Metal/MTLGraphicsContext.cs
+++ b/sources/Zenith.NET.Metal/MTLGraphicsContext.cs
@@ -30,7 +30,11 @@ public void Unregister(MTLAllocation allocation)
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.MTLDevice => Device.NativePtr,
+ _ => default
+ };
}
protected override void Initialize(bool useValidationLayer,
diff --git a/sources/Zenith.NET.Metal/MTLTexture.cs b/sources/Zenith.NET.Metal/MTLTexture.cs
index 88cd42ca..68797dd7 100644
--- a/sources/Zenith.NET.Metal/MTLTexture.cs
+++ b/sources/Zenith.NET.Metal/MTLTexture.cs
@@ -42,7 +42,11 @@ public MTLTexture(MTLGraphicsContext context, TextureDesc desc, MtlTexture textu
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.MTLTexture => Texture.NativePtr,
+ _ => default
+ };
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET.Metal/MTLTimeline.cs b/sources/Zenith.NET.Metal/MTLTimeline.cs
index ca93362f..ce042a35 100644
--- a/sources/Zenith.NET.Metal/MTLTimeline.cs
+++ b/sources/Zenith.NET.Metal/MTLTimeline.cs
@@ -8,7 +8,11 @@ internal class MTLTimeline(MTLGraphicsContext context, MTLCommandQueue queue) :
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.MTLSharedEvent => Event.NativePtr,
+ _ => default
+ };
}
protected override ulong GetCompletedValue()
diff --git a/sources/Zenith.NET.Vulkan/VKBottomLevelAccelerationStructure.cs b/sources/Zenith.NET.Vulkan/VKBottomLevelAccelerationStructure.cs
index b542a751..3760d856 100644
--- a/sources/Zenith.NET.Vulkan/VKBottomLevelAccelerationStructure.cs
+++ b/sources/Zenith.NET.Vulkan/VKBottomLevelAccelerationStructure.cs
@@ -83,7 +83,12 @@ public void Update(VKCommandBuffer commandBuffer, BottomLevelAccelerationStructu
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanAccelerationStructure => (nint)AccelerationStructure.Handle,
+ NativeObjectType.VulkanDeviceAddress => (nint)DeviceAddress,
+ _ => default
+ };
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET.Vulkan/VKBuffer.cs b/sources/Zenith.NET.Vulkan/VKBuffer.cs
index 0370a18f..e982585a 100644
--- a/sources/Zenith.NET.Vulkan/VKBuffer.cs
+++ b/sources/Zenith.NET.Vulkan/VKBuffer.cs
@@ -152,7 +152,14 @@ public VKBuffer(VKGraphicsContext context, BufferDesc desc, VkBuffer buffer, VKA
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanBuffer => (nint)Buffer.Handle,
+ NativeObjectType.VulkanDeviceAddress => (nint)DeviceAddress,
+ NativeObjectType.VulkanDeviceMemory => (nint)Allocation.DeviceMemory.Handle,
+ NativeObjectType.VulkanDeviceMemoryOffset => (nint)Allocation.OffsetInBytes,
+ _ => default
+ };
}
public override nint Map()
diff --git a/sources/Zenith.NET.Vulkan/VKCommandBuffer.cs b/sources/Zenith.NET.Vulkan/VKCommandBuffer.cs
index 96dc0631..513f3e38 100644
--- a/sources/Zenith.NET.Vulkan/VKCommandBuffer.cs
+++ b/sources/Zenith.NET.Vulkan/VKCommandBuffer.cs
@@ -34,7 +34,11 @@ public VKCommandBuffer(VKGraphicsContext context, VKCommandQueue queue) : base(c
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanCommandBuffer => CommandBuffer.Handle,
+ _ => default
+ };
}
protected override void BarrierImpl(BarrierStages before, BarrierStages after)
diff --git a/sources/Zenith.NET.Vulkan/VKCommandQueue.cs b/sources/Zenith.NET.Vulkan/VKCommandQueue.cs
index 97fd5a42..edc102d4 100644
--- a/sources/Zenith.NET.Vulkan/VKCommandQueue.cs
+++ b/sources/Zenith.NET.Vulkan/VKCommandQueue.cs
@@ -22,7 +22,12 @@ public VKCommandQueue(VKGraphicsContext context, CommandQueueType type, Queue qu
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanQueue => Queue.Handle,
+ NativeObjectType.VulkanQueueFamilyIndex => (nint)QueueFamilyIndex,
+ _ => default
+ };
}
protected override CommandBuffer CreateCommandBuffer()
diff --git a/sources/Zenith.NET.Vulkan/VKFormats.cs b/sources/Zenith.NET.Vulkan/VKFormats.cs
index 948165f0..e7380972 100644
--- a/sources/Zenith.NET.Vulkan/VKFormats.cs
+++ b/sources/Zenith.NET.Vulkan/VKFormats.cs
@@ -61,8 +61,8 @@ public static (PipelineStageFlags2 Stage, AccessFlags2 Access) Vulkan(BarrierSta
if (barrierStages.HasFlag(BarrierStages.VertexShading))
{
- stage |= PipelineStageFlags2.IndexInputBit | PipelineStageFlags2.VertexAttributeInputBit | PipelineStageFlags2.VertexShaderBit | PipelineStageFlags2.DrawIndirectBit;
- access |= AccessFlags2.VertexAttributeReadBit | AccessFlags2.UniformReadBit | AccessFlags2.IndexReadBit | AccessFlags2.ShaderReadBit | AccessFlags2.ShaderWriteBit | AccessFlags2.IndirectCommandReadBit | AccessFlags2.AccelerationStructureReadBitKhr;
+ stage |= PipelineStageFlags2.DrawIndirectBit | PipelineStageFlags2.VertexShaderBit | PipelineStageFlags2.IndexInputBit | PipelineStageFlags2.VertexAttributeInputBit;
+ access |= AccessFlags2.IndirectCommandReadBit | AccessFlags2.IndexReadBit | AccessFlags2.VertexAttributeReadBit | AccessFlags2.UniformReadBit | AccessFlags2.ShaderReadBit | AccessFlags2.ShaderWriteBit | AccessFlags2.AccelerationStructureReadBitKhr;
}
if (barrierStages.HasFlag(BarrierStages.FragmentShading))
@@ -73,8 +73,8 @@ public static (PipelineStageFlags2 Stage, AccessFlags2 Access) Vulkan(BarrierSta
if (barrierStages.HasFlag(BarrierStages.ComputeShading))
{
- stage |= PipelineStageFlags2.ComputeShaderBit | PipelineStageFlags2.DrawIndirectBit;
- access |= AccessFlags2.UniformReadBit | AccessFlags2.ShaderReadBit | AccessFlags2.ShaderWriteBit | AccessFlags2.IndirectCommandReadBit | AccessFlags2.AccelerationStructureReadBitKhr;
+ stage |= PipelineStageFlags2.DrawIndirectBit | PipelineStageFlags2.ComputeShaderBit;
+ access |= AccessFlags2.IndirectCommandReadBit | AccessFlags2.UniformReadBit | AccessFlags2.ShaderReadBit | AccessFlags2.ShaderWriteBit | AccessFlags2.AccelerationStructureReadBitKhr;
}
if (barrierStages.HasFlag(BarrierStages.Copy))
diff --git a/sources/Zenith.NET.Vulkan/VKGraphicsContext.cs b/sources/Zenith.NET.Vulkan/VKGraphicsContext.cs
index 1b200450..c9eb5bc7 100644
--- a/sources/Zenith.NET.Vulkan/VKGraphicsContext.cs
+++ b/sources/Zenith.NET.Vulkan/VKGraphicsContext.cs
@@ -129,7 +129,15 @@ public uint FindMemoryTypeIndex(uint memoryTypeBits, MemoryResidency residency)
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanDevice => Device.Handle,
+ NativeObjectType.VulkanGetDeviceProcAddr => Vk.GetInstanceProcAddr(Instance, "vkGetDeviceProcAddr"),
+ NativeObjectType.VulkanGetInstanceProcAddr => Vk.GetInstanceProcAddr(default, "vkGetInstanceProcAddr"),
+ NativeObjectType.VulkanInstance => Instance.Handle,
+ NativeObjectType.VulkanPhysicalDevice => PhysicalDevice.Handle,
+ _ => default
+ };
}
protected override void Initialize(bool useValidationLayer,
diff --git a/sources/Zenith.NET.Vulkan/VKHeap.cs b/sources/Zenith.NET.Vulkan/VKHeap.cs
index 8971ba17..371d7556 100644
--- a/sources/Zenith.NET.Vulkan/VKHeap.cs
+++ b/sources/Zenith.NET.Vulkan/VKHeap.cs
@@ -25,7 +25,11 @@ public VKHeap(VKGraphicsContext context, HeapDesc desc) : base(context, desc)
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanDeviceMemory => (nint)DeviceMemory.Handle,
+ _ => default
+ };
}
protected override Buffer CreateBufferImpl(ulong offsetInBytes, BufferDesc desc)
diff --git a/sources/Zenith.NET.Vulkan/VKTexture.cs b/sources/Zenith.NET.Vulkan/VKTexture.cs
index 88945368..2eabdcb6 100644
--- a/sources/Zenith.NET.Vulkan/VKTexture.cs
+++ b/sources/Zenith.NET.Vulkan/VKTexture.cs
@@ -78,7 +78,13 @@ public VKTexture(VKGraphicsContext context, TextureDesc desc, Image image, VKAll
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanDeviceMemory => (nint)Allocation.DeviceMemory.Handle,
+ NativeObjectType.VulkanDeviceMemoryOffset => (nint)Allocation.OffsetInBytes,
+ NativeObjectType.VulkanImage => (nint)Image.Handle,
+ _ => default
+ };
}
public ImageView GetAttachmentView(TextureSubresource subresource)
diff --git a/sources/Zenith.NET.Vulkan/VKTimeline.cs b/sources/Zenith.NET.Vulkan/VKTimeline.cs
index 578fc051..a7379bd4 100644
--- a/sources/Zenith.NET.Vulkan/VKTimeline.cs
+++ b/sources/Zenith.NET.Vulkan/VKTimeline.cs
@@ -21,7 +21,11 @@ public VKTimeline(VKGraphicsContext context, VKCommandQueue queue) : base(contex
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanSemaphore => (nint)Semaphore.Handle,
+ _ => default
+ };
}
protected override ulong GetCompletedValue()
diff --git a/sources/Zenith.NET.Vulkan/VKTopLevelAccelerationStructure.cs b/sources/Zenith.NET.Vulkan/VKTopLevelAccelerationStructure.cs
index cbd0eb80..f81304c5 100644
--- a/sources/Zenith.NET.Vulkan/VKTopLevelAccelerationStructure.cs
+++ b/sources/Zenith.NET.Vulkan/VKTopLevelAccelerationStructure.cs
@@ -100,7 +100,11 @@ public void Update(VKCommandBuffer commandBuffer, TopLevelAccelerationStructureD
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanAccelerationStructure => (nint)AccelerationStructure.Handle,
+ _ => default
+ };
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET/Enums/NativeObjectType.cs b/sources/Zenith.NET/Enums/NativeObjectType.cs
index 1872e9d6..a7ac7219 100644
--- a/sources/Zenith.NET/Enums/NativeObjectType.cs
+++ b/sources/Zenith.NET/Enums/NativeObjectType.cs
@@ -2,4 +2,53 @@
public enum NativeObjectType
{
+ D3D12Adapter,
+
+ D3D12CommandQueue,
+
+ D3D12Device,
+
+ D3D12Fence,
+
+ D3D12GpuVirtualAddress,
+
+ D3D12GraphicsCommandList,
+
+ D3D12Resource,
+
+ MTLDevice,
+
+ MTLSharedEvent,
+
+ MTLTexture,
+
+ VulkanAccelerationStructure,
+
+ VulkanBuffer,
+
+ VulkanCommandBuffer,
+
+ VulkanDevice,
+
+ VulkanDeviceAddress,
+
+ VulkanDeviceMemory,
+
+ VulkanDeviceMemoryOffset,
+
+ VulkanGetDeviceProcAddr,
+
+ VulkanGetInstanceProcAddr,
+
+ VulkanImage,
+
+ VulkanInstance,
+
+ VulkanPhysicalDevice,
+
+ VulkanQueue,
+
+ VulkanQueueFamilyIndex,
+
+ VulkanSemaphore
}