diff --git a/Beryl.RHI/Beryl.RHI.csproj b/Beryl.RHI/Beryl.RHI.csproj
index dc88b8b..319dcb2 100644
--- a/Beryl.RHI/Beryl.RHI.csproj
+++ b/Beryl.RHI/Beryl.RHI.csproj
@@ -9,7 +9,9 @@
-
+
+
+
diff --git a/Beryl.RHI/GraphicsBackendSupport.cs b/Beryl.RHI/GraphicsBackendSupport.cs
new file mode 100644
index 0000000..c6a2163
--- /dev/null
+++ b/Beryl.RHI/GraphicsBackendSupport.cs
@@ -0,0 +1,115 @@
+// This file is part of the Beryl Game Engine.
+// Licensed under the MIT license. (https://github.com/Beryl-Engine/Beryl/blob/main/LICENSE)
+
+using Vortice.Direct3D;
+using Vortice.Direct3D12;
+using Vortice.DXGI;
+using Vortice.Vulkan;
+
+namespace Beryl.RHI;
+
+internal static class GraphicsBackendSupport
+{
+ public static bool IsDirect3D12Supported()
+ {
+ if (DXGI.CreateDXGIFactory1(out var factory).Failure || factory == null)
+ return false;
+
+ using (factory)
+ {
+ for (uint index = 0; factory.EnumAdapters1(index, out var adapter).Success; index++)
+ {
+ using (adapter)
+ {
+ if ((adapter.Description1.Flags & AdapterFlags.Software) == 0 && D3D12.IsSupported(adapter, FeatureLevel.Level_11_0))
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public static unsafe bool IsVulkanSupported()
+ {
+ if (Vulkan.vkInitialize() != VkResult.Success || Vulkan.vkEnumerateInstanceVersion() < VkVersion.Version_1_1)
+ return false;
+
+ if (Vulkan.vkEnumerateInstanceExtensionProperties(out uint extensionCount) != VkResult.Success || extensionCount == 0)
+ return false;
+
+ VkExtensionProperties[] extensions = new VkExtensionProperties[extensionCount];
+ if (Vulkan.vkEnumerateInstanceExtensionProperties(extensions) != VkResult.Success ||
+ !HasExtension(extensions, "VK_KHR_surface\0"u8) ||
+ !(OperatingSystem.IsWindows()
+ ? HasExtension(extensions, "VK_KHR_win32_surface\0"u8)
+ : HasExtension(extensions, "VK_KHR_xlib_surface\0"u8) || HasExtension(extensions, "VK_KHR_wayland_surface\0"u8)))
+ {
+ return false;
+ }
+
+ VkApplicationInfo application = new() { apiVersion = VkVersion.Version_1_1 };
+ VkInstanceCreateInfo description = new() { pApplicationInfo = &application };
+ if (Vulkan.vkCreateInstance(in description, out VkInstance instance) != VkResult.Success)
+ return false;
+
+ var api = new VkInstanceApi(in instance);
+ try
+ {
+ if (api.vkEnumeratePhysicalDevices(out uint deviceCount) != VkResult.Success || deviceCount == 0)
+ return false;
+
+ VkPhysicalDevice[] devices = new VkPhysicalDevice[deviceCount];
+ if (api.vkEnumeratePhysicalDevices(devices) != VkResult.Success)
+ return false;
+
+ foreach (var device in devices)
+ {
+ if (device.IsNotNull && api.vkGetPhysicalDeviceProperties(device).apiVersion >= VkVersion.Version_1_1 && HasRequiredQueue(api, device) && HasSwapchainSupport(api, device))
+ return true;
+ }
+ return false;
+ }
+ finally
+ {
+ api.vkDestroyInstance();
+ }
+ }
+
+ private static bool HasRequiredQueue(VkInstanceApi api, VkPhysicalDevice device)
+ {
+ api.vkGetPhysicalDeviceQueueFamilyProperties(device, out uint count);
+ VkQueueFamilyProperties[] queues = new VkQueueFamilyProperties[count];
+
+ api.vkGetPhysicalDeviceQueueFamilyProperties(device, queues);
+
+ const VkQueueFlags required = VkQueueFlags.Graphics | VkQueueFlags.Compute;
+ foreach (var queue in queues)
+ {
+ if (queue.queueCount > 0 && (queue.queueFlags & required) == required)
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool HasSwapchainSupport(VkInstanceApi api, VkPhysicalDevice device)
+ {
+ if (api.vkEnumerateDeviceExtensionProperties(device, out uint count) != VkResult.Success || count == 0)
+ return false;
+
+ VkExtensionProperties[] extensions = new VkExtensionProperties[count];
+ return api.vkEnumerateDeviceExtensionProperties(device, extensions) == VkResult.Success && HasExtension(extensions, "VK_KHR_swapchain\0"u8);
+ }
+
+ private static unsafe bool HasExtension(ReadOnlySpan extensions, ReadOnlySpan name)
+ {
+ foreach (var extension in extensions)
+ {
+ if (new ReadOnlySpan(extension.extensionName, 256).StartsWith(name))
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/Beryl.RHI/GraphicsDeviceFactory.cs b/Beryl.RHI/GraphicsDeviceFactory.cs
index 3ab6b21..f2a6804 100644
--- a/Beryl.RHI/GraphicsDeviceFactory.cs
+++ b/Beryl.RHI/GraphicsDeviceFactory.cs
@@ -8,14 +8,24 @@ namespace Beryl.RHI;
///
public static class GraphicsDeviceFactory
{
- /// Determines whether is supported.
- public static bool IsBackendSupported(RendererBackend backend) => backend switch
+ /// Checks platform, runtime and driver support for . Window presentation is validated during device initialization.
+ public static bool IsBackendSupported(RendererBackend backend)
{
- RendererBackend.Vulkan => Veldrith.GraphicsDevice.IsBackendSupported(Veldrith.GraphicsBackend.Vulkan),
- RendererBackend.Direct3D12 => Veldrith.GraphicsDevice.IsBackendSupported(Veldrith.GraphicsBackend.Direct3D12),
- _ => false
- };
+ try
+ {
+ return backend switch
+ {
+ RendererBackend.Vulkan => (OperatingSystem.IsWindows() || OperatingSystem.IsLinux()) && GraphicsBackendSupport.IsVulkanSupported(),
+ RendererBackend.Direct3D12 => OperatingSystem.IsWindowsVersionAtLeast(10) && GraphicsBackendSupport.IsDirect3D12Supported(),
+ _ => false
+ };
+ }
+ catch (Exception exception) when (exception is DllNotFoundException or EntryPointNotFoundException or BadImageFormatException or TypeInitializationException or SharpGen.Runtime.SharpGenException)
+ {
+ return false;
+ }
+ }
/// Creates a graphics device for .
- public static IGraphicsDevice Create(RendererBackend backend) => new VeldrithBackend.VeldrithDevice(backend);
+ public static IGraphicsDevice Create(RendererBackend backend) => new WebGPUBackend.WebGPUDevice(backend);
}
diff --git a/Beryl.RHI/VeldrithBackend/VeldrithCommandBuffer.cs b/Beryl.RHI/VeldrithBackend/VeldrithCommandBuffer.cs
deleted file mode 100644
index 3500f8d..0000000
--- a/Beryl.RHI/VeldrithBackend/VeldrithCommandBuffer.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-// This file is part of the Beryl Game Engine.
-// Licensed under the MIT license. (https://github.com/Beryl-Engine/Beryl/blob/main/LICENSE)
-
-using Beryl.Math;
-using Beryl.RHI.Resources;
-using Veldrith;
-
-namespace Beryl.RHI.VeldrithBackend;
-
-///
-/// Veldrith-based implementation of .
-///
-internal sealed class VeldrithCommandBuffer : ICommandBuffer
-{
- public CommandList CommandList { get; }
-
- private IFramebuffer? target;
-
- ///
- public IFramebuffer? CurrentFramebuffer => target;
-
- /// Initializes a new instance of the class.
- internal VeldrithCommandBuffer(VeldrithDevice device) => CommandList = device.GraphicsDevice.ResourceFactory.CreateCommandList();
-
- ///
- public void Dispose() => CommandList.Dispose();
-
- ///
- public void Begin() => CommandList.Begin();
-
- ///
- public void End() => CommandList.End();
-
- ///
- public void SetPipeline(IPipeline pipeline) => CommandList.SetPipeline(((VeldrithPipeline)pipeline).Resource);
-
- ///
- public void SetComputePipeline(IComputePipeline pipeline) => CommandList.SetPipeline(((VeldrithPipeline)pipeline).Resource);
-
- ///
- public void SetVertexBuffer(uint slot, IBuffer buffer) => CommandList.SetVertexBuffer(slot, ((VeldrithBuffer)buffer).Resource);
-
- ///
- public void SetIndexBuffer(IBuffer buffer) => CommandList.SetIndexBuffer(((VeldrithBuffer)buffer).Resource, IndexFormat.UInt32);
-
- ///
- public void SetGraphicsResourceSet(uint slot, IResourceSet resourceSet) => CommandList.SetGraphicsResourceSet(slot, ((VeldrithResourceSet)resourceSet).Resource);
-
- ///
- public void SetComputeResourceSet(uint slot, IResourceSet resourceSet) => CommandList.SetComputeResourceSet(slot, ((VeldrithResourceSet)resourceSet).Resource);
-
- ///
- public void UpdateBuffer(IBuffer buffer, uint offset, ReadOnlySpan data) => CommandList.UpdateBuffer(((VeldrithBuffer)buffer).Resource, offset, data);
-
- ///
- public void DrawIndexed(uint indexCount) => CommandList.DrawIndexed(indexCount);
-
- ///
- public void Dispatch(uint x, uint y, uint z) => CommandList.Dispatch(x, y, z);
-
- ///
- public void SetFrameBuffer(IFramebuffer frameBuffer)
- {
- target = frameBuffer;
- CommandList.SetFramebuffer(((VeldrithFramebuffer)frameBuffer).Framebuffer);
- }
-
- ///
- public void ClearDepth(float depth) => CommandList.ClearDepthStencil(depth);
-
- ///
- public void ClearColor(Color color) => CommandList.ClearColorTarget(0, new RgbaFloat(color.R, color.G, color.B, color.A));
-}
diff --git a/Beryl.RHI/VeldrithBackend/VeldrithDevice.cs b/Beryl.RHI/VeldrithBackend/VeldrithDevice.cs
deleted file mode 100644
index 27916fb..0000000
--- a/Beryl.RHI/VeldrithBackend/VeldrithDevice.cs
+++ /dev/null
@@ -1,197 +0,0 @@
-// This file is part of the Beryl Game Engine.
-// Licensed under the MIT license. (https://github.com/Beryl-Engine/Beryl/blob/main/LICENSE)
-
-using Beryl.Common.Utility;
-using Beryl.RHI.Resources;
-using Silk.NET.Core.Contexts;
-using Silk.NET.Windowing;
-
-using Veldrith;
-using Vortice.Vulkan;
-
-namespace Beryl.RHI.VeldrithBackend;
-
-///
-/// Interface into low-level Veldrith rendering.
-///
-internal sealed class VeldrithDevice : IGraphicsDevice
-{
- /// The underlying Veldrith .
- public GraphicsDevice GraphicsDevice { get; private set; } = null!;
-
- ///
- public VulkanInfo? VulkanInfo
- {
- get
- {
- Veldrith.BackendInfoVulkan backend = GraphicsDevice.GetVulkanInfo();
- return new VulkanInfo()
- {
- Instance = backend.Instance,
- Device = backend.Device,
- PhysicalDevice = backend.PhysicalDevice,
- GraphicsQueueFamilyIndex = backend.GraphicsQueueFamilyIndex
- };
- }
- }
-
- ///
- public DeviceFeatures Features
- {
- get
- {
- DeviceFeatures features = DeviceFeatures.None;
-
- if (GraphicsDevice.IsClipSpaceYInverted)
- features |= DeviceFeatures.ClipSpaceYInverted;
-
- return features;
- }
- }
-
- ///
- public IFramebuffer SwapchainFramebuffer { get; private set; }
-
- ///
- public IResourceFactory ResourceFactory { get; }
-
- private readonly RendererBackend backend;
-
- /// Initializes a new instance of the class.
- public VeldrithDevice(RendererBackend backend)
- {
- this.backend = backend;
- ResourceFactory = new VeldrithResourceFactory(this);
- SwapchainFramebuffer = null!;
- }
-
- ///
- public void Initialize(IWindow window)
- {
- var native = window.Native;
-
- var options = new GraphicsDeviceOptions()
- {
-#if DEBUG
- Debug = true,
-#endif
- SyncToVerticalBlank = window.VSync,
- };
-
- if (native is null)
- throw new NullReferenceException(nameof(native));
-
- SwapchainSource source = CreateSwapchainSource(native);
-
- SwapchainDescription desc = new(source, (uint)window.Size.X, (uint)window.Size.Y, PixelFormat.D32FloatS8UInt, window.VSync);
-
- switch (backend)
- {
- case RendererBackend.Vulkan:
- GraphicsDevice = GraphicsDevice.CreateVulkan(options, desc);
- break;
- case RendererBackend.Direct3D12:
- GraphicsDevice = GraphicsDevice.CreateD3D12(options, desc);
- break;
- }
-
- CheckDebugTools(backend);
- SwapchainFramebuffer = new VeldrithFramebuffer(GraphicsDevice.SwapchainFramebuffer);
- }
-
-
- ///
- public void SubmitCommands(ICommandBuffer commandBuffer)
- {
- var buffer = (VeldrithCommandBuffer)commandBuffer;
- GraphicsDevice.SubmitCommands(buffer.CommandList);
- }
-
- ///
- public void ResizeSwapchain(uint width, uint height) => GraphicsDevice.MainSwapchain.Resize(width, height);
-
- ///
- public void UpdateBuffer(IBuffer buffer, uint offset, ReadOnlySpan data) where T : unmanaged => GraphicsDevice.UpdateBuffer(((VeldrithBuffer)buffer).Resource, offset, data);
-
- ///
- public void SwapBuffers() => GraphicsDevice.SwapBuffers();
-
- ///
- public void Dispose() => GraphicsDevice.Dispose();
-
- ///
- public ICommandBuffer GetCommandBuffer() => new VeldrithCommandBuffer(this);
-
- /// Checks if the debug tools are available for the given .
- public static unsafe bool CheckDebugTools(RendererBackend backend)
- {
- if (backend == RendererBackend.Vulkan)
- {
- bool hasValidation = false;
- const string targetLayer = "VK_LAYER_KHRONOS_validation";
-
- uint layerCount = 0;
- VkResult result = Vulkan.vkEnumerateInstanceLayerProperties(&layerCount, null);
-
- if (result == VkResult.Success && layerCount > 0)
- {
- VkLayerProperties* availableLayers = stackalloc VkLayerProperties[(int)layerCount];
- result = Vulkan.vkEnumerateInstanceLayerProperties(&layerCount, availableLayers);
-
- if (result == VkResult.Success)
- {
- for (int i = 0; i < layerCount; i++)
- {
- byte* namePtr = availableLayers[i].layerName;
-
- string currentLayerName = System.Text.Encoding.UTF8.GetString(namePtr, GetNullTerminatedLength(namePtr, 256));
-
- if (currentLayerName == targetLayer)
- {
- hasValidation = true;
- break;
- }
- }
- }
- }
-
- if (!hasValidation)
- BerylConsole.Warning("Rendering debugging is enabled but Vulkan validation layers are not currently available, debugging capabilities will be limited. (https://vulkan.lunarg.com/sdk/home)");
-
- return hasValidation;
- }
-
- if (backend == RendererBackend.Direct3D12)
- {
- BerylConsole.Warning("Direct3D12 debug tools are not currently available.");
- return false;
- }
-
- return false;
- }
-
- private static unsafe int GetNullTerminatedLength(byte* ptr, int maxLength)
- {
- int length = 0;
- while (length < maxLength && ptr[length] != 0)
- length++;
-
- return length;
- }
-
- private static SwapchainSource CreateSwapchainSource(INativeWindow? native)
- {
- ArgumentNullException.ThrowIfNull(native);
-
- if (native.Win32 is { } win32)
- return SwapchainSource.CreateWin32(win32.Hwnd, win32.HInstance);
-
- if (native.X11 is { } x11)
- return SwapchainSource.CreateXlib(x11.Display, (nint)x11.Window);
-
- if (native.Wayland is { } wayland)
- return SwapchainSource.CreateWayland(wayland.Display, wayland.Surface);
-
- throw new PlatformNotSupportedException("No supported native window handle was available.");
- }
-}
diff --git a/Beryl.RHI/VeldrithBackend/VeldrithResourceFactory.cs b/Beryl.RHI/VeldrithBackend/VeldrithResourceFactory.cs
deleted file mode 100644
index 085bb9b..0000000
--- a/Beryl.RHI/VeldrithBackend/VeldrithResourceFactory.cs
+++ /dev/null
@@ -1,209 +0,0 @@
-// This file is part of the Beryl Game Engine.
-// Licensed under the MIT license. (https://github.com/Beryl-Engine/Beryl/blob/main/LICENSE)
-
-using Beryl.RHI.Resources;
-using Veldrith;
-
-namespace Beryl.RHI.VeldrithBackend;
-
-internal sealed class VeldrithResourceFactory(VeldrithDevice device) : IResourceFactory
-{
- ///
- public ICommandBuffer CreateCommandBuffer() => new VeldrithCommandBuffer(device);
-
- ///
- public IShader CreateShader(ShaderDescriptor key)
- {
- Veldrith.ShaderStages stage = key.Stage switch
- {
- Resources.ShaderStages.Vertex => Veldrith.ShaderStages.Vertex,
- Resources.ShaderStages.Fragment => Veldrith.ShaderStages.Fragment,
- Resources.ShaderStages.Compute => Veldrith.ShaderStages.Compute,
- _ => throw new NotSupportedException($"Shader stage '{key.Stage}' is not supported.")
- };
-
- var description = new ShaderDescription(stage, key.Bytecode.ToArray(), "main");
- return new VeldrithResources(device.GraphicsDevice.ResourceFactory.CreateShader(description));
- }
-
- ///
- public IBuffer CreateBuffer(BufferDescriptor key)
- {
- var description = new BufferDescription(key.Size, ToVeldrith(key.Usage));
- return new VeldrithBuffer(device.GraphicsDevice.ResourceFactory.CreateBuffer(description));
- }
-
- ///
- public ITexture CreateTexture(TextureDescriptor key)
- {
- var description = new TextureDescription(
- key.Width,
- key.Height,
- 1,
- 1,
- 1,
- PixelFormat.R8G8B8A8UNorm,
- TextureUsage.Sampled,
- TextureType.Texture2D);
-
- Texture texture = device.GraphicsDevice.ResourceFactory.CreateTexture(description);
-
- device.GraphicsDevice.UpdateTexture(texture, key.Data, 0, 0, 0, key.Width, key.Height, 1, 0, 0);
- return new VeldrithTexture(texture);
- }
-
- ///
- public ITextureView CreateTextureView(TextureViewDescriptor key)
- {
- TextureView textureView = device.GraphicsDevice.ResourceFactory.CreateTextureView(((VeldrithTexture)key.Texture).Resource);
- return new VeldrithTextureView(textureView);
- }
-
- ///
- public ISampler CreateSampler(SamplerDescriptor key)
- {
- var description = new SamplerDescription
- {
- Filter = key.Sampler.Filter == Resources.SamplerFilter.Nearest
- ? Veldrith.SamplerFilter.MinPointMagPointMipPoint
- : Veldrith.SamplerFilter.MinLinearMagLinearMipPoint,
- AddressModeU = key.Sampler.WrapU == TextureWrapMode.Clamp
- ? Veldrith.SamplerAddressMode.Clamp
- : Veldrith.SamplerAddressMode.Wrap,
- AddressModeV = key.Sampler.WrapV == TextureWrapMode.Clamp
- ? Veldrith.SamplerAddressMode.Clamp
- : Veldrith.SamplerAddressMode.Wrap
- };
-
- return new VeldrithSampler(device.GraphicsDevice.ResourceFactory.CreateSampler(description));
- }
-
- ///
- public IResourceLayout CreateResourceLayout(ResourceLayoutDescriptor key)
- {
- Veldrith.ResourceLayoutElementDescription[] elements = key.Elements.ToArray()
- .Select(element => new Veldrith.ResourceLayoutElementDescription(
- element.Name,
- Veldrith.ResourceKind.UniformBuffer,
- ToVeldrith(element.Stages)))
- .ToArray();
- var description = new ResourceLayoutDescription(elements);
-
- return new VeldrithResourceLayout(device.GraphicsDevice.ResourceFactory.CreateResourceLayout(description));
- }
-
- ///
- public IResourceSet CreateResourceSet(ResourceDescriptor key)
- {
- var description = new ResourceSetDescription(
- ((VeldrithResourceLayout)key.Layout).Resource,
- key.BoundResources.ToArray().Select(ToVeldrith).ToArray());
-
- return new VeldrithResourceSet(device.GraphicsDevice.ResourceFactory.CreateResourceSet(description));
- }
-
- ///
- public IPipeline CreateGraphicsPipeline(PipelineDescriptor key)
- {
- ResourceLayout[] layouts = key.ResourceGroups.ToArray().Select(group => ((VeldrithResourceLayout)CreateResourceLayout(new ResourceLayoutDescriptor(group.LayoutElements.AsSpan()))).Resource).ToArray();
-
- var vertexLayout = new VertexLayoutDescription(
- new VertexElementDescription("POSITION", VertexElementSemantic.Position, VertexElementFormat.Float3),
- new VertexElementDescription("NORMAL", VertexElementSemantic.Normal, VertexElementFormat.Float3),
- new VertexElementDescription("TEXCOORD0", VertexElementSemantic.TextureCoordinate, VertexElementFormat.Float2));
-
- var shaderSet = new ShaderSetDescription([vertexLayout], [((VeldrithResources)CreateShader(key.VertShader)).Resource, ((VeldrithResources)CreateShader(key.FragShader)).Resource]);
-
- var description = new GraphicsPipelineDescription
- {
- BlendState = BlendStateDescription.SINGLE_OVERRIDE_BLEND,
- DepthStencilState = new DepthStencilStateDescription(true, key.DepthInfo.WriteDepth, ToVeldrith(key.DepthInfo.DepthComparison)),
- RasterizerState = ToVeldrith(key.CullingMode),
- PrimitiveTopology = PrimitiveTopology.TriangleList,
- ShaderSet = shaderSet,
- ResourceLayouts = layouts,
- Outputs = ((VeldrithFramebuffer)key.Output).Framebuffer.OutputDescription
- };
-
- return new VeldrithPipeline(device.GraphicsDevice.ResourceFactory.CreateGraphicsPipeline(ref description));
- }
-
- ///
- public IComputePipeline CreateComputePipeline(ComputePipelineDescriptor key)
- {
- ResourceLayout[] layouts = key.ResourceGroups.ToArray().Select(group => ((VeldrithResourceLayout)CreateResourceLayout(new ResourceLayoutDescriptor(group.LayoutElements.AsSpan()))).Resource).ToArray();
-
- var description = new ComputePipelineDescription(
- ((VeldrithResources)CreateShader(key.Shader)).Resource,
- layouts,
- 1,
- 1,
- 1);
-
- return new VeldrithPipeline(device.GraphicsDevice.ResourceFactory.CreateComputePipeline(ref description));
- }
-
- private static Veldrith.BufferUsage ToVeldrith(Resources.BufferUsage usage)
- {
- Veldrith.BufferUsage result = 0;
-
- if (usage.HasFlag(Resources.BufferUsage.VertexBuffer))
- result |= Veldrith.BufferUsage.VertexBuffer;
-
- if (usage.HasFlag(Resources.BufferUsage.IndexBuffer))
- result |= Veldrith.BufferUsage.IndexBuffer;
-
- if (usage.HasFlag(Resources.BufferUsage.UniformBuffer))
- result |= Veldrith.BufferUsage.UniformBuffer;
-
- if (usage.HasFlag(Resources.BufferUsage.Dynamic))
- result |= Veldrith.BufferUsage.Dynamic;
-
- return result;
- }
-
- private static Veldrith.ShaderStages ToVeldrith(Resources.ShaderStages stages)
- {
- Veldrith.ShaderStages result = 0;
-
- if (stages.HasFlag(Resources.ShaderStages.Vertex))
- result |= Veldrith.ShaderStages.Vertex;
-
- if (stages.HasFlag(Resources.ShaderStages.Fragment))
- result |= Veldrith.ShaderStages.Fragment;
-
- if (stages.HasFlag(Resources.ShaderStages.Compute))
- result |= Veldrith.ShaderStages.Compute;
-
- return result;
- }
-
- private static RasterizerStateDescription ToVeldrith(CullingMode culling) => culling switch
- {
- CullingMode.Back => new RasterizerStateDescription(FaceCullMode.Back, PolygonFillMode.Solid, FrontFace.Clockwise, true, true),
- CullingMode.Front => new RasterizerStateDescription(FaceCullMode.Front, PolygonFillMode.Solid, FrontFace.Clockwise, true, true),
- _ => RasterizerStateDescription.CULL_NONE
- };
-
- private static ComparisonKind ToVeldrith(ComparisonMode comparison) => comparison switch
- {
- ComparisonMode.Always => ComparisonKind.Always,
- ComparisonMode.Less => ComparisonKind.Less,
- ComparisonMode.LessOrEqual => ComparisonKind.LessEqual,
- ComparisonMode.Equal => ComparisonKind.Equal,
- ComparisonMode.NotEqual => ComparisonKind.NotEqual,
- ComparisonMode.GreaterOrEqual => ComparisonKind.GreaterEqual,
- ComparisonMode.Greater => ComparisonKind.Greater,
- ComparisonMode.Never => ComparisonKind.Never,
- _ => ComparisonKind.Never
- };
-
- private static Veldrith.IBindableResource ToVeldrith(Resources.IBindableResource resource) => resource switch
- {
- VeldrithBuffer buffer => buffer.Resource,
- VeldrithTexture texture => texture.Resource,
- VeldrithTextureView view => view.Resource,
- VeldrithSampler sampler => sampler.Resource,
- _ => throw new NotSupportedException($"Unsupported Veldrith resource type '{resource.GetType().Name}'.")
- };
-}
diff --git a/Beryl.RHI/VeldrithBackend/VeldrithResources.cs b/Beryl.RHI/VeldrithBackend/VeldrithResources.cs
deleted file mode 100644
index 1e5f0b2..0000000
--- a/Beryl.RHI/VeldrithBackend/VeldrithResources.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-// This file is part of the Beryl Game Engine.
-// Licensed under the MIT license. (https://github.com/Beryl-Engine/Beryl/blob/main/LICENSE)
-
-using Beryl.RHI.Resources;
-using Veldrith;
-
-namespace Beryl.RHI.VeldrithBackend;
-
-internal sealed class VeldrithFramebuffer(Framebuffer framebuffer) : IFramebuffer
-{
- internal Framebuffer Framebuffer { get; } = framebuffer;
-
- ///
- public void Dispose() => Framebuffer.Dispose();
-}
-
-
-internal sealed class VeldrithResources(Shader resource) : IShader
-{
- internal Shader Resource { get; } = resource;
-
- ///
- public void Dispose() => Resource.Dispose();
-}
-
-internal sealed class VeldrithBuffer(DeviceBuffer resource) : IBuffer
-{
- internal DeviceBuffer Resource { get; } = resource;
-
- ///
- public uint SizeInBytes => Resource.SizeInBytes;
-
- ///
- public void Dispose() => Resource.Dispose();
-}
-
-internal sealed class VeldrithTexture(Texture resource) : ITexture
-{
- internal Texture Resource { get; } = resource;
-
- ///
- public void Dispose() => Resource.Dispose();
-}
-
-internal sealed class VeldrithTextureView(TextureView resource) : ITextureView
-{
- internal TextureView Resource { get; } = resource;
-
- ///
- public void Dispose() => Resource.Dispose();
-}
-
-internal sealed class VeldrithSampler(global::Veldrith.Sampler resource) : ISampler
-{
- internal global::Veldrith.Sampler Resource { get; } = resource;
-
- ///
- public void Dispose() => Resource.Dispose();
-}
-
-internal sealed class VeldrithResourceLayout(ResourceLayout resource) : IResourceLayout
-{
- internal ResourceLayout Resource { get; } = resource;
-
- ///
- public void Dispose() => Resource.Dispose();
-}
-
-internal sealed class VeldrithResourceSet(ResourceSet resource) : IResourceSet
-{
- internal ResourceSet Resource { get; } = resource;
-
- ///
- public void Dispose() => Resource.Dispose();
-}
-
-internal sealed class VeldrithPipeline(Pipeline resource) : IPipeline, IComputePipeline
-{
- internal Pipeline Resource { get; } = resource;
-
- ///
- public void Dispose() => Resource.Dispose();
-}
diff --git a/Beryl.RHI/WebGPUBackend/WebGPUCommandBuffer.cs b/Beryl.RHI/WebGPUBackend/WebGPUCommandBuffer.cs
new file mode 100644
index 0000000..1e6207b
--- /dev/null
+++ b/Beryl.RHI/WebGPUBackend/WebGPUCommandBuffer.cs
@@ -0,0 +1,338 @@
+// This file is part of the Beryl Game Engine.
+// Licensed under the MIT license. (https://github.com/Beryl-Engine/Beryl/blob/main/LICENSE)
+
+using Beryl.RHI.Resources;
+using WebGpuSharp;
+
+namespace Beryl.RHI.WebGPUBackend;
+
+internal sealed class WebGPUCommandBuffer(WebGPUDevice device) : ICommandBuffer
+{
+ private CommandEncoder encoder;
+ private WebGpuSharp.CommandBuffer commands;
+ private RenderPassEncoder renderPass;
+ private ComputePassEncoder computePass;
+ private bool rendering;
+ private bool computing;
+ private bool recording;
+ private bool finished;
+ private bool disposed;
+ private WebGPUFramebuffer? target;
+ private WebGPUPipeline? pipeline;
+ private WebGPUComputePipeline? computePipeline;
+ private WebGPUBuffer? indexBuffer;
+ private readonly Dictionary vertexBuffers = [];
+ private readonly Dictionary graphicsSets = [];
+ private readonly Dictionary computeSets = [];
+ private readonly List uploadBuffers = [];
+ private int uploadBufferIndex;
+
+ ///
+ public IFramebuffer? CurrentFramebuffer => target;
+
+ ///
+ public void Begin()
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+ if (recording)
+ throw new InvalidOperationException("The command buffer is already recording.");
+
+ commands = default;
+ finished = false;
+ target = null;
+ pipeline = null;
+ computePipeline = null;
+ indexBuffer = null;
+ vertexBuffers.Clear();
+ graphicsSets.Clear();
+ computeSets.Clear();
+ uploadBufferIndex = 0;
+
+ foreach (var upload in uploadBuffers)
+ upload.Length = 0;
+
+ encoder = device.Device.CreateCommandEncoder();
+ recording = true;
+ }
+
+ ///
+ public void End()
+ {
+ EnsureRecording();
+ EndPass();
+
+ commands = encoder.Finish();
+ encoder = default;
+ recording = false;
+ finished = true;
+ }
+
+ internal void Submit()
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+ if (!finished)
+ throw new InvalidOperationException("End the command buffer before submitting it.");
+
+ foreach (var upload in uploadBuffers)
+ {
+ if (upload.Length != 0)
+ device.Queue.WriteBuffer(upload.Resource, 0, upload.Data.AsSpan(0, upload.Length));
+ }
+
+ device.Queue.Submit(commands);
+ commands = default;
+ finished = false;
+ }
+
+ ///
+ public void SetFrameBuffer(IFramebuffer frameBuffer)
+ {
+ EnsureRecording();
+ EndPass();
+
+ target = (WebGPUFramebuffer)frameBuffer;
+ }
+
+ ///
+ public void SetPipeline(IPipeline pipeline)
+ {
+ EnsureRecording();
+
+ this.pipeline = (WebGPUPipeline)pipeline;
+ if (rendering)
+ renderPass.SetPipeline(this.pipeline.Resource);
+ }
+
+ ///
+ public void SetComputePipeline(IComputePipeline pipeline)
+ {
+ EnsureRecording();
+
+ computePipeline = (WebGPUComputePipeline)pipeline;
+ if (computing)
+ computePass.SetPipeline(computePipeline.Resource);
+ }
+
+ ///
+ public void SetVertexBuffer(uint slot, IBuffer buffer)
+ {
+ EnsureRecording();
+
+ vertexBuffers[slot] = (WebGPUBuffer)buffer;
+ if (rendering)
+ renderPass.SetVertexBuffer(slot, vertexBuffers[slot].Resource, 0);
+ }
+
+ ///
+ public void SetIndexBuffer(IBuffer buffer)
+ {
+ EnsureRecording();
+
+ indexBuffer = (WebGPUBuffer)buffer;
+ if (rendering)
+ renderPass.SetIndexBuffer(indexBuffer.Resource, IndexFormat.Uint32, 0);
+ }
+
+ ///
+ public void SetGraphicsResourceSet(uint slot, IResourceSet resourceSet)
+ {
+ EnsureRecording();
+
+ graphicsSets[slot] = (WebGPUResourceSet)resourceSet;
+ if (rendering)
+ renderPass.SetBindGroup(slot, graphicsSets[slot].Resource);
+ }
+
+ ///
+ public void SetComputeResourceSet(uint slot, IResourceSet resourceSet)
+ {
+ EnsureRecording();
+
+ computeSets[slot] = (WebGPUResourceSet)resourceSet;
+ if (computing)
+ computePass.SetBindGroup(slot, computeSets[slot].Resource);
+ }
+
+ ///
+ public void UpdateBuffer(IBuffer buffer, uint offset, ReadOnlySpan data)
+ {
+ EnsureRecording();
+
+ var destination = (WebGPUBuffer)buffer;
+ WebGPUDevice.ValidateBufferUpdate(destination, offset, data.Length);
+ if (data.IsEmpty)
+ return;
+
+ EndPass();
+ var upload = GetUploadBuffer(data.Length);
+ int sourceOffset = upload.Length;
+ data.CopyTo(upload.Data.AsSpan(sourceOffset));
+ upload.Length += data.Length;
+ encoder.CopyBufferToBuffer(upload.Resource, (ulong)sourceOffset, destination.Resource, offset, (ulong)data.Length);
+ }
+
+ private UploadBuffer GetUploadBuffer(int length)
+ {
+ while (uploadBufferIndex < uploadBuffers.Count)
+ {
+ var upload = uploadBuffers[uploadBufferIndex];
+ if (upload.Data.Length - upload.Length >= length)
+ return upload;
+ uploadBufferIndex++;
+ }
+
+ int capacity = System.Math.Max(64 * 1024, length);
+ var resource = device.Device.CreateBuffer(new WebGpuSharp.BufferDescriptor
+ {
+ Size = (ulong)capacity,
+ Usage = WebGpuSharp.BufferUsage.CopySrc | WebGpuSharp.BufferUsage.CopyDst
+ });
+ var buffer = new UploadBuffer(resource, capacity);
+ uploadBuffers.Add(buffer);
+ return buffer;
+ }
+
+ ///
+ public void DrawIndexed(uint indexCount)
+ {
+ EnsureRecording();
+ if (pipeline == null || indexBuffer == null)
+ throw new InvalidOperationException("Indexed draws require a graphics pipeline and index buffer.");
+
+ BeginRenderPass();
+ renderPass.DrawIndexed(indexCount, 1, 0, 0, 0);
+ }
+
+ ///
+ public void Dispatch(uint x, uint y, uint z)
+ {
+ EnsureRecording();
+ if (computePipeline == null)
+ throw new InvalidOperationException("Dispatch requires a compute pipeline.");
+
+ if (!computing)
+ {
+ EndPass();
+ computePass = encoder.BeginComputePass();
+ computing = true;
+ computePass.SetPipeline(computePipeline.Resource);
+ foreach (var (slot, set) in computeSets)
+ computePass.SetBindGroup(slot, set.Resource);
+ }
+
+ computePass.DispatchWorkgroups(x, y, z);
+ }
+
+ ///
+ public void ClearDepth(float depth)
+ {
+ EnsureRecording();
+ EndPass();
+ BeginRenderPass(depth: depth);
+ }
+
+ ///
+ public void ClearColor(Beryl.Math.Color color)
+ {
+ EnsureRecording();
+ EndPass();
+ BeginRenderPass(color: new WebGpuSharp.Color(color.R, color.G, color.B, color.A));
+ }
+
+ private void BeginRenderPass(WebGpuSharp.Color? color = null, float? depth = null)
+ {
+ if (rendering)
+ return;
+
+ if (target == null)
+ throw new InvalidOperationException("A framebuffer must be set before rendering.");
+
+ EndPass();
+
+ renderPass = encoder.BeginRenderPass(new RenderPassDescriptor
+ {
+ ColorAttachments = [new RenderPassColorAttachment
+ {
+ View = target.ColorView,
+ LoadOp = color.HasValue ? LoadOp.Clear : LoadOp.Load,
+ StoreOp = StoreOp.Store,
+ ClearValue = color.GetValueOrDefault()
+ }],
+ DepthStencilAttachment = new RenderPassDepthStencilAttachment
+ {
+ View = target.DepthView,
+ DepthLoadOp = depth.HasValue ? LoadOp.Clear : LoadOp.Load,
+ DepthStoreOp = StoreOp.Store,
+ DepthClearValue = depth.GetValueOrDefault(1)
+ }
+ });
+
+ rendering = true;
+ if (pipeline != null)
+ renderPass.SetPipeline(pipeline.Resource);
+
+ foreach (var (slot, buffer) in vertexBuffers)
+ renderPass.SetVertexBuffer(slot, buffer.Resource, 0);
+
+ if (indexBuffer != null)
+ renderPass.SetIndexBuffer(indexBuffer.Resource, IndexFormat.Uint32, 0);
+
+ foreach (var (slot, set) in graphicsSets)
+ renderPass.SetBindGroup(slot, set.Resource);
+ }
+
+ private void EndPass()
+ {
+ if (rendering)
+ renderPass.End();
+
+ if (computing)
+ computePass.End();
+
+ rendering = false;
+ computing = false;
+ renderPass = default;
+ computePass = default;
+ }
+
+ private void EnsureRecording()
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+ if (!recording)
+ throw new InvalidOperationException("Begin the command buffer before recording commands.");
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (disposed)
+ return;
+
+ EndPass();
+
+ commands = default;
+ encoder = default;
+ target = null;
+ pipeline = null;
+ computePipeline = null;
+ indexBuffer = null;
+ vertexBuffers.Clear();
+ graphicsSets.Clear();
+ computeSets.Clear();
+
+ foreach (var upload in uploadBuffers)
+ upload.Resource.Destroy();
+
+ uploadBuffers.Clear();
+ recording = false;
+ finished = false;
+ disposed = true;
+ }
+
+ private sealed class UploadBuffer(WebGpuSharp.Buffer resource, int capacity)
+ {
+ internal WebGpuSharp.Buffer Resource { get; } = resource;
+ internal byte[] Data { get; } = new byte[capacity];
+ internal int Length { get; set; }
+ }
+}
diff --git a/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs b/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs
new file mode 100644
index 0000000..1c8bff7
--- /dev/null
+++ b/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs
@@ -0,0 +1,259 @@
+// This file is part of the Beryl Game Engine.
+// Licensed under the MIT license. (https://github.com/Beryl-Engine/Beryl/blob/main/LICENSE)
+
+using System.Runtime.InteropServices;
+using Beryl.Common.Utility;
+using Beryl.RHI.Resources;
+using Silk.NET.Core.Contexts;
+using Silk.NET.Windowing;
+using WebGpuSharp;
+using WebGpuSharp.FFI;
+using WebGpuSharp.Marshalling;
+
+namespace Beryl.RHI.WebGPUBackend;
+
+internal sealed class WebGPUDevice : IGraphicsDevice
+{
+ internal Surface Surface { get; private set; } = null!;
+ internal Instance Instance { get; private set; } = null!;
+ internal Device Device { get; private set; } = null!;
+ internal Queue Queue { get; private set; } = null!;
+ internal TextureView DepthView { get; private set; } = null!;
+ internal WebGpuSharp.TextureFormat SurfaceFormat { get; private set; }
+
+ ///
+ public VulkanInfo? VulkanInfo => null;
+
+ ///
+ public DeviceFeatures Features => DeviceFeatures.None;
+
+ ///
+ public IFramebuffer SwapchainFramebuffer { get; }
+
+ ///
+ public IResourceFactory ResourceFactory { get; }
+
+ private readonly RendererBackend backend;
+ private Texture? depthTexture;
+ private Texture? currentTexture;
+ private TextureView? currentView;
+ private PresentMode presentMode;
+ private uint width;
+ private uint height;
+ private bool disposed;
+
+ public WebGPUDevice(RendererBackend backend)
+ {
+ this.backend = backend;
+ ResourceFactory = new WebGPUResourceFactory(this);
+ SwapchainFramebuffer = new WebGPUFramebuffer(this);
+ }
+
+ ///
+ public void Initialize(IWindow window)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+ if (Instance != null)
+ throw new InvalidOperationException("The WebGPU device is already initialized.");
+
+ try
+ {
+ Instance = WebGPU.CreateInstance() ?? throw new InvalidOperationException("Failed to create WebGPU instance.");
+ Surface = CreateSurface(window.Native, Instance) ?? throw new PlatformNotSupportedException("No supported native window handle was available.");
+
+ var adapter = Instance.RequestAdapterSync(new RequestAdapterOptions
+ {
+ CompatibleSurface = Surface,
+ PowerPreference = PowerPreference.HighPerformance,
+ BackendType = backend switch
+ {
+ RendererBackend.Vulkan => BackendType.Vulkan,
+ RendererBackend.Direct3D12 => BackendType.D3D12,
+ _ => throw new ArgumentOutOfRangeException(nameof(backend))
+ }
+ });
+
+ BerylConsole.Log($"Found GPU - {adapter.GetInfo().Device}");
+
+ Device = adapter.RequestDeviceSync();
+ Queue = Device.GetQueue();
+
+ var capabilities = Surface.GetCapabilities(adapter) ?? throw new InvalidOperationException("Failed to query surface capabilities.");
+ if (capabilities.Formats.IsEmpty)
+ throw new InvalidOperationException("The surface has no supported texture formats.");
+
+ SurfaceFormat = capabilities.Formats.Contains(WebGpuSharp.TextureFormat.BGRA8Unorm) ? WebGpuSharp.TextureFormat.BGRA8Unorm : capabilities.Formats[0];
+ presentMode = !window.VSync && capabilities.PresentModes.Contains(PresentMode.Immediate) ? PresentMode.Immediate : PresentMode.Fifo;
+
+ ResizeSwapchain((uint)System.Math.Max(1, window.FramebufferSize.X), (uint)System.Math.Max(1, window.FramebufferSize.Y));
+ }
+ catch
+ {
+ Dispose();
+ throw;
+ }
+ }
+
+ ///
+ public void ResizeSwapchain(uint width, uint height)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+ if (Surface == null || Device == null || width == 0 || height == 0)
+ return;
+
+ ReleaseCurrentTexture();
+
+ depthTexture?.Destroy();
+ this.width = width;
+ this.height = height;
+
+ ConfigureSurface();
+
+ depthTexture = Device.CreateTexture(new WebGpuSharp.TextureDescriptor
+ {
+ Size = new(width, height, 1),
+ Dimension = TextureDimension.D2,
+ Format = WebGpuSharp.TextureFormat.Depth32Float,
+ Usage = TextureUsage.RenderAttachment
+ });
+
+ DepthView = depthTexture.CreateView();
+ }
+
+ private void ConfigureSurface() => Surface.Configure(new SurfaceConfiguration
+ {
+ Device = Device,
+ Format = SurfaceFormat,
+ Width = width,
+ Height = height,
+ Usage = TextureUsage.RenderAttachment,
+ PresentMode = presentMode,
+ AlphaMode = CompositeAlphaMode.Auto
+ });
+
+ internal TextureView GetCurrentView()
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+ if (currentView != null)
+ return currentView;
+
+ var surfaceTexture = Surface.GetCurrentTexture();
+ if (surfaceTexture.Status is SurfaceGetCurrentTextureStatus.Outdated or SurfaceGetCurrentTextureStatus.Lost)
+ {
+ ConfigureSurface();
+ surfaceTexture = Surface.GetCurrentTexture();
+ }
+
+ if (surfaceTexture.Status is not (SurfaceGetCurrentTextureStatus.SuccessOptimal or SurfaceGetCurrentTextureStatus.SuccessSuboptimal) || surfaceTexture.Texture == null)
+ throw new InvalidOperationException($"Failed to acquire a WebGPU surface texture: {surfaceTexture.Status}.");
+
+ currentTexture = surfaceTexture.Texture;
+ currentView = currentTexture.CreateView();
+ return currentView;
+ }
+
+ internal void ReleaseCurrentTexture()
+ {
+ currentView = null;
+ currentTexture = null;
+ }
+
+ ///
+ public void SubmitCommands(ICommandBuffer buffer) => ((WebGPUCommandBuffer)buffer).Submit();
+
+ ///
+ public void SwapBuffers()
+ {
+ if (currentTexture == null)
+ return;
+
+ Surface.Present();
+ ReleaseCurrentTexture();
+ }
+
+ ///
+ public void UpdateBuffer(IBuffer buffer, uint offset, ReadOnlySpan data) where T : unmanaged
+ {
+ var target = (WebGPUBuffer)buffer;
+ ReadOnlySpan bytes = MemoryMarshal.AsBytes(data);
+ ValidateBufferUpdate(target, offset, bytes.Length);
+ if (!bytes.IsEmpty)
+ Queue.WriteBuffer(target.Resource, offset, bytes);
+ }
+
+ internal static void ValidateBufferUpdate(WebGPUBuffer buffer, uint offset, int length)
+ {
+ if ((offset & 3) != 0 || (length & 3) != 0)
+ throw new ArgumentException("WebGPU buffer updates require four-byte aligned offsets and lengths.");
+
+ if ((ulong)offset + (ulong)length > buffer.SizeInBytes)
+ throw new ArgumentOutOfRangeException(nameof(offset), "The update exceeds the buffer size.");
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (disposed)
+ return;
+
+ disposed = true;
+ ReleaseCurrentTexture();
+ depthTexture?.Destroy();
+ depthTexture = null;
+ DepthView = null!;
+
+ if (Surface != null && width != 0)
+ Surface.Unconfigure();
+
+ if (Device != null)
+ WebGPUMarshal.GetHandle(Device).Destroy();
+
+ Queue = null!;
+ Device = null!;
+ Surface = null!;
+ Instance = null!;
+ }
+
+ private static unsafe Surface? CreateSurface(INativeWindow? native, Instance instance)
+ {
+ ArgumentNullException.ThrowIfNull(native);
+
+ if (native.Win32 is { } win32)
+ {
+ var winDesc = new SurfaceSourceWindowsHWNDFFI
+ {
+ Chain = new ChainedStruct { SType = SType.SurfaceSourceWindowsHWND },
+ Hinstance = (void*)win32.HInstance,
+ Hwnd = (void*)win32.Hwnd
+ };
+
+ return instance.CreateSurface(new SurfaceDescriptor(ref winDesc));
+ }
+
+ if (native.X11 is { } x11)
+ {
+ var x11Desc = new SurfaceSourceXlibWindowFFI
+ {
+ Chain = new ChainedStruct { SType = SType.SurfaceSourceXlibWindow },
+ Display = (void*)x11.Display,
+ Window = x11.Window
+ };
+
+ return instance.CreateSurface(new SurfaceDescriptor(ref x11Desc));
+ }
+
+ if (native.Wayland is { } wayland)
+ {
+ var waylandDesc = new SurfaceSourceWaylandSurfaceFFI
+ {
+ Chain = new ChainedStruct { SType = SType.SurfaceSourceWaylandSurface },
+ Display = (void*)wayland.Display,
+ Surface = (void*)wayland.Surface
+ };
+
+ return instance.CreateSurface(new SurfaceDescriptor(ref waylandDesc));
+ }
+
+ return null;
+ }
+}
diff --git a/Beryl.RHI/WebGPUBackend/WebGPUResourceFactory.cs b/Beryl.RHI/WebGPUBackend/WebGPUResourceFactory.cs
new file mode 100644
index 0000000..30b81fd
--- /dev/null
+++ b/Beryl.RHI/WebGPUBackend/WebGPUResourceFactory.cs
@@ -0,0 +1,232 @@
+// This file is part of the Beryl Game Engine.
+// Licensed under the MIT license. (https://github.com/Beryl-Engine/Beryl/blob/main/LICENSE)
+
+using Beryl.RHI.Resources;
+using WebGpuSharp;
+
+using BufferDescriptor = Beryl.RHI.Resources.BufferDescriptor;
+using ComputePipelineDescriptor = Beryl.RHI.Resources.ComputePipelineDescriptor;
+using SamplerDescriptor = Beryl.RHI.Resources.SamplerDescriptor;
+using TextureDescriptor = Beryl.RHI.Resources.TextureDescriptor;
+using TextureViewDescriptor = Beryl.RHI.Resources.TextureViewDescriptor;
+
+namespace Beryl.RHI.WebGPUBackend;
+
+internal sealed class WebGPUResourceFactory(WebGPUDevice device) : IResourceFactory
+{
+ ///
+ public ICommandBuffer CreateCommandBuffer() => new WebGPUCommandBuffer(device);
+
+ ///
+ public IBuffer CreateBuffer(BufferDescriptor key)
+ {
+ WebGpuSharp.BufferUsage usage = WebGpuSharp.BufferUsage.CopyDst;
+ if (key.Usage.HasFlag(Resources.BufferUsage.VertexBuffer))
+ usage |= WebGpuSharp.BufferUsage.Vertex;
+ if (key.Usage.HasFlag(Resources.BufferUsage.IndexBuffer))
+ usage |= WebGpuSharp.BufferUsage.Index;
+ if (key.Usage.HasFlag(Resources.BufferUsage.UniformBuffer))
+ usage |= WebGpuSharp.BufferUsage.Uniform;
+ ArgumentOutOfRangeException.ThrowIfZero(key.Size);
+ return new WebGPUBuffer(device.Device.CreateBuffer(new WebGpuSharp.BufferDescriptor
+ {
+ Size = ((ulong)key.Size + 3) & ~3UL,
+ Usage = usage
+ }), key.Size);
+ }
+
+ ///
+ public IShader CreateShader(ShaderDescriptor key) => new WebGPUShader(device.Device.CreateShaderModuleWGSL(new ShaderModuleWGSLDescriptor { Code = key.Bytecode }));
+
+ ///
+ public ITexture CreateTexture(TextureDescriptor key)
+ {
+ ArgumentOutOfRangeException.ThrowIfZero(key.Width);
+ ArgumentOutOfRangeException.ThrowIfZero(key.Height);
+
+ int pixels = checked((int)(key.Width * key.Height));
+ int channels = key.Format switch
+ {
+ Resources.TextureFormat.RGBA8 => 4,
+ Resources.TextureFormat.RGB8 => 3,
+ _ => throw new NotSupportedException($"Unsupported texture format '{key.Format}'.")
+ };
+
+ if (!key.Data.IsEmpty && key.Data.Length != checked(pixels * channels))
+ throw new ArgumentException("Texture data does not match its dimensions and format.", nameof(key));
+
+ Texture texture = device.Device.CreateTexture(new WebGpuSharp.TextureDescriptor
+ {
+ Size = new(key.Width, key.Height, 1),
+ Dimension = TextureDimension.D2,
+ Format = WebGpuSharp.TextureFormat.RGBA8Unorm,
+ Usage = TextureUsage.TextureBinding | TextureUsage.CopyDst
+ });
+
+ if (!key.Data.IsEmpty)
+ {
+ ReadOnlySpan data = key.Data;
+ if (channels == 3)
+ {
+ byte[] rgba = new byte[checked(pixels * 4)];
+ for (int i = 0; i < pixels; i++)
+ {
+ data.Slice(i * 3, 3).CopyTo(rgba.AsSpan(i * 4, 3));
+ rgba[i * 4 + 3] = 255;
+ }
+ data = rgba;
+ }
+ device.Queue.WriteTexture(new TexelCopyTextureInfo { Texture = texture }, data, new TexelCopyBufferLayout { BytesPerRow = checked(key.Width * 4), RowsPerImage = key.Height }, new Extent3D(key.Width, key.Height, 1));
+ }
+
+ return new WebGPUTexture(texture);
+ }
+
+ ///
+ public ITextureView CreateTextureView(TextureViewDescriptor key) => new WebGPUTextureView(((WebGPUTexture)key.Texture).Resource.CreateView());
+
+ ///
+ public ISampler CreateSampler(SamplerDescriptor key) => new WebGPUSampler(device.Device.CreateSampler(new WebGpuSharp.SamplerDescriptor
+ {
+ MinFilter = key.Sampler.Filter == Resources.SamplerFilter.Nearest ? FilterMode.Nearest : FilterMode.Linear,
+ MagFilter = key.Sampler.Filter == Resources.SamplerFilter.Nearest ? FilterMode.Nearest : FilterMode.Linear,
+ MipmapFilter = MipmapFilterMode.Nearest,
+ AddressModeU = key.Sampler.WrapU == TextureWrapMode.Clamp ? AddressMode.ClampToEdge : AddressMode.Repeat,
+ AddressModeV = key.Sampler.WrapV == TextureWrapMode.Clamp ? AddressMode.ClampToEdge : AddressMode.Repeat
+ }) ?? throw new InvalidOperationException("Failed to create WebGPU sampler."));
+
+ ///
+ public IResourceLayout CreateResourceLayout(ResourceLayoutDescriptor key)
+ {
+ BindGroupLayoutEntry[] entries = new BindGroupLayoutEntry[key.Elements.Length];
+ for (int i = 0; i < entries.Length; i++)
+ {
+ var element = key.Elements[i];
+ if (element.Kind != ResourceKind.UniformBuffer)
+ throw new NotSupportedException($"Unsupported resource kind '{element.Kind}'.");
+ entries[i] = new()
+ {
+ Binding = (uint)i,
+ Visibility = ToWebGPU(element.Stages),
+ Buffer = new() { Type = BufferBindingType.Uniform }
+ };
+ }
+
+ return new WebGPUResourceLayout(device.Device.CreateBindGroupLayout(new BindGroupLayoutDescriptor { Entries = entries }), entries.Length);
+ }
+
+ ///
+ public IResourceSet CreateResourceSet(ResourceDescriptor key)
+ {
+ var layout = (WebGPUResourceLayout)key.Layout;
+ if (key.BoundResources.Length != layout.Count)
+ throw new ArgumentException("Resource count does not match the layout.", nameof(key));
+
+ BindGroupEntry[] entries = new BindGroupEntry[key.BoundResources.Length];
+ for (int i = 0; i < entries.Length; i++)
+ {
+ var buffer = (WebGPUBuffer)key.BoundResources[i];
+ entries[i] = new() { Binding = (uint)i, Buffer = buffer.Resource, Size = buffer.SizeInBytes };
+ }
+
+ return new WebGPUResourceSet(device.Device.CreateBindGroup(new BindGroupDescriptor { Layout = layout.Resource, Entries = entries }));
+ }
+
+ ///
+ public IPipeline CreateGraphicsPipeline(PipelineDescriptor key)
+ {
+ var output = (WebGPUFramebuffer)key.Output;
+ var vertex = (WebGPUShader)CreateShader(key.VertShader);
+ var fragment = (WebGPUShader)CreateShader(key.FragShader);
+ return new WebGPUPipeline(device.Device.CreateRenderPipelineSync(new RenderPipelineDescriptor
+ {
+ Layout = CreatePipelineLayout(key.ResourceGroups),
+ Vertex = new()
+ {
+ Module = vertex.Resource,
+ Buffers = [new VertexBufferLayout
+ {
+ ArrayStride = 32,
+ StepMode = VertexStepMode.Vertex,
+ Attributes = [
+ new VertexAttribute { ShaderLocation = 0, Offset = 0, Format = VertexFormat.Float32x3 },
+ new VertexAttribute { ShaderLocation = 1, Offset = 12, Format = VertexFormat.Float32x3 },
+ new VertexAttribute { ShaderLocation = 2, Offset = 24, Format = VertexFormat.Float32x2 }]
+ }]
+ },
+ Fragment = new() { Module = fragment.Resource, Targets = [new ColorTargetState { Format = output.Format, WriteMask = ColorWriteMask.All }] },
+ Primitive = new()
+ {
+ Topology = PrimitiveTopology.TriangleList,
+ FrontFace = FrontFace.CW,
+ CullMode = key.CullingMode switch { CullingMode.Back => CullMode.Back, CullingMode.Front => CullMode.Front, _ => CullMode.None }
+ },
+ DepthStencil = new()
+ {
+ Format = WebGpuSharp.TextureFormat.Depth32Float,
+ DepthWriteEnabled = key.DepthInfo.WriteDepth ? OptionalBool.True : OptionalBool.False,
+ DepthCompare = ToWebGPU(key.DepthInfo.DepthComparison)
+ }
+ }));
+ }
+
+ ///
+ public IComputePipeline CreateComputePipeline(ComputePipelineDescriptor key)
+ {
+ var shader = (WebGPUShader)CreateShader(key.Shader);
+ return new WebGPUComputePipeline(device.Device.CreateComputePipelineSync(new WebGpuSharp.ComputePipelineDescriptor
+ {
+ Layout = CreatePipelineLayout(key.ResourceGroups),
+ Compute = new() { Module = shader.Resource }
+ }));
+ }
+
+ private PipelineLayout CreatePipelineLayout(ReadOnlySpan groups)
+ {
+ int count = 0;
+ foreach (var group in groups)
+ count = System.Math.Max(count, checked((int)group.Set + 1));
+
+ BindGroupLayout[] layouts = new BindGroupLayout[count];
+ foreach (var group in groups)
+ {
+ if (layouts[group.Set] != null)
+ throw new ArgumentException("Duplicate resource set index.", nameof(groups));
+
+ layouts[group.Set] = ((WebGPUResourceLayout)CreateResourceLayout(new ResourceLayoutDescriptor(group.LayoutElements.AsSpan()))).Resource;
+ }
+
+ for (int i = 0; i < count; i++)
+ layouts[i] ??= device.Device.CreateBindGroupLayout(new BindGroupLayoutDescriptor { Entries = [] });
+
+ return device.Device.CreatePipelineLayout(new PipelineLayoutDescriptor { BindGroupLayouts = layouts });
+ }
+
+ private static ShaderStage ToWebGPU(Resources.ShaderStages stages)
+ {
+ ShaderStage result = 0;
+ if (stages.HasFlag(Resources.ShaderStages.Vertex))
+ result |= ShaderStage.Vertex;
+
+ if (stages.HasFlag(Resources.ShaderStages.Fragment))
+ result |= ShaderStage.Fragment;
+
+ if (stages.HasFlag(Resources.ShaderStages.Compute))
+ result |= ShaderStage.Compute;
+
+ return result;
+ }
+
+ private static CompareFunction ToWebGPU(ComparisonMode comparison) => comparison switch
+ {
+ ComparisonMode.Always => CompareFunction.Always,
+ ComparisonMode.Less => CompareFunction.Less,
+ ComparisonMode.LessOrEqual => CompareFunction.LessEqual,
+ ComparisonMode.Equal => CompareFunction.Equal,
+ ComparisonMode.NotEqual => CompareFunction.NotEqual,
+ ComparisonMode.GreaterOrEqual => CompareFunction.GreaterEqual,
+ ComparisonMode.Greater => CompareFunction.Greater,
+ ComparisonMode.Never => CompareFunction.Never,
+ _ => throw new ArgumentOutOfRangeException(nameof(comparison))
+ };
+}
diff --git a/Beryl.RHI/WebGPUBackend/WebGPUResources.cs b/Beryl.RHI/WebGPUBackend/WebGPUResources.cs
new file mode 100644
index 0000000..177b73d
--- /dev/null
+++ b/Beryl.RHI/WebGPUBackend/WebGPUResources.cs
@@ -0,0 +1,104 @@
+// This file is part of the Beryl Game Engine.
+// Licensed under the MIT license. (https://github.com/Beryl-Engine/Beryl/blob/main/LICENSE)
+
+using Beryl.RHI.Resources;
+using WebGpuSharp;
+
+namespace Beryl.RHI.WebGPUBackend;
+
+// WebGPUSharp owns native references through managed handles. Clear them on disposal;
+// buffers and textures additionally destroy their GPU storage explicitly.
+
+internal sealed class WebGPUFramebuffer(WebGPUDevice device) : IFramebuffer
+{
+ internal TextureView ColorView => device.GetCurrentView();
+ internal TextureView DepthView => device.DepthView;
+ internal WebGpuSharp.TextureFormat Format => device.SurfaceFormat;
+
+ ///
+ public void Dispose() => device.ReleaseCurrentTexture();
+}
+
+internal sealed class WebGPUShader(ShaderModule resource) : IShader
+{
+ internal ShaderModule Resource { get; private set; } = resource;
+
+ ///
+ public void Dispose() => Resource = null!;
+}
+
+internal sealed class WebGPUBuffer(WebGpuSharp.Buffer resource, uint size) : IBuffer
+{
+ internal WebGpuSharp.Buffer Resource { get; private set; } = resource;
+
+ ///
+ public uint SizeInBytes { get; } = size;
+
+ ///
+ public void Dispose()
+ {
+ Resource?.Destroy();
+ Resource = null!;
+ }
+}
+
+internal sealed class WebGPUTexture(Texture resource) : ITexture
+{
+ internal Texture Resource { get; private set; } = resource;
+
+ ///
+ public void Dispose()
+ {
+ Resource?.Destroy();
+ Resource = null!;
+ }
+}
+
+internal sealed class WebGPUTextureView(TextureView resource) : ITextureView
+{
+ internal TextureView Resource { get; private set; } = resource;
+
+ ///
+ public void Dispose() => Resource = null!;
+}
+
+internal sealed class WebGPUSampler(WebGpuSharp.Sampler resource) : ISampler
+{
+ internal WebGpuSharp.Sampler Resource { get; private set; } = resource;
+
+ ///
+ public void Dispose() => Resource = null!;
+}
+
+internal sealed class WebGPUResourceLayout(BindGroupLayout resource, int count) : IResourceLayout
+{
+ internal BindGroupLayout Resource { get; private set; } = resource;
+ internal int Count { get; } = count;
+
+ ///
+ public void Dispose() => Resource = null!;
+}
+
+internal sealed class WebGPUResourceSet(BindGroup resource) : IResourceSet
+{
+ internal BindGroup Resource { get; private set; } = resource;
+
+ ///
+ public void Dispose() => Resource = null!;
+}
+
+internal sealed class WebGPUPipeline(RenderPipeline resource) : IPipeline
+{
+ internal RenderPipeline Resource { get; private set; } = resource;
+
+ ///
+ public void Dispose() => Resource = null!;
+}
+
+internal sealed class WebGPUComputePipeline(ComputePipeline resource) : IComputePipeline
+{
+ internal ComputePipeline Resource { get; private set; } = resource;
+
+ ///
+ public void Dispose() => Resource = null!;
+}
diff --git a/Modules/Beryl.Rendering/Entities/FlyController.cs b/Modules/Beryl.Rendering/Entities/FlyController.cs
index 2059328..923705b 100644
--- a/Modules/Beryl.Rendering/Entities/FlyController.cs
+++ b/Modules/Beryl.Rendering/Entities/FlyController.cs
@@ -8,7 +8,6 @@
using Beryl.Math;
using Beryl.Physics;
using Beryl.Rendering.Components;
-using Beryl.Rendering.Resources;
using Beryl.Scenes.Entities;
namespace Beryl.Rendering.Entities;
diff --git a/Modules/Beryl.Rendering/Resources/Shader.cs b/Modules/Beryl.Rendering/Resources/Shader.cs
index da10183..61c1e5b 100644
--- a/Modules/Beryl.Rendering/Resources/Shader.cs
+++ b/Modules/Beryl.Rendering/Resources/Shader.cs
@@ -142,15 +142,15 @@ public string Source
Resources = resources.OrderBy(r => r.Set).ThenBy(r => r.Binding).ToImmutableArray();
- var layouts = Resources.Select(r => new ResourceLayoutElementDescription(r.Name, r.Kind, ShaderStages.Vertex | ShaderStages.Fragment | ShaderStages.Compute)).ToImmutableArray();
-
ResourceGroups = Resources
.GroupBy(r => r.Set)
.Select(g => new ShaderResourceGroup
{
Set = g.Key,
Resources = g.OrderBy(r => r.Binding).ToImmutableArray(),
- LayoutElements = layouts
+ LayoutElements = g.OrderBy(r => r.Binding)
+ .Select(r => new ResourceLayoutElementDescription(r.Name, r.Kind, ShaderStages.Vertex | ShaderStages.Fragment | ShaderStages.Compute))
+ .ToImmutableArray()
})
.ToImmutableArray();
diff --git a/Modules/Beryl.Rendering/ShaderBaker.cs b/Modules/Beryl.Rendering/ShaderBaker.cs
index 4768ac8..ef86aff 100644
--- a/Modules/Beryl.Rendering/ShaderBaker.cs
+++ b/Modules/Beryl.Rendering/ShaderBaker.cs
@@ -1,7 +1,6 @@
// This file is part of the Beryl Game Engine.
// Licensed under the MIT license. (https://github.com/Beryl-Engine/Beryl/blob/main/LICENSE)
-using Beryl.Common;
using SlangShaderSharp;
namespace Beryl.Rendering;
@@ -12,40 +11,11 @@ namespace Beryl.Rendering;
public static class ShaderBaker
{
/// The for the current rendering backend.
- public static SlangCompileTarget SlangTarget
- {
- get
- {
- switch (backend)
- {
- case RHI.RendererBackend.Vulkan:
- return SlangCompileTarget.Spirv;
- case RHI.RendererBackend.Direct3D12:
- return SlangCompileTarget.Dxil;
- default:
- throw new NotImplementedException($"Can't compile Slang for target {backend}.");
- }
- }
- }
+ public static SlangCompileTarget SlangTarget => SlangCompileTarget.Wgsl;
- /// The for the current rendering backend.
- public static SlangProfileID SlangProfile
- {
- get
- {
- switch (backend)
- {
- case RHI.RendererBackend.Vulkan:
- return globalSession.FindProfile("spirv_1_3");
- case RHI.RendererBackend.Direct3D12:
- return globalSession.FindProfile("sm_6_0");
- default:
- throw new NotImplementedException($"Can't compile Slang for target {backend}.");
- }
- }
- }
- private static RHI.RendererBackend backend => ModuleManager.GetModule()?.Backend ?? RHI.RendererBackend.Vulkan;
+ /// The for the current rendering backend.
+ public static SlangProfileID SlangProfile => globalSession.FindProfile("wgsl_1_0");
private static readonly IGlobalSession globalSession;
private static readonly ISession localSession;
diff --git a/Players/Beryl.Players.Desktop/Program.cs b/Players/Beryl.Players.Desktop/Program.cs
index 8f4d005..57aa843 100644
--- a/Players/Beryl.Players.Desktop/Program.cs
+++ b/Players/Beryl.Players.Desktop/Program.cs
@@ -47,12 +47,6 @@ static void Main(string[] args)
cube.RigidBody.Colliders.Add(new BoxCollider(1, 1, 1));
ModuleManager.GetModule()?.CurrentScene.StartAll();
-
- using var buffer = GPUBufferPool.Shared.RentAuto();
- buffer.Object.AddInt(10);
-
- Shader? compute = Application.ResourceProvider.GetResource("Assets/Beryl/Compute.slang");
- compute?.EnqueueDispatch(1, 1, 1, buffer);
};
Application.OnExit += () =>