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 += () =>