From 442becaadf3c6a1215cb040043df2be39ce3772d Mon Sep 17 00:00:00 2001 From: Tweety Date: Thu, 20 Aug 2026 20:50:39 +1000 Subject: [PATCH 1/7] Initialize WebGPU. --- Beryl.RHI/Beryl.RHI.csproj | 1 + Beryl.RHI/GraphicsDeviceFactory.cs | 2 +- Beryl.RHI/WebGPUBackend/WebGPUDevice.cs | 172 ++++++++++++++++++ .../WebGPUBackend/WebGPUResourceFactory.cs | 39 ++++ .../Beryl.Rendering/Entities/FlyController.cs | 1 - 5 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 Beryl.RHI/WebGPUBackend/WebGPUDevice.cs create mode 100644 Beryl.RHI/WebGPUBackend/WebGPUResourceFactory.cs diff --git a/Beryl.RHI/Beryl.RHI.csproj b/Beryl.RHI/Beryl.RHI.csproj index dc88b8b..5b5a5e4 100644 --- a/Beryl.RHI/Beryl.RHI.csproj +++ b/Beryl.RHI/Beryl.RHI.csproj @@ -10,6 +10,7 @@ + diff --git a/Beryl.RHI/GraphicsDeviceFactory.cs b/Beryl.RHI/GraphicsDeviceFactory.cs index 3ab6b21..02c2478 100644 --- a/Beryl.RHI/GraphicsDeviceFactory.cs +++ b/Beryl.RHI/GraphicsDeviceFactory.cs @@ -17,5 +17,5 @@ public static class GraphicsDeviceFactory }; /// 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/WebGPUBackend/WebGPUDevice.cs b/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs new file mode 100644 index 0000000..f41629c --- /dev/null +++ b/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs @@ -0,0 +1,172 @@ +// 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 WebGpuSharp; +using WebGpuSharp.FFI; + +namespace Beryl.RHI.WebGPUBackend; + +internal sealed class WebGPUDevice : IGraphicsDevice +{ + /// + public VulkanInfo? VulkanInfo { get; private set; } + + /// + public DeviceFeatures Features { get; } = DeviceFeatures.None; + + /// + public IFramebuffer SwapchainFramebuffer => throw new NotImplementedException(); + + /// + public IResourceFactory ResourceFactory { get; } + + private Surface? surface; + private Instance? instance; + private Device? device; + + private readonly RendererBackend backend; + + public WebGPUDevice(RendererBackend backend) + { + ResourceFactory = new WebGPUResourceFactory(this); + this.backend = backend; + } + + /// + public void Initialize(IWindow window) + { + instance = WebGPU.CreateInstance(); + if (instance == null) + throw new Exception("Failed to create WebGPU instance."); + + surface = CreateSurface(window.Native, instance); + if (surface == null) + throw new Exception("Failed to create surface for window."); + + BackendType backendType = BackendType.Undefined; + switch (backend) + { + case RendererBackend.Vulkan: + backendType = BackendType.Vulkan; + break; + case RendererBackend.Direct3D12: + backendType = BackendType.D3D12; + break; + } + + RequestAdapterOptions adapterOptions = new() + { + CompatibleSurface = surface, + PowerPreference = PowerPreference.HighPerformance, + + BackendType = backendType + }; + + instance.RequestAdapter(in adapterOptions, (adapterStatus, adapter, bytes) => + { + if (adapterStatus != RequestAdapterStatus.Success || adapter == null) + throw new Exception("Failed to create WebGPU adapter."); + + var info = adapter.GetInfo(); + + BerylConsole.Log($"Found GPU - {info.Device}"); + + adapter.RequestDevice((deviceStatus, device, bytes) => + { + if (deviceStatus != RequestDeviceStatus.Success || device == null) + throw new Exception("Failed to create WebGPU device."); + + this.device = device; + + surface.Configure(new SurfaceConfiguration() + { + Device = this.device, + Format = WebGpuSharp.TextureFormat.BGRA8Unorm, + Width = (uint)window.Size.X, + Height = (uint)window.Size.Y, + Usage = TextureUsage.RenderAttachment, + PresentMode = PresentMode.Immediate + }); + }); + }); + } + + /// + public void ResizeSwapchain(uint width, uint height) + { + if (surface == null || device == null) + return; + + surface.Configure(new SurfaceConfiguration() + { + Device = device, + Format = WebGpuSharp.TextureFormat.BGRA8Unorm, + Width = width, + Height = height, + Usage = TextureUsage.RenderAttachment, + PresentMode = PresentMode.Immediate + }); + } + + /// + public void SubmitCommands(ICommandBuffer buffer) => throw new NotImplementedException(); + + /// + public void SwapBuffers() => surface?.Present(); + + /// + public void UpdateBuffer(IBuffer buffer, uint offset, ReadOnlySpan data) where T : unmanaged => throw new NotImplementedException(); + + /// + public void Dispose() + { + + } + + 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..a997597 --- /dev/null +++ b/Beryl.RHI/WebGPUBackend/WebGPUResourceFactory.cs @@ -0,0 +1,39 @@ +// 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; + +namespace Beryl.RHI.WebGPUBackend; + +internal sealed class WebGPUResourceFactory(WebGPUDevice device) : IResourceFactory +{ + /// + public IBuffer CreateBuffer(BufferDescriptor key) => throw new NotImplementedException(); + + /// + public ICommandBuffer CreateCommandBuffer() => throw new NotImplementedException(); + + /// + public IComputePipeline CreateComputePipeline(ComputePipelineDescriptor key) => throw new NotImplementedException(); + + /// + public IPipeline CreateGraphicsPipeline(PipelineDescriptor key) => throw new NotImplementedException(); + + /// + public IResourceLayout CreateResourceLayout(ResourceLayoutDescriptor key) => throw new NotImplementedException(); + + /// + public IResourceSet CreateResourceSet(ResourceDescriptor key) => throw new NotImplementedException(); + + /// + public ISampler CreateSampler(SamplerDescriptor key) => throw new NotImplementedException(); + + /// + public IShader CreateShader(ShaderDescriptor key) => throw new NotImplementedException(); + + /// + public ITexture CreateTexture(TextureDescriptor key) => throw new NotImplementedException(); + + /// + public ITextureView CreateTextureView(TextureViewDescriptor key) => throw new NotImplementedException(); +} 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; From d48da45caa7b1aad8183357a5250cddb515b3f40 Mon Sep 17 00:00:00 2001 From: Tweety Date: Sat, 22 Aug 2026 02:14:46 +1000 Subject: [PATCH 2/7] Start setting up WGPU resources. --- Beryl.RHI/WebGPUBackend/WebGPUDevice.cs | 34 +++++++++--------- .../WebGPUBackend/WebGPUResourceFactory.cs | 18 +++++++++- Beryl.RHI/WebGPUBackend/WebGPUResources.cs | 18 ++++++++++ Modules/Beryl.Rendering/ShaderBaker.cs | 36 ++----------------- 4 files changed, 55 insertions(+), 51 deletions(-) create mode 100644 Beryl.RHI/WebGPUBackend/WebGPUResources.cs diff --git a/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs b/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs index f41629c..3e9f351 100644 --- a/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs +++ b/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs @@ -12,6 +12,10 @@ 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!; + /// public VulkanInfo? VulkanInfo { get; private set; } @@ -24,10 +28,6 @@ internal sealed class WebGPUDevice : IGraphicsDevice /// public IResourceFactory ResourceFactory { get; } - private Surface? surface; - private Instance? instance; - private Device? device; - private readonly RendererBackend backend; public WebGPUDevice(RendererBackend backend) @@ -39,12 +39,12 @@ public WebGPUDevice(RendererBackend backend) /// public void Initialize(IWindow window) { - instance = WebGPU.CreateInstance(); - if (instance == null) + Instance = WebGPU.CreateInstance(); + if (Instance == null) throw new Exception("Failed to create WebGPU instance."); - surface = CreateSurface(window.Native, instance); - if (surface == null) + Surface = CreateSurface(window.Native, Instance); + if (Surface == null) throw new Exception("Failed to create surface for window."); BackendType backendType = BackendType.Undefined; @@ -60,13 +60,13 @@ public void Initialize(IWindow window) RequestAdapterOptions adapterOptions = new() { - CompatibleSurface = surface, + CompatibleSurface = Surface, PowerPreference = PowerPreference.HighPerformance, BackendType = backendType }; - instance.RequestAdapter(in adapterOptions, (adapterStatus, adapter, bytes) => + Instance.RequestAdapter(in adapterOptions, (adapterStatus, adapter, bytes) => { if (adapterStatus != RequestAdapterStatus.Success || adapter == null) throw new Exception("Failed to create WebGPU adapter."); @@ -80,11 +80,11 @@ public void Initialize(IWindow window) if (deviceStatus != RequestDeviceStatus.Success || device == null) throw new Exception("Failed to create WebGPU device."); - this.device = device; + Device = device; - surface.Configure(new SurfaceConfiguration() + Surface.Configure(new SurfaceConfiguration() { - Device = this.device, + Device = Device, Format = WebGpuSharp.TextureFormat.BGRA8Unorm, Width = (uint)window.Size.X, Height = (uint)window.Size.Y, @@ -98,12 +98,12 @@ public void Initialize(IWindow window) /// public void ResizeSwapchain(uint width, uint height) { - if (surface == null || device == null) + if (Surface == null || Device == null) return; - surface.Configure(new SurfaceConfiguration() + Surface.Configure(new SurfaceConfiguration() { - Device = device, + Device = Device, Format = WebGpuSharp.TextureFormat.BGRA8Unorm, Width = width, Height = height, @@ -116,7 +116,7 @@ public void ResizeSwapchain(uint width, uint height) public void SubmitCommands(ICommandBuffer buffer) => throw new NotImplementedException(); /// - public void SwapBuffers() => surface?.Present(); + public void SwapBuffers() => Surface?.Present(); /// public void UpdateBuffer(IBuffer buffer, uint offset, ReadOnlySpan data) where T : unmanaged => throw new NotImplementedException(); diff --git a/Beryl.RHI/WebGPUBackend/WebGPUResourceFactory.cs b/Beryl.RHI/WebGPUBackend/WebGPUResourceFactory.cs index a997597..a009a34 100644 --- a/Beryl.RHI/WebGPUBackend/WebGPUResourceFactory.cs +++ b/Beryl.RHI/WebGPUBackend/WebGPUResourceFactory.cs @@ -2,6 +2,13 @@ // 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; @@ -29,7 +36,16 @@ internal sealed class WebGPUResourceFactory(WebGPUDevice device) : IResourceFact public ISampler CreateSampler(SamplerDescriptor key) => throw new NotImplementedException(); /// - public IShader CreateShader(ShaderDescriptor key) => throw new NotImplementedException(); + public IShader CreateShader(ShaderDescriptor key) + { + ShaderModuleWGSLDescriptor descriptor = new() + { + Code = key.Bytecode + }; + + ShaderModule module = device.Device.CreateShaderModuleWGSL(in descriptor); + return new WebGPUShader(module); + } /// public ITexture CreateTexture(TextureDescriptor key) => throw new NotImplementedException(); diff --git a/Beryl.RHI/WebGPUBackend/WebGPUResources.cs b/Beryl.RHI/WebGPUBackend/WebGPUResources.cs new file mode 100644 index 0000000..9b98237 --- /dev/null +++ b/Beryl.RHI/WebGPUBackend/WebGPUResources.cs @@ -0,0 +1,18 @@ +// 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 WebGPUShader(ShaderModule shader) : IShader +{ + /// + public void Dispose() + { + + } +} + + 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; From 83b86941a8d8b27d94c48794e6e4547bcdac6eec Mon Sep 17 00:00:00 2001 From: Tweety Date: Mon, 19 Oct 2026 20:28:43 +1100 Subject: [PATCH 3/7] Improve nullability. --- Beryl.RHI/WebGPUBackend/WebGPUDevice.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs b/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs index 3e9f351..0956472 100644 --- a/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs +++ b/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs @@ -39,13 +39,8 @@ public WebGPUDevice(RendererBackend backend) /// public void Initialize(IWindow window) { - Instance = WebGPU.CreateInstance(); - if (Instance == null) - throw new Exception("Failed to create WebGPU instance."); - - Surface = CreateSurface(window.Native, Instance); - if (Surface == null) - throw new Exception("Failed to create surface for window."); + Instance = WebGPU.CreateInstance() ?? throw new Exception("Failed to create WebGPU instance."); + Surface = CreateSurface(window.Native, Instance) ?? throw new Exception("Failed to create surface for window."); BackendType backendType = BackendType.Undefined; switch (backend) From 7ae00a221f3cfbd07096fe0927fa3b7dc3bcfd88 Mon Sep 17 00:00:00 2001 From: Tweety Date: Mon, 19 Oct 2026 20:44:24 +1100 Subject: [PATCH 4/7] Get bare wgpu functionality. --- Beryl.RHI/GraphicsDeviceFactory.cs | 7 +- .../WebGPUBackend/WebGPUCommandBuffer.cs | 296 ++++++++++++++++++ Beryl.RHI/WebGPUBackend/WebGPUDevice.cs | 222 +++++++++---- .../WebGPUBackend/WebGPUResourceFactory.cs | 207 +++++++++++- Beryl.RHI/WebGPUBackend/WebGPUResources.cs | 88 +++++- Modules/Beryl.Rendering/Resources/Shader.cs | 6 +- Players/Beryl.Players.Desktop/Program.cs | 6 - 7 files changed, 746 insertions(+), 86 deletions(-) create mode 100644 Beryl.RHI/WebGPUBackend/WebGPUCommandBuffer.cs diff --git a/Beryl.RHI/GraphicsDeviceFactory.cs b/Beryl.RHI/GraphicsDeviceFactory.cs index 02c2478..387f884 100644 --- a/Beryl.RHI/GraphicsDeviceFactory.cs +++ b/Beryl.RHI/GraphicsDeviceFactory.cs @@ -9,12 +9,7 @@ namespace Beryl.RHI; public static class GraphicsDeviceFactory { /// Determines whether is supported. - public static bool IsBackendSupported(RendererBackend backend) => backend switch - { - RendererBackend.Vulkan => Veldrith.GraphicsDevice.IsBackendSupported(Veldrith.GraphicsBackend.Vulkan), - RendererBackend.Direct3D12 => Veldrith.GraphicsDevice.IsBackendSupported(Veldrith.GraphicsBackend.Direct3D12), - _ => false - }; + public static bool IsBackendSupported(RendererBackend backend) => WebGPUBackend.WebGPUDevice.IsBackendSupported(backend); /// Creates a graphics device for . public static IGraphicsDevice Create(RendererBackend backend) => new WebGPUBackend.WebGPUDevice(backend); diff --git a/Beryl.RHI/WebGPUBackend/WebGPUCommandBuffer.cs b/Beryl.RHI/WebGPUBackend/WebGPUCommandBuffer.cs new file mode 100644 index 0000000..606ab4f --- /dev/null +++ b/Beryl.RHI/WebGPUBackend/WebGPUCommandBuffer.cs @@ -0,0 +1,296 @@ +// 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 = []; + + /// + 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(); + 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."); + + 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 staging = device.Device.CreateBuffer(new WebGpuSharp.BufferDescriptor + { + Size = (ulong)data.Length, + Usage = WebGpuSharp.BufferUsage.CopySrc, + MappedAtCreation = true + }); + + staging.GetMappedRange>(0, (nuint)data.Length, static (mapped, source) => source.CopyTo(mapped), data); + staging.Unmap(); + encoder.CopyBufferToBuffer(staging, 0, destination.Resource, offset, (ulong)data.Length); + } + + /// + 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(); + recording = false; + finished = false; + disposed = true; + } +} diff --git a/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs b/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs index 0956472..5798896 100644 --- a/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs +++ b/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs @@ -1,12 +1,14 @@ // 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; @@ -15,111 +17,221 @@ 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 { get; private set; } + public VulkanInfo? VulkanInfo => null; /// - public DeviceFeatures Features { get; } = DeviceFeatures.None; + public DeviceFeatures Features => DeviceFeatures.None; /// - public IFramebuffer SwapchainFramebuffer => throw new NotImplementedException(); + 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) { - ResourceFactory = new WebGPUResourceFactory(this); this.backend = backend; + ResourceFactory = new WebGPUResourceFactory(this); + SwapchainFramebuffer = new WebGPUFramebuffer(this); } - /// - public void Initialize(IWindow window) + internal static bool IsBackendSupported(RendererBackend backend) { - Instance = WebGPU.CreateInstance() ?? throw new Exception("Failed to create WebGPU instance."); - Surface = CreateSurface(window.Native, Instance) ?? throw new Exception("Failed to create surface for window."); + if (backend is not (RendererBackend.Vulkan or RendererBackend.Direct3D12)) + return false; - BackendType backendType = BackendType.Undefined; - switch (backend) + try { - case RendererBackend.Vulkan: - backendType = BackendType.Vulkan; - break; - case RendererBackend.Direct3D12: - backendType = BackendType.D3D12; - break; + var instance = WebGPU.CreateInstance(); + return instance?.RequestAdapterSync(new RequestAdapterOptions + { + CompatibleSurface = null!, + BackendType = backend == RendererBackend.Vulkan ? BackendType.Vulkan : BackendType.D3D12 + }) != null; } - - RequestAdapterOptions adapterOptions = new() + catch (Exception) { - CompatibleSurface = Surface, - PowerPreference = PowerPreference.HighPerformance, + return false; + } + } - BackendType = backendType - }; + /// + public void Initialize(IWindow window) + { + ObjectDisposedException.ThrowIf(disposed, this); + if (Instance != null) + throw new InvalidOperationException("The WebGPU device is already initialized."); - Instance.RequestAdapter(in adapterOptions, (adapterStatus, adapter, bytes) => + try { - if (adapterStatus != RequestAdapterStatus.Success || adapter == null) - throw new Exception("Failed to create WebGPU adapter."); + 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)) + } + }); - var info = adapter.GetInfo(); + BerylConsole.Log($"Found GPU - {adapter.GetInfo().Device}"); - BerylConsole.Log($"Found GPU - {info.Device}"); + Device = adapter.RequestDeviceSync(); + Queue = Device.GetQueue(); - adapter.RequestDevice((deviceStatus, device, bytes) => - { - if (deviceStatus != RequestDeviceStatus.Success || device == null) - throw new Exception("Failed to create WebGPU device."); + 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."); - Device = device; + 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; - Surface.Configure(new SurfaceConfiguration() - { - Device = Device, - Format = WebGpuSharp.TextureFormat.BGRA8Unorm, - Width = (uint)window.Size.X, - Height = (uint)window.Size.Y, - Usage = TextureUsage.RenderAttachment, - PresentMode = PresentMode.Immediate - }); - }); - }); + 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) { - if (Surface == null || Device == null) + ObjectDisposedException.ThrowIf(disposed, this); + if (Surface == null || Device == null || width == 0 || height == 0) return; - Surface.Configure(new SurfaceConfiguration() + ReleaseCurrentTexture(); + + depthTexture?.Destroy(); + this.width = width; + this.height = height; + + ConfigureSurface(); + + depthTexture = Device.CreateTexture(new WebGpuSharp.TextureDescriptor { - Device = Device, - Format = WebGpuSharp.TextureFormat.BGRA8Unorm, - Width = width, - Height = height, - Usage = TextureUsage.RenderAttachment, - PresentMode = PresentMode.Immediate + 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) => throw new NotImplementedException(); + public void SubmitCommands(ICommandBuffer buffer) => ((WebGPUCommandBuffer)buffer).Submit(); /// - public void SwapBuffers() => Surface?.Present(); + public void SwapBuffers() + { + if (currentTexture == null) + return; + + Surface.Present(); + ReleaseCurrentTexture(); + } /// - public void UpdateBuffer(IBuffer buffer, uint offset, ReadOnlySpan data) where T : unmanaged => throw new NotImplementedException(); + 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) @@ -130,7 +242,7 @@ public void Dispose() { var winDesc = new SurfaceSourceWindowsHWNDFFI { - Chain = new ChainedStruct { SType = SType.SurfaceSourceWindowsHWND }, + Chain = new ChainedStruct { SType = SType.SurfaceSourceWindowsHWND }, Hinstance = (void*)win32.HInstance, Hwnd = (void*)win32.Hwnd }; diff --git a/Beryl.RHI/WebGPUBackend/WebGPUResourceFactory.cs b/Beryl.RHI/WebGPUBackend/WebGPUResourceFactory.cs index a009a34..30b81fd 100644 --- a/Beryl.RHI/WebGPUBackend/WebGPUResourceFactory.cs +++ b/Beryl.RHI/WebGPUBackend/WebGPUResourceFactory.cs @@ -15,41 +15,218 @@ namespace Beryl.RHI.WebGPUBackend; internal sealed class WebGPUResourceFactory(WebGPUDevice device) : IResourceFactory { /// - public IBuffer CreateBuffer(BufferDescriptor key) => throw new NotImplementedException(); + public ICommandBuffer CreateCommandBuffer() => new WebGPUCommandBuffer(device); /// - public ICommandBuffer CreateCommandBuffer() => throw new NotImplementedException(); + 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 IComputePipeline CreateComputePipeline(ComputePipelineDescriptor key) => throw new NotImplementedException(); + public IShader CreateShader(ShaderDescriptor key) => new WebGPUShader(device.Device.CreateShaderModuleWGSL(new ShaderModuleWGSLDescriptor { Code = key.Bytecode })); /// - public IPipeline CreateGraphicsPipeline(PipelineDescriptor key) => throw new NotImplementedException(); + 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 IResourceLayout CreateResourceLayout(ResourceLayoutDescriptor key) => throw new NotImplementedException(); + public ITextureView CreateTextureView(TextureViewDescriptor key) => new WebGPUTextureView(((WebGPUTexture)key.Texture).Resource.CreateView()); /// - public IResourceSet CreateResourceSet(ResourceDescriptor key) => throw new NotImplementedException(); + 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 ISampler CreateSampler(SamplerDescriptor key) => throw new NotImplementedException(); + 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 IShader CreateShader(ShaderDescriptor key) + public IResourceSet CreateResourceSet(ResourceDescriptor key) { - ShaderModuleWGSLDescriptor descriptor = new() + 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++) { - Code = key.Bytecode - }; + var buffer = (WebGPUBuffer)key.BoundResources[i]; + entries[i] = new() { Binding = (uint)i, Buffer = buffer.Resource, Size = buffer.SizeInBytes }; + } - ShaderModule module = device.Device.CreateShaderModuleWGSL(in descriptor); - return new WebGPUShader(module); + return new WebGPUResourceSet(device.Device.CreateBindGroup(new BindGroupDescriptor { Layout = layout.Resource, Entries = entries })); } /// - public ITexture CreateTexture(TextureDescriptor key) => throw new NotImplementedException(); + 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 ITextureView CreateTextureView(TextureViewDescriptor key) => throw new NotImplementedException(); + 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 index 9b98237..177b73d 100644 --- a/Beryl.RHI/WebGPUBackend/WebGPUResources.cs +++ b/Beryl.RHI/WebGPUBackend/WebGPUResources.cs @@ -6,13 +6,99 @@ namespace Beryl.RHI.WebGPUBackend; -internal sealed class WebGPUShader(ShaderModule shader) : IShader +// 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/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/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 += () => From 8eb7fd781cad026c3bb46e7ecf29b5a42d973a6c Mon Sep 17 00:00:00 2001 From: Tweety Date: Mon, 19 Oct 2026 21:00:48 +1100 Subject: [PATCH 5/7] Cache webgpu upload buffers. --- .../WebGPUBackend/WebGPUCommandBuffer.cs | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/Beryl.RHI/WebGPUBackend/WebGPUCommandBuffer.cs b/Beryl.RHI/WebGPUBackend/WebGPUCommandBuffer.cs index 606ab4f..1e6207b 100644 --- a/Beryl.RHI/WebGPUBackend/WebGPUCommandBuffer.cs +++ b/Beryl.RHI/WebGPUBackend/WebGPUCommandBuffer.cs @@ -24,6 +24,8 @@ internal sealed class WebGPUCommandBuffer(WebGPUDevice device) : ICommandBuffer private readonly Dictionary vertexBuffers = []; private readonly Dictionary graphicsSets = []; private readonly Dictionary computeSets = []; + private readonly List uploadBuffers = []; + private int uploadBufferIndex; /// public IFramebuffer? CurrentFramebuffer => target; @@ -44,6 +46,11 @@ public void Begin() vertexBuffers.Clear(); graphicsSets.Clear(); computeSets.Clear(); + uploadBufferIndex = 0; + + foreach (var upload in uploadBuffers) + upload.Length = 0; + encoder = device.Device.CreateCommandEncoder(); recording = true; } @@ -53,6 +60,7 @@ public void End() { EnsureRecording(); EndPass(); + commands = encoder.Finish(); encoder = default; recording = false; @@ -65,6 +73,12 @@ internal void Submit() 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; @@ -150,16 +164,32 @@ public void UpdateBuffer(IBuffer buffer, uint offset, ReadOnlySpan data) return; EndPass(); - var staging = device.Device.CreateBuffer(new WebGpuSharp.BufferDescriptor + 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) { - Size = (ulong)data.Length, - Usage = WebGpuSharp.BufferUsage.CopySrc, - MappedAtCreation = true - }); + var upload = uploadBuffers[uploadBufferIndex]; + if (upload.Data.Length - upload.Length >= length) + return upload; + uploadBufferIndex++; + } - staging.GetMappedRange>(0, (nuint)data.Length, static (mapped, source) => source.CopyTo(mapped), data); - staging.Unmap(); - encoder.CopyBufferToBuffer(staging, 0, destination.Resource, offset, (ulong)data.Length); + 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; } /// @@ -289,8 +319,20 @@ public void Dispose() 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; } + } } From 6333362834d5232a4c6af84609c936b33ab10d22 Mon Sep 17 00:00:00 2001 From: Tweety Date: Mon, 19 Oct 2026 21:17:02 +1100 Subject: [PATCH 6/7] Add proper `GraphicsDeviceFactory.IsBackendSupported`. --- Beryl.RHI/GraphicsBackendSupport.cs | 115 ++++++++++++++++++++++++ Beryl.RHI/GraphicsDeviceFactory.cs | 19 +++- Beryl.RHI/WebGPUBackend/WebGPUDevice.cs | 20 ----- 3 files changed, 132 insertions(+), 22 deletions(-) create mode 100644 Beryl.RHI/GraphicsBackendSupport.cs 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 387f884..f2a6804 100644 --- a/Beryl.RHI/GraphicsDeviceFactory.cs +++ b/Beryl.RHI/GraphicsDeviceFactory.cs @@ -8,8 +8,23 @@ namespace Beryl.RHI; /// public static class GraphicsDeviceFactory { - /// Determines whether is supported. - public static bool IsBackendSupported(RendererBackend backend) => WebGPUBackend.WebGPUDevice.IsBackendSupported(backend); + /// Checks platform, runtime and driver support for . Window presentation is validated during device initialization. + public static bool IsBackendSupported(RendererBackend backend) + { + 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 WebGPUBackend.WebGPUDevice(backend); diff --git a/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs b/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs index 5798896..1c8bff7 100644 --- a/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs +++ b/Beryl.RHI/WebGPUBackend/WebGPUDevice.cs @@ -49,26 +49,6 @@ public WebGPUDevice(RendererBackend backend) SwapchainFramebuffer = new WebGPUFramebuffer(this); } - internal static bool IsBackendSupported(RendererBackend backend) - { - if (backend is not (RendererBackend.Vulkan or RendererBackend.Direct3D12)) - return false; - - try - { - var instance = WebGPU.CreateInstance(); - return instance?.RequestAdapterSync(new RequestAdapterOptions - { - CompatibleSurface = null!, - BackendType = backend == RendererBackend.Vulkan ? BackendType.Vulkan : BackendType.D3D12 - }) != null; - } - catch (Exception) - { - return false; - } - } - /// public void Initialize(IWindow window) { From c6bbcca849dd3653d49aa6c5dd8e2cbb6dae3433 Mon Sep 17 00:00:00 2001 From: Tweety Date: Mon, 19 Oct 2026 21:20:04 +1100 Subject: [PATCH 7/7] Remove Veldrith backend. --- Beryl.RHI/Beryl.RHI.csproj | 5 +- .../VeldrithBackend/VeldrithCommandBuffer.cs | 73 ------ Beryl.RHI/VeldrithBackend/VeldrithDevice.cs | 197 ----------------- .../VeldrithResourceFactory.cs | 209 ------------------ .../VeldrithBackend/VeldrithResources.cs | 83 ------- 5 files changed, 3 insertions(+), 564 deletions(-) delete mode 100644 Beryl.RHI/VeldrithBackend/VeldrithCommandBuffer.cs delete mode 100644 Beryl.RHI/VeldrithBackend/VeldrithDevice.cs delete mode 100644 Beryl.RHI/VeldrithBackend/VeldrithResourceFactory.cs delete mode 100644 Beryl.RHI/VeldrithBackend/VeldrithResources.cs diff --git a/Beryl.RHI/Beryl.RHI.csproj b/Beryl.RHI/Beryl.RHI.csproj index 5b5a5e4..319dcb2 100644 --- a/Beryl.RHI/Beryl.RHI.csproj +++ b/Beryl.RHI/Beryl.RHI.csproj @@ -9,8 +9,9 @@ - - + + + 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(); -}